mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 04:39:00 +08:00
feat(flow): 优化拒绝处理逻辑,支持多实例结束后重定向到发起人
- 更新 ApproveServiceTask 中拒绝处理逻辑,支持多实例结束后当前执行流尚未落库的情况 - 增加对 CommandContextUtil 的使用,确保执行流迁移的正确性 - 添加 ModelUtilMultiMatchTests 中的单元测试,验证拒绝到发起人的逻辑 (cherry picked from commit 9e37864b02b14a2c980b74e01e126b5d38d10b09)
This commit is contained in:
1 parent
2f089f029f
commit
0974a56294
2 files changed
+105
-15
No files matched your search
+38
-10
@@ -1,17 +1,25 @@
|
||||
package com.pig4cloud.pigx.flow.support.tasks;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.pig4cloud.pigx.flow.constant.NodeStatusEnum;
|
||||
import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant;
|
||||
import com.pig4cloud.pigx.flow.dto.Node;
|
||||
import com.pig4cloud.pigx.flow.dto.Refuse;
|
||||
import com.pig4cloud.pigx.flow.entity.ProcessNodeRecord;
|
||||
import com.pig4cloud.pigx.flow.entity.ProcessNodeRecordAssignUser;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessNodeDataService;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessNodeRecordService;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessNodeRecordAssignUserService;
|
||||
import org.flowable.engine.RuntimeService;
|
||||
import org.flowable.engine.delegate.DelegateExecution;
|
||||
import org.flowable.engine.delegate.JavaDelegate;
|
||||
import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl;
|
||||
import org.flowable.engine.impl.util.CommandContextUtil;
|
||||
import org.flowable.engine.runtime.Execution;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -68,7 +76,8 @@ public class ApproveServiceTask implements JavaDelegate {
|
||||
/**
|
||||
* 处理任务被拒绝时的流程重定向逻辑
|
||||
* <p>
|
||||
* 根据节点配置的拒绝策略,将流程重定向到指定节点或结束节点。 支持的拒绝策略: 1. TO_NODE:跳转到指定节点 2. 其他:直接结束流程
|
||||
* 根据节点配置跳转到指定节点、发起人或结束节点。TO_START 会收回实例全部执行流,并结束被收回节点的本轮审批记录;
|
||||
* 与引擎状态迁移共用当前审批事务,不覆盖已经完成的审批历史。
|
||||
* @param processDefinitionId 流程定义 ID
|
||||
* @param nodeId 当前节点标识符
|
||||
* @param currentActivityId 当前活动标识符
|
||||
@@ -97,17 +106,36 @@ public class ApproveServiceTask implements JavaDelegate {
|
||||
// 根据拒绝设置重定向到特定节点
|
||||
stateChangeBuilder.moveActivityIdTo(currentActivityId, refuseConfig.getNodeId()).changeState();
|
||||
}
|
||||
else if (StrUtil.equals(rejectionHandler, "TO_START")) {
|
||||
// 驳回到发起人:设置驳回标识变量,跳转到 root 节点
|
||||
// 并行、多选分支内驳回时,兄弟分支执行流需一并收回,否则重新提交后汇聚网关会多出令牌
|
||||
runtimeService.setVariable(processInstanceId, "rejectToStarter", true);
|
||||
List<String> executionIds = runtimeService.createExecutionQuery()
|
||||
.parentId(processInstanceId)
|
||||
.list()
|
||||
else if (StrUtil.equals(rejectionHandler, ProcessInstanceConstant.USER_TASK_REFUSE_TYPE_TO_START)) {
|
||||
// 多实例结束后当前执行流尚未落库,必须使用包含命令缓存的执行树,并排除已删除的旧 scope。
|
||||
// 按活动迁移会合并为一个 root;按执行流迁移会为每条分支各建一个 root 待办。
|
||||
List<String> activityIds = CommandContextUtil.getExecutionEntityManager()
|
||||
.findChildExecutionsByProcessInstanceId(processInstanceId)
|
||||
.stream()
|
||||
.map(Execution::getId)
|
||||
.filter(child -> !child.isDeleted() && StrUtil.equals(child.getParentId(), processInstanceId))
|
||||
.map(Execution::getActivityId)
|
||||
.distinct()
|
||||
.toList();
|
||||
stateChangeBuilder.moveExecutionsToSingleActivityId(executionIds, "root").changeState();
|
||||
runtimeService.setVariable(processInstanceId, ProcessInstanceConstant.REJECT_TO_STARTER_VAR, true);
|
||||
|
||||
// 先结束本轮活动记录,避免重提复用被收回的节点及参与人;迁移新建的 root 不应被关闭。
|
||||
Date now = new Date();
|
||||
SpringUtil.getBean(IProcessNodeRecordService.class)
|
||||
.lambdaUpdate()
|
||||
.set(ProcessNodeRecord::getStatus, NodeStatusEnum.YJS.getCode())
|
||||
.set(ProcessNodeRecord::getEndTime, now)
|
||||
.eq(ProcessNodeRecord::getProcessInstanceId, processInstanceId)
|
||||
.eq(ProcessNodeRecord::getStatus, NodeStatusEnum.JXZ.getCode())
|
||||
.update(new ProcessNodeRecord());
|
||||
SpringUtil.getBean(IProcessNodeRecordAssignUserService.class)
|
||||
.lambdaUpdate()
|
||||
.set(ProcessNodeRecordAssignUser::getStatus, ProcessInstanceConstant.ASSIGN_USER_STATUS_WITHDRAW)
|
||||
.set(ProcessNodeRecordAssignUser::getEndTime, Convert.toLocalDateTime(now))
|
||||
.set(ProcessNodeRecordAssignUser::getTaskType, "WITHDRAW")
|
||||
.eq(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceId)
|
||||
.eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode())
|
||||
.update(new ProcessNodeRecordAssignUser());
|
||||
stateChangeBuilder.moveActivityIdsToSingleActivityId(activityIds, "root").changeState();
|
||||
}
|
||||
else {
|
||||
// 默认行为:重定向到结束节点
|
||||
|
||||
+67
-5
@@ -1,10 +1,15 @@
|
||||
package com.pig4cloud.pigx.flow.support.utils;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
|
||||
import com.pig4cloud.pigx.flow.entity.ProcessNodeRecord;
|
||||
import com.pig4cloud.pigx.flow.entity.ProcessNodeRecordAssignUser;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.flow.dto.Node;
|
||||
import com.pig4cloud.pigx.flow.dto.Refuse;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessNodeDataService;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessNodeRecordService;
|
||||
import com.pig4cloud.pigx.flow.service.IProcessNodeRecordAssignUserService;
|
||||
import org.flowable.bpmn.model.BpmnModel;
|
||||
import org.flowable.bpmn.model.ExclusiveGateway;
|
||||
import org.flowable.bpmn.model.InclusiveGateway;
|
||||
@@ -17,6 +22,7 @@ import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
@@ -25,6 +31,7 @@ import org.flowable.spring.SpringProcessEngineConfiguration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -65,6 +72,18 @@ class ModelUtilMultiMatchTests {
|
||||
context = new GenericApplicationContext();
|
||||
context.getBeanFactory().registerSingleton("runtimeService", engine.getRuntimeService());
|
||||
context.getBeanFactory().registerSingleton("processNodeDataService", nodes);
|
||||
// set/eq 泛型返回值擦除为 Object,RETURNS_SELF 不会返回自身;链式调用返回 mock,终结的 update 返回 true
|
||||
Answer<Object> chain = invocation -> "update".equals(invocation.getMethod().getName()) ? true
|
||||
: invocation.getMock();
|
||||
IProcessNodeRecordService records = mock(IProcessNodeRecordService.class);
|
||||
LambdaUpdateChainWrapper<ProcessNodeRecord> recordUpdate = mock(LambdaUpdateChainWrapper.class, chain);
|
||||
when(records.lambdaUpdate()).thenReturn(recordUpdate);
|
||||
IProcessNodeRecordAssignUserService assignments = mock(IProcessNodeRecordAssignUserService.class);
|
||||
LambdaUpdateChainWrapper<ProcessNodeRecordAssignUser> assignmentUpdate = mock(LambdaUpdateChainWrapper.class,
|
||||
chain);
|
||||
when(assignments.lambdaUpdate()).thenReturn(assignmentUpdate);
|
||||
context.getBeanFactory().registerSingleton("processNodeRecordService", records);
|
||||
context.getBeanFactory().registerSingleton("processNodeRecordAssignUserService", assignments);
|
||||
context.refresh();
|
||||
new SpringUtil().setApplicationContext(context);
|
||||
}
|
||||
@@ -83,8 +102,8 @@ class ModelUtilMultiMatchTests {
|
||||
assertThat(model.getMainProcess().getFlowElement("gw")).isInstanceOf(InclusiveGateway.class);
|
||||
assertThat(model.getMainProcess().getFlowElement("gw_merge_gateway")).isInstanceOf(InclusiveGateway.class);
|
||||
|
||||
BpmnModel single = ModelUtil.buildBpmnModel(tree(false, approval("a"), approval("b"), approval("other")),
|
||||
"f", "f");
|
||||
BpmnModel single = ModelUtil.buildBpmnModel(tree(false, approval("a"), approval("b"), approval("other")), "f",
|
||||
"f");
|
||||
assertThat(single.getMainProcess().getFlowElement("gw")).isInstanceOf(ExclusiveGateway.class);
|
||||
assertThat(single.getMainProcess().getFlowElement("gw_merge_gateway")).isInstanceOf(ExclusiveGateway.class);
|
||||
}
|
||||
@@ -156,6 +175,45 @@ class ModelUtilMultiMatchTests {
|
||||
engine.getTaskService().complete(task(instance, "root").getId(), Map.of("a_approve_condition", true));
|
||||
assertThat(activeTaskKeys(instance)).containsExactlyInAnyOrder("a", "b");
|
||||
complete(instance, "a");
|
||||
assertThat(activeTaskKeys(instance)).containsExactly("b");
|
||||
complete(instance, "b");
|
||||
assertThat(activeTaskKeys(instance)).containsExactly("after");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectToStarterOnSinglePathStopsAtRoot() {
|
||||
Refuse refuse = new Refuse();
|
||||
refuse.setHandler("TO_START");
|
||||
Node rejectConfig = new Node();
|
||||
rejectConfig.setRefuse(refuse);
|
||||
when(nodes.getPublishedNode(anyString(), eq("a"))).thenReturn(R.ok(rejectConfig));
|
||||
Node root = tree(false, approval("x"), approval("y"), approval("z"));
|
||||
Node a = approval("a");
|
||||
a.setChildren(approval("after"));
|
||||
root.setChildren(a);
|
||||
ProcessInstance instance = submit(root, 200, "it");
|
||||
|
||||
engine.getTaskService().complete(task(instance, "a").getId(), Map.of("a_approve_condition", false));
|
||||
assertThat(activeTaskKeys(instance)).containsExactly("root");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectToStarterAfterSiblingWaitsAtJoin() {
|
||||
Refuse refuse = new Refuse();
|
||||
refuse.setHandler("TO_START");
|
||||
Node rejectConfig = new Node();
|
||||
rejectConfig.setRefuse(refuse);
|
||||
when(nodes.getPublishedNode(anyString(), eq("a"))).thenReturn(R.ok(rejectConfig));
|
||||
ProcessInstance instance = submit(tree(true, approval("a"), approval("b"), approval("other")), 200, "it");
|
||||
complete(instance, "b");
|
||||
|
||||
engine.getTaskService().complete(task(instance, "a").getId(), Map.of("a_approve_condition", false));
|
||||
assertThat(activeTaskKeys(instance)).containsExactly("root");
|
||||
|
||||
engine.getTaskService().complete(task(instance, "root").getId(), Map.of("a_approve_condition", true));
|
||||
assertThat(activeTaskKeys(instance)).containsExactlyInAnyOrder("a", "b");
|
||||
complete(instance, "a");
|
||||
assertThat(activeTaskKeys(instance)).containsExactly("b");
|
||||
complete(instance, "b");
|
||||
assertThat(activeTaskKeys(instance)).containsExactly("after");
|
||||
}
|
||||
@@ -178,6 +236,7 @@ class ModelUtilMultiMatchTests {
|
||||
|
||||
engine.getTaskService().complete(task(instance, "root").getId(), Map.of("a_approve_condition", true));
|
||||
complete(instance, "a");
|
||||
assertThat(activeTaskKeys(instance)).containsExactly("b");
|
||||
complete(instance, "b");
|
||||
assertThat(activeTaskKeys(instance)).containsExactly("after");
|
||||
}
|
||||
@@ -223,7 +282,7 @@ class ModelUtilMultiMatchTests {
|
||||
}
|
||||
|
||||
/**
|
||||
* 部署后发起并提交发起人节点;剥离监听器和多实例,只保留真实网关、连线和审批服务任务。
|
||||
* 部署后发起并提交发起人节点;保留多实例执行树,隔离人员解析和业务监听器。
|
||||
*/
|
||||
private ProcessInstance submit(Node root, int amount, String dept) {
|
||||
BpmnModel model = ModelUtil.buildBpmnModel(root, "f", "f");
|
||||
@@ -231,13 +290,16 @@ class ModelUtilMultiMatchTests {
|
||||
for (var element : model.getMainProcess().getFlowElements()) {
|
||||
if (element instanceof UserTask task) {
|
||||
task.setTaskListeners(new ArrayList<>());
|
||||
task.setLoopCharacteristics(null);
|
||||
if (Objects.nonNull(task.getLoopCharacteristics())) {
|
||||
task.getLoopCharacteristics().setInputDataItem("${assignees}");
|
||||
task.getLoopCharacteristics().setCompletionCondition("${nrOfCompletedInstances == nrOfInstances}");
|
||||
}
|
||||
task.setAssignee("9000");
|
||||
}
|
||||
}
|
||||
engine.getRepositoryService().createDeployment().addBpmnModel("f.bpmn20.xml", model).deploy();
|
||||
ProcessInstance instance = engine.getRuntimeService()
|
||||
.startProcessInstanceByKey("f", Map.of("amount", amount, "dept", dept));
|
||||
.startProcessInstanceByKey("f", Map.of("amount", amount, "dept", dept, "assignees", List.of("9000")));
|
||||
complete(instance, "root");
|
||||
return instance;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user