feat(admin/product-v2): 行程详情新增 mileageWarnings 字段 + POI 选错事故复盘 (PR #1938, Issue #1937)

这个提交包含在:
API Changelog Bot 2026-05-11 14:07:16 +08:00
父节点 cb6fa7328e
当前提交 1f1c15cedf

查看文件

@ -0,0 +1,149 @@
# admin/product-v2: 行程编辑详情新增里程异常 warnings 字段 + 建议前端 POI 搜索控件改进
> **服务**: hl-product-service-v2 (端口 8083)
> **PR**: #1938
> **Issue**: #1937
> **日期**: 2026-05-11
> **影响范围**: 管理端产品编辑(step2 行程编排)详情响应; POI 搜索控件(可选改进)
---
## ⚠️ 关键变化
后端在产品编辑详情响应里**新增了一个 `mileageWarnings: List<String>` 字段**(按天), 用于在运营选错 POI 时弹红旗提示。前端可在编辑页该天显示警告徽章。
**同时强烈建议(可选)**:POI 搜索控件 UX 优化, 防止运营选错同名异地 POI。已发生事故见下文背景。
---
## 一、背景
2026-05-11 发现产品《一次走透呼伦贝尔的旷野心脏》DAY 3 `daily_mileage = 475.2 km`, 客户用高德地图手测同样 5 点路线 = **275.0 公里**, 差 **+200 km(+73%)**。
### 根因(实证)
运营在产品 step2 编辑器搜"恩和", 选了高德返回的 POI `poiId=B0LALCK6P1`, 该 POI 名字叫"恩和"但**实际坐标在陈巴尔虎旗宝日希勒镇** `(lat=49.279092, lng=119.616988)`, 跟真正"额尔古纳市恩和俄罗斯民族乡" `(lat≈50.83)` 偏南 **167 km**。后端调高德 API 算驾车里程, 从骑马场(50.83°) → 错位"恩和"(49.28°)单段就多算 200 km。
### 平台 F 维度扫描
正式服全平台扫描坐标落在 `(lat 49.27-49.30, lng 119.60-119.65)` 范围 → 共 **6 条记录, 涉及 4 个产品**:
| 产品 | 状态 | 命中 |
|------|------|------|
| 2053003990289129474 一次走透呼伦贝尔的旷野心脏 | PUBLISHED | DAY3 dismissal + DAY4 gather |
| 2052985893312008193 呼伦贝尔5天4晚自由行 | COMPLETED | DAY2 dismissal + DAY3 gather |
| 2052926308213891073 燕6天5晚私人订制 | COMPLETED | DAY3 gather |
| 2052984406884225025 草原慢旅行 | DRAFT | DAY3 dismissal + DAY4 gather |
已通过 SQL `JSON_SET` 修正全部 6 条 location 到 `(50.828414, 119.912452)`, 修复后 F 扫描 = 0 行。
### 数据库 schema 差异
- **正式服旧 schema**: `product_itinerary_day.gather_place / dismissal_place` 是 JSON, 含 `location.lat/lng`
- **测试服新 schema**: 同字段已升级为**行政区粒度**(`name / level / adcode / fullPath / levelName`), 不嵌套坐标; 途经点搬到新表 `product_route_point`
- 测试服 F 维度扫描 0 命中, 新 schema 已天然阻断此 bug 模式
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 管理端获取产品详情(含 step2 行程) | GET | `/admin/product/item/{id}` | 响应新增字段 | 每个 `days[]` 元素新增 `mileageWarnings: List<String>`, 默认空数组 |
---
## 三、接口详情
### 1. 管理端获取产品详情 `GET /admin/product/item/{id}`
**VO**: `ProductDetailRespVO.days[] -> ItineraryDayRespVO`
#### 出参新增字段
| 字段 | 类型 | 说明 | 默认 |
|------|------|------|------|
| `days[i].mileageWarnings` | `List<String>` | 该天里程异常 warning 列表, 空数组表示正常。每条 warning 是给运营看的中文短句, 建议在编辑页该天显示红色感叹号 / 黄色徽章, 鼠标悬停展示具体内容 | `[]` |
#### 三条 warning 规则(后端自动检测)
| 规则 | 触发条件 | warning 文案样例 |
|------|----------|-----------------|
| 单段直线超长 | 同一天内相邻两节点直线距离 > 100km | `DAY3 第4段 直线距离 173.6km, 跨城市级跳跃, 可能 POI 坐标错位` |
| 驾车/直线比异常 | 某段 driving / linear > 3.5 (正常 1.4~2.5) | `DAY3 第4段 驾车/直线比 4.2, 正常 1.4~2.5, 强烈疑似 POI 错位或绕远` |
| 全天里程过大 | `dailyMileage > 600km` | `DAY3 全天里程 720km 异常大` |
不阻塞保存, 不持久化, 每次 GET 实时计算。资源服务异常时静默退化为空数组。
#### 响应示例
```json
{
"code": 0,
"data": {
"id": "2053003990289129474",
"name": "一次走透呼伦贝尔的旷野心脏",
"days": [
{
"dayNumber": 3,
"dailyMileage": 275.0,
"dailyDuration": 210,
"mileageWarnings": []
},
{
"dayNumber": 4,
"dailyMileage": null,
"dailyDuration": null,
"mileageWarnings": [
"DAY4 第2段 直线距离 145.2km, 跨城市级跳跃, 可能 POI 坐标错位"
]
}
]
}
}
```
---
## 四、前端建议改造(强烈推荐, 防再发)
### A. step2 编辑页展示 mileageWarnings
每天的里程展示位旁加一个图标:
- `mileageWarnings.length === 0` → 不显示 / 显示绿色 ✓
- `mileageWarnings.length > 0` → 显示红色 ⚠️, 鼠标 hover 展开 warning 列表
### B. POI 搜索控件 UX 改进(根因防御)
当前组件:运营搜"恩和", 高德 API 返回多个同名 POI, 用户只看到 name 选错。
建议改造:
1. **搜索结果项显示完整地址**:`name | district + township`, 例如:
- `恩和 | 呼伦贝尔市 / 陈巴尔虎旗 / 宝日希勒镇` ← 错的恩和
- `恩和 | 呼伦贝尔市 / 额尔古纳市 / 恩和俄罗斯族民族乡` ← 对的恩和
2. **按产品 cityCode 限定搜索范围**:产品基础信息已存 city/adcode, POI 搜索时 region 参数限定本市, 同名异地 POI 自然过滤
3. **选完 POI 后展示地图小预览**:`<map markers={[poi.location]}>`, 让运营肉眼校核位置对不对
不强求一次做完, B.1 最便宜也最有效。
---
## 五、不需要前端做的事
- 后端已修 6 条已发现错坐标记录(SQL JSON_SET), 前端不需要清缓存或重建索引
- `daily_mileage` NULL 的天 = 等运营在后台对该产品 step2 点一次「保存」让 `ItineraryAutoCalculator` 走高德重算, 前端展示 NULL 即可
---
## 六、回归风险
- `ProductDetailAggregator.fillItinerary` 多一次 Feign 调用资源服务取节点坐标, 已 try/catch 退化, 资源服务挂掉不影响详情主流程
- 阈值 100km 对跨地级市出发产品会触发 warning(例如"鄂尔多斯出发去敦煌"), 这是 feature 不是 bug, 让运营核对一下确认 POI 没选错就好
---
## 七、相关 PR / Issue
- PR #1938 后端实现
- Issue #1937 工单
- 上下文相关 PR: 6 条错坐标 SQL 修复在工单 #1937 评论里记录