feat(alerts): 添加微信告警、WebSocket实时推送和地图功能
- 新增微信告警服务(wechat_service)和告警任务(alert_tasks) - 新增 WebSocket 实时推送端点 - 新增监控管理模块(monitor) - 增强统计仪表板:趋势图、区域分布、光功率历史 - 设备管理:添加坐标信息、标签系统、复合索引优化 - 前端:重构Dashboard/Charts页面,新增业务组件 - 新增5个数据库迁移(坐标、复合索引、标签、光功率历史、显示名) - 更新部署配置和脚本 - 新增测试框架基础结构
This commit is contained in:
+177
-7
@@ -179,16 +179,14 @@ def get_trend(
|
||||
)
|
||||
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 = (
|
||||
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()
|
||||
)
|
||||
@@ -198,9 +196,9 @@ def get_trend(
|
||||
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)
|
||||
latest_subq,
|
||||
(DeviceStatusHistory.onu_device_id == latest_subq.c.onu_device_id) &
|
||||
(DeviceStatusHistory.checked_at == latest_subq.c.max_checked_at)
|
||||
)
|
||||
.one()
|
||||
)
|
||||
@@ -246,3 +244,175 @@ def get_trend(
|
||||
|
||||
return result
|
||||
|
||||
@router.get("/olt-stats")
|
||||
def get_olt_stats(
|
||||
db: Session = Depends(get_db),
|
||||
_: dict = Depends(require_permission('device.view')),
|
||||
):
|
||||
"""获取每台 OLT 下的设备在线率统计"""
|
||||
from app.models.device import OLTDevice
|
||||
|
||||
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(
|
||||
OLTDevice.id,
|
||||
OLTDevice.ip_address,
|
||||
OLTDevice.location,
|
||||
OLTDevice.region,
|
||||
func.count(ONUDevice.id).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(ONUDevice, ONUDevice.olt_id == OLTDevice.id)
|
||||
.outerjoin(latest_status_subq, ONUDevice.id == latest_status_subq.c.onu_device_id)
|
||||
.group_by(OLTDevice.id)
|
||||
.order_by(OLTDevice.region, OLTDevice.location)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"olt_id": r.id,
|
||||
"name": r.location or r.ip_address,
|
||||
"ip": r.ip_address,
|
||||
"region": r.region or "未知",
|
||||
"total": int(r.total or 0),
|
||||
"online": int(r.online or 0),
|
||||
"offline": int(r.offline or 0),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get("/offline-schools")
|
||||
def get_offline_schools(
|
||||
db: Session = Depends(get_db),
|
||||
_: dict = Depends(require_permission('device.view')),
|
||||
):
|
||||
"""获取全部离线的学校列表"""
|
||||
_subq = (
|
||||
db.query(DeviceStatusHistory.onu_device_id,
|
||||
func.max(DeviceStatusHistory.checked_at).label("max_checked_at"))
|
||||
.group_by(DeviceStatusHistory.onu_device_id).subquery()
|
||||
)
|
||||
_status_subq = (
|
||||
db.query(DeviceStatusHistory.onu_device_id, DeviceStatusHistory.status)
|
||||
.join(_subq,
|
||||
(DeviceStatusHistory.onu_device_id == _subq.c.onu_device_id) &
|
||||
(DeviceStatusHistory.checked_at == _subq.c.max_checked_at)).subquery()
|
||||
)
|
||||
rows = (
|
||||
db.query(
|
||||
ONUDevice.school_name, ONUDevice.region,
|
||||
func.count().label("total"),
|
||||
func.sum(case((_status_subq.c.status == 'online', 1), else_=0)).label("online"),
|
||||
)
|
||||
.outerjoin(_status_subq, ONUDevice.id == _status_subq.c.onu_device_id)
|
||||
.group_by(ONUDevice.school_name, ONUDevice.region)
|
||||
.having(func.sum(case((_status_subq.c.status == 'online', 1), else_=0)) == 0)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{"school_name": r.school_name or "未知", "region": r.region or "未知", "total": int(r.total or 0)}
|
||||
for r in rows if int(r.total or 0) > 0
|
||||
]
|
||||
|
||||
|
||||
@router.get("/model-distribution")
|
||||
def get_model_distribution(
|
||||
db: Session = Depends(get_db),
|
||||
_: dict = Depends(require_permission('device.view')),
|
||||
):
|
||||
"""统计 ONU 设备型号分布"""
|
||||
rows = (
|
||||
db.query(
|
||||
ONUDevice.model,
|
||||
func.count(ONUDevice.id).label("count"),
|
||||
)
|
||||
.filter(ONUDevice.model.isnot(None), ONUDevice.model != '')
|
||||
.group_by(ONUDevice.model)
|
||||
.order_by(func.count(ONUDevice.id).desc())
|
||||
.all()
|
||||
)
|
||||
unknown = db.query(func.count(ONUDevice.id)).filter(
|
||||
(ONUDevice.model.is_(None)) | (ONUDevice.model == '')
|
||||
).scalar() or 0
|
||||
|
||||
result = [{"model": r.model or "未知", "count": r.count} for r in rows]
|
||||
if unknown > 0:
|
||||
result.append({"model": "未知型号", "count": unknown})
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/school-locations")
|
||||
def get_school_locations(
|
||||
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.school_name,
|
||||
ONUDevice.region,
|
||||
ONUDevice.latitude,
|
||||
ONUDevice.longitude,
|
||||
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)
|
||||
.filter(ONUDevice.latitude.isnot(None))
|
||||
.filter(ONUDevice.longitude.isnot(None))
|
||||
.group_by(ONUDevice.school_name, ONUDevice.region, ONUDevice.latitude, ONUDevice.longitude)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"school_name": row.school_name or "未知",
|
||||
"region": row.region or "未知",
|
||||
"latitude": row.latitude,
|
||||
"longitude": row.longitude,
|
||||
"total": int(row.total or 0),
|
||||
"online": int(row.online or 0),
|
||||
"offline": int(row.offline or 0),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user