likes
comments
collection
share

手把手带你从0到1封装Gin框架:08 validator数据校验&自定义验证规则

作者站长头像
站长
· 阅读数 21

这篇我们加一下请求参数的校验

基本使用

Gin 框架本身自带数据校验的,这里我们直接使用

新建app/request/admin_request.go文件:

package request  
  
type SaveAdmin struct {  
    ID       uint   `form:"id" json:"id"`  
    Name     string `form:"name" json:"name" binding:"required"`  
    Password string `form:"password" json:"password" binding:"required"`  
    Age      int    `form:"age" json:"age" binding:"required,min=1,max=100"`  
    Gender   int    `form:"gender" json:"gender" binding:"required,oneof=1 2"`  
}

这里先定义一个接收请求参数的结构体,并给每个字段定义了校验规则

修改app/route/admin.go文件:

package route

import (
	"eve/app/api/admin"
	"github.com/gin-gonic/gin"
)

func genAdminRouter(rg *gin.RouterGroup) {
	rg.GET("/profile", admin.AdminApi.Profile)
	rg.POST("/save", admin.AdminApi.Save)
}

新增路由/admin/save

再新增Save接口的方法,修改app/api/admin/admin_api.go文件:

package admin

import (
	"eve/app/request"
	"eve/app/response"
	"eve/app/service"
	"github.com/gin-gonic/gin"
)

type adminApi struct{}

func (a *adminApi) Profile(c *gin.Context) {
	admin := service.AdminService.Profile()

	response.Success(c, admin)
}

// Save 新增/编辑管理员信息
func (a *adminApi) Save(c *gin.Context) {
	var admin request.SaveAdmin
	if err := c.ShouldBind(&admin); err != nil {
		response.ValidateFailed(c, err.Error())
	}
}

参数校验应该有一个统一的返回格式,那我们再新增一个ValidateFailed方法,修改app/response/response.go文件:

...

func ValidateFailed(c *gin.Context, msg string) {
	c.JSON(http.StatusOK, Response{
		1,
		nil,
		msg,
	})
}

...

然后启动服务,发起请求:

➜  ~ curl --location --request POST 'http://127.0.0.1:8082/admin/save'

{
    "error_code": 1,
    "data": null,
    "message": "Key: 'SaveAdmin.Name' Error:Field validation for 'Name' failed on the 'required' tag\nKey: 'SaveAdmin.Password' Error:Field validation for 'Password' failed on the 'required' tag\nKey: 'SaveAdmin.Age' Error:Field validation for 'Age' failed on the 'required' tag\nKey: 'SaveAdmin.Gender' Error:Field validation for 'Gender' failed on the 'required' tag"
}

错误信息拆解: Key: 'SaveAdmin.Name' Error:Field validation for 'Name' failed on the 'required' tag Key: 'SaveAdmin.Password' Error:Field validation for 'Password' failed on the 'required' tag Key: 'SaveAdmin.Age' Error:Field validation for 'Age' failed on the 'required' tag Key: 'SaveAdmin.Gender' Error:Field validation for 'Gender' failed on the 'required' tag

本次请求没有提交任何参数,所以有报错,但是错误信息很不友好,不适合直接给前端展示

commit-hash: 0c4ee74

错误信息自定义

在上边c.ShouldBind(&admin)返回了一个error,这个error有好几种类型,数据校验失败返回的是validator.ValidationErrors类型,需要根据不同的类型来获取相应的错误信息

首先修改app/request/validator.go文件:

package request

import (
	"errors"
	"github.com/go-playground/validator/v10"
)

// ValidateErrorMessages 错误消息类型
type ValidateErrorMessages map[string]string

// Validator 验证规则
type Validator interface {
	GetMessages() ValidateErrorMessages
}

// GetErrorMsg 获取错误信息的方法
func GetErrorMsg(request interface{}, err error) string {
	// 定义 ValidationErrors 变量,可能包含错误数据
	var validationErrors validator.ValidationErrors

	// 如果request是Validator类型,并且err是validator.ValidationErrors类型
	if _, isValidator := request.(Validator); isValidator && errors.As(err, &validationErrors) && len(validationErrors) > 0 {
		// 获取第一个错误信息
		errMsg := validationErrors[0]
		if message, exist := request.(Validator).GetMessages()[errMsg.Field()+"."+errMsg.Tag()]; exist {
			return message
		} else {
			// 如果没有自定义消息,返回错误成员本身的错误信息
			return errMsg.Error()
		}
	}

	// 其他类型的错误直接返回错误信息
	return err.Error()
}

可以看到我们定义了一个统一的验证器类型,还定义了GetErrorMsg方法来获取错误信息,这个方法只是针对上边不友好的validator.ValidationErrors类型的错误进行了解读,其他类型的错误还是直接返回错误信息

修改app/request/admin_request.go文件:

package request

