# Service层规范 ## 概述 Service 层是业务逻辑层,各模块 Service 继承 `BaseService` 获得通用 CRUD 能力。子类只需声明差异点(repo、model、过滤字段、排序、唯一性等),即可获得完整的增删改查接口。 :::tip 设计模式 BaseService 采用模板方法模式:基类定义流程骨架,子类通过声明差异点和覆盖钩子定制行为。 ::: ## PositionService 示例 以岗位模块为例,`src/modules/position/service.py` 完整内容: ```python from typing import Optional from core.base_service import BaseService from position.models import Position from position.repository import position_repo from user.models import User from 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',)` | | `file_dir` | str | 文件存储子目录名 | 默认从表名去掉 `DB_PREFIX` 推导 | | `serialize_extra` | Callable | 列表/详情共用序列化钩子,返回需合并进字典的额外字段 | — | | `detail_serialize` | Callable | 详情专用序列化钩子,仅在 `get_detail` 时合并 | — | :::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 ``` ## 删除方法 `BaseService.delete()` 和 `BaseService.batch_delete()` 内部调用 `repo.batch_delete(ids)` 完成软删除,并统一 R 响应文案: ```python async def delete(self, handler, ids) -> R: try: id_list = list(dict.fromkeys(parse_id_list(str(ids)))) if not id_list: return R.failed(handler, "记录ID不存在") if len(self.repo.get_by_ids(id_list)) != len(id_list): return R.failed(handler, "记录不存在") err = self._before_delete(ids) if err: return R.failed(handler, err) count = self.repo.batch_delete(ids) return R.ok(handler, msg="本次共删除{0}条数据".format(count)) finally: self.repo.close() ``` :::tip 复用规则 自定义 Service 可通过 `_before_delete` 钩子添加业务校验,删除响应文案由基类统一处理。 ::: ## 同步/异步约定 | 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` 获得通用 CRUD 能力。子类声明差异点(repo_cls/model/page_like_fields/page_eq_fields/page_order_by/unique_fields/serialize_maps),覆盖钩子(_before_delete/_serialize 等)定制业务行为。删除操作由基类统一处理。模块级单例供 Handler 调用。