规则写成带版本的数据
会变的制度条款存成带版本的行,改参数不改代码,每个时点的取值都查得到
SZV 把金额线、签核位序列、开关这类会变的制度条款存成带版本的数据行,用已有判据改参数时不必改代码。接入 AI 时,金额、是否需要审批这类判定用的值取自这些行,不取自模型。判据结构常变、规则极少且多年不动,或要用任意循环、任意跨单据汇总的业务,不适合这样存。
程序不含取值 但含取值的语义
常见的说法是「程序里没有条款」。更准确的说法是:程序不含取值,含解释取值的语义。
| 放在数据里 | 留在程序里 |
|---|---|
| 金额线、时限、开关的值 | 值怎样读:同一个键取哪一行,缺键怎么办 |
| 一个值的类型与合法范围 | 每种类型怎样校验;类型本身是字典表里的一行,新增类型要加一行,再加一支校验函数 |
| 签核位序列与金额线引用 | 每种判据的含义:「何时」一栏只写判据名,含义在解释器里 |
用已有判据重排位序列、换金额线,改的是数据,不改代码:注册表与金额档表随版本发布追加新行,旧行由触发器拒改拒删。新增一种判据,要给解释器加代码,见审批流转。
数据不都走同一条改法,哪类走哪条路见修改规则参数的唯一入口:规则参数的值与分支登记经变更单;金额档位与审批单型注册表随版本发布追加。
核对一次改动时,先问它用到新判据没有。没有,就只是改几行数据。
改一次值就追加一行
一条规则参数占一行,主键是键加生效时间。同一个键可以有多行,各带自己的生效时间。取值时,系统取已生效、未到期、生效时间最大的一行。生效时间写在未来,就是预约生效。
图里 10-28 查询取到 7,11-01 查询取到 14,第一行没有动。键不存在时,结果是拒绝,没有缺省值顶替。
每一行还带版本整数(每次变更自增)、变更人、变更时间、变更理由和来源。键与生效时间不可改,改值只能追加。每次追加,前一行与新行的整行内容都写进另一张只追加的历史表。
生效时间取「期望生效时间」与「批准时刻」中较晚的一个,不倒填,见修改规则参数的唯一入口。这样回放取到的,是当时实际用过的值。
取值只按生效时间这一条轴,规则不表达追溯更正:事后发现过去的值设错了,只能追加一行,旧行仍留在历史里。要同时回答「当时生效的是什么」和「事后才知道应该是什么」,需要第二条时间轴,即双时间:一条记事实何时成立,一条记系统何时得知。
缺键就拒绝 租户行压过全局行
开关与参数的取值有固定顺序:全局行与租户行并存时,租户行压过全局行;同一级里取生效时间最晚的一行;每行有生效区间。
键不存在时,系统直接失败,不回落到环境变量,也不回落到代码里的缺省值。漏配的后果因此是「某件事做不了」,会有人来报告。若回落到缺省值,系统会悄悄用上一个没有人批准过的数。
规则表里查不到的东西,系统一律当作没有,不会替它编一个值。
类型 范围 次序由数据库检查
一行规则的类型与合法范围写在行里,写入时由数据库校验,越界的值进不了表。业务账号直连数据库,也要过这道检查。
次序约束也是数据。例如「提醒起点要早于升级起点」,这类跨两个键的关系存在一张字典表里,改值时一并校验。单行的 CHECK 约束只能比一个值,比不了两个键之间的先后。
防改写靠触发器:键与生效时间由触发器冻结,历史表拒绝改和删。触发器有一个前提。PostgreSQL 文档写明:默认启用的触发器只在会话复制角色为 origin 或 local 时触发;表属主可以停用触发器。SZV 把这些触发器声明为 ENABLE ALWAYS,让它在任何复制角色下都触发。这样挡住的是业务账号与误操作,挡不住拥有表属主或超级用户权限的人。
分支可调的属性是另一套
分支沿用总部的规则,只在少数属性上自己填值。这一套与上面的参数表是两种机制。
| 对象 | 存什么 |
|---|---|
| 属性定义 | 键、类型、范围、缺省值 |
| 取值覆盖行 | 一个分支对一个属性填的值,只写与总部不同的那一项 |
| 分支主数据 | 分支本身的一行;主数据指描述分支这类对象本身的基础资料 |
| 总部规则 | 审批链、权限策略、规则参数、金额线挂在租户一级,分支沿用 |
取值规则很短:有覆盖行,用覆盖行;没有,用定义里的缺省值。新增一项可覆盖的属性,是加一行定义,分支的编辑页按定义表生成,多出一项,所有分支先显示缺省值。字典型属性的取值范围,是选项字典里已启用的那些选项。
新开分支是登记一行主数据。它的归属范围由所属类别推出,不由提交人自填;名称与编码查重;只新增,不修改已有分支。它走与其他规则改值同一条审批,批准后由写入入口追加,下一次读取即含新分支,见修改规则参数的唯一入口。完整做法见新开一家分支。
这一套有以下限度:
- 加定义多出来的只是一个可填的值。没有代码读取它,它就不改变行为。新的计算方式仍要加代码。
- 这是 EAV(实体、属性、值)形态:加属性不改表结构,代价是值的类型偏弱、难做联表汇总、难加外键。定义表里的类型与范围,收的是弱类型这一头。
- 取值表里一个分支一个属性只有一行,没有生效时间。
总部改了某一项,所有沿用它的分支同步生效。这是便利,也让一次改动的影响面扩大到全部沿用者。
限制与不适用
- 解释器仍在程序里。新增判据种类要发版,规则本身也要审查、要测试。
- 解释器只提供已有的判据,包括累计线与窗口。要用任意循环、任意跨单据汇总的逻辑,须新增判据。
- 判据结构常变,或规则极少且多年不动的业务,不适合这样存。
与常见做法对照
| 做法 | 规则放在哪 | 擅长 | 取舍 |
|---|---|---|---|
| SZV 规则表 | 数据库里的行,值带版本与生效时间,判据含义在解释器 | 阈值、位序列、开关这类取值型规则,按时点回放 | 只提供已有判据,含累计线与窗口;任意循环、任意跨单汇总要新增判据,随发版 |
| DMN 决策表 | 决策建模标准:一行一条规则,多行同时命中怎么办由「命中策略」定,如 UNIQUE、FIRST、COLLECT(Camunda 文档) | 条件列多、要业务人员读懂的决策 | 条件与结果的列要先建模好 |
| OPA | 用 Rego 语言写策略,策略与数据打成 bundle 下发,bundle 可带 revision 标识版本(OPA 文档) | 判定逻辑复杂、要跨系统统一策略 | 策略是代码,审查与测试走代码流程 |
SZV 的签核规则是「按序首条命中,最后一条必须是无条件默认」,相当于 DMN 的 FIRST 命中策略,所以每张单总能找到一条规则可走。SZV 选表而不选策略语言,是因为要改的大多是取值:谁来改、改成多少,都要财务负责人自己看得懂、查得到。判定逻辑复杂到取值表达不了时,应改用策略语言。
其他做法
| 做法 | 它解决什么 | 为什么不用 |
|---|---|---|
| 条款写成程序里的常量 | 取值快,没有解释器 | 改一条线要改代码、发版、重测;事后难查谁在何时改过 |
| 原地更新规则行 | 表最小,只有当前值 | 旧值没了,一张旧单当时按什么判的无从回放,见重放一张旧单的判定 |
| 缺键时回落到代码缺省 | 漏配也能跑 | 悄悄用上没人批准过的值,问题到出事才暴露 |
验收时可以问的问题
- 请演示:提议一个早于批准时刻的生效时间,实际生效时间取批准时刻。
- 请演示:查询一个不存在的键,结果是拒绝,没有缺省值顶替。
- 请现场查一条规则的历史表记录,确认前一行与新行的整行内容都在。
- 请现场打开一个分支的属性页,区分哪些属性是覆盖值,哪些显示「缺省值(沿用)」。
相关页
- 重放一张旧单的判定:规则行与判据版本怎样冻结在单据上。
- 修改规则参数的唯一入口:规则改值怎样提议、检查、审批、追加。
- 修改一条规则:核心经营系统里改规则的操作。
- 新开一家分支:分支主数据与覆盖行的完整做法。