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_idDGJ2 商品主键t_bs_goods.id单据明细和库存表大量使用
number原厂商产品码srcCompanyProductCode不能代替 skuId 或 invId
productCode快准产品码companyProductCode与厂商产品码语义不同
buId业务单元/核算组织业务单据影响价格、成本、财务口径,不是仓库 ID
storeId门店 IDt_bs_store一个门店可映射多个仓库
locationId仓库 IDt_bs_storage实时库存主维度之一
locationAreaId货位 IDt_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.phpadd()MaterielSer::formatData()、updateMateriel()
商品分类或展示属性更新同上update()updateMaterielExtraData()
单物料更新application/controllers/inner/Materiel.phpupdate()updateMateriel(..., false)
老商品同步application/config/apis.phpshmInfoEdit -> goods.InfoEdit老 goods 控制器链路,与 inner/Goods.php 并存
客户维护application/controllers/basedata/Contact.phpquery/add/update/disable/...客户、地址、分组、员工和账户相关模型
报价规则application/controllers/basedata/QuoteManager.phpruleList/ruleSet/batchSetCustomPrice/...PriceManagerSer、QuoteManagerSer
调拨选品和仓库库存application/controllers/basedata/Allot.phpgetSalesGoodsList()、getStorageAreaStock()基础库存查询 Service

4.2 App 仓内和盘点入口

App 不是通过传统控制器逐个暴露,而是由 application/config/appapis.php 的接口码映射到 application/service/api/app/... 文件。

接口码服务文件/动作作用
appwmsSignListapp.wms.sign.signList待签收物流列表
appwmsSignInapp.wms.sign.signIn物流签收
appwmsinstoListapp.wms.insto.inStoList待入库采购单列表
appwmsinstoDetailapp.wms.insto.inStoDetail入库单明细
appwmsStorageAreaListapp.wms.storageAreaList仓库对应货位
appwmsInStoSkuInfoapp.wms.insto.inStoSkuInfo扫码取得物料信息
appwmsInStroComfirmapp.wms.insto.inStroComfirm扫描入库确认
appwmsInStoInStorageapp.wms.insto.inStoInStorage批量完成采购入库和差异处理
appwmsCodeVerifyapp.wms.codeVerify判断扫描码类型
appinvCheckAddSaveapp.invCheck.invAddSave创建盘点单
appinvCheckScanInfoapp.invCheck.invScanInfo扫码读取盘点商品/货位
appinvCheckInfoAddSaveapp.invCheck.invInfoAddSave保存实盘明细
appinvCheckStatapp.invCheck.invStat计算盘点统计和异常
appinvCheckStaUpdateapp.invCheck.invStaUpdate提交、更新或撤回盘点状态
appinvCheckConfirmapp.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 字段规则
skuCodeskuId跨系统物料编码
skuSupplyStatusCodestatus供货状态原值写入
skuStockStatusCodestockType备货模式原值写入
skuBrandCodebrandId/brandName先查品牌基础资料
unitCodeunitId/unitName先查单位基础资料
shipperCodessaleModel逐个查供应商,将有效供应商编号逗号拼接
packQty + unitNamepackSpec例如 10个/箱
enable == 2isDelete = 1禁用被转换成逻辑删除标志
flag == 1goodsType广宣品,否则普通商品
companyProductCodeproductCode快准产品码
srcCompanyProductCodenumber原厂商产品码
relCompanyProductCodedef关联产品码
paramsitem_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:

  1. 先通过缓存或数据库按 skuId 查商品。
  2. 已存在则调用 GoodsModel::updateMateielBySkuId()。
  3. 不存在且 $isNeedAdd=true 时新增。
  4. inner/Materiel::update() 传入 $isNeedAdd=false,因此不存在会明确报错,不会静默新增。
  5. 更新品牌系列关系。
  6. 写物料缓存。
  7. 发送 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_GOODSt_bs_goods商品主数据id/invId、skuId、sid、状态
