# feat: 管理端产品列表 `/admin/product/item/list` 响应新增 `tierPrices` 档位价格摘要 - **日期**: 2026-04-20 - **PR**: [#955](https://git.1814.love:8443/wx/HL/pulls/955) (Closes #954) - **类型**: FEATURE(响应新增字段,无破坏性变更) - **服务**: hl-product-service-v2 - **前端是否需要改动**: **建议改动**(管理端产品列表新增"档位摘要"列展示) --- ## 一、为什么加这个字段 管理端产品管理列表此前每行只能看到: - `tierCount`(档位总数) - `startPrice`(产品起步价 = 所有档位最低) 但运营在列表里需要快速判断**每个档位是否已定价**、**哪个档位起步价多少**,否则要点进详情页一个个看,效率低。 本次新增 `tierPrices` 字段:**列表页保留无价档位**(startPrice=null),让前端可以一眼看出"档位待定价"的状态。 --- ## 二、变更接口清单 | # | 方法 | 路径 | 影响 | |---|------|------|------| | 1 | GET | `/admin/product/item/list` | 响应 `records[]` 每项新增 `tierPrices` 字段(数组) | **其他接口不变**: - `GET /admin/product/{id}`(产品详情):**行为零变化**。详情接口的 `tierPrices` **仍剔除无价档位**,与本次列表接口语义不同(见第五节)。 - 既有的 `tierCount` / `startPrice` 字段保留,不受影响。 --- ## 三、字段定义 ### 新增 `tierPrices`(数组) 每个产品记录新增 `tierPrices: TierPriceItem[]`,结构: | 字段 | 类型 | 说明 | 可能为 null | |------|------|------|-------------| | `tierSeq` | Integer | 档位序号(从 1 起) | 否 | | `tierName` | String | 档位名称(如"经济档"/"舒适档"/"豪华档") | 否 | | `tierDescription` | String | 档位描述(产品配置时填写) | 是(未填即 null) | | `startPrice` | BigDecimal | **该档位**未来日期的最低售价;**无价档位返回 null** | **是** ⚠️ | ### 既有字段(保留,不变) | 字段 | 说明 | |------|------| | `tierCount` | 档位总数(包含无价档位) | | `startPrice` | 产品起步价(所有档位中最低;CORE/CUSTOM 取自 `product_price_calendar`,GROUP 同详情口径) | --- ## 四、响应示例对比 ### 前 ```json { "code": 200, "data": { "total": 25, "records": [ { "id": 1001, "name": "小蒙马·呼伦贝尔5日", "productType": "GROUP", "tierCount": 3, "startPrice": 4580.00 } ] } } ``` ### 后 ```json { "code": 200, "data": { "total": 25, "records": [ { "id": 1001, "name": "小蒙马·呼伦贝尔5日", "productType": "GROUP", "tierCount": 3, "startPrice": 4580.00, "tierPrices": [ { "tierSeq": 1, "tierName": "经济档", "tierDescription": "标准住宿+常规交通", "startPrice": 4580.00 }, { "tierSeq": 2, "tierName": "舒适档", "tierDescription": "四星住宿+商务车", "startPrice": 5160.00 }, { "tierSeq": 3, "tierName": "豪华档", "tierDescription": "五星住宿+豪华车", "startPrice": null } ] } ] } } ``` > 第 3 个档位 "豪华档" 未配价格 → `startPrice: null`,前端可展示为"档位待定价"。 --- ## 五、列表 vs 详情语义差异(重要) | 接口 | 无价档位(startPrice=null)处理 | |---|---| | **列表 `/admin/product/item/list`**(本次) | **保留**,`startPrice=null`,便于运营在列表里识别 | | **详情 `/admin/product/{id}`**(不变) | **剔除**,`tierPrices` 中只保留有价档位 | > 这个差异是设计意图:详情页面向"已成型可售产品",列表页面向"运营治理"。 --- ## 六、GROUP 类型的特别说明 | 产品类型 | 列表 `tierPrices[].startPrice` 数据源 | |---|---| | `CORE` / `CUSTOM` | `product_price_calendar` 表(按档位 + 日期取未来最低) | | `GROUP`(小蒙马) | 仍走 `product_price_calendar`,**与详情接口口径一致** | ⚠️ **已知偏差**:GROUP 真实成交价存在 `group_tour_batch.adult_price`(班期表),与日历价可能存在差异。这是已知问题,本次不修复,保持与详情接口同口径以避免列表/详情数字不一致。 --- ## 七、前端使用建议 ### 推荐:列表新增"档位摘要"列 ```vue ``` ### ⚠️ 容错要求 - `tierPrices[].startPrice` **可能为 null**(无价档位),必须做 null 判断后再格式化金额,避免 `undefined.toFixed()` 报错 - `tierDescription` 可能为 null,渲染时做空值兜底 - `tierPrices` 数组本身不会为 null(无档位时为空数组 `[]`) --- ## 八、不兼容变更 **无**。纯新增字段,前端不读不受影响;既有 `tierCount` / `startPrice` 字段保留,含义与之前完全一致。 --- ## 九、回归验证 测试环境部署完成后,用管理端 token 调用: ```bash # 1. 列表接口检查 tierPrices 字段存在 curl "https://api.test.1814.love/admin/product/item/list?pageNo=1&pageSize=10" \ -H "Authorization: Bearer {admin-token}" \ | jq '.data.records[0] | {id, name, tierCount, startPrice, tierPrices}' # 预期:tierPrices 字段存在;数组长度 == tierCount;含无价档位时 startPrice=null # 2. 详情接口对比(应仍剔除无价档位) curl "https://api.test.1814.love/admin/product/{id}" \ -H "Authorization: Bearer {admin-token}" \ | jq '.data.tierPrices' # 预期:详情接口的 tierPrices 长度 ≤ 列表接口(剔除了无价档位) # 3. GROUP 产品(小蒙马)口径核对 curl "https://api.test.1814.love/admin/product/item/list?productType=GROUP&pageNo=1&pageSize=5" \ -H "Authorization: Bearer {admin-token}" \ | jq '.data.records[] | {id, productType, tierPrices}' # 预期:GROUP 产品的 tierPrices[].startPrice 与详情接口一致(同走 product_price_calendar) ``` **预期**: - 列表新字段 `tierPrices` 必出现 - 无价档位 `startPrice: null` 而非被剔除 - 与详情接口语义差异符合第五节说明