Преглед изворни кода

feat(validation): 添加自定义验证器并优化数据结构

- 匿名导入自定义验证器以激活功能
- 更新数据请求结构,添加验证标签
- 实现自定义 'hostport' 验证规则
- 集成翻译功能,提供中文错误信息
- 优化现有数据结构,提高数据完整性
fusu пре 1 месец
родитељ
комит
c511b1bbbc
7 измењених фајлова са 229 додато и 48 уклоњено
  1. 14 14
      api/v1/tcpForwarding.go
  2. 10 18
      api/v1/udpForwarding.go
  3. 14 15
      api/v1/webForwarding.go
  4. 2 0
      cmd/server/main.go
  5. 1 1
      go.mod
  6. 2 0
      go.sum
  7. 186 0
      pkg/validation/validation.go

+ 14 - 14
api/v1/tcpForwarding.go

@@ -3,24 +3,24 @@ package v1
 
 
 type TcpForwardingDataRequest struct {
-	Id                int    `form:"id" json:"id"`
-	CdnWebId          int    `form:"cdnWebId" json:"cdnWebId"`
-	Port              string    `form:"port" json:"port" binding:"required"`
-	BackendList       []string `form:"backendList" json:"backendList"`
-	AllowIpList       []string `form:"allowIpList" json:"allowIpList"`
-	DenyIpList        []string `form:"denyIpList" json:"denyIpList"`
-	AccessRule        string `form:"accessRule" json:"accessRule"`
-	Comment           string `form:"comment" json:"comment"`
+	Id                int      `form:"id" json:"id"`
+	CdnWebId          int      `form:"cdnWebId" json:"cdnWebId"`
+	Port              string   `form:"port" json:"port" validate:"required,numeric,min=1,max=65535"`
+	BackendList       []string `form:"backendList" json:"backendList" validate:"required,dive,hostport"`
+	AllowIpList       []string `form:"allowIpList" json:"allowIpList" validate:"dive,ip"`
+	DenyIpList        []string `form:"denyIpList" json:"denyIpList" validate:"dive,ip"`
+	AccessRule        string   `form:"accessRule" json:"accessRule"`
+	Comment           string   `form:"comment" json:"comment" validate:"max=50"`
 }
 
 type DeleteTcpForwardingRequest struct {
-	Ids   []int `form:"ids" json:"ids" binding:"required"`
-	Uid   int   `form:"uid" json:"uid" binding:"required"`
-	HostId   int   `form:"hostId" json:"hostId" binding:"required"`
+	Ids    []int `form:"ids" json:"ids" validate:"required,min=1,dive,required"`
+	Uid    int   `form:"uid" json:"uid" validate:"required"`
+	HostId int   `form:"hostId" json:"hostId" validate:"required"`
 }
 
 type TcpForwardingRequest struct {
-	HostId            int `form:"hostId" json:"hostId" binding:"required"`
-	Uid               int `form:"uid" json:"uid" binding:"required"`
-	TcpForwardingData TcpForwardingDataRequest `form:"data" json:"data"`
+	HostId            int                      `form:"hostId" json:"hostId" validate:"required"`
+	Uid               int                      `form:"uid" json:"uid" validate:"required"`
+	TcpForwardingData TcpForwardingDataRequest `form:"data" json:"data" validate:"required"`
 }

+ 10 - 18
api/v1/udpForwarding.go

