feat(product-v2): 费用包含自动生成去掉住宿 + 每节点 1 条独立 VO 带行程图标 (#2477)
PR wx/HL#2484, 测试服已部署 + 数据清理(117 条 / 32 产品). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
这个提交包含在:
父节点
addb9cc8cf
当前提交
1326504962
@ -0,0 +1,193 @@
|
||||
# 产品费用包含: 自动生成不再产「住宿」+ 每节点 1 条独立 VO 带行程图标
|
||||
|
||||
> **服务**: hl-product-service-v2 (端口 8083)
|
||||
> **PR**: #2484
|
||||
> **Issue**: #2477
|
||||
> **日期**: 2026-05-18
|
||||
> **影响范围**: 管理端 产品编辑「补充信息 → 费用说明 → 费用包含」+ 小程序产品详情 includedFees
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
1. 「费用包含 → 根据行程自动生成」按钮的后端返回结构**从 N 条合并改为 N 条独立**:
|
||||
- 之前: 同一 feeType 的所有节点合并成 1 条,`name="A,B,C,D"` 用「,」拼接字符串
|
||||
- 现在: 每个 SCENIC / ACTIVITY 行程节点产出 1 条独立 `FeeDeductionPreviewVO`,`name=单个节点名`
|
||||
2. **不再产出 `feeType="住宿"`**:酒店表 `product_day_hotel` + HOTEL 行程节点两个来源全部禁用
|
||||
3. **icon 字段透传行程节点 `emoji_icon`**:可能是 lucide 图标名 / emoji / SVG 字符串,**前端需兼容渲染**
|
||||
4. 同名节点跨天去重:同一景区在多天出现只保留行程顺序最早的那条
|
||||
5. 排序:外层按 feeLabel 优先级(景区 → 游玩项目 → 餐饮 → 交通),内层按 dayId asc + sortOrder asc
|
||||
|
||||
---
|
||||
|
||||
## 一、背景
|
||||
|
||||
用户期望小程序产品详情「景区门票」「游玩项目」分组下每个景区/游玩项目独立 1 个 chip + 图标(参见小程序详情截图),但当前合并字符串无法满足,且自动产出的「住宿」条目跟专门住宿配置重复,语义混乱。
|
||||
|
||||
| 维度 | 旧行为 | 新行为 |
|
||||
|------|--------|--------|
|
||||
| 同 feeType 多节点 | 合并 1 条 name="A,B,C" | N 条独立 VO |
|
||||
| HOTEL 节点 / day_hotel | 产「住宿」 1 条 | **不产出** |
|
||||
| icon | 一直 null | 透传 `node.emojiIcon` |
|
||||
| 同名跨天 | 合并字符串中重复出现 | 去重保留首次出现 |
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 费用推导预览 | GET | `/admin/product/item/{id}/fee-deduction-preview` | 返回结构(数组语义) | 从 N 条合并 → N 条独立,无「住宿」,带 icon |
|
||||
| 2 | 保存补充信息 | PUT | `/admin/product/item/{id}/supplement` | 入参语义(不强制变) | `includedFees` 现在支持 N 条独立项 + icon(VO 字段已有,只是含义) |
|
||||
| 3 | 管理端产品详情 | GET | `/admin/product/item/{id}` | 返回 includedFees 数组形态 | DB 保存什么读什么(取决于用户保存方式) |
|
||||
| 4 | 小程序产品详情 | GET | `/mp/product/{id}` | 返回 includedFees 数组形态 | 同上 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 费用推导预览 `GET /admin/product/item/{id}/fee-deduction-preview`
|
||||
|
||||
**VO**: `FeeDeductionPreviewVO`
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| id | Path | Long | ✅ | 产品ID |
|
||||
|
||||
#### 出参 `Result<List<FeeDeductionPreviewVO>>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| feeType | String | 景区 / 游玩项目 / 餐饮 / 交通(**不再出现「住宿」**) |
|
||||
| name | String | 单个节点名(`node.nodeName`),**不再用「,」拼接** |
|
||||
| source | String | `AUTO`(常量) |
|
||||
| sourceNodeName | String | 来源节点名(目前 = name) |
|
||||
| icon | String? | 来自 `node.emojiIcon`:**可能是 lucide 名 / emoji / SVG 字符串**,前端兼容渲染;无图标的节点为 null |
|
||||
|
||||
#### 响应示例(测试服真测产品 2045345825172639746)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": [
|
||||
{
|
||||
"feeType": "景区",
|
||||
"name": "中俄边境公路(卡线)",
|
||||
"source": "AUTO",
|
||||
"sourceNodeName": "中俄边境公路(卡线)",
|
||||
"icon": "<svg xmlns=\"http://www.w3.org/...\"/>"
|
||||
},
|
||||
{ "feeType": "景区", "name": "黑山头", "source": "AUTO", "sourceNodeName": "黑山头", "icon": "<svg ...>" },
|
||||
{ "feeType": "景区", "name": "百里雾凇画廊", "source": "AUTO", "sourceNodeName": "百里雾凇画廊", "icon": "<svg ...>" },
|
||||
{ "feeType": "景区", "name": "海拉尔国家森林公园", "source": "AUTO", "sourceNodeName": "海拉尔国家森林公园", "icon": "<svg ...>" },
|
||||
{ "feeType": "游玩项目", "name": "满洲里草原篝火晚会", "source": "AUTO", "sourceNodeName": "满洲里草原篝火晚会", "icon": "<svg ...>" },
|
||||
{ "feeType": "游玩项目", "name": "冬日烟花", "source": "AUTO", "sourceNodeName": "冬日烟花", "icon": null },
|
||||
{ "feeType": "游玩项目", "name": "ATV全地形越野车穿越", "source": "AUTO", "sourceNodeName": "ATV全地形越野车穿越", "icon": "<svg ...>" },
|
||||
{ "feeType": "游玩项目", "name": "蒙古族小型冰雪那达慕", "source": "AUTO", "sourceNodeName": "蒙古族小型冰雪那达慕", "icon": "<svg ...>" }
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 2 / 3 / 4. 保存与详情接口
|
||||
|
||||
VO 结构未变(`FeeItem.icon` / `FeeInfo.icon` 字段早已预留),只是含义换成「N 条独立项」:每个景区/游玩项目 1 个 chip,前端无需聚合。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约与渲染建议(前端必看)
|
||||
|
||||
### icon 字段三种格式都要兼容渲染
|
||||
|
||||
| 来源 | icon 形态 | 渲染方式建议 |
|
||||
|------|-----------|------|
|
||||
| 自动推导(行程节点 emojiIcon) | lucide 图标名(如 `"mountain"`)或 emoji(如 `"🏔"`) | lucide 图标渲染器 + emoji 直接 text 渲染 |
|
||||
| 手动添加(图标库选择) | SVG 字符串 `<svg ...>...</svg>` | dangerouslySetInnerHTML 渲染 |
|
||||
| 节点没设图标 | `null` | 占位或不渲染 |
|
||||
|
||||
**最简兼容写法**:`if (icon?.startsWith('<svg')) {渲染 SVG} else if (icon) {lucide(icon) or 当 emoji} else {默认占位}`
|
||||
|
||||
### 展示方式变化
|
||||
|
||||
- admin 产品编辑第 5 步「费用包含」: 从 1 条多名字字符串 → **N 个独立 chip**,与小程序「景区门票/游玩项目」分组下的 chip 对齐
|
||||
- 小程序产品详情 includedFees: 每条 1 个 chip,无需前端拆分逗号
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 场景 | product_fee_item 表 |
|
||||
|------|---------------------|
|
||||
| 自动生成 + 保存 1 个含 4 景区 3 游玩项目的产品 | 7 行 INCLUDED + AUTO 记录(每节点 1 行),icon 各自从 emoji_icon 来 |
|
||||
| 不再产出「住宿」自动行 | 0 行 source=AUTO + fee_category=INCLUDED + fee_type=住宿 |
|
||||
| 同名跨天 | 1 行(去重) |
|
||||
|
||||
### ⚠️ 测试环境数据已清理(2026-05-18 10:58)
|
||||
|
||||
部署后跑了一次性 SQL,软删了 117 条 source=AUTO + fee_category=INCLUDED 的历史数据(涉及 32 个产品):
|
||||
|
||||
```sql
|
||||
UPDATE product_fee_item SET deleted_at = NOW()
|
||||
WHERE source = 'AUTO' AND fee_category = 'INCLUDED' AND deleted_at IS NULL;
|
||||
```
|
||||
|
||||
**这些产品的费用包含目前为空**,需要运营/定制师重新打开产品编辑页 → 第 5 步「补充信息 → 费用包含 → 根据行程自动生成」+ 保存,系统会按新格式产出 N 条独立条目。
|
||||
|
||||
**未受影响**:`excludedFees`(费用不含)/ `customFees`(自定义费用)/ 手动添加的 INCLUDED 项保留不动。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 产品无节点 / 无 SCENIC + ACTIVITY → 返回 `[]`
|
||||
- 节点 emoji_icon 为 null → icon 字段为 null(前端走「无图标占位」分支)
|
||||
- 节点 nodeType=HOTEL → 不进入返回(被过滤)
|
||||
- supplement.feeIgnoredTypes 含「门票」/「活动」/等 → 该类型整组被过滤
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**:
|
||||
- admin 产品编辑第 5 步「费用包含」展示与保存
|
||||
- 小程序产品详情 `includedFees` 渲染
|
||||
- **零影响**:
|
||||
- 费用不含 `excludedFees` / 自定义费用 `customFees`
|
||||
- 产品定价 / 班期 / 订单创建 / 算价 / 报价
|
||||
- 已有的住宿专门配置(`product_day_hotel`)— 它仍然是住宿数据的权威来源,只是不再走「费用包含-自动生成」这条路
|
||||
- 手动添加的费用项不动
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
```
|
||||
GET /admin/product/item/2045345825172639746/fee-deduction-preview → 200 + 8 条独立 VO ✓
|
||||
- 4 景区 + 4 游玩项目,无「住宿」
|
||||
- icon non-null 7/8(对应行程节点 emoji_icon 已设)
|
||||
- feeType breakdown 与小程序产品详情「景区门票/游玩项目」chip 精确对应
|
||||
|
||||
UPDATE product_fee_item ... → 117 条 / 32 个产品 软删,清理精准(excludedFees 7 条保留) ✓
|
||||
|
||||
GET /admin/product/item/2045345825172639746 → includedFees=[](清理生效) ✓
|
||||
```
|
||||
|
||||
验证产品: `productId=2045345825172639746`(测试服「游牧的森林-短途版」)
|
||||
|
||||
---
|
||||
|
||||
## 九、相关历史 PR
|
||||
|
||||
| PR | Issue | 说明 |
|
||||
|----|-------|------|
|
||||
| 早期 | (无) | 「自动生成」按钮的合并字符串实现 |
|
||||
| **#2484** | **#2477** | 取消合并 + 去住宿 + 带 icon,本次 |
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#2477](https://git.1814.love:8443/wx/HL/issues/2477)
|
||||
- 关联 PR: [wx/HL#2484](https://git.1814.love:8443/wx/HL/pulls/2484)
|
||||
正在加载...
x
在新工单中引用
屏蔽一个用户