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

183 lines
7.0 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` 获得通用 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` 表达。