diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java index 893c4d053c..2dcb7db5cd 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java +++ b/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_"; + /** * 流程实例的变量 - 是否跳过表达式 * diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index a3414cedb4..2b2b221468 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/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 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())) { + return true; + } + // 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?) + if (sourceFlowElement instanceof ParallelGateway) { continue; } - // 如果目标节点为并行网关,则不继续 - if (sourceFlowElement instanceof ParallelGateway) { - return false; - } - // 否则就继续迭代 - 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 variables, List resultElements, Set visitElements) { // 如果为空,或者已经遍历过,则直接结束 diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 9dfb98725c..33604030e7 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/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 needSimulateTaskDefKeysByReturn = new HashSet<>(); + if (StrUtil.isNotEmpty(reqVO.getProcessInstanceId())) { + Map variables = runtimeService.getVariables(reqVO.getProcessInstanceId()); + Map 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 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 getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel, BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - List activities) { + List activities, + Set needSimulateTaskDefKeysByReturn) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 Set runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId); @@ -555,7 +572,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService List 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 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 processVariables, - BpmSimpleModelNodeVO node, Set runActivityIds) { + BpmSimpleModelNodeVO node, Set runActivityIds, + Set 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 flowElements, BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - FlowElement node, Set runActivityIds) { - if (runActivityIds.contains(node.getId())) { + FlowElement node, Set runActivityIds, + Set 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()); } }); } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 60b435c6ce..0fd434e0ed 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/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 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 taskDefinitionKeyList = needSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement); + Map 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 needSimulateTaskDefinitionKeys(BpmnModel bpmnModel, Task currentTask, FlowElement targetElement) { + // 获取需要预测的任务的 definition key。 当前任务还没完成。也需要预测。 + Set taskDefinitionKeys = CollUtil.newHashSet(currentTask.getTaskDefinitionKey()); + // 从已结束任务中找到要回退的目标任务。按时间倒序最近的一个目标任务 + List 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));