SpringBoot+Flowable 审批候选人策略设计:十余种 Strategy + Invoker,一次讲清下一关谁审
🌐 文档地址:https://ruoyioffice.com
👇👇👇 文章底部获取源码和演示地址 👇👇👇
💬 :17156169080(获取产品咨询)
流程画得再漂亮,下一关「没人可审」就全废。硬编码 assignee 只能应付 demo;真实企业要的是:按角色、按部门负责人、按发起人自选、按表单里填的「项目经理」……正确姿势是策略模式:每个规则一个 Strategy,Invoker 统一注册、计算与部署校验。

▲ 一屏看清:Invoker 注册/分发/校验 → 四类策略(组织 · 发起人 · 表单 · 兜底)→ candidateStrategy / candidateParam / validateBpmnConfig
引言:下一关谁审,才是流程真正的难题
| BPMN 写死 userId | 换人就改图、发版 |
| 只支持「指定角色」 | 发起人自选、表单字段全做不了 |
| 运行时算不出人再报错 | 单据卡死,运维救火 |
| 每种规则 if-else 堆在监听器 | 三个月后无法扩展 |
目标架构一句话:设计器写 strategy+param;运行时 Invoker 算人;部署前先校验。
一、策略枚举:把「人从哪来」编码化
BpmTaskCandidateStrategyEnum 用整型策略码区分规则,例如:
| 组织类 | 角色、部门成员、部门负责人、岗位、用户、用户组 |
| 发起人相关 | 发起人自己、发起人自选、发起人部门负责人、连续多级负责人 |
| 审批中自选 | 当前审批人指定下一节点审批人 |
| 表单类 | 表单内用户字段、表单内部门负责人 |
| 表达式 / 兜底 | 流程表达式、审批人为空 |
设计器侧把 candidateStrategy / candidateParam 写入 UserTask 扩展属性;运行时再解析。

▲ 流程模型是策略配置的入口:每个审批节点选一种候选人规则,而不是写死工号
二、Strategy 接口:统一契约
public interface BpmTaskCandidateStrategy {
BpmTaskCandidateStrategyEnum getStrategy();
void validateParam(String param);
default boolean isParamRequired() { return true; }
Set<Long> calculateUsersByTask(DelegateExecution execution, String param);
Set<Long> calculateUsersByActivity(BpmnModel model, String activityId,
String param, Long startUserId, String processDefinitionId,
Map<String, Object> processVariables);
}
每个策略一个 Spring Bean,构造 BpmTaskCandidateInvoker 时注入 List<BpmTaskCandidateStrategy>,放进 strategyMap,重复策略码直接 Assert 失败——避免两个实现抢同一个码。
三、Invoker:计算链路与三道保险
运行时核心是 calculateUsersByTask:
Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement);
String param = BpmnModelUtils.parseCandidateParam(flowElement);
Set<Long> userIds = getCandidateStrategy(strategy).calculateUsersByTask(execution, param);
removeDisableUsers(userIds); // 去掉禁用账号
if (CollUtil.isEmpty(userIds)) {
// 候选人为空 → 走 ASSIGN_EMPTY 兜底策略
userIds = getCandidateStrategy(ASSIGN_EMPTY).calculateUsersByTask(execution, param);
}
removeStartUserIfSkip(userIds, flowElement, startUserId); // 发起人跳过配置
三道保险值得抄:
自动通过 / 自动拒绝的节点直接返回空集合,不再算人。

▲ 设计器节点配置把策略落到模型;部署前 Invoker.validateBpmnConfig 会扫所有 UserTask
四、部署前校验:宁可发不出去,也不要跑到一半卡住
userTaskList.forEach(userTask -> {
// 自动通过/拒绝:跳过
Integer strategy = BpmnModelUtils.parseCandidateStrategy(userTask);
String param = BpmnModelUtils.parseCandidateParam(userTask);
if (strategy == null) {
throw exception(MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG, userTask.getName());
}
if (candidateStrategy.isParamRequired() && StrUtil.isBlank(param)) {
throw exception(MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG, userTask.getName());
}
getCandidateStrategy(strategy).validateParam(param);
});
这是产品体验关键:错配在发布时报错,而不是员工提交后卡在待办黑洞。

▲ 策略算对了,待办才会落到真人;算错了再漂亮的时间轴也救不了
五、四类策略怎么选(产品视角)
| 财务岗固定审报销 | 角色 / 岗位 |
| 「谁的单子谁领导批」 | 发起人部门负责人 / 连续多级 |
| 提交时指定审批人 | 发起人自选 |
| 表单选了项目经理 | 表单内用户字段 |
| 规则很绕、要脚本 | 流程表达式 |
| 组织变动导致暂时无人 | 审批人为空(转管理员/跳过等) |
扩展新规则时:加枚举码 → 实现 Strategy Bean → 设计器下拉加一项。Invoker 不用改。
六、技术亮点总结
| 策略模式 | Strategy + Invoker Map | 新规则可插拔 |
| 双参数 | strategy + param | 配置与计算解耦 |
| 部署校验 | validateBpmnConfig | 防无人可审上线 |
| 算人关数据权限 | @DataPermission(false) | 避免过滤过头 |
| 空人兜底 | ASSIGN_EMPTY | 流程不僵死 |
| 发起人跳过 | removeStartUserIfSkip | 减少「自己批自己」 |
七、快速体验
常见问题(FAQ)
为什么算候选人要关闭数据权限?
候选人计算是「系统找谁该审」,不是「当前登录人能看哪些数据」。若带着数据权限过滤,部门负责人可能被滤掉,流程直接断。
候选人为空一定会失败吗?
不一定。会再走「审批人为空」策略(转交管理员、自动通过等,取决于配置)。兜底策略计算时不再剔除禁用用户,防止二次清空。
发起人自选和审批人自选有什么区别?
发起人自选:提交申请时选本节点审批人。审批人自选:当前节点审批时指定下一节点审批人。时机不同。
表单内用户字段策略的 param 是什么?
通常是表单字段名;运行时从流程变量里取出用户 ID 再解析。适合「项目经理」「对接人」等随单变化的角色。
新策略要改 Flowable 引擎吗?
不用。扩展属性仍是 strategy/param;新增一个实现 BpmTaskCandidateStrategy 的 Bean 即可被 Invoker 自动注册。
结语
候选人策略的本质,是把「下一关谁审」从硬编码变成可配置、可校验、可扩展的策略族。Invoker 负责注册与兜底,设计器负责把规则写进模型——流程才能在组织变动中活下去。
你们项目里审批人是写死的,还是已经策略化了?有没有踩过「部署成功但无人可审」?欢迎评论区交流。
如果这篇对你有用,点个「在看」或收藏。
🌐 演示地址
https://ruoyioffice.com/web
📦 *GitHub 源码
https://github.com/yuqing2026/ruoyi-office
📦 Gitee 源码
https://gitee.com/yqzy1688/ruoyi-office
💬 微信:17156169080(获取产品咨询)




