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

@@ -55,7 +55,6 @@ class DeviceCreate(DeviceBase):
class DeviceUpdate(BaseModel):
name: str | None = Field(None, max_length=100)
status: Literal["online", "offline"] | None = Field(None, description="Device status")
iccid: str | None = Field(None, max_length=30)
imsi: str | None = Field(None, max_length=20)
timezone: str | None = Field(None, max_length=30)
@@ -144,7 +143,7 @@ class LocationListResponse(APIResponse[PaginatedList[LocationRecordResponse]]):
class AlarmRecordBase(BaseModel):
device_id: int
alarm_type: str = Field(..., max_length=30)
alarm_source: str | None = Field(None, max_length=10)
alarm_source: str | None = Field(None, max_length=20)
protocol_number: int
latitude: float | None = Field(None, ge=-90, le=90)
longitude: float | None = Field(None, ge=-180, le=180)
@@ -320,7 +319,7 @@ class BluetoothListResponse(APIResponse[PaginatedList[BluetoothRecordResponse]])
class BeaconConfigBase(BaseModel):
beacon_mac: str = Field(..., max_length=20, pattern=r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$", description="信标MAC地址 (AA:BB:CC:DD:EE:FF)")
beacon_uuid: str | None = Field(None, max_length=36, pattern=r"^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$", description="iBeacon UUID")
beacon_uuid: str | None = Field(None, max_length=36, description="iBeacon UUID")
beacon_major: int | None = Field(None, ge=0, le=65535, description="iBeacon Major")
beacon_minor: int | None = Field(None, ge=0, le=65535, description="iBeacon Minor")
name: str = Field(..., max_length=100, description="信标名称")
@@ -337,7 +336,7 @@ class BeaconConfigCreate(BeaconConfigBase):
class BeaconConfigUpdate(BaseModel):
beacon_uuid: str | None = Field(None, max_length=36, pattern=r"^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$")
beacon_uuid: str | None = Field(None, max_length=36)
beacon_major: int | None = Field(None, ge=0, le=65535)
beacon_minor: int | None = Field(None, ge=0, le=65535)
name: str | None = Field(None, max_length=100)
@@ -482,3 +481,35 @@ class CommandResponse(BaseModel):
class CommandListResponse(APIResponse[PaginatedList[CommandResponse]]):
pass
# ---------------------------------------------------------------------------
# API Key schemas
# ---------------------------------------------------------------------------
class ApiKeyCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="Key name / 名称")
permissions: Literal["read", "write", "admin"] = Field(default="read", description="Permission level")
class ApiKeyUpdate(BaseModel):
name: str | None = Field(None, max_length=100)
permissions: Literal["read", "write", "admin"] | None = None
is_active: bool | None = None
class ApiKeyResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
permissions: str
is_active: bool
last_used_at: datetime | None = None
created_at: datetime
class ApiKeyCreateResponse(ApiKeyResponse):
"""Returned only on creation — includes the plaintext key (shown once)."""
key: str