6.3 KiB
6.3 KiB
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(...)注册字典查询(涉及字典的模块)