改了工作流引擎的流程定义,在跑的审批单有的跟着变、有的不变

作者:mldong日期:2026/9/21

做过工作流引擎的,都遇到过这么一个瞬间:

周一把审批流程从"两级审批"改成"三级审批",发布了。周二早上,业务同学问:昨天那 20 张还在审批中的单子,现在走两级还是三级?

如果你的引擎回答不了这个问题,或者回答得很随意——那多半是设计时就没想过"定义"和"正在用这份定义跑着的实例"是两回事。

这篇文章做的事,就是把这个问题在 jeeflow 里拆到底:一条在跑的审批单,它到底"记着"流程定义的什么;改了定义之后,下一次它走到引擎时,引擎读到的到底是新版还是旧版。拆完你会发现一个反直觉的结论——答案不是"都走新版",也不是"都走旧版",而是取决于你当时点的是哪个按钮

一、先分清两个东西:设计稿,和流程定义

jeeflow 里,"一份流程"在数据库里其实分住在两张表里,对应两个完全不同的东西:

  • 设计稿(草稿)wf_process_design + wf_process_design_his。设计器里画的东西、改了又改的东西,都存在这里。his 表是快照历史,你每保存一次就留一个版本。
  • 流程定义(发布产物)wf_process_define。引擎真正拿去跑审批的东西,带一个 version 列。

规范里这句话是硬约定(06-facade.md):

设计稿(草稿)≠ 定义(发布产物):save/updateDefine 存草稿快照(历史表),deploy 才生成流程定义。设计稿内容变更后 isDeployed 自动置 0(防误用旧定义)。

翻译成人话:你在设计器里画得再欢,不点"发布",引擎一无所知。 而且你一旦在设计器里改了内容,那份"已发布"的标记会自动被抹掉——这是防"你以为改了已经生效"的护栏,isDeployed=0 意味着"当前草稿和线上跑的那版对不上了"。

1// JeeflowFacade.java:897  processDesign/updateDefine
2// 内容快照变更  置为未部署(对齐 boot3 updateDefine 语义)
3if (contentBytes(args) != null) design.setIsDeployed(0);
4

所以"设计稿 → 定义"是一次发布动作,不是自动同步。这一步是后文所有版本行为的分界。

二、两个按钮,两种改法:deployredeploy

发布有两条路,它们对"版本号"和"定义 id"的处理不一样,这正是后文在跑的单子走向不同的根源。

deploy:发一个新版本,拿到一个新 id

deploy 的语义是"按流程名(name)找最新那条定义,有就把版本号 +1 插一条新记录,没有就从 0 起":

1// JeeflowFacade.java:1202  deploy 版本管理(对齐 boot3)
2private Long saveDeployedDefine(ProcessModel model, byte[] bytes) {
3    //  name 查最新定义
4    PageResult<...> page = repository.pageDefines(query);
5    int version = 0;
6    if (page.getRows() != null && !page.getRows().isEmpty()) {
7        Integer latest = page.getRows().get(0).getVersion();
8        version = (latest != null ? latest : 0) + 1;   //  version+1
9    }
10    def.setVersion(version);
11    repository.saveDefine(def);        //  插一条新记录,新 id
12    return def.getId();                //  返回新 id
13}
14

Go 侧同一语义(facade.go:275):version = latest.Version + 1

关键在最后一行:deploy 返回的是一条新记录的 id。旧记录还在库里躺着(所以 wf_process_define 表里同一个 name 会有 v0、v1、v2… 多行),新版本是一个全新的 id

redeploy:原地改内容,id 和版本号都不动

redeploy(重新发布)的语义完全反过来——它不换行,就在原来那条记录上把内容(content)覆盖掉,版本号保持原样:

1// JeeflowFacade.java:1005  designRedeploy  name 取最新定义
2IProcessRepository.DefineRow last = page.getRows().get(0);
3ProcessInstance.ProcessDefine def = new ProcessInstance.ProcessDefine();
4def.setId(last.getId());          //  还是原来那条 id
5def.setContent(bytes);            //  只覆盖内容
6// issues/59:保留原 version(替换语义,不递增)
7def.setVersion(last.getVersion());
8repository.updateDefine(def);     //  UPDATE 原记录,不插新行
9

这里那行注释值得单独看:issues/59。这是引擎里一个真实修过的 bug——redeploy 本该"原地替换、版本号不变",但某个版本漏设了 version 字段,被 JDBC 兜底逻辑误写成了 1,导致一次原地重发把版本号从 3 打回了 1。这个 bug 本身说明一件事:redeploy 的设计意图是"换内容、不换身份",版本号和 id 都是"身份"的一部分,不能被顺带改掉。

