Skip to content

数据模型设计

ops-console 的数据库需要四张表:用户、资产、任务、事件——覆盖"谁(用户)对什么(资产)做了什么(任务),留下了什么记录(事件)"。前三个表的作用比较直观(存用户、存资产、存任务),第四张事件表可能让人疑惑:任务记录里不是已经有"谁发起的"吗?

区别在于:任务记录的是"一次操作的执行过程和结果",事件记录的是"谁在什么时间做了什么"的审计日志。任务可以被删除(清理历史),但审计日志不能删——出了事故要追责时,"谁在几点删了一台生产服务器"这条记录必须能查到。没有审计日志,出了问题只能猜是谁干的。

一、User——用户

python
# backend/app/models.py
from sqlalchemy import String, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column
from datetime import datetime


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
    hashed_password: Mapped[str] = mapped_column(String(200))
    role: Mapped[str] = mapped_column(String(20), default="viewer")  # admin / operator / viewer
    created_at: Mapped[datetime] = mapped_column(server_default=func.now())

role 用字符串存角色名,不用整数——"admin"3 可读得多,日志里一眼看懂。三种角色对应三种权限级别:admin(全权)、operator(能执行任务)、viewer(只读)。

unique=True 保证用户名不重复,index=True 给用户名加索引——按用户名查用户(登录时)走索引快。

二、Asset——资产

python
class Asset(Base):
    __tablename__ = "assets"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    hostname: Mapped[str] = mapped_column(String(100), index=True)
    ip: Mapped[str] = mapped_column(String(45), index=True)  # IPv6 最长 45 字符
    port: Mapped[int] = mapped_column(default=22)
    role: Mapped[str] = mapped_column(String(50))     # web / db / redis / ...
    env: Mapped[str] = mapped_column(String(20))      # dev / test / prod
    status: Mapped[str] = mapped_column(String(20), default="active")  # active / inactive
    remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
    created_at: Mapped[datetime] = mapped_column(server_default=func.now())

ipString(45) 而不是存成整数——IPv6 地址很长,而且运维场景下 IP 经常按字符串前缀匹配(192.168.%),存成整数反而不好查。

env 字段区分环境——列表页按环境筛选是高频操作("只看生产环境的资产"),后面接口会加 ?env=prod 过滤参数。

remark 是可选字段(nullable=True),运维备注信息,不填就是 NULL

三、Task——任务

任务是对资产执行的操作。用户发起任务,系统执行,记录结果。

python
class Task(Base):
    __tablename__ = "tasks"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    asset_id: Mapped[int] = mapped_column(ForeignKey("assets.id"), index=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
    action: Mapped[str] = mapped_column(String(50))
    # action 的值:check_port / restart_service / run_script / ...
    status: Mapped[str] = mapped_column(String(20), default="pending")
    # status 的值:pending / running / done / failed
    result: Mapped[str | None] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(server_default=func.now())
    completed_at: Mapped[datetime | None] = mapped_column(nullable=True)

两个外键:asset_id 指向 assets 表(对哪台机器操作),user_id 指向 users 表(谁发起的)。ForeignKey 建立表之间的关系——删资产时关联的任务怎么办,由外键的级联策略决定(默认禁止删除有任务的资产)。

status 是状态机:pending(刚创建)→ running(正在执行)→ done(成功)或 failed(失败)。前端轮询这个状态显示进度——跟 16 篇"提交任务 → 轮询状态"的模式一致。

result 存任务的输出(比如检查端口的返回、脚本的标准输出),用 Text 类型不限长度。

completed_at 在任务完成时填入——created_atcompleted_at 的差值就是任务执行耗时。

四、Event——事件(审计日志)

谁在什么时间对什么做了什么——审计追溯的基础。

python
class Event(Base):
    __tablename__ = "events"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
    action: Mapped[str] = mapped_column(String(50))
    # action 的值:login / create_asset / delete_asset / run_task / ...
    target_type: Mapped[str] = mapped_column(String(50), nullable=True)
    # target_type:asset / task / user(操作的对象类型)
    target_id: Mapped[int | None] = mapped_column(nullable=True)
    detail: Mapped[str | None] = mapped_column(String(500), nullable=True)
    created_at: Mapped[datetime] = mapped_column(server_default=func.now())

Event 是"只追加"的表——只有 INSERT,没有 UPDATEDELETE。审计日志不能改、不能删,这是合规要求。

target_typetarget_id 是"多态关联"——一个事件可能关联资产、任务或用户。用两个字段表示:target_type="asset", target_id=5 表示"操作了 ID 为 5 的资产"。不用外键——因为 target 可能是多种表,外键只能指向一张表。

五、模型关系

一个用户可以发起多个任务(User → Task 是一对多),一个资产可以被执行多个任务(Asset → Task 也是一对多)。用户的所有操作产生事件记录(User → Event 一对多)。

六、创建迁移

模型定义好后,用 Alembic 生成迁移脚本(13 篇讲的流程):

bash
cd backend
uv run alembic revision --autogenerate -m "create users assets tasks events"
uv run alembic upgrade head

四个表一次性建出来。后面模型有变化(加字段、改类型),再用 alembic revision --autogenerate 生成增量迁移——不用手动 ALTER TABLE