Browse Source

perf: [BPM 工作流] 优化回退操作,流程预测不准确的问题

jason 8 months ago
parent
commit
8be3a3f87e

+ 10 - 1
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java

@@ -43,14 +43,23 @@ public class BpmnVariableConstants {
      * @see ProcessInstance#getProcessVariables()
      */
     public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID";
+
     /**
      * 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id}
      *
-     * 目的是:回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过
+     * 目的是:退回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过
      *
      * @see ProcessInstance#getProcessVariables()
      */
     public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s";
+
+    /**
+     * 流程实例的变量前缀 - 用于退回操作。记录需要预测的节点. 格式 NEED_SIMULATE_TASK_{节点定义 id}
+     *
+     * 目的是:退回操作,预测节点会不准,在流程变量中记录需要预测的节点,来辅助预测
+     */
+    public static final String PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX = "NEED_SIMULATE_TASK_";
+
     /**
      * 流程实例的变量 - 是否跳过表达式
      *

+ 12 - 11
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java

@@ -658,10 +658,11 @@ public class BpmnModelUtils {
 
         // 根据类型,获取入口连线
         List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source);
+        // 1.没有入口连线,则返回 false
         if (CollUtil.isEmpty(sequenceFlows)) {
-            return true;
+            return false;
         }
-        // 循环找目标元素
+        // 2.循环找目标元素, 找到目标节点
         for (SequenceFlow sequenceFlow : sequenceFlows) {
             // 如果发现连线重复,说明循环了,跳过这个循环
             if (visitedElements.contains(sequenceFlow.getId())) {
@@ -669,21 +670,22 @@ public class BpmnModelUtils {
             }
             // 添加已经走过的连线
             visitedElements.add(sequenceFlow.getId());
-            // 这条线路存在目标节点,这条线路完成,进入下个线路
+            // 这条线路存在目标节点,直接返回 true
             FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement();
             if (target.getId().equals(sourceFlowElement.getId())) {
-                continue;
+               return true;
             }
-            // 如果目标节点为并行网关,则不继续
+            // 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?)
             if (sourceFlowElement instanceof ParallelGateway) {
-                return false;
+                continue;
             }
-            // 否则就继续迭代
-            if (!isSequentialReachable(sourceFlowElement, target, visitedElements)) {
-                return false;
+            // 继续迭代, 如果找到目标节点直接返回 true
+            if (isSequentialReachable(sourceFlowElement, target, visitedElements)) {
+                return true;
             }
         }
-        return true;
+        // 未找到返回 false
+        return false;
     }
 
     /**
@@ -783,7 +785,6 @@ public class BpmnModelUtils {
         return resultElements;
     }
 
-    @SuppressWarnings("PatternVariableCanBeUsed")
     private static void simulateNextFlowElements(FlowElement currentElement, Map<String, Object> variables,
                                                  List<FlowElement> resultElements, Set<FlowElement> visitElements) {
         // 如果为空,或者已经遍历过,则直接结束

+ 35 - 12
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.*;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@@ -72,6 +73,7 @@ import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmA
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum.REJECT_CHILD_PROCESS;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType;
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
@@ -218,10 +220,24 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
 
         // 3.1 计算当前登录用户的待办任务
         BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId());
-        // 3.2 预测未运行节点的审批信息
+
+        // 3.2 获取由于退回操作,需要预测的节点。 从流程变量中获取。 回退操作会设置这些变量
+        Set<String> needSimulateTaskDefKeysByReturn = new HashSet<>();
+        if (StrUtil.isNotEmpty(reqVO.getProcessInstanceId())) {
+            Map<String, Object> variables = runtimeService.getVariables(reqVO.getProcessInstanceId());
+            Map<String, Object> simulateTaskVariables = MapUtil.filter(variables,
+                    item -> item.getKey().startsWith(PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX));
+            simulateTaskVariables.forEach(
+                    (key, value) -> needSimulateTaskDefKeysByReturn.add(StrUtil.removePrefix(key, PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX)));
+        }
+        // 移除运行中的节点,运行中的节点无需预测
+        CollectionUtils.convertList(runActivityNodes, ActivityNode::getId).forEach(needSimulateTaskDefKeysByReturn::remove);
+
+
+        // 3.3 预测未运行节点的审批信息
         List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel,
                 processDefinitionInfo,
-                processVariables, activities);
+                processVariables, activities, needSimulateTaskDefKeysByReturn);
 
         // 4. 拼接最终数据
         return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
@@ -461,7 +477,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     }
 
     /**
-     *  获取结束节点的状态
+     * 获取结束节点的状态
      */
     private Integer getEndActivityNodeStatus(HistoricTaskInstance task) {
         Integer status = FlowableUtils.getTaskStatus(task);
@@ -546,7 +562,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     private List<ActivityNode> getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel,
                                                           BpmProcessDefinitionInfoDO processDefinitionInfo,
                                                           Map<String, Object> processVariables,
-                                                          List<HistoricActivityInstance> activities) {
+                                                          List<HistoricActivityInstance> activities,
+                                                          Set<String> needSimulateTaskDefKeysByReturn) {
         // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance
         // 包括了历史的操作,不是只有 startEvent 到当前节点的记录
         Set<String> runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId);
@@ -555,7 +572,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             List<FlowElement> flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables);
             return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn(
                     startUserId, bpmnModel, flowElements,
-                    processDefinitionInfo, processVariables, flowElement, runActivityIds));
+                    processDefinitionInfo, processVariables, flowElement, runActivityIds, needSimulateTaskDefKeysByReturn));
         }
         // 情况二:SIMPLE 设计器
         if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) {
@@ -564,17 +581,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
             List<BpmSimpleModelNodeVO> simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables);
             return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple(
                     startUserId, bpmnModel,
-                    processDefinitionInfo, processVariables, simpleNode, runActivityIds));
+                    processDefinitionInfo, processVariables, simpleNode, runActivityIds, needSimulateTaskDefKeysByReturn));
         }
         throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType());
     }
 
     private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel,
                                                          BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables,
