Request对象支持非常完美的请求校验能力,通过给结构体属性绑定v标签即可。由于底层校验功能通过gvalid模块实现,更详细的校验规则和介绍请参考 数据校验-Struct校验 章节。

需要注意的是,从goframe v1.16版本开始,如果参数采用结构化的输入输出管理,HTTP请求的数据校验不再受结构体默认值的影响,底层调用的是gvalid组件的CheckStructWithData方法,即直接使用请求的参数执行数据校验,而给定的结构体对象仅用于校验规则和错误提示信息的定义管理。

示例1,基本使用

我们将之前的示例做下调整,增加v校验标签。

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

// 注册请求数据结构
type RegisterReq struct {
	Name  string `p:"username"  v:"required|length:4,30#请输入账号|账号长度为:min到:max位"`
	Pass  string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"`
	Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"`
}

// 注册返回数据结构
type RegisterRes struct {
	Code  int         `json:"code"`
	Error string      `json:"error"`
	Data  interface{} `json:"data"`
}

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/register", func(r *ghttp.Request) {
			var req *RegisterReq
			if err := r.Parse(&req); err != nil {
				r.Response.WriteJsonExit(RegisterRes{
					Code:  1,
					Error: err.Error(),
				})
			}
			// ...
			r.Response.WriteJsonExit(RegisterRes{
				Data: req,
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}

在该示例中,我们定义了两个结构体:RegisterReq用于参数接收,RegisterRes用于数据返回。由于该接口返回的是JSON数据结构,可以看到,只有返回的结构体中存在json标签,而接收的结构体中只有p标签。因为RegisterReq仅用于参数接收,无需设置返回的json标签。

p标签是可选的,默认情况下会通过 忽略特殊字符+不区分大小写 的规则进行属性名称匹配转换,默认匹配规则满足绝大部分业务场景。

为了演示测试效果,这里在正常的返回结果Data属性中将RegisterReq对象返回,由于该对象没有绑定json标签,因此返回的JSON字段将会为其属性名称。

执行后,我们通过curl工具来测试一下:

$ curl "http://127.0.0.1:8199/register?name=john&password1=123456&password2=123456"
{"code":0,"error":"","data":{"Name":"john","Pass":"123456","Pass2":"123456"}}

$ curl "http://127.0.0.1:8199/register?name=john&password1=123456&password2=12345"
{"code":1,"error":"密码长度不够; 两次密码不一致","data":null}

$ curl "http://127.0.0.1:8199/register"
{"code":1,"error":"请输入账号; 账号长度为4到30位; 请输入密码; 密码长度不够; 请确认密码; 密码长度不够; 两次密码不一致","data":null}

示例2,校验错误处理

可以看到在以上示例中,当请求校验错误时,所有校验失败的错误都返回了,这样对于用户体验不是特别友好。当产生错误时,我们可以将校验错误转换为gvalid.Error接口对象,随后可以通过灵活的方法控制错误的返回。

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gogf/gf/v2/util/gvalid"
)

type RegisterReq struct {
	Name  string `p:"username"  v:"required|length:4,30#请输入账号|账号长度为:min到:max位"`
	Pass  string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"`
	Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"`
}

type RegisterRes struct {
	Code  int         `json:"code"`
	Error string      `json:"error"`
	Data  interface{} `json:"data"`
}

