spring boot 2+activiti 6 与应用用户整合,提交时手工选环节、选人,退回,转办,审批权限校验

  1. 场景描述
    由于项目初期没有去集成工作流,现由于业务需要,需要集成之。
    目前市面上开源的工作流有JBPMACTIVITIFLOWABLE 三个,JBPM是早期的产物,秉着【用新不用旧】原则,JBPM直接被淘汰,再尝试使用FLOWABLE时发现资料太少。最后选择了ACTIVITI
    目前ACTIVITI有5.x,6.x ,7.x三个版本,笔者这里整合的是6.x。
    以下所有的代码都是基于6.x。
    springboot版本:2.1.1.RELEASE
    ACTIVITI版本:6.0.0.4

  1. 相关准备工作
    项目并没有实时编辑流程图的需求,就没有去整合 modeler在线编辑。流程图BPMN则用eclipse下的插件来画即可(IDEA的不太友好),故直接引入starter即可
		
			org.activiti
			activiti-spring-boot-starter-basic
			6.0.0
		

另外,我们这里有个场景(下面会讲到)会使用到 mvel 来计算el表达式,所以

		
			org.mvel
			mvel2
			2.2.1.Final
		

然后则需要在这个文件夹下建立一个流程图

/resources/processes/

spring boot 2+activiti 6 与应用用户整合,提交时手工选环节、选人,退回,转办,审批权限校验_第1张图片
对应的xml文件为



    
        
        
        
        
        
        
        
        
            
        
        
        
        
            
        
        
    
    
        
            
                
            
            
                
            
            
                
            
            
                
            
            
                
            
            
                
            
            
                
                
            
            
                
                
            
            
                
                
                
                
                
                
                    
                
            
            
                
                
            
            
                
                
                
                    
                
            
            
                
                
            
        
    

随着项目启动,可以看到,这两个表就已经有数据了

ACT_GE_BYTEARRAY
ACT_RE_PROCDEF

基本的整合就算完成了

3. 用户整合
其实就是把activiti的用户与系统中的整合。
ACTIVITI的认证信息其实在这四个表里,流程中配置的处理人/组 也都是依赖以下四个表。
例如上面BPMN文件中的 activiti:candidateGroups=“电费稽核员” 就表示 当前环节由【电费稽核员】这个用户组的成员进行审批。

    ACT_ID_GROUP
    ACT_ID_INFO
    ACT_ID_MEMBERSHIP
    ACT_ID_USER

方案一:弃用activiti自带的认证表
方案二:将自己系统内的用户表同步至activiti

我们这里使用了方案二,由于系统内并没有用户组所以就直接使用了ACT_ID_MEMBERSHIP 作为用户组。
而用户在增删改时,同步至ACT_ID_USER。这里代码很简单 ,只展示出来一些关键的代码

用户整合:

其中 com.account.project.system.user.domain.User 是系统的用户

    @Transactional
    public void syncSysUser2ActUser(String loginName) {
        /*  判断当前登录名对应的流程用户是否存在
         *  存在则更新之  否则新增
         *  */
        if (StringUtils.isEmpty(loginName)) {
            throw new RuntimeException("loginName不可为空!");
        }
        com.account.project.system.user.domain.User u = userMapper.selectUserByLoginName(loginName);//根据登录名查询出系统用户
        if (u == null) {
            throw new RuntimeException("无此用户" + loginName);
        }

        User user = turnSysUser2ActUser(u);//转化为activiti用户
        identityService.saveUser(user);

        /* 更新其他附属信息 */
        fillUserRefInfo(u);
    }
    /**
     * @Description: 将系统用户转换为流程用户
     * @Author: fg
     * @Date: 2019/8/8
     */
    User turnSysUser2ActUser(com.account.project.system.user.domain.User sysUser) {
        User resUser = selectActUserByLoginName(sysUser.getUserAccount());
        if (resUser == null) {
            resUser = identityService.newUser(sysUser.getUserAccount());
        }
        resUser.setEmail(sysUser.getMailAddress());
        resUser.setPassword(sysUser.getPassword());
        return resUser;
    }