两个按钮一句话对比:

deployredeploy
记录插新行覆盖原行
版本号+1不变
定义 id新的不变
语义发新版本热改当前版内容

三、一条在跑的单子,"记着"流程定义的什么

要回答"在跑的单子走哪版",得先知道它记了定义的什么。

答案有点反直觉:它不记版本号,只记 id。 看实例表的建表语句(规范 01-data-model.md,各语言建表都从这一份派生):

1CREATE TABLE wf_process_instance (
2    id                  BIGINT PRIMARY KEY,
3    process_define_id   BIGINT NOT NULL COMMENT '流程定义ID',   -- 只有 id
4    state               INT DEFAULT 10,
5    ...
6) COMMENT='流程实例表';
7

对比一下定义表,version 列只在 wf_process_define 上:

1CREATE TABLE wf_process_define (
2    id      BIGINT PRIMARY KEY,
3    name    VARCHAR(100) NOT NULL,
4    content BLOB,
5    version INT DEFAULT 0,     -- 版本号只在定义表
6    ...
7);
8

实例表里压根没有 version 这一列。 一条在跑的审批单,跟流程定义的全部关系就是 process_define_id 这一个外键。它不快照版本号,不快照流程内容——它只记"我这条单是按哪条定义发起的"。

那引擎执行的时候拿什么跑?答案是:每次执行,都按这个 id 现查定义、现解析。 Java 引擎在发起时(JeeflowEngineImpl:72):

1ProcessInstance.ProcessDefine define = repository.findDefineById(defineId);
2ProcessModel model = ModelParser.parse(define.getContent());   // 解析内容
3

而在每一次办理任务时(JeeflowEngineImpl:244,这才是关键的循环):

1ProcessInstance instance = repository.findInstanceById(task.getProcessInstanceId());
2ProcessInstance.ProcessDefine define = repository.findDefineById(instance.getDefineId());
3ProcessModel model = ModelParser.parse(define.getContent());
4

注意是 findDefineById(instance.getDefineId())——按 id 查,不是按"我发起时那个版本"查。Go 引擎一模一样(engine_impl.go:257):

1def, _ := e.repo.FindDefineByID(ctx, inst.DefineID)
2

把这两条事实放在一起,结论就自己浮出来了。

四、所以,在跑的单子到底走哪一版

引擎每次执行都按实例记的那个 define_id 现查定义内容——那么"走哪一版"完全取决于:你改定义的动作,有没有换掉这个 id

  • 你用 redeploy 改了流程:id 不变、版本号不变,只有 content 变了。在跑的单子记的 id 没变,但引擎下一次按这个 id 现查,读到的是你刚改过的内容。→ 在跑的单子跟着变。
  • 你用 deploy 发了新版本:新记录、新 id、版本号 +1。在跑的单子记的还是旧 id,引擎按旧 id 查,读到的还是旧内容。→ 在跑的单子不受影响,走旧版。

用开头那个场景收个尾:周一你把"两级审批"改成"三级审批"——

  • 若你走的是 redeploy(原地改那一条):昨天那 20 张在跑的单子,今天批到下一步时,会开始按三级审批走。业务同学昨天看到的待办,今天多了一个审批环节。
  • 若你走的是 deploy(发 v1 新版本):昨天那 20 张单子继续按两级审批跑完,只有今天之后新发起的单子才走三级。

同一次改动,两个按钮,在跑的单子走向相反。 这就是"有的跟着变、有的不变"的完整机理,而且它不是 bug——是"引擎按 id 现查定义"这个设计 + "两个按钮对 id 的不同处理"叠加出来的必然结果。

五、redeploy 是能力,也是刀口

上面的结论顺带回答了一个更尖锐的问题:redeploy 让"改流程不用等单子跑完"成了可能——这既是工作流引擎里很受欢迎的热更能力,也是一把刀口。

它危险的地方在于:你 redeploy 改完内容的那一刻,所有钉在这个 id 上的在跑实例,下一次执行都会读到新结构。如果你的新结构把某个审批节点删了、把审批人换了、把会签比例改了——那些正在半路的单子会按新规则继续走。有的场景你正想要这个效果(线上紧急加一个审批人),有的场景这是事故(在跑的报销单突然多了个环节,财务懵了)。

反过来,deploy 发新版本因为换了 id,天然把"在跑的"和"新发起的"隔开了——老单子安安稳稳按老版跑完,新单子走新版。这是更接近"发布 = 不可变快照"的心智模型,代价是你无法热改在跑的单子。

两个按钮其实对应两种工程取舍,选哪个取决于你对"改动影响面"的偏好:

  • 想让改动立刻作用到存量在跑的单子(热更)→ redeploy,但要清楚你在改所有存量的运行轨迹。
  • 想让改动只影响新发起的单子(版本隔离)→ deploy,老单子按老版自然走完。

