【2025】camunda 使用Bean和Rest接口方式实现串行并行多种复杂情况下的驳回、取回功能(4)

前言

在前面的三篇文章我们介绍了Camunda的的使用以及相关api介绍。这篇主要在实际开发过程中都会用到的一个场景需求。工作流流程任务中的驳回和取回功能实现

该专栏主要为介绍camunda的学习和使用

场景展现

我们在进行执行一个流程的时候可能会出现多种情况:

  • 情况1:1 -> 2 -> 3 -> 4
    顺序执行,直接根据流程图从第一个活动执行到最后一个活动,这个流程任务实例在执行过程中没有发生过驳回取回等情况(可查看历史的活动实例表ACT_HI_ACTINST

    在这里插入图片描述
    也可以通过ACT_HI_ACTINST表查看到他的执行过程中时间是按照顺序从开始的startEvent活动到介绍的EndEvent活动
    在这里插入图片描述

  • 情况2:1 -> 2 -> 3 -> 2 -> 3
    出现过驳回(取回)的情况,出现过的话,ACT_HI_ACTINST历史实例表就可以看到执行的活动就会混乱

    在这里插入图片描述

    可以看到,下面的执行顺序,在主管审批后,下一步又回到了发起人的这个用户活动中

    在这里插入图片描述

  • 情况3:1 -> 2 -> 3 -> 2 -> 3 -> 4, 5

在执行的过程中出现了并行网关,把一个任务同步分配到两个用户活动里面去,这种情况下,我们应该怎么去实现任务的驳回,当一个用户驳回时,另外一个用户怎么对应的同步驳回。
在这里插入图片描述
这里直接拿上面的那个实例进行的,可以看到经过了并行网关后,对应的两个用户活动都没结束,在执行中
在这里插入图片描述

一、环境准备

1、版本

先创建好一个springboot项目,这里依旧使用的camunda7.20的版本,然后jdk使用的是17的版本,springboot版本使用的是3.3.0。这里可以根据自己的项目情况去跟换camunda7.20版本。可以在官方文档查看版本适配。

2、流程图

我这边画好了一个测试的流程图,有串行执行,也有并行执行的情况。每个用户绑定的表单可以随便设置一个简单的就行,因为这个不是重点,重点是实现驳回和取回的情况
在这里插入图片描述
简单起见我这边直接发起人和主管的审批的一个表单
在这里插入图片描述
在这里插入图片描述

经理1和经理2则直接使用Form fields的方式直接内置在里面了
在这里插入图片描述

3、依赖

和前面的一致,cmaunda的基础常用依赖

<!--        camunda相关 依赖-->
<!--        引擎-->
        <dependency>
            <groupId>org.camunda.bpm.springboot</groupId>
            <artifactId>camunda-bpm-spring-boot-starter</artifactId>
            <version>${camunda.version}</version>
        </dependency>
<!--        web ui界面-->
        <dependency>
            <groupId>org.camunda.bpm.springboot</groupId>
            <artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
            <version>${camunda.version}</version>
        </dependency>
<!--        rest接口-->
        <dependency>
            <groupId>org.camunda.bpm.springboot</groupId>
            <artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
            <version>${camunda.version}</version>
        </dependency>
<!--       openapi 文档 -->
        <dependency>
            <groupId>org.camunda.bpm</groupId>
            <artifactId>camunda-engine-rest-openapi</artifactId>
            <version>${camunda.version}</version>
        </dependency>

二、实现

实现我这里使用了两种情况,因为我们使用camunda流程引擎主要会出现两种使用情况。一种是提供maven依赖的方式直接集成在一个springboot服务内部,那样我们可以直接调用引擎的bean;还有一种是流程引擎是一个单独的服务,我们在外部通过调用接口的方式实现驳回等功能。

1、添加任务

  1. 先往部署好的流程定义里添加一个任务
    在这里插入图片描述
  2. 在把这个任务执行到下一步
    在这里插入图片描述

1、直接maven集成实现

1.1、具体实现

具体实现代码

import cn.hutool.core.collection.CollectionUtil;
import org.camunda.bpm.engine.HistoryService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.history.HistoricActivityInstance;
import org.camunda.bpm.engine.runtime.ProcessInstanceModificationBuilder;
import org.camunda.bpm.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.*;

@SpringBootTest
public class RejectTests2 {
    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private HistoryService historyService;

    @Autowired
    private TaskService taskService;


    /**
     * 驳回到开始节点
     */
    @Test
    public void returnToStartNode(){


        String processInstanceId = "4299b727-f835-11ef-b9af-46c5fd5dfd3a";
        String assignee = "demo";
        String rejectComment = "测试驳回到开始节点";

        //        设置流程中的可变参数
        HashMap<String, Object> taskVariable  = new HashMap<>(2);
        taskVariable.put("rejectUser",assignee);   //驳回人
        taskVariable.put("rejectCause", rejectComment);    //驳回原因



        rejectToLastNode(processInstanceId,0,taskVariable);

    }

    /**
     * 驳回到上一个节点
     */
    @Test
    public void rejectToLastNode(){
        String processInstanceId = "e5662008-f897-11ef-85be-46c5fd5dfd3a";
        String assignee = "安其拉";
        String rejectComment = "测试驳回到上一个节点";


        //        设置流程中的可变参数
        HashMap<String, Object> taskVariable  = new HashMap<>(2);
        taskVariable.put("rejectUser",assignee);   //驳回人
        taskVariable.put("rejectCause", rejectComment);    //驳回原因


        rejectToLastNode(processInstanceId,1,taskVariable);



    }


    /**
     *
     * @param processInstanceId  任务id
     * @param rollbackType  驳回类型(0开始节点,1上一个节点)
     * @param taskVariable 驳回写入参数
     */
    public void rejectToLastNode( String processInstanceId,Integer rollbackType,HashMap<String, Object> taskVariable){


        // 1.获取所有历史用户任务
        List<HistoricActivityInstance> historyList = historyService
                .createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId)    //任务id
                .activityType("userTask")   //活动类型
                .finished()   //已经执行完成
                .orderByHistoricActivityInstanceEndTime()
                .asc()
                .list();

        //            校验当前节点前面是否有用户节点
        if (CollectionUtil.isEmpty(historyList)){
            throw new RuntimeException("当前用户无法驳回");
        }


        // 2. 获取所有当前活动的用户任务的 activityInstanceIds,
        List<HistoricActivityInstance> operationList = historyService
                .createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId)
                .activityType("userTask")   //活动类型
                .unfinished() // 执行中
                .orderByHistoricActivityInstanceEndTime()
                .asc()
                .list();

        // 3. 获取要驳回到的节点
        String toActId ;
        if (rollbackType == 1){   //        得到上一个任务节点的ActivityId
            List<String> idList = operationList.stream().map(HistoricActivityInstance::getActivityId).toList();
            toActId = getLastNode(historyList,idList);
            if (null == toActId){
                throw new RuntimeException("回退节点异常!");
            }
        }else {   //        获取开始节点的ActivityId
            HistoricActivityInstance historicActivityInstance = historyList.get(0);
             toActId = historicActivityInstance.getActivityId();
        }


        // 4. 校验节点数据、记录日志
        List<Task> taskList = taskService.createTaskQuery()
                .processInstanceId(processInstanceId)
                .list();

        //            回退的节点和和当前节点一致表示错误
        if (taskList.stream().map(Task::getTaskDefinitionKey).toList().contains(toActId)){
            throw new RuntimeException("当前用户无法驳回!");
        }


        //        记录日志
        for (Task task : taskList){
            taskService.createComment(task.getId(),processInstanceId,"驳回原因:"+ taskVariable.get("rejectCause"));
        }


        //  5、执行回退操作
        ProcessInstanceModificationBuilder modificationBuilder  = runtimeService.createProcessInstanceModification(processInstanceId);
        //            可能会因为网关出现并行等情况,需要把所有的节点取消掉
        for (HistoricActivityInstance operation : operationList){
            modificationBuilder.cancelActivityInstance(operation.getId());     //取消当前任务节点集合
        }
        //            驳回到指定节点
        modificationBuilder
                .setAnnotation("进行驳回到上一个任务节点操作")  //记录r
                .startBeforeActivity(toActId) //驳回到指定节点
                .setVariables(taskVariable)
                .execute();


        System.out.println("成功!");
    }








    /**
     * 获取上一节点信息
     * @param resultList   历史执行节点集合
     * @param currentActivityIdList   当前活动的节点信息
     * @return String
     */
    private static String getLastNode(List<HistoricActivityInstance> resultList,List<String> currentActivityIdList){
//        新建一个不重复的有序集合
        LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap<>();

        for (HistoricActivityInstance hai : resultList){
            linkedHashMap.put(hai.getActivityId(),hai.getAssignee());
        }

        boolean flag = currentActivityIdList.stream().anyMatch(linkedHashMap::containsKey);
        if(!flag) {
//            情况1:当前节点不在历史节点里;最后一个节点是完成节点
            HistoricActivityInstance historicActivityInstance = resultList.get(resultList.size() - 1);

            return historicActivityInstance.getActivityId();
        }else {
//          情况2:当前节点在历史节里(已回退过的)

            ListIterator<Map.Entry<String, String>> li = new ArrayList<>(linkedHashMap.entrySet()).listIterator();

            while (li.hasNext()){
                Map.Entry<String, String> entry = li.next();
                System.out.println("已回退过的");
                if (currentActivityIdList.contains(entry.getKey())) {
                    li.previous();
                    Map.Entry<String, String> previous = li.previous();
                    return previous.getKey();
                }
            }
        }
        return null;
    }

}

