场景描述:
由于公司需要在流程图中明确的显示不同部门的流程情况,而这些部门是并行的,最后要有系统自动判断这些节点的审核结果给出一个最终的结果,来确定流程的走向,如果是单个审核节点的话可以使用多实例(设置多实例结束条件即可),但是通常业务中包含着不止一个节点,这种情况我们可以使用子流程解决,也可以使用下边我要介绍的方案,这个方案我只画了每个并行节点的一个审核,但是实际通常会有多个,我只做例子讲解,就不画多个了。
注意:如果大家有更好的解决,请大家在下方留言,谢谢。(还有一种简单的解决方案是设置任务过期时间)
- 流程图如下
- 流程图xml
<?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="audit-article" name="文章审核" isExecutable="true"> <documentation>测试流程</documentation> <startEvent id="start" name="开始"></startEvent> <userTask id="group_leader_approve" name="组长审核" flowable:candidateGroups="role_group_leader"></userTask> <sequenceFlow id="sid-F0149E5B-E9A1-49C9-A243-638C4E21E378" sourceRef="start" targetRef="group_leader_approve"></sequenceFlow> <parallelGateway id="sid-6E2896F8-BF54-4F7B-BF14-DF9C850E7BDE"></parallelGateway> <userTask id="editor1_approval" name="主编1审核" flowable:assignee="${group_editor1}"> <extensionElements> <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete> </extensionElements> </userTask> <userTask id="editor2_approval" name="主编2审核" flowable:assignee="${group_editor2}"> <extensionElements> <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete> </extensionElements> </userTask> <parallelGateway id="sid-56FAADCB-CE8E-4F10-834C-EB29D9A30F72"></parallelGateway> <userTask id="system_judge" name="系统判断" flowable:assignee="system_judge"> <extensionElements> <flowable:taskListener event="create" class="com.spark.platform.flowable.biz.listener.MyTaskListener"></flowable:taskListener> <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete> </extensionElements> </userTask> <exclusiveGateway id="sid-AC406909-E994-4CD0-9AEC-901ABB27688E"></exclusiveGateway> <userTask id="submit_approval" name="退回修改" flowable:assignee="${submitter}"> <extensionElements> <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete> </extensionElements> </userTask> <endEvent id="pass" name="通过"></endEvent> <exclusiveGateway id="sid-E2A8AD68-FA23-4D9B-8CA4-863CA08E483A"></exclusiveGateway> <exclusiveGateway id="sid-A9213851-D3FA-450B-8A9D-FCD4C54F2440"></exclusiveGateway> <endEvent id="over" name="关闭流程"></endEvent> <sequenceFlow id="sid-89703E3C-E4C1-4DBF-9462-2DAE98EF4D55" sourceRef="editor1_approval" targetRef="sid-56FAADCB-CE8E-4F10-834C-EB29D9A30F72"></sequenceFlow> <sequenceFlow id="sid-C91352A8-84D9-474E-89A5-0AF0EFC5F58B" sourceRef="editor2_approval" targetRef="sid-56FAADCB-CE8E-4F10-834C-EB29D9A30F72"></sequenceFlow> <sequenceFlow id="sid-FF7E4059-6000-4335-8F8E-AC2295A8DB5B" sourceRef="sid-56FAADCB-CE8E-4F10-834C-EB29D9A30F72" targetRef="system_judge"></sequenceFlow> <sequenceFlow id="sid-71206A86-A1DC-42CE-92FF-179507B2AC04" sourceRef="submit_approval" targetRef="sid-A9213851-D3FA-450B-8A9D-FCD4C54F2440"></sequenceFlow> <sequenceFlow id="sid-606C543E-F974-42F7-8BBE-86E19E6A40FC" sourceRef="group_leader_approve" targetRef="sid-E2A8AD68-FA23-4D9B-8CA4-863CA08E483A"></sequenceFlow> <sequenceFlow id="sid-8882D268-789B-4A66-99A8-513C8F19E21C" sourceRef="sid-A9213851-D3FA-450B-8A9D-FCD4C54F2440" targetRef="group_leader_approve"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${SUBMIT_APPROVAL_SUBMIT_VALUE == 'GROUP_LEADER_APPROVAL'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="sid-32CBC3C6-757D-41CC-AC34-3CC7E71C226A" sourceRef="sid-6E2896F8-BF54-4F7B-BF14-DF9C850E7BDE" targetRef="editor1_approval"></sequenceFlow> <sequenceFlow id="sid-9D742EBD-78A0-4CBE-884C-BBE85340FC5A" sourceRef="sid-6E2896F8-BF54-4F7B-BF14-DF9C850E7BDE" targetRef="editor2_approval"></sequenceFlow> <sequenceFlow id="sid-C39C6BE4-57FF-4153-8C1E-1D97D37146DE" sourceRef="sid-A9213851-D3FA-450B-8A9D-FCD4C54F2440" targetRef="over"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${SUBMIT_APPROVAL_SUBMIT_VALUE == 'OVER'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="sid-E2AE9738-1E1E-4C66-853C-8E8E50E7D5BD" name="退回修改" sourceRef="sid-E2A8AD68-FA23-4D9B-8CA4-863CA08E483A" targetRef="submit_approval"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${GROUP_LEADER_APPROVE_SUBMIT_VALUE=='SUBMIT_APPROVAL'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="sid-97861805-692B-48CA-BBC8-C3BCF604E7AB" name="通过" sourceRef="sid-E2A8AD68-FA23-4D9B-8CA4-863CA08E483A" targetRef="sid-6E2896F8-BF54-4F7B-BF14-DF9C850E7BDE"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${GROUP_LEADER_APPROVE_SUBMIT_VALUE=='GROUP_EDITOR'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="sid-88320958-56A0-458B-A8DC-D6EB60D548BA" sourceRef="system_judge" targetRef="sid-AC406909-E994-4CD0-9AEC-901ABB27688E"></sequenceFlow> <sequenceFlow id="sid-7492310C-5318-4379-8DEB-53D6CA7E6AD9" name="退回修改" sourceRef="sid-AC406909-E994-4CD0-9AEC-901ABB27688E" targetRef="submit_approval"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${SYSTEM_JUDGE_SUBMIT_VALUE == 'SUBMIT_APPROVAL'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="sid-38F4E395-FC66-4BA5-B5E5-FB4C217FF3DC" sourceRef="sid-AC406909-E994-4CD0-9AEC-901ABB27688E" targetRef="pass"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${SYSTEM_JUDGE_SUBMIT_VALUE == 'PASS'}]]></conditionExpression> </sequenceFlow> </process> <bpmndi:BPMNDiagram id="BPMNDiagram_audit-article"> <bpmndi:BPMNPlane bpmnElement="audit-article" id="BPMNPlane_audit-article"> <bpmndi:BPMNShape bpmnElement="start" id="BPMNShape_start"> <omgdc:Bounds height="30.0" width="30.0" x="10.0" y="150.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="group_leader_approve" id="BPMNShape_group_leader_approve"> <omgdc:Bounds height="80.0" width="100.0" x="105.0" y="125.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="sid-6E2896F8-BF54-4F7B-BF14-DF9C850E7BDE" id="BPMNShape_sid-6E2896F8-BF54-4F7B-BF14-DF9C850E7BDE"> <omgdc:Bounds height="40.0" width="40.0" x="370.0" y="145.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="editor1_approval" id="BPMNShape_editor1_approval"> <omgdc:Bounds height="80.0" width="100.0" x="460.0" y="45.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="editor2_approval" id="BPMNShape_editor2_approval"> <omgdc:Bounds height="80.0" width="100.0" x="460.0" y="180.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="sid-56FAADCB-CE8E-4F10-834C-EB29D9A30F72" id="BPMNShape_sid-56FAADCB-CE8E-4F10-834C-EB29D9A30F72"> <omgdc:Bounds height="40.0" width="40.0" x="593.5" y="145.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="system_judge" id="BPMNShape_system_judge"> <omgdc:Bounds height="80.0" width="100.0" x="678.5" y="125.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="sid-AC406909-E994-4CD0-9AEC-901ABB27688E" id="BPMNShape_sid-AC406909-E994-4CD0-9AEC-901ABB27688E"> <omgdc:Bounds height="40.0" width="40.0" x="823.5" y="145.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="submit_approval" id="BPMNShape_submit_approval"> <omgdc:Bounds height="80.0" width="100.0" x="222.5" y="270.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="pass" id="BPMNShape_pass"> <omgdc:Bounds height="28.0" width="28.0" x="908.5" y="151.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="sid-E2A8AD68-FA23-4D9B-8CA4-863CA08E483A" id="BPMNShape_sid-E2A8AD68-FA23-4D9B-8CA4-863CA08E483A"> <omgdc:Bounds height="40.0" width="40.0" x="255.0" y="145.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="sid-A9213851-D3FA-450B-8A9D-FCD4C54F2440" id="BPMNShape_sid-A9213851-D3FA-450B-8A9D-FCD4C54F2440"> <omgdc:Bounds height="40.0" width="40.0" x="135.0" y="290.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="over" id="BPMNShape_over"> <omgdc:Bounds height="28.0" width="28.0" x="141.0" y="390.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNEdge bpmnElement="sid-9D742EBD-78A0-4CBE-884C-BBE85340FC5A" id="BPMNEdge_sid-9D742EBD-78A0-4CBE-884C-BBE85340FC5A"> <omgdi:waypoint x="390.5" y="184.43264652014656"></omgdi:waypoint> <omgdi:waypoint x="390.5" y="220.0"></omgdi:waypoint> <omgdi:waypoint x="460.0" y="220.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-E2AE9738-1E1E-4C66-853C-8E8E50E7D5BD" id="BPMNEdge_sid-E2AE9738-1E1E-4C66-853C-8E8E50E7D5BD"> <omgdi:waypoint x="274.66101694915255" y="184.6118644067797"></omgdi:waypoint> <omgdi:waypoint x="273.1887931034483" y="270.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-32CBC3C6-757D-41CC-AC34-3CC7E71C226A" id="BPMNEdge_sid-32CBC3C6-757D-41CC-AC34-3CC7E71C226A"> <omgdi:waypoint x="390.5" y="145.5"></omgdi:waypoint> <omgdi:waypoint x="390.5" y="85.0"></omgdi:waypoint> <omgdi:waypoint x="460.0" y="85.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-88320958-56A0-458B-A8DC-D6EB60D548BA" id="BPMNEdge_sid-88320958-56A0-458B-A8DC-D6EB60D548BA"> <omgdi:waypoint x="778.4499999999999" y="165.0"></omgdi:waypoint> <omgdi:waypoint x="823.5" y="165.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-38F4E395-FC66-4BA5-B5E5-FB4C217FF3DC" id="BPMNEdge_sid-38F4E395-FC66-4BA5-B5E5-FB4C217FF3DC"> <omgdi:waypoint x="863.4373893805309" y="165.0"></omgdi:waypoint> <omgdi:waypoint x="908.5" y="165.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-F0149E5B-E9A1-49C9-A243-638C4E21E378" id="BPMNEdge_sid-F0149E5B-E9A1-49C9-A243-638C4E21E378"> <omgdi:waypoint x="39.94999891869115" y="165.0"></omgdi:waypoint> <omgdi:waypoint x="105.0" y="165.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-C91352A8-84D9-474E-89A5-0AF0EFC5F58B" id="BPMNEdge_sid-C91352A8-84D9-474E-89A5-0AF0EFC5F58B"> <omgdi:waypoint x="559.9499999999681" y="220.0"></omgdi:waypoint> <omgdi:waypoint x="613.5" y="220.0"></omgdi:waypoint> <omgdi:waypoint x="613.5" y="184.9180783242259"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-7492310C-5318-4379-8DEB-53D6CA7E6AD9" id="BPMNEdge_sid-7492310C-5318-4379-8DEB-53D6CA7E6AD9"> <omgdi:waypoint x="843.5" y="184.94312543073747"></omgdi:waypoint> <omgdi:waypoint x="843.5" y="310.0"></omgdi:waypoint> <omgdi:waypoint x="322.45000000000005" y="310.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-97861805-692B-48CA-BBC8-C3BCF604E7AB" id="BPMNEdge_sid-97861805-692B-48CA-BBC8-C3BCF604E7AB"> <omgdi:waypoint x="294.94133362293655" y="165.0"></omgdi:waypoint> <omgdi:waypoint x="370.0" y="165.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-89703E3C-E4C1-4DBF-9462-2DAE98EF4D55" id="BPMNEdge_sid-89703E3C-E4C1-4DBF-9462-2DAE98EF4D55"> <omgdi:waypoint x="559.9499999999823" y="85.0"></omgdi:waypoint> <omgdi:waypoint x="613.5" y="85.0"></omgdi:waypoint> <omgdi:waypoint x="613.5" y="145.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-C39C6BE4-57FF-4153-8C1E-1D97D37146DE" id="BPMNEdge_sid-C39C6BE4-57FF-4153-8C1E-1D97D37146DE"> <omgdi:waypoint x="155.0" y="329.93939957492034"></omgdi:waypoint> <omgdi:waypoint x="155.0" y="390.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-FF7E4059-6000-4335-8F8E-AC2295A8DB5B" id="BPMNEdge_sid-FF7E4059-6000-4335-8F8E-AC2295A8DB5B"> <omgdi:waypoint x="633.4413336229366" y="165.0"></omgdi:waypoint> <omgdi:waypoint x="678.5" y="165.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-71206A86-A1DC-42CE-92FF-179507B2AC04" id="BPMNEdge_sid-71206A86-A1DC-42CE-92FF-179507B2AC04"> <omgdi:waypoint x="222.5" y="310.0"></omgdi:waypoint> <omgdi:waypoint x="174.9084540032961" y="310.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-606C543E-F974-42F7-8BBE-86E19E6A40FC" id="BPMNEdge_sid-606C543E-F974-42F7-8BBE-86E19E6A40FC"> <omgdi:waypoint x="204.95" y="165.0"></omgdi:waypoint> <omgdi:waypoint x="255.0" y="165.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="sid-8882D268-789B-4A66-99A8-513C8F19E21C" id="BPMNEdge_sid-8882D268-789B-4A66-99A8-513C8F19E21C"> <omgdi:waypoint x="155.0" y="290.0"></omgdi:waypoint> <omgdi:waypoint x="155.0" y="204.95"></omgdi:waypoint> </bpmndi:BPMNEdge> </bpmndi:BPMNPlane> </bpmndi:BPMNDiagram> </definitions>
此时我们可以看到主编1和主编2 是并行审核的,当他们审核完的时候要有有一个系统判断来判断最终的审核结果,决定流程的走向。
解决方案:
在系统判断这个节点的时候我们就要是使用任务监听器:TaskListener
/**
* @ProjectName: spark-platform
* @Package: com.spark.platform.flowable.biz.listener
* @ClassName: TaskListener
* @Author: wangdingfeng
* @Description: 任务监听器
* @Date: 2020/4/8 14:22
* @Version: 1.0
*/
@Slf4j
@Component
public class MyTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
String eventName = delegateTask.getEventName();
switch (eventName) {
case BaseTaskListener.EVENTNAME_CREATE:
log.info("当前监听创建事件:create");
//开启消息发送线程
Thread thread = new Thread(
new MessageRunnableTask(delegateTask));
thread.start();
break;
case BaseTaskListener.EVENTNAME_ASSIGNMENT:
log.info("当前监听指派事件:assignment");
break;
case BaseTaskListener.EVENTNAME_COMPLETE:
log.info("当前监听完成事件:complete");
break;
case BaseTaskListener.EVENTNAME_DELETE:
log.info("当前监听销毁事件:delete");
break;
default:
break;
}
}
}
TaskListener 虽然是可以做监听,但是由于此时任务数据还没有在数据库中落地,所以无法获取到当前节点的taskID,不能自动推动流程的走向,所以我加了一个线程用来刷此个节点的taskId,代码如下
/**
* @ProjectName: spark-platform
* @Package: com.spark.platform.flowable.biz.listener
* @ClassName: RunnableTask
* @Author: wangdingfeng
* @Description: 线程
* @Date: 2020/4/14 11:07
* @Version: 1.0
*/
@Slf4j
public class MessageRunnableTask implements Runnable {
private DelegateTask delegateTask;
MessageRunnableTask(DelegateTask delegateTask) {
this.delegateTask = delegateTask;
}
@Override
public void run() {
Task task = getTask(delegateTask);
StringRedisTemplate redisTemplate = SpringContextHolder.getBean(StringRedisTemplate.class);
TaskVO taskVO = new TaskVO();
BeanUtil.copyProperties(task, taskVO, "variables");
taskVO.setVariables(task.getProcessVariables());
redisTemplate.convertAndSend(RedisTopicName.topicName, JSONObject.toJSONString(taskVO));
}
/**
* 递归查询 直到查询到task任务已经落地为止,然后发布消息
*
* @param delegateTask
* @return
*/
private Task getTask(DelegateTask delegateTask) {
ActTaskQueryService actTaskQueryService = SpringContextHolder.getBean(ActTaskQueryService.class);
Task task = null;
while (null == task){
log.info("开是休眠40S");
ThreadUtil.sleep(1000 * 40);
task = actTaskQueryService.createTaskQuery().processInstanceId(delegateTask.getProcessInstanceId()).taskAssignee(delegateTask.getAssignee()).includeProcessVariables().singleResult();
}
log.info("代办任务数据库已经落地----");
return task;
}
}
由于通常我们的业务代码不会在工作流这个服务中判断,通常由业务发起服务器判断,所以我使用了消息订阅。此示例代码用的是redis消息发布订阅,实际开发中不要使用redis,由于redis的消息队列一旦消费者不存在,就会出现消息丢失的情况,请使用rabbitmq这些消息队列。
Redis 订阅处理消息
@Component
@Slf4j
public class RedisChannelListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
//接受消息
byte[] channel = message.getChannel();
byte[] body = message.getBody();
try {
String content = new String(body, "UTF-8");
String address = new String(channel, "UTF-8");
log.info("接收到频道{}发来的消息:{}", address, address);
switch (address) {
case RedisTopicName.topicName:
articleMessage(content);
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 文章消息处理
*
* @param content
*/
public void articleMessage(String content) {
log.info("开始处理文章信息");
TaskVO taskVO = JSONObject.parseObject(content,TaskVO.class);
//推进任务
boolean flag = true;
Iterator<Map.Entry<String, Object>> it = taskVO.getVariables().entrySet().iterator();
//判断两个主编的最终审核结果 只有当两个主编都审核通过才通过
while (it.hasNext()) {
Map.Entry<String, Object> entry = it.next();
if (entry.getKey().startsWith("multiInstance_result")) {
if (!(boolean) entry.getValue()) {
flag = false;
break;
}
}
}
ActTaskService actTaskService = SpringContextHolder.getBean(ActTaskService.class);
Map<String, Object> map = ImmutableMap.of("SYSTEM_JUDGE_SUBMIT_VALUE", "SUBMIT_APPROVAL");
if (flag) {
map = ImmutableMap.of("SYSTEM_JUDGE_SUBMIT_VALUE", "PASS");
}
actTaskService.complete(taskVO.getId(), map);
}
}
具体的例子,请参考我的开源项目:
spark: https://gitee.com/dreamfeng/spark-platform