hl-api-changelog/changelogs/2026-04/30_fix_product_fee-deduction-dedupe-and-name.md

4.8 KiB

修复: 费用包含按名字去重 + name 字段填具体景区/活动/酒店名

类型: 后端 BUG 修复(PR #1568 follow-up) 关联: 工单 #1576 / PR #1577 日期: 2026-04-30 前端处理者: mmg 影响范围: 管理后台「产品编辑 → 补充信息 → 费用说明 →『根据行程自动生成』」按钮


背景

PR #1568 已修复「自动生成」漏景区/活动/酒店的问题(每节点 1 条 + 追加酒店),但用户使用后反馈 2 个新问题:

问题 1: 同名条目重复

同一酒店多天住就出现多次 — 产品 2045018534152478721「游牧的森林·南线经典」自动生成 35 条结果中:

  • 「海拉尔洲季酒店」出现 11 次(11 天住同一酒店)
  • 「呼伦贝尔香格里拉大酒店」出现 4 次
  • 「额尔古纳玖成万豪水汇智能酒店」出现 4 次
  • 27 条住宿真实只有 9 家不同酒店

问题 2: name 字段无信息量

三种类型(景区/活动/酒店)的 name 字段都填 feeType 标签 — 即:

  • 6 条门票项 name 都是 "门票"(实际景区名是黑山头/白桦林景区/莫日格勒河等)
  • 27 条住宿项 name 都是 "住宿"(实际酒店名各不同)

前端中间 label 区域显示重复的 chip 文字,无信息量。

修复

hl-product-service-v2 ProductValidationService.feeDeductionPreview():

1. 按名字去重

LinkedHashSet<String> seenNodeNames = new LinkedHashSet<>();
for (ProductItineraryNodeDO node : sortedNodes) {
    String nodeName = node.getNodeName();
    if (nodeName == null || nodeName.isBlank()) continue;
    if (!seenNodeNames.add(nodeName)) continue;        // ← 按名字去重
    result.add(...);
}

LinkedHashSet<String> seenHotelNames = new LinkedHashSet<>();
for (ProductDayHotelDO hotel : sortedHotels) {
    String hotelName = hotel.getHotelName();
    if (hotelName == null || hotelName.isBlank()) continue;
    if (!seenHotelNames.add(hotelName)) continue;
    result.add(...);
}

LinkedHashSet 保留首次出现(行程顺序里第一次见到该名字的那条)。

2. name 字段填具体名

字段 改前 改后
feeType 门票/活动/住宿/餐饮/交通 不变(前端 chip 用)
name 同 feeType 节点名/酒店名
sourceNodeName 节点名/酒店名 不变(与 name 同值)
icon null 不变(PR #1573 行为)

影响接口

方法 路径 改动
GET /admin/product/item/{id}/fee-deduction-preview 数组长度变短(去重),每项 name 改为具体名,其他字段语义不变

请求签名完全不变,响应字段不变。

响应示例

GET /admin/product/item/2045018534152478721/fee-deduction-preview
{
  "code": 200,
  "data": [
    /* 6 门票 */
    {"feeType": "门票", "name": "黑山头", "source": "AUTO", "sourceNodeName": "黑山头", "icon": null},
    {"feeType": "门票", "name": "白桦林景区", "source": "AUTO", "sourceNodeName": "白桦林景区", "icon": null},
    /* ...  6  */
    /* 2 活动 */
    {"feeType": "活动", "name": "ATV全地形越野车穿越", "source": "AUTO", "sourceNodeName": "ATV全地形越野车穿越", "icon": null},
    {"feeType": "活动", "name": "驯鹿拉车体验", "source": "AUTO", "sourceNodeName": "驯鹿拉车体验", "icon": null},
    /* 9 住宿(去重后,不再有重复) */
    {"feeType": "住宿", "name": "海拉尔洲季酒店", "source": "AUTO", "sourceNodeName": "海拉尔洲季酒店", "icon": null},
    /* ...  9  */
  ]
}

旧返回 35 条,新返回 17 条(6 门票 + 2 活动 + 9 住宿)。

前端要做的改动

0 改动。前端模板 {{ item.name }} 直接渲染就显示具体名字了。

sourceNodeName 字段值与 name 同值,如果前端模板里有 ← {{ item.sourceNodeName }} 来源标签,会出现"门票 chip + 黑山头 + ← 黑山头"的视觉重复。前端可选:

  • 保留来源标签作为视觉标识(用户已习惯)
  • 或加 v-if="item.sourceNodeName !== item.name" 隐藏冗余

测试覆盖

mvn test -pl hl-product-service-v2: 1045/1045 全绿

新增 3 单测:

  • feeDeductionPreview_duplicateNodeNames_dedupedByName — 5 个同名 SCENIC 节点 → result.size=1
  • feeDeductionPreview_sameHotelMultipleDays_dedupedByName — 11 个同酒店多天 → result.size=1
  • feeDeductionPreview_nullOrBlankName_skipped — null/空名字跳过

测试服 round-trip(产品 2045018534152478721)实测:

  • 总计 17 条(6 + 2 + 9)
  • 全部 name unique(去重生效)
  • 全部 name 是具体名字(黑山头/ATV 全地形越野车穿越/海拉尔洲季酒店 等)
  • 全部 icon null(PR #1573 行为不变)

部署状态

  • Deploy Panel 已部署测试服 hl-product-service-v2 双实例(2026-04-30 16:56)
  • 正式环境暂未发布(等 dev → main release PR)