第 6 题 · MES 后端缺陷修复
------------------------------------------------------------

【附件】6_MES项目.zip —— 完整可运行项目（Node.js 22.5+，零第三方依赖）。
解压后请先读 README.md、CONTRACT.md、docs/SPEC.md 再动手。

------------------------------------------------------------

backend-test

一套正在生产环境跑的制造质检系统（MES）的后端。功能都齐、公开测试全绿，
但最近现场陆续报来一批"说不清但确实不对"的问题。请你作为接手的后端工程师，
定位并修复它们。

环境与运行

- Node.js ≥ 22.5.0（用到内置 node:sqlite / node:test，零第三方依赖）。
- 安装：无需安装依赖。

下面这些问题都发生在更真实的场景里（多人同时干活、跨单位、单据流转、盘点月结等）。

你的任务

修复下列现场报障。

----------------------------------------

现场报障

现象一 · 明明有货，系统说不够料

仓库反映：领料时系统总说"库存不够"，可货架上明明有货。比如台账记了 2 千克的货、
需求单填 1.2 千克，系统有时候会判不够、领料单发不出去，有时候又能发得出去。

现象二 · 模板改了版，老单子内容反而跟着变

工艺模板升了新版，维护界面看也是新版。可新建的批次套出来还是旧内容；
更奇怪的是有些早就建好的老批次，内容反而跟着新版变了。

现象三 · 主单的资料跑到子单名下

主单上传的图纸、工艺卡，会出现在它下属批次（子单）的资料列表里，还被标成是
子单自己的。财务按资料归属出归档清单时对不上数——一份主单的图纸被算成了子单的。

现象四 · 两张单共用一道工序，报工记录乱套

热处理是几张单共用的工序。忙时几个人同时用手持机报工，事后发现：有的记录
跑到别的单上去了，有的多出来一模一样的一条，还有执行记录列表和这张单的
汇总数字对不上。不忙的时候主观感觉很慢。

现象五 · 盘点做完，三个地方的数字对不上

盘点走完复核、审定、对账后，同一个物料能看出三个数：台账余额、复核录入的实盘数
（换算过来）、对账汇总页三边打架。此外海外仓按"克"复核、本部台账按"千克"记；
几个人同时复核同一张盘点单时，还出现过有人录的数不见了、数对上了名字却记成别人。

现象六 · 会签走乱：有的卡住、作废了还能往下走

单据要走四段会签（初审 → 中间两段并行 → 终审），中间能驳回重走、也能整单作废。
最近出现：并行两段几个人同时点，结果有的单终审莫名批过了，
也有两段都批了却卡在终审；被驳回重走的单只重批了初审就被当成"全批完"；
整单作废的单还能继续走；看板显示的段位跟实际对不上。

----------------------------------------

提示

- 报障人是运营、仓库、财务、车间班组长，可能缺乏专业素养。
- "一个人顺序操作从不出错、多人/跨单位/流转时才出错"是很强的线索。
- 仓库里其它随手写的说明文档不一定是最新的。

冻结契约

这份文件本身、test/ 目录、docs/SPEC.md 与 package.json 不得修改。
判分会核对哈希，改动即判定提交物不合规、不进入计分。

§1 公开入口

src/index.js 导出的函数签名是对外契约（车间终端、ERP 对接、运维脚本都依赖它）。
可以改实现，不可以改签名、不可以删函数。

§2 已发布测试

test/ 下的所有文件是回归基线，一行都不许改，也不许新增 .test.js。

发现测试本身有问题（写错了、锁死了错误的期望值），不要改它——
把这件事写进对应问题的答复 notes 里。

§3 数据表

核心表（表名、列名、语义冻结，不许改结构、不许改语义）：

    表 | 说明
    items | 物料档案
    unit_conversions | 单位换算
    work_orders | 主单
    sub_orders | 子单
    attachments | 附件
    templates | 模板/工艺定义
    sub_order_content | 子单套用内容
    routing_segments | 工序段
    process_exec | 工序执行记录
    ledger | 物料台账
    stock_counts | 盘点单（开单快照）
    count_reviews | 盘点复核记录
    count_settlements | 盘点审定记录
    approvals | 审批会签记录

派生表（为读得快而存在的投影/物化，可以随意改结构、重建、甚至删掉现算）：

order_progress、board_projection、item_balance、approval_board

