Activiti 6研究02 - 使用Activiti与业务协同工作的一些约定

流程与业务代码必然有一定的耦合性,这里建立的约定,旨在尽可能达到以下目的:

  • 在不增加任务的情况下改变流程流向,不需要更改代码;
  • 业务代码能够复用;
  • 展示业务流程历史时间轴时,可展示每一步的执行结果,如批准/拒绝等;
  • 审批流程一般是最广泛的流程应用之一,故在约定中关于审批流程做了一些特殊约定;

主要思路

  • Activiti负责流程控制,业务服务管理业务数据,两者通过BusinessKey关联
  • 业务数据影响流程走向的(如请假天数超过3天,需经理批准;请款金额超过5万,需财务总监批准等),由业务服务通过流程变量控制;
  • 每步流程任务节点的结果需要在时间轴中展示的,由任务局部变量记录;
  • 流程任务的特定需求(如可完成任务的人选等),通过任务监听器交由业务处理;
  • 业务需要根据流程节点和结果更新业务状态的(如性能优化不去遍历流程状态),通过添加执行监听器出发业务服务进行处理。

约定如下:

  • 流程定义需定义变量starterID,存储流程启动者ID
    此变量有以下几个作用
    1. 在业务代码可随时获得流程的发起者
    2. 某些任务,如提交申请任务,执行者就是发起者,则该任务的代理人直接设置为${starterID}即可。在流程返回此任务时,无需通过监听器设置代理人
  • 流程配置中,对于任务执行者,约定如下:
    • 一般情况下不直接指定任务代理人(Assignee)为某个账户ID
    • 一般情况下,不指定一个或多个候选人(CandidateUser)
    • 配置一个或多个候选组(CandidateGroup),其中设置为可执行该任务的角色ID
  • 任务动态分配执行人、候选人,在任务中添加create任务监听器,在监听器里根据候选组里设置的角色进行动态人员分配
    此处,基于spring boot自动加载实例注解的方式,需要在任务监听器中采用委托表达式的方式指定实例
    在这里插入图片描述
    对应的监听器类声明如下:
@Service("commonTaskCreatedListener")
public class CommonTaskCreatedListener implements TaskListener {
	...
}
  • 任务执行时,先认领任务
            // 认领该任务
            taskService.claim(taskID, String.valueOf(userID));
  • 任务的执行结果,需要在流程时间轴中展示结果的情况,存入任务局部结果变量result。对应的任务服务,在调用compete接口前,将任务结果保存到result中。
			// 添加或设置任务结果变量
			Map<String, Object> args = new HashMap<>();
			args.put("result", 20001);
			// 存入任务局部变量,用于任务结果历史记录
			taskService.setVariablesLocal(task.getId(), args);
  • 任务的结果,不必在流程时间轴中表示结果的情况,可以不设置任务的结果值。
  • 任务的执行结果,会影响流程走向或表示业务状态时,存入流程全局变量procResult。对应的任务服务,在调用compete接口前,将任务结果保存到procResult中。
			// 添加或设置任务结果变量
			Map<String, Object> args = new HashMap<>();
			args.put("procResult", 20001);
			
			// 存入全局变量,用于流程流向控制
			taskService.setVariables(task.getId(), args);
  • 基于Activiti的变量管理机制,流程全局变量与任务局部变量尽量不要同名。确实需要同名的情况,必须先保存全局变量,再保存任务局部变量,否则会因为存在同名局部变量而导致全局变量不会更新。
			// 添加或设置任务结果变量
			Map<String, Object> args = new HashMap<>();
			args.put("result", 20001);
			
			// 存入全局变量,用于流程流向控制
			taskService.setVariables(task.getId(), args);
			// 存入任务局部变量,用于任务结果历史记录
			taskService.setVariablesLocal(task.getId(), args);
			
			taskService.complete(task.getId());
  • 任务结果result变量值为整型数,其描述保存于字典表中。数值用于对结果的判断,描述用于展示。
    数值从20000开始,20000 ~ 20010为系统定义值
    流程可通过此结果判断走向,同时可用于流程历史时间轴中的对应结果展示
