0021_remove_assigned_scope.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. """移除"可访问部门"维度:ASSIGNED 矩阵行删除 + sys_role_dept 删表。
  2. Revision ID: 0021_remove_assigned_scope
  3. Revises: 0020_sub_plan_to_attachment
  4. Create Date: 2026-08-31
  5. 变更要点:
  6. 1. 部门范围由 OWN/ASSIGNED/OTHER 收敛为 OWN/OTHER,"其他部门"涵盖
  7. 所属部门子树之外的全部部门(含无归属文档);
  8. 2. sys_role_permission 中全部 dept_scope='ASSIGNED' 行物理删除(含软删行),
  9. 存量"可访问部门权限"数据按需求清除;
  10. 3. dept_scope CHECK 约束换名收敛:旧名 ck_sys_role_permission_role_permission_dept_scope
  11. DROP IF EXISTS,新名 ck_sys_role_permission_dept_scope_v2 ADD IF NOT EXISTS
  12. (按名幂等,与 Flyway V6 一致);
  13. 4. sys_role_dept 角色可访问部门表 DROP IF EXISTS;
  14. 5. 降级:重建 sys_role_dept 空表、恢复旧 CHECK,并为存活角色补种
  15. ASSIGNED 默认全否行以还原 15 行矩阵不变量(原有勾选数据不可恢复)。
  16. """
  17. from __future__ import annotations
  18. from collections.abc import Sequence
  19. import sqlalchemy as sa
  20. from alembic import op
  21. from sqlalchemy.dialects import mysql
  22. revision: str = "0021_remove_assigned_scope"
  23. down_revision: str | None = "0020_sub_plan_to_attachment"
  24. branch_labels: str | Sequence[str] | None = None
  25. depends_on: str | Sequence[str] | None = None
  26. _OLD_CHECK = "ck_sys_role_permission_role_permission_dept_scope"
  27. _NEW_CHECK = "ck_sys_role_permission_dept_scope_v2"
  28. _NEW_CHECK_DDL = (
  29. "ALTER TABLE sys_role_permission ADD CONSTRAINT " + _NEW_CHECK + " "
  30. "CHECK (dept_scope IN ('OWN', 'OTHER'))"
  31. )
  32. _OLD_CHECK_DDL = (
  33. "ALTER TABLE sys_role_permission ADD CONSTRAINT " + _OLD_CHECK + " "
  34. "CHECK (dept_scope IN ('OWN', 'ASSIGNED', 'OTHER'))"
  35. )
  36. def _constraint_exists(name: str) -> bool:
  37. bind = op.get_bind()
  38. return bool(
  39. bind.scalar(
  40. sa.text(
  41. "SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS "
  42. "WHERE TABLE_SCHEMA = DATABASE() "
  43. "AND TABLE_NAME = 'sys_role_permission' AND CONSTRAINT_NAME = :name"
  44. ),
  45. {"name": name},
  46. )
  47. )
  48. def _table_exists(name: str) -> bool:
  49. bind = op.get_bind()
  50. return bool(
  51. bind.scalar(
  52. sa.text(
  53. "SELECT COUNT(*) FROM information_schema.TABLES "
  54. "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :name"
  55. ),
  56. {"name": name},
  57. )
  58. )
  59. def _run(statement: str) -> None:
  60. op.get_bind().execute(sa.text(statement))
  61. def _create_role_dept_table() -> None:
  62. op.create_table(
  63. "sys_role_dept",
  64. sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
  65. sa.Column("created_by", sa.BigInteger(), nullable=True),
  66. sa.Column("updated_by", sa.BigInteger(), nullable=True),
  67. sa.Column(
  68. "created_at",
  69. mysql.DATETIME(fsp=3),
  70. server_default=sa.text("CURRENT_TIMESTAMP(3)"),
  71. nullable=False,
  72. ),
  73. sa.Column(
  74. "updated_at",
  75. mysql.DATETIME(fsp=3),
  76. server_default=sa.text("CURRENT_TIMESTAMP(3)"),
  77. nullable=False,
  78. ),
  79. sa.Column("deleted_at", mysql.DATETIME(fsp=3), nullable=True),
  80. sa.Column(
  81. "is_deleted",
  82. sa.Boolean(),
  83. server_default=sa.text("0"),
  84. nullable=False,
  85. ),
  86. sa.Column(
  87. "row_version",
  88. sa.Integer(),
  89. server_default=sa.text("0"),
  90. nullable=False,
  91. ),
  92. sa.Column("role_id", sa.BigInteger(), nullable=False),
  93. sa.Column("organization_id", sa.BigInteger(), nullable=False),
  94. sa.Column(
  95. "active_marker",
  96. sa.Integer(),
  97. sa.Computed("IF(is_deleted = 0, 1, NULL)", persisted=False),
  98. nullable=True,
  99. ),
  100. sa.ForeignKeyConstraint(
  101. ["role_id"],
  102. ["sys_role.id"],
  103. name="fk_sys_role_dept_role_id_sys_role",
  104. ondelete="RESTRICT",
  105. ),
  106. sa.ForeignKeyConstraint(
  107. ["organization_id"],
  108. ["sys_organization.id"],
  109. name="fk_sys_role_dept_organization_id_sys_organization",
  110. ondelete="RESTRICT",
  111. ),
  112. sa.UniqueConstraint(
  113. "role_id",
  114. "organization_id",
  115. "active_marker",
  116. name="uq_sys_role_dept_active",
  117. ),
  118. sa.PrimaryKeyConstraint("id"),
  119. )
  120. op.create_index(
  121. "ix_sys_role_dept_role_active",
  122. "sys_role_dept",
  123. ["role_id", "is_deleted"],
  124. )
  125. op.create_index(
  126. "ix_sys_role_dept_org_active",
  127. "sys_role_dept",
  128. ["organization_id", "is_deleted"],
  129. )
  130. def upgrade() -> None:
  131. # 1) 存量 ASSIGNED 权限行物理删除(含软删行;新 CHECK 前置条件)
  132. _run("DELETE FROM sys_role_permission WHERE dept_scope = 'ASSIGNED'")
  133. # 2) CHECK 约束收敛为 OWN/OTHER(按名幂等)
  134. if _constraint_exists(_OLD_CHECK):
  135. _run(f"ALTER TABLE sys_role_permission DROP CHECK {_OLD_CHECK}")
  136. if not _constraint_exists(_NEW_CHECK):
  137. _run(_NEW_CHECK_DDL)
  138. # 3) 角色可访问部门表下线
  139. _run("DROP TABLE IF EXISTS sys_role_dept")
  140. def downgrade() -> None:
  141. # 1) 恢复旧 CHECK(先删新名,避免取值集合冲突)
  142. if _constraint_exists(_NEW_CHECK):
  143. _run(f"ALTER TABLE sys_role_permission DROP CHECK {_NEW_CHECK}")
  144. if not _constraint_exists(_OLD_CHECK):
  145. _run(_OLD_CHECK_DDL)
  146. # 2) 重建角色可访问部门空表(原数据已在升级时删除,不可恢复)
  147. if not _table_exists("sys_role_dept"):
  148. _create_role_dept_table()
  149. # 3) 为存活角色补种 ASSIGNED 默认全否行,还原"每角色 15 行"不变量
  150. _run(
  151. """
  152. INSERT INTO sys_role_permission
  153. (role_id, security_level, dept_scope, created_by, updated_by)
  154. SELECT r.id, lv.security_level, 'ASSIGNED', r.created_by, r.updated_by
  155. FROM sys_role r
  156. JOIN (
  157. SELECT 'PUBLIC' AS security_level
  158. UNION ALL SELECT 'INTERNAL'
  159. UNION ALL SELECT 'SECRET'
  160. UNION ALL SELECT 'CONFIDENTIAL'
  161. UNION ALL SELECT 'TOP_SECRET'
  162. ) lv
  163. WHERE r.is_deleted = 0
  164. AND NOT EXISTS (
  165. SELECT 1 FROM sys_role_permission p
  166. WHERE p.role_id = r.id
  167. AND p.security_level = lv.security_level
  168. AND p.dept_scope = 'ASSIGNED'
  169. )
  170. """
  171. )