流程引擎这个词在协同办公领域被说得太多,落到实处却常常只剩一句“能配流程”。这几年我做过几轮流程改造,从最基础的请假、报销,到跨部门的合同评审、供应商准入,真正决定项目成败的往往不是页面画得漂不漂亮,而是底下那套流程引擎的模型设计得对不对。这篇就围绕 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 List<String> 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角色下的人员 List<String> 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. 我个人的几条实操体会
配流程这件事,配置本身不难,难的是把业务方的口头描述翻译成精确的节点、条件和人员规则。我现在养成了一个习惯:拿到需求先不打开配置界面,拿张纸把节点顺序、每个节点的办理人、每个分支的判断条件写下来,跟业务方逐条对一遍。对完之后再动手,返工率能降一大半。很多所谓的“流程配错了”,其实是需求理解阶段就偏了。
另一个体会是测试要造真实数据。测试环境里的组织架构往往很干净,一个人一个岗位,跑起来顺风顺水。生产环境里有兼岗、有代理、有虚拟部门,这些边缘情况才是事故来源。上线前拿生产的组织快照在测试环境跑一遍,尤其是那些跨部门、跨层级的流程,能提前发现不少问题。
最后一条关于维护:每条流程上线时都写一份不超过一页的配置说明,写清楚节点划分理由、参与人算法、路由条件、兜底策略、相关扩展点的类名。这东西当时写着费劲,半年后回来改流程或者排查问题时,能救命。我见过太多流程因为没人说得清当初为什么这么配,最后只能推倒重来。