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-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.4",
"pinia": "^2.1.6",
"qrcode.vue": "^3.4.0",
"screenfull": "^6.0.2",
"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 QrcodeVue from 'qrcode.vue';
import { personApi } from '@/views/personal/api';
import { AccountUsernamePattern } from '@/common/pattern';
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
@@ -205,7 +206,14 @@ const state = reactive({
name: '',
},
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' }],
},
},

View File

@@ -2,11 +2,17 @@
<div class="account-dialog">
<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-item prop="name" label="姓名:" required>
<el-input v-model.trim="form.name" placeholder="请输入姓名" auto-complete="off"></el-input>
<el-form-item prop="name" label="姓名:">
<el-input v-model.trim="form.name" placeholder="请输入姓名" auto-complete="off" clearable></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入账号用户名,密码默认与账号名一致" auto-complete="off"></el-input>
<el-form-item prop="username" label="用户名:">
<el-input
:disabled="edit"
v-model.trim="form.username"
placeholder="请输入账号用户名,密码默认与账号名一致"
auto-complete="off"
clearable
></el-input>
</el-form-item>
<el-form-item v-if="edit" prop="password" label="密码:">
<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 { accountApi } from '../api';
import { ElMessage } from 'element-plus';
import { AccountUsernamePattern } from '@/common/pattern';
const props = defineProps({
visible: {
@@ -59,6 +66,11 @@ const rules = {
message: '请输入用户名',
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"
integrity sha1-8fBh3o9qS/AiiS4tEoI0+5gwKXI=
pinia@^2.1.4:
version "2.1.4"
resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.4.tgz#a642adfe6208e10c36d3dc16184a91064788142a"
integrity sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==
pinia@^2.1.6:
version "2.1.6"
resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.6.tgz#e88959f14b61c4debd9c42d0c9944e2875cbe0fa"
integrity sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==
dependencies:
"@vue/devtools-api" "^6.5.0"
vue-demi ">=0.14.5"

View File

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

View File

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

View File

@@ -2,14 +2,14 @@ package form
type AccountCreateForm struct {
Id uint64 `json:"id"`
Name string `json:"name" binding:"required"`
Username string `json:"username" binding:"required,min=4,max=16"`
Name string `json:"name" binding:"required,max=16"`
Username string `json:"username" binding:"pattern=account_username"`
Password string `json:"password"`
}
type AccountUpdateForm struct {
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"`
}

View File

@@ -6,17 +6,19 @@ import (
"mayfly-go/pkg/global"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/structx"
"mayfly-go/pkg/validatorx"
"net/http"
"runtime/debug"
"strconv"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// 绑定并校验请求结构体参数
func BindJsonAndValid[T any](g *gin.Context, data T) T {
if err := g.ShouldBindJSON(data); err != nil {
panic(biz.NewBizErr(err.Error()))
panic(ConvBindValidationError(data, err))
} else {
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 {
if err := g.BindQuery(data); err != nil {
panic(biz.NewBizErr(err.Error()))
panic(ConvBindValidationError(data, err))
} else {
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) {
if err := g.BindQuery(data); err != nil {
panic(biz.NewBizErr(err.Error()))
panic(ConvBindValidationError(data, err))
} else {
return data, GetPageParam(g)
}
@@ -111,3 +113,11 @@ func ErrorRes(g *gin.Context, err any) {
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"
)
var Log = logrus.New()
func Init() {
Log.SetFormatter(new(LogFormatter))
Log.SetReportCaller(true)
logger := logrus.New()
logger.SetFormatter(new(LogFormatter))
logger.SetReportCaller(true)
logConf := config.Conf.Log
// 如果不存在日志配置信息则默认debug级别
if logConf == nil {
Log.SetLevel(logrus.DebugLevel)
logger.SetLevel(logrus.DebugLevel)
return
}
@@ -30,9 +29,9 @@ func Init() {
if err != nil {
panic(fmt.Sprintf("日志级别不存在: %s", level))
}
Log.SetLevel(l)
logger.SetLevel(l)
} else {
Log.SetLevel(logrus.DebugLevel)
logger.SetLevel(logrus.DebugLevel)
}
if logFile := logConf.File; logFile != nil {
@@ -42,10 +41,10 @@ func Init() {
panic(fmt.Sprintf("创建日志文件失败: %s", err.Error()))
}
Log.Out = file
logger.Out = file
}
global.Log = Log
global.Log = logger
}
type LogFormatter struct{}

View File

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

View File

@@ -13,9 +13,18 @@ import (
"github.com/golang-jwt/jwt/v5"
)
// 初始化jwt key与expire time等
func InitTokenConfig() {
JwtKey = config.Conf.Jwt.Key
ExpTime = config.Conf.Jwt.ExpireTime
if ExpTime == 0 {
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 (
@@ -25,6 +34,8 @@ var (
// 创建用户token
func CreateToken(userId uint64, username string) string {
InitTokenConfig()
// 带权限创建令牌
// 设置有效期过期需要重新登录获取token
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(),
})
// 如果配置文件中的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
tokenString, err := token.SignedString([]byte(JwtKey))
biz.ErrIsNil(err, "token创建失败")
biz.ErrIsNilAppendErr(err, "token创建失败: %s")
return tokenString
}
@@ -49,6 +55,8 @@ func ParseToken(tokenStr string) (*model.LoginAccount, error) {
if tokenStr == "" {
return nil, errors.New("token error")
}
InitTokenConfig()
// Parse token
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) {
return []byte(JwtKey), nil

View File

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