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

This commit is contained in:
jason 2025-09-27 20:56:10 +08:00
parent 7925af994b
commit 8be3a3f87e
4 changed files with 111 additions and 45 deletions

View File

@ -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_";
/**
* 流程实例的变量 - 是否跳过表达式
*

View File

@ -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())) {
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<String, Object> variables,
List<FlowElement> resultElements, Set<FlowElement> visitElements) {
// 如果为空或者已经遍历过则直接结束

View File

@ -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());
}
});
}

View File

@ -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));