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:
87
app/main.py
87
app/main.py
@@ -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)."""
|
||||
|
||||
Reference in New Issue
Block a user