1.什么是会签?

在流程业务管理中,任务是通常都是由一个人去处理的,而多个人同时处理一个任务,这种任务我们称之为会签任务。这种业务需求很常见,如一个请款单,领导审批环节中,就需要多个部门领导签字。在流程业务中,我们可以把每个领导签字的环节都定义为任务,并且这个会签的人员是不固定的,若固定的我们可以通过Activiti的并行任务或串行任务来处理。会签的引入说明,无非就是为了流程流转至某一环节点,其审批的人员是动态的,并且需要根据会签审批的结果实现流程的不同流转。

2.中国特色的会签需求是什么?

会签需求主要有以下两方面:

  1. 会签的参与人员
  2. 会签审批的顺序
  3. 会签审批的结果
  4. 动态加签

以下我们就是围绕以上的需求进行扩展实现的

3.Activiti对于会签的实现

BPMN2的标准中并没有对以上这种情景提供完善的支持,因此要在Activiti中实现会签审批,我们需要结合Activiti提供的流程任务的多实例特性,进行一些必要的扩展,以支持我们的中国特色的会签需求。
会签任务也是一种人工任务,其在activiti的定义中,也是使用UserTask来定义,但在属性上我们需要对这个定义的类型进行特殊的配置,即为多任务实例类型(并行或串行)任何一种。另外需要定义会签的参与人员,再定义会签的完成条件(若不定义,表示其是所有参与人均完成后,流程才往下跳转)。

3.1.多实例的人工任务配置

通过在UserTask节点的属性上配置,如下所示:

其生成的BPMN的配置文件如下所示:

 <userTask id=”sid-78A17A9B-1185-48AA-A1CA-611421251D52″ name=”经理会签”>
<multiInstanceLoopCharacteristics isSequential=”false” activiti:collection=”${counterSignService.getUsers(execution)}”>
<completionCondition>${counterSignService.isComplete(execution)}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>

【说明】:

  1. isSequential=”false” 表示这是非串行会签,即为并行会签,如三个人参与会签,是三个人同时收到待办,任务实例是同时产生的。
  2. activiti:collection 表示是会签的参与人员集合,用户可以通过定义自身的服务类来获取
  3. completionCondition  表示是任务往下跳转的完成条件,返回true是,表示条件成立,流程会跳至下一审批环节。

我们就是围绕着这几点来实现中国式的流程会签的

3.2 会签任务的人员集合计算处理

我们在Spring容器中定义一个会签服务类(counterSignService)里面提供两个api接口,一个是获得任务的人员集合,另一个是判断当前任务是否已经完成了会签的计算,其参考代码如下所示:

