diff --git a/src/main/java/com/budwk/app/MainLauncher.java b/src/main/java/com/budwk/app/MainLauncher.java index 8d088b14..b220ad24 100644 --- a/src/main/java/com/budwk/app/MainLauncher.java +++ b/src/main/java/com/budwk/app/MainLauncher.java @@ -13,6 +13,8 @@ import com.budwk.app.web.commons.base.Globals; import com.budwk.app.web.commons.ext.pubsub.WebPubSub; import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration; import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService; +import com.budwk.app.zhgh.club.service.ClubPayRecordService; +import com.budwk.app.zhgh.club.service.SysClubUserService; import lombok.extern.slf4j.Slf4j; import org.beetl.core.GroupTemplate; import org.nutz.boot.NbApp; @@ -81,6 +83,15 @@ public class MainLauncher { Globals.AppBase = Mvcs.getServletContext().getContextPath(); Globals.AppRoot = Mvcs.getServletContext().getRealPath("/"); init_sys(); + // 2026年10月3日 12:01:28;操作人:wxg:先升级会员表及补齐历史资料,再处理缴费快照和启动计划任务;不依赖 DEBUG。 + SysClubUserService members = ioc.get(SysClubUserService.class, "sysClubUserServiceImpl"); + members.upgradeMemberSchema(); + members.backfillMemberSnapshots(); + // 2026年10月3日 09:05:37;操作人:wxg:缴费表升级独立于 DEBUG,先完成字段检查和旧数据补齐再启动任务。 + // 2026年10月3日 09:28:37;操作人:wxg:按实际注册的实现类 Bean 名称获取服务,避免接口默认名称不存在导致启动失败。 + ClubPayRecordService payRecords = ioc.get(ClubPayRecordService.class, "clubPayRecordServiceImpl"); + payRecords.upgradePayRecordSchema(); + payRecords.backfillPayRecordSnapshots(); init_task(); init_auth(); ioc.get(Globals.class); diff --git a/src/main/java/com/budwk/app/sys/controller/SysLoginController.java b/src/main/java/com/budwk/app/sys/controller/SysLoginController.java index 7ad931a5..9df02a29 100644 --- a/src/main/java/com/budwk/app/sys/controller/SysLoginController.java +++ b/src/main/java/com/budwk/app/sys/controller/SysLoginController.java @@ -56,7 +56,7 @@ public class SysLoginController { private static final Log log = Logs.get(); private static final LoginRsaKeyRing LOGIN_RSA_KEY_RING = new LoginRsaKeyRing(); /** - * TODO: 临时屏蔽 localhost 平台登录验证码,恢复时改为 false。 + * TODO: 临时屏蔽回环地址及指定局域网 IP 的平台登录验证码,全部恢复时改为 false。 */ private static final boolean DISABLE_LOGIN_CAPTCHA = true; @Inject @@ -114,14 +114,15 @@ public class SysLoginController { return Result.error("参数缺失"); } - String lockKey = RedisConstant.USER_LOGIN_LOCK_PREFIX + username; - int errCount = Convert.toInt(StrUtil.blankToDefault(redisService.get(lockKey), "0")); - log.info("用户名:" + username + "登录失败次数:" + errCount); + // TODO: 临时停用所有账号的登录失败次数和 5 分钟锁定限制;恢复时同步取消下方失败计数写入的注释。 + // String lockKey = RedisConstant.USER_LOGIN_LOCK_PREFIX + username; + // int errCount = Convert.toInt(StrUtil.blankToDefault(redisService.get(lockKey), "0")); + // log.info("用户名:" + username + "登录失败次数:" + errCount); - if (errCount > 5) { - redisService.setex(lockKey, 5 * 60, String.valueOf(errCount + 1)); - return Result.error("登录失败次数过多,请5分钟后再试"); - } + // if (errCount > 5) { + // redisService.setex(lockKey, 5 * 60, String.valueOf(errCount + 1)); + // return Result.error("登录失败次数过多,请5分钟后再试"); + // } try { // 验证码校验 if (!isLoginCaptchaDisabled(req)) { @@ -154,7 +155,8 @@ public class SysLoginController { return Result.success("login.success"); } catch (Exception e) { log.error(e.getMessage(), e); - redisService.set(lockKey, Convert.toStr(errCount + 1)); + // TODO: 临时暂停累计登录失败次数;恢复限制时与上方计数读取、锁定拦截一并恢复。 + // redisService.set(lockKey, Convert.toStr(errCount + 1)); String message = e.getMessage(); return Result.error(e.getMessage()); } @@ -165,7 +167,9 @@ public class SysLoginController { return false; } String serverName = req.getServerName(); + // 临时允许该局域网地址免验证码;恢复 IP 验证码时删除此条件,并同步删除登录页中的该 IP。 return "localhost".equalsIgnoreCase(serverName) + || "192.168.0.108".equals(serverName) || "127.0.0.1".equals(serverName) || "0:0:0:0:0:0:0:1".equals(serverName) || "::1".equals(serverName); diff --git a/src/main/java/com/budwk/app/task/job/club/ClubGeneratePayRecordJob.java b/src/main/java/com/budwk/app/task/job/club/ClubGeneratePayRecordJob.java index ebb2f978..58fe3a3b 100644 --- a/src/main/java/com/budwk/app/task/job/club/ClubGeneratePayRecordJob.java +++ b/src/main/java/com/budwk/app/task/job/club/ClubGeneratePayRecordJob.java @@ -1,60 +1,35 @@ package com.budwk.app.task.job.club; import cn.hutool.core.date.DateUtil; -import cn.hutool.json.JSONObject; -import com.budwk.app.web.commons.auth.utils.SecurityUtil; -import com.budwk.app.zhgh.club.model.ClubPayRecord; -import com.budwk.app.zhgh.club.model.ClubUser; -import org.nutz.dao.Cnd; -import org.nutz.dao.Dao; +import com.budwk.app.zhgh.club.service.ClubPayRecordService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.quartz.DisallowConcurrentExecution; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; -import java.util.ArrayList; -import java.util.List; /** * @ClassName ClubGeneratePayRecordJob * @Author JyuHsin * @Date 2026/1/22 16:56 * @Version 1.0 - * @Description 每年的1月1日生成社团成员下年的缴费记录 + * @Description 每年的1月1日生成社团成员当年的缴费记录 */ @IocBean +@DisallowConcurrentExecution public class ClubGeneratePayRecordJob implements Job { @Inject - private Dao dao; + private ClubPayRecordService payRecordService; - @Override - public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { - // 当前年份 - int thisYear = DateUtil.thisYear(); - // 获取最新的社团成员信息 - List clubUserList = dao.query(ClubUser.class, Cnd.NEW()); - // 生成缴费记录对象 - List list = new ArrayList<>(); - for (ClubUser clubUser : clubUserList) { - ClubPayRecord clubPayRecord = new ClubPayRecord(); - clubPayRecord.setYear(thisYear); - clubPayRecord.setClubId(clubUser.getClubId()); - clubPayRecord.setUserId(clubUser.getUserId()); - clubPayRecord.setPayed(false); - - JSONObject log = new JSONObject(); - log.set("operatorUserId", "task"); - log.set("operatorUserName", "定时任务"); - log.set("operatorTime", DateUtil.now()); - log.set("operatorType", "缴费管理-定时任务自动设置"); - log.set("oldValue", null); - log.set("newValue", false); - clubPayRecord.setOperateLogs(List.of(log)); - - list.add(clubPayRecord); - } - dao.insert(list); - } + /** 2026年10月3日 09:05:37;操作人:wxg:生成当年完整快照名单,service 批量去重并在事务中保存。 */ + @Override + @Aop(TransAop.READ_COMMITTED) + public void execute(JobExecutionContext context) throws JobExecutionException { + payRecordService.generateYearPayRecords(DateUtil.thisYear()); + } } diff --git a/src/main/java/com/budwk/app/zhgh/club/controller/apply/ClubUserJoinApplyController.java b/src/main/java/com/budwk/app/zhgh/club/controller/apply/ClubUserJoinApplyController.java index a563d30b..59d147cd 100644 --- a/src/main/java/com/budwk/app/zhgh/club/controller/apply/ClubUserJoinApplyController.java +++ b/src/main/java/com/budwk/app/zhgh/club/controller/apply/ClubUserJoinApplyController.java @@ -87,6 +87,7 @@ public class ClubUserJoinApplyController { Sql sql = Sqls.create(""" SELECT cua.*, + CAST(cua.mode AS SIGNED) AS mode, club.clubName, u.loginname AS loginName, u.username AS userName, @@ -109,14 +110,14 @@ public class ClubUserJoinApplyController { @At @ApiOperation("保存申请") + @Aop(TransAop.READ_COMMITTED) @SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR) @SLog(tag = "社团管理系统-申请入会", msg = "保存社团入会") public Result save(@Param("data") ClubUserApply clubUserApply) { clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name()); - clubUserApply.setMode(true); + clubUserApply.setMode(1); clubUserApply.setApplyDate(new Date()); - clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId())); - clubUserApply.setExitTime(null); + clubUserJoinService.preparePendingJoinTimes(clubUserApply); dao.insertOrUpdate(clubUserApply); return Result.success(); } @@ -151,13 +152,13 @@ public class ClubUserJoinApplyController { if(!mode) { clubUserApply.setId(null); } - clubUserApply.setMode(mode); + // 请求参数沿用 PC/H5 的布尔值;持久化统一使用 1 入会、0 退会,2 仅由管理端生成。 + clubUserApply.setMode(Boolean.TRUE.equals(mode) ? 1 : 0); Date applyDate = new Date(); clubUserApply.setApplyDate(applyDate); if (mode) { clubUserApply.setRoleCode(RoleConstant.CLUB_MEMBER.name()); - clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId())); - clubUserApply.setExitTime(null); + clubUserJoinService.preparePendingJoinTimes(clubUserApply); } else { clubUserApply.setJoinTime(clubUserJoinService.resolveJoinTime(clubUserApply.getClubId(), clubUserApply.getUserId())); clubUserApply.setExitTime(applyDate); @@ -184,6 +185,7 @@ public class ClubUserJoinApplyController { @Aop(TransAop.READ_COMMITTED) @SaCheckPermission(value = {"club.join.apply", "h5.club.join.apply"}, mode = SaMode.OR) public Result submitAgain(@Param("data") ClubUserApply clubUserApply, @Param("taskId") Long taskId) { + clubUserJoinService.preparePendingJoinTimes(clubUserApply); dao.insertOrUpdate(clubUserApply); Dict dict = Dict.create(); diff --git a/src/main/java/com/budwk/app/zhgh/club/controller/apply/ClubUserJoinMineController.java b/src/main/java/com/budwk/app/zhgh/club/controller/apply/ClubUserJoinMineController.java index feeb5544..31d02d19 100644 --- a/src/main/java/com/budwk/app/zhgh/club/controller/apply/ClubUserJoinMineController.java +++ b/src/main/java/com/budwk/app/zhgh/club/controller/apply/ClubUserJoinMineController.java @@ -65,9 +65,30 @@ public class ClubUserJoinMineController { public Result pageData(@Valid PageForm pageForm, @Param("year") Integer year, @Param("clubName") String clubName){ + // 明确列出申请字段,避免分页总数子查询出现重名列;mode 只输出一次整数转换,保留系统处理值 2。 Sql sql = Sqls.create(""" SELECT - info.*, + info.id, + info.clubId, + info.userId, + info.roleCode, + info.clubPosition, + info.email, + info.mobile, + info.birthday, + info.avatar, + info.sameTimeJoinOtherClubSituation, + info.awardsExperience, + info.signature, + info.applyDate, + info.joinTime, + info.exitTime, + info.createdBy, + info.createdAt, + info.updatedBy, + info.updatedAt, + info.delFlag, + CAST(info.mode AS SIGNED) AS mode, club.clubName, u.loginname as loginName, u.username as userName, diff --git a/src/main/java/com/budwk/app/zhgh/club/controller/common/ClubCommonController.java b/src/main/java/com/budwk/app/zhgh/club/controller/common/ClubCommonController.java index 3175236b..860d7e8c 100644 --- a/src/main/java/com/budwk/app/zhgh/club/controller/common/ClubCommonController.java +++ b/src/main/java/com/budwk/app/zhgh/club/controller/common/ClubCommonController.java @@ -39,18 +39,9 @@ public class ClubCommonController { @At @SaCheckLogin + /** 无请求参数;返回入会社团选项,每项包含 id、clubName 等社团字段,同一社团不重复。 */ public Result listClub() { - Sql sql = Sqls.create(""" - SELECT - c.* - FROM - `sys_club` c - LEFT JOIN wf_process_instance inst ON inst.businessNo = c.id - WHERE - inst.state = @state - """).setParam("state", ProcessInstanceStateEnum.FINISHED.getCode()); - List list = baseService.listVO(sql, SysClub.class); - return Result.success(list); + return Result.success(sysClubService.listJoinClubOptions()); } @At diff --git a/src/main/java/com/budwk/app/zhgh/club/controller/examine/ClubExamineApplyController.java b/src/main/java/com/budwk/app/zhgh/club/controller/examine/ClubExamineApplyController.java index 6f037b99..85d0211f 100644 --- a/src/main/java/com/budwk/app/zhgh/club/controller/examine/ClubExamineApplyController.java +++ b/src/main/java/com/budwk/app/zhgh/club/controller/examine/ClubExamineApplyController.java @@ -93,15 +93,30 @@ public class ClubExamineApplyController { return Result.success(myManageClub); } - @At - @SaCheckLogin - public Result getClubUserNum(String clubId, Integer year) { - if(StrUtil.isBlank(clubId)) { - return Result.error("社团信息为空,请核查"); - } - List result = sysClubExamineService.getClubUserNum(clubId, year); - return Result.success(result); - } + // 2026年10月2日wxg屏蔽操作:保留原统计参数处理入口。 +// @At +// @SaCheckLogin +// public Result getClubUserNum(String clubId, Integer year) { +// if(StrUtil.isBlank(clubId)) { +// return Result.error("社团信息为空,请核查"); +// } +// List result = sysClubExamineService.getClubUserNum(clubId, year); +// return Result.success(result); +// } +// +// + /** + * 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。 + * 接收 clubId(所选社团ID)、year(公历年度),基础校验后调用 service; + * 返回 Result,data为两类计数及统计核查信息列表,异常参数返回错误提示。 + */ + @At + @SaCheckLogin + public Result getClubUserNum(String clubId, Integer year) { + if (StrUtil.isBlank(clubId)) return Result.error("社团信息为空,请核查"); + if (year == null || year < 1 || year > 9998) return Result.error("请选择有效统计年度"); + return Result.success(sysClubExamineService.getClubUserNum(clubId, year)); + } @At @SaCheckLogin diff --git a/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubInfoManageController.java b/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubInfoManageController.java index d8889a44..85fe6658 100644 --- a/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubInfoManageController.java +++ b/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubInfoManageController.java @@ -30,6 +30,7 @@ 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.SysClubUserService; +import com.budwk.app.zhgh.club.service.ClubPayRecordService; import com.budwk.app.zhgh.club.vo.ClubCommonPageVo; import com.budwk.app.zhgh.club.vo.ClubUserImportVo; import io.swagger.annotations.ApiOperation; @@ -77,6 +78,8 @@ public class ClubInfoManageController { private SysUserService sysUserService; @Inject private SysClubUserService clubUserService; + @Inject + private ClubPayRecordService payRecordService; @At("") @Ok("beetl:/platform/zhgh/club/infoManage/manage/index.html") @@ -137,19 +140,7 @@ public class ClubInfoManageController { @SaCheckPermission("club.infoManage.manage") @SLog(tag = "社团管理系统-信息管理", msg = "退会") public Result exitClub(@Valid String id) { - ClubUser clubUser = dao.fetch(ClubUser.class, id); - dao.delete(ClubUser.class,id); - List roleCodes = clubUser.getRoleCode(); - for (String roleCode : roleCodes) { - Sys_role sysRole = sysRoleService.getByCode(roleCode); - if (ObjectUtil.isNotEmpty(sysRole)) { - dao.clear(Sys_user_role.class, Cnd.NEW().and("userId", "=", clubUser.getUserId()) - .and("roleId", "=", sysRole.getId()) - .and("clubId", "=", clubUser.getClubId())); - } - } - sysUserService.clearCache(); - sysRoleService.clearCache(); + clubInfoManageService.exitClub(id); return Result.success(); } @@ -310,6 +301,8 @@ public class ClubInfoManageController { clubUser.setClubId(clubId); clubUser.setUserId(user); clubUser.setRoleCode(List.of(roleCode)); + // 2026年10月3日 12:01:28;操作人:wxg:仅新会员调用 service,统一填写确认入会时间和姓名/工号/电话。 + clubUserService.prepareDirectJoinTimes(List.of(clubUser)); } dao.insertOrUpdate(clubUser); @@ -335,6 +328,8 @@ public class ClubInfoManageController { log.set("newValue", payed != null ? payed : false); record.setOperateLogs(List.of(log)); + // 2026年10月3日 09:05:37;操作人:wxg:新增记录先填写人员快照。 + payRecordService.prepareSnapshots(List.of(record)); dao.insert(record); } else { payRecord.setPayed(payed != null ? payed : false); @@ -348,6 +343,8 @@ public class ClubInfoManageController { log.set("newValue", payed != null ? payed : false); payRecord.setOperateLogs(List.of(log)); + // 已有年度记录仅补缺失快照,原有年度资料和加入时间不覆盖。 + payRecordService.prepareSnapshots(List.of(payRecord)); dao.update(payRecord); } } @@ -356,7 +353,13 @@ public class ClubInfoManageController { return Result.success(); } + /** + * @param clubId 左侧选中的社团ID,用于导出该社团登记表 + * @param response 成功时写入 DOCX 附件;失败由统一异常处理返回含 code/msg 的错误结果 + * @return void,附件直接写入响应,不再渲染 JSON 成功结果 + */ @At + @Ok("void") @SaCheckPermission("club.infoManage.manage") @ApiOperation("导出登记表") public void exportRegistrationDoc(@Valid String clubId, HttpServletResponse response){ @@ -412,6 +415,8 @@ public class ClubInfoManageController { cUser.setRoleCode(List.of(RoleConstant.CLUB_MEMBER.name())); clubUserList.add(cUser); } + // 2026年10月3日 12:01:28;操作人:wxg:导入新增会员统一从用户资料批量填写人员字段及后端确认时间。 + clubUserService.prepareDirectJoinTimes(clubUserList); dao.insert(clubUserList); List sysUserRoleList = new ArrayList<>(); @@ -450,6 +455,8 @@ public class ClubInfoManageController { log.set("newValue", false); record.setOperateLogs(List.of(log)); + // 2026年10月3日 09:05:37;操作人:wxg:新增记录先填写人员快照。 + payRecordService.prepareSnapshots(List.of(record)); dao.insert(record); } else { payRecord.setPayed(false); @@ -463,6 +470,8 @@ public class ClubInfoManageController { log.set("newValue", false); payRecord.setOperateLogs(List.of(log)); + // 已有年度记录仅补缺失快照,原有年度资料和加入时间不覆盖。 + payRecordService.prepareSnapshots(List.of(payRecord)); dao.update(payRecord); } } diff --git a/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubPayRecordController.java b/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubPayRecordController.java index fd779bff..9f1b05d1 100644 --- a/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubPayRecordController.java +++ b/src/main/java/com/budwk/app/zhgh/club/controller/infoManage/ClubPayRecordController.java @@ -1,42 +1,22 @@ package com.budwk.app.zhgh.club.controller.infoManage; import cn.dev33.satoken.annotation.SaCheckPermission; -import cn.dev33.satoken.annotation.SaMode; -import cn.hutool.core.date.DateUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; 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.PageForm; import com.budwk.app.base.result.Result; -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.ClubPayRecord; -import com.budwk.app.zhgh.club.model.ClubUserApply; -import com.budwk.app.zhgh.club.model.SysClub; import com.budwk.app.zhgh.club.service.ClubPayRecordService; -import com.budwk.app.zhgh.club.service.SysClubService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; -import org.nutz.dao.Cnd; -import org.nutz.dao.Dao; -import org.nutz.dao.Sqls; -import org.nutz.dao.sql.Sql; -import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.aop.interceptor.ioc.TransAop; +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.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; import javax.validation.Valid; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; /** * @ClassName ClubPayRecordController @@ -55,8 +35,6 @@ public class ClubPayRecordController { @Inject private ClubPayRecordService payRecordService; - @Inject - private SysClubService sysClubService; @At("") @SaCheckPermission("club.infoManage.pay") @@ -66,135 +44,40 @@ public class ClubPayRecordController { @At @SaCheckPermission("club.infoManage.pay") + /** + * 2026年10月3日 14:33:18;操作人:wxg:接收查询条件,业务筛选交给 service。 + * pageForm 为分页和姓名/工号关键字;year 为年度、clubId 为社团ID; + * payed/assign 为缴费/拨付状态(1是、0否、空值不限);memberState 为当前会员状态(1在会、0退会、空值全部)。 + * 返回 Result,data 为分页对象,包含 list、totalCount 等;list.count 为当前会员关系是否存在(1在会、0退会)。 + */ public Result pageData(@Valid PageForm pageForm, @Param("year") Integer year, @Param("clubId") String clubId, @Param("payed") Integer payed, - @Param("assign") Integer assign) { - Sql sql = Sqls.create(""" - SELECT - cpr.*, - c.clubName, - u.loginName, - u.userName, - u.unitName, - u.retireDate, - (SELECT createdAt FROM club_user WHERE clubId = cpr.clubId AND userId = cpr.userId ORDER BY createdAt LIMIT 1) AS applyTime, - u.userState, - (select count(1) from club_user where clubId = cpr.clubId and userId = cpr.userId) as count - FROM - `club_pay_record` cpr - LEFT JOIN sys_club c ON cpr.clubid = c.id - LEFT JOIN vw_user u ON u.id = cpr.userId - $condition - """); - Cnd cnd = Cnd.NEW(); - List clubIdList = sysClubService.getMyManageClub().stream() - .map(SysClub::getId) - .toList(); - cnd.andEX("cpr.year", "=", year); - if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) { - cnd.andEX("cpr.clubId", "in", clubIdList); - }else{ - cnd.andEX("cpr.clubId", "=", clubId); - } - cnd.andEX("cpr.payed", "=", payed); - if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { - SqlExpressionGroup seg = new SqlExpressionGroup(); - seg.orLike("u.userName", pageForm.getSearchKeyword()); - seg.orLike("u.loginName", pageForm.getSearchKeyword()); - cnd.and(seg); - } + @Param("assign") Integer assign, + @Param("memberState") Integer memberState) { + if (memberState != null && memberState != 0 && memberState != 1) { + return Result.error("会员状态只能为在会、退会或全部"); + } + return Result.success(payRecordService.pageData(pageForm, year, clubId, payed, assign, memberState)); + } - if (assign != null) { - SqlExpressionGroup mainGroup = new SqlExpressionGroup(); - if (assign == 0) { - SqlExpressionGroup noAssignGroup = new SqlExpressionGroup(); - - // 不拨付条件1:payed=false(原代码第一行直接return false) - noAssignGroup.or("cpr.payed", "=", false); - - // 不拨付条件2:payed=true,但用户状态是退休 且 无退休日期 - SqlExpressionGroup retireNoDateGroup = new SqlExpressionGroup(); - retireNoDateGroup.and("cpr.payed", "=", true); - retireNoDateGroup.and("u.userState", "=", "退休"); - retireNoDateGroup.and("u.retireDate", "is", null); - - // 不拨付条件3:payed=true,用户状态是退休,有退休日期,但退休年份 < 缴费年份 - SqlExpressionGroup retireYearLessGroup = new SqlExpressionGroup(); - retireYearLessGroup.and("cpr.payed", "=", true); - retireYearLessGroup.and("u.userState", "=", "退休"); - retireYearLessGroup.and("u.retireDate", "is not", null); - // 计算缴费年份 - // 退休年份 < 缴费年份 - retireYearLessGroup.and("YEAR(u.retireDate)", "<", "cpr.year"); - - // 合并所有不拨付条件(满足任一即不拨付) - noAssignGroup.or(retireNoDateGroup); - noAssignGroup.or(retireYearLessGroup); - - mainGroup.and(noAssignGroup); - } else { - SqlExpressionGroup assignGroup = new SqlExpressionGroup(); - - // 拨付前提:payed必须为true - assignGroup.and("cpr.payed", "=", true); - - // 拨付条件1:用户状态≠退休 - SqlExpressionGroup userNotRetireGroup = new SqlExpressionGroup(); - userNotRetireGroup.or("u.userState", "<>", "退休"); - - // 拨付条件2:用户状态=退休,有退休日期,且退休年份≥缴费年份 - SqlExpressionGroup retireQualifiedGroup = new SqlExpressionGroup(); - retireQualifiedGroup.and("u.userState", "=", "退休"); - retireQualifiedGroup.and("u.retireDate", "is not", null); - retireQualifiedGroup.and("YEAR(u.retireDate)", ">=", "cpr.year"); - - // 合并拨付条件(满足任一即拨付) - userNotRetireGroup.or(retireQualifiedGroup); - assignGroup.and(userNotRetireGroup); - - mainGroup.and(assignGroup); - } - cnd.and(mainGroup); - } - - sql.setCondition(cnd); - Pagination pagination = payRecordService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); - - List list = pagination.getList(); - for (NutMap nutMap : list) { - boolean assigned = payRecordService.calAssigned(nutMap.getString("id")); - nutMap.put("assign", assigned); - } - - return Result.success(pagination); + /** 返回当前用户授权社团的下拉选项,字段包含 id、clubName、clubCode。 */ + @At + @SaCheckPermission("club.infoManage.pay") + public Result listClubs() { + return Result.success(payRecordService.getPayManageClubList()); } @At @ApiOperation("修改社团成员缴费状态") @SaCheckPermission("club.infoManage.pay") @SLog(tag = "社团管理系统-缴费记录", msg = "修改社团成员缴费状态") + @Aop(TransAop.READ_COMMITTED) public Result handle(@Param("ids") String[] ids, @Param("payed") Boolean payed) { - List list = payRecordService.query(Cnd.where("id", "in", ids)); - - for (ClubPayRecord payRecord : list) { - List logs = Optional.ofNullable(payRecord.getOperateLogs()).orElseGet(ArrayList::new); - JSONObject log = new JSONObject(); - log.set("operatorUserId", SecurityUtil.getUserId()); - log.set("operatorUserName", SecurityUtil.getUserUsername()); - log.set("operatorTime", DateUtil.now()); - log.set("operatorType", "缴费管理-手动设置"); - log.set("oldValue", payRecord.getPayed()); - log.set("newValue", payed); - logs.add(log); - - payRecord.setOperateLogs(logs); - payRecord.setPayed(payed); - } - - payRecordService.update(list); + // 2026年10月3日 09:51:00;操作人:wxg:controller 只接收参数,缴费、拨付结果和日志由 service 在同一事务内保存。 + payRecordService.updatePayStatus(ids, payed); return Result.success(); } } diff --git a/src/main/java/com/budwk/app/zhgh/club/interceptor/ClubUserExitInterceptor.java b/src/main/java/com/budwk/app/zhgh/club/interceptor/ClubUserExitInterceptor.java index 915bb359..d9c8fac3 100644 --- a/src/main/java/com/budwk/app/zhgh/club/interceptor/ClubUserExitInterceptor.java +++ b/src/main/java/com/budwk/app/zhgh/club/interceptor/ClubUserExitInterceptor.java @@ -13,6 +13,7 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord; import com.budwk.app.zhgh.club.model.ClubUser; import com.budwk.app.zhgh.club.model.ClubUserApply; import com.budwk.app.zhgh.club.service.SysClubUserService; +import com.budwk.app.zhgh.club.service.ClubPayRecordService; import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; @@ -43,6 +44,8 @@ public class ClubUserExitInterceptor implements FlowInterceptor { // 删人 dao.clear(ClubUser.class, Cnd.where(ClubUser::getUserId, "=", clubUserApply.getUserId()).and(ClubUser::getClubId, "=", clubUserApply.getClubId())); + // 2026年10月3日 09:51:00;操作人:wxg:WF 退会完成后,同步将该社团会员的拨付字段置为 false。 + ServiceContext.find(ClubPayRecordService.class).disableExitMemberPayment(clubUserApply.getClubId(), clubUserApply.getUserId()); // 删角色 dao.clear(Sys_user_role.class, Cnd.where(Sys_user_role::getUserId, "=", clubUserApply.getUserId()).and(Sys_user_role::getClubId, "=", clubUserApply.getClubId())); } diff --git a/src/main/java/com/budwk/app/zhgh/club/interceptor/ClubUserJoinInterceptor.java b/src/main/java/com/budwk/app/zhgh/club/interceptor/ClubUserJoinInterceptor.java index 690935c9..7d8d1d8d 100644 --- a/src/main/java/com/budwk/app/zhgh/club/interceptor/ClubUserJoinInterceptor.java +++ b/src/main/java/com/budwk/app/zhgh/club/interceptor/ClubUserJoinInterceptor.java @@ -12,6 +12,8 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord; import com.budwk.app.zhgh.club.model.ClubUser; import com.budwk.app.zhgh.club.model.ClubUserApply; import com.budwk.app.zhgh.club.service.SysClubUserService; +import com.budwk.app.zhgh.club.service.ClubUserJoinService; +import com.budwk.app.zhgh.club.service.ClubPayRecordService; import org.bouncycastle.jcajce.provider.util.SecretKeyUtil; import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; @@ -41,9 +43,20 @@ public class ClubUserJoinInterceptor implements FlowInterceptor { // 获取操作类 Dao dao = ServiceContext.find(Dao.class); + // 入会时间以本次会长同意审核任务的完成时间为准,交由 service 校验并写入申请表。 + clubUserApply.setJoinTime(ServiceContext.find(ClubUserJoinService.class).completeJoinApproval( + execution.getProcessInstance().getBusinessNo(), execution.getProcessTaskId())); // 将申请信息复制一份,插入社团成员表 ClubUser clubUser = BeanUtil.copyProperties(clubUserApply, ClubUser.class); + // 读取本次已写入的入会申请时间,不能取成立时间或用插入时刻替代审核完成时间。 + ClubUserApply approvedApply = dao.fetch(ClubUserApply.class, execution.getProcessInstance().getBusinessNo()); + if (approvedApply == null || approvedApply.getJoinTime() == null) { + throw new IllegalArgumentException("本次入会申请尚未生成审核完成时间"); + } + clubUser.setJoinTime(approvedApply.getJoinTime()); + // 2026年10月3日 12:01:28;操作人:wxg:审核入会保存当前用户资料,joinTime 仍使用本次会长审核完成时间。 + ServiceContext.find(SysClubUserService.class).prepareMemberSnapshots(List.of(clubUser)); dao.insert(clubUser); // 还要加到社团成员缴费表中 @@ -70,6 +83,8 @@ public class ClubUserJoinInterceptor implements FlowInterceptor { log.set("newValue", payed); record.setOperateLogs(List.of(log)); + // 2026年10月3日 09:05:37;操作人:wxg:审核新增缴费记录,保存本次会员加入时间及人员快照。 + ServiceContext.find(ClubPayRecordService.class).prepareSnapshots(List.of(record)); dao.insert(record); } else { payRecord.setPayed(payed); @@ -83,6 +98,8 @@ public class ClubUserJoinInterceptor implements FlowInterceptor { log.set("newValue", payed); payRecord.setOperateLogs(List.of(log)); + // 同年度重新入会复用原缴费记录,只补缺失资料,不重写已保存的年度快照。 + ServiceContext.find(ClubPayRecordService.class).prepareSnapshots(List.of(payRecord)); dao.update(payRecord); } diff --git a/src/main/java/com/budwk/app/zhgh/club/model/ClubPayRecord.java b/src/main/java/com/budwk/app/zhgh/club/model/ClubPayRecord.java index 1c03ec1c..ce9e83ac 100644 --- a/src/main/java/com/budwk/app/zhgh/club/model/ClubPayRecord.java +++ b/src/main/java/com/budwk/app/zhgh/club/model/ClubPayRecord.java @@ -8,6 +8,7 @@ import org.nutz.dao.entity.annotation.*; import org.nutz.dao.interceptor.annotation.PrevInsert; import java.util.List; +import java.util.Date; /** * @ClassName ClubPayRecord @@ -56,6 +57,38 @@ public class ClubPayRecord extends BaseModel { @Default(value = "0") private Boolean isGiveMoney; + + // 2026年10月3日 09:05:37;操作人:wxg:保存年度名单人员快照,列表不再联查用户视图。 + @Column + @Comment("工号") + @ColDefine(type = ColType.VARCHAR, width = 120) + private String loginName; + + @Column + @Comment("姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String userName; + + @Column + @Comment("所在单位") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String unitName; + + @Column + @Comment("人员类型") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String personType; + + @Column + @Comment("加入时间") + @ColDefine(type = ColType.DATETIME) + private Date joinTime; + + @Column + @Comment("退休时间") + @ColDefine(type = ColType.DATETIME) + private Date retireDate; + // 防止有人今天操作缴费,明天操作未缴费,还是记录一下吧 @Column @Comment("操作记录") diff --git a/src/main/java/com/budwk/app/zhgh/club/model/ClubUser.java b/src/main/java/com/budwk/app/zhgh/club/model/ClubUser.java index 266c9d3c..19e1a672 100644 --- a/src/main/java/com/budwk/app/zhgh/club/model/ClubUser.java +++ b/src/main/java/com/budwk/app/zhgh/club/model/ClubUser.java @@ -7,6 +7,7 @@ import org.nutz.dao.entity.annotation.*; import org.nutz.dao.interceptor.annotation.PrevInsert; import java.util.ArrayList; +import java.util.Date; import java.util.List; @Data @@ -62,5 +63,25 @@ public class ClubUser extends BaseModel { @ColDefine(type = ColType.VARCHAR, width = 500) private String awardsExperience; + /** 审核入会取本次申请的 joinTime;直接录入或导入取后端确认时间。 */ + @Column + @Comment("入会时间") + @ColDefine(type = ColType.DATETIME) + private Date joinTime; + + /** 2026年10月3日 12:01:28;操作人:wxg:会员资料在加入时保存,供会员列表直接展示和姓名/工号查询。 */ + @Column + @Comment("姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) private String userName; + + @Column + @Comment("工号") + @ColDefine(type = ColType.VARCHAR, width = 120) + private String loginName; + + @Column + @Comment("电话") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String mobile; } diff --git a/src/main/java/com/budwk/app/zhgh/club/model/ClubUserApply.java b/src/main/java/com/budwk/app/zhgh/club/model/ClubUserApply.java index ff3a21d4..d4521de3 100644 --- a/src/main/java/com/budwk/app/zhgh/club/model/ClubUserApply.java +++ b/src/main/java/com/budwk/app/zhgh/club/model/ClubUserApply.java @@ -37,9 +37,9 @@ public class ClubUserApply extends BaseModel { private String roleCode; @Column - @Comment("加入/退出") - @ColDefine(type = ColType.BOOLEAN) - private Boolean mode; + @Comment("申请模式:0退出、1加入、2系统处理退会") + @ColDefine(type = ColType.INT) + private Integer mode; @Column @Comment("社团职务") diff --git a/src/main/java/com/budwk/app/zhgh/club/param/ClubUserPageForm.java b/src/main/java/com/budwk/app/zhgh/club/param/ClubUserPageForm.java index 236b6179..0223c94e 100644 --- a/src/main/java/com/budwk/app/zhgh/club/param/ClubUserPageForm.java +++ b/src/main/java/com/budwk/app/zhgh/club/param/ClubUserPageForm.java @@ -18,7 +18,17 @@ public class ClubUserPageForm extends PageForm { private String clubId; private String clubName; private String personType; + /** 重点关注名单:true 同时筛选自聘人员、其他;false 或未传时不增加该限制。 */ + private Boolean focusMembers; private String userState; + /** 会员入会审核开始日期,前端传 yyyy-MM-dd;为空时不限制开始时间。 */ + private String joinDateStart; + /** 会员入会审核结束日期,前端传 yyyy-MM-dd,包含当天;为空时不限制结束时间。 */ + private String joinDateEnd; + /** 2026年10月3日 19:42:45;操作人:wxg:退会日期范围起点,传 yyyy-MM-dd,空值不限制。 */ + private String exitDateStart; + /** 2026年10月3日 19:42:45;操作人:wxg:退会日期范围终点,传 yyyy-MM-dd,包含当天,空值不限制。 */ + private String exitDateEnd; private String unitId; private Integer applyType; private Integer auditType; diff --git a/src/main/java/com/budwk/app/zhgh/club/service/ClubPayRecordService.java b/src/main/java/com/budwk/app/zhgh/club/service/ClubPayRecordService.java index 8b4375ec..001ab83b 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/ClubPayRecordService.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/ClubPayRecordService.java @@ -2,6 +2,10 @@ package com.budwk.app.zhgh.club.service; import com.budwk.app.base.service.BaseService; import com.budwk.app.zhgh.club.model.ClubPayRecord; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import org.nutz.lang.util.NutMap; +import java.util.List; /** * @ClassName ClubPayRecordService @@ -12,6 +16,55 @@ import com.budwk.app.zhgh.club.model.ClubPayRecord; */ public interface ClubPayRecordService extends BaseService { + /** 2026年10月3日 09:05:37;操作人:wxg:定向检查并新增缴费表缺失列;不删除字段,不生成独立升级 SQL 文件。 */ + void upgradePayRecordSchema(); + + /** 2026年10月3日 09:05:37;操作人:wxg:启动时补齐旧记录缺失快照,不覆盖已保存资料;返回 void。 */ + void backfillPayRecordSnapshots(); + + /** + * 2026年10月3日 09:05:37;操作人:wxg:批量填写新增缴费记录的人员快照。 + * @param records 已设置 year、clubId、userId 的缴费记录;现有人员快照不覆盖,空加入时间取有效会员 joinTime;根据当前会员关系及缴费状态计算拨付结果 + * @return void;填写工号、姓名、单位、人员类型、退休日期、加入时间和 isGiveMoney,由调用方在事务中保存 + */ + void prepareSnapshots(List records); + + /** 2026年10月3日 09:51:00;操作人:wxg:设置缴费状态并保存拨付结果及日志。 + * @param ids 非空缴费记录ID数组;只允许操作当前用户授权社团的记录 + * @param payed true已缴费、false未缴费,不能为空 + * @return void;参数错误或记录不在授权范围时抛出异常,事务回滚 + */ + void updatePayStatus(String[] ids, Boolean payed); + + /** 2026年10月3日 09:51:00;操作人:wxg:退会后将该用户在指定社团的各年度缴费记录置为不拨付。 + * @param clubId 退会社团ID,不能为空 + * @param userId 退会用户ID,不能为空 + * @return void;保留缴费状态和人员快照,仅更新 isGiveMoney + */ + void disableExitMemberPayment(String clubId, String userId); + + /** + * 2026年10月3日 09:05:37;操作人:wxg:按当前会员生成指定年度名单,已存在的会员记录不重复生成、不重置缴费。 + * @param year 要生成名单的公历年度,年度计划任务传当前年份 + * @return 本次实际新增的缴费记录条数 + */ + int generateYearPayRecords(Integer year); + + /** 返回当前用户可查询的社团选项,每项包含 id、clubName、clubCode;无授权社团时返回空列表。 */ + List getPayManageClubList(); + + /** + * 按权限范围分页查询缴费记录,并按社团编号及缴费记录保存的加入时间排序。 + * @param pageForm 页码、每页条数及姓名/工号关键字 + * @param year 缴费年度,空值表示不限制 + * @param clubId 社团ID,空值表示查询全部授权社团 + * @param payed 缴费状态:1已缴费、0未缴费、空值不限 + * @param assign 保存的拨付状态:1拨付、0不拨付、空值不限 + * 2026年10月3日 14:33:18;操作人:wxg:在分页前按实时会员关系筛选,历史年度也使用当前会员状态。 + * @param memberState 1在会、0退会、null不限;在会以同一 clubId/userId 存在 club_user 记录为准 + * @return 分页记录,list 包含缴费字段、社团及人员信息、applyTime(保存的入会时间)与 assign(isGiveMoney 保存值转换为布尔值) + */ + Pagination pageData(PageForm pageForm, Integer year, String clubId, Integer payed, Integer assign, Integer memberState); /** * 获取社团成员是否拨付 diff --git a/src/main/java/com/budwk/app/zhgh/club/service/ClubUserJoinService.java b/src/main/java/com/budwk/app/zhgh/club/service/ClubUserJoinService.java index bf195fa3..73e73476 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/ClubUserJoinService.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/ClubUserJoinService.java @@ -5,6 +5,20 @@ import com.budwk.app.zhgh.club.model.ClubUserApply; public interface ClubUserJoinService extends BaseService { + /** + * 清除待审核入会申请的入退会时间,防止沿用协会成立时间或上次入会时间。 + * @param application 待保存、提交或重新提交的申请;仅处理 mode=1 的入会申请 + */ + void preparePendingJoinTimes(ClubUserApply application); + + /** + * 会长同意审核后,将本次 WF 任务的实际完成时间写入申请入会时间。 + * @param applicationId 当前流程 businessNo 对应的入会申请ID + * @param taskId 当前会长审核任务ID,必须属于该申请且已完成、同意 + * @return 写入 club_user_apply.joinTime 的审核完成时间;无有效审核任务时抛出异常 + */ + java.util.Date completeJoinApproval(String applicationId, Long taskId); + /** * 构造退会申请数据。 * 当用户没有历史入会申请记录时,使用当前社团成员关系和人员基础信息兜底生成一份退会申请, @@ -17,8 +31,8 @@ public interface ClubUserJoinService extends BaseService { ClubUserApply buildExitApply(String clubId, String userId); /** - * 获取社团成员的入会时间,优先取历史有效入会申请记录, - * 没有时兜底为社团创办时间。 + * 获取社团成员的入会时间,优先取当前 club_user.joinTime。 + * 旧会员关系尚无入会时间时,保留历史申请及社团创办时间的原有回退规则。 * * @param clubId 社团ID * @param userId 用户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 1a96c476..3bc943aa 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,12 +13,24 @@ import java.util.List; public interface SysClubExamineService extends BaseService { + // 2026年10月2日wxg屏蔽操作:保留原统计接口说明。 +// /** +// * 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。 +// * +// * @param clubId 社团ID +// * @param year 统计年度 +// * @return 在职和退休人员的年初、增加、减少、年末人数 +// */ +// List getClubUserNum(@Valid String clubId, Integer year); +// +// /** - * 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。 - * - * @param clubId 社团ID - * @param year 统计年度 - * @return 在职和退休人员的年初、增加、减少、年末人数 + * 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。 + * @param clubId 所选社团ID,不能为空 + * @param year 所选公历年度,1至9998;按该年1月1日至下一年1月1日左闭右开统计 + * @return 两类人员计数列表:yearFirstNum年初、yearAddNum增加、yearReduceNum减少、thisYearNum公式年末; + * personType为类别,userState仅兼容已有查看页标签;statisticsWarnings为核查提示, + * statisticsValid=false时各人数为null,表示数据缺失或冲突,不能当作零人 */ List getClubUserNum(@Valid String clubId, Integer year); diff --git a/src/main/java/com/budwk/app/zhgh/club/service/SysClubInfoManageService.java b/src/main/java/com/budwk/app/zhgh/club/service/SysClubInfoManageService.java index 74ff1ba2..fd42b40f 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/SysClubInfoManageService.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/SysClubInfoManageService.java @@ -22,6 +22,13 @@ public interface SysClubInfoManageService extends BaseService Pagination infoManagePageData(@Valid ClubUserPageForm pageForm); + /** + * 修改时间:2026年10月2日 23:06:55;操作人:wxg。 + * 当前会员查询前,用已完成 WF 入会流程的最近一次会长同意审核时间补齐空入会时间。 + * @param pageForm clubId 为选中社团ID;radioType=4 查历史成员且不补齐,其他值查当前会员; + * 姓名/工号、人员类型、在职状态、入会日期范围及分页排序参数用于列表筛选 + * @return 会员分页对象,list 中 joinTime 为会员关系保存的入会时间;历史成员仍取申请表时间 + */ Pagination infoManageUserPageData(@Valid ClubUserPageForm pageForm); Pagination exitManagePageData(@Valid ClubUserPageForm pageForm); @@ -30,6 +37,13 @@ public interface SysClubInfoManageService extends BaseService ClubUserJoinVo exitManageInfo(String id); + /** + * 管理端直接退会,同时新增系统处理记录并清理当前社团会员关系及角色关联。 + * @param membershipId club_user 的会员关系ID,不是用户ID或申请ID + * @return void;会员关系不存在或已退会时抛出异常,事务回滚 + */ + void exitClub(String membershipId); + List getClubTreeData(); /** diff --git a/src/main/java/com/budwk/app/zhgh/club/service/SysClubService.java b/src/main/java/com/budwk/app/zhgh/club/service/SysClubService.java index 68ca80f1..6cc44c48 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/SysClubService.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/SysClubService.java @@ -23,6 +23,12 @@ import java.util.List; **/ public interface SysClubService extends BaseService { + /** + * 获取入会申请的社团下拉选项,供 PC 与 H5 共用。 + * @return 存在已完成 WF 流程的社团列表,每项为 SysClub,包含 id、clubName 等社团字段;同一社团只返回一次 + */ + List listJoinClubOptions(); + Pagination getAllClubWithPage(@Valid PageForm pageForm, Cnd cnd); ClubRegisterVo findOne(String clubId); diff --git a/src/main/java/com/budwk/app/zhgh/club/service/SysClubUserService.java b/src/main/java/com/budwk/app/zhgh/club/service/SysClubUserService.java index 00b57ca3..dadeecff 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/SysClubUserService.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/SysClubUserService.java @@ -17,6 +17,32 @@ import java.util.List; **/ public interface SysClubUserService extends BaseService { + /** + * 为直接新增的会员关系设置本批次的后端确认时间,不覆盖已有入会时间。 + * @param members 手工录入、导入或社团创建时直接新增的会员关系;不得传入审核入会记录 + * @return void;仅填写列表内 joinTime 为空的对象,由调用方在同一事务中保存 + */ + void prepareDirectJoinTimes(List members); + + /** + * 2026年10月3日 12:01:28;操作人:wxg:启动时定向补充会员表缺失列,不删除列或调整索引。 + * @return void;DDL 失败则抛出异常,阻止缺少字段的应用继续启动 + */ + void upgradeMemberSchema(); + + /** + * 启动升级时补齐缺失的姓名、工号、电话及 WF 会长审核入会时间;不覆盖非空历史数据。 + * @return void;没有用户资料或有效 WF 审核记录的字段保留为空 + */ + void backfillMemberSnapshots(); + + /** + * 为真正新增的会员关系批量填写用户当前姓名、工号、电话,不修改入会时间。 + * @param members 待新增的会员关系,必须包含有效 userId;不得传入仅修改身份的既有关系 + * @return void;填写传入对象的 userName/loginName/mobile,由调用方在同一事务中保存 + */ + void prepareMemberSnapshots(List members); + List getClubUser(@Valid String clubId); Pagination pageDataByApplyJoinClubAudit(@Valid ClubUserPageForm pageForm); diff --git a/src/main/java/com/budwk/app/zhgh/club/service/impl/ClubPayRecordServiceImpl.java b/src/main/java/com/budwk/app/zhgh/club/service/impl/ClubPayRecordServiceImpl.java index 67fa9761..0ae6649b 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/impl/ClubPayRecordServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/impl/ClubPayRecordServiceImpl.java @@ -3,13 +3,35 @@ package com.budwk.app.zhgh.club.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.service.impl.BaseServiceImpl; -import com.budwk.app.sys.models.Sys_user; +import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.dao.util.cri.Static; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import java.util.List; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.zhgh.club.model.ClubUser; +import cn.hutool.json.JSONObject; +import org.nutz.dao.util.Daos; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Date; import com.budwk.app.zhgh.club.model.ClubPayRecord; import com.budwk.app.zhgh.club.service.ClubPayRecordService; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.lang.Lang; import java.util.Objects; @@ -27,6 +49,301 @@ public class ClubPayRecordServiceImpl extends BaseServiceImpl imp super(dao); } + + // 2026年10月3日 09:51:00;操作人:wxg:列表只读取已保存结果;缴费设置和退会操作负责更新 isGiveMoney。 + private static final String ASSIGNED_SQL = "CASE WHEN cpr.isGiveMoney = true THEN 1 ELSE 0 END"; + + @Override + @Aop(TransAop.READ_COMMITTED) + public void upgradePayRecordSchema() { + // 定向检查独立于 DEBUG;DDL 在 MySQL 中会隐式提交,失败时终止升级以免运行缺列查询。 + if (!dao().exists("club_pay_record")) { + dao().create(ClubPayRecord.class, false); + } else { + Daos.migration(dao(), ClubPayRecord.class, true, false, false); + } + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void backfillPayRecordSnapshots() { + // 已有工号或姓名表示快照已建立;退休日期为空可能是真实资料,不能每次启动重新覆盖快照。 + dao().execute(Sqls.create(""" + UPDATE club_pay_record cpr + INNER JOIN vw_user u ON u.id = cpr.userId + SET cpr.loginName = COALESCE(cpr.loginName, u.loginName), + cpr.userName = COALESCE(cpr.userName, u.userName), + cpr.unitName = COALESCE(cpr.unitName, u.unitName), + cpr.personType = COALESCE(cpr.personType, u.personType), + cpr.retireDate = COALESCE(cpr.retireDate, u.retireDate) + WHERE cpr.loginName IS NULL AND cpr.userName IS NULL + """)); + // 按年度批量补时间,不能将本年度重新入会时间写入之前年度;无可靠来源保持为空。 + for (NutMap row : listMap(Sqls.create("SELECT DISTINCT year FROM club_pay_record WHERE joinTime IS NULL AND year IS NOT NULL"))) { + int year = row.getInt("year"); + Sql sql = Sqls.create(""" + UPDATE club_pay_record cpr + LEFT JOIN ( + SELECT clubId, userId, MIN(joinTime) AS joinTime FROM club_user + WHERE joinTime < @yearEnd GROUP BY clubId, userId + ) memberTime ON memberTime.clubId = cpr.clubId AND memberTime.userId = cpr.userId + LEFT JOIN ( + SELECT application.clubId, application.userId, MAX(task.finishTime) AS joinTime + FROM club_user_apply application + INNER JOIN wf_process_instance instance ON instance.businessNo = application.id + INNER JOIN wf_process_task task ON task.processInstanceId = instance.id + WHERE application.mode = 1 AND instance.state = 20 AND task.taskState = 20 + AND task.taskName = '1625a683-3890-4788-95b7-cab8240f6731' + AND JSON_UNQUOTE(JSON_EXTRACT(task.variable, '$.submitType')) = '1' + AND task.finishTime < @yearEnd + GROUP BY application.clubId, application.userId + ) approvalTime ON approvalTime.clubId = cpr.clubId AND approvalTime.userId = cpr.userId + SET cpr.joinTime = COALESCE(memberTime.joinTime, approvalTime.joinTime) + WHERE cpr.year = @year AND cpr.joinTime IS NULL + AND COALESCE(memberTime.joinTime, approvalTime.joinTime) IS NOT NULL + """); + sql.setParam("year", year); + sql.setParam("yearEnd", (year + 1) + "-01-01"); + dao().execute(sql); + } + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void prepareSnapshots(List records) { + if (records == null || records.isEmpty()) { + return; + } + // 新增记录须明确所属年度、社团和人员,避免缺失参数形成不受限制的批量查询。 + for (ClubPayRecord record : records) { + if (record == null || record.getYear() == null || record.getYear() < 1 || record.getYear() > 9998 + || StrUtil.isBlank(record.getClubId()) || StrUtil.isBlank(record.getUserId())) { + throw new IllegalArgumentException("缴费记录必须提供有效年度、社团和用户"); + } + } + List userIds = records.stream().map(ClubPayRecord::getUserId).distinct().toList(); + List clubIds = records.stream().map(ClubPayRecord::getClubId).distinct().toList(); + Map users = new HashMap<>(); + for (View_user user : dao().query(View_user.class, Cnd.where("id", "in", userIds))) { + users.put(user.getId(), user); + } + Map joinTimes = new HashMap<>(); + Set activeMembers = new HashSet<>(); + for (ClubUser member : dao().query(ClubUser.class, Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds))) { + activeMembers.add(member.getClubId() + ":" + member.getUserId()); + if (member.getJoinTime() != null) { + joinTimes.merge(member.getClubId() + ":" + member.getUserId(), member.getJoinTime(), + (first, next) -> first.before(next) ? first : next); + } + } + for (ClubPayRecord record : records) { + View_user user = users.get(record.getUserId()); + if (user != null && record.getLoginName() == null && record.getUserName() == null) { + record.setLoginName(user.getLoginname()); + record.setUserName(user.getUsername()); + if (record.getUnitName() == null) record.setUnitName(user.getUnitName()); + if (record.getPersonType() == null) record.setPersonType(user.getPersonType()); + if (record.getRetireDate() == null) record.setRetireDate(user.getRetireDate()); + } + Date joinTime = joinTimes.get(record.getClubId() + ":" + record.getUserId()); + if (record.getJoinTime() == null && joinTime != null + && (record.getYear() == null || joinTime.before(DateUtil.parse((record.getYear() + 1) + "-01-01")))) { + record.setJoinTime(joinTime); + } + // 2026年10月3日 09:51:00;操作人:wxg:新增名单或入会更新缴费时,同步保存资格;退会成员优先判定不拨付。 + record.setIsGiveMoney(qualifiesForPayment(record, activeMembers.contains(record.getClubId() + ":" + record.getUserId()))); + } + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public int generateYearPayRecords(Integer year) { + if (year == null || year < 1 || year > 9998) { + throw new IllegalArgumentException("缴费年度不正确"); + } + Set recorded = new HashSet<>(); + for (ClubPayRecord record : dao().query(ClubPayRecord.class, Cnd.where("year", "=", year))) { + recorded.add(record.getClubId() + ":" + record.getUserId()); + } + List records = new ArrayList<>(); + for (ClubUser member : dao().query(ClubUser.class, Cnd.NEW())) { + if (!recorded.add(member.getClubId() + ":" + member.getUserId())) { + continue; + } + ClubPayRecord record = new ClubPayRecord(); + record.setYear(year); + record.setClubId(member.getClubId()); + record.setUserId(member.getUserId()); + // 加入时间与人员资料交由 prepareSnapshots 批量读取,并校验所属年度。 + record.setPayed(false); + JSONObject log = new JSONObject(); + log.set("operatorUserId", "task"); + log.set("operatorUserName", "定时任务"); + log.set("operatorTime", DateUtil.now()); + log.set("operatorType", "缴费管理-定时任务自动设置"); + log.set("oldValue", null); + log.set("newValue", false); + record.setOperateLogs(List.of(log)); + records.add(record); + } + prepareSnapshots(records); + if (!records.isEmpty()) { + dao().insert(records); + } + return records.size(); + } + + /** 校工会社团管理员与系统管理员可查看全部社团缴费记录。 */ + private boolean canManageAllPayClubs() { + return AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name()); + } + + /** 普通菜单授权用户仅能查看其有效角色关联的社团;无关联时显式返回空范围。 */ + private void addPayClubScope(Cnd cnd, String clubField) { + if (!canManageAllPayClubs()) { + // 用户ID使用 SQL 参数绑定,社团集合交给 Cnd 处理,避免空集合被可选条件忽略。 + Sql scopeSql = Sqls.create(""" + SELECT DISTINCT ur.clubId FROM sys_user_role ur + INNER JOIN sys_role r ON r.id = ur.roleId AND r.disabled = false + WHERE ur.userId = @userId AND ur.enable = true AND ur.clubId IS NOT NULL + """).setParam("userId", SecurityUtil.getUserId()); + List clubIds = listMap(scopeSql).stream() + .map(row -> row.getString("clubId")) + .filter(StrUtil::isNotBlank) + .toList(); + if (clubIds.isEmpty()) { + cnd.and(new Static("1 = 0")); + } else { + cnd.and(clubField, "in", clubIds); + } + } + } + + @Override + public List getPayManageClubList() { + Sql sql = Sqls.create("SELECT c.id, c.clubName, c.clubCode FROM sys_club c $condition"); + Cnd cnd = Cnd.NEW(); + addPayClubScope(cnd, "c.id"); + cnd.asc("c.clubCode"); + cnd.asc("c.id"); + sql.setCondition(cnd); + return listMap(sql); + } + + @Override + public Pagination pageData(PageForm pageForm, Integer year, String clubId, Integer payed, Integer assign, Integer memberState) { + // 2026年10月3日 14:33:18;操作人:wxg:当前会员状态为可选参数,service 拒绝非法值,避免错误条件被当作全部。 + if (memberState != null && memberState != 0 && memberState != 1) { + throw new IllegalArgumentException("会员状态只能为在会、退会或全部"); + } + // 2026年10月3日 09:05:37;操作人:wxg:人员资料直接读年度快照,取消用户视图及逐行会员子查询。 + Sql sql = Sqls.create("SELECT cpr.*, c.clubName, cpr.joinTime AS applyTime, (" + + ASSIGNED_SQL + ") AS assign FROM club_pay_record cpr " + + "LEFT JOIN sys_club c ON cpr.clubId = c.id $condition"); + Cnd cnd = Cnd.NEW(); + cnd.andEX("cpr.year", "=", year); + cnd.andEX("cpr.clubId", "=", clubId); + addPayClubScope(cnd, "cpr.clubId"); + cnd.andEX("cpr.payed", "=", payed); + // 在分页前筛选,保证总条数及页数准确;EXISTS 不会因重复会员关系增加缴费记录条数。 + // 与列表 count 标签一致,只看该人员在该社团的当前关系,不依赖退会申请或缴费记录年份。 + if (memberState != null) { + String membershipExists = "EXISTS (SELECT 1 FROM club_user membership " + + "WHERE membership.clubId = cpr.clubId AND membership.userId = cpr.userId)"; + cnd.and(new Static(memberState == 1 ? membershipExists : "NOT " + membershipExists)); + } + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.orLike("cpr.userName", pageForm.getSearchKeyword()); + seg.orLike("cpr.loginName", pageForm.getSearchKeyword()); + cnd.and(seg); + } + + if (assign != null) { + cnd.and(new Static("(" + ASSIGNED_SQL + ") = " + (assign == 0 ? 0 : 1))); + } + + // 全社团管理角色先按社团编号分组;普通角色仅在授权社团内按加入时间倒序。 + if (canManageAllPayClubs()) { + cnd.asc("c.clubCode"); + cnd.asc("c.id"); + } + cnd.desc("cpr.joinTime"); + cnd.asc("cpr.id"); + sql.setCondition(cnd); + Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + + List list = pagination.getList(); + Set activeMembers = new HashSet<>(); + if (!list.isEmpty()) { + List userIds = list.stream().map(row -> row.getString("userId")).distinct().toList(); + List clubIds = list.stream().map(row -> row.getString("clubId")).distinct().toList(); + Sql membershipSql = Sqls.create("SELECT clubId, userId FROM club_user $condition"); + membershipSql.setCondition(Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds)); + for (NutMap member : listMap(membershipSql)) { + activeMembers.add(member.getString("clubId") + ":" + member.getString("userId")); + } + } + // 2026年10月3日 09:51:00;操作人:wxg:当前状态实时查询,拨付状态使用保存值,查询期间不重算、不写库。 + for (NutMap row : list) { + row.put("count", activeMembers.contains(row.getString("clubId") + ":" + row.getString("userId")) ? 1 : 0); + row.put("assign", row.getInt("assign") == 1); + } + + return pagination; + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void updatePayStatus(String[] ids, Boolean payed) { + // 2026年10月3日 09:51:00;操作人:wxg:明确记录ID及缴费布尔值,避免空参数造成批量误更新。 + if (ids == null || ids.length == 0 || payed == null || java.util.Arrays.stream(ids).anyMatch(StrUtil::isBlank)) { + throw new IllegalArgumentException("请选择缴费记录并提供缴费状态"); + } + Cnd scope = Cnd.where("id", "in", ids); + addPayClubScope(scope, "clubId"); + List records = dao().query(ClubPayRecord.class, scope); + if (records.size() != java.util.Arrays.stream(ids).distinct().count()) { + throw new IllegalArgumentException("部分缴费记录不存在或无权操作,请刷新列表"); + } + List userIds = records.stream().map(ClubPayRecord::getUserId).distinct().toList(); + List clubIds = records.stream().map(ClubPayRecord::getClubId).distinct().toList(); + Set activeMembers = new HashSet<>(); + for (ClubUser member : dao().query(ClubUser.class, Cnd.where("userId", "in", userIds).and("clubId", "in", clubIds))) { + activeMembers.add(member.getClubId() + ":" + member.getUserId()); + } + for (ClubPayRecord record : records) { + List logs = record.getOperateLogs() == null ? new ArrayList<>() : new ArrayList<>(record.getOperateLogs()); + JSONObject log = new JSONObject(); + log.set("operatorUserId", SecurityUtil.getUserId()); + log.set("operatorUserName", SecurityUtil.getUserUsername()); + log.set("operatorTime", DateUtil.now()); + log.set("operatorType", "缴费管理-手动设置"); + log.set("oldValue", record.getPayed()); + log.set("newValue", payed); + log.set("oldIsGiveMoney", record.getIsGiveMoney()); + record.setPayed(payed); + record.setIsGiveMoney(qualifiesForPayment(record, activeMembers.contains(record.getClubId() + ":" + record.getUserId()))); + log.set("newIsGiveMoney", record.getIsGiveMoney()); + logs.add(log); + record.setOperateLogs(logs); + } + dao().update(records); + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void disableExitMemberPayment(String clubId, String userId) { + // 2026年10月3日 09:51:00;操作人:wxg:退会影响实时会员关系对应的各年度名单,保留原缴费状态。 + if (StrUtil.isBlank(clubId) || StrUtil.isBlank(userId)) { + throw new IllegalArgumentException("退会社团和用户不能为空"); + } + Sql sql = Sqls.create("UPDATE club_pay_record SET isGiveMoney = false WHERE clubId = @clubId AND userId = @userId"); + sql.setParam("clubId", clubId); + sql.setParam("userId", userId); + dao().execute(sql); + } + @Override public boolean calAssigned(String userId, String clubId) { Cnd cnd = Cnd.NEW(); @@ -45,18 +362,23 @@ public class ClubPayRecordServiceImpl extends BaseServiceImpl imp @Override public boolean calAssigned(ClubPayRecord payRecord) { - // 如果是未缴费,直接是不拨付 - if (!payRecord.getPayed()) { - return false; - } - Sys_user user = dao().fetch(Sys_user.class, payRecord.getUserId()); - if (!Objects.equals("退休", user.getUserState())) { - return true; - } - if (Lang.isEmpty(user.getRetireDate())) { - return false; - } - int retireYear = DateUtil.year(user.getRetireDate()); - return retireYear >= (payRecord.getYear() != null ? payRecord.getYear() : DateUtil.thisYear()); - } + // 2026年10月3日 09:51:00;操作人:wxg:业务更新时计算资格,会员关系不存在即对应页面退会标签。 + boolean active = payRecord != null && dao().count(ClubUser.class, Cnd.where("clubId", "=", payRecord.getClubId()) + .and("userId", "=", payRecord.getUserId())) > 0; + return qualifiesForPayment(payRecord, active); + } + + /** 退休年份规则沿用原口径;未缴费、已退会或退休日期缺失时不拨付。 */ + private boolean qualifiesForPayment(ClubPayRecord payRecord, boolean active) { + if (!active || payRecord == null || !Boolean.TRUE.equals(payRecord.getPayed())) { + return false; + } + if (!Objects.equals("离退休人员", payRecord.getPersonType())) { + return true; + } + if (payRecord.getRetireDate() == null) { + return false; + } + return DateUtil.year(payRecord.getRetireDate()) >= (payRecord.getYear() != null ? payRecord.getYear() : DateUtil.thisYear()); + } } diff --git a/src/main/java/com/budwk/app/zhgh/club/service/impl/ClubUserJoinServiceImpl.java b/src/main/java/com/budwk/app/zhgh/club/service/impl/ClubUserJoinServiceImpl.java index befd1811..ae33143a 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/impl/ClubUserJoinServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/impl/ClubUserJoinServiceImpl.java @@ -4,36 +4,77 @@ import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.service.impl.BaseServiceImpl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.budwk.app.flow.constant.FlowConst; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.sys.views.View_user; import com.budwk.app.zhgh.club.model.ClubUser; import com.budwk.app.zhgh.club.model.ClubUserApply; import com.budwk.app.zhgh.club.model.SysClub; import com.budwk.app.zhgh.club.service.ClubUserJoinService; import org.nutz.dao.Cnd; +import org.nutz.dao.Chain; import org.nutz.dao.Dao; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.IocBean; import java.util.Date; import java.util.List; +import java.util.Objects; @IocBean(args = {"refer:dao"}) public class ClubUserJoinServiceImpl extends BaseServiceImpl implements ClubUserJoinService { + // PC 与 H5 入会资格校验共用此名单,人员类型须与用户视图中的值完全一致。 private static final List ALLOW_JOIN_PERSON_TYPES = List.of( "教职工", - "未起薪人员", - "教职工新入会", - "教职工其他", "劳动服务公司", - "后勤聘用人员", - "退休人员[一]", - "退休人员[二]", - "校聘劳务派遣全额" + "劳务派遣人员" ); public ClubUserJoinServiceImpl(Dao dao) { super(dao); } + @Override + public void preparePendingJoinTimes(ClubUserApply application) { + if (Integer.valueOf(1).equals(application.getMode())) { + // 审核前尚未正式入会;后端清除客户端回传或草稿遗留的时间,退会申请保留原入会时间。 + application.setJoinTime(null); + application.setExitTime(null); + } + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public Date completeJoinApproval(String applicationId, Long taskId) { + if (StrUtil.isBlank(applicationId) || taskId == null) { + throw new IllegalArgumentException("入会申请或会长审核任务不能为空"); + } + // WF 引擎已先保存完成时间;Execution 中的任务对象可能仍是完成前的数据,必须重新查库。 + ProcessTask task = dao().fetch(ProcessTask.class, taskId); + if (task == null + || !"1625a683-3890-4788-95b7-cab8240f6731".equals(task.getTaskName()) + || !ProcessTaskStateEnum.FINISHED.getCode().equals(task.getTaskState()) + || task.getFinishTime() == null + || StrUtil.isBlank(task.getVariable()) + || !ProcessSubmitTypeEnum.AGREE.getCode().equals(JSONUtil.parseObj(task.getVariable()).getInt(FlowConst.SUBMIT_TYPE))) { + throw new IllegalArgumentException("没有已完成且同意的会长入会审核任务"); + } + ProcessInstance instance = dao().fetch(ProcessInstance.class, task.getProcessInstanceId()); + ClubUserApply application = fetch(applicationId); + // 只更新当前任务所属的入会申请,不覆盖其他申请、退会记录或之前的入会周期。 + if (instance == null || !Objects.equals(applicationId, instance.getBusinessNo()) + || application == null || !Integer.valueOf(1).equals(application.getMode())) { + throw new IllegalArgumentException("会长审核任务与入会申请不匹配"); + } + dao().update(ClubUserApply.class, Chain.make("joinTime", task.getFinishTime()), Cnd.where("id", "=", applicationId)); + return task.getFinishTime(); + } + @Override public String checkJoinQualification(String userId) { View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", userId)); @@ -51,6 +92,8 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl impl @Override public ClubUserApply buildExitApply(String clubId, String userId) { + // 退会以当前会员关系的入会时间为准,不能沿用上一轮退出申请的时间。 + ClubUser currentMember = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId)); // 优先复用最近一次申请记录中的补充信息,避免用户重复填写基础资料。 ClubUserApply latestApply = fetch( Cnd.where("userId", "=", userId) @@ -59,11 +102,14 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl impl ); if (latestApply != null) { latestApply.setId(null); + if (currentMember != null && currentMember.getJoinTime() != null) { + latestApply.setJoinTime(currentMember.getJoinTime()); + } return latestApply; } // 对于会长等注册社团时直接入会的人员,没有申请记录时从当前成员信息兜底生成退会申请。 - ClubUser clubUser = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId)); + ClubUser clubUser = currentMember; if (clubUser == null) { return null; } @@ -105,6 +151,11 @@ public class ClubUserJoinServiceImpl extends BaseServiceImpl impl @Override public Date resolveJoinTime(String clubId, String userId) { + // 新会员直接使用关系表的入会时间;旧记录尚未补齐时仍保留原有回退逻辑。 + ClubUser member = dao().fetch(ClubUser.class, Cnd.where("clubId", "=", clubId).and("userId", "=", userId)); + if (member != null && member.getJoinTime() != null) { + return member.getJoinTime(); + } ClubUserApply latestJoinApply = fetch( Cnd.where("userId", "=", userId) .and("clubId", "=", 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 505a5b59..9e77e3c9 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 @@ -36,6 +36,7 @@ import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -50,260 +51,501 @@ public class SysClubExamineServiceImpl extends BaseServiceImpl getClubUserNum(String clubId, Integer year) { - 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("退休"); + // 2026年10月2日wxg屏蔽操作:原统计入口及专用辅助代码逐行注释,保留原文便于对比或恢复。 +// @Override +// public List getClubUserNum(String clubId, Integer year) { +// 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("退休"); +// +// 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); +// } +// +// /** +// * 合并旧系统会员生命周期与新系统已完成申请,生成可用于年度统计的入退会区间。 +// * 旧系统记录负责迁移历史,新系统申请只补充旧记录缺失的退会时间和迁移后新增的入会周期。 +// */ +// private List getMembershipPeriods(String clubId) { +// Sql legacySql = Sqls.create(""" +// SELECT +// 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 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)); +// +// Sql applySql = Sqls.create(""" +// SELECT +// cu.userId, +// cu.id, +// 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 +// ORDER BY cu.applyDate, cu.mode +// """) +// .setParam("clubId", clubId); +// List applications = listMap(applySql); +// if (!applications.isEmpty()) { +// // 流程状态改为按当前社团申请批量查询,避免相关子查询对流程实例表重复扫描。 +// List applicationIds = applications.stream() +// .map(application -> application.getString("id")) +// .filter(StrUtil::isNotBlank) +// .collect(Collectors.toList()); +// List finishedInstances = applicationIds.isEmpty() ? new ArrayList<>() +// : dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", applicationIds) +// .and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.FINISHED.getCode())); +// Set finishedBusinessNos = finishedInstances.stream() +// .map(ProcessInstance::getBusinessNo) +// .filter(StrUtil::isNotBlank) +// .collect(Collectors.toCollection(HashSet::new)); +// applications.removeIf(application -> !finishedBusinessNos.contains(application.getString("id"))); +// } +// applications.sort(Comparator.comparing(row -> row.getTime("applyDate"), Comparator.nullsLast(Date::compareTo))); +// +// for (NutMap application : applications) { +// Date applyDate = application.getTime("applyDate"); +// Boolean mode = application.getBoolean("mode"); +// if (applyDate == null || mode == null) { +// continue; +// } +// if (mode) { +// mergeJoinApplication(periods, application, applyDate); +// } else { +// mergeExitApplication(periods, application, applyDate); +// } +// } +// 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 (StrUtil.isBlank(userState)) { +// return null; +// } +// 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; +// } +// } +// +// + // 2026年10月2日22:00点开始新开发的代码。 + // 实际整改时间:2026年10月3日 10:14:04;操作人:wxg。 + @Override + public List getClubUserNum(String clubId, Integer year) { + if (StrUtil.isBlank(clubId) || year == null || year < 1 || year > 9998) { + throw new IllegalArgumentException("请选择有效社团和统计年度"); + } + MemberChangeStatistics stats = new MemberChangeStatistics(year); + // 年初基数来自当年名单,不受缴费、拨付或当前退会状态影响;同一会员按 userId 去重。 + List roster = listMap(Sqls.create("SELECT userId, joinTime, personType, retireDate, operateLogs FROM club_pay_record WHERE clubId = @clubId AND year = @year") + .setParam("clubId", clubId).setParam("year", year)); + if (roster.isEmpty()) { + stats.invalidate(year + "年度该社团没有缴费名单,请检查年度名单生成计划任务及名单数据,不能将缺失名单视为年初零人。"); + return stats.result(); + } + boolean generated = roster.stream().anyMatch(row -> { + String logs = row.getString("operateLogs"); + return logs != null && logs.contains("缴费管理-定时任务自动设置"); + }); + if (!generated) { + stats.warnings.add("未发现该社团年度名单的定时生成记录,请检查计划任务及名单完整性;手工入会记录不能证明年度名单已完整生成。"); + } + Map snapshots = new java.util.LinkedHashMap<>(); + Map periods = new java.util.LinkedHashMap<>(); + for (NutMap row : roster) { + String userId = row.getString("userId"); + if (StrUtil.isBlank(userId) || row.getTime("joinTime") == null) { + stats.invalidate("缴费名单存在用户或加入时间缺失,请补齐后核查统计。"); + continue; + } + NutMap prior = snapshots.putIfAbsent(userId, row); + if (prior != null) { + stats.warnings.add("缴费名单存在同一会员重复记录,已按用户去重,请核查。"); + if (!Objects.equals(prior.getTime("joinTime"), row.getTime("joinTime"))) { + stats.invalidate("重复缴费记录的加入时间不一致,无法确定年初基数,请核查。"); + } + continue; + } + AnnualMemberPeriod period = addAnnualPeriod(periods, userId, row.getTime("joinTime"), stats); + if (period != null) period.rosterMember = true; + } + // 只接纳已完成 WF 的 mode=0/1;mode=2 是已生效的系统退会,不需要 WF 实例。 + // mode 显式转为整数,避免 MySQL tinyint(1) 被驱动返回布尔值后将系统退会误判为入会。 + List applications = listMap(Sqls.create(""" + SELECT a.userId, a.id, CAST(a.mode AS SIGNED) AS mode, a.joinTime, a.exitTime + FROM club_user_apply a + WHERE a.clubId = @clubId AND COALESCE(a.delFlag, 0) = 0 + AND (a.mode = 2 OR (a.mode IN (0, 1) AND EXISTS ( + SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = a.id AND ins.state = 20))) + """).setParam("clubId", clubId)); + for (NutMap application : applications) { + Date joinTime = application.getTime("joinTime"); + Date exitTime = application.getTime("exitTime"); + int mode = application.getInt("mode"); + if (mode == 1 && joinTime == null) { + stats.invalidate("已完成入会申请缺少 joinTime,请核查会长审核时间。"); + } else if (mode != 1 && exitTime == null) { + stats.invalidate("已生效退会记录缺少 exitTime,请核查退会数据。"); + } + // 年初只使用缴费基数,旧入会申请不能另建一个年初周期;同年退会资料补充手工入会。 + if (stats.inYear(joinTime) && (mode == 1 || stats.inYear(exitTime))) { + // 退会记录中的入会时间也能还原已被物理删除的手工入会周期。 + addAnnualPeriod(periods, application.getString("userId"), joinTime, stats); + } + } + List members = listMap(Sqls.create("SELECT userId, joinTime FROM club_user WHERE clubId = @clubId").setParam("clubId", clubId)); + for (NutMap member : members) { + if (member.getTime("joinTime") == null) { + if (!snapshots.containsKey(member.getString("userId"))) stats.invalidate("当前会员存在加入时间缺失,无法确定年度增加,请核查。"); + } else { + // 手工录入、导入和成立社团时加入的成员没有 mode=1,也需要计入可靠的年度增加。 + addAnnualPeriod(periods, member.getString("userId"), member.getTime("joinTime"), stats); + } + } + for (NutMap application : applications) { + if (application.getInt("mode") == 1 || application.getTime("exitTime") == null) continue; + Date exitTime = application.getTime("exitTime"); + if (!exitTime.before(stats.end)) continue; + String userId = application.getString("userId"); + Date joinTime = application.getTime("joinTime"); + AnnualMemberPeriod target = joinTime == null ? null : periods.get(periodKey(userId, joinTime)); + // 旧退会申请可能仍保存迁移前的入会日期,只关闭当时唯一有效的最近周期。 + if (target == null && (joinTime == null || joinTime.before(stats.start))) { + target = periods.values().stream().filter(period -> Objects.equals(userId, period.userId) + && !period.joinTime.after(exitTime) && (period.exitTime == null || period.exitTime.equals(exitTime))) + .max(Comparator.comparing(period -> period.joinTime)).orElse(null); + } + if (target == null) { + if (!exitTime.before(stats.start)) stats.invalidate("年度退会记录无法匹配有效入会周期,请核查加入时间。"); + } else if (exitTime.before(target.joinTime)) { + stats.invalidate("退会时间早于入会时间,请核查申请记录。"); + } else if (target.exitTime == null || exitTime.before(target.exitTime)) { + target.exitTime = exitTime; + } + } + Set userIds = periods.values().stream().map(period -> period.userId).collect(Collectors.toSet()); + Map profiles = new java.util.HashMap<>(); + if (!userIds.isEmpty()) { + // 人员快照可能早于退休资料补录,批量读取当前人员类型及退休日期,不读取 userState。 + Sql profileSql = Sqls.create("SELECT id, personType, retireDate FROM sys_user $condition"); + profileSql.setCondition(Cnd.where("id", "in", userIds)); + for (NutMap profile : listMap(profileSql)) profiles.put(profile.getString("id"), profile); + } + Map previousEnd = new java.util.HashMap<>(); + Set retiredTransfers = new HashSet<>(); + List ordered = new ArrayList<>(periods.values()); + ordered.sort(Comparator.comparing((AnnualMemberPeriod period) -> period.userId).thenComparing(period -> period.joinTime)); + for (AnnualMemberPeriod period : ordered) { + // 同一用户不能同时占有两个未结束的周期;异常不强行合并,提示核查。 + if (previousEnd.containsKey(period.userId)) { + Date priorEnd = previousEnd.get(period.userId); + if (priorEnd == null || priorEnd.after(period.joinTime)) stats.invalidate("同一会员的入会周期重叠,请核查重复入会或缺失的退会记录。"); + } + previousEnd.put(period.userId, period.exitTime); + NutMap snapshot = snapshots.get(period.userId); + NutMap profile = profiles.get(period.userId); + String snapshotType = snapshot == null ? null : snapshot.getString("personType"); + String currentType = profile == null ? snapshotType : profile.getString("personType"); + if (StrUtil.isBlank(currentType)) currentType = snapshotType; + Date retireDate = profile == null ? null : profile.getTime("retireDate"); + if (retireDate == null && snapshot != null) retireDate = snapshot.getTime("retireDate"); + boolean atStart = period.joinTime.before(stats.start) && (period.exitTime == null || !period.exitTime.before(stats.start)); + if (atStart && period.rosterMember) { + stats.increase(classifyPersonType(snapshotType, retireDate, stats.start, true, stats), "yearFirstNum"); + } else if (atStart) { + stats.invalidate("存在年初已入会但不在年度缴费基数中的会员,请检查名单完整性。"); + } + if (stats.inYear(period.joinTime)) { + stats.increase(classifyPersonType(currentType, retireDate, period.joinTime, false, stats), "yearAddNum"); + } + if (stats.inYear(period.exitTime)) { + stats.increase(classifyPersonType(currentType, retireDate, period.exitTime, false, stats), "yearReduceNum"); + } + // 同日同刻采用先退休再退会;入会时已退休只算退休入会,不额外算一次转类。 + if (stats.inYear(retireDate) && period.joinTime.before(retireDate) + && (period.exitTime == null || !period.exitTime.before(retireDate)) + && recognizedPersonType(currentType) && retiredTransfers.add(period.userId)) { + stats.increase("在职人员", "yearReduceNum"); + stats.increase("离退休人员", "yearAddNum"); + } + } + return stats.result(); + } - 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); - } + /** 合并同一用户、同一加入时间的多来源记录;年度末边界不属于本年。 */ + private AnnualMemberPeriod addAnnualPeriod(Map periods, String userId, Date joinTime, MemberChangeStatistics stats) { + if (StrUtil.isBlank(userId)) { + stats.invalidate("会员记录缺少用户ID,请核查。"); + return null; + } + if (joinTime == null || !joinTime.before(stats.end)) return null; + return periods.computeIfAbsent(periodKey(userId, joinTime), key -> new AnnualMemberPeriod(userId, joinTime)); + } - /** - * 合并旧系统会员生命周期与新系统已完成申请,生成可用于年度统计的入退会区间。 - * 旧系统记录负责迁移历史,新系统申请只补充旧记录缺失的退会时间和迁移后新增的入会周期。 - */ - private List getMembershipPeriods(String clubId) { - Sql legacySql = Sqls.create(""" - SELECT - 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 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)); + private String periodKey(String userId, Date joinTime) { + return userId + ":" + joinTime.getTime(); + } - Sql applySql = Sqls.create(""" - SELECT - cu.userId, - cu.id, - 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 - ORDER BY cu.applyDate, cu.mode - """) - .setParam("clubId", clubId); - List applications = listMap(applySql); - if (!applications.isEmpty()) { - // 流程状态改为按当前社团申请批量查询,避免相关子查询对流程实例表重复扫描。 - List applicationIds = applications.stream() - .map(application -> application.getString("id")) - .filter(StrUtil::isNotBlank) - .collect(Collectors.toList()); - List finishedInstances = applicationIds.isEmpty() ? new ArrayList<>() - : dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", applicationIds) - .and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.FINISHED.getCode())); - Set finishedBusinessNos = finishedInstances.stream() - .map(ProcessInstance::getBusinessNo) - .filter(StrUtil::isNotBlank) - .collect(Collectors.toCollection(HashSet::new)); - applications.removeIf(application -> !finishedBusinessNos.contains(application.getString("id"))); - } - applications.sort(Comparator.comparing(row -> row.getTime("applyDate"), Comparator.nullsLast(Date::compareTo))); + /** 只接纳指定的四种人员类型,其他已知类型不纳入这两类统计。 */ + private boolean recognizedPersonType(String personType) { + return List.of("教职工", "劳务派遣人员", "劳动服务公司", "离退休人员").contains(personType == null ? "" : personType); + } - for (NutMap application : applications) { - Date applyDate = application.getTime("applyDate"); - Boolean mode = application.getBoolean("mode"); - if (applyDate == null || mode == null) { - continue; - } - if (mode) { - mergeJoinApplication(periods, application, applyDate); - } else { - mergeExitApplication(periods, application, applyDate); - } - } - return periods; - } + /** 年初边界当天退休仍属于年初在职基数,随后转类;事件时刻已退休则归入离退休。 */ + private String classifyPersonType(String personType, Date retireDate, Date moment, boolean boundary, MemberChangeStatistics stats) { + if (StrUtil.isBlank(personType)) { + stats.invalidate("人员类型为空,不能按在职状态替代分类,请核查人员资料。"); + return null; + } + if (!recognizedPersonType(personType)) { + stats.warnings.add("存在不属于指定两类的人员类型,已排除,请核查统计范围。"); + return null; + } + if (retireDate != null) { + boolean retired = boundary ? retireDate.before(moment) : !retireDate.after(moment); + return retired ? "离退休人员" : "在职人员"; + } + return "离退休人员".equals(personType) ? "离退休人员" : "在职人员"; + } - /** - * 已迁移的入会申请与旧会员记录精确对应时不重复新增;迁移后的新申请以审核申请时间作为入会时间。 - */ - 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 static class AnnualMemberPeriod { + final String userId; + final Date joinTime; + Date exitTime; + boolean rosterMember; - /** - * 退会申请优先关闭入会时间相同的会员周期;新系统复入会后再退会时关闭最近的有效周期。 - */ - 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; - } - } + AnnualMemberPeriod(String userId, Date joinTime) { + this.userId = userId; + this.joinTime = joinTime; + } + } - private NutMap createUserChangeResult(String userState) { - return NutMap.NEW() - .addv("userState", userState) - .addv("yearFirstNum", 0) - .addv("yearAddNum", 0) - .addv("yearReduceNum", 0) - .addv("thisYearNum", 0); - } + /** 返回两类计数及核查信息;userState 仅为已有查看页的显示字段别名,不作为分类数据来源。 */ + private static class MemberChangeStatistics { + final Date start; + final Date end; + final Set warnings = new java.util.LinkedHashSet<>(); + final List rows = List.of(row("在职人员"), row("离退休人员")); + boolean valid = true; - 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); - } - } + MemberChangeStatistics(int year) { + start = DateUtil.parse(year + "-01-01 00:00:00"); + end = DateUtil.parse((year + 1) + "-01-01 00:00:00"); + } - private boolean isInYear(Date date, Date yearStart, Date nextYearStart) { - return date != null && !date.before(yearStart) && date.before(nextYearStart); - } + static NutMap row(String type) { + return NutMap.NEW().addv("personType", type).addv("userState", type) + .addv("yearFirstNum", 0).addv("yearAddNum", 0).addv("yearReduceNum", 0).addv("thisYearNum", 0); + } - private boolean isMemberAt(MembershipPeriod period, Date date) { - return !period.joinTime.after(date) && (period.exitTime == null || period.exitTime.after(date)); - } + boolean inYear(Date time) { + return time != null && !time.before(start) && time.before(end); + } - private boolean isMemberAtYearBoundary(MembershipPeriod period, Date boundary) { - return period.joinTime.before(boundary) && (period.exitTime == null || !period.exitTime.before(boundary)); - } + void invalidate(String message) { + valid = false; + warnings.add(message); + } - private String classifyAtBoundary(MembershipPeriod period, Date boundary) { - if (period.retireDate != null) { - return period.retireDate.before(boundary) ? "退休" : "在职"; - } - return normalizeUserState(period.userState); - } + void increase(String type, String field) { + for (NutMap row : rows) if (row.getString("personType").equals(type)) row.put(field, row.getInt(field) + 1); + } - 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 (StrUtil.isBlank(userState)) { - return null; - } - 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; - } - } + List result() { + for (NutMap row : rows) { + int yearEnd = row.getInt("yearFirstNum") + row.getInt("yearAddNum") - row.getInt("yearReduceNum"); + row.put("thisYearNum", yearEnd); + if (yearEnd < 0) invalidate("年末人数出现负数,请核查年初名单与入退会记录。"); + } + for (NutMap row : rows) { + row.put("statisticsValid", valid); + row.put("statisticsWarnings", new ArrayList<>(warnings)); + // 无法确定基数或周期时展示空值,避免用户将部分计数当成完整统计提交。 + if (!valid) for (String field : List.of("yearFirstNum", "yearAddNum", "yearReduceNum", "thisYearNum")) row.put(field, null); + } + return rows; + } + } @Override public List getJgUser(String clubId) { 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 7a146145..d8221bd3 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 @@ -2,6 +2,7 @@ package com.budwk.app.zhgh.club.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.page.Pagination; import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.utils.CommonDownloadUtil; @@ -9,6 +10,14 @@ import com.budwk.app.base.utils.SysOfficeTemplateUtil; import com.budwk.app.bpm.enums.BpmProcessInstanceStatusEnum; import com.budwk.app.flow.enums.ProcessInstanceStateEnum; import com.budwk.app.sys.models.Sys_user; +import com.budwk.app.sys.models.Sys_role; +import com.budwk.app.sys.services.SysRoleService; +import com.budwk.app.sys.services.SysUserService; +import com.budwk.app.zhgh.club.model.ClubUserApply; +import com.budwk.app.zhgh.club.service.ClubUserJoinService; +import com.budwk.app.zhgh.club.service.ClubPayRecordService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import com.budwk.app.sys.models.Sys_user_role; import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil; @@ -39,12 +48,17 @@ import org.nutz.lang.Strings; import org.nutz.lang.util.NutMap; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDate; import java.util.ArrayList; +import java.util.Date; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -64,12 +78,74 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl sortColumns = Map.of( + "loginName", "scu.loginName", "userName", "scu.userName", "mobile", "scu.mobile", + "joinTime", "scu.joinTime", "sex", "u.sex", "personType", "u.personType", + "userAttribute", "u.userAttribute", "userState", "u.userState", "unitName", "u.unitName"); + String orderColumn = StrUtil.isBlank(pageForm.getPageOrderName()) ? null : sortColumns.get(pageForm.getPageOrderName()); + boolean orderRequested = orderColumn != null && StrUtil.isNotBlank(pageForm.getPageOrderBy()); + boolean needsUnitSort = orderRequested && "unitName".equals(pageForm.getPageOrderName()); + boolean needsUser = Boolean.TRUE.equals(pageForm.getFocusMembers()) + || StrUtil.isNotBlank(pageForm.getPersonType()) || StrUtil.isNotBlank(pageForm.getUserState()) + || (orderRequested && orderColumn.startsWith("u.")); Sql sql = Sqls.create(""" - SELECT - scu.*, - u.username as userName, - u.loginname as loginName, - u.sex, - u.personType as personType, - u.userState, - u.unitid as unitId, - u.mobile, - club.clubName, - u.unitname as unitName, - DATE_FORMAT(IFNULL(joinInfo.joinTime, club.foundTime), '%Y-%m-%d %H:%i:%s') as joinTime - FROM - club_user scu - LEFT JOIN sys_club club ON scu.clubId = club.id - RIGHT JOIN `vw_user` u ON scu.userId = u.id - LEFT JOIN ( - SELECT - joinApply.userId, - joinApply.clubId, - MAX(joinApply.joinTime) AS joinTime - FROM - club_user_apply joinApply - LEFT JOIN wf_process_instance joinIns ON joinIns.businessNo = joinApply.id - WHERE - joinApply.mode = true - AND joinIns.state = 20 - GROUP BY - joinApply.userId, - joinApply.clubId - ) joinInfo ON joinInfo.userId = scu.userId AND joinInfo.clubId = scu.clubId + SELECT scu.* + FROM club_user scu + $userJoin $condition - $order """); - Cnd cnd = Cnd.NEW(); - - cnd.and("scu.clubId", "=", pageForm.getClubId()); - if (StrUtil.isNotBlank(pageForm.getPersonType())) { - cnd.and("u.personType", "=", pageForm.getPersonType()); + // 默认查询和姓名/工号/日期筛选不关联 vw_user;单位排序保留视图的单位名称口径。 + sql.setVar("userJoin", needsUnitSort ? "LEFT JOIN vw_user u ON u.id = scu.userId" + : needsUser ? "LEFT JOIN sys_user u ON u.id = scu.userId" : ""); + Cnd cnd = Cnd.where("scu.clubId", "=", pageForm.getClubId()); + // 在分页和总数统计前同时筛选两类人员,保持当前社团及其他查询条件的限制。 + if (Boolean.TRUE.equals(pageForm.getFocusMembers())) { + cnd.and("u.personType", "in", List.of("自聘人员", "其他")); } - if (StrUtil.isNotBlank(pageForm.getUserState())) { - cnd.and("u.userState", "=", pageForm.getUserState()); + cnd.andEX("u.personType", "=", pageForm.getPersonType()); + cnd.andEX("u.userState", "=", pageForm.getUserState()); + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + cnd.and(Cnd.exps("scu.loginName", "like", "%" + pageForm.getSearchKeyword() + "%") + .or("scu.userName", "like", "%" + pageForm.getSearchKeyword() + "%")); } - if (Strings.isNotBlank(pageForm.getSearchKeyword())) { - cnd.and(Cnd.exps("loginname", "like", "%" + pageForm.getSearchKeyword() + "%").or("username", "like", "%" + pageForm.getSearchKeyword() + "%")); + // 结束日期使用次日零点上界,包含结束当天;只读取已保存的时间,不触发 WF 补齐 SQL。 + if (StrUtil.isNotBlank(pageForm.getJoinDateStart())) { + cnd.and("scu.joinTime", ">=", LocalDate.parse(pageForm.getJoinDateStart()).toString()); } - - if (StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { - cnd.orderBy(pageForm.getPageOrderName(), "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + if (StrUtil.isNotBlank(pageForm.getJoinDateEnd())) { + cnd.and("scu.joinTime", "<", LocalDate.parse(pageForm.getJoinDateEnd()).plusDays(1).toString()); + } + if (orderRequested) { + cnd.orderBy(orderColumn, "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); } else { - sql.setVar("order", """ - ORDER BY - CASE - WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_PRESIDENT"') THEN 1 - WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_VICE_PRESIDENT"') THEN 2 - WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_SECRETARY"') THEN 3 - WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_VICE_SECRETARY"') THEN 4 - WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_OPERATOR"') THEN 5 - WHEN JSON_CONTAINS(scu.roleCode, '"CLUB_MEMBER"') THEN 6 - ELSE 99 - END - """); - } - - //查询理事机构 - if (pageForm.getRadioType() != null && 1 == pageForm.getRadioType()) { + cnd.desc("scu.joinTime"); + } + cnd.asc("scu.id"); + if (Integer.valueOf(1).equals(pageForm.getRadioType())) { cnd.and(new Static("NOT JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')")); } - //查询社团成员 - if (pageForm.getRadioType() != null && 2 == pageForm.getRadioType()) { + if (Integer.valueOf(2).equals(pageForm.getRadioType())) { cnd.and(new Static("JSON_CONTAINS(scu.roleCode, '\"CLUB_MEMBER\"')")); - //cnd.and(new Static("JSON_LENGTH(scu.roleCode) = 1")); } sql.setCondition(cnd); - Pagination listPageVO = sysClubService.listPageVO(pageForm, sql, ClubUserCommonPageVo.class); - List listMap = listPageVO.getList(ClubUserCommonPageVo.class); - for (ClubUserCommonPageVo vo : listMap) { - List list = vo.getRoleCode(); - List cleanedCodes = list.stream() - .filter(Objects::nonNull) - .map(String::strip) + Pagination page = sysClubService.listPageVO(pageForm, sql, ClubUserCommonPageVo.class); + List members = page.getList(ClubUserCommonPageVo.class); + fillMemberPageDetails(members, pageForm.getClubId()); + for (ClubUserCommonPageVo member : members) { + List roleCodes = member.getRoleCode() == null ? List.of() : member.getRoleCode(); + List cleanedCodes = roleCodes.stream().filter(Objects::nonNull).map(String::strip) .map(code -> code.startsWith("\"") && code.endsWith("\"") - ? code.substring(1, code.length() - 1) - : code) - .toList(); - String roleName = SysClubUserServiceImpl.convertRoleName(cleanedCodes); - vo.setRoleName(roleName); - vo.setRoleCode(cleanedCodes); + ? code.substring(1, code.length() - 1) : code).toList(); + member.setRoleName(SysClubUserServiceImpl.convertRoleName(cleanedCodes)); + member.setRoleCode(cleanedCodes); } - listPageVO.setList(listMap); - return listPageVO; + page.setList(members); + return page; } + /** + * 2026年10月3日 12:01:28;操作人:wxg:分页后批量获取本页其他资料,避免把完整用户视图带入默认统计和分页。 + * @param members 当前页会员关系;空页不查询,姓名/工号/电话/入会时间始终保留会员表值 + * @param clubId 当前选中社团ID,用于补充社团名称 + */ + private void fillMemberPageDetails(List members, String clubId) { + if (members.isEmpty()) return; + List userIds = members.stream().map(ClubUser::getUserId).filter(Objects::nonNull).distinct().toList(); + Map users = new HashMap<>(); + if (!userIds.isEmpty()) { + Sql details = Sqls.create(""" + SELECT id AS id, sex AS sex, personType AS personType, userAttribute AS userAttribute, + userState AS userState, unitId AS unitId, unitName AS unitName + FROM vw_user + $condition + """); + details.setCondition(Cnd.where("id", "in", userIds)); + for (NutMap user : listMap(details)) users.put(user.getString("id"), user); + } + SysClub club = dao().fetch(SysClub.class, clubId); + for (ClubUserCommonPageVo member : members) { + member.setClubName(club == null ? null : club.getClubName()); + NutMap user = users.get(member.getUserId()); + // 用户资料缺失仍展示会员关系及已保存资料,不用当前用户资料覆盖三个持久化字段。 + if (user == null) continue; + member.setSex(user.getString("sex")); + member.setPersonType(user.getString("personType")); + member.setUserAttribute(user.getString("userAttribute")); + member.setUserState(user.getString("userState")); + member.setUnitId(user.getString("unitId")); + member.setUnitName(user.getString("unitName")); + } + } + + /** + * 当前社团历史退会记录分页;每次退会保留一行,不按人员去重,也不要求关联 WF 已完成流程。 + * @param pageForm clubId 为当前社团ID;姓名/工号、人员类型、在职状态及 yyyy-MM-dd 入会日期范围为可选筛选; + * focusMembers=true 筛选当前人员类型为自聘人员或其他的历史成员,false/空值不限制; + * pageNumber/pageSize 为分页参数,pageOrderName/pageOrderBy 为列排序参数 + * @return 会员展示分页对象;id 为申请ID,joinTime/exitTime 来自申请,人员资料取用户视图当前值 + */ + private Pagination historyMemberPageData(ClubUserPageForm pageForm) { + Sql sql = Sqls.create(""" + SELECT + cua.id, cua.clubId, cua.userId, cua.roleCode AS applyRoleCode, + u.username AS userName, u.loginname AS loginName, u.sex, u.mobile, + u.personType, u.userAttribute, u.userState, + u.unitid AS unitId, u.unitname AS unitName, + DATE_FORMAT(cua.joinTime, '%Y-%m-%d %H:%i:%s') AS joinTime, + DATE_FORMAT(cua.exitTime, '%Y-%m-%d %H:%i:%s') AS exitTime + FROM club_user_apply cua + LEFT JOIN vw_user u ON u.id = cua.userId + $condition + """); + Cnd cnd = Cnd.where("cua.clubId", "=", pageForm.getClubId()) + .and("cua.mode", "in", List.of(0, 2)); + // 历史页同样按用户当前人员类型筛选,两类人员共同参与分页和总数统计。 + if (Boolean.TRUE.equals(pageForm.getFocusMembers())) { + cnd.and("u.personType", "in", List.of("自聘人员", "其他")); + } + cnd.andEX("u.personType", "=", pageForm.getPersonType()); + cnd.andEX("u.userState", "=", pageForm.getUserState()); + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + cnd.and(Cnd.exps("u.loginname", "like", "%" + pageForm.getSearchKeyword() + "%") + .or("u.username", "like", "%" + pageForm.getSearchKeyword() + "%")); + } + // 时间查询保持入会日期语义,结束日期用次日零点上界,包含选择的结束当天。 + if (StrUtil.isNotBlank(pageForm.getJoinDateStart())) { + cnd.and("cua.joinTime", ">=", LocalDate.parse(pageForm.getJoinDateStart()).toString()); + } + if (StrUtil.isNotBlank(pageForm.getJoinDateEnd())) { + cnd.and("cua.joinTime", "<", LocalDate.parse(pageForm.getJoinDateEnd()).plusDays(1).toString()); + } + // 仅允许表格实际展示的列参与排序,退出时间相同则按申请ID保证分页顺序稳定。 + List sortableFields = List.of("loginName", "userName", "sex", "personType", "userAttribute", + "userState", "unitName", "joinTime", "exitTime"); + if (StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy()) + && sortableFields.contains(pageForm.getPageOrderName())) { + cnd.orderBy(pageForm.getPageOrderName(), "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + } else { + cnd.desc("cua.exitTime"); + } + cnd.asc("cua.id"); + sql.setCondition(cnd); + Pagination pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class); + List members = pagination.getList(ClubUserCommonPageVo.class); + for (ClubUserCommonPageVo member : members) { + if (StrUtil.isNotBlank(member.getApplyRoleCode())) { + member.setRoleName(SysClubUserServiceImpl.convertRoleName(List.of(member.getApplyRoleCode()))); + } + } + pagination.setList(members); + return pagination; + } + + /** + * 2026年10月3日 19:42:45;操作人:wxg:查询审核退会、系统处理与历史直接移出记录。 + * @param pageForm clubId、姓名/工号、personType、userState 为可选筛选;exitDateStart/exitDateEnd 传 yyyy-MM-dd,结束日包含当天;pageNumber/pageSize 控制分页 + * @return 分页会员资料,exitType 区分 AUDIT_EXIT、SYSTEM_EXIT、DIRECT_REMOVE,exitTime 为退会时间 + */ @Override public Pagination exitManagePageData(ClubUserPageForm pageForm) { Sql sql = Sqls.create(""" @@ -257,6 +419,13 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl=", LocalDate.parse(pageForm.getExitDateStart()).toString()); + } + if (StrUtil.isNotBlank(pageForm.getExitDateEnd())) { + cnd.and("exitInfo.exitTime", "<", LocalDate.parse(pageForm.getExitDateEnd()).plusDays(1).toString()); + } cnd.andEX("u.personType", "=", pageForm.getPersonType()); cnd.andEX("u.userState", "=", pageForm.getUserState()); if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { @@ -268,7 +437,9 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class); List list = pagination.getList(ClubUserCommonPageVo.class); @@ -321,6 +492,8 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl implements SysC @Inject private SysClubUserService sysClubUserService; @Inject + private ClubPayRecordService payRecordService; + @Inject private CommonService commonService; @Inject private BpmService bpmService; + @Override + public List listJoinClubOptions() { + // 注册、换届等流程可能使用同一社团ID作为业务号;仅判断流程存在,避免联表将社团重复返回。 + Sql sql = Sqls.create(""" + SELECT c.* + FROM sys_club c + WHERE EXISTS ( + SELECT 1 + FROM wf_process_instance inst + WHERE inst.businessNo = c.id AND inst.state = @state + ) + """).setParam("state", ProcessInstanceStateEnum.FINISHED.getCode()); + return listVO(sql, SysClub.class); + } + @Override public Pagination getAllClubWithPage(PageForm pageForm, Cnd cnd) { Sql sql = Sqls.create(""" @@ -77,10 +98,11 @@ public class SysClubServiceImpl extends BaseServiceImpl implements SysC FROM sys_club club LEFT JOIN sys_user u on club.concatPerson = u.id - LEFT JOIN wf_process_instance ins ON ins.businessNo = club.id $condition """).setParam("userId", SecurityUtil.getUserId()); - cnd.and("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode()); + // 同一社团可能关联注册、换届等多个已完成 WF 流程;仅检查流程存在,避免列表和分页总数重复。 + cnd.and(new Static("EXISTS (SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = club.id AND ins.state = %s)" + .formatted(ProcessInstanceStateEnum.FINISHED.getCode()))); sql.setCondition(cnd); return listPageVO(pageForm, sql, ClubCommonPageVo.class); } @@ -164,6 +186,7 @@ public class SysClubServiceImpl extends BaseServiceImpl implements SysC } @Override + @Aop(TransAop.READ_COMMITTED) public SysClub doAdd(SysClub club, List managePerson) { club.setDismiss(false); club.setUserId(SecurityUtil.getUserId()); @@ -178,6 +201,7 @@ public class SysClubServiceImpl extends BaseServiceImpl implements SysC clubUser.setUserId(person.getString("userId")); return clubUser; }).toList(); + sysClubUserService.prepareDirectJoinTimes(clubUsers); dao().insert(clubUsers); for (ClubUser clubUser : clubUsers) { @@ -203,6 +227,8 @@ public class SysClubServiceImpl extends BaseServiceImpl implements SysC log.set("newValue", false); record.setOperateLogs(List.of(log)); + // 2026年10月3日 09:05:37;操作人:wxg:社团创建直接入会的成员同样保存缴费快照。 + payRecordService.prepareSnapshots(List.of(record)); dao().insert(record); } } @@ -211,6 +237,7 @@ public class SysClubServiceImpl extends BaseServiceImpl implements SysC } @Override + @Aop(TransAop.READ_COMMITTED) public SysClub doEdit(SysClub club, List deleteIds, List managePerson) { // 修改社团表 dao().update(club); @@ -219,6 +246,8 @@ public class SysClubServiceImpl extends BaseServiceImpl implements SysC // 重新插入发起人 dao().insertLinks(club, "sponsors"); + // 现有编辑流程会重建会员关系,删除前保存原入会时间,避免把编辑时刻算成再次入会。 + List previousMembers = dao().query(ClubUser.class, Cnd.where("clubId", "=", club.getId())); dao().clear(Sys_user_role.class, Cnd.where("clubId", "=", club.getId())); dao().clear(ClubUser.class, Cnd.where(ClubUser::getClubId, "=", club.getId())); @@ -228,8 +257,24 @@ public class SysClubServiceImpl extends BaseServiceImpl implements SysC clubUser.setClubId(club.getId()); clubUser.setRoleCode(List.of(person.getString("roleCode"))); clubUser.setUserId(person.getString("userId")); + previousMembers.stream() + .filter(member -> java.util.Objects.equals(member.getUserId(), clubUser.getUserId())) + .findFirst() + .ifPresent(member -> { + // 2026年10月3日 12:01:28;操作人:wxg:编辑社团重建既有会员关系时保留原入会时间及人员资料。 + clubUser.setJoinTime(member.getJoinTime()); + clubUser.setUserName(member.getUserName()); + clubUser.setLoginName(member.getLoginName()); + clubUser.setMobile(member.getMobile()); + }); return clubUser; }).toList(); + // 仅真正新增的理事成员赋确认时间;原成员即使历史时间为空也保留原值。 + List newMembers = clubUsers.stream() + .filter(member -> previousMembers.stream().noneMatch(previous -> + java.util.Objects.equals(previous.getUserId(), member.getUserId()))) + .toList(); + sysClubUserService.prepareDirectJoinTimes(newMembers); dao().insert(clubUsers); } return club; diff --git a/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubUserServiceImpl.java b/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubUserServiceImpl.java index 4cdcf770..d1e85462 100644 --- a/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubUserServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/club/service/impl/SysClubUserServiceImpl.java @@ -1,6 +1,7 @@ package com.budwk.app.zhgh.club.service.impl; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.service.impl.BaseServiceImpl; @@ -22,6 +23,7 @@ import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; +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; @@ -55,15 +57,96 @@ public class SysClubUserServiceImpl extends BaseServiceImpl implements super(dao); } + /** 2026年10月3日 12:01:28;操作人:wxg:与缴费表升级一致,只增加缺失列,保留现有数据、额外字段和索引。 */ + @Override + @Aop(TransAop.READ_COMMITTED) + public void upgradeMemberSchema() { + // MySQL DDL 会隐式提交,升级失败必须终止启动,不得继续执行缺列的业务查询。 + if (!dao().exists("club_user")) { + dao().create(ClubUser.class, false); + } else { + Daos.migration(dao(), ClubUser.class, true, false, false); + } + } + + /** 2026年10月3日 12:01:28;操作人:wxg:历史资料和空入会时间仅在启动升级时补齐,会员分页查询不再执行 UPDATE。 */ + @Override + @Aop(TransAop.READ_COMMITTED) + public void backfillMemberSnapshots() { + // 每个字段独立补缺;没有电话号码属于正常情况,不覆盖已经保存的非空资料。 + dao().execute(Sqls.create(""" + UPDATE club_user member + INNER JOIN sys_user u ON u.id = member.userId + SET member.userName = CASE WHEN NULLIF(TRIM(member.userName), '') IS NULL THEN u.username ELSE member.userName END, + member.loginName = CASE WHEN NULLIF(TRIM(member.loginName), '') IS NULL THEN u.loginname ELSE member.loginName END, + member.mobile = CASE WHEN NULLIF(TRIM(member.mobile), '') IS NULL THEN u.mobile ELSE member.mobile END + WHERE (NULLIF(TRIM(member.userName), '') IS NULL AND NULLIF(TRIM(u.username), '') IS NOT NULL) + OR (NULLIF(TRIM(member.loginName), '') IS NULL AND NULLIF(TRIM(u.loginname), '') IS NOT NULL) + OR (NULLIF(TRIM(member.mobile), '') IS NULL AND NULLIF(TRIM(u.mobile), '') IS NOT NULL) + """)); + // 没有空入会时间时跳过 WF 汇总,避免每次启动扫描不必要的流程历史。 + if (dao().count(ClubUser.class, Cnd.where("joinTime", "is", null)) == 0) return; + // 沿用原查询前补齐规则:同一会员最近一次已完成 WF 会长同意审核;没有有效流程保持空值。 + dao().execute(Sqls.create(""" + UPDATE club_user member + INNER JOIN ( + SELECT application.userId, application.clubId, MAX(task.finishTime) AS joinTime + FROM club_user_apply application + INNER JOIN wf_process_instance instance ON instance.businessNo = application.id + INNER JOIN wf_process_task task ON task.processInstanceId = instance.id + WHERE application.mode = 1 + AND instance.state = 20 + AND task.taskName = '1625a683-3890-4788-95b7-cab8240f6731' + AND task.taskState = 20 + AND task.finishTime IS NOT NULL + AND JSON_UNQUOTE(JSON_EXTRACT(task.variable, '$.submitType')) = '1' + GROUP BY application.userId, application.clubId + ) approval ON approval.userId = member.userId AND approval.clubId = member.clubId + SET member.joinTime = approval.joinTime + WHERE member.joinTime IS NULL + """)); + } + + /** 2026年10月3日 12:01:28;操作人:wxg:统一从系统用户资料取值,禁止依赖申请页面传回的姓名、工号和电话。 */ + @Override + @Aop(TransAop.READ_COMMITTED) + public void prepareMemberSnapshots(List members) { + if (members == null || members.isEmpty()) return; + if (members.stream().anyMatch(member -> member == null || StrUtil.isBlank(member.getUserId()))) { + throw new IllegalArgumentException("新增会员必须提供有效用户ID"); + } + List userIds = members.stream().map(ClubUser::getUserId).distinct().toList(); + Map users = dao().query(Sys_user.class, Cnd.where("id", "in", userIds)).stream() + .collect(Collectors.toMap(Sys_user::getId, user -> user)); + for (ClubUser member : members) { + Sys_user user = users.get(member.getUserId()); + if (user == null) throw new IllegalArgumentException("新增会员的用户资料不存在:" + member.getUserId()); + member.setUserName(user.getUsername()); + member.setLoginName(user.getLoginname()); + member.setMobile(user.getMobile()); + } + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void prepareDirectJoinTimes(List members) { + // 2026年10月3日 12:01:28;操作人:wxg:直接新增入口同时准备人员资料,确认时间不覆盖已保存的入会时间。 + prepareMemberSnapshots(members); + if (members == null || members.isEmpty()) return; + Date confirmedAt = new Date(); + for (ClubUser member : members) { + if (member.getJoinTime() == null) { + member.setJoinTime(confirmedAt); + } + } + } + @Override public List getClubUser(String clubId) { Sql sql = Sqls.create(""" SELECT scu.*, - u.username as userName, - u.loginname as loginName, u.sex, - u.mobile, u.userState, unit.`name` unitName FROM @@ -96,10 +179,7 @@ public class SysClubUserServiceImpl extends BaseServiceImpl implements Sql sql = Sqls.create(""" SELECT scu.*, - u.username as userName, - u.loginname as loginName, u.sex, - u.mobile, u.userState, u.birthday, u.unionname as unionName, 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 0039f761..c4e2f0ea 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 @@ -14,22 +14,21 @@ public class ClubUserCommonPageVo extends ClubUser { private String clubName; private String clubCode; private String typeName; - private String userName; + // 2026年10月3日 12:01:28;操作人:wxg:姓名、工号、电话继承 ClubUser 的持久化字段,避免子类同名属性遮蔽。 private List replaceReport; private List rulesFile; private List afterRulesFile; private List establishReport; - private String loginName; private String sex; private String personType; + /** 用户当前人员属性,来自 vw_user.userAttribute,用于会员列表展示。 */ + private String userAttribute; private String userState; private String unitId; - private String mobile; private String unitName; private String unionName; private String birthday; private String unionId; - private String joinTime; private String exitTime; private String exitType; private String applyRoleCode; diff --git a/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserJoinPageVo.java b/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserJoinPageVo.java index 451ede73..f01a11a4 100644 --- a/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserJoinPageVo.java +++ b/src/main/java/com/budwk/app/zhgh/club/vo/ClubUserJoinPageVo.java @@ -13,7 +13,8 @@ public class ClubUserJoinPageVo{ private String clubId; private String userId; private String roleCode; - private Boolean mode; + // 0:普通退会;1:入会;2:系统处理退会。 + private Integer mode; private String clubPosition; private String email; private String avatar; diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubController.java index 68b5760d..70c32b9e 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/club/controller/OutlayManageClubController.java @@ -18,6 +18,9 @@ import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.club.model.ClubUser; import com.budwk.app.zhgh.club.model.SysClub; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; +import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.service.OutlayManageClubQueryService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -53,6 +56,9 @@ public class OutlayManageClubController { @Inject private SysRoleService sysRoleService; + @Inject + private OutlayManageClubQueryService clubQueryService; + @At("") @Ok("beetl:/platform/zhgh/dayofficework/outlay/outlayManage/club/manage/index.html") @SaCheckPermission("outlay.outlayManage.club.manage") @@ -111,74 +117,17 @@ public class OutlayManageClubController { return Result.success(count); } + /** + * 2026年10月3日 16:23:13;操作人:wxg:年度分配只委托 service,避免 WF 一对多关联造成重复社团预算。 + * 无业务参数,年度固定为系统当前年;返回 Result.success,新增及重复检查在同一事务完成。 + */ @At @ApiOperation("下发预算") @SaCheckPermission("outlay.outlayManage.club.manage") @SLog(tag = "社团预算分配", msg = "根据申报的金额修改本年的预算预算") + @Aop(TransAop.READ_COMMITTED) public Result issuedOutlay() { - Sql sql = Sqls.create(""" - SELECT - c.*, - COALESCE(pay_stats.historyGiveMoneyNum, 0) AS historyGiveMoneyNum, - COALESCE(pay_stats.historyNotGiveMoneyNum, 0) AS historyNotGiveMoneyNum, - COALESCE(pay_stats.historyUserNum, 0) AS historyUserNum - FROM - sys_club c - LEFT JOIN wf_process_instance inst ON inst.businessNo = c.id - LEFT JOIN ( - SELECT - cu.clubId, - COUNT(CASE WHEN re.isGiveMoney = 1 THEN 1 END) AS historyGiveMoneyNum, - COUNT(CASE WHEN re.isGiveMoney = 0 THEN 1 END) AS historyNotGiveMoneyNum, - COUNT(*) AS historyUserNum - FROM - club_user cu - INNER JOIN club_user_apply cua - ON cu.userId = cua.userId - AND cu.clubId = cua.clubId - LEFT JOIN club_pay_record re - ON re.userId = cu.userId - AND re.clubId = cu.clubId - AND re.year = @year - WHERE - YEAR(cua.applyDate) < @year - AND cua.`mode` = 1 - GROUP BY - cu.clubId - ) pay_stats ON pay_stats.clubId = c.id - WHERE - inst.state = 20; - """).setParam("year", DateUtil.thisYear()); - - List clubList = baseService.listMap(sql); - - - List insertUnionList = new ArrayList<>(); - clubList.forEach(v -> { - - OutlayManageClub outlayManageClub = new OutlayManageClub(); - outlayManageClub.setYear(DateUtil.thisYear()); - outlayManageClub.setClubName(v.getString("clubName")); - outlayManageClub.setClubCode(v.getString("clubCode")); - outlayManageClub.setClubId(v.getString("id")); - outlayManageClub.setHistoryUserNum(v.getInt("historyUserNum")); - outlayManageClub.setHistoryNotGiveMoneyNum(v.getInt("historyNotGiveMoneyNum")); - outlayManageClub.setHistoryGiveMoneyNum(v.getInt("historyGiveMoneyNum")); - - BigDecimal totalBudgetMoney; - //如果人数超过50人就是5000,否则就是3000 - if (v.getInt("historyGiveMoneyNum") >= 50) { - totalBudgetMoney = BigDecimal.valueOf(5000); - } else { - totalBudgetMoney = BigDecimal.valueOf(3000); - } - if (v.getInt("historyGiveMoneyNum") == 0) { - totalBudgetMoney = BigDecimal.ZERO; - } - outlayManageClub.setTotalQuota(totalBudgetMoney); - insertUnionList.add(outlayManageClub); - }); - baseService.insert(insertUnionList); + clubQueryService.generateCurrentYearAllocation(); return Result.success(); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayManageClubQueryService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayManageClubQueryService.java index 7c8ce949..5bbb2cb4 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayManageClubQueryService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/OutlayManageClubQueryService.java @@ -9,6 +9,13 @@ import javax.servlet.http.HttpServletResponse; public interface OutlayManageClubQueryService extends BaseService { + /** + * 2026年10月3日 16:23:13;操作人:wxg:生成当前年度社团预算,同年度同社团已存在时跳过,不覆盖金额和核对状态。 + * 社团须存在已完成 WF 流程;保留原年度人数统计和拨付金额规则。 + * @return 本次实际新增的预算记录条数,重复调用且没有新增社团时为 0 + */ + int generateCurrentYearAllocation(); + void doMoney(String clubId); Sql backUserSql(PageForm pageForm, String clubId, String type, Integer year, String payed); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageClubQueryServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageClubQueryServiceImpl.java index f0d456d4..a4f56346 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageClubQueryServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/outlay/outlayManage/service/impl/OutlayManageClubQueryServiceImpl.java @@ -39,6 +39,8 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.HashSet; +import java.util.Set; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -58,6 +60,80 @@ public class OutlayManageClubQueryServiceImpl extends BaseServiceImpl implements super(dao); } + /** 2026年10月3日 16:23:13;操作人:wxg:WF 仅用于判断存在;锁定社团并检查已有年度预算,连续或并发分配不重复新增。 */ + @Override + @Aop(TransAop.READ_COMMITTED) + public int generateCurrentYearAllocation() { + int year = DateUtil.thisYear(); + // 同一事务先按固定顺序锁定候选社团,串行处理分配请求,随后读取最新年度预算。 + // 不改动 WF 流程,不增加索引;多个已完成流程不会增加候选社团行数。 + Sql lockSql = Sqls.create(""" + SELECT c.id FROM sys_club c + WHERE EXISTS ( + SELECT 1 FROM wf_process_instance inst + WHERE inst.businessNo = c.id AND inst.state = 20 + ) + ORDER BY c.id + FOR UPDATE + """); + List clubs = listMap(lockSql); + if (clubs.isEmpty()) return 0; + Set recordedClubs = new HashSet<>(); + for (OutlayManageClub existing : dao().query(OutlayManageClub.class, Cnd.where("year", "=", year))) { + recordedClubs.add(existing.getClubId()); + } + // 2026年10月3日 17:11:57;操作人:wxg:年度预算人数取预算年度减一的缴费名单。 + // 按名单保存的 isGiveMoney 分别统计拨付、未拨付人数,不依赖当前会员关系或入会申请,不重新计算历史拨付资格。 + Sql sql = Sqls.create(""" + SELECT + c.*, + COALESCE(pay_stats.historyGiveMoneyNum, 0) AS historyGiveMoneyNum, + COALESCE(pay_stats.historyNotGiveMoneyNum, 0) AS historyNotGiveMoneyNum, + COALESCE(pay_stats.historyUserNum, 0) AS historyUserNum + FROM + sys_club c + LEFT JOIN ( + SELECT + re.clubId, + COUNT(CASE WHEN re.isGiveMoney = 1 THEN 1 END) AS historyGiveMoneyNum, + COUNT(CASE WHEN re.isGiveMoney = 0 THEN 1 END) AS historyNotGiveMoneyNum, + COUNT(*) AS historyUserNum + FROM + club_pay_record re + WHERE + re.year = @historyYear + GROUP BY + re.clubId + ) pay_stats ON pay_stats.clubId = c.id + WHERE + EXISTS ( + SELECT 1 FROM wf_process_instance inst + WHERE inst.businessNo = c.id AND inst.state = 20 + ) + ORDER BY c.id; + """).setParam("historyYear", year - 1); + List clubList = listMap(sql); + Set lockedClubs = clubs.stream().map(club -> club.getString("id")).collect(Collectors.toSet()); + List budgets = new ArrayList<>(); + for (NutMap club : clubList) { + String clubId = club.getString("id"); + // 新增社团若在候选锁定后完成审批,则留到下次处理;既有预算不得重置或覆盖。 + if (!lockedClubs.contains(clubId) || !recordedClubs.add(clubId)) continue; + OutlayManageClub budget = new OutlayManageClub(); + budget.setYear(year); + budget.setClubId(clubId); + budget.setClubName(club.getString("clubName")); + budget.setClubCode(club.getString("clubCode")); + budget.setHistoryUserNum(club.getInt("historyUserNum")); + budget.setHistoryGiveMoneyNum(club.getInt("historyGiveMoneyNum")); + budget.setHistoryNotGiveMoneyNum(club.getInt("historyNotGiveMoneyNum")); + budget.setTotalQuota(getAllocateStandard(club.getInt("historyGiveMoneyNum"))); + budgets.add(budget); + } + if (!budgets.isEmpty()) dao().insert(budgets); + return budgets.size(); + } + @Aop(TransAop.READ_COMMITTED) @Override public void doMoney(String clubId) { diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/aidFund/service/impl/AidFundMemberPayServiceImpl.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/aidFund/service/impl/AidFundMemberPayServiceImpl.java index 5f054706..2331a5d5 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/aidFund/service/impl/AidFundMemberPayServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/medicalMutualAid/aidFund/service/impl/AidFundMemberPayServiceImpl.java @@ -88,6 +88,8 @@ public class AidFundMemberPayServiceImpl extends BaseServiceImpl sysUserList = dao().query(Sys_user.class, Cnd.where("id", "in", ids)); + List sysUserList = lockChangeUsers(ids); + validateRepeatedChange(sysUserList, changeType); List recordArrayList = new ArrayList<>(); - for (String id : ids) { - Sys_user user = sysUserList.stream().filter(sysUser -> sysUser.getId().equals(id)).findFirst().orElse(null); - if (user == null) { - continue; - } + for (Sys_user user : sysUserList) { recordArrayList.add(buildAdminChangeRecord(user, changeType)); } updateUserChangeType(ids, changeType); @@ -279,14 +281,78 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund @Override @Aop(TransAop.READ_COMMITTED) public void doEditChangeTypeByAdmin(String userId, String changeType) { - Sys_user user = dao().fetch(Sys_user.class, userId); - if (user == null) { + List users = lockChangeUsers(new String[]{userId}); + if (users.isEmpty()) { return; } + validateRepeatedChange(users, changeType); + Sys_user user = users.get(0); dao().insert(buildAdminChangeRecord(user, changeType)); updateUserChangeType(new String[]{userId}, changeType); } + /** + * 在调用方 READ_COMMITTED 事务中按固定顺序锁定用户,串行处理同一人的并发变更。 + * ids 为单人或批量操作的用户 ID;返回去重后的当前用户,锁持续到事务结束。 + */ + private List lockChangeUsers(String[] ids) { + List users = new ArrayList<>(); + for (String id : Arrays.stream(ids).filter(StrUtil::isNotBlank).distinct().sorted().toList()) { + Sql sql = Sqls.create("SELECT * FROM sys_user WHERE id=@id FOR UPDATE"); + sql.setParam("id", id); + sql.setEntity(dao().getEntity(Sys_user.class)); + sql.setCallback(Sqls.callback.entities()); + dao().execute(sql); + users.addAll(sql.getList(Sys_user.class)); + } + return users; + } + + /** + * 整批写入前检查最近已生效变更;有重复时一次性提示全部人员,不更新档案或新增记录。 + * users 为已加行锁的用户,changeType 为变更类型字典编码;正常返回表示允许提交。 + * 只比较连续的相同变更,并核对当前状态;退会后重新入会允许再次合法退会。 + */ + private void validateRepeatedChange(List users, String changeType) { + List repeated = new ArrayList<>(); + Sys_dict dict = dao().fetch(Sys_dict.class, Cnd.where("code", "=", changeType)); + String typeName = dict == null ? changeType : dict.getName(); + for (Sys_user user : users) { + // 管理员记录和无流程的历史记录直接生效;wf 申请仅纳入已完成流程,排除处理中、撤回等记录。 + Sql sql = Sqls.create(""" + SELECT info.* FROM aid_fund_member_change_record info + WHERE info.userId=@userId AND IFNULL(info.delFlag, 0)=0 + AND (info.adminOperate=1 + OR NOT EXISTS (SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo=info.id) + OR EXISTS (SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo=info.id AND ins.state=20)) + ORDER BY info.applyTime DESC, info.createdAt DESC, info.id DESC + LIMIT 1 + """); + sql.setParam("userId", user.getId()); + sql.setEntity(dao().getEntity(AidFundMemberChangeRecord.class)); + sql.setCallback(Sqls.callback.entities()); + dao().execute(sql); + List records = sql.getList(AidFundMemberChangeRecord.class); + if (records.isEmpty()) { + continue; + } + AidFundMemberChangeRecord record = records.get(0); + boolean retirement = ObjectUtil.equals(changeType, "AIDFUND_MEMBER_CHANGE_TYPE_EIGTH"); + boolean targetState = retirement ? ObjectUtil.equals(user.getAidFundMemberUserType(), "离退休人员") + : ObjectUtil.equals(user.getAidFundMember(), AidFundMemberMode.NONE.getCode()); + // 入会时间晚于上次退会申请时,属于新的会员周期,不受旧退会记录限制。 + boolean rejoined = !retirement && user.getAidFundMemberJoinTime() != null && record.getApplyTime() != null + && user.getAidFundMemberJoinTime().after(record.getApplyTime()); + if (ObjectUtil.equals(record.getChangeType(), changeType) && targetState && !rejoined) { + String time = record.getApplyTime() == null ? "未知时间" : DateUtil.format(record.getApplyTime(), "yyyy-MM-dd HH:mm:ss"); + repeated.add(user.getUsername() + "(" + user.getLoginname() + ")已于" + time + "执行“" + typeName + "”"); + } + } + if (!repeated.isEmpty()) { + throw new BaseException(String.join(";", repeated) + ",请勿重复操作。本次全部未保存,请调整名单后重新提交。"); + } + } + /** * 管理端变更落记录时,需要明确标记记录来源,避免被“我的申请”误识别为本人发起。 * @@ -323,6 +389,7 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund * @param ids 需要更新的用户ID数组 * @param changeType 变更类型字典值 */ + @Aop(TransAop.READ_COMMITTED) private void updateUserChangeType(String[] ids, String changeType) { Chain add; if (ObjectUtil.equals(changeType, "AIDFUND_MEMBER_CHANGE_TYPE_EIGTH")) { diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/param/PendingConfirmUserBatchChangeParam.java b/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/param/PendingConfirmUserBatchChangeParam.java index 7e7b0180..80afccca 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/param/PendingConfirmUserBatchChangeParam.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/param/PendingConfirmUserBatchChangeParam.java @@ -26,7 +26,7 @@ public class PendingConfirmUserBatchChangeParam { private String userState; /** - * 批量设置的退休时间,仅在目标在职状态为退休且传入日期时参与变更。 + * 批量设置的退休时间;目标在职状态为退休或人员类型为离退休人员时必填,并统一应用于勾选人员。 */ private Date retireDate; diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/param/PendingConfirmUserPageForm.java b/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/param/PendingConfirmUserPageForm.java index 7c210f6a..1e351479 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/param/PendingConfirmUserPageForm.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/param/PendingConfirmUserPageForm.java @@ -4,6 +4,8 @@ import com.budwk.app.base.param.PageForm; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + /** * 待确认人员记录分页查询参数。 */ @@ -20,6 +22,9 @@ public class PendingConfirmUserPageForm extends PageForm { /** 人员类型。 */ private String personType; + /** 人员属性字典编码集合,前端以 JSON 数组提交;空集合表示不限制人员属性。 */ + private List userAttributes; + /** 在职状态。 */ private String userState; diff --git a/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/service/impl/PendingConfirmUserServiceImpl.java b/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/service/impl/PendingConfirmUserServiceImpl.java index ebf1ea80..fa7e0d6f 100644 --- a/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/service/impl/PendingConfirmUserServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/staffmanage/sourcechange/service/impl/PendingConfirmUserServiceImpl.java @@ -150,6 +150,10 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl imp cnd.andEX("userInfo.unionId", "=", pageForm.getUnionId()); cnd.andEX("userInfo.unitId", "=", pageForm.getUnitId()); cnd.andEX("userInfo.personType", "=", pageForm.getPersonType()); + // 多选人员属性匹配任一所选编码;未选择时不增加条件,列表和导出共用此筛选。 + if (pageForm.getUserAttributes() != null && !pageForm.getUserAttributes().isEmpty()) { + cnd.and("userInfo.userAttribute", "in", pageForm.getUserAttributes()); + } cnd.andEX("userInfo.userState", "=", pageForm.getUserState()); String orderColumn = resolveOrderColumn(pageForm.getPageOrderName()); @@ -179,6 +183,14 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl imp throw new BaseException("请选择人员类型"); } validatePersonType(record.getPersonType()); + // 单人退休变更必须显式提交退休日期,避免绕过页面条件必填校验;不影响批量入口。 + String retiredPersonTypeCode = resolveRetiredPersonTypeCode(); + boolean requiresRetireDate = StrUtil.equals("退休", record.getUserState()) + || (StrUtil.isNotBlank(retiredPersonTypeCode) + && StrUtil.equals(retiredPersonTypeCode, StrUtil.trim(record.getPersonType()))); + if (requiresRetireDate && record.getRetireDate() == null) { + throw new BaseException("请选择退休日期"); + } // 单人页面虽然提交完整表单,服务层只采纳待确认业务允许编辑的字段,防止请求篡改其他只读字段。 return changePendingUserFields(record); } @@ -203,6 +215,15 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl imp validatePersonType(changeParam.getPersonType()); } + // 在修改任何人员前校验批量退休时间,避免绕过前端后复用各人员原日期或产生部分更新。 + String retiredPersonTypeCode = resolveRetiredPersonTypeCode(); + boolean requiresRetireDate = StrUtil.equals("退休", changeParam.getUserState()) + || (StrUtil.isNotBlank(retiredPersonTypeCode) + && StrUtil.equals(retiredPersonTypeCode, StrUtil.trim(changeParam.getPersonType()))); + if (requiresRetireDate && changeParam.getRetireDate() == null) { + throw new BaseException("请选择退休时间"); + } + List userIds; try { userIds = JSONUtil.parseArray(changeParam.getUserIds()).toList(String.class); @@ -229,8 +250,8 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl imp if (StrUtil.isNotBlank(changeParam.getUserState())) { record.setUserState(changeParam.getUserState()); } - // 退休时间只随“退休”状态批量写入,其他状态忽略该参数,避免请求篡改隐藏字段。 - if (StrUtil.equals("退休", changeParam.getUserState()) && changeParam.getRetireDate() != null) { + // 满足退休状态或离退休人员类型时统一写入本次指定日期,其他情况忽略隐藏字段。 + if (requiresRetireDate) { record.setRetireDate(changeParam.getRetireDate()); } if (StrUtil.equals("退休", changeParam.getUserState()) && changeParam.getWelfareStopDate() != null) { diff --git a/src/main/resources/views/layouts/v4/baseLayout.html b/src/main/resources/views/layouts/v4/baseLayout.html index d3c05ffe..a7e6cc72 100644 --- a/src/main/resources/views/layouts/v4/baseLayout.html +++ b/src/main/resources/views/layouts/v4/baseLayout.html @@ -696,10 +696,10 @@ 应用中心 - + 待办中心 diff --git a/src/main/resources/views/platform/sys/data/user/pull.html b/src/main/resources/views/platform/sys/data/user/pull.html index 18808793..7d120f6b 100644 --- a/src/main/resources/views/platform/sys/data/user/pull.html +++ b/src/main/resources/views/platform/sys/data/user/pull.html @@ -47,11 +47,13 @@ layout("/layouts/platform.html"){ + @@ -124,14 +126,25 @@ layout("/layouts/platform.html"){ }, methods: { pull() { + // 拉取未结束时不再打开确认框或发送重复请求。 + if (this.pullLoading) return this.$confirm("确定要拉取数据源吗?", "提示", { type: "warning", confirmButtonText: "确定", cancelButtonText: "取消" }) .then(() => { - this.pullLoading = true - this.$axios + if (this.pullLoading) return + this.$set(this, "pullLoading", true) + // 全屏提示正在拉取,锁定页面操作,避免仅按钮转圈不易察觉。 + const loading = this.$loading({ + fullscreen: true, + lock: true, + text: "正在拉取信息中心数据,请稍候", + spinner: "el-icon-loading", + background: "rgba(0, 0, 0, 0.5)" + }) + return this.$axios .post("/platform/sys/data/user/pull/pullData") .then((resp) => { if (resp.code === 0) { @@ -141,7 +154,9 @@ layout("/layouts/platform.html"){ } }) .finally(() => { - this.pullLoading = false + // 成功、业务失败或网络异常均关闭遮罩,并恢复按钮状态。 + loading.close() + this.$set(this, "pullLoading", false) }) }) .catch(() => {}) 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 e734e3de..c956f33b 100644 --- a/src/main/resources/views/platform/sys/data/user/update.html +++ b/src/main/resources/views/platform/sys/data/user/update.html @@ -249,6 +249,8 @@ layout("/layouts/platform.html"){ }) }, execUpdate() { + // 人员更新未结束时不再打开确认框或提交重复请求。 + if (this.updateLoading) return if (!this.updateFromData.pullTime) { this.$message.warning("请选择数据源时间") return @@ -269,8 +271,17 @@ layout("/layouts/platform.html"){ type: "warning" }) .then(() => { - this.updateLoading = true - this.$axios + if (this.updateLoading) return + this.$set(this, "updateLoading", true) + // 遮罩覆盖人员更新弹窗及页面,明确提示处理中并阻止重复操作。 + const loading = this.$loading({ + fullscreen: true, + lock: true, + text: "正在更新人员数据,请稍候", + spinner: "el-icon-loading", + background: "rgba(0, 0, 0, 0.5)" + }) + return this.$axios .post("/platform/sys/data/user/update/updateUserFormSource", { param: JSON.stringify(this.updateFromData) }) .then((resp) => { if (resp.code === 0) { @@ -280,9 +291,14 @@ layout("/layouts/platform.html"){ } }) .finally(() => { - this.updateLoading = false + // 成功、业务失败及网络异常均释放遮罩,恢复人员更新按钮。 + loading.close() + this.$set(this, "updateLoading", false) }) }) + .catch(() => { + // 取消确认不提交;请求错误提示继续由全局 axios 拦截器处理。 + }) }, listUnit() { this.$businessTool.listUnit(this.pageForm.unionId).then((res) => { diff --git a/src/main/resources/views/platform/sys/login.html b/src/main/resources/views/platform/sys/login.html index 7c89de05..2898d2ac 100644 --- a/src/main/resources/views/platform/sys/login.html +++ b/src/main/resources/views/platform/sys/login.html @@ -124,7 +124,8 @@ } diff --git a/src/main/resources/views/platform/zhgh/club/infoManage/exitManage/info.js b/src/main/resources/views/platform/zhgh/club/infoManage/exitManage/info.js index 0316482a..03a6db3d 100644 --- a/src/main/resources/views/platform/zhgh/club/infoManage/exitManage/info.js +++ b/src/main/resources/views/platform/zhgh/club/infoManage/exitManage/info.js @@ -86,6 +86,8 @@ const clubExitInfo = { }, getExitTypeName() { const exitType = this.row.exitType || this.viewData.exitType + // 2026年10月3日 19:42:45;操作人:wxg:详情与列表保持一致,mode=2 显示系统处理。 + if (exitType === "SYSTEM_EXIT") return "系统处理" return exitType === "AUDIT_EXIT" ? "审核退会" : "直接移出" }, openChart() { diff --git a/src/main/resources/views/platform/zhgh/club/infoManage/manage/clubInfoManage.js b/src/main/resources/views/platform/zhgh/club/infoManage/manage/clubInfoManage.js index ace6b6e2..d338e742 100644 --- a/src/main/resources/views/platform/zhgh/club/infoManage/manage/clubInfoManage.js +++ b/src/main/resources/views/platform/zhgh/club/infoManage/manage/clubInfoManage.js @@ -1,4 +1,4 @@ -const CLUB_INFO_MANAGE_TEMPLATE = { +const CLUB_INFO_MANAGE_TEMPLATE = { template: `
@@ -7,29 +7,44 @@ - + + + + + + + - + 全部 - 社团成员 + + 社团会员 理事机构 + 历史会员 - @@ -37,11 +52,15 @@ + + - + + + @@ -157,6 +179,37 @@ }, data() { return { + pageForm: { + focusMembers: false, + joinDateRange: [], + joinDateStart: null, + joinDateEnd: null, + pageOrderName: "joinTime", + pageOrderBy: "descending" + }, + // 快捷范围按自然月计算,月底由 moment 自动调整为目标月份的有效日期。 + joinDatePickerOptions: { + shortcuts: [ + { + text: "当前年度", + onClick: picker => { + picker.$emit("pick", [this.$moment().startOf("year").toDate(), new Date()]) + } + }, + { + text: "近半年", + onClick: picker => { + picker.$emit("pick", [this.$moment().subtract(6, "months").startOf("day").toDate(), new Date()]) + } + }, + { + text: "近三个月", + onClick: picker => { + picker.$emit("pick", [this.$moment().subtract(3, "months").startOf("day").toDate(), new Date()]) + } + } + ] + }, roleCodeFormData: {}, roleCodeFormRules: { userName: [{ required: true, message: "必填", trigger: ["blur"] }], @@ -183,6 +236,28 @@ "file-import": httpVueLoader("/components/plugins/sysImport/index.vue?v=" + new Date().getTime()) }, methods: { + /** 启用重点关注时同时筛选自聘人员和其他人员,清空单选条件以避免冲突;取消后恢复普通查询。 */ + toggleFocusMembers() { + this.$set(this.pageForm, "focusMembers", !this.pageForm.focusMembers) + if (this.pageForm.focusMembers) { + this.$set(this.pageForm, "personType", "") + } + this.doSearch() + }, + /** 页签切换只调整业务排序,查询和重置页码继续复用 initTableMixins。 */ + memberTabChange() { + this.$set(this.pageForm, "pageOrderName", this.pageForm.radioType === "4" ? "exitTime" : "joinTime") + this.$set(this.pageForm, "pageOrderBy", "descending") + this.doSearch() + }, + /** 日期范围始终筛选入会时间;当前会员按入会时间倒序,历史成员按退出时间倒序。 */ + joinDateRangeChange(val) { + this.$set(this.pageForm, "joinDateStart", val && val.length === 2 ? val[0] : null) + this.$set(this.pageForm, "joinDateEnd", val && val.length === 2 ? val[1] : null) + this.$set(this.pageForm, "pageOrderName", this.pageForm.radioType === "4" ? "exitTime" : "joinTime") + this.$set(this.pageForm, "pageOrderBy", "descending") + this.doSearch() + }, openImport() { this.importData = { isfg: 1, @@ -316,21 +391,29 @@ } } }, - async exitClub(row) { - const confirm = await this.$confirm("确定要将其退会吗?", "提示", { + exitClub(row) { + if (this.formLoading) return + return this.$confirm("确定要将其退会吗?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" + }).then(() => { + if (this.formLoading) return + this.$set(this, "formLoading", true) + return this.$axios.post("/platform/club/infoManage/manage/exitClub", { id: row.id }).then(resp => { + if (resp.code === 0) { + this.$message.success(resp.msg) + return this.pageData() + } else { + this.$message.warning(resp.msg) + } + }).finally(() => { + // 接口或列表刷新异常时也释放提交状态,允许后续操作。 + this.$set(this, "formLoading", false) + }) + }, () => { + // 取消或关闭确认框时,不发送退会请求。 }) - if (confirm === "confirm") { - const resp = await this.$axios.post("/platform/club/infoManage/manage/exitClub", { id: row.id }) - if (resp.code === 0) { - this.$message.success(resp.msg) - await this.pageData() - } else { - this.$message.warning(resp.msg) - } - } }, //导出登记表 @@ -369,4 +452,3 @@ this.pageData() } } - diff --git a/src/main/resources/views/platform/zhgh/club/infoManage/manage/index.html b/src/main/resources/views/platform/zhgh/club/infoManage/manage/index.html index 706270cc..9ffe55f1 100644 --- a/src/main/resources/views/platform/zhgh/club/infoManage/manage/index.html +++ b/src/main/resources/views/platform/zhgh/club/infoManage/manage/index.html @@ -42,6 +42,16 @@ layout("/layouts/platform.html"){ #app .club-manage-query-card { flex: 0 0 auto; } + /* 日期范围框预留约 320px,包含标签的查询项同步加宽;窄屏仍可换行并适配容器。 */ + #app .club-manage-query-card .search .club-join-date-query { + flex: 0 0 420px; + width: 420px; + max-width: 100%; + } + #app .club-manage-query-card .club-join-date-query .search-item-option { + flex: 1 1 auto; + min-width: 0; + } #app .club-manage-list-card { flex: 1 1 auto; min-height: 0; @@ -71,6 +81,7 @@ layout("/layouts/platform.html"){
{ - this.$refs.clubInfo.pageData() + // 一级节点展示社团总览,只有具体社团节点才需要刷新会员列表。 + if (node.level === 2 && this.$refs.clubInfo) { + this.$refs.clubInfo.pageData() + } }) }, getTreeData() { this.$axios.post("/platform/club/infoManage/manage/getClubTreeData").then((res) => { this.treeData = res.data + this.$nextTick(() => { + // 首次进入时选中一级节点,同步树高亮与右侧总览;后续刷新保留用户选择。 + if (!this.currentTreeNode && this.treeData && this.treeData.length > 0) { + const rootData = this.treeData[0] + this.$refs.treeRef.setCurrentKey(rootData.id) + this.$set(this, "currentTreeData", rootData) + this.$set(this, "currentTreeNode", this.$refs.treeRef.getNode(rootData.id)) + } + }) }) }, async userRemoteMethod(query) { diff --git a/src/main/resources/views/platform/zhgh/club/infoManage/manage/schoolClubManage.js b/src/main/resources/views/platform/zhgh/club/infoManage/manage/schoolClubManage.js index cffc0ab3..cf07a87b 100644 --- a/src/main/resources/views/platform/zhgh/club/infoManage/manage/schoolClubManage.js +++ b/src/main/resources/views/platform/zhgh/club/infoManage/manage/schoolClubManage.js @@ -21,8 +21,12 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = { -