本文说明 DGJ2.0 从“销售已出库或采购已下单”到“接单、取货、轨迹、送达、签收、取消、退回”的完整履约链路。

这里最容易产生误解的地方是:代码中的“配送”不是一套系统,而是四条业务链同时存在。

链路发起业务主系统DGJ 本地主要凭据单号特征
商家自配送销售出库后由服务站员工配送TMS销售出库单 delivery_no、关系表KZPS...
三方配送销售出库后调用达达/美团等运力TMS 三方物流聚合配送订单表、关系表、销售出库单SPS...
急件自提服务站采购时到供给仓自提调拨中心采购单扩展表采购类型 30-Cxx-70
采购配送履约普通采购由调拨中心/WMS 出库配送调拨中心、TMS、WMS采购出库流水采购单、配送单、箱号、运单号
结论:排查问题时必须先识别业务链和单号类型。只查 t_scm_third_party_delivery_order 无法覆盖商家自配送和采购履约,只查销售出库单也无法解释急件自提地址。

1. 业务目标

配送域承担以下职责:

  1. 将销售出库单转换为可执行的配送任务。
  2. 支持服务站自有骑手接单、改派、轨迹上报、到达和签收。
  3. 支持向三方运力询价、下单、取消、退回确认和轨迹查询。
  4. 将 TMS 状态转换为 DGJ 销售出库状态。
  5. 接收调拨中心采购出库、预计到货、急件自提和退货关闭消息。
  6. 保存 DGJ 单据与外部配送单、运单、箱号之间的可追踪关系。
  7. 在撤销出库、撤销对账、取消配送等逆向流程中恢复正确业务状态。
flowchart LR
  subgraph DGJ["DGJ2.0"]
    SO["销售订单"] --> SI["销售出库单"]
    PO["采购订单"] --> POX["采购扩展/出库流水"]
    SI --> REL["配送关系"]
  end
  subgraph TMS["TMS/运力中心"]
    SELF["商家自配送 KZPS"]
    THIRD["三方配送 SPS"]
    TRACK["骑手与轨迹"]
  end
  subgraph DC["调拨中心/WMS"]
    STOCK["采购出库"]
    PICK["急件自提信息"]
    EXPRESS["箱号/运单/预计到货"]
  end
  REL --> SELF
  REL --> THIRD
  SELF --> TRACK
  THIRD --> TRACK
  STOCK --> POX
  PICK --> POX
  EXPRESS --> POX

2. 适用场景

本文适用于以下研发、联调和线上排查场景:

场景优先阅读章节
出库后没有生成配送单10、11、26
骑手看不到待配送任务12、13、27
接单后出库单状态不对14、22、28
轨迹不更新15、27
三方配送询价失败或余额不足16、17、29
三方下单成功但 DGJ 没记录18、24、30
取消后仍显示派送中19、22、28
三方退回无法确认20、29
急件自提地址未显示21、31
采购单未进入配送中23、32
运单号或预计到货时间缺失24、32
撤销对账后状态错误25、28
修改配送公共逻辑的回归评估33、34、35

3. 先建立统一业务语言

3.1 容易混用的名词

名词本文定义代码中的主要字段
销售订单客户购买商品的业务订单t_scm_sa_order*
销售出库单商品实际从服务站库存出库的单据t_scm_sa_invoice_0_n.billNo
商家自配送服务站自己的配送员履约,由 TMS 管任务和轨迹delivery_no=KZPS...
三方配送达达、美团等外部运力履约delivery_order_no=SPS...
配送关系销售出库单与 TMS 配送单的桥接记录t_sa_invoice_delivery_relation
急件自提服务站采购后自行到供给仓取货orderType=30-Cxx-70
采购出库流水调拨中心/WMS 已经从供给仓发出的行级事实t_scm_po_out_info_0_n
到达骑手到收货点或执行到达动作,不等同于财务对账TMS arrive
签收/送达履约完成,销售出库状态通常变为 3 已送达TMS 90、DGJ 3

3.2 三类“自提/自配送”不能混为一谈

名称是谁取/送发生在哪一侧核心实现
商家自配送服务站骑手送到修理厂销售侧SelfDeliverySer
急件自提服务站去供给仓取采购货物采购侧PoOrderEnums::ORDERTYPE_SELF_PICK_ORDER
三方退回三方骑手把拒收商品退回门店销售逆向ThirdDeliverySer::backConfirm
flowchart TD
  Q{"看到“自提/自配送”"}
  Q -->|"销售出库单、KZPS"| A["商家自配送"]
  Q -->|"采购单、30-Cxx-70"| B["急件自提"]
  Q -->|"SPS、backStatus"| C["三方配送退回"]
  A --> A1["查 TMS 自配送和销售出库"]
  B --> B1["查调拨中心回调和采购扩展表"]
  C --> C1["查三方配送订单、退回状态和确认接口"]

4. 系统边界与责任归属

系统/模块负责什么不负责什么
DGJ 销售域出库单、配送员引用、配送单号、单据状态骑手实时轨迹的主存储
DGJ 配送服务参数组装、关系落库、状态翻译、入口鉴权外部配送任务调度算法
TMS 自配送配送任务、接单、改派、轨迹、送达DGJ 财务对账与核销
TMS 三方物流聚合询价、运力下单、取消、三方状态和轨迹DGJ 销售库存扣减
调拨中心采购订单履约、仓库出库、自提信息DGJ 销售配送
WMS箱号、运单和仓库出库事实最终客户收款
RabbitMQ异步投递状态和物流事件业务幂等本身
Redis白名单、APP 选店、未匹配运单临时补偿配送主状态事实
flowchart TB
  UI["PC/APP/AI 控制台"] --> CTRL["Controller / app apiCode"]
  CTRL --> SER["DGJ Service"]
  SER --> DB[("DGJ MySQL")]
  SER --> TMS["TMS/运力中心 HTTP"]
  DC["调拨中心/WMS"] --> MQ["RabbitMQ"]
  TMS --> MQ
  MQ --> TASK["DGJ CLI Consumer"]
  TASK --> DB
  TASK --> REDIS[("Redis 补偿缓存")]

5. 代码入口地图

5.1 Web、APP 和 AI 入口

