"""统计 API""" from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from sqlalchemy import func, case from app.core.database import get_db from app.middleware.permission_middleware import require_permission from app.models.device import ONUDevice, DeviceStatusHistory, DeviceDailySnapshot from datetime import datetime, timedelta, date router = APIRouter(prefix="/api/stats", tags=["统计"]) @router.get("/dashboard") def get_dashboard( db: Session = Depends(get_db), _: dict = Depends(require_permission('device.view')), ): """仪表板统计:总体、城区、城郊、乡镇在线率""" # 每台设备最新状态子查询 latest_subq = ( db.query( DeviceStatusHistory.onu_device_id, func.max(DeviceStatusHistory.checked_at).label("max_checked_at") ) .group_by(DeviceStatusHistory.onu_device_id) .subquery() ) latest_status_subq = ( db.query( DeviceStatusHistory.onu_device_id, DeviceStatusHistory.status ) .join( latest_subq, (DeviceStatusHistory.onu_device_id == latest_subq.c.onu_device_id) & (DeviceStatusHistory.checked_at == latest_subq.c.max_checked_at) ) .subquery() ) rows = ( db.query( ONUDevice.region, ONUDevice.school_name, func.count().label("total"), func.sum(case((latest_status_subq.c.status == 'online', 1), else_=0)).label("online"), func.sum(case((latest_status_subq.c.status == 'offline', 1), else_=0)).label("offline"), ) .outerjoin(latest_status_subq, ONUDevice.id == latest_status_subq.c.onu_device_id) .group_by(ONUDevice.region, ONUDevice.school_name) .order_by(ONUDevice.region, ONUDevice.school_name) .all() ) overall = {"total": 0, "online": 0, "offline": 0} urban_schools, suburban_schools = [], [] rural_towns = {} for row in rows: region = row.region or "" total = int(row.total or 0) online = int(row.online or 0) offline = int(row.offline or 0) overall["total"] += total overall["online"] += online overall["offline"] += offline school_stat = {"name": row.school_name or "未知", "total": total, "online": online, "offline": offline} if region == "城区": urban_schools.append(school_stat) elif region == "城郊": suburban_schools.append(school_stat) else: if region not in rural_towns: rural_towns[region] = {"region": region, "total": 0, "online": 0, "offline": 0, "schools": []} rural_towns[region]["total"] += total rural_towns[region]["online"] += online rural_towns[region]["offline"] += offline rural_towns[region]["schools"].append(school_stat) def sort_by_rate(items): return sorted(items, key=lambda x: x["online"] / x["total"] if x["total"] > 0 else 0) def agg(items): return {"total": sum(s["total"] for s in items), "online": sum(s["online"] for s in items), "offline": sum(s["offline"] for s in items)} rural_list = sort_by_rate(list(rural_towns.values())) return { "overall": overall, "urban": {**agg(urban_schools), "schools": sort_by_rate(urban_schools)}, "suburban": {**agg(suburban_schools), "schools": sort_by_rate(suburban_schools)}, "rural": {**agg(list(rural_towns.values())), "towns": rural_list}, } @router.get("/summary") def get_summary( db: Session = Depends(get_db), _: dict = Depends(require_permission('device.view')), ): """获取统计摘要""" total = db.query(ONUDevice).count() latest_status = db.query( DeviceStatusHistory.status, func.count(DeviceStatusHistory.id) ).group_by(DeviceStatusHistory.status).all() status_dict = dict(latest_status) return {"total": total, "online": status_dict.get('online', 0), "offline": status_dict.get('offline', 0)} @router.get("/by-region") def get_by_region( db: Session = Depends(get_db), _: dict = Depends(require_permission('device.view')), ): """各区域设备数量及在线率(用于饼图)""" latest_subq = ( db.query( DeviceStatusHistory.onu_device_id, func.max(DeviceStatusHistory.checked_at).label("max_checked_at") ) .group_by(DeviceStatusHistory.onu_device_id) .subquery() ) latest_status_subq = ( db.query( DeviceStatusHistory.onu_device_id, DeviceStatusHistory.status ) .join( latest_subq, (DeviceStatusHistory.onu_device_id == latest_subq.c.onu_device_id) & (DeviceStatusHistory.checked_at == latest_subq.c.max_checked_at) ) .subquery() ) rows = ( db.query( ONUDevice.region, func.count().label("total"), func.sum(case((latest_status_subq.c.status == 'online', 1), else_=0)).label("online"), func.sum(case((latest_status_subq.c.status == 'offline', 1), else_=0)).label("offline"), ) .outerjoin(latest_status_subq, ONUDevice.id == latest_status_subq.c.onu_device_id) .group_by(ONUDevice.region) .order_by(func.count().desc()) .all() ) return [ { "region": row.region or "未知", "total": int(row.total or 0), "online": int(row.online or 0), "offline": int(row.offline or 0), } for row in rows ] @router.get("/trend") def get_trend( days: int = 7, db: Session = Depends(get_db), _: dict = Depends(require_permission('device.view')), ): """获取状态趋势数据(优先查快照表,不足时实时聚合)""" today = date.today() date_range = [(today - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(days - 1, -1, -1)] # 查快照表(不含今天,今天用实时数据) snapshots = ( db.query(DeviceDailySnapshot) .filter(DeviceDailySnapshot.snapshot_date.in_(date_range[:-1])) .all() ) snapshot_map = {s.snapshot_date: s for s in snapshots} # 今天实时聚合 today_str = today.strftime('%Y-%m-%d') start_of_today = datetime.combine(today, datetime.min.time()) daily_latest_subq = ( db.query( DeviceStatusHistory.onu_device_id, func.max(DeviceStatusHistory.checked_at).label("max_checked_at"), ) .filter(DeviceStatusHistory.checked_at >= start_of_today) .group_by(DeviceStatusHistory.onu_device_id) .subquery() ) today_row = ( db.query( func.sum(case((DeviceStatusHistory.status == 'online', 1), else_=0)).label("online"), func.sum(case((DeviceStatusHistory.status == 'offline', 1), else_=0)).label("offline"), ) .join( daily_latest_subq, (DeviceStatusHistory.onu_device_id == daily_latest_subq.c.onu_device_id) & (DeviceStatusHistory.checked_at == daily_latest_subq.c.max_checked_at) ) .one() ) result = [] for d in date_range: if d == today_str: result.append({ "date": d, "online": int(today_row.online or 0), "offline": int(today_row.offline or 0), }) elif d in snapshot_map: s = snapshot_map[d] result.append({"date": d, "online": s.online, "offline": s.offline}) else: # 快照缺失时实时聚合该天数据 day = datetime.strptime(d, '%Y-%m-%d').date() day_start = datetime.combine(day, datetime.min.time()) day_end = datetime.combine(day, datetime.max.time()) subq = ( db.query( DeviceStatusHistory.onu_device_id, func.max(DeviceStatusHistory.checked_at).label("max_checked_at"), ) .filter(DeviceStatusHistory.checked_at.between(day_start, day_end)) .group_by(DeviceStatusHistory.onu_device_id) .subquery() ) row = ( db.query( func.sum(case((DeviceStatusHistory.status == 'online', 1), else_=0)).label("online"), func.sum(case((DeviceStatusHistory.status == 'offline', 1), else_=0)).label("offline"), ) .join( subq, (DeviceStatusHistory.onu_device_id == subq.c.onu_device_id) & (DeviceStatusHistory.checked_at == subq.c.max_checked_at) ) .one() ) result.append({"date": d, "online": int(row.online or 0), "offline": int(row.offline or 0)}) return result