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

545 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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