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即可
		<dependency>
			<groupId>org.activiti</groupId>
			<artifactId>activiti-spring-boot-starter-basic</artifactId>
			<version>6.0.0</version>
		</dependency>

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

		<dependency>
			<groupId>org.mvel</groupId>
			<artifactId>mvel2</artifactId>
			<version>2.2.1.Final</version>
		</dependency>

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

/resources/processes/

在这里插入图片描述
对应的xml文件为

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
    <process id="P026" name="营业厅电费稽核流程" isExecutable="true">
        <startEvent id="startevent1" name="开始"></startEvent>
        <endEvent id="endevent1" name="结束"></endEvent>
        <userTask id="STEP_10" name="报账员录入" activiti:assignee="#{applyUserAccount}"></userTask>
        <userTask id="STEP_20" name="稽核员审批" activiti:candidateGroups="电费稽核员"></userTask>
        <exclusiveGateway id="exclusivegateway1" name="审批"></exclusiveGateway>
        <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="STEP_10"></sequenceFlow>
        <sequenceFlow id="flow2" sourceRef="STEP_10" targetRef="STEP_20"></sequenceFlow>
        <sequenceFlow id="flow5" name="驳回" sourceRef="exclusivegateway1" targetRef="STEP_10">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[${action == "reject"}]]></conditionExpression>
        </sequenceFlow>
        <sequenceFlow id="flow6" sourceRef="STEP_20" targetRef="exclusivegateway1"></sequenceFlow>
        <userTask id="STEP_90" name="报账员归档" activiti:assignee="#{applyUserAccount}"></userTask>
        <sequenceFlow id="flow7" name="通过" sourceRef="exclusivegateway1" targetRef="STEP_90">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[${action == "next"}]]></conditionExpression>
        </sequenceFlow>
        <sequenceFlow id="flow8" sourceRef="STEP_90" targetRef="endevent1"></sequenceFlow>
    </process>
    <bpmndi:BPMNDiagram id="BPMNDiagram_P026">
        <bpmndi:BPMNPlane bpmnElement="P026" id="BPMNPlane_P026">
            <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
                <omgdc:Bounds height="35.0" width="35.0" x="40.0" y="210.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
                <omgdc:Bounds height="35.0" width="35.0" x="670.0" y="210.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="STEP_10" id="BPMNShape_STEP_10">
                <omgdc:Bounds height="55.0" width="105.0" x="150.0" y="200.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="STEP_20" id="BPMNShape_STEP_20">
                <omgdc:Bounds height="55.0" width="105.0" x="300.0" y="200.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="exclusivegateway1" id="BPMNShape_exclusivegateway1">
                <omgdc:Bounds height="40.0" width="40.0" x="450.0" y="207.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNShape bpmnElement="STEP_90" id="BPMNShape_STEP_90">
                <omgdc:Bounds height="55.0" width="105.0" x="530.0" y="200.0"></omgdc:Bounds>
            </bpmndi:BPMNShape>
            <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
                <omgdi:waypoint x="75.0" y="227.0"></omgdi:waypoint>
                <omgdi:waypoint x="150.0" y="227.0"></omgdi:waypoint>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
                <omgdi:waypoint x="255.0" y="227.0"></omgdi:waypoint>
                <omgdi:waypoint x="300.0" y="227.0"></omgdi:waypoint>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
                <omgdi:waypoint x="470.0" y="207.0"></omgdi:waypoint>
                <omgdi:waypoint x="470.0" y="131.0"></omgdi:waypoint>
                <omgdi:waypoint x="378.0" y="131.0"></omgdi:waypoint>
                <omgdi:waypoint x="202.0" y="131.0"></omgdi:waypoint>
                <omgdi:waypoint x="202.0" y="200.0"></omgdi:waypoint>
                <bpmndi:BPMNLabel>
                    <omgdc:Bounds height="14.0" width="24.0" x="472.0" y="195.0"></omgdc:Bounds>
                </bpmndi:BPMNLabel>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="flow6" id="BPMNEdge_flow6">
                <omgdi:waypoint x="405.0" y="227.0"></omgdi:waypoint>
                <omgdi:waypoint x="450.0" y="227.0"></omgdi:waypoint>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="flow7" id="BPMNEdge_flow7">
                <omgdi:waypoint x="490.0" y="227.0"></omgdi:waypoint>
                <omgdi:waypoint x="530.0" y="227.0"></omgdi:waypoint>
                <bpmndi:BPMNLabel>
                    <omgdc:Bounds height="14.0" width="24.0" x="490.0" y="227.0"></omgdc:Bounds>
                </bpmndi:BPMNLabel>
            </bpmndi:BPMNEdge>
            <bpmndi:BPMNEdge bpmnElement="flow8" id="BPMNEdge_flow8">
                <omgdi:waypoint x="635.0" y="227.0"></omgdi:waypoint>
                <omgdi:waypoint x="670.0" y="227.0"></omgdi:waypoint>
            </bpmndi:BPMNEdge>
        </bpmndi:BPMNPlane>
    </bpmndi:BPMNDiagram>
