feat(api): add Tianditu geocoding

This commit is contained in:
2026-06-09 17:09:42 +08:00
parent 1712ecd4c7
commit e588d1cf33
6 changed files with 259 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
from typing import Any
from fastapi import APIRouter, HTTPException, status
from app.services.geocoding import (
TiandituGeocodeRequest,
TiandituGeocodingAPIError,
TiandituGeocodingConfigError,
geocode_tianditu,
)
router = APIRouter()
@router.post(
"/tianditu/geocode",
summary="Tianditu Geocoding",
description="调用天地图地理编码服务,将结构化地址转换为经纬度",
)
async def tianditu_geocode(request: TiandituGeocodeRequest) -> dict[str, Any]:
try:
return await geocode_tianditu(request)
except TiandituGeocodingConfigError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=str(exc),
) from exc
except TiandituGeocodingAPIError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
+2
View File
@@ -19,6 +19,7 @@ from app.api.v1.endpoints import (
audit, # 新增:审计日志
meta,
web_search,
geocoding,
)
from app.api.v1.endpoints.network import (
general,
@@ -95,6 +96,7 @@ api_router.include_router(misc.router, tags=["Misc"])
api_router.include_router(risk.router, tags=["Risk"])
api_router.include_router(cache.router, tags=["Cache"])
api_router.include_router(web_search.router, tags=["Web Search"])
api_router.include_router(geocoding.router, tags=["Geocoding"])
api_router.include_router(leakage.router, prefix="/leakage", tags=["Leakage"])
api_router.include_router(
burst_detection.router, prefix="/burst-detection", tags=["Burst Detection"]
+5
View File
@@ -69,6 +69,11 @@ class Settings(BaseSettings):
BOCHA_WEB_SEARCH_URL: str = "https://api.bochaai.com/v1/web-search"
BOCHA_WEB_SEARCH_TIMEOUT_SECONDS: float = 30.0
# Tianditu Geocoding API
TIANDITU_GEOCODER_TOKEN: str = ""
TIANDITU_GEOCODER_URL: str = "https://api.tianditu.gov.cn/geocoder"
TIANDITU_GEOCODER_TIMEOUT_SECONDS: float = 30.0
@property
def SQLALCHEMY_DATABASE_URI(self) -> str:
db_password = quote_plus(self.DB_PASSWORD)
+76
View File
@@ -0,0 +1,76 @@
import json
from typing import Any
import httpx
from pydantic import AliasChoices, BaseModel, Field
from app.core.config import settings
class TiandituGeocodeRequest(BaseModel):
keyword: str = Field(
...,
min_length=1,
validation_alias=AliasChoices("keyword", "keyWord"),
description="地理编码地址关键字",
)
class TiandituGeocodingConfigError(RuntimeError):
pass
class TiandituGeocodingAPIError(RuntimeError):
def __init__(self, status_code: int, detail: Any):
super().__init__("Tianditu Geocoding API request failed")
self.status_code = status_code
self.detail = detail
async def geocode_tianditu(
request: TiandituGeocodeRequest,
*,
client: httpx.AsyncClient | None = None,
) -> dict[str, Any]:
if not settings.TIANDITU_GEOCODER_TOKEN:
raise TiandituGeocodingConfigError("TIANDITU_GEOCODER_TOKEN is not configured")
params = {
"ds": json.dumps({"keyWord": request.keyword}, ensure_ascii=False),
"tk": settings.TIANDITU_GEOCODER_TOKEN,
}
if client is not None:
response = await client.get(settings.TIANDITU_GEOCODER_URL, params=params)
return _parse_response(response)
async with httpx.AsyncClient(
timeout=settings.TIANDITU_GEOCODER_TIMEOUT_SECONDS
) as managed_client:
response = await managed_client.get(
settings.TIANDITU_GEOCODER_URL,
params=params,
)
return _parse_response(response)
def _parse_response(response: httpx.Response) -> dict[str, Any]:
try:
response.raise_for_status()
except httpx.HTTPStatusError as exc:
raise TiandituGeocodingAPIError(
exc.response.status_code,
_response_detail(exc.response),
) from exc
data = response.json()
if str(data.get("status")) != "0":
raise TiandituGeocodingAPIError(502, data)
return data
def _response_detail(response: httpx.Response) -> Any:
try:
return response.json()
except ValueError:
return response.text