本文说明 DGJ2.0 中服务站间调拨申请、发货站调出、申请站调入、撤销调出、调拨收付款,以及旧普通调拨 TF 与新申请式 STF 的关系。

服务站间调拨同时跨越两个服务站、两组库存空间、一个在途阶段和后续财务结算。排查时不能只看一张主表,也不能把“已出库”等同于“对方已入库”,更不能把库存完成等同于款项已结算。

阅读后应能回答:

  • 谁是申请方、谁是发货方,sid 和 toSid 在不同表中分别代表谁。
  • 一次包含多个发货站的申请为什么会拆成多张申请单。
  • 申请数量、出库数量、入库数量和结算金额如何传递。
  • 出库、入库分别写哪些主表、明细、库存流水和实时库存。
  • 调出库存为什么要扣除采购/销售锁定量。
  • 出库后如何撤销,为什么已收付款后禁止撤销。
  • 调拨收付款是否由入库自动生成。
  • t_scm_tf_invoice、t_scm_stf_invoice、t_scm_stf_apply 三套表应如何区分。
  • 出现“发货站已扣、收货站未加”“金额对不上”“撤销后库存没恢复”时如何定位。
结论先行:申请式 STF 的库存主链是 Apply -> Invoice::out -> Invoice::in。调出和调入都使用 transType=103092,方向由明细数量正负和 entryId 区分。调入完成不会自动创建收付款,财务结算通过独立的调拨收款 154001 / 调拨付款 154002 入口完成。

1. 业务边界

1.1 本文覆盖

能力操作方是否改变库存是否改变资金
创建调拨申请调入/申请服务站否否
催办调入/申请服务站否否
取消申请调入/申请服务站否否
驳回申请调出/发货服务站否否
调出展示和价格导入调出/发货服务站否否
调出确认调出/发货服务站减少调出方库存不自动收款
撤销调出调出/发货服务站恢复调出方库存已结算时禁止
调入确认调入/申请服务站增加调入方库存不自动付款
调拨收款/付款财务人员否写收付款及账户相关事实
普通站内调拨 TF当前服务站同站仓库间变化通常不跨站结算

1.2 不属于本文

  • 普通采购入库、销售出库,见采购和销售专题。
  • PDA 货位调整,见《26_PDA_GPDA扫码盘点与仓内作业》。
  • 微仓补货拆单的全部业务,本文只说明它与 TF_SOURCE_TYPE_MOVE 和旧调拨入口的交点。
  • 财务总账、账户分片和收付款通用规则,见《18_财务和支付排查手册》。

2. 三套“调拨”概念

2.1 申请式 STF

当前专题主流程:

t_scm_stf_apply / t_scm_stf_apply_info
    -> t_scm_stf_invoice / t_scm_stf_invoice_info
    -> 库存流水 / 实时库存

先由调入方发起申请,再由调出方确认出库,最后调入方入库。

2.2 旧服务站间 STF

旧控制器 scm/InvTf.php 和 InvTfService.php 仍保留 addSTF、saveTFIn、addTFpayment 等方法。历史 t_scm_stf_invoice 数据可能没有 apply_bill_no,迁移任务曾尝试补造申请记录,但任务入口当前以 exit("初始化已完成") 直接停止。

2.3 普通 TF

普通 TF 使用:

  • t_scm_tf_invoice
  • t_scm_tf_invoice_info
  • transType=103091

它主要描述同一服务站内的自定义调拨或微仓调拨,状态由 TfInvoiceEnums 管理,不应套用申请式 STF 的状态。

flowchart TB
  A["业务说‘调拨’"] --> B{"是否跨服务站?"}
  B -->|否| C["普通 TF: t_scm_tf_invoice, 103091"]
  B -->|是| D{"是否先有调拨申请?"}
  D -->|是| E["申请式 STF: apply + stf_invoice, 103092"]
  D -->|否/历史数据| F["旧 STF: InvTfService 历史入口"]

3. 角色、方向与标识

3.1 参与者

角色业务责任代码中的常用字段
申请服务站希望调入商品,创建申请,最终入库apply_sid
发货服务站接收申请,决定数量/价格/货位并调出deliver_sid
调出操作人选择库存、执行出库uid/userName、会话 sid
调入操作人选择目标仓库货位、执行入库会话 sid
财务人员对已出/已入调拨进行收款或付款154001/154002
消息消费者向发货站展示新申请提醒站内信/SAAS 消息链路

3.2 关键标识

标识含义生成位置
申请主键 idt_scm_stf_apply.id创建申请
bill_no申请单号ApplySer::create()
调出单 billNo调出库存单号InvoiceSer::out()
调入单 toBillNo调入库存单号InvoiceSer::in()
apply_bill_no调拨主单回指申请单号调出创建
调拨主键 iidt_scm_stf_invoice.id调出创建,调入复用
inv_id / invId服务站商品库存档案申请表下划线,库存单驼峰
sku_id / skuId商品中心 SKU跨站商品识别
entryId明细方向1 调入,2 调出

3.3 sid / toSid 方向

在 t_scm_stf_invoice 主表中:

字段调出时含义
sid发货/调出服务站
toSid申请/调入服务站

调出明细同样是 sid=调出方、toSid=调入方;调入明细当前代码写 sid=调入方、toSid=调入方。因此不能只靠调入明细 toSid 追溯原始发货站,必须关联调拨主表。

4. 系统和代码地图

