Files
WonderQ-Project/WonderQ-Admin/alembic/versions/0025_admin_rbac.py
duanshuwen 4e04feda5b feat(admin, miniapp): 优化数据库迁移逻辑与小程序图片资源处理
- 为后端Admin新增migration_compat工具,支持离线迁移时的基线资源检查,重构全部alembic迁移脚本适配离线模式,避免重复创建已存在的数据库表和字段
- 完善初始数据库迁移脚本,添加默认部门种子数据,新增ConciergeAdvisor详情字段从JSON到JSONB的类型转换迁移
- 小程序端新增图片资源解析工具resolveNetworkImage,实现本地静态资源到CDN的自动映射,添加图片加载容错降级机制
- 完善HeroSlide类型定义,新增actionLabel字段,优化首页轮播组件的文本渲染与错误处理逻辑
- 新增对应测试用例验证迁移路径正确性与前端数据处理逻辑
- 新增临时 brainstorm UI对比草稿与开发临时状态文件
2026-08-27 11:37:03 +08:00

190 lines
10 KiB
Python

"""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")