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. **配置化**:错误消息和校验规则可以配置化管理
|
|||
|
|
|||
|
这样的封装既保持了格式校验和业务校验的清晰边界,又提供了便捷的使用接口。
|