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

@@ -89,9 +89,14 @@ async def lifespan(app: FastAPI):
for stmt in [
"CREATE INDEX IF NOT EXISTS ix_alarm_type ON alarm_records(alarm_type)",
"CREATE INDEX IF NOT EXISTS ix_alarm_ack ON alarm_records(acknowledged)",
"CREATE INDEX IF NOT EXISTS ix_alarm_source ON alarm_records(alarm_source)",
"CREATE INDEX IF NOT EXISTS ix_bt_beacon_mac ON bluetooth_records(beacon_mac)",
"CREATE INDEX IF NOT EXISTS ix_loc_type ON location_records(location_type)",
"CREATE INDEX IF NOT EXISTS ix_att_type ON attendance_records(attendance_type)",
"CREATE INDEX IF NOT EXISTS ix_att_source ON attendance_records(attendance_source)",
"CREATE INDEX IF NOT EXISTS ix_att_device_type_time ON attendance_records(device_id, attendance_type, recorded_at)",
"CREATE INDEX IF NOT EXISTS ix_device_status ON devices(status)",
"CREATE INDEX IF NOT EXISTS ix_fence_active ON fence_configs(is_active)",
]:
await conn.execute(sa_text(stmt))
logger.info("Database indexes verified/created")
@@ -107,6 +112,9 @@ async def lifespan(app: FastAPI):
logger.info("Shutting down TCP server...")
await tcp_manager.stop()
tcp_task.cancel()
# Close shared HTTP session
from app.geocoding import close_http_session
await close_http_session()
app = FastAPI(
title="KKS Badge Management System / KKS工牌管理系统",
@@ -186,12 +194,14 @@ app.include_router(geocoding.router, dependencies=[*_api_deps])
_STATIC_DIR = Path(__file__).parent / "static"
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
# Cache admin.html in memory at startup (avoid disk read per request)
_admin_html_cache: str = (_STATIC_DIR / "admin.html").read_text(encoding="utf-8")
@app.get("/admin", response_class=HTMLResponse, tags=["Admin"])
async def admin_page():
"""管理后台页面 / Admin Dashboard"""
html_path = _STATIC_DIR / "admin.html"
return HTMLResponse(content=html_path.read_text(encoding="utf-8"))
return HTMLResponse(content=_admin_html_cache)
@app.get("/", tags=["Root"])