From 5b565d57fc6888a5e7b77178a1cd9e5daba06794 Mon Sep 17 00:00:00 2001 From: lingniu Date: Wed, 12 Aug 2026 12:04:39 +0800 Subject: [PATCH] fix(map): rate limit station reverse geocoding --- vehicle-map/.env.example | 2 ++ vehicle-map/README.md | 2 +- vehicle-map/server.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/vehicle-map/.env.example b/vehicle-map/.env.example index 99393ca3..75fb2509 100644 --- a/vehicle-map/.env.example +++ b/vehicle-map/.env.example @@ -10,3 +10,5 @@ AMAP_REGEOCODE_KEY=replace-with-amap-web-service-key # 行政区结果按坐标落盘缓存 7 天,避免每次刷新重复调用高德。 STATION_GEOCODE_CACHE_SECONDS=604800 STATION_GEOCODE_CACHE_PATH=/opt/lingniu-vehicle-map/cache/station-geocode.json +# 高德 Web 服务常见 QPS 配额较低;全局限速以避免冷缓存时触发 10021。 +AMAP_REGEOCODE_MIN_INTERVAL_SECONDS=0.25 diff --git a/vehicle-map/README.md b/vehicle-map/README.md index 6b366f48..1120f1b4 100644 --- a/vehicle-map/README.md +++ b/vehicle-map/README.md @@ -32,7 +32,7 @@ python3 server.py - `POST /api/v1/vehicles/mileage/query`:全部授权车辆当日里程; - `POST /api/v1/hydrogen-stations/query`:资产库只读加氢站地图点位。 -以上三个接口均使用开放平台的 `Authorization: Bearer ` 鉴权。生产 AppKey 仅保存在 ECS 的 `/opt/lingniu-vehicle-map/env/vehicle-map.env`,文件权限为 `root:vehicle-map 0640`。同一配置文件中的 `AMAP_REGEOCODE_KEY` 仅在服务端使用:站点省、市、区按 GCJ-02 坐标调用高德逆地理生成,不能使用资产表的行政区字段;结果以坐标为键落盘缓存 7 天。详细地址仍直接保留资产原始字段。 +以上三个接口均使用开放平台的 `Authorization: Bearer ` 鉴权。生产 AppKey 仅保存在 ECS 的 `/opt/lingniu-vehicle-map/env/vehicle-map.env`,文件权限为 `root:vehicle-map 0640`。同一配置文件中的 `AMAP_REGEOCODE_KEY` 仅在服务端使用:站点省、市、区按 GCJ-02 坐标调用高德逆地理生成,不能使用资产表的行政区字段;请求通过全局速率限制避免高德 QPS 限流,结果以坐标为键落盘缓存 7 天。详细地址仍直接保留资产原始字段。 本服务提供: diff --git a/vehicle-map/server.py b/vehicle-map/server.py index 6d545162..5a7961c5 100644 --- a/vehicle-map/server.py +++ b/vehicle-map/server.py @@ -38,12 +38,15 @@ STATION_GEOCODE_CACHE_SECONDS = int(os.getenv("STATION_GEOCODE_CACHE_SECONDS", " STATION_GEOCODE_CACHE_PATH = Path(os.getenv( "STATION_GEOCODE_CACHE_PATH", str(ROOT / ".station-geocode-cache.json") )) +AMAP_REGEOCODE_MIN_INTERVAL_SECONDS = float(os.getenv("AMAP_REGEOCODE_MIN_INTERVAL_SECONDS", "0.25")) _cache_lock = threading.Lock() _cache = {} _cache_load_locks = {} _station_geocode_cache_lock = threading.Lock() _station_geocode_cache = None _station_geocode_cache_dirty = False +_station_geocode_rate_lock = threading.Lock() +_station_geocode_next_request_at = 0.0 class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): @@ -142,8 +145,15 @@ def _persist_station_geocode_cache(): def _reverse_geocode_station(longitude, latitude): + global _station_geocode_next_request_at if not AMAP_REGEOCODE_KEY: raise RuntimeError("AMAP_REGEOCODE_KEY is not configured") + with _station_geocode_rate_lock: + now = time.monotonic() + wait_seconds = max(0.0, _station_geocode_next_request_at - now) + _station_geocode_next_request_at = max(now, _station_geocode_next_request_at) + AMAP_REGEOCODE_MIN_INTERVAL_SECONDS + if wait_seconds: + time.sleep(wait_seconds) query = urlencode({ "key": AMAP_REGEOCODE_KEY, "location": "{:.6f},{:.6f}".format(longitude, latitude),