# 【修改接口·管理后台】退费说明融合进资源编辑接口（景区 / 活动 / 服务）

> **PR**: #3407（工单 #3406）
> **服务**: hl-resource-service | **更新时间**: 2026-06-03
>
> **存放目录**: `changelogs-v2/2026-06/`
> **影响范围**: 管理后台「资源管理 / 景区·游玩项目·服务」编辑页（基本信息）
> **状态**: 已合并 dev-v3 + 测试服部署双实例 + API 实测三类通过

---

## ⚠️ 关键说明

退费说明**不再走独立的** `/admin/refund-note` 接口，已**融合进资源编辑接口**——前端在景区/活动/服务编辑页里直接编辑退费说明，随资源一起保存、随详情一起回显，**一个接口搞定**。

- **覆盖三类资源**：景区 SCENIC、游玩项目 ACTIVITY、**服务 SERVICE（本次新增支持）**。
- **详情** `GET` 响应新增 `refundNote` 字段（未配置为 `null`）。
- **保存** `POST`(新建) / `PUT`(编辑) 请求体新增 `refundNote` 字段，三态语义见 §3.2。
- **独立接口 `/admin/refund-note` 仍保留**（向后兼容 + 订单快照内部仍用），但前端**改用融合接口**，不要再调独立接口。

> 替代说明：本文取代 `03_3389_...` changelog 中「退费说明接入（独立接口）」一节。前端**按本文融合接口接入**，勿按旧文的 `/admin/refund-note` 独立接口接。

---

## 1. 背景

退费说明（resource_refund_note）此前是独立 CRUD 模块（`/admin/refund-note`），前端要单独调一套接口维护。运营反馈应在资源编辑页里直接配，故融合进资源的详情/保存接口。同时把适用资源从「景区+活动」扩展到「景区+活动+**服务**」。

---

## 2. 变更清单

| # | 资源 | 详情（回显 refundNote） | 保存（含 refundNote） |
|---|------|------|------|
| 1 | 景区 Scenic | GET `/admin/scenic/spot/{scenicId}` | POST `/admin/scenic/spot`、PUT `/admin/scenic/spot/{scenicId}` |
| 2 | 游玩项目 Activity | GET `/admin/activity/item/{activityId}` | POST `/admin/activity/item`、PUT `/admin/activity/item/{activityId}` |
| 3 | 服务 Service（**新增**） | GET `/admin/service/item/{serviceId}` | POST `/admin/service/item`、PUT `/admin/service/item/{serviceId}` |

> 列表接口（`/spots`、`/items`）**不返回** refundNote（避免 N+1），退费说明只在单条详情里有。

---

## 3. 接口详情

### 3.1 详情回显

三类资源的详情响应（`ScenicSpotVO` / `ActivityVO` / `ServiceItemVO`）新增字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `refundNote` | object \| null | 退费说明；该资源未配置时为 `null` |
| `refundNote.noteId` | string | 退费说明 ID（雪花，String 透传防精度丢失） |
| `refundNote.intro` | string | 资源级备注 |
| `refundNote.items[]` | array | 退费明细（见 §6.2） |
| `refundNote.createTime` / `updateTime` | datetime | — |

### 3.2 保存（三态语义）

三类资源的新建/编辑请求体新增 `refundNote`（类型 `RefundNoteEmbedReqVO`，见 §6.1）。后端按 `refundNote` 是否传、items 是否为空分三态处理：

| 入参 refundNote | 行为 |
|---|---|
| **不传**（字段缺省 / null） | 保持该资源现有退费说明**不动** |
| 传了，`items` **为空数组或不传** | **删除**该资源退费说明（清空语义） |
| 传了，`items` **非空** | **整块 upsert**（连 intro 一起覆盖） |

> 编辑接口是字段级更新：只传 `refundNote` 即可单独维护退费说明，不影响其它字段。

### 3.3 示例（以景区为例，活动/服务同理换路径）

**保存退费说明**（PUT 只传 refundNote，其它字段不动）：

