geo:移除地址联想代理接口(端上改用鸿蒙自带 Map Kit)laoyang/FamilyBook#33

用户指示「端侧不用高德,用鸿蒙自带的接口」。
端上已改用 @kit.MapKit 的 site.queryAutoComplete(),这个服务端代理随之无用:

- ApiHandler:删 case 'geo' 与 geo() 方法
- Geo:删 TIPS_URL 常量与 tips() 方法

网页端不受影响:它走浏览器端 JS API(AMap.AutoComplete),
从来不经过 /api/v1/geo/tips(已确认 addr_suggest.js 零引用)。

验证:接口返回 404(已删),calendar/books 等其它接口仍 401(正常鉴权)。
This commit is contained in:
2026-10-30 08:11:47 +08:00
parent 29b2ca0e3c
commit 73c84c1910
2 changed files with 0 additions and 192 deletions
-50
View File
@@ -112,13 +112,6 @@ final class ApiHandler
case 'resources': $this->resources($id, $sub, $method); return; case 'resources': $this->resources($id, $sub, $method); return;
case 'books': $this->books($id, $sub, $method); return; case 'books': $this->books($id, $sub, $method); return;
case 'notes': $this->notes($id, $sub, $method); return; case 'notes': $this->notes($id, $sub, $method); return;
/* 🔴 2026-10-30(工单 #36):高德**地址联想**代理。
端上地点输入框要"输入简单地址 → 下拉带详细地址",
但高德 Web 服务 key 不能进客户端(反编译即泄漏、配额被跑光)。
⇒ 走服务端代理,与网页端 `assets/js/addr_suggest.js` 同一个上游。
⚠️ 与 `geo` 平级,不能做成 `calendar/tips` —— 它是站点级工具,
不属于某个业务模块(影院/行程也用得上)。 */
case 'geo': $this->geo($sub, $method); return;
// 🔴 v45:登录(手机号 + 密码 → API 令牌)。 // 🔴 v45:登录(手机号 + 密码 → API 令牌)。
// 为什么必须走 API 而不是网页登录后复制 Cookie: // 为什么必须走 API 而不是网页登录后复制 Cookie:
// App 是 Bearer 令牌那套,没有会话;而原先前端只能让人 // App 是 Bearer 令牌那套,没有会话;而原先前端只能让人
@@ -1416,49 +1409,6 @@ final class ApiHandler
Api::ok($rows); Api::ok($rows);
} }
/**
* `GET /api/v1/geo/tips?kw=星巴克&limit=10` —— 地址联想(高德输入提示代理)。
*
* 🔴 2026-10-30(工单 #36):网页端地址联想早就有,"地名后面必须带详细地址"
* 是用户同一天的诉求(#32);鸿蒙端原本**连联想都没有**(纯文本框),
* 这里给端上补上,字段口径与网页端 `result.tips` 对齐:
* 每条都给 `name` + `detail`(省市区 + 门牌),端上把 detail 画在名字下面,
* 同名地点才分得开 —— 这正是用户要的"作为参考,让用户更好地选择"。
*
* 为什么要**服务端代理**而不是端上直连高德:
* 高德 Web 服务 key 是服务端密钥,打进安装包等于公开(反编译/抓包即可拿到),
* 配额会被跑光、key 还可能被高德封。网页端那套 JS API key + 安全密钥
* 是给浏览器域名白名单用的,原生端没有等价机制。
*
* @param string $sub 只认 `tips`(留出扩展余地,将来要 `regeo` 也能挂这里)
* @param string $method 只允许 GET
*/
private function geo(string $sub, string $method): void
{
/* ⚠️ 要登录:这是**要花配额**的接口,不开放匿名调用
(否则别人拿本站在跑自己的地址联想,配额算我们的)。 */
Api::me();
if ($method !== 'GET') {
Api::fail('地址联想只支持 GET', 'method_not_allowed', 405);
}
if ($sub !== 'tips') {
Api::fail('未知的地理接口:' . $sub, 'not_found', 404);
}
$kw = Api::str('kw', '', 60);
/* 少于 2 个字直接返回空数组(而不是报错):
端上照常手敲,不该因为"字太少"弹一个错误框。
阈值与网页端 addr_suggest.js 一致,两边体验才对得上。 */
if (mb_strlen($kw, 'UTF-8') < 2) {
Api::ok([]);
}
$limit = (int)Api::str('limit', '10');
if ($limit <= 0) { $limit = 10; }
Api::ok(Geo::tips($kw, $limit));
}
private function events(int $id, string $sub, string $method): void private function events(int $id, string $sub, string $method): void
{ {
[$fam, $pid] = Api::me(); [$fam, $pid] = Api::me();
-142
View File
@@ -344,8 +344,6 @@ final class Geo
* 旅程要"坐标/地址 → 城市"(必须联网问高德,因为地址千奇百怪)。 * 旅程要"坐标/地址 → 城市"(必须联网问高德,因为地址千奇百怪)。
*/ */
private const REGEO_URL = 'https://restapi.amap.com/v3/geocode/regeo'; private const REGEO_URL = 'https://restapi.amap.com/v3/geocode/regeo';
/** 高德输入提示(地址联想)端点 —— 与网页端 AutoComplete **同一个上游** */
private const TIPS_URL = 'https://restapi.amap.com/v3/assistant/inputtips';
private const HTTP_TIMEOUT = 3; private const HTTP_TIMEOUT = 3;
/** 直辖市(高德对它们的 `city` 字段返回空数组,要认 `province`) */ /** 直辖市(高德对它们的 `city` 字段返回空数组,要认 `province`) */
@@ -426,146 +424,6 @@ final class Geo
return is_array($ac) ? $ac : null; return is_array($ac) ? $ac : null;
} }
/**
* 高德**输入提示**(地址联想)—— 给**鸿蒙端**用。
*
* 🔴 为什么这个接口在服务端、而不是让端上直接调高德(2026-10-30,工单 #36):
* 1. 高德 Web 服务 key 是**服务端密钥**,放进客户端安装包就等于公开
* (反编译/抓包都能拿到,配额被人跑光、还可能被高德封 key);
* 2. 网页端用的是 **JS API key + 安全密钥**(`jsConfig()`),
* 那套机制是给浏览器域名白名单用的,原生端没有等价物;
* 3. 端上要的字段和网页 `result.tips` 一致(名字 + 省市区 + 门牌),
* 在服务端归一一次,端上就不用重复解析高德那三套不齐的字段。
*
* 🔴 用户 2026-10-30 的原始诉求(网页端已实现,本单同步到端上):
* 「输入一个简单地址、下拉显示详细地址时,不能只把地点告诉我,
* 一定要把详细地址也放在后面。因为同一个地址可能有好几个,
* 我根本不知道应该选哪一个。」
* ⇒ 返回的每一项都必须带 `detail`(省市区 + 门牌),端上把它画在名字下面,
* 同名地点才分得开。
*
* ⚠️ 高德的 `tips` 字段**不齐**(实测):
* · POI 类:`name` + `address` + `district`
* · 纯行政区划:`name`(就是地名)+ `district`,**没有 address**
* ⇒ `detail` 要把能拿到的都拼上,全空就退回 `district`,不能是空串。
* ⚠️ 有些 tip 是**公交线路/地铁站**(`typecode` 150x),`location` 也可能为空数组;
* 这类对"填地点"没用,过滤掉。
*
* @param string $kw 关键词(用户已输入的文字)
* @param int $limit 最多返回几条(端上视口有限,默认 10)
* @return array<int, array{name:string, detail:string, district:string, address:string, lng:string, lat:string}>
* 失败/没配 key/关键词太短 ⇒ **返回空数组**,不抛错(调用方照常手敲)
*/
public static function tips(string $kw, int $limit = 10): array
{
$kw = trim($kw);
/* 少于 2 个字不查:一个字的候选既多又没用,还白烧配额。
(与网页端 `addr_suggest.js` 的阈值一致,两边体验才对得上。) */
if (mb_strlen($kw, 'UTF-8') < 2) {
return [];
}
$key = self::apiKey();
if ($key === '' || !function_exists('curl_init')) {
return [];
}
$url = self::TIPS_URL . '?' . http_build_query([
'key' => $key,
'keywords' => $kw,
/* `city` 留空 = 全国范围。⚠️ 不要默认塞"青岛":
用户可能正在记外地的行程,限定城市反而搜不到。 */
'city' => '',
'citylimit' => 'false',
'output' => 'JSON',
]);
$ch = curl_init($url);
if ($ch === false) {
return [];
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => self::HTTP_TIMEOUT,
CURLOPT_CONNECTTIMEOUT => self::HTTP_TIMEOUT,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_USERAGENT => 'FamilyBook/1.0',
]);
$body = curl_exec($ch);
$err = curl_errno($ch);
curl_close($ch);
if ($err !== 0 || !is_string($body) || $body === '') {
return [];
}
$j = json_decode($body, true);
/* ⚠️ 成败看 body 里的 `status`(字符串 "1"),**不是** HTTP 状态码:
配额用完时高德返回 200 + status="0"。 */
if (!is_array($j) || (string)($j['status'] ?? '') !== '1') {
return [];
}
$tips = $j['tips'] ?? [];
if (!is_array($tips)) {
return [];
}
$out = [];
foreach ($tips as $t) {
if (!is_array($t)) {
continue;
}
$name = trim((string)($t['name'] ?? ''));
if ($name === '') {
continue;
}
/* 过滤公交/地铁线路(typecode 以 15 开头)。
⚠️ 高德的 `typecode` 有时是空数组 ⇒ 只在**确实是字符串**时判。 */
$tc = $t['typecode'] ?? '';
if (is_string($tc) && strpos($tc, '15') === 0) {
continue;
}
/* `district` 在不同条目里是字符串,也可能是空数组 ⇒ 统一成串 */
$district = '';
if (isset($t['district']) && is_string($t['district'])) {
$district = trim($t['district']);
} elseif (isset($t['adcode']) && is_string($t['adcode'])) {
// 只有 adcode 时拿不到地名,留空,别硬凑
$district = '';
}
$address = '';
if (isset($t['address']) && is_string($t['address'])) {
$address = trim($t['address']);
}
/* detail = 省市区 + 门牌,去重(address 常以 district 开头或反之) */
$parts = [];
if ($district !== '') { $parts[] = $district; }
if ($address !== '' && strpos($address, $district) !== 0) { $parts[] = $address; }
elseif ($address !== '' && $district === '') { $parts[] = $address; }
$detail = trim(preg_replace('/\s+/u', ' ', implode(' ', $parts)) ?? '');
/* 坐标:高德给 "lng,lat" 字符串;也可能是空数组 */
$lng = '';
$lat = '';
if (isset($t['location']) && is_string($t['location']) && strpos($t['location'], ',') !== false) {
[$lng, $lat] = array_map('trim', explode(',', $t['location'], 2));
}
$out[] = [
'name' => $name,
'detail' => $detail,
'district' => $district,
'address' => $address,
'lng' => $lng,
'lat' => $lat,
];
if (count($out) >= max(1, min(20, $limit))) {
break;
}
}
return $out;
}
/** /**
* 从高德的 addressComponent 里取出"城市"。 * 从高德的 addressComponent 里取出"城市"。
* *