Files
djangoadmin-wiki/DjangoAdmin_Flask_AntdVue/zh/volume3/base-classes.md
T
2026-09-12 23:12:15 +08:00

510 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 核心基类体系
本章详细描述项目的核心基类体系,包括 `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 倍,同时保证了代码风格和行为的一致性。