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:
@@ -289,12 +289,10 @@ async def batch_delete_devices(
|
||||
|
||||
async def get_device_stats(db: AsyncSession) -> dict:
|
||||
"""
|
||||
获取设备统计信息 / Get device statistics.
|
||||
获取设备统计信息(增强版)/ Get enhanced device statistics.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
{"total": int, "online": int, "offline": int}
|
||||
Returns dict with total/online/offline counts, online_rate,
|
||||
by_type breakdown, battery/signal distribution.
|
||||
"""
|
||||
total_result = await db.execute(select(func.count(Device.id)))
|
||||
total = total_result.scalar() or 0
|
||||
@@ -303,11 +301,57 @@ async def get_device_stats(db: AsyncSession) -> dict:
|
||||
select(func.count(Device.id)).where(Device.status == "online")
|
||||
)
|
||||
online = online_result.scalar() or 0
|
||||
|
||||
offline = total - online
|
||||
|
||||
# By device_type
|
||||
type_result = await db.execute(
|
||||
select(Device.device_type, func.count(Device.id))
|
||||
.group_by(Device.device_type)
|
||||
.order_by(func.count(Device.id).desc())
|
||||
)
|
||||
by_type = {row[0] or "unknown": row[1] for row in type_result.all()}
|
||||
|
||||
# Battery distribution: 0-20, 20-50, 50-100, unknown
|
||||
from sqlalchemy import case
|
||||
battery_result = await db.execute(
|
||||
select(
|
||||
func.sum(case((Device.battery_level < 20, 1), else_=0)).label("low"),
|
||||
func.sum(case(((Device.battery_level >= 20) & (Device.battery_level < 50), 1), else_=0)).label("medium"),
|
||||
func.sum(case((Device.battery_level >= 50, 1), else_=0)).label("high"),
|
||||
func.sum(case((Device.battery_level.is_(None), 1), else_=0)).label("unknown"),
|
||||
)
|
||||
)
|
||||
brow = battery_result.one()
|
||||
battery_distribution = {
|
||||
"low_0_20": int(brow.low or 0),
|
||||
"medium_20_50": int(brow.medium or 0),
|
||||
"high_50_100": int(brow.high or 0),
|
||||
"unknown": int(brow.unknown or 0),
|
||||
}
|
||||
|
||||
# Signal distribution: 0=none, 1-10=weak, 11-20=medium, 21-31=strong
|
||||
signal_result = await db.execute(
|
||||
select(
|
||||
func.sum(case(((Device.gsm_signal.is_not(None)) & (Device.gsm_signal <= 10), 1), else_=0)).label("weak"),
|
||||
func.sum(case(((Device.gsm_signal > 10) & (Device.gsm_signal <= 20), 1), else_=0)).label("medium"),
|
||||
func.sum(case((Device.gsm_signal > 20, 1), else_=0)).label("strong"),
|
||||
func.sum(case((Device.gsm_signal.is_(None), 1), else_=0)).label("unknown"),
|
||||
)
|
||||
)
|
||||
srow = signal_result.one()
|
||||
signal_distribution = {
|
||||
"weak_0_10": int(srow.weak or 0),
|
||||
"medium_11_20": int(srow.medium or 0),
|
||||
"strong_21_31": int(srow.strong or 0),
|
||||
"unknown": int(srow.unknown or 0),
|
||||
}
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"online": online,
|
||||
"offline": offline,
|
||||
"online_rate": round(online / total * 100, 1) if total else 0,
|
||||
"by_type": by_type,
|
||||
"battery_distribution": battery_distribution,
|
||||
"signal_distribution": signal_distribution,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user