func main() {
	s := g.Server()
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.ALL("/register", func(r *ghttp.Request) {
			var req *RegisterReq
			if err := r.Parse(&req); err != nil {
				// Validation error.
				if v, ok := err.(gvalid.Error); ok {
					r.Response.WriteJsonExit(RegisterRes{
						Code:  1,
						Error: v.FirstString(),
					})
				}
				// Other error.
				r.Response.WriteJsonExit(RegisterRes{
					Code:  1,
					Error: err.Error(),
				})
			}
			// ...
			r.Response.WriteJsonExit(RegisterRes{
				Data: req,
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}

可以看到,当错误产生后,我们可以通过err.(gvalid.Error)断言的方式判断错误是否为校验错误,如果是的话则返回第一条校验错误,而不是所有都返回。更详细的错误控制方法,请参考 数据校验-校验结果 章节。

此外,我们这里也可以使用gerror.Current获取第一条报错信息,而不是使用断言判断。例如:

var req *RegisterReq
if err := r.Parse(&req); err != nil {
	r.Response.WriteJsonExit(RegisterRes{
		Code:  1,
		Error: gerror.Current(err).Error(),
	})
}

执行后,我们通过curl工具来测试一下:

$ curl "http://127.0.0.1:8199/register"
{"code":1,"error":"请输入账号","data":null}

$ curl "http://127.0.0.1:8199/register?name=john&password1=123456&password2=12345"
{"code":1,"error":"两次密码不一致","data":null}





Content Menu

  • No labels

6 Comments

  1. 我认为使用断言的方式更好,因为这样可以获得校验类型的错误信息,而不是panic抛出的错误信息

  2. 郭强 API model 已经定义好参数校验规则,期望能根据I18N输出校验失败的信息,但是看起来没有生效?怎么破?

    // api/user.go 
    
    type UserCreateReq struct {
    	g.Meta      `path:"/user" method:"post" summary:"创建用户" tags:"用户"`
    	LoginName   string `p:"loginName" v:"required|passport"  dc:"登录名,字母开头,只能包含字母、数字和下划线,长度在6~18之间"`
    	DisplayName string `p:"displayName" v:"required" dc:"姓名"`
    	Password    string `p:"password"  v:"required"  dc:"密码"`
    	Enabled     string `p:"enabled" v:"required|in:enabled,disabled"  d:"enabled" dc:"用户的启用状态,enabled表示启用,disabled表示禁用"`
    	Email       string `p:"email" d:"" v:"email"  dc:"邮箱"`
    	Phone       string `p:"phone" d:"" v:"phone" dc:"电话"`
    	Desc        string `p:"desc" d:"" v:"max-length:255"  dc:"描述信息"`
    }
    // main.go
    func main() {
    	s := g.Server()
    	// 设置行号,日期,时间:日期+时间+毫秒,如:2009-01-23 01:23:23.675
    	//g.Log().SetFlags(g.Log().GetFlags() | glog.F_FILE_SHORT | glog.F_TIME_DATE | glog.F_TIME_MILLI)  // 通过配置文件实现
    	s.Group("/", func(group *ghttp.RouterGroup) {
    		group.Middleware(
    			service.Middleware().I18NMiddleware,
    			//service.Middleware().Ctx,
    			service.Middleware().ResponseHandler,
    		)
    		group.Bind(
    			controller.User, // 用户
    		)
    	})
    	// 启动Http Server
    	s.Run()
    }
    // internal/service/middleware.go
    
    func (s *sMiddleware) I18NMiddleware(r *ghttp.Request) {
    	configLang, _ := g.Cfg().Get(r.Context(), "server.lang", "zh_CN")
    	lang := fmt.Sprint(configLang)
    	lang1 := r.GetHeader("Lang") // 获取不到返回""
    	lang2 := r.GetQuery("Lang")  // 获取不到返回 nil
    	// url参数Lang优先级高于header的Lang
    	if gconv.Bool(lang1) {
    		lang = lang1
    	}
    	if lang2 != nil {
    		lang = fmt.Sprint(lang2.Val())
    	}
    	g.Log().Info(r.Context(), "切换当前语言为:", lang)  # 设置为cn
    	r.SetCtx(gi18n.WithLanguage(r.Context(), lang))
    	g.Log().Info(r.Context(), g.I18n().Tf(r.Context(), `{#hello}`, "beep"))
    
    	r.Middleware.Next()
    }

    以上是main/middleware/入参结构体,的部分代码,发起请求:

    curl -X 'POST' \
      'http://127.0.0.1:8199/user' \
      -H 'accept: application/json' \
      -H 'Content-Type: application/json' \
      -d '{
      "desc": "string",
      "displayName": "1qwe1234",
      "email": "",
      "enabled": "enabled",
      "loginName": "1qwe1234",
      "password": "string",
      "phone": ""
    }'

    reponse为:

    {
      "code": 51,
      "message": "The LoginName value `1qwe1234` is not a valid passport format",
      "data": {}
    }

    为何设置了国际化,输出的message并没有进行i18n 转化呢?


    1. 在   service.Middleware().ResponseHandler
      这里面把err.Error() 去匹配一下是否是I18N的 如果是 那就把Err重写一下就好了
  3. 请求校验JSON 的问题,如果结构体字段是一个map 验证JSON ,json内某个字段的值为nil或者null 。框架要出错误,2022-04-09 13:51:07.496 {f8b8f16e7924e416179d0d6c3456e0e8} 500 "POST http 127.0.0.1:8000 /sysConfig/EditData HTTP/1.1" 0.001, 127.0.0.1, "", "PostmanRuntime/7.15.2", 50, "Internal Error", ""

  4. 此处文档未更新

    账号长度为:min到:max位
    账号长度为:{min}到{max}
  5.  v.FirstString(),这个FirstString好像没有了,可以更新下文档