本文从真实代码出发,说明 DGJ2.0 与 SAAS/E站之间的销售退货申请、后台审核、退货入库、DGJ 财务冲账、下游退款和取消申请链路。
它要解决的不是“怎么新增一张销退单”这个单点问题,而是把下面六个容易混在一起的事实拆开:
- SAAS 退款单和 DGJ 退货申请单是两个系统里的两张单。
- DGJ 申请审核通过只进入“待入库”,不会直接增加库存。
- 真正的库存回增发生在生成
150602销退单和库存流水时。 - DGJ 会同时生成负金额的销售退款/冲账记录,但支付渠道退款由 SAAS 在收到完成事件后按支付方式执行。
- 已发货退货、未发货退款、挂账领料退货走的状态和库存路径不同。
- 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_log | PC 直接销退数量异常记录,不是申请主表 |
1.3 参与角色
| 角色 | 主要动作 | 系统边界 |
|---|---|---|
| 修理厂/E站用户 | 选择退货商品、数量、原因、正常品/不良品 | SAAS/E站 |
| SAAS 订单服务 | 计算可退数量和优惠分摊,创建退款单,调用 DGJ | SAAS |
| 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_no | OPS 查询、审核、入库 |
| DGJ 销退单 | XS... | t_scm_sa_invoice_0_N.billNo | 库存、账务和报表事实 |
| DGJ 退款/冲账单 | SKD... | t_scm_payment_N.billNo | DGJ 客户往来和账户流水 |
4.2 ID 传递关系
| 来源 | 字段 | 目标字段 | 说明 |
|---|---|---|---|
SAAS t_refund_order.id | order_id | 申请表 src_order_id | 回调消息中的 order_id |
SAAS refund_order_no | order_no | 申请表 src_order_no | SAAS 消费者按它查退款单 |
| SAAS 原订单 ID | src_order_id | 申请表 src_sa_order_id | 查 DGJ 原销售记录 |
| SAAS 原订单号 | src_order_no | 申请表 src_sa_order_no | 查 DGJ 销售单和出库 |
| DGJ 申请 ID | applyReturnId | 销退单 srcOrderId | 销退单来源指向申请 |
| DGJ 申请号 | bill_no | 销退单 srcOrderNo | 由申请号反查销退单 |
| DGJ 销退单 ID | saInvoiceId | 退款明细 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 主链路
| 层级 | 文件 | 关键方法 | 职责 |
|---|---|---|---|
| 内部 API | application/controllers/inner/moveMall/Order.php | createOrderReturn | 已发货退货申请 |
| 内部 API | 同上 | createChargeOrderReturn | 挂账领料退货 |
| 内部 API | 同上 | returnOrderNoOut | 未发货整单/部分退款 |
| 内部 API | 同上 | cancelReturnOrder | 取消待审核申请 |
| APP 销售服务 | application/Services/SaOrders/SaOrderSer.php | retOrder2 | 校验并创建普通申请 |
| APP 销售服务 | 同上 | returnChargeOrder | 申请和销退一次完成 |
| APP 销售服务 | 同上 | returnOrderNoOut | 创建完成态申请并关闭销售单 |
| 申请服务 | application/Services/ApplyReturn/ApplyReturnSer.php | add、check、inStorage | 申请主明细、审核、入库 |
| OPS 控制器 | application/controllers/applyReturn/ApplyReturnOrder.php | 五个接口方法 | 列表、详情、审核、入库 |
| 库存落账 | application/Services/SaOrders/SaOrderSer.php | saOrderOut | 建销退主明细并调用库存服务 |
| 库存服务 | application/Services/Storage/InventorySer.php | save | 库存流水和实时库存 |
| DGJ 财务 | SaOrderSer::addPaymentForApp | paymentMainFields | 生成退款/冲账主明细和账户流水 |
| MQ | application/Services/Mq/MqSer.php | 三个发送方法 | 审核、完成、库存事件 |
5.2 SAAS 上下游证据
| 层级 | 文件 | 关键方法 | 职责 |
|---|---|---|---|
| 退款业务 | app/Service/Order/RefundService.php | submit、cancel | 可退计算、退款单、调用 DGJ |
| 未发货退款 | app/Service/Inner/RefundInnerService.php | 未发货退款流程 | 生成退款单并调用 DGJ 关单 |
| DGJ Provider | app/Providers/Dgj/OrderProvider.php | 四个退货方法 | 组装 DGJ URL 并 POST JSON |
| 请求 DTO | app/Providers/Dgj/Request/OrderReturnRequest.php | setter 集合 | 主请求字段定义 |
| 明细 DTO | OrderReturnRequestEntity.php | setter 集合 | SKU、数量、价格、优惠和货位 |
| MQ 消费 | app/Amqp/Consumer/Dgj/DgjConsumer.php | returnChecked、returnFinish | 推进退款状态并执行退款 |
6. 接口总表
6.1 SAAS 调 DGJ
| 场景 | 方法与 URL | 核心 Service | 成功返回 | 关键副作用 |
|---|---|---|---|---|
| 已发货退货申请 | POST index.php/inner/moveMall/Order/createOrderReturn | retOrder2 | DGJ 申请 id、bill_no | 新增申请主明细,发站内消息 |
| 挂账领料退货 | POST .../createChargeOrderReturn | returnChargeOrder | 申请 id、bill_no | 申请完成、销退入库、移动退货记录 |
| 未发货退款 | POST .../returnOrderNoOut | returnOrderNoOut | 申请 id、bill_no | 申请完成、关闭销售未出库量、发完成事件 |
| 取消申请 | POST .../cancelReturnOrder | cancelReturnOrder | 更新结果 | DGJ 状态 1→4 |
6.2 DGJ OPS
| 场景 | Controller 方法 | 权限 | 核心请求 | 成功返回 | 副作用 |
|---|---|---|---|---|---|
| 列表 | ApplyReturnOrder::getList | APPLY_RETURN | 日期、客户、状态、搜索类型 | 分页 rows | 只读 |
| 详情 | detail | APPLY_RETURN | id | 主单、明细、原销售数量 | 只读 |
| 审核 | check | APPLY_RETURN_CHECK | id + 登录人 | 更新结果 | 1→2,发审核 MQ |
| 入库预览 | inStorageDetail | 当前权限检查被注释 | id | 默认门店、原出库仓位、候选货位 | 只读 |
| 确认入库 | inStorage | APPLY_RETURN_IN | postData JSON | 销退单 id、billNo | 销退、库存、退款冲账、申请完成、MQ |
6.3 PC 手工销退
| 入口 | 权限 | 服务选择 | 与申请链差异 |
|---|---|---|---|
scm/InvSa?action=returnSale | SA_ADD | 白名单决定新旧页面,最终进入销售退货 Service | 直接创建 150602,不创建 AR 申请 |
scm/InvSa?action=shopReturnSale | SA_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_id | SAAS 原订单 | DGJ 服务站 | DGJ 转成 sid |
order_id | SAAS 退款单 ID | 当前售后来源 ID | 不是原销售订单 ID |
order_no | SAAS 退款单号 | 跨系统回调键 | DGJ 用它做重复申请检查 |
src_order_id | SAAS 原订单 ID | 原销售来源 ID | 写入 src_sa_order_id |
src_order_no | SAAS 原订单号 | 查 DGJ 原销售单 | 入库必须能找到 |
contact_id | 原订单客户 | 退款客户 | 写入 customer_id |
totalQty | SAAS 结算 | 申请总数量 | DGJ 直接保存,未重新汇总 |
totalAmount | SAAS 结算 | 优惠处理后的退货总额 | 写申请 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 | 优惠券/折扣分摊 | discount | amount = -abs(amount-discount) |
is_gift | 是否赠品 | is_gift | 申请展示和上游匹配 |
areaId | SAAS 原/建议货位 | 当前申请表不保存 | 挂账领料等特殊流程使用 |
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_type | 1 | skey 按 DGJ 申请号查 |
search_type | 2 | skey 按 DGJ 销退单号查,再反查申请 |
status | 1/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_name | locationId 和仓库表 |
area_id、area_name | locationAreaId 和货位表 |
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 后 11 | MQ 丢失会出现状态不一致 |
| 入库成功 | 3 已完成 | 理论上仍 11,等待完成事件处理 | DGJ 库存已回增 |
| 下游退款执行 | 3 | 1 或支付处理中 | 由支付方式决定 |
| 取消 | 4 | 99 | 只允许待确认/待处理 |
11. 普通已发货申请创建
11.1 SAAS 侧先做业务结算
RefundService::submit 在本地事务中:
- 读取原订单和明细。
- 计算可退数量、原金额、优惠券分摊和可退金额。
- 校验退款后金额不能低于
0.1元。 - 校验申请退款金额不大于可退金额。
- 新增
t_refund_order和t_refund_order_detail。 - 累加原订单、原明细的退款中数量和金额。
- 组装
OrderReturnRequest调用 DGJ。 - 把 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站申请在 DGJvalidform_rt中没有再次根据历史150601/150602计算可退数量。可退数量主要由 SAAS 提交前控制;DGJ 申请入库接口也只校验数量大于 0。因此不能把“DGJ 一定会阻止超申请入库”写成既定事实。
11.3 防重复锁
内部 Controller 调用:
RequestCheck::checkRepeat(
RedisKeys::KEY_MOVE_OD_ADD_LOCK,
json_encode($data)
);
实际行为:
| 机制 | 键 | 有效期 | 能防什么 | 不能防什么 |
|---|---|---|---|---|
| Redis NX | dgj: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_qty | totalQty |
total_amount | totalAmount |
expect_amount、amount | expectAmount |
src_order_id/no | SAAS 退款单 ID/号 |
src_sa_order_id/no | SAAS 原订单 ID/号 |
acc_id、way_id | DGJ APP 结算账户和方式 |
status | 普通申请为 1 |
申请明细保存 sku_id、qty、price、return_price、amount、discount、remark、is_gift。仓库、货位不在申请明细中固化,实际入库时重新选择。
12. 审核流程
ApplyReturnSer::check:
- 按
sid + id + status=1读取申请。 - 更新
status=2。 - 写
check_id、check_name。 - 调用
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 属性 | 值 |
|---|---|
| destination | dgj_notify |
| routing key | app_apply_return_checked |
| DGJ 生产者 | MqSer::sendApplyReturnCheckedToAPP |
| SAAS 消费者 | DgjConsumer::returnChecked |
| SAAS 查询键 | refund_order_no = order_no |
13. 入库预览如何选仓库和货位
inStorageDetail 不是库存写入,它用于给页面准备候选数据:
- 读取申请主明细和客户。
- 按
src_sa_order_no找 DGJ 原销售单。 - 按销售单
billNo找原销售出库明细。 - 以
skuId建映射,默认带出原出库仓库和货位。 - 再调用库存服务加载当前门店可选仓库和货位。
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→3 | t_scm_apply_return_order |
事务提交后才发送两个 MQ。数据库事务不能回滚已经发生的外部消息,外部消息失败也不能回滚已提交数据库。
15. 销退单字段和正负方向
15.1 主表
saOutMainFieldsForApplyReturnId 和 saOutMainFields 共同组装实际销退单:
| 字段 | 值/规则 | 业务含义 |
|---|---|---|
billNo | 新 XS... | DGJ 销退单号 |
transType | 150602 | 销售退货 |
billStatus | 最终被覆盖为 5 | 已核销 |
srcOrderId | DGJ 申请 ID | 不是原销售单 ID |
srcOrderNo | DGJ AR... | 由销退反查申请 |
sourceType | 普通申请 7,领料场景可能 11 | APP退货来源 |
totalQty | 申请总数量,正数 | 退回数量 |
totalAmount | -abs(申请总额) | 负金额冲减销售 |
amount | -abs(申请期望额) | 实际退款/冲账金额 |
arrears | 负申请金额 | 冲减客户欠款 |
hxStateCode | 2 | 已核销口径 |
15.2 明细
| 字段 | 规则 |
|---|---|
qty | 使用入库请求正数 |
price | 申请明细 return_price |
amount | -abs(申请明细 amount - discount) |
disAmount | 与 amount 同方向 |
deduction | 申请明细 discount |
srcOrderEntryId | 申请明细 ID |
srcOrderId | DGJ 申请 ID |
srcOrderNo | DGJ 申请号 |
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["库存下游消费者"]
核对库存时要同时证明:
- 销退明细存在且未删除。
- 当月库存流水存在,数量方向为正。
- 实时库存对应
sid+invId+仓库+货位已增加。 inventory_event是否发送成功只影响异步下游,不代表本地库存是否落库。
17. DGJ 退款/冲账记录
addPaymentForApp 读取刚生成的销退单,然后:
- 创建
SKD...主单。 - 主单
billType=RECEIPT、transType=153001。 - 退款明细的
transType=150602、transTypeName=销售退款。 stlId/stlNo指向 DGJ 销退单。amount使用销退单负金额。- 调用
AccountService::saveAccounts写账户流水。
flowchart LR
XS["销退单 XS / 150602"] --> PM["退款主单 SKD / 153001"]
XS --> PI["退款明细 / 150602 / 销售退款"]
PM --> PI
PI --> ACC["账户流水"]
PI --> CHECK["客户余额/对账单"]
这里的“销售退款”是 DGJ 财务系统里的负向收款和往来冲账事实。它不等于银联、微信或第三方资金已经退回用户。
18. 审核和完成 MQ
18.1 事件矩阵
| 时机 | destination | routing key | payload | DGJ 事务关系 |
|---|---|---|---|---|
| 审核成功后 | dgj_notify | app_apply_return_checked | order_id、order_no | 状态更新后发送,无 outbox |
| 入库提交后 | dgj | inventory_event | 库存明细、sid 等 | DB commit 后发送 |
| 入库提交后 | dgj_notify | app_apply_return_finish | order_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:
- 按
refund_order_no = order_no查退款单。 - 更新
order_status=11待退款。 - 无论成功还是异常,
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 状态不是 0 | SAAS 先拒绝 |
| 取消后用同一个退款单号重建 | 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 | 否 | 下游库存事件 |
| 8 | SaInvoiceSer::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 | 整单关闭/部分关闭 |
| 完成 MQ | 1/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_order | order_id/order_no |
| 原商城明细 | t_order_detail | 原商品、购买数、退款中/已退数量 |
| 退款主表 | t_refund_order | refund_order_no 对应 DGJ order_no |
| 退款明细 | t_refund_order_detail | SKU、申请数量、金额 |
| 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 单号追踪优先级
- 有
RFC:先查退货申请,再取sid、原销售单和申请状态。 - 有 SAAS 退款单号:按
refund_order_no = DGJ order_no关联。 - 有
XS/SKD:查trans_type=150602的销退单,再反查来源申请。 - 只有原销售单:先确认是否已出库,再分别查申请、销退和未发货关闭记录。
- 只有 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 |
| 审核 MQ | event_type + application_id + status_version |
| 完成 MQ | event_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 审核状态更新失败 | 1 | 0 | 无 | 可重新审核 | 先查 DB 再操作 |
DGJ 审核已变 2,审核 MQ 失败 | 2 | 0 | 无 | 状态重审不能补发 | 补发 checked 事件或修正 SAAS 状态 |
| DGJ 入库事务回滚 | 2 | 11 | 无新增库存/销退 | 可修正后重入库 | 核对没有半成品单据 |
| DGJ 入库提交,库存 MQ 失败 | 3 | 11 | DGJ 库存已增 | 无 outbox 保证 | 补发 inventory_event |
| 库存 MQ 成功,完成 MQ 失败 | 3 | 11 | DGJ 已完成,SAAS 未退款 | 无 | 补发 finish 事件 |
| SAAS checked 消费异常 | 2 | 0 | 无 | 消费者 ACK | 修数据后重放 checked |
SAAS finish 消费时状态不是 11 | 3 | 0 | DGJ 已增库存,SAAS 未退款 | 消费者 ACK | 先补 checked,再重放 finish |
| 在线退款渠道失败 | 3 | 11 | DGJ 已完成,资金未退 | 消费者 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 补偿顺序
- 冻结继续人工操作,保存申请号、原单号、退款单号和
sid。 - 判断退货场景:普通已发货、挂账领料、未发货、PC 直接销退。
- 查申请状态和 SAAS 退款状态,确定断点。
- 查
150602、库存流水、实时库存和退款记录。 - 只补缺失的一步,不重跑整条链路。
- MQ 补发携带原业务幂等键,并先确认消费者当前状态门槛。
- 补偿后做两边状态、数量、金额和消息日志四组对账。
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已提交"]
检查顺序:
- 用退款单号查
t_scm_apply_return_order。 - 查同一
sid + src_order_no是否已有申请。 - 对齐 SAAS
t_refund_order。 - 查调用日志是否存在超时、重复点击或网关重试。
- 仅在确认 DGJ 没有业务事实后才允许重提。
27.2 DGJ 待入库,SAAS 仍待确认
这通常是 app_apply_return_checked 发送或消费失败。
- 确认 DGJ 申请已经从
1变为2。 - 查 DGJ MQ 发送日志。
- 查 SAAS
app_apply_return_checked.log。 - 查 SAAS 退款单是否存在且当前为
0。 - 修正阻塞原因后,幂等补发 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 库存多了一次
- 按业务单号查
150602是否重复。 - 按销退单号查库存流水主表是否重复。
- 比较申请数量、销退明细数量、流水数量和实时库存增量。
- 检查是否同时走了移动商城入库和 PC 直接销退。
- 检查并发入库请求日志。
- 修复时以业务流水为依据,不直接凭当前库存做减法。
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.log | checked 事件业务结果 |
| SAAS 完成消费 | /consumer/dgj/app_apply_return_finish.log | finish、支付退款和库存回补 |
| DGJ API | order_no、src_order_no、接口路径 | 判断请求是否到达 |
| DGJ MQ | app_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_SAAS | DGJ 有申请,SAAS 无退款单 |
CHECKED_MISSING | DGJ 2,SAAS 仍 0 |
RETURN_FACT_MISSING | 普通已发货申请 3,无 150602 |
INVENTORY_MISMATCH | 销退数量与库存回增不一致 |
FINISH_MISSING | DGJ 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 | 未发货部分退款 | 仅指定明细关闭,其他明细可履约 |
| R09 | PC 直接销售退货 | 可退数量口径和 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 | 空明细 | 拒绝 |
| R20 | SKU 不存在 | 拒绝 |
| R21 | 数量为 0/负数 | 拒绝 |
| R22 | 数量超过申请数 | 必须拒绝 |
| R23 | 数量超过原单可退数 | 必须拒绝 |
| R24 | 仓库为空 | 拒绝 |
| R25 | 货位为空或不属于仓库 | 拒绝 |
| R26 | 请求 SKU 不属于申请 | 必须拒绝 |
| R27 | 重复 SKU 行 | 明确合并或拒绝,结果不可重复入账 |
| R28 | 金额小数和舍入 | DGJ、SAAS、支付渠道一致 |
31.4 幂等与故障
| 编号 | 场景 | 期望 |
|---|---|---|
| R29 | 10 秒内重复创建 | 只产生一张申请 |
| R30 | Redis 锁过期后重复创建 | 数据库层仍只产生一张 |
| R31 | 两个并发入库请求 | 只产生一张 150602 和一次库存回增 |
| R32 | 审核 MQ 发送失败 | 有可执行的补发路径 |
| R33 | checked 消费异常 | 告警且可重放,不能静默丢失 |
| R34 | 库存 MQ 发送失败 | DB 不重复,事件可补发 |
| R35 | finish 消费先于 checked | 受控失败,补 checked 后可恢复 |
| R36 | 在线退款失败 | 业务状态和资金状态可区分、可重试 |
| R37 | 重放 checked/finish | 消费幂等,不重复退款、不重复库存 |
31.5 老流程影响
| 编号 | 回归对象 | 原因 |
|---|---|---|
| R38 | 普通销售出库 | 共用 SaOrderSer、库存和 MQ |
| R39 | PC 销售退货 | 共用 150602、库存和财务 |
| R40 | 销售订单关闭 | 未发货退款复用关单能力 |
| R41 | 移动商城订单查询 | 退货状态和数量会回显 |
| R42 | 对账单/客户余额 | 销退和负向收款影响往来 |
| R43 | 报表 | 销售、毛利、库存、退货报表口径 |
| R44 | 渠道售后 | 避免改公共 Service 影响渠道链路 |
32. 当前代码高风险点
| 风险 | 证据 | 可能后果 | 建议 |
|---|---|---|---|
inStorageDetail 权限校验被注释 | Controller 可见 | 越权查看入库预览 | 恢复权限并补 R13 |
| 创建防重没有明确数据库唯一约束 | Redis 10 秒 + 先查后插 | 并发重复申请 | 评估唯一索引 |
| 审核先改状态再发 MQ | ApplyReturnSer::check | MQ 失败后不能正常重审补发 | outbox 或事件补偿表 |
| 入库提交后再发 MQ | inStorage 调用链 | 数据完成、下游不知情 | outbox + 定时补发 |
| SAAS 消费异常仍 ACK | DgjConsumer | 消息静默丢失 | 失败落补偿表/可控 NACK |
| 入库提交校验偏弱 | inStorage 参数校验 | 超申请、错 SKU、错仓位 | 服务端重读申请并逐行比对 |
inStorageDetail 按 SKU 映射 | 当前组装逻辑 | 同 SKU 多仓位覆盖 | 以原明细/库存 id 作为键 |
| 状态更新未见明确 CAS | updateByIid/update | 并发重复入库 | 条件更新 where status=2 |
orderFinish 查询/更新边界弱 | 主要按 id | 多租户隔离风险 | 所有更新带 sid + id |
同名 return_type 多义 | 三类契约 | 联调误解和错误分支 | 新字段拆义,旧字段文档化 |
| 挂账领料 finish 事件被注释 | returnChargeOrder | SAAS 闭环依赖其他同步 | 明确契约并补监控 |
| 修复任务硬编码 sid | tasks/SaleReturn.php::fix | 误操作其他数据 | 禁止通用执行,参数化和审批 |
33. 静态代码无法证明的事项
以下事项必须通过环境配置、数据库定义、MQ 控制台或真实联调确认:
- 退货申请表是否已有唯一索引以及索引列。
- Model 更新方法是否隐含乐观锁、行锁或租户条件。
RequestCheck使用的 Redis 集群、TTL 时钟和故障降级策略。- MQ producer 的 publisher confirm、持久化、重试和死信配置。
dgj_notify、dgj的真实交换机、队列绑定和消费并发。- SAAS 消费异常告警是否稳定到达人,以及是否存在人工重放后台。
- 各支付渠道同日/跨日退款的生产规则和资金到账 SLA。
- 挂账领料路径当前产品契约是否确实不需要 finish 事件。
- 入库仓库/货位是否允许前端修改,以及 WMS 是否有二次约束。
- 线上物理分表名是否与代码默认格式完全一致。
t_scm_abnormal_return_log的现网用途和数据修复治理流程。- 渠道、预售、采购退货是否复用了同一公共方法的隐藏分支。
文档维护时,确认一项就把它移动到对应事实章节,并记录验证日期、环境和证据。
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 key | application/Services/Mq/MqSer.php |
交易类型 150602 | application/KzData/Enums/TransTypeEnums.php |
| 表名和分表数 | application/config/tables.php |
| 高风险修复任务 | application/controllers/tasks/SaleReturn.php |
34.2 SAAS
| 结论 | 权威代码 |
|---|---|
| DGJ 请求 URL | app/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 阅读代码的推荐顺序
- 先读
ApplyReturnEnums和RefundOrderEnums,理解两边状态。 - 从
OrderProvider确认真实接口 URL 和调用方。 - 读
RefundService,理解 SAAS 本地事务和字段映射。 - 读 DGJ
OrderController,区分四个入口。 - 读
SaOrderSer的三个退货分支。 - 读
ApplyReturnSer的审核、入库和取消。 - 读
MqSer和 SAASDgjConsumer,补齐异步闭环。 - 最后读 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
记住六句话:
- 先分场景:已发货退货、未发货退款和 PC 直退不是一条链路。
- 先查状态再操作:DGJ
1/2/3/4与 SAAS0/11/1/99要成对理解。 - 申请不等于入库:普通申请创建时库存不变。
- 完成不等于资金已退:DGJ
3只证明 DGJ 业务完成。 - ACK 不等于消费成功:SAAS 异常路径也会 ACK。
- 只补断点,不重跑全链路:避免重复
150602、重复库存和重复退款。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| E站售后申请 | inner/moveMall/Order | 原销售/出库单、SKU、数量、原因 | MoveMall Order | ApplyReturnSer/SaOrderSer | apply return ID | 售后申请主明细创建 |
| PC 审核 | /applyReturn/ApplyReturnOrder | 申请单、审核动作、意见、可退量 | ApplyReturn Controller | ApplyReturnSer | 申请单 | 审核通过/拒绝状态 |
| 销售退货 | PC scm/InvSa | 原出库、退货商品/数量/金额 | InvSa | NormalSaleReturnSer | 销售退货单 | 退货业务单和退款/入库 |
| 退货入库/MQ | 仓库动作、售后通知 | 退货单、仓位、实收量、消息 ID | 入库入口/Consumer | InventorySer、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、reason | application/controllers/applyReturn/ApplyReturnOrder.php -> application/Services/ApplyReturn/ApplyReturnSer.php | 原有效出库、已申请/已退量、申请窗口和来源唯一性 | 售后本地事务 insert 主明细,status none -> pending、applyQty=n | request ID + source/applyNo + SKU | 超可申请量/重复来源零写入;超时按来源键回查 |
| 申请回查 | 创建后无记录/数量错 | applyNo、source entryId | application/Components/RequestCheck.php | 申请主明细、原单和累计量 | 查询只读 不写 | apply/source billNo + entryId | 主单无明细走领域取消/重建,不手补孤儿行 |
子模块追踪:return-audit 售后审核
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 审核动作 | 审核通过/驳回售后 | applyNo、action、operator、remark | application/Services/ApplyReturn/ApplyReturnSer.php | 当前 pending、最新可退量和审核权限 | 审核本地事务 pending -> approved/rejected,写操作人/时间 | request ID + applyNo + action/operator | 终态重复请求零变化;并发原单变化时重新校验可退量 |
| 审核后置 | 通过后准备建退货单 | applyNo、approved qty | application/controllers/tasks/SaleReturn.php | 已审批申请和已有退货关系 | 单任务本地事务创建关系;重复任务 0 新增 | task/message ID + applyNo + returnNo | 任务失败只补建单,不重新审核或累计申请量 |
子模块追踪:return-order 销售退货单创建
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 建退货单 | 审核通过申请转销售退货 | apply/source/return billNo、SKU、qty | application/Services/InvSa/NormalSaleReturnSer.php | 申请终态、原销售/出库、已建关系和可退量 | 退货本地事务 insert 主明细/关系,原单累计退货 old -> old+n | request ID + three billNos + SKU | 来源唯一键防重复;任一明细失败整单回滚 |
| 关系回查 | 申请通过但查不到退货单 | applyNo、source billNo | application/Services/SaOrders/SaOrderSer.php | 申请、退货、原单三方关系 | 查询只读 不写 | apply/source/return IDs | 已有退货单只补关系/响应,不重复建单 |
子模块追踪:return-inbound 退货入库与库存增加
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 收货入库 | 销退商品到仓确认 | return billNo、SKU、location、receivedQty | application/Services/InvSa/NormalSaleReturnSer.php -> application/Services/Storage/InventorySer.php | 可收量、已收量、目标仓位、实时库存 | 退货与库存本地事务 received +n、库存 qty +n、写 150602 流水 | request ID + return/inventory billNo + SKU | 超收/重复整单回滚;网络失败先查退货和库存流水 |
| 完成回查 | 退货单完成但库存未回 | return entryId、inventory key | application/KzData/Enums/TransTypeEnums.php | 退货明细、150602 流水和实时量 | 查询只读;三层数量应一致 | return/inventory billNo + transType | 只补缺失库存段,不再次累计退货数量 |
子模块追踪:return-refund 退款与资金回退
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 发起退款 | 售后/退货达到退款条件 | apply/returnNo、original payNo、amount | application/Services/Mq/MqSer.php | 原成功支付、应退/已退、退货状态 | 本地退款关系事务 none -> pending;支付中心调用事务外 | request ID + original/refundNo + returnNo | timeout 先查外部退款终态,禁止重复申请 |
| 退款落账 | 退款结果回调 | message ID、refundNo、status | application/controllers/tasks/SaleReturn.php | 外部最终态、已有退款和本地售后态 | 回调本地事务 refunded old -> old+n、状态到完成,重复 0 增量 | message ID + original/refund/return IDs | 库存完成不等于退款完成;资金链独立补偿 |
子模块追踪:return-reject-cancel 驳回、取消与额度释放
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 驳回取消 | 审核驳回或用户取消未履约申请 | applyNo、action、reason | application/Services/ApplyReturn/ApplyReturnSer.php | 当前申请态、已建退货/退款和占用可退量 | 售后本地事务 pending -> rejected/cancelled,释放申请占量 old -> old-n | request ID + applyNo + action | 已建退货/退款后不得简单取消;按下游事实分支处理 |
| 授信释放 | 取消涉及信用占额 | sourceOrderNo、credit hold、amount | application/Services/Mq/MqSer.php | 原冻结/已用、已释放和外部账本 | 额度本地事务 frozen/used -n, available +n,单键一次 | source/apply + adjust/message ID | 外部不确定先回查;不同时由两个流程重复返还 |
子模块追踪:return-notify 售后状态通知
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 状态出站 | 申请审核、入库、退款完成后 | apply/return/source billNo、event/status | application/Services/Mq/MqSer.php | 已提交售后快照和下游目标 | commit 后事务外发 MQ,本地核心 DB 不变 | business IDs + routing key + message ID | 发布失败只补通知,不重做审核/库存/退款 |
| 入站同步 | 外部售后状态事件 | message ID、event、external/local ID | application/controllers/tasks/SaleReturn.php | 当前本地态、事件版本和关系 | 单消息本地事务单向 old -> new,重复零副作用 | message ID + event + both IDs | 乱序不回退终态;环境 ACK 策略待验证 |