6.3 KiB
6.3 KiB
小程序行程路线图接口 - /mp/product/{productId}/route-map
- 日期: 2026-04-21
- PR: #1098 (Closes #1093)
- 类型: FEATURE(新增接口,免登录)
- 服务: hl-product-service-v2(路由
/mp/product/**) - 前端是否需要改动: 是(需新增接口调用 + 地图展示)
一、背景
小程序需要按产品 ID 拉取每天的「路线图」用于地图展示,所有节点必须带经纬度。无需登录(路线图是基础地理信息,浏览阶段即可看到)。
二、接口
| # | 方法 | 路径 | 鉴权 |
|---|---|---|---|
| 1 | GET | /mp/product/{productId}/route-map |
免登录(白名单) |
Path 参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
productId |
Long (字符串) | 是 | 产品 ID |
无 Query/Body 参数。
三、响应结构
顶层 data: MpProductRouteMapVO
| 字段 | 类型 | 说明 |
|---|---|---|
title |
String | 产品标题 |
days |
MpProductRouteDayVO[] |
每天的路线图,按 dayNumber 升序 |
MpProductRouteDayVO
| 字段 | 类型 | 说明 |
|---|---|---|
dayNumber |
Integer | 第几天,从 1 开始 |
title |
String | 当天标题(如 "D1 成都-四姑娘山") |
color |
String / null | 当天主题色(后端不返回,固定 null,前端用调色板兜底) |
origin |
MpRoutePointVO / null |
当天起点;缺数据时为 null |
destination |
MpRoutePointVO / null |
当天终点;缺数据时为 null |
waypoints |
MpRoutePointVO[] |
当天途经点(按 sort_order 升序),可能空数组 |
scenicSpots |
MpRouteScenicVO[] |
当天景点(含封面图),可能空数组 |
MpRoutePointVO(起点 / 终点 / 途经点)
| 字段 | 类型 | 说明 |
|---|---|---|
lng |
Number | 经度 |
lat |
Number | 纬度 |
name |
String | 名称 |
MpRouteScenicVO(景点)
| 字段 | 类型 | 说明 |
|---|---|---|
lng |
Number | 经度 |
lat |
Number | 纬度 |
name |
String | 名称 |
coverUrl |
String / null | 封面图完整 URL,缺失为 null |
四、业务规则
- 起终点:优先取行程当天「集合地 / 解散地」JSON 字段;若缺失,回退到该天
product_route_point首/末 - 途经点:取该天
product_route_point全量按sort_order升序 - 景点:取该天行程节点中
nodeType ∈ {SCENIC, ACTIVITY}的资源,经纬度 + 封面图来自资源服务(景点表 / 活动表) - 经纬度缺失节点直接过滤(不会返回
lng=0/lat=0或null坐标)—— 前端拿到的所有点都保证有有效坐标 - 未上架 / 已下架 / 不存在产品 → 业务码
code=404, message="产品不存在或已下架"(与现有/mp/product/{id}详情接口对齐) - 景点封面图加载失败(资源服务异常):景点列表降级为空数组,路线骨架仍正常返回(不阻断主流程)
五、响应示例
PUBLISHED 产品(部分天有数据 + 部分天缺数据)
{
"code": 200,
"success": true,
"data": {
"title": "川西亲子研学5日游(升级版)",
"days": [
{
"dayNumber": 1,
"title": "抵达成都",
"color": null,
"origin": { "lng": 103.947086, "lat": 30.578528, "name": "成都双流机场" },
"destination": { "lng": 104.061902, "lat": 30.673933, "name": "宽窄巷子" },
"waypoints": [
{ "lng": 104.061902, "lat": 30.673933, "name": "宽窄巷子" }
],
"scenicSpots": []
},
{
"dayNumber": 2,
"title": "成都-四姑娘山",
"color": null,
"origin": { "lng": 104.066301, "lat": 30.572961, "name": "成都" },
"destination": { "lng": 102.8987, "lat": 31.0827, "name": "四姑娘山" },
"waypoints": [
{ "lng": 102.8987, "lat": 31.0827, "name": "四姑娘山" }
],
"scenicSpots": []
},
{
"dayNumber": 3,
"title": "四姑娘山-丹巴",
"color": null,
"origin": null,
"destination": null,
"waypoints": [],
"scenicSpots": []
}
]
}
}
未上架 / 已下架 / 不存在
{
"code": 404,
"success": false,
"message": "产品不存在或已下架",
"data": null
}
六、前端使用建议
调用示例
const res = await wx.request({
url: `https://api.test.1814.love:9443/mp/product/${productId}/route-map`,
method: 'GET'
// 不需要带 Authorization header
});
if (res.data.code !== 200) {
// 已下架 / 不存在
return;
}
const { title, days } = res.data.data;
渲染地图(高德/腾讯地图,伪代码)
const palette = ['#4285F4', '#EA4335', '#FBBC04', '#34A853', '#9C27B0'];
days.forEach((day, idx) => {
const color = day.color || palette[idx % palette.length];
// 折线:origin → waypoints → destination
const polylinePoints = [
day.origin,
...day.waypoints,
day.destination
].filter(Boolean); // 缺失的点跳过
drawPolyline(polylinePoints, color);
// 景点 marker
day.scenicSpots.forEach(spot => {
drawMarker(spot, { icon: spot.coverUrl, label: spot.name });
});
});
⚠️ 注意
- 所有节点都保证有 lng/lat,前端无需再判空坐标
origin/destination可能为 null(当天数据不全),渲染折线时要过滤color后端固定返回 null,前端按 dayNumber index 取调色板色scenicSpots[].coverUrl可能为 null(景点未配封面),需有兜底图waypoints/scenicSpots可能空数组
七、不兼容变更
无。新增接口,与既有接口无交集。
八、回归验证
# PUBLISHED 产品
curl -sk "https://api.test.1814.love:9443/mp/product/2044803580070703106/route-map" | jq .
# DRAFT 产品(应返回 404 业务码)
curl -sk "https://api.test.1814.love:9443/mp/product/2045018534152478721/route-map" | jq .
预期:
- PUBLISHED:
code=200,data.title非空,data.days[]非空,每个有数据的 day 节点都带有效 lng/lat - DRAFT:
code=404, message="产品不存在或已下架"