Files
H3ConuMS-v2/backend/app/api/v1/devices.py
T
2026-04-02 23:40:04 +08:00

232 lines
8.0 KiB
Python

"""设备管理 API"""
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import asc, desc, distinct, or_
from pydantic import BaseModel
from typing import Optional
from app.core.database import get_db
from app.models.device import ONUDevice, DeviceStatusHistory, OLTDevice
from app.schemas.device import DeviceListResponse, ONUDeviceResponse
router = APIRouter(prefix="/api/devices", tags=["设备管理"])
@router.get("", response_model=DeviceListResponse)
def get_devices(
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
region: str = None,
school_name: str = None,
keyword: str = None,
status: str = None, # online / offline
db: Session = Depends(get_db)
):
"""获取设备列表"""
# 子查询:每台设备最新一条状态记录
from sqlalchemy import func
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_history = (
db.query(DeviceStatusHistory)
.join(
latest_subq,
(DeviceStatusHistory.onu_device_id == latest_subq.c.onu_device_id) &
(DeviceStatusHistory.checked_at == latest_subq.c.max_checked_at)
)
.subquery()
)
query = db.query(ONUDevice)
if region:
query = query.filter(ONUDevice.region == region)
if school_name:
query = query.filter(ONUDevice.school_name.contains(school_name))
if keyword:
query = query.filter(
or_(
ONUDevice.mac_address.contains(keyword.lower()),
ONUDevice.building.contains(keyword),
ONUDevice.place_type.contains(keyword),
ONUDevice.school_name.contains(keyword),
ONUDevice.region.contains(keyword),
ONUDevice.room_number.contains(keyword),
)
)
if status in ("online", "offline", "unknown"):
if status in ("online", "offline"):
query = query.join(
latest_history,
ONUDevice.id == latest_history.c.onu_device_id
).filter(latest_history.c.status == status)
else:
# unknown:最新状态为 unknown,或没有任何状态记录
query = query.outerjoin(
latest_history,
ONUDevice.id == latest_history.c.onu_device_id
).filter(
or_(
latest_history.c.onu_device_id == None,
latest_history.c.status == "unknown"
)
)
# 多级排序:区域 > 学校名称 > 楼宇 > 房间号(均为升序)
query = query.order_by(
asc(ONUDevice.region),
asc(ONUDevice.school_name),
asc(ONUDevice.building),
asc(ONUDevice.room_number)
)
total = query.count()
items = query.offset(skip).limit(limit).all()
# 获取每个设备最新的状态(批量,避免 N+1)
device_ids = [item.id for item in items]
history_map = {}
if device_ids:
histories = (
db.query(DeviceStatusHistory)
.join(
latest_subq,
(DeviceStatusHistory.onu_device_id == latest_subq.c.onu_device_id) &
(DeviceStatusHistory.checked_at == latest_subq.c.max_checked_at)
)
.filter(DeviceStatusHistory.onu_device_id.in_(device_ids))
.all()
)
history_map = {h.onu_device_id: h for h in histories}
# 批量加载 OLT 信息
olt_ids = {item.olt_id for item in items if item.olt_id}
olt_map = {}
if olt_ids:
olts = db.query(OLTDevice).filter(OLTDevice.id.in_(olt_ids)).all()
olt_map = {o.id: o for o in olts}
result_items = []
for item in items:
latest_status = history_map.get(item.id)
olt = olt_map.get(item.olt_id)
item_dict = {
"id": item.id,
"mac_address": item.mac_address,
"olt_id": item.olt_id,
"region": item.region,
"school_name": item.school_name,
"building": item.building,
"place_type": item.place_type,
"room_number": item.room_number,
"notes": item.notes,
"status": latest_status.status if latest_status else None,
"distance_m": latest_status.distance_m if latest_status else None,
"slot_number": item.slot_number,
"port_number": item.port_number,
"port_id": item.port_id,
"model": item.model,
"olt_location": olt.location if olt else None,
"created_at": item.created_at
}
result_items.append(ONUDeviceResponse(**item_dict))
return {"total": total, "items": result_items}
@router.get("/regions")
def get_regions(db: Session = Depends(get_db)):
"""获取所有区域列表"""
regions = db.query(distinct(ONUDevice.region)).filter(
ONUDevice.region.isnot(None),
ONUDevice.region != ''
).order_by(ONUDevice.region).all()
return [r[0] for r in regions]
@router.get("/{device_id}", response_model=ONUDeviceResponse)
def get_device(device_id: int, db: Session = Depends(get_db)):
"""获取设备详情"""
device = db.query(ONUDevice).filter(ONUDevice.id == device_id).first()
if not device:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="设备不存在")
latest_status = db.query(DeviceStatusHistory).filter(
DeviceStatusHistory.onu_device_id == device.id
).order_by(desc(DeviceStatusHistory.checked_at)).first()
olt = db.query(OLTDevice).filter(OLTDevice.id == device.olt_id).first() if device.olt_id else None
return ONUDeviceResponse(
id=device.id,
mac_address=device.mac_address,
olt_id=device.olt_id,
region=device.region,
school_name=device.school_name,
building=device.building,
place_type=device.place_type,
room_number=device.room_number,
notes=device.notes,
status=latest_status.status if latest_status else None,
distance_m=latest_status.distance_m if latest_status else None,
slot_number=device.slot_number,
port_number=device.port_number,
port_id=device.port_id,
model=device.model,
olt_location=olt.location if olt else None,
created_at=device.created_at
)
@router.post("/{device_id}/refresh")
def refresh_device_status(device_id: int, db: Session = Depends(get_db)):
"""通过 SSH 单独更新一台设备的状态和距离"""
from app.services.check_service import CheckService
try:
service = CheckService(db)
result = service.check_single_device(device_id)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
class DeviceUpdate(BaseModel):
region: Optional[str] = None
school_name: Optional[str] = None
building: Optional[str] = None
room_number: Optional[str] = None
notes: Optional[str] = None
@router.delete("/status/all")
def clear_all_status(db: Session = Depends(get_db)):
"""清空所有设备状态历史记录"""
db.query(DeviceStatusHistory).delete()
db.commit()
return {"message": "已清空所有设备状态"}
@router.put("/{device_id}")
def update_device(device_id: int, body: DeviceUpdate, db: Session = Depends(get_db)):
"""更新设备信息(区域、学校、楼宇、房间号、备注)"""
device = db.query(ONUDevice).filter(ONUDevice.id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
device.region = body.region
device.school_name = body.school_name
device.building = body.building or None
device.room_number = body.room_number or None
device.notes = body.notes or None
db.commit()
return {"message": "更新成功"}