会签(会审),指的是在流程中某个业务需要经过多人表决,并且根据表决意见的汇总结果以及设定的规则,来决定流程的走向,它是审批流程中常见的需求。
会签可以分为两种:
- 单步 - 只使用一个活动来处理。
- 多步 - 由多个活动所组成的。
单步会签比较常见,也容易实现,主要的解决方案是在会签活动的主任务基础上,动态创建若干个子任务来实现,它的具体解决方案是:
- 编写专门用于会签活动的任务分配处理器(实现 AssignmentHandler),在这个处理器中,通过流程变量来获取参加会议的用户 id,并为这些用户动态地创建 “会签任务” 对象。
- 编写完成 “会签任务” 的命令,在命令中设定会签的业务逻辑。
会签的业务逻辑有以下 4 种情况:
情况 | 说明 |
---|---|
一票否决制 | 参加会签的用户中,只要有一个人不同意,会签就结束,进入 “会签否决” 转移;如果所有人都同意,则进入 “会签通过” 转移。 |
一票通过制 | 逻辑与 “一票否决制”相反。 |
按比例否决制 | 全部参加会签的用户提交任务后,根据提交的意见,按比例来决定是否进入 “会签否决” 转移。 |
意见收集制 | 简单,全部会签用户通过任务表单提交任务后,收集这些意见,然后结束任务。 |
多步会签,相对较复杂,建议使用动态创建子流程的方式来实现。
至于更复杂的业务场景,比如将第三方业务系统接入会签,可以考虑使用 JMS 活动来发送消息,并监听第三方业务系统的应答模式,异步地实现会签需求。
jPDL:
- assignment-handler 定义了任务分配处理器,它会根据参与者(participants)动态地创建出相应的子任务。
- 可以在 participants 设定为流程变量的值,这样就可以动态地决定参与会签的用户啦(通过上一步任务表单来选定会签的任务)
- 会签被否决,则流程结束。
- 全体通过会签,则进入【执行】活动。
会签活动的任务处理器:
public class JointSignAssignment implements AssignmentHandler {
//会签参与者 ID 列表(在流程定义中注入)
private List participants;
//任务服务
private final static TaskService taskService = Configuration.getProcessEngine()
.getTaskService();
@Override
public void assign(Assignable assignable, OpenExecution execution) throws Exception {
String instanceId = execution.getProcessInstance().getId();
//获取会签活动任务对象
Task task = taskService.createTaskQuery().processInstanceId(instanceId)
.activityName(execution.getName()).uniqueResult();
//创建会签子任务
createSubTasks3(task);
}
...
}
创建会签子任务有三种实现方法。
基于主任务:
private void createSubTasks(Task task) {
if (participants == null) {
return;
}
for (String participant : participants) {
//基于主任务,创建会签子任务
Task subTask = taskService.newTask(task.getId());
//设置会签参与者为子任务的可处理者
subTask.setAssignee(participant);
taskService.addTaskParticipatingUser(task.getId(), participant, Participation.CLIENT);
}
}
这样做有问题,因为这个 taskService.newTask()
方法会立即持久化子任务及其历史,而此时的主任务还未提交。因此这样创建的子任务无法关联到主任务,会抛出持久化异常。
脱离主任务:
private void createSubTasks2(Task task) {
if (participants == null) {
return;
}
for (String participant : participants) {
//创建独立的会签子任务
Task subTask = taskService.newTask();
//设置会签参与者为子任务的可处理者
subTask.setAssignee(participant);
taskService.addTaskParticipatingUser(task.getId(), participant, Participation.CLIENT);
}
}
这样也有问题,因为这样凭空创建的任务,虽然不会在持久化时出现异常,但它无法关联到主任务。这样创建的任务其实是孤立的,这在后续的会签操作、级联删除以及历史分析,都会出现很大的问题。
使用主任务的 Task 对象:
private void createSubTasks3(Task task) {
if (participants == null) {
return;
}
for (String participant : participants) {
//使用主任务的 Task 对象的 createSubTask 方法(不会持久化)来创建会签子任务
Task subTask = ((OpenTask)task).createSubTask();
//设置会签参与者为子任务的可处理者
subTask.setAssignee(participant);
//关联会签任务到主任务
taskService.addTaskParticipatingUser(task.getId(), participant, Participation.CLIENT);
}
}
这样做既可以关联到主任务以及流程实例,又可以随着主任务一同被持久化。就选这种方法啦O(∩_∩)O哈哈~
基于【一票否决制】的会签任务提交命令类设计如下:
public class SubmitJoinSignTaskCmd implements Command {
//传递会签意见的任务变量
public static final String VAR_SIGN = "sign";
//会签通过时的转移路径名称(由构造函数传入)
private String passTransitionName;
//会签否决时的转移路径名称(由构造函数传入)
private String noPassTransitionName;
//主任务 ID(由构造函数传入)
private String mainTaskId;
//主任务对象
private Task mainTask;
//流程实例 ID
private String instanceId;
//会签任务对象(Setter 方法传入)
private Task joinSignTask;
public void setJoinSignTask(Task joinSignTask) {
this.joinSignTask = joinSignTask;
}
public String getInstanceId() {
return instanceId;
}
public SubmitJoinSignTaskCmd(String mainTaskId, String passTransitionName, String noPassTransitionName) {
this.mainTaskId = mainTaskId;
this.passTransitionName = passTransitionName;
this.noPassTransitionName = noPassTransitionName;
}
@Override
public Boolean execute(Environment environment) throws Exception {
//获取任务服务
TaskService taskService = environment.get(TaskService.class);
//获取主任务与流程实例 ID
mainTask = taskService.getTask(mainTaskId);
instanceId = mainTask.getExecutionId();
//获取当前会签任务
String joinSignTaskId = joinSignTask.getId();
//从当前会签任务的任务变量中获取 ”会签意见“
String sign = (String) taskService.getVariable(joinSignTaskId, VAR_SIGN);
//规则如下:如果意见为“不同意”,则表示否决
if (sign != null && sign.equals("不同意")) {
//存在否决意见,则会签活动结束(一票否决制)
//结束会签任务
taskService.completeTask(joinSignTaskId);
//为主任务增加一条会签意见记录(注释)
taskService.addTaskComment(mainTaskId, "用户:" + joinSignTask.getAssignee()
+ ",会签意见:" + sign);
//结束主任务,流向【否决】转移
taskService.completeTask(mainTaskId, noPassTransitionName);
//会签结束
return true;
}
/**
* 通过会签
*/
//完成会签任务
taskService.completeTask(joinSignTaskId);
//为主任务增加一条会签意见
taskService.addTaskComment(mainTaskId,"用户:"+joinSignTask.getAssignee()+
";会签意见为:"+sign);
//判定是否还有会签子任务
if(taskService.getSubTasks(mainTaskId).isEmpty()){//通过会签
//结束主任务,流向会签通过转移
taskService.completeTask(mainTaskId,passTransitionName);
return true;
}else{//会签活动还未结束
return false;
}
}
}
单元测试:
//发起流程实例
ProcessInstance processInstance=executionService.startProcessInstanceByKey("JointlySign");
instanceId=processInstance.getId();//实例 ID
//获取会签主任务
Task task=taskService.createTaskQuery().processInstanceId(instanceId)
.activityName(processInstance.findActiveActivityNames().iterator().next()
).uniqueResult();
//断言当前活动为会签
assertTrue(processInstance.isActive("会签"));
List subTasks=taskService.getSubTasks(task.getId());
//断言主任务产生了 3 条子任务
assertEquals(3, subTasks.size());
//获取主任务 ID
String taskId=task.getId();
//创建会签任务命令,指定会签通过转移以及会签否决转移
cmd=new SubmitJoinSignTaskCmd(taskId,"to execute","to end");
至此又分为两种情况:
否决会签:
//获取会签用户 Deniro 的任务
Task deniroTask=taskService.findPersonalTasks("Deniro").get(0);
//通过变量来模拟否决会签
Map vars=new HashMap<>();
vars.put(SubmitJoinSignTaskCmd.VAR_SIGN,"不同意");
taskService.setVariables(deniroTask.getId(), vars);
cmd.setJoinSignTask(deniroTask);
//提交会签任务(执行自定义命令)
boolean result= Configuration.getProcessEngine().execute(cmd);
//断言会签活动已完成
assertTrue(result);
//断言流程实例结束
assertProcessInstanceEnded(cmd.getInstanceId());
通过会签:
//获取会签用户 Deniro 的任务
Task deniroTask=taskService.findPersonalTasks("Deniro").get(0);
cmd.setJoinSignTask(deniroTask);//不设置否决意见,即通过
//提交会签任务(执行自定义命令)
boolean result= Configuration.getProcessEngine().execute(cmd);
//断言会签任务未完成
assertFalse(result);
//获取会签用户 Jack 的任务
Task jackTask=taskService.findPersonalTasks("Jack").get(0);
cmd.setJoinSignTask(jackTask);//不设置否决意见,即通过
//提交会签任务(执行自定义命令)
result= Configuration.getProcessEngine().execute(cmd);
//断言会签任务未完成
assertFalse(result);
//获取会签用户 Lucy 的任务
Task lucyTask=taskService.findPersonalTasks("Lucy").get(0);
cmd.setJoinSignTask(lucyTask);//不设置否决意见,即通过
//提交会签任务(执行自定义命令)
result= Configuration.getProcessEngine().execute(cmd);
//断言会签活动已完成
assertTrue(result);
//断言流程实例到达【执行】活动
ProcessInstance processInstance=executionService.findProcessInstanceById
(instanceId);
assertTrue(processInstance.isActive("执行"));
//完成【执行】活动
String executionId=processInstance.findActiveExecutionIn("执行").getId();
executionService.signalExecutionById(executionId);
//断言流程实例结束
assertProcessInstanceEnded(instanceId);
现在清楚了吧O(∩_∩)O哈哈~