2026-03-27 22:20:25 +08:00
|
|
|
|
# 多租户系统实现指南
|
|
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
本系统实现了完整的多租户架构,支持:
|
|
|
|
|
|
- 每个租户独立的数据隔离(用户、角色、权限、菜单等)
|
|
|
|
|
|
- 每个租户独立的访问链接(通过租户编码或域名)
|
|
|
|
|
|
- 超级租户可以创建和管理其他租户
|
|
|
|
|
|
- 超级租户可以为租户分配菜单
|
|
|
|
|
|
|
|
|
|
|
|
## 数据库设计
|
|
|
|
|
|
|
|
|
|
|
|
### 核心表结构
|
|
|
|
|
|
|
|
|
|
|
|
1. **Tenant(租户表)**
|
|
|
|
|
|
- `id`: 租户ID
|
|
|
|
|
|
- `name`: 租户名称
|
|
|
|
|
|
- `code`: 租户编码(唯一,用于访问链接)
|
|
|
|
|
|
- `domain`: 租户域名(可选,用于子域名访问)
|
|
|
|
|
|
- `isSuper`: 是否为超级租户(0-否,1-是)
|
|
|
|
|
|
- `validState`: 有效状态(1-有效,2-失效)
|
|
|
|
|
|
|
|
|
|
|
|
2. **TenantMenu(租户菜单关联表)**
|
|
|
|
|
|
- `tenantId`: 租户ID
|
|
|
|
|
|
- `menuId`: 菜单ID
|
|
|
|
|
|
- 用于关联租户和菜单,实现菜单分配
|
|
|
|
|
|
|
|
|
|
|
|
3. **其他表添加租户字段**
|
|
|
|
|
|
- `User`: 添加 `tenantId` 字段
|
|
|
|
|
|
- `Role`: 添加 `tenantId` 字段
|
|
|
|
|
|
- `Permission`: 添加 `tenantId` 字段
|
|
|
|
|
|
- `Dict`: 添加 `tenantId` 字段
|
|
|
|
|
|
- `Config`: 添加 `tenantId` 字段
|
|
|
|
|
|
|
|
|
|
|
|
### 唯一性约束调整
|
|
|
|
|
|
|
|
|
|
|
|
- `User.username`: 从全局唯一改为 `(tenantId, username)` 唯一
|
|
|
|
|
|
- `User.email`: 从全局唯一改为 `(tenantId, email)` 唯一
|
|
|
|
|
|
- `Role.name/code`: 从全局唯一改为 `(tenantId, name/code)` 唯一
|
|
|
|
|
|
- `Permission.code`: 从全局唯一改为 `(tenantId, code)` 唯一
|
|
|
|
|
|
- 其他类似字段也做了相应调整
|
|
|
|
|
|
|
|
|
|
|
|
## 租户识别机制
|
|
|
|
|
|
|
|
|
|
|
|
系统支持多种方式识别租户:
|
|
|
|
|
|
|
|
|
|
|
|
1. **请求头方式**(推荐)
|
|
|
|
|
|
- `X-Tenant-Code`: 租户编码
|
|
|
|
|
|
- `X-Tenant-Id`: 租户ID
|
|
|
|
|
|
|
|
|
|
|
|
2. **子域名方式**
|
|
|
|
|
|
- 从 `Host` 请求头提取子域名
|
|
|
|
|
|
- 匹配租户的 `code` 或 `domain` 字段
|
|
|
|
|
|
|
|
|
|
|
|
3. **JWT Token方式**
|
|
|
|
|
|
- Token中包含 `tenantId` 字段
|
|
|
|
|
|
- 登录时自动关联租户
|
|
|
|
|
|
|
|
|
|
|
|
4. **登录参数方式**
|
|
|
|
|
|
- 登录接口支持 `tenantCode` 参数
|
|
|
|
|
|
|
|
|
|
|
|
## 使用流程
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 数据库迁移
|
|
|
|
|
|
|
|
|
|
|
|
首先需要生成并执行数据库迁移:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 生成迁移文件
|
|
|
|
|
|
npm run prisma:migrate:dev -- --name add_tenant_support
|
|
|
|
|
|
|
|
|
|
|
|
# 执行迁移
|
|
|
|
|
|
npm run prisma:migrate
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 初始化超级租户
|
|
|
|
|
|
|
|
|
|
|
|
运行初始化脚本创建超级租户:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
npm run init:super-tenant
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这将创建:
|
|
|
|
|
|
- 超级租户(code: `super`)
|
|
|
|
|
|
- 超级管理员用户(username: `admin`, password: `admin123`)
|
|
|
|
|
|
- 超级管理员角色
|
|
|
|
|
|
- 基础权限
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 创建普通租户
|
|
|
|
|
|
|
|
|
|
|
|
使用超级租户的管理员账号登录后,通过租户管理接口创建新租户:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
POST /api/tenants
|
|
|
|
|
|
Headers:
|
|
|
|
|
|
Authorization: Bearer <token>
|
|
|
|
|
|
X-Tenant-Code: super
|
|
|
|
|
|
Body:
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "租户A",
|
|
|
|
|
|
"code": "tenant-a",
|
|
|
|
|
|
"domain": "tenant-a.example.com",
|
|
|
|
|
|
"description": "租户A的描述",
|
|
|
|
|
|
"menuIds": [1, 2, 3] // 分配的菜单ID列表
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 为租户分配菜单
|
|
|
|
|
|
|
|
|
|
|
|
超级租户可以为租户分配菜单:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
PATCH /api/tenants/:id
|
|
|
|
|
|
Headers:
|
|
|
|
|
|
Authorization: Bearer <token>
|
|
|
|
|
|
X-Tenant-Code: super
|
|
|
|
|
|
Body:
|
|
|
|
|
|
{
|
|
|
|
|
|
"menuIds": [1, 2, 3, 4, 5]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5. 租户用户登录
|
|
|
|
|
|
|
|
|
|
|
|
租户用户登录时需要指定租户:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
POST /api/auth/login
|
|
|
|
|
|
Body:
|
|
|
|
|
|
{
|
|
|
|
|
|
"username": "user1",
|
|
|
|
|
|
"password": "password123",
|
|
|
|
|
|
"tenantCode": "tenant-a" // 可选,也可以从请求头获取
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
或者在请求头中指定:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
POST /api/auth/login
|
|
|
|
|
|
Headers:
|
|
|
|
|
|
X-Tenant-Code: tenant-a
|
|
|
|
|
|
Body:
|
|
|
|
|
|
{
|
|
|
|
|
|
"username": "user1",
|
|
|
|
|
|
"password": "password123"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 6. 访问租户数据
|
|
|
|
|
|
|
|
|
|
|
|
所有API请求都会自动根据租户ID过滤数据:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
GET /api/users
|
|
|
|
|
|
Headers:
|
|
|
|
|
|
Authorization: Bearer <token>
|
|
|
|
|
|
X-Tenant-Code: tenant-a
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
返回的数据只会包含该租户的用户。
|
|
|
|
|
|
|
|
|
|
|
|
## API接口
|
|
|
|
|
|
|
|
|
|
|
|
### 租户管理接口
|
|
|
|
|
|
|
|
|
|
|
|
- `POST /api/tenants` - 创建租户(需要 `tenant:create` 权限)
|
|
|
|
|
|
- `GET /api/tenants` - 获取租户列表(需要 `tenant:read` 权限)
|
|
|
|
|
|
- `GET /api/tenants/:id` - 获取租户详情(需要 `tenant:read` 权限)
|
|
|
|
|
|
- `PATCH /api/tenants/:id` - 更新租户(需要 `tenant:update` 权限)
|
|
|
|
|
|
- `DELETE /api/tenants/:id` - 删除租户(需要 `tenant:delete` 权限)
|
|
|
|
|
|
- `GET /api/tenants/:id/menus` - 获取租户的菜单树(需要 `tenant:read` 权限)
|
|
|
|
|
|
|
|
|
|
|
|
### 其他接口
|
|
|
|
|
|
|
|
|
|
|
|
所有其他接口(用户、角色、权限等)都支持租户隔离,会自动根据请求中的租户信息过滤数据。
|
|
|
|
|
|
|
|
|
|
|
|
## 前端集成
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 请求拦截器
|
|
|
|
|
|
|
|
|
|
|
|
在前端请求拦截器中添加租户信息:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// utils/request.ts
|
|
|
|
|
|
service.interceptors.request.use(
|
|
|
|
|
|
(config: InternalAxiosRequestConfig) => {
|
|
|
|
|
|
const token = getToken();
|
|
|
|
|
|
const tenantCode = getTenantCode(); // 从localStorage或store获取
|
|
|
|
|
|
|
|
|
|
|
|
if (token && config.headers) {
|
|
|
|
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (tenantCode && config.headers) {
|
|
|
|
|
|
config.headers['X-Tenant-Code'] = tenantCode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
|
},
|
|
|
|
|
|
(error) => {
|
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 登录时保存租户信息
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 登录成功后
|
|
|
|
|
|
localStorage.setItem('tenantCode', response.data.user.tenantCode);
|
|
|
|
|
|
localStorage.setItem('tenantId', response.data.user.tenantId);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 租户切换
|
|
|
|
|
|
|
|
|
|
|
|
如果需要支持租户切换,可以在前端实现租户选择器,切换时更新localStorage中的租户信息并重新加载数据。
|
|
|
|
|
|
|
|
|
|
|
|
## 权限控制
|
|
|
|
|
|
|
|
|
|
|
|
### 超级租户权限
|
|
|
|
|
|
|
|
|
|
|
|
超级租户的用户拥有所有权限,包括:
|
|
|
|
|
|
- 创建、查看、更新、删除租户
|
|
|
|
|
|
- 为租户分配菜单
|
|
|
|
|
|
- 管理所有租户的数据(如果需要在超级租户中查看所有租户数据)
|
|
|
|
|
|
|
|
|
|
|
|
### 普通租户权限
|
|
|
|
|
|
|
|
|
|
|
|
普通租户的用户只能:
|
|
|
|
|
|
- 管理自己租户内的数据
|
|
|
|
|
|
- 查看分配给租户的菜单
|
|
|
|
|
|
- 无法访问其他租户的数据
|
|
|
|
|
|
|
|
|
|
|
|
## 注意事项
|
|
|
|
|
|
|
|
|
|
|
|
1. **数据隔离**: 所有查询都会自动添加租户过滤条件,确保数据隔离
|
|
|
|
|
|
2. **唯一性**: 用户名、邮箱等在租户内唯一,不同租户可以有相同的用户名
|
|
|
|
|
|
3. **菜单管理**: 菜单是全局的(由超级租户管理),但通过 `TenantMenu` 表分配给各个租户
|
|
|
|
|
|
4. **超级租户**: 超级租户不能被删除,且拥有所有权限
|
|
|
|
|
|
5. **迁移数据**: 如果现有系统已有数据,需要编写迁移脚本将现有数据关联到超级租户
|
|
|
|
|
|
|
|
|
|
|
|
## 迁移现有数据
|
|
|
|
|
|
|
|
|
|
|
|
如果系统已有数据,需要将现有数据迁移到超级租户:
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 假设超级租户ID为1
|
|
|
|
|
|
UPDATE users SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|
|
|
|
|
UPDATE roles SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|
|
|
|
|
UPDATE permissions SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|
|
|
|
|
-- 其他表类似
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 故障排查
|
|
|
|
|
|
|
|
|
|
|
|
1. **租户识别失败**: 检查请求头是否正确设置,或检查JWT token中是否包含tenantId
|
|
|
|
|
|
2. **数据查询为空**: 确认租户ID正确,且数据确实属于该租户
|
|
|
|
|
|
3. **权限不足**: 确认用户角色有相应权限,且角色属于正确的租户
|
|
|
|
|
|
|
|
|
|
|
|
## 扩展功能
|
|
|
|
|
|
|
|
|
|
|
|
未来可以考虑的扩展:
|
|
|
|
|
|
1. 租户级别的配置(每个租户可以有自己的系统配置)
|
|
|
|
|
|
2. 租户级别的主题和品牌定制
|
|
|
|
|
|
3. 租户级别的功能开关
|
|
|
|
|
|
4. 租户使用统计和监控
|
|
|
|
|
|
5. 租户数据导出和备份
|
|
|
|
|
|
|