六、还有两件事,容易被忽略

1. "按名字取最新"是另一条隐线。 引擎里多处用 getLastByName(按流程名取最新版本)来决定"新发起的单子用哪条定义"。也就是说,发起路径认的是"这个名字的最新定义",而运行路径认的是"这条实例钉住的那个 id"。这两条线在大多数时候指向同一条记录(最新且未被替换),但一旦你 deploy 出新版本,发起会指到新 id、存量还在跑旧 id——"新发起的"和"在跑的"从此分叉,这正是上一节说的版本隔离的由来。

2. 定义被禁用(upAndDown state=0)会拦住发起,但不拦在跑的。 processDefine/upAndDown 把定义 state 置 0 后,前端"发起申请"页靠 listByType 返回的 processDefineState 控制发起按钮(state=0 时不可发起)——这是发起路径的硬依赖。但一条已经发起、正在跑的实例,它的运行路径是按 id 现查定义内容来执行的,并不会因为定义被停用而停下。所以"停用"拦的是新单,不是老单——这也是同一个"发起认 name、运行认 id"设计的延伸。

写在最后

把这条链捋一遍:

1设计稿(his 快照)
2   └─ deploy ──→ 定义(新行、version+1、新 id)
3   └─ redeploy ─→ 定义(原行、version 不变、内容覆盖)
4                          
5        实例只记 process_define_id(不记 version)
6                          
7        引擎每次执行:findDefineById(实例.id) 现查现解析
8                          
9   redeploy 改过  在跑的单子跟着变
10   deploy 发新版  在跑的单子走旧版
11

一条在跑的审批单,它记的永远只是"我按哪条定义发起的"这一个 id;而引擎每次执行都按这个 id 现查内容。所以改了定义之后它走哪一版,不由"版本号"决定,而由"你的改动有没有换掉这个 id"决定——redeploy 换内容不换 id,所以在跑的单子跟着变;deploy 换新 id,所以在跑的单子被隔在旧版里。

如果你的引擎被问"改了流程,在跑的单子走哪版"时答不上来,多半是"定义"和"实例"在它的模型里还没分开——这条 process_define_id 外键,就是那个分界的物理体现。

参考资料(往期相关篇目与链接):


改了工作流引擎的流程定义,在跑的审批单有的跟着变、有的不变》 是转载文章,点击查看原文


相关推荐


Python 进阶:海象运算符与模式匹配,让你的代码更优雅!
90后晨仔2026/9/13

摘要:Python 3.8 引入的海象运算符 := 和 3.10 引入的模式匹配 match-case,是近年来 Python 语法层面最重要的两次革新。本文将从实战角度出发,详解这两个特性的核心用法、最佳实践及避坑指南,助你写出更简洁、更具表现力的 Pythonic 代码。 💡 前言 作为一名 Python 开发者,你是否经历过以下场景: 为了在 if 判断后复用某个计算结果,不得不提前写一行赋值语句; 面对复杂的嵌套数据结构,写了一长串 if-elif-else 来判断类型和解包数据,


NestJS 12升级踩坑:从Webpack到Rspack,我折腾了一整个周末
Flynt2026/9/5

说实话,看到NestJS 12的changelog时我是有点兴奋的。 8月27号发布,ESM-first、Rspack替代Webpack、Standard Schema验证、@nestjs/observe原生可观测性——每一条都戳在痛点上。特别是Rspack替代Webpack那条,我心想:终于不用等turbopack慢吞吞地编译了。 然后我花了整个周末才把项目跑起来。 先说下项目情况:一个中等体量的B端后台系统,NestJS 11.2.0 + Webpack monorepo架构,用了NATS消


别把豆包当聊天用了,现在的豆包和以前不一样了
程序员晓凡2026/8/28

大家好,我是晓凡。 豆包算是全民 AI 应用了,上班族、老人、小孩,手机里基本人手一个。我很早就给爸妈装上了,他们用得特别顺手,用他们的话说,豆包正好用,什么都懂。以前手机字体怎么调大、照片怎么发到家庭群、体检报告上的箭头是什么意思,这些事他们都要打电话来问我,现在直接跟豆包语音聊,问完自己就能搞定。 不过最近我才发现,豆包又悄悄更新了一大波东西:能操作本地电脑、使用浏览器,支持调用 Skills 技能和定时任务,内置了 Office 办公套件,还支持专业的图片视频设计。 不知道你是不是和我一样


Android 架构进阶:为什么项目越大,越需要把对象创建权拿走
潜龙勿用之化骨龙2026/8/20