idcategory_idcategoryvalue
2000020000流程任务结果完成
2000120000流程任务结果批准
2000220000流程任务结果拒绝
2000320000流程任务结果取消
  • 对于一般审批流程,在流程结束节点,增加start执行监听器,判断全局流程变量procResult的值,更新对应的业务状态“批准”,“拒绝”或“取消”
    同样,执行监听器也需要基于spring boot自动加载注解方式进行配置
    在这里插入图片描述
@Service("vacationProcessEndListener")
public class VacationProcessEndListener implements ExecutionListener {
	...
}
  • 某些情况下,为考虑业务获取数据的性能,可能需要更新业务的当前状态。此种情况,也可采用添加监听器的方式进行处理。
  • 考虑流程编辑的简单性,对于不同结果的输出流向,无需都添加网关节点
    以请假流程为例:
    在这里插入图片描述
  • 流程中存在多个一般审批任务节点,任务节点的定义Key设置为Approve_XXX。意义在于判断用户是否可操作该任务时,可调用Like方法getTaskChkDefKeyLike。
  • 若流程改为主管审批拒绝后回到请假申请,可通过在请假申请节点增加监听器,在监听器中更新业务状态为已拒绝(还在考虑优化方案中···)
  • 封装通用流程时间轴获取方法:
    1. 考虑到性能问题(将代理人ID转换为姓名),采用直接从Activiti历史相关表中进行查询。
    2. 查询出任务列表后,查看最后一个任务是否已完成(END_TIME_不为空),若为空代表任务尚未执行,此时查询对应任务的候选人列表
    3. 同时考虑用户权限问题,在时间轴数据查询完成后,检查当前用户是否参与了此流程的任务
      此处没有采用activiti提供的createProcessInstanceQuery().involvedUser()接口,是因为此接口的原则是,用户只要流程中任何一个任务的候选者,均视为参与者。而我考虑,对于已完成的任务,只将执行者视为参与者,其他候选人则不再视为参与者。
  • 在流程时间轴查询过程中,发现任务批注表act_hi_comment中,TASK_ID_与PROC_INST_ID_列均没有建立索引,查询任务批注时效率会非常低。不清楚是不是我没搞清楚Activiti设计批注表时的理念。
    解决方案是:在项目初始化Activiti表之后,需手动为TASK_ID_列增加索引

基于以上约定,封装了一个简单的流程操作类ProcessUtil,实现了一些简单通用的方法

package com.dippersoft.metroplex.webadmin.ProcessUtil;

import com.dippersoft.metroplex.webadmin.Person.Vacation.VacationService;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.task.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

public class ProcessUtil {
    private final static Logger logger = LoggerFactory.getLogger(VacationService.class);

    public static final int COMPLETE = 20000;
    public static final int APPROVE = 20001;
    public static final int REJECT = 20002;

    /**
     * 判断指定Key的流程是否存在且可使用
     * @param key
     * @return
     */
    public static boolean isProcessReady(String key) {
        ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
        ProcessDefinition processDefinition = engine.getRepositoryService().createProcessDefinitionQuery()
                .processDefinitionKey(key)
                .latestVersion()
                .singleResult();
        if (processDefinition == null) {
            logger.error("Can NOT find the process definition of '{}'.", key);
            return false;
        }
        if (processDefinition.isSuspended()) {
            logger.error("The process '{}' is suspended.", key);
            return false;
        }

        return true;
    }

    /**
     * 获得指定流程实例ID的当前任务。
     * 会验证流程定义Key,任务定义Key,及用户是否可操作该任务(代理人或候选人)
     *
     * @param userID
     * @param procInstID
     * @param procDefKey
     * @param taskDefKey
     * @return
     */
    public static Task getTaskChkDefKey(long userID, String procInstID, String procDefKey, String taskDefKey) {
        ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
        TaskService taskService = engine.getTaskService();
        Task task = taskService.createTaskQuery()
                .processInstanceId(procInstID)
                .processDefinitionKey(procDefKey)
                .taskDefinitionKey(taskDefKey)
                .taskCandidateOrAssigned(String.valueOf(userID))
                .singleResult();

        if (task == null) {
            logger.error("Can NOT access the task. [ProcInstID: {}, UserID: {}]", procInstID, userID);
            return null;
        }

        return task;
    }

