188 lines
6.9 KiB
Markdown
188 lines
6.9 KiB
Markdown
# 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. 基座禁止"按模块分支"——差异进不了声明与钩子就手写,这是合法逃生舱。
|
||
:::
|