6.6 KiB
6.6 KiB
feat: 管理端产品列表 /admin/product/item/list 响应新增 tierPrices 档位价格摘要
- 日期: 2026-04-20
- PR: #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 同详情口径) |
四、响应示例对比
前
{
"code": 200,
"data": {
"total": 25,
"records": [
{
"id": 1001,
"name": "小蒙马·呼伦贝尔5日",
"productType": "GROUP",
"tierCount": 3,
"startPrice": 4580.00
}
]
}
}
后
{
"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(班期表),与日历价可能存在差异。这是已知问题,本次不修复,保持与详情接口同口径以避免列表/详情数字不一致。
七、前端使用建议
推荐:列表新增"档位摘要"列
<el-table-column label="档位摘要" min-width="240">
<template #default="{ row }">
<div v-for="t in row.tierPrices" :key="t.tierSeq" class="tier-line">
<span class="tier-name">{{ t.tierName }}</span>
<span v-if="t.startPrice != null" class="tier-price">
起 ¥{{ t.startPrice }}
</span>
<el-tag v-else type="warning" size="small">待定价</el-tag>
</div>
</template>
</el-table-column>
⚠️ 容错要求
tierPrices[].startPrice可能为 null(无价档位),必须做 null 判断后再格式化金额,避免undefined.toFixed()报错tierDescription可能为 null,渲染时做空值兜底tierPrices数组本身不会为 null(无档位时为空数组[])
八、不兼容变更
无。纯新增字段,前端不读不受影响;既有 tierCount / startPrice 字段保留,含义与之前完全一致。
九、回归验证
测试环境部署完成后,用管理端 token 调用:
# 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而非被剔除 - 与详情接口语义差异符合第五节说明