# 行程编排集合地/解散地:行政区 → 高德 POI 精准定位 **类型**: 后端 FEAT(字段扩展+读路径增强) + 前端 picker 切换 + regeo 调用 **关联**: 工单 #1563 / PR #1570 + follow-up 工单 #1574 / PR #1575(镇级行政区字段) **日期**: 2026-04-30 **前端处理者**: mmg **影响范围**: 管理后台产品编辑页 Step 2「行程编排」→ DAY X 的「开始 / 结束」字段(即 `gatherPlace` / `dismissalPlace`) > ⚠️ **2026-04-30 16:25 update(PR #1575)**:行政区精度要求**到镇/街道级**。POI 自身只到区县,前端选 POI 后**必须额外调高德 regeo (逆地理编码)** 拿镇级数据。详见下方 schema 和 mmg 前端 mapping。 --- ## 业务目的 集合地 / 解散地从「行政区」(如「海拉尔区」)升级为「精准 POI」(如「海拉尔东山国际机场 / 海拉尔火车站」)。前端 `gatherPlace/dismissalPlace` 选择器从行政区下拉换成**高德 POI Autocomplete**。 途经路线 (`routePoints`) 不在本次范围内(已是 lng/lat 结构)。 --- ## JSON Schema 升级 ### 新格式(POI 精准定位 + 镇级行政区,2026-04-30 起新数据按这个落库) ```jsonc { "name": "海拉尔东山国际机场", // POI 名称 "address": "内蒙古呼伦贝尔市海拉尔区机场路", // 详细地址 "location": { "lng": 119.825, "lat": 49.205 }, // POI 精确坐标 "poiId": "B0FFGTEST1", // 高德 POI ID "adcode": "150702", // 区县编码(POI 自带) "provinceName": "内蒙古自治区", // 省(regeo addressComponent.province) "cityName": "呼伦贝尔市", // 市(regeo addressComponent.city) "districtName": "海拉尔区", // 区(regeo addressComponent.district) "township": "东山街道", // 镇/街道(regeo addressComponent.township) "towncode": "150702002000", // 镇编码(regeo addressComponent.towncode) "fullPath": "内蒙古自治区/呼伦贝尔市/海拉尔区/东山街道" // 4 段(前端拼) } ``` **关键点**:POI 本身(高德 PlaceSearch 返回)只到区县级 `adname`,**到镇级必须前端额外调高德 regeo (`AMap.Geocoder.getAddress`)** 拿 `addressComponent.{township, towncode}` 后 merge 到 gatherPlace JSON。 ### 老格式(行政区,2026-04-30 之前已落库的存量数据,零迁移继续工作) ```jsonc { "name": "海拉尔区", "level": "district", "adcode": "150702", "fullPath": "内蒙古自治区/呼伦贝尔市/海拉尔区", "levelName": "区/县" } ``` 后端**同时兼容三种格式**: - 新 POI 精准定位(含 `location`) - 老行政区无 `center`(依赖 `adcode` 反查) - 更早期行政区带 `center: 'lng,lat'`(直接 split) --- ## mmg 前端要做的事(hl-ui 仓库) ### 1. 替换 picker 组件 `src/views/product/edit/Step2*.vue` 行程编排 DAY X 块里,「开始」「结束」两个 `n-select` / 行政区 picker,**替换成高德 POI Autocomplete 输入框**。 参考高德 JS API:[`AMap.Autocomplete` + `AMap.PlaceSearch`](https://lbs.amap.com/api/javascript-api-v2/documentation#autocomplete);输入关键字 → 用户选择 POI → 把 POI 数据组装成上面的新 JSON 结构。 ### 2. 落库 JSON 字段映射(高德 POI → 后端字段) ```js // 选 POI 后,先用 location 调 regeo 拿镇级行政区,再 merge function poiToPlace(poi, callback) { const geocoder = new AMap.Geocoder(); geocoder.getAddress([poi.location.lng, poi.location.lat], (status, result) => { if (status !== 'complete') { // 降级:regeo 失败时退回到 POI 自带数据(无 township/towncode) callback({ name: poi.name, address: poi.address || '', location: { lng: Number(poi.location.lng), lat: Number(poi.location.lat) }, poiId: poi.id || '', adcode: poi.adcode || '', cityName: poi.cityname || '', districtName: poi.adname || '' // 此时 provinceName/township/towncode/fullPath 留空,后端兼容 }); return; } const ac = result.regeocode.addressComponent; callback({ name: poi.name, address: poi.address || result.regeocode.formattedAddress, location: { lng: Number(poi.location.lng), lat: Number(poi.location.lat) }, poiId: poi.id || '', adcode: poi.adcode || ac.adcode, provinceName: ac.province || '', cityName: (ac.city && ac.city.length) ? ac.city : '', // 直辖市 city 是空数组 districtName: ac.district || '', township: ac.township || '', towncode: ac.towncode || '', fullPath: [ac.province, ac.city, ac.district, ac.township].filter(s => s && s.length).join('/') }); }); } ``` ### 3. PUT 行程接口 接口路径不变:`PUT /admin/product/item/{productId}/itinerary`,`days[].gatherPlace` / `days[].dismissalPlace` 用上面的新 JSON 结构。后端字段全保留(向后兼容)。 ### 4. 详情回显 `GET /admin/product/item/{productId}` 的 `data.itinerary[i].gatherPlace` / `dismissalPlace`: - **新数据**:包含 `name / address / location / poiId / adcode / cityName / districtName`(7 字段全在) - **老数据**:包含 `name / level / adcode / fullPath / levelName`(5 字段) - 前端组件渲染时按字段存在性切换:有 `location.lng` 就用 POI 模式回显;否则按老行政区回显(兼容期) --- ## 后端测试服 round-trip(已完成) 测试服部署:commit `982558f9` 已部署 hl-product-service-v2 + hl-order-service-v2 双实例。 ```bash # 1) 老格式产品 GET 详情 → PlaceItem 老字段完整回显(零回归) GET /admin/product/item/2047268852147875842 → day1.gatherPlace = {"name":"海拉尔区","level":"district","adcode":"150702","fullPath":"内蒙古...","levelName":"区/县"} ✅ # 2) PUT 新 POI 格式 → GET 详情回显 7 字段全在 PUT /admin/product/item/{draftId}/itinerary body: { days:[{ gatherPlace:{name,address,location:{lng,lat},poiId,adcode,cityName,districtName} }] } → HTTP 200 「行程保存成功」 GET → day1.gatherPlace 7 字段完整含 location.lng=119.825/lat=49.205 ✅ # 3) 同 day 混合格式 (gather=POI, dismiss=老) 兼容 → 两边各自字段完整回显,互不干扰 ✅ ``` --- ## 后端改动(参考) 13 个文件 +846 / -48,PR #1570: - `InternalProductDetailVO.PlaceItem` + `ProductDetailVO.PlaceItem` (Feign 镜像) **扩展 7 个 POI 字段 + 嵌套 Location 静态类**,旧字段保留兼容 - `MpProductRouteMapService.parsePlaceVO`:路径优先级 `location → center → adcode 反查` - `AmapDrivingService.parseCenterToLngLat`:同上优先级 - `TeamReportService.parseCityFromSnapshot`:取城市优先 `cityName/districtName`,回退 `fullPath` - VO 注释批量升级三种格式说明 - 单测 +13(含 Hutool BeanUtil 嵌套 Map → Location 运行时验证) - DB schema 不变(仍是 JSON 列) - 老数据零迁移 --- ## 风险 / 兼容性 - **零回归承诺**:老行政区数据继续按原行为工作(路线图 origin/destination / 每日里程 / 团报城市 全部正常) - **新旧混合期**:同一产品 itinerary 不同 day 可以混存新 POI / 老行政区结构,互不影响 - **降级**:POI 缺 `location` 时自动回退到 `adcode` 反查中心点(高德 API),与现行 `MpProductRouteMapService` 老路径一致 --- ## 联调建议 mmg 前端改完后: 1. 本地起 mock 服务返回新 POI 结构,验证组件回显 2. 接到测试服 `api.test.1814.love:9443` 后端,PUT → GET round-trip 验证字段对齐 3. 看路线图 / 团报 / 每日里程,确认坐标用的是 POI 精确坐标(比之前 adcode 中心点更精准)