package com.redxun.bpm.core.service.sign;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set; import javax.annotation.Resource; import org.activiti.engine.impl.pvm.delegate.ActivityExecution;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import com.redxun.bpm.activiti.util.ProcessHandleHelper;
import com.redxun.bpm.core.entity.BpmDestNode;
import com.redxun.bpm.core.entity.BpmRuPath;
import com.redxun.bpm.core.entity.BpmSignData;
import com.redxun.bpm.core.entity.IExecutionCmd;
import com.redxun.bpm.core.entity.ProcessMessage;
import com.redxun.bpm.core.entity.config.MultiTaskConfig;
import com.redxun.bpm.core.entity.config.TaskVotePrivConfig;
import com.redxun.bpm.core.entity.config.UserTaskConfig;
import com.redxun.bpm.core.identity.service.BpmIdentityCalService;
import com.redxun.bpm.core.manager.BpmNodeSetManager;
import com.redxun.bpm.core.manager.BpmSignDataManager;
import com.redxun.bpm.enums.TaskOptionType;
import com.redxun.org.api.model.IdentityInfo;
import com.redxun.sys.org.entity.OsGroup;
import com.redxun.sys.org.entity.OsRelType;
import com.redxun.sys.org.entity.OsUser;
import com.redxun.sys.org.manager.OsGroupManager;
import com.redxun.sys.org.manager.OsUserManager; /**
* 会签配置服务类
* @author csx
*
*/
public class CounterSignService {
@Resource
private BpmSignDataManager bpmSignDataManager;
@Resource
private BpmNodeSetManager bpmNodeSetManager;
@Resource
BpmIdentityCalService bpmIdentityCalService;
@Resource
private OsGroupManager osGroupManager;
@Resource
private OsUserManager osUserManager; private Log logger=LogFactory.getLog(CounterSignService.class);
/**
* 获得会签任务中的人员计算集合
* @param execution
* @return
*/
public Set<String> getUsers(ActivityExecution execution){ logger.debug("enter the CounterSignService "); Set<String> userIds=new LinkedHashSet<String>();
String nodeId=execution.getActivity().getId();
//1.回退处理通过
BpmRuPath backRuPath=ProcessHandleHelper.getBackPath();
if(backRuPath!=null && "YES".equals(backRuPath.getIsMultiple())){
String uIds=backRuPath.getUserIds();
userIds.addAll(Arrays.asList(uIds.split("[,]")));
execution.setVariable("signUserIds_"+nodeId,uIds);
return userIds;
} //2.通过变量来判断是否第一次进入该方法
String signUserIds=(String)execution.getVariable("signUserIds_"+nodeId); if(StringUtils.isNotEmpty(signUserIds)){
String[]uIds=signUserIds.split("[,]");
userIds.addAll(Arrays.asList(uIds));
return userIds;
} //3.从界面中的提交变量取用户
IExecutionCmd nextCmd=ProcessHandleHelper.getProcessCmd();
BpmDestNode bpmDestNode=nextCmd.getNodeUserMap().get(nodeId); if(bpmDestNode!=null && StringUtils.isNotEmpty(bpmDestNode.getUserIds())){
//加至流程变量中,以使后续继续不需要从线程及数据库中获取
execution.setVariable("signUserIds_"+nodeId,bpmDestNode.getUserIds());
execution.setVariable("priority_"+nodeId,bpmDestNode.getPriority());
execution.setVariable("expiretime_"+nodeId, bpmDestNode.getExpireTime()); String[]uIds=bpmDestNode.getUserIds().split("[,]");
userIds.addAll(Arrays.asList(uIds));
return userIds;
} //4.从数据库中读取节点人员配置获得参与人员列表
Collection<IdentityInfo> idInfoList=bpmIdentityCalService.calNodeUsersOrGroups(execution.getProcessDefinitionId(), execution.getCurrentActivityId(),execution.getVariables()); for(IdentityInfo identityInfo:idInfoList){
if(IdentityInfo.IDENTIFY_TYPE_USER.equals(identityInfo.getIdentityType())){
userIds.add(identityInfo.getIdentityInfoId());
}else{
List<OsUser> users= osUserManager.getByGroupIdRelTypeId(identityInfo.getIdentityInfoId(), OsRelType.REL_CAT_GROUP_USER_BELONG_ID);
for(OsUser u:users){
userIds.add(u.getUserId());
}
}
}
if(userIds.size()>0){
StringBuffer sb=new StringBuffer();
for(String uId:userIds){
sb.append(uId).append(",");
}
if(sb.length()>0){
sb.deleteCharAt(sb.length()-1);
}
execution.setVariable("signUserIds_"+nodeId,sb.toString());
}else{
String name=(String)execution.getActivity().getProperty("name");
ProcessMessage msg=ProcessHandleHelper.getProcessMessage();
msg.getErrorMsges().add("会签节点["+name+"]没有设置执行人员,请联系管理员!");
} return userIds;
} /**
* 会签是否计算完成
* @param execution
* @return
*/
public boolean isComplete(ActivityExecution execution){
//完成会签的次数
Integer completeCounter=(Integer)execution.getVariable("nrOfCompletedInstances");
//总循环次数
Integer instanceOfNumbers=(Integer)execution.getVariable("nrOfInstances"); String solId=(String)execution.getVariable("solId");
String nodeId=execution.getActivity().getId();
UserTaskConfig taskConfig=bpmNodeSetManager.getTaskConfig(solId, nodeId); //获得任务及其多实例的配置,则任务不进行任何投票的设置及处理,即需要所有投票完成后来才跳至下一步。
if(taskConfig.getMultiTaskConfig()==null){
return completeCounter==instanceOfNumbers;
} //获得会签的数据
List<BpmSignData> bpmSignDatas=bpmSignDataManager.getByInstIdNodeId(execution.getProcessInstanceId(), nodeId);
MultiTaskConfig multiTask=taskConfig.getMultiTaskConfig();
//通过票数
int passCount=0;
//反对票数
int refuseCount=0;
//弃权票数
int abstainCount=0; for(BpmSignData data:bpmSignDatas){
int calCount=1;
//弃权不作票数统计
if(TaskOptionType.ABSTAIN.name().equals(data.getVoteStatus())){
abstainCount++;
continue;
}
String userId=data.getUserId();
//检查是否有特权的处理
if(multiTask.getVotePrivConfigs().size()>0){
//计算用户的用户组
List<OsGroup> osGroups=osGroupManager.getBelongGroups(userId); for(TaskVotePrivConfig voteConfig:multiTask.getVotePrivConfigs()){
//是否在特权里
boolean isInPriv=false;
//为用户类型
if(TaskVotePrivConfig.USER.equals(voteConfig.getIdentityType())
&& voteConfig.getIdentityIds().contains(userId)){
isInPriv=true;
}else{//为用户组类型
for(OsGroup osGroup:osGroups){
if(voteConfig.getIdentityIds().contains(osGroup.getGroupId())){
isInPriv=true;
break;
}
}
}
//若找到特权,则计算其值
if(isInPriv){
calCount=voteConfig.getVoteNums();
break;
}
}
}
//统计同意票数
if(TaskOptionType.AGREE.name().equals(data.getVoteStatus())){
passCount+=calCount;
}else{//统计反对票数
refuseCount+=calCount;
}
} logger.debug("==============================passCount:"+passCount
+" refuseCount:" + refuseCount +" abstainCount:"+abstainCount);
//是否可以跳出会签
boolean isNext=false;
String result=null; //按投票通过数进行计算
if(MultiTaskConfig.VOTE_TYPE_PASS.equals(multiTask.getVoteResultType())){
//计算是否通过
//按投票数进行统计
if(MultiTaskConfig.CAL_TYPE_NUMS.equals(multiTask.getCalType())){
//代表通过
if(passCount>=multiTask.getVoteValue()){
isNext=true;
result="PASS";
}
}else{//按百分比进行计算
int resultPercent=new Double(passCount*100/(passCount+refuseCount+abstainCount)).intValue();
//代表通过
if(resultPercent>=multiTask.getVoteValue()){
isNext=true;
result="PASS";
}
}
}else{//按投票反对数进行计算
//计算是否通过
//按投票数进行统计
if(MultiTaskConfig.CAL_TYPE_NUMS.equals(multiTask.getCalType())){
//代表通过
if(refuseCount>=multiTask.getVoteValue()){
isNext=true;
result="REFUSE";
}
}else{//按百分比进行计算
int resultPercent=new Double(refuseCount*100/(passCount+refuseCount+abstainCount)).intValue();
//代表通过
if(resultPercent>=multiTask.getVoteValue()){
isNext=true;
result="REFUSE";
}
}
} if((MultiTaskConfig.HANDLE_TYPE_DIRECT.equals(multiTask.getHandleType())&& isNext)//直接处理
|| (MultiTaskConfig.HANDLE_TYPE_WAIT_TO.equals(multiTask.getHandleType()) && completeCounter==instanceOfNumbers)){//等待所有的处理完
execution.setVariable("voteResult_"+nodeId, result);
//删除该节点的会签数据
for(BpmSignData data:bpmSignDatas){
bpmSignDataManager.deleteObject(data);
}
return true;
} return false; }
}

