上一篇文章,大概讲解了这个项目关于工作流实现的流程模型定义流程和一个流程发起的流程,接下来就是关于任务的回退和加签功能:
任务回退
关于任务回退,通俗的将就是任务流程到某一个节点,流程中间可能有什么错误,需要修改,但是直接拒绝任务,有需要发起人重新发起一次流程,所有人都需要在重复一遍流程,这样不方便,所以就有一个回退功能,帮助我们将任务回退到我们想要回退的节点,这样就避免了重复性的发起流程。
代码实现
从前端页面开始看
当我们点击回退按钮,首先我们需要获得当前任务可以回退的节点,然后展示到前端,用户在选择回退到哪个具体的节点。
可回退节点,也就是不是所有节点都支持回退,如果是复杂的流程,我们回退到的节点,走下一遍流程,可能走不到结束,比如说并行网关这种,所以我们查询回退节点的时候就需要注意,一定是串行节点才能回退;
查看可回退节点:
/**
* 获取当前任务的可回退的流程集合
*
* @param taskId 当前的任务 ID
* @return 可以回退的节点列表
*/
@Override
public List<BpmTaskSimpleRespVO> getReturnTaskList(String taskId) {
// 1. 校验当前任务 task 存在
Task task = validateTaskExist(taskId);
// 根据流程定义获取流程模型信息
BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
//获取流程元素信息
FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
if (source == null) {
throw exception(TASK_NOT_EXISTS);
}
// 2.1 查询该任务的前置任务节点的 key 集合
List<UserTask> previousUserList = BpmnModelUtils.getPreviousUserTaskList(source, null, null);
if (CollUtil.isEmpty(previousUserList)) {
return Collections.emptyList();
}
// 2.2 过滤:只有串行可到达的节点,才可以回退。类似非串行、子流程无法退回
previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null));
return BpmTaskConvert.INSTANCE.convertList(previousUserList);
}
一步一步看,首先需要校验我们当前传过来的任务是否存在
private Task validateTaskExist(String id) {
Task task = getTask(id);
if (task == null) {
throw exception(TASK_NOT_EXISTS);
}
return task;
}
如果不存在就给前端返回对应的错误提示
2、接下来根据流程定义的编号获取到对应的流程模型实体
/**
* 获得流程定义编号对应的 BPMN Model
*
* @param processDefinitionId 流程定义编号
* @return BPMN Model
*/
@Override
public BpmnModel getBpmnModelByDefinitionId(String processDefinitionId) {
return repositoryService.getBpmnModel(processDefinitionId);
}
3、在通过流程模型实体获取到每一个流程的元素信息
/**
* 获取流程元素信息
*
* @param model bpmnModel 对象
* @param flowElementId 元素 ID
* @return 元素信息
*/
public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) {
Process process = model.getMainProcess();
return process.getFlowElement(flowElementId);
}
4、这下就该查询该任务前置任务的节点了:
/**
* 找到 source 节点之前的所有用户任务节点
*
* @param source 起始节点
* @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复
* @param userTaskList 已找到的用户任务节点
* @return 用户任务节点 数组
*/
public static List<UserTask> getPreviousUserTaskList(FlowElement source, Set<String> hasSequenceFlow, List<UserTask> userTaskList) {
userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList;
hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow;
// 如果该节点为开始节点,且存在上级子节点,则顺着上级子节点继续迭代
if (source instanceof StartEvent && source.getSubProcess() != null) {
userTaskList = getPreviousUserTaskList(source.getSubProcess(), hasSequenceFlow, userTaskList);
}
// 根据类型,获取入口连线
List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source);
if (sequenceFlows == null) {
//如果没有入口连线说明结束了,直接返回结果
return userTaskList;
}
// 循环找到目标元素
for (SequenceFlow sequenceFlow : sequenceFlows) {
// 如果发现连线重复,说明循环了,跳过这个循环
if (hasSequenceFlow.contains(sequenceFlow.getId())) {
continue;
}
// 添加已经走过的连线
hasSequenceFlow.add(sequenceFlow.getId());
// 类型为用户节点,则新增父级节点
if (sequenceFlow.getSourceFlowElement() instanceof UserTask) {
userTaskList.add((UserTask) sequenceFlow.getSourceFlowElement());
}
// 类型为子流程,则添加子流程开始节点出口处相连的节点
if (sequenceFlow.getSourceFlowElement() instanceof SubProcess) {
// 获取子流程用户任务节点
List<UserTask> childUserTaskList = findChildProcessUserTaskList((StartEvent) ((SubProcess) sequenceFlow.getSourceFlowElement()).getFlowElements().toArray()[0], null, null);
// 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续
if (CollUtil.isNotEmpty(childUserTaskList)) {
userTaskList.addAll(childUserTaskList);
}
}
// 继续迭代
userTaskList = getPreviousUserTaskList(sequenceFlow.getSourceFlowElement(), hasSequenceFlow, userTaskList);
}
return userTaskList;
}
这个方法用了递归调用,通过我们传过来的当前任务的节点,往前找,找到所有的任务节点,这个方法有三个参数,第一个是我们需要传过来的当前节点,第二个节点是已经连过线的节点,这个参数的意义是当我们递归的时候,已经走过的流程就不需要在走一遍了,避免重复,最后一个参数也就是我们存放数据的参数,我们将最后要返回的结果都存在这个集合里面;
第一次调用后两个参数都是空,我们就需要判断,是否为空,为空我们就新建一个对应的参数,用来存值。
先判断是否是开始节点并且有上级子节点,我们就顺着上级子节点继续迭代
然后通过当前节点获取到入口连线,也就是当前节点之前的所有节点
然后非空判断后,就遍历我们拿到的节点
在遍历这些节点的时候,就先要跟我们之前存的已经连过线的key比较,看里面是否包含了我们正在遍历的这个节点,如果有的话我们就跳过这一次,如果没有的话就先给我们的hasSequenceFlow这个集合中存入,记录这个节点已经遍历过了,然后在判断节点的类型,如果是用户节点的话:就给我们的结果存进去,如果是子流程的话,我们就需要遍历对应的子流程的所有节点。
查询子流程的所有节点逻辑跟上面的代码差不多,可以看一下
/**
* 迭代获取子流程用户任务节点
*
* @param source 起始节点
* @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复
* @param userTaskList 需要撤回的用户任务列表
* @return 用户任务节点
*/
public static List<UserTask> findChildProcessUserTaskList(FlowElement source, Set<String> hasSequenceFlow, List<UserTask> userTaskList) {
hasSequenceFlow = hasSequenceFlow == null ? new HashSet<>() : hasSequenceFlow;
userTaskList = userTaskList == null ? new ArrayList<>() : userTaskList;
// 根据类型,获取出口连线
List<SequenceFlow> sequenceFlows = getElementOutgoingFlows(source);
if (sequenceFlows == null) {
return userTaskList;
}
// 循环找到目标元素
for (SequenceFlow sequenceFlow : sequenceFlows) {
// 如果发现连线重复,说明循环了,跳过这个循环
if (hasSequenceFlow.contains(sequenceFlow.getId())) {
continue;
}
// 添加已经走过的连线
hasSequenceFlow.add(sequenceFlow.getId());
// 如果为用户任务类型,且任务节点的 Key 正在运行的任务中存在,添加
if (sequenceFlow.getTargetFlowElement() instanceof UserTask) {
userTaskList.add((UserTask) sequenceFlow.getTargetFlowElement());
continue;
}
// 如果节点为子流程节点情况,则从节点中的第一个节点开始获取
if (sequenceFlow.getTargetFlowElement() instanceof SubProcess) {
List<UserTask> childUserTaskList = findChildProcessUserTaskList((FlowElement) (((SubProcess) sequenceFlow.getTargetFlowElement()).getFlowElements().toArray()[0]), hasSequenceFlow, null);
// 如果找到节点,则说明该线路找到节点,不继续向下找,反之继续
if (CollUtil.isNotEmpty(childUserTaskList)) {
userTaskList.addAll(childUserTaskList);
continue;
}
}
// 继续迭代
userTaskList = findChildProcessUserTaskList(sequenceFlow.getTargetFlowElement(), hasSequenceFlow, userTaskList);
}
return userTaskList;
}
需要注意的是,迭代子流程是往后迭代的,不是跟上个方法一样往前迭代的,所以就需要迭代后面的所有节点;
也是传过来一个节点,通过这个节点获取他之后的所有节点,然后非空判断,之后遍历,然后判断每个节点的类型,对每个类型做不同的操作,比如是用户节点就存到结果里,如果还是子流程就递归调用继续遍历,遍历完之后,如果结果里有数据就给结果里面存 ,存完之后,接着递归调用下一个元素。
这样迭代完之后,我们就获取到了当前任务节点之前的所有节点(用户节点),接下来就得判断这些节点那些不是串行节点,需要将这些节点去掉,就是我们需要的数据;
过滤:只有串行可到达的节点,才可以回退。类似非串行、子流程无法退回
previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null));
这里是一句lamda表达式,List的remoceIf()方法是里面的表达式为true,则去掉这个元素,刚好能满足我们的需求,接下来就看我们定义的方法:
/**
* 迭代从后向前扫描,判断目标节点相对于当前节点是否是串行
* 不存在直接回退到子流程中的情况,但存在从子流程出去到父流程情况
*
* @param source 起始节点
* @param target 目标节点
* @param visitedElements 已经经过的连线的 ID,用于判断线路是否重复
* @return 结果
*/
public static boolean isSequentialReachable(FlowElement source, FlowElement target, Set<String> visitedElements) {
visitedElements = visitedElements == null ? new HashSet<>() : visitedElements;
// 不能是开始事件和子流程
if (source instanceof StartEvent && isInEventSubprocess(source)) {
return false;
}
// 根据类型,获取入口连线
List<SequenceFlow> sequenceFlows = getElementIncomingFlows(source);
if (CollUtil.isEmpty(sequenceFlows)) {
return true;
}
// 循环找到目标元素
for (SequenceFlow sequenceFlow : sequenceFlows) {
// 如果发现连线重复,说明循环了,跳过这个循环
if (visitedElements.contains(sequenceFlow.getId())) {
continue;
}
// 添加已经走过的连线
visitedElements.add(sequenceFlow.getId());
// 这条线路存在目标节点,这条线路完成,进入下个线路
FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement();
if (target.getId().equals(sourceFlowElement.getId())) {
continue;
}
// 如果目标节点为并行网关,则不继续
if (sourceFlowElement instanceof ParallelGateway) {
return false;
}
// 否则就继续迭代
if (!isSequentialReachable(sourceFlowElement, target, visitedElements)) {
return false;
}
}
return true;
}
方法跟上面两个差不多,也是递归,将上面查出来的任务集合进行遍历,去除不符合要求的节点,最终就是我们需要的节点,最后做一个类型转换就可以返回给前端了;
回退逻辑
接下来就是回退逻辑了,当我们选择好对应的目标节点后,我们进行回退;
@Override
@Transactional(rollbackFor = Exception.class)
public void returnTask(Long userId, BpmTaskReturnReqVO reqVO) {
// 1.1 当前任务 task
Task task = validateTask(userId, reqVO.getId());
if (task.isSuspended()) {
throw exception(TASK_IS_PENDING);
}
// 1.2 校验源头和目标节点的关系,并返回目标元素
FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(), reqVO.getTargetDefinitionKey(), task.getProcessDefinitionId());
// 2. 调用 flowable 框架的回退逻辑
returnTask0(task, targetElement, reqVO);
// 3. 更新任务扩展表
taskExtMapper.updateByTaskId(new BpmTaskExtDO().setTaskId(task.getId())
.setResult(BpmProcessInstanceResultEnum.BACK.getResult())
.setEndTime(LocalDateTime.now()).setReason(reqVO.getReason()));
}
第一步依旧是校验任务是否存在,并且看任务是否是挂起状态,如果是挂起状态则不能操作;
然后校验一下源头和目标节点的关系,校验成功后返回
/**
* 回退流程节点时,校验目标任务节点是否可回退
*
* @param sourceKey 当前任务节点 Key
* @param targetKey 目标任务节点 key
* @param processDefinitionId 当前流程定义 ID
* @return 目标任务节点元素
*/
private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) {
// 1.1 获取流程模型信息
BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(processDefinitionId);
// 1.3 获取当前任务节点元素
FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey);
// 1.3 获取跳转的节点元素
FlowElement target = BpmnModelUtils.getFlowElementById(bpmnModel, targetKey);
if (target == null) {
throw exception(TASK_TARGET_NODE_NOT_EXISTS);
}
// 2.2 只有串行可到达的节点,才可以回退。类似非串行、子流程无法退回
if (!BpmnModelUtils.isSequentialReachable(source, target, null)) {
throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
}
return target;
}
最后调用flowable的api实现回退逻辑
/**
* 执行回退逻辑
*
* @param currentTask 当前回退的任务
* @param targetElement 需要回退到的目标任务
* @param reqVO 前端参数封装
*/
public void returnTask0(Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
// 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤
// 1.1 获取所有正常进行的任务节点 Key
List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
List<String> runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey);
// 1.2 通过 targetElement 的出口连线,计算在 runTaskKeyList 有哪些 key 需要被撤回
// 为什么不直接使用 runTaskKeyList 呢?因为可能存在多个审批分支,例如说:A -> B -> C 和 D -> F,而只要 C 撤回到 A,需要排除掉 F
List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
// 2. 给当前要被回退的 task 数组,设置回退意见
taskList.forEach(task -> {
// 需要排除掉,不需要设置回退意见的任务
if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
return;
}
taskService.addComment(task.getId(), currentTask.getProcessInstanceId(),
BpmCommentTypeEnum.BACK.getType().toString(), reqVO.getReason());
});
// 3. 执行驳回
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId())
.moveActivityIdsToSingleActivityId(returnTaskKeyList, // 当前要跳转的节点列表( 1 或多)
reqVO.getTargetDefinitionKey()) // targetKey 跳转到的节点(1)
.changeState();
}
最后更新一下我们扩展的数据库的数据,回退逻辑就做好了