feat: 信标设备绑定 + 蓝牙模式管理 + 系统管理增强 + 数据导出

- 新增 DeviceBeaconBinding 模型,信标-设备多对多绑定 CRUD
- 蓝牙打卡模式批量配置/恢复正常模式 API
- 反向同步: 查询设备 BTMACSET 配置更新数据库绑定 (独立 session 解决事务隔离)
- 设备列表快捷操作弹窗修复 (fire-and-forget IIFE 替代阻塞轮询)
- 保存按钮防抖: 围栏/信标绑定保存点击后 disabled + 转圈防重复提交
- 审计日志中间件 + 系统配置/备份/固件 API
- 设备分组管理 + 告警规则配置
- 5个数据导出 API (CSV UTF-8 BOM)
- 位置热力图 + 告警条件删除 + 位置清理

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

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
2026-04-01 07:06:37 +00:00
parent 9daa81621c
commit 9cd9dd9d76
19 changed files with 3403 additions and 100 deletions

View File

@@ -59,6 +59,7 @@ class DeviceUpdate(BaseModel):
imsi: str | None = Field(None, max_length=20)
timezone: str | None = Field(None, max_length=30)
language: str | None = Field(None, max_length=10)
group_id: int | None = None
class DeviceResponse(DeviceBase):
@@ -72,6 +73,7 @@ class DeviceResponse(DeviceBase):
last_login: datetime | None = None
iccid: str | None = None
imsi: str | None = None
group_id: int | None = None
created_at: datetime
updated_at: datetime | None = None
@@ -374,6 +376,23 @@ class BeaconConfigResponse(BaseModel):
updated_at: datetime | None = None
# ---------------------------------------------------------------------------
# Device-Beacon Binding schemas
# ---------------------------------------------------------------------------
class DeviceBeaconBindRequest(BaseModel):
device_ids: list[int] = Field(..., min_length=1, max_length=100, description="设备ID列表")
class BeaconDeviceDetail(BaseModel):
"""Binding detail with device info."""
binding_id: int
device_id: int
device_name: str | None = None
imei: str | None = None
# ---------------------------------------------------------------------------
# Fence Config schemas
# ---------------------------------------------------------------------------
@@ -588,6 +607,132 @@ class CommandListResponse(APIResponse[PaginatedList[CommandResponse]]):
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Device Group schemas
# ---------------------------------------------------------------------------
class DeviceGroupCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="分组名称")
description: str | None = Field(None, max_length=500, description="描述")
color: str = Field(default="#3b82f6", max_length=20, description="颜色")
class DeviceGroupUpdate(BaseModel):
name: str | None = Field(None, max_length=100)
description: str | None = None
color: str | None = Field(None, max_length=20)
class DeviceGroupResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
description: str | None = None
color: str
created_at: datetime
updated_at: datetime | None = None
class DeviceGroupWithCount(DeviceGroupResponse):
device_count: int = 0
# ---------------------------------------------------------------------------
# Alert Rule schemas
# ---------------------------------------------------------------------------
class AlertRuleCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="规则名称")
rule_type: Literal["low_battery", "no_heartbeat", "fence_stay", "speed_limit", "offline_duration"] = Field(
..., description="规则类型"
)
conditions: dict = Field(..., description="条件参数, 如 {\"threshold\": 20}")
is_active: bool = Field(default=True)
device_ids: str | None = Field(None, description="适用设备ID (逗号分隔), null=全部")
group_id: int | None = Field(None, description="适用分组ID")
description: str | None = Field(None, max_length=500)
class AlertRuleUpdate(BaseModel):
name: str | None = Field(None, max_length=100)
rule_type: Literal["low_battery", "no_heartbeat", "fence_stay", "speed_limit", "offline_duration"] | None = None
conditions: dict | None = None
is_active: bool | None = None
device_ids: str | None = None
group_id: int | None = None
description: str | None = None
class AlertRuleResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
rule_type: str
conditions: dict
is_active: bool
device_ids: str | None = None
group_id: int | None = None
description: str | None = None
created_at: datetime
updated_at: datetime | None = None
# ---------------------------------------------------------------------------
# Audit Log schemas
# ---------------------------------------------------------------------------
class AuditLogResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
method: str
path: str
status_code: int
operator: str | None = None
client_ip: str | None = None
request_body: dict[str, Any] | None = None
response_summary: str | None = None
duration_ms: int | None = None
created_at: datetime
# ---------------------------------------------------------------------------
# System Config schemas
# ---------------------------------------------------------------------------
class SystemConfigResponse(BaseModel):
"""Current runtime configuration (read-only and writable fields)."""
data_retention_days: int
data_cleanup_interval_hours: int
tcp_idle_timeout: int
fence_check_enabled: bool
fence_lbs_tolerance_meters: int
fence_wifi_tolerance_meters: int
fence_min_inside_seconds: int
rate_limit_default: str
rate_limit_write: str
track_max_points: int
geocoding_cache_size: int
class SystemConfigUpdate(BaseModel):
"""Fields that can be updated at runtime."""
data_retention_days: int | None = Field(None, ge=1, le=3650)
data_cleanup_interval_hours: int | None = Field(None, ge=1, le=720)
tcp_idle_timeout: int | None = Field(None, ge=0, le=86400)
fence_check_enabled: bool | None = None
fence_lbs_tolerance_meters: int | None = Field(None, ge=0, le=10000)
fence_wifi_tolerance_meters: int | None = Field(None, ge=0, le=10000)
fence_min_inside_seconds: int | None = Field(None, ge=0, le=3600)
track_max_points: int | None = Field(None, ge=100, le=100000)
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")