本文把 DGJ2 中容易混在一起的“大客户销售”“临采”“渠道平台订单”“渠道售后”拆成四类业务对象,再按接口请求、状态流转、金额与数量、库存记账、MQ 回写和异常补偿重新串起来。
阅读本文后,应能回答以下问题:
- 平台订单、DGJ 渠道订单和大客户销售出库单是不是同一张单。
created、wait_ship、part_ship、finish分别由谁推进。- 普通、直采、临采只影响什么,是否等价于渠道来源。
- 哪一步才真正扣减库存,哪一步只是登记渠道意图。
- 一张渠道退单为什么可能拆成多张
180602大客户销退单。 - OPS、订单中心、DGJ、服务站操作端之间分别传递什么。
- 重复下单、部分发货、重复售后、消息失败时如何定位。
1. 业务目标
大客户链路要解决的不是普通零售开单,而是服务站代表快准体系向指定大客户渠道履约:
| 目标 | 业务含义 | DGJ 承担的职责 |
|---|---|---|
| 接收平台采购意图 | 外部平台传入门店、商品、数量和销售价 | 建立可追踪的渠道订单 |
| 人工代建平台订单 | 服务站为孚创、车之谷等人工录入 | 校验结算价、库存和限价,再推 OPS |
| 订单审核 | 渠道确认或拒绝人工订单 | 将待审核变为待发货或取消 |
| 仓内履约 | 服务站选择仓库、区位和配送人 | 生成 180601 大客户销售单并扣库存 |
| 渠道回写 | 告知 OPS/渠道实际出库与发货信息 | 通过 MQ 和订单中心接口异步回传 |
| 签收 | 接收渠道签收批次 | 累计签收明细,形成履约证据 |
| 售后 | 渠道申请退货,服务站审核入库 | 生成 180602 销退单并增加库存 |
| 财务与利润 | 保存销售价、结算成本、配送费 | 为大客户报表、NC/OPS 同步提供依据 |
2. 先建立正确的对象模型
2.1 四类核心对象
| 对象 | 代表表 | 是否直接改库存 | 状态拥有方 | 关键编号 |
|---|---|---|---|---|
| 大客户类型/客户关系 | t_bs_big_customer、客户关系表 | 否 | 基础资料 | merchant_code、contact_id |
| 渠道平台订单 | t_channel_order* | 否 | Channel Service | CHS...、source_order_no |
| 大客户销售/销退单 | t_scm_big_order* | 是 | InvCuService | billNo,180601/180602 |
| 渠道售后申请 | t_channel_aftersale* | 否;审核通过后间接入库 | Channel Aftersale | CHR...、source_refund_no |
最重要的认知:t_channel_order是平台业务编排单,t_scm_big_order才是 DGJ 库存与财务事实单。
flowchart LR
P["外部渠道订单"] --> C["渠道订单 t_channel_order"]
C -->|"发货"| B["大客户销售单 180601"]
B --> I["库存出库流水"]
B --> O["OPS / NC / 渠道回写"]
P --> A["渠道售后 t_channel_aftersale"]
A -->|"审核入库"| R["大客户销退单 180602"]
R --> J["库存入库流水"]
2.2 三个“来源/类型”维度不能混用
| 字段 | 枚举 | 回答的问题 |
|---|---|---|
scene_code | dcd、fc、czg | 订单来自哪个渠道场景 |
srcOrderSource | default、channel | 大客户事实单是自制还是由渠道订单生成 |
serviceType | 1 普通、2 直采、3 临采 | 大客户销售采用哪种服务模式 |
transType | 180601、180602 | 事实单是销售出库还是销售退货 |
flowchart TD
A["一张大客户事实单"] --> B{"来源是什么"}
B -->|"自制"| C["srcOrderSource=default"]
B -->|"平台"| D["srcOrderSource=channel"]
A --> E{"服务方式是什么"}
E -->|"普通"| F["serviceType=1"]
E -->|"直采"| G["serviceType=2"]
E -->|"临采"| H["serviceType=3"]
A --> I{"业务方向是什么"}
I -->|"销售"| J["transType=180601"]
I -->|"退货"| K["transType=180602"]
3. 适用场景与非适用场景
3.1 本专题覆盖
- 服务站手工创建普通、直采、临采大客户销售单。
- 懂车帝、孚创、车之谷渠道商品、下单、审核、发货和签收。
- 服务站人工代建孚创/车之谷平台订单。
- 渠道退货申请、服务站拒绝、服务站确认入库。
- 渠道订单与
180601/180602事实单之间的映射。 - OPS、订单中心、NC、MQ 的通知和补偿。
- 临采白名单、渠道自动发货配置及一次性发货约束。
3.2 不在本文展开
| 内容 | 应阅读 |
|---|---|
| 普通销售订单、销售出库和收款 | 04_销售_出库_配送与收款.md、15_销售完整排查手册.md |
| 全车件订单 | 07_外部渠道_全车件与大客户订单.md |
| 成本价公式 | 45_成本价_动态成本_利润计算专题.md |
| MQ 基础设施 | 13_MQ回调与补偿任务地图.md |
| 导入模板通用机制 | 20_导入导出和Excel模板.md |
4. 代码地图
4.1 接口入口
| 层次 | 文件 | 主要方法 |
|---|---|---|
| 外部渠道订单 | application/controllers/inner/channel/Order.php | create、confirm_receipt、order_approve |
| 外部渠道售后 | application/controllers/inner/channel/Aftersale.php | apply、get_return_stock_in |
| 外部渠道商品 | application/controllers/inner/channel/Goods.php | lists、detail |
| 服务站大客户页面/API | application/controllers/scm/InvCu.php | 大客户单、渠道单、发货、售后、导入、补推、配置 |
| MQ 消费 | application/controllers/tasks/ChannelOrderNotify.php | 四类渠道事件 |
| 历史/预发临时任务 | application/controllers/tasks/TmpChannelOrder.php | autoDelivery、autoRefundConfirm |
4.2 服务层
| 服务 | 责任 |
|---|---|
application/Services/Channel/BaseService.php | 场景公共模型、客户绑定、商家编码 |
application/Services/Channel/BaseOrder.php | 列表、详情、发货、签收、转大客户销售单 |
application/Services/Channel/Traits/ChannelOrderTrait.php | FC/CZG 人工建单、审核、复制、自动发货 |
application/Services/Channel/BaseAftersale.php | 售后申请、审核、按原出库单拆退货 |
application/Services/Channel/Dcd/* | 懂车帝差异实现 |
application/Services/Channel/Fc/* | 孚创差异实现 |
application/Services/Channel/Czg/* | 车之谷差异实现 |
application/service/scm/InvCuService.php | 180601/180602 事实单、库存、OPS/NC 同步 |
application/Services/BigCustomer/BigCustomerService.php | 大客户第三方商品销售控制 |
4.3 配置与校验
| 文件 | 内容 |
|---|---|
application/Services/Channel/SceneConst.php | 场景映射、服务类映射、Apollo 配置读取 |
application/KzData/Enums/ChannelOrderEnums.php | 渠道订单状态和审核动作 |
application/KzData/Enums/ChannelAftersaleEnums.php | 售后状态和动作 |
application/KzData/Enums/BigOrderEnums.php | 来源、服务类型、平台单拦截 |
application/Validate/Channel/* | 外部及站内请求字段校验 |
application/Providers/OrderCenter/BigOrderProvider.php | 订单中心/OPS 对接地址封装 |
5. 渠道场景与能力矩阵
SceneConst 当前定义三种场景:
| 场景码 | 名称 | source 值 | 外部下单 | 人工代建 | 渠道审核 | 复制订单 |
|---|---|---|---|---|---|---|
dcd | 懂车帝 | 5 | 支持 | 明确不支持 | 明确不支持 | 不支持 |
fc | 孚创 | 6 | 支持 | 支持 | 支持 | 支持 |
czg | 车之谷 | 7 | 支持 | 支持 | 支持 | 支持 |
能力不是通过大量 if 堆叠,而是通过服务映射选择实现:
flowchart TD
R["请求 sceneCode"] --> S["SceneConst 映射"]
S --> G["CHANNEL_GOODS"]
S --> O["CHANNEL_ORDER"]
S --> A["CHANNEL_AFTERSALE"]
O --> D["DcdOrderService"]
O --> F["FcOrderService"]
O --> C["CzgOrderService"]
5.1 Apollo 动态配置
| 配置键 | 示例语义 | 默认行为 | 风险 |
|---|---|---|---|
dkh.max_price_rate | 销售价最多为结算价的倍数 | 未配置场景时 1.0 | 缺配置可能意外收紧限价 |
dkh.merchant_map | 场景到 OPS 大客户编码 | 未配置返回 null | 无编码无法查结算价/推 OPS |
dkh.max_submit_row | 单据最大商品行数 | 50 | 配置变更影响人工开单 |
dkh.once_delivery_merchant | 必须一次发完的场景 | 空数组 | 缺配置可能开放部分发货 |
flowchart LR
A["Apollo 配置"] --> B["SceneConst 静态读取方法"]
B --> C["人工开单限价"]
B --> D["OPS 商家编码"]
B --> E["最大明细行"]
B --> F["是否允许部分发货"]
6. 客户绑定模型
所有 inner/channel/* 请求先经过 BaseChannel::initStationAndContact():
- 请求必须带
sceneCode和shopId。 sceneCode必须在dcd/fc/czg中。- 调用
ContactSer::getStationAndContactBySourceId(shopId, sceneCode)。 - 找到渠道门店对应的 DGJ 服务站和修理厂。
- 注入
channelSid、channelContactId、stationInfo、contactInfo。
sequenceDiagram
participant P as 渠道平台
participant BC as BaseChannel
participant CS as ContactSer
participant DB as 客户关系/缓存
P->>BC: sceneCode + shopId
BC->>CS: getStationAndContactBySourceId
CS->>DB: 查询渠道门店绑定
DB-->>CS: stationInfo + contactInfo
CS-->>BC: 服务站与修理厂
alt 未绑定
BC-->>P: 服务站和修理厂绑定信息异常
else 已绑定
BC->>BC: 注入 sid/contactId
end
排查“服务站和修理厂绑定信息异常”时,不要先查订单表,应先查渠道门店关系和 ContactSer 的 300 秒关系缓存。
7. 核心表字典
7.1 渠道订单域
| 表 | 关键字段 | 业务意义 |
|---|---|---|
t_channel_order | id、order_no、source_order_no | DGJ 渠道单号与外部单号 |
scene_code、merchant_code、source_shop_id | 渠道、商家和门店 | |
sid、contact_id | 履约服务站和客户 | |
status、approve_* | 渠道订单状态与审核信息 | |
total_amount、cost_amount、profit_amount | 售价、成本和价差 | |
t_channel_order_info | chan_order_id、order_no | 关联主单 |
source_row_no、sku_code、inv_id | 外部行号、SKU、DGJ 商品 | |
qty、shiped_num、refund_num | 下单、已发、已申请售后数量 | |
sale_price、cost_price、profit | 销售价、成本、单件价差 | |
t_channel_order_sign_detail | signing_no、order_row_no | 签收批次与订单行 |
qty、signing_person、signing_time | 签收数量与凭据 |
7.2 渠道售后域
| 表 | 关键字段 | 业务意义 |
|---|---|---|
t_channel_aftersale | refund_no、source_refund_no | DGJ 售后号与渠道售后号 |
order_no、source_order_no | 关联渠道原单 | |
status、apply_remark、audit_remark | 售后状态和意见 | |
total_num、total_amount | 申请退货数量和金额 | |
t_channel_aftersale_info | order_row_no | 关联渠道订单明细行 |
qty、price、amount | 申请退货数量和金额 | |
source_order_row_no | 渠道原始明细行号 |
7.3 大客户事实域
| 表 | 关键字段 | 业务意义 |
|---|---|---|
t_scm_big_order | billNo、transType | 180601 销售或 180602 销退 |
srcOrderSource | default 自制或 channel 平台来源 | |
srcChannelOrder | 关联 CHS/CHR 平台单号 | |
srcOrderId/srcOrderNo | 销退关联原销售事实单 | |
serviceType | 普通、直采、临采 | |
t_scm_big_order_info | iid、invId、qty | 事实单明细和正负数量 |
srcOrderEntryId | 退货关联原出库明细 | |
srcOrderId/srcOrderNo | 来源主单 | |
t_scm_big_lincai_whitelist | sid 等 | 临采可用范围 |
7.4 关系图
erDiagram
CHANNEL_ORDER ||--o{ CHANNEL_ORDER_INFO : contains
CHANNEL_ORDER ||--o{ BIG_ORDER : delivers
BIG_ORDER ||--o{ BIG_ORDER_INFO : contains
CHANNEL_ORDER ||--o{ CHANNEL_AFTERSALE : applies
CHANNEL_AFTERSALE ||--o{ CHANNEL_AFTERSALE_INFO : contains
CHANNEL_ORDER_INFO ||--o{ CHANNEL_AFTERSALE_INFO : returns
BIG_ORDER_INFO ||--o{ BIG_ORDER_INFO : returned_by
CHANNEL_ORDER ||--o{ CHANNEL_ORDER_SIGN_DETAIL : signed
实际表没有统一外键约束,很多关系依赖业务字段和代码约定,因此排查时要同时比对 ID 与单号。
8. 编号与幂等键
| 编号 | 生成/来源 | 用途 | 推荐唯一范围 |
|---|---|---|---|
source_order_no | 外部平台传入 | 外部订单幂等 | scene_code + source_order_no |
order_no | DGJ 生成 CHS... 或雪花号 | 渠道订单主键 | 全局或 sid + order_no |
source_row_no | 外部平台传入 | 明细映射 | scene + source_order_no + source_row_no |
billNo | InvCuService 生成 | 大客户事实单号 | sid + billNo |
source_refund_no | 外部平台传入 | 售后幂等 | scene + source_refund_no |
refund_no | DGJ CHR... | 渠道售后号 | 全局或 sid + refund_no |
signing_no | 渠道传入 | 签收批次 | scene + signing_no |
代码中的一秒 redis_lock(md5(request)) 只能削平瞬时重复请求,不能替代数据库唯一键和业务幂等。
flowchart TD
R["重复请求"] --> L{"1 秒 Redis 锁命中?"}
L -->|"是"| X["快速拒绝/串行"]
L -->|"否或锁过期"| U{"业务唯一键存在?"}
U -->|"是"| E["返回原结果"]
U -->|"否"| N["创建新记录"]
9. 渠道订单状态机
9.1 状态字典
| 状态 | 中文 | 进入条件 | 允许动作 |
|---|---|---|---|
created | 待审核 | FC/CZG 人工代建;部分场景创建 | 渠道审核通过/拒绝 |
wait_ship | 待发货 | 外部下单直接待履约,或审核通过 | 整单/部分发货 |
part_ship | 部分发货 | 累计已发小于订单量 | 继续发货 |
finish | 已完成 | 全部发货完成 | 签收、售后、查询 |
cancel | 已取消 | 审核拒绝 | 只读 |
stateDiagram-v2
[*] --> created: 人工代建
[*] --> wait_ship: 外部订单直接进入履约
created --> wait_ship: auditStatus=1
created --> cancel: auditStatus=0
wait_ship --> part_ship: 部分发货
wait_ship --> finish: 一次发完
part_ship --> part_ship: 再次部分发货
part_ship --> finish: 剩余全部发完
9.2 状态真正推进的位置
| 转换 | 入口 | 核心实现 |
|---|---|---|
| 创建 -> 待审核 | manual_create | ChannelOrderTrait::create 强制 created |
| 创建 -> 待发货 | 外部 create | 场景实现/formatRequestToOrderEntry |
| 待审核 -> 待发货/取消 | order_approve | ChannelOrderTrait::order_approve |
| 待发货 -> 部分/完成 | channelOrderDelivery | BaseOrder::delivery |
| 签收登记 | confirm_receipt | BaseOrder::confirm_receipt,不等于发货状态推进 |
10. 渠道售后状态机
| 状态 | 中文 | 说明 |
|---|---|---|
created | 退单创建 | 枚举存在,当前主要申请流程可能直接进入 pass |
pass | 待收货 | 已接受申请,等待服务站确认退货入库 |
reject | 已拒绝 | 服务站拒绝并回退占用的 refund_num |
finish | 已完成 | 已生成 180602 销退事实单 |
stateDiagram-v2
[*] --> pass: 当前 apply 逻辑
created --> pass: 审核通过(兼容状态)
created --> reject: 审核拒绝(兼容状态)
pass --> finish: 服务站确认入库
pass --> reject: 服务站拒绝
ACTION_PASS/ACTION_REJECT是动作,STATUS_PASS/STATUS_REJECT是状态。当前字符串都为pass/reject,语义相同但代码阅读时必须区分。
11. 外部商品查询流程
渠道下单前通常先查询 DGJ 可售商品:
sequenceDiagram
participant P as 渠道
participant G as inner/channel/Goods
participant S as 场景 GoodsService
participant M as Mongo/商品缓存
participant O as OPS结算价
P->>G: sceneCode, shopId, 查询条件
G->>G: GoodsValidate
G->>S: lists/detail
S->>M: 商品、库存、服务站可售范围
S->>O: 大客户结算价/渠道规则
O-->>S: 价格
S-->>P: SKU、库存、售价/结算信息
商品列表成功不保证最终能下单。建单时还会重新检查:
- SKU 是否存在于服务站 Mongo 商品缓存。
- 数量是否大于零。
- 价格是否大于等于
0.01。 price * qty是否等于amount。- 服务站成本价是否存在。
- 人工代建时结算价是否存在、售价是否超过上限、即时库存是否大于零。
12. 外部渠道下单接口
12.1 逻辑入口
控制器方法:inner/channel/Order::create()。
部署 URL 受网关和 CodeIgniter 路由影响,排查时以网关配置为准;代码层可按以下逻辑请求理解:
POST /inner/channel/order/create
Content-Type: application/json
{
"sceneCode": "dcd",
"merchantCode": "<渠道商家编码>",
"merchantName": "示例大客户",
"shopId": "<渠道门店编号>",
"orderNo": "<渠道采购单号>",
"submitTime": "2026-07-16 10:00:00",
"totalAmount": "260.00",
"remark": "门店补货",
"details": [
{
"orderRowNo": "ROW-001",
"skuCode": "<SKU编码>",
"qty": 2,
"price": "130.00",
"amount": "260.00",
"remark": ""
}
]
}
12.2 服务端处理顺序
sequenceDiagram
participant P as 渠道
participant C as Order Controller
participant B as BaseChannel
participant S as 场景 OrderService
participant M as Mongo/成本服务
participant DB as MySQL
P->>B: sceneCode + shopId
B->>B: 解析服务站/客户绑定
P->>C: 订单请求
C->>C: 1秒请求锁 + 参数校验
C->>S: create(request)
S->>DB: 按 source_order_no 查幂等
alt 已存在
DB-->>S: 原 id/order_no
else 不存在
S->>M: SKU、invId、成本价
S->>S: 计算成本与利润
S->>DB: 事务写主表和明细
end
C->>C: createCarType
C-->>P: DGJ saleOrderNo + 服务站信息
12.3 金额计算
对每一行:
sale_amount = qty * sale_price
cost_amount = qty * cost_price
profit = sale_price - cost_price
profit_amount = qty * profit
主单:
total_amount = Σ sale_amount
cost_amount = Σ cost_amount
profit_amount = total_amount - cost_amount
系统注释中将渠道订单的 profit_amount 称为“配送费”,报表或接口是否也采用该口径必须另行确认。
13. 服务站人工代建平台订单
13.1 适用范围
- 支持
fc、czg。 dcd的manual_create明确抛出“该渠道不支持该业务”。- 入口:
InvCu::channelOrderManualCreate()。
13.2 请求示例
POST /scm/invCu/channelOrderManualCreate
Content-Type: application/json
{
"sceneCode": "fc",
"shopId": "<渠道门店编号>",
"totalAmount": "260.00",
"remark": "人工代建",
"details": [
{
"skuCode": "<SKU编码>",
"qty": 2,
"price": "130.00",
"amount": "260.00"
}
]
}
13.3 校验链
flowchart TD
A["人工开单请求"] --> B["scene 仅 FC/CZG"]
B --> C["商品行不重复且不超过 Apollo 上限"]
C --> D["查询 OPS 大客户结算价"]
D --> E{"每行有结算价?"}
E -->|"否"| X["拒绝"]
E -->|"是"| F["售价 <= 结算价 * max_price_rate"]
F --> G["Mongo 商品存在且即时库存 > 0"]
G --> H["生成渠道单号和行号"]
H --> I["写 created 渠道订单"]
I --> J["发送 channel_order_manual_create"]
J --> K["消费者推 OPS 等待审核"]
13.4 人工建单的两个成功层次
| 层次 | 判定 | 失败后结果 |
|---|---|---|
| DGJ 本地建单成功 | t_channel_order* 已提交 | 页面返回成功 |
| 推 OPS 成功 | MQ 消费调用订单中心成功 | OPS 可看到并审核 |
本地建单后的 MQ 发送异常只记日志,不回滚本地订单。因此“页面成功但 OPS 没单”应查 channel_order_manual_create 消息和补推接口,而不是重复建单。
14. 渠道审核流程
14.1 请求示例
POST /inner/channel/order/order_approve
Content-Type: application/json
{
"sceneCode": "fc",
"merchantCode": "<商家编码>",
"merchantName": "示例大客户",
"shopId": "<门店编号>",
"saleOrderNo": "<DGJ渠道单号>",
"sourceOrderNo": "<渠道正式订单号>",
"auditStatus": 1,
"submitTime": "2026-07-16 10:10:00",
"cancelReason": ""
}
14.2 状态推进
sequenceDiagram
participant P as FC/CZG
participant C as Order::order_approve
participant S as ChannelOrderTrait
participant DB as t_channel_order
participant MQ as dgj_channel
P->>C: 审核请求
C->>C: 参数锁和校验
C->>S: order_approve
S->>DB: 只查 status=created
alt 通过
S->>DB: 校验 source_order_no 未重复
S->>DB: status=wait_ship
else 拒绝
S->>DB: status=cancel
end
S->>MQ: channel_order_approve
MQ->>MQ: 尝试自动发货
审核通过后不等于已经扣库存,只表示允许发货。真正库存变更仍在下一步。
15. 自动发货决策
channel_order_approve 消费者会按 scene_code 找到 OrderService 并调用 auto_delivery()。
自动发货是否执行还受以下条件控制:
| 条件 | 来源 |
|---|---|
| 订单状态必须允许发货 | t_channel_order.status |
| 服务站开启渠道自动发货 | SettingSer::getChannelOrderSetting |
| 仓库/区位能满足出库 | 库存查询 |
| 场景是否要求一次发完 | dkh.once_delivery_merchant |
| 商品实际库存足够 | 库存服务 |
flowchart TD
A["审核通过事件"] --> B{"服务站开启自动发货?"}
B -->|"否"| M["留在待发货,人工处理"]
B -->|"是"| C["加载订单及仓库区位库存"]
C --> D{"全部明细可满足?"}
D -->|"否"| M
D -->|"是"| E["构造 delivery 请求"]
E --> F["生成 180601 事实单"]
消费者捕获自动发货异常后仍返回 ACK,不会依赖 MQ 自动重投。此类失败只能靠人工发货、业务补偿或专门任务恢复。
16. 渠道发货接口
16.1 请求示例
POST /scm/invCu/channelOrderDelivery
Content-Type: application/json
{
"orderNo": "<DGJ渠道单号>",
"delieverId": 1001,
"delieverName": "配送员",
"remark": "第一批发货",
"details": [
{
"rowNo": 12345,
"outQty": 1,
"locationId": 10000,
"locationName": "主仓",
"locationAreaId": 500001,
"locationAreaName": "A区",
"remark": ""
}
]
}
16.2 发货主流程
sequenceDiagram
participant U as 服务站
participant BO as BaseOrder
participant DB as 渠道订单表
participant IC as InvCuService
participant INV as InventorySer
participant MQ as dgj_channel
U->>BO: delivery(orderNo, details)
BO->>BO: Redis 锁 + 参数校验
BO->>DB: checkCanDelivery
DB-->>BO: 状态、订单量、已发量
BO->>BO: buildBigOrderDeliverData
BO->>IC: submitOrderV2(transType=180601)
IC->>INV: 保存库存出库事实
IC-->>BO: billNo
BO->>DB: 累加 shiped_num
BO->>DB: 更新 part_ship/finish
BO->>MQ: channel_order_delivery
BO-->>U: 大客户销售单号
16.3 发货数量守恒
对每个渠道订单明细行:
0 <= shiped_num <= qty
本次 outQty <= qty - shiped_num
主单状态:
所有明细 shiped_num = qty -> finish
至少一行 shiped_num > 0 -> part_ship
所有明细 shiped_num = 0 -> wait_ship
16.4 一次性发货
若 scene_code 在 dkh.once_delivery_merchant 中:
- 请求必须覆盖所有未发数量。
- 不能只选择部分明细。
- 不能只发某行的一部分。
这项配置改变发货合法性,变更前必须回归人工发货和自动发货。
17. 大客户事实单如何落库存
BaseOrder::delivery() 将新渠道结构转换为老 InvCuService::submitOrderV2() 参数:
| 渠道字段 | 大客户事实单字段 | 说明 |
|---|---|---|
渠道 order_no | srcChannelOrder | 事实单追溯平台意图单 |
渠道主单 id | srcOrderId | 关联渠道主表 |
渠道明细 id | srcOrderEntryId | 关联渠道明细 |
outQty | qty | 本次实际出库数量 |
sale_price | price | 大客户销售价 |
cost_price | 成本字段 | 结算/成本快照 |
| 仓库与区位 | location* | 库存扣减位置 |
| 固定类型 | transType=180601 | 大客户销售 |
| 固定来源 | srcOrderSource=channel | 禁止普通编辑/取消 |
flowchart LR
C["渠道订单明细 qty"] --> D["本次发货 outQty"]
D --> B["t_scm_big_order_info qty"]
B --> I["库存流水负向"]
B --> S["channel_order_info.shiped_num 累加"]
平台来源事实单被 channelSourceOrderIntercept() 拦截,普通页面不允许编辑或取消,提示:
大客户平台订单或退单,暂不支持编辑和取消操作
这是为了避免绕开渠道状态和数量守恒直接改库存事实。
18. 发货后的异步回写
发货成功发送 channel_order_delivery 到 DEST_DGJ_CHANNEL。
flowchart TD
A["180601 创建成功"] --> B["sendChannelOrderDelivery"]
B --> C["ChannelOrderNotify::channelOrderDelivery"]
C --> D{"事实单存在且来源=channel?"}
D -->|"否"| X["记录并 ACK 丢弃"]
D -->|"是"| E["channelOrderDeliveryEvent"]
E --> F["推渠道发货信息到 OPS"]
E --> G["推 180601 出库事实到 OPS"]
这里有两个回写目标:
- 渠道履约信息:渠道关心发了哪些商品、多少数量、何时发货。
- 大客户事实单:OPS/SAP/NC 关心销售出库及财务事实。
因此只看到其中一个成功,不能判定整条链路完成。
19. 签收流程
19.1 请求示例
POST /inner/channel/order/confirm_receipt
Content-Type: application/json
{
"sceneCode": "dcd",
"merchantCode": "<商家编码>",
"shopId": "<门店编号>",
"signingPerson": "收货人",
"signingTime": "2026-07-16 16:00:00",
"signingNo": "SIGN-001",
"totalQty": 2,
"details": [
{
"orderNo": "<渠道订单号>",
"orderRowNo": "<渠道明细行号>",
"skuCode": "<SKU编码>",
"qty": 2
}
]
}
19.2 签收与发货的区别
| 维度 | 发货 | 签收 |
|---|---|---|
| 发起方 | 服务站 | 渠道/门店 |
| 库存变化 | 扣减 | 不再次扣减 |
| 事实单 | 生成 180601 | 记录签收凭据 |
| 数量字段 | shiped_num | t_channel_order_sign_detail.qty |
| 失败影响 | 不能完成履约 | 发货已发生但签收凭据缺失 |
flowchart LR
O["下单数量"] --> S["已发数量"]
S --> R["签收批次累计"]
R --> C{"累计签收 <= 已发?"}
C -->|"是"| OK["合法"]
C -->|"否"| ERR["数量异常"]
20. 渠道售后申请接口
20.1 请求示例
POST /inner/channel/aftersale/apply
Content-Type: application/json
{
"sceneCode": "dcd",
"merchantCode": "<商家编码>",
"shopId": "<门店编号>",
"orderNo": "<渠道原订单号>",
"sourceRefundNo": "<渠道售后申请号>",
"submitTime": "2026-07-16 17:00:00",
"totalAmount": "130.00",
"remark": "商品退回",
"details": [
{
"orderRowNo": 12345,
"sourceOrderRowNo": "ROW-001",
"skuCode": "<SKU编码>",
"qty": 1,
"price": "130.00",
"amount": "130.00",
"remark": ""
}
]
}
RefundApplyValidate 顶层 scene 中没有显式列出 details,但 BaseAftersale::apply() 实际依赖明细。修改校验器时必须用真实服务逻辑反查,不能只根据 scene 列表判断接口契约。
20.2 申请校验
flowchart TD
A["售后申请"] --> B["查渠道原单"]
B --> C["按 orderRowNo 查原明细"]
C --> D{"SKU/单价/小计匹配?"}
D -->|"否"| X["拒绝"]
D -->|"是"| E{"refund_num + qty <= order qty?"}
E -->|"否"| X
E -->|"是"| F{"申请 qty <= shiped_num?"}
F -->|"否"| X
F -->|"是"| G["生成 CHR 售后单"]
G --> H["累计原行 refund_num"]
H --> I["状态 pass,等待实物入库"]
20.3 申请数量占用
创建售后申请时立即执行:
channel_order_info.refund_num += abs(apply_qty)
这不是库存入库,而是占用“可申请售后数量”,防止同一已发数量被并发重复申请。
21. 服务站确认退货入库
21.1 请求示例
POST /scm/invCu/channelAftersalePass
Content-Type: application/json
{
"refundNo": "<DGJ渠道售后号>",
"delieverId": 1001,
"delieverName": "收货员",
"remark": "实物已验收",
"details": [
{
"rowNo": 67890,
"refundNo": "<DGJ渠道售后号>",
"locationId": 10000,
"locationName": "主仓",
"locationAreaId": 500001,
"locationAreaName": "退货区",
"remark": "外观正常"
}
]
}
21.2 为什么必须按原出库单拆单
一个渠道订单可以分多次发货:
渠道明细订购 10 件
第一次 180601-A 发 4 件
第二次 180601-B 发 6 件
渠道一次申请退 7 件
DGJ 销退必须关联原销售出库明细,因此 7 件要摊派:
180602-R1 -> 关联 180601-A,退 4 件
180602-R2 -> 关联 180601-B,退 3 件
flowchart TD
A["渠道退货申请 7"] --> B["查询该渠道明细对应的历史 180601 明细"]
B --> C["扣除历史已退数量"]
C --> D["按 bigOutId 分组"]
D --> E["依次摊派申请数量"]
E --> F["生成 180602-R1: 4"]
E --> G["生成 180602-R2: 3"]
21.3 通过流程
sequenceDiagram
participant U as 服务站
participant A as BaseAftersale
participant O as OPS/订单中心
participant DB as 渠道售后表
participant IC as InvCuService
participant INV as 库存
participant MQ as dgj_channel
U->>A: refund_pass
A->>DB: status 必须为 pass
A->>A: 校验明细并按原出库单拆分
A->>O: 先推售后审核通过
A->>DB: 开启本地事务
loop 每个原出库单
A->>IC: submitOrderV2(transType=180602)
IC->>INV: 退货入库
A->>DB: status=finish
end
A->>DB: 提交事务
A->>MQ: 每张 180602 发送 channel_refund_action
注意:OPS 审核结果推送发生在本地事务之前。若推送成功而本地销退失败,会出现外部认为通过、本地未入库的不一致,需要补偿而不是再次盲目审核。
22. 服务站拒绝售后
sequenceDiagram
participant U as 服务站
participant A as BaseAftersale
participant O as OPS
participant DB as MySQL
participant MQ as dgj_channel
U->>A: refund_reject(refundNo, remark)
A->>DB: 校验 status=pass
A->>O: 推送拒绝
A->>DB: 事务 status=reject
A->>DB: 原渠道明细 refund_num -= qty
A->>DB: 提交
A->>MQ: channel_refund_action(action=reject)
拒绝不产生 180602,也不改变实物库存;它只释放售后申请占用。
23. 全链路数量守恒
定义:
Q_order = 渠道订购数量
Q_ship = 渠道累计发货数量
Q_apply = 当前已占用售后申请数量 refund_num
Q_return = 已生成 180602 的退货数量
Q_sign = 累计签收数量
应满足:
0 <= Q_ship <= Q_order
0 <= Q_apply <= Q_ship
0 <= Q_return <= Q_ship
0 <= Q_sign <= Q_ship
拒绝售后后 Q_apply 应回退
flowchart LR
O["Q_order 订购"] -->|"发货"| S["Q_ship 已发"]
S -->|"申请占用"| A["Q_apply"]
A -->|"审核通过"| R["Q_return 已退"]
A -->|"审核拒绝"| B["释放 Q_apply"]
S -->|"签收"| G["Q_sign"]
23.1 金额守恒
| 对象 | 公式 |
|---|---|
| 渠道订单行 | sale_amount = qty * sale_price |
| 售后申请行 | amount = refund_qty * original_price |
| 大客户销退行 | amount = splitQty * original_price |
| 订单总额 | 各行金额求和,保留两位小数 |
接口按字符串两位小数比较时,前端和渠道应先统一舍入方式,避免浮点差异导致“小计有误”。
24. MQ 事件地图
目标 Topic:dgj_channel。
| 事件 | 生产时点 | 消费方法 | 失败策略 |
|---|---|---|---|
channel_order_manual_create | 本地人工平台单创建后 | channelOrderManualCreate | 推 OPS 失败返回 NACK |
channel_order_approve | 渠道审核后 | channelOrderApprove | 自动发货失败仍 ACK |
channel_order_delivery | 180601 发货成功后 | channelOrderDelivery | 非法/找不到事实单时 ACK 丢弃 |
channel_refund_action | 售后通过/拒绝后 | channelRefundAction | 参数缺失时 ACK 丢弃 |
flowchart LR
P["生产者"] --> T["DEST_DGJ_CHANNEL"]
T --> C["ChannelOrderNotify"]
C --> M["人工单推 OPS"]
C --> A["审核后自动发货"]
C --> D["发货/出库推 OPS"]
C --> R["售后事实推 OPS"]
24.1 消息幂等关注点
channel_order_manual_create重投必须按渠道单号幂等。channel_order_delivery重投不能重复通知渠道或重复创建事实单。channel_refund_action一张渠道退单可能对应多个180602 billNo,不能只按refundNo粗暴去重。handleBefore/handleAfter的事件记录只解决消费追踪,不自动保证下游幂等。
25. OPS、订单中心与 NC 对接
BigOrderProvider 主要封装以下业务接口:
| 常量/方法语义 | 业务用途 |
|---|---|
bigcustomer/order/submitorder | 推送大客户销售事实 |
bigcustomer/aftersale/dgj/save | 推送大客户售后事实 |
dgj/partner/order/delivery | 推渠道发货信息 |
dgj/partner/aftersale/confirm | 推渠道退货审核/入库结果 |
dgj/partner/purchase/part | 查询大客户结算价 |
flowchart TD
CH["渠道平台"] --> OC["OPS / 订单中心"]
OC --> DGJ["DGJ inner/channel"]
DGJ --> BO["180601 / 180602"]
BO --> OC
BO --> NC["NC / 财务链路"]
DGJ --> CH
生产排查必须区分:
- 渠道到订单中心失败。
- 订单中心到 DGJ 失败。
- DGJ 本地建单失败。
- DGJ 到订单中心回写失败。
- 订单中心到渠道回写失败。
- 大客户事实单到 NC 失败。
26. 普通、直采与临采
26.1 服务类型
serviceType | 名称 | 代码语义 |
|---|---|---|
1 | 普通 | 平台订单也归普通业务 |
2 | 直采 | 运费、供应关系等有独立处理 |
3 | 临采 | 受临采资格/白名单控制 |
26.2 临采白名单
相关对象:
- 表:
t_scm_big_lincai_whitelist。 - Redis:
RedisKeys::HASH_LINCAI_WHITELIST。 - 客户服务:
ContactService中支持临采的大客户类型查询。
flowchart TD
A["选择 serviceType=3 临采"] --> B{"服务站/大客户类型在白名单?"}
B -->|"否"| X["不展示或拒绝"]
B -->|"是"| C["允许临采开单"]
C --> D["仍需执行商品、价格、库存校验"]
临采白名单只决定能否使用该业务模式,不表示商品一定有库存,也不代表平台来源。
27. 服务站渠道配置
入口:
InvCu::channelGetConfig()。InvCu::channelSaveConfig()。SettingSer::getChannelOrderSetting()。SettingSer::setChannelOrderSetting()。
配置影响自动发货等站点级行为。排查时应同时确认:
| 层级 | 检查项 |
|---|---|
| Apollo | 场景全局约束、限价、一次发货 |
| 服务站配置 | 是否开启自动发货 |
| 大客户类型 | 商家编码、客户关系 |
| 临采白名单 | 是否允许临采 |
| 当前库存 | 仓库和区位是否可满足 |
28. 导入、复制与最近价格
28.1 导入模板
SceneConst::IMPORT_GOODS_TEMPLATE:
| Excel 列 | 必填 | 含义 |
|---|---|---|
* 物料编码 | 是 | sku_id |
* 数量 | 是 | sale_num |
单价(选填) | 否 | sale_price |
入口包括:
channelGoodsImport()。getImportTemplate()。application/Services/Import/Type/BigSaleGoodsService.php。
28.2 最近价格
ChannelOrderInfoModel::getSkuLastSalePrice() 只从 status=finish 的渠道订单中按 sid + contact_id + sku_code 取最新明细,可选再按 scene_code 过滤。
28.3 复制订单
- 只支持 FC/CZG。
- 复制的是开单输入,不应复制旧状态、已发量和售后量。
- 新单仍需重新校验结算价、库存、限价和客户绑定。
flowchart LR
O["历史完成订单"] --> P["最近价格"]
O --> C["复制订单草稿"]
E["Excel 模板"] --> I["导入明细"]
P --> N["新人工平台单"]
C --> N
I --> N
N --> V["重新校验"]
29. 核心查询 SQL
以下 SQL 仅作为排查模板。先确认环境和分片,不执行写操作。
29.1 按渠道外部单号查主单
SELECT id, sid, scene_code, merchant_code, source_shop_id,
order_no, source_order_no, status,
total_amount, cost_amount, profit_amount,
approve_user, approve_remark, create_time, update_time
FROM t_channel_order
WHERE scene_code = 'fc'
AND source_order_no = '<外部订单号>';
29.2 查渠道订单明细数量
SELECT id, chan_order_id, order_no, source_row_no, sku_code, inv_id,
qty, shiped_num, refund_num,
sale_price, cost_price, profit, sale_amount
FROM t_channel_order_info
WHERE order_no = '<DGJ渠道单号>'
ORDER BY id;
29.3 查对应大客户事实单
SELECT id, sid, billNo, transType, billStatus,
srcOrderSource, srcChannelOrder, srcOrderId, srcOrderNo,
serviceType, totalQty, totalAmount, amount, createTime
FROM t_scm_big_order
WHERE sid = <sid>
AND srcChannelOrder IN ('<CHS渠道单号>', '<CHR售后单号>')
ORDER BY id;
29.4 查事实单明细与原单关系
SELECT id, iid, skuId, invId, qty, price, amount,
srcOrderId, srcOrderNo, srcOrderEntryId
FROM t_scm_big_order_info
WHERE iid IN (<大客户事实单ID列表>)
ORDER BY iid, id;
29.5 查渠道售后
SELECT id, sid, scene_code, refund_no, source_refund_no,
order_no, source_order_no, status,
total_num, total_amount, apply_remark, audit_remark,
create_time, finish_time
FROM t_channel_aftersale
WHERE refund_no = '<CHR售后单号>'
OR source_refund_no = '<渠道售后号>';
SELECT id, refund_id, refund_no, source_refund_no,
order_row_no, source_order_row_no, sku_code,
qty, price, amount, cost_price
FROM t_channel_aftersale_info
WHERE refund_no = '<CHR售后单号>'
ORDER BY id;
29.6 查数量守恒异常
SELECT id, order_no, sku_code, qty, shiped_num, refund_num
FROM t_channel_order_info
WHERE sid = <sid>
AND (
shiped_num < 0
OR shiped_num > qty
OR refund_num < 0
OR refund_num > shiped_num
);
29.7 查签收批次
SELECT signing_no, order_no, order_row_no, sku_code,
qty, signing_person, signing_time, create_time
FROM t_channel_order_sign_detail
WHERE order_no = '<DGJ渠道单号>'
ORDER BY id;
30. 常用代码检索命令
rg -n "class (Order|Aftersale|Goods)" application/controllers/inner/channel
rg -n "function (create|delivery|confirm_receipt|order_approve)" application/Services/Channel
rg -n "function (apply|refund_pass|refund_reject|splitToRefundOrder)" application/Services/Channel
rg -n "STATUS_(CREATED|WAIT_SHIP|PART_SHIP|FINISH|CANCEL)" application
rg -n "STATUS_(PASS|REJECT)|ACTION_(PASS|REJECT)" application/Services/Channel application/controllers
rg -n "srcOrderSource|srcChannelOrder|serviceType|180601|180602" application/service/scm/InvCuService.php
rg -n "sendChannel(Order|Refund)|DEST_DGJ_CHANNEL" application
rg -n "dkh\.(max_price_rate|merchant_map|max_submit_row|once_delivery_merchant)" application
rg -n "LINCAI|lincai|临采" application
31. 排查 SOP:平台下单失败
flowchart TD
A["平台下单失败"] --> B{"BaseChannel 初始化成功?"}
B -->|"否"| C["查 sceneCode/shopId 绑定"]
B -->|"是"| D{"Validate 通过?"}
D -->|"否"| E["查必填字段/金额/明细"]
D -->|"是"| F{"source_order_no 已存在?"}
F -->|"是"| G["确认是否幂等返回原单"]
F -->|"否"| H{"SKU 与成本价存在?"}
H -->|"否"| I["查 Mongo 和成本服务"]
H -->|"是"| J["查事务/唯一键/数据库异常"]
检查顺序:
- 用
sceneCode + shopId验证服务站/修理厂绑定。 - 核对
orderNo是否已写入source_order_no。 - 核对每行
price * qty = amount。 - 查服务站 Mongo 是否存在 SKU。
- 查
CuOrderMaterielSer是否返回成本价。 - 查主表有而明细无,判断事务或批量写异常。
32. 排查 SOP:页面成功但 OPS 没单
- 查
t_channel_order是否存在,状态是否created。 - 查日志“平台人工开单消息事件触发异常”。
- 查
channel_order_manual_create是否生产。 - 查
ChannelOrderNotify消费结果,失败时应为NACK。 - 查
pushChannelManualCreateToOps()请求和响应。 - 确认 Apollo
merchant_map是否有该场景。 - 使用
rePushChannelOrderToOps()前先确认下游幂等。
flowchart LR
A["DGJ 有单"] --> B["MQ 有消息?"]
B -->|"无"| C["生产阶段失败"]
B -->|"有"| D["消费者成功?"]
D -->|"否"| E["NACK/重试"]
D -->|"是"| F["订单中心有记录?"]
F -->|"否"| G["查 Provider 响应与幂等"]
33. 排查 SOP:审核通过但未自动发货
- 查渠道订单是否从
created到wait_ship。 - 查
channel_order_approve消息是否消费。 - 查
scene_code,老消息缺失时消费者会从 DB 查询并默认回退 FC。 - 查服务站自动发货配置。
- 查
dkh.once_delivery_merchant是否要求一次发完。 - 查仓库、区位、实时库存能否覆盖全部明细。
- 查消费者日志“大客户自动发货异常”。
- 因异常被
ACK,确认是否需要人工发货或补偿任务。
34. 排查 SOP:发货失败或库存不一致
| 现象 | 首查 |
|---|---|
| “当前订单不能发货” | 渠道主单状态 |
| “超过可发数量” | qty - shiped_num |
| “需要一次性发完” | Apollo 一次发货场景 |
| 仓库/区位错误 | locationId/locationAreaId 与实时库存 |
| 有渠道状态无事实单 | submitOrderV2 事务和返回 |
| 有事实单但渠道仍待发 | shiped_num 更新是否成功 |
| 库存已扣但 OPS 无记录 | channel_order_delivery 消费与 Provider |
正确核对顺序:
渠道订单行 -> 180601 事实单行 -> 库存流水 -> 实时库存 -> MQ -> OPS
不要只看 t_channel_order.status 推断库存已经扣减。
35. 排查 SOP:售后申请失败
- 按
sourceRefundNo查是否重复申请。 - 查原渠道订单和
orderRowNo。 - 对比 SKU、单价、金额。
- 对比
qty、shiped_num、refund_num。 - 查是否申请量大于已发数量。
- 查事务后
refund_num是否已增加。 - 若主售后存在、明细缺失,查事务和批量插入。
flowchart TD
A["售后失败"] --> B{"原订单已发货?"}
B -->|"否"| X["不能退未发商品"]
B -->|"是"| C{"申请量 <= 已发-已占用?"}
C -->|"否"| Y["重复/超量申请"]
C -->|"是"| D{"原行与金额一致?"}
D -->|"否"| Z["渠道明细映射错误"]
D -->|"是"| E["查写库异常"]
36. 排查 SOP:售后通过但库存没增加
- 查
t_channel_aftersale.status是否仍为pass还是已finish。 - 查
splitToRefundOrder()是否找到历史180601明细。 - 查原出库明细已退数量,确认是否无剩余可退。
- 查生成了几张
180602,不要只按退单号期待一张。 - 查每张
180602的srcOrderEntryId。 - 查退货仓库/区位。
- 查库存流水与实时库存。
- 若 OPS 已收到通过、本地无
180602,按跨系统不一致处理。
37. 排查 SOP:拒绝后仍不能再次申请
- 查售后主单是否
reject。 - 汇总售后明细
qty。 - 查原渠道明细
refund_num是否已减回。 - 查拒绝事务是否回滚。
- 查是否存在其他尚未完成的售后占用。
- 禁止直接将
refund_num改为零,应按所有有效售后重新汇总。
推荐只读核对公式:
期望 refund_num = Σ(status in [pass, finish] 的售后明细绝对数量)
38. 已识别代码风险
| 编号 | 风险 | 可能后果 | 建议 |
|---|---|---|---|
| R44-01 | 一秒请求锁过短 | 延迟重试后重复建单 | 数据库唯一键 + 幂等返回 |
| R44-02 | source_order_no 幂等查询未显式带场景 | 不同渠道同号可能串单 | 确认唯一范围并补场景 |
| R44-03 | 人工单本地提交后 MQ 失败只记日志 | DGJ 有单、OPS 无单 | outbox/可查询补推状态 |
| R44-04 | 审核拒绝分支中 $sourceOrderNo 可能未初始化 | notice 或写入不确定值 | 分支前初始化 |
| R44-05 | 自动发货异常仍 ACK | 消息不再自动重试 | 业务补偿队列/失败状态 |
| R44-06 | 发货事实单与渠道累计更新为复合事务 | 跨服务副作用难原子 | 明确本地事务范围与补偿 |
| R44-07 | OPS 发货和事实单为两次推送 | 只成功一半 | 分别记录回写状态 |
| R44-08 | 售后申请立即占用 refund_num | 中间失败可能残留占用 | 确保同一事务并可重算 |
| R44-09 | checkRefundDetails 中退单号判断表达式疑似优先级错误 | 明细可能串售后单 | 改为严格不等并补测试 |
| R44-10 | 售后通过先推 OPS、后开本地事务 | 外部通过而本地未入库 | outbox/状态化补偿 |
| R44-11 | 多张销退单循环中反复更新主状态 | 部分成功时状态难解释 | 子任务状态与统一汇总 |
| R44-12 | 拒绝先推 OPS、后本地事务 | 外部拒绝而本地占用未释放 | 调整顺序或补偿 |
| R44-13 | refund_num 使用算术增减 | 并发和重复消费可能漂移 | 条件更新/版本号/重算 |
| R44-14 | 场景能力散落在子类抛异常 | 新场景容易漏能力声明 | 建立显式能力矩阵 |
| R44-15 | Apollo 缺配置采用默认值 | 行为静默变化 | 启动检查和配置监控 |
| R44-16 | TmpChannelOrder 内有固定预发 sid/用户 | 误调度有数据风险 | 标记环境并禁止生产注册 |
| R44-17 | 平台事实单禁止普通取消 | 错单只能走专门补偿 | 建立渠道撤单流程 |
| R44-18 | 签收与发货状态分离 | 已发未签长期悬挂 | 签收差异报表/超时提醒 |
39. 数据修复原则
39.1 禁止的修复方式
- 只改
t_channel_order.status=finish。 - 只改
shiped_num而不补180601和库存流水。 - 直接把
refund_num清零。 - 只改售后
status=finish而不生成180602。 - 重放 MQ 前不确认下游幂等。
- 将平台来源事实单伪装成
srcOrderSource=default后普通删除。
39.2 推荐修复顺序
flowchart TD
A["冻结目标单据"] --> B["备份主表/明细/库存快照"]
B --> C["计算期望数量和状态"]
C --> D["优先调用原业务补偿入口"]
D --> E["核对事实单和库存"]
E --> F["补发 MQ/外部回写"]
F --> G["记录审计与回滚 SQL"]
40. 日志定位关键词
| 业务 | 关键词 |
|---|---|
| 外部下单 | 平台订单创建异常、平台下单参数 |
| 人工建单 | 平台人工开单异常、平台人工开单消息事件 |
| 审核 | 审核平台异常、渠道方确认平台订单异常 |
| 自动发货 | 大客户自动发货异常 |
| 人工发货 | 平台订单发货异常、大客户库存变动通知 |
| OPS 回写 | 推送大客户发货信息到ops异常、大客户未到OPS单号 |
| 售后申请 | 退货申请异常 |
| 售后拆单 | 拆单结果、退货时拆单异常 |
| 售后入库 | 平台退货确认入库异常 |
| 售后拒绝 | 平台售后单拒绝异常 |
日志检索时至少带一个业务唯一号:source_order_no、CHS、billNo、source_refund_no 或 CHR。
41. 监控指标建议
| 指标 | 告警条件 |
|---|---|
created 待审核时长 | 超过渠道约定时限 |
wait_ship 待发货时长 | 超过服务站履约时限 |
part_ship 悬挂时长 | 长期无后续发货 |
渠道单无 180601 | 状态已发但无事实单 |
180601 无库存流水 | 事实单与库存不一致 |
refund_num > shiped_num | 数量守恒破坏,立即告警 |
pass 售后时长 | 长期未确认入库/拒绝 |
| OPS 回写失败数 | 连续失败或积压 |
| MQ ACK 丢弃数 | 参数缺失或事实单不合法 |
42. 单元测试清单
42.1 订单
- 同一外部订单重复请求返回原单。
- 不同场景相同外部单号不串单。
- 缺 SKU、成本价、结算价分别失败。
- 金额小计不等失败。
- 人工价格超过场景上限失败。
- 明细重复、超过最大行数失败。
- FC/CZG 人工建单成功,DCD 明确拒绝。
- 只有
created可审核。 - 审核通过和拒绝分别推进正确状态。
42.2 发货
- 待发货整单发货生成一张
180601。 - 允许部分发货场景正确进入
part_ship。 - 最后一批发货后进入
finish。 - 一次发货场景拒绝部分发货。
- 并发发货不超过订单数量。
- 仓库/区位库存不足不生成事实单。
42.3 售后
- 不能退未发数量。
- 不能超过可售后数量。
- 重复售后号幂等。
- 申请成功增加
refund_num。 - 拒绝成功释放
refund_num。 - 一次售后跨两张原出库单时生成两张
180602。 - 重复确认入库不重复增加库存。
- 退货明细不能串到其他
refundNo。
43. 集成回归清单
43.1 DCD 外部下单链
- 查询商品。
- 外部创建订单。
- 服务站查看待发货。
- 整单发货并生成
180601。 - OPS 收到发货与事实单。
- 渠道签收。
- 渠道申请退货。
- 服务站确认入库并生成
180602。
43.2 FC/CZG 人工单链
- 导入/搜索商品。
- 校验结算价、限价和库存。
- 本地创建
created订单。 - MQ 推 OPS。
- 渠道审核通过。
- 自动或人工发货。
- 验证渠道、OPS、NC 和库存。
43.3 临采链
- 无白名单时隐藏或拒绝。
- 加白名单后可选
serviceType=3。 - 保存、提交、库存和成本均正确。
- 移除白名单不影响历史单查询。
44. 上线前回归矩阵
| 变更点 | 最少回归 |
|---|---|
SceneConst | 三渠道商品、下单、审核、发货 |
BaseChannel | 三渠道门店绑定与错误提示 |
BaseOrder | 整单、部分、重复、一次发货、签收 |
ChannelOrderTrait | FC/CZG 人工单、审核、复制、自动发货 |
BaseAftersale | 申请、拆单、通过、拒绝、重复操作 |
InvCuService | 自制/平台来源、180601/180602、库存、OPS/NC |
| MQ 事件 | 四种事件 ACK/NACK、重投和下游幂等 |
| 临采白名单 | 缓存命中、失效、历史数据 |
| Apollo 配置 | 缺失、灰度、回滚和多场景差异 |
45. 待环境确认项
以下内容不能仅凭仓库代码最终确认,上线或事故复盘时应补入环境事实:
t_channel_order的生产唯一索引是否覆盖scene_code + source_order_no。t_channel_aftersale的唯一索引是否覆盖scene_code + source_refund_no。t_channel_order_sign_detail的签收幂等唯一键。- 各环境实际
dkh.*Apollo 配置值及变更负责人。 - 服务站渠道自动发货配置的真实存储位置和默认值。
created售后状态是否仍有线上流量,当前apply为何直接写pass。- OPS/订单中心各接口的最终幂等键和超时重试策略。
channel_order_delivery和channel_refund_action下游是否支持重复消息。- NC 同步任务调度频率、失败表和人工补推入口。
- 临采白名单 Redis 与 MySQL 的刷新/失效机制。
- DCD/FC/CZG 当前各自是否允许部分发货。
- 签收累计量与已发量是否有数据库级约束。
- 历史渠道单号生成规则及全局唯一性。
- 预发
TmpChannelOrder是否被任何生产调度误引用。
46. 一页式业务脉络
flowchart TD
A["渠道商品查询 / 人工选品"] --> B["渠道下单意图"]
B --> C{"外部直接下单还是人工代建"}
C -->|"外部"| D["创建渠道订单"]
C -->|"人工"| E["结算价+限价+库存校验"]
E --> F["created 待渠道审核"]
F -->|"拒绝"| X["cancel"]
F -->|"通过"| G["wait_ship"]
D --> G
G --> H["选择仓库/区位/数量"]
H --> I["生成 180601 大客户销售单"]
I --> J["扣库存"]
I --> K["part_ship / finish"]
I --> L["MQ 推 OPS/渠道/NC"]
K --> M["渠道签收"]
K --> N["渠道售后申请 CHR"]
N --> O{"服务站审核"}
O -->|"拒绝"| P["释放 refund_num"]
O -->|"通过"| Q["按原 180601 拆单"]
Q --> R["生成一张或多张 180602"]
R --> S["退货入库"]
R --> T["MQ 回写售后结果"]
47. 结论
大客户渠道链路的核心不是某一个接口,而是三层事实保持一致:
- 渠道业务事实:谁下单、谁审核、发了多少、签收多少、申请退多少。
- DGJ 库存事实:
180601扣了什么库存,180602又退回了什么库存。 - 外部系统事实:OPS、订单中心、渠道和 NC 是否都收到同一个最终结果。
排查任何问题都应沿以下路径逐层确认:
外部编号
-> 渠道主单/明细状态
-> 180601/180602 事实单
-> 库存流水与实时库存
-> MQ 事件
-> OPS/订单中心/渠道/NC 回写
只修状态、不修事实单;只修事实单、不修库存;只补本地、不补外部,都会留下下一次更难解释的不一致。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 渠道订单 | /inner/channel/Order | channel、外部单、站点、商品、地址 | Channel Order Controller | BaseOrder + Dcd/Fc/Czg Service | channel+source order | 渠道主明细和内部履约单 |
| 临采/大客户 | OPS scm/InvCu/BigOrder | 客户、需求、采购/销售转换参数 | InvCu Controller | InvCuService/BigCustomerService | big/temporary order | 形成临采事实和订单中心关系 |
| 售后 | /inner/channel/Aftersale | 原渠道单、SKU、申请/退款量 | Aftersale Controller | BaseAftersale | source aftersale + original order | 售后、退货和退款 |
| MQ/迁移 | ChannelOrderNotify/TmpChannelOrder | action/event、业务键、批次 | Task Consumer | Channel Service | message/batch + source order | 异步发货审核或历史数据补齐 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 接入 | Channel Controller | request_id、channel、source order/aftersale | ACK/返回本地单号 | 签名、重复、商品映射失败 | 外部单号查 CHANNEL 表 | | 事实单转换 | BaseOrder/InvCu/BigCustomer | 渠道/大客户单、内部 order/invoice/PO | 关系完整且数量一致 | 一边 commit、一边失败 | 关系字段串联事实单 | | 履约库存 | Order/Invoice/Inventory Service | internal bill、SKU、transType | 出入库和库存同向 | 状态变但无事实单/库存 | 单号查流水和实时量 | | 外部通知 | MqSer/ChannelNotify/OrderCenter Provider | message ID、source/internal bill | 对方 ACK 且状态推进 | 重复、NACK、外部已成功本地失败 | 外部/内部单号双向检索 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 接收订单 | BaseOrder/BigCustomerService | 接入事务 | 来源唯一性、客户商品映射 | CHANNEL/大客户主明细 | insert;status created;orderQty=n | source order+SKU |
| 转事实单 | BaseOrder/InvCuService | 跨域事务需验证 | 渠道需求、供给/价格/地址 | 采购/销售/出库事实单、关系 | internal bill 写入;status -> wait_ship | source/internal 双向关系 |
| 履约 | 销售/采购/库存 Service | 领域事务 | 可发/可收/可退量 | invoice/PO/inventory | shipped/received/return +n;库存 -/+n | 每 SKU 数量守恒 |
| 售后退款 | BaseAftersale | 售后事务+外部异步 | 原履约、可退量、原支付 | aftersale/return/refund/MQ | refund/return +n,状态完成一次 | original/aftersale/refund IDs |
| 迁移补偿 | TmpChannelOrder/领域补偿 | 小批幂等 | 缺失关系/状态与真实事实单 | 关系/状态/MQ | 只补缺口;不重造事实单/库存 | batch、before/after、重跑 0 变化 |
子模块追踪:big-goods 大客户渠道商品查询
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 商品查询 | 渠道按客户/站点查商品 | channel/customer、sid、query/SKU | application/controllers/inner/channel/Goods.php -> application/Services/BigCustomer/BigCustomerService.php | 客户映射、商品状态、渠道价、库存和服务类型 | 查询只读 不写;返回渠道可售快照 | request ID + channel/customer + SKU + hit count | 查不到分层核对映射/价/库存;不直接改渠道订单 |
| 导入范围 | 批量维护大客户销售商品 | file/task、rowNo、customer/SKU | application/Services/Import/Type/BigSaleGoodsService.php | 模板、客户商品、重复和现有范围 | 校验不写;每批本地事务 old goods set -> imported | task + row/error/affected counts | 只重跑失败键;commit 后缓存/搜索刷新 |
子模块追踪:big-order-create 外部渠道下单
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 接收订单 | 渠道 API/MQ 创建订单 | source/channelOrderNo、customer、items | application/controllers/inner/channel/Order.php -> application/Services/Channel/BaseOrder.php | 来源唯一性、客户商品映射、价格地址和服务类型 | 接入本地事务 insert CHANNEL 主明细,status none -> created | request/message ID + source/channel order + item count | 重复来源返回既有单;校验失败零写入 |
| 转内部单 | 渠道单生成销售/采购事实单 | channelOrderNo、internal billNo | application/Services/BigCustomer/BigCustomerService.php | 渠道需求、供给、价格、地址和已有关系 | 跨域本地事务创建 internal bill/双向关系,渠道 created -> wait_ship | channel/internal billNos + relation | 部分失败回滚/补关系,不重复生成已存在事实单 |
子模块追踪:big-manual 服务站人工代建订单
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 人工代建 | OPS/站点为渠道手工创建 | operator、channel/customer、source key、items | application/controllers/scm/InvCu.php -> application/service/scm/InvCuService.php | 操作权限、客户商品、来源唯一性和代建标记 | 大客户本地事务 insert 渠道/事实关系,status created/wait audit | request ID + operator + channel/internal bill | 人工来源也必须稳定幂等键;不能绕过渠道价/范围校验 |
| 代建回查 | 页面成功渠道未识别 | channel/internal billNos、manual flag | application/KzData/Enums/BigOrderEnums.php | 主明细、双向关系、创建人和来源类型 | 查询只读 不写 | both billNos + source/manual type | 已有内部单只补渠道关系/通知,不重建事实单 |
子模块追踪:big-audit 渠道审核与自动发货决策
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 审核 | 人工/渠道订单审核 | channelOrderNo、action、operator | application/controllers/tasks/ChannelOrderNotify.php | created/待审、客户规则、事实单和库存供给 | 审核本地事务 created -> wait_ship/cancel/reject | message/request ID + channel order + action | 终态旧请求 0 变化;拒绝不触发库存 |
| 自动决策 | 审核后判断自动发货/待处理 | order/service type、stock/scene | application/Services/Channel/SceneConst.php -> application/Services/Channel/Traits/ChannelOrderTrait.php | 普通/直采/临采场景、库存、事实单状态 | 决策查询只读;满足时调用领域出库事务 | channel/internal bill + scene/decision | 自动分支必须窄守卫;库存/外部不确定转人工 |
子模块追踪:big-ship 渠道发货与库存事实单
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 发货出库 | 渠道/人工确认发货 | channel/internal/invoice billNos、SKU、qty | application/service/scm/InvCuService.php | 可发量、事实销售/采购单、库存四维和当前态 | 履约与库存本地事务 shippedQty +n、库存 qty -n,渠道到 part_ship/finish | request/message ID + all billNos + SKU/transType | 超发/重复零增量;返回异常先查 invoice/库存流水 |
| 库存回查 | 渠道发货有状态无库存单 | invoice/inventory billNo、SKU | application/Services/Channel/BaseOrder.php | 出库事实单、库存流水/实时和渠道累计发货 | 查询只读;三方数量守恒 | channel/invoice/inventory IDs | 只补缺失领域段,不直接改渠道状态代替库存 |
子模块追踪:big-sign 签收与异步回写
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 签收回调 | 渠道/TMS 签收事件 | message ID、channel/delivery/invoice IDs | application/controllers/tasks/ChannelOrderNotify.php | 配送/出库关系、当前渠道态和事件版本 | 单消息本地事务 wait/part_ship -> finish,保存签收事实 | message ID + all IDs + sign time | 乱序/重复不回退终态;签收不重复扣库存 |
| 回写渠道 | 本地签收后通知外部 | channel/internal order、status | application/Providers/OrderCenter/BigOrderProvider.php | 已提交本地终态和发送记录 | 外部调用事务外;本地同步态 pending -> success/failed | request ID + channel/internal + provider code | 失败只补回写,不重做签收/库存 |
子模块追踪:big-aftersale 渠道售后申请与审核
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建售后 | 渠道发起退货/退款申请 | channelOrder/aftersaleNo、SKU、qty/amount | application/controllers/inner/channel/Aftersale.php -> application/Services/Channel/BaseAftersale.php | 原履约、可退量/金额、已有售后和服务类型 | 售后本地事务 insert,status none -> pending | request ID + original/aftersale + SKU | 超退/重复来源零写入;售后对象独立于正向状态 |
| 审核 | 人工/渠道审核售后 | aftersaleNo、action、operator | application/controllers/tasks/ChannelOrderNotify.php | pending、当前可退事实和审核权限 | 本地事务 pending -> approved/rejected | message/request + aftersale + action | 通过后库存/退款分两条链;失败不重复审核 |
子模块追踪:big-return 渠道退货入库
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 退货入库 | approved 售后收货 | aftersale/return billNo、SKU、location、qty | application/Services/Channel/BaseAftersale.php | 可收/已收量、原出库和库存维度 | 退货与库存本地事务 received +n、库存 qty +n、售后状态推进 | request ID + aftersale/return/inventory IDs | 超收/重复回滚;资金退款独立验收 |
| 退款回写 | 退货后退款/渠道通知 | original/refundNo、channel aftersale | application/Services/Mq/MqSer.php | 原支付、已退额、退货终态和发送状态 | 资金本地事务 refunded +n;渠道通知 commit 后 | original/refund/aftersale + message ID | 库存成功只补退款/通知;不重复入库 |
子模块追踪:big-service-type 普通、直采与临采
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 场景判定 | 渠道建单/审核选择服务类型 | channel order、service type、supply scene | application/Services/Channel/SceneConst.php | 客户合同、商品供给、库存和类型白名单 | 判定查询只读;创建时将 service type 固化到快照 | request ID + channel order + scene/type | 未知类型零写入;不能运行中静默换类型 |
| 差异履约 | 普通/直采/临采各自生成内部单 | channel/internal bill、type | application/Services/Channel/BaseService.php | 固化类型、采购/销售路径和状态机 | 对应领域本地事务创建所需事实单 none -> created | channel/internal + type + source relation | 修改共享 BaseService 时三类都回归;补偿按原类型执行 |