From d656e0f7b4147599dc6d91a7df2becc4819f7570 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 11 May 2026 14:23:52 +0800 Subject: [PATCH] =?UTF-8?q?followup(admin/product-v2):=20POI=20=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=8C=89=E5=9F=8E=E5=B8=82=E9=99=90=E5=AE=9A=20?= =?UTF-8?q?=E2=80=94=20=E5=AD=97=E6=AE=B5=E5=B7=B2=E5=9C=A8=20itinerary[0]?= =?UTF-8?q?.gatherPlace,=20=E5=89=8D=E7=AB=AF=E8=AF=BB=E8=BF=99=E4=B8=AA?= =?UTF-8?q?=E5=8D=B3=E5=8F=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...in_poi-search-city-source-clarification.md | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 changelogs/2026-05/11_followup_admin_poi-search-city-source-clarification.md diff --git a/changelogs/2026-05/11_followup_admin_poi-search-city-source-clarification.md b/changelogs/2026-05/11_followup_admin_poi-search-city-source-clarification.md new file mode 100644 index 0000000..9b22fc3 --- /dev/null +++ b/changelogs/2026-05/11_followup_admin_poi-search-city-source-clarification.md @@ -0,0 +1,172 @@ +# 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 参数)