hl-api-changelog/changelogs/2026-04/2026-04-18_mp-group-product-list.md
2026-04-18 19:14:07 +08:00

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 封面图 URLOSS 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)
  • TTL600 秒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=[] 时显示"暂无可售团期"或灰显

跳转详情

// 卡片点击
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 === []