新增 mp 小蒙马产品全部列表接口 changelog
这个提交包含在:
父节点
c61c1d2d84
当前提交
cefe2ae145
@ -0,0 +1,338 @@
|
||||
# 小程序端 - 新增小蒙马产品全部列表接口
|
||||
|
||||
- **日期**: 2026-04-18
|
||||
- **PR**: [#847](https://git.1814.love:8443/wx/HL/pulls/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=亲子
|
||||
```
|
||||
|
||||
#### 完整响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"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 字段:
|
||||
|
||||
```java
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long productId;
|
||||
```
|
||||
|
||||
序列化结果:`"productId": "120000000000001"` (**带引号的字符串**)
|
||||
|
||||
前端**禁止**:
|
||||
|
||||
```javascript
|
||||
// ❌ 错误: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 已成团 │ │ │ │ │ │
|
||||
│ └────────────────┘ └────────────────┘ └────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 卡片渲染要点
|
||||
|
||||
1. **顶部图区**:`coverImageUrl`,建议固定宽高比(如 16:10)
|
||||
2. **标题区**:`name`(粗体)+ `subtitle`(小字灰色)
|
||||
3. **标签区**:`tags` 取前 2-3 个,超出折叠
|
||||
4. **价格区**:用 `startPriceLabel` 直接渲染(已带 `¥` 和 `起/人` 后缀)
|
||||
5. **团期区**:`batches` 取前 2 条展示(按出发日期升序),显示 `departureDate` + 状态标签 + 剩余名额
|
||||
6. **空团期**:`batches=[]` 时显示"暂无可售团期"或灰显
|
||||
|
||||
### 跳转详情
|
||||
|
||||
```javascript
|
||||
// 卡片点击
|
||||
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 === []`
|
||||
正在加载...
x
在新工单中引用
屏蔽一个用户