1. 业务目标与边界
销售域负责服务站向维修厂、E站用户、移动商城客户、APP/机器人客户和外部 ERP 客户销售商品。完整链路不是一张“销售表”,而是三类业务事实:
- 销售订单:客户要什么、数量和价格是多少,尚未必然扣库存。
- 销售出库单:实际从哪个仓库/货位出了多少货,是扣库存和履约的事实。
- 销售退货单:客户退回了什么,是库存增加和财务冲回的反向事实。
出库之后还会进入配送、自提、送达、对账、核销、撤销和下游同步。本文把这些状态、表、接口和数据变化分开说明。
2. 最容易混淆的入口
当前 InvSa.php 中的方法名带有明显历史包袱:
| 方法 | 实际业务 | 不要误解为 |
|---|---|---|
addInvSaOr() | 新增或修改销售订单 | 销售退货 |
addOutBound() | 从销售订单生成销售出库单 | 只改销售订单状态 |
add() | 新增销售退货单,使用 NormalSaleReturn Factory | 普通销售开单 |
updateInvSa() | 修改销售退货单 | 修改销售订单 |
cancel() | 撤销销售出库单并生成反向业务 | 关闭未出库销售订单 |
closeSaleOr() | 关闭/结单销售订单 | 撤销已出库单 |
阅读代码或排查时,必须先确认当前单号属于 t_scm_sa_order、t_scm_sa_invoice 还是销售退货交易,再进入对应方法。
3. 参与角色和前置条件
| 角色/系统 | 主要动作 | 前置条件 |
|---|---|---|
| 服务站销售员 | 开销售订单、选客户、定价、出库 | 有销售/出库权限,客户和物料有效 |
| 仓库人员 | 选择仓库货位、确认出库或销退入库 | 实时库存充足,货位未删除 |
| 客户/维修厂 | 收货、自提、挂账、付款、退货 | 联系人和地址有效,支付方式可用 |
| 配送中心/TMS | 配送建单、出库、轨迹、送达、自提 | 出库单和配送关系完整 |
| 财务/对账人员 | 收款、对账、撤销对账、核销 | 账户、结算方式、金额和单据状态有效 |
| SAAS/E站/APP/机器人 | 创建来源订单、接收出库和状态 | sourceType、来源单号和同步事件正确 |
| 库存域 | 销售出库减库存、销退入库加库存 | 交易类型、仓库、货位、数量正确 |
4. 代码入口地图
| 层级 | 文件/配置 | 主要职责 |
|---|---|---|
| PC Controller | application/controllers/scm/InvSa.php | 销售订单、出库、销退、对账、撤销 |
| 新出库 Controller | application/controllers/sale/OutOrder.php | 新销售出库入口 |
| 报价 Controller | application/controllers/sale/Offer.php、RapidOffer.php | 报价和快速报价 |
| App code | application/config/appapis.php 的 appsale* | App 销售、出库、状态更新 |
| 销售订单 Service | application/Services/SaOrders/SaOrderSer.php | 创建订单、来源映射、出库数量推进 |
| 出库单 Service | application/Services/SaOrders/SaInvoiceSer.php | 出库单查询、详情和状态 |
| 老销售 Service | application/service/scm/InvSaService.php | 页面列表、出库、对账、销退、导出 |
| 配送 Service | TransportSer.php、ThirdDeliverySer.php | 配送、自提、三方配送 |
| 下游同步 | application/Services/SyncOrder/SyncOrderSer.php | 销售创建/更新、出库创建/撤销同步 |
| MQ 发送 | application/Services/Mq/MqSer.php | 出库、状态、SAAS 事件 |
| 配送回调 | application/controllers/tasks/DispatchCenterNotify.php | 配送出库、物流、自提、退货关闭 |
| 库存 | application/Services/Storage/InventorySer.php | 出库/退货库存流水和实时库存 |
| 枚举 | application/KzData/Enums/SaOrderEnums.php | 订单、出库、来源、支付和收款状态 |
5. PC 接口总表
5.1 销售订单
| 接口方法 | 核心请求 | 权限/锁 | 关键调用 | 数据副作用 |
|---|---|---|---|---|
addInvSaOr | postData,客户、商品、价格、来源 | 新增/修改权限;按 sid+buId Redis 锁 3 秒 | salesOrValidform -> InvSaService::addInvSaOr | 新增/更新销售主单和明细 |
getSalesOrderlist | 筛选、分页 | SAOR_QUERY | SaOrderSer::list | 只读 |
getSalesOrderListNew | 筛选、分页 | SAOR_QUERY | 新服务 getSaOrderList | 只读,数据源不同 |
salesOrUpdate | 销售订单 ID | SAOR_UPDATE | InvSaService::salesOrUpdate | 读取编辑数据 |
closeSaleOr | ID、type、关闭原因/明细 | SAOR_CLOSE(部分分支) | InvSaService::closeSaleOr | 待出库量关闭,订单转部分/完成/关闭 |
SaleOrDetail | 销售订单 ID | SAOR_QUERY | InvSaService::SaleOrDetail | 只读 |
descUpdate | ID、type=order/invoice、备注 | 订单/出库备注权限 | InvSaService::descUpdate | 更新备注 |
5.2 销售出库和履约
| 接口方法 | 核心请求 | 权限/锁 | 关键调用 | 数据副作用 |
|---|---|---|---|---|
addOutBound | postData,销售单 ID、出库明细、仓库货位 | SA_OUT_ADD;按 sid+销售单ID 锁 3 秒 | InvSaService::addOutBound | 新增出库主明细、扣库存、同步下游 |
outBoundList | 筛选、分页 | SA_OUT_QUERY | 老 InvSaService::outBoundList | 只读 |
outboundListNew | 筛选、分页 | SA_OUT_QUERY | 新 InvSaSer::outboundList | 只读,可能与老口径不同 |
outBoundDetails | 出库单 ID | SA_OUT_QUERY | SaInvoiceSer::outBound | 只读 |
cancel | 出库单 ID、原因 | SA_OUT_RECONCILIATION;按 sid+id 锁 | InvSaFactory('NormalSale')->deleteInvoice | 撤销出库、反向库存、删配送、通知下游 |
reconciliation | 出库单 ID、对账信息 | SA_OUT_CANCEL | InvSaService::reconciliation | 出库单转已对账/核销 |
cancelReconciliation | 出库单 ID | 撤销对账权限 | InvSaService::cancelReconciliation | 回退对账状态 |
batchReconciliation | 逗号分隔 ids | 页面校验 | 逐批对账 | 批量更新状态 |
5.3 销售退货
| 接口方法 | 核心请求 | 核心调用 | 数据副作用 |
|---|---|---|---|
add | postData,原出库/客户/退货商品/货位/结算 | InvSaFactory('NormalSaleReturn')->addInvoice | 新增销退单、退货入库、登记待对账客户 |
update/returnupdate | 销退单 ID | InvSaService 查询编辑数据 | 只读 |
updateInvSa | postData | NormalSaleReturn::updateInvoice | 更新可编辑销退单 |
6. App 接口 code
| code | 映射 | 用途 |
|---|---|---|
appsaleOrderAdd | app.sale.saleOrderAdd | 新增销售单 |
appsaleOrderList | app.sale.saleOrderList | 销售列表 |
appsaleOrderlistDetail | app.sale.saleOrderlistDetail | 销售出库/退货详情 |
appsaleOrderListDetailSingle | app.sale.saleOrderListDetailSingle | 销售订单详情 |
appsaleOrderDetailOption | app.sale.saleOrderDetailOption | 获取修改选项 |
appsaleOrderlistDelete | app.sale.saleOrderlistDelete | 删除销售订单 |
appsaleOutList | app.sale.out.saleList | 出库列表 |
appsaleOutDetail | app.sale.out.saleDetail | 出库详情 |
appsaleOutStatUp | app.sale.out.saleStatUp | 配送/履约状态更新 |
appsaleOutStockOut | app.sale.out.saleStockOut | 销售出库 |
appsale2CanOut | app.sale2.SaleCanOut | v2 出库前可操作校验 |
appsale2StockOut | app.sale2.SaleStockOut | v2 销售出库 |
appsale2SaleAreaQty | app.sale2.SaleAreaQty | v2 选择货位和库存数量 |
App code 只表示协议映射。鉴权、签名、网关 URL 和统一 request envelope 需在 API 字典中按环境补齐。
7. 销售订单请求与主流程
7.1 请求结构示例
{
"postData": "{\"buId\":20001,\"billNo\":\"SO-EXAMPLE\",\"sourceType\":0,\"payType\":0,\"description\":\"门店销售\",\"entries\":[{\"invId\":10001,\"skuId\":\"SKU-001\",\"qty\":4,\"price\":199.00}]}"
}
示例只表达业务结构,具体字段和金额单位必须以当前页面 Network、InvSaService 校验和模型为准。服务端会从会话加入 sid/uid/userName,不应接受前端跨服务站指定。
核心字段:
| 字段 | 含义 | 风险点 |
|---|---|---|
buId | 客户/往来单位 ID | Redis 锁以客户维度限制短时重复操作 |
entries | 商品、数量、价格等明细 | 同一 SKU 不应重复;出库时还需仓库货位 |
sourceType/场景字段 | 来源业务 | 实际常由 saOrderMainFields() 根据场景映射 |
payType | 挂账/现金/微信 | 与来源、是否已支付、收款状态联动 |
billNo/来源单号 | 业务单号 | 外部来源必须保持唯一和可追踪 |
7.2 创建流程
flowchart TD
A[PC/APP/E站/SAAS/机器人发起销售] --> B[识别 order_scene/sourceType]
B --> C[校验登录服务站 客户 商品 数量 价格 支付方式]
C --> D{重复 SKU/客户/规则是否通过}
D -- 否 --> X[返回校验错误]
D -- 是 --> E[saOrderMainFields 映射来源和支付口径]
E --> F[写销售订单主表]
F --> G[写销售订单明细和扩展快照]
G --> H{是否立即出库}
H -- 否 --> I[销售单状态=待出库]
H -- 是 --> J[进入出库流程]
I --> K[SyncOrderSer 同步销售创建/更新]
J --> K
SaOrderSer::saOrderMainFields() 是来源理解的关键:它把 E站商城、维修、活动、E站 APP IM/商城/活动和机器人等场景映射为 DGJ2 的 sourceType/payType/is_pay。不要由页面展示名称反推来源值。
8. 销售来源和支付口径
sourceType | 来源 | 支付差异 |
|---|---|---|
| 0 | 普通销售开单 | 表单选择挂账/现金等 |
| 1 | ERP/快维 SAAS | 下游同步和撤销规则特殊 |
| 8 | E站商城 | 挂账 |
| 9 | E站商城 | 微信支付 |
| 10 | E站维修马上送 | 挂账 |
| 15 | E站维修马上送 | 微信支付 |
| 14 | E站活动 | 按活动场景 |
| 16 | AI 助手 | 挂账 |
| 17/18 | E站 APP IM | 挂账/微信 |
| 19/20 | E站 APP 商城 | 挂账/微信 |
| 21/22 | E站 APP 活动 | 挂账/微信 |
历史保留值 2、5、6、7、11、12、13 仍可能存在数据库中。新增逻辑不要删除旧枚举,否则列表、报表和消息反序列化会失去解释能力。
9. 销售出库请求与流程
9.1 请求结构示例
{
"postData": "{\"id\":30001,\"buId\":20001,\"billNo_type\":0,\"entries\":[{\"saOrderInfoId\":40001,\"invId\":10001,\"skuId\":\"SKU-001\",\"qty\":4,\"locationId\":101,\"locationAreaId\":1001}]}"
}
出库的真实依据不是销售订单页面数量,而是每行剩余可出数量和当前货位实时库存。Controller 还会:
- 对微仓铺货校验客户已绑定微仓、仓库是可用移动仓。
- 判断销售单是否已有 SAAS 关联,决定
enableSync。 - 从报价关系补充每个商品的
saOfferBillNo。 - 使用
sid + 销售订单IDRedis 锁降低重复出库。
9.2 主流程
flowchart TD
A[选择待出库销售单] --> B[读取每行销售量 已出库量 已关闭量]
B --> C[选择仓库 货位 出库数量]
C --> D[校验货位有效和实时库存]
D --> E{数量是否超过剩余可出或库存}
E -- 是 --> X[拒绝出库]
E -- 否 --> F[Redis 锁销售订单]
F --> G[事务新增销售出库主表]
G --> H[新增出库明细和快照]
H --> I[InventorySer 写销售出库流水]
I --> J[扣减货位和实时库存]
J --> K[更新销售订单明细已出库量]
K --> L{订单剩余数量}
L -- 全部出库 --> M[销售订单=已完成]
L -- 仍有未出 --> N[销售订单=部分出库]
M --> O[创建配送/自提并同步下游]
N --> O
10. 出库系统时序
sequenceDiagram
participant U as 服务站
participant C as InvSa Controller
participant S as InvSaService/SaOrderSer
participant DB as 销售数据库
participant INV as InventorySer
participant MQ as dgj_notify/MQ
participant D as 配送中心/TMS
participant DOWN as SAAS/E站/APP
U->>C: addOutBound(postData)
C->>S: addOutBound(登录上下文+enableSync)
S->>DB: 锁/读取销售单和剩余数量
S->>INV: 保存销售出库库存事务
INV->>DB: 新增 inventory/info 并扣实时库存
S->>DB: 新增 sa_invoice/info 更新 sa_order
S->>D: 创建配送或自提关系
S->>MQ: syncOutboundCreate / sendSaOrderOutToMq
MQ-->>DOWN: dgj_notify 出库创建事件
S-->>U: 出库单 ID/单号和状态
D-->>C: 配送出库/物流/自提回调
C->>DB: 更新出库履约状态
C->>MQ: 同步状态到下游
11. 两套状态机
11.1 销售订单状态
| 值 | 常量 | 含义 | 关注数量 |
|---|---|---|---|
| 0 | SA_STATUS_WAIT | 待出库 | 已出库量为 0,仍有可出量 |
| 1 | SA_STATUS_OUT_FEW | 部分出库 | 已出库量 > 0 且仍有剩余 |
| 2 | SA_STATUS_OUTED | 已完成 | 可履约数量已全部出库/结单 |
| 3 | SA_STATUS_CLOSE | 已关闭 | 剩余未履约量被关闭 |
stateDiagram-v2
[*] --> 待出库: 创建销售订单
待出库 --> 部分出库: 部分数量出库
待出库 --> 已完成: 一次全部出库
部分出库 --> 已完成: 剩余数量全部出库
待出库 --> 已关闭: 关闭全部未出数量
部分出库 --> 已关闭: 结单/关闭剩余数量
已完成 --> [*]
已关闭 --> [*]
11.2 销售出库单状态
| 值 | 常量 | 含义 | 触发方 |
|---|---|---|---|
| 0 | OUT_BILLSTATUS_RE_OUTED | 历史已出库/销退已出库 | 历史兼容 |
| 1 | OUT_BILLSTATUS_OUTED | 已出库 | 出库成功 |
| 2 | OUT_BILLSTATUS_SENDIND | 派送中 | 配送中心/APP |
| 3 | OUT_BILLSTATUS_ARRIVE | 已送达 | 签收/送达回调 |
| 4 | OUT_BILLSTATUS_CHECK | 已对账 | 人工/批量对账 |
| 5 | OUT_BILLSTATUS_OVER | 已核销 | 收款和核销逻辑 |
| 6 | OUT_BILLSTATUS_CANCELED | 已撤销 | 出库撤销 |
stateDiagram-v2
[*] --> 已出库: 出库成功
已出库 --> 派送中: 配送接单/发车
已出库 --> 已送达: 自提确认或直接送达
派送中 --> 已送达: 配送完成
已送达 --> 已对账: reconciliation
已对账 --> 已送达: cancelReconciliation
已对账 --> 已核销: 收款核销完成
已出库 --> 已撤销: cancel 满足条件
已撤销 --> [*]
已核销 --> [*]
出库单状态不等于收款状态。已送达 可以仍未收款,已对账 也不必然全部核销。
12. 配送、自提和三方履约
DispatchCenterNotify 处理的关键回调:
| 方法 | 外部动作 | DGJ2 影响 |
|---|---|---|
orderTypeChange | 配送订单类型变更 | 更新配送/订单类型关联 |
dispatchCenterDeliveryStockOut | 配送中心确认出库 | 推进出库和配送状态 |
dispatchCenterLogisticeInfoNotify | 物流轨迹/状态 | 更新物流信息并通知页面/下游 |
dispatchCenterSelfPickUpNotify | 自提完成 | 出库单可直接推进送达 |
dispatchCenterReturnClose | 配送退回关闭 | 关闭配送关系并联动业务单 |
三方配送至少涉及:
t_scm_third_party_delivery_order:三方配送业务单。t_sa_invoice_delivery_relation:销售出库单和配送单关系。- 出库单自身履约状态。
- 外部配送单号、轨迹和回调状态。
排查“已送达但页面仍派送中”时,要先确认回调有没有命中正确的出库单关系,而不是直接改出库状态。
13. 对账、收款和核销
13.1 三个独立维度
| 维度 | 典型字段/表 | 含义 |
|---|---|---|
| 履约状态 | t_scm_sa_invoice.billStatus | 是否出库、派送、送达、对账、核销、撤销 |
| 收款状态 | hxStateCode | 0 未收、1 部分、2 全部 |
| 资金分录 | t_scm_payment、t_scm_payment_info | 实际收款单和每张业务单核销金额 |
13.2 收款方式
0挂账:形成客户应收,后续对账和收款核销。1现金:开单/出库时按业务规则形成现金收款。2微信:依赖外部支付结果或来源系统已支付口径。
13.3 对账流程
flowchart TD
A[已送达/可对账出库单] --> B[选择单据或批量 ids]
B --> C[校验出库状态 客户 金额和权限]
C --> D[reconciliation/batchReconciliation]
D --> E[出库单状态=已对账]
E --> F{是否已足额收款/满足自动核销}
F -- 否 --> G[保留未收或部分收款]
F -- 是 --> H[写/关联收款分录]
H --> I[收款状态=全部 出库单=已核销]
E --> J[cancelReconciliation]
J --> K[满足条件时回退对账状态]
对账不是简单改一个 billStatus。批量对账、自动对账和撤销对账需要核对付款金额、差额、客户、已核销记录及是否已进入不可逆财务流程。
14. 销售退货和出库撤销
14.1 销售退货
InvSa::add() 实际创建销售退货:
- 校验
SA_ADD权限。 - 解析
postData。 - 校验结算方式
wayId未禁用。 - 校验每个
locationAreaId货位未删除。 - 校验结算账户
accId未禁用。 - 从登录态注入
sid/uid/userName。 InvSaFactory('NormalSaleReturn')->addInvoice创建销退单。- 将客户登记进需要生成对账单的用户集合。
销售退货交易类型通常是 150602,库存方向为增加;普通销售出库交易类型是 150601,方向为减少。历史撤销单可能仍以销售出库类型配合 billStatus=6 展示为销售退货效果,报表必须兼容。
14.2 出库撤销
sequenceDiagram
participant U as 操作人
participant C as InvSa::cancel
participant F as NormalSale Factory
participant INV as 库存域
participant TMS as TransportSer
participant SYNC as SyncOrderSer
U->>C: cancel(id,reason)
C->>C: 权限+参数+Redis锁
C->>F: deleteInvoice(id,pageData)
F->>INV: 生成反向库存/销退事实
F-->>C: 原出库单和反向单结果
C->>TMS: transportDelete(出库单撤销)
C->>SYNC: syncOutboundCancel
C-->>U: 撤销成功和业务 ID
来源为 ORDER_SOURCE_TYPE_KW 的出库撤销不走同样的下游同步条件。已配送、已对账或已核销的单据是否允许撤销,必须由 Service 状态校验决定,不能只依赖按钮是否显示。
15. 下游同步事件
SyncOrderSer 使用 destination dgj_notify 发送销售域事件:
| 方法 | 业务时点 | 典型下游用途 |
|---|---|---|
syncOrderCreate | 销售订单创建 | SAAS/E站建立对应订单 |
syncOrderUpdate | 销售订单修改 | 同步商品、金额或备注变更 |
syncOutboundCreate | 销售出库成功 | 下游推进履约和库存 |
syncOutboundCancel | 出库撤销 | 下游回退出库/履约 |
MqSer 还提供 sendSaOrderOutToMq、sendSaOrderStatusToMq、sendSaOrderStatusToSass 等发送方法。维护时先查实际调用方和路由键,避免同一动作重复发两套消息。
16. 核心表读写矩阵
| 物理表 | 关键字段 | 业务作用 | 典型读写时机 |
|---|---|---|---|
t_scm_sa_order | id、billNo、sid、status、sourceType、客户、金额 | 销售订单主表 | 创建/修改;出库后推进部分/完成;关闭剩余量 |
t_scm_sa_order_info | 销售订单 ID、商品、数量、价格、已出库量 | 订单明细 | 创建;出库/关闭累计数量 |
t_scm_sa_order_info_ext | 明细 ID、来源/业务扩展 | 明细扩展快照 | E站、报价、活动等场景保存 |
t_scm_sa_invoice | id、billNo、sid、billStatus、来源、收款状态 | 销售出库/销退主单 | 出库、配送、对账、核销、撤销 |
t_scm_sa_invoice_info | 出库单 ID、原订单明细、商品、数量、金额、仓库货位 | 实际出库/退货明细 | 出库减库存或销退加库存 |
t_scm_sa_invoice_info_ext | 出库明细扩展 | 报价、来源和特殊场景 | 出库保存/查询 |
t_scm_abnormal_return_log | 出库/退货单、异常原因 | 异常退货审计 | 失败/异常退货记录 |
t_scm_third_party_delivery_order | 配送单号、状态、承运商 | 三方配送主单 | 建单、回调、取消 |
t_sa_invoice_delivery_relation | 出库单 ID、配送单 ID | 履约关联 | 配送创建和查询 |
t_scm_inventory | 单号、transType、来源单 | 库存业务主事实 | 销售出库/退货新增 |
t_scm_inventory_info | 商品、仓库、货位、数量、成本 | 库存流水 | 出库负向、退货正向 |
t_scm_payment | 收款单号、客户、状态、总额 | 收款主单 | 现金/微信/挂账收款、核销 |
t_scm_payment_info | 收款单、出库单、账户、金额 | 核销分录 | 对账/收款/核销 |
17. 数据关系图
erDiagram
SCM_SA_ORDER ||--|{ SCM_SA_ORDER_INFO : order_id
SCM_SA_ORDER_INFO ||--o| SCM_SA_ORDER_INFO_EXT : order_info_id
SCM_SA_ORDER ||--o{ SCM_SA_INVOICE : source_order
SCM_SA_INVOICE ||--|{ SCM_SA_INVOICE_INFO : invoice_id
SCM_SA_INVOICE_INFO ||--o| SCM_SA_INVOICE_INFO_EXT : invoice_info_id
SCM_SA_INVOICE ||--o{ DELIVERY_RELATION : invoice_id
THIRD_DELIVERY_ORDER ||--o{ DELIVERY_RELATION : delivery_id
SCM_SA_INVOICE ||--o{ SCM_PAYMENT_INFO : settlement_order
SCM_PAYMENT ||--|{ SCM_PAYMENT_INFO : payment_id
SCM_SA_INVOICE ||--o{ SCM_INVENTORY : source_order
SCM_INVENTORY ||--|{ SCM_INVENTORY_INFO : inventory_id
业务追踪键至少包括 sid、销售订单 ID/单号、出库单 ID/单号、原订单明细 ID、来源类型和来源单号。不要只按数字 ID 跨服务站关联。
18. 数量、库存和金额口径
18.1 数量
对每个销售订单明细:
剩余可出数量 = 销售数量 - 已出库有效数量 - 已关闭数量
实际字段和历史撤销口径以 Service 为准。撤销出库不能继续计入有效已出库量;销售退货是新的反向事实,不等价于恢复原销售订单“未出库量”。
18.2 库存
- 销售出库:交易类型
150601,从指定仓库/货位扣减实时库存。 - 销售退货:交易类型
150602,退回指定仓库/货位并增加库存。 - 撤销出库:通常生成反向库存事实或销退效果,不能删除原库存流水抹掉审计。
- 列表库存、货位库存和报表库存可能来自不同数据源,排查时核对来源时间。
18.3 金额
- 销售订单是客户承诺和价格快照,出库单是实际履约金额事实。
- 折扣、赠品、报价单、历史售价和客户价可能影响每行金额。
- 收款金额通过付款/收款明细与出库单关联,部分收款不能标记全部核销。
- 销售退货要冲减收入/应收并可能影响原收款,不能只增加库存。
19. 事务、锁和幂等
| 场景 | 当前保护 | 主要风险 |
|---|---|---|
| 新增销售订单 | sid+buId Redis 锁 3 秒 | 同一客户不同请求、锁超时后仍依赖单号/数据库幂等 |
| 销售出库 | sid+销售单ID Redis 锁 3 秒 | 并发出库最后库存和剩余量必须在事务内重校验 |
| 出库撤销 | sid+出库单ID Redis 锁 | 库存反向、配送取消、下游同步可能部分成功 |
| 批量对账 | 输入 ids 后逐批处理 | 中途失败可能部分完成,需要结果和重跑策略 |
| 配送回调 | 外部消息消费 | 重复回调不得重复推进或创建关系 |
| 下游同步 | MQ dgj_notify | 业务事务提交与发消息先后、重试幂等需确认 |
Redis 锁只是短期互斥,不等同业务幂等。销售出库必须同时依靠销售剩余量和实时库存的数据库级校验。
20. 按单号排查 SOP
20.1 销售订单和出库单
SELECT id, billNo, sid, status, sourceType, buId,
payType, totalAmount, createTime, modifyTime, isDelete
FROM t_scm_sa_order
WHERE sid = :sid AND billNo = :sa_order_no;
SELECT *
FROM t_scm_sa_order_info
WHERE sid = :sid AND orderId = :sa_order_id
ORDER BY id;
SELECT id, billNo, sid, billStatus, sourceType, buId,
hxStateCode, totalAmount, createTime, modifyTime, isDelete
FROM t_scm_sa_invoice
WHERE sid = :sid AND (srcOrderId = :sa_order_id OR srcOrderNo = :sa_order_no)
ORDER BY id;
SELECT *
FROM t_scm_sa_invoice_info
WHERE sid = :sid AND billId IN (:invoice_ids)
ORDER BY billId, id;
字段名可能因历史版本存在大小写差异,先以模型或只读 SHOW COLUMNS 校正。
20.2 库存和收款
SELECT i.id, i.billNo, i.transType, i.billDate, d.*
FROM t_scm_inventory i
JOIN t_scm_inventory_info d ON d.billId = i.id AND d.sid = i.sid
WHERE i.sid = :sid
AND (i.srcOrderId IN (:invoice_ids) OR i.srcOrderNo IN (:invoice_nos))
ORDER BY i.id, d.id;
SELECT p.id, p.billNo, p.billStatus, p.totalAmount,
d.stlId, d.stlNo, d.amount, d.accId, d.wayId
FROM t_scm_payment p
JOIN t_scm_payment_info d ON d.billId = p.id AND d.sid = p.sid
WHERE d.sid = :sid
AND (d.stlId IN (:invoice_ids) OR d.stlNo IN (:invoice_nos))
ORDER BY p.id, d.id;
20.3 配送关系
SELECT r.*, d.*
FROM t_sa_invoice_delivery_relation r
LEFT JOIN t_scm_third_party_delivery_order d ON d.id = r.delivery_order_id
WHERE r.sid = :sid AND r.sa_invoice_id IN (:invoice_ids)
ORDER BY r.id;
实际关系字段以模型为准。查不到关系时继续按外部配送单号、出库单号和回调日志搜索。
20.4 代码定位
rg -n "function addInvSaOr|function addOutBound|function cancel\(" application/controllers/scm/InvSa.php
rg -n "function saCreate|function saOrderOut|function saOrderMainFields" application/Services/SaOrders
rg -n "syncOutboundCreate|syncOutboundCancel|dgj_notify" application/Services
rg -n "dispatchCenterDeliveryStockOut|dispatchCenterLogisticeInfoNotify" application/controllers/tasks
21. 常见故障决策表
| 现象 | 第一检查点 | 第二检查点 | 常见原因 |
|---|---|---|---|
| 销售订单不能创建 | 客户、商品、来源、支付方式 | Redis 锁和重复单号 | 客户禁用、商品重复、来源映射缺字段 |
| 销售单不能出库 | 订单状态/剩余量 | 货位实时库存 | 已关闭、并发出库、货位删除或库存不足 |
| 出库成功库存未减 | 库存主/明细流水 | 实时库存和事务日志 | 库存步骤失败、历史部分提交或查询维度错 |
| 库存已减但销售单仍待出库 | 出库主明细 | 订单明细已出库量 | 事务/修复不完整或来源关联错 |
| 下游没收到出库 | syncOutboundCreate 日志 | dgj_notify 路由和消费 | enableSync=false、MQ 失败或下游幂等拒绝 |
| 出库撤销后仍在配送 | 配送关系 | transportDelete 和回调 | 配送取消失败或外部状态不可回退 |
| 已送达仍未对账 | 出库状态 | 客户/金额/对账权限 | 履约完成不自动等于对账 |
| 已对账但未核销 | hxStateCode | 收款明细和差额 | 未收/部分收款或分录未关联 |
| E站来源展示错误 | sourceType | saOrderMainFields 场景输入 | 历史值或支付场景映射错误 |
| 不同列表结果不一致 | 老/新列表接口 | 数据源开关和过滤条件 | outBoundList 与 outboundListNew 口径差异 |
22. 改动风险和回归清单
高风险公共点
InvSaService.php:老 PC 列表、出库、对账和退货仍大量共用。SaOrderSer.php:来源映射、订单和出库数量更新影响多个入口。InventorySer.php:销售出库、销退和其他库存业务共用。SyncOrderSer.php:SAAS/E站/APP 下游合同,字段改动会导致跨系统失败。SaOrderEnums.php:历史来源值和两套状态被页面、报表、MQ 共同依赖。
最小回归集
- 普通销售订单新增、修改、关闭;重复 SKU 和禁用客户拦截。
- E站商城、E站维修、活动、机器人、E站 APP 的挂账/微信来源映射。
- 一次全部出库、分批出库、剩余关闭、并发抢同一库存。
- 普通仓、移动仓和微仓铺货校验,已删除货位不可出库。
- 出库库存流水、实时库存、销售订单数量和出库单状态一致。
- 配送、自提、送达和重复回调;三方配送关系正确。
- 出库撤销生成反向库存、取消配送并通知下游。
- 挂账、现金、微信;未收、部分、全部收款。
- 单笔/批量对账、撤销对账、核销和有财务分录时的限制。
- 销售退货新增/修改、退货入库、原单/收款冲回。
- 老/新列表、PC/App 接口对同一单据的状态和金额一致。
23. 证据索引和待确认项
已确认
- PC 方法语义:
application/controllers/scm/InvSa.php。 - 状态/来源/支付:
application/KzData/Enums/SaOrderEnums.php。 - 表名:
application/config/tables.php。 - 销售订单/出库核心:
SaOrderSer.php、SaInvoiceSer.php、InvSaService.php。 - App code:
application/config/appapis.php。 - 下游同步:
application/Services/SyncOrder/SyncOrderSer.php。 - 配送回调:
application/controllers/tasks/DispatchCenterNotify.php。 - 交易类型:
application/KzData/Enums/TransTypeEnums.php。
待代码/环境确认
addOutBound()前段对请求进行 JSON 解析的历史代码和当前运行时实际数据形态。- 老/新销售、出库列表的数据源切换开关和各环境覆盖范围。
- 销售创建/出库/撤销 MQ 的完整 payload、幂等键和重试策略。
- 自动核销的金额容差、差额和账户优先级规则。
- 第三方配送关系表在各版本的实际字段名和外部状态映射。
- E站微信支付成功与 DGJ2 收款分录的最终回调链。
待确认项会在销售排查、配送和财务专题继续回填,当前文章不把未验证行为写成绝对结论。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| PC 销售开单 | PC scm/InvSa/* | sid、客户、SKU、数量、价格、仓库、收款方式 | controllers/scm/InvSa.php | SaOrderSer、InvSaService | 销售单号 | 销售主明细创建并进入待出库/履约状态 |
| App/E站下单 | appapis.php 销售 API 或 inner/moveMall/Order | E站来源、收货人、商品、价格和支付上下文 | App 映射入口/moveMall/Order.php | SaOrderSer | sourceOrderType + 来源单号 | 与 PC 汇合到销售订单及明细 |
| 仓内出库 | PC/PDA/WMS 出库请求 | 销售单号、SKU、仓位、实出量 | InvSa/App API Controller | SaInvoiceSer、InvSaService、InventorySer | 销售单号 + 出库单号 | 出库单生成,库存减少,销售履约量增加 |
| 配送/签收回调 | 配送中心 MQ | 配送单、出库单、骑手/三方状态 | tasks/DispatchCenterNotify.php | 配送关系与销售 Service | 出库单号/配送单号 | 配送、签收、拒收状态回写 |
| 收款/退货 | PC、支付回调或售后流程 | 销售单、金额、支付单;退货商品数量 | 收款 Controller/Consumer、销售退货入口 | 账户/支付 Service、退货入库 Service | 销售单号、支付单号、退货单号 | 应收核销或退货入库并反向库存/金额 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 |
|---|---|---|---|---|---|
| 下单入口 | Web/App/Inner 请求日志、InvSa/Order 方法 | URI、sid、来源单号、客户 | 返回销售单号 | 参数、价格、来源重复或权限失败 | 来源单号映射内部销售单号 |
| 订单落库 | SaOrderSer/InvSaService 异常 | 类方法、销售单号、sourceOrderType | 主明细事务提交 | 状态冲突、库存策略/客户校验失败 | 单号查 SCM_SA_ORDER* |
| 出库扣减 | SaInvoiceSer/InventorySer | 销售单、出库单、SKU、transType=150601 | 出库明细与库存流水一致 | 重复出库、超出可出量、负库存限制 | 出库单查 SCM_SA_INVOICE* 和库存 |
| 配送消费 | DispatchCenterNotify | 配送/出库单号、事件名、消息 ID | ACK 且配送关系状态推进 | NACK、找不到关系、回调乱序 | 出库单号关联配送关系和销售状态 |
| 收款/退货 | 财务或退货 Service/Consumer | 销售单、支付/退货单号、金额/数量 | 应收核销或退货入库完成 | 金额不平、重复退款、退货超量 | 单号串联账户、支付、退货和库存流水 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 创建销售单 | SaOrderSer | 销售事务 | 客户、商品、价格、来源唯一性 | SCM_SA_ORDER、SCM_SA_ORDER_INFO、SCM_SA_ORDER_INFO_EXT | insert 主明细;status 进入初始/待履约态;应收金额生成 | sid、销售单号、来源单号、金额合计 |
| 创建出库单 | SaInvoiceSer、InvSaService | 出库事务 | 可出数量、仓位、订单状态 | SCM_SA_INVOICE、SCM_SA_INVOICE_INFO、扩展表 | outQty: old -> old+n;出库单状态初始化 | 销售明细可出量等式、出库单明细 |
| 扣减库存 | InventorySer | 应与出库事实同一业务边界 | 实时库存四维键 | SCM_INVENTORY*、SCM_INVENTORY_REAL_TIME | qty: old -> old-n;写 transType=150601 流水 | 出库单号、流水明细、实时库存 |
| 配送履约 | DispatchCenterNotify + 配送 Service | 消息消费事务 | 当前配送/签收状态 | SCM_THIRD_PARTY_DELIVERY_ORDER、SCM_SA_INVOICE_DELIVERY_RELATION、销售/出库状态 | 配送状态按事件推进;重复/旧事件不得回退 | 配送单号、出库单号、消费 ACK |
| 收款核销 | 支付/账户 Service | 财务事务 | 应收余额、支付结果 | SCM_ACCOUNT*、SCM_PAYMENT*、销售收款事实 | 已收金额 old -> old+n;余额 old -> old-n | 销售单、支付单、分录借贷/金额平衡 |
| 销售退货 | 退货 Service -> InventorySer | 退货与库存事务边界需核对 | 已出/已退数量、退款金额 | 退货单/日志、库存流水、退款消息 | returnQty: old -> old+n;库存 old -> old+n;应收/退款反向 | transType=150502/150602、退货单与原出库单 |
子模块级独立追踪
子模块追踪:sale-create 销售订单创建
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| PC 创建 | scm/InvSa 保存/提交销售单 | request ID、sid、客户、SKU、数量、售价、来源单号 | application/controllers/scm/InvSa.php -> InvSaService/SaOrderSer | 客户状态、商品可售、价格权限、来源唯一性 | 销售事务 insert SCM_SA_ORDER/INFO/INFO_EXT;状态由无 -> 初始/待履约,应收和订购量写入 | request ID + sid+saleBillNo/sourceOrderNo 查 Controller、主明细和创建时间 | 参数或价格校验失败零写入;来源重复先返回/查询原销售单,不新建第二单 |
| E站/App 创建 | App code 或 inner/moveMall/Order | request ID、E站来源、用户、地址、商品价格 | application/controllers/inner/moveMall/Order.php -> SaOrderSer | E站用户/站点、商品、来源场景和收货信息 | 同一销售事务写销售主明细及来源关系,sourceOrderType/sourceOrderNo 由空 -> E站值 | 来源单号 + 内部销售单号贯穿 Inner 日志和表关系 | 远程/响应超时先按来源单查内部单;禁止因未收到响应重复创建 |
子模块追踪:sale-outbound 销售出库单创建
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建出库 | PC InvSa/sale/OutOrder、App/PDA 出库 API | request ID、销售单号、SKU、仓库货位、实出量 | application/controllers/scm/InvSa.php / application/controllers/sale/OutOrder.php -> SaInvoiceSer/InvSaService | 销售状态、可出=订购-已出-关闭/退回、仓位和配送方式 | 出库事务 insert SCM_SA_INVOICE/INFO/INFO_EXT;出库量 old -> old+n,状态由无 -> 待出库/配送 | request ID + 销售/出库单号 + SKU;回查两类明细数量 | 超量、非法状态或仓位失败不应有出库单;部分创建按出库业务键补明细,不重建主单 |
| 状态回写 | 出库主明细 commit 后 | 销售单号、出库单号、各 SKU 累计量 | application/Services/SaOrders/SaInvoiceSer.php -> 销售状态更新 | 全部销售明细的订购、已出、关闭量 | 销售状态 wait -> part_out/outed;仅部分出库保持部分态,DB 事务内影响行数应为 1 | 两种单号、状态更新时间、明细数量合计 | 主状态与数量不一致时不直接改状态,先补缺失出库/关闭过程事实再重算 |
子模块追踪:sale-stock 销售出库库存扣减
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 扣减 | 出库确认事务调用库存域 | 出库单号、sid、SKU/invId、仓库货位、数量、transType=150601 | application/Services/SaOrders/SaInvoiceSer.php / InvSaService -> application/Services/Storage/InventorySer.php | 实时库存四维行、负库存配置、出库明细 | 写 SCM_INVENTORY/INFO 流水;实时 qty: old -> old-n,流水明细 qty=-n | 出库单 + transType + 库存四维键;比较流水净额和实时前后值 | 库存不足/死锁应 rollback 出库;出库已成库存未扣时只按原业务键补库存,防止新增第二出库单 |
| 派生同步 | 库存 commit 后 MQ/缓存动作 | 出库单、routing key、库存键、message ID | application/Services/Mq/MqSer.php、库存缓存/预警 Service | 已提交实时库存和商品维度 | 本地业务表 DB 不变;MQ/cache/search 从旧量 -> 新量,属于异步边界 | message ID + 出库单 + 库存键串联生产、消费、缓存时间 | 派生层失败不反向重做扣库存;仅补发库存事件/刷新缓存和预警 |
子模块追踪:sale-delivery 配送、自提与签收
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建履约 | 出库选择自提、自配送或三方配送 | request ID、出库单、地址快照、配送模式 | application/Services/Transport/SelfDeliverySer.php / SaOrders/ThirdDeliverySer.php -> TMS/三方 Provider | 出库状态、地址坐标、已有配送关系 | 本地事务 insert 配送单/SCM_SA_INVOICE_DELIVERY_RELATION,status 由无 -> pending;外部下单在异步/HTTP 边界 | request ID + 出库单 + local/external delivery ID | 外部超时先查对方是否有单;外部有、本地无时补关系,禁止直接再次下单 |
| 状态签收 | 配送中心 MQ、自提核销或骑手动作 | message ID、配送/出库单号、核销码、event | application/controllers/tasks/DispatchCenterNotify.php 或配送 Controller -> Delivery Service | 当前配送状态、事件时序、签收幂等键 | 单消息事务 pending -> accepted/picked/delivering/signed/canceled;签收后出库/销售履约态同步推进 | message ID + local/external delivery ID + 出库单;按事件绝对时间排序 | 旧事件不得回退状态;重复核销/回调为 new -> new,外部成功本地失败按原配送号补写 |
子模块追踪:sale-receipt 对账、收款与核销
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 收款制单 | PC Receipt/销售收款或支付回调 | request ID、销售/出库单、支付单、客户、金额 | application/service/scm/ReceiptService.php / 财务 Service | 应收总额、已收/已核销、账户余额和支付最终态 | 财务事务 insert 收款/账户分录;已收 old -> old+n,未收余额 old -> old-n | request ID + 销售/出库/收款/支付单号;核对分录和来源分摊合计 | 金额超额、重复支付或账户异常 rollback;支付中心成功本地失败按原 payOrderNo 幂等落账 |
| 对账核销 | 对账任务、人工核销或收款同步 MQ | message ID、对账单、源出库单集合、核销金额 | application/Services/Statement/* / Receipt Service | 全部来源出库和收款状态、账单快照 | 单事务/单消息 update 核销关系和状态;核销额 old -> old+n,全额才进入完成 | 对账单 + 源出库单 + message ID;账单明细总额=核销+未核销 | 漏消息按源出库单重算;不可只改账单完成态而不补核销关系和资金分录 |
子模块追踪:sale-revoke 撤销出库与状态恢复
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 撤销申请 | PC 销售出库撤销动作 | request ID、sid、销售/出库单、原因、操作人 | application/controllers/scm/InvSa.php -> InvSaService 撤销方法 | 出库是否配送/签收/对账/收款,已撤数量和库存事实 | 撤销事务将出库有效态 old -> canceled/revoked,销售已出量 old -> old-n;仅符合条件可执行 | request ID + 出库单 + action;查主明细、操作日志、影响行数 | 已配送/核销时先撤外部/财务依赖;任何前置失败都不得直接改出库状态 |
| 库存与外部恢复 | 撤销领域 Service 后续 | 出库单、SKU、原库存流水、配送 ID、message ID | application/Services/Storage/InventorySer.php + Delivery/Mq Service | 原 150601 流水、配送最终态和实时库存 | 反向库存流水 insert,实时 qty: old -> old+n;配送/下游状态同步撤销,属于跨系统边界 | 出库单 + 原/反向 transType + 配送/消息 ID | 本地撤销成功、外部失败只补外部取消;库存已回加不得再次执行整段撤销 |
子模块追踪:sale-return 销售退货与退货入库
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建退货 | PC InvSa 或审核通过的 ApplyReturn | request ID、原销售/出库单、退货单、SKU、数量、金额 | application/Services/InvSa/NormalSaleReturnSer.php / ApplyReturnSer | 原已出、已退、已申请数量和可退金额 | 退货事务 insert 销退主明细/关系;退货量由无 -> n,原单累计退货 old -> old+n | request ID + 原出库/退货/申请单号 + SKU | 超退或重复来源零写入;响应未知先按申请/来源关系查已有退货单 |
| 退货入库退款 | 仓库确认入库、退款回调 | 退货单、SKU、仓库货位、实收量、refund/pay ID | application/Services/InvSa/NormalSaleReturnSer.php -> InventorySer;财务退款 Service | 可收退货量、实时库存、原支付及已退款金额 | 退货事务写 transType=150602,库存 qty: old -> old+n;实收和退款金额 old -> old+n | 退货单 + 150602 + 原支付/refund ID;库存和资金分别回查 | 库存与退款非同一原子边界;只补失败一侧,重复回调不得重复入库或退款 |
子模块追踪:sale-sync 销售与出库下游同步
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 生产消息 | 销售创建、出库、撤销或退货 commit 后 | 销售/出库单、source type、routing key、稳定业务键 | application/Services/SyncOrder/SyncOrderSer.php -> SyncOrderBuilder.php -> MqSer | 已提交主明细、状态、数量金额和来源映射 | 本地核心表 DB 不变;构造 dgj_notify 等消息快照,状态由本地已提交 -> 下游待消费 | 业务单号 + routing key + message ID;保存脱敏 payload 摘要 | 序列化/发送失败只补同一业务事件;不能再次执行销售或出库主事务 |
| 下游消费确认 | App/SAAS/订单中心消费者 | message ID、业务单号、event/version | 外部消费者;本地回调入口位于 application/controllers/tasks/*Notify.php | 下游当前状态和消息版本 | 下游 old -> new;本地仅在合同需要时写同步/回调状态,消费属于异步边界 | 生产与消费端用同一业务单号,不以随机 message ID 判断业务幂等 | 下游未收到按业务键重发;下游已处理但 ACK 丢失时重复消息必须零副作用 |