feat(alerts): 添加微信告警、WebSocket实时推送和地图功能
- 新增微信告警服务(wechat_service)和告警任务(alert_tasks) - 新增 WebSocket 实时推送端点 - 新增监控管理模块(monitor) - 增强统计仪表板:趋势图、区域分布、光功率历史 - 设备管理:添加坐标信息、标签系统、复合索引优化 - 前端:重构Dashboard/Charts页面,新增业务组件 - 新增5个数据库迁移(坐标、复合索引、标签、光功率历史、显示名) - 更新部署配置和脚本 - 新增测试框架基础结构
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
"""设备管理 API"""
|
||||
import csv
|
||||
import io
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import asc, desc, distinct, or_
|
||||
from pydantic import BaseModel
|
||||
@@ -20,6 +23,7 @@ def get_devices(
|
||||
school_name: str = None,
|
||||
keyword: str = None,
|
||||
status: str = None,
|
||||
tag: str = None,
|
||||
db: Session = Depends(get_db),
|
||||
current: dict = Depends(require_permission('device.view')),
|
||||
):
|
||||
@@ -68,6 +72,8 @@ def get_devices(
|
||||
query = query.filter(ONUDevice.region == region)
|
||||
if school_name:
|
||||
query = query.filter(ONUDevice.school_name.contains(school_name))
|
||||
if tag:
|
||||
query = query.filter(ONUDevice.tags.contains(tag))
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
or_(
|
||||
@@ -395,6 +401,7 @@ class DeviceUpdate(BaseModel):
|
||||
room_number: Optional[str] = None
|
||||
place_type: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
|
||||
|
||||
class DeviceReplaceRequest(BaseModel):
|
||||
@@ -430,6 +437,7 @@ def update_device(
|
||||
device.room_number = body.room_number or None
|
||||
device.place_type = body.place_type or None
|
||||
device.notes = body.notes or None
|
||||
device.tags = body.tags or None
|
||||
|
||||
# 若该设备 MAC 在 new_devices 待入库列表中,自动移除(已在设备列表中补全信息)
|
||||
from app.models.device import NewDevice
|
||||
@@ -613,6 +621,18 @@ def get_device_optical_power(
|
||||
data = IMCService().get_optical_power(device.mac_address)
|
||||
if data is None:
|
||||
raise HTTPException(status_code=502, detail="获取光功率失败,iMC 接口无响应")
|
||||
# 记录光功率历史
|
||||
try:
|
||||
from app.models.device import OpticalPowerHistory
|
||||
db.add(OpticalPowerHistory(
|
||||
onu_device_id=device_id,
|
||||
power_in=data.get("powerIn"),
|
||||
power_out=data.get("powerOut"),
|
||||
))
|
||||
db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return OpticalPowerResponse(
|
||||
power_in=data.get("powerIn"),
|
||||
power_out=data.get("powerOut"),
|
||||
@@ -661,3 +681,75 @@ def get_onu_events(
|
||||
raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
|
||||
@router.get("/{device_id}/optical-power-history")
|
||||
def get_optical_power_history(
|
||||
device_id: int,
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
_: dict = Depends(require_permission('device.view')),
|
||||
):
|
||||
"""获取设备光功率历史记录"""
|
||||
from app.models.device import OpticalPowerHistory
|
||||
rows = (
|
||||
db.query(OpticalPowerHistory)
|
||||
.filter(OpticalPowerHistory.onu_device_id == device_id)
|
||||
.order_by(OpticalPowerHistory.recorded_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{"power_in": r.power_in, "power_out": r.power_out,
|
||||
"recorded_at": r.recorded_at.isoformat() if r.recorded_at else None}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get("/tags")
|
||||
def get_all_tags(db: Session = Depends(get_db)):
|
||||
"""获取所有不重复的设备标签"""
|
||||
from sqlalchemy import func as _func
|
||||
rows = db.query(ONUDevice.tags).filter(
|
||||
ONUDevice.tags.isnot(None), ONUDevice.tags != ''
|
||||
).all()
|
||||
tags = set()
|
||||
for (tag_str,) in rows:
|
||||
for t in tag_str.split(','):
|
||||
t = t.strip()
|
||||
if t:
|
||||
tags.add(t)
|
||||
return sorted(tags)
|
||||
|
||||
|
||||
@router.get("/export/csv")
|
||||
def export_devices_csv(
|
||||
region: Optional[str] = Query(None),
|
||||
school_name: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
_: dict = Depends(require_permission('device.view')),
|
||||
):
|
||||
"""导出设备列表为 CSV"""
|
||||
from sqlalchemy import func as _func
|
||||
|
||||
query = db.query(ONUDevice)
|
||||
if region:
|
||||
query = query.filter(ONUDevice.region == region)
|
||||
if school_name:
|
||||
query = query.filter(ONUDevice.school_name == school_name)
|
||||
devices = query.order_by(ONUDevice.region, ONUDevice.school_name).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["MAC地址", "区域", "学校", "楼宇", "场所类型", "房间号", "端口", "型号", "LOID", "距离(m)", "备注"])
|
||||
for d in devices:
|
||||
writer.writerow([d.mac_address, d.region or "", d.school_name or "", d.building or "",
|
||||
d.place_type or "", d.room_number or "", d.port_id or "", d.model or "",
|
||||
d.loid or "", d.distance_m or "", d.notes or ""])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=onu_devices.csv"}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user