"""操作审计日志模型 设计参考 MOM(KCGL) 的 audit_logs,但按 Track 的技术栈与诉求做了取舍: - 主键用 UUID(与库内其它表一致),而非 MOM 的自增 int。 - 增加 request_id:与 core/logging.py 的结构化日志打通 —— 凭一个 ID 就能把 「接口访问日志」和「审计记录」对上,排障时不用再猜。MOM 无此字段。 - 保留 module / action / target_* 的业务语义,使审计能按业务维度检索, 而不是只能按时间翻。 - 绝不记录请求体:登录等接口 body 含明文密码,一旦落库就成了长期泄露面。 与既有 task_logs 的分工:task_logs 是「任务流转轨迹」(有 task_id 非空约束, 只能挂在任务上,供流转树渲染);本表是「操作审计」,覆盖登录、导出、 产品增删改、权限变更等与单个任务无关的动作,且额外记录来源 IP / UA / 耗时结果。 """ import uuid from datetime import datetime from sqlalchemy import DateTime, Integer, String, Text from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base from app.core.time_utils import get_beijing_time class AuditLog(Base): __tablename__ = "audit_logs" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, ) # ---- 操作人(逻辑外键 → MOM sys_user,仅存账号,无物理约束)---- user_id: Mapped[str | None] = mapped_column( String(64), nullable=True, index=True, comment="操作人账号(逻辑外键→MOM)", ) display_name: Mapped[str | None] = mapped_column( String(100), nullable=True, comment="操作人显示名", ) role: Mapped[str | None] = mapped_column( String(50), nullable=True, comment="操作时角色快照", ) # ---- 业务语义 ---- action: Mapped[str] = mapped_column( String(50), nullable=False, index=True, comment="动作: create/update/delete/export/login/...", ) module: Mapped[str] = mapped_column( String(50), nullable=False, index=True, comment="业务模块: product/task/order/auth/print/...", ) target_type: Mapped[str | None] = mapped_column( String(50), nullable=True, comment="目标类型(表名或实体名)", ) target_id: Mapped[str | None] = mapped_column( String(100), nullable=True, index=True, comment="目标ID", ) target_name: Mapped[str | None] = mapped_column( String(200), nullable=True, comment="目标显示名(如产品身份证/工单号)", ) details: Mapped[dict | None] = mapped_column( JSONB, nullable=True, comment="变更详情 {old:{}, new:{}};禁止写入密码等敏感字段", ) # ---- 请求上下文(由中间件自动填充)---- ip_address: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="来源IP") user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True, comment="浏览器UA") method: Mapped[str | None] = mapped_column(String(10), nullable=True, comment="HTTP方法") url: Mapped[str | None] = mapped_column(String(500), nullable=True, comment="请求路径") status_code: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="响应状态码") error_message: Mapped[str | None] = mapped_column(Text, nullable=True, comment="错误信息(如有)") # ---- 与结构化日志对账用 ---- request_id: Mapped[str | None] = mapped_column( String(64), nullable=True, index=True, comment="关联 core/logging 的 request_id", ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=get_beijing_time, index=True, comment="操作时间", ) def __repr__(self) -> str: return f""