Go中最常用的数据验证库


项目地址: github.com/go-playground/validator/v10

标记标记说明
required必填Field或Struct validate:"required"
omitempty空时忽略Field或Struct validate:"omitempty"
len长度Field validate:"len=0"
eq等于Field validate:"eq=0"
gt大于Field validate:"gt=0"
gte大于等于Field validate:"gte=0"
lt小于Field validate:"lt=0"
lte小于等于Field validate:"lte=0"
eqfield同一结构体字段相等Field validate:"eqfield=Field2"
nefield同一结构体字段不相等Field validate:"nefield=Field2"
gtfield大于同一结构体字段Field validate:"gtfield=Field2"
gtefield大于等于同一结构体字段Field validate:"gtefield=Field2"
ltfield小于同一结构体字段Field validate:"ltfield=Field2"
ltefield小于等于同一结构体字段Field validate:"ltefield=Field2"
eqcsfield跨不同结构体字段相等Struct1.Field validate:"eqcsfield=Struct2.Field2"
necsfield跨不同结构体字段不相等Struct1.Field validate:"necsfield=Struct2.Field2"
gtcsfield大于跨不同结构体字段Struct1.Field validate:"gtcsfield=Struct2.Field2"
gtecsfield大于等于跨不同结构体字段Struct1.Field validate:"gtecsfield=Struct2.Field2"
ltcsfield小于跨不同结构体字段Struct1.Field validate:"ltcsfield=Struct2.Field2"
ltecsfield小于等于跨不同结构体字段Struct1.Field validate:"ltecsfield=Struct2.Field2"
min最大值Field validate:"min=1"
max最小值Field validate:"max=2"
structonly仅验证结构体,不验证任何结构体字段Struct validate:"structonly"
nostructlevel不运行任何结构级别的验证Struct validate:"nostructlevel"
dive向下延伸验证,多层向下需要多个dive标记[][]string validate:"gt=0,dive,len=1,dive,required"
dive Keys & EndKeys与dive同时使用,用于对map对象的键的和值的验证,keys为键,endkeys为值map[string]string validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"
required_with其他字段其中一个不为空且当前字段不为空Field validate:"required_with=Field1 Field2"
required_with_all其他所有字段不为空且当前字段不为空Field validate:"required_with_all=Field1 Field2"
required_without其他字段其中一个为空且当前字段不为空Field `validate:"required_without=Field1 Field2"
required_without_all其他所有字段为空且当前字段不为空Field validate:"required_without_all=Field1 Field2"
isdefault是默认值Field validate:"isdefault=0"
oneof其中之一Field validate:"oneof=5 7 9"
containsfield字段包含另一个字段Field validate:"containsfield=Field2"
excludesfield字段不包含另一个字段Field validate:"excludesfield=Field2"
unique是否唯一,通常用于切片或结构体Field validate:"unique"
alphanum字符串值是否只包含 ASCII 字母数字字符Field validate:"alphanum"
alphaunicode字符串值是否只包含 unicode 字符Field validate:"alphaunicode"
alphanumunicode字符串值是否只包含 unicode 字母数字字符Field validate:"alphanumunicode"
numeric字符串值是否包含基本的数值Field validate:"numeric"
hexadecimal字符串值是否包含有效的十六进制Field validate:"hexadecimal"
hexcolor字符串值是否包含有效的十六进制颜色Field validate:"hexcolor"
lowercase符串值是否只包含小写字符Field validate:"lowercase"
uppercase符串值是否只包含大写字符Field validate:"uppercase"
email字符串值包含一个有效的电子邮件Field validate:"email"
json字符串值是否为有效的 JSONField validate:"json"
file符串值是否包含有效的文件路径,以及该文件是否存在于计算机上Field validate:"file"
url符串值是否包含有效的 urlField validate:"url"
uri符串值是否包含有效的 uriField validate:"uri"
base64字符串值是否包含有效的 base64值Field validate:"base64"
contains字符串值包含子字符串值Field validate:"contains=@"
containsany字符串值包含子字符串值中的任何字符Field validate:"containsany=abc"
containsrune字符串值包含提供的特殊符号值Field validate:"containsrune=☢"
excludes字符串值不包含子字符串值Field validate:"excludes=@"
excludesall字符串值不包含任何子字符串值Field validate:"excludesall=abc"
excludesrune字符串值不包含提供的特殊符号值Field validate:"containsrune=☢"
startswith字符串以提供的字符串值开始Field validate:"startswith=abc"
endswith字符串以提供的字符串值结束Field validate:"endswith=abc"
ip字符串值是否包含有效的 IP 地址Field validate:"ip"
ipv4字符串值是否包含有效的 ipv4地址Field validate:"ipv4"
datetime字符串值是否包含有效的 日期Field validate:"datetime"

单字段校验


对前端传参进行校验

单字段多个条件 校验


package main

import (
 "fmt"
 "strings"

 "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"

 zhtrans "github.com/go-playground/validator/v10/translations/zh"
 // entrans "github.com/go-playground/validator/v10/translations/en"
)

/*
https://www.cnblogs.com/jiujuan/p/13823864.html

https://www.liwenzhou.com/posts/Go/validator-usages/

https://juejin.cn/post/7056823502640250893

https://juejin.cn/post/6847902214279659533
*/

type Student struct {
 Name  string `validate:required`
 Email string `validate:"email"`
 Age   int    `validate:"max=30,min=12"`
}

func main() {
 en := en.New() //英文翻译器
 zh := zh.New() //中文翻译器

 // 第一个参数是必填,如果没有其他的语言设置,就用这第一个
 // 后面的参数是支持多语言环境(
 // uni := ut.New(en, en) 也是可以的
 // uni := ut.New(en, zh, tw)
 uni := ut.New(en, zh)
 trans, _ := uni.GetTranslator("zh"//获取需要的语言

 student := Student{
  Name:  "tom",
  Email: "testemal",
  Age:   40,
 }
 validate := validator.New()

 zhtrans.RegisterDefaultTranslations(validate, trans)

 err := validate.Struct(student)
 if err != nil {
  // fmt.Println(err)

  errs := err.(validator.ValidationErrors)
  fmt.Println(removeStructName(errs.Translate(trans)))
 }
}

func removeStructName(fields map[string]string) map[string]string {
 result := map[string]string{}

 for field, err := range fields {
  result[field[strings.Index(field, ".")+1:]] = err
 }
 return result
}

在线代码

输出:

map[Age:Age必须小于或等于30 Email:Email必须是一个有效的邮箱]


package main

import (
 "fmt"

 "github.com/go-playground/validator/v10"
)

type User struct {
 FirstName string               `validate:"required"`
 LastName  string               `validate:"required"`
 Age       uint8                `validate:"gte=0,lte=130"`
 Email     string               `validate:"required,email"`
 Test      string               `validate:"len=0|min=6,max=24,len=0|alphanum"` // 或者 条件之一, 使用|。 但每个,都是独立的一个逻辑,之间是&的关系(有一个不满足就报错"Error:Field validation"),且条件没有传递,所以要在alphanum前面也加一个len=0|。 而max=24和长度为0不冲突,所以不需要加
 Products  []CreateOrderProduct `validate:"min=1"`                             // 产品列表
}

type CreateOrderProduct struct {
 SkuCode  string `json:"skuCode"`  // sku编码
 Quantity int64  `json:"quantity"` // 商品数量
}

//  oneof:只能是列举出的值其中一个,这些值必须是数值或字符串,以空格分隔,如果字符串中有空格,将字符串用单引号包围,validate:"oneof=red green"
// https://www.cnblogs.com/jiujuan/p/13823864.html

func main() {

 user := &User{
  FirstName: "Badger",
  LastName:  "Smith",
  Age:       115,
  Email:     "Badger.Smith@gmail.com",
  Test:      "",
  Products:  []CreateOrderProduct{},
 }

 validate := validator.New()
 err := validate.Struct(user)
 if err != nil {
  fmt.Println("=== error msg ====")
  fmt.Println(err)

  //if _, ok := err.(*validator.InvalidValidationError); ok {
  // fmt.Println(err)
  // return
  //}
  //
  //fmt.Println("\r\n=========== error field info ====================")
  //for _, err := range err.(validator.ValidationErrors) {
  // // 列出效验出错字段的信息
  // fmt.Println("Namespace: ", err.Namespace())
  // fmt.Println("Fild: ", err.Field())
  // fmt.Println("StructNamespace: ", err.StructNamespace())
  // fmt.Println("StructField: ", err.StructField())
  // fmt.Println("Tag: ", err.Tag())
  // fmt.Println("ActualTag: ", err.ActualTag())
  // fmt.Println("Kind: ", err.Kind())
  // fmt.Println("Type: ", err.Type())
  // fmt.Println("Value: ", err.Value())
  // fmt.Println("Param: ", err.Param())
  // fmt.Println()
  //}

  // from here you can create your own error messages in whatever language you wish
  return
 }
}

在线代码

alt

输出:

=== error msg ====
Key: 'User.Products' Error:Field validation for 'Products' failed on the 'min' tag



跨字段验证


eqfield 同一结构体字段验证相等


eqfield=Field:必须等于 Field 的值

最常见的就是输入2次密码验证

package main

import (
 "fmt"

 "github.com/go-playground/validator/v10"
)

// 多字段联合校验

// eqfield:同一结构体字段验证相等,最常见的就是输入2次密码验证

type Account struct {
 Name      string `validate:"lte=16"`
 Age       int    `validate:"min=20"`
 Password  string `validate:"min=8"`
 Password2 string `validate:"eqfield=Password"`
}

func main() {
 account := &Account{
  Name:      "Badger",
  Age:       115,
  Password:  "qwert12345",
  Password2: "111111",
 }

 validate := validator.New()
 err := validate.Struct(account)
 if err != nil {
  fmt.Println("=== error msg ====")
  fmt.Println(err)

  if _, ok := err.(*validator.InvalidValidationError); ok {
   fmt.Println(err)
   return
  }

  fmt.Println("\r\n=========== error field info ====================")
  for _, err := range err.(validator.ValidationErrors) {
   // 列出效验出错字段的信息
   fmt.Println("Namespace: ", err.Namespace())
   fmt.Println("Fild: ", err.Field())
   fmt.Println("StructNamespace: ", err.StructNamespace())
   fmt.Println("StructField: ", err.StructField())
   fmt.Println("Tag: ", err.Tag())
   fmt.Println("ActualTag: ", err.ActualTag())
   fmt.Println("Kind: ", err.Kind())
   fmt.Println("Type: ", err.Type())
   fmt.Println("Value: ", err.Value())
   fmt.Println("Param: ", err.Param())
   fmt.Println()
  }

  // from here you can create your own error messages in whatever language you wish
  return
 }
}

在线运行

输出:

=== error msg ====
Key: 'Account.Password2' Error:Field validation for 'Password2' failed on the 'eqfield' tag

=========== error field info ====================
Namespace:  Account.Password2
Fild:  Password2
StructNamespace:  Account.Password2
StructField:  Password2
Tag:  eqfield
ActualTag:  eqfield
Kind:  string
Type:  string
Value:  111111
Param:  Password

nefield:同一结构体字段验证不相等


nefield=Field:必须不等于 Field 的值

例如,验证密码不能和用户名相同

package main

import (
 "fmt"

 "github.com/go-playground/validator/v10"
)

// 多字段联合校验

// eqfield:同一结构体字段验证相等,最常见的就是输入2次密码验证

type Account struct {
 Name     string `validate:"lte=16"`
 Age      int    `validate:"min=20"`
 Password string `validate:"min=1,nefield=Name"`
}

func main() {
 account := &Account{
  Name:     "Badger",
  Age:      115,
  Password: "Badger",
 }

 validate := validator.New()
 err := validate.Struct(account)
 if err != nil {
  fmt.Println("=== error msg ====")
  fmt.Println(err)

  if _, ok := err.(*validator.InvalidValidationError); ok {
   fmt.Println(err)
   return
  }

  fmt.Println("\r\n=========== error field info ====================")
  for _, err := range err.(validator.ValidationErrors) {
   // 列出效验出错字段的信息
   fmt.Println("Namespace: ", err.Namespace())
   fmt.Println("Fild: ", err.Field())
   fmt.Println("StructNamespace: ", err.StructNamespace())
   fmt.Println("StructField: ", err.StructField())
   fmt.Println("Tag: ", err.Tag())
   fmt.Println("ActualTag: ", err.ActualTag())
   fmt.Println("Kind: ", err.Kind())
   fmt.Println("Type: ", err.Type())
   fmt.Println("Value: ", err.Value())
   fmt.Println("Param: ", err.Param())
   fmt.Println()
  }

  // from here you can create your own error messages in whatever language you wish
  return
 }
}

输出:

=== error msg ====
Key: 'Account.Password' Error:Field validation for 'Password' failed on the 'nefield' tag

=========== error field info ====================
Namespace:  Account.Password
Fild:  Password
StructNamespace:  Account.Password
StructField:  Password
Tag:  nefield
ActualTag:  nefield
Kind:  string
Type:  string
Value:  Badger
Param:  Name

类似的还有

  • gtfield=Field:必须大于 Field 的值。
  • gtefield=Field: 必须大于等于 Field 的值。
  • ltfield=Field:必须小于 Field 的值。
  • ltefield=Field:必须小于等于 Field 的值。



eqcsfield=Other.Field:必须等于 struct Other 中 Field 的值。


用于验证跨结构体的两个字段是否相等,需要指定另一个字段的名称或路径作为参数,比如 eqcsfield=Other.Field 中的 Other.Field 就是指定的另一个字段。

在使用该选项时,会比较当前字段和指定的另一个字段的值是否相等,如果相等则验证通过,否则验证失败。这个选项通常用于验证密码和确认密码等类似的场景。


package main

import (
 "fmt"

 "github.com/go-playground/validator/v10"
)

type Struct1 struct {
 Field1  string `validate:"eqcsfield=Struct2.Field2""`
 Struct2 struct {
  Field2 string
 }
}

