diff --git a/changelogs/2026-04/30_fix_product_fee-deduction-merge-by-feetype.md b/changelogs/2026-04/30_fix_product_fee-deduction-merge-by-feetype.md new file mode 100644 index 0000000..281ca44 --- /dev/null +++ b/changelogs/2026-04/30_fix_product_fee-deduction-merge-by-feetype.md @@ -0,0 +1,124 @@ +# 修复: 费用包含按 feeType 合并为单条 name 拼接 + +**类型**: 后端 BUG 修复(PR #1577 follow-up) +**关联**: 工单 #1578 / PR #1579 +**日期**: 2026-04-30 +**前端处理者**: yst / mmg +**影响范围**: 管理后台「产品编辑 → 补充信息 → 费用说明 →『根据行程自动生成』」按钮 + +--- + +## 演进 + +| PR | 行为 | 产品 2045018534152478721 返回数 | +|---|---|---| +| #1568 | 修复漏景区/活动/酒店,每节点 1 条+追加酒店 | 35 条 | +| #1577 | 按名字去重,name 改具体名 | 17 条 | +| **#1579 本次** | **按 feeType 合并为单条,name 拼接所有具体名** | **3 条** | + +用户期望:每种 feeType 一条记录,中间 label 显示完整字符串。 + +## 用户原文 + +> 「门票:黑山头,莫日格勒河 门票是一条数据」 +> 「其他也是」(指活动/住宿/餐饮/交通也都按 feeType 合并) + +## 设计 + +``` +name = "{feeType}:{name1,name2,...}" (中文「:」前缀 + 中文「,」分隔) +``` + +| 字段 | 值 | +|---|---| +| `feeType` | 门票 / 活动 / 住宿 / 餐饮 / 交通(chip 用) | +| `name` | `"门票:黑山头,白桦林景区,莫日格勒河,..."` 完整拼接串 | +| `sourceNodeName` | **null**(避免重复) | +| `icon` | **null**(PR #1573 行为不变) | + +## 影响接口 + +| 方法 | 路径 | 改动 | +|------|------|------| +| GET | `/admin/product/item/{id}/fee-deduction-preview` | 数组**长度从 N 条压缩到 ≤ 5 条**(每 feeType 1 条),`name` 字段值变为完整拼接串,`sourceNodeName` 设为 null | + +请求签名完全不变,响应字段不变。 + +## 响应示例 + +```jsonc +GET /admin/product/item/2045018534152478721/fee-deduction-preview +{ + "code": 200, + "data": [ + { + "feeType": "门票", + "name": "门票:黑山头,白桦林景区,莫日格勒河,莫日格勒河断崖,呼伦湖,阿尔山火车站", + "source": "AUTO", + "sourceNodeName": null, + "icon": null + }, + { + "feeType": "活动", + "name": "活动:ATV全地形越野车穿越,驯鹿拉车体验", + "source": "AUTO", + "sourceNodeName": null, + "icon": null + }, + { + "feeType": "住宿", + "name": "住宿:海拉尔洲季酒店,呼伦贝尔香格里拉大酒店,额尔古纳玖成万豪水汇智能酒店,额尔古纳豪星酒店,额尔古纳白桦大酒店,海拉尔海棠酒店,满洲里饭店(百年俄式),敖根栖息地,阿尔善国际维景度假温泉酒店", + "source": "AUTO", + "sourceNodeName": null, + "icon": null + } + ] +} +``` + +## 实现要点 + +```java +// 1. LinkedHashMap> 双保序去重收集 +Map> feeTypeToNames = new LinkedHashMap<>(); +// (扫节点 + 扫酒店,按 feeType 分桶) + +// 2. feeIgnoredTypes 在 map 阶段移除 entry(效率优于 result.removeIf) +feeTypeToNames.keySet().removeAll(ignored); + +// 3. 排序(门票/活动/住宿/餐饮/交通)+ 拼接 + 输出 +return feeTypeToNames.entrySet().stream() + .filter(e -> !e.getValue().isEmpty()) + .sorted(byFeeTypeOrder) + .map(e -> FeeDeductionPreviewVO.builder() + .feeType(e.getKey()) + .name(e.getKey() + ":" + String.join(",", e.getValue())) + .source(SOURCE_AUTO) + .sourceNodeName(null) + .build()) + .toList(); +``` + +## 前端要做的改动 + +**0 改动**。前端模板 `{{ item.name }}` 直接渲染就显示 `"门票:黑山头,白桦林景区,..."` 完整字符串。 + +`sourceNodeName=null` 后,前端 `v-if="item.sourceNodeName"` 来源标签自动不渲染,UI 自然更清爽。 + +## 测试覆盖 + +`mvn test -pl hl-product-service-v2`: **1046/1046 全绿**。 + +10 现有断言更新(从「按 name 多条」改为「按 feeType 一条」)+ 1 新增 `feeDeductionPreview_mixedTypes_groupedByFeeType` 混合类型分组测试。 + +测试服 round-trip(产品 `2045018534152478721`)实测: +- 总计 **3 条**(门票/活动/住宿 各一) +- 每条 `name` 是 `"{feeType}:{name1,name2,...}"` 完整串 +- `sourceNodeName` 全 null +- `feeType` 排序正确(门票 → 活动 → 住宿) +- `icon` 全 null + +## 部署状态 + +- ✅ Deploy Panel 已部署测试服 hl-product-service-v2 双实例(2026-04-30 17:11) +- 正式环境暂未发布(等 dev → main release PR)