tianyuan-api-server/validator封装设计.md
2025-07-13 20:37:12 +08:00

14 KiB
Raw Blame History

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. 封装的优势

  1. 统一的接口:所有校验都通过统一的接口调用
  2. 错误处理标准化:统一的错误格式和消息
  3. 可扩展性:容易添加新的校验规则
  4. 代码复用:通用校验逻辑可以复用
  5. 测试友好:校验器可以独立测试
  6. 配置化:错误消息和校验规则可以配置化管理

这样的封装既保持了格式校验和业务校验的清晰边界,又提供了便捷的使用接口。