func main() {

 s := &Struct1{
  Field1:  "必须一致",
  Struct2: struct{ Field2 string }{Field2: "没有一致"},
 }

 validate := validator.New()
 err := validate.Struct(s)
 if err != nil {
  fmt.Println("=== error msg ====")
  fmt.Println(err)

  if _, ok := err.(*validator.InvalidValidationError); ok {
   fmt.Println(err)
   return
  }

  fmt.Println("\r\n=========== error field info ====================")
  for _, err := range err.(validator.ValidationErrors) {
   // 列出效验出错字段的信息
   fmt.Println("Namespace: ", err.Namespace())
   fmt.Println("Fild: ", err.Field())
   fmt.Println("StructNamespace: ", err.StructNamespace())
   fmt.Println("StructField: ", err.StructField())
   fmt.Println("Tag: ", err.Tag())
   fmt.Println("ActualTag: ", err.ActualTag())
   fmt.Println("Kind: ", err.Kind())
   fmt.Println("Type: ", err.Type())
   fmt.Println("Value: ", err.Value())
   fmt.Println("Param: ", err.Param())
   fmt.Println()
  }

  // from here you can create your own error messages in whatever language you wish
  return
 }
}

输出:

=== error msg ====
Key: 'Struct1.Field1' Error:Field validation for 'Field1' failed on the 'eqcsfield' tag

