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
266 lines
7.1 KiB
Markdown
266 lines
7.1 KiB
Markdown
# 多租户数据隔离快速参考
|
|
|
|
## 🎯 核心原则
|
|
|
|
### 三层隔离架构
|
|
```
|
|
系统层 (System) → 租户层 (Tenant) → 客户层 (Customer)
|
|
```
|
|
|
|
### 隔离字段决策表
|
|
|
|
| 问题 | tenant_id | customer_id |
|
|
|------|-----------|-------------|
|
|
| 是租户表本身? | ❌ | ❌ |
|
|
| 是客户表? | ✅ 必填 | ❌ |
|
|
| 是组织架构(部门/角色/岗位)? | ✅ 必填 | ❌ |
|
|
| 是用户表? | ✅ 必填 | ✅ 可选 |
|
|
| 是配置类(菜单/字典/参数)? | ✅ 可选 | ❌ |
|
|
| 是业务数据? | ✅ 必填 | ✅ 根据业务 |
|
|
| 是日志/通知? | ✅ 必填 | ✅ 根据操作人 |
|
|
|
|
## 📊 数据权限 (data_scope)
|
|
|
|
| 值 | 名称 | SQL WHERE 条件 |
|
|
|----|------|----------------|
|
|
| 1 | 仅本人 | `created_id = current_user.id` |
|
|
| 2 | 本部门 | `user.dept_id = current_user.dept_id` |
|
|
| 3 | 本部门及以下 | `dept.tree_path LIKE 'current%'` |
|
|
| 4 | 全部数据 | `tenant_id = current_user.tenant_id` |
|
|
| 5 | 自定义 | `dept_id IN (role_depts)` |
|
|
|
|
**客户用户特殊规则**: 无论什么权限,都额外加 `customer_id = current_user.customer_id`
|
|
|
|
## 🔧 代码模板
|
|
|
|
### 1. 租户级业务表
|
|
|
|
```python
|
|
class ProductModel(ModelMixin):
|
|
"""产品表 - 租户级"""
|
|
__tablename__ = "business_product"
|
|
|
|
name: Mapped[str] = mapped_column(String(100), comment="产品名称")
|
|
|
|
# 数据隔离
|
|
tenant_id: Mapped[int] = mapped_column(
|
|
Integer,
|
|
ForeignKey("system_tenant.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True
|
|
)
|
|
created_id: Mapped[int | None] = mapped_column(
|
|
Integer,
|
|
ForeignKey("system_user.id", ondelete="SET NULL"),
|
|
nullable=True
|
|
)
|
|
```
|
|
|
|
### 2. 客户级业务表
|
|
|
|
```python
|
|
class OrderModel(ModelMixin):
|
|
"""订单表 - 客户级"""
|
|
__tablename__ = "business_order"
|
|
|
|
order_no: Mapped[str] = mapped_column(String(50), comment="订单号")
|
|
|
|
# 数据隔离
|
|
tenant_id: Mapped[int] = mapped_column(
|
|
Integer,
|
|
ForeignKey("system_tenant.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True
|
|
)
|
|
customer_id: Mapped[int | None] = mapped_column(
|
|
Integer,
|
|
ForeignKey("system_customer.id", ondelete="CASCADE"),
|
|
nullable=True, # 可选,客户订单时必填
|
|
index=True
|
|
)
|
|
created_id: Mapped[int | None] = mapped_column(
|
|
Integer,
|
|
ForeignKey("system_user.id", ondelete="SET NULL"),
|
|
nullable=True
|
|
)
|
|
```
|
|
|
|
### 3. 查询过滤器
|
|
|
|
```python
|
|
class DataPermissionFilter:
|
|
"""通用数据权限过滤器"""
|
|
|
|
@staticmethod
|
|
def apply(query: Select, model, user: UserModel) -> Select:
|
|
# 1. 租户隔离 (必须)
|
|
query = query.where(model.tenant_id == user.tenant_id)
|
|
|
|
# 2. 客户隔离 (如果是客户用户)
|
|
if user.customer_id:
|
|
query = query.where(model.customer_id == user.customer_id)
|
|
|
|
# 3. 数据权限
|
|
max_scope = max(int(r.data_scope) for r in user.roles)
|
|
|
|
if max_scope == 1: # 仅本人
|
|
query = query.where(model.created_id == user.id)
|
|
|
|
elif max_scope == 2: # 本部门
|
|
query = query.where(
|
|
model.created_id.in_(
|
|
select(UserModel.id)
|
|
.where(UserModel.dept_id == user.dept_id)
|
|
)
|
|
)
|
|
|
|
elif max_scope == 3: # 本部门及以下
|
|
query = query.where(
|
|
model.created_id.in_(
|
|
select(UserModel.id)
|
|
.join(DeptModel)
|
|
.where(DeptModel.tree_path.like(f'{user.dept.tree_path}%'))
|
|
)
|
|
)
|
|
|
|
# 4 和 5 省略...
|
|
|
|
return query
|
|
```
|
|
|
|
### 4. 创建记录
|
|
|
|
```python
|
|
async def create_order(data: dict, current_user: UserModel):
|
|
new_order = OrderModel(
|
|
**data,
|
|
tenant_id=current_user.tenant_id, # 必须
|
|
customer_id=current_user.customer_id, # 如果是客户用户
|
|
created_id=current_user.id,
|
|
updated_id=current_user.id
|
|
)
|
|
session.add(new_order)
|
|
await session.commit()
|
|
```
|
|
|
|
## ⚠️ 常见错误
|
|
|
|
### ❌ 错误1: 缺少租户过滤
|
|
|
|
```python
|
|
# 危险! 可能查到其他租户的数据
|
|
query = select(Model).where(Model.name == 'xxx')
|
|
```
|
|
|
|
**✅ 正确**:
|
|
```python
|
|
query = (
|
|
select(Model)
|
|
.where(Model.tenant_id == current_user.tenant_id)
|
|
.where(Model.name == 'xxx')
|
|
)
|
|
```
|
|
|
|
### ❌ 错误2: 客户用户缺少customer_id过滤
|
|
|
|
```python
|
|
# 客户用户可能看到其他客户的数据
|
|
query = select(Model).where(Model.tenant_id == current_user.tenant_id)
|
|
```
|
|
|
|
**✅ 正确**:
|
|
```python
|
|
query = select(Model).where(Model.tenant_id == current_user.tenant_id)
|
|
if current_user.customer_id:
|
|
query = query.where(Model.customer_id == current_user.customer_id)
|
|
```
|
|
|
|
### ❌ 错误3: 给不该有的表加customer_id
|
|
|
|
```python
|
|
# 错误: 部门不属于客户
|
|
class DeptModel(ModelMixin):
|
|
customer_id: Mapped[int] # ❌
|
|
```
|
|
|
|
**✅ 正确**:
|
|
```python
|
|
class DeptModel(ModelMixin):
|
|
tenant_id: Mapped[int] # ✅ 只需要tenant_id
|
|
```
|
|
|
|
### ❌ 错误4: 租户表加tenant_id
|
|
|
|
```python
|
|
# 错误: 租户不属于租户
|
|
class TenantModel(ModelMixin):
|
|
tenant_id: Mapped[int] # ❌ 循环引用
|
|
```
|
|
|
|
**✅ 正确**:
|
|
```python
|
|
class TenantModel(ModelMixin):
|
|
# 不需要tenant_id ✅
|
|
created_id: Mapped[int | None] # 只需要审计字段
|
|
```
|
|
|
|
## 📝 检查清单
|
|
|
|
创建新表时,请检查:
|
|
|
|
- [ ] 是否需要 `tenant_id`? (几乎所有业务表都需要)
|
|
- [ ] 是否需要 `customer_id`? (只有部分表需要)
|
|
- [ ] 是否需要 `created_id/updated_id`? (推荐加上,用于审计)
|
|
- [ ] 是否创建了必要的索引?
|
|
- [ ] `idx_tenant_id`
|
|
- [ ] `idx_customer_id` (如果有)
|
|
- [ ] `idx_created_id` (如果有)
|
|
- [ ] 关联关系是否正确设置了 `foreign_keys`?
|
|
- [ ] 是否在 `TYPE_CHECKING` 下导入相关模型?
|
|
|
|
查询时,请检查:
|
|
|
|
- [ ] 是否添加了 `tenant_id` 过滤?
|
|
- [ ] 如果当前用户是客户用户,是否添加了 `customer_id` 过滤?
|
|
- [ ] 是否根据角色的 `data_scope` 添加了数据权限过滤?
|
|
- [ ] 是否使用了数据权限过滤器中间件?
|
|
|
|
创建/更新记录时,请检查:
|
|
|
|
- [ ] 是否设置了 `tenant_id`?
|
|
- [ ] 是否根据当前用户设置了 `customer_id`?
|
|
- [ ] 是否设置了 `created_id/updated_id`?
|
|
|
|
## 🚀 性能优化
|
|
|
|
### 必须创建的索引
|
|
|
|
```sql
|
|
-- 单列索引
|
|
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_tenant_customer ON table_name(tenant_id, customer_id);
|
|
CREATE INDEX idx_tenant_status ON table_name(tenant_id, status);
|
|
|
|
-- 树形路径索引 (本部门及以下查询)
|
|
CREATE INDEX idx_tree_path ON system_dept(tree_path);
|
|
```
|
|
|
|
### 分区表 (大数据量场景)
|
|
|
|
```sql
|
|
-- 按租户分区
|
|
CREATE TABLE orders (
|
|
id BIGINT,
|
|
tenant_id INT,
|
|
...
|
|
) PARTITION BY HASH(tenant_id) PARTITIONS 10;
|
|
```
|
|
|
|
## 📚 相关文档
|
|
|
|
详细设计请参考: [多租户数据隔离设计方案.md](./多租户数据隔离设计方案.md)
|