# 二期 v3:资源退费说明 CRUD 模块(给司机行程单展示用) > **服务**: hl-resource-service > **PR**: #3273 > **Issue**: #3272 > **日期**: 2026-05-30 > **影响**: 🟢 新增能力。给运营在景区/活动详情下"维护退费说明"提供 CRUD 接口;最终用于行程单 PDF 渲染 + 核单 Step 5 录入对账。本期**仅交付资源服务端**,后续 PR 接入产品快照 + PDF + 核单。 --- ## 总览(前端 mmg 必读) 每个景区(SCENIC)和活动(ACTIVITY)资源都可以挂一份"退费说明",包含资源级备注 + 多条退费明细(标题/金额/单位/备注/生效期)。 **界面**:景区 / 活动列表行加一个按钮 → 弹窗里调本期 3 个接口做 CRUD。 **接口前缀**:`/admin/refund-note`(**无 `/v3/` 前缀**,因 resource-service 沿用一期路径风格;消费方仍是二期 v3 管理后台)。 **本期不做**:行程单 PDF 渲染、产品 ProductDetailVO 注入 refundNote、订单 OrderProductSnapshotContent 反序列化、核单 Step 5 录入对接。这些留后续 PR。 --- ## 接口清单(3 个) ### 1. 查询单资源的退费说明 ``` GET /admin/refund-note?resourceType=SCENIC&resourceId=12345 ``` **入参**(query) | 字段 | 类型 | 必填 | 枚举 | 说明 | |---|---|---|---|---| | `resourceType` | string | ✅ | `SCENIC` / `ACTIVITY` | 资源类型 | | `resourceId` | long | ✅ | - | 资源 ID(字符串透传雪花)| **返回** ```jsonc { "code": 200, "data": { // 未配置时直接返 null,前端按 null 隐藏弹窗内容 "noteId": "20596378108368322580", // 雪花 ID 字符串透传 "resourceType": "SCENIC", "resourceId": "12345", "intro": "苔藓为赠送项目,不退费", "items": [ { "title": "成人未参加", "amount": 44.00, "unitLabel": "/人", // 展示文案,给人看 "settleScope": "PER_PERSON", // 结算粒度枚举,给规则引擎用 "remark": "", "effectiveFrom": null, // yyyy-MM-dd "effectiveTo": null }, ... ], "createTime": "2026-05-30 10:15:00", "updateTime": "2026-05-30 10:15:00" } } ``` **枚举** - `settleScope`: - `PER_PERSON` 按人结算(默认) - `PER_TEAM` 按团结算(如寻龙诀 100/团 整团一次性) - `PER_VEHICLE` 按车辆结算(如卡丁车 2 人/辆,按辆数乘单价) **注意**:`unitLabel`("/人" / "/团" / "/辆")和 `settleScope` **职责不同**: - 卡丁车场景:`unitLabel="/人"`(给客户看是 120/人)+ `settleScope=PER_VEHICLE`(引擎按车辆数乘单价) - 两个字段独立维护,前端展示用 `unitLabel`,未来规则引擎用 `settleScope`。 --- ### ⚡ 更新(2026-06-01 PR #3291):settleScope 字典化 + 出参补 settleScopeLabel 中文 后端**已落字典 + 接口出参直接返中文**,前端**不用自己维护英文→中文映射**。两种用法二选一: #### 用法 A:直接用出参的 `settleScopeLabel`(展示场景推荐) `GET /admin/refund-note` 返回的 `items[]` 每条**新增 `settleScopeLabel` 字段**: ```jsonc "items": [ { "title": "成人未参加", "settleScope": "PER_PERSON", "settleScopeLabel": "按人", // 🆕 后端字典拼装,直接展示 ... } ] ``` **列表 / 详情 UI 直接 `{{ item.settleScopeLabel }}` 渲染**即可,无需自己映射。 #### 用法 B:调字典接口拉下拉框选项(编辑场景推荐) 管理后台编辑退费规则时,需要下拉框让运营选"按人/按团/按车"。调统一字典接口: ```http GET /internal/dict/data?dictType=order_refund_settle_scope Authorization: Bearer → Result>: [ { "dictValue": "PER_PERSON", "dictLabel": "按人", "sortOrder": 10 }, { "dictValue": "PER_TEAM", "dictLabel": "按团", "sortOrder": 20 }, { "dictValue": "PER_VEHICLE", "dictLabel": "按车", "sortOrder": 30 } ] ``` - 下拉框 `option.label = dictLabel`,`option.value = dictValue` - 提交时 `req.items[].settleScope = dictValue`(提交 `settleScopeLabel` 后端会忽略,label 由后端拼) - 建议前端**全局缓存字典 5-10 分钟**避免高频请求;如果有统一字典组件直接复用 #### 容错(前端可忽略) - 字典服务异常 / 字典数据缺失 → `settleScopeLabel = null`,前端展示 fallback 用 `settleScope` 英文原值即可 - 字典文案后续改了(如"按人" → "按人头")→ **运维改 sys_dict_data + 清 Redis 缓存即生效**,后端无需重启,前端无需发版 #### 字典元信息 | 项 | 值 | |---|---| | `dict_type` | `order_refund_settle_scope` | | `dict_name` | 退费明细结算粒度 | | 字典数据 | PER_PERSON→按人 / PER_TEAM→按团 / PER_VEHICLE→按车 | | 字典存储 | `hl_user_service.sys_dict_data` | | 缓存 key | `cache:dict:data:order_refund_settle_scope` | ### 2. 保存(upsert 整块) ``` PUT /admin/refund-note ``` **入参**(body) ```jsonc { "resourceType": "SCENIC", "resourceId": 12345, "intro": "苔藓为赠送项目,不退费", "items": [ { "title": "成人未参加", // 必填 "amount": 44.00, // 必填,>= 0(赠送项填 0) "unitLabel": "/人", // 默认 "/人",可省略 "settleScope": "PER_PERSON", // 默认 "PER_PERSON",可省略 "remark": "", "effectiveFrom": null, "effectiveTo": null } ] } ``` **约束** - `resourceType`:必须是 `SCENIC` / `ACTIVITY` - `resourceId`:**必须在 scenic_spot / activity 表存在且未软删**,否则返 390805 "关联资源不存在" - `items`:至少 1 条,最多 50 条 - `items[].amount`:>= 0(赠送项目填 0) - `items[].settleScope`:必须是上述 3 枚举之一(null 时默认 PER_PERSON) - `items[].effectiveFrom` <= `items[].effectiveTo`(同时存在时) **返回** ```jsonc { "code": 200, "data": "20596378108368322580" } // 落库后的 noteId(字符串透传) ``` **语义**:upsert——按 (resourceType, resourceId) 找现有记录,有则**整块覆盖**(不做 item 级 diff),无则 insert。 ### 3. 软删整份 ``` DELETE /admin/refund-note?resourceType=SCENIC&resourceId=12345 ``` **入参**:同 GET。 **返回**:`{ "code": 200, "data": true }`(不存在或已软删返 false,不报错)。 软删用主键自身写 deleted_at,UNIQUE KEY 永不撞键,支持同资源无限次"删→重建"。 --- ## 错误码(段位 39080x) | code | 错误信息 | |---|---| | 390801 | 退费明细金额必须 ≥ 0 | | 390802 | 退费明细生效起日不能晚于止日 | | 390803 | 退费明细结算粒度非法: {0} | | 390804 | 退费说明资源类型非法: {0} | | 390805 | 关联资源不存在: type={0}, id={1} | 非业务错误(参数校验失败)走通用 400。 --- ## 业务边界 - **资源类型**:一期仅 `SCENIC` + `ACTIVITY`,其他资源(餐饮/酒店/物资等)有各自退订/退款政策,不复用本结构。 - **一份生效**:同资源同时刻只有一份生效的退费说明(DB 唯一键 `(resource_type, resource_id, deleted_at)` 保证)。 - **关联校验**:保存时校验 resourceId 在主资源表存在,防孤儿数据。 - **资源软删后**:本退费说明仍存在但孤儿(查不到对应资源),不主动清理;运营侧需手动删除。 --- ## 数据示例(典型) ### 白桦林(4 条规则) ```jsonc { "resourceType": "SCENIC", "resourceId": 12345, "intro": "苔藓为赠送项目, 不退费", "items": [ {"title": "儿童/学生/无证件未参加", "amount": 15, "unitLabel": "/人", "settleScope": "PER_PERSON"}, {"title": "免票座电瓶车", "amount": 30, "unitLabel": "/人", "settleScope": "PER_PERSON"}, {"title": "白桦林未参加", "amount": 44, "unitLabel": "/人", "settleScope": "PER_PERSON"}, {"title": "桦树皮画未参加", "amount": 50, "unitLabel": "/人", "settleScope": "PER_PERSON", "remark": "仅儿童"} ] } ``` ### 寻龙诀(按团结算) ```jsonc { "resourceType": "ACTIVITY", "resourceId": 99999, "items": [ {"title": "寻龙诀未参加", "amount": 100, "unitLabel": "/团", "settleScope": "PER_TEAM"} ] } ``` ### 卡丁车(按车结算,文案 /人) ```jsonc { "resourceType": "ACTIVITY", "resourceId": 88888, "items": [ {"title": "卡丁车未骑", "amount": 120, "unitLabel": "/人", "settleScope": "PER_VEHICLE", "remark": "2 人/辆共享单价"} ] } ``` ### 套娃(时间窗口) ```jsonc { "resourceType": "SCENIC", "resourceId": 77777, "items": [ {"title": "老人只看大马戏", "amount": 65, "settleScope": "PER_PERSON"}, {"title": "6/25 后没去或免票", "amount": 165, "settleScope": "PER_PERSON", "effectiveFrom": "2026-06-25", "effectiveTo": null} ] } ``` --- ## 影响评估 - **后端**:仅 hl-resource-service 新增 1 张表 `resource_refund_note` + 3 个 admin 接口,对其他业务零影响。 - **DB**:Flyway `V20260529_002__create_resource_refund_note.sql` 自动建表,重启服务后生效。 - **前端**:新增弹窗 UI(建议在景区 / 活动详情页加按钮 → 弹窗 CRUD),完全独立的新页面,老页面不影响。 --- ## 注意事项 1. 雪花 ID 透传为字符串(`noteId` / `resourceId`),前端**不要 `Number()`**。 2. 资源软删后的退费说明不会自动清理,运营侧需先删退费说明再删资源(否则成孤儿数据)。 3. 同资源不可有 2 份生效退费说明(DB 唯一键约束)。 4. 删除是软删,可同资源重建;不支持物理删除。 --- ## 关联 - **Issue**: [#3272](https://git.1814.love:8443/wx/HL/issues/3272) - **PR**: [#3273](https://git.1814.love:8443/wx/HL/pulls/3273) - feat(resource): 新增资源退费说明 CRUD 模块 - **Commit**: [ffae5ab90](https://git.1814.love:8443/wx/HL/commit/ffae5ab90) - **后续 PR**(不在本期): - 产品服务 `ProductDetailVO.NodeItem.refundNote` 注入(参考 #3147 serviceStandard 模式) - 订单 `OrderProductSnapshotContent` 加 refundNote 反序列化(10 行) - 行程单 PDF 渲染退费表(订单 v3 + PDF 服务) - 核单 Step 5 录入半结构化对接(可选)