=========== error field info ====================
Namespace:  Struct1.Field1
Fild:  Field1
StructNamespace:  Struct1.Field1
StructField:  Field1
Tag:  eqcsfield
ActualTag:  eqcsfield
Kind:  string
Type:  string
Value:  必须一致
Param:  Struct2.Field2

看起来只支持嵌套结构体,不支持两个独立的结构体之间某个字段的比较


eqfieldeqcsfield 的区别在于它们用于比较的字段的位置不同:eqfield 比较的是同一个结构体中的两个字段的值,而 eqcsfield 比较的是当前结构体中的某个字段和另一个(子?)结构体中的字段的值


类似的还有

  • necsfield=Other.Field:必须不等于 struct Other 中 Field 的值。
  • gtcsfield=Other.Field:必须大于 struct Other 中 Field 的值;
  • gtecsfield=Other.Field:必须大于等于 struct Other 中 Field 的值。
  • ltcsfield=Other.Field:必须小于 struct Other 中 Field 的值。
  • ltecsfield=Other.Field:必须小于等于 struct Other 中 Field 的值。

如何比较两个独立结构体中某两个字段的值?




required_with=Field1 Field2:在 Field1 或者 Field2 存在时,必须;

required_with=Field2:在 Field2被填写(即不为空)时,Field1也必须不能为空


