<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
小夥伴們知道鬆哥最近在錄 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請假審批流程的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45