flowchart TB
  IN["申请站 PC"] --> AC["stf/Apply Controller"]
  AC --> AS["Stf/ApplySer"]
  AS --> AP[("stf_apply")]
  AS --> MQ["RabbitMQ 站内消息"]
  MQ --> OUT["发货站待办"]
  OUT --> IC["stf/Invoice Controller"]
  IC --> IS["Stf/InvoiceSer"]
  IS --> IV[("stf_invoice")]
  IS --> INV["Storage/InventorySer"]
  INV --> LEDGER[("库存流水128分片")]
  INV --> RT[("实时库存")]
  FIN["财务人员"] --> LEG["scm/InvTf + InvTfService"]
  LEG --> PAY[("Payment 32分片")]
  LEG --> ACC[("Account 16分片")]

4.1 新申请式入口

控制器方法用途
controllers/stf/Apply.phplist、settleList、info查询和结算候选
同上format、create拆单预览和创建
同上copy、hasten、cancel复制、催办、取消/驳回
同上export、printInfo导出和打印
同上getGoodsInfo获取调拨商品信息
controllers/stf/Invoice.phpoutShow、out调出展示和确认
同上getOutShowTemplate、importTfPrice调拨价模板和导入
同上inShow、in调入展示和确认
同上rollback撤销调出

4.2 核心服务

Service责任
Services/Stf/ApplySer.php申请拆单、创建、列表、催办、取消、打印、结算候选
Services/Stf/InvoiceSer.php调出/调入格式化、库存动作、撤销
Services/BaseData/AllotSer.php调出默认仓库/货位和库存展示
Services/Storage/InventorySer.php写实时库存和 128 分片流水
Services/Mq/MqSer.php新申请提醒、库存事件
service/scm/InvTfService.php旧 TF/STF 和调拨收付款

4.3 迁移和服务化痕迹

文件含义当前判断
controllers/tasks/ApplyTransfer.php给历史 STF 补申请表方法开头已退出,不应当作在线任务
Components/Traits/SwitchRouterTransfer.phpRedis 开关路由新 DGJ 服务默认无 key 时走旧服务
Providers/TransferUriProvider.phpJSON HTTP 请求封装超时 5 秒

5. 数据模型

5.1 核心表

常量物理表用途关键字段
SCM_STF_APPLYt_scm_stf_apply跨站调拨申请主表单号、两站、状态、金额和时间
SCM_STF_APPLY_INFOt_scm_stf_apply_info申请商品明细申请/出/入数量、价格
SCM_STF_INVOICEt_scm_stf_invoice跨站调拨库存主单出入单号、两站、状态
SCM_STF_INVOICE_INFOt_scm_stf_invoice_info调出和调入明细方向、数量、价格、仓库货位
SCM_TF_INVOICEt_scm_tf_invoice普通同站调拨主单来源类型、状态、仓库
SCM_TF_INVOICE_INFOt_scm_tf_invoice_info普通调拨明细出入仓库货位和数量
SCM_INVENTORY 分表t_scm_inventory_0_{sid%128}调拨库存流水TRANSFER、交易类型、数量
SCM_INVENTORY_REAL_TIMEt_scm_inventory_real_time当前库存sid+inv+仓库+货位
SCM_PAYMENT 分表t_scm_payment_{sid%32}调拨收付款主单154001/154002
SCM_PAYMENT_INFO 分表t_scm_payment_info_{sid%32}调拨结算明细stlNo、金额、差异、账户
SCM_ACCOUNT 分表t_scm_account_{sid%16}账户流水资金方向和账户

5.2 实体关系

