# 售后管理 — 技术方案

> **依据：** 《售后管理功能需求.md》v1.0  
> **关联：** C 端《我的订单技术方案》v1.2；平台《订单管理技术方案》v1.0.2、《订单管理功能需求.md》v1.0.1、《关联需求分析.md》v1.6；商家《全部订单技术方案》v1.0.2、《发货管理技术方案》v1.0、《商家端店铺上下文接口说明》  
> **范围：** 商家端 **订单管理 · 售后管理** — 当前店铺售后列表、检索、详情、**处理完结**；**不含** C 端提交售后（已有 `OrderAftersaleAppController`）、平台仲裁。**退款** 在商家 **处理完结** 时 **自动原路退回**（见 §8.5）。
> **原则：** 读写 **`biz_order_aftersale`**、**`biz_refund_record`**；`shop_id = X-Shop-Id` 隔离；与 C 端 **同表同源**；完结 **更新** `process_result / aftersale_status / finish_time` 并 **关单**、**触发退款**。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 店铺上下文 | `SellerShopContextInterceptor` + **`X-Shop-Id`** |
| 脱敏 | 列表收货手机 `MemberMobileUtils.mask` |
| 日志 | 处理完结 `@Log` |
| 凭证/晒图 | 存 `evidence_pics` 逗号分隔 URL；VO 转 `List<String>`（复用 `OrderAppSupport.splitPics`） |

### 1.1 与 C 端 / 平台边界

| 维度 | C 端 · 我的订单 | **商家 · 售后管理** | 平台 · 订单管理 |
|------|-----------------|---------------------|-----------------|
| 路径 | `/api/order/aftersale/**` | **`/agri/seller/aftersale`** | `/agri/order`（**无** 售后菜单） |
| 写售后 | **POST submit**（已实现） | **PUT finish**（待建） | **无** |
| 读售后 | 按 **member_id** | 按 **shop_id** | **无** |
| 表 | `biz_order_aftersale` | **同表** | — |
| 退款 | **无** | **无** | 另册 |

### 1.2 协作链

```text
【C 端】订单详情 → POST /api/order/aftersale/submit → INSERT biz_order_aftersale（status=1）
            ↓
【登录】GET /agri/seller/context → X-Shop-Id
            ↓
【本模块】GET  /agri/seller/aftersale/list
           GET  /agri/seller/aftersale/{aftersaleId}
           PUT  /agri/seller/aftersale/{aftersaleId}/finish
            ↓
【C 端】GET /api/order/aftersale/{id} → 读 processResult + progressStage
```

| 关联模块 | 协作 |
|----------|------|
| **C 端 · 我的订单** | `OrderAftersaleAppServiceImpl.submit` 写入；**一行一进行中**（`countInProgressByOrderItem`） |
| **商家 · 全部订单** | 详情 **只读** `order_no`；可选跳转订单详情 |
| **商家 · 发货管理** | **不耦合**；履约与售后 **并行** |
| **平台 · 支付/退款** | **不调用**；商家在处理结果中 **文字说明** 退款安排 |
| **订单模块** | **不 UPDATE** `biz_order.order_status` / 库存 |

### 1.3 模块落位

**文档目录：** `doc/店铺后台/售后管理/`  
**菜单归属：** **订单管理** → 售后管理  
**后端包路径：** `com.ruoyi.web.modules.order`（与 C 端 `OrderAftersaleAppController` **同包**）

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/order/
├── controller/
│   ├── OrderAftersaleAppController.java    # C 端 /api/order/aftersale（已有）
│   └── SellerAftersaleController.java      # 商家 /agri/seller/aftersale（待建）
├── service/
│   ├── IOrderAftersaleAppService.java      # C 端（已有）
│   ├── ISellerAftersaleService.java        # 商家列表/详情/完结（待建）
│   └── impl/
│       ├── OrderAftersaleAppServiceImpl.java
│       └── SellerAftersaleServiceImpl.java
├── domain/BizOrderAftersale.java           # 已有
├── dto/
│   ├── OrderAftersaleSubmitDTO.java        # C 端（已有）
│   ├── SellerAftersaleQuery.java           # 商家列表检索（待建）
│   └── SellerAftersaleFinishDTO.java       # 完结 Body（待建）
├── vo/
│   ├── OrderAftersaleListRowVO.java        # C 端（已有）
│   ├── OrderAftersaleDetailVO.java         # C 端（已有）
│   ├── SellerAftersaleListRowVO.java       # 商家列表行（待建）
│   └── SellerAftersaleDetailVO.java        # 商家详情（待建）
├── mapper/BizOrderAftersaleMapper.java     # 扩展商家 SQL（待建）
├── constant/OrderAppConstants.java         # 售后枚举/msg（已有，可补商家 msg）
└── support/OrderAppSupport.java            # nextAftersaleNo / splitPics / progressStage（已有）

