13 KiB
13 KiB
小程序端 - 新增小蒙马产品全部列表接口
- 日期: 2026-04-18
- PR: #847 (Closes #843)
- 类型: FEATURE
- 状态: 已合并到 dev + 测试环境部署中
- 服务: hl-mp-service + hl-product-service-v2
一、功能说明
业务场景
小程序「小蒙马专题页」需要一次性铺满展示全部已上架的小蒙马(GROUP)产品:
- 不分页(瀑布流/卡片墙一屏看全)
- 每个产品卡片要直接显示最近的可售团期(出发日期、起步价、剩余名额)
- 支持关键词模糊搜索(产品名 / 副标题 / 标签)
设计要点
| 维度 | 设计 | 说明 |
|---|---|---|
| 是否分页 | 否 | hard cap 100 条(业务上小蒙马产品总数远 < 100,专题页一次性渲染) |
| 排序 | sort_order ASC, product_id DESC |
运营通过 sort_order 控制置顶;同序号按新产品在前 |
| 缓存 | 服务端 600s TTL | @MpCache(prefix="product:group:list:", ttl=600),发布/下架后 10 分钟内生效 |
| batches 注入 | 与 /mp/product/list 完全一致 |
复用 injectBatchesForGroupItems 私有方法,并行 Feign 调用,单个失败不影响整体 |
| productId 类型 | String | @JsonSerialize(ToStringSerializer) 防 JS 雪花 ID 精度丢失 |
| 详情接口 | 零改动 | 直接复用 GET /mp/product/{productId}(详见下方"重要提示") |
| N+1 防护 | 4 步批量查询 | basic 列表 + paymentType 批量 + 起步价日历批量 + lineName 批量 |
| 权限 | OPTIONAL | 无需登录即可访问 |
二、变更接口清单
| # | 方法 | 路径 | 说明 | 鉴权 | 缓存 |
|---|---|---|---|---|---|
| 1 | GET | /mp/product/group/list |
小蒙马产品全部列表(不分页 + batches) | OPTIONAL | 600s |
三、接口详细定义
1. 小蒙马产品全部列表
GET /mp/product/group/list
鉴权
- OPTIONAL:登录与未登录均可访问,逻辑无差异
- 已加入网关白名单,可直接调用
请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
keyword |
query | String | 否 | 模糊搜索关键词(匹配产品名/副标题/标签) |
请求示例
GET /mp/product/group/list
GET /mp/product/group/list?keyword=亲子
完整响应示例
{
"code": 200,
"message": "success",
"data": [
{
"productId": "120000000000001",
"productType": "GROUP",
"name": "草原星空亲子3日游",
"subtitle": "8人精品小团·1对1管家",
"coverImageUrl": "https://oss.example.com/cover.jpg",
"tripDays": 3,
"tripNights": 2,
"lineName": "亲子草原",
"tags": ["亲子", "星空"],
"seasons": ["summer", "autumn"],
"startPrice": 3980.00,
"startPriceLabel": "¥3980起/人",
"paymentType": "DEPOSIT",
"sortOrder": 10,
"batches": [
{
"batchId": "9001000000000001",
"departureDate": "2026-05-01",
"batchLabel": "五一团",
"batchName": "草原星空亲子3日游 5月1日团",
"batchStatus": "ENROLLING",
"startingPrice": 3980.00,
"enrolledCount": 6,
"remainingSlots": 2,
"isConfirmed": false
},
{
"batchId": "9001000000000002",
"departureDate": "2026-05-15",
"batchLabel": "5月中旬团",
"batchName": "草原星空亲子3日游 5月15日团",
"batchStatus": "CONFIRMED",
"startingPrice": 3980.00,
"enrolledCount": 8,
"remainingSlots": 0,
"isConfirmed": true
}
]
},
{
"productId": "120000000000002",
"productType": "GROUP",
"name": "呼伦贝尔大环线4日游",
"subtitle": "深度纯玩·全程5星住宿",
"coverImageUrl": "https://oss.example.com/cover2.jpg",
"tripDays": 4,
"tripNights": 3,
"lineName": "呼伦贝尔环线",
"tags": ["纯玩", "5星"],
"seasons": ["summer"],
"startPrice": 5680.00,
"startPriceLabel": "¥5680起/人",
"paymentType": "FULL",
"sortOrder": 20,
"batches": []
}
]
}
空数据:当不存在已上架 GROUP 产品时,
data返回[](不是 null)。 batches 为空:某产品当前无可售团期时,该产品的batches返回[]。
响应字段说明
| 字段 | 类型 | 必有 | 说明 |
|---|---|---|---|
data[].productId |
String | 是 | 产品ID(字符串,由后端 ToStringSerializer 序列化,前端禁止用 Number 接收) |
data[].productType |
String | 是 | 产品类型,固定值 GROUP(小蒙马) |
data[].name |
String | 是 | 产品名称 |
data[].subtitle |
String | 否 | 副标题/卖点(一行短文案) |
data[].coverImageUrl |
String | 否 | 封面图 URL(OSS CDN 地址) |
data[].tripDays |
Integer | 是 | 行程天数 |
data[].tripNights |
Integer | 是 | 行程晚数 |
data[].lineName |
String | 否 | 产品线(主题)名称 |
data[].tags |
List<String> | 否 | 标签数组(如 ["亲子","星空"]) |
data[].seasons |
List<String> | 否 | 适合季节数组,字典 product_season |
data[].startPrice |
BigDecimal | 否 | 起步价(元,未来日期最低成人售价) |
data[].startPriceLabel |
String | 否 | 起步价文案(用于卡片标价,如 ¥3980起/人) |
data[].paymentType |
String | 是 | 支付方式,字典 payment_type |
data[].sortOrder |
Integer | 否 | 运营排序号(值越小越靠前) |
data[].batches |
List<Object> | 是 | 可售团期列表(即使为空也返回 []),结构见下表 |
batches[] 字段说明(与 /mp/product/list 完全一致)
| 字段 | 类型 | 说明 |
|---|---|---|
batches[].batchId |
String/Long | 团期 ID(与 v1 团期接口一致,建议用 String 接收) |
batches[].departureDate |
String | 出发日期 yyyy-MM-dd(列表已按出发日期升序) |
batches[].batchLabel |
String | 团期标签(如 五一团 暑假首发 等运营自定义) |
batches[].batchName |
String | 团期完整名称 |
batches[].batchStatus |
String | 团期状态,字典 batch_status |
batches[].startingPrice |
BigDecimal | 团期起步价(元) |
batches[].enrolledCount |
Integer | 已报名人数 |
batches[].remainingSlots |
Integer | 剩余名额 |
batches[].isConfirmed |
Boolean | 是否已成团 |
四、重要提示给前端
1. 详情接口零改动 — 直接复用现有接口
不要再问"小蒙马是不是要专属详情接口",没有,也不需要。
GET /mp/product/{productId}
这个接口已经支持 GROUP 产品,会自动注入 batches 字段(详情接口 notes 已写明)。
前端动作:
- 列表页:调用
GET /mp/product/group/list拿到 productId - 详情页:直接
GET /mp/product/{productId}(productId 用 String 拼接,不要 parseInt) - 详情返回结构与现有产品详情完全一致,GROUP 产品的
batches字段在 detail VO 中已存在
2. productId 必须用 String 处理
后端 VO 字段:
@JsonSerialize(using = ToStringSerializer.class)
private Long productId;
序列化结果:"productId": "120000000000001" (带引号的字符串)
前端禁止:
// ❌ 错误:JS Number 最大安全整数 2^53,会精度丢失
const id = Number(item.productId);
// ✅ 正确:直接用 String
const id = item.productId;
fetch(`/mp/product/${id}`);
3. 与 /mp/product/list?productType=GROUP 的差异
| 维度 | /mp/product/list?productType=GROUP |
/mp/product/group/list |
|---|---|---|
| 分页 | 有(page+pageSize) | 无(hard cap 100) |
| 返回结构 | Result<PageResult<Map>> |
Result<List<Map>> |
| 字段 | 通用产品字段 + batches | 同左(一致) |
| 缓存 key | product:list: |
product:group:list: |
| 适用场景 | 通用列表(带筛选/分页) | 小蒙马专题页一次性铺满 |
建议:专题页用
/mp/product/group/list;通用列表(首页、搜索)继续用/mp/product/list。
五、缓存说明
- 服务端缓存:
@MpCache(prefix="product:group:list:", ttl=600) - TTL:600 秒(10 分钟)
- 缓存粒度:按
keyword区分,无 keyword 与有 keyword 是不同的 cache key - 失效时机:依赖自然过期(10 分钟内运营发布/下架的产品不会立即可见)
- 前端缓存建议:列表数据可在小程序端短期缓存(如 30 秒),下拉刷新时强制清缓存
六、字典对照
product_type(产品类型)
| 值 | 中文 | 说明 |
|---|---|---|
CORE |
核心产品 | 主推标准产品 |
ROUTE |
自驾路书 | 自驾游线路 |
CUSTOM |
私人定制 | 1对1定制行程 |
GROUP |
小蒙马 | 本接口固定返回此类型 |
batch_status(团期状态)
| 值 | 中文 | UI 建议 |
|---|---|---|
ENROLLING |
报名中 | 显示绿色标签 + 剩余名额 |
CONFIRMED |
已成团 | 显示蓝色标签"已成团" |
FULL |
已满员 | 显示灰色标签 + 禁用报名按钮 |
CLOSED |
已关闭 | 显示灰色标签 + 隐藏 CTA |
payment_type(支付方式)
| 值 | 中文 |
|---|---|
FULL |
全额支付 |
DEPOSIT |
订金 + 尾款 |
product_season(适合季节)
| 值 | 中文 |
|---|---|
spring |
春季 |
summer |
夏季 |
autumn |
秋季 |
winter |
冬季 |
七、前端实现建议
推荐:瀑布流 / 卡片墙
┌──────────────────────── 小蒙马专题页 ────────────────────────┐
│ [搜索框 keyword] │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ [封面图] │ │ [封面图] │ │ [封面图] │ │
│ │ 草原星空亲子3日 │ │ 呼伦贝尔大环线 │ │ 阿尔山雪国5日 │ │
│ │ 8人精品小团 │ │ 深度纯玩 │ │ 冬季限定 │ │
│ │ #亲子 #星空 │ │ #纯玩 #5星 │ │ #雪景 │ │
│ │ ¥3980起/人 │ │ ¥5680起/人 │ │ ¥6280起/人 │ │
│ │ ────────────── │ │ ────────────── │ │ ────────────── │ │
│ │ 最近团期: │ │ 最近团期: │ │ 最近团期: │ │
│ │ • 5/1 报名中(2) │ │ • 5/8 报名中(5) │ │ • 12/20 报名中 │ │
│ │ • 5/15 已成团 │ │ │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└──────────────────────────────────────────────────────────────┘
卡片渲染要点
- 顶部图区:
coverImageUrl,建议固定宽高比(如 16:10) - 标题区:
name(粗体)+subtitle(小字灰色) - 标签区:
tags取前 2-3 个,超出折叠 - 价格区:用
startPriceLabel直接渲染(已带¥和起/人后缀) - 团期区:
batches取前 2 条展示(按出发日期升序),显示departureDate+ 状态标签 + 剩余名额 - 空团期:
batches=[]时显示"暂无可售团期"或灰显
跳转详情
// 卡片点击
function onCardTap(item) {
// productId 必须用 String 拼接
wx.navigateTo({ url: `/pages/product/detail?id=${item.productId}` });
}
// 详情页加载
function loadDetail(id) {
// 直接复用现有详情接口,无需任何改动
return request.get(`/mp/product/${id}`);
}
八、回归验证
测试环境部署完成后,前端可通过以下方式验证:
GET https://api.test.1814.love/mp/product/group/list
预期:
- HTTP 200
data为数组(即使为空也是[])- 每个 item 的
productType === "GROUP" - 每个 item 的
productId是带引号的 String - GROUP 产品有可售团期时
batches.length > 0,否则batches === []