From 85f27e93fdf40953b30075f6b17f5dcf34940fa3 Mon Sep 17 00:00:00 2001 From: wx Date: Thu, 30 Apr 2026 16:38:17 +0800 Subject: [PATCH] =?UTF-8?q?2026-04-30:=20=E4=BA=A7=E5=93=81=E8=B4=B9?= =?UTF-8?q?=E7=94=A8=E8=AF=B4=E6=98=8E=E5=8F=8C=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(product): 补充信息「费用包含」自动生成漏景区/活动/酒店 (PR #1568) - feat(product): 费用包含/费用不含项支持图标(SVG)字段 (PR #1573) --- ...30_feat_product_supplement-fee-icon-svg.md | 144 ++++++++++++++++++ ...fee-deduction-preview-include-all-nodes.md | 97 ++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 changelogs/2026-04/30_feat_product_supplement-fee-icon-svg.md create mode 100644 changelogs/2026-04/30_fix_product_fee-deduction-preview-include-all-nodes.md diff --git a/changelogs/2026-04/30_feat_product_supplement-fee-icon-svg.md b/changelogs/2026-04/30_feat_product_supplement-fee-icon-svg.md new file mode 100644 index 0000000..0f1ec93 --- /dev/null +++ b/changelogs/2026-04/30_feat_product_supplement-fee-icon-svg.md @@ -0,0 +1,144 @@ +# 新增: 产品「补充信息」费用包含/费用不含项支持图标(SVG)字段 + +**类型**: 后端字段扩展 +**关联**: 工单 #1572 / PR #1573 +**日期**: 2026-04-30 +**前端处理者**: yst / mmg +**影响范围**: 管理后台「产品编辑 → 补充信息 → 费用说明 →『费用包含』/『费用不含』」每一项的图标 + +--- + +## 背景 + +产品编辑页 → 补充信息 → 费用说明 →「费用包含」/「费用不含」每一项前面有「选择」按钮(IconPicker),用户选完图标后**保存草稿/重新打开页面图标丢失**。 + +**根因**:后端 `product_fee_item` 表 / VO / DO 没有 icon 字段,前端选择无处落库。 + +## 字段设计 + +- DB 列类型 **TEXT**(可装完整 `` 字符串,也兼容短 code) +- 字段命名 `icon` +- 注释:`图标SVG字符串(前端图标库选中后存完整...内容,可空)` +- 自动生成接口 `feeDeductionPreview` 返回项 `icon` **恒为 null**,由前端选完 SVG 后保存(后端**不硬编码**默认 lucide code) + +## 改动 + +`hl-product-service-v2`: + +| 文件 | 改动 | +|------|------| +| `entity/ProductFeeItemDO.java` | +`String icon` 字段 | +| `vo/admin/ProductSupplementSaveReqVO.FeeItem` | +`String icon` 字段 | +| `vo/admin/ProductDetailRespVO.FeeItemVO` | +`String icon` 字段 | +| `vo/admin/FeeDeductionPreviewVO` | +`String icon` 字段(默认 null) | +| `service/admin/ProductValidationService.feeDeductionPreview()` | icon 不再填默认值 | +| `src/main/resources/schema.sql` `product_fee_item` 表 | +`icon TEXT NULL` 列 | +| `sql/V20260430__product_fee_item_add_icon.sql` | 增量 migration | + +## 影响接口 + +| 方法 | 路径 | 改动 | +|------|------|------| +| PUT | `/admin/product/item/{id}/supplement` | `includedFees[*]` / `excludedFees[*]` / `customFees[*]` **新增可选字段 `icon` (String)** | +| GET | `/admin/product/item/{id}` | `includedFees[*]` / `excludedFees[*]` / `customFees[*]` **新增字段 `icon` (String,可能为 null)** | +| GET | `/admin/product/item/{id}/fee-deduction-preview` | 响应项 **新增字段 `icon` (String,自动推导时恒为 null)** | + +请求字段全部**可选** (前端不传 `icon` 表示不变更),响应字段**总是出现**(可能为 null)。 + +## 请求 / 响应示例 + +### 请求(PUT supplement) + +```jsonc +PUT /admin/product/item/{id}/supplement +{ + "includedFees": [ + { + "feeType": "门票", + "name": "门票", + "icon": "", + "sortOrder": 1 + }, + { + "feeType": "住宿", + "name": "住宿", + "icon": "", + "sortOrder": 2 + } + ], + "excludedFees": [ + {"feeType": "个人消费", "name": "个人消费", "icon": "", "sortOrder": 1} + ] +} +``` + +### 响应(GET detail) + +```jsonc +{ + "code": 200, + "data": { + "includedFees": [ + {"feeType": "门票", "name": "门票", "icon": "", "sortOrder": 1}, + ... + ], + "excludedFees": [...] + } +} +``` + +## 前端要做的改动 + +### 1. 新增字段类型扩展 + +`includedFees` / `excludedFees` / `customFees` 数组的 item 类型增加可选 `icon: string | null`。 + +### 2. IconPicker 组件 v-model 双向绑定 `item.icon` + +`hl-ui/src/views/product/edit/components/supplement/CostDescPanel.vue` 已经使用 `store.productData.includedFees / excludedFees`,补 IconPicker 时: + +```vue + +``` + +IconPicker 选中图标后产出 SVG 字符串(`...`),写入 `item.icon`,保存接口透传。 + +### 3. 渲染时显示图标 + +```vue + +``` + +`v-html` 直接渲染 SVG 字符串(因 SVG 内容由后端管理员存储,不来自终端用户,XSS 风险可控)。 + +### 4. 图标库选完即保存 + +IconPicker 内已有图标集合(`flame / ticket / hotel / bed / utensils / coffee / plane / car / bus / train-front / ship / bike / shield-check / star / crown / gem / heart / bookmark / tag / gift / percent / hourglass / alarm-clock / bell / lock / zap / sparkles / rocket` 等共约 150 个),选中后用 lucide-vue-next 渲染对应组件 → 拿 outerHTML 序列化为 SVG 字符串 → 写入 `item.icon`。 + +### 5. 自动生成的项 icon 是 null + +「根据行程自动生成」按钮的返回项 `icon` **总是 null**,前端补 UI 时: +- card 默认显示「选择」占位按钮(已在原型里) +- 用户点击「选择」展开 IconPicker → 选中后写入 `item.icon` → 用户点保存按钮触发 PUT supplement 落库 + +## 测试覆盖 + +`mvn test -pl hl-product-service-v2`:1026/1026 全绿。 +新增 4 条 `@ExtendWith(MockitoExtension.class)` 单测覆盖:保存 SVG 透传 / 保存 null / 自动生成 icon 全 null / 兼容旧数据。 + +测试服 round-trip 验证(产品 `2043722892864000001` DRAFT): +- PUT supplement 携带 SVG icon → 200 OK +- GET detail 返回 icon 与发送字符串**完全一致**(含 viewBox / xmlns / path 全部保留) + +## 兼容性 + +- DB 列允许 NULL,旧数据 `icon=NULL` 读取正常 +- VARCHAR(64)→TEXT 是放大方向,MySQL 自动兼容 +- TEXT 列可存任意长度字符串,前端发短 code(如 `"ticket"`)也能正常存取 +- MyBatis-Plus `updateById` 默认 `FieldStrategy.NOT_NULL`,前端不传 `icon` 不会清空已存值 + +## 部署状态 + +- ✅ 测试服 DB `product_fee_item` 已 ALTER ADD COLUMN icon TEXT NULL(2026-04-30 16:33) +- ✅ Deploy Panel 已部署测试服 hl-product-service-v2(2026-04-30 16:35) +- 正式环境暂未发布(等 dev → main release PR + 同步 DDL) diff --git a/changelogs/2026-04/30_fix_product_fee-deduction-preview-include-all-nodes.md b/changelogs/2026-04/30_fix_product_fee-deduction-preview-include-all-nodes.md new file mode 100644 index 0000000..2918d2a --- /dev/null +++ b/changelogs/2026-04/30_fix_product_fee-deduction-preview-include-all-nodes.md @@ -0,0 +1,97 @@ +# 修复: 产品「补充信息」费用包含「根据行程自动生成」漏景区/活动/酒店 + +**类型**: 后端 BUG 修复 +**关联**: 工单 #1567 / PR #1568 +**日期**: 2026-04-30 +**前端处理者**: yst / mmg +**影响范围**: 管理后台「产品编辑 → 补充信息 → 费用说明 →『根据行程自动生成』」按钮 + +--- + +## 现象 + +点击「根据行程自动生成」后,前端展示的「费用包含」列表项严重不全: + +- 同类型节点只展示**第一个**:产品有 6 个景区只回 1 条「门票」 +- 酒店**完全不出现** + +测试样本:产品 id=`2045018534152478721`「游牧的森林·南线经典」(用户原图) +- 旧逻辑只返回 2 条:活动「← ATV 全地形越野穿越」 + 门票「← 白桦林景区」 +- 修复后返回 35 条:6 门票 + 2 活动 + 27 住宿(全部景区 / 全部活动 / 全部酒店) + +## 根因 + +`hl-product-service-v2` `ProductValidationService.feeDeductionPreview()` 旧实现: + +1. `Set deducted` 按 `feeLabel`(门票/活动/住宿/餐饮/交通)去重 → 同 feeType 只保留**第一条** +2. 完全没查 `product_day_hotel` 表(酒店数据已早期迁离 `product_itinerary_node`,但 fee-deduction-preview 没同步更新) + +## 修复 + +`feeDeductionPreview()` 重写为: + +- **不再按 feeLabel 去重**,每个 `ProductItineraryNodeDO` 都生成 1 条 `FeeDeductionPreviewVO`(前提 `getFeeLabel() ≠ null`) +- **追加查 `dayHotelService.selectByProductId(productId)`**,全部酒店并入结果,`feeType="住宿"`,`sourceNodeName=酒店名` +- 节点按 `dayId + sortOrder` 排序,酒店按 `dayNumber + sortOrder` 排序 +- 整体按 `门票 → 活动 → 住宿 → 餐饮 → 交通` 分组排序 +- NPE 兜底:`allNodes / allHotels` null → 空集合;`hotelName / dayId / sortOrder` null → 排序用 `MAX_VALUE` 兜底 + +## 影响接口 + +| 方法 | 路径 | 改动 | +|------|------|------| +| GET | `/admin/product/item/{id}/fee-deduction-preview` | 返回项数从去重后的 N 条 → 全节点 + 全酒店,字段不变 | + +请求签名完全不变,响应字段不变,只是 `data` 数组**变长且含酒店**。 + +## 响应示例 + +```jsonc +GET /admin/product/item/2045018534152478721/fee-deduction-preview +{ + "code": 200, + "data": [ + { "feeType": "门票", "name": "门票", "source": "AUTO", "sourceNodeName": "黑山头", "icon": null }, + { "feeType": "门票", "name": "门票", "source": "AUTO", "sourceNodeName": "白桦林景区", "icon": null }, + { "feeType": "门票", "name": "门票", "source": "AUTO", "sourceNodeName": "莫日格勒河", "icon": null }, + /* ... 共 6 条门票 */ + { "feeType": "活动", "name": "活动", "source": "AUTO", "sourceNodeName": "ATV 全地形越野车穿越", "icon": null }, + { "feeType": "活动", "name": "活动", "source": "AUTO", "sourceNodeName": "驯鹿拉车体验", "icon": null }, + { "feeType": "住宿", "name": "住宿", "source": "AUTO", "sourceNodeName": "海拉尔洲季酒店", "icon": null }, + /* ... 共 27 条住宿 */ + ] +} +``` + +## 前端要做的改动 + +### 已是 `v-for` 列表渲染 — 0 改动 + +`hl-ui/src/views/product/edit/components/supplement/CostDescPanel.vue` 第 109-119 行已经是 +`v-for="(item, idx) in includedCosts"` 网格布局,每张 card 显示 `feeType + name + ← sourceNodeName`,**新方案完美适配**: +- 6 个景区会渲染 6 张「门票」card,每张带不同景区名(如「门票 ← 黑山头」「门票 ← 白桦林景区」…) +- 27 个酒店会渲染 27 张「住宿」card,每张带不同酒店名 + +UI 上多张同 feeType card 紧邻成一组排开,符合用户期望「看到所有节点」。 + +### 如果想做「按 feeType 聚合显示」(可选) + +如果前端反馈「列表太长,希望同 feeType 合并为一张 card,内部分行列出来源」,可以前端 `Object.groupBy(includedCosts, c => c.feeType)` 自行聚合,无需后端改动。 + +## 测试覆盖 + +`mvn test -pl hl-product-service-v2`:1026/1026 全绿。 +新增/增强 6 条 `@ExtendWith(MockitoExtension.class)` 单测覆盖:多景区 / 多活动 / 含酒店 / 空产品 / 未知类型(FREE/NOTE/SERVICE/CUSTOM)/ 忽略类型。 + +测试服 round-trip 验证(产品 `2045018534152478721`):返回 35 条,包含 6 景区 + 2 活动 + 27 酒店,排序正确,**用户原图问题修复**。 + +## 不动 + +- 数据库表结构不变(只是查询逻辑扩展) +- VO 字段不变(只是 `data` 数组长度) +- `feeIgnoredTypes` 过滤逻辑保留,被忽略的 feeType 仍会从结果中剔除 + +## 部署状态 + +- ✅ Deploy Panel 已部署测试服 hl-product-service-v2(2026-04-30 16:35) +- 正式环境暂未发布(等 dev → main release PR)