Activit7任务回退

引言

最近公司需要搭建一个类似钉钉、飞书的流程系统,小公司自行实现流程框架比较吃力,于是选择了Activiti7。那么问题来了,一些定制化的业务,比如:节点回退、加签、减签等Activiti没有默认的实现,这就需要自己来实现。刚开始接触Activiti7也是各种头大,经过一个星期啃源码,终于入了门。下面是节点回退的实现思路和一些代码做个分享

演示地址

前端流程编辑器

思路

  1. 根据BpmnModel查询上一个任务节点,如果是单节点任务,完成当前任务,任务流转到上一个节点
  2. 如果上一个节点是多节点任务(排他网关或者是多个支线流入)但是不是并行任务(并行网关、包含网关),查询历史任务,找出上历史任务中上一个完成的任务,完成当前任务,任务流转到上一个任务
  3. 如果上一个节点是并行网关、包含网关,这类情况直接禁止回退就行。钉钉、飞书不会涉及到这类的情况

实现

/**
 * 回退到上一个任务节点命令 <br>
 * <b>上个节点是并行网关时, 不能回退</b>
 * 
 * @author zhangjin
 *
 */
@Slf4j
public class BackOneStepCmd implements Command<Void>, Serializable {
    private static final long serialVersionUID = 1L;

    // 当前任务
    private Task task;
    private String backReason;

    public BackOneStepCmd(Task task) {
        this.task = Objects.requireNonNull(task, "Task can not be null");
    }

    public BackOneStepCmd(Task task, String deleteReason) {
        this(task);
        this.backReason = deleteReason;
    }

    @Override
    public Void execute(CommandContext commandContext) {
        // 查询BpmnModel
        String processDefinitionId = task.getProcessDefinitionId();
        BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(processDefinitionId);

        // 当前节点
        String activityId = task.getTaskDefinitionKey();
        UserTask userTask = (UserTask) bpmnModel.getFlowElement(activityId);

        // 检测是否可以回退
        // 当前节点流入是并行, 包含网关时禁止回退
        if (ActivitiUtil.checkCurrentFlowNodeIsParallelIncomingBaseProcessDefinition(userTask)) {
            throw new RuntimeException("Current node is parallel gateway, can not execute back cmd");
        }

        // 查询上一个UserTask
        UserTask lastUserTask = ActivitiUtil.getLastUserTask((TaskEntity) task, userTask);
        log.info("back one step to {}", lastUserTask.getId());

        // 建立回退方向
        SequenceFlow backStepSequenceFlow = new SequenceFlow();
        backStepSequenceFlow.setId(FlowNodeIDFactory.randomSequenceFlowId());
        backStepSequenceFlow.setSourceFlowElement(userTask);
        backStepSequenceFlow.setTargetFlowElement(lastUserTask);

        // 并发问题
        // 记录当前节点的原活动方向
        // List<SequenceFlow> originalOutgoingFlows =
        // ListUtil.toList(currentUserTask.getOutgoingFlows());
        // 设置新的流程走向
        // currentUserTask.setOutgoingFlows(ListUtil.toList(backStepSequenceFlow));
        // 完成任务
        // taskService.complete(task.getId());
        // 还原源活动方向
        // currentUserTask.setOutgoingFlows(originalOutgoingFlows);

        // 解决并发流向出错问题
        // 查询任务, 删除的任务不需要触发任何事件
        TaskEntityManager taskEntityManager = commandContext.getTaskEntityManager();
        TaskEntity taskEntity = taskEntityManager.findById(task.getId());

        // 挂起的任务需要先激活
        if (taskEntity.getDelegationState() != null
                && taskEntity.getDelegationState().equals(DelegationState.PENDING)) {
            throw new ActivitiException("A delegated task cannot be completed, but should be resolved instead.");
        }

        // 完成当前任务
        taskEntityManager.deleteTask(taskEntity, backReason, false, false);
        ExecutionEntityManager executionEntityManager = commandContext.getExecutionEntityManager();
        ExecutionEntity executionEntity = executionEntityManager.findById(taskEntity.getExecutionId());

        // 检测当前任务节点类型
        MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = userTask.getLoopCharacteristics();
        if (multiInstanceLoopCharacteristics != null) {
            if (multiInstanceLoopCharacteristics.isSequential()) {
                // 序签提前结束
                leaveSequentialMultiInstance(executionEntity, backStepSequenceFlow);
            } else {
                // 会签或签提前结束
                leaveParallelMultiInstance(executionEntity, backStepSequenceFlow);
            }
        } else {
            // 一般任务节点
            leaveUserTask(executionEntity, backStepSequenceFlow);
        }
        return null;
    }

