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
14 KiB
14 KiB
业务表设计指南
📋 目录
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继承顺序
# 正确的继承顺序 (从左到右)
class MyModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
pass
# 说明:
# 1. ModelMixin 必须在最前面
# 2. 其他Mixin顺序不影响功能,但建议按上述顺序
业务表分类与设计
1. 系统级表 (无租户隔离)
特征: 不需要租户隔离,全局共享
继承: MappedBase (不继承ModelMixin)
示例:
- 租户表本身 (
TenantModel) - 系统配置表
- 全局字典表
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
适用场景:
- 组织架构 (部门、角色、岗位)
- 租户配置 (菜单、参数、字典)
- 租户级业务数据
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
适用场景:
- 客户用户
- 客户订单
- 客户专属数据
- 客户通知
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
适用场景:
- 菜单 (系统菜单 + 租户菜单)
- 字典 (系统字典 + 租户字典)
- 应用 (系统应用 + 租户应用 + 客户应用)
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: 租户级业务表
# -*- 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: 客户级业务表
# -*- 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
class GenTableModel(ModelMixin, UserMixin, TenantMixin):
"""代码生成表 - 租户级"""
pass
场景2: 定时任务
需求: 支持系统任务、租户任务、客户任务
设计: ModelMixin + UserMixin + TenantMixin + CustomerMixin
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为可选
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
class DemoModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin):
"""示例表 - 客户级"""
pass
场景5: MCP服务器配置
需求:
- 系统预置MCP (所有租户可用)
- 租户自定义MCP (仅本租户可用)
- 客户专属MCP (仅该客户可用)
设计: ModelMixin + UserMixin + TenantMixin + CustomerMixin
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: 重复定义基础字段
class MyModel(ModelMixin):
id: Mapped[int] = mapped_column(...) # ❌ ModelMixin已经定义了
status: Mapped[str] = mapped_column(...) # ❌
✅ 正确:
class MyModel(ModelMixin, UserMixin, TenantMixin):
# 只定义业务字段
name: Mapped[str] = mapped_column(...)
❌ 错误2: 不需要CustomerMixin却继承了
# 部门是租户级资源,不需要客户隔离
class DeptModel(ModelMixin, UserMixin, TenantMixin, CustomerMixin): # ❌
pass
✅ 正确:
class DeptModel(ModelMixin, UserMixin, TenantMixin): # ✅
pass
❌ 错误3: 忘记覆盖relationship
class MyModel(ModelMixin, UserMixin, TenantMixin):
# ❌ 没有定义relationship,property返回None
pass
✅ 正确:
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"
)