重放一张旧单的判定
建单时冻结输入、规则行和判据版本,规则与程序都变了,旧单仍能原样重放
一张单事后要能原样重放,SZV 的做法是建单时把单据输入、命中的规则行和判据版本冻结在单上,规则表与程序之后怎么变,都不影响它。判定本身不调用模型;模型起草的单据走的是同一套规则,没有另开的通道。判定依赖汇率、库存、风控分这类实时外部量,而这些量又写不进单据时,重放只能回答「现在会怎么判」,这套做法不适用。
重放要钉住什么
「判定是纯函数」是必要条件。纯函数指同样的输入得到同样的输出,不读外部状态。但一张旧单重放,输入的不只是这张单。
| 变量 | 会因什么变 | 怎样钉住 |
|---|---|---|
| 单据输入 | 单据只追加,输入不变 | 单据本身 |
| 规则行 | 规则表追加新行 | 审批单建单时,把命中的金额档行存在单据上 |
| 判据版本 | 解释器随发布变化 | 单据记下签法版本;签核位序列按签法版本分组,版本只追加,单据按冻结的版本走 |
签法版本,指一类单据的签核位序列的版本号。第三行最容易漏。只快照规则行,而判定函数在发版里改了含义,同一份快照也会得出不同的结果。Temporal 把同类问题放在工作流代码上:它靠重跑代码、重放事件历史恢复状态,所以代码必须对同一段历史做出同样的决定,不能读时钟、随机数和工作流之外的网络调用(Temporal 文档)。
图里,建单之后规则表与程序都变了,重放仍只用单据卡里的这些格,所以结果应与当时一致。重放结果与当时记下的结果逐项比对,不一致时,要查的就是存下的快照与解释器。
审计时重放一张旧单,不必还原当天的规则表和程序版本。
一张旧单怎样重放
示例:一张采购申请在金额线调整之前建单,命中旧的档位行,签核位序列是三位。调整之后,同样金额的新单命中新的档位行,只要两位。建单日的那张旧单今天重放,仍是三位,不会因为新规则「翻案」。重放不查规则表,只用单据上存下的档位行与签法版本。
重放的保证是:同一张单加同一份冻结,得到同一个结果。它不保证结果不随时间变化:同样金额的单今天新建,按新规则就得到另一个结果。
时钟与模型调用放在判定之外
判定的设计要求是:不读时钟,不读随机数,不读实时表,也不调模型;需要「现在几点」,由调用方当输入传进来。模型调用在判定之外,输出当作不可信的输入,只产出待审批的草案,见模型能碰什么 不能碰什么。
这个分法在软件设计里叫 Functional Core, Imperative Shell:核心是无副作用的判定,外壳做有副作用的事。SZV 的外壳就是唯一的写入入口:提议、检查、审批、追加、留痕,见修改规则参数的唯一入口。
两份解释器靠对拍
同一条判据在数据库和应用里各有一份解释器时,两份会分叉。SZV 的做法是用例逐键对拍:同一组输入分别交给两侧,结果必须相同。例如审批状态转移,判定写在 PostgreSQL 里,守卫触发器调用它,所以应用之外的连接写库也要过这道判定,见审批流转。
另有代码扫描,拦截业务代码里的第二份判断。扫描是启发式的,拦得住照抄的判断,证明不了语义上只剩一份。对拍也只覆盖取样的键,不能证明所有输入一致。所以「判据只有一份」说得过满,准确的说法是:重复的判据靠对拍与扫描看住。
表驱动的适用范围
同类单据共用一条处理管线,差异放在数据里。一类单据在单型注册表里占一行:单据号前缀、签核位序列、金额线引用、时限、批准后执行写入的那一步。每条规则写「何时」「位序列」「是否先审」,按序首条命中,最后一条必须是无条件默认,所以每张单总能找到一条规则可走。这相当于 DMN 决策表的 FIRST 命中策略(Camunda 文档)。
状态也是注册表里的行:加一种状态是加一行,全表恰有一个处于可裁决阶段的状态。
这是把差异放进数据、由一个解释器按名字取行的做法,不是函数式意义上的函数组合。它成立的前提是:
- 新情况与旧情况同形。差旅报销若要按城市分级,规则行的形状要多一列,这是改代码,要发版。
- 判据与签核位的种类已有。新种类要给解释器加解释,随版本发布,见规则写成带版本的数据。
核对一类新单据时,先看它的差异能不能写成已有判据与已有位种类的参数。能,核对新加的行即可。
限制与不适用
- 每张单带一份快照,旧版本的判据要一直保留。
- 不表达追溯更正:旧行保留,追加新行。
- 判定依赖实时外部量(汇率、库存、风控分)时,这些量必须当输入并入快照;并不进去,重放只能回答「现在会怎么判」。
- 新判据种类与新位种类要发版。
与事件溯源、工作流引擎对照
| Temporal | 事件溯源 | SZV | |
|---|---|---|---|
| 重放什么 | 工作流代码,对着事件历史重跑 | 事件序列,重放得到状态 | 一张单的一次判定 |
| 靠什么保证一致 | 代码必须是确定的 | 事件不可改 | 冻结的输入、规则行与判据版本 |
| 追溯更正 | — | 要引入双时间 | 不表达;旧行保留,追加新行 |
SZV 把等待状态与判定依据存成单据上的数据,和规则表放在同一个 PostgreSQL 库里,不重放事件流来恢复状态。
其他做法
| 做法 | 它解决什么 | 为什么不用 |
|---|---|---|
| 重放时查当前规则表 | 不用在单据上存快照 | 只能回答「现在会怎么判」,答不了「当时为什么这么判」 |
| 只存规则行,不钉判据版本 | 快照小 | 判据随发布变化,同一份快照得出不同结果 |
验收时可以问的问题
- 请演示:重放一张规则调整之前建的旧单,把结果与当时记下的结果逐项比对。
- 请现场查这张旧单上冻结的金额档行与签法版本。
- 请给出库内与应用内两份解释器的对拍用例清单。
相关页
- 规则写成带版本的数据:规则行怎样存、怎样按时点取。
- 修改规则参数的唯一入口:判定之外的那层外壳。
- 留痕与证据链:判定结果与依据怎样留下记录。
- 审批流转:一张单怎样沿冻结的链走。