sql/biz_order_aftersale.sql                 # 权威 DDL
```

**前端（建议）：**

| 项 | 值 |
|----|-----|
| 页面 | `ruoyi-ui/src/views/agri/seller/aftersale/index.vue` |
| 详情 | `ruoyi-ui/src/views/agri/seller/aftersale/detail.vue`（抽屉） |
| API | `ruoyi-ui/src/api/agri/seller/aftersale.js` |

---

## 2. 数据库设计

**无新建表。** 权威 DDL：[`sql/biz_order_aftersale.sql`](../../../sql/biz_order_aftersale.sql)。

### 2.1 表 `biz_order_aftersale`

| 字段 | C 端写 | 商家列表 | 商家详情 | 商家写 | 说明 |
|------|:------:|:--------:|:--------:|:------:|------|
| aftersale_id | — | ✓ | ✓ | 条件 | PK |
| aftersale_no | — | ✓ | ✓ | — | **uk_aftersale_no** |
| shop_id | insert | 过滤 | ✓ | 条件 | **= X-Shop-Id** |
| order_id | insert | — | ✓ | — | |
| order_item_id | insert | — | ✓ | — | 并行校验键 |
| member_id | insert | — | — | — | C 端隔离 |
| apply_type | insert | ✓ | ✓ | — | `1/2/3` |
| apply_reason | insert | ✓ | ✓ | — | |
| return_quantity | insert | ✓ | ✓ | — | 类型 1 可 NULL |
| apply_amount | insert | ✓ | ✓ | — | |
| description | insert | — | ✓ | — | |
| evidence_pics | insert | — | ✓ | — | 逗号 URL |
| goods_* / order_no / pay_amount | insert | ✓ | ✓ | — | **快照** |
| member_nick_name / consignee_* | insert | ✓ | ✓ | — | 快照；手机 **脱敏** |
| aftersale_status | insert=`1` | ✓ | ✓ | **finish→`2`** | |
| process_result | — | — | ✓ | **finish** | 完结必填 |
| finish_time | — | ✓ | ✓ | **finish** | |
| create_time | insert | ✓ | ✓ | — | 申请时间 |
| update_time | insert | — | ✓ | finish | |

> **购买数量：** DDL **无** `quantity` 列；商家列表/详情 **JOIN** `biz_order_item`（`order_item_id = item_id`）取 `quantity` 展示（ASM4 快照口径补充）。

### 2.2 枚举

| 字段 | 值 | 含义 | 展示文案 |
|------|-----|------|----------|
| apply_type | `1` | 仅退款-未发货 | 仅退款-未发货 |
| apply_type | `2` | 仅退款-已发货 | 仅退款-已发货 |
| apply_type | `3` | 退货退款 | 退货退款 |
| aftersale_status | `1` | 进行中 | 进行中 |
| aftersale_status | `2` | 已完结 | 已完结 |

**进度 stage（VO 推导，不落库）：**

| aftersale_status | progressStage | C 端文案节点 |
|------------------|---------------|--------------|
| `1` | `MERCHANT_PROCESS` | 商家处理 |
| `2` | `FINISHED` | 售后完结 |

实现：复用 `OrderAppSupport.aftersaleProgressStage(aftersaleStatus)`。

### 2.3 商家列表 SQL 条件

```sql
WHERE a.shop_id = #{shopId}
  -- aftersaleNo / orderNo / applyType / aftersaleStatus（Query）
ORDER BY a.create_time DESC, a.aftersale_id DESC
```

**可选 JOIN（数量）：**

```sql
LEFT JOIN biz_order_item oi ON a.order_item_id = oi.item_id
```

### 2.4 索引

| 索引 | 用途 |
|------|------|
| `idx_shop_status (shop_id, aftersale_status, create_time)` | 商家列表 + 状态筛选 |
| `idx_member_status (member_id, aftersale_status, create_time)` | C 端列表 |
| `idx_order_item (order_id, order_item_id)` | 订单/明细关联 |
| `uk_aftersale_no (aftersale_no)` | 编号唯一 |

> **并行售后：** v1 由应用层 `countInProgressByOrderItem` 保证 **同一 order_item 仅一条进行中**；DDL **无** 部分唯一索引。

---

## 3. 接口设计

**基路径：** `/agri/seller/aftersale`  
**上下文：** **`X-Shop-Id`** 必填（`requireShopId()`）

### 3.1 接口一览

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/list` | `agri:seller:aftersale:list` | 分页列表 |
| GET | `/{aftersaleId}` | `agri:seller:aftersale:query` | 详情 |
| PUT | `/{aftersaleId}/finish` | `agri:seller:aftersale:finish` | 处理完结 |

