Files
juwan-backend/common/converter/README.md
T
2026-02-27 19:17:01 +08:00

261 lines
6.5 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.
# Converter - 通用结构体转换工具
利用 Go 反射机制,实现自动的 model 到 protobuf 结构体转换。
## 功能特性
**自动字段映射** - 自动匹配同名字段并赋值
**智能类型转换** - 自动处理常见类型转换
**通用设计** - 支持任何 model 和 pb 结构体,无需手动编写
**灵活扩展** - 支持自定义类型转换规则
## 支持的类型转换
| 源类型 | 目标类型 | 说明 |
|-------|---------|------|
| `time.Time` | `int64` | 转换为 Unix 时间戳 |
| `sql.NullTime` | `int64` | 有效时自动转换,无效则为 0 |
| `sql.NullTime` | `time.Time` | 有效时自动转换,无效则为零值 |
| `sql.NullInt64` | `int64` | 有效时自动转换,无效则为 0 |
| `sql.NullString` | `string` | 有效时自动转换,无效则为空字符串 |
| `sql.NullBool` | `bool` | 有效时自动转换,无效则为 false |
| `int` | `int64` | 自动转换 |
| `int64` | `int` | 自动转换 |
| 相同类型 | 相同类型 | 直接复制 |
## 核心函数
### 1. StructToStruct - 单个结构体转换
```go
func StructToStruct(src, dst interface{}) error
```
**参数:**
- `src` - 源结构体(可以是指针或值类型)
- `dst` - 目标结构体(必须是指针)
**示例:**
```go
import "app/common/converter"
// 单个 models 转 pb
user, _ := m.FindOne(ctx, userId)
pbUser := &pb.Users{}
converter.StructToStruct(user, pbUser)
// 或直接点对点转换
pbUser := &pb.Users{}
_ = converter.StructToStruct(user, pbUser)
```
### 2. SliceToSlice - 切片转换
```go
func SliceToSlice(src interface{}, dstSliceType interface{}) (interface{}, error)
```
**参数:**
- `src` - 源切片
- `dstSliceType` - 目标切片类型(用于推导元素类型)
**示例:**
```go
// 多个 models 转 pb
users := []*models.Users{user1, user2, user3}
pbUsersIface, _ := converter.SliceToSlice(users, []*pb.Users{})
pbUsers := pbUsersIface.([]*pb.Users)
```
### 3. UserModelToPb - Users 专用转换(推荐)
```go
func UserModelToPb(user *models.Users) *pb.Users
```
简化的 Users model 转 pb 的快捷函数。
**示例:**
```go
user, _ := m.FindOne(ctx, userId)
pbUser := converter.UserModelToPb(user)
```
### 4. UserModelsToPb - Users 批量转换(推荐)
```go
func UserModelsToPb(users []*models.Users) []*pb.Users
```
简化的批量转换快捷函数。
**示例:**
```go
users, _ := m.FindAll(ctx)
pbUsers := converter.UserModelsToPb(users)
```
## 使用场景
### 场景 1:在 Logic 层直接转换
```go
package logic
import (
"context"
"app/common/converter"
"app/users/rpc/internal/models"
"app/users/rpc/pb"
)
type GetUserByIdLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
}
func (l *GetUserByIdLogic) GetUserById(req *pb.GetUserByIdReq) (*pb.Users, error) {
// 查询数据库
user, err := l.svcCtx.UsersModel.FindOne(l.ctx, req.UserId)
if err != nil {
return nil, err
}
// 直接转换,无需手动赋值每个字段
pbUser := converter.UserModelToPb(user)
return pbUser, nil
}
```
### 场景 2:批量操作
```go
func (l *ListUsersLogic) ListUsers(req *pb.ListUsersReq) (*pb.ListUsersResp, error) {
users, err := l.svcCtx.UsersModel.FindAll(l.ctx)
if err != nil {
return nil, err
}
// 批量转换
pbUsers := converter.UserModelsToPb(users)
return &pb.ListUsersResp{
Users: pbUsers,
}, nil
}
```
### 场景 3:搜索/过滤结果
```go
func (l *SearchUsersLogic) SearchUsers(req *pb.SearchReq) (*pb.SearchResp, error) {
// 搜索数据库
results, err := l.svcCtx.UsersModel.SearchByKeyword(l.ctx, req.Keyword)
if err != nil {
return nil, err
}
pbUsers := converter.UserModelsToPb(results)
return &pb.SearchResp{
Results: pbUsers,
}, nil
}
```
## 处理特殊字段
### NULLable 字段
当源字段是 `sql.NullTime` 或其他 `sql.Null*` 类型时,转换器会自动处理:
```go
// sql.NullTime -> int64(有效情况)
user.DeletedAt = sql.NullTime{
Time: time.Now(),
Valid: true,
}
// 转换后 pb.Users.DeletedAt 会包含 Unix 时间戳
// sql.NullTime -> int64(无效情况)
user.DeletedAt = sql.NullTime{
Valid: false,
}
// 转换后 pb.Users.DeletedAt 为 0
```
### 时间戳字段
数据库中的 `time.Time` 字段会自动转换为 protobuf 中的 `int64` Unix 时间戳:
```go
// Model
type Users struct {
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt sql.NullTime `db:"deleted_at"`
}
// Protobuf
type Users struct {
CreatedAt int64 // 自动转换为 Unix 时间戳
UpdatedAt int64 // 自动转换为 Unix 时间戳
DeletedAt int64 // 有效时转换,无效时为 0
}
```
## 扩展 - 添加自定义类型转换
如果需要支持新的类型转换,可以在 `generic.go``assignValue` 函数中添加:
```go
// 处理自定义类型 MyType -> int32 的转换
if srcType == reflect.TypeOf(MyType{}) && dstType.Kind() == reflect.Int32 {
mt := srcField.Interface().(MyType)
dstField.SetInt(int64(mt.SomeIntField))
return nil
}
```
## 性能考虑
- 反射操作相对于直接赋值会有性能开销(通常很小)
- 如果需要转换大量数据(>10000 条),考虑性能测试
- 对于热点代码路径,可以写针对性的转换函数
## 错误处理
```go
err := converter.StructToStruct(src, dst)
if err != nil {
log.Printf("转换失败: %v", err)
// 处理错误
}
```
大多数字段级别的转换错误会被忽略(自动跳过),但结构化错误(如 dst 不是指针)会返回。
## 常见问题
**Q: 字段名必须完全相同吗?**
A: 是的,转换器通过反射按字段名匹配。如果 model 字段名是 `UserId`pb 字段也必须是 `UserId`
**Q: 如果某个字段转换失败怎么办?**
A: 单个字段的转换失败会被忽略,继续处理其他字段。确保其他字段正确设置。
**Q: 能否自定义字段映射规则(比如 `db_name` -> `pbName`)?**
A: 当前不支持。如果需要,应该在 protobuf 定义中使用与 model 相同的字段名。
**Q: 转换速度快吗?**
A: 反射会有性能开销,但对于大多数应用场景是可接受的。如果有极端性能要求,可以手写转换函数。
## 相关文件
- `generic.go` - 通用转换函数核心实现
- `user_converter.go` - Users model 专用转换函数(示例)