签入脚本

This commit is contained in:
小蚂蚁云
2026-10-20 11:37:25 +08:00
commit 7f5e8470af
909 changed files with 4045080 additions and 0 deletions
@@ -0,0 +1,266 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 04 模块开发规范与 CRUD 基座
================================================================================
代码位置:api/v1/{模块}.go、internal/controller/{模块}.go、
internal/logic/{模块}.go、internal/service/{模块}.go、
internal/logic/module_spec.go(ModuleSpec 基座)
--------------------------------------------------------------------------------
1. 新增模块四件套(平铺放置,同包 init 自动注册)
--------------------------------------------------------------------------------
api/v1/{模块}.go 全部 Req/Res 结构体 + gmeta 注解(单包 v1)
internal/controller/{模块}.go 控制器 CXxx,方法签名统一:
func (c *CXxx) Page(ctx, req *v1.XxxReq) (*v1.XxxRes, error)
internal/logic/{模块}.go 业务实现 sXxx + var xxxSpec = ModuleSpec[...]{...}
+ init(){ service.RegisterXxx(&sXxx{}) }
internal/service/{模块}.go 手维护接口 IXxx + RegisterXxx + 取用函数 Xxx()
路由:router.go registerControllers 追加 Bind(s, new(controller.CXxx))
(生成器自动写入;手动新增则自行追加在最后一个 Bind 行后)
dao 四件(dao/{m}.go、dao/internal/{m}.go、entity、do)由 gf gen dao 或
生成器产出。
文件命名:表名去 goframe_ 前缀的 snake_case(config_item.go / file_template.go);
前端文件仍用驼峰(fileTemplate.ts)。
--------------------------------------------------------------------------------
2. 标准七件套 + 扩展
--------------------------------------------------------------------------------
方法 HTTP 路由模板 说明
Page GET /api/{prefix}/page 分页(pageNo/pageSize + 筛选)
List GET /api/{prefix}/list 全量/树形(下拉、树选择)
Detail GET /api/{prefix}/detail/:id 详情
Add POST /api/{prefix}/add 新增,返回 {id}
Update PUT /api/{prefix}/update 修改(body 含 id)
Delete DELETE /api/{prefix}/delete/:id 软删(del_flag=1)
BatchDelete DELETE /api/{prefix}/batchDelete body {ids:[]}
Import POST /api/{prefix}/import multipart file(xls/xlsx)
Export GET /api/{prefix}/export 导出文件
Status PUT /api/{prefix}/status 状态切换(有 status 列的模块)
命名约定:结构体 {Mod}{Action}Req / {Mod}{Action}Res;controller 方法名 =
logic 方法名 = 上表方法名。日志标题 "{模块中文名}-{动作}",动作映射:
page=查询分页记录 list=查询列表(树形=查询树形列表) detail=查询详情
add=新增记录 update=修改记录 delete=删除记录 batchDelete=批量删除记录
import=导入数据 export=导出数据 status=设置状态 reset=重置。
--------------------------------------------------------------------------------
3. gmeta 注解完整示例
--------------------------------------------------------------------------------
type UserPageReq struct {
gmeta.Meta `method:"get" path:"/api/user/page" summary:"用户分页"
permission:"sys:user:page" log:"用户管理-查询分页记录" logType:"query"`
common.PageReq
Keyword string `json:"keyword" dc:"关键词"`
Status int `json:"status" dc:"状态"`
}
* permission 空 = 登录即可(如 /api/index/*、/api/job/daemonStatus、
字典别名 /api/dictionary/* 等)
* public:"true" 仅用于免登录路由,且必须同时出现在 middleware/public.go
* 入参 json 一律驼峰;路由参数 :id 由 gmeta path 声明、req 结构同名字段承接
--------------------------------------------------------------------------------
4. ModuleSpec 声明式 CRUD 基座(module_spec.go)
--------------------------------------------------------------------------------
泛型 ModuleSpec[Item, LItem, DRes]:
Item 分页行类型(Page/AfterPageList)
LItem 列表行类型(List/树形,可与 Item 相同)
DRes 详情类型
设计纪律:
1) 基座禁止“按模块分支”——差异只进声明字段与钩子;写不进钩子 → 手写(合法逃生舱)
2) 钩子用具体函数类型;req any 经 mustAs[T] 断言(失败=编程错误,panic)
3) 钩子内可改元素/data,禁止 append、禁止 HTTP/文件 IO(文件迁移统一在 saveTransform)
4) applyFilters 固定顺序:LikeFields → EqFields → KeywordFields → Filter → DataScope;
租户/软删隔离由 dao.Ctx 先于本链注入
5) ToData:Add=全量必填;Update=全量表单语义(前端 Detail 回显 → 全量 PUT)
6) 钩子错误记 stage 日志后原样上抛(保留 gerror 语义码)
4.1 声明字段
Model func(ctx) *gdb.Model 数据源(通常 dao.Xxx.Ctx)
LikeFields []string 逐列 LIKE(值取 req 同名驼峰字段,空跳过)
EqFields []string 等值(零值跳过)
KeywordFields []string req.keyword 多列 OR(能力保留在基座,
模块默认不声明,需要时再用)
Filter func(ctx, m, req) *gdb.Model 复杂条件逃生舱
OrderBy 默认排序(如 "id DESC");ListOrderBy 列表排序;
OrderableFields 前端排序白名单(orderField/orderType 仅白名单内生效)
IsTree/ParentOf/ChildSetter 树形(List 末尾建树;缺 ParentOf fail-fast)
SerializeMaps map[字段]字典编码 {field}Text 填充
FileFields / MultiFileFields / ContentFields /
FileSaveDir 文件/富文本声明(写侧转正、读侧补 URL/还原占位符,见 05 章)
FieldLabels 唯一校验提示覆盖
UniqueFields []UniqueRule{Field, Group} 唯一性预检(Group 支持分组唯一,
如 Group:"tenant_id")
DataScope 行级数据权限(共享工厂 ApplyDataScope(deptColumn),
logic/data_scope.go;nil 跳过;仅 Page/List/导出生效;
模块一行声明即接线,机制见 03 章第 6 节,安全边界见 17 章)
钩子 BeforeAdd/AfterAdd/BeforeUpdate/AfterUpdate/BeforeDelete/
AfterDelete/BeforeBatchDelete/AfterBatchDelete/
AfterPageList(禁 append)/AfterDetail
ToData req → map[string]any(新增/修改共用转换)
4.2 基座流水线
Page applyFilters → Count → applyOrder.Page → structRows → readTransform
→ AfterPageList
List applyFilters → 排序 → req.limit 上限 → structRows → readTransform
→ IsTree 时 buildTree(叶子 Children 预置 [] 而非 null)
Detail Model.Where(id).One → Struct → readTransform → AfterDetail
Add ToData → BeforeAdd → saveTransform(文件/富文本转正)→ checkUniqueRules
→ Insert → LastInsertId → AfterAdd → 返回 id
Update ToData → BeforeUpdate → saveTransform → checkUniqueRules(排除自身)
→ Update(仅更新 data 中出现的键)→ AfterUpdate
Delete BeforeDelete → Delete(钩子改写软删)→ AfterDelete
BatchDelete BeforeBatchDelete → 逐条 BeforeDelete+软删 → AfterBatchDelete
MergeOld(data, id):把 data 中缺失/零值键用库中旧值补齐(全量覆盖辅助)
4.3 标准模块声明示例(节选自 job 模块)
var jobSpec = ModuleSpec[v1.JobItem, v1.JobItem, v1.JobDetailRes]{
Model: dao.Job.Ctx,
LikeFields: []string{"job_name", "job_alias"},
EqFields: []string{"status"},
OrderBy: "id DESC",
SerializeMaps: map[string]string{"status": "job_status", ...},
ToData: jobToData,
BeforeAdd: jobDefaultStatus,
BeforeUpdate: jobExistGuard,
AfterUpdate: jobRemount,
BeforeDelete: jobDeleteGuard,
}
业务方法三行委托:
func (s *sJob) Page(ctx, req) (*v1.JobPageRes, error) {
list, total, err := jobSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize())
...
return v1.NewPageData(..., derefSlice(list)), nil
}
4.4 唯一校验语义(重要)
UniqueFields 为“先查后插”的 UX 快速报错,存在 TOCTOU 竞态;并发唯一性以
数据库 uk_* 唯一索引为准(调用点必须保证对应索引存在)。
Group 语义:校验范围必须等于唯一索引范围——索引含 tenant_id 时必须
Group:"tenant_id",否则范围取窄漏报 1062、取宽跨租户误报。
手写检查(如 user 的 username/mobile)按“被改行现租户”校验,理由相同。
4.5 模块文件结构图(以 config_item 为例)
api/v1/config_item.go
├── type ConfigItemPageReq { gmeta.Meta(get /api/config/item/page …), PageReq, 筛选字段 }
├── type ConfigItemAddReq / UpdateReq / DeleteReq / BatchDeleteReq / …
└── type ConfigItemItem / ListItem / DetailRes / …Res
internal/controller/config_item.go
└── type CConfigItem struct{};方法转发 service.ConfigItem().Page(ctx, req)…
internal/service/config_item.go(手维护)
├── interface IConfigItem { Page/List/Detail/Add/Update/Delete/BatchDelete… }
├── var localConfigItem IConfigItem
├── func ConfigItem() IConfigItem ← controller 取用
└── func RegisterConfigItem(i IConfigItem) ← logic init 注册
internal/logic/config_item.go
├── type sConfigItem struct{}
├── var configItemSpec = ModuleSpec[…]{…} ← 差异全部进声明
├── init(){ service.RegisterConfigItem(&sConfigItem{}) }
└── 方法:三行委托基座 + 手写扩展接口(逃生舱)
internal/dao/config_item.go + dao/internal + entity + do ← 生成器/gf gen dao 产出
4.6 CRUD 流水线流程图(基座执行顺序,横切逻辑落点)
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
--------------------------------------------------------------------------------
5. 字典驱动的 {field}Text
--------------------------------------------------------------------------------
模块头部声明 SerializeMaps = map[字段]字典编码,编码规范
{模块snake}_{字段snake}(如 user_status、job_execute_policy)。
填充路径(二选一,机制同源):
* 基座 FillTextFields:结构体存在 {Field}Text string 字段即自动填充
(反射计划按类型缓存)
* map 场景 SerializeMapOnRead(ctx, row, maps):原地补 row[field+"Text"]
取值 DictGetText(ctx, code, value):查 dict→dict_item 的 name,
进程内缓存 300s(dictTextCache);未命中返回原值。
文案唯一真相在字典管理页(dict_item.name),修改最迟 5 分钟生效
(或调 GET /api/dict/refreshCache 刷新)。
例外:operation_log.type 存注解常量值,用静态表 LogTypeText
(consts.LogTypeNames),不走字典。
--------------------------------------------------------------------------------
6. 筛选对齐三件套(前端 ↔ 后端)
--------------------------------------------------------------------------------
前端 views/{域}/{模块}/querySchemas.ts 每个筛选输入后端对应:
字符串 Input(LikeFields)→ 逐列 LIKE,req 同名驼峰字段取值
Select/InputNumber(EqFields)→ 等值,零值跳过
关键词框(KeywordFields,可选)→ req.keyword 多列 OR
导出统一走 xxxSpec.applyFilters 与列表同套筛选(保证导出=所见)。
声明式之外的复杂条件写 Filter 钩子,不要重复 Like/Eq 已覆盖的条件。
--------------------------------------------------------------------------------
7. service 接口文件规范(手维护)
--------------------------------------------------------------------------------
internal/service/{模块}.go 结构:
type IXCity interface {
Page(ctx context.Context, req *v1.CityPageReq) (res *v1.CityPageRes, err error)
...
}
var localCity ICity
func City() ICity {
if localCity == nil { panic("implement not found ... forgot register?") }
return localCity
}
func RegisterCity(ic ICity) { localCity = ic }
logic 的 init() 调用 RegisterXxx;controller 通过 service.Xxx() 取用。
logic 方法签名变更时同步手工更新接口文件;禁止整体运行 gf gen service。
--------------------------------------------------------------------------------
8. 导入导出实现模式(user / level / example 启用)
--------------------------------------------------------------------------------
Import:POST multipart file(*ghttp.UploadFile + p:"file" 标签)
excel.ReadSheet 解析(首行表头,全单元格字符串,空行跳过)→
逐行映射/校验(唯一、上限、字典值)→ 批量 Insert(默认密码/角色/城市编码
等按模块规则处理)→ 返回导入统计。
Export:GET,与列表同筛选 → ChunkQuery 分批(默认 500/页,防 OOM)→
excel.WriteSheet 写临时文件 → 返回文件 URL(/upload/temp/...)。
日志:logType import/export;权限 sys:{module}:import/export。
--------------------------------------------------------------------------------
9. 跨模块共享的包级函数(logic 单包的红利)
--------------------------------------------------------------------------------
因全部 logic 同包,跨模块复用无需 import:
ResolveCityNames(ctx, codes, sep) / ResolveCityCodes(ctx, cityInfo)(city.go,
user 的 Add/Update/Import/Detail 已接入)
LoadIdentity / LoadTenant(auth.go,供中间件接线与登录路径复用)
ApplyDataScope(deptColumn)(data_scope.go,行级数据权限工厂,
spec 声明 DataScope 即接线,见 03 章第 6 节)
DictGetText / SerializeMapOnRead / FillTextFields(base.go / module_spec.go)
新增包级共享函数时注意 base.go 的“导出函数上限 15 个”纪律(超出拆子包)。
--------------------------------------------------------------------------------
10. 开发一个新模块的推荐步骤
--------------------------------------------------------------------------------
1) 设计表结构(统一审计列 + del_flag + uk_* 唯一索引 + 列注释枚举
"状态:1-正常 2-禁用"),执行 DDL
2) 用生成器生成全套文件(见 07 章),或手动按四件套模板编写
3) 调整 logic:声明 xxxSpec(筛选/树形/字典/文件字段/唯一规则)、补充扩展接口;
表含部门归属列且需行级可见性时补 DataScope: ApplyDataScope("dept_id")
4) 维护 service 接口文件与 router 注册行
5) go build ./... + gofmt + 重启;在菜单管理中为角色勾选新权限节点