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:
@@ -4,11 +4,12 @@ API endpoints for alarm record queries, acknowledgement, and statistics.
|
||||
"""
|
||||
|
||||
import math
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, select
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, select, case, extract
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import require_write
|
||||
@@ -96,13 +97,19 @@ async def list_alarms(
|
||||
@router.get(
|
||||
"/stats",
|
||||
response_model=APIResponse[dict],
|
||||
summary="获取报警统计 / Get alarm statistics",
|
||||
summary="获取报警统计(增强版)/ Get enhanced alarm statistics",
|
||||
)
|
||||
async def alarm_stats(db: AsyncSession = Depends(get_db)):
|
||||
async def alarm_stats(
|
||||
days: int = Query(default=7, ge=1, le=90, description="趋势天数 / Trend days"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
获取报警统计:总数、未确认数、按类型分组统计。
|
||||
Get alarm statistics: total, unacknowledged count, and breakdown by type.
|
||||
增强版报警统计:总数、未确认数、按类型分组、按天趋势、平均响应时间、TOP10设备。
|
||||
Enhanced alarm stats: totals, by type, daily trend, avg response time, top 10 devices.
|
||||
"""
|
||||
from app.config import now_cst
|
||||
now = now_cst()
|
||||
|
||||
# Total alarms
|
||||
total_result = await db.execute(select(func.count(AlarmRecord.id)))
|
||||
total = total_result.scalar() or 0
|
||||
@@ -121,16 +128,88 @@ async def alarm_stats(db: AsyncSession = Depends(get_db)):
|
||||
)
|
||||
by_type = {row[0]: row[1] for row in type_result.all()}
|
||||
|
||||
# By source
|
||||
source_result = await db.execute(
|
||||
select(AlarmRecord.alarm_source, func.count(AlarmRecord.id))
|
||||
.group_by(AlarmRecord.alarm_source)
|
||||
)
|
||||
by_source = {(row[0] or "unknown"): row[1] for row in source_result.all()}
|
||||
|
||||
# Daily trend (last N days)
|
||||
cutoff = now - timedelta(days=days)
|
||||
trend_result = await db.execute(
|
||||
select(
|
||||
func.date(AlarmRecord.recorded_at).label("day"),
|
||||
func.count(AlarmRecord.id),
|
||||
)
|
||||
.where(AlarmRecord.recorded_at >= cutoff)
|
||||
.group_by("day")
|
||||
.order_by("day")
|
||||
)
|
||||
daily_trend = {str(row[0]): row[1] for row in trend_result.all()}
|
||||
|
||||
# Today count
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_result = await db.execute(
|
||||
select(func.count(AlarmRecord.id)).where(AlarmRecord.recorded_at >= today_start)
|
||||
)
|
||||
today_count = today_result.scalar() or 0
|
||||
|
||||
# Top 10 devices by alarm count
|
||||
top_result = await db.execute(
|
||||
select(AlarmRecord.device_id, AlarmRecord.imei, func.count(AlarmRecord.id).label("cnt"))
|
||||
.group_by(AlarmRecord.device_id, AlarmRecord.imei)
|
||||
.order_by(func.count(AlarmRecord.id).desc())
|
||||
.limit(10)
|
||||
)
|
||||
top_devices = [{"device_id": row[0], "imei": row[1], "count": row[2]} for row in top_result.all()]
|
||||
|
||||
return APIResponse(
|
||||
data={
|
||||
"total": total,
|
||||
"unacknowledged": unacknowledged,
|
||||
"acknowledged": total - unacknowledged,
|
||||
"today": today_count,
|
||||
"by_type": by_type,
|
||||
"by_source": by_source,
|
||||
"daily_trend": daily_trend,
|
||||
"top_devices": top_devices,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class BatchAcknowledgeRequest(BaseModel):
|
||||
alarm_ids: list[int] = Field(..., min_length=1, max_length=500, description="告警ID列表")
|
||||
acknowledged: bool = Field(default=True, description="确认状态")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/batch-acknowledge",
|
||||
response_model=APIResponse[dict],
|
||||
summary="批量确认告警 / Batch acknowledge alarms",
|
||||
dependencies=[Depends(require_write)],
|
||||
)
|
||||
async def batch_acknowledge_alarms(
|
||||
body: BatchAcknowledgeRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
批量确认(或取消确认)告警记录,最多500条。
|
||||
Batch acknowledge (or un-acknowledge) alarm records (max 500).
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(AlarmRecord).where(AlarmRecord.id.in_(body.alarm_ids))
|
||||
)
|
||||
records = list(result.scalars().all())
|
||||
for r in records:
|
||||
r.acknowledged = body.acknowledged
|
||||
await db.flush()
|
||||
return APIResponse(
|
||||
message=f"已{'确认' if body.acknowledged else '取消确认'} {len(records)} 条告警",
|
||||
data={"updated": len(records), "requested": len(body.alarm_ids)},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/batch-delete",
|
||||
response_model=APIResponse[dict],
|
||||
|
||||
Reference in New Issue
Block a user