package main

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

type User struct {
    Name     string `validate:"required"`
    Email    string `validate:"required_with=Phone"`
    Phone    string
}

func main() {
    user1 := User{
        Name:     "John",
        Email:    "",
        Phone:    "",
    }

    user2 := User{
        Name:     "Mary",
        Email:    "mary@example.com",
        Phone:    "",
    }

    validate := validator.New()

    err1 := validate.Struct(user1)
    if err1 != nil {
        fmt.Println(err1)
    }

    err2 := validate.Struct(user2)
    if err2 != nil {
        fmt.Println(err2)
    }
}

验证通过~

在这个例子中,User 结构体包含 Name、Email 和 Phone 字段。Email 字段被标记为 required_with=Phone,这意味着当 Phone 字段被填写时,Email 字段也必须被填写。

而如果把user1改为:

user1 := User{
  Name:  "John",
  Email: "",
  Phone: "123",
 }

则会报错:

Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required_with' tag

验证不通过




required_with_all=Field1 Field2:在 Field1 与 Field2 都存在时,必须;(仅当所有其他指定的字段都存在时,验证字段才必须存在)


要么有这个tag的全部为空,如果有一个不为空,那所有其他的也都不能为空~

package main

import (
 "fmt"

 "github.com/go-playground/validator/v10"
)