```bash
curl -X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{
    "refundNote": {
      "intro": "苔藓为赠送项目，不退费",
      "items": [
        { "title": "成人未参加", "amount": 50.00, "settleScope": "PER_PERSON", "unitLabel": "/人", "remark": "仅限成人" }
      ]
    }
  }' \
  "https://api.test.1814.love:9443/admin/scenic/spot/3001000000000000019"
```

**详情回显**（GET，data 节选）：

```json
{
  "code": 200,
  "data": {
    "scenicId": "3001000000000000019",
    "name": "中俄边境公路（卡线）",
    "refundNote": {
      "noteId": "20596378108368322580",
      "intro": "苔藓为赠送项目，不退费",
      "items": [
        { "title": "成人未参加", "amount": 50.00, "settleScope": "PER_PERSON", "settleScopeLabel": "按人", "unitLabel": "/人", "remark": "仅限成人" }
      ],
      "createTime": "2026-06-03 17:30:00",
      "updateTime": "2026-06-03 17:30:00"
    }
  }
}
```

**清空退费说明**（PUT 传空 items）：

```bash
curl -X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{ "refundNote": { "items": [] } }' \
  "https://api.test.1814.love:9443/admin/scenic/spot/3001000000000000019"
# 之后 GET，data.refundNote 为 null
```

---

## 6. 数据结构

### 6.1 `refundNote` 请求体（`RefundNoteEmbedReqVO`）

前端只传 intro + items，`resourceType` / `resourceId` 由后端从当前资源上下文回填，前端无需传。

| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
| `intro` | string | ❌ | `@Size(max=255)` | 资源级备注 |
| `items` | array | ❌ | `@Size(max=50)` | 退费明细；空/不传=清空 |

### 6.2 退费明细项（`items[]` / `RefundNoteItemVO`）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `title` | string | ✅ | 展示标题，如「成人未参加」 |
| `amount` | BigDecimal | ✅ | 退费金额（赠送项目填 0，不可为负） |
| `unitLabel` | string | ❌ | 展示文案 /人 /团 /辆 |
| `settleScope` | string | ❌ | 结算粒度 PER_PERSON / PER_TEAM / PER_VEHICLE |
| `settleScopeLabel` | string | — | 结算粒度中文（出参后端拼字典 order_refund_settle_scope，入参可省） |
| `remark` | string | ❌ | 备注 |
| `effectiveFrom` / `effectiveTo` | date | ❌ | 规则生效起止（null=无限制；同传时 from≤to） |

---

## 9. 业务边界

- ✅ **三态语义统一**：不传=不动 / 空 items=删除 / 非空=upsert（三类资源一致）。
- ✅ **同事务**：退费说明与资源主表在同一事务保存，主表失败则退费说明回滚。
- ✅ **列表不返回 refundNote**：仅单条详情回显，避免列表 N+1。
- ✅ **settleScopeLabel 后端拼**：出参按字典 `order_refund_settle_scope` 回填中文，字典异常时 fallback 为 null，不影响主流程。
- ✅ **独立接口保留**：`/admin/refund-note`（GET/PUT/DELETE）仍可用，订单快照内部仍取退费说明；前端改用融合接口即可。
- ✅ **items 上限 50**：与独立端点对齐。

---

## 11. 影响评估

- **是否破坏向后兼容**：否（详情新增字段、保存新增可选字段；独立接口未删）。
- **前端是否必须同步**：是（要在编辑页接入退费说明子表单，改用融合接口）。
- **影响已有数据**：无（存量退费说明照常，独立接口与融合接口读写同一张表）。

---

## 12. 注意事项

- **改用融合接口**：前端不要再调 `/admin/refund-note` 独立接口（虽保留但即将不维护）。
- **清空靠空 items**：要删退费说明就传 `refundNote:{items:[]}`，不是不传（不传=不动）。
- **Long 主键 String**：`refundNote.noteId` 为 String，勿当 Number 解析。
- **服务也支持了**：服务资源（ServiceItem）本次起可配退费说明，与景区/活动一致。

---

## 13. 关联

- 取代：`03_3389_资源默认收费+收费否禁价格日历+退费说明接入-管理后台.md` 的「退费说明（独立接口）」一节。
- 后端负责人：@wx
- 前端对接（管理后台）：mmg
