"""数据库 Schema 定义。 说明: - ORM 模型:用于结构相对稳定、需要 ORM 能力的表(例如 Simulation) - Core Table:用于时序/大数据量写入或更灵活的 SQL 操作(例如 vehicle_signals、server_metrics、RBAC 表等) """ from __future__ import annotations from datetime import datetime from sqlalchemy import JSON, BigInteger, Boolean, Column, DateTime, Float, ForeignKey, Index, Integer, String, Table, text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): """SQLAlchemy Declarative Base。""" pass class SimulationScene(Base): """仿真场景配置(非时序数据)。""" __tablename__ = "sim_scenes" scene_id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="场景 ID") scene_name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True, comment="场景名称") scene_desc: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="场景描述(可选)") scene_config: Mapped[dict] = mapped_column(JSON, default=dict, comment="场景配置信息(JSON)") is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True, comment="是否启用") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=text("now()"), index=True, comment="创建时间(UTC)") updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, comment="更新时间(UTC)") class SimulationTask(Base): """仿真任务记录(非时序数据)。""" __tablename__ = "sim_tasks" """以下为仿真任务相关配置""" task_id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="任务 ID") task_name: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="任务名称") scene_id: Mapped[str | None] = mapped_column( String(64), ForeignKey("sim_scenes.scene_id"), nullable=True, index=True, comment="仿真场景 ID(场景表中选择)" ) scene_name: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True, comment="仿真场景名称") scene_config: Mapped[dict] = mapped_column(JSON, default=dict, comment="仿真场景配置信息") config_created_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True, comment="配置创建时间(UTC)") """以下为仿真启停操作状态相关记录信息""" 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)") status: Mapped[str] = mapped_column(String(32), index=True, comment="仿真任务状态(wait/running/stopped/archived 等)") operator: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True, comment="仿真操作员") """以下为开始时发送给Unity程序的连接及初始化配置""" unity_host: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="Unity Socket 主机") unity_port: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="Unity Socket 端口") sync_timestamp: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="同步基准时间戳(毫秒)") init_config: Mapped[dict | None] = mapped_column(JSONB, nullable=True, comment="InitConfig 原始内容(主控→Unity,JSONB)") init_sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True, comment="InitConfig 发送时间(UTC)") 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(sim_tasks.task_id)"), Column("vehicle_id", String(64), nullable=False, index=True, comment="实物车辆ID,默认值为'0'"), 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_frames = Table( "sim_unity_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(sim_tasks.task_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(sim_tasks.task_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: """初始化数据库表结构与必要的兼容性变更。 该函数会: - create_all:创建 Base.metadata 里声明的表 - 插入默认角色(若不存在) - 对历史表做列/索引补齐(兼容升级) """ 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.execute(text("DROP INDEX IF EXISTS idx_vehicle_signals_sim_ts")) await conn.execute(text("DROP INDEX IF EXISTS idx_unity_frames_sim_vehicle_ts")) await conn.execute(text("DROP INDEX IF EXISTS idx_screen_recordings_sim_screen_created")) await conn.execute(text("DROP INDEX IF EXISTS idx_screen_recordings_sim_screen_time")) await conn.execute(text("DROP INDEX IF EXISTS idx_sys_role_permission_role")) await conn.execute(text("DROP INDEX IF EXISTS idx_sys_role_permission_perm")) await conn.execute(text("DROP INDEX IF EXISTS idx_sys_logs_user_ts")) await conn.execute(text("DROP INDEX IF EXISTS idx_sys_logs_action_ts")) await conn.execute(text("DROP INDEX IF EXISTS idx_server_metrics_host_ts")) await conn.run_sync(Base.metadata.create_all) await conn.execute( text( """ INSERT INTO sys_role (role_id, role_name, role_desc, is_active) VALUES ('admin', '系统管理员', '系统管理员', TRUE), ('auditor', '审计员', '审计员', TRUE), ('teacher', '老师', '老师', TRUE), ('student', '学生', '学生', TRUE) ON CONFLICT (role_id) DO NOTHING """ ) ) await conn.execute( text( """ INSERT INTO sim_scenes (scene_id, scene_name, scene_desc, scene_config, is_active) VALUES ('scene_01', '城市道路', '默认场景 1', '{}'::json, TRUE), ('scene_02', '高速公路', '默认场景 2', '{}'::json, TRUE), ('scene_03', '学校道路', '默认场景 3', '{}'::json, TRUE), ('scene_04', '场地训练', '默认场景 4', '{}'::json, TRUE), ('scene_05', '综合测试', '默认场景 5', '{}'::json, TRUE) ON CONFLICT (scene_id) DO NOTHING """ ) ) async def init_timescaledb(engine) -> None: """初始化 TimescaleDB 扩展与 hypertable/索引(若启用)。""" 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_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_frames (simulation_id, vehicle_id, ts DESC)" ) )