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