本手册用于处理“页面有库存但下单/出库失败”“业务单完成但库存未变”“库存流水与实时库存不一致”“盘点和报表库存不一致”等问题。

库存不是一个数字,而是一组带站点、商品、仓库、货位、货位类型、时间和业务来源的事实。排查时必须同时看业务单、128 分片库存流水、实时库存单表、历史归档、盘点差异、缓存/索引和预警;任何只改单表的修复都可能让另一个视图继续错误。

1. 业务目标

  • 从 sid + invId/skuId + 仓库 + 货位 找到该库存维度的完整事实。
  • 明确业务单、库存流水和实时库存之间的写入顺序与事务边界。
  • 区分库存过程事实、当前结果、查询缓存、预警结果和报表快照。
  • 用数量方向和 transType 判断每条流水应增加还是减少库存。
  • 识别分表错误、重复写入、漏写流水、漏改实时库存、归档漏查和维度串仓。
  • 解释负库存是否被允许,以及为什么快准商品与第三方商品走不同分支。
  • 修复前保存可回滚证据,修复后完成业务、流水、实时、缓存、报表五层验收。

2. 先建立库存事实模型

flowchart LR
    DOC["业务单据"] --> LEDGER["库存流水\n过程事实"]
    DOC --> RT["实时库存\n当前结果"]
    LEDGER --> ARCHIVE["历史归档"]
    RT --> QUERY["列表/可售查询"]
    LEDGER --> REPORT["报表/成本"]
    RT --> WARN["库存预警"]
    RT --> CACHE["ES/缓存展示"]
    DOC --> CHECK["盘点单"]
    CHECK --> LEDGER
    CHECK --> RT

2.1 五类数据的职责

数据层代表表/系统回答的问题是否库存最终事实
业务单采购入库、销售出库、调拨、盘点等为什么发生变化是,业务来源事实
库存流水t_scm_inventory_0_{sid%128}发生了多少变化是,过程事实
实时库存t_scm_inventory_real_time现在还有多少是,当前结果
缓存/索引Redis、Mongo、ES页面如何快速查否,可重建
预警/报表warning、DWD/Hologres是否需要补货、经营统计派生结果

2.2 不能用一个数字代替全部口径

同一 invId 可能同时存在:

  • 多个仓库 location_id。
  • 多个货位 location_area_id。
  • 普通品、不良品、微仓、微仓退货等 area_type。
  • 快准中心商品和服务站自建第三方商品。
  • 正常库存、业务锁定量、活动锁定量和在途量。
  • 当前月流水与历史归档流水。

3. 适用现象与首查方向

现象首查常见原因
列表显示有库存,加购失败列表聚合口径与交易仓/货位ES 旧值、活动指定仓、锁量、排除不良品
出库提示库存不足实时库存精确维度查错仓、锁定量、禁止负库存分支
入库成功库存没增加业务明细、流水、实时表库存 Service 未执行、事务回滚、维度错误
流水存在但实时库存没变流水和实时写入顺序非同一事务、实时更新失败后异常处理错误
实时库存变了但无流水InventorySer::save 顺序实时先改、批量流水插入失败且外层无事务
删除业务单后库存未恢复软删流水和反向数量delete() 未走、billType/transType 不匹配
库存出现负数负库存开关和商品归属当前站点允许负库存或历史脏数据
同维度有两条实时记录唯一索引线上唯一键缺失、并发首次插入
盘点后仍不一致盘点分片、差异流水查错 10 分片、盘盈盘亏未落库存
预警数量错误实时量、在途量、上下限inventory_event 未消费、公式口径误解
报表与页面不一致历史归档和同步时间只查当月流水、DWD 延迟、成本口径不同
秒杀显示库存但不能买活动范围、锁定/已用量页面库存不是最终可购买量

4. 排查前事实卡

环境:
异常时间范围:
sid:
invId:
skuId / 商品编码:
商品归属 sid(快准中心=1 或服务站自建):
仓库 locationId:
货位 locationAreaId:
area_type:
业务单类型/单号/主键:
transType:
页面展示库存:
交易校验库存:
预期变化量:
实际实时变化量:
是否涉及锁定量/在途量/活动量:
是否跨月:

缺少 sid、invId、仓库或时间点时,不要直接开始修数据。

5. 三套分表规则必须分开

5.1 库存流水:128 分片

库存流水物理表 = t_scm_inventory_0_{sid % 128}

InventorySubModel::setSid() 和 getInventorySubTable() 已确认使用这一规则。

5.2 实时库存:当前为单表

t_scm_inventory_real_time

InventoryRealTimeModel 所有读写直接使用 SCM_INVENTORY_REAL_TIME,当前代码没有追加 sid % 128。sid 是表内租户字段,不是表名后缀。

5.3 盘点明细:10 分片

盘点明细物理表 = t_scm_inventory_check_info{sid 最后一位}

代码既有 sid % 10,也有 substr($sid, -1),结果等价。

5.4 表定位字典

数据物理表规则备注
新库存流水t_scm_inventory_0_{sid%128}128 分片当前 InventorySer 写入
旧当月库存t_scm_inventory历史基表当前保存代码已注释旧写入
旧流水明细t_scm_inventory_info以历史代码/DDL 为准不应默认是当前主路径
往月库存th_scm_inventory历史归档跨月对账必须考虑
实时库存t_scm_inventory_real_time单表当前数量结果
库存预警t_scm_inventory_warning单表派生结果
盘点主单t_scm_inventory_check当前代码/DDL盘点批次
盘点明细t_scm_inventory_check_info{sid%10}10 分片PDDId 关联主单
flowchart TB
    SID["sid"] --> M128["sid % 128"]
    SID --> M10["sid % 10"]
    SID --> COL["单表 sid 字段"]
    M128 --> LEDGER["inventory_0_n"]
    M10 --> CHECK["inventory_check_infon"]
    COL --> RT["inventory_real_time"]
    COL --> WARN["inventory_warning"]

