normalize line endings to LF and add envoy dockerfile in deploy/dev
This commit is contained in:
+260
-260
@@ -1,260 +1,260 @@
|
||||
# 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 专用转换函数(示例)
|
||||
# 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 专用转换函数(示例)
|
||||
|
||||
Reference in New Issue
Block a user