package validator import ( "errors" "fmt" "reflect" "strings" "sync" "github.com/gin-gonic/gin/binding" playground "github.com/go-playground/validator/v10" ) // Validate holds the validator instance. var validate *playground.Validate // ValidationError keeps the original validator error for errors.As while // exposing a user-facing message instead of validator's internal English text. type ValidationError struct { cause error } func (e *ValidationError) Error() string { return validationMessage(e.cause) } func (e *ValidationError) Unwrap() error { return e.cause } // Init initializes the global validator. func Init() { validate = newValidator("") } // ValidateStruct validates a struct using go-playground/validator tags. func ValidateStruct(s any) error { if validate == nil { Init() } err := validate.Struct(s) return wrapValidationError(err) } // Var validates a single variable. func Var(field any, tag string) error { if validate == nil { Init() } err := validate.Var(field, tag) return wrapValidationError(err) } // ValidationMessage returns a safe, localized message for a validation error. func ValidationMessage(err error) string { if err == nil { return "" } if localized, ok := err.(*ValidationError); ok { return validationMessage(localized.cause) } var localized *ValidationError if errors.As(err, &localized) { return validationMessage(localized.cause) } return validationMessage(err) } // ValidationMessageText removes validator's raw diagnostic format from a // message that has already been converted to a string by a caller. func ValidationMessageText(message string) string { message = strings.TrimSpace(message) if message == "" { return message } lower := strings.ToLower(message) for _, prefix := range []string{"validation error:", "validation:"} { if strings.HasPrefix(lower, prefix) { message = strings.TrimSpace(message[len(prefix):]) lower = strings.ToLower(message) break } } if strings.Contains(lower, "field validation for") && strings.Contains(lower, "failed on the '") { return "请求参数不符合要求" } return message } // IsValidationError reports whether err came from go-playground validation, // including errors wrapped by a service. func IsValidationError(err error) bool { if err == nil { return false } var localized *ValidationError if errors.As(err, &localized) { return true } var fieldErrors playground.ValidationErrors if errors.As(err, &fieldErrors) { return true } var invalidValidation *playground.InvalidValidationError if errors.As(err, &invalidValidation) { return true } var sliceErrors binding.SliceValidationError return errors.As(err, &sliceErrors) } type ginStructValidator struct { once sync.Once validate *playground.Validate } var _ binding.StructValidator = (*ginStructValidator)(nil) // init replaces Gin's default validator so every ShouldBind* call gets the // same field names and user-facing messages as service-layer validation. func init() { binding.Validator = &ginStructValidator{} } func (v *ginStructValidator) ValidateStruct(obj any) error { if obj == nil { return nil } value := reflect.ValueOf(obj) switch value.Kind() { case reflect.Ptr: if value.IsNil() { return nil } if value.Elem().Kind() != reflect.Struct { return v.ValidateStruct(value.Elem().Interface()) } return v.validateStruct(obj) case reflect.Struct: return v.validateStruct(obj) case reflect.Slice, reflect.Array: validationErrors := make(binding.SliceValidationError, 0) for i := 0; i < value.Len(); i++ { if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { validationErrors = append(validationErrors, err) } } if len(validationErrors) > 0 { return validationErrors } } return nil } func (v *ginStructValidator) validateStruct(obj any) error { v.once.Do(func() { v.validate = newValidator("binding") }) return wrapValidationError(v.validate.Struct(obj)) } func (v *ginStructValidator) Engine() any { v.once.Do(func() { v.validate = newValidator("binding") }) return v.validate } func newValidator(tagName string) *playground.Validate { v := playground.New() if tagName != "" { v.SetTagName(tagName) } v.RegisterTagNameFunc(func(field reflect.StructField) string { for _, tagName := range []string{"json", "form", "uri"} { name, _, _ := strings.Cut(field.Tag.Get(tagName), ",") if name != "" && name != "-" { return name } } return field.Name }) return v } func wrapValidationError(err error) error { if err == nil { return nil } return &ValidationError{cause: err} } func validationMessage(err error) string { if err == nil { return "" } var fieldErrors playground.ValidationErrors if errors.As(err, &fieldErrors) { messages := make([]string, 0, len(fieldErrors)) seen := make(map[string]struct{}, len(fieldErrors)) for _, fieldError := range fieldErrors { message := formatFieldError(fieldError) if _, ok := seen[message]; ok { continue } seen[message] = struct{}{} messages = append(messages, message) } if len(messages) > 0 { return strings.Join(messages, ";") } } var sliceErrors binding.SliceValidationError if errors.As(err, &sliceErrors) { messages := make([]string, 0, len(sliceErrors)) for _, item := range sliceErrors { if item != nil { messages = append(messages, ValidationMessage(item)) } } if len(messages) > 0 { return strings.Join(messages, ";") } return "请求参数不符合要求" } var invalidValidation *playground.InvalidValidationError if errors.As(err, &invalidValidation) { return "请求参数不符合要求" } return ValidationMessageText(err.Error()) } func formatFieldError(err playground.FieldError) string { field := fieldLabel(err.Field()) param := err.Param() switch err.Tag() { case "required", "required_if", "required_unless", "required_with", "required_with_all", "required_without", "required_without_all": return "请填写" + field case "email": return "请输入有效的" + field case "min": if err.Kind() == reflect.String { return fmt.Sprintf("%s长度不能少于 %s 个字符", field, param) } return fmt.Sprintf("%s不能小于 %s", field, param) case "max": if err.Kind() == reflect.String { return fmt.Sprintf("%s长度不能超过 %s 个字符", field, param) } return fmt.Sprintf("%s不能大于 %s", field, param) case "len": if err.Kind() == reflect.String { return fmt.Sprintf("%s长度必须为 %s 个字符", field, param) } return fmt.Sprintf("%s长度必须为 %s", field, param) case "gte": return fmt.Sprintf("%s不能小于 %s", field, param) case "lte": return fmt.Sprintf("%s不能大于 %s", field, param) case "gt": return fmt.Sprintf("%s必须大于 %s", field, param) case "lt": return fmt.Sprintf("%s必须小于 %s", field, param) case "oneof": return field + "取值不正确" case "eqfield": return field + "与" + fieldLabel(param) + "不一致" case "nefield": return field + "不能与" + fieldLabel(param) + "相同" case "url", "uri", "http_url": return "请输入有效的" + field case "uuid", "uuid3", "uuid4", "uuid5", "uuid8", "json": return field + "格式不正确" case "numeric", "number", "integer", "boolean": return field + "格式不正确" case "excluded", "excluded_if", "excluded_unless", "excluded_with", "excluded_with_all", "excluded_without", "excluded_without_all": return field + "不允许填写" default: return field + "格式不正确" } } var fieldLabels = map[string]string{ "account_id": "账户", "agent_id": "坐席", "assignee_id": "负责人", "avatar_url": "头像地址", "client_id": "客户端 ID", "confirmation_token": "确认令牌", "contact_id": "联系人", "conversation_id": "会话", "conversation_ids": "会话", "content": "内容", "custom_attributes": "自定义属性", "email": "邮箱", "event_id": "事件 ID", "external_url": "外部地址", "file": "文件", "inbox_id": "收件箱", "identifier": "标识", "identifier_hash": "标识校验值", "label": "标签", "locale": "语言", "message": "消息", "message_id": "消息", "name": "名称", "password": "密码", "password_confirmation": "确认密码", "phone_number": "手机号", "priority": "优先级", "refresh_token": "刷新令牌", "reset_password_token": "重置密码令牌", "role": "角色", "source_id": "来源 ID", "status": "状态", "team_id": "团队", "typing_status": "输入状态", "user_id": "用户", "user_ids": "用户", "website_token": "网站令牌", "widget_token": "组件令牌", } func fieldLabel(field string) string { field = strings.TrimSpace(field) if dot := strings.LastIndex(field, "."); dot >= 0 { field = field[dot+1:] } if bracket := strings.IndexByte(field, '['); bracket >= 0 { field = field[:bracket] } key := strings.ToLower(field) if label, ok := fieldLabels[key]; ok { return label } switch { case strings.Contains(key, "password"): return "密码" case strings.Contains(key, "email"): return "邮箱" case strings.Contains(key, "token"): return "令牌" case strings.Contains(key, "url"): return "地址" default: return "该字段" } }