type SaveAdmin struct {
	ID       uint   `form:"id" json:"id"`
	Name     string `form:"name" json:"name" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
	Age      int    `form:"age" json:"age" binding:"required,min=1,max=100"`
	Gender   int    `form:"gender" json:"gender" binding:"required,oneof=1 2"`
}

func (SaveAdmin) GetMessages() ValidateErrorMessages {
	return ValidateErrorMessages{
		"Name.required":     "用户名称不能为空",
		"Password.required": "密码不能为空",
		"Age.required":      "年龄不能为空",
		"Age.min":           "年龄最小为1",
		"Age.max":           "年龄最大为100",
		"Gender.required":   "性别不能为空",
		"Gender.oneof":      "性别只能是男或者女",
	}
}

上边的Validator类型定义了GetMessages方法,这里实现验证器的GetMessages方法

然后在修改app/api/admin/admin_api.go文件中Save方法:

...
// Save 新增/编辑管理员信息
func (a *adminApi) Save(c *gin.Context) {
	var admin request.SaveAdmin
	if err := c.ShouldBind(&admin); err != nil {
		fmt.Println(err)
		response.ValidateFailed(c, request.GetErrorMsg(admin, err))
	}
}
...

通过request.GetErrormsg方法来分析验证失败返回的err并返回错误信息

修改之后再重启项目,再发起请求:

➜  ~ curl --location --request POST 'http://127.0.0.1:8082/admin/save' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "fawef",
    "password": "23",
    "age": 2
}'
{"error_code":1,"data":null,"message":"性别不能为空"}
➜  ~ curl --location --request POST 'http://127.0.0.1:8082/admin/save' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "fawef",
    "password": "23",
    "age": 2,
    "gender": 3
}'
{"error_code":1,"data":null,"message":"性别只能是男或者女"}

可以看到自定义的错误信息已经生效了

commit-hash: 9c01407

自定义验证规则

通过官方文档可以看到validator提供了丰富的验证规则,但是如果官方没有我们项目中需要的验证规则呢?比如 手机号验证是否在数据库中存在验证 等,这就需要我们自己定义验证规则,正好validator 也支持我们自定义验证规则

现在我们新增internal/validator/validator.go文件:

package validator

import (
	"eve/internal/global"
	"fmt"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/validator/v10"
	"regexp"
	"strings"
)

func InitValidator() {
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		// 自定义验证器注册
		_ = v.RegisterValidation("mobile", validateMobile)
		_ = v.RegisterValidation("exist", validateExist)
	}
}

// 校验手机号
func validateMobile(fl validator.FieldLevel) bool {
	mobile := fl.Field().String()
	// 手机号为空则不验证,这里只验证有值的
	if mobile == "" {
		return true
	}

	ok, _ := regexp.MatchString(`^1[3-9]\d{9}$`, mobile)

	return ok
}

// 验证是否在数据库表中存在
func validateExist(fl validator.FieldLevel) bool {
	value := fmt.Sprint(fl.Field())
	// 如果值为空则不验证
	if value == "" {
		return true
	}

	param := strings.Split(fl.Param(), " ")

	sql := fmt.Sprintf(`SELECT 1 FROM %s WHERE %s = '%s' LIMIT 1`, param[0], param[1], value)

	var count int64
	if global.DB.Raw(sql).Count(&count); count > 0 {
		return true
	}

	return false
}

在初始化文件中调用一下,修改internal/bootstrap/init.go文件:

func init() {
	...

	// 注册验证器
	validator.InitValidator()
}

然后我们再修改app/request/admin_request.go文件:

package request

type SaveAdmin struct {
	ID       uint   `form:"id" json:"id" binding:"exist=admin id"`
	Name     string `form:"name" json:"name" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
	Age      int    `form:"age" json:"age" binding:"required,min=1,max=100"`
	Gender   int    `form:"gender" json:"gender" binding:"required,oneof=1 2"`
	Mobile   string `form:"mobile" json:"mobile" binding:"required,mobile"`
}

func (SaveAdmin) GetMessages() ValidateErrorMessages {
	return ValidateErrorMessages{
		"ID.exist":          "记录不存在",
		"Name.required":     "用户名称不能为空",
		"Password.required": "密码不能为空",
		"Age.required":      "年龄不能为空",
		"Age.min":           "年龄最小为1",
		"Age.max":           "年龄最大为100",
		"Gender.required":   "性别不能为空",
		"Gender.oneof":      "性别只能是男或者女",
		"Mobile.required":   "手机号不能为空",
		"Mobile.mobile":     "手机号格式不正确",
	}
}

可以看到对ID字段绑定了exist=admin id验证规则,表示这个值应该在表adminid字段中存在,还给Mobile字段绑定了mobile验证规则,表示这个值是一个手机号,这两个规则在上边都已经注册好了

测试:

➜  ~ curl --location --request POST 'http://127.0.0.1:8082/admin/save' \
--header 'Content-Type: application/json' \
--data-raw '{
    "id": 2,
    "name": "nfwoe",
    "password": "23",
    "age": 2,
    "gender": 2,
    "mobile": "18800008888"
}'
{"error_code":1,"data":null,"message":"记录不存在"}

➜  ~ curl --location --request POST 'http://127.0.0.1:8082/admin/save' \
--header 'Content-Type: application/json' \
--data-raw '{
    "id": 1,
    "name": "nfwoe",
    "password": "23",
    "age": 2,
    "gender": 2,
    "mobile": "1234"
}'
{"error_code":1,"data":null,"message":"手机号格式不正确"}

可以看到,验证规则都可以正常使用,当然我们还可以根据自己的需要来注册更多的验证规则,比如start_date不能在end_date之后等~

总结

  • 基本使用
  • 错误消息自定义
  • 验证规则自定义

commit-hash: d9a7f16

转载自:https://juejin.cn/post/7396235489831649332
评论
请登录