"""新增操作日志表 Revision ID: a45a8087831b Revises: 6165691326d4 Create Date: 2026-10-10 10:06:17.435697 """ from collections.abc import Sequence from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import mysql # revision identifiers, used by Alembic. revision: str = "a45a8087831b" down_revision: str | Sequence[str] | None = "6165691326d4" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: """应用迁移。""" # ### commands auto generated by Alembic - please adjust! ### op.create_table( "operation_logs", sa.Column( "id", mysql.BIGINT(unsigned=True), autoincrement=True, nullable=False, comment="主键ID" ), sa.Column( "user_id", mysql.BIGINT(unsigned=True), nullable=True, comment="操作人ID(账号删除后置空)", ), sa.Column( "username", sa.String(length=64), nullable=False, comment="操作人用户名(冗余存储便于追溯)", ), sa.Column("module", sa.String(length=64), nullable=False, comment="所属模块,如 用户管理"), sa.Column( "action", sa.String(length=32), nullable=False, comment="操作类型:新增/修改/删除/导出" ), sa.Column( "summary", sa.String(length=255), nullable=False, comment="操作摘要(中文,供人阅读)" ), sa.Column("method", sa.String(length=8), nullable=True, comment="HTTP 方法"), sa.Column("path", sa.String(length=255), nullable=True, comment="请求路径"), sa.Column("ip", sa.String(length=45), nullable=True, comment="客户端IP(45位兼容IPv6)"), sa.Column( "user_agent", sa.String(length=255), nullable=True, comment="客户端User-Agent(截断存储)", ), sa.Column("request_data", sa.Text(), nullable=True, comment="请求参数(已脱敏)"), sa.Column( "success", mysql.TINYINT(), server_default="0", nullable=False, comment="是否成功:1成功 0失败", ), sa.Column( "error_message", sa.String(length=500), nullable=True, comment="失败原因(写入方须截断)", ), sa.Column("duration_ms", sa.Integer(), nullable=True, comment="处理耗时(毫秒)"), sa.Column( "create_time", sa.DateTime(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False, comment="创建时间", ), sa.ForeignKeyConstraint( ["user_id"], ["users.id"], name=op.f("fk_operation_logs_user_id"), ondelete="SET NULL" ), sa.PrimaryKeyConstraint("id", name=op.f("pk_operation_logs")), comment="操作日志表", mysql_charset="utf8mb4", mysql_collate="utf8mb4_general_ci", mysql_engine="InnoDB", ) op.create_index( "idx_operation_logs_create_time", "operation_logs", ["create_time"], unique=False ) op.create_index("idx_operation_logs_module", "operation_logs", ["module"], unique=False) op.create_index("idx_operation_logs_user_id", "operation_logs", ["user_id"], unique=False) op.create_index("idx_operation_logs_username", "operation_logs", ["username"], unique=False) # ### end Alembic commands ### def downgrade() -> None: """回滚迁移。 **注意(autogenerate 不会替你处理)**:生成的 `op.drop_index` 与 `op.drop_table` 顺序需人工检查 —— 若某索引是外键的唯一支撑索引, 单独 drop_index 会报 MySQL 1553(needed in a foreign key constraint)。 **整表回滚应只保留 `op.drop_table`**,DROP TABLE 会连带删除索引与外键。 """ # 整表回滚只保留 drop_table —— DROP TABLE 会连带删除索引与外键。 # 逐个 drop_index 会失败:idx_operation_logs_user_id 是外键 # fk_operation_logs_user_id 的唯一支撑索引,MySQL 拒绝删除(errno 1553)。 op.drop_table("operation_logs")