audit_log.py 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  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 不设 CHECK 白名单(0035 移除):审计动作是持续扩展的枚举,
  11. # DB 约束导致新动作写入失败并连带业务操作 500,合法性由应用层字典约束;
  12. # 前端字典无匹配时回退显示英文原码。
  13. __table_args__ = (
  14. CheckConstraint(
  15. "target_type IS NULL OR target_type IN ("
  16. "'AUTH', 'CATEGORY', 'DOCUMENT', 'ATTACHMENT', "
  17. "'PERMISSION', 'ATTACHMENT_BINDING', 'ORGANIZATION', 'USER', "
  18. "'ROLE', 'IP_ENTRY', 'BACKUP'"
  19. ")",
  20. name="audit_target_type",
  21. ),
  22. CheckConstraint(
  23. "operation_result IN ('SUCCESS', 'FAILURE')",
  24. name="audit_operation_result",
  25. ),
  26. Index("ix_sys_audit_log_created_at", "created_at"),
  27. Index("ix_sys_audit_log_user_created", "user_id", "created_at"),
  28. Index(
  29. "ix_sys_audit_log_organization_created",
  30. "organization_id",
  31. "created_at",
  32. ),
  33. Index("ix_sys_audit_log_action_created", "action_type", "created_at"),
  34. Index(
  35. "ix_sys_audit_log_target_created",
  36. "target_type",
  37. "target_id",
  38. "created_at",
  39. ),
  40. Index(
  41. "ix_sys_audit_log_result_created",
  42. "operation_result",
  43. "created_at",
  44. ),
  45. Index("ix_sys_audit_log_request_id", "request_id"),
  46. )
  47. id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
  48. user_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
  49. username: Mapped[str | None] = mapped_column(String(64), nullable=True)
  50. real_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
  51. organization_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
  52. organization_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
  53. action_type: Mapped[str] = mapped_column(String(32), nullable=False)
  54. target_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
  55. target_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
  56. target_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
  57. operation_result: Mapped[str] = mapped_column(String(20), nullable=False)
  58. failure_reason: Mapped[str | None] = mapped_column(String(1000), nullable=True)
  59. client_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
  60. user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True)
  61. request_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
  62. operation_detail: Mapped[dict[str, object] | None] = mapped_column(
  63. JSON,
  64. nullable=True,
  65. )
  66. created_at: Mapped[datetime] = mapped_column(
  67. DATETIME(fsp=3),
  68. nullable=False,
  69. # 应用侧 UTC 落库:server_default CURRENT_TIMESTAMP 为服务器本地时区,
  70. # 与全系统 UTC 口径(序列化追加 Z)不一致会导致前端显示快 8 小时
  71. default=lambda: datetime.now(timezone.utc).replace(tzinfo=None),
  72. server_default=text("CURRENT_TIMESTAMP(3)"),
  73. )