+ * 监听Flowable流程引擎的全生命周期事件,实现算例状态的实时同步: + * - PROCESS_COMPLETED: 流程正常完成 + * - PROCESS_CANCELLED: 流程被取消/终止 + * - ENTITY_SUSPENDED: 实体挂起(需过滤流程实例) + * - ENTITY_ACTIVATED: 实体激活(需过滤流程实例) + * - JOB_MOVED_TO_DEADLETTER: 作业移入死信队列(ERROR状态) + *
+ * + * @author SDM + * @date 2026-01-23 + */ +@Slf4j +@Component +public class GlobalStatusEventListener implements FlowableEventListener { + + @Autowired + private ISimulationRunFeignClient simulationRunFeignClient; + + @Autowired + @Lazy + private RuntimeService runtimeService; + + @Override + public Set
+ * 前提条件:ServiceTask必须配置async="true"和R0/PT0S重试策略
+ */
+ private void handleDeadLetter(FlowableEngineEntityEvent event) {
+ Object entity = event.getEntity();
+
+ if (entity instanceof Job) {
+ Job job = (Job) entity;
+ String processInstanceId = job.getProcessInstanceId();
+ String exceptionMessage = job.getExceptionMessage();
+
+ // 对于Job事件,需要通过RuntimeService查询流程变量
+ Long userId = null;
+ Long tenantId = null;
+ if (processInstanceId != null) {
+ Map
@@ -4441,6 +5254,8 @@ import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
+import static com.sdm.flowable.enums.ExecuteTypeEnum.LOCAL_APP;
+
/**
* DTO → Flowable BpmnModel 映射工具类(核心)
*/
@@ -4477,6 +5292,9 @@ public class Dto2BpmnConverter {
// 3.1、存储等待用户输入任务映射关系(原节点ID → waitUserTask节点ID)
// 这里 Value 存的是 _waitUser 节点 ID,用于标识这个节点开启了等待用户操作
Map
+ * 正常判定:依赖 origId 的历史 endTime。 + *
+ * 跳转兜底:当强制跳转绕过 origId 的实际创建/执行时,可能缺失 origId 的历史记录; + * 这时若链路上(_register/_wait/_check)任意节点已出现结束时间记录,且当前没有任何链路节点仍在活动, + * 则认为 origId 的聚合状态应为 FINISHED。 + * + * @param ctx 状态上下文 + * @param origId Original 逻辑节点ID + * @param registerId _register 节点ID + * @param waitId _wait 节点ID + * @param checkId _check 节点ID + * @return true 表示应标记为 FINISHED + */ + private boolean isLocalAppOrigFinished(ProcessStateContext ctx, String origId, String registerId, String waitId, String checkId) { + HistoricActivityInstance lastHist = ctx.historyMap.get(origId); + if (lastHist != null && lastHist.getEndTime() != null) { + return true; + } + + // 跳转兜底:链路没有活动 token,且历史上至少有一个链路节点出现过结束时间 + boolean chainHasActiveToken = ctx.activeActivityIds.contains(registerId) + || ctx.activeActivityIds.contains(waitId) + || ctx.activeActivityIds.contains(checkId) + || ctx.activeActivityIds.contains(origId); + if (chainHasActiveToken) { + return false; + } + + Date registerEnd = getEndTime(ctx.historyMap.get(registerId)); + Date waitEnd = getEndTime(ctx.historyMap.get(waitId)); + Date checkEnd = getEndTime(ctx.historyMap.get(checkId)); + + return registerEnd != null || waitEnd != null || checkEnd != null; + } + + /** + * 安全获取 HistoricActivityInstance 的 endTime。 + * + * @param hist 历史活动实例 + * @return endTime;如果 hist 为空或 endTime 为空则返回 null + */ + private Date getEndTime(HistoricActivityInstance hist) { + return hist != null ? hist.getEndTime() : null; + } + private void determineServiceTaskUnifiedState(NodeDetailInfo node, ProcessStateContext ctx) { String origId = node.getId(); String waitUserId = FlowNodeIdUtils.generateWaitUserTaskId(origId); @@ -606,15 +666,22 @@ public class ProcessService implements Iprocess{ String checkId = FlowNodeIdUtils.generateCheckTaskId(origId); // 1. 判断 ERROR (优先级最高) - // 链条任何一环报错,原始节点都算错 - String errorMsg = ctx.errorMap.get(checkId); // 最常见:check挂了 - if (errorMsg == null) errorMsg = ctx.errorMap.get(origId); // 自己挂了 - // 也可以加上 waitId 的判断,虽然 ReceiveTask 很难挂 + // 1. 判断 ERROR (Check 节点死信) + boolean hasError = ctx.errorMap.containsKey(waitUserId) + ||ctx.errorMap.containsKey(origId) + || ctx.errorMap.containsKey(waitId) + || ctx.errorMap.containsKey(checkId); + + if (hasError) { + // 从 registerId、waitId、checkId、origId 中获取第一个非空的错误信息 + String errorMessage = ctx.errorMap.get(waitUserId); + if (errorMessage == null) errorMessage =ctx.errorMap.get(origId); + if (errorMessage == null) errorMessage = ctx.errorMap.get(waitId); + if (errorMessage == null) errorMessage = ctx.errorMap.get(checkId); - if (errorMsg != null) { node.setStatus(NodeStateEnum.ERROR.getCode()); - node.setErrorMessage(errorMsg); - calculateAggregatedTime(node, ctx, waitUserId, origId, checkId); + node.setErrorMessage(errorMessage); + calculateAggregatedTime(node, ctx, waitUserId, origId,waitId, checkId); return; } @@ -640,21 +707,17 @@ public class ProcessService implements Iprocess{ // 停留在 origId 或 waitId 或 checkId-> 视为通用执行中 node.setStatus(NodeStateEnum.ACTIVE.getCode()); } - calculateAggregatedTime(node, ctx, waitUserId, origId, checkId); + calculateAggregatedTime(node, ctx, waitUserId, origId,waitId, checkId); return; } // 3. 判断 FINISHED - // 必须是链条的"最后一个环节"结束了,才算整个节点结束 - // 顺序:WaitUser -> Original -> Wait -> Check - // 我们检查 Check 是否有历史;如果没有 Check (非HPC节点),检查 Original - HistoricActivityInstance lastHist = ctx.historyMap.get(checkId); - if (lastHist == null) lastHist = ctx.historyMap.get(waitId); // 兼容 - if (lastHist == null) lastHist = ctx.historyMap.get(origId); - - if (lastHist != null && lastHist.getEndTime() != null) { + // 兜底:当发生“强制跳转”绕过链路最后环节时,可能缺失 check/orig 的历史记录。 + // 只要链路(waitUser/orig/wait/check)中任意节点产生过结束记录,并且当前不再处于链路活动中, + // 就将 origId 视为已完成,避免界面长期停留在 PENDING。 + if (isServiceTaskOrigFinished(ctx, waitUserId, origId, waitId, checkId)) { node.setStatus(NodeStateEnum.FINISHED.getCode()); - calculateAggregatedTime(node, ctx, waitUserId, origId, checkId); + calculateAggregatedTime(node, ctx, waitUserId, origId, waitId,checkId); return; } @@ -662,6 +725,39 @@ public class ProcessService implements Iprocess{ node.setStatus(NodeStateEnum.PENDING.getCode()); } + /** + * 判断 ServiceTask 聚合节点(Original)是否应当被标记为 FINISHED。 + *
+ * 正常判定:依赖链路最后环节(通常是 _check)的历史 endTime。 + *
+ * 跳转兜底:当强制跳转绕过了链路最后环节或 origId 的实际创建/执行时,可能缺失关键节点历史记录;
+ * 这时若链路上(waitUser/orig/wait/check)任意节点已出现结束时间记录,且当前没有任何链路节点仍在活动,
+ * 则认为 origId 的聚合状态应为 FINISHED。
+ *
+ * @param ctx 状态上下文
+ * @param waitUserId _waitUser 节点ID
+ * @param origId Original 逻辑节点ID
+ * @param waitId _wait 节点ID
+ * @param checkId _check 节点ID
+ * @return true 表示应标记为 FINISHED
+ */
+ private boolean isServiceTaskOrigFinished(ProcessStateContext ctx, String waitUserId, String origId, String waitId, String checkId) {
+ boolean chainHasActiveToken = ctx.activeActivityIds.contains(waitUserId)
+ || ctx.activeActivityIds.contains(origId)
+ || ctx.activeActivityIds.contains(waitId)
+ || ctx.activeActivityIds.contains(checkId);
+ if (chainHasActiveToken) {
+ return false;
+ }
+
+ Date waitUserEnd = getEndTime(ctx.historyMap.get(waitUserId));
+ Date origEnd = getEndTime(ctx.historyMap.get(origId));
+ Date waitEnd = getEndTime(ctx.historyMap.get(waitId));
+ Date checkEnd = getEndTime(ctx.historyMap.get(checkId));
+
+ return waitUserEnd != null || origEnd != null || waitEnd != null || checkEnd != null;
+ }
+
// 判断是否为本地应用节点
private boolean isLocalApp(NodeDetailInfo node) {
@@ -718,7 +814,7 @@ public class ProcessService implements Iprocess{
* 工具:计算聚合时间 (Original Start -> Check End)
*/
private void calculateAggregatedTime(NodeDetailInfo node, ProcessStateContext ctx,
- String waitUserId, String origId, String checkId) {
+ String waitUserId, String origId, String waitId,String checkId) {
// Start Time: 链条最早的开始时间
HistoricActivityInstance startNode = ctx.historyMap.get(waitUserId);
if (startNode == null) startNode = ctx.historyMap.get(origId);
@@ -726,12 +822,20 @@ public class ProcessService implements Iprocess{
Date startTime = (startNode != null) ? startNode.getStartTime() : null;
node.setStartTime(startTime);
- // End Time: 只有状态是 FINISHED 才有结束时间,取 Check 的结束时间
+ // End Time: 只有状态是 FINISHED才有结束时间,取四个节点(waitUser, orig, wait, check)的结束时间的最大值
Date endTime = null;
if (NodeStateEnum.FINISHED.getCode().equals(node.getStatus())) {
- HistoricActivityInstance endNode = ctx.historyMap.get(checkId);
- if (endNode == null) endNode = ctx.historyMap.get(origId);
- if (endNode != null) endTime = endNode.getEndTime();
+ List