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 包内已有的 parseIDsparseIDs 已在 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())
}三条铁律
- 错误一律
_ = c.Error(...)后return,绝不自己c.JSON错误响应——统一由middleware.Recover渲染(HTTP 状态恒 200)。 - 只翻译 service 导出的 哨兵错误(
errors.Is),其余原样c.Error(err)兜底。 - 出参只用
response.Ok(...)/response.OkVoid(), 不写gin.H。
错误码选用
响应体字段 code( HTTP 状态恒为 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() 这类内部细节。详见 编码约定 / 错误处理。