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:
2026-03-31 09:41:09 +00:00
parent b970b78136
commit b25eafc483
12 changed files with 1030 additions and 244 deletions

View File

@@ -1,11 +1,12 @@
"""
Shared FastAPI dependencies.
Supports master API key (env) and database-managed API keys with permission levels.
Includes in-memory cache to avoid DB lookup on every request.
"""
import hashlib
import secrets
from datetime import datetime, timezone
import time
from fastapi import Depends, HTTPException, Security
from fastapi.security import APIKeyHeader
@@ -20,6 +21,10 @@ _api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
# Permission hierarchy: admin > write > read
_PERMISSION_LEVELS = {"read": 1, "write": 2, "admin": 3}
# In-memory auth cache: {key_hash: (result_dict, expire_timestamp)}
_AUTH_CACHE: dict[str, tuple[dict, float]] = {}
_AUTH_CACHE_TTL = 60 # seconds
def _hash_key(key: str) -> str:
"""SHA-256 hash of an API key."""
@@ -32,7 +37,7 @@ async def verify_api_key(
) -> dict | None:
"""Verify API key. Returns key info dict or None (auth disabled).
Checks master key first, then database keys.
Checks master key first, then in-memory cache, then database keys.
Returns {"permissions": "admin"|"write"|"read", "key_id": int|None, "name": str}.
"""
if settings.API_KEY is None:
@@ -45,23 +50,34 @@ async def verify_api_key(
if secrets.compare_digest(api_key, settings.API_KEY):
return {"permissions": "admin", "key_id": None, "name": "master"}
# Check in-memory cache first
key_hash = _hash_key(api_key)
now = time.monotonic()
cached = _AUTH_CACHE.get(key_hash)
if cached is not None:
result, expires = cached
if now < expires:
return result
# Check database keys
from app.models import ApiKey
key_hash = _hash_key(api_key)
result = await db.execute(
select(ApiKey).where(ApiKey.key_hash == key_hash, ApiKey.is_active == True) # noqa: E712
)
db_key = result.scalar_one_or_none()
if db_key is None:
_AUTH_CACHE.pop(key_hash, None)
raise HTTPException(status_code=401, detail="Invalid API key / 无效的 API Key")
# Update last_used_at
# Update last_used_at (deferred — only on cache miss, not every request)
from app.config import now_cst
db_key.last_used_at = now_cst()
await db.flush()
return {"permissions": db_key.permissions, "key_id": db_key.id, "name": db_key.name}
key_info = {"permissions": db_key.permissions, "key_id": db_key.id, "name": db_key.name}
_AUTH_CACHE[key_hash] = (key_info, now + _AUTH_CACHE_TTL)
return key_info
def require_permission(min_level: str):