SaaS 多租户
一套库给多家组织用。隔离方式是字段隔离:同一张表用 tenant_id 区分,登录用户默认只能看见本租户的行。透明能力在 yudao-spring-boot-starter-biz-tenant,覆盖 Web、Security、DB、Redis、Job、MQ。管理端前缀 /admin-api。
示意图:Header 写入上下文,拦截器给 SQL 拼本租户。
怎么配
菜单 系统管理 → 租户管理。官方列表页 ruoyi-office-vben/apps/web-antd/src/views/system/tenant/。套餐页在 src/views/system/tenantPackage/。
新建租户会建租户管理员和角色,再按套餐灌菜单、流程模型、默认角色、组织骨架。packageId = 0 是系统内置租户,不走套餐。

本系统截图:系统管理 → 租户。名称、套餐、过期时间、状态。
套餐编辑四个 Tab,见 tenantPackage/modules/form.vue:
| Tab | 固化什么 | 表 |
|---|---|---|
| 基本信息与菜单 | 套餐名、勾选 system_menu | system_tenant_package |
| 流程模型 | 按分类勾选流程,快照进资产 | system_tenant_package_asset,asset_type = BPM_MODEL |
| 默认角色 | 角色模板 | ROLE_TEMPLATE |
| 组织骨架 | 根部门、岗位,缺省「总公司」 | ORG_SKELETON |
流程要先保存套餐拿到 packageId,再固化。新建租户时 TenantServiceImpl 插入 system_tenant,再用 TenantUtils.execute 切到该租户建管理员,并写一条 CREATE 类型的 system_tenant_transfer_job 异步灌资产。已有租户用「补齐套餐」,只增不覆盖。

本系统截图:系统管理 → 租户套餐。点「修改」后四个 Tab:基本信息与菜单、流程模型、默认角色、组织骨架。流程要先保存再固化。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
TenantDO | 租户主表,自身 @TenantIgnore | yudao-module-system-server/.../dal/dataobject/tenant/TenantDO.java |
TenantPackageDO | 套餐与菜单 | 同目录 TenantPackageDO.java |
TenantPackageAssetDO | 套餐资产 | 表 system_tenant_package_asset |
| 管理端 | @RequestMapping("/system/tenant") | .../controller/admin/tenant/TenantController.java |
| 套餐 | /system/tenant-package | TenantPackageController.java |
| 编排 | 建租户灌资产 | TenantPackageInitOrchestrator |
websites 可填管理端域名或小程序 appId。登录页按 host 调 /system/tenant/get-by-website,拿到 tenant-id。
开关
前后端各有一个开关,要一起改。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
yudao.tenant.enable | 默认 true。false 关闭后端多租户 | ruoyi-office/yudao-server/src/main/resources/application.yaml |
VITE_APP_TENANT_ENABLE | 登录页是否走租户 | ruoyi-office-vben/apps/web-antd/.env |
只有一家公司用时,两边都设 false。不要先删代码。真要拆干净,见 删除功能。
同文件还有 ignore-urls、ignore-tables、ignore-visit-urls、ignore-caches。
请求头与切换
默认每个请求必须带 Header tenant-id,值是 system_tenant.id。PC 在 apps/web-antd/src/api/request.ts 写入。不带且不在忽略名单,会报「租户的请求未传递」。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
TenantContextWebFilter | 从 Header 写入上下文 | yudao-spring-boot-starter-biz-tenant/.../web/TenantContextWebFilter.java |
TenantContextHolder | TransmittableThreadLocal | 同模块 .../context/TenantContextHolder.java |
登录按名称查租户、按域名换皮这类接口,方法上加 @TenantIgnore。Controller 上的注解启动时扫进忽略 URL。积木报表、资产扫码短链带不上 tenant-id,必须忽略。
TenantSecurityWebFilter:已登录用户不能拿别人的 tenant-id 越权;校验租户是否停用、过期。
拥有 system:tenant:visit 的用户可以切到别的租户看数据。前端额外带 visit-tenant-id,TenantVisitContextInterceptor 把上下文改成访问租户,请求结束后改回。个人信息、登录态不能跟切换走,路径放进 ignore-visit-urls(/admin-api/system/user/profile/**、/admin-api/system/auth/**)。权限点挂在「租户管理 → 租户切换」。
搬家分层
跨环境搬家走 系统管理 → 租户 的导出 / 导入。始终新建 tenant_id,主键重映射。不迁 Flowable 运行实例和待办;业务单上的 process_instance_id 导入时清空。先办结源租户在途审批再搬。
| 层 | 内容 |
|---|---|
| L0 租户信息 | 名称、套餐、品牌 Logo |
| L1 组织权限 | 部门、岗位、角色、用户(导入未完成不要登录) |
| L2 流程模型 | 分类与可移植定义,导入后自动发布 |
| LBiz 业务表 | 带 tenant_id 的业务表,排除引擎表和日志 |
| L5 附件元数据 | 附件行;文件本体可选 |
接口在 TenantTransferController:/system/tenant/transfer/export|import|job-get|download。菜单、字典不随包走,依赖目标环境版本。
SQL 与二开
要隔离的业务表:加 tenant_id 列,DO 继承 TenantBaseDO。
public abstract class TenantBaseDO extends BaseDO {
private Long tenantId;
}不要隔离的表:写进 yudao.tenant.ignore-tables,或 DO 上标 @TenantIgnore。漏配时拦截器仍拼 tenant_id,没有这一列的表会报错。system_user_role 必须忽略,见 权限体系。
TenantDatabaseInterceptor 接 MyBatis Plus TenantLineInnerInterceptor。手写 Mapper XML 经常拼不上,要自己带条件。类内部 this.xxx() 不会走 @TenantIgnore 代理。
TenantUtils.execute(tenantId, runnable) 临时切租户;executeIgnore 整段不拼 tenant_id。创建租户、跨租户灌数据用前者。
Redis 走 Spring Cache 时,TenantRedisCacheManager 把缓存名收成 name:tenantId。Job 加 @TenantJob 才会按租户循环,方法必须幂等。MQ / Feign 发送时带 tenant-id。
配置与操作
管理员在 租户列表 / 套餐 建租户、勾菜单和流程。忽略表和 URL 改 yudao.tenant.ignore-* 后重启。Job 加 @TenantJob。开启见 框架层。
