Files
H3ConuMS-v2/backend/app/api/v1/stats.py
T
v6ole f1f8518985 ```
feat(auth): 添加用户权限获取接口并完善JWT令牌角色信息

- 在JWT令牌中添加用户角色信息
- 新增get_my_permissions接口用于获取当前用户权限码列表
- 重构认证回调逻辑,增加错误日志记录
- 更新用户信息获取接口使用Authorization头验证
```
2026-04-06 00:40:08 +08:00

249 lines
8.8 KiB
Python

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