848f804169
- FastAPI 后端 + Vue 3 前端 - Docker Compose 一键部署 - Casdoor OAuth 认证集成 - LogHive 集中式日志 - 设备批量 CSV 导入/导出 - WebSocket 实时状态推送 - 企业微信告警通知 - fping 高性能并发 Ping 检测 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
184 lines
6.4 KiB
Python
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
|