Files
desungongpai/app/routers/devices.py
default 11281e5be2 Add WebSocket, multi API key, geocoding proxy, beacon map picker, and comprehensive bug fixes
- Multi API Key + permission system (read/write/admin) with SHA-256 hash
- WebSocket real-time push (location, alarm, device_status, attendance, bluetooth)
- Geocoding proxy endpoints for Amap POI search and reverse geocode
- Beacon modal map-based location picker with search and click-to-select
- GCJ-02 ↔ WGS-84 bidirectional coordinate conversion
- Data cleanup scheduler (configurable retention days)
- Fix GPS longitude sign inversion (course_status bit 11: 0=East, 1=West)
- Fix 2G CellID 2→3 bytes across all protocols (0x28, 0x2C, parser.py)
- Fix parser loop guards, alarm_source field length, CommandLog.sent_at
- Fix geocoding IMEI parameterization, require_admin import
- Improve API error messages for 422 validation errors
- Remove beacon floor/area fields (consolidated into name)

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
2026-03-24 05:10:05 +00:00

254 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Devices Router - 设备管理接口
API endpoints for device CRUD operations and statistics.
"""
import math
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
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(
"/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 / 设备删除成功")