BS_GOODS_EXTt_bs_goods_ext商品扩展配置商品扩展属性
BS_GOODS_STOt_bs_goods_sto商品仓库配置商品与仓库配置
BS_STOREt_bs_store门店sid、门店
BS_STORE_EXTt_bs_store_ext门店扩展storeId 到 locationId 的关系
BS_STORAGEt_bs_storage仓库locationId
BS_AREAt_bs_storage_area货位locationAreaId、area_type
SCM_INVENTORY_REAL_TIMEt_scm_inventory_real_time当前库存结果sid + inv_id + location_id + location_area_id
SCM_INVENTORYt_scm_inventory库存流水表名前缀实际使用 128 张分片表
SCM_INVENTORY_WARNINGt_scm_inventory_warning库存预警预警上下限和结果
SCM_INVENTORY_CHECKt_scm_inventory_check盘点主表盘点状态、仓库、时间
SCM_INVENTORY_CHECK_INFOt_scm_inventory_check_info{0-9}盘点明细分表按 sid 个位数分 10 张

7.2 库存流水分片

当前分片代码在 InventorySubModel::setSid():

table = t_scm_inventory_0_{sid % 128}

例如:

sid计算流水表
10011001 % 128 = 105t_scm_inventory_0_105
20482048 % 128 = 0t_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: 返回

这里有四个容易忽略的事实:

  1. 实时库存逐行修改,库存流水在循环结束后才批量写入。
  2. 传入非空 iid 时,先调用 delete() 冲销旧流水,再写新流水。
  3. 所谓删除流水实际是把 isDelete 更新为 1,不是物理删除。
  4. InventorySer::save() 自身没有开启事务;原子性依赖外层业务 Service 是否在同一数据库连接上包裹事务。

因此调用方如果未正确事务化,可能出现“前几行实时库存已改,后续异常,流水没有完整写入”的中间状态。


9. 实时库存增减算法和负库存开关

9.1 增库存

InventoryRealTimeModel::increaseQty():

  1. 按 sid + invId + locationId + locationAreaId 执行 SELECT ... FOR UPDATE。
  2. 已有记录则 qty = qty + ?。
  3. 没有记录则插入新行,并带入 skuId 和 areaType。
  4. 受影响行数小于等于 0 时,InventorySer 抛出“库存增加失败”。

9.2 扣库存

扣减前同样执行 SELECT ... FOR UPDATE。模型的 $minus 参数含义容易误读:

$minusSQL 条件结果
falseUPDATE ... SET qty=qty-? WHERE qty>=?严格校验,不允许扣成负数
trueUPDATE ... 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_PURCHASE150501采购入库,通常 qty > 0
TRANSTYPE_PU_SALE_RETURN150502销售退货入库,通常 qty > 0
TRANSTYPE_SA_INVOICE_SALE150601销售出库,通常 qty < 0
TRANSTYPE_SA_INVOICE_RETURN150602销售退货类流水,方向需看具体组装调用
TRANSTYPE_OI_IN150706其他入库
TRANSTYPE_OI_OUT150806其他出库
TRANSTYPE_OI_PROFIT150701盘盈
TRANSTYPE_OI_LOSS150801盘亏
TRANSTYPE_TRANSFER103091调拨
TRANSTYPE_TRANSFER_S103092服务站间调拨
TRANSTYPE_INVEN_INI180001期初库存
TRANSTYPE_INVEN_OO180510移动盘库
TRANSTYPE_OI_HWTZ170500货位调整主类型
TRANSTYPE_OI_HWTZ_OUT170501货位调整出
TRANSTYPE_OI_HWTZ_IN170502货位调整入;与普通销售单编码存在历史复用

注意: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 库存事实核对顺序

  1. 确认 sid、skuId、invId 对应是否正确。
  2. 确认业务单据是否已经执行到实际入库/出库节点,而不只是创建或审核。
  3. 计算 sid % 128,查询正确的库存流水分表。
  4. 排除 isDelete = 1 的已冲销流水。
  5. 按仓库和货位查询实时库存,不要先做全站汇总掩盖局部错误。
  6. 查看负库存开关,确认扣减是否允许穿透零库存。
  7. 再对比库存中心、活动锁定量、报表或 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 列表有库存,下单或出库提示不足

按以下顺序确认:

  1. 列表展示的是库存中心供给还是服务站实时库存。
  2. 请求使用的 sid、invId、指定仓是否一致。
  3. 列表是否把不良品货位或移动仓也汇总进去了。
  4. 出库明细的 locationId/locationAreaId 是否正好是有库存的货位。
  5. 当前商品属于快准商品还是第三方商品。
  6. 当前服务站对应的负库存 Hash 是否命中。
  7. 并发出库期间,另一事务是否已通过行锁先扣减。
  8. 秒杀或移动商城是否另有活动锁定量、指定供给仓和起订量校验。

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
物料校验和 Upsertapplication/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. 待环境确认项