erDiagram
  STF_APPLY ||--|{ STF_APPLY_INFO : "apply_id"
  STF_APPLY ||--o| STF_INVOICE : "apply_bill_no"
  STF_INVOICE ||--|{ STF_INVOICE_INFO : "iid"
  STF_INVOICE_INFO }o--|| INVENTORY_REAL_TIME : "库存维度"
  STF_INVOICE_INFO ||--o| INVENTORY_LEDGER : "库存动作"
  STF_INVOICE_INFO ||--o{ PAYMENT_INFO : "billNo_stlNo"
  PAYMENT ||--|{ PAYMENT_INFO : "iid"

5.3 申请明细数量

字段形成时间业务含义
apply_qty创建申请申请方希望数量
out_qty发货站调出实际发出数量,可小于申请数量
in_qty申请站调入实际收入数量,可小于等于出库数量
price调出发货方确认的调拨单价
0 <= in_qty <= out_qty <= apply_qty
并且整单 total_out_qty > 0

6. 状态机

6.1 申请状态

值常量中文允许动作
0WAIT_OUT待出库催办、取消/驳回、调出
1WAIT_IN待入库调入、满足条件时撤销调出
2IN_FINISH已入库结算/查询,库存终态
3CANCEL_BY_DELIVER已驳回发货方终止
4CANCEL_BY_APPLY已取消申请方终止
stateDiagram-v2
  [*] --> 待出库
  待出库 --> 已取消: 申请方 cancel
  待出库 --> 已驳回: 发货方 cancel
  待出库 --> 待入库: 发货方 out
  待入库 --> 待出库: 发货方 rollback
  待入库 --> 已入库: 申请方 in
  已取消 --> [*]
  已驳回 --> [*]
  已入库 --> [*]

6.2 调拨库存主单状态

值常量含义
1StfInvoiceEnums::OUT_FINISH调出完成、等待调入
2StfInvoiceEnums::IN_FINISH调入完成

调入不是新建第二张主单,而是在原调拨主单上写 toBillNo、状态改 2,并追加 entryId=1 的调入明细。

6.3 普通 TF 状态

值含义
1草稿
2调出未调入
3调出已调入
4已调出部分调入
5调入关闭

普通 TF 状态 1/2/3/4/5 与申请式 STF 的 0/1/2/3/4 不是同一套枚举。

7. 创建申请:按发货站拆单

7.1 代表性请求

{
  "remark": "门店补货",
  "data": "[{\\"deliver_sid\\":20002,\\"inv_id\\":31001,\\"apply_qty\\":3,\\"remark\\":\\"急用\\"},{\\"deliver_sid\\":20003,\\"inv_id\\":31002,\\"apply_qty\\":2,\\"remark\\":\\"\\"}]"
}

服务端从会话注入 JXCSID、JXCUID 和 JXCUNAME,业务层不应信任客户端替代会话上传申请站。

7.2 拆单流程

flowchart TD
  A["接收商品数组"] --> B["逐行校验商品/数量/发货站"]
  B --> C{"发货站等于当前站?"}
  C -->|是| D["拒绝"]
  C -->|否| E["按 deliver_sid 分组"]
  E --> F["批量查服务站和商品物料"]
  F --> G{"同发货站商品重复?"}
  G -->|是| H["拒绝整批"]
  G -->|否| I["补 SKU/单位/包装/站名"]
  I --> J["返回拆单预览"]

一份数据包含两个 deliver_sid 时,format() 返回两组,create() 在同一事务中创建两张申请主单及各自明细。

7.3 创建时序

sequenceDiagram
  participant U as 申请站
  participant C as Apply Controller
  participant S as ApplySer
  participant A as stf_apply
  participant D as stf_apply_info
  participant Q as RabbitMQ
  U->>C: create(remark,data)
  C->>S: format并按发货站拆组
  loop 每个发货站
    S->>A: 插入申请主表
    S->>D: 批量插入商品明细
  end
  S-->>C: messageData
  C->>Q: 每张申请发送提醒

申请事务提交后控制器才逐条发消息。多张拆单消息可部分成功,MQ 失败不会回滚申请。

7.4 创建校验和代码风险

项目规则/风险
inv_id必填数字,物料必须存在
apply_qty必填且大于 0
deliver_sid必填、不能等于当前站
重复商品同一发货站内拒绝重复
主备注create() 给 remark 赋值,但主表字段白名单未包含它,可能不落库

8. 通知、催办、取消和驳回

8.1 新申请消息

{
  "billNo": "申请单号",
  "sid": 20002,
  "type": 2
}

sid 是发货服务站,type=2 是 MessageTypeEnums::APPLY。消息通过 DEST_DGJ + TYPE_CREATE_SAAS_ORDER 发送,页面指向 /stf/apply?action=list。

8.2 催办

只有申请方且状态 0 才能催办;催办只重发提醒,不改状态,代码中未见频率限制。

flowchart LR
  A["申请方催办"] --> B{"当前站=apply_sid?"}
  B -->|否| C["非法请求"]
  B -->|是| D{"状态=0?"}
  D -->|否| E["拒绝"]
  D -->|是| F["重发 APPLY 消息"]

8.3 取消和驳回

操作者前置状态目标状态含义
申请方04已取消
发货方03已驳回
其他站任意无非法操作

此时尚未出库,只写取消时间和操作人,不恢复库存、不处理资金。

9. 调出展示与调拨价

outShow() 要求当前站是 deliver_sid 且申请状态为 0,并通过 AllotSer::getStorageAndArea() 附加可选仓库、货位和库存。

价格模板包含名称、物料编码、最近采购价、调拨价。导入按 SKU 是否属于申请判断,成功行只返回前端,最终价格仍以 out() 请求为准。

风险说明
文件类型按原始扩展名保存,需依赖上传层限制
价格类型number_format 返回字符串,应回归千分位和非法字符
重复 SKU导入控制器未明确去重
部分成功错误行生成错误文件,成功行仍可继续

10. 发货站调出

10.1 代表性请求

{
  "id": 9001,
  "data": "[{\\"id\\":9101,\\"out_qty\\":3,\\"out_price\\":25.50,\\"locationId\\":101,\\"locationAreaId\\":1001}]"
}

请求中的明细 id 是申请明细主键,不是 invId。

10.2 控制器并发锁

Invoice::out() 调用:

redis_lock([Invoice, out, sid, apply_id], 3)

静态代码只能证明锁键包含站点和申请主键;参数 3 的 TTL/锁模式、是否自动释放和续期,需要结合全局 redis_lock() 实现及真实并发测试确认。

10.3 调出校验

校验规则
操作站必须是 deliver_sid
申请状态必须是 0
明细完整性每条申请明细都必须出现在请求中
out_qty整数,0 到 apply_qty
整单数量合计必须大于 0
out_price数字且大于 0
仓库/货位必须属于发货站
微仓is_move=1 禁止调拨
可用库存实时库存扣除业务锁定量后足够
可调数量 = 货位实时库存 - unsaleLockNum
可调数量 >= out_qty

unsaleLockNum() 来自采购订单明细模型,用于排除已经被其他业务锁定、不可再调出的库存。

10.4 调出明细符号

字段值
qty-out_qty
price-out_price
amount-out_price * out_qty
entryId2
billTypeTRANSFER
transType103092
sid发货站
toSid申请站

10.5 调出事务

sequenceDiagram
  participant O as 发货站
  participant C as Invoice Controller
  participant S as InvoiceSer
  participant A as 申请表
  participant T as STF调拨表
  participant I as InventorySer
  participant R as 实时库存
  participant Q as MQ
  O->>C: out(apply_id,lines)
  C->>C: Redis并发锁
  C->>S: out
  S->>A: 校验状态0和发货站
  S->>S: 校验仓库货位/库存/锁定量
  S->>T: 插主表status=1和负数明细
  S->>A: 状态0到1,写数量价格时间
  S->>I: save负数明细
  I->>R: 扣发货站实时库存
  S->>S: 提交MySQL事务
  S->>Q: sendInventoryEvent

10.6 调出后应有事实

层级结果
申请主表bill_status=1、out_price、out_time
申请明细out_qty、price
调拨主表billStatus=1、调出 billNo、apply_bill_no
调拨明细entryId=2、负数量/价格/金额
库存流水发货站 103092 负数量
实时库存发货站指定仓库货位减少
MQ发货站库存变化事件

11. 在途阶段

WAIT_IN=1 表示发货站已出库、申请站尚未入库:

flowchart LR
  A["发货站库存 -N"] --> B["在途 N"]
  B --> C["申请站库存尚未 +N"]
  C --> D{"申请站执行 in?"}
  D -->|是| E["申请站库存 +实际入库量"]
  D -->|否| F["保持待入库"]
事实在途时状态
发货站实时库存已减少
申请站实时库存未增加
申请主表1 待入库
STF 主表1 出库完成
调出单号已生成
调入单号 toBillNo未生成

若没有独立在途库存表,在途量应由有效调出明细减调入明细推导,不能在申请站实时库存中查到。

12. 申请站调入

12.1 代表性请求

{
  "id": 9001,
  "data": "[{\\"id\\":9101,\\"in_qty\\":3,\\"locationId\\":201,\\"locationAreaId\\":2001}]"
}

12.2 调入规则

校验规则
操作站必须是 apply_sid
申请状态必须是 1
in_qty整数,0 到 out_qty
仓库/货位必须属于申请站
微仓禁止作为调入仓库
重复调入调拨主表行锁内检查 billStatus != 2

12.3 数据库行锁

select id, billStatus
from t_scm_stf_invoice
where apply_bill_no = :apply_bill_no
  and isDelete = 0
for update;

同一申请的并发调入会串行读取主表,第二个请求看到状态 2 后返回“已调拨入库,请勿重复入库”。

12.4 调入明细

字段值
qtyin_qty 正数
price原调出价格绝对值
amount正价格乘入库数量
entryId1
billTypeTRANSFER
transType103092
sid申请/调入站
toSid当前代码也写调入站

12.5 调入事务

sequenceDiagram
  participant I as 申请站
  participant C as Invoice Controller
  participant S as InvoiceSer
  participant A as 申请表
  participant T as STF调拨表
  participant W as InventoryService
  participant V as InventorySer
  participant Q as MQ
  I->>C: in(apply_id,lines)
  C->>C: Redis锁
  C->>S: in
  S->>A: 校验状态1和申请站
  S->>T: SELECT FOR UPDATE
  S->>T: 状态1到2,生成toBillNo
  S->>T: 原出库明细isEnter=1,追加调入明细
  S->>A: 状态1到2,写入库数量金额时间
  S->>W: updateCost
  S->>V: save正数明细
  S->>S: 提交MySQL事务
  S->>Q: sendInventoryEvent

12.6 入库金额

真正落主表的口径:

单行入库金额 = abs(调出单价) * in_qty
整单 in_price = 所有单行入库金额之和

inFormat() 内另有一个未返回使用的局部 totalInAmount,且使用 out_qty 计算。维护时不能把该废弃局部值当作最终入库金额。

13. 少入、零入与数量差异

验证允许 in_qty=0,也允许 in_qty < out_qty;调入完成后主单仍整体进入状态 2。

场景出库数量入库数量差异
全量入库10100
少入1082
某行零入10010
flowchart TD
  A["out_qty=10"] --> B{"in_qty"}
  B -->|10| C["库存跨站守恒"]
  B -->|8| D["2件形成跨站差异"]
  B -->|0| E["整行未入但主单可完成"]
  D --> F["确认损耗/拒收/财务差异规则"]
  E --> F

少入差异不会自动回补发货站库存。运输损耗、拒收、补发或财务差异由业务规则承担,必须在联调和结算口径中明确。

14. 撤销调出

14.1 允许条件

  • 当前站是 deliver_sid。
  • 申请状态是 WAIT_IN=1。
  • 尚未执行调入。
  • PaymentInfoModel::getInfoByApplyNo() 查不到该申请收付款。

14.2 撤销事务

sequenceDiagram
  participant O as 发货站
  participant S as InvoiceSer
  participant A as 申请表
  participant T as STF调拨表
  participant V as InventorySer
  participant Q as MQ
  O->>S: rollback(apply_id)
  S->>A: 校验发货站和状态1
  S->>S: 检查未收付款
  S->>A: 状态1到0,out_price/out_qty归零
  S->>T: 主明细isDelete=1并记删除人
  S->>V: delete(iid,TRANSFER,103092)
  V->>V: 恢复实时库存并作废流水
  S->>S: 提交事务
  S->>Q: 发送库存撤销事件

撤销后可重新调出并产生新 STF 主单;旧主单保留为 isDelete=1 的历史。排查“重复调拨单”必须先区分有效和已撤销记录。

14.3 已结算为何禁止撤销

库存撤销会恢复发货站库存,但不会自动反冲收付款和账户事实。代码因此阻断已有结算的申请,避免“库存恢复、资金仍已结算”。

15. 调拨收款和付款

15.1 与库存链分离

InvoiceSer::out() 和 in() 都不创建 Payment/Account。财务通过旧 InvTf 控制器的:

  • addTFpayment
  • paymentList
  • paymentInfo
  • updatePayment
  • deletePayment

完成独立结算。

15.2 交易类型

值常量中文方向
154001TRANSTYPE_TRANSFER_IN_PAY调拨收款发货方收款
154002TRANSTYPE_TRANSFER_OUT_PAY调拨付款调入方付款

15.3 待结算列表

type页面含义代码展示
payment调拨付款候选已入库、调入
receipt调拨收款候选已出库、调出

旧模型通过 stlNo 把 PaymentInfo 与调拨 billNo 关联,待结算金额由调拨金额扣除已结算金额和 diffAmount 得出。

flowchart LR
  A["调拨明细 amount"] --> D["待结算金额"]
  B["PaymentInfo 已结算 amount"] --> D
  C["diffAmount"] --> D
  D --> E["创建154001或154002"]

15.4 资金排查层次

  1. 调拨出/入明细金额。
  2. 当前站应收还是应付。
  3. Payment 主表 32 分片。
  4. PaymentInfo 32 分片的 stlNo、amount、diffAmount。
  5. Account 和账户流水 16 分片。
  6. 修改/删除收付款后的反向事实。

16. 事务与异步边界

阶段本地事务内事务外
创建申请多张申请主/明细每张申请提醒 MQ
调出申请、STF、库存流水、实时库存库存事件 MQ
调入STF 行锁、申请、成本、库存库存事件 MQ
撤销申请回退、STF 软删、库存恢复库存撤销 MQ
收付款Payment/PaymentInfo/账户相关写入通用财务通知
flowchart TD
  A["MySQL事务提交"] --> B["发送MQ"]
  B -->|成功| C["下游同步"]
  B -->|失败| D["本地主事实成功,下游滞后"]
  D --> E["按申请号/调拨单号补发"]
  E --> F["禁止重做out/in来补消息"]

多发货站申请的提醒是逐条发送,可能部分成功。补偿前应逐个 billNo 查消费结果。

17. 幂等、并发与唯一性

17.1 现有防线

动作防线
调出Redis 锁 + 申请状态 0
调入Redis 锁 + MySQL 行锁 + STF 状态检查
撤销申请状态 1 + 结算检查
取消申请状态 0

17.2 待环境验证

  • apply_bill_no 是否有数据库唯一索引。
  • 调出 Redis 锁过期后能否并发插入两张有效 STF。
  • MQ 是否有 publisher confirm、重试、死信或 outbox。
  • 收付款 stlNo 是否有防重复结算唯一约束。
  • 库存实时账和流水在回滚中的顺序。

17.3 客户端超时

重试前按顺序查:

  1. 申请状态。
  2. apply_bill_no 对应的有效/删除 STF 数量。
  3. 出入明细和单号。
  4. 两站库存流水。
  5. 两站实时库存。
  6. 再判断是否允许重试。

18. 易错代码和维护风险

位置风险影响
ApplyValidatein_end_time 与自身做 egt 比较入库结束时间范围校验可能无效
ApplySer::create主表字段白名单不含 remark申请主备注可能丢失
Apply::getGoodsInfocatch 中使用未定义 $exception原错误可能被覆盖
Invoice::incompact 时 u_id/u_name 未定义当前只用 sid,未来审计扩展有坑
InvoiceSer::inFormat局部金额按 out_qty 算且未返回后续维护可能误用
调入明细 toSid写为调入站自身不能用它追发货站
少入后整单完成无自动库存差异补偿业务/财务口径需明确
新旧入口并存状态和表相似修一套漏一套

19. 典型故障决策树

19.1 发货站已扣、申请站没加

flowchart TD
  A["发货站已扣,申请站没加"] --> B["查申请状态"]
  B -->|1 待入库| C["正常在途,确认是否执行in"]
  B -->|2 已入库| D["查toBillNo和entryId=1明细"]
  D --> E["查申请站103092正流水"]
  E --> F["查申请站实时库存"]
  F --> G{"流水和实时库存一致?"}
  G -->|否| H["库存服务/分表/货位异常"]
  G -->|是| I["页面缓存或查询维度错误"]

19.2 同一申请出现两张调出单

  1. 按 apply_bill_no 查所有 STF 主单,包括 isDelete=1。
  2. 一张删除、一张有效,可能是撤销后重出。
  3. 多张均有效时查调出时间、Redis 锁和申请状态更新时间。
  4. 按每张 iid 查库存流水,判断是否重复扣减。
  5. 修复必须成对处理 STF、流水、实时库存,禁止只软删主表。

19.3 已入库但金额对不上

flowchart TD
  A["已入库金额异常"] --> B["逐行查申请/出/入数量和价格"]
  B --> C["重算out_price=sum(out_qty*price)"]
  B --> D["重算in_price=sum(in_qty*abs(price))"]
  C --> E{"是否少入?"}
  D --> E
  E -->|是| F["出入金额不同可能符合当前规则"]
  E -->|否| G["查价格导入/请求/重复明细"]
  F --> H["再查PaymentInfo和diffAmount"]
  G --> H

19.4 撤销成功但库存没恢复

  1. 申请是否回到状态 0。
  2. 原 STF 主/明细是否 isDelete=1。
  3. 原 iid + TRANSFER + 103092 流水是否被反向处理。
  4. 发货站原仓库货位实时库存是否恢复。
  5. InventorySer::delete() 是否抛错。
  6. 撤销库存 MQ 是否仅下游滞后。

19.5 调拨收付款没有候选

  1. 当前站是申请方还是发货方。
  2. settleList.type 是 payment 还是 receipt。
  3. 对应阶段是否已经出库/入库。
  4. 调拨金额是否为 0。
  5. PaymentInfo 已结算金额和 diffAmount 是否已覆盖全部金额。

20. 只读 SQL

以下为模板。先确认环境和字段版本,只读执行;库存、Payment、Account 按 sid 算分表。

20.1 申请主表

select id, bill_no, apply_sid, deliver_sid, bill_status,
       out_price, in_price, create_time, out_time, in_time,
       cancel_time, cancel_u_id, cancel_u_name
from t_scm_stf_apply
where bill_no = :apply_bill_no
   or id = :apply_id;

20.2 申请明细

select id, apply_id, bill_no, inv_id, sku_id,
       apply_qty, out_qty, in_qty, price, remark
from t_scm_stf_apply_info
where apply_id = :apply_id
order by id;

20.3 调拨主表

select id, apply_bill_no, billNo, toBillNo,
       sid, toSid, billStatus, totalQty, totalAmount,
       isDelete, createTime, delete_time, delete_uid
from t_scm_stf_invoice
where apply_bill_no = :apply_bill_no
order by id;

20.4 调拨明细

select id, iid, billNo, entryId, sid, toSid,
       invId, skuId, qty, price, amount,
       locationId, locationAreaId, isEnter, isDelete
from t_scm_stf_invoice_info
where iid = :iid
order by entryId desc, id;

20.5 核对跨站数量

select invId,
       sum(case when entryId = 2 then abs(qty) else 0 end) as out_qty,
       sum(case when entryId = 1 then abs(qty) else 0 end) as in_qty,
       sum(case when entryId = 2 then abs(qty) else 0 end)
       - sum(case when entryId = 1 then abs(qty) else 0 end) as diff_qty
from t_scm_stf_invoice_info
where iid = :iid and isDelete = 0
group by invId;

20.6 发货站库存流水

{out_shard}=deliver_sid%128:

select id, sid, iid, billNo, billType, transType,
       invId, locationId, locationAreaId, qty, price, amount
from t_scm_inventory_0_{out_shard}
where sid = :deliver_sid
  and billType = 'TRANSFER'
  and transType = 103092
  and billNo = :out_bill_no;

20.7 申请站库存流水

{in_shard}=apply_sid%128:

select id, sid, iid, billNo, billType, transType,
       invId, locationId, locationAreaId, qty, price, amount
from t_scm_inventory_0_{in_shard}
where sid = :apply_sid
  and billType = 'TRANSFER'
  and transType = 103092
  and billNo = :in_bill_no;

20.8 两站实时库存

select sid, inv_id, sku_id, location_id, location_area_id, qty
from t_scm_inventory_real_time
where sid in (:deliver_sid, :apply_sid)
  and inv_id = :inv_id
order by sid, location_id, location_area_id;

20.9 调拨收付款

{pay_shard}=sid%32:

select p.id, p.sid, p.billNo, p.transType, p.totalAmount,
       i.stlNo, i.amount, i.diffAmount, i.accId, i.isDelete
from t_scm_payment_{pay_shard} p
join t_scm_payment_info_{pay_shard} i on i.iid = p.id
where p.sid = :sid
  and p.transType in (154001, 154002)
  and i.stlNo in (:out_bill_no, :in_bill_no);

21. 代码和日志定位

21.1 新申请式 STF

rg -n "function (format|create|hasten|cancel|getWaitSettleList)" application/Services/Stf/ApplySer.php
rg -n "function (out|in|rollback|outFormat|inFormat)" application/Services/Stf/InvoiceSer.php
rg -n "redis_lock|for update" application/controllers/stf application/Services/Stf

21.2 库存和消息

rg -n "TRANSTYPE_TRANSFER_S|103092|BillTypeEnums::TRANSFER" application
rg -n "sendInventoryEvent|sendMailToMq|MessageTypeEnums::APPLY" application/Services application/controllers/stf
rg -n "function (save|delete)" application/Services/Storage/InventorySer.php

21.3 旧 TF/STF 和结算

rg -n "function (addSTF|saveTFIn|addTFpayment|paymentList|updatePayment)" application/controllers/scm/InvTf.php application/service/scm/InvTfService.php
rg -n "154001|154002|TFPAYMENT|stlNo|diffAmount" application/service/scm application/models
rg -n "TF_SOURCE_TYPE_|TF_BILL_STATUS_" application/KzData/Enums/TfInvoiceEnums.php

21.4 服务化路由

rg -n "SwitchRouterTransfer|NEW_DGJ_SERVICE|TransferUriProvider" application
rg -n "request-to-dgj-service|新站管家路由服务开关" application

22. 故障证据顺序

现象第一证据第二证据第三证据不应先做
创建后无提醒申请主/明细MQ 生产日志消费侧站内信重复创建
调出库存不足实时货位库存锁定量请求仓库货位手工加库存
调出后仍待出库申请状态STF 主单缓存/读延迟再次调出
长期待入库状态 1实物流转调入操作申请站手加库存
入库后未增加调入单/明细正库存流水实时库存再点入库
撤销后异常STF 软删原流水反向实时库存只改申请状态
金额不一致三种数量STF 正负金额PaymentInfo直接改 Payment
新旧页面不同实际 URI三套表/状态路由开关混改两套数据

23. 回归测试矩阵

23.1 申请

场景预期
单发货站多商品一主多明细
多发货站按站拆多主单,单事务落库
发货站等于当前站拒绝
同站重复商品拒绝整批
数量 0/负数拒绝
商品不存在拒绝
某提醒 MQ 失败申请保留,可按单补消息
申请方取消状态 4
发货方驳回状态 3
非参与站取消拒绝
非状态 0 催办/取消拒绝

23.2 调出

场景预期
全量调出申请 0 到 1,负流水,发货站库存减少
部分调出合法,金额按实际出库
所有行 0整单拒绝
请求漏明细整单拒绝
库存足但被锁拒绝并显示锁定量
微仓拒绝
价格 0/非法拒绝
并发调出仅一张有效 STF、一次扣库存
MQ 失败本地正确,补消息不重做调出

23.3 调入

场景预期
全量调入申请 1 到 2,正流水,申请站库存增加
少量调入按 in_qty 入库存和金额
某行 0 入库不写该行库存变化,确认业务差异
入库大于出库拒绝
微仓入库拒绝
并发入库行锁保证一次成功
重复入库返回已调拨入库
中途异常状态、成本、库存整体回滚

23.4 撤销和财务

场景预期
待入库未结算撤销申请回 0、STF 软删、库存恢复
已入库撤销拒绝
已收付款撤销拒绝
撤销后重出新 STF 有效、旧 STF 保留删除历史
调拨收款154001,关联正确调拨单
调拨付款154002,关联正确调拨单
重复结算待结算余额不允许超额
修改/删除结算PaymentInfo 和账户事实一致反向更新

23.5 新旧兼容

场景预期
普通 TF使用 103091 和 TF 状态,不写申请表
新申请 STF使用 103092 和申请状态
历史 STF 无申请号旧查询可解释,迁移任务不误执行
路由开关开/关明确请求到新服务还是本地旧服务

24. 高风险改动

改动位置影响最小回归
ApplySer::format/create拆单、商品、消息单/多站、重复、事务、MQ
InvoiceSer::outFormat/out发货库存和金额锁定、部分出、并发、负流水
InvoiceSer::inFormat/in申请站库存和成本少入、零入、并发、正流水
InvoiceSer::rollback库存反向和软删未/已结算、重出、MQ
InventorySer全系统库存采购、销售、盘点、调拨、活动
InvTfService新旧调拨和资金TF、旧 STF、154001/154002
SwitchRouterTransfer新旧服务流量两侧响应、超时、降级
STF 表索引幂等和历史唯一键、软删重建、旧数据

25. 已确认事实

  • apply_sid 是申请/调入站,deliver_sid 是发货/调出站。
  • 创建按 deliver_sid 拆单。
  • 状态 0 才能取消、驳回、催办和调出。
  • 调出数量可小于申请数量,但整单必须大于 0。
  • 发货库存校验扣除 unsaleLockNum。
  • 微仓不能作为调出或调入仓库。
  • 调出负数量、调入正数量,交易类型均为 103092。
  • 调入在 MySQL 行锁内防重复。
  • 撤销只允许待入库且未收付款。
  • 入库不会自动创建收付款。
  • 调拨收款/付款分别为 154001/154002。
  • 普通 TF 使用 103091 和另一套状态。

26. 环境待确认

待确认项验证方式
新旧入口线上流量占比网关日志按 URI 统计
apply_bill_no 唯一索引show create table
Redis 调出锁语义和 TTL读实现并发压测
MQ confirm/重试/死信查 RabbitMQ 配置
少入责任和损耗口径产品/财务确认
结算唯一键和账户原子性查 Payment/Account DDL 和事务
服务化路由开关只读查 Redis key 和部署配置

27. 源码证据

结论权威路径
申请接口application/controllers/stf/Apply.php
调出/调入/撤销接口application/controllers/stf/Invoice.php
申请拆单和状态application/Services/Stf/ApplySer.php
出入库和回滚application/Services/Stf/InvoiceSer.php
申请状态application/KzData/Enums/StfApplyEnums.php
STF 状态/方向application/KzData/Enums/StfInvoiceEnums.php
普通 TF 状态application/KzData/Enums/TfInvoiceEnums.php
交易类型application/KzData/Enums/TransTypeEnums.php
表名application/config/tables.php
STF 模型application/models/stf/*
库存写入application/Services/Storage/InventorySer.php
消息生产application/Services/Mq/MqSer.php
旧调拨和结算application/service/scm/InvTfService.php
历史迁移application/controllers/tasks/ApplyTransfer.php
服务化路由application/Components/Traits/SwitchRouterTransfer.php

28. 一页式排查顺序

flowchart TD
  A["收到调拨问题"] --> B["确认新申请STF/旧STF/普通TF"]
  B --> C["确认申请站、发货站、申请号"]
  C --> D["查申请状态和三种数量"]
  D --> E["查有效/删除STF和出入单号"]
  E --> F["按entryId核对负调出和正调入"]
  F --> G["分别按两个sid算库存分表"]
  G --> H["核对两站流水和实时库存"]
  H --> I["核对MQ是否仅下游滞后"]
  I --> J["金额问题再查154001/154002"]
  J --> K["按业务事实补偿,禁止只改状态"]

调拨的最小闭环不是“一张单状态正确”,而是申请状态、STF 主明细、发货站负库存事实、申请站正库存事实、在途差异和资金结算分别可解释。

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

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
发起申请PC/E站调拨申请申请/供给 sid、SKU、申请量STF/Move ControllerApply/Move ServiceSCM_STF_APPLY 单号申请主明细等待供给站处理
供给站出库PC/PDA/配送STF 单、SKU、实发量、仓位调拨出库入口STF/库存 ServiceSTF/TF 出库单发货站库存减少、在途增加
申请站入库PC/PDA 签收调拨单、SKU、实收量、仓位调拨入库入口STF/库存 Service原调拨单申请站库存增加、在途结清
取消/结算申请方/发货方动作或财务取消原因、未履约量、结算金额Controller/Task调拨/Payment/Account Service调拨单号取消剩余量并处理资金

日志证据矩阵

| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 申请 | Controller/Apply Service | request_id、申请单、两端 sid、SKU | 主明细 commit | 同站调拨、重复申请、不可调商品 | 申请单映射 STF 单 | | 出库 | STF/Inventory Service | STF 单、出库单、SKU、transType | 发站库存 -n、状态 WAIT_IN | 超发、重复出库、部分成功 | 出库单查库存流水/在途 | | 入库 | STF/Inventory Service | 原调拨单、入库单、SKU | 收站库存 +n、状态 IN_FINISH | 超收、重复收货、仓位错误 | 原单串联两站流水 | | 取消结算 | 调拨/财务日志 | 调拨单、取消动作、支付单 | 未履约关闭且账款一次结算 | 已发后误全取消、重复退款 | 调拨单关联支付/账户 |

环节数据变更台账

步骤代码位置事务读取事实写入表/缓存/MQ字段或数量变化回查证据
创建申请Apply Service调拨事务供需站点、商品、申请量SCM_STF_APPLY(_INFO)insert;状态 WAIT_OUT;applyQty=n申请单、SKU 明细
生成发货STF Service出库事务可发量、发站库存SCM_STF_INVOICE(_INFO)、SCM_TF_INVOICE*shippedQty +n;状态 WAIT_OUT -> WAIT_IN申请/发货单数量等式
发站扣库InventorySer应与出库同边界发站四维库存SCM_INVENTORY*、实时表发站 qty -n,在途 +n发站流水、在途量
收站入库STF/Inventory Service入库事务可收量、收站仓位调拨明细、库存表receivedQty +n;收站 qty +n;在途 -n;全收 -> IN_FINISH两站流水 + 在途=0
取消结算Cancel/Payment Service取消与资金事务未发/未收量、结算事实调拨状态、SCM_PAYMENT*/SCM_ACCOUNT*状态 CANCEL_*;只关闭剩余量;资金一次冲正数量守恒、支付分录

子模块追踪:stf-apply 服务站调拨申请

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
创建申请申请站提交跨站调拨request ID、applyNo、from/to sid、SKU、qtyapplication/controllers/stf/Apply.php -> application/Services/Stf/ApplySer.php两站关系、商品、申请量、重复来源和权限调拨本地事务 insert 申请主明细,status none -> WAIT_OUTrequest ID + applyNo + both sid + SKU校验/重复失败零写入;超时按申请业务键回查
申请回查提交后页面无单/数量错applyNo、entryIdapplication/KzData/Enums/StfApplyEnums.php主明细、申请量和状态查询只读 不写applyNo + entryId + row count主单无明细走领域撤销/重建,不手补孤儿行

子模块追踪:stf-audit 申请审核与供给确认

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
审核确认供给站接受/拒绝申请applyNo、action、operator、supply qtyapplication/Services/Stf/ApplySer.php当前申请态、可供量、已确认和取消量审核本地事务 WAIT_AUDIT -> WAIT_OUT/rejected、确认量 old -> nrequest ID + applyNo + action/operator状态变化后旧请求零写入;并发确认重新读取可供量
后置关系审核通过准备发货applyNo、supply sidapplication/controllers/tasks/ApplyTransfer.php已审核申请和已有调拨关系单任务本地事务创建/确认关系;重复任务 0 新增task/message ID + applyNo任务失败只补关系,不重复审核和申请

子模块追踪:stf-outbound 供给站调拨出库

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
发货出库供给站按申请发货apply/invoiceNo、SKU、source location、qtyapplication/controllers/stf/Invoice.php -> application/Services/Stf/InvoiceSer.php可发量、发站库存、已发/取消量出库与库存本地事务写 STF/TF invoice,shippedQty +n、发站 qty -n、在途 +nrequest ID + apply/invoiceNo + SKU + transType超发/库存不足整单回滚;重试前查 invoice/流水
出库回查发站扣库或状态不一致invoiceNo、inventory billNoapplication/Services/Storage/InventorySer.php出库明细、发站流水、实时和在途查询只读 不写invoice/inventory billNo + SKU只补缺失段;禁止既重发 invoice 又手工扣库

子模块追踪:stf-intransit 调拨在途

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
进入在途发货事务完成apply/invoiceNo、shipped qtyapplication/KzData/Enums/StfInvoiceEnums.php发货单、已收量、当前调拨态本地事务 WAIT_OUT -> WAIT_IN,在途 old -> old+ninvoiceNo + shipped/intransit qty状态不以消息大小判断,按有效发货事实计算
在途对账发站已扣、收站未收both sid、invoiceNo、SKUapplication/KzData/Enums/TfInvoiceEnums.php发站负向流水、收站正向流水、取消和在途量查询只读;在途=有效发货-有效收货-关闭invoiceNo + both station flows长期在途先查物流/收货事实;不可直接给收站加库存

子模块追踪:stf-inbound 申请站调拨入库

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
收货入库申请站确认收货apply/invoiceNo、SKU、target location、qtyapplication/Services/Stf/InvoiceSer.php -> application/Services/Storage/InventorySer.php可收量、已收/在途、目标仓位和重复单入库本地事务 receivedQty +n、收站 qty +n、在途 -n;全收到完成request ID + invoice/inventory billNo + SKU超收/重复整单回滚;部分收货保持 WAIT_IN
两站验收调拨完成后核对invoiceNo、两端库存键application/service/scm/InvTfService.php发站、在途、收站有效流水和状态查询只读;非损耗总量守恒、在途为 0invoiceNo + source/target flows不平时只补缺失端并保留原/补偿单

子模块追踪:stf-cancel 调拨取消与剩余量关闭

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
取消关闭申请或发货剩余量取消apply/invoiceNo、reason、remaining qtyapplication/Services/Stf/ApplySer.php已发、已收、已关和当前状态取消本地事务只关未履约量 closedQty +n、状态到对应 CANCELrequest ID + apply/invoiceNo + qty已发/已收部分不反向抹除;并发履约后重算剩余量
库存补偿已扣未收部分退回/关闭invoiceNo、inventory flowsapplication/Services/Storage/InventorySer.php外部物流终态、在途和两站库存独立库存事务按业务反向 source qty +n/intransit -noriginal/compensate billNo + SKU只有确认退回才恢复发站;补偿重跑 0 变化

子模块追踪:stf-settlement 调拨资金结算

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
生成结算调拨履约达到结算条件apply/invoiceNo、双方 sid、amountapplication/Services/Stf/InvoiceSer.php有效收货、价格、已结/退款和账户资金本地事务写 Payment/Account 分录,settled old -> old+nrequest ID + transfer/payment billNo + amount数量未确认不结算;重复来源键不重复记账
取消冲正取消/退回后的资金反向original payment、cancel/return billNoapplication/Services/Mq/MqSer.php原成功分录、已冲正额和外部终态反向资金事务 reversed +n,不超过原结算;通知事务外original/reverse billNo + message ID外部不确定先查终态;只补缺失资金或消息段