@@ -3,27 +3,19 @@ package v1
 type UdpForwardingDataRequest struct {
 	Id                int    `form:"id" json:"id"`
 	CdnWebId          int    `form:"cdnWebId" json:"cdnWebId"`
-	Port              string    `form:"port" json:"port" binding:"required"`
-	BackendList       []string `form:"backendList" json:"backendList"`
-	AllowIpList       []string `form:"allowIpList" json:"allowIpList"`
-	DenyIpList        []string `form:"denyIpList" json:"denyIpList"`
-	AccessRule        string `form:"accessRule" json:"accessRule"`
-	Comment           string `form:"comment" json:"comment"`
+	Port              string   `form:"port" json:"port" validate:"required,numeric,min=1,max=65535"`
+	BackendList       []string `form:"backendList" json:"backendList" validate:"required,dive,hostport"`
+	AllowIpList       []string `form:"allowIpList" json:"allowIpList" validate:"dive,ip"`
+	DenyIpList        []string `form:"denyIpList" json:"denyIpList" validate:"dive,ip"`
+	AccessRule        string   `form:"accessRule" json:"accessRule"`
+	Comment           string   `form:"comment" json:"comment" validate:"max=50"`
 }
 type DeleteUdpForwardingRequest struct {
-	Ids   []int `form:"ids" json:"ids" binding:"required"`
+	Ids   []int `form:"ids" json:"ids" validate:"required,min=1,dive,required"`
 }
 
 type UdpForwardingRequest struct {
-	HostId            int `form:"hostId" json:"hostId" binding:"required"`
-	Uid               int `form:"uid" json:"uid" binding:"required"`
-	UdpForwardingData UdpForwardingDataRequest `form:"data" json:"data"`
-}
-
-type UdpForwardingRequire struct {
-	HostId            int    `form:"hostId" json:"hostId" binding:"required"`
-	Uid               int    `form:"uid" json:"uid" binding:"required"`
-	Comment           string `form:"comment" json:"comment" binding:"required"`
-	GatewayGroupId int    `form:"gatewayGroupId" json:"gatewayGroupId"`
-	Tag               string `form:"tag" json:"tag" binding:"required"`
+	HostId            int `form:"hostId" json:"hostId" validate:"required"`
+	Uid               int `form:"uid" json:"uid" validate:"required"`
+	UdpForwardingData UdpForwardingDataRequest `form:"data" json:"data" validate:"required"`
 }

+ 14 - 15
api/v1/webForwarding.go

@@ -4,36 +4,35 @@ package v1
 type WebForwardingDataRequest struct {
 	Id                 int    `form:"id" json:"id"`
 	CdnWebId          int    `form:"cdnWebId" json:"cdnWebId"`
-	Port               string    `form:"port" json:"port" binding:"required"`
-	Domain             string `form:"domain" json:"domain"`
-	CustomHost         string `form:"customHost" json:"customHost"`
-	BackendList        []BackendList `form:"backendList" json:"backendList"`
-	AllowIpList        []string `form:"allowIpList" json:"allowIpList"`
-	DenyIpList         []string `form:"denyIpList" json:"denyIpList"`
+	Port               string    `form:"port" json:"port" validate:"required,numeric,min=1,max=65535"`
+	Domain             string `form:"domain" json:"domain" validate:"hostname_rfc1123"`
+	BackendList        []BackendList `form:"backendList" json:"backendList" validate:"required,dive"`
+	AllowIpList        []string `form:"allowIpList" json:"allowIpList" validate:"dive,ip"`
+	DenyIpList         []string `form:"denyIpList" json:"denyIpList" validate:"dive,ip"`
 	AccessRule         string `form:"accessRule" json:"accessRule"`
 	IsHttps            int    `form:"isHttps" json:"isHttps" default:"0"`
-	Comment            string `form:"comment" json:"comment"`
+	Comment            string `form:"comment" json:"comment" validate:"max=50"`
 	HttpsCert          string `form:"httpsCert" json:"httpsCert"`
 	HttpsKey           string `form:"httpsKey" json:"httpsKey"`
 	SslCertId          int64    `form:"sslCertId" json:"sslCertId"`
 }
 
 type DeleteWebForwardingRequest struct {
-	Ids   []int `form:"ids" json:"ids" binding:"required"`
-	Uid   int   `form:"uid" json:"uid" binding:"required"`
-	HostId   int   `form:"hostId" json:"hostId" binding:"required"`
+	Ids   []int `form:"ids" json:"ids" validate:"required,min=1,dive,required"`
+	Uid   int   `form:"uid" json:"uid" validate:"required"`
+	HostId   int   `form:"hostId" json:"hostId" validate:"required"`
 }
 
 type WebForwardingRequest struct {
-	HostId            int `form:"hostId" json:"hostId" binding:"required"`
-	Uid               int `form:"uid" json:"uid" binding:"required"`
-	WebForwardingData WebForwardingDataRequest `form:"data" json:"data"`
+	HostId            int `form:"hostId" json:"hostId" validate:"required"`
+	Uid               int `form:"uid" json:"uid" validate:"required"`
+	WebForwardingData WebForwardingDataRequest `form:"data" json:"data" validate:"required"`
 }
 
 
 type BackendList struct {
-	Addr     string `json:"addr,omitempty" form:"addr"`
-	CustomHost string `json:"customHost,omitempty" form:"customHost"`
+	Addr     string `json:"addr,omitempty" form:"addr" validate:"required,hostport"`
+	CustomHost string `json:"customHost,omitempty" form:"customHost" validate:"omitempty,hostname_rfc1123"`
 	IsHttps  int    `json:"isHttps,omitempty" form:"isHttps" default:"0"`
 }
 

