commit
This commit is contained in:
@@ -8,18 +8,25 @@ import com.budwk.app.sys.models.Sys_user;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.FieldFilter;
|
||||
import org.nutz.dao.util.Daos;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -29,11 +36,23 @@ import java.util.stream.Collectors;
|
||||
@Api(tags = "用户审批意见")
|
||||
public class SysUserApprovalOpinionController {
|
||||
|
||||
/**
|
||||
* 系统内置的常用审批意见,统一提供给所有使用审核意见组件的页面。
|
||||
*/
|
||||
private static final List<String> DEFAULT_APPROVAL_OPINIONS = List.of("同意", "不同意");
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
/**
|
||||
* 保存当前用户的自定义审批意见。
|
||||
*
|
||||
* @param opinions 前端提交的意见数组,每项包含 id 和 text;系统内置的“同意”“不同意”不会写入用户数据
|
||||
* @return Result,成功时不返回额外数据,失败时 msg 说明校验结果
|
||||
*/
|
||||
@At
|
||||
@SaCheckLogin
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@ApiOperation("保存审批意见")
|
||||
public Result update(@Param("opinions")JSONObject[] opinions){
|
||||
boolean hasBlank = Arrays.stream(opinions).anyMatch(v -> StrUtil.isBlank(v.getStr("text").trim()));
|
||||
@@ -41,35 +60,68 @@ public class SysUserApprovalOpinionController {
|
||||
return Result.error("内容不能为空,请删除后再提交!");
|
||||
}
|
||||
|
||||
Set<String> opinionTexts = Arrays.stream(opinions).map(v -> v.getStr("text").trim()).collect(Collectors.toSet());
|
||||
if(opinionTexts.size() != opinions.length){
|
||||
// 系统默认意见只参与页面展示,不占用用户自定义意见数量,也不写入用户表。
|
||||
JSONObject[] customOpinions = Arrays.stream(opinions)
|
||||
.filter(v -> !DEFAULT_APPROVAL_OPINIONS.contains(v.getStr("text").trim()))
|
||||
.toArray(JSONObject[]::new);
|
||||
|
||||
Set<String> opinionTexts = Arrays.stream(customOpinions).map(v -> v.getStr("text").trim()).collect(Collectors.toSet());
|
||||
if(opinionTexts.size() != customOpinions.length){
|
||||
return Result.error("内容重复,请删除后再提交!");
|
||||
}
|
||||
|
||||
boolean text = Arrays.stream(opinions).anyMatch(v -> v.getStr("text").trim().length() > 50);
|
||||
boolean text = Arrays.stream(customOpinions).anyMatch(v -> v.getStr("text").trim().length() > 50);
|
||||
if(text){
|
||||
return Result.error("单条内容不能超过50个字!");
|
||||
}
|
||||
|
||||
if(opinions.length > 5){
|
||||
if(customOpinions.length > 5){
|
||||
return Result.error("最多可添加10条审批意见!");
|
||||
}
|
||||
|
||||
for (int i = 0; i < opinions.length; i++) {
|
||||
opinions[i].set("id",i+1);
|
||||
for (int i = 0; i < customOpinions.length; i++) {
|
||||
customOpinions[i].set("id",i+1);
|
||||
}
|
||||
|
||||
dao.update(Sys_user.class, Chain.make("customApprovalOpinions", Arrays.asList(opinions)), Cnd.where(Sys_user::getId,"=", SecurityUtil.getUserId()));
|
||||
dao.update(Sys_user.class, Chain.make("customApprovalOpinions", Arrays.asList(customOpinions)), Cnd.where(Sys_user::getId,"=", SecurityUtil.getUserId()));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户可选的审批意见。
|
||||
*
|
||||
* @return Result,data 为意见数组,每项包含 id 和 text;系统默认意见排在用户自定义意见之前
|
||||
*/
|
||||
@At
|
||||
@SaCheckLogin
|
||||
@ApiOperation("获取审批意见")
|
||||
public Result list(){
|
||||
FieldFilter fieldFilter = FieldFilter.create(Sys_user.class, "^customApprovalOpinions");
|
||||
Sys_user sysUser = Daos.ext(dao, fieldFilter).fetch(Sys_user.class, Cnd.where(Sys_user::getId, "=", SecurityUtil.getUserId()));
|
||||
return Result.success(sysUser.getCustomApprovalOpinions());
|
||||
List<JSONObject> opinions = new ArrayList<>();
|
||||
Set<String> opinionTexts = new HashSet<>();
|
||||
|
||||
// 默认意见始终置顶,并通过文本去重,避免与用户历史自定义意见重复。
|
||||
for (String defaultOpinion : DEFAULT_APPROVAL_OPINIONS) {
|
||||
JSONObject opinion = new JSONObject();
|
||||
opinion.set("id", opinions.size() + 1);
|
||||
opinion.set("text", defaultOpinion);
|
||||
opinions.add(opinion);
|
||||
opinionTexts.add(defaultOpinion);
|
||||
}
|
||||
|
||||
List<JSONObject> customOpinions = Optional.ofNullable(sysUser.getCustomApprovalOpinions()).orElseGet(Collections::emptyList);
|
||||
for (JSONObject customOpinion : customOpinions) {
|
||||
String text = customOpinion.getStr("text");
|
||||
if (StrUtil.isBlank(text) || !opinionTexts.add(text.trim())) {
|
||||
continue;
|
||||
}
|
||||
JSONObject opinion = new JSONObject();
|
||||
opinion.set("id", opinions.size() + 1);
|
||||
opinion.set("text", text);
|
||||
opinions.add(opinion);
|
||||
}
|
||||
return Result.success(opinions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+36
-102
@@ -2,7 +2,6 @@ package com.budwk.app.sys.services.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.hutool.core.date.DateField;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
@@ -25,7 +24,6 @@ import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Chain;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
@@ -52,7 +50,6 @@ import java.util.stream.Collectors;
|
||||
@IocBean
|
||||
public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService {
|
||||
private static final int BATCH_SIZE = 500;
|
||||
private static final int ROLE_DELETE_BATCH_SIZE = 50;
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@@ -114,34 +111,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
||||
return fieldMappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否符合会员条件
|
||||
*
|
||||
* @param userState 用户状态
|
||||
* @param preparedBy 编制类别
|
||||
* @param postDoctoralJoinDate 博士后进站时间
|
||||
* @return 是否符合会员条件
|
||||
*/
|
||||
private boolean checkMembershipEligibility(String userState, String preparedBy, Date postDoctoralJoinDate) {
|
||||
// 博士后单独判断:只要进站时间在两年内就是会员
|
||||
if ("博士后".equals(preparedBy)) {
|
||||
if (postDoctoralJoinDate != null) {
|
||||
Date twoYearsAgo = DateUtil.offset(DateUtil.date(), DateField.YEAR, -2).toJdkDate();
|
||||
return postDoctoralJoinDate.after(twoYearsAgo);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 其他人员需要判断在岗状态和聘用方式
|
||||
if (!"在岗".equals(userState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断聘用方式
|
||||
Set<String> memberPreparedByTypes = new HashSet<>(Arrays.asList("新人事代理", "校聘合同制", "事业编制"));
|
||||
return memberPreparedByTypes.contains(preparedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量更新用户数据
|
||||
*
|
||||
@@ -156,7 +125,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
||||
|
||||
// 获取角色信息
|
||||
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
|
||||
Sys_role memberRole = sysRoleService.getByCode(RoleConstant.MEMBER);
|
||||
|
||||
// 获取单位信息,后续判断单位变更
|
||||
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1"));
|
||||
@@ -181,23 +149,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
||||
List<Sys_user> needDoUpdateList = new CopyOnWriteArrayList<>();
|
||||
List<Sys_user> needInitUserList = new CopyOnWriteArrayList<>();
|
||||
List<Sys_user_history> histories = new CopyOnWriteArrayList<>();
|
||||
List<String> addMemberUserIds = Collections.synchronizedList(new ArrayList<>());
|
||||
List<String> removeMemberUserIds = Collections.synchronizedList(new ArrayList<>());
|
||||
Set<String> sourceLoginNames = sources.stream()
|
||||
.map(Sys_user_source::getLoginname)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
List<String> leaveUserIds = sysUsers.stream()
|
||||
.filter(user -> StrUtil.isNotBlank(user.getLoginname()))
|
||||
.filter(user -> !sourceLoginNames.contains(user.getLoginname()))
|
||||
.map(Sys_user::getId)
|
||||
.toList();
|
||||
List<String> leaveMemberUserIds = sysUsers.stream()
|
||||
.filter(user -> StrUtil.isNotBlank(user.getLoginname()))
|
||||
.filter(user -> !sourceLoginNames.contains(user.getLoginname()))
|
||||
.filter(user -> Boolean.TRUE.equals(user.getMember()))
|
||||
.map(Sys_user::getId)
|
||||
.toList();
|
||||
|
||||
// 处理每条数据
|
||||
for (Sys_user_source source : sources) {
|
||||
@@ -214,22 +165,15 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
||||
u.setSalt(salt);
|
||||
u.setPassword(PwdUtil.getPassword(PwdUtil.generate(12), salt));
|
||||
|
||||
/* if (u.getMember()) {
|
||||
addMemberUserIds.add(u.getId());
|
||||
}*/
|
||||
needInitUserList.add(u);
|
||||
} else {
|
||||
// 修改现有用户
|
||||
u.setId(user.getId());
|
||||
|
||||
// 更新会员状态
|
||||
boolean currentIsMember = user.getMember() != null && user.getMember();
|
||||
|
||||
if (!currentIsMember) {
|
||||
// addMemberUserIds.add(user.getId());
|
||||
} else{
|
||||
// removeMemberUserIds.add(user.getId());
|
||||
}
|
||||
/*
|
||||
* aidFundMember 由基金会员业务维护,数据中心源表默认值为 0。
|
||||
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
|
||||
*/
|
||||
u.setAidFundMember(null);
|
||||
|
||||
needDoUpdateList.add(u);
|
||||
}
|
||||
@@ -312,45 +256,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
||||
updateTasks.add(updateTask);
|
||||
}
|
||||
|
||||
if (Lang.isNotEmpty(leaveUserIds)) {
|
||||
CompletableFuture<Void> leaveTask = CompletableFuture.runAsync(() -> {
|
||||
log.info("数据源缺失用户转为不在岗并取消会员: {} 个", leaveUserIds.size());
|
||||
List<List<String>> batches = ListUtil.split(leaveUserIds, BATCH_SIZE);
|
||||
batches.forEach(batch -> {
|
||||
try {
|
||||
/*
|
||||
* 本次拉取批次中没有出现、但系统用户表仍存在的人员,按离岗处理:
|
||||
* 1. userState 写为“不在岗”,用于后续人员状态筛选和业务判断;
|
||||
* 2. member 写为 false,避免仍按会员身份参与后续业务判断。
|
||||
*/
|
||||
dao.update(Sys_user.class, Chain.make("userState", "不在岗").add("member", false), Cnd.where("id", "in", batch));
|
||||
} catch (Exception e) {
|
||||
log.error("批量更新数据源缺失用户状态异常", e);
|
||||
}
|
||||
});
|
||||
}, executorService);
|
||||
updateTasks.add(leaveTask);
|
||||
}
|
||||
|
||||
if (Lang.isNotEmpty(leaveMemberUserIds)) {
|
||||
CompletableFuture<Void> leaveMemberRoleTask = CompletableFuture.runAsync(() -> {
|
||||
log.info("数据源缺失会员移除会员角色: {} 个", leaveMemberUserIds.size());
|
||||
List<List<String>> batches = ListUtil.split(leaveMemberUserIds, ROLE_DELETE_BATCH_SIZE);
|
||||
batches.forEach(batch -> {
|
||||
try {
|
||||
/*
|
||||
* 只对原本是会员的缺失人员移除会员角色,并缩小删除批次,
|
||||
* 降低 sys_user_role 大批量 DELETE 时的锁等待概率。
|
||||
*/
|
||||
dao.clear(Sys_user_role.class, Cnd.where("userId", "in", batch).and("roleId", "=", memberRole.getId()));
|
||||
} catch (Exception e) {
|
||||
log.error("批量移除数据源缺失会员角色异常", e);
|
||||
}
|
||||
});
|
||||
}, executorService);
|
||||
updateTasks.add(leaveMemberRoleTask);
|
||||
}
|
||||
|
||||
if (Lang.isNotEmpty(histories)) {
|
||||
CompletableFuture<Void> historyTask = CompletableFuture.runAsync(() -> {
|
||||
log.info("添加历史记录: {} 条", histories.size());
|
||||
@@ -444,8 +349,11 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
||||
long endTime = System.currentTimeMillis();
|
||||
log.info("全量更新用户数据完成,耗时: {} 毫秒", (endTime - startTime));
|
||||
|
||||
return "更新完成: 新增用户 " + needInitUserList.size() + " 个, 更新用户 " + needDoUpdateList.size()
|
||||
+ " 个, 数据源缺失转不在岗 " + leaveUserIds.size() + " 个, 待添加会员 " + addMemberUserIds.size() + " 个, 待移除会员 " + removeMemberUserIds.size() + " 个";
|
||||
if (needInitUserList.isEmpty() && histories.isEmpty()) {
|
||||
return "本次没有可更新的数据。";
|
||||
}
|
||||
return "更新完成: 新增用户 " + needInitUserList.size() + " 个, 有效变更 " + histories.size()
|
||||
+ " 条";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,6 +387,18 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
||||
Object sourceValue = mapping.field.get(source);
|
||||
Object userValue = mapping.field.get(user);
|
||||
|
||||
/*
|
||||
* 全量更新使用 updateIgnoreNull,数据源空值不会覆盖系统已有值。
|
||||
* 因此当新值为空、旧值有值时不生成历史记录,避免每次更新都重复记录“有值→空”。
|
||||
* 同时将 null、空字符串、纯空格统一视为业务空值,避免页面显示为空但底层值不同导致“空→空”被记录。
|
||||
*/
|
||||
if (isBlankChangeValue(sourceValue) && isBlankChangeValue(userValue)) {
|
||||
continue;
|
||||
}
|
||||
if (isBlankChangeValue(sourceValue) && !isBlankChangeValue(userValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果值不相等,记录变更
|
||||
if (!ObjectUtil.equals(sourceValue, userValue)) {
|
||||
NutMap change = NutMap.NEW();
|
||||
@@ -543,6 +463,20 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
|
||||
return history;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断历史变更字段是否为业务空值。
|
||||
* null、空字符串、纯空格在页面上都会展示为空,统一视为无有效新值,避免生成无意义的空值变更记录。
|
||||
*/
|
||||
private boolean isBlankChangeValue(Object value) {
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
if (value instanceof CharSequence) {
|
||||
return StrUtil.isBlank(value.toString());
|
||||
}
|
||||
return ObjectUtil.isEmpty(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单位变更记录展示值。
|
||||
* 入参 unitId 为系统用户旧单位或数据源新单位ID;返回值优先使用单位名称,查不到名称时保留单位ID,避免变更记录为空导致更新中断。
|
||||
|
||||
+22
-36
@@ -2,6 +2,7 @@ package com.budwk.app.sys.services.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.thread.AsyncUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
@@ -15,18 +16,18 @@ import com.budwk.app.sys.param.SysDataUserUpdateParam;
|
||||
import com.budwk.app.sys.services.SysDataUserUpdateService;
|
||||
import com.budwk.app.sys.services.SysRoleService;
|
||||
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
|
||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
|
||||
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.random.R;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Calendar;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
@@ -41,6 +42,7 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
|
||||
private SysRoleService sysRoleService;
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public String update(SysDataUserUpdateParam updateParam) {
|
||||
// 基本SQL查询
|
||||
Cnd cnd = Cnd.where("pullTime", "=", updateParam.getPullTime())
|
||||
@@ -67,34 +69,6 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
|
||||
String pwd = PwdUtil.generate(12);
|
||||
sysUser.setPassword(PwdUtil.getPassword(pwd, salt));
|
||||
sysUser.setLoginCount(0);
|
||||
|
||||
// 检查是否符合会员条件
|
||||
// 1. 用户状态为"在岗"
|
||||
if ("在岗".equals(sysUser.getUserState())) {
|
||||
// 2. 检查聘用方式条件
|
||||
List<String> membershipQualifyingPreparedBy = Arrays.asList("新人事代理", "校聘合同制", "事业编制");
|
||||
|
||||
// 第一种情况:聘用方式在指定列表中
|
||||
boolean isQualifiedByPreparedBy = membershipQualifyingPreparedBy.contains(sysUser.getPreparedBy());
|
||||
|
||||
// 第二种情况:聘用方式是博士后,且进站时间不超过两年
|
||||
boolean isQualifiedPostdoc = false;
|
||||
if ("博士后".equals(sysUser.getPreparedBy()) && sysUser.getPostDoctoralJoinDate() != null) {
|
||||
Calendar twoYearsAgo = Calendar.getInstance();
|
||||
twoYearsAgo.add(Calendar.YEAR, -2);
|
||||
Date twoYearsAgoDate = twoYearsAgo.getTime();
|
||||
isQualifiedPostdoc = sysUser.getPostDoctoralJoinDate().after(twoYearsAgoDate);
|
||||
}
|
||||
|
||||
// 如果满足任一条件,设置为会员
|
||||
if (isQualifiedByPreparedBy || isQualifiedPostdoc) {
|
||||
sysUser.setMember(true);
|
||||
} else {
|
||||
sysUser.setMember(false);
|
||||
}
|
||||
} else {
|
||||
sysUser.setMember(false);
|
||||
}
|
||||
}
|
||||
|
||||
List<List<Sys_user>> splitSysUsers = ListUtil.split(sysUsers, 500);
|
||||
@@ -102,6 +76,18 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
|
||||
//等待任务全部完成
|
||||
AsyncUtil.waitAll(CompletableFuture.allOf(splitSysUsersFutures.toArray(new CompletableFuture[0])));
|
||||
|
||||
// 增量更新只处理新增人员,需要同步写入用户历史记录;列表的变更类型、变更时间通过 sys_user_history 最新记录展示。
|
||||
List<Sys_user_history> histories = BeanUtil.copyToList(userSources, Sys_user_history.class);
|
||||
for (Sys_user_history history : histories) {
|
||||
history.setId(R.UU32());
|
||||
history.setChangeTypes(List.of(MemberChangeType.NEW.name()));
|
||||
history.setChangeTime(DateUtil.date());
|
||||
history.setChangeOrigin(MemberChangeOrigin.SYSTEM.name());
|
||||
}
|
||||
List<List<Sys_user_history>> splitHistories = ListUtil.split(histories, 500);
|
||||
List<CompletableFuture<List<Sys_user_history>>> splitHistoryFutures = splitHistories.stream().map(v -> CompletableFuture.supplyAsync(() -> dao.fastInsert(v))).toList();
|
||||
AsyncUtil.waitAll(CompletableFuture.allOf(splitHistoryFutures.toArray(new CompletableFuture[0])));
|
||||
|
||||
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
|
||||
|
||||
//添加公共角色
|
||||
@@ -143,15 +129,15 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
|
||||
dao.insert(insertUnits);
|
||||
});
|
||||
|
||||
// 计算设置为会员的人数
|
||||
long memberCount = sysUsers.stream().filter(Sys_user::getMember).count();
|
||||
|
||||
// 6.1 更新提案的校领导角色,发送订阅
|
||||
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
|
||||
for (String unitId : config.getSchoolLeaderUnitIds()) {
|
||||
RoleEventPublisher.broadcast(new RoleEventMsg(unitId, RoleConstant.PROPOSAL_BRANCH_SCHOOL_LEADER.name(), RoleEventMsg.RENEW_ROLE));
|
||||
}
|
||||
|
||||
return StrUtil.format("增量更新成功,本次更新新入职老师{}人,新增单位{}个,设置为会员{}人。", sysUsers.size(), insertUnits.size(), memberCount);
|
||||
if (sysUsers.isEmpty() && insertUnits.isEmpty()) {
|
||||
return "本次没有新增人员或单位。";
|
||||
}
|
||||
return StrUtil.format("增量更新成功,本次新增人员{}人,新增单位{}个。", sysUsers.size(), insertUnits.size());
|
||||
}
|
||||
}
|
||||
|
||||
+11
-4
@@ -80,8 +80,9 @@ public class ProposalUnderTakeReplyController {
|
||||
public Result pageData(@Valid ProposalSearchParam pageForm, boolean approval, boolean transfer) {
|
||||
/*
|
||||
* 承办单位答复列表需要按“承办单位”维度展示。
|
||||
* 同一个任务可能挂了多个承办单位负责人,若只按 taskId 分组会把多条待审核合并成一条,
|
||||
* 因此这里优先使用任务参与人的承办单位信息拆行,历史数据缺失时再回退到流程变量。
|
||||
* 新流程生成的会签任务可能未写入 underTakeName、underTakeId、underTakeIsMaster,
|
||||
* 因此优先读取任务变量,变量缺失时再按“提案ID + 任务参与人单位ID”关联承办单位记录兜底,
|
||||
* 兼容已经生成的历史待办,同时保持原有任务变量的数据优先级。
|
||||
*/
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
@@ -107,8 +108,13 @@ public class ProposalUnderTakeReplyController {
|
||||
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName,
|
||||
IFNULL(GROUP_CONCAT(DISTINCT nt.taskName),'结束') curTaskKey,
|
||||
CASE WHEN t.taskState = 20 AND ( rt.id IS NULL OR rt.taskState = 10 ) THEN 1 ELSE 0 END AS canRevoke,
|
||||
t.variable->>'$.underTakeName' AS underTakeName,
|
||||
IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') = true, 1, 0) AS underTakeIsMaster,
|
||||
COALESCE(NULLIF(t.variable->>'$.underTakeName', ''), replyUnit.unitName) AS underTakeName,
|
||||
COALESCE(NULLIF(t.variable->>'$.underTakeId', ''), replyUnit.unitId) AS underTakeId,
|
||||
CASE
|
||||
WHEN JSON_EXTRACT(t.variable, '$.underTakeIsMaster') IS NOT NULL
|
||||
THEN IF(JSON_EXTRACT(t.variable, '$.underTakeIsMaster') = true, 1, 0)
|
||||
ELSE IFNULL(replyUnit.isMaster, 0)
|
||||
END AS underTakeIsMaster,
|
||||
t.variable->>'$.tf_transferUserId' AS tf_transferUserId,
|
||||
transferUser.username AS transferUserName,
|
||||
GROUP_CONCAT(DISTINCT ta.actorName, '(', ta.actorAccount, ')' ) AS auditUser
|
||||
@@ -119,6 +125,7 @@ public class ProposalUnderTakeReplyController {
|
||||
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
|
||||
LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id
|
||||
LEFT JOIN proposal_info info ON info.id = ins.businessNo
|
||||
LEFT JOIN proposal_reply_unit replyUnit ON replyUnit.proposalId = info.id AND replyUnit.unitId = ta.actorUnitId
|
||||
LEFT JOIN proposal_type type on type.id = info.typeId
|
||||
LEFT JOIN proposal_merge mer ON mer.proposalId = info.id
|
||||
LEFT JOIN teacher_congress_session tcs on tcs.id = info.sessionId
|
||||
|
||||
+21
-6
@@ -6,6 +6,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.flow.constant.FlowConst;
|
||||
import com.budwk.app.flow.engine.AssignmentHandler;
|
||||
import com.budwk.app.flow.engine.core.Execution;
|
||||
import com.budwk.app.flow.engine.core.ServiceContext;
|
||||
@@ -36,12 +37,7 @@ public class ProposalUnitAssignmentHandler implements AssignmentHandler {
|
||||
SysRoleService sysRoleService = ServiceContext.find(SysRoleService.class);
|
||||
String proposalId = execution.getProcessInstance().getBusinessNo();
|
||||
|
||||
// 当前节点需要同时处理主办和协办负责人。
|
||||
// 这里不能按单位ID去重,必须保留每个承办单位对应的一条分派记录,
|
||||
// 否则同一用户负责多个单位时,会被错误合并成更少的待办。
|
||||
List<String> unitIds = new ArrayList<>();
|
||||
unitIds.addAll(resolveMasterUnitIds(execution, dao, proposalId));
|
||||
unitIds.addAll(resolveSlaveUnitIds(execution, dao, proposalId));
|
||||
List<String> unitIds = resolveUnitIds(model, execution, dao, proposalId);
|
||||
|
||||
if (unitIds.isEmpty()) {
|
||||
throw new BaseException("提案没有配置承办单位");
|
||||
@@ -76,6 +72,25 @@ public class ProposalUnitAssignmentHandler implements AssignmentHandler {
|
||||
return assignee;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定当前答复节点需要分派的单位范围。
|
||||
* 二次答复页面已明确提交 tf_secondReplyUnitIds 时,该字段就是唯一分派范围;
|
||||
* 未选择的主办或协办单位不能再从历史承办单位记录中补回。
|
||||
* 旧流程没有该字段时,继续按主办、协办配置回退,保证历史流程可以正常运行。
|
||||
*/
|
||||
private List<String> resolveUnitIds(TaskModel model, Execution execution, Dao dao, String proposalId) {
|
||||
String secondReplyUnitIdsKey = FlowConst.TASK_FORM_DATA_PREFIX + "secondReplyUnitIds";
|
||||
if ("two_unit_reply".equals(model.getName()) && execution.getArgs().containsKey(secondReplyUnitIdsKey)) {
|
||||
return getStringList(execution.getArgs(), secondReplyUnitIdsKey);
|
||||
}
|
||||
|
||||
// 初次答复需要同时处理主办和协办负责人,旧二次答复流程也沿用该兼容逻辑。
|
||||
List<String> unitIds = new ArrayList<>();
|
||||
unitIds.addAll(resolveMasterUnitIds(execution, dao, proposalId));
|
||||
unitIds.addAll(resolveSlaveUnitIds(execution, dao, proposalId));
|
||||
return unitIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先从当前流程变量读取主办单位,读不到时回落到承办单位记录表,兼容重新流转场景。
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,11 @@ public class ProposalInfo extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String code;
|
||||
|
||||
@Column
|
||||
@Comment("立案编号")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String caseFilingCode;
|
||||
|
||||
@Column
|
||||
@Comment("提案名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
|
||||
+32
-2
@@ -76,6 +76,8 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
|
||||
throw new BaseException("没有进行中的流程任务");
|
||||
}
|
||||
|
||||
validateAndNormalizeCaseFilingCode(args);
|
||||
|
||||
String proposalId = args.getStr("proposalId");
|
||||
List<String> proposalIds = proposalCommonService.mergeProposal(proposalId);
|
||||
if (ObjectUtil.isEmpty(proposalIds)) {
|
||||
@@ -98,16 +100,44 @@ public class ProposalCommitteeFilingUnitServiceImpl extends BaseServiceImpl<Prop
|
||||
|
||||
syncCommitteeFilingUnitData(proposalIds, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* old-flow 页面提交后,需要把立案结果以及主协办单位同步落业务表。
|
||||
* 校验并规范化立案编号。确定立案时必须传入不超过 20 个字符的编号;
|
||||
* 其他立案结果不保存编号,避免切换选项后把历史输入带入业务表和流程变量。
|
||||
*
|
||||
* @param args 流程提交参数,其中 tf_caseFilingResult 为立案结果,tf_caseFilingCode 为手工填写的立案编号
|
||||
*/
|
||||
private void validateAndNormalizeCaseFilingCode(Dict args) {
|
||||
String caseFilingResultKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult";
|
||||
String caseFilingCodeKey = FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode";
|
||||
if (!"CONFIRM_FILING".equals(args.getStr(caseFilingResultKey))) {
|
||||
args.put(caseFilingCodeKey, "");
|
||||
return;
|
||||
}
|
||||
|
||||
String caseFilingCode = StrUtil.trim(args.getStr(caseFilingCodeKey));
|
||||
if (StrUtil.isBlank(caseFilingCode)) {
|
||||
throw new BaseException("立案编号不能为空");
|
||||
}
|
||||
if (caseFilingCode.length() > 20) {
|
||||
throw new BaseException("立案编号不能超过20个字符");
|
||||
}
|
||||
args.put(caseFilingCodeKey, caseFilingCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* old-flow 页面提交后,需要把立案编号、立案结果以及主协办单位同步落业务表。
|
||||
* 若这次改成“不予立案”等无需承办单位的结果,也要先清掉旧的承办单位记录,避免脏数据残留。
|
||||
*/
|
||||
private void syncCommitteeFilingUnitData(List<String> proposalIds, Dict args) {
|
||||
String caseFilingCode = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingCode");
|
||||
String caseFilingResult = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingResult");
|
||||
String caseFilingType = args.getStr(FlowConst.TASK_FORM_DATA_PREFIX + "caseFilingType");
|
||||
|
||||
dao().update(ProposalInfo.class,
|
||||
Chain.make("caseFilingResult", caseFilingResult).add("caseFilingType", caseFilingType),
|
||||
Chain.make("caseFilingCode", caseFilingCode)
|
||||
.add("caseFilingResult", caseFilingResult)
|
||||
.add("caseFilingType", caseFilingType),
|
||||
Cnd.where(ProposalInfo::getId, "in", proposalIds));
|
||||
|
||||
dao().clear(ProposalReplyUnit.class, Cnd.where(ProposalReplyUnit::getProposalId, "in", proposalIds));
|
||||
|
||||
+19
-9
@@ -83,15 +83,18 @@ public class TeacherCongressInstitutionController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单中的树结构
|
||||
* 获取新增机构表单使用的固定机构字典树。
|
||||
*
|
||||
* @param sessionId
|
||||
* @return
|
||||
* @param sessionId 当前教代会届次ID,用于保持接口参数与机构管理页面上下文一致
|
||||
* @return Result,data.treeList 为级联组件树结构,data.treeFlat 为名称和代码回填使用的扁平字典列表
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("tc.institution")
|
||||
public Result formTree(@Valid String sessionId) {
|
||||
Sys_dict institutionFixedDict = sysDictService.fetch(Cnd.where(Sys_dict::getCode, "=", "TEACHER_CONGRESS_INSTITUTION_FIXED"));
|
||||
if (ObjectUtil.isEmpty(institutionFixedDict)) {
|
||||
return Result.error("未配置双代会固定机构字典");
|
||||
}
|
||||
List<Sys_dict> children = sysDictService.query(Cnd.where(Sys_dict::getPath, "like", institutionFixedDict.getPath() + "%"));
|
||||
List<TreeNode<String>> treeNodes = children.stream().map(sysDict -> {
|
||||
TreeNode<String> treeNode = new TreeNode<>(sysDict.getId(), sysDict.getParentId(), sysDict.getName(), sysDict.getLocation());
|
||||
@@ -134,19 +137,24 @@ public class TeacherCongressInstitutionController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加机构
|
||||
* 添加当前届次的机构。
|
||||
*
|
||||
* @param institution
|
||||
* @return
|
||||
* @param institution 机构数据,包含 sessionId、name、code、parentId、location 和 introduce;parentId 可能是固定字典ID
|
||||
* @return Result,机构重复或父级不存在时返回错误,否则返回成功
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("tc.institution")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result insert(Teacher_congress_institution institution) {
|
||||
if (dao.count(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getCode, "=", institution.getCode()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())) > 0) {
|
||||
return Result.error("机构已存在");
|
||||
}
|
||||
if (StrUtil.isNotBlank(institution.getParentId()) && !institution.getParentId().equals("0") && dao.count(Teacher_congress_institution.class, Cnd.where(Teacher_congress_institution::getId, "=", institution.getParentId()).and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())) == 0) {
|
||||
// 级联框提交的是固定字典父级ID,需要按机构代码匹配当前届次的实际父机构。
|
||||
Sys_dict sysDict = sysDictService.fetch(Cnd.where(Sys_dict::getId, "=", institution.getParentId()));
|
||||
if (ObjectUtil.isEmpty(sysDict)) {
|
||||
return Result.error("父级机构不存在");
|
||||
}
|
||||
Teacher_congress_institution parentInstitution = dao.fetch(Teacher_congress_institution.class,
|
||||
Cnd.where(Teacher_congress_institution::getCode, "=", sysDict.getCode())
|
||||
.and(Teacher_congress_institution::getSessionId, "=", institution.getSessionId())
|
||||
@@ -162,13 +170,14 @@ public class TeacherCongressInstitutionController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除机构
|
||||
* 更新机构名称、描述和排序。
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @param institution 机构数据,id 标识待更新机构,name、introduce、location 为可更新内容
|
||||
* @return Result,机构不存在时返回错误,否则返回成功
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("tc.institution")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result update(Teacher_congress_institution institution) {
|
||||
Teacher_congress_institution dbInstitution = dao.fetch(Teacher_congress_institution.class, institution.getId());
|
||||
if (ObjectUtil.isEmpty(dbInstitution)) {
|
||||
@@ -267,6 +276,7 @@ public class TeacherCongressInstitutionController {
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("tc.institution")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result userDelete(@Valid String id) {
|
||||
teacherCongressInstitutionUserService.userDelete(id);
|
||||
return Result.success();
|
||||
|
||||
+13
-1
@@ -61,6 +61,18 @@ public class TeacherCongressSessionController {
|
||||
return Result.success(pagination);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询历史届次已保存的提案类型,不需要请求参数。
|
||||
*
|
||||
* @return VO 列表,每项的 proposalType 字段表示一个下拉选项
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("tc.prepare.session")
|
||||
@ApiOperation(value = "查询届次提案类型选项")
|
||||
public Result listProposalTypes() {
|
||||
return Result.success(teacherCongressSessionService.listProposalTypes());
|
||||
}
|
||||
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("tc.prepare.session")
|
||||
@@ -74,7 +86,7 @@ public class TeacherCongressSessionController {
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("tc.prepare.session")
|
||||
@ApiOperation(value = "修改届次信息")
|
||||
public Result update(Teacher_congress_session session) {
|
||||
public Result update(@Valid Teacher_congress_session session) {
|
||||
int count = dao.count(Teacher_congress_session.class,
|
||||
Cnd.where(Teacher_congress_session::getJ, "=", session.getJ())
|
||||
.and(Teacher_congress_session::getC, "=", session.getC())
|
||||
|
||||
+7
@@ -43,6 +43,13 @@ public class Teacher_congress_session extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String fullName;
|
||||
|
||||
@Column
|
||||
@Comment("提案类型")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 50)
|
||||
@NotBlank(message = "提案类型不能为空")
|
||||
@Size(max = 50, message = "提案类型最多50个字")
|
||||
private String proposalType;
|
||||
|
||||
@Column
|
||||
@Comment("教代会开启时间")
|
||||
@ColDefine(type = ColType.DATE)
|
||||
|
||||
+10
@@ -1,13 +1,23 @@
|
||||
package com.budwk.app.zhgh.democratic.teachercongress.prepare.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.prepare.controller.vo.TeacherCongressSessionProposalTypeVO;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 教代会届次
|
||||
*/
|
||||
public interface TeacherCongressSessionService extends BaseService<Teacher_congress_session> {
|
||||
|
||||
/**
|
||||
* 查询届次中已经保存过的提案类型,供可输入下拉框复用。
|
||||
*
|
||||
* @return 提案类型 VO 列表,每项的 proposalType 字段为一个可选类型
|
||||
*/
|
||||
List<TeacherCongressSessionProposalTypeVO> listProposalTypes();
|
||||
|
||||
void insertSession(Teacher_congress_session session, boolean isExtend);
|
||||
|
||||
void deleteSession(String id);
|
||||
|
||||
+18
@@ -24,12 +24,15 @@ import com.budwk.app.zhgh.democratic.teachercongress.institution.models.Teacher_
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.institution.models.Teacher_congress_institution_user;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.meetings.models.Teacher_congress_meeting;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.meetings.models.Teacher_congress_meeting_file;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.prepare.controller.vo.TeacherCongressSessionProposalTypeVO;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_prepare_file;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.prepare.models.Teacher_congress_session;
|
||||
import com.budwk.app.zhgh.democratic.teachercongress.prepare.service.TeacherCongressSessionService;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
@@ -52,6 +55,21 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl<Teacher_c
|
||||
super(dao);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取历史届次中非空的提案类型并去重,用户既可复用已有类型,也可在前端输入新类型。
|
||||
*
|
||||
* @return 提案类型 VO 列表,每项只包含 proposalType 字段
|
||||
*/
|
||||
@Override
|
||||
public List<TeacherCongressSessionProposalTypeVO> listProposalTypes() {
|
||||
Sql sql = Sqls.create("SELECT DISTINCT proposalType FROM teacher_congress_session " +
|
||||
"WHERE proposalType IS NOT NULL AND TRIM(proposalType) <> '' ORDER BY proposalType");
|
||||
sql.setCallback(Sqls.callback.strList());
|
||||
dao().execute(sql);
|
||||
return sql.getList(String.class).stream()
|
||||
.map(TeacherCongressSessionProposalTypeVO::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
|
||||
+7
-1
@@ -91,7 +91,13 @@ public class AidFundChangeRecordController {
|
||||
$condition
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("us.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType());
|
||||
if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) {
|
||||
SqlExpressionGroup seg = new SqlExpressionGroup();
|
||||
seg.or("us.loginname", "like", "%" + pageForm.getSearchKeyword() + "%");
|
||||
seg.or("us.username", "like", "%" + pageForm.getSearchKeyword() + "%");
|
||||
cnd.and(seg);
|
||||
}
|
||||
cnd.andEX("IFNULL(info.aidFundMemberUserType, us.aidFundMemberUserType)", "=", pageForm.getAidFundMemberUserType());
|
||||
cnd.andEX("us.unitid", "=", pageForm.getUnitId());
|
||||
cnd.andEX("us.unionid", "=", pageForm.getUnionId());
|
||||
cnd.andEX("info.changeType", "=", pageForm.getChangeType());
|
||||
|
||||
Reference in New Issue
Block a user