判断标准：一张表如果丢了能从核心表原样重算出来，它就是派生表。

§4 故障注入点

src/faults.js 里登记的注入点是契约的一部分。
混沌测试靠它验证原子性，不许删除已有的注入点（可以新增，请同时登记）。

§5 交回什么

- 改好的整个目录。
- 一个 _答复/ 目录，每修一个问题放一个 JSON（文件名自取，例如 现象一.json）：


{
  "题目": "现象一",
  "verdict": "fixed",
  "rootCause": "一句话说清真正的根因",
  "evidence": ["读了哪个文件的哪一段，看到了什么"],
  "notes": "可选。发现测试或文档本身有问题写在这里。"
}


verdict 取值：fixed（是问题，已修复）/ not-a-bug（查证后认为不是问题）。

如果你在通读代码的过程中发现了 README 现象清单之外的其它问题，欢迎一并修复、
并按同样格式补一个答复——把系统真正读懂，往往比只对着现象逐条排查更快。

系统规格（权威）

> 本文件是口径的唯一权威来源。代码、注释、其他文档与本文件冲突时，以本文件为准。

§1 键与标识

    键 | 语义 | 约束
    apply_no | 系统唯一键 | 全局唯一。一切内部关联、一切"定位某一张单"都必须用它
    order_no | 业务单号 | 由客户/ERP 给。允许多张单共用——一个客户订单拆成几张工单是正常业务形态

推论：order_no 只能用来"筛出一批单"，不能用来定位一张单。

§2 计量口径

一切数量的比较与汇总，都必须先按 unit_conversions 折算到该物料的 base_unit。

录入端可以用任意顺手的单位（克、只、箱），台账存的一律是 base 口径。
两边单位不同是常态，不是异常。

§3 工序执行

共享工序（routing_segments.shared = 1）是公共产能，会被多张单同时挂在工艺路线上。

1. seq 是「该单 × 该工序」内部的顺序号，从 1 开始连续递增、不重复。
   A 单报到第几号，与 B 单报到第几号毫无关系。
2. 一次成功的报工，在 process_exec 里留下恰好一条记录，且归属于它自己那张单。
3. 派生的进度投影（order_progress）显示的数字，必须等于 process_exec 的真实汇总。

§4 物料台账

1. 复式：入库 dir='in'、出库 dir='out'；余额 = Σ(valid=1 行的带符号量)。
2. 撤销不删行：另记一条方向相反的行，reversal_of 指回原行，原行 valid 置 0。
   撤销后的余额必须回到撤销前。
3. voucher_no 是业务凭证号，同一张凭证可以涉及多种物料，因此它不是唯一键。
   撤销必须同时限定物料，只按凭证号撤会连带撤掉别的物料。

§5 附件归属

附件归属是一对严格的 (owner_type, owner_id)，由上传时决定，任何读取路径都不得改写它。

子单的附件列表里只有子单自己的附件，主单的附件属于主单，两者不混。
财务归档按这个归属出清单。

为方便现场作业，允许存在"一屏看全"这类聚合展示入口，把主单的资料一并列给子单页面。
但聚合只改变展示范围，不改变归属：主单的附件在任何地方被列出来，
它的归属仍然是那张主单。

§6 模板套用

子单套用模板的语义是存档：套用那一刻看到的内容，此后必须一直是这张子单看到的内容。
模板后续无论以何种方式变动（升版、订正、勘误），都不得回溯影响已建子单——
这是质量追溯的硬要求，客户审厂查的就是这个。

新建子单套用套用时刻已生效的最新版本。判断"已生效"用的时间基准是套用时刻，
不是主单的下达时刻，也不是别的什么时刻。

sub_order_content 里存引用还是存正文快照，是实现自由；但无论怎么存，
上面两条语义必须成立。

§7 状态语义

planned_done_at（计划完成）与 actual_done_at（实际完成）是两个独立字段。
一切"是否已完成"的判定必须读 actual_done_at。计划时间填了不代表事情做完了。

物料名称：正式物料（items.category='standard'）以物料档案 items.name 为准。
第三类临时物料（category='temporary'）没有正式档案，其名称以录入端记下的名字为准，
任何详情/展示都必须能给出一个非空名称，不得因为它没有正式档案就显示空白。

§8 物料台账余额与盘点

§8.1 余额真值

