diff --git a/changelogs/2026-04/2026-04-21_mp-product-route-map.md b/changelogs/2026-04/2026-04-21_mp-product-route-map.md new file mode 100644 index 0000000..1b815e0 --- /dev/null +++ b/changelogs/2026-04/2026-04-21_mp-product-route-map.md @@ -0,0 +1,216 @@ +# 小程序行程路线图接口 - `/mp/product/{productId}/route-map` + +- **日期**: 2026-04-21 +- **PR**: [#1098](https://git.1814.love:8443/wx/HL/pulls/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 产品(部分天有数据 + 部分天缺数据) + +```json +{ + "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": [] + } + ] + } +} +``` + +### 未上架 / 已下架 / 不存在 + +```json +{ + "code": 404, + "success": false, + "message": "产品不存在或已下架", + "data": null +} +``` + +--- + +## 六、前端使用建议 + +### 调用示例 + +```ts +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; +``` + +### 渲染地图(高德/腾讯地图,伪代码) + +```ts +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` 可能空数组** + +--- + +## 七、不兼容变更 + +**无**。新增接口,与既有接口无交集。 + +--- + +## 八、回归验证 + +```bash +# 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="产品不存在或已下架"`