Files
easygoadmin-wiki/EasyGoAdmin_Gin_EleVue/zh/guide/service.md
T
2026-10-25 17:24:27 +08:00

6.3 KiB
Raw Blame History

3.5 业务层开发(service)

:::tip 概述 service 是业务逻辑的唯一去处。标准五方法(Page/List/Detail/Add/Update/Delete/BatchDelete)通过声明 ModuleSpec 委托基座,差异全部进声明字段与钩子;写不进钩子的复杂逻辑手写方法,合法逃生舱。 :::

标准结构(三段式)

// internal/service/example.go
package service

// sExample 案例演示业务实现(空结构体仅作方法命名空间,不持有状态)
type sExample struct{}

func init() {
    Example = &sExample{}
}

// Example 案例演示业务实例(handler 层引用)
var Example *sExample

跨模块调用同包直调,无需 import:User.Page(...)、repository.NewDeptRepo()。

ModuleSpec 声明

// internal/service/example.go
var exampleSpec = ModuleSpec[dto.ExampleItem, dto.ExampleItem, dto.ExampleDetailRes]{
    Model:         func(ctx context.Context) *gorm.DB { return repository.NewExampleRepo().Ctx(ctx) },
    LikeFields:    []string{"name"},              // name 模糊,空串跳过
    EqFields:      []string{"type", "status"},    // 等值过滤,零值跳过
    OrderBy:       "sort, id",                    // 默认排序
    SerializeMaps: exampleSerializeMaps,          // status → 字典 example_status 回填 statusText
    FileFields:    []string{"avatar"},            // 单图:写侧 temp 转正,读侧补域名
    FileSaveDir:   "example",                     // 转正到 upload/example
    ToData:        exampleToData,                 // req → 萃库 map
}

泛型三参数:行结构 Item、列表元素 LItem(树形时可不同于 Item)、详情结构 DRes。

查询能力声明(固定顺序生效)

声明 生效顺序 行为
Model 1 数据源工厂,必填,通常就是 NewXxxRepo().Ctx(ctx)
LikeFields 2 逐列按请求同名字段拼 LIKE,空串跳过
EqFields 3 等值过滤,零值跳过(需精确匹配零值的条件写 Filter)
KeywordFields 4 请求 keyword 字段对多列 LIKE,括号包成单个 AND
Filter 5 复杂条件追加(禁止重复声明前四项已覆盖的条件)
DataScope 6 数据权限:DataScope: ApplyDataScope("dept_id")

排序链:请求 orderField 命中 OrderableFields 白名单时前端排序生效,否则用 OrderBy。白名单是排序列注入的唯一防线。

输出与写入声明

声明 行为
SerializeMaps 键为枚举字段(snake),值为字典编码;读侧自动回填 {field}Text
FileFields / MultiFileFields 单文件 / 逗号分隔多文件:写侧 saveTransform 转正,读侧补访问域名
ContentFields 富文本字段:写侧占位符化,读侧经 ExpandContentFields 还原
IsTree + ParentOf 树形输出(仅 List 建树;ParentOf 返回 0 表根)
UniqueFields Add/Update 前逐条唯一性预检(并发唯一性仍以数据库 uk 索引为准)

ToData:请求转落库字段

// internal/service/example.go
func exampleToData(req any) map[string]any {
    switch r := req.(type) {
    case *dto.ExampleAddReq:
        return map[string]any{"name": r.Name, "avatar": r.Avatar, "type": r.Type, "status": r.Status, "sort": r.Sort}
    case *dto.ExampleUpdateReq:
        return map[string]any{"name": r.Name, "avatar": r.Avatar, "type": r.Type, "status": r.Status, "sort": r.Sort}
    }
    panic(fmt.Sprintf("exampleToData: 不支持的入参类型 %T", req))
}

只映射业务字段——审计字段(create_user/create_time/update_user/update_time/del_flag/tenant_id)由全局钩子统一补齐,不要在这里赋值。

标准方法(委托基座)

// internal/service/example.go
func (s *sExample) Page(ctx context.Context, req *dto.ExamplePageReq) (*dto.PageData[dto.ExampleItem], error) {
    list, total, err := exampleSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize())
    if err != nil {
        return nil, err
    }
    return dto.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil
}

func (s *sExample) Add(ctx context.Context, req *dto.ExampleAddReq) (*dto.ExampleIdRes, error) {
    id, err := exampleSpec.Add(ctx, req)
    if err != nil {
        return nil, err
    }
    return &dto.ExampleIdRes{Id: id}, nil
}

func (s *sExample) Update(ctx context.Context, req *dto.ExampleUpdateReq) (*dto.EmptyRes, error) {
    if err := exampleSpec.Update(ctx, req.Id, req); err != nil {
        return nil, err
    }
    return &dto.EmptyRes{}, nil
}

func (s *sExample) Delete(ctx context.Context, req *dto.ExampleDeleteReq) (*dto.EmptyRes, error) {
    if err := exampleSpec.Delete(ctx, req.Id); err != nil {
        return nil, err
    }
    return &dto.EmptyRes{}, nil
}

手写方法(复杂逻辑)

标准方法覆盖不了的场景直接手写——这是合法逃生舱,不是妥协:

// internal/service/example.go
// Status 手写单列更新(仅更新 status 一列)
func (s *sExample) Status(ctx context.Context, req *dto.ExampleStatusReq) (*dto.EmptyRes, error) {
    if err := repository.NewExampleRepo().Ctx(ctx).
        Where("id = ?", req.Id).Updates(map[string]any{"status": req.Status}).Error; err != nil {
        return nil, err
    }
    return &dto.EmptyRes{}, nil
}

导入导出、登录、分配菜单、状态流转等都属于手写范畴。导出可复用基座过滤器 spec.applyFilters(ctx, spec.Model(ctx), req) 与字典查询 DictGetText(ctx, dictCode, value)。

业务错误上抛

// internal/service/auth.go(实际代码)——哨兵错误直接返回
return nil, consts.CodeTenantDisabled

// internal/service/base.go(实际代码)——唯一性预检的格式化错误
return apperr.Newf(consts.CodeRecordExists.Code(), "%s已存在", label)

错误原样上抛,禁止在钩子与 service 内做 HTTP 响应或文件 IO 之外的越层处理(钩子还禁止 append 修改切片)。

编写检查清单

  • 三段式结构(sXxx / init / var)
  • 标准方法全部委托 ModuleSpec,业务差异进声明与钩子
  • ToData 不映射审计字段
  • 跨模块同包直调,事务用 db.WithTrans 且回调内用入参 ctx
  • dict 模块 init 中调用 SetDictLookup(...) 注册字典查询(涉及字典的模块)

下一步

→ 3.6 接口层开发(handler)