2025-07-02 16:17:59 +08:00
|
|
|
|
package http
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-07-11 21:05:58 +08:00
|
|
|
|
"fmt"
|
2025-07-02 16:17:59 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"tyapi-server/internal/shared/interfaces"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
"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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// RequestValidatorZh 中文验证器实现
|
|
|
|
|
|
type RequestValidatorZh struct {
|
|
|
|
|
|
response interfaces.ResponseBuilder
|
2025-07-11 21:05:58 +08:00
|
|
|
|
translator ut.Translator
|
2025-07-02 16:17:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewRequestValidatorZh 创建支持中文翻译的请求验证器
|
|
|
|
|
|
func NewRequestValidatorZh(response interfaces.ResponseBuilder) interfaces.RequestValidator {
|
|
|
|
|
|
// 创建中文locale
|
|
|
|
|
|
zhLocale := zh.New()
|
|
|
|
|
|
uni := ut.New(zhLocale, zhLocale)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取中文翻译器
|
|
|
|
|
|
trans, _ := uni.GetTranslator("zh")
|
|
|
|
|
|
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// 注册官方中文翻译
|
|
|
|
|
|
zh_translations.RegisterDefaultTranslations(validator.New(), trans)
|
2025-07-02 16:17:59 +08:00
|
|
|
|
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// 注册自定义翻译
|
|
|
|
|
|
registerCustomTranslations(trans)
|
2025-07-02 16:17:59 +08:00
|
|
|
|
|
|
|
|
|
|
return &RequestValidatorZh{
|
|
|
|
|
|
response: response,
|
2025-07-11 21:05:58 +08:00
|
|
|
|
translator: trans,
|
2025-07-02 16:17:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// registerCustomTranslations 注册自定义翻译
|
|
|
|
|
|
func registerCustomTranslations(trans ut.Translator) {
|
|
|
|
|
|
// 自定义 eqfield 翻译(更友好的提示)
|
|
|
|
|
|
_ = trans.Add("eqfield", "{0}必须与{1}一致", true)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义 required 翻译
|
|
|
|
|
|
_ = trans.Add("required", "{0}不能为空", true)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义 min 翻译
|
|
|
|
|
|
_ = trans.Add("min", "{0}长度不能少于{1}位", true)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义 max 翻译
|
|
|
|
|
|
_ = trans.Add("max", "{0}长度不能超过{1}位", true)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义 len 翻译
|
|
|
|
|
|
_ = trans.Add("len", "{0}长度必须为{1}位", true)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义 email 翻译
|
|
|
|
|
|
_ = trans.Add("email", "{0}必须是有效的邮箱地址", true)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义手机号翻译
|
|
|
|
|
|
_ = trans.Add("phone", "{0}必须是有效的手机号", true)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义用户名翻译
|
|
|
|
|
|
_ = trans.Add("username", "{0}格式不正确,只能包含字母、数字、下划线,且不能以数字开头", true)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义强密码翻译
|
|
|
|
|
|
_ = trans.Add("strong_password", "{0}强度不足,必须包含大小写字母和数字,且不少于8位", true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-02 16:17:59 +08:00
|
|
|
|
// Validate 验证请求体
|
|
|
|
|
|
func (v *RequestValidatorZh) Validate(c *gin.Context, dto interface{}) error {
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// 直接使用 Gin 的绑定和验证
|
|
|
|
|
|
return v.BindAndValidate(c, dto)
|
2025-07-02 16:17:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ValidateQuery 验证查询参数
|
|
|
|
|
|
func (v *RequestValidatorZh) ValidateQuery(c *gin.Context, dto interface{}) error {
|
|
|
|
|
|
if err := c.ShouldBindQuery(dto); err != nil {
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// 处理查询参数验证错误
|
|
|
|
|
|
if _, ok := err.(validator.ValidationErrors); ok {
|
|
|
|
|
|
validationErrors := v.formatValidationErrorsZh(err)
|
|
|
|
|
|
v.response.ValidationError(c, validationErrors)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
v.response.BadRequest(c, "查询参数格式错误", err.Error())
|
|
|
|
|
|
}
|
2025-07-02 16:17:59 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ValidateParam 验证路径参数
|
|
|
|
|
|
func (v *RequestValidatorZh) ValidateParam(c *gin.Context, dto interface{}) error {
|
|
|
|
|
|
if err := c.ShouldBindUri(dto); err != nil {
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// 处理路径参数验证错误
|
|
|
|
|
|
if _, ok := err.(validator.ValidationErrors); ok {
|
|
|
|
|
|
validationErrors := v.formatValidationErrorsZh(err)
|
|
|
|
|
|
v.response.ValidationError(c, validationErrors)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
v.response.BadRequest(c, "路径参数格式错误", err.Error())
|
|
|
|
|
|
}
|
2025-07-02 16:17:59 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BindAndValidate 绑定并验证请求
|
|
|
|
|
|
func (v *RequestValidatorZh) BindAndValidate(c *gin.Context, dto interface{}) error {
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// 绑定请求体(Gin 会自动进行 binding 标签验证)
|
2025-07-02 16:17:59 +08:00
|
|
|
|
if err := c.ShouldBindJSON(dto); err != nil {
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// 处理 Gin binding 验证错误
|
|
|
|
|
|
if _, ok := err.(validator.ValidationErrors); ok {
|
|
|
|
|
|
// 所有验证错误都使用 422 状态码
|
|
|
|
|
|
validationErrors := v.formatValidationErrorsZh(err)
|
|
|
|
|
|
v.response.ValidationError(c, validationErrors)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
v.response.BadRequest(c, "请求体格式错误", err.Error())
|
|
|
|
|
|
}
|
2025-07-02 16:17:59 +08:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-11 21:05:58 +08:00
|
|
|
|
return nil
|
2025-07-02 16:17:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// formatValidationErrorsZh 格式化验证错误(中文翻译版)
|
|
|
|
|
|
func (v *RequestValidatorZh) formatValidationErrorsZh(err error) map[string][]string {
|
|
|
|
|
|
errors := make(map[string][]string)
|
|
|
|
|
|
|
|
|
|
|
|
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
|
|
|
|
|
for _, fieldError := range validationErrors {
|
|
|
|
|
|
fieldName := v.getFieldNameZh(fieldError)
|
|
|
|
|
|
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// 获取友好的中文错误消息
|
|
|
|
|
|
errorMessage := v.getFriendlyErrorMessage(fieldError)
|
2025-07-02 16:17:59 +08:00
|
|
|
|
|
|
|
|
|
|
if _, exists := errors[fieldName]; !exists {
|
|
|
|
|
|
errors[fieldName] = []string{}
|
|
|
|
|
|
}
|
|
|
|
|
|
errors[fieldName] = append(errors[fieldName], errorMessage)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return errors
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-11 21:05:58 +08:00
|
|
|
|
// getFriendlyErrorMessage 获取友好的中文错误消息
|
|
|
|
|
|
func (v *RequestValidatorZh) getFriendlyErrorMessage(fieldError validator.FieldError) string {
|
|
|
|
|
|
field := fieldError.Field()
|
|
|
|
|
|
tag := fieldError.Tag()
|
|
|
|
|
|
param := fieldError.Param()
|
|
|
|
|
|
|
|
|
|
|
|
fieldDisplayName := v.getFieldDisplayName(field)
|
|
|
|
|
|
|
|
|
|
|
|
// 优先使用官方翻译器
|
|
|
|
|
|
errorMessage := fieldError.Translate(v.translator)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果官方翻译成功且不是英文,使用官方翻译
|
|
|
|
|
|
if errorMessage != fieldError.Error() {
|
|
|
|
|
|
// 替换字段名为中文
|
|
|
|
|
|
if fieldDisplayName != fieldError.Field() {
|
|
|
|
|
|
errorMessage = strings.ReplaceAll(errorMessage, fieldError.Field(), fieldDisplayName)
|
|
|
|
|
|
}
|
|
|
|
|
|
return errorMessage
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 回退到自定义翻译
|
|
|
|
|
|
switch tag {
|
|
|
|
|
|
case "required":
|
|
|
|
|
|
return fmt.Sprintf("%s不能为空", fieldDisplayName)
|
|
|
|
|
|
case "email":
|
|
|
|
|
|
return fmt.Sprintf("%s必须是有效的邮箱地址", fieldDisplayName)
|
|
|
|
|
|
case "min":
|
|
|
|
|
|
return fmt.Sprintf("%s长度不能少于%s位", fieldDisplayName, param)
|
|
|
|
|
|
case "max":
|
|
|
|
|
|
return fmt.Sprintf("%s长度不能超过%s位", fieldDisplayName, param)
|
|
|
|
|
|
case "len":
|
|
|
|
|
|
return fmt.Sprintf("%s长度必须为%s位", fieldDisplayName, param)
|
|
|
|
|
|
case "eqfield":
|
|
|
|
|
|
paramDisplayName := v.getFieldDisplayName(param)
|
|
|
|
|
|
return fmt.Sprintf("%s必须与%s一致", fieldDisplayName, paramDisplayName)
|
|
|
|
|
|
case "phone":
|
|
|
|
|
|
return fmt.Sprintf("%s必须是有效的手机号", fieldDisplayName)
|
|
|
|
|
|
case "username":
|
|
|
|
|
|
return fmt.Sprintf("%s格式不正确,只能包含字母、数字、下划线,且不能以数字开头", fieldDisplayName)
|
|
|
|
|
|
case "strong_password":
|
|
|
|
|
|
return fmt.Sprintf("%s强度不足,必须包含大小写字母和数字,且不少于8位", fieldDisplayName)
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 默认错误消息
|
|
|
|
|
|
return fmt.Sprintf("%s格式不正确", fieldDisplayName)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-02 16:17:59 +08:00
|
|
|
|
// getFieldNameZh 获取字段名(JSON标签优先)
|
|
|
|
|
|
func (v *RequestValidatorZh) getFieldNameZh(fieldError validator.FieldError) string {
|
|
|
|
|
|
fieldName := fieldError.Field()
|
|
|
|
|
|
return v.toSnakeCase(fieldName)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getFieldDisplayName 获取字段显示名称(中文)
|
|
|
|
|
|
func (v *RequestValidatorZh) getFieldDisplayName(field string) string {
|
|
|
|
|
|
fieldNames := map[string]string{
|
|
|
|
|
|
"phone": "手机号",
|
|
|
|
|
|
"password": "密码",
|
|
|
|
|
|
"confirm_password": "确认密码",
|
|
|
|
|
|
"old_password": "原密码",
|
|
|
|
|
|
"new_password": "新密码",
|
|
|
|
|
|
"confirm_new_password": "确认新密码",
|
|
|
|
|
|
"code": "验证码",
|
|
|
|
|
|
"username": "用户名",
|
|
|
|
|
|
"email": "邮箱",
|
|
|
|
|
|
"display_name": "显示名称",
|
|
|
|
|
|
"scene": "使用场景",
|
|
|
|
|
|
"Password": "密码",
|
|
|
|
|
|
"NewPassword": "新密码",
|
|
|
|
|
|
"ConfirmPassword": "确认密码",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if displayName, exists := fieldNames[field]; exists {
|
|
|
|
|
|
return displayName
|
|
|
|
|
|
}
|
|
|
|
|
|
return field
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// toSnakeCase 转换为snake_case
|
|
|
|
|
|
func (v *RequestValidatorZh) toSnakeCase(str string) string {
|
|
|
|
|
|
var result strings.Builder
|
|
|
|
|
|
for i, r := range str {
|
|
|
|
|
|
if i > 0 && (r >= 'A' && r <= 'Z') {
|
|
|
|
|
|
result.WriteRune('_')
|
|
|
|
|
|
}
|
|
|
|
|
|
result.WriteRune(r)
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.ToLower(result.String())
|
|
|
|
|
|
}
|