用户组新增

   /**
     * @Description: 添加用户组
     * @Author: fg
     * @Date: 2019/8/8
     */
    public void addGroup(String groupId, String groupName) {
        if (StringUtils.isEmpty(groupId)) {
            throw new BusinessException("ID不可为空");
        }
        if (StringUtils.isEmpty(groupName)) {
            throw new BusinessException("组名不可为空");
        }
        Group group = identityService.newGroup(groupId);
        group.setName(groupName);
        identityService.saveGroup(group);
    }

添加用户至用户组:

    /**
     * @Description: 添加组织下的用户
     * @Author: fg
     * @Date: 2019/8/9
     */
    public void addGroupUser(String groupId, String userAccount) {
        if (StringUtils.isEmpty(groupId)) {
            throw new BusinessException("groupId不可为空");
        }
        if (StringUtils.isEmpty(userAccount)) {
            throw new BusinessException("用户登录名不可为空");
        }
        User u = identityService.createUserQuery()
                .userId(userAccount)
                .memberOfGroup(groupId)
                .singleResult();
        if (u != null) {
            throw new BusinessException("当前用户已经在当前组中");
        }
        /* 关联用户和用户组 */
        identityService.createMembership(userAccount, groupId);
    }

4. 如何退回
我们这里使用了互斥网关来解决“中国式”的退回
参考上面的流程图

        
.....
        
            
        

关键点在于这个地方
执行退回

Map map = new HashMap<>();
map.put("action","reject");
taskService.complete(task.getId(), map);

这样就会根据网关中的条件自动走 驳回的线路。

5. 如何转办
转办就很简单了

taskService.setAssignee(taskId,userId); 

6. 审批权限校验
大致想要的效果是 在执行每个节点时,会去校验当前用户是否有当前节点的处理权限。
从处理人那里其实分为两类:
一类是用户组,要根据用户组去查询用户
一类是办理人,直接匹配用户。
确定了思路,下面就简单了

    /**
     * @Description: 校验当前用户是否有权限去审批当前节点
     * @Author: fg
     * @Date: 2019/8/14
     */
    private void validateAuthority(String userAccount, Task task) {
        if (StringUtils.isNotEmpty(task.getAssignee())) {//如果是指定了审批人去审批
            if (!userAccount.equals(task.getAssignee())) {//当前节点的审批人和即将要执行审批的审批人不一致
                throw new BusinessException("当前节点审批人" + userAccount + "无权限审批![" + task.getAssignee() + "]");
            }
        } else {
            List list = getThisNodeByInsId(task.getProcessInstanceId());
            for (FlowElement f : list) {//遍历所有节点
                List listGroup = ((UserTask) f).getCandidateGroups();
                if (listGroup.size() == 0) {
                    return;
                }
                for (String group : listGroup) {//遍历所有组
                    List listUser = identityService.createUserQuery().memberOfGroup(group).list();
                    for (User u : listUser) {
                        if (u.getId().equals(userAccount)) {//匹配上了
                            return;
                        }
                    }
                }
            }
            throw new BusinessException("无权限审批");
        }
    }

然后在执行任务下一步/退回/转办时,执行下这个方法即可(注意控制好事务,BusinessException是我自定义的异常,可自行修改)

7. 提交时选环节选人
这个就比较麻烦了,由于ACTIVITI原生是不支持的,所以就需要自己实现。
为了验证改造测试,我们更改了流程文件的模型。spring boot 2+activiti 6 与应用用户整合,提交时手工选环节、选人,退回,转办,审批权限校验_第2张图片
对应的XML如下



  
    
    
    
    
      
        
          
    
    
      
        
      
    
    
    
    
    
      
    
    
      
    
    
    
    
    
      
    
    
    
      
    
    
    
      
    
    
      
    
    
  
  
    
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
        
      
      
        
        
      
      
        
        
        
      
      
        
        
        
      
      
        
        
      
      
        
        
        
          
        
      
      
        
        
        
          
        
      
      
        
        
      
      
        
        
        
        
        
          
        
      
      
        
        
        
        
        
          
        
      
      
        
        
      
    
  

