# 核心基类体系 本章详细描述 的核心基类体系,包括 `base_model`、`base_db`、`BaseForm`、`BaseRepository`、`BaseService` 五大基类的设计理念、继承关系和使用方式。以岗位(Position)模块为实例贯穿说明。 ## 基类继承关系 :::tip 基类体系总览 五大基类各司其职,覆盖从数据模型到请求校验再到业务逻辑的完整链路。子类只需声明差异点,即可获得标准化的增删改查能力。 ::: ``` ┌──────────────────────────────────────────────────────────────────┐ │ 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 = 单例 │ └──────────────────────────────────┘ ``` ## base_model — 模型基类 **文件位置**:`src/core/base_model.py` ### 设计目标 为所有业务模型提供统一的通用字段和序列化方法,消除重复定义。 ### 通用字段 ```python 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=已删除 | ### 表注释钩子 ```python # 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=...)`。 ### 序列化方法 ```python # ============================================================ # 子类钩子:自动将 __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 ``` 关键设计点: 1. **敏感字段排除**:`is_delete`、`password`、`salt` 不会出现在序列化结果中 2. **时区处理**:MySQL 的 `DATETIME` 读回为 naive UTC,统一转 aware UTC 后再转本地时间 3. **驼峰命名**:数据库下划线字段名自动转为前端习惯的驼峰命名 ## base_db — 数据库操作基类 **文件位置**:`src/core/base_db.py` ### 设计目标 提供模型实例的持久化方法,事务边界由中间件统一管理。 ```python # 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 ``` :::tip 为什么 save() 不 commit? 事务提交由 `src/middleware/db_session.py` 的 `db_session_middleware` 统一管理:响应体 `code==0` 时 commit,否则 rollback。这样业务代码无需关心事务边界,也避免了部分操作成功、部分失败导致的数据不一致。审计日志等需要独立提交的场景,可使用 `core.database.commit_independently(obj)` 绕过请求事务。 ::: ## BaseForm — 表单基类 **文件位置**:`src/core/base_schemas.py` ### 设计目标 所有创建/编辑表单的 Pydantic 基类,提供统一的可选 `id` 字段和前端空字符串容错处理。 ### 完整代码 ```python # 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` 字段**: ```python id: Optional[int] = Field(None, description="主键ID") ``` `id` 默认为 `None`。创建记录时前端不传 `id`,Pydantic 自动赋默认值;编辑记录时前端传入 `id`,Service 层据此判断是新增还是更新。 **空字符串容错**: ```python @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`。 ### 子类示例 ```python # 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`: ```json { "name": "测试岗位", "status": 1, "sort": 0 } ``` 编辑时传入 `id`: ```json { "id": 1, "name": "修改后", "status": 1, "sort": 1 } ``` ### 使用场景 | 场景 | 是否传 id | BaseForm 行为 | |:----|:----|:----| | 新增 | 不传 | `id = None`,Service 走 `add()` 分支 | | 编辑 | 传入 | `id = 123`,Service 走 `update()` 分支 | | 特定操作 | 不需要 id | 继承 `BaseModel` 而非 `BaseForm`(如 `PositionStatusForm`) | :::tip 何时继承 BaseForm vs BaseModel - **创建/编辑共用的表单**:继承 `BaseForm`,自动获得可选 `id` - **特定操作的表单**(如状态更新、排序调整):直接继承 `BaseModel`,只定义必要字段 ::: ## BaseRepository — 仓库基类 **文件位置**:`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 软删除 | — | ### 软删除约定 :::warning 重要设计约定 - **等值便捷方法**(get_by_id / get_one / get_all / count / filter_by / exists / exists_by_field / paginate)自动追加 `is_delete=0` 过滤,调用方无需关心 - **原始出口** `filter(*criterion)` **不做**软删过滤,由调用方自行追加 `Model.is_delete == 0`,用于 `in_` / `!=` / `and_` / 区间等复杂条件 ::: ### 数据清洗 ```python # 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`。 ## BaseService — 服务基类 **文件位置**:`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)` + 更新人 | ### 批量删除的 R 响应统一 ```python # 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` 将同步删除操作放入线程池: ```python # 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) ``` :::tip 设计要点 Repository 层的 `batch_delete` 只返回删除条数(数据原语),Service 层的 `batch_delete_with_r` 负责 R 响应封装和文案统一。自定义 Service 的删除方法也应复用此静态方法,保证全仓删除提示一致。 ::: ## 实例:Position 模块的基类使用 ### Model(继承 base_model + base_db) ```python # 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) ``` 获得的能力: - 6 个通用字段(id / create_user / create_time / update_user / update_time / is_delete) - `to_dict()` 序列化方法 - `save()` 持久化方法 - 表注释自动写入 ### Repository(继承 BaseRepository) ```python # 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) ``` 获得的能力: - 全部 12 个通用 CRUD 方法(get_by_id / get_by_ids / get_one / get_all / count / exists / exists_by_field / paginate / filter / filter_by / create / update / batch_delete) - 软删除自动过滤(等值方法自动追加 `is_delete=0`) - camelCase → snake_case 数据清洗(`_clean_model_data`) ### Service(继承 BaseService) ```python # 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() ``` 获得的能力: - 8 个标准接口(get_page / get_list / get_detail / add / update / delete / batch_delete / update_status) - 唯一性自动校验 - 分页查询自动构建 - 序列化自动处理 :::tip 极简 CRUD 对于最简单的模块(如 Level 职级),Repository 和 Service 只需声明差异点,无需编写任何自定义方法,即可获得完整的增删改查能力。 ::: ## 总结 五大基类通过模板方法模式和声明式配置,将通用 CRUD 流程固化为标准实现: | 基类 | 职责 | 核心能力 | |:----|:----|:----| | `base_model` | 模型基类 | 通用字段、to_dict() 序列化 | | `base_db` | 持久化基类 | save() 方法 | | `BaseForm` | 表单基类 | 可选 id、空字符串容错 | | `BaseRepository` | 仓库基类 | 12 个 CRUD 方法、软删除自动过滤 | | `BaseService` | 服务基类 | 8 个标准接口、钩子方法、声明式配置 | 子类只需声明差异点(过滤字段、排序规则、唯一性约束等),即可获得企业级的增删改查能力。以岗位模块为例,完整实现只需约 80 行有效代码(不含版权头),其中 Repository 几乎零代码。这种设计使得新模块的开发效率提升 3-5 倍,同时保证了代码风格和行为的一致性。