# 多租户系统实现指南

## 概述

本系统实现了完整的多租户架构，支持：
- 每个租户独立的数据隔离（用户、角色、权限、菜单等）
- 每个租户独立的访问链接（通过租户编码或域名）
- 超级租户可以创建和管理其他租户
- 超级租户可以为租户分配菜单

## 数据库设计

### 核心表结构

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. 租户数据导出和备份

