hl-api-changelog/changelogs/2026-04/30_feat_product_itinerary-place-poi-precision.md
API Changelog Bot 51ddc2f58e docs(changelog): 行程精准定位 schema 追加镇级字段 (#1574/#1575)
PlaceItem 加 provinceName/township/towncode 三段镇级行政区字段。
@mmg 选 POI 后多调一次高德 regeo 拿 addressComponent.township/towncode + 拼 4 段 fullPath。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:27:55 +08:00

7.8 KiB

行程编排集合地/解散地:行政区 → 高德 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 updatePR #1575:行政区精度要求到镇/街道级。POI 自身只到区县,前端选 POI 后必须额外调高德 regeo (逆地理编码) 拿镇级数据。详见下方 schema 和 mmg 前端 mapping。


业务目的

集合地 / 解散地从「行政区」(如「海拉尔区」)升级为「精准 POI」(如「海拉尔东山国际机场 / 海拉尔火车站」)。前端 gatherPlace/dismissalPlace 选择器从行政区下拉换成高德 POI Autocomplete

途经路线 (routePoints) 不在本次范围内(已是 lng/lat 结构)。


JSON Schema 升级

新格式POI 精准定位 + 镇级行政区,2026-04-30 起新数据按这个落库)

{
  "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 之前已落库的存量数据,零迁移继续工作)

{
  "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 APIAMap.Autocomplete + AMap.PlaceSearch;输入关键字 → 用户选择 POI → 把 POI 数据组装成上面的新 JSON 结构。

2. 落库 JSON 字段映射(高德 POI → 后端字段)

// 选 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}/itinerarydays[].gatherPlace / days[].dismissalPlace 用上面的新 JSON 结构。后端字段全保留(向后兼容)。

4. 详情回显

GET /admin/product/item/{productId}data.itinerary[i].gatherPlace / dismissalPlace

  • 新数据:包含 name / address / location / poiId / adcode / cityName / districtName7 字段全在)
  • 老数据:包含 name / level / adcode / fullPath / levelName5 字段)
  • 前端组件渲染时按字段存在性切换:有 location.lng 就用 POI 模式回显;否则按老行政区回显(兼容期)

后端测试服 round-trip已完成

测试服部署commit 982558f9 已部署 hl-product-service-v2 + hl-order-service-v2 双实例。

# 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 中心点更精准)