type User6 struct {
 Name  string `validate:"required"`
 Email string `validate:"required_with_all=Phone"`
 Phone string `validate:"required_with_all=Email"`
}

func main() {
 user := User6{
  Name:  "John",
  Email: "",
  Phone: "",
 }

 validate := validator.New()

 err := validate.Struct(user)
 if err != nil {
  fmt.Println(err)
 }
}

Email 和 Phone 字段都被标记为 required_with_all,

这意味着当 Email 和 Phone

  • 要么全都为空

  • 如果其中的任何一个被填写时,另一个也必须被填写(不为空即可,可以不一样)

所以上面代码可以验证通过


如下也是合法的:

package main

import (
 "fmt"

 "github.com/go-playground/validator/v10"
)

type User6 struct {
 Name  string `validate:"required"`
 Email string `validate:"required_with_all=Phone"`
 Phone string `validate:"required_with_all=Email"`
}

func main() {
 user := User6{
  Name:  "John",
  Email: "1",
  Phone: "2",
 }

 validate := validator.New()

 err := validate.Struct(user)
 if err != nil {
  fmt.Println(err)
 }
}

类似的还有:

required_without=Field1 Field2:在 Field1 或者 Field2 不存在时,必须;

Field1 Field2字段其中(至少)一个为空,则当前字段不能为空

package main

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

type User struct {
    Name     string `validate:"required"`
    Email    string
    Phone    string
    Address  string `validate:"required_without=Email Phone"`// Email Phone 中间不能加逗号
}

func main() {
    user1 := User{
        Name:     "John",
        Email:    "",
        Phone:    "",
        Address:  "123 Main St.",
    }

    user2 := User{
        Name:     "Mary",
        Email:    "mary@example.com",
        Phone:    "",
        Address:  "",
    }

    validate := validator.New()

    err1 := validate.Struct(user1)
    if err1 != nil {
        fmt.Println("err1:", err1)
    }

    err2 := validate.Struct(user2)
    if err2 != nil {
       fmt.Println("err2:", err2)
    }
}

输出:

err2: Key: 'User7.Address' Error:Field validation for 'Address' failed on the 'required_without' tag

User 结构体包含 Name、Email、Phone 和 Address 字段。Address 字段被标记为 required_without=Email Phone,这意味着当 Email 和 Phone 字段至少一个为空时,Address 字段必须被填写。


required_without_all=Field1 Field2:在 Field1 与 Field2 都存在时,必须; (仅当所有其他指定字段都不存在时,验证字段才必须...)


Field1 Field2字段都为空时,则当前字段不能为空

package main

import (
 "fmt"

 "github.com/go-playground/validator/v10"
)

type User7 struct {
 Name    string `validate:"required"`
 Email   string
 Phone   string
 Address string `validate:"required_without_all=Email Phone"`
}

