前言
实际业务中的会签,代表一个任务节点由多个人进行审批,可能会衍生出多种情况:
- 只要一个人审批后,就到下一个节点
- 全部审批后,才能到下一个节点
- 审批人数占比多少,就到下一个节点,本案例就选择这个最麻烦的。
在Flowable BPMN 用户手册中,”会签“的介绍在”多实例“的章节,也就是说,会签,其实就是多实例。
多实例活动(multi-instance activity)是在业务流程中,为特定步骤定义重复的方式。在编程概念中,多实例类似for each结构:可以为给定集合中的每一条目,顺序或并行地,执行特定步骤,甚至是整个子流程。多实例是一个普通活动,加上定义(被称作“多实例特性的”)额外参数,会使得活动在运行时被多次执行。
具体的可参考:https://tkjohn.github.io/flowable-userguide/#bpmnBusinessRuleTask
特别注意其描述的xml部分:
要将活动变成多实例,该活动的XML元素必须有multiInstanceLoopCharacteristics子元素
<multiInstanceLoopCharacteristics isSequential="false|true">
...
</multiInstanceLoopCharacteristics>
isSequential属性代表了活动的实例为顺序还是并行执行。
实例的数量在进入活动时,计算一次。有几种不同方法可以配置数量。一个方法是通过loopCardinality子元素,直接指定数字。
<multiInstanceLoopCharacteristics isSequential="false|true">
<loopCardinality>5</loopCardinality>
</multiInstanceLoopCharacteristics>
也可以使用解析为正整数的表达式:
<multiInstanceLoopCharacteristics isSequential="false|true">
<loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
</multiInstanceLoopCharacteristics>
另一个定义实例数量的方法,是使用loopDataInputRef子元素,指定一个集合型流程变量的名字。对集合中的每一项,都会创建一个实例。可以使用inputDataItem子元素,将该项设置给该实例的局部变量。在下面的XML示例中展示:
<userTask id="miTasks" name="My Task ${loopCounter}" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false">
<loopDataInputRef>assigneeList</loopDataInputRef>
<inputDataItem name="assignee" />
</multiInstanceLoopCharacteristics>
</userTask>
假设变量assigneeList包含[kermit, gonzo, fozzie]。上面的代码会创建三个并行的用户任务。每一个执行都有一个名为assignee的(局部)流程变量,含有集合中的一项,并在这个例子中被用于指派用户任务。
loopDataInputRef与inputDataItem的缺点是名字很难记,并且由于BPMN 2.0概要的限制,不能使用表达式。Flowable通过在multiInstanceCharacteristics上提供collection与elementVariable属性解决了这些问题:
<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="${myService.resolveUsersForTask()}" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
请注意collection属性会作为表达式进行解析。如果表达式解析为字符串而不是一个集合,不论是因为本身配置的就是静态字符串值,还是表达式计算结果为字符串,这个字符串都会被当做变量名,在流程变量中用于获取实际的集合。
例如,下面的代码片段会要求集合存储在assigneeList流程变量中:
<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="assigneeList" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
假如myService.getCollectionVariableName()返回字符串值,引擎就会用这个值作为变量名,获取流程变量保存的集合。
<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="${myService.getCollectionVariableName()}" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
多实例活动在所有实例都完成时结束。然而,也可以指定一个表达式,在每个实例结束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例活动,继续执行流程。这个表达式必须通过completionCondition子元素定义。
<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="assigneeList" flowable:elementVariable="assignee" >
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
在这个例子里,会为assigneeList集合中的每个元素创建并行实例。当60%的任务完成时,其他的任务将被删除,流程继续运行。
接下来具体介绍本文的案例
案例中,只有一个风控审批的节点,审核人员是风控人员组的成员,审核人数占比超过50%就结束会签。
流程
<?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" exporter="Flowable Open Source Modeler" exporterVersion="6.7.2">
<process id="shenpi1" name="审批1" isExecutable="true">
<startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent>
<userTask id="sid-B0A762DE-D600-4998-96D6-49F7BE676159" name="风控审批" flowable:candidateGroups="${风控人员组}" flowable:formFieldValidation="true">
<multiInstanceLoopCharacteristics isSequential="false" flowable:collection="persons" flowable:elementVariable="person">
<extensionElements></extensionElements>
<completionCondition>${compCondition.getComCondition(execution)}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow id="sid-0129BF6A-C57C-4383-A14C-879FD48EED4E" sourceRef="startEvent1" targetRef="sid-B0A762DE-D600-4998-96D6-49F7BE676159"></sequenceFlow>
<endEvent id="sid-80CB6AF8-5DD5-4AC1-A1B2-05A5569B402D"></endEvent>
<sequenceFlow id="sid-3349C437-5FF2-4565-B2CD-75CCA54FAF03" sourceRef="sid-B0A762DE-D600-4998-96D6-49F7BE676159" targetRef="sid-80CB6AF8-5DD5-4AC1-A1B2-05A5569B402D"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_shenpi1">
<bpmndi:BPMNPlane bpmnElement="shenpi1" id="BPMNPlane_shenpi1">
<bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
<omgdc:Bounds height="30.0" width="30.0" x="100.0" y="163.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-B0A762DE-D600-4998-96D6-49F7BE676159" id="BPMNShape_sid-B0A762DE-D600-4998-96D6-49F7BE676159">
<omgdc:Bounds height="80.0" width="100.0" x="225.0" y="135.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-80CB6AF8-5DD5-4AC1-A1B2-05A5569B402D" id="BPMNShape_sid-80CB6AF8-5DD5-4AC1-A1B2-05A5569B402D">
<omgdc:Bounds height="28.0" width="28.0" x="435.0" y="161.00000216744158"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-3349C437-5FF2-4565-B2CD-75CCA54FAF03" id="BPMNEdge_sid-3349C437-5FF2-4565-B2CD-75CCA54FAF03" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="2.0" flowable:targetDockerY="14.0">
<omgdi:waypoint x="324.9499999999799" y="175.0000006682945"></omgdi:waypoint>
<omgdi:waypoint x="434.99999863624566" y="175.00000214068302"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-0129BF6A-C57C-4383-A14C-879FD48EED4E" id="BPMNEdge_sid-0129BF6A-C57C-4383-A14C-879FD48EED4E" flowable:sourceDockerX="15.0" flowable:sourceDockerY="15.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
<omgdi:waypoint x="129.9474255708386" y="177.71879843198602"></omgdi:waypoint>
<omgdi:waypoint x="225.0" y="175.9365625"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
注意
这里有必要说一下设置的重点,我采用的是flowable-ui绘制的
- 多实例类型:并行或者串行,案例中是并行的
- 基数:创造几个实例,除非特别固定,否则不会用这个
- 集合:很常用,比如案例中风控审批人员组有多少个人,就根据这个集合创造多少实例,也就是多少人来会签
- 元素:对应集合中的元素,也就是每个风控人员,会使用变量保存下来
- 完成条件:什么条件下会签结束,我这里传递了一个类的方法${compCondition.getComCondition(execution)},当有人会签后,就调用这个方法,计算会签完成比例是否大于0.5,如果是,就会签结束,没有完成的task自动结束。
代码核心
@Transactional
public void startProcess() {
Map<String, Object> variables = new HashMap<String, Object>();
//根据当前用户获取所在的用户组
Group group = identityService.createGroupQuery().groupMember("三井寿1").singleResult();
variables.put("风控人员组",group.getId());
//获取当前组的人员集合
List<String> persons = identityService.createUserQuery().memberOfGroup(group.getId()).list().stream().map((li) -> {
return li.getId();
}).collect(Collectors.toList());
variables.put("persons",persons);
runtimeService.startProcessInstanceByKey("shenpi1",variables);
}
//领取任务,act_ru_task表中ASSIGNEE_字段之前为空,谁领取就变成谁。
@Transactional
public void claimTask() {
//根据当前用户获取所在的组
Group group = identityService.createGroupQuery().groupMember("三井寿2").singleResult();
//根据组查询任务,查询到没有分配处理人的任务就申领这个任务,然后结束循环
List<Task> list = taskService.createTaskQuery().processInstanceId("a4bdf5bb-b57b-11ec-85bf-3c9c0f202230").taskCandidateGroup(group.getId()).list();
if (list != null){
for (int i = 0; i < list.size(); i++) {
Task task = list.get(i);
if (task.getAssignee() == null){
taskService.claim(task.getId(),"三井寿2");//领取任务
System.out.println(task.getAssignee());
break;
}
}
}
}
// 完成任务
@Transactional
public void completeTask() {
Task task = taskService.createTaskQuery().processInstanceId("a4bdf5bb-b57b-11ec-85bf-3c9c0f202230").taskAssignee("三井寿2").singleResult();
if (task != null){
taskService.complete(task.getId());
}
}
@Transactional
public void addGroupAndUser() {
//实际开发中,可将本身的用户表、角色表、部门表以及之间的关系同步到flowable的act_id_user、act_id_group、act_id_membership表中
//其中角色和部门可当作组
User user3 =identityService.newUser("三井寿1");
user3.setFirstName("三井");
user3.setLastName("寿1");
identityService.saveUser(user3);//注意必须这样保存
User user4 =identityService.newUser("三井寿2");
user4.setFirstName("三井");
user4.setLastName("寿2");
identityService.saveUser(user4);//注意必须这样保存
User user5 =identityService.newUser("三井寿3");
user5.setFirstName("三井");
user5.setLastName("寿3");
identityService.saveUser(user5);//注意必须这样保存
Group group2 = identityService.newGroup("风控人员组");
group2.setName("风控员");
identityService.saveGroup(group2);//注意必须这样保存
//创建关联关系
identityService.createMembership("三井寿1","风控人员组");
identityService.createMembership("三井寿2","风控人员组");
identityService.createMembership("三井寿3","风控人员组");
完成条件代码
package com.example.flowable.service;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
import java.io.Serializable;
@Component("compCondition")
public class CompCondition implements Serializable {
// 多实例活动在所有实例都完成时结束。
// 然而,也可以指定一个表达式(或类的方法),在每个实例结束时进行计算。
// 当表达式计算为true时,将销毁所有剩余的实例,并结束多实例活动,继续执行流程。
// 这个表达式必须通过completionCondition子元素定义。
public boolean getComCondition(DelegateExecution execution){
Object nrOfInstances = execution.getVariable("nrOfInstances");//实例总数
Object nrOfActiveInstances = execution.getVariable("nrOfActiveInstances");//未完成的实例
Object nrOfCompletedInstances = execution.getVariable("nrOfCompletedInstances");//已完成实例
System.out.println("总实例数量"+Integer.parseInt(nrOfCompletedInstances.toString()));
System.out.println("未完成的实例"+Integer.parseInt(nrOfActiveInstances.toString()));
System.out.println("已完成实例"+Integer.parseInt(nrOfCompletedInstances.toString()));
if (Float.parseFloat(nrOfCompletedInstances.toString())/Float.parseFloat(nrOfInstances.toString()) > 0.5){
return true;//如果完成的比例高于50%就返回ture,代表会签结束,没有完成的任务就自动结束了
}else {
return false;//如果完成的比例不高于50%就返回false,还需要继续会签
}
}
}
测试与内部机制思考
当部署后开启实例,在风控审批节点创造三个任务,因为会签组有三个人。
我们分别用会签组成员去领取任务并完成,当第一个完成后,还剩两个任务。
然后再完成一个任务,任务列表就没有了,原因是,根据”完成条件代码“,超过了50%的成员会签了,其余任务就自动结束。
可以查看act_hi_taskinst 历史任务表,确实有个任务没有申领,就结束了。
对我而言,我必须用自己的钱支持自己的观点。我的亏损已经教会我,在无法确定自己必须撤退前,我根本不应该前进。但如果我不前进,我就不会有任何行动。讲这一点,我意思并不是说,当一个人犯错的时候他不应该去止损。他应该这样做,但那不能让他变得优柔寡断。我一辈子都在犯错,但是在亏损中我收获了经验,积累了诸多宝贵的教训。我破产过好多次,但我的亏损从来都不是彻底的失败,否则我现在也不会在这里了。我一直相信我有其他机会,而且犯过的错误我不会再犯第二次。只有一件事情可以让我相信自己犯错误了,那就是赔钱。只有赚到了钱,我才是正确的,这就是投机