-                                                         BpmSimpleModelNodeVO node, Set<String> runActivityIds) {
+                                                         BpmSimpleModelNodeVO node, Set<String> runActivityIds,
+                                                         Set<String> needSimulateTaskDefKeysByReturn) {
         // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance
         // 包括了历史的操作,不是只有 startEvent 到当前节点的记录
-        if (runActivityIds.contains(node.getId())) {
+        // 回退操作时候,会记录需要预测的节点到流程变量中。即使在历史操作中,也需要预测。
+        if (!needSimulateTaskDefKeysByReturn.contains(node.getId()) && runActivityIds.contains(node.getId())) {
             return null;
         }
         Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
@@ -621,13 +640,17 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
     private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, List<FlowElement> flowElements,
                                                        BpmProcessDefinitionInfoDO processDefinitionInfo,
                                                        Map<String, Object> processVariables,
-                                                       FlowElement node, Set<String> runActivityIds) {
-        if (runActivityIds.contains(node.getId())) {
+                                                       FlowElement node, Set<String> runActivityIds,
+                                                       Set<String> needSimulateTaskDefKeysByReturn) {
+
+        // 回退操作时候,会记录需要预测的节点到流程变量中。即使节点在历史操作中,也需要预测。
+        if (!needSimulateTaskDefKeysByReturn.contains(node.getId()) && runActivityIds.contains(node.getId())) {
             return null;
         }
+
         Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
         // 如果节点被跳过,状态设置为跳过
-        if(BpmnModelUtils.isSkipNode(node, processVariables)){
+        if (BpmnModelUtils.isSkipNode(node, processVariables)) {
             status = BpmTaskStatusEnum.SKIP.getStatus();
         }
         ActivityNode activityNode = new ActivityNode().setId(node.getId())
@@ -967,7 +990,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
                     if (ObjectUtil.equal(transactionStatus, TransactionSynchronization.STATUS_ROLLED_BACK)) {
                         return;
                     }
-                    taskService.moveTaskToEnd(parentProcessInstance.getId(),REJECT_CHILD_PROCESS.getReason());
+                    taskService.moveTaskToEnd(parentProcessInstance.getId(), REJECT_CHILD_PROCESS.getReason());
                 }
             });
         }

+ 53 - 20
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java

@@ -2,10 +2,12 @@ package cn.iocoder.yudao.module.bpm.service.task;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.convert.Convert;
+import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.*;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
@@ -68,8 +70,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
-import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
-import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.*;
 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
 
 /**
@@ -590,7 +591,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                 bpmnModel, reqVO.getNextAssignees(), instance);
         runtimeService.setVariables(task.getProcessInstanceId(), variables);
 
-        // 5. 调用 BPM complete 去完成任务
+        // 5. 移除辅助预测的流程变量,这些变量在回退操作中设置
+        String simulateVariableName = StrUtil.concat(false, PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, task.getTaskDefinitionKey());
+        runtimeService.removeVariable(task.getProcessInstanceId(), simulateVariableName);
+
+        // 6. 调用 BPM complete 去完成任务
         taskService.complete(task.getId(), variables, true);
 
         // 【加签专属】处理加签任务
@@ -840,25 +845,26 @@ public class BpmTaskServiceImpl implements BpmTaskService {
         if (task.isSuspended()) {
             throw exception(TASK_IS_PENDING);
         }
-        // 1.2 校验源头和目标节点的关系,并返回目标元素
-        FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(),
-                reqVO.getTargetTaskDefinitionKey(), task.getProcessDefinitionId());
+        // 1.2 获取流程模型信息
+        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
+        // 1.3 校验源头和目标节点的关系,并返回目标元素
+        FlowElement targetElement = validateTargetTaskCanReturn(bpmnModel, task.getTaskDefinitionKey(),
+                reqVO.getTargetTaskDefinitionKey());
 
         // 2. 调用 Flowable 框架的退回逻辑
-        returnTask(userId, task, targetElement, reqVO);
+        returnTask(userId, bpmnModel, task, targetElement, reqVO);
     }
 
     /**
      * 退回流程节点时,校验目标任务节点是否可退回
      *
-     * @param sourceKey           当前任务节点 Key
-     * @param targetKey           目标任务节点 key
-     * @param processDefinitionId 当前流程定义 ID
+     * @param bpmnModel 流程模型
+     * @param sourceKey 当前任务节点 Key
+     * @param targetKey 目标任务节点 key
      * @return 目标任务节点元素
      */
