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:
110
app/models.py
110
app/models.py
@@ -36,6 +36,7 @@ class Device(Base):
|
||||
imsi: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
timezone: Mapped[str] = mapped_column(String(30), default="+8", nullable=False)
|
||||
language: Mapped[str] = mapped_column(String(10), default="cn", nullable=False)
|
||||
group_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow, nullable=True
|
||||
@@ -297,6 +298,27 @@ class BeaconConfig(Base):
|
||||
return f"<BeaconConfig(id={self.id}, mac={self.beacon_mac}, name={self.name})>"
|
||||
|
||||
|
||||
class DeviceBeaconBinding(Base):
|
||||
"""Many-to-many binding between devices and beacons."""
|
||||
|
||||
__tablename__ = "device_beacon_bindings"
|
||||
__table_args__ = (
|
||||
Index("ix_dbb_device_beacon", "device_id", "beacon_id", unique=True),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
device_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("devices.id", ondelete="CASCADE"), index=True, nullable=False
|
||||
)
|
||||
beacon_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("beacon_configs.id", ondelete="CASCADE"), index=True, nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<DeviceBeaconBinding(device_id={self.device_id}, beacon_id={self.beacon_id})>"
|
||||
|
||||
|
||||
class FenceConfig(Base):
|
||||
"""Geofence configuration for area monitoring."""
|
||||
|
||||
@@ -403,6 +425,94 @@ class CommandLog(Base):
|
||||
)
|
||||
|
||||
|
||||
class DeviceGroup(Base):
|
||||
"""Device groups for organizing devices."""
|
||||
|
||||
__tablename__ = "device_groups"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
color: Mapped[str] = mapped_column(String(20), default="#3b82f6", nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow, nullable=True
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<DeviceGroup(id={self.id}, name={self.name})>"
|
||||
|
||||
|
||||
class DeviceGroupMember(Base):
|
||||
"""Many-to-many: devices belong to groups."""
|
||||
|
||||
__tablename__ = "device_group_members"
|
||||
__table_args__ = (
|
||||
Index("ix_dgm_device_group", "device_id", "group_id", unique=True),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
device_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("devices.id", ondelete="CASCADE"), index=True, nullable=False
|
||||
)
|
||||
group_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("device_groups.id", ondelete="CASCADE"), index=True, nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<DeviceGroupMember(device_id={self.device_id}, group_id={self.group_id})>"
|
||||
|
||||
|
||||
class AlertRule(Base):
|
||||
"""Configurable alert rules for custom thresholds."""
|
||||
|
||||
__tablename__ = "alert_rules"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
rule_type: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
# rule_type values: low_battery, no_heartbeat, fence_stay, speed_limit, offline_duration
|
||||
conditions: Mapped[dict] = mapped_column(JSON, nullable=False)
|
||||
# e.g. {"threshold": 20} for low_battery, {"minutes": 30} for no_heartbeat
|
||||
is_active: Mapped[bool] = mapped_column(Integer, default=1, nullable=False)
|
||||
device_ids: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
# comma-separated device IDs, null = all devices
|
||||
group_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
# apply to a device group
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow, nullable=True
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AlertRule(id={self.id}, name={self.name}, type={self.rule_type})>"
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
"""Audit trail for write operations (POST/PUT/DELETE)."""
|
||||
|
||||
__tablename__ = "audit_logs"
|
||||
__table_args__ = (
|
||||
Index("ix_audit_created", "created_at"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
method: Mapped[str] = mapped_column(String(10), nullable=False) # POST, PUT, DELETE
|
||||
path: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
status_code: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
operator: Mapped[str | None] = mapped_column(String(100), nullable=True) # API key name or "master"
|
||||
client_ip: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
request_body: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
response_summary: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AuditLog(id={self.id}, {self.method} {self.path})>"
|
||||
|
||||
|
||||
class ApiKey(Base):
|
||||
"""API keys for external system authentication."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user