diff --git a/jeecg-boot-module/jeecg-module-flow/pom.xml b/jeecg-boot-module/jeecg-module-flow/pom.xml index 608b87b..4512356 100644 --- a/jeecg-boot-module/jeecg-module-flow/pom.xml +++ b/jeecg-boot-module/jeecg-module-flow/pom.xml @@ -36,6 +36,10 @@ org.jeecgframework.boot jeecg-system-biz + + org.jeecgframework.boot + jeecg-module-supervision + \ No newline at end of file diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakConfig.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakConfig.java index d5f6ef8..4cbba04 100644 --- a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakConfig.java +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakConfig.java @@ -31,5 +31,10 @@ public class XiSpeakConfig { private String implDeptCollectionUsedKey; /** 对应sdw-role-id*/ private String sdwRoleId; + /** 对应business-key*/ + private String businessKey; + /** 对应impl-dept-key-underscore-key*/ + private String implDeptKeyUnderscoreKey; + private String deptWorkerKey; } } diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakFlow.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakFlow.java index 61576fd..1645eda 100644 --- a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakFlow.java +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/XiSpeakFlow.java @@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.util.StringUtils; import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.DelegateExecution; +import org.jeecg.common.constant.SymbolConstant; import org.jeecg.common.system.api.ISysBaseAPI; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -83,8 +85,12 @@ public class XiSpeakFlow { } else { data = JSONObject.parseObject(JSONObject.toJSONString(jsonData)); } - log.info("needSdwApproval结果为{}", data.getInteger(codeName)); - return data == null ? 0 : data.getInteger(codeName); + int finalResult = Optional.ofNullable(data) + .map(d -> d.getInteger(codeName)) + .orElse(0); + + log.info("needSdwApproval结果为{}", finalResult); + return finalResult; } catch (Exception e) { log.warn("getJsondata parse failed, codeName={}, jsonData={}", codeName, jsonData, e); return 0; @@ -154,4 +160,42 @@ public class XiSpeakFlow { return resultList.size(); } + + public List getDeptWorkerList(DelegateExecution execution) { + String variableName = xiSpeakConfig.getXiSpeak().getDeptWorkerKey(); + if (execution == null || org.apache.commons.lang.StringUtils.isBlank(variableName)) { + return Collections.emptyList(); + } + + try { + // 1. 优先尝试从当前 Execution 的局部作用域获取 + // 在多实例(会签)中,每个分支特有的变量(如 item 变量)通常存储在这里 + Object value = execution.getVariableLocal(variableName); + + // 2. 如果局部没有,直接使用 getVariable 获取 + // 该方法会自动向上追溯:当前 Execution -> 父 Execution -> 流程实例(Global) + if (value == null) { + value = execution.getVariable(variableName); + } + + // 3. 结果处理 + if (value != null) { + return splitUsernames(value); + } + } catch (Exception e) { + log.warn("getSonProcessVariable 失败, executionId={}, variableName={}", + execution.getId(), variableName, e); + } + + return Collections.emptyList(); + } + private List splitUsernames(Object value) { + if (value == null) { + return Collections.emptyList(); + } + return Arrays.stream(String.valueOf(value).split(SymbolConstant.COMMA)) + .map(String::trim) + .filter(org.apache.commons.lang.StringUtils::isNotBlank) + .collect(Collectors.toList()); + } } diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterImplDeptLeaderApproveListener.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterImplDeptLeaderApproveListener.java new file mode 100644 index 0000000..0a302bb --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterImplDeptLeaderApproveListener.java @@ -0,0 +1,131 @@ +package org.jeecg.xispeak.listener; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService; +import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression; +import lombok.RequiredArgsConstructor; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.ExecutionListener; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals; +import org.jeecg.modules.tasktask.entity.TaskTask; +import org.jeecg.modules.tasktask.service.ITaskTaskService; +import org.jeecg.xispeak.XiSpeakConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Component("AfterImplDeptLeaderApproveListener") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class AfterImplDeptLeaderApproveListener implements TaskListener { + + private static final long serialVersionUID = 1L; + private final FlowNodeExpression flowNodeExpression; + private final RuntimeService runtimeService; + private final XiSpeakConfig xiSpeakConfig; + private final ITaskTaskService taskTaskService; + private final IBgXiSpeakService bgXiSpeakService; + +// @Override +// @Transactional(rollbackFor = Exception.class) +// public void notify(DelegateTask delegateTask) { +// +// // 1. 获取审批人 +// String currentAssignee = delegateTask.getAssignee(); +// if (StringUtils.isEmpty(currentAssignee)) { +// // 使用你给出的正确获取方式:扫描 Execution 树获取局部变量 +// return; +// } +// +// // 2. 获取业务主键:通常业务 ID 是全局变量,可以直接 getVariable +// Object rawTaskTaskId = delegateTask.getVariable(xiSpeakConfig.getXiSpeak().getBusinessKey()); +// +// if (StringUtils.isEmpty(currentAssignee) || rawTaskTaskId == null) { +// log.warn("无法获取有效的审批人或业务ID,ExecutionId: {}, TaskId: {}", +// delegateTask.getExecutionId(), delegateTask.getId()); +// return; +// } +// +// // 3. 获取业务实体 +// BgXiSpeak xiSpeak = bgXiSpeakService.getById(rawTaskTaskId.toString()); +// if (xiSpeak == null) return; +// +// // 4. 处理数据结构 +// Map deptApproveDetailMap = Optional.ofNullable(xiSpeak.getApproveInfo()) +// .orElse(new HashMap<>()); +// +// // 5. 获取实施部门 ID:同样使用全路径搜索,确保在嵌套会签中能精准定位局部变量 +// String implDeptKey = xiSpeakConfig.getXiSpeak().getImplDeptCollectionUsedKey(); +// String rawDeptVar = delegateTask.getVariable(implDeptKey).toString(); +// +// if (StringUtils.isBlank(rawDeptVar)) { +// throw new RuntimeException("流程数据异常:未找到实施部门ID变量: " + implDeptKey); +// } +// +// // 清洗字符串(处理 [id1, id2] 格式) +// String cleanDeptId = rawDeptVar.replace("[", "").replace("]", "").trim(); +// +// // computeIfAbsent 的特性:如果 key 存在,返回 value;如果不存在,运行 lambda 并存入再返回。 +// DeptApproveDetail detail = deptApproveDetailMap.computeIfAbsent(cleanDeptId, k -> new DeptApproveDetail()); +// +// // 2. 只有当 approverName 为空时(即该对象是刚新建的,或者之前没存过审批人),才进行赋值 +// if (StringUtils.isBlank(detail.getApproverName())) { +// detail.setApproverName(currentAssignee); +// +// // 3. 只有发生了赋值,才执行数据库更新(可选,为了性能) +// xiSpeak.setApproveInfo(deptApproveDetailMap); +// bgXiSpeakService.updateById(xiSpeak); +// log.info("部门 {} 首次审批,已记录审批人: {}", cleanDeptId, currentAssignee); +// } else { +// log.info("部门 {} 已存在审批记录: {},跳过本次回写", cleanDeptId, detail.getApproverName()); +// } +// } + + @Transactional(rollbackFor = Exception.class) + public void notify(DelegateTask delegateTask) { + // 1. 获取基础数据 + String currentAssignee = delegateTask.getAssignee(); + Object rawTaskTaskId = delegateTask.getVariable(xiSpeakConfig.getXiSpeak().getBusinessKey()); + String implDeptKey = xiSpeakConfig.getXiSpeak().getImplDeptCollectionUsedKey(); + String rawDeptVar = delegateTask.getVariable(implDeptKey).toString(); + String cleanDeptId = rawDeptVar.replace("[", "").replace("]", "").trim(); + + // 2. 【核心】使用行锁查询,确保拿到的是数据库此刻最真实、最新的数据 + // 同时也让其他并发的审批线程在这里“排队”等待 + BgXiSpeak xiSpeak = bgXiSpeakService.getByIdForUpdate(rawTaskTaskId.toString()); + if (xiSpeak == null) return; + + // 3. 操作 Map + Map map = xiSpeak.getApproveInfo(); + if (map == null) map = new HashMap<>(); + + // 4. 获取或创建详情对象 + // 使用你定义的 POJO + DeptApproveDetail detail = map.getOrDefault(cleanDeptId, new DeptApproveDetail()); + + // 只有没审批过才记录(防止重复触发覆盖已有数据) + if (StringUtils.isBlank(detail.getApproverName())) { + detail.setDeptId(cleanDeptId); + detail.setApproverName(currentAssignee); + // 这里可以根据需要设置其他字段,比如 detail.setApproverId(...) + + map.put(cleanDeptId, detail); + xiSpeak.setApproveInfo(map); + + // 5. 写回数据库 + // 因为有了 FOR UPDATE,这里的更新绝对是基于最新数据的增量合并 + bgXiSpeakService.updateById(xiSpeak); + } + } +} \ No newline at end of file diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterImplWorkerApproveListener.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterImplWorkerApproveListener.java new file mode 100644 index 0000000..017011e --- /dev/null +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterImplWorkerApproveListener.java @@ -0,0 +1,119 @@ +package org.jeecg.xispeak.listener; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.task.service.delegate.DelegateTask; +import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak; +import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetail; +import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService; +import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression; +import lombok.RequiredArgsConstructor; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.ExecutionListener; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals; +import org.jeecg.modules.tasktask.entity.TaskTask; +import org.jeecg.modules.tasktask.service.ITaskTaskService; +import org.jeecg.xispeak.XiSpeakConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; +@Slf4j +@Component("AfterImplWorkerApproveListener") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class AfterImplWorkerApproveListener implements TaskListener { + + private static final long serialVersionUID = 1L; + private final XiSpeakConfig xiSpeakConfig; + private final IBgXiSpeakService bgXiSpeakService; + + // 引入 Jackson 的 ObjectMapper 用于处理类型转换失败的情况 + private final com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper(); + + @Override + @Transactional(rollbackFor = Exception.class) + public void notify(DelegateTask delegateTask) { + String currentWorkerName = delegateTask.getAssignee(); + Object rawBusinessKey = delegateTask.getVariable(xiSpeakConfig.getXiSpeak().getBusinessKey()); + + if (StringUtils.isEmpty(currentWorkerName) || rawBusinessKey == null) { + log.warn("无法获取有效的审批人或业务ID,略过处理"); + return; + } + + BgXiSpeak xiSpeak = bgXiSpeakService.getByIdForUpdate(rawBusinessKey.toString()); + if (xiSpeak == null) { + log.error("业务数据不存在: {}", rawBusinessKey); + return; + } + + String implDeptKey = xiSpeakConfig.getXiSpeak().getImplDeptCollectionUsedKey(); + Object rawDeptVar = delegateTask.getVariableLocal(implDeptKey); + if (rawDeptVar == null) { + rawDeptVar = delegateTask.getVariable(implDeptKey); + } + + if (rawDeptVar == null) { + log.error("流程数据异常:未找到实施部门ID变量: {}", implDeptKey); + return; + } + + String cleanDeptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim(); + + // --- 修改开始:处理复杂的 Map 转换问题 --- + + // 1. 获取原始 Map (这里使用通配符,避免直接强转报错) + Map rawApproveMap = (Map) xiSpeak.getApproveInfo(); + if (rawApproveMap == null) { + rawApproveMap = new HashMap<>(); + } + + // 2. 获取该部门对应的详情,并进行安全类型检查 + Object rawDetail = rawApproveMap.get(cleanDeptId); + DeptApproveDetail detail; + + if (rawDetail == null) { + // 情况 A: 部门记录不存在,新建 + detail = new DeptApproveDetail(); + detail.setDeptId(cleanDeptId); + detail.setImplUserNameList(new ArrayList<>()); + rawApproveMap.put(cleanDeptId, detail); + } else if (rawDetail instanceof DeptApproveDetail) { + // 情况 B: 类型正确,直接使用 + detail = (DeptApproveDetail) rawDetail; + } else { + // 情况 C: 就是你遇到的报错点!类型是 LinkedHashMap,需要转换 + log.debug("检测到 LinkedHashMap 类型,执行手动转换..."); + detail = objectMapper.convertValue(rawDetail, DeptApproveDetail.class); + // 转换后放回 Map,防止后续逻辑再次触发转换 + rawApproveMap.put(cleanDeptId, detail); + } + + // --- 修改结束 --- + + // 5. 更新落实人列表 + List workerList = detail.getImplUserNameList(); + if (workerList == null) { + workerList = new ArrayList<>(); + } + + if (!workerList.contains(currentWorkerName)) { + workerList.add(currentWorkerName); + detail.setImplUserNameList(workerList); + + // 重新写回 Map 并保存 + // 声明:虽然 rawApproveMap 里的 Value 是 Object,但写回数据库时 TypeHandler 会处理它 + xiSpeak.setApproveInfo((Map) rawApproveMap); + bgXiSpeakService.updateById(xiSpeak); + log.info("部门 {} 经办人审批记录更新成功: {}", cleanDeptId, currentWorkerName); + } else { + log.info("部门 {} 经办人 {} 已在列表中,跳过更新", cleanDeptId, currentWorkerName); + } + } +} \ No newline at end of file diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterSDWApproveHqListener.java b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterSDWApproveHqListener.java index 9d13a44..17557d5 100644 --- a/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterSDWApproveHqListener.java +++ b/jeecg-boot-module/jeecg-module-flow/src/main/java/org/jeecg/xispeak/listener/AfterSDWApproveHqListener.java @@ -1,4 +1,4 @@ -package org.jeecg.modules.extbpm.listener.execution; +package org.jeecg.xispeak.listener; import lombok.extern.slf4j.Slf4j; import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression; diff --git a/jeecg-boot-module/jeecg-module-flow/src/main/resources/application-flow.yml b/jeecg-boot-module/jeecg-module-flow/src/main/resources/application-flow.yml index b4b7d1d..d6111c3 100644 --- a/jeecg-boot-module/jeecg-module-flow/src/main/resources/application-flow.yml +++ b/jeecg-boot-module/jeecg-module-flow/src/main/resources/application-flow.yml @@ -5,5 +5,10 @@ flow-biz: need-sdw-approve-code: "needSdwApproval" json-data-key: "json_data" impl-dept-key: "implDept" + impl-dept-key-underscore-key: "impl_dept" impl-dept-collection-used-key: "impl_dept_id" sdw-role-id: "2044676455280570370" + business-key: "business_id" + dept-worker-key: "subApproveUser" + + diff --git a/jeecg-boot-module/pom.xml b/jeecg-boot-module/pom.xml index dcfafbc..09f9d60 100644 --- a/jeecg-boot-module/pom.xml +++ b/jeecg-boot-module/pom.xml @@ -18,6 +18,7 @@ jeecg-boot-module-airag + jeecg-module-flow diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/BgXiSpeak.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/BgXiSpeak.java index 511c5b8..3b071cc 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/BgXiSpeak.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/BgXiSpeak.java @@ -3,10 +3,9 @@ package org.jeecg.modules.bg.xispeak.entity; import java.io.Serializable; import java.util.Date; import java.math.BigDecimal; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.annotation.TableLogic; + +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import org.jeecg.common.constant.ProvinceCityArea; import org.jeecg.common.util.SpringContextUtils; import lombok.Data; @@ -17,6 +16,8 @@ import org.jeecg.common.aspect.annotation.Dict; import io.swagger.v3.oas.annotations.media.Schema; import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Map; /** * @Description: 习总书记重要讲话指示批示情况 @@ -25,7 +26,7 @@ import java.io.UnsupportedEncodingException; * @Version: V1.0 */ @Data -@TableName("bg_xi_speak") +@TableName(value = "bg_xi_speak", autoResultMap = true) @Schema(description="习总书记重要讲话指示批示情况") public class BgXiSpeak implements Serializable { private static final long serialVersionUID = 1L; @@ -165,4 +166,7 @@ public class BgXiSpeak implements Serializable { @Excel(name = "是否需要sdw审批", width = 15) @Schema(description = "是否需要sdw审批") private java.lang.Integer needSdwApproval; + + @TableField(typeHandler = JacksonTypeHandler.class) + private Map approveInfo; } diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/DeptApproveDetail.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/DeptApproveDetail.java new file mode 100644 index 0000000..217ba1d --- /dev/null +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/entity/DeptApproveDetail.java @@ -0,0 +1,17 @@ +package org.jeecg.modules.bg.xispeak.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +@Data +@NoArgsConstructor // 必须确保有这个 +@AllArgsConstructor +public class DeptApproveDetail { + private String deptId; // 部门ID + private String approverId; // 审批人ID(从监听器获取的) + private String approverName; // 审批人姓名(可选) + private List implUserIdList; // 落实人Id + private List implUserNameList; // 落实人姓名 +} \ No newline at end of file diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/IBgXiSpeakService.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/IBgXiSpeakService.java index 688714a..a12cff5 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/IBgXiSpeakService.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/IBgXiSpeakService.java @@ -71,4 +71,5 @@ public interface IBgXiSpeakService extends IService { */ List queryListByPid(String pid); + BgXiSpeak getByIdForUpdate(String id); } diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/impl/BgXiSpeakServiceImpl.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/impl/BgXiSpeakServiceImpl.java index 1166be2..c8a6c60 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/impl/BgXiSpeakServiceImpl.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/service/impl/BgXiSpeakServiceImpl.java @@ -216,4 +216,10 @@ public class BgXiSpeakServiceImpl extends ServiceImpl() + .eq(BgXiSpeak::getId, id) + .last("for update")); + } }