Become a sponsor

本章详细描述 的核心基类体系,包括 base_model、base_db、BaseForm、BaseRepository、BaseService 五大基类的设计理念、继承关系和使用方式。以岗位(Position)模块为实例贯穿说明。
基类体系总览
五大基类各司其职,覆盖从数据模型到请求校验再到业务逻辑的完整链路。子类只需声明差异点,即可获得标准化的增删改查能力。
┌──────────────────────────────────────────────────────────────────┐
│ SQLAlchemy Base (declarative_base) │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ base_model (src/core/base_model.py) │
│ ─────────────────────────────────────────────────────────────── │
│ 通用字段: id / create_user / create_time / update_user / │
│ update_time / is_delete │
│ 序列化: to_dict() → 驼峰字段名 + UTC 转本地 + 排除敏感字段 │
└──────────────────────────┬───────────────────────────────────────┘
│ 多继承
▼
┌──────────────────────────────────────────────────────────────────┐
│ base_db (src/core/base_db.py) │
│ ─────────────────────────────────────────────────────────────── │
│ 持久化: save() → db.add + db.flush(不 commit) │
│ 事务: 由 db_session_middleware 统一管理 │
└──────────────────────────────────────────────────────────────────┘
│
▼
Position(base_model, base_db)
┌──────────────────────────┐
│ __tablename__ │
│ name / status / sort │
└──────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ BaseForm (src/core/base_schemas.py) │
│ ─────────────────────────────────────────────────────────────── │
│ Pydantic BaseModel 子类 │
│ 可选 id 字段: Optional[int],创建时可省略 │
│ 空字符串容错: '' / 'null' / 'undefined' → None │
└──────────────────────────────────────────────────────────────────┘
│
▼
PositionForm(BaseForm)
┌──────────────────────────┐
│ name / status / sort │
│ 继承可选 id │
└──────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ BaseRepository[M] (src/core/base_repository.py) │
│ ─────────────────────────────────────────────────────────────── │
│ 查询: get_by_id / get_by_ids / get_one / get_all / count │
│ 分页: paginate → (items, total) │
│ 写操作: create / update / batch_delete │
│ 条件: filter (原始) / filter_by (等值+软删) │
│ 软删除: 等值方法自动追加 is_delete=0 │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
PositionRepository(BaseRepository[Position])
┌──────────────────────────┐
│ pass │
│ position_repo = 单例 │
└──────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ BaseService[M] (src/core/base_service.py) │
│ ─────────────────────────────────────────────────────────────── │
│ 查询: get_page / get_list / get_detail │
│ 写操作: add / update / delete / batch_delete / update_status │
│ 差异点: repo / model / page_like_fields / page_eq_fields / │
│ page_order_by / unique_fields / serialize_maps │
│ 钩子: _before_add / _before_update / _before_delete │
│ _apply_page_filters / _serialize / _serialize_detail │
│ _build_create_fields / _build_update_fields │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
PositionService(BaseService[Position])
┌──────────────────────────────────┐
│ repo = position_repo │
│ model = Position │
│ page_like_fields = ('name',) │
│ page_eq_fields = ('status',) │
│ page_order_by = (('sort','asc'),)│
│ unique_fields = {'name': '...'} │
│ _before_delete() → 引用检查 │
│ position_service = 单例 │
└──────────────────────────────────┘文件位置:src/core/base_model.py
为所有业务模型提供统一的通用字段和序列化方法,消除重复定义。
class base_model(Base):
__abstract__ = True # 抽象基类,不会在数据库中创建对应的表
# 主键:自增整数,所有业务表的唯一标识
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键ID")
# 创建人:记录该条记录的创建者用户名,由 Service 层自动填充
create_user = Column(String(50), nullable=True, comment="创建人用户名")
# 创建时间:记录创建时的 UTC 时间,数据库默认自动生成
create_time = Column(DateTime, default=_UTC_NOW, server_default=func.now(), comment="创建时间")
# 更新人:记录最后修改该条记录的用户名,由 Service 层自动填充
update_user = Column(String(50), nullable=True, comment="最后更新人用户名")
# 更新时间:记录最后修改时间,更新时自动刷新,数据库默认自动生成
update_time = Column(DateTime, default=_UTC_NOW, onupdate=_UTC_NOW, server_default=func.now(), comment="最后更新时间")
# 逻辑删除标记:0 表示正常记录,1 表示已删除,物理删除不使用,保证数据可恢复;添加索引以提升查询性能,Repository 层默认过滤 is_delete=0
is_delete = Column(Integer, default=0, server_default=text('0'), index=True, comment="逻辑删除标识:0-正常,1-已删除")| 字段 | 类型 | 说明 |
|---|---|---|
id | Integer, PK, AI | 主键自增 |
create_user | String(50) | 创建人(realname) |
create_time | DateTime | 创建时间(UTC,server_default) |
update_user | String(50) | 更新人(realname) |
update_time | DateTime | 更新时间(UTC,onupdate 自动触发) |
is_delete | Integer | 软删除标识:0=正常,1=已删除 |
# src/core/base_model.py
# ============================================================
# 子类钩子:自动将 __table_comment__ 写入 __table_args__
# ============================================================
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
comment = getattr(cls, '__table_comment__', None)
if not comment:
return
# 仅对有 __tablename__ 的具体模型操作(跳过纯抽象中间层)
if not getattr(cls, '__tablename__', None):
return
existing = getattr(cls, '__table_args__', {})
if isinstance(existing, dict):
merged = {**existing}
elif isinstance(existing, tuple) and existing:
merged = dict(existing[-1]) if isinstance(existing[-1], dict) else {}
else:
merged = {}
if 'comment' not in merged:
merged['comment'] = comment
cls.__table_args__ = merged子类只需声明 __table_comment__ = "岗位表",基类通过 __init_subclass__ 钩子自动将其写入 __table_args__,无需手动配置 Table(comment=...)。
# ============================================================
# 子类钩子:自动将 __table_comment__ 写入 __table_args__
# ============================================================
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
comment = getattr(cls, '__table_comment__', None)
if not comment:
return
# 仅对有 __tablename__ 的具体模型操作(跳过纯抽象中间层)
if not getattr(cls, '__tablename__', None):
return
existing = getattr(cls, '__table_args__', {})
if isinstance(existing, dict):
merged = {**existing}
elif isinstance(existing, tuple) and existing:
merged = dict(existing[-1]) if isinstance(existing[-1], dict) else {}
else:
merged = {}
if 'comment' not in merged:
merged['comment'] = comment
cls.__table_args__ = merged关键设计点:
is_delete、password、salt 不会出现在序列化结果中DATETIME 读回为 naive UTC,统一转 aware UTC 后再转本地时间文件位置:src/core/base_db.py
提供模型实例的持久化方法,事务边界由中间件统一管理。
# src/core/base_db.py
class base_db:
# 抽象基类,不直接实例化,由业务模型继承
__abstract__ = True
def save(self):
# 从 contextvars 获取当前请求的数据库会话(由 db_session_middleware 注入)
db = get_db_session()
# 将当前模型实例添加到 SQLAlchemy 会话的持久化上下文中
db.add(self)
# 立即发送 INSERT SQL 到数据库,获取自增 ID,但不提交事务(事务由中间件统一控制)
db.flush()
# 返回当前实例,支持链式调用,便于后续操作
return self为什么 save() 不 commit?
事务提交由 src/middleware/db_session.py 的 db_session_middleware 统一管理:响应体 code==0 时 commit,否则 rollback。这样业务代码无需关心事务边界,也避免了部分操作成功、部分失败导致的数据不一致。审计日志等需要独立提交的场景,可使用 core.database.commit_independently(obj) 绕过请求事务。
文件位置:src/core/base_schemas.py
所有创建/编辑表单的 Pydantic 基类,提供统一的可选 id 字段和前端空字符串容错处理。
# src/core/base_schemas.py
# +======================================================================
# | 模块: 表单基类
# | 说明: 统一处理前端传入的空字符串 id → None 等通用校验
# +======================================================================
from typing import Optional
from pydantic import BaseModel, Field, field_validator
# ============================================================
# 表单基类
# ============================================================
class BaseForm(BaseModel):
"""
表单基类
所有创建/编辑表单应继承此类,自动获得:
1. id 字段:Optional[int],前端传空字符串时自动转为 None
"""
id: Optional[int] = Field(None, description="主键ID")
@field_validator('id', mode='before')
@classmethod
def empty_str_to_none(cls, v):
"""前端 POST 空字符串 '' 时转为 None,避免 Pydantic 整数解析报错"""
if v == '' or v == 'null' or v == 'undefined':
return None
return v可选 id 字段:
id: Optional[int] = Field(None, description="主键ID")id 默认为 None。创建记录时前端不传 id,Pydantic 自动赋默认值;编辑记录时前端传入 id,Service 层据此判断是新增还是更新。
空字符串容错:
@field_validator('id', mode='before')
@classmethod
def empty_str_to_none(cls, v):
if v == '' or v == 'null' or v == 'undefined':
return None
return v前端表单在未填写 id 时,可能提交空字符串 ''、字面量 'null' 或 'undefined'。Pydantic 无法将这些值解析为 int,会抛出校验错误。mode='before' 确保此验证器在 Pydantic 类型转换之前执行,将这些边界值统一转为 None。
# src/modules/system/position/schemas.py
# +======================================================================
# | 模块: 岗位表单验证
# | 说明: 岗位创建/编辑/状态更新的请求数据校验
# +======================================================================
from core.base_schemas import BaseForm
# ============================================================
# 岗位表单类
# ============================================================
class PositionForm(BaseForm):
"""岗位创建/编辑表单"""
name: str = Field(..., min_length=1, max_length=150, description="岗位名称")
status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用")
sort: int = Field(..., ge=0, le=99999, description="岗位排序")PositionForm 继承 BaseForm 后自动获得可选 id 字段。创建时 JSON 请求体无需包含 id:
{ "name": "测试岗位", "status": 1, "sort": 0 }编辑时传入 id:
{ "id": 1, "name": "修改后", "status": 1, "sort": 1 }| 场景 | 是否传 id | BaseForm 行为 |
|---|---|---|
| 新增 | 不传 | id = None,Service 走 add() 分支 |
| 编辑 | 传入 | id = 123,Service 走 update() 分支 |
| 特定操作 | 不需要 id | 继承 BaseModel 而非 BaseForm(如 PositionStatusForm) |
何时继承 BaseForm vs BaseModel
BaseForm,自动获得可选 idBaseModel,只定义必要字段文件位置:src/core/base_repository.py
封装各模型的数据访问层公共操作,提供统一的 CRUD 接口和软删除约定。
| 方法 | 签名 | 说明 | 软删过滤 |
|---|---|---|---|
get_by_id | (obj_id) → Optional[M] | 按 ID 获取单条记录 | 是 |
get_by_ids | (ids) → list | 按 ID 列表批量获取 | 是 |
get_one | (**filters) → Optional[M] | 等值条件获取单条 | 是 |
get_all | (**filters) → List[M] | 等值条件获取所有 | 是 |
count | (**filters) → int | 等值条件计数 | 是 |
exists | (obj_id) → bool | 检查记录是否存在 | 是 |
exists_by_field | (field, value, exclude_id) → bool | 唯一性校验 | 是 |
paginate | (page_no, page_size, query) → (items, total) | 分页查询 | 是 |
filter | (*criterion) → Query | 原始条件查询 | 否 |
filter_by | (**kwargs) → Query | 等值条件查询 | 是 |
create | (**fields) → M | 创建记录 | — |
update | (obj_id, data) → int | 按 ID 更新 | 是 |
batch_delete | (ids_str) → int | 按 ID 软删除 | — |
重要设计约定
is_delete=0 过滤,调用方无需关心filter(*criterion) 不做软删过滤,由调用方自行追加 Model.is_delete == 0,用于 in_ / != / and_ / 区间等复杂条件# src/core/base_repository.py
def _clean_model_data(self, data: dict) -> dict:
"""清洗入库数据:camelCase 键转 snake_case 并仅保留模型真实列字段
Pydantic schema 可能用 camelCase(如 parentId)而模型列是 snake_case
(parent_id),直传 Query.update / model(**fields) 会触发 CompileError /
TypeError。统一在数据访问层转换,业务模块无需各自处理。
"""
if not data:
return {}
columns = {c.name for c in self.model.__table__.columns}
return {
convert_snake_case(k): v
for k, v in data.items()
if convert_snake_case(k) in columns
}Pydantic Schema 可能用 camelCase(如 parentId),而模型列是 snake_case(如 parent_id)。_clean_model_data 在数据访问层统一转换,同时过滤掉模型中不存在的字段,业务模块无需各自处理。convert_snake_case 工具函数位于 src/utils/string.py。
文件位置:src/core/base_service.py
通过模板方法模式封装通用 CRUD 流程。子类只需声明差异点,即可获得分页、详情、添加、编辑、删除、批量删除、状态切换等标准接口。
| 差异点 | 类型 | 说明 |
|---|---|---|
repo | BaseRepository[M] | 必填,数据仓库实例 |
model | type[M] | 必填,ORM 模型类 |
page_like_fields | Tuple[str, ...] | 分页模糊查询字段(取同名查询参数,LIKE %值%) |
page_eq_fields | Tuple[str, ...] | 分页等值查询字段(取同名查询参数,=值) |
page_order_by | Tuple[Tuple[str, str], ...] | 分页排序规则,默认 (('id', 'desc'),) |
unique_fields | Dict[str, str] | 唯一性校验字段及提示,如 {'name': '岗位名称不能重复'} |
serialize_maps | Dict[str, Dict] | 枚举显示名映射。值为 str 时从数据字典取 {value: label},值为 dict 时直接按值取名称 |
serialize_extra | Callable | 列表/详情共用序列化钩子,返回需合并进字典的额外字段 |
detail_serialize | Callable | 详情专用序列化钩子,仅在 get_detail 时合并 |
file_fields | Tuple[str, ...] | 文件字段(add/update 时自动迁移临时文件,serialize 时自动补全 URL) |
rich_text_fields | Tuple[str, ...] | 富文本字段(含嵌入图片迁移 + XSS 清洗) |
file_dir | str | 文件存储子目录名,默认从表名去掉 DB_PREFIX 推导 |
| 方法 | 说明 | 返回值 |
|---|---|---|
get_page(request) | 分页查询 | R.page(records, total, size, current, pages) |
get_list(request) | 不分页查询 | R.ok(data=[...]) |
get_detail(obj_id) | 详情查询 | dict 或 None |
add(request, data) | 新增记录 | R.ok(msg='添加成功') |
update(request, data) | 编辑记录 | R.ok(msg='更新成功') |
delete(ids) | 删除记录 | R.ok(msg='本次共删除N条数据') |
batch_delete(request) | 批量删除 | R.ok(...) |
update_status(request, data) | 状态切换 | R.ok(msg='设置成功') |
| 钩子 | 触发时机 | 默认行为 |
|---|---|---|
_before_add(request, data) | 新增前 | 空操作 |
_before_update(request, data) | 编辑前 | 空操作 |
_before_delete(ids) | 删除前 | 返回 None(放行) |
_apply_page_filters(query, request) | 分页查询构建 | 按声明字段过滤 |
_serialize(item) | 列表行序列化 | to_dict() + 枚举映射 + 文件 URL |
_serialize_detail(obj) | 详情序列化 | 同列表 + detail_serialize |
_build_create_fields(request, data) | 新增字段组装 | model_dump(exclude={'id'}) + 创建人 |
_build_update_fields(request, data) | 更新字段组装 | model_dump(exclude={'id'}, exclude_none=True) + 更新人 |
# src/core/base_service.py
@staticmethod
def batch_delete_with_r(repo: BaseRepository, ids) -> R:
"""按逗号分隔ID软删除并返回标准R响应(供自定义 service 复用)
数据原语(repo.batch_delete)在 repository 层;业务校验与文案收敛于此,
保证删除失败/成功的提示全仓一致。
"""
id_list = parse_id_list(str(ids))
if not id_list:
return R.failed("记录ID不存在")
count = repo.batch_delete(ids)
if count != len(id_list):
return R.failed("记录不存在")
return R.ok(msg="本次共删除{0}条数据".format(count))批量删除端点为 async def,因为需要 await parse_batch_ids(request) 异步读取请求体。Service 层的 batch_delete 方法内部通过 asyncio.to_thread 将同步删除操作放入线程池:
# src/core/base_service.py
async def batch_delete(self, request) -> R:
"""批量删除:请求体为 ID 数组 [1,2,3],线程池执行避免阻塞事件循环"""
ids, err = await parse_batch_ids(request)
if err:
return err
return await asyncio.to_thread(self.delete, ids)设计要点
Repository 层的 batch_delete 只返回删除条数(数据原语),Service 层的 batch_delete_with_r 负责 R 响应封装和文案统一。自定义 Service 的删除方法也应复用此静态方法,保证全仓删除提示一致。
# src/modules/system/position/models.py
from sqlalchemy import Column, String, Integer, text
from core.base_db import base_db
from core.base_model import base_model
from core.config import DB_PREFIX
# ============================================================
# 岗位模型
# ============================================================
class Position(base_model, base_db):
"""岗位模型类"""
# ============================================================
# 表名配置
# ============================================================
__tablename__ = DB_PREFIX + "position"
__table_comment__ = "岗位表"
# ============================================================
# 字段定义
# ============================================================
# 岗位名称
name = Column(String(255), nullable=False, index=True, comment="岗位名称")
# 岗位状态:1-在用 2-停用
status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:1-在用 2-停用")
# 岗位排序
sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序")
# ============================================================
# 内置方法
# ============================================================
def __repr__(self):
"""返回岗位名称作为字符串表示"""
return '岗位:{}'.format(self.name)获得的能力:
to_dict() 序列化方法save() 持久化方法# src/modules/system/position/repository.py
# +======================================================================
# | 模块: 岗位数据访问层
# | 说明: 封装岗位模型的数据访问操作
# +======================================================================
"""modules/system/position 数据访问层。"""
from modules.system.position.models import Position
from core.base_repository import BaseRepository
# ============================================================
# 岗位仓库类
# ============================================================
class PositionRepository(BaseRepository[Position]):
"""岗位数据仓库,继承基础仓库获得通用 CRUD 能力"""
pass
# ============================================================
# 仓库实例
# ============================================================
position_repo = PositionRepository(Position)获得的能力:
is_delete=0)_clean_model_data)# src/modules/system/position/service.py
# +======================================================================
# | 模块: 岗位业务逻辑层
# | 说明: 岗位的增删改查、唯一性校验、状态管理等业务处理
# +======================================================================
from typing import Optional
from core.base_service import BaseService
from modules.system.position.models import Position
from modules.system.position.repository import position_repo
from modules.system.user.models import User
from modules.system.user.repository import user_repo
from utils.request import parse_id_list
# ============================================================
# 岗位业务服务类
# ============================================================
class PositionService(BaseService[Position]):
"""岗位业务服务类,继承基础服务获得通用 CRUD 能力"""
# 数据访问与模型
repo = position_repo
model = Position
# 分页差异点
page_like_fields = ('name',)
page_eq_fields = ('status',)
page_order_by = (('sort', 'asc'),)
# 唯一性校验
unique_fields = {'name': '岗位名称不能重复'}
# ============================================================
# 删除前校验
# ============================================================
def _before_delete(self, ids) -> Optional[str]:
"""删除前校验:存在用户引用该岗位时禁止删除,避免用户岗位悬空"""
id_list = parse_id_list(str(ids))
if id_list and user_repo.filter(User.position_id.in_(id_list), User.is_delete == 0).first():
return "存在用户引用该岗位,请先调整用户岗位"
return None
# ============================================================
# 获取岗位数据列表
# ============================================================
def get_position_list(self, request):
"""获取岗位数据列表"""
# 与 /page 一致的筛选条件:name 模糊、status 精确;保留软删过滤与排序
query = self._apply_page_filters(self.repo.filter_by(), request)
query = query.order_by(self.model.sort.asc())
# 返回结果
return [v.to_dict() for v in query.all()]
# ============================================================
# 模块级单例
# ============================================================
position_service = PositionService()获得的能力:
极简 CRUD
对于最简单的模块(如 Level 职级),Repository 和 Service 只需声明差异点,无需编写任何自定义方法,即可获得完整的增删改查能力。
五大基类通过模板方法模式和声明式配置,将通用 CRUD 流程固化为标准实现:
| 基类 | 职责 | 核心能力 |
|---|---|---|
base_model | 模型基类 | 通用字段、to_dict() 序列化 |
base_db | 持久化基类 | save() 方法 |
BaseForm | 表单基类 | 可选 id、空字符串容错 |
BaseRepository | 仓库基类 | 12 个 CRUD 方法、软删除自动过滤 |
BaseService | 服务基类 | 8 个标准接口、钩子方法、声明式配置 |
子类只需声明差异点(过滤字段、排序规则、唯一性约束等),即可获得企业级的增删改查能力。以岗位模块为例,完整实现只需约 80 行有效代码(不含版权头),其中 Repository 几乎零代码。这种设计使得新模块的开发效率提升 3-5 倍,同时保证了代码风格和行为的一致性。