-    private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) {
-        // 1.1 获取流程模型信息
-        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);
+    private FlowElement validateTargetTaskCanReturn(BpmnModel bpmnModel, String sourceKey, String targetKey) {
+
         // 1.3 获取当前任务节点元素
         FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey);
         // 1.3 获取跳转的节点元素
@@ -878,11 +884,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
      * 执行退回逻辑
      *
      * @param userId        用户编号
+     * @param bpmnModel     流程模型
      * @param currentTask   当前退回的任务
      * @param targetElement 需要退回到的目标任务
      * @param reqVO         前端参数封装
      */
-    public void returnTask(Long userId, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
+    public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
         // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤
         // 1.1 获取所有正常进行的任务节点 Key
         List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
@@ -915,18 +922,45 @@ public class BpmTaskServiceImpl implements BpmTaskService {
             }
         });
 
-        // 3. 设置流程变量节点驳回标记:用于驳回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略。导致自动通过
-        runtimeService.setVariable(currentTask.getProcessInstanceId(),
-                String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE);
+        // 3. 构建需要预测的任务流程变量
+        Set<String> taskDefinitionKeyList = needSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement);
+        Map<String, Object> needSimulateVariables = convertMap(taskDefinitionKeyList,
+                taskId -> StrUtil.concat(false, PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, taskId), item -> Boolean.TRUE);
+
         // 4. 执行驳回
         // 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId 原因:
         // 当多实例任务回退的时候有问题。相关 issue: https://github.com/flowable/flowable-engine/issues/3944
         runtimeService.createChangeActivityStateBuilder()
                 .processInstanceId(currentTask.getProcessInstanceId())
                 .moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey())
+                // 设置需要预测的任务流程变量。用于辅助预测
+                .processVariables(needSimulateVariables)
+                 // 设置流程变量(local)节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略。导致自动通过
+                .localVariable(reqVO.getTargetTaskDefinitionKey(),
+                        String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE)
                 .changeState();
     }
 
+    private Set<String> needSimulateTaskDefinitionKeys(BpmnModel bpmnModel, Task currentTask, FlowElement targetElement) {
+        // 获取需要预测的任务的 definition key。 当前任务还没完成。也需要预测。
+        Set<String> taskDefinitionKeys = CollUtil.newHashSet(currentTask.getTaskDefinitionKey());
+        // 从已结束任务中找到要回退的目标任务。按时间倒序最近的一个目标任务
+        List<HistoricTaskInstance> endTaskList = CollectionUtils.filterList(
+                getTaskListByProcessInstanceId(currentTask.getProcessInstanceId(), Boolean.FALSE), item -> item.getEndTime() != null);
+        HistoricTaskInstance targetTask = findFirst(endTaskList,
+                item -> item.getTaskDefinitionKey().equals(targetElement.getId()));
+        endTaskList.forEach(item -> {
+            FlowElement element = getFlowElementById(bpmnModel, item.getTaskDefinitionKey());
+            // 如果已结束的任务在回退目标节点之后生成,且串行可达,则标记为需要预算节点。
+            // TODO 串行可达的方法需要和判断可回退节点 validateTargetTaskCanReturn 分开吗? 并行网关可能会有问题。
+            if (targetTask != null && DateUtil.compare(item.getCreateTime(), targetTask.getCreateTime()) > 0
+                    && BpmnModelUtils.isSequentialReachable(element, targetElement, null)) {
+                taskDefinitionKeys.add(item.getTaskDefinitionKey());
+            }
+        });
+        return taskDefinitionKeys;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO) {
@@ -1438,9 +1472,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
                     return;
                 }
                 FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
-                // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略
-                // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识
-                Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
+                // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略, 使用 local variable
+                Boolean returnTaskFlag = runtimeService.getVariableLocal(task.getExecutionId(),
                         String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
                 Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
                         PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));