Skip to content

Service 层规范 ​

概述 ​

Service 层是业务逻辑层,各模块 Service 继承 BaseService 获得通用 CRUD 能力。子类只需声明差异点(repository / form / model_name),即可获得完整的增删改查接口。

设计模式

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

必须声明的属性 ​

属性类型说明示例
repositoryRepository 类数据访问层类PositionRepository
formForm 类新增/更新表单校验类PositionForm
status_formForm 类状态更新表单校验类(可选)PositionStatusForm
model_namestr业务对象名称,用于错误提示"岗位"
serialize_mapsdict枚举显示名映射(可选){'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

为什么需要代理函数?

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 表达。

小蚂蚁云团队 · 提供技术支持