package validation import ( "errors" "github.com/gin-gonic/gin/binding" "github.com/go-playground/locales/en" "github.com/go-playground/locales/zh" ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" zh_translations "github.com/go-playground/validator/v10/translations/zh" "golang.org/x/net/idna" "net" "reflect" "strconv" "strings" "sync" ) // validate 是一个单例的 validator 实例,确保全局只有一个验证器,以提高性能。 // validator 实例的创建是昂贵的,因此我们使用 sync.Once 来确保它只被创建一次。 // validate 是一个单例的 validator 实例,确保全局只有一个验证器,以提高性能。 // validator 实例的创建是昂贵的,因此我们使用 sync.Once 来确保它只被创建一次。 var ( validate *validator.Validate trans ut.Translator // 全局翻译器 once sync.Once ) func init() { getInstance() } // getInstance 返回一个初始化好的 validator 单例实例。 func getInstance() *validator.Validate { once.Do(func() { validate = validator.New() // 注册一个函数,获取struct tag中自定义的json字段名 validate.RegisterTagNameFunc(func(fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] if name == "-" { return "" } return name }) // 创建翻译器 en := en.New() zh := zh.New() uni := ut.New(en, zh, en) // 第一个参数是备用语言 // 我们假设服务主要使用中文,所以获取中文翻译器 trans, _ = uni.GetTranslator("zh") // 注册中文翻译 zh_translations.RegisterDefaultTranslations(validate, trans) // 注册自定义校验 registerCustomValidations(validate) // 将我们自定义的校验器与 gin 的校验器绑定 binding.Validator = &defaultValidator{validate: validate} // 在这里可以注册自定义的验证函数 // 例如: validate.RegisterValidation("custom_tag", customValidationFunc) }) return validate } // defaultValidator 实现了 gin.Engine 的 validator 接口 type defaultValidator struct { validate *validator.Validate } // ValidateStruct 接收一个对象并校验它。 // ValidateStruct 接收一个对象并校验它,它会返回翻译后的错误信息。 func (v *defaultValidator) ValidateStruct(obj interface{}) error { err := v.validate.Struct(obj) if err != nil { // 如果发生校验错误,则调用我们的翻译函数 return translateError(err) } return nil } // Engine 返回底层的 validator 引擎 func (v *defaultValidator) Engine() interface{} { return v.validate } // translateError 将 validator.ValidationErrors 翻译成更友好的中文错误信息 func translateError(err error) error { var validationErrors validator.ValidationErrors if errors.As(err, &validationErrors) { var errs []string for _, e := range validationErrors { // 使用我们注册的翻译器进行翻译 errs = append(errs, e.Translate(trans)) } return errors.New(strings.Join(errs, "; ")) } return err } // Validate 函数用于验证一个结构体的所有字段。 // 它现在会返回翻译后的中文错误信息。 func Validate(s interface{}) error { err := getInstance().Struct(s) if err != nil { return translateError(err) } return nil } // ValidateVar 函数用于验证单个变量。 // 它现在也会返回翻译后的中文错误信息。 func ValidateVar(field interface{}, tag string) error { err := getInstance().Var(field, tag) if err != nil { return translateError(err) } return nil } // registerCustomValidations 注册所有自定义的校验规则和翻译 func registerCustomValidations(v *validator.Validate) { // 注册 hostport 校验器 v.RegisterValidation("hostport", validateHostPort) v.RegisterValidation("idn_fqdn",isIdnFqdn) // 为 hostport 校验器注册中文翻译 v.RegisterTranslation("hostport", trans, func(ut ut.Translator) error { return ut.Add("hostport", "{0} 必须是有效的 'IP:端口' 或 '域名:端口' 格式", true) }, func(ut ut.Translator, fe validator.FieldError) string { t, _ := ut.T("hostport", fe.Field()) return t }) v.RegisterTranslation("idn_fqdn", trans, func(ut ut.Translator) error { return ut.Add("idn_fqdn", "{0} 必须是有效的 IDN 格式", true) }, func(ut ut.Translator, fe validator.FieldError) string { t, _ := ut.T("idn_fqdn", fe.Field()) return t }) } // validateHostPort 是一个自定义校验函数,用于检查字符串是否为有效的 "host:port"。 // 主机部分可以是 IP 地址或域名。 func validateHostPort(fl validator.FieldLevel) bool { addr := fl.Field().String() // 我们可以使用 net.SplitHostPort 来检查格式。 // 它能正确处理 IPv6 地址,例如 "[::1]:8080"。 host, port, err := net.SplitHostPort(addr) if err != nil || host == "" || port == "" { return false // 格式不正确,或者主机或端口为空 } // 检查主机部分是否为 IP 地址 if net.ParseIP(host) != nil { return true } portInt, err := strconv.Atoi(port) if err != nil { return false } if portInt <= 0 || portInt > 65535 { return false } // 如果不是 IP,则检查它是否为有效的主机名 (使用 validator 内置的 hostname_rfc1123 逻辑) // 我们不能直接调用,但可以通过 Var 来间接使用它 err = getInstance().Var(host, "hostname_rfc1123") return err == nil } func isIdnFqdn(fl validator.FieldLevel) bool { // 获取字段的值 domain := fl.Field().String() // omitempty 的行为由 validator 自身处理, // 如果字段为空,我们的函数不应该判定为 false。 if domain == "" { return true } // idna.ToASCII 会将域名转换为 Punycode 格式。 // 这个过程本身就包含了对 IDNA2008 标准的严格验证。 // 如果域名格式不正确(例如包含非法字符、标签过长等),它会返回一个错误。 // 因此,我们只需要检查这个函数是否返回错误即可。 _, err := idna.ToASCII(domain) // 如果没有错误,说明它是一个有效的(可能是国际化的)域名。 return err == nil }