!24 Merge branch 'master' into feature/20260519

Merge pull request !24 from new_new_new/feature/20260519
This commit is contained in:
new_new_new
2026-05-22 08:53:41 +00:00
committed by Gitee
20 changed files with 655 additions and 107 deletions
@@ -41,5 +41,6 @@ public class XiSpeakConfig {
private String temImplWorkerKey;
private String temImplLeaderKey;
private String temBgLeaderKey;
private String sdwLeaderListKey;
}
}
@@ -61,8 +61,8 @@ public class XiSpeakFlow {
return userList;
}
public int getSDWLeaderLength() {
return this.getSDWLeader().size();
public int getSDWLeaderLength(Object jsonData) {
return this.getSdwLeaderList(jsonData).size();
}
//流程表达式内用法 ${xiSpeakFlow.getBgDeptLdUserIdListLength()}
@@ -108,6 +108,53 @@ public class XiSpeakFlow {
}
}
private List<String> getListCodeValueFromData(Object jsonData, String codeName, String logLabel) {
// 1. 基础校验:如果入参为空或配置的 Key 为空,直接返回空列表
if (jsonData == null || org.apache.commons.lang.StringUtils.isBlank(codeName)) {
return Collections.emptyList();
}
try {
JSONObject data;
// 2. 统一转换为 JSONObject
if (jsonData instanceof JSONObject) {
data = (JSONObject) jsonData;
} else if (jsonData instanceof String) {
String jsonStr = (String) jsonData;
if (org.apache.commons.lang.StringUtils.isBlank(jsonStr)) {
return Collections.emptyList();
}
data = JSONObject.parseObject(jsonStr);
} else {
// 处理普通 POJO 对象
data = JSONObject.parseObject(JSONObject.toJSONString(jsonData));
}
// 3. 提取目标字段的字符串值
String rawValue = data.getString(codeName);
// 4. 转换逗号分隔的字符串为 List
List<String> resultList;
if (org.apache.commons.lang.StringUtils.isBlank(rawValue)) {
resultList = Collections.emptyList();
} else {
// 使用逗号分割,并过滤掉每个元素前后的空格
resultList = Arrays.stream(rawValue.split(","))
.map(String::trim)
.filter(org.apache.commons.lang.StringUtils::isNotBlank) // 过滤掉因连续逗号产生的空元素
.collect(Collectors.toList());
}
log.info("{} 业务解析结果: {}", logLabel, resultList);
return resultList;
} catch (Exception e) {
log.warn("JSON解析失败: 业务={}, codeKey={}, 数据={}", logLabel, codeName, jsonData, e);
// 异常情况下返回空列表,确保调用方不崩溃
return Collections.emptyList();
}
}
// --- 调用处 ---
//流程表达式内用法${xiSpeakFlow.getNeedSdwApproval(jsonData)}
@@ -122,6 +169,17 @@ public class XiSpeakFlow {
return getCodeValueFromData(jsonData, codeName, "立即反馈");
}
//流程表达式内用法${xiSpeakFlow.getSdwLeaderList(json_data)}
public List<String> getSdwLeaderList(Object jsonData) {
String codeName = xiSpeakConfig.getXiSpeak().getSdwLeaderListKey();
return getListCodeValueFromData(jsonData, codeName, "立即反馈");
}
//流程表达式内用法${xiSpeakFlow.getSdwLeaderListLength(json_data)}
public int getSdwLeaderListLength(Object jsonData) {
return this.getSdwLeaderList(jsonData).size();
}
/**
* 流程表达式内用法: ${xiSpeakFlow.getTemImplWorkerList(execution)}
*/
@@ -0,0 +1,59 @@
package org.jeecg.xispeak.listener;
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.constant.XiSpeakConstant;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService;
import lombok.RequiredArgsConstructor;
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.*;
@Slf4j
@Component("AfterBgWorkerFinalApproveListener")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class AfterBgWorkerFinalApproveListener implements TaskListener {
private static final long serialVersionUID = 1L;
private final XiSpeakConfig xiSpeakConfig;
private final IBgXiSpeakService bgXiSpeakService;
@Override
public void notify(DelegateTask delegateTask) {
// 💡 优化 2: 安全获取变量,使用 Objects.toString 防止流程变量缺失导致 NPE 崩溃
Object businessKeyObj = delegateTask.getVariable(xiSpeakConfig.getXiSpeak().getBusinessKey());
String rawBusinessKey = Objects.toString(businessKeyObj, "").trim();
if (rawBusinessKey.isEmpty()) {
log.warn("【xispeak最终审批监听器】未查询到对应业务表单 id,跳过更新。TaskId: {}", delegateTask.getId());
return;
}
try {
// 💡 优化 3: 极致性能!告别先查后改。直接 new 一个只带 ID 的干净实体进行局部更新
BgXiSpeak updateEntity = new BgXiSpeak();
updateEntity.setId(rawBusinessKey); // 假设你的主键是 String 类型
updateEntity.setCompletionStatus(XiSpeakConstant.CompletionStatus.FINISHED);
// 执行更新
boolean success = bgXiSpeakService.updateById(updateEntity);
if (success) {
log.info("【xispeak最终审批监听器】成功将业务表单 [id={}] 的状态更新为已完成", rawBusinessKey);
} else {
// 如果数据库里根本没这条记录,updateById 会返回 false
log.warn("【xispeak最终审批监听器】更新失败,数据库中不存在 id 为 {} 的业务数据", rawBusinessKey);
}
} catch (Exception e) {
// 💡 优化 4: 增加异常捕获,避免因为业务表更新失败导致整个工作流引擎在推进时抛出未捕获异常
log.error("【xispeak最终审批监听器】更新业务表单异常, businessKey: " + rawBusinessKey, e);
throw e; // 如果希望流程为此阻断、回滚,再将其抛出
}
}
}
@@ -0,0 +1,138 @@
package org.jeecg.xispeak.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.flowable.task.service.delegate.DelegateTask;
import org.flowable.engine.delegate.TaskListener; // 🎯 1. 必须引入标准接口
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetailMap;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService;
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.ArrayList;
import java.util.Collection;
import java.util.Objects;
@Slf4j
@Component("AfterImplDeptLeaderStoreJsonListener")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
// 🎯 2. 显式实现 TaskListener 接口,确保 Flowable 引擎能正常识别并回调
public class AfterImplDeptLeaderStoreJsonListener implements TaskListener {
private final XiSpeakConfig xiSpeakConfig;
private final IBgXiSpeakService bgXiSpeakService;
@Override // 🎯 3. 明确标记重写
@Transactional(rollbackFor = Exception.class)
public void notify(DelegateTask delegateTask) {
String currentWorkerName = delegateTask.getAssignee();
Object rawBusinessKey = delegateTask.getVariable(xiSpeakConfig.getXiSpeak().getBusinessKey());
String taskId = delegateTask.getId();
// 1. 基础边界防御性校验
if (StringUtils.isEmpty(currentWorkerName) || rawBusinessKey == null) {
log.warn("【任务通知】无法获取有效的审批人或业务ID,略过处理。TaskId: {}, Assignee: {}, BusinessKey: {}",
taskId, currentWorkerName, rawBusinessKey);
return;
}
String businessKeyStr = rawBusinessKey.toString();
try {
// 2. 🎯 并发安全提示:
// 如果此节点属于会签/并行节点,建议 bgXiSpeakService.getById() 内部实体类带有 MyBatis-Plus 的 @Version 乐观锁。
// 或者此处若业务允许,仍旧推荐使用悲观锁(如 selectForUpdate),工作流死锁通常可以通过优化流程设计或统一加锁顺序解决。
BgXiSpeak xiSpeak = bgXiSpeakService.getById(businessKeyStr);
if (xiSpeak == null) {
log.error("【任务通知】业务数据不存在,终止处理。BusinessKey: {}", businessKeyStr);
return;
}
// 3. 部门变量安全解析
String implDeptKey = xiSpeakConfig.getXiSpeak().getImplDeptCollectionUsedKey();
Object rawDeptVar = delegateTask.getVariableLocal(implDeptKey);
if (rawDeptVar == null) {
rawDeptVar = delegateTask.getVariable(implDeptKey);
}
if (rawDeptVar == null) {
log.error("【任务通知】流程数据异常:未找到实施部门ID变量: {}, BusinessKey: {}", implDeptKey, businessKeyStr);
return;
}
// 安全解析
String cleanDeptId = parseDeptId(rawDeptVar);
if (StringUtils.isEmpty(cleanDeptId)) {
log.error("【任务通知】解析部门ID为空,跳过更新。RawDeptVar: {}, BusinessKey: {}", rawDeptVar, businessKeyStr);
return;
}
// 4. 安全获取或创建部门审批详情
DeptApproveDetailMap approveMap = xiSpeak.getApproveInfo();
if (approveMap == null) {
approveMap = new DeptApproveDetailMap();
}
DeptApproveDetail detail = approveMap.get(cleanDeptId);
if (detail == null) {
detail = new DeptApproveDetail();
detail.setDeptId(cleanDeptId);
detail.setImplUserNameList(new ArrayList<>());
approveMap.put(cleanDeptId, detail);
}
// 5. 状态比对与幂等更新
if (!Objects.equals(detail.getApproverName(), currentWorkerName)) {
detail.setApproverName(currentWorkerName);
// 写回并更新
xiSpeak.setApproveInfo(approveMap);
// 🎯 如果有乐观锁,更新失败会抛出 OptimisticLockException,从而触发事务回滚与 Flowable 重试机制
bgXiSpeakService.updateById(xiSpeak);
log.info("【任务通知】业务单据 [{}] 部门 [{}] 经办人审批记录成功更新为: {}",
businessKeyStr, cleanDeptId, currentWorkerName);
} else {
log.info("【任务通知】业务单据 [{}] 部门 [{}] 经办人未发生变化 [{}], 跳过数据库更新",
businessKeyStr, cleanDeptId, currentWorkerName);
}
} catch (Exception e) {
log.error(String.format("【任务通知】处理任务结束监听器异常。TaskId: %s, BusinessKey: %s", taskId, businessKeyStr), e);
throw e;
}
}
/**
* 辅助方法:安全解析部门ID
*/
private String parseDeptId(Object rawDeptVar) {
if (rawDeptVar == null) {
return "";
}
// 如果原本就是集合
if (rawDeptVar instanceof Collection) {
Collection<?> collection = (Collection<?>) rawDeptVar;
if (!collection.isEmpty()) {
return Objects.toString(collection.iterator().next(), "").trim();
}
return "";
}
String str = rawDeptVar.toString().trim();
// 🎯 增强防御:如果是 "[1001,1002]" 这种拼成的字符串,只取第一个元素
if (str.startsWith("[") && str.endsWith("]")) {
str = str.substring(1, str.length() - 1);
if (str.contains(",")) {
str = str.split(",")[0];
}
}
return str.trim();
}
}
@@ -8,6 +8,7 @@ import org.flowable.engine.runtime.Execution;
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.entity.DeptApproveDetailMap;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression;
import lombok.RequiredArgsConstructor;
@@ -35,9 +36,6 @@ public class AfterImplWorkerApproveListener implements TaskListener {
private final IBgXiSpeakService bgXiSpeakService;
private final RuntimeService runtimeService;
// 引入 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) {
@@ -78,38 +76,20 @@ public class AfterImplWorkerApproveListener implements TaskListener {
String cleanDeptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim();
// --- 修改开始:处理复杂的 Map 转换问题 ---
// 1. 获取原始 Map (这里使用通配符,避免直接强转报错)
Map<String, Object> rawApproveMap = (Map) xiSpeak.getApproveInfo();
if (rawApproveMap == null) {
rawApproveMap = new HashMap<>();
DeptApproveDetailMap approveMap = xiSpeak.getApproveInfo();
if (approveMap == null) {
approveMap = new DeptApproveDetailMap();
}
// 2. 获取该部门对应的详情,并进行安全类型检查
Object rawDetail = rawApproveMap.get(cleanDeptId);
DeptApproveDetail detail;
if (rawDetail == null) {
// 情况 A: 部门记录不存在,新建
DeptApproveDetail detail = approveMap.get(cleanDeptId);
if (detail == null) {
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);
approveMap.put(cleanDeptId, detail);
}
// --- 修改结束 ---
// 5. 更新落实人列表
// 更新落实人列表
List<String> workerList = detail.getImplUserNameList();
if (workerList == null) {
workerList = new ArrayList<>();
@@ -119,9 +99,7 @@ public class AfterImplWorkerApproveListener implements TaskListener {
workerList.add(currentWorkerName);
detail.setImplUserNameList(workerList);
// 重新写回 Map 并保存
// 声明:虽然 rawApproveMap 里的 Value 是 Object,但写回数据库时 TypeHandler 会处理它
xiSpeak.setApproveInfo((Map) rawApproveMap);
xiSpeak.setApproveInfo(approveMap);
bgXiSpeakService.updateById(xiSpeak);
log.info("部门 {} 经办人审批记录更新成功: {}", cleanDeptId, currentWorkerName);
} else {
@@ -2,7 +2,6 @@ package org.jeecg.xispeakfb;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -13,6 +12,7 @@ import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetailMap;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.tasktask.entity.TaskTask;
import org.jeecg.modules.tasktask.service.ITaskTaskService;
@@ -53,7 +53,7 @@ public class XiSpeakFeedbackFlow {
return Collections.emptyList();
}
Map<String, DeptApproveDetail> approveInfoMap = xiSpeak.getApproveInfo();
DeptApproveDetailMap approveInfoMap = xiSpeak.getApproveInfo();
if (approveInfoMap == null || approveInfoMap.isEmpty()) {
log.warn("根据查询到的业务表单数据无法获取对应的审批信息![BgXiSpeak: {}]", xiSpeak);
return Collections.emptyList();
@@ -106,28 +106,9 @@ public class XiSpeakFeedbackFlow {
return StringUtils.EMPTY;
}
// --- 核心修复部分 ---
// 从 DB 或缓存取出的 Map,其 Value 在运行时往往是 LinkedHashMap
Map<String, ?> approveInfoMap = xiSpeak.getApproveInfo();
// 建议将 ObjectMapper 定义为静态常量或通过 Spring 注入,此处为演示保持局部定义
ObjectMapper mapper = new ObjectMapper();
DeptApproveDetailMap approveInfoMap = xiSpeak.getApproveInfo();
return approveInfoMap.values().stream()
// 重点 1: 强制声明为 Object 接收,跳过隐式的 (DeptApproveDetail) 强转
.map((Object obj) -> {
try {
// 重点 2: 如果已经是对象则直接转,否则进行 Jackson 转换
if (obj instanceof DeptApproveDetail) {
return (DeptApproveDetail) obj;
}
return mapper.convertValue(obj, DeptApproveDetail.class);
} catch (Exception e) {
log.error("审批明细类型转换失败: {}", obj, e);
return null;
}
})
.filter(Objects::nonNull) // 过滤掉转换失败的元素
.filter(detail -> detail.getImplUserNameList() != null && detail.getImplUserNameList().contains(handlerName))
.map(DeptApproveDetail::getApproverName)
.findFirst()
@@ -2,21 +2,26 @@ package org.jeecg.xispeakfb.listener;
import lombok.RequiredArgsConstructor;
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.common.api.vo.Result;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeakFeedback;
import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.bg.xispeak.entity.DeptApproveDetailMap;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakFeedbackService;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.tasktask.constant.FlowConstants;
import org.jeecg.modules.tasktask.entity.TaskTask;
import org.jeecg.modules.tasktask.service.ITaskTaskService;
import org.jeecg.xispeak.XiSpeakConfig;
import org.jeecg.xispeakfb.XiSpeakFeedbackConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
@Slf4j
@Component("AfterImplDeptLeaderApproveFeedbackListener")
@@ -24,13 +29,96 @@ import java.util.Map;
public class AfterImplDeptLeaderApproveListener implements TaskListener {
private static final long serialVersionUID = 1L;
private final XiSpeakFeedbackConfig xiSpeakFeedbackConfig;
private final IBgXiSpeakFeedbackService bgXiSpeakFeedbackService;
private final ITaskTaskService taskTaskService;
private final XiSpeakConfig xiSpeakConfig;
private final IBgXiSpeakService bgXiSpeakService;
@Transactional(rollbackFor = Exception.class)
public void notify(DelegateTask delegateTask) {
String currentWorkerName = delegateTask.getAssignee();
Object rawBusinessKey = delegateTask.getVariable(xiSpeakConfig.getXiSpeak().getBusinessKey());
String taskId = delegateTask.getId();
// 1. 基础边界防御性校验
if (StringUtils.isEmpty(currentWorkerName) || rawBusinessKey == null) {
log.warn("【任务通知】无法获取有效的审批人或业务ID,略过处理。TaskId: {}, Assignee: {}, BusinessKey: {}",
taskId, currentWorkerName, rawBusinessKey);
return;
}
String businessKeyStr = rawBusinessKey.toString();
try {
// 2. 🎯 架构优化:移除 FOR UPDATE 悲观锁,改用普通查询,预防工作流交叉死锁
BgXiSpeak xiSpeak = bgXiSpeakService.getById(businessKeyStr);
if (xiSpeak == null) {
log.error("【任务通知】业务数据不存在,终止处理。BusinessKey: {}", businessKeyStr);
return;
}
// 3. 部门变量安全解析
String implDeptKey = xiSpeakConfig.getXiSpeak().getImplDeptCollectionUsedKey();
Object rawDeptVar = delegateTask.getVariableLocal(implDeptKey);
if (rawDeptVar == null) {
rawDeptVar = delegateTask.getVariable(implDeptKey);
}
if (rawDeptVar == null) {
log.error("【任务通知】流程数据异常:未找到实施部门ID变量: {}, BusinessKey: {}", implDeptKey, businessKeyStr);
return;
}
// 🎯 优化:更安全的解析机制
String cleanDeptId = parseDeptId(rawDeptVar);
// 4. 安全获取或创建部门审批详情
DeptApproveDetailMap approveMap = xiSpeak.getApproveInfo();
if (approveMap == null) {
approveMap = new DeptApproveDetailMap();
}
DeptApproveDetail detail = approveMap.get(cleanDeptId);
if (detail == null) {
detail = new DeptApproveDetail();
detail.setDeptId(cleanDeptId);
detail.setImplUserNameList(new ArrayList<>());
approveMap.put(cleanDeptId, detail);
}
// 5. 状态比对与幂等更新
if (!Objects.equals(detail.getApproverName(), currentWorkerName)) {
detail.setApproverName(currentWorkerName);
// 写回并更新
xiSpeak.setApproveInfo(approveMap);
bgXiSpeakService.updateById(xiSpeak);
log.info("【任务通知】业务单据 [{}] 部门 [{}] 经办人审批记录成功更新为: {}",
businessKeyStr, cleanDeptId, currentWorkerName);
} else {
log.info("【任务通知】业务单据 [{}] 部门 [{}] 经办人未发生变化 [{}], 跳过数据库更新",
businessKeyStr, cleanDeptId, currentWorkerName);
}
} catch (Exception e) {
// 6. 异常全上下文捕获,方便线上无缝排查
log.error(String.format("【任务通知】处理任务结束监听器异常。TaskId: %s, BusinessKey: %s", taskId, businessKeyStr), e);
throw e; // 抛出异常以触发 Spring 事务和 Flowable 流程回滚
}
}
/**
* 辅助方法:安全解析部门ID,支持 Collection 和 String 表现形式
*/
private String parseDeptId(Object rawDeptVar) {
if (rawDeptVar instanceof Collection) {
Collection<?> collection = (Collection<?>) rawDeptVar;
if (!collection.isEmpty()) {
return Objects.toString(collection.iterator().next(), "").trim();
}
}
return rawDeptVar.toString().replace("[", "").replace("]", "").trim();
}
}
@@ -16,6 +16,7 @@ flow-biz:
tem-impl-worker-key: "tem_impl_worker"
tem-impl-leader-key: "tem_impl_leader"
tem-bg-leader-key: "tem_bg_leader"
sdw-leader-list-key: "sdwLeaderList"
xi-speak-fb:
dept-id: "x"
business-id: "business_id"
@@ -0,0 +1,18 @@
package org.jeecg.modules.bg.xispeak.constant;
/**
* 语音/演讲模块常量类
*/
public final class XiSpeakConstant {
// 1. 私有化构造器,不允许外部 new 实例化
private XiSpeakConstant() {
throw new UnsupportedOperationException("This is a constant class and cannot be instantiated");
}
// 3. 建议:使用内部类进行“业务分组”
public static final class CompletionStatus {
public static final int FINISHED = 1; // 已完成
public static final int UN_FINISHED = 0; // 未完成
}
}
@@ -1,8 +1,6 @@
package org.jeecg.modules.bg.xispeak.controller;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@@ -17,12 +15,15 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.system.vo.SelectTreeModel;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeakFeedback;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakFeedbackService;
import org.jeecg.modules.bg.xispeak.dto.BgXiSpeakBpmSaveDTO;
import org.flowable.engine.RuntimeService;
@@ -58,6 +59,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
@Slf4j
public class BgXiSpeakController extends JeecgController<BgXiSpeak, IBgXiSpeakService>{
private final IBgXiSpeakService bgXiSpeakService;
private final IBgXiSpeakFeedbackService bgXiSpeakFeedbackService;
private final RuntimeService runtimeService;
/**
* 分页列表查询
@@ -100,22 +102,19 @@ public class BgXiSpeakController extends JeecgController<BgXiSpeak, IBgXiSpeakSe
/**
* 【vue3专用】加载节点的子数据
*
* @param pid
* @param deptIds
* @return
*/
@RequestMapping(value = "/loadTreeChildren", method = RequestMethod.GET)
public Result<List<SelectTreeModel>> loadTreeChildren(@RequestParam(name = "pid") String pid) {
Result<List<SelectTreeModel>> result = new Result<>();
try {
List<SelectTreeModel> ls = bgXiSpeakService.queryListByPid(pid);
result.setResult(ls);
result.setSuccess(true);
} catch (Exception e) {
e.printStackTrace();
result.setMessage(e.getMessage());
result.setSuccess(false);
@RequestMapping(value = "/getRootListWithDept", method = RequestMethod.GET)
public Result<IPage<BgXiSpeak>> getRootListWithDept(@RequestParam(name = "dept_ids") String deptIds) {
if(StringUtils.isEmpty(deptIds)){
return Result.error("查询牵头部门为空,请选择用于查询的牵头部门");
}
return result;
List<BgXiSpeak> res = bgXiSpeakService.getRootListWithDept(deptIds);
IPage<BgXiSpeak> pageList = new Page<>(1, 10, res.size());
pageList.setRecords(res);
return Result.OK(pageList);
}
/**
@@ -354,4 +353,45 @@ public class BgXiSpeakController extends JeecgController<BgXiSpeak, IBgXiSpeakSe
return super.importExcel(request, response, BgXiSpeak.class);
}
/**
* 批量查询多个 xispeak 节点下的反馈统计数量
*
* @param ids 逗号分隔的 xispeak 主键 ID 列表
* @return Map&lt;id, {total: 总条数, closed: 已闭环数}&gt;
*/
@Operation(summary="批量查询反馈统计数量")
@GetMapping(value = "/queryFeedbackCounts")
public Result<Map<String, Map<String, Integer>>> queryFeedbackCounts(
@RequestParam(name="ids", required=true) String ids) {
if (oConvertUtils.isEmpty(ids)) {
return Result.OK(new HashMap<>());
}
List<String> idList = Arrays.asList(ids.split(","));
// 一次性查询所有相关 feedback 记录
List<BgXiSpeakFeedback> feedbackList = bgXiSpeakFeedbackService.list(
new LambdaQueryWrapper<BgXiSpeakFeedback>()
.in(BgXiSpeakFeedback::getMainId, idList));
// 初始化结果集
Map<String, Map<String, Integer>> result = new HashMap<>();
for (String id : idList) {
if (oConvertUtils.isNotEmpty(id)) {
Map<String, Integer> counts = new HashMap<>();
counts.put("total", 0);
counts.put("closed", 0);
result.put(id.trim(), counts);
}
}
// 统计
for (BgXiSpeakFeedback fb : feedbackList) {
Map<String, Integer> counts = result.get(fb.getMainId());
if (counts != null) {
counts.put("total", counts.get("total") + 1);
if ("1".equals(fb.getCompletionStatus())) {
counts.put("closed", counts.get("closed") + 1);
}
}
}
return Result.OK(result);
}
}
@@ -7,8 +7,11 @@ import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeakFeedback;
import org.jeecg.modules.bg.xispeak.service.IBgXiSpeakFeedbackService;
@@ -70,6 +73,49 @@ public class BgXiSpeakFeedbackController extends JeecgController<BgXiSpeakFeedba
return Result.OK(pageList);
}
/**
* 分页列表查询
*
* @param bgXiSpeakFeedback
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@Operation(summary="习总书记重要讲话反馈表-分页列表查询(含当前用户部门过滤)")
@GetMapping(value = "/listBgXiSpeakFeedbackByMainIdWithUserDept")
public Result<IPage<BgXiSpeakFeedback>> queryPageListWithUserDept(BgXiSpeakFeedback bgXiSpeakFeedback,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
// 1. 初始化通用的查询条件
QueryWrapper<BgXiSpeakFeedback> queryWrapper = QueryGenerator.initQueryWrapper(bgXiSpeakFeedback, req.getParameterMap());
// 2. 获取当前登录用户
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
if (sysUser != null) {
// 3. 获取当前用户的部门 ID
// 在 JeecgBoot 中,如果是多部门或当前登录部门,通常从 sysUser.getOrgCode() 或通过系统的部门服务获取
// 这里假设从当前登录上下文可以直接获取到部门 ID (或者你系统里支持的 sysUser.getDepartIds())
String currentDeptId = sysUser.getOrgId(); // 或者是你业务逻辑里获取当前选中部门的方法
// 4. 将部门 ID 写入 queryWrapper 实施强制过滤(小范围控制)
if (oConvertUtils.isNotEmpty(currentDeptId)) {
// 如果用户属于多个部门,用 in;如果是单个部门,用 eq
if (currentDeptId.contains(",")) {
queryWrapper.in("respon_deptid", currentDeptId.split(","));
} else {
queryWrapper.eq("respon_deptid", currentDeptId);
}
}
}
// 5. 执行分页查询
Page<BgXiSpeakFeedback> page = new Page<BgXiSpeakFeedback>(pageNo, pageSize);
IPage<BgXiSpeakFeedback> pageList = bgXiSpeakFeedbackService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
@@ -82,6 +82,11 @@ public class BgXiSpeak implements Serializable {
@Excel(name = "学习传达研究部署情况", width = 15)
@Schema(description = "学习传达研究部署情况")
private java.lang.String status;
/**密级*/
@Excel(name = "密级", width = 15)
@Dict(dicCode = "secret_level")
@Schema(description = "密级")
private java.lang.String secretLevel;
/**责任人*/
@Excel(name = "责任人", width = 15, dictTable = "sys_user", dicText = "realname", dicCode = "username")
@Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username")
@@ -168,8 +173,13 @@ public class BgXiSpeak implements Serializable {
@Schema(description = "是否需要sdw审批")
private java.lang.Integer needSdwApproval;
/**dw领导userId字段*/
@Excel(name = "截止日期", width = 15)
@Schema(description = "截止日期")
private java.util.Date deadline;
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, DeptApproveDetail> approveInfo;
private DeptApproveDetailMap approveInfo;
/**督办次数*/
@Excel(name = "发起类型", width = 15)
@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.jeecg.common.aspect.annotation.Dict;
import org.springframework.format.annotation.DateTimeFormat;
import org.jeecgframework.poi.excel.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -77,10 +78,18 @@ public class BgXiSpeakFeedback implements Serializable {
@Excel(name = "落实情况", width = 15)
@Schema(description = "落实情况")
private java.lang.String implStatus;
/**完成状态*/
@Excel(name = "完成状态", width = 15)
@Schema(description = "完成状态")
/**完成状态*/
@Excel(name = "反馈措施完成状态", width = 15)
@Schema(description = "反馈措施完成状态(0推进中,1已完成)")
@Dict( dicCode = "measure_complete_status")
private java.lang.String completionStatus;
/**是否存在完成风险(0不存在,1存在)*/
@Excel(name = "是否有报送上级报告(0不存在,1存在)", width = 15)
@Schema(description = "是否有报送上级报告(0不存在,1存在)")
@Dict( dicCode = "need_report_higher")
private java.lang.Integer isReportedToHigher;
/**外键*/
@Excel(name = "外键", width = 15)
@Schema(description = "外键")
@@ -112,6 +121,18 @@ public class BgXiSpeakFeedback implements Serializable {
@Schema(description = "进展情况")
private java.lang.String progress;
/**是否存在完成风险(0不存在,1存在)*/
@Excel(name = "是否存在完成风险(0不存在,1存在)", width = 15)
@Schema(description = "是否存在完成风险(0不存在,1存在)")
private java.lang.Integer isCompletionRisk;
/**拖期风险应对措施*/
@Excel(name = "拖期风险应对措施", width = 15)
@Schema(description = "拖期风险应对措施")
private java.lang.String delayRiskMitigation;
/**报告反馈情况*/
@Excel(name = "报告反馈情况", width = 15)
@Schema(description = "报告反馈情况")
private java.lang.String reportFeedbackStatus; /**报告反馈情况*/
@Excel(name = "落实计划", width = 15)
@Schema(description = "落实计划")
@@ -140,28 +161,6 @@ public class BgXiSpeakFeedback implements Serializable {
/**审批信息Map*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, DeptApproveDetail> approveInfo;
// /**发起类型 0立即 1周期*/
// @Excel(name = "发起类型 0立即 1周期", width = 15)
// @Schema(description = "发起类型 0立即 1周期")
// private java.lang.Integer launchType;
//
// /**周期类型 1日 2周 3两周 4月 5季*/
// @Excel(name = "周期类型", width = 15)
// @Schema(description = "周期类型 1日 2周 3两周 4月 5季")
// private java.lang.Integer intervalType;
//
// /**周期任务数量*/
// @Excel(name = "周期任务数量", width = 15)
// @Schema(description = "周期任务数量")
// private java.lang.Integer startCount;
//
// /**周期开始时间*/
// @Excel(name = "周期开始时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
// @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
// @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
// @Schema(description = "周期开始时间")
// private java.util.Date startTime;
private DeptApproveDetailMap approveInfo;
}
@@ -1,17 +1,21 @@
package org.jeecg.modules.bg.xispeak.entity;
import com.fasterxml.jackson.annotation.JsonAlias;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor // 必须确保有这个
@NoArgsConstructor
@AllArgsConstructor
public class DeptApproveDetail {
private String deptId; // 部门ID
private String approverId; // 审批人ID(从监听器获取的)
private String approverName; // 审批人姓名(可选)
@JsonAlias("implUserId")
private List<String> implUserIdList; // 落实人Id
@JsonAlias("implUserName")
private List<String> implUserNameList; // 落实人姓名
}
@@ -0,0 +1,6 @@
package org.jeecg.modules.bg.xispeak.entity;
import java.util.LinkedHashMap;
public class DeptApproveDetailMap extends LinkedHashMap<String, DeptApproveDetail> {
}
@@ -81,4 +81,8 @@ public interface IBgXiSpeakService extends IService<BgXiSpeak> {
BgXiSpeak getByIdForUpdate(String id);
boolean withDrawXiSpeak(String id);
List<BgXiSpeak> getRootListWithDept(String pid);
void checkAndMarkCompletedByProcessInstId(String processInstanceId);
}
@@ -16,10 +16,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -280,4 +278,71 @@ public class BgXiSpeakServiceImpl extends ServiceImpl<BgXiSpeakMapper, BgXiSpeak
return true;
}
public List<BgXiSpeak> getRootListWithDept(String pid) {
// 1. 这里的参数 pid 实际是前端传过来的部门逗号拼接字符串,将其清洗并转换为 Set
Set<String> deptIdSet = Arrays.stream(pid.split(","))
.map(String::trim)
.filter(oConvertUtils::isNotEmpty)
.collect(Collectors.toSet());
// 2. 初始化 Lambda 查询构造器
LambdaQueryWrapper<BgXiSpeak> lq = new LambdaQueryWrapper<>();
// 3. 【只查根节点】固定查询条件为父节点是 "0"
lq.eq(BgXiSpeak::getPid, "0");
// 4. 核心:只有当 deptIdSet 不为空时,才拼接多部门完全包含的逻辑
if (deptIdSet != null && !deptIdSet.isEmpty()) {
// 使用 and(...) 动态包裹,实现各个 FIND_IN_SET 之间用 AND 连接
lq.and(wrapper -> {
for (String deptId : deptIdSet) {
// {0} 占位符会被 MyBatis-Plus 自动预编译替换,保障 SQL 安全
wrapper.apply("FIND_IN_SET({0}, impl_dept)", deptId);
}
});
}
// 5. 执行查询并返回列表(这里假设你在 ServiceImpl 中,可以直接调用 baseMapper 或 this.list
return this.list(lq);
}
@Override
public void checkAndMarkCompletedByProcessInstId(String processInstanceId) {
if (oConvertUtils.isEmpty(processInstanceId)) {
return;
}
LambdaQueryWrapper<TaskTask> qw = new LambdaQueryWrapper<TaskTask>()
.eq(TaskTask::getProcessInstId, processInstanceId)
.last("limit 1");
TaskTask taskTask = taskTaskServiceImpl.getOne(qw, false);
if (taskTask == null || oConvertUtils.isEmpty(taskTask.getBusinessId())) {
return;
}
String businessId = taskTask.getBusinessId();
LambdaQueryWrapper<TaskTask> allQw = new LambdaQueryWrapper<TaskTask>()
.eq(TaskTask::getBusinessId, businessId);
List<TaskTask> allTaskList = taskTaskServiceImpl.list(allQw);
if (allTaskList == null || allTaskList.isEmpty()) {
return;
}
for (TaskTask tt : allTaskList) {
if (oConvertUtils.isNotEmpty(tt.getProcessInstId())) {
long running = runtimeService.createProcessInstanceQuery()
.processInstanceId(tt.getProcessInstId())
.count();
if (running > 0) {
return;
}
}
}
BgXiSpeak entity = this.getById(businessId);
if (entity != null && (entity.getCompletionStatus() == null || entity.getCompletionStatus() != 1)) {
entity.setCompletionStatus(1);
this.updateById(entity);
log.info("业务表单 {} 的所有流程已结束,完成状态已更新为已完成", businessId);
}
}
}
@@ -206,6 +206,22 @@ public class TaskTaskController extends JeecgController<TaskTask, ITaskTaskServi
return Result.OK(taskTaskList);
}
/**
* 更新部门经办人姓名
*/
@AutoLog(value = "事项任务计划表-更新部门经办人姓名")
@Operation(summary = "事项任务计划表-更新部门经办人姓名")
@PostMapping(value = "/updateDeptHandlerName")
public Result<String> updateDeptHandlerName(@RequestBody Map<String, String> params) {
String processInstanceId = params.get("processInstanceId");
String deptHandlerName = params.get("deptHandlerName");
if (StringUtils.isBlank(processInstanceId) || StringUtils.isBlank(deptHandlerName)) {
return Result.error("processInstanceId 和 deptHandlerName 不能为空");
}
boolean success = taskTaskService.updateDeptHandlerName(processInstanceId, deptHandlerName);
return success ? Result.OK("更新成功") : Result.error("更新失败,未找到对应任务记录");
}
/**
* 导出excel
*
@@ -17,4 +17,5 @@ public interface ITaskTaskService extends IService<TaskTask> {
boolean multiFlowStart(TaskTask taskTask,String userName,Integer intervalType, Integer startCount) throws Exception;
Result<?> handleFlowStart(TaskTask taskTask, String username, Integer intervalType, Integer startCount)throws Exception;
List<TaskTask> getTaskTaskListByBusinessId(String businessId) throws Exception;
boolean updateDeptHandlerName(String processInstanceId, String deptHandlerName);
}
@@ -2,8 +2,11 @@ package org.jeecg.modules.tasktask.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.util.Assert;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.ProcessInstance;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.extbpm.process.exception.BpmException;
import org.jeecg.modules.extbpm.process.service.impl.BpmBaseExtApiImpl;
@@ -28,10 +31,13 @@ import java.util.*;
* @Date: 2026-04-17
* @Version: V1.0
*/
@Slf4j
@Service
public class TaskTaskServiceImpl extends ServiceImpl<TaskTaskMapper, TaskTask> implements ITaskTaskService {
@Autowired
private BpmBaseExtApiImpl bpmBaseExtApiImpl;
@Autowired
private RuntimeService runtimeService;
@Transactional(rollbackFor = Exception.class)
public boolean singleFlowStart(TaskTask taskTask, String userName) throws Exception {
String jsonString = Optional.ofNullable(taskTask.getJsonData())
@@ -139,4 +145,33 @@ public class TaskTaskServiceImpl extends ServiceImpl<TaskTaskMapper, TaskTask> i
// 2. 查询不到数据 → MP 本身会返回空集合,不是 null
return this.list(queryWrapper);
}
@Override
public boolean updateDeptHandlerName(String processInstanceId, String deptHandlerName) {
if (StringUtils.isBlank(processInstanceId) || StringUtils.isBlank(deptHandlerName)) {
log.warn("updateDeptHandlerName: processInstanceId 或 deptHandlerName 为空");
return false;
}
// 1. 通过 Flowable RuntimeService 获取流程实例,进而获取 businessKey(即 taskTask ID
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
if (processInstance == null) {
log.warn("updateDeptHandlerName: 未找到 processInstanceId={} 对应的流程实例", processInstanceId);
return false;
}
String businessKey = processInstance.getBusinessKey();
if (StringUtils.isBlank(businessKey)) {
log.warn("updateDeptHandlerName: processInstanceId={} 的 businessKey 为空", processInstanceId);
return false;
}
// 2. businessKey 即为 taskTask 的主键 ID,直接通过 ID 查询
TaskTask taskTask = this.getById(businessKey);
if (taskTask == null) {
log.warn("updateDeptHandlerName: 未找到 id={} 对应的 taskTask", businessKey);
return false;
}
taskTask.setDeptHandlerName(deptHandlerName);
return this.updateById(taskTask);
}
}