在企业或事业单位,经常需要把一个任务分派给多条线去处理,每条线可以由一个或多个步骤构成,多条线的任务完成后需要再汇总一起于某个任务上。最常见的应用例子就是总经理把一个任务分发下去给多个部门领导,每个部门领导再分发给多个主管,每个主管再分发给多个人处理,然后再汇总给每个主管,每个主管再汇总给部门领导,每个部门领导再汇总至总经理。这就是我们说的任务层层分发,再层层汇总的概念,我们统称为多级分发与汇总。
以下示意图为该流程在运行过程中的实例产生任务的示例,可以看到分发出来的任务是动态的,并且其后续的任务也会随着产生出来。
解决方法一:
我们可以把多个任务线用子流程去实现也可以,这样在分发那里会产生多个子流程,子流程完成后,需要汇总。但若有多级分发与汇总,则需要子流程再嵌套子流程。
解决方法二:
把分发的任务线当作普通的任务来实现,该产生多少个任务可由分发任务决定,这些任务的名称是一样的,但任务实例id不一样,执行人不一样。
在Jbpm4或Activiti5上,动态创建子流程及对子流程的处理上,相对要完成的工作多一些,主要是activity或jbpm4上没有提供这块 api。而动态创建任务在jbpm4或activiti5上也是没有提供的,只有activiti5上提供了一个 taskService.newTask,而该方法产生的新任务则跟流程定义无关,则表示该任务完成后,不能产生后续的任务。在此,我们先提供 activiti5的解决办法。Jbpm4的解决方法可以参照该方式实现,以下为解决方案的步骤:
以下我们来分析如按第二种方法来实现,首先要解决两个问题,一个是分发,另一个是汇总。
分发任务其实就是复制任务的实现,Activiti任务产生后,我们再基于该任务复制新的任务出来以实现任务分发的功能。实现代码如下所示:
/**
* 从一个任务中复制另一任务出来
* @param taskEntity
* @param uIds 用户Id或组织Id或角色Id或岗位ID
* @param userType 值来自TaskUser里的type值
*/
public void newForkTasks(TaskEntity taskEntity,Set<String>uIds,String userType){
//获取当前任务的分发的令牌, value as T_1 or T_1_2
String token=(String)taskEntity.getVariableLocal(TaskFork.TAKEN_VAR_NAME);
if(token==null) token=TaskFork.TAKEN_PRE;
Iterator<String> uIt=uIds.iterator();
int i=0;
while(uIt.hasNext()){
if(i++==0){//原activiti产生的流程仅需要进行人员授权
assignTask(taskEntity,uIt.next(),userType);
//加上分发令牌变量
taskEntity.setVariableLocal(TaskFork.TAKEN_VAR_NAME, token+ "_"+ i);
//同时改变其Execution,以防止当该线的任务进行汇总时,其exectionId及proc_inst_id_相同时,删除该任务引起关联级别的错误
changeTaskExecution(taskEntity);
}else{//扩展产生的流程任务,另需要加上审批意见的记录
ProcessTask processTask=newTask(taskEntity,uIt.next(),userType);
TaskEntity newTask=getTask(processTask.getId());
//加上新的任务至新任务列队,以方便后续的任务回退处理
TaskThreadService.addTask(newTask);
//加上分发令牌变量
taskService.setVariableLocal(processTask.getId(),TaskFork.TAKEN_VAR_NAME, token+ "_"+ i);
TaskOpinion taskOpinion=new TaskOpinion(processTask);
try{
taskOpinion.setOpinionId(UniqueIdUtil.genId());
taskOpinion.setTaskToken(token);
taskOpinionService.add(taskOpinion);
}catch(Exception ex){
logger.error(ex.getMessage());
}
}
}
}
在任务分发时,还包括了分发的令牌处理,令牌的作用是用于区别不同分发线上的任务,如上图,“主管2分发”退回至“部门领导分发 ”时,不会对主管3的任务产生影响,当主管1,主管2汇总任务时,仅产生一个“部门领导汇总”的任务。令牌的变化规则如下图所示:
T令牌格式为:T_层序号,分发的时候,不断加上层序号,如两层分发则令牌变化为T_层序号_层序号。令牌的保存目前是存于任务的变量中,以方便任务获取,作用域仅限于任务。任务的汇总就是需要把多个任务合并回一个任务,实现该目标的方法就是利用任务跳转时,检查其后面的任务节点是否为汇总节点,若是的话,还要看同线上的兄弟节点是否已经完成,若没有完成,则仅是完成该任务则可,若已经完成,则产生后续的汇总节点。实现的代码如下所示:
//若目标节点为空,则代表没有进行自由跳转,同时后续的节点也不为汇总节点
if (destAct == null){
//完成当前任务
taskService.complete(task.getId(), variables);
return;
}
//是否需要去除当前任务的后续跳转线
boolean isNeedRemoveTran=false;
//检查目标任务是否为汇总节点,若为汇总节点,需要拿到当前任务的令牌,检查该令牌对应的TaskFork的已经汇总的任务个数,若目前的任务为最后一个汇总,则删除TaskFork,并且任务进行下一步的跳转,
//若目前任务不是最后一个汇总,则需要更新汇总完成的任务个数,并且只是完成当前任务,不作跳转。
if(bpmNodeSet==null){
bpmNodeSet=bpmNodeSetService.getByActDefIdJoinTaskKey(task.getProcessDefinitionId(), destAct.getId());
}
if(bpmNodeSet!=null){//目前该目标任务为分发汇总
//取当前任务的分发令牌
String token=(String)taskService.getVariableLocal(task.getId(),TaskFork.TAKEN_VAR_NAME);
if(token!=null){
TaskFork taskFork =taskForkService.getByInstIdJoinTaskKeyForkToken(task.getProcessInstanceId(), destAct.getId(), token);
if(taskFork!=null){
if(taskFork.getFininshCount()<taskFork.getForkCount()-1){
//更新完成任务的个数
taskFork.setFininshCount(taskFork.getFininshCount()+1);
taskForkService.update(taskFork);
//更新Token
String[]tokenSplits=token.split("[_]");
if(tokenSplits.length==2){//若为最外层的汇总,格式如T_1,则需要删除该令牌
taskService.setVariableLocal(task.getId(), TaskFork.TAKEN_VAR_NAME, null);
}
isNeedRemoveTran=true;
}else{
taskForkService.delById(taskFork.getTaskForkId());
//更新Token
String[]tokenSplits=token.split("[_]");
if(tokenSplits.length==2){//若为最外层的汇总,格式如T_1,则需要删除该令牌
taskService.setVariableLocal(task.getId(), TaskFork.TAKEN_VAR_NAME, null);
}else if(tokenSplits.length>=3){//更新token,转换如:T_1_1转成T_1
String newToken=token.substring(0, token.lastIndexOf("_"+tokenSplits[tokenSplits.length-1]));
taskService.setVariableLocal(task.getId(), TaskFork.TAKEN_VAR_NAME, newToken);
}
}
}
}
}
//进行锁定
lockTransto.lock();
//记录原来的跳转线
List<PvmTransition> backTransList = new ArrayList<PvmTransition>();
backTransList.addAll(curActi.getOutgoingTransitions());
try{
// 清除当前任务的外出节点
curActi.getOutgoingTransitions().clear();
if(!isNeedRemoveTran){
//创建一个连接
TransitionImpl transitionImpl = curActi.createOutgoingTransition();
transitionImpl.setDestination(destAct);
}
//完成当前任务
taskService.complete(task.getId(), variables);
}
finally{
// 删除创建的输出
curActi.getOutgoingTransitions().clear();
//加回原来的旧连接
curActi.getOutgoingTransitions().addAll(backTransList);
}
//解除锁定。
lockTransto.unlock();
数据结构
表名:BPM_TASK_FORK
功能说明:任务的分发与汇总记录
字段名 字段类型 备注
TASKFORKID NUMBER(18) TASKFORKID
ACTINSTID VARCHAR2(128) 流程实例ID
FORKTASKNAME VARCHAR2(256) 分发任务名称
FORKTASKKEY VARCHAR2(256) 分发任务Key
FORKSN NUMBER(0,0) 分发序号
FORKCOUNT NUMBER(0,0) 分发个数
FININSHCOUNT NUMBER(0,0) 完成个数
FORKTIME DATE 分发时间
JOINTASKNAME VARCHAR2(256) 汇总任务名称
JOINTASKKEY VARCHAR2(256) 汇总任务Key
FORKTOKENS VARCHAR2(512) 分发令牌号 格式如: T_1_1,T_1_2,T_1_3, 或 T_1,T_2,T_3,
FORKTOKENPRE VARCHAR2(64) 格式为T_ 或格式T_1 或T_1_2等
当任务进行分发时,需要在此表加记录,任务完成时,也需要对此表进行记录,任务的分发与汇总设置在BPM_NODE_SET表中也有记录,如下表的红色部分:
表名:BPM_NODE_SET
功能说明:BPM_NODE_SET
字段名 字段类型 备注
SETID NUMBER(18) SETID
DEFID NUMBER(18) 流程定义ID
NODENAME VARCHAR2(128) 节点名
NODEID VARCHAR2(128) activiti 节点ID
FORMTYPE NUMBER(0,0) 0=在线流程表单 1=URL表单
FORMURL VARCHAR2(128) 当表单类型为1时,表单对应的url
ACTDEFID VARCHAR2(127) Act流程发布ID
FORMDEFNAME VARCHAR2(128) FORMDEFNAME
NODETYPE NUMBER(0,0) NODETYPE
JOINTASKKEY VARCHAR2(128) JOINTASKKEY
JOINTASKNAME VARCHAR2(128) JOINTASKNAME
BEFOREHANDLER VARCHAR2(100) 前置处理器
AFTERHANDLER VARCHAR2(100) 后置处理器
ISALLOWBACK NUMBER(0,0) 1=允许 2=不允许
JUMPTYPE VARCHAR2(32) JUMPTYPE
SETTYPE NUMBER(1) 设置类型 0.任务节点1.开始表单2.默认表单
FORMKEY NUMBER(18) 表单KEY
剩下的工作则是进行分发节点的设置与如何根据分发的用户人数进行分发处理。
分发节点的设置类似如下所示:
任务的分发其实就是任务复制的过程,可以在TaskCreateListener里进行处理,任务汇总时,需要进行任务的合并,则不是最后一个分支,不进行新的后续的任务产生则可。