• 请假要组长和经理同时审批该怎么办?来看看工作流中的会签功能



    今天松哥和小伙伴们介绍一下 Spring Security 中另外一个好玩的会签功能。

    会签的意思就是,在一个流程中的某一个 Task 上,这个 Task 需要多个用户审批,当多个用户全部审批通过,或者多个用户中的某几个用户审批通过,就算通过。这就是我们说的 Flowable 中的会签功能!

    例如我们之前的请假流程,假设这个请假流程需要组长和经理都审批了,才算审批通过,那么我们就需要设置这个 Task 是会签节点。

    以我们之前的请假流程为例,我和大家演示一下我们这次要实现的效果。

    1. 首先员工提交请假申请,可以提交给多个审批人:

    1. 提交成功之后,员工的历史请假列表中,可以看到刚刚提交的请假申请,但是选择的三个审批人都是灰色的,表示三个人都还没有审批。

    1. 接下来,以 javaboy 的身份登录到系统中,就可以看到刚刚用户提交的请假申请,然后进行审批。

    1. 审批完成后,以 zhangsan 的身份登录到系统中,就可以看到 javaboy 已经完成审批了,等三个人都完成审批之后,这个请假流程的状态也就会变成已通过,要是三个人中有一个人点击了拒绝,那么这个请假流程的状态就会变为已拒绝。

    好啦,这就是我们本文要实现的一个功能。本文也是基于之前的文章完成,如果小伙伴们还没看过松哥之前发的关于 Flowable 流程引擎的文章,可以在公众号江南一点雨上先翻一下。

    1. 会签流程图

    首先我们来画一下这个请假流程图,这个流程图基本上还是和之前的一样,如下图:

    这跟我们之前的流程图有两个不一样的地方:

    1. 首先就是最最核心的的这个批准或者拒绝的节点,这个节点下面多个三个竖线,这三个竖线的意思就是多个用户审批时是并发执行的,相互之间没有先后顺序,还有一种是三个横线,三个横线的意思是多个用户顺序执行。当然,这里不是说流程图上多三个竖线就行了,还需要稍微配置一下,如下:

    这里配置的属性主要有五个:

    • 多实例类型:这个选项主要有两个,分别是 Parallel 和 Sequential,表示并发执行还是顺序执行,选择是 Parallel 就是多个用户并发执行,相互之间没有先后顺序,选择 Sequential 则是顺序执行,多个用户之间有先后顺序。
    • 集合(多实例):这个地方我配置了一个 ${userTasks},这个表示当流程执行到这个节点的时候,我会传进来一个变量,这个变量的名字是 userTasks,这个变量中包含了所有要审批这个 Task 的用户名。
    • 元素变量(多实例):由于上面的是一个集合,这里配置的则是集合中每一个元素的变量名,这就类似于 Java 里增强 for 循环的变量名。
    • 完成条件(多实例):这里我配置的值是 ${nrOfCompletedInstances== nrOfInstances},涉及到两个变量,nrOfCompletedInstances 这个表示已经完成审批的实例个数,nrOfInstances 则表示总共的实例个数,也就是当完成审批的实例个数等于总的实例个数的时候,这个节点就算执行完了,换句话说,也就是 zhangsan 将请假申请提交给 javaboy 和 lisi,必须这两个人都审批了,这个节点才算执行完。另外这里还有一个内置的变量可用就是 nrOfActiveInstances 表示未完成审批的实例个数,只不过在本案例中没有用到这个内置变量。
    • 分配用户:这个是说这个 Task 的执行人,当然就是我们前面配置的 userTask,也就是从集合中拿出来的每一个元素的变量名。
    1. 去掉了审批通过之后的 UserTask。

    在之前的请假流程图中,当请假审批通过之后,发送了请假通过通知之后,还会进入到一个 UserTask 流程中,这里为了方便,我把这个流程删掉了。

    好啦,这就是新流程图和以前旧流程图之间的一个区别,现在我们来看下这个流程图对应的 XML 文件:

    
    <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:flowable="http://flowable.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.flowable.org/processdef" exporter="Flowable Open Source Modeler" exporterVersion="6.7.2">
      <process id="holidayRequest" name="holidayRequest" isExecutable="true">
        <startEvent id="startEvent1" flowable:formFieldValidation="true">startEvent>
        <userTask id="sid-1A1AA050-1900-4CAD-A277-18BD97BD61FB" flowable:assignee="${userTask}" flowable:formFieldValidation="true">
          <extensionElements>
            <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler">modeler:initiator-can-complete>
          extensionElements>
          <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="${userTasks}" flowable:elementVariable="userTask">
            <extensionElements>extensionElements>
            <completionCondition>${nrOfCompletedInstances == nrOfInstances}completionCondition>
          multiInstanceLoopCharacteristics>
        userTask>
        <sequenceFlow id="sid-2597F958-175E-4F9F-9BEA-6E89D6C5B0A4" sourceRef="startEvent1" targetRef="sid-1A1AA050-1900-4CAD-A277-18BD97BD61FB">sequenceFlow>
        <exclusiveGateway id="sid-A04AF65B-D8B2-4F30-BFD5-7F2C9FCEFA51">exclusiveGateway>
        <sequenceFlow id="sid-7CD68B1D-C2CE-4A1A-ABA7-216D0F80BDD8" sourceRef="sid-1A1AA050-1900-4CAD-A277-18BD97BD61FB" targetRef="sid-A04AF65B-D8B2-4F30-BFD5-7F2C9FCEFA51">sequenceFlow>
        <serviceTask id="sid-4412386C-15E9-40C6-AB6B-66919A8D1302" flowable:class="org.javaboy.flowable03.flowable.Approve">serviceTask>
        <serviceTask id="sid-903B79F3-2020-419E-AD42-215C2E26C784" flowable:class="org.javaboy.flowable03.flowable.Reject">serviceTask>
        <endEvent id="sid-FE27FA28-2B2F-4572-A2D6-BFE83EBA9370">endEvent>
        <sequenceFlow id="sid-474E5177-9B1A-4757-877F-5A0DA72B0A59" sourceRef="sid-903B79F3-2020-419E-AD42-215C2E26C784" targetRef="sid-FE27FA28-2B2F-4572-A2D6-BFE83EBA9370">sequenceFlow>
        <sequenceFlow id="sid-85E7B515-734C-4E46-9889-D74FC5A04891" sourceRef="sid-A04AF65B-D8B2-4F30-BFD5-7F2C9FCEFA51" targetRef="sid-903B79F3-2020-419E-AD42-215C2E26C784">
          <conditionExpression xsi:type="tFormalExpression">conditionExpression>
        sequenceFlow>
        <sequenceFlow id="sid-EEC3F695-D61D-40BC-BA68-BCDD4DA40299" sourceRef="sid-A04AF65B-D8B2-4F30-BFD5-7F2C9FCEFA51" targetRef="sid-4412386C-15E9-40C6-AB6B-66919A8D1302">
          <conditionExpression xsi:type="tFormalExpression">conditionExpression>
        sequenceFlow>
        <endEvent id="sid-92DA7300-EFD1-437B-96D5-EAADAFABC507">endEvent>
        <sequenceFlow id="sid-E882A39F-9E88-4BB8-B7CE-F975B6ADC862" sourceRef="sid-4412386C-15E9-40C6-AB6B-66919A8D1302" targetRef="sid-92DA7300-EFD1-437B-96D5-EAADAFABC507">sequenceFlow>
      process>
    definitions>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    这个流程图也没有啥特别值得说的地方,基本上前面该说的都说了,小伙伴们可以自行结合流程图对比看下这个 XML 文件。

    2. 请假处理

    2.1 前端提交请假流程

    接下来我们看下前端如何提交请假申请:

    先来看页面:

    对应的 HTML 如下:

    <h1>提交请假申请h1>
    <table>
        <tr>
            <td>请输入请假天数:td>
            <td>
                <el-input type="text" v-model="afl.days"/>
            td>
        tr>
        <tr>
            <td>请输入请假理由:td>
            <td>
                <el-input type="text" v-model="afl.reason"/>
            td>
        tr>
        <tr>
            <td>审批人:td>
            <td>
                <el-select v-model="afl.approveUsers" style="width: 226px" placeholder="请选择审批人" multiple>
                    <el-option
                            v-for="item in users"
                            :key="item.id"
                            :label="item.username"
                            :value="item.username"/>
                el-select>
            td>
        tr>
    table>
    <el-button type="primary" @click="submit">提交请假申请el-button>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    跟之前不同的是,这里的下拉框是多选的,当用户提交请假申请的时候,可以选择多个审批人,多个审批人的值将保存在 afl.approveUsers 变量中。

    再来看提交请假方法:

    submit() {
        let _this = this;
        axios.post('/ask_for_leave', this.afl)
            .then(function (response) {
                if (response.data.status == 200) {
                    //提交成功
                    _this.$message.success(response.data.msg);
                    _this.search();
                } else {
                    //提交失败
                    _this.$message.error(response.data.msg);
                }
            })
            .catch(function (error) {
                console.log(error);
            });
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这个方法其实没啥好说的,唯一需要和小伙伴们强调的是请求的参数,来看下:

    我们来看下我这里提交的三个请求参数:

    1. approveUsers:这是审批当前流程的三个用户,当这三个用户都审批通过后,请假流程就通过了。
    2. days:这是请假的天数。
    3. reason:这是请假理由。

    2.2 服务端处理请假请求

    我们再来看看服务端如何处理这个请假请求,我这里跟大家展示最核心的流程处理代码,文末可以下载完整代码。

    @Transactional
    public RespBean askForLeave(AskForLeaveVO askForLeaveVO) {
        Map<String, Object> variables = new HashMap<>();
        askForLeaveVO.setName(SecurityContextHolder.getContext().getAuthentication().getName());
        variables.put("name", askForLeaveVO.getName());
        variables.put("days", askForLeaveVO.getDays());
        variables.put("reason", askForLeaveVO.getReason());
        variables.put("userTasks", askForLeaveVO.getApproveUsers());
        try {
            runtimeService.startProcessInstanceByKey("holidayRequest", askForLeaveVO.getName(), variables);
            return RespBean.ok("已提交请假申请");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return RespBean.error("提交申请失败");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以看到,从前端一共传递过来三个参数,但是执行这个流程需要四个参数,其中一个 name 表示当前登录的用户名,也就是这个请假是谁发起的。另外三个参数就是前端传来的参数。

    2.3 服务端返回待审批数据

    接下来我们来看看服务端如何返回待审批数据,也就是下面这张图要展示的数据:

    /**
     * 待审批列表
     *
     * @return
     */
    public RespBean leaveList() {
        String identity = SecurityContextHolder.getContext().getAuthentication().getName();
        //找到所有分配给你的任务
        List<Task> tasks = taskService.createTaskQuery().taskAssignee(identity).list();
        //重新组装返回的数据,为每个流程增加任务 id,方便后续执行批准或者拒绝操作
        List<Map<String, Object>> list = new ArrayList<>();
        for (int i = 0; i < tasks.size(); i++) {
            Task task = tasks.get(i);
            Map<String, Object> variables = taskService.getVariables(task.getId());
            variables.put("id", task.getId());
            list.add(variables);
        }
        return RespBean.ok("加载成功", list);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这个整体上分了两步:

    1. 首先查询出来当前用户所有待审批的 Task。
    2. 查询出来这些 Task 上的 variables,这是一个 Map 集合,然后我们再手动加上 id 这个参数。

    最后将组装好的 list 弄成一个 JSON 返回即可。

    2.4 服务端批准 OR 拒绝

    我们再来看看服务端批准或者拒绝请假流程的代码:

    public RespBean askForLeaveHandler(ApproveRejectVO approveRejectVO) {
        try {
            Task task = taskService.createTaskQuery().taskId(approveRejectVO.getTaskId()).singleResult();
            boolean approved = approveRejectVO.getApprove();
            Map<String, Object> variables = new HashMap<String, Object>();
            variables.put("approved", approved);
            variables.put("approveUser#" + task.getAssignee(), SecurityContextHolder.getContext().getAuthentication().getName());
            taskService.complete(task.getId(), variables);
            return RespBean.ok("操作成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return RespBean.error("操作失败");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    批准或者拒绝,最主要的参数就是 approved,true 表示批准,false 表示拒绝。

    另一方面,由于现在是会签,我们需要知道目前谁已经审批了,谁还没审批,所以这里额外多加了一个参数 approveUser#XXX,表示审批这个节点的用户名(也就是当前登录用户)。

    注意这个参数的 key 我没有固定,主要是因为这个节点会有多个人审批,如果固定的话,后面审批的人会覆盖掉前面的人,所以这个节点的 key 设置成动态的了,approveUser# 后面加上处理这个节点的用户名。

    2.5 服务端返回流程数据

    最后还有服务端展示流程数据。就是当用户提交流程之后,想要查看自己的流程处理到哪一步了,也就是下图中的数据:

    这张图中的数据其实包含了两部分,一部分是已经执行完的流程,还有一部分是正在执行中的流程,所以在查询中,我们也得分为两步来完成,如下:

    public RespBean searchResult() {
        String name = SecurityContextHolder.getContext().getAuthentication().getName();
        List<HistoryInfo> infos = new ArrayList<>();
        //未完成流程
        List<HistoricProcessInstance> unFinishedHistoricProcessInstances = historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(name).unfinished().orderByProcessInstanceStartTime().desc().list();
        for (HistoricProcessInstance unFinishedHistoricProcessInstance : unFinishedHistoricProcessInstances) {
            HistoryInfo historyInfo = new HistoryInfo();
            Date startTime = unFinishedHistoricProcessInstance.getStartTime();
            Date endTime = unFinishedHistoricProcessInstance.getEndTime();
            List<HistoricVariableInstance> historicVariableInstances = historyService.createHistoricVariableInstanceQuery()
                    .processInstanceId(unFinishedHistoricProcessInstance.getId())
                    .list();
            System.out.println("historicVariableInstances = " + historicVariableInstances);
            for (HistoricVariableInstance historicVariableInstance : historicVariableInstances) {
                String variableName = historicVariableInstance.getVariableName();
                Object value = historicVariableInstance.getValue();
                if ("reason".equals(variableName)) {
                    historyInfo.setReason((String) value);
                } else if ("days".equals(variableName)) {
                    historyInfo.setDays(Integer.parseInt(value.toString()));
                } else if ("name".equals(variableName)) {
                    historyInfo.setName((String) value);
                } else if (variableName.startsWith("approveUser")) {
                    historyInfo.getApproveUsers().add((String) value);
                } else if ("userTask".equals(variableName)) {
                    historyInfo.getCandidateUsers().add((String) value);
                }
            }
            historyInfo.setStatus(3);
            historyInfo.setStartTime(startTime);
            historyInfo.setEndTime(endTime);
            infos.add(historyInfo);
        }
        //已结束流程
        List<HistoricProcessInstance> finishHistoricProcessInstances = historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(name)
                .finished()
                .orderByProcessInstanceStartTime().desc().list();
        for (HistoricProcessInstance historicProcessInstance : finishHistoricProcessInstances) {
            HistoryInfo historyInfo = new HistoryInfo();
            Date startTime = historicProcessInstance.getStartTime();
            Date endTime = historicProcessInstance.getEndTime();
            List<HistoricVariableInstance> historicVariableInstances = historyService.createHistoricVariableInstanceQuery()
                    .processInstanceId(historicProcessInstance.getId())
                    .list();
            System.out.println(historicVariableInstances);
            for (HistoricVariableInstance historicVariableInstance : historicVariableInstances) {
                String variableName = historicVariableInstance.getVariableName();
                Object value = historicVariableInstance.getValue();
                if ("reason".equals(variableName)) {
                    historyInfo.setReason((String) value);
                } else if ("days".equals(variableName)) {
                    historyInfo.setDays(Integer.parseInt(value.toString()));
                } else if ("approved".equals(variableName)) {
                    Boolean v = (Boolean) value;
                    if (v) {
                        historyInfo.setStatus(1);
                    } else {
                        historyInfo.setStatus(2);
                    }
                } else if ("name".equals(variableName)) {
                    historyInfo.setName((String) value);
                } else if (variableName.startsWith("approveUser")) {
                    historyInfo.getApproveUsers().add((String) value);
                } else if ("userTask".equals(variableName)) {
                    historyInfo.getCandidateUsers().add((String) value);
                }
            }
            historyInfo.setStartTime(startTime);
            historyInfo.setEndTime(endTime);
            infos.add(historyInfo);
        }
        return RespBean.ok("ok", infos);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    这段代码比较长,但是比较像。整体上分为两部分,前面是查询未执行完的流程,后面是查询已经执行完毕的流程。对于未执行完的流程,我们在 historyInfo 中设置 status 为 3,表示待审批。当我们去读取一个流程的历史变量时,有一个以 approveUser 开头的变量,这个就表示这个流程已经被谁审批过了,我们将这个存到一个 List 集合中,将来返回给前端。流程的历史变量中还有一个 userTask,表示这个流程中这个节点待审批的用户都有谁,我们也将之保存到 List 集合中,将来返回给前端。

    2.6 前端渲染审批数据

    最后,我们再来看看前端如何渲染 2.5 小节返回的数据,如下:

    <div>
        <el-tag>历史请假列表el-tag>
        <el-table border strip :data="historyInfos">
            <el-table-column prop="name" label="姓名">el-table-column>
            <el-table-column prop="startTime" label="提交时间">el-table-column>
            <el-table-column prop="endTime" label="审批时间">el-table-column>
            <el-table-column prop="reason" label="事由">el-table-column>
            <el-table-column prop="days" label="天数">el-table-column>
            <el-table-column label="审批人">
                <template #default="scope">
                    <template v-for="(cu,i) in scope.row.candidateUsers" :key="i">
                        <el-tag v-if="scope.row.approveUsers.indexOf(cu)!=-1" type="success">
                            {{cu}}
                        el-tag>
                        <el-tag v-else style="color: gray">{{cu}}el-tag>
                    template>
                template>
            el-table-column>
            <el-table-column label="状态">
                <template #default="scope">
                    <el-tag type="success" v-if="scope.row.status==1">已通过el-tag>
                    <el-tag type="danger" v-else-if="scope.row.status==2">已拒绝el-tag>
                    <el-tag type="info" v-else>待审批el-tag>
                template>
            el-table-column>
        el-table>
    div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    大家看到,在审批人这个字段中,我们先去遍历显示这个流程所有的审批人(candidateUsers),在遍历的过程中,如果发现这个用户存在于 approveUsers 集合中,就表示这个用户已经审批,用绿色的 el-tag 显示,否则表示这个用户还没有审批,我们就用灰色的 el-tag 显示。

    好啦,这就可以啦!一个简简单单的会签功能就完成了,测试流程我就不演示了,小伙伴们参考本文一开始的内容~

    3. 或签

    说完了会签,再来和大家说一说或签。

    或签意思就是 A 的请假流程提交给 B、C、D,但是并不需要 B/C/D 同时审批通过,只需要 B/C/D 中的任意一个审批即可,这就是或签,注意,我这里的表述,只需要 B/C/D 任意一个审批即可,这个审批即可以是审批通过,也可以是审批拒绝,反正只要审批,这个 UserTask 就算完成了

    将会签改为或签其实非常容易,我们只需要修改一下 UserTask 的属性即可,和会签相比,我这里主要改了一个地方,都在下图中用箭头标出来了:

    完成条件(多实例)这里改为了 ${nrOfCompletedInstances >= 1},表示只要有一个同意或者拒绝,这个 UserTask 就算过了。

    改完之后,我们重新下载这个流程图的 XML 文件,并放到前文中的代码上去运行,就可以看到或签效果了,我就不演示了,小伙伴们可以自行尝试。

    本文也有配套视频,感兴趣的小伙伴戳这里查看视频详情:TienChin 项目配套视频来啦

    小伙伴们在公众号江南一点雨后台回复 flowable04 可以下载本文完整案例~

  • 相关阅读:
    对大文件压缩包分割和恢复的方法_python
    《Java编程思想》读书笔记(三)
    leetcode 45. 跳跃游戏 II(dp + 贪心)
    Golang练手算法
    Codeforces Round #771 (Div. 2) D. Big Brush
    从使用的角度看 ByConity 和 ClickHouse 的差异
    RabbitMQ 入门系列:2、基础含义理解:链接、通道、队列、交换机
    SXSSFWorkbook-MinIo-大数据-流式导出
    C++20:constexpr、consteval和constinit
    Lesson 02 类与对象 (终)
  • 原文地址:https://blog.csdn.net/u012702547/article/details/126764946