Appearance
基控制器
关于控制器
- 控制器也称处理器(handler),它只处理参数,然后返回值,即:解析与效验请求参数(JSON → struct)> 调用 Service > 序列化响应(struct → JSON),可做参数格式合法性检查,如参数非空。
- 控制器和服务隔离的一大好处是,服务层不依赖 HTTP 请求参数,你可以使用其他方式调用服务(方便扩展和测试等),只要把参数传好服务就能单独跑。
- 基控制器代码位于
internal\handler\base.go。
基控制器 handler.Handler[T] 封装了几个通用方法:
| 方法名 | 作用 |
|---|---|
Get | 获取一行编辑数据 |
List | 获取表格列表数据 |
Create | 创建一行数据 |
Update | 更新一行数据 |
Delete | 删除多行数据 |
Sort | 数据排序(配合前端拖拽排序功能) |
Config | 获取控制器层配置 |
BuildSerOpts | 构建服务层所需的选项数据 |
RegisterBaseRoutes | 注册通用 CRUD 路由 |
子业务模块,可直接嵌入 *handler.Handler[model.XXX] 复用以上方法,支持通过函数式选项做差异化配置,或在需要时覆写某个方法,也可以自定义新的方法。
路由注册
基控制器预设了一个 RegisterBaseRoutes 方法,里边使用 Gin 框架原生的路由注册语法,逐一注册了 list、create、delete、sort、get、update 路由。
此方法一般会被子业务模块薄调用,像这样:
go
// RegisterRoutes 注册路由
func (h *AuthAdminHandler) RegisterRoutes(group *gin.RouterGroup) {
// 这种写法可自动挂载重写后的方法
handler.RegisterBaseRoutes(h, group)
}即注册路由时,并不直接使用 RegisterBaseRoutes,而是调用类似以上的子模块的 RegisterRoutes 方法,好处是由于此方法位于子模块内,所以能实现:子模块覆写基控制器方法时,不再需要单独注册一遍路由。
还有个好处是需要额外注册其他路由时,直接在下面写就行了,路由注册的调用处不用改,比如:
go
// RegisterRoutes 注册路由
func (h *AdminHandler) RegisterRoutes(group *gin.RouterGroup) {
// 本控制器只注册自定义路由,不注册基控制器的 CRUD 路由
// handler.RegisterBaseRoutes(h, group)
// 注册登录和注销接口路由
group.POST("/login", h.Login)
group.POST("/logout", middleware.AdminAuthOptional(), h.Logout)
}路由注册方法的调用一般是在
internal\router目录内统一放置,其中按子模块的对应目录结构建立路由文件,然后实例化控制器、服务、仓储,并完成以上写好的RegisterRoutes 注册路由方法的调用。
函数式选项
基控制器可用选项函数有:
| 选项 | 说明 |
|---|---|
WithAdapter(Adapter) | 设置 Get / List 的数据适配器,对出库数据二次加工,避免覆写整个方法 |
WithPreloads([]repository.Preload) | 设置 GORM Preload 预加载关联(关联表) |
WithExtension(ExtensionResolver) | 设置任意扩展数据,赋值给 service.Options.Extension |
WithOmitFields(ActionFields) | 按 CRUD 动作设置出入库黑名单字段 |
WithSelectFields(ActionFields) | 按 CRUD 动作设置出入库白名单字段 |
WithAdapter
如果您需对 Get / List 的出库数据做加工(如补摘要字段、格式化、转树状结构)时,可以使用 WithAdapter,避免覆写整个方法,示例如下:
| 文件 | 功能名 | 说明 |
|---|---|---|
internal\handler\admin\auth\rule.go | 后台权限规则管理 | 利用 WithAdapter 将出库菜单数据转为了 树状 |
internal\handler\admin\crud\log.go | 后台CRUD记录 | 利用 WithAdapter 为出库数据增加了 Label、LangBasicData、ModelBasicData 等多个字段 |
部分开发者考虑到控制器职能的问题:你当然可以在
WithAdapter里边调服务层的方法,控制器层还是只负责调用服务层。
WithPreloads
用于配置预加载关联表的数据;基础抽象不仅仅支持更新当前模型的数据,还支持关联创建/更新/查询,其中关联表的创建和更新可以直接在模型层面定义,而查询时的 预加载,则可以使用 WithPreloads 声明。
GORM 中定义关联预加载使用 Preload 方法,而 WithPreloads 接受的参数,和 GORM Preload 的参数一模一样(切片类型,支持多组定义,遍历后直接透传给 Preload)。
使用关联预加载,需要先在模型上定义好关联关系,您可以参考以下示例代码:
bash
1. 打开后台可视化CRUD,添加一个【远程下拉】字段。
2. 关联表选择【admins - 管理员表】,关联模型选择【Admin】,数据接口 URL 填写:/admin/auth/admin/list
3. 填写任意表名,如:tests,完成CRUD代码生成。
4. 查阅模型文件(internal/model/test.go)中的关联关系建立。
5. 查阅控制器(internal/handler/admin/test.go)中的 WithPreloads 使用。bash
1. 模型位于【internal/model/admin.go】文件的【Admin】和【AdminGroupAccess】模型,其中建立了关联关系。
2. 控制器位于【internal/handler/admin/auth/admin.go】,其中使用了 WithPreloads。
3. 此示例是【嵌套预加载 + 一对多】的关联,可能略显复杂。WithExtension
用于设置任意 扩展数据,赋值给 service.Options.Extension。
当我们需要向服务层的 Create / Update 等基方法传递数据时,由于基方法的签名来自通用接口,有那些参数是固定不可变的,只能额外想办法传递(自建方法时,能直接走方法参数传递的就直接传,不需要走 WithExtension)。
先在子业务模块的服务层定义好 扩展数据 的类型,如:
go
// AuthAdminExtension 管理员操作扩展参数
type AuthAdminExtension struct {
AdminSession *dto.AdminSession
Xxxx string
}控制器使用 WithExtension 手动传递 扩展数据:
go
// NewAuthAdminHandler 创建管理员账号管理控制器实例
func NewAuthAdminHandler(svc *svcAuth.AuthAdminService) *AuthAdminHandler {
return &AuthAdminHandler{
Handler: handler.NewHandler(svc,
handler.WithExtension(func(c *gin.Context) any {
return &svcAuth.AuthAdminExtension{
// 避免 HTTP 层的中间件侵入到服务层,
// 此处将 middleware.GetAdmin(c) 显式传递为扩展参数
AdminSession: middleware.GetAdmin(c),
Xxxx: "string",
}
}),
),
svc: svc,
}
}服务层使用 扩展数据:
go
// Create 覆写服务通用创建方法
func (s *AuthAdminService) Create(ctx context.Context, tri *bindx.Tri[model.Admin], opts service.Options) error {
ext, ok := opts.Extension.(*AuthAdminExtension)
if !ok || ext.AdminSession == nil {
return errors.New("参数错误,缺少 AdminSession 扩展数据")
}
// 使用扩展数据,带类型
session := ext.AdminSession
}WithOmitFields / WithSelectFields
按 CRUD 动作设定出入库时:选择特定字段(Select),忽略特定字段(Omit)选项,配置会经服务层透传到仓储层,供 *gorm.DB.Omit() 和 *gorm.DB.Select() 方法直接使用。
控制器层配置示例:
go
// NewAuthAdminHandler 创建管理员账号管理控制器实例
func NewAuthAdminHandler(svc *svcAuth.AuthAdminService) *AuthAdminHandler {
return &AuthAdminHandler{
Handler: handler.NewHandler(svc,
handler.WithOmitFields(handler.ActionFields{
// 创建时忽略以下字段不入库
Create: []string{"id", "login_failure", "last_login_at", "last_login_ip", "deleted_at"},
// 更新时忽略以下字段不入库
Update: []string{"id", "group_ids"},
// 获取数据列表时忽略以下字段不获取(其他字段名任然存在,值固定为对应类型的空值)
List: []string{"title"},
// 获取单行数据时忽略以下字段不获取(其他字段名任然存在,值固定为对应类型的空值)
Get: []string{"title"},
}),
handler.WithSelectFields(handler.ActionFields{
// 获取单行数据时只获取以下字段(其他字段名任然存在,值固定为对应类型的空值)
Get: []string{"title"},
// 获取数据列表时只获取以下字段(其他字段名任然存在,值固定为对应类型的空值)
List: []string{"title"},
// 创建时,只入库以下字段
Create: []string{"title"},
// 更新时,只入库以下字段
Update: []string{"title"},
}),
),
svc: svc,
}
}仓储层使用以下方法应用了字段配置:
go
var q gorm.CreateInterface[T] = gorm.G[T](r.DB())
// 入库字段的选择与忽略
if len(opts.SelectFields) > 0 {
q = q.Select(opts.SelectFields[0], opts.SelectFields[1:])
}
if len(opts.OmitFields) > 0 {
q = q.Omit(opts.OmitFields...)
}所有
Select / Omit字段配置都是按需使用的,不需要可以删除对应代码片段。