    /**
     * 获得指定流程实例ID的当前任务。
     * 会验证流程定义Key,任务定义Key(Like方式),及用户是否可操作该任务(代理人或候选人)
     *
     * @param userID
     * @param procInstID
     * @param procDefKey
     * @param taskDefKey
     * @return
     */
    public static Task getTaskChkDefKeyLike(long userID, String procInstID, String procDefKey, String taskDefKey) {
        ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
        TaskService taskService = engine.getTaskService();
        Task task = taskService.createTaskQuery()
                .processInstanceId(procInstID)
                .processDefinitionKey(procDefKey)
                .taskDefinitionKeyLike(taskDefKey)
                .taskCandidateOrAssigned(String.valueOf(userID))
                .singleResult();

        if (task == null) {
            logger.error("Can NOT access the task. [ProcInstID: {}, UserID: {}]", procInstID, userID);
            return null;
        }

        return task;
    }

    /**
     * 执行通用审批任务
     * 适用于一般审批流程中的审批任务节点,即最后一个审批任务的结果记为整个审批流程的结果。
     * 审批流程结果保存在变量approved中;每个审批步骤的结果保存在通用任务结果变量result中。
     *
     * @param userID
     * @param taskID
     * @param isApprove
     * @param comment
     * @return
     */
    public static boolean commonApprove(long userID, String taskID, boolean isApprove, String comment) {
        ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
        TaskService taskService = engine.getTaskService();

        // 添加审批结果变量
        Map<String, Object> args = new HashMap<>();
        try {
            // 认领该任务
            taskService.claim(taskID, String.valueOf(userID));

            // 保存流程审批结果,可用于流程流向控制及表示整体流程审批结果。
            args.put("approved", isApprove);
            taskService.setVariables(taskID, args);

            // 存入任务局部变量,用于任务结果历史记录
            args.clear();
            if (isApprove)
                args.put("result", APPROVE);
            else
                args.put("result", REJECT);
            taskService.setVariablesLocal(taskID, args);

            if (comment != null && !comment.isBlank()) {
                taskService.addComment(taskID, null, comment);
            }

            taskService.complete(taskID);
        } catch (Exception ex) {
            logger.error("Approve task has exception.\n", ex);
            return false;
        }

        return true;
    }

    public static boolean commonApprove(long userID, String taskID, boolean isApprove) {
        return commonApprove(userID, taskID, isApprove, null);
    }

    /**
     * 执行通用任务
     *
     * @param userID
     * @param taskID
     * @param taskResult
     * @param comment
     * @return
     */
    public static boolean commonComplete(long userID, String taskID, Integer taskResult, String comment) {
        ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
        TaskService taskService = engine.getTaskService();

        // 添加审批结果变量
        Map<String, Object> args = new HashMap<>();
        try {
            // 认领该任务
            taskService.claim(taskID, String.valueOf(userID));

            // 存入任务局部变量,用于任务结果历史记录
            if (taskResult != null) {
                args.put("result", taskResult);
                taskService.setVariablesLocal(taskID, args);
            }

            if (comment != null && !comment.isBlank()) {
                taskService.addComment(taskID, null, comment);
            }

            taskService.complete(taskID);
        } catch (Exception ex) {
            logger.error("Complete task has exception.\n", ex);
            return false;
        }

        return true;
    }

    public static boolean commonComplete(long userID, String taskID, Integer taskResult) {
        return commonComplete(userID, taskID, taskResult, null);
    }

    public static boolean commonComplete(long userID, String taskID, String comment) {
        return commonComplete(userID, taskID, null, comment);
    }

    public static boolean commonComplete(long userID, String taskID) {
        return commonComplete(userID, taskID, null, null);
    }
}

封装获取时间轴的代码,由于还牵涉到对Activiti数据表数据的查询,适应情况不一定完全,且代码较多,暂就不贴出了。若是有网友确实需要,我再贴吧。

以上是一些不成熟想法,特此记录。希望对大家能有所帮助,同时也希望多多指正,谢谢。

(草稿,持续更新中······)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值