refactor: 后端validator校验错误转译

This commit is contained in:
meilin.huang
2023-07-31 17:34:32 +08:00
parent 2479412334
commit c2ee4f9955
15 changed files with 216 additions and 42 deletions

View File

@@ -23,7 +23,7 @@
"monaco-sql-languages": "^0.11.0", "monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.4", "monaco-themes": "^0.4.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.4", "pinia": "^2.1.6",
"qrcode.vue": "^3.4.0", "qrcode.vue": "^3.4.0",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"sortablejs": "^1.13.0", "sortablejs": "^1.13.0",

View File

@@ -0,0 +1,4 @@
export const AccountUsernamePattern = {
pattern: /^[a-zA-Z0-9_]{5,20}$/g,
message: '只允许输入5-20位大小写字母、数字、下划线',
};

View File

@@ -138,6 +138,7 @@ import { letterAvatar } from '@/common/utils/string';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import QrcodeVue from 'qrcode.vue'; import QrcodeVue from 'qrcode.vue';
import { personApi } from '@/views/personal/api'; import { personApi } from '@/views/personal/api';
import { AccountUsernamePattern } from '@/common/pattern';
const rules = { const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
@@ -205,7 +206,14 @@ const state = reactive({
name: '', name: '',
}, },
rules: { rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{
pattern: AccountUsernamePattern.pattern,
message: AccountUsernamePattern.message,
trigger: ['blur'],
},
],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }], name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
}, },
}, },

View File

@@ -2,11 +2,17 @@
<div class="account-dialog"> <div class="account-dialog">
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="35%" :destroy-on-close="true"> <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="35%" :destroy-on-close="true">
<el-form :model="form" ref="accountForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="accountForm" :rules="rules" label-width="auto">
<el-form-item prop="name" label="姓名:" required> <el-form-item prop="name" label="姓名:">
<el-input v-model.trim="form.name" placeholder="请输入姓名" auto-complete="off"></el-input> <el-input v-model.trim="form.name" placeholder="请输入姓名" auto-complete="off" clearable></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="username" label="用户名:" required> <el-form-item prop="username" label="用户名:">
<el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入账号用户名,密码默认与账号名一致" auto-complete="off"></el-input> <el-input
:disabled="edit"
v-model.trim="form.username"
placeholder="请输入账号用户名,密码默认与账号名一致"
auto-complete="off"
clearable
></el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="edit" prop="password" label="密码:"> <el-form-item v-if="edit" prop="password" label="密码:">
<el-input type="password" v-model.trim="form.password" placeholder="输入密码可修改用户密码" autocomplete="new-password"></el-input> <el-input type="password" v-model.trim="form.password" placeholder="输入密码可修改用户密码" autocomplete="new-password"></el-input>
@@ -27,6 +33,7 @@
import { toRefs, reactive, watch, ref } from 'vue'; import { toRefs, reactive, watch, ref } from 'vue';
import { accountApi } from '../api'; import { accountApi } from '../api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { AccountUsernamePattern } from '@/common/pattern';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -59,6 +66,11 @@ const rules = {
message: '请输入用户名', message: '请输入用户名',
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
{
pattern: AccountUsernamePattern.pattern,
message: AccountUsernamePattern.message,
trigger: ['blur'],
},
], ],
}; };

View File

@@ -1558,10 +1558,10 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
resolved "https://registry.nlark.com/picomatch/download/picomatch-2.3.0.tgz?cache=0&sync_timestamp=1621648246651&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fpicomatch%2Fdownload%2Fpicomatch-2.3.0.tgz" resolved "https://registry.nlark.com/picomatch/download/picomatch-2.3.0.tgz?cache=0&sync_timestamp=1621648246651&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fpicomatch%2Fdownload%2Fpicomatch-2.3.0.tgz"
integrity sha1-8fBh3o9qS/AiiS4tEoI0+5gwKXI= integrity sha1-8fBh3o9qS/AiiS4tEoI0+5gwKXI=
pinia@^2.1.4: pinia@^2.1.6:
version "2.1.4" version "2.1.6"
resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.4.tgz#a642adfe6208e10c36d3dc16184a91064788142a" resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.6.tgz#e88959f14b61c4debd9c42d0c9944e2875cbe0fa"
integrity sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ== integrity sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==
dependencies: dependencies:
"@vue/devtools-api" "^6.5.0" "@vue/devtools-api" "^6.5.0"
vue-demi ">=0.14.5" vue-demi ">=0.14.5"

