"""audit_logs 表 — 高危操作/登录/MFA 审计日志 Revision ID: 027_audit_logs Revises: 026_drop_agent_otp_legacy Create Date: 2026-06-22 (v0.7.1) v0.7.1 task #89 实施,配合 RBAC 5 角色的 audit_log 资源(给 auditor 角色只读用) 字段: - id: UUID 主键 - employee_id: 操作人(企微 UserID / 'system') - action: 操作类型 - resource: 目标资源类型 - resource_id: 目标资源 ID - details: JSON 详细上下文 - result: success / failure / partial - ip_address: 来源 IP - user_agent: 来源 UA - created_at: 时间 索引: - idx_audit_employee_id: 按操作人查 - idx_audit_action: 按操作类型查 - idx_audit_resource: 按资源类型+ID 查 - idx_audit_created_at: 按时间范围查(默认倒序) """ from alembic import op import sqlalchemy as sa # revision identifiers revision = '027_audit_logs' down_revision = '026_drop_agent_otp_legacy' branch_labels = None depends_on = None def upgrade() -> None: """建 audit_logs 表 + 索引。""" bind = op.get_bind() inspector = sa.inspect(bind) if not inspector.has_table('audit_logs'): op.create_table( 'audit_logs', sa.Column('id', sa.String(36), primary_key=True), sa.Column('employee_id', sa.String(100), nullable=False, comment='操作人(employee_id / system)'), sa.Column('action', sa.String(50), nullable=False, comment='操作类型'), sa.Column('resource', sa.String(50), nullable=False, comment='目标资源类型'), sa.Column('resource_id', sa.String(100), nullable=True, comment='目标资源 ID'), sa.Column('details', sa.JSON, nullable=True, comment='详细上下文(JSON)'), sa.Column('result', sa.String(20), nullable=False, server_default='success', comment='执行结果'), sa.Column('ip_address', sa.String(64), nullable=True, comment='来源 IP'), sa.Column('user_agent', sa.Text, nullable=True, comment='来源 User-Agent'), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='时间'), ) # 4 个索引 (IF NOT EXISTS 兼容) op.execute("CREATE INDEX IF NOT EXISTS idx_audit_employee_id ON audit_logs (employee_id)") op.execute("CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs (action)") op.execute("CREATE INDEX IF NOT EXISTS idx_audit_resource ON audit_logs (resource, resource_id)") op.execute("CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs (created_at)") def downgrade() -> None: """删 audit_logs 表(顺序: 删索引 → 删表)。""" op.execute("DROP INDEX IF EXISTS idx_audit_created_at") op.execute("DROP INDEX IF EXISTS idx_audit_resource") op.execute("DROP INDEX IF EXISTS idx_audit_action") op.execute("DROP INDEX IF EXISTS idx_audit_employee_id") op.execute("DROP TABLE IF EXISTS audit_logs")