Appearance
数据模型设计
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())ip 用 String(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_at 到 completed_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,没有 UPDATE 和 DELETE。审计日志不能改、不能删,这是合规要求。
target_type 和 target_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。