</definitions>

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

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. 如何退回
我们这里使用了互斥网关来解决“中国式”的退回
参考上面的流程图

        <userTask id="STEP_10" name="报账员录入" activiti:assignee="#{applyUserAccount}"></userTask>
.....
        <sequenceFlow id="flow5" name="驳回" sourceRef="exclusivegateway1" targetRef="STEP_10">
            <conditionExpression xsi:type="tFormalExpression"><![CDATA[${action == "reject"}]]></conditionExpression>
        </sequenceFlow>

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

Map<String,Object> 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<FlowElement> list = getThisNodeByInsId(task.getProcessInstanceId());
            for (FlowElement f : list) {//遍历所有节点
                List<String> listGroup = ((UserTask) f).getCandidateGroups();
                if (listGroup.size() == 0) {
                    return;
                }
                for (String group : listGroup) {//遍历所有组
                    List<User> listUser = identityService.createUserQuery().memberOfGroup(group).list();
                    for (User u : listUser) {
                        if (u.getId().equals(userAccount)) {//匹配上了
                            return;
                        }
                    }
                }
            }
            throw new BusinessException("无权限审批");
        }
    }

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

7. 提交时选环节选人
这个就比较麻烦了,由于ACTIVITI原生是不支持的,所以就需要自己实现。
为了验证改造测试,我们更改了流程文件的模型。在这里插入图片描述
对应的XML如下

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
  <process id="P026" name="营业厅电费稽核流程" isExecutable="true">
    <startEvent id="startevent1" name="开始"></startEvent>
    <endEvent id="endevent1" name="结束"></endEvent>
    <userTask id="STEP_10" name="报账员录入" activiti:assignee="#{applyUserAccount}"></userTask>
    <userTask id="STEP_21" name="审核组1" activiti:candidateGroups="电费稽核员">
      <extensionElements>
        <activiti:taskListener event="create" class="com.account.ibm.account.listener.ApproverChosserTaskListener"></activiti:taskListener>
      </extensionElements>    
    </userTask>
    <userTask id="STEP_22" name="审核组2" activiti:candidateGroups="电费稽核员">
      <extensionElements>
        <activiti:taskListener event="create" class="com.account.ibm.account.listener.ApproverChosserTaskListener"></activiti:taskListener>
      </extensionElements>
    </userTask>
    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="STEP_10"></sequenceFlow>
    <exclusiveGateway id="exclusivegateway1" name="Exclusive Gateway"></exclusiveGateway>
    <sequenceFlow id="flow3" sourceRef="STEP_10" targetRef="exclusivegateway1"></sequenceFlow>
    <sequenceFlow id="flow4" sourceRef="exclusivegateway1" targetRef="STEP_21">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${taskKey == 'STEP_21'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow5" sourceRef="exclusivegateway1" targetRef="STEP_22">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${taskKey == 'STEP_22'}]]></conditionExpression>
    </sequenceFlow>
    <userTask id="STEP_90" name="归档环节" activiti:assignee="#{applyUserAccount}"></userTask>
    <sequenceFlow id="flow8" sourceRef="STEP_90" targetRef="endevent1"></sequenceFlow>
    <exclusiveGateway id="exclusivegateway2" name="Exclusive Gateway"></exclusiveGateway>
    <sequenceFlow id="flow11" name="同意" sourceRef="exclusivegateway2" targetRef="STEP_90">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${action == 'next'}]]></conditionExpression>
    </sequenceFlow>
    <exclusiveGateway id="exclusivegateway3" name="Exclusive Gateway"></exclusiveGateway>
    <sequenceFlow id="flow12" name="同意" sourceRef="exclusivegateway3" targetRef="STEP_90">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${action == 'next'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow13" sourceRef="STEP_21" targetRef="exclusivegateway3"></sequenceFlow>
    <sequenceFlow id="flow14" name="退回" sourceRef="exclusivegateway2" targetRef="STEP_10">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${action == 'reject'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow15" name="退回" sourceRef="exclusivegateway3" targetRef="STEP_10">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${action == 'reject'}]]></conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow16" sourceRef="STEP_22" targetRef="exclusivegateway2"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_P026">
    <bpmndi:BPMNPlane bpmnElement="P026" id="BPMNPlane_P026">
      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="180.0" y="230.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="910.0" y="230.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="STEP_10" id="BPMNShape_STEP_10">
        <omgdc:Bounds height="55.0" width="105.0" x="280.0" y="220.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="STEP_21" id="BPMNShape_STEP_21">
        <omgdc:Bounds height="55.0" width="105.0" x="540.0" y="150.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="STEP_22" id="BPMNShape_STEP_22">
        <omgdc:Bounds height="55.0" width="105.0" x="540.0" y="290.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="exclusivegateway1" id="BPMNShape_exclusivegateway1">
        <omgdc:Bounds height="40.0" width="40.0" x="440.0" y="227.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="STEP_90" id="BPMNShape_STEP_90">
        <omgdc:Bounds height="55.0" width="105.0" x="710.0" y="220.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="exclusivegateway2" id="BPMNShape_exclusivegateway2">
        <omgdc:Bounds height="40.0" width="40.0" x="742.0" y="297.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="exclusivegateway3" id="BPMNShape_exclusivegateway3">
        <omgdc:Bounds height="40.0" width="40.0" x="742.0" y="157.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
        <omgdi:waypoint x="215.0" y="247.0"></omgdi:waypoint>
        <omgdi:waypoint x="280.0" y="247.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="385.0" y="247.0"></omgdi:waypoint>
        <omgdi:waypoint x="440.0" y="247.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
        <omgdi:waypoint x="460.0" y="227.0"></omgdi:waypoint>
        <omgdi:waypoint x="460.0" y="177.0"></omgdi:waypoint>
        <omgdi:waypoint x="540.0" y="177.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
        <omgdi:waypoint x="460.0" y="267.0"></omgdi:waypoint>
        <omgdi:waypoint x="460.0" y="317.0"></omgdi:waypoint>
        <omgdi:waypoint x="540.0" y="317.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow8" id="BPMNEdge_flow8">
        <omgdi:waypoint x="815.0" y="247.0"></omgdi:waypoint>
        <omgdi:waypoint x="910.0" y="247.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow11" id="BPMNEdge_flow11">
        <omgdi:waypoint x="762.0" y="297.0"></omgdi:waypoint>
        <omgdi:waypoint x="762.0" y="275.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="14.0" width="24.0" x="762.0" y="297.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow12" id="BPMNEdge_flow12">
        <omgdi:waypoint x="762.0" y="197.0"></omgdi:waypoint>
        <omgdi:waypoint x="762.0" y="220.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="14.0" width="24.0" x="762.0" y="197.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow13" id="BPMNEdge_flow13">
        <omgdi:waypoint x="645.0" y="177.0"></omgdi:waypoint>
        <omgdi:waypoint x="742.0" y="177.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow14" id="BPMNEdge_flow14">
        <omgdi:waypoint x="762.0" y="337.0"></omgdi:waypoint>
        <omgdi:waypoint x="761.0" y="379.0"></omgdi:waypoint>
        <omgdi:waypoint x="332.0" y="379.0"></omgdi:waypoint>
        <omgdi:waypoint x="332.0" y="275.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="14.0" width="24.0" x="762.0" y="337.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow15" id="BPMNEdge_flow15">
        <omgdi:waypoint x="762.0" y="157.0"></omgdi:waypoint>
        <omgdi:waypoint x="761.0" y="119.0"></omgdi:waypoint>
        <omgdi:waypoint x="332.0" y="119.0"></omgdi:waypoint>
        <omgdi:waypoint x="332.0" y="220.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="14.0" width="24.0" x="762.0" y="157.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow16" id="BPMNEdge_flow16">
        <omgdi:waypoint x="645.0" y="317.0"></omgdi:waypoint>
        <omgdi:waypoint x="742.0" y="317.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

思路如下:

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

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

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

    public List<User> getUsers() {
        return users;
    }

    public void setUsers(List<User> 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<FlowElement> getNextNodeByInsId(String insId,String taskKey) {
        List<FlowElement> list = new ArrayList<>();
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
                .processInstanceId(insId)
                .singleResult();
        Task task = taskService.createTaskQuery()
                .processInstanceId(insId)
                .singleResult();//获取到执行中的task
        Map<String, Object> params = taskService.getVariables(task.getId());//获取当前task中的参数
        //当前活动节点
        List<String> 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<Process> processList = bpmnModel.getProcesses();
        //循环多个物理流程
        for (Process process : processList) {//返回该流程的所有任务,事件
            Collection<FlowElement> cColl = process.getFlowElements();
            //遍历节点
            for (FlowElement f : cColl) {//如果该节点是当前节点  输出该节点的下一个节点
                if (!f.getId().equals(activitiId)) {//表名是当前的节点
                    continue;
                }
                //通过反射来判断是哪种类型
                if (f instanceof org.activiti.bpmn.model.UserTask) {
                    List<SequenceFlow> 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<SequenceFlow> 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<String, Object> 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<String> getNextTaskUserByInsId(String insId,String taskKey) {
        List<String> list = new ArrayList<>();
        List<FlowElement> fList = getNextNodeByInsId(insId,taskKey);
        for (FlowElement u : fList) {
            List<String> groups = ((org.activiti.bpmn.model.UserTask) u).getCandidateGroups();
            for (String n : groups) {
                List<User> 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<ProcessChooser> queryProcessChooserByHeadId(Integer headId) {
        List<ProcessChooser> resList = new ArrayList<>();
        List<FlowElement> 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<String,Object> 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

    <userTask id="STEP_21" name="审核组1" activiti:candidateGroups="电费稽核员">
      <extensionElements>
        <activiti:taskListener event="create" class="com.account.ibm.account.listener.ApproverChosserTaskListener"></activiti:taskListener>
      </extensionElements>    
    </userTask>
    <userTask id="STEP_22" name="审核组2" activiti:candidateGroups="电费稽核员">
      <extensionElements>
        <activiti:taskListener event="create" class="com.account.ibm.account.listener.ApproverChosserTaskListener"></activiti:taskListener>
      </extensionElements>
    </userTask>
/**
 * @author fg
 * @description: 用于选人的任务选择器
 * @date 2019/8/1414:47
 */
public class ApproverChosserTaskListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        Map<String,Object> map = delegateTask.getVariables();
        if(map.get("userAccount") !=null && StringUtils.isNotEmpty(map.get("userAccount").toString())){
            delegateTask.setAssignee(map.get("userAccount").toString());
        }
    }
}

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值