思路如下:

  • 提交时,我们需要去查询出当前流程的下一节点(可能会是多个)。
  • 根据这些节点,查询出每个节点的审批人员。
  • 把上面的数据返回给前台,选择不同的流程,展示出不同的审批人。
  • 提交审批时要把审批人环节都放到流程参数里。
  • 根据互斥网关+提交的审批人和环节会自动走固定的环节。

于是代码就有了:
首先自定义一个用于返回前台审批环节和审批人的对象

/**
 * @author fg
 * @description: 用于流程中选择审批环节和人的vo
 * @date 2019/8/1411:45
 */
public class ProcessChooser {
    private List users;
    private String processKey;
    private String processName;

    public List getUsers() {
        return users;
    }

    public void setUsers(List users) {
        this.users = users;
    }

    public String getProcessKey() {
        return processKey;
    }

    public void setProcessKey(String processKey) {
        this.processKey = processKey;
    }

    public String getProcessName() {
        return processName;
    }

    public void setProcessName(String processName) {
        this.processName = processName;
    }
}

然后构造一个根据流程实例ID 去获取下个流程节点的方法:

   /**
     * @Description: 根据insId 获取下一个可能出现的节点
     * @Author: fg
     * @Date: 2019/8/14
     */
    public List getNextNodeByInsId(String insId,String taskKey) {
        List list = new ArrayList<>();
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
                .processInstanceId(insId)
                .singleResult();
        Task task = taskService.createTaskQuery()
                .processInstanceId(insId)
                .singleResult();//获取到执行中的task
        Map params = taskService.getVariables(task.getId());//获取当前task中的参数
        //当前活动节点
        List currentActs = runtimeService.getActiveActivityIds(processInstance.getId());
        if (currentActs.size() != 1) {
            throw new BusinessException("当前节点不支持选人!");
        }
        String activitiId = currentActs.get(0);//默认第一个节点  除了会签 应该不会出现这种情况
        currentActs.forEach(n -> {
            System.out.println("当前活动节点是【" + n + "】");
        });
        //pmmnModel 遍历节点需要它
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());
        List processList = bpmnModel.getProcesses();
        //循环多个物理流程
        for (Process process : processList) {//返回该流程的所有任务,事件
            Collection cColl = process.getFlowElements();
            //遍历节点
            for (FlowElement f : cColl) {//如果该节点是当前节点  输出该节点的下一个节点
                if (!f.getId().equals(activitiId)) {//表名是当前的节点
                    continue;
                }
                //通过反射来判断是哪种类型
                if (f instanceof org.activiti.bpmn.model.UserTask) {
                    List sequenceFlowList = ((org.activiti.bpmn.model.UserTask) f).getOutgoingFlows();
                    for (SequenceFlow sf : sequenceFlowList) {
                        String targetRef = sf.getTargetRef();
                        FlowElement ref = process.getFlowElement(targetRef);
                        if (ref instanceof org.activiti.bpmn.model.ExclusiveGateway) {//如果是网关  则查询网关后的节点
                            ExclusiveGateway gateway = ((org.activiti.bpmn.model.ExclusiveGateway) ref);
                            List tmpList = gateway.getOutgoingFlows();//网关后面的连线
                            for (SequenceFlow sf2 : tmpList) {//遍历这些连线 并查询到相关的userTask
                                if(StringUtils.isEmpty(sf2.getConditionExpression())){//SequenceFlow上如果没有conditionExpression 就不去关注
                                    continue;
                                }
                                if(sf2.getConditionExpression().contains("taskKey") && StringUtils.isEmpty(taskKey)){//当前节点是选择的节点  不做处理
                                    String targetRef2 = sf2.getTargetRef();
                                    FlowElement ref2 = process.getFlowElement(targetRef2);
                                    list.add(ref2);
                                }else if(!StringUtils.isEmpty(taskKey)){//根据输入条件的
                                    Map tmpMap = new HashMap<>();
                                    tmpMap.put("taskKey",taskKey);
                                    Serializable compiled =MVEL.compileExpression(sf2.getConditionExpression().replace("${","").replace("}",""));
                                    Boolean result = MVEL.executeExpression(compiled, tmpMap, Boolean.class);//判断是否会走当前节点
                                    if(result){//符合条件
                                        String targetRef2 = sf2.getTargetRef();
                                        FlowElement ref2 = process.getFlowElement(targetRef2);
                                        list.add(ref2);
                                    }
                                }else{
                                    Serializable compiled = MVEL.compileExpression(sf2.getConditionExpression().replace("${","").replace("}",""));
                                    Boolean result = MVEL.executeExpression(compiled, params, Boolean.class);//判断是否会走当前节点
                                    if(result){//符合条件
                                        String targetRef2 = sf2.getTargetRef();
                                        FlowElement ref2 = process.getFlowElement(targetRef2);
                                        list.add(ref2);
                                    }
                                }

                            }
                        } else {//货真价实的 userTask
                            list.add(ref);
                        }
                    }
                } else if (f instanceof org.activiti.bpmn.model.SequenceFlow) {//SequenceFlow --暂时用不到
                } else if (f instanceof org.activiti.bpmn.model.EndEvent) {//结束节点不做处理 --暂时用不到
                } else if (f instanceof org.activiti.bpmn.model.ExclusiveGateway) {//路由节点 --暂时用不到
                } else if (f instanceof org.activiti.bpmn.model.StartEvent) {//开始节点 --暂时用不到
                }
                break;
            }
        }
        return list;
    }
        /**
     * @Description: 根据insId获取下一个节点的审批人
     * @Author: fg
     * @Date: 2019/8/14
     */
    public List getNextTaskUserByInsId(String insId,String taskKey) {
        List list = new ArrayList<>();
        List fList = getNextNodeByInsId(insId,taskKey);
        for (FlowElement u : fList) {
            List groups = ((org.activiti.bpmn.model.UserTask) u).getCandidateGroups();
            for (String n : groups) {
                List listUser = identityService.createUserQuery().memberOfGroup(n).list();
                for (User m : listUser) {
                    list.add(m.getId());
                }
            }
        }
        return list;
    }

