fix(outbound): 执行校验兼容无 base_id 的历史单据,修复老单必然被拒
2026-09-10 预占改造(b57c21a)之前建的单,items_json 里没有 base_id。
这类单只要在改造上线时仍处于「已通过待执行」状态,就再也执行不了:
批准侧 build_approval_index() → identity_key(base_id, name, spec)
无 base_id → 退化成 ('name', 名称, 规格)
扫码侧 stock_identity() → ('base', id)
两侧键不同构,**永远不相等**,verify_scanned() 必然抛
「扫码物料【…】不在该申请单的批准明细中,禁止出库」——批的就是这件货。
identity_key() 的文档本就把 (name, spec_model) 写作「历史数据的兜底」,
只是校验时只有批准侧降了级、扫码侧没有,两侧因此错开。
实测(APR-OUT-20260909-1313-0007,建于 09-09,改造上线前一天):
批准侧 ('name', '派里肯安全箱1600(黑色)', 'PS-9640B001-black')
扫码侧 ('base', 3012)
修复:
· 新增 stock_name_spec() —— 库存行 → 名称型身份键,刻意丢掉 base_id
· 新增 build_legacy_approval_index() —— 无 base_id 的历史明细旁路索引
· verify_scanned() —— 主索引落空时用库存行名称+规格再比一次;命中则
改用**批准侧的键**继续,使 acc 累计与 approved_idx 的批准量同口径
安全边界(关键):降级只对真正来自老单据的名称型键放行,白名单是
legacy_idx 而非 approved_idx 本身——build_legacy_approval_index() 只收
identity_key() 产出名称型键的明细,有 base_id 的一律跳过;名称与规格皆空
的明细直接丢弃,绝不退化成「任意物料都能匹配」。无老单时该索引为空集,
降级分支恒不命中,行为与改造前逐字节一致。
影响范围:当时处于 status=1 的老单全库仅 1 张(即上述单号)。其余 358 张
老单(已完成 327 / 已驳回 9 / 已完结 22)均为终态,不受影响——它们在改造
上线前就已执行完毕。
实测验证:
T1 老单 360 + 扫批准范围内物料 修复前必拒 → 修复后通过
T2 老单 360 + 扫单外物料 仍拒绝
T3 老单 360 + 数量超批准 仍拒绝
T4 现代单 + 扫批准范围内物料 通过(旁路索引 0 键,未受影响)
T5 现代单 + 扫单外物料 仍拒绝
副作用改善:降级后标签改用批准侧的键,超量报错从「物料#3012」变为
「派里肯安全箱1600(黑色)(PS-9640B001-black)」,可读性提升。