当前位置: 首页 > news >正文

Flowable7.x学习笔记(十五)动态指定用户分配参数启动工作流程

前言

        得益于之前我们的基础工程准备,我们终于可以正式启动工作流程了,在启动之前我们需要分配一下每个用户任务的用户信息,其中有三个选择:【办理人】/【候选组】/【候选用户】,我们需要将系统中的用户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

http://www.xdnf.cn/news/217045.html

相关文章:

  • AutogenStudio使用
  • 快速掌握向量数据库-Milvus探索2_集成Embedding模型
  • AI技术前沿:Function Calling、RAG与MCP的深度解析与应用实践
  • 基于PyTorch的图像分类特征提取与模型训练文档
  • 集群系统的五大核心挑战与困境解析
  • EtherCAT转CANopen方案落地:推动运动控制器与传感器通讯的工程化实践
  • CKESC Breeze 6S 40A_4S 50A FOC BEC电调测评:全新vfast 技术赋能高效精准控制
  • 低代码平台部署方案解析:百特搭四大部署方式
  • 大模型推理:Qwen3 32B vLLM Docker本地部署
  • 强化学习贝尔曼方程推导
  • 流量守门员:接口限流艺术
  • Manus AI多语言手写识别技术全解析:从模型架构到实战部署
  • JavaScript 中深拷贝浅拷贝的区别?如何实现一个深拷贝?
  • 信雅达 AI + 悦数 Graph RAG | 大模型知识管理平台在金融行业的实践
  • C# 类的基本概念(实例成员)
  • 【2024-NIPS-版权】Evaluating Copyright Takedown Methods for Language Models
  • 《云原生》核心内容梳理和分阶段学习计划
  • Alibaba第四版JDK源码学习笔记2025首次开源
  • HCIP【VLAN技术(详解)】
  • Java高频面试之并发编程-11
  • 第三部分:赋予网页灵魂 —— JavaScript(下)
  • Spring Boot - 配置管理与自动化配置进阶
  • 【Bash】可以请您解释性地说明一下“2>1”这个语法吗?
  • Windows 系统下使用 Docker 搭建Redis 集群(6 节点,带密码)
  • C++日更八股--first
  • SpringBoot应用:Docker与Kubernetes全栈实战秘籍
  • git fetch和git pull的区别
  • 域对齐是什么
  • 判断用户选择的Excel单元格区域是否跨页?
  • 力扣hot100——239.滑动窗口最大值