Python 官方文档:入门教程 => 点击学习
目录1. 效果展示2. 工程创建3. 流程图分析4. 请假申请4.1 服务端接口4.2 前端页面5. 任务展示6. 请假审批7. 结果查询小伙伴们知道松哥最近在录 TienChin
小伙伴们知道松哥最近在录 TienChin 项目视频,这个项目会用到工作流,为了帮助小伙伴们更好的理解这个项目,松哥最近会出几篇文章和大伙聊一聊工作流 flowable 的使用,算是给 TienChin 项目的第一个铺垫,当然,在 TienChin 项目的系列视频中,我也会和大家详细聊一聊 flowable 流程引擎的使用。
今天我就先写一个简单的请假流程,让小伙伴们对 flowable 先有一个直观的认知。
在正式开搞之前,我先来给小伙伴们看下我们今天要完成的效果。
简单起见,我这里并没有引入用户、角色等概念,涉及到用户的地方都是手动输入,在后续的文章中我会继续结合 spring Security 来和大家展示引入用户之后的情况。
我们先来看看请假页面:
员工可以在这个页面输入姓名,请假天数以及请假理由等,然后点击按钮提交一个请假申请。
当员工提交请假申请之后,这个请假申请默认是由经理来处理的,此时经理登录之后,就可以看到员工提交上来的请求:
经理此时可以选择批准或者拒绝。无论是批准还是拒绝,都可以通过短信或者邮件等告知员工。
对于员工来说,也可以在一个页面查询自己请假流程的最终情况:
可能有小伙伴已经注意到了,我们这里所有涉及到用户名的地方,都需要手动输入。这是因为我为了让这个案例足够简单,暂时没有引入 Spring Security,只是单纯的和大家分享 Flowable 的用法,等小伙伴们通过这篇文章掌握了 Flowable 的基本用法之后,下篇文章我会和大家分享如何结合具体的用户来使用。
我就直接来和小伙伴们展示 Spring Boot 中 flowable 的用法了。
首先我们创建一个 Spring Boot 项目,创建的时候引入 WEB 和 Mysql 驱动依赖即可,项目创建成功之后,再引入 flowable 依赖,最终的依赖文件如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.7.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
项目创建成功之后,首先需要我们在 application.properties 中配置一下数据库连接信息,如下:
spring.datasource.username=root
spring.datasource.passWord=123
spring.datasource.url=jdbc:mysql:///flowable02?serverTimezone=Asia/Shanghai&useSSL=false&nullCatalogMeansCurrent=true
配置完成之后,当 Spring Boot 项目第一次启动的时候,会自动创建出来对应的表和需要的数据。
同时,Spring Boot 项目也会自动创建并暴露 Flowable 中的 ProcessEngine、CmmnEngine、DmnEngine、FORMEngine、ContentEngine 及 IdmEngine 等 Bean。
并且所有的 Flowable 服务都暴露为 Spring Bean。例如 RuntimeService、TaskService、HistoryService 等等服务,我们都可以在需要使用的时候,直接注入就可以使用了。
同时:
今天这个例子比较简单,就是一个请假流程,我暂时先不跟小伙伴们去扯画流程图的事,咱们直接用一个官网现成的请假流程图:
我们先来简单分析一下这张图:
这个流程图对应的 XML 文件位于 src/main/resources/processes/holiday-request.bpmn20.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:flowable="http://flowable.org/bpmn"
typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.flowable.org/processdef">
<process id="holidayRequest" name="Holiday Request" isExecutable="true">
<startEvent id="startEvent"/>
<sequenceFlow sourceRef="startEvent" targetRef="approveTask"/>
<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>
<sequenceFlow sourceRef="approveTask" targetRef="decision"/>
<exclusiveGateway id="decision"/>
<sequenceFlow sourceRef="decision" targetRef="externalSystemCall">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[
${approved}
]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="decision" targetRef="rejectLeave">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[
${!approved}
]]>
</conditionExpression>
</sequenceFlow>
<serviceTask id="externalSystemCall" name="Enter holidays in external system"
flowable:class="org.javaboy.flowable02.flowable.Approve"/>
<sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>
<userTask id="holidayApprovedTask" flowable:assignee="${employee}" name="Holiday approved"/>
<sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>
<serviceTask id="rejectLeave" name="Send out rejection email"
flowable:class="org.javaboy.flowable02.flowable.Reject"/>
<sequenceFlow sourceRef="rejectLeave" targetRef="rejectEnd"/>
<endEvent id="approveEnd"/>
<endEvent id="rejectEnd"/>
</process>
</definitions>
很多想学习流程引擎的小伙伴都会被这个 XML 文件劝退,但是!!!
如果你愿意静下心来认真阅读这个 XML 文件,你会发现流程引擎原来如此简单!
我们来挨个看下这里的每一个节点:
总而言之,只要小伙伴们静下心来认真阅读一下上面的 XML,你会发现 So Easy!
好了,接下来我们就来看一个具体的请假申请。由于请假流程只要放对位置,就会自动加载,所以我们并不需要手动加载请假流程,直接开始一个请假申请流程即可。
首先我们需要一个实体类来接受前端传来的请假参数:用户名、请假天数以及请假理由:
public class AskForLeaveVO {
private String name;
private Integer days;
private String reason;
// 省略 getter/setter
}
再拿出祖传的 RespBean,以便响应数据方便一些:
public class RespBean {
private Integer status;
private String msg;
private Object data;
public static RespBean ok(String msg, Object data) {
return new RespBean(200, msg, data);
}
public static RespBean ok(String msg) {
return new RespBean(200, msg, null);
}
public static RespBean error(String msg, Object data) {
return new RespBean(500, msg, data);
}
public static RespBean error(String msg) {
return new RespBean(500, msg, null);
}
private RespBean() {
}
private RespBean(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
// 省略 getter/setter
}
接下来我们提供一个处理请假申请的接口:
@RestController
public class AskForLeaveController {
@Autowired
AskForLeaveService askForLeaveService;
@PostMapping("/ask_for_leave")
public RespBean askForLeave(@RequestBody AskForLeaveVO askForLeaveVO) {
return askForLeaveService.askForLeave(askForLeaveVO);
}
}
核心逻辑在 AskForLeaveService 中,来继续看:
@Service
public class AskForLeaveService {
@Autowired
RuntimeService runtimeService;
@Transactional
public RespBean askForLeave(AskForLeaveVO askForLeaveVO) {
Map<String, Object> variables = new HashMap<>();
variables.put("name", askForLeaveVO.getName());
variables.put("days", askForLeaveVO.getDays());
variables.put("reason", askForLeaveVO.getReason());
try {
runtimeService.startProcessInstanceByKey("holidayRequest", askForLeaveVO.getName(), variables);
return RespBean.ok("已提交请假申请");
} catch (Exception e) {
e.printStackTrace();
}
return RespBean.error("提交申请失败");
}
}
小伙伴们看一下,在提交请假申请的时候,分别传入了 name、days 以及 reason 三个参数,我们将这三个参数放入到一个 Map 中,然后通过 RuntimeService#startProcessInstanceByKey 方法来开启一个流程,开启流程的时候一共传入了三个参数:
好了,这服务端就写好了。
接下来我们来开发前端页面。
前端我使用 Vue+ElementUI+AxiOS,咱们这个案例比较简单,就没有必要搭建单页面了,直接用普通的 html 就行了。另外,Vue 我是用了 vue3:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- Import style -->
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.CSS" rel="external nofollow" rel="external nofollow" rel="external nofollow" />
<script src="https://unpkg.com/vue@3"></script>
<!-- Import component library -->
<script src="//unpkg.com/element-plus"></script>
</head>
<body>
<div id="app">
<h1>开始一个请假流程</h1>
<table>
<tr>
<td>请输入姓名:</td>
<td>
<el-input type="text" v-model="afl.name"/>
</td>
</tr>
<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>
</table>
<el-button type="primary" @click="submit">提交请假申请</el-button>
</div>
<script>
Vue.createApp(
{
data() {
return {
afl: {
name: 'javaboy',
days: 3,
reason: '休息一下'
}
}
},
methods: {
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);
} else {
//提交失败
_this.$message.error(response.data.msg);
}
})
.catch(function (error) {
console.log(error);
});
}
}
}
).use(ElementPlus).mount('#app')
</script>
</body>
</html>
这个页面有几个需要注意的点:
好啦,这就写好了。
然而,提交完成后,没有一个直观的展示,虽然前端提示说提交成功了,但是究竟成功没,还得眼见为实。
好了,接下来我们要做的事情就是把用户提交的流程展示出来。
按理说,比如经理登录成功之后,系统页面就自动展示出来经理需要审批的流程,但是我们当前这个例子为了简单,就没有登录这个操作了,需要需要用户将来在网页上选一下自己的身份,接下来就会展示出这个身份所对应的需要操作的流程。
我们来看任务接口:
@GetMapping("/list")
public RespBean leaveList(String identity) {
return askForLeaveService.leaveList(identity);
}
这个请求参数 identity 就表示当前用户的身份(本来应该是登录后自动获取,但是因为我们目前没有登录,所以这个参数是由前端传递过来)。来继续看 askForLeaveService 中的方法:
@Service
public class AskForLeaveService {
@Autowired
TaskService taskService;
public RespBean leaveList(String identity) {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup(identity).list();
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 就是流程中要做的每一件事情,我们首先通过 TaskService,查询出来这个用户需要处理的任务,例如前端前传来的是 managers,那么这里就是查询所有需要由 managers 用户组处理的任务。
这段代码要结合流程图一起来理解,小伙伴们回顾下我们流程图中有如下一句:
<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>
这意思就是说这个 userTask 是由 managers 这个组中的用户来处理,所以上面 Java 代码中的查询就是查询 managers 这个组中的用户需要审批的任务。
我们将所有需要审批的任务查询出来后,通过 taskId 可以进一步查询到这个任务中当时传入的各种变量,我们将这些数据封装成一个对象,并最终返回到前端。
最后,我们再来看下前端页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- Import style -->
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" />
<script src="https://unpkg.com/vue@3"></script>
<!-- Import component library -->
<script src="//unpkg.com/element-plus"></script>
</head>
<body>
<div id="app">
<div>
<div>请选择你的身份:</div>
<div>
<el-select name="" id="" v-model="identity" @change="initTasks">
<el-option :value="iden" v-for="(iden,index) in identities" :key="index" :label="iden"></el-option>
</el-select>
<el-button type="primary" @click="initTasks">刷新一下</el-button>
</div>
</div>
<el-table border strip :data="tasks">
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="days" label="请假天数"></el-table-column>
<el-table-column prop="reason" label="请假原因"></el-table-column>
<el-table-column lable="操作">
<template #default="scope">
<el-button type="primary" @click="approveOrReject(scope.row.id,true,scope.row.name)">批准</el-button>
<el-button type="danger" @click="approveOrReject(scope.row.id,false,scope.row.name)">拒绝</el-button>
</template>
</el-table-column>
</el-table>
</div>
<script>
Vue.createApp(
{
data() {
return {
tasks: [],
identities: [
'managers'
],
identity: ''
}
},
methods: {
initTasks() {
let _this = this;
axios.get('/list?identity=' + this.identity)
.then(function (response) {
_this.tasks = response.data.data;
})
.catch(function (error) {
console.log(error);
});
}
}
}
).use(ElementPlus).mount('#app')
</script>
</body>
</html>
大家看到,首先有一个下拉框,我们在这个下拉框中来选择用户的身份。选择完成后,触发 initTasks 方法,然后在这个方法中,发起网络请求,最终将请求结果渲染出来。
最终效果如下:
当然用户也可以点击刷新按钮,刷新列表。
这样,当第五小节中,员工提交了一个请假审批之后,我们在这个列表中就可以查看到员工提交的请假审批了(在流程图中,我们直接设置了用户的请假审批固定提交给 managers,在后续的文章中,松哥会教大家如何把这个提交的目标用户变成一个动态的)。
接下来经理就可以选择批准或者是拒绝这请假了。
首先我们封装一个实体类用来接受前端传来的请求:
public class ApproveRejectVO {
private String taskId;
private Boolean approve;
private String name;
// 省略 getter/setter
}
参数都好理解,approve 为 true 表示申请通过,false 表示申请被拒绝。
接下来我们来看接口:
@PostMapping("/handler")
public RespBean askForLeaveHandler(@RequestBody ApproveRejectVO approveRejectVO) {
return askForLeaveService.askForLeaveHandler(approveRejectVO);
}
看具体的 askForLeaveHandler 方法:
@Service
public class AskForLeaveService {
@Autowired
TaskService taskService;
public RespBean askForLeaveHandler(ApproveRejectVO approveRejectVO) {
try {
boolean approved = approveRejectVO.getApprove();
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("approved", approved);
variables.put("employee", approveRejectVO.getName());
Task task = taskService.createTaskQuery().taskId(approveRejectVO.getTaskId()).singleResult();
taskService.complete(task.getId(), variables);
if (approved) {
//如果是同意,还需要继续走一步
Task t = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).singleResult();
taskService.complete(t.getId());
}
return RespBean.ok("操作成功");
} catch (Exception e) {
e.printStackTrace();
}
return RespBean.error("操作失败");
}
}
大家注意这个审批流程:
好啦,接口就写好了。
当然,这里还涉及到两个自定义的逻辑,就是批准或者拒绝之后的自定义逻辑,这个其实很好写,如下:
public class Approve implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) {
System.out.println("申请通过:"+execution.getVariables());
}
}
我们自定义类实现 JavaDelegate 接口即可,然后我们在 execute 方法中做自己想要做的事情即可,execution 中有这个流程中的所有变量。我们可以在这里发邮件、发短信等等。Reject 的定义方式也是类似的。这些自定义类写好之后,将来配置到流程图中即可(可查看上文的流程图)。
最后再来看看前端提交方法就简单了(页面源码上文已经列出):
approveOrReject(taskId, approve,name) {
let _this = this;
axios.post('/handler', {taskId: taskId, approve: approve,name:name})
.then(function (response) {
_this.initTasks();
})
.catch(function (error) {
console.log(error);
});
}
这就一个普通的 ajax 请求,批准的话第二个参数就为 true,拒绝的话第二个参数就为 false。
最后,每个用户都可以查看自己曾经的申请记录。本来这个登录之后就可以展示了,但是因为我们没有登录,所以这里也是需要手动输入查询的用户,然后根据用户名查询这个用户的历史记录,我们先来看查询接口:
@GetMapping("/search")
public RespBean searchResult(String name) {
return askForLeaveService.searchResult(name);
}
参数就是要查询的用户名。具体的查询流程如下:
public RespBean searchResult(String name) {
List<HistoryInfo> historyInfos = new ArrayList<>();
List<HistoricProcessInstance> historicProcessInstances = historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(name).finished().orderByProcessInstanceEndTime().desc().list();
for (HistoricProcessInstance historicProcessInstance : historicProcessInstances) {
HistoryInfo historyInfo = new HistoryInfo();
Date startTime = historicProcessInstance.getStartTime();
Date endTime = historicProcessInstance.getEndTime();
List<HistoricVariableInstance> historicVariableInstances = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(historicProcessInstance.getId())
.list();
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)) {
historyInfo.setStatus((Boolean) value);
} else if ("name".equals(variableName)) {
historyInfo.setName((String) value);
}
}
historyInfo.setStartTime(startTime);
historyInfo.setEndTime(endTime);
historyInfos.add(historyInfo);
}
return RespBean.ok("ok", historyInfos);
}
最后,前端通过表格展示这个数据即可:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- Import style -->
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" rel="external nofollow" rel="external nofollow" rel="external nofollow" />
<script src="https://unpkg.com/vue@3"></script>
<!-- Import component library -->
<script src="//unpkg.com/element-plus"></script>
</head>
<body>
<div id="app">
<div style="margin-top: 50px">
<el-input v-model="name" style="width: 300px" placeholder="请输入用户名"></el-input>
<el-button type="primary" @click="search">查询</el-button>
</div>
<div>
<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">
<el-tag type="success" v-if="scope.row.status">已通过</el-tag>
<el-tag type="danger" v-else>已拒绝</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
<script>
Vue.createApp(
{
data() {
return {
historyInfos: [],
name: 'zhangsan'
}
},
methods: {
search() {
let _this = this;
axios.get('/search?name=' + this.name)
.then(function (response) {
if (response.data.status == 200) {
_this.historyInfos=response.data.data;
} else {
_this.$message.error(response.data.msg);
}
})
.catch(function (error) {
console.log(error);
});
}
}
).use(ElementPlus).mount('#app')
</script>
</body>
</html>
这个都是一些常规操作,我就不多说了,最终展示效果如下:
以上就是SpringBoot+Vue+Flowable模拟实现请假审批流程的详细内容,更多关于SpringBoot Vue Flowable请假审批流程的资料请关注编程网其它相关文章!
--结束END--
本文标题: SpringBoot+Vue+Flowable模拟实现请假审批流程
本文链接: https://lsjlt.com/news/172303.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-03-01
2024-03-01
2024-03-01
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0