主数据需求设计里最容易返工的部分,不是字段表,而是状态机。
一开始的状态机是"抄"出来的
做 MES 的主数据需求设计时,物料、客户、供应商、BOM 这几类对象的结构高度相似,最省事的做法是先写一份骨架,再逐份复制、改改字段。
复制本身没错,问题在于复制之后没有逐份校验差异。状态迁移表、允许操作矩阵、红线约束、验收项这四处,是四个不同的人(或者同一个人的四个不同时段)写出来的,一旦某份文档只改了其中两处,剩下两处就变成了自相矛盾的证据。
最典型的三处矛盾:
- 「关闭」和「停用」被当成同一个状态用;
- 删除允许范围的矩阵和正文对不上;
- 变更被驳回之后,状态应该回到哪里没有定义。
这三处都不是笔误,是语义漂移:同一个词在不同主数据里承担了不同的含义。
六态只是起点
最终定下来的统一状态机是六态:
| 状态 | 含义 | 谁触发 |
|---|---|---|
| 草稿 | 已创建未提交,不参与任何引用校验 | 创建人 |
| 审批中 | 已提交,审批流实例运行中 | 提交动作 |
| 审批通过 | 生效,可被业务单据引用 | 审批完成 |
| 已驳回 | 审批未通过,可修改后重提 | 审批人 |
| 关闭 | 未审批通过就放弃,等价于"这件事不做了" | 创建人或管理员 |
| 撤回 | 提交后、审批完成前由发起人收回 | 发起人 |
然后追加第七个:停用(INACTIVE),只对审批通过的对象开放,表示"这条数据还在,但不能再被新单据引用"。
加这个态的理由很直接:一个已经上线的物料,发现录错了,直接删会破坏历史单据的引用完整性;直接改会污染已发生业务。唯一正确的动作是让它退出流转,但保留可查。
这里必须把「关闭」和「停用」彻底分开:
- 关闭 = 未生效前的放弃,这条记录从此不参与业务;
- 停用 = 生效后的退场,历史引用依然有效,新引用被阻断。
一个状态承载互斥语义,后面所有矩阵都会写错。
不是所有主数据都该有停用
把这个结论无差别推广到所有对象,就会出问题。两类例外:
BOM 不启用停用。 BOM 的"失效"应该由生效区间和 ECN(工程变更)承担。一个 BOM 版本的退场,本质是某一天之后不再适用,这是一个时间语义,不是一个状态语义。给它加停用,等于允许"某个 BOM 既在生效区间内,又处于停用"这种自相矛盾的数据存在。
AVL(合格供应商名录)由名录状态承担。 名录本身的准入/退出就是它的业务状态,再叠一层停用只会让"这家供应商对这物料还能不能供货"变成两个开关的组合判断。
判断标准可以简化成一句话:如果这个对象的失效可以用时间来精确表达,就不要用状态表达。
引用保护:只在审批通过态开口
状态机定完,真正决定系统能不能用的,是引用规则:
- 业务单据只能引用审批通过态的主数据,停用态不可选;
- 已被引用的主数据要改关键字段,必须二次确认,并留下影响面提示;
- 要停用一条已被引用的主数据,必须先清掉在途引用,否则阻断。
这三条如果不写进红线约束(R-MD 系列),实现阶段一定会被"先让功能跑起来"这个理由绕过去。绕过去之后的代价,是数据出问题以后只能人工修库。
四方一致:落稿前的强制动作
写完一版文档之后,我固定做一遍交叉核对,核对的不是文字,是四张表之间的一致性:
状态迁移表 ↔ 操作矩阵 ↔ 红线约束(R-MD*) ↔ 验收项(A*)
任何一处新增状态、新增动作、放宽条件,都要同时问四个问题:
- 迁移表里有没有这条边?
- 操作矩阵里,哪个角色在哪个状态下能做这个动作?
- 红线约束需不需要加一条?
- 验收项怎么验证它?
这一步花的时间,远少于开发阶段发现矛盾后回炉的时间。
另一类坑:编号体系会互相污染
同一份文档里同时存在两套 Q 开头的编号——一套是质检节点 Q01~Q05(来料 IQC、首检、巡检 IPQC、完工 FQC、出货 OQC),另一套是待确认问题 Q1~Q10。
结果就是引用句子互相指错:本来要说"免检策略待确认",实际指到了质检节点;本来要引功能点 F23(数据分发接口),写成了 F22(追溯查询)。
规则是:一份文档里只保留一套编号语义。引用另一套体系时,必须写全名("质检节点 Q01 来料 IQC"),不能只写编号。落稿后再用脚本扫一遍所有 F* A* Q* 引用,确认每个编号都真实存在且指向正确条目。
小结
状态机看起来是几个枚举值,实际上是主数据、业务单据、审批流三方共享的契约。它一旦漂移,下游全部错位。
所以值得在文档阶段较真:把互斥语义拆开、把时间语义还给时间、把引用规则写成硬约束,然后在落稿前做一遍四方交叉核对。这几步不做,省下的时间会在联调时加倍还回来。