Files
djangoadmin-wiki/DjangoAdmin_FastAPI_NaiveVue/zh/volume7/service.md
T
2026-09-12 23:12:15 +08:00

188 lines
6.6 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.
# Service层规范
## 概述
Service 层是业务逻辑层,各模块 Service 继承 `BaseService[Model]` 获得通用 CRUD 能力。子类只需声明差异点(repo、model、过滤字段、排序、唯一性等),即可获得完整的增删改查接口。
:::tip 设计模式
BaseService 采用模板方法模式:基类定义流程骨架,子类通过声明差异点和覆盖钩子定制行为。
:::
## PositionService 示例
以岗位模块为例,`src/modules/system/position/service.py` 完整内容:
```python
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()
```
## 差异点声明
| 属性 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `repo` | BaseRepository | 数据仓库实例 | `position_repo` |
| `model` | Model | ORM 模型类 | `Position` |
| `page_like_fields` | tuple | 分页模糊查询字段 | `('name',)` |
| `page_eq_fields` | tuple | 分页等值查询字段 | `('status',)` |
| `page_order_by` | tuple | 分页排序规则 | `(('sort', 'asc'),)` |
| `unique_fields` | dict | 唯一性校验字段 | `{'name': '名称重复'}` |
| `serialize_maps` | dict | 枚举显示名映射 | `{'status': {1: '在用', 2: '停用'}}` |
| `file_fields` | tuple | 文件字段 | `('cover', 'image')` |
| `rich_text_fields` | tuple | 富文本字段 | `('content',)` |
:::tip 单例模式
每个模块在文件末尾创建 Service 实例(如 `position_service`),供 Endpoint 层使用。
:::
## 可覆盖钩子
### 查询钩子
```python
def _apply_page_filters(self, query, request):
"""组装分页过滤条件"""
# 调用基类默认实现(处理 page_like_fields / page_eq_fields)
query = super()._apply_page_filters(query, request)
# 追加固定过滤
query = query.filter(self.model.status == 1)
return query
def _serialize(self, item) -> Dict[str, Any]:
"""列表行序列化"""
data = item.to_dict()
# 自定义序列化逻辑
return data
def _serialize_detail(self, obj) -> Dict[str, Any]:
"""详情序列化"""
data = self._serialize(obj)
# 详情特有字段
return data
```
### 写操作钩子
```python
def _before_add(self, request, data) -> None:
"""新增前扩展(如密码哈希)"""
def _before_update(self, request, data) -> None:
"""编辑前扩展"""
def _before_delete(self, ids) -> Optional[str]:
"""删除前校验:返回 None 放行,返回字符串拦截"""
def _build_create_fields(self, request, data) -> Dict[str, Any]:
"""组装新增字段"""
fields = super()._build_create_fields(request, data)
# 追加自定义字段
return fields
def _build_update_fields(self, request, data) -> Dict[str, Any]:
"""组装更新字段"""
fields = super()._build_update_fields(request, data)
# 追加自定义字段
return fields
```
## batch_delete_with_r 静态方法
```python
@staticmethod
def batch_delete_with_r(repo: BaseRepository, ids) -> R:
"""按逗号分隔ID软删除并返回标准R响应"""
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))
```
:::tip 复用规则
自定义 Service 的删除方法应复用 `BaseService.batch_delete_with_r(repo, ids)`,保证删除响应文案全仓一致。禁止直接调用 `repo.batch_delete` 后自行封装 R。
:::
## 同步/异步约定
| Service 方法 | 同步/异步 | 原因 |
|-------------|---------|------|
| `get_page` / `get_detail` / `add` / `update` / `delete` | 同步 `def` | 纯 DB 操作 |
| `batch_delete` | 异步 `async def` | 需 `await parse_batch_ids(request)` |
| 含 Redis 缓存的方法 | 异步 `async def` | Redis IO 需 await |
:::warning 禁止混用
- 同步 `def` Service 中禁止直接调用异步方法
- 异步 `async def` Endpoint 中禁止直接调用同步 DB 查询(用 `asyncio.to_thread` 包裹)
:::
## serialize_maps 枚举映射
```python
class ArticleService(BaseService[Article]):
# 字典编码方式(推荐)
serialize_maps = {
'source': 'article_source', # 从数据字典取 {value: label}
}
# 字面字典方式(历史兼容)
serialize_maps = {
'status': {1: '在用', 2: '停用'},
}
```
:::tip 序列化结果
`serialize_maps` 中的字段会在序列化时自动补一个 `{field}Text` 字段。例如 `source=1` 会生成 `sourceText="原创"`。
:::
## 总结
Service 层通过继承 `BaseService[Model]` 获得通用 CRUD 能力。子类声明差异点(repo/model/page_like_fields/page_eq_fields/page_order_by/unique_fields/serialize_maps),覆盖钩子(_apply_page_filters/_before_delete/_serialize 等)定制业务行为。删除操作统一复用 `batch_delete_with_r` 静态方法。模块级单例模式供 Endpoint 调用。