这些内容当前代码证据不足,后续应通过网关配置、测试环境或线上只读数据确认:

  1. inner/Goods、inner/Materiel 在各环境的完整 HTTP URL、鉴权头和商品中心重试策略。
  2. vin_goods_update 的实际消费者服务、失败重试、死信和告警配置。
  3. 两个负库存 Redis Hash 当前有哪些服务站,以及谁负责变更。
  4. 实时库存表是否存在覆盖四维键的唯一索引;increaseQty() 的并发正确性依赖这一点。
  5. 所有 InventorySer 调用方是否都在同一连接事务中执行,尤其是历史和数据修复入口。
  6. 盘点审核通过后最终调整库存的完整调用链和状态中文映射。
  7. 历史库存迁移的基准时间,决定流水净变化与实时库存的正确对账起点。

这些待确认项不影响理解当前代码主链路,但在修改库存公共逻辑或处理生产数据前必须补证。

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

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
商品中心同步Item Center MQSKU、价格、上下架/限购/敏感词事件tasks/ItemCenterNotify.php商品、物料和缓存 ServiceskuId/物料编码BS_GOODS、扩展与服务站商品事实更新
业务库存增减采购、销售、退货、调拨、盘点 Servicesid、skuId、invId、仓库货位、数量、transType、单号各领域 Controller/TaskServices/Storage/InventorySer.phpInventorySer::save/changeInventoryQty库存流水与实时库存同步变化
商品/库存查询PC、App、E站和内部 APIsid、SKU、仓库、货位、可售范围Goods/Materiel/Storage 相关 Controller商品/库存查询 Service、Model、缓存商品主键与库存四维键返回主数据、实时量、仓位和可售状态
盘点调整PDA/GPDA/PC 盘点入口盘点单、盘点明细、实盘数量盘点 ControllerInvCheck 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 则 insertSKU、更新时间、消息消费记录
建库存流水InventorySer::save调用方业务事务内业务单、仓库货位、交易类型SCM_INVENTORY、SCM_INVENTORY_INFO/128 分片写主单;明细 qty = 本次正负变化量业务单号、transType、SKU 明细合计
更新实时库存InventorySer::changeInventoryQty、InventoryRealTimeModel与流水尽量同事务四维库存行与当前 qtySCM_INVENTORY_REAL_TIME入库 qty: old -> old + n;出库 old -> old - n四维键当前量 = 基准量 + 流水净额
维护仓位/库存域仓库、货位和服务站商品 Service独立业务事务BS_STORAGE、BS_STORAGE_LOCATION、BS_GOODS_STO对应基础表启停、默认仓位、可售范围 old -> newsid + 仓库/货位/SKU 回查
通知下游MqSer::sendInventoryEvent 等本地提交后的异步边界已提交库存/商品事实MQ、Redis、搜索索引DB 已变更 -> 缓存/下游待同步routing key、业务单号、消费 ACK

子模块级独立追踪

