feat: KKS P240/P241 蓝牙工牌管理系统初始提交
FastAPI + SQLAlchemy + asyncio TCP 服务器,支持设备管理、实时定位、 告警、考勤打卡、蓝牙记录、指令下发、TTS语音播报等功能。
This commit is contained in:
342
app/models.py
Normal file
342
app/models.py
Normal file
@@ -0,0 +1,342 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class Device(Base):
|
||||
"""Registered Bluetooth badge devices."""
|
||||
|
||||
__tablename__ = "devices"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
imei: Mapped[str] = mapped_column(String(20), unique=True, index=True, nullable=False)
|
||||
device_type: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), default="offline", nullable=False)
|
||||
battery_level: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
gsm_signal: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
last_heartbeat: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
last_login: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
iccid: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||
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)
|
||||
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
|
||||
)
|
||||
|
||||
# Relationships
|
||||
locations: Mapped[list["LocationRecord"]] = relationship(
|
||||
back_populates="device", cascade="all, delete-orphan", lazy="noload"
|
||||
)
|
||||
alarms: Mapped[list["AlarmRecord"]] = relationship(
|
||||
back_populates="device", cascade="all, delete-orphan", lazy="noload"
|
||||
)
|
||||
heartbeats: Mapped[list["HeartbeatRecord"]] = relationship(
|
||||
back_populates="device", cascade="all, delete-orphan", lazy="noload"
|
||||
)
|
||||
attendance_records: Mapped[list["AttendanceRecord"]] = relationship(
|
||||
back_populates="device", cascade="all, delete-orphan", lazy="noload"
|
||||
)
|
||||
bluetooth_records: Mapped[list["BluetoothRecord"]] = relationship(
|
||||
back_populates="device", cascade="all, delete-orphan", lazy="noload"
|
||||
)
|
||||
command_logs: Mapped[list["CommandLog"]] = relationship(
|
||||
back_populates="device", cascade="all, delete-orphan", lazy="noload"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Device(id={self.id}, imei={self.imei}, status={self.status})>"
|
||||
|
||||
|
||||
class LocationRecord(Base):
|
||||
"""GPS / LBS / WIFI location records."""
|
||||
|
||||
__tablename__ = "location_records"
|
||||
__table_args__ = (
|
||||
Index("ix_location_device_time", "device_id", "recorded_at"),
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
location_type: Mapped[str] = mapped_column(
|
||||
String(10), nullable=False
|
||||
) # gps, lbs, wifi, gps_4g, lbs_4g, wifi_4g
|
||||
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
speed: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
course: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
gps_satellites: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
gps_positioned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
mcc: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
mnc: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
lac: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
cell_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
rssi: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
neighbor_cells: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
wifi_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
report_mode: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
is_realtime: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
mileage: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
raw_data: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
device: Mapped["Device"] = relationship(back_populates="locations")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<LocationRecord(id={self.id}, device_id={self.device_id}, "
|
||||
f"type={self.location_type})>"
|
||||
)
|
||||
|
||||
|
||||
class AlarmRecord(Base):
|
||||
"""Alarm events raised by devices."""
|
||||
|
||||
__tablename__ = "alarm_records"
|
||||
__table_args__ = (
|
||||
Index("ix_alarm_device_time", "device_id", "recorded_at"),
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
alarm_type: Mapped[str] = mapped_column(
|
||||
String(30), nullable=False
|
||||
) # sos, low_battery, power_on, power_off, enter_fence, exit_fence, ...
|
||||
alarm_source: Mapped[str | None] = mapped_column(
|
||||
String(20), nullable=True
|
||||
) # single_fence, multi_fence, lbs, wifi
|
||||
protocol_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
speed: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
course: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
mcc: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
mnc: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
lac: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
cell_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
battery_level: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
gsm_signal: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
fence_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
wifi_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
acknowledged: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
device: Mapped["Device"] = relationship(back_populates="alarms")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<AlarmRecord(id={self.id}, device_id={self.device_id}, "
|
||||
f"type={self.alarm_type})>"
|
||||
)
|
||||
|
||||
|
||||
class HeartbeatRecord(Base):
|
||||
"""Heartbeat history from devices."""
|
||||
|
||||
__tablename__ = "heartbeat_records"
|
||||
__table_args__ = (
|
||||
Index("ix_heartbeat_device_time", "device_id", "created_at"),
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
protocol_number: Mapped[int] = mapped_column(Integer, nullable=False) # 0x13 or 0x36
|
||||
terminal_info: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
battery_level: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
gsm_signal: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
extension_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
device: Mapped["Device"] = relationship(back_populates="heartbeats")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<HeartbeatRecord(id={self.id}, device_id={self.device_id})>"
|
||||
|
||||
|
||||
class AttendanceRecord(Base):
|
||||
"""Attendance punch records from badges."""
|
||||
|
||||
__tablename__ = "attendance_records"
|
||||
__table_args__ = (
|
||||
Index("ix_attendance_device_time", "device_id", "recorded_at"),
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
attendance_type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False
|
||||
) # clock_in, clock_out
|
||||
protocol_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
gps_positioned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
speed: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
course: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
gps_satellites: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
battery_level: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
gsm_signal: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
mcc: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
mnc: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
lac: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
cell_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
wifi_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
lbs_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
device: Mapped["Device"] = relationship(back_populates="attendance_records")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<AttendanceRecord(id={self.id}, device_id={self.device_id}, "
|
||||
f"type={self.attendance_type})>"
|
||||
)
|
||||
|
||||
|
||||
class BluetoothRecord(Base):
|
||||
"""Bluetooth punch card and location records."""
|
||||
|
||||
__tablename__ = "bluetooth_records"
|
||||
__table_args__ = (
|
||||
Index("ix_bluetooth_device_time", "device_id", "recorded_at"),
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
record_type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False
|
||||
) # punch, location
|
||||
protocol_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
beacon_mac: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
beacon_uuid: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
||||
beacon_major: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
beacon_minor: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
rssi: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
beacon_battery: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
beacon_battery_unit: Mapped[str | None] = mapped_column(String(10), nullable=True)
|
||||
attendance_type: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
bluetooth_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
device: Mapped["Device"] = relationship(back_populates="bluetooth_records")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<BluetoothRecord(id={self.id}, device_id={self.device_id}, "
|
||||
f"type={self.record_type})>"
|
||||
)
|
||||
|
||||
|
||||
class BeaconConfig(Base):
|
||||
"""Registered Bluetooth beacon configuration for indoor positioning."""
|
||||
|
||||
__tablename__ = "beacon_configs"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
beacon_mac: Mapped[str] = mapped_column(String(20), unique=True, index=True, nullable=False)
|
||||
beacon_uuid: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
||||
beacon_major: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
beacon_minor: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
floor: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
area: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), default="active", 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"<BeaconConfig(id={self.id}, mac={self.beacon_mac}, name={self.name})>"
|
||||
|
||||
|
||||
class CommandLog(Base):
|
||||
"""Log of commands sent to devices."""
|
||||
|
||||
__tablename__ = "command_logs"
|
||||
__table_args__ = (
|
||||
Index("ix_command_device_status", "device_id", "status"),
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
command_type: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
command_content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
server_flag: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
response_content: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20), default="pending", nullable=False
|
||||
) # pending, sent, success, failed
|
||||
sent_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
response_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
device: Mapped["Device"] = relationship(back_populates="command_logs")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<CommandLog(id={self.id}, device_id={self.device_id}, "
|
||||
f"type={self.command_type}, status={self.status})>"
|
||||
)
|
||||
|
||||
|
||||
class ApiKey(Base):
|
||||
"""API keys for external system authentication."""
|
||||
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
key_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
permissions: Mapped[str] = mapped_column(
|
||||
String(20), default="read", nullable=False
|
||||
) # read, write, admin
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
last_used_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ApiKey(id={self.id}, name={self.name}, permissions={self.permissions})>"
|
||||
Reference in New Issue
Block a user