前言
? ? ? ? 得益于之前我們的基礎工程準備,我們終于可以正式啟動工作流程了,在啟動之前我們需要分配一下每個用戶任務的用戶信息,其中有三個選擇:【辦理人】/【候選組】/【候選用戶】,我們需要將系統中的用戶ID填入作為固定參數啟動工作流程,也可以填入參數在啟動時使用動態表單讓用戶自己選擇啟動工作流程。
一、參數解析
① 辦理人-assignee
? ? ? ? 1)定義
? ? ? ? 指定一個唯一的任務辦理人,該用戶在任務創建時就被鎖定為責任人,其他用戶無法認領。?可以填寫固定用戶名,如 flowable:assignee="john",也可以使用表達式動態傳入變量,如 flowable:assignee="${userId}"。
? ? ? ? 2)作用與行為
????????優先級最高:若同時配置了 assignee 與候選組或候選用戶,流程引擎會忽略候選設置,直接將任務委派給 assignee。擁有直接完成任務的權限,且該任務只會在他的待辦列表中出現。
②?候選組-candidateGroups
????????1)定義
????????指定一個或多個用戶組,格式為逗號分隔的組 ID 列表,或使用表達式 flowable:candidateGroups="${groupList}" 動態獲取組集合。
? ? ? ? 2)作用與行為
????????任務創建后會出現在所有指定組中每位成員的待辦列表,成員需通過 “Claim” 操作認領后才能完成任務。不指定 assignee 時,使用候選組可實現池化任務分發,適用于審批類、輪詢類場景。
③?候選用戶-candidateUsers
?????????1)定義
????????指定一個或多個具體用戶 ID 列表,格式為逗號分隔,或使用表達式如 flowable:candidateUsers="${userList}" 來動態獲取用戶集合。?
? ? ? ? 2)作用與行為
????????任務池化:任務創建后同時出現在所有候選用戶的待辦列表,任一候選人認領(Claim)即可完成任務。
????????細粒度控制:適用于團隊扁平化或小組協作場景,明確列出可處理人員。
????????并列候選:與 candidateGroups 可并存,用于覆蓋不同層級的權限模型。
④ 使用場景推薦
????????單一負責人場景:優先使用 assignee,確保任務責任清晰。
????????
二、比較靜態參數和動態參數
① 靜態參數
? ? ? ? 1)樣例
????????靜態配置即在 BPMN XML 中直接寫明目標用戶或組
<userTask id="approveTask"
? ? ? ? ? name="審批任務"
? ? ? ? ? flowable:assignee="john"
? ? ? ? ? flowable:candidateGroups="managers"
? ? ? ? ? flowable:candidateUsers="mary,paul"/>
? ? ? ? 2)優點
????????配置簡單:無需引擎解析表達式即可直接使用,模型直觀易懂。
????????無運行時開銷:完全避開了表達式求值過程,對性能幾乎無影響。
? ? ? ? 3)缺點
????????缺乏靈活性:一旦用戶或組有變更,需修改流程定義并重新部署。
????????重復配置:相似場景下多個任務需分別寫入同樣的固定值,易產生冗余和維護成本。
????????環境耦合:對于多租戶或不同部署環境,無法在不改流程的情況下實現差異化分配。
② 動態參數
? ? ? ? 1)樣例
????????在 Assignee/候選字段中使用 OGNL 表達式
<userTask id="holidayApprovedTask"
? ? ? ? ? name="假期審批"
? ? ? ? ? flowable:assignee="${employee}"
? ? ? ? ? flowable:candidateGroups="${departmentGroups}"/>
?????????2)優點
? ? ? ??高度靈活:可依據流程變量、業務數據、甚至自定義后端邏輯決定辦理人/組。
????????模型復用:同一流程定義可在不同場景下重用,僅需在啟動或執行中傳入不同參數。
????????無需重部署:修改分配邏輯只需傳入或修改流程變量,不必重新上傳 BPMN 文件。
????????與監聽器結合:可通過任務監聽器(Task Listener)在 create
事件中進一步增強分配策略。
? ? ? ? 3)缺點?
????????調試困難:OGNL 語法及變量來源分散,不同于模型中直觀的固定值,易出現“未知屬性”錯誤。
????????運行時開銷:每次創建任務時都要進行表達式求值,在高并發場景需評估性能影響。
????????變量依賴:若執行上下文未設置所需變量,會導致任務無法正確分配或拋錯。
????????已創建任務難更新:對于已激活的任務,僅改變流程變量并不會改變候選組,需要借助 API 手動更新身份鏈接。
③ 參數方式取舍
? ? ? ? 結合實際項目場景中的需求,我們需要高度靈活,不用頻繁部署的動態參數方式來完成啟動流程,至于復雜的表達式解析,本來也來盡量通過系統方式優化一下實踐,若有更好的方案請在評論區留言。
三、獲取動態參數解析
? ? ? ? 首先先定義一個流程,根據上文的規則定義動態參數。
? ? ? ? 在成功創建流程并發布之后,可以通過【RepositoryService】的【getBpmnModel】方法獲取流程模型,我們這模型里找找參數在哪里。
? ? ? ? 首先我們找到了用戶任務在processes的flowElementList里,繼續深入查看用戶任務。
? ? ? ? ok,我們這就找到想要的動態參數了,我們把這些參數抽取出來。
四、獲取動態參數實現
① 后端:定義查詢參數
????????這里只要部署的流程定義ID即可,我們后臺還是要根據ID重新查詢一次的
package com.ceair.entity.request;import lombok.Data;import java.io.Serial;
import java.io.Serializable;/*** @author wangbaohai* @ClassName QueryDynamicParametersReq* @description: 查詢部署流程的動態參數請求* @date 2025年04月27日* @version: 1.0.0*/
@Data
public class QueryDynamicParametersReq implements Serializable {@Serialprivate static final long serialVersionUID = 1L;/*** 流程定義ID*/private String processDefinitionId;}
② 后端:定義響應參數
????????需要的是一個表單元素list,包含表單的名稱,任務節點的名稱,動態變量的名稱(這里前端不需要但是后續要返還給后端做啟動操作的時候要用到),下拉項個數限制,下拉框數據(所有用戶或者所有角色)。
package com.ceair.entity.vo;import lombok.Data;import java.io.Serial;
import java.io.Serializable;
import java.util.List;/*** @author wangbaohai* @ClassName DynamicParametersVO* @description: 動態參數綜合返回對象VO* @date 2025年04月27日* @version: 1.0.0*/
@Data
public class DynamicParametersVO implements Serializable {@Serialprivate static final long serialVersionUID = 1L;// 表單名稱private String formName;// 動態變量節點名稱private String taskName;// 動態變量名稱private String dynamicVariableName;// 下拉選項個數限制private Integer selectLimit;// 下拉框數據private List<SelectDataVO> selectData;}
package com.ceair.entity.vo;import lombok.Data;import java.io.Serial;
import java.io.Serializable;/*** @author wangbaohai* @ClassName SelectDataVO* @description: 動態參數下拉選擇數據對象VO* @date 2025年04月27日* @version: 1.0.0*/
@Data
public class SelectDataVO implements Serializable {@Serialprivate static final long serialVersionUID = 1L;// 下拉Keyprivate String key;// 下拉labelprivate String label;// 下拉valueprivate String value;}
③ 后端:定義一個接口服務
/*** 查詢動態參數信息* <p>* 該方法用于根據給定的查詢條件獲取動態參數列表每個動態參數都封裝在一個DynamicParametersVO對象中** @param queryDynamicParametersReq 查詢動態參數的請求對象,包含了查詢動態參數所需的條件和參數* @return 返回一個List集合,集合中每個元素都是一個DynamicParametersVO對象,包含了一組動態參數信息*/
List<DynamicParametersVO> queryDynamicParameters(QueryDynamicParametersReq queryDynamicParametersReq);
④ 后端:實現接口服務
1)首先參數校驗
2)feign接口獲取下拉數據
????????一共是兩個feign接口,一個是查詢所有用戶,一個是查詢所有角色;這兩個接口大家可以自行實現,如果使用我的腳手架可以參照我的代碼,我是寫在pm-system服務中的。
Ⅰ 定義feign客戶端
package com.ceair.api;import com.ceair.entity.result.Result;
import com.ceair.entity.vo.Oauth2BasicUserVO;
import com.ceair.entity.vo.SysRoleVO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;import java.util.List;/*** @author wangbaohai* @ClassName SystemFeignClient* @description: 系統設置對外Feign接口* @date 2025年04月27日* @version: 1.0.0*/
@FeignClient(name = "pm-system")
public interface SystemFeignClient {/*** 查詢所有用戶信息* <p>* 該方法通過POST請求處理查詢所有用戶信息的請求它主要用于提供一個接口,* 讓客戶端能夠獲取系統中所有用戶的列表信息** @return 返回一個Result對象,其中包含用戶列表(Oauth2BasicUserVO類型)如果查詢成功,* 則在Result對象中包含用戶列表數據;如果查詢失敗,則在Result對象中包含錯誤信息*/@PostMapping("/systemClient/api/v1/user/queryAllUsers")Result<List<Oauth2BasicUserVO>> queryAllUsers();/*** 查詢所有角色信息* <p>* 該接口用于獲取系統中所有的角色信息,返回一個包含多個SysRoleVO對象的列表* 主要用于用戶管理、權限控制等場景** @return 包含SysRoleVO對象列表的Result對象,表示查詢結果和狀態*/@PostMapping("/systemClient/api/v1/user/queryAllRoles")Result<List<SysRoleVO>> queryAllRoles();}
package com.ceair.entity.result;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;import java.io.Serializable;/*** @author wangbaohai* @ClassName Result* @description: 公共響應類* @date 2024年11月20日* @version: 1.0.0*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {/*** 響應狀態碼*/private Integer code;/*** 響應信息*/private String message;/*** 接口是否處理成功*/private Boolean success;/*** 接口響應時攜帶的數據*/private T data;/*** 操作成功攜帶數據** @param data 數據* @param <T> 類型* @return 返回統一響應*/public static <T> Result<T> success(T data) {return new Result<>(HttpStatus.OK.value(), ("操作成功."), Boolean.TRUE, data);}/*** 操作成功不帶數據** @return 返回統一響應*/public static Result<String> success() {return new Result<>(HttpStatus.OK.value(), ("操作成功."), Boolean.TRUE, (null));}/*** 操作成功攜帶數據** @param message 成功提示消息* @param data 成功攜帶數據* @param <T> 類型* @return 返回統一響應*/public static <T> Result<T> success(String message, T data) {return new Result<>(HttpStatus.OK.value(), message, Boolean.TRUE, data);}/*** 操作失敗返回** @param message 成功提示消息* @param <T> 類型* @return 返回統一響應*/public static <T> Result<T> error(String message) {return new Result<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), message, Boolean.FALSE, (null));}/*** 操作失敗返回** @param code 錯誤碼* @param message 成功提示消息* @param <T> 類型* @return 返回統一響應*/public static <T> Result<T> error(Integer code, String message) {return new Result<>(code, message, Boolean.FALSE, (null));}/*** oauth2 問題** @param message 失敗提示消息* @param data 具體的錯誤信息* @param <T> 類型* @return 返回統一響應*/public static <T> Result<T> oauth2Error(Integer code, String message, T data) {return new Result<>(code, message, Boolean.FALSE, data);}/*** oauth2 問題** @param message 失敗提示消息* @param data 具體的錯誤信息* @param <T> 類型* @return 返回統一響應*/public static <T> Result<T> oauth2Error(String message, T data) {return new Result<>(HttpStatus.UNAUTHORIZED.value(), message, Boolean.FALSE, data);}}
package com.ceair.entity.vo;import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;/*** @author wangbaohai* @ClassName Oauth2BasicUserVO* @description: 用戶信息前端交互層對象* @date 2025年02月16日* @version: 1.0.0*/
@Data
public class Oauth2BasicUserVO implements Serializable {/*** id*/private Long id;/*** 用戶名、昵稱*/private String name;/*** 賬號*/private String account;/*** 密碼*/private String password;/*** 手機號*/private String mobile;/*** 郵箱*/private String email;/*** 頭像地址*/private String avatarUrl;/*** 是否已刪除*/private Boolean deleted;/*** 用戶來源*/private String sourceFrom;/*** 創建人id*/private Long creatorId;/*** 創建人名稱*/private String creatorName;/*** 創建時間*/private LocalDateTime createTime;/*** 修改人id*/private Long modifierId;/*** 修改人名稱*/private String modifierName;/*** 修改時間*/private LocalDateTime modifyTime;/*** 版本號*/private Integer recordVersion;/*** 擴展字段2*/private String attribute2;/*** 擴展字段3*/private String attribute3;/*** 擴展字段4*/private String attribute4;/*** 擴展字段5*/private String attribute5;/*** 擴展字段1*/private String attribute1;}
package com.ceair.entity.vo;import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;/*** @author wangbaohai* @ClassName SysRoleVO* @description: 系統角色表VO* @date 2025年02月26日* @version: 1.0.0*/
@Data
public class SysRoleVO implements Serializable {/*** 角色ID*/private Long id;/*** 角色名*/private String roleName;/*** 0:啟用,1:刪除*/private Boolean deleted;/*** 排序*/private Integer sort;/*** 創建人id*/private Long creatorId;/*** 創建人名稱*/private String creatorName;/*** 創建時間*/private LocalDateTime createTime;/*** 修改人id*/private Long modifierId;/*** 修改人名稱*/private String modifierName;/*** 修改時間*/private LocalDateTime modifyTime;/*** 版本號*/private Integer recordVersion;/*** 擴展字段2*/private String attribute2;/*** 擴展字段3*/private String attribute3;/*** 擴展字段4*/private String attribute4;/*** 擴展字段5*/private String attribute5;/*** 擴展字段1*/private String attribute1;}
Ⅱ 實現feign接口
????????先在server服務模塊引入api模塊
<!-- system-api -->
<dependency>
? ? <groupId>com.ceair</groupId>
? ? <artifactId>system-api</artifactId>
? ? <version>${project.version}</version>
</dependency>
????????然后就是實現了,這里需要注意,響應的實體都需要使用api模塊中的,避免數據轉換錯誤。
package com.ceair.feignController;import com.ceair.entity.Oauth2BasicUser;
import com.ceair.entity.SysRole;
import com.ceair.entity.result.Result;
import com.ceair.entity.vo.Oauth2BasicUserVO;
import com.ceair.entity.vo.SysRoleVO;
import com.ceair.service.IOauth2BasicUserService;
import com.ceair.service.ISysRoleService;
import com.ceair.utils.structMapper.Oauth2BasicUserStructMapper;
import com.ceair.utils.structMapper.SysRoleStructMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** @author wangbaohai* @ClassName Oauth2BasicUserFeignController* @description: 用戶管理相關feign接口* @date 2025年04月27日* @version: 1.0.0*/
@RestController
@RequestMapping("/systemClient/api/v1/user")
@RequiredArgsConstructor
@Slf4j
public class Oauth2BasicUserFeignController {private final IOauth2BasicUserService oauth2BasicUserService;private final ISysRoleService sysRoleService;/*** 查詢所有用戶的接口* 該方法通過POST請求查詢系統中所有未刪除的用戶信息,并以列表形式返回* 使用了Oauth2BasicUserStructMapper來轉換用戶實體類到API視圖對象** @return 返回一個Result對象,其中包含用戶信息列表如果查詢失敗,返回錯誤信息*/@PostMapping("/queryAllUsers")public Result<List<Oauth2BasicUserVO>> queryAllUsers() {try {// 查詢所有未刪除的用戶信息List<Oauth2BasicUser> oauth2BasicUsers = oauth2BasicUserService.lambdaQuery().eq(Oauth2BasicUser::getDeleted, false).list();// 使用 Oauth2BasicUserStructMapper 轉換輸出結果List<Oauth2BasicUserVO> oauth2BasicUserVOS =oauth2BasicUsers.stream().map(Oauth2BasicUserStructMapper.INSTANCE::toApiVO).toList();// 返回成功結果return Result.success(oauth2BasicUserVOS);} catch (Exception e) {// 記錄錯誤日志并返回錯誤結果log.error("查詢所有用戶失敗,失敗原因:{}", e.getMessage(), e);return Result.error("查詢所有用戶失敗,失敗原因:" + e.getMessage());}}/*** 處理查詢所有角色的POST請求* <p>* 該方法通過調用sysRoleService查詢所有未刪除的角色信息,并使用SysRoleStructMapper將查詢結果轉換為API輸出格式* 如果查詢過程中發生異常,將記錄錯誤日志并返回錯誤結果** @return 返回一個Result對象,其中包含查詢到的角色列表如果查詢失敗,返回錯誤信息*/@PostMapping("/queryAllRoles")public Result<List<SysRoleVO>> queryAllRoles() {try {// 查詢所有未刪除的角色信息List<SysRole> sysRoles = sysRoleService.lambdaQuery().eq(SysRole::getDeleted, false).list();// 使用 SysRoleStructMapper 轉換輸出結果List<SysRoleVO> sysRoleVOS =sysRoles.stream().map(SysRoleStructMapper.INSTANCE::toApiVO).toList();// 返回成功結果return Result.success(sysRoleVOS);} catch (Exception e) {// 記錄錯誤日志并返回錯誤結果log.error("查詢所有角色失敗,失敗原因:{}", e.getMessage(), e);return Result.error("查詢所有角色失敗,失敗原因:" + e.getMessage());}}}
Ⅲ 放開feign接口鑒權
????????注意需要配置資源服務器的放行設置,把feign接口全部放開,先不鑒權,不然heaer里要放token,這里的方案還不是最優解,后續我再優化吧。所有資源服務器配置我是放在common里的。
Ⅳ 流程服務引入feign接口
主啟動類里添加feign接口指定路徑
使用feign
3)獲取流程 Process
4) 獲取 flowElements
5)獲取所有?UserTask
6)從UserTask中獲取信息封裝響應參數
????????這里的主要思路就是獲取到所有用戶任務的信息,每個參數就是對應前端的一個表單數據行,這個表單而且應該都是下拉選項菜單,并且候選用戶和候選組都是多個的,所有需要限制用戶選擇不超過流程定義bpmnjs中屬性菜單里的參數個數,并且保留變量名稱,雖然變量名稱前端用不到,但是后續再返回給后端的時候,啟動流程的時候需要知道用戶的選擇數據需要放到哪個參數中去。
完整代碼:
/*** 查詢流程動態參數。* <p>* 該方法根據指定的流程定義ID,查詢并生成與用戶任務相關的動態參數表單數據,供前端使用。* 動態參數表單包括用戶和角色的選擇數據,用于在流程中分配任務。* <p>* 參數說明:** @param queryDynamicParametersReq 請求對象,包含流程定義ID(processDefinitionId)。* - 不可為空。* - processDefinitionId 必須為有效的非空字符串。* <p>* 返回值:* @return 返回一個 DynamicParametersVO 對象列表,包含所有用戶任務的動態參數表單數據。* 如果發生異常,則不會返回值,而是拋出相應的異常。* <p>* 異常說明:* - IllegalArgumentException: 當請求對象或流程定義ID無效時拋出。* - BusinessException: 當業務邏輯出現問題(如遠程調用失敗、流程定義元素為空等)時拋出。* - Exception: 捕獲其他未知異常,并將其包裝為 BusinessException 拋出。*/
@Override
public List<DynamicParametersVO> queryDynamicParameters(QueryDynamicParametersReq queryDynamicParametersReq) {try {// 初始化動態參數列表List<DynamicParametersVO> dynamicParametersVOList = new ArrayList<>();// 參數校驗:確保請求對象不為空if (queryDynamicParametersReq == null) {log.error("獲取流程動態參數失敗,原因:請求對象不能為空");throw new IllegalArgumentException("獲取流程動態參數失敗,原因:請求對象不能為空");}String processDefinitionId = queryDynamicParametersReq.getProcessDefinitionId();// 參數校驗:確保流程定義ID不為空或空字符串if (StringUtils.isBlank(processDefinitionId)) {log.error("獲取流程動態參數失敗,原因:流程定義ID不能為空或空字符串,流程定義ID:{}", processDefinitionId);throw new IllegalArgumentException("獲取流程動態參數失敗,原因:流程定義ID不能為空或空字符串");}// 根據流程定義ID查詢流程定義元素BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);// 調用遠程接口獲取所有用戶信息Result<List<Oauth2BasicUserVO>> userResult = systemFeignClient.queryAllUsers();if (!userResult.getSuccess() || userResult.getData() == null) {log.error("獲取流程動態參數失敗,原因:獲取所有用戶信息失敗,失敗原因:{}", userResult.getMessage());throw new BusinessException("獲取流程動態參數失敗,原因:獲取所有用戶信息失敗,失敗原因:" + userResult.getMessage());}List<SelectDataVO> userSelectDataList = convertToSelectDataVOList(userResult.getData());// 調用遠程接口獲取所有角色信息Result<List<SysRoleVO>> roleResult = systemFeignClient.queryAllRoles();if (!roleResult.getSuccess() || roleResult.getData() == null) {log.error("獲取流程動態參數失敗,原因:獲取所有角色信息失敗,失敗原因:{}", roleResult.getMessage());throw new BusinessException("獲取流程動態參數失敗,原因:獲取所有角色信息失敗,失敗原因:" + roleResult.getMessage());}List<SelectDataVO> roleSelectDataList = convertToSelectDataVOList(roleResult.getData());// 從 bpmnModel 獲取主流程元素Process process = bpmnModel.getMainProcess();if (process == null) {log.error("獲取流程動態參數失敗,原因:流程定義元素為空,流程定義ID:{}", processDefinitionId);throw new BusinessException("獲取流程動態參數失敗,原因:流程定義元素為空,流程定義ID:" + processDefinitionId);}// 獲取流程中的所有 FlowElement 元素Collection<FlowElement> flowElements = process.getFlowElements();// 處理所有用戶任務,生成動態參數表單數據flowElements.stream().filter(Objects::nonNull).filter(flowElement -> flowElement instanceof UserTask).map(UserTask.class::cast).forEach(userTask -> processUserTask(userTask, userSelectDataList, roleSelectDataList,dynamicParametersVOList));// 返回動態參數表單數據return dynamicParametersVOList;} 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);}
}/*** 檢查字符串是否以特殊模式包圍* 該方法使用正則表達式判斷字符串是否以"${"開始并以"}"結束* 這種模式常用于變量或表達式的包圍,例如"${variable}"** @param input 待檢查的字符串* @return 如果字符串符合模式則返回true,否則返回false* 空字符串或null也會返回false*/
private boolean isSurroundedRegex(String input) {// 校驗輸入是否為非空字符串if (input == null || input.isEmpty()) {return false; // 明確空字符串或 null 的處理方式}// 使用非貪婪匹配優化正則表達式性能return input.matches("\\$\\{.*?\\}");
}/*** 將給定的VO列表轉換為SelectDataVO列表,用于在選擇框中顯示* 此方法主要處理的是將不同類型的VO對象轉換為SelectDataVO對象,以便在界面上提供選擇數據* 它特別關注于處理Oauth2BasicUserVO和SysRoleVO類型的對象,并根據條件判斷是否將其包含在最終結果中** @param voList 含有各種類型VO對象的列表,可以為空* @return 轉換后的SelectDataVO列表,用于在選擇框中顯示*/
private List<SelectDataVO> convertToSelectDataVOList(List<?> voList) {return voList.stream().filter(Objects::nonNull).map(vo -> {// 檢查vo對象是否為Oauth2BasicUserVO類型,并且符合條件if (vo instanceof Oauth2BasicUserVO oauth2BasicUserVO && isEligible(oauth2BasicUserVO)) {// 創建并返回一個SelectDataVO對象,用于顯示用戶信息return createSelectDataVO(oauth2BasicUserVO.getId().toString(), oauth2BasicUserVO.getName());} else if (vo instanceof SysRoleVO sysRoleVO && isEligible(sysRoleVO)) {// 創建并返回一個SelectDataVO對象,用于顯示角色信息return createSelectDataVO(sysRoleVO.getId().toString(), sysRoleVO.getRoleName());}// 對于不符合條件的vo對象,返回nullreturn null;}).filter(Objects::nonNull).toList();
}/*** 檢查用戶是否符合資格條件* <p>* 此方法用于確定提供的用戶信息是否滿足特定條件主要檢查用戶對象是否非空,* 用戶ID是否已分配,以及用戶名是否已提供這些檢查確保在后續的認證流程中,* 用戶信息是有效且可以安全使用的** @param oauth2BasicUserVO 用戶信息對象,包含用戶的基本信息* @return 如果用戶符合所有資格條件,則返回true;否則返回false*/
private boolean isEligible(Oauth2BasicUserVO oauth2BasicUserVO) {// 確保用戶信息對象不為空return oauth2BasicUserVO != null// 確保用戶的ID已分配,即ID不為空&& oauth2BasicUserVO.getId() != null// 確保用戶名已提供,即用戶名非空且不只包含空白字符&& !StringUtils.isEmpty(oauth2BasicUserVO.getName());
}/*** 判斷一個系統角色對象是否符合資格條件* <p>* 本方法用于檢查系統角色對象是否非空、角色ID是否非空以及角色名稱是否非空* 這些檢查確保了角色對象在后續操作中具有必要的信息,從而避免空指針異常等問題** @param sysRoleVO 系統角色對象,包含角色相關信息* @return 如果角色對象非空、角色ID非空且角色名稱非空,則返回true;否則返回false*/
private boolean isEligible(SysRoleVO sysRoleVO) {// 檢查系統角色對象是否非空return sysRoleVO != null// 檢查角色ID是否非空&& sysRoleVO.getId() != null// 檢查角色名稱是否非空&& !StringUtils.isEmpty(sysRoleVO.getRoleName());
}/*** 創建SelectDataVO對象* 此方法用于初始化一個SelectDataVO實例,并設置其關鍵屬性* 選擇數據視圖對象(SelectDataVO)是用于在界面上展示選擇框的數據結構* 它需要關鍵(key)、標簽(label)和值(value)三個屬性,其中值通常與鍵相同** @param key 選擇項的唯一標識符,用于后端識別選擇項* @param label 選擇項在界面上顯示的文本* @return 返回一個已初始化的SelectDataVO對象*/
private SelectDataVO createSelectDataVO(String key, String label) {SelectDataVO selectDataVO = new SelectDataVO();selectDataVO.setKey(key);selectDataVO.setLabel(label);selectDataVO.setValue(key);return selectDataVO;
}/*** 處理用戶任務的分配和動態參數設置** @param userTask 用戶任務對象,包含任務的分配信息* @param userSelectDataList 用戶選擇數據列表,用于動態表單的設置* @param roleSelectDataList 角色選擇數據列表,用于動態表單的設置* @param dynamicParametersVOList 動態參數列表,用于存儲生成的動態表單參數*/
private void processUserTask(UserTask userTask, List<SelectDataVO> userSelectDataList,List<SelectDataVO> roleSelectDataList,List<DynamicParametersVO> dynamicParametersVOList) {// 獲取任務名稱String taskName = userTask.getName();// 獲取 assignee / candidateUsers / candidateGroupsString assignee = userTask.getAssignee();List<String> candidateUsers = userTask.getCandidateUsers();List<String> candidateGroups = userTask.getCandidateGroups();// 這里雖然 候選人/候選組 的數量只能有一個,但是可以設置多個,所以這里需要判斷一下,提示用戶在設計的時候只能填一個參數if (candidateUsers.size() > 1 || candidateGroups.size() > 1) {log.error("獲取動態參數不正確,請修改流程定義屬性,辦理人/候選組/候選用戶的參數只能有一個");throw new BusinessException("獲取動態參數不正確,請修改流程定義屬性,辦理人/候選組/候選用戶的參數只能有一個");}// 處理 assignee 設置動態表單if (!StringUtils.isEmpty(assignee) && isSurroundedRegex(assignee)) {// 創建并添加動態參數對象到列表中DynamicParametersVO dynamicParametersVO = createDynamicParametersVO(taskName, assignee, 1,userSelectDataList, "辦理人");dynamicParametersVOList.add(dynamicParametersVO);}// 處理 candidateUsers 設置動態表單processCandidates(candidateUsers, taskName, userSelectDataList, dynamicParametersVOList, "候選人");// 處理 candidateGroups 設置動態表單processCandidates(candidateGroups, taskName, roleSelectDataList, dynamicParametersVOList, "候選組");
}/*** 處理候選變量字符串列表,構建動態參數對象并添加到動態參數列表中** @param candidates 候選變量字符串列表* @param taskName 任務名稱,用于動態參數構建* @param selectDataList 選擇數據列表,用于動態參數構建* @param dynamicParametersVOList 動態參數列表,處理后將添加新的動態參數對象* @param label 動態參數的標簽,用于動態參數構建*/
private void processCandidates(List<String> candidates, String taskName, List<SelectDataVO> selectDataList,List<DynamicParametersVO> dynamicParametersVOList, String label) {// 構建變量名稱字符串,用于后續動態參數對象的創建StringBuilder variableNameBuilder = new StringBuilder();// 初始化選擇限制計數器,用于記錄有效候選變量的數量int selectLimit = 0;// 遍歷候選變量列表for (String candidate : candidates) {// 檢查候選變量是否非空且符合正則表達式包圍的條件if (!StringUtils.isEmpty(candidate) && isSurroundedRegex(candidate)) {// 如果變量名稱構建器非空,追加逗號分隔符if (!variableNameBuilder.isEmpty()) {variableNameBuilder.append(",");}// 將符合條件的候選變量追加到變量名稱構建器中variableNameBuilder.append(candidate);// 增加選擇限制計數selectLimit++;}}// 如果存在有效候選變量,創建動態參數對象并添加到列表中,并且不限制多選個數if (selectLimit > 0) {DynamicParametersVO dynamicParametersVO = createDynamicParametersVO(taskName,variableNameBuilder.toString(), 0, selectDataList, label);dynamicParametersVOList.add(dynamicParametersVO);}
}/*** 創建DynamicParametersVO對象的方法* 該方法用于根據傳入的任務名稱、動態變量名稱、選擇限制和選擇數據列表來構建一個DynamicParametersVO對象** @param taskName 任務名稱,用于標識特定的任務* @param dynamicVariableName 動態變量名稱,用于標識動態參數* @param selectLimit 選擇限制,定義用戶可以選擇的項數限制* @param selectDataList 選擇數據列表,包含用戶可以選擇的數據項* @param label 動態參數的標簽,用于描述動態參數* @return 返回構建好的DynamicParametersVO對象*/
private DynamicParametersVO createDynamicParametersVO(String taskName, String dynamicVariableName,int selectLimit, List<SelectDataVO> selectDataList,String label) {// 創建DynamicParametersVO對象實例DynamicParametersVO dynamicParametersVO = new DynamicParametersVO();// 設置表單名稱,由任務名稱和動態變量名稱組合而成,用于唯一標識該動態參數dynamicParametersVO.setFormName(taskName + "-" + label);// 設置任務名稱dynamicParametersVO.setTaskName(taskName);// 設置動態變量名稱dynamicParametersVO.setDynamicVariableName(dynamicVariableName);// 設置選擇限制dynamicParametersVO.setSelectLimit(selectLimit);// 設置選擇數據列表dynamicParametersVO.setSelectData(selectDataList);// 返回構建好的DynamicParametersVO對象return dynamicParametersVO;
}
⑤ 后端:創建接口
/*** 查詢流程定義的動態參數* <p>* 此方法接收一個QueryDynamicParametersReq對象作為請求體,用于指定查詢條件* 使用Spring Security的注解進行權限控制,只有擁有特定權限的用戶才能訪問此方法* 它調用actReProcdefService的queryDynamicParameters方法來獲取動態參數列表* 如果調用成功,返回包含DynamicParametersVO列表的Result對象* 如果調用失敗,返回一個錯誤的Result對象,包含錯誤信息** @param queryDynamicParametersReq 查詢流程動態參數的請求對象* @return 包含動態參數列表的Result對象,或包含錯誤信息的Result對象*/
@PreAuthorize("hasAnyAuthority('/api/v1/actReProcdef/queryDynamicParameters')")
@Parameter(name = "queryDynamicParametersReq", description = "查詢不俗流程動態參數", required = true)
@Operation(summary = "查詢流程定義動態參數")
@PostMapping("/queryDynamicParameters")
public Result<List<DynamicParametersVO>> queryDynamicParameters(@RequestBody QueryDynamicParametersReqqueryDynamicParametersReq) {try {// 調用服務層方法查詢動態參數return Result.success(actReProcdefService.queryDynamicParameters(queryDynamicParametersReq));} catch (Exception e) {// 記錄查詢失敗的日志log.error("查詢流程定義動態參數失敗 具體原因為 : {}", e.getMessage());// 返回查詢失敗的錯誤信息return Result.error("查詢流程定義動態參數失敗,失敗原因:" + e.getMessage());}
}
⑥ 前端:定義請求和結果數據類型
// 查詢流程啟動動態參數請求對象
export interface QueryDynamicParametersReq {processDefinitionId: string // 流程定義ID
}// 動態參數綜合返回對象 VO
export interface DynamicParametersVO {formName: string // 表單名稱taskName: string // 動態變量節點名稱dynamicVariableName: string // 動態變量名稱selectLimit: number // 下拉選項個數限制(Java Integer → number)selectData: SelectDataVO[]// 下拉框數據列表(Java List<SelectDataVO> → SelectDataVO[])dynamicVariableValue: string[]
}// 動態參數下拉選擇數據對象 VO
export interface SelectDataVO {key: string // 下拉 Key(Java String → string)label: string // 下拉 Labelvalue: string // 下拉 Value
}
⑦ 前端:封裝請求接口
// 查詢動態參數
export function queryDynamicParameters(data: QueryDynamicParametersReq) {return request.post<any>({url: '/pm-process/api/v1/actReProcdef/queryDynamicParameters',data,})
}
⑧ 前端:創建按鈕和動態表單
<el-button v-hasButton="`btn.actReProcdef.queryDynamicParameters`" type="primary" @click="onStart(scope.row)">
? 啟動
</el-button>
<!-- 啟動流程 彈出框 -->
<el-dialog v-model="showStart" title="啟動流程" width="30%"><el-form ref="dynamicFormRef" :model="dynamicForm"><el-form-itemv-for="(item, index) in dynamicForm.data":key="index":label="item.formName":prop="`data.${index}.dynamicVariableValue`":rules="{ required: true, message: '請選擇選項', trigger: ['change', 'blur'] }"><el-selectv-model="item.dynamicVariableValue"multiple placeholder="請選擇":multiple-limit="item.selectLimit > 0 ? item.selectLimit : undefined"><el-optionv-for="one in item.selectData":key="one.value":label="one.label":value="one.value"/></el-select></el-form-item></el-form><template #footer><div class="dialog-footer"><el-button @click="showStart = false">取消</el-button><el-button v-hasButton="`btn.actReProcdef.startProcess`" type="primary" @click="onSaveDynamicParameters">確認</el-button></div></template>
</el-dialog>
⑨ 前端:創建打開對話框方法
// 定義響應式數據 showStart 表示是否顯示流程定義的啟動對話框
const showStart = ref(false)
// 定義響應式數據 DynamicParametersData 表示動態參數數據
const dynamicParametersData = ref<DynamicParametersVO[]>([])
// 定義 動態參數表單數據
const dynamicForm = reactive<{ data: DynamicParametersVO[] }>({data: [], // 顯式初始化 data 屬性為一個空數組
})
// 收集 動態參數表單 實例
const dynamicFormRef = ref()
// 定義響應式數據收集 processDefinitionId 表示流程定義ID
const processDefinitionId = ref('')/*** 異步函數:在流程開始時調用* 該函數負責查詢流程定義的動態參數,并在成功時顯示流程啟動對話框* @param data 流程定義的詳細信息,用于獲取流程定義ID*/
async function onStart(data: ActReProcdefVO) {try {// 組裝查詢參數,包括流程定義 IDconst param: QueryDynamicParametersReq = {processDefinitionId: data.id,}// 調用后端接口獲取流程定義的動態參數const result: any = await queryDynamicParameters(param)// 判斷查詢結果是否成功if (result.success && result.code === 200) {// 如果成功,則更新流程定義的動態參數dynamicParametersData.value = result.data// 將數據放入 dynamicForm 表單數據中dynamicForm.data = dynamicParametersData.value// 收集流程部署定義IDprocessDefinitionId.value = data.id}// 打開 流程啟動對話框showStart.value = true}catch (error) {// 捕獲異常并提取錯誤信息let errorMessage = '未知錯誤'if (error instanceof Error) {errorMessage = error.message}// 顯示操作失敗的錯誤提示信息ElMessage({message: `查詢流程動態參數失敗: ${errorMessage || '未知錯誤'}`,type: 'error',})}
}
⑩ 添加按鈕和權限
演示
????????可以看到,這里兩個用戶任務的 辦理人。候選組。候選用戶都動態的展示出來了,并且對應的下拉數據也是有的,選擇好之后數據會保存到 【DynamicParametersVO】的【dynamicVariableValue】數組數據中,傳遞給后臺用于啟動。
五、啟動流程
① 后端:定義啟動參數
package com.ceair.entity.request;import lombok.Data;import java.io.Serial;
import java.io.Serializable;
import java.util.List;/*** @author wangbaohai* @ClassName SaveDynamicParametersReq* @description: 啟動部署流程動態參數請求對象* @date 2025年04月28日* @version: 1.0.0*/
@Data
public class StartProcessReq implements Serializable {@Serialprivate static final long serialVersionUID = 1L;// 流程定義IDprivate String processDefinitionId;// 動態參數列表private List<StartProcessDynamicParametersReq> dynamicParameters;}
package com.ceair.entity.request;import lombok.Data;import java.io.Serial;
import java.io.Serializable;
import java.util.List;/*** @author wangbaohai* @ClassName StartProcessDynamicParametersReq* @description: 啟動流程動態參數請求對象* @date 2025年04月28日* @version: 1.0.0*/
@Data
public class StartProcessDynamicParametersReq implements Serializable {@Serialprivate static final long serialVersionUID = 1L;// 動態變量名稱private String dynamicVariableName;// 動態變量值private List<String> dynamicVariableValue;}
② 后端:定義一個接口服務
/*** 啟動流程方法* 該方法接收一個啟動流程請求對象,并嘗試啟動一個新的流程實例* 主要用途是作為流程管理的一部分,允許外部系統或用戶通過提供特定的請求參數來啟動定義好的流程** @param startProcessReq 啟動流程所需的請求對象,包含啟動流程所需的所有參數和信息* @return 返回一個布爾值,表示流程是否成功啟動true表示成功啟動,false表示啟動失敗*/
Boolean startProcess(StartProcessReq startProcessReq);
③ 后端:實現接口服務
? ? ? ? 這里的主要思路還是先判空,然后將參數中的動態變量名稱去除首位包圍的【${}】符號,再傳入參數中的動態變量值組裝list,放到map中作為啟動參數。其中需要注意,辦理人只能有一個參數,但是其他都是可以設置list到參數中的。
/*** 啟動流程實例的方法** @param startProcessReq 啟動流程的請求對象,包含流程定義ID和動態參數等信息* @return 流程啟動成功返回true,否則拋出異常* @throws BusinessException 當流程啟動失敗時拋出業務異常*/
@Override
public Boolean startProcess(StartProcessReq startProcessReq) {try {// 參數校驗:確保請求對象不為空if (startProcessReq == null) {log.error("啟動流程失敗,原因:請求對象不能為空");throw new IllegalArgumentException("啟動流程失敗,原因:請求對象不能為空");}String processDefinitionId = startProcessReq.getProcessDefinitionId();// 參數校驗:確保流程定義ID不為空或空字符串if (StringUtils.isBlank(processDefinitionId)) {log.error("啟動流程失敗,原因:流程定義ID不能為空或空字符串,流程定義ID:{}", processDefinitionId);throw new IllegalArgumentException("啟動流程失敗,原因:流程定義ID不能為空或空字符串");}// 初始化 流程參數MapMap<String, Object> vars = new HashMap<>();// 獲取流程動態參數List<StartProcessDynamicParametersReq> dynamicParameters = startProcessReq.getDynamicParameters();dynamicParameters.stream().filter(Objects::nonNull).forEach(dynamicParameter -> {// 獲取 動態參數名稱String dynamicVariableName = dynamicParameter.getDynamicVariableName();if (!StringUtils.isBlank(dynamicVariableName)) {// dynamicVariableName 去除首尾的 ${} 符號,使用正則表達式String dynamicVariableKey = dynamicVariableName.replaceAll("^\\$\\{", "").replaceAll("\\}$", "");// 獲取動態參數值List<String> dynamicVariableValue = dynamicParameter.getDynamicVariableValue();// 設置參數if (dynamicVariableValue.size() == 1) {vars.put(dynamicVariableKey, dynamicVariableValue.get(0));} else if (dynamicVariableValue.size() > 1) {vars.put(dynamicVariableKey, dynamicVariableValue);} else {vars.put(dynamicVariableKey, null);}}});// 獲取當前用戶信息UserInfo userInfo = userInfoUtils.getUserInfoFromAuthentication();// 設置流程發起人,獲取當前用戶IDif (userInfo != null) {identityService.setAuthenticatedUserId(String.valueOf(userInfo.getId()));}// 指定ID和動態參數啟動流程runtimeService.startProcessInstanceById(processDefinitionId, vars);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);}
}
④ 前端:定義請求和響應
// 啟動流程請求對象
export interface StartProcessReq {processDefinitionId: stringdynamicParameters: StartProcessDynamicParametersReq[]
}// 啟動流程動態參數請求對象
export interface StartProcessDynamicParametersReq {dynamicVariableName: stringdynamicVariableValue: string[]
}
⑤ 前端:封裝請求接口
// 帶著動態參數啟動流程實例
export function startProcess(data: StartProcessReq) {return request.post<any>({url: '/pm-process/api/v1/actReProcdef/startProcess',data,})
}
⑥ 前端:創建按鈕
????????參照本文的第四步的第⑧小步,都已經畫好了,如果還是不清楚,可以在本文的后記找找到完成代碼倉庫地址和倉庫分支。
⑦ 前端:創建開始流程方法
/*** 保存動態參數并啟動流程的異步函數* 此函數首先驗證動態表單的數據有效性,然后組裝請求參數,最后調用后端接口啟動流程*/
async function onSaveDynamicParameters() {try {// 先執行表單校驗成功才能執行后續保存操作await dynamicFormRef.value.validate()// 組裝查詢參數,包括流程定義 ID和動態參數const startProcessDynamicParametersReq = ref<StartProcessDynamicParametersReq[]>([])dynamicForm.data.forEach((item: DynamicParametersVO) => {startProcessDynamicParametersReq.value.push({dynamicVariableName: item.dynamicVariableName,dynamicVariableValue: item.dynamicVariableValue,})})const startProcessReq: StartProcessReq = {processDefinitionId: processDefinitionId.value,dynamicParameters: startProcessDynamicParametersReq.value,}// 調用后端接口啟動流程實例const result: any = await startProcess(startProcessReq)// 判斷執行結果是否成功if (result.success && result.code === 200) {// 如果成功,則更新流程定義的動態參數ElMessage({message: '啟動流程成功',type: 'success',})// 關閉對話框showStart.value = false}else {// 提示操作失敗的錯誤提示信息ElMessage({message: '啟動流程失敗',type: 'error',})}}catch (error) {// 捕獲異常并提取錯誤信息let errorMessage = '請檢查'if (error instanceof Error) {errorMessage = error.message}// 顯示操作失敗的錯誤提示信息ElMessage({message: `必填項校驗失敗: ${errorMessage || '未知錯誤'}`,type: 'error',})}
}
⑧ 添加按鈕和權限
演示
????????首先我們下拉選擇參數
? ? ? ? 然后我們在后端先debug一下,可以看到將要傳入的參數和前臺選擇的是對的上的,這里參數都用的下拉框數據的ID,后續查待辦任務會更準確一些。
? ? ? ? 驗證我們的接口是執行ok的
? ? ? ? 最后再驗證一下數據庫,因為我們指定了辦理人,這個最優先,所有辦理人會直接使用 assignee到數據庫,驗證是ok的,數據對的上用戶任務1的辦理人assignee1參數。
后記
如果本文的樣例代碼各位覺得有哪里不全的話,請到本專欄的第一篇文章,最后有倉庫地址。
本文的后端分支是 process-8
本文的前端分支是 process-10