diff --git a/TODO.MD b/TODO.MD index 276180371..a5bfb112b 100644 --- a/TODO.MD +++ b/TODO.MD @@ -42,7 +42,7 @@ 1. gform参考 https://gohouse.github.io/gorose/dist/index.html 进行改进 1. 模板引擎增加对对象的支持(参考https://segmentfault.com/q/1010000016829214); 1. 改进gfpool在文件指针变化时的更新; - +1. 去掉ghhtp路由注册和检索时的并发安全锁控制,由于注册并不会产生并发安全,并且不能动态注册(去掉动态注册特性),因此并发安全锁便没有意义; diff --git a/g/util/gvalid/gvalid.go b/g/util/gvalid/gvalid.go index 8401b4e10..5982795d4 100644 --- a/g/util/gvalid/gvalid.go +++ b/g/util/gvalid/gvalid.go @@ -1,25 +1,15 @@ -// Copyright 2017 gf Author(https://gitee.com/johng/gf). All Rights Reserved. +// 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. // 数据校验. -// 本来打算取名gvalidator的,名字太长了,缩写一下 package gvalid import ( - "strings" - "regexp" - "strconv" - "gitee.com/johng/gf/third/github.com/fatih/structs" - "gitee.com/johng/gf/g/os/gtime" - "gitee.com/johng/gf/g/net/gipv4" - "gitee.com/johng/gf/g/net/gipv6" "gitee.com/johng/gf/g/util/gregex" - "gitee.com/johng/gf/g/util/gconv" - "gitee.com/johng/gf/g/encoding/gjson" - "gitee.com/johng/gf/g/container/gmap" + "strings" ) /* @@ -67,701 +57,43 @@ not-in 格式:not-in:value1,value2,... 说明:参 regex 格式:regex:pattern 说明:参数值应当满足正则匹配规则pattern */ -// 默认规则校验错误消息(可以通过接口自定义错误消息) -var defaultMessages = map[string]string { - "required" : "字段不能为空", - "required-if" : "字段不能为空", - "required-unless" : "字段不能为空", - "required-with" : "字段不能为空", - "required-with-all" : "字段不能为空", - "required-without" : "字段不能为空", - "required-without-all" : "字段不能为空", - "date" : "日期格式不正确", - "date-format" : "日期格式不正确", - "email" : "邮箱地址格式不正确", - "phone" : "手机号码格式不正确", - "telephone" : "电话号码格式不正确", - "passport" : "账号格式不合法,必需以字母开头,只能包含字母、数字和下划线,长度在6~18之间", - "password" : "密码格式不合法,密码格式为任意6-18位的可见字符", - "password2" : "密码格式不合法,密码格式为任意6-18位的可见字符,必须包含大小写字母和数字", - "password3" : "密码格式不合法,密码格式为任意6-18位的可见字符,必须包含大小写字母、数字和特殊字符", - "postcode" : "邮政编码不正确", - "id-number" : "身份证号码不正确", - "qq" : "QQ号码格式不正确", - "ip" : "IP地址格式不正确", - "ipv4" : "IPv4地址格式不正确", - "ipv6" : "IPv6地址格式不正确", - "mac" : "MAC地址格式不正确", - "url" : "URL地址格式不正确", - "domain" : "域名格式不正确", - "length" : "字段长度为:min到:max个字符", - "min-length" : "字段最小长度为:min", - "max-length" : "字段最大长度为:max", - "between" : "字段大小为:min到:max", - "min" : "字段最小值为:min", - "max" : "字段最大值为:max", - "json" : "字段应当为JSON格式", - "xml" : "字段应当为XML格式", - "array" : "字段应当为数组", - "integer" : "字段应当为整数", - "float" : "字段应当为浮点数", - "boolean" : "字段应当为布尔值", - "same" : "字段值不合法", - "different" : "字段值不合法", - "in" : "字段值不合法", - "not-in" : "字段值不合法", - "regex" : "字段值不合法", +// 自定义错误信息: map[键名] => 字符串|map[规则]错误信息 +type CustomMsg = map[string]interface{} + +// 解析单条sequence tag,格式: [数值键名/别名@]校验规则[#错误提示], +// 其中校验规则如果有多个那么以"|"符号分隔,错误提示同理。 +func parseSequenceTag(tag string) (name, rule, msg string) { + match, _ := gregex.MatchString(`\s*((\w+)\s*@){0,1}\s*([^#]+)\s*(#\s*(.*)){0,1}\s*`, tag) + return strings.TrimSpace(match[2]), strings.TrimSpace(match[3]), strings.TrimSpace(match[5]) } -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{}{}, - } -) - -// 初始化错误消息管理对象 -func init() { - errorMsgMap.BatchSet(defaultMessages) -} - -// 替换默认的错误提示为指定的自定义提示 -// 主要作用: -// 1、便于多语言错误提示设置; -// 2、默认错误提示信息不满意; -func SetDefaultErrorMsgs(msgs map[string]string) { - errorMsgMap.BatchSet(msgs) -} - -// 判断必须字段 -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 -} - -// 检测键值对参数Map,注意返回参数是一个2维的关联数组,第一维键名为参数键名,第二维为带有错误的校验规则名称,值为错误信息 -func CheckMap(params map[string]interface{}, rules map[string]string, msgs...map[string]interface{}) Error { - var value interface{} - // 自定义消息,非必须参数,因此这里需要做判断 - customMsgs := make(map[string]interface{}) - if len(msgs) > 0 && len(msgs[0]) > 0 { - customMsgs = msgs[0] - } - errorMsgs := make(map[string]map[string]string) - // 以校验规则作为基础 - for key, rule := range rules { - value = nil - if v, ok := params[key]; ok { - value = v +// 解析sequence tag为标准校验规则及自定义错误 +func parseSequenceTags(tags []string) (rules map[string]string, msgs map[string]interface{}) { + rules = make(map[string]string) + msgs = make(map[string]interface{}) + for _, tag := range tags { + name, rule, msg := parseSequenceTag(tag) + // 校验规则 + if len(name) == 0 { + continue } - msg, _ := customMsgs[key] - if e := Check(value, rule, msg, params); e != nil { - _, m := e.FirstItem() - // 如果值为nil|"",并且不需要require*验证时,其他验证失效 - if value == nil || gconv.String(value) == "" { - required := false - // rule => error - for k, _ := range m { - if _, ok := mustCheckRulesEvenValueEmpty[k]; ok { - required = true - break - } - } - if !required { + rules[name] = rule + // 错误提示 + if len(msg) > 0 { + ruleArray := strings.Split(rule, "|") + msgArray := strings.Split(msg, "|") + for k, v := range ruleArray { + if len(msgArray[k]) == 0 { continue } - } - if _, ok := errorMsgs[key]; !ok { - errorMsgs[key] = make(map[string]string) - } - for k, v := range m { - errorMsgs[key][k] = v + // 关联校验会有":"符号 + array := strings.Split(v, ":") + if _, ok := msgs[name]; !ok { + msgs[name] = make(map[string]string) + } + msgs[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k]) } } } - if len(errorMsgs) > 0 { - return errorMsgs - } - return nil -} - -// 校验struct对象属性,object参数也可以是一个指向对象的指针,返回值同CheckMap方法 -func CheckStruct(st interface{}, rules map[string]string, msgs...map[string]interface{}) Error { - fields := structs.Fields(st) - if rules == nil { - rules = make(map[string]string) - } - params := make(map[string]interface{}) - errMsgs := (map[string]interface{})(nil) - if len(msgs) == 0 { - errMsgs = make(map[string]interface{}) - } else { - errMsgs = msgs[0] - } - for _, field := range fields { - params[field.Name()] = field.Value() - if tag := field.Tag("gvalid"); tag != "" { - match, _ := gregex.MatchString(`\s*((\w+)\s*@){0,1}\s*([^#]+)\s*(#\s*(.*)){0,1}\s*`, tag) - name := match[2] - rule := match[3] - msg := match[5] - if len(name) == 0 { - name = field.Name() - } - // params参数使用别名**扩容**(而不仅仅使用别名),仅用于验证使用 - if _, ok := params[name]; !ok { - params[name] = field.Value() - } - // 校验规则 - if _, ok := rules[name]; !ok { - rules[name] = rule - } - // 错误提示 - if len(msg) > 0 { - ruleArray := strings.Split(rule, "|") - msgArray := strings.Split(msg, "|") - for k, v := range ruleArray { - if len(msgArray[k]) == 0 { - continue - } - array := strings.Split(v, ":") - if _, ok := errMsgs[name]; !ok { - errMsgs[name] = make(map[string]string) - } - errMsgs[name].(map[string]string)[array[0]] = msgArray[k] - } - } - } - - } - return CheckMap(params, rules, errMsgs) -} - -// 检测单条数据的规则. -// val为校验数据,可以为任意基本数据格式; -// msgs为自定义错误信息,由于同一条数据的校验规则可能存在多条,为方便调用,参数类型支持string/map[string]string,允许传递多个自定义的错误信息,如果类型为string,那么中间使用"|"符号分隔多个自定义错误; -// params参数为表单联合校验参数,对于需要联合校验的规则有效,如:required-*、same、different; -func Check(val interface{}, rules string, msgs interface{}, params...map[string]interface{}) Error { - // 内部会将参数全部转换为字符串类型进行校验 - value := strings.TrimSpace(gconv.String(val)) - 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) - custonMsgs := make(map[string]string) - switch value := msgs.(type) { - case map[string]string: - custonMsgs = value - case string: - list = strings.Split(value, "|") - } - 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 { - custonMsgs[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(value, ruleKey, ruleVal, data) - - // 长度范围 - case "length": fallthrough - case "min-length": fallthrough - case "max-length": - if msg := checkLength(value, ruleKey, ruleVal, custonMsgs); msg != "" { - errorMsgs[ruleKey] = msg - } else { - match = true - } - - // 大小范围 - case "min": fallthrough - case "max": fallthrough - case "between": - if msg := checkSize(value, ruleKey, ruleVal, custonMsgs); 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, value) - - // 日期格式, - case "date": - if _, err := gtime.StrToTime(value); err == nil { - match = true - break - } - - // 日期格式,需要给定日期格式 - case "date-format": - if _, err := gtime.StrToTimeFormat(value, ruleVal); err == nil { - match = true - } - - // 两字段值应相同(非敏感字符判断,非类型判断) - case "same": - if v, ok := data[ruleVal]; ok { - if strings.Compare(value, v) == 0 { - match = true - } - } - - // 两字段值不应相同(非敏感字符判断,非类型判断) - case "different": - match = true - if v, ok := data[ruleVal]; ok { - if strings.Compare(value, v) == 0 { - match = false - } - } - - // 字段值应当在指定范围中 - case "in": - array := strings.Split(ruleVal, ",") - for _, v := range array { - if strings.Compare(value, strings.TrimSpace(v)) == 0 { - match = true - break - } - } - - // 字段值不应当在指定范围中 - case "not-in": - match = true - array := strings.Split(ruleVal, ",") - for _, v := range array { - if strings.Compare(value, strings.TrimSpace(v)) == 0 { - match = false - break - } - } - - /* - * 验证所给手机号码是否符合手机号的格式. - * 移动:134、135、136、137、138、139、150、151、152、157、158、159、182、183、184、187、188、178(4G)、147(上网卡); - * 联通:130、131、132、155、156、185、186、176(4G)、145(上网卡)、175; - * 电信:133、153、180、181、189 、177(4G); - * 卫星通信: 1349 - * 虚拟运营商:170、173 - */ - case "phone": - match = gregex.IsMatchString(`^13[\d]{9}$|^14[5,7]{1}\d{8}$|^15[^4]{1}\d{8}$|^17[0,3,5,6,7,8]{1}\d{8}$|^18[\d]{9}$`, value) - - // 国内座机电话号码:"XXXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"、"XXXXXXXX" - case "telephone": - match = gregex.IsMatchString(`^((\d{3,4})|\d{3,4}-)?\d{7,8}$`, value) - - // 腾讯QQ号,从10000开始 - case "qq": - match = gregex.IsMatchString(`^[1-9][0-9]{4,}$`, value) - - // 中国邮政编码 - case "postcode": - match = gregex.IsMatchString(`^[1-9]\d{5}$`, value) - - /* - 公民身份证号 - 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}$)`, value) - - // 通用帐号规则(字母开头,只能包含字母、数字和下划线,长度在6~18之间) - case "passport": - match = gregex.IsMatchString(`^[a-zA-Z]{1}\w{5,17}$`, value) - - // 通用密码(任意可见字符,长度在6~18之间) - case "password": - match = gregex.IsMatchString(`^[\w\S]{6,18}$`, value) - - // 中等强度密码(在弱密码的基础上,必须包含大小写字母和数字) - case "password2": - if gregex.IsMatchString(`^[\w\S]{6,18}$`, value) && gregex.IsMatchString(`[a-z]+`, value) && gregex.IsMatchString(`[A-Z]+`, value) && gregex.IsMatchString(`\d+`, value) { - match = true - } - - // 强等强度密码(在弱密码的基础上,必须包含大小写字母、数字和特殊字符) - case "password3": - if gregex.IsMatchString(`^[\w\S]{6,18}$`, value) && gregex.IsMatchString(`[a-z]+`, value) && gregex.IsMatchString(`[A-Z]+`, value) && gregex.IsMatchString(`\d+`, value) && gregex.IsMatchString(`\S+`, value) { - match = true - } - - // json - case "json": - if _, err := gjson.Decode([]byte(value)); err == nil { - match = true - } - - // 整数 - case "integer": - if _, err := strconv.Atoi(value); err == nil { - match = true - } - - // 小数 - case "float": - if _, err := strconv.ParseFloat(value, 10); err == nil { - match = true - } - - // 布尔值(1,true,on,yes:true | 0,false,off,no,"":false) - case "boolean": - if value != "" && value != "0" && value != "false" && value != "off" && value != "no" { - match = true - } - - // 邮件 - case "email": - match = gregex.IsMatchString(`^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$`, value) - - // URL - case "url": - match = gregex.IsMatchString(`^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$`, value) - - // 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})\.?$`, value) - - // IP(IPv4/IPv6) - case "ip": - match = gipv4.Validate(value) || gipv6.Validate(value) - - // IPv4 - case "ipv4": - match = gipv4.Validate(value) - - // IPv6 - case "ipv6": - match = gipv6.Validate(value) - - // MAC地址 - case "mac": - match = gregex.IsMatchString(`^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$`, value) - - default: - errorMsgs[ruleKey] = "Invalid rule name:" + ruleKey - } - - // 错误消息整合 - if !match { - // 不存在则使用默认的错误信息, - // 如果在校验过程中已经设置了错误信息,那么这里便不作处理 - if _, ok := errorMsgs[ruleKey]; !ok { - if msg, ok := custonMsgs[ruleKey]; ok { - errorMsgs[ruleKey] = msg - } else { - errorMsgs[ruleKey] = errorMsgMap.Get(ruleKey) - } - } - } - index++ - } - if len(errorMsgs) > 0 { - e := make(Error) - e[value] = errorMsgs - return e - } - return nil + return } \ No newline at end of file diff --git a/g/util/gvalid/gvalid_check.go b/g/util/gvalid/gvalid_check.go new file mode 100644 index 000000000..cc3ae4744 --- /dev/null +++ b/g/util/gvalid/gvalid_check.go @@ -0,0 +1,566 @@ +// 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": + if _, err := gtime.StrToTime(val); 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 + } + } + + /* + * 验证所给手机号码是否符合手机号的格式. + * 移动:134、135、136、137、138、139、150、151、152、157、158、159、182、183、184、187、188、178(4G)、147(上网卡); + * 联通:130、131、132、155、156、185、186、176(4G)、145(上网卡)、175; + * 电信:133、153、180、181、189 、177(4G); + * 卫星通信: 1349 + * 虚拟运营商:170、173 + */ + case "phone": + match = gregex.IsMatchString(`^13[\d]{9}$|^14[5,7]{1}\d{8}$|^15[^4]{1}\d{8}$|^17[0,3,5,6,7,8]{1}\d{8}$|^18[\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 +} + diff --git a/g/util/gvalid/gvalid_check_map.go b/g/util/gvalid/gvalid_check_map.go new file mode 100644 index 000000000..3f26bbd4a --- /dev/null +++ b/g/util/gvalid/gvalid_check_map.go @@ -0,0 +1,108 @@ +// 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/util/gconv" + "strings" +) + +// 检测键值对参数Map, +// rules参数支持 []string / map[string]string 类型,前面一种类型支持返回校验结果顺序(具体格式参考struct tag),后一种不支持; +// rules参数中得 map[string]string 是一个2维的关联数组,第一维键名为参数键名,第二维为带有错误的校验规则名称,值为错误信息。 +func CheckMap(params map[string]interface{}, rules interface{}, msgs...CustomMsg) *Error { + // 真实校验规则数据结构 + checkRules := make(map[string]string) + // 真实自定义错误信息数据结构 + customMsgs := make(CustomMsg) + // 返回的顺序规则 + errorRules := make([]string, 0) + // 返回的校验错误 + errorMaps := make(ErrorMap) + // 解析rules参数 + switch v := rules.(type) { + // 支持校验错误顺序: []sequence tag + case []string: + for _, tag := range v { + name, rule, msg := parseSequenceTag(tag) + if len(name) == 0 { + continue + } + // 错误提示 + if len(msg) > 0 { + ruleArray := strings.Split(rule, "|") + msgArray := strings.Split(msg, "|") + for k, v := range ruleArray { + // 如果msg条数比rule少,那么多余的rule使用默认的错误信息 + if len(msgArray) <= k { + continue + } + if len(msgArray[k]) == 0 { + continue + } + array := strings.Split(v, ":") + if _, ok := customMsgs[name]; !ok { + customMsgs[name] = make(map[string]string) + } + customMsgs[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k]) + } + } + checkRules[name] = rule + errorRules = append(errorRules, name + "@" + rule) + } + + // 不支持校验错误顺序: map[键名]校验规则 + case map[string]string: + checkRules = v + } + // 自定义错误消息,非必须参数,优先级比rules参数中定义的错误消息更高 + if len(msgs) > 0 && len(msgs[0]) > 0 { + if len(customMsgs) > 0 { + for k, v := range msgs[0] { + customMsgs[k] = v + } + } else { + customMsgs = msgs[0] + } + } + // 开始执行校验: 以校验规则作为基础进行遍历校验 + value := (interface{})(nil) + // 这里的rule变量为多条校验规则,不包含名字或者错误信息定义 + for key, rule := range checkRules { + value = nil + if v, ok := params[key]; ok { + value = v + } + if e := Check(value, rule, customMsgs[key], params); e != nil { + _, item := e.FirstItem() + // 如果值为nil|"",并且不需要require*验证时,其他验证失效 + if value == nil || gconv.String(value) == "" { + required := false + // rule => error + for k, _ := range item { + if _, ok := mustCheckRulesEvenValueEmpty[k]; ok { + required = true + break + } + } + if !required { + continue + } + } + if _, ok := errorMaps[key]; !ok { + errorMaps[key] = make(map[string]string) + } + for k, v := range item { + errorMaps[key][k] = v + } + } + } + if len(errorMaps) > 0 { + return newError(errorRules, errorMaps) + } + return nil +} diff --git a/g/util/gvalid/gvalid_check_struct.go b/g/util/gvalid/gvalid_check_struct.go new file mode 100644 index 000000000..4bbed0f2f --- /dev/null +++ b/g/util/gvalid/gvalid_check_struct.go @@ -0,0 +1,153 @@ +// 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/util/gconv" + "gitee.com/johng/gf/third/github.com/fatih/structs" + "strings" +) + +// 校验struct对象属性,object参数也可以是一个指向对象的指针,返回值同CheckMap方法。 +// struct的数据校验结果信息是顺序的。 +func CheckStruct(object interface{}, rules interface{}, msgs...CustomMsg) *Error { + fields := structs.Fields(object) + params := make(map[string]interface{}) + checkRules := make(map[string]string) + customMsgs := make(CustomMsg) + // 返回的顺序规则 + errorRules := make([]string, 0) + // 返回的校验错误 + errorMaps := make(ErrorMap) + // 解析rules参数 + switch v := rules.(type) { + // 支持校验错误顺序: []sequence tag + case []string: + for _, tag := range v { + name, rule, msg := parseSequenceTag(tag) + if len(name) == 0 { + continue + } + // 错误提示 + if len(msg) > 0 { + ruleArray := strings.Split(rule, "|") + msgArray := strings.Split(msg, "|") + for k, v := range ruleArray { + // 如果msg条数比rule少,那么多余的rule使用默认的错误信息 + if len(msgArray) <= k { + continue + } + if len(msgArray[k]) == 0 { + continue + } + array := strings.Split(v, ":") + if _, ok := customMsgs[name]; !ok { + customMsgs[name] = make(map[string]string) + } + customMsgs[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k]) + } + } + checkRules[name] = rule + errorRules = append(errorRules, name + "@" + rule) + } + + // 不支持校验错误顺序: map[键名]校验规则 + case map[string]string: + checkRules = v + } + // 首先, 按照属性循环一遍将strcut的属性、数值、tag解析 + for _, field := range fields { + params[field.Name()] = field.Value() + if tag := field.Tag("gvalid"); tag != "" { + // sequence tag == struct tag, 这里的name为别名 + name, rule, msg := parseSequenceTag(tag) + if len(name) == 0 { + name = field.Name() + } + // params参数使用别名**扩容**(而不仅仅使用别名),仅用于验证使用 + if _, ok := params[name]; !ok { + params[name] = field.Value() + } + // 校验规则 + if _, ok := checkRules[name]; !ok { + checkRules[name] = rule + errorRules = append(errorRules, name + "@" + rule) + } else { + // 传递的rules规则会覆盖struct tag的规则 + continue + } + // 错误提示 + if len(msg) > 0 { + ruleArray := strings.Split(rule, "|") + msgArray := strings.Split(msg, "|") + for k, v := range ruleArray { + // 如果msg条数比rule少,那么多余的rule使用默认的错误信息 + if len(msgArray) <= k { + continue + } + if len(msgArray[k]) == 0 { + continue + } + array := strings.Split(v, ":") + if _, ok := customMsgs[name]; !ok { + customMsgs[name] = make(map[string]string) + } + customMsgs[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k]) + } + } + } + } + // 自定义错误消息,非必须参数,优先级比rules参数中以及struct tag中定义的错误消息更高 + if len(msgs) > 0 && len(msgs[0]) > 0 { + if len(customMsgs) > 0 { + for k, v := range msgs[0] { + customMsgs[k] = v + } + } else { + customMsgs = msgs[0] + } + } + + /* 以下逻辑和CheckMap相同 */ + + // 开始执行校验: 以校验规则作为基础进行遍历校验 + value := (interface{})(nil) + // 这里的rule变量为多条校验规则,不包含名字或者错误信息定义 + for key, rule := range checkRules { + value = nil + if v, ok := params[key]; ok { + value = v + } + if e := Check(value, rule, customMsgs[key], params); e != nil { + _, item := e.FirstItem() + // 如果值为nil|"",并且不需要require*验证时,其他验证失效 + if value == nil || gconv.String(value) == "" { + required := false + // rule => error + for k, _ := range item { + if _, ok := mustCheckRulesEvenValueEmpty[k]; ok { + required = true + break + } + } + if !required { + continue + } + } + if _, ok := errorMaps[key]; !ok { + errorMaps[key] = make(map[string]string) + } + for k, v := range item { + errorMaps[key][k] = v + } + } + } + if len(errorMaps) > 0 { + return newError(errorRules, errorMaps) + } + return nil +} diff --git a/g/util/gvalid/gvalid_error.go b/g/util/gvalid/gvalid_error.go new file mode 100644 index 000000000..8db62bba7 --- /dev/null +++ b/g/util/gvalid/gvalid_error.go @@ -0,0 +1,132 @@ +// Copyright 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 "strings" + +// 校验错误对象 +type Error struct { + rules []string // 校验结果顺序(可能为nil) + errors ErrorMap // 校验结果(map无序) + firstKey string // 第一条错误项键名(常用操作冗余数据) + firstItem map[string]string // 第一条错误项(常用操作冗余数据) +} + +// 校验错误信息: map[键名]map[规则名]错误信息 +type ErrorMap map[string]map[string]string + + +// 创建一个校验错误对象指针 +func newError(rules []string, errors map[string]map[string]string) *Error { + return &Error { + rules : rules, + errors : errors, + } +} + +// 获得规则与错误信息的map; 当校验结果为多条数据校验时,返回第一条错误map(此时类似FirstItem) +func (e *Error) Map() map[string]string { + _, m := e.FirstItem() + return m +} + +// 获得原始校验结果ErrorMap +func (e *Error) Maps() ErrorMap { + return e.errors +} + +// 只获取第一个键名的校验错误项 +func (e *Error) FirstItem() (key string, msgs map[string]string) { + if e.firstItem != nil { + return e.firstKey, e.firstItem + } + // 有序 + if len(e.rules) > 0 { + for _, v := range e.rules { + name, _, _ := parseSequenceTag(v) + if m, ok := e.errors[name]; ok { + e.firstKey = name + e.firstItem = m + return name, m + } + } + } + // 无序 + for k, m := range e.errors { + e.firstKey = k + e.firstItem = m + return k, m + } + return "", nil +} + +// 只获取第一个校验错误项的规则及错误信息 +func (e *Error) FirstRule() (rule string, err string) { + // 有序 + if len(e.rules) > 0 { + for _, v := range e.rules { + name, rule, _ := parseSequenceTag(v) + if m, ok := e.errors[name]; ok { + for _, rule := range strings.Split(rule, "|") { + array := strings.Split(rule, ":") + rule = strings.TrimSpace(array[0]) + if err, ok := m[rule]; ok { + return rule, err + } + } + } + } + } + // 无序 + for _, m := range e.errors { + for k, v := range m { + return k, v + } + } + return "", "" +} + +// 只获取第一个校验错误项的错误信息 +func (e *Error) FirstString() (err string) { + _, err = e.FirstRule() + return +} + +// 将所有错误信息构建称字符串,多个错误信息字符串使用"; "符号分隔 +func (e *Error) String() string { + return strings.Join(e.Strings(), "; ") +} + +// 只返回错误信息,构造成字符串数组返回 +func (e *Error) Strings() (errs []string) { + errs = make([]string, 0) + // 有序 + if len(e.rules) > 0 { + for _, v := range e.rules { + name, rule, _ := parseSequenceTag(v) + if m, ok := e.errors[name]; ok { + for _, rule := range strings.Split(rule, "|") { + array := strings.Split(rule, ":") + rule = strings.TrimSpace(array[0]) + if err, ok := m[rule]; ok { + errs = append(errs, err) + } + } + } + } + return errs + } + // 无序 + for _, m := range e.errors { + for _, err := range m { + errs = append(errs, err) + } + } + return +} \ No newline at end of file diff --git a/g/util/gvalid/gvalid_message.go b/g/util/gvalid/gvalid_message.go new file mode 100644 index 000000000..ce24172d1 --- /dev/null +++ b/g/util/gvalid/gvalid_message.go @@ -0,0 +1,68 @@ +// Copyright 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 + +// 默认规则校验错误消息(可以通过接口自定义错误消息) +var defaultMessages = map[string]string { + "required" : "字段不能为空", + "required-if" : "字段不能为空", + "required-unless" : "字段不能为空", + "required-with" : "字段不能为空", + "required-with-all" : "字段不能为空", + "required-without" : "字段不能为空", + "required-without-all" : "字段不能为空", + "date" : "日期格式不正确", + "date-format" : "日期格式不正确", + "email" : "邮箱地址格式不正确", + "phone" : "手机号码格式不正确", + "telephone" : "电话号码格式不正确", + "passport" : "账号格式不合法,必需以字母开头,只能包含字母、数字和下划线,长度在6~18之间", + "password" : "密码格式不合法,密码格式为任意6-18位的可见字符", + "password2" : "密码格式不合法,密码格式为任意6-18位的可见字符,必须包含大小写字母和数字", + "password3" : "密码格式不合法,密码格式为任意6-18位的可见字符,必须包含大小写字母、数字和特殊字符", + "postcode" : "邮政编码不正确", + "id-number" : "身份证号码不正确", + "qq" : "QQ号码格式不正确", + "ip" : "IP地址格式不正确", + "ipv4" : "IPv4地址格式不正确", + "ipv6" : "IPv6地址格式不正确", + "mac" : "MAC地址格式不正确", + "url" : "URL地址格式不正确", + "domain" : "域名格式不正确", + "length" : "字段长度为:min到:max个字符", + "min-length" : "字段最小长度为:min", + "max-length" : "字段最大长度为:max", + "between" : "字段大小为:min到:max", + "min" : "字段最小值为:min", + "max" : "字段最大值为:max", + "json" : "字段应当为JSON格式", + "xml" : "字段应当为XML格式", + "array" : "字段应当为数组", + "integer" : "字段应当为整数", + "float" : "字段应当为浮点数", + "boolean" : "字段应当为布尔值", + "same" : "字段值不合法", + "different" : "字段值不合法", + "in" : "字段值不合法", + "not-in" : "字段值不合法", + "regex" : "字段值不合法", +} + +// 初始化错误消息管理对象 +func init() { + errorMsgMap.BatchSet(defaultMessages) +} + +// 替换默认的错误提示为指定的自定义提示 +// 主要作用: +// 1、便于多语言错误提示设置; +// 2、默认错误提示信息不满意; +func SetDefaultErrorMsgs(msgs map[string]string) { + errorMsgMap.BatchSet(msgs) +} \ No newline at end of file diff --git a/g/util/gvalid/gvalid_result.go b/g/util/gvalid/gvalid_result.go deleted file mode 100644 index 807d812ff..000000000 --- a/g/util/gvalid/gvalid_result.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 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 "strings" - -// 校验错误对象 -type Error map[string]map[string]string - -// 只获取第一个校验错误项 -func (e Error) FirstItem() (string, map[string]string) { - for k, m := range e { - return k, m - } - return "", nil -} - -// 只获取第一个校验错误项的规则及错误信息 -func (e Error) FirstRule() (string, string) { - for _, m := range e { - for k, v := range m { - return k, v - } - } - return "", "" -} - -// 只获取第一个校验错误项的错误信息 -func (e Error) FirstString() (string) { - for _, m := range e { - for _, v := range m { - return v - } - } - return "" -} - -// 将所有错误信息构建称字符串,多个错误信息字符串使用"; "符号分隔 -func (e Error) String() string { - return strings.Join(e.Strings(), "; ") -} - -// 只返回错误信息,构造成字符串数组返回 -func (e Error) Strings() []string { - array := make([]string, 0) - for _, m := range e { - for _, v := range m { - array = append(array, v) - } - } - return array -} \ No newline at end of file diff --git a/g/util/gvalid/gvalid_test.go b/g/util/gvalid/gvalid_test.go index fd96f0206..61b6e1cd1 100644 --- a/g/util/gvalid/gvalid_test.go +++ b/g/util/gvalid/gvalid_test.go @@ -7,20 +7,19 @@ // 单元测试 // go test *.go -bench=".*" -package gvalid_test +package gvalid import ( - "testing" - "gitee.com/johng/gf/g/util/gvalid" "strings" + "testing" ) func Test_Regex(t *testing.T) { rule := `regex:\d{6}|\D{6}|length:6,16` - if m := gvalid.Check("123456", rule, nil); m != nil { + if m := Check("123456", rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check("abcde6", rule, nil); m == nil { + if m := Check("abcde6", rule, nil); m == nil { t.Error("校验失败") } } @@ -34,14 +33,14 @@ func Test_CheckMap(t *testing.T) { "id" : "required|between:1,100", "name" : "required|length:6,16", } - msgs := map[string]interface{} { + msgs := CustomMsg { "id" : "ID不能为空|ID范围应当为:min到:max", "name" : map[string]string { "required" : "名称不能为空", "length" : "名称长度为:min到:max个字符", }, } - if m := gvalid.CheckMap(kvmap, rules, msgs); m == nil { + if m := CheckMap(kvmap, rules, msgs); m == nil { t.Error("CheckMap校验失败") } @@ -60,7 +59,7 @@ func Test_CheckMap(t *testing.T) { "length" : "名称长度为:min到:max个字符", }, } - if m := gvalid.CheckMap(kvmap, rules, msgs); m != nil { + if m := CheckMap(kvmap, rules, msgs); m != nil { t.Error(m) } } @@ -82,80 +81,80 @@ func Test_CheckObject(t *testing.T) { "Age" : "年龄为18到30周岁", } obj := &Object{"john", 16} - if m := gvalid.CheckStruct(obj, rules, msgs); m == nil { + if m := CheckStruct(obj, rules, msgs); m == nil { t.Error("CheckObject校验失败") } } func Test_Required(t *testing.T) { - if m := gvalid.Check("1", "required", nil); m != nil { + if m := Check("1", "required", nil); m != nil { t.Error(m) } - if m := gvalid.Check("", "required", nil); m == nil { + if m := Check("", "required", nil); m == nil { t.Error(m) } - if m := gvalid.Check("", "required-if:id,1,age,18", nil, map[string]interface{}{"id" : 1, "age" : 19}); m == nil { + if m := Check("", "required-if:id,1,age,18", nil, map[string]interface{}{"id" : 1, "age" : 19}); m == nil { t.Error("Required校验失败") } - if m := gvalid.Check("", "required-if:id,1,age,18", nil, map[string]interface{}{"id" : 2, "age" : 19}); m != nil { + if m := Check("", "required-if:id,1,age,18", nil, map[string]interface{}{"id" : 2, "age" : 19}); m != nil { t.Error("Required校验失败") } } func Test_Ip(t *testing.T) { - if m := gvalid.Check("10.0.0.1", "ipv4", nil); m != nil { + if m := Check("10.0.0.1", "ipv4", nil); m != nil { t.Error(m) } - if m := gvalid.Check("0.0.0.0", "ipv4", nil); m != nil { + if m := Check("0.0.0.0", "ipv4", nil); m != nil { t.Error(m) } - if m := gvalid.Check("1920.0.0.0", "ipv4", nil); m == nil { + if m := Check("1920.0.0.0", "ipv4", nil); m == nil { t.Error("ipv4校验失败") } - if m := gvalid.Check("fe80::5484:7aff:fefe:9799", "ipv6", nil); m != nil { + if m := Check("fe80::5484:7aff:fefe:9799", "ipv6", nil); m != nil { t.Error(m) } - if m := gvalid.Check("fe80::5484:7aff:fefe:9799123", "ipv6", nil); m == nil { + if m := Check("fe80::5484:7aff:fefe:9799123", "ipv6", nil); m == nil { t.Error(m) } } func Test_Length(t *testing.T) { rule := "length:6,16" - if m := gvalid.Check("123456", rule, nil); m != nil { + if m := Check("123456", rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check("12345", rule, nil); m == nil { + if m := Check("12345", rule, nil); m == nil { t.Error("长度校验失败") } } func Test_MinLength(t *testing.T) { rule := "min-length:6" - if m := gvalid.Check("123456", rule, nil); m != nil { + if m := Check("123456", rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check("12345", rule, nil); m == nil { + if m := Check("12345", rule, nil); m == nil { t.Error("长度校验失败") } } func Test_MaxLength(t *testing.T) { rule := "max-length:6" - if m := gvalid.Check("12345", rule, nil); m != nil { + if m := Check("12345", rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check("1234567", rule, nil); m == nil { + if m := Check("1234567", rule, nil); m == nil { t.Error("长度校验失败") } } func Test_Between(t *testing.T) { rule := "between:6.01, 10.01" - if m := gvalid.Check(10, rule, nil); m != nil { + if m := Check(10, rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check(10.02, rule, nil); m == nil { + if m := Check(10.02, rule, nil); m == nil { t.Error("大小范围校验失败") } } @@ -166,17 +165,17 @@ func Test_SetDefaultErrorMsgs(t *testing.T) { "integer" : "请输入一个整数", "length" : "参数长度不对啊老铁", } - gvalid.SetDefaultErrorMsgs(msgs) - m := gvalid.Check("6.66", rule, nil) - if len(m) != 2 { + SetDefaultErrorMsgs(msgs) + e := Check("6.66", rule, nil) + if e == nil || len(e.Map()) != 2 { t.Error("规则校验失败") } else { - if v, ok := m["integer"]; ok { + if v, ok := e.Map()["integer"]; ok { if strings.Compare(v, msgs["integer"]) != 0 { t.Error("错误信息不匹配") } } - if v, ok := m["length"]; ok { + if v, ok := e.Map()["length"]; ok { if strings.Compare(v, msgs["length"]) != 0 { t.Error("错误信息不匹配") } @@ -190,16 +189,16 @@ func Test_CustomError1(t *testing.T) { "integer" : "请输入一个整数", "length" : "参数长度不对啊老铁", } - m := gvalid.Check("6.66", rule, msgs) - if len(m) != 2 { + e := Check("6.66", rule, msgs) + if e == nil || len(e.Map()) != 2 { t.Error("规则校验失败") } else { - if v, ok := m["integer"]; ok { + if v, ok := e.Map()["integer"]; ok { if strings.Compare(v, msgs["integer"]) != 0 { t.Error("错误信息不匹配") } } - if v, ok := m["length"]; ok { + if v, ok := e.Map()["length"]; ok { if strings.Compare(v, msgs["length"]) != 0 { t.Error("错误信息不匹配") } @@ -210,16 +209,16 @@ func Test_CustomError1(t *testing.T) { func Test_CustomError2(t *testing.T) { rule := "integer|length:6,16" msgs := "请输入一个整数|参数长度不对啊老铁" - m := gvalid.Check("6.66", rule, msgs) - if len(m) != 2 { + e := Check("6.66", rule, msgs) + if e == nil || len(e.Map()) != 2 { t.Error("规则校验失败") } else { - if v, ok := m["integer"]; ok { + if v, ok := e.Map()["integer"]; ok { if strings.Compare(v, "请输入一个整数") != 0 { t.Error("错误信息不匹配") } } - if v, ok := m["length"]; ok { + if v, ok := e.Map()["length"]; ok { if strings.Compare(v, "参数长度不对啊老铁") != 0 { t.Error("错误信息不匹配") } @@ -236,7 +235,7 @@ func Test_CheckMapWithNilAndNotRequiredField(t *testing.T) { "id" : "required", "name" : "length:4,16", } - if m := gvalid.CheckMap(data, rules); m != nil { + if m := CheckMap(data, rules); m != nil { t.Error(m) } } \ No newline at end of file diff --git a/geg/util/gvalid/gvalid.go b/geg/util/gvalid/gvalid.go index ad7e7795e..90e41572d 100644 --- a/geg/util/gvalid/gvalid.go +++ b/geg/util/gvalid/gvalid.go @@ -1,8 +1,8 @@ package main import ( + "gitee.com/johng/gf/g" "gitee.com/johng/gf/g/util/gvalid" - "gitee.com/johng/gf/g/util/gutil" ) func main() { @@ -60,6 +60,8 @@ func main() { "same" : "两次密码输入不相等", }, } - gutil.Dump(gvalid.CheckMap(params, rules, msgs)) + if e := gvalid.CheckMap(params, rules, msgs); e != nil { + g.Dump(e.Maps()) + } // map[passport:map[length:账号长度应当在6到16之间] password:map[same:两次密码输入不相等]] } diff --git a/geg/util/gvalid/gvalid_error.go b/geg/util/gvalid/gvalid_error.go index fa7b61ff1..cf033b191 100644 --- a/geg/util/gvalid/gvalid_error.go +++ b/geg/util/gvalid/gvalid_error.go @@ -1,12 +1,12 @@ package main import ( - "gitee.com/johng/gf/g/util/gutil" + "gitee.com/johng/gf/g" "gitee.com/johng/gf/g/util/gvalid" ) -// same校验 +// 返回结果方法示例 func main() { type User struct { Password string `gvalid:"password@password"` @@ -18,11 +18,12 @@ func main() { ConfiemPassword : "", } - err := gvalid.CheckStruct(user, nil) - gutil.Dump(err) - gutil.Dump(err.String()) - gutil.Dump(err.Strings()) - gutil.Dump(err.FirstItem()) - gutil.Dump(err.FirstRule()) - gutil.Dump(err.FirstString()) + e := gvalid.CheckStruct(user, nil) + g.Dump(e.Map()) + g.Dump(e.Maps()) + g.Dump(e.String()) + g.Dump(e.Strings()) + g.Dump(e.FirstItem()) + g.Dump(e.FirstRule()) + g.Dump(e.FirstString()) } diff --git a/geg/util/gvalid/gvalid_result.go b/geg/util/gvalid/gvalid_result.go index ca456b7d7..931eb5c5e 100644 --- a/geg/util/gvalid/gvalid_result.go +++ b/geg/util/gvalid/gvalid_result.go @@ -1,13 +1,13 @@ package main import ( - "gitee.com/johng/gf/g/util/gutil" + "gitee.com/johng/gf/g" "gitee.com/johng/gf/g/util/gvalid" ) func main() { type User struct { - Name string `gvalid:"name @required|length:6,30#请输入用户名称|用户名称长度非法"` + Name string `gvalid:"name @required|length:6,30#请输入用户名称|用户名称长度不够哦"` Pass1 string `gvalid:"password1@required|password3"` Pass2 string `gvalid:"password2@required|password3|same:password1#||两次密码不一致,请重新输入"` } @@ -18,8 +18,7 @@ func main() { Pass2: "123", } - err := gvalid.CheckStruct(user, nil) - gutil.Dump(err) - gutil.Dump(err.String()) - gutil.Dump(err.FirstString()) + e := gvalid.CheckStruct(user, nil) + g.Dump(e.String()) + g.Dump(e.FirstString()) } diff --git a/geg/util/gvalid/gvalid_struct1.go b/geg/util/gvalid/gvalid_struct1.go index 4d3ead963..b00b75ce8 100644 --- a/geg/util/gvalid/gvalid_struct1.go +++ b/geg/util/gvalid/gvalid_struct1.go @@ -1,12 +1,12 @@ package main import ( - "gitee.com/johng/gf/g/util/gutil" + "gitee.com/johng/gf/g" "gitee.com/johng/gf/g/util/gvalid" ) type User struct { - Uid int `gvalid:"uid @integer|min:1"` + Uid int `gvalid:"uid @integer|min:1#用户UID不能为空"` Name string `gvalid:"name @required|length:6,30#请输入用户名称|用户名称长度非法"` Pass1 string `gvalid:"password1@required|password3"` Pass2 string `gvalid:"password2@required|password3|same:password1#||两次密码不一致,请重新输入"` @@ -20,7 +20,7 @@ func main() { } // 使用结构体定义的校验规则和错误提示进行校验 - gutil.Dump(gvalid.CheckStruct(user, nil)) + g.Dump(gvalid.CheckStruct(user, nil).Map()) // 自定义校验规则和错误提示,对定义的特定校验规则和错误提示进行覆盖 rules := map[string]string { @@ -31,5 +31,5 @@ func main() { "password3" : "名称不能为空", }, } - gutil.Dump(gvalid.CheckStruct(user, rules, msgs)) + g.Dump(gvalid.CheckStruct(user, rules, msgs).Map()) } diff --git a/geg/util/gvalid/gvalid_struct2.go b/geg/util/gvalid/gvalid_struct2.go index d5bf56db6..16ef7c1c8 100644 --- a/geg/util/gvalid/gvalid_struct2.go +++ b/geg/util/gvalid/gvalid_struct2.go @@ -1,7 +1,7 @@ package main import ( - "gitee.com/johng/gf/g/util/gutil" + "gitee.com/johng/gf/g" "gitee.com/johng/gf/g/util/gvalid" ) @@ -14,5 +14,5 @@ func main() { user := &User{} - gutil.Dump(gvalid.CheckStruct(user, nil)) + g.Dump(gvalid.CheckStruct(user, nil)) } diff --git a/geg/util/gvalid/gvalid_struct3.go b/geg/util/gvalid/gvalid_struct3.go index 7ac19f36c..732779384 100644 --- a/geg/util/gvalid/gvalid_struct3.go +++ b/geg/util/gvalid/gvalid_struct3.go @@ -1,7 +1,7 @@ package main import ( - "gitee.com/johng/gf/g/util/gutil" + "gitee.com/johng/gf/g" "gitee.com/johng/gf/g/util/gvalid" ) @@ -18,5 +18,5 @@ func main() { ConfirmPassword : "", } - gutil.Dump(gvalid.CheckStruct(user, nil)) + g.Dump(gvalid.CheckStruct(user, nil)) }