签入版本

This commit is contained in:
小蚂蚁云
2026-10-25 17:24:27 +08:00
parent 48911cc08f
commit fdbeae092a
1010 changed files with 30682 additions and 95274 deletions
@@ -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)