【说明】
以上的代码的人员计算有几个来源:

  1. 流程回退时获得原来参与的人员
  2. 直接从流程变量中获取
  3. 从界面中的提交变量取用户
  4. 从数据库中读取节点人员配置获得参与人员列表

会签的投票处理结果放至流程变量中去,供后续的分支条件来处理,我们把会签的配置设置管理如下:

我们提供了按票数、百分比的投票处理,同时允许有特权的用户的投票规则以支持灵活的会签结果运算。

实现了以上配置后,流程的会签就比较简单,具体的效果如下视频演示:

http://www.redxun.cn/?p=456

了解探讨,请联系1361783075

JSAAS的Activiti会签开发扩展处理的更多相关文章

  1. Java学习-039-源码 jar 包的二次开发扩展实例(源码修改)

    最近在使用已有的一些 jar 包时,发现有些 jar 包中的一些方法无法满足自己的一些需求,例如返回固定的格式,字符串处理等等,因而需要对原有 jar 文件中对应的 class 文件进行二次开发扩展, ...

  2. 【前端工具】Chrome 扩展程序的开发与发布 -- 手把手教你开发扩展程序

    关于 chrome 扩展的文章,很久之前也写过一篇.清除页面广告?身为前端,自己做一款简易的chrome扩展吧. 本篇文章重在分享一些制作扩展的过程中比较重要的知识及难点. 什么是 chrome 扩展 ...

  3. PHP-CPP开发扩展(一)

    PHP-CPP是一个用于开发PHP扩展的C++库.PHP-CPP提供了一系列完善的文档.易于使用和扩展的类,让你可以相对快速的创建PHP的原生扩展. 为什么使用PHP-CPP 很快 用C++编写的代码 ...

  4. ----转载----【前端工具】Chrome 扩展程序的开发与发布 -- 手把手教你开发扩展程序

    关于 chrome 扩展的文章,很久之前也写过一篇.清除页面广告?身为前端,自己做一款简易的chrome扩展吧. 本篇文章重在分享一些制作扩展的过程中比较重要的知识及难点. 什么是 chrome 扩展 ...

  5. 搭配 VS Code Remote 远程开发扩展在 WSL 下开发

    ❗ 注意:远程开发扩展需要在 Visual Studio Code Insiders 上使用. Visual Studio Code Remote - WSL 扩展允许你直接借助 VS Code 令  ...

  6. php开发扩展环境的搭建(Windows)

    php开发扩展环境的搭建(Windows) 前期准备: (1) 下载php-5.3.10源码包(php-5.3.10.tar.bz2)并解压到C:\php-5.3.10:下载二进制包php-5.3.1 ...

  7. Activiti 工作流会签开发设计思路

    http://man1900.iteye.com/blog/1607753 在流程业务管理中,任务是通常都是由一个人去处理的,而多个人同时处理一个任务,这种任务我们称之为会签任务.这种业务需求也很常见 ...

  8. 准备Activiti的开发环境

    1.创建项目

  9. 浅谈Activiti Modeler 的扩展

    为什么要扩展         最近项目打算用activiti工作流中activiti modeler来做模块的可视化订阅,但是原生的activiti任务节点,有一些不符合业务需要,比如 配置项多,属性 ...

