流程与业务代码必然有一定的耦合性,这里建立的约定,旨在尽可能达到以下目的:
- 在不增加任务的情况下改变流程流向,不需要更改代码;
- 业务代码能够复用;
- 展示业务流程历史时间轴时,可展示每一步的执行结果,如批准/拒绝等;
- 审批流程一般是最广泛的流程应用之一,故在约定中关于审批流程做了一些特殊约定;
主要思路
- Activiti负责流程控制,业务服务管理业务数据,两者通过BusinessKey关联
- 业务数据影响流程走向的(如请假天数超过3天,需经理批准;请款金额超过5万,需财务总监批准等),由业务服务通过流程变量控制;
- 每步流程任务节点的结果需要在时间轴中展示的,由任务局部变量记录;
- 流程任务的特定需求(如可完成任务的人选等),通过任务监听器交由业务处理;
- 业务需要根据流程节点和结果更新业务状态的(如性能优化不去遍历流程状态),通过添加执行监听器出发业务服务进行处理。
约定如下:
- 流程定义需定义变量starterID,存储流程启动者ID
此变量有以下几个作用- 在业务代码可随时获得流程的发起者
- 某些任务,如提交申请任务,执行者就是发起者,则该任务的代理人直接设置为${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为系统定义值
流程可通过此结果判断走向,同时可用于流程历史时间轴中的对应结果展示
id | category_id | category | value |
---|---|---|---|
20000 | 20000 | 流程任务结果 | 完成 |
20001 | 20000 | 流程任务结果 | 批准 |
20002 | 20000 | 流程任务结果 | 拒绝 |
20003 | 20000 | 流程任务结果 | 取消 |
- 对于一般审批流程,在流程结束节点,增加start执行监听器,判断全局流程变量procResult的值,更新对应的业务状态“批准”,“拒绝”或“取消”
同样,执行监听器也需要基于spring boot自动加载注解方式进行配置
@Service("vacationProcessEndListener")
public class VacationProcessEndListener implements ExecutionListener {
...
}
- 某些情况下,为考虑业务获取数据的性能,可能需要更新业务的当前状态。此种情况,也可采用添加监听器的方式进行处理。
- 考虑流程编辑的简单性,对于不同结果的输出流向,无需都添加网关节点
以请假流程为例:
- 流程中存在多个一般审批任务节点,任务节点的定义Key设置为Approve_XXX。意义在于判断用户是否可操作该任务时,可调用Like方法getTaskChkDefKeyLike。
- 若流程改为主管审批拒绝后回到请假申请,可通过在请假申请节点增加监听器,在监听器中更新业务状态为已拒绝(还在考虑优化方案中···)
- 封装通用流程时间轴获取方法:
- 考虑到性能问题(将代理人ID转换为姓名),采用直接从Activiti历史相关表中进行查询。
- 查询出任务列表后,查看最后一个任务是否已完成(END_TIME_不为空),若为空代表任务尚未执行,此时查询对应任务的候选人列表
- 同时考虑用户权限问题,在时间轴数据查询完成后,检查当前用户是否参与了此流程的任务
此处没有采用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数据表数据的查询,适应情况不一定完全,且代码较多,暂就不贴出了。若是有网友确实需要,我再贴吧。
以上是一些不成熟想法,特此记录。希望对大家能有所帮助,同时也希望多多指正,谢谢。
(草稿,持续更新中······)