子模块追踪:goods-sync 商品中心资料同步

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
消费Item Center SKU/价格/状态事件destination/event、message ID、SKU/物料编码、versionapplication/controllers/tasks/ItemCenterNotify.php -> Materiel/Goods Service当前商品、扩展、服务站商品和消息版本单消息事务 insert/update BS_GOODS/EXT/STO;名称、属性、状态、价格 old -> newevent + message ID + SKU + update time旧版本不得覆盖新值;重复消息 0 副作用,失败按 SKU/version 重放
派生同步商品 DB commit 后SKU、routing key、cache/index versionapplication/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 timeapplication/controllers/tasks/ItemCenterNotify.php -> 对应 Goods/Materiel Service当前状态、商品类型、服务站范围和事件时间商品事务 status/flag old -> new;相关服务站商品或限购集合同步标记event + SKU + old/new status + message ID未知状态不落库;乱序按版本/时间拒绝,不能手工只改页面字段
业务校验采购/销售/活动查询或提交request ID、sid、SKU、scene、billNoapplication/Services/Materiels/* / 各领域 Service主商品、站点商品、价格和场景限制校验只读;非法商品零业务写入,合法请求进入领域事务request ID + sid + SKU + scene/billNo页面可见与提交不一致时逐层对比 DB/cache/业务硬校验,不绕过状态限制

子模块追踪:storage-location 仓库与货位维护

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
维护基础资料仓库/货位接口request ID、sid、storage/location ID/code、name/statusapplication/Services/Storage/StorageSer.php 及 basedata Controller仓库归属、货位唯一性、是否被业务占用配置事务 insert/update BS_STORAGE/BS_STORAGE_LOCATION;name/status/default old -> newrequest ID + sid + storage/location ID + operator被使用货位停用按代码限制;失败 rollback,禁止物理删除历史维度
业务引用出入库、盘点、调拨请求billNo、sid、SKU、storage/locationapplication/Services/Storage/InventorySer.php / 领域 Service仓库货位有效态和库存四维行领域事务将仓位写入业务明细/库存流水;基础仓位表 DB 不变billNo + storage/location + SKU + transType无效仓位零库存写入;历史单保留快照,不因主数据改名重写流水

子模块追踪:inventory-flow 库存流水写入

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
建流水采购/销售/退货/调拨/盘点调用request ID、business bill、sid、SKU/invId、qty、transTypeapplication/Services/Storage/InventorySer.php::save -> Inventory Model/SubModel业务单、交易类型、仓库货位和分片键外层事务 insert SCM_INVENTORY 主单和分片 SCM_INVENTORY_INFO;明细 qty=本次 +n/-nbusiness bill + transType + inventory main ID + shard主单/分片部分成功需按原业务键补缺失明细,不能新造第二流水主单
对账业务完成后只读核对billNo、transType、SKU、date/shardapplication/models/bs/InventoryModel.php / InventorySubModel.php业务数量和全部分片明细只读,DB 不变;按业务键汇总流水净额billNo + inventory ID + shard suffix查错月份/分片会误判无流水;先按模型分片规则定位

子模块追踪:realtime-inventory 实时库存增减

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
原子增减InventorySer 接收领域数量变化business bill、sid、SKU/invId、storage/location、deltaapplication/Services/Storage/InventorySer.php::changeInventoryQty -> InventoryRealTimeModel四维实时行、当前 qty、负库存配置与业务/流水同事务为目标;SCM_INVENTORY_REAL_TIME.qty: old -> old+delta,无行则 insertbusiness bill + four-dimension key + affected rows/deadlockupdate 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、sceneapplication/Services/Storage/InventorySer.php -> application/Components/KzRestrictStation.php / RedisKeys实时 qty、站点负库存 Hash、商品归属和业务场景判定只读;禁止时库存/业务表零写入,允许时进入原子扣减request ID + sid + SKU + Redis key membershipRedis 不可用按代码降级合同处理;不得为通过请求临时改生产名单
扣减后验收允许负库存的业务完成billNo、four-dimension key、delta、config snapshotapplication/models/bs/InventoryRealTimeModel.php扣减前 qty 和当时配置事务内 qty old -> old-n,可为负;流水同步 insertbillNo + transType + config key + before/after qty配置后改不回写历史;异常负数需区分合法白名单与重复扣减

子模块追踪:stock-check 盘点差异调整

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
保存实盘PC/PDA/GPDA 盘点request ID、check bill、sid、SKU、book qty、actual qtyapplication/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 180510TakeStock Service -> application/Services/Storage/InventorySer.php最终盘点差异、当前状态、仓位审核/库存事务 diff>0 实时 old+diff,diff<0 old-diff;写 180510 流水,status -> finishcheck bill + SKU + transType + inventory ID重复审核 0 变化;流水/实时部分成功按 check bill 补断点

子模块追踪:inventory-cache 库存缓存、搜索与预警同步

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
派生同步库存事务 commit 后事件/任务business bill、routing key、sid+SKU、update timeapplication/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、sceneapplication/Services/Storage/InventoryQuerySer.php + Cache/Materiel Service实时表、缓存、索引和预警表查询只读,不写业务表;按页面口径合并返回request ID + inventory key + cache/index version页面旧先比较各层更新时间;修复派生层,不为页面显示直接改库存事实