SmartEDT/backend/database/schema.py

225 lines
13 KiB
Python
Raw Normal View History

from __future__ import annotations
from datetime import datetime
from sqlalchemy import JSON, BigInteger, Boolean, Column, DateTime, Float, ForeignKey, Index, String, Table, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Simulation(Base):
__tablename__ = "simulations"
simulation_id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="仿真 ID")
status: Mapped[str] = mapped_column(String(32), index=True, comment="仿真状态running/stopped 等)")
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True, comment="开始时间UTC")
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True, comment="结束时间UTC")
scenario_name: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True, comment="仿真场景名称")
scenario_config: Mapped[dict] = mapped_column(JSON, default=dict, comment="仿真场景配置JSON")
config_created_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True, comment="配置创建时间UTC")
operator: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True, comment="仿真操作员")
archived: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否归档")
vehicle_signals = Table(
"sim_vehicle_signals",
Base.metadata,
Column("ts", DateTime(timezone=True), nullable=False, index=True, comment="信号采样时间UTC"),
Column("simulation_id", String(64), nullable=False, index=True, comment="仿真 ID"),
Column("device_id", String(64), nullable=False, index=True, comment="设备 ID"),
Column("seq", BigInteger, nullable=False, comment="信号序列号(单仿真内递增)"),
Column("signals", JSONB, nullable=False, comment="车辆信号载荷JSONB"),
Index("idx_vehicle_signals_sim_ts", "simulation_id", "ts"),
comment="车辆信号时序数据TimescaleDB hypertable",
)
unity_vehicle_frames = Table(
"sim_unity_vehicle_frames",
Base.metadata,
Column("ts", DateTime(timezone=True), nullable=False, index=True, comment="帧时间UTC"),
Column("simulation_id", String(64), nullable=False, index=True, comment="仿真 ID"),
Column("vehicle_id", String(64), nullable=False, index=True, comment="虚拟车辆 ID"),
Column("seq", BigInteger, nullable=False, comment="帧序号(单仿真单车内递增)"),
Column("pos_x", Float, nullable=False, comment="位置 X世界坐标"),
Column("pos_y", Float, nullable=False, comment="位置 Y世界坐标"),
Column("pos_z", Float, nullable=False, comment="位置 Z世界坐标"),
Column("rot_x", Float, nullable=False, comment="旋转四元数 X"),
Column("rot_y", Float, nullable=False, comment="旋转四元数 Y"),
Column("rot_z", Float, nullable=False, comment="旋转四元数 Z"),
Column("rot_w", Float, nullable=False, comment="旋转四元数 W"),
Column("lin_vel_x", Float, nullable=True, comment="线速度 X可选"),
Column("lin_vel_y", Float, nullable=True, comment="线速度 Y可选"),
Column("lin_vel_z", Float, nullable=True, comment="线速度 Z可选"),
Column("ang_vel_x", Float, nullable=True, comment="角速度 X可选"),
Column("ang_vel_y", Float, nullable=True, comment="角速度 Y可选"),
Column("ang_vel_z", Float, nullable=True, comment="角速度 Z可选"),
Column("controls", JSONB, nullable=True, comment="控制量(油门/刹车/方向/档位等JSONB"),
Column("extra", JSONB, nullable=True, comment="扩展字段仿真引擎自定义JSONB"),
Index("idx_unity_frames_sim_vehicle_ts", "simulation_id", "vehicle_id", "ts"),
comment="虚拟车辆驱动仿真帧数据(用于 Unity 车辆模型运动与回放TimescaleDB hypertable",
)
screen_recordings = Table(
"sim_screen_videos",
Base.metadata,
Column("video_id", String(64), primary_key=True, comment="录制文件记录 ID"),
Column("simulation_id", String(64), nullable=False, index=True, comment="仿真 ID"),
Column("screen_type", String(32), nullable=False, index=True, comment="屏幕类型big_screen/vehicle_screen 等)"),
Column("source_name", String(64), nullable=True, index=True, comment="录制源名称(可选,如设备号/通道号)"),
Column("status", String(32), nullable=False, index=True, comment="状态recording/ready/failed 等)"),
Column("relative_path", String(1024), nullable=False, comment="相对文件根目录的路径(用于下载/归档)"),
Column("file_name", String(255), nullable=True, comment="文件名(可选)"),
Column("format", String(32), nullable=True, comment="容器格式mp4/mkv 等)"),
Column("codec", String(64), nullable=True, comment="编码信息H264/H265 等)"),
Column("width", BigInteger, nullable=True, comment="视频宽度(像素)"),
Column("height", BigInteger, nullable=True, comment="视频高度(像素)"),
Column("fps", Float, nullable=True, comment="帧率(可选)"),
Column("duration_ms", BigInteger, nullable=True, comment="时长(毫秒,可选)"),
Column("size_bytes", BigInteger, nullable=True, comment="文件大小(字节,可选)"),
Column("recorded_started_at", DateTime(timezone=True), nullable=True, index=True, comment="录制开始时间UTC可选"),
Column("recorded_ended_at", DateTime(timezone=True), nullable=True, index=True, comment="录制结束时间UTC可选"),
Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="记录创建时间UTC"),
Column("extra", JSONB, nullable=True, comment="扩展信息JSONB"),
Index("idx_screen_recordings_sim_screen_created", "simulation_id", "screen_type", "created_at"),
Index("idx_screen_recordings_sim_screen_time", "simulation_id", "screen_type", "recorded_started_at"),
comment="仿真过程屏幕录制文件元数据(显示大屏/车载屏等)",
)
sys_role = Table(
"sys_role",
Base.metadata,
Column("role_id", String(64), primary_key=True, comment="角色 ID"),
Column("role_name", String(64), nullable=False, unique=True, index=True, comment="角色名称(唯一)"),
Column("role_desc", String(255), nullable=True, comment="角色描述"),
Column("is_active", Boolean, nullable=False, server_default=text("TRUE"), index=True, comment="是否启用"),
Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间UTC"),
Column("updated_at", DateTime(timezone=True), nullable=True, comment="更新时间UTC"),
Column("extra", JSONB, nullable=True, comment="扩展信息JSONB"),
comment="系统角色",
)
sys_permission = Table(
"sys_permission",
Base.metadata,
Column("perm_code", String(128), primary_key=True, comment="权限编码(唯一)"),
Column("perm_name", String(128), nullable=False, index=True, comment="权限名称"),
Column("perm_group", String(64), nullable=True, index=True, comment="权限分组(可选)"),
Column("perm_desc", String(255), nullable=True, comment="权限描述"),
Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间UTC"),
comment="系统功能权限",
)
sys_role_permission = Table(
"sys_role_permission",
Base.metadata,
Column("role_id", String(64), ForeignKey("sys_role.role_id", ondelete="CASCADE"), primary_key=True, comment="角色 ID"),
Column("perm_code", String(128), ForeignKey("sys_permission.perm_code", ondelete="CASCADE"), primary_key=True, comment="权限编码"),
Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间UTC"),
Index("idx_sys_role_permission_role", "role_id"),
Index("idx_sys_role_permission_perm", "perm_code"),
comment="角色功能权限关联表",
)
sys_user = Table(
"sys_user",
Base.metadata,
Column("user_id", String(64), primary_key=True, comment="用户 ID"),
Column("username", String(64), nullable=False, unique=True, index=True, comment="登录名(唯一)"),
Column("display_name", String(64), nullable=True, index=True, comment="显示名称"),
Column("password_hash", String(255), nullable=False, comment="密码哈希"),
Column("role_id", String(64), ForeignKey("sys_role.role_id"), nullable=False, index=True, comment="所属角色 ID"),
Column("is_active", Boolean, nullable=False, server_default=text("TRUE"), index=True, comment="是否启用"),
Column("last_login_at", DateTime(timezone=True), nullable=True, index=True, comment="最近登录时间UTC"),
Column("created_at", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间UTC"),
Column("updated_at", DateTime(timezone=True), nullable=True, comment="更新时间UTC"),
Column("extra", JSONB, nullable=True, comment="扩展信息JSONB"),
comment="系统用户(含所属角色)",
)
sys_logs = Table(
"sys_logs",
Base.metadata,
Column("log_id", BigInteger, primary_key=True, comment="日志 ID"),
Column("ts", DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="操作时间UTC"),
Column("user_id", String(64), nullable=True, index=True, comment="用户 ID可为空如匿名"),
Column("username", String(64), nullable=True, index=True, comment="登录名快照(可选)"),
Column("role_id", String(64), nullable=True, index=True, comment="角色 ID 快照(可选)"),
Column("action", String(128), nullable=False, index=True, comment="操作动作(如 login/start_simulation"),
Column("resource", String(255), nullable=True, index=True, comment="资源标识(如 URL/对象 ID"),
Column("success", Boolean, nullable=False, server_default=text("TRUE"), index=True, comment="是否成功"),
Column("ip", String(64), nullable=True, comment="客户端 IP可选"),
Column("user_agent", String(512), nullable=True, comment="User-Agent可选"),
Column("detail", JSONB, nullable=True, comment="操作明细JSONB可选"),
Index("idx_sys_logs_user_ts", "user_id", "ts"),
Index("idx_sys_logs_action_ts", "action", "ts"),
comment="系统操作日志",
)
server_metrics = Table(
"server_metrics",
Base.metadata,
Column("ts", DateTime(timezone=True), nullable=False, index=True, comment="采样时间UTC"),
Column("host_name", String(64), nullable=False, index=True, comment="主机名"),
Column("cpu_usage_percent", JSONB, nullable=False, comment="CPU 使用率百分比JSONB"),
Column("memory_usage_bytes", JSONB, nullable=False, comment="内存使用情况字节JSONB"),
Column("disk_usage_bytes", JSONB, nullable=True, comment="磁盘使用情况字节JSONB"),
Index("idx_server_metrics_host_ts", "host_name", "ts"),
comment="服务器监控指标时序数据TimescaleDB hypertable",
)
async def init_schema(engine) -> None:
from sqlalchemy.ext.asyncio import AsyncEngine
if not isinstance(engine, AsyncEngine):
raise TypeError("engine must be AsyncEngine")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await conn.execute(text("ALTER TABLE simulations ADD COLUMN IF NOT EXISTS scenario_name VARCHAR(255)"))
await conn.execute(text("ALTER TABLE simulations ADD COLUMN IF NOT EXISTS config_created_at TIMESTAMPTZ"))
await conn.execute(text("ALTER TABLE simulations ADD COLUMN IF NOT EXISTS operator VARCHAR(64)"))
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_simulations_scenario_name ON simulations (scenario_name)"))
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_simulations_config_created_at ON simulations (config_created_at)"))
await conn.execute(text("CREATE INDEX IF NOT EXISTS idx_simulations_operator ON simulations (operator)"))
async def init_timescaledb(engine) -> None:
async with engine.begin() as conn:
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS timescaledb"))
await conn.execute(
text(
"SELECT create_hypertable('sim_vehicle_signals', 'ts', if_not_exists => TRUE)"
)
)
await conn.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_vehicle_signals_sim_ts_desc ON sim_vehicle_signals (simulation_id, ts DESC)"
)
)
await conn.execute(
text(
"SELECT create_hypertable('server_metrics', 'ts', if_not_exists => TRUE)"
)
)
await conn.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_server_metrics_host_ts_desc ON server_metrics (host_name, ts DESC)"
)
)
await conn.execute(
text(
"SELECT create_hypertable('sim_unity_vehicle_frames', 'ts', if_not_exists => TRUE)"
)
)
await conn.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_unity_frames_sim_vehicle_ts_desc ON sim_unity_vehicle_frames (simulation_id, vehicle_id, ts DESC)"
)
)