510 lines
24 KiB
Markdown
510 lines
24 KiB
Markdown
# 核心基类体系
|
||
|
||
本章详细描述项目的核心基类体系,包括 `BaseModel`、`BaseDB`、`BaseForm`、`BaseRepository`、`BaseService` 五大基类的设计理念、继承关系和使用方式。以岗位(Position)模块为实例贯穿说明。
|
||
|
||
## 基类继承关系
|
||
|
||
:::tip 基类体系总览
|
||
五大基类各司其职,覆盖从数据模型到请求校验再到业务逻辑的完整链路。子类只需声明差异点,即可获得标准化的增删改查能力。
|
||
:::
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ SQLAlchemy Base (declarative_base) │
|
||
└──────────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ BaseModel (src/common/base_model.py) │
|
||
│ ─────────────────────────────────────────────────────────────── │
|
||
│ 通用字段: id / create_user / create_time / update_user / │
|
||
│ update_time / is_delete │
|
||
│ 序列化: to_dict() → 驼峰字段名 + UTC 转本地 + 排除敏感字段 │
|
||
└──────────────────────────┬───────────────────────────────────────┘
|
||
│ 多继承
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ BaseDB (src/common/base_db.py) │
|
||
│ ─────────────────────────────────────────────────────────────── │
|
||
│ 持久化: save() → db.session.add + db.session.flush(不 commit) │
|
||
│ 事务: 由 AOP 事务中间件统一管理 │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
Position(BaseModel, BaseDB)
|
||
┌──────────────────────────┐
|
||
│ __tablename__ │
|
||
│ name / status / sort │
|
||
└──────────────────────────┘
|
||
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ BaseForm (src/common/forms/base_form.py) │
|
||
│ ─────────────────────────────────────────────────────────────── │
|
||
│ WTForms 表单基类 │
|
||
│ 可选 id 字段: Optional,创建时可省略 │
|
||
│ 空字符串容错: '' / 'null' / 'undefined' → None │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
PositionForm(BaseForm)
|
||
┌──────────────────────────┐
|
||
│ name / status / sort │
|
||
│ 继承可选 id │
|
||
└──────────────────────────┘
|
||
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ BaseRepository (src/common/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)
|
||
┌──────────────────────────┐
|
||
│ model = Position │
|
||
│ _repository = 单例 │
|
||
└──────────────────────────┘
|
||
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ BaseService (src/common/base_service.py) │
|
||
│ ─────────────────────────────────────────────────────────────── │
|
||
│ 查询: get_page / get_list / get_detail / get_info │
|
||
│ 写操作: add / update / remove / update_status │
|
||
│ 差异点: repository / form / status_form / model_name │
|
||
│ 钩子: before_add / before_update / before_delete │
|
||
│ after_add / after_update / after_delete │
|
||
│ _enrich_page_records / _detail_dict │
|
||
│ _validate_form / _clean_form_data │
|
||
└──────────────────────────┬───────────────────────────────────────┘
|
||
│
|
||
▼
|
||
PositionService(BaseService)
|
||
┌──────────────────────────────────┐
|
||
│ repository = PositionRepository │
|
||
│ form = PositionForm │
|
||
│ status_form = PositionStatusForm │
|
||
│ model_name = "岗位" │
|
||
│ before_delete() → 引用检查 │
|
||
│ 模块级代理函数 │
|
||
└──────────────────────────────────┘
|
||
```
|
||
|
||
## BaseModel — 模型基类
|
||
|
||
**文件位置**:`src/common/base_model.py`
|
||
|
||
### 设计目标
|
||
|
||
为所有业务模型提供统一的通用字段和序列化方法,消除重复定义。
|
||
|
||
### 通用字段
|
||
|
||
```python
|
||
class BaseModel(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/common/base_model.py
|
||
def __init_subclass__(cls, **kwargs):
|
||
super().__init_subclass__(**kwargs)
|
||
comment = getattr(cls, '__table_comment__', None)
|
||
if not comment:
|
||
return
|
||
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=...)`。
|
||
|
||
### 序列化方法
|
||
|
||
关键设计点:
|
||
|
||
1. **敏感字段排除**:`is_delete`、`password`、`salt` 不会出现在序列化结果中
|
||
2. **时区处理**:MySQL 的 `DATETIME` 读回为 naive UTC,统一转 aware UTC 后再转本地时间
|
||
3. **驼峰命名**:数据库下划线字段名自动转为前端习惯的驼峰命名
|
||
|
||
## BaseDB — 数据库操作基类
|
||
|
||
**文件位置**:`src/common/base_db.py`
|
||
|
||
### 设计目标
|
||
|
||
提供模型实例的持久化方法,事务边界由中间件统一管理。
|
||
|
||
```python
|
||
# src/common/base_db.py
|
||
class BaseDB:
|
||
def save(self):
|
||
# 将当前模型实例添加到 SQLAlchemy 会话的持久化上下文中
|
||
db.session.add(self)
|
||
# 立即发送 INSERT SQL 到数据库,获取自增 ID,但不提交事务(事务由中间件统一控制)
|
||
db.session.flush()
|
||
# 返回当前实例,支持链式调用,便于后续操作
|
||
return self
|
||
```
|
||
|
||
:::tip 为什么 save() 不 commit?
|
||
事务提交由 `src/middleware/transaction.py` 的 `register_transaction` 统一管理:响应体 `code==0` 时 commit,否则 rollback。这样业务代码无需关心事务边界,也避免了部分操作成功、部分失败导致的数据不一致。审计日志等需要独立提交的场景,可使用 `commit_independently(obj)` 绕过请求事务。
|
||
:::
|
||
|
||
## BaseForm — 表单基类
|
||
|
||
**文件位置**:`src/common/forms/base_form.py`
|
||
|
||
### 设计目标
|
||
|
||
所有创建/编辑表单的 WTForms 基类,提供统一的可选 `id` 字段和前端空字符串容错处理。
|
||
|
||
### 完整代码
|
||
|
||
```python
|
||
# src/common/forms/base_form.py
|
||
# +======================================================================
|
||
# | 模块: 表单基类
|
||
# | 说明: 统一处理前端传入的空字符串 id → None 等通用校验
|
||
# +======================================================================
|
||
|
||
from wtforms import Form, IntegerField
|
||
from wtforms.validators import Optional
|
||
|
||
|
||
class BaseForm(Form):
|
||
"""
|
||
表单基类
|
||
|
||
所有创建/编辑表单应继承此类,自动获得:
|
||
1. id 字段:Optional,前端传空字符串时自动转为 None
|
||
"""
|
||
|
||
id = IntegerField('ID', validators=[Optional()])
|
||
```
|
||
|
||
### 代码解析
|
||
|
||
**可选 `id` 字段**:
|
||
|
||
```python
|
||
id = IntegerField('ID', validators=[Optional()])
|
||
```
|
||
|
||
`id` 默认为 `None`。创建记录时前端不传 `id`,WTForms 自动赋默认值;编辑记录时前端传入 `id`,Service 层据此判断是新增还是更新。
|
||
|
||
### 子类示例
|
||
|
||
```python
|
||
# src/modules/system/position/schema.py
|
||
# +======================================================================
|
||
# | 模块: 岗位表单验证
|
||
# | 说明: 岗位创建/编辑/状态更新的请求数据校验
|
||
# +======================================================================
|
||
from src.common.forms.base_form import BaseForm
|
||
|
||
class PositionForm(BaseForm):
|
||
"""岗位创建/编辑表单"""
|
||
name = StringField('岗位名称', validators=[...])
|
||
status = IntegerField('岗位状态', validators=[...])
|
||
sort = IntegerField('岗位排序', validators=[...])
|
||
```
|
||
|
||
`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 | 继承 `Form` 而非 `BaseForm`(如 `PositionStatusForm`) |
|
||
|
||
:::tip 何时继承 BaseForm vs Form
|
||
- **创建/编辑共用的表单**:继承 `BaseForm`,自动获得可选 `id`
|
||
- **特定操作的表单**(如状态更新、排序调整):直接继承 `Form`,只定义必要字段
|
||
:::
|
||
|
||
## BaseRepository — 仓库基类
|
||
|
||
**文件位置**:`src/common/base_repository.py`
|
||
|
||
### 设计目标
|
||
|
||
封装各模型的数据访问层公共操作,提供统一的 CRUD 接口和软删除约定。
|
||
|
||
### 核心方法一览
|
||
|
||
| 方法 | 签名 | 说明 | 软删过滤 |
|
||
|:---|:---|:---|:---:|
|
||
| `get_by_id` | `(obj_id) → Optional[M]` | 按 ID 获取单条记录 | 是 |
|
||
| `get_by_ids` | `(ids) → List[M]` | 按 ID 列表批量获取 | 是 |
|
||
| `first_by` | `(filters) → Optional[M]` | 等值条件获取单条 | 是 |
|
||
| `exists_by` | `(filters, exclude_id) → bool` | 唯一性校验 | 是 |
|
||
| `count_by` | `(filters) → int` | 等值条件计数 | 是 |
|
||
| `exists` | `(obj_id) → bool` | 检查记录是否存在 | 是 |
|
||
| `count_children` | `(parent_id) → int` | 统计子记录数 | 是 |
|
||
| `find_page` | `(...) → (items, total, pages)` | 统一分页查询 | 是 |
|
||
| `find_list` | `(...) → List[M]` | 统一不分页查询 | 是 |
|
||
| `create` | `(**kwargs) → M` | 创建记录 | — |
|
||
| `update_by_id` | `(obj_id, data) → int` | 按 ID 更新 | — |
|
||
| `update_status` | `(obj_id, status) → int` | 更新状态字段 | — |
|
||
| `delete_by_id` | `(obj_id) → bool` | 软删除单条 | — |
|
||
| `batch_delete` | `(ids) → int` | 批量软删除 | — |
|
||
| `batch_restore` | `(ids) → int` | 批量恢复 | — |
|
||
|
||
### 软删除约定
|
||
|
||
:::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/common/base_service.py
|
||
@classmethod
|
||
def _clean_form_data(cls, form):
|
||
"""
|
||
过滤表单数据
|
||
|
||
仅保留模型真实列字段,防止伪字段进入 update_by_id 触发 SQL 错误。
|
||
camelCase 字段名自动转为 snake_case 后与模型列集合比对。
|
||
"""
|
||
if form is None:
|
||
return {}
|
||
columns = {c.name for c in cls.repository.model.__table__.columns}
|
||
return {k: v for k, v in form.data.items() if convert_snake_case(k) in columns}
|
||
```
|
||
|
||
WTForms Schema 可能用 camelCase(如 `parentId`),而模型列是 snake_case(如 `parent_id`)。`_clean_form_data` 在 Service 层统一转换,同时过滤掉模型中不存在的字段,业务模块无需各自处理。
|
||
|
||
## BaseService — 服务基类
|
||
|
||
**文件位置**:`src/common/base_service.py`
|
||
|
||
### 设计目标
|
||
|
||
通过模板方法模式封装通用 CRUD 流程。子类只需声明差异点,即可获得分页、详情、添加、编辑、删除、批量删除、状态切换等标准接口。
|
||
|
||
### 差异点声明
|
||
|
||
| 差异点 | 类型 | 说明 |
|
||
|:---|:---|:---|
|
||
| `repository` | `BaseRepository` | **必填**,数据仓库类 |
|
||
| `model` | `type[M]` | **必填**,ORM 模型类 |
|
||
| `repository` | Repository 类 | 数据访问层类 | `PositionRepository` |
|
||
| `form` | Form 类 | 新增/更新表单校验类 | `PositionForm` |
|
||
| `status_form` | Form 类 | 状态更新表单校验类(可选) | `PositionStatusForm` |
|
||
| `model_name` | str | 业务对象名称,用于错误提示 | `"岗位"` |
|
||
| `serialize_maps` | dict | 枚举显示名映射(可选) | `{'type': 'operation_type'}` |
|
||
| `serialize_maps` | `Dict[str, Dict]` | 枚举显示名映射 |
|
||
| `file_fields` | `Tuple[str, ...]` | 文件字段(add/update 时自动迁移临时文件,serialize 时自动补全 URL) |
|
||
| `rich_text_fields` | `Tuple[str, ...]` | 富文本字段(含嵌入图片迁移 + XSS 清洗) |
|
||
|
||
### 标准接口
|
||
|
||
| 方法 | 说明 | 返回值 |
|
||
|:---|:---|:---|
|
||
| `get_page()` | 分页查询 | `R.ok(data=[...], count=total)` |
|
||
| `get_list()` | 不分页查询 | `R.ok(data=[...])` |
|
||
| `get_detail(obj_id)` | 详情查询 | `dict` 或 `None` |
|
||
| `add(data)` | 新增记录 | `R.ok(msg='添加成功')` |
|
||
| `update(data)` | 编辑记录 | 含表单校验 + 存在性校验 |
|
||
| `remove(ids)` | 批量软删除 | 逗号分隔 ID 字符串 |
|
||
| `update_status(data)` | 状态切换 | 含状态表单校验 |
|
||
|
||
### 钩子方法
|
||
|
||
| 钩子 | 触发时机 | 默认行为 |
|
||
|:---|:---|:---|
|
||
| `before_add(form)` | 新增前 | 空操作,可返回额外字段字典 |
|
||
| `after_add(obj)` | 新增后 | 空操作 |
|
||
| `before_update(form, obj)` | 编辑前 | 空操作,可返回额外字段字典 |
|
||
| `after_update(obj)` | 编辑后 | 空操作 |
|
||
| `before_delete(ids)` | 删除前 | 空操作,抛 BizError 中止 |
|
||
| `after_delete(count)` | 删除后 | 空操作 |
|
||
| `_enrich_page_records(records)` | 分页富化 | 应用 serialize_maps |
|
||
| `_detail_dict(obj)` | 详情序列化 | model_to_dict + serialize_maps |
|
||
| `_validate_form(data)` | 表单校验 | 调用 form.validate() |
|
||
| `_clean_form_data(form)` | 表单数据过滤 | 保留模型列字段 |
|
||
|
||
## 实例:Position 模块的基类使用
|
||
|
||
### Model(继承 BaseModel + BaseDB)
|
||
|
||
```python
|
||
# src/modules/system/position/models.py
|
||
from sqlalchemy import Column, String, Integer, text
|
||
|
||
from src.common.base_db import BaseDB
|
||
from src.common.base_model import BaseModel
|
||
from config.database import DB_PREFIX
|
||
|
||
|
||
class Position(BaseModel, BaseDB):
|
||
"""岗位模型类"""
|
||
__tablename__ = DB_PREFIX + "position"
|
||
__table_comment__ = "岗位表"
|
||
|
||
name = Column(String(255), nullable=False, index=True, comment="岗位名称")
|
||
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
|
||
from src.common.base_repository import BaseRepository
|
||
from src.common.constants.common import PAGE_SIZE
|
||
from src.modules.system.position.models import Position
|
||
|
||
|
||
class PositionRepository(BaseRepository):
|
||
"""岗位数据仓储"""
|
||
model = Position
|
||
|
||
@classmethod
|
||
def page(cls, params: dict):
|
||
"""岗位分页查询"""
|
||
return cls.find_page(
|
||
page=int(params.get('pageNo', 1)),
|
||
page_size=int(params.get('pageSize', PAGE_SIZE)),
|
||
filters={'status': params.get('status')},
|
||
like_filters={'name': params.get('name')},
|
||
order_by='sort',
|
||
)
|
||
|
||
@classmethod
|
||
def list_enabled(cls, params=None):
|
||
"""查询启用的岗位列表"""
|
||
params = params or {}
|
||
return cls.find_list(
|
||
filters={'status': 1},
|
||
like_filters={'name': params.get('name')},
|
||
order_by='sort',
|
||
)
|
||
```
|
||
|
||
获得的能力:
|
||
- 全部通用 CRUD 方法(classmethod)
|
||
- 软删除自动过滤
|
||
- camelCase → snake_case 数据清洗
|
||
|
||
### Service(继承 BaseService)
|
||
|
||
```python
|
||
# src/modules/system/position/service.py
|
||
from src.common.base_service import BaseService
|
||
from src.core.exceptions import BizError
|
||
from src.modules.system.position.schema import PositionForm, PositionStatusForm
|
||
from src.modules.system.position.repository import PositionRepository
|
||
from src.modules.system.user.repository import UserRepository
|
||
|
||
|
||
class PositionService(BaseService):
|
||
"""岗位服务类"""
|
||
repository = PositionRepository
|
||
form = PositionForm
|
||
status_form = PositionStatusForm
|
||
model_name = "岗位"
|
||
|
||
@classmethod
|
||
def before_delete(cls, ids):
|
||
"""删除前校验:存在用户引用该岗位时禁止删除"""
|
||
for pid in ids:
|
||
if UserRepository.count_by({'position_id': pid}) > 0:
|
||
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
|
||
|
||
|
||
# 模块级代理函数
|
||
get_position_page = PositionService.get_page
|
||
get_position_detail = PositionService.get_detail
|
||
get_info = PositionService.get_info
|
||
add_position = PositionService.add
|
||
update_position = PositionService.update
|
||
delete_positions = PositionService.remove
|
||
update_position_status = PositionService.update_status
|
||
get_position_list = PositionService.get_list
|
||
```
|
||
|
||
获得的能力:
|
||
- 8 个标准接口
|
||
- 唯一性自动校验
|
||
- 分页查询自动构建
|
||
- 序列化自动处理
|
||
|
||
:::tip 极简 CRUD
|
||
对于最简单的模块(如 Level 职级),Repository 和 Service 只需声明差异点,无需编写任何自定义方法,即可获得完整的增删改查能力。
|
||
:::
|
||
|
||
## 总结
|
||
|
||
五大基类通过模板方法模式和声明式配置,将通用 CRUD 流程固化为标准实现:
|
||
|
||
| 基类 | 职责 | 核心能力 |
|
||
|:----|:----|:----|
|
||
| `BaseModel` | 模型基类 | 通用字段、to_dict() 序列化 |
|
||
| `BaseDB` | 持久化基类 | save() 方法 |
|
||
| `BaseForm` | 表单基类 | 可选 id、空字符串容错 |
|
||
| `BaseRepository` | 仓库基类 | 12 个 CRUD 方法、软删除自动过滤 |
|
||
| `BaseService` | 服务基类 | 8 个标准接口、钩子方法、声明式配置 |
|
||
|
||
子类只需声明差异点(过滤字段、排序规则、唯一性约束等),即可获得企业级的增删改查能力。以岗位模块为例,完整实现只需约 80 行有效代码(不含版权头),其中 Repository 几乎零代码。这种设计使得新模块的开发效率提升 3-5 倍,同时保证了代码风格和行为的一致性。
|