1.2、代码解析

  1. 获取所有历史用户任务
    获取到ACT_HI_ACTINST表的历史活动情况,processInstanceId任务的id,匹配表中的proc_inst_id_字段;在通过activityType去匹配用户活动,如果需要驳回到的节点不是用户任务也可以通过这个字段设置;finished表示已经执行完毕的(unfinished表示正在执行中的,二选一),然后再根据时间排序,获取到集合

          // 1.获取所有历史用户任务
        List<HistoricActivityInstance> historyList = historyService
                .createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId)    //任务id
                .activityType("userTask")   //活动类型
                .finished()   //已经执行完成
                .orderByHistoricActivityInstanceEndTime()
                .asc()
                .list();
    
  2. 获取所有当前活动的用户任务
    同样是和上面一样获取ACT_HI_ACTINST历史表的数据,查询的条件也基本一致,只不过是使用unfinished改为去查询当前正在执行的实例。

            // 2. 获取所有当前活动的用户任务的 activityInstanceIds,
            List<HistoricActivityInstance> operationList = historyService
                    .createHistoricActivityInstanceQuery()
                    .processInstanceId(processInstanceId)
                    .activityType("userTask")   //活动类型
                    .unfinished() // 执行中
                    .orderByHistoricActivityInstanceStartTime()
                    .asc()
                    .list();
    
  3. 获取要驳回到的节点
    这个地方分两种情况,一般我们可能会出现驳回上一个节点和取回到开始节点两种情况。

            String toActId ;
            if (rollbackType == 1){   //        得到上一个任务节点的ActivityId
                List<String> idList = operationList.stream().map(HistoricActivityInstance::getActivityId).toList();
                toActId = getLastNode(historyList,idList);
                if (null == toActId){
                    throw new RuntimeException("回退节点异常!");
                }
            }else {   //        获取开始节点的ActivityId
                HistoricActivityInstance historicActivityInstance = historyList.get(0);
                 toActId = historicActivityInstance.getActivityId();
            }
    

    获取上一个节点具体实现代码:

    • 获取开始节点:我们直接通过前面获取的historyList 历史用户节点取出第一个就行,就是我们的开始用户节点。
    • 获取上一节点:这会出现两种情况:
      • 1 -> 2 -> 3 -> 4:如果没有出现驳回过的情况,直接按照时间排序,取出最后一个即可。
      • 1 -> 2 -> 3 -> 2 -> 3:如果已经出现过了驳回的情况,则通过时间排序没法取出,因为上一个可能是驳回的,2前面可能会是3。我们只需要创建一个有序LinkedHashMap,因为key是可覆盖的,所以我们的最终排序只会出现:1 -> 2 -> 3,3是当前的节点,我们只需要取出当前的3的上一个节点即可找到需要驳回的节点。

    执行示例:
    可以看到依旧可以找到对应的上一个历史节点在这里插入图片描述

    
    /**
     * 获取上一节点信息
     * @param resultList   历史执行节点集合
     * @param currentActivityIdList   当前活动的节点信息
     * @return String
     */
        private static String getLastNode(List<HistoricActivityInstance> resultList,List<String> currentActivityIdList){
    //        新建一个不重复的有序集合
            LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap<>();
    
            for (HistoricActivityInstance hai : resultList){
                linkedHashMap.put(hai.getActivityId(),hai.getAssignee());
            }
    
            boolean flag = currentActivityIdList.stream().anyMatch(linkedHashMap::containsKey);
            if(!flag) {
    //            情况1:当前节点不在历史节点里;最后一个节点是完成节点
                HistoricActivityInstance historicActivityInstance = resultList.get(resultList.size() - 1);
                System.out.println("未回退过的");
                return historicActivityInstance.getActivityId();
            }else {
    //          情况2:当前节点在历史节里(已回退过的)
    
                ListIterator<Map.Entry<String, String>> li = new ArrayList<>(linkedHashMap.entrySet()).listIterator();
    
                while (li.hasNext()){
                    Map.Entry<String, String> entry = li.next();
                    System.out.println("已回退过的");
                    if (currentActivityIdList.contains(entry.getKey())) {   //匹配当前节点
                        li.previous();
                        Map.Entry<String, String> previous = li.previous();  //获取上一个节点
                        return previous.getKey();
                    }
                }
            }
            return null;
        }
    
  4. 校验节点数据、记录日志
    通过taskService去act_ru_task获取当前实例处在的活动节点集合(如果并行会出现多个)。如果和驳回的节点一致则代表有问题。并且记录驳回的一个日志记录(记录在:act_hi_comment

            // 4. 校验节点数据、记录日志
            List<Task> taskList = taskService.createTaskQuery()
                    .processInstanceId(processInstanceId)
                    .list();
    
            //            回退的节点和和当前节点一致表示错误
            if (taskList.stream().map(Task::getTaskDefinitionKey).toList().contains(toActId)){
                throw new RuntimeException("当前用户无法驳回!");
            }
    
    
            //        记录日志
            for (Task task : taskList){
                taskService.createComment(task.getId(),processInstanceId,"驳回原因:"+ taskVariable.get("rejectCause"));
            }
    
  5. 执行回退操作
    分为了两部分:
    首先我们要把当前正在执行的节点取消掉(如果有在并行执行,是会出现多个的);
    然后再记录日志,再通过startBeforeActivity驳回到指定的节点;以及我们可能再驳回的时候需要写对应的一些驳回信息变量。则通过setVariables以Map的方式写入。

            ProcessInstanceModificationBuilder modificationBuilder  = runtimeService.createProcessInstanceModification(processInstanceId);
            //            可能会因为网关出现并行等情况,需要把所有的节点取消掉
            for (HistoricActivityInstance operation : operationList){
                modificationBuilder.cancelActivityInstance(operation.getId());     //取消当前任务节点集合
            }
            //            驳回到指定节点
            modificationBuilder
                    .setAnnotation("进行驳回到上一个任务节点操作")  //记录r
                    .startBeforeActivity(toActId) //驳回到指定节点
                    .setVariables(taskVariable)
                    .execute();
    
  6. 查看测试结果:
    在这里插入图片描述
    在这里插入图片描述
    执行历史
    在这里插入图片描述

### 实现或处理 Camunda 中的驳回流程 在 Camunda 工作流引擎中,实现或处理驳回流程涉及定义特定路径来管理任务被拒绝的情况。当审批者决定不批准某个任务时,工作流应能够将该任务返回给发起人或其他指定人员进行修正。 #### 定义业务流程模型中的驳回路径 为了支持这一功能,在 BPMN 图纸里可以创建一条从当前活动指向之前的节点或是专门用于处理拒绝对策的新子流程线程。这通常通过引入网关(Gateway)额外的任务来完成逻辑判断与流转控制[^1]。 对于具体的实施方法: - **使用排他网关**:设置条件表达式以区分正常推进还是进入驳回分支; - **增加服务任务/人工任务**:用来执行实际的数据更新操作或者是通知相关人员; - **消息事件监听器**:如果需要外部系统参与,则可以通过发布订阅模式触发相应的动作。 下面是一个简单的例子展示如何利用排他网关来进行基本的驳回机制设计: ```xml <process id="expenseReportProcess"> <!-- ...其他部分省略... --> <exclusiveGateway id="decisionPoint" name="Decision Point"/> <sequenceFlow sourceRef="decisionPoint" targetRef="approvedTask"> <conditionExpression xsi:type="tFormalExpression">${approvalResult == 'approve'}</conditionExpression> </sequenceFlow> <sequenceFlow sourceRef="decisionPoint" targetRef="rejectionHandlingSubProcess"> <conditionExpression xsi:type="tFormalExpression">${approvalResult != 'approve'}</conditionExpression> </sequenceFlow> <subProcess id="rejectionHandlingSubProcess" name="Rejection Handling Sub Process"> <!-- 驳回后的具体处理逻辑 --> <startEvent id="rejStart"/> <serviceTask id="notifyUserOfRejection" name="Notify User of Rejection" camunda:class="com.example.NotifyUserService"/> <endEvent id="rejEnd"/> <sequenceFlow sourceRef="rejStart" targetRef="notifyUserOfRejection"/> <sequenceFlow sourceRef="notifyUserOfRejection" targetRef="rejEnd"/> </subProcess> </process> ``` 此代码片段展示了如何构建一个包含决策点的工作流结构,并针对不同结果导向不同的后续步骤。特别是 `rejectionHandlingSubProcess` 子过程负责处理所有关于驳回事宜的操作,比如发送邮件提醒用户其提交的内容已被否决并可能指明原因等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值