Skip to content

基控制器

关于控制器

  1. 控制器也称处理器(handler),它只处理参数,然后返回值,即:解析与效验请求参数(JSON → struct)> 调用 Service > 序列化响应(struct → JSON),可做参数格式合法性检查,如参数非空。
  2. 控制器和服务隔离的一大好处是,服务层不依赖 HTTP 请求参数,你可以使用其他方式调用服务(方便扩展和测试等),只要把参数传好服务就能单独跑。
  3. 基控制器代码位于 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 字段配置都是按需使用的,不需要可以删除对应代码片段。

定制

TIP

您还可以通过 覆写已有方法自定义方法 来定制子业务控制器。