本手册从一个销售单号、出库单号或来源订单号出发,完整追踪“销售开单 -> 分仓出库 -> 扣减库存 -> 配送签收 -> 对账核销 -> 销退入库 -> APP/SAAS 同步”。
适用于销售单不能出库、部分出库状态错误、库存已扣但单据失败、配送状态不推进、收款后未核销、销退后库存未恢复、APP/SAAS 未收到事件等问题。本文只提供只读查询和安全判断方法;任何生产修复必须先保存证据、明确业务唯一键并完成多方复核。
1. 业务目标
- 用
sid + billNo找到正确的 16 分片销售单和 64 分片出库单。 - 区分销售订单状态、销售出库状态、配送状态、收款状态和下游状态。
- 解释每一行商品的订购量、有效出库量、结单量、退货量和库存变化。
- 判断故障发生在入口校验、事务落库、库存扣减、TMS、资金还是 MQ。
- 识别“数据库提交但 MQ 未发”和“MQ 已发但数据库回滚”两种跨系统不一致。
- 为重复出库、重复销退、重复回调和重复同步建立幂等检查依据。
- 修复后同时验收销售、库存、配送、资金、报表和下游六个视图。
2. 适用现象与首查方向
| 现象 | 首查对象 | 常见原因 |
|---|---|---|
| 销售单列表看不到 | t_scm_sa_order_{sid%16} | 查错分片、isDelete、站点上下文错误 |
| 销售单一直待出库 | 订单明细与有效出库明细 | 出库未落库、出库单撤销、来源行关联错误 |
| 已出一部分却显示待出库 | srcOrderEntryId、订单状态回写 | 出库行未关联原销售行、状态统计未执行 |
| 已全部出库仍显示部分出库 | 出库量、结单量、退货量口径 | 撤销单被计入、数量符号处理错误、历史脏数据 |
| 页面显示有库存但不能出库 | 实时库存、锁定量、货位 | qty - lockNum 不足、货位删除、仓库维度不一致 |
| 出库成功库存没减少 | 出库明细、库存流水、实时库存 | 库存事务失败、维度错误、实时表未同步 |
| 接口报错但库存已扣 | 本地事务与异常路径 | 非同一连接、事务边界外写入、历史实现差异 |
| 出库成功但 TMS 无配送单 | 站点开关、来源类型、配送创建 | AI 工作台开关未开、来源不支持、远程调用失败 |
| 配送中一直不送达 | 三方配送主表与关系表 | 回调未到、配送号关联错误、状态映射不一致 |
| 已送达但未对账 | 出库状态、支付明细、人工操作 | 配送只推进到 3,对账动作未执行 |
| 已收全款仍未核销 | amount/payAmount/diffAmount | 金额精度、支付单未关联、状态回写失败 |
| 撤销对账失败 | PaymentInfo 与出库状态 | 已存在有效收款,不允许直接回退 |
| 销退后库存没恢复 | 150602 出库表记录与库存流水 | 退货事务失败、货位错误、可退量计算错误 |
| APP 未收到出库通知 | dgj_notify、routing key | 生产失败、下游未消费、业务键不一致 |
| SAAS 出现重复出库 | 消息 ID 与业务唯一键 | uniqid() 每次不同,下游未按出库单号幂等 |
3. 五类单号不要混用
| 标识 | 常见字段 | 用途 |
|---|---|---|
| 销售单主键 | saOrder.id | 关联订单明细和出库单 srcOrderId |
| 销售单号 | saOrder.billNo | 页面、日志、下游订单业务键 |
| 来源订单主键 | srcOrderId | E站、APP、机器人、活动或 SAAS 原订单 |
| 来源订单号 | srcOrderNo | 跨系统人工追踪 |
| 出库单主键 | saInvoice.id | 关联出库明细 iid 和资金 stlId |
| 出库单号 | saInvoice.billNo | 配送、收款、下游出库业务键 |
| 原销售单主键 | saInvoice.srcOrderId | 由出库单反查销售单 |
| 原销售单号 | saInvoice.srcOrderNo | 跨页面展示和撤销同步 |
| 配送单号 | delivery_no 等 | TMS 主表和关系表关联 |
排查记录至少保留:
环境:
sid:
销售单 id / billNo:
来源 sourceType / srcOrderId / srcOrderNo:
出库单 id / billNo:
配送单号:
客户 buId:
异常 invId / skuId:
仓库 locationId / 货位 locationAreaId:
支付方式 payType / 收款状态 hxStateCode:
首次异常时间:
最近一次操作:
期望结果:
实际结果:
4. 真实分表规则
4.1 先计算两个后缀
销售订单分片 = sid % 16
销售出库分片 = sid % 64
以 sid = 10001 为例:
10001 % 16 = 1
10001 % 64 = 17
销售主单:t_scm_sa_order_1
销售明细:t_scm_sa_order_info_1
出库主单:t_scm_sa_invoice_0_17
出库明细:t_scm_sa_invoice_info_0_17
4.2 表定位字典
| 业务数据 | 物理表规则 | 是否分片 | 主要关联键 |
|---|---|---|---|
| 销售主单 | t_scm_sa_order_{sid%16} | 16 | id、sid、billNo |
| 销售明细 | t_scm_sa_order_info_{sid%16} | 16 | iid -> 销售主单.id |
| 销售明细扩展 | t_scm_sa_order_info_ext | 当前 Model 不分片 | iid、invId、vin |
| 销售日志 | t_scm_sa_order_log | 以线上 DDL 为准 | iid、状态和操作类型 |
| 销售出库主单 | t_scm_sa_invoice_0_{sid%64} | 64,前缀含业务类型 0 | id、srcOrderId、billNo |
| 销售出库明细 | t_scm_sa_invoice_info_0_{sid%64} | 64 | iid -> 出库主单.id |
| 出库明细扩展 | t_scm_sa_invoice_info_ext | 当前 Model 不分片 | iid、invId、vin |
| 异常退货日志 | t_scm_abnormal_return_log | 当前 Model 为准 | 原出库、退货、异常原因 |
| 三方配送主表 | t_scm_third_party_delivery_order | 当前 Model 不按 sid 拼后缀 | 配送单号、sid |
| 出库配送关系 | t_sa_invoice_delivery_relation | 当前 Model 不按 sid 拼后缀 | 出库单、配送单 |
| 库存流水主表 | t_scm_inventory_* | 参考库存手册,通常按 sid 分片 | sid、单号、transType |
| 实时库存 | t_scm_inventory_real_time_* | 参考 Model 实际后缀 | sid/inv_id/location_area_id |
| 支付明细 | t_scm_payment_info_{sid%32} | 32 | stlId -> 出库单.id |
4.3 常见误查
- 查
t_scm_sa_order基表但真实数据在_0到_15。 - 出库表写成
t_scm_sa_invoice_{sid%64},漏掉中间业务类型_0_。 - 用销售单号查出库明细的
billNo,实际应先通过srcOrderId/srcOrderNo找出库主单。 - 扩展表机械追加站点后缀;当前两个 Ext Model 都直接使用常量表。
- 只查
isDelete = 0,却忘了撤销使用billStatus = 6而不是软删。
flowchart LR
SID["sid"] --> S16["sid % 16"]
SID --> S64["sid % 64"]
S16 --> SO["sa_order_n"]
S16 --> SOI["sa_order_info_n"]
S64 --> SI["sa_invoice_0_n"]
S64 --> SII["sa_invoice_info_0_n"]
SO -->|"id = iid"| SOI
SO -->|"id = srcOrderId"| SI
SI -->|"id = iid"| SII
5. 入口、Service 和模型
5.1 入口文件
| 入口 | 文件 | 作用 |
|---|---|---|
| PC 销售 | application/controllers/scm/InvSa.php | 销售单、出库、销退、对账等老入口 |
| 销售出库页面 | application/controllers/sale/OutOrder.php | 新销售出库入口 |
| App code | application/config/appapis.php | tradeCode 到 Controller 的映射 |
| PDA 销售 | application/Services/Pda/SaleSer.php | 扫码/移动出库场景 |
| 外部/内部 API | application/config/apis.php、application/config/appapis.php | 外部状态、库存和销售相关调用 |
| MQ 任务 | application/controllers/tasks/* | 下游和配送回调 |
5.2 核心业务实现
| 模块 | 文件 | 排查重点 |
|---|---|---|
| 老销售编排 | application/service/scm/InvSaService.php | 入口参数、事务、权限、配送、同步 |
| 销售工厂 | application/Services/InvSaFactory.php | 根据类型选择销售/销退实现 |
| 普通销售出库 | application/Services/InvSa/NormalSaleSer.php | 数量校验、负数量明细、库存写入 |
| 普通销退 | application/Services/InvSa/NormalSaleReturnSer.php | 可退量、正数量入库、退款 |
| 新销售领域 | application/Services/SaOrders/SaOrderSer.php | 新接口销售/销退编排 |
| 出库查询和同步 | application/Services/SaOrders/SaInvoiceSer.php | 出库查询、状态和 SAAS 消息 |
| 三方配送 | application/Services/SaOrders/ThirdDeliverySer.php | 配送创建、关系、取消和状态 |
| 库存 | application/Services/Storage/InventorySer.php | 流水与实时库存 |
| 下游同步 | application/Services/SyncOrder/* | dgj_notify 事件构建和发送 |
| MQ 生产 | application/Services/Mq/MqSer.php | APP/SAAS/TMS 相关 routing key |
| 收款 | application/service/scm/ReceiptService.php | 收款、退款、核销和撤销对账 |
5.3 核心 Model
rg -n "class SaOrderModel|class SaOrderInfoModel|class SaInvoiceModel|class SaInvoiceInfoModel" application/models/saOrders
rg -n "SCM_SA_ORDER_SUB_NUM|SCM_SA_INVOICE_SUB_NUM" application/config/tables.php
rg -n "setSid\(|getTable\(|invoice_sub|invoice_info_sub" application/models/saOrders
6. 状态不是一条链,而是五条链
6.1 销售单状态 billStatus
| 值 | 枚举 | 业务含义 | 允许的后续动作 |
|---|---|---|---|
| 0 | SA_STATUS_WAIT | 待出库 | 编辑、出库、无出库关单 |
| 1 | SA_STATUS_OUT_FEW | 部分出库 | 继续出库、结单 |
| 2 | SA_STATUS_OUTED | 已完成 | 查看、收款、售后 |
| 3 | SA_STATUS_CLOSE | 已关闭 | 通常只读,不再出库 |
订单状态主要由有效出库量和结单量决定,不能只依赖前端传值。
stateDiagram-v2
[*] --> 待出库
待出库 --> 部分出库: 部分有效出库
待出库 --> 已完成: 一次全部出库
待出库 --> 已关闭: 无有效出库时关单
部分出库 --> 部分出库: 再次部分出库
部分出库 --> 已完成: 全部出库或结单
已完成 --> [*]
已关闭 --> [*]
6.2 出库单状态
| 值 | 枚举 | 业务含义 | 注意 |
|---|---|---|---|
| 0 | BILL_STATUS_RE_OUTED | 历史单据已出库 | 与 1 文案相同,兼容旧数据 |
| 1 | BILL_STATUS_OUTED | 已出库 | 库存已经扣减 |
| 2 | BILL_STATUS_SENDIND | 派送中 | 配送状态推进 |
| 3 | BILL_STATUS_ARRIVE | 已送达 | 可进入对账 |
| 4 | BILL_STATUS_CHECK | 已对账 | 尚未必全部收款 |
| 5 | BILL_STATUS_OVER | 已核销 | 资金已结清或金额为 0 |
| 6 | BILL_STATUS_CANCELED | 已撤销 | 原出库不再计有效履约 |
stateDiagram-v2
[*] --> 已出库
已出库 --> 派送中: 配送接单/发车
派送中 --> 已送达: 签收回调
已出库 --> 已送达: 自提/跳过配送
已送达 --> 已对账: 对账
已对账 --> 已核销: 收款和优惠覆盖应收
已出库 --> 已撤销: 撤销出库
派送中 --> 已撤销: 配送取消且业务允许
6.3 收款状态 hxStateCode
| 值 | 含义 | 与出库状态的关系 |
|---|---|---|
| 0 | 未收款 | 出库可已完成,但资金未结清 |
| 1 | 部分收款 | 通常仍处于对账或未核销 |
| 2 | 全部收款 | 满足金额条件时出库单可到 5 |
6.4 交易类型
transType | 含义 | 库存方向 | 资金方向 |
|---|---|---|---|
170502 | 销售订单 | 不直接代表库存事实 | 订单金额 |
150601 | 销售出库/销售收款 | 出库,明细通常为负数 | 应收/收款 |
150602 | 销售退货/销售退款 | 入库,明细通常为正数 | 应退/退款 |
6.5 配送和下游状态
配送平台可能有自己的接单、取货、配送、签收、取消状态;下游 APP/SAAS 也有订单和出库状态。它们只能通过业务键映射,不能直接把外部数字写进本地 billStatus。
flowchart TB
SO["销售单状态 0/1/2/3"]
OUT["出库状态 0..6"]
PAY["收款状态 0/1/2"]
TMS["配送平台状态"]
DOWN["APP/SAAS状态"]
SO --> OUT
OUT --> PAY
OUT --> TMS
SO --> DOWN
OUT --> DOWN
TMS -.回调映射.-> OUT
PAY -.金额判断.-> OUT
7. 来源类型与下游映射
sourceType | 来源 | 支付/场景 | 下游订单类型 |
|---|---|---|---|
| 0 | 销售开单 | 普通线下 | 7 |
| 1 | 快维 ERP | 外部 ERP | 当前同步逻辑会排除部分事件 |
| 8 | E站商城 | 挂账 | 2 |
| 9 | E站商城 | 微信 | 2 |
| 10 | E站维修马上送 | 挂账 | 4 |
| 14 | E站活动 | 活动订单 | 5 |
| 15 | E站维修马上送 | 微信 | 4 |
| 16 | AI 助手/机器人 | 挂账 | 8 |
| 17/18 | E站 APP IM | 挂账/微信 | 9 |
| 19/20 | E站 APP 商城 | 挂账/微信 | 2 |
| 21/22 | E站 APP 活动 | 挂账/微信 | 5 |
历史值 2、5、6、7、11、12、13 仍可能存在于数据库,不能因为当前页面不再创建就忽略。
风险点:SyncOrderBuilder::ORDER_TYPE_MAP 不是对所有历史 sourceType 都天然完备。执行关单、结单同步前,应确认当前值存在映射,否则数组索引异常可能让本地状态已改、下游消息未发。
8. 正向销售主流程
sequenceDiagram
participant U as PC/App/PDA
participant C as Controller
participant S as InvSaService
participant O as 销售订单16分片
participant I as 销售明细16分片
participant M as dgj_notify
U->>C: 创建/编辑销售单
C->>S: 注入 sid/uid 后校验
S->>S: 客户/商品/价格/来源校验
S->>O: 写主单
S->>I: 写明细
S->>O: 写扩展/日志/关联
S->>M: garage_repair_order_created/updated
M-->>U: 下游异步同步
8.1 创建前校验
- Session 注入的
sid必须覆盖或校验客户端站点。 - 客户
buId、仓库、商品invId/skuId必须属于当前站点。 - 数量大于 0;金额、折扣、优惠和总额应能重算。
- 来源订单的
srcOrderId/srcOrderNo/sourceType要形成稳定唯一关系。 - 销售站点黑名单、系统锁定和权限菜单会阻断出库。
- 0 元行在下游构建中会被视为赠品,需要确认业务是否允许。
8.2 主单写入字段
| 字段 | 用途 |
|---|---|
id | Snowflake 主键 |
sid | 分片和租户边界 |
billNo | 销售单业务号 |
buId | 客户/修理厂 |
sourceType | 来源类型 |
srcOrderId/srcOrderNo | 来源系统关联 |
billStatus | 订单状态,初始通常 0 |
payType | 挂账/现金/微信 |
hxStateCode | 收款状态 |
totalAmount/disAmount/totalDiscount | 金额与优惠 |
billNo_type | 销售、铺货、微仓铺货 |
delivery_info | 配送相关上下文,具体结构以 DDL/代码为准 |
8.3 明细写入字段
| 字段 | 用途 |
|---|---|
iid | 销售主单 ID |
id | 销售行 ID,出库行通过 srcOrderEntryId 关联 |
invId/skuId | 站内物料和中心 SKU |
qty | 订购数量,订单层通常为正 |
price/deduction/amount | 单价、行优惠、金额 |
srcOrderId/srcOrderNo | 来源行/来源单关联 |
vin/carModel | 车型信息,部分写入扩展表 |
8.4 创建后的验证
-- 将 :so_suffix 替换为 sid % 16,只读执行
select id, sid, billNo, sourceType, srcOrderId, srcOrderNo,
billStatus, payType, hxStateCode, totalQty, totalAmount,
createTime, modifyTime, isDelete
from t_scm_sa_order_:so_suffix
where sid = :sid and billNo = :sale_bill_no;
select id, iid, billNo, invId, skuId, qty, price, deduction, amount,
srcOrderId, srcOrderNo, isDelete
from t_scm_sa_order_info_:so_suffix
where sid = :sid and iid = :sale_order_id
order by id;
预期:主单唯一、有效明细非空、sum(qty) 与主单总数量一致、行金额汇总能解释主单金额。
9. 销售出库请求与校验
9.1 脱敏请求结构
InvSaService::addOutBound() 接收页面 postData JSON,并注入登录上下文。示例仅用于理解字段:
{
"saleOrId": "1000000000001",
"saleOrNo": "XS202607150001",
"buId": 20001,
"billNo_type": 0,
"entries": [
{
"srcOrderEntryId": "1000000000101",
"invId": 30001,
"skuId": "SKU-DEMO-001",
"qty": 2,
"totalQty": 5,
"lockNum": 0,
"price": "100.00",
"amount": "200.00",
"qtyGrid": [
{
"locationId": 10,
"locationAreaId": 101,
"outqty": 2
}
]
}
]
}
9.2 入口层已确认校验
- PC 校验
SA_OUT_ADD权限;App 走权限 ID 248。 - Redis
HASH_SALE_STATION_BLACKLIST命中时阻止销售。 OpsMenuRuleNewModel的lock_status = 1时阻止销售。- 每行必须有
qtyGrid,否则提示货位已删除。 qty <= totalQty - lockNum,否则提示锁定数量导致库存不足。- 批量检查货位是否已删除。
NormalSaleSer再校验可出数量、原销售行和库存。
9.3 出库事务主链
sequenceDiagram
participant U as 操作端
participant S as InvSaService
participant F as NormalSaleSer
participant DB as 销售/出库/库存DB
participant T as TMS
participant MQ as dgj_notify
U->>S: addOutBound(postData)
S->>S: 权限/黑名单/锁定/货位校验
S->>DB: begin transaction
S->>F: addInvoice(data)
F->>DB: 写出库主单和负数明细
F->>DB: 写库存流水并扣实时库存
F->>DB: 回写销售订单状态
S->>T: 条件满足时创建/更新配送单
S->>MQ: outbound created/updated
S->>DB: commit
S-->>U: 出库单 id/billNo
9.4 事务边界风险
当前入口在本地数据库事务提交前调用 TMS 处理和 SyncOrderSer 发布消息。由此必须考虑:
| 场景 | 本地 DB | 外部/MQ | 结果 |
|---|---|---|---|
| 全部成功 | 提交 | 成功 | 正常 |
| MQ 抛异常 | 回滚 | 失败 | 本地无单,通常可重试整个操作 |
| MQ 成功后 DB 提交失败 | 回滚 | 已成功 | 下游出现本地不存在的出库事件 |
| TMS 成功后后续异常 | 回滚 | 配送单已创建 | TMS 孤儿配送单 |
| DB 提交后客户端超时 | 已提交 | 可能成功 | 用户重试可能重复出库 |
因此“接口报错”绝不等于“什么都没发生”。必须先查本地出库单、库存、TMS 和 MQ 日志,再决定是否重试。
10. 出库数量与状态回写
10.1 数量符号
- 销售订单明细
qty通常为正数,表示订购量。 - 销售出库明细在
NormalSaleSer中写为-abs(outqty)。 - 销售退货明细使用正数量,表示库存回流。
- 页面展示通常会取绝对值;SQL 对账不能直接把正负号抹掉。
10.2 当前状态判断
老实现 getOutboundQty() 的核心判断可抽象为:
orderedQty = 销售订单总数量
outQty = 有效销售出库数量绝对值
closeQty = 结单不再出库数量
outQty = 0 -> 待出库 0
0 < outQty 且 orderedQty > outQty+closeQty -> 部分出库 1
outQty > 0 且 orderedQty = outQty+closeQty -> 已完成 2
closeQty 的真实来源由 InvSaModel::getOutboundQty() 聚合口径决定。历史版本可能存在主表和分表并存的 SQL,排查时要以当前部署代码生成的物理表为准。
flowchart TD
A["汇总有效出库量"] --> B{"outQty = 0?"}
B -->|是| W["待出库 0"]
B -->|否| C{"orderedQty > outQty + closeQty?"}
C -->|是| P["部分出库 1"]
C -->|否| D{"orderedQty = outQty + closeQty?"}
D -->|是| F["已完成 2"]
D -->|否| E["数量异常,禁止直接改状态"]
10.3 有效出库口径
建议只把以下记录视为正向有效出库:
- 主单
transType = 150601。 isDelete = 0。billStatus < 6,撤销单排除。- 明细
isDelete = 0。 srcOrderId、srcOrderEntryId正确指向原销售单和销售行。
11. 出库落库与库存核对
11.1 出库主单
-- :out_suffix = sid % 64
select id, sid, billNo, billStatus, transType, sourceType,
srcOrderId, srcOrderNo, buId, totalQty, amount,
hxStateCode, createTime, modifyTime, cancelTime, isDelete
from t_scm_sa_invoice_0_:out_suffix
where sid = :sid
and (billNo = :out_bill_no or srcOrderId = :sale_order_id or srcOrderNo = :sale_bill_no)
order by createTime, id;
11.2 出库明细
select id, iid, sid, billNo, transType, invId, skuId,
qty, price, amount, locationId, locationAreaId,
srcOrderId, srcOrderEntryId, srcOrderNo, isDelete
from t_scm_sa_invoice_info_0_:out_suffix
where sid = :sid and iid in (:invoice_ids)
order by iid, srcOrderEntryId, id;
11.3 行级对账
select soi.id as sale_line_id,
soi.invId,
soi.skuId,
soi.qty as ordered_qty,
coalesce(sum(case
when si.transType = 150601 and s.billStatus < 6 and si.isDelete = 0
then abs(si.qty) else 0 end), 0) as valid_out_qty,
coalesce(sum(case
when si.transType = 150602 and s.billStatus < 6 and si.isDelete = 0
then abs(si.qty) else 0 end), 0) as return_qty
from t_scm_sa_order_info_:so_suffix soi
left join t_scm_sa_invoice_info_0_:out_suffix si
on si.srcOrderEntryId = soi.id and si.sid = soi.sid
left join t_scm_sa_invoice_0_:out_suffix s
on s.id = si.iid and s.sid = si.sid
where soi.sid = :sid and soi.iid = :sale_order_id and soi.isDelete = 0
group by soi.id, soi.invId, soi.skuId, soi.qty;
这段 SQL 是排查模板,字段和索引必须先与目标环境 DDL 对齐。
11.4 库存方向
flowchart LR
SALE["销售订单 170502"] --> OUT["销售出库 150601"]
OUT -->|"出库明细 qty < 0"| FLOW1["库存流水减少"]
FLOW1 --> RT1["实时库存减少"]
OUT --> RETURN["销售退货 150602"]
RETURN -->|"退货明细 qty > 0"| FLOW2["库存流水增加"]
FLOW2 --> RT2["实时库存恢复"]
11.5 库存核对维度
不能只按 invId 汇总,至少包含:
sid + invId/skuId + locationId + locationAreaId + storageType
套包、微仓、特殊仓、批次或效期场景还需加入对应维度。
12. 出库失败故障树
flowchart TD
A["点击出库失败"] --> B{"权限通过?"}
B -->|否| B1["检查 PC 权限/APP 248"]
B -->|是| C{"站点被黑名单或锁定?"}
C -->|是| C1["核对 Redis 和 lock_status"]
C -->|否| D{"qtyGrid 非空且货位有效?"}
D -->|否| D1["货位被删或页面缓存旧"]
D -->|是| E{"qty <= totalQty-lockNum?"}
E -->|否| E1["锁定量或库存不足"]
E -->|是| F{"原销售行关联正确?"}
F -->|否| F1["srcOrderEntryId 错误"]
F -->|是| G{"本地出库/库存已落库?"}
G -->|否| H["查事务异常和 SQL"]
G -->|是| I{"TMS/MQ 是否失败?"}
I -->|是| I1["跨系统部分成功"]
I -->|否| J["检查客户端超时/响应解析"]
12.1 错误文案到检查项
| 错误 | 检查 |
|---|---|
| 没有权限进行出库操作 | App 权限 248、PC SA_OUT_ADD |
| 销售业务已被暂时停止 | Redis 销售站点黑名单 |
| 系统已被锁定 | OpsMenuRuleNewModel.lock_status |
| 货位已删除 | qtyGrid、StorageArea 软删、页面缓存 |
| 库存不足,请检查锁定数量 | qty、totalQty、lockNum |
| 单据不存在或已删除 | sid/16 分片、id、billType=SALE、isDelete |
| 已经生成出库单不能关闭 | 64 分片存在状态小于等于 5 的有效 150601 出库 |
13. 配送与 TMS 排查
13.1 创建条件
当前 addOutBound() 中,TMS 创建/更新至少受以下条件影响:
StationSer::isShowAIwork(sid)返回开启。- 销售来源属于普通、ERP、E站商城、维修、活动、机器人或新 E站 APP 来源集合。
- 出库已经由
NormalSaleSer::addInvoice()成功返回。 handleTransportUpsert()返回配送号后,继续保存配送号关系。
13.2 数据关系
flowchart LR
SO["销售单"] --> OUT["销售出库单"]
OUT --> REL["出库配送关系"]
REL --> TMS["三方配送主单"]
TMS --> EXT["外部TMS订单"]
EXT --> CALLBACK["配送状态回调"]
CALLBACK --> TMS
CALLBACK --> OUT
OUT --> APP["APP状态通知"]
13.3 配送排查顺序
- 从出库单拿
id/billNo/sid。 - 查
t_sa_invoice_delivery_relation是否有出库到配送的关系。 - 查
t_scm_third_party_delivery_order的配送号、外部号、状态和时间。 - 查 TMS 请求日志,确认创建还是更新。
- 查回调日志,确认外部状态及本地映射。
- 查出库主单是否从 1 推到 2/3。
- 查
sendSaOrderStatusToMq()是否通知 APP。
13.4 配送异常分类
| 类别 | 本地出库 | 配送关系 | 外部订单 | 处理方向 |
|---|---|---|---|---|
| 未触发 | 有 | 无 | 无 | 检查开关、来源、调用条件 |
| 创建失败 | 有 | 无/半条 | 无 | 查请求异常,确认能否安全重试 |
| 关系丢失 | 有 | 无 | 有 | 先核对唯一键,再补关系 |
| 回调未到 | 有 | 有 | 状态已变 | 查网关、回调地址、签名和日志 |
| 回调未落库 | 有 | 有 | 状态已变 | 查状态映射、事务和幂等 |
| 本地撤销外部未撤销 | 6 | 有 | 配送中 | 检查取消调用和补偿 |
14. 对账、收款与核销
14.1 三个金额
代码对账时会比较:
出库应收 amount
已收 payAmount
优惠/差额 diffAmount
已确认判断:
amount == payAmount + diffAmount 或 amount == 0
-> billStatus = 5,hxStateCode = 2,记录 hasTime
否则
-> billStatus = 4
14.2 资金流程
flowchart TD
O["出库单已送达/可对账"] --> P["汇总 PaymentInfo"]
P --> C{"amount = payAmount + diffAmount?"}
C -->|是| H["状态5 已核销\nhxStateCode=2"]
C -->|否| R["状态4 已对账"]
R --> MORE["继续收款"]
MORE --> C
R --> CANCEL["撤销对账"]
CANCEL --> CHECK{"已有有效收款?"}
CHECK -->|是| DENY["禁止直接撤销"]
CHECK -->|否| BACK["回退到配送/送达状态"]
14.3 支付关联
PaymentInfo 通常通过 stlId = 销售出库单.id 关联,且按 sid % 32 分片。排查时同时带 transType、billStatus、isDelete,避免把已撤销收款或退款混入。
-- :pay_suffix = sid % 32
select id, sid, stlId, billNo, transType, amount, billStatus,
createTime, modifyTime, isDelete
from t_scm_payment_info_:pay_suffix
where sid = :sid and stlId in (:invoice_ids)
order by createTime, id;
14.4 金额问题常见原因
- 金额精度和字符串/浮点比较导致等式不成立。
- 收款关联到错误出库单
stlId。 - 退款与收款正负号被混算。
- PaymentInfo 软删或状态 6 未排除。
- 出库单优惠字段与支付差额字段口径不同。
- 销退退款已经发生,但原销售核销状态未重算。
15. 关单与结单
15.1 两个动作
| 动作 | 请求 type | 前置条件 | 订单结果 | 下游事件 |
|---|---|---|---|---|
| 关闭 | 1 | 无有效正向出库 | 状态 3 | garage_repair_order_cancelled |
| 结单 | 2 | 已有部分出库,不再继续 | 状态 2 | garage_repair_order_stopped |
15.2 关闭校验
closeSaleOr() 会查 64 分片出库主表,判断是否存在:
srcOrderId = 销售单.id
sid = 当前站点
isDelete = 0
transType = 150601
billStatus <= 5
非 App 关闭还会排除 sourceType = 4 的核销式撤销记录。存在有效出库时返回“已经生成出库单不能关闭”。
15.3 本地与下游边界
关单/结单先在本地事务中更新销售单并提交,然后调用 SyncOrderSer 发送下游消息。消息异常是否会回传取决于调用路径;排查时必须分别确认本地状态和 dgj_notify 日志。
16. 撤销出库
撤销不是简单把原出库单设为 6。需要确认:
- 原出库单是否允许撤销,是否已配送、对账、收款。
- 是否生成反向库存事实或撤销类型记录。
- 原销售单的有效出库量是否重算。
- 配送单是否取消,关系是否保留审计。
- 收款、对账、核销是否回退。
- 下游是否收到
garage_repair_outbound_cancelled。
sequenceDiagram
participant U as 操作人
participant S as 销售服务
participant DB as 出库/库存
participant T as TMS
participant MQ as dgj_notify
U->>S: 撤销出库
S->>S: 校验状态和资金
S->>DB: 原单状态6/反向库存
S->>DB: 重算销售单状态
S->>T: 取消配送
S->>MQ: garage_repair_outbound_cancelled
MQ-->>U: 下游异步回执
16.1 不能直接改状态 6 的原因
如果只改主表状态:
- 库存不会自动恢复。
- 销售单仍可能显示已完成。
- TMS 继续配送。
- PaymentInfo 仍然有效。
- APP/SAAS 仍认为已出库。
- 报表可能按库存流水而不是主表状态统计。
17. 销售退货完整链路
17.1 两类入口
- 基于原销售/出库单退货:校验原始可退数量。
- 异常退货或无原单退货:需要异常日志和更严格权限。
17.2 可退数量
基础思想:
可退数量 = 有效销售出库数量 - 已完成有效销退数量 - 在途/审核中退货占用
真实代码可能按出库行、商品、客户和来源单聚合,不能只按 invId。同一商品在不同价格、货位、VIN 或多个出库单上时,必须保留原行关联。
17.3 销退库存和资金
sequenceDiagram
participant U as 操作端/APP
participant R as 销退服务
participant DB as 64分片出库表
participant INV as 库存
participant PAY as 资金
participant MQ as APP/SAAS
U->>R: 提交退货
R->>R: 校验原出库和可退量
R->>DB: 写150602销退主单/正数明细
R->>INV: 增加对应仓库货位库存
R->>PAY: 生成/更新退款关系
R->>MQ: 退货审核/完成通知
17.4 销退排查 SQL
select id, billNo, transType, sourceType, return_type,
srcOrderId, srcOrderNo, billStatus, amount, hxStateCode,
createTime, cancelTime, isDelete
from t_scm_sa_invoice_0_:out_suffix
where sid = :sid
and transType = 150602
and (srcOrderId in (:source_ids) or srcOrderNo in (:source_nos))
order by createTime, id;
select iid, srcOrderId, srcOrderEntryId, invId, skuId,
qty, amount, locationId, locationAreaId, isDelete
from t_scm_sa_invoice_info_0_:out_suffix
where sid = :sid and iid in (:return_invoice_ids)
order by iid, id;
17.5 销退异常判断
| 现象 | 判断 |
|---|---|
| 提示超过可退量 | 原出库有效量、历史销退、审核中占用 |
| 退货单存在但库存未增 | 查库存流水事务、货位和实时库存 |
| 库存增加但退款未生成 | 本地事务和资金调用边界 |
| APP 一直显示审核中 | sendApplyReturnCheckedToAPP 是否发送/消费 |
| APP 显示完成但本地未入库 | 回调或下游事件顺序错误 |
| 重复退货 | 原出库行和外部售后号未幂等 |
18. 下游 MQ 同步地图
18.1 新同步事件
目标 destination:dgj_notify。
| routing key | 触发 | 业务唯一键建议 |
|---|---|---|
garage_repair_order_created | 销售单创建 | stationId + orderNo |
garage_repair_order_updated | 销售单编辑 | stationId + orderNo + version/modifyTime |
garage_repair_order_cancelled | 关闭 | stationId + orderNo + cancelType |
garage_repair_order_stopped | 结单 | stationId + orderNo |
garage_repair_outbound_created | 新建出库 | sid + saInvoiceBillNo |
garage_repair_outbound_updated | 编辑出库 | sid + saInvoiceBillNo |
garage_repair_outbound_cancelled | 撤销出库 | stationId + outBoundNo |
18.2 旧 APP/SAAS 事件
| 方法 | 用途 |
|---|---|
sendSaOrderCloseToAPP | 销售单关闭 |
sendSaOrderCutOffToAPP | 销售单结单 |
sendApplyReturnFinishToAPP | 退货入库完成 |
sendApplyReturnCheckedToAPP | 退货审核通过 |
sendSaOrderOutToMq | 销售出库通知 |
sendSaOrderStatusToMq | 配送状态变化 |
sendSaOrderStatusToSass | 销售创建/出库状态给 SAAS |
sendOutBoundToSaas | 出库同步 SAAS |
sendInvoiceToSass | 微仓/销售出库同步 |
18.3 消息幂等风险
SyncOrderSer 和多个 MqSer 生产方法用 uniqid() 生成消息 ID。每次补发都会得到新 ID,消费者不能只按消息 ID 去重,必须按订单号/出库单号等业务键幂等。
flowchart LR
DB["本地销售/出库"] --> P["Producer"]
P -->|"messageId=uniqid()"| Q["dgj_notify"]
Q --> C["APP/SAAS消费者"]
C --> IDEM{"业务键已处理?"}
IDEM -->|是| ACK["幂等ACK"]
IDEM -->|否| WRITE["落下游订单/出库"]
WRITE --> ACK
18.4 同步消息字段风险
buildOrderEvent()新建/编辑都把订单状态固定构造为待出库,需要下游理解事件语义。- 普通销售创建的
orderType固定为 7;取消则通过来源映射取下游类型。 - 出库消息依赖
saleOrId/saleOrNo/billNo,编辑页面不一定原样带回,入口已经有补齐逻辑。 qtyGrid中多货位时,Builder 当前循环会保留最后一个非零货位作为消息字段;实际出库明细仍可能有多货位,需确认下游合同。goodsOutPrice字段当前填入行amount,命名像单价但实际是金额,前后端和下游不能望文生义。
19. 下游未收到的排查 SOP
flowchart TD
A["下游未收到"] --> B{"本地业务已提交?"}
B -->|否| B1["先查本地事务错误"]
B -->|是| C{"该sourceType应同步?"}
C -->|否| C1["例如快维来源可能跳过"]
C -->|是| D{"生产日志存在?"}
D -->|否| D1["代码分支未走或生产异常"]
D -->|是| E{"队列有消息/积压?"}
E -->|是| E1["查消费者、重试、死信"]
E -->|否| F{"下游消费日志存在?"}
F -->|否| F1["binding/routing key配置"]
F -->|是| G{"业务键幂等拒绝?"}
G -->|是| G1["核对旧数据和版本"]
G -->|否| H["查下游事务和字段合同"]
19.1 日志关键字
rg -n "SyncOrder发送消息|garage_repair_order_|garage_repair_outbound_" application
rg -n "sendSaOrderOutToMq|sendSaOrderStatusToMq|sendInvoiceToSass|sendOutBoundToSaas" application
rg -n "saleOrId|saleOrNo|saInvoiceBillNo|outBoundNo" application/Services/SyncOrder application/service/scm/InvSaService.php
19.2 补发安全门槛
补发前逐项确认:
- 本地销售/出库事实已经提交且不会回滚。
- 下游是否已有同业务键数据。
- routing key 与当前消费者所有权一致。
- 事件应是 create、update、cancel 还是 stop。
- payload 的站点、订单号、出库单号和数量与当前事实一致。
- 消费者支持按业务键幂等。
- 单次、单号、可追踪;禁止无条件整批重放。
20. 一单到底只读 SQL 包
下面模板中的表后缀必须先计算;字段以目标环境 DDL 为准。先在只读连接执行。
20.1 销售主单和明细
select *
from t_scm_sa_order_:so_suffix
where sid = :sid
and (id = :sale_order_id or billNo = :sale_bill_no
or srcOrderId = :src_order_id or srcOrderNo = :src_order_no)
order by id;
select *
from t_scm_sa_order_info_:so_suffix
where sid = :sid and iid = :sale_order_id
order by id;
20.2 有效出库和销退
select id, billNo, transType, billStatus, sourceType, srcOrderId,
srcOrderNo, totalQty, amount, hxStateCode, createTime, cancelTime
from t_scm_sa_invoice_0_:out_suffix
where sid = :sid and srcOrderId = :sale_order_id and isDelete = 0
order by createTime, id;
select i.iid, i.id, i.srcOrderEntryId, i.invId, i.skuId,
i.transType, i.qty, i.amount, i.locationId, i.locationAreaId,
m.billNo, m.billStatus
from t_scm_sa_invoice_info_0_:out_suffix i
join t_scm_sa_invoice_0_:out_suffix m on m.id = i.iid
where i.sid = :sid and m.srcOrderId = :sale_order_id
order by i.srcOrderEntryId, m.createTime, i.id;
20.3 孤儿关系
-- 出库主单找不到销售主单
select o.id, o.billNo, o.srcOrderId, o.srcOrderNo
from t_scm_sa_invoice_0_:out_suffix o
left join t_scm_sa_order_:so_suffix s
on s.id = o.srcOrderId and s.sid = o.sid
where o.sid = :sid and o.isDelete = 0 and s.id is null;
-- 出库明细找不到销售明细
select i.id, i.iid, i.srcOrderEntryId, i.invId, i.qty
from t_scm_sa_invoice_info_0_:out_suffix i
left join t_scm_sa_order_info_:so_suffix s
on s.id = i.srcOrderEntryId and s.sid = i.sid
where i.sid = :sid and i.isDelete = 0 and s.id is null;
20.4 重复出库候选
select srcOrderEntryId, invId, locationId, locationAreaId,
count(*) as row_count, sum(abs(qty)) as total_out_qty
from t_scm_sa_invoice_info_0_:out_suffix i
join t_scm_sa_invoice_0_:out_suffix m on m.id = i.iid
where i.sid = :sid
and m.transType = 150601
and m.billStatus < 6
and m.isDelete = 0
and i.isDelete = 0
group by srcOrderEntryId, invId, locationId, locationAreaId
having count(*) > 1;
多次部分出库本身合法,因此该 SQL 只产生候选,必须与订单数量、操作时间和幂等键继续核对。
21. 按现象执行的分支 SOP
21.1 销售单不能出库
- 固定
sid,计算 16/64 后缀。 - 查销售主单:状态必须是 0 或 1,且未删除。
- 查销售明细和
srcOrderEntryId。 - 查站点黑名单、系统锁定和权限。
- 查货位是否存在、实时库存和锁定量。
- 查已经有效出库的数量,计算剩余可出。
- 查应用日志中的具体异常。
- 若接口报错,再查是否已经产生出库单、库存和 TMS。
21.2 出库后销售状态不对
- 查所有 150601 出库主单,排除状态 6 和软删。
- 按
srcOrderEntryId汇总abs(qty)。 - 查结单量口径。
- 与订单明细
qty逐行比较。 - 查状态回写日志和
getOutboundQty()是否执行。 - 先修数量/关联事实,再重算状态;不要先改主单状态。
21.3 出库单有记录但库存没扣
- 确认出库明细是负数量。
- 按单号和 150601 查库存流水。
- 按站点、商品、仓库、货位查实时库存。
- 查数据库连接和事务是否一致。
- 查是否只有主明细落库、库存步骤失败。
- 修复时用已有领域库存接口,不直接改单个实时库存值。
21.4 库存扣了但出库单没有
- 查请求时间附近库存流水的来源单号。
- 查出库分片是否算错。
- 查软删/撤销出库。
- 查事务是否跨连接导致库存先提交。
- 查重试是否生成新出库号。
- 保留流水,评审反向业务单或补齐业务单,禁止直接删除流水。
21.5 配送状态不推进
- 查出库状态是否 1。
- 查配送关系和三方配送主单。
- 查外部 TMS 实际状态。
- 查回调是否到达、验签和幂等结果。
- 查映射后是否更新出库状态 2/3。
- 查 APP 状态 MQ 是否发送。
21.6 收款后未核销
- 查出库
amount/hxStateCode/billStatus。 - 查 32 分片 PaymentInfo。
- 排除软删、撤销、退款记录。
- 计算
payAmount + diffAmount。 - 检查金额精度和币种/单位。
- 查对账/收款状态回写日志。
21.7 销退后库存未恢复
- 查 150602 销退主明细是否提交。
- 确认明细数量为正,仓库货位正确。
- 查库存入库流水和实时库存。
- 查退款和 APP 完成通知。
- 查原出库可退量是否被正确占用/释放。
22. 修复决策树
flowchart TD
A["发现销售链不一致"] --> B["冻结单号并保存证据"]
B --> C{"哪一层是真实业务事实?"}
C --> D["订单"]
C --> E["出库/销退"]
C --> F["库存"]
C --> G["配送"]
C --> H["资金"]
C --> I["下游"]
D --> J["建立期望状态和数量"]
E --> J
F --> J
G --> J
H --> J
I --> J
J --> K{"现有领域补偿接口可用?"}
K -->|是| L["单号级幂等补偿"]
K -->|否| M["评审修复脚本和回滚方案"]
L --> N["六层验收"]
M --> N
22.1 修复优先级
- 使用原业务重试/补偿接口。
- 使用已有领域 Service 创建反向业务事实。
- 单号级可审计脚本,带前置断言和影响行数。
- 最后才考虑直接 SQL,且必须有备份、复核和回滚。
22.2 禁止操作
- 只改销售主单
billStatus。 - 直接把出库单改成 6 而不恢复库存。
- 删除库存流水以“让报表好看”。
- 未检查下游时重复发 create 事件。
- 按消息 ID 判断业务是否已处理。
- 不算分片就跨全库更新同名表。
23. 回归测试矩阵
23.1 销售订单
- [ ] 普通挂账销售创建、编辑、关闭。
- [ ] 现金/微信销售创建和收款状态。
- [ ] E站商城、维修、活动、机器人和新 APP 来源。
- [ ] 快维来源不应发送的下游事件保持不发送。
- [ ] 0 元赠品、折扣、优惠和多 VIN。
- [ ] 旧来源类型历史单查询和关闭不报映射错误。
23.2 出库和库存
- [ ] 一次全部出库,订单 0 -> 2。
- [ ] 多次部分出库,订单 0 -> 1 -> 2。
- [ ] 多仓库、多货位同一商品出库。
- [ ] 锁定量不足、货位删除、站点锁定。
- [ ] 出库明细负数量,库存流水和实时库存同步减少。
- [ ] 客户端超时后重试不产生重复出库。
- [ ] 事务异常时业务表和库存共同回滚。
23.3 配送
- [ ] AI 工作台关闭时不创建 TMS。
- [ ] 支持来源创建和更新配送单。
- [ ] 配送中、送达、自提、取消状态映射。
- [ ] 出库撤销同步取消配送。
- [ ] 配送回调重复投递幂等。
23.4 资金
- [ ] 未收、部分收、全收。
- [ ]
amount = pay + diff自动核销。 - [ ] 零金额出库核销。
- [ ] 对账、撤销对账和已有收款阻断。
- [ ] 销退退款、重复退款和撤销。
23.5 下游同步
- [ ] order created/updated/cancelled/stopped。
- [ ] outbound created/updated/cancelled。
- [ ] 消费者按业务单号幂等,而非随机消息 ID。
- [ ] MQ 发送异常时入口行为符合预期。
- [ ] MQ 成功后本地回滚场景有对账补偿。
- [ ] APP、SAAS 的订单号和出库单号映射正确。
24. 监控与告警建议
| 指标 | 维度 | 建议告警 |
|---|---|---|
| 销售单待出库时长 | sid/sourceType | 超过业务 SLA |
| 部分出库停留时长 | sid/sourceType | 长时间无后续出库/结单 |
| 出库事务失败数 | 接口/异常类型 | 突增 |
| 负库存拦截数 | sid/invId | 突增或集中 SKU |
| 出库无库存流水 | sid/billNo | 任意新增 |
| 库存流水无出库单 | sid/srcBillNo | 任意新增 |
| TMS 创建失败 | sid/来源 | 连续失败 |
| 配送回调延迟 | 配送商/状态 | 超过 SLA |
| 已对账未核销金额 | sid/账龄 | 超过阈值 |
| dgj_notify 生产失败 | routing key | 任意持续失败 |
| 下游消费积压 | routing key | 队列深度/最老消息超阈值 |
25. 已确认与待确认
25.1 静态代码已确认
- 销售主单和明细按
sid % 16。 - 销售出库主单和明细按
_0_{sid % 64}。 - 销售和出库两个 Ext Model 当前直接使用不分片常量表。
- 销售订单状态为 0/1/2/3。
- 出库状态为历史 0,以及 1/2/3/4/5/6。
- 销售出库交易类型为 150601,销退为 150602。
- 普通销售出库明细数量写成负数。
- 出库入口校验 PC/App 权限、销售黑名单、系统锁定、货位和锁定量。
- 出库事务内会处理 TMS,并在提交前发送新的下游出库事件。
- 快维来源在新出库同步逻辑中被排除。
- 新下游事件目标为
dgj_notify,消息 ID 使用uniqid()。 - 关单要求没有有效出库;结单用于已有部分出库后停止。
- 对账时金额完全覆盖会直接到状态 5,否则到状态 4。
25.2 环境待确认
- 生产 DDL、唯一索引和扩展表实际分片/归档策略。
- 当前 PC、App、PDA 各入口真实流量占比。
- TMS 服务的幂等键、超时、重试和取消 SLA。
dgj_notify的 exchange、binding、prefetch、重试和死信配置。- 下游消费者对七类销售事件的幂等实现。
- 历史主表与 16/64 分表是否仍有双写或迁移遗留。
- 库存流水与实时库存的线上分片和唯一键。
- PaymentInfo 的线上分片、有效状态和金额精度。
closeQty的当前生产聚合实现及历史数据完整性。- 事务内远程调用成功、本地提交失败后的现有补偿任务。
26. 证据来源
| 主题 | 文件 |
|---|---|
| 表常量和分片数 | application/config/tables.php |
| 销售状态和来源 | application/KzData/Enums/SaOrderEnums.php |
| 出库状态 | application/KzData/Enums/SaInvoiceEnums.php |
| 交易类型 | application/KzData/Enums/TransTypeEnums.php |
| 销售主单 Model | application/models/saOrders/SaOrderModel.php |
| 销售明细 Model | application/models/saOrders/SaOrderInfoModel.php |
| 销售扩展 | application/models/saOrders/SaOrderInfoExtModel.php |
| 出库主单 Model | application/models/saOrders/SaInvoiceModel.php |
| 出库明细 Model | application/models/saOrders/SaInvoiceInfoModel.php |
| 出库扩展 | application/models/saOrders/SaInvoiceInfoExtModel.php |
| 老销售编排 | application/service/scm/InvSaService.php |
| 普通销售出库 | application/Services/InvSa/NormalSaleSer.php |
| 普通销退 | application/Services/InvSa/NormalSaleReturnSer.php |
| 新销售领域 | application/Services/SaOrders/SaOrderSer.php |
| 出库领域 | application/Services/SaOrders/SaInvoiceSer.php |
| 三方配送 | application/Services/SaOrders/ThirdDeliverySer.php |
| 下游构建 | application/Services/SyncOrder/SyncOrderBuilder.php |
| 下游生产 | application/Services/SyncOrder/SyncOrderSer.php |
| MQ 事件 | application/Services/Mq/MqSer.php |
| 收款核销 | application/service/scm/ReceiptService.php |
27. 一页式执行结论
先拿 sid,算 sid%16 和 sid%64;销售表与出库表不是同一种后缀。
先查销售主单和明细,再按 srcOrderId/srcOrderEntryId 找出库主明细。
销售订单量为正;有效销售出库明细通常为负;销退通常为正。
状态异常先重算有效出库量和结单量,不先改 billStatus。
出库失败后必须同时查本地出库、库存、TMS 和 MQ,接口报错不代表全部失败。
配送、对账、核销、APP/SAAS 各有独立状态,不能互相替代。
补发 MQ 按销售单号或出库单号幂等,随机 messageId 不能证明未处理。
任何修复最后验收订单、出库、库存、配送、资金、下游和报表。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| PC 销售 | scm/InvSa/sale/OutOrder | sid、客户、SKU、数量、价格、动作 | InvSa.php/OutOrder.php | InvSaService、SaOrderSer | 销售单号 | 销售订单与应收事实 |
| App/PDA/SAAS | appapis.php/老 apis.php | 来源单、销售/出库参数 | App/PDA/OpenAPI | Pda/SaleSer、领域 Service | 来源单号 + 销售/出库单 | 共用销售出库库存链 |
| 配送回调 | 配送中心 MQ | 出库单、配送状态、消息 ID | DispatchCenterNotify | ThirdDeliverySer | 出库/配送单号 | 配送状态和签收结果 |
| 收款/退货 | 财务入口、售后申请 | 销售单、金额;原出库、退货量 | Receipt/退货 Controller/Consumer | ReceiptService、NormalSaleReturnSer | 销售/支付/退货单号 | 核销应收或反向入库退款 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 下单 | 请求日志、SaOrderSer | request_id、来源单、销售单、sid | 主明细 commit | 来源重复、价格/客户校验失败 | 来源单映射销售单 | | 出库 | SaInvoiceSer/InventorySer | 销售单、出库单、SKU、150601 | 出库量和库存扣减一致 | 超出可出量、负库存限制、部分成功 | 出库单查发票明细与库存流水 | | 配送 | DispatchCenterNotify | 配送单、出库单、event、message ID | 状态单向推进且 ACK | 乱序、重复、关系缺失 | 两种单号查配送关系 | | 收退 | ReceiptService/退货 Service | 支付/退货单、原销售单、金额/数量 | 核销或退货库存完成 | 超退、重复核销/退款、金额不平 | 原单贯穿资金与库存 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 创建订单 | InvSaService/SaOrderSer | 销售事务 | 客户、商品、来源唯一性 | SCM_SA_ORDER* | insert;应收和订购量写入;状态初始化 | 主明细金额数量合计 |
| 生成出库 | SaInvoiceSer | 出库事务 | 可出量、订单状态、仓位 | SCM_SA_INVOICE* | 出库量 old -> old+n;状态待/部分/完成 | 销售与出库明细数量等式 |
| 扣库存 | InventorySer | 应与出库同业务边界 | 实时库存和限制配置 | SCM_INVENTORY*、实时表 | qty: old -> old-n,流水 150601 insert | 流水净额、实时量 |
| 配送收款 | ThirdDeliverySer、ReceiptService | 各自事务/异步边界 | 出库状态、应收余额 | 配送关系、账户/支付表 | 配送态推进;已收 +n、应收余额 -n | 配送回执、资金分录 |
| 退货修复 | NormalSaleReturnSer、补偿任务 | 退货与库存边界需确认 | 已出、已退、已退金额 | 退货单、库存、退款/MQ | 退货量 +n;库存 +n;退款一次 | 原出库、退货、库存、支付闭环 |
子模块追踪:sale-create-check 销售创建排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建订单 | PC/App/OpenAPI 提交销售 | request ID、sid、来源单号、客户、SKU | application/controllers/scm/InvSa.php -> application/Services/SaOrders/SaOrderSer.php | 来源唯一性、客户、商品、价格、税额规则 | 销售本地事务 insert 主表、明细、扩展,状态初始化 none -> created | request ID + sourceOrderNo + sale billNo | 参数/来源重复零写入或返回既有关系;超时按来源单回查 |
| 创建回查 | 响应后核对订单金额数量 | 销售单、entryId、来源单 | application/models/saOrders/SaOrderModel.php | 主明细、扩展、应收和来源关系 | 查询只读 不写;主明细数量金额应守恒 | sale billNo + sourceOrderNo + affected rows | 主单无明细按部分失败处理,使用领域撤销/重建,不手补明细 |
子模块追踪:sale-out-check 销售出库排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 出库提交 | PC/PDA 创建销售出库 | sid、销售单、SKU、仓库货位、数量 | application/controllers/sale/OutOrder.php -> application/Services/SaOrders/SaInvoiceSer.php | 可出量、订单状态、库存维度、负库存配置 | 出库本地事务 insert SCM_SA_INVOICE*,有效出库量 old -> old+n | request ID + sale/invoice billNo + SKU | 超出可出量或状态不允许时零写入;网络重试前查已有出库行 |
| 状态回算 | 出库后销售状态不正确 | 销售单、出库单、entryId | application/service/scm/InvSaService.php | 有效未软删出库量、结单量、订单量 | 回算事务仅在等式满足时 billStatus: old -> partial/finish | 两种单号 + qty summary | 软删/撤销行必须排除;先修过程事实再重算状态 |
子模块追踪:sale-stock-check 出库与库存一致性排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 扣减库存 | 销售出库事务调用库存服务 | 出库单、SKU/invId、仓库货位、数量 | application/Services/SaOrders/SaInvoiceSer.php -> application/Services/Storage/InventorySer.php | 实时四维库存、库存策略和出库明细 | 应在本地业务事务写 150601 流水,实时 qty: old -> old-n | invoice billNo + SKU + transType 150601 | 库存失败应回滚出库;若只一侧成功,先固定事务事实再补偿 |
| 一致性核对 | 出库单与库存不一致 | 出库 entryId、库存业务单 | application/models/saOrders/SaInvoiceInfoModel.php | 有效出库量、流水净额、实时前后值 | 查询只读 不写;同一 SKU/仓位数量方向应一致 | invoice/inventory billNo + entryId | 禁止只改实时库存;走反向出库或库存领域补偿并保留流水 |
子模块追踪:sale-delivery-check 配送与 TMS 排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建配送 | 满足配送条件的销售出库 | 出库单、地址、承运方式 | application/Services/SaOrders/ThirdDeliverySer.php | 出库状态、已有配送关系、地址 | 本地事务写配送关系 none -> pending;TMS 调用为事务外边界 | request ID + invoice/deliveryNo | 外部超时先按出库单查询是否已创建,避免重复配送单 |
| 状态回调 | TMS 配送、签收、失败事件 | message ID、配送单、出库单、状态 | application/controllers/tasks/DispatchCenterNotify.php | 当前配送态和映射关系 | 回调本地事务单向 old -> in_transit/signed/failed | message ID + delivery/invoiceNo | 乱序不回退签收;关系缺失只补映射/回调,不重做出库 |
子模块追踪:sale-receipt-check 对账收款核销排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 收款制单 | PC/财务对销售单收款 | 销售单、收款单、账户、金额 | application/service/scm/ReceiptService.php | 应收、已收、已核、退款和有效单据 | 财务本地事务 insert 收款/分录,received: old -> old+n | request ID + sale/receipt billNo | 超收、账户非法零写入;重复来源键不得生成第二笔收款 |
| 核销回查 | 已收款但销售未核销 | 销售/收款/支付单、金额 | application/service/scm/InvSaService.php | 有效收款过滤、核销关系和金额单位 | 核销事务 verified: old -> old+n;报表查询只读 | 三种单号 + transType + amount | 分录正确但状态旧时只重算核销;金额不平先查软删/退款 |
子模块追踪:sale-close-check 关单、结单与撤销出库排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 关闭结单 | 用户关闭未履约量或系统结单 | 销售单、action、关闭原因 | application/service/scm/InvSaService.php | 订单量、有效出库、退货、已关量、资金状态 | 关闭本地事务 closeQty +n、billStatus: old -> closed/finished | request ID + sale billNo + action | 已出库部分不能直接关闭;并发出库后重新计算剩余可关量 |
| 下游同步 | 关结单后通知 App/SAAS | 销售单、最终状态、event | application/Services/SyncOrder/SyncOrderSer.php | 本地已提交终态和上次发送事实 | commit 后异步 MQ,DB 主状态不变 | sale billNo + event + routing key | 发布失败仅补消息;下游未收到不重复执行关结单 |
子模块追踪:sale-return-check 销售退货排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建销退 | 原销售/出库单发起退货 | 原单、销退单、SKU、退货量 | application/Services/InvSa/NormalSaleReturnSer.php | 原有效出库量、已退量、可退金额、库存维度 | 销退与库存本地事务写退货单,退货量 old -> old+n、库存 qty +n | request ID + original/return billNo + SKU | 超退零写入;若库存已回但退货单失败,按流水事实走领域补偿 |
| 退款回查 | 销退完成后退款/核销反向 | 销退单、原收款/支付单 | application/service/scm/ReceiptService.php | 原已收已核额、已退额、销退终态 | 反向财务事务 refunded +n、核销额按规则减少 | return/original/payment billNo | 库存完成不等于退款完成;库存和资金分别验收、分别幂等补偿 |
子模块追踪:sale-mq-check 销售下游 MQ 排查
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 构造发布 | 销售创建、出库、撤销、退货 commit 后 | 销售/出库单、event、来源单 | application/Services/SyncOrder/SyncOrderBuilder.php -> application/Services/Mq/MqSer.php | 已提交业务快照、来源和目标类型 | 本地核心 DB 不变;事务外发布新旧 App/SAAS 事件 | billNo + event/routing key + message ID | 序列化/发布失败只补发消息,不重做订单和库存 |
| 下游未到 | App/SAAS 查询不到已完成业务 | 两端单号、发送时间窗 | application/Services/SyncOrder/SyncOrderSer.php | 本地主明细、已发送事件和下游回执 | 查询只读 不写;补发沿用稳定业务键 | local/source billNo + publish/consume logs | 随机 messageId 不证明幂等;目标环境消费日志需实际确认 |