232 lines
8.0 KiB
Python
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": "更新成功"}
|
|
|
|
|