feat: 性能优化 + 设备总览轨迹展示 + 广播指令API
性能: SQLite WAL模式、aiohttp Session复用、TCP连接锁+空闲超时、 device_id缓存、WebSocket并发广播、API Key认证缓存、围栏N+1查询 批量化、逆地理编码并行化、新增5个DB索引、日志降级DEBUG 功能: 广播指令API(broadcast)、exclude_type低精度后端过滤、 前端设备总览Tab+多设备轨迹叠加+高亮联动+搜索+专属颜色 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
171
app/geocoding.py
171
app/geocoding.py
@@ -27,6 +27,31 @@ AMAP_HARDWARE_SECRET: Optional[str] = _settings.AMAP_HARDWARE_SECRET
|
||||
_CACHE_MAX_SIZE = _settings.GEOCODING_CACHE_SIZE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared aiohttp session (reused across all geocoding calls)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_http_session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
|
||||
async def _get_http_session() -> aiohttp.ClientSession:
|
||||
"""Get or create the shared aiohttp session (lazy init)."""
|
||||
global _http_session
|
||||
if _http_session is None or _http_session.closed:
|
||||
_http_session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=5),
|
||||
)
|
||||
return _http_session
|
||||
|
||||
|
||||
async def close_http_session() -> None:
|
||||
"""Close the shared session (call on app shutdown)."""
|
||||
global _http_session
|
||||
if _http_session and not _http_session.closed:
|
||||
await _http_session.close()
|
||||
_http_session = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WGS-84 → GCJ-02 coordinate conversion (server-side)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -316,38 +341,36 @@ async def _geocode_amap_v5(
|
||||
|
||||
url = f"https://restapi.amap.com/v5/position/IoT?key={api_key}"
|
||||
|
||||
logger.info("Amap v5 request body: %s", body)
|
||||
logger.debug("Amap v5 request body: %s", body)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
url, data=body, timeout=aiohttp.ClientTimeout(total=5)
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json(content_type=None)
|
||||
logger.info("Amap v5 response: %s", data)
|
||||
if data.get("status") == "1":
|
||||
position = data.get("position", {})
|
||||
location = position.get("location", "") if isinstance(position, dict) else ""
|
||||
if location and "," in location:
|
||||
lon_str, lat_str = location.split(",")
|
||||
gcj_lat = float(lat_str)
|
||||
gcj_lon = float(lon_str)
|
||||
lat, lon = gcj02_to_wgs84(gcj_lat, gcj_lon)
|
||||
radius = position.get("radius", "?") if isinstance(position, dict) else "?"
|
||||
logger.info(
|
||||
"Amap v5 geocode: GCJ-02(%.6f,%.6f) -> WGS-84(%.6f,%.6f) radius=%s",
|
||||
gcj_lat, gcj_lon, lat, lon, radius,
|
||||
)
|
||||
return (lat, lon)
|
||||
else:
|
||||
infocode = data.get("infocode", "")
|
||||
logger.warning(
|
||||
"Amap v5 geocode error: %s (code=%s) body=%s",
|
||||
data.get("info", ""), infocode, body,
|
||||
session = await _get_http_session()
|
||||
async with session.post(url, data=body) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json(content_type=None)
|
||||
logger.debug("Amap v5 response: %s", data)
|
||||
if data.get("status") == "1":
|
||||
position = data.get("position", {})
|
||||
location = position.get("location", "") if isinstance(position, dict) else ""
|
||||
if location and "," in location:
|
||||
lon_str, lat_str = location.split(",")
|
||||
gcj_lat = float(lat_str)
|
||||
gcj_lon = float(lon_str)
|
||||
lat, lon = gcj02_to_wgs84(gcj_lat, gcj_lon)
|
||||
radius = position.get("radius", "?") if isinstance(position, dict) else "?"
|
||||
logger.info(
|
||||
"Amap v5 geocode: GCJ-02(%.6f,%.6f) -> WGS-84(%.6f,%.6f) radius=%s",
|
||||
gcj_lat, gcj_lon, lat, lon, radius,
|
||||
)
|
||||
return (lat, lon)
|
||||
else:
|
||||
logger.warning("Amap v5 geocode HTTP %d", resp.status)
|
||||
infocode = data.get("infocode", "")
|
||||
logger.warning(
|
||||
"Amap v5 geocode error: %s (code=%s) body=%s",
|
||||
data.get("info", ""), infocode, body,
|
||||
)
|
||||
else:
|
||||
logger.warning("Amap v5 geocode HTTP %d", resp.status)
|
||||
except Exception as e:
|
||||
logger.warning("Amap v5 geocode error: %s", e)
|
||||
|
||||
@@ -400,37 +423,35 @@ async def _geocode_amap_legacy(
|
||||
|
||||
url = "https://apilocate.amap.com/position"
|
||||
|
||||
logger.info("Amap legacy request params: %s", {k: v for k, v in params.items() if k != 'key'})
|
||||
logger.debug("Amap legacy request params: %s", {k: v for k, v in params.items() if k != 'key'})
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
url, params=params, timeout=aiohttp.ClientTimeout(total=5)
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json(content_type=None)
|
||||
logger.info("Amap legacy response: %s", data)
|
||||
if data.get("status") == "1" and data.get("result"):
|
||||
result = data["result"]
|
||||
location = result.get("location", "")
|
||||
if location and "," in location:
|
||||
lon_str, lat_str = location.split(",")
|
||||
gcj_lat = float(lat_str)
|
||||
gcj_lon = float(lon_str)
|
||||
lat, lon = gcj02_to_wgs84(gcj_lat, gcj_lon)
|
||||
logger.info(
|
||||
"Amap legacy geocode: GCJ-02(%.6f,%.6f) -> WGS-84(%.6f,%.6f)",
|
||||
gcj_lat, gcj_lon, lat, lon,
|
||||
)
|
||||
return (lat, lon)
|
||||
else:
|
||||
infocode = data.get("infocode", "")
|
||||
if infocode == "10012":
|
||||
logger.debug("Amap legacy geocode: insufficient permissions (enterprise cert needed)")
|
||||
else:
|
||||
logger.warning("Amap legacy geocode error: %s (code=%s)", data.get("info", ""), infocode)
|
||||
session = await _get_http_session()
|
||||
async with session.get(url, params=params) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json(content_type=None)
|
||||
logger.debug("Amap legacy response: %s", data)
|
||||
if data.get("status") == "1" and data.get("result"):
|
||||
result = data["result"]
|
||||
location = result.get("location", "")
|
||||
if location and "," in location:
|
||||
lon_str, lat_str = location.split(",")
|
||||
gcj_lat = float(lat_str)
|
||||
gcj_lon = float(lon_str)
|
||||
lat, lon = gcj02_to_wgs84(gcj_lat, gcj_lon)
|
||||
logger.info(
|
||||
"Amap legacy geocode: GCJ-02(%.6f,%.6f) -> WGS-84(%.6f,%.6f)",
|
||||
gcj_lat, gcj_lon, lat, lon,
|
||||
)
|
||||
return (lat, lon)
|
||||
else:
|
||||
logger.warning("Amap legacy geocode HTTP %d", resp.status)
|
||||
infocode = data.get("infocode", "")
|
||||
if infocode == "10012":
|
||||
logger.debug("Amap legacy geocode: insufficient permissions (enterprise cert needed)")
|
||||
else:
|
||||
logger.warning("Amap legacy geocode error: %s (code=%s)", data.get("info", ""), infocode)
|
||||
else:
|
||||
logger.warning("Amap legacy geocode HTTP %d", resp.status)
|
||||
except Exception as e:
|
||||
logger.warning("Amap legacy geocode error: %s", e)
|
||||
|
||||
@@ -490,28 +511,26 @@ async def _reverse_geocode_amap(
|
||||
url = "https://restapi.amap.com/v3/geocode/regeo"
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
url, params=params, timeout=aiohttp.ClientTimeout(total=5)
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json(content_type=None)
|
||||
if data.get("status") == "1":
|
||||
regeocode = data.get("regeocode", {})
|
||||
formatted = regeocode.get("formatted_address", "")
|
||||
if formatted and formatted != "[]":
|
||||
logger.info(
|
||||
"Amap reverse geocode: %.6f,%.6f -> %s",
|
||||
lat, lon, formatted,
|
||||
)
|
||||
return formatted
|
||||
else:
|
||||
logger.warning(
|
||||
"Amap reverse geocode error: info=%s, infocode=%s",
|
||||
data.get("info", ""), data.get("infocode", ""),
|
||||
session = await _get_http_session()
|
||||
async with session.get(url, params=params) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json(content_type=None)
|
||||
if data.get("status") == "1":
|
||||
regeocode = data.get("regeocode", {})
|
||||
formatted = regeocode.get("formatted_address", "")
|
||||
if formatted and formatted != "[]":
|
||||
logger.info(
|
||||
"Amap reverse geocode: %.6f,%.6f -> %s",
|
||||
lat, lon, formatted,
|
||||
)
|
||||
return formatted
|
||||
else:
|
||||
logger.warning("Amap reverse geocode HTTP %d", resp.status)
|
||||
logger.warning(
|
||||
"Amap reverse geocode error: info=%s, infocode=%s",
|
||||
data.get("info", ""), data.get("infocode", ""),
|
||||
)
|
||||
else:
|
||||
logger.warning("Amap reverse geocode HTTP %d", resp.status)
|
||||
except Exception as e:
|
||||
logger.warning("Amap reverse geocode error: %s", e)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user