一、jbpm中审批人的概念
在工作流中每一个人工任务的办理者被称作审批人,在原生的jbpm定义中,我们可以看到assignee和assignmentHandler这两个标签。
<task name="review" g="96,16,127,52"> <assignment-handler class="org.jbpm.examples.task.assignmenthandler.AssignTask"> <field name="assignee"> <string value="chao.gao" /> </field> </assignment-handler> <transition to="wait" /> </task>
assignee如果是单用户,可以是字符串;如果是多用户,则是列表或数组;另外也可以使用类似jsp中的正则表达式,进行动态审批人的指定。
如果有assignee,而没有分配处理器标签,则引擎自动将该人工任务分配给所有assignee。如有assignmentHandler会按照默认或定制的分配处理器制定的规则进行审批人的智能筛选以及对任务进行动态的指派。
在会签人方案一文中,提到了关于使用assignmentHandler生成会签子任务的实例,在决策里也提到了DefaultDecisionHandler,jbpm使用监听器模式对不同的处理类进行调用。
/** * @author Tom Baeyens */ public class AssignTask implements AssignmentHandler { private static final long serialVersionUID = 1L; String assignee; public void assign(Assignable assignable, OpenExecution execution) { assignable.setAssignee(assignee); } }
以上是jbpm审批人的原生概念以及如何将指定或动态指定的用户与人工任务相绑定的理论知识。
下面阐述我们的审批人选择以及分配任务的思路
二、智能选择审批人
审批人设置表结构
数据列表如下
页面设置:
一般情况下,商用工作流都提供基于组织架构、汇报关系、角色的审批人选择方法,OA中也提供了相应功能。基于组织架构上下级关系,主要有当前部门、当前部门之上级部门、当前部门上级部门之上级部门,基于组织架构的层级关系,主要有一级部门、二级部门、三级部门、子公司等。以上两种规则,除当前部门外还可以通过表单数据指定部门的编码。基于组织架构的成员,可以将本架构内所有成员全部纳入审批人列表。
代码如下:
private Set<String> getAssigneeByAssignSetting(FlowRunTime flowRunTimeInfo, UserAssignEntity assignSetting) { Set<String> candidateUserIdList = new HashSet<String>(); if("Y".equals(assignSetting.getIsForm())){ String fieldValue = getFiledValueByAssignSetting(assignSetting, flowRunTimeInfo); if(StringUtils.isNotEmpty(fieldValue) && assignSetting.getFieldName().contains(",")){ String[] staffIds = fieldValue.split("[,]"); for(String staffId : staffIds){ if(StringUtils.isNotEmpty(staffId)){ StaffinfoEntity staffinfoEntity = staffInfoService.queryStaffInfoByStaffId(staffId); if(staffinfoEntity != null && StringUtils.isNotEmpty(staffinfoEntity.getStaffCode())){ candidateUserIdList.add(staffinfoEntity.getStaffCode()); }else { throw new UserAssignException("通过表单选人出现错误,请检查!"); } } } }else { StaffinfoEntity staffinfoEntity = staffInfoService.queryStaffInfoByStaffId(fieldValue); if(StringUtils.isEmpty(fieldValue)){ throw new UserAssignException("通过表单选人出现错误,请检查!"); } OrgEntity org = orgService.queryOrgByCode(fieldValue); if(!"ShanghaiHQ".equals(fieldValue)){ List<HrbpEntity> hrbpEntityList = hrbpService.queryHrbpByDqCode(fieldValue); if(!CollectionUtils.isEmpty(hrbpEntityList)){ for(HrbpEntity hrbp : hrbpEntityList){ String staffCode = staffInfoService.queryStaffByStaffId(hrbp.getHrbpId()).getStaffCode(); if(StringUtils.isNotEmpty(staffCode) && !candidateUserIdList.contains(staffCode)){ candidateUserIdList.add(staffCode); } } } } if(staffinfoEntity != null){ if (UserAssignConstants.USERASSIGN_LEADER_OF_LASTSTEP.equals(assignSetting.getChooseRule()) || UserAssignConstants.USERASSIGN_LEADER_OF_STARTASSIGNEE.equals(assignSetting.getChooseRule())) { String directleaderCode = staffInfoService.queryLeader(fieldValue).getDirectLeaderCode(); if(StringUtils.isNotEmpty(directleaderCode)){ StaffinfoEntity directLeader = staffInfoService.queryStaffInfoByStaffId(directleaderCode); if (directLeader != null) { candidateUserIdList.add(directLeader.getStaffCode()); } } }else if(UserAssignConstants.USERASSIGN_LEADER_OF_LEADER.equals(assignSetting.getChooseRule())){ String indirectleaderCode = staffInfoService.queryLeader(fieldValue).getLeapfrogLeaderCode(); StaffinfoEntity indirectLeader = staffInfoService.queryStaffInfoByStaffId(indirectleaderCode); if(indirectLeader != null){ candidateUserIdList.add(indirectLeader.getStaffCode()); } } else if(UserAssignConstants.USERASSIGN_LEADER_OF_LEADER_OF_LEADER.equals(assignSetting.getChooseRule())){ String indirectleaderCode = staffInfoService.queryLeader(fieldValue).getLeapfrogLeaderCode(); //越级主管 if(StringUtils.isNotEmpty(indirectleaderCode)){ StaffinfoEntity indirectLeader = staffInfoService.queryStaffInfoByStaffId(indirectleaderCode); if(indirectLeader != null){ String inIndirectLeaderStaffId = staffInfoService.queryLeader(indirectLeader.getStaffId()).getDirectLeaderCode(); if(StringUtils.isNotEmpty(inIndirectLeaderStaffId)){ StaffinfoEntity inIndirectLeader = staffInfoService.queryStaffInfoByStaffId(inIndirectLeaderStaffId); if(inIndirectLeader != null){ candidateUserIdList.add(inIndirectLeader.getStaffCode()); } } } } } if(UserAssignConstants.USERASSIGN_STARTER.equals(assignSetting.getChooseRule())){ candidateUserIdList.add(staffinfoEntity.getStaffCode()); } } if(org != null){ if(UserAssignConstants.USERASSIGN_LEADER_OF_ORG.equals(assignSetting.getChooseRule())){//当前部门的领导: if (org != null && !StringUtils.isBlank(org.getManagerCode())) { String managerCode = org.getManagerCode(); candidateUserIdList.add(managerCode); } } else if(UserAssignConstants.USERASSIGN_LEADER_OF_LEADER_OF_ORG.equals(assignSetting.getChooseRule())){//申请部门的上级领导: if (org != null && !StringUtils.isBlank(org.getParentCode())) { OrgEntity orgParent = orgService.queryOrgByCode(org.getParentCode()); if (orgParent!=null && !StringUtils.isBlank(orgParent.getManagerCode())) { String managerCode = orgParent.getManagerCode(); candidateUserIdList.add(managerCode); } } } else if(UserAssignConstants.USERASSIGN_LEADER_OF_LEADER_OF_LEADER_OF_ORG.equals(assignSetting.getChooseRule())){//申请部门的上级之上级领导: if (org != null && !StringUtils.isBlank(org.getParentCode())) { OrgEntity orgtwo = orgService.queryOrgByCode(org.getParentCode()); if (orgtwo!=null && !StringUtils.isBlank(orgtwo.getManagerCode())) { OrgEntity orgParent = orgService.queryOrgByCode(orgtwo.getParentCode()); if (orgParent!=null && !StringUtils.isBlank(orgParent.getManagerCode())) { String managerCode = orgParent.getManagerCode(); candidateUserIdList.add(managerCode); } } } } else if(UserAssignConstants.USERASSIGN_LEADER_OF_PLATFORM_OR_COMPANY.equals(assignSetting.getChooseRule())){//申请部门的子公司领导:子公司领导 OrgEntity orgPlatform = orgService.queryPlatformEntityByDepCode(fieldValue); if (orgPlatform != null && !StringUtils.isBlank(orgPlatform.getManagerCode())) { String managerCode = orgPlatform.getManagerCode(); candidateUserIdList.add(managerCode); } } else if(UserAssignConstants.USERASSIGN_LEADER_OF_THIRD_ORG.equals(assignSetting.getChooseRule())){//三级部门 if (OrgConstants.LEVEL_THIRD_ORG.equals(org.getBranchOrg())) { if (StringUtils.isNotEmpty(org.getManagerCode())) { String managerCode = org.getManagerCode(); candidateUserIdList.add(managerCode); } } else if(OrgConstants.LEVEL_SECOND_ORG.equals(org.getBranchOrg()) || OrgConstants.LEVEL_FIRST_ORG.equals(org.getBranchOrg()) || OrgConstants.LEVEL_PLATFORM.equals(org.getBranchOrg())){ if(StringUtils.isNotEmpty(org.getManagerCode())){ candidateUserIdList.add(org.getManagerCode()); } }//如果是一级部门却要找三级部门,只能提示找不到; } else if(UserAssignConstants.USERASSIGN_LEADER_OF_SECOND_ORG.equals(assignSetting.getChooseRule())){//二级部门 if(OrgConstants.LEVEL_SECOND_ORG.equals(org.getBranchOrg())){ String managerCode = org.getManagerCode(); if(!StringUtils.isBlank(managerCode)){ candidateUserIdList.add(managerCode); } }else if(OrgConstants.LEVEL_THIRD_ORG.equals(org.getBranchOrg())){ if (!StringUtils.isBlank(org.getParentCode())) { OrgEntity orgParent = orgService.queryOrgByCode(org.getParentCode()); if (orgParent!=null && !StringUtils.isBlank(orgParent.getManagerCode())) { String managerCode = orgParent.getManagerCode(); candidateUserIdList.add(managerCode); } } } else if(OrgConstants.LEVEL_FIRST_ORG.equals(org.getBranchOrg()) || OrgConstants.LEVEL_PLATFORM.equals(org.getBranchOrg())){ if(StringUtils.isNotEmpty(org.getManagerCode())){ candidateUserIdList.add(org.getManagerCode()); } } } else if(UserAssignConstants.USERASSIGN_LEADER_OF_FIRST_ORG.equals(assignSetting.getChooseRule())){//一级部门 if(OrgConstants.LEVEL_FIRST_ORG.equals(org.getBranchOrg())){ String managerCode = org.getManagerCode(); if(!StringUtils.isBlank(managerCode)){ candidateUserIdList.add(managerCode); } }else if(OrgConstants.LEVEL_SECOND_ORG.equals(org.getBranchOrg())){ if (!StringUtils.isBlank(org.getParentCode())) { OrgEntity orgParent = orgService.queryOrgByCode(org.getParentCode()); if (orgParent!=null && !StringUtils.isBlank(orgParent.getManagerCode())) { String managerCode = orgParent.getManagerCode(); candidateUserIdList.add(managerCode); } } } else if(OrgConstants.LEVEL_THIRD_ORG.equals(org.getBranchOrg())){ if (!StringUtils.isBlank(org.getParentCode())) { OrgEntity orgParent = orgService.queryOrgByCode(org.getParentCode()); if (orgParent != null && !StringUtils.isBlank(orgParent.getParentCode())) { OrgEntity orgParentPa = orgService.queryOrgByCode(orgParent.getParentCode()); if (orgParentPa!=null && !StringUtils.isBlank(orgParentPa.getManagerCode())) { String managerCode = orgParentPa.getManagerCode(); candidateUserIdList.add(managerCode); } } } } else if(OrgConstants.LEVEL_PLATFORM.equals(org.getBranchOrg())){ if(StringUtils.isNotEmpty(org.getManagerCode())){ candidateUserIdList.add(org.getManagerCode()); } } } } } }
根据汇报关系,目前有发起人本人,发起人直接主管、越级主管、第二越级主管。同样,除发起人外,还可以通过表单数据指定的工号或用户名找出其汇报关系。
基于角色,同基于组织架构的成员规则相同,即将该角色下的所有成员加入审批人列表
当部门和角色均全选时,将按照其交集不空,输出交集;否则输出全集的逻辑进一步筛选。
代码如下:
if(candidateUserIdList.isEmpty()){ if (assignSetting != null){ //获取直接指定的用户 if (StringUtils.isNotEmpty(assignSetting.getUserId())) { String[] userIds = assignSetting.getUserId().split("[,]"); for (String user : userIds) { candidateUserIdList.add(user); } } //通过角色和部门选择用户 Set<String> roleAndDeptSet = getRoleAndDept(assignSetting); if(!CollectionUtils.isEmpty(roleAndDeptSet)){ candidateUserIdList.addAll(roleAndDeptSet); } String startAssignee = ""; if(StringUtils.isNotEmpty(flowRunTimeInfo.getProcessExecutionId())){ startAssignee = processExecutionService.queryById(flowRunTimeInfo.getProcessExecutionId()).getCreateUserCode(); } else { startAssignee = OAUserContext.getUserCode(); } HashSet<String> allUser = chooseCandidateUser(startAssignee, assignSetting, candidateUserIdList); if(!CollectionUtils.isEmpty(allUser)){ candidateUserIdList = allUser; } } }
如汇报关系与组织架构的基准并非来自发起人或发起人所属组织时,isForm(即来自表单)需勾选且需填写字段名称。该字段名称所代表的用户或组织即作为智能选人规则的基准。目前情况下,如表单字段值有多个,则只代表直接输出字段所指定的用户,如果无法筛选出用户,则审批人列表显示为空。
代码如下:
private String getFiledValueByAssignSetting(UserAssignEntity assignSetting, FlowRunTime flowRunTime) { String fieldName_ = assignSetting.getFieldName(); String[] fieldNames = fieldName_.split("[,]"); String formData = flowRunTime.getFormData(); String formDefineId = flowRunTime.getFormDefineId(); String fieldValue = ""; for(String fieldName : fieldNames){ List<Map<String, Object>> filedValuelist = formDataService.resolveFormData(formDefineId, formData); if(filedValuelist != null && filedValuelist.size() > 0){ Map<String, String> valueMap = new HashMap<String, String>(); for(Map<String, Object> map : filedValuelist){ for(String key : map.keySet()){ if(FormDefineConstants.FORMDEFINE_MAP.equals(key)){ valueMap = (Map<String, String>)map.get(key); } } } if(!valueMap.isEmpty()){ String value = valueMap.get(fieldName); if(StringUtils.isNotEmpty(value)){ if("".equals(fieldValue) ){ fieldValue = value; } else { fieldValue = fieldValue + "," + value; } } } } else { throw new UserAssignException(UserAssignConstants.USERASSIGN_EXCEPTION_FORM_ERROR); } } return fieldValue; }
另外,我们还提供了直接指定审批人的方法,对一些特殊无法通过以上逻辑筛选,但审批人又固定的即可直接指定审批人。
三、对智能选人规则及任务分配的展望
通过以上分析也可以看出,审批人可以在流程运行期间通过某些规则直接指定,所以可以将其称为建模期(或定义期)指派。即通过流程定义进行建模的期间,直接为流程中的活动指派参与者的集合。而在运行期间才能指定审批人(3.1、3.2)或进行分配任务(3.3、3.4、3.5)的则被称为运行期规则。
3.1同前驱
顾名思义,就指的是在流程的某个环节的办理人,与当前流程当前环节的某个前驱节眯是同一个人,这也与工作流“保持熟悉(Retain familiar)”原则相切合。
同前驱的选人规则目前无法通过建模期指派实现,比如以下场景:
1.招聘需求有“编制管理员审核”(审批候选人可能有多个)以及“招聘管理员签收”环节,其中要求“招聘管理员签收”环节必须与其前驱节点“编制管理员审核”确定;
2.档案借阅的借出环节与还进环节也要求必须是同一个人。
目前我们已经就这种需求形成两种方案:一种是杜提出的在前驱中后置将某代表其同后继的隐藏工号字段填充,后继则使用表单字段选人将审批人选出;一种是我提出的用同前驱节点及execution id遍历历史任务表,将前驱节点历史审批人拿出做为后继节点审批人。
3.2基于历史、基于能力、随机或循环
以上的三种选人规则,是一种自学习自组织的选人方法。如通过以上选人得出的任选审批人为多个,那么可以使用以上规则筛选为一个或将多人按以规则排序。
基于历史的是一种笼统的概念,又可以细分为当前任务最少、完成历史任务最多等规则;基于能力则可细分为当前列表中职务最大、层级最高、办理任务耗时最少等;随机则是指列表元素的先后顺序或指定用户是随机的,循环是指列表元素的先后顺序或指定是循环呈现的。
3.3委托\代理、移交\转交
委托是一种运行期间的智能选人模式。对于一个人工任务,在当前活动的参与者出差或休假期间,会用到委托功能。针对委托目前形成的方案是委托模块(委托表除用作工作流选人外,还可能用作),委托表结构如下:
委托人在休假期间新建一个委托,标识委托人、委托时间以及委托的流程(有一些重要流程可能不允许委托)。在每次审批人选出后需要遍历委托表中的有效数据,如当前审批人存在有效委托,则将审批人替换为被委托人,显示名为“被委托人(委托人代理)”,向委托人和被委托人均发送邮件,同时在我的工作中增加“代理工作”模块,将我被代理审批的工作展示出来。
另外的一个委托实现方案来自《流程的永恒之道》一书,即通过另外一个工作流请假单或工作代理单的方式将代理数据写入。代理数据等同于上文中的委托数据。
关于移交\转交:在当前审批者可以使用工作流功能,但由于职责变化或其他原因,将本任务直接转与其他用户。移交\转交功能是下文强行更改审批人的特例。
某用户将属于自己的且正在执行的任务移交给其他人执行。移交模式与委托模式的区别在于,委托是在事前,而移交模式是在事后。
3.4加签
对于会签节点,存在这样一种可能,会签的目前某参与者或者管理员认为有必要临时将某用户加入会签人列表并为其下发会签任务。加签的实现方案即是:将当前子任务的父任务取出,基于父任务生成一个新的子任务,然后将该子任务分配给该临时用户,并发送提醒邮件。
3.5强行更改审批人
用于某些原因(如当前审批人离职或休假),在运行期间需要将某审批单改分配给另外的用户。逻辑如下:
/** * * @author chao.gao * @date 2014-12-26 上午9:43:40 * @see com.gaochao.oa.module.bpm.workflow.api.server.service.IProcessActivityService#changePeople(com.gaochao.oa.module.bpm.workflow.api.shared.dto.FlowRunTime) * @param flowRunTime * @return */ @Override public boolean changePeople(FlowRunTime flowRunTime,TaskVo task) { if(StringUtils.isNotEmpty(flowRunTime.getCurrentUser())){ ProcessExecutionEntity processExecutionEntity = processExecutionService.queryProcessExecutionByPkValue(flowRunTime.getPkValue()); List<Task> list = jbpmOperatorService.getTasksByProcessInstanceId(processExecutionEntity.getProcessInstanceId()); processExecutionService.changePeople(flowRunTime); MailSettingEntity mailSetting = mailSettingService.queryMailSetting(); Map<String, List<MailblackwhitelistEntity>> mailBlackWhiteListMap = mailblackwhitelistService.queryMailBlackWhiteList(); //设置流程处理ID flowRunTime.setProcessExecutionId(processExecutionEntity.getId()); for(int i=0;i<list.size();i++){ Task task1 = list.get(i); task.setTaskId(task1.getId()); processTaskService.changePeople(task); //设置任务Id flowRunTime.setNextTaskId(task1.getId()); //经办人 final String assign = flowRunTime.getCurrentUser(); jbpmOperatorService.assignTask(task1.getId(), assign); //发送邮件 EmailUtil.sendMail2Proxy(assign, flowRunTime, mailSetting, mailBlackWhiteListMap); } }else{ throw new ProcessActivityException("没有选择人员"); } return true; }