"""Q3业务规则对齐、角色收敛和正文检索字段。 Revision ID: 0003_q3_business_alignment_and_content_search Revises: 0002_add_restore_audit_action Create Date: 2026-08-07 """ from __future__ import annotations from collections.abc import Sequence from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import mysql revision: str = "0003_q3_business_alignment_and_content_search" down_revision: str | None = "0002_add_restore_audit_action" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None ROLE_CONSTRAINT = "user_role_code" CONTENT_STATUS_CONSTRAINT = "document_content_extract_status" def upgrade() -> None: # 本阶段按契约使用可读revision名,长度超过Alembic默认VARCHAR(32)。 op.alter_column( "alembic_version", "version_num", existing_type=sa.String(length=32), type_=sa.String(length=128), existing_nullable=False, ) op.add_column( "doc_document", sa.Column("content_text", mysql.LONGTEXT(), nullable=True), ) op.add_column( "doc_document", sa.Column( "content_extract_status", sa.String(length=32), server_default=sa.text("'UNSUPPORTED'"), nullable=False, ), ) op.add_column( "doc_document", sa.Column("content_extracted_at", mysql.DATETIME(fsp=3), nullable=True), ) op.alter_column( "doc_document", "search_text", existing_type=sa.Text(), type_=mysql.LONGTEXT(), existing_nullable=True, ) op.create_check_constraint( CONTENT_STATUS_CONSTRAINT, "doc_document", "content_extract_status IN " "('SUCCESS', 'EMPTY', 'UNSUPPORTED', 'FAILED')", ) # 历史审计员必须先使全部旧Token失效,再禁用并逻辑删除;绝不升级为管理员。 op.execute( sa.text( "UPDATE sys_user SET " "auth_version = auth_version + 1, " "row_version = row_version + 1, " "status = 'DISABLED', " "is_deleted = 1, " "deleted_at = COALESCE(deleted_at, CURRENT_TIMESTAMP(3)), " "updated_at = CURRENT_TIMESTAMP(3), " "role_code = 'USER' " "WHERE role_code = 'AUDITOR'" ) ) op.drop_constraint(ROLE_CONSTRAINT, "sys_user", type_="check") op.create_check_constraint( ROLE_CONSTRAINT, "sys_user", "role_code IN ('USER', 'ADMIN')", ) def downgrade() -> None: bind = op.get_bind() extracted_count = bind.scalar( sa.text( "SELECT COUNT(*) FROM doc_document WHERE " "content_text IS NOT NULL OR content_extracted_at IS NOT NULL OR " "content_extract_status <> 'UNSUPPORTED'" ) ) oversized_search_count = bind.scalar( sa.text( "SELECT COUNT(*) FROM doc_document " "WHERE OCTET_LENGTH(search_text) > 65535" ) ) if extracted_count or oversized_search_count: raise RuntimeError( "0003已包含正文提取结果或超出TEXT容量的检索文本,拒绝有损降级;" "请先备份并按数据治理流程处理" ) op.drop_constraint(ROLE_CONSTRAINT, "sys_user", type_="check") op.create_check_constraint( ROLE_CONSTRAINT, "sys_user", "role_code IN ('USER', 'ADMIN', 'AUDITOR')", ) # 已失效的历史AUDITOR不会被自动复活或重新授权。 op.drop_constraint( CONTENT_STATUS_CONSTRAINT, "doc_document", type_="check", ) op.alter_column( "doc_document", "search_text", existing_type=mysql.LONGTEXT(), type_=sa.Text(), existing_nullable=True, ) op.drop_column("doc_document", "content_extracted_at") op.drop_column("doc_document", "content_extract_status") op.drop_column("doc_document", "content_text")