SpringBoot+Vue+Flowable,模擬一個請假審批流程!

語言: CN / TW / HK
ead>

小夥伴們知道鬆哥最近在錄 TienChin 專案視訊,這個專案會用到工作流,為了幫助小夥伴們更好的理解這個專案,鬆哥最近會出幾篇文章和大夥聊一聊工作流 flowable 的使用,算是給 TienChin 專案的第一個鋪墊,當然,在 TienChin 專案的系列視訊中,我也會和大家詳細聊一聊 flowable 流程引擎的使用。

今天我就先寫一個簡單的請假流程,讓小夥伴們對 flowable 先有一個直觀的認知。

  1. 效果展示

在正式開搞之前,我先來給小夥伴們看下我們今天要完成的效果。

簡單起見,我這裡並沒有引入使用者、角色等概念,涉及到使用者的地方都是手動輸入,在後續的文章中我會繼續結合 Spring Security 來和大家展示引入使用者之後的情況。

我們先來看看請假頁面:

員工可以在這個頁面輸入姓名,請假天數以及請假理由等,然後點選按鈕提交一個請假申請。

當員工提交請假申請之後,這個請假申請預設是由經理來處理的,此時經理登入之後,就可以看到員工提交上來的請求:

經理此時可以選擇批准或者拒絕。無論是批准還是拒絕,都可以通過簡訊或者郵件等告知員工。

對於員工來說,也可以在一個頁面查詢自己請假流程的最終情況:

可能有小夥伴已經注意到了,我們這裡所有涉及到使用者名稱的地方,都需要手動輸入。這是因為我為了讓這個案例足夠簡單,暫時沒有引入 Spring Security,只是單純的和大家分享 Flowable 的用法,等小夥伴們通過這篇文章掌握了 Flowable 的基本用法之後,下篇文章我會和大家分享如何結合具體的使用者來使用。

  1. 工程建立

我就直接來和小夥伴們展示 Spring Boot 中 flowable 的用法了。

首先我們建立一個 Spring Boot 專案,建立的時候引入 Web 和 MySQL 驅動依賴即可,專案建立成功之後,再引入 flowable 依賴,最終的依賴檔案如下:

org.springframework.boot spring-boot-starter-web org.flowable flowable-spring-boot-starter 6.7.2 mysql mysql-connector-java runtime

專案建立成功之後,首先需要我們在 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. 流程圖分析

今天這個例子比較簡單,就是一個請假流程,我暫時先不跟小夥伴們去扯畫流程圖的事,咱們直接用一個官網現成的請假流程圖:

我們先來簡單分析一下這張圖:

最左側的圓圈叫做啟動事件(start event),這表示一個流程例項的起點。 一個流程啟動之後,首先到達第一個有使用者圖示的矩形中,這個矩形稱為一個 User Task,在這個 User Task 中,經理可以選擇批准亦或者拒絕。 UserTask 的下一步是一個菱形,這個稱作排他閘道器(Exclusive Gateway),這個會將請求路由到不同的地方。 先說批准,如果在第一個矩形中,經理選擇了批准,那麼就會進入到一個帶有齒輪圖示的矩形中,在這個矩形中我們我們可以額外做一些事情,然後又會呼叫到一個 UserTask,最終完成整個流程。 如果經理選擇了拒絕,則會進入到下面的發郵件的矩形中,在這個中我們可以給員工傳送一個通知,告知他請假沒有通過。 當系統走到最右邊的圓圈之後,就表示這個流程執行結束了。

這個流程圖對應的 XML 檔案位於 src/main/resources/processes/holiday-request.bpmn20.xml 位置,其內容如下:

    <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>

很多想學習流程引擎的小夥伴都會被這個 XML 檔案勸退,但是!!!

如果你願意靜下心來認真閱讀這個 XML 檔案,你會發現流程引擎原來如此簡單!

我們來挨個看下這裡的每一個節點:

process:這表示一個流程,例如本文和大家分享的請假就是一個流程。 startEvent:這表示流程的開始,這就是一個開始事件。 userTask:這就是一個具體的流程節點了,flowable:candidateGroups 屬性表示這個節點該由哪個使用者組中的使用者來處理。 sequenceFlow:這就是連線各個流程節點之間的線條,這個裡邊一般有兩個屬性,sourceRef 和 targetRef,前者表示線條的起點,後者表示線條的終點。 exclusiveGateway:表示一個排他性閘道器,也就是那個菱形選擇框。 從排他性網關出來的線條有兩個,大家注意看上面的程式碼,這兩個線條中都涉及到一個變數 approved,如果這個變數為 true,則 targeRef 就是 externalSystemCall;如果這個變數為 false,則 targetRef 就是 rejectLeave。 serviceTask:這就是我們定義的一個具體的外部服務,如果在整個流程執行的過程中,你有一些需要自己完成的事情,那麼可以通過 serviceTask 來實現,這個節點會有一個 flowable:class 屬性,這個屬性的值就是一個自定義類。 另外,上文中部分節點中還涉及到變數 ${},這個變數是在流程執行的過程中傳入進來的。

