签入版本
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# 3.8 参数校验与错误上抛
|
||||
|
||||
:::tip 概述
|
||||
校验在契约层用标签声明,错误在业务层用哨兵错误上抛——两头都不写手工 if。本页说明绑定优先级、校验标签用法与错误传播路径。
|
||||
:::
|
||||
|
||||
## 绑定优先级
|
||||
|
||||
请求参数按固定顺序写入,**后写覆盖**:
|
||||
|
||||
```text
|
||||
query(GET 参数) → body(JSON 体) → path(路径参数)
|
||||
```
|
||||
|
||||
最终优先级表现为 **path > body > query**:同名字段以路径参数为准。每个字段同时带 `json`(body)与 `form`(query)标签即可参与两种来源绑定。
|
||||
|
||||
绑定引擎的入口在 `router.Bind` 包装函数内,handler 拿到的 req 已完成解析与校验。
|
||||
|
||||
## binding / msg 标签
|
||||
|
||||
校验读标准 `binding` 标签,错误消息取 `msg` 标签:
|
||||
|
||||
```go
|
||||
// internal/model/dto/example.go
|
||||
type ExampleDetailReq struct {
|
||||
Id int64 `json:"id" binding:"required" msg:"缺少ID" dc:"记录ID"`
|
||||
}
|
||||
|
||||
type ExampleBatchDeleteReq struct {
|
||||
Ids []int64 `json:"ids" binding:"min=1" msg:"请选择要删除的记录" dc:"ID列表"`
|
||||
}
|
||||
|
||||
// internal/model/dto/user.go(多规则 + 多消息示例)
|
||||
type UserAddReq struct {
|
||||
Username string `json:"username" binding:"required,min=2,max=30" msg:"请输入用户名;用户名长度需在2~30位"`
|
||||
Password string `json:"password" binding:"required,min=6,max=32" msg:"请输入密码;密码长度需在6~32位"`
|
||||
Mobile string `json:"mobile" binding:"omitempty,phone" msg:"手机号格式不正确"`
|
||||
Email string `json:"email" binding:"omitempty,email" msg:"邮箱格式不正确"`
|
||||
}
|
||||
```
|
||||
|
||||
常用规则:
|
||||
|
||||
| 标签 | 含义 | 示例 |
|
||||
|---|---|---|
|
||||
| required | 必填(零值也算缺失) | `binding:"required"` |
|
||||
| min / max | 数值/长度边界 | `binding:"min=1" msg:"请选择记录"` |
|
||||
| oneof | 枚举取值 | `binding:"oneof=1 2"` |
|
||||
| gte / lte | 大于等于 / 小于等于 | `binding:"gte=1,lte=500"` |
|
||||
| omitempty | 空值放行(配合格式规则) | `binding:"omitempty,email"` |
|
||||
| phone / email | 内置格式规则(本项目 validator 扩展) | `binding:"omitempty,phone"` |
|
||||
|
||||
**多消息映射**(utility/validator):`msg` 以 `;` 分段与 `binding` 多规则**一一对应**(如 `required,min=2,max=30` ↔ `请输入用户名;用户名长度需在2~30位`);只写一段时适用于全部规则;分段缺失按规则默认提示兜底。校验**快速失败**:返回第一条错误,消息含字段提示语。内嵌结构体(PageReq)由引擎递归校验。
|
||||
|
||||
校验失败时框架直接以统一信封返回,msg 即上述文案——**不需要 handler 做任何判断**。
|
||||
|
||||
## UnmarshalBody 逃生舱
|
||||
|
||||
文件上传等非 JSON 载荷,结构体实现 `UnmarshalBody(c *gin.Context) error` 即可接管解析:
|
||||
|
||||
```go
|
||||
// internal/model/dto/example.go
|
||||
func (r *ExampleImportReq) UnmarshalBody(c *gin.Context) error {
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return errors.New("请选择文件")
|
||||
}
|
||||
r.File = f
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
返回 error 即校验失败,同样走统一错误信封。
|
||||
|
||||
## 业务错误上抛
|
||||
|
||||
service / 钩子内业务失败一律返回 apperr 错误或哨兵错误,**原样上抛,不做包装**:
|
||||
|
||||
```go
|
||||
// internal/service/auth.go(实际代码)
|
||||
return nil, consts.CodeCaptchaError
|
||||
return nil, consts.CodeUserDisabled
|
||||
|
||||
// internal/service/category.go(实际代码,格式化消息)
|
||||
return apperr.Newf(consts.CodeParamError.Code(), "上级不能选择自己")
|
||||
```
|
||||
|
||||
response 中间件捕获 error 后:
|
||||
|
||||
```text
|
||||
apperr 错误 → 按其 code/msg 直接输出
|
||||
普通 error → 映射为通用失败码,msg 取错误文本
|
||||
panic → recovery 捕获,输出 500 信封并打错误栈日志
|
||||
```
|
||||
|
||||
## 错误处理禁则
|
||||
|
||||
| 禁则 | 原因 |
|
||||
|---|---|
|
||||
| 在 handler 转发 HTTP 状态码 | 统一信封由 response 中间件负责 |
|
||||
| 吞掉错误只记日志 | 上层与前端失去失败感知 |
|
||||
| 包装后重复加前缀 | 钩子与 service 禁止 wrap,错误原样上抛 |
|
||||
| 用 error 做流程控制 | 分支结果用返回值表达,error 只表示失败 |
|
||||
|
||||
## 编写检查清单
|
||||
|
||||
- [ ] 必填字段有 required + msg
|
||||
- [ ] 零值合法的字段不加 required(等值过滤靠 EqFields 零值跳过)
|
||||
- [ ] 文件字段实现 UnmarshalBody
|
||||
- [ ] 业务失败返回哨兵/apperr,不 fmt.Errorf 包装
|
||||
- [ ] 不在 handler 手写参数判断
|
||||
|
||||
## 下一步
|
||||
|
||||
→ [3.9 权限节点配置](/zh/guide/permission.html)
|
||||
Reference in New Issue
Block a user