考虑如下场景:
- 某一流程进行到下一步需要指派给多人,当每个被指派的人都选择完成任务后,流程继续
- 某一流程进行到下一步需要指派给多人,但是当某一个人选择完成任务后,流程继续
- 指派任务时,其人数是不固定的,可能是2-5个人,在流程创建时是未知的;只有在启动流程实例时,指派人的具体信息可以确认
其中需求1就是常说的会签,需求2就是常说的或签;在AI和互联网的帮助下搜到了一些解决方案:
- 多实例
- 多实例 + 任务监听器 + 执行监听器
- 网关 + 子流程
- 使用candidateGroup的(或签)
这篇文章记录一下用最简单的多实例完成以上需求的过程,FLOWABLE版本7.0.0.M1
具体主要是参考官方代码中的单元测试类MultiInstanceTest中的testParallelUserTasksBasedOnCollection方法。
先看下具体代码:
BPMN
<?xml version="1.0" encoding="UTF-8"?>
<definitions id="definition"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:activiti="http://activiti.org/bpmn"
targetNamespace="Examples">
<process id="miParallelUserTasksBasedOnCollection">
<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="miTasks" />
<userTask id="miTasks" name="My Task ${loopCounter}" activiti:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false">
<loopDataInputRef>assigneeList</loopDataInputRef>
<inputDataItem name="assignee" />
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow id="flow3" sourceRef="miTasks" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
</definitions>
单元测试方法
@Test
@Deployment
public void testParallelUserTasksBasedOnCollection() {
List<String> assigneeList = Arrays.asList("kermit", "gonzo", "mispiggy", "fozzie", "bubba");
String procId = runtimeService.startProcessInstanceByKey("miParallelUserTasksBasedOnCollection", CollectionUtil.singletonMap("assigneeList", assigneeList)).getId();
List<org.flowable.task.api.Task> tasks = taskService.createTaskQuery().orderByTaskAssignee().asc().list();
assertThat(tasks)
.extracting(Task::getAssignee)
.containsExactly("bubba", "fozzie", "gonzo", "kermit", "mispiggy");
// Completing 3 tasks will trigger completioncondition
taskService.complete(tasks.get(0).getId());
taskService.complete(tasks.get(1).getId());
taskService.complete(tasks.get(2).getId());
assertThat(taskService.createTaskQuery().count()).isZero();
assertProcessEnded(procId);
}
打开AI,解释下此段代码,
总结
这段代码测试了一个基于集合的并行用户任务的完成条件。具体步骤如下:
- 创建一个包含多个分配者的列表
assigneeList
。- 启动一个流程实例,并传递
assigneeList
作为变量。- 查询并断言任务的分配者顺序。
- 依次完成前3个任务。
- 验证所有任务都已完成,任务数量为0。
- 断言流程实例已经结束。
解释下工作流:
工作流解释
定义
- 该BPMN文件定义了一个名为
miParallelUserTasksBasedOnCollection
的流程。起始事件
- 流程从一个起始事件
theStart
开始。并行用户任务
- 从起始事件通过一个顺序流
flow1
连接到一个并行用户任务miTasks
。- 该用户任务
miTasks
的名称为My Task ${loopCounter}
,其中${loopCounter}
是一个循环计数器,表示当前任务的索引。- 用户任务的分配者通过表达式
${assignee}
动态设置。- 该用户任务配置了多实例循环特性:
isSequential="false"
表示这些任务是并行执行的。loopDataInputRef="assigneeList"
指定了一个集合变量assigneeList
,用于生成多个任务实例。inputDataItem name="assignee"
表示每个任务实例的分配者从assigneeList
中获取。completionCondition="${nrOfCompletedInstances/nrOfInstances >= 0.6 }"
表示当已完成的任务实例数占总任务实例数的比例达到或超过60%时,整个多实例任务将完成。结束事件
- 多实例用户任务完成后,通过一个顺序流
flow3
连接到结束事件theEnd
,表示流程结束。总结
该BPMN工作流定义了一个基于集合的并行用户任务流程。流程从起始事件开始,生成多个并行用户任务,每个任务的分配者从
assigneeList
中获取。当已完成的任务实例数达到总任务实例数的60%时,多实例任务完成,流程结束。
在看下FLOWABLE官方文档对多实例的说明。
至此,掌握的事实如下,已基本可以满足需求,总结如下:
- 可以使用loopDataInputRef和inputDataItem指定的变量创建多实例以及使用的参数名
- 可以使用completionCondition以及内置变量nrOfCompletedInstances,nrOfInstances等来判断何时终结多实例。比如nrOfCompletedInstances == nrOfInstances即所有多实例都完成,即为会签;nrOfCompletedInstances >= 1即多实例有一个以上完成了,即为或签;当然也可以和例子中类似使用nrOfCompletedInstances/nrOfInstances >= 0.6来表示60%以上的人完成后,流程继续。
- 如果loopDataInputRef和inputDataItem比较难以记住,可以使用标签flowable:collection="assigneeList" flowable:elementVariable="assignee"来达到同等效果
我们开始自己的流程,期望建立一个流程,第一个任务做会签,然后进行一步Service Task,第二个任务做或签,
流程代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:flowable="http://flowable.org/bpmn"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.flowable.org/processdef">
<process id="simpleProcess" isExecutable="true">
<startEvent id="startEvent1"/>
<sequenceFlow id="flow1" sourceRef="startEvent1" targetRef="userTask1"/>
<userTask id="userTask1" name="My User Task ${loopCounter}" flowable:assignee="${userId}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="${userIds}"
flowable:elementVariable="userId">
<loopCardinality>${userIds.size()}</loopCardinality>
<completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow id="flow2" sourceRef="userTask1" targetRef="serviceTask1"/>
<serviceTask id="serviceTask1" name="Print Variables" flowable:class="com.suoshi.astralstream.PrintVariablesDelegate"/>
<sequenceFlow id="flow4" sourceRef="serviceTask1" targetRef="approvalTask"/>
<userTask id="approvalTask" name="Approval Task ${loopCounter}" flowable:assignee="${approver}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="${approvers}"
flowable:elementVariable="approver">
<loopCardinality>${approvers.size()}</loopCardinality>
<completionCondition>${nrOfCompletedInstances >= 1}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow id="flow5" sourceRef="approvalTask" targetRef="endEvent1"/>
<endEvent id="endEvent1"/>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_a">
<bpmndi:BPMNPlane bpmnElement="simpleProcess" id="BPMNPlane_a">
<bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
<omgdc:Bounds height="36.0" width="36.0" x="100.0" y="100.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="userTask1" id="BPMNShape_userTask1">
<omgdc:Bounds height="80.0" width="100.0" x="200.0" y="80.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="serviceTask1" id="BPMNShape_serviceTask1">
<omgdc:Bounds height="80.0" width="100.0" x="350.0" y="80.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="approvalTask" id="BPMNShape_approvalTask">
<omgdc:Bounds height="80.0" width="100.0" x="500.0" y="80.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endEvent1" id="BPMNShape_endEvent1">
<omgdc:Bounds height="36.0" width="36.0" x="700.0" y="100.0"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
<omgdi:waypoint x="136.0" y="118.0"/>
<omgdi:waypoint x="200.0" y="120.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
<omgdi:waypoint x="300.0" y="120.0"/>
<omgdi:waypoint x="350.0" y="120.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
<omgdi:waypoint x="450.0" y="120.0"/>
<omgdi:waypoint x="500.0" y="120.0"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
<omgdi:waypoint x="600.0" y="120.0"/>
<omgdi:waypoint x="700.0" y="118.0"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
单元测试代码
@Test
public void testStartSimpleProcess() {
// 部署流程定义
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("simple-process.bpmn20.xml")
.deploy();
// 设置任务变量,包括 assignee 列表
List<String> userIds = new ArrayList<>();
userIds.add("johnDoe");
userIds.add("janeDoe");
userIds.add("alice");
// 设置审批者列表
List<String> approvers = new ArrayList<>();
approvers.add("bob");
approvers.add("charlie");
Map<String, Object> variables = new HashMap<>();
variables.put("userIds", userIds);
variables.put("approvers", approvers);
// 启动流程实例
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("simpleProcess", variables);
// 断言流程实例是否成功启动
assertNotNull(processInstance);
System.out.println("流程实例ID: " + processInstance.getId());
// 查询用户任务
List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
assertEquals(userIds.size(), tasks.size());
// 验证每个任务是否正确分配给指定的用户
for (Task task : tasks) {
System.out.println("用户任务ID: " + task.getId());
System.out.println("任务名称: " + task.getName());
System.out.println("任务分配给的执行人员: " + task.getAssignee());
// 验证任务分配给的执行人员
assertTrue(userIds.contains(task.getAssignee()));
}
// 模拟两个用户完成任务
for (int i = 0; i < 2; i++) {
Task task = tasks.get(i);
taskService.complete(task.getId());
}
// 检查流程实例是否仍然处于活动状态
boolean isEnded = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstance.getId())
.singleResult() == null;
assertFalse(isEnded);
// 处理最后一个人
Task lastTask = tasks.get(tasks.size() - 1);
taskService.complete(lastTask.getId());
List<Task> approvalTasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
// 模拟只有一个人完成任务
Task approvalTask = approvalTasks.get(0);
taskService.complete(approvalTask.getId());
// 检查流程实例是否已经结束
boolean isEnded2 = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstance.getId())
.singleResult() == null;
assertTrue(isEnded2);
}
输出日志
流程实例ID: 3b57f0ad-b76a-11ef-bc19-2ea2d97adec2
用户任务ID: 3b5e5963-b76a-11ef-bc19-2ea2d97adec2
任务名称: My User Task 0
任务分配给的执行人员: johnDoe
用户任务ID: 3b5ea788-b76a-11ef-bc19-2ea2d97adec2
任务名称: My User Task 1
任务分配给的执行人员: janeDoe
用户任务ID: 3b5ece9d-b76a-11ef-bc19-2ea2d97adec2
任务名称: My User Task 2
任务分配给的执行人员: alice
Service Task 'Print Variables' triggered.
Variable 'userIds' = [johnDoe, janeDoe, alice]
Variable 'approvers' = [bob, charlie]
目前看已经可以完成第一个任务会签,第二个任务或签的需求。
FLOWABLE还有很多其他实现方式,待后续慢慢研究。Enjoy Coding!