Skip to content

Handler

固定骨架,ClientApi / ConfigApi 逐字同构:

go
type XxxApi struct{}
var XxxApiApp = new(XxxApi)

分页查询:绑两次

go
func (a *XxxApi) List(c *gin.Context) {
	var q bo.SysXxxQueryBo
	if err := c.ShouldBindQuery(&q); err != nil {
		_ = c.Error(errs.New(response.CodeBadRequest, "参数校验失败", err.Error()))
		return
	}
	var page pkgrepo.PageQuery
	if err := c.ShouldBindQuery(&page); err != nil {
		_ = c.Error(errs.New(response.CodeBadRequest, "分页参数有误", err.Error()))
		return
	}
	res, err := systemservice.XxxSvcApp.QueryPageList(c.Request.Context(), q, page)
	if err != nil { _ = c.Error(err); return }
	c.JSON(http.StatusOK, response.Ok(res))
}

筛选条件与分页参数同在 query 上, 分两次绑定同一份 URL 参数——query 绑定不消费 body,可重复调用。

导出:POST + ShouldBind + 多取一行

go
func (a *XxxApi) Export(c *gin.Context) {
	var q bo.SysXxxQueryBo
	if err := c.ShouldBind(&q); err != nil {   // 同时吃 form body 与 query
		_ = c.Error(errs.New(response.CodeBadRequest, "参数校验失败", err.Error()))
		return
	}
	rows, err := systemservice.XxxSvcApp.QueryList(c.Request.Context(), q, excel.MaxRows+1)
	if err != nil { _ = c.Error(err); return }
	// 工作簿在 excel.Export 内部先建满缓冲再落笔:
	// middleware.Recover 只在 !Written() 时渲染错误,抢先写字节会让后续错误被静默吞掉。
	if err := excel.Export(c, rows, "Xxx数据"); err != nil { _ = c.Error(err); return }
}
  • POST(前端 commonExport 以 form 表单提交),不是 GET。
  • excel.MaxRows+1 多取一行判超限,避免「先捞完百万行再拒绝」。
  • 业务 handler 中唯一不返回 response.R 的接口,响应体是二进制附件。
  • sheetName = Java .sheetName("...") 的原文。

主键入参

go
// 主键走路径参数,超出 JS 安全整数的 ID 前端会以字符串下发,ParseInt 两种形态都吃得下。
id, err := strconv.ParseInt(c.Param("xxxId"), 10, 64)
if err != nil || id <= 0 {
	_ = c.Error(errs.New(response.CodeBadRequest, "主键不能为空", c.Param("xxxId")))
	return
}

ids, err := parseIDs(c.Param("xxxIds"))   // 批量:复用 handler 包内已有的 parseIDs

parseIDs 已在 internal/system/handler/client_handler.go 定义( 同包共用,别重复定义 ):逗号分隔,任一段非法即整体拒绝——静默丢弃会删成部分成功。其他模块的 handler 包(如 internal/auth/handler没有 这个函数,需要时在各自包内补一个。

写接口:绑 JSON + 翻译哨兵错误

go
func (a *XxxApi) Edit(c *gin.Context) {
	var b bo.SysXxxBo
	if err := c.ShouldBindJSON(&b); err != nil {
		_ = c.Error(errs.New(response.CodeBadRequest, "参数校验失败", err.Error()))
		return
	}
	// 主键校验单独做:SysXxxBo 与新增共用,加 binding:"required" 会连带卡住新增。
	if b.XxxID <= 0 {
		_ = c.Error(errs.New(response.CodeBadRequest, "主键不能为空", ""))
		return
	}
	if err := systemservice.XxxSvcApp.UpdateXxx(c.Request.Context(), &b); err != nil {
		if errors.Is(err, systemservice.ErrXxxKeyExists) {
			_ = c.Error(errs.New(response.CodeFail,
				fmt.Sprintf("修改'%s'失败,key已存在", b.Name), ""))
			return
		}
		if errors.Is(err, systemservice.ErrXxxNotFound) {
			_ = c.Error(errs.New(response.CodeNotFound, "Xxx 不存在", ""))
			return
		}
		_ = c.Error(err)
		return
	}
	c.JSON(http.StatusOK, response.OkVoid())
}

三条铁律

  1. 错误一律 _ = c.Error(...)return,绝不自己 c.JSON 错误响应——统一由 middleware.Recover 渲染(HTTP 状态恒 200)。
  2. 只翻译 service 导出的 哨兵错误errors.Is),其余原样 c.Error(err) 兜底。
  3. 出参只用 response.Ok(...) / response.OkVoid()不写 gin.H

错误码选用

响应体字段 codeHTTP 状态恒为 200,下面列的是 body 里的 code 值):

场景构造响应体 code
参数格式 / 缺失errs.New(response.CodeBadRequest, "参数校验失败", err.Error())400
业务失败(重复、冲突)errs.New(response.CodeFail, "xx失败,yy已存在", "")500
资源不存在errs.New(response.CodeNotFound, "Xxx 不存在", "")404
service 内自定义文案errs.New(0, "内置参数【k】不能删除", "")500 + 该文案

第三个参数 Detail 只进日志不回前端,放 err.Error() 这类内部细节。详见 编码约定 / 错误处理

基于 MIT 协议开源