"""Add normalized administrator RBAC tables and bootstrap the current admin.""" from datetime import datetime, timezone from alembic import context, op import sqlalchemy as sa from sqlalchemy import inspect revision = "0025_admin_rbac" down_revision = "0024_vehicle_demand" branch_labels = None depends_on = None RBAC_TABLES = ( "AdminRole", "AdminMenu", "AdminDepartment", "AdminUserRole", "AdminRoleMenu", "AdminUserDepartment", "AdminRoleDepartment", ) DEFAULT_DEPT_ID = "00000000-0000-0000-0000-000000000001" SUPER_ROLE_ID = "00000000-0000-0000-0000-000000000002" MENU_ROWS = ( ("00000000-0000-0000-0000-000000000101", "仪表盘", "page", "/dashboard", "Dashboard", "admin:dashboard", 10), ("00000000-0000-0000-0000-000000000102", "首页配置", "page", "/home", "HomePage", "admin:site-config", 20), ("00000000-0000-0000-0000-000000000103", "玩法与详情", "page", "/play", "WanfaPage", "admin:wanfa:read", 30), ("00000000-0000-0000-0000-000000000104", "管家管理", "page", "/concierge", "ConciergePage", "admin:concierge:read", 40), ("00000000-0000-0000-0000-000000000105", "需求线索", "page", "/leads", "LeadsPage", "admin:leads:read", 50), ("00000000-0000-0000-0000-000000000106", "用户管理", "page", "/system/users", "SystemUsers", "system:user:read", 60), ("00000000-0000-0000-0000-000000000107", "角色管理", "page", "/system/roles", "SystemRoles", "system:role:read", 70), ("00000000-0000-0000-0000-000000000108", "菜单管理", "page", "/system/menus", "SystemMenus", "system:menu:read", 80), ("00000000-0000-0000-0000-000000000109", "部门管理", "page", "/system/depts", "SystemDepts", "system:dept:read", 90), ) def is_offline_mode() -> bool: try: return context.is_offline_mode() except (NameError, AttributeError): return False def _existing_table_names() -> set[str]: if is_offline_mode(): # The initial baseline creates the current RBAC tables before this # revision runs, but an offline connection cannot inspect them. return set(RBAC_TABLES) return set(inspect(op.get_bind()).get_table_names()) def _create_table_if_missing(existing_tables: set[str], table_name: str, *elements) -> None: if table_name in existing_tables: return op.create_table(table_name, *elements) existing_tables.add(table_name) def _table_has_rows(table_name: str) -> bool: if is_offline_mode(): return False return op.get_bind().execute(sa.text(f'SELECT 1 FROM "{table_name}" LIMIT 1')).first() is not None def upgrade() -> None: now = datetime.now(timezone.utc).replace(tzinfo=None) existing_tables = _existing_table_names() _create_table_if_missing( existing_tables, "AdminRole", sa.Column("id", sa.String(), nullable=False), sa.Column("name", sa.String(), nullable=False), sa.Column("code", sa.String(), nullable=False), sa.Column("dataScope", sa.String(), nullable=False, server_default="all"), sa.Column("description", sa.Text(), nullable=True), sa.Column("isActive", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("createdAt", sa.DateTime(), nullable=False), sa.Column("updatedAt", sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("code", name="uq_AdminRole_code"), ) _create_table_if_missing( existing_tables, "AdminDepartment", sa.Column("id", sa.String(), nullable=False), sa.Column("parentId", sa.String(), nullable=True), sa.Column("name", sa.String(), nullable=False), sa.Column("sortOrder", sa.Integer(), nullable=False, server_default="0"), sa.Column("isActive", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("createdAt", sa.DateTime(), nullable=False), sa.Column("updatedAt", sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(["parentId"], ["AdminDepartment.id"], ondelete="SET NULL"), sa.PrimaryKeyConstraint("id"), ) _create_table_if_missing( existing_tables, "AdminMenu", sa.Column("id", sa.String(), nullable=False), sa.Column("parentId", sa.String(), nullable=True), sa.Column("name", sa.String(), nullable=False), sa.Column("type", sa.String(), nullable=False, server_default="page"), sa.Column("path", sa.String(), nullable=True), sa.Column("componentKey", sa.String(), nullable=True), sa.Column("permissionCode", sa.String(), nullable=True), sa.Column("icon", sa.String(), nullable=True), sa.Column("sortOrder", sa.Integer(), nullable=False, server_default="0"), sa.Column("isVisible", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("isActive", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("createdAt", sa.DateTime(), nullable=False), sa.Column("updatedAt", sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(["parentId"], ["AdminMenu.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("permissionCode", name="uq_AdminMenu_permissionCode"), ) _create_table_if_missing( existing_tables, "AdminUserRole", sa.Column("userId", sa.String(), nullable=False), sa.Column("roleId", sa.String(), nullable=False), sa.Column("createdAt", sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(["roleId"], ["AdminRole.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["userId"], ["AdminUser.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("userId", "roleId"), ) _create_table_if_missing( existing_tables, "AdminRoleMenu", sa.Column("roleId", sa.String(), nullable=False), sa.Column("menuId", sa.String(), nullable=False), sa.Column("createdAt", sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(["menuId"], ["AdminMenu.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["roleId"], ["AdminRole.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("roleId", "menuId"), ) _create_table_if_missing( existing_tables, "AdminUserDepartment", sa.Column("userId", sa.String(), nullable=False), sa.Column("deptId", sa.String(), nullable=False), sa.Column("createdAt", sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(["deptId"], ["AdminDepartment.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["userId"], ["AdminUser.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("userId", "deptId"), ) _create_table_if_missing( existing_tables, "AdminRoleDepartment", sa.Column("roleId", sa.String(), nullable=False), sa.Column("deptId", sa.String(), nullable=False), sa.Column("createdAt", sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(["deptId"], ["AdminDepartment.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["roleId"], ["AdminRole.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("roleId", "deptId"), ) role_table = sa.table("AdminRole", sa.column("id", sa.String), sa.column("name", sa.String), sa.column("code", sa.String), sa.column("dataScope", sa.String), sa.column("isActive", sa.Boolean), sa.column("createdAt", sa.DateTime), sa.column("updatedAt", sa.DateTime)) dept_table = sa.table("AdminDepartment", sa.column("id", sa.String), sa.column("name", sa.String), sa.column("sortOrder", sa.Integer), sa.column("isActive", sa.Boolean), sa.column("createdAt", sa.DateTime), sa.column("updatedAt", sa.DateTime)) menu_table = sa.table("AdminMenu", sa.column("id", sa.String), sa.column("name", sa.String), sa.column("type", sa.String), sa.column("path", sa.String), sa.column("componentKey", sa.String), sa.column("permissionCode", sa.String), sa.column("sortOrder", sa.Integer), sa.column("isVisible", sa.Boolean), sa.column("isActive", sa.Boolean), sa.column("createdAt", sa.DateTime), sa.column("updatedAt", sa.DateTime)) if not _table_has_rows("AdminRole"): op.bulk_insert(role_table, [{"id": SUPER_ROLE_ID, "name": "超级管理员", "code": "super_admin", "dataScope": "all", "isActive": True, "createdAt": now, "updatedAt": now}]) if not _table_has_rows("AdminDepartment"): op.bulk_insert(dept_table, [{"id": DEFAULT_DEPT_ID, "name": "默认部门", "sortOrder": 0, "isActive": True, "createdAt": now, "updatedAt": now}]) if not _table_has_rows("AdminMenu"): op.bulk_insert( menu_table, [ {"id": menu_id, "name": name, "type": menu_type, "path": path, "componentKey": component, "permissionCode": permission, "sortOrder": sort_order, "isVisible": True, "isActive": True, "createdAt": now, "updatedAt": now} for menu_id, name, menu_type, path, component, permission, sort_order in MENU_ROWS ], ) op.execute(sa.text('INSERT INTO "AdminRoleMenu" ("roleId", "menuId", "createdAt") SELECT :role_id, "id", :created_at FROM "AdminMenu" ON CONFLICT DO NOTHING').bindparams(role_id=SUPER_ROLE_ID, created_at=now)) op.execute(sa.text('INSERT INTO "AdminRoleDepartment" ("roleId", "deptId", "createdAt") VALUES (:role_id, :dept_id, :created_at) ON CONFLICT DO NOTHING').bindparams(role_id=SUPER_ROLE_ID, dept_id=DEFAULT_DEPT_ID, created_at=now)) op.execute(sa.text('INSERT INTO "AdminUserRole" ("userId", "roleId", "createdAt") SELECT "id", :role_id, :created_at FROM "AdminUser" ON CONFLICT DO NOTHING').bindparams(role_id=SUPER_ROLE_ID, created_at=now)) op.execute(sa.text('INSERT INTO "AdminUserDepartment" ("userId", "deptId", "createdAt") SELECT "id", :dept_id, :created_at FROM "AdminUser" ON CONFLICT DO NOTHING').bindparams(dept_id=DEFAULT_DEPT_ID, created_at=now)) def downgrade() -> None: op.drop_table("AdminRoleDepartment") op.drop_table("AdminUserDepartment") op.drop_table("AdminRoleMenu") op.drop_table("AdminUserRole") op.drop_table("AdminMenu") op.drop_table("AdminDepartment") op.drop_table("AdminRole")