0024_manage_permission_matrix.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. """管理权限矩阵化:sys_role 增加管理权限位,sys_user 移除旧管理权限列。
  2. Revision ID: 0024_manage_permission_matrix
  3. Revises: 0023_admin_permissions
  4. Create Date: 2026-09-03
  5. 变更要点:
  6. 1. sys_role 增加 manage_category_edit / manage_org_edit / manage_user_edit
  7. TINYINT(1) NOT NULL DEFAULT 0 与 managed_org_id BIGINT NULL:
  8. 管理权限改挂角色,判定读"生效角色"(用户显式角色,未挂角色回退默认角色);
  9. 2. 存量迁移:把活跃 ADMIN 用户旧有的 (三权限位, 锚点) 组合收敛到生效角色——
  10. 生效角色仅被一组占用时直接置位;被多组占用时克隆角色(复制权限矩阵 10 行,
  11. 命名追加 -M{n} 后缀)并回填该组用户 role_id;
  12. 3. sys_user 删除 perm_category_edit / perm_org_edit / perm_user_edit /
  13. managed_org_id 四列(判定改读生效角色,用户级配置下线);
  14. 4. 升级幂等:加列先探测;数据迁移以 sys_user.perm_category_edit 存在为前提,
  15. 列已删(已迁移)则整体跳过;中断重跑可续(已置位/已克隆的组自动跳过)。
  16. """
  17. from __future__ import annotations
  18. from collections.abc import Sequence
  19. import sqlalchemy as sa
  20. from alembic import op
  21. revision: str = "0024_manage_permission_matrix"
  22. down_revision: str | None = "0023_admin_permissions"
  23. branch_labels: str | Sequence[str] | None = None
  24. depends_on: str | Sequence[str] | None = None
  25. _MANAGE_FLAG_COLUMNS = (
  26. "manage_category_edit",
  27. "manage_org_edit",
  28. "manage_user_edit",
  29. )
  30. _LEGACY_FLAG_COLUMNS = (
  31. "perm_category_edit",
  32. "perm_org_edit",
  33. "perm_user_edit",
  34. )
  35. _NAME_SUFFIX_BUDGET = 61 # role_name 宽 64,预留 "-M" + 两位序号
  36. def _columns_of(table: str) -> set[str]:
  37. inspector = sa.inspect(op.get_bind())
  38. return {column["name"] for column in inspector.get_columns(table)}
  39. def _migrate_legacy_permissions(bind: sa.engine.Connection) -> None:
  40. """把 sys_user 旧管理位收敛到生效角色(置位或克隆),随后删旧列。"""
  41. default_role_id = bind.execute(
  42. sa.text(
  43. "SELECT id FROM sys_role WHERE is_default = 1 AND is_deleted = 0 "
  44. "AND status = 'ENABLED' ORDER BY sort_no ASC, id ASC"
  45. )
  46. ).scalar()
  47. role_rows = bind.execute(
  48. sa.text(
  49. "SELECT id, role_name, sort_no, status, remark, "
  50. "manage_category_edit, manage_org_edit, manage_user_edit, managed_org_id "
  51. "FROM sys_role WHERE is_deleted = 0"
  52. )
  53. ).mappings().all()
  54. roles = {row["id"]: dict(row) for row in role_rows}
  55. def effective_role_id(user: dict) -> int | None:
  56. role_id = user["role_id"]
  57. if role_id is not None:
  58. role = roles.get(role_id)
  59. if role is not None and role["status"] == "ENABLED":
  60. return role_id
  61. return default_role_id
  62. user_rows = bind.execute(
  63. sa.text(
  64. "SELECT id, role_id, perm_category_edit, perm_org_edit, "
  65. "perm_user_edit, managed_org_id FROM sys_user "
  66. "WHERE role_code = 'ADMIN' AND is_deleted = 0"
  67. )
  68. ).mappings().all()
  69. # 组键 = (生效角色, 三权限位, 锚点) → 组内用户 id 列表
  70. groups: dict[tuple[int, int, int, int, int | None], list[int]] = {}
  71. for row in user_rows:
  72. eff_id = effective_role_id(dict(row))
  73. if eff_id is None:
  74. continue
  75. key = (
  76. eff_id,
  77. int(bool(row["perm_category_edit"])),
  78. int(bool(row["perm_org_edit"])),
  79. int(bool(row["perm_user_edit"])),
  80. row["managed_org_id"],
  81. )
  82. groups.setdefault(key, []).append(row["id"])
  83. for (eff_id, cat, org, usr, anchor), member_ids in groups.items():
  84. role = roles[eff_id]
  85. current = (
  86. int(bool(role["manage_category_edit"])),
  87. int(bool(role["manage_org_edit"])),
  88. int(bool(role["manage_user_edit"])),
  89. role["managed_org_id"],
  90. )
  91. target = (cat, org, usr, anchor)
  92. if current == target:
  93. continue
  94. # 占用该生效角色的全部组数(含已一致的组):唯一占用直接置位,否则克隆
  95. occupancy = {key for key in groups if key[0] == eff_id}
  96. if len(occupancy) == 1:
  97. bind.execute(
  98. sa.text(
  99. "UPDATE sys_role SET manage_category_edit = :cat, "
  100. "manage_org_edit = :org, manage_user_edit = :usr, "
  101. "managed_org_id = :anchor, row_version = row_version + 1 "
  102. "WHERE id = :rid"
  103. ),
  104. {"cat": cat, "org": org, "usr": usr, "anchor": anchor, "rid": eff_id},
  105. )
  106. role.update(
  107. manage_category_edit=cat,
  108. manage_org_edit=org,
  109. manage_user_edit=usr,
  110. managed_org_id=anchor,
  111. )
  112. continue
  113. new_name = _free_clone_name(bind, role["role_name"])
  114. bind.execute(
  115. sa.text(
  116. "INSERT INTO sys_role (role_name, sort_no, status, remark, is_default, "
  117. "manage_category_edit, manage_org_edit, manage_user_edit, managed_org_id, "
  118. "row_version) VALUES (:name, :sort_no, 'ENABLED', :remark, 0, "
  119. ":cat, :org, :usr, :anchor, 0)"
  120. ),
  121. {
  122. "name": new_name,
  123. "sort_no": role["sort_no"],
  124. "remark": role["remark"],
  125. "cat": cat,
  126. "org": org,
  127. "usr": usr,
  128. "anchor": anchor,
  129. },
  130. )
  131. new_id = bind.execute(
  132. sa.text(
  133. "SELECT id FROM sys_role WHERE role_name = :name AND is_deleted = 0"
  134. ),
  135. {"name": new_name},
  136. ).scalar_one()
  137. bind.execute(
  138. sa.text(
  139. "INSERT INTO sys_role_permission (role_id, security_level, dept_scope, "
  140. "can_view, can_download, can_edit, can_delete, can_config_permission, "
  141. "can_bind_attachment, can_unbind_attachment) "
  142. "SELECT :new_id, security_level, dept_scope, can_view, can_download, "
  143. "can_edit, can_delete, can_config_permission, can_bind_attachment, "
  144. "can_unbind_attachment FROM sys_role_permission "
  145. "WHERE role_id = :old_id AND is_deleted = 0"
  146. ),
  147. {"new_id": new_id, "old_id": eff_id},
  148. )
  149. for user_id in member_ids:
  150. bind.execute(
  151. sa.text("UPDATE sys_user SET role_id = :nid WHERE id = :uid"),
  152. {"nid": new_id, "uid": user_id},
  153. )
  154. roles[new_id] = {
  155. "id": new_id,
  156. "role_name": new_name,
  157. "sort_no": role["sort_no"],
  158. "status": "ENABLED",
  159. "remark": role["remark"],
  160. "manage_category_edit": cat,
  161. "manage_org_edit": org,
  162. "manage_user_edit": usr,
  163. "managed_org_id": anchor,
  164. }
  165. def _free_clone_name(bind: sa.engine.Connection, base_name: str) -> str:
  166. """为克隆角色取空闲名:截断原名后追加 -M{n} 后缀,避开活跃角色重名。"""
  167. stem = base_name[:_NAME_SUFFIX_BUDGET]
  168. taken = {
  169. row[0]
  170. for row in bind.execute(
  171. sa.text("SELECT role_name FROM sys_role WHERE is_deleted = 0")
  172. )
  173. }
  174. suffix = 1
  175. while suffix < 100:
  176. candidate = f"{stem}-M{suffix}"
  177. if candidate not in taken:
  178. return candidate
  179. suffix += 1
  180. raise RuntimeError(f"无法为角色“{base_name}”生成空闲的克隆名")
  181. def upgrade() -> None:
  182. existing = _columns_of("sys_role")
  183. for name in _MANAGE_FLAG_COLUMNS:
  184. if name not in existing:
  185. op.add_column(
  186. "sys_role",
  187. sa.Column(
  188. name,
  189. sa.Boolean(),
  190. nullable=False,
  191. server_default=sa.text("0"),
  192. ),
  193. )
  194. if "managed_org_id" not in existing:
  195. op.add_column(
  196. "sys_role",
  197. sa.Column("managed_org_id", sa.BigInteger(), nullable=True),
  198. )
  199. user_columns = _columns_of("sys_user")
  200. if "perm_category_edit" in user_columns:
  201. _migrate_legacy_permissions(op.get_bind())
  202. user_columns = _columns_of("sys_user")
  203. if "managed_org_id" in user_columns:
  204. op.drop_column("sys_user", "managed_org_id")
  205. for name in _LEGACY_FLAG_COLUMNS:
  206. if name in user_columns:
  207. op.drop_column("sys_user", name)
  208. def downgrade() -> None:
  209. # 旧列为纯用户级配置,删列后无法从角色位反推原值,仅重建结构不恢复数据。
  210. user_columns = _columns_of("sys_user")
  211. for name in _LEGACY_FLAG_COLUMNS:
  212. if name not in user_columns:
  213. op.add_column(
  214. "sys_user",
  215. sa.Column(
  216. name,
  217. sa.Boolean(),
  218. nullable=False,
  219. server_default=sa.text("0"),
  220. ),
  221. )
  222. if "managed_org_id" not in user_columns:
  223. op.add_column(
  224. "sys_user",
  225. sa.Column("managed_org_id", sa.BigInteger(), nullable=True),
  226. )
  227. existing = _columns_of("sys_role")
  228. if "managed_org_id" in existing:
  229. op.drop_column("sys_role", "managed_org_id")
  230. for name in reversed(_MANAGE_FLAG_COLUMNS):
  231. if name in existing:
  232. op.drop_column("sys_role", name)