2

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

 1 year ago
source link: http://www.javaboy.org/2022/0908/flowable.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
3 个月前 24 分钟 读完 (大约 3603 个字)

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

[TOC]

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

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

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

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

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

20220902194011.png
  1. 提交成功之后,员工的历史请假列表中,可以看到刚刚提交的请假申请,但是选择的三个审批人都是灰色的,表示三个人都还没有审批。
  1. 接下来,以 javaboy 的身份登录到系统中,就可以看到刚刚用户提交的请假申请,然后进行审批。
  1. 审批完成后,以 zhangsan 的身份登录到系统中,就可以看到 javaboy 已经完成审批了,等三个人都完成审批之后,这个请假流程的状态也就会变成已通过,要是三个人中有一个人点击了拒绝,那么这个请假流程的状态就会变为已拒绝。

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

1. 会签流程图

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

20220902194824.png

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

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

20220902200335.png

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

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

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

先来看页面:

20220902194011.png

对应的 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);
});
},

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

20220902210820.png

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

  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("提交申请失败");
}

可以看到,从前端一共传递过来三个参数,但是执行这个流程需要四个参数,其中一个 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. 首先查询出来当前用户所有待审批的 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("操作失败");
}

批准或者拒绝,最主要的参数就是 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 显示。

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

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

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

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

20220903212658.png

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

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

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

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

喜欢这篇文章吗?扫码关注公众号【江南一点雨】【江南一点雨】专注于 SPRING BOOT+微服务以及前后端分离技术,每天推送原创技术干货,关注后回复 JAVA,领取松哥为你精心准备的 JAVA 干货!

javaboy.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK