545 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			545 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # 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 校验器接口定义
 | ||
| 
 | ||
| ```go
 | ||
| // 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 格式校验器实现
 | ||
| 
 | ||
| ```go
 | ||
| // 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 自定义校验规则
 | ||
| 
 | ||
| ```go
 | ||
| // 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 校验中间件
 | ||
| 
 | ||
| ```go
 | ||
| // 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 业务校验器基类
 | ||
| 
 | ||
| ```go
 | ||
| // 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 具体业务校验器
 | ||
| 
 | ||
| ```go
 | ||
| // 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 中使用格式校验
 | ||
| 
 | ||
| ```go
 | ||
| // 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 中使用业务校验
 | ||
| 
 | ||
| ```go
 | ||
| // 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 中初始化
 | ||
| 
 | ||
| ```go
 | ||
| // client/client.go
 | ||
| func main() {
 | ||
|     // 初始化格式校验器
 | ||
|     if err := validator.Init(); err != nil {
 | ||
|         log.Fatalf("初始化校验器失败: %v", err)
 | ||
|     }
 | ||
| 
 | ||
|     // 其他初始化代码...
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| ## 5. 封装的优势
 | ||
| 
 | ||
| 1. **统一的接口**:所有校验都通过统一的接口调用
 | ||
| 2. **错误处理标准化**:统一的错误格式和消息
 | ||
| 3. **可扩展性**:容易添加新的校验规则
 | ||
| 4. **代码复用**:通用校验逻辑可以复用
 | ||
| 5. **测试友好**:校验器可以独立测试
 | ||
| 6. **配置化**:错误消息和校验规则可以配置化管理
 | ||
| 
 | ||
| 这样的封装既保持了格式校验和业务校验的清晰边界,又提供了便捷的使用接口。
 |