前言
在前面的三篇文章我们介绍了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、直接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、代码解析
-
获取所有历史用户任务
获取到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();
-
获取所有当前活动的用户任务
同样是和上面一样获取ACT_HI_ACTINST
历史表的数据,查询的条件也基本一致,只不过是使用unfinished
改为去查询当前正在执行的实例。// 2. 获取所有当前活动的用户任务的 activityInstanceIds, List<HistoricActivityInstance> operationList = historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .activityType("userTask") //活动类型 .unfinished() // 执行中 .orderByHistoricActivityInstanceStartTime() .asc() .list();
-
获取要驳回到的节点
这个地方分两种情况,一般我们可能会出现驳回上一个节点和取回到开始节点两种情况。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; }
- 获取开始节点:我们直接通过前面获取的
-
校验节点数据、记录日志
通过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")); }
-
执行回退操作
分为了两部分:
首先我们要把当前正在执行的节点取消掉(如果有在并行执行,是会出现多个的);
然后再记录日志,再通过startBeforeActivity
驳回到指定的节点;以及我们可能再驳回的时候需要写对应的一些驳回信息变量。则通过setVariables
以Map的方式写入。ProcessInstanceModificationBuilder modificationBuilder = runtimeService.createProcessInstanceModification(processInstanceId); // 可能会因为网关出现并行等情况,需要把所有的节点取消掉 for (HistoricActivityInstance operation : operationList){ modificationBuilder.cancelActivityInstance(operation.getId()); //取消当前任务节点集合 } // 驳回到指定节点 modificationBuilder .setAnnotation("进行驳回到上一个任务节点操作") //记录r .startBeforeActivity(toActId) //驳回到指定节点 .setVariables(taskVariable) .execute();
-
查看测试结果:
执行历史