    /**
     * 当前节点流转
     * 
     * @param executionEntity
     * @param backStepSequenceFlow
     */
    protected void leaveUserTask(ExecutionEntity executionEntity, SequenceFlow backStepSequenceFlow) {
        executionEntity.setCurrentFlowElement(backStepSequenceFlow);
        Context.getAgenda().planTakeOutgoingSequenceFlowsOperation(executionEntity, true);
    }

    /**
     * 平行节点流转
     * 
     * @param childExecution
     * @param backStepSequenceFlow
     */
    protected void leaveParallelMultiInstance(ExecutionEntity childExecution, SequenceFlow backStepSequenceFlow) {
        Context.getCommandContext().getHistoryManager().recordActivityEnd(childExecution, null);
        ExecutionEntity multiInstanceRootExecution = getMultiInstanceRootExecution(childExecution);

        // 清除变量和子执行线
        childExecution.removeVariableLocal(EActiviti.LOOP_COUNTER);
        multiInstanceRootExecution.removeVariableLocal(EActiviti.NUMBER_OF_COMPLETED_INSTANCES);
        multiInstanceRootExecution.removeVariableLocal(EActiviti.NUMBER_OF_ACTIVE_INSTANCES);
        multiInstanceRootExecution.removeVariableLocal(EActiviti.NUMBER_OF_INSTANCES);
        this.deleteChildExecutions((ExecutionEntity) multiInstanceRootExecution, false, Context.getCommandContext());

        // 流转到新的节点
        multiInstanceRootExecution.setScope(false);
        multiInstanceRootExecution.setMultiInstanceRoot(false);
        multiInstanceRootExecution.setActive(true);
        this.leaveUserTask(multiInstanceRootExecution, backStepSequenceFlow);
    }

    /**
     * 序签节点流转
     * 
     * @param childExecution
     * @param backStepSequenceFlow
     */
    protected void leaveSequentialMultiInstance(ExecutionEntity childExecution, SequenceFlow backStepSequenceFlow) {
        Context.getCommandContext().getHistoryManager().recordActivityEnd(childExecution, null);
        ExecutionEntity multiInstanceRootExecution = getMultiInstanceRootExecution(childExecution);

        // 清除变量和子执行线
        childExecution.removeVariableLocal(EActiviti.LOOP_COUNTER);
        multiInstanceRootExecution.removeVariableLocal(EActiviti.NUMBER_OF_COMPLETED_INSTANCES);
        multiInstanceRootExecution.removeVariableLocal(EActiviti.NUMBER_OF_ACTIVE_INSTANCES);
        multiInstanceRootExecution.removeVariableLocal(EActiviti.NUMBER_OF_INSTANCES);
        Context.getCommandContext().getExecutionEntityManager()
                .deleteChildExecutions((ExecutionEntity) multiInstanceRootExecution, "MI_END", false);

        // 流转到新的节点
        multiInstanceRootExecution.setScope(false);
        multiInstanceRootExecution.setMultiInstanceRoot(false);
        multiInstanceRootExecution.setActive(true);
        this.leaveUserTask(multiInstanceRootExecution, backStepSequenceFlow);
    }

    // TODO: can the ExecutionManager.deleteChildExecution not be used?
    protected void deleteChildExecutions(ExecutionEntity parentExecution, boolean deleteExecution,
            CommandContext commandContext) {
        // Delete all child executions
        ExecutionEntityManager executionEntityManager = commandContext.getExecutionEntityManager();
        Collection<ExecutionEntity> childExecutions = executionEntityManager
                .findChildExecutionsByParentExecutionId(parentExecution.getId());
        if (CollectionUtil.isNotEmpty(childExecutions)) {
            for (ExecutionEntity childExecution : childExecutions) {
                deleteChildExecutions(childExecution, true, commandContext);
            }
        }

        if (deleteExecution) {
            executionEntityManager.deleteExecutionAndRelatedData(parentExecution, null, false);
        }
    }

    protected ExecutionEntity getMultiInstanceRootExecution(ExecutionEntity executionEntity) {
        ExecutionEntity multiInstanceRootExecution = null;
        ExecutionEntity currentExecution = executionEntity;
        while (currentExecution != null && multiInstanceRootExecution == null && currentExecution.getParent() != null) {
            if (currentExecution.isMultiInstanceRoot()) {
                multiInstanceRootExecution = currentExecution;
            } else {
                currentExecution = currentExecution.getParent();
            }
        }
        return multiInstanceRootExecution;
    }

}
其他相关的类
public class ActivitiUtil {

    /**
     * 基于流程定义查询任务上一个任务节点
     * 
     * @param userTask
     * @return
     */
    public static List<UserTask> getLastUserTaskBaseProcessDefinition(FlowNode userTask) {
        if (Objects.isNull(userTask)) {
            return null;
        }

        List<SequenceFlow> sequenceFlows = userTask.getIncomingFlows();
        List<UserTask> lastUserTasks = new ArrayList<UserTask>();
        if (CollectionUtil.isNotEmpty(sequenceFlows)) {
            for (SequenceFlow sequenceFlow : sequenceFlows) {
                FlowNode flowNode = (FlowNode) sequenceFlow.getSourceFlowElement();
                if (flowNode instanceof UserTask) {
                    lastUserTasks.add((UserTask) flowNode);
                } else {
                    lastUserTasks.addAll(getLastUserTaskBaseProcessDefinition(flowNode));
                }
            }
        }

        return lastUserTasks;
    }

