多租户说明
概述
系统基于字段隔离的多租户架构设计,使用 tenant_id 作为租户标识符,实现在单一数据库实例中为多个租户提供隔离的数据存储和服务。
基于字段隔离的多租户架构是一种成本效益高的解决方案,适用于中小型应用。通过合理的设计和实现,可以在保证数据隔离的同时,实现资源的共享和高效利用。关键是要在应用层的各个层面都严格实施租户隔离策略,确保数据安全和系统稳定性。
与老版本(v3.3 tenant)的差异:老版在每个 logic 中内嵌
filter.TenantFilter,通过s.Model(ctx, dao.Xxx.Ctx(ctx), s.WithWhere())手动包装模型;本项目已升级为 dao 外层壳覆写Ctx()统一自动过滤(查询/修改/删除自动注入tenant_id条件 + 新增 Hook 自动填充),logic 层直接使用dao.Xxx.Ctx(ctx)即获得租户隔离。
架构模式
字段隔离(共享数据库,共享表结构)
字段隔离是一种多租户架构模式,所有租户共享同一个数据库实例和表结构,通过在每个表中添加 tenant_id 字段来区分不同租户的数据。
+-----------------+ +-----------------+ +-----------------+
| 租户 A 数据 | | 租户 B 数据 | | 租户 C 数据 |
| tenant_id=1 | | tenant_id=2 | | tenant_id=3 |
+-----------------+ +-----------------+ +-----------------+
\ / \ / \ /
\ / \ / \ /
+-------------------------------------------------------------+
| 共享数据库表结构 |
| (所有数据存储在同一表中,通过tenant_id区分) |
+-------------------------------------------------------------+安装部署方式
采用数据表字段隔离方案,安装部署方式与相关配置和标准版操作完全一致,区别仅在于数据初始化:
- 核心业务表已包含
tenant_id字段; - 系统额外提供租户相关表:
sys_tenant(租户)、sys_user_tenant(用户-租户成员关系)、sys_tenant_menu(租户菜单分配); - casbin 采用域感知模型,策略第 4 段为租户 ID。
数据库设计
基础表结构
所有租户隔离的业务表都需要包含 tenant_id 字段作为租户标识,并为常用查询建立索引:
-- 业务表示例
CREATE TABLE biz_demo (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID',
name VARCHAR(50) NOT NULL,
created_at DATETIME DEFAULT NULL COMMENT '创建时间',
updated_at DATETIME DEFAULT NULL COMMENT '修改时间',
INDEX idx_tenant_id (tenant_id),
UNIQUE KEY uk_tenant_name (tenant_id, name) -- 唯一约束按租户维度建立
);
当前已接入租户隔离的核心表:
| 表名 | 说明 | 隔离方式 |
|---|---|---|
sys_user / sys_role / sys_post |
用户、角色、岗位 | 严格隔离(tenant_id = 当前租户) |
sys_user_tenant |
用户-租户成员关系 | 严格隔离 |
sys_tenant_menu |
租户菜单分配 | 严格隔离 |
sys_dict_type / sys_dict_data / sys_config |
字典与参数 | 两级共享(tenant_id = 当前租户 OR 0,0 表示全局) |
租户表
CREATE TABLE `sys_tenant` (
`id` bigint(20) unsigned NOT NULL COMMENT '主键(雪花ID)',
`name` varchar(50) NOT NULL COMMENT '租户名称',
`en_no` varchar(255) NOT NULL COMMENT '租户编码(域名或租户代码)',
`status` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '状态;0:禁用,1:正常',
`created_by` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '创建人',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `en_no` (`en_no`, `deleted_at`) -- 编码唯一约束含软删除字段,删除后编码可复用
) ENGINE=InnoDB COMMENT='租户表';
应用层实现
1. 中间件处理租户ID
在路由文件中,分组路由加上 service.Middleware().Tenant 中间件处理(必须挂在 Ctx 之后,依赖 token 解析出的用户上下文):
group.Middleware(service.Middleware().Ctx, service.Middleware().Tenant)
中间件规则(internal/app/system/logic/middleware/middleware.go):
- 已登录用户:一律以 token 中的激活租户(
SysTenantId)为准,禁止 header/参数覆盖,防止伪造租户上下文绕过数据隔离; - 未登录(公开接口/登录页):才允许 header/请求参数
tenantId兜底。
2. 数据访问层:dao 壳自动过滤
对租户隔离表,dao 外层壳覆写 Ctx(),自动应用租户过滤(以 sys_role 为例):
// 查询/修改/删除自动带上 tenant_id 条件;新增通过 Insert Hook 自动填充 tenant_id
func (d sysRoleDao) Ctx(ctx context.Context) *gdb.Model {
return filter.Apply(ctx, d.SysRoleDao.Ctx(ctx))
}
// 主表 As(别名) 并 Join 的查询必须用 CtxAs:
// gdb 会把裸字段条件自动补成真实表名前缀(如 `sys_role`.`tenant_id`),
// 主表一旦有别名即报 1054 Unknown column
func (d sysRoleDao) CtxAs(ctx context.Context, alias string) *gdb.Model {
return filter.Apply(ctx, d.SysRoleDao.Ctx(ctx), alias)
}
internal/app/common/filter/tenant.go 提供两种过滤语义:
| 方法 | SQL 语义 | 适用场景 |
|---|---|---|
filter.Apply(ctx, m, alias...) |
tenant_id = 当前租户 |
严格租户隔离表;Insert Hook 自动填充 tenant_id(仅当数据未显式提供时) |
filter.ApplyShared(ctx, m, alias...) |
(tenant_id = 当前租户 OR tenant_id = 0) |
字典/参数等”全局 + 租户覆盖”表 |
3. 服务层(logic)
logic 层无需任何租户包装,直接使用 dao.Xxx.Ctx(ctx) 即获得租户隔离。
查询数据:
func (s *sDemo) List(ctx context.Context, req *model.DemoSearchReq) (listRes *model.DemoSearchRes, err error) {
...
err = dao.Demo.Ctx(ctx).Page(req.PageNum, req.PageSize).Order(order).Scan(&res)
...
}
新增数据:
func (s *sDemo) Add(ctx context.Context, req *model.DemoAddReq) (err error) {
...
_, err = dao.Demo.Ctx(ctx).Data(data).Insert()
// 注意:data 中不需要(也不应)携带 tenant_id 字段,Insert Hook 中自动按当前租户填充
...
}
修改数据:
func (s *sDemo) Edit(ctx context.Context, req *model.DemoEditReq) (err error) {
...
_, err = dao.Demo.Ctx(ctx).WherePri(req.Id).Update(data) // 更新范围自动限定在本租户内
...
}
删除数据:
func (s *sDemo) Delete(ctx context.Context, ids []uint) (err error) {
...
_, err = dao.Demo.Ctx(ctx).Delete(dao.Demo.Columns().Id+" in (?)", ids)
...
}
若主表带别名并 Join 其它表,改用 dao.Demo.CtxAs(ctx, "别名") 创建模型即可,其余写法不变。
4. 特殊场景
- 无请求上下文(定时任务、消息消费、C端应用):
ctx = filter.WithTenant(ctx, tenantId)显式注入租户; - 平台超管跨租户管理、全量级联更新:
ctx = filter.WithGlobal(ctx)跳过租户过滤; tenant_id = 0表示全局共享数据(如全局字典/参数);ctx 无租户(未登录、后台任务未注入)时自动跳过过滤。
代码生成支持
代码生成支持多租户功能:只要表中存在 tenant_id 字段,便会自动生成租户处理能力,具体包括:
- dao 外层壳覆写
Ctx()并注入filter.Apply(查询/更新/删除自动带tenant_id、新增自动填充); AddReq/EditReq/SearchReq/ListRes等请求/返回结构不含tenant_id,客户端无法伪造租户归属;- 新增/修改/删除 logic 自动补充租户条件;
- Excel 导入不映射
tenant_id(防止恶意构造跨租户注入或全量落入tenant_id=0); - 关联表含
tenant_id时,关联查询自动补租户条件; - 不含
tenant_id的普通表,生成产物保持不变。
代码生成模板行为由 internal/app/system/logic/toolsGenTable/tenant_render_test.go 保障回归。
安全考虑
1. 数据隔离
- 所有数据库查询、修改、删除由 dao 壳自动包含
tenant_id条件; - 新增数据由 Insert Hook 自动写入
tenant_id; - 使用中间件自动注入租户上下文;
- 隔离收敛在数据访问层,业务代码默认无法绕过。
2. 防伪造与越权
- 已登录用户的租户上下文只信任 token,header/参数中的
tenantId不生效; - 生成代码的请求结构不含
tenant_id字段,租户归属由服务端决定; - 跨租户管理仅对平台超管开放(
filter.WithGlobal),普通租户管理员只能在本租户域内操作。
3. 权限模型
casbin 采用域感知模型:p 规则第 4 段为租户 ID,g 规则格式为 g, u_用户ID, 角色ID, 租户ID,权限校验天然限定在租户域内。
性能优化
1. 数据库索引
-- 为所有租户隔离表的 tenant_id 字段创建索引
CREATE INDEX idx_users_tenant_id ON sys_user(tenant_id);
CREATE INDEX idx_roles_tenant_id ON sys_role(tenant_id);
-- 创建复合索引优化高频查询
CREATE INDEX idx_users_tenant_username ON sys_user(tenant_id, user_name);
2. 缓存策略
缓存键统一以租户 ID 作为前缀,保证各租户缓存互不干扰:
// 字典缓存:前缀 + 租户ID + 字典类型
cacheKey := consts.CacheSysDict + ":" + gconv.String(libUtils.GetTenantId(ctx)) + ":" + dictType
// 参数缓存:完整 key 形如 APP:sysConfig:<tenantId>:<key>
cacheKey := consts.CacheSysConfig + ":" + gconv.String(libUtils.GetTenantId(ctx)) + ":" + key
字典全量缓存同样按租户区分(...:<tenantId>_dict_type_all),字典/参数变更时按租户维度精准清理。
系统使用
不同租户下的用户只能访问自己归属的租户相关数据,不能跨租户访问数据(租户管理员除外),租户管理员可以选择登录不同租户来设置租户相关数据(比如设置不同租户的字典,参数等相关信息)。
字典和参数分为全局和租户,全局:所有租户可以使用;租户:不同租户的字典和参数相互隔离,示例如下:
如:租户A只能使用全局和租户A的字典:

租户B只能使用全局和租户B的字典:

租户管理员在角色授权时,选择是租户管理员则可以在不同租户中切换维护租户数据:

注意:普通管理人员不要给租户维护相关的权限(这个权限应该只是给系统维护人员或运维人员)。
最后编辑:管理员 更新时间:2026-09-20 15:48