Files
TakeoutSaaS.Docs/Document/17_后台菜单RBAC_Portal拆分与套餐白名单设计.md

6.1 KiB
Raw Blame History

后台菜单 & RBACPortal 拆分与套餐权限白名单设计

目标:在不引入任何兼容/兜底逻辑的前提下,将“管理端(Admin) / 租户端(Tenant)”的后台菜单与 RBAC 权限体系彻底隔离,同时支持“租户后台菜单按套餐固定”的能力控制。

1. 总体原则(强约束)

  1. 权限码全局唯一:权限 Code 作为系统全局唯一标识,不再按租户重复维护。
  2. 菜单固定:后台菜单定义固定,不允许运行期在管理端新增/修改菜单结构(只允许代码/种子变更)。
  3. 租户菜单按套餐固定:租户套餐决定“可用功能集合(权限白名单)”,再叠加租户内 RBAC 进行二次裁剪。
  4. 无兜底
    • 不允许“缺字段时使用备用字段”的回退行为(例如菜单 RequiredPermissions 缺失就回退 MetaPermissions
    • 不允许“租户为空就自动用默认租户”等默认化行为。

2. 数据模型(推荐落库形态)

2.1 Identity 库takeout_identity_db

2.1.1 permissions(全局权限定义)

  • 定位:全局权限字典(权限码唯一)。
  • 变化
    • 移除 TenantId 维度(不再按租户存权限定义)。
    • 新增 Portalint枚举0=Admin1=Tenant)。
    • Code 建唯一索引(UNIQUE(Code))。

2.1.2 menu_definitions(后台菜单定义)

  • 定位:菜单树定义(仅菜单结构与元数据),按 Portal 分两套Admin / Tenant。
  • 新增字段
    • Portalint枚举0=Admin1=Tenant)。
  • 变化
    • 移除 TenantId(菜单不按租户维护)。
    • 新增索引:(Portal, ParentId, SortOrder)
  • 可见性规则
    • RequiredPermissions 为空:菜单默认可见。
    • RequiredPermissions 非空只要命中“权限集合”即可可见Any 语义)。

2.1.3 roles / user_roles / role_permissions / identity_users

  • 定位RBAC 核心表;支持同表存 Admin 与 Tenant 两套体系。
  • 新增字段
    • Portalint0=Admin1=Tenant)。
  • 变化
    • TenantId 改为可空:
      • Portal=AdminTenantId IS NULL(平台侧账号/角色不绑定租户)。
      • Portal=TenantTenantId IS NOT NULL(租户侧 RBAC 强制绑定租户)。
    • 建议加 CHECK 约束,彻底杜绝脏数据。
    • 索引建议(使用部分索引/过滤索引避免 NULL 唯一性漏洞):
      • roles
        • UNIQUE(Code) WHERE Portal=0
        • UNIQUE(TenantId, Code) WHERE Portal=1
      • identity_users
        • UNIQUE(Account) WHERE Portal=0
        • UNIQUE(TenantId, Account) WHERE Portal=1
        • UNIQUE(Email) WHERE Portal=0 AND Email IS NOT NULL
        • UNIQUE(TenantId, Email) WHERE Portal=1 AND Email IS NOT NULL
        • UNIQUE(Phone) WHERE Portal=0 AND Phone IS NOT NULL
        • UNIQUE(TenantId, Phone) WHERE Portal=1 AND Phone IS NOT NULL
      • user_roles
        • UNIQUE(UserId, RoleId) WHERE Portal=0
        • UNIQUE(TenantId, UserId, RoleId) WHERE Portal=1
      • role_permissions
        • UNIQUE(RoleId, PermissionId) WHERE Portal=0
        • UNIQUE(TenantId, RoleId, PermissionId) WHERE Portal=1

2.2 App 库takeout_app_db

2.2.1 新增 tenant_package_permission_codes(套餐权限白名单)

  • 定位:定义“某套餐允许哪些权限码”。
  • 字段建议
    • idbigint雪花
    • tenant_package_idbigint引用 tenant_packages.id
    • permission_codevarchar存权限码
    • 审计字段CreatedAt/UpdatedAt/DeletedAt/CreatedBy/UpdatedBy/DeletedBy
  • 唯一约束
    • UNIQUE(tenant_package_id, permission_code)

说明:权限定义与套餐在不同数据库时,不建议做跨库外键;以 permission_code 作为稳定契约即可(依赖 permissions.code 全局唯一)。


3. 迁移与回填策略(可直接落到 EF Migration

3.1 Identity 库迁移Portal & 去 TenantId

  1. 加列:给 identity_users/roles/user_roles/role_permissions/menu_definitions 增加 Portal(先允许 NULL
  2. 改列:将 identity_users/roles/user_roles/role_permissionsTenantId 改为可空。
  3. 回填 Portal(示例规则,避免人工配置):
    • rolesCode='PlatformAdmin' => Portal=Admin,其余 Portal=Tenant
    • identity_users:凡是绑定了 PlatformAdmin 角色的用户 => Portal=Admin,其余 Portal=Tenant
    • user_roles/role_permissions:按关联 roles.Portal 回填 Portal
  4. 回填 TenantId
    • Portal=Adminroles/user_roles/role_permissions/identity_users:将 TenantId 置空。
  5. 去租户列
    • permissions:删除 TenantId,新增 Portal,重建 UNIQUE(Code)
    • menu_definitions:删除 TenantId,新增索引 (Portal, ParentId, SortOrder)
  6. 约束与索引
    • Portal 改为 NOT NULL
    • 增加 CHECK 约束(见 2.1.3)。
    • 删除旧的 TenantId_* 唯一索引,建立新的过滤索引。

3.2 App 库迁移(套餐白名单表)

  1. 新建 tenant_package_permission_codes
  2. (可选)从旧的 FeaturePoliciesJson 或配置中一次性回填白名单(没有就先留空,表示该套餐无额外能力)。

4. 读取链路(租户后台菜单)

  1. 获取租户当前订阅tenant_subscriptions -> tenant_package_id
  2. 获取套餐白名单tenant_package_permission_codes -> permission_code[]
  3. 获取用户 RBAC 权限:由租户侧 user_roles + role_permissions + permissions 计算(或由 Token claim 携带)。
  4. 权限集合求交集Allowed = PackageWhitelist ∩ UserPermissions
  5. 加载菜单树menu_definitions where Portal=Tenant,按 Allowed 过滤并构建树。

5. 回滚策略(建议)

  • Identity迁移前先做数据库快照或导出回滚时以 migration down 还原结构,并按需要将 TenantId 回填回原表(仅在确认数据未被新逻辑写入时执行)。
  • App删除新表即可若无业务写入