
流程引擎这个词在协同办公领域被说得太多落到实处却常常只剩一句“能配流程”。这几年我做过几轮流程改造从最基础的请假、报销到跨部门的合同评审、供应商准入真正决定项目成败的往往不是页面画得漂不漂亮而是底下那套流程引擎的模型设计得对不对。这篇就围绕 EKP 18 这套协同办公套件里的流程引擎把它的整体结构、核心构件、配置方法、常见坑按我自己的理解捋一遍。适合两类人看一类是刚接手流程配置的实施和运维需要一份能照着操作的说明书另一类是要在流程上做二次开发的后端得先搞清楚引擎的边界在哪哪些该改配置、哪些必须写代码。流程定义、节点类型、参与人解析、路由条件、流程变量、版本管理这几个核心概念会贯穿全文读完之后你至少能做到拿到一个审批需求知道该拆成几个节点、办理人怎么算、条件怎么写、出问题往哪儿查。1. 先把“流程引擎”这个概念拆开1.1 从一张请假单说起引擎到底在管什么很多人第一次接触流程引擎是从画一张请假单开始的员工提交直属上级审批超过三天加一级部门负责人最后人事备案。这个场景简单到用一张状态表加几个 if 就能实现所以不少人会怀疑为什么要专门上一套引擎。答案在规模上。当组织里有几百条流程、每条流程有七八个版本、每个节点有五六种办理人算法、还要支持退回、撤销、转办、加签、催办、超时升级的时候硬编码的维护成本会指数级上升。引擎做的第一件事是把“流程怎么走”从代码里抽出来变成一份可配置、可版本化、可在运行时解释的流程定义。具体来说引擎负责三类职责。第一是流转控制当前在哪个节点、下一个节点是谁、什么条件下走哪条分支。第二是参与人解析这个节点该谁办是某个人、某个岗位、某个角色的所有人还是根据表单里的项目负责人动态算出来。第三是状态与数据管理流程实例跑到哪一步、每个节点的办理意见和时间、流程变量存了什么值、附件挂在哪里。理解了这三层你就明白为什么配置一条流程时界面上永远在填三类东西节点和连线、办理人规则、表单字段权限。这三者其实是引擎三个职责在配置层的投影任何一类没配对流程都会在某个环节卡住。1.2 为什么不用状态机硬写引擎解决的四个真问题我见过一些团队用“状态字段 定时任务 一堆 if-else”来实现审批流短期确实能跑但撑不过半年。原因不在于技术能力而在于这类实现绕不开四个问题。第一是流程变更的代价。业务部门说“金额超过五万要加财务总监”硬编码就得改代码、走发布、回归测试引擎里只是在路由条件上加一条规则改完发布新版本在途实例不受影响。这个差距在流程频繁调整的组织里是决定性的。第二是组织架构的耦合。审批人很少是写死的某个人通常是“发起人的直属上级”“部门负责人”“项目组的财务 BP”。这些关系会随人事变动而变硬编码意味着每次调岗都要改程序。引擎把这层关系抽象成参与人解析链配置里写的是“关系”运行时才解析成具体的人。第三是可追溯性。审计和内部合规要求能回答“这单当时是谁批的、什么时候批的、意见是什么、有没有被退回过”。引擎天然会落办理记录硬编码则需要你自己设计并维护一整套日志表还容易漏。第四是并发与一致性。会签场景下多个审批人同时操作同一条任务退回和撤销又可能和正常提交撞在一起。引擎内部通常有任务锁和状态机校验能保证同一条任务不会被两个人同时提交成功。自己写的话这块的坑最深。所以选引擎不是图省事是把“流程”这个变化频繁的东西从“代码资产”变成“配置资产”。这个判断很重要后面所有的设计取舍都建立在这个前提上。1.3 EKP 18 流程引擎在整个平台里的位置放到 EKP 18 这套协同办公套件的整体架构里看流程引擎处在中间层往上承接表单、门户、待办中心、移动端往下依赖组织架构、权限体系、消息通知、附件存储横向还要和公文、合同、费控这些业务模块对接。这个位置决定了两件事。一是引擎不能太“重”它不该关心业务语义什么合同金额、供应商等级对引擎来说都只是流程变量业务判断应该交给表单规则和路由条件去做。二是引擎必须足够“开”因为上层业务模块的形态差异很大公文要支持多级会签和阅办分离费控要支持预算校验的自动节点合同要支持外部单位盖章回传这些都得靠扩展点和事件钩子接进去。我个人的经验是评估一套流程引擎好不好用别只看它能画多复杂的图先看三件事参与人解析能不能覆盖你组织里那些“奇怪”的汇报关系版本升级对在途实例的处理策略是不是明确待办查询在几十万条数据量下还快不快。这三点决定了你上线一年之后是轻松还是天天救火。2. 引擎的整体设计与技术选型思路2.1 元数据驱动定义、表单、权限三线分离EKP 18 的流程配置走的是元数据驱动路线一条流程由三份互相独立又互相引用的定义组成流程定义描述节点和连线表单定义描述字段和布局权限定义描述每个节点上每个字段是可见、可编辑还是隐藏。为什么要拆成三份而不是揉在一起因为它们的变更频率完全不同。表单字段可能一个季度调一次节点和路由可能一个月调一次字段权限几乎每条新流程都要重新配。揉在一起的结果是改一个字段的可见性要连带重新发布整个流程定义风险大、影响面广。拆开之后有个直接好处同一张表单可以被多条流程复用。比如“采购申请单”这张表单小额直采走三级审批大额集采走五级审批加会签表单是同一份流程定义是两份字段权限分别配置。维护成本立刻降下来。这里有个容易被忽略的细节字段权限是挂在节点上的不是挂在流程上的。也就是说同一张表单在“申请人填写”节点上金额可编辑到了“部门审批”节点金额只读到了“财务复核”节点又可能对某几个字段开放编辑比如调整预算科目。配置时一定要逐个节点过一遍别图省事整条流程用一套权限后期业务方提需求时你会返工。2.2 内核与外延为什么保留自有模型而不照搬标准做流程平台绕不开一个选择是直接采用业界通用的流程建模标准还是自建一套更贴合业务的原生模型。EKP 18 走的是后者节点类型、路由方式、参与人模型都是围绕“人机协同审批”这个主场景设计的而不是围绕“服务编排”。这个取舍我认为是合理的原因在于主场景差异太大。通用建模标准擅长描述系统间的自动调用节点是服务、连线是消息人只是其中一种特殊任务。而协同办公的主场景恰恰相反九成的节点是人在审批剩下那一成才是有系统参与的自动节点。如果硬套通用模型配置界面会变得异常抽象业务人员看不懂实施人员配起来也累。代价也有主要是互操作性。当外部系统希望通过标准接口来驱动流程时需要一层适配。EKP 18 的做法是提供统一的流程服务接口把发起、提交、退回、查询这些动作封装成平台能力外部系统调平台接口而不是直接操作引擎内部表。这个约定很重要我在项目里反复强调任何外部集成都不许直连流程实例表一旦直连后续版本升级、字段调整都会变成灾难。2.3 同步与异步主干走同步、旁支走异步流程引擎里有个必须要做的决定节点流转是同步执行还是异步执行。全同步的问题是一个节点上挂了三件耗时的事——发消息、写外部系统、生成待办任何一件慢了都会拖住提交按钮全异步的问题是用户点完提交看不到下一步待办体验很差而且状态一致性难保证。EKP 18 采用的是主干同步加旁支异步的组合。流程状态推进、任务创建、待办写入是同步的保证用户点完提交立刻能在待办里看到消息推送、外部系统回调、附件转码、报表统计这些是异步的扔进队列慢慢处理失败了重试不阻塞主链路。这个设计的实战价值在于故障隔离。我遇到过一次外部费控系统接口超时因为走的是异步审批流程照常走完只是凭证回写延迟了几分钟业务方几乎无感。如果是同步调用那天上午的审批会全部卡住排队等接口恢复。需要注意的一点是异步带来的最终一致性问题。提交成功后如果消息队列积压可能出现“流程已到下一节点但审批人没收到提醒”的情况。排查这类问题时先看队列表的堆积量别一上来就怀疑引擎丢了任务。我们在运维手册里专门写了一条待办能查到但对方说没收到通知优先查消息投递日志。2.4 版本模型定义版本与实例的绑定关系流程版本管理是最容易出事故的地方必须讲清楚。EKP 18 的模型是每次发布流程定义版本号递增流程实例在发起的那一刻绑定当时的版本之后无论定义怎么改这个实例都按它绑定的版本跑完。这个策略的好处是稳定在途实例的行为不会因为一次发布而改变。坏处是如果你发现流程画错了改完发布已经在跑的单子不会自动修正需要人工处理或者走实例迁移。我的建议是分场景处理。纯粹的界面调整或者意见栏加字段直接发新版本老实例按老版本跑完就行没必要迁移。如果改的是路由条件或者参与人规则那就要评估在途实例的数量和影响数量少就人工退回到发起节点重走数量多且影响大的得用实例迁移工具把在途实例迁到新版本迁移前一定要在测试环境完整演练一遍尤其是会签节点和并行分支迁移后容易出现任务重复创建或者分支丢失。还有个小细节流程定义的版本号是全局递增还是按流程内递增不同实现不一样。EKP 18 是按流程内递增的所以你会看到同一个 key 下面有 V1、V2、V3不同流程的版本号互不影响。做接口对接的时候传的是 key 加版本不传版本默认取最新发布版本这个默认行为要特别注意很多时候“新发起的单子走错分支”就是因为调用方没指定版本。3. 核心构件逐一拆解3.1 节点类型盘点人工、自动、路由、子流程节点是流程图的骨架EKP 18 里常用的节点可以归成四类配置时先判断需求属于哪一类能省很多返工。人工节点是最常见的需要有人来操作落到待办里。它下面还能细分普通审批节点一人办理即可、会签节点多人办理按规则汇总、传阅节点只读不阻塞流程、填写节点发起人或指定人补录数据。区分这几种的关键在于“是否阻塞”和“是否汇总”配置时选错了流程会以你意想不到的方式往下走。自动节点不需要人参与进入即执行执行完自动流转。常见用途有更新表单字段比如自动写入审批通过时间、调用外部接口比如同步到合同台账、执行脚本做数据校验。自动节点最怕的是执行失败后没有兜底我的习惯是每个自动节点都配上失败策略——要么重试有限次后转人工节点要么直接挂起并通知管理员绝不允许静默失败。路由节点本身不产生待办只负责分流和汇聚包括条件分支、并行分支、聚合节点。这部分后面单独讲。子流程节点用于把一段公共逻辑抽出来复用比如“用印申请”在合同流程和投标流程里都要走一遍。子流程分同步和异步两种同步子流程会阻塞父流程子流程走完父流程才继续异步子流程发起后就各走各的父流程不等它。选哪种取决于业务是否强依赖盖章结果影响付款就必须同步员工入职手续这类就可以异步。还有一类特殊的自由流程节点允许办理人自己指定下一处理人。这类节点适合那些无法预先定义审批路径的场景比如突发的跨部门协调事项。但我不太建议在核心业务里大量使用因为路径不可预测事后追溯和效率分析都很难做用多了流程就退化成邮件流转了。3.2 参与人解析链从组织架构到具体办理人的五级过滤参与人配置是整个流程配置里最容易出错、也最值得花时间研究的部分。EKP 18 的解析逻辑可以理解为一条过滤链从宽到窄逐级收敛直到确定最终办理人集合。第一级是对象类型人、岗位、角色、部门、组织关系。第二级是取值来源固定值、表单字段、流程变量、系统内置发起人、上级、上上级。第三级是范围限定比如限定某个部门范围内找角色或者限定某个组织层级内找岗位。第四级是排除规则排除发起人本人、排除已办理过的人、排除某个特定岗位。第五级是兜底策略算出来是空集怎么办是转给指定人、转给管理员还是直接报错终止。举个实际例子。“项目立项审批”里有个节点要项目所属事业部的财务负责人审批。配置方式是对象类型选岗位取值来源选表单字段“所属事业部”范围限定到该事业部下的“财务负责人”岗位排除规则为空兜底策略选转管理员。如果表单里事业部没填这一级就解析出空集按兜底策略转给管理员流程不会莫名卡死。这里有几个血泪教训。一是岗位没人一定会出问题配置时觉得“这个岗位肯定有人”上线三个月人家调岗了流程就卡住。所以兜底策略必须配别嫌麻烦。二是角色和岗位的语义要统一有的组织角色代表职能、岗位代表位置混着用会导致同一个人被算出来两次去重没做好的话会生成两条待办。三是“发起人的上级”这种关系型参与人遇到虚拟部门或者矩阵汇报时会解析异常上线前一定要拿真实的组织数据跑一遍别只在测试租户里点两下。3.3 路由规则条件分支、并行汇聚与按比例分流路由决定了流程的走向配置时最需要想清楚的是互斥性和完备性。互斥性指多条分支的条件不能同时为真完备性指所有可能的情况都要有分支接住。条件分支的写法通常是表达式形如“表单金额大于五万”或者“申请类型等于合同”。EKP 18 支持在路由上配条件表达式也支持配默认分支。默认分支是必须配的因为表达式可能因为字段为空、类型不匹配等原因算不出布尔值没有默认分支时流程会直接报错挂起。并行分支用于多个环节同时推进比如“技术评审”和“商务评审”同时进行两边都完成才进入下一节点。这里的关键是汇聚节点的模式是全部到达才继续还是任意一个到达就继续。选错模式的后果很严重全部到达模式下一个分支卡住整条流程就停在那儿任意到达模式下先到的分支一过后面还在办的任务就变成了孤儿任务。按比例分流在实操中偶有使用比如抽检场景按 20% 概率走复核分支。这类分支的随机性会让问题排查变得困难因为同一条流程两次跑出来的路径不一样测试时很难复现。我的建议是能用确定条件就别用随机实在需要抽检把抽检结果写成流程变量落库至少事后能查到为什么走了这条分支。3.4 流程变量的作用域与生命周期流程变量是流程运行时携带的数据表单字段、参与人计算结果、自动节点的输出都可以写进变量。变量的作用域决定了它在哪些节点可见生命周期决定了它什么时候被清理。EKP 18 的变量大致分三层。全局变量贯穿整个实例任何节点都能读比如申请人、申请部门、单据编号。节点变量只在某个节点范围内有效比如会签过程中每个办理人的意见。临时变量用于节点间的一次性传递节点流转后可能被清理。配置时一个常见误区是把所有数据都塞进全局变量。后果有两个一是实例数据膨胀流程实例表越来越大查询变慢二是变量名冲突两个节点用了同一个变量名后写的覆盖先写的排查时非常痛苦。我的做法是给变量加前缀比如 form_ 开头的是表单字段calc_ 开头的是计算字段ext_ 开头的是外部系统回传一看名字就知道来源。变量的类型也要注意。表单里取出来的金额可能是字符串参与条件表达式做数值比较时会因为类型不匹配出现意外结果。写表达式时养成显式转换的习惯宁可多写几个字符也别指望引擎帮你猜。4. 实操从零搭一条能跑通的流程4.1 环境与前置准备动手之前先确认几件事缺一样都会在中间环节卡住。第一是组织架构数据要干净。至少保证涉及的部门、岗位、人员都有正确的隶属关系虚拟部门、兼岗、代理关系这些特殊情况提前梳理清楚。我一般会先跑几条查询把要用到的岗位下的人员列出来看看。-- 确认目标岗位下的人员是否已正确挂载 SELECT p.post_name, u.user_name, u.status FROM org_post p JOIN org_post_user pu ON pu.post_id p.post_id JOIN org_user u ON u.user_id pu.user_id WHERE p.post_code IN (FIN_MGR, DEPT_HEAD) ORDER BY p.post_name, u.user_name;如果查出来某岗位人员为空或者状态是停用先补数据再配流程别指望引擎在运行时报错时再发现。第二是表单已经建好并发布。流程绑定表单时字段列表是从表单定义里读的表单没发布流程里就选不到字段。第三是流程分类和权限范围确定。哪些部门能用这条流程、发起入口挂在哪个门户这些要在发布前想清楚发布后再调整权限会影响到已经在用的用户。4.2 画流程图与配置节点属性画图这一步没什么难度难的是节点属性里的细节。以一条“采购申请”流程为例节点划分大致是发起节点、部门审批、金额判断路由、财务复核、总经理审批、结束。每个节点要配的不只是名字还有几项关键属性。节点类型决定它是不是产生待办办理人规则决定谁来办审批方式决定是一人办理、会签还是依次审批超时策略决定多久没办要催办或升级退回策略决定这个节点能不能退回、退回后回到哪。金额判断是个路由节点配两条分支加一条默认分支{ nodeKey: route_amount, routes: [ { routeKey: to_finance, name: 金额大于五万走财务复核, condition: toNumber(form_amount) 50000, target: node_finance_review }, { routeKey: to_gm, name: 金额大于二十万加成总经理, condition: toNumber(form_amount) 200000, target: node_gm_approve }, { routeKey: to_end, name: 默认分支, isDefault: true, target: node_end } ] }注意这里条件的顺序金额大于二十万时前面两条都成立实际执行时取第一条命中的分支所以要把范围小的条件放前面或者把条件写得更精确一些。这个顺序问题我在至少三个项目里见过有人踩。4.3 表单绑定与字段权限控制表单绑定流程后逐个节点配字段权限。以一个“费用报销”流程为例我一般列一张表来对避免漏配。节点可编辑字段只读字段隐藏字段填写申请全部业务字段、附件单据编号、申请人审批意见、财务科目直属上级审批审批意见业务字段、附件财务科目财务复核财务科目、预算科目、审批意见业务字段、金额无结束无全部无配完之后一定要走一遍全流程试跑模拟不同角色登录确认该看到的能看到、该改的能改。我踩过一次坑财务复核节点把金额设成只读但业务要求财务可以调减金额结果上线第一天就被打回。字段权限这种事配置界面上看不出来只有真的走一遍才发现。还有一点隐藏字段不等于后台没值。有些实现里隐藏只是前端不渲染后端变量仍然存在条件表达式照样能读到。如果你希望某个字段在某个节点彻底不参与计算得在流程变量里显式清空别只靠隐藏。4.4 参与人与路由公式的配置示例参与人配置前面讲了五级过滤这里给一个具体写法。假设财务复核节点要由“申请部门所属事业部”的财务 BP 办理配置思路是对象类型选角色取值来源选表单字段“所属事业部”范围限定到该事业部兜底转财务共享中心管理员。如果内置的参与人类型覆盖不了就得用扩展点。EKP 18 支持自定义参与人解析器实现一个接口即可返回办理人 ID 列表。public class ProjectFinanceBpResolver implements ParticipantResolver { Override public ListString resolve(ProcessContext ctx) { // 从流程变量取项目编号 String projectId (String) ctx.getVariable(form_project_id); if (StringUtils.isBlank(projectId)) { return Collections.emptyList(); } // 查项目主数据拿到归属事业部 ProjectInfo project projectService.get(projectId); if (project null || StringUtils.isBlank(project.getBuCode())) { return Collections.emptyList(); } // 按事业部查财务BP角色下的人员 ListString users orgService.findUsersByRoleAndBu(FIN_BP, project.getBuCode()); // 去重避免兼岗导致重复待办 return users.stream().distinct().collect(Collectors.toList()); } }这段代码有两个细节值得说。一是返回空列表时的行为引擎会走兜底策略所以兜底必须配二是去重兼岗的人很容易被算出来两次不去重就生成两条一模一样的待办办理人点完一条另一条还挂着流程永远走不完。4.5 发布、试跑与实例数据核对配置完成后先保存草稿用测试账号发起几条不同金额的单子覆盖每一条分支。跑通之后再去数据库核对落地数据这一步很多人跳过出问题时才发现数据不对。-- 核对实例、任务、变量的落库情况 SELECT i.instance_id, i.def_key, i.def_version, i.status, t.node_key, t.assignee_id, t.status AS task_status, v.var_name, v.var_value FROM wf_instance i LEFT JOIN wf_task t ON t.instance_id i.instance_id LEFT JOIN wf_variable v ON v.instance_id i.instance_id WHERE i.instance_id TEST-20240501-0001 ORDER BY t.create_time, v.var_name;重点看三样实例绑定的版本号是不是你刚发布的那个任务里的 assignee 是不是你预期的人变量里的金额类型是不是数值。这三样对了基本就能放心发到生产。生产发布建议错峰别在上班第一分钟发万一有问题早高峰的发起量会把影响放大好几倍。5. 高频问题与排查手册5.1 流程走不动路由选不到人、条件算不出最常见的故障是“提交后没动静”。排查顺序我一般固定为四步从下往上找。先看实例状态是正常运行、挂起还是异常终止。挂起状态通常是自动节点执行失败或参与人解析为空触发的兜底。再看当前节点的任务是否存在。任务表里有记录但办理人没收到待办问题在待办推送任务表里压根没记录问题在参与人解析或路由。第三看路由条件的求值结果。条件表达式里引用的变量如果为空很多实现会直接抛异常而不是返回 false表现就是流程停在路由节点不动。这时候去日志里搜表达式求值相关的报错基本能定位到是哪个变量缺值。第四看参与人解析结果。把解析链一级一级往回拆对象类型对不对、取值来源有没有值、范围限定是不是限得太死、排除规则有没有把唯一的人排掉。现象高概率原因快速验证方式提交后无待办生成参与人解析为空触发兜底查节点变量与解析日志卡在路由节点条件表达式变量为空或类型不符打印变量值与求值结果待办生成了但收不到提醒消息异步投递积压或失败查消息投递日志与队列堆积会签节点一直不结束汇聚模式配置错误或存在孤儿任务查该节点所有任务状态退回后重复生成待办退回策略未清理原任务查任务表同节点多条记录5.2 会签、加签、转办的典型坑会签是最容易出问题的地方本质原因是它把“一条任务”变成了“一组任务”状态汇总逻辑复杂。第一个坑是会签比例算错。比如设置“超过半数同意即通过”五个人会签两人同意两人反对一人未办这时候算不算通过不同实现对“未办”的处理不一样有的算弃权有的直接阻塞。配置前一定要问清楚业务方的真实意图别想当然。第二个坑是加签后的顺序问题。前加签、后加签、并行加签语义完全不同。前加签是插在当前办理人之前办完才轮到原办理人后加签是当前办理人办完后再加一个人并行加签是同时生成两条任务。加签用错事后追溯时会发现办理顺序和业务理解完全对不上。第三个坑是转办和代理混淆。转办是把这条任务彻底交给别人自己不再持有代理是自己不在时临时委托回来后还能看到。这两个在数据上的表现不一样转办后原办理人查不到这条任务代理则两条记录都关联。用户说“我把单子转给他了但他没收到”先确认他做的是转办还是代理很多误会来自这里。第四个坑是撤销与退回的边界。撤销是发起人把整条流程收回通常在第一个节点还没办理时可用退回是把流程退到前面某个节点重走。撤销后重新发起会生成新实例退回则是在原实例上继续两者的数据轨迹完全不同做统计分析时要区分开。5.3 版本升级后老实例行为异常版本发布后收到“老单子走错了”的反馈先别急着改代码确认三件事这个实例绑定的版本号是多少、新版本改了什么、发布时是否执行了实例迁移。如果实例绑的还是老版本那它走老逻辑是正常的不是 bug。如果确实需要让它在途实例按新规则跑就得做迁移。迁移的注意点迁移前备份实例与任务数据迁移后核对每个在途实例的当前节点在新版本里是否还存在节点 key 被删掉的话实例会悬空会签节点迁移后要检查任务是否需要补齐或合并。我的经验是节点 key 一旦发布就不要删哪怕业务上不用了也保留在定义里标记为废弃。删 key 是迁移事故的头号原因。5.4 性能问题实例膨胀与待办查询变慢流程跑了一两年之后最常见的性能抱怨是“我的待办打开要好几秒”。原因通常不是引擎慢是数据量大了而索引没跟上。待办查询的核心条件是“办理人 任务状态”这两个字段必须有联合索引。另外实例表和任务表建议按月或按季度做归档历史数据迁到归档库主库只保留近一年的活跃数据。归档策略要在项目初期就定别等数据涨到千万级才想起来。还有一类隐性性能问题来自参与人解析。如果某个节点配了复杂的组织关系查询每次任务创建都要跑一遍全表扫描高并发发起时数据库压力会很明显。解决办法是给组织查询加缓存并在组织架构变更时主动失效缓存。这个优化做过一次待办生成的平均耗时从几百毫秒降到几十毫秒效果很直观。6. 二次开发与外部系统集成6.1 常用扩展点与事件钩子EKP 18 的流程引擎开放了几类扩展点选对了能少写很多代码。参与人解析器用于自定义办理人算法路由条件扩展用于内置表达式覆盖不了的判断逻辑自动节点处理器用于在节点上执行自定义业务动作事件监听器用于在流程发起、节点进入、任务完成、流程结束这些时机插入业务逻辑。事件监听器是最常用的但要注意同步事件里别做重活。流程发起后监听器里同步调用外部接口接口一慢整个发起就卡住。我的做法是监听器里只做轻量级的落库和消息投递真正的业务处理交给异步消费者。Component public class ProcessEndListener implements ProcessEventListener { Override public void onEvent(ProcessEvent event) { if (!PROCESS_END.equals(event.getEventType())) { return; } // 只投递消息不做耗时操作 mqProducer.send(wf.process.finished, event.getInstanceId()); } }6.2 外部系统回调与幂等设计流程和外部系统对接最容易忽略的是幂等。消息可能重复投递接口可能被重试同一笔业务被处理两次的后果往往比不处理更严重。做法很简单每次回调带一个业务唯一键消费端先查再处理处理完记录状态。流程侧在发起时就把这个唯一键写进流程变量后续所有回调都带上它。public void handleCallback(String bizKey, String result) { // 先查是否已处理 if (callbackLogService.exists(bizKey)) { log.info(重复回调忽略。bizKey{}, bizKey); return; } // 加唯一索引兜底防止并发重复 callbackLogService.save(bizKey, result); // 执行业务动作 businessService.applyResult(bizKey, result); }6.3 多端一致性待办、消息与移动端现在审批大部分在移动端完成多端一致性就成了必须考虑的问题。核心是待办数据只有一个来源PC 端和移动端都从同一套待办服务取数不要各自维护一份。已办、待办、抄送这些状态变更要实时同步否则会出现手机上批完了电脑上还显示待办的情况。消息推送这块要注意去重和聚合。同一个人短时间内收到十几条待办提醒体验很差。我们一般按流程类型和发起人做聚合比如“你有 3 条采购申请待审批”点进去看列表。另外推送失败要有补偿别让用户因为没收到提醒而超时。7. 我个人的几条实操体会配流程这件事配置本身不难难的是把业务方的口头描述翻译成精确的节点、条件和人员规则。我现在养成了一个习惯拿到需求先不打开配置界面拿张纸把节点顺序、每个节点的办理人、每个分支的判断条件写下来跟业务方逐条对一遍。对完之后再动手返工率能降一大半。很多所谓的“流程配错了”其实是需求理解阶段就偏了。另一个体会是测试要造真实数据。测试环境里的组织架构往往很干净一个人一个岗位跑起来顺风顺水。生产环境里有兼岗、有代理、有虚拟部门这些边缘情况才是事故来源。上线前拿生产的组织快照在测试环境跑一遍尤其是那些跨部门、跨层级的流程能提前发现不少问题。最后一条关于维护每条流程上线时都写一份不超过一页的配置说明写清楚节点划分理由、参与人算法、路由条件、兜底策略、相关扩展点的类名。这东西当时写着费劲半年后回来改流程或者排查问题时能救命。我见过太多流程因为没人说得清当初为什么这么配最后只能推倒重来。