This commit is contained in:
2026-08-20 10:55:04 +08:00
parent c6609dcb0b
commit 34fe53caf9
14 changed files with 219 additions and 582 deletions
@@ -51,11 +51,6 @@ public class Sys_user_history extends Sys_user {
@Comment("变更原因")
private String changeReason;
@Column
@ColDefine(type = ColType.VARCHAR, customType = "text")
@Comment("备注")
private String remark;
@Column
@Comment("会员变更表记录id")
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -22,11 +22,12 @@ import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import com.budwk.app.zhgh.staffmanage.sourcechange.service.PendingUserHistoryService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -51,6 +52,10 @@ import java.util.stream.Collectors;
@IocBean
public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService {
private static final int BATCH_SIZE = 500;
/** 不需要人工确认。 */
private static final int PENDING_CONFIRM_STATUS_NONE = 0;
/** 数据源缺失,等待人工确认。 */
private static final int PENDING_CONFIRM_STATUS_PENDING = 1;
@Inject
private Dao dao;
@@ -62,8 +67,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
private SysUserService sysUserService;
@Inject
private ThreadPoolTaskExecutor executorService;
@Inject
private PendingUserHistoryService pendingUserHistoryService;
private static Map<String, String> unitIdNameMap;
@@ -177,7 +180,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
*/
u.setAidFundMember(null);
u.setPersonType(null);
needDoUpdateList.add(u);
}
@@ -292,12 +295,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
return "更新失败: " + e.getMessage();
}
/*
* 用户及普通变更记录完成后,再以本次完整源数据校正待确认人员。
* 该步骤仅存在于 ALL 实现中,增量更新不会误判未包含在增量数据中的系统用户。
*/
int pendingConfirmCount = pendingUserHistoryService.reconcile(sysUsers, sources);
/* // 4. 异步添加会员角色 - 不等待完成
if (Lang.isNotEmpty(addMemberUserIds)) {
executorService.execute(() -> {
@@ -355,14 +352,63 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
}
/*
* 所有异步用户更新完成后再校正待确认状态,避免主事务提前锁定 sys_user,
* 导致异步更新线程等待主事务释放行锁而形成相互等待。
*/
reconcilePendingConfirmStatus(updateParam.getPullTime());
long endTime = System.currentTimeMillis();
log.info("全量更新用户数据完成,耗时: {} 毫秒", (endTime - startTime));
if (needInitUserList.isEmpty() && histories.isEmpty() && pendingConfirmCount == 0) {
if (needInitUserList.isEmpty() && histories.isEmpty()) {
return "本次没有可更新的数据。";
}
return "更新完成: 新增用户 " + needInitUserList.size() + " 个, 有效变更 " + histories.size()
+ ", 新增待确认人员 " + pendingConfirmCount + "";
+ "";
}
/**
* 根据指定批次的完整数据源校正系统用户待确认状态。
* 已人工确认且仍未重新出现的人员保持已确认状态,避免每次全量更新重复转为待确认。
*
* @param pullTime 本次全量数据的拉取时间
*/
private void reconcilePendingConfirmStatus(String pullTime) {
Sql resetStatusSql = Sqls.create("""
UPDATE sys_user userInfo
SET userInfo.pendingConfirmStatus = @noneStatus
WHERE userInfo.personType IN ('教职工', '劳务派遣人员')
AND userInfo.pendingConfirmStatus <> @noneStatus
AND EXISTS (
SELECT 1
FROM sys_user_source sourceInfo
WHERE sourceInfo.pullTime = @pullTime
AND sourceInfo.loginname = userInfo.loginname
AND sourceInfo.personType IN ('教职工', '劳务派遣人员')
)
""");
resetStatusSql.setParam("noneStatus", PENDING_CONFIRM_STATUS_NONE);
resetStatusSql.setParam("pullTime", pullTime);
dao.execute(resetStatusSql);
Sql pendingStatusSql = Sqls.create("""
UPDATE sys_user userInfo
SET userInfo.pendingConfirmStatus = @pendingStatus
WHERE userInfo.personType IN ('教职工', '劳务派遣人员')
AND IFNULL(userInfo.pendingConfirmStatus, @noneStatus) = @noneStatus
AND NOT EXISTS (
SELECT 1
FROM sys_user_source sourceInfo
WHERE sourceInfo.pullTime = @pullTime
AND sourceInfo.loginname = userInfo.loginname
AND sourceInfo.personType IN ('教职工', '劳务派遣人员')
)
""");
pendingStatusSql.setParam("pendingStatus", PENDING_CONFIRM_STATUS_PENDING);
pendingStatusSql.setParam("noneStatus", PENDING_CONFIRM_STATUS_NONE);
pendingStatusSql.setParam("pullTime", pullTime);
dao.execute(pendingStatusSql);
}
/**
@@ -17,7 +17,6 @@ import lombok.Getter;
public enum MemberChangeType {
//变更类型
PENDING_CONFIRM("PENDING_CONFIRM", "待确认"),
NEW("NEW", "新入职"),
RESTORE("RESTORE", "入会"),
WITHDRAWAL("WITHDRAWAL", "退会"),
@@ -156,7 +156,12 @@ public class MemberChangeApplyController {
* @return
*/
@At
@SaCheckPermission(value = {"member.change.branchUnion.audit","member.change.schoolUnion.audit", "member.change.unionGroup.audit"}, mode = SaMode.OR)
@SaCheckPermission(value = {
"member.change.branchUnion.audit",
"member.change.schoolUnion.audit",
"member.change.unionGroup.audit",
"staff.sourcechange.pending"
}, mode = SaMode.OR)
public Result getUserByIdForMemberChange(String userId){
View_user user = dao.fetch(View_user.class, Cnd.where("id", "=", userId));
NutMap nutMap = Lang.obj2nutmap(user);
@@ -36,6 +36,7 @@ import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import com.budwk.app.zhgh.staffmanage.member.models.MemberChangeRecord;
import com.budwk.app.zhgh.staffmanage.member.param.pageform.MemberManagePageForm;
import com.budwk.app.zhgh.staffmanage.member.service.MemberCommonService;
import com.budwk.app.zhgh.staffmanage.sourcechange.service.PendingConfirmUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -79,6 +80,8 @@ public class MemberChangeManageController {
private CommonService commonService;
@Inject
private MemberCommonService memberCommonService;
@Inject
private PendingConfirmUserService pendingConfirmUserService;
@At("")
@Ok("beetl:/platform/zhgh/staffmanage/member/change/manage/index.html")
@@ -101,7 +104,11 @@ public class MemberChangeManageController {
@RepeatSubmit
@Aop(TransAop.READ_COMMITTED)
@SLog(tag = "会员高级管理-会员变更", msg = "分工会/校工会会员管理员提交变更")
@SaCheckPermission(value = {"member.change.mange", "staff.member.change.mange"}, mode = SaMode.OR)
@SaCheckPermission(value = {
"member.change.mange",
"staff.member.change.mange",
"staff.sourcechange.pending"
}, mode = SaMode.OR)
public Result doSubmitChange(MemberChangeRecord record){
// 检验是否有变更
if (Lang.isEmpty(record)) {
@@ -162,6 +169,8 @@ public class MemberChangeManageController {
if (schoolRoleBool) {
memberCommonService.compareChangeInfoAndUpdateMember(record);
}
// 待确认人员完成变更提交后转为已确认;普通人员调用时不会产生状态变化。
pendingConfirmUserService.markConfirmed(record.getUserId());
return Result.success();
}
@@ -171,8 +180,7 @@ public class MemberChangeManageController {
@SaCheckPermission(value = {
"member.change.mange",
"staff.member.change.mange",
"staff.sourcechange.manage",
"staff.sourcechange.pending"
"staff.sourcechange.manage"
}, mode = SaMode.OR)
public Result getAllChangeMemberInfo(String userId){
List<NutMap> mapList = memberCommonService.getAllChangeInfo(userId);
@@ -441,12 +441,8 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
MemberChangeRecord record = dao().fetch(MemberChangeRecord.class, Cnd.where("userId", "=", userId).desc("applyDateTime"));
/*
* 待确认人员来自全量数据差异校正,没有常规的字段前后值 changeInfos。
* 此处保留该人员的全部历史,再在组装时间线时仅接纳普通变更或待确认记录。
*/
List<Sys_user_history> historyList = dao().query(Sys_user_history.class,
Cnd.where("loginname", "=", loginName).desc("changeTime"));
List<Sys_user_history> historyList = dao().query(Sys_user_history.class, Cnd.where("loginname", "=", loginName)
.and("changeInfos", "is not", null).desc("changeTime"));
List<String> recordList = historyList.stream().map(Sys_user_history::getRecordId).toList();
@@ -459,25 +455,11 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
resultMapList.add(map);
}
for (Sys_user_history history : historyList) {
boolean pendingConfirm = Lang.isNotEmpty(history.getChangeTypes())
&& history.getChangeTypes().contains(MemberChangeType.PENDING_CONFIRM.name());
if (Lang.isEmpty(history.getChangeInfos()) && !pendingConfirm) {
continue;
}
NutMap map = NutMap.NEW();
List<NutMap> changeInfos = history.getChangeInfos();
if (pendingConfirm) {
// 待确认记录没有人工异动表,使用差异校正原因构造可阅读的变更说明。
changeInfos = List.of(NutMap.NEW()
.setv("field", "pendingConfirm")
.setv("fieldName", "人员状态")
.setv("sourceValue", "系统用户中存在")
.setv("newValue", StrUtil.blankToDefault(history.getChangeReason(), "本次全量人员数据中不存在")));
}
map.put("changeInfos", changeInfos);
map.put("timestamp", DateUtil.format(history.getChangeTime(), "yyyy-MM-dd HH:mm:ss"));
String changeOriginName = MemberChangeOrigin.valueOf(MemberChangeOrigin.class, history.getChangeOrigin()).changeOriginName;
map.put("changeOriginName", pendingConfirm ? changeOriginName + "(待确认)" : changeOriginName);
map.put("changeOriginName", MemberChangeOrigin.valueOf(MemberChangeOrigin.class, history.getChangeOrigin()).changeOriginName);
resultMapList.add(map);
}
return resultMapList;
@@ -1,71 +0,0 @@
package com.budwk.app.zhgh.staffmanage.sourcechange.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingUserConfirmParam;
import com.budwk.app.zhgh.staffmanage.sourcechange.param.SourceChangePageForm;
import com.budwk.app.zhgh.staffmanage.sourcechange.service.PendingUserHistoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
/**
* 待确认人员记录页面及接口。
*/
@IocBean
@At("/platform/sourcechange/pending")
@Ok("json")
@Api(tags = "待确认人员记录")
public class PendingSourceChangeController {
@Inject
private PendingUserHistoryService pendingUserHistoryService;
/**
* 打开待确认人员记录页面。
*/
@At("")
@Ok("beetl:/platform/zhgh/staffmanage/sourcechange/pending/index.html")
@SaCheckPermission("staff.sourcechange.pending")
public void index() {
}
/**
* 分页查询当前仍需人工确认的人员记录。
*
* @param pageForm 变更记录通用查询条件
* @return 分页结果
*/
@At
@ApiOperation("分页查询待确认人员")
@SaCheckPermission("staff.sourcechange.pending")
public Result pageData(SourceChangePageForm pageForm) {
return Result.success(pendingUserHistoryService.listPage(pageForm));
}
/**
* 将待确认人员记录确认成最终异动类型。
*
* @param confirmParam 异动类型及备注
* @return 处理结果
*/
@At
@ApiOperation("确认待确认人员")
@SLog(tag = "待确认人员记录", msg = "确认人员")
@SaCheckPermission("staff.sourcechange.pending")
public Result confirm(@Valid @Param("data") PendingUserConfirmParam confirmParam) {
try {
pendingUserHistoryService.confirm(confirmParam);
return Result.success();
} catch (IllegalArgumentException | IllegalStateException e) {
return Result.error(e.getMessage());
}
}
}
@@ -1,28 +0,0 @@
package com.budwk.app.zhgh.staffmanage.sourcechange.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
/**
* 待确认人员确认参数。
*/
@Data
@ApiModel("待确认人员确认参数")
public class PendingUserConfirmParam {
@NotBlank(message = "待确认记录不能为空")
@ApiModelProperty("sys_user_history 记录ID")
private String id;
@NotBlank(message = "请选择异动类型")
@ApiModelProperty("最终异动类型")
private String changeType;
@Size(max = 1000, message = "备注不能超过1000个字符")
@ApiModelProperty("确认备注")
private String remark;
}
@@ -1,39 +0,0 @@
package com.budwk.app.zhgh.staffmanage.sourcechange.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.models.Sys_user_source;
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingUserConfirmParam;
import com.budwk.app.zhgh.staffmanage.sourcechange.param.SourceChangePageForm;
import java.util.List;
/**
* 待确认人员历史记录业务服务。
*/
public interface PendingUserHistoryService {
/**
* 根据变更记录页面的查询条件分页查询待确认人员。
*
* @param pageForm 查询及分页参数
* @return 待确认人员分页数据
*/
Pagination listPage(SourceChangePageForm pageForm);
/**
* 将待确认记录更新为人工选择的最终异动类型并保存备注。
*
* @param confirmParam 确认参数
*/
void confirm(PendingUserConfirmParam confirmParam);
/**
* 依据本次全量源数据校正待确认记录:缺失人员补充记录,重新出现人员删除待确认记录。
*
* @param systemUsers 更新开始时系统中已有的用户
* @param sourceUsers 本次全量拉取的用户
* @return 本次新增的待确认记录数量
*/
int reconcile(List<Sys_user> systemUsers, List<Sys_user_source> sourceUsers);
}
@@ -1,197 +0,0 @@
package com.budwk.app.zhgh.staffmanage.sourcechange.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.models.Sys_user_history;
import com.budwk.app.sys.models.Sys_user_source;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import com.budwk.app.zhgh.staffmanage.sourcechange.param.PendingUserConfirmParam;
import com.budwk.app.zhgh.staffmanage.sourcechange.param.SourceChangePageForm;
import com.budwk.app.zhgh.staffmanage.sourcechange.service.PendingUserHistoryService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.dao.util.cri.Static;
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.random.R;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 待确认人员历史记录业务实现。
*/
@IocBean
public class PendingUserHistoryServiceImpl implements PendingUserHistoryService {
private static final int BATCH_SIZE = 500;
@Inject
private Dao dao;
@Inject
private SysUserService sysUserService;
/**
* 查询待确认人员,查询字段与人员变更记录页面保持一致,但未选择日期时查询全部待确认数据。
*/
@Override
public Pagination listPage(SourceChangePageForm pageForm) {
Sql sql = Sqls.create("""
SELECT
his.id,
his.loginname,
his.username,
COALESCE(u.sex, his.sex) AS sex,
COALESCE(u.unitName, his.unitName) AS unitName,
COALESCE(u.unionName, his.unionName) AS unionName,
his.userState,
his.personType,
his.preparedBy,
his.changeTime,
his.changeTypes,
his.changeOrigin,
his.remark,
u.id AS userId
FROM sys_user_history his
LEFT JOIN vw_user u ON his.loginname = u.loginname
$condition
""");
Cnd cnd = Cnd.NEW();
// 待确认页面固定限定待确认类型,避免已处理记录继续出现在列表中。
cnd.and(new Static("JSON_CONTAINS(his.changeTypes, JSON_QUOTE('" + MemberChangeType.PENDING_CONFIRM.name() + "'), '$')"));
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
SqlExpressionGroup keywordGroup = new SqlExpressionGroup();
keywordGroup.orLike("his.username", pageForm.getSearchKeyword());
keywordGroup.orLike("his.loginname", pageForm.getSearchKeyword());
cnd.and(keywordGroup);
}
cnd.andEX("u.unionId", "=", pageForm.getUnionId());
cnd.andEX("u.unitId", "=", pageForm.getUnitId());
cnd.andEX("u.sex", "=", pageForm.getSex());
cnd.andEX("his.personType", "=", pageForm.getPersonType());
cnd.andEX("his.preparedBy", "=", pageForm.getPreparedBy());
cnd.andEX("his.userState", "=", pageForm.getUserState());
cnd.andEX("his.changeOrigin", "=", pageForm.getChangeOrigin());
// 日期范围使用参数化条件,结束日期按次日零点前处理,包含所选结束日期的全部数据。
if (StrUtil.isAllNotBlank(pageForm.getChangeDateBefore(), pageForm.getChangeDateEnd())) {
Date startDate = DateUtil.parseDate(pageForm.getChangeDateBefore());
Date endDateExclusive = DateUtil.offsetDay(DateUtil.parseDate(pageForm.getChangeDateEnd()), 1);
cnd.and("his.changeTime", ">=", startDate);
cnd.and("his.changeTime", "<", endDateExclusive);
}
if (StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) {
cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy()));
} else {
cnd.desc("his.changeTime");
}
sql.setCondition(cnd);
return sysUserService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
/**
* 确认待确认人员记录,异动类型只允许选择已定义且非待确认的最终类型。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void confirm(PendingUserConfirmParam confirmParam) {
MemberChangeType selectedType;
try {
selectedType = MemberChangeType.valueOf(confirmParam.getChangeType());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("异动类型不存在");
}
if (MemberChangeType.PENDING_CONFIRM == selectedType) {
throw new IllegalArgumentException("请选择最终异动类型");
}
// 更新条件同时校验记录仍处于待确认状态,防止重复确认覆盖已经处理的数据。
Cnd cnd = Cnd.where("id", "=", confirmParam.getId())
.and(new Static("JSON_CONTAINS(changeTypes, JSON_QUOTE('" + MemberChangeType.PENDING_CONFIRM.name() + "'), '$')"));
Chain chain = Chain.make("changeTypes", Json.toJson(List.of(selectedType.name())))
.add("remark", StrUtil.trim(confirmParam.getRemark()));
int updated = dao.update(Sys_user_history.class, chain, cnd);
if (updated == 0) {
throw new IllegalStateException("待确认记录不存在或已被处理");
}
}
/**
* 根据本次全量源数据双向校正待确认历史:源数据重新出现则删除,源数据缺失则幂等新增。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public int reconcile(List<Sys_user> systemUsers, List<Sys_user_source> sourceUsers) {
Set<String> sourceLoginNames = sourceUsers.stream()
.map(Sys_user_source::getLoginname)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
List<Sys_user_history> pendingHistories = dao.query(Sys_user_history.class, pendingCondition());
// 已重新出现在完整源数据中的人员不再需要人工确认,仅删除尚未处理的待确认记录。
List<String> recoveredPendingIds = pendingHistories.stream()
.filter(history -> sourceLoginNames.contains(history.getLoginname()))
.map(Sys_user_history::getId)
.toList();
for (List<String> idBatch : ListUtil.split(recoveredPendingIds, BATCH_SIZE)) {
dao.clear(Sys_user_history.class, Cnd.where("id", "in", idBatch));
}
Set<String> existingPendingLoginNames = pendingHistories.stream()
.filter(history -> !sourceLoginNames.contains(history.getLoginname()))
.map(Sys_user_history::getLoginname)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(HashSet::new));
List<Sys_user_history> historiesToInsert = new ArrayList<>();
// 同一工号最多保留一条待确认记录,重复执行全部更新不会重复生成数据。
for (Sys_user user : systemUsers) {
String loginname = user.getLoginname();
if (StrUtil.isBlank(loginname)
|| sourceLoginNames.contains(loginname)
|| !existingPendingLoginNames.add(loginname)) {
continue;
}
Sys_user_history history = new Sys_user_history();
BeanUtil.copyProperties(user, history);
history.setId(R.UU32());
history.setChangeTime(DateUtil.date());
history.setChangeTypes(List.of(MemberChangeType.PENDING_CONFIRM.name()));
history.setChangeOrigin(MemberChangeOrigin.SYSTEM.name());
history.setChangeReason("全量人员数据中不存在");
historiesToInsert.add(history);
}
for (List<Sys_user_history> historyBatch : ListUtil.split(historiesToInsert, BATCH_SIZE)) {
dao.fastInsert(historyBatch);
}
return historiesToInsert.size();
}
/**
* 构造待确认 JSON 类型条件,供查询及差异校正统一使用。
*/
private Cnd pendingCondition() {
return Cnd.where(new Static("JSON_CONTAINS(changeTypes, JSON_QUOTE('" + MemberChangeType.PENDING_CONFIRM.name() + "'), '$')"));
}
}
@@ -28,7 +28,7 @@ SELECT
1,
0,
'staff.sourcechange.pending',
'全量人员数据差异待人工确认记录',
'系统用户待确认和已确认人员记录',
source_menu.location + 1,
0,
'',
@@ -49,7 +49,7 @@ WHERE source_menu.permission = 'staff.sourcechange.manage'
FROM (SELECT id FROM sys_menu WHERE permission = 'staff.sourcechange.pending') existing_menu
);
-- 原先能够查看“变更记录”的角色默认拥有待确认人员记录权限,避免新页面上线后无人可见
-- 原先能够查看“变更记录”的角色默认拥有待确认人员记录权限。
INSERT INTO sys_role_menu (roleId, menuId)
SELECT source_role.roleId, pending_menu.id
FROM sys_role_menu source_role
@@ -1,18 +0,0 @@
-- 待确认人员人工确认备注字段。已有字段的环境重复执行不会改变表结构。
SET @pending_remark_column_count = (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sys_user_history'
AND COLUMN_NAME = 'remark'
);
SET @pending_remark_ddl = IF(
@pending_remark_column_count = 0,
'ALTER TABLE sys_user_history ADD COLUMN remark TEXT NULL COMMENT ''备注'' AFTER changeReason',
'SELECT 1'
);
PREPARE pending_remark_stmt FROM @pending_remark_ddl;
EXECUTE pending_remark_stmt;
DEALLOCATE PREPARE pending_remark_stmt;
@@ -1,7 +1,7 @@
const MEMBER_CHANGE = {
template: /*language=HTML*/ `
<el-card shadow="never">
<snaker-start slot="header" label="会员变更" define_key="HYBG"></snaker-start>
<snaker-start v-if="showFlowHeader" slot="header" label="会员变更" define_key="HYBG"></snaker-start>
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=""
class="flow-task-form">
<el-descriptions :column="3" border>
@@ -155,12 +155,12 @@ const MEMBER_CHANGE = {
style="width: 100%"></dict-select>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="人员分类">
<el-form-item prop="aidFundMemberUserType">
<dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"
style="width: 100%"></dict-select>
</el-form-item>
</el-descriptions-item>
<!-- <el-descriptions-item label="人员分类">-->
<!-- <el-form-item prop="aidFundMemberUserType">-->
<!-- <dict-select v-model="formData.aidFundMemberUserType" code="AIDFUND_MEMBER_USER_TYPE"-->
<!-- style="width: 100%"></dict-select>-->
<!-- </el-form-item>-->
<!-- </el-descriptions-item>-->
<template v-if="formData.userState == '退休'">
<el-descriptions-item label="退休日期">
@@ -288,6 +288,8 @@ const MEMBER_CHANGE = {
props: {
id: { type: String, default: '' },
showThreeUnit: { type: Boolean, default: false },
// 控制公共变更组件的标题及流程图入口,默认保留原会员变更页面样式。
showFlowHeader: { type: Boolean, default: true },
},
mixins: [initTableMixins],
store,
@@ -2,145 +2,116 @@
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号">
<el-input v-model="pageForm.searchKeyword" clearable placeholder="请输入姓名工号"></el-input>
</search-item>
<search-item label="所属工会">
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会"
style="width: 100%" @change="flushUnits" @clear="flushUnits">
<el-option v-for="item in unions" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select v-model="pageForm.unitId" clearable filterable placeholder="请选择所属单位" style="width: 100%">
<el-option v-for="item in units" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="在职状态">
<dict-select v-model="pageForm.userState" code="USER_STATE" clearable placeholder="请选择在职状态"></dict-select>
</search-item>
<search-item label="人员类型">
<dict-select v-model="pageForm.personType" code="USER_PERSON_TYPE" clearable placeholder="请选择人员类型"></dict-select>
</search-item>
<search-item label="变更来源">
<el-select v-model="pageForm.changeOrigin" clearable filterable placeholder="请选择变更来源" style="width: 100%">
<el-option v-for="item in changeOriginData" :key="item.name"
:label="item.changeOriginName" :value="item.name"></el-option>
</el-select>
</search-item>
<search-item label="变更日期">
<el-date-picker v-model="pageForm.changeDateRange" type="daterange" value-format="yyyy-MM-dd"
start-placeholder="开始日期" end-placeholder="结束日期" range-separator="-"
style="width: 100%" @change="changeDateRangeChange"></el-date-picker>
</search-item>
</search>
</el-card>
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="姓名/工号">
<el-input clearable placeholder="请输入姓名或工号"
v-model="pageForm.searchKeyword"></el-input>
</search-item>
<search-item label="所属工会">
<el-select @change="flushUnits" @clear="flushUnits" clearable filterable
placeholder="请选择所属工会" style="width: 100%"
v-model="pageForm.unionId">
<el-option :key="item.id" :label="item.name" :value="item.id"
v-for="item in unions"></el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select clearable filterable placeholder="请选择所属单位" style="width: 100%"
v-model="pageForm.unitId">
<el-option :key="item.id" :label="item.name" :value="item.id"
v-for="item in units"></el-option>
</el-select>
</search-item>
<search-item label="人员类型">
<dict-select @change="doSearch" code="USER_PERSON_TYPE" placeholder="请选择人员类型"
v-model="pageForm.personType"></dict-select>
</search-item>
<search-item label="在职状态">
<dict-select @change="doSearch" code="USER_STATE" placeholder="请选择在职状态"
v-model="pageForm.userState"></dict-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="待确认人员记录"></table-tool>
<el-table ref="table" :data="tableData" :size="tableSize" row-key="id"
style="width: 100%" @sort-change="pageOrder">
<el-table-column type="index" label="序号" width="70" align="center" :index="indexMethod"></el-table-column>
<el-table-column prop="loginname" label="工号" min-width="110" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column prop="username" label="姓名" min-width="100" sortable="custom" show-overflow-tooltip></el-table-column>
<el-table-column prop="sex" label="性别" width="80" align="center">
<template slot-scope="{row}">
<dict-tag code="USER_SEX" :value="row.sex"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="userState" label="在职状态" min-width="100">
<template slot-scope="{row}">
<dict-tag code="USER_STATE" :value="row.userState"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="personType" label="人员类型" min-width="110">
<template slot-scope="{row}">
<dict-tag code="USER_PERSON_TYPE" :value="row.personType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="unionName" label="所属工会" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="unitName" label="所属单位" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column prop="changeTime" label="待确认时间" width="170" sortable="custom"></el-table-column>
<el-table-column prop="changeOrigin" label="变更来源" min-width="120">
<template slot-scope="{row}">{{ getChangeOrigin(row.changeOrigin) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template slot-scope="{row}">
<el-button type="primary" size="mini" @click="openConfirm(row)">确认人员</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-card class="mt10" shadow="never">
<table-tool label="待确认人员记录">
<el-radio-group @change="doSearch" size="small" v-model="pageForm.pendingConfirmStatus">
<el-radio-button :label="1">待确认</el-radio-button>
<el-radio-button :label="2">已确认</el-radio-button>
</el-radio-group>
</table-tool>
<el-dialog title="确认人员" :visible.sync="confirmVisible" width="520px" append-to-body
:close-on-click-modal="false" @closed="resetConfirmForm">
<el-form ref="confirmFormRef" :model="confirmForm" :rules="confirmRules" label-width="100px">
<el-form-item label="异动类型" prop="changeType">
<el-select v-model="confirmForm.changeType" filterable placeholder="请选择异动类型" style="width: 100%">
<el-option v-for="item in finalChangeTypeData" :key="item.code"
:label="item.changeTypeName" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="confirmForm.remark" type="textarea" :rows="4" maxlength="1000"
show-word-limit placeholder="请输入备注"></el-input>
</el-form-item>
</el-form>
<template slot="footer">
<el-button @click="confirmVisible = false">取消</el-button>
<el-button type="primary" :loading="confirmLoading" @click="submitConfirm">确定</el-button>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder"
class="vi-table" ref="table" row-key="id" style="width: 100%">
<el-table-column :index="indexMethod" align="center" fixed="left"
header-align="center" label="序号" type="index" width="80"></el-table-column>
<el-table-column align="center" header-align="center" label="工号" min-width="120"
prop="loginname" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="姓名" min-width="100"
prop="username" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="性别" min-width="80"
prop="sex" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="所属工会" min-width="150"
prop="unionName" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="所属单位" min-width="180"
prop="unitName" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="人员类型" min-width="120"
prop="personType" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="在职状态" min-width="100"
prop="userState" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="确认状态" min-width="100"
prop="pendingConfirmStatus">
<template slot-scope="{row}">
<el-tag type="warning" v-if="row.pendingConfirmStatus === 1">待确认</el-tag>
<el-tag type="success" v-else-if="row.pendingConfirmStatus === 2">已确认</el-tag>
</template>
</el-table-column>
<el-table-column align="center" fixed="right" header-align="center" label="操作" width="100">
<template slot-scope="{row}">
<el-button @click="openChange(row.id)" size="mini" type="primary">变更</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #public>
<member-change ref="memberChangeRef" :show-three-unit="true" :show-flow-header="false"
@do-back="closeChange" @do-submit="doSubmit"></member-change>
</template>
</el-dialog>
</guava>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../member/change/common/memberChange.js"){}#-->
new Vue({
el: "#app",
store,
mixins: [initTableMixins],
components: {
"member-change": MEMBER_CHANGE
},
data() {
return {
pageForm: {
searchKeyword: "",
unionId: "",
unitId: "",
userState: "",
personType: "",
changeOrigin: "",
changeDateRange: [],
changeDateBefore: "",
changeDateEnd: ""
userState: "",
pendingConfirmStatus: 1
},
unions: [],
units: [],
changeOriginData: [],
changeTypeData: [],
confirmVisible: false,
confirmLoading: false,
confirmForm: {
id: "",
changeType: "",
remark: ""
},
confirmRules: {
changeType: [{required: true, message: "请选择异动类型", trigger: "change"}],
remark: [{max: 1000, message: "备注不能超过1000个字符", trigger: "blur"}]
}
}
},
computed: {
finalChangeTypeData() {
return this.changeTypeData.filter((item) => item.code !== "PENDING_CONFIRM")
units: []
}
},
methods: {
/** 分页加载待确认人员,查询参数沿用人员变更记录页面。 */
/** 分页查询待确认或已确认人员。 */
pageData() {
this.$axios.post("/platform/sourcechange/pending/pageData", this.pageForm).then((resp) => {
return $.post("/platform/sourcechange/pending/pageData", this.pageForm).then((resp) => {
if (resp.code === 0) {
this.tableData = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
@@ -149,81 +120,63 @@ layout("/layouts/platform.html"){
}
})
},
/** 工会变更时重新加载其下属单位,避免带入其他工会的单位条件。 */
/** 切换工会时重新加载属单位。 */
flushUnits() {
this.$set(this.pageForm, "unitId", "")
if (!this.pageForm.unionId) {
this.units = []
if (this.pageForm.unionId) {
this.$businessTool.listUnit(this.pageForm.unionId).then((data) => {
this.units = data
})
} else {
this.$businessTool.listUnit().then((data) => {
this.units = data
})
return
}
this.$businessTool.listUnit(this.pageForm.unionId).then((data) => {
this.units = data
},
/** 打开人员变更组件并加载当前人员信息。 */
openChange(userId) {
this.$refs.guava.public(() => {
this.$refs.memberChangeRef.init(userId)
})
},
/** 将日期组件值转换成后台通用的起止日期参数。 */
changeDateRangeChange(value) {
this.pageForm.changeDateBefore = value && value.length ? value[0] : ""
this.pageForm.changeDateEnd = value && value.length ? value[1] : ""
/** 关闭变更页面并刷新列表。 */
closeChange() {
this.$refs.guava.index()
this.pageData()
},
/** 根据枚举数据展示变更来源名称。 */
getChangeOrigin(value) {
const option = this.changeOriginData.find((item) => item.name === value)
return option ? option.changeOriginName : value
/** 复用人员变更提交接口,成功后刷新待确认状态。 */
doSubmit(data) {
const loading = createLoading("正在提交中")
this.$axios.post("/platform/member/change/manage/doSubmitChange", data).then((resp) => {
loading.close()
if (resp.code === 0) {
this.$message.success(resp.msg)
this.closeChange()
} else {
this.$message.error(resp.msg)
}
}).catch(() => {
loading.close()
this.$message.error("提交失败,请稍后重试")
})
},
/** 打开确认弹窗并绑定当前待确认历史记录。 */
openConfirm(row) {
this.confirmForm.id = row.id
this.confirmForm.changeType = ""
this.confirmForm.remark = row.remark || ""
this.confirmVisible = true
},
/** 提交最终异动类型,成功后刷新列表使已确认人员立即移除。 */
submitConfirm() {
this.$refs.confirmFormRef.validate((valid) => {
if (!valid) return
this.confirmLoading = true
this.$axios.post("/platform/sourcechange/pending/confirm", {
data: JSON.stringify(this.confirmForm)
}).then((resp) => {
if (resp.code === 0) {
this.$message.success("人员确认成功")
this.confirmVisible = false
this.pageData()
} else {
this.$message.warning(resp.msg)
}
}).finally(() => {
this.confirmLoading = false
/** 初始化工会和单位查询选项。 */
initData() {
return Promise.all([
this.$businessTool.listUnion().then((data) => {
this.unions = data
}),
this.$businessTool.listUnit().then((data) => {
this.units = data
})
})
},
/** 关闭弹窗后清理表单校验状态及业务字段。 */
resetConfirmForm() {
this.confirmForm = {id: "", changeType: "", remark: ""}
if (this.$refs.confirmFormRef) {
this.$refs.confirmFormRef.clearValidate()
}
},
/** 加载当前登录人可用的组织及异动枚举数据。 */
initOptions() {
Promise.all([
this.$businessTool.listUnion(),
this.$businessTool.listUnit(),
this.$businessTool.getEnumOptions("MemberChangeType"),
this.$businessTool.getEnumOptions("MemberChangeOrigin")
]).then((result) => {
this.unions = result[0]
this.units = result[1]
this.changeTypeData = result[2]
this.changeOriginData = result[3]
this.pageData()
})
])
}
},
created() {
this.initOptions()
this.initData().then(() => {
this.pageData()
})
}
})
</script>