本手册用于处理“页面有库存但下单/出库失败”“业务单完成但库存未变”“库存流水与实时库存不一致”“盘点和报表库存不一致”等问题。
库存不是一个数字,而是一组带站点、商品、仓库、货位、货位类型、时间和业务来源的事实。排查时必须同时看业务单、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 | 按仓库/货位聚合 |
| 实时库存 Model | application/models/bs/InventoryRealTimeModel.php | 行锁、增加、扣减、聚合 |
| 流水分片 Model | application/models/bs/InventorySubModel.php | 保存、软删、查询 128 分片 |
| 旧库存 Model | application/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.php | PDA 扫描和盘点处理 |
| 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()
对每一行库存变化,代码按以下顺序执行:
- 如果传入旧业务单
iid,先调用delete(iid, billType)反向恢复旧流水。 - 构建新的库存流水数组。
qty != 0时调用changeInventoryQty()修改实时库存。- 所有行处理完成后,批量写入 128 分片库存流水。
- 旧
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():
- 按
sid + invId + locationId + locationAreaId执行SELECT ... FOR UPDATE。 - 已有行则
qty = qty + change。 - 无行则插入新记录,包含
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 | 允许扣成负数 |
| 快准商品 | 为假 | false | qty >= 扣减量 才成功 |
| 第三方商品 | 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 盘点排查
- 查盘点主单状态、盘点时间和仓库范围。
- 计算
sid个位数,查正确盘点明细表。 - 对比
sysQty、checkQty和diff。 - 查差异是否生成盘盈/盘亏业务事实。
- 查 128 分片流水是否有对应正/负数量。
- 查实时库存是否应用差异。
- 确认盘点期间是否仍允许其他出入库,避免时间截面漂移。
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 列表有库存但出库失败
- 确认列表使用
skuId还是invId。 - 查列表展示值来自 ES、实时表还是聚合接口。
- 查交易使用的具体仓库和货位。
- 排除不良品/特殊货位。
- 减去锁定量、活动量或其他占用。
- 确认站点负库存分支。
- 查请求时间是否有并发扣减。
22.2 入库成功但实时库存未增加
- 查业务入库主明细已提交。
- 查 128 分片是否有正数量流水。
- 查实时精确货位行。
- 查业务 Service 是否调用
InventorySer。 - 查事务是否回滚或用了不同连接。
- 查是否入到另一仓库/货位。
22.3 流水有、实时少
- 按精确维度汇总异常时间段流水。
- 找一个可信期初截面。
- 排除软删流水。
- 查重复反向删除或重复扣减。
- 查实时表重复行和唯一索引。
- 评审按有效流水重建该维度。
22.4 实时有、流水少
- 查旧库存表和历史归档。
- 查期初库存 180001。
- 查是否手工/脚本直接改实时表。
- 查
InventorySer::save()批量流水插入异常。 - 查调用方有无外层事务。
- 根据真实业务决定补流水或反向恢复实时。
22.5 删除业务单后数量不对
- 查原流水的
iid/billType/transType。 - 查旧流水是否
isDelete=1。 - 查反向变化是否作用于同仓同货位。
- 查删除调用传入的 billType 是否一致。
- 查业务单是否其实不允许删除而应撤销。
22.6 报表与实时不一致
- 固定同一截止时间。
- 确认报表按业务日期还是创建时间。
- 查当前流水和历史归档是否都进入。
- 查 DWD/Hologres 同步时间和失败任务。
- 确认报表是否排除不良品、撤销和软删。
- 分开数量差异和成本金额差异。
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 修复优先级
- 原业务撤销、退货、反向入出库。
- 已有
InventorySer领域接口。 - 单业务单、单商品、单货位补偿脚本。
- 只在确认过程事实完整时重建实时库存。
- 最后才直接 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/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 业务出入库 | 采购/销售/退货/调拨 | 业务单、SKU、仓库货位、数量、transType | 各领域入口 | InventorySer | 业务单号 + 交易类型 | 流水和实时库存同步变化 |
| PDA/GPDA 盘点 | 扫码盘点入口 | 盘点单、账面量、实盘量 | PDA/GPDA Controller | TakeStockSer | 盘点单 + SKU | 差异形成调整流水 |
| SAAS/MQ 库存事件 | DEST_DGJ inventory_event | 站点、商品、数量、事件键 | SaasOrderNotify | 库存 Service/MQ | 消息业务键 | 异步库存动作幂等执行 |
| 查询/缓存 | 商品列表、库存查询、预警 | 四维库存键、可售口径 | 查询 Controller | InventoryQuerySer/Cache/Warning | sid+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 | 外层事务依赖调用方 | 四维行当前 qty | SCM_INVENTORY_REAL_TIME | 入 old+n;出 old-n | 前后值、影响行数 |
| 写过程流水 | InventorySer::save | 应与实时更新同事务 | 业务单和本次变化 | SCM_INVENTORY、分片 SCM_INVENTORY_INFO | 主单 insert;明细 qty=±n | 单号、transType、明细合计 |
| 派生同步 | MqSer、Cache、Warning | commit 后异步 | 新实时量 | MQ、Redis、预警表 | DB 不变;缓存/预警追平 | ACK、缓存值、预警上下限 |
| 一致性修复 | 领域反向单/补偿 Service | 小批事务 | 基准量、期间流水、当前实时量 | 反向流水及实时表 | 期末=期初+期间净额;不直接孤立改实时量 | 原单+补偿单、重复执行零变化 |
子模块追踪:inventory-write-check 库存写入与实时量排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 写入库存 | 采购、销售、退货、调拨调用 | request ID、业务单、sid、SKU、transType | application/Services/Storage/InventorySer.php | 四维库存键、数量方向、负库存配置、重复业务单 | 外层本地事务内更新实时 qty: old +/- n,再写库存主单和分片流水 | business/inventory billNo + SKU + transType | 任一步失败需确认外层事务是否回滚;无事务时按事实补缺失一侧 |
| 结果回查 | 业务返回后核对库存 | 库存业务单、四维键、entryId | application/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;不允许时影响行 0 | request ID + SKU + storage/location + affected rows | 配置允许时负数可为合法事实;不允许时回滚业务单 |
| 并发回查 | 页面有量但交易提示不足 | 同一四维键、并发请求 ID | application/models/bs/InventoryRealTimeModel.php | 聚合页面口径与交易精确口径、锁后当前量 | 查询只读 不写;以事务时精确行结果为准 | request IDs + four-dimension key + deadlock/row count | 死锁可重试;余额不足不可用缓存旧值强制重试 |
子模块追踪:inventory-archive-check 分片、跨月与历史归档排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 期间对账 | 跨月库存报表或历史追溯 | sid、SKU、月初/月末截面 | application/models/bs/InventoryModel.php | TH_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、entryId | application/Services/Storage/InventorySer.php | 原流水、软删标记、实时量、请求幂等键 | 本地事务已有业务键则 0 新增;编辑/删除走反向数量 old -> corrected | request IDs + billNo + transType + row count | billType/transType 错配可能误删;先确认原流水集合再操作 |
| 修复验收 | 历史重复流水或反向未生效 | 原/补偿库存单、SKU、四维键 | application/models/bs/InventoryModel.php | 有效与软删流水净额、实时结果 | 查询只读 不写;补偿后重复执行影响行 0 | original/compensate billNo + SKU | 保留原记录和审计链,禁止物理删流水或孤立改实时表 |
子模块追踪:inventory-stocktake-check 盘点一致性排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 盘点提交 | PDA/GPDA 扫码提交实盘数 | 盘点单、sid、SKU、仓位、账面/实盘量 | application/Services/Pda/TakeStockSer.php、application/Services/Gpda/TakeStockSer.php | 盘点状态、账面截面、重复明细 | 盘点本地事务写分片明细和差异;确认后按差额 qty: old +/- diff | request 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.php | BOM/套包关系、成品和组件四维库存、倍数 | 组拆包本地事务组件与成品反向变化,分别写流水 old +/- n | request ID + package billNo + parent/component SKU | 任一组件不足整单回滚;部分成功按同一套包单核对全组件 |
| 守恒回查 | 套包库存或组件数量不平 | 套包单、BOM 版本、库存单 | application/Services/Storage/PackageSer.php | 每组件应耗/应返量和有效流水 | 查询只读 不写;按 BOM 倍数验证成品与组件守恒 | package/inventory billNo + SKU | BOM 版本变化需按交易时快照解释,不能用当前 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/invId | application/controllers/tasks/SaasOrderNotify.php -> application/models/bs/InventoryWarningModel.php | 实时库存、预警上下限、受影响商品集合 | 消费本地事务 upsert 预警 old -> new;不改变实时库存 | message ID + event + sid/SKU | ACK 不证明页面已刷新;失败按受影响 SKU 重算,不重做库存交易 |
| 结果核对 | 预警与实际库存不一致 | 四维/聚合库存键、warning row | application/Services/Mq/MqSer.php | 实时量、阈值、预警更新时间和消息时间 | 查询只读 不写;公式结果应等于预警状态 | SKU + warning ID + updatedAt | 配置和消费者进程需环境确认;DB 主事实正确时仅补预警事件 |
子模块追踪:inventory-cache-check Redis、ES 与页面库存排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 页面查询 | 商品列表、搜索、库存列表 | sid、SKU/invId、仓库口径、query | application/Services/Storage/InventoryQuerySer.php | DB 实时库存、Redis 最近出入库信息、ES/物料缓存 | 查询只读 不写;页面返回可能是聚合/派生口径 | request ID + query + sid/SKU + cache hit | 页面值不作交易真值;先用 DB 四维量证明,再定位缓存/索引 |
| 派生刷新 | 库存 commit 后消息驱动缓存/索引 | business key、SKU、routing key | application/Services/Mq/MqSer.php | 已提交实时量、缓存版本/更新时间 | DB 事务外更新 Redis/ES old -> latest,核心库存不变 | billNo + SKU + routing key + refresh result | 发布失败只补刷新;Redis 单值大小和 TTL 需查目标环境配置 |