audit_log.py 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. """追加写审计日志模型。"""
  2. from __future__ import annotations
  3. from datetime import datetime, timezone
  4. from sqlalchemy import BigInteger, CheckConstraint, Index, JSON, String, Text, text
  5. from sqlalchemy.dialects.mysql import DATETIME
  6. from sqlalchemy.orm import Mapped, mapped_column
  7. from dms.extensions import db
  8. class AuditLog(db.Model):
  9. __tablename__ = "sys_audit_log"
  10. # action_type / target_type 均不设 CHECK 白名单(0035 / 0037 移除):两者都是
  11. # 持续扩展的枚举(SYSTEM、BORROW_REQUEST 等相继加入),DB 约束导致新值写入
  12. # 失败并连带业务操作 500,合法性由应用层字典约束,前端字典无匹配时回退显示
  13. # 英文原码;operation_result 为 SUCCESS/FAILURE 二值稳定枚举,CHECK 保留。
  14. __table_args__ = (
  15. CheckConstraint(
  16. "operation_result IN ('SUCCESS', 'FAILURE')",
  17. name="audit_operation_result",
  18. ),
  19. Index("ix_sys_audit_log_created_at", "created_at"),
  20. Index("ix_sys_audit_log_user_created", "user_id", "created_at"),
  21. Index(
  22. "ix_sys_audit_log_organization_created",
  23. "organization_id",
  24. "created_at",
  25. ),
  26. Index("ix_sys_audit_log_action_created", "action_type", "created_at"),
  27. Index(
  28. "ix_sys_audit_log_target_created",
  29. "target_type",
  30. "target_id",
  31. "created_at",
  32. ),
  33. Index(
  34. "ix_sys_audit_log_result_created",
  35. "operation_result",
  36. "created_at",
  37. ),
  38. Index("ix_sys_audit_log_request_id", "request_id"),
  39. )
  40. id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
  41. user_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
  42. username: Mapped[str | None] = mapped_column(String(64), nullable=True)
  43. real_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
  44. organization_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
  45. organization_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
  46. action_type: Mapped[str] = mapped_column(String(32), nullable=False)
  47. target_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
  48. target_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
  49. target_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
  50. operation_result: Mapped[str] = mapped_column(String(20), nullable=False)
  51. failure_reason: Mapped[str | None] = mapped_column(String(1000), nullable=True)
  52. client_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
  53. user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True)
  54. request_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
  55. operation_detail: Mapped[dict[str, object] | None] = mapped_column(
  56. JSON,
  57. nullable=True,
  58. )
  59. created_at: Mapped[datetime] = mapped_column(
  60. DATETIME(fsp=3),
  61. nullable=False,
  62. # 应用侧 UTC 落库:server_default CURRENT_TIMESTAMP 为服务器本地时区,
  63. # 与全系统 UTC 口径(序列化追加 Z)不一致会导致前端显示快 8 小时
  64. default=lambda: datetime.now(timezone.utc).replace(tzinfo=None),
  65. server_default=text("CURRENT_TIMESTAMP(3)"),
  66. )