SpringBoot+Vue+Flowable,模拟一个请假审批流程

Posted _江南一点雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot+Vue+Flowable,模拟一个请假审批流程相关的知识,希望对你有一定的参考价值。

小伙伴们知道松哥最近在录 TienChin 项目视频,这个项目会用到工作流,为了帮助小伙伴们更好的理解这个项目,松哥最近会出几篇文章和大伙聊一聊工作流 flowable 的使用,算是给 TienChin 项目的第一个铺垫,当然,在 TienChin 项目的系列视频中,我也会和大家详细聊一聊 flowable 流程引擎的使用。

今天我就先写一个简单的请假流程,让小伙伴们对 flowable 先有一个直观的认知。

1. 效果展示

在正式开搞之前,我先来给小伙伴们看下我们今天要完成的效果。

简单起见,我这里并没有引入用户、角色等概念,涉及到用户的地方都是手动输入,在后续的文章中我会继续结合 Spring Security 来和大家展示引入用户之后的情况。

我们先来看看请假页面:

员工可以在这个页面输入姓名,请假天数以及请假理由等,然后点击按钮提交一个请假申请。

当员工提交请假申请之后,这个请假申请默认是由经理来处理的,此时经理登录之后,就可以看到员工提交上来的请求:

经理此时可以选择批准或者拒绝。无论是批准还是拒绝,都可以通过短信或者邮件等告知员工。

对于员工来说,也可以在一个页面查询自己请假流程的最终情况:

可能有小伙伴已经注意到了,我们这里所有涉及到用户名的地方,都需要手动输入。这是因为我为了让这个案例足够简单,暂时没有引入 Spring Security,只是单纯的和大家分享 Flowable 的用法,等小伙伴们通过这篇文章掌握了 Flowable 的基本用法之后,下篇文章我会和大家分享如何结合具体的用户来使用。

2. 工程创建

我就直接来和小伙伴们展示 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 等等服务,我们都可以在需要使用的时候,直接注入就可以使用了。

同时:

  • resources/processes 目录下的任何 BPMN 2.0 流程定义都会被自动部署,所以在 Spring Boot 项目中,我们只需要将自己的流程文件放对位置即可,剩下的事情就会自动完成。
  • cases 目录下的任何 CMMN 1.1 事例都会被自动部署。
  • forms 目录下的任何 Form 定义都会被自动部署。

3. 流程图分析

今天这个例子比较简单,就是一个请假流程,我暂时先不跟小伙伴们去扯画流程图的事,咱们直接用一个官网现成的请假流程图:

我们先来简单分析一下这张图:

  1. 最左侧的圆圈叫做启动事件(start event),这表示一个流程实例的起点。
  2. 一个流程启动之后,首先到达第一个有用户图标的矩形中,这个矩形称为一个 User Task,在这个 User Task 中,经理可以选择批准亦或者拒绝。
  3. UserTask 的下一步是一个菱形,这个称作排他网关(Exclusive Gateway),这个会将请求路由到不同的地方。
  4. 先说批准,如果在第一个矩形中,经理选择了批准,那么就会进入到一个带有齿轮图标的矩形中,在这个矩形中我们我们可以额外做一些事情,然后又会调用到一个 UserTask,最终完成整个流程。
  5. 如果经理选择了拒绝,则会进入到下面的发邮件的矩形中,在这个中我们可以给员工发送一个通知,告知他请假没有通过。
  6. 当系统走到最右边的圆圈之后,就表示这个流程执行结束了。

这个流程图对应的 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 文件,你会发现流程引擎原来如此简单!

我们来挨个看下这里的每一个节点:

  1. process:这表示一个流程,例如本文和大家分享的请假就是一个流程。
  2. startEvent:这表示流程的开始,这就是一个开始事件。
  3. userTask:这就是一个具体的流程节点了,flowable:candidateGroups 属性表示这个节点该由哪个用户组中的用户来处理。
  4. sequenceFlow:这就是连接各个流程节点之间的线条,这个里边一般有两个属性,sourceRef 和 targetRef,前者表示线条的起点,后者表示线条的终点。
  5. exclusiveGateway:表示一个排他性网关,也就是那个菱形选择框。
  6. 从排他性网关出来的线条有两个,大家注意看上面的代码,这两个线条中都涉及到一个变量 approved,如果这个变量为 true,则 targeRef 就是 externalSystemCall;如果这个变量为 false,则 targetRef 就是 rejectLeave。
  7. serviceTask:这就是我们定义的一个具体的外部服务,如果在整个流程执行的过程中,你有一些需要自己完成的事情,那么可以通过 serviceTask 来实现,这个节点会有一个 flowable:class 属性,这个属性的值就是一个自定义类。
  8. 另外,上文中部分节点中还涉及到变量 $,这个变量是在流程执行的过程中传入进来的。

总而言之,只要小伙伴们静下心来认真阅读一下上面的 XML,你会发现 So Easy!

4. 请假申请

好了,接下来我们就来看一个具体的请假申请。由于请假流程只要放对位置,就会自动加载,所以我们并不需要手动加载请假流程,直接开始一个请假申请流程即可。

4.1 服务端接口

首先我们需要一个实体类来接受前端传来的请假参数:用户名、请假天数以及请假理由:

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 方法来开启一个流程,开启流程的时候一共传入了三个参数:

  1. 第一个参数表示流程引擎的名字,这就是我们刚才在流程的 XML 文件中定义的名字。
  2. 第二个参数表示当前这个流程的 key,我用了申请人的名字,将来我们可以通过申请人的名字查询这个人曾经提交的所有申请流程。
  3. 第三个参数就是我们的变量了。

好了,这服务端就写好了。

4.2 前端页面

接下来我们来开发前端页面。

前端我使用 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"/>
    <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>

这个页面有几个需要注意的点:

  1. 通过 Vue.createApp 来创建一个 Vue 实例,这跟以前 Vue2 中直接 new 一个 Vue 实例不一样。
  2. 在最下面,通过 use 来配置 ElementPlus 插件,这个跟 Vue2 也不一样。在 Vue2 中,如果我们单纯的在 HTML 页面中引用 ElementUI 并不需要这个步骤。
  3. 剩下的东西就比较简单了,上面先引入 Vue3、Axios 以及 ElementPlus,然后三个输入框,点击按钮提交请求,参数就是三个输入框中的数据,提交成功或者失败,分别弹个框出来提示一下就行了。

好啦,这就写好了。

然而,提交完成后,没有一个直观的展示,虽然前端提示说提交成功了,但是究竟成功没,还得眼见为实。

5. 任务展示

好了,接下来我们要做的事情就是把用户提交的流程展示出来。

按理说,比如经理登录成功之后,系统页面就自动展示出来经理需要审批的流程,但是我们当前这个例子为了简单,就没有登录这个操作了,需要需要用户将来在网页上选一下自己的身份,接下来就会展示出这个身份所对应的需要操作的流程。

我们来看任务接口:

@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()<

以上是关于SpringBoot+Vue+Flowable,模拟一个请假审批流程的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot+Vue+Flowable,模拟一个请假审批流程

SpringBoot+Vue+Flowable,模拟一个请假审批流程

Spring Security + Vue + Flowable 怎么玩?

Spring Security + Vue + Flowable 怎么玩?

java OA项目源码 flowable activiti流程引擎 Springboot html vue.js 前后分离

springboot 整合 flowable 流程引擎