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 | 等值过滤(零值跳过) |
KeywordFields | req.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 | 生命周期钩子 |
ToData | req → 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. 基座禁止"按模块分支"——差异进不了声明与钩子就手写,这是合法逃生舱。
:::