入口文件作用
PC 三方配送application/controllers/sale/ThirdDelivery.php白名单、地址、询价、下单、详情、取消、退回
PC 商家自配送application/controllers/sale/SelfDelivery.php创建、取消、详情
APP 配送application/config/appapis.phptransport* API Code 映射
APP API 实现application/service/api/app/transport/*.php列表、接单、到达、签收、取消、轨迹、选店
AI 控制台application/controllers/aiConsole/Courier.php骑手列表、补推 TMS、按单号查详情、收货人列表
销售出库页面application/views_v2/scm/invSa/deliveryList.php配送列表与三方配送组件入口
AI IM 工作台application/views_v2/settings/ai-im-workspace.php从客户会话发起三方配送
配送地图application/views_v2/settings/delivery-map.php骑手、修理厂和轨迹地图

5.2 核心服务与 Provider

层文件关键职责
商家自配送编排application/Services/Transport/SelfDeliverySer.php从销售出库构建 TMS 请求、保存单号和关系
TMS 统一适配application/Services/Transport/TransportSer.php自配送 API 包装、列表格式化、状态回写
TMS Providerapplication/Providers/OpsCenter/Tmsprod/TmsProvider.php调用 /tmsprod/tramgr/*
三方配送编排application/Services/SaOrders/ThirdDeliverySer.php询价、下单、取消、地址、轨迹、退回
三方 Providerapplication/Providers/OpsCenter/ThirdLogistics/ThirdLogisticsProvider.php调用 /tmsprod/zgj/third/logistics/v2/*
配送状态消费者application/controllers/tasks/ExpressNoSyncNotify.phpTMS 自配送、三方、运单、预计到货事件
调拨中心消费者application/controllers/tasks/DispatchCenterNotify.php采购出库、自提、预计到货、退货关闭
运单补偿任务application/controllers/tasks/ExpressNoSupplement.php未匹配运单号的延迟补偿

5.3 枚举和模型

文件内容
application/KzData/Enums/TransportEnums.php自配送状态、单号前缀、地图和收发方类型
application/KzData/Enums/ThirdDeliveryEnums.php三方渠道、配送、退回状态
application/KzData/Enums/SaInvoiceEnums.php销售出库状态
application/KzData/Enums/PoOrderEnums.php采购状态和急件自提类型
application/KzData/Enums/MqEventEnums.phpTMS、调拨中心 Routing Key
application/KzData/Enums/RedisKeys.php白名单、APP 选店、运单补偿 Key
application/models/saOrders/ThirdPartyDeliveryOrderModel.php三方配送订单 CRUD
application/models/saOrders/ThirdPartyDeliveryRelationModel.php出库单与配送单关系
application/models/scm/PoOrderExtModel.php急件自提信息

6. API 入口字典

以下为代码中的逻辑入口。APP 的对外 URL 由统一 API 分发器和部署路由决定,联调时以当前环境网关为准;apiCode 与处理类映射是仓库可确认事实。

6.1 APP 配送 API Code

apiCode处理类用途关键参数
transportStoreapp.transport.store查询/切换门店user_id、store_id
transportListapp.transport.transportList配送列表sid、senderCode、courierCode、transportStatus
transportCancelListapp.transport.transportCancelList中止配送列表sid
transportCourierStatusCountapp.transport.transportCourierStatusCount骑手状态统计sid、senderCode、courierCode
transportTakingapp.transport.transportTaking骑手接单sid、uid、outShipmentNo
transportArriveapp.transport.transportArrive到达收货点TMS 配送参数
transportReceiptapp.transport.transportReceipt签收TMS 配送参数、签收凭据
transportCancelapp.transport.transportCancel中止/改派sid、outShipmentNo、changeCourier
trasnportOtherCourierListapp.transport.otherCourierList可改派人员sid、uid
courierReportRegisterapp.transport.courierReportRegister注册轨迹设备骑手、手机号、deviceId
courierReportapp.transport.courierReport上报轨迹TMS 要求的坐标和设备字段
courierConfigapp.transport.courierConfig查询骑手配置sid
updateStoreLocationapp.transport.updateStoreLocation更新门店坐标门店和坐标字段
updateContactLocationapp.transport.updateContactLocation更新修理厂坐标客户和坐标字段

6.2 PC 商家自配送接口

HTTP 逻辑路径方法请求重点成功结果
/sale/SelfDelivery/createPOSTsaInvoiceId、delieverId、remarkdelivery_no、saInvoiceId
/sale/SelfDelivery/cancelPOSTsaInvoiceId、cancelReasonTMS 删除结果
/sale/SelfDelivery/detailPOSTsaInvoiceIdTMS 配送详情

6.3 PC 三方配送接口

HTTP 逻辑路径方法说明
/sale/ThirdDelivery/canDeliveryPOST查询站点是否在 Redis 白名单
/sale/ThirdDelivery/updateStoreAddressPOST同步门店地址到运力中心并保存本地
/sale/ThirdDelivery/updateContactAddrPOST更新客户省市区、地址和经纬度
/sale/ThirdDelivery/updateContactAddrInfoPOST更新收货人姓名和手机号
/sale/ThirdDelivery/inquiryPOST根据出库单询价
/sale/ThirdDelivery/createPOST使用 inquiryNo 和运力能力下单
/sale/ThirdDelivery/detailPOST按出库单查配送详情和轨迹 URL
/sale/ThirdDelivery/cancelCheckPOST查询能否取消及违约金
/sale/ThirdDelivery/cancelReasonsPOST查询可选取消原因
/sale/ThirdDelivery/cancelPOST执行三方取消并回退出库状态
/sale/ThirdDelivery/outCancelCheckPOST撤销出库前校验配送单
/sale/ThirdDelivery/backConfirmPOST三方退回到店后确认收货

6.4 AI 控制台接口

路径作用注意点
/aiConsole/Courier/lists查询配送员轨迹列表数据来自 StaffService
/aiConsole/Courier/pushTmsTransportCreate手工补推 TMS接受完整 jsonData,属于高风险运维入口
/aiConsole/Courier/getTransportByTmsNo按 transNo 查详情用于补推后确认
/aiConsole/Courier/getTransportReceiverList查询收货人配送单要求 sid

7. 请求与响应示例

示例只展示业务字段,不包含域名、Cookie、Token 和生产数据。

7.1 创建商家自配送

POST /sale/SelfDelivery/create
Content-Type: application/json

{
  "saInvoiceId": 928001,
  "delieverId": 3102,
  "remark": "到店前电话联系"
}
{
  "status": "success",
  "data": {
    "delivery_no": "KZPS202607160000001",
    "saInvoiceId": 928001
  }
}

7.2 APP 查询待配送

{
  "apiCode": "transportList",
  "sid": "9999",
  "senderCode": "211",
  "courierCode": "1175",
  "transportStatus": "10",
  "offset": 1,
  "limit": 20
}

待配送响应被拆成:

字段含义
myDeliverList当前骑手已经关联的待配送单
otherDeliverList未分配或分配给其他骑手的待配送单
pageDeliveryList配送中/已完成时的分页列表
totalPagesTMS 返回总页数
totalCountsTMS 返回总记录数

7.3 APP 接单

{
  "apiCode": "transportTaking",
  "sid": 9999,
  "uid": 1175,
  "outShipmentNo": "XS9999202607160001",
  "transNo": "KZPS202607160000001"
}

7.4 轨迹设备注册与上报

{
  "apiCode": "courierReportRegister",
  "sid": 9999,
  "courierCode": "1175",
  "courierName": "配送员A",
  "courierMobile": "13800000000",
  "deviceId": "device-example-001"
}
{
  "apiCode": "courierReport",
  "sid": "9999",
  "courierCode": "1175",
  "deviceId": "device-example-001",
  "localType": "GAODE",
  "localX": 30.000001,
  "localY": 120.000001,
  "reportTime": "2026-07-16 14:30:00"
}
轨迹上报字段由 DGJ 原样转发给 TMS,最终契约应以当前 TMS 文档为准;仓库内 TransportSer::courierReport() 没有二次字段校验。

7.5 三方配送询价

POST /sale/ThirdDelivery/inquiry
Content-Type: application/json

{
  "sid": 9999,
  "saInvoiceId": 928001
}

核心响应结构:

{
  "customerAddress": {
    "contactName": "客户A",
    "mobile": "13800000000",
    "latitude": "30.000001",
    "longitude": "120.000001"
  },
  "storeAddress": {
    "storeNumber": "STORE001",
    "linkName": "门店联系人",
    "latitude": "30.100001",
    "longitude": "120.100001"
  },
  "saInvoiceId": 928001,
  "inquiry": {
    "items": [
      {
        "channel": "imdada",
        "channelName": "达达",
        "inquiryNo": "INQ-EXAMPLE",
        "deliveryAmt": 12.50,
        "balanceAmt": 100.00,
        "expiredTime": 1784185200,
        "recommend": 1,
        "code": 0
      }
    ]
  },
  "expiredTime": 1784185200
}

7.6 三方配送下单

POST /sale/ThirdDelivery/create
Content-Type: application/json

{
  "sid": 9999,
  "saInvoiceId": 928001,
  "inquiryNo": "INQ-EXAMPLE",
  "capacityIdList": ["CAPACITY-A"],
  "remark": "易碎品"
}
{
  "status": "success",
  "data": {
    "deliveryNo": "SPS202607160000001"
  }
}

7.7 三方状态 MQ 示例

{
  "deliveryNo": "SPS202607160000001",
  "deliveryStatus": "30",
  "backStatus": "00",
  "courierName": "骑手A",
  "courierMobile": "13800000000"
}

7.8 TMS 自配送状态 MQ 示例

{
  "sid": "9999",
  "bizType": "STATUS",
  "transNo": "KZPS202607160000001",
  "status": "20",
  "completeStatus": "NOTHING",
  "courierCode": "1175",
  "courierName": "配送员A",
  "courierMobile": "13800000000",
  "details": [
    {
      "itemCode": "10326742",
      "itemName": "示例商品",
      "unitName": "个",
      "qty": 1
    }
  ]
}

8. 核心表和分片规则

8.1 销售配送表

常量/逻辑表物理表作用关键字段
SCM_SA_INVOICEt_scm_sa_invoice_0_{sid % N}销售出库主单id、sid、billNo、srcOrderNo、billStatus、delieverId、delivery_no、storeId、buId
SCM_SA_INVOICE_INFO销售出库明细分表配送商品明细iid、invId、skuId、qty
SCM_THIRD_PARTY_DELIVERY_ORDERt_scm_third_party_delivery_order三方配送本地订单快照delivery_order_no、渠道、状态、骑手、收发地址、sid
SCM_SA_INVOICE_DELIVERY_RELATIONt_sa_invoice_delivery_relation出库单到配送单的桥接sa_invoice_no、sa_invoice_id、delivery_no、delivery_id、store_id、sid

关系表名称带“三方”,但当前代码也用它保存 KZPS 商家自配送关系。因此更准确的理解是“销售出库与配送关系表”。

erDiagram
  SCM_SA_INVOICE ||--|{ SCM_SA_INVOICE_INFO : contains
  SCM_SA_INVOICE ||--o{ SA_INVOICE_DELIVERY_RELATION : links
  THIRD_PARTY_DELIVERY_ORDER ||--o{ SA_INVOICE_DELIVERY_RELATION : referenced_by
  SCM_SA_INVOICE {
    bigint id
    bigint sid
    string billNo
    int billStatus
    bigint delieverId
    string delivery_no
  }
  THIRD_PARTY_DELIVERY_ORDER {
    bigint id
    bigint sid
    string delivery_order_no
    string delivery_channel
    string delivery_status
    string back_status
  }
  SA_INVOICE_DELIVERY_RELATION {
    bigint id
    bigint sid
    bigint sa_invoice_id
    string sa_invoice_no
    bigint delivery_id
    string delivery_no
    bigint store_id
  }

8.2 采购履约表

常量物理表用途关键字段
SCM_PO_ORDERt_scm_po_order采购主单id、billNo、billStatus、orderType、sid
SCM_PO_ORDER_INFO采购明细分表采购商品行id、iid、invId、skuId、qty
SCM_PO_ORDER_EXTt_scm_po_order_ext急件自提扩展iid、pick_connect、pick_mobile、pick_address、pick_time
SCM_PO_OUT_INFO采购出库流水分表调拨/WMS 出库事实srcOrderOutId、srcOrderEntryId、deliveryCode、boxNo、express_no、wmsNo、toArriveTime

8.3 索引要求

仓库没有发现建表 DDL,以下是根据查询模式推导的生产索引要求,必须在数据库环境确认,不能把它们当成已存在事实。

表建议唯一/普通索引原因
配送订单表唯一 delivery_order_noMQ 回调只按配送单号查询,无 sid
配送关系表唯一 (sid, sa_invoice_no, delivery_no, is_delete) 或等价约束防止外部重试产生重复关系
配送关系表索引 (delivery_no, sid, is_delete)所有状态回调的反查入口
配送关系表索引 (sa_invoice_no, sid, is_delete)出库详情和撤销查询
采购扩展表唯一 iidinsertPickInfo() 按采购单一对一更新/插入
采购出库流水唯一或强索引 (sid, srcOrderOutId)调拨出库消息以行号幂等

9. Redis 数据

Redis Key类型Field/Value用途主数据吗
HASH_THIRD_PARTY_DELIVERY_WHITE_LISTHashfield=sid是否开放三方配送否,功能开关
APP_TRANSPORT_SWITCH_STOREHashfield=uid,value=storeIdAPP 配送页当前门店否,界面偏好
TMS_WMS_NO_SYNC_HASHHashfield=sid,value=JSON暂存早于 DGJ 出库流水到达的运单映射否,补偿队列

9.1 白名单判断

ThirdDeliverySer::canDelivery() 执行:

HGET {PREFIX}hash_third_party_delivery_white_list {sid}

值只要在 PHP 中为真就开放。若运营配置写入字符串 0、空字符串或删除 Field,页面都会视为未开放。

9.2 APP 门店切换

HGET {PREFIX}app_transport_store {uid}
HSET {PREFIX}app_transport_store {uid} {storeId}

当前 store.php 会先按账号的 storeDefault + storeLever 查询可用门店,但写入 switchStoreId 前没有在方法内再次判断它是否属于可用集合。入口层或调用方是否限制,需要联调确认。

9.3 运单延迟补偿

sequenceDiagram
  participant TMS as TMS MQ
  participant C as ExpressNoSyncNotify
  participant DB as 采购出库流水
  participant R as Redis Hash
  participant J as ExpressNoSupplement
  TMS->>C: expressNo -> outExpressNo
  C->>DB: 按 express_no 查询
  alt 已有出库流水
    C->>DB: 更新 wmsNo
  else 消息早到
    C->>R: HSET sid JSON映射
  end
  J->>R: HGETALL
  J->>DB: 再按 express_no 查询
  J->>DB: 更新 wmsNo
  J->>R: 写回未匹配项

10. 三套状态机

10.1 销售出库状态

值枚举业务含义
0BILL_STATUS_RE_OUTED老销售退货兼容的已出库
1BILL_STATUS_OUTED已出库/待配送
2BILL_STATUS_SENDIND派送中
3BILL_STATUS_ARRIVE已送达
4BILL_STATUS_CHECK已对账
5BILL_STATUS_OVER已核销
6BILL_STATUS_CANCELED已撤销
stateDiagram-v2
  [*] --> 已出库
  已出库 --> 派送中: 接单/三方建单
  派送中 --> 已送达: TMS完成回调
  派送中 --> 已出库: 取消/退回
  已送达 --> 已对账: 财务对账
  已对账 --> 已核销: 收款核销
  已对账 --> 已送达: 撤销对账并按配送状态恢复
  已出库 --> 已撤销: 撤销出库

10.2 TMS 商家自配送状态

值含义对应 DGJ 出库状态
10待揽收通常 1 已出库
20配送中2 派送中
90已送达3 已送达
99已取消恢复 1 已出库,但消费者当前不处理该值
stateDiagram-v2
  [*] --> 待揽收: 创建 KZPS
  待揽收 --> 配送中: 骑手接单/取件
  配送中 --> 已送达: 到达并签收
  待揽收 --> 已取消: 删除配送任务
  配送中 --> 已取消: 中止配送

10.3 三方配送状态

值含义DGJ 回调动作
10待接单只更新配送订单状态
20待取货只更新配送订单状态
21骑手到店只更新配送订单状态
30配送中只更新配送订单状态;建单时已把出库改为 2
90已完成出库改为 3 已送达
98已退回出库改为 1 已出库
99已取消出库改为 1 已出库

退回状态另有一套维度:

字段值含义
back_type00无退回
back_type10收货方拒收
back_type20妥投异常
back_status00无
back_status10返回中
back_status20已返回
stateDiagram-v2
  [*] --> 待接单
  待接单 --> 待取货
  待取货 --> 骑手到店
  骑手到店 --> 配送中
  配送中 --> 已完成
  待接单 --> 已取消
  待取货 --> 已取消
  配送中 --> 已退回: 拒收/妥投异常
  已退回 --> [*]: 门店确认退回

10.4 状态转换不是简单同值映射

来源来源值目标值代码位置
自配送 MQ20出库 2ExpressNoSyncNotify::selfTransportStatus
自配送 MQ90出库 3同上
三方 MQ90出库 3thirdPartyDeliveryChangeStatus
三方 MQ98/99出库 1同上
撤销对账查询 TMS10/20/90/99出库 1/2/3/1InvSaService::resetSaInvoiceStatus

TransportSer::transportStatusChange() 会保护 4 已对账、5 已核销、6 已撤销 不被配送回调覆盖,但对 1/2/3 没有单调性判断,因此乱序消息仍可能把 3 已送达 回退到 2 派送中。


11. 商家自配送创建主流程

入口:SelfDelivery::create() -> SelfDeliverySer::create()。

11.1 前置条件

  1. Session 中存在合法 sid。
  2. saInvoiceId 对应的销售出库单存在。
  3. 传入配送员时,员工存在且已关联“配送员”角色。
  4. 配送员员工必须关联可用管理员账号,才能得到 TMS courierCode=adminUid。
  5. 门店、客户默认地址、销售出库明细能够查询。

11.2 创建步骤

sequenceDiagram
  actor U as PC操作人
  participant C as SelfDelivery Controller
  participant S as SelfDeliverySer
  participant DB as DGJ MySQL
  participant T as TMS
  U->>C: saInvoiceId, delieverId, remark
  C->>S: 注入 sid/uid/uname
  S->>DB: 查询销售出库单
  S->>DB: 校验员工与配送员身份
  alt 已有 KZPS
    S->>T: update(outOrderNo,outShipmentNo,courier)
  else 无 KZPS
    S->>DB: 查询门店/客户/商品/配送时限
    S->>T: create(完整收发货和商品信息)
    T-->>S: KZPS配送单号
  end
  S->>DB: 开事务
  S->>DB: 更新 invoice.delivery_no/delieverId
  S->>DB: 不存在时插入配送关系
  S->>DB: 提交
  S-->>U: delivery_no

11.3 发给 TMS 的字段来源

TMS 字段DGJ 来源
sourceSystem固定 DGJ
senceCode固定 tramgr_create_service_shipment
sid当前服务站
senderCode出库单 storeId
senderName服务站名称 + 门店名称
senderPerson/Mobile门店联系人、电话
senderLocation门店高德经纬度
receiverCode客户 contactId
receiverName客户名称
receiverPerson/Mobile默认收货地址联系人、电话
receiverAddress/Location客户默认地址和高德经纬度
courier配送员姓名、手机号、管理员 UID
outOrderNo出库单 srcOrderNo
outShipmentNo出库单 billNo
detailList出库明细 + 物料缓存中的标准名、单位
receiverDuration客户-门店配送时限;异常时默认 30 分钟

11.4 更新已有配送单

若销售出库单已保存 KZPS,再次创建不会新建 TMS 任务,而是调用 transportUpdate() 更新骑手。

这意味着“创建”接口同时承担“更新配送员”的语义。调用方必须根据返回和当前 delivery_no 判断是首次创建还是修改。


12. APP 配送列表与选店

12.1 门店范围

APP 先从管理员账号读取:

storeDefault + storeLever -> 去重 -> 过滤已删除/禁用门店

当前选店写入 Redis APP_TRANSPORT_SWITCH_STORE[uid]。

12.2 列表分支

flowchart TD
  A["transportList"] --> V{"transportStatus"}
  V -->|"10 待揽收"| W["固定拉取最多200条"]
  W --> L["补齐门店/客户经纬度"]
  L --> M["拆 myDeliverList"]
  L --> O["拆 otherDeliverList"]
  V -->|"20 配送中"| D["按 offset/limit 请求 TMS"]
  V -->|"90 已完成"| F["按 offset/limit 请求 TMS"]
  D --> P["pageDeliveryList"]
  F --> P

12.3 列表格式化

TransportSer::formatList() 会扫描 transportDetail.itemCode,批量查询商品分类,再将 TMS 明细格式化为 DGJ 页面所需结构。

待配送列表还会在 TMS 未保存坐标时,用 DGJ 门店和客户默认地址补齐坐标。

12.4 已知分页语义

配送中和已完成分支中:

page.number = intval(offset)
page.size   = intval(limit)

这里的 offset 实际是“页码”,不是 SQL 偏移量。前端改分页组件时不能直接传 (page-1)*limit。


13. 配送员身份和改派

配送员不是普通员工字段,而是三层关系:

flowchart LR
  STAFF["员工 Staff"] --> ROLE["员工角色关系 Courier Role"]
  STAFF --> ACCOUNT["adminAccount"]
  ACCOUNT --> ADMIN["管理员账号 uid"]
  ADMIN --> TMS["TMS courierCode"]
检查失败表现
员工未删除且未禁用“送货人账号不可用”
员工具有配送员角色无法在 APP 配送
员工 adminAccount 不为空无 TMS 骑手编码
对应管理员状态可用adminUid 为空或身份校验失败

改派入口 transportCancel 接收:

{
  "sid": 9999,
  "outShipmentNo": "XS9999202607160001",
  "changeCourier": {
    "courierCode": 1188
  },
  "cancelReason": "改由其他配送员配送"
}

前端传入的 changeCourier.courierCode 实际是管理员 UID。服务会反查员工 ID,并更新销售出库单 delieverId。


14. 接单、到达和签收

14.1 接单

TransportSer::transportTaking() 的顺序是:

  1. 校验当前 uid 是否有配送员身份。
  2. 按 outShipmentNo 查询销售出库单。
  3. 更新 delieverId。
  4. 将出库状态改为 2 派送中。
  5. 调用 TMS transport/taking。
sequenceDiagram
  actor R as 骑手
  participant A as APP API
  participant D as DGJ DB
  participant T as TMS
  R->>A: transportTaking
  A->>D: 校验员工/账号/角色
  A->>D: 写 delieverId
  A->>D: billStatus=2
  A->>T: taking
  T-->>A: 接单结果

14.2 到达和签收

transportArrive() 和 transportReceipt() 主要是 TMS 透传,DGJ 本地状态依赖后续 tmsprod_self_transport_status MQ 回调推进。

因此接口返回成功但页面状态暂未变化时,应先判断是短暂异步延迟,还是 MQ 消费失败。

14.3 重要一致性风险

接单流程先改 DGJ,再调用 TMS,并且没有事务包围外部调用。如果 TMS 接单失败,DGJ 可能已经显示 派送中 且写入配送员。

建议修复方向:

  1. 优先调用 TMS,成功后再更新本地;或
  2. 为本地写入增加 taking_pending/taking_failed 操作记录;或
  3. 失败时显式回滚 delieverId 和出库状态。

15. 轨迹链路

sequenceDiagram
  participant APP as 骑手APP
  participant DGJ as DGJ API
  participant TMS as TMS
  participant MAP as 配送地图/AI控制台
  APP->>DGJ: courierReportRegister(deviceId)
  DGJ->>TMS: 注册设备和骑手
  loop 配送过程中
    APP->>DGJ: courierReport(经纬度,时间)
    DGJ->>TMS: 原样转发轨迹
  end
  MAP->>DGJ: 查询骑手/配送详情
  DGJ->>TMS: courier/list 或 transport/getno
  TMS-->>MAP: 当前坐标和任务

15.1 坐标约定

字段含义
LocalX纬度 Latitude
LocalY经度 Longitude
localTypeGAODE 或 BAIDU

项目中发货方和收货方明确按高德坐标组装。若 APP 上报百度坐标却标成高德,地图会出现位置偏移。

15.2 轨迹失败检查顺序

  1. courierReportRegister 是否成功。
  2. deviceId 是否稳定且与骑手一致。
  3. APP 是否持续调用 courierReport。
  4. 坐标系和值域是否正确。
  5. TMS 是否保存并在查询接口返回。
  6. 地图页是否使用正确 courierCode/transNo。

16. 三方配送准入和地址准备

16.1 准入条件

三方配送入口是否显示由 Redis 白名单决定。业务下单还依赖:

数据必需字段
门店storeNumber、linkName、linkMobile、linkAddress、经纬度
客户默认地址联系人、手机号、详细地址、经纬度
客户客户编码/名称
出库单billNo、数量、金额、门店、客户

16.2 更新门店地址

updateStoreAddress() 先调用运力中心 store/update,再保存 DGJ 门店。

sequenceDiagram
  actor U as 操作人
  participant D as DGJ
  participant T as 三方物流中心
  participant DB as DGJ DB
  U->>D: 联系人/电话/地址/经纬度
  D->>DB: 查询门店和服务站名称
  D->>T: storeUpdate
  T-->>D: 成功
  D->>DB: updateStoreInfo

外部更新成功、本地更新失败时会不一致,当前没有补偿事务。

16.3 更新客户地址

updateContactAddr():

  1. 调用 ContactSer::saveAddressCode() 处理地址编码。
  2. 校验省、区和三级编码、详细地址、经纬度、客户 ID。
  3. 事务内新增或更新默认地址。
  4. 提交后调用 syncContactToSaas()。

注意:SAAS 同步发生在本地提交之后;同步失败时 catch 会调用 rollback,但数据库事务已提交,无法真正回滚。


17. 三方询价流程

17.1 参数组装

ThirdDeliverySer::inquiry() 按销售出库单查询门店和客户地址,向运力中心传递:

类别字段
业务单orderNo、totalQty、orderAmt
收货方姓名、手机号、客户编码、完整地址、经纬度
发货方门店编码、联系人、电话、地址、经纬度
业务控制默认重量 5、小费 0、操作人

17.2 报价选择

运力中心返回 items 后,DGJ 会:

  1. 取所有报价最小 expiredTime 作为整体过期时间。
  2. 取最小 deliveryAmt。
  3. 若 deliveryAmt > balanceAmt,标记 code=-1、message=余额不足。
  4. 最低配送费的报价标记 recommend=1。
flowchart TD
  A["读取出库单"] --> B{"门店和客户地址完整?"}
  B -->|否| E["当前代码进入空报价风险分支"]
  B -->|是| C["调用 v2/inquiry"]
  C --> D["遍历运力 items"]
  D --> F{"配送费 > 余额?"}
  F -->|是| G["code=-1 余额不足"]
  F -->|否| H["可选择"]
  G --> I["按最低配送费标推荐"]
  H --> I

17.3 当前空地址缺陷

地址不完整时,代码构造的是一个“数组中的默认报价”,但后面固定读取 $inquiryDataTmp['items']。这两种结构不一致,随后 min(array_column(...)) 也无法处理空列表。

正确行为应是明确返回“请先完善门店/客户地址”,而不是继续计算报价。


18. 三方配送下单和落库

18.1 切换配送方式限制

若销售出库单 delivery_no 以 KZPS 开头,三方下单直接拒绝:

若要变更为三方配送,请先在出库单详情,取消商家自配送

反向从 SPS 切换到自配送时,也应先取消三方配送并确认状态已恢复。

18.2 下单步骤

sequenceDiagram
  actor U as 操作人
  participant DGJ as ThirdDeliverySer
  participant TMS as 三方物流中心
  participant O as 配送订单表
  participant R as 配送关系表
  participant I as 销售出库分表
  U->>DGJ: inquiryNo, capacityIdList, saInvoiceId
  DGJ->>I: 查询出库单
  DGJ->>TMS: v2/create
  TMS-->>DGJ: deliveryNo/channel/thirdDeliveryNo/fetchCode
  DGJ->>DGJ: 开本地事务
  DGJ->>O: 插入三方配送快照
  DGJ->>I: billStatus=2
  DGJ->>R: 插入出库配送关系
  DGJ->>DGJ: 提交

18.3 本地配送订单保存内容

字段组内容
单号DGJ 聚合配送单号、三方平台单号、询价单号
渠道delivery_channel、delivery_channel_name
发货门店地址、联系人、电话
收货客户详细地址、联系人、电话
操作created_by、备注、sid

18.4 事务边界风险

运力中心 create 在本地事务之前执行。若外部下单成功而本地插入失败,会产生:

TMS 有 SPS 单号
DGJ 配送订单表无记录
DGJ 关系表无记录
销售出库单仍可能显示待配送

当前代码没有自动按 inquiryNo 或出库单号补偿落库。建议至少增加:

  1. 外部请求幂等键;
  2. 外部成功响应持久化日志;
  3. 按出库单查询 TMS 并重建本地关系的补偿任务;
  4. 配送订单号和关系唯一索引。

19. 三方取消、撤销出库和改派

19.1 普通取消

flowchart TD
  A["cancelCheck"] --> B{"允许取消?"}
  B -->|否| X["阻止或要求强确认"]
  B -->|是且有违约金| C["展示违约金/取消原因"]
  B -->|是且无违约金| D["直接允许"]
  C --> E["cancel"]
  D --> E
  E --> F["调用三方 v2/cancel"]
  F --> G["本地配送状态=99"]
  G --> H["销售出库状态=1"]

19.2 撤销出库前判断

outCancelCheck() 返回三类结果:

allowCancelStatus含义页面建议
1可取消且无违约金可继续
2可取消但存在进行中配送/违约信息先取消配送
3已完成、已取消、已退回或外部不允许取消强提示后由业务决定

19.3 本地与外部顺序

三方取消先调用外部,再分别更新本地配送订单和出库状态,未使用统一事务。如果第二步失败,外部已取消但 DGJ 仍显示派送中。

19.4 商家自配送取消有两种语义

入口TMS 动作DGJ 动作
TransportSer::transportCancel中止/改派配送员更新 delieverId,不直接清空 delivery_no
SelfDeliverySer::cancel删除配送任务清空 delivery_no,出库恢复 1

排查“取消失败”时先确认页面调用的是哪个入口。


20. 三方退回流程

三方配送拒收或妥投异常时,MQ 会把 deliveryStatus 更新为 98 已退回,并可携带 backStatus。

sequenceDiagram
  participant T as 三方物流/TMS
  participant MQ as RabbitMQ
  participant C as ExpressNoSyncNotify
  participant DB as DGJ DB
  participant U as 门店操作人
  T->>MQ: deliveryStatus=98, backStatus=10/20
  MQ->>C: thirdPartyDeliveryChangeStatus
  C->>DB: 更新配送/退回状态
  C->>DB: 销售出库恢复已出库
  U->>T: callback/confirm
  T-->>U: 确认退回完成

backConfirm() 当前只调用外部确认接口,没有同步修改本地 back_status。本地最终状态依赖外部再次回调,若外部不回调,页面可能长期停留旧状态。


21. 急件自提采购流程

急件自提订单类型:

PoOrderEnums::ORDERTYPE_SELF_PICK_ORDER = 30-Cxx-70

21.1 下单前校验

PoOrderSer::selfPickCheck():

  1. 商品列表不能为空。
  2. 只支持快准车服供货主体,不支持撮合商物料。
  3. 按急件自提订单类型查询供给仓库存。
  4. 逐商品校验请求数量不超过允许数量。

21.2 自提信息回调

Routing Key:dispatchcenter_self_pick_up_notify。

{
  "orderCode": "CG9999202607160001",
  "customerCode": "9999",
  "contact": "仓库联系人",
  "contactNumber": "13800000000",
  "selfPickUpAddress": "示例自提仓地址",
  "selfPickUpTime": "2026-07-16 16:00:00"
}
sequenceDiagram
  participant DC as 调拨中心
  participant MQ as RabbitMQ
  participant C as DispatchCenterNotify
  participant PO as 采购主单
  participant EXT as 采购扩展表
  DC->>MQ: self_pick_up_notify
  MQ->>C: 消费消息
  C->>PO: 按 orderCode 查询
  C->>C: 校验 orderType=30-Cxx-70
  alt 已有扩展行
    C->>EXT: update pick_* by iid
  else 无扩展行
    C->>EXT: insert iid + pick_*
  end

21.3 页面显示

采购详情读取 pick_address/pick_connect/pick_mobile/pick_time。回调尚未到达时显示“请于付完款5分钟后查看”。

insertPickInfo() 按 iid 先查后写,应用层具备重复消息覆盖能力,但并发重复插入仍依赖数据库唯一索引保证。


22. TMS 状态回调

消费者:ExpressNoSyncNotify::consume(),Destination:tmsprod_notify。

Routing Key回调方法业务
tmsprod_self_transport_statusselfTransportStatus商家自配送状态
tmsprod_third_party_delivery_change_statusthirdPartyDeliveryChangeStatus三方配送状态
tmsprod_out_expressno_relationcallback采购运单映射
tmsprod_waybill_prearrvied_timesyncArrvveTime预计到货、自动签收时间
tmsprod_express_split_carton_noexpressNoUpdate拆箱后更新运单
tmsprod_third_pickup_order_statusthirdReturnPickupOrderStatus售后上门取件状态

22.1 商家自配送回调

flowchart TD
  A["收到 transNo/sid/status"] --> B["关系表按 delivery_no+sid 查询"]
  B --> C{"存在未撤销出库单?"}
  C -->|否| X["记录日志并 ACK"]
  C -->|是| D{"TMS status"}
  D -->|20| E["出库=2 派送中"]
  D -->|90| F["出库=3 已送达"]
  D -->|其他| X
  E --> G["尝试更新本地配送订单状态"]
  F --> G
  G --> H["ACK"]

当前消费者不处理 10 和 99,因此取消后的恢复主要依赖同步取消接口,而不是 MQ。

22.2 三方回调

处理步骤:

  1. 只按 deliveryNo 查询配送订单。
  2. 更新 delivery_status、可选 back_status、骑手姓名和电话。
  3. 状态 90 时将关联出库单改为 3。
  4. 状态 98/99 时将关联出库单改为 1。
  5. 无论异常与否,finally 返回 ACK。

22.3 ACK 策略

两个状态回调在多数异常场景都会 ACK,意味着 RabbitMQ 不会自动重试。可靠性依赖:

  • TMS 是否会主动重复推送;
  • 是否有人工 resend;
  • 是否有状态巡检;
  • 日志和钉钉告警是否及时发现。

这些外部运行机制需要在部署环境确认。


23. 调拨中心采购出库流程

Destination:DEST_DISPATCHCENTER_NOTIFY。

23.1 消息过滤

dispatchCenterDeliveryStockOut() 只保留:

  1. outOrderCode 非空;
  2. sourceChannel 为 1 或 4;
  3. DGJ 能查到对应采购单;
  4. 采购状态为待发货、配送中或已完成。

23.2 行级落库

sequenceDiagram
  participant DC as 调拨中心
  participant C as DispatchCenterNotify
  participant PO as 采购主单
  participant PI as 采购明细
  participant OUT as 采购出库流水
  DC->>C: delivery_stock_out(details)
  C->>C: 按 outOrderCode 分组
  C->>PO: 查询采购单和状态
  C->>PI: 按 outOrderRowCode 查询商品
  C->>OUT: 按 stockRowId 查重
  loop 每个新出库行
    C->>OUT: 组装数量/金额/箱号/运单/仓库
  end
  C->>PO: 首次出库改为配送中(4)
  C->>OUT: 批量插入

23.3 幂等键

外部 stockRowId 保存为 srcOrderOutId。重复消息先查询已存在行并跳过,因此它是采购出库回调的业务幂等键。

23.4 售后延迟队列

若相关售后采购单仍在“已提交/审核中”,消费者把消息发到 20 秒延迟队列,最多重试 3 次,之后告警并 ACK。

flowchart TD
  A["采购出库消息"] --> B{"有关联售后且仍审核中?"}
  B -->|否| C["正常落采购出库流水"]
  B -->|是| D{"retryTimes > 2?"}
  D -->|否| E["发20秒延迟消息并ACK"]
  E --> A
  D -->|是| F["告警并ACK,不再自动重试"]

24. 运单、箱号和预计到货

24.1 三种标识

字段来源含义
boxNo调拨/WMS箱号
express_noTMS/调拨快准侧运单号
wmsNo外部映射三方承运运单号或 WMS 号

24.2 预计到货

调拨中心 dispatchcenter_logistice_info_notify 按采购明细行聚合;同一 outOrderLineId 多条时间取较晚的 planReceiptTime,再调用 PoOrderReceiveSer::timeAddOrUpdate()。

TMS waybill_prearrvied_time 则按配送单列表更新采购出库流水 toArriveTime,并保存 autoSignTime 扩展信息。

24.3 消息早到

运单映射消息可能早于采购出库流水。代码用 Redis 暂存,再由 ExpressNoSupplement::expressNoDelaySync() 补偿。

当前补偿代码有一个需要验证的问题:构造 ['express_no' => ['in', $expressNosStr]] 时传入的是拼接后的字符串,不是原始数组;这可能与 mix_where 的预期不一致,需在本地/预发用真实查询验证。


25. 撤销对账与配送状态恢复

销售出库撤销对账时,不能一律恢复为 3 已送达,而是根据当前配送状态判断。

flowchart TD
  A["撤销对账"] --> B{"delivery_no前缀"}
  B -->|KZPS| C["向TMS查询 transportStatus"]
  B -->|SPS| D["查本地三方 delivery_status"]
  B -->|其他| E{"历史上是否有配送关系?"}
  C --> F["10=>1,20=>2,90=>3,99=>1"]
  D --> F
  E -->|无且原状态4| G["恢复1 已出库"]
  E -->|有/未知| H["保持默认3"]

25.1 三方状态映射缺口

resetSaInvoiceStatus() 复用自配送的状态表:10/20/90/99。但三方配送的“配送中”是 30,不是 20,另有 21 和 98。

结果是三方单处于 30 配送中 或 98 已退回 时,撤销对账可能无法得到正确恢复状态。

建议按配送类型使用两张显式映射表。


26. 数据流总图

flowchart TB
  subgraph Sale["销售侧"]
    SA["销售订单"] --> INV["销售出库单"]
    INV --> MODE{"配送方式"}
    MODE -->|自配送| KZPS["TMS KZPS"]
    MODE -->|三方| SPS["TMS SPS"]
    KZPS --> REL["配送关系表"]
    SPS --> TDO["三方配送订单表"]
    TDO --> REL
    REL --> INV
  end
  subgraph App["骑手履约"]
    KZPS --> TAKE["接单"]
    TAKE --> REPORT["轨迹"]
    REPORT --> SIGN["到达/签收"]
  end
  subgraph Purchase["采购侧"]
    PO["采购订单"] --> DC["调拨中心"]
    DC --> OUT["采购出库流水"]
    DC --> PICK["急件自提扩展"]
    DC --> ETA["预计到货/运单"]
  end
  KZPS -.MQ.-> INV
  SPS -.MQ.-> INV

27. 商家自配送排查手册

27.1 出库单没有 KZPS

按顺序检查:

  1. 出库单是否存在且 sid 正确。
  2. 配送员工是否存在、启用并绑定管理员账号。
  3. 门店、客户默认地址是否完整。
  4. 出库明细商品是否能从物料缓存读取名称和单位。
  5. Services/transport 日志中的“创建配送单参数/结果”。
  6. TMS transport/create 是否超时或返回业务错误。
  7. 外部已创建时,是否本地事务失败导致未保存 delivery_no。

27.2 骑手看不到任务

检查项说明
APP 当前门店Redis 选店是否与出库单 storeId 一致
TMS senderCode创建时来自门店 ID
骑手 UIDTMS courierCode 是管理员 UID,不是员工 ID
状态筛选待配送只查 10
isSelf配送中/已完成列表会传该筛选
TMS 任务按 transNo 直接查询确认

27.3 接单后 DGJ 派送中但 TMS 未接单

这是当前调用顺序可产生的已知中间态。查询操作时点日志,确认 transportTaking 在本地更新后是否抛出 TMS 错误,再人工回退或重试,不能直接重复创建配送单。

27.4 已签收但 DGJ 未送达

  1. TMS 是否产生 tmsprod_self_transport_status 且 status=90。
  2. 消费者是否启动。
  3. 关系表是否有 (delivery_no,sid)。
  4. 关系指向的出库单是否已撤销。
  5. 出库单是否已是 4/5/6,被保护逻辑跳过。

28. 状态异常排查决策树

flowchart TD
  A["页面状态不符合预期"] --> B{"delivery_no前缀"}
  B -->|KZPS| C["查TMS自配送详情"]
  B -->|SPS| D["查三方配送订单"]
  B -->|空| E["尚未建配送或已取消清空"]
  C --> F{"TMS状态正确?"}
  D --> F
  F -->|否| G["查发起接口/外部运力"]
  F -->|是| H["查MQ消息和消费者日志"]
  H --> I{"关系表存在?"}
  I -->|否| J["外部成功、本地落库失败或关系丢失"]
  I -->|是| K{"出库状态4/5/6?"}
  K -->|是| L["保护状态,不会被配送覆盖"]
  K -->|否| M["检查乱序消息/映射缺口"]

28.1 乱序判断

同一配送单如果先收到 90,后收到历史 20/30:

  • 三方回调会先将出库改为 3;
  • 后到的三方 30 不改出库,所以相对安全;
  • 自配送后到 20 会把出库从 3 回退到 2。

当前代码没有比较事件时间、版本号或状态优先级。是否由 MQ 分区保证有序需要环境确认。


29. 三方配送排查手册

29.1 页面没有三方入口

HGET {PREFIX}hash_third_party_delivery_white_list {sid}

同时检查 application/controllers/scm/InvSa.php 是否把 thirdDeliveryOpen 传给页面。

29.2 询价报错

现象重点检查
无报价/代码警告门店或客户经纬度为空触发结构缺陷
余额不足每个报价 deliveryAmt > balanceAmt
地址不识别三级行政区编码、详细地址、坐标
操作人为空user_id 无法查到管理员
超时Provider 超时为 5 秒

29.3 外部有 SPS、本地无记录

  1. 按出库单号调用三方 detail/orderno。
  2. 查 Services 或 Provider 请求日志取得 deliveryNo。
  3. 查本地配送订单表。
  4. 查关系表和销售出库 delivery_no。
  5. 不要直接再次下单,先确认外部接口是否幂等。

29.4 取消后仍派送中

  1. 外部 cancel 是否成功。
  2. 本地配送订单是否 99。
  3. 关系表能否反查出库单。
  4. 出库单是否 4/5/6 被保护。
  5. 是否有后到的状态回调再次覆盖配送订单状态。

30. 一致性故障矩阵

故障点外部状态本地状态当前自动恢复建议
自配送创建外部成功、本地失败有 KZPS无单号/关系无明确补偿按出库单查询 TMS 重建
三方创建外部成功、本地失败有 SPS无订单/关系无明确补偿幂等查询 + 补偿落库
接单本地成功、TMS 失败未接单出库已派送中无失败回滚或待确认状态
三方取消外部成功、本地失败已取消仍派送中可能靠 MQ状态巡检
门店外部更新成功、本地失败新地址旧地址无重试本地或反向同步
客户本地更新成功、SAAS 失败新地址SAAS 旧地址取决于同步重试Outbox
MQ 消费异常后 ACK新状态旧状态Broker 不重试DLQ/补偿扫描
运单消息早到已有映射无出库流水Redis 半小时补偿修复查询参数并监控积压
quadrantChart
  title 配送一致性风险优先级
  x-axis 低发生概率 --> 高发生概率
  y-axis 低业务影响 --> 高业务影响
  quadrant-1 优先治理
  quadrant-2 重点监控
  quadrant-3 常规观察
  quadrant-4 快速修复
  "外部建单成功本地失败": [0.55, 0.92]
  "接单本地先改状态": [0.65, 0.72]
  "MQ异常仍ACK": [0.45, 0.85]
  "运单消息早到": [0.78, 0.45]
  "APP选店越权写入": [0.25, 0.55]

31. 急件自提排查手册

31.1 无法下单

检查 selfPickCheck() 返回的逐商品错误:

  • 是否包含撮合商物料;
  • 快准车服供给仓是否有 allowQty;
  • 请求数量是否超库存;
  • skuId 与货主编码是否正确。

31.2 支付后仍无自提地址

  1. 采购类型是否确为 30-Cxx-70。
  2. 调拨中心是否发出 dispatchcenter_self_pick_up_notify。
  3. 消息的 orderCode 是否为 DGJ 采购单号。
  4. customerCode 是否与采购单 sid 一致;当前服务只取值但查询订单未显式按 sid。
  5. t_scm_po_order_ext 是否有对应 iid。
  6. 消费日志 DispatchCenterNotify/selfPickUpNotify 是否报“非急件自提订单”。

31.3 重复自提消息

正常情况下同一 iid 更新覆盖。若出现两行,检查数据库是否缺少 iid 唯一索引,以及并发消息是否同时通过“先查为空”。


32. 采购物流排查手册

32.1 采购单未变配送中

检查预期
details[].outOrderCode有 DGJ 采购单号
sourceChannel1 或 4
采购状态3 待发货、4 配送中 或 5 已完成
outOrderRowCode能查到采购明细 ID
stockRowId未在采购出库流水中出现

32.2 重复采购出库

按 srcOrderOutId=stockRowId 查询。应用会跳过已存在行;若仍重复,检查:

  • 两条消息是否使用不同 stockRowId 表示同一物理出库;
  • 数据库是否缺唯一约束;
  • 分片 sid 是否错误导致查重查错表。

32.3 运单/预计到货为空

依次查询:

  1. 采购出库流水是否已生成;
  2. boxNo、express_no、wmsNo 哪个为空;
  3. TMS 运单映射消息是否消费;
  4. Redis TMS_WMS_NO_SYNC_HASH[sid] 是否积压;
  5. 半小时补偿任务是否运行;
  6. waybill_prearrvied_time 是否包含正确 deliveryNos。

33. 常用 SQL

生产只执行只读查询;销售出库和采购流水为分片表,先按 sid 计算实际后缀。以下表名中的 {n} 必须替换。

33.1 查销售出库配送状态

SELECT id, sid, billNo, srcOrderNo, billStatus, delieverId,
       delivery_no, storeId, buId, modifyTime, isDelete
FROM t_scm_sa_invoice_0_{n}
WHERE sid = :sid
  AND billNo = :sa_invoice_no;

33.2 查配送关系

SELECT id, sid, sa_invoice_id, sa_invoice_no, delivery_id,
       delivery_no, store_id, is_delete
FROM t_sa_invoice_delivery_relation
WHERE sid = :sid
  AND (sa_invoice_no = :sa_invoice_no OR delivery_no = :delivery_no)
ORDER BY id DESC;

33.3 查三方配送订单

SELECT id, sid, delivery_order_no, third_party_order_no,
       delivery_channel, delivery_channel_name, delivery_status,
       back_type, back_status, delivery_person,
       delivery_person_phone, inquiry_no, is_delete
FROM t_scm_third_party_delivery_order
WHERE delivery_order_no = :delivery_no;

33.4 查重复关系

SELECT sid, sa_invoice_no, delivery_no, COUNT(*) AS cnt
FROM t_sa_invoice_delivery_relation
WHERE is_delete = 0
GROUP BY sid, sa_invoice_no, delivery_no
HAVING COUNT(*) > 1;

33.5 查急件自提

SELECT po.id, po.sid, po.billNo, po.billStatus, po.orderType,
       ext.pick_connect, ext.pick_mobile, ext.pick_address, ext.pick_time
FROM t_scm_po_order po
LEFT JOIN t_scm_po_order_ext ext ON ext.iid = po.id
WHERE po.billNo = :po_order_no;

33.6 查采购出库流水

SELECT id, sid, srcOrderNo, srcOrderEntryId, srcOrderOutId,
       billNo, boxNo, express_no, wmsNo, qty,
       warehouseCode, warehouseName, toArriveTime, createTime
FROM t_scm_po_out_info_0_{n}
WHERE sid = :sid
  AND (srcOrderNo = :po_order_no OR express_no = :express_no)
ORDER BY createTime, id;

33.7 查同一外部行是否重复

SELECT sid, srcOrderOutId, COUNT(*) AS cnt, SUM(qty) AS total_qty
FROM t_scm_po_out_info_0_{n}
WHERE sid = :sid
  AND srcOrderOutId IN (:stock_row_ids)
GROUP BY sid, srcOrderOutId
HAVING COUNT(*) > 1;

34. Redis、MQ 和日志排查

34.1 Redis 只读命令

redis-cli HGET '<prefix>hash_third_party_delivery_white_list' '<sid>'
redis-cli HGET '<prefix>app_transport_store' '<uid>'
redis-cli HGET '<prefix>tms_wms_no_sync_hash' '<sid>'
redis-cli HLEN '<prefix>tms_wms_no_sync_hash'

不要在生产直接执行 DEL、HDEL 或批量改白名单。运单补偿 Hash 中可能包含仍待数据库落单的数据。

34.2 MQ 检查点

DestinationRouting Key消费者
tmsprod_notifytmsprod_self_transport_statusExpressNoSyncNotify
tmsprod_notifytmsprod_third_party_delivery_change_statusExpressNoSyncNotify
tmsprod_notifytmsprod_out_expressno_relationExpressNoSyncNotify
tmsprod_notifytmsprod_waybill_prearrvied_timeExpressNoSyncNotify
DEST_DISPATCHCENTER_NOTIFYdispatchcenter_delivery_stock_outDispatchCenterNotify
DEST_DISPATCHCENTER_NOTIFYdispatchcenter_self_pick_up_notifyDispatchCenterNotify

34.3 日志关键字

rg -n "创建配送单|创建配送单失败|开始取件|配送单取消失败" application/Services/Transport
rg -n "第三方配送单状态变更|TMS自配送单状态变更|自配送单和出库单关联关系不存在" application/controllers/tasks/ExpressNoSyncNotify.php
rg -n "急件自提消息|出库开始|行号:.*重复|设置预计到货时间" application/controllers/tasks/DispatchCenterNotify.php
rg -n "同步数据|express_no|TMS_WMS_NO_SYNC_HASH" application/controllers/tasks/ExpressNoSyncNotify.php application/controllers/tasks/ExpressNoSupplement.php

34.4 代码定位命令

rg -n "transportCreate|transportTaking|transportArrive|transportReceipt|transportStatusChange" application/Services/Transport application/service/api/app/transport
rg -n "inquiry|create|cancelCheck|outCancelCheck|backConfirm" application/Services/SaOrders/ThirdDeliverySer.php application/controllers/sale/ThirdDelivery.php
rg -n "ORDERTYPE_SELF_PICK_ORDER|selfPickCheck|insertPickInfo" application
rg -n "TMSPROD_SELF_TRANSPORT_STATUS|TMSPROD_THIRD_PARTY_DELIVERY_CHANGE_STATUS|TYPE_DISPATCHCENTER_DELIVERY_STOCK_OUT" application

35. 已确认风险清单

级别风险证据影响
P0自配送和三方建单都先调用外部、后落本地两个 create() 的调用顺序外部有单、本地无单
P0自配送接单先改本地状态、后调用 TMStransportTaking()TMS 失败但 DGJ 已派送中
P0状态回调异常仍 ACK两个 MQ 回调的 finally消息丢失且无 Broker 重试
P1三方询价空地址的默认结构与后续读取不匹配inquiryDataTmp 数字下标 vs ['items']空地址时警告/异常
P1三方撤销对账复用自配送状态映射resetSaInvoiceStatus()30/98 恢复错误
P1出库 1/2/3 无状态版本或单调保护transportStatusChange()自配送乱序可回退已送达
P1三方取消外部与本地更新无统一事务ThirdDeliverySer::cancel()取消状态不一致
P1关系表应用层先查后插SelfDeliverySer::create()并发重复关系依赖唯一索引
P1三方回调只按配送单号查订单getThirdDeliveryOrderByNo()隐含单号全局唯一要求
P2APP 选店写入未在方法内校验授权集合store::service()可能写入无权限门店偏好
P2轨迹上报原样透传,缺字段校验courierReport()错坐标/设备字段延后暴露
P2backConfirm() 不更新本地退回状态外部调用后直接返回依赖后续回调
P2自配送回调尝试更新三方配送订单表selfTransportStatus()通常无对应行,更新无效且语义混乱
P2运单补偿 in 条件疑似传拼接字符串ExpressNoSupplement补偿可能长期不命中

36. 建议治理方案

36.1 可靠建单

flowchart LR
  REQ["创建配送请求"] --> OP["本地操作单 CREATED"]
  OP --> EXT["带幂等键调用TMS"]
  EXT -->|成功| SAVE["事务保存配送单+关系+出库状态"]
  EXT -->|超时| QUERY["按业务单号查询TMS"]
  QUERY -->|存在| SAVE
  QUERY -->|不存在| RETRY["可安全重试"]
  SAVE --> DONE["操作单 DONE"]

36.2 状态事件版本化

每条配送状态事件建议至少保存:

字段用途
delivery_no聚合键
event_id消息幂等键
event_time判断乱序
status外部状态
status_rank单调比较
raw_payload审计与重放
consume_result成功/失败/忽略原因

36.3 状态映射拆分

不要再让 KZPS 和 SPS 共用一个数值映射表。分别建立:

selfDeliveryToInvoiceStatus
thirdDeliveryToInvoiceStatus

并为取消、退回、撤销对账分别定义合法目标状态。


37. 回归测试清单

37.1 商家自配送

  • [ ] 无配送员创建,TMS 能生成待揽收单。
  • [ ] 指定合法配送员创建,员工 ID 与管理员 UID 映射正确。
  • [ ] 员工未设置配送员角色时明确拦截。
  • [ ] 已有 KZPS 再次操作只更新骑手,不重复建单。
  • [ ] 创建成功后出库单和关系表都保存同一 delivery_no。
  • [ ] TMS 成功、本地模拟失败后可以查到并补偿。
  • [ ] APP 待配送、配送中、已完成列表筛选正确。
  • [ ] 接单、改派、到达、签收完整闭环。
  • [ ] 轨迹设备注册与连续上报可在地图查看。
  • [ ] 删除自配送后清空 delivery_no,出库恢复已出库。

37.2 三方配送

  • [ ] 白名单开/关决定页面入口。
  • [ ] 门店地址缺失时明确提示,不进入空报价计算。
  • [ ] 客户地址缺失、坐标缺失、手机号缺失逐项提示。
  • [ ] 达达/美团多报价时最低价推荐正确。
  • [ ] 余额不足报价不可选。
  • [ ] 询价过期后下单有明确错误。
  • [ ] 已有 KZPS 时禁止直接创建 SPS。
  • [ ] 下单后配送订单、关系、出库状态一致。
  • [ ] 待接单、待取货、骑手到店、配送中、完成状态显示正确。
  • [ ] 取消前校验、违约金、取消原因流程完整。
  • [ ] 取消和已退回后出库恢复已出库。
  • [ ] 退回确认后本地状态最终收敛。

37.3 MQ 与乱序

  • [ ] 同一三方状态重复投递不产生重复关系或错误状态。
  • [ ] 自配送 90 后再投 20 不应回退已送达。
  • [ ] 关系不存在时消息进入待补偿,而不是静默永久 ACK。
  • [ ] 消费数据库异常时可以重试或进入 DLQ。
  • [ ] 已对账、已核销、已撤销不被配送回调覆盖。
  • [ ] 撤销对账对 KZPS 的 10/20/90/99 恢复正确。
  • [ ] 撤销对账对 SPS 的 10/20/21/30/90/98/99 恢复正确。

37.4 急件自提与采购物流

  • [ ] 撮合商物料被急件自提拦截。
  • [ ] 供给仓库存不足被逐商品拦截。
  • [ ] 自提回调首次插入和重复更新都正确。
  • [ ] 并发重复消息不产生两条采购扩展记录。
  • [ ] 调拨出库重复 stockRowId 不重复落流水。
  • [ ] 售后审核中消息按 20 秒延迟并限制重试次数。
  • [ ] 箱号、快准运单、三方运单映射正确。
  • [ ] 运单消息早到时进入 Redis,补偿后清除已成功项。
  • [ ] 多条预计到货时间按当前业务规则取较晚值。

37.5 兼容业务

  • [ ] 普通销售出库、销售退货、撤销出库不受影响。
  • [ ] 已对账和已核销财务状态不被覆盖。
  • [ ] AI IM 工作台发起三方配送与销售出库页面一致。
  • [ ] 多门店账号切店后只看到授权门店配送单。
  • [ ] 老数据无 delivery_no 时页面仍显示“无配送”。

38. 发布和运行检查

配送功能依赖常驻 CLI 消费者和外部系统。发布后至少确认:

检查项验证方式
ExpressNoSyncNotify 消费进程进程管理器状态、最新消费日志
DispatchCenterNotify 消费进程进程状态、采购测试消息
20 秒延迟队列队列声明成功且 DLX Routing 正确
运单补偿任务调度平台最近运行时间和 Redis 积压
TMS HTTP自配送详情只读调用成功
三方物流 HTTP白名单站点询价测试成功
Redis三个 Key 可访问且前缀正确
数据库分片测试 sid 命中正确销售/采购分表

不要用生产真实出库单测试创建、取消、签收。优先使用预发专用站点和可回收订单。


39. 监控指标建议

指标建议告警条件
配送创建外部成功、本地失败数>0 立即告警
无关系的 TMS 状态消息连续 5 分钟 >0
状态消费异常后 ACK 数>0 立即告警
Redis 运单补偿待处理数超过两个调度周期仍增长
出库已送达后回退派送中数>0 立即告警
三方询价 5 秒超时率5 分钟超过基线
三方余额不足率作为运营指标观察
急件自提付款后无地址时长超过约定 SLA
调拨出库重复行消息数异常突增告警

40. 证据、推断与待确认项

40.1 已由代码确认

  • 自配送单前缀 KZPS,三方配送前缀 SPS。
  • APP 自配送状态为 10/20/90/99。
  • 三方状态为 10/20/21/30/90/98/99。
  • 销售出库状态为 1 已出库、2 派送中、3 已送达、4 已对账、5 已核销、6 已撤销。
  • 同一配送关系表同时保存 KZPS 和 SPS。
  • 两种建单均先调用外部,再落本地。
  • TMS 状态通过 tmsprod_notify 消费。
  • 急件自提通过调拨中心消息写采购扩展表。
  • 采购出库按 stockRowId/srcOrderOutId 做应用层幂等。
  • 运单消息早到时进入 Redis Hash 等待补偿。

40.2 根据查询模式推断

  • delivery_order_no 应全局唯一,否则三方回调可能串站。
  • 配送关系应有组合唯一索引,否则并发创建可重复。
  • MQ 最好按 delivery_no 保序,否则自配送状态可能回退。
  • 运单补偿任务注释称每半小时运行,但实际调度需查部署平台。

40.3 待环境确认

  1. 两张配送表的真实 DDL、索引和字段长度。
  2. SCM_SA_INVOICE_SUB_NUM 在各环境的实际值。
  3. TMS create/cancel 是否支持业务幂等键。
  4. tmsprod_notify 的队列分区、顺序和重试策略。
  5. 消费进程由 Supervisor、容器还是调度平台托管。
  6. ExpressNoSupplement::expressNoDelaySync 的真实调度频率。
  7. 三方渠道当前是否仍同时启用达达和美团。
  8. 轨迹保存周期、隐私脱敏和查询权限。
  9. APP 统一分发器的环境实际 URL 与鉴权头。

41. 一页式排查卡

第一步:拿到 sid + 业务单号 + delivery_no + 问题发生时间。

第二步:识别链路。
  KZPS -> 商家自配送
  SPS  -> 三方配送
  CG + 30-Cxx-70 -> 急件自提
  采购单 + 箱号/运单 -> 调拨/WMS履约

第三步:核对主事实。
  销售:销售出库分表 + 配送关系 + TMS/三方详情
  采购:采购主单 + 采购扩展 + 采购出库流水

第四步:核对异步链。
  tmsprod_notify / dispatchcenter_notify
  消费日志、ACK策略、关系是否存在、消息是否乱序

第五步:判断一致性方向。
  外部有、本地无 -> 建单落库补偿
  外部新、本地旧 -> MQ/消费问题
  外部旧、本地新 -> 同步接口本地先写或人工修改
  两边一致、页面旧 -> 查询缓存/筛选/门店上下文

第六步:修复后按链路完整回归,不只改一张表。

42. 相关文档

  • 04_销售_出库_配送与收款.md:销售主链与收款关系。
  • 15_销售完整排查手册.md:销售订单、出库、退货整体排查。
  • 16_库存一致性排查手册.md:出库与库存事实核对。
  • 23_外部系统对接清单.md:TMS、调拨中心、WMS 边界。
  • 13_MQ回调与补偿任务地图.md:MQ Destination、Routing Key 和补偿任务。
  • 24_本地开发联调和日志定位.md:消费者启动与日志定位。
  • 25_高风险公共文件影响面.md:修改 InvSaService、MQ 公共逻辑的回归要求。

43. 最终结论

配送域的主线不是“创建一张配送单”,而是:

业务单据
  -> 选择履约模式
  -> 外部系统创建任务
  -> 本地保存可追踪关系
  -> 骑手/仓库执行
  -> MQ 推进状态
  -> 到达/签收/退回
  -> 对账核销或逆向恢复

维护这部分代码时必须同时守住三件事:

  1. 单号关系完整:没有 销售出库单 <-> 配送单 或 采购行 <-> 外部出库行 的关系,后续回调无法落地。
  2. 状态转换有序:TMS、三方和 DGJ 的数值含义不同,不能直接复用映射,也不能忽略乱序。
  3. 跨系统结果可补偿:数据库事务无法覆盖 HTTP 和 MQ。外部成功、本地失败不是偶发小问题,而是必须有明确恢复路径的正常分布式故障场景。

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

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
自提sale/SelfDelivery/App transport出库单、自提人、核销码、时间SelfDelivery Controller/App APISelfDeliverySerout invoice + pickup ID待自提、核销和签收状态
自有骑手sale/ThirdDelivery/AI Courier出库单、地址、骑手、费用Delivery/Courier ControllerTransportSer/TmsProviderdelivery order ID创建配送并分配骑手
三方配送销售 Service/ThirdDelivery出库单、物流商、地址、包裹Controller/ServiceThirdDeliverySer/ThirdLogisticsProviderlocal+external delivery ID三方下单和物流状态
回调/补单Dispatch/Express MQ、supplement taskdelivery/out invoice、event、运单号DispatchCenterNotify、ExpressNoSyncNotify、ExpressNoSupplement配送 Service出库/配送/外部运单号回写接单、取货、签收、取消与运单

日志证据矩阵

| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 发起配送 | Controller/TransportSer | request_id、出库单、delivery ID、mode | 本地关系保存,Provider 调用发起 | 地址/状态/重复下单失败 | local delivery ID 进入 Provider | | 外部下单 | Tms/ThirdLogistics Provider | external request/order ID、HTTP/业务码 | 对方受理并返回运单/配送号 | timeout、外部成功本地未存 | 外部 ID 用于回调 | | 状态回调 | DispatchCenterNotify | message ID、event、local/external ID | 状态单向推进并 ACK | 乱序、重复、关系缺失 | ID 查配送关系与出库单 | | 运单补偿 | Express task | task batch、out invoice、external ID | 补齐缺失运单且重跑 0 变化 | 扫描过宽、覆盖正确运单 | 批次清单回查关系 |

环节数据变更台账

步骤代码位置事务读取事实写入表/缓存/MQ字段或数量变化回查证据
建本地配送Self/ThirdDelivery/Transport Service本地事务出库状态、地址、已有关系配送主表、SCM_SA_INVOICE_DELIVERY_RELATIONinsert;status pending;mode/fee 写入出库单+local delivery ID
调外部Tms/ThirdLogistics ProviderDB 外部边界配送快照外部配送系统本地 pending,获得 external ID;超时不可直接重建request/external ID、对方回执
接单配送Dispatch Consumer单消息事务当前状态、事件时序配送/出库状态、骑手/运单字段pending -> accepted/picked/delivering/signed/canceledmessage ID、状态时间
自提核销SelfDeliverySer核销事务核销码、待自提状态自提/出库状态wait_pickup -> signed;同码仅一次pickup ID、核销人/时间
补偿ExpressNoSupplement小批幂等对方已存在、本地缺失的运单本地关系/运单字段null -> external no;已有值不覆盖batch、out invoice、重复运行 0 变化

子模块追踪:self-delivery-create 商家自配送创建

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
创建配送销售出库选择商家自配送invoice billNo、address、fee、modeapplication/controllers/sale/SelfDelivery.php -> application/Services/Transport/SelfDeliverySer.php出库可配送态、地址、已有关系和骑手配置配送本地事务 insert 主单/关系,status none -> pendingrequest ID + invoice/local delivery ID重复出库键返回既有配送;创建失败不推进出库配送态
创建回查页面无配送单/关系invoice/local delivery IDapplication/models/saOrders/ThirdPartyDeliveryRelationModel.php配送主表、出库关系和状态查询只读 不写invoice + local delivery + relation count只有主单时补关系/领域撤销,不重复创建配送单

子模块追踪:self-delivery-rider 配送员接单、改派与到达

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
接单改派骑手接单/运营改派/到达delivery ID、rider ID、action、versionapplication/controllers/aiConsole/Courier.php -> application/Services/Transport/SelfDeliverySer.php当前配送态、原骑手、骑手可用性和版本配送本地事务 pending -> accepted/arrived,rider old -> newrequest ID + delivery/rider + action/version非法迁移/旧版本零写入;改派保留历史
状态回查骑手端和后台不同步delivery ID、event timeapplication/KzData/Enums/TransportEnums.php状态历史、当前骑手和消息顺序查询只读 不写delivery + rider + status timeline乱序不回退到达态;只补事件/缓存,不重建配送

子模块追踪:self-delivery-sign 自配送签收与轨迹

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
轨迹上报骑手位置/状态更新delivery/rider、coordinate、timestampapplication/Services/Transport/SelfDeliverySer.php当前任务归属、坐标时间和配送态轨迹事务/外部地图边界 append;核心订单状态按事件单向推进request ID + delivery/rider + timestamp过旧坐标不覆盖新轨迹;敏感位置按权限访问
签收骑手/客户确认签收delivery/invoiceNo、proof/sign codeapplication/controllers/sale/SelfDelivery.phpdelivering/arrived、签收证据和重复键配送本地事务 old -> signed,写签收时间;后置订单通知 commit 后request ID + delivery/invoice + signer/time重复签收 0 副作用;通知失败只补消息

子模块追踪:third-delivery-quote 三方配送准入与询价

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
准入询价选择三方配送前计算服务与费用sid、address/coordinate、goods/weightapplication/Providers/OpsCenter/ThirdLogistics/ThirdLogisticsProvider.php站点准入、地址、货物、承运商和本地配置外部查询事务外,本地业务 DB 不写;返回 quote snapshotrequest ID + sid + quote/provider codetimeout/无服务明确不可选;不伪造价格或准入
报价复核下单价与询价价不同quote ID、delivery requestapplication/Services/SaOrders/ThirdDeliverySer.phpquote 有效期、请求货物地址和最终费用查询只读;下单时重新询价/校验并保存最终 feequote/request ID + old/final fee过期报价不直接使用;差异由用户/规则确认

子模块追踪:third-delivery-create 三方配送下单与本地落库

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
本地建单出库确认三方配送invoiceNo、local delivery ID、quote/addressapplication/controllers/sale/ThirdDelivery.php -> application/Services/SaOrders/ThirdDeliverySer.php出库态、quote、已有关系和地址本地事务 insert delivery/relation,status none -> pendingrequest ID + invoice/local delivery + fee本地失败不调用外部;重复 invoice 返回既有关系
外部下单pending 配送提交承运商/TMSlocal/external delivery IDapplication/Providers/OpsCenter/Tmsprod/TmsProvider.php本地配送快照和外部受理态外部调用事务外;受理后本地 pending -> created、保存 external IDrequest ID + local/external IDs + codestimeout 按本地业务键查外部,禁止重复外部单

子模块追踪:third-delivery-cancel 三方取消、改派与异常退回

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
取消改派用户/运营取消或换承运商local/external delivery、action/reasonapplication/Services/SaOrders/ThirdDeliverySer.php当前外部/本地态、可取消规则和费用外部调用事务外;确认后本地事务 old -> canceled/reassignedrequest ID + both IDs + action/resulttimeout 先查原单终态;未取消成功不得建新外部单
异常退回承运失败/退回回调message ID、delivery/invoice、eventapplication/controllers/tasks/DispatchCenterNotify.php当前配送态、出库/库存和退回关系回调本地事务推进 failed/returned;库存反向按独立领域事务message ID + delivery/invoice + event状态成功不代表库存已返;分别补偿状态和库存

子模块追踪:urgent-pickup 采购急件自提

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
创建自提采购急件选择自提po billNo、pickup ID/code、contactapplication/Services/Transport/TransportSer.php采购状态、已发/收量、提货信息和重复关系自提本地事务 insert,status none -> wait_pickuprequest ID + po/pickup IDs不允许状态零写入;核销码日志脱敏
自提核销提货人确认取货pickup ID/code、operatorapplication/Services/Transport/SelfDeliverySer.phpwait_pickup、码有效性和已核销态核销本地事务 wait_pickup -> signed/picked,同码一次request ID + pickup/po + operator/time重复码 0 变化;后置采购状态缺失只补通知

子模块追踪:tms-callback TMS 状态回调

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
回调消费TMS 接单/取件/运输/签收/取消message ID、event、external/local deliveryapplication/controllers/tasks/DispatchCenterNotify.php关系、当前状态、事件版本/时间单消息本地事务 status old -> mapped new;ACK 在 commit 后message ID + event + both IDs未知/乱序不回退;重复 0 副作用
下游推进签收/取消后更新出库/来源单delivery/invoice/source IDsapplication/Services/Transport/TransportSer.php配送终态、出库和来源当前态单回调事务或后置消息单向推进;外部边界另记three IDs + affected rows/routing主配送已成只补来源状态/MQ,不重放 TMS 业务

子模块追踪:transfer-delivery 调拨中心采购出库

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
调拨出库调拨中心回传采购发货message ID、po/transferNo、SKU、qtyapplication/controllers/tasks/DispatchCenterNotify.php采购可发量、已有 out info、调拨关系回调本地事务 outQty +n、采购状态推进,库存按业务类型处理message ID + po/transfer + SKU重复/超发 0 增量;不要将销售配送逻辑套用采购
履约对账出库后采购/调拨状态不平po/transfer/delivery IDsapplication/models/scm/PoOrderExtModel.php采购扩展、调拨单、配送和已发/收量查询只读 不写all IDs + qty/status timeline只补缺失关系/状态;库存事实单独验收

子模块追踪:express-supplement 运单、箱号与预计到货补偿

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
异步同步外部运单/箱号消息message ID、invoice/delivery、express/box/ETAapplication/controllers/tasks/ExpressNoSyncNotify.php本地关系、已有字段、外部版本单消息本地事务 null/old -> external latest,不覆盖更新版本message ID + invoice/delivery + expressNo hash重复/旧消息 0 变化;字段敏感时脱敏日志
定时补偿扫描外部有、本地缺失记录batch、invoice IDsapplication/controllers/tasks/ExpressNoSupplement.php外部查询结果、本地空字段和任务水位每单本地事务只补 null 字段;已有值不覆盖,重跑 0 变化task + batch + invoice + before/after小批执行;外部不确定/冲突进入人工核对