正式に始める前に、まずは今日完成するエフェクトをお見せします。
わかりやすくするために、ここではユーザーやロールなどの概念は導入しませんでした。ユーザーが関与する場所はすべて手動で入力されます。以降の記事では、引き続き Spring Security を組み合わせて、その後の状況を示します。ユーザーのご紹介です。
まず休暇ページを見てみましょう:
#従業員は名前、休暇日数、休暇理由などを入力できます。このページで、[休暇申請を送信する] ボタンをクリックして申請します。
従業員が休暇申請を提出すると、デフォルトで休暇申請はマネージャーによって処理されます。マネージャーはログインすると、従業員によって提出された休暇申請を確認できます:
マネージャーはこの時点で承認または拒否を選択できます。承認か拒否かにかかわらず、従業員はテキスト メッセージまたは電子メールで通知を受けることができます。
従業員の場合は、休暇プロセスの最終ステータスを 1 つのページで確認することもできます:
Me Spring Boot で Flowable を使用する方法を友達に見せてみましょう。
最初に Spring Boot プロジェクトを作成します。作成するときは、Web ドライバーと MySQL ドライバーの依存関係を導入するだけです。プロジェクトが正常に作成されたら、流動的な依存関係を導入します。最終的な依存関係ファイルは次のとおりです:
<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 プロジェクトは、ProcessEngine、CmmnEngine、DmnEngine、FormEngine、ContentEngine、IdmEngine などの Bean を Flowable に自動的に作成して公開します。
すべての Flowable サービスは Spring Bean として呼び出すことができます。たとえば、RuntimeService、TaskService、HistoryService およびその他のサービスを、使用する必要があるときに直接挿入して使用できます。
同時に:
resources/processes ディレクトリ内のすべての BPMN 2.0 プロセス定義は自動的にデプロイされるため、Spring Boot プロジェクトでは次の作業のみが必要です。独自のプロセスを追加 ファイルを適切な場所に置くだけで、残りは自動的に行われます。
cases ディレクトリ内の CMMN 1.1 ケースは、自動的にデプロイされます。
forms ディレクトリ内のフォーム定義はすべて自動的に展開されます。
今日の例は比較的単純で、休暇申請プロセスです。友達と休暇申請のフローチャートを描くことについては話しません。公式 Web サイトにある既製の休暇申請フローチャートを直接使用してみましょう:
この図を簡単に分析してみましょう:
一番左 横の円は開始イベントと呼ばれ、プロセスインスタンスの開始点を表します。
プロセスが開始されると、最初にユーザー アイコンの付いた最初の四角形に到達します。この四角形はユーザー タスクと呼ばれます。このユーザー タスクでは、マネージャーは承認または拒否を選択できます。 . .
UserTask の次のステップは、排他的ゲートウェイと呼ばれるダイヤモンドで、リクエストをさまざまな場所にルーティングします。
まず承認について話しましょう。マネージャーが最初の四角形で承認を選択した場合、歯車アイコンの付いた四角形に入ります。この四角形では、いくつかの追加の操作を行うことができます。次に、UserTask を呼び出して、最終的にプロセス全体を完了します。
マネージャーが拒否することを選択した場合は、下の電子メールの四角形を入力します。ここで、休暇申請が承認されなかったことを伝える通知を従業員に送信できます。
システムが右端の円に到達すると、このプロセスの実行が終了したことを意味します。
このフローチャートに対応する XML ファイルは、src/main/resources/processes/holiday-request.bpmn20.xml にあり、その内容は次のとおりです。 ##多くの考え プロセス エンジンを学習している友人は、この XML ファイルを見て落胆するかもしれませんが、! ! !
落ち着いてこの XML ファイルを注意深く読んでみると、プロセス エンジンが非常に単純であることがわかるでしょう。
ここで各ノードを 1 つずつ見てみましょう:
sequenceFlow:这就是连接各个流程节点之间的线条,这个里边一般有两个属性,sourceRef 和 targetRef,前者表示线条的起点,后者表示线条的终点。
exclusiveGateway:表示一个排他性网关,也就是那个菱形选择框。
从排他性网关出来的线条有两个,大家注意看上面的代码,这两个线条中都涉及到一个变量 approved,如果这个变量为 true,则 targeRef 就是 externalSystemCall;如果这个变量为 false,则 targetRef 就是 rejectLeave。
serviceTask:这就是我们定义的一个具体的外部服务,如果在整个流程执行的过程中,你有一些需要自己完成的事情,那么可以通过 serviceTask 来实现,这个节点会有一个 flowable:class 属性,这个属性的值就是一个自定义类。
另外,上文中部分节点中还涉及到变量 ${},这个变量是在流程执行的过程中传入进来的。
总而言之,只要小伙伴们静下心来认真阅读一下上面的 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 方法来开启一个流程,开启流程的时候一共传入了三个参数:
第一个参数表示流程引擎的名字,这就是我们刚才在流程的 XML 文件中定义的名字。
第二个参数表示当前这个流程的 key,我用了申请人的名字,将来我们可以通过申请人的名字查询这个人曾经提交的所有申请流程。
第三个参数就是我们的变量了。
好了,这服务端就写好了。
接下来我们来开发前端页面。
前端我使用 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"> <h2>开始一个请假流程</h2> <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>
这个页面有几个需要注意的点:
通过 Vue.createApp 来创建一个 Vue 实例,这跟以前 Vue2 中直接 new 一个 Vue 实例不一样。
使用 use 方法来配置 ElementPlus 插件,这一点与 Vue2 不同。在 Vue2 中,使用 ElementUI 只需要在HTML页面中进行简单的引用即可,不需要额外的步骤。
剩下的东西就比较简单了,上面先引入 Vue3、Axios 以及 ElementPlus,然后三个输入框,点击按钮提交请求,参数就是三个输入框中的数据,提交成功或者失败,分别弹个框出来提示一下就行了。
好啦,这就写好了。
然而,提交完成后,没有一个直观的展示,虽然前端提示说提交成功了,但是究竟成功没,还得眼见为实。
好了,接下来我们要做的事情就是把用户提交的流程展示出来。
按理说,比如经理登录成功之后,系统页面就自动展示出来经理需要审批的流程,但是我们当前这个例子为了简单,就没有登录这个操作了,需要需要用户将来在网页上选一下自己的身份,接下来就会展示出这个身份所对应的需要操作的流程。
我们来看任务接口:
@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("操作失败"); } }
大家注意这个审批流程:
审批时需要两个参数,approved 和 employee,approved 为 true,就会自动进入到审批通过的流程中,approved 为 false 则会自动进入到拒绝流程中。
通过 taskService,结合 taskId,从流程中查询出对应的 task,然后调用 taskService.complete 方法传入 taskId 和 变量,以使流程向下走。
小伙伴们再回顾一下我们前面的流程图,如果请求被批准备了,那么在执行完自定义的 Approve 逻辑后,就会进入到 Holiday approved 这个 userTask 中,注意此时并不会继续向下走了(还差一步到结束事件);如果是请求拒绝,则在执行完自定义的 Reject 逻辑后,就进入到结束事件了,这个流程就结束了。
针对第三条,所以代码中我们还需要额外再加一步,如果是 approved 为 true,那么就再从当前流程中查询出来需要执行的 task,再调用 complete 继续走一步,此时就到了结束事件了,这个流程就结束了。注意这次的查询是根据当前流程的 ID 查询的,一个流程就是一条线,这条线上有很多 Task,我们可以从 Task 中获取到流程的 ID。
好啦,接口就写好了。
当然,这里还涉及到两个自定义的逻辑,就是批准或者拒绝之后的自定义逻辑,这个其实很好写,如下:
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); }
我们当时在开启流程的时候,传入了一个参数 key,这里就是再次通过这个 key,也就是用户名去查询历史流程,查询的时候还加上了 finished 方法,这个表示要查询的流程必须是执行完毕的流程,对于没有执行完毕的流程,这里不查询,查完之后,按照流程最后的处理时间进行排序。
遍历第一步的查询结果,从 HistoricProcessInstance 中提取出每一个流程的详细信息,并存入到集合中,并最终返回。
这里涉及到两个历史数据查询,createHistoricProcessInstanceQuery 用来查询历史流程,而 createHistoricVariableInstanceQuery 则主要是用来查询流程变量的。
最后,前端通过表格展示这个数据即可:
<!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 > <el-input v-model="name" 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 を使用して休暇承認プロセスをシミュレートする方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。