mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 12:52:26 +00:00
refactor(backend): 重构模型基类支持租户与客户隔离 feat(backend): 添加客户模块相关模型、CRUD和参数校验 docs(backend): 新增SaaS数据隔离设计方案文档 refactor(backend): 优化日志模块并添加类型注解 fix(backend): 修正字典模块查询参数移除creator字段 style(frontend): 统一按钮组件代码格式 fix(frontend): 修复表格序号计算逻辑 chore(frontend): 更新lint脚本使用pnpm替代npm
12 KiB
12 KiB
SaaS多租户数据隔离设计方案
📋 目录
设计概述
本系统采用三层数据隔离架构:
系统层 (System Level)
└── 租户层 (Tenant Level)
├── 部门/角色/用户等组织架构
└── 客户层 (Customer Level)
└── 客户用户及业务数据
核心隔离字段
| 字段 | 用途 | 必填性 |
|---|---|---|
tenant_id |
租户隔离,实现SaaS多租户架构 | 大部分表必填 |
customer_id |
客户隔离,租户下的二级隔离 | 部分表可选 |
created_id |
记录创建者,用于数据权限控制 | 可选 |
updated_id |
记录更新者,用于审计追踪 | 可选 |
数据隔离层级
1. 系统级 (tenant_id=1 或 NULL)
适用场景: 平台管理、系统配置
特征:
- 由平台超级管理员管理
- 所有租户共享或可见
- 如: 系统菜单、系统字典、系统参数
示例表:
system_tenant(租户表本身)system_menu(tenant_id=NULL的系统菜单)system_dict_type(tenant_id=NULL的系统字典)
2. 租户级 (tenant_id>1, customer_id=NULL)
适用场景: 租户内部管理
特征:
- 属于特定租户,租户间完全隔离
- 租户管理员可管理
- 如: 部门、角色、岗位、租户用户
示例表:
system_dept- 部门system_role- 角色system_position- 岗位system_user(租户用户)
3. 客户级 (tenant_id>1, customer_id>1)
适用场景: 租户下的客户业务数据
特征:
- 属于租户下的特定客户
- 客户用户只能访问本客户数据
- 如: 客户用户、客户专属通知、客户业务数据
示例表:
system_user(客户用户: customer_id>1)system_notice(客户通知: customer_id>1)system_log(客户用户操作日志)
表级别隔离规则
完整隔离规则表
| 表名 | tenant_id | customer_id | 说明 |
|---|---|---|---|
| TenantModel | ❌ | ❌ | 租户表本身,不需要隔离字段 |
| CustomerModel | ✅ 必填 | ❌ | 客户属于租户,不属于客户 |
| UserModel | ✅ 必填 | ✅ 可选 | 租户用户(NULL)或客户用户(>1) |
| DeptModel | ✅ 必填 | ❌ | 部门是租户级组织架构 |
| RoleModel | ✅ 必填 | ❌ | 角色是租户级权限管理 |
| PositionModel | ✅ 必填 | ❌ | 岗位是租户级职位定义 |
| MenuModel | ✅ 可选 | ❌ | NULL=系统菜单,>1=租户菜单 |
| DictTypeModel | ✅ 可选 | ❌ | NULL=系统字典,>1=租户字典 |
| DictDataModel | ✅ 可选 | ❌ | 继承字典类型的隔离级别 |
| NoticeModel | ✅ 必填 | ✅ 可选 | NULL=租户通知,>1=客户通知 |
| OperationLogModel | ✅ 必填 | ✅ 可选 | 记录操作人的租户和客户信息 |
| ParamsModel | ✅ 可选 | ❌ | NULL=系统参数,>1=租户参数 |
业务表隔离规则 (示例)
对于自定义业务表,根据业务场景选择:
# 场景1: 租户级业务表 (所有租户用户共享)
class ProductModel(ModelMixin):
tenant_id: Mapped[int] # 必填
customer_id: Mapped[int | None] = None # 不需要
# 场景2: 客户级业务表 (客户用户独享)
class OrderModel(ModelMixin):
tenant_id: Mapped[int] # 必填
customer_id: Mapped[int | None] # 可选,客户订单时必填
数据权限实现
角色数据权限 (data_scope字段)
RoleModel.data_scope 定义了角色的数据访问范围:
| 值 | 名称 | SQL过滤条件 | 使用场景 |
|---|---|---|---|
| 1 | 仅本人数据权限 | WHERE created_id = current_user.id |
普通员工只能看自己创建的数据 |
| 2 | 本部门数据权限 | WHERE user.dept_id = current_user.dept_id |
部门经理看本部门所有人的数据 |
| 3 | 本部门及以下数据权限 | WHERE dept.tree_path LIKE 'current_dept.tree_path%' |
总监看本部门及所有下级部门数据 |
| 4 | 全部数据权限 | WHERE tenant_id = current_user.tenant_id |
租户管理员看租户内所有数据 |
| 5 | 自定义数据权限 | WHERE dept_id IN (role关联的部门列表) |
跨部门权限,如人事看多个指定部门 |
权限叠加规则
一个用户有多个角色时:
- 取所有角色
data_scope的最大值 (4>3>2>5>1) - 当包含
data_scope=5时,合并所有角色关联的部门
客户用户特殊限制:
- 无论
data_scope如何,都必须额外加上:AND customer_id = current_user.customer_id - 即客户用户永远只能看本客户的数据
SQL查询示例
1. 仅本人数据权限 (data_scope=1)
# 租户用户查询
query = (
select(OrderModel)
.where(OrderModel.tenant_id == current_user.tenant_id)
.where(OrderModel.created_id == current_user.id)
)
# 客户用户查询 (额外限制客户)
query = (
select(OrderModel)
.where(OrderModel.tenant_id == current_user.tenant_id)
.where(OrderModel.customer_id == current_user.customer_id)
.where(OrderModel.created_id == current_user.id)
)
2. 本部门数据权限 (data_scope=2)
# 查询本部门所有用户创建的数据
dept_user_ids = (
select(UserModel.id)
.where(UserModel.dept_id == current_user.dept_id)
)
query = (
select(OrderModel)
.where(OrderModel.tenant_id == current_user.tenant_id)
.where(OrderModel.created_id.in_(dept_user_ids))
)
3. 本部门及以下数据权限 (data_scope=3)
# 利用 dept.tree_path 字段高效查询
current_tree_path = current_user.dept.tree_path # 如: /1/3/
# 查询本部门及所有子部门
sub_dept_ids = (
select(DeptModel.id)
.where(DeptModel.tree_path.like(f'{current_tree_path}%'))
)
# 查询这些部门的所有用户
dept_user_ids = (
select(UserModel.id)
.where(UserModel.dept_id.in_(sub_dept_ids))
)
query = (
select(OrderModel)
.where(OrderModel.tenant_id == current_user.tenant_id)
.where(OrderModel.created_id.in_(dept_user_ids))
)
4. 全部数据权限 (data_scope=4)
# 租户管理员 - 查询租户内所有数据
query = (
select(OrderModel)
.where(OrderModel.tenant_id == current_user.tenant_id)
)
# 客户用户 - 即使有全部权限也只能看本客户
query = (
select(OrderModel)
.where(OrderModel.tenant_id == current_user.tenant_id)
.where(OrderModel.customer_id == current_user.customer_id)
)
5. 自定义数据权限 (data_scope=5)
# 获取角色关联的部门列表
custom_dept_ids = (
select(RoleDeptsModel.dept_id)
.join(UserRolesModel, UserRolesModel.role_id == RoleDeptsModel.role_id)
.where(UserRolesModel.user_id == current_user.id)
)
# 查询这些部门的用户
dept_user_ids = (
select(UserModel.id)
.where(UserModel.dept_id.in_(custom_dept_ids))
)
query = (
select(OrderModel)
.where(OrderModel.tenant_id == current_user.tenant_id)
.where(OrderModel.created_id.in_(dept_user_ids))
)
最佳实践
1. 新建业务表时的决策树
是否需要多租户隔离?
├─ 是 → 添加 tenant_id (必填)
│ └─ 是否需要客户级隔离?
│ ├─ 是 → 添加 customer_id (可选)
│ └─ 否 → 不添加 customer_id
└─ 否 → 系统级表,不添加 tenant_id
2. 数据权限过滤中间件
建议在 FastAPI 依赖注入中实现数据权限过滤:
from typing import Optional
from sqlalchemy import Select
class DataPermissionFilter:
"""数据权限过滤器"""
def __init__(self, current_user: UserModel):
self.current_user = current_user
self.max_data_scope = self._get_max_data_scope()
def _get_max_data_scope(self) -> int:
"""获取用户所有角色的最大权限范围"""
if not self.current_user.roles:
return 1 # 默认仅本人
return max(int(role.data_scope) for role in self.current_user.roles)
def apply_filter(self, query: Select, model) -> Select:
"""应用数据权限过滤"""
# 1. 基础租户隔离
query = query.where(model.tenant_id == self.current_user.tenant_id)
# 2. 客户用户额外隔离
if self.current_user.customer_id:
query = query.where(model.customer_id == self.current_user.customer_id)
# 3. 数据权限过滤
if self.max_data_scope == 1: # 仅本人
query = query.where(model.created_id == self.current_user.id)
elif self.max_data_scope == 2: # 本部门
dept_user_ids = self._get_dept_user_ids(self.current_user.dept_id)
query = query.where(model.created_id.in_(dept_user_ids))
elif self.max_data_scope == 3: # 本部门及以下
sub_dept_user_ids = self._get_sub_dept_user_ids(self.current_user.dept)
query = query.where(model.created_id.in_(sub_dept_user_ids))
elif self.max_data_scope == 4: # 全部数据
pass # 已通过租户隔离,无需额外过滤
elif self.max_data_scope == 5: # 自定义
custom_dept_user_ids = self._get_custom_dept_user_ids()
query = query.where(model.created_id.in_(custom_dept_user_ids))
return query
3. 查询时必须检查的字段
任何业务查询都应该包含:
# ✅ 正确的查询
query = (
select(Model)
.where(Model.tenant_id == current_user.tenant_id) # 必须
# 如果用户是客户用户
.where(Model.customer_id == current_user.customer_id) # 必须
# 然后再应用其他业务过滤
)
# ❌ 错误的查询 (缺少租户隔离)
query = select(Model).where(Model.name == 'xxx') # 危险!
4. 审计字段的使用
# 创建记录时
new_record = Model(
tenant_id=current_user.tenant_id,
customer_id=current_user.customer_id, # 如果是客户用户
created_id=current_user.id,
updated_id=current_user.id,
# ... 其他字段
)
# 更新记录时
record.updated_id = current_user.id
record.updated_time = datetime.now()
5. 树形路径 (tree_path) 的维护
def update_dept_tree_path(dept: DeptModel, session: AsyncSession):
"""更新部门树路径"""
if dept.parent_id:
parent = session.get(DeptModel, dept.parent_id)
dept.tree_path = f"{parent.tree_path}{dept.id}/"
else:
dept.tree_path = f"/{dept.id}/"
# 递归更新所有子部门的 tree_path
for child in dept.children:
update_dept_tree_path(child, session)
6. 性能优化建议
索引:
-- 必须创建的索引
CREATE INDEX idx_tenant_id ON table_name(tenant_id);
CREATE INDEX idx_customer_id ON table_name(customer_id);
CREATE INDEX idx_created_id ON table_name(created_id);
CREATE INDEX idx_dept_tree_path ON system_dept(tree_path);
-- 联合索引 (提高查询性能)
CREATE INDEX idx_tenant_customer ON table_name(tenant_id, customer_id);
分区表 (当数据量巨大时):
-- 按租户分区
CREATE TABLE orders (
id BIGINT,
tenant_id INT,
-- ...
) PARTITION BY HASH(tenant_id) PARTITIONS 10;
附录: 常见问题
Q1: 为什么 TenantModel 没有 tenant_id?
A: 租户表本身就是顶层隔离单位,它不属于任何租户。如果加上 tenant_id 会导致循环引用。
Q2: customer_id 何时必填?
A:
- 用户表: 当
user_type=2(客户用户)时必填 - 业务表: 当该数据仅属于特定客户时必填
- 日志/通知表: 当记录客户用户操作或发送给客户时必填
Q3: 如何处理跨租户的场景?
A: 系统设计为严格租户隔离,不支持跨租户查询。如需跨租户集成,应通过 API 接口或消息队列实现。
Q4: 客户用户能否拥有 data_scope=4 (全部数据权限)?
A: 可以,但实际查询时仍会限制为 customer_id = current_user.customer_id,即"客户内的全部数据"。
Q5: 如何实现"查看下属数据"的权限?
A: 通过部门树结构实现:
- 设置领导为部门负责人
- 分配
data_scope=3(本部门及以下)的角色 - 系统自动通过
tree_path查询所有下级部门数据
修订历史
| 版本 | 日期 | 说明 |
|---|---|---|
| v1.0 | 2025-11-22 | 初始版本,完成数据隔离设计 |