func main() {
 user1 := User7{
  Name:    "John",
  Email:   "",
  Phone:   "111",
  Address: "123 Main St.",
 }

 user2 := User7{
  Name:    "Mary",
  Email:   "",
  Phone:   "",
  Address: "",
 }

 validate := validator.New()

 err1 := validate.Struct(user1)
 if err1 != nil {
  fmt.Println("err1:", err1)
 }

 err2 := validate.Struct(user2)
 if err2 != nil {
  fmt.Println("err2:", err2)
 }
}

输出:

err2: Key: 'User7.Address' Error:Field validation for 'Address' failed on the 'required_without_all' tag



验证proto

可参考 Go gRPC进阶-proto数据验证(九)




更复杂的判断


type User struct {
Age uint8 validate:"gte=0,lte=130"
Email string validate:"required,email"
Score int validate:"min=1" // 分数
Gender string validate:"required,oneof=男 女"
}

如果满足以下任意一项则认为通过:

  • Gender=男,Age小于35,Score大于60
  • Gender=女,Age小于40,Score大于50

这种用validator/v10能判断吗?..


这种复杂的验证规则超出了validator/v10的基本功能,需要进行自定义验证函数。可以使用validator/v10的Func函数,通过编写自定义的验证函数来实现这种验证规则。

如下是一个示例代码:

package main

import (
 "fmt"

 "github.com/go-playground/validator/v10"
)

type User struct {
 Age    uint8  `validate:"gte=0,lte=130,customValidation"`
 Email  string `validate:"required,email"`
 Score  int    `validate:"required,gte=0,customValidation"`
 Gender string `validate:"required,oneof=男 女,customValidation"`
}

func validateUser(fl validator.FieldLevel) bool {

 user, ok := fl.Top().Interface().(*User)

 fmt.Println("user is:", user)

 if !ok {
  return false
 }

 if user.Gender == "男" && user.Age < 35 && user.Score > 60 {
  return true
 }
 if user.Gender == "女" && user.Age < 40 && user.Score > 50 {
  return true
 }
 return false
}

// - Gender=男,Age小于35,Score大于60
// - Gender=女,Age小于40,Score大于50

func main() {
 user := &User{
  Age:    36,
  Email:  "example@gmail.com",
  Score:  1711,
  Gender: "男",
 }

 validate := validator.New()
 err := validate.RegisterValidation("customValidation", validateUser)
 if err != nil {
  fmt.Println(err)
  return
 }

 err = validate.Struct(user)

 fmt.Println("err is:", err)
 if err != nil {
  fmt.Println(err)
  return
 }

 fmt.Println("validation succeeded")
}

输出:

user is: &{36 example@gmail.com 1711 男}
user is: &{36 example@gmail.com 1711 男}
user is: &{36 example@gmail.com 1711 男}
err is: Key: 'User.Age' Error:Field validation for 'Age' failed on the 'customValidation' tag
Key: 'User.Score' Error:Field validation for 'Score' failed on the 'customValidation' tag
Key: 'User.Gender' Error:Field validation for 'Gender' failed on the 'customValidation' tag
Key: 'User.Age' Error:Field validation for 'Age' failed on the 'customValidation' tag
Key: 'User.Score' Error:Field validation for 'Score' failed on the 'customValidation' tag
Key: 'User.Gender' Error:Field validation for 'Gender' failed on the 'customValidation' tag



参考资料:

golang之验证器validator

【Go】数据验证-validator

Go 使用validator进行后端数据校验

gopkg.in/go-playground/validator.v10

结构字段验证--validator.v9

Golang验证器之validator使用详解

Go 每日一库之 validator

golang常用库:字段参数验证库-validator使用


有空还想探究下,这么一个工具怎么可以讲这么多期..

B站视频:论一款强大的验证组件在web开发中的重要性

B站视频:go语言验证框架Validator的6种高端操作

B站视频:go开源验证框架validator必会的3个操作

B站视频:validator验证框架3种自定义验证方法

B站视频:validator快速实现字段格式的验证

B站视频:validator快速搞定字段格式验证

B站视频:validator自定义验证与本地化提示

本文由 mdnice 多平台发布

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值