validate([ 'keyword' => ['required', 'string', 'max:120'], 'region' => ['nullable', 'string', 'max:80'], ]); $tk = $this->tiandituServerTk(); if ($tk !== null) { $rows = $this->searchTianditu($data['keyword'], $tk); if ($rows !== null) { return response()->json($rows); } } return response()->json($this->searchTencent($data['keyword'], $data['region'] ?? '苏州')); } public function reverseGeocode(Request $request): JsonResponse { $data = $request->validate([ 'lat' => ['required', 'numeric'], 'lng' => ['required', 'numeric'], ]); $tk = $this->tiandituServerTk(); if ($tk !== null) { $row = $this->reverseTianditu((float) $data['lat'], (float) $data['lng'], $tk); if ($row !== null) { return response()->json($row); } } return response()->json($this->reverseTencent((float) $data['lat'], (float) $data['lng'])); } /** @return array>|null null 表示应回退腾讯 */ private function searchTianditu(string $keyword, string $tk): ?array { $postStr = json_encode([ 'keyWord' => $keyword, 'level' => 12, 'mapBound' => self::SUZHOU_MAP_BOUND, 'queryType' => 1, 'start' => 0, 'count' => 15, ], JSON_UNESCAPED_UNICODE); $resp = Http::get('https://api.tianditu.gov.cn/v2/search', [ 'postStr' => $postStr, 'type' => 'query', 'tk' => $tk, ])->json(); if (! is_array($resp)) { return null; } if ($this->tiandituApiFailed($resp)) { return null; } $pois = $resp['pois'] ?? $resp['data'] ?? []; if (! is_array($pois)) { return []; } return collect($pois)->map(function ($item) { $item = is_array($item) ? $item : []; $lonlat = (string) ($item['lonlat'] ?? $item['lonLat'] ?? ''); $lng = null; $lat = null; if ($lonlat !== '' && str_contains($lonlat, ',')) { [$lngRaw, $latRaw] = array_map('trim', explode(',', $lonlat, 2)); $lng = is_numeric($lngRaw) ? (float) $lngRaw : null; $lat = is_numeric($latRaw) ? (float) $latRaw : null; } if ($lat === null) { $lat = data_get($item, 'lat') ?? data_get($item, 'location.lat'); } if ($lng === null) { $lng = data_get($item, 'lng') ?? data_get($item, 'lon') ?? data_get($item, 'location.lng'); } return [ 'title' => $item['name'] ?? $item['title'] ?? '', 'address' => $item['address'] ?? '', 'province' => $item['province'] ?? '', 'city' => $item['city'] ?? $item['cityName'] ?? '', 'district' => $item['district'] ?? $item['county'] ?? '', 'lat' => $lat, 'lng' => $lng, ]; })->filter(fn ($row) => $row['lat'] !== null && $row['lng'] !== null)->values()->all(); } /** @return array> */ private function searchTencent(string $keyword, string $region): array { $resp = Http::get('https://apis.map.qq.com/ws/place/v1/suggestion', [ 'keyword' => $keyword, 'region' => $region, 'region_fix' => 1, 'key' => $this->tencentServerKey(), ])->json(); $status = (int) ($resp['status'] ?? -1); if ($status === 121) { abort(422, '腾讯地图 WebService Key 每日调用量已达上限。请配置 TIANDITU_SERVER_TK 或更换腾讯 Key。'); } if ($status !== 0) { abort(422, $resp['message'] ?? '地图搜索失败'); } return collect($resp['data'] ?? [])->map(function ($item) { return [ 'title' => $item['title'] ?? '', 'address' => $item['address'] ?? '', 'province' => $item['province'] ?? '', 'city' => $item['city'] ?? '', 'district' => $item['district'] ?? '', 'lat' => data_get($item, 'location.lat'), 'lng' => data_get($item, 'location.lng'), ]; })->filter(fn ($row) => $row['lat'] !== null && $row['lng'] !== null)->values()->all(); } /** @return array|null */ private function reverseTianditu(float $lat, float $lng, string $tk): ?array { $postStr = json_encode([ 'lon' => $lng, 'lat' => $lat, 'ver' => 1, ], JSON_UNESCAPED_UNICODE); $resp = Http::get('https://api.tianditu.gov.cn/geocoder', [ 'postStr' => $postStr, 'type' => 'geocode', 'tk' => $tk, ])->json(); if (! is_array($resp) || $this->tiandituApiFailed($resp)) { return null; } $result = $resp['result'] ?? []; $addr = is_array($result) ? ($result['formatted_address'] ?? $result['address'] ?? '') : ''; $comp = is_array($result) ? ($result['addressComponent'] ?? []) : []; return [ 'address' => $addr, 'province' => data_get($comp, 'province'), 'city' => data_get($comp, 'city'), 'district' => data_get($comp, 'district') ?? data_get($comp, 'county'), ]; } /** @return array */ private function reverseTencent(float $lat, float $lng): array { $resp = Http::get('https://apis.map.qq.com/ws/geocoder/v1/', [ 'location' => $lat . ',' . $lng, 'get_poi' => 0, 'key' => $this->tencentServerKey(), ])->json(); $status = (int) ($resp['status'] ?? -1); if ($status === 121) { abort(422, '腾讯地图 WebService Key 每日调用量已达上限。请配置 TIANDITU_SERVER_TK 或更换腾讯 Key。'); } if ($status !== 0) { abort(422, $resp['message'] ?? '逆地理编码失败'); } $result = $resp['result'] ?? []; return [ 'address' => $result['address'] ?? '', 'province' => data_get($result, 'address_component.province'), 'city' => data_get($result, 'address_component.city'), 'district' => data_get($result, 'address_component.district'), ]; } /** @param array $resp */ private function tiandituApiFailed(array $resp): bool { $code = $resp['code'] ?? $resp['status'] ?? null; if ($code === 301012 || (is_string($resp['msg'] ?? null) && str_contains((string) $resp['msg'], '权限'))) { return true; } if ($code !== null && (string) $code !== '0' && (int) $code !== 0) { return true; } return false; } }