14 KiB
14 KiB
Validator 封装设计
1. 整体架构设计
shared/
├── validator/ # 格式校验器封装
│ ├── validator.go # 校验器初始化和接口定义
│ ├── format_validator.go # 格式校验实现
│ ├── custom_rules.go # 自定义校验规则
│ ├── messages.go # 错误消息配置
│ └── middleware.go # 校验中间件
├── errcode/
│ ├── validator_errors.go # 校验相关错误码
└── response/
└── validator_response.go # 校验错误响应格式
domains/
└── product/rpc/internal/logic/
└── validator/ # 业务校验器封装
├── base.go # 业务校验器基类
├── product_validator.go
└── category_validator.go
2. 格式校验器封装(shared/validator)
2.1 校验器接口定义
// shared/validator/validator.go
package validator
import (
"reflect"
"github.com/go-playground/validator/v10"
"tianyuan/shared/errcode"
)
// 校验器接口
type IValidator interface {
Validate(data interface{}) error
ValidateStruct(data interface{}) error
AddCustomRule(tag string, fn validator.Func) error
}
// 全局校验器实例
var GlobalValidator IValidator
// 初始化校验器
func Init() error {
v := &FormatValidator{
validator: validator.New(),
}
// 注册自定义规则
if err := v.registerCustomRules(); err != nil {
return err
}
// 注册中文错误消息
if err := v.registerMessages(); err != nil {
return err
}
GlobalValidator = v
return nil
}
// 便捷函数
func Validate(data interface{}) error {
return GlobalValidator.Validate(data)
}
func ValidateStruct(data interface{}) error {
return GlobalValidator.ValidateStruct(data)
}
2.2 格式校验器实现
// shared/validator/format_validator.go
package validator
import (
"fmt"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
"tianyuan/shared/errcode"
)
type FormatValidator struct {
validator *validator.Validate
}
// 校验接口实现
func (v *FormatValidator) Validate(data interface{}) error {
if err := v.validator.Struct(data); err != nil {
return v.formatError(err)
}
return nil
}
func (v *FormatValidator) ValidateStruct(data interface{}) error {
return v.Validate(data)
}
func (v *FormatValidator) AddCustomRule(tag string, fn validator.Func) error {
return v.validator.RegisterValidation(tag, fn)
}
// 错误格式化
func (v *FormatValidator) formatError(err error) error {
if validationErrors, ok := err.(validator.ValidationErrors); ok {
var errMsgs []string
for _, fieldError := range validationErrors {
errMsg := v.getErrorMessage(fieldError)
errMsgs = append(errMsgs, errMsg)
}
return errcode.NewValidationError(strings.Join(errMsgs, "; "))
}
return err
}
// 获取错误消息
func (v *FormatValidator) getErrorMessage(fieldError validator.FieldError) string {
// 获取字段的中文名称
fieldName := v.getFieldName(fieldError)
// 根据校验标签获取错误消息
switch fieldError.Tag() {
case "required":
return fmt.Sprintf("%s不能为空", fieldName)
case "min":
return fmt.Sprintf("%s最小值为%s", fieldName, fieldError.Param())
case "max":
return fmt.Sprintf("%s最大值为%s", fieldName, fieldError.Param())
case "email":
return fmt.Sprintf("%s格式不正确", fieldName)
case "mobile":
return fmt.Sprintf("%s格式不正确", fieldName)
default:
return fmt.Sprintf("%s校验失败", fieldName)
}
}
// 获取字段中文名称
func (v *FormatValidator) getFieldName(fieldError validator.FieldError) string {
// 可以通过反射获取struct tag中的中文名称
// 或者维护一个字段名映射表
fieldName := fieldError.Field()
// 简单示例,实际可以更复杂
nameMap := map[string]string{
"CategoryId": "分类ID",
"PageNum": "页码",
"PageSize": "每页数量",
"Keyword": "关键词",
"Mobile": "手机号",
"Email": "邮箱",
}
if chineseName, exists := nameMap[fieldName]; exists {
return chineseName
}
return fieldName
}
2.3 自定义校验规则
// shared/validator/custom_rules.go
package validator
import (
"regexp"
"github.com/go-playground/validator/v10"
)
// 注册自定义校验规则
func (v *FormatValidator) registerCustomRules() error {
// 手机号校验
if err := v.validator.RegisterValidation("mobile", validateMobile); err != nil {
return err
}
// 身份证号校验
if err := v.validator.RegisterValidation("idcard", validateIDCard); err != nil {
return err
}
// 企业统一社会信用代码校验
if err := v.validator.RegisterValidation("creditcode", validateCreditCode); err != nil {
return err
}
return nil
}
// 手机号校验函数
func validateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
pattern := `^1[3-9]\d{9}$`
matched, _ := regexp.MatchString(pattern, mobile)
return matched
}
// 身份证号校验函数
func validateIDCard(fl validator.FieldLevel) bool {
idcard := fl.Field().String()
pattern := `^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$`
matched, _ := regexp.MatchString(pattern, idcard)
return matched
}
// 企业统一社会信用代码校验函数
func validateCreditCode(fl validator.FieldLevel) bool {
code := fl.Field().String()
pattern := `^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$`
matched, _ := regexp.MatchString(pattern, code)
return matched
}
2.4 校验中间件
// shared/validator/middleware.go
package validator
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"tianyuan/shared/response"
)
// 校验中间件
func ValidationMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 这里可以添加全局校验逻辑
// 比如请求头校验、通用参数校验等
next.ServeHTTP(w, r)
})
}
}
// Handler校验辅助函数
func ValidateAndParse(r *http.Request, req interface{}) error {
// 1. 参数绑定
if err := httpx.Parse(r, req); err != nil {
return err
}
// 2. 格式校验
if err := Validate(req); err != nil {
return err
}
return nil
}
3. 业务校验器封装(各域内部)
3.1 业务校验器基类
// domains/product/rpc/internal/logic/validator/base.go
package validator
import (
"context"
"tianyuan/domains/product/rpc/internal/svc"
"tianyuan/shared/errcode"
)
// 业务校验器基类
type BaseValidator struct {
svcCtx *svc.ServiceContext
ctx context.Context
}
func NewBaseValidator(ctx context.Context, svcCtx *svc.ServiceContext) *BaseValidator {
return &BaseValidator{
svcCtx: svcCtx,
ctx: ctx,
}
}
// 业务校验接口
type IBusinessValidator interface {
ValidatePermission(userId int64, action string) error
ValidateResourceExists(resourceType string, resourceId int64) error
ValidateBusinessRules(data interface{}) error
}
// 通用业务校验方法
func (v *BaseValidator) ValidatePermission(userId int64, action string) error {
// 调用用户域RPC检查权限
resp, err := v.svcCtx.UserRpc.CheckPermission(v.ctx, &user.CheckPermissionReq{
UserId: userId,
Permission: action,
})
if err != nil {
return err
}
if !resp.HasPermission {
return errcode.NewBusinessError("用户无权限执行此操作")
}
return nil
}
func (v *BaseValidator) ValidateResourceExists(resourceType string, resourceId int64) error {
// 根据资源类型检查资源是否存在
switch resourceType {
case "category":
return v.validateCategoryExists(resourceId)
case "product":
return v.validateProductExists(resourceId)
default:
return errcode.NewBusinessError("未知的资源类型")
}
}
func (v *BaseValidator) validateCategoryExists(categoryId int64) error {
category, err := v.svcCtx.CategoryModel.FindOne(v.ctx, categoryId)
if err != nil {
return errcode.NewBusinessError("分类不存在")
}
if category.Status != 1 {
return errcode.NewBusinessError("分类已禁用")
}
return nil
}
func (v *BaseValidator) validateProductExists(productId int64) error {
product, err := v.svcCtx.ProductModel.FindOne(v.ctx, productId)
if err != nil {
return errcode.NewBusinessError("产品不存在")
}
if product.Status != 1 {
return errcode.NewBusinessError("产品已下架")
}
return nil
}
3.2 具体业务校验器
// domains/product/rpc/internal/logic/validator/product_validator.go
package validator
import (
"context"
"tianyuan/domains/product/rpc/internal/svc"
"tianyuan/domains/product/rpc/product"
"tianyuan/shared/errcode"
)
type ProductValidator struct {
*BaseValidator
}
func NewProductValidator(ctx context.Context, svcCtx *svc.ServiceContext) *ProductValidator {
return &ProductValidator{
BaseValidator: NewBaseValidator(ctx, svcCtx),
}
}
// 校验获取产品列表请求
func (v *ProductValidator) ValidateGetProductListRequest(req *product.GetProductListReq) error {
// 1. 校验用户权限
if err := v.ValidatePermission(req.UserId, "product:list"); err != nil {
return err
}
// 2. 校验分类存在性
if req.CategoryId > 0 {
if err := v.ValidateResourceExists("category", req.CategoryId); err != nil {
return err
}
}
// 3. 校验用户是否有查看该分类的权限
if req.CategoryId > 0 {
if err := v.validateCategoryAccess(req.UserId, req.CategoryId); err != nil {
return err
}
}
return nil
}
// 校验创建产品请求
func (v *ProductValidator) ValidateCreateProductRequest(req *product.CreateProductReq) error {
// 1. 校验用户权限
if err := v.ValidatePermission(req.UserId, "product:create"); err != nil {
return err
}
// 2. 校验分类存在性
if err := v.ValidateResourceExists("category", req.CategoryId); err != nil {
return err
}
// 3. 校验产品名称是否重复
if err := v.validateProductNameUnique(req.Name, req.CategoryId); err != nil {
return err
}
return nil
}
// 校验分类访问权限
func (v *ProductValidator) validateCategoryAccess(userId, categoryId int64) error {
// 检查用户是否有访问该分类的权限
// 这里可能涉及到用户等级、VIP权限等业务逻辑
userInfo, err := v.svcCtx.UserRpc.GetUserInfo(v.ctx, &user.GetUserInfoReq{
UserId: userId,
})
if err != nil {
return err
}
// 示例:VIP用户可以访问所有分类,普通用户只能访问基础分类
category, err := v.svcCtx.CategoryModel.FindOne(v.ctx, categoryId)
if err != nil {
return err
}
if category.RequireVip && userInfo.UserType != "vip" {
return errcode.NewBusinessError("该分类需要VIP权限")
}
return nil
}
// 校验产品名称唯一性
func (v *ProductValidator) validateProductNameUnique(name string, categoryId int64) error {
exists, err := v.svcCtx.ProductModel.CheckNameExists(v.ctx, name, categoryId)
if err != nil {
return err
}
if exists {
return errcode.NewBusinessError("同分类下产品名称已存在")
}
return nil
}
4. 使用示例
4.1 在 Handler 中使用格式校验
// client/internal/handler/product/getproductlisthandler.go
func (h *GetProductListHandler) GetProductList(w http.ResponseWriter, r *http.Request) {
var req types.GetProductListReq
// 使用封装的校验函数
if err := validator.ValidateAndParse(r, &req); err != nil {
response.ParamErrorResponse(w, err)
return
}
// 调用Logic层
resp, err := h.logic.GetProductList(&req)
if err != nil {
response.ErrorResponse(w, err)
return
}
response.SuccessResponse(w, resp)
}
4.2 在 RPC Logic 中使用业务校验
// domains/product/rpc/internal/logic/getproductlistlogic.go
func (l *GetProductListLogic) GetProductList(req *product.GetProductListReq) (*product.GetProductListResp, error) {
// 创建业务校验器
validator := validator.NewProductValidator(l.ctx, l.svcCtx)
// 执行业务校验
if err := validator.ValidateGetProductListRequest(req); err != nil {
return nil, err
}
// 执行业务逻辑
products, total, err := l.svcCtx.ProductModel.FindList(l.ctx, req)
if err != nil {
return nil, err
}
return &product.GetProductListResp{
List: products,
Total: total,
}, nil
}
4.3 在 main.go 中初始化
// client/client.go
func main() {
// 初始化格式校验器
if err := validator.Init(); err != nil {
log.Fatalf("初始化校验器失败: %v", err)
}
// 其他初始化代码...
}
5. 封装的优势
- 统一的接口:所有校验都通过统一的接口调用
- 错误处理标准化:统一的错误格式和消息
- 可扩展性:容易添加新的校验规则
- 代码复用:通用校验逻辑可以复用
- 测试友好:校验器可以独立测试
- 配置化:错误消息和校验规则可以配置化管理
这样的封装既保持了格式校验和业务校验的清晰边界,又提供了便捷的使用接口。