This commit is contained in:
wwweww
2026-02-23 20:36:21 +08:00
parent 4898aecd3b
commit fdbcde13b2
52 changed files with 11263 additions and 194 deletions
+260
View File
@@ -0,0 +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"
// 单个 model 转 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
// 多个 model 转 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 专用转换函数(示例)
+207
View File
@@ -0,0 +1,207 @@
package converter
import (
"database/sql"
"reflect"
"time"
)
// StructToStruct 通用结构体转换函数,利用反射将源结构体的字段值复制到目标结构体
// src: 源结构体(通常是 model)
// dst: 目标结构体(通常是 pb),必须是指针
// 支持的自动转换:
// - time.Time -> int64 (Unix 时间戳)
// - sql.NullTime -> int64 (如果有效)
// - sql.NullInt64 -> int64
// - sql.NullString -> string
// - 相同名称和兼容类型的字段
func StructToStruct(src, dst interface{}) error {
if src == nil {
return nil
}
srcVal := reflect.ValueOf(src)
dstVal := reflect.ValueOf(dst)
// 确保 dst 是指针
if dstVal.Kind() != reflect.Ptr {
return newError("destination must be a pointer")
}
dstVal = dstVal.Elem()
// 如果 src 是指针,解引用
if srcVal.Kind() == reflect.Ptr {
srcVal = srcVal.Elem()
}
// 都必须是结构体
if srcVal.Kind() != reflect.Struct || dstVal.Kind() != reflect.Struct {
return newError("both source and destination must be structs")
}
srcType := srcVal.Type()
// 遍历源结构体的所有字段
for i := 0; i < srcVal.NumField(); i++ {
srcField := srcVal.Field(i)
srcFieldName := srcType.Field(i).Name
// 在目标结构体中查找同名字段
dstField := dstVal.FieldByName(srcFieldName)
if !dstField.IsValid() || !dstField.CanSet() {
continue
}
// 进行类型转换和赋值
if err := assignValue(srcField, dstField); err != nil {
continue // 如果单个字段转换失败,继续处理其他字段
}
}
return nil
}
// assignValue 尝试将源字段值赋给目标字段
func assignValue(srcField, dstField reflect.Value) error {
// 如果是可直接赋值的类型
if srcField.Type() == dstField.Type() {
dstField.Set(srcField)
return nil
}
srcType := srcField.Type()
dstType := dstField.Type()
// 处理 time.Time -> int64 的转换
if srcType == reflect.TypeOf(time.Time{}) && dstType.Kind() == reflect.Int64 {
t := srcField.Interface().(time.Time)
dstField.SetInt(t.Unix())
return nil
}
// 处理 sql.NullTime -> int64 的转换
if srcType == reflect.TypeOf(sql.NullTime{}) && dstType.Kind() == reflect.Int64 {
nt := srcField.Interface().(sql.NullTime)
if nt.Valid {
dstField.SetInt(nt.Time.Unix())
}
return nil
}
// 处理 sql.NullTime -> time.Time 的转换
if srcType == reflect.TypeOf(sql.NullTime{}) && dstType == reflect.TypeOf(time.Time{}) {
nt := srcField.Interface().(sql.NullTime)
if nt.Valid {
dstField.Set(reflect.ValueOf(nt.Time))
}
return nil
}
// 处理 sql.NullInt64 -> int64 的转换
if srcType == reflect.TypeOf(sql.NullInt64{}) && dstType.Kind() == reflect.Int64 {
ni := srcField.Interface().(sql.NullInt64)
if ni.Valid {
dstField.SetInt(ni.Int64)
}
return nil
}
// 处理 sql.NullString -> string 的转换
if srcType == reflect.TypeOf(sql.NullString{}) && dstType.Kind() == reflect.String {
ns := srcField.Interface().(sql.NullString)
if ns.Valid {
dstField.SetString(ns.String)
}
return nil
}
// 处理 sql.NullBool -> bool 的转换
if srcType == reflect.TypeOf(sql.NullBool{}) && dstType.Kind() == reflect.Bool {
nb := srcField.Interface().(sql.NullBool)
if nb.Valid {
dstField.SetBool(nb.Bool)
}
return nil
}
// 处理 int -> int64 的转换
if srcType.Kind() == reflect.Int && dstType.Kind() == reflect.Int64 {
dstField.SetInt(int64(srcField.Int()))
return nil
}
// 处理 int64 -> int 的转换
if srcType.Kind() == reflect.Int64 && dstType.Kind() == reflect.Int {
dstField.SetInt(srcField.Int())
return nil
}
// 处理 string -> string(某些情况下可能存在复制)
if srcType.Kind() == reflect.String && dstType.Kind() == reflect.String {
dstField.SetString(srcField.String())
return nil
}
// 处理 bool -> bool
if srcType.Kind() == reflect.Bool && dstType.Kind() == reflect.Bool {
dstField.SetBool(srcField.Bool())
return nil
}
return newError("unsupported type conversion from " + srcType.String() + " to " + dstType.String())
}
// SliceToSlice 通用切片转换函数,使用 StructToStruct 转换每个元素
func SliceToSlice(src interface{}, dstSliceType interface{}) (interface{}, error) {
srcVal := reflect.ValueOf(src)
// src 必须是切片
if srcVal.Kind() != reflect.Slice {
return nil, newError("source must be a slice")
}
// 获取原始 dst slice type
dstSliceVal := reflect.ValueOf(dstSliceType)
if dstSliceVal.Kind() != reflect.Slice {
return nil, newError("dstSliceType must be a slice type")
}
dstSliceElemType := dstSliceVal.Type().Elem()
// 创建新的目标切片
dstSlice := reflect.MakeSlice(dstSliceVal.Type(), srcVal.Len(), srcVal.Len())
// 逐个转换元素
for i := 0; i < srcVal.Len(); i++ {
srcElem := srcVal.Index(i)
dstElem := reflect.New(dstSliceElemType)
// 如果 src 元素是指针,需要解引用
if srcElem.Kind() == reflect.Ptr {
srcElem = srcElem.Elem()
}
// 转换单个元素
dstElemIface := dstElem.Interface()
if err := StructToStruct(srcElem.Interface(), dstElemIface); err != nil {
return nil, err
}
dstSlice.Index(i).Set(dstElem.Elem())
}
return dstSlice.Interface(), nil
}
type Error struct {
msg string
}
func (e *Error) Error() string {
return e.msg
}
func newError(msg string) error {
return &Error{msg: msg}
}
+29
View File
@@ -0,0 +1,29 @@
package converter
import (
"app/users/rpc/internal/models"
"app/users/rpc/pb"
)
// UserModelToPb 将 Users Model 转换为 protobuf Users
// 使用通用转换函数,自动处理所有字段
func UserModelToPb(user *models.Users) *pb.Users {
if user == nil {
return nil
}
pbUser := &pb.Users{}
_ = StructToStruct(user, pbUser)
return pbUser
}
// UserModelsToPb 将多个 Users Model 转换为 protobuf Users
// 使用通用转换函数,自动处理所有元素
func UserModelsToPb(users []*models.Users) []*pb.Users {
if len(users) == 0 {
return []*pb.Users{}
}
result, _ := SliceToSlice(users, []*pb.Users{})
return result.([]*pb.Users)
}