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

@@ -234,6 +234,19 @@ async def check_device_fences(
"cell_id": cell_id,
}
# 2. Batch-load all fence states in one query (avoid N+1)
fence_ids = [f.id for f in fences]
states_result = await session.execute(
select(DeviceFenceState).where(
DeviceFenceState.device_id == device_id,
DeviceFenceState.fence_id.in_(fence_ids),
)
)
states_map: dict[int, DeviceFenceState] = {s.fence_id: s for s in states_result.scalars().all()}
# Pre-check today's attendance dedup once (not per-fence)
_today_clock_in = await _has_attendance_today(session, device_id, "clock_in")
tolerance = _get_tolerance_for_location_type(location_type)
events: list[dict] = []
now = now_cst()
@@ -242,14 +255,7 @@ async def check_device_fences(
for fence in fences:
currently_inside = is_inside_fence(latitude, longitude, fence, tolerance)
# 2. Get or create state record
state_result = await session.execute(
select(DeviceFenceState).where(
DeviceFenceState.device_id == device_id,
DeviceFenceState.fence_id == fence.id,
)
)
state = state_result.scalar_one_or_none()
state = states_map.get(fence.id)
was_inside = bool(state and state.is_inside)
@@ -266,8 +272,8 @@ async def check_device_fences(
_update_state(state, currently_inside, now, latitude, longitude)
continue
# Daily dedup: only one clock_in per device per day
if await _has_attendance_today(session, device_id, "clock_in"):
# Daily dedup: only one clock_in per device per day (pre-fetched)
if _today_clock_in:
logger.info(
"Fence skip clock_in: device=%d fence=%d(%s) already clocked in today",
device_id, fence.id, fence.name,