物料余额的唯一真值是 ledger 里 valid=1 行的带符号净额（§4）。
为读得快，允许维护物化余额 item_balance 之类的派生缓存，但它只是缓存：
任何时刻，对账/汇总给出的余额都必须与台账真值一致。缓存与真值不一致即为缺陷。

§8.2 盘点四步

盘点的语义是"用一次实地清点校正账面"，四步：开单 → 复核 → 审定 → 对账。

1. 开单：记下开单时刻的账面余额（快照），仅供参考。
2. 复核：录入实地清点数。实盘数可用任意计量单位录入，一切比较与动账前
   必须先折算到该物料的 base_unit（§2）。以最后一次有效复核为准；
   多人同时复核同一张盘点单时，不得互相覆盖、不得串单（甲的经手人配上乙的数）、不得丢数据。
3. 审定：使台账余额反映实盘结果。审定后，该物料的台账余额必须等于最后有效复核的实盘数（base 口径）。
   盘点期间发生的正常出入库是真实业务，照常计入台账；审定只补实盘与当前账面之间的差异，
   不得吞掉、也不得重复计入盘点期间的正常出入。
4. 对账：给出汇总余额。这个数字必须与台账真值（§8.1）一致，不得只反映某个可能过期的缓存。

推论（全链一致 I2）：盘点审定并对账之后，台账余额 = 最后有效复核实盘数（base）= 对账汇总，三者必须同时相等。

§9 审批状态机

一张单走四段会签：1 初审 →（2、3 两段并行会签）→ 4 终审。

1. 合法推进：第 1 段通过后，第 2、3 段才可批（两段并行，先后不限）；
   第 2、3 段都通过后，第 4 段才可批；第 4 段通过即整单通过。
   任何"越段"推进（如 2、3 未齐就终审，或第 1 段未过就批 2/3）都不合法。
2. 并发收口：多段并行提交时，放行第 4 段的依据必须是"本轮第 2、3 段都已通过"这一
   持久事实（读核心表得到），不得依赖任何跨请求共享的内存计数。并发下不得越段、
   不得重复落章。
3. 驳回重走：任一段被驳回，则进入新一轮（cycle+1）。被驳后，
   下游在旧轮次里的既有裁决一律作废、必须在新轮次重走；
   判断"这张单批到哪"只能看当前轮次的裁决，旧轮次的残章不得当作有效。
4. 整单作废：作废后进入终态，此后任何段都不得再推进（decide 必须拒绝）。
5. 看板一致：审批看板是派生投影，它显示的段位/状态必须与 approvals 核心表
   按上述规则算出的真实状态一致，不得美化（如把驳回/作废显示成"进行中"或停在旧段位）。

现场操作口径速查（内部）

> 写给车间、库管、客服的速查页，方便日常回答和培训新人。
> 最初整理于 2025-10，之后陆陆续续加过几条，不一定跟最新系统一致。

单位与计量

各车间按自己顺手的单位录入就行，系统展示时也尽量按录入单位显示，
让大家少做心算。库存够不够、盘点差多少这些，看界面上的数字即可。

业务单号

客户和 ERP 那边都认业务单号，所以查一张单，报业务单号最快。
客服台按业务单号就能调出单据详情、进度和执行明细，一号通查。

> 提示：同一个客户订单有时会拆成好几张单，但日常沟通报业务单号一般不会有歧义。

完工判定

排产时会把每张单的计划完成时间填好。到了计划完成时间，基本就可以认为这批做完了，
下游可以接着安排。个别赶工或延期的情况另说。

模板与工艺

模板升版后，相关子单会用上新版工艺，保证大家看到的都是最新标准。
维护界面改了模板，现场看到的也会跟着更新。

附件

子单页面能一屏看全这张批次相关的所有资料（含主单带下来的），
归档时按页面上列出来的清点就行。

盘点

盘点复核录实盘数，审定时系统按开单时记下的账面数和实盘数的差额调账，
对账页出汇总。几个人一起复核同一张单可以加快进度。

审批

四段会签，中间两段可以并行。看板上能看到每张单走到哪一段，以看板为准最直观。

----------------------------------------

> ⚠ 2026-04 有同事反映这一页好几条跟 docs/SPEC.md 对不上（尤其是单位、单号定位、
> 完工判定、模板语义、附件归属这几处），一直没抽出时间逐条核对。
> 凡有冲突，一律以 docs/SPEC.md 为准，别照这页办。
