Files
FastapiAdmin/backend/docs/业务表设计指南.md
T
zhangtao 9a39a64faf feat(backend): 实现多租户数据隔离与权限管理架构
refactor(backend): 重构模型基类支持租户与客户隔离
feat(backend): 添加客户模块相关模型、CRUD和参数校验
docs(backend): 新增SaaS数据隔离设计方案文档
refactor(backend): 优化日志模块并添加类型注解
fix(backend): 修正字典模块查询参数移除creator字段

style(frontend): 统一按钮组件代码格式
fix(frontend): 修复表格序号计算逻辑
chore(frontend): 更新lint脚本使用pnpm替代npm
2025-11-23 18:59:43 +08:00

14 KiB

业务表设计指南

📋 目录

  1. 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继承顺序

# 正确的继承顺序 (从左到右)
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_bytenant 等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"
    )

参考文档