總而言之,只要小夥伴們靜下心來認真閱讀一下上面的 XML,你會發現 So Easy!

  1. 請假申請

好了,接下來我們就來看一個具體的請假申請。由於請假流程只要放對位置,就會自動載入,所以我們並不需要手動載入請假流程,直接開始一個請假申請流程即可。

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 方法來開啟一個流程,開啟流程的時候一共傳入了三個引數:

第一個引數表示流程引擎的名字,這就是我們剛才在流程的 XML 檔案中定義的名字。 第二個引數表示當前這個流程的 key,我用了申請人的名字,將來我們可以通過申請人的名字查詢這個人曾經提交的所有申請流程。 第三個引數就是我們的變量了。

好了,這服務端就寫好了。

4.2 前端頁面

接下來我們來開發前端頁面。

前端我使用 Vue+ElementUI+Axios,咱們這個案例比較簡單,就沒有必要搭建單頁面了,直接用普通的 HTML 就行了。另外,Vue 我是用了 Vue3:

Title

開始一個請假流程

請輸入姓名:
請輸入請假天數:
請輸入請假理由:
提交請假申請

這個頁面有幾個需要注意的點:

通過 Vue.createApp 來建立一個 Vue 例項,這跟以前 Vue2 中直接 new 一個 Vue 例項不一樣。 在最下面,通過 use 來配置 ElementPlus 外掛,這個跟 Vue2 也不一樣。在 Vue2 中,如果我們單純的在 HTML 頁面中引用 ElementUI 並不需要這個步驟。 剩下的東西就比較簡單了,上面先引入 Vue3、Axios 以及 ElementPlus,然後三個輸入框,點選按鈕提交請求,引數就是三個輸入框中的資料,提交成功或者失敗,分別彈個框出來提示一下就行了。

好啦,這就寫好了。

然而,提交完成後,沒有一個直觀的展示,雖然前端提示說提交成功了,但是究竟成功沒,還得眼見為實。

  1. 任務展示

好了,接下來我們要做的事情就是把使用者提交的流程展示出來。

按理說,比如經理登入成功之後,系統頁面就自動展示出來經理需要審批的流程,但是我們當前這個例子為了簡單,就沒有登入這個操作了,需要需要使用者將來在網頁上選一下自己的身份,接下來就會展示出這個身份所對應的需要操作的流程。

我們來看任務介面:

@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 是由 managers 這個組中的使用者來處理,所以上面 Java 程式碼中的查詢就是查詢 managers 這個組中的使用者需要審批的任務。

我們將所有需要審批的任務查詢出來後,通過 taskId 可以進一步查詢到這個任務中當時傳入的各種變數,我們將這些資料封裝成一個物件,並最終返回到前端。

最後,我們再來看下前端頁面:

Title

請選擇你的身份:
重新整理一下

大家看到,首先有一個下拉框,我們在這個下拉框中來選擇使用者的身份。選擇完成後,觸發 initTasks 方法,然後在這個方法中,發起網路請求,最終將請求結果渲染出來。

最終效果如下:

當然使用者也可以點選重新整理按鈕,重新整理列表。

這樣,當第五小節中,員工提交了一個請假審批之後,我們在這個列表中就可以檢視到員工提交的請假審批了(在流程圖中,我們直接設定了使用者的請假審批固定提交給 managers,在後續的文章中,鬆哥會教大家如何把這個提交的目標使用者變成一個動態的)。

  1. 請假審批

接下來經理就可以選擇批准或者是拒絕這請假了。

首先我們封裝一個實體類用來接受前端傳來的請求:

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 中有這個流程中的所有變數。我們可以在這裡發郵件(公眾號江南一點雨後臺回覆 666 有發郵件教程)、發簡訊等等。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。

  1. 結果查詢

最後,每個使用者都可以檢視自己曾經的申請記錄。本來這個登入之後就可以展示了,但是因為我們沒有登入,所以這裡也是需要手動輸入查詢的使用者,然後根據使用者名稱查詢這個使用者的歷史記錄,我們先來看查詢介面:

@GetMapping("/search") public RespBean searchResult(String name) { return askForLeaveService.searchResult(name); }

引數就是要查詢的使用者名稱。具體的查詢流程如下:

public RespBean searchResult(String name) { List historyInfos = new ArrayList<>(); List 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 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 則主要是用來查詢流程變數的。

最後,前端通過表格展示這個資料即可:

Title

查詢

這個都是一些常規操作,我就不多說了,最終展示效果如下:

  1. 小結

好啦,一個簡單的請假流程,讓大家對 Flowable 的玩法有一個基本的認知,下篇文章鬆哥來和大家繼續完善本文。Flowable 的視訊將會出現在 TienChin 專案中,大家不要錯過哦:TienChin 專案配套視訊來啦。

來源: SpringBoot+Vue+Flowable,模擬一個請假審批流程!