0006_document_organization.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. """文档归属部门:doc_document 新增 organization_id/organization_name。
  2. Revision ID: 0006_document_organization
  3. Revises: 0005_category_flat_mode
  4. Create Date: 2026-08-14
  5. 变更要点:
  6. 1. doc_document 新增 organization_id(外键 → sys_organization.id,RESTRICT)
  7. 与 organization_name(冗余字符串,便于检索/展示)。
  8. 2. 适用于主案/子案/附件三类文档;与创建者字段独立维护——
  9. 用户调岗不影响历史文档归属。
  10. 3. 历史数据回填:按 created_by → sys_user.organization_id 关联填充;
  11. 无创建者或创建者无部门的记录保持 NULL。
  12. 4. 新增联合索引 ix_doc_document_organization_active(organization_id, is_deleted)
  13. 支撑按部门过滤的列表查询。
  14. 5. 机构改名时由 organization_service.update_organization 级联刷新
  15. organization_name(与现有 User 冗余字段策略一致)。
  16. """
  17. from __future__ import annotations
  18. from collections.abc import Sequence
  19. from alembic import op
  20. import sqlalchemy as sa
  21. revision: str = "0006_document_organization"
  22. down_revision: str | None = "0005_category_flat_mode"
  23. branch_labels: str | Sequence[str] | None = None
  24. depends_on: str | Sequence[str] | None = None
  25. _TABLE = "doc_document"
  26. _INDEX = "ix_doc_document_organization_active"
  27. def _existing_columns(table: str) -> set[str]:
  28. inspector = sa.inspect(op.get_bind())
  29. return {item["name"] for item in inspector.get_columns(table)}
  30. def _existing_index_names(table: str) -> set[str]:
  31. inspector = sa.inspect(op.get_bind())
  32. return {item["name"] for item in inspector.get_indexes(table)}
  33. def upgrade() -> None:
  34. columns = _existing_columns(_TABLE)
  35. if "organization_id" not in columns:
  36. op.add_column(
  37. _TABLE,
  38. sa.Column(
  39. "organization_id",
  40. sa.BigInteger(),
  41. nullable=True,
  42. ),
  43. )
  44. op.create_foreign_key(
  45. "fk_doc_document_organization_id_sys_organization",
  46. _TABLE,
  47. "sys_organization",
  48. ["organization_id"],
  49. ["id"],
  50. ondelete="RESTRICT",
  51. )
  52. if "organization_name" not in columns:
  53. op.add_column(
  54. _TABLE,
  55. sa.Column(
  56. "organization_name",
  57. sa.String(length=128),
  58. nullable=True,
  59. ),
  60. )
  61. # 历史回填:按创建者当前部门填充;剩余保持 NULL
  62. op.execute(
  63. sa.text(
  64. "UPDATE doc_document d "
  65. "JOIN sys_user u ON d.created_by = u.id "
  66. "SET d.organization_id = u.organization_id, "
  67. " d.organization_name = u.organization_name "
  68. "WHERE d.organization_id IS NULL "
  69. " AND u.organization_id IS NOT NULL"
  70. )
  71. )
  72. indexes = _existing_index_names(_TABLE)
  73. if _INDEX not in indexes:
  74. op.create_index(
  75. _INDEX,
  76. _TABLE,
  77. ["organization_id", "is_deleted"],
  78. unique=False,
  79. )
  80. def downgrade() -> None:
  81. indexes = _existing_index_names(_TABLE)
  82. if _INDEX in indexes:
  83. op.drop_index(_INDEX, table_name=_TABLE)
  84. # 外键存在性检测:MySQL 8 下 fk 名稳定,按名清理
  85. bind = op.get_bind()
  86. fk_rows = bind.execute(
  87. sa.text(
  88. "SELECT CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE "
  89. "WHERE TABLE_SCHEMA = DATABASE() "
  90. " AND TABLE_NAME = 'doc_document' "
  91. " AND COLUMN_NAME = 'organization_id' "
  92. " AND REFERENCED_TABLE_NAME = 'sys_organization'"
  93. )
  94. ).fetchall()
  95. for row in fk_rows:
  96. op.drop_constraint(row[0], _TABLE, type_="foreignkey")
  97. columns = _existing_columns(_TABLE)
  98. if "organization_name" in columns:
  99. op.drop_column(_TABLE, "organization_name")
  100. if "organization_id" in columns:
  101. op.drop_column(_TABLE, "organization_id")