feat: 13个统计/聚合API + 前端同步 + 待完成功能文档

API新增:
- GET /api/system/overview 系统总览(在线率/今日统计/表大小)
- GET /api/locations/stats 位置统计(类型分布/小时趋势)
- GET /api/locations/track-summary/{id} 轨迹摘要(距离/时长/速度)
- POST /api/alarms/batch-acknowledge 批量确认告警
- GET /api/attendance/report 考勤日报表(每设备每天汇总)
- GET /api/bluetooth/stats 蓝牙统计(类型/TOP信标/RSSI分布)
- GET /api/heartbeats/stats 心跳统计(活跃设备/电量/间隔分析)
- GET /api/fences/stats 围栏统计(绑定/进出状态/今日事件)
- GET /api/fences/{id}/events 围栏进出事件历史
- GET /api/commands/stats 指令统计(成功率/类型/趋势)

API增强:
- devices/stats: 新增by_type/battery_distribution/signal_distribution
- alarms/stats: 新增today/by_source/daily_trend/top_devices
- attendance/stats: 新增today/by_source/daily_trend/by_device

前端同步:
- 仪表盘: 今日告警/考勤/定位卡片 + 在线率
- 告警页: 批量确认按钮 + 今日计数
- 考勤页: 今日计数
- 轨迹: 加载后显示距离/时长/速度摘要
- 蓝牙/围栏/指令页: 统计面板

文档: CLAUDE.md待完成功能按优先级重新规划

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
2026-03-31 10:11:33 +00:00
parent b25eafc483
commit 8157f9cb52
10 changed files with 1044 additions and 51 deletions

View File

@@ -239,6 +239,93 @@ async def health():
}
@app.get("/api/system/overview", tags=["System / 系统管理"])
async def system_overview():
"""
系统总览:设备/在线率/今日告警/今日考勤/各表记录数/运行状态。
System overview: devices, online rate, today stats, table sizes, uptime.
"""
from sqlalchemy import func, select, text
from app.models import (
Device, LocationRecord, AlarmRecord, AttendanceRecord,
BluetoothRecord, HeartbeatRecord, CommandLog, BeaconConfig, FenceConfig,
)
from app.config import now_cst
from app.websocket_manager import ws_manager
now = now_cst()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
async with async_session() as db:
# Device stats
total_devices = (await db.execute(select(func.count(Device.id)))).scalar() or 0
online_devices = (await db.execute(
select(func.count(Device.id)).where(Device.status == "online")
)).scalar() or 0
# Today counts
today_alarms = (await db.execute(
select(func.count(AlarmRecord.id)).where(AlarmRecord.recorded_at >= today_start)
)).scalar() or 0
today_unack = (await db.execute(
select(func.count(AlarmRecord.id)).where(
AlarmRecord.recorded_at >= today_start,
AlarmRecord.acknowledged == False, # noqa: E712
)
)).scalar() or 0
today_attendance = (await db.execute(
select(func.count(AttendanceRecord.id)).where(AttendanceRecord.recorded_at >= today_start)
)).scalar() or 0
today_locations = (await db.execute(
select(func.count(LocationRecord.id)).where(LocationRecord.recorded_at >= today_start)
)).scalar() or 0
# Table sizes
table_sizes = {}
for name, model in [
("devices", Device), ("locations", LocationRecord),
("alarms", AlarmRecord), ("attendance", AttendanceRecord),
("bluetooth", BluetoothRecord), ("heartbeats", HeartbeatRecord),
("commands", CommandLog), ("beacons", BeaconConfig), ("fences", FenceConfig),
]:
cnt = (await db.execute(select(func.count(model.id)))).scalar() or 0
table_sizes[name] = cnt
# DB file size
db_size_mb = None
try:
import os
db_path = settings.DATABASE_URL.replace("sqlite+aiosqlite:///", "")
if os.path.exists(db_path):
db_size_mb = round(os.path.getsize(db_path) / 1024 / 1024, 2)
except Exception:
pass
return {
"code": 0,
"data": {
"devices": {
"total": total_devices,
"online": online_devices,
"offline": total_devices - online_devices,
"online_rate": round(online_devices / total_devices * 100, 1) if total_devices else 0,
},
"today": {
"alarms": today_alarms,
"unacknowledged_alarms": today_unack,
"attendance": today_attendance,
"locations": today_locations,
},
"table_sizes": table_sizes,
"connections": {
"tcp_devices": len(tcp_manager.connections),
"websocket_clients": ws_manager.connection_count,
},
"database_size_mb": db_size_mb,
},
}
@app.post("/api/system/cleanup", tags=["System / 系统管理"], dependencies=[Depends(require_admin)] if settings.API_KEY else [])
async def manual_cleanup():
"""手动触发数据清理 / Manually trigger data cleanup (admin only)."""