1

通用拒绝

从这章开始,就正式进入activiti的实战开发,使用activiti实现各种审批动作,包括一些中国式流程操作,比如回退,征询等,这些操作activiti的标准功能是没有的,但因为activiti不算复杂,也比较灵活,因此可以通过一些技巧或者变通的方法实现,这章就讨论通用拒绝的实现。为什么叫通用拒绝,因为在activiti里,正常的拒绝都是通过连接线加条件判断实现,你可以定义一个变量如outcome,拒绝的时候给这个变量赋值REJECT,在连接线上设置条件表达式从而实现拒绝操作。如图:

项目经理拒绝到发起人的表达式为${outcome=='REJECT'},在流程里设置好变量就能实现拒绝操作:

taskService.setVariable(taskId, "outcome", "approve");
taskService.complete(taskId);

这种拒绝实现方式优点是简单,标准支持,灵活性强,能够从任意节点拒绝回任意节点,但缺点也是明显的

  • 一般流程每个节点都有可能拒绝,那就意味着每个节点都需要设置判断条件,如果都要拒绝回发起人,那么都要跟发起人节点进行连接,如果节点多的话会大大增加流程图的复杂度,让流程图变成一张“蜘蛛网”。

因此我们需要一个通用拒绝的功能,需求是,在任意节点拒绝后自动回到发起人节点,发起人重新提交后流程重新开始。

那么面临的两个问题是

  • 流程图中没有发起人节点,怎么造出这个发起人节点
  • 流程已经在流转中了,如何重新流转

我们依次解决以上两个问题

发起人节点处理

activiti提供动态修改流程模型的api,但修改流程模型后全局生效,所有的流程都会受影响,因此就算我们能通过代码“造出”发起人几点,也是不可取的。其实仔细想想,我们是需要一个发起人节点,还是需要一个审批人是发起人的节点,显然,我们的需求是后面那个,明白了这个道理后,问题就变得简单了,如何让当前节点的审批人变成发起人,方案可以是这样的:

  • 删除当前节点所有的待办,只保留一个待办
  • 将保留下来的那个待办审批人设置为发起人

通过以上两个步骤我们可以实现拒绝后将待办转移到发起人那里,当然为了在流程里能够获取到发起人,你应该在流程发起的时候将发起人信息存储到变量中。

那么问题又来了,我们这是将当前节点伪造成了发起人节点,但假的毕竟是假的,等发起人一审批,就露馅了,因为流程会继续往下走,那么为了达到以假乱真的地步,我们要继续完成以下两件事

  • 审批接口需要知道当前节点的审批是否是“伪造”的发起人节点
  • 如果审批接口知道了当前节点的审批是发起人发起的,那么就需要将流程重新拨回到第一个节点

第一个需求可以通过设置一个变量进行标识,第二个需求是我们的下一个议题。

拒绝实现代码参考:

public TaskResponse reject(TaskResponse task, String user) {
    //删除所有当前task,保留一个,并且将该task的审批人设为发起人
    //设置reject标志
    Task t = taskService.createTaskQuery()
            .taskId(task.getTaskId())
            .singleResult();
    String instanceId = t.getProcessInstanceId();
    List<Task> tasks = taskService.createTaskQuery()
            .processInstanceId(instanceId)
            .list();
    Task luckyTask = tasks.get(0);
    managementService.executeCommand(new ExecutionVariableDeleteCmd(t.getExecutionId()));
    for (int i = 1; i < tasks.size(); ++i) {
        managementService.executeCommand(new TaskDeleteCmd(tasks.get(i).getId()));
        managementService.executeCommand(new ExecutionVariableDeleteCmd(tasks.get(i).getExecutionId()));
    }
    //将发起人设置为当前审批人
    taskService.setAssignee(luckyTask.getId(), (String) taskService.getVariable(luckyTask.getId(), "submitter"));
    //设置变量标识当前状态是已拒绝状态
    taskService.setVariable(luckyTask.getId(), "status", "reject");
    return this.taskResponse(t, instanceId);
}

审批的代码参考如下:

String status = (String) taskService.getVariable(taskId, "status");
if ("reject".equals(status)) {
    //发起人重新发起
    this.rollbackFirstask(task, user);
} else {
    //正常审批
    taskService.complete(taskId, taskParams);
}

流程重新发起

剩下最后一个问题,就算上面的代码中rollbackFirstask如何实现,该方法将流程拨回到第一个节点重新开始,在讨论实现之前,我们需要了解下命令模式,也是设计模式中的一种,其实并不陌生,我们可以在日常的开发中就用到了,但并不知道原来这个还有一个专门的名字。简单说就像Linux下的shell脚本,调用一个个命令一样,将每个独立的操作封装成一个命令(Command),由命令调用者(Command Executor)进行调用,每个命令只负责自己的业务逻辑,不与其他命令交互,上下文信息(Command Context)由命令调用者提供。activiti就是采用命令模式对流程资源进行操作,比如删除一个任务,会有一个DeleteTaskCmd的命令类。activiti命令声明如下:

public interface Command<T> {