5.5 常见误查

  • 对实时库存追加 _0_{sid%128},得到不存在或错误表。
  • 只查旧 t_scm_inventory,漏掉新 128 分片流水。
  • 跨月只查当前分片,不查历史归档。
  • 盘点明细写成下划线后缀,实际常量直接拼个位数。
  • 用 skuId 查流水但业务记录只稳定保存 invId,或反过来。

6. 核心入口、Service 和模型

模块文件作用
库存写入application/Services/Storage/InventorySer.php写实时数量和 128 分片流水
库存查询application/Services/Storage/InventoryQuerySer.php按仓库/货位聚合
实时库存 Modelapplication/models/bs/InventoryRealTimeModel.php行锁、增加、扣减、聚合
流水分片 Modelapplication/models/bs/InventorySubModel.php保存、软删、查询 128 分片
旧库存 Modelapplication/models/bs/InventoryModel.php历史查询兼容
库存预警application/models/bs/InventoryWarningModel.php预警/建议数量
仓库application/Services/Storage/StorageSer.php仓库与货位关系
套包application/Services/Storage/PackageSer.php拆包/组包双向库存
盘点application/Services/Gpda/TakeStockSer.php盘点差异和 10 分片明细
PDA 盘点application/Services/Pda/TakeStockSer.phpPDA 扫描和盘点处理
MQ 生产application/Services/Mq/MqSer.php发送 inventory_event
MQ 消费application/controllers/tasks/SaasOrderNotify.php重算库存预警
商品查询application/Services/Materiels/*ES/缓存商品库存展示
rg -n "function save|function changeInventoryQty|function delete" application/Services/Storage/InventorySer.php
rg -n "increaseQty|decrementQty|FOR UPDATE" application/models/bs/InventoryRealTimeModel.php
rg -n "SCM_INVENTORY.'_0_'|SCM_INVENTORY_SUB_NUM" application/models/bs/InventorySubModel.php application/config/tables.php

7. 库存写入真实顺序

7.1 InventorySer::save()

对每一行库存变化,代码按以下顺序执行:

  1. 如果传入旧业务单 iid,先调用 delete(iid, billType) 反向恢复旧流水。
  2. 构建新的库存流水数组。
  3. qty != 0 时调用 changeInventoryQty() 修改实时库存。
  4. 所有行处理完成后,批量写入 128 分片库存流水。
  5. 旧 InventoryModel->save() 已被注释,当前写新分片。
sequenceDiagram
    participant B as 业务Service
    participant I as InventorySer
    participant R as 实时库存单表
    participant L as 128分片流水
    B->>I: save(inventory, iid, billType)
    alt 编辑旧单
        I->>L: 查旧有效流水
        I->>R: 按 -old.qty 反向恢复
        I->>L: 旧流水 isDelete=1
    end
    loop 每一行
        I->>R: changeInventoryQty(qty)
    end
    I->>L: insert_batch 新流水

7.2 事务风险

InventorySer::save() 本身没有显式开启事务,依赖外层业务 Service 的事务。如果外层没有事务:

  • 实时库存可能已逐行修改。
  • 最后的流水批量插入可能失败。
  • 多行处理中间失败时,前几行可能已生效。

所以排查“实时变了但流水没有”时,要确认调用方是否在同一个数据库连接和事务中调用。

7.3 删除/编辑的反向语义

InventorySer::delete():

查旧有效流水 -> 对每行执行 0 - old.qty -> 旧流水软删

例如旧销售出库 qty = -2,删除时变化量为 +2,实时库存恢复;旧采购入库 qty = +5,删除时变化量为 -5。

8. 实时库存并发模型

8.1 增加库存

increaseQty():

  1. 按 sid + invId + locationId + locationAreaId 执行 SELECT ... FOR UPDATE。
  2. 已有行则 qty = qty + change。
  3. 无行则插入新记录,包含 skuId 和 areaType。

8.2 扣减库存

decrementQty() 同样先 FOR UPDATE,然后根据 minus 参数选择:

-- minus = true:允许扣成负数
update ... set qty = qty - :qty where id = :id;

-- minus = false:不允许负数
update ... set qty = qty - :qty where id = :id and qty >= :qty;

受影响行数小于等于 0 时,库存 Service 抛出“库存不足,出库失败”。

8.3 首次并发插入风险

两个事务同时发现该维度不存在时,都可能尝试插入。是否能防止重复行依赖线上唯一索引。建议唯一键至少覆盖:

sid + inv_id + location_id + location_area_id

sku_id/area_type 是否属于唯一键需以生产 DDL 和业务维度确认。

flowchart TD
    A["库存变化"] --> Q["FOR UPDATE 查精确维度"]
    Q --> E{"记录存在?"}
    E -->|是| U["加/减 qty"]
    E -->|否且增加| I["INSERT 新行"]
    E -->|否且扣减| F["返回失败"]
    U --> R{"扣减且禁止负库存?"}
    R -->|是| C["WHERE qty >= change"]
    R -->|否| N["允许负数"]

9. 负库存控制分支

9.1 先区分商品归属

GoodsModel->getGoodsBaseByInvId(invId) 返回商品归属站点:

  • good.sid == 1:快准中心商品。
  • 其他:服务站自建第三方商品。

9.2 当前静态代码分支

商品站点配置结果decrementQty(..., minus)效果
快准商品getStockControlStation(sid) 为真true允许扣成负数
快准商品为假falseqty >= 扣减量 才成功
第三方商品getThirdStockControlStation(sid) 为真false禁止负数
第三方商品为假true允许扣成负数

配置方法名与 minus 的最终效果容易凭直觉读反,排查时应以实际传参和 SQL 条件为准。

9.3 负库存不一定是脏数据

先确认:

  • 商品属于快准还是第三方。
  • 当前站点是否在对应控制名单。
  • 负数来自正常业务扣减、盘亏、货位调整还是历史修复。
  • 页面是否聚合了其他正库存货位。
  • 业务是否允许暂时负库存后再入库冲回。

10. 库存维度与查询口径

10.1 精确维度

sid + inv_id + location_id + location_area_id

这是实时更新行锁使用的维度。

10.2 常见聚合层级

场景聚合字段是否排除特殊货位
商品总库存sid + inv_id视接口而定
仓库库存sid + inv_id + location_id视 area_type 条件
货位库存精确四维否
门店库存门店关联多个 location_id 后按 invId 汇总可选排除不良品
可售库存实时量减锁定量通常排除不良品
活动库存指定仓/服务站仓 + 活动锁定/已用活动规则决定

10.3 已确认查询差异

  • getInventoryByStoreId(..., isNotBadArea=true) 增加 area_type = 0。
  • getStorageQty() 只包含普通、微仓、微仓退货三类货位。
  • getQtyByInvIdsWithSid(..., isBad=false) 会查不良品货位 ID 并排除。
  • 有些列表只判断任一行 qty > 0,有些按 sum(qty) > 0。
flowchart LR
    ROW["实时库存行"] --> EXACT["精确货位库存"]
    ROW --> STORAGE["仓库聚合"]
    ROW --> GOODS["商品聚合"]
    STORAGE --> SALEABLE["减锁定/排除不良品"]
    GOODS --> DISPLAY["ES/列表展示"]
    SALEABLE --> TRADE["加购/出库校验"]

“列表有库存、交易失败”的本质通常是 DISPLAY 与 TRADE 使用了不同聚合条件。

11. 交易类型与数量方向

11.1 增加库存

transType业务流水 qty
150501采购入库正
150502销售退货入库正
150706其他入库正
150701盘盈正
180001期初数量正或按初始化口径
191102拆包子件入库正
191202组包成品入库正
170502货位调整入正;但与销售订单编码冲突

11.2 减少库存

transType业务流水 qty
150601销售出库负
170402采购退货负
150806其他出库负
150801盘亏负
191101拆包成品出库负
191201组包子件出库负
170501货位调整出负

11.3 编码冲突

170502 同时出现在销售订单和货位调整入注释中。仅凭 transType 无法唯一解释业务,必须联合:

billType + transType + billNo/iid + 来源表

12. 一单到底排查流程

flowchart TD
    A["拿到业务单号"] --> B["确认 sid/invId/仓库货位"]
    B --> C["查业务主明细是否成功"]
    C --> D["计算 sid%128 查库存流水"]
    D --> E{"流水存在且方向正确?"}
    E -->|否| E1["业务到库存Service断点"]
    E -->|是| F["查实时库存精确维度"]
    F --> G{"变化量一致?"}
    G -->|否| G1["事务/重复/漏更新"]
    G -->|是| H["查缓存和交易查询口径"]
    H --> I["查预警/报表/下游"]

12.1 业务单据

先证明业务动作是否真的提交:

  • 采购入库:采购入库主明细和原采购/出库数量。
  • 销售出库:64 分片 150601 主明细,状态小于 6。
  • 销售退货:64 分片 150602 主明细。
  • 采购退货:采购退货单和出库事实。
  • 调拨:调出、调入两端和运输状态。
  • 盘点:盘点主单、10 分片明细、盘盈/盘亏流水。
  • 套包:主物料和子物料必须成对变化。

12.2 库存流水

-- :inv_suffix = sid % 128
select id, iid, sid, billNo, billType, transType, transTypeName,
       invId, skuId, locationId, locationAreaId,
       qty, price, amount, createTime, isDelete
from t_scm_inventory_0_:inv_suffix
where sid = :sid
  and (billNo = :bill_no or iid = :business_id)
order by createTime, id;

12.3 实时库存

select id, sid, inv_id, sku_id, location_id, location_area_id,
       area_type, qty
from t_scm_inventory_real_time
where sid = :sid and inv_id = :inv_id
order by location_id, location_area_id, id;

12.4 精确比较

对于一次业务变化:

期望实时变化量 = 同维度本次有效库存流水 qty 之和

不要直接用“当前实时库存 = 当前月流水总和”,因为还存在期初、历史归档和跨月余额。

13. 跨月与历史归档

13.1 正确守恒式

在某一时间截面:

当前库存
= 期初/历史结转
+ 历史有效入出库流水
+ 当期有效入出库流水

13.2 跨月误差来源

  • 只查 t_scm_inventory_0_n 当前活跃数据,漏历史归档。
  • 归档过程中重复复制或漏复制。
  • 归档后旧表数据未按相同 isDelete 口径处理。
  • 报表按业务日期,实时库存按实际提交时间。
  • 跨月撤销业务单,反向流水落当前月但原流水在历史表。

13.3 安全对账方式

优先选择一个已知正确的期初截面,再累计之后所有有效变化;不要在不知道归档策略时直接全量 sum(qty)。

14. 流水与实时不一致分类

分类流水实时典型原因
A有未变实时更新失败后流水仍提交、历史脚本直插流水
B无已变实时先改、流水批量插入失败且无外层事务
C重复重复变化客户端/MQ 重试无业务幂等
D重复单次变化流水重复导入但实时只更新一次
E单次重复变化同流水被多次应用到实时表
F正确聚合错误仓库/货位/不良品过滤不同
G正确页面旧值ES/缓存未刷新
H正确报表不同历史归档或离线同步延迟
flowchart TD
    A["发现差异"] --> B{"业务单真实吗?"}
    B -->|否| C["处理业务单/反向业务"]
    B -->|是| D{"流水正确?"}
    D -->|否| E["补/撤销流水"]
    D -->|是| F{"实时正确?"}
    F -->|否| G["按有效流水重建精确维度"]
    F -->|是| H{"页面/报表仍错?"}
    H -->|页面| I["刷新缓存/索引"]
    H -->|报表| J["查同步/归档口径"]

15. 删除、编辑和重复操作

15.1 编辑库存业务

正确语义通常是:

反向旧流水 -> 软删旧流水 -> 应用新数量 -> 写新流水

InventorySer::save($inventory, $iid, $billType) 会在 iid 非空时先删除旧库存事实。

15.2 billType/transType 匹配风险

删除查旧流水使用:

iid + billType + 可选 transType + isDelete=0

任一字段与原保存值不一致都会查不到旧流水,导致编辑时新库存叠加到旧库存之上。

15.3 重复提交检查

select iid, billType, transType, invId, locationId, locationAreaId,
       qty, count(*) as cnt
from t_scm_inventory_0_:inv_suffix
where sid = :sid and isDelete = 0
group by iid, billType, transType, invId, locationId, locationAreaId, qty
having count(*) > 1;

多行同维度可能是合法拆行,必须结合业务明细 ID、时间和单号判断,不能见 cnt > 1 就删除。

16. 盘点一致性

16.1 盘点数据链

sequenceDiagram
    participant P as PDA/GPDA
    participant C as 盘点主单
    participant D as 10分片盘点明细
    participant I as 库存Service
    participant R as 实时库存
    P->>C: 创建盘点批次
    P->>D: 保存 sysQty/checkQty
    D->>D: diff = checkQty - sysQty
    alt diff > 0
        D->>I: 盘盈 150701
    else diff < 0
        D->>I: 盘亏 150801
    end
    I->>R: 应用差异

16.2 盘点排查

  1. 查盘点主单状态、盘点时间和仓库范围。
  2. 计算 sid 个位数,查正确盘点明细表。
  3. 对比 sysQty、checkQty 和 diff。
  4. 查差异是否生成盘盈/盘亏业务事实。
  5. 查 128 分片流水是否有对应正/负数量。
  6. 查实时库存是否应用差异。
  7. 确认盘点期间是否仍允许其他出入库,避免时间截面漂移。

16.3 盘点 SQL

select *
from t_scm_inventory_check
where sid = :sid and id = :check_id;

select PDDId, sid, invId, skuId, locationId, locationAreaId,
       sysQty, checkQty, checkQty - sysQty as diff, checkType
from t_scm_inventory_check_info:check_suffix
where sid = :sid and PDDId = :check_id
order by invId, locationAreaId;

17. 套包、拆包和组包

17.1 数量方向

动作主/成品子件事件
拆包成品减少 191101子件增加 191102分别发库存事件
组包子件减少 191201成品增加 191202分别发库存事件

17.2 守恒检查

套包不一定是简单 1:1,需要按 BOM 数量:

成品变化量 * BOM单位用量 = 每个子件应变化量

金额和成本也可能按不同规则分摊,数量正确不代表成本正确。

flowchart LR
    P["成品包"] -->|"拆包 -1"| OUT["191101"]
    OUT -->|"BOM比例 +N"| C["子件 191102"]
    C -->|"组包 -N"| IN["191201"]
    IN -->|"+1"| P2["成品 191202"]

18. 货位调整和调拨

18.1 货位调整

同一商品总库存理论上不变:

原货位 170501 负数量 + 新货位 170502 正数量 = 0

InventorySer 不为这两类交易写最近出入库 Redis 时间,避免把内部搬位误算成真实进销。

18.2 站内/站间调拨

  • 调出端减少库存。
  • 调入端在确认入库后增加库存。
  • 在途期间两端实时库存之和可能暂时减少,需联合调拨状态理解。
  • 撤销调出要反向恢复原仓货位。
  • 站间调拨还涉及两个 sid,不能在一个站点维度内强行守恒。

19. 库存预警不是实时库存

19.1 inventory_event 用途

库存业务完成后,多个 Service 调用 MqSer::sendInventoryEvent(),消息包含:

{
  "inventoryData": [{"invId": 30001}],
  "iid": 0,
  "billType": "",
  "transType": null,
  "sid": 10001,
  "po_order_id": 0
}

该消息消费方不会再次修改实时库存,主要用于确定受影响商品并重算库存预警。

19.2 受影响商品来源

  • inventoryData[*].invId。
  • 删除业务单时,从已软删 128 分片流水按 iid/billType/transType 找 invId。
  • 采购在途变化时,从采购明细按 po_order_id 找 invId。

19.3 预警公式

warning_num = 实时可用库存 qty + 在途 waitQty - 下限 lowQty
suggest_num = 上限 highQty - 实时可用库存 qty - 在途 waitQty

页面将 warning_num < 0 视为需要预警。

flowchart LR
    EVENT["inventory_event"] --> IDS["收集 invIds"]
    IDS --> EXT["读取 highQty/lowQty"]
    EXT --> STATUS["仅正常/暂供商品"]
    STATUS --> RT["实时库存\n排除不良品"]
    STATUS --> WAY["采购在途 waitQty"]
    RT --> CALC["warning/suggest"]
    WAY --> CALC
    CALC --> UPSERT["inventory_warning"]

19.4 预警异常

现象检查
实时库存正确但预警旧消息是否发送/消费、invId 是否收集到
不再设置上下限但仍有预警lowQty/highQty 是否都为 0,删除是否执行
停用商品仍有预警商品状态过滤和旧记录删除
建议数量为负当前量+在途已超过上限,可能合理
预警库存少于页面库存消费者排除了不良品货位

20. Redis、ES 与页面库存

20.1 最近出入库时间

每次非货位调整库存变化后,InventorySer 更新 Redis Hash:

key: GOODS_INVENTORY_CHANGE_TIME + sid
field: invId
value: {"in":"时间", "out":"时间"}

它是时间辅助信息,不保存库存数量,不应拿来修复实时库存。

20.2 ES/物料缓存

  • 商品列表可能走 MaterielCacheStationEsSer。
  • 销售出库后会异步/容错更新近期销售数量字段。
  • ES 更新失败只记录日志,不一定回滚业务出库。
  • 列表中的库存或销售量可能短暂滞后。

20.3 判断原则

交易校验:以业务库实时库存和业务规则为准。
页面展示:可能来自聚合、缓存或索引。
缓存错误:重建缓存,不改业务库存。

21. 常用只读 SQL

21.1 实时库存精确维度

select id, sid, inv_id, sku_id, location_id, location_area_id,
       area_type, qty
from t_scm_inventory_real_time
where sid = :sid
  and inv_id = :inv_id
  and location_id = :location_id
  and location_area_id = :location_area_id;

21.2 商品全仓汇总

select inv_id, sum(qty) as total_qty,
       sum(case when area_type = 0 then qty else 0 end) as normal_qty
from t_scm_inventory_real_time
where sid = :sid and inv_id in (:inv_ids)
group by inv_id;

21.3 重复实时行

select sid, inv_id, location_id, location_area_id,
       count(*) as cnt, sum(qty) as qty_sum
from t_scm_inventory_real_time
group by sid, inv_id, location_id, location_area_id
having count(*) > 1;

21.4 业务单流水

select iid, billNo, billType, transType, invId, skuId,
       locationId, locationAreaId, qty, amount, createTime, isDelete
from t_scm_inventory_0_:inv_suffix
where sid = :sid and (iid = :iid or billNo = :bill_no)
order by createTime, id;

21.5 软删前后

select iid, billType, transType, invId, locationId, locationAreaId,
       isDelete, sum(qty) as qty
from t_scm_inventory_0_:inv_suffix
where sid = :sid and iid = :iid
group by iid, billType, transType, invId, locationId, locationAreaId, isDelete;

21.6 预警

select sid, inv_id, sku_id, warning_num, suggest_num
from t_scm_inventory_warning
where sid = :sid and inv_id in (:inv_ids);

22. 按现象执行的 SOP

22.1 列表有库存但出库失败

  1. 确认列表使用 skuId 还是 invId。
  2. 查列表展示值来自 ES、实时表还是聚合接口。
  3. 查交易使用的具体仓库和货位。
  4. 排除不良品/特殊货位。
  5. 减去锁定量、活动量或其他占用。
  6. 确认站点负库存分支。
  7. 查请求时间是否有并发扣减。

22.2 入库成功但实时库存未增加

  1. 查业务入库主明细已提交。
  2. 查 128 分片是否有正数量流水。
  3. 查实时精确货位行。
  4. 查业务 Service 是否调用 InventorySer。
  5. 查事务是否回滚或用了不同连接。
  6. 查是否入到另一仓库/货位。

22.3 流水有、实时少

  1. 按精确维度汇总异常时间段流水。
  2. 找一个可信期初截面。
  3. 排除软删流水。
  4. 查重复反向删除或重复扣减。
  5. 查实时表重复行和唯一索引。
  6. 评审按有效流水重建该维度。

22.4 实时有、流水少

  1. 查旧库存表和历史归档。
  2. 查期初库存 180001。
  3. 查是否手工/脚本直接改实时表。
  4. 查 InventorySer::save() 批量流水插入异常。
  5. 查调用方有无外层事务。
  6. 根据真实业务决定补流水或反向恢复实时。

22.5 删除业务单后数量不对

  1. 查原流水的 iid/billType/transType。
  2. 查旧流水是否 isDelete=1。
  3. 查反向变化是否作用于同仓同货位。
  4. 查删除调用传入的 billType 是否一致。
  5. 查业务单是否其实不允许删除而应撤销。

22.6 报表与实时不一致

  1. 固定同一截止时间。
  2. 确认报表按业务日期还是创建时间。
  3. 查当前流水和历史归档是否都进入。
  4. 查 DWD/Hologres 同步时间和失败任务。
  5. 确认报表是否排除不良品、撤销和软删。
  6. 分开数量差异和成本金额差异。

23. 修复决策

flowchart TD
    A["库存差异"] --> B["保存业务单/流水/实时快照"]
    B --> C{"业务单是否真实?"}
    C -->|否| D["撤销/反向业务单"]
    C -->|是| E{"流水是否完整?"}
    E -->|否| F["通过领域接口补业务流水"]
    E -->|是| G{"实时是否等于可信截面+流水?"}
    G -->|否| H["单维度重建实时结果"]
    G -->|是| I["刷新缓存/预警/报表"]
    D --> J["五层验收"]
    F --> J
    H --> J
    I --> J

23.1 修复优先级

  1. 原业务撤销、退货、反向入出库。
  2. 已有 InventorySer 领域接口。
  3. 单业务单、单商品、单货位补偿脚本。
  4. 只在确认过程事实完整时重建实时库存。
  5. 最后才直接 SQL,必须带断言、备份、行数和回滚。

23.2 不应做

  • 只把实时库存改成页面期望值。
  • 删除原库存流水。
  • 用缓存值覆盖业务库。
  • 忽略历史归档直接汇总当月。
  • 把所有负库存都归零。
  • 不区分仓库货位按 invId 全站修改。

24. 修复后五层验收

24.1 业务层

  • [ ] 原业务单状态和数量正确。
  • [ ] 撤销/反向业务有审计记录。
  • [ ] 不会再次被定时任务重放。

24.2 流水层

  • [ ] 128 分片流水完整。
  • [ ] 正负方向与 transType 一致。
  • [ ] 旧流水软删和新流水关系清楚。
  • [ ] 无无法解释的重复行。

24.3 实时层

  • [ ] 精确仓库货位数量正确。
  • [ ] 商品/仓库聚合结果正确。
  • [ ] 不良品和特殊货位口径正确。
  • [ ] 并发出库仍受负库存规则保护。

24.4 派生层

  • [ ] inventory_event 重算预警。
  • [ ] ES/物料缓存刷新。
  • [ ] 最近出入库时间符合业务。
  • [ ] 报表在同步后与口径一致。

24.5 回归层

  • [ ] 同商品再次采购入库。
  • [ ] 同商品再次销售出库。
  • [ ] 销退/采退、盘点和调拨。
  • [ ] 套包拆组和活动库存。

25. 完整回归矩阵

25.1 正向库存

  • [ ] 快准采购入库。
  • [ ] 第三方商品入库。
  • [ ] 销售退货入库。
  • [ ] 其他入库、盘盈、期初。
  • [ ] 调拨入库、货位调整入。

25.2 负向库存

  • [ ] 普通销售出库。
  • [ ] 采购退货出库。
  • [ ] 其他出库、盘亏。
  • [ ] 调拨出库、货位调整出。
  • [ ] 快准/第三方不同负库存配置。

25.3 编辑撤销

  • [ ] 编辑业务单先反向旧流水再写新流水。
  • [ ] 删除/撤销恢复实时库存。
  • [ ] 重复请求幂等。
  • [ ] 中间异常同事务回滚。

25.4 特殊场景

  • [ ] 普通品和不良品。
  • [ ] 多仓库、多货位、微仓。
  • [ ] 套包拆包/组包守恒。
  • [ ] 盘点期间并发出入库。
  • [ ] 跨月撤销和历史归档。
  • [ ] 秒杀指定仓、锁定量和已用量。

26. 监控建议

指标维度告警条件
实时库存负数sid/商品归属不允许负数站点出现负值
同维度重复行sid/inv/仓/货位任意新增
业务单无库存流水billType/transType完成单超过短延迟仍无流水
流水无业务单sid/iid任意孤儿候选
实时变化无流水sid/inv/时间窗超过容忍阈值
inventory_event 失败routing key连续失败/积压
预警重算延迟sid/inv超过 SLA
ES 库存延迟sid/inv展示与实时差异超时
归档对账差异月份/分片非零
盘点差异率sid/仓库突增

27. 已确认与待确认

27.1 静态代码已确认

  • 新库存流水表按 t_scm_inventory_0_{sid%128}。
  • 实时库存当前是单表 t_scm_inventory_real_time。
  • 盘点明细按 sid 个位数 10 分片。
  • InventorySer::save() 先逐行改实时库存,再批量写流水。
  • InventorySer 自身不显式开事务,依赖调用方外层事务。
  • 编辑/删除通过反向旧流水数量恢复实时库存,并软删旧流水。
  • 实时库存增加和扣减都先按精确维度 FOR UPDATE。
  • minus=true 允许负数,minus=false 使用 qty >= change 限制。
  • 快准商品和第三方商品的负库存配置分支不同。
  • 非货位调整会在 Redis 记录最近入/出库时间。
  • inventory_event 重算库存预警,不直接修改实时库存。
  • 预警公式包含实时库存、采购在途和上下限。
  • 当前旧库存表保存调用已注释,新流水是主写路径。

27.2 环境待确认

  • 实时库存线上唯一索引定义。
  • 128 分片建表完整性、索引和数据量分布。
  • 当月流水到 th_scm_inventory 的归档任务、时间和幂等。
  • 历史旧表与新分片的迁移/双写边界。
  • 各业务入口是否都使用同一数据库连接和外层事务。
  • 快准/第三方负库存配置的业务命名和当前站点名单。
  • ES/Mongo/Redis 库存字段刷新 SLA。
  • inventory_event 的 binding、重试、死信和积压监控。
  • 盘点期间是否冻结出入库及时间截面算法。
  • 报表从业务库、历史表还是 DWD/Hologres 取数。

28. 证据来源

主题文件
表和分片常量application/config/tables.php
库存写入application/Services/Storage/InventorySer.php
库存查询application/Services/Storage/InventoryQuerySer.php
128 分片流水application/models/bs/InventorySubModel.php
实时库存application/models/bs/InventoryRealTimeModel.php
旧库存兼容application/models/bs/InventoryModel.php
库存预警application/models/bs/InventoryWarningModel.php
库存事件生产application/Services/Mq/MqSer.php
库存事件消费application/controllers/tasks/SaasOrderNotify.php
盘点application/Services/Gpda/TakeStockSer.php
PDA 盘点application/Services/Pda/TakeStockSer.php
交易类型application/KzData/Enums/TransTypeEnums.php
套包application/Services/Storage/PackageSer.php
仓库货位调整application/service/bs/InvlocationService.php

29. 一页式执行结论

先固定 sid、invId、仓库、货位和时间截面。
库存流水查 t_scm_inventory_0_{sid%128};实时库存查单表;盘点明细查 sid 个位数分片。
先证明业务单,再看流水,最后验证实时结果;缓存和报表是派生层。
正数增加、负数减少,但 transType 170502 有语义冲突,必须联合 billType。
列表库存与交易库存可能因仓库、货位、不良品、锁定量不同而不同。
实时先改、流水后批量插入,外层事务缺失会产生部分成功风险。
负库存是否允许要按商品归属和站点配置分支判断。
修复优先做反向业务或领域补偿,不能只改实时库存。
修复后验收业务、流水、实时、缓存预警和报表。

请求-日志-数据变更追踪卡

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
业务出入库采购/销售/退货/调拨业务单、SKU、仓库货位、数量、transType各领域入口InventorySer业务单号 + 交易类型流水和实时库存同步变化
PDA/GPDA 盘点扫码盘点入口盘点单、账面量、实盘量PDA/GPDA ControllerTakeStockSer盘点单 + SKU差异形成调整流水
SAAS/MQ 库存事件DEST_DGJ inventory_event站点、商品、数量、事件键SaasOrderNotify库存 Service/MQ消息业务键异步库存动作幂等执行
查询/缓存商品列表、库存查询、预警四维库存键、可售口径查询 ControllerInventoryQuerySer/Cache/Warningsid+skuId/invId+storage+location返回实时量、预警与缓存视图

日志证据矩阵

| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 调用方 | 采购/销售/盘点 Service 日志 | request_id、业务单、SKU、transType | 调用 InventorySer 且业务 commit | 业务失败但库存有变、参数方向错误 | 单号查库存主单 | | 流水写入 | InventorySer/InventorySubModel | 库存主单、业务单、分片键 | 主单和全部明细 insert | 分片漏写、批量插入部分失败 | 主单 PK 查分片明细 | | 实时更新 | InventoryRealTimeModel | 四维键、影响行数、deadlock | qty 按符号改变 | update 0、重复扣加、负库存限制 | 四维键与流水净额对账 | | MQ/缓存预警 | MqSer/Cache/Warning | 业务单、routing key、库存键 | 消费 ACK,缓存/预警追平 | DB 正确但派生层旧 | 同一库存键比较数据时间 |

环节数据变更台账

步骤代码位置事务读取事实写入表/缓存/MQ字段或数量变化回查证据
校验维度InventorySer/StorageSer事务内sid、SKU/inv、仓库、货位、负库存配置无非法维度零写入输入快照、基础表记录
更新实时量changeInventoryQty外层事务依赖调用方四维行当前 qtySCM_INVENTORY_REAL_TIME入 old+n;出 old-n前后值、影响行数
写过程流水InventorySer::save应与实时更新同事务业务单和本次变化SCM_INVENTORY、分片 SCM_INVENTORY_INFO主单 insert;明细 qty=±n单号、transType、明细合计
派生同步MqSer、Cache、Warningcommit 后异步新实时量MQ、Redis、预警表DB 不变;缓存/预警追平ACK、缓存值、预警上下限
一致性修复领域反向单/补偿 Service小批事务基准量、期间流水、当前实时量反向流水及实时表期末=期初+期间净额;不直接孤立改实时量原单+补偿单、重复执行零变化

子模块追踪:inventory-write-check 库存写入与实时量排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
写入库存采购、销售、退货、调拨调用request ID、业务单、sid、SKU、transTypeapplication/Services/Storage/InventorySer.php四维库存键、数量方向、负库存配置、重复业务单外层本地事务内更新实时 qty: old +/- n,再写库存主单和分片流水business/inventory billNo + SKU + transType任一步失败需确认外层事务是否回滚;无事务时按事实补缺失一侧
结果回查业务返回后核对库存库存业务单、四维键、entryIdapplication/models/bs/InventoryRealTimeModel.php流水净额和实时前后量查询只读 不写;after=before+sum(flow)inventory billNo + four-dimension key不允许只看页面缓存;流水/实时不平时走领域反向或补偿

子模块追踪:inventory-negative-check 负库存控制排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
扣减校验销售、退货、调拨扣库存sid、SKU/invId、仓库货位、扣减量application/Services/Storage/InventorySer.php商品归属、站点负库存配置、当前四维量本地事务条件更新 qty: old -> old-n;不允许时影响行 0request ID + SKU + storage/location + affected rows配置允许时负数可为合法事实;不允许时回滚业务单
并发回查页面有量但交易提示不足同一四维键、并发请求 IDapplication/models/bs/InventoryRealTimeModel.php聚合页面口径与交易精确口径、锁后当前量查询只读 不写;以事务时精确行结果为准request IDs + four-dimension key + deadlock/row count死锁可重试;余额不足不可用缓存旧值强制重试

子模块追踪:inventory-archive-check 分片、跨月与历史归档排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
期间对账跨月库存报表或历史追溯sid、SKU、月初/月末截面application/models/bs/InventoryModel.phpTH_SCM_INVENTORY 月归档、当期流水、实时量查询只读 不写;验证 期末=期初+期间净额report request ID + period + inventory key分片/时区/软删口径不一致先统一,禁止拿当前实时代替历史截面
归档核验月结归档生成后检查归档月份、批次、站点application/KzData/Enums/TransTypeEnums.php归档行数、期间最后流水、任务完成状态归档任务事务写历史快照 none -> month snapshot;不改业务流水batch + month + sid + row count生产任务配置需环境确认;缺片只补缺失分片并重算校验和

子模块追踪:inventory-repeat-check 删除、编辑与重复操作排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
重复识别同业务单重复提交、编辑或删除business billNo、billType/transType、entryIdapplication/Services/Storage/InventorySer.php原流水、软删标记、实时量、请求幂等键本地事务已有业务键则 0 新增;编辑/删除走反向数量 old -> correctedrequest IDs + billNo + transType + row countbillType/transType 错配可能误删;先确认原流水集合再操作
修复验收历史重复流水或反向未生效原/补偿库存单、SKU、四维键application/models/bs/InventoryModel.php有效与软删流水净额、实时结果查询只读 不写;补偿后重复执行影响行 0original/compensate billNo + SKU保留原记录和审计链,禁止物理删流水或孤立改实时表

子模块追踪:inventory-stocktake-check 盘点一致性排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
盘点提交PDA/GPDA 扫码提交实盘数盘点单、sid、SKU、仓位、账面/实盘量application/Services/Pda/TakeStockSer.php、application/Services/Gpda/TakeStockSer.php盘点状态、账面截面、重复明细盘点本地事务写分片明细和差异;确认后按差额 qty: old +/- diffrequest ID + stocktake billNo + SKU/location重复扫码按明细键更新/拒绝;账面截面变化需重新确认差异
差异回查盘盈盘亏后实时库存不符盘点单、差异库存单application/KzData/Enums/TransTypeEnums.php盘点明细、盘盈/盘亏流水、实时四维量查询只读 不写;差异和 150701/150801 等交易方向一致stocktake/inventory billNo + transType有差异单无流水时只补库存动作,不重开整张盘点

子模块追踪:inventory-package-check 套包、拆包与组包排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
组拆包套包组装或拆解套包业务单、成品 SKU、组件 SKU、数量application/Services/Storage/PackageSer.phpBOM/套包关系、成品和组件四维库存、倍数组拆包本地事务组件与成品反向变化,分别写流水 old +/- nrequest ID + package billNo + parent/component SKU任一组件不足整单回滚;部分成功按同一套包单核对全组件
守恒回查套包库存或组件数量不平套包单、BOM 版本、库存单application/Services/Storage/PackageSer.php每组件应耗/应返量和有效流水查询只读 不写;按 BOM 倍数验证成品与组件守恒package/inventory billNo + SKUBOM 版本变化需按交易时快照解释,不能用当前 BOM 直接补算

子模块追踪:inventory-transfer-check 货位调整与调拨排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
货位调拨货位调整、站内/站间调拨出入库调拨单、SKU、源/目标仓位、数量application/service/bs/InvlocationService.php -> application/Services/Storage/StorageSer.php源库存、目标维度、调拨状态、在途量调拨本地事务源 qty -n、目标 qty +n 或先在途后入库,并写成对流水request ID + transfer billNo + source/target key目标写失败必须回滚/补偿源扣减;站间调拨需分别查出库和入库终态
对账回查调拨后总量或货位不对调拨单、两端库存业务单application/Services/Storage/InventorySer.php源、在途、目标流水及实时量查询只读 不写;非损耗调拨总量守恒transfer/inventory billNos + SKU只补缺失端,已完成端不得重放;生产修复先快照两端

子模块追踪:inventory-warning-check 库存预警排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
预警重算inventory_event 或库存变动后异步刷新message ID、sid、SKU/invIdapplication/controllers/tasks/SaasOrderNotify.php -> application/models/bs/InventoryWarningModel.php实时库存、预警上下限、受影响商品集合消费本地事务 upsert 预警 old -> new;不改变实时库存message ID + event + sid/SKUACK 不证明页面已刷新;失败按受影响 SKU 重算,不重做库存交易
结果核对预警与实际库存不一致四维/聚合库存键、warning rowapplication/Services/Mq/MqSer.php实时量、阈值、预警更新时间和消息时间查询只读 不写;公式结果应等于预警状态SKU + warning ID + updatedAt配置和消费者进程需环境确认;DB 主事实正确时仅补预警事件

子模块追踪:inventory-cache-check Redis、ES 与页面库存排查

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
页面查询商品列表、搜索、库存列表sid、SKU/invId、仓库口径、queryapplication/Services/Storage/InventoryQuerySer.phpDB 实时库存、Redis 最近出入库信息、ES/物料缓存查询只读 不写;页面返回可能是聚合/派生口径request ID + query + sid/SKU + cache hit页面值不作交易真值;先用 DB 四维量证明,再定位缓存/索引
派生刷新库存 commit 后消息驱动缓存/索引business key、SKU、routing keyapplication/Services/Mq/MqSer.php已提交实时量、缓存版本/更新时间DB 事务外更新 Redis/ES old -> latest,核心库存不变billNo + SKU + routing key + refresh result发布失败只补刷新;Redis 单值大小和 TTL 需查目标环境配置