feat(map): derive station regions from GPS
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
面向运维和司机调度的独立公共站点导航服务,默认本地端口为 `20804`。
|
||||
|
||||
它只调用一次上游加氢站目录接口,并通过正向字段白名单对外提供:名称、位置、省市区、地址与合作状态。车辆数据、加氢量及其他运营字段不会被加载或从这个服务的 API 返回。
|
||||
它复用运营地图已经按 GPS 逆地理解析、落盘缓存的站点目录,并只对外提供:名称、位置、省市区、地址与合作状态。车辆数据、加氢量及其他运营字段不会被加载或从这个服务的 API 返回。
|
||||
|
||||
## 本地运行
|
||||
|
||||
@@ -19,7 +19,7 @@ export OPEN_PLATFORM_APP_KEY='<32位AppKey>'
|
||||
- `GET /api/health`:进程健康状态;
|
||||
- 其他 `/api/*` 路由均返回 404。
|
||||
|
||||
站点目录在服务端缓存 2 分钟,并在服务启动后后台预热;因此正常页面打开直接返回缓存数据,最多延迟 2 分钟更新一次。
|
||||
站点目录在服务端缓存 2 分钟,并在服务启动后后台预热;因此正常页面打开直接返回缓存数据,最多延迟 2 分钟更新一次。省、市、区由运营地图根据 GCJ-02 坐标调用高德逆地理服务生成,结果按坐标缓存 7 天;详细地址仍直接展示资产原始字段。
|
||||
|
||||
## ECS 部署
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ OPEN_PLATFORM_BASE_URL = os.getenv("OPEN_PLATFORM_BASE_URL", "https://open.d.lno
|
||||
OPEN_PLATFORM_APP_KEY = os.getenv("OPEN_PLATFORM_APP_KEY", "").strip()
|
||||
UPSTREAM_TIMEOUT_SECONDS = float(os.getenv("UPSTREAM_TIMEOUT_SECONDS", "15"))
|
||||
STATION_CACHE_SECONDS = int(os.getenv("STATION_NAVIGATION_CACHE_SECONDS", "120"))
|
||||
VEHICLE_MAP_INTERNAL_BASE_URL = os.getenv("VEHICLE_MAP_INTERNAL_BASE_URL", "http://127.0.0.1:20800").rstrip("/")
|
||||
_cache_lock = threading.Lock()
|
||||
_cache = {}
|
||||
|
||||
@@ -91,27 +92,24 @@ def _cached(key, ttl_seconds, loader):
|
||||
return value
|
||||
|
||||
|
||||
def _public_station(station):
|
||||
"""Positive allowlist: do not add operational or hydrogen fields here."""
|
||||
fields = (
|
||||
"id", "name", "shortName", "province", "city", "district", "address",
|
||||
"longitude", "latitude", "cooperative",
|
||||
)
|
||||
return {field: station.get(field) for field in fields if station.get(field) is not None}
|
||||
|
||||
|
||||
def _load_station_directory():
|
||||
stations = _post_open_platform("/api/v1/hydrogen-stations/query", {})
|
||||
directory = [_public_station(station) for station in stations]
|
||||
return {
|
||||
"status": "ok",
|
||||
"asOf": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||
"summary": {
|
||||
"totalStations": len(directory),
|
||||
"cooperativeStations": sum(1 for station in directory if station.get("cooperative")),
|
||||
},
|
||||
"stations": directory,
|
||||
}
|
||||
"""Use the operations map's GPS-resolved public station directory.
|
||||
|
||||
The map service owns reverse-geocoding and its persistent cache, keeping
|
||||
the public navigation view and the operations view on identical boundaries.
|
||||
"""
|
||||
request = Request(
|
||||
VEHICLE_MAP_INTERNAL_BASE_URL + "/api/stations",
|
||||
headers={"Accept": "application/json", "User-Agent": "lingniu-station-navigation/1.0"},
|
||||
)
|
||||
try:
|
||||
with urlopen(request, timeout=UPSTREAM_TIMEOUT_SECONDS) as response:
|
||||
payload = json.load(response)
|
||||
except (HTTPError, URLError, ValueError) as exc:
|
||||
raise RuntimeError("vehicle map station directory unavailable: {}".format(exc)) from exc
|
||||
if payload.get("status") != "ok" or not isinstance(payload.get("stations"), list):
|
||||
raise RuntimeError("vehicle map returned an invalid station directory")
|
||||
return payload
|
||||
|
||||
|
||||
def _prewarm_station_directory_cache():
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
@@ -11,21 +13,24 @@ SPEC.loader.exec_module(server)
|
||||
|
||||
class StationNavigationTest(unittest.TestCase):
|
||||
def test_directory_is_a_positive_allowlist_without_hydrogen_data(self):
|
||||
source = [{
|
||||
source = {
|
||||
"status": "ok",
|
||||
"asOf": "2026-08-12 12:00:00",
|
||||
"summary": {"totalStations": 1, "cooperativeStations": 1},
|
||||
"stations": [{
|
||||
"id": "GD-1", "name": "广州合作站", "province": "广东省", "city": "广州市",
|
||||
"district": "黄埔区", "address": "开源大道1号", "longitude": 113.2, "latitude": 23.1,
|
||||
"cooperative": True, "monthlyHydrogenKg": 88.8, "totalHydrogenKg": 999.9,
|
||||
"vehicleCount": 20,
|
||||
}]
|
||||
with patch.object(server, "_post_open_platform", return_value=source):
|
||||
"cooperative": True,
|
||||
}],
|
||||
}
|
||||
with patch.object(server, "urlopen", return_value=io.BytesIO(json.dumps(source).encode("utf-8"))):
|
||||
result = server._load_station_directory()
|
||||
|
||||
self.assertEqual(result["summary"], {"totalStations": 1, "cooperativeStations": 1})
|
||||
station = result["stations"][0]
|
||||
self.assertEqual(station["name"], "广州合作站")
|
||||
self.assertNotIn("monthlyHydrogenKg", station)
|
||||
self.assertNotIn("totalHydrogenKg", station)
|
||||
self.assertNotIn("vehicleCount", station)
|
||||
self.assertEqual(station["province"], "广东省")
|
||||
self.assertEqual(server.VEHICLE_MAP_INTERNAL_BASE_URL, "http://127.0.0.1:20800")
|
||||
|
||||
def test_static_assets_are_fingerprinted(self):
|
||||
self.assertRegex(server.STATIC_ASSET_VERSION, r"^[0-9a-f]{16}$")
|
||||
|
||||
Reference in New Issue
Block a user