tianyuan-api-server/校验体系设计优化.md
2025-07-13 20:37:12 +08:00

6.8 KiB
Raw Blame History

校验体系设计优化

1. 校验层次重新设计

Handler 层client-api

// types.go - 只做格式校验
type GetProductListReq struct {
    CategoryId int64  `form:"category_id" validate:"min=1"`           // 格式校验必须大于0
    PageNum    int64  `form:"page_num" validate:"min=1,max=1000"`     // 格式校验:分页范围
    PageSize   int64  `form:"page_size" validate:"min=1,max=100"`     // 格式校验:每页数量
    Keyword    string `form:"keyword" validate:"max=50"`              // 格式校验:关键词长度
}

// handler.go - 只做参数绑定和格式校验
func (l *GetProductListHandler) GetProductList(w http.ResponseWriter, r *http.Request) {
    var req types.GetProductListReq

    // 1. 参数绑定
    if err := httpx.Parse(r, &req); err != nil {
        response.ParamErrorResponse(w, err)
        return
    }

    // 2. 格式校验(只校验格式,不校验业务)
    if err := validator.Validate(&req); err != nil {
        response.ParamErrorResponse(w, err)
        return
    }

    // 3. 调用Logic层
    resp, err := l.svcCtx.GetProductListLogic(l.ctx, &req)
    // ...
}

RPC 层product-rpc

// product.proto - RPC接口定义
message GetProductListReq {
    int64 category_id = 1;
    int64 page_num = 2;
    int64 page_size = 3;
    string keyword = 4;
    int64 user_id = 5;        // 业务相关用户ID
    string user_role = 6;     // 业务相关:用户角色
}

// logic.go - 业务校验
func (l *GetProductListLogic) GetProductList(req *product.GetProductListReq) (*product.GetProductListResp, error) {
    // 1. 业务校验(在相关业务域内)
    if err := l.validateBusinessRules(req); err != nil {
        return nil, err
    }

    // 2. 业务逻辑处理
    // ...
}

func (l *GetProductListLogic) validateBusinessRules(req *product.GetProductListReq) error {
    // 业务规则校验:用户是否有权限查看该分类
    if !l.svcCtx.UserModel.HasCategoryPermission(req.UserId, req.CategoryId) {
        return errors.New("用户无权限查看该分类")
    }

    // 业务规则校验:分类是否存在且启用
    category, err := l.svcCtx.CategoryModel.FindOne(req.CategoryId)
    if err != nil || category.Status != 1 {
        return errors.New("分类不存在或已禁用")
    }

    return nil
}

2. 参数传递和转换

Logic 层的作用

// client/internal/logic/product/getproductlistlogic.go
func (l *GetProductListLogic) GetProductList(req *types.GetProductListReq) (*types.GetProductListResp, error) {
    // 1. 获取用户信息从JWT中解析
    userInfo := l.ctx.Value("userInfo").(auth.UserInfo)

    // 2. 转换为RPC请求添加业务上下文
    rpcReq := &product.GetProductListReq{
        CategoryId: req.CategoryId,
        PageNum:    req.PageNum,
        PageSize:   req.PageSize,
        Keyword:    req.Keyword,
        UserId:     userInfo.UserId,      // 添加业务上下文
        UserRole:   userInfo.Role,        // 添加业务上下文
    }

    // 3. 调用RPC服务
    rpcResp, err := l.svcCtx.ProductRpc.GetProductList(l.ctx, rpcReq)
    if err != nil {
        return nil, err
    }

    // 4. 转换响应格式
    return &types.GetProductListResp{
        List:  convertToApiFormat(rpcResp.List),
        Total: rpcResp.Total,
    }, nil
}

3. 校验规则组织优化

目录结构调整

shared/
├── validator/           # 通用格式校验器
│   ├── validator.go     # 校验器初始化
│   ├── rules.go         # 通用校验规则
│   └── messages.go      # 错误消息
├── errcode/            # 错误码定义
│   ├── api_errors.go   # API层错误
│   └── business_errors.go # 业务层错误
└── utils/              # 工具函数
    └── converter.go    # 数据转换

domains/
├── product/
│   ├── rpc/
│   │   └── internal/
│   │       └── logic/
│   │           └── validator/    # 产品域业务校验
│   │               ├── product_validator.go
│   │               └── category_validator.go
├── user/
│   └── rpc/
│       └── internal/
│           └── logic/
│               └── validator/    # 用户域业务校验
│                   ├── user_validator.go
│                   └── auth_validator.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"
)

type ProductValidator struct {
    svcCtx *svc.ServiceContext
}

func NewProductValidator(svcCtx *svc.ServiceContext) *ProductValidator {
    return &ProductValidator{
        svcCtx: svcCtx,
    }
}

// 校验用户是否有权限查看产品列表
func (v *ProductValidator) ValidateGetProductListPermission(ctx context.Context, req *product.GetProductListReq) error {
    // 检查用户权限
    hasPermission, err := v.svcCtx.UserRpc.CheckPermission(ctx, &user.CheckPermissionReq{
        UserId:     req.UserId,
        Permission: "product:list",
    })
    if err != nil {
        return err
    }

    if !hasPermission.HasPermission {
        return errors.New("用户无权限查看产品列表")
    }

    return nil
}

// 校验分类相关业务规则
func (v *ProductValidator) ValidateCategoryAccess(ctx context.Context, req *product.GetProductListReq) error {
    // 检查分类是否存在且用户有访问权限
    // ...
    return nil
}

4. 关于 httpx.Parse 的说明

httpx.Parse 是 go-zero 提供的参数绑定函数,它会:

  1. 自动绑定参数:根据 struct tag 从 HTTP 请求中解析参数
  2. 支持多种来源query 参数、form 数据、JSON body 等
  3. 类型转换:自动进行基础类型转换
// 示例httpx.Parse 的使用
type GetProductListReq struct {
    CategoryId int64  `form:"category_id"`  // 从form或query中获取
    PageNum    int64  `form:"page_num"`
    PageSize   int64  `form:"page_size"`
    Keyword    string `form:"keyword"`
}

func Handler(w http.ResponseWriter, r *http.Request) {
    var req GetProductListReq

    // 这一行会自动从 r *http.Request 中解析参数到 req 结构体
    if err := httpx.Parse(r, &req); err != nil {
        // 参数解析失败
        return
    }

    // 此时 req 已经包含了解析后的参数值
}

5. 最佳实践总结

  1. Handler 层:只做格式校验,使用 validator tag
  2. Logic 层:参数转换和聚合,添加业务上下文
  3. RPC 层:业务规则校验,放在对应的业务域内
  4. 校验器分离:格式校验器放 shared业务校验器放各自域内
  5. 参数绑定:使用 go-zero 的 httpx.Parse 自动绑定