一、引子
在06篇中我以查询历史绩效结果这个业务场景,来帮助大家熟悉和掌握相关API的使用,通过查询历史流程和变量来实现这个目的。回到我们本篇的博客,我们依然从业务场景出发,还是这个绩效流程,如果你的上级对你的自评结果不满意,他想要驳回让你重评;或者你自评后发现有遗漏填写数据需要补充,也需要上级给你驳回重填。
通过以上的业务场景,我们可以发现其实驳回功能在实际业务中是一个很普遍且有用的功能。因此,在本篇中我们将学习如何对任务进行驳回。
二、问题分析
首先抛开技术,我们只是想想将当前任务节点驳回到上个任务节点这个操作都需要什么?答案就在问题上,需要当前所在处的任务节点信息以及要驳回到的目标节点信息。那么,回到Flowable引擎上,它支持将一个任务节点驳回到历史走过的任意一个节点。我们这里的需求很明确,因为是一个绩效流程,所以驳回要驳回到上一个任务节点,即一级一级往回驳,进而我们要知道驳回操作本身很简单,用引擎自带的API就可以实现,主要的工作其实是查和清除:查就是要查当前的节点信息和它的上一个节点信息,而清除则是需要清除节点的老数据,因为驳回之后需要重新提交的。明确了这一点后,我们开始今天的实操。
三、代码编写
1.获取目标任务节点信息
大家想想,在实际业务中,假如我们当前这个绩效流程走到了上级评环节,但在前端展示上通常会把已经走过的节点还是要展示出来,因此前端是有自评节点的任务id的,那么我们设计驳回接口时就可以将要驳回到的前一个任务id由前端传过来,如下:
@PostMapping("/rollback/targetTaskId/{targetTaskId}/userId/{userId}")
public ResponseEntity<Object> rollBackTask(@PathVariable String targetTaskId) {
//1.查询目标任务数据
HistoricTaskInstance targetTask = historyService
.createHistoricTaskInstanceQuery()
.taskId(targetTaskId)
.singleResult();
//2.判空
if (Objects.isNull(targetTask)) {
return new ResponseEntity<>(new BaseResponse<>("驳回目的任务不存在!",null),
HttpStatus.BAD_REQUEST);
}
}
通过historyService去查前一个节点的数据并且做相应的判空处理,这里还需要提一点,在前面的博客中我为了演示API,其实省略了很多判断,大家在实际业务中使用的时候,该做的判断可不能少。接下来,我们继续获取当前任务的节点信息。
2.获取当前任务节点信息
通过获取目标任务的数据,我们可以拿到当前流程的实例id,进而通过流程实例id获取当前执行到的任务节点,如下:
//3.获取流程实例id
String processInstanceId = targetTask.getProcessInstanceId();
//4.获取当前任务节点
Task sourceTask = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.active()
.singleResult();
//5.判空
if (Objects.isNull(sourceTask)) {
return new ResponseEntity<>(new BaseResponse<>("驳回当前任务不存在!",null),
HttpStatus.BAD_REQUEST);
}
//6.不能驳回到自己
if (Objects.equals(targetTask, sourceTask)) {
return new ResponseEntity<>(new BaseResponse<>("不能驳回到当前节点!",null),
HttpStatus.BAD_REQUEST);
}
在业务需求下该有的判断都得有,当前节点都判断了,我们还需要对目标节点做下判断,这里可能有同学会有疑问,为什么通过查询历史节点就可以找到的目标节点,这里还要再回查流程定义呢?这里我们需要科普一下:其实这里的“回退”并不真的是回退到目标节点,因为目标节点实际上已经执行过了,我们要做的是根据流程定义创造出一个新的目标节点,然后回到它那一步继续执行,代码如下:
//7.获取流程定义id
String processDefinitionId = targetTask.getProcessDefinitionId();
//8.使用流程定义id查询流程定义,获取流程定义的所有节点信息
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
Process process = bpmnModel.getProcesses().get(0);
Collection<FlowElement> flowElements = process.getFlowElements();
//9.遍历流程定义的所有节点,找到目标节点的定义信息
FlowElement targetElement = null;
for (FlowElement flowElement : flowElements) {
if (ObjectUtil.equal(flowElement.getId(), targetTask.getTaskDefinitionKey())) {
targetElement = flowElement;
break;
}
}
//10.判空
if (Objects.isNull(targetElement)) {
return new ResponseEntity<>(new BaseResponse<>("驳回目的任务定义不存在!",null),
HttpStatus.BAD_REQUEST);
}
3.回退节点
该做的判断都做了,接下来就该调用相关API进行节点回退了,我把相关逻辑进行了封装,如下:
/**
* 回退节点并清理数据.
*
* @param sourceTask 当前任务
* @param targetTask 目标任务
* @return 驳回后的任务
*/
public Task rollbackTask(Task sourceTask, HistoricTaskInstance targetTask) {
//1.获取流程实例id
String processInstanceId = sourceTask.getProcessInstanceId();
//2.回退节点
runtimeService.createChangeActivityStateBuilder()
//指定流程实例ID
.processInstanceId(processInstanceId)
//指定源节点和目标节点
.moveActivityIdTo(sourceTask.getTaskDefinitionKey(), targetTask.getTaskDefinitionKey())
.changeState();
//3.清理两个节点之间的脏数据
clearDirtyData(processInstanceId, sourceTask, targetTask);
//4.返回新目标节点
return taskService.createTaskQuery()
//指定流程实例ID
.processInstanceId(processInstanceId)
//指定目标节点定义
.taskDefinitionKey(targetTask.getTaskDefinitionKey())
.singleResult();
}
4.处理老节点的脏数据
但是仅仅是处理节点还是不够的,大家想想我们在这个流程里还有什么东西也需要处理一下呢?没错,就是流程变量,对老节点处理同时也要对老节点关联的变量进行处理,代码如下:
/**
* 清除节点脏数据.
*
* @param processInstanceId 流程实例id
* @param sourceTask 当前任务id
* @param targetTask 目标任务id
*/
private void clearDirtyData(String processInstanceId, Task sourceTask,
HistoricTaskInstance targetTask) {
//1.获取历史任务列表
List<HistoricTaskInstance> historyTasks = historyService
.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
//按照任务开始时间降序排序
.orderByHistoricTaskInstanceStartTime().desc()
.list();
//2.获取sourceTaskId和targetTaskId之间的所有任务节点,包括 targetTask 和 sourceTask,回滚之后删除。
List<HistoricTaskInstance> betweenNodes =
getBetweenNodes(historyTasks, sourceTask.getId(), targetTask.getId());
//3.获取节点id列表
List<String> taskIds = betweenNodes.stream()
.map(HistoricTaskInstance::getId)
.toList();
//4.删除流程变量
List<String> variableKeys = new ArrayList<>();
for (String taskId : taskIds) {
//分数key
String scoreKey = "score";
//所属人key
String ownerKey = "ownerId";
variableKeys.add(scoreKey);
variableKeys.add(ownerKey);
//删除历史节点
historyService.deleteHistoricTaskInstance(taskId);
}
//5.删除流程实例中的指定变量
runtimeService.removeVariables(processInstanceId, variableKeys);
}
getBetweenNodes方法的作用是为了确定需要删除的任务节点范围,因为可能在实际业务场景中,我们是跨节点驳回的,那么这些中间跨过的节点的变量也是需要处理的,不然可能对业务有影响,代码如下:
/**
* 获取sourceTaskId和targetTaskId之间的所有任务节点,包括 targetTask 和 sourceTask.
*
* @param sourceTaskId 当前任务id
* @param targetTaskId 目标任务id
* @return 任务节点列表
*/
List<HistoricTaskInstance> getBetweenNodes(List<HistoricTaskInstance> historicTasks,
String sourceTaskId,
String targetTaskId) {
//1.初始化一个列表,用于存储源任务和目标任务之间的所有任务节点
List<HistoricTaskInstance> betweenNodes = new ArrayList<>();
//2.定义一个标志,用于判断是否开始记录任务节点
boolean startRecord = false;
for (HistoricTaskInstance historicTask : historicTasks) {
//3.如果历史任务的ID等于源任务的ID,那么开始记录任务节点
if (ObjectUtil.equal(historicTask.getId(), sourceTaskId)) {
startRecord = true;
}
//4.如果开始记录任务节点,那么将当前历史任务添加到任务节点列表中
if (startRecord) {
betweenNodes.add(historicTask);
}
//5.如果历史任务的ID等于目标任务的ID,那么停止记录任务节点,并结束循环
if (ObjectUtil.equal(historicTask.getId(), targetTaskId)) {
break;
}
}
//6.返回任务节点列表
return betweenNodes;
}
四、测试
完成了驳回接口相关代码的编写,我们可以启动我们的绩效流程进行测试,如下:
目前处于自评节点,我们调用完成任务的接口将流程走到上级评节点,如下:
然后调用我们的驳回任务接口,将流程驳回到自评节点,如下:
响应成功后,我们再看下act_ru_task表里的任务又回到了自评节点,如下:
除此之外,我们再去看看act_hi_varinst表和act_hi_taskinst表,还记得我们代码里有清除节点脏数据的逻辑吗?让我们看看这两张历史表里的数据的变化:
可以看到历史任务表里只剩下自评了,按照正常流程执行到的节点都会记录进历史,但上级评已经被成功地清理掉了。
历史流程变量表里“干干净净”,分数变量也被清除掉了,由此我们的清理脏数据的逻辑也正常执行了。还有一个小细节,就是驳回理由,它是Flowable提供的API添加的,所以自然是有被持久化保存下来的,我们可以在act_hi_comment表中查到,如下:
五、小结
最近工作很忙,更新节奏比较慢,希望各位读者见谅哈!但好饭不怕晚嘛,我宁愿慢点打磨也不想随随便便写些东西。承蒙各位读者的厚爱,Flowable系列文章将会持续更新,你们的点赞、评论和收藏都是我更新的动力!