Files
PingWatch/backend/app/api/stats.py
T
v6ole 848f804169 PingWatch 网络设备离线监控系统
- FastAPI 后端 + Vue 3 前端
- Docker Compose 一键部署
- Casdoor OAuth 认证集成
- LogHive 集中式日志
- 设备批量 CSV 导入/导出
- WebSocket 实时状态推送
- 企业微信告警通知
- fping 高性能并发 Ping 检测

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:02:04 +08:00

184 lines
6.4 KiB
Python

"""统计 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