diff --git a/src/main/java/com/budwk/app/sys/controller/SysUserApprovalOpinionController.java b/src/main/java/com/budwk/app/sys/controller/SysUserApprovalOpinionController.java index 56cace85..c316cfb6 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysUserApprovalOpinionController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysUserApprovalOpinionController.java @@ -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 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 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 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 Result,data 为意见数组,每项包含 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 opinions = new ArrayList<>(); + Set 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 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); } } diff --git a/src/main/java/com/budwk/app/sys/models/Sys_user.java b/src/main/java/com/budwk/app/sys/models/Sys_user.java index 4302b913..85a8349c 100644 --- a/src/main/java/com/budwk/app/sys/models/Sys_user.java +++ b/src/main/java/com/budwk/app/sys/models/Sys_user.java @@ -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; diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysDataUserAllUpdateServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysDataUserAllUpdateServiceImpl.java index dd7e359f..fbf37f69 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysDataUserAllUpdateServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysDataUserAllUpdateServiceImpl.java @@ -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 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 unitList = dao.query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1")); @@ -181,23 +149,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService List needDoUpdateList = new CopyOnWriteArrayList<>(); List needInitUserList = new CopyOnWriteArrayList<>(); List histories = new CopyOnWriteArrayList<>(); - List addMemberUserIds = Collections.synchronizedList(new ArrayList<>()); - List removeMemberUserIds = Collections.synchronizedList(new ArrayList<>()); - Set sourceLoginNames = sources.stream() - .map(Sys_user_source::getLoginname) - .filter(StrUtil::isNotBlank) - .collect(Collectors.toSet()); - List leaveUserIds = sysUsers.stream() - .filter(user -> StrUtil.isNotBlank(user.getLoginname())) - .filter(user -> !sourceLoginNames.contains(user.getLoginname())) - .map(Sys_user::getId) - .toList(); - List 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 leaveTask = CompletableFuture.runAsync(() -> { - log.info("数据源缺失用户转为不在岗并取消会员: {} 个", leaveUserIds.size()); - List> 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 leaveMemberRoleTask = CompletableFuture.runAsync(() -> { - log.info("数据源缺失会员移除会员角色: {} 个", leaveMemberUserIds.size()); - List> 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 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,避免变更记录为空导致更新中断。 diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysDataUserIncrUpdateServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysDataUserIncrUpdateServiceImpl.java index 78b34691..251258d1 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysDataUserIncrUpdateServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysDataUserIncrUpdateServiceImpl.java @@ -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 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> 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 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> splitHistories = ListUtil.split(histories, 500); + List>> 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()); } } diff --git a/src/main/java/com/budwk/app/zhgh/club/controller/statistics/ClubStatisticsController.java b/src/main/java/com/budwk/app/zhgh/club/controller/statistics/ClubStatisticsController.java index 575ae938..acd6b017 100644 --- a/src/main/java/com/budwk/app/zhgh/club/controller/statistics/ClubStatisticsController.java +++ b/src/main/java/com/budwk/app/zhgh/club/controller/statistics/ClubStatisticsController.java @@ -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 examineRegisters = sysClubService.dao().query(SysClubExamineRegister.class, Cnd.where("year(registerDate)", "=", DateUtil.thisYear())); @@ -458,7 +470,6 @@ public class ClubStatisticsController { List 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"); diff --git a/src/main/java/com/budwk/app/zhgh/club/service/SysClubExamineService.java b/src/main/java/com/budwk/app/zhgh/club/service/SysClubExamineService.java index 5836c312..1a96c476 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/SysClubExamineService.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/SysClubExamineService.java @@ -13,6 +13,13 @@ import java.util.List; public interface SysClubExamineService extends BaseService { + /** + * 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。 + * + * @param clubId 社团ID + * @param year 统计年度 + * @return 在职和退休人员的年初、增加、减少、年末人数 + */ List getClubUserNum(@Valid String clubId, Integer year); List getJgUser(@Valid String clubId); diff --git a/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubExamineServiceImpl.java b/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubExamineServiceImpl.java index 0811b055..e90aca78 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubExamineServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubExamineServiceImpl.java @@ -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 implements SysClubExamineService { - @Inject - private SysDictService sysDictService; @Inject private SysClubService sysClubService; @@ -49,128 +49,244 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl getClubUserNum(String clubId, Integer year) { - // 定义统计的用户状态类型 - List userStateList = List.of("在职", "退休"); - List 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 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 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 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 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 applications = listMap(applySql); + applications.sort(Comparator.comparing(row -> row.getTime("applyDate"), Comparator.nullsLast(Date::compareTo))); - // 根据用户状态匹配对应的人员类型集合 - List personTypeList = switch (userState) { - case "在职" -> List.of("在职", "在岗"); - case "退休" -> List.of("退休", "退休【变号】"); - default -> new ArrayList<>(); - }; - - // 过滤出符合当前人员类型的用户(非空判断避免NPE) - List 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 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 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 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 diff --git a/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubInfoManageServiceImpl.java b/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubInfoManageServiceImpl.java index 9cc4424c..c4ad1c2f 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubInfoManageServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubInfoManageServiceImpl.java @@ -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 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 pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class); List 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 clubList = sysClubService.getMyManageClub().stream() + .sorted(Comparator.comparing(SysClub::getClubCode, Comparator.nullsLast(String::compareTo))) + .toList(); + nutMap.put("children", clubList); result.add(nutMap); return result; } diff --git a/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserCommonPageVo.java b/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserCommonPageVo.java index fb421266..0039f761 100644 --- a/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserCommonPageVo.java +++ b/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserCommonPageVo.java @@ -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; diff --git a/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserJoinVo.java b/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserJoinVo.java index c0ac0c8e..71d99f72 100644 --- a/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserJoinVo.java +++ b/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserJoinVo.java @@ -23,4 +23,5 @@ public class ClubUserJoinVo extends ClubUserApply { private String academicDegree; private String position; private String clubName; + private String exitType; } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/honor/controller/HonorManageController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/honor/controller/HonorManageController.java index 02e647da..77f9a2da 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/honor/controller/HonorManageController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/honor/controller/HonorManageController.java @@ -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)); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java index b6758ac0..d2a5127d 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java @@ -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("线路名称不能为空"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java index 06d75f2c..02913aec 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java @@ -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()); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java index 08dab7e9..67aa165c 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java @@ -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("周期开始年度不能大于周期结束年度"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java index 9876437a..846ed2dd 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java @@ -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()); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java index 900a571a..60db6f95 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java @@ -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("联系人不能为空"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java index 0e1af548..eada8569 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java @@ -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; diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java index 9fbfaef4..0f761c32 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java @@ -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; diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java index 37e212d4..8b0b1db8 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java @@ -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 lots; + /** + * 分工会人员分配,仅用于配置弹窗回显与提交,不作为 tour_setting 表字段保存。 + */ + private List unionQuotas; + @Column @Comment("服务须知") @ColDefine(type = ColType.TEXT) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingUnionQuota.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingUnionQuota.java new file mode 100644 index 00000000..2c95da11 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingUnionQuota.java @@ -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; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java index 88f0865c..6778850f 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java @@ -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; diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java index 076a5dd4..296c95dc 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java @@ -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 { + + /** + * 校验当前人员在省内间隔周期内是否已经实际参加过省内线路。 + * + * @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); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java index ca1cb4ef..b30b5624 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java @@ -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 { + + /** + * 查询全部分工会在指定配置下的人员数量,并实时补充分工会人员总数。 + * + * @param settingId 疗休养配置ID,新增配置时允许为空 + * @return 按分工会编码排序的人员分配行 + */ + List 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); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java index 88c2f668..0b63a786 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java @@ -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 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 ""; + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java index 68f57fcf..296fc684 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java @@ -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 implements TourSettingService { public TourSettingServiceImpl(Dao dao) { super(dao); } + + @Override + public List 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 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 quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas); + if (Lang.isEmpty(quotaList)) { + return; + } + // 工会名称以当前组织表为准,防止前端篡改或历史名称继续写入。 + Map 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 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 quotaList; + try { + quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas); + } catch (Exception e) { + return "人员分配数据格式不正确"; + } + if (Lang.isEmpty(quotaList)) { + return ""; + } + long total = 0L; + Set unionIds = new HashSet<>(); + Set 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 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; + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseCollectController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseCollectController.java index ae499ab4..deeea605 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseCollectController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/controller/UnionReimburseCollectController.java @@ -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) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/model/UnionReimburseDesc.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/model/UnionReimburseDesc.java index 86117039..c9c2998e 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/model/UnionReimburseDesc.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/model/UnionReimburseDesc.java @@ -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({ diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseService.java index 6fef206a..7bac1561 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/UnionReimburseService.java @@ -69,6 +69,11 @@ public interface UnionReimburseService extends BaseService { */ Result updateActuallyAmount(String id, Double realMoney); + /** + * 修改非慰问报销记录的活动项目。 + */ + Result updateReimburseProject(String id, String reimburseProject); + /** * 批量审核选中的待审核报销申请。 * diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java index dfa08007..775df28a 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseServiceImpl.java @@ -66,6 +66,13 @@ import java.util.stream.Collectors; @Slf4j @IocBean(args = {"refer:dao"}) public class UnionReimburseServiceImpl extends BaseServiceImpl implements UnionReimburseService { + /** 非慰问报销项目允许在三个活动项目之间切换。 */ + private static final Set 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 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 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<>()); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigTypeController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigTypeController.java index f020cefe..4a3b81dd 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigTypeController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigTypeController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java index 61cf7986..b5822600 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/dashboard/ProposalDashboardController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/dashboard/ProposalDashboardController.java index de4450fc..f4b5b30b 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/dashboard/ProposalDashboardController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/dashboard/ProposalDashboardController.java @@ -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 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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportComprehensiveController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportComprehensiveController.java index c5110305..fb37eeff 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportComprehensiveController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportComprehensiveController.java @@ -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") diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportSingleCustomController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportSingleCustomController.java index 08972423..a0f1a2c3 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportSingleCustomController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportSingleCustomController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryAnalysisController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryAnalysisController.java index f816b8aa..4d839728 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryAnalysisController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryAnalysisController.java @@ -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 STATISTICS_ORDER_COLUMNS = Set.of( + "dimension", "itemName", "count", "rate" + ); + + /** + * 分析数据表允许排序的字段白名单。 + */ + private static final Set 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 为统计行列表,包含 dimension、itemName、count、rate。 - */ + 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 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 为分析行列表,包含 dimension、total、categoryCount、topItem、topCount、topRate、analysis。 - */ + 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 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); } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryCollectProgressController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryCollectProgressController.java index a0c0abd4..ab386417 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryCollectProgressController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryCollectProgressController.java @@ -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 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 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 list = (List) pagination.getList(); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryComprehensiveController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryComprehensiveController.java index 28be5a7f..7c6fbb82 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryComprehensiveController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryComprehensiveController.java @@ -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 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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryDelegationController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryDelegationController.java index 9561967f..65fe6e44 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryDelegationController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryDelegationController.java @@ -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 DELEGATION_SUMMARY_ORDER_COLUMNS = Map.of( + "dbtName", "dbt.name", + "TOTAL", "TOTAL", + "caseRate", "caseRateOrder" + ); + + /** + * 代表团提案明细表允许排序的字段映射。 + */ + private static final Map 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 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 summaryOrderColumns = new HashMap<>(DELEGATION_SUMMARY_ORDER_COLUMNS); + List 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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryHistoryController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryHistoryController.java index 3f3e01ad..27a9a4e3 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryHistoryController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryHistoryController.java @@ -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 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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryUnitReplyController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryUnitReplyController.java index 811f3976..d726b185 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryUnitReplyController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryUnitReplyController.java @@ -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 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)); } } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryYearReportController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryYearReportController.java index 46728b36..2c1e0549 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryYearReportController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalQueryYearReportController.java @@ -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 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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalUnderTakeSatisfactionController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalUnderTakeSatisfactionController.java index 8bfd4b08..32286e40 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalUnderTakeSatisfactionController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/query/ProposalUnderTakeSatisfactionController.java @@ -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 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 feedbackCodes = sysDictService.getSubListByCode("PROPOSAL_FEEDBACK"); + Set 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); } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalExpeditingController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalExpeditingController.java index 19ead9f6..c96630de 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalExpeditingController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalExpeditingController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorBasicController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorBasicController.java index abe824cf..6b44aef9 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorBasicController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorBasicController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorDeleteController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorDeleteController.java index 04b326eb..697967b6 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorDeleteController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorDeleteController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorFeedBackController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorFeedBackController.java index 2ecc9421..6e57f67a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorFeedBackController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorFeedBackController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorTypeController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorTypeController.java index 448488d1..a0d83b3d 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorTypeController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/senior/ProposalSeniorTypeController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalAllFinishedController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalAllFinishedController.java index d6d3016a..c4c0f0cd 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalAllFinishedController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalAllFinishedController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCaseCheckController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCaseCheckController.java index 069da813..6cd1af9a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCaseCheckController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCaseCheckController.java @@ -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("查询当前提案承办单位") diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommissionerController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommissionerController.java index ec5d51cd..91046e88 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommissionerController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommissionerController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommitteeFilingController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommitteeFilingController.java index 1da9b452..f3f487a3 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommitteeFilingController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommitteeFilingController.java @@ -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("并案审核") diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommitteeFilingUnitController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommitteeFilingUnitController.java index dae7c618..38d4af98 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommitteeFilingUnitController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalCommitteeFilingUnitController.java @@ -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("执行任务") diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalControlController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalControlController.java index bb7b3de6..9f77a5ec 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalControlController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalControlController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalDelegationController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalDelegationController.java index eb57d6b9..cd024a5a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalDelegationController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalDelegationController.java @@ -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); + } + } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalFeedbackEvaluationController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalFeedbackEvaluationController.java index 22f0086a..bef308e3 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalFeedbackEvaluationController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalFeedbackEvaluationController.java @@ -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("获取立案信息") diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalInviteController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalInviteController.java index fba3c315..741abae7 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalInviteController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalInviteController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalMineController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalMineController.java index 538aaca7..fa81cbb8 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalMineController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalMineController.java @@ -62,6 +62,28 @@ import java.util.Objects; @Ok("json:full") @Api(tags = "提案管理-我的提案") public class ProposalMineController { + /** 我的提案列表允许排序的页面字段与 SQL 字段映射。 */ + private static final Map 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 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 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 pagination = proposalCommonService.listPageVO(pageForm, sql, ProposalInviteSeconderVO.class); return Result.success(pagination); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalPreAuditController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalPreAuditController.java index 6a60c9e4..a2381d9d 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalPreAuditController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalPreAuditController.java @@ -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); + } } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalSchoolLeaderApprovalController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalSchoolLeaderApprovalController.java index dff9ab32..7561186b 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalSchoolLeaderApprovalController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalSchoolLeaderApprovalController.java @@ -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("获取主办单位") diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalSecondedController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalSecondedController.java index 326cf7a7..b54cb81a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalSecondedController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalSecondedController.java @@ -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 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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnSubmitQueryController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnSubmitQueryController.java index 25dd611d..24065458 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnSubmitQueryController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnSubmitQueryController.java @@ -96,6 +96,8 @@ public class ProposalUnSubmitQueryController { cnd.and("info.sessionId", "=", pageForm.getSessionId()); } + // 未提交查询包含子查询统计列,统一通过页面独立白名单排序。 + proposalCommonService.applySafeProposalListOrder(cnd, pageForm, "unSubmitQuery"); sql.setCondition(cnd); Pagination pagination = proposalCommonService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); return Result.success(pagination); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnderTakeReadController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnderTakeReadController.java index 4d32fc79..63eda76a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnderTakeReadController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnderTakeReadController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnderTakeReplyController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnderTakeReplyController.java index a8fa6146..47e4a5d8 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnderTakeReplyController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUnderTakeReplyController.java @@ -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 分组会把多条待审核合并成一条, - * 因此这里优先使用任务参与人的承办单位信息拆行,历史数据缺失时再回退到流程变量。 + * 新流程生成的会签任务可能未写入 underTakeName、underTakeId、underTakeIsMaster, + * 因此优先读取任务变量,变量缺失时再按“提案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); + } + /** * 承办单位答复节点撤回需要基于整组当前节点判断。 * 只有“当前流程正在办理的节点”和“这条列表记录所属节点”完全一致时,才允许撤回; diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUndertakeSuggestionController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUndertakeSuggestionController.java index 4bfe8140..42252763 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUndertakeSuggestionController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalUndertakeSuggestionController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalViceDelegationController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalViceDelegationController.java index 6b0562dd..2a0d0028 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalViceDelegationController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalViceDelegationController.java @@ -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); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalWriteController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalWriteController.java index 52f0da5e..c05c1a5b 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalWriteController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalWriteController.java @@ -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(); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/vo/ProposalWriteTypeVO.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/vo/ProposalWriteTypeVO.java new file mode 100644 index 00000000..d21a37a1 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/vo/ProposalWriteTypeVO.java @@ -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; +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/handler/ProposalUnitAssignmentHandler.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/handler/ProposalUnitAssignmentHandler.java index 4425574f..e8374f9e 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/handler/ProposalUnitAssignmentHandler.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/handler/ProposalUnitAssignmentHandler.java @@ -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 unitIds = new ArrayList<>(); - unitIds.addAll(resolveMasterUnitIds(execution, dao, proposalId)); - unitIds.addAll(resolveSlaveUnitIds(execution, dao, proposalId)); + List 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 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 unitIds = new ArrayList<>(); + unitIds.addAll(resolveMasterUnitIds(execution, dao, proposalId)); + unitIds.addAll(resolveSlaveUnitIds(execution, dao, proposalId)); + return unitIds; + } + /** * 优先从当前流程变量读取主办单位,读不到时回落到承办单位记录表,兼容重新流转场景。 */ diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalInfo.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalInfo.java index 94235067..5fc6284e 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalInfo.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalInfo.java @@ -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) diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalType.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalType.java index f4968382..0f807be1 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalType.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalType.java @@ -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 diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/param/ProposalQueryComprehensiveParam.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/param/ProposalQueryComprehensiveParam.java index 5315291a..f3776cfc 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/param/ProposalQueryComprehensiveParam.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/param/ProposalQueryComprehensiveParam.java @@ -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())) { diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalExportService.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalExportService.java index ad65c3df..0015ea1a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalExportService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalExportService.java @@ -34,6 +34,14 @@ public interface ProposalExportService extends BaseService { */ void exportSummaryAsExcel(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response); + /** + * 按办理单位导出提案承办单位表压缩包。 + * + * @param pageForm 综合导出页面查询参数;sessionId 应传当前教代会届次ID,其他条件沿用页面查询条件 + * @param response HTTP 响应,方法直接输出 ZIP;ZIP 中每个有提案的单位对应一个 XSSF Excel + */ + void exportUndertakeUnitTablesAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response); + /** * 导出提案立案汇总表excel * diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalWriteService.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalWriteService.java index 10a932c4..ae07bd50 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalWriteService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalWriteService.java @@ -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 { + /** + * 查询指定教代会届次允许选择的提案类型。 + * + * @param sessionId 教代会届次ID + * @return 提案类型 VO 列表,id 用于保存,name 用于页面展示 + */ + List listSessionProposalTypes(String sessionId); + + /** + * 校验所选提案类型是否属于当前教代会届次。 + * + * @param sessionId 教代会届次ID + * @param typeId 提案类型ID + */ + void validateProposalType(String sessionId, Integer typeId); + List listSource(String sessionId); /** diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonService.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonService.java index a416a455..207a0ade 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonService.java @@ -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 { + /** + * 按页面允许的字段白名单添加排序条件,避免前端字段直接进入SQL。 + * + * @param cnd 查询条件 + * @param pageForm 分页及排序参数 + * @param allowedOrderColumns 前端字段与数据库字段或查询别名的映射 + * @return 是否成功添加安全排序条件 + */ + boolean applySafePageOrder(Cnd cnd, PageForm pageForm, Map 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 rows, PageForm pageForm, Set allowedOrderColumns); + /** * 查询提案信息 */ @@ -105,6 +139,18 @@ public interface ProposalCommonService extends BaseService { */ 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); + /** * 合并提案 diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonServiceImpl.java index 00dc0c2f..b19294ff 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonServiceImpl.java @@ -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 implements ProposalCommonService { + /** PC端提案列表页面的排序字段白名单,页面代码由对应Controller固定传入。 */ + private static final Map> PROPOSAL_LIST_ORDER_COLUMNS = createProposalListOrderColumns(); + + /** 办理页面允许导出的列表字段,避免请求参数携带非页面字段。 */ + private static final Map> WORKFLOW_EXPORT_COLUMNS = createWorkflowExportColumns(); + //找出富文本里面上传的图片 static String IMG_SRC_REGEX = "]*src\\s*=\\s*([\"'])(/platform/sys/file/download\\?id=.*?)\\1[^>]*>"; @@ -88,6 +107,391 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl imp super(dao); } + /** + * 使用业务页面提供的字段白名单构建排序条件,非法字段或非法排序方向将被忽略。 + * + * @param cnd 查询条件 + * @param pageForm 分页及排序参数 + * @param allowedOrderColumns 前端字段与数据库字段或查询别名的映射 + * @return 是否成功添加安全排序条件 + */ + @Override + public boolean applySafePageOrder(Cnd cnd, PageForm pageForm, Map 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> createProposalListOrderColumns() { + Map> 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 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 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 orderColumns(String... mappings) { + if (mappings == null || mappings.length % 2 != 0) { + throw new IllegalArgumentException("排序字段映射必须成对配置"); + } + Map 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 mergeOrderColumns(Map base, Map extension) { + Map 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 exportColumns = getWorkflowExportColumns(pageCode, tableColumns); + if (exportColumns.isEmpty()) { + throw Lang.makeThrow("请选择至少一个列表字段后再导出"); + } + + Sql sql = buildWorkflowExportSql(pageCode, pageForm, approval); + List rows = listMap(sql); + convertWorkflowExportValues(rows, pageCode); + + List 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 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 rows, String pageCode) { + Map caseFilingResultMap = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT") + .stream().collect(Collectors.toMap(Sys_dict::getCode, Sys_dict::getName)); + Map 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 unitNames = new ArrayList<>(); + if (StrUtil.isNotBlank(masterUnitNames)) { + unitNames.add("主办:" + masterUnitNames); + } + if (StrUtil.isNotBlank(slaveUnitNames)) { + unitNames.add("协办:" + slaveUnitNames); + } + return StrUtil.join(";", unitNames); + } + return ""; + } + + /** 根据页面列定义过滤导出字段,确保导出范围只包含列表可展示字段。 */ + private List getWorkflowExportColumns(String pageCode, ExportTableColumns[] tableColumns) { + Set 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> createWorkflowExportColumns() { + Set commonColumns = Set.of("code", "caseFilingCode", "name", "createUserName", "typeName", + "sessionName", "delegationName", "curTaskName", "instanceState"); + Map> 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 rows, PageForm pageForm, Set 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 imp } // 按任务节点分组 - Map> taskGroups = doneTaskVos.stream().collect(Collectors.groupingBy(ProcessTaskVO::getDisplayName)); + Map> taskGroups = doneTaskVos.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(ProcessTaskVO::getDisplayName)); docData.putAll(taskGroups); + putProposalExportTaskAliases(id, taskGroups, docData); // 提案附议 List secondInfos = new ArrayList<>(); @@ -300,22 +707,79 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl 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 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> taskGroups, + NutMap docData) { + putTaskAlias(docData, "提案委主任审核", taskGroups.get("校工会预审核")); + putTaskAlias(docData, "提案委主任意见", taskGroups.get("提案委员会立案")); + putTaskAlias(docData, "提案委委员审议", taskGroups.get("委员会确认承办单位")); + + List 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 masterUnitRows = listMap(masterUnitSql); + Set masterUnitNames = masterUnitRows.stream() + .map(row -> row.getString("unitName")) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + + List masterReplyTasks = new ArrayList<>(); + List 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 tasks) { + if (Lang.isNotEmpty(tasks)) { + docData.put(templateKey, tasks); + } + } + @Override public void exportProposalFeedBackAsDocx(String id, ByteArrayOutputStream byteArrayOutputStream) { if (id == null || id.isEmpty()) { diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalCommitteeFilingUnitServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalCommitteeFilingUnitServiceImpl.java index fba790be..0b26d685 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalCommitteeFilingUnitServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalCommitteeFilingUnitServiceImpl.java @@ -76,6 +76,8 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl proposalIds = proposalCommonService.mergeProposal(proposalId); if (ObjectUtil.isEmpty(proposalIds)) { @@ -98,16 +100,44 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl 20) { + throw new BaseException("立案编号不能超过20个字符"); + } + args.put(caseFilingCodeKey, caseFilingCode); + } + + /** + * old-flow 页面提交后,需要把立案编号、立案结果以及主协办单位同步落业务表。 * 若这次改成“不予立案”等无需承办单位的结果,也要先清掉旧的承办单位记录,避免脏数据残留。 */ private void syncCommitteeFilingUnitData(List 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)); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalExportServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalExportServiceImpl.java index 5b23f4fd..721fdc83 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalExportServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalExportServiceImpl.java @@ -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 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 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 unitRows = queryUndertakeUnitExportRows(pageForm); + if (Lang.isEmpty(unitRows)) { + throw new BaseException("暂无可导出的承办单位提案数据"); + } + + Map proposalRows = new LinkedHashMap<>(); + Map unitNames = new LinkedHashMap<>(); + Map> 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()) + .addv("slaveUnitNames", new LinkedHashSet())); + 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 zipEntryNameCounts = new HashMap<>(); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(zipBytes)) { + for (Map.Entry> unitEntry : unitProposalIds.entrySet()) { + String unitName = unitNames.get(unitEntry.getKey()); + List 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 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 buildUndertakeUnitExcelRows(LinkedHashSet proposalIds, + Map proposalRows) { + List filingRows = new ArrayList<>(); + List suggestionRows = new ArrayList<>(); + for (String proposalId : proposalIds) { + NutMap proposalRow = proposalRows.get(proposalId); + if (proposalRow == null) { + continue; + } + LinkedHashSet masterUnitNames = getUnitNameSet(proposalRow, "masterUnitNames"); + LinkedHashSet 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 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 excelRows) { + List 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 getUnitNameSet(NutMap proposalRow, String key) { + return (LinkedHashSet) proposalRow.get(key); + } + @Override public void exportProposalRegisterSummaryAsExcel(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) { Sql sql = exportComprehensiveSql(pageForm); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalWriteServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalWriteServiceImpl.java index e559478f..adc755b5 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalWriteServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalWriteServiceImpl.java @@ -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 impl super(dao); } + @Override + public List listSessionProposalTypes(String sessionId) { + List typeNames = getSessionProposalTypeNames(sessionId); + if (typeNames.isEmpty()) { + return List.of(); + } + List proposalTypes = dao().query(ProposalType.class, + Cnd.where(ProposalType::getName, "in", typeNames)); + List 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 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 listSource(String sessionId) { Sql sql = Sqls.create(""" @@ -118,7 +185,11 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl impl @Override public List 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 impl throw new RuntimeException("没有开启的教代会"); } Teacher_congress_session teacherCongressSession = teacherCongressSessions.get(0); + validateProposalType(teacherCongressSession.getId(), proposalTypeObj.getId()); // 保存到数据库 ProposalInfo proposalInfo = new ProposalInfo(); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/controller/TeacherCongressDelegationController.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/controller/TeacherCongressDelegationController.java index fe7a8ba2..a86d1a43 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/controller/TeacherCongressDelegationController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/controller/TeacherCongressDelegationController.java @@ -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 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(); } } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/service/TeacherCongressDelegationService.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/service/TeacherCongressDelegationService.java new file mode 100644 index 00000000..7d47a167 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/service/TeacherCongressDelegationService.java @@ -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 { + + /** + * 按角色查询代表团负责人信息。 + * + * @param sessionId 届次ID + * @param delegationId 代表团ID + * @param type 负责人角色编码 + * @return 负责人用户列表 + */ + List 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); +} \ No newline at end of file diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/service/impl/TeacherCongressDelegationServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/service/impl/TeacherCongressDelegationServiceImpl.java new file mode 100644 index 00000000..6d539c7a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/delegation/service/impl/TeacherCongressDelegationServiceImpl.java @@ -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 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 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))); + } +} \ No newline at end of file diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/TeacherCongressSessionController.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/TeacherCongressSessionController.java index ee82ea1c..92bdb5cf 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/TeacherCongressSessionController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/TeacherCongressSessionController.java @@ -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(); } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/vo/TeacherCongressSessionProposalTypeVO.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/vo/TeacherCongressSessionProposalTypeVO.java new file mode 100644 index 00000000..cbcd8003 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/vo/TeacherCongressSessionProposalTypeVO.java @@ -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; +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/models/Teacher_congress_session.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/models/Teacher_congress_session.java index 1e860aab..caaadb81 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/models/Teacher_congress_session.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/models/Teacher_congress_session.java @@ -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) diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/TeacherCongressSessionService.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/TeacherCongressSessionService.java index d7b4c26f..8e61c20a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/TeacherCongressSessionService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/TeacherCongressSessionService.java @@ -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 { + /** + * 查询届次中已经保存过的提案类型,供可输入下拉框复用。 + * + * @return 提案类型 VO 列表,每项的 proposalType 字段为一个可选类型 + */ + List listProposalTypes(); + void insertSession(Teacher_congress_session session, boolean isExtend); + /** + * 修改届次基础信息并统一校验多选提案类型。 + * + * @param session 届次信息,其中 proposalType 为 JSON 字符串数组 + */ + void updateSession(Teacher_congress_session session); + void deleteSession(String id); } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/impl/TeacherCongressSessionServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/impl/TeacherCongressSessionServiceImpl.java index a0ec55a9..4825c3fb 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/impl/TeacherCongressSessionServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/impl/TeacherCongressSessionServiceImpl.java @@ -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 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 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 values = parseProposalTypes(session.getProposalType()); + LinkedHashSet 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 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 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) { diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/aidFund/controller/AidFundChangeRecordController.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/aidFund/controller/AidFundChangeRecordController.java index fbdf093e..ce54435a 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/aidFund/controller/AidFundChangeRecordController.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/aidFund/controller/AidFundChangeRecordController.java @@ -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()); diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyBranchUnionApprovalController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyBranchUnionApprovalController.java index 24b993b4..d17e8083 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyBranchUnionApprovalController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyBranchUnionApprovalController.java @@ -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 diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplySchoolUnionApprovalController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplySchoolUnionApprovalController.java index eb5377c3..4f6ca767 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplySchoolUnionApprovalController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplySchoolUnionApprovalController.java @@ -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 diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyUnionGroupApprovalController.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyUnionGroupApprovalController.java index 4e5d3902..f747e094 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyUnionGroupApprovalController.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/controller/apply/MemberApplyUnionGroupApprovalController.java @@ -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 diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberApplyPageForm.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberApplyPageForm.java index 051166ec..b3999485 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberApplyPageForm.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberApplyPageForm.java @@ -34,6 +34,9 @@ public class MemberApplyPageForm extends PageForm { private String userAttribute; //人员属性 + /** 人员属性多选值,供开启多选查询的页面使用。 */ + private List 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()); + } } } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberChangePageForm.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberChangePageForm.java index e3b49fe0..85c8cf90 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberChangePageForm.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberChangePageForm.java @@ -47,6 +47,9 @@ public class MemberChangePageForm extends PageForm { @ApiModelProperty("人员属性") private String userAttribute; + @ApiModelProperty("人员属性组") + private List 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()); + } } } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberInfoPageForm.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberInfoPageForm.java index 271f5339..f9558c84 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberInfoPageForm.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberInfoPageForm.java @@ -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())) { diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberManagePageForm.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberManagePageForm.java index 365b24d4..117ab932 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberManagePageForm.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/param/pageform/MemberManagePageForm.java @@ -42,6 +42,9 @@ public class MemberManagePageForm extends PageForm { private String userAttribute; + /** 人员属性多选查询条件。 */ + private List 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"); } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberInfoServiceImpl.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberInfoServiceImpl.java index e1617358..b4b203b8 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberInfoServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberInfoServiceImpl.java @@ -396,6 +396,12 @@ public class MemberInfoServiceImpl extends BaseServiceImpl 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())); } diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java index 03828356..3673b382 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/member/service/impl/MemberManageServiceImpl.java @@ -175,6 +175,12 @@ public class MemberManageServiceImpl extends BaseServiceImpl 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()))); } diff --git a/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareFilterUserPageForm.java b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareFilterUserPageForm.java index 1bd27b0f..1346694a 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareFilterUserPageForm.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/param/WelfareFilterUserPageForm.java @@ -46,6 +46,9 @@ public class WelfareFilterUserPageForm extends PageForm { @ApiModelProperty("人员类型") private String[] personTypes; + @ApiModelProperty("人员属性") + private String[] userAttributes; + @ApiModelProperty("聘用方式") private String[] preparedBys; diff --git a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareListServiceImpl.java b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareListServiceImpl.java index 95f04c04..189ce5b8 100644 --- a/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareListServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/welfare/service/impl/WelfareListServiceImpl.java @@ -444,11 +444,13 @@ public class WelfareListServiceImpl extends BaseServiceImpl 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 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 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()); diff --git a/src/main/resources/db/evaluate_apply_add_history_import_fields.sql b/src/main/resources/db/evaluate_apply_add_history_import_fields.sql new file mode 100644 index 00000000..c40919dc --- /dev/null +++ b/src/main/resources/db/evaluate_apply_add_history_import_fields.sql @@ -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`; diff --git a/src/main/resources/db/menu/init_tour_menu.sql b/src/main/resources/db/menu/init_tour_menu.sql new file mode 100644 index 00000000..b1af6f7f --- /dev/null +++ b/src/main/resources/db/menu/init_tour_menu.sql @@ -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; diff --git a/src/main/resources/db/tour_setting_add_travel_people_quota.sql b/src/main/resources/db/tour_setting_add_travel_people_quota.sql new file mode 100644 index 00000000..fb773462 --- /dev/null +++ b/src/main/resources/db/tour_setting_add_travel_people_quota.sql @@ -0,0 +1,3 @@ +-- 疗休养配置增加出行人数指标,历史配置默认按 0 处理。 +ALTER TABLE `tour_setting` + ADD COLUMN `travelPeopleQuota` int DEFAULT 0 COMMENT '出行人数指标' AFTER `tourType`; diff --git a/src/main/resources/db/tour_setting_create.sql b/src/main/resources/db/tour_setting_create.sql new file mode 100644 index 00000000..8e41e475 --- /dev/null +++ b/src/main/resources/db/tour_setting_create.sql @@ -0,0 +1,184 @@ +-- 普惠疗休养基础配置表。 +-- 若生产环境未开启 Nutz 自动建表,请先执行本脚本再使用“疗休养设置”菜单。 +CREATE TABLE IF NOT EXISTS `tour_setting` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `year` int DEFAULT NULL COMMENT '年度', + `configName` varchar(100) DEFAULT NULL COMMENT '疗休养配置名称', + `tourType` varchar(50) DEFAULT NULL COMMENT '疗休养类型', + `activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID', + `sortNo` int DEFAULT NULL COMMENT '排序编号', + `minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数', + `maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数', + `inProvinceYears` int DEFAULT 0 COMMENT '省内几年去一次', + `outProvinceYears` int DEFAULT NULL COMMENT '省外几年去一次', + `outProvinceRatio` decimal(10,2) DEFAULT NULL COMMENT '省外人数占比', + `outProvinceRatioType` varchar(50) DEFAULT '当年报名人数' COMMENT '省外人数占比类型', + `outProvinceFixedPeople` int DEFAULT 0 COMMENT '省外固定人数', + `cycleStartYear` int DEFAULT NULL COMMENT '周期开始年度', + `cycleEndYear` int DEFAULT NULL COMMENT '周期结束年度', + `cycleTotalCost` int DEFAULT NULL COMMENT '周期内总费用', + `cycleAllowedTimes` int DEFAULT NULL COMMENT '周期允许次数', + `allowFamily` tinyint(1) DEFAULT 0 COMMENT '是否允许携带家属', + `fillBedInfo` tinyint(1) DEFAULT 1 COMMENT '是否填报床位信息', + `enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用', + `serviceNotice` text COMMENT '服务须知', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + KEY `idx_tour_setting_year` (`year`), + KEY `idx_tour_setting_activity_group` (`activityGroupId`), + KEY `idx_tour_setting_sort` (`sortNo`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置'; + +-- 疗休养配置标段表。 +-- 标段先挂在配置上维护,后续线路、旅行社、报名等模块可继续通过 lotId 做业务关联。 +CREATE TABLE IF NOT EXISTS `tour_setting_lot` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `settingId` varchar(32) DEFAULT NULL COMMENT '所属配置ID', + `lotName` varchar(50) DEFAULT NULL COMMENT '标段名称', + `lotValue` varchar(50) DEFAULT NULL COMMENT '标段值', + `activityCost` int DEFAULT NULL COMMENT '标段费用', + `allowOverReimbursement` tinyint(1) DEFAULT 0 COMMENT '允许超出报销', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + KEY `idx_tour_setting_lot_setting` (`settingId`), + KEY `idx_tour_setting_lot_value` (`lotValue`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置标段'; + +-- 旅行社管理表。 +CREATE TABLE IF NOT EXISTS `tour_travel_agency` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `year` int DEFAULT NULL COMMENT '年度', + `agencyCode` varchar(50) DEFAULT NULL COMMENT '旅行社编号', + `agencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称', + `contactName` varchar(30) DEFAULT NULL COMMENT '联系人', + `contactPhone` varchar(30) DEFAULT NULL COMMENT '联系人手机', + `email` varchar(100) DEFAULT NULL COMMENT '邮箱', + `remark` text COMMENT '备注', + `mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图', + `enabled` tinyint(1) DEFAULT 1 COMMENT '激活状态', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tour_travel_agency_year_code` (`year`, `agencyCode`), + KEY `idx_tour_travel_agency_name` (`agencyName`), + KEY `idx_tour_travel_agency_contact` (`contactName`, `contactPhone`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养旅行社'; + +-- 线路管理表。 +CREATE TABLE IF NOT EXISTS `tour_line` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `year` int DEFAULT NULL COMMENT '创建年度', + `lineCode` varchar(50) DEFAULT NULL COMMENT '线路编号', + `lineName` varchar(100) DEFAULT NULL COMMENT '线路名称', + `travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID', + `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID', + `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人', + `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID', + `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', + `lineType` varchar(50) DEFAULT NULL COMMENT '线路类型', + `lotId` varchar(32) DEFAULT NULL COMMENT '时间标段ID', + `lineContent` text COMMENT '线路内容', + `mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图', + `openFlag` tinyint(1) DEFAULT 1 COMMENT '是否对外开放', + `directFamilyUnitLine` tinyint(1) DEFAULT 0 COMMENT '是否直系亲属线路', + `enabled` tinyint(1) DEFAULT 1 COMMENT '激活状态', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tour_line_year_code` (`year`, `lineCode`), + KEY `idx_tour_line_year` (`year`), + KEY `idx_tour_line_name` (`lineName`), + KEY `idx_tour_line_agency` (`travelAgencyId`), + KEY `idx_tour_line_creator` (`creatorUserId`), + KEY `idx_tour_line_unit` (`unitId`), + KEY `idx_tour_line_lot` (`lotId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养线路'; + +-- 疗休养事项表。 +CREATE TABLE IF NOT EXISTS `tour_matter` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `year` int DEFAULT NULL COMMENT '年度', + `matterName` varchar(100) DEFAULT NULL COMMENT '事项名称', + `settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID', + `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID', + `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人', + `unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID', + `organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式', + `mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图', + `enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tour_matter_year_name` (`year`, `matterName`), + KEY `idx_tour_matter_year` (`year`), + KEY `idx_tour_matter_creator` (`creatorUserId`), + KEY `idx_tour_matter_setting` (`settingId`), + KEY `idx_tour_matter_union` (`unionId`), + KEY `idx_tour_matter_org_type` (`organizationType`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项'; + +-- 疗休养事项批次表。 +CREATE TABLE IF NOT EXISTS `tour_matter_batch` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `matterId` varchar(32) DEFAULT NULL COMMENT '事项ID', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `batchName` varchar(100) DEFAULT NULL COMMENT '批次名称', + `signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间', + `signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间', + `travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间', + `travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间', + `changeDeadline` varchar(20) DEFAULT NULL COMMENT '变更截止时间', + `contactName` varchar(30) DEFAULT NULL COMMENT '联系人', + `contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式', + `minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数', + `maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数', + `estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用', + `enabled` tinyint(1) DEFAULT 1 COMMENT '状态', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + KEY `idx_tour_matter_batch_matter` (`matterId`), + KEY `idx_tour_matter_batch_line` (`lineId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项批次'; + +-- 直系亲属线路报名信息表。 +CREATE TABLE IF NOT EXISTS `tour_ledger_direct_relative` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `ledgerId` varchar(32) DEFAULT NULL COMMENT '台账ID', + `relativeName` varchar(100) DEFAULT NULL COMMENT '亲属姓名', + `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', + `relationshipCode` varchar(50) DEFAULT NULL COMMENT '亲属关系编码', + `relationshipName` varchar(50) DEFAULT NULL COMMENT '亲属关系', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `lineName` varchar(100) DEFAULT NULL COMMENT '线路名称', + `travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始日期', + `travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束日期', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + KEY `idx_tour_ledger_direct_relative_ledger` (`ledgerId`), + KEY `idx_tour_ledger_direct_relative_line` (`lineId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养直系亲属线路报名信息'; diff --git a/src/main/resources/db/tour_setting_union_quota_create.sql b/src/main/resources/db/tour_setting_union_quota_create.sql new file mode 100644 index 00000000..598d8b54 --- /dev/null +++ b/src/main/resources/db/tour_setting_union_quota_create.sql @@ -0,0 +1,17 @@ +-- 疗休养配置分工会人员分配表,人员数量合计受配置的出行人数指标约束。 +CREATE TABLE IF NOT EXISTS `tour_setting_union_quota` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID', + `unionId` varchar(32) DEFAULT NULL COMMENT '分工会ID', + `unionName` varchar(100) DEFAULT NULL COMMENT '分工会名称', + `peopleQuota` int DEFAULT 0 COMMENT '人员数量', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tour_setting_union_quota_setting_union` (`settingId`, `unionId`), + KEY `idx_tour_setting_union_quota_setting` (`settingId`), + KEY `idx_tour_setting_union_quota_union` (`unionId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养配置分工会人员分配'; diff --git a/src/main/resources/static/components/plugins/sysTextEditor/index.vue b/src/main/resources/static/components/plugins/sysTextEditor/index.vue index 7109db8d..786fa097 100644 --- a/src/main/resources/static/components/plugins/sysTextEditor/index.vue +++ b/src/main/resources/static/components/plugins/sysTextEditor/index.vue @@ -170,6 +170,16 @@ module.exports = { type: Number, default: 300 }, + // 富文本编辑器占位提示,透传给 wangEditor 初始化配置。 + placeholder: { + type: String, + default: "" + }, + // 编辑区顶部红色提示,只做展示,不写入编辑器正文内容。 + topTip: { + type: String, + default: "" + }, menus: { type: Array, default: () => { @@ -205,13 +215,18 @@ module.exports = { data() { return { editor: null, - cursorPos: 0 + cursorPos: 0, + topTipElement: null } }, mounted() { this.initEditor() }, + beforeDestroy() { + this.removeTopTip() + }, + watch: { value: { handler: function (newValue) { @@ -229,6 +244,13 @@ module.exports = { }) }, immediate: true + }, + topTip: { + handler: function () { + this.$nextTick(() => { + this.renderTopTip() + }) + } } }, @@ -266,7 +288,43 @@ module.exports = { this.editor.config.pasteFilterStyle = false this.editor.config.height = this.height this.editor.config.menus = this.menus + // wangEditor 的 placeholder 必须在 create 前设置,父页面传入才会生效。 + this.editor.config.placeholder = this.placeholder this.editor.create() + this.renderTopTip() + }, + renderTopTip() { + if (!this.editor || !this.$refs.editorRef) { + return + } + const textContainer = this.$refs.editorRef.querySelector(".w-e-text-container") + if (!textContainer) { + return + } + if (!this.topTip) { + this.removeTopTip() + return + } + if (!this.topTipElement) { + this.topTipElement = document.createElement("div") + this.topTipElement.setAttribute("contenteditable", "false") + this.topTipElement.style.color = "#f56c6c" + this.topTipElement.style.fontSize = "12px" + this.topTipElement.style.lineHeight = "20px" + this.topTipElement.style.padding = "6px 10px 0" + this.topTipElement.style.userSelect = "none" + this.topTipElement.style.pointerEvents = "none" + } + // 提示节点放在可编辑正文区域外层,保证用户无法删除且保存内容不携带该提示。 + this.topTipElement.innerText = this.topTip + if (textContainer.firstChild !== this.topTipElement) { + textContainer.insertBefore(this.topTipElement, textContainer.firstChild) + } + }, + removeTopTip() { + if (this.topTipElement && this.topTipElement.parentNode) { + this.topTipElement.parentNode.removeChild(this.topTipElement) + } }, uploadFiles(resultFiles) { return resultFiles.map(async (file) => { diff --git a/src/main/resources/static/components/plugins/sysUpload/index.vue b/src/main/resources/static/components/plugins/sysUpload/index.vue index 9ddfa92f..b23d1f16 100644 --- a/src/main/resources/static/components/plugins/sysUpload/index.vue +++ b/src/main/resources/static/components/plugins/sysUpload/index.vue @@ -133,6 +133,12 @@ module.exports = { default: 1024 * 1024 * 10, required: false }, + // 是否限制上传文件大小,默认保留原有大小校验逻辑。 + limit_upload_size: { + type: Boolean, + default: true, + required: false + }, // 上传按钮文字 upload_text: { type: String, @@ -216,7 +222,7 @@ module.exports = { } else { tips = tips + '不限格式;' } - if (fileSize) { + if (this.limit_upload_size && fileSize) { tips = tips + `单个文件大小不能超过${fileSize / 1024 / 1024}M;` } return tips @@ -313,7 +319,8 @@ module.exports = { // 这是兜底逻辑,保准只让这个image类型上传图片 beforeUpload(file) { - if (file.size > this.upload_size) { + // 需要兼容部分业务不限制附件大小,关闭后仅跳过大小校验,不影响数量和格式控制。 + if (this.limit_upload_size && file.size > this.upload_size) { this.$message.error("文件过大,请上传小于" + (this.upload_size / 1024 / 1024).toFixed(0) + "M的文件!") return false } diff --git a/src/main/resources/views/layouts/platform.html b/src/main/resources/views/layouts/platform.html index 3575c275..88818ddc 100644 --- a/src/main/resources/views/layouts/platform.html +++ b/src/main/resources/views/layouts/platform.html @@ -419,6 +419,25 @@ layout("/layouts/v4/baseLayout.html"){ }, defaultSelect() { + // 应用中心点击根菜单时会缓存完整应用信息,根菜单配置了href则应优先进入根菜单页面。 + try { + const rootApp = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app")) + const currentAppId = GetQueryString("appId") + const hasRootHref = rootApp && typeof rootApp.href === "string" && rootApp.href.trim() !== "" + // 校验缓存应用与当前URL一致,避免复用旧标签页缓存时跳转到其他应用。 + if (hasRootHref && String(rootApp.id) === currentAppId) { + this.activeMenuIndex = rootApp.id + this.openedMenus = [] + this.$nextTick(() => { + this.menuSelect(this.activeMenuIndex, this.openedMenus) + commonUtil.pjaxPush(getBaseSubAppPath(rootApp.href)) + }) + return + } + } catch (e) { + // 根菜单缓存缺失或格式异常时,继续使用子菜单href作为默认入口。 + } + // 查找第一个有href的子菜单 function findFirstChildWithHref(menus) { for (const menu of menus) { diff --git a/src/main/resources/views/platform/sys/data/user/update.html b/src/main/resources/views/platform/sys/data/user/update.html index 3d184425..e734e3de 100644 --- a/src/main/resources/views/platform/sys/data/user/update.html +++ b/src/main/resources/views/platform/sys/data/user/update.html @@ -276,11 +276,12 @@ layout("/layouts/platform.html"){ if (resp.code === 0) { this.$message.success(resp.msg) this.updateDialog = false + this.pageData() } }) - }) - .finally(() => { - this.updateLoading = false + .finally(() => { + this.updateLoading = false + }) }) }, listUnit() { diff --git a/src/main/resources/views/platform/sys/union/branchUnionGroupManage.js b/src/main/resources/views/platform/sys/union/branchUnionGroupManage.js index 52b19956..1e289846 100644 --- a/src/main/resources/views/platform/sys/union/branchUnionGroupManage.js +++ b/src/main/resources/views/platform/sys/union/branchUnionGroupManage.js @@ -19,7 +19,7 @@ const branchUnionGroupManage = { - + @@ -85,6 +85,11 @@ const branchUnionGroupManage = { union_id: { type: String, required: true + }, + // 工会小组列表包含分页,父页面传入高度后预留分页区域。 + table_height: { + type: Number, + default: null } }, data() { diff --git a/src/main/resources/views/platform/sys/union/branchUnionPartUnitManage.js b/src/main/resources/views/platform/sys/union/branchUnionPartUnitManage.js index 4eef5e20..413730bd 100644 --- a/src/main/resources/views/platform/sys/union/branchUnionPartUnitManage.js +++ b/src/main/resources/views/platform/sys/union/branchUnionPartUnitManage.js @@ -16,7 +16,7 @@ const branchUnionPartUnitManage = { - + @@ -48,6 +48,11 @@ const branchUnionPartUnitManage = { union_id: { type: String, required: true + }, + // 父页面可传入可用高度;默认值保持组件在其他页面的原有展示能力。 + table_height: { + type: Number, + default: null } }, data() { diff --git a/src/main/resources/views/platform/sys/union/branchUnionUserManage.js b/src/main/resources/views/platform/sys/union/branchUnionUserManage.js index 20b71059..87e1ce61 100644 --- a/src/main/resources/views/platform/sys/union/branchUnionUserManage.js +++ b/src/main/resources/views/platform/sys/union/branchUnionUserManage.js @@ -45,7 +45,7 @@ const branchUnionUserManage = { - + @@ -123,6 +123,11 @@ const branchUnionUserManage = { union_id: { type: String, required: true + }, + // 父页面可传入可用高度;默认值保持组件在其他页面的原有展示能力。 + table_height: { + type: Number, + default: null } }, data() { diff --git a/src/main/resources/views/platform/zhgh/club/infoManage/exitManage/index.html b/src/main/resources/views/platform/zhgh/club/infoManage/exitManage/index.html index 5ab88f44..b66e30f5 100644 --- a/src/main/resources/views/platform/zhgh/club/infoManage/exitManage/index.html +++ b/src/main/resources/views/platform/zhgh/club/infoManage/exitManage/index.html @@ -38,6 +38,13 @@ layout("/layouts/platform.html"){ + + +