183 lines
7.0 KiB
Markdown
183 lines
7.0 KiB
Markdown
# Service 层规范
|
||
|
||
## 概述
|
||
|
||
Service 层是业务逻辑层,各模块 Service 继承 `BaseService` 获得通用 CRUD 能力。子类只需声明差异点(`repository` / `form` / `model_name`),即可获得完整的增删改查接口。
|
||
|
||
:::tip 设计模式
|
||
BaseService 采用模板方法模式:基类定义流程骨架,子类通过声明差异点和覆盖钩子定制行为。所有方法均为 `@classmethod`。
|
||
:::
|
||
|
||
## 标准模块结构
|
||
|
||
```python
|
||
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):
|
||
"""
|
||
岗位服务类
|
||
|
||
继承 BaseService 获得标准 CRUD 能力,并扩展删除前用户引用校验。
|
||
"""
|
||
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} 已被用户引用,无法删除")
|
||
|
||
|
||
# ============================================================
|
||
# 模块级代理函数:保持 router 调用兼容
|
||
# ============================================================
|
||
|
||
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
|
||
```
|
||
|
||
## 必须声明的属性
|
||
|
||
| 属性 | 类型 | 说明 | 示例 |
|
||
|:---|:---|:---|:---|
|
||
| `repository` | Repository 类 | 数据访问层类 | `PositionRepository` |
|
||
| `form` | Form 类 | 新增/更新表单校验类 | `PositionForm` |
|
||
| `status_form` | Form 类 | 状态更新表单校验类(可选) | `PositionStatusForm` |
|
||
| `model_name` | str | 业务对象名称,用于错误提示 | `"岗位"` |
|
||
| `serialize_maps` | dict | 枚举显示名映射(可选) | `{'type': 'operation_type'}` |
|
||
|
||
## 基类提供的通用方法
|
||
|
||
| 方法 | 说明 |
|
||
|:---|:---|
|
||
| `get_page(params)` | 分页查询(委托 `repository.page`) |
|
||
| `get_detail(id)` | 查询详情(返回字典) |
|
||
| `get_info(id)` | 查询模型对象(内部使用) |
|
||
| `add(data)` | 新增记录(含表单校验 + 钩子) |
|
||
| `update(data)` | 更新记录(含表单校验 + 存在性校验 + 钩子) |
|
||
| `remove(ids)` | 批量软删除(逗号分隔 ID 字符串) |
|
||
| `update_status(data)` | 更新状态(含状态表单校验) |
|
||
| `get_list(params)` | 无分页列表查询(委托 `repository.list_enabled`) |
|
||
|
||
## 可覆写钩子方法
|
||
|
||
| 钩子 | 触发时机 | 说明 |
|
||
|:---|:---|:---|
|
||
| `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)` | 详情序列化时 | 自定义返回字段(基类自动应用 serialize_maps) |
|
||
|
||
## 模块级代理函数
|
||
|
||
```python
|
||
# 将 Service 类方法代理为模块级函数,方便路由层直接调用
|
||
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
|
||
```
|
||
|
||
:::tip 为什么需要代理函数?
|
||
Service 的所有方法都是 `@classmethod`,但 Router 层习惯通过模块级函数调用。代理函数将类方法暴露为模块级函数,保持 Router 层调用简洁。
|
||
:::
|
||
|
||
## 复杂模块示例(User)
|
||
|
||
复杂模块可覆写 `add` / `update` 等方法,实现头像保存、密码加密、角色关联等定制逻辑:
|
||
|
||
```python
|
||
# src/modules/system/user/service.py
|
||
class UserService(BaseService):
|
||
repository = UserRepository
|
||
form = UserForm
|
||
status_form = UserStatusForm
|
||
model_name = "用户"
|
||
|
||
@classmethod
|
||
def add(cls, data: dict) -> None:
|
||
"""新增用户(覆写基类方法)"""
|
||
form = cls._validate_form(data)
|
||
|
||
# 头像保存
|
||
avatar = form.data.get("avatar")
|
||
if avatar:
|
||
form.avatar.data = save_file(avatar, "user")
|
||
|
||
# 密码加密
|
||
pwd = form.data.get("password") or DEFAULT_PASSWORD
|
||
salt = generate_salt()
|
||
|
||
# 行政区划校验
|
||
city = form.data.get("city", [])
|
||
if len(city) != 4:
|
||
raise BizError("请选择完整的所在城市(省/市/区/街道)")
|
||
roles = form.data.get("roles")
|
||
|
||
# 清理表单数据
|
||
create_data = cls._clean_form_data(form)
|
||
create_data["password"] = encrypt_password(pwd, salt)
|
||
create_data["salt"] = salt
|
||
create_data["create_user"] = get_realname()
|
||
_apply_city(create_data, city)
|
||
|
||
# 创建用户
|
||
user = cls.repository.create(**create_data)
|
||
user.code = f"{user.id:06d}"
|
||
|
||
# 关联角色
|
||
_add_user_role(user.id, roles)
|
||
# 清除权限缓存
|
||
invalidate_user_cache(user.id)
|
||
|
||
@classmethod
|
||
def _enrich_page_records(cls, records):
|
||
"""分页富化:补充部门、职级、岗位、角色等关联数据"""
|
||
return _enrich_user_records(records)
|
||
|
||
@classmethod
|
||
def _detail_dict(cls, obj):
|
||
"""详情序列化:剔除密码字段,补充关联数据"""
|
||
data = model_to_dict(obj)
|
||
if data:
|
||
data.pop("password", None)
|
||
return _enrich_user_detail(data)
|
||
```
|
||
|
||
## 开发要点
|
||
|
||
1. **必须声明 `repository`**:基类所有数据访问方法依赖此属性
|
||
2. **声明 `form` 和 `status_form`**:用于自动表单校验
|
||
3. **声明 `model_name`**:用于错误提示信息
|
||
4. **删除前校验用 `before_delete` 钩子**:抛出 BizError 中止操作
|
||
5. **必须创建模块级代理函数**:供 Router 层调用
|
||
6. **Service 层不直接读取 request**:参数通过函数传入(dict 类型)
|
||
7. **业务失败通过 `raise BizError` 表达**:不返回 R 对象
|
||
|
||
## 总结
|
||
|
||
Service 层继承 `BaseService` 获得通用 CRUD 能力,只需声明差异点(`repository` / `form` / `model_name`)并覆写钩子方法。所有方法均为 `@classmethod`,必须创建模块级代理函数供 Router 层调用。入参为数据字典,出参为纯数据,业务失败通过 `raise BizError` 表达。
|