# 3.5 逻辑层开发 :::tip 概述 逻辑层是业务核心。本项目采用 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. 基座禁止"按模块分支"——差异进不了声明与钩子就手写,这是合法逃生舱。 :::