hl-api-changelog/changelogs/2026-05/11_followup_admin_poi-search-city-source-clarification.md

173 行
5.9 KiB
Markdown

# admin/product-v2: POI 搜索按城市限定 — 城市字段已在响应里, 在 `itinerary[0].gatherPlace`
> **服务**: hl-product-service-v2 (端口 8083)
> **关联**: 11_feat_admin_itinerary-mileage-anomaly-warnings.md 的第四节"前端建议改造 B"
> **日期**: 2026-05-11
> **影响范围**: 管理端产品编辑(step2) POI 搜索控件 — 仅前端改造, 后端 0 改动
---
## ⚠️ 关键变化(澄清前端误判)
前端反馈"产品详情没返回城市, 没法按 cityCode 限定 POI 搜索"。**这个判断不准确**:
- 顶层 `ProductDetailRespVO` 确实没有 `city/cityCode` 字段
-`itinerary[i].gatherPlace` / `itinerary[i].dismissalPlace` 这两个 JSON 字段 **早就含完整城市/行政区信息**(后端从高德 POI 选点时就一并存了 cityName / adcode / districtName / provinceName / fullPath / township)
后端**不需要新加字段**, 前端直接读 `itinerary[0].gatherPlace.cityName / adcode` 即可。
---
## 一、字段位置实证
正式服 GET `/admin/product/item/{id}` 实测响应(产品 2053003990289129474、2052985893312008193、2052926308213891073 三例一致):
```json
{
"data": {
"itinerary": [
{
"dayNumber": 1,
"gatherPlace": {
"name": "云上呼伦贝尔",
"poiId": "B0FFL...",
"adcode": "150702",
"address": "呼伦贝尔东山国际机场",
"cityName": "呼伦贝尔市",
"fullPath": "内蒙古自治区/呼伦贝尔市/海拉尔区/胜利街道",
"location": { "lng": 119.809336, "lat": 49.5568 },
"towncode": "150702010000",
"township": "胜利街道",
"districtName": "海拉尔区",
"provinceName": "内蒙古自治区"
},
"dismissalPlace": { /* 同样结构 */ }
}
]
}
}
```
`dismissalPlace` 字段结构同 `gatherPlace`
---
## 二、前端实现建议
### 1. 城市数据源(优先级)
```typescript
function getProductCity(detail) {
const firstDay = detail.itinerary?.[0];
const gather = firstDay?.gatherPlace;
// 高德 POI 搜索 region 参数支持 adcode (推荐) 或 cityname
return {
cityName: gather?.cityName,
adcode: gather?.adcode,
fullPath: gather?.fullPath,
};
}
```
### 2. 高德 POI 搜索调用
```typescript
// 高德 web 服务 v3 关键字搜索: region + citylimit=true
const params = {
key: AMAP_KEY,
keywords: userInput,
region: productCity.adcode, // adcode 比 cityname 更精确
city_limit: true, // 强制只在 region 内搜
output: 'json',
};
```
### 3. POI 搜索结果显示完整地址(根因防御)
每个 POI 结果项除了 `name` 之外, 把 `district + township` 也显示出来:
```
[结果项]
恩和 ← name
呼伦贝尔市 / 额尔古纳市 / 恩和俄罗斯族民族乡 ← district + township
```
让运营肉眼区分同名异地 POI(例如"恩和"在宝日希勒镇 vs 恩和俄罗斯族民族乡)。
---
## 三、边界场景处理
### Case A: DAY 1 集合地还没填(新建产品首次进 step2)
- 此时 `itinerary[0].gatherPlace``null`
- 第一个 POI 选择无 city 可限定 → **降级为全国搜索**, 选完后 setState 本地缓存 `cityName/adcode`
- 后续所有 POI 选择(DAY 1 节点 / DAY 1 解散 / DAY 2+...)都拿本地缓存做 region 限定
```typescript
const [productCity, setProductCity] = useState(null);
// 编辑已有产品时初始化:
useEffect(() => {
if (detail.itinerary?.[0]?.gatherPlace?.cityName) {
setProductCity(getProductCity(detail));
}
}, [detail]);
// 用户选完 DAY 1 集合地后:
function onGatherPlacePicked(poi) {
if (!productCity) {
setProductCity({ cityName: poi.cityName, adcode: poi.adcode });
}
// ... 落库
}
```
### Case B: 跨城市产品(如内蒙呼伦贝尔 + 蒙古国边境)
- 取 DAY 1 gather 即可, 这是产品的"主城市"
- 跨城市产品本身在编辑时, 跨城段的 POI 会被 detector 的 `单段 > 100km` warning 命中, 让运营人工确认, 不依赖 region 限定
### Case C: gatherPlace 是老行政区格式(无 cityName, 只有 adcode + fullPath)
- 测试服新 schema 是行政区粒度, 字段更少
- 兜底: `cityName = fullPath?.split('/')?.[1]`(第 1 段省, 第 2 段市)
- 或: 拿 adcode 前 4 位 → 高德反查城市
```typescript
function extractCity(place) {
if (!place) return null;
if (place.cityName && place.adcode) return { cityName: place.cityName, adcode: place.adcode };
if (place.fullPath) {
const parts = place.fullPath.split('/');
return { cityName: parts[1], adcode: place.adcode };
}
return null;
}
```
---
## 四、后端不需要做的事
明确不做(避免误改):
- ❌ 不在 `ProductDetailRespVO` 顶层加 `cityName/adcode` 冗余字段(数据已在 itinerary 里, 加冗余字段反而双源更易错乱)
- ❌ 不加 `product.city` 数据库字段(产品级城市当前业务无明确归属, "产品起点 = itinerary[0].gather" 这个隐式约定足够用)
- ❌ 不抽 `routeInfo.city` 字段(routeInfo VO 当前不含 city, 改它需要改 Entity + VO + 保存逻辑, 不值)
---
## 五、Test plan(给 mmg)
- [ ] 编辑产品 `2053003990289129474`, 在 DAY 4 集合地 POI 搜索输入"恩和", 验证只返回额尔古纳市恩和(50.83°), 不返回宝日希勒错点(49.28°)
- [ ] 新建产品, DAY 1 集合地选呼伦贝尔机场后, DAY 2/3/4 节点 POI 搜索自动限定到呼伦贝尔市
- [ ] POI 结果项显示 `district + township`, 让运营肉眼区分同名 POI
- [ ] 跨城市产品(如海拉尔出发去阿尔山)能正常编辑, 跨城段触发 detector warning 但不阻塞保存
---
## 六、参考
- 上一份 changelog: `11_feat_admin_itinerary-mileage-anomaly-warnings.md` 第四节有 POI 搜索改造的高层建议
- 实证脚本(只读 dump 字段): `D:\work2\HL\.tmp\prod_smoke_pr1939_mileage_warnings.py` (修改版可复用)
- 高德 web 服务文档: https://lbs.amap.com/api/webservice/guide/api/search (POI 搜索, region + citylimit 参数)