刚开始写 Android 项目的时候,我其实不太理解 DI 有什么必要。 一个对象而已: val repository = UserRepositoryImpl() 直接创建不就完了吗? 如果只是一个小项目,确实没什么问题。 甚至我觉得,这时候为了 Hilt、Koin 再引入一套依赖注入体系,反而有点重。 真正让我改变想法的,是项目开始变大以后。 你会发现,项目变复杂以后,麻烦来了: 到处都是创建对象。 一、项目小的时候,直接创建对象没有问题 比如: class UserRepository


Java对象头Mark Word状态与锁膨胀过程剖析
wuminyu2026/8/6

Java对象头Mark Word状态与锁膨胀过程剖析 前言对象头Mark Word状态与锁膨胀过程剖析1. 64位 HotSpot JVM 下 Mark Word 内存布局64位 JVM Mark Word 位结构图表C++ 头文件位常量定义 (`markOop.hpp`) 2. 状态机迁移全景与锁演进路线3. 偏向锁(Biased Locking)的加锁与撤销源码解析3.1 偏向锁的快速进入(Fast Path)3.2 偏向锁的撤销(Revocation) 4. 轻量级锁(Lig


2026 哪个远程控制软件好用?国内海外 6 款主流远程软件深度测评(附评分表与选型建议)
禁止默2026/7/28

2026 年了,远程控制软件早就不是"偶尔帮老妈修电脑"的低频工具了——居家连公司工位改方案、出差路上用平板回邮件、周末秒回 OA、被控端跑训练任务、远程串流玩 3A,这些场景已经把远程软件变成了天天开的生产力底座。 但"哪个好用"这个问题在 2026 年变得更难回答了,原因是市场出现了三条暗线: 国内三巨头分层明显:UU 远程靠网易的网络底子杀进来,把 4K/144fps、端口映射、远程终端全做成了免费;ToDesk 免费版开始卡时长;向日葵免费版广告越塞越多,并且开始排队收费。


让家里网速有据可查:用MySpeed做一个24小时测速看板
是店小二呀2026/7/20

前言 很多人办理宽带时看到的是500M、1000M,可真正用起来却不是那么回事。晚上看视频卡顿,家里多人同时上网就变慢,联系运营商时,对方一句“后台检测正常”,问题往往就很难继续推进。 问题不一定出在你不会测速,而是普通测速方式很难留下连续证据。偶尔打开第三方测速网站测一次,只能说明当时那一刻的结果,不能反映一天内不同时段的波动,也很难判断是不是晚高峰、Wi-Fi环境或线路质量造成的影响。 如果家里本来就有群晖NAS,可以把它变成一台长期在线的测速服务器。MySpeed就是适合这种场景的轻


PDF解析实现
马里马里奥-2026/7/12

1. 项目背景与需求分析 在现代Web应用中,PDF 文档处理是一个常见但复杂的需求。无论是企业 OA 系统、在线教育平台还是知识管理工具,都需要能够高效解析 PDF 内容。本次作业要求实现一个中间件,专门处理前端上传的PDF文件,具体要求如下: 输入格式:接收 Base64 编码的 PDF 数据核心功能:将 Base64 转换为正常 PDF 文件并提取文本内容输出要求:将提取的文本内容完整放入 system_message 中,作为后续对话的上下文扩展目标:最终版本应支持上传或发送包含完整


【Agent 学习日记】从问题到答案:RAG系统完整处理流程与核心机制深度拆解
小假是真的2026/7/4

目录 🍬前言 🍬一、 RAG 系统全流程总览(宏观视角) 🍬二、 离线预处理:决定 RAG 效果的上限 🍬三、 在线推理第一步:问题是如何被“拆解”的?(Query 拆解) 🍬四、 向量检索与重排序:从“大海捞针”到“精准定位” 🍬五、 大模型(LLM)在 RAG 中到底负责什么? 🍬六、 最终输出:不仅是“答案” 🍬七、 关键痛点与优化方向(总结展望) 🍬八、面试回答 🍡RAG完整处理流程 🍡问题如何拆解? 🍡大模型负责什么? 🍡最终输出什么


GitHub 热榜项目 - 周榜(2026-06-21)
CoderJia_2026/6/26

GitHub 热榜项目 - 周榜(2026-06-21) 生成于:2026-06-21 统计摘要 共发现热门项目: 21 个 Token赞助:siliconflow 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 本期热点趋势总结 本期 GitHub 热榜呈现出明显的 AI 工程化 与基础设施化 趋势:MCP 服务、Agent 技能、提示词安全扫描、RAG 压缩、代码知识图谱等项目集中爆发,说明开发重点已

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读