SmartEDT/backend/database/schema.py

295 lines
17 KiB
Python
Raw Permalink Normal View History

"""数据库 Schema 定义。
说明
- ORM 模型用于结构相对稳定需要 ORM 能力的表例如 Simulation
- Core Table用于时序/大数据量写入或更灵活的 SQL 操作例如 vehicle_signalsserver_metricsRBAC 表等
"""
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 原始内容主控→UnityJSONB")
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="仿真任务 IDsim_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="仿真任务 IDsim_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="仿真任务 IDsim_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)"
)
)