add
This commit is contained in:
544
validator封装设计.md
Normal file
544
validator封装设计.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# 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. **配置化**:错误消息和校验规则可以配置化管理
|
||||
|
||||
这样的封装既保持了格式校验和业务校验的清晰边界,又提供了便捷的使用接口。
|
||||
Reference in New Issue
Block a user