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
490 lines
14 KiB
Markdown
490 lines
14 KiB
Markdown
# 业务表设计指南
|
|
|
|
## 📋 目录
|
|
1. [Mixin类使用说明](#mixin类使用说明)
|
|
2. [业务表分类与设计](#业务表分类与设计)
|
|
3. [代码示例](#代码示例)
|
|
4. [常见场景](#常见场景)
|
|
|
|
---
|
|
|
|
## Mixin类使用说明
|
|
|
|
### 可用的Mixin类
|
|
|
|
系统提供了以下Mixin类用于快速构建业务表:
|
|
|
|
| Mixin类 | 提供字段 | 用途 | 是否必须 |
|
|
|---------|---------|------|----------|
|
|
| **ModelMixin** | id, uuid, status, description, created_time, updated_time | 基础模型字段 | ✅ 必须 |
|
|
| **UserMixin** | created_id, updated_id | 审计字段,记录创建人和更新人 | ✅ 推荐 |
|
|
| **TenantMixin** | tenant_id | 租户隔离字段 | ✅ 几乎必须 |
|
|
| **CustomerMixin** | customer_id | 客户隔离字段 | ❓ 按需使用 |
|
|
|
|
### Mixin继承顺序
|
|
|
|
```python
|
|
# 正确的继承顺序 (从左到右)
|
|
class MyModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
|
pass
|
|
|
|
# 说明:
|
|
# 1. ModelMixin 必须在最前面
|
|
# 2. 其他Mixin顺序不影响功能,但建议按上述顺序
|
|
```
|
|
|
|
---
|
|
|
|
## 业务表分类与设计
|
|
|
|
### 1. 系统级表 (无租户隔离)
|
|
|
|
**特征**: 不需要租户隔离,全局共享
|
|
|
|
**继承**: `MappedBase` (不继承ModelMixin)
|
|
|
|
**示例**:
|
|
- 租户表本身 (`TenantModel`)
|
|
- 系统配置表
|
|
- 全局字典表
|
|
|
|
```python
|
|
class GlobalConfigModel(MappedBase):
|
|
"""全局配置表 - 不需要租户隔离"""
|
|
__tablename__ = 'sys_global_config'
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
config_key: Mapped[str] = mapped_column(String(100))
|
|
config_value: Mapped[str] = mapped_column(String(500))
|
|
```
|
|
|
|
### 2. 租户级表 (仅租户隔离)
|
|
|
|
**特征**:
|
|
- 属于特定租户
|
|
- 租户间完全隔离
|
|
- 不需要客户级隔离
|
|
|
|
**继承**: `ModelMixin + UserMixin + TenantMixin`
|
|
|
|
**适用场景**:
|
|
- 组织架构 (部门、角色、岗位)
|
|
- 租户配置 (菜单、参数、字典)
|
|
- 租户级业务数据
|
|
|
|
```python
|
|
class ProductModel(ModelMixin, UserMixin, TenantMixin):
|
|
"""产品表 - 租户级"""
|
|
__tablename__ = 'business_product'
|
|
__table_args__ = ({'comment': '产品表'})
|
|
|
|
name: Mapped[str] = mapped_column(String(100), comment='产品名称')
|
|
price: Mapped[float] = mapped_column(Float, comment='价格')
|
|
|
|
# 关联关系
|
|
created_by: Mapped["UserModel | None"] = relationship(
|
|
foreign_keys="ProductModel.created_id",
|
|
lazy="selectin"
|
|
)
|
|
tenant: Mapped["TenantModel"] = relationship(
|
|
foreign_keys="ProductModel.tenant_id",
|
|
lazy="selectin"
|
|
)
|
|
```
|
|
|
|
### 3. 客户级表 (租户+客户隔离)
|
|
|
|
**特征**:
|
|
- 属于特定租户
|
|
- 支持客户级隔离
|
|
- 客户用户只能访问本客户数据
|
|
|
|
**继承**: `ModelMixin + UserMixin + TenantMixin + CustomerMixin`
|
|
|
|
**适用场景**:
|
|
- 客户用户
|
|
- 客户订单
|
|
- 客户专属数据
|
|
- 客户通知
|
|
|
|
```python
|
|
class OrderModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
|
"""订单表 - 客户级"""
|
|
__tablename__ = 'business_order'
|
|
__table_args__ = ({'comment': '订单表'})
|
|
|
|
order_no: Mapped[str] = mapped_column(String(50), comment='订单号')
|
|
amount: Mapped[float] = mapped_column(Float, comment='订单金额')
|
|
|
|
# 关联关系
|
|
created_by: Mapped["UserModel | None"] = relationship(
|
|
foreign_keys="OrderModel.created_id",
|
|
lazy="selectin"
|
|
)
|
|
tenant: Mapped["TenantModel"] = relationship(
|
|
foreign_keys="OrderModel.tenant_id",
|
|
lazy="selectin"
|
|
)
|
|
customer: Mapped["CustomerModel | None"] = relationship(
|
|
foreign_keys="OrderModel.customer_id",
|
|
lazy="selectin"
|
|
)
|
|
```
|
|
|
|
### 4. 混合级表 (支持多级隔离)
|
|
|
|
**特征**:
|
|
- `tenant_id`: 可选 (NULL=系统级, >1=租户级)
|
|
- `customer_id`: 可选 (NULL=租户级, >1=客户级)
|
|
|
|
**继承**: `ModelMixin + UserMixin + TenantMixin + CustomerMixin`
|
|
|
|
**适用场景**:
|
|
- 菜单 (系统菜单 + 租户菜单)
|
|
- 字典 (系统字典 + 租户字典)
|
|
- 应用 (系统应用 + 租户应用 + 客户应用)
|
|
|
|
```python
|
|
class ApplicationModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
|
"""应用表 - 支持三级隔离"""
|
|
__tablename__ = 'app_application'
|
|
|
|
name: Mapped[str] = mapped_column(String(100), comment='应用名称')
|
|
|
|
# 覆盖tenant_id为可选
|
|
tenant_id: Mapped[int | None] = mapped_column(
|
|
Integer,
|
|
ForeignKey("system_tenant.id", ondelete="CASCADE"),
|
|
nullable=True, # 可选,NULL表示系统级应用
|
|
index=True,
|
|
comment="所属租户ID(NULL=系统级,>1=租户级)"
|
|
)
|
|
|
|
# customer_id已经是可选的,无需覆盖
|
|
```
|
|
|
|
---
|
|
|
|
## 代码示例
|
|
|
|
### 完整示例1: 租户级业务表
|
|
|
|
```python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from typing import TYPE_CHECKING
|
|
from sqlalchemy import String, Integer, Float, ForeignKey
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.core.base_model import ModelMixin, UserMixin, TenantMixin
|
|
|
|
if TYPE_CHECKING:
|
|
from app.api.v1.module_system.user.model import UserModel
|
|
from app.api.v1.module_system.tenant.model import TenantModel
|
|
|
|
|
|
class ProductModel(ModelMixin, UserMixin, TenantMixin):
|
|
"""
|
|
产品表 - 租户级业务表
|
|
|
|
数据隔离策略:
|
|
- tenant_id: 必填,实现租户间隔离
|
|
- 不需要customer_id: 产品是租户级资源,所有租户用户共享
|
|
|
|
数据权限:
|
|
- 通过created_id支持"仅本人数据权限"
|
|
- 通过user.dept_id支持部门数据权限
|
|
"""
|
|
__tablename__ = 'business_product'
|
|
__table_args__ = ({'comment': '产品表'})
|
|
__loader_options__ = ["created_by", "updated_by", "tenant"]
|
|
|
|
# 业务字段
|
|
name: Mapped[str] = mapped_column(String(100), nullable=False, comment='产品名称')
|
|
code: Mapped[str] = mapped_column(String(50), nullable=False, comment='产品编码')
|
|
price: Mapped[float] = mapped_column(Float, nullable=False, comment='价格')
|
|
stock: Mapped[int] = mapped_column(Integer, default=0, comment='库存')
|
|
|
|
# 关联关系 (覆盖Mixin中的property)
|
|
created_by: Mapped["UserModel | None"] = relationship(
|
|
foreign_keys="ProductModel.created_id",
|
|
lazy="selectin"
|
|
)
|
|
updated_by: Mapped["UserModel | None"] = relationship(
|
|
foreign_keys="ProductModel.updated_id",
|
|
lazy="selectin"
|
|
)
|
|
tenant: Mapped["TenantModel"] = relationship(
|
|
foreign_keys="ProductModel.tenant_id",
|
|
lazy="selectin"
|
|
)
|
|
```
|
|
|
|
### 完整示例2: 客户级业务表
|
|
|
|
```python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from typing import TYPE_CHECKING
|
|
from datetime import datetime
|
|
from sqlalchemy import String, Integer, Float, DateTime, ForeignKey
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.core.base_model import ModelMixin, UserMixin, TenantMixin, CustomerMixin
|
|
|
|
if TYPE_CHECKING:
|
|
from app.api.v1.module_system.user.model import UserModel
|
|
from app.api.v1.module_system.tenant.model import TenantModel
|
|
from app.api.v1.module_system.customer.model import CustomerModel
|
|
|
|
|
|
class OrderModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
|
"""
|
|
订单表 - 客户级业务表
|
|
|
|
数据隔离策略:
|
|
- tenant_id: 必填,实现租户间隔离
|
|
- customer_id: 可选,客户订单时必填
|
|
* NULL: 租户级订单
|
|
* >0: 客户级订单
|
|
|
|
数据权限:
|
|
- 客户用户: 只能看 customer_id = current_user.customer_id 的订单
|
|
- 租户用户: 可以看本租户所有订单(根据role.data_scope)
|
|
"""
|
|
__tablename__ = 'business_order'
|
|
__table_args__ = ({'comment': '订单表'})
|
|
__loader_options__ = ["created_by", "updated_by", "tenant", "customer"]
|
|
|
|
# 业务字段
|
|
order_no: Mapped[str] = mapped_column(String(50), nullable=False, unique=True, comment='订单号')
|
|
order_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment='下单时间')
|
|
amount: Mapped[float] = mapped_column(Float, nullable=False, comment='订单金额')
|
|
order_status: Mapped[str] = mapped_column(String(10), default='0', comment='订单状态(0:待付款 1:已付款 2:已发货)')
|
|
|
|
# 关联关系
|
|
created_by: Mapped["UserModel | None"] = relationship(
|
|
foreign_keys="OrderModel.created_id",
|
|
lazy="selectin"
|
|
)
|
|
updated_by: Mapped["UserModel | None"] = relationship(
|
|
foreign_keys="OrderModel.updated_id",
|
|
lazy="selectin"
|
|
)
|
|
tenant: Mapped["TenantModel"] = relationship(
|
|
foreign_keys="OrderModel.tenant_id",
|
|
lazy="selectin"
|
|
)
|
|
customer: Mapped["CustomerModel | None"] = relationship(
|
|
foreign_keys="OrderModel.customer_id",
|
|
lazy="selectin"
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## 常见场景
|
|
|
|
### 场景1: 代码生成器
|
|
|
|
**需求**: 租户管理员可以生成代码,不需要客户隔离
|
|
|
|
**设计**: `ModelMixin + UserMixin + TenantMixin`
|
|
|
|
```python
|
|
class GenTableModel(ModelMixin, UserMixin, TenantMixin):
|
|
"""代码生成表 - 租户级"""
|
|
pass
|
|
```
|
|
|
|
### 场景2: 定时任务
|
|
|
|
**需求**: 支持系统任务、租户任务、客户任务
|
|
|
|
**设计**: `ModelMixin + UserMixin + TenantMixin + CustomerMixin`
|
|
|
|
```python
|
|
class JobModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
|
"""
|
|
定时任务表 - 支持三级隔离
|
|
- tenant_id=1, customer_id=NULL: 系统任务
|
|
- tenant_id>1, customer_id=NULL: 租户任务
|
|
- tenant_id>1, customer_id>0: 客户任务
|
|
"""
|
|
pass
|
|
```
|
|
|
|
### 场景3: 应用管理
|
|
|
|
**需求**:
|
|
- 平台预置应用 (所有租户可见)
|
|
- 租户自定义应用 (仅本租户可见)
|
|
- 客户专属应用 (仅该客户可见)
|
|
|
|
**设计**: `ModelMixin + UserMixin + TenantMixin + CustomerMixin` + 覆盖tenant_id为可选
|
|
|
|
```python
|
|
class ApplicationModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
|
"""应用表 - 支持三级隔离"""
|
|
|
|
# 覆盖tenant_id为可选
|
|
tenant_id: Mapped[int | None] = mapped_column(
|
|
Integer,
|
|
ForeignKey("system_tenant.id", ondelete="CASCADE"),
|
|
nullable=True,
|
|
index=True,
|
|
comment="所属租户ID(NULL=系统级,>1=租户级)"
|
|
)
|
|
```
|
|
|
|
### 场景4: 示例/Demo表
|
|
|
|
**需求**: 用于演示,支持客户级隔离
|
|
|
|
**设计**: `ModelMixin + UserMixin + TenantMixin + CustomerMixin`
|
|
|
|
```python
|
|
class DemoModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
|
"""示例表 - 客户级"""
|
|
pass
|
|
```
|
|
|
|
### 场景5: MCP服务器配置
|
|
|
|
**需求**:
|
|
- 系统预置MCP (所有租户可用)
|
|
- 租户自定义MCP (仅本租户可用)
|
|
- 客户专属MCP (仅该客户可用)
|
|
|
|
**设计**: `ModelMixin + UserMixin + TenantMixin + CustomerMixin`
|
|
|
|
```python
|
|
class McpModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
|
|
"""MCP服务器表 - 支持三级隔离"""
|
|
pass
|
|
```
|
|
|
|
---
|
|
|
|
## 决策流程图
|
|
|
|
```
|
|
创建新业务表
|
|
│
|
|
├─ 是否需要多租户隔离?
|
|
│ ├─ 否 → 继承 MappedBase (系统级表)
|
|
│ └─ 是 → 继续
|
|
│
|
|
├─ 是否需要记录创建人/更新人?
|
|
│ ├─ 是 → 添加 UserMixin
|
|
│ └─ 否 → 跳过
|
|
│
|
|
├─ 添加 TenantMixin (租户隔离)
|
|
│
|
|
└─ 是否需要客户级隔离?
|
|
├─ 是 → 添加 CustomerMixin
|
|
│ └─ customer_id可选,根据业务决定是否必填
|
|
└─ 否 → 完成
|
|
|
|
|
|
最终继承组合:
|
|
- 系统级: MappedBase
|
|
- 租户级: ModelMixin + UserMixin + TenantMixin
|
|
- 客户级: ModelMixin + UserMixin + TenantMixin + CustomerMixin
|
|
- 混合级: ModelMixin + UserMixin + TenantMixin + CustomerMixin (覆盖tenant_id为可选)
|
|
```
|
|
|
|
---
|
|
|
|
## 检查清单
|
|
|
|
创建新业务表时,请检查:
|
|
|
|
### 基础检查
|
|
- [ ] 是否继承了 `ModelMixin`? (除非是系统级表)
|
|
- [ ] 是否继承了 `UserMixin`? (推荐,用于审计)
|
|
- [ ] 是否继承了 `TenantMixin`? (几乎所有业务表都需要)
|
|
- [ ] 是否需要 `CustomerMixin`? (根据业务需求)
|
|
|
|
### 字段检查
|
|
- [ ] 是否覆盖了Mixin中的基础字段? (通常不需要)
|
|
- [ ] 是否添加了必要的业务字段?
|
|
- [ ] 字段类型是否正确?
|
|
- [ ] 是否添加了适当的注释?
|
|
|
|
### 关联关系检查
|
|
- [ ] 是否使用了 `TYPE_CHECKING` 导入关联模型?
|
|
- [ ] 是否正确覆盖了 `created_by`、`tenant` 等relationship?
|
|
- [ ] `foreign_keys` 是否使用了字符串形式?
|
|
- [ ] 是否设置了 `lazy="selectin"`?
|
|
|
|
### 表配置检查
|
|
- [ ] 是否设置了 `__tablename__`?
|
|
- [ ] 是否设置了 `__table_args__` (comment)?
|
|
- [ ] 是否设置了 `__loader_options__`?
|
|
|
|
### 索引检查
|
|
- [ ] Mixin已经为 tenant_id、customer_id、created_id 创建了索引
|
|
- [ ] 是否需要为业务字段创建额外索引?
|
|
|
|
---
|
|
|
|
## 常见错误
|
|
|
|
### ❌ 错误1: 重复定义基础字段
|
|
|
|
```python
|
|
class MyModel(ModelMixin):
|
|
id: Mapped[int] = mapped_column(...) # ❌ ModelMixin已经定义了
|
|
status: Mapped[str] = mapped_column(...) # ❌
|
|
```
|
|
|
|
**✅ 正确**:
|
|
```python
|
|
class MyModel(ModelMixin, UserMixin, TenantMixin):
|
|
# 只定义业务字段
|
|
name: Mapped[str] = mapped_column(...)
|
|
```
|
|
|
|
### ❌ 错误2: 不需要CustomerMixin却继承了
|
|
|
|
```python
|
|
# 部门是租户级资源,不需要客户隔离
|
|
class DeptModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin): # ❌
|
|
pass
|
|
```
|
|
|
|
**✅ 正确**:
|
|
```python
|
|
class DeptModel(ModelMixin, UserMixin, TenantMixin): # ✅
|
|
pass
|
|
```
|
|
|
|
### ❌ 错误3: 忘记覆盖relationship
|
|
|
|
```python
|
|
class MyModel(ModelMixin, UserMixin, TenantMixin):
|
|
# ❌ 没有定义relationship,property返回None
|
|
pass
|
|
```
|
|
|
|
**✅ 正确**:
|
|
```python
|
|
class MyModel(ModelMixin, UserMixin, TenantMixin):
|
|
created_by: Mapped["UserModel | None"] = relationship(
|
|
foreign_keys="MyModel.created_id",
|
|
lazy="selectin"
|
|
)
|
|
tenant: Mapped["TenantModel"] = relationship(
|
|
foreign_keys="MyModel.tenant_id",
|
|
lazy="selectin"
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## 参考文档
|
|
|
|
- [多租户数据隔离设计方案](./多租户数据隔离设计方案.md)
|
|
- [数据隔离快速参考](./数据隔离快速参考.md)
|