Skip to content

3.5 逻辑层开发 ​

概述

逻辑层是业务核心。本项目采用 ModuleSpec 泛型声明式基座:查询条件、排序、字典、文件字段、唯一校验、数据权限全部进声明,方法只做三行委托;写不进声明的差异进钩子;钩子也装不下的合法逃生舱是手写方法。

逻辑层文件结构(internal/logic/{模块}.go) ​

text
1. 包注释 + import
2. 类型 sXxx{}(实现 service.IXxx 接口)
3. init() 中 service.RegisterXxx(&sXxx{})     ← 同包自动注册,无需 blank import
4. var xxxSpec = ModuleSpec[...]{...}          ← 差异全部进声明
5. ToData 转换函数(新增/修改共用)
6. 标准方法:三行委托基座
7. 扩展方法:手写实现(Status/Import/Export 等非标准接口)

ModuleSpec 声明(真实代码,摘自 internal/logic/example.go) ​

go
// ============================== 案例演示业务实现 ==============================

// sExample 案例演示业务实现
// 实现 service.IExample 接口,接口定义见 api/v1/example.go
type sExample struct{}

func init() {
	service.RegisterExample(&sExample{})
}

// exampleSerializeMaps 枚举字段到字典编码的映射
// {field}Text 由基座自动填充
var exampleSerializeMaps = map[string]string{
	"status": "example_status",
}

// exampleSpec 案例演示模块声明
//
// 采用声明式实现:查询、输出与规则均在声明中定义,方法仅做委托
var exampleSpec = ModuleSpec[v1.ExampleItem, v1.ExampleItem, v1.ExampleDetailRes]{
	Model:         dao.Example.Ctx,
	LikeFields:    []string{"name"},
	EqFields:      []string{"type", "status"},
	OrderBy:       "sort, id",
	SerializeMaps: exampleSerializeMaps,
	FileFields: []string{
		"avatar",
	},
	FileSaveDir: "example",
	ToData:      exampleToData,
}

泛型三参数:ModuleSpec[Item, LItem, DRes] = 分页行类型 / 列表行类型 / 详情类型(列表与分页同型时写同一个类型)。

ToData 全量表单语义转换(真实代码) ​

go
// exampleToData 全量表单语义转换
// 新增与修改均提交全部字段,兼容两种入参类型
func exampleToData(req any) map[string]any {
	switch r := req.(type) {
	case *v1.ExampleAddReq:
		return map[string]any{
			"name":   r.Name,
			"avatar": r.Avatar,
			"type":   r.Type,
			"status": r.Status,
			"sort":   r.Sort,
		}
	case *v1.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))
}

标准方法:三行委托(真实代码) ​

go
// Page 案例演示分页
// 按查询条件分页返回案例演示列表
func (s *sExample) Page(ctx context.Context, req *v1.ExamplePageReq) (res *v1.ExamplePageRes, err error) {
	list, total, err := exampleSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize())
	if err != nil {
		return nil, err
	}
	return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil
}

// Detail 案例演示详情
// 按主键ID查询详情
func (s *sExample) Detail(ctx context.Context, req *v1.ExampleDetailReq) (res *v1.ExampleDetailRes, err error) {
	return exampleSpec.Detail(ctx, req.Id, req)
}

// Add 新增案例演示
// 新增成功后返回新记录ID
func (s *sExample) Add(ctx context.Context, req *v1.ExampleAddReq) (res *v1.ExampleIdRes, err error) {
	id, err := exampleSpec.Add(ctx, req)
	if err != nil {
		return nil, err
	}
	return &v1.ExampleIdRes{Id: id}, nil
}

// Update 修改案例演示
// 按主键ID更新记录
func (s *sExample) Update(ctx context.Context, req *v1.ExampleUpdateReq) (res *v1.ExampleUpdateRes, err error) {
	if err = exampleSpec.Update(ctx, req.Id, req); err != nil {
		return nil, err
	}
	return &v1.ExampleUpdateRes{}, nil
}

扩展方法:手写实现(真实代码) ​

go
// Status 案例演示状态
// 非标准方法,保留手写实现,设置记录的状态字段
func (s *sExample) Status(ctx context.Context, req *v1.ExampleStatusReq) (res *v1.ExampleStatusRes, err error) {
	if _, err = dao.Example.Ctx(ctx).Where("id", req.Id).Data(map[string]any{"status": req.Status}).Update(); err != nil {
		return nil, err
	}
	return &v1.ExampleStatusRes{}, nil
}

声明字段速查 ​

字段作用
Model数据源,通常 dao.Xxx.Ctx
LikeFields逐列 LIKE(值取 req 同名驼峰字段,空跳过)
EqFields等值过滤(零值跳过)
KeywordFieldsreq.keyword 多列 OR(能力保留在基座,模块默认不声明)
Filter复杂条件逃生舱 func(ctx, m, req) *gdb.Model
OrderBy / ListOrderBy默认排序;OrderableFields 前端排序白名单
IsTree/ParentOf/ChildSetter树形(menu/dept/category 使用)
SerializeMaps{field}Text 字典填充映射
FileFields/MultiFileFields/ContentFields/FileSaveDir文件/富文本声明(写转正、读补 URL)
UniqueFields唯一性预检 []UniqueRule{Field, Group}(Group 如 "tenant_id")
DataScope行级数据权限,ApplyDataScope("dept_id") 一行接线
BeforeAdd/AfterAdd/BeforeUpdate/AfterUpdate/BeforeDelete/AfterDelete/BeforeBatchDelete/AfterBatchDelete/AfterPageList/AfterDetail生命周期钩子
ToDatareq → map[string]any 转换

基座流水线(CRUD 落点,横切逻辑注入点标注) ​

text
Page/List:
  Model(ctx) ─▶ LikeFields ─▶ EqFields ─▶ KeywordFields ─▶ Filter ─▶ DataScope
                  └─ 前置:dao.Ctx 已注入 del_flag=0 + 租户 IN(0,自身)
     ─▶ Count ─▶ applyOrder(白名单) ─▶ .Page(pageNo,pageSize) ─▶ structRows[T]
     ─▶ readTransform:FileFields 补URL → MultiFileFields 补URL
                       → ContentFields 展开[IMG_URL] → FillTextFields({field}Text)
     ─▶ AfterPageList(禁 append)/IsTree 时 List 走 buildTree

Add:
  ToData(req) ─▶ BeforeAdd ─▶ saveTransform(temp→正式, 富文本占位符化)
     ─▶ checkUniqueRules(UniqueFields 预检,Group 支持分组唯一)
     ─▶ Insert(hooks: 盖章 create_*/del_flag/tenant_id,主键自增)
     ─▶ LastInsertId ─▶ AfterAdd ─▶ 返回 {id}

Update:
  ToData(req) ─▶ BeforeUpdate(可用 MergeOld 补零值)─▶ saveTransform
     ─▶ checkUniqueRules(排除自身) ─▶ Update(仅更新 data 键,hooks 盖章)
     ─▶ AfterUpdate

Delete / BatchDelete:
  BeforeDelete(守卫: 存在性/子级/引用) ─▶ Delete(deleteHook 改写 del_flag=1)
     ─▶ AfterDelete;批量为逐条循环 + Before/AfterBatchDelete

::: warning 钩子纪律
1. 钩子内可改元素/data,**禁止 append、禁止 HTTP/文件 IO**(文件迁移统一在 saveTransform)。
2. 钩子错误记 stage 日志后原样上抛(保留 gerror 语义码)。
3. 基座禁止"按模块分支"——差异进不了声明与钩子就手写,这是合法逃生舱。
:::

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