Camunda如何实现驳回

💖专栏简介

✔️本专栏将从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】审批通过后还是会卡在并行网关

正是因为允许随意启动和取消活动,过于灵活自然会带来一些问题,取决于具体的流程模型。

  • 44
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LoneWalker、

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值