5.6 KiB
新增: 产品「补充信息」费用包含/费用不含项支持图标(SVG)字段
类型: 后端字段扩展 关联: 工单 #1572 / PR #1573 日期: 2026-04-30 前端处理者: mmg 影响范围: 管理后台「产品编辑 → 补充信息 → 费用说明 →『费用包含』/『费用不含』」每一项的图标
背景
产品编辑页 → 补充信息 → 费用说明 →「费用包含」/「费用不含」每一项前面有「选择」按钮(IconPicker),用户选完图标后保存草稿/重新打开页面图标丢失。
根因:后端 product_fee_item 表 / VO / DO 没有 icon 字段,前端选择无处落库。
字段设计
- DB 列类型 TEXT(可装完整
<svg viewBox="..."><path d="..."/></svg>字符串,也兼容短 code) - 字段命名
icon - 注释:
图标SVG字符串(前端图标库选中后存完整<svg>...</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)
PUT /admin/product/item/{id}/supplement
{
"includedFees": [
{
"feeType": "门票",
"name": "门票",
"icon": "<svg viewBox=\"0 0 24 24\"><path d=\"M5 5h14v14H5z\"/></svg>",
"sortOrder": 1
},
{
"feeType": "住宿",
"name": "住宿",
"icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M3 7h18\"/><circle cx=\"7\" cy=\"12\" r=\"2\"/></svg>",
"sortOrder": 2
}
],
"excludedFees": [
{"feeType": "个人消费", "name": "个人消费", "icon": "<svg viewBox=\"0 0 24 24\"><path d=\"M12 8v8\"/></svg>", "sortOrder": 1}
]
}
响应(GET detail)
{
"code": 200,
"data": {
"includedFees": [
{"feeType": "门票", "name": "门票", "icon": "<svg viewBox=\"0 0 24 24\"><path d=\"M5 5h14v14H5z\"/></svg>", "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 时:
<IconPicker v-model="item.icon" />
IconPicker 选中图标后产出 SVG 字符串(<svg>...</svg>),写入 item.icon,保存接口透传。
3. 渲染时显示图标
<span v-if="item.icon" class="included-card__icon" v-html="item.icon"></span>
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)