Conflicts:
	src/main/java/com/budwk/app/zhgh/democratic/teachercongress/institution/controller/TeacherCongressInstitutionController.java
	src/main/resources/views/platform/zhgh/democratic/teachercongress/institution/basicTable.js
	src/main/resources/views/platform/zhgh/democratic/teachercongress/institution/index.html
This commit is contained in:
zhouhefeng
2026-07-15 11:49:49 +08:00
204 changed files with 5657 additions and 1459 deletions
@@ -8,18 +8,25 @@ import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter;
import org.nutz.dao.util.Daos;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -29,11 +36,23 @@ import java.util.stream.Collectors;
@Api(tags = "用户审批意见")
public class SysUserApprovalOpinionController {
/**
* 系统内置的常用审批意见,统一提供给所有使用审核意见组件的页面。
*/
private static final List<String> DEFAULT_APPROVAL_OPINIONS = List.of("同意", "不同意");
@Inject
private Dao dao;
/**
* 保存当前用户的自定义审批意见。
*
* @param opinions 前端提交的意见数组,每项包含 id 和 text;系统内置的“同意”“不同意”不会写入用户数据
* @return Result,成功时不返回额外数据,失败时 msg 说明校验结果
*/
@At
@SaCheckLogin
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("保存审批意见")
public Result update(@Param("opinions")JSONObject[] opinions){
boolean hasBlank = Arrays.stream(opinions).anyMatch(v -> StrUtil.isBlank(v.getStr("text").trim()));
@@ -41,35 +60,68 @@ public class SysUserApprovalOpinionController {
return Result.error("内容不能为空,请删除后再提交!");
}
Set<String> opinionTexts = Arrays.stream(opinions).map(v -> v.getStr("text").trim()).collect(Collectors.toSet());
if(opinionTexts.size() != opinions.length){
// 系统默认意见只参与页面展示,不占用用户自定义意见数量,也不写入用户表。
JSONObject[] customOpinions = Arrays.stream(opinions)
.filter(v -> !DEFAULT_APPROVAL_OPINIONS.contains(v.getStr("text").trim()))
.toArray(JSONObject[]::new);
Set<String> opinionTexts = Arrays.stream(customOpinions).map(v -> v.getStr("text").trim()).collect(Collectors.toSet());
if(opinionTexts.size() != customOpinions.length){
return Result.error("内容重复,请删除后再提交!");
}
boolean text = Arrays.stream(opinions).anyMatch(v -> v.getStr("text").trim().length() > 50);
boolean text = Arrays.stream(customOpinions).anyMatch(v -> v.getStr("text").trim().length() > 50);
if(text){
return Result.error("单条内容不能超过50个字!");
}
if(opinions.length > 5){
if(customOpinions.length > 5){
return Result.error("最多可添加10条审批意见!");
}
for (int i = 0; i < opinions.length; i++) {
opinions[i].set("id",i+1);
for (int i = 0; i < customOpinions.length; i++) {
customOpinions[i].set("id",i+1);
}
dao.update(Sys_user.class, Chain.make("customApprovalOpinions", Arrays.asList(opinions)), Cnd.where(Sys_user::getId,"=", SecurityUtil.getUserId()));
dao.update(Sys_user.class, Chain.make("customApprovalOpinions", Arrays.asList(customOpinions)), Cnd.where(Sys_user::getId,"=", SecurityUtil.getUserId()));
return Result.success();
}
/**
* 获取当前用户可选的审批意见。
*
* @return Resultdata 为意见数组,每项包含 id 和 text;系统默认意见排在用户自定义意见之前
*/
@At
@SaCheckLogin
@ApiOperation("获取审批意见")
public Result list(){
FieldFilter fieldFilter = FieldFilter.create(Sys_user.class, "^customApprovalOpinions");
Sys_user sysUser = Daos.ext(dao, fieldFilter).fetch(Sys_user.class, Cnd.where(Sys_user::getId, "=", SecurityUtil.getUserId()));
return Result.success(sysUser.getCustomApprovalOpinions());
List<JSONObject> opinions = new ArrayList<>();
Set<String> opinionTexts = new HashSet<>();
// 默认意见始终置顶,并通过文本去重,避免与用户历史自定义意见重复。
for (String defaultOpinion : DEFAULT_APPROVAL_OPINIONS) {
JSONObject opinion = new JSONObject();
opinion.set("id", opinions.size() + 1);
opinion.set("text", defaultOpinion);
opinions.add(opinion);
opinionTexts.add(defaultOpinion);
}
List<JSONObject> customOpinions = Optional.ofNullable(sysUser.getCustomApprovalOpinions()).orElseGet(Collections::emptyList);
for (JSONObject customOpinion : customOpinions) {
String text = customOpinion.getStr("text");
if (StrUtil.isBlank(text) || !opinionTexts.add(text.trim())) {
continue;
}
JSONObject opinion = new JSONObject();
opinion.set("id", opinions.size() + 1);
opinion.set("text", text);
opinions.add(opinion);
}
return Result.success(opinions);
}
}
@@ -266,6 +266,11 @@ public class Sys_user extends BaseModel implements Serializable {
@DataCenterColumn(name = "单位", key = "SZDWH")
private String unitId;
@Column
@Comment("三级单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String threeUnitId;
@Column
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitPath;
@@ -2,7 +2,6 @@ package com.budwk.app.sys.services.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
@@ -25,7 +24,6 @@ import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
@@ -52,7 +50,6 @@ import java.util.stream.Collectors;
@IocBean
public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService {
private static final int BATCH_SIZE = 500;
private static final int ROLE_DELETE_BATCH_SIZE = 50;
@Inject
private Dao dao;
@@ -114,34 +111,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return fieldMappings;
}
/**
* 检查用户是否符合会员条件
*
* @param userState 用户状态
* @param preparedBy 编制类别
* @param postDoctoralJoinDate 博士后进站时间
* @return 是否符合会员条件
*/
private boolean checkMembershipEligibility(String userState, String preparedBy, Date postDoctoralJoinDate) {
// 博士后单独判断:只要进站时间在两年内就是会员
if ("博士后".equals(preparedBy)) {
if (postDoctoralJoinDate != null) {
Date twoYearsAgo = DateUtil.offset(DateUtil.date(), DateField.YEAR, -2).toJdkDate();
return postDoctoralJoinDate.after(twoYearsAgo);
}
return false;
}
// 其他人员需要判断在岗状态和聘用方式
if (!"在岗".equals(userState)) {
return false;
}
// 判断聘用方式
Set<String> memberPreparedByTypes = new HashSet<>(Arrays.asList("新人事代理", "校聘合同制", "事业编制"));
return memberPreparedByTypes.contains(preparedBy);
}
/**
* 全量更新用户数据
*
@@ -156,7 +125,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
// 获取角色信息
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
Sys_role memberRole = sysRoleService.getByCode(RoleConstant.MEMBER);
// 获取单位信息,后续判断单位变更
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1"));
@@ -181,23 +149,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
List<Sys_user> needDoUpdateList = new CopyOnWriteArrayList<>();
List<Sys_user> needInitUserList = new CopyOnWriteArrayList<>();
List<Sys_user_history> histories = new CopyOnWriteArrayList<>();
List<String> addMemberUserIds = Collections.synchronizedList(new ArrayList<>());
List<String> removeMemberUserIds = Collections.synchronizedList(new ArrayList<>());
Set<String> sourceLoginNames = sources.stream()
.map(Sys_user_source::getLoginname)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
List<String> leaveUserIds = sysUsers.stream()
.filter(user -> StrUtil.isNotBlank(user.getLoginname()))
.filter(user -> !sourceLoginNames.contains(user.getLoginname()))
.map(Sys_user::getId)
.toList();
List<String> leaveMemberUserIds = sysUsers.stream()
.filter(user -> StrUtil.isNotBlank(user.getLoginname()))
.filter(user -> !sourceLoginNames.contains(user.getLoginname()))
.filter(user -> Boolean.TRUE.equals(user.getMember()))
.map(Sys_user::getId)
.toList();
// 处理每条数据
for (Sys_user_source source : sources) {
@@ -214,22 +165,15 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
u.setSalt(salt);
u.setPassword(PwdUtil.getPassword(PwdUtil.generate(12), salt));
/* if (u.getMember()) {
addMemberUserIds.add(u.getId());
}*/
needInitUserList.add(u);
} else {
// 修改现有用户
u.setId(user.getId());
// 更新会员状态
boolean currentIsMember = user.getMember() != null && user.getMember();
if (!currentIsMember) {
// addMemberUserIds.add(user.getId());
} else{
// removeMemberUserIds.add(user.getId());
}
/*
* aidFundMember 由基金会员业务维护,数据中心源表默认值为 0。
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
*/
u.setAidFundMember(null);
needDoUpdateList.add(u);
}
@@ -312,45 +256,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
updateTasks.add(updateTask);
}
if (Lang.isNotEmpty(leaveUserIds)) {
CompletableFuture<Void> leaveTask = CompletableFuture.runAsync(() -> {
log.info("数据源缺失用户转为不在岗并取消会员: {} 个", leaveUserIds.size());
List<List<String>> batches = ListUtil.split(leaveUserIds, BATCH_SIZE);
batches.forEach(batch -> {
try {
/*
* 本次拉取批次中没有出现、但系统用户表仍存在的人员,按离岗处理:
* 1. userState 写为“不在岗”,用于后续人员状态筛选和业务判断;
* 2. member 写为 false,避免仍按会员身份参与后续业务判断。
*/
dao.update(Sys_user.class, Chain.make("userState", "不在岗").add("member", false), Cnd.where("id", "in", batch));
} catch (Exception e) {
log.error("批量更新数据源缺失用户状态异常", e);
}
});
}, executorService);
updateTasks.add(leaveTask);
}
if (Lang.isNotEmpty(leaveMemberUserIds)) {
CompletableFuture<Void> leaveMemberRoleTask = CompletableFuture.runAsync(() -> {
log.info("数据源缺失会员移除会员角色: {} 个", leaveMemberUserIds.size());
List<List<String>> batches = ListUtil.split(leaveMemberUserIds, ROLE_DELETE_BATCH_SIZE);
batches.forEach(batch -> {
try {
/*
* 只对原本是会员的缺失人员移除会员角色,并缩小删除批次,
* 降低 sys_user_role 大批量 DELETE 时的锁等待概率。
*/
dao.clear(Sys_user_role.class, Cnd.where("userId", "in", batch).and("roleId", "=", memberRole.getId()));
} catch (Exception e) {
log.error("批量移除数据源缺失会员角色异常", e);
}
});
}, executorService);
updateTasks.add(leaveMemberRoleTask);
}
if (Lang.isNotEmpty(histories)) {
CompletableFuture<Void> historyTask = CompletableFuture.runAsync(() -> {
log.info("添加历史记录: {} 条", histories.size());
@@ -444,8 +349,11 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
long endTime = System.currentTimeMillis();
log.info("全量更新用户数据完成,耗时: {} 毫秒", (endTime - startTime));
return "更新完成: 新增用户 " + needInitUserList.size() + " 个, 更新用户 " + needDoUpdateList.size()
+ " 个, 数据源缺失转不在岗 " + leaveUserIds.size() + " 个, 待添加会员 " + addMemberUserIds.size() + " 个, 待移除会员 " + removeMemberUserIds.size() + "";
if (needInitUserList.isEmpty() && histories.isEmpty()) {
return "本次没有可更新的数据。";
}
return "更新完成: 新增用户 " + needInitUserList.size() + " 个, 有效变更 " + histories.size()
+ "";
}
/**
@@ -479,6 +387,18 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
Object sourceValue = mapping.field.get(source);
Object userValue = mapping.field.get(user);
/*
* 全量更新使用 updateIgnoreNull,数据源空值不会覆盖系统已有值。
* 因此当新值为空、旧值有值时不生成历史记录,避免每次更新都重复记录“有值→空”。
* 同时将 null、空字符串、纯空格统一视为业务空值,避免页面显示为空但底层值不同导致“空→空”被记录。
*/
if (isBlankChangeValue(sourceValue) && isBlankChangeValue(userValue)) {
continue;
}
if (isBlankChangeValue(sourceValue) && !isBlankChangeValue(userValue)) {
continue;
}
// 如果值不相等,记录变更
if (!ObjectUtil.equals(sourceValue, userValue)) {
NutMap change = NutMap.NEW();
@@ -543,6 +463,20 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return history;
}
/**
* 判断历史变更字段是否为业务空值。
* null、空字符串、纯空格在页面上都会展示为空,统一视为无有效新值,避免生成无意义的空值变更记录。
*/
private boolean isBlankChangeValue(Object value) {
if (value == null) {
return true;
}
if (value instanceof CharSequence) {
return StrUtil.isBlank(value.toString());
}
return ObjectUtil.isEmpty(value);
}
/**
* 获取单位变更记录展示值。
* 入参 unitId 为系统用户旧单位或数据源新单位ID;返回值优先使用单位名称,查不到名称时保留单位ID,避免变更记录为空导致更新中断。
@@ -2,6 +2,7 @@ package com.budwk.app.sys.services.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.AsyncUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
@@ -15,18 +16,18 @@ import com.budwk.app.sys.param.SysDataUserUpdateParam;
import com.budwk.app.sys.services.SysDataUserUpdateService;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import java.util.List;
import java.util.Arrays;
import java.util.Date;
import java.util.Calendar;
import java.util.concurrent.CompletableFuture;
/**
@@ -41,6 +42,7 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
private SysRoleService sysRoleService;
@Override
@Aop(TransAop.READ_COMMITTED)
public String update(SysDataUserUpdateParam updateParam) {
// 基本SQL查询
Cnd cnd = Cnd.where("pullTime", "=", updateParam.getPullTime())
@@ -67,34 +69,6 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
String pwd = PwdUtil.generate(12);
sysUser.setPassword(PwdUtil.getPassword(pwd, salt));
sysUser.setLoginCount(0);
// 检查是否符合会员条件
// 1. 用户状态为"在岗"
if ("在岗".equals(sysUser.getUserState())) {
// 2. 检查聘用方式条件
List<String> membershipQualifyingPreparedBy = Arrays.asList("新人事代理", "校聘合同制", "事业编制");
// 第一种情况:聘用方式在指定列表中
boolean isQualifiedByPreparedBy = membershipQualifyingPreparedBy.contains(sysUser.getPreparedBy());
// 第二种情况:聘用方式是博士后,且进站时间不超过两年
boolean isQualifiedPostdoc = false;
if ("博士后".equals(sysUser.getPreparedBy()) && sysUser.getPostDoctoralJoinDate() != null) {
Calendar twoYearsAgo = Calendar.getInstance();
twoYearsAgo.add(Calendar.YEAR, -2);
Date twoYearsAgoDate = twoYearsAgo.getTime();
isQualifiedPostdoc = sysUser.getPostDoctoralJoinDate().after(twoYearsAgoDate);
}
// 如果满足任一条件,设置为会员
if (isQualifiedByPreparedBy || isQualifiedPostdoc) {
sysUser.setMember(true);
} else {
sysUser.setMember(false);
}
} else {
sysUser.setMember(false);
}
}
List<List<Sys_user>> splitSysUsers = ListUtil.split(sysUsers, 500);
@@ -102,6 +76,18 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
//等待任务全部完成
AsyncUtil.waitAll(CompletableFuture.allOf(splitSysUsersFutures.toArray(new CompletableFuture[0])));
// 增量更新只处理新增人员,需要同步写入用户历史记录;列表的变更类型、变更时间通过 sys_user_history 最新记录展示。
List<Sys_user_history> histories = BeanUtil.copyToList(userSources, Sys_user_history.class);
for (Sys_user_history history : histories) {
history.setId(R.UU32());
history.setChangeTypes(List.of(MemberChangeType.NEW.name()));
history.setChangeTime(DateUtil.date());
history.setChangeOrigin(MemberChangeOrigin.SYSTEM.name());
}
List<List<Sys_user_history>> splitHistories = ListUtil.split(histories, 500);
List<CompletableFuture<List<Sys_user_history>>> splitHistoryFutures = splitHistories.stream().map(v -> CompletableFuture.supplyAsync(() -> dao.fastInsert(v))).toList();
AsyncUtil.waitAll(CompletableFuture.allOf(splitHistoryFutures.toArray(new CompletableFuture[0])));
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
//添加公共角色
@@ -143,15 +129,15 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
dao.insert(insertUnits);
});
// 计算设置为会员的人数
long memberCount = sysUsers.stream().filter(Sys_user::getMember).count();
// 6.1 更新提案的校领导角色,发送订阅
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
for (String unitId : config.getSchoolLeaderUnitIds()) {
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
}
return StrUtil.format("增量更新成功,本次更新新入职老师{}人,新增单位{}个,设置为会员{}人。", sysUsers.size(), insertUnits.size(), memberCount);
if (sysUsers.isEmpty() && insertUnits.isEmpty()) {
return "本次没有新增人员或单位。";
}
return StrUtil.format("增量更新成功,本次新增人员{}人,新增单位{}个。", sysUsers.size(), insertUnits.size());
}
}
@@ -425,6 +425,7 @@ public class ClubStatisticsController {
}
private Sql generateSql(String clubId, Boolean auditState, String userState, String sex, Boolean giveMoney) {
// 注册流程按社团去重后再关联,避免换届等流程复用社团ID时将会员人数重复统计。
Sql sql = Sqls.create("""
SELECT
club.id,
@@ -438,11 +439,22 @@ public class ClubStatisticsController {
sum( CASE WHEN ('CLUB_PRESIDENT' MEMBER OF (cu.roleCode) or 'CLUB_VICE_PRESIDENT' MEMBER OF (cu.roleCode) or 'CLUB_SECRETARY' MEMBER OF (cu.roleCode) or 'CLUB_VICE_SECRETARY' MEMBER OF (cu.roleCode)) AND 1 = 1 $myCondition THEN 1 ELSE 0 END ) AS governing_body
FROM
sys_club club
INNER JOIN (
SELECT DISTINCT registerInstance.businessNo
FROM wf_process_instance registerInstance
INNER JOIN wf_process_define registerDefine
ON registerDefine.id = registerInstance.processDefineId
WHERE registerInstance.state = @finishedState
AND registerInstance.delFlag = 0
AND registerDefine.name = @registerProcessKey
AND registerDefine.delFlag = 0
) registeredClub ON registeredClub.businessNo = club.id
LEFT JOIN club_user cu ON cu.clubId = club.id
LEFT JOIN sys_user su ON cu.userId = su.id
LEFT JOIN wf_process_instance ins ON ins.businessNo = club.id
$condition
""");
sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode());
sql.setParam("registerProcessKey", "XHZC");
Cnd cnd = Cnd.NEW();
if (auditState != null) {
List<SysClubExamineRegister> examineRegisters = sysClubService.dao().query(SysClubExamineRegister.class, Cnd.where("year(registerDate)", "=", DateUtil.thisYear()));
@@ -458,7 +470,6 @@ public class ClubStatisticsController {
List<String> myClubId = userRoles.stream().map(Sys_user_role::getClubId).toList();
cnd.and("club.id", "in", myClubId);
}
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("club.id", "=", clubId);
cnd.and("club.dismiss", "=", false);
cnd.groupBy("club.id");
@@ -13,6 +13,13 @@ import java.util.List;
public interface SysClubExamineService extends BaseService<SysClubExamineRegister> {
/**
* 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。
*
* @param clubId 社团ID
* @param year 统计年度
* @return 在职和退休人员的年初、增加、减少、年末人数
*/
List<NutMap> getClubUserNum(@Valid String clubId, Integer year);
List<NutMap> getJgUser(@Valid String clubId);
@@ -10,7 +10,6 @@ import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.club.model.SysClub;
@@ -32,14 +31,15 @@ import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineRegister> implements SysClubExamineService {
@Inject
private SysDictService sysDictService;
@Inject
private SysClubService sysClubService;
@@ -49,128 +49,244 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl<SysClubExamineReg
@Override
public List<NutMap> getClubUserNum(String clubId, Integer year) {
// 定义统计的用户状态类型
List<String> userStateList = List.of("在职", "退休");
List<NutMap> result = new ArrayList<>();
Date yearStart = DateUtil.parse(year + "-01-01 00:00:00");
Date nextYearStart = DateUtil.parse((year + 1) + "-01-01 00:00:00");
List<MembershipPeriod> membershipPeriods = getMembershipPeriods(clubId);
NutMap activeResult = createUserChangeResult("在职");
NutMap retiredResult = createUserChangeResult("退休");
// 查询上一年度考核登记数据(用于获取年初人数)
SysClubExamineRegister examineRegister = dao().fetch(SysClubExamineRegister.class,
Cnd.where("year", "=", year - 1).and("clubId", "=", clubId));
for (MembershipPeriod period : membershipPeriods) {
// 非正常历史会员没有退会时间且没有已完成退会申请时,无法确定所属年度,不计入统计。
if (!period.normal && period.exitTime == null) {
continue;
}
if (isMemberAtYearBoundary(period, yearStart)) {
increaseResult(classifyAtBoundary(period, yearStart), activeResult, retiredResult, "yearFirstNum");
}
if (isInYear(period.joinTime, yearStart, nextYearStart)) {
increaseResult(classifyAtEvent(period, period.joinTime), activeResult, retiredResult, "yearAddNum");
}
if (isInYear(period.exitTime, yearStart, nextYearStart)) {
increaseResult(classifyBeforeEvent(period, period.exitTime), activeResult, retiredResult, "yearReduceNum");
}
// 退休发生在会员有效期内时,作为在职减少一人、退休增加一人处理。
if (isInYear(period.retireDate, yearStart, nextYearStart)
&& period.joinTime.before(period.retireDate)
&& (period.exitTime == null || period.exitTime.after(period.retireDate))) {
increaseResult("在职", activeResult, retiredResult, "yearReduceNum");
increaseResult("退休", activeResult, retiredResult, "yearAddNum");
}
if (isMemberAtYearBoundary(period, nextYearStart)) {
increaseResult(classifyAtBoundary(period, nextYearStart), activeResult, retiredResult, "thisYearNum");
}
}
return List.of(activeResult, retiredResult);
}
// 构建社团用户申请记录查询SQL(关联用户信息、流程实例)
Sql sql = Sqls.create("""
/**
* 合并旧系统会员生命周期与新系统已完成申请,生成可用于年度统计的入退会区间。
* 旧系统记录负责迁移历史,新系统申请只补充旧记录缺失的退会时间和迁移后新增的入会周期。
*/
private List<MembershipPeriod> getMembershipPeriods(String clubId) {
Sql legacySql = Sqls.create("""
SELECT
cu.*,
year(cu.applyDate) as applyYear,
cu.userId,
CAST(cu.joinTime AS DATETIME) joinTime,
STR_TO_DATE(NULLIF(cu.changeTime, ''), '%Y-%m-%d %H:%i:%s') exitTime,
cu.isNormal,
u.userState,
u.retireDate
FROM
club_user_apply cu
LEFT JOIN sys_user u ON cu.userId = u.id
LEFT JOIN wf_process_instance ins ON ins.businessNo = cu.id
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("cu.clubId", "=", clubId);
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.groupBy("cu.clubId, cu.userId, cu.mode");
sql.setCondition(cnd);
List<NutMap> userList = listMap(sql);
FROM sys_club_user cu
LEFT JOIN sys_user u ON u.id = cu.userId
WHERE cu.clubId = @clubId
AND cu.status = 5
AND cu.joinTime IS NOT NULL
AND cu.joinTime <> ''
""").setParam("clubId", clubId);
List<MembershipPeriod> periods = listMap(legacySql).stream()
.map(row -> new MembershipPeriod(
row.getString("userId"),
row.getTime("joinTime"),
row.getTime("exitTime"),
!Boolean.FALSE.equals(row.getBoolean("isNormal")),
true,
row.getString("userState"),
row.getTime("retireDate")
))
.filter(period -> period.joinTime != null)
.collect(Collectors.toCollection(ArrayList::new));
// 遍历在职/退休状态,分别统计人员变动数据
for (String userState : userStateList) {
NutMap nutMap = new NutMap();
nutMap.addv("userState", userState);
Sql applySql = Sqls.create("""
SELECT
cu.userId,
cu.mode,
cu.applyDate,
cu.joinTime,
cu.exitTime,
u.userState,
u.retireDate
FROM club_user_apply cu
LEFT JOIN sys_user u ON u.id = cu.userId
WHERE cu.clubId = @clubId
AND COALESCE(cu.delFlag, 0) = 0
AND EXISTS (
SELECT 1
FROM wf_process_instance ins
WHERE ins.businessNo = cu.id
AND ins.state = @finishedState
)
ORDER BY cu.applyDate, cu.mode
""")
.setParam("clubId", clubId)
.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode());
List<NutMap> applications = listMap(applySql);
applications.sort(Comparator.comparing(row -> row.getTime("applyDate"), Comparator.nullsLast(Date::compareTo)));
// 根据用户状态匹配对应的人员类型集合
List<String> personTypeList = switch (userState) {
case "在职" -> List.of("在职", "在岗");
case "退休" -> List.of("退休", "退休【变号】");
default -> new ArrayList<>();
};
// 过滤出符合当前人员类型的用户(非空判断避免NPE)
List<NutMap> personTypeUsers = userList.stream()
.filter(u -> StrUtil.isNotBlank(u.getString("userState"))
&& personTypeList.contains(u.getString("userState")))
.collect(Collectors.toList());
// 初始化年末人数为0
int thisYearNum = 0;
if (!Lang.isEmpty(personTypeUsers)) {
// 统计年末人数:加入社团且无退出记录的用户数量
List<String> exitUserIds = personTypeUsers.stream()
.filter(u -> u.getInt("mode") == 0)
.map(u -> u.getString("userId"))
.toList();
thisYearNum = (int) personTypeUsers.stream()
.filter(u -> u.getInt("mode") == 1)
.filter(u -> !exitUserIds.contains(u.getString("userId")))
.count();
for (NutMap application : applications) {
Date applyDate = application.getTime("applyDate");
Boolean mode = application.getBoolean("mode");
if (applyDate == null || mode == null) {
continue;
}
// 统计今年退休人数:已入会且退休日期在本年度的用户
int retireCount = (int) personTypeUsers.stream()
.filter(u -> u.getTime("retireDate") != null
&& u.getInt("mode") == 1
&& DateUtil.year(u.getTime("retireDate")) == year)
.count();
// 封装年初人数:从上一年度登记数据中获取,无数据则为0
if (Lang.isNotEmpty(examineRegister) && Lang.isNotEmpty(examineRegister.getChangeUserNum())) {
JSONObject uState = examineRegister.getChangeUserNum().stream()
.filter(c -> c.getStr("userState").equals(userState))
.findFirst()
.orElse(null);
nutMap.addv("yearFirstNum", Lang.isEmpty(uState) ? 0 : uState.getInt("thisYearNum"));
if (mode) {
mergeJoinApplication(periods, application, applyDate);
} else {
nutMap.addv("yearFirstNum", 0);
mergeExitApplication(periods, application, applyDate);
}
// 统计年度增加相关数据
int joinThisYear = (int) personTypeUsers.stream()
.filter(u -> u.getInt("mode") == 1 && u.getInt("applyYear") == year)
.count();
int thisYearJoinAndRetire = (int) userList.stream()
.filter(u -> u.getInt("mode") == 1 && u.getInt("applyYear") == year
&& u.getTime("retireDate") != null
&& DateUtil.year(u.getTime("retireDate")) == year)
.count();
int beforeYearJoinAndRetire = (int) userList.stream()
.filter(u -> u.getInt("mode") == 1 && u.getInt("applyYear") == year
&& u.getTime("retireDate") != null
&& DateUtil.year(u.getTime("retireDate")) < year)
.count();
// 按用户状态封装年度增加人数
nutMap.addv("yearAddNum", "在职".equals(userState)
? joinThisYear + thisYearJoinAndRetire
: beforeYearJoinAndRetire + retireCount);
// 统计年度减少相关数据
int quitThisYear = (int) personTypeUsers.stream()
.filter(u -> u.getInt("mode") == 0 && u.getInt("applyYear") == year)
.count();
// 在职人员年度减少:退休人数+主动退出人数;退休人员仅统计主动退出
if ("在职".equals(userState)) {
int retireThisYear = (int) userList.stream()
.filter(u -> u.getTime("retireDate") != null
&& DateUtil.year(u.getTime("retireDate")) == year)
.count();
nutMap.addv("yearReduceNum", retireThisYear + quitThisYear);
} else {
nutMap.addv("yearReduceNum", quitThisYear);
}
// 封装年末人数
nutMap.addv("thisYearNum", thisYearNum);
result.add(nutMap);
}
return result;
return periods;
}
/**
* 已迁移的入会申请与旧会员记录精确对应时不重复新增;迁移后的新申请以审核申请时间作为入会时间。
*/
private void mergeJoinApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
String userId = application.getString("userId");
Date applicationJoinTime = application.getTime("joinTime");
boolean representedByLegacy = periods.stream()
.filter(period -> period.legacy && Objects.equals(period.userId, userId))
.filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
.anyMatch(period -> period.exitTime == null || !applyDate.after(period.exitTime));
boolean representedByNewPeriod = periods.stream()
.filter(period -> !period.legacy && Objects.equals(period.userId, userId))
.anyMatch(period -> isMemberAt(period, applyDate));
if (representedByLegacy || representedByNewPeriod) {
return;
}
periods.add(new MembershipPeriod(
userId,
applyDate,
null,
true,
false,
application.getString("userState"),
application.getTime("retireDate")
));
}
/**
* 退会申请优先关闭入会时间相同的会员周期;新系统复入会后再退会时关闭最近的有效周期。
*/
private void mergeExitApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
String userId = application.getString("userId");
Date applicationJoinTime = application.getTime("joinTime");
Date exitTime = application.getTime("exitTime") == null ? applyDate : application.getTime("exitTime");
MembershipPeriod target = periods.stream()
.filter(period -> Objects.equals(period.userId, userId))
.filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
.filter(period -> period.exitTime == null)
.max(Comparator.comparing(period -> period.joinTime))
.orElse(null);
if (target == null) {
target = periods.stream()
.filter(period -> Objects.equals(period.userId, userId))
.filter(period -> period.exitTime == null && !period.joinTime.after(exitTime))
.max(Comparator.comparing(period -> period.joinTime))
.orElse(null);
}
if (target != null) {
target.exitTime = exitTime;
}
}
private NutMap createUserChangeResult(String userState) {
return NutMap.NEW()
.addv("userState", userState)
.addv("yearFirstNum", 0)
.addv("yearAddNum", 0)
.addv("yearReduceNum", 0)
.addv("thisYearNum", 0);
}
private void increaseResult(String userState, NutMap activeResult, NutMap retiredResult, String field) {
NutMap result = "退休".equals(userState) ? retiredResult : "在职".equals(userState) ? activeResult : null;
if (result != null) {
result.put(field, result.getInt(field) + 1);
}
}
private boolean isInYear(Date date, Date yearStart, Date nextYearStart) {
return date != null && !date.before(yearStart) && date.before(nextYearStart);
}
private boolean isMemberAt(MembershipPeriod period, Date date) {
return !period.joinTime.after(date) && (period.exitTime == null || period.exitTime.after(date));
}
private boolean isMemberAtYearBoundary(MembershipPeriod period, Date boundary) {
return period.joinTime.before(boundary) && (period.exitTime == null || !period.exitTime.before(boundary));
}
private String classifyAtBoundary(MembershipPeriod period, Date boundary) {
if (period.retireDate != null) {
return period.retireDate.before(boundary) ? "退休" : "在职";
}
return normalizeUserState(period.userState);
}
private String classifyAtEvent(MembershipPeriod period, Date eventTime) {
if (period.retireDate != null) {
return period.retireDate.after(eventTime) ? "在职" : "退休";
}
return normalizeUserState(period.userState);
}
private String classifyBeforeEvent(MembershipPeriod period, Date eventTime) {
if (period.retireDate != null) {
return period.retireDate.before(eventTime) ? "退休" : "在职";
}
return normalizeUserState(period.userState);
}
private String normalizeUserState(String userState) {
if (List.of("在职", "在岗").contains(userState)) {
return "在职";
}
if (List.of("退休", "退休【变号】").contains(userState)) {
return "退休";
}
return null;
}
private static class MembershipPeriod {
private final String userId;
private final Date joinTime;
private Date exitTime;
private final boolean normal;
private final boolean legacy;
private final String userState;
private final Date retireDate;
private MembershipPeriod(String userId, Date joinTime, Date exitTime, boolean normal, boolean legacy,
String userState, Date retireDate) {
this.userId = userId;
this.joinTime = joinTime;
this.exitTime = exitTime;
this.normal = normal;
this.legacy = legacy;
this.userState = userState;
this.retireDate = retireDate;
}
}
@Override
@@ -15,6 +15,7 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.web.controllers.open.commons.service.CommonService;
import com.budwk.app.zhgh.club.model.ClubUser;
import com.budwk.app.zhgh.club.model.SysClub;
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
import com.budwk.app.zhgh.club.service.SysClubInfoManageService;
import com.budwk.app.zhgh.club.service.SysClubService;
@@ -41,6 +42,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
@@ -227,19 +229,23 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
public Pagination<ClubUserCommonPageVo> exitManagePageData(ClubUserPageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
info.id,
info.clubId,
info.userId,
info.roleCode AS applyRoleCode,
info.clubPosition,
info.email,
info.mobile,
info.birthday,
info.avatar,
info.sameTimeJoinOtherClubSituation,
info.awardsExperience,
COALESCE(info.id, scu.id) AS id,
scu.clubId,
scu.userId,
COALESCE(info.roleCode, scu.roleCode) AS applyRoleCode,
COALESCE(info.clubPosition, scu.position) AS clubPosition,
COALESCE(info.email, scu.email, u.email) AS email,
COALESCE(info.mobile, u.mobile) AS mobile,
COALESCE(info.birthday, u.birthday) AS birthday,
COALESCE(info.avatar, scu.avatar, u.avatar) AS avatar,
COALESCE(info.sameTimeJoinOtherClubSituation, scu.sameTimeJoinOtherClubSituation) AS sameTimeJoinOtherClubSituation,
COALESCE(info.awardsExperience, scu.awardsExperience) AS awardsExperience,
info.signature,
info.applyDate,
COALESCE(
info.applyDate,
STR_TO_DATE(scu.changeTime, '%Y-%m-%d %H:%i:%s'),
STR_TO_DATE(JSON_UNQUOTE(JSON_EXTRACT(scu.exitAuditList, '$[0].auditTime')), '%Y-%m-%d %H:%i:%s')
) AS applyDate,
u.username AS userName,
u.loginname AS loginName,
u.sex,
@@ -247,24 +253,32 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
u.userState,
club.clubName,
u.unitname AS unitName,
DATE_FORMAT(IFNULL(info.joinTime, club.foundTime), '%Y-%m-%d %H:%i:%s') AS joinTime,
DATE_FORMAT(info.exitTime, '%Y-%m-%d %H:%i:%s') AS exitTime,
COALESCE(scu.joinTime, DATE_FORMAT(club.foundTime, '%Y-%m-%d %H:%i:%s')) AS joinTime,
COALESCE(
DATE_FORMAT(info.exitTime, '%Y-%m-%d %H:%i:%s'),
JSON_UNQUOTE(JSON_EXTRACT(scu.exitAuditList, '$[0].auditTime')),
scu.changeTime
) AS exitTime,
IF(info.id IS NULL, 'DIRECT_REMOVE', 'AUDIT_EXIT') AS exitType,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId
FROM
club_user_apply info
LEFT JOIN sys_club club ON club.id = info.clubId
LEFT JOIN vw_user u ON u.id = info.userId
sys_club_user scu
LEFT JOIN club_user_apply info ON info.clubId = scu.clubId
AND info.userId = scu.userId
AND info.mode = false
LEFT JOIN sys_club club ON club.id = scu.clubId
INNER JOIN vw_user u ON u.id = scu.userId
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
AND ins.state = 20
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("info.mode", "=", false);
cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode());
cnd.andEX("info.clubId", "=", pageForm.getClubId());
cnd.and("scu.isNormal", "=", false);
cnd.andEX("scu.clubId", "=", pageForm.getClubId());
cnd.andEX("u.personType", "=", pageForm.getPersonType());
cnd.andEX("u.userState", "=", pageForm.getUserState());
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
@@ -273,15 +287,17 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(), RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name())) {
List<String> clubIdList = commonService.findUserRoleByRoleCode(List.of(RoleConstant.CLUB_PRESIDENT.name(), RoleConstant.CLUB_VICE_PRESIDENT.name(), RoleConstant.CLUB_SECRETARY.name(), RoleConstant.CLUB_VICE_SECRETARY.name(), RoleConstant.CLUB_OPERATOR.name()));
List<Sys_user_role> clubList = dao().query(Sys_user_role.class, Cnd.where("roleId", "in", clubIdList).and("userId", "=", SecurityUtil.getUserId()).and("clubId", "is not", null));
cnd.and("info.clubId", "in", clubList.stream().map(Sys_user_role::getClubId).toList());
cnd.and("scu.clubId", "in", clubList.stream().map(Sys_user_role::getClubId).toList());
}
cnd.groupBy("info.id");
cnd.desc("info.applyDate");
cnd.groupBy("scu.id");
cnd.desc("exitTime");
sql.setCondition(cnd);
Pagination<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
List<ClubUserCommonPageVo> list = pagination.getList(ClubUserCommonPageVo.class);
for (ClubUserCommonPageVo vo : list) {
vo.setRoleName(SysClubUserServiceImpl.convertRoleName(List.of(vo.getApplyRoleCode())));
if (StrUtil.isNotBlank(vo.getApplyRoleCode())) {
vo.setRoleName(SysClubUserServiceImpl.convertRoleName(List.of(vo.getApplyRoleCode())));
}
}
pagination.setList(list);
return pagination;
@@ -291,7 +307,30 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
public ClubUserJoinVo exitManageInfo(String id) {
Sql sql = Sqls.create("""
SELECT
cua.*,
COALESCE(cua.id, scu.id) AS id,
scu.clubId,
scu.userId,
COALESCE(cua.roleCode, scu.roleCode) AS roleCode,
COALESCE(cua.clubPosition, scu.position) AS clubPosition,
COALESCE(cua.email, scu.email, u.email) AS email,
COALESCE(cua.mobile, u.mobile) AS mobile,
COALESCE(cua.birthday, u.birthday) AS birthday,
COALESCE(cua.avatar, scu.avatar, u.avatar) AS avatar,
COALESCE(cua.sameTimeJoinOtherClubSituation, scu.sameTimeJoinOtherClubSituation) AS sameTimeJoinOtherClubSituation,
COALESCE(cua.awardsExperience, scu.awardsExperience) AS awardsExperience,
cua.signature,
COALESCE(
cua.applyDate,
STR_TO_DATE(scu.changeTime, '%Y-%m-%d %H:%i:%s'),
STR_TO_DATE(JSON_UNQUOTE(JSON_EXTRACT(scu.exitAuditList, '$[0].auditTime')), '%Y-%m-%d %H:%i:%s')
) AS applyDate,
STR_TO_DATE(scu.joinTime, '%Y-%m-%d %H:%i:%s') AS joinTime,
COALESCE(
cua.exitTime,
STR_TO_DATE(JSON_UNQUOTE(JSON_EXTRACT(scu.exitAuditList, '$[0].auditTime')), '%Y-%m-%d %H:%i:%s'),
STR_TO_DATE(scu.changeTime, '%Y-%m-%d %H:%i:%s')
) AS exitTime,
IF(cua.id IS NULL, 'DIRECT_REMOVE', 'AUDIT_EXIT') AS exitType,
club.clubName,
u.loginname AS loginName,
u.username AS userName,
@@ -304,10 +343,14 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
u.academicDegree,
u.position
FROM
club_user_apply cua
LEFT JOIN vw_user u ON u.id = cua.userId
LEFT JOIN sys_club club ON club.id = cua.clubId
WHERE cua.id = @id
sys_club_user scu
LEFT JOIN club_user_apply cua ON cua.clubId = scu.clubId
AND cua.userId = scu.userId
AND cua.mode = false
LEFT JOIN vw_user u ON u.id = scu.userId
LEFT JOIN sys_club club ON club.id = scu.clubId
WHERE scu.isNormal = false
AND COALESCE(cua.id, scu.id) = @id
""");
sql.setParam("id", id);
return fetchVO(sql, ClubUserJoinVo.class);
@@ -346,7 +389,11 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
NutMap nutMap = NutMap.NEW();
nutMap.put("id", "0");
nutMap.put("clubName", Globals.AppName);
nutMap.put("children", sysClubService.getMyManageClub());
// 社团管理树只按社团编号展示,避免影响公共社团列表的原有排序。
List<SysClub> clubList = sysClubService.getMyManageClub().stream()
.sorted(Comparator.comparing(SysClub::getClubCode, Comparator.nullsLast(String::compareTo)))
.toList();
nutMap.put("children", clubList);
result.add(nutMap);
return result;
}
@@ -31,6 +31,7 @@ public class ClubUserCommonPageVo extends ClubUser {
private String unionId;
private String joinTime;
private String exitTime;
private String exitType;
private String applyRoleCode;
private String roleName;
@@ -23,4 +23,5 @@ public class ClubUserJoinVo extends ClubUserApply {
private String academicDegree;
private String position;
private String clubName;
private String exitType;
}
@@ -262,7 +262,7 @@ public class HonorManageController {
excelExportEntity.add(new ExcelExportEntity("分工会", "unionName", 20));
excelExportEntity.add(new ExcelExportEntity("单位", "unitName", 20));
excelExportEntity.add(new ExcelExportEntity("奖项", "prize", 20));
excelExportEntity.add(new ExcelExportEntity("荣誉", "honorLevel", 20));
excelExportEntity.add(new ExcelExportEntity("荣誉", "honorLevel", 20));
excelExportEntity.add(new ExcelExportEntity("授予单位", "grantUnit", 20));
excelExportEntity.add(new ExcelExportEntity("授予时间", "grantDate", 20));
excelExportEntity.add(new ExcelExportEntity("奖项介质", "awardType", 20));
@@ -276,7 +276,7 @@ public class HonorManageController {
excelExportEntity.add(new ExcelExportEntity("会员人数", "memberNumber", 10));
excelExportEntity.add(new ExcelExportEntity("工会小组数", "unionGroupNumber", 10));
excelExportEntity.add(new ExcelExportEntity("奖项", "prize", 20));
excelExportEntity.add(new ExcelExportEntity("荣誉", "honorLevel", 20));
excelExportEntity.add(new ExcelExportEntity("荣誉", "honorLevel", 20));
excelExportEntity.add(new ExcelExportEntity("授予单位", "grantUnit", 20));
excelExportEntity.add(new ExcelExportEntity("参加比赛名称", "gameName", 20));
excelExportEntity.add(new ExcelExportEntity("授予时间", "grantDate", 20));
@@ -209,7 +209,7 @@ public class TourLineController {
return Result.error("创建年度不能为空");
}
if (StrUtil.isBlank(line.getTravelAgencyId())) {
return Result.error("旅行社名称不能为空");
return Result.error("服务单位名称不能为空");
}
if (StrUtil.isBlank(line.getLineName())) {
return Result.error("线路名称不能为空");
@@ -466,7 +466,13 @@ public class TourMySignupController {
ledger.setSignupTime(defaultIfBlank(oldLedger.getSignupTime(), ledger.getSignupTime()));
fillStaffInfo(ledger);
tourLedgerService.updateIgnoreNull(ledger);
// 修改报名也重新锁定并校验当前分工会名额,兼容人员所属分工会发生变化的场景。
String quotaMessage = tourLedgerService.saveWithUnionSignupQuota(
ledger, matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), true);
if (StrUtil.isNotBlank(quotaMessage)) {
return Result.error(quotaMessage);
}
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
if (Lang.isNotEmpty(familyList)) {
@@ -661,6 +667,14 @@ public class TourMySignupController {
}
}
if (isInProvinceLine(line.getLineType())) {
// 修改报名时排除当前台账,再按实际参加记录校验省内线路间隔。
String inProvinceMessage = tourLedgerService.checkInProvinceParticipation(
jobNo,
setting.getInProvinceYears(),
oldLedger == null ? null : oldLedger.getId());
if (StrUtil.isNotBlank(inProvinceMessage)) {
return Result.error(inProvinceMessage);
}
Cnd sameYearCnd = Cnd.where(TourLedger::getDelFlag, "=", false)
.and(TourLedger::getJobNo, "=", jobNo)
.and(TourLedger::getYear, "=", matter.getYear());
@@ -69,6 +69,8 @@ public class TourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
// 编辑时同时返回分工会人员数量,确保页面按当前组织和人员总数完整回显。
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(id));
return Result.success(tourSetting);
}
@@ -89,15 +91,30 @@ public class TourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
// 延用上一年配置时带出原分配数量,前端会清除子表ID后作为新配置提交。
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(tourSetting.getId()));
return Result.success(tourSetting);
}
/**
* 查询人员分配页签所需的全部分工会、当前人员总数和已保存人员数量。
*
* @param settingId 疗休养配置ID,新增时为空
* @return 分工会人员分配行
*/
@At
@SaCheckPermission("tour.setting")
public Result unionQuotaRows(String settingId) {
return Result.success(tourSettingService.listUnionQuotaRows(settingId));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.setting")
@SLog(type = "tour", tag = "疗休养设置", msg = "保存疗休养配置")
public Result doSubmit(TourSetting tourSetting,
@Param(value = "lots") String lots,
@Param(value = "unionQuotas") String unionQuotas,
@Param(value = "lotDeleteList") String[] lotDeleteList) {
Result checkResult = check(tourSetting);
if (checkResult != null) {
@@ -113,6 +130,14 @@ public class TourSettingController {
return Result.error("同年度下配置名称已存在");
}
// 后端再次校验分配总量,避免绕过页面直接提交超出出行人数指标的数据。
int travelPeopleQuota = tourSetting.getTravelPeopleQuota() == null
? 0 : tourSetting.getTravelPeopleQuota();
String quotaLimitMessage = tourSettingService.checkUnionQuotaLimit(unionQuotas, travelPeopleQuota);
if (StrUtil.isNotBlank(quotaLimitMessage)) {
return Result.error(quotaLimitMessage);
}
// 布尔值给默认值,避免前端未传时出现空状态。
if (tourSetting.getEnabled() == null) {
tourSetting.setEnabled(true);
@@ -148,6 +173,7 @@ public class TourSettingController {
} else {
tourSettingService.insertWith(tourSetting, "lots");
}
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
} else {
// 编辑时先处理页面删除的标段,再保存配置和当前标段行。
if (Lang.isNotEmpty(lotDeleteList)) {
@@ -155,6 +181,7 @@ public class TourSettingController {
}
tourSettingService.updateIgnoreNull(tourSetting);
saveLots(tourSetting);
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
}
return Result.success();
}
@@ -168,6 +195,8 @@ public class TourSettingController {
return Result.error("参数错误");
}
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id));
// 配置删除时同步清理人员分配,避免遗留不可达的子表数据。
tourSettingService.clearUnionQuotas(id);
tourSettingService.delete(id);
return Result.success();
}
@@ -205,10 +234,16 @@ public class TourSettingController {
if (StrUtil.isBlank(tourSetting.getConfigName())) {
return Result.error("配置名称不能为空");
}
if (tourSetting.getTravelPeopleQuota() == null || tourSetting.getTravelPeopleQuota() < 0) {
return Result.error("出行人数指标必须为非负整数");
}
if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null
&& tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) {
return Result.error("最少成团人数不能大于最多成团人数");
}
if (tourSetting.getInProvinceYears() != null && tourSetting.getInProvinceYears() < 0) {
return Result.error("省内间隔年限必须为非负整数");
}
if (tourSetting.getCycleStartYear() != null && tourSetting.getCycleEndYear() != null
&& tourSetting.getCycleStartYear() > tourSetting.getCycleEndYear()) {
return Result.error("周期开始年度不能大于周期结束年度");
@@ -531,6 +531,21 @@ public class TourSignupController {
.addv("currentLineCost", cycleTotalCost.currentCost())
.addv("message", buildCycleTotalCostNotice(cycleTotalCost)));
}
if (isInProvinceLine(line.getLineType())) {
// 进入报名表单前先校验省内参加间隔,最终提交时还会再次执行相同规则。
String inProvinceMessage = tourLedgerService.checkInProvinceParticipation(
currentJobNo(),
setting.getInProvinceYears(),
oldLedger == null ? null : oldLedger.getId());
if (StrUtil.isNotBlank(inProvinceMessage)) {
return Result.success(NutMap.NEW()
.addv("canApply", false)
.addv("noticeRequired", true)
.addv("noticeType", "inProvinceYears")
.addv("message", inProvinceMessage));
}
return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false));
}
if (!isOutProvinceLine(line.getLineType())) {
return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false));
}
@@ -699,12 +714,16 @@ public class TourSignupController {
ledger.setOverCostReimbursed(Boolean.TRUE.equals(ledger.getOverCostReimbursed()));
fillStaffInfo(ledger);
// PC和移动端共用本接口;在写入台账前锁定分工会名额,防止并发报名超过人员分配数量。
String quotaMessage = tourLedgerService.saveWithUnionSignupQuota(
ledger, matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), update);
if (StrUtil.isNotBlank(quotaMessage)) {
return Result.error(quotaMessage);
}
if (update) {
tourLedgerService.updateIgnoreNull(ledger);
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
} else {
tourLedgerService.insert(ledger);
}
if (Lang.isNotEmpty(familyList)) {
familyList.forEach(item -> {
@@ -936,6 +955,14 @@ public class TourSignupController {
}
}
if (isInProvinceLine(line.getLineType())) {
// 最终提交以台账中的实际参加记录为准,避免绕过资格提示接口直接报名。
String inProvinceMessage = tourLedgerService.checkInProvinceParticipation(
jobNo,
setting.getInProvinceYears(),
oldLedger == null ? null : oldLedger.getId());
if (StrUtil.isNotBlank(inProvinceMessage)) {
return Result.error(inProvinceMessage);
}
Cnd sameYearCnd = Cnd.where(TourLedger::getDelFlag, "=", false)
.and(TourLedger::getJobNo, "=", jobNo)
.and(TourLedger::getYear, "=", matter.getYear());
@@ -58,13 +58,13 @@ public class TourTravelAgencyController {
return Result.error("参数错误");
}
TourTravelAgency agency = travelAgencyService.fetch(id);
return agency == null ? Result.error("旅行社不存在") : Result.success(agency);
return agency == null ? Result.error("服务单位不存在") : Result.success(agency);
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.travelAgency")
@SLog(type = "tour", tag = "旅行社管理", msg = "保存旅行社信息")
@SLog(type = "tour", tag = "服务单位管理", msg = "保存服务单位信息")
public Result doSubmit(TourTravelAgency agency) {
Result checkResult = check(agency);
if (checkResult != null) {
@@ -77,7 +77,7 @@ public class TourTravelAgencyController {
sameCodeCnd.and(TourTravelAgency::getId, "<>", agency.getId());
}
if (travelAgencyService.count(sameCodeCnd) > 0) {
return Result.error("同年度下旅行社编号已存在");
return Result.error("同年度下服务单位编号已存在");
}
if (agency.getEnabled() == null) {
agency.setEnabled(true);
@@ -93,7 +93,7 @@ public class TourTravelAgencyController {
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.travelAgency")
@SLog(type = "tour", tag = "旅行社管理", msg = "删除旅行社信息")
@SLog(type = "tour", tag = "服务单位管理", msg = "删除服务单位信息")
public Result doDelete(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
@@ -110,10 +110,10 @@ public class TourTravelAgencyController {
return Result.error("年度不能为空");
}
if (StrUtil.isBlank(agency.getAgencyName())) {
return Result.error("旅行社名称不能为空");
return Result.error("服务单位名称不能为空");
}
if (StrUtil.isBlank(agency.getAgencyCode())) {
return Result.error("旅行社编号不能为空");
return Result.error("服务单位编号不能为空");
}
if (StrUtil.isBlank(agency.getContactName())) {
return Result.error("联系人不能为空");
@@ -102,12 +102,12 @@ public class TourLedger extends BaseModel implements Serializable {
private String hotelName;
@Column
@Comment("旅行社ID")
@Comment("服务单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String travelAgencyId;
@Column
@Comment("旅行社")
@Comment("服务单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String travelAgencyName;
@@ -42,7 +42,7 @@ public class TourLine extends BaseModel implements Serializable {
private String lineName;
@Column
@Comment("旅行社ID")
@Comment("服务单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String travelAgencyId;
@@ -43,6 +43,12 @@ public class TourSetting extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 50)
private String tourType;
@Column
@Comment("出行人数指标")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer travelPeopleQuota;
@Column
@Comment("可参加人员范围ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -63,6 +69,11 @@ public class TourSetting extends BaseModel implements Serializable {
@ColDefine(type = ColType.INT)
private Integer maxGroupPeople;
@Column
@Comment("省内几年去一次")
@ColDefine(type = ColType.INT)
private Integer inProvinceYears;
@Column
@Comment("省外几年去一次")
@ColDefine(type = ColType.INT)
@@ -122,11 +133,16 @@ public class TourSetting extends BaseModel implements Serializable {
private Boolean enabled;
/**
* 标段管理沿用老疗休养配置的子表设计,后续线路、旅行社等模块可通过标段ID继续关联。
* 标段管理沿用老疗休养配置的子表设计,后续线路、服务单位等模块可通过标段ID继续关联。
*/
@Many(field = "settingId")
private List<TourSettingLot> lots;
/**
* 分工会人员分配,仅用于配置弹窗回显与提交,不作为 tour_setting 表字段保存。
*/
private List<TourSettingUnionQuota> unionQuotas;
@Column
@Comment("服务须知")
@ColDefine(type = ColType.TEXT)
@@ -0,0 +1,60 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养配置下的分工会人员数量分配。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_setting_union_quota")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养配置分工会人员分配")
public class TourSettingUnionQuota extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("所属疗休养配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("分工会名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("人员数量")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer peopleQuota;
/**
* 当前分工会人员总数,仅用于页面分配参考,不写入数据库。
*/
private Integer personCount;
}
@@ -9,14 +9,14 @@ import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 旅行社管理。
* 先维护疗休养线路创建会复用的旅行社基础信息,后续线路模块可通过旅行社ID关联。
* 服务单位管理。
* 先维护疗休养线路创建会复用的服务单位基础信息,后续线路模块可通过服务单位ID关联。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_travel_agency")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("普惠疗休养旅行社")
@Comment("普惠疗休养服务单位")
public class TourTravelAgency extends BaseModel implements Serializable {
@Column
@@ -32,12 +32,12 @@ public class TourTravelAgency extends BaseModel implements Serializable {
private Integer year;
@Column
@Comment("旅行社编号")
@Comment("服务单位编号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String agencyCode;
@Column
@Comment("旅行社名称")
@Comment("服务单位名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String agencyName;
@@ -3,5 +3,36 @@ package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
/**
* 疗休养报名台账服务,统一处理报名记录及并发名额校验。
*/
public interface TourLedgerService extends BaseService<TourLedger> {
/**
* 校验当前人员在省内间隔周期内是否已经实际参加过省内线路。
*
* @param jobNo 当前登录人工号
* @param inProvinceYears 包含当前年份在内的省内限制年数,小于等于0表示不限制
* @param excludeLedgerId 修改报名时需要排除的当前台账ID,新增时允许为空
* @return 校验通过返回空字符串,否则返回不能报名的业务提示
*/
String checkInProvinceParticipation(String jobNo,
Integer inProvinceYears,
String excludeLedgerId);
/**
* 在同一事务中锁定分工会名额、校验并发容量并写入报名台账。
*
* @param ledger 待新增或修改的报名台账
* @param settingId 疗休养配置ID
* @param matterId 当前报名事项ID
* @param userId 当前登录用户ID
* @param update true 表示修改已有报名,false 表示新增报名
* @return 保存成功返回空字符串,否则返回业务提示
*/
String saveWithUnionSignupQuota(TourLedger ledger,
String settingId,
String matterId,
String userId,
boolean update);
}
@@ -2,6 +2,44 @@ package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota;
import java.util.List;
/**
* 疗休养配置服务,统一处理配置及分工会人员数量分配业务。
*/
public interface TourSettingService extends BaseService<TourSetting> {
/**
* 查询全部分工会在指定配置下的人员数量,并实时补充分工会人员总数。
*
* @param settingId 疗休养配置ID,新增配置时允许为空
* @return 按分工会编码排序的人员分配行
*/
List<TourSettingUnionQuota> listUnionQuotaRows(String settingId);
/**
* 保存指定配置的分工会人员数量,提交内容为 JSON 数组字符串。
*
* @param settingId 疗休养配置ID
* @param unionQuotas 分工会人员分配 JSON
*/
void saveUnionQuotas(String settingId, String unionQuotas);
/**
* 校验各分工会人员数量是否均为非负整数且合计不超过出行人数指标。
*
* @param unionQuotas 分工会人员分配 JSON
* @param travelPeopleQuota 出行人数指标
* @return 校验通过返回空字符串,否则返回业务提示
*/
String checkUnionQuotaLimit(String unionQuotas, int travelPeopleQuota);
/**
* 清除指定配置下的全部分工会人员分配。
*
* @param settingId 疗休养配置ID
*/
void clearUnionQuotas(String settingId);
}
@@ -1,15 +1,178 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.time.LocalDate;
/**
* 疗休养报名台账服务实现。
*/
@IocBean(args = {"refer:dao"})
public class TourLedgerServiceImpl extends BaseServiceImpl<TourLedger> implements TourLedgerService {
public TourLedgerServiceImpl(Dao dao) {
super(dao);
}
@Override
public String checkInProvinceParticipation(String jobNo,
Integer inProvinceYears,
String excludeLedgerId) {
if (inProvinceYears == null || inProvinceYears <= 0) {
return "";
}
if (StrUtil.isBlank(jobNo)) {
return "未查询到当前登录人工号,不能校验省内线路报名资格";
}
int currentYear = LocalDate.now().getYear();
// N年按包含当前年份在内的N个自然年度计算,例如2026年的3年周期为2024至2026年。
long calculatedStartYear = (long) currentYear - inProvinceYears + 1L;
int startYear = calculatedStartYear < 0 ? 0 : (int) calculatedStartYear;
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId";
Sql sql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger
WHERE delFlag = 0
AND joined = 1
AND jobNo = @jobNo
AND `year` >= @startYear
AND `year` <= @currentYear
AND lineType IN ('省内线路', '省内')
""" + excludeSql);
sql.setParam("jobNo", jobNo);
sql.setParam("startYear", startYear);
sql.setParam("currentYear", currentYear);
if (StrUtil.isNotBlank(excludeLedgerId)) {
sql.setParam("excludeLedgerId", excludeLedgerId);
}
sql.setCallback(Sqls.callback.integer());
dao().execute(sql);
if (sql.getInt() <= 0) {
return "";
}
return "您在 " + startYear + "" + currentYear
+ " 年内已参加过省内线路,省内线路每 " + inProvinceYears + " 年可参加一次,当前不能报名";
}
@Override
@Aop(TransAop.READ_COMMITTED)
public String saveWithUnionSignupQuota(TourLedger ledger,
String settingId,
String matterId,
String userId,
boolean update) {
if (ledger == null) {
return "报名数据不能为空";
}
String quotaMessage = lockAndCheckUnionSignupQuota(
settingId, matterId, userId, update ? ledger.getId() : null);
if (StrUtil.isNotBlank(quotaMessage)) {
return quotaMessage;
}
// 名额校验通过后立即在同一事务内写入台账,事务提交前始终持有分工会名额行锁。
if (update) {
updateIgnoreNull(ledger);
} else {
insert(ledger);
}
return "";
}
/**
* 锁定当前配置和登录人所属分工会的名额行,并校验报名后是否超过人员数量。
*/
private String lockAndCheckUnionSignupQuota(String settingId,
String matterId,
String userId,
String excludeLedgerId) {
if (StrUtil.isBlank(settingId) || StrUtil.isBlank(matterId)) {
return "疗休养配置或报名事项不存在";
}
// 分工会信息从用户视图读取,不接受报名表单传入的工会ID,防止跨工会占用名额。
View_user user = dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", userId));
if (user == null || StrUtil.isBlank(user.getUnionId()) || StrUtil.isBlank(user.getLoginname())) {
return "未查询到您所属的分工会,不能报名";
}
// FOR UPDATE 串行化同一配置、同一分工会的抢名额请求,不同分工会之间互不阻塞。
Sql quotaSql = Sqls.create("""
SELECT peopleQuota
FROM tour_setting_union_quota
WHERE settingId = @settingId
AND unionId = @unionId
AND delFlag = 0
LIMIT 1
FOR UPDATE
""");
quotaSql.setParam("settingId", settingId);
quotaSql.setParam("unionId", user.getUnionId());
quotaSql.setCallback(Sqls.callback.map());
dao().execute(quotaSql);
NutMap quotaRow = quotaSql.getObject(NutMap.class);
if (quotaRow == null || quotaRow.isEmpty()) {
return "您所在分工会未分配报名名额";
}
int peopleQuota = quotaRow.getInt("peopleQuota", 0);
if (peopleQuota <= 0) {
return "您所在分工会的报名名额已满";
}
// 取得名额锁后再次检查同事项重复报名,关闭双击或并发请求绕过前置查询的时间窗口。
String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId";
Sql duplicateSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger
WHERE delFlag = 0
AND matterId = @matterId
AND jobNo = @jobNo
""" + excludeSql);
duplicateSql.setParam("matterId", matterId);
duplicateSql.setParam("jobNo", user.getLoginname());
if (StrUtil.isNotBlank(excludeLedgerId)) {
duplicateSql.setParam("excludeLedgerId", excludeLedgerId);
}
duplicateSql.setCallback(Sqls.callback.integer());
dao().execute(duplicateSql);
if (duplicateSql.getInt() > 0) {
return "您已报名当前出行时段,请勿重复提交";
}
// 名额按配置统计,覆盖该配置下的全部事项;待审核报名也占用名额,取消后因台账删除自动释放。
String countExcludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND ledger.id <> @excludeLedgerId";
Sql countSql = Sqls.create("""
SELECT COUNT(1)
FROM tour_ledger ledger
INNER JOIN tour_matter matter
ON matter.id = ledger.matterId
AND matter.delFlag = 0
WHERE ledger.delFlag = 0
AND matter.settingId = @settingId
AND ledger.unionId = @unionId
""" + countExcludeSql);
countSql.setParam("settingId", settingId);
countSql.setParam("unionId", user.getUnionId());
if (StrUtil.isNotBlank(excludeLedgerId)) {
countSql.setParam("excludeLedgerId", excludeLedgerId);
}
countSql.setCallback(Sqls.callback.integer());
dao().execute(countSql);
int signupCount = countSql.getInt();
if (signupCount + 1 > peopleQuota) {
return "您所在分工会的报名名额已满(已报名 " + signupCount
+ " 人,分配 " + peopleQuota + " 人)";
}
return "";
}
}
@@ -1,15 +1,172 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 疗休养配置服务实现,负责分工会人员分配的查询、校验和持久化。
*/
@IocBean(args = {"refer:dao"})
public class TourSettingServiceImpl extends BaseServiceImpl<TourSetting> implements TourSettingService {
public TourSettingServiceImpl(Dao dao) {
super(dao);
}
@Override
public List<TourSettingUnionQuota> listUnionQuotaRows(String settingId) {
// 一次查询分工会、已保存数量和实时人员总数,避免逐个工会统计产生 N+1 查询。
Sql sql = Sqls.create("""
SELECT
quota.id AS id,
@settingId AS settingId,
un.id AS unionId,
un.name AS unionName,
COALESCE(quota.peopleQuota, 0) AS peopleQuota,
COALESCE(user_count.personCount, 0) AS personCount
FROM sys_union un
LEFT JOIN tour_setting_union_quota quota
ON quota.unionId = un.id
AND quota.settingId = @settingId
AND COALESCE(quota.delFlag, 0) = 0
LEFT JOIN (
SELECT unionId, COUNT(1) AS personCount
FROM vw_user
GROUP BY unionId
) user_count ON user_count.unionId = un.id
WHERE COALESCE(un.delFlag, 0) = 0
ORDER BY un.unionCode ASC
""");
sql.setParam("settingId", StrUtil.blankToDefault(settingId, ""));
List<NutMap> rows = listMap(sql);
return rows.stream().map(row -> {
TourSettingUnionQuota quota = new TourSettingUnionQuota();
quota.setId(row.getString("id"));
quota.setSettingId(row.getString("settingId"));
quota.setUnionId(row.getString("unionId"));
quota.setUnionName(row.getString("unionName"));
quota.setPeopleQuota(toNonNegativeInteger(row.getInt("peopleQuota")));
quota.setPersonCount(toNonNegativeInteger(row.getInt("personCount")));
return quota;
}).collect(Collectors.toList());
}
@Override
public void saveUnionQuotas(String settingId, String unionQuotas) {
if (StrUtil.isBlank(settingId)) {
return;
}
clearUnionQuotas(settingId);
if (StrUtil.isBlank(unionQuotas)) {
return;
}
List<TourSettingUnionQuota> quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas);
if (Lang.isEmpty(quotaList)) {
return;
}
// 工会名称以当前组织表为准,防止前端篡改或历史名称继续写入。
Map<String, Sys_union> unionMap = dao().query(Sys_union.class,
Cnd.where(Sys_union::getDelFlag, "=", false)).stream()
.collect(Collectors.toMap(Sys_union::getId, item -> item, (first, second) -> first));
List<TourSettingUnionQuota> saveList = quotaList.stream()
.filter(item -> item != null
&& StrUtil.isNotBlank(item.getUnionId())
&& unionMap.containsKey(item.getUnionId()))
.map(item -> normalizeUnionQuota(settingId, item, unionMap))
.filter(item -> item.getPeopleQuota() > 0)
.collect(Collectors.toList());
if (Lang.isNotEmpty(saveList)) {
dao().insert(saveList);
}
}
@Override
public String checkUnionQuotaLimit(String unionQuotas, int travelPeopleQuota) {
if (StrUtil.isBlank(unionQuotas)) {
return "";
}
final List<TourSettingUnionQuota> quotaList;
try {
quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas);
} catch (Exception e) {
return "人员分配数据格式不正确";
}
if (Lang.isEmpty(quotaList)) {
return "";
}
long total = 0L;
Set<String> unionIds = new HashSet<>();
Set<String> validUnionIds = dao().query(Sys_union.class,
Cnd.where(Sys_union::getDelFlag, "=", false)).stream()
.map(Sys_union::getId)
.collect(Collectors.toSet());
for (int index = 0; index < quotaList.size(); index++) {
TourSettingUnionQuota quota = quotaList.get(index);
if (quota == null || quota.getPeopleQuota() == null) {
continue;
}
// 工会ID必须来自当前组织表且不能重复,防止伪造行绕过总量和组织范围校验。
if (StrUtil.isBlank(quota.getUnionId())
|| !validUnionIds.contains(quota.getUnionId())
|| !unionIds.add(quota.getUnionId())) {
return "" + (index + 1) + "行分工会信息无效或重复";
}
if (quota.getPeopleQuota() < 0) {
return "" + (index + 1) + "行人员数量必须为非负整数";
}
total += quota.getPeopleQuota();
}
if (total > travelPeopleQuota) {
return "当前人员数量合计 " + total + ",不能超过出行人数指标 " + travelPeopleQuota;
}
return "";
}
@Override
public void clearUnionQuotas(String settingId) {
if (StrUtil.isNotBlank(settingId)) {
dao().clear(TourSettingUnionQuota.class,
Cnd.where(TourSettingUnionQuota::getSettingId, "=", settingId));
}
}
/**
* 将前端分配行转换为可信的持久化对象,只接受当前组织表中的工会名称。
*/
private TourSettingUnionQuota normalizeUnionQuota(String settingId,
TourSettingUnionQuota item,
Map<String, Sys_union> unionMap) {
Sys_union union = unionMap.get(item.getUnionId());
TourSettingUnionQuota quota = new TourSettingUnionQuota();
quota.setSettingId(settingId);
quota.setUnionId(item.getUnionId());
quota.setUnionName(union.getName());
quota.setPeopleQuota(toNonNegativeInteger(item.getPeopleQuota()));
return quota;
}
/**
* 将空值或负数统一转换为零,保证页面统计和数据库保存使用相同口径。
*/
private Integer toNonNegativeInteger(Integer value) {
return value == null || value < 0 ? 0 : value;
}
}
@@ -124,6 +124,13 @@ public class UnionReimburseCollectController {
return unionReimburseService.updateActuallyAmount(id, realMoney);
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"unionReimburse.collect", "h5.unionReimburse.collect"}, mode = SaMode.OR)
public Result updateReimburseProject(String id, String reimburseProject) {
return unionReimburseService.updateReimburseProject(id, reimburseProject);
}
@At
@Ok("void")
@SaCheckPermission(value = {"unionReimburse.collect", "h5.unionReimburse.collect"}, mode = SaMode.OR)
@@ -45,6 +45,11 @@ public class UnionReimburseDesc extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 200)
private String fileDesc;
@Column
@Comment("报销项目提示")
@ColDefine(type = ColType.VARCHAR, width = 500)
private String projectTip;
@Column
@Comment("排序字段")
@Prev({
@@ -69,6 +69,11 @@ public interface UnionReimburseService extends BaseService<UnionReimburse> {
*/
Result updateActuallyAmount(String id, Double realMoney);
/**
* 修改非慰问报销记录的活动项目。
*/
Result updateReimburseProject(String id, String reimburseProject);
/**
* 批量审核选中的待审核报销申请。
*
@@ -66,6 +66,13 @@ import java.util.stream.Collectors;
@Slf4j
@IocBean(args = {"refer:dao"})
public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> implements UnionReimburseService {
/** 非慰问报销项目允许在三个活动项目之间切换。 */
private static final Set<String> ACTIVITY_REIMBURSE_PROJECTS = Set.of(
"UNION_REIMBURSE_PROJECT_2",
"UNION_REIMBURSE_PROJECT_3",
"UNION_REIMBURSE_PROJECT_4"
);
@Inject
private SysFileService sysFileService;
@Inject
@@ -424,6 +431,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (dbRecord == null) {
return Result.error("未找到对应的报销记录");
}
// 实际报销金额不得超过申请金额,避免绕过前端校验直接修改数据。
BigDecimal declaredMoney = getDeclaredApplyMoney(dbRecord);
if (newMoney.compareTo(declaredMoney) > 0) {
return Result.error("实际报销金额不能超过申请金额:" + declaredMoney.toPlainString());
}
if (Integer.valueOf(3).equals(dbRecord.getStateId()) && !skipBudgetDeduct(dbRecord)) {
OutlayUseDetail oldDetail = dao().fetch(OutlayUseDetail.class, Cnd.where("outlayReimburseId", "=", dbRecord.getId()));
BigDecimal oldMoney = oldDetail != null && oldDetail.getAdjustMoney() != null
@@ -439,6 +451,28 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
return Result.success();
}
@Override
public Result updateReimburseProject(String id, String reimburseProject) {
if (!isAdmin()) {
return Result.error("无权修改报销项目");
}
if (StrUtil.hasBlank(id, reimburseProject)) {
return Result.error("报销记录ID和报销项目不能为空");
}
UnionReimburse dbRecord = this.fetch(id);
if (dbRecord == null) {
return Result.error("未找到对应的报销记录");
}
// 慰问记录与活动项目字段不同,只允许活动项目之间切换。
if (!ACTIVITY_REIMBURSE_PROJECTS.contains(dbRecord.getReimburseProject())
|| !ACTIVITY_REIMBURSE_PROJECTS.contains(reimburseProject)) {
return Result.error("慰问记录不可修改,报销项目只能在三个活动项目之间切换");
}
dbRecord.setReimburseProject(reimburseProject);
this.updateIgnoreNull(dbRecord);
return Result.success();
}
private void normalizeInvoiceDetails(UnionReimburse unionReimburse) {
if (unionReimburse.getInvoiceDetails() == null) {
unionReimburse.setInvoiceDetails(new ArrayList<>());
@@ -8,6 +8,7 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalType;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
@@ -33,6 +34,8 @@ public class ProposalConfigTypeController {
private Dao dao;
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/config/type/index.html")
@@ -47,6 +50,8 @@ public class ProposalConfigTypeController {
Sql sql = Sqls.create("select * from proposal_type $condition");
Cnd cnd = Cnd.NEW();
cnd.and(Cnd.likeEX("name", name));
// 提案类型列表只允许名称和编码参与排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "configType");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageVO(pageForm, sql, ProposalType.class);
return Result.success(pagination);
@@ -19,6 +19,7 @@ import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
@@ -54,6 +55,8 @@ public class ProposalConfigUnitController {
private BaseService baseService;
@Inject
private SysRoleService sysRoleService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/config/unit/index.html")
@@ -99,7 +102,10 @@ public class ProposalConfigUnitController {
cnd.where().orLike("u2.loginname", pageForm.getSearchKeyword());
cnd.where().orLike("t1.name", pageForm.getSearchKeyword());
}
cnd.asc("t1.code");
// 承办单位列表未指定有效排序时继续按单位编码升序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "configUnit")) {
cnd.asc("t1.code");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -28,6 +28,7 @@ import org.nutz.mvc.annotation.Param;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@IocBean
@At("/platform/proposal/dashboard")
@@ -35,6 +36,23 @@ import java.util.List;
@Ok("json:full")
public class ProposalDashboardController {
/**
* 工作台表格允许排序的字段映射值为查询中的真实字段或安全别名
*/
private static final Map<String, String> DASHBOARD_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("isConsolidation", "info.isConsolidation"),
Map.entry("masterUnitName", "masterUnitName"),
Map.entry("slaveUnitNames", "slaveUnitNames"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private ProposalCommonService proposalCommonService;
@Inject
@@ -178,6 +196,8 @@ public class ProposalDashboardController {
}
}
// 根据工作台字段白名单追加排序未选择排序时保持原有查询顺序
proposalCommonService.applySafePageOrder(cnd, pageForm, DASHBOARD_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -7,6 +7,8 @@ import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalExportService;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalWriteService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
@@ -29,6 +31,10 @@ public class ProposalExportComprehensiveController {
@Inject
private ProposalExportService proposalExportService;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private ProposalWriteService proposalWriteService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/export/comprehensive/index.html")
@@ -73,11 +79,26 @@ public class ProposalExportComprehensiveController {
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 综合导出主列表的聚合字段仅通过固定查询别名排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "exportComprehensive");
sql.setCondition(cnd);
Pagination pagination = proposalExportService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
/**
* 查询综合导出页面当前教代会届次配置的提案类别
*
* @param sessionId 教代会届次ID用于读取该届次配置的提案类别
* @return VO 列表每项包含筛选使用的 id 和页面展示使用的 name
*/
@At
@SaCheckPermission("proposal.export.comprehensive")
@ApiOperation("查询当前届次提案类别")
public Result listSessionProposalTypes(@Valid String sessionId) {
return Result.success(proposalWriteService.listSessionProposalTypes(sessionId));
}
@At
@Ok("void")
@@ -87,6 +108,21 @@ public class ProposalExportComprehensiveController {
proposalExportService.exportSummaryAsExcel(pageForm, response);
}
/**
* 按办理单位导出提案承办单位表压缩包
*
* @param pageForm 综合导出页面查询参数其中 sessionId 为必传的教代会届次ID
* @param response HTTP 响应返回内容为包含多个 XSSF Excel ZIP 文件
*/
@At
@Ok("void")
@ApiOperation("按办理单位导出提案承办单位表压缩包")
@SaCheckPermission("proposal.query.comprehensive")
public void exportUndertakeUnitTablesAsZip(@Valid @Param("pageForm") ProposalQueryComprehensiveParam pageForm,
HttpServletResponse response) {
proposalExportService.exportUndertakeUnitTablesAsZip(pageForm, response);
}
@At
@Ok("void")
@@ -17,6 +17,7 @@ import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
@@ -63,6 +64,8 @@ public class ProposalExportSingleCustomController {
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private FlowEngine flowEngine;
@@ -100,7 +103,10 @@ public class ProposalExportSingleCustomController {
Cnd cnd = Cnd.NEW();
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
cnd.desc("info.createdAt");
// 自定义导出列表未指定有效排序时保留提案创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "exportSingleCustom")) {
cnd.desc("info.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -6,11 +6,13 @@ import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -31,6 +33,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean
@@ -40,10 +43,26 @@ import java.util.stream.Collectors;
@Api(tags = "提案统计分析")
public class ProposalQueryAnalysisController {
/**
* 统计数据表允许排序的字段白名单
*/
private static final Set<String> STATISTICS_ORDER_COLUMNS = Set.of(
"dimension", "itemName", "count", "rate"
);
/**
* 分析数据表允许排序的字段白名单
*/
private static final Set<String> ANALYSIS_ORDER_COLUMNS = Set.of(
"dimension", "total", "categoryCount", "topItem", "topCount", "topRate", "analysis"
);
@Inject
private BaseService baseService;
@Inject
private SysDictService sysDictService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/analysis/index.html")
@@ -51,36 +70,54 @@ public class ProposalQueryAnalysisController {
public void index() {
}
/**
* 查询提案统计数据
*
* @param sessionId 教代会届次ID
* @param dimension 统计维度为空时返回全部维度
* @param statisticsOrderName 统计表排序字段
* @param statisticsOrderBy 统计表排序方向
* @return 统计行列表包含维度分类项数量和占比
*/
@At
@ApiOperation("统计数据")
@SaCheckPermission("proposal.query.analysis")
public Result statisticsData(String sessionId, String dimension) {
/*
* 查询参数说明
* sessionId教代会届次ID用于限定统计提案范围
* dimension统计维度可传提案人单位提案类型代表团立案结果满意度为空时返回全部维度
* 返回值Result.data 为统计行列表包含 dimensionitemNamecountrate
*/
public Result statisticsData(String sessionId, String dimension, String statisticsOrderName, String statisticsOrderBy) {
if (StrUtil.isBlank(sessionId)) {
return Result.error("请先选择教代会届次");
}
return Result.success(buildStatisticsRows(sessionId, dimension));
List<NutMap> rows = buildStatisticsRows(sessionId, dimension);
PageForm orderForm = new PageForm();
orderForm.setPageOrderName(statisticsOrderName);
orderForm.setPageOrderBy(statisticsOrderBy);
// 统计数据生成后按独立白名单执行后端排序
proposalCommonService.sortStatisticsRows(rows, orderForm, STATISTICS_ORDER_COLUMNS);
return Result.success(rows);
}
/**
* 查询提案分析数据
*
* @param sessionId 教代会届次ID
* @param dimension 统计维度为空时返回全部维度
* @param analysisOrderName 分析表排序字段
* @param analysisOrderBy 分析表排序方向
* @return 分析行列表包含总量最高项最高占比和分析结论
*/
@At
@ApiOperation("分析数据")
@SaCheckPermission("proposal.query.analysis")
public Result analysisData(String sessionId, String dimension) {
/*
* 查询参数说明
* sessionId教代会届次ID用于限定分析提案范围
* dimension统计维度可传提案人单位提案类型代表团立案结果满意度为空时返回全部维度
* 返回值Result.data 为分析行列表包含 dimensiontotalcategoryCounttopItemtopCounttopRateanalysis
*/
public Result analysisData(String sessionId, String dimension, String analysisOrderName, String analysisOrderBy) {
if (StrUtil.isBlank(sessionId)) {
return Result.error("请先选择教代会届次");
}
return Result.success(buildAnalysisRows(sessionId, dimension));
List<NutMap> rows = buildAnalysisRows(sessionId, dimension);
PageForm orderForm = new PageForm();
orderForm.setPageOrderName(analysisOrderName);
orderForm.setPageOrderBy(analysisOrderBy);
// 分析数据生成后按独立白名单执行后端排序
proposalCommonService.sortStatisticsRows(rows, orderForm, ANALYSIS_ORDER_COLUMNS);
return Result.success(rows);
}
@At
@@ -94,9 +131,9 @@ public class ProposalQueryAnalysisController {
entities.add(new ExcelExportEntity("分类项", "itemName", 30));
entities.add(new ExcelExportEntity("数量", "count", 12));
entities.add(new ExcelExportEntity("占比", "rate", 12));
ExportParams params = new ExportParams();
params.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, entities, list);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, list);
CommonDownloadUtil.download("提案统计数据.xlsx", workbook, response);
}
@@ -114,9 +151,9 @@ public class ProposalQueryAnalysisController {
entities.add(new ExcelExportEntity("最高数量", "topCount", 12));
entities.add(new ExcelExportEntity("最高占比", "topRate", 12));
entities.add(new ExcelExportEntity("分析结论", "analysis", 60));
ExportParams params = new ExportParams();
params.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, entities, list);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, list);
CommonDownloadUtil.download("提案分析数据.xlsx", workbook, response);
}
@@ -27,6 +27,7 @@ import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@IocBean
@@ -36,6 +37,20 @@ import java.util.List;
@Api(tags = "征集进度查询")
public class ProposalQueryCollectProgressController {
/**
* 征集进度表格允许排序的字段映射值为查询中的真实字段或安全别名
*/
private static final Map<String, String> COLLECT_PROGRESS_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("delegationName", "delegationName"),
Map.entry("inviteCount", "inviteCount"),
Map.entry("finishCount", "finishCount"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
static List<NutMap> states = new ArrayList<>() {{
add(NutMap.NEW().addv("code", 10).addv("id", 10).addv("name", "撰写提案"));
add(NutMap.NEW().addv("code", 20).addv("id", 20).addv("name", "附议提案"));
@@ -83,8 +98,9 @@ public class ProposalQueryCollectProgressController {
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
$condition
""");
sql.setParam("sessionId", pageForm.getSessionId());
Cnd cnd = Cnd.NEW();
// 征集进度必须按页面选中的教代会过滤避免共享参数未处理sessionId导致跨届次查询
cnd.andEX("info.sessionId", "=", pageForm.getSessionId());
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
SqlExpressionGroup seg = new SqlExpressionGroup();
@@ -106,6 +122,8 @@ public class ProposalQueryCollectProgressController {
cnd.and(seg);
}
cnd.groupBy("info.id");
// 征集进度包含关联字段和人数统计别名只允许白名单字段进入排序条件
proposalCommonService.applySafePageOrder(cnd, pageForm, COLLECT_PROGRESS_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
List<NutMap> list = (List<NutMap>) pagination.getList();
@@ -25,6 +25,7 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/comprehensive")
@@ -33,6 +34,23 @@ import java.util.Arrays;
@Api(tags = "提案综合查询")
public class ProposalQueryComprehensiveController {
/**
* 综合查询表格允许排序的字段映射值为查询中的真实字段或安全别名
*/
private static final Map<String, String> COMPREHENSIVE_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("caseFilingType", "info.caseFilingType"),
Map.entry("merge", "merge"),
Map.entry("undertakeUnits", "undertakeUnitsOrder"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private BaseService baseService;
@Inject
@@ -62,7 +80,8 @@ public class ProposalQueryComprehensiveController {
ins.processDefineId instanceProcessDefineId,
t.displayName curTaskName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM
proposal_info info
LEFT JOIN proposal_type type on type.id = info.typeId
@@ -120,6 +139,8 @@ public class ProposalQueryComprehensiveController {
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 综合查询包含多表字段及聚合别名只允许白名单字段进入排序条件
proposalCommonService.applySafePageOrder(cnd, pageForm, COMPREHENSIVE_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -1,13 +1,16 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -19,8 +22,10 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/delegation")
@@ -29,9 +34,34 @@ import java.util.stream.Collectors;
@Api(tags = "代表团提案统计")
public class ProposalQueryDelegationController {
/**
* 代表团汇总表固定列的安全排序映射动态立案结果列在查询时追加
*/
private static final Map<String, String> DELEGATION_SUMMARY_ORDER_COLUMNS = Map.of(
"dbtName", "dbt.name",
"TOTAL", "TOTAL",
"caseRate", "caseRateOrder"
);
/**
* 代表团提案明细表允许排序的字段映射
*/
private static final Map<String, String> DELEGATION_PROPOSAL_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("sessionName", "sessionName"),
Map.entry("delegationName", "delegationName"),
Map.entry("curTaskName", "curTaskName")
);
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private SysDictService sysDictService;
@@ -46,28 +76,45 @@ public class ProposalQueryDelegationController {
@At
@ApiOperation("代表团提案统计")
@SaCheckPermission("proposal.query.delegation")
public Result pageData(String sessionId) {
public Result pageData(PageForm pageForm, String sessionId) {
List<Sys_dict> dictList = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT");
Sql sql = Sqls.create("""
SELECT
dbt.id,
dbt.`name` AS dbtName,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId ) as TOTAL,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.isSubmit = 1 ) as SUBMIT_COUNT,
$resultSql
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.isSubmit = 1 ) as SUBMIT_COUNT
$resultSql,
(select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult = @confirmFiling)
/ NULLIF((select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId), 0) AS caseRateOrder
FROM
teacher_congress_delegation dbt
$condition
""");
String resultStr = dictList.stream().map(v -> {
return "( select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult= '%s' ) as `%s`".formatted(v.getCode(), v.getCode());
}).collect(Collectors.joining(","));
sql.setVar("resultSql", resultStr);
Map<String, String> summaryOrderColumns = new HashMap<>(DELEGATION_SUMMARY_ORDER_COLUMNS);
List<String> resultSqlParts = new ArrayList<>();
int resultIndex = 0;
for (Sys_dict dict : dictList) {
String resultCode = dict.getCode();
// 动态别名仅允许字母数字和下划线查询值使用参数绑定避免字典内容进入SQL结构
if (StrUtil.isBlank(resultCode) || !resultCode.matches("[A-Za-z0-9_]+")) {
continue;
}
String paramName = "caseFilingResult" + resultIndex++;
resultSqlParts.add(", (select count(1) from proposal_info info where info.delegationId = dbt.id and info.sessionId = dbt.sessionId and info.caseFilingResult = @" + paramName + ") AS `" + resultCode + "`");
sql.setParam(paramName, resultCode);
summaryOrderColumns.put(resultCode, "`" + resultCode + "`");
}
sql.setVar("resultSql", String.join("", resultSqlParts));
sql.setParam("confirmFiling", "CONFIRM_FILING");
Cnd cnd = Cnd.NEW();
cnd.andEX("dbt.sessionId", "=", sessionId);
if (!AuthUtil.hasRoleOr("SYSADMIN", "SCHOOL_UNION_ADMIN")) {
}
cnd.asc("dbt.`code`");
// 未选择排序时保留原有代表团编码升序选择后仅应用白名单字段
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, summaryOrderColumns)) {
cnd.asc("dbt.`code`");
}
sql.setCondition(cnd);
return Result.success(baseService.listMap(sql));
}
@@ -101,7 +148,18 @@ public class ProposalQueryDelegationController {
$condition
""");
Cnd cnd = Cnd.NEW();
ProposalSearchParam.buildSearch(cnd, pageForm);
// 先应用明细字段白名单再隔离公共搜索中的原始排序处理阻止任意字段进入SQL
proposalCommonService.applySafePageOrder(cnd, pageForm, DELEGATION_PROPOSAL_ORDER_COLUMNS);
String requestedOrderName = pageForm.getPageOrderName();
String requestedOrderBy = pageForm.getPageOrderBy();
pageForm.setPageOrderName(null);
pageForm.setPageOrderBy(null);
try {
ProposalSearchParam.buildSearch(cnd, pageForm);
} finally {
pageForm.setPageOrderName(requestedOrderName);
pageForm.setPageOrderBy(requestedOrderBy);
}
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -5,6 +5,7 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -18,6 +19,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/history")
@@ -26,8 +28,27 @@ import javax.validation.Valid;
@Api(tags = "提案管理系统-查询统计-历史提案查询")
public class ProposalQueryHistoryController {
/**
* 历史提案表格允许排序的字段映射值为查询中的真实字段或安全别名
*/
private static final Map<String, String> HISTORY_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("sessionName", "sessionName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("caseFilingType", "info.caseFilingType"),
Map.entry("merge", "merge"),
Map.entry("undertakeUnits", "undertakeUnitsOrder"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/history/index.html")
@@ -53,7 +74,8 @@ public class ProposalQueryHistoryController {
ins.processDefineId instanceProcessDefineId,
t.displayName curTaskName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM
proposal_info info
LEFT JOIN proposal_type type on type.id = info.typeId
@@ -69,6 +91,8 @@ public class ProposalQueryHistoryController {
Cnd cnd = Cnd.NEW();
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 历史查询包含多表字段及聚合别名只允许白名单字段进入排序条件
proposalCommonService.applySafePageOrder(cnd, pageForm, HISTORY_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessTask;
@@ -31,6 +32,15 @@ import java.util.stream.Collectors;
@Slf4j
@Ok("json:full")
public class ProposalQueryUnitReplyController {
/**
* 承办单位办理统计允许排序的字段白名单
*/
private static final Set<String> UNIT_REPLY_ORDER_COLUMNS = Set.of(
"unitName", "sum", "masterSum", "masterReplySum", "masterNoReplySum",
"slaveSum", "slaveReplySum", "slaveNoReplySum"
);
@Inject
private Dao dao;
@Inject
@@ -45,9 +55,10 @@ public class ProposalQueryUnitReplyController {
@At
@SaCheckPermission("proposal.query.unitReply")
public Result data(String sessionId, String undertakeUnitId, String caseFilingResult) {
public Result data(PageForm pageForm, String sessionId, String undertakeUnitId, String caseFilingResult) {
/*
* 查询参数说明
* pageForm表格排序字段和排序方向
* sessionId教代会届次ID用于限定统计的提案范围
* undertakeUnitId承办单位ID用于只统计某个承办单位为空时统计全部承办单位
* caseFilingResult立案结果字典值对应 proposal_info.caseFilingResult为空时不限制立案结果
@@ -139,6 +150,8 @@ public class ProposalQueryUnitReplyController {
tableData.add(tableRow);
});
// 统计结果生成后在服务层按白名单字段排序避免前端字段参与任意业务处理
proposalCommonService.sortStatisticsRows(tableData, pageForm, UNIT_REPLY_ORDER_COLUMNS);
return Result.success().addData(Map.of("tableData", tableData, "slaveNeedReply", slaveNeedReply));
}
}
@@ -21,6 +21,7 @@ import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Map;
@IocBean
@At("/platform/proposal/query/yearReport")
@@ -29,6 +30,22 @@ import javax.validation.Valid;
@Api(tags = "提案年度报告")
public class ProposalQueryYearReportController {
/**
* 年度报告表格允许排序的字段映射值为查询中的真实字段或安全别名
*/
private static final Map<String, String> YEAR_REPORT_ORDER_COLUMNS = Map.ofEntries(
Map.entry("code", "info.code"),
Map.entry("name", "info.name"),
Map.entry("createUserName", "info.createUserName"),
Map.entry("typeName", "typeName"),
Map.entry("delegationName", "delegationName"),
Map.entry("caseFilingResult", "info.caseFilingResult"),
Map.entry("isConsolidation", "info.isConsolidation"),
Map.entry("undertakeUnits", "undertakeUnitsOrder"),
Map.entry("curTaskName", "curTaskName"),
Map.entry("instanceState", "instanceState")
);
@Inject
private ProposalCommonService proposalCommonService;
@@ -56,7 +73,8 @@ public class ProposalQueryYearReportController {
ins.processDefineId instanceProcessDefineId,
t.displayName curTaskName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
GROUP_CONCAT(DISTINCT pru.unitName ORDER BY pru.isMaster DESC, pru.unitName) AS undertakeUnitsOrder
FROM
proposal_info info
LEFT JOIN proposal_type type on type.id = info.typeId
@@ -70,8 +88,12 @@ public class ProposalQueryYearReportController {
$condition
""");
Cnd cnd = Cnd.NEW();
// 年度报告必须按页面选中的教代会过滤避免共享参数未处理sessionId导致跨届次查询
cnd.andEX("info.sessionId", "=", pageForm.getSessionId());
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 年度报告包含多表字段及聚合别名只允许白名单字段进入排序条件
proposalCommonService.applySafePageOrder(cnd, pageForm, YEAR_REPORT_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(),sql);
return Result.success(pagination);
@@ -1,6 +1,8 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessInstance;
@@ -10,6 +12,7 @@ import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalReplyUnit;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
@@ -30,10 +33,19 @@ import java.util.stream.Collectors;
@Api(tags = "提案承办单位满意度")
public class ProposalUnderTakeSatisfactionController {
/**
* 满意度统计固定列的排序白名单动态满意度列在查询时追加
*/
private static final Set<String> SATISFACTION_ORDER_COLUMNS = Set.of(
"unitName", "sum", "masterSum", "slaveSum"
);
@Inject
private Dao dao;
@Inject
private SysDictService sysDictService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/underTakeSatisfaction/index.html")
@@ -42,10 +54,24 @@ public class ProposalUnderTakeSatisfactionController {
}
/**
* 查询指定教代会和承办单位的满意度统计结果
*
* @param pageForm 表格排序参数
* @param sessionId 教代会届次ID
* @param undertakeUnitId 承办单位ID
* @return 满意度统计结果
*/
@At
@SaCheckPermission("proposal.query.underTake.satisfaction")
public Result data(String sessionId, String undertakeUnitId) {
public Result data(PageForm pageForm, String sessionId, String undertakeUnitId) {
List<Sys_dict> feedbackCodes = sysDictService.getSubListByCode("PROPOSAL_FEEDBACK");
Set<String> orderColumns = new HashSet<>(SATISFACTION_ORDER_COLUMNS);
// 动态满意度列仅从后端字典加入白名单不接受前端自行扩展字段
feedbackCodes.stream()
.map(Sys_dict::getCode)
.filter(StrUtil::isNotBlank)
.forEach(orderColumns::add);
// 当前届次所有的提案ID
Sql sql = Sqls.create("select id from proposal_info where sessionId = @sessionId").setParam("sessionId", sessionId);
@@ -109,6 +135,8 @@ public class ProposalUnderTakeSatisfactionController {
tableData.add(tableRow);
});
// 汇总完成后在服务层执行白名单排序数字列按数值顺序处理
proposalCommonService.sortStatisticsRows(tableData, pageForm, orderColumns);
return Result.success(tableData);
}
@@ -111,7 +111,10 @@ public class ProposalExpeditingController {
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 提案催办列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "expediting")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -78,6 +78,8 @@ public class ProposalSeniorBasicController {
Cnd cnd = Cnd.NEW();
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 内容修改列表仅允许页面展示字段参与排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "seniorBasic");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -78,6 +78,8 @@ public class ProposalSeniorDeleteController {
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 高级删除列表仅允许页面展示字段参与排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "seniorDelete");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -93,6 +93,8 @@ public class ProposalSeniorFeedBackController {
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
// cnd.and("latestFeedBack.id","is not",null);
cnd.groupBy("info.id");
// 反馈评分修改列表包含反馈计算列使用页面独立白名单排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "seniorFeedback");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -82,6 +82,8 @@ public class ProposalSeniorTypeController {
}
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
// 类型修改列表仅允许页面展示字段参与排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "seniorType");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -5,23 +5,23 @@ import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.validation.Valid;
import java.util.List;
/**
* @version 1.0
@@ -39,6 +39,8 @@ public class ProposalAllFinishedController {
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/allFinished/index.html")
@@ -62,7 +64,9 @@ public class ProposalAllFinishedController {
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId
FROM wf_process_instance ins
LEFT JOIN proposal_info info ON info.id = ins.businessNo
/* Limit all-finished data to proposal workflow instances and discard other module instances. */
INNER JOIN wf_process_define def ON def.id = ins.processDefineId AND def.name = 'JDHTA_NC'
INNER JOIN proposal_info info ON info.id = ins.businessNo
LEFT JOIN proposal_type type on type.id = info.typeId
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd on tcd.id = info.delegationId
@@ -72,9 +76,12 @@ public class ProposalAllFinishedController {
cnd.and("ins.state", "=", "20");
if(!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())){
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
// Non-admin users can only see proposal flows where they participated in at least one task.
cnd.and(new Static("EXISTS (SELECT 1 FROM wf_process_task wt INNER JOIN wf_process_task_actor wta ON wta.processTaskId = wt.id WHERE wt.processInstanceId = ins.id AND wta.actorId = '" + SecurityUtil.getUserId() + "')"));
}
ProposalSearchParam.buildSearch(cnd, pageForm);
// 已办结列表仅允许页面展示字段参与排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "allFinished");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
@@ -10,6 +11,7 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCaseCheckService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -20,7 +22,9 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.List;
@@ -42,6 +46,8 @@ public class ProposalCaseCheckController {
private BaseService baseService;
@Inject
private ProposalCaseCheckService proposalCaseCheckService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/caseCheck/index.html")
@@ -104,12 +110,24 @@ public class ProposalCaseCheckController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 委员会审查列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "caseCheck")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.caseCheck")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("caseCheck", pageForm, approval, tableColumns, response);
}
@At
@SaCheckPermission("proposal.caseCheck")
@ApiOperation("查询当前提案承办单位")
@@ -85,7 +85,10 @@ public class ProposalCommissionerController {
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.and("pco.id", approval ? "is not" : "is", null);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 委员查询包含统计列未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "commissioner")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -4,12 +4,14 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.lang.Dict;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalCommitteeFilingService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -27,6 +29,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@IocBean
@@ -40,6 +43,8 @@ public class ProposalCommitteeFilingController {
private Dao dao;
@Inject
private ProposalCommitteeFilingService proposalCommitteeFilingService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/committeeFiling/index.html")
@@ -102,12 +107,24 @@ public class ProposalCommitteeFilingController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 立案审核列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "committeeFiling")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommitteeFilingService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.committeeFiling")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("committeeFiling", pageForm, approval, tableColumns, response);
}
@At
@SaCheckPermission("proposal.committeeFiling")
@ApiOperation("并案审核")
@@ -6,6 +6,7 @@ import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.entity.ProcessInstance;
@@ -36,6 +37,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@IocBean
@@ -118,12 +120,24 @@ public class ProposalCommitteeFilingUnitController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 确认承办单位列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "committeeFilingUnit")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommitteeFilingUnitService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.committeeFilingUnit")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("committeeFilingUnit", pageForm, approval, tableColumns, response);
}
@At
@SaCheckPermission("proposal.committeeFilingUnit")
@ApiOperation("执行任务")
@@ -10,6 +10,7 @@ import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -33,6 +34,8 @@ public class ProposalControlController {
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/control/index.html")
@@ -84,7 +87,10 @@ public class ProposalControlController {
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 状态调整列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "control")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.bpm.enums.BpmProcessTaskStatusEnum;
@@ -32,6 +33,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
@@ -118,10 +120,22 @@ public class ProposalDelegationController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 团长审核列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "delegation")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.delegation")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("delegation", pageForm, approval, tableColumns, response);
}
}
@@ -5,6 +5,7 @@ import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
@@ -26,6 +27,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@IocBean
@@ -107,12 +109,24 @@ public class ProposalFeedbackEvaluationController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 反馈评分列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "feedbackEvaluation")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.feedbackEvaluation")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("feedbackEvaluation", pageForm, approval, tableColumns, response);
}
@At
@SaCheckPermission("proposal.feedbackEvaluation")
@ApiOperation("获取立案信息")
@@ -6,7 +6,6 @@ import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -99,10 +98,9 @@ public class ProposalInviteController {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
// 页面排序仅使用邀请列表白名单未指定有效字段时保留创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "invite")) {
cnd.desc("info.createdAt");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
}
cnd.groupBy("t.id");
sql.setCondition(cnd);
@@ -62,6 +62,28 @@ import java.util.Objects;
@Ok("json:full")
@Api(tags = "提案管理-我的提案")
public class ProposalMineController {
/** 我的提案列表允许排序的页面字段与 SQL 字段映射。 */
private static final Map<String, String> MINE_ORDER_COLUMNS = Map.of(
"code", "info.code",
"name", "info.name",
"createTime", "info.createTime",
"typeName", "type.name",
"sourceName", "sd.name",
"sessionName", "tcs.fullName",
"taskName", "t.displayName",
"instanceState", "ins.state"
);
/** 邀请附议人列表允许排序的页面字段与 SQL 字段映射。 */
private static final Map<String, String> SECONDER_ORDER_COLUMNS = Map.of(
"loginName", "t1.loginName",
"userName", "t1.userName",
"sex", "t1.sex",
"unitName", "t1.unitName",
"unionName", "t1.unionName",
"delegationName", "t2.name"
);
@Inject
private Dao dao;
@Inject
@@ -128,6 +150,8 @@ public class ProposalMineController {
cnd.and("info.createdBy", "=", SecurityUtil.getUserId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.andEX("info.sessionId", "=", sessionId);
// 排序字段必须经过白名单映射避免前端参数直接参与 SQL 排序
proposalCommonService.applySafePageOrder(cnd, pageForm, MINE_ORDER_COLUMNS);
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination<NutMap> pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -255,6 +279,8 @@ public class ProposalMineController {
seg.orLike("t1.userName", pageForm.getSearchKeyword());
cnd.and(seg);
}
// 邀请列表与主列表使用独立白名单确保关联表字段排序准确且安全
proposalCommonService.applySafePageOrder(cnd, pageForm, SECONDER_ORDER_COLUMNS);
sql.setCondition(cnd);
Pagination<ProposalInviteSeconderVO> pagination = proposalCommonService.listPageVO(pageForm, sql, ProposalInviteSeconderVO.class);
return Result.success(pagination);
@@ -3,12 +3,14 @@ package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -19,7 +21,9 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.List;
@@ -39,6 +43,8 @@ public class ProposalPreAuditController {
@Inject
private BaseService baseService;
@Inject
private ProposalCommonService proposalCommonService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/preAudit/index.html")
@@ -102,9 +108,21 @@ public class ProposalPreAuditController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 预审核列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "preAudit")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(),pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.preAudit")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("preAudit", pageForm, approval, tableColumns, response);
}
}
@@ -7,6 +7,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.vo.LabelValueVO;
import com.budwk.app.flow.constant.FlowConst;
@@ -38,6 +39,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
@@ -135,12 +137,24 @@ public class ProposalSchoolLeaderApprovalController {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 分管领导审批列表未指定有效排序时保留任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "schoolLeaderApproval")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.schoolLeaderApproval")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("schoolLeaderApproval", pageForm, approval, tableColumns, response);
}
@At
@SaCheckPermission("proposal.schoolLeaderApproval")
@ApiOperation("获取主办单位")
@@ -21,6 +21,7 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalSecond;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalSecondedService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -41,6 +42,7 @@ import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
/**
* 附议提案
@@ -51,6 +53,17 @@ import java.util.List;
@Ok("json:full")
@Api(tags = "提案管理系统-提案附议")
public class ProposalSecondedController {
/** 提案附议列表允许排序的页面字段与 SQL 字段或查询别名映射。 */
private static final Map<String, String> ORDER_COLUMNS = Map.of(
"code", "info.code",
"name", "info.name",
"createUserName", "info.createUserName",
"typeName", "type.name",
"delegationName", "tcd.name",
"taskActorName", "taskActorName",
"curTaskName", "curTaskName",
"instanceState", "ins.state"
);
@Inject
private Dao dao;
@@ -59,6 +72,8 @@ public class ProposalSecondedController {
@Inject
private ProposalSecondedService proposalSecondedService;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private FlowCommonService flowCommonService;
@At("")
@@ -141,7 +156,10 @@ public class ProposalSecondedController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 用户未指定有效排序字段时继续沿用原有任务创建时间倒序
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, ORDER_COLUMNS)) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -96,6 +96,8 @@ public class ProposalUnSubmitQueryController {
cnd.and("info.sessionId", "=", pageForm.getSessionId());
}
// 未提交查询包含子查询统计列统一通过页面独立白名单排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "unSubmitQuery");
sql.setCondition(cnd);
Pagination<NutMap> pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -137,6 +137,8 @@ public class ProposalUnderTakeReadController {
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id","task.id","JSON_EXTRACT( unit_data.DATA, '$.underTakeId' )");
// 初步阅览列表的计算字段使用固定查询别名排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "unitRead");
// cnd.groupBy("task.id");
// cnd.groupBy("JSON_EXTRACT( unit_data.DATA, '$.underTakeId' ) ");
sql.setCondition(cnd);
@@ -5,6 +5,7 @@ import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
@@ -37,6 +38,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
@@ -80,8 +82,9 @@ public class ProposalUnderTakeReplyController {
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval, boolean transfer) {
/*
* 承办单位答复列表需要按承办单位维度展示
* 同一个任务可能挂了多个承办单位负责人若只按 taskId 分组会把多条待审核合并成一条
* 因此这里优先使用任务参与人的承办单位信息拆行历史数据缺失时再回退到流程变量
* 新流程生成的会签任务可能未写入 underTakeNameunderTakeIdunderTakeIsMaster
* 因此优先读取任务变量变量缺失时再按提案ID + 任务参与人单位ID关联承办单位记录兜底
* 兼容已经生成的历史待办同时保持原有任务变量的数据优先级
*/
Sql sql = Sqls.create("""
SELECT
@@ -107,8 +110,13 @@ public class ProposalUnderTakeReplyController {
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
IFNULL(GROUP_CONCAT(DISTINCT nt.taskName),'结束') curTaskKey,
CASE WHEN t.taskState = 20 AND ( rt.id IS NULL OR rt.taskState = 10 ) THEN 1 ELSE 0 END AS canRevoke,
t.variable->>'$.underTakeName' AS underTakeName,
IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') = true, 1, 0) AS underTakeIsMaster,
COALESCE(NULLIF(t.variable->>'$.underTakeName', ''), replyUnit.unitName) AS underTakeName,
COALESCE(NULLIF(t.variable->>'$.underTakeId', ''), replyUnit.unitId) AS underTakeId,
CASE
WHEN JSON_EXTRACT(t.variable, '$.underTakeIsMaster') IS NOT NULL
THEN IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') = true, 1, 0)
ELSE IFNULL(replyUnit.isMaster, 0)
END AS underTakeIsMaster,
t.variable->>'$.tf_transferUserId' AS tf_transferUserId,
transferUser.username AS transferUserName,
GROUP_CONCAT(DISTINCT ta.actorName, '(', ta.actorAccount, ')' ) AS auditUser
@@ -119,6 +127,7 @@ public class ProposalUnderTakeReplyController {
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id
LEFT JOIN proposal_info info ON info.id = ins.businessNo
LEFT JOIN proposal_reply_unit replyUnit ON replyUnit.proposalId = info.id AND replyUnit.unitId = ta.actorUnitId
LEFT JOIN proposal_type type on type.id = info.typeId
LEFT JOIN proposal_merge mer ON mer.proposalId = info.id
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
@@ -141,7 +150,10 @@ public class ProposalUnderTakeReplyController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
// 承办答复列表未指定有效排序时继续按任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "unitReply")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -160,6 +172,15 @@ public class ProposalUnderTakeReplyController {
return Result.success(pagination);
}
@At
@Ok("void")
@SaCheckPermission("proposal.unitReply")
@ApiOperation("导出列表")
public void exportList(@Valid @Param("pageForm") ProposalSearchParam pageForm, boolean approval,
@Param("tableColumns") ExportTableColumns[] tableColumns, HttpServletResponse response) {
proposalCommonService.exportWorkflowList("unitReply", pageForm, approval, tableColumns, response);
}
/**
* 承办单位答复节点撤回需要基于整组当前节点判断
* 只有当前流程正在办理的节点这条列表记录所属节点完全一致时才允许撤回
@@ -100,7 +100,10 @@ public class ProposalUndertakeSuggestionController {
cnd.groupBy("t.id");
cnd.having(Cnd.where("count", approval ? ">" : "=", 0));
cnd.desc("t.createdAt");
// 承办意见列表未指定有效排序时继续按任务创建时间倒序
if (!proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "suggestion")) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -80,6 +80,8 @@ public class ProposalViceDelegationController {
cnd.and("info.delegationId", "in", proposalCommonService.getSelfManageDelegationIds());
}
ProposalSearchParam.buildSearch(cnd, pageForm);
// 副团长查阅列表仅允许页面已展示字段参与排序
proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "viceDelegation");
cnd.groupBy("info.id");
sql.setCondition(cnd);
Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -76,6 +76,19 @@ public class ProposalWriteController {
}
/**
* 查询当前教代会届次可用于撰写提案的类型
*
* @param sessionId 教代会届次ID
* @return VO 列表每项包含保存使用的 id 和页面展示使用的 name
*/
@At
@SaCheckPermission("proposal.write")
@ApiOperation("查询届次可用提案类型")
public Result listSessionProposalTypes(@Valid String sessionId) {
return Result.success(proposalWriteService.listSessionProposalTypes(sessionId));
}
@At
@SaCheckPermission("proposal.write")
@@ -84,6 +97,7 @@ public class ProposalWriteController {
@SLog(tag = "提案管理系统-我的提案", msg = "保存提案")
public Result save(@Param("info") ProposalInfo proposalInfo) {
proposalWriteService.checkCurrentUserDelegate(proposalInfo.getSessionId());
proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId());
if (StrUtil.isBlank(proposalInfo.getCode())) {
proposalInfo.setCode(proposalWriteService.generateProposalCode(proposalInfo.getSessionId(), proposalInfo.getDelegationId()));
}
@@ -100,6 +114,7 @@ public class ProposalWriteController {
@SLog(tag = "提案管理系统-我的提案", msg = "提交提案")
public Result submit(@Param("info") ProposalInfo proposalInfo) {
proposalWriteService.checkCurrentUserDelegate(proposalInfo.getSessionId());
proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId());
if (StrUtil.isBlank(proposalInfo.getCode())) {
proposalInfo.setCode(proposalWriteService.generateProposalCode(proposalInfo.getSessionId(), proposalInfo.getDelegationId()));
}
@@ -130,6 +145,7 @@ public class ProposalWriteController {
@ApiOperation("重新提交提案")
@SLog(tag = "提案管理系统-我的提案", msg = "重新提交提案")
public Result submitAgain(@Param("info") ProposalInfo proposalInfo, @Param("taskId") Long taskId) {
proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId());
dao.insertOrUpdate(proposalInfo);
Dict dict = Dict.create();
@@ -0,0 +1,21 @@
package com.budwk.app.zhgh.democratic.proposal.controller.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 撰写提案时当前教代会届次允许选择的提案类型
*/
@Data
@AllArgsConstructor
@ApiModel("届次可用提案类型")
public class ProposalWriteTypeVO {
@ApiModelProperty("提案类型ID,保存提案时写入 proposal_info.typeId")
private Integer id;
@ApiModelProperty("提案类型名称,来源于当前教代会届次的提案类型配置")
private String name;
}
@@ -6,6 +6,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.AssignmentHandler;
import com.budwk.app.flow.engine.core.Execution;
import com.budwk.app.flow.engine.core.ServiceContext;
@@ -36,12 +37,7 @@ public class ProposalUnitAssignmentHandler implements AssignmentHandler {
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
String proposalId = execution.getProcessInstance().getBusinessNo();
// 当前节点需要同时处理主办和协办负责人
// 这里不能按单位ID去重必须保留每个承办单位对应的一条分派记录
// 否则同一用户负责多个单位时会被错误合并成更少的待办
List<String> unitIds = new ArrayList<>();
unitIds.addAll(resolveMasterUnitIds(execution, dao, proposalId));
unitIds.addAll(resolveSlaveUnitIds(execution, dao, proposalId));
List<String> unitIds = resolveUnitIds(model, execution, dao, proposalId);
if (unitIds.isEmpty()) {
throw new BaseException("提案没有配置承办单位");
@@ -76,6 +72,25 @@ public class ProposalUnitAssignmentHandler implements AssignmentHandler {
return assignee;
}
/**
* 确定当前答复节点需要分派的单位范围
* 二次答复页面已明确提交 tf_secondReplyUnitIds 该字段就是唯一分派范围
* 未选择的主办或协办单位不能再从历史承办单位记录中补回
* 旧流程没有该字段时继续按主办协办配置回退保证历史流程可以正常运行
*/
private List<String> resolveUnitIds(TaskModel model, Execution execution, Dao dao, String proposalId) {
String secondReplyUnitIdsKey = FlowConst.TASK_FORM_DATA_PREFIX + "secondReplyUnitIds";
if ("two_unit_reply".equals(model.getName()) && execution.getArgs().containsKey(secondReplyUnitIdsKey)) {
return getStringList(execution.getArgs(), secondReplyUnitIdsKey);
}
// 初次答复需要同时处理主办和协办负责人旧二次答复流程也沿用该兼容逻辑
List<String> unitIds = new ArrayList<>();
unitIds.addAll(resolveMasterUnitIds(execution, dao, proposalId));
unitIds.addAll(resolveSlaveUnitIds(execution, dao, proposalId));
return unitIds;
}
/**
* 优先从当前流程变量读取主办单位读不到时回落到承办单位记录表兼容重新流转场景
*/
@@ -33,6 +33,11 @@ public class ProposalInfo extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 20)
private String code;
@Column
@Comment("立案编号")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String caseFilingCode;
@Column
@Comment("提案名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
@@ -25,7 +25,7 @@ public class ProposalType extends BaseModel {
@Column
@Comment("名称")
@ColDefine(type = ColType.VARCHAR, width = 10)
@ColDefine(type = ColType.VARCHAR, width = 50)
private String name;
@Column
@@ -21,6 +21,8 @@ public class ProposalQueryComprehensiveParam extends PageForm {
private String code;
@ApiModelProperty(name = "教代会ID")
private String sessionId;
@ApiModelProperty(name = "search session id")
private String searchSessionId;
@ApiModelProperty(name = "教代会ID")
private String[] sessionIds;
@ApiModelProperty(name = "代表团ID")
@@ -79,6 +81,8 @@ public class ProposalQueryComprehensiveParam extends PageForm {
} else {
// cnd.andEX("info.sessionId", "=", searchParam.getSessionId());
}
// Isolated single-session filter for pages that should not reuse sessionId semantics.
cnd.andEX("info.sessionId", "=", searchParam.getSearchSessionId());
//提案名称
if (StrUtil.isNotBlank(searchParam.getName())) {
@@ -34,6 +34,14 @@ public interface ProposalExportService extends BaseService<ProposalInfo> {
*/
void exportSummaryAsExcel(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response);
/**
* 按办理单位导出提案承办单位表压缩包
*
* @param pageForm 综合导出页面查询参数sessionId 应传当前教代会届次ID其他条件沿用页面查询条件
* @param response HTTP 响应方法直接输出 ZIPZIP 中每个有提案的单位对应一个 XSSF Excel
*/
void exportUndertakeUnitTablesAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response);
/**
* 导出提案立案汇总表excel
*
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.democratic.proposal.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalWriteTypeVO;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import javax.validation.Valid;
@@ -10,6 +11,22 @@ import java.util.Map;
public interface ProposalWriteService extends BaseService<ProposalInfo> {
/**
* 查询指定教代会届次允许选择的提案类型
*
* @param sessionId 教代会届次ID
* @return 提案类型 VO 列表id 用于保存name 用于页面展示
*/
List<ProposalWriteTypeVO> listSessionProposalTypes(String sessionId);
/**
* 校验所选提案类型是否属于当前教代会届次
*
* @param sessionId 教代会届次ID
* @param typeId 提案类型ID
*/
void validateProposalType(String sessionId, Integer typeId);
List<Sys_dict> listSource(String sessionId);
/**
@@ -1,17 +1,51 @@
package com.budwk.app.zhgh.democratic.proposal.service.common;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import org.nutz.dao.Cnd;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface ProposalCommonService extends BaseService<ProposalInfo> {
/**
* 按页面允许的字段白名单添加排序条件避免前端字段直接进入SQL
*
* @param cnd 查询条件
* @param pageForm 分页及排序参数
* @param allowedOrderColumns 前端字段与数据库字段或查询别名的映射
* @return 是否成功添加安全排序条件
*/
boolean applySafePageOrder(Cnd cnd, PageForm pageForm, Map<String, String> allowedOrderColumns);
/**
* 按提案列表页面注册的独立字段白名单添加排序条件
*
* @param cnd 查询条件
* @param pageForm 分页及排序参数
* @param pageCode 提案列表页面代码
* @return 是否成功添加安全排序条件
*/
boolean applySafeProposalListOrder(Cnd cnd, PageForm pageForm, String pageCode);
/**
* 按白名单字段对已汇总的统计结果进行后端排序
*
* @param rows 统计结果行
* @param pageForm 排序字段及方向参数
* @param allowedOrderColumns 允许排序的统计字段
*/
void sortStatisticsRows(List<NutMap> rows, PageForm pageForm, Set<String> allowedOrderColumns);
/**
* 查询提案信息
*/
@@ -105,6 +139,18 @@ public interface ProposalCommonService extends BaseService<ProposalInfo> {
*/
void exportYearReport(String sessionId, HttpServletResponse response);
/**
* 按办理页面当前筛选条件和可见列导出提案列表
*
* @param pageCode 页面代码用于固定查询范围数据权限和排序白名单
* @param pageForm 页面筛选条件
* @param approval 审核状态筛选条件
* @param tableColumns 当前页面可见列
* @param response HTTP 响应
*/
void exportWorkflowList(String pageCode, ProposalSearchParam pageForm, boolean approval,
ExportTableColumns[] tableColumns, HttpServletResponse response);
/**
* 合并提案
@@ -7,16 +7,24 @@ import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.engine.util.FlowUtil;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.sys.models.Sys_dict;
@@ -26,11 +34,13 @@ import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.zhgh.democratic.proposal.constants.ProposalState;
import com.budwk.app.zhgh.democratic.proposal.models.*;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalWriteService;
import com.budwk.app.zhgh.democratic.teachercongress.delegate.models.Teacher_congress_delegate;
import com.budwk.app.zhgh.democratic.teachercongress.delegate.service.TeacherCongressDelegateService;
@@ -53,12 +63,15 @@ import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.apache.poi.ss.usermodel.Workbook;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.text.Collator;
import java.util.*;
import java.util.stream.Collectors;
@@ -66,6 +79,12 @@ import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalCommonService {
/** PC端提案列表页面的排序字段白名单,页面代码由对应Controller固定传入。 */
private static final Map<String, Map<String, String>> PROPOSAL_LIST_ORDER_COLUMNS = createProposalListOrderColumns();
/** 办理页面允许导出的列表字段,避免请求参数携带非页面字段。 */
private static final Map<String, Set<String>> WORKFLOW_EXPORT_COLUMNS = createWorkflowExportColumns();
//找出富文本里面上传的图片
static String IMG_SRC_REGEX = "<img\\s+[^>]*src\\s*=\\s*([\"'])(/platform/sys/file/download\\?id=.*?)\\1[^>]*>";
@@ -88,6 +107,391 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
super(dao);
}
/**
* 使用业务页面提供的字段白名单构建排序条件非法字段或非法排序方向将被忽略
*
* @param cnd 查询条件
* @param pageForm 分页及排序参数
* @param allowedOrderColumns 前端字段与数据库字段或查询别名的映射
* @return 是否成功添加安全排序条件
*/
@Override
public boolean applySafePageOrder(Cnd cnd, PageForm pageForm, Map<String, String> allowedOrderColumns) {
if (cnd == null || pageForm == null || allowedOrderColumns == null) {
return false;
}
// 仅允许白名单中的字段参与排序防止构造任意SQL排序字段
String orderColumn = allowedOrderColumns.get(pageForm.getPageOrderName());
String orderBy = PageUtil.getOrder(pageForm.getPageOrderBy());
if (StrUtil.isBlank(orderColumn) || StrUtil.isBlank(orderBy)) {
return false;
}
cnd.orderBy(orderColumn, orderBy);
return true;
}
/**
* 根据页面代码读取独立白名单并应用排序未注册页面不会接受前端排序字段
*
* @param cnd 查询条件
* @param pageForm 分页及排序参数
* @param pageCode 提案列表页面代码
* @return 是否成功添加安全排序条件
*/
@Override
public boolean applySafeProposalListOrder(Cnd cnd, PageForm pageForm, String pageCode) {
if (StrUtil.isBlank(pageCode)) {
return false;
}
return applySafePageOrder(cnd, pageForm, PROPOSAL_LIST_ORDER_COLUMNS.get(pageCode));
}
/**
* 创建各列表页独立排序白名单显示字段与真实SQL字段或固定查询别名一一对应
*
* @return 不可变的页面排序白名单
*/
private static Map<String, Map<String, String>> createProposalListOrderColumns() {
Map<String, Map<String, String>> pages = new HashMap<>();
pages.put("invite", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("viceDelegation", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("unSubmitQuery", orderColumns("proposalCode", "info.code", "proposalName", "info.name", "username", "vu.username", "mobile", "vu.mobile", "unitName", "vu.unitName", "delegationName", "tcd.name", "typeName", "pt.name", "mannerName", "manner.name", "secondedNum", "secondedNum", "secondedAgreeNum", "secondedAgreeNum", "delegationAudit", "delegationAudit"));
pages.put("unitReply", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "caseFilingResult", "info.caseFilingResult", "merge", "merge", "underTakeName", "underTakeName", "underTakeIsMaster", "underTakeIsMaster", "curTaskName", "curTaskName", "auditUser", "auditUser", "instanceState", "ins.state", "transfer", "transferUserName"));
pages.put("unitRead", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "isConsolidation", "isConsolidation", "isMasterUnderTake", "isMasterUnderTake", "underTakeName", "underTakeName", "processInstanceNodeName", "inst.processInstanceNodeName"));
pages.put("suggestion", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "suggestUnits", "info.suggestUnits", "count", "count", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("commissioner", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "CONFIRM_FILING_COUNT", "CONFIRM_FILING_COUNT", "SUGGESTION_COUNT", "SUGGESTION_COUNT", "NOT_COUNT", "NOT_COUNT", "taskName", "taskName", "instanceState", "ins.state"));
pages.put("feedbackEvaluation", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "merge", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("delegation", standardOrderColumns());
pages.put("control", standardOrderColumns());
pages.put("schoolLeaderApproval", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "undertakeUnits", "masterUnitName", "merge", "merge", "curTaskName", "curTaskName", "auditUser", "auditUser", "instanceState", "ins.state"));
pages.put("committeeFilingUnit", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("allFinished", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "instanceState", "ins.state"));
pages.put("preAudit", standardOrderColumns());
pages.put("committeeFiling", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("caseCheck", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state", "finishTime", "t.finishTime"));
pages.put("exportComprehensive", orderColumns("code", "info.code", "caseFilingCode", "info.caseFilingCode", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "caseFilingType", "info.caseFilingType", "merge", "merge", "brief", "info.brief", "measures", "info.measures", "undertakeUnits", "masterUnitName", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("exportSingleCustom", standardOrderColumns());
pages.put("configUnit", orderColumns("name", "t1.name", "code", "t1.code", "unitLeader", "u2.username"));
pages.put("configType", orderColumns("name", "name", "code", "code"));
pages.put("seniorBasic", seniorOrderColumns());
pages.put("expediting", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "curTaskName", "curTaskName", "underTakeName", "underTakeName", "taskName", "t.displayName", "actorName", "ta.actorName", "instanceState", "ins.state"));
pages.put("seniorDelete", seniorOrderColumns());
pages.put("seniorFeedback", mergeOrderColumns(seniorOrderColumns(), orderColumns("tf_feedback", "tf_feedback")));
pages.put("seniorType", seniorOrderColumns());
return Collections.unmodifiableMap(pages);
}
/** 创建常规流程列表共用的字段映射。 */
private static Map<String, String> standardOrderColumns() {
return orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state");
}
/** 创建高级管理列表共用的字段映射。 */
private static Map<String, String> seniorOrderColumns() {
return orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "isConsolidation", "merge", "curTaskName", "curTaskName", "instanceState", "ins.state");
}
/**
* 按前端字段SQL字段成对创建不可变映射
*
* @param mappings 交替排列的前端字段和SQL字段
* @return 不可变字段映射
*/
private static Map<String, String> orderColumns(String... mappings) {
if (mappings == null || mappings.length % 2 != 0) {
throw new IllegalArgumentException("排序字段映射必须成对配置");
}
Map<String, String> columns = new LinkedHashMap<>();
for (int i = 0; i < mappings.length; i += 2) {
columns.put(mappings[i], mappings[i + 1]);
}
return Collections.unmodifiableMap(columns);
}
/** 合并基础字段和页面扩展字段,并返回不可变映射。 */
private static Map<String, String> mergeOrderColumns(Map<String, String> base, Map<String, String> extension) {
Map<String, String> columns = new LinkedHashMap<>(base);
columns.putAll(extension);
return Collections.unmodifiableMap(columns);
}
/**
* 按办理页面当前筛选条件和可见列导出列表查询范围与页面分页列表保持一致
*
* @param pageCode 页面代码
* @param pageForm 页面筛选条件
* @param approval 审核状态
* @param tableColumns 页面可见列
* @param response HTTP 响应
*/
@Override
public void exportWorkflowList(String pageCode, ProposalSearchParam pageForm, boolean approval,
ExportTableColumns[] tableColumns, HttpServletResponse response) {
List<ExportTableColumns> exportColumns = getWorkflowExportColumns(pageCode, tableColumns);
if (exportColumns.isEmpty()) {
throw Lang.makeThrow("请选择至少一个列表字段后再导出");
}
Sql sql = buildWorkflowExportSql(pageCode, pageForm, approval);
List<NutMap> rows = listMap(sql);
convertWorkflowExportValues(rows, pageCode);
List<ExcelExportEntity> excelColumns = new ArrayList<>();
for (ExportTableColumns column : exportColumns) {
excelColumns.add(new ExcelExportEntity(column.getLabel(), column.getProp(), 20));
}
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle("提案列表");
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, excelColumns, rows);
CommonDownloadUtil.download("提案列表.xlsx", workbook, response);
}
/** 根据页面代码构造导出查询,保证数据权限、筛选条件及排序规则与列表页一致。 */
private Sql buildWorkflowExportSql(String pageCode, ProposalSearchParam pageForm, boolean approval) {
boolean isUnitReply = "unitReply".equals(pageCode);
boolean isSchoolLeaderApproval = "schoolLeaderApproval".equals(pageCode);
boolean requiresUnionFilter = Set.of("preAudit", "committeeFiling", "committeeFilingUnit",
"schoolLeaderApproval", "caseCheck").contains(pageCode);
String taskName = getWorkflowTaskName(pageCode);
String schoolLeaderColumns = isSchoolLeaderApproval ? """
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 1 THEN pru.unitName END) AS masterUnitName,
GROUP_CONCAT(DISTINCT CASE WHEN pru.isMaster = 0 THEN pru.unitName END) AS slaveUnitNames,
""" : "";
String auditUserColumn = (isSchoolLeaderApproval || isUnitReply)
? "GROUP_CONCAT(DISTINCT ta.actorName, '(', ta.actorAccount, ')') AS auditUser,"
: "";
String unitReplyColumns = isUnitReply ? """
COALESCE(NULLIF(t.variable->>'$.underTakeName', ''), replyUnit.unitName) AS underTakeName,
CASE
WHEN JSON_EXTRACT(t.variable, '$.underTakeIsMaster') IS NOT NULL
THEN IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') = true, 1, 0)
ELSE IFNULL(replyUnit.isMaster, 0)
END AS underTakeIsMaster,
""" : "";
String unionJoin = requiresUnionFilter ? "LEFT JOIN vw_user vu ON vu.id = info.createUserId" : "";
String schoolLeaderJoin = isSchoolLeaderApproval ? "LEFT JOIN proposal_reply_unit pru ON pru.proposalId = info.id" : "";
String unitReplyJoin = isUnitReply
? "LEFT JOIN proposal_reply_unit replyUnit ON replyUnit.proposalId = info.id AND replyUnit.unitId = ta.actorUnitId\n LEFT JOIN sys_user transferUser ON transferUser.id = t.variable->>'$.tf_transferUserId'"
: "";
Sql sql = Sqls.create("""
SELECT
info.*,
type.name AS typeName,
IF(mer.proposalId IS NOT NULL, 1, 0) AS merge,
tcs.fullName AS sessionName,
tcd.`name` AS delegationName,
ins.state AS instanceState,
t.finishTime,
%s
%s
%s
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName
FROM wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN proposal_info info ON info.id = ins.businessNo
LEFT JOIN proposal_type type ON type.id = info.typeId
LEFT JOIN proposal_merge mer ON mer.proposalId = info.id
LEFT JOIN teacher_congress_session tcs ON tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd ON tcd.id = info.delegationId
%s
%s
%s
$condition
""".formatted(schoolLeaderColumns, auditUserColumn, unitReplyColumns, unionJoin, schoolLeaderJoin, unitReplyJoin));
Cnd cnd = Cnd.NEW();
if (isUnitReply) {
cnd.and("t.taskName", "in", List.of("unit_reply", "two_unit_reply", "opinion_unit_reply"));
if (!"superadmin".equals(SecurityUtil.getUserLoginname())) {
cnd.and("ta.actorId", "=", SecurityUtil.getUserId());
}
} else {
cnd.and("t.taskName", "=", taskName);
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
}
if (approval) {
cnd.and("t.taskState", "in", isUnitReply
? List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.TRANSFER.getCode())
: List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode()));
} else {
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
}
if (requiresUnionFilter) {
cnd.andEX("vu.unionId", "=", pageForm.getUnionId());
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
if (!applySafeProposalListOrder(cnd, pageForm, pageCode)) {
cnd.desc("t.createdAt");
}
sql.setCondition(cnd);
return sql;
}
/** 获取页面对应的流程任务编码,页面代码不匹配时拒绝导出。 */
private String getWorkflowTaskName(String pageCode) {
Map<String, String> taskNames = Map.of(
"delegation", "delegation",
"preAudit", "preAudit",
"committeeFiling", "committee",
"committeeFilingUnit", "committeeFilingUnit",
"schoolLeaderApproval", "schoolLeader",
"feedbackEvaluation", "feedback",
"caseCheck", "caseCheck"
);
String taskName = taskNames.get(pageCode);
if (StrUtil.isBlank(taskName) && !"unitReply".equals(pageCode)) {
throw Lang.makeThrow("不支持的提案列表导出页面");
}
return taskName;
}
/** 将字典、枚举及组合字段转换为与列表页面相同的显示文本。 */
private void convertWorkflowExportValues(List<NutMap> rows, String pageCode) {
Map<String, String> caseFilingResultMap = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT")
.stream().collect(Collectors.toMap(Sys_dict::getCode, Sys_dict::getName));
Map<Integer, String> processStateMap = Arrays.stream(ProcessInstanceStateEnum.values())
.collect(Collectors.toMap(ProcessInstanceStateEnum::getCode, ProcessInstanceStateEnum::getMessage));
for (NutMap row : rows) {
if ("schoolLeaderApproval".equals(pageCode)) {
row.put("undertakeUnits", formatUndertakeUnits(row));
}
// 所有含是否并案列的办理页面统一导出/避免未并案记录出现空白
row.put("merge", row.getInt("merge") == 1 ? "" : "");
if ("unitReply".equals(pageCode)) {
row.put("underTakeIsMaster", row.getInt("underTakeIsMaster") == 1 ? "主办" : "协办");
}
row.put("caseFilingResult", caseFilingResultMap.getOrDefault(row.getString("caseFilingResult"), row.getString("caseFilingResult")));
Integer instanceState = row.getInt("instanceState");
row.put("instanceState", processStateMap.getOrDefault(instanceState, row.getString("instanceState")));
}
}
/** 校领导审批页按页面展示规则拼接承办单位。 */
private String formatUndertakeUnits(NutMap row) {
String masterUnitNames = StrUtil.nullToEmpty(row.getString("masterUnitName"));
String slaveUnitNames = StrUtil.nullToEmpty(row.getString("slaveUnitNames"));
if ("SUGGESTION".equals(row.getString("caseFilingResult"))) {
return StrUtil.isNotBlank(masterUnitNames) ? masterUnitNames : slaveUnitNames;
}
if ("CONFIRM_FILING".equals(row.getString("caseFilingResult"))) {
List<String> unitNames = new ArrayList<>();
if (StrUtil.isNotBlank(masterUnitNames)) {
unitNames.add("主办:" + masterUnitNames);
}
if (StrUtil.isNotBlank(slaveUnitNames)) {
unitNames.add("协办:" + slaveUnitNames);
}
return StrUtil.join("", unitNames);
}
return "";
}
/** 根据页面列定义过滤导出字段,确保导出范围只包含列表可展示字段。 */
private List<ExportTableColumns> getWorkflowExportColumns(String pageCode, ExportTableColumns[] tableColumns) {
Set<String> allowedColumns = WORKFLOW_EXPORT_COLUMNS.get(pageCode);
if (allowedColumns == null || tableColumns == null) {
return List.of();
}
return Arrays.stream(tableColumns)
.filter(Objects::nonNull)
.filter(column -> StrUtil.isNotBlank(column.getLabel()) && allowedColumns.contains(column.getProp()))
.collect(Collectors.toList());
}
/** 初始化各办理页面可导出的列表字段白名单。 */
private static Map<String, Set<String>> createWorkflowExportColumns() {
Set<String> commonColumns = Set.of("code", "caseFilingCode", "name", "createUserName", "typeName",
"sessionName", "delegationName", "curTaskName", "instanceState");
Map<String, Set<String>> columns = new HashMap<>();
columns.put("delegation", commonColumns);
columns.put("preAudit", commonColumns);
columns.put("caseCheck", Set.of("code", "caseFilingCode", "name", "createUserName", "typeName",
"sessionName", "delegationName", "curTaskName", "instanceState", "finishTime"));
columns.put("committeeFiling", Set.of("code", "caseFilingCode", "name", "typeName", "sessionName",
"caseFilingResult", "delegationName", "curTaskName", "instanceState"));
columns.put("committeeFilingUnit", Set.of("code", "caseFilingCode", "name", "typeName", "sessionName",
"caseFilingResult", "delegationName", "curTaskName", "instanceState"));
columns.put("schoolLeaderApproval", Set.of("code", "caseFilingCode", "name", "typeName", "undertakeUnits",
"merge", "curTaskName", "auditUser", "instanceState"));
columns.put("unitReply", Set.of("code", "caseFilingCode", "name", "typeName", "caseFilingResult",
"merge", "underTakeName", "underTakeIsMaster", "curTaskName", "auditUser", "instanceState"));
columns.put("feedbackEvaluation", Set.of("code", "caseFilingCode", "name", "typeName", "merge",
"curTaskName", "instanceState"));
return Collections.unmodifiableMap(columns);
}
/**
* 按白名单字段对统计结果执行后端排序数字按数值比较文本按中文区域规则比较
*
* @param rows 统计结果行
* @param pageForm 排序字段及方向参数
* @param allowedOrderColumns 允许排序的统计字段
*/
@Override
public void sortStatisticsRows(List<NutMap> rows, PageForm pageForm, Set<String> allowedOrderColumns) {
if (rows == null || pageForm == null || allowedOrderColumns == null) {
return;
}
String orderName = pageForm.getPageOrderName();
String orderBy = pageForm.getPageOrderBy();
boolean ascending = "ascending".equals(orderBy);
boolean descending = "descending".equals(orderBy);
if (StrUtil.isBlank(orderName) || !allowedOrderColumns.contains(orderName) || (!ascending && !descending)) {
return;
}
// 空值始终排在末尾降序只反转有效值的比较结果
rows.sort((leftRow, rightRow) -> {
Object leftValue = leftRow.get(orderName);
Object rightValue = rightRow.get(orderName);
if (leftValue == null && rightValue == null) {
return 0;
}
if (leftValue == null) {
return 1;
}
if (rightValue == null) {
return -1;
}
int result = compareStatisticsValue(leftValue, rightValue);
return descending ? -result : result;
});
}
/**
* 比较统计字段值兼容不同整数类型以及中文文本
*
* @param leftValue 左侧字段值
* @param rightValue 右侧字段值
* @return 标准比较结果
*/
private int compareStatisticsValue(Object leftValue, Object rightValue) {
if (leftValue instanceof Number && rightValue instanceof Number) {
BigDecimal leftNumber = new BigDecimal(leftValue.toString());
BigDecimal rightNumber = new BigDecimal(rightValue.toString());
return leftNumber.compareTo(rightNumber);
}
String leftText = String.valueOf(leftValue);
String rightText = String.valueOf(rightValue);
String leftPercent = StrUtil.removeSuffix(leftText, "%");
String rightPercent = StrUtil.removeSuffix(rightText, "%");
// 百分比展示值按实际数值比较避免字符串顺序导致10%排在9%之前
if (leftText.endsWith("%") && rightText.endsWith("%")
&& NumberUtil.isNumber(leftPercent) && NumberUtil.isNumber(rightPercent)) {
return new BigDecimal(leftPercent).compareTo(new BigDecimal(rightPercent));
}
return Collator.getInstance(Locale.CHINA).compare(leftText, rightText);
}
@Override
public NutMap info(String id) {
if (StrUtil.isBlank(id)) {
@@ -228,8 +632,11 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
}
// 按任务节点分组
Map<String, List<ProcessTaskVO>> taskGroups = doneTaskVos.stream().collect(Collectors.groupingBy(ProcessTaskVO::getDisplayName));
Map<String, List<ProcessTaskVO>> taskGroups = doneTaskVos.stream()
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(ProcessTaskVO::getDisplayName));
docData.putAll(taskGroups);
putProposalExportTaskAliases(id, taskGroups, docData);
// 提案附议
List<NutMap> secondInfos = new ArrayList<>();
@@ -300,22 +707,79 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
Configure config = Configure.builder()
.bind("secondedList", policy)
.bind("seconders", policy)
.bind("提案附议", policy)
.bind("提案委委员审议", policy)
.bind("brief", htmlRenderPolicy)
.bind("measures", htmlRenderPolicy)
.bind("taskFormData.tf_opinion", htmlRenderPolicy)
.build();
Map<String, Object> renderDocCellDataMap = MapUtil.of("proposal", docCellDataMap);
renderDocCellDataMap.put("secondedList", secondInfos);
renderDocCellDataMap.put("delegationAuditList", delegationAuditList);
// 数据库模板使用平铺占位符例如 {{code}}{{sessionName}}不能将基础字段嵌套到 proposal 节点
// 流程节点分组已写入 docData附议人与代表团审核列表也必须写入同一上下文才能被模板的循环标签识别
docData.put("secondedList", secondInfos);
docData.put("delegationAuditList", delegationAuditList);
try {
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("proposal"), config).render(renderDocCellDataMap).writeAndClose(byteArrayOutputStream);
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("proposal"), config).render(docData).writeAndClose(byteArrayOutputStream);
} catch (IOException e) {
log.error("提案导出失败{},提案id:{}", e.getMessage(), id);
}
}
/**
* 将现行工作流节点映射到历史提案表模板使用的区块名称并按主协办单位拆分办理答复
* 模板仍使用旧节点名称时避免已完成的意见和答复因键名不一致而无法渲染
*
* @param proposalId 提案ID用于查询主办单位
* @param taskGroups 已完成任务按显示名称分组后的数据
* @param docData Word 模板的平铺渲染数据
*/
private void putProposalExportTaskAliases(String proposalId, Map<String, List<ProcessTaskVO>> taskGroups,
NutMap docData) {
putTaskAlias(docData, "提案委主任审核", taskGroups.get("校工会预审核"));
putTaskAlias(docData, "提案委主任意见", taskGroups.get("提案委员会立案"));
putTaskAlias(docData, "提案委委员审议", taskGroups.get("委员会确认承办单位"));
List<ProcessTaskVO> replyTasks = taskGroups.get("承办单位答复");
if (Lang.isEmpty(replyTasks)) {
return;
}
Sql masterUnitSql = Sqls.create("SELECT unitName FROM proposal_reply_unit WHERE proposalId = @proposalId AND isMaster = 1")
.setParam("proposalId", proposalId);
List<NutMap> masterUnitRows = listMap(masterUnitSql);
Set<String> masterUnitNames = masterUnitRows.stream()
.map(row -> row.getString("unitName"))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
List<ProcessTaskVO> masterReplyTasks = new ArrayList<>();
List<ProcessTaskVO> slaveReplyTasks = new ArrayList<>();
for (ProcessTaskVO replyTask : replyTasks) {
Dict taskFormData = replyTask.getTaskFormData();
String unitName = taskFormData == null ? null : taskFormData.getStr("unitName");
if (masterUnitNames.contains(unitName)) {
masterReplyTasks.add(replyTask);
} else {
slaveReplyTasks.add(replyTask);
}
}
putTaskAlias(docData, "主办单位答复", masterReplyTasks);
putTaskAlias(docData, "协办单位答复", slaveReplyTasks);
}
/**
* 仅在存在已完成任务时写入模板区块空任务不触发 Word 条件标签
*
* @param docData Word 模板的平铺渲染数据
* @param templateKey Word 模板中的条件或循环标签名称
* @param tasks 对应的已完成流程任务
*/
private void putTaskAlias(NutMap docData, String templateKey, List<ProcessTaskVO> tasks) {
if (Lang.isNotEmpty(tasks)) {
docData.put(templateKey, tasks);
}
}
@Override
public void exportProposalFeedBackAsDocx(String id, ByteArrayOutputStream byteArrayOutputStream) {
if (id == null || id.isEmpty()) {
@@ -76,6 +76,8 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
throw new BaseException("没有进行中的流程任务");
}
validateAndNormalizeCaseFilingCode(args);
String proposalId = args.getStr("proposalId");
List<String> proposalIds = proposalCommonService.mergeProposal(proposalId);
if (ObjectUtil.isEmpty(proposalIds)) {
@@ -98,16 +100,44 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
syncCommitteeFilingUnitData(proposalIds, args);
}
/**
* old-flow 页面提交后需要把立案结果以及主协办单位同步落业务表
* 校验并规范化立案编号确定立案时必须传入不超过 20 个字符的编号
* 其他立案结果不保存编号避免切换选项后把历史输入带入业务表和流程变量
*
* @param args 流程提交参数其中 tf_caseFilingResult 为立案结果tf_caseFilingCode 为手工填写的立案编号
*/
private void validateAndNormalizeCaseFilingCode(Dict args) {
String caseFilingResultKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult";
String caseFilingCodeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode";
if (!"CONFIRM_FILING".equals(args.getStr(caseFilingResultKey))) {
args.put(caseFilingCodeKey, "");
return;
}
String caseFilingCode = StrUtil.trim(args.getStr(caseFilingCodeKey));
if (StrUtil.isBlank(caseFilingCode)) {
throw new BaseException("立案编号不能为空");
}
if (caseFilingCode.length() > 20) {
throw new BaseException("立案编号不能超过20个字符");
}
args.put(caseFilingCodeKey, caseFilingCode);
}
/**
* old-flow 页面提交后需要把立案编号立案结果以及主协办单位同步落业务表
* 若这次改成不予立案等无需承办单位的结果也要先清掉旧的承办单位记录避免脏数据残留
*/
private void syncCommitteeFilingUnitData(List<String> proposalIds, Dict args) {
String caseFilingCode = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode");
String caseFilingResult = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult");
String caseFilingType = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingType");
dao().update(ProposalInfo.class,
Chain.make("caseFilingResult", caseFilingResult).add("caseFilingType", caseFilingType),
Chain.make("caseFilingCode", caseFilingCode)
.add("caseFilingResult", caseFilingResult)
.add("caseFilingType", caseFilingType),
Cnd.where(ProposalInfo::getId, "in", proposalIds));
dao().clear(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds));
@@ -4,8 +4,10 @@ import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
@@ -27,7 +29,15 @@ import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xwpf.usermodel.BreakType;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
@@ -51,6 +61,8 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -61,6 +73,9 @@ import java.util.zip.ZipOutputStream;
@Slf4j
public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalExportService {
private static final String WORK_SUGGESTION_SECTION_MARKER = "__WORK_SUGGESTION_SECTION__";
private static final int UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT = 8;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
@@ -246,6 +261,239 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
CommonDownloadUtil.download(title + ".xlsx", workbook, response);
}
/**
* 按办理单位生成提案承办单位表查询时忽略页面单个承办单位条件其他查询条件继续生效
* 同一提案涉及多个单位时会进入每个相关单位的 Excel并保留完整主办协办单位信息
*
* @param pageForm 综合导出页面查询参数sessionId 必须为当前教代会届次ID
* @param response HTTP 响应直接输出包含多个 XSSF Excel ZIP 文件
*/
@Override
public void exportUndertakeUnitTablesAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
if (pageForm == null || StrUtil.isBlank(pageForm.getSessionId())) {
throw new BaseException("请选择教代会");
}
Teacher_congress_session session = dao().fetch(Teacher_congress_session.class, pageForm.getSessionId());
if (session == null) {
throw new BaseException("教代会届次不存在");
}
List<NutMap> unitRows = queryUndertakeUnitExportRows(pageForm);
if (Lang.isEmpty(unitRows)) {
throw new BaseException("暂无可导出的承办单位提案数据");
}
Map<String, NutMap> proposalRows = new LinkedHashMap<>();
Map<String, String> unitNames = new LinkedHashMap<>();
Map<String, LinkedHashSet<String>> unitProposalIds = new LinkedHashMap<>();
for (NutMap row : unitRows) {
String proposalId = row.getString("id");
String unitId = row.getString("unitId");
if (StrUtil.isBlank(proposalId) || StrUtil.isBlank(unitId)) {
continue;
}
String unitName = StrUtil.blankToDefault(row.getString("unitName"), "未命名单位");
NutMap proposalRow = proposalRows.computeIfAbsent(proposalId, key -> NutMap.NEW()
.addv("id", proposalId)
.addv("code", row.getString("code"))
.addv("caseFilingCode", row.getString("caseFilingCode"))
.addv("name", row.getString("name"))
.addv("createUserName", row.getString("createUserName"))
.addv("caseFilingResult", row.getString("caseFilingResult"))
.addv("masterUnitNames", new LinkedHashSet<String>())
.addv("slaveUnitNames", new LinkedHashSet<String>()));
getUnitNameSet(proposalRow, Boolean.TRUE.equals(row.getBoolean("isMaster"))
? "masterUnitNames" : "slaveUnitNames").add(unitName);
unitNames.putIfAbsent(unitId, unitName);
unitProposalIds.computeIfAbsent(unitId, key -> new LinkedHashSet<>()).add(proposalId);
}
if (unitProposalIds.isEmpty()) {
throw new BaseException("暂无可导出的承办单位提案数据");
}
String title = StrUtil.format("{}第{}教职工代表大会第{}会议提案承办单位表",
Globals.AppName, session.getJ(), session.getC());
ByteArrayOutputStream zipBytes = new ByteArrayOutputStream();
Map<String, Integer> zipEntryNameCounts = new HashMap<>();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(zipBytes)) {
for (Map.Entry<String, LinkedHashSet<String>> unitEntry : unitProposalIds.entrySet()) {
String unitName = unitNames.get(unitEntry.getKey());
List<NutMap> excelRows = buildUndertakeUnitExcelRows(unitEntry.getValue(), proposalRows);
try (Workbook workbook = buildUndertakeUnitWorkbook(title, unitName, excelRows);
ByteArrayOutputStream workbookBytes = new ByteArrayOutputStream()) {
workbook.write(workbookBytes);
String baseEntryName = "提案承办单位表-" + sanitizeZipFileName(unitName);
int sameNameCount = zipEntryNameCounts.merge(baseEntryName, 1, Integer::sum);
String entryName = baseEntryName + (sameNameCount > 1 ? "-" + sameNameCount : "") + ".xlsx";
zipOutputStream.putNextEntry(new ZipEntry(entryName));
workbookBytes.writeTo(zipOutputStream);
zipOutputStream.closeEntry();
}
}
zipOutputStream.finish();
} catch (IOException e) {
log.error("按办理单位导出提案承办单位表失败:{}", e.getMessage(), e);
throw new BaseException("导出提案承办单位表失败");
}
CommonDownloadUtil.download(sanitizeZipFileName(title) + ".zip", zipBytes.toByteArray(), response);
}
/**
* 查询当前届次已确定立案或作为工作建议的提案及其全部办理单位
* 页面承办单位单选条件会被清空避免只生成一个单位文件其他页面条件继续沿用
*/
private List<NutMap> queryUndertakeUnitExportRows(ProposalQueryComprehensiveParam pageForm) {
ProposalQueryComprehensiveParam exportPageForm = new ProposalQueryComprehensiveParam();
BeanUtil.copyProperties(pageForm, exportPageForm);
exportPageForm.setUnderTakeUnitId(null);
Sql sql = Sqls.create("""
SELECT DISTINCT
info.id,
info.code,
info.caseFilingCode,
info.name,
info.createUserName,
info.caseFilingResult,
pru.unitId,
pru.unitName,
pru.isMaster
FROM
proposal_info info
LEFT JOIN proposal_type type ON type.id = info.typeId
LEFT JOIN teacher_congress_session tcs ON tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd ON tcd.id = info.delegationId
LEFT JOIN teacher_congress_delegate tcde ON tcde.loginName = info.createUserLoginName
INNER JOIN proposal_reply_unit pru ON pru.proposalId = info.id
$condition
""");
Cnd cnd = Cnd.where("info.sessionId", "=", pageForm.getSessionId());
cnd.and("info.caseFilingResult", "in", List.of("CONFIRM_FILING", "SUGGESTION"));
ProposalQueryComprehensiveParam.buildSearch(cnd, exportPageForm);
cnd.asc("info.code");
cnd.asc("pru.unitName");
sql.setCondition(cnd);
return listMap(sql);
}
/**
* 将某个办理单位涉及的提案整理为 Excel 确定立案在前工作建议通过标记行单独分组
*/
private List<NutMap> buildUndertakeUnitExcelRows(LinkedHashSet<String> proposalIds,
Map<String, NutMap> proposalRows) {
List<NutMap> filingRows = new ArrayList<>();
List<NutMap> suggestionRows = new ArrayList<>();
for (String proposalId : proposalIds) {
NutMap proposalRow = proposalRows.get(proposalId);
if (proposalRow == null) {
continue;
}
LinkedHashSet<String> masterUnitNames = getUnitNameSet(proposalRow, "masterUnitNames");
LinkedHashSet<String> slaveUnitNames = getUnitNameSet(proposalRow, "slaveUnitNames");
NutMap excelRow = NutMap.NEW()
.addv("code", proposalRow.getString("code"))
.addv("caseFilingCode", proposalRow.getString("caseFilingCode"))
.addv("name", proposalRow.getString("name"))
.addv("createUserName", proposalRow.getString("createUserName"))
.addv("masterUnitNames", "")
.addv("slaveUnitNames", "")
.addv("workSuggestionUnitNames", "")
.addv("remark", "");
if ("CONFIRM_FILING".equals(proposalRow.getString("caseFilingResult"))) {
excelRow.put("masterUnitNames", String.join("", masterUnitNames));
excelRow.put("slaveUnitNames", String.join("", slaveUnitNames));
filingRows.add(excelRow);
} else {
LinkedHashSet<String> workSuggestionUnitNames = new LinkedHashSet<>(masterUnitNames);
workSuggestionUnitNames.addAll(slaveUnitNames);
excelRow.put("workSuggestionUnitNames", String.join("", workSuggestionUnitNames));
suggestionRows.add(excelRow);
}
}
if (!suggestionRows.isEmpty()) {
filingRows.add(NutMap.NEW()
.addv("code", WORK_SUGGESTION_SECTION_MARKER)
.addv("caseFilingCode", "")
.addv("name", "")
.addv("createUserName", "")
.addv("masterUnitNames", "")
.addv("slaveUnitNames", "")
.addv("workSuggestionUnitNames", "")
.addv("remark", ""));
filingRows.addAll(suggestionRows);
}
return filingRows;
}
/**
* 创建单个办理单位的 XSSF Excel并把工作建议标记行合并为分组标题
*/
private Workbook buildUndertakeUnitWorkbook(String title, String unitName, List<NutMap> excelRows) {
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("提案编号", "code", 18));
exportEntities.add(new ExcelExportEntity("立案编号", "caseFilingCode", 18));
exportEntities.add(new ExcelExportEntity("提案名称", "name", 45));
exportEntities.add(new ExcelExportEntity("提案人", "createUserName", 15));
exportEntities.add(new ExcelExportEntity("主办单位", "masterUnitNames", 28));
exportEntities.add(new ExcelExportEntity("协办单位", "slaveUnitNames", 28));
exportEntities.add(new ExcelExportEntity("工作建议送达单位", "workSuggestionUnitNames", 32));
exportEntities.add(new ExcelExportEntity("备注", "remark", 20));
for (ExcelExportEntity exportEntity : exportEntities) {
exportEntity.setWrap(true);
}
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(title);
exportParams.setSecondTitle("" + unitName + "");
exportParams.setSheetName("提案承办单位表");
exportParams.setHeaderHeight(30);
exportParams.setHeight((short) 24);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, excelRows);
mergeWorkSuggestionSection(workbook);
return workbook;
}
/**
* 查找工作建议标记行将八个单元格合并并设置为居中的分组标题
*/
private void mergeWorkSuggestionSection(Workbook workbook) {
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
Cell firstCell = row.getCell(0);
if (firstCell == null || !WORK_SUGGESTION_SECTION_MARKER.equals(firstCell.toString())) {
continue;
}
CellStyle sectionStyle = workbook.createCellStyle();
sectionStyle.cloneStyleFrom(firstCell.getCellStyle());
sectionStyle.setAlignment(HorizontalAlignment.CENTER);
sectionStyle.setVerticalAlignment(VerticalAlignment.CENTER);
Font sectionFont = workbook.createFont();
sectionFont.setFontName("宋体");
sectionFont.setFontHeightInPoints((short) 11);
sectionFont.setBold(true);
sectionStyle.setFont(sectionFont);
for (int columnIndex = 0; columnIndex < UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT; columnIndex++) {
Cell cell = row.getCell(columnIndex);
if (cell == null) {
cell = row.createCell(columnIndex);
}
cell.setCellValue(columnIndex == 0 ? "作为工作建议的提案" : "");
cell.setCellStyle(sectionStyle);
}
row.setHeightInPoints(24);
sheet.addMergedRegion(new CellRangeAddress(row.getRowNum(), row.getRowNum(), 0,
UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT - 1));
return;
}
}
/** 获取提案行中用于去重且保持顺序的办理单位名称集合。 */
@SuppressWarnings("unchecked")
private LinkedHashSet<String> getUnitNameSet(NutMap proposalRow, String key) {
return (LinkedHashSet<String>) proposalRow.get(key);
}
@Override
public void exportProposalRegisterSummaryAsExcel(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
Sql sql = exportComprehensiveSql(pageForm);
@@ -4,6 +4,7 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.models.Sys_user;
@@ -11,6 +12,7 @@ import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.constants.ProposalState;
import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalWriteTypeVO;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalType;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalWriteService;
@@ -24,6 +26,7 @@ import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Strings;
import java.util.ArrayList;
@@ -44,6 +47,70 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl<ProposalInfo> impl
super(dao);
}
@Override
public List<ProposalWriteTypeVO> listSessionProposalTypes(String sessionId) {
List<String> typeNames = getSessionProposalTypeNames(sessionId);
if (typeNames.isEmpty()) {
return List.of();
}
List<ProposalType> proposalTypes = dao().query(ProposalType.class,
Cnd.where(ProposalType::getName, "in", typeNames));
List<ProposalWriteTypeVO> result = new ArrayList<>();
for (String typeName : typeNames) {
ProposalType proposalType = proposalTypes.stream()
.filter(item -> typeName.equals(item.getName()))
.findFirst()
.orElse(null);
if (proposalType != null) {
result.add(new ProposalWriteTypeVO(proposalType.getId(), proposalType.getName()));
}
}
return result;
}
@Override
public void validateProposalType(String sessionId, Integer typeId) {
if (typeId == null) {
throw new BaseException("请选择提案类型");
}
ProposalType proposalType = dao().fetch(ProposalType.class, typeId);
if (proposalType == null || !getSessionProposalTypeNames(sessionId).contains(proposalType.getName())) {
throw new BaseException("所选提案类型不属于当前教代会届次");
}
}
/**
* 解析指定届次配置的提案类型标准数据为 JSON 数组同时兼容历史单值字符串
*
* @param sessionId 教代会届次ID
* @return 按届次配置顺序排列的提案类型名称未配置时返回空列表
*/
private List<String> getSessionProposalTypeNames(String sessionId) {
if (StrUtil.isBlank(sessionId)) {
throw new BaseException("所属教代会不能为空");
}
Teacher_congress_session session = dao().fetch(Teacher_congress_session.class, sessionId);
if (session == null) {
throw new BaseException("所属教代会不存在");
}
if (StrUtil.isBlank(session.getProposalType())) {
return List.of();
}
String proposalTypeValue = StrUtil.trim(session.getProposalType());
if (!proposalTypeValue.startsWith("[")) {
return List.of(proposalTypeValue);
}
try {
return Json.fromJsonAsList(String.class, proposalTypeValue).stream()
.map(StrUtil::trim)
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
} catch (Exception e) {
throw new BaseException("教代会届次提案类型配置格式不正确");
}
}
@Override
public List<Sys_dict> listSource(String sessionId) {
Sql sql = Sqls.create("""
@@ -118,7 +185,11 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl<ProposalInfo> impl
@Override
public List<Sys_dict> listSourceByCode(String code) {
Sys_dict dict = sysDictService.fetch(Cnd.where("code", "=", code));
return dict == null ? new ArrayList<>() : sysDictService.query(Cnd.where("parentId", "=", Strings.sNull(dict.getId())).asc("location"));
return dict == null ? new ArrayList<>() : sysDictService.query(
Cnd.NEW().where("parentId", "=", Strings.sNull(dict.getId()))
.and("disabled","=",0)
.asc("location")
);
}
@Override
@@ -220,6 +291,7 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl<ProposalInfo> impl
throw new RuntimeException("没有开启的教代会");
}
Teacher_congress_session teacherCongressSession = teacherCongressSessions.get(0);
validateProposalType(teacherCongressSession.getId(), proposalTypeObj.getId());
// 保存到数据库
ProposalInfo proposalInfo = new ProposalInfo();
@@ -7,7 +7,6 @@ import cn.hutool.core.lang.tree.TreeNode;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
@@ -20,15 +19,14 @@ import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation_union;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation_unit;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.service.TeacherCongressDelegationService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.aop.Aop;
@@ -43,7 +41,6 @@ import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@IocBean
@At("/platform/teacherCongress/delegation")
@@ -63,6 +60,9 @@ public class TeacherCongressDelegationController {
@Inject
private SysRoleService sysRoleService;
@Inject
private TeacherCongressDelegationService teacherCongressDelegationService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/teachercongress/delegation/index.html")
@SaCheckPermission("tc.delegation")
@@ -288,97 +288,50 @@ public class TeacherCongressDelegationController {
}
/**
* 查询团长
* 查询代表团负责人副团长返回多条以兼容V3多个副团长
*
* @param sessionId
* @param delegationId
* @return
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param type 负责人角色编码
* @return 负责人列表
*/
@At
@SaCheckPermission("tc.delegation")
public Result headUser(@Valid String sessionId, @Valid String delegationId, @Param(value = "type", df = "TEACHER_CONGRESS_DELEGATION_HEAD") String type) {
Sys_role role = null;
if (type.equals(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD.name())) {
role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
} else if (type.equals(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD.name())) {
role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD);
} else if (type.equals(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT.name())) {
role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT);
} else {
throw new BaseException("参数错误");
}
Record record = dao.fetch("sys_user_role", Cnd.where("tcSessionId", "=", sessionId).and("tcDelegationId", "=", delegationId).and("roleId", "=", role.getId()));
String userId = Optional.ofNullable(record).map(r -> r.getString("userId")).orElse(null);
Sql sql = Sqls.create("""
SELECT
u.id as userId,
u.loginname as loginName,
u.username as userName,
u.mobile,
n.name as unitName
FROM
sys_user u
LEFT JOIN sys_unit n on n.id = u.unitId
WHERE
u.id = @userId
""");
sql.setParam("userId", userId);
sql.setCallback(Sqls.callback.map());
dao.execute(sql);
NutMap user = (NutMap) sql.getResult();
return Result.success().addData(user);
List<NutMap> users = teacherCongressDelegationService.listHeadUsers(sessionId, delegationId, type);
return Result.success().addData(users);
}
/**
* 设置团长
* 设置代表团团长副团长和联络人
*
* @param delegationId 代表团ID
* @param sessionId 届次ID
* @param userId 团长id
* @param viceUserId 副团长id
* @param contactUserId 联络人
* @return
* @param userId 团长ID
* @param viceUserIds 副团长ID数组
* @param contactUserId 联络人ID
* @return 操作结果
*/
@At
@SaCheckPermission("tc.delegation")
@Aop(TransAop.READ_COMMITTED)
public Result insertHead(@Valid String delegationId, @Valid String sessionId, @Valid String userId, @Valid String viceUserId, String contactUserId) {
// 团长
Sys_role role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId).and(Sys_user_role::getTcSessionId, "=", sessionId).and(Sys_user_role::getRoleId, "=", role.getId()));
dao.insert("sys_user_role", Chain.make("userId", userId).add("roleId", role.getId()).add("tcDelegationId", delegationId).add("tcSessionId", sessionId));
// 副团长
Sys_role role2 = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD);
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId).and(Sys_user_role::getTcSessionId, "=", sessionId).and(Sys_user_role::getRoleId, "=", role2.getId()));
dao.insert("sys_user_role", Chain.make("userId", viceUserId).add("roleId", role2.getId()).add("tcDelegationId", delegationId).add("tcSessionId", sessionId));
// 联络人
Sys_role role3 = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT);
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId).and(Sys_user_role::getTcSessionId, "=", sessionId).and(Sys_user_role::getRoleId, "=", role3.getId()));
dao.insert("sys_user_role", Chain.make("userId", contactUserId).add("roleId", role3.getId()).add("tcDelegationId", delegationId).add("tcSessionId", sessionId));
sysUserService.clearCache();
public Result insertHead(@Valid String delegationId, @Valid String sessionId, @Valid String userId, @Param("viceUserIds") String[] viceUserIds, String contactUserId) {
teacherCongressDelegationService.setHeadUsers(sessionId, delegationId, userId, viceUserIds, contactUserId);
return Result.success();
}
/**
* 删除团长角色
* 删除代表团负责人角色
*
* @param delegationId
* @param sessionId
* @param userId
* @return
* @param delegationId 代表团ID
* @param sessionId 届次ID
* @param userId 用户ID
* @param type 负责人角色编码
* @return 操作结果
*/
@At
@SaCheckPermission("tc.delegation")
public Result deleteHead(@Valid String delegationId, @Valid String sessionId, @Valid String userId) {
Sys_role role = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
//先删除角色
dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId).and(Sys_user_role::getTcSessionId, "=", sessionId).and(Sys_user_role::getRoleId, "=", role.getId()).and(Sys_user_role::getUserId, "=", userId));
sysUserService.clearCache();
public Result deleteHead(@Valid String delegationId, @Valid String sessionId, @Valid String userId, @Param(value = "type", df = "TEACHER_CONGRESS_DELEGATION_HEAD") String type) {
teacherCongressDelegationService.deleteHeadUser(sessionId, delegationId, userId, type);
return Result.success();
}
}
@@ -0,0 +1,44 @@
package com.budwk.app.zhgh.democratic.teachercongress.delegation.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import org.nutz.lang.util.NutMap;
import java.util.List;
/**
* 教代会代表团业务服务
*/
public interface TeacherCongressDelegationService extends BaseService<Teacher_congress_delegation> {
/**
* 按角色查询代表团负责人信息
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param type 负责人角色编码
* @return 负责人用户列表
*/
List<NutMap> listHeadUsers(String sessionId, String delegationId, String type);
/**
* 设置代表团团长副团长和联络人
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param userId 团长用户ID
* @param viceUserIds 副团长用户ID列表
* @param contactUserId 联络人用户ID
*/
void setHeadUsers(String sessionId, String delegationId, String userId, String[] viceUserIds, String contactUserId);
/**
* 删除代表团指定负责人角色
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param userId 用户ID
* @param type 负责人角色编码
*/
void deleteHeadUser(String sessionId, String delegationId, String userId, String type);
}
@@ -0,0 +1,164 @@
package com.budwk.app.zhgh.democratic.teachercongress.delegation.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_role;
import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.service.TeacherCongressDelegationService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import java.util.Arrays;
import java.util.List;
/**
* 教代会代表团业务服务实现
*/
@IocBean(args = {"refer:dao"})
public class TeacherCongressDelegationServiceImpl extends BaseServiceImpl<Teacher_congress_delegation> implements TeacherCongressDelegationService {
@Inject
private SysRoleService sysRoleService;
@Inject
private SysUserService sysUserService;
public TeacherCongressDelegationServiceImpl(Dao dao) {
super(dao);
}
/**
* 按角色查询代表团负责人信息副团长允许返回多条记录以兼容V3多副团长数据
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param type 负责人角色编码
* @return 负责人用户列表
*/
@Override
public List<NutMap> listHeadUsers(String sessionId, String delegationId, String type) {
Sys_role role = getHeadRole(type);
Sql sql = Sqls.create("""
SELECT
u.id as userId,
u.loginname as loginName,
u.username as userName,
u.mobile,
n.name as unitName
FROM
sys_user_role sur
LEFT JOIN sys_user u ON u.id = sur.userId
LEFT JOIN sys_unit n ON n.id = u.unitId
WHERE
sur.tcSessionId = @sessionId
AND sur.tcDelegationId = @delegationId
AND sur.roleId = @roleId
ORDER BY u.loginname
""");
sql.setParam("sessionId", sessionId);
sql.setParam("delegationId", delegationId);
sql.setParam("roleId", role.getId());
return listMap(sql);
}
/**
* 设置代表团团长副团长和联络人保存前按角色清理旧值避免同一角色残留脏数据
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param userId 团长用户ID
* @param viceUserIds 副团长用户ID列表
* @param contactUserId 联络人用户ID
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void setHeadUsers(String sessionId, String delegationId, String userId, String[] viceUserIds, String contactUserId) {
// 团长仍然保持单人设置
Sys_role headRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
replaceRoleUsers(sessionId, delegationId, headRole.getId(), new String[]{userId});
// 副团长支持多人设置完整保留V3迁移后的多个副团长关系
Sys_role viceHeadRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD);
replaceRoleUsers(sessionId, delegationId, viceHeadRole.getId(), viceUserIds);
// 联络人保持单人设置未选择时只清理旧联络人
Sys_role contactRole = sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT);
replaceRoleUsers(sessionId, delegationId, contactRole.getId(), new String[]{contactUserId});
sysUserService.clearCache();
}
/**
* 删除代表团指定负责人角色按角色类型精确删除避免删除副团长时影响团长
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param userId 用户ID
* @param type 负责人角色编码
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteHeadUser(String sessionId, String delegationId, String userId, String type) {
Sys_role role = getHeadRole(type);
dao().clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", role.getId())
.and(Sys_user_role::getUserId, "=", userId));
sysUserService.clearCache();
}
/**
* 根据负责人类型解析系统角色集中校验避免 controller 层出现业务分支
*
* @param type 负责人角色编码
* @return 系统角色
*/
private Sys_role getHeadRole(String type) {
if (RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD.name().equals(type)) {
return sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_HEAD);
}
if (RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD.name().equals(type)) {
return sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_VICE_DELEGATION_HEAD);
}
if (RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT.name().equals(type)) {
return sysRoleService.getByCode(RoleConstant.TEACHER_CONGRESS_DELEGATION_CONTACT);
}
throw new BaseException("参数错误");
}
/**
* 替换指定角色用户关系先清理再按去重后的用户ID批量插入
*
* @param sessionId 届次ID
* @param delegationId 代表团ID
* @param roleId 角色ID
* @param userIds 用户ID数组
*/
private void replaceRoleUsers(String sessionId, String delegationId, String roleId, String[] userIds) {
dao().clear(Sys_user_role.class, Cnd.where(Sys_user_role::getTcDelegationId, "=", delegationId)
.and(Sys_user_role::getTcSessionId, "=", sessionId)
.and(Sys_user_role::getRoleId, "=", roleId));
if (userIds == null || userIds.length == 0) {
return;
}
Arrays.stream(userIds)
.filter(StrUtil::isNotBlank)
.distinct()
.forEach(userId -> dao().insert("sys_user_role", Chain.make("userId", userId)
.add("roleId", roleId)
.add("tcDelegationId", delegationId)
.add("tcSessionId", sessionId)));
}
}
@@ -1,7 +1,6 @@
package com.budwk.app.zhgh.democratic.teachercongress.prepare.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
@@ -61,6 +60,18 @@ public class TeacherCongressSessionController {
return Result.success(pagination);
}
/**
* 查询历史届次已保存的提案类型不需要请求参数
*
* @return VO 列表每项的 proposalType 字段表示一个下拉选项
*/
@At
@SaCheckPermission("tc.prepare.session")
@ApiOperation(value = "查询届次提案类型选项")
public Result listProposalTypes() {
return Result.success(teacherCongressSessionService.listProposalTypes());
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tc.prepare.session")
@@ -74,17 +85,8 @@ public class TeacherCongressSessionController {
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tc.prepare.session")
@ApiOperation(value = "修改届次信息")
public Result update(Teacher_congress_session session) {
int count = dao.count(Teacher_congress_session.class,
Cnd.where(Teacher_congress_session::getJ, "=", session.getJ())
.and(Teacher_congress_session::getC, "=", session.getC())
.and(Teacher_congress_session::getId, "!=", session.getId())
);
if (count > 0) {
throw new BaseException("请勿重复创建");
}
session.setFullName("" + session.getJ() + "" + session.getC());
dao.updateIgnoreNull(session);
public Result update(@Valid Teacher_congress_session session) {
teacherCongressSessionService.updateSession(session);
return Result.success();
}
@@ -0,0 +1,18 @@
package com.budwk.app.zhgh.democratic.teachercongress.prepare.controller.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 届次提案类型下拉选项
*/
@Data
@AllArgsConstructor
@ApiModel("届次提案类型选项")
public class TeacherCongressSessionProposalTypeVO {
@ApiModelProperty("提案类型,自定义类型保存后也会成为后续届次的可选项")
private String proposalType;
}
@@ -43,6 +43,13 @@ public class Teacher_congress_session extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 20)
private String fullName;
@Column
@Comment("提案类型(JSON数组)")
@ColDefine(type = ColType.VARCHAR, width = 1000)
@NotBlank(message = "提案类型不能为空")
@Size(max = 1000, message = "提案类型数据不能超过1000个字符")
private String proposalType;
@Column
@Comment("教代会开启时间")
@ColDefine(type = ColType.DATE)
@@ -1,14 +1,31 @@
package com.budwk.app.zhgh.democratic.teachercongress.prepare.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.controller.vo.TeacherCongressSessionProposalTypeVO;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session;
import java.util.List;
/**
* 教代会届次
*/
public interface TeacherCongressSessionService extends BaseService<Teacher_congress_session> {
/**
* 查询届次中已经保存过的提案类型供可输入下拉框复用
*
* @return 提案类型 VO 列表每项的 proposalType 字段为一个可选类型
*/
List<TeacherCongressSessionProposalTypeVO> listProposalTypes();
void insertSession(Teacher_congress_session session, boolean isExtend);
/**
* 修改届次基础信息并统一校验多选提案类型
*
* @param session 届次信息其中 proposalType JSON 字符串数组
*/
void updateSession(Teacher_congress_session session);
void deleteSession(String id);
}
@@ -16,6 +16,7 @@ import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalType;
import com.budwk.app.zhgh.democratic.teachercongress.delegate.models.Teacher_congress_delegate;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation_union;
@@ -24,19 +25,25 @@ import com.budwk.app.zhgh.democratic.teachercongress.institution.models.Teacher_
import com.budwk.app.zhgh.democratic.teachercongress.institution.models.Teacher_congress_institution_user;
import com.budwk.app.zhgh.democratic.teachercongress.meetings.models.Teacher_congress_meeting;
import com.budwk.app.zhgh.democratic.teachercongress.meetings.models.Teacher_congress_meeting_file;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.controller.vo.TeacherCongressSessionProposalTypeVO;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_prepare_file;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session;
import com.budwk.app.zhgh.democratic.teachercongress.prepare.service.TeacherCongressSessionService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.random.R;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -52,10 +59,36 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl<Teacher_c
super(dao);
}
/**
* 读取历史届次中非空的提案类型并去重用户既可复用已有类型也可在前端输入新类型
*
* @return 提案类型 VO 列表每项只包含 proposalType 字段
*/
@Override
public List<TeacherCongressSessionProposalTypeVO> listProposalTypes() {
Sql sql = Sqls.create("SELECT DISTINCT proposalType FROM teacher_congress_session " +
"WHERE proposalType IS NOT NULL AND TRIM(proposalType) <> '' ORDER BY proposalType");
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
LinkedHashSet<String> proposalTypes = new LinkedHashSet<>();
for (String proposalTypeValue : sql.getList(String.class)) {
for (String proposalType : parseProposalTypes(proposalTypeValue)) {
String normalizedProposalType = StrUtil.trim(proposalType);
if (StrUtil.isNotBlank(normalizedProposalType)) {
proposalTypes.add(normalizedProposalType);
}
}
}
return proposalTypes.stream()
.sorted()
.map(TeacherCongressSessionProposalTypeVO::new)
.toList();
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void insertSession(Teacher_congress_session session, boolean isExtend) {
normalizeAndValidateProposalTypes(session);
int count = count(Cnd.where(Teacher_congress_session::getJ, "=", session.getJ())
.and(Teacher_congress_session::getC, "=", session.getC()));
if (count > 0) {
@@ -239,6 +272,114 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl<Teacher_c
}
}
/**
* 修改届次基础信息提案类型在保存前统一完成 JSON 解析去重和长度校验
* 避免新增与编辑使用不同的数据格式
*
* @param session 届次信息其中 proposalType 应传 JSON 字符串数组
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateSession(Teacher_congress_session session) {
normalizeAndValidateProposalTypes(session);
int count = count(Cnd.where(Teacher_congress_session::getJ, "=", session.getJ())
.and(Teacher_congress_session::getC, "=", session.getC())
.and(Teacher_congress_session::getId, "!=", session.getId()));
if (count > 0) {
throw new BaseException("请勿重复创建");
}
session.setFullName("" + session.getJ() + "" + session.getC());
dao().updateIgnoreNull(session);
}
/**
* 将多选提案类型规范化为 JSON 数组字符串每个类型去除首尾空格去重且最多 50 个字符
* 整体序列化结果最多 1000 个字符
*
* @param session 待新增或修改的届次实体
*/
private void normalizeAndValidateProposalTypes(Teacher_congress_session session) {
List<String> values = parseProposalTypes(session.getProposalType());
LinkedHashSet<String> normalizedValues = new LinkedHashSet<>();
for (String value : values) {
String normalizedValue = StrUtil.trim(value);
if (StrUtil.isBlank(normalizedValue)) {
continue;
}
if (normalizedValue.length() > 50) {
throw new BaseException("单个提案类型不能超过50个字符");
}
normalizedValues.add(normalizedValue);
}
if (normalizedValues.isEmpty()) {
throw new BaseException("请至少选择或输入一个提案类型");
}
String proposalTypeJson = Json.toJson(new ArrayList<>(normalizedValues));
if (proposalTypeJson.length() > 1000) {
throw new BaseException("提案类型数据不能超过1000个字符");
}
session.setProposalType(proposalTypeJson);
syncProposalTypes(normalizedValues);
}
/**
* 将届次中新输入的类型同步到全局提案类型表以便继续使用 proposal_info.typeId
* 以及现有列表统计和导出的关联查询已有同名类型直接复用不删除历史类型
*
* @param proposalTypeNames 当前届次规范化去重后的提案类型名称
*/
private void syncProposalTypes(LinkedHashSet<String> proposalTypeNames) {
ProposalType lastType = dao().fetch(ProposalType.class, Cnd.NEW().desc(ProposalType::getSort));
int nextSort = lastType == null || lastType.getSort() == null ? 1 : lastType.getSort() + 1;
for (String proposalTypeName : proposalTypeNames) {
ProposalType proposalType = dao().fetch(ProposalType.class,
Cnd.where(ProposalType::getName, "=", proposalTypeName));
if (proposalType != null) {
continue;
}
ProposalType newProposalType = new ProposalType();
newProposalType.setCode(generateProposalTypeCode());
newProposalType.setName(proposalTypeName);
newProposalType.setSort(nextSort++);
dao().insert(newProposalType);
}
}
/**
* 生成不超过 10 个字符的内部提案类型编码
*
* @return TC 开头且在 proposal_type 表中未使用的编码
*/
private String generateProposalTypeCode() {
String code;
do {
code = "TC" + R.UU32().substring(0, 8).toUpperCase();
} while (dao().count(ProposalType.class, Cnd.where(ProposalType::getCode, "=", code)) > 0);
return code;
}
/**
* 解析届次中保存的提案类型标准数据为 JSON 字符串数组同时兼容调整前可能存在的单值数据
*
* @param proposalTypeValue 数据库字段值或前端提交的 JSON 字符串
* @return 提案类型字符串列表空值返回空列表
*/
private List<String> parseProposalTypes(String proposalTypeValue) {
if (StrUtil.isBlank(proposalTypeValue)) {
return List.of();
}
String value = proposalTypeValue.trim();
if (!value.startsWith("[")) {
return List.of(value);
}
try {
return Json.fromJsonAsList(String.class, value);
} catch (Exception e) {
throw new BaseException("提案类型数据格式不正确");
}
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteSession(String id) {
@@ -91,7 +91,13 @@ public class AidFundChangeRecordController {
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("us.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType());
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.or("us.loginname", "like", "%" + pageForm.getSearchKeyword() + "%");
seg.or("us.username", "like", "%" + pageForm.getSearchKeyword() + "%");
cnd.and(seg);
}
cnd.andEX("IFNULL(info.aidFundMemberUserType, us.aidFundMemberUserType)", "=", pageForm.getAidFundMemberUserType());
cnd.andEX("us.unitid", "=", pageForm.getUnitId());
cnd.andEX("us.unionid", "=", pageForm.getUnionId());
cnd.andEX("info.changeType", "=", pageForm.getChangeType());
@@ -98,6 +98,7 @@ public class MemberApplyBranchUnionApprovalController {
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN member_apply_record info ON info.id = ins.businessNo
LEFT JOIN vw_user u ON u.id = info.userId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
@@ -113,6 +113,7 @@ public class MemberApplySchoolUnionApprovalController {
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN member_apply_record info ON info.id = ins.businessNo
LEFT JOIN vw_user u ON u.id = info.userId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
@@ -87,6 +87,7 @@ public class MemberApplyUnionGroupApprovalController {
wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN member_apply_record info ON info.id = ins.businessNo
LEFT JOIN vw_user u ON u.id = info.userId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id AND rt.taskState = 10
@@ -34,6 +34,9 @@ public class MemberApplyPageForm extends PageForm {
private String userAttribute; //人员属性
/** 人员属性多选值,供开启多选查询的页面使用。 */
private List<String> userAttributes;
public static void buildSearch(Cnd cnd, MemberApplyPageForm pageForm) {
if (cnd == null) {
cnd = Cnd.NEW();
@@ -50,6 +53,11 @@ public class MemberApplyPageForm extends PageForm {
cnd.andEX("info.personType","in",pageForm.getPersonTypes());
cnd.andEX("info.preparedBy","in",pageForm.getPreparedBys());
cnd.andEX("u.aidFundMemberUserType","=",pageForm.getAidFundMemberUserType());
cnd.andEX("u.userAttribute","=",pageForm.getUserAttribute());
// 多选值优先使用IN查询未传多选值时保留原单值查询兼容其他调用页面
if (pageForm.getUserAttributes() != null && !pageForm.getUserAttributes().isEmpty()) {
cnd.and("u.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("u.userAttribute", "=", pageForm.getUserAttribute());
}
}
}
@@ -47,6 +47,9 @@ public class MemberChangePageForm extends PageForm {
@ApiModelProperty("人员属性")
private String userAttribute;
@ApiModelProperty("人员属性组")
private List<String> userAttributes;
public void buildSearch(Cnd cnd, String prefix){
if (cnd == null) {
@@ -66,6 +69,11 @@ public class MemberChangePageForm extends PageForm {
cnd.andEX(prefix + "preparedBy", "in", this.getPreparedBys());
cnd.andEX(prefix + "personType", "in", this.getPersonTypes());
cnd.andEX(prefix + "aidFundMemberUserType", "=", this.getAidFundMemberUserType());
cnd.andEX(prefix + "userAttribute", "=", this.getUserAttribute());
// 多选条件优先兼容仍按单值提交人员属性的旧页面
if (this.getUserAttributes() != null && !this.getUserAttributes().isEmpty()) {
cnd.and(prefix + "userAttribute", "in", this.getUserAttributes());
} else {
cnd.andEX(prefix + "userAttribute", "=", this.getUserAttribute());
}
}
}
@@ -162,10 +162,11 @@ public class MemberInfoPageForm extends PageForm {
} else if (Lang.isNotEmpty(this.getPersonTypes())) {
cnd.and(prefix + "personType", "in", this.getPersonTypes());
}
if (StrUtil.isNotBlank(this.getUserAttribute())) {
cnd.and(prefix + "userAttribute", "=", this.getUserAttribute());
} else if (Lang.isNotEmpty(this.getUserAttributes())) {
// 多选条件优先兼容仍按单值提交人员属性的旧页面
if (Lang.isNotEmpty(this.getUserAttributes())) {
cnd.and(prefix + "userAttribute", "in", this.getUserAttributes());
} else if (StrUtil.isNotBlank(this.getUserAttribute())) {
cnd.and(prefix + "userAttribute", "=", this.getUserAttribute());
}
if (StrUtil.isNotBlank(this.getAidFundMemberUserType())) {
@@ -42,6 +42,9 @@ public class MemberManagePageForm extends PageForm {
private String userAttribute;
/** 人员属性多选查询条件。 */
private List<String> userAttributes;
private String changeType;
//变更状态数组
@@ -93,7 +96,12 @@ public class MemberManagePageForm extends PageForm {
cnd.andEX("u.sex", "=", pageForm.getSex());
cnd.andEX("u.personType", "=", pageForm.getPersonType());
cnd.andEX("u.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType());
cnd.andEX("u.userAttribute", "=", pageForm.getUserAttribute());
// 多选条件优先兼容仍按单值提交人员属性的旧页面
if (Lang.isNotEmpty(pageForm.getUserAttributes())) {
cnd.and("u.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("u.userAttribute", "=", pageForm.getUserAttribute());
}
if (StrUtil.isNotBlank(pageForm.getChangeDateBefore()) && StrUtil.isNotBlank(pageForm.getChangeDateEnd())) {
cnd.and(new Static(String.format("Date(his.changeTime) >= '%s' and Date(his.changeTime) <= '%s'", pageForm.getChangeDateBefore(), pageForm.getChangeDateEnd())));
@@ -148,7 +156,12 @@ public class MemberManagePageForm extends PageForm {
cnd.andEX("record.preparedBy", "=", pageForm.getPreparedBy());
cnd.andEX("record.personType", "=", pageForm.getPersonType());
cnd.andEX("record.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType());
cnd.andEX("record.userAttribute", "=", pageForm.getUserAttribute());
// 历史记录查询与当前会员查询使用一致的人员属性多选规则
if (Lang.isNotEmpty(pageForm.getUserAttributes())) {
cnd.and("record.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("record.userAttribute", "=", pageForm.getUserAttribute());
}
cnd.groupBy("record.id,task.id");
}
@@ -396,6 +396,12 @@ public class MemberInfoServiceImpl extends BaseServiceImpl<Sys_user> implements
cnd.andEX("mh.personType", "=", pageForm.getPersonType());
cnd.andEX("mh.preparedBy", "=", pageForm.getPreparedBy());
cnd.andEX("mh.userState", "=", pageForm.getUserState());
// 历史台账未复用通用 buildSearch需要在此显式处理人员属性多选条件
if (Lang.isNotEmpty(pageForm.getUserAttributes())) {
cnd.and("mh.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("mh.userAttribute", "=", pageForm.getUserAttribute());
}
if (StrUtil.isAllNotBlank(pageForm.getMemberSearchName(), pageForm.getMemberSearchKeyWord())) {
cnd.and(new SqlExpressionGroup().andLike("u." + pageForm.getMemberSearchName(), pageForm.getMemberSearchKeyWord()));
}
@@ -175,6 +175,12 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
cnd.andEX("his.unitId", "=", pageForm.getUnitId());
cnd.andEX("his.personType", "=", pageForm.getPersonType());
cnd.andEX("his.userState", "=", pageForm.getUserState());
// 变更记录查询自行拼接历史表条件需要单独补充人员属性多选及旧单值兼容
if (Lang.isNotEmpty(pageForm.getUserAttributes())) {
cnd.and("his.userAttribute", "in", pageForm.getUserAttributes());
} else {
cnd.andEX("his.userAttribute", "=", pageForm.getUserAttribute());
}
if (StrUtil.isNotBlank(pageForm.getChangeType())){
cnd.and(new Static("JSON_CONTAINS(his.changeTypes, JSON_ARRAY('%s'), '$')".formatted(pageForm.getChangeType())));
}
@@ -46,6 +46,9 @@ public class WelfareFilterUserPageForm extends PageForm {
@ApiModelProperty("人员类型")
private String[] personTypes;
@ApiModelProperty("人员属性")
private String[] userAttributes;
@ApiModelProperty("聘用方式")
private String[] preparedBys;
@@ -444,11 +444,13 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
u.userState,
u.unitId,
u.unitName,
threeUnit.name AS threeUnitName,
u.unionId,
u.unionName,
u.member
FROM
vw_user u
LEFT JOIN sys_unit threeUnit ON threeUnit.id = u.threeUnitId
LEFT JOIN welfare_list w ON u.id = w.userId AND w.projectId = @projectId
$condition
""");
@@ -471,6 +473,7 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
cnd.andEX("u.unionId", "=", pageForm.getUnionId());
cnd.andEX("u.unitId", "in", pageForm.getUnitIds());
cnd.andEX("u.personType", "in", pageForm.getPersonTypes());
cnd.andEX("u.userAttribute", "in", pageForm.getUserAttributes());
cnd.andEX("u.preparedBy", "in", pageForm.getPreparedBys());
cnd.andEX("u.userState", "in", pageForm.getUserStates());
cnd.andEX("u.member","=",pageForm.getIsMember());
@@ -523,6 +526,7 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
cnd.andEX("unionId", "=", pageForm.getUnionId());
cnd.andEX("unitId", "in", pageForm.getUnitIds());
cnd.andEX("personType", "in", pageForm.getPersonTypes());
cnd.andEX("userAttribute", "in", pageForm.getUserAttributes());
cnd.andEX("preparedBy", "in", pageForm.getPreparedBys());
cnd.andEX("userState", "in", pageForm.getUserStates());
cnd.andEX("member","=",pageForm.getIsMember());
@@ -0,0 +1,4 @@
-- 评优评先申请表新增历史导入来源和类型名称字段。
ALTER TABLE `evaluate_apply`
ADD COLUMN `source` int(2) DEFAULT 0 COMMENT '来源 0.流程申请 1.历史导入' AFTER `honorTypeId`,
ADD COLUMN `typeName` varchar(200) DEFAULT NULL COMMENT '类型名称' AFTER `source`;
@@ -0,0 +1,122 @@
-- 普惠疗休养平台电脑端菜单。
-- 使用 permission 做幂等判断,避免同一环境重复执行时插入重复菜单。
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT
'b7c63f0c73a44a9a8c4f3d3bb1a10001',
'',
LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'),
'普惠疗休养',
'Tour',
'menu',
'',
'',
'ti-map-alt',
1,
0,
'tour',
NULL,
991,
1,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
0,
'PC',
NULL,
NULL,
'p',
0,
0
FROM sys_menu
WHERE (parentId = '' OR parentId IS NULL)
AND CHAR_LENGTH(path) = 4
HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10002', p.id, CONCAT(p.path, '0001'), '疗休养设置', 'Tour Setting', 'menu', '/platform/tour/setting', 'data-pjax', '', 1, 0, 'tour.setting', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.setting') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10003', p.id, CONCAT(p.path, '0002'), '服务单位管理', 'Service Unit', 'menu', '/platform/tour/travelAgency', 'data-pjax', '', 1, 0, 'tour.travelAgency', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.travelAgency') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10004', p.id, CONCAT(p.path, '0003'), '线路管理', 'Route Manage', 'menu', '/platform/tour/route', 'data-pjax', '', 1, 0, 'tour.route', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.route') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10005', p.id, CONCAT(p.path, '0004'), '疗休养事项', 'Tour Matter', 'menu', '/platform/tour/matter', 'data-pjax', '', 1, 0, 'tour.matter', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.matter') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10006', p.id, CONCAT(p.path, '0005'), '疗休养报名', 'Tour Signup', 'menu', '/platform/tour/signup', 'data-pjax', '', 1, 0, 'tour.signup', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.signup') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10008', p.id, CONCAT(p.path, '0006'), '我的报名', 'My Signup', 'menu', '/platform/tour/mysignup', 'data-pjax', '', 1, 0, 'tour.mysignup', NULL, 6, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'w', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.mysignup') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10009', p.id, CONCAT(p.path, '0007'), '分工会查询', 'Union Ledger', 'menu', '/platform/tour/unionledger', 'data-pjax', '', 1, 0, 'tour.unionledger', NULL, 7, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.unionledger') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10011', p.id, CONCAT(p.path, '0010'), '分工会审核', 'Union Approval', 'menu', '/platform/tour/unionApproval', 'data-pjax', '', 1, 0, 'tour.unionApproval', NULL, 8, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.unionApproval') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10012', p.id, CONCAT(p.path, '0011'), '校工会审核', 'School Union Approval', 'menu', '/platform/tour/schoolUnionApproval', 'data-pjax', '', 1, 0, 'tour.schoolUnionApproval', NULL, 9, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.schoolUnionApproval') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10007', p.id, CONCAT(p.path, '0008'), '疗休养台账', 'Tour Ledger', 'menu', '/platform/tour/ledger', 'data-pjax', '', 1, 0, 'tour.ledger', NULL, 10, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.ledger') t);
INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService)
SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10010', p.id, CONCAT(p.path, '0009'), '线路成团', 'Tour Group', 'menu', '/platform/tour/group', 'data-pjax', '', 1, 0, 'tour.group', NULL, 11, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0
FROM sys_menu p
WHERE p.permission = 'tour'
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.group') t);
-- 给系统管理员默认授权,其他角色可在角色管理中按需分配。
INSERT INTO sys_role_menu (roleId, menuId)
SELECT r.id, m.id
FROM sys_role r
JOIN sys_menu m ON m.permission IN (
'tour',
'tour.setting',
'tour.travelAgency',
'tour.route',
'tour.matter',
'tour.signup',
'tour.group',
'tour.mysignup',
'tour.unionledger',
'tour.unionApproval',
'tour.schoolUnionApproval',
'tour.ledger'
)
LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id
WHERE r.code = 'SYSADMIN'
AND rm.roleId IS NULL;

Some files were not shown because too many files have changed in this diff Show More