0003_q3_business_alignment_and_content_search.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. """Q3业务规则对齐、角色收敛和正文检索字段。
  2. Revision ID: 0003_q3_business_alignment_and_content_search
  3. Revises: 0002_add_restore_audit_action
  4. Create Date: 2026-08-07
  5. """
  6. from __future__ import annotations
  7. from collections.abc import Sequence
  8. from alembic import op
  9. import sqlalchemy as sa
  10. from sqlalchemy.dialects import mysql
  11. revision: str = "0003_q3_business_alignment_and_content_search"
  12. down_revision: str | None = "0002_add_restore_audit_action"
  13. branch_labels: str | Sequence[str] | None = None
  14. depends_on: str | Sequence[str] | None = None
  15. ROLE_CONSTRAINT = "user_role_code"
  16. CONTENT_STATUS_CONSTRAINT = "document_content_extract_status"
  17. def upgrade() -> None:
  18. # 本阶段按契约使用可读revision名,长度超过Alembic默认VARCHAR(32)。
  19. op.alter_column(
  20. "alembic_version",
  21. "version_num",
  22. existing_type=sa.String(length=32),
  23. type_=sa.String(length=128),
  24. existing_nullable=False,
  25. )
  26. op.add_column(
  27. "doc_document",
  28. sa.Column("content_text", mysql.LONGTEXT(), nullable=True),
  29. )
  30. op.add_column(
  31. "doc_document",
  32. sa.Column(
  33. "content_extract_status",
  34. sa.String(length=32),
  35. server_default=sa.text("'UNSUPPORTED'"),
  36. nullable=False,
  37. ),
  38. )
  39. op.add_column(
  40. "doc_document",
  41. sa.Column("content_extracted_at", mysql.DATETIME(fsp=3), nullable=True),
  42. )
  43. op.alter_column(
  44. "doc_document",
  45. "search_text",
  46. existing_type=sa.Text(),
  47. type_=mysql.LONGTEXT(),
  48. existing_nullable=True,
  49. )
  50. op.create_check_constraint(
  51. CONTENT_STATUS_CONSTRAINT,
  52. "doc_document",
  53. "content_extract_status IN "
  54. "('SUCCESS', 'EMPTY', 'UNSUPPORTED', 'FAILED')",
  55. )
  56. # 历史审计员必须先使全部旧Token失效,再禁用并逻辑删除;绝不升级为管理员。
  57. op.execute(
  58. sa.text(
  59. "UPDATE sys_user SET "
  60. "auth_version = auth_version + 1, "
  61. "row_version = row_version + 1, "
  62. "status = 'DISABLED', "
  63. "is_deleted = 1, "
  64. "deleted_at = COALESCE(deleted_at, CURRENT_TIMESTAMP(3)), "
  65. "updated_at = CURRENT_TIMESTAMP(3), "
  66. "role_code = 'USER' "
  67. "WHERE role_code = 'AUDITOR'"
  68. )
  69. )
  70. op.drop_constraint(ROLE_CONSTRAINT, "sys_user", type_="check")
  71. op.create_check_constraint(
  72. ROLE_CONSTRAINT,
  73. "sys_user",
  74. "role_code IN ('USER', 'ADMIN')",
  75. )
  76. def downgrade() -> None:
  77. bind = op.get_bind()
  78. extracted_count = bind.scalar(
  79. sa.text(
  80. "SELECT COUNT(*) FROM doc_document WHERE "
  81. "content_text IS NOT NULL OR content_extracted_at IS NOT NULL OR "
  82. "content_extract_status <> 'UNSUPPORTED'"
  83. )
  84. )
  85. oversized_search_count = bind.scalar(
  86. sa.text(
  87. "SELECT COUNT(*) FROM doc_document "
  88. "WHERE OCTET_LENGTH(search_text) > 65535"
  89. )
  90. )
  91. if extracted_count or oversized_search_count:
  92. raise RuntimeError(
  93. "0003已包含正文提取结果或超出TEXT容量的检索文本,拒绝有损降级;"
  94. "请先备份并按数据治理流程处理"
  95. )
  96. op.drop_constraint(ROLE_CONSTRAINT, "sys_user", type_="check")
  97. op.create_check_constraint(
  98. ROLE_CONSTRAINT,
  99. "sys_user",
  100. "role_code IN ('USER', 'ADMIN', 'AUDITOR')",
  101. )
  102. # 已失效的历史AUDITOR不会被自动复活或重新授权。
  103. op.drop_constraint(
  104. CONTENT_STATUS_CONSTRAINT,
  105. "doc_document",
  106. type_="check",
  107. )
  108. op.alter_column(
  109. "doc_document",
  110. "search_text",
  111. existing_type=mysql.LONGTEXT(),
  112. type_=sa.Text(),
  113. existing_nullable=True,
  114. )
  115. op.drop_column("doc_document", "content_extracted_at")
  116. op.drop_column("doc_document", "content_extract_status")
  117. op.drop_column("doc_document", "content_text")