From 65fecf74e83221e4921ef25eef04fe59a318d08d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 29 Jan 2026 10:45:50 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E8=8F=9C=E5=8D=95RBAC=20Portal=E6=8B=86=E5=88=86=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...后台菜单RBAC_Portal拆分与套餐白名单设计.md | 118 ++++++++++++++++++ Document/README.md | 9 ++ 2 files changed, 127 insertions(+) create mode 100644 Document/17_后台菜单RBAC_Portal拆分与套餐白名单设计.md diff --git a/Document/17_后台菜单RBAC_Portal拆分与套餐白名单设计.md b/Document/17_后台菜单RBAC_Portal拆分与套餐白名单设计.md new file mode 100644 index 0000000..9c09929 --- /dev/null +++ b/Document/17_后台菜单RBAC_Portal拆分与套餐白名单设计.md @@ -0,0 +1,118 @@ +# 后台菜单 & RBAC:Portal 拆分与套餐权限白名单设计 + +> 目标:在**不引入任何兼容/兜底逻辑**的前提下,将“管理端(Admin) / 租户端(Tenant)”的后台菜单与 RBAC 权限体系彻底隔离,同时支持“租户后台菜单按套餐固定”的能力控制。 + +## 1. 总体原则(强约束) +1. **权限码全局唯一**:权限 `Code` 作为系统全局唯一标识,不再按租户重复维护。 +2. **菜单固定**:后台菜单定义固定,不允许运行期在管理端新增/修改菜单结构(只允许代码/种子变更)。 +3. **租户菜单按套餐固定**:租户套餐决定“可用功能集合(权限白名单)”,再叠加租户内 RBAC 进行二次裁剪。 +4. **无兜底**: + - 不允许“缺字段时使用备用字段”的回退行为(例如菜单 RequiredPermissions 缺失就回退 MetaPermissions)。 + - 不允许“租户为空就自动用默认租户”等默认化行为。 + +--- + +## 2. 数据模型(推荐落库形态) + +### 2.1 Identity 库(takeout_identity_db) + +#### 2.1.1 `permissions`(全局权限定义) +- **定位**:全局权限字典(权限码唯一)。 +- **变化**: + - 移除 `TenantId` 维度(不再按租户存权限定义)。 + - 新增 `Portal`(int,枚举:`0=Admin`,`1=Tenant`)。 + - `Code` 建唯一索引(`UNIQUE(Code)`)。 + +#### 2.1.2 `menu_definitions`(后台菜单定义) +- **定位**:菜单树定义(仅菜单结构与元数据),按 Portal 分两套:Admin / Tenant。 +- **新增字段**: + - `Portal`(int,枚举:`0=Admin`,`1=Tenant`)。 +- **变化**: + - 移除 `TenantId`(菜单不按租户维护)。 + - 新增索引:`(Portal, ParentId, SortOrder)`。 +- **可见性规则**: + - `RequiredPermissions` 为空:菜单默认可见。 + - `RequiredPermissions` 非空:只要命中“权限集合”即可可见(Any 语义)。 + +#### 2.1.3 `roles / user_roles / role_permissions / identity_users` +- **定位**:RBAC 核心表;支持同表存 Admin 与 Tenant 两套体系。 +- **新增字段**: + - `Portal`(int,`0=Admin`,`1=Tenant`)。 +- **变化**: + - `TenantId` 改为可空: + - `Portal=Admin`:`TenantId IS NULL`(平台侧账号/角色不绑定租户)。 + - `Portal=Tenant`:`TenantId 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`(套餐权限白名单) +- **定位**:定义“某套餐允许哪些权限码”。 +- **字段建议**: + - `id`(bigint,雪花) + - `tenant_package_id`(bigint,引用 `tenant_packages.id`) + - `permission_code`(varchar,存权限码) + - 审计字段(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_permissions` 的 `TenantId` 改为可空。 +3. **回填 Portal**(示例规则,避免人工配置): + - `roles`:`Code='PlatformAdmin'` => `Portal=Admin`,其余 `Portal=Tenant`。 + - `identity_users`:凡是绑定了 `PlatformAdmin` 角色的用户 => `Portal=Admin`,其余 `Portal=Tenant`。 + - `user_roles/role_permissions`:按关联 `roles.Portal` 回填 `Portal`。 +4. **回填 TenantId**: + - `Portal=Admin` 的 `roles/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:删除新表即可(若无业务写入)。 diff --git a/Document/README.md b/Document/README.md index d929a4a..6535266 100644 --- a/Document/README.md +++ b/Document/README.md @@ -143,6 +143,15 @@ --- +### 10. [后台菜单 & RBAC:Portal 拆分与套餐权限白名单设计](17_后台菜单RBAC_Portal拆分与套餐白名单设计.md) +- Admin/Tenant 两套后台菜单隔离 +- 权限码全局唯一与 RBAC 同表 Portal 化 +- 套餐权限白名单与菜单过滤链路 + +**适合人群**:架构师、后端开发人员、DevOps + +--- + ## 🚀 快速导航 ### 我是新人,从哪里开始?