### 3.2 列表 `GET /list`

**Query（SellerAftersaleQuery）：**

| 参数 | 说明 |
|------|------|
| pageNum, pageSize | 分页 |
| aftersaleNo | 售后编号（模糊） |
| orderNo | 订单编号（模糊） |
| applyType | 空=全部；`1` / `2` / `3` |
| aftersaleStatus | 空=全部；`1` 进行中 / `2` 已完结 |

**固定条件：** `shop_id = requireShopId()`。

**rows（SellerAftersaleListRowVO）：**

| 字段 | 说明 |
|------|------|
| aftersaleId, aftersaleNo | |
| applyType, applyTypeText | |
| orderNo | |
| goodsImage, goodsName, goodsSpec | 快照 |
| quantity | JOIN `biz_order_item.quantity` |
| payAmount | 订单实付快照 |
| memberNickName | |
| consigneeName, consigneeMobile | 手机 **脱敏** |
| consigneeAddress | |
| applyReason, returnQuantity, applyAmount | |
| createTime | `@JsonFormat yyyy-MM-dd HH:mm:ss` |
| aftersaleStatus, aftersaleStatusText | |
| finishTime | 已完结时有值 |
| actions | 可选：`QUERY` / `FINISH`（进行中） |

### 3.3 详情 `GET /{aftersaleId}`

**前置：** `shop_id` 匹配。

**data（SellerAftersaleDetailVO）：**

| 分区 | 字段 |
|------|------|
| progress | progressStage, aftersaleStatus, aftersaleStatusText |
| aftersale | aftersaleId, aftersaleNo, applyType, applyTypeText, applyReason, returnQuantity, applyAmount, createTime, finishTime |
| goods | goodsId, goodsImage, goodsName, goodsSpec, quantity |
| order | orderId, orderNo, payAmount |
| member | memberNickName |
| consignee | consigneeName, consigneeMobile, consigneeAddress |
| applyExtra | description, evidencePics[] |
| process | processResult（已完结）；进行中为 null |

### 3.4 处理完结 `PUT /{aftersaleId}/finish`

**Body（SellerAftersaleFinishDTO）：**

```json
{ "processResult": "已同意退款，预计3个工作日内到账，请注意查收。" }
```

| 规则 | 实现 |
|------|------|
| 前置 | `shop_id` 匹配；`aftersale_status = '1'` |
| 校验 | `processResult` 非空；长度 ≤1000 |
| 效果 | `UPDATE` 售后完结；订单 **关单**（`order_status→4`，`close_type=4`）；**自动退款** |
| 幂等 | 已完结再次调用 → 业务错误「售后已完结，不可重复处理」 |
| 退款 | 调用 `OrderAftersaleRefundServiceImpl.refundOnAftersaleFinish`；金额=`apply_amount`；Mock 模式直成功；真实模式调微信 V3 `/v3/refund/domestic/refunds` |
| 库存 | **不** 自动回滚 |

```text
商家点击「处理售后」
    → PUT /finish
    → SellerAftersaleServiceImpl.finish
    → BizOrderAftersaleMapper.updateForFinish
    → BizOrderMapper.updateForAftersaleClose
    → OrderAftersaleRefundServiceImpl.refundOnAftersaleFinish
    → C 端 GET detail 自动读到 processResult + FINISHED
```

### 3.5 自动退款（`biz_refund_record`）

| 项 | 说明 |
|----|------|
| 触发 | 商家 **处理完结** 成功后 **同步** 发起 |
| 金额 | 售后 `apply_amount`；须 ≤ 支付成功金额且 ≤ 订单可退余额 |
| 幂等 | `uk_aftersale_id`；同一售后仅退款一次 |
| Mock | `wechat.pay.mock=true` 或 `enabled=false` 时写入 `MOCK_RF{aftersale_no}` |
| 失败 | 抛业务异常，**整笔 finish 事务回滚**（售后/关单/退款均不生效） |

### 3.6 业务错误 msg

| 场景 | msg |
|------|-----|
| 未选店铺 | 请先选择当前店铺（`ShopConstants.MSG_SHOP_CONTEXT_REQUIRED`） |
| 越权/不存在 | 售后单不存在或无权访问（`OrderAppConstants.MSG_AFTERSALE_NOT_FOUND`） |
| 非进行中 | 售后已完结，不可重复处理 |
| 处理结果为空 | 请填写处理结果 |
| 无可退支付 | 未找到可退款的支付记录 |
| 退款失败 | 微信退款失败，请稍后重试 |

---

## 4. Service 分层

```text
SellerAftersaleController
    → ISellerAftersaleService
        → selectSellerList(shopId, query)
        → getSellerDetail(shopId, aftersaleId)
        → finish(shopId, aftersaleId, dto, operator)
        → BizOrderAftersaleMapper
        → OrderAppSupport（splitPics / progressStage / applyTypeText）

OrderAftersaleAppController（C 端 · 已有）
    → IOrderAftersaleAppService.submit / list / getDetail
```

