0002_add_restore_audit_action.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """增加文档恢复审计动作和回收站查询索引。
  2. Revision ID: 0002_add_restore_audit_action
  3. Revises: 0001_initial_schema
  4. Create Date: 2026-07-26
  5. """
  6. from __future__ import annotations
  7. from collections.abc import Sequence
  8. from alembic import op
  9. import sqlalchemy as sa
  10. revision: str = "0002_add_restore_audit_action"
  11. down_revision: str | None = "0001_initial_schema"
  12. branch_labels: str | Sequence[str] | None = None
  13. depends_on: str | Sequence[str] | None = None
  14. AUDIT_ACTIONS_V1 = (
  15. "LOGIN",
  16. "LOGOUT",
  17. "VIEW_DOCUMENT",
  18. "DOWNLOAD_DOCUMENT",
  19. "UPLOAD_DOCUMENT",
  20. "BATCH_IMPORT",
  21. "EDIT_DOCUMENT",
  22. "DELETE_DOCUMENT",
  23. "CREATE_CATEGORY",
  24. "EDIT_CATEGORY",
  25. "DELETE_CATEGORY",
  26. "CHANGE_PERMISSION",
  27. "BIND_ATTACHMENT",
  28. "UNBIND_ATTACHMENT",
  29. )
  30. RESTORE_ACTION = "RESTORE_DOCUMENT"
  31. AUDIT_CONSTRAINT = "audit_action_type"
  32. RECYCLE_INDEX = "ix_doc_document_recycle_deleted"
  33. def _check_sql(actions: tuple[str, ...]) -> str:
  34. values = ", ".join(f"'{action}'" for action in actions)
  35. return f"action_type IN ({values})"
  36. def _check_exists() -> bool:
  37. inspector = sa.inspect(op.get_bind())
  38. return any(
  39. item.get("name")
  40. in {AUDIT_CONSTRAINT, f"ck_sys_audit_log_{AUDIT_CONSTRAINT}"}
  41. for item in inspector.get_check_constraints("sys_audit_log")
  42. )
  43. def _index_exists() -> bool:
  44. inspector = sa.inspect(op.get_bind())
  45. return any(
  46. item.get("name") == RECYCLE_INDEX
  47. for item in inspector.get_indexes("doc_document")
  48. )
  49. def upgrade() -> None:
  50. if _check_exists():
  51. op.drop_constraint(
  52. AUDIT_CONSTRAINT,
  53. "sys_audit_log",
  54. type_="check",
  55. )
  56. op.create_check_constraint(
  57. AUDIT_CONSTRAINT,
  58. "sys_audit_log",
  59. _check_sql((*AUDIT_ACTIONS_V1, RESTORE_ACTION)),
  60. )
  61. if not _index_exists():
  62. op.create_index(
  63. RECYCLE_INDEX,
  64. "doc_document",
  65. ["is_deleted", "deleted_at", "id"],
  66. unique=False,
  67. )
  68. def downgrade() -> None:
  69. restore_count = op.get_bind().scalar(
  70. sa.text(
  71. "SELECT COUNT(*) FROM sys_audit_log "
  72. "WHERE action_type = :action_type"
  73. ),
  74. {"action_type": RESTORE_ACTION},
  75. )
  76. if restore_count:
  77. raise RuntimeError(
  78. "存在RESTORE_DOCUMENT历史审计,拒绝无损降级;"
  79. "请保留0002或先按数据治理流程处理"
  80. )
  81. if _index_exists():
  82. op.drop_index(RECYCLE_INDEX, table_name="doc_document")
  83. if _check_exists():
  84. op.drop_constraint(
  85. AUDIT_CONSTRAINT,
  86. "sys_audit_log",
  87. type_="check",
  88. )
  89. op.create_check_constraint(
  90. AUDIT_CONSTRAINT,
  91. "sys_audit_log",
  92. _check_sql(AUDIT_ACTIONS_V1),
  93. )