这里有几点需要解释:
1,思路是通过instanceId 去获取到当前执行的usertask,然后找到后面的连线(SequenceFlow),再根据连线找到后面的元素X,这里的元素X如果是个usertask 就直接返回,如果是个网关,就把当前流程的参数带入EL表达式,来判断满足条件。把不符合条件的给过滤掉,再查询符合条件的SequenceFlow所连接的userTask,最后根据userTask的参数来确定可选的用户(组)。
2,这个方法不适用与会签,如果有需要,则自行更改
3,仔细阅读下1,可能还需要增加逻辑。


最后封装出用于返回给前台的数据(这里的headId 不用管 实际上就是我业务数据的ID和instanceId是一对一的关系)

    public List queryProcessChooserByHeadId(Integer headId) {
        List resList = new ArrayList<>();
        List fowList = queryNextElementByHeadId(headId,null);
        for(FlowElement ele:fowList){
            ProcessChooser chooser = new ProcessChooser();
            chooser.setProcessKey(ele.getId());
            chooser.setProcessName(ele.getName());
            chooser.setUsers(userService.selectListUserByList(queryApproverByHeadId(headId,ele.getId())));
            resList.add(chooser);
        }
        return resList;
    }

提交时就好处理了

        /* 放置参数 */
        Map map = new HashMap<>();
        map.put("action",actProcessActionEnum.getCode());//执行操作  是下一步还是退回
        if(StringUtils.isNotEmpty(taskKey)){
            map.put("taskKey",taskKey);//选择下个环节
        }
        if(StringUtils.isNotEmpty(userAccount)){//userAccount 指定由谁来审批
            map.put("userAccount",userAccount);//选择下一环节由谁来审批
        }

然后把这个参数放到流程里就可以了

最后的最后 指定一个listener

    
      
        
          
    
    
      
        
      
    
/**
 * @author fg
 * @description: 用于选人的任务选择器
 * @date 2019/8/1414:47
 */
public class ApproverChosserTaskListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        Map map = delegateTask.getVariables();
        if(map.get("userAccount") !=null && StringUtils.isNotEmpty(map.get("userAccount").toString())){
            delegateTask.setAssignee(map.get("userAccount").toString());
        }
    }
}

这样就实现了选人,有什么疑问的可以加我的QQ 116475939一起探讨。

你可能感兴趣的:(java后台,框架,ACTIVITI,JAVA,SPRINGBOOT,工作流)