专栏简介
️本专栏将从Camunda(卡蒙达) 7中的关键概念到实现中国式工作流相关功能。
️文章中只包含演示核心代码及测试数据,完整代码可查看作者的开源项目snail-camunda
️请给snail-camunda 点颗星吧
前言
驳回可以算是我们国家特色流程的一种方式,驳回在流程图上不应该有具体的流程线,它应该是隐性的,是审批人对自己待办任务的一种操作。而很多教程都是在图上设置连线【如下图】,通过满足连线上的条件来达到驳回的目的,这种实现方式简单一点的流程图还能凑合看,你要是10多个节点还怎么看图啊,严重影响体验。
简单的流程实现驳回
先演示一个简单的驳回并且动态获取审批人的案例
和之前的一样,把执行监听器的路径换成自己的
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1i3dpos" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.19.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.20.0">
<bpmn:process id="Process_18ein8i" isExecutable="true" camunda:historyTimeToLive="180">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1e2ksci</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1e2ksci" sourceRef="StartEvent_1" targetRef="Activity_0t0na59">
<bpmn:extensionElements />
</bpmn:sequenceFlow>
<bpmn:userTask id="Activity_0t0na59" name="发起人" camunda:assignee="${initiator}">
<bpmn:incoming>Flow_1e2ksci</bpmn:incoming>
<bpmn:outgoing>Flow_048c77g</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_048c77g" sourceRef="Activity_0t0na59" targetRef="Activity_1gi2y5i">
<bpmn:extensionElements>
<camunda:executionListener class="com.lonewalker.demo.listener.CustomExecutionListener" event="take" />
</bpmn:extensionElements>
</bpmn:sequenceFlow>
<bpmn:userTask id="Activity_1gi2y5i" name="经理" camunda:assignee="${assignee}">
<bpmn:incoming>Flow_048c77g</bpmn:incoming>
<bpmn:outgoing>Flow_1iykwgr</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics camunda:collection="${assigneeList}" camunda:elementVariable="assignee">
<bpmn:completionCondition xsi:type="bpmn:tFormalExpression">${nrOfCompletedInstances == 1}</bpmn:completionCondition>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:userTask>
<bpmn:endEvent id="Event_0ksobhd">
<bpmn:incoming>Flow_1iykwgr</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1iykwgr" sourceRef="Activity_1gi2y5i" targetRef="Event_0ksobhd" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_18ein8i">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0tb19ah_di" bpmnElement="Activity_0t0na59">
<dc:Bounds x="270" y="77" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0psrsxg_di" bpmnElement="Activity_1gi2y5i">
<dc:Bounds x="430" y="77" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0ksobhd_di" bpmnElement="Event_0ksobhd">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1e2ksci_di" bpmnElement="Flow_1e2ksci">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_048c77g_di" bpmnElement="Flow_048c77g">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1iykwgr_di" bpmnElement="Flow_1iykwgr">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
新增驳回任务接口
/**
* 流程实例相关接口
*
* @author lonewalker
*/
@RequestMapping("/process/instance")
@RequiredArgsConstructor
@RestController
public class ProcessInstanceController {
private final RuntimeService runtimeService;
private final TaskService taskService;
/**
* 根据流程定义key发起流程实例
*
* @param requestParam 请求参数
* @return 流程实例id
*/
@PostMapping("/startProcessInstanceByKey")
public String startProcessInstanceByKey(@RequestBody StartProcessRequest requestParam) {
Map<String, Object> paramMap = new HashMap<>(8);
paramMap.put("initiator", requestParam.getInitiator());
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(requestParam.getProcessDefinitionKey(), requestParam.getBusinessKey(), paramMap);
return processInstance.getProcessInstanceId();
}
/**
* 完成单个任务
*
* @param requestParam 请求参数
* @return {@code true 成功}
*/
@PostMapping("/completeSingleTask")
public Boolean completeSingleTask(@RequestBody @Validated CompleteTaskRequest requestParam) {
taskService.complete(requestParam.getTaskId());
return true;
}
/**
* 转交任务
*
* @param requestParam 请求参数
* @return {@code true 成功}
*/
@PostMapping("/transferTask")
public Boolean transferTask(@RequestBody TransferTaskRequest requestParam){
taskService.setAssignee(requestParam.getTaskId(), requestParam.getUserId());
return true;
}
/**
* 驳回任务
*
* @param requestParam 请求参数
* @return {@code true 成功}
*/
@PostMapping("/rejectTask")
public Boolean rejectTask(@RequestBody RejectTaskRequest requestParam){
String processInstanceId = requestParam.getProcessInstanceId();
//获取当前环节实例 这是个树结构
ActivityInstance activity = runtimeService.getActivityInstance(processInstanceId);
runtimeService.createProcessInstanceModification(processInstanceId)
//取消现有的活动实例
.cancelActivityInstance(activity.getId())
//设置备注
.setAnnotation("驳回")
//让流程实例从目标活动重新开始
.startBeforeActivity(requestParam.getTargetNodeId())
.execute();
return true;
}
}
驳回接口的请求参数类
@Data
public class RejectTaskRequest {
private String processInstanceId;
/**
* 驳回的目标节点id
*/
private String targetNodeId;
private String taskId;
}
自定义执行监听器,基本的注释已经写上了,不懂的可评论区讨论
@Slf4j
@Component
public class CustomExecutionListener implements ExecutionListener {
@Resource
private RepositoryService repositoryService;
@Override
public void notify(DelegateExecution execution) {
ExecutionEntity executionEntity = (ExecutionEntity) execution;
TransitionImpl transition = executionEntity.getTransition();
//连线的目标节点id
String targetNodeId = transition.getDestination().getId();
//针对多实例节点
if (targetNodeId.contains(ActivityTypes.MULTI_INSTANCE_BODY)) {
targetNodeId = targetNodeId.substring(0, targetNodeId.indexOf("#"));
}
//无论审批人是角色还是具体的人员,都是和节点关联并在保存流程定义时就入库的
// 所以使用流程定义key+版本号+节点id 查询节点配置的审批人
//流程定义id
String processDefinitionId = executionEntity.getProcessDefinitionId();
//流程定义key
String processDefinitionKey = processDefinitionId.substring(0, processDefinitionId.indexOf(":"));
//获取流程定义的版本
final int version = repositoryService.getProcessDefinition(processDefinitionId).getVersion();
String processInstanceId = executionEntity.getProcessInstanceId();
//模拟不同的节点分配不同的审批人
List<String> assigneeList = new ArrayList<>();
if ("Activity_03k4yru".equals(targetNodeId)) {
assigneeList.add("10087");
} else if ("Activity_108tjp3".equals(targetNodeId)){
assigneeList.add("10088");
}
//设置审批人
execution.setVariable("assigneeList", assigneeList);
}
}
部署流程定义并发起后,第一次经理节点是一个人
在调用驳回接口之前,先去修改执行监听器中的审批人数据,模拟角色对应的人员变动:
可以看到流程已经回到发起人节点,发起人再提交其实就是把自己这个待办任务完成
再次回到该节点可以发现审批人是更新后的:
经过执行监听器的驳回
上述那种可以看做是审批人不是通过执行监听器设置的情况,可以直接驳回到目标节点上。
当审批人是通过执行监听器设置的,就需要让流程实例从目标节点前的连线重新开始。
修改驳回接口:
/**
* 驳回任务
*
* @param requestParam 请求参数
* @return {@code true 成功}
*/
@PostMapping("/rejectTask")
public Boolean rejectTask(@RequestBody RejectTaskRequest requestParam){
String processInstanceId = requestParam.getProcessInstanceId();
String targetNodeId = requestParam.getTargetNodeId();
//获取当前环节实例 这是个树结构
ActivityInstance activity = runtimeService.getActivityInstance(processInstanceId);
//获取实例的流程定义 这里主要是为了拿到节点前的那条线的Id
List<HistoricActivityInstance> hisActivityList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).finished().list();
List<String> nodeIds = hisActivityList.stream().map(HistoricActivityInstance::getActivityId).collect(Collectors.toList());
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
BpmnModelInstance bpmnModel = repositoryService.getBpmnModelInstance(processInstance.getProcessDefinitionId());
ModelElementInstance modelElement = bpmnModel.getModelElementById(targetNodeId);
UserTask userTask = (UserTask) modelElement;
Collection<SequenceFlow> incoming = userTask.getIncoming();
String transitionId = "";
for (SequenceFlow sequenceFlow : incoming) {
FlowNode source = sequenceFlow.getSource();
if (nodeIds.contains(source.getId())) {
transitionId = sequenceFlow.getId();
break;
}
}
//注意这里的调整,取消的活动实例是驳回的任务所对应的
runtimeService.createProcessInstanceModification(processInstanceId)
.cancelActivityInstance(activity.getId())
.setAnnotation("驳回")
.startTransition(transitionId)
.execute();
return true;
}
发起流程后让流程来到【主管】节点
此时调用驳回,会再次触发【经理】节点前的执行监听器
扩展
假设上图的多实例节点处于活动状态,并且有两个实例,则它的活动树如下:
ProcessInstance
客户 - Multi-Instance Body
客户
客户
下面的修改在同一个多实例主体活动中启动了【客户】活动的第三个实例:
ProcessInstance processInstance = ...;
runtimeService.createProcessInstanceModification(processInstance.getId())
.startBeforeActivity("客户")
.execute();
是不是有点加签的意思了?️
但是凡事有利必有弊,流程实例修改是一个非常强大的工具,允许随意启动和取消活动。因此,很容易创建正常流程执行无法到达的情况,如下图:
当存在并行网关时,【经理2】已通过,【经理1】驳回,但是【经理2】通过后执行会卡在并行网关上,因为没有令牌会到达其他传入序列流,从而激活并行网关。【我们知道并行网关有个Join机制,也就是到达并行网关的所有并发执行在网关等待,直到每个传入序列流的执行到达为止。】
然而当发起人再次发起,原先卡在并行网关的执行反而会通过,流程走到【主管节点】
【经理1】审批通过后还是会卡在并行网关
正是因为允许随意启动和取消活动,过于灵活自然会带来一些问题,取决于具体的流程模型。
文章评论