JooLun Plus 百科 ⬇ 源码下载

03.05 多租户功能

102353 于 2026-10-17 14:14:33 +08:00 修改了此页面

# 多租户功能

多租户功能基于“同一套系统服务 + 租户数据隔离”实现。项目中既有共库共表按 tenant_id 隔离的场景,也有按租户分库、分表的场景,因此租户ID不仅影响 MyBatis-Plus 的多租户过滤,也会影响分库分表、业务数据路由和跨服务调用。

如果对 MyBatis-Plus 多租户拦截器不熟悉,可以先看官方文档:

https://mybatis.plus/guide/interceptor-tenant-line.html

1. 功能说明

  • 一个租户通常对应一个独立的业务主体,例如一个商城。
  • 同一租户下可以有多个展示端,例如 App、小程序、H5,但读取的是同一份租户业务数据。
  • 有租户约束的表,各租户数据默认互相隔离。
  • 无租户约束的表,属于平台公共数据,不按租户隔离。
  • 不要在业务代码中手工维护 tenant_id,系统会自动维护当前请求租户上下文。

2. 租户表配置

需要做租户隔离的业务表,需要:

  1. 表结构增加 tenant_id 字段
  2. 在对应服务的 Nacos 配置中声明该表启用租户拦截

示例:

base:
  tenant:
    column: tenant_id
    tables:
      - wx_app
      - wx_menu
      - wx_user
      - wx_auto_reply
      - wx_msg
      - wx_mass_msg

上面的配置表示这些表会自动追加 tenant_id 过滤条件。

3. 后台租户ID自动维护

后台请求的租户ID自动维护,核心逻辑在:

TenantContextHolderFilter.java

该过滤器是最高优先级执行的过滤器,主要职责是:

  • 从请求中识别当前租户
  • 将租户ID写入 TenantContextHolder
  • 让后续 MyBatis 多租户、分库分表、Feign 透传统一使用同一个租户上下文
  • 请求完成后清理 ThreadLocal,避免串租户

3.1 处理总流程

后台请求进入后,过滤器按下面的顺序处理:

  1. 先读取 Authorization 请求头
  2. 如果有 Bearer Token,则到 Redis 中查出当前 token 对应的登录用户
  3. 从登录用户中取出默认租户ID
  4. 如果满足平台管理员切换租户条件,再尝试使用 switch-tenant-id
  5. 将最终租户ID写入 TenantContextHolder
  6. 请求执行完成后调用 TenantContextHolder.clear()

涉及的关键请求头:

  • Authorization
  • switch-tenant-id
  • tenant-id

涉及的关键常量:

  • CommonConstants.SYSTEM_TENANT_ID = "-1"
  • CommonConstants.USER_TYPE_S = "-1"
  • SecurityConstants.SWITCH_TENANT_ID = "switch-tenant-id"
  • CommonConstants.TENANT_ID = "tenant-id"

3.2 情况1:租户账号登录

租户账号登录后,token 中的用户信息本身就带有所属租户ID。

处理逻辑:

  1. TenantContextHolderFilter 从 Bearer Token 对应的登录用户中取出 baseUser.getTenantId()
  2. 该租户ID直接写入 TenantContextHolder
  3. 后续数据库查询、分库路由、Feign 调用都使用这个租户ID

也就是说:

  • 租户账号不需要传 switch-tenant-id
  • 系统会自动 set 成该账号所属的租户ID
  • 该账号只能管理自己租户的数据

3.3 情况2:平台管理员账号登录

平台管理员是多租户管理模式,不适合一次查出所有租户数据统一处理,原因是:

  • 系统本身是多租户机制
  • 数据库根据实际业务可能做了分库、分表
  • 不同业务模块的数据量和路由规则都可能不同

因此平台管理员管理租户数据的方式不是“一次查所有租户”,而是“先切换租户,再按当前租户上下文处理”。

平台管理员切换租户生效需要同时满足以下条件:

  1. 当前请求有 Bearer Token
  2. 当前登录用户类型是平台管理员,即 user.type = -1
  3. 当前登录用户默认租户ID是系统租户,即 tenantId = -1
  4. 请求头传入了 switch-tenant-id
  5. switch-tenant-id 在当前平台管理员被授权可管理的租户列表中

满足后,过滤器会把本次请求租户切换为 switch-tenant-id。

也就是说:

  • 平台管理员登录后,默认不是直接查“所有租户数据”
  • 平台管理员需要先切换到目标租户,再管理该租户的数据
  • 切换后的租户ID由 TenantContextHolderFilter 自动维护

4. 哪些后台页面会传 switch-tenant-id

前端是否传 switch-tenant-id,核心逻辑在:

util.ts

和:

index.ts

4.1 普通租户业务页面

