feat: 高德IoT v5 API升级、电子围栏管理、设备绑定自动考勤
- 前向地理编码升级为高德IoT v5 API (POST restapi.amap.com/v5/position/IoT) - 修复LBS定位偏差: 添加network=LTE参数区分4G/2G, bts格式补充cage字段 - 新增电子围栏管理模块 (circle/polygon/rectangle), 支持地图绘制和POI搜索 - 新增设备-围栏多对多绑定 (DeviceFenceBinding/DeviceFenceState) - 围栏自动考勤引擎 (fence_checker.py): haversine距离、ray-casting多边形判定、容差机制、防抖 - TCP位置上报自动检测围栏进出, 生成考勤记录并WebSocket广播 - 前端围栏页面: 绑定设备弹窗、POI搜索定位、左侧围栏面板 - 新增fence_attendance WebSocket topic via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -121,6 +121,7 @@ class LocationRecordResponse(LocationRecordBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
imei: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@@ -169,6 +170,7 @@ class AlarmRecordResponse(AlarmRecordBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
imei: str | None = None
|
||||
acknowledged: bool
|
||||
created_at: datetime
|
||||
|
||||
@@ -211,6 +213,7 @@ class HeartbeatRecordResponse(HeartbeatRecordBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
imei: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@@ -253,6 +256,7 @@ class AttendanceRecordResponse(AttendanceRecordBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
imei: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@@ -298,6 +302,7 @@ class BluetoothRecordResponse(BluetoothRecordBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
imei: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@@ -368,6 +373,100 @@ class BeaconConfigResponse(BaseModel):
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fence Config schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FenceConfigCreate(BaseModel):
|
||||
name: str = Field(..., max_length=100, description="围栏名称")
|
||||
fence_type: Literal["circle", "polygon", "rectangle"] = Field(..., description="围栏类型")
|
||||
center_lat: float | None = Field(None, ge=-90, le=90, description="中心纬度 (WGS-84)")
|
||||
center_lng: float | None = Field(None, ge=-180, le=180, description="中心经度 (WGS-84)")
|
||||
radius: float | None = Field(None, ge=0, description="半径 (米)")
|
||||
points: str | None = Field(None, description="多边形顶点 JSON [[lng,lat],...]")
|
||||
color: str = Field(default="#3b82f6", max_length=20, description="边框颜色")
|
||||
fill_color: str | None = Field(None, max_length=20, description="填充颜色")
|
||||
fill_opacity: float = Field(default=0.2, ge=0, le=1, description="填充透明度")
|
||||
description: str | None = Field(None, description="描述")
|
||||
is_active: bool = Field(default=True, description="是否启用")
|
||||
|
||||
|
||||
class FenceConfigUpdate(BaseModel):
|
||||
name: str | None = Field(None, max_length=100)
|
||||
fence_type: Literal["circle", "polygon", "rectangle"] | None = None
|
||||
center_lat: float | None = Field(None, ge=-90, le=90)
|
||||
center_lng: float | None = Field(None, ge=-180, le=180)
|
||||
radius: float | None = Field(None, ge=0)
|
||||
points: str | None = None
|
||||
color: str | None = Field(None, max_length=20)
|
||||
fill_color: str | None = Field(None, max_length=20)
|
||||
fill_opacity: float | None = Field(None, ge=0, le=1)
|
||||
description: str | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class FenceConfigResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
fence_type: str
|
||||
center_lat: float | None = None
|
||||
center_lng: float | None = None
|
||||
radius: float | None = None
|
||||
points: str | None = None
|
||||
color: str
|
||||
fill_color: str | None = None
|
||||
fill_opacity: float
|
||||
description: str | None = None
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Device-Fence Binding schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DeviceFenceBindRequest(BaseModel):
|
||||
device_ids: list[int] = Field(..., min_length=1, max_length=100, description="设备ID列表")
|
||||
|
||||
|
||||
class FenceBindForDeviceRequest(BaseModel):
|
||||
fence_ids: list[int] = Field(..., min_length=1, max_length=100, description="围栏ID列表")
|
||||
|
||||
|
||||
class DeviceFenceBindingResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
device_id: int
|
||||
fence_id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class FenceDeviceDetail(BaseModel):
|
||||
"""Binding detail with device info."""
|
||||
binding_id: int
|
||||
device_id: int
|
||||
device_name: str | None = None
|
||||
imei: str | None = None
|
||||
is_inside: bool = False
|
||||
last_check_at: datetime | None = None
|
||||
|
||||
|
||||
class DeviceFenceDetail(BaseModel):
|
||||
"""Binding detail with fence info."""
|
||||
binding_id: int
|
||||
fence_id: int
|
||||
fence_name: str | None = None
|
||||
fence_type: str | None = None
|
||||
is_inside: bool = False
|
||||
last_check_at: datetime | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Command Log schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user