1. 这篇文档解决什么问题
基础资料和库存不是一个孤立功能,而是采购、销售、活动、移动商城、渠道订单和财务成本共同依赖的业务地基。
这篇文档用于回答下面这些具体问题:
- 商品中心传来一个 SKU 后,DGJ2 怎样校验、落库、更新缓存并通知搜索平台?
skuId、invId、sid、仓库和货位分别代表什么,查询时为什么不能混用?- 采购入库、销售出库、退货、调拨、盘点最终怎样改变实时库存?
- 为什么库存流水正确,实时库存仍可能不正确;反过来又为什么可能实时库存正确但报表不一致?
- 同一个商品为什么在列表有库存,在下单或出库时仍提示库存不足?
- 修改或撤销单据时,旧库存是怎样反向冲销的?
- 库存流水落在哪一张分片表,如何根据
sid找到正确表?
本文以当前工作区代码为证据。没有从代码确认的线上网关、环境开关和数据库约束会放在“待确认项”,不把推测写成事实。
2. 业务边界和责任划分
| 能力 | 负责的事实 | 不负责的事实 |
|---|---|---|
| 商品/物料 | SKU 编码、商品名称、品牌、分类、单位、供货状态、箱规、条码、适配信息 | 某个服务站当前可售数量 |
| 客户/供应商 | 交易对象、联系人、地址、账户和客户分组 | 具体订单的应收应付状态 |
| 门店/仓库/货位 | 库存放在哪里、是否是不良品货位 | 商品中心的供货库存 |
| 实时库存 | 当前 sid + invId + 仓库 + 货位 的数量结果 | 数量为什么变化 |
| 库存流水 | 哪张业务单据、以哪种交易类型、在何时改变了多少库存 | 当前结果一定等于流水简单求和;还要考虑软删除、迁移和历史数据 |
| 库存中心 | 快准供给库存、极速达/普通采购等外部库存口径 | DGJ2 服务站自有仓每个货位的实时数量 |
| 盘点 | 实盘数量、系统数量、差异和审核后的调整 | 在审核前直接替换实时库存 |
2.1 先区分三种“库存”
flowchart LR
A[商品中心或库存中心供给] -->|采购可买口径| B[采购和商城可订数量]
C[DGJ2 实时库存表] -->|服务站自有库存| D[销售出库和仓内作业]
E[库存流水分表] -->|过程证据| F[追溯入库出库调拨盘点]
B -.不是同一口径.-> C
E -->|业务上应能解释| C
“商品中心有货”“服务站仓里有货”“活动还有额度”是三种不同事实。出现库存争议时,第一步不是直接查某张表,而是先确认页面或接口采用哪一种口径。
3. 核心标识和仓储维度
| 字段 | 业务含义 | 常见来源 | 排查注意点 |
|---|---|---|---|
sid | 服务站/租户标识 | 登录会话、请求上下文 | 同一 invId 在不同 sid 的库存互不等价 |
skuId / sku_id | 商品中心物料编码 | 请求字段 skuCode | 字符串编码,适合跨系统定位 |
invId / inv_id | DGJ2 商品主键 | t_bs_goods.id | 单据明细和库存表大量使用 |
number | 原厂商产品码 | srcCompanyProductCode | 不能代替 skuId 或 invId |
productCode | 快准产品码 | companyProductCode | 与厂商产品码语义不同 |
buId | 业务单元/核算组织 | 业务单据 | 影响价格、成本、财务口径,不是仓库 ID |
storeId | 门店 ID | t_bs_store | 一个门店可映射多个仓库 |
locationId | 仓库 ID | t_bs_storage | 实时库存主维度之一 |
locationAreaId | 货位 ID | t_bs_storage_area | 同一仓库内可有正常品、不良品等货位 |
area_type | 货位性质 | 货位和实时库存 | 当前模型注释确认 0 普通、1 不良品 |
iid | 业务单据内部 ID | 采购/销售/调拨等主表 | 库存流水冲销的关键关联键 |
billType | 业务单据大类 | BillTypeEnums | 与 iid 一起定位流水,如 PUR、SALE |
transType | 库存动作类型 | TransTypeEnums | 决定入/出方向和业务含义 |
3.1 门店到货位的层级
flowchart TD
SID[服务站 sid] --> STORE[门店 t_bs_store]
STORE --> EXT[门店扩展 t_bs_store_ext]
EXT -->|locationId| STORAGE[仓库 t_bs_storage]
STORAGE --> REL[仓库货位关系]
REL --> AREA[货位 t_bs_storage_area]
AREA --> NORMAL[普通品 area_type=0]
AREA --> BAD[不良品 area_type=1]
AREA --> OTHER[移动仓等扩展类型]
InventorySer::getInventoryByStoreId() 会先通过门店扩展取得仓库列表,再按这些 locationId 汇总实时库存。调用方传了 storeId,并不代表库存表里直接有 storeId。
4. 代码入口地图
4.1 商品和基础资料入口
| 场景 | 入口 | 核心方法 | 主要下游 |
|---|---|---|---|
| 商品中心批量新增/覆盖物料 | application/controllers/inner/Goods.php | add() | MaterielSer::formatData()、updateMateriel() |
| 商品分类或展示属性更新 | 同上 | update() | updateMaterielExtraData() |
| 单物料更新 | application/controllers/inner/Materiel.php | update() | updateMateriel(..., false) |
| 老商品同步 | application/config/apis.php | shmInfoEdit -> goods.InfoEdit | 老 goods 控制器链路,与 inner/Goods.php 并存 |
| 客户维护 | application/controllers/basedata/Contact.php | query/add/update/disable/... | 客户、地址、分组、员工和账户相关模型 |
| 报价规则 | application/controllers/basedata/QuoteManager.php | ruleList/ruleSet/batchSetCustomPrice/... | PriceManagerSer、QuoteManagerSer |
| 调拨选品和仓库库存 | application/controllers/basedata/Allot.php | getSalesGoodsList()、getStorageAreaStock() | 基础库存查询 Service |
4.2 App 仓内和盘点入口
App 不是通过传统控制器逐个暴露,而是由 application/config/appapis.php 的接口码映射到 application/service/api/app/... 文件。
| 接口码 | 服务文件/动作 | 作用 |
|---|---|---|
appwmsSignList | app.wms.sign.signList | 待签收物流列表 |
appwmsSignIn | app.wms.sign.signIn | 物流签收 |
appwmsinstoList | app.wms.insto.inStoList | 待入库采购单列表 |
appwmsinstoDetail | app.wms.insto.inStoDetail | 入库单明细 |
appwmsStorageAreaList | app.wms.storageAreaList | 仓库对应货位 |
appwmsInStoSkuInfo | app.wms.insto.inStoSkuInfo | 扫码取得物料信息 |
appwmsInStroComfirm | app.wms.insto.inStroComfirm | 扫描入库确认 |
appwmsInStoInStorage | app.wms.insto.inStoInStorage | 批量完成采购入库和差异处理 |
appwmsCodeVerify | app.wms.codeVerify | 判断扫描码类型 |
appinvCheckAddSave | app.invCheck.invAddSave | 创建盘点单 |
appinvCheckScanInfo | app.invCheck.invScanInfo | 扫码读取盘点商品/货位 |
appinvCheckInfoAddSave | app.invCheck.invInfoAddSave | 保存实盘明细 |
appinvCheckStat | app.invCheck.invStat | 计算盘点统计和异常 |
appinvCheckStaUpdate | app.invCheck.invStaUpdate | 提交、更新或撤回盘点状态 |
appinvCheckConfirm | app.invCheck.beforeSubmitCheck | 提交前库存变化校验 |
表里的“接口码”是当前代码确认的路由键。HTTP 域名、统一网关路径、鉴权头和签名方式由具体环境决定,不能只凭此文件推断。
5. 商品中心同步接口契约
5.1 Goods::add() 请求结构
下面是根据当前控制器和 MaterielSer::checkData() 反推的最小业务结构,示例值已脱敏:
{
"itemCategoryCode": "CAT-001",
"itemRetailPrice": "299.00",
"itemShopPattenCarDesc": "适配车型说明",
"itemShopName": "商品展示标题",
"itemMinSubscribeQty": 1,
"productModel": "MODEL-X",
"skuInfoList": [
{
"skuCode": "SKU-10001",
"unitCode": "PCS",
"unitName": "个",
"skuSupplyStatusCode": 1,
"skuStockStatusCode": 1,
"skuHeatCode": 1,
"shipperCodes": "SHIPPER-01",
"skuBrandCode": "BRAND-01",
"skuBrandSeriesList": [],
"name": "示例物料",
"spec": "标准规格",
"packQty": 10,
"skuBarCode": "6900000000000",
"enable": 1,
"enableDSR": 1,
"enableRSR": 1,
"companyProductCode": "KZ-P-001",
"srcCompanyProductCode": "FACTORY-P-001",
"itemMinSubscribeQty": 1
}
]
}
关键校验:
skuInfoList必须存在且非空。- 每个 SKU 至少需要
skuCode、unitCode、skuSupplyStatusCode、skuStockStatusCode、itemMinSubscribeQty、skuHeatCode、shipperCodes、skuBrandCode。 - 单位必须已经同步,否则报“计量单位未同步”。
- 品牌必须已经同步,否则报“品牌未同步”。
- 商品分类必须存在,否则报“商品分类未找到”。
- 品牌系列存在于请求时,每个系列也必须已经同步。
itemMinSubscribeQty必须是数字。
5.2 字段转换口径
| 上游字段 | DGJ2 字段 | 规则 |
|---|---|---|
skuCode | skuId | 跨系统物料编码 |
skuSupplyStatusCode | status | 供货状态原值写入 |
skuStockStatusCode | stockType | 备货模式原值写入 |
skuBrandCode | brandId/brandName | 先查品牌基础资料 |
unitCode | unitId/unitName | 先查单位基础资料 |
shipperCodes | saleModel | 逐个查供应商,将有效供应商编号逗号拼接 |
packQty + unitName | packSpec | 例如 10个/箱 |
enable == 2 | isDelete = 1 | 禁用被转换成逻辑删除标志 |
flag == 1 | goodsType | 广宣品,否则普通商品 |
companyProductCode | productCode | 快准产品码 |
srcCompanyProductCode | number | 原厂商产品码 |
relCompanyProductCode | def | 关联产品码 |
params | item_attribute | 按属性名排序后以 *#|#* 拼接 |
5.3 商品同步完整时序
sequenceDiagram
autonumber
participant IC as 商品中心
participant G as inner/Goods
participant M as MaterielSer
participant DB as t_bs_goods及扩展关系
participant C as MaterielCacheSer
participant MQ as DGJ Notify MQ
participant VIN as VIN/搜索平台
IC->>G: 商品新增或更新请求
G->>M: formatGoodsData + formatData + formatExtData
M->>DB: 查询单位、品牌、分类、供应商
alt 前置基础资料缺失
DB-->>M: 未找到
M-->>IC: 业务错误,不落商品
else 校验通过
M->>DB: 按 skuId 更新或新增商品
M->>DB: 更新品牌系列关系
M->>C: saveMaterielCache(skuId, data)
M->>MQ: vin_goods_update(old_item,new_item)
MQ-->>VIN: 刷新搜索使用的物料数据
M-->>IC: 成功
end
5.4 新增与更新不是两个完全独立动作
MaterielSer::updateMateriel() 实际是 Upsert:
- 先通过缓存或数据库按
skuId查商品。 - 已存在则调用
GoodsModel::updateMateielBySkuId()。 - 不存在且
$isNeedAdd=true时新增。 inner/Materiel::update()传入$isNeedAdd=false,因此不存在会明确报错,不会静默新增。- 更新品牌系列关系。
- 写物料缓存。
- 发送
vin_goods_update,消息体包含变更前后的字段集合。
当前方法内部没有显式数据库事务。商品主表、品牌系列关系、缓存和 MQ 之间不是单一原子提交,排查“主表已变但搜索没变”时必须分段检查。
6. 物料状态和可用性
状态定义来自 application/KzData/Enums/MaterielEnums.php。
6.1 供货状态
| 值 | 名称 | 业务理解 |
|---|---|---|
1 | 正常 | 常规供货状态 |
2 | 暂供 | 临时可供,具体渠道是否展示还受查询条件约束 |
3 | 停供 | 不应再作为正常供给商品 |
4 | 停用 | 商品被停用 |
5 | 新品 | 新品状态,仍需结合渠道和采购规则判断能否购买 |
6.2 备货类型
| 值 | 名称 |
|---|---|
1 | 总仓 |
2 | 总仓/子仓 |
3 | 子仓 |
6.3 状态判断路径
flowchart TD
A[按 skuId 找到商品] --> B{isDelete 是否为 0}
B -- 否 --> X[禁用/逻辑删除]
B -- 是 --> C{供货状态是否被当前场景允许}
C -- 否 --> Y[不可采购或不展示]
C -- 是 --> D{服务站缓存或 ES 是否有该商品}
D -- 否 --> Z[主表有数据但查询侧不可见]
D -- 是 --> E{当前业务还需库存/价格/活动校验吗}
E -- 是 --> F[继续校验对应业务口径]
E -- 否 --> G[可进入下一步]
“商品存在”不等于“当前场景可购买”。业务还可能检查客户报价、服务站维度缓存、库存中心、活动范围、仓库库存和起订量。
7. 库存表与分片规则
7.1 核心物理表
| 常量 | 物理表 | 作用 | 关键维度 |
|---|---|---|---|
BS_GOODS | t_bs_goods | 商品主数据 | id/invId、skuId、sid、状态 |
BS_GOODS_EXT | t_bs_goods_ext | 商品扩展配置 | 商品扩展属性 |
BS_GOODS_STO | t_bs_goods_sto | 商品仓库配置 | 商品与仓库配置 |
BS_STORE | t_bs_store | 门店 | sid、门店 |
BS_STORE_EXT | t_bs_store_ext | 门店扩展 | storeId 到 locationId 的关系 |
BS_STORAGE | t_bs_storage | 仓库 | locationId |
BS_AREA | t_bs_storage_area | 货位 | locationAreaId、area_type |
SCM_INVENTORY_REAL_TIME | t_scm_inventory_real_time | 当前库存结果 | sid + inv_id + location_id + location_area_id |
SCM_INVENTORY | t_scm_inventory | 库存流水表名前缀 | 实际使用 128 张分片表 |
SCM_INVENTORY_WARNING | t_scm_inventory_warning | 库存预警 | 预警上下限和结果 |
SCM_INVENTORY_CHECK | t_scm_inventory_check | 盘点主表 | 盘点状态、仓库、时间 |
SCM_INVENTORY_CHECK_INFO | t_scm_inventory_check_info{0-9} | 盘点明细分表 | 按 sid 个位数分 10 张 |
7.2 库存流水分片
当前分片代码在 InventorySubModel::setSid():
table = t_scm_inventory_0_{sid % 128}
例如:
sid | 计算 | 流水表 |
|---|---|---|
1001 | 1001 % 128 = 105 | t_scm_inventory_0_105 |
2048 | 2048 % 128 = 0 | t_scm_inventory_0_0 |
查询流水前必须先确认 sid。只查 t_scm_inventory 或凭经验猜分表,会漏掉真实数据。
7.3 盘点明细分片不是同一规则
盘点明细在 InvCheckInfoModel 中使用:
table = t_scm_inventory_check_info + sid最后一位
库存流水是 128 分片,盘点明细是 10 分片,两者不能套用同一个计算方法。
8. InventorySer::save() 的真实执行顺序
8.1 调用方需要准备的库存行
{
"iid": 123456,
"sid": 1001,
"buId": 1,
"billNo": "XS-EXAMPLE-001",
"billDate": "2026-07-15",
"billType": "SALE",
"transType": 150601,
"transTypeName": "普通销售出库",
"invId": 98765,
"skuId": "SKU-10001",
"locationId": 20,
"locationName": "主仓",
"locationAreaId": 201,
"locationAreaName": "A-01-01",
"area_type": 0,
"price": "20.50",
"qty": -2,
"amount": "-41.00",
"discountRate": "1.0000",
"deduction": "0.00"
}
方向以 qty 正负为准:正数增加库存,负数减少库存。price 写流水前会取绝对值,但 qty 和 amount 的方向由业务调用方组装。
8.2 保存算法
sequenceDiagram
autonumber
participant Biz as 采购/销售/调拨业务 Service
participant IS as InventorySer
participant RT as 实时库存表
participant FL as 库存流水分表
participant R as Redis最近出入库时间
Biz->>IS: save(rows, iid, billType)
opt iid 非空,表示重写旧单据库存
IS->>FL: findByIid(iid,billType,isDelete=0)
loop 每条旧流水
IS->>RT: 按 -old.qty 反向修改实时库存
IS->>R: 更新最近入/出库时间
end
IS->>FL: 旧流水 isDelete=1
end
loop 每条新库存行
IS->>IS: 生成 Snowflake 流水 id
IS->>RT: changeInventoryQty(qty)
IS->>R: 更新最近入/出库时间
end
IS->>FL: insert_batch 新流水
IS-->>Biz: 返回
这里有四个容易忽略的事实:
- 实时库存逐行修改,库存流水在循环结束后才批量写入。
- 传入非空
iid时,先调用delete()冲销旧流水,再写新流水。 - 所谓删除流水实际是把
isDelete更新为1,不是物理删除。 InventorySer::save()自身没有开启事务;原子性依赖外层业务 Service 是否在同一数据库连接上包裹事务。
因此调用方如果未正确事务化,可能出现“前几行实时库存已改,后续异常,流水没有完整写入”的中间状态。
9. 实时库存增减算法和负库存开关
9.1 增库存
InventoryRealTimeModel::increaseQty():
- 按
sid + invId + locationId + locationAreaId执行SELECT ... FOR UPDATE。 - 已有记录则
qty = qty + ?。 - 没有记录则插入新行,并带入
skuId和areaType。 - 受影响行数小于等于 0 时,
InventorySer抛出“库存增加失败”。
9.2 扣库存
扣减前同样执行 SELECT ... FOR UPDATE。模型的 $minus 参数含义容易误读:
$minus | SQL 条件 | 结果 |
|---|---|---|
false | UPDATE ... SET qty=qty-? WHERE qty>=? | 严格校验,不允许扣成负数 |
true | UPDATE ... SET qty=qty-? | 允许出现负库存 |
9.3 快准商品与第三方商品使用不同名单
商品主表记录的商品归属 good.sid == 1 时,代码按“快准商品”处理;否则按“第三方商品”处理。
flowchart TD
A[qty 小于等于 0] --> B[按 invId 查商品]
B --> C{good.sid == 1}
C -- 快准商品 --> D{HASH_STOCK_CONTROL_STATION 有当前 sid}
D -- 有 --> E[minus=true 允许负库存]
D -- 无 --> F[minus=false 严格扣减]
C -- 第三方商品 --> G{HASH_THIRD_STOCK_CONTROL_STATION 有当前 sid}
G -- 有 --> H[minus=false 严格扣减]
G -- 无 --> I[minus=true 允许负库存]
这两个 Redis Hash 的注释和分支语义不同:
HASH_STOCK_CONTROL_STATION:代码注释是“库存限制服务站”,但在快准商品分支命中后实际传minus=true。CacheManage.php又把它展示为“允许快准商品负库存销售白名单”。排查时应以实际分支和管理端名称为准。HASH_THIRD_STOCK_CONTROL_STATION:命中后第三方商品严格扣减,可理解为“不允许第三方商品负库存销售名单”。
9.4 最近出入库时间缓存
除货位调整出/入 170501、170502 外,每次库存变化会写:
Redis Hash: goods_inventory_change_time_sid:{sid}
field: invId
value: {"in":"最近入库时间","out":"最近出库时间"}
这个缓存是辅助查询数据,不是库存事实来源。缓存写失败和库存事务的关系还需结合 Redis 客户端异常策略验证。
10. 常见库存交易类型
| 常量 | 值 | 方向/用途 |
|---|---|---|
TRANSTYPE_PU_PURCHASE | 150501 | 采购入库,通常 qty > 0 |
TRANSTYPE_PU_SALE_RETURN | 150502 | 销售退货入库,通常 qty > 0 |
TRANSTYPE_SA_INVOICE_SALE | 150601 | 销售出库,通常 qty < 0 |
TRANSTYPE_SA_INVOICE_RETURN | 150602 | 销售退货类流水,方向需看具体组装调用 |
TRANSTYPE_OI_IN | 150706 | 其他入库 |
TRANSTYPE_OI_OUT | 150806 | 其他出库 |
TRANSTYPE_OI_PROFIT | 150701 | 盘盈 |
TRANSTYPE_OI_LOSS | 150801 | 盘亏 |
TRANSTYPE_TRANSFER | 103091 | 调拨 |
TRANSTYPE_TRANSFER_S | 103092 | 服务站间调拨 |
TRANSTYPE_INVEN_INI | 180001 | 期初库存 |
TRANSTYPE_INVEN_OO | 180510 | 移动盘库 |
TRANSTYPE_OI_HWTZ | 170500 | 货位调整主类型 |
TRANSTYPE_OI_HWTZ_OUT | 170501 | 货位调整出 |
TRANSTYPE_OI_HWTZ_IN | 170502 | 货位调整入;与普通销售单编码存在历史复用 |
注意:170502 在枚举里同时被普通销售单和货位调整入使用,代码注释也明确提示“需要调整”。只按数字判断业务可能误判,必须同时看 billType、单据号、数量方向和调用入口。
11. 各业务如何接入库存
flowchart LR
PO[采购入库/采购退货] --> IS[InventorySer]
SA[销售出库/销售退货] --> IS
TF[站内调拨] --> IS
STF[服务站间调拨] --> IS
OI[其他出入库/货位调整] --> IS
PD[盘点审核后的盈亏] --> IS
IS --> RT[(实时库存)]
IS --> FL[(分片流水)]
IS --> RC[(最近出入库 Redis)]
SA --> ES[销售出库后更新 ES 销量]
当前代码中可直接找到的调用包括:
InvPoService:采购入库、采购退货、不良品售后出库。NormalSaleSer:销售出库、撤销和隔天核销式冲销。NormalSaleReturnSer:销售退货入库和删除冲销。InvTfService:站内调拨。Stf/InvoiceSer:服务站间调拨出库、入库和撤销。InvOiService:其他出入库、盈亏和货位调整。InvlocationService:货位相关调整。
销售出库完成后,NormalSaleSer 还会调用 InventorySer::updateEsGoodsGoodsSaleNum()。它只提取 transType == 150601 的商品,更新服务站 ES 的 goods_sale_num。该方法捕获异常并记录日志,不会让销售出库事务因 ES 更新失败而失败,因此存在“业务已出库但排序销量稍后不一致”的可接受降级状态。
12. 盘点业务链路
12.1 用户看到的流程
代码内盘点说明页明确给出的流程是:
创建盘点单 -> 盘点 -> 提交审核 -> 审核通过 -> 盘点成功
并明确说明:提交审核时不更新库存,审核通过后才更新库存;盘点期间可以营业,但发生过出入库的 SKU 建议重新盘点。
12.2 数据流
sequenceDiagram
autonumber
participant APP as WMS/盘点 App
participant API as app.invCheck
participant PM as 盘点主表
participant PI as 盘点明细分表
participant RT as 实时库存
participant AUDIT as 审核处理
APP->>API: appinvCheckAddSave
API->>PM: 创建 PDD 单,transType=180510
APP->>API: 扫货位/商品
API->>RT: 读取当时系统库存
API->>PI: 保存 checkQty 和货位明细
APP->>API: appinvCheckConfirm
API->>RT: 检查盘点期间库存变化
APP->>API: appinvCheckStaUpdate submit
API->>PM: 进入待审核
AUDIT->>PM: 审核通过
AUDIT->>RT: 按盘盈/盘亏差额调整库存
12.3 已确认的状态约束
invStaUpdate.php 对动作做了前置状态判断:
submit只允许当前checkStatus == 2。update只允许当前checkStatus == 4。rollback只允许当前checkStatus == 3。invInfoAddSave只允许待盘点或盘点中状态保存明细。
完整状态中文映射应以 PddEnums 和审核代码为准,本文不根据零散页面文案补造未确认状态。
13. 数据一致性必须同时核对什么
13.1 库存事实核对顺序
- 确认
sid、skuId、invId对应是否正确。 - 确认业务单据是否已经执行到实际入库/出库节点,而不只是创建或审核。
- 计算
sid % 128,查询正确的库存流水分表。 - 排除
isDelete = 1的已冲销流水。 - 按仓库和货位查询实时库存,不要先做全站汇总掩盖局部错误。
- 查看负库存开关,确认扣减是否允许穿透零库存。
- 再对比库存中心、活动锁定量、报表或 ES;它们是不同投影,可能存在同步延迟。
13.2 推荐对账维度
流水净变化 = SUM(qty WHERE isDelete=0)
实时结果 = SUM(t_scm_inventory_real_time.qty)
两者不能在没有期初基准和历史迁移边界的情况下直接断言必须相等。正确做法是选定一个已知正确的起点,再核对起点之后的有效流水净变化。
14. 常用排查 SQL
以下 SQL 只提供结构,执行前替换占位符并确认环境。默认只读。
14.1 商品编码映射
SELECT id AS invId, sid, skuId, number, productCode, name,
status, stockType, isDelete, categoryId, brandId
FROM t_bs_goods
WHERE skuId = :sku_id
OR id = :inv_id;
14.2 按仓库货位查看实时库存
SELECT 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;
14.3 汇总正常品与不良品
SELECT area_type, SUM(qty) AS qty
FROM t_scm_inventory_real_time
WHERE sid = :sid
AND inv_id = :inv_id
GROUP BY area_type;
14.4 查有效库存流水
先计算分片:
SELECT MOD(:sid, 128) AS shard_no;
再查询 t_scm_inventory_0_{shard_no}:
SELECT id, iid, sid, billNo, billType, transType, transTypeName,
invId, skuId, locationId, locationAreaId, qty, amount,
isDelete, createTime
FROM t_scm_inventory_0_XXX
WHERE sid = :sid
AND (iid = :iid OR billNo = :bill_no OR invId = :inv_id)
ORDER BY createTime, id;
14.5 查被冲销的旧流水
SELECT iid, billNo, billType, transType, invId, qty, isDelete, createTime
FROM t_scm_inventory_0_XXX
WHERE sid = :sid
AND iid = :iid
ORDER BY isDelete, createTime;
14.6 查盘点主表与正确明细分表
SELECT id, sid, pddNo, checkStatus, locationId, startDate, transType
FROM t_scm_inventory_check
WHERE sid = :sid
AND id = :pdd_id;
-- 明细表后缀 = sid 最后一位
SELECT *
FROM t_scm_inventory_check_infoX
WHERE sid = :sid
AND PDDId = :pdd_id
ORDER BY invId, locationAreaId;
字段名以目标环境真实表结构为准;历史环境可能存在命名差异。
15. 按现象排查
15.1 商品中心返回成功,但 DGJ2 搜不到
flowchart TD
A[DGJ2 搜不到商品] --> B{t_bs_goods 按 skuId 有记录吗}
B -- 否 --> C[查 Goods/Materiel 请求日志和基础资料校验错误]
B -- 是 --> D{isDelete和供货状态允许吗}
D -- 否 --> E[核对 enable 与供货状态同步]
D -- 是 --> F{MaterielCache 可查到吗}
F -- 否 --> G[检查缓存保存或旧缓存]
F -- 是 --> H{依赖 ES/VIN 搜索吗}
H -- 是 --> I[查 vin_goods_update 发送和消费]
H -- 否 --> J[继续查服务站和业务场景过滤条件]
建议检索:
rg -n "Goods_add|Goods_update|Materiel_updte|品牌未同步|计量单位未同步|商品分类未找到" application
rg -n "vin_goods_update|sendGoodsUpdateToVin" application
15.2 列表有库存,下单或出库提示不足
按以下顺序确认:
- 列表展示的是库存中心供给还是服务站实时库存。
- 请求使用的
sid、invId、指定仓是否一致。 - 列表是否把不良品货位或移动仓也汇总进去了。
- 出库明细的
locationId/locationAreaId是否正好是有库存的货位。 - 当前商品属于快准商品还是第三方商品。
- 当前服务站对应的负库存 Hash 是否命中。
- 并发出库期间,另一事务是否已通过行锁先扣减。
- 秒杀或移动商城是否另有活动锁定量、指定供给仓和起订量校验。
15.3 流水有记录,实时库存没变化
- 查该流水是否
isDelete = 1;它可能已被新版本单据冲销。 - 查同一
iid + billType是否有一组反向流水或重写记录。 - 查实时库存是否在另一个货位,不要只查商品总量。
- 查调用方事务是否在后续异常时回滚了实时库存,但日志保留了尝试过程。
- 确认查询的是与
sid对应的 128 分片。
15.4 实时库存变了,找不到对应流水
- 检查
InventorySer::save()是否在逐行改实时库存后、insert_batch前抛错。 - 检查外层调用是否没有事务或使用了不同数据库连接。
- 搜索是否存在直接调用
InventoryRealTimeModel::changeQty/increaseQty/decrementQty的旁路代码。 - 检查旧库存实现或数据修复任务是否直接操作实时表。
- 查
controllers/tasks/DataFix.php等修复入口的执行记录。
15.5 商品主表更新了,VIN/ES 没更新
updateMateriel()在主表、扩展关系和缓存之后同步发送 MQ;发送异常是否向上抛出取决于 MQ 实现。- 查路由键
vin_goods_update的生产日志和消费者。 - 销售销量 ES 更新是捕获异常后继续,不会回滚出库;查
updateEsGoodsInfo/goodsSaleNum日志。
16. 改动风险
| 改动位置 | 主要风险 | 必须回归 |
|---|---|---|
MaterielSer::formatData() | 上游字段兼容、商品批量同步全失败 | 新增、更新、禁用、品牌/单位缺失、箱规和税率 |
updateMateriel() | 主表、扩展关系、缓存、MQ 部分成功 | 新增和更新两支、物料不存在、消息失败 |
InventorySer::save() | 实时库存和流水不一致 | 新增、修改重写、多商品部分失败、事务回滚 |
delete() | 重复冲销或把库存反向扣错 | 删除一次、重复删除、指定 transType 删除 |
decrementQty() | 负库存策略反转 | 快准/第三方商品各自命中和未命中名单 |
| 分片规则 | 查错表或写错租户数据 | 多个 sid,尤其余数 0、1、127 |
| 仓库货位查询 | 正常品和不良品混算 | 单仓、多仓、无门店映射、不良品过滤 |
| 盘点状态 | 审核前误改库存、营业期间差异丢失 | 创建、扫描、提交前校验、撤回、审核通过 |
| ES 销量更新 | 业务成功但搜索排序滞后 | 150601 与其他交易类型、ES 异常降级 |
17. 回归清单
17.1 商品同步
- [ ] 新 SKU 在品牌、单位、分类齐全时可以新增。
- [ ] 同一
skuId再次同步走更新,不产生重复主商品。 - [ ]
inner/Materiel::update()遇到不存在 SKU 明确失败。 - [ ] 品牌、单位、分类、品牌系列缺失时错误可定位且不产生半条商品。
- [ ]
enable=2的禁用语义和查询侧一致。 - [ ] 商品缓存读取与数据库更新一致。
- [ ]
vin_goods_update包含old_item/new_item,消费侧能够刷新搜索。
17.2 库存
- [ ] 采购入库增加指定仓库、指定货位实时库存并写有效流水。
- [ ] 销售出库扣减同一货位并写负向流水。
- [ ] 销售退货增加退货货位库存。
- [ ] 修改单据先冲销旧流水,再按新明细重写。
- [ ] 删除或撤销后旧流水
isDelete=1,实时库存完成反向变化。 - [ ] 多商品中途失败时,外层事务能回滚实时库存和流水。
- [ ] 快准商品与第三方商品的负库存名单行为符合配置预期。
- [ ] 正常品查询不错误包含不良品货位。
- [ ]
sid % 128的分片可由单据反查。 - [ ] 销售出库后 ES 销量更新失败不影响主业务,但日志可观测。
17.3 盘点
- [ ] 创建盘点单后明细进入
sid个位数对应分表。 - [ ] 首次保存实盘明细后状态进入盘点中。
- [ ] 盘点期间发生出入库时,提交前校验能提示差异。
- [ ] 提交审核不直接改实时库存。
- [ ] 审核通过后按盘盈/盘亏调整库存并留下流水。
- [ ] 非法状态不能重复提交、更新或撤回。
18. 源码证据索引
| 主题 | 当前源码证据 |
|---|---|
| 商品同步入口 | application/controllers/inner/Goods.php、inner/Materiel.php |
| 物料校验和 Upsert | application/Services/Materiels/MaterielSer.php |
| 物料状态 | application/KzData/Enums/MaterielEnums.php |
| App WMS/盘点路由 | application/config/appapis.php |
| 库存写入总入口 | application/Services/Storage/InventorySer.php |
| 实时库存行锁与扣减 | application/models/bs/InventoryRealTimeModel.php |
| 128 分片流水 | application/models/bs/InventorySubModel.php |
| 盘点主表/明细分片 | application/models/app/InvCheckModel.php、InvCheckInfoModel.php |
| 交易类型 | application/KzData/Enums/TransTypeEnums.php |
| 负库存名单 | application/Components/KzRestrictStation.php、RedisKeys.php |
| 商品和 VIN 消息 | application/Services/Mq/MqSer.php、MqEventEnums.php |
| 物理表常量 | application/config/tables.php |
19. 待环境确认项
这些内容当前代码证据不足,后续应通过网关配置、测试环境或线上只读数据确认:
inner/Goods、inner/Materiel在各环境的完整 HTTP URL、鉴权头和商品中心重试策略。vin_goods_update的实际消费者服务、失败重试、死信和告警配置。- 两个负库存 Redis Hash 当前有哪些服务站,以及谁负责变更。
- 实时库存表是否存在覆盖四维键的唯一索引;
increaseQty()的并发正确性依赖这一点。 - 所有
InventorySer调用方是否都在同一连接事务中执行,尤其是历史和数据修复入口。 - 盘点审核通过后最终调整库存的完整调用链和状态中文映射。
- 历史库存迁移的基准时间,决定流水净变化与实时库存的正确对账起点。
这些待确认项不影响理解当前代码主链路,但在修改库存公共逻辑或处理生产数据前必须补证。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 商品中心同步 | Item Center MQ | SKU、价格、上下架/限购/敏感词事件 | tasks/ItemCenterNotify.php | 商品、物料和缓存 Service | skuId/物料编码 | BS_GOODS、扩展与服务站商品事实更新 |
| 业务库存增减 | 采购、销售、退货、调拨、盘点 Service | sid、skuId、invId、仓库货位、数量、transType、单号 | 各领域 Controller/Task | Services/Storage/InventorySer.php | InventorySer::save/changeInventoryQty | 库存流水与实时库存同步变化 |
| 商品/库存查询 | PC、App、E站和内部 API | sid、SKU、仓库、货位、可售范围 | Goods/Materiel/Storage 相关 Controller | 商品/库存查询 Service、Model、缓存 | 商品主键与库存四维键 | 返回主数据、实时量、仓位和可售状态 |
| 盘点调整 | PDA/GPDA/PC 盘点入口 | 盘点单、盘点明细、实盘数量 | 盘点 Controller | InvCheck Service + InventorySer | 盘点单号、transType=180510 | 差异生成库存调整流水并刷新实时量 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 |
|---|---|---|---|---|---|
| 商品 MQ 消费 | ItemCenterNotify 消费入口 | 事件名、SKU/物料编码、消息 ID | 目标处理方法完成并 ACK | 字段缺失、消费异常、重复失败 | SKU 关联商品主表和缓存/索引 |
| 库存调用 | 调用方 Service 与 InventorySer 异常日志 | sid、skuId、invId、业务单号、transType | 流水主单/明细与实时行均成功 | 库存不足、维度缺失、事务回滚 | 业务单号 + transType 查库存流水 |
| 并发更新 | InventoryRealTimeModel 数据库异常 | 库存四维键、SQL duplicate/deadlock | 影响行数符合预期 | 唯一键冲突、死锁、更新 0 行 | 四维键回查实时表并汇总流水 |
| 查询返回 | Controller/Service 请求日志 | URI、sid、SKU、仓库/货位 | 返回数量与主数据状态一致 | 查无商品、仓位无效、缓存旧值 | 对比 DB、缓存和返回值时间戳 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 商品资料同步 | ItemCenterNotify -> 商品/物料 Service | 单次事件事务 | BS_GOODS、BS_GOODS_EXT 当前 SKU | 商品主表、扩展、服务站商品、缓存/索引 | 名称/属性/状态/价格 old -> new,新增 SKU 则 insert | SKU、更新时间、消息消费记录 |
| 建库存流水 | InventorySer::save | 调用方业务事务内 | 业务单、仓库货位、交易类型 | SCM_INVENTORY、SCM_INVENTORY_INFO/128 分片 | 写主单;明细 qty = 本次正负变化量 | 业务单号、transType、SKU 明细合计 |
| 更新实时库存 | InventorySer::changeInventoryQty、InventoryRealTimeModel | 与流水尽量同事务 | 四维库存行与当前 qty | SCM_INVENTORY_REAL_TIME | 入库 qty: old -> old + n;出库 old -> old - n | 四维键当前量 = 基准量 + 流水净额 |
| 维护仓位/库存域 | 仓库、货位和服务站商品 Service | 独立业务事务 | BS_STORAGE、BS_STORAGE_LOCATION、BS_GOODS_STO | 对应基础表 | 启停、默认仓位、可售范围 old -> new | sid + 仓库/货位/SKU 回查 |
| 通知下游 | MqSer::sendInventoryEvent 等 | 本地提交后的异步边界 | 已提交库存/商品事实 | MQ、Redis、搜索索引 | DB 已变更 -> 缓存/下游待同步 | routing key、业务单号、消费 ACK |
子模块级独立追踪
子模块追踪:goods-sync 商品中心资料同步
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 消费 | Item Center SKU/价格/状态事件 | destination/event、message ID、SKU/物料编码、version | application/controllers/tasks/ItemCenterNotify.php -> Materiel/Goods Service | 当前商品、扩展、服务站商品和消息版本 | 单消息事务 insert/update BS_GOODS/EXT/STO;名称、属性、状态、价格 old -> new | event + message ID + SKU + update time | 旧版本不得覆盖新值;重复消息 0 副作用,失败按 SKU/version 重放 |
| 派生同步 | 商品 DB commit 后 | SKU、routing key、cache/index version | application/Services/Notify/MaterielNotifySer.php / MqSer / Cache Service | 已提交商品事实 | Redis/Mongo/ES 文档 old -> new,属于异步边界 | SKU + update time + message/task ID | 派生失败只补缓存索引,不重复更新商品主表 |
子模块追踪:goods-status 商品与物料状态可用性
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 状态变更 | 商品中心上下架、套餐、限购或价格启停事件 | message ID、SKU、status/type、effective time | application/controllers/tasks/ItemCenterNotify.php -> 对应 Goods/Materiel Service | 当前状态、商品类型、服务站范围和事件时间 | 商品事务 status/flag old -> new;相关服务站商品或限购集合同步标记 | event + SKU + old/new status + message ID | 未知状态不落库;乱序按版本/时间拒绝,不能手工只改页面字段 |
| 业务校验 | 采购/销售/活动查询或提交 | request ID、sid、SKU、scene、billNo | application/Services/Materiels/* / 各领域 Service | 主商品、站点商品、价格和场景限制 | 校验只读;非法商品零业务写入,合法请求进入领域事务 | request ID + sid + SKU + scene/billNo | 页面可见与提交不一致时逐层对比 DB/cache/业务硬校验,不绕过状态限制 |
子模块追踪:storage-location 仓库与货位维护
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 维护 | 基础资料仓库/货位接口 | request ID、sid、storage/location ID/code、name/status | application/Services/Storage/StorageSer.php 及 basedata Controller | 仓库归属、货位唯一性、是否被业务占用 | 配置事务 insert/update BS_STORAGE/BS_STORAGE_LOCATION;name/status/default old -> new | request ID + sid + storage/location ID + operator | 被使用货位停用按代码限制;失败 rollback,禁止物理删除历史维度 |
| 业务引用 | 出入库、盘点、调拨请求 | billNo、sid、SKU、storage/location | application/Services/Storage/InventorySer.php / 领域 Service | 仓库货位有效态和库存四维行 | 领域事务将仓位写入业务明细/库存流水;基础仓位表 DB 不变 | billNo + storage/location + SKU + transType | 无效仓位零库存写入;历史单保留快照,不因主数据改名重写流水 |
子模块追踪:inventory-flow 库存流水写入
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 建流水 | 采购/销售/退货/调拨/盘点调用 | request ID、business bill、sid、SKU/invId、qty、transType | application/Services/Storage/InventorySer.php::save -> Inventory Model/SubModel | 业务单、交易类型、仓库货位和分片键 | 外层事务 insert SCM_INVENTORY 主单和分片 SCM_INVENTORY_INFO;明细 qty=本次 +n/-n | business bill + transType + inventory main ID + shard | 主单/分片部分成功需按原业务键补缺失明细,不能新造第二流水主单 |
| 对账 | 业务完成后只读核对 | billNo、transType、SKU、date/shard | application/models/bs/InventoryModel.php / InventorySubModel.php | 业务数量和全部分片明细 | 只读,DB 不变;按业务键汇总流水净额 | billNo + inventory ID + shard suffix | 查错月份/分片会误判无流水;先按模型分片规则定位 |
子模块追踪:realtime-inventory 实时库存增减
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 原子增减 | InventorySer 接收领域数量变化 | business bill、sid、SKU/invId、storage/location、delta | application/Services/Storage/InventorySer.php::changeInventoryQty -> InventoryRealTimeModel | 四维实时行、当前 qty、负库存配置 | 与业务/流水同事务为目标;SCM_INVENTORY_REAL_TIME.qty: old -> old+delta,无行则 insert | business bill + four-dimension key + affected rows/deadlock | update 0、duplicate、deadlock rollback/重试;同业务键不得重复 delta |
| 一致性回查 | 请求完成或巡检 | sid、SKU/invId、仓库货位、基准时点 | application/Services/Storage/InventoryQuerySer.php | 期初实时量和期间所有库存流水 | 只读,DB 不变;验证期末=期初+期间净额 | four-dimension key + transType/date range | 差异优先找漏/重复业务流水;不孤立覆盖实时 qty |
子模块追踪:negative-inventory 负库存控制
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 判定 | 出库/扣减前 | request ID、sid、SKU/invId、required qty、scene | application/Services/Storage/InventorySer.php -> application/Components/KzRestrictStation.php / RedisKeys | 实时 qty、站点负库存 Hash、商品归属和业务场景 | 判定只读;禁止时库存/业务表零写入,允许时进入原子扣减 | request ID + sid + SKU + Redis key membership | Redis 不可用按代码降级合同处理;不得为通过请求临时改生产名单 |
| 扣减后验收 | 允许负库存的业务完成 | billNo、four-dimension key、delta、config snapshot | application/models/bs/InventoryRealTimeModel.php | 扣减前 qty 和当时配置 | 事务内 qty old -> old-n,可为负;流水同步 insert | billNo + transType + config key + before/after qty | 配置后改不回写历史;异常负数需区分合法白名单与重复扣减 |
子模块追踪:stock-check 盘点差异调整
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存实盘 | PC/PDA/GPDA 盘点 | request ID、check bill、sid、SKU、book qty、actual qty | application/Services/Pda/TakeStockSer.php / Gpda/TakeStockSer.php | 盘点状态、账面快照和已有扫描 | 盘点事务 insert/update 盘点主明细;actual qty old -> new,尚不一定改库存 | request ID + check bill + SKU + operator | 重复扫码按明细规则累计/覆盖;未审核前不手工改实时库存 |
| 审核调库 | 盘点审核通过 | check bill、SKU、diff=actual-book、transType 180510 | TakeStock Service -> application/Services/Storage/InventorySer.php | 最终盘点差异、当前状态、仓位 | 审核/库存事务 diff>0 实时 old+diff,diff<0 old-diff;写 180510 流水,status -> finish | check bill + SKU + transType + inventory ID | 重复审核 0 变化;流水/实时部分成功按 check bill 补断点 |
子模块追踪:inventory-cache 库存缓存、搜索与预警同步
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 派生同步 | 库存事务 commit 后事件/任务 | business bill、routing key、sid+SKU、update time | application/Services/Mq/MqSer.php::sendInventoryEvent -> Cache/Search/Warning consumer | 已提交实时库存、预警上下限和商品索引 | OLTP DB 不变;Redis/ES/预警 old -> new,异步边界 | billNo + message ID + cache/index key + update time | 消费失败只补事件/刷新;旧事件不得覆盖较新库存版本 |
| 页面回查 | 商品列表/库存查询/预警页 | request ID、sid、SKU、warehouse/location、scene | application/Services/Storage/InventoryQuerySer.php + Cache/Materiel Service | 实时表、缓存、索引和预警表 | 查询只读,不写业务表;按页面口径合并返回 | request ID + inventory key + cache/index version | 页面旧先比较各层更新时间;修复派生层,不为页面显示直接改库存事实 |