84 lines
3.8 KiB
Python
84 lines
3.8 KiB
Python
|
|
"""操作审计日志模型
|
|||
|
|
|
|||
|
|
设计参考 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"<AuditLog {self.action} {self.module} by {self.user_id}>"
|