Files
gf/g/util/gvalid/gvalid_check.go

574 lines
20 KiB
Go
Raw Normal View History

// Copyright 2017-2018 gf Author(https://gitee.com/johng/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://gitee.com/johng/gf.
package gvalid
import (
"gitee.com/johng/gf/g/container/gmap"
"gitee.com/johng/gf/g/encoding/gjson"
"gitee.com/johng/gf/g/net/gipv4"
"gitee.com/johng/gf/g/net/gipv6"
"gitee.com/johng/gf/g/os/gtime"
"gitee.com/johng/gf/g/util/gconv"
"gitee.com/johng/gf/g/util/gregex"
"regexp"
"strconv"
"strings"
)
const (
gSINGLE_RULE_PATTERN = `^([\w-]+):{0,1}(.*)` // 单条规则匹配正则
)
var (
// 默认错误消息管理对象(并发安全)
errorMsgMap = gmap.NewStringStringMap()
// 单规则正则对象,这里使用包内部变量存储,不需要多次解析
ruleRegex, _ = regexp.Compile(gSINGLE_RULE_PATTERN)
// 即使参数为空(nil|"")也需要校验的规则,主要是必需规则及关联规则
mustCheckRulesEvenValueEmpty = map[string]struct{} {
"required" : struct{}{},
"required-if" : struct{}{},
"required-unless" : struct{}{},
"required-with" : struct{}{},
"required-with-all" : struct{}{},
"required-without" : struct{}{},
"required-without-all" : struct{}{},
"same" : struct{}{},
"different" : struct{}{},
"in" : struct{}{},
"not-in" : struct{}{},
"regex" : struct{}{},
}
)
// 检测单条数据的规则:
// value为需要校验的数据可以为任意基本数据类型
// msgs为自定义错误信息由于同一条数据的校验规则可能存在多条为方便调用参数类型支持 string/map[string]string 允许传递多个自定义的错误信息如果类型为string那么中间使用"|"符号分隔多个自定义错误;
// params参数为联合校验参数对于需要联合校验的规则有效required-*、same、different
func Check(value interface{}, rules string, msgs interface{}, params...map[string]interface{}) *Error {
// 内部会将参数全部转换为字符串类型进行校验
val := strings.TrimSpace(gconv.String(value))
data := make(map[string]string)
errorMsgs := make(map[string]string)
if len(params) > 0 {
for k, v := range params[0] {
data[k] = gconv.String(v)
}
}
// 自定义错误消息处理
list := make([]string, 0)
customMsgs := make(map[string]string)
switch v := msgs.(type) {
case map[string]string:
customMsgs = v
case string:
list = strings.Split(v, "|")
}
items := strings.Split(strings.TrimSpace(rules), "|")
for index := 0; index < len(items); {
item := items[index]
results := ruleRegex.FindStringSubmatch(item)
ruleKey := strings.TrimSpace(results[1])
ruleVal := strings.TrimSpace(results[2])
match := false
if len(list) > index {
customMsgs[ruleKey] = strings.TrimSpace(list[index])
}
switch ruleKey {
// 必须字段
case "required": fallthrough
case "required-if": fallthrough
case "required-unless": fallthrough
case "required-with": fallthrough
case "required-with-all": fallthrough
case "required-without": fallthrough
case "required-without-all":
match = checkRequired(val, ruleKey, ruleVal, data)
// 长度范围
case "length": fallthrough
case "min-length": fallthrough
case "max-length":
if msg := checkLength(val, ruleKey, ruleVal, customMsgs); msg != "" {
errorMsgs[ruleKey] = msg
} else {
match = true
}
// 大小范围
case "min": fallthrough
case "max": fallthrough
case "between":
if msg := checkSize(val, ruleKey, ruleVal, customMsgs); msg != "" {
errorMsgs[ruleKey] = msg
} else {
match = true
}
// 自定义正则判断
case "regex":
// 需要判断是否被|符号截断,如果是,那么需要进行整合
for i := index + 1; i < len(items); i++ {
// 判断下一个规则是否合法,不合法那么和当前正则规则进行整合
if !gregex.IsMatchString(gSINGLE_RULE_PATTERN, items[i]) {
ruleVal += "|" + items[i]
index++
}
}
match = gregex.IsMatchString(ruleVal, val)
// 日期格式,
case "date":
2018-11-16 18:20:09 +08:00
// 使用标准日期格式检查,但是日期之间必须带连接符号
if _, err := gtime.StrToTime(val); err == nil {
match = true
break
}
2018-11-16 18:20:09 +08:00
// 检查是否不带日期连接符号的格式
if _, err := gtime.StrToTime(val, "Ymd"); err == nil {
match = true
break
}
// 日期格式,需要给定日期格式
case "date-format":
if _, err := gtime.StrToTimeFormat(val, ruleVal); err == nil {
match = true
}
// 两字段值应相同(非敏感字符判断,非类型判断)
case "same":
if v, ok := data[ruleVal]; ok {
if strings.Compare(val, v) == 0 {
match = true
}
}
// 两字段值不应相同(非敏感字符判断,非类型判断)
case "different":
match = true
if v, ok := data[ruleVal]; ok {
if strings.Compare(val, v) == 0 {
match = false
}
}
// 字段值应当在指定范围中
case "in":
array := strings.Split(ruleVal, ",")
for _, v := range array {
if strings.Compare(val, strings.TrimSpace(v)) == 0 {
match = true
break
}
}
// 字段值不应当在指定范围中
case "not-in":
match = true
array := strings.Split(ruleVal, ",")
for _, v := range array {
if strings.Compare(val, strings.TrimSpace(v)) == 0 {
match = false
break
}
}
/*
* 验证所给手机号码是否符合手机号的格式.
* 移动: 134135136137138139150151152157158159182183184187188178(4G)147(上网卡)
* 联通: 130131132155156185186176(4G)145(上网卡)175
* 电信: 133153180181189 177(4G)
* 卫星通信: 1349
* 虚拟运营商: 170173
* 2018新增: 16x, 19x
*/
case "phone":
match = gregex.IsMatchString(`^13[\d]{9}$|^14[5,7]{1}\d{8}$|^15[^4]{1}\d{8}$|^16[\d]{9}$|^17[0,3,5,6,7,8]{1}\d{8}$|^18[\d]{9}$|^19[\d]{9}$`, val)
// 国内座机电话号码:"XXXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"、"XXXXXXXX"
case "telephone":
match = gregex.IsMatchString(`^((\d{3,4})|\d{3,4}-)?\d{7,8}$`, val)
// 腾讯QQ号从10000开始
case "qq":
match = gregex.IsMatchString(`^[1-9][0-9]{4,}$`, val)
// 中国邮政编码
case "postcode":
match = gregex.IsMatchString(`^[1-9]\d{5}$`, val)
/*
公民身份证号
xxxxxx yyyy MM dd 375 0 十八位
xxxxxx yy MM dd 75 0 十五位
地区[1-9]\d{5}
年的前两位(18|19|([23]\d)) 1800-2399
年的后两位\d{2}
月份((0[1-9])|(10|11|12))
天数(([0-2][1-9])|10|20|30|31) 闰年不能禁止29+
三位顺序码\d{3}
两位顺序码\d{2}
校验码 [0-9Xx]
十八位^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$
十五位^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$
(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)
*/
case "id-number":
match = gregex.IsMatchString(`(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)`, val)
// 通用帐号规则(字母开头只能包含字母、数字和下划线长度在6~18之间)
case "passport":
match = gregex.IsMatchString(`^[a-zA-Z]{1}\w{5,17}$`, val)
// 通用密码(任意可见字符长度在6~18之间)
case "password":
match = gregex.IsMatchString(`^[\w\S]{6,18}$`, val)
// 中等强度密码(在弱密码的基础上,必须包含大小写字母和数字)
case "password2":
if gregex.IsMatchString(`^[\w\S]{6,18}$`, val) && gregex.IsMatchString(`[a-z]+`, val) && gregex.IsMatchString(`[A-Z]+`, val) && gregex.IsMatchString(`\d+`, val) {
match = true
}
// 强等强度密码(在弱密码的基础上,必须包含大小写字母、数字和特殊字符)
case "password3":
if gregex.IsMatchString(`^[\w\S]{6,18}$`, val) && gregex.IsMatchString(`[a-z]+`, val) && gregex.IsMatchString(`[A-Z]+`, val) && gregex.IsMatchString(`\d+`, val) && gregex.IsMatchString(`\S+`, val) {
match = true
}
// json
case "json":
if _, err := gjson.Decode([]byte(val)); err == nil {
match = true
}
// 整数
case "integer":
if _, err := strconv.Atoi(val); err == nil {
match = true
}
// 小数
case "float":
if _, err := strconv.ParseFloat(val, 10); err == nil {
match = true
}
// 布尔值(1,true,on,yes:true | 0,false,off,no,"":false)
case "boolean":
if val != "" && val != "0" && val != "false" && val != "off" && val != "no" {
match = true
}
// 邮件
case "email":
match = gregex.IsMatchString(`^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$`, val)
// URL
case "url":
match = gregex.IsMatchString(`^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$`, val)
// domain
case "domain":
match = gregex.IsMatchString(`^([0-9a-zA-Z][0-9a-zA-Z-]{0,62}\.)+([0-9a-zA-Z][0-9a-zA-Z-]{0,62})\.?$`, val)
// IP(IPv4/IPv6)
case "ip":
match = gipv4.Validate(val) || gipv6.Validate(val)
// IPv4
case "ipv4":
match = gipv4.Validate(val)
// IPv6
case "ipv6":
match = gipv6.Validate(val)
// MAC地址
case "mac":
match = gregex.IsMatchString(`^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$`, val)
default:
errorMsgs[ruleKey] = "Invalid rule name:" + ruleKey
}
// 错误消息整合
if !match {
// 不存在则使用默认的错误信息,
// 如果在校验过程中已经设置了错误信息,那么这里便不作处理
if _, ok := errorMsgs[ruleKey]; !ok {
if msg, ok := customMsgs[ruleKey]; ok {
errorMsgs[ruleKey] = msg
} else {
errorMsgs[ruleKey] = errorMsgMap.Get(ruleKey)
}
}
}
index++
}
if len(errorMsgs) > 0 {
return newError([]string{rules}, ErrorMap {
// 单条数值校验没有键名
"" : errorMsgs,
})
}
return nil
}
// 判断必须字段
func checkRequired(value, ruleKey, ruleVal string, params map[string]string) bool {
required := false
switch ruleKey {
// 必须字段
case "required":
required = true
// 必须字段(当任意所给定字段值与所给值相等时)
case "required-if":
required = false
array := strings.Split(ruleVal, ",")
// 必须为偶数,才能是键值对匹配
if len(array)%2 == 0 {
for i := 0; i < len(array); {
tk := array[i]
tv := array[i+1]
if v, ok := params[tk]; ok {
if strings.Compare(tv, v) == 0 {
required = true
break
}
}
i += 2
}
}
// 必须字段(当所给定字段值与所给值都不相等时)
case "required-unless":
required = true
array := strings.Split(ruleVal, ",")
// 必须为偶数,才能是键值对匹配
if len(array)%2 == 0 {
for i := 0; i < len(array); {
tk := array[i]
tv := array[i+1]
if v, ok := params[tk]; ok {
if strings.Compare(tv, v) == 0 {
required = false
break
}
}
i += 2
}
}
// 必须字段(当所给定任意字段值不为空时)
case "required-with":
required = false
array := strings.Split(ruleVal, ",")
for i := 0; i < len(array); i++ {
if v, ok := params[array[i]]; ok {
if v != "" {
required = true
break
}
}
}
// 必须字段(当所给定所有字段值都不为空时)
case "required-with-all":
required = true
array := strings.Split(ruleVal, ",")
for i := 0; i < len(array); i++ {
if v, ok := params[array[i]]; ok {
if v == "" {
required = false
break
}
}
}
// 必须字段(当所给定任意字段值为空时)
case "required-without":
required = false
array := strings.Split(ruleVal, ",")
for i := 0; i < len(array); i++ {
if v, ok := params[array[i]]; ok {
if v == "" {
required = true
break
}
}
}
// 必须字段(当所给定所有字段值都为空时)
case "required-without-all":
required = true
array := strings.Split(ruleVal, ",")
for i := 0; i < len(array); i++ {
if v, ok := params[array[i]]; ok {
if v != "" {
required = false
break
}
}
}
}
if required {
return !(value == "")
} else {
return true
}
}
// 对字段值长度进行检测
func checkLength(value, ruleKey, ruleVal string, custonMsgs map[string]string) string {
msg := ""
switch ruleKey {
// 长度范围
case "length":
array := strings.Split(ruleVal, ",")
min := 0
max := 0
if len(array) > 0 {
if v, err := strconv.Atoi(strings.TrimSpace(array[0])); err == nil {
min = v
}
}
if len(array) > 1 {
if v, err := strconv.Atoi(strings.TrimSpace(array[1])); err == nil {
max = v
}
}
if len(value) < min || len(value) > max {
if v, ok := custonMsgs[ruleKey]; !ok {
msg = errorMsgMap.Get(ruleKey)
} else {
msg = v
}
msg = strings.Replace(msg, ":min", strconv.Itoa(min), -1)
msg = strings.Replace(msg, ":max", strconv.Itoa(max), -1)
return msg
}
// 最小长度
case "min-length":
if min, err := strconv.Atoi(ruleVal); err == nil {
if len(value) < min {
if v, ok := custonMsgs[ruleKey]; !ok {
msg = errorMsgMap.Get(ruleKey)
} else {
msg = v
}
msg = strings.Replace(msg, ":min", strconv.Itoa(min), -1)
}
} else {
msg = "校验参数[" + ruleVal + "]应当为整数类型"
}
// 最大长度
case "max-length":
if max, err := strconv.Atoi(ruleVal); err == nil {
if len(value) > max {
if v, ok := custonMsgs[ruleKey]; !ok {
msg = errorMsgMap.Get(ruleKey)
} else {
msg = v
}
msg = strings.Replace(msg, ":max", strconv.Itoa(max), -1)
}
} else {
msg = "校验参数[" + ruleVal + "]应当为整数类型"
}
}
return msg
}
// 对字段值大小进行检测
func checkSize(value, ruleKey, ruleVal string, custonMsgs map[string]string) string {
msg := ""
switch ruleKey {
// 大小范围
case "between":
array := strings.Split(ruleVal, ",")
min := float64(0)
max := float64(0)
if len(array) > 0 {
if v, err := strconv.ParseFloat(strings.TrimSpace(array[0]), 10); err == nil {
min = v
}
}
if len(array) > 1 {
if v, err := strconv.ParseFloat(strings.TrimSpace(array[1]), 10); err == nil {
max = v
}
}
if v, err := strconv.ParseFloat(value, 10); err == nil {
if v < min || v > max {
if v, ok := custonMsgs[ruleKey]; !ok {
msg = errorMsgMap.Get(ruleKey)
} else {
msg = v
}
msg = strings.Replace(msg, ":min", strconv.FormatFloat(min, 'f', -1, 64), -1)
msg = strings.Replace(msg, ":max", strconv.FormatFloat(max, 'f', -1, 64), -1)
}
} else {
msg = "输入参数[" + value + "]应当为数字类型"
}
// 最小值
case "min":
if min, err := strconv.ParseFloat(ruleVal, 10); err == nil {
if v, err := strconv.ParseFloat(value, 10); err == nil {
if v < min {
if v, ok := custonMsgs[ruleKey]; !ok {
msg = errorMsgMap.Get(ruleKey)
} else {
msg = v
}
msg = strings.Replace(msg, ":min", strconv.FormatFloat(min, 'f', -1, 64), -1)
}
} else {
msg = "输入参数[" + value + "]应当为数字类型"
}
} else {
msg = "校验参数[" + ruleVal + "]应当为数字类型"
}
// 最大值
case "max":
if max, err := strconv.ParseFloat(ruleVal, 10); err == nil {
if v, err := strconv.ParseFloat(value, 10); err == nil {
if v > max {
if v, ok := custonMsgs[ruleKey]; !ok {
msg = errorMsgMap.Get(ruleKey)
} else {
msg = v
}
msg = strings.Replace(msg, ":max", strconv.FormatFloat(max, 'f', -1, 64), -1)
}
} else {
msg = "输入参数[" + value + "]应当为数字类型"
}
} else {
msg = "校验参数[" + ruleVal + "]应当为数字类型"
}
}
return msg
}