    /**
     * 基于流程定义和历史任务获取当前节点的上一个任务节点
     * 
     * @param taskEntity
     * @param userTask
     * @return
     */
    public static UserTask getLastUserTask(TaskEntity taskEntity, FlowNode userTask) {
        UserTask lastUserTask = null;
        List<UserTask> lastUserTasks = getLastUserTaskBaseProcessDefinition(userTask);
        BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(taskEntity.getProcessDefinitionId());
        if (CollectionUtil.isNotEmpty(lastUserTasks)) {
            if (lastUserTasks.size() == 1) {
                lastUserTask = lastUserTasks.get(0);
            } else {// 多分支流入情况处理
                List<String> lastUserTaskDefinitionKeys = lastUserTasks.stream().map(UserTask::getId)
                        .collect(Collectors.toList());

                // 查询上一个完成任务的节点
                String executionId = taskEntity.getExecutionId();
                List<HistoricTaskInstance> historicTaskInstances = SpringContext.getBean(HistoryService.class)//
                        .createHistoricTaskInstanceQuery()//
                        .executionId(executionId)//
                        .finished()//
                        .orderByTaskCreateTime().desc()//
                        .list();

                if (CollectionUtil.isNotEmpty(historicTaskInstances)) {
                    HistoricTaskInstance historicTaskInstance = historicTaskInstances.stream()
                            .filter(item -> lastUserTaskDefinitionKeys.contains(item.getTaskDefinitionKey()))
                            .findFirst().orElse(null);
                    if (Objects.nonNull(historicTaskInstance)) {
                        String lastActivityId = historicTaskInstance.getTaskDefinitionKey();
                        lastUserTask = (UserTask) bpmnModel.getFlowElement(lastActivityId);
                    }
                }
            }
        }

        if (Objects.isNull(lastUserTask)) {
            throw new FlowableException("Last task is null");
        }
        return lastUserTask;
    }

    /**
     * 基于流程定义检查当前节点流入是否是并行网关, 包含网关
     * 
     * @param userTask
     * @return
     */
    public static boolean checkCurrentFlowNodeIsParallelIncomingBaseProcessDefinition(FlowNode userTask) {
        if (Objects.nonNull(userTask)) {
            List<SequenceFlow> sequenceFlows = userTask.getIncomingFlows();
            if (CollectionUtil.isNotEmpty(sequenceFlows)) {
                for (SequenceFlow sequenceFlow : sequenceFlows) {
                    FlowNode flowNode = (FlowNode) sequenceFlow.getSourceFlowElement();
                    if (flowNode instanceof InclusiveGateway || flowNode instanceof ParallelGateway) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

}
/**
 * activiti的内置变量和属性名称
 *
 */
public interface EActiviti {

    // 内置变量
    // 多实例执行变量_ACTIVIT_NULL_ASSIGNEE
    String NUMBER_OF_INSTANCES = "nrOfInstances";
    String NUMBER_OF_ACTIVE_INSTANCES = "nrOfActiveInstances";
    String NUMBER_OF_COMPLETED_INSTANCES = "nrOfCompletedInstances";
    // 多实例分支执行变量
    String LOOP_COUNTER = "loopCounter";

    // 内置属性
    String BUSINESS_KEY = "businessKey";
    String VARIABLES = "variables";
    String INITIATOR = "initiator";

    // 候选用户组前缀
    String CANDIDATE_GROUPS_PREFIX = "_GROUPS:";
    // 候选用户前缀
    String CANDIDATE_USERS_PREFIX = "_USERS:";
    // 节点空执行人
    String ACTIVITI_NULL_ASSIGNEE = "_ACTIVIT_NULL_ASSIGNEE";
    // 自动通过机器人
    String ACTIVITI_AUTO_PASS_ROBOT = "_ACTIVITI_AUTO_PASS_ROBOT";
    // 自动拒绝机器人
    String ACTIVITI_AUTO_REFUSE_ROBOT = "_ACTIVITI_AUTO_REFUSE_ROBOT";
    // 节点自动skip表达式
    String ACTIVITI_SKIP_EXPRESSION = "_ACTIVITI_SKIP_EXPRESSION_ENABLED";
}
/**
 * 流程定义节点id生成器
 */
public class FlowNodeIDFactory {
        // 基于雪花算法的id
    public static String randomSequenceFlowId() {
        return IDFactory.snowflakeId();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容