+ 2 - 0
cmd/server/main.go

@@ -5,6 +5,8 @@ import (
 	"flag"
 	"fmt"
 
+	_ "github.com/go-nunu/nunu-layout-advanced/pkg/validation" // 匿名导入以激活自定义校验器
+
 	"github.com/go-nunu/nunu-layout-advanced/cmd/server/wire"
 	"github.com/go-nunu/nunu-layout-advanced/pkg/config"
 	"github.com/go-nunu/nunu-layout-advanced/pkg/log"

+ 1 - 1
go.mod

@@ -71,7 +71,7 @@ require (
 	github.com/go-openapi/swag v0.22.9 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-playground/validator/v10 v10.26.0 // indirect
+	github.com/go-playground/validator/v10 v10.27.0 // indirect
 	github.com/go-sql-driver/mysql v1.7.1 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect

+ 2 - 0
go.sum

@@ -188,6 +188,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
 github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
+github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
 github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=

+ 186 - 0
pkg/validation/validation.go

@@ -0,0 +1,186 @@
+// package validation 提供了一个基于 go-playground/validator/v10 的通用验证器。
+// 它支持通过结构体标签进行声明式验证,也可以用于验证单个变量。
+//
+// 使用示例 (在 Gin handler 中):
+// type MyRequest struct {
+//     IPAddress string `json:"ip_address" validate:"required,ipv4"`
+//     Domain    string `json:"domain" validate:"required,hostname_rfc1123"`
+// }
+//
+// func MyHandler(c *gin.Context) {
+//     var req MyRequest
+//     if err := c.ShouldBindJSON(&req); err != nil {
+//         // ... handle binding error
+//         return
+//     }
+//
+//     if err := validation.Validate(req); err != nil {
+//         // ... handle validation error
+//         c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+//         return
+//     }
+//
+//     // ... process valid data
+// }
+//
+// 单个变量验证:
+// err := validation.ValidateVar("192.168.1.1", "ipv4") // success
+// err := validation.ValidateVar("not-an-ip", "ipv4")   // error
+package validation
+
+import (
+	"errors"
+	"net"
+	"reflect"
+	"strings"
+	"sync"
+
+	"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"
+)
+
+// 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)
+
+	// 为 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
+	})
+}
+
+// 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
+	}
+
+	// 如果不是 IP,则检查它是否为有效的主机名 (使用 validator 内置的 hostname_rfc1123 逻辑)
+	// 我们不能直接调用,但可以通过 Var 来间接使用它
+	err = getInstance().Var(host, "hostname_rfc1123")
+	return err == nil
+}