mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
refactor: 后端validator校验错误转译
This commit is contained in:
@@ -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",
|
||||
|
||||
4
mayfly_go_web/src/common/pattern.ts
Normal file
4
mayfly_go_web/src/common/pattern.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const AccountUsernamePattern = {
|
||||
pattern: /^[a-zA-Z0-9_]{5,20}$/g,
|
||||
message: '只允许输入5-20位大小写字母、数字、下划线',
|
||||
};
|
||||
@@ -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' }],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package initialize
|
||||
|
||||
import machineInit "mayfly-go/internal/machine/initialize"
|
||||
import machineInit "mayfly-go/internal/machine/init"
|
||||
|
||||
func InitOther() {
|
||||
machineInit.Init()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package initialize
|
||||
package init
|
||||
|
||||
import "mayfly-go/internal/machine/application"
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
41
server/pkg/validatorx/pattern.go
Normal file
41
server/pkg/validatorx/pattern.go
Normal 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())
|
||||
}
|
||||
92
server/pkg/validatorx/validatorx.go
Normal file
92
server/pkg/validatorx/validatorx.go
Normal 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, ", ")
|
||||
}
|
||||
Reference in New Issue
Block a user