"""设备管理 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": "更新成功"}