  T execute(CommandContext commandContext);

}

命令调用者执行命令的execute方法,命令可以通过commandContext获取上下文,commandContext里包含了对所有资源的管理类。了解了命令模式后,我们就可以开始执行我们的回滚方案了,具体方案步骤:

    1. 清除现场,清除所有中间过程的变量
    1. 找到开始节点,调用api将流程拨回到开始节点

实现代码如下:

/**
 * 流程回退到第一个节点
 *
 * @param context
 * @param request
 * @param user
 * @return
 */
public TaskResponse rollbackFirstask(String taskId, String user) {
    //移除标记REJECT的status
    taskService.removeVariable(taskId, "status");
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();

    //删除任务
    managementService.executeCommand(new TaskDeleteCmd(request.getTaskId()));
    //删除变量
    managementService.executeCommand(new ExecutionVariableDeleteCmd(task.getExecutionId()));
    //将流程回滚到第一个节点
    managementService.executeCommand(new FlowToFirstCmd(task));
    return this.taskResponse(task.getProcessInstanceId());
}

几个命令的实现如下:

TaskDeleteCmd
import org.activiti.engine.impl.cmd.NeedsActiveTaskCmd;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.*;

import java.util.List;

/**
 * @Copyright: Shanghai Definesys Company.All rights reserved.
 * @Description:
 * @author: jianfeng.zheng
 * @since: 2019/9/24 6:09 PM
 * @history: 1.2019/9/24 created by jianfeng.zheng
 */
public class TaskDeleteCmd extends NeedsActiveTaskCmd<String> {

    public TaskDeleteCmd(String taskId) {
        super(taskId);
    }

    @Override
    public String execute(CommandContext commandContext, TaskEntity currentTask) {
        TaskEntityManagerImpl taskEntityManager = (TaskEntityManagerImpl) commandContext.getTaskEntityManager();
        ExecutionEntity executionEntity = currentTask.getExecution();
        taskEntityManager.deleteTask(currentTask, "reject", false, false);
        return executionEntity.getId();
    }
}
ExecutionVariableDeleteCmd
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.VariableInstanceEntity;
import org.activiti.engine.impl.persistence.entity.VariableInstanceEntityManager;

import java.util.List;

/**
 * @Copyright: Shanghai Definesys Company.All rights reserved.
 * @Description:
 * @author: jianfeng.zheng
 * @since: 2019/9/24 6:10 PM
 * @history: 1.2019/9/24 created by jianfeng.zheng
 */
public class ExecutionVariableDeleteCmd implements Command<String> {
    private String executionId;

    public ExecutionVariableDeleteCmd(String executionId) {
        this.executionId = executionId;
    }

    @Override
    public String execute(CommandContext commandContext) {
        VariableInstanceEntityManager vm = commandContext.getVariableInstanceEntityManager();
        List<VariableInstanceEntity> vs = vm.findVariableInstancesByExecutionId(this.executionId);
        for (VariableInstanceEntity v : vs) {
            vm.delete(v);
        }
        return executionId;
    }
}
FlowToFirstCmd
import com.definesys.mpaas.common.exception.MpaasBusinessException;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.FlowNode;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.task.Task;

import java.util.List;

/**
 * @Copyright: Shanghai Definesys Company.All rights reserved.
 * @Description:
 * @author: jianfeng.zheng
 * @since: 2019/9/25 12:36 AM
 * @history: 1.2019/9/25 created by jianfeng.zheng
 */
public class FlowToFirstCmd implements Command<String> {

    private Task task;

    public FlowToFirstCmd(Task task) {
        this.task = task;
    }

    @Override
    public String execute(CommandContext context) {
        FlowElement startNode = this.getFirstNode(this.task, context);
        ExecutionEntity executionEntity = context.getExecutionEntityManager().findById(task.getExecutionId());
        executionEntity.setCurrentFlowElement(startNode);
        context.getAgenda().planTakeOutgoingSequenceFlowsOperation(executionEntity, true);
        return executionEntity.getId();
    }

    private FlowElement getFirstNode(Task task, CommandContext context) {
        HistoryService historyService = context.getProcessEngineConfiguration().getHistoryService();
        HistoricActivityInstance startNode = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(task.getProcessInstanceId())
                .activityType("startEvent")
                .singleResult();
        if (startNode == null) {
            throw new MpaasBusinessException("未找到开始节点");
        }
        RepositoryService repositoryService = context.getProcessEngineConfiguration().getRepositoryService();
        org.activiti.bpmn.model.Process process = repositoryService.getBpmnModel(task.getProcessDefinitionId()).getMainProcess();
        FlowElement node = process.getFlowElement(startNode.getActivityId());
        return node;
    }
}

总结

其实,稍微改造下FlowToFirstCmd命令,就能将流程路由到任意节点,一开始我们也想靠这个实现任意节点路由的功能,但仔细一想里面的坑非常多,遇到子流程,并行审批等复杂的流程时,会产生很多矛盾点,想想也就放弃了。


DQuery
300 声望94 粉丝

幸福是奋斗出来的