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)" ) )