View File

@@ -1,6 +1,6 @@
package initialize package initialize
import machineInit "mayfly-go/internal/machine/initialize" import machineInit "mayfly-go/internal/machine/init"
func InitOther() { func InitOther() {
machineInit.Init() machineInit.Init()

View File

@@ -1,4 +1,4 @@
package initialize package init
import "mayfly-go/internal/machine/application" import "mayfly-go/internal/machine/application"

View File

@@ -2,14 +2,14 @@ package form
type AccountCreateForm struct { type AccountCreateForm struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required,max=16"`
Username string `json:"username" binding:"required,min=4,max=16"` Username string `json:"username" binding:"pattern=account_username"`
Password string `json:"password"` Password string `json:"password"`
} }
type AccountUpdateForm struct { type AccountUpdateForm struct {
Name string `json:"name" binding:"max=16"` // 姓名 Name string `json:"name" binding:"max=16"` // 姓名
Username string `json:"username" binding:"max=20"` Username string `json:"username" binding:"omitempty,pattern=account_username"`
Password *string `json:"password"` Password *string `json:"password"`
} }

View File

@@ -6,17 +6,19 @@ import (
"mayfly-go/pkg/global" "mayfly-go/pkg/global"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/structx" "mayfly-go/pkg/utils/structx"
"mayfly-go/pkg/validatorx"
"net/http" "net/http"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
) )
// 绑定并校验请求结构体参数 // 绑定并校验请求结构体参数
func BindJsonAndValid[T any](g *gin.Context, data T) T { func BindJsonAndValid[T any](g *gin.Context, data T) T {
if err := g.ShouldBindJSON(data); err != nil { if err := g.ShouldBindJSON(data); err != nil {
panic(biz.NewBizErr(err.Error())) panic(ConvBindValidationError(data, err))
} else { } else {
return data return data
} }
@@ -32,7 +34,7 @@ func BindJsonAndCopyTo[T any](g *gin.Context, form any, toStruct T) T {
// 绑定查询字符串到指定结构体 // 绑定查询字符串到指定结构体
func BindQuery[T any](g *gin.Context, data T) T { func BindQuery[T any](g *gin.Context, data T) T {
if err := g.BindQuery(data); err != nil { if err := g.BindQuery(data); err != nil {
panic(biz.NewBizErr(err.Error())) panic(ConvBindValidationError(data, err))
} else { } else {
return data return data
} }
@@ -41,7 +43,7 @@ func BindQuery[T any](g *gin.Context, data T) T {
// 绑定查询字符串到指定结构体,并将分页信息也返回 // 绑定查询字符串到指定结构体,并将分页信息也返回
func BindQueryAndPage[T any](g *gin.Context, data T) (T, *model.PageParam) { func BindQueryAndPage[T any](g *gin.Context, data T) (T, *model.PageParam) {
if err := g.BindQuery(data); err != nil { if err := g.BindQuery(data); err != nil {
panic(biz.NewBizErr(err.Error())) panic(ConvBindValidationError(data, err))
} else { } else {
return data, GetPageParam(g) return data, GetPageParam(g)
} }
@@ -111,3 +113,11 @@ func ErrorRes(g *gin.Context, err any) {
global.Log.Error(t) global.Log.Error(t)
} }
} }
// 转换参数校验错误为业务异常错误
func ConvBindValidationError(data any, err error) error {
if e, ok := err.(validator.ValidationErrors); ok {
return biz.NewBizErrCode(403, validatorx.Translate2Str(data, e))
}
return err
}

View File

@@ -11,16 +11,15 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var Log = logrus.New()
func Init() { func Init() {
Log.SetFormatter(new(LogFormatter)) logger := logrus.New()
Log.SetReportCaller(true) logger.SetFormatter(new(LogFormatter))
logger.SetReportCaller(true)
logConf := config.Conf.Log logConf := config.Conf.Log
// 如果不存在日志配置信息则默认debug级别 // 如果不存在日志配置信息则默认debug级别
if logConf == nil { if logConf == nil {
Log.SetLevel(logrus.DebugLevel) logger.SetLevel(logrus.DebugLevel)
return return
} }
@@ -30,9 +29,9 @@ func Init() {
if err != nil { if err != nil {
panic(fmt.Sprintf("日志级别不存在: %s", level)) panic(fmt.Sprintf("日志级别不存在: %s", level))
} }
Log.SetLevel(l) logger.SetLevel(l)
} else { } else {
Log.SetLevel(logrus.DebugLevel) logger.SetLevel(logrus.DebugLevel)
} }
if logFile := logConf.File; logFile != nil { if logFile := logConf.File; logFile != nil {
@@ -42,10 +41,10 @@ func Init() {
panic(fmt.Sprintf("创建日志文件失败: %s", err.Error())) panic(fmt.Sprintf("创建日志文件失败: %s", err.Error()))
} }
Log.Out = file logger.Out = file
} }
global.Log = Log global.Log = logger
} }
type LogFormatter struct{} type LogFormatter struct{}

View File

@@ -3,7 +3,7 @@ package req
import ( import (
"fmt" "fmt"
"mayfly-go/pkg/biz" "mayfly-go/pkg/biz"
"mayfly-go/pkg/logger" "mayfly-go/pkg/global"
"mayfly-go/pkg/utils/anyx" "mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/stringx" "mayfly-go/pkg/utils/stringx"
@@ -63,10 +63,10 @@ func LogHandler(rc *Ctx) error {
go saveLog(rc) go saveLog(rc)
} }
if err := rc.Err; err != nil { if err := rc.Err; err != nil {
logger.Log.WithFields(lfs).Error(getErrMsg(rc, err)) global.Log.WithFields(lfs).Error(getErrMsg(rc, err))
return nil return nil
} }
logger.Log.WithFields(lfs).Info(getLogMsg(rc)) global.Log.WithFields(lfs).Info(getLogMsg(rc))
return nil return nil
} }

View File

@@ -13,9 +13,18 @@ import (
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
// 初始化jwt key与expire time等
func InitTokenConfig() { func InitTokenConfig() {
JwtKey = config.Conf.Jwt.Key if ExpTime == 0 {
ExpTime = config.Conf.Jwt.ExpireTime JwtKey = config.Conf.Jwt.Key
ExpTime = config.Conf.Jwt.ExpireTime
// 如果配置文件中的jwt key为空则随机生成字符串
if JwtKey == "" {
JwtKey = stringx.Rand(32)
global.Log.Infof("config.yml未配置jwt.key, 随机生成key为: %s", JwtKey)
}
}
} }
var ( var (
@@ -25,6 +34,8 @@ var (
// 创建用户token // 创建用户token
func CreateToken(userId uint64, username string) string { func CreateToken(userId uint64, username string) string {
InitTokenConfig()
// 带权限创建令牌 // 带权限创建令牌
// 设置有效期过期需要重新登录获取token // 设置有效期过期需要重新登录获取token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
@@ -33,14 +44,9 @@ func CreateToken(userId uint64, username string) string {
"exp": time.Now().Add(time.Minute * time.Duration(ExpTime)).Unix(), "exp": time.Now().Add(time.Minute * time.Duration(ExpTime)).Unix(),
}) })
// 如果配置文件中的jwt key为空则随机生成字符串
if JwtKey == "" {
JwtKey = stringx.Rand(32)
global.Log.Infof("config.yml未配置jwt.key, 随机生成key为: %s", JwtKey)
}
// 使用自定义字符串加密 and get the complete encoded token as a string // 使用自定义字符串加密 and get the complete encoded token as a string
tokenString, err := token.SignedString([]byte(JwtKey)) tokenString, err := token.SignedString([]byte(JwtKey))
biz.ErrIsNil(err, "token创建失败") biz.ErrIsNilAppendErr(err, "token创建失败: %s")
return tokenString return tokenString
} }
@@ -49,6 +55,8 @@ func ParseToken(tokenStr string) (*model.LoginAccount, error) {
if tokenStr == "" { if tokenStr == "" {
return nil, errors.New("token error") return nil, errors.New("token error")
} }
InitTokenConfig()
// Parse token // Parse token
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) { token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) {
return []byte(JwtKey), nil return []byte(JwtKey), nil

View File

@@ -6,7 +6,7 @@ import (
"mayfly-go/pkg/config" "mayfly-go/pkg/config"
"mayfly-go/pkg/global" "mayfly-go/pkg/global"
"mayfly-go/pkg/logger" "mayfly-go/pkg/logger"
"mayfly-go/pkg/req" "mayfly-go/pkg/validatorx"
) )
func RunWebServer() { func RunWebServer() {
@@ -16,9 +16,6 @@ func RunWebServer() {
// 初始化日志配置信息 // 初始化日志配置信息
logger.Init() logger.Init()
// 初始化jwt key与expire time等
req.InitTokenConfig()
// 打印banner // 打印banner
printBanner() printBanner()
@@ -33,6 +30,9 @@ func RunWebServer() {
global.Log.Fatalf("数据库升级失败: %v", err) global.Log.Fatalf("数据库升级失败: %v", err)
} }
// 参数校验器初始化、如错误提示中文转译等
validatorx.Init()
// 初始化其他需要启动时运行的方法 // 初始化其他需要启动时运行的方法
initialize.InitOther() initialize.InitOther()

View File

@@ -0,0 +1,41 @@
package validatorx
import (
"mayfly-go/pkg/global"
"regexp"
"github.com/go-playground/validator/v10"
)
const CustomPatternTagName = "pattern"
var (
regexpMap map[string]*regexp.Regexp
patternErrMsg map[string]string
)
// 注册自定义正则表达式校验规则
func RegisterCustomPatterns() {
// 账号用户名校验
RegisterPattern("account_username", "^[a-zA-Z0-9_]{5,20}$", "只允许输入5-20位大小写字母、数字、下划线")
}
// 注册自定义正则表达式
func RegisterPattern(patternName string, regexpStr string, errMsg string) {
if regexpMap == nil {
regexpMap = make(map[string]*regexp.Regexp, 0)
patternErrMsg = make(map[string]string)
}
regexpMap[patternName] = regexp.MustCompile(regexpStr)
patternErrMsg[patternName] = errMsg
}
func patternValidFunc(f validator.FieldLevel) bool {
reg := regexpMap[f.Param()]
if reg == nil {
global.Log.Warnf("%s的正则校验规则不存在!", f.Param())
return false
}
return reg.MatchString(f.Field().String())
}

View File

@@ -0,0 +1,92 @@
package validatorx
import (
"mayfly-go/pkg/utils/structx"
"reflect"
"strings"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_trans "github.com/go-playground/validator/v10/translations/zh"
)
var (
trans ut.Translator
)
func Init() {
// 获取gin的校验器
validate, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
return
}
// 修改返回字段key的格式
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
// 如果存在校验错误提示消息,则使用字段名,后续需要通过该字段名获取相应错误消息
if _, ok := fld.Tag.Lookup("valid_msg"); ok {
return fld.Name
}
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
// 注册翻译器
zh := zh.New()
uni := ut.New(zh, zh)
trans, _ = uni.GetTranslator("zh")
// 注册翻译器
zh_trans.RegisterDefaultTranslations(validate, trans)
// 注册自定义校验器
validate.RegisterValidation(CustomPatternTagName, patternValidFunc)
// 注册自定义正则校验规则
RegisterCustomPatterns()
}
// Translate 翻译错误信息
func Translate(data any, err error) map[string][]string {
var result = make(map[string][]string)
errors := err.(validator.ValidationErrors)
for _, err := range errors {
fieldName := err.Field()
// 判断该字段是否设置了自定义的错误描述信息,存在则使用自定义错误信息进行提示
if field, ok := structx.IndirectType(reflect.TypeOf(data)).FieldByName(fieldName); ok {
if errMsg, ok := field.Tag.Lookup("valid_msg"); ok {
result[fieldName] = append(result[fieldName], errMsg)
break
}
}
// 如果是自定义正则校验规则,则使用自定义的错误描述信息
if err.Tag() == CustomPatternTagName {
result[fieldName] = append(result[fieldName], fieldName+patternErrMsg[err.Param()])
break
}
result[fieldName] = append(result[fieldName], err.Translate(trans))
}
return result
}
// Translate 翻译错误信息为字符串
func Translate2Str(data any, err error) string {
res := Translate(data, err)
errMsgs := make([]string, 0)
for _, v := range res {
errMsgs = append(errMsgs, v...)
}
return strings.Join(errMsgs, ", ")
}