230 lines
6.8 KiB
Markdown
230 lines
6.8 KiB
Markdown
# 校验体系设计优化
|
||
|
||
## 1. 校验层次重新设计
|
||
|
||
### Handler 层(client-api)
|
||
|
||
```go
|
||
// 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)
|
||
|
||
```go
|
||
// 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 层的作用
|
||
|
||
```go
|
||
// 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
|
||
```
|
||
|
||
### 业务校验器实现
|
||
|
||
```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. **类型转换**:自动进行基础类型转换
|
||
|
||
```go
|
||
// 示例: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 自动绑定
|