本文从真实代码出发,说明 DGJ2.0 与 SAAS/E站之间的销售退货申请、后台审核、退货入库、DGJ 财务冲账、下游退款和取消申请链路。

它要解决的不是“怎么新增一张销退单”这个单点问题,而是把下面六个容易混在一起的事实拆开:

  1. SAAS 退款单和 DGJ 退货申请单是两个系统里的两张单。
  2. DGJ 申请审核通过只进入“待入库”,不会直接增加库存。
  3. 真正的库存回增发生在生成 150602 销退单和库存流水时。
  4. DGJ 会同时生成负金额的销售退款/冲账记录,但支付渠道退款由 SAAS 在收到完成事件后按支付方式执行。
  5. 已发货退货、未发货退款、挂账领料退货走的状态和库存路径不同。
  6. PC 手工销退、渠道售后、预订单售后不是这张退货申请单的同一条流程。

读完后,应当可以只凭订单号和服务站 sid 回答:

  • 申请是谁发起的,属于已发货退货还是未发货关单。
  • SAAS 退款单、DGJ 申请单、DGJ 销退单和退款记录分别是什么单号。
  • 当前停在 SAAS、DGJ 审核、DGJ 入库、MQ 还是支付渠道。
  • 库存有没有回增,回增到哪个仓库和货位。
  • DGJ 账务有没有生成,外部真实退款有没有发起。
  • 当前能不能取消、重试、补消息或人工补偿。

1. 业务目标与范围

1.1 本文覆盖

场景是否有 DGJ 申请单是否人工审核是否生成 150602是否改变 DGJ 库存是否通知 SAAS 退款
E站/APP 已发货普通退货有有入库时生成是审核、完成各一次
E站/APP 不良品退货有有入库时生成是,进入选择的不良品货位审核、完成各一次
微仓挂账领料单退货有无,创建即完成创建时生成是当前代码不发完成事件,另有 SAAS 同步
未发货整单退款有无,创建即完成不生成否,只关闭未出库销售量直接发完成事件
未发货部分退款有无,创建即完成不生成否,只关闭相应未出库量直接发完成事件
SAAS 取消待审核申请使用已有申请单仅待处理可取消不生成否调用方本地同步取消

1.2 本文不当作同一流程的场景

