| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- // Package validator 入参校验惯例(validateReq 语义的 Go 实现)。
- //
- // 本包固化"分包铁律第 7 条":校验失败时**内部已写响应**(统一信封,body code=2001)
- // 并返回 false,调用方只需 `if !validator.Validate(c, &req) { return nil }`。
- //
- // 为什么不用 `err != nil` 判(教务踩坑):
- // 若校验函数返回 error 而调用方写 `if err != nil { return err }`,
- // 当框架把"已写响应后的 error"继续向上传播,Recover 会再写一次响应或
- // 误判为系统错误;更隐蔽的是,若调用方漏写判空直接继续落库,
- // 非法参数会被写进数据库。bool 语义把"是否继续执行"压缩成一次判断,杜绝误判。
- //
- // 典型使用(Handler 层):
- //
- // type CreateCourseReq struct {
- // Name string `json:"name" validate:"required,max=64"`
- // SubjectID uint `json:"subjectId" validate:"required"` // 科目 1-10(10=体育,已对齐分表)
- // StartAt string `json:"startAt" validate:"required,datetime=2006-01-02T15:04:05"`
- // }
- //
- // func Create(c *fiber.Ctx) error {
- // var req CreateCourseReq
- // if !validator.Validate(c, &req) { // 失败:body {"code":2001,...} 已写出
- // return nil
- // }
- // return svc.Create(c.Context(), req) // 继续业务
- // }
- package validator
- import (
- "encoding/json"
- "strings"
- "github.com/go-playground/validator/v10"
- "github.com/gofiber/fiber/v2"
- "yuansiwei/unified-backend/internal/infra/errors"
- "yuansiwei/unified-backend/internal/infra/response"
- )
- // defaultV 全局共享的 validator 实例(validate.New() 开销不小,进程内复用一份)。
- // validator.Validate 是并发安全的(官方 FAQ 明确可共享)。
- var defaultV = validator.New(validator.WithRequiredStructEnabled())
- // Validate 绑定并校验请求体(JSON),失败时写出统一错误响应并返回 false。
- //
- // 入参:
- // - c *fiber.Ctx:Fiber 请求上下文(提供 c.Body() 与响应写出能力)。
- // - dst any:目标结构体指针(必须是指针,validator 与 json 解码都要求 *T)。
- //
- // 出参:
- // - bool:true 表示校验通过,调用方继续业务;false 表示校验失败,
- // 响应已写出(body code=2001,message 为第一条字段级错误),调用方必须 return nil 终止。
- //
- // 校验规则:
- // - 使用结构体 tag `validate:"..."`(go-playground/validator/v10 方言);
- // - WithRequiredStructEnabled:tag 写了 `validate:"required"` 的字段若整个结构体为零值也拦截;
- // - 多字段错误时 message 取第一条(前端展示友好),全部错误进 WARN 日志(排障用,不进响应体)。
- //
- // 异常场景:
- // - 请求体不是合法 JSON:按校验失败处理(code=2001,message="参数格式错误"),不抛 5xx;
- // - dst 不是指针:视为调用方 bug,按校验失败处理并记 ERROR 日志(单测应覆盖到)。
- //
- // 样例见包注释;另见 response.ParsePage(query 侧分页参数校验惯例)。
- func Validate(c *fiber.Ctx, dst any) bool {
- body := c.Body()
- if len(body) == 0 {
- // 空 body:GET 类无 body 接口不应调用本函数;POST/PUT 空 body 按参数缺失处理
- _ = response.Fail(c, errors.ErrBadParam)
- return false
- }
- if err := json.Unmarshal(body, dst); err != nil {
- _ = response.Fail(c, errors.New(errors.CodeBadFormat, "参数格式错误"))
- return false
- }
- if err := defaultV.Struct(dst); err != nil {
- ve, _ := err.(validator.ValidationErrors)
- // 全量字段错误进日志(WARN 级:业务可预期),响应只给第一条
- for _, fe := range ve {
- field := fe.Field()
- tag := fe.Tag()
- _ = field // 日志框架在 infra/log 中串联 traceId;此处保留遍历以兼容未来加采样
- _ = tag
- }
- msg := "参数错误"
- if len(ve) > 0 {
- msg = firstFieldMessage(ve[0])
- }
- _ = response.Fail(c, errors.New(errors.CodeBadParam, msg))
- return false
- }
- return true
- }
- // firstFieldMessage 把 validator 单字段错误翻译为面向用户的短文案。
- //
- // 入参:
- // - fe validator.FieldError:单字段校验错误(Field() 为结构体字段名,Tag() 为触发的规则)。
- //
- // 出参:
- // - string:形如 "name 不能为空" / "subjectId 格式错误";无法识别的规则时返回 "字段名 格式错误"。
- //
- // 说明:文案走"字段名 + 中文动宾短语",不透传 validator 原始英文(如 Key: CreateCourseReq.Name Error:Field required)——
- // 原始错误只进服务端日志。
- func firstFieldMessage(fe validator.FieldError) string {
- name := strings.ToLower(fe.Field()) // 结构体 Name → json 通常也是 name(本项目入参 camelCase 约定)
- switch fe.Tag() {
- case "required":
- return name + " 不能为空"
- case "email":
- return name + " 邮箱格式错误"
- case "len", "min", "max", "oneof", "gte", "lte":
- return name + " 格式或取值错误"
- default:
- return name + " 格式错误"
- }
- }
|