王总电脑修改代码

This commit is contained in:
zhouhefeng
2026-10-08 09:43:38 +08:00
parent ce75b40656
commit c829cbf725
62 changed files with 2430 additions and 833 deletions
@@ -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);
@@ -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);
@@ -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<ClubUser> clubUserList = dao.query(ClubUser.class, Cnd.NEW());
// 生成缴费记录对象
List<ClubPayRecord> 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());
}
}
@@ -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();
@@ -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,
@@ -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
@@ -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<NutMap> 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<NutMap> 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
@@ -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<String> 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<Sys_user_role> 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);
}
}
@@ -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<String> 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<NutMap> 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<ClubPayRecord> list = payRecordService.query(Cnd.where("id", "in", ids));
for (ClubPayRecord payRecord : list) {
List<JSONObject> 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();
}
}
@@ -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()));
}
@@ -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);
}
@@ -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("操作记录")
@@ -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;
}
@@ -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("社团职务")
@@ -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;
@@ -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<ClubPayRecord> {
/** 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<ClubPayRecord> 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<NutMap> 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);
/**
* 获取社团成员是否拨付
@@ -5,6 +5,20 @@ import com.budwk.app.zhgh.club.model.ClubUserApply;
public interface ClubUserJoinService extends BaseService<ClubUserApply> {
/**
* 清除待审核入会申请的入退会时间,防止沿用协会成立时间或上次入会时间。
* @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> {
ClubUserApply buildExitApply(String clubId, String userId);
/**
* 获取社团成员的入会时间,优先取历史有效入会申请记录,
* 没有时兜底为社团创办时间。
* 获取社团成员的入会时间,优先取当前 club_user.joinTime。
* 旧会员关系尚无入会时间时,保留历史申请及社团创办时间的原有回退规则。
*
* @param clubId 社团ID
* @param userId 用户ID
@@ -13,12 +13,24 @@ import java.util.List;
public interface SysClubExamineService extends BaseService<SysClubExamineRegister> {
// 2026年10月2日wxg屏蔽操作:保留原统计接口说明。
// /**
// * 按会员入会、退休、退会时间统计指定年度的在职和退休人数变化。
// *
// * @param clubId 社团ID
// * @param year 统计年度
// * @return 在职和退休人员的年初、增加、减少、年末人数
// */
// List<NutMap> 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<NutMap> getClubUserNum(@Valid String clubId, Integer year);
@@ -22,6 +22,13 @@ public interface SysClubInfoManageService extends BaseService<ClubCommonPageVo>
Pagination<ClubCommonPageVo> infoManagePageData(@Valid ClubUserPageForm pageForm);
/**
* 修改时间:2026年10月2日 23:06:55;操作人:wxg。
* 当前会员查询前,用已完成 WF 入会流程的最近一次会长同意审核时间补齐空入会时间。
* @param pageForm clubId 为选中社团ID;radioType=4 查历史成员且不补齐,其他值查当前会员;
* 姓名/工号、人员类型、在职状态、入会日期范围及分页排序参数用于列表筛选
* @return 会员分页对象,list 中 joinTime 为会员关系保存的入会时间;历史成员仍取申请表时间
*/
Pagination<ClubUserCommonPageVo> infoManageUserPageData(@Valid ClubUserPageForm pageForm);
Pagination<ClubUserCommonPageVo> exitManagePageData(@Valid ClubUserPageForm pageForm);
@@ -30,6 +37,13 @@ public interface SysClubInfoManageService extends BaseService<ClubCommonPageVo>
ClubUserJoinVo exitManageInfo(String id);
/**
* 管理端直接退会,同时新增系统处理记录并清理当前社团会员关系及角色关联。
* @param membershipId club_user 的会员关系ID,不是用户ID或申请ID
* @return void;会员关系不存在或已退会时抛出异常,事务回滚
*/
void exitClub(String membershipId);
List<NutMap> getClubTreeData();
/**
@@ -23,6 +23,12 @@ import java.util.List;
**/
public interface SysClubService extends BaseService<SysClub> {
/**
* 获取入会申请的社团下拉选项,供 PC 与 H5 共用。
* @return 存在已完成 WF 流程的社团列表,每项为 SysClub,包含 id、clubName 等社团字段;同一社团只返回一次
*/
List<SysClub> listJoinClubOptions();
Pagination<ClubCommonPageVo> getAllClubWithPage(@Valid PageForm pageForm, Cnd cnd);
ClubRegisterVo findOne(String clubId);
@@ -17,6 +17,32 @@ import java.util.List;
**/
public interface SysClubUserService extends BaseService<ClubUser> {
/**
* 为直接新增的会员关系设置本批次的后端确认时间,不覆盖已有入会时间。
* @param members 手工录入、导入或社团创建时直接新增的会员关系;不得传入审核入会记录
* @return void;仅填写列表内 joinTime 为空的对象,由调用方在同一事务中保存
*/
void prepareDirectJoinTimes(List<ClubUser> 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<ClubUser> members);
List<NutMap> getClubUser(@Valid String clubId);
Pagination pageDataByApplyJoinClubAudit(@Valid ClubUserPageForm pageForm);
@@ -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<ClubPayRecord> 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<ClubPayRecord> 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<String> userIds = records.stream().map(ClubPayRecord::getUserId).distinct().toList();
List<String> clubIds = records.stream().map(ClubPayRecord::getClubId).distinct().toList();
Map<String, View_user> users = new HashMap<>();
for (View_user user : dao().query(View_user.class, Cnd.where("id", "in", userIds))) {
users.put(user.getId(), user);
}
Map<String, Date> joinTimes = new HashMap<>();
Set<String> 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<String> recorded = new HashSet<>();
for (ClubPayRecord record : dao().query(ClubPayRecord.class, Cnd.where("year", "=", year))) {
recorded.add(record.getClubId() + ":" + record.getUserId());
}
List<ClubPayRecord> 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<String> 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<NutMap> 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<NutMap> list = pagination.getList();
Set<String> activeMembers = new HashSet<>();
if (!list.isEmpty()) {
List<String> userIds = list.stream().map(row -> row.getString("userId")).distinct().toList();
List<String> 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<ClubPayRecord> records = dao().query(ClubPayRecord.class, scope);
if (records.size() != java.util.Arrays.stream(ids).distinct().count()) {
throw new IllegalArgumentException("部分缴费记录不存在或无权操作,请刷新列表");
}
List<String> userIds = records.stream().map(ClubPayRecord::getUserId).distinct().toList();
List<String> clubIds = records.stream().map(ClubPayRecord::getClubId).distinct().toList();
Set<String> 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<JSONObject> 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<ClubPayRecord> 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());
}
}
@@ -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<ClubUserApply> implements ClubUserJoinService {
// PC 与 H5 入会资格校验共用此名单,人员类型须与用户视图中的值完全一致。
private static final List<String> 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<ClubUserApply> 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<ClubUserApply> 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<ClubUserApply> 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)
@@ -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<SysClubExamineReg
super(dao);
}
@Override
public List<NutMap> 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<MembershipPeriod> membershipPeriods = getMembershipPeriods(clubId);
NutMap activeResult = createUserChangeResult("在职");
NutMap retiredResult = createUserChangeResult("退休");
// 2026年10月2日wxg屏蔽操作:原统计入口及专用辅助代码逐行注释,保留原文便于对比或恢复。
// @Override
// public List<NutMap> 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<MembershipPeriod> 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<MembershipPeriod> 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<MembershipPeriod> periods = listMap(legacySql).stream()
// .map(row -> new MembershipPeriod(
// row.getString("userId"),
// row.getTime("joinTime"),
// row.getTime("exitTime"),
// !Boolean.FALSE.equals(row.getBoolean("isNormal")),
// true,
// row.getString("userState"),
// row.getTime("retireDate")
// ))
// .filter(period -> period.joinTime != null)
// .collect(Collectors.toCollection(ArrayList::new));
//
// 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<NutMap> applications = listMap(applySql);
// if (!applications.isEmpty()) {
// // 流程状态改为按当前社团申请批量查询,避免相关子查询对流程实例表重复扫描。
// List<String> applicationIds = applications.stream()
// .map(application -> application.getString("id"))
// .filter(StrUtil::isNotBlank)
// .collect(Collectors.toList());
// List<ProcessInstance> finishedInstances = applicationIds.isEmpty() ? new ArrayList<>()
// : dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", applicationIds)
// .and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.FINISHED.getCode()));
// Set<String> 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<MembershipPeriod> periods, NutMap application, Date applyDate) {
// String userId = application.getString("userId");
// Date applicationJoinTime = application.getTime("joinTime");
// boolean representedByLegacy = periods.stream()
// .filter(period -> period.legacy && Objects.equals(period.userId, userId))
// .filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
// .anyMatch(period -> period.exitTime == null || !applyDate.after(period.exitTime));
// boolean representedByNewPeriod = periods.stream()
// .filter(period -> !period.legacy && Objects.equals(period.userId, userId))
// .anyMatch(period -> isMemberAt(period, applyDate));
// if (representedByLegacy || representedByNewPeriod) {
// return;
// }
// periods.add(new MembershipPeriod(
// userId,
// applyDate,
// null,
// true,
// false,
// application.getString("userState"),
// application.getTime("retireDate")
// ));
// }
//
// /**
// * 退会申请优先关闭入会时间相同的会员周期;新系统复入会后再退会时关闭最近的有效周期。
// */
// private void mergeExitApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
// String userId = application.getString("userId");
// Date applicationJoinTime = application.getTime("joinTime");
// Date exitTime = application.getTime("exitTime") == null ? applyDate : application.getTime("exitTime");
// MembershipPeriod target = periods.stream()
// .filter(period -> Objects.equals(period.userId, userId))
// .filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
// .filter(period -> period.exitTime == null)
// .max(Comparator.comparing(period -> period.joinTime))
// .orElse(null);
// if (target == null) {
// target = periods.stream()
// .filter(period -> Objects.equals(period.userId, userId))
// .filter(period -> period.exitTime == null && !period.joinTime.after(exitTime))
// .max(Comparator.comparing(period -> period.joinTime))
// .orElse(null);
// }
// if (target != null) {
// target.exitTime = exitTime;
// }
// }
//
// private NutMap createUserChangeResult(String userState) {
// return NutMap.NEW()
// .addv("userState", userState)
// .addv("yearFirstNum", 0)
// .addv("yearAddNum", 0)
// .addv("yearReduceNum", 0)
// .addv("thisYearNum", 0);
// }
//
// private void increaseResult(String userState, NutMap activeResult, NutMap retiredResult, String field) {
// NutMap result = "退休".equals(userState) ? retiredResult : "在职".equals(userState) ? activeResult : null;
// if (result != null) {
// result.put(field, result.getInt(field) + 1);
// }
// }
//
// private boolean isInYear(Date date, Date yearStart, Date nextYearStart) {
// return date != null && !date.before(yearStart) && date.before(nextYearStart);
// }
//
// private boolean isMemberAt(MembershipPeriod period, Date date) {
// return !period.joinTime.after(date) && (period.exitTime == null || period.exitTime.after(date));
// }
//
// private boolean isMemberAtYearBoundary(MembershipPeriod period, Date boundary) {
// return period.joinTime.before(boundary) && (period.exitTime == null || !period.exitTime.before(boundary));
// }
//
// private String classifyAtBoundary(MembershipPeriod period, Date boundary) {
// if (period.retireDate != null) {
// return period.retireDate.before(boundary) ? "退休" : "在职";
// }
// return normalizeUserState(period.userState);
// }
//
// private String classifyAtEvent(MembershipPeriod period, Date eventTime) {
// if (period.retireDate != null) {
// return period.retireDate.after(eventTime) ? "在职" : "退休";
// }
// return normalizeUserState(period.userState);
// }
//
// private String classifyBeforeEvent(MembershipPeriod period, Date eventTime) {
// if (period.retireDate != null) {
// return period.retireDate.before(eventTime) ? "退休" : "在职";
// }
// return normalizeUserState(period.userState);
// }
//
// private String normalizeUserState(String userState) {
// // 历史成员数据可能未同步人员状态,空值不参与在职、退休人数统计。
// if (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<NutMap> 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<NutMap> 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<String, NutMap> snapshots = new java.util.LinkedHashMap<>();
Map<String, AnnualMemberPeriod> 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<NutMap> 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<NutMap> 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<String> userIds = periods.values().stream().map(period -> period.userId).collect(Collectors.toSet());
Map<String, NutMap> 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<String, Date> previousEnd = new java.util.HashMap<>();
Set<String> retiredTransfers = new HashSet<>();
List<AnnualMemberPeriod> 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<String, AnnualMemberPeriod> 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<MembershipPeriod> 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<MembershipPeriod> periods = listMap(legacySql).stream()
.map(row -> new MembershipPeriod(
row.getString("userId"),
row.getTime("joinTime"),
row.getTime("exitTime"),
!Boolean.FALSE.equals(row.getBoolean("isNormal")),
true,
row.getString("userState"),
row.getTime("retireDate")
))
.filter(period -> period.joinTime != null)
.collect(Collectors.toCollection(ArrayList::new));
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<NutMap> applications = listMap(applySql);
if (!applications.isEmpty()) {
// 流程状态改为按当前社团申请批量查询,避免相关子查询对流程实例表重复扫描。
List<String> applicationIds = applications.stream()
.map(application -> application.getString("id"))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
List<ProcessInstance> finishedInstances = applicationIds.isEmpty() ? new ArrayList<>()
: dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", applicationIds)
.and(ProcessInstance::getState, "=", ProcessInstanceStateEnum.FINISHED.getCode()));
Set<String> 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<MembershipPeriod> periods, NutMap application, Date applyDate) {
String userId = application.getString("userId");
Date applicationJoinTime = application.getTime("joinTime");
boolean representedByLegacy = periods.stream()
.filter(period -> period.legacy && Objects.equals(period.userId, userId))
.filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
.anyMatch(period -> period.exitTime == null || !applyDate.after(period.exitTime));
boolean representedByNewPeriod = periods.stream()
.filter(period -> !period.legacy && Objects.equals(period.userId, userId))
.anyMatch(period -> isMemberAt(period, applyDate));
if (representedByLegacy || representedByNewPeriod) {
return;
}
periods.add(new MembershipPeriod(
userId,
applyDate,
null,
true,
false,
application.getString("userState"),
application.getTime("retireDate")
));
}
private static class AnnualMemberPeriod {
final String userId;
final Date joinTime;
Date exitTime;
boolean rosterMember;
/**
* 退会申请优先关闭入会时间相同的会员周期;新系统复入会后再退会时关闭最近的有效周期。
*/
private void mergeExitApplication(List<MembershipPeriod> periods, NutMap application, Date applyDate) {
String userId = application.getString("userId");
Date applicationJoinTime = application.getTime("joinTime");
Date exitTime = application.getTime("exitTime") == null ? applyDate : application.getTime("exitTime");
MembershipPeriod target = periods.stream()
.filter(period -> Objects.equals(period.userId, userId))
.filter(period -> Objects.equals(period.joinTime, applicationJoinTime))
.filter(period -> period.exitTime == null)
.max(Comparator.comparing(period -> period.joinTime))
.orElse(null);
if (target == null) {
target = periods.stream()
.filter(period -> Objects.equals(period.userId, userId))
.filter(period -> period.exitTime == null && !period.joinTime.after(exitTime))
.max(Comparator.comparing(period -> period.joinTime))
.orElse(null);
}
if (target != null) {
target.exitTime = exitTime;
}
}
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<String> warnings = new java.util.LinkedHashSet<>();
final List<NutMap> 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<NutMap> 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<NutMap> getJgUser(String clubId) {
@@ -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<ClubCommonPage
private SysClubService sysClubService;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@Inject
private ClubUserJoinService clubUserJoinService;
@Inject
private ClubPayRecordService payRecordService;
@Inject
private SysUserService sysUserService;
@Inject
private SysRoleService sysRoleService;
public SysClubInfoManageServiceImpl(Dao dao) {
super(dao);
}
/**
* 直接退会不启动 WF;系统处理记录、会员关系删除和角色清理必须同时成功。
* @param membershipId 当前 club_user 会员关系ID
* @return void;重复退会或会员不存在时抛出异常,避免生成重复记录
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void exitClub(String membershipId) {
ClubUser member = dao().fetch(ClubUser.class, membershipId);
if (member == null) {
throw new IllegalArgumentException("该会员已退会或会员关系不存在");
}
ClubUserApply source = clubUserJoinService.buildExitApply(member.getClubId(), member.getUserId());
if (source == null) {
throw new IllegalArgumentException("无法获取该会员的退会信息");
}
// 使用新对象,只复制业务资料,防止沿用历史申请ID、创建人和创建时间。
ClubUserApply application = new ClubUserApply();
application.setClubId(member.getClubId());
application.setUserId(member.getUserId());
application.setRoleCode(source.getRoleCode());
application.setClubPosition(source.getClubPosition());
application.setEmail(source.getEmail());
application.setMobile(source.getMobile());
application.setBirthday(source.getBirthday());
application.setAvatar(source.getAvatar());
application.setSameTimeJoinOtherClubSituation(source.getSameTimeJoinOtherClubSituation());
application.setAwardsExperience(source.getAwardsExperience());
application.setSignature(source.getSignature());
application.setJoinTime(source.getJoinTime());
Date exitTime = new Date();
application.setMode(2);
application.setApplyDate(exitTime);
application.setExitTime(exitTime);
// 条件删除只能成功一次;并发重复操作不再新增退会记录,delFlag 沿用默认值。
if (dao().delete(ClubUser.class, membershipId) != 1) {
throw new IllegalArgumentException("该会员已退会,请刷新列表");
}
dao().insert(application);
// 2026年10月3日 09:51:00;操作人:wxg:系统直接退会与取消拨付处于同一事务,避免退会后仍保存可拨付。
payRecordService.disableExitMemberPayment(member.getClubId(), member.getUserId());
if (member.getRoleCode() != null) {
for (String roleCode : member.getRoleCode()) {
Sys_role role = sysRoleService.getByCode(roleCode);
if (role != null) {
dao().clear(Sys_user_role.class, Cnd.where("userId", "=", member.getUserId())
.and("roleId", "=", role.getId()).and("clubId", "=", member.getClubId()));
}
}
}
sysUserService.clearCache();
sysRoleService.clearCache();
}
@Override
public Pagination<ClubCommonPageVo> infoManagePageData(ClubUserPageForm pageForm) {
Cnd cnd = Cnd.NEW();
@@ -129,102 +205,188 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
return listPageVO(pageForm, sql, ClubCommonPageVo.class);
}
/**
* 2026年10月3日 12:01:28;操作人:wxg:当前会员以 club_user 分页,查询期间不补齐或修改数据库。
* @param pageForm clubId 为选中社团ID;radioType=4 查历史成员,其他值查当前会员;
* searchKeyword 匹配会员表姓名/工号,personType/userState 筛选当前用户资料;
* focusMembers=true 同时筛选当前人员类型为自聘人员或其他的会员,false/空值不限制;
* joinDateStart/joinDateEnd 为 yyyy-MM-dd,pageNumber/pageSize 为分页参数,排序只允许列表字段
* @return 会员分页;姓名、工号、电话、joinTime 来自会员表,其余人员资料只补充本页;历史页沿用申请记录
*/
@Override
public Pagination<ClubUserCommonPageVo> infoManageUserPageData(ClubUserPageForm pageForm) {
if (Integer.valueOf(4).equals(pageForm.getRadioType())) {
return historyMemberPageData(pageForm);
}
if (StrUtil.isBlank(pageForm.getClubId())) {
throw new IllegalArgumentException("请选择社团后查询会员信息");
}
// 姓名、工号、电话及入会时间使用持久化字段;列排序需要其他资料时才关联用户表。
Map<String, String> 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<ClubUserCommonPageVo> listPageVO = sysClubService.listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
List<ClubUserCommonPageVo> listMap = listPageVO.getList(ClubUserCommonPageVo.class);
for (ClubUserCommonPageVo vo : listMap) {
List<String> list = vo.getRoleCode();
List<String> cleanedCodes = list.stream()
.filter(Objects::nonNull)
.map(String::strip)
Pagination<ClubUserCommonPageVo> page = sysClubService.listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
List<ClubUserCommonPageVo> members = page.getList(ClubUserCommonPageVo.class);
fillMemberPageDetails(members, pageForm.getClubId());
for (ClubUserCommonPageVo member : members) {
List<String> roleCodes = member.getRoleCode() == null ? List.of() : member.getRoleCode();
List<String> 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<ClubUserCommonPageVo> members, String clubId) {
if (members.isEmpty()) return;
List<String> userIds = members.stream().map(ClubUser::getUserId).filter(Objects::nonNull).distinct().toList();
Map<String, NutMap> 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<ClubUserCommonPageVo> 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<String> 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<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
List<ClubUserCommonPageVo> 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<ClubUserCommonPageVo> exitManagePageData(ClubUserPageForm pageForm) {
Sql sql = Sqls.create("""
@@ -257,6 +419,13 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
""");
Cnd cnd = Cnd.NEW();
cnd.andEX("exitInfo.clubId", "=", pageForm.getClubId());
// 2026年10月3日 19:42:45;操作人:wxg:按实际退会时间筛选三类记录,结束日期使用次日零点上界,包含当天。
if (StrUtil.isNotBlank(pageForm.getExitDateStart())) {
cnd.and("exitInfo.exitTime", ">=", 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<ClubCommonPage
cnd.and("exitInfo.clubId", "in", clubList.stream().map(Sys_user_role::getClubId).toList());
}
cnd.groupBy("exitInfo.id");
cnd.desc("exitTime");
// 2026年10月3日 19:42:45;操作人:wxg:最新退会记录优先,时间相同时按ID保证分页稳定;空时间排在最后。
cnd.desc("exitInfo.exitTime");
cnd.asc("exitInfo.id");
sql.setCondition(cnd);
Pagination<ClubUserCommonPageVo> pagination = listPageVO(pageForm, sql, ClubUserCommonPageVo.class);
List<ClubUserCommonPageVo> list = pagination.getList(ClubUserCommonPageVo.class);
@@ -321,6 +492,8 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
/**
* 退会申请审批完成后会删除club_user成员关系,因此退会管理以退会申请记录为主数据源。
* 同时保留旧sys_club_user表中已退会的数据,避免历史直接移出记录无法查询。
* 2026年10月3日 19:42:45;操作人:wxg:mode=2 为系统处理,无需 WF;mode=0 通过 EXISTS 判断 WF 完成,避免一对多重复。
* 同一用户、社团存在以上申请时不再重复展示旧历史表记录;每条申请仍独立保留,列表与详情共用。
*
* @return 退会管理列表和详情共用的数据来源SQL
*/
@@ -343,11 +516,12 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
cua.applyDate,
cua.joinTime,
cua.exitTime,
'AUDIT_EXIT' AS exitType
CASE WHEN cua.mode = 2 THEN 'SYSTEM_EXIT' ELSE 'AUDIT_EXIT' END AS exitType
FROM
club_user_apply cua
INNER JOIN wf_process_instance ins ON ins.businessNo = cua.id AND ins.state = 20
WHERE cua.mode = false
WHERE cua.mode = 2 OR (cua.mode = 0 AND EXISTS (
SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = cua.id AND ins.state = 20
))
UNION ALL
SELECT
scu.id,
@@ -378,10 +552,11 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
AND NOT EXISTS (
SELECT 1
FROM club_user_apply cua
INNER JOIN wf_process_instance ins ON ins.businessNo = cua.id AND ins.state = 20
WHERE cua.mode = false
AND cua.clubId = scu.clubId
WHERE cua.clubId = scu.clubId
AND cua.userId = scu.userId
AND (cua.mode = 2 OR (cua.mode = 0 AND EXISTS (
SELECT 1 FROM wf_process_instance ins WHERE ins.businessNo = cua.id AND ins.state = 20
)))
)
) exitInfo
LEFT JOIN sys_club club ON club.id = exitInfo.clubId
@@ -431,6 +606,12 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
return result;
}
/**
* 按现有成员排序渲染 clubRegis Word 模板,模板读取为空时提前阻止解析及下载。
* @param clubId 当前选中社团ID,登记表包含该社团资料和成员信息
* @param response 写入生成的 DOCX 附件;模板或渲染失败时抛出中文业务异常
* @return void,文件内容通过响应输出,错误结果由统一异常处理返回
*/
@Override
public void exportRegistrationDoc(String clubId, HttpServletResponse response) {
Sql sql = Sqls.create("""
@@ -501,11 +682,22 @@ public class SysClubInfoManageServiceImpl extends BaseServiceImpl<ClubCommonPage
.bind("cys", policy)
.build();
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("clubRegis"), config).render(docData).writeAndClose(byteArrayOutputStream);
try (InputStream templateStream = sysOfficeTemplateUtil.getTemplate("clubRegis");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
// 文件存储读取失败会返回空内容,先校验再交给 POI,避免显示英文 EmptyFileException。
byte[] templateBytes = templateStream == null ? new byte[0] : templateStream.readAllBytes();
if (templateBytes.length == 0) {
throw new BaseException("社团登记表模板读取失败,请检查模板文件及文件存储服务。");
}
XWPFTemplate.compile(new ByteArrayInputStream(templateBytes), config).render(docData).writeAndClose(byteArrayOutputStream);
CommonDownloadUtil.download("教职工文体社团登记表(" + clubInfo.getString("clubName") + ").docx", byteArrayOutputStream.toByteArray(), response);
} catch (IOException e) {
log.error("社团登记表导出失败,id:{},错误信息:{}", clubId, e.getMessage());
} catch (BaseException e) {
// 保留模板配置及空文件校验的具体提示,同时记录完整异常以便排查。
log.error("社团登记表导出失败,id:{}", clubId, e);
throw e;
} catch (IOException | RuntimeException e) {
log.error("社团登记表导出失败,id:{}", clubId, e);
throw new BaseException("社团登记表生成失败,请检查模板是否为有效的 Word 文件或联系管理员。");
}
}
}
@@ -29,13 +29,17 @@ import com.budwk.app.zhgh.club.model.SysClubSponsor;
import com.budwk.app.zhgh.club.param.ClubUserPageForm;
import com.budwk.app.zhgh.club.service.SysClubService;
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.ClubRegisterPageVo;
import com.budwk.app.zhgh.club.vo.ClubRegisterVo;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
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.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@@ -60,10 +64,27 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
@Inject
private SysClubUserService sysClubUserService;
@Inject
private ClubPayRecordService payRecordService;
@Inject
private CommonService commonService;
@Inject
private BpmService bpmService;
@Override
public List<SysClub> 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<ClubCommonPageVo> getAllClubWithPage(PageForm pageForm, Cnd cnd) {
Sql sql = Sqls.create("""
@@ -77,10 +98,11 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> 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<SysClub> implements SysC
}
@Override
@Aop(TransAop.READ_COMMITTED)
public SysClub doAdd(SysClub club, List<NutMap> managePerson) {
club.setDismiss(false);
club.setUserId(SecurityUtil.getUserId());
@@ -178,6 +201,7 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> 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<SysClub> 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<SysClub> implements SysC
}
@Override
@Aop(TransAop.READ_COMMITTED)
public SysClub doEdit(SysClub club, List<String> deleteIds, List<NutMap> managePerson) {
// 修改社团表
dao().update(club);
@@ -219,6 +246,8 @@ public class SysClubServiceImpl extends BaseServiceImpl<SysClub> implements SysC
// 重新插入发起人
dao().insertLinks(club, "sponsors");
// 现有编辑流程会重建会员关系,删除前保存原入会时间,避免把编辑时刻算成再次入会。
List<ClubUser> 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<SysClub> 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<ClubUser> 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;
@@ -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<ClubUser> 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<ClubUser> members) {
if (members == null || members.isEmpty()) return;
if (members.stream().anyMatch(member -> member == null || StrUtil.isBlank(member.getUserId()))) {
throw new IllegalArgumentException("新增会员必须提供有效用户ID");
}
List<String> userIds = members.stream().map(ClubUser::getUserId).distinct().toList();
Map<String, Sys_user> 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<ClubUser> 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<NutMap> 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<ClubUser> 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,
@@ -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<JSONObject> replaceReport;
private List<JSONObject> rulesFile;
private List<JSONObject> afterRulesFile;
private List<JSONObject> 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;
@@ -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;
@@ -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<NutMap> clubList = baseService.listMap(sql);
List<OutlayManageClub> 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();
}
@@ -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);
@@ -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<NutMap> clubs = listMap(lockSql);
if (clubs.isEmpty()) return 0;
Set<String> 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<NutMap> clubList = listMap(sql);
Set<String> lockedClubs = clubs.stream().map(club -> club.getString("id")).collect(Collectors.toSet());
List<OutlayManageClub> 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) {
@@ -88,6 +88,8 @@ public class AidFundMemberPayServiceImpl extends BaseServiceImpl<AidFundMemberPa
}
if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
// 2026年10月3日 19:04:27;操作人:wxg:默认优先展示最新年度,同年度内保留单位编码倒序;主动指定排序时沿用原规则。
cnd.desc("pay.year");
cnd.desc("us.unitCode");
} else {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
@@ -4,9 +4,11 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -30,6 +32,7 @@ import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@@ -140,6 +143,7 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
*/
@Override
public Sql getManageSql(AidFundPageForm pageForm) {
// 返回当前人员类型,供人员类型变化名单与基金会员类型对照核实。
Sql sql = Sqls.create("""
SELECT
id,
@@ -150,6 +154,7 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
unitName,
unionName,
aidFundMemberUserType,
personType,
aidFundMemberJoinTime
FROM
vw_user
@@ -255,13 +260,10 @@ public class AidFundMemberServiceImpl extends BaseServiceImpl implements AidFund
@Override
@Aop(TransAop.READ_COMMITTED)
public void doBatchChangeTypeByAdmin(String[] ids, String changeType) {
List<Sys_user> sysUserList = dao().query(Sys_user.class, Cnd.where("id", "in", ids));
List<Sys_user> sysUserList = lockChangeUsers(ids);
validateRepeatedChange(sysUserList, changeType);
List<AidFundMemberChangeRecord> 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<Sys_user> 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<Sys_user> lockChangeUsers(String[] ids) {
List<Sys_user> 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<Sys_user> users, String changeType) {
List<String> 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<AidFundMemberChangeRecord> 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")) {
@@ -26,7 +26,7 @@ public class PendingConfirmUserBatchChangeParam {
private String userState;
/**
* 批量设置的退休时间,仅在目标在职状态为退休且传入日期时参与变更。
* 批量设置的退休时间;目标在职状态为退休或人员类型为离退休人员时必填,并统一应用于勾选人员。
*/
private Date retireDate;
@@ -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<String> userAttributes;
/** 在职状态。 */
private String userState;
@@ -150,6 +150,10 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> 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<Sys_user> 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<Sys_user> 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<String> userIds;
try {
userIds = JSONUtil.parseArray(changeParam.getUserIds()).toList(String.class);
@@ -229,8 +250,8 @@ public class PendingConfirmUserServiceImpl extends BaseServiceImpl<Sys_user> 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) {
@@ -696,10 +696,10 @@
<i class="fa fa-th-large"></i>
应用中心
</a>
<!-- <a href="/platform/v4/serv" data-pjax class="v4-nav-item">
<a href="/platform/v4/serv" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
流程中心
</a>-->
</a>
<a href="/platform/v4/todo" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
待办中心
@@ -47,11 +47,13 @@ layout("/layouts/platform.html"){
<el-option v-for="item in userStateOptions" :label="item" :value="item"></el-option>
</el-select>
</search-item>
<!-- 暂停用人方式筛选,保留查询控件供后续恢复。
<search-item label="用人方式">
<el-select v-model="pageForm.preparedBy" clearable filterable>
<el-option v-for="item in preparedByOptions" :label="item" :value="item"></el-option>
</el-select>
</search-item>
-->
<search-item label="人员类型">
<el-select v-model="pageForm.personType" clearable filterable>
<el-option v-for="item in personTypeOptions" :label="item" :value="item"></el-option>
@@ -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(() => {})
@@ -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) => {
@@ -124,7 +124,8 @@
}
</script>
<script nonce="${cspNonce!}">
const DISABLE_LOGIN_CAPTCHA = ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname)
// 临时屏蔽指定局域网 IP 的验证码;恢复时删除 192.168.0.108,并同步删除后端对应条件。
const DISABLE_LOGIN_CAPTCHA = ["localhost", "127.0.0.1", "::1", "192.168.0.108"].includes(window.location.hostname)
new Vue({
el: "#app",
@@ -5,7 +5,7 @@ const clubUserJoin = {
<div>
<div class="process-title">
申请信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
<el-link v-if="row.mode !== 2" type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions border>
<el-descriptions-item label="姓名">
@@ -112,7 +112,7 @@ layout("/layouts/platform.html"){
style="width: 100%"
>
<el-option
v-for="item in clubOptions"
v-for="item in sortedClubOptions"
:key="item.id"
:label="item.clubName"
:value="item.id"
@@ -380,9 +380,14 @@ layout("/layouts/platform.html"){
>
{{(lastClubData.year - 1) + '年度注册会员人数为' + lastClubData.history_member + ',其中拨付人数为' + lastClubData.history_y_givemoney_member}}
</div>
<el-table :data="formData.changeUserNum" max-height="300">
<!-- 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。 -->
<el-alert v-if="memberStatisticsWarnings.length"
:title="memberStatisticsWarnings.join(';')"
type="warning" :closable="false" show-icon></el-alert>
<el-table :data="formData.changeUserNum" max-height="300" v-loading="memberStatisticsLoading">
<el-table-column label="序号" type="index" width="50"></el-table-column>
<el-table-column label="人员类型" prop="userState"></el-table-column>
<!-- 2026年10月2日wxg屏蔽操作:<el-table-column label="人员类型" prop="userState"></el-table-column> -->
<el-table-column label="人员类型" prop="personType"></el-table-column>
<el-table-column label="年初人员" prop="yearFirstNum"></el-table-column>
<el-table-column label="年度增加" prop="yearAddNum"></el-table-column>
<el-table-column label="年度减少" prop="yearReduceNum"></el-table-column>
@@ -651,6 +656,11 @@ layout("/layouts/platform.html"){
memberList: [], // 成员名单
lastClubData: {}, // 上年度社团数据
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。统计加载与核查提示只服务于成员变化表。
memberStatisticsLoading: false,
memberStatisticsWarnings: [],
memberStatisticsSequence: 0,
// 核心表单数据
formData: {
year: new Date().getFullYear() + "", // 默认当前年度
@@ -678,6 +688,18 @@ layout("/layouts/platform.html"){
},
// 常量定义(便于维护和修改)
computed: {
// 仅对本页社团选项按编码升序展示;空编码放末尾,保留接口原始数组。
sortedClubOptions() {
return (this.clubOptions || []).slice().sort((left, right) => {
const leftCode = left.clubCode == null ? "" : String(left.clubCode);
const rightCode = right.clubCode == null ? "" : String(right.clubCode);
if (!leftCode && rightCode) return 1;
if (leftCode && !rightCode) return -1;
if (leftCode < rightCode) return -1;
if (leftCode > rightCode) return 1;
return 0;
});
},
// 收支类型选项
INCOME_TYPE_OPTIONS() {
return [
@@ -780,8 +802,9 @@ layout("/layouts/platform.html"){
async fetchClubList() {
const resp = await this.$axios.post("/platform/club/examine/apply/getClubsByRole");
this.clubOptions = resp.data || [];
if (this.clubOptions.length > 0) {
this.formData.clubId = this.clubOptions[0].id;
if (this.sortedClubOptions.length > 0) {
// 新增登记默认选中编码最小的社团;编辑回显仍使用原登记社团。
this.$set(this.formData, "clubId", this.sortedClubOptions[0].id);
await this.handleClubChange(this.formData.clubId);
}
},
@@ -833,17 +856,50 @@ layout("/layouts/platform.html"){
return [...sortedDefaults, ...customItems];
},
// 获取社团成员数量
async fetchClubUserNum(clubId) {
const resp = await this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
clubId,
year: this.formData.year
});
if (resp.code === 0) {
this.formData.changeUserNum = resp.data || [];
} else {
this.$message.warning(resp.msg);
// 2026年10月2日wxg屏蔽操作:原统计请求逐行注释保留。
// // 获取社团成员数量
// async fetchClubUserNum(clubId) {
// const resp = await this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
// clubId,
// year: this.formData.year
// });
// if (resp.code === 0) {
// this.formData.changeUserNum = resp.data || [];
// } else {
// this.$message.warning(resp.msg);
// }
// },
//
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。
fetchClubUserNum(clubId) {
const sequence = ++this.memberStatisticsSequence;
this.$set(this.formData, "changeUserNum", []);
this.memberStatisticsWarnings = [];
if (!clubId || !this.formData.year) {
this.memberStatisticsLoading = false;
return Promise.resolve();
}
this.memberStatisticsLoading = true;
return this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
clubId: clubId,
year: this.formData.year
}).then((resp) => {
// 切换年度或社团时只接收最近一次请求,避免旧条件的结果覆盖当前统计。
if (sequence !== this.memberStatisticsSequence) return;
if (resp.code === 0) {
const rows = resp.data || [];
this.$set(this.formData, "changeUserNum", rows);
this.memberStatisticsWarnings = rows.length ? rows[0].statisticsWarnings || [] : [];
} else {
this.memberStatisticsWarnings = [resp.msg || "成员变化统计获取失败,请核查"];
}
}).catch(() => {
if (sequence === this.memberStatisticsSequence) {
this.memberStatisticsWarnings = ["成员变化统计请求失败,请检查后重新选择社团或年度"];
}
}).finally(() => {
if (sequence === this.memberStatisticsSequence) this.memberStatisticsLoading = false;
});
},
// 获取成员名单
async fetchMemberList(clubId) {
@@ -1101,13 +1157,22 @@ layout("/layouts/platform.html"){
},
/************************ 事件处理方法 ************************/
// 年度变化处理
handleYearChange: function() {
// 2026年10月2日wxg屏蔽操作:保留原年度切换事件。
// // 年度变化处理
// handleYearChange: function() {
// this.checkIsRegistered();
// },
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。所选年度改变后同步刷新成员变化表。
handleYearChange() {
this.checkIsRegistered();
return this.fetchClubUserNum(this.formData.clubId);
},
// 社团变化处理
async handleClubChange(val) {
if (!val) return;
if (!val) {
this.fetchClubUserNum(val);
return;
}
const club = this.clubOptions.find(item => item.id === val);
// 更新社团基础信息
this.formData.due = club?.due || "";
@@ -25,6 +25,14 @@ layout("/layouts/platform.html"){
<search-item label="在职状态">
<dict-select code="USER_STATE" placeholder="请选择人员状态" clearable v-model="pageForm.userState"></dict-select>
</search-item>
<!-- 2026年10月3日 19:42:45;操作人:wxg:搜索按钮前提供退会日期范围,覆盖审核、系统处理及历史直接移出。 -->
<search-item label="退会时间查询">
<el-date-picker v-model="pageForm.exitDateRange" :picker-options="exitDatePickerOptions"
@change="exitDateRangeChange" type="datetimerange" clearable align="right"
start-placeholder="开始日期" end-placeholder="结束日期" range-separator="-"
format="yyyy-MM-dd" value-format="yyyy-MM-dd" style="width: 100%">
</el-date-picker>
</search-item>
</search>
</el-card>
@@ -76,7 +84,23 @@ layout("/layouts/platform.html"){
},
data() {
return {
clubList: []
clubList: [],
pageForm: { exitDateRange: [], exitDateStart: null, exitDateEnd: null },
// 2026年10月3日 19:42:45;操作人:wxg:复用通用查询和分页,快捷范围按自然月计算。
pageDataUrl: "/platform/club/infoManage/exitManage/pageData",
exitDatePickerOptions: {
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()])
} }
]
}
}
},
methods: {
@@ -85,28 +109,35 @@ layout("/layouts/platform.html"){
this.$refs.infoRef.onOpen(row)
})
},
async getClubList() {
const resp = await this.$axios.post("/platform/club/infoManage/exitManage/getClubList")
if (resp.code === 0) {
this.$set(this, "clubList", resp.data)
}
getClubList() {
return this.$axios.post("/platform/club/infoManage/exitManage/getClubList").then(resp => {
if (resp.code === 0) {
// 仅调整当前页面的社团选项顺序;编码为空时排在有编码的社团之后。
const clubs = (resp.data || []).slice().sort((left, right) => {
const leftCode = left.clubCode == null ? "" : String(left.clubCode)
const rightCode = right.clubCode == null ? "" : String(right.clubCode)
if (!leftCode && rightCode) return 1
if (leftCode && !rightCode) return -1
return leftCode < rightCode ? -1 : leftCode > rightCode ? 1 : 0
})
this.$set(this, "clubList", clubs)
}
})
},
exitTypeName(row) {
// 2026年10月3日 19:42:45;操作人:wxg:系统处理退会独立标识,保留原有两类名称。
if (row.exitType === "SYSTEM_EXIT") return "系统处理"
return row.exitType === "AUDIT_EXIT" ? "审核退会" : "直接移出"
},
async pageData() {
const resp = await this.$axios.post("/platform/club/infoManage/exitManage/pageData", this.pageForm)
if (resp.code === 0) {
this.$set(this, "tableData", resp.data.list)
this.$set(this.pageForm, "totalCount", resp.data.totalCount)
} else {
this.$message.warning(resp.msg)
}
/** 2026年10月3日 19:42:45;操作人:wxg:清空范围时同时清空接口参数,筛选后由 mixin 重置页码。 */
exitDateRangeChange(val) {
this.$set(this.pageForm, "exitDateStart", val && val.length === 2 ? val[0] : null)
this.$set(this.pageForm, "exitDateEnd", val && val.length === 2 ? val[1] : null)
this.doSearch()
}
},
async created() {
await this.getClubList()
await this.pageData()
created() {
this.getClubList().then(() => this.pageData())
}
})
</script>
@@ -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() {
@@ -1,4 +1,4 @@
const CLUB_INFO_MANAGE_TEMPLATE = {
const CLUB_INFO_MANAGE_TEMPLATE = {
template: `
<div class="club-manage-panel">
<el-card class="club-manage-query-card" shadow="never">
@@ -7,29 +7,44 @@
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
</search-item>
<search-item label="人员类型">
<dict-select code="USER_PERSON_TYPE" placeholder="请选择人员类型" clearable v-model="pageForm.personType"></dict-select>
<dict-select code="USER_PERSON_TYPE" placeholder="请选择人员类型" clearable v-model="pageForm.personType" :disabled="pageForm.focusMembers"></dict-select>
</search-item>
<search-item label="在职状态">
<dict-select clearable code="USER_STATE" placeholder="请选择人员状态" v-model="pageForm.userState"></dict-select>
</search-item>
<!-- 2026年10月3日 19:28:31;操作人:wxg:查询标签明确为入会时间,日期筛选逻辑保持不变。 -->
<search-item label="入会时间查询" class="club-join-date-query">
<el-date-picker v-model="pageForm.joinDateRange" :picker-options="joinDatePickerOptions"
@change="joinDateRangeChange" type="datetimerange" clearable align="right"
start-placeholder="开始日期" end-placeholder="结束日期" range-separator="-"
format="yyyy-MM-dd" value-format="yyyy-MM-dd" style="width: 100%">
</el-date-picker>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10 club-manage-list-card">
<table-tool label="会员信息">
<template slot="content">
<el-tag :effect="pageForm.focusMembers ? 'dark' : 'plain'" @click="toggleFocusMembers"
class="mr10" style="cursor: pointer">重点关注名单</el-tag>
<span class="text-danger">提醒:再次点击可切换</span>
</template>
<template v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])">
<el-button size="small" type="primary" icon="el-icon-download" @click="exportRegistrationDoc">导出登记表</el-button>
<el-button @click="openImport" size="small" type="primary">导入成员</el-button>
<el-button size="small" type="primary" @click="openUserAdd(true)">录入成员</el-button>
<el-button size="small" type="primary" @click="openUserAdd(false)">设置理事机构</el-button>
</template>
<el-radio-group @change="doSearch" style="margin-left: 10px" v-model="pageForm.radioType" size="small">
<el-radio-group @change="memberTabChange" style="margin-left: 10px" v-model="pageForm.radioType" size="small">
<el-radio-button label="3">全部</el-radio-button>
<el-radio-button label="2">社团成员</el-radio-button>
<!-- 修改时间:2026-10-03 12:47:38;操作人:wxg;会员页签统一使用“会员”称谓,保留原筛选值。 -->
<el-radio-button label="2">社团会员</el-radio-button>
<el-radio-button label="1">理事机构</el-radio-button>
<el-radio-button label="4">历史会员</el-radio-button>
</el-radio-group>
</table-tool>
<el-table v-loading="tableLoading" :data="tableData" :height="tableHeight"
<el-table :key="pageForm.radioType" v-loading="tableLoading" :data="tableData" :height="tableHeight"
row-key="id" ref="tableRef" @sort-change='pageOrder'>
<el-table-column :index="indexMethod" label="序号" type="index" width="70"></el-table-column>
<el-table-column label="工号" prop="loginName" show-overflow-tooltip sortable></el-table-column>
@@ -37,11 +52,15 @@
<el-table-column label="性别" prop="sex" show-overflow-tooltip sortable></el-table-column>
<el-table-column label="电话" prop="mobile" show-overflow-tooltip></el-table-column>
<el-table-column label="人员类型" prop="personType" show-overflow-tooltip sortable></el-table-column>
<!-- 展示用户当前人员属性,便于结合人员类型及在职状态核实会员信息。 -->
<el-table-column label="人员属性" prop="userAttribute" show-overflow-tooltip sortable></el-table-column>
<el-table-column label="在职状态" prop="userState" show-overflow-tooltip sortable></el-table-column>
<el-table-column label="所属单位" prop="unitName" sortable show-overflow-tooltip></el-table-column>
<el-table-column label="身份" prop="roleName" show-overflow-tooltip></el-table-column>
<el-table-column label="入会时间" prop="joinTime" show-overflow-tooltip sortable></el-table-column>
<el-table-column label="操作">
<!-- 历史成员仅供查询,退出时间取申请记录,不提供会员关系修改入口。 -->
<el-table-column v-if="pageForm.radioType === '4'" label="退出时间" prop="exitTime" show-overflow-tooltip sortable></el-table-column>
<el-table-column v-if="pageForm.radioType !== '4'" label="操作">
<template slot-scope="{row}">
<el-dropdown>
<el-button size="mini">
@@ -54,8 +73,11 @@
</el-dropdown-item>
<!--<el-dropdown-item @click.native="viewInfo(row)">查看注册信息</el-dropdown-item>
<el-dropdown-item @click.native="exportInfo(row)">导出登记表</el-dropdown-item>-->
<!-- 临时屏蔽删除入口,保留原调用以便后续恢复。
<el-dropdown-item @click.native="userDelete(row)">删除</el-dropdown-item>
<!--<el-dropdown-item @click.native="exitClub(row)">退会</el-dropdown-item>-->
-->
<!-- 管理端直接退会沿用现有接口,提交期间禁用入口以防重复操作。 -->
<el-dropdown-item :disabled="formLoading" @click.native="exitClub(row)">退会</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
@@ -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()
}
}
@@ -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"){
<div class="club-tree-scroll">
<el-tree
:data="treeData"
node-key="id"
ref="treeRef"
:expand-on-click-node="false"
:props="{children: 'children', label: 'clubName'}"
@@ -221,12 +232,24 @@ layout("/layouts/platform.html"){
this.currentTreeData = data
this.currentTreeNode = node
this.$nextTick(() => {
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) {
@@ -21,8 +21,12 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
</el-card>
<el-card shadow="never" class="mt10 club-manage-list-card">
<table-tool label="社团列表">
<template>
<template slot="content">
<!-- 一级节点展示社团总览时,引导用户从左侧树选择社团查看详细信息。 -->
<span v-if="parentNode.currentTreeNode && parentNode.currentTreeNode.level === 1"
style="display: block; text-align: center; color: #f56c6c;">
温馨提醒:选择左侧树中的某个社团,可查询该社团的详细信息。
</span>
</template>
</table-tool>
<el-table v-loading="tableLoading" :data="tableData" :height="tableHeight"
@@ -28,14 +28,14 @@ layout("/layouts/platform.html"){
@change="doSearch"
></el-date-picker>
</search-item>
<search-item label="社团名称" v-if="$auth.hasRoleOr(['SYSADMIN'])">
<search-item label="社团名称">
<el-select clearable filterable placeholder="请选择社团" @change="doSearch"
style="width: 100%;" v-model="pageForm.clubId">
<el-option :label="item.clubName" :value="item.id"
v-for="item in clubs"></el-option>
</el-select>
</search-item>
<search-item label="关键字">
<search-item label="姓名工号">
<el-input v-model="pageForm.searchKeyword" placeholder="请输入工号或者姓名查询"
clearable @keyup.enter="doSearch"></el-input>
</search-item>
@@ -61,8 +61,14 @@ layout("/layouts/platform.html"){
<el-button @click="batchOperatePay" size="small" type="primary">
批量设置
</el-button>
<!-- 2026年10月3日 14:33:18;操作人:wxg:筛选当前会员状态,按钮风格沿用历史拨付页面,首次进入默认在会。 -->
<el-radio-group v-model="pageForm.memberState" size="small" @change="memberStateChange" class="ml5">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="1">在会</el-radio-button>
<el-radio-button label="0">退会</el-radio-button>
</el-radio-group>
</table-tool>
<el-table :data="tableData" :height="tableHeight" row-key="id" @selection-change="handleSelectionChange" ref="tableRef">
<el-table :data="tableData" :height="tableHeight" v-loading="tableLoading" row-key="id" @selection-change="handleSelectionChange" ref="tableRef">
<el-table-column
:reserve-selection="true"
type="selection"
@@ -75,15 +81,16 @@ layout("/layouts/platform.html"){
<el-table-column label="所属单位" prop="unitName" sortable show-overflow-tooltip></el-table-column>
<el-table-column label="加入时间" prop="applyTime" sortable show-overflow-tooltip>
<template v-slot="{ row }">
{{ $moment(row.applyTime).format('YYYY-MM-DD HH:mm:ss') }}
{{ row.applyTime ? $moment(row.applyTime).format('YYYY-MM-DD') : '' }}
</template>
</el-table-column>
<el-table-column label="在职状态" prop="userState" sortable show-overflow-tooltip>
<!-- 离退休人员有退休日期时补充退休年份;其他类型或日期缺失时保留原值。 -->
<el-table-column label="人员类型" prop="personType" min-width="120" sortable show-overflow-tooltip>
<template v-slot="{ row }">
<span v-if="row.userState === '退休' && row.retireDate">
{{ row.userState + '(' + row.retireDate + ')' }}
</span>
<span v-else>{{ row.userState }}</span>
<span v-if="row.personType === '离退休人员' && row.retireDate">
{{ row.personType + '(' + $moment(row.retireDate).format('YYYY') + ')' }}
</span>
<span v-else>{{ row.personType }}</span>
</template>
</el-table-column>
<el-table-column label="当前状态" prop="count" sortable show-overflow-tooltip>
@@ -97,10 +104,12 @@ layout("/layouts/platform.html"){
<el-tooltip placement="bottom">
<div slot="content">
是否具备拨付资格,将按以下规则判定:<br/>
第一步:缴费记录必须是已缴费状态,否则直接判定无资格;<br/>
第二步:如果在职状态不是“退休”,则直接判定有领取资格;<br/>
第三步:如果是“退休”状态但未有退休日期信息,判定无领取资格;<br/>
第四步:如果是“退休”状态且填写了退休日期,当您的退休年份 ≥ 缴费记录年份(无缴费年份则按今年算)时,判定有资格,反之则无。
第一步:当前状态为“退会”,直接判定不拨付;<br/>
第二步:缴费记录必须是已缴费状态,否则判定不拨付;<br/>
第三步:如果人员类型不是“离退休人员”,则直接判定有领取资格;<br/>
第四步:如果人员类型为“离退休人员”但未有退休日期信息,判定无领取资格;<br/>
第五步:如果人员类型为“离退休人员”且填写了退休日期,当您的退休年份 ≥ 缴费记录年份(无缴费年份则按今年算)时,判定有资格,反之则无。<br/>
修改缴费状态时重新计算并保存;退会时保存为不拨付。列表读取已保存的拨付结果。
</div>
<span>拨付状态<i class="el-icon-question"></i></span>
</el-tooltip>
@@ -153,7 +162,7 @@ layout("/layouts/platform.html"){
<span class="dialog-footer" slot="footer">
<el-button @click="row = {}; payDialogVisible = false">取消</el-button>
<el-button @click="onBatchHandle" type="primary">确定</el-button>
<el-button @click="onBatchHandle" :loading="formLoading" type="primary">确定</el-button>
</span>
</el-dialog>
</div>
@@ -170,7 +179,10 @@ layout("/layouts/platform.html"){
return {
pageForm: {
payed: null,
// 2026年10月3日 14:33:18;操作人:wxg:列表默认锚点及首次请求均为“在会”。
memberState: '1',
year: new Date().getFullYear() + '',
clubId: null,
},
clubs: [],
row: {},
@@ -178,9 +190,16 @@ layout("/layouts/platform.html"){
payDialogVisible: false,
payed: false,
multipleSelection: [],
pageRequestSequence: 0,
}
},
methods: {
// 2026年10月3日 14:33:18;操作人:wxg:切换会员状态清空原勾选;复用 mixin 搜索方法回到第一页。
memberStateChange() {
this.$set(this, "multipleSelection", [])
if (this.$refs.tableRef) this.$refs.tableRef.clearSelection()
this.doSearch()
},
handleSelectionChange(val) {
this.multipleSelection = val
},
@@ -221,48 +240,64 @@ layout("/layouts/platform.html"){
}
this.operateRequest(data)
},
// 2026年10月3日 09:51:00;操作人:wxg:缴费设置会同步重算并保存拨付状态;列表刷新读取保存结果。
operateRequest(data) {
let message
if (this.payed === false) {
message = '您确定要设置为未缴费吗,此操作将影响该成员的拨付状态。'
message = '您确定要设置为未缴费吗?此操作将同步保存为不拨付。'
} else {
message = '您确定要设置为缴费吗?'
message = '您确定要设置为已缴费吗?此操作将按退会状态、人员类型及退休年份重新计算并保存拨付状态。'
}
this.$confirm(message, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const resp = await this.$axios.post("/platform/club/pay/handle", {
.then(() => {
this.formLoading = true
return this.$axios.post("/platform/club/pay/handle", {
ids: JSON.stringify(data),
payed: this.payed,
}).then(resp => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
this.payDialogVisible = false
this.multipleSelection = []
this.$refs.tableRef.clearSelection()
} else {
this.$message.error(resp.msg)
}
}).finally(() => {
this.formLoading = false
})
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
this.payDialogVisible = false
this.multipleSelection = []
this.$refs.tableRef.clearSelection()
} else {
this.$message.error(resp.msg)
}
})
.catch(() => {})
},
async pageData() {
const resp = await this.$axios.post("/platform/club/pay/pageData", this.pageForm)
if (resp.code === 0) {
this.tableData = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.warning(resp.msg)
}
// 每次只请求当前分页,连续切换条件时仅接收最后一次响应,避免旧页覆盖新结果。
pageData() {
const requestSequence = ++this.pageRequestSequence
this.tableLoading = true
return this.$axios.post("/platform/club/pay/pageData", this.pageForm).then(resp => {
if (requestSequence !== this.pageRequestSequence) return
if (resp.code === 0) {
this.tableData = resp.data.list || []
this.$set(this.pageForm, "totalCount", resp.data.totalCount || 0)
this.updateClubInfoListTableHeight()
} else {
this.$message.warning(resp.msg)
}
}).finally(() => {
if (requestSequence === this.pageRequestSequence) this.tableLoading = false
})
}
},
async created() {
this.clubs = await this.$businessTool.listClubByRole()
await this.pageData()
created() {
// 下拉选项与缴费列表共用后端授权范围,不复用其他业务的社团权限口径。
this.$axios.post("/platform/club/pay/listClubs").then(resp => {
if (resp.code === 0) this.clubs = resp.data || []
})
this.pageData()
}
})
</script>
@@ -56,7 +56,7 @@ layout("/layouts/platform.html"){
<el-table-column prop="clubName" label="申请社团" show-overflow-tooltip></el-table-column>
<el-table-column prop="mode" label="申请模式">
<template slot-scope="scope">
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
<el-tag size="mini" v-else type="danger">退出</el-tag>
</template>
</el-table-column>
@@ -90,7 +90,7 @@ layout("/layouts/platform.html"){
{{formData.taskName}}
</div>
<el-form :model="formData" ref="formRef" label-width="80px" :rules="formRules" label-suffix="">
<el-form-item v-if="formData.mode === true" label="缴费状态" prop="payed"
<el-form-item v-if="formData.mode === 1" label="缴费状态" prop="payed"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<span class="form-item-tooltip">
(说明:请确认{{ formData.userName }}当年是否缴费)
@@ -28,8 +28,10 @@ layout("/layouts/platform.html"){
<el-table-column prop="clubName" label="社团名称"></el-table-column>
<el-table-column prop="mode" label="申请模式">
<template v-slot="scope">
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
<el-tag size="mini" v-else type="danger">退出</el-tag>
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
<el-tag size="mini" v-else-if="scope.row.mode === 0" type="danger">退出</el-tag>
<!-- 系统直接退会以 mode=2 标识,不属于待提交的 WF 申请。 -->
<el-tag size="mini" v-else-if="scope.row.mode === 2" type="info">系统处理</el-tag>
</template>
</el-table-column>
<el-table-column prop="applyDate" label="申请时间"></el-table-column>
@@ -42,10 +44,10 @@ layout("/layouts/platform.html"){
<el-table-column label="操作" width="300">
<template v-slot="{row}">
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === true" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === false" @click="exitSubmitAgain(row)" size="mini" type="primary">提交</el-button>
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 1" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 0" @click="exitSubmitAgain(row)" size="mini" type="primary">提交</el-button>
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="onRevoke(row)">撤回</el-button>
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onDelete(row.id)" size="mini" type="danger">
<el-button v-if="row.mode !== 2 && (row.taskKey === 'startTask' || !row.instanceId)" @click="onDelete(row.id)" size="mini" type="danger">
删除
</el-button>
</template>
@@ -49,7 +49,7 @@ layout("/layouts/platform.html"){
<el-table-column prop="clubName" label="申请社团" show-overflow-tooltip></el-table-column>
<el-table-column prop="mode" label="申请模式">
<template slot-scope="scope">
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
<el-tag size="mini" v-else type="danger">退出</el-tag>
</template>
</el-table-column>
@@ -100,15 +100,18 @@ layout("/layouts/platform.html"){
</el-dropdown>
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder"
<!-- 仅名单模式切换时重建表格,清除动态列切换后残留的单元格渲染及 tooltip 宽度。 -->
<el-table :key="pageForm.personTypeChanged ? 'member-type-changed' : 'member-all'"
:data="tableData" :size="tableSize" @sort-change="pageOrder"
ref="table" row-key="id" style="width: 100%" v-loading="tableLoading">
<el-table-column
key="member-selection"
type="selection"
width="55">
</el-table-column>
<el-table-column :index="indexMethod" header-align="center" label="序号"
<el-table-column key="member-index" :index="indexMethod" header-align="center" label="序号"
type="index" width="60px"></el-table-column>
<el-table-column
:label="column.label"
@@ -119,7 +122,7 @@ layout("/layouts/platform.html"){
header-align="center"
min-width="100px"
show-overflow-tooltip
v-for="column in tableColumns"
v-for="column in visibleTableColumns"
>
<template v-slot="{row}" v-if="column.prop=='aidFundMemberUserType'">
<dict-tag :options="dict.type.AIDFUND_MEMBER_USER_TYPE"
@@ -132,7 +135,8 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300">
<!-- 操作列使用独立标识,避免动态增减人员类型列时复用其他列的渲染状态。 -->
<el-table-column key="member-actions" label="操作" fixed="right" width="300">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onEditMemberType(row)" size="mini" type="primary">修改基金会员类型
@@ -212,7 +216,7 @@ layout("/layouts/platform.html"){
<span class="dialog-footer" slot="footer">
<el-button @click="batchChangeTypeVisible = false">取 消</el-button>
<el-button @click="doBatchChangeType" type="primary">提 交</el-button>
<el-button @click="doBatchChangeType" :loading="formLoading" :disabled="formLoading" type="primary">提 交</el-button>
</span>
</el-dialog>
@@ -243,7 +247,7 @@ layout("/layouts/platform.html"){
</el-form>
<span class="dialog-footer" slot="footer">
<el-button @click="changeTypeVisible = false">取 消</el-button>
<el-button @click="doEditChangeType" type="primary">提 交</el-button>
<el-button @click="doEditChangeType" :loading="formLoading" :disabled="formLoading" type="primary">提 交</el-button>
</span>
</el-dialog>
@@ -274,6 +278,7 @@ layout("/layouts/platform.html"){
{prop: "unitName", label: "所属单位"},
{prop: "unionName", label: "所属工会"},
{prop: "aidFundMemberUserType", label: "基金会员类型"},
{prop: "personType", label: "人员类型"},
{prop: "aidFundMemberJoinTime", label: "加入时间"},
],
showApprovalForm: false,
@@ -298,11 +303,29 @@ layout("/layouts/platform.html"){
changeTableData: []
}
},
computed: {
// 仅在筛选人员类型变化名单时展示当前人员类型,取消筛选后恢复原有列表列。
visibleTableColumns() {
return this.tableColumns.filter(column => column.prop !== "personType" || this.pageForm.personTypeChanged)
},
},
watch: {
// 动态列完成渲染后再计算宽度,使右侧固定操作列与表头、表体保持一致。
visibleTableColumns() {
this.$nextTick(() => {
if (this.$refs.table) {
this.$refs.table.doLayout()
}
})
},
},
components: {
'info': AID_FUND_INFO,
},
methods: {
doBatchChangeType() {
// 提交完成前禁止重复点击,后端仍会校验历史重复记录。
if (this.formLoading) return
const selection = this.$refs.table.selection
if (selection.length === 0) {
this.$message.warning('请勾选需要变更的人员')
@@ -318,8 +341,10 @@ layout("/layouts/platform.html"){
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
if (this.formLoading) return
this.formLoading = true
const ids = selection.map(v => v.id)
this.$axios.post('/platform/medicalMutualAid/aidFund/manage/doBatchChangeType', {
return this.$axios.post('/platform/medicalMutualAid/aidFund/manage/doBatchChangeType', {
ids: JSON.stringify(ids),
changeType: this.formData.changeType
}).then(resp => {
@@ -328,9 +353,17 @@ layout("/layouts/platform.html"){
this.doSearch()
this.$refs.table.clearSelection()
this.batchChangeTypeVisible = false
} else {
// 重复校验失败时保留弹框及名单,方便操作员调整后重新提交。
this.$alert(resp.msg, '变更提醒', {type: 'warning'})
}
})
})
}).catch(error => {
// 公共 axios 拦截器会 reject 业务错误,此处保留完整重复名单供操作员核对。
if (error && error.msg) this.$alert(error.msg, '变更提醒', {type: 'warning'})
}).finally(() => {
this.formLoading = false
}).catch(() => {})
}).catch(() => {})
})
@@ -358,12 +391,16 @@ layout("/layouts/platform.html"){
this.changeTypeVisible = true
},
doEditChangeType() {
// 提交完成前禁止重复点击,后端仍会校验历史重复记录。
if (this.formLoading) return
this.$confirm('确定要变更【' + this.formData.username + '】吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post(loc() + "/doEditChangeType", {
if (this.formLoading) return
this.formLoading = true
return this.$axios.post(loc() + "/doEditChangeType", {
userId: this.formData.id,
changeType: this.formData.changeType
}).then(resp => {
@@ -371,9 +408,17 @@ layout("/layouts/platform.html"){
this.$message.success(resp.msg)
this.doSearch()
this.changeTypeVisible = false
} else {
// 保留当前选择并展示后端返回的重复变更人员及上次操作时间。
this.$alert(resp.msg, '变更提醒', {type: 'warning'})
}
})
})
}).catch(error => {
// 公共 axios 拦截器会 reject 业务错误,此处保留完整重复名单供操作员核对。
if (error && error.msg) this.$alert(error.msg, '变更提醒', {type: 'warning'})
}).finally(() => {
this.formLoading = false
}).catch(() => {})
}).catch(() => {})
},
doEditAidFundMemberUserType() {
this.$confirm('确定要修改【' + this.formData.username + '】的基金会员类型吗?', '提示', {
@@ -31,6 +31,10 @@ layout("/layouts/platform.html"){
<dict-select v-model="pageForm.personType" placeholder="请选择人员类型" @change="doSearch"
code="USER_PERSON_TYPE"></dict-select>
</search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
<search-item label="变更类型">
<el-select v-model="pageForm.changeType" placeholder="变更类型" @change="doSearch"
clearable filterable style="width: 100%">
@@ -42,10 +46,6 @@ layout("/layouts/platform.html"){
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>-->
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
</search>
</el-card>
@@ -43,7 +43,8 @@
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>-->
<search-item label="人员属性">
<!-- 数据管理页使用独立标签筛选;其他页面继续显示人员属性下拉框。 -->
<search-item label="人员属性" v-if="!query_disable">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
@@ -119,6 +119,29 @@ layout("/layouts/platform.html"){
</el-link>
</div>
</div>
<!-- 人员属性按字典编码多选,选中条件同时用于列表查询和批量设置会员。 -->
<div class="search-other mt20">
<div class="search-other-label">人员属性</div>
<div>
<el-tag
v-for="item in dict.type.USER_ATTRIBUTE"
:key="item.code"
:type="item.name"
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
@click="tagClick('userAttributes',item.code)">
{{ item.name }}
</el-tag>
<el-link type="danger"
v-if="dict.type.USER_ATTRIBUTE?.length && pageForm.userAttributes.length"
:underline="false"
@click="this.$set(pageForm, 'userAttributes', []); doSearch()">清空
</el-link>
<el-link :underline="false"
@click="this.$set(pageForm, 'userAttributes', (dict.type.USER_ATTRIBUTE || []).map(p=>p.code)); doSearch()"
type="success">全部
</el-link>
</div>
</div>
</el-card>
@@ -195,13 +218,14 @@ layout("/layouts/platform.html"){
'member-info': MEMBER_INFO,
'common-query': COMMON_QUERY,
},
dicts: ['USER_PERSON_TYPE', 'USER_STATE'],
dicts: ['USER_PERSON_TYPE', 'USER_STATE', 'USER_ATTRIBUTE'],
data() {
return {
visible: false,
pageForm: {
personTypes: [],
userStates: [],
userAttributes: [],
},
tableColumns: [
{ prop: "loginName", label: "工号", sortable: true },
@@ -246,6 +270,8 @@ layout("/layouts/platform.html"){
const pageForm = clone(this.pageForm)
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.userStates = JSON.stringify(pageForm.userStates)
// 人员属性标签保存编码数组,提交时按后端多选参数格式转换为 JSON 字符串。
pageForm.userAttributes = JSON.stringify(pageForm.userAttributes)
this.$axios.post("/platform/member/info/dataManage/setMemberByCnd", {
"pageForm": JSON.stringify(pageForm),
"append": append
@@ -262,6 +288,8 @@ layout("/layouts/platform.html"){
const pageForm = clone(this.pageForm)
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.userStates = JSON.stringify(pageForm.userStates)
// 人员属性标签保存编码数组,提交时按后端多选参数格式转换为 JSON 字符串。
pageForm.userAttributes = JSON.stringify(pageForm.userAttributes)
pageForm.append = append
this.$axios.post("/platform/member/info/dataManage/setWelfareMemberByCnd", {
"pageForm": JSON.stringify(pageForm),
@@ -291,13 +319,14 @@ layout("/layouts/platform.html"){
},
search(pageForm) {
if (pageForm) {
// 当前页的在职状态、人员类型使用标签数组筛选,公共查询组件回传的是字符串,
// 这里保留当前页数组,避免选择人员属性后把标签筛选状态覆盖掉。
// 公共查询组件回传 JSON 字符串;保留本页三组标签数组,避免顶部搜索覆盖选中条件。
const currentUserStates = this.pageForm.userStates
const currentPersonTypes = this.pageForm.personTypes
const currentUserAttributes = this.pageForm.userAttributes
this.pageForm = {...this.pageForm, ...pageForm}
this.$set(this.pageForm, "userStates", currentUserStates)
this.$set(this.pageForm, "personTypes", currentPersonTypes)
this.$set(this.pageForm, "userAttributes", currentUserAttributes)
}
this.doSearch()
},
@@ -305,12 +334,10 @@ layout("/layouts/platform.html"){
const pageForm = clone(this.pageForm)
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.userStates = JSON.stringify(pageForm.userStates)
this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
// 人员属性标签保存编码数组,提交时按后端多选参数格式转换为 JSON 字符串。
pageForm.userAttributes = JSON.stringify(pageForm.userAttributes)
// 仅转换本页标签参数,查询、分页结果和 loading 处理复用公共 mixin。
return initTableMixins.methods.pageData.call(this, pageForm)
},
},
created() {
@@ -108,6 +108,10 @@ layout("/layouts/platform.html"){
<dict-select @change="doSearch" code="USER_PERSON_TYPE" placeholder="请选择人员类型"
v-model="pageForm.personType"></dict-select>
</search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
<search-item label="在职状态">
<dict-select @change="doSearch" code="USER_STATE" placeholder="请选择在职状态"
v-model="pageForm.userState"></dict-select>
@@ -118,7 +122,7 @@ layout("/layouts/platform.html"){
<el-card class="mt10 pending-confirm-table-card" shadow="never">
<table-tool label="待确认人员记录">
<template slot="content">
<span class="pending-confirm-tip">温馨提示:以下人员不存在信息中心数据中台端,而智慧工会端还继续存在的,这类人员可能离职、退休、去世。</span>
<span class="pending-confirm-tip">温馨提示:以下人员不存在信息中心数据中台端,而智慧工会端还继续存在的,这类人员可能离职、退休、去世。【劳动服务公司】的人员类型,不在此变更,在会员高级管理中变更。</span>
</template>
<div class="pending-list-toolbar-actions">
<el-button class="pending-batch-change-button" type="primary"
@@ -196,6 +200,7 @@ layout("/layouts/platform.html"){
ref="batchChangeFormRef" label-width="100px">
<el-form-item label="人员类型">
<el-select v-model="batchChangeForm.personType" clearable filterable
@change="handleBatchPersonTypeChange"
placeholder="请选择人员类型" style="width: 100%">
<el-option v-for="item in pendingPersonTypeOptions" :key="item.code"
:label="item.name" :value="item.code"></el-option>
@@ -207,7 +212,7 @@ layout("/layouts/platform.html"){
placeholder="请选择在职状态" style="width: 100%"></dict-select>
</el-form-item>
<el-form-item label="退休时间" prop="retireDate"
v-if="batchChangeForm.userState === '退休'">
v-if="batchShowRetireDate" required>
<el-date-picker v-model="batchChangeForm.retireDate" type="date"
placeholder="请选择退休时间"
format="yyyy-MM-dd" value-format="yyyy-MM-dd"
@@ -261,6 +266,7 @@ layout("/layouts/platform.html"){
unionId: "",
unitId: "",
personType: "",
userAttributes: [],
userState: "",
pendingConfirmStatus: 1
},
@@ -278,16 +284,42 @@ layout("/layouts/platform.html"){
member: null,
welfareMember: null
},
batchChangeRules: {},
batchChangeRules: {
retireDate: [{
// 批量目标为退休状态或离退休人员时,必须为全部勾选人员指定退休时间。
validator: (rule, value, callback) => {
if (this.batchShowRetireDate && !value) {
callback(new Error("请选择退休时间"))
return
}
callback()
},
trigger: ["change", "blur"]
}]
},
pendingTableHeight: 300,
pendingTableResizeObserver: null
}
},
computed: {
/** 按字典编码识别离退休人员,统一批量退休时间的展示、校验和提交条件。 */
batchShowRetireDate() {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
return this.batchChangeForm.userState === "退休"
|| (!!retiredOption && this.batchChangeForm.personType === retiredOption.code)
}
},
methods: {
/** 列表与导出均提交人员属性编码的 JSON 数组,保留原表单数组供多选组件回显。 */
buildQueryPageForm(source = this.pageForm) {
const pageForm = clone(source)
pageForm.userAttributes = JSON.stringify(source.userAttributes || [])
return pageForm
},
/** 分页查询待确认或已确认人员。 */
pageData() {
this.selectedPendingUsers = []
return $.post("/platform/sourcechange/pending/pageData", this.pageForm).then((resp) => {
return $.post("/platform/sourcechange/pending/pageData", this.buildQueryPageForm()).then((resp) => {
if (resp.code === 0) {
this.tableData = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
@@ -344,7 +376,7 @@ layout("/layouts/platform.html"){
},
/** 按当前列表筛选条件和排序导出全部匹配人员,不受当前分页大小限制。 */
exportExcel() {
this.$downLoad("/platform/sourcechange/pending/exportExcel", this.pageForm)
this.$downLoad("/platform/sourcechange/pending/exportExcel", this.buildQueryPageForm())
},
/** 保存当前页勾选的待确认人员,供批量变更使用。 */
handleSelectionChange(selection) {
@@ -367,7 +399,14 @@ layout("/layouts/platform.html"){
|| this.batchChangeForm.member !== null && this.batchChangeForm.member !== undefined && this.batchChangeForm.member !== ""
|| this.batchChangeForm.welfareMember !== null && this.batchChangeForm.welfareMember !== undefined && this.batchChangeForm.welfareMember !== ""
},
/** 退休状态自动匹配人员类型;非退休状态清理隐藏日期,避免沿用上次结果。 */
/** 选择离退休人员时同步设置退休状态;选择其他类型不推断或恢复在职状态。 */
handleBatchPersonTypeChange(personType) {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
if (retiredOption && personType === retiredOption.code) {
this.$set(this.batchChangeForm, "userState", "退休")
}
},
/** 退休状态自动匹配人员类型;切换状态保留退休时间,仅清理隐藏的福利截止时间。 */
handleBatchUserStateChange(userState) {
if (userState === "退休") {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
@@ -376,7 +415,6 @@ layout("/layouts/platform.html"){
}
return
}
this.$set(this.batchChangeForm, "retireDate", "")
this.$set(this.batchChangeForm, "welfareStopDate", "")
},
/** 二次确认批量操作,防止误改多名人员。 */
@@ -409,7 +447,8 @@ layout("/layouts/platform.html"){
if (this.batchChangeForm.userState) {
params.userState = this.batchChangeForm.userState
}
if (this.batchChangeForm.userState === "退休" && this.batchChangeForm.retireDate) {
// 退休时间仅在退休状态或离退休人员类型下提交,隐藏时保留表单值但不参与变更。
if (this.batchShowRetireDate && this.batchChangeForm.retireDate) {
params.retireDate = this.batchChangeForm.retireDate
}
if (this.batchChangeForm.userState === "退休" && this.batchChangeForm.welfareStopDate) {
@@ -112,6 +112,7 @@
<el-input v-if="pendingChangeReadonly" v-model="pendingChangeForm.personType"
readonly></el-input>
<el-select v-else v-model="pendingChangeForm.personType" filterable
@change="handlePendingPersonTypeChange"
placeholder="请选择人员类型" style="width: 100%">
<el-option v-for="item in pendingPersonTypeOptions" :key="item.code"
:label="item.name" :value="item.code"></el-option>
@@ -126,15 +127,18 @@
</el-form-item>
</el-descriptions-item>
<template v-if="pendingChangeForm.userState === '退休'">
<!-- 退休日期满足退休状态或离退休人员类型任一条件即显示并要求填写。 -->
<template v-if="pendingShowRetireDate">
<el-descriptions-item label="退休日期">
<el-form-item prop="retireDate">
<el-form-item prop="retireDate" :required="!pendingChangeReadonly">
<el-date-picker v-model="pendingChangeForm.retireDate" type="date"
:disabled="pendingAllowFields('retireDate')"
placeholder="请选择退休日期" format="yyyy-MM-dd"
value-format="yyyy-MM-dd" style="width: 100%"></el-date-picker>
</el-form-item>
</el-descriptions-item>
</template>
<template v-if="pendingChangeForm.userState === '退休'">
<el-descriptions-item label="福利享受截止日期">
<el-form-item prop="welfareStopDate">
<el-date-picker v-model="pendingChangeForm.welfareStopDate" type="date"
@@ -11,7 +11,18 @@ const PENDING_MEMBER_CHANGE = {
},
pendingChangeRules: {
username: [{required: false, message: "必填", trigger: ["change", "blur"]}],
sex: [{required: false, message: "必填", trigger: ["change", "blur"]}]
sex: [{required: false, message: "必填", trigger: ["change", "blur"]}],
retireDate: [{
// 退休状态或离退休人员类型需要填写退休日期,其他人员不受此校验限制。
validator: (rule, value, callback) => {
if (this.pendingShowRetireDate && !value) {
callback(new Error("请选择退休日期"))
return
}
callback()
},
trigger: ["change", "blur"]
}]
},
pendingPersonTypeOptions: [],
pendingThreeUnits: [],
@@ -20,7 +31,22 @@ const PENDING_MEMBER_CHANGE = {
pendingChangeLoadingUserId: ""
}
},
computed: {
/** 满足退休状态或离退休人员类型任一条件时展示退休日期;人员类型按字典编码识别。 */
pendingShowRetireDate() {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
return this.pendingChangeForm.userState === "退休"
|| (!!retiredOption && this.pendingChangeForm.personType === retiredOption.code)
}
},
methods: {
/** 手动选择离退休人员时同步设置退休状态;选择其他类型时保留当前状态与日期。 */
handlePendingPersonTypeChange(personType) {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
if (retiredOption && personType === retiredOption.code) {
this.$set(this.pendingChangeForm, "userState", "退休")
}
},
/** 编辑模式仅允许修改待确认业务确认所需字段;查看模式下所有字段均为只读。 */
pendingAllowFields(prop) {
const editableFields = ["personType", "userState", "userAttribute", "member", "welfareMember",
@@ -97,7 +97,7 @@ layout("/layouts/platform_h5.html"){
<template v-slot="{row}">
<table-column label="社团名称">{{ row.clubName || '--' }}</table-column>
<table-column label="所属工会">{{ row.unionName || '--' }}</table-column>
<table-column label="申请类型">{{ row.mode ? '加入社团' : '退出社团' }}</table-column>
<table-column label="申请类型">{{ row.mode === 1 ? '加入社团' : '退出社团' }}</table-column>
<table-column label="申请时间">{{ row.applyDate || '--' }}</table-column>
<table-column label="所属单位">{{ row.unitName || '--' }}</table-column>
<table-column label="当前节点">{{ row.curTaskName || '--' }}</table-column>
@@ -119,7 +119,7 @@ layout("/layouts/platform_h5.html"){
<section class="club-audit-card">
<h2 class="club-audit-card__title">{{ formData.taskName || '社团审核' }}</h2>
<van-form ref="formRef">
<van-field v-if="formData.mode === true" v-model="formData.tf_payed" class="club-audit-payment-field" name="tf_payed" label="缴费状态" :rules="[{ required: true, message: '请选择缴费状态' }]" required>
<van-field v-if="formData.mode === 1" v-model="formData.tf_payed" class="club-audit-payment-field" name="tf_payed" label="缴费状态" :rules="[{ required: true, message: '请选择缴费状态' }]" required>
<template #input>
<van-radio-group v-model="formData.tf_payed" direction="horizontal"><van-radio name="0">未缴费</van-radio><van-radio name="1">已缴费</van-radio></van-radio-group>
</template>
@@ -67,10 +67,10 @@ layout("/layouts/platform_h5.html"){
<div v-if="row" class="club-card-header"><div class="club-card-header__heading"><div class="club-card-header__title">{{ row.userName || '--' }}</div><div class="club-card-header__sub">工号:{{ row.loginName || '--' }}</div></div><enum-tag class="club-card-header__status" v-if="row.instanceState" :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag></div>
</template>
<template v-slot="{row}">
<template v-if="row"><table-column label="社团名称">{{ row.clubName || '--' }}</table-column><table-column label="申请类型">{{ row.mode ? '加入社团' : '退出社团' }}</table-column><table-column label="申请时间">{{ row.applyDate || '--' }}</table-column><table-column label="当前节点">{{ row.taskName || '--' }}</table-column></template>
<template v-if="row"><table-column label="社团名称">{{ row.clubName || '--' }}</table-column><table-column label="申请类型"><van-tag v-if="row.mode === 2" type="primary">系统处理</van-tag><span v-else>{{ row.mode === 1 ? '加入社团' : '退出社团' }}</span></table-column><table-column label="申请时间">{{ row.applyDate || '--' }}</table-column><table-column label="当前节点">{{ row.taskName || '--' }}</table-column></template>
</template>
<template #actions="{row}">
<template v-if="row"><div class="action-btn" @click="onView(row)"><van-icon name="eye-o"></van-icon><span>查看</span></div><div class="action-btn" @click="onEdit(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === true"><van-icon name="edit"></van-icon><span>编辑</span></div><div class="action-btn" @click="exitSubmitAgain(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === false"><van-icon name="passed"></van-icon><span>提交</span></div><div class="action-btn delete" @click="onRevoke(row)" v-if="row.canRevoke"><van-icon name="revoke"></van-icon><span>撤回</span></div><div class="action-btn delete" @click="onDelete(row)" v-if="row.taskKey === 'startTask' || !row.instanceId"><van-icon name="delete-o"></van-icon><span>删除</span></div></template>
<template v-if="row"><div class="action-btn" @click="onView(row)"><van-icon name="eye-o"></van-icon><span>查看</span></div><div class="action-btn" @click="onEdit(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 1"><van-icon name="edit"></van-icon><span>编辑</span></div><div class="action-btn" @click="exitSubmitAgain(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 0"><van-icon name="passed"></van-icon><span>提交</span></div><div class="action-btn delete" @click="onRevoke(row)" v-if="row.canRevoke"><van-icon name="revoke"></van-icon><span>撤回</span></div><div class="action-btn delete" @click="onDelete(row)" v-if="row.mode !== 2 && (row.taskKey === 'startTask' || !row.instanceId)"><van-icon name="delete-o"></van-icon><span>删除</span></div></template>
</template>
<template #empty>
<div v-if="listLoading" class="club-list-skeleton"><div v-for="item in 2" :key="item" class="club-skeleton-card"><div class="club-skeleton-line club-skeleton-line--title"></div><div class="club-skeleton-line"></div><div class="club-skeleton-line club-skeleton-line--short"></div></div></div>
@@ -34,7 +34,7 @@ layout("/layouts/platform_h5.html"){
<template v-slot="{index,row}">
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="工号">{{row.loginName}}</table-column>
<table-column label="申请模式">{{row.mode ? '加入' : '退出'}}</table-column>
<table-column label="申请模式">{{row.mode === 1 ? '加入' : '退出'}}</table-column>
<table-column label="申请时间">{{row.applyDate}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
</template>