相邻业务真实入口/表与本文主链路的区别
PC 手工销售退货InvSa?action=returnSale、NormalSaleReturnSer不先建 t_scm_apply_return_order,直接生成销退单和库存
全车件销退allcarpart/sale/sa-return-order/*DGJ 控制器主要承担转发和打印,业务事实由全车件服务维护
渠道售后t_channel_aftersale、t_channel_aftersale_info渠道订单自己的售后状态、回调和明细
预订单售后t_pre_after_sale、PreAfterSaleService对接订单中心,维护预订单退款状态
采购退货采购退货单、采购退货出库方向是退给供应商,库存减少,不是客户退回
销售出库撤销销售出库单撤销撤回错误出库事实,不是客户售后申请
异常退货日志t_scm_abnormal_return_logPC 直接销退数量异常记录,不是申请主表

1.3 参与角色

角色主要动作系统边界
修理厂/E站用户选择退货商品、数量、原因、正常品/不良品SAAS/E站
SAAS 订单服务计算可退数量和优惠分摊,创建退款单,调用 DGJSAAS
DGJ 退货审核人查看申请、审核通过DGJ OPS
DGJ 仓库人员选择实际仓库和货位,确认退货入库DGJ OPS
DGJ 销售/库存/财务服务生成销退单、库存流水、实时库存和退款冲账单DGJ
RabbitMQ传递审核、完成、库存变化事件跨系统
SAAS 消费者推进退款单状态,调用银联、支付服务或资金系统退款SAAS

2. 一句话业务脉络

flowchart LR
    A["SAAS 创建退款单 RFC..."] --> B["调用 DGJ createOrderReturn"]
    B --> C["DGJ 创建申请单 AR... 状态 1"]
    C --> D["OPS 审核 状态 1→2"]
    D --> E["MQ app_apply_return_checked"]
    E --> F["SAAS 退款单 0→11"]
    D --> G["OPS 选择仓库货位并入库"]
    G --> H["DGJ 创建 XS... 销退单 150602"]
    H --> I["库存流水和实时库存回增"]
    H --> J["DGJ 创建负金额销售退款记录"]
    I --> K["DGJ 申请单 2→3"]
    K --> L["MQ app_apply_return_finish"]
    L --> M["SAAS 按支付方式执行退款"]

必须记住:

AR... 是退货申请,XS... 是实际销退业务事实,RFC... 是 SAAS 退款单。审核、库存和外部退款分别发生在不同阶段。

3. 系统边界与调用关系

flowchart TB
    subgraph SAAS["SAAS / E站"]
        U["用户提交退货"]
        RS["RefundService"]
        RO["t_refund_order"]
        RD["t_refund_order_detail"]
        DC["DgjConsumer"]
        PAY["支付渠道/资金系统"]
    end
    subgraph DGJ["DGJ2.0"]
        API["inner/moveMall/Order"]
        SVC["SaOrderSer + ApplyReturnSer"]
        AR["申请主明细"]
        OPS["OPS 审核与入库"]
        INV["销退单 + 库存"]
        FIN["收款/退款冲账"]
    end
    subgraph MQ["RabbitMQ"]
        CHECKED["app_apply_return_checked"]
        FINISH["app_apply_return_finish"]
        INVENTORY["inventory_event"]
    end
    U --> RS
    RS --> RO
    RS --> RD
    RS --> API
    API --> SVC
    SVC --> AR
    AR --> OPS
    OPS --> INV
    OPS --> FIN
    OPS --> CHECKED
    OPS --> FINISH
    INV --> INVENTORY
    CHECKED --> DC
    FINISH --> DC
    DC --> RO
    DC --> PAY

4. 三类单号和关联键

4.1 单号不要混用

业务事实示例前缀DGJ/SAAS 字段用途
SAAS 原销售订单业务订单号src_sa_order_no / src_order_no找原销售单和原出库
SAAS 退款单RFC...DGJ src_order_no;SAAS refund_order_no跨系统回调的核心业务键
DGJ 退货申请单AR...t_scm_apply_return_order.bill_noOPS 查询、审核、入库
DGJ 销退单XS...t_scm_sa_invoice_0_N.billNo库存、账务和报表事实
DGJ 退款/冲账单SKD...t_scm_payment_N.billNoDGJ 客户往来和账户流水

4.2 ID 传递关系

来源字段目标字段说明
SAAS t_refund_order.idorder_id申请表 src_order_id回调消息中的 order_id
SAAS refund_order_noorder_no申请表 src_order_noSAAS 消费者按它查退款单
SAAS 原订单 IDsrc_order_id申请表 src_sa_order_id查 DGJ 原销售记录
SAAS 原订单号src_order_no申请表 src_sa_order_no查 DGJ 销售单和出库
DGJ 申请 IDapplyReturnId销退单 srcOrderId销退单来源指向申请
DGJ 申请号bill_no销退单 srcOrderNo由申请号反查销退单
DGJ 销退单 IDsaInvoiceId退款明细 stlId退款明细结算对象
flowchart LR
    SO["SAAS 原订单 order_no"] --> RF["SAAS 退款单 refund_order_no"]
    RF -->|src_order_no| AR["DGJ 申请单 AR"]
    SO -->|src_sa_order_no| AR
    AR -->|srcOrderId/srcOrderNo| XS["DGJ 销退单 XS"]
    XS -->|stlId/stlNo| SKD["DGJ 退款冲账单 SKD"]
    RF -->|MQ order_no| CB["SAAS 退款回调处理"]

5. 代码入口地图

5.1 DGJ 主链路

层级文件关键方法职责
内部 APIapplication/controllers/inner/moveMall/Order.phpcreateOrderReturn已发货退货申请
内部 API同上createChargeOrderReturn挂账领料退货
内部 API同上returnOrderNoOut未发货整单/部分退款
内部 API同上cancelReturnOrder取消待审核申请
APP 销售服务application/Services/SaOrders/SaOrderSer.phpretOrder2校验并创建普通申请
APP 销售服务同上returnChargeOrder申请和销退一次完成
APP 销售服务同上returnOrderNoOut创建完成态申请并关闭销售单
申请服务application/Services/ApplyReturn/ApplyReturnSer.phpadd、check、inStorage申请主明细、审核、入库
OPS 控制器application/controllers/applyReturn/ApplyReturnOrder.php五个接口方法列表、详情、审核、入库
库存落账application/Services/SaOrders/SaOrderSer.phpsaOrderOut建销退主明细并调用库存服务
库存服务application/Services/Storage/InventorySer.phpsave库存流水和实时库存
DGJ 财务SaOrderSer::addPaymentForApppaymentMainFields生成退款/冲账主明细和账户流水
MQapplication/Services/Mq/MqSer.php三个发送方法审核、完成、库存事件

5.2 SAAS 上下游证据

层级文件关键方法职责
退款业务app/Service/Order/RefundService.phpsubmit、cancel可退计算、退款单、调用 DGJ
未发货退款app/Service/Inner/RefundInnerService.php未发货退款流程生成退款单并调用 DGJ 关单
DGJ Providerapp/Providers/Dgj/OrderProvider.php四个退货方法组装 DGJ URL 并 POST JSON
请求 DTOapp/Providers/Dgj/Request/OrderReturnRequest.phpsetter 集合主请求字段定义
明细 DTOOrderReturnRequestEntity.phpsetter 集合SKU、数量、价格、优惠和货位
MQ 消费app/Amqp/Consumer/Dgj/DgjConsumer.phpreturnChecked、returnFinish推进退款状态并执行退款

6. 接口总表

6.1 SAAS 调 DGJ

场景方法与 URL核心 Service成功返回关键副作用
已发货退货申请POST index.php/inner/moveMall/Order/createOrderReturnretOrder2DGJ 申请 id、bill_no新增申请主明细,发站内消息
挂账领料退货POST .../createChargeOrderReturnreturnChargeOrder申请 id、bill_no申请完成、销退入库、移动退货记录
未发货退款POST .../returnOrderNoOutreturnOrderNoOut申请 id、bill_no申请完成、关闭销售未出库量、发完成事件
取消申请POST .../cancelReturnOrdercancelReturnOrder更新结果DGJ 状态 1→4

6.2 DGJ OPS

场景Controller 方法权限核心请求成功返回副作用
列表ApplyReturnOrder::getListAPPLY_RETURN日期、客户、状态、搜索类型分页 rows只读
详情detailAPPLY_RETURNid主单、明细、原销售数量只读
审核checkAPPLY_RETURN_CHECKid + 登录人更新结果1→2,发审核 MQ
入库预览inStorageDetail当前权限检查被注释id默认门店、原出库仓位、候选货位只读
确认入库inStorageAPPLY_RETURN_INpostData JSON销退单 id、billNo销退、库存、退款冲账、申请完成、MQ

6.3 PC 手工销退

入口权限服务选择与申请链差异
scm/InvSa?action=returnSaleSA_ADD白名单决定新旧页面,最终进入销售退货 Service直接创建 150602,不创建 AR 申请
scm/InvSa?action=shopReturnSaleSA_ADD商铺退货页面仍属于直接销退类
allcarpart/sale/sa-return-order/print全车件模块权限转发到全车件服务只读取和打印,不是申请审核入口

7. 已发货退货请求契约

7.1 代表性请求

下面是根据 SAAS OrderReturnRequest 和 DGJ validform_rt 还原的脱敏结构。真实域名、签名和内部鉴权由环境配置决定。

{
  "order_scene": "e_shop",
  "station_id": 10001,
  "storage_id": 3001,
  "contact_id": 88001,
  "order_id": 760001,
  "order_no": "RFC10001XXXX",
  "src_order_id": 650001,
  "src_order_no": "EORDERXXXX",
  "totalQty": 2,
  "totalAmount": 198.00,
  "expectAmount": 188.00,
  "remark": "客户退货",
  "return_type": 0,
  "entries": [
    {
      "skuId": "KZ-SKU-001",
      "qty": 2,
      "price": 99.00,
      "return_price": 94.00,
      "amount": 188.00,
      "discount": 10.00,
      "is_gift": 0,
      "areaId": 5001
    }
  ]
}

7.2 主字段解释

字段来源含义校验/风险
station_idSAAS 原订单DGJ 服务站DGJ 转成 sid
order_idSAAS 退款单 ID当前售后来源 ID不是原销售订单 ID
order_noSAAS 退款单号跨系统回调键DGJ 用它做重复申请检查
src_order_idSAAS 原订单 ID原销售来源 ID写入 src_sa_order_id
src_order_noSAAS 原订单号查 DGJ 原销售单入库必须能找到
contact_id原订单客户退款客户写入 customer_id
totalQtySAAS 结算申请总数量DGJ 直接保存,未重新汇总
totalAmountSAAS 结算优惠处理后的退货总额写申请 total_amount
expectAmount用户申请期望退款金额写 amount,决定 DGJ 负金额
return_type已发货退货0 正常品,1 不良品和未发货场景的字符串枚举同名但语义不同
order_scene原订单E站、微仓、活动等场景影响来源类型和特殊退货记录

7.3 明细字段解释

字段含义申请表字段入库用途
skuId物料编码sku_id反查 invId
qty申请数量,正数qty销退单明细数量、库存回增数量
price原价price展示和金额基线
return_price退货单价return_price销退明细 price
amount明细退货金额amount销退明细金额计算基数
discount优惠券/折扣分摊discountamount = -abs(amount-discount)
is_gift是否赠品is_gift申请展示和上游匹配
areaIdSAAS 原/建议货位当前申请表不保存挂账领料等特殊流程使用

7.4 成功响应

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 12345,
    "bill_no": "ARXXXX"
  }
}

SAAS 随后把这两个值保存到退款单的 sa_invoice_order_id、sa_invoice_order_no。字段名沿用历史命名,实际保存的是 DGJ 申请单,不是最终 150602 销退单。

8. OPS 接口请求

8.1 列表查询

{
  "page": 1,
  "rows": 20,
  "customer_id": 88001,
  "beginDate": "2026-07-01",
  "endDate": "2026-07-31",
  "status": 2,
  "search_type": 1,
  "skey": "ARXXXX"
}
参数值含义
search_type1skey 按 DGJ 申请号查
search_type2skey 按 DGJ 销退单号查,再反查申请
status1/2/3/4待处理/待入库/已完成/已取消
JXCSID登录上下文Controller 不要求前端显式提交

8.2 审核

{
  "id": 12345
}

JXCSID、JXCUID、JXCUNAME 来自 DGJ 登录上下文。审核接口没有“驳回”参数;当前代码只支持通过,取消由 SAAS 在待处理状态调用。

8.3 入库

inStorage 接收表单字段 postData,其值是 JSON 字符串:

{
  "postData": "{\"id\":12345,\"entries\":[{\"skuId\":\"KZ-SKU-001\",\"qty\":2,\"return_price\":94,\"amount\":188,\"discount\":10,\"locationId\":3001,\"locationAreaId\":5001,\"description\":\"退货入库\"}]}"
}

服务端随后补充:

字段来源
invId、unitId、carModel按 skuId 查服务站物料缓存
storage_id、storage_namelocationId 和仓库表
area_id、area_namelocationAreaId 和货位表
uid、userName登录上下文
transType固定 150602

9. DGJ 申请状态机

application/KzData/Enums/ApplyReturnEnums.php 定义四个状态:

值名称可执行动作下一状态是否库存已回增
1待处理审核、取消2 或 4否
2待入库入库3否
3已完成只读终态已发货流程是;未发货流程不涉及库存
4已取消只读终态否
stateDiagram-v2
    [*] --> Pending: 普通已发货申请
    Pending: 1 待处理
    Stock: 2 待入库
    Finish: 3 已完成
    Cancel: 4 已取消
    Pending --> Stock: OPS 审核通过
    Pending --> Cancel: SAAS 取消申请
    Stock --> Finish: OPS 退货入库
    [*] --> Finish: 挂账领料/未发货 needAudit=false
    Finish --> [*]
    Cancel --> [*]

当前实现没有“审核驳回”状态。产品页面若展示“驳回”,必须先确认是否由前端映射为取消,不能凭名称推断后端存在驳回动作。

10. SAAS 退款状态机

SAAS RefundOrderEnums 与 DGJ 申请状态不是同一套枚举:

值名称触发
0待确认SAAS 已建退款单,等待 DGJ 审核
11待退款收到 app_apply_return_checked,或未发货退款直接进入
1已完成挂账直接结束或支付退款后续完成
99已取消SAAS 取消待确认申请
stateDiagram-v2
    [*] --> WaitAck: SAAS 提交已发货退货
    WaitAck: 0 待确认
    WaitRefund: 11 待退款
    Submitted: 1 已完成
    Canceled: 99 已取消
    WaitAck --> WaitRefund: DGJ 审核事件
    WaitAck --> Canceled: 用户取消
    WaitRefund --> Submitted: 支付/挂账退款完成
    [*] --> WaitRefund: 未发货退款

双系统状态对照:

业务阶段DGJ 申请SAAS 退款单说明
提交成功1 待处理0 待确认两边都没有库存和真实退款
审核成功2 待入库理论上收到 MQ 后 11MQ 丢失会出现状态不一致
入库成功3 已完成理论上仍 11,等待完成事件处理DGJ 库存已回增
下游退款执行31 或支付处理中由支付方式决定
取消499只允许待确认/待处理

11. 普通已发货申请创建

11.1 SAAS 侧先做业务结算

RefundService::submit 在本地事务中:

  1. 读取原订单和明细。
  2. 计算可退数量、原金额、优惠券分摊和可退金额。
  3. 校验退款后金额不能低于 0.1 元。
  4. 校验申请退款金额不大于可退金额。
  5. 新增 t_refund_order 和 t_refund_order_detail。
  6. 累加原订单、原明细的退款中数量和金额。
  7. 组装 OrderReturnRequest 调用 DGJ。
  8. 把 DGJ 返回的申请 ID、申请号回写 SAAS 退款单。
sequenceDiagram
    participant User as E站用户
    participant Refund as SAAS RefundService
    participant SDB as SAAS数据库
    participant Provider as DGJ Provider
    participant DGJ as DGJ Order API
    User->>Refund: 退货商品、数量、原因、期望退款
    Refund->>SDB: 读原订单/明细/已退数量
    Refund->>Refund: 计算数量、优惠和退款金额
    Refund->>SDB: 新增退款主明细并累加退货中数量
    Refund->>Provider: OrderReturnRequest
    Provider->>DGJ: createOrderReturn
    DGJ-->>Provider: DGJ申请ID与AR单号
    Provider-->>Refund: data
    Refund->>SDB: 回写sa_invoice_order_id/no

11.2 DGJ 防重复和基础校验

SaOrderSer::validform_rt 当前验证:

校验失败提示/结果
同一 sid + src_order_no 已存在申请“订单号重复创建”
entries 为空“明细信息为空”
SKU 列表为空或服务站物料不存在商品不存在或不可用
任一 qty <= 0数量需大于 0
领料场景仓库或货位不存在仓库/货位错误

重要边界:

普通 E站申请在 DGJ validform_rt 中没有再次根据历史 150601/150602 计算可退数量。可退数量主要由 SAAS 提交前控制;DGJ 申请入库接口也只校验数量大于 0。因此不能把“DGJ 一定会阻止超申请入库”写成既定事实。

11.3 防重复锁

内部 Controller 调用:

RequestCheck::checkRepeat(
    RedisKeys::KEY_MOVE_OD_ADD_LOCK,
    json_encode($data)
);

实际行为:

机制键有效期能防什么不能防什么
Redis NXdgj:move_od_add_lock:{md5(body)}10 秒完全相同请求短时间并发参数顺序/内容变化、10 秒后重放
DB 业务检查sid + src_order_no 查询长期同一 SAAS 退款号重复创建没有代码证据证明存在数据库唯一索引
flowchart TD
    A["收到创建请求"] --> B{"10秒请求锁成功?"}
    B -->|否| X["请勿重复提交"]
    B -->|是| C{"sid+退款单号已有申请?"}
    C -->|是| Y["订单号重复创建"]
    C -->|否| D["校验SKU和正数量"]
    D --> E["新增AR主表"]
    E --> F["批量新增AR明细"]
    F --> G["提交事务"]
    G --> H["发送新申请站内消息"]

11.4 申请落表

ApplyReturnSer::add 新增:

主表字段数据来源
bill_no服务端 str_no('AR')
bill_date当前日期
sid、customer_id服务站和客户
total_qtytotalQty
total_amounttotalAmount
expect_amount、amountexpectAmount
src_order_id/noSAAS 退款单 ID/号
src_sa_order_id/noSAAS 原订单 ID/号
acc_id、way_idDGJ APP 结算账户和方式
status普通申请为 1

申请明细保存 sku_id、qty、price、return_price、amount、discount、remark、is_gift。仓库、货位不在申请明细中固化,实际入库时重新选择。

12. 审核流程

ApplyReturnSer::check:

  1. 按 sid + id + status=1 读取申请。
  2. 更新 status=2。
  3. 写 check_id、check_name。
  4. 调用 sendApplyReturnCheckedToAPP。
sequenceDiagram
    participant Ops as OPS审核人
    participant C as ApplyReturnOrder
    participant S as ApplyReturnSer
    participant DB as DGJ数据库
    participant MQ as RabbitMQ
    participant SaaS as SAAS DgjConsumer
    Ops->>C: check(id)
    C->>C: 校验APPLY_RETURN_CHECK
    C->>S: check(JXCSID/JXCUID/JXCUNAME)
    S->>DB: 查status=1
    S->>DB: 更新status=2和审核人
    S->>MQ: app_apply_return_checked
    MQ->>SaaS: order_id/order_no
    SaaS->>SaaS: 退款单0→11

审核事件:

{
  "order_id": 760001,
  "order_no": "RFC10001XXXX"
}
MQ 属性值
destinationdgj_notify
routing keyapp_apply_return_checked
DGJ 生产者MqSer::sendApplyReturnCheckedToAPP
SAAS 消费者DgjConsumer::returnChecked
SAAS 查询键refund_order_no = order_no

13. 入库预览如何选仓库和货位

inStorageDetail 不是库存写入,它用于给页面准备候选数据:

  1. 读取申请主明细和客户。
  2. 按 src_sa_order_no 找 DGJ 原销售单。
  3. 按销售单 billNo 找原销售出库明细。
  4. 以 skuId 建映射,默认带出原出库仓库和货位。
  5. 再调用库存服务加载当前门店可选仓库和货位。
flowchart LR
    AR["申请明细 sku_id"] --> GOODS["物料中心 invId/名称/单位"]
    AR --> SO["原销售单 src_sa_order_no"]
    SO --> OUT["原销售出库明细"]
    OUT --> LOC["默认仓库和货位"]
    LOC --> PAGE["入库页面候选项"]
    GOODS --> PAGE

当前实现风险:

风险原因可能表现
重复 SKU 覆盖原出库明细 array_column(..., skuId)同 SKU 多仓/多货位只保留一行
原销售单查不到来源单号映射不一致“未查询到销售出库单”
权限边界较弱inStorageDetail 的 APPLY_RETURN_IN 检查被注释无入库权限用户可能读取预览
默认门店依赖管理员使用 storeDefault默认门店与原出库门店可能不同

14. 退货入库事务

14.1 前置校验

ApplyReturnSer::validate 只明确验证:

  • 申请 ID 非空。
  • 明细非空。
  • 每行 SKU 非空。
  • 每行数量大于 0。
  • 仓库和货位 ID 大于 0。

随后 inStorage 还会验证:

  • 申请必须是当前 sid 且状态为 2。
  • SKU 能在服务站物料缓存中找到。
  • 仓库和货位能查询到。
  • 原 DGJ 销售记录能按 src_sa_order_id/no 找到。

当前没有看到以下服务端强校验:

  • 入库 SKU 集合必须与申请 SKU 集合完全相等。
  • 入库数量不能大于申请数量。
  • 入库总数量必须等于 total_qty。
  • 入库货位一定属于选择的门店。

这些不是“系统一定允许”的结论,而是静态代码显示的风险点,必须通过接口回归和生产表约束继续确认。

14.2 事务内步骤

sequenceDiagram
    participant Ops as 仓库人员
    participant AR as ApplyReturnSer
    participant SO as SaOrderSer
    participant DB as DGJ数据库
    participant INV as InventorySer
    participant ACC as AccountService
    participant MQ as RabbitMQ
    Ops->>AR: inStorage(postData)
    AR->>DB: 查申请status=2
    AR->>DB: BEGIN
    AR->>SO: saOrderOut(150602)
    SO->>DB: 新增销退主表
    SO->>DB: 新增销退明细
    SO->>INV: save(销售退货)
    INV->>DB: 库存流水+实时库存
    AR->>SO: addPaymentForApp
    SO->>DB: 新增退款主明细
    SO->>ACC: 保存账户流水
    AR->>DB: 申请status 2→3
    AR->>DB: COMMIT
    AR->>MQ: inventory_event
    AR->>MQ: app_apply_return_finish

事务内包含:

顺序动作主要表
1新增 150602 销退主表t_scm_sa_invoice_0_{sid%64}
2新增销退明细t_scm_sa_invoice_info_0_{sid%64}
3写库存流水和实时库存t_scm_inventory_0_{sid%128}、t_scm_inventory_real_time
4生成退款/冲账主明细t_scm_payment_{sid%32}、t_scm_payment_info_{sid%32}
5保存账户流水账户服务维护的账户明细表
6申请状态 2→3t_scm_apply_return_order

事务提交后才发送两个 MQ。数据库事务不能回滚已经发生的外部消息,外部消息失败也不能回滚已提交数据库。

15. 销退单字段和正负方向

15.1 主表

saOutMainFieldsForApplyReturnId 和 saOutMainFields 共同组装实际销退单:

字段值/规则业务含义
billNo新 XS...DGJ 销退单号
transType150602销售退货
billStatus最终被覆盖为 5已核销
srcOrderIdDGJ 申请 ID不是原销售单 ID
srcOrderNoDGJ AR...由销退反查申请
sourceType普通申请 7,领料场景可能 11APP退货来源
totalQty申请总数量,正数退回数量
totalAmount-abs(申请总额)负金额冲减销售
amount-abs(申请期望额)实际退款/冲账金额
arrears负申请金额冲减客户欠款
hxStateCode2已核销口径

15.2 明细

字段规则
qty使用入库请求正数
price申请明细 return_price
amount-abs(申请明细 amount - discount)
disAmount与 amount 同方向
deduction申请明细 discount
srcOrderEntryId申请明细 ID
srcOrderIdDGJ 申请 ID
srcOrderNoDGJ 申请号
locationId/AreaId入库时实际选择
flowchart LR
    A["申请数量 +2"] --> B["销退明细 qty +2"]
    B --> C["库存流水 +2"]
    C --> D["实时库存 +2"]
    E["申请金额 188"] --> F["销退主表 amount -188"]
    F --> G["退款明细 amount -188"]
    G --> H["客户往来冲减 188"]

数量和金额方向不能用同一个正负号理解:

  • 销售出库 150601 通常库存数量为负。
  • 销售退货 150602 库存数量为正。
  • 销退业务金额为负,用于冲减销售收入和应收。

16. 库存回增

SaOrderSer::saOrderOut 将销退明细传给:

$InventorySer->save($v, 0, BillTypeEnums::SALE);

库存维度至少包含:

维度字段
服务站sid
商品invId、skuId
仓库locationId
货位locationAreaId
货位类型area_type
业务来源billNo、iid、transType=150602
数量qty > 0
flowchart TD
    XS["150602销退明细"] --> FLOW["128分片库存流水"]
    XS --> RT["实时库存表"]
    FLOW --> REPORT["库存/销售/利润报表"]
    RT --> QUERY["可售库存和仓位查询"]
    XS --> MQ["inventory_event"]
    MQ --> DOWN["库存下游消费者"]

核对库存时要同时证明:

  1. 销退明细存在且未删除。
  2. 当月库存流水存在,数量方向为正。
  3. 实时库存对应 sid+invId+仓库+货位 已增加。
  4. inventory_event 是否发送成功只影响异步下游,不代表本地库存是否落库。

17. DGJ 退款/冲账记录

addPaymentForApp 读取刚生成的销退单,然后:

  1. 创建 SKD... 主单。
  2. 主单 billType=RECEIPT、transType=153001。
  3. 退款明细的 transType=150602、transTypeName=销售退款。
  4. stlId/stlNo 指向 DGJ 销退单。
  5. amount 使用销退单负金额。
  6. 调用 AccountService::saveAccounts 写账户流水。
flowchart LR
    XS["销退单 XS / 150602"] --> PM["退款主单 SKD / 153001"]
    XS --> PI["退款明细 / 150602 / 销售退款"]
    PM --> PI
    PI --> ACC["账户流水"]
    PI --> CHECK["客户余额/对账单"]

这里的“销售退款”是 DGJ 财务系统里的负向收款和往来冲账事实。它不等于银联、微信或第三方资金已经退回用户。

18. 审核和完成 MQ

18.1 事件矩阵

时机destinationrouting keypayloadDGJ 事务关系
审核成功后dgj_notifyapp_apply_return_checkedorder_id、order_no状态更新后发送,无 outbox
入库提交后dgjinventory_event库存明细、sid 等DB commit 后发送
入库提交后dgj_notifyapp_apply_return_finishorder_id、order_no、return_type库存 MQ 后发送

18.2 完成事件

{
  "order_id": 760001,
  "order_no": "RFC10001XXXX",
  "return_type": 1
}
return_type含义
1 默认已发货退货入库完成
2未发货退款/关单完成

注意它与创建请求里的 return_type=0/1(正常品/不良品)不是同一个维度。

19. SAAS 收到事件后的退款

19.1 审核事件

DgjConsumer::returnChecked:

  1. 按 refund_order_no = order_no 查退款单。
  2. 更新 order_status=11 待退款。
  3. 无论成功还是异常,finally 都返回 ACK。

19.2 完成事件

DgjConsumer::returnFinish 要求退款单当前状态必须是 11,然后按来源和支付方式分支:

订单/支付条件退款动作
巨划算/微信来源 + 银联,同日调用支付退款服务
巨划算/微信来源 + 银联,跨日建线下退款记录并打清分标识
巨划算/微信来源 + 挂账直接把退款单设为已完成
巨划算/微信来源 + 套包回补套包库存和缓存
巨划算/微信来源 + 普通商品回补 SAAS 商品库存
其他来源调用 TdmgrProvider::refund
flowchart TD
    A["收到app_apply_return_finish"] --> B{"SAAS退款单存在?"}
    B -->|否| X["记录不存在并ACK"]
    B -->|是| C{"状态是否11待退款?"}
    C -->|否| Y["告警并ACK"]
    C -->|是| D{"巨划算/微信来源?"}
    D -->|否| E["TdmgrProvider退款"]
    D -->|是| F{"支付方式"}
    F -->|银联同日| G["在线退款"]
    F -->|银联跨日| H["线下退款+清分标识"]
    F -->|挂账| I["退款单直接完成"]
    G --> J["回补SAAS库存"]
    H --> J
    I --> J

19.3 关键可靠性边界

SAAS 两个消费者都在 finally 返回 ACK。业务异常会记录日志和告警,但消息不会因为抛错自动 NACK 重投。因此:

  • 审核事件消费失败会让 DGJ 已待入库、SAAS 仍待确认。
  • 完成事件消费失败会让 DGJ 库存已回增、SAAS 仍待退款。
  • 修复数据后需要明确的消息重放或业务补偿,不能等待自动重试。

20. 取消待审核申请

取消请求:

{
  "order_no": "RFC10001XXXX",
  "station_id": 10001
}

调用链:

sequenceDiagram
    participant User as SAAS用户
    participant RF as RefundService
    participant DGJ as DGJ cancelReturnOrder
    participant AR as ApplyReturnSer
    participant DB as 两边数据库
    User->>RF: 取消退款申请
    RF->>DB: 校验SAAS状态=0
    RF->>DGJ: 退款单号+sid
    DGJ->>AR: cancel
    AR->>DB: DGJ申请status 1→4
    DGJ-->>RF: success
    RF->>DB: 回退原单/明细退款中数量
    RF->>DB: SAAS退款单status 0→99

限制:

条件结果
DGJ 状态 1可以取消
DGJ 状态 2/3/4“单据状态已变更”
SAAS 状态不是 0SAAS 先拒绝
取消后用同一个退款单号重建DGJ 重复检查仍能找到取消单,可能拒绝

21. 挂账领料退货:创建即完成

21.1 适用条件

SaOrderSer::returnChargeOrder 处理“领料/自提类订单 + 挂账支付”的特殊退货。它与普通已发货退货最大的区别是:

  • 不进入人工审核。
  • 不等待 OPS 退货入库。
  • 创建申请时直接生成 150602 销售退货事实。
  • 同一请求中直接回增 DGJ 库存。
  • 申请单初始状态就是 3 已完成。

SAAS 创建退款单时也会识别这一组合,将退款单直接放到已提交/待退款阶段,而不是普通的 0 待确认。

21.2 主流程

sequenceDiagram
    participant U as SAAS用户
    participant RS as RefundService
    participant API as createChargeOrderReturn
    participant SO as SaOrderSer
    participant AR as ApplyReturnSer
    participant DB as DGJ数据库
    participant MQ as MQ
    U->>RS: 提交挂账领料退货
    RS->>API: 退款单号、原销售单、退货明细
    API->>SO: returnChargeOrder
    SO->>AR: 创建status=3申请
    SO->>DB: 生成150602销退单
    SO->>DB: 回增库存和实时库存
    SO->>DB: 生成移动商城退货同步数据
    SO-->>API: application_id/application_no
    API-->>RS: success
    SO->>MQ: inventory_event
    SO->>MQ: 销退单同步SAAS

21.3 同一事务内外发生了什么

顺序动作事务内可查询事实
1校验原销售单、退货商品和数量是校验失败则整体退出
2创建退货申请主明细是t_scm_apply_return_order*
3创建 150602 销退单是t_scm_sa_invoice_*
4回增库存流水和实时库存是t_scm_inventory_*、t_scm_inventory_real_time
5写移动商城退货关联是由 addMoveRetOrder 负责
6提交数据库事务-前述事实同时生效
7发送 inventory_event否下游库存事件
8SaInvoiceSer::sendInvoiceToSass否销退事实同步 SAAS

21.4 与普通退货的差异

对比项普通已发货退货挂账领料退货
DGJ 初始状态1 待审核3 已完成
OPS 审核需要不需要
OPS 入库需要不需要
150602 生成时机OPS 入库时申请创建时
DGJ 库存回增入库时创建时
addPaymentForApp入库时执行当前路径未执行
完成通知app_apply_return_finish当前代码中发送语句被注释
重要风险:不能只根据申请状态 3 判断 SAAS 已经完成资金退款。此路径没有普通退货的完成事件闭环,必须同时核对 SAAS 退款单状态、销退单同步和支付类型。

22. 未发货退款:关原销售单而不是做退货入库

22.1 业务语义

未发货退款处理销售订单已经创建,但全部或部分商品尚未形成实际出库事实的场景。因为货还没有从 DGJ 库存扣出,所以它不应生成 150602 销售退货入库,也不应再次增加库存。

入口为:

  • DGJ:inner/moveMall/Order/returnOrderNoOut
  • Service:SaOrderSer::returnOrderNoOut
  • SAAS:RefundInnerService 相关未发货退款流程

22.2 主流程

flowchart TD
    A["SAAS提交未发货退款"] --> B["DGJ校验原销售单和退款明细"]
    B --> C{"退全部还是退部分?"}
    C -->|overall| D["关闭整张销售订单"]
    C -->|portion| E["关闭指定未出库明细/数量"]
    D --> F["失效接单/待履约关系"]
    E --> F
    F --> G["创建状态3的退货申请事实"]
    G --> H["提交事务"]
    H --> I["发送app_apply_return_finish"]
    I --> J["return_type=2"]
    J --> K["SAAS继续退款"]

22.3 数据变化

数据对象是否变化说明
DGJ 退货申请是创建即 3 已完成,用于记录退款业务事实
原销售订单是整单或部分关闭
销售订单接单关系可能已关闭部分不再继续履约
150602 销退单否没有真实出库就不做退货入库
DGJ 库存否原流程没有扣出,不应回增
DGJ 收退款单由后续业务决定静态代码不足以概括所有支付方式
完成 MQ是app_apply_return_finish,return_type=2

22.4 return_type 三套含义不能混用

出现位置取值真实含义
已发货退货创建请求0/1正常品/不良品
未发货退款请求overall/portion整单关闭/部分关闭
完成 MQ1/2已发货退货/未发货退款

建议新接口或新事件拆成 goods_condition、close_scope、refund_scene 三个字段。现有代码必须结合接口和事件上下文解释,不能只看字段名。

23. PC 直接销售退货:另一条更强校验的链路

23.1 业务边界

PC 端 scm/InvSa/returnSale 和移动商城售后申请不是同一条链路。PC 直接销退通常由服务站人员在销售退货页面发起,直接形成 150602,核心实现位于:

  • Controller:application/controllers/scm/InvSa.php
  • Service:application/Services/InvSa/NormalSaleReturnSer.php
  • 可退数量:InventoryService::getCanReturnQty
  • 历史口径修正:NormalSaleSer::saReturnQtyFix
  • 新旧页面灰度:SaleReturnPrevilegeCheck

allcarpart/sale/SaReturnOrder.php 主要是打印或转发入口,不能当作销售退货业务创建核心。

23.2 可退数量口径

对同一原出库明细,可退数量可抽象为:

可退数量
= 原销售出库数量
- 已生效销售退货数量
+ 当前编辑单据原先占用的退货数量
+ 历史修复表允许补回的数量

历史修正会参考 t_bs_return_goods_qty_fix。排查“明明卖过却不能退”时,不能只查原销售出库数量。

23.3 强校验项

校验PC 直接销退移动商城申请入库
原出库明细存在强校验创建阶段依赖上游传参和原单
可退数量重新计算是入库阶段未看到同等级重算
仓库与货位是入库提交只校验必要字段
电池/序列号属性按商品类型校验依请求明细进入
历史退货和修复量纳入口径申请链路主要依赖 SAAS 控量
编辑时释放原占用支持申请链路无编辑

23.4 PC 主流程

flowchart LR
    A["PC销售退货页面"] --> B["InvSa::returnSale"]
    B --> C["NormalSaleReturnSer"]
    C --> D["读取原出库及已退数量"]
    D --> E["getCanReturnQty"]
    E --> F{"数量/仓库/货位/属性通过?"}
    F -->|否| X["返回具体校验错误"]
    F -->|是| G["生成150602销退单"]
    G --> H["库存回增"]
    H --> I["财务往来处理"]
    I --> J["事务提交"]
    J --> K["发送库存/同步事件"]

23.5 排查时先区分入口

现象优先判断
有 RFC 申请单号,需要审核入库移动商城/SAAS 申请链路
PC 页面直接生成 XS/SKD 退货单PC 直接销退链路
有退款单但没有真实出库未发货退款
挂账领料创建后立即有销退单挂账领料特殊链路
渠道售后单先查 CHANNEL_AFTERSALE*,不要套 RFC 流程

24. 表结构、分表和关系

24.1 DGJ 核心表

业务对象物理表分表规则关键字段
退货申请主表t_scm_apply_return_order不分表id、sid、order_no、src_order_no、status
退货申请明细t_scm_apply_return_order_info不分表order_id、sku_id、return_qty、return_type
销售订单t_scm_sa_order_0_{sid%16}16 张id、sid、bill_no、status
销售订单明细t_scm_sa_order_info_0_{sid%16}16 张oid、sku_id、qty、out_qty
销售出库/销退主表t_scm_sa_invoice_0_{sid%64}64 张id、sid、bill_no、trans_type、src_order_id
销售出库/销退明细t_scm_sa_invoice_info_0_{sid%64}64 张iid、sku_id、qty、warehouse_id、location_id
库存流水主表t_scm_inventory_0_{sid%128}128 张id、sid、bill_no、trans_type
库存流水明细t_scm_inventory_info_0_{sid%128}128 张iid、sku_id、inv_id、qty、cost
实时库存t_scm_inventory_real_time不分表sid、sku_id/inv_id、仓库、货位、实时数量
收退款主表t_scm_payment_{sid%32}32 张id、sid、bill_no、trans_type、金额
收退款明细t_scm_payment_info_{sid%32}32 张pid、来源单据、往来金额
历史可退修复t_bs_return_goods_qty_fix不分表原销售记录、补偿数量
异常退货日志t_scm_abnormal_return_log以实际模型为准异常退货修复过程
分表后缀必须按代码中的模型格式确认。本文给出当前常量和模型表达的取模关系,执行 SQL 前仍要用具体 Model 的 getTableName 或同类方法复核完整表名。

24.2 SAAS 核心表

业务对象表关键关联
原商城订单t_orderorder_id/order_no
原商城明细t_order_detail原商品、购买数、退款中/已退数量
退款主表t_refund_orderrefund_order_no 对应 DGJ order_no
退款明细t_refund_order_detailSKU、申请数量、金额
DGJ 申请回写退款表中的 sa_invoice_order_id/no 类字段实际保存 RFC 申请 id/no,字段名有历史语义

24.3 关系图

erDiagram
    SAAS_ORDER ||--o{ SAAS_ORDER_DETAIL : contains
    SAAS_ORDER ||--o{ SAAS_REFUND_ORDER : requests
    SAAS_REFUND_ORDER ||--o{ SAAS_REFUND_DETAIL : contains
    SAAS_REFUND_ORDER ||--|| DGJ_APPLY_RETURN : "refund_order_no=order_no"
    DGJ_APPLY_RETURN ||--o{ DGJ_APPLY_RETURN_INFO : contains
    DGJ_APPLY_RETURN }o--|| DGJ_SA_ORDER : "src_order_id/no"
    DGJ_APPLY_RETURN ||--o| DGJ_SA_RETURN_INVOICE : creates
    DGJ_SA_RETURN_INVOICE ||--o{ DGJ_SA_RETURN_INFO : contains
    DGJ_SA_RETURN_INVOICE ||--o{ DGJ_INVENTORY_FLOW : changes
    DGJ_SA_RETURN_INVOICE ||--o{ DGJ_PAYMENT : offsets

24.4 单号追踪优先级

  1. 有 RFC:先查退货申请,再取 sid、原销售单和申请状态。
  2. 有 SAAS 退款单号:按 refund_order_no = DGJ order_no 关联。
  3. 有 XS/SKD:查 trans_type=150602 的销退单,再反查来源申请。
  4. 只有原销售单:先确认是否已出库,再分别查申请、销退和未发货关闭记录。
  5. 只有 SKU:必须同时带 sid、时间范围、仓库和业务单号,避免扫分表。

25. 事务、幂等与并发

25.1 一致性边界

环节数据库事务幂等依据当前弱点
SAAS 创建退款单SAAS 本地事务退款单号/业务校验调 DGJ 与本地提交不是同库事务
DGJ 创建申请DGJ 本地事务Redis 10 秒锁 + sid/src_order_no 查询数据库查询和插入不是原子唯一约束
DGJ 审核未形成完整事务闭环仅允许状态 1状态先更新、MQ 后发送
DGJ 入库DGJ 本地事务仅允许状态 2状态判断不是明确 CAS;提交后 MQ 无 outbox
SAAS 审核消费消费者逻辑按退款单号和状态异常仍 ACK
SAAS 完成消费消费者逻辑要求状态 11异常仍 ACK,需人工重放

25.2 创建接口的两层防重

flowchart TD
    A["重复请求到达"] --> B{"Redis NX锁存在?"}
    B -->|是| X["拒绝重复提交"]
    B -->|否| C["设置10秒锁"]
    C --> D["查询sid+src_order_no是否已有申请"]
    D -->|有| Y["返回重复申请"]
    D -->|无| E["插入申请主明细"]
    E --> F["事务提交"]

Redis 锁只能压缩并发窗口,不能代替数据库唯一约束:

  • 锁过期后重试仍依赖数据库查询。
  • 两个进程在查询后、插入前仍存在竞态窗口。
  • 取消单仍然属于“已存在申请”,同号重建可能失败。
  • 若需要长期强幂等,应评估 sid + order_no 或 sid + src_order_no + scene 的唯一索引和兼容数据。

25.3 并发入库风险

sequenceDiagram
    participant A as 请求A
    participant B as 请求B
    participant DB as 数据库
    A->>DB: 读取申请status=2
    B->>DB: 读取申请status=2
    A->>DB: 创建150602和库存
    B->>DB: 也尝试创建150602和库存
    A->>DB: 更新申请status=3
    B->>DB: 更新申请status=3
    Note over A,B: 若没有行锁/唯一约束/CAS,存在重复入库窗口

静态代码能确认状态校验存在,但不能仅凭 Service 代码证明底层模型已使用 SELECT ... FOR UPDATE 或唯一索引。因此并发压测和数据库索引核验是发布前必要项。

25.4 建议的强幂等键

动作建议幂等键
创建退货申请sid + refund_order_no
审核申请application_id + target_status=2
退货入库application_id + trans_type=150602
库存回增sid + business_bill_no + sku + warehouse + location
审核 MQevent_type + application_id + status_version
完成 MQevent_type + application_id + return_scene
SAAS 退款执行refund_order_no + pay_channel + refund_amount

26. 失败窗口与补偿

26.1 失败矩阵

失败位置DGJ 状态SAAS 状态库存/资金影响自动重试建议动作
SAAS 本地创建前失败无申请无退款单无由请求方重试修正入参后重提
SAAS 已建退款单,DGJ 调用失败无申请可能已存在待确认无依 SAAS 事务实现核对本地回滚,再决定重提
DGJ 已建申请,SAAS 响应超时1创建方未知无库存变化请求重试受防重保护按退款单号查询,不直接换单号
DGJ 审核状态更新失败10无可重新审核先查 DB 再操作
DGJ 审核已变 2,审核 MQ 失败20无状态重审不能补发补发 checked 事件或修正 SAAS 状态
DGJ 入库事务回滚211无新增库存/销退可修正后重入库核对没有半成品单据
DGJ 入库提交,库存 MQ 失败311DGJ 库存已增无 outbox 保证补发 inventory_event
库存 MQ 成功,完成 MQ 失败311DGJ 已完成,SAAS 未退款无补发 finish 事件
SAAS checked 消费异常20无消费者 ACK修数据后重放 checked
SAAS finish 消费时状态不是 1130DGJ 已增库存,SAAS 未退款消费者 ACK先补 checked,再重放 finish
在线退款渠道失败311DGJ 已完成,资金未退消费者 ACK按支付渠道规则人工重试
SAAS 库存回补失败3取决于失败点DGJ/SAAS 库存不一致消费者 ACK对账并执行幂等库存补偿
挂账领料同步失败3可能未完成DGJ 销退和库存已完成无 finish 事件兜底核对销退同步日志并补发

26.2 补偿决策树

flowchart TD
    A["收到售后异常"] --> B{"DGJ申请存在?"}
    B -->|否| C{"SAAS退款单存在?"}
    C -->|否| C1["修正请求后重新创建"]
    C -->|是| C2["核对SAAS是否应回滚或重调DGJ"]
    B -->|是| D{"申请状态"}
    D -->|1待审核| E["核对业务后正常审核/取消"]
    D -->|2待入库| F{"SAAS是否11待退款?"}
    F -->|否| F1["补checked事件/修正SAAS状态"]
    F -->|是| F2["继续正常入库"]
    D -->|3已完成| G{"150602是否应存在?"}
    G -->|已发货且不存在| G1["停止自动补库存,确认路径和数据缺口"]
    G -->|存在| H{"库存流水和实时库存一致?"}
    H -->|否| H1["按业务单做幂等库存补偿"]
    H -->|是| I{"SAAS退款完成?"}
    I -->|否| I1["按顺序补checked/finish/支付退款"]
    I -->|是| I2["闭环完成"]
    D -->|4已取消| J["确认SAAS=99且原单退款占用已释放"]

26.3 补偿顺序

  1. 冻结继续人工操作,保存申请号、原单号、退款单号和 sid。
  2. 判断退货场景:普通已发货、挂账领料、未发货、PC 直接销退。
  3. 查申请状态和 SAAS 退款状态,确定断点。
  4. 查 150602、库存流水、实时库存和退款记录。
  5. 只补缺失的一步,不重跑整条链路。
  6. MQ 补发携带原业务幂等键,并先确认消费者当前状态门槛。
  7. 补偿后做两边状态、数量、金额和消息日志四组对账。

26.4 禁止操作

  • 不要仅因 SAAS 未退款就再次执行 DGJ 入库。
  • 不要直接把 DGJ 2 改成 3 而不生成销退、库存和财务事实。
  • 不要先补发 finish,再补 checked;SAAS finish 要求状态 11。
  • 不要把未发货退款当作库存回增。
  • 不要只修实时库存而遗漏库存流水和来源单据。
  • 不要在生产直接运行 tasks/SaleReturn.php::fix;当前代码包含硬编码 sid=1092,必须先专项审查。

27. 按现象排查

27.1 提交退货提示重复

flowchart TD
    A["提示重复提交/已有申请"] --> B["按refund_order_no查DGJ申请"]
    B --> C{"是否存在?"}
    C -->|是| D{"状态"}
    D -->|1/2/3| E["复用原申请继续流程"]
    D -->|4| F["确认业务是否允许换新退款单号"]
    C -->|否| G["检查10秒Redis锁和并发请求日志"]
    G --> H["确认是否请求超时但DB已提交"]

检查顺序:

  1. 用退款单号查 t_scm_apply_return_order。
  2. 查同一 sid + src_order_no 是否已有申请。
  3. 对齐 SAAS t_refund_order。
  4. 查调用日志是否存在超时、重复点击或网关重试。
  5. 仅在确认 DGJ 没有业务事实后才允许重提。

27.2 DGJ 待入库,SAAS 仍待确认

这通常是 app_apply_return_checked 发送或消费失败。

  1. 确认 DGJ 申请已经从 1 变为 2。
  2. 查 DGJ MQ 发送日志。
  3. 查 SAAS app_apply_return_checked.log。
  4. 查 SAAS 退款单是否存在且当前为 0。
  5. 修正阻塞原因后,幂等补发 checked 事件或按审批结果修正状态。

27.3 DGJ 已完成,SAAS 一直待退款

flowchart TD
    A["DGJ状态3/SAAS状态11"] --> B{"退货场景"}
    B -->|普通已发货| C["查150602和finish MQ"]
    B -->|挂账领料| D["查sendInvoiceToSass及特殊退款分支"]
    B -->|未发货| E["查close结果和return_type=2事件"]
    C --> F{"finish消费日志成功?"}
    E --> F
    F -->|否| G["修复后补发finish"]
    F -->|是| H["查支付渠道退款及SAAS库存回补"]

消费者异常也会 ACK,不能只看“收到消息”,必须看业务日志结果和退款单最终状态。

27.4 库存多了一次

  1. 按业务单号查 150602 是否重复。
  2. 按销退单号查库存流水主表是否重复。
  3. 比较申请数量、销退明细数量、流水数量和实时库存增量。
  4. 检查是否同时走了移动商城入库和 PC 直接销退。
  5. 检查并发入库请求日志。
  6. 修复时以业务流水为依据,不直接凭当前库存做减法。

27.5 申请商品和实际入库商品不一致

优先核对:

  • 入库请求 entries 是否与申请明细逐行一致。
  • 同 SKU 是否来自多个仓库/货位,inStorageDetail 的按 SKU 映射是否覆盖。
  • 数量是否超过申请数。
  • 请求中的 sku_id、仓库、货位是否来自前端可编辑字段。
  • 150602 明细和库存流水最终写入了什么。

当前静态代码未证明入库阶段会完整重算“申请 SKU + 申请数量 + 原出库可退数量”,因此这是高优先级安全回归点。

28. 常用只读 SQL

以下 SQL 只用于定位思路。字段名、库名和完整分表名必须先按实际环境模型确认;生产环境只执行只读查询,不在文档中保存连接信息。

28.1 计算分表

SELECT
  :sid AS sid,
  MOD(:sid, 16)  AS sa_order_shard,
  MOD(:sid, 64)  AS sa_invoice_shard,
  MOD(:sid, 128) AS inventory_shard,
  MOD(:sid, 32)  AS payment_shard;

28.2 查退货申请

SELECT
  id, sid, order_no, src_order_id, src_order_no,
  status, checked, created_at, updated_at
FROM t_scm_apply_return_order
WHERE sid = :sid
  AND (order_no = :refund_order_no OR src_order_no = :source_order_no)
ORDER BY id DESC;
SELECT
  order_id, sku_id, return_qty, return_type,
  warehouse_id, location_id
FROM t_scm_apply_return_order_info
WHERE order_id = :application_id
ORDER BY id;

28.3 查原销售单

SELECT id, sid, bill_no, status, pay_type, created_at, updated_at
FROM t_scm_sa_order_0_N
WHERE sid = :sid
  AND (id = :source_order_id OR bill_no = :source_order_no);
SELECT id, oid, sku_id, qty, out_qty, close_qty
FROM t_scm_sa_order_info_0_N
WHERE oid = :source_order_id
ORDER BY id;

28.4 查 150602 销退单

SELECT
  id, sid, bill_no, trans_type, src_order_id, src_order_no,
  qty, amount, created_at
FROM t_scm_sa_invoice_0_N
WHERE sid = :sid
  AND trans_type = 150602
  AND (
    src_order_id = :application_id
    OR src_order_no = :refund_order_no
    OR bill_no = :sale_return_bill_no
  )
ORDER BY id DESC;
SELECT
  iid, sku_id, qty, warehouse_id, location_id, cost
FROM t_scm_sa_invoice_info_0_N
WHERE iid = :sale_return_invoice_id
ORDER BY id;

28.5 查库存流水和实时库存

SELECT id, sid, bill_no, trans_type, business_date, created_at
FROM t_scm_inventory_0_N
WHERE sid = :sid
  AND bill_no = :sale_return_bill_no
ORDER BY id DESC;
SELECT iid, sku_id, inv_id, warehouse_id, location_id, qty, cost
FROM t_scm_inventory_info_0_N
WHERE iid = :inventory_id
ORDER BY id;
SELECT sid, sku_id, inv_id, warehouse_id, location_id, qty, updated_at
FROM t_scm_inventory_real_time
WHERE sid = :sid
  AND sku_id IN (:sku_ids)
  AND warehouse_id = :warehouse_id
ORDER BY sku_id, location_id;

28.6 查 DGJ 收退款

SELECT id, sid, bill_no, trans_type, amount, status, created_at
FROM t_scm_payment_N
WHERE sid = :sid
  AND (
    bill_no = :refund_order_no
    OR src_order_no = :sale_return_bill_no
  )
ORDER BY id DESC;
SELECT pid, src_bill_no, amount, account_id
FROM t_scm_payment_info_N
WHERE pid = :payment_id
ORDER BY id;

28.7 查 SAAS 退款单

SELECT
  id, order_id, refund_order_no, order_status,
  refund_amount, sa_invoice_order_id, sa_invoice_order_no,
  created_at, updated_at
FROM t_refund_order
WHERE refund_order_no = :refund_order_no
ORDER BY id DESC;
SELECT refund_order_id, order_detail_id, sku_id, refund_qty, refund_amount
FROM t_refund_order_detail
WHERE refund_order_id = :refund_id
ORDER BY id;

28.8 四方数量对账

-- 分别从申请明细、150602明细、库存流水明细和SAAS退款明细汇总,
-- 再以 sku_id 为键比较。不要跨库直接照抄执行,建议分别查询后离线对齐。
SELECT sku_id, SUM(return_qty) AS apply_qty
FROM t_scm_apply_return_order_info
WHERE order_id = :application_id
GROUP BY sku_id;

期望关系:

普通已发货退货:
申请数量 = 150602数量 = DGJ库存回增数量 = SAAS退款明细数量

未发货退款:
申请关闭数量 = SAAS退款明细数量
且 150602数量 = 0、DGJ库存回增数量 = 0

29. 日志与代码检索

29.1 DGJ 代码定位

rg -n "createOrderReturn|createChargeOrderReturn|returnOrderNoOut|cancelReturnOrder" \
  application/controllers application/config

rg -n "function (add|check|inStorage|cancel|orderFinish)" \
  application/Services/ApplyReturn/ApplyReturnSer.php

rg -n "retOrder2|returnChargeOrder|returnOrderNoOut|addPaymentForApp" \
  application/Services/SaOrders/SaOrderSer.php

rg -n "sendApplyReturnCheckedToAPP|sendApplyReturnFinishToAPP|sendInventoryEvent" \
  application/Services/Mq/MqSer.php

29.2 SAAS 代码定位

rg -n "createOrderReturn|createChargeOrderReturn|returnOrderNoOut|cancelReturnOrder" \
  app/Providers/Dgj app/Service

rg -n "APP_APPLY_RETURN_CHECKED|APP_APPLY_RETURN_FINISH|returnChecked|returnFinish" \
  app/Amqp/Consumer/Dgj

rg -n "ORDER_STATUS_WAIT_ACK|ORDER_STATUS_SUBMITTED|ORDER_STATUS_FINISH|ORDER_STATUS_CANCEL" \
  app/Common/Constants/RefundOrderEnums.php

29.3 日志关键字

系统日志/关键字用途
SAAS 调 DGJ/provider/dgj/createOrderReturn.log创建请求、响应和耗时
SAAS 审核消费/consumer/dgj/app_apply_return_checked.logchecked 事件业务结果
SAAS 完成消费/consumer/dgj/app_apply_return_finish.logfinish、支付退款和库存回补
DGJ APIorder_no、src_order_no、接口路径判断请求是否到达
DGJ MQapp_apply_return_checked、app_apply_return_finish、inventory_event发送是否成功
支付渠道refund_order_no、支付流水号判断真实资金退款

日志检索至少同时带两个条件,例如 refund_order_no + event_type 或 sid + application_id,避免同名业务单混淆。

30. 监控与对账

30.1 建议监控指标

指标维度告警建议
创建退货申请失败数接口、sid、错误码连续增长告警
申请状态停留时长状态 1/2、业务类型超过业务 SLA 告警
checked 发送/消费差小时、事件类型差值非零告警
finish 发送/消费差小时、退货场景差值非零告警
DGJ 3 且 SAAS 非完成数退款单、支付方式实时或小时对账
150602 与库存流水数量差sid、SKU、仓库差异即告警
重复 150602申请 id/退款单号差异即告警
消费者异常但 ACK 数事件类型、异常类必须单独统计
退款渠道失败数渠道、日期按资金 SLA 告警

30.2 每日对账口径

flowchart LR
    A["DGJ退货申请"] --> D["按退款单号/申请ID对齐"]
    B["DGJ 150602与库存"] --> D
    C["SAAS退款单与支付退款"] --> D
    D --> E{"状态、数量、金额一致?"}
    E -->|是| F["闭环归档"]
    E -->|否| G["生成差异类型"]
    G --> H["checked缺失"]
    G --> I["finish缺失"]
    G --> J["库存差异"]
    G --> K["资金退款差异"]

30.3 差异类型编码建议

编码含义
AR_NO_SAASDGJ 有申请,SAAS 无退款单
CHECKED_MISSINGDGJ 2,SAAS 仍 0
RETURN_FACT_MISSING普通已发货申请 3,无 150602
INVENTORY_MISMATCH销退数量与库存回增不一致
FINISH_MISSINGDGJ 3,SAAS 11
PAY_REFUND_MISSING业务完成但资金未退
DUP_RETURN_FACT同申请存在重复销退事实

31. 回归测试矩阵

31.1 主流程

编号场景关键断言
R01正常品已发货整单退货1→2→3;150602、库存、退款全闭环
R02不良品已发货退货商品状态、仓库和库存属性正确
R03已发货部分退货仅申请数量入库,原单剩余可退正确
R04多 SKU 退货每个 SKU 数量、金额、仓位一致
R05同 SKU 多仓位退货不发生 SKU 映射覆盖
R06挂账领料退货创建即完成,不重复审核入库
R07未发货整单退款原单关闭,无 150602、无库存回增
R08未发货部分退款仅指定明细关闭,其他明细可履约
R09PC 直接销售退货可退数量口径和 150602 正确

31.2 状态和权限

编号场景期望
R10无 APPLY_RETURN 权限访问列表拒绝
R11无 APPLY_RETURN_CHECK 审核拒绝
R12无 APPLY_RETURN_IN 入库拒绝
R13未授权访问 inStorageDetail当前权限注释风险需验证并修正
R14状态 2 再审核拒绝
R15状态 1 直接入库拒绝
R16状态 3 重复入库拒绝且无重复库存
R17状态 2 取消拒绝
R18取消后重复提交同退款号明确产品规则并验证返回

31.3 参数和边界

编号场景期望
R19空明细拒绝
R20SKU 不存在拒绝
R21数量为 0/负数拒绝
R22数量超过申请数必须拒绝
R23数量超过原单可退数必须拒绝
R24仓库为空拒绝
R25货位为空或不属于仓库拒绝
R26请求 SKU 不属于申请必须拒绝
R27重复 SKU 行明确合并或拒绝,结果不可重复入账
R28金额小数和舍入DGJ、SAAS、支付渠道一致

31.4 幂等与故障

编号场景期望
R2910 秒内重复创建只产生一张申请
R30Redis 锁过期后重复创建数据库层仍只产生一张
R31两个并发入库请求只产生一张 150602 和一次库存回增
R32审核 MQ 发送失败有可执行的补发路径
R33checked 消费异常告警且可重放,不能静默丢失
R34库存 MQ 发送失败DB 不重复,事件可补发
R35finish 消费先于 checked受控失败,补 checked 后可恢复
R36在线退款失败业务状态和资金状态可区分、可重试
R37重放 checked/finish消费幂等,不重复退款、不重复库存

31.5 老流程影响

编号回归对象原因
R38普通销售出库共用 SaOrderSer、库存和 MQ
R39PC 销售退货共用 150602、库存和财务
R40销售订单关闭未发货退款复用关单能力
R41移动商城订单查询退货状态和数量会回显
R42对账单/客户余额销退和负向收款影响往来
R43报表销售、毛利、库存、退货报表口径
R44渠道售后避免改公共 Service 影响渠道链路

32. 当前代码高风险点

风险证据可能后果建议
inStorageDetail 权限校验被注释Controller 可见越权查看入库预览恢复权限并补 R13
创建防重没有明确数据库唯一约束Redis 10 秒 + 先查后插并发重复申请评估唯一索引
审核先改状态再发 MQApplyReturnSer::checkMQ 失败后不能正常重审补发outbox 或事件补偿表
入库提交后再发 MQinStorage 调用链数据完成、下游不知情outbox + 定时补发
SAAS 消费异常仍 ACKDgjConsumer消息静默丢失失败落补偿表/可控 NACK
入库提交校验偏弱inStorage 参数校验超申请、错 SKU、错仓位服务端重读申请并逐行比对
inStorageDetail 按 SKU 映射当前组装逻辑同 SKU 多仓位覆盖以原明细/库存 id 作为键
状态更新未见明确 CASupdateByIid/update并发重复入库条件更新 where status=2
orderFinish 查询/更新边界弱主要按 id多租户隔离风险所有更新带 sid + id
同名 return_type 多义三类契约联调误解和错误分支新字段拆义,旧字段文档化
挂账领料 finish 事件被注释returnChargeOrderSAAS 闭环依赖其他同步明确契约并补监控
修复任务硬编码 sidtasks/SaleReturn.php::fix误操作其他数据禁止通用执行,参数化和审批

33. 静态代码无法证明的事项

以下事项必须通过环境配置、数据库定义、MQ 控制台或真实联调确认:

  1. 退货申请表是否已有唯一索引以及索引列。
  2. Model 更新方法是否隐含乐观锁、行锁或租户条件。
  3. RequestCheck 使用的 Redis 集群、TTL 时钟和故障降级策略。
  4. MQ producer 的 publisher confirm、持久化、重试和死信配置。
  5. dgj_notify、dgj 的真实交换机、队列绑定和消费并发。
  6. SAAS 消费异常告警是否稳定到达人,以及是否存在人工重放后台。
  7. 各支付渠道同日/跨日退款的生产规则和资金到账 SLA。
  8. 挂账领料路径当前产品契约是否确实不需要 finish 事件。
  9. 入库仓库/货位是否允许前端修改,以及 WMS 是否有二次约束。
  10. 线上物理分表名是否与代码默认格式完全一致。
  11. t_scm_abnormal_return_log 的现网用途和数据修复治理流程。
  12. 渠道、预售、采购退货是否复用了同一公共方法的隐藏分支。

文档维护时,确认一项就把它移动到对应事实章节,并记录验证日期、环境和证据。

34. 证据索引

34.1 DGJ

结论权威代码
申请状态和未发货范围application/KzData/Enums/ApplyReturnEnums.php
创建、审核、入库、取消application/Services/ApplyReturn/ApplyReturnSer.php
OPS 权限入口application/controllers/applyReturn/ApplyReturnOrder.php
移动商城接口application/controllers/inner/moveMall/Order.php
Redis 10 秒防重application/Components/RequestCheck.php、RedisKeys
普通/挂账/未发货退货application/Services/SaOrders/SaOrderSer.php
PC 直接销退application/Services/InvSa/NormalSaleReturnSer.php
可退数量InventoryService::getCanReturnQty、NormalSaleSer::saReturnQtyFix
MQ payload 和 routing keyapplication/Services/Mq/MqSer.php
交易类型 150602application/KzData/Enums/TransTypeEnums.php
表名和分表数application/config/tables.php
高风险修复任务application/controllers/tasks/SaleReturn.php

34.2 SAAS

结论权威代码
DGJ 请求 URLapp/Providers/Dgj/OrderProvider.php
请求字段实体app/Providers/Dgj/Request/OrderReturnRequest.php、OrderReturnRequestEntity.php
创建和取消事务app/Service/Order/RefundService.php
未发货退款app/Service/Inner/RefundInnerService.php
退款状态app/Common/Constants/RefundOrderEnums.php
checked/finish 消费app/Amqp/Consumer/Dgj/DgjConsumer.php
事件类型app/Common/Constants/EventTypeEnums.php

34.3 阅读代码的推荐顺序

  1. 先读 ApplyReturnEnums 和 RefundOrderEnums,理解两边状态。
  2. 从 OrderProvider 确认真实接口 URL 和调用方。
  3. 读 RefundService,理解 SAAS 本地事务和字段映射。
  4. 读 DGJ Order Controller,区分四个入口。
  5. 读 SaOrderSer 的三个退货分支。
  6. 读 ApplyReturnSer 的审核、入库和取消。
  7. 读 MqSer 和 SAAS DgjConsumer,补齐异步闭环。
  8. 最后读 PC NormalSaleReturnSer,理解共用能力和老流程影响。

35. 一页式排查 SOP

flowchart TD
    START["输入:sid + RFC/退款单号 + 原销售单号"] --> S1["确认场景:已发货/挂账领料/未发货/PC直退"]
    S1 --> S2["查DGJ申请主明细和状态"]
    S2 --> S3["查SAAS退款主明细和状态"]
    S3 --> S4{"是否应生成150602?"}
    S4 -->|普通已发货/挂账领料| S5["查150602主明细"]
    S4 -->|未发货| S6["查原销售单关闭数量"]
    S5 --> S7["查库存流水与实时库存"]
    S6 --> S8["确认无库存回增"]
    S7 --> S9["查DGJ收退款和MQ发送"]
    S8 --> S9
    S9 --> S10["查SAAS checked/finish 消费"]
    S10 --> S11["查支付渠道真实退款"]
    S11 --> S12{"状态、数量、金额一致?"}
    S12 -->|是| DONE["记录证据,闭环"]
    S12 -->|否| FIX["按断点只补缺失步骤"]
    FIX --> VERIFY["重查四方数据和消息日志"]
    VERIFY --> DONE

记住六句话:

  1. 先分场景:已发货退货、未发货退款和 PC 直退不是一条链路。
  2. 先查状态再操作:DGJ 1/2/3/4 与 SAAS 0/11/1/99 要成对理解。
  3. 申请不等于入库:普通申请创建时库存不变。
  4. 完成不等于资金已退:DGJ 3 只证明 DGJ 业务完成。
  5. ACK 不等于消费成功:SAAS 异常路径也会 ACK。
  6. 只补断点,不重跑全链路:避免重复 150602、重复库存和重复退款。

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

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
E站售后申请inner/moveMall/Order原销售/出库单、SKU、数量、原因MoveMall OrderApplyReturnSer/SaOrderSerapply return ID售后申请主明细创建
PC 审核/applyReturn/ApplyReturnOrder申请单、审核动作、意见、可退量ApplyReturn ControllerApplyReturnSer申请单审核通过/拒绝状态
销售退货PC scm/InvSa原出库、退货商品/数量/金额InvSaNormalSaleReturnSer销售退货单退货业务单和退款/入库
退货入库/MQ仓库动作、售后通知退货单、仓位、实收量、消息 ID入库入口/ConsumerInventorySer、MqSer原出库+退货单150602 流水、库存增加、外部状态同步

日志证据矩阵

| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 申请 | MoveMall/ApplyReturnSer | request_id、原单、apply ID、SKU | 申请主明细 commit | 超可退量、来源单不符、重复申请 | apply ID 关联原出库 | | 审核 | ApplyReturn Controller/Service | apply ID、动作、操作人 | 状态单向到通过/拒绝 | 并发审核、非法状态 | 申请状态触发退货单 | | 退货入库 | Return Service/InventorySer | return/original invoice、SKU、150602 | 实收量和库存 +n | 重复入库、超收、仓位错 | 退货单查流水/实时表 | | 退款同步 | Payment/MQ | refund/pay order、原销售单、message ID | 退款一次完成并 ACK | 重复退款、金额不符、漏回调 | 原支付单串联退款和申请 |

环节数据变更台账

步骤代码位置事务读取事实写入表/缓存/MQ字段或数量变化回查证据
创建申请ApplyReturnSer申请事务已出、已申请/已退数量APPLY_RETURN 主明细insert;申请状态初始化;applyQty=n原出库+SKU 数量等式
审核ApplyReturn Service审核事务当前申请态、可退量申请主表/审核记录status pending -> approved/rejected操作人、时间、状态
建退货单NormalSaleReturnSer退货事务审核通过申请、原销售/出库销售退货主明细、关系returnQty 初始化;原单累计退货 +n申请/退货/原单三方关系
退货入库Return Service -> InventorySer退货和库存边界需确认可收量、仓位、实时库存库存流水/实时、退货状态实收 +n;库存 +n;流水 150602 一次return bill、transType、实时量
退款/通知Payment/MqSer外部异步应退金额、原支付退款关系、MQ、申请/退货状态refunded +n;最终状态完成原支付/退款单、ACK、重复执行 0 变化

子模块追踪:return-apply 售后申请创建

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
创建申请PC/移动商城发起售后source sale/invoiceNo、SKU、applyQty、reasonapplication/controllers/applyReturn/ApplyReturnOrder.php -> application/Services/ApplyReturn/ApplyReturnSer.php原有效出库、已申请/已退量、申请窗口和来源唯一性售后本地事务 insert 主明细,status none -> pending、applyQty=nrequest ID + source/applyNo + SKU超可申请量/重复来源零写入;超时按来源键回查
申请回查创建后无记录/数量错applyNo、source entryIdapplication/Components/RequestCheck.php申请主明细、原单和累计量查询只读 不写apply/source billNo + entryId主单无明细走领域取消/重建,不手补孤儿行

子模块追踪:return-audit 售后审核

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
审核动作审核通过/驳回售后applyNo、action、operator、remarkapplication/Services/ApplyReturn/ApplyReturnSer.php当前 pending、最新可退量和审核权限审核本地事务 pending -> approved/rejected,写操作人/时间request ID + applyNo + action/operator终态重复请求零变化;并发原单变化时重新校验可退量
审核后置通过后准备建退货单applyNo、approved qtyapplication/controllers/tasks/SaleReturn.php已审批申请和已有退货关系单任务本地事务创建关系;重复任务 0 新增task/message ID + applyNo + returnNo任务失败只补建单,不重新审核或累计申请量

子模块追踪:return-order 销售退货单创建

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
建退货单审核通过申请转销售退货apply/source/return billNo、SKU、qtyapplication/Services/InvSa/NormalSaleReturnSer.php申请终态、原销售/出库、已建关系和可退量退货本地事务 insert 主明细/关系,原单累计退货 old -> old+nrequest ID + three billNos + SKU来源唯一键防重复;任一明细失败整单回滚
关系回查申请通过但查不到退货单applyNo、source billNoapplication/Services/SaOrders/SaOrderSer.php申请、退货、原单三方关系查询只读 不写apply/source/return IDs已有退货单只补关系/响应,不重复建单

子模块追踪:return-inbound 退货入库与库存增加

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
收货入库销退商品到仓确认return billNo、SKU、location、receivedQtyapplication/Services/InvSa/NormalSaleReturnSer.php -> application/Services/Storage/InventorySer.php可收量、已收量、目标仓位、实时库存退货与库存本地事务 received +n、库存 qty +n、写 150602 流水request ID + return/inventory billNo + SKU超收/重复整单回滚;网络失败先查退货和库存流水
完成回查退货单完成但库存未回return entryId、inventory keyapplication/KzData/Enums/TransTypeEnums.php退货明细、150602 流水和实时量查询只读;三层数量应一致return/inventory billNo + transType只补缺失库存段,不再次累计退货数量

子模块追踪:return-refund 退款与资金回退

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
发起退款售后/退货达到退款条件apply/returnNo、original payNo、amountapplication/Services/Mq/MqSer.php原成功支付、应退/已退、退货状态本地退款关系事务 none -> pending;支付中心调用事务外request ID + original/refundNo + returnNotimeout 先查外部退款终态,禁止重复申请
退款落账退款结果回调message ID、refundNo、statusapplication/controllers/tasks/SaleReturn.php外部最终态、已有退款和本地售后态回调本地事务 refunded old -> old+n、状态到完成,重复 0 增量message ID + original/refund/return IDs库存完成不等于退款完成;资金链独立补偿

子模块追踪:return-reject-cancel 驳回、取消与额度释放

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
驳回取消审核驳回或用户取消未履约申请applyNo、action、reasonapplication/Services/ApplyReturn/ApplyReturnSer.php当前申请态、已建退货/退款和占用可退量售后本地事务 pending -> rejected/cancelled,释放申请占量 old -> old-nrequest ID + applyNo + action已建退货/退款后不得简单取消;按下游事实分支处理
授信释放取消涉及信用占额sourceOrderNo、credit hold、amountapplication/Services/Mq/MqSer.php原冻结/已用、已释放和外部账本额度本地事务 frozen/used -n, available +n,单键一次source/apply + adjust/message ID外部不确定先回查;不同时由两个流程重复返还

子模块追踪:return-notify 售后状态通知

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
状态出站申请审核、入库、退款完成后apply/return/source billNo、event/statusapplication/Services/Mq/MqSer.php已提交售后快照和下游目标commit 后事务外发 MQ,本地核心 DB 不变business IDs + routing key + message ID发布失败只补通知,不重做审核/库存/退款
入站同步外部售后状态事件message ID、event、external/local IDapplication/controllers/tasks/SaleReturn.php当前本地态、事件版本和关系单消息本地事务单向 old -> new,重复零副作用message ID + event + both IDs乱序不回退终态;环境 ACK 策略待验证