在普通租户业务页面中,如果浏览器当前已经保存了切换租户ID,并且当前页面不是平台页面,请求拦截器会自动带上:

switch-tenant-id: 当前切换的租户ID

这样后台就能在 TenantContextHolderFilter 中识别出当前要管理哪个租户的数据。

4.2 平台功能页面不传 switch-tenant-id

后台并不是所有菜单都区分租户。

例如:

  • 微信、商城这类业务功能,通常区分租户
  • 平台管理、平台供应商这类平台功能,通常不区分租户

前端通过 judgePlatformPage() 判断当前是否属于平台页面。当前代码中,以下路径会被判定为平台类页面,不自动附带 switch-tenant-id:

  • #/platform/
  • #/platform-supplier/
  • #/info/index
  • #/authredirect
  • #/tool
  • #/login

这类页面下的接口请求,不会在通用请求拦截器中自动带 switch-tenant-id。

这样设计的目的,是避免平台公共功能误带租户上下文,导致把本应查平台公共数据的请求错误地路由到某个租户数据中。

4.3 特殊接口说明

通用请求拦截器里还有两个特殊点:

  • /upms/phone/code 这类验证码接口,不传 switch-tenant-id,而是传 tenant-id
  • 如果明确需要跨租户请求数据,可以使用 requestTenantData() 手工指定 switch-tenant-id

5. 后台自动维护与分库分表的关系

后台自动维护租户ID不是只为了给 SQL 拼 tenant_id 条件,它还会影响:

  • MyBatis-Plus 多租户 SQL 拦截
  • 分库分表路由
  • 动态数据源
  • Feign 调用时的租户透传

例如:

  1. 后台当前请求被识别为租户 1001
  2. TenantContextHolder 中保存的是 1001
  3. 本地查询会自动按 tenant_id = 1001 过滤
  4. 如有分库规则,也会按 1001 路由到对应租户库
  5. Feign 调用其他服务时,会通过 BaseFeignTenantInterceptor 自动透传 tenant-id
  6. 下游服务再继续按相同租户上下文处理

因此后台租户管理必须走“切换租户 -> 自动维护上下文 -> 正常查询”的方式,不能简单理解为只是前端传一个参数。

6. 前端用户端租户ID自动维护

前端用户端不是走后台登录 token 这一套,而是通过:

ThirdSessionInterceptor

自动维护租户上下文。

典型逻辑是:

  • 从 thirdSession 关联的用户信息中识别租户ID
  • 必要时从请求头 tenant-id 中读取租户
  • 写入 TenantContextHolder

因此后台管理端和用户端的租户识别机制不同:

  • 后台管理端:TenantContextHolderFilter
  • 用户端:ThirdSessionInterceptor

7. 无租户ID场景

7.1 平台公共表或公共查询

如果某个 Mapper 方法不需要租户隔离,可以在方法上加:

@InterceptorIgnore(tenantLine = "true")
SysUser getByNoTenant(SysUser sysUser);

加上后,MyBatis-Plus 不会自动追加租户条件。

7.2 没有 token 但仍需要租户

有些请求没有后台 token,也没有用户端 thirdSession,但仍需要租户上下文。

例如:

  • 某些回调接口
  • 某些特殊开放接口

这类场景可以先按业务标识查出租户ID,再手工设置:

TenantContextHolder.setTenantId(tenantId);

7.3 完全不区分租户

平台公共功能、本身不受租户隔离的表或接口,可以不设置租户,或者明确把租户切回系统租户。

8. 常用方法

获取当前租户:

TenantContextHolder.getTenantId()

手工设置租户:

TenantContextHolder.setTenantId(tenantId)

清理租户:

TenantContextHolder.clear()

注意:

  • 正常增删改查过程中,不要手工给 tenant_id 赋值
  • 也不要绕过 TenantContextHolderFilter 自己拼租户参数
  • 否则容易出现串租户、分库路由错误或 SQL 结果异常

9. 新增租户

  • 在后台【平台管理】->【租户管理】中新增租户
  • 为租户配置租户管理员账号
  • 给租户管理员分配对应菜单、角色和数据权限
  • 租户管理员登录后,系统会自动带入所属租户ID

租户表:

sys_tenant

10. 平台用户管理指定租户

新增租户后,可以把租户分配给指定的平台管理员。

分配后,平台管理员可以在后台左上角切换租户,再管理该租户的数据。

典型流程:

  1. 平台管理员登录
  2. 左上角切换到目标租户
  3. 前端请求自动带 switch-tenant-id
  4. TenantContextHolderFilter 自动把当前请求租户切换为目标租户
  5. 后续查询、分库、跨服务调用都按该租户执行

如果当前访问的是平台功能页面,则不会自动带 switch-tenant-id,接口将按平台公共逻辑处理。