From d0b80d17e2b36a7ace0e13366b523f8ca14847c9 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Fri, 12 Dec 2025 08:19:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=A4=9A=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/多租户表隔离策略.md | 288 ++++++++++++++++++++++++ sql/mysql/tenant_add.sql | 7 +- src/main/resources/application-dev.yml | 1 + src/main/resources/application-prod.yml | 30 +++ 4 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 docs/多租户表隔离策略.md diff --git a/docs/多租户表隔离策略.md b/docs/多租户表隔离策略.md new file mode 100644 index 00000000..6b8e9c5f --- /dev/null +++ b/docs/多租户表隔离策略.md @@ -0,0 +1,288 @@ +# 多租户表隔离策略说明 + +## 📋 概述 + +本文档说明系统中各业务表的多租户隔离策略,帮助理解哪些表需要租户隔离,哪些表应该共享。 + +--- + +## 🎯 设计原则 + +### 1. **数据隔离**(Tenant Isolation) +- 租户私有数据必须严格隔离 +- 通过 `tenant_id` 字段实现 +- MyBatis-Plus 多租户插件自动添加过滤条件 + +### 2. **功能共享**(Feature Sharing) +- 系统功能定义应该标准化 +- 避免重复数据和维护成本 +- 通过角色和权限控制访问 + +### 3. **灵活配置**(Flexible Configuration) +- 通过配置文件控制隔离策略 +- 可随时调整隔离范围 +- 零成本切换单租户/多租户 + +--- + +## 📊 表隔离策略 + +### ✅ 需要租户隔离的表 + +这些表存储租户私有数据,必须添加 `tenant_id` 字段: + +| 表名 | 说明 | 隔离原因 | +|------|------|---------| +| `sys_user` | 用户表 | 用户属于特定租户,数据必须隔离 | +| `sys_role` | 角色表 | 角色是租户自定义的,不同租户角色不同 | +| `sys_dept` | 部门表 | 部门结构是租户私有的组织架构 | +| `sys_notice` | 通知公告表 | 通知是租户内部的信息 | +| `sys_log` | 系统日志表 | 日志记录租户的操作行为 | +| `sys_role_menu` | 角色菜单关联表 | 角色是租户隔离的,关联表也需要隔离 | +| `sys_user_role` | 用户角色关联表 | 用户和角色都是租户隔离的 | +| `ai_command_record` | AI命令记录表 | 命令记录是租户私有数据 | + +**实现方式**: +```sql +-- 添加 tenant_id 字段 +ALTER TABLE `sys_user` +ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, +ADD INDEX `idx_tenant_id` (`tenant_id`); + +-- 初始化为默认租户 +UPDATE `sys_user` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; +``` + +--- + +### ❌ 不需要租户隔离的表 + +这些表存储系统公共数据,应该所有租户共享: + +| 表名 | 说明 | 共享原因 | +|------|------|---------| +| `sys_tenant` | 租户表 | 租户表本身不能隔离 | +| **`sys_menu`** | **菜单表** | **功能入口定义,标准化共享** | +| `sys_dict` | 字典表 | 系统字典通常是标准化的 | +| `sys_dict_item` | 字典项表 | 字典值应该统一 | +| `sys_config` | 系统配置表 | 系统级配置应该全局统一 | + +**配置方式**: +```yaml +youlai: + tenant: + enabled: true + ignore-tables: + - sys_tenant # 租户表本身 + - sys_menu # 菜单表(重点!) + - sys_dict # 字典表 + - sys_dict_item # 字典项表 + - sys_config # 系统配置表 +``` + +--- + +## 🔍 重点说明:为什么菜单不隔离? + +### 问题背景 +```sql +-- 错误示例:如果菜单隔离,会产生大量冗余 +租户A的菜单: + - 系统管理 → 用户管理 → 角色管理 +租户B的菜单: + - 系统管理 → 用户管理 → 角色管理 +租户C的菜单: + - 系统管理 → 用户管理 → 角色管理 +(完全相同的菜单定义重复了3次!) +``` + +### 推荐方案:菜单共享 + 角色控制 + +#### 1. **菜单定义共享** +``` +所有租户共享同一套菜单定义: +├─ 系统管理 +│ ├─ 用户管理 +│ ├─ 角色管理 +│ ├─ 菜单管理 +│ └─ 租户管理 +├─ 业务管理 +│ ├─ 订单管理 +│ └─ 商品管理 +``` + +#### 2. **权限通过角色控制** +```typescript +// 租户A的管理员角色 +角色:租户A管理员 +权限:系统管理、业务管理(全部菜单) + +// 租户A的普通员工角色 +角色:租户A员工 +权限:业务管理(部分菜单) + +// 租户B的管理员角色 +角色:租户B管理员 +权限:系统管理、业务管理(全部菜单) +``` + +#### 3. **优势** + +| 维度 | 菜单共享 | 菜单隔离 | +|------|---------|---------| +| **数据量** | ✅ 少量 | ❌ 大量冗余 | +| **升级维护** | ✅ 一次升级 | ❌ 需迁移所有租户 | +| **管理成本** | ✅ 低 | ❌ 高 | +| **功能一致性** | ✅ 保证统一 | ⚠️ 可能不一致 | +| **定制能力** | ⚠️ 通过角色实现 | ✅ 每租户独立 | + +--- + +## 💡 权限控制流程 + +### 用户访问菜单的流程 + +```mermaid +graph TD + A[用户登录] --> B[获取用户角色] + B --> C{角色是否有权限?} + C -->|是| D[显示菜单] + C -->|否| E[隐藏菜单] + + style A fill:#e1f5ff + style D fill:#d4edda + style E fill:#f8d7da +``` + +### 示例代码 + +```java +// 1. 菜单定义(所有租户共享) +sys_menu: + id: 1, name: "用户管理", perm: "sys:user:list" + +// 2. 租户A的角色(租户隔离) +sys_role (tenant_id=1): + id: 10, name: "管理员", tenant_id: 1 + +// 3. 角色菜单关联(租户隔离) +sys_role_menu (tenant_id=1): + role_id: 10, menu_id: 1, tenant_id: 1 + +// 查询时自动过滤 +SELECT t3.perm, t2.code +FROM sys_role_menu t1 +INNER JOIN sys_role t2 ON t1.role_id = t2.id + AND t2.tenant_id = 1 -- ✅ 角色租户过滤 +INNER JOIN sys_menu t3 ON t1.menu_id = t3.id + -- ❌ 菜单不需要租户过滤(通过 ignore-tables 配置) +WHERE t1.tenant_id = 1 -- ✅ 关联表租户过滤 +``` + +--- + +## 🔧 配置示例 + +### application-dev.yml + +```yaml +youlai: + tenant: + # 启用多租户 + enabled: true + + # 租户字段名 + column: tenant_id + + # 默认租户ID + default-tenant-id: 1 + + # 忽略多租户过滤的表(重点配置) + ignore-tables: + - sys_tenant # 租户表本身 + - sys_menu # 菜单表(所有租户共享) + - sys_dict # 字典表 + - sys_dict_item # 字典项表 + - sys_config # 系统配置表 +``` + +--- + +## ⚠️ 常见问题 + +### Q1: 如果需要为不同租户定制菜单怎么办? + +**A:** 有两种方案: + +#### 方案1: 通过角色权限控制(推荐) +``` +租户A看到:菜单A、B、C(通过角色权限配置) +租户B看到:菜单A、B(通过角色权限配置) +``` + +#### 方案2: 菜单隔离(不推荐) +```yaml +# 将 sys_menu 从 ignore-tables 中移除 +ignore-tables: + - sys_tenant + # - sys_menu # 注释掉,启用菜单隔离 + +# 然后执行 SQL 添加 tenant_id +ALTER TABLE sys_menu +ADD COLUMN tenant_id bigint DEFAULT 1; +``` + +--- + +### Q2: 如果后端报错 `Unknown column 't3.tenant_id'` 怎么办? + +**A:** 这个错误说明: +1. ❌ `sys_menu` 表没有 `tenant_id` 字段 +2. ❌ 但配置文件中没有将 `sys_menu` 添加到 `ignore-tables` +3. ✅ 解决方案:将 `sys_menu` 添加到 `ignore-tables`(本文档已说明) + +--- + +### Q3: 字典表需要隔离吗? + +**A:** 通常不需要,原因: +- 字典是系统标准配置(如:性别、状态等) +- 所有租户应该使用统一的字典定义 +- 如果需要租户级字典,可以单独创建 `tenant_dict` 表 + +--- + +## 📝 总结 + +### 核心原则 + +1. **数据隔离**:用户、角色、部门等业务数据必须隔离 +2. **功能共享**:菜单、字典、配置等系统定义应该共享 +3. **权限控制**:通过角色和权限实现访问控制 + +### 最佳实践 + +``` +✅ 推荐做法: +- 菜单定义共享 +- 角色租户隔离 +- 通过角色控制菜单访问权限 + +❌ 不推荐做法: +- 为每个租户复制菜单 +- 菜单和角色都隔离但逻辑相同 +- 升级时需要迁移所有租户的菜单 +``` + +--- + +## 🔗 相关文档 + +- [多租户用户管理改进说明](./多租户用户管理改进说明.md) +- [tenant_add.sql](../sql/mysql/tenant_add.sql) - 多租户SQL脚本 +- [TenantProperties.java](../src/main/java/com/youlai/boot/config/property/TenantProperties.java) - 配置类 + +--- + +**更新时间**:2025-12-12 +**版本**:v3.0.0 diff --git a/sql/mysql/tenant_add.sql b/sql/mysql/tenant_add.sql index 07305776..b1b4b0ee 100644 --- a/sql/mysql/tenant_add.sql +++ b/sql/mysql/tenant_add.sql @@ -41,8 +41,11 @@ INSERT INTO `sys_tenant` (`id`, `name`, `code`, `status`, `create_time`) VALUES -- ============================================ -- 2. 为业务表添加 tenant_id 字段 -- ============================================ --- 注意:MySQL 5.7 不支持 IF NOT EXISTS,如果字段已存在会报错 --- 建议先检查字段是否存在,或使用 MySQL 8.0+ +-- 注意事项: +-- 1. MySQL 5.7 不支持 IF NOT EXISTS,如果字段已存在会报错 +-- 2. 菜单表(sys_menu)不添加 tenant_id,所有租户共享菜单定义 +-- 权限控制通过角色实现(角色是租户隔离的) +-- 3. 建议先检查字段是否存在,或使用 MySQL 8.0+ -- 用户表:仅在不存在时添加列和索引,避免重复执行报错 ALTER TABLE `sys_user` diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 38af018b..cee52f9e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -289,6 +289,7 @@ youlai: # 忽略多租户过滤的表名列表(系统表、租户表等不需要租户隔离的表) ignore-tables: - sys_tenant # 租户表本身 + - sys_menu # 菜单表(功能入口定义,所有租户共享) - sys_dict # 字典表(通常共享) - sys_dict_item # 字典项表(通常共享) - sys_config # 系统配置表(通常共享) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 2dcf4b99..2bccb95d 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -318,3 +318,33 @@ ai: rate-limit: max-executions-per-minute: 10 max-executions-per-day: 100 + +# ============================================ +# 多租户配置 +# ============================================ +# 说明:通过 youlai.tenant.enabled 控制是否启用多租户功能 +# 启用后,所有 SQL 查询会自动添加 tenant_id 过滤条件 +# ============================================ +youlai: + tenant: + # 是否启用多租户功能(默认:false) + # 设置为 true 启用多租户,设置为 false 禁用多租户(零成本切换) + enabled: true + + # 租户字段名(默认:tenant_id) + column: tenant_id + + # 默认租户ID(用于兼容旧数据,tenant_id 为 NULL 时使用) + default-tenant-id: 1 + + # 请求头中的租户ID字段名(默认:tenant-id) + header-name: tenant-id + + # 忽略多租户过滤的表名列表(系统表、租户表等不需要租户隔离的表) + ignore-tables: + - sys_tenant # 租户表本身 + - sys_menu # 菜单表(功能入口定义,所有租户共享) + - sys_dict # 字典表(通常共享) + - sys_dict_item # 字典项表(通常共享) + - sys_config # 系统配置表(通常共享) +# ============================================