**归属校验：**

```java
BizOrderAftersale row = aftersaleMapper.selectByIdAndShop(aftersaleId, shopId);
if (row == null) {
    throw new ServiceException(OrderAppConstants.MSG_AFTERSALE_NOT_FOUND);
}
```

**完结更新（乐观）：**

```sql
UPDATE biz_order_aftersale
SET process_result = #{processResult},
    aftersale_status = '2',
    finish_time = #{finishTime},
    update_time = #{updateTime}
WHERE aftersale_id = #{aftersaleId}
  AND shop_id = #{shopId}
  AND aftersale_status = '1'
```

`updated == 0` → 已完结或并发冲突 → 抛「售后已完结，不可重复处理」。

---

## 5. C 端接口协作（已有）

| 方法 | 路径 | 说明 | 状态 |
|------|------|------|------|
| POST | `/api/order/aftersale/submit` | 买家提交 | **已实现** |
| GET | `/api/order/aftersale/list` | tab=IN_PROGRESS/FINISHED | **已实现** |
| GET | `/api/order/aftersale/{aftersaleId}` | 详情+进度 | **已实现** |

> 商家 **完结** 后，C 端 **无需** 额外推送；读库即得 `processResult`、`finishTime`、`progressStage`。

**C 端 submit 关键规则（与商家只读字段对齐）：**

| 规则 | 实现类 |
|------|--------|
| 编号 AS+yyyyMMdd+6 位 | `OrderAppSupport.nextAftersaleNo()` |
| 类型 1 仅待发货 | `OrderAftersaleAppServiceImpl.validateApplyType` |
| 同行进行中拒绝 | `countInProgressByOrderItem > 0` |
| 金额 ≤ pay_amount | submit 内校验 |

---

## 6. 菜单与权限

**上级菜单：** **订单管理**（与全部订单、发货管理并列）

| 菜单 | 路由 | 组件 | 权限前缀 |
|------|------|------|----------|
| 售后管理 | `/agri/seller/aftersale` | `agri/seller/aftersale/index` | `agri:seller:aftersale` |

| 权限 | 说明 |
|------|------|
| `agri:seller:aftersale:list` | 列表 |
| `agri:seller:aftersale:query` | 详情 |
| `agri:seller:aftersale:finish` | 处理完结 |

---

## 7. Mapper 扩展（待建）

| 方法 | 说明 |
|------|------|
| `selectByIdAndShop(aftersaleId, shopId)` | 详情/校验 |
| `selectSellerList(shopId, query)` | 商家分页 + 可选 JOIN item |
| `updateForFinish(aftersale)` | 完结（status 1→2） |

C 端已有：`insert`、`selectMemberList`、`selectByIdAndMember`、`countInProgressByOrderItem`、`countInProgressByMember`、`selectMaxAftersaleNoByPrefix`。

---

## 8. 测试要点

| 编号 | 场景 | 预期 |
|------|------|------|
| T1 | 列表 shop 隔离 | 仅本店 |
| T2 | 检索 aftersaleNo/orderNo | 命中/空 |
| T3 | 详情越权 shop | 404 msg |
| T4 | finish 进行中 | status=2；processResult 写入 |
| T5 | finish 已完结 | 业务错误 |
| T6 | finish 空 processResult | 校验失败 |
| T7 | C 端 detail 完结后 | processResult + FINISHED |
| T8 | 列表手机脱敏 | mask |
| T9 | evidencePics 拆分 | List 长度正确 |

**建议测试类：**

| 类 | 类型 |
|----|------|
| `SellerAftersaleServiceImplTest` | 单元 |
| `SellerAftersaleControllerTest` | MockMvc |

---

## 9. 实现状态

| 项 | 状态 |
|----|------|
| DDL `biz_order_aftersale` | **已有** |
| C 端 submit/list/detail | **已实现** |
| `BizOrderAftersaleMapper` 商家 SQL | **待建** |
| `SellerAftersaleController` / Service | **待建** |
| ruoyi-ui 售后管理页 | **待建** |
| 退款原路 / 关单 / 回库存 | **另册** |

---

## 10. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：商家列表/详情/完结；复用 `biz_order_aftersale` 与 C 端写入链；与 ASM 规则及我的订单技术方案 §2.3 对齐 |

---

*文档版本：v1.0 · 关联《售后管理功能需求.md》v1.0、《我的订单技术方案.md》v1.2、《订单管理技术方案.md》v1.0.2 · DDL：[`sql/biz_order_aftersale.sql`](../../../sql/biz_order_aftersale.sql)*
