@[toc]
明天松哥和小伙伴们介绍一下 Spring Security 中另外一个好玩的会签性能。
会签的意思就是,在一个流程中的某一个 Task 上,这个 Task 须要多个用户审批,当多个用户全副审批通过,或者多个用户中的某几个用户审批通过,就算通过。这就是咱们说的 Flowable 中的会签性能!
例如咱们之前的销假流程,假如这个销假流程须要组长和经理都审批了,才算审批通过,那么咱们就须要设置这个 Task 是会签节点。
以咱们之前的销假流程为例,我和大家演示一下咱们这次要实现的成果。
- 首先员工提交销假申请,能够提交给多个审批人:
- 提交胜利之后,员工的历史销假列表中,能够看到刚刚提交的销假申请,然而抉择的三个审批人都是灰色的,示意三个人都还没有审批。
- 接下来,以 javaboy 的身份登录到零碎中,就能够看到刚刚用户提交的销假申请,而后进行审批。
- 审批实现后,以 zhangsan 的身份登录到零碎中,就能够看到 javaboy 曾经实现审批了,等三个人都实现审批之后,这个销假流程的状态也就会变成已通过,要是三个人中有一个人点击了回绝,那么这个销假流程的状态就会变为已回绝。
好啦,这就是咱们本文要实现的一个性能。本文也是基于之前的文章实现,如果小伙伴们还没看过松哥之前发的对于 Flowable 流程引擎的文章,能够在公众号江南一点雨上先翻一下。
1. 会签流程图
首先咱们来画一下这个销假流程图,这个流程图基本上还是和之前的一样,如下图:
这跟咱们之前的流程图有两个不一样的中央:
- 首先就是最最外围的的这个批准或者回绝的节点,这个节点上面多个三个竖线,这三个竖线的意思就是多个用户审批时是并发执行的,相互之间没有先后顺序,还有一种是三个横线,三个横线的意思是多个用户程序执行。当然,这里不是说流程图上多三个竖线就行了,还须要略微配置一下,如下:
这里配置的属性次要有五个:
- 多实例类型:这个选项次要有两个,别离是 Parallel 和 Sequential,示意并发执行还是程序执行,抉择是 Parallel 就是多个用户并发执行,相互之间没有先后顺序,抉择 Sequential 则是程序执行,多个用户之间有先后顺序。
- 汇合(多实例):这个中央我配置了一个 ${userTasks},这个示意当流程执行到这个节点的时候,我会传进来一个变量,这个变量的名字是 userTasks,这个变量中蕴含了所有要审批这个 Task 的用户名。
- 元素变量(多实例):因为下面的是一个汇合,这里配置的则是汇合中每一个元素的变量名,这就相似于 Java 里加强 for 循环的变量名。
- 实现条件(多实例):这里我配置的值是
${nrOfCompletedInstances== nrOfInstances}
,波及到两个变量,nrOfCompletedInstances 这个示意曾经实现审批的实例个数,nrOfInstances 则示意总共的实例个数,也就是当实现审批的实例个数等于总的实例个数的时候,这个节点就算执行完了,换句话说,也就是 zhangsan 将销假申请提交给 javaboy 和 lisi,必须这两个人都审批了,这个节点才算执行完。另外这里还有一个内置的变量可用就是 nrOfActiveInstances 示意未实现审批的实例个数,只不过在本案例中没有用到这个内置变量。 - 调配用户:这个是说这个 Task 的执行人,当然就是咱们后面配置的 userTask,也就是从汇合中拿进去的每一个元素的变量名。
- 去掉了审批通过之后的 UserTask。
在之前的销假流程图中,当销假审批通过之后,发送了销假通过告诉之后,还会进入到一个 UserTask 流程中,这里为了不便,我把这个流程删掉了。
好啦,这就是新流程图和以前旧流程图之间的一个区别,当初咱们来看下这个流程图对应的 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: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"><![CDATA[false]]></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"><![CDATA[${!approved}]]></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"><![CDATA[${approved}]]></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>
这个流程图也没有啥特地值得说的中央,基本上后面该说的都说了,小伙伴们能够自行联合流程图比照看下这个 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>
跟之前不同的是,这里的下拉框是多选的,当用户提交销假申请的时候,能够抉择多个审批人,多个审批人的值将保留在 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); });},
这个办法其实没啥好说的,惟一须要和小伙伴们强调的是申请的参数,来看下:
咱们来看下我这里提交的三个申请参数:
- approveUsers:这是审批以后流程的三个用户,当这三个用户都审批通过后,销假流程就通过了。
- days:这是销假的天数。
- reason:这是销假理由。
2.2 服务端解决销假申请
咱们再来看看服务端如何解决这个销假申请,我这里跟大家展现最外围的流程解决代码,文末能够下载残缺代码。
@Transactionalpublic 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("提交申请失败");}
能够看到,从前端一共传递过去三个参数,然而执行这个流程须要四个参数,其中一个 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);}
这个整体上分了两步:
- 首先查问进去以后用户所有待审批的 Task。
- 查问进去这些 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("操作失败");}
批准或者回绝,最次要的参数就是 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);}
这段代码比拟长,然而比拟像。整体上分为两局部,后面是查问未执行完的流程,前面是查问曾经执行结束的流程。对于未执行完的流程,咱们在 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>
大家看到,在审批人这个字段中,咱们先去遍历显示这个流程所有的审批人(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 能够下载本文残缺案例~