随机推荐

  1. 今天遇到的面试题for(j=0,i=0;j<6,i<10;j++,i++) { k=i+j; } k 值最后是多少?

    for(j=0,i=0;j<6,i<10;j++,i++) { k=i+j; } k 值最后是多少? <script type="text/javascript" ...

  2. android学习6——canvas的save,restore作用

    先看如下代码 public class SaveRestoreActivity extends Activity { @Override public void onCreate(Bundle sav ...

  3. 漂亮的代码3:flatten 一个数组

    看到一个题目: flatten([1,2,3]) // => [1,2,3] flatten([[1,2,3],["a","b","c" ...

  4. php 手动搭建环境

    php手动搭建环境有好多种组合,版本号不一致,会导致搭建失败. 我搭建的组合是: php5.6+MySQL5.6+Apache2.4的组合. 一.PHP语言包下载 首先从官网上下载php5.6 htt ...

  5. linux下keepalived 安装配置

    keepalived是一个类似于layer3, 4 & 7交换机制的软件,也就是我们平时说的第3层.第4层和第7层交换.Keepalived的作用是检测web服务器的状态,如果有一台web服务 ...

  6. [前言] 实现一个Android电子书阅读APP

    大家好,我是小方,我将在接下来的几篇文章中从零实现一个网络小说阅读器,从安卓编程最基础的部分讲起,直至成功完成我们的应用,从新建一个项目开始,不断添加新的代码,添加新的界面,循序渐进,涵盖所有我们需要 ...

  7. 撸基础篇系列,JAVA的NIO部分

    前言:撸基础篇系列,避免每次都要从头开始看,写个自己的知识体系树 NIO 核心就是异步, 比如,复制文件,让操作系统去处理,等通知 BIO核心类 一,BIO NIO基本操作类 Bytebuffer 构 ...

  8. 翻译:如何使用CSS实现多行文本的省略号显示

    本文翻译自CSS Ellipsis: How to Manage Multi-Line Ellipsis in Pure CSS,文中某些部分有些许改动,并添加译者的一些感想,请各位读者谅解. 合理的 ...

  9. gulp快速入门&初体验

    前言 一句话先 gulp 是一个可以简单和自动化"管理"前端文件的构建工具 先说我以前的主要工作,我主要是做游戏服务端的,用c++/python,所以我对东西的概念理解难免要套到自 ...

  10. 编程那些事儿:如何快速地"借用"CSS

    做前端开发有时候会碰到任务紧急,需要马上写好静态页的问题.比如,设计师给你扔了一个设计稿,要你在下班之前搞定.这时候你如热锅上的蚂蚁,如果自己写css的话,时间紧张,于是上网找了一下相关模板页面,找到 ...