Files
easygoadmin-wiki/EasyGoAdmin_GoFrame_EleVue/zh/guide/logic.md
T
2026-10-20 21:19:44 +08:00

188 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 基座禁止"按模块分支"——差异进不了声明与钩子就手写,这是合法逃生舱。
:::