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>
This commit is contained in:
2026-03-24 05:10:05 +00:00
parent 7d6040af41
commit 11281e5be2
24 changed files with 1636 additions and 730 deletions

View File

@@ -22,8 +22,10 @@ from app.schemas import (
PaginatedList,
)
from app.config import settings
from app.dependencies import require_write
from app.extensions import limiter
from app.services import device_service
from app.schemas import LocationRecordResponse
from app.services import device_service, location_service
router = APIRouter(prefix="/api/devices", tags=["Devices / 设备管理"])
@@ -88,11 +90,26 @@ async def get_device_by_imei(imei: str, db: AsyncSession = Depends(get_db)):
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)):
@@ -118,6 +135,7 @@ async def batch_create_devices(request: Request, body: BatchDeviceCreateRequest,
"/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)):
@@ -138,6 +156,7 @@ async def batch_update_devices(request: Request, body: BatchDeviceUpdateRequest,
"/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(
@@ -179,6 +198,7 @@ async def get_device(device_id: int, db: AsyncSession = Depends(get_db)):
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)):
"""
@@ -201,6 +221,7 @@ async def create_device(device_data: DeviceCreate, db: AsyncSession = Depends(ge
"/{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)
@@ -219,6 +240,7 @@ async def update_device(
"/{device_id}",
response_model=APIResponse,
summary="删除设备 / Delete device",
dependencies=[Depends(require_write)],
)
async def delete_device(device_id: int, db: AsyncSession = Depends(get_db)):
"""