ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flowable动态审批人:规则外置与可视化配置实践

Flowable动态审批人:规则外置与可视化配置实践 1. 告别硬编码审批人先想清楚你到底要解决什么问题做Flowable开发有一段时间的朋友大概率都遇到过这种场景流程图画好了节点也配好了结果审批人写死在流程文件里比如flowable:assigneezhangsan。上线那天用户跑过来说张三调走了审批人改成李四你打开流程图改完XML重新部署再清理缓存——一天过去了。更麻烦的场景还在后面。同样是“部门经理审批”A部门的单子和B部门的单子审批人根本就不是同一个人。你要么给每个部门各画一条流程要么在Java代码里写if-else判断判断多了自己都记不住哪条规则挂在哪个节点上。项目组的单子更复杂得按发起人拉到的项目去找项目经理项目信息还在第三方系统里。这篇文章给的就是一套我实际落地过的方案用Flowable Spring Boot做后端基础前端通过可视化界面配置这个节点该找谁审批的规则把审批人的计算逻辑从流程定义里彻底剥离出来。部门领导、项目经理、按角色找人的场景全部走配置不用改代码、不用重新部署流程。适合谁看被硬编码审批人折磨的后端开发、要接Flowable但还没想清楚审批人怎么设计的架构师以及对动态审批人只有概念没有落地思路的人。我会把表结构、核心代码、前端配置交互、排查问题的方法全部展开讲踩过的坑也会一起列出来。先提醒一句动态审批人不是Flowable的一个开箱即用的功能它是一套设计的组合拳——流程引擎负责流转你负责告诉引擎每一步该找谁。搞清楚这个边界后面所有实现都顺了。2. 方案选型为什么我不建议你在XML和监听器里硬肝2.1 硬编码审批人到底硬在哪先盘点一下常见的审批人写死方式看看它们各自的坑在哪。第一种是把审批人直接写死在BPMN文件里。userTask idmanagerApprove name部门负责人审批 flowable:assigneezhangsan/这是最原始的做法。张三一旦离职整个流程体就瘸了。就算你用了flowable:cadidateUsers配候选人多对一本质上还是静态的换人的成本一点没降。第二种是写监(听器)在流程节点触发时通过Java代码动态计算审批人。思路是对的但实现上容易失控——每个需要动态算人的节点都要写一个ExecutionListener或者TaskListener规则散落在十几个类里改一条规则得发一次版。时间一长代码里全是判断部门ID等于A还是B的逻辑新人根本不敢碰。第三种是Flowable自带的表达式方式比如${departmentManagerService.getManagerByUserId(execution)}比监听器优雅一些但表达式还是写死在BPMN文件里的。只要流程定义不重新部署表达式指向的服务或者入参就无法改变。这意味着业务人员想调整规则仍然必须经过开发。这三种方式有一个共同的本质问题审批策略和流程定义绑死在一起。流程定义在Flowable里的地位是版本化、不可轻易变动的而审批策略恰恰是业务中最容易变的部分。拿不变的东西承载易变的东西自然处处难受。2.2 我最终选定的核心思路规则外置 表达式回填我的做法概括成一句话就是流程文件里只留一个固定入口统一表达式具体找谁由前端配置的规则决定。流程定义中所有需要动态决定审批人的节点不写任何具体的用户ID统一用这样一个表达式占位userTask iddeptManagerApprove name部门领导审批 flowable:assignee${dynamicAssigneeCalculator.calculate(execution, task)}/dynamicAssigneeCalculator是后端暴露出来的统一入口。它会去查一张我自定义的规则表这张表通过前端页面配置实时生效。规则表里配置了当前节点ID 某种条件比如发起人部门 → 审批人类型比如部门领导后端解析出实际审批人后返回。这样流程文件本身几乎不用改节点上的表达式永远是同一个规则变化只改数据库记录前端维护一套可视化配置界面点两下就完成一次调整彻底告别“改代码—打包—部署”的循环。2.3 为什么前端可视化是必须的不是可有可无可能会有朋友说我后端做一个配置表通过接口维护不就行了为什么非要前端可视化因为我踩过这个坑。光有后端配置接口你只是把写死代码换成了写死数据——每次规则调整还是得找开发帮忙改数据库。真正的业务价值是让运营、PMO、HR这类非技术角色也能够在界面上看懂并修改规则。可视化带来的另一个隐性好处是可验证性。前端配置界面能把规则表的内容以如果满足某条件则找某类人的形式展示出来业务人员能够直观地判断逻辑是否符合预期。后端接口配置则只能看到一堆JSON不直观也很难核对。所以我的结论是后端规则引擎是基础前端可视化配置是灵魂。两者缺一不可。3. 后端落地规则表设计 统一解析器 表达式接入3.1 建一张能覆盖80%场景的审批规则表设计动态审批人方案最重要的不是代码怎么写而是规则表怎么建。表结构设计不合理后面扩展规则类型、增加条件组合会非常痛苦。我用的核心规则表大概长这样CREATE TABLE flow_assign_rule ( id bigint(20) NOT NULL AUTO_INCREMENT, process_key varchar(64) NOT NULL COMMENT 流程定义Key, node_id varchar(64) NOT NULL COMMENT 流程节点ID对应userTask的id, rule_name varchar(128) DEFAULT NULL COMMENT 规则名称方便前端展示, condition_type varchar(32) DEFAULT NULL COMMENT 条件类型none/department/project/owner, condition_param varchar(256) DEFAULT NULL COMMENT 条件参数JSON格式存放额外参数, assignee_type varchar(32) NOT NULL COMMENT 审批人类型manager/project_manager/role/user, assignee_param varchar(256) DEFAULT NULL COMMENT 审批人参数JSON格式, enabled tinyint(1) DEFAULT 1, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_process_node (process_key, node_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审批人动态分配规则表;这张表的核心设计思想process_key node_id定位唯一场景一个流程的一个节点对应一条或多条规则。condition_type condition_param描述条件满足什么条件才走这条规则。assignee_type assignee_param描述结果满足条件后找谁审批。我实际还加了一张表的扩展——存储流程运行时某个实例里实际落定的审批人后面讲排查问题时细说。可能有人会问同一个流程节点会不会有多条规则会。比如部门领导审批这个节点本部门发起的单子找本部门领导跨部门发起的单子可能要找流程发起人的上级部门领导。这种情况我在规则表里允许同一个process_key node_id存在多条记录每条记录用condition_type区分哪种情况解析器按优先级从上到下匹配。3.2 审批人类型设计用策略模式吃住各种找人逻辑规则表里分条件类型和结果类型实际上对应了两组策略。条件类型目前我支持这些条件类型含义参数示例none无条件所有该节点的单子都走这条规则无department按发起人所在部门匹配无project按单据关联的项目匹配{projectField:businessKey}owner按某种自定义归属人匹配{ownerField:creator}结果类型的策略更重要它决定了最终找谁。我目前支持这几种结果类型含义参数示例manager部门领导{deptField:departmentId}project_manager项目经理{projectField:projectId}role指定角色下的人{roleCode:admin}user指定用户{userId:zhangsan}每一种结果类型对应一个策略类统一实现同一个接口public interface AssigneeStrategy { ListString calculate(DelegateExecution execution, RuleEntity rule); }比如manager策略的伪代码Component(managerStrategy) public class ManagerStrategy implements AssigneeStrategy { Autowired private UserService userService; Override public ListString calculate(DelegateExecution execution, RuleEntity rule) { // 找出流程发起人 String initiator (String) execution.getVariable(initiator); // 根据发起人查出部门 UserInfo user userService.getUserById(initiator); // 根据部门ID查出部门经理 ListString managers userService.listManagerByDeptId(user.getDeptId()); // 组装成Flowable需要的候选人/审批人格式返回 return managers; } }这么设计之后新增一种找谁的逻辑只需要加一个策略类改一下assignee_type参数无需改动已有的类和方法。这也是我强烈建议的做法——别在解析器里写一堆if-else策略模式在这里简直是为动态审批人量身定做的。3.3 统一解析器前端配置如何变成真正的审批人规则和策略都有了中间需要一个翻译官——根据流程实例的上下文从规则表查出匹配规则再用策略解析出具体审批人。这个解析器就是流程文件里那个统一表达式对应的方法Component(dynamicAssigneeCalculator) public class DynamicAssigneeCalculator implements Serializable { private static final long serialVersionUID 1L; Autowired private RuleService ruleService; public String calculate(DelegateExecution execution, DelegateTask task) { String processKey execution.getProcessDefinitionKey(); String nodeId task.getTaskDefinitionKey(); // 1. 查规则表获取匹配当前流程节点的启用规则多条按优先级排序 ListRuleEntity rules ruleService.selectMatchRules(processKey, nodeId); // 2. 遍历规则找到第一个满足条件的规则 for (RuleEntity rule : rules) { if (conditionMatcher.matches(execution, rule)) { // 3. 通过策略解析出具体审批人 ListString assignees strategyRouter.resolve(rule.getAssigneeType()) .calculate(execution, rule); // 4. 返回给Flowable return String.join(,, assignees); } } throw new RuntimeException(未找到匹配的审批人规则节点 nodeId); } }这里有两个重要的设计细节第一返回值用逗号拼接字符串。如果策略解析出多人Flowable会把它们作为候选人多对一处理。如果你确定一个节点只有一个审批人直接返回单值即可。第二解析器抛异常的设计。这是有意为之——规则没配全就和空指针一样应该尽早暴露。我见过很多项目在没配规则时静默地把任务分配给发起人看起来友好实际上掩盖了配置遗漏生产环境出了乱子还不知道问题在哪。3.4 Spring Boot集成时的关键配置接入这套方案时Spring Boot Flowable的集成有几个地方要特别留意。第一个是Flowable的表达式管理器需要扫描到我们的dynamicAssigneeCalculator。如果你用的是Spring Boot的自动配置Flowable默认会从Spring容器里取Bean所以DynamicAssigneeCalculator加上Component注解即可。但要注意——如果你的类实现了Serializable它有序列化ID的问题但Flowable的表达式默认在Spring环境中不会被序列化存储这个在实际运行中不需要过度担心。第二个是ProcessEngine的配置。我在实际项目中设置了严格一些的配置项flowable: database-schema-update: true async-executor-activate: falseasync-executor-activate关不关取决于你是否用了定时器、异步任务。对于纯审批流关闭它能让问题定位简单很多少一个异步维度。第三个是流程文件的部署方式。我建议在启动时用代码部署而不是放在resources/processes下由Flowable自动部署Configuration public class ProcessDeployConfig { Bean public CommandLineRunner deployProcesses(RepositoryService repositoryService) { return args - { DeploymentBuilder builder repositoryService.createDeployment() .addClasspathResource(processes/leave.bpmn20.xml) .name(请假流程); builder.deploy(); antMatcher... }; } }自己控制部署能确保每次发布时流程版本的可预期性避免自动部署目录下出现一些奇奇怪怪的旧流程文件还一直生效的问题。这个问题我踩过往resources/processes放了新的BPMN文件旧版本流程还在跑新单子已经用到新版本了两边对不上查半天才明白是版本控制的问题。4. 前端可视化配置给业务人员一把顺手的扳手4.1 配置界面的核心交互设计前端配置界面不能做成一张光秃秃的表格让用户直接填SQL字段而是要设计成业务人员看得懂的规则编辑器。我做的是两段式结构左侧是流程定义列表右侧是选中流程的节点配置面板。左侧流程列表展示所有已部署的流程定义包括流程名称、版本、部署时间。点进去之后当前流程的BPMN图直接展示出来每个节点映射到右侧的规则列表。右侧规则列表的每一行是条件 → 审批人的映射大致长这样[节点部门领导审批] 当 无条件 时审批人 部门领导依据流程发起人部门 [节点部门领导审批] 当 项目 所属项目的项目经理 时审批人 项目经理 [节点总经理审批] 当 金额 大于 50000 时审批人 角色总经理这个条件 → 审批人的结构必须完全贴合后端规则表。前端维护的是一个JSON结构提交时一次性保存{ processKey: leaveProcess, rules: [ { nodeId: deptManagerApprove, conditionType: none, assigneeType: manager, assigneeParam: {deptField: departmentId} } ] }后端收到这个结构后经过格式校验写入规则表整个过程就是一次普通的CRUD。但前端交互的难点在于业务人员对部门领导项目经理这些概念理解得很快对deptFieldassigneeType这些字段毫无概念所以前端要用下拉框、选择器把这些概念翻译成人能看懂的操作。4.2 可视化配置需要的前端组件我梳理一下实际用到的几个关键前端组件方便大家直接按图索骥条件选择器下拉框选择条件类型选按部门就出现部门选择器选按项目就出现项目选择器。项目信息可以通过接口从第三方系统拉取。审批人选择器下拉框选择结果类型选部门领导就出现部门字段映射的配置选角色就出现角色下拉框选指定用户就出现用户搜索选择器。规则排序每个节点下的多条规则允许拖拽排序因为解析器会按顺序匹配第一条命中的规则。预览按钮配置完之后允许输入一个模拟的流程发起人预览该节点会分配给谁。这个功能极大减少了业务人员的疑惑。预览功能是我特别推荐的它相当于给配置加了一个沙盒验证。业务人员配完规则点一下预览看到输出结果是张三还是李四立刻就知道配得对不对不用等到真正发起流程才知道结果。实现预览也不复杂后端把统一解析器暴露成一个HTTP接口给定流程Key、节点ID、发起人ID内部走一遍条件匹配和策略计算返回结果。4.3 如何保证前端配置与流程定义不脱节一个容易忽视的问题是流程定义会升级节点ID可能会变。前端配置界面不能对流程定义的变化视而不见。我的做法是部署新版本流程时把旧版本的所有规则复制一份到新版本的Key 新节点ID下同时标记规则来源继承自旧版本。如果新版本调整了节点ID会导致旧的规则匹配不到在前端界面上用高亮红色提示该节点在当前流程版本中不存在规则已失效。这个复制机制可以有效避免升级流程后所有审批人规则归零流程跑起来直接报错的惨剧。我就在生产环境踩过类似的问题——流程加了一个会签分支旧的规则全部失配升级后第一单就卡在节点上后台日志一片红最后紧急把规则一条条重新配置才缓过来。所以现在所有流程升级操作都会先跑一遍规则完整性校验确保每个需要配置的节点都有至少一条可用规则。5. 部门领导、项目经理自动分配两类场景的完整实现拆解5.1 扩展用户组织模型的必要性实现部门领导自动分配之前你首先得有个东西能回答谁是这个部门的领导这个问题。Flowable自身不维护组织架构所以你必须自建一套用户-部门-角色的数据模型或者对接HR系统。我的建议是做一个精简版的组织模型表至少包含用户表包含id、name、department_id、部门表包含id、name、manager_id、角色表包含id、role_code、user_id。有了这套模型策略模式里的manager和project_manager实现起来就很快了。5.2 部门领导自动分配的代码细节部门领导分配的完整逻辑是发起人发起流程 - 走到部门审批节点 - 解析器查规则 - 查发起人的部门ID - 查部门的manager_id- 作为审批人返回。有个边界情况需要考虑如果发起人自己就是部门领导呢那流程就变成自己审批自己了。实际业务中通常需要跳过当前节点直接进入下一环节。这个需求不在规则表里直接用简单配置体现我是在manager策略里增加了一个配置项skipIfInitiatorIsManagerComponent(managerStrategy) public class ManagerStrategy implements AssigneeStrategy { Override public ListString calculate(DelegateExecution execution, RuleEntity rule) { String initiator (String) execution.getVariable(initiator); UserInfo user userService.getUserById(initiator); // 从规则的参数中读取是否跳过发起人本人 JSONObject param JSON.parseObject(rule.getAssigneeParam()); boolean skipSelf param.getBooleanValue(skipSelf); ListString managers userService.listManagerByDeptId(user.getDeptId()); if (skipSelf managers.contains(initiator)) { // 这里需要配合Flowable的SequenceFlow跳转或者标记为自动完成 return Collections.emptyList(); } return managers; } }返回空列表不是一个完整的解决方案因为Flowable拿到空审批人要么直接报错要么挂起。我实际的方案是让解析器抛一个特殊的SkipNodeException在旁边监听的流程事件里捕获后自动完成当前任务引导流程往下走。这个细节比较进阶但非常实用——没有这一步你就得靠hardcode判断跳过又回到硬编码的老路了。5.3 项目经理自动分配的跨系统联动项目经理自动分配比部门领导要复杂一个层级因为项目信息往往不在流程系统里而在PM系统、OA系统或者一张Excel表里。我在规则表里用condition_type project来标记该项目节点。流程发起时的表单需要携带projectId作为流程变量。策略解析时通过HTTP接口从PM系统拉取项目负责人Component(projectManagerStrategy) public class ProjectManagerStrategy implements AssigneeStrategy { Autowired private RestTemplate restTemplate; Override public ListString calculate(DelegateExecution execution, RuleEntity rule) { JSONObject param JSON.parseObject(rule.getAssigneeParam()); String projectVar param.getString(projectVar); String projectId (String) execution.getVariable(projectVar); // 调用PM系统接口 JSONObject resp restTemplate.getForObject( http://pm-system/project/{id}/manager, JSONObject.class, projectId); return Collections.singletonList(resp.getString(userId)); } }这里踩过的坑是跨系统调用的接口超时。如果PM系统响应慢整个流程实例会被卡住任务节点处于假死状态。我是这么解决的给跨系统调用设置合理的超时时间比如3秒。加一层本地缓存项目负责人短时间内不会频繁变化缓存5分钟足够。如果调用失败降级到查本地缓存或返回该项目的创建人为兜底。这些处理都要写在策略类的异常处理代码里而不是期望统一框架来处理因为每个外部系统的降级策略都可能不同。5.4 流程变量从哪来发起人ID的三种传递姿势动态审批人的计算严重依赖当前流程是谁发起的。发起人信息怎么进来我碰到过的做法有三种启动流程时通过processInstanceBusinessKey关联业务单号再通过业务单号反查发起人。适合所有业务单都入库的场景。启动流程时手动设置execution.setVariable(initiator, userId)。最直接。通过Spring Bean从安全上下文取登录用户。适合流程系统本身有登录态的场景但异步任务里拿不到。我推荐第二种显式传递、足够稳定。而且initiator这个变量在流程多个节点的表达式中都会用到建议在解析器读不到时直接抛异常避免后续策略拿null到内部做各种隐性处理。6. 实际部署中的坑规则失效、多人串行、缓存穿透6.1 流程定义升级导致规则失配这个问题前面提到过这里详细说一次完整的故事。有一次发布新流程版本我没有先在前端界面复制旧规则而是直接把新BPMN部署上去。新版本把部门领导审批这个节点的ID从deptManagerApprove改成了deptLeaderApprove意图是更准确地表达节点含义。结果就是新单据跑到这个节点时规则表里查不到任何记录解析器直接抛异常流程阻塞。当时排查方式还算顺利——看流程实例日志发现这个节点forward不了再去查规则表发现确实没有deptLeaderApprove的记录。现在我的处理是加了一个规则完整性校验的端点流程部署完成后自动调用public ListString validateProcessRules(String processKey, String processVersion) { // 读取BPMN所有userTask节点ID // 查询规则表中该流程Key下所有节点 // 找出有节点但没配规则的节点返回警告列表 }这个校验能提前拦住大部分流程升级导致规则失配的问题。比在运行时才报错强太多了。6.2 多人审批场景是或签、会签还是依次审批动态审批人解析出来可能是多个人这时候要和业务确认这个节点的审批模式。Flowable里对应的概念是多人时每人各自审批或签、需要所有人审批会签、按顺序审批依次。或签返回多个候选人或多个审批人对应flowable:assignee多值任务可以任意一人完成其他人任务自动消失。会签需要用Flowable多实例multiInstanceLoopCharacteristicsflowable:collection指向一个集合每个元素是一个审批人。依次审批需要结合collection和elementVariable配合completionCondition来控制哪个节点结束。我的建议是动态审批人方案先解决找谁再单独解决多人怎么审。如果流程节点经常需要多人审批最好把多实例配置也纳入前端可视化的范围——节点配置项里增加审批模式字段后端根据这个字段在流程启动时创建不同的任务类型。不过也要坦白说前端的审批模式配置做起来比单纯配置审批人要复杂涉及Flowable多实例的变量注入。如果是在第一批上线我建议先把单人审批、候选人或签跑通会签场景后续再单独迭代。6.3 前端配置保存失败时常见的三类问题第一类是JSON格式校验不过。前端组装规则JSON时assigneeParam这个字段内容可能为空但后端要求必须有{}这种空对象不然JSON解析直接失败。解决方案前端把空参数统一序列化为{}后端解析时对空值做兜底。第二类是规则顺序调整后匹配结果和预期不一致。条件类型为none的规则在排序中被拖到了最后而前面的条件规则都没匹配上结果默认规则反而没命中。这个问题不是技术问题是使用规范问题。我是在前端加了排序校验强制none类型必须放在节点的最后一位并且一行提示无条件规则应放在最后面。第三类是部门选择器的数据同步延迟。有些企业的组织架构是每天凌晨从HR系统同步一次白天新增的部门在配置界面看不到。我当时的处理方式是条件选择器支持手动输入部门ID的兜底同时标记该部门与HR系统同步可能存在延迟。虽然丑了点但至少给了业务人员操作的空间。6.4 解析器性能优化别让每条任务都去查数据库动态审批人方案儿上线初期没什么问题但当流程实例数量上去后解析器的调用频率会显著上升。每次任务创建时都要执行一次表达式计算而这背后带着一次规则表查询 一次用户服务调用。我做了两件事来优化第一规则缓存。process_key node_id维度的规则变更频率极低完全可以放在本地内存缓存里TTL 5分钟。规则表更新时主动清一下缓存让改动尽快生效。第二用户和组织信息缓存。部门的manager_id改动频率同样不高加一层缓存即可。加上这两层缓存后一次动态审批人解析的耗时从几十毫秒降低到几毫秒。对于一个高频审批场景这是可以接受的。6.5 排查问题必备的审批人回看表最后强烈建议多建一张表存每次审批人的回看记录CREATE TABLE flow_assign_history ( id bigint(20) NOT NULL AUTO_INCREMENT, process_instance_id varchar(64) NOT NULL, task_id varchar(64) NOT NULL, node_id varchar(64) NOT NULL, assignee_id varchar(64) NOT NULL, assignee_name varchar(64) DEFAULT NULL, matched_rule_id bigint(20) DEFAULT NULL, calc_time datetime DEFAULT NULL, remark varchar(512) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;每次解析器算出结果后顺手往这张表里插一条记录。这样当一个任务审批人不对时你可以快速查出是因为规则没配还是条件匹配出错了还是策略计算有问题它不参与流程运行纯粹是排障用的。这个表的价值在排查为什么审批人是张三而不是李四时体现得淋漓尽致。7. 从这一版方案到更完整的审批中台写到这里核心方案的落地已经完整了。按照我个人的习惯关于这套动态审批人设计再补充几个延展方向的经验和省坑技巧。第一个是规则优先级。现在规则表里是同级的按ID排序。但如果业务复杂到需要项目优先级 部门优先级 默认优先级最好在建表时就加一个priority字段而不是靠排序来隐式表达优先级。这个字段会让配置界面和解析逻辑都清晰不少。第二个是审批人的岗位概念。很多企业的审批人是按岗位定的比如法务部实习生不能审批法务经理可以。这种情况下单纯用manager一个策略就不够了需要引入岗位 部门 人员三者关联的数据模型。方案的整体骨架不用改只是在策略层增加一种position类型。第三个是规则变更的历史追踪。前端可视化配置上线后非技术角色能改规则了这就带来一个问题改错了谁来背锅我的做法是给规则表配备操作日志每次增删改都记录操作人、操作时间和前后值对比。这不仅是审计需要也是回滚时的重要依据。还有一个容易被忽略的细节流程实例启动时截图规则版本。规则表是实时生效的同一个流程的不同实例可能在A时刻和B时刻走了不同的审批人规则。这在合规审计场景下是个问题。如果业务上有流程发起时的规则为准的要求就需要在启动流程时把当时的规则快照存起来解析器优先读取快照。最后再分享一个操作习惯新规则上线前先用预览功能模拟几条不同场景的数据确认输出符合预期再正式启用。哪怕规则很简单这个动作也不要省。我见过太多次改完就上线上线就报错的情况前端预览按钮其实就是为了干掉这种问题存在的。
返回列表