""" Devices Router - 设备管理接口 API endpoints for device CRUD operations and statistics. """ import math from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import Response from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models import Device from app.services.export_utils import build_csv_content, csv_filename from app.schemas import ( APIResponse, BatchDeviceCreateRequest, BatchDeviceCreateResponse, BatchDeviceCreateResult, BatchDeviceDeleteRequest, BatchDeviceUpdateRequest, DeviceCreate, DeviceResponse, DeviceUpdate, PaginatedList, ) from app.config import settings from app.dependencies import require_write from app.extensions import limiter from app.schemas import LocationRecordResponse from app.services import device_service, location_service router = APIRouter(prefix="/api/devices", tags=["Devices / 设备管理"]) @router.get( "", response_model=APIResponse[PaginatedList[DeviceResponse]], summary="获取设备列表 / List devices", ) async def list_devices( page: int = Query(default=1, ge=1, description="页码 / Page number"), page_size: int = Query(default=20, ge=1, le=100, description="每页数量 / Items per page"), status: str | None = Query(default=None, description="状态过滤 / Status filter (online/offline)"), search: str | None = Query(default=None, description="搜索IMEI或名称 / Search by IMEI or name"), db: AsyncSession = Depends(get_db), ): """ 获取设备列表,支持分页、状态过滤和搜索。 List devices with pagination, optional status filter, and search. """ devices, total = await device_service.get_devices( db, page=page, page_size=page_size, status_filter=status, search=search ) return APIResponse( data=PaginatedList( items=[DeviceResponse.model_validate(d) for d in devices], total=total, page=page, page_size=page_size, total_pages=math.ceil(total / page_size) if total else 0, ) ) @router.get( "/stats", response_model=APIResponse[dict], summary="获取设备统计 / Get device statistics", ) async def device_stats(db: AsyncSession = Depends(get_db)): """ 获取设备统计信息:总数、在线、离线。 Get device statistics: total, online, offline counts. """ stats = await device_service.get_device_stats(db) return APIResponse(data=stats) @router.get( "/export", summary="导出设备列表 CSV / Export devices CSV", ) async def export_devices( status: str | None = Query(default=None, description="状态过滤 (online/offline)"), search: str | None = Query(default=None, description="搜索IMEI或名称"), db: AsyncSession = Depends(get_db), ): """导出设备列表为 CSV 文件,支持状态/搜索筛选。""" from sqlalchemy import or_ query = select(Device) if status: query = query.where(Device.status == status) if search: pattern = f"%{search}%" query = query.where(or_(Device.imei.ilike(pattern), Device.name.ilike(pattern))) query = query.order_by(Device.id) result = await db.execute(query) devices = list(result.scalars().all()) headers = ["ID", "IMEI", "名称", "类型", "状态", "电量%", "信号", "ICCID", "IMSI", "最后心跳", "最后登录", "创建时间"] fields = ["id", "imei", "name", "device_type", "status", "battery_level", "gsm_signal", "iccid", "imsi", "last_heartbeat", "last_login", "created_at"] content = build_csv_content(headers, devices, fields) return Response( content=content, media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f"attachment; filename={csv_filename('devices')}"}, ) @router.get( "/imei/{imei}", response_model=APIResponse[DeviceResponse], summary="按IMEI查询设备 / Get device by IMEI", ) async def get_device_by_imei(imei: str, db: AsyncSession = Depends(get_db)): """ 按IMEI号查询设备信息。 Get device details by IMEI number. """ device = await device_service.get_device_by_imei(db, imei) if device is None: raise HTTPException(status_code=404, detail=f"Device with IMEI {imei} not found / 未找到IMEI为{imei}的设备") return APIResponse(data=DeviceResponse.model_validate(device)) @router.get( "/all-latest-locations", response_model=APIResponse[list[LocationRecordResponse]], summary="获取所有在线设备位置 / Get all online device locations", ) async def all_latest_locations(db: AsyncSession = Depends(get_db)): """ 获取所有在线设备的最新位置,用于地图总览。 Get latest location for all online devices, for map overview. """ records = await location_service.get_all_online_latest_locations(db) return APIResponse(data=[LocationRecordResponse.model_validate(r) for r in records]) @router.post( "/batch", response_model=APIResponse[BatchDeviceCreateResponse], status_code=201, summary="批量创建设备 / Batch create devices", dependencies=[Depends(require_write)], ) @limiter.limit(settings.RATE_LIMIT_WRITE) async def batch_create_devices(request: Request, body: BatchDeviceCreateRequest, db: AsyncSession = Depends(get_db)): """ 批量注册设备(最多500台),跳过IMEI重复的设备。 Batch register devices (up to 500). Skips devices with duplicate IMEIs. """ results = await device_service.batch_create_devices(db, body.devices) created = sum(1 for r in results if r["success"]) failed = len(results) - created return APIResponse( message=f"Batch create: {created} created, {failed} failed", data=BatchDeviceCreateResponse( total=len(results), created=created, failed=failed, results=[BatchDeviceCreateResult(**r) for r in results], ), ) @router.put( "/batch", response_model=APIResponse[dict], summary="批量更新设备 / Batch update devices", dependencies=[Depends(require_write)], ) @limiter.limit(settings.RATE_LIMIT_WRITE) async def batch_update_devices(request: Request, body: BatchDeviceUpdateRequest, db: AsyncSession = Depends(get_db)): """ 批量更新设备信息(名称、状态等),最多500台。 Batch update device fields (name, status, etc.) for up to 500 devices. """ results = await device_service.batch_update_devices(db, body.device_ids, body.update) updated = sum(1 for r in results if r["success"]) failed = len(results) - updated return APIResponse( message=f"Batch update: {updated} updated, {failed} failed", data={"total": len(results), "updated": updated, "failed": failed, "results": results}, ) @router.post( "/batch-delete", response_model=APIResponse[dict], summary="批量删除设备 / Batch delete devices", dependencies=[Depends(require_write)], ) @limiter.limit(settings.RATE_LIMIT_WRITE) async def batch_delete_devices( request: Request, body: BatchDeviceDeleteRequest, db: AsyncSession = Depends(get_db), ): """ 批量删除设备(最多100台)。通过 POST body 传递 device_ids 列表。 Batch delete devices (up to 100). Pass device_ids in request body. """ results = await device_service.batch_delete_devices(db, body.device_ids) deleted = sum(1 for r in results if r["success"]) failed = len(results) - deleted return APIResponse( message=f"Batch delete: {deleted} deleted, {failed} failed", data={"total": len(results), "deleted": deleted, "failed": failed, "results": results}, ) @router.get( "/{device_id}", response_model=APIResponse[DeviceResponse], summary="获取设备详情 / Get device details", ) async def get_device(device_id: int, db: AsyncSession = Depends(get_db)): """ 按ID获取设备详细信息。 Get device details by ID. """ device = await device_service.get_device(db, device_id) if device is None: raise HTTPException(status_code=404, detail=f"Device {device_id} not found / 未找到设备{device_id}") return APIResponse(data=DeviceResponse.model_validate(device)) @router.post( "", response_model=APIResponse[DeviceResponse], status_code=201, summary="创建设备 / Create device", dependencies=[Depends(require_write)], ) async def create_device(device_data: DeviceCreate, db: AsyncSession = Depends(get_db)): """ 手动注册新设备。 Manually register a new device. """ # Check for duplicate IMEI existing = await device_service.get_device_by_imei(db, device_data.imei) if existing is not None: raise HTTPException( status_code=400, detail=f"Device with IMEI {device_data.imei} already exists / IMEI {device_data.imei} 已存在", ) device = await device_service.create_device(db, device_data) return APIResponse(data=DeviceResponse.model_validate(device)) @router.put( "/{device_id}", response_model=APIResponse[DeviceResponse], summary="更新设备信息 / Update device", dependencies=[Depends(require_write)], ) async def update_device( device_id: int, device_data: DeviceUpdate, db: AsyncSession = Depends(get_db) ): """ 更新设备信息(名称、状态等)。 Update device information (name, status, etc.). """ device = await device_service.update_device(db, device_id, device_data) if device is None: raise HTTPException(status_code=404, detail=f"Device {device_id} not found / 未找到设备{device_id}") return APIResponse(data=DeviceResponse.model_validate(device)) @router.delete( "/{device_id}", response_model=APIResponse, summary="删除设备 / Delete device", dependencies=[Depends(require_write)], ) async def delete_device(device_id: int, db: AsyncSession = Depends(get_db)): """ 删除设备及其关联数据。 Delete a device and all associated records. """ deleted = await device_service.delete_device(db, device_id) if not deleted: raise HTTPException(status_code=404, detail=f"Device {device_id} not found / 未找到设备{device_id}") return APIResponse(message="Device deleted successfully / 设备删除成功")