"""统计 API""" from datetime import datetime, timedelta from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy import select, func, and_, case from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db from app.core.auth import get_current_user from app.models.device import Device from app.models.ping_record import PingRecord from app.models.alert_event import AlertEvent, AlertTypeEnum from app.models.user import User from app.schemas.stats import DeviceStatusSummary, DeviceStatsItem, TimeSeriesPoint, DashboardStats router = APIRouter(prefix="/api/stats", tags=["统计"]) @router.get("/summary", response_model=DeviceStatusSummary) async def get_summary( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """获取当前设备状态汇总""" result = await db.execute(select(Device)) devices = list(result.scalars().all()) total = len(devices) online = sum(1 for d in devices if d.current_status == "online") offline = sum(1 for d in devices if d.current_status == "offline") checking = sum(1 for d in devices if d.current_status == "checking") unknown = sum(1 for d in devices if d.current_status == "unknown") online_rate = round(online / max(total, 1) * 100, 2) return DeviceStatusSummary( total=total, online=online, offline=offline, checking=checking, unknown=unknown, online_rate=online_rate, ) @router.get("/offline-trend", response_model=list[TimeSeriesPoint]) async def get_offline_trend( days: int = Query(7, ge=1, le=90), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """获取指定天数内的每日离线次数趋势""" start = datetime.now() - timedelta(days=days) result = await db.execute( select( func.date(AlertEvent.start_at).label("day"), func.count(AlertEvent.id).label("count"), ) .where(AlertEvent.alert_type == AlertTypeEnum.offline) .where(AlertEvent.start_at >= start) .group_by(func.date(AlertEvent.start_at)) .order_by("day") ) rows = result.all() return [TimeSeriesPoint(time=datetime.strptime(r.day, "%Y-%m-%d"), value=r.count) for r in rows] @router.get("/online-rate-trend", response_model=list[TimeSeriesPoint]) async def get_online_rate_trend( days: int = Query(7, ge=1, le=90), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """获取每日在线率趋势""" start = datetime.now() - timedelta(days=days) # 按天统计每轮的存活/总数比例 result = await db.execute( select( func.date(PingRecord.created_at).label("day"), func.round( func.sum(case((PingRecord.is_alive == True, 1), else_=0)) / func.count(PingRecord.id) * 100, 2 ).label("rate"), ) .where(PingRecord.created_at >= start) .group_by(func.date(PingRecord.created_at)) .order_by("day") ) rows = result.all() return [TimeSeriesPoint(time=datetime.strptime(r.day, "%Y-%m-%d"), value=r.rate) for r in rows] @router.get("/packet-loss-top", response_model=list[DeviceStatsItem]) async def get_packet_loss_top( limit: int = Query(10, ge=1, le=50), days: int = Query(7, ge=1, le=90), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """获取丢包率最高的设备排名""" start = datetime.now() - timedelta(days=days) subq = ( select( PingRecord.device_id, func.count(PingRecord.id).label("total_pings"), func.sum(case((PingRecord.is_alive == True, 1), else_=0)).label("alive_pings"), func.avg(PingRecord.response_time_ms).label("avg_rtt"), ) .where(PingRecord.created_at >= start) .group_by(PingRecord.device_id) .subquery() ) result = await db.execute( select( subq.c.device_id, subq.c.total_pings, subq.c.alive_pings, subq.c.avg_rtt, Device.name, Device.ip, Device.device_type, Device.location, Device.project_name, ) .join(Device, subq.c.device_id == Device.id) .order_by( (subq.c.total_pings - subq.c.alive_pings) * 1.0 / subq.c.total_pings ) .limit(limit) ) rows = result.all() items = [] for r in rows: loss_rate = round((r.total_pings - r.alive_pings) / max(r.total_pings, 1) * 100, 2) # 离线次数和总时长 alert_result = await db.execute( select( func.count(AlertEvent.id), func.coalesce(func.sum(AlertEvent.duration_minutes), 0), func.coalesce(func.max(AlertEvent.duration_minutes), 0), ) .where(AlertEvent.device_id == r.device_id) .where(AlertEvent.alert_type == AlertTypeEnum.offline) .where(AlertEvent.start_at >= start) ) cnt, total_dur, max_dur = alert_result.one() items.append(DeviceStatsItem( device_id=r.device_id, device_name=r.name, device_ip=r.ip, device_type=r.device_type.value if hasattr(r.device_type, 'value') else str(r.device_type), location=r.location or "", project_name=r.project_name or "", total_pings=r.total_pings, alive_pings=r.alive_pings, packet_loss_rate=loss_rate, avg_response_time=round(r.avg_rtt, 2) if r.avg_rtt else None, offline_count=cnt, total_offline_duration=total_dur, max_offline_duration=max_dur, )) return items @router.get("/dashboard", response_model=DashboardStats) async def get_dashboard( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """仪表盘聚合数据""" summary = await get_summary(db, current_user) recent = await get_latest_alerts(limit=10, db=db, current_user=current_user) trend = await get_offline_trend(7, db, current_user) top_loss = await get_packet_loss_top(10, 7, db, current_user) return DashboardStats( summary=summary, recent_offline=recent, offline_trend=trend, packet_loss_top=top_loss, ) # 复用上面定义的函数 from app.api.alerts import get_latest_alerts