# 新增: 产品「补充信息」费用包含/费用不含项支持图标(SVG)字段 **类型**: 后端字段扩展 **关联**: 工单 #1572 / PR #1573 **日期**: 2026-04-30 **前端处理者**: 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)