SpringBoot+Flowable 审批流版本设计:新规则按时生效,历史单据不被悄悄改写
企业把审批流程加了一级负责人,又调整了下一周期的计价规则。新业务应当使用新规则,这很好理解;难的是已经提交的申请、尚未完成的订单,以及几个月前需要重新查看的单据。它们到底跟着最新配置走,还是保留当时依据?如果系统没有明确答案,修改配置就可能变成一次悄无声息的历史改写。
▲ 设计示意:新版生效后,后续业务按明确策略采用新版;历史单据保留原版本依据。版本发布、生效时间与单据绑定是不同动作。
一、企业配置为什么不能总是“查最新的一条”
规则配置决定系统怎样解释业务。审批定义决定由谁处理、经过哪些节点;计价规则决定采用什么计算依据;表单定义决定字段如何解释;打印模板决定正式材料以什么结构展示。它们可以更新,但被这些规则处理过的事实不能随便漂移。
最常见的实现是单据只保存一个规则 ID,每次查看时再查询这条配置的当前内容。如果配置被原地修改,旧单据虽然没有更新,展示或重算结果却已经改变。用户会感觉“昨天明明不是这样”,而日志里却找不到单据变更记录。
给配置加一个 version 数字也未必解决问题。如果 ID 仍指向一条不断覆盖的记录,历史版本内容没有保留,这个数字只是计数器。真正的版本设计需要让某一版内容可以被稳定识别、稳定读取,并明确何时被业务采用。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
本文讨论的是企业软件治理,不替企业决定价格、财务或人事政策。不同业务应采用哪个时间点,必须由业务方确认;软件的职责是把这个决定稳定执行,并保留解释历史的依据。
二、流程模型、已发布定义与运行实例,是三个不同对象
1. 模型是可以继续编辑的工作区
流程模型页面允许配置名称、表单、流程与扩展设置。它适合承载下一次变更,但不能被直接理解为所有历史实例都应该实时跟随的唯一真相。
▲ 真实模型配置页面。打开编辑页不等于发布,也不意味着已经改变在途业务;本次截图未保存或部署流程。
业务人员修改模型时,往往希望先保存草稿,验证条件分支和处理人,再选择何时发布。如果“保存”就让所有新旧单据立即使用新内容,既无法安全试验,也很难定位变更影响。
因此,编辑、发布、生效应是可以区分的概念。某些系统可以把发布与立即生效合并成一次操作,但产品仍应说明合并后的语义,而不是让数据库更新时间成为隐含的生效规则。
2. 发布定义承担确定内容的身份
流程定义历史提供了已发布版本的观察入口。历史版本不是为了在列表里展示一个好看的序号,而是为了让运行实例能够指向具体发布结果。
▲ 真实流程定义历史列表。版本号帮助识别,但历史解释依赖的是具体定义身份以及对应保存的配置内容。
当前 BpmProcessDefinitionServiceImpl 在部署后创建流程定义扩展记录,保存流程定义 ID、模型信息、简化流程配置,并写入表单字段与配置。存在表单发布版本时,还会记录相应表单版本;自定义业务表单的节点字段权限也有快照构建逻辑。
这给了版本治理一个具体参照:**发布时把运行需要的定义从可编辑模型中分离出来。**但它不意味着组织人员、价格表、外部接口和所有关联主数据也会自动冻结。哪些内容是快照、哪些仍然动态读取,需要继续检查。
3. 表单结构更新,也必须考虑历史解释
表单字段从文本变成选项、枚举名称改变、明细结构调整,都会影响历史数据的展示与校验。如果历史单据始终采用最新表单解释,旧值可能丢失含义,甚至被新校验规则判成不合法。
▲ 真实表单设计器。字段定义属于需要版本治理的内容;它与单据里当时填写的数据不是同一份对象。
因此,既要保存业务数据,也要保存解释这些数据所需要的定义身份。并不是所有字段都要复制成大 JSON,但至少要能找回当时的字段含义、选项依据和必要展示规则。
三、先确定绑定时点,再讨论技术实现
规则可以在创建草稿、提交审批、审批通过、确认订单或实际履行时绑定。不同选择都可能合理,但它们会产生不同的业务结果。
以计价为例,草稿阶段通常允许重新试算;客户确认后,价格依据可能需要稳定;后续新增的履行部分又可能按另一条约定处理。软件不能简单使用“最后一次保存时间”决定价格版本。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
对流程来说,现有实例创建逻辑使用 .processDefinitionId(definition.getId()),而不是仅把一个可变模型地址留给以后解释。这种明确绑定,避免了“查看历史时总去取最新定义”的错误方向。
下面摘出当前实例创建阶段的主要调用。上游权限校验、变量准备及独立表数据保存没有全部展开,不能把这段独立调用当成完整发起接口。
ProcessInstanceBuilder builder = runtimeService.createProcessInstanceBuilder().processDefinitionId(definition.getId()).businessKey(businessKey).variables(variables);BpmModelMetaInfoVO.ProcessIdRule idRule = processDefinitionInfo.getProcessIdRule();if (idRule != null && Boolean.TRUE.equals(idRule.getEnable())) {builder.predefineProcessInstanceId(processIdRedisDAO.generate(idRule));}builder.name(generateProcessInstanceName(userId, definition, processDefinitionInfo, variables));ProcessInstance instance = builder.start();if (managedDataId != null) {formManagedDataService.bindProcessInstance(processDefinitionInfo, managedDataId, instance.getId());}return instance.getId();
这段代码能证明实例绑定具体定义,但不能进一步承诺“升级后所有旧流程都永远无需处理”。节点实现、表达式依赖的服务、动态人员解析和业务表字段仍可能变化。版本身份是稳定运行的重要条件,不是消除所有兼容问题的魔法。
四、有效区间要表达业务时间,而不是任务执行时间
假设新规则应从某个业务日开始使用。最直接的实现是定时任务到点把“当前版本”指针改掉。但如果任务延迟、服务重启或存在补录业务,实际采用的版本就可能偏离业务规定。
更稳定的设计是保存版本的有效区间,在业务绑定时根据明确的业务时间选择版本。定时任务可以负责通知、预热和操作提示,但不应成为唯一的时间真相。
▲ 建议逻辑模型。规则身份管理系列,版本保存确定内容,业务绑定引用具体版本,纠错和迁移单独记录。
区间通常可采用起点包含、终点不包含的表达方式,避免交界时点同时命中两版。没有结束时间可以表示持续有效,但应在类型和查询中明确处理,而不是随意填一个极大的日期。
下面是版本选择与绑定的扩展设计伪代码。versionRepository 等接口代表设计角色,仓库并没有因此新增通用规则引擎。
public RuleBinding confirm(BusinessCommand command, Subject subject) {Business business = businesses.lockForUpdate(subject.tenantId(), command.id());permission.requireConfirm(subject, business);if (business.isConfirmed()) return bindings.requireExisting(business.id());Instant at = businessTime.resolveBindingInstant(business, command);Scope scope = scopes.resolveFromBusiness(business);List<RuleVersion> matches = versionRepository.findPublishedAt(business.ruleKey(), scope, at);if (matches.size() != 1) {throw new IllegalStateException("有效规则缺失或存在重叠");}RuleVersion chosen = matches.get(0);RuleBinding binding = RuleBinding.of(business, chosen, at);binding.setInputSnapshot(snapshots.requiredInputs(business, chosen));bindings.insertUnique(binding);businesses.markConfirmed(business, binding.id());return binding;}
发布版本时同样要防止区间重叠。两个管理员同时发布新版本,仅靠各自先查“当前没有重叠”并不安全。应围绕同一规则身份和作用域使用一致的锁定或数据库约束策略,让校验与发布作为一个受控操作完成。
作用域也是版本选择的一部分。同一规则可能存在公司、地区或客户分支。如果允许更具体的配置覆盖一般配置,就需要明确定义优先级及冲突处理。不能由数据库返回顺序决定先采用哪一条。
五、引用版本和保存快照,应该如何取舍
只保存版本 ID 的成本低,但前提是版本内容不可变且可长期读取。保存必要输入快照,则能防止人员职级、组织归属或业务参数后续变化,影响对当时结果的解释。两者往往需要结合,而不是二选一。
可以把历史解释依赖分成三类:规则内容、业务输入、外部结果。规则内容用不可变版本引用;业务输入保存稳定字段和必要展示值;外部结果若无法可靠重放,则保留当时的响应依据或输出文件摘要。
例如,规则引用的是“员工等级”,而员工后来发生调岗。只保存员工 ID,不足以解释当时为什么采用某一档。可以保存绑定时的等级及来源身份,而不是把员工档案里的所有敏感字段都复制进快照。
快照也有安全成本。无差别复制整张表,会扩大敏感信息留存范围,并让清理和授权变得困难。需要哪些字段应由历史解释目标决定,口令、令牌和与本次业务无关的个人信息不应成为快照的一部分。
发布时固化定义,是一个可以借鉴的工程动作
当前流程发布代码会根据表单版本或当前表单,保存相应字段和配置。下面保留这部分核心逻辑,省略无关部署过程与业务字段权限的具体遍历。
var info = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class).setModelId(model.getId()).setCategory(model.getCategory()).setProcessDefinitionId(definition.getId()).setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson);if (formVersion != null) {info.setFormFields(formVersion.getFields()).setFormConf(formVersion.getConf()).setFormVersion(formVersion.getVersion());} else if (form != null) {info.setFormFields(form.getFields()).setFormConf(form.getConf());}processDefinitionMapper.insert(info);return definition.getId();
这里保存的是定义层的配置,不是把所有业务事实冻结。当前身份权限也不能因为存在历史节点配置就永远沿用旧授权。谁可以查看历史单据,应按当前安全策略判断;当时流程如何要求处理,则按历史定义解释。业务历史和访问安全必须分开。
六、在途业务怎么处理:保持、迁移还是重新发起
新规则发布后,已经绑定的在途业务通常有三种选择:保持原版继续运行;按明确映射迁移到新版;终止原业务并按新规则重新发起。没有一种选择适用于所有场景。
保持原版的优势是历史连续性强,但旧版依赖需要继续可用;迁移可以消除旧规则缺陷,但必须处理节点、字段和状态兼容;重新发起看似简单,却可能涉及已产生的占用、审批意见和外部动作,不能只复制一份表单。
▲ 建议时序:确认时保存版本依据,之后查看历史时读取原绑定。迁移与重算需要新的显式操作,而不是在查询时偷偷换版本。
对于流程升级,已经完成的节点不能因为新版删掉了节点就从历史轨迹中消失;正在处理的任务也不能随意映射到一个语义不同的节点。是否迁移,应先明确业务允许的变化,再验证引擎和扩展数据能否承接。
在产品上,迁移最好提供影响预览:涉及多少在途实例、哪些实例可直接映射、哪些需要人工处理、哪些已经产生不可逆动作。用户确认的是具体范围和风险,而不是一句笼统的“升级到最新版”。
七、纠错与重算不能覆盖原来的解释链
如果旧规则本身确实有误,保持原版不等于拒绝纠错。关键是把纠错作为一笔可追溯操作:原来按什么算、现在改按什么算、为什么改、谁确认、影响哪些结果。
对于尚未产生外部影响的业务,可以在受控流程中重新计算;对于已经结算、出库或通知第三方的业务,还要处理相应差异和补偿。重新得到一个数字只是计算步骤,不代表相关业务影响已经全部修复。
不应为了让历史页面看起来“已经正确”,直接覆盖原快照或版本内容。更可靠的做法是保留原记录,并关联纠错记录或新的计算结果,让查看者知道哪一个是原始事实、哪一个是后续修订。
重算还需要考虑算法版本。即使输入数据和规则配置相同,执行规则的程序代码已经改变,结果也可能不同。对需要严格复现的场景,应记录足够的计算环境身份,或保留当时结果,不要承诺仅凭一个规则 ID 就可以无限期按位重放。
有时业务只需要解释“当时采用了什么依据”,并不要求重新执行出完全相同的二进制结果。把这两个目标分开,可以避免过度建设,同时减少向用户承诺无法实现的历史重放能力。
八、缓存、导出和打印也必须尊重版本
规则版本如果被缓存,应使用版本身份作为稳定键。可编辑草稿与已发布版本不能混在同一个缓存项里。查询“当前适用版本”时,还应考虑作用域、业务时点和未来生效版本,而不是只设置一个长时间过期的全局指针。
历史导出和打印同样不能总去读取当前配置。原单据的模板、字段含义和必要主数据都可能不同。产品可以提供“查看原始材料”和“按当前规则重新生成”,但应该分别命名并保留生成依据。
删除版本也需要反向依赖检查。一个版本停止给新业务使用,不意味着已经没有历史单据引用它。可以停止新增绑定、归档内容或迁移存储位置,但应保留可访问的历史解释路径。
如果规则内容存储在外部文件或对象存储里,数据库版本行存在不代表内容一定还在。引用完整性、文件摘要和保留策略同样需要纳入治理,避免过几年才发现历史单据只有一个无法打开的地址。
九、版本治理的验收,不只是能看到 V1 和 V2
可以先用一个规则系列建立可复核样本:旧版有效、新版未来生效、两张不同时点确认的单据,以及一次显式纠错。再把边界逐个补齐。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
错误处理也应具备业务语义。找不到有效版本时,不要默默使用最新版;同时命中多个版本时,也不宜取列表第一条。这样的“自动兜底”表面上让流程继续,实际上制造了更难解释的历史。
十、怎样从流程版本经验推广到其他企业规则
先选择一个有明确确认时点的业务,写清规则身份、版本身份和绑定策略。然后核对现有数据是否已经保存足够依据,避免为了套框架重复复制数据。
接着限制已发布内容的修改方式,建立版本发布和停止新增绑定的操作。再处理未来生效、重叠冲突与历史读取,最后考虑迁移和重算工具。这个顺序比一开始设计万能规则引擎更容易验证价值。
RuoYi Office 中可先查看流程模型、历史定义与表单设计器,再沿 BpmProcessDefinitionServiceImpl 和 BpmProcessInstanceServiceImpl 核对发布快照与实例绑定。本文的通用规则仓库是设计建议,不是新增上线的现成模块。
常见问题
版本号越大,是不是就应该优先使用?
不一定。版本是否可用还受发布状态、作用域和业务时间影响。未来版本即使编号更大,也不应自动用于尚未进入其有效区间的业务。
把规则内容复制到每张单据,是否最安全?
它能增强独立性,但也增加存储、敏感信息和维护成本。通常可以结合不可变版本引用与必要输入快照,按历史解释目标选择,而不是无差别复制全部配置。
新流程发布后,旧流程会自动变成新版吗?
当前实例创建代码绑定具体 processDefinitionId。新版本发布与旧实例迁移是不同动作,不能把两者混同;具体迁移能力和影响仍需按引擎与业务实现验证。
配置已经改错了,能否直接恢复旧版?
恢复配置只能影响后续采用规则的业务,不能自动抵消错误配置期间已经产生的结果。需要识别受影响单据,并按显式纠错与重算流程处理。
结语
企业规则必须能够变化,但变化应该有清晰的时间、范围和身份。让新业务采用适用的新规则,让历史单据保留当时依据,让纠错留下新的证据,这三件事共同构成了版本治理。
以流程定义快照和实例绑定为参照,再逐步扩展到价格、表单和其他业务配置,SpringBoot3 企业系统才能在不断迭代的同时,依然准确回答“当时为什么是这个结果”。
打开演示地址直接查看系统。

