Files
desungongpai/app/routers/devices.py

292 lines
10 KiB
Python
Raw Normal View History

"""
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 / 设备删除成功")