前言
? ? ? ? 本文從解讀源碼到實現功能,完整的學習Flowable的【TaskService】-【claim】方法實現的任務拾取功能。
一、概述
????????當調用 TaskService.claim(taskId, userId) 時,Flowable 會先加載并校驗任務實體,再判斷該任務是否已被認領;若未被認領,則將 assignee 字段設為傳入的 userId(null 則表示“解綁”),并在身份鏈接(IdentityLink)表中做相應的增刪操作;隨后觸發任務分配事件、寫入歷史記錄(如開啟歷史配置時),最后將更新結果刷新到數據庫。所有這些操作最終都委托給了 TaskServiceImpl 中的 ClaimTaskCmd 來完成。
二、方法簽名與定位
????????claim(String taskId, String userId) 方法在 org.flowable.engine.TaskService 接口中定義,由 TaskServiceImpl 實現,實現層直接調用命令執行器(commandExecutor.execute(new ClaimTaskCmd(taskId, userId))),ClaimTaskCmd 是執行認領邏輯的核心。
????????ClaimTaskCmd 繼承自 NeedsActiveTaskCmd,用于確保任務處于激活狀態后再執行認領邏輯。
public class ClaimTaskCmd extends NeedsActiveTaskCmd {
? ? protected String userId;
? ? public ClaimTaskCmd(String taskId, String userId) {
? ? ? ? super(taskId);
? ? ? ? this.userId = userId;
? ? }
}
????????構造器保存了要認領的 taskId 和 userId,并由父類處理掛起狀態檢查,當傳入的 userId 非空時,表示要將任務分配給某位用戶。
①?設置認領時間與狀態
????????使用引擎的 Clock 獲取當前時間,調用 task.setClaimTime(...) 和 task.setClaimedBy(userId) 設置認領元數據。將任務狀態更新為 Task.CLAIMED。
②?沖突檢查
????????若該任務已被分配給某人(task.getAssignee()!=null),且與當前 userId 不同,則拋出 FlowableTaskAlreadyClaimedException,避免多用戶并發認領。
????????若已分配給同一用戶,則僅記錄一次 recordTaskInfoChange(...),保持歷史一致性。
③?首次分配
????????當任務尚未有 assignee 時,調用 TaskHelper.changeTaskAssignee(task, userId) 為任務設置新認領者。
????????如果配置了 UserTaskStateInterceptor,會觸發其 handleClaim(...) 回調,用于外部擴展。
④?寫入身份鏈接歷史
????????最后,無論是首次分配還是重復分配,都通過 HistoryManager.createUserIdentityLinkComment(...) 將 ASSIGNEE 類型的身份鏈接(認領記錄)寫入歷史審計表。
三、加載與校驗任務
①?加載任務實體
????????命令中首先通過 TaskEntityManager.findById(taskId) 從 ACT_RU_TASK 表中獲取 TaskEntity。
②?任務存在性檢查
????????若返回 null,拋出 FlowableObjectNotFoundException,提示“無此任務”。
③?已認領沖突檢查
????????若 task.getAssignee() 不為 null 且與傳入的 userId 不同,則拋出沖突異常(FlowableConflictException),禁止不同用戶重復認領.
四、賦值 assignee 與解綁
①?設為認領
????????當 userId 非空時,調用 TaskEntity.setAssignee(userId) 將任務歸屬新認領者。
②?設為解綁
????????當 userId==null 時,等價于 unclaim 操作,相當于將 assignee 設回 null。
五、身份鏈接(IdentityLink)處理
①?歷史遺留機制
????????Flowable 中“候選人”與“認領人”都存儲在 ACT_RU_IDENTITYLINK 表,但 ASSIGNEE 類型在表中常伴隨特殊處理(空 TASK_ID 或 PROC_INSTANCE_ID 字段)。
② 若是認領
????????在命令里會先 刪除 原有與該任務相關的 ASSIGNEE 類型 IdentityLink,再 新增 一條新的 IdentityLinkType.ASSIGNEE,以確保數據一致。
③?若是解綁
????????則只執行刪除操作,不新增。
六、事件分發與歷史記錄
①?事件分發
????????完成認領后,Flowable 會通過 EventDispatcher 發布 ENTITY_LINK_CREATED(或刪除時 ENTITY_LINK_DELETED)以及 TASK_ASSIGNED 等事件,供監聽器或審計插件消費。
②?歷史記錄
????????若引擎配置 historyLevel ≥ AUDIT,會在 ACT_HI_TASKINST 表中記錄任務認領時間、認領者等信息。
七、持久化更新
????????最后,命令通過 TaskEntityManager.update(taskEntity) 將變更刷回 ACT_RU_TASK 表,并同步提交事務。若配置了異步歷史,歷史記錄寫入也可能延遲到異步作業中完成,進一步提升性能。
八、完成后端接口
① 定義請求參數
? ? ? ? 這里我們只需要傳入任務ID即可,用戶ID我們從框架的session中獲取,一般的腳手架都是這樣的,請結合自己的腳手架處理。
package com.ceair.entity.request;import lombok.Data;import java.io.Serial;
import java.io.Serializable;/*** @author wangbaohai* @ClassName PickupMyTaskReq* @description: 拾取我的任務請求參數* @date 2025年05月03日* @version: 1.0.0*/
@Data
public class PickupMyTaskReq implements Serializable {@Serialprivate static final long serialVersionUID = 1L;// 任務編號private String taskId;}
② 定義服務接口
/*** 領取我的任務接口* 此方法允許用戶領取屬于自己的任務,根據提供的條件和參數** @param pickupMyTaskReq 領取任務的請求對象,包含領取任務所需的信息和條件* @return 返回一個Boolean值,表示任務領取是否成功true表示成功,false表示失敗*/
Boolean pickupMyTask(PickupMyTaskReq pickupMyTaskReq);
③ 實現服務接口
/*** 拾取我的待辦任務* <p>* 此方法允許當前登錄用戶拾取一個待辦任務通過提供任務ID和當前用戶信息,* 系統將該任務分配給當前用戶** @param pickupMyTaskReq 包含任務ID的請求對象如果請求對象或任務ID為空,* 將拋出IllegalArgumentException異常* @return 任務拾取成功返回true,否則拋出異常* @throws BusinessException 如果用戶未登錄或任務拾取過程中發生業務異常* @throws IllegalArgumentException 如果輸入參數不合法,如任務ID為空*/
@Override
public Boolean pickupMyTask(PickupMyTaskReq pickupMyTaskReq) {try {// 參數判空if (pickupMyTaskReq == null || StringUtils.isBlank(pickupMyTaskReq.getTaskId())) {log.error("任務拾取失敗:非法的任務ID");throw new IllegalArgumentException("任務拾取失敗:非法的任務ID");}// 獲取當前登錄用戶信息UserInfo userInfo = userInfoUtils.getUserInfoFromAuthentication();if (userInfo == null) {log.error("拾取我的待辦任務失敗,原因:用戶未登錄");throw new BusinessException("拾取我的待辦任務失敗,原因:用戶未登錄");}// 通過 taskService 拾取任務taskService.claim(pickupMyTaskReq.getTaskId(), String.valueOf(userInfo.getId()));return true;} catch (IllegalArgumentException e) {log.error("任務拾取失敗,原因:參數錯誤", e);throw new BusinessException("任務拾取失敗,原因:參數錯誤", e);} catch (BusinessException e) {log.error("任務拾取失敗,原因:業務異常", e);throw new BusinessException("任務拾取失敗,原因:業務異常", e);} catch (Exception e) {log.error("任務拾取失敗,原因:未知異常", e);throw new BusinessException("任務拾取失敗,原因:未知異常", e);}
}
④ 定義功能接口
/*** 任務拾取。* <p>* 權限: /api/v1/myTask/pickupMyTask* 參數: pickupMyTaskReq - 包含任務拾取相關信息的請求對象* 返回: Result<Boolean> 表示任務拾取是否成功* <p>* 異常處理:* - 業務層異常 返回任務拾取失敗信息* - 其他未知異常 系統異常提示*/
@PreAuthorize("hasAnyAuthority('/api/v1/myTask/pickupMyTask')")
@Parameter(name = "pickupMyTaskReq", description = "任務拾取請求對象", required = true)
@Operation(summary = "任務拾取")
@PostMapping("/pickupMyTask")
public Result<Boolean> pickupMyTask(@RequestBody PickupMyTaskReq pickupMyTaskReq) {try {// 調用業務層方法,將任務分配給當前登錄用戶return Result.success(mayTaskService.pickupMyTask(pickupMyTaskReq));} catch (Exception e) {log.error("任務拾取失敗,原因:{}", e.getMessage());return Result.error("任務拾取失敗,原因:" + e.getMessage());}
}
九、完善前端功能按鈕
① 定義前端參數類型
// 拾取任務請求參數
export interface PickupMyTaskReq {
? taskId: string // 任務編號,對應 Java 中的 String taskId
}
② 封裝請求接口
/**
?* 拾取任務
?*/
export function pickupMyTask(data: PickupMyTaskReq) {
? return request.post<any>({
? ? url: '/pm-process/api/v1/myTask/pickupMyTask',
? ? data,
? })
}
③ 優化界面按鈕
? ? ? ? 這里把拾取按鈕加上權限自定義指令,以及點擊事件功能
<el-button v-if="scope.row.status === 1" v-hasButton="`btn.myTask.pickupMyTask`" type="primary" @click="onPickup(scope.row)">
? 拾取
</el-button>
④ 完成按鈕功能
/*** 異步函數用于處理任務拾取操作* @param row 任務對象,包含任務ID等信息*/
async function onPickup(row: TaskVO) {try {// 獲取當前任務ID并設置參數const param: PickupMyTaskReq = {taskId: row.taskId,}// 調用后端接口進行拾取操作const result: any = await pickupMyTask(param)// 如果接口調用成功且返回的狀態碼為200,則顯示成功提示信息if (result.success && result.code === 200) {ElMessage({message: '拾取成功',type: 'success',})// 重新加載數據handerPageData()}else {ElMessage({message: `拾取失敗: ${result.message}`,type: 'error',})}}catch (error) {// 捕獲異常并提取錯誤信息let errorMessage = '未知錯誤'if (error instanceof Error) {errorMessage = error.message}// 顯示操作失敗的錯誤提示信息ElMessage({message: `拾取失敗: ${errorMessage || '未知錯誤'}`,type: 'error',})}
}
十、添加權限
創建按鈕
分配權限
十一、驗證功能
定義一個流程并且發布,第一個節點我們作為候選人,讓我們可以拾取
啟動流程
查看待辦
拾取待辦
可以看到我們拾取任務成功后,就可以辦理或者歸還任務了。
后記
????????下一篇文章來梳理歸還任務的梳理以及具體的實現方法,本文的完整代碼倉庫地址請查看專欄第一篇文章的說明。
本文的后端分支是 process-10
本文的前端分支是 process-12
?