diff --git a/changelogs/2026-05/14_backend_admin_amap-poi-proxy-ready-replace-frontend-direct.md b/changelogs/2026-05/14_backend_admin_amap-poi-proxy-ready-replace-frontend-direct.md new file mode 100644 index 0000000..113af66 --- /dev/null +++ b/changelogs/2026-05/14_backend_admin_amap-poi-proxy-ready-replace-frontend-direct.md @@ -0,0 +1,196 @@ +--- +date: 2026-05-14 +type: backend-feature +module: admin-amap-poi-proxy +priority: high +backend_pr: ["#2253", "#2259", "#2261", "#2264"] +backend_issue: "#2249" +notify: ["@mmg"] +status: ready-for-frontend +related: ["12_frontend_bug_admin_mappicker-citylimit-blocks-cross-city-search.md"] +--- + +# 后端代理高德 POI 搜索接口已就绪 — 前端可切换不再直连高德 + +## TL;DR + +后端新增 2 个 admin 接口代理高德 POI 搜索, **citylimit 后端固定 false 不可改**, 解决多次复发的「跨盟市/跨省搜不到 POI」 BUG (`citylimit:true` 锁城市)。 + +测试服已真测通过(`阿尔山`/`故宫` 跨盟市/跨省都返). 前端 `PoiAutocomplete.vue` / `MapPicker.vue` 两个组件可以删 `AMap.AutoComplete` / `AMap.PlaceSearch` 直连, 改调本接口。 + +## 接口契约 + +### 1. 输入提示 (下拉联想, 替代 `AMap.AutoComplete`) + +``` +GET /admin/amap/poi/inputtips + ?keywords=阿尔山 必填, 用户输入关键字 (空白 returns 400 关键字不能为空) + &city=呼伦贝尔 可选, 仅影响排序权重, 不约束搜索范围 + &type= 可选, 高德 POI 类型代码 (typecode 多个逗号分隔) + +Headers: Authorization: Bearer {admin_token} +``` + +返回(真实样本): + +```json +{ + "code": 200, "message": "成功", + "data": [ + { + "id": "B01E20023T", + "name": "阿尔山站", + "district": "内蒙古自治区兴安盟阿尔山市", + "address": "兴林路附近", + "lng": 119.947575, "lat": 47.172702, + "adcode": "152202", "typecode": "150200" + }, + { "name": "阿尔山伊尔施机场", "district": "内蒙古自治区兴安盟阿尔山市", "lng": 119.91948, "lat": 47.311868, "adcode": "152202", "typecode": "150104" }, + { "name": "阿尔山", "district": "内蒙古自治区呼和浩特市回民区", "address": "成吉思汗西街101号慧谷·蓝庭", "lng": ..., "lat": ... } + ] +} +``` + +### 2. 关键字搜索 (回车精确搜, 替代 `AMap.PlaceSearch`) + +``` +GET /admin/amap/poi/search + ?keywords=故宫 必填 + &city= 可选 + &page=1 可选, 默认 1, 范围 [1,100] + &size=20 可选, 默认 20, 上限 25 (高德硬限制, >25 returns 400) +``` + +返回: + +```json +{ + "code": 200, "message": "成功", + "data": { + "total": 100, + "page": 1, "size": 20, + "list": [ + { + "id": "B000A8UIN8", + "name": "故宫博物院", + "district": "北京市东城区", + "address": "景山前街4号", + "lng": 116.397029, "lat": 39.917839, + "adcode": "...", "typecode": "..." + }, + { "name": "故宫博物院-午门", "district": "北京市东城区", ... } + ] + } +} +``` + +> 注: `district` 字段两接口都是**完整省市区路径** (含直辖市去重), 直接显示给用户即可。 + +## 前端改造点(逐文件) + +### `hl-ui/src/components/PoiAutocomplete.vue` (第 100-104 行 + 130 行) + +```js +// 删除 +autoComplete = new window.AMap.AutoComplete({ + city: props.city || '呼伦贝尔', + citylimit: true, + datatype: 'all', +}) +async function doAutoComplete(query) { + ... + autoComplete.search(query, (status, result) => {...}) +} + +// 替换 (新增 src/api/amap.js 包一层) +import { poiInputtips } from '@/api/amap' + +async function doAutoComplete(query) { + loading.value = true + try { + const list = await poiInputtips({ keywords: query, city: props.city || '' }) + options.value = list.slice(0, 15).map((tip) => ({ + label: tip.name, + value: tip, // 整个对象往上传, 含 lng/lat/district/adcode/typecode + fullPath: tip.district || '', + })) + } finally { + loading.value = false + } +} +``` + +### `hl-ui/src/components/MapPicker.vue` (第 360-431 行 + 444-448 行) + +```js +// 删除两段 +new window.AMap.AutoComplete({...}) +new window.AMap.PlaceSearch({...}) + +// 删除第 444-448 行 filteredPois 二次过滤 (后端已不锁城, 全国结果都该展示) + +// 替换为 +import { poiInputtips, poiSearch } from '@/api/amap' + +// AutoComplete (输入框联想) → poiInputtips({keywords, city}) +// PlaceSearch (回车精确搜) → poiSearch({keywords, city, page:1, size:20}) +``` + +### `hl-ui/src/views/product/edit/components/itinerary/RouteTagBar.vue` (第 26-28/79/105 行) + +```js +// 删除 productCity computed (不再传 city 锁范围) +const productCity = computed(() => store.productData.itinerary?.[0]?.gatherPlace?.cityName || '呼伦贝尔') + +// PoiAutocomplete 上的 :city="productCity" 也删掉, 不传 city 即全国搜 + +``` + +### 地图渲染部分**保留**前端 JS SDK + +`MapPicker.vue` 里 `new AMap.Map()` / Marker / 逆地理编码 (`AMap.Geocoder`) 仍前端 JS SDK 直连, 渲染必须前端。前端 JS API key (VITE_AMAP_KEY / sys_frontend_config.AMAP_KEY) 保留不动, 只是搜索这块走后端。 + +### 新建 `hl-ui/src/api/amap.js` + +```js +import request from '@/utils/request' + +export function poiInputtips(params) { + return request.get('/admin/amap/poi/inputtips', { params }).then(r => r.data) +} + +export function poiSearch(params) { + return request.get('/admin/amap/poi/search', { params }).then(r => r.data) +} +``` + +## 测试服真测结果(已 PASS, 前端可放心切) + +| 验收 | 结果 | +|---|---| +| `inputtips?keywords=阿尔山` 跨盟市 | ✅ 10 条, 9 条非呼伦贝尔, 含「阿尔山站/阿尔山伊尔施机场/阿尔山北站/阿尔山服务区」 | +| `search?keywords=故宫` 跨省 | ✅ total=100, 返「故宫博物院/午门/西南角楼/景仁宫/慈宁宫」全在「北京市东城区」 | +| `inputtips?keywords=` (空) | ✅ 业务码 400 「关键字不能为空」 | +| `search?size=30` (>25) | ✅ 业务码 400 「每页大小不能超过 25(高德硬限制)」 | + +测试服环境: `https://api.test.1814.love:9443/admin/amap/poi/...`, Bearer token 走管理后台登录拿到即可。 + +## 灰度建议 + +1. **后端已上线 dev 测试服** (4 个 PR 已合并) +2. **建议先切 `PoiAutocomplete.vue`** (1 个组件 / 1 个页面 = 行程编排「开始/结束」), 验证「阿尔山市」「呼伦贝尔学院」等跨地搜索正常 +3. **再切 `MapPicker.vue`** (5 个资源 EditModal: 景区/酒店/餐厅/活动/服务), 验证「阿尔山白狼峰景区」搜得到 +4. 浏览器 Network 看搜索请求 URL **应是 `/admin/amap/poi/...`**, 不应再有 `restapi.amap.com` 直接调用 + +## 关闭老 BUG + +本次后端实现彻底解决 `12_frontend_bug_admin_mappicker-citylimit-blocks-cross-city-search.md` 的根因 — 前端不再有 `citylimit` 这个开关, 业务上根本不会再被锁城。前端切完后老 changelog 可以 status: closed。 + +## 不在本 PR 范围 + +- 地图渲染 (`new AMap.Map()` / Marker) 仍前端 JS SDK +- 行政区划查询 / 驾车路线 / 天气 / 静态地图 / 地理编码 都已有后端实现, 不动 + +@mmg 接口可用了, 请按上面 4 个文件改造点替换前端代码。问题随时找我。