diff --git a/app/ApiHandler.php b/app/ApiHandler.php index e75c654..2d586cb 100644 --- a/app/ApiHandler.php +++ b/app/ApiHandler.php @@ -112,13 +112,6 @@ final class ApiHandler case 'resources': $this->resources($id, $sub, $method); return; case 'books': $this->books($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 令牌)。 // 为什么必须走 API 而不是网页登录后复制 Cookie: // App 是 Bearer 令牌那套,没有会话;而原先前端只能让人 @@ -1416,49 +1409,6 @@ final class ApiHandler 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 { [$fam, $pid] = Api::me(); diff --git a/app/Geo.php b/app/Geo.php index 697f4f7..1d76d5e 100644 --- a/app/Geo.php +++ b/app/Geo.php @@ -344,8 +344,6 @@ final class Geo * 旅程要"坐标/地址 → 城市"(必须联网问高德,因为地址千奇百怪)。 */ 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; /** 直辖市(高德对它们的 `city` 字段返回空数组,要认 `province`) */ @@ -426,146 +424,6 @@ final class Geo 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 - * 失败/没配 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 里取出"城市"。 *