hl-api-changelog/changelogs/2026-04/30_feat_product_supplement-fee-icon-svg.md

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)