// Package validation 提供请求参数校验失败的可定位中文提示。 // 提示只使用字段的 description 与校验规则,不拼接底层错误文本,也不回显字段值。 package validation import ( "reflect" "strings" "github.com/go-playground/validator/v10" ) // Message 把请求校验失败转换为可定位字段的中文提示。 func Message(prefix string, req any, err error) string { fieldErrs, ok := err.(validator.ValidationErrors) if !ok || len(fieldErrs) == 0 { return prefix } return prefix + ":" + describeFieldError(req, fieldErrs[0]) } // describeFieldError 用字段中文名与失败规则描述单个字段错误。 func describeFieldError(req any, fieldErr validator.FieldError) string { label := fieldDescription(req, fieldErr.StructField()) switch fieldErr.Tag() { case "required": // 数字字段的 required 只在零值失败;说“不能为空”会误导为缺字段。 if isNumericField(req, fieldErr.StructField()) { return label + "必须大于 0" } return label + "不能为空" case "min": if isNumericField(req, fieldErr.StructField()) { return label + "不能小于 " + fieldErr.Param() } return label + "长度不能小于 " + fieldErr.Param() case "max": if isNumericField(req, fieldErr.StructField()) { return label + "不能超过 " + fieldErr.Param() } return label + "长度不能超过 " + fieldErr.Param() case "oneof": return label + "必须为 " + strings.ReplaceAll(fieldErr.Param(), " ", "/") + " 之一" default: return label + "不合法(" + fieldErr.Tag() + ")" } } // isNumericField 判断字段是否为整数或浮点类型。 func isNumericField(req any, fieldName string) bool { field, ok := lookupField(req, fieldName) if !ok { return false } switch field.Type.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: return true default: return false } } // lookupField 在去指针的结构体类型上按名取字段。 func lookupField(req any, fieldName string) (reflect.StructField, bool) { typ := reflect.TypeOf(req) for typ != nil && typ.Kind() == reflect.Ptr { typ = typ.Elem() } if typ == nil || typ.Kind() != reflect.Struct { return reflect.StructField{}, false } if field, ok := typ.FieldByName(fieldName); ok { return field, true } // 嵌套(含匿名嵌入)结构体字段:校验错误报告的是内层字段名,提示也要能取到它的 description。 for index := range typ.NumField() { field := typ.Field(index) nested := field.Type for nested.Kind() == reflect.Ptr { nested = nested.Elem() } if nested.Kind() != reflect.Struct { continue } if inner, ok := nested.FieldByName(fieldName); ok { return inner, true } } return reflect.StructField{}, false } // fieldDescription 取字段 description 的首个中文短语作为提示名,缺失时退回字段名。 func fieldDescription(req any, fieldName string) string { field, ok := lookupField(req, fieldName) if !ok { return fieldName } description := strings.TrimSpace(field.Tag.Get("description")) if description == "" { return fieldName } if cut := strings.IndexAny(description, "((::,,;;"); cut > 0 { description = strings.TrimSpace(description[:cut]) } if description == "" { return fieldName } // 提示名以拉丁字母/数字结尾时补一个空格,避免与后续中文粘连。 if last := description[len(description)-1]; last < 0x80 { description += " " } return description }