diff --git a/README.md b/README.md index 0d30876..d2e52c0 100644 --- a/README.md +++ b/README.md @@ -104,20 +104,27 @@ now := datetime.Now() str := datetime.FormatDateTime(now) ``` -#### HTTP响应 +#### HTTP响应(Handler黑盒模式) ```go -import "git.toowon.com/jimmy/go-common/http" +import ( + "net/http" + commonhttp "git.toowon.com/jimmy/go-common/http" +) -http.Success(w, data) -http.SuccessPage(w, list, total, page, pageSize) -http.Error(w, 1001, "业务错误") +// 使用Handler(黑盒模式) +func GetUser(h *commonhttp.Handler) { + id := h.GetQueryInt64("id", 0) // 无需传递r + h.Success(data) // 无需传递w +} + +http.HandleFunc("/user", commonhttp.HandleFunc(GetUser)) ``` #### 中间件 ```go import ( "git.toowon.com/jimmy/go-common/middleware" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" ) // CORS + 时区中间件 @@ -127,8 +134,10 @@ chain := middleware.NewChain( ) handler := chain.ThenFunc(yourHandler) -// 在处理器中获取时区 -timezone := http.GetTimezone(r) +// 在Handler中获取时区 +func handler(h *commonhttp.Handler) { + timezone := h.GetTimezone() +} ``` #### 配置管理 @@ -144,77 +153,95 @@ redisAddr := cfg.GetRedisAddr() corsConfig := cfg.GetCORS() ``` -#### 文件上传和查看 +#### 文件上传和查看(推荐使用工厂黑盒模式) ```go -import "git.toowon.com/jimmy/go-common/storage" +import ( + "context" + "git.toowon.com/jimmy/go-common/factory" +) -// 创建存储实例 +fac, _ := factory.NewFactoryFromFile("./config.json") +ctx := context.Background() + +// 黑盒模式(推荐,自动选择OSS或MinIO) +file, _ := os.Open("test.jpg") +url, _ := fac.UploadFile(ctx, "images/test.jpg", file, "image/jpeg") + +// 获取文件URL +url, _ := fac.GetFileURL("images/test.jpg", 0) // 永久有效 +url, _ := fac.GetFileURL("images/test.jpg", 3600) // 1小时后过期 + +// 或使用存储处理器(需要HTTP处理器时) storage, _ := storage.NewStorage(storage.StorageTypeOSS, cfg) - -// 创建上传处理器 -uploadHandler := storage.NewUploadHandler(storage.UploadHandlerConfig{ - Storage: storage, - MaxFileSize: 10 * 1024 * 1024, - AllowedExts: []string{".jpg", ".png"}, -}) - -// 创建代理查看处理器 -proxyHandler := storage.NewProxyHandler(storage) +uploadHandler := storage.NewUploadHandler(...) ``` -#### 邮件发送 -```go -import "git.toowon.com/jimmy/go-common/email" - -// 从配置创建邮件发送器 -mailer, _ := email.NewEmail(cfg.GetEmail()) - -// 发送邮件 -mailer.SendSimple( - []string{"recipient@example.com"}, - "主题", - "正文", -) -``` - -#### 短信发送 -```go -import "git.toowon.com/jimmy/go-common/sms" - -// 从配置创建短信发送器 -smsClient, _ := sms.NewSMS(cfg.GetSMS()) - -// 发送短信 -smsClient.SendSimple( - []string{"13800138000"}, - map[string]string{"code": "123456"}, -) -``` - -#### 使用工厂直接获取客户端(推荐) +#### 邮件发送(推荐使用工厂黑盒模式) ```go import "git.toowon.com/jimmy/go-common/factory" -// 方式1:直接从配置文件创建工厂(最推荐) fac, _ := factory.NewFactoryFromFile("./config.json") -// 直接获取数据库对象(已初始化,可直接使用) -db, _ := fac.GetDatabase() -db.Find(&users) // 直接使用,无需再创建连接 +// 黑盒模式(推荐) +fac.SendEmail([]string{"user@example.com"}, "主题", "正文") +fac.SendEmail([]string{"user@example.com"}, "主题", "纯文本", "

HTML内容

") -// 直接获取Redis客户端(已初始化,可直接使用) -redisClient, _ := fac.GetRedisClient() -val, _ := redisClient.Get(ctx, "key").Result() - -// 直接获取已初始化的客户端(无需重复实现创建逻辑) +// 或获取客户端对象(需要高级功能时) emailClient, _ := fac.GetEmailClient() -smsClient, _ := fac.GetSMSClient() -logger, _ := fac.GetLogger() - -// 直接使用 emailClient.SendSimple(...) +``` + +#### 短信发送(推荐使用工厂黑盒模式) +```go +import "git.toowon.com/jimmy/go-common/factory" + +fac, _ := factory.NewFactoryFromFile("./config.json") + +// 黑盒模式(推荐) +fac.SendSMS([]string{"13800138000"}, map[string]string{"code": "123456"}) + +// 或获取客户端对象(需要高级功能时) +smsClient, _ := fac.GetSMSClient() smsClient.SendSimple(...) -logger.Info("Application started") +``` + +#### 使用工厂(黑盒模式,推荐) +```go +import ( + "context" + "git.toowon.com/jimmy/go-common/factory" +) + +// 从配置文件创建工厂(最推荐) +fac, _ := factory.NewFactoryFromFile("./config.json") +ctx := context.Background() + +// 日志(黑盒模式,直接调用) +fac.LogInfo("用户登录成功") +fac.LogError("登录失败: %v", err) + +// 邮件发送(黑盒模式,直接调用) +fac.SendEmail([]string{"user@example.com"}, "验证码", "您的验证码是:123456") + +// 短信发送(黑盒模式,直接调用) +fac.SendSMS([]string{"13800138000"}, map[string]string{"code": "123456"}) + +// 文件上传(黑盒模式,自动选择OSS或MinIO) +file, _ := os.Open("test.jpg") +url, _ := fac.UploadFile(ctx, "images/test.jpg", file, "image/jpeg") + +// 获取文件URL +url, _ := fac.GetFileURL("images/test.jpg", 0) // 永久有效 +url, _ := fac.GetFileURL("images/test.jpg", 3600) // 1小时后过期 + +// Redis操作(黑盒模式,直接调用) +fac.RedisSet(ctx, "key", "value", time.Hour) +value, _ := fac.RedisGet(ctx, "key") +fac.RedisDelete(ctx, "key") + +// 数据库(GORM已经很灵活,直接返回对象) +db, _ := fac.GetDatabase() +db.Find(&users) ``` 更多示例请查看 [examples](./examples/) 目录。 diff --git a/docs/README.md b/docs/README.md index 8c83d20..9baf764 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,22 +49,29 @@ now := datetime.Now() str := datetime.FormatDateTime(now) ``` -#### HTTP响应 +#### HTTP响应(Handler黑盒模式) ```go -import "git.toowon.com/jimmy/go-common/http" +import ( + "net/http" + commonhttp "git.toowon.com/jimmy/go-common/http" +) -http.Success(w, data) -http.SuccessPage(w, list, total, page, pageSize) -http.Error(w, 1001, "业务错误") +func GetUser(h *commonhttp.Handler) { + id := h.GetQueryInt64("id", 0) + h.Success(data) +} + +http.HandleFunc("/user", commonhttp.HandleFunc(GetUser)) ``` #### 中间件 ```go import ( + "net/http" "git.toowon.com/jimmy/go-common/middleware" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" ) // CORS + 时区中间件 @@ -72,10 +79,13 @@ chain := middleware.NewChain( middleware.CORS(), middleware.Timezone, ) -handler := chain.ThenFunc(yourHandler) -// 在处理器中获取时区 -timezone := http.GetTimezone(r) +handler := chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) { + h := commonhttp.NewHandler(w, r) + // 在Handler中获取时区 + timezone := h.GetTimezone() + h.Success(data) +}) ``` #### 配置管理 diff --git a/docs/factory.md b/docs/factory.md index 510e0c2..fd3ceb3 100644 --- a/docs/factory.md +++ b/docs/factory.md @@ -2,204 +2,249 @@ ## 概述 -工厂工具提供了从配置直接创建已初始化的客户端对象的功能,避免调用方重复实现创建逻辑。 +工厂工具提供了从配置直接创建已初始化客户端对象的功能,并提供了黑盒模式的便捷方法,让调用方无需关心底层实现细节,大大降低业务复杂度。 ## 功能特性 -- 从配置直接创建已初始化的客户端对象 -- 统一的工厂接口 -- 避免调用方重复实现创建逻辑 +- **黑盒模式**:提供直接调用的方法,无需获取客户端对象 +- **延迟初始化**:所有客户端在首次使用时才创建 +- **自动选择**:存储类型(OSS/MinIO)根据配置自动选择 +- **统一接口**:所有操作通过工厂方法调用 +- **向后兼容**:保留 `GetXXX()` 方法,需要时可获取对象 ## 使用方法 -### 1. 从配置文件直接创建工厂(推荐) +### 1. 创建工厂(推荐) ```go import "git.toowon.com/jimmy/go-common/factory" -// 直接传入配置文件路径,自动加载配置并创建工厂 +// 方式1:直接从配置文件创建(最推荐) fac, err := factory.NewFactoryFromFile("./config.json") if err != nil { log.Fatal(err) } -// 直接获取已初始化的对象 -db, _ := fac.GetDatabase() // 直接获取数据库对象 -logger, _ := fac.GetLogger() // 直接获取日志对象 -emailClient, _ := fac.GetEmailClient() // 直接获取邮件客户端 -``` - -### 2. 从配置对象创建工厂 - -```go -import ( - "git.toowon.com/jimmy/go-common/config" - "git.toowon.com/jimmy/go-common/factory" -) - -// 加载配置 -cfg, err := config.LoadFromFile("./config.json") -if err != nil { - log.Fatal(err) -} - -// 创建工厂实例 +// 方式2:从配置对象创建 +cfg, _ := config.LoadFromFile("./config.json") fac := factory.NewFactory(cfg) ``` -### 3. 获取数据库对象(已初始化,推荐) +### 2. 日志记录(黑盒模式,推荐) ```go -// 直接获取已初始化的数据库对象(*gorm.DB) +// 简单日志 +fac.LogDebug("调试信息: %s", "test") +fac.LogInfo("用户登录成功") +fac.LogWarn("警告信息") +fac.LogError("错误信息: %v", err) + +// 带字段的日志 +fac.LogInfof(map[string]interface{}{ + "user_id": 123, + "ip": "192.168.1.1", +}, "用户登录成功") + +fac.LogErrorf(map[string]interface{}{ + "error_code": 1001, +}, "登录失败: %v", err) +``` + +### 3. 邮件发送(黑盒模式,推荐) + +```go +// 简单邮件 +err := fac.SendEmail( + []string{"user@example.com"}, + "验证码", + "您的验证码是:123456", +) + +// HTML邮件 +err := fac.SendEmail( + []string{"user@example.com"}, + "验证码", + "纯文本内容", + "

HTML内容

", +) +``` + +### 4. 短信发送(黑盒模式,推荐) + +```go +// 使用配置中的模板代码 +resp, err := fac.SendSMS( + []string{"13800138000"}, + map[string]string{"code": "123456"}, +) + +// 指定模板代码 +resp, err := fac.SendSMS( + []string{"13800138000"}, + map[string]string{"code": "123456"}, + "SMS_123456789", // 模板代码 +) +``` + +### 5. 文件上传和查看(黑盒模式,推荐) + +```go +import ( + "context" + "os" +) + +ctx := context.Background() + +// 上传文件(自动选择OSS或MinIO) +file, _ := os.Open("test.jpg") +defer file.Close() + +url, err := fac.UploadFile(ctx, "images/test.jpg", file, "image/jpeg") +if err != nil { + log.Fatal(err) +} +fmt.Println("文件URL:", url) + +// 获取文件URL(永久有效) +url, _ := fac.GetFileURL("images/test.jpg", 0) + +// 获取临时访问URL(1小时后过期) +url, _ := fac.GetFileURL("images/test.jpg", 3600) +``` + +### 6. Redis操作(黑盒模式,推荐) + +```go +import "context" + +ctx := context.Background() + +// 设置值(不过期) +err := fac.RedisSet(ctx, "user:123", "value") + +// 设置值(带过期时间) +err := fac.RedisSet(ctx, "user:123", "value", time.Hour) + +// 获取值 +value, err := fac.RedisGet(ctx, "user:123") + +// 删除键 +err := fac.RedisDelete(ctx, "user:123", "user:456") + +// 检查键是否存在 +exists, err := fac.RedisExists(ctx, "user:123") +``` + +### 7. 数据库操作 + +数据库保持返回 GORM 对象,因为 GORM 已经提供了很好的抽象: + +```go +// 获取数据库对象(延迟初始化) db, err := fac.GetDatabase() if err != nil { log.Fatal(err) } -// 直接使用,无需再创建连接 +// 直接使用GORM var users []User db.Find(&users) db.Create(&user) ``` -### 4. 获取Redis客户端(已初始化,推荐) -```go -import ( - "context" - "github.com/redis/go-redis/v9" -) - -// 直接获取已初始化的Redis客户端对象 -redisClient, err := fac.GetRedisClient() -if err != nil { - log.Fatal(err) -} - -// 直接使用,无需再创建连接 -ctx := context.Background() -val, err := redisClient.Get(ctx, "key").Result() -if err != nil && err != redis.Nil { - log.Printf("Redis error: %v", err) -} else if err == redis.Nil { - fmt.Println("Key not found") -} else { - fmt.Printf("Value: %s\n", val) -} -``` - -### 5. 获取邮件客户端(已初始化) - -```go -// 直接获取已初始化的邮件客户端 -emailClient, err := fac.GetEmailClient() -if err != nil { - log.Fatal(err) -} - -// 直接使用,无需再创建 -err = emailClient.SendSimple( - []string{"recipient@example.com"}, - "主题", - "正文", -) -``` - -### 6. 获取短信客户端(已初始化) - -```go -// 直接获取已初始化的短信客户端 -smsClient, err := fac.GetSMSClient() -if err != nil { - log.Fatal(err) -} - -// 直接使用,无需再创建 -resp, err := smsClient.SendSimple( - []string{"13800138000"}, - map[string]string{"code": "123456"}, -) -``` - -### 7. 获取日志记录器(已初始化) - -```go -// 直接获取已初始化的日志记录器 -logger, err := fac.GetLogger() -if err != nil { - log.Fatal(err) -} - -// 直接使用,无需再创建 -logger.Info("Application started") -logger.Error("Error occurred: %v", err) -``` - -### 8. 完整示例 +## 完整示例 ```go package main import ( + "context" "log" - - "git.toowon.com/jimmy/go-common/config" + "os" + "time" + "git.toowon.com/jimmy/go-common/factory" ) func main() { - // 加载配置 - cfg, err := config.LoadFromFile("./config.json") + // 创建工厂 + fac, err := factory.NewFactoryFromFile("./config.json") if err != nil { log.Fatal(err) } - // 创建工厂 - fac := factory.NewFactory(cfg) + ctx := context.Background() - // 获取邮件客户端(已初始化,可直接使用) - emailClient, err := fac.GetEmailClient() + // 日志记录(黑盒模式) + fac.LogInfo("应用启动") + fac.LogInfof(map[string]interface{}{ + "version": "1.0.0", + }, "应用启动成功") + + // 邮件发送(黑盒模式) + err = fac.SendEmail( + []string{"user@example.com"}, + "欢迎", + "欢迎使用我们的服务", + ) if err != nil { - log.Printf("Email client not available: %v", err) - } else { - // 直接使用 - err = emailClient.SendSimple( - []string{"recipient@example.com"}, - "测试邮件", - "这是测试内容", - ) - if err != nil { - log.Printf("Failed to send email: %v", err) - } + fac.LogError("发送邮件失败: %v", err) } - // 获取短信客户端(已初始化,可直接使用) - smsClient, err := fac.GetSMSClient() + // 短信发送(黑盒模式) + resp, err := fac.SendSMS( + []string{"13800138000"}, + map[string]string{"code": "123456"}, + ) if err != nil { - log.Printf("SMS client not available: %v", err) + fac.LogError("发送短信失败: %v", err) } else { - // 直接使用 - resp, err := smsClient.SendSimple( - []string{"13800138000"}, - map[string]string{"code": "123456"}, - ) - if err != nil { - log.Printf("Failed to send SMS: %v", err) - } else { - log.Printf("SMS sent: %s", resp.RequestID) - } + fac.LogInfo("短信发送成功: %s", resp.RequestID) } - // 如果需要访问配置对象 - cfgObj := fac.GetConfig() - dsn, _ := cfgObj.GetDatabaseDSN() - log.Printf("Database DSN: %s", dsn) + // 文件上传(黑盒模式,自动选择OSS或MinIO) + file, _ := os.Open("test.jpg") + defer file.Close() + + url, err := fac.UploadFile(ctx, "images/test.jpg", file, "image/jpeg") + if err != nil { + fac.LogError("上传文件失败: %v", err) + } else { + fac.LogInfo("文件上传成功: %s", url) + } + + // Redis操作(黑盒模式) + err = fac.RedisSet(ctx, "user:123", "value", time.Hour) + if err != nil { + fac.LogError("Redis设置失败: %v", err) + } + + value, err := fac.RedisGet(ctx, "user:123") + if err != nil { + fac.LogError("Redis获取失败: %v", err) + } else { + fac.LogInfo("Redis值: %s", value) + } + + // 数据库操作 + db, err := fac.GetDatabase() + if err != nil { + fac.LogError("数据库连接失败: %v", err) + } else { + var count int64 + db.Table("users").Count(&count) + fac.LogInfo("用户数量: %d", count) + } } ``` ## API 参考 -### NewFactory(cfg *config.Config) *Factory +### 工厂创建 + +#### NewFactory(cfg *config.Config) *Factory 创建工厂实例。 @@ -208,7 +253,7 @@ func main() { **返回:** 工厂实例 -### NewFactoryFromFile(filePath string) (*Factory, error) +#### NewFactoryFromFile(filePath string) (*Factory, error) 从配置文件直接创建工厂实例(便捷方法)。 @@ -219,31 +264,132 @@ func main() { **说明:** 这是推荐的使用方式,一步完成配置加载和工厂创建。 -### (f *Factory) GetEmailClient() (*email.Email, error) +### 日志方法(黑盒模式) -获取邮件客户端(已初始化)。 +#### LogDebug(message string, args ...interface{}) -**返回:** 已初始化的邮件客户端对象和错误信息 +记录调试日志。 -**说明:** 如果邮件配置为nil,返回错误。 +#### LogDebugf(fields map[string]interface{}, message string, args ...interface{}) -### (f *Factory) GetSMSClient() (*sms.SMS, error) +记录调试日志(带字段)。 -获取短信客户端(已初始化)。 +#### LogInfo(message string, args ...interface{}) -**返回:** 已初始化的短信客户端对象和错误信息 +记录信息日志。 -**说明:** 如果短信配置为nil,返回错误。 +#### LogInfof(fields map[string]interface{}, message string, args ...interface{}) -### (f *Factory) GetLogger() (*logger.Logger, error) +记录信息日志(带字段)。 -获取日志记录器(已初始化)。 +#### LogWarn(message string, args ...interface{}) -**返回:** 已初始化的日志记录器对象和错误信息 +记录警告日志。 -**说明:** 如果日志配置为nil,会使用默认配置创建。 +#### LogWarnf(fields map[string]interface{}, message string, args ...interface{}) -### (f *Factory) GetDatabase() (*gorm.DB, error) +记录警告日志(带字段)。 + +#### LogError(message string, args ...interface{}) + +记录错误日志。 + +#### LogErrorf(fields map[string]interface{}, message string, args ...interface{}) + +记录错误日志(带字段)。 + +### 邮件方法(黑盒模式) + +#### SendEmail(to []string, subject, body string, htmlBody ...string) error + +发送邮件。 + +**参数:** +- `to`: 收件人列表 +- `subject`: 邮件主题 +- `body`: 邮件正文(纯文本) +- `htmlBody`: HTML正文(可选,如果设置了会优先使用) + +### 短信方法(黑盒模式) + +#### SendSMS(phoneNumbers []string, templateParam interface{}, templateCode ...string) (*sms.SendResponse, error) + +发送短信。 + +**参数:** +- `phoneNumbers`: 手机号列表 +- `templateParam`: 模板参数(map或JSON字符串) +- `templateCode`: 模板代码(可选,如果为空使用配置中的模板代码) + +### 存储方法(黑盒模式) + +#### UploadFile(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) (string, error) + +上传文件。 + +**参数:** +- `ctx`: 上下文 +- `objectKey`: 对象键(文件路径) +- `reader`: 文件内容 +- `contentType`: 文件类型(可选) + +**返回:** 文件访问URL和错误信息 + +**说明:** 自动根据配置选择OSS或MinIO(优先级:MinIO > OSS) + +#### GetFileURL(objectKey string, expires int64) (string, error) + +获取文件访问URL。 + +**参数:** +- `objectKey`: 对象键 +- `expires`: 过期时间(秒),0表示永久有效 + +**返回:** 文件访问URL和错误信息 + +### Redis方法(黑盒模式) + +#### RedisGet(ctx context.Context, key string) (string, error) + +获取Redis值。 + +**参数:** +- `ctx`: 上下文 +- `key`: Redis键 + +**返回:** 值和错误信息(key不存在时返回空字符串) + +#### RedisSet(ctx context.Context, key string, value interface{}, expiration ...time.Duration) error + +设置Redis值。 + +**参数:** +- `ctx`: 上下文 +- `key`: Redis键 +- `value`: Redis值 +- `expiration`: 过期时间(可选,0表示不过期) + +#### RedisDelete(ctx context.Context, keys ...string) error + +删除Redis键。 + +**参数:** +- `ctx`: 上下文 +- `keys`: Redis键列表 + +#### RedisExists(ctx context.Context, key string) (bool, error) + +检查Redis键是否存在。 + +**参数:** +- `ctx`: 上下文 +- `key`: Redis键 + +**返回:** 是否存在和错误信息 + +### 数据库方法 + +#### GetDatabase() (*gorm.DB, error) 获取数据库连接对象(已初始化)。 @@ -253,78 +399,35 @@ func main() { - 支持MySQL、PostgreSQL、SQLite - 自动配置连接池参数 - 数据库时间统一使用UTC时区 +- 延迟初始化,首次调用时创建连接 -### (f *Factory) GetRedisClient() (*redis.Client, error) -获取Redis客户端对象(已初始化)。 - -**返回:** 已初始化的Redis客户端对象和错误信息 - -**说明:** -- 自动处理所有配置检查和连接测试 -- 自动设置默认值(连接池大小、超时时间等) -- 连接失败时会自动关闭客户端并返回错误 -- 返回的客户端已通过Ping测试,可直接使用 - -### (f *Factory) GetRedisConfig() *config.RedisConfig - -获取Redis配置(用于创建Redis客户端)。 - -**返回:** Redis配置对象(可能为nil) - -**说明:** -- 推荐使用 `GetRedisClient()` 方法直接获取已初始化的客户端 -- 如果需要自定义创建Redis客户端,可以使用此方法获取配置 - -### (f *Factory) GetConfig() *config.Config +#### GetConfig() *config.Config 获取配置对象。 **返回:** 配置对象 -## 优势 +## 设计优势 -### 之前的方式(需要调用方实现) +### 优势总结 -```go -// 调用方需要自己实现创建逻辑 -cfg, _ := config.LoadFromFile("./config.json") -dsn, _ := cfg.GetDatabaseDSN() -db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) -if err != nil { - log.Fatal(err) -} -// 配置连接池 -sqlDB, _ := db.DB() -sqlDB.SetMaxOpenConns(100) -// 才能使用 -db.Find(&users) -``` - -### 使用工厂方式(直接获取已初始化对象) - -```go -// 方式1:直接从配置文件创建(最推荐) -fac, _ := factory.NewFactoryFromFile("./config.json") -db, _ := fac.GetDatabase() // 直接获取已初始化的数据库对象 -// 直接使用 -db.Find(&users) - -// 方式2:从配置对象创建 -cfg, _ := config.LoadFromFile("./config.json") -fac := factory.NewFactory(cfg) -db, _ := fac.GetDatabase() // 直接获取已初始化的数据库对象 -// 直接使用 -db.Find(&users) -``` +1. **降低复杂度**:调用方无需关心客户端对象的创建和管理 +2. **延迟初始化**:所有客户端在首次使用时才创建,提高性能 +3. **自动选择**:存储类型根据配置自动选择,无需手动指定 +4. **统一接口**:所有操作通过工厂方法调用,接口统一 +5. **容错处理**:日志初始化失败时自动回退到标准输出 +6. **代码简洁**:只提供黑盒模式方法,保持代码简洁清晰 ## 注意事项 1. **配置检查**:工厂方法会自动检查配置是否存在,如果配置为nil会返回错误 -2. **错误处理**:所有Get方法都可能返回错误,需要正确处理 -3. **配置对象**:可以通过`GetConfig()`方法访问原始配置对象,获取其他配置信息 +2. **错误处理**:所有方法都可能返回错误,需要正确处理 +3. **延迟初始化**:所有客户端在首次使用时才创建,首次调用可能稍慢 +4. **存储选择**:存储类型根据配置自动选择(优先级:MinIO > OSS) +5. **数据库对象**:数据库保持返回GORM对象,因为GORM已经提供了很好的抽象 +6. **黑盒模式**:所有功能都通过工厂方法直接调用,无需获取底层客户端对象 ## 示例 完整示例请参考 `examples/factory_example.go` - diff --git a/docs/http.md b/docs/http.md index b75d52b..653f67a 100644 --- a/docs/http.md +++ b/docs/http.md @@ -2,16 +2,16 @@ ## 概述 -HTTP Restful工具提供了标准化的HTTP请求和响应处理功能,包含统一的响应结构、分页支持和HTTP状态码与业务状态码的分离。 +HTTP Restful工具提供了标准化的HTTP请求和响应处理功能,采用Handler黑盒模式,封装了`ResponseWriter`和`Request`,提供简洁的API,无需每次都传递这两个参数。 ## 功能特性 -- 标准化的响应结构:`{code, message, timestamp, data}` -- 分离HTTP状态码和业务状态码 -- 支持分页响应 -- 提供便捷的请求参数解析方法 -- 支持JSON请求体解析 -- 提供常用的HTTP错误响应方法 +- **黑盒模式**:封装`ResponseWriter`和`Request`,提供简洁的API +- **标准化的响应结构**:`{code, message, timestamp, data}` +- **分离HTTP状态码和业务状态码** +- **支持分页响应** +- **提供便捷的请求参数解析方法** +- **支持JSON请求体解析** ## 响应结构 @@ -44,151 +44,224 @@ HTTP Restful工具提供了标准化的HTTP请求和响应处理功能,包含 ## 使用方法 -### 1. 成功响应 +### 1. 创建Handler ```go import ( "net/http" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" ) -// 简单成功响应(data为nil) -http.Success(w, nil) - -// 带数据的成功响应 -data := map[string]interface{}{ - "id": 1, - "name": "test", +// 方式1:使用HandleFunc包装器(推荐,最简洁) +func GetUser(h *commonhttp.Handler) { + id := h.GetQueryInt64("id", 0) + h.Success(data) } -http.Success(w, data) -// 带消息的成功响应 -http.SuccessWithMessage(w, "操作成功", data) +http.HandleFunc("/user", commonhttp.HandleFunc(GetUser)) + +// 方式2:手动创建Handler(需要更多控制时) +http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { + h := commonhttp.NewHandler(w, r) + GetUser(h) +}) ``` -### 2. 错误响应 +### 2. 成功响应 ```go -// 业务错误(HTTP 200,业务code非0) -http.Error(w, 1001, "用户不存在") - -// 系统错误(HTTP 500) -http.SystemError(w, "服务器内部错误") - -// 请求错误(HTTP 400) -http.BadRequest(w, "请求参数错误") - -// 未授权(HTTP 401) -http.Unauthorized(w, "未登录") - -// 禁止访问(HTTP 403) -http.Forbidden(w, "无权限访问") - -// 未找到(HTTP 404) -http.NotFound(w, "资源不存在") +func handler(h *commonhttp.Handler) { + // 简单成功响应(data为nil) + h.Success(nil) + + // 带数据的成功响应 + data := map[string]interface{}{ + "id": 1, + "name": "test", + } + h.Success(data) + + // 带消息的成功响应 + h.SuccessWithMessage("操作成功", data) +} ``` -### 3. 分页响应 +### 3. 错误响应 ```go -// 获取分页参数 -page, pageSize := http.GetPaginationParams(r) - -// 查询数据(示例) -list, total := getDataList(page, pageSize) - -// 返回分页响应 -http.SuccessPage(w, list, total, page, pageSize) - -// 带消息的分页响应 -http.SuccessPageWithMessage(w, "查询成功", list, total, page, pageSize) +func handler(h *commonhttp.Handler) { + // 业务错误(HTTP 200,业务code非0) + h.Error(1001, "用户不存在") + + // 系统错误(HTTP 500) + h.SystemError("服务器内部错误") + + // 其他HTTP错误状态码,使用WriteJSON直接指定 + // 请求错误(HTTP 400) + h.WriteJSON(http.StatusBadRequest, 400, "请求参数错误", nil) + + // 未授权(HTTP 401) + h.WriteJSON(http.StatusUnauthorized, 401, "未登录", nil) + + // 禁止访问(HTTP 403) + h.WriteJSON(http.StatusForbidden, 403, "无权限访问", nil) + + // 未找到(HTTP 404) + h.WriteJSON(http.StatusNotFound, 404, "资源不存在", nil) +} ``` -### 4. 解析请求 +### 4. 分页响应 + +```go +func handler(h *commonhttp.Handler) { + // 获取分页参数 + pagination := h.ParsePaginationRequest() + page := pagination.GetPage() + pageSize := pagination.GetSize() + + // 查询数据(示例) + list, total := getDataList(page, pageSize) + + // 返回分页响应(使用默认消息) + h.SuccessPage(list, total, page, pageSize) + + // 返回分页响应(自定义消息) + h.SuccessPage(list, total, page, pageSize, "查询成功") +} +``` + +### 5. 解析请求 #### 解析JSON请求体 ```go -type CreateUserRequest struct { - Name string `json:"name"` - Email string `json:"email"` -} - -var req CreateUserRequest -err := http.ParseJSON(r, &req) -if err != nil { - http.BadRequest(w, "请求参数解析失败") - return +func handler(h *commonhttp.Handler) { + type CreateUserRequest struct { + Name string `json:"name"` + Email string `json:"email"` + } + + var req CreateUserRequest + if err := h.ParseJSON(&req); err != nil { + h.WriteJSON(http.StatusBadRequest, 400, "请求参数解析失败", nil) + return + } + + // 使用req... } ``` #### 获取查询参数 ```go -// 获取字符串参数 -name := http.GetQuery(r, "name", "") -email := http.GetQuery(r, "email", "default@example.com") - -// 获取整数参数 -id := http.GetQueryInt(r, "id", 0) -age := http.GetQueryInt(r, "age", 18) - -// 获取int64参数 -userId := http.GetQueryInt64(r, "userId", 0) - -// 获取布尔参数 -isActive := http.GetQueryBool(r, "isActive", false) - -// 获取浮点数参数 -price := http.GetQueryFloat64(r, "price", 0.0) +func handler(h *commonhttp.Handler) { + // 获取字符串参数 + name := h.GetQuery("name", "") + email := h.GetQuery("email", "default@example.com") + + // 获取整数参数 + id := h.GetQueryInt("id", 0) + age := h.GetQueryInt("age", 18) + + // 获取int64参数 + userId := h.GetQueryInt64("userId", 0) + + // 获取布尔参数 + isActive := h.GetQueryBool("isActive", false) + + // 获取浮点数参数 + price := h.GetQueryFloat64("price", 0.0) +} ``` #### 获取表单参数 ```go -// 获取表单字符串 -name := http.GetFormValue(r, "name", "") - -// 获取表单整数 -age := http.GetFormInt(r, "age", 0) - -// 获取表单int64 -userId := http.GetFormInt64(r, "userId", 0) - -// 获取表单布尔值 -isActive := http.GetFormBool(r, "isActive", false) +func handler(h *commonhttp.Handler) { + // 获取表单字符串 + name := h.GetFormValue("name", "") + + // 获取表单整数 + age := h.GetFormInt("age", 0) + + // 获取表单int64 + userId := h.GetFormInt64("userId", 0) + + // 获取表单布尔值 + isActive := h.GetFormBool("isActive", false) +} ``` #### 获取请求头 ```go -token := http.GetHeader(r, "Authorization", "") -contentType := http.GetHeader(r, "Content-Type", "application/json") +func handler(h *commonhttp.Handler) { + token := h.GetHeader("Authorization", "") + contentType := h.GetHeader("Content-Type", "application/json") +} ``` #### 获取分页参数 -```go -// 自动解析page和pageSize参数 -// 默认: page=1, pageSize=10 -// 限制: pageSize最大1000 -page, pageSize := http.GetPaginationParams(r) +**方式1:使用 PaginationRequest 结构(推荐)** -// 计算数据库查询偏移量 -offset := http.GetOffset(page, pageSize) +```go +func handler(h *commonhttp.Handler) { + // 定义请求结构(包含分页字段) + type ListUserRequest struct { + Keyword string `json:"keyword"` + commonhttp.PaginationRequest // 嵌入分页请求结构 + } + + // 从JSON请求体解析(分页字段会自动解析) + var req ListUserRequest + if err := h.ParseJSON(&req); err != nil { + h.WriteJSON(http.StatusBadRequest, 400, "请求参数解析失败", nil) + return + } + + // 使用分页方法 + page := req.GetPage() // 获取页码(默认1) + size := req.GetSize() // 获取每页数量(默认20,最大100,优先使用page_size) + offset := req.GetOffset() // 计算偏移量 +} + +// 或者从查询参数/form解析分页 +func handler(h *commonhttp.Handler) { + pagination := h.ParsePaginationRequest() + page := pagination.GetPage() + size := pagination.GetSize() + offset := pagination.GetOffset() +} ``` -### 5. 自定义响应 +#### 获取时区 ```go -// 使用WriteJSON自定义响应 -http.WriteJSON(w, http.StatusOK, 0, "success", data) +func handler(h *commonhttp.Handler) { + // 从请求的context中获取时区 + // 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 + // 如果未设置,返回默认时区 AsiaShanghai + timezone := h.GetTimezone() +} +``` -// 参数说明: -// - httpCode: HTTP状态码(200, 400, 500等) -// - code: 业务状态码(0表示成功,非0表示业务错误) -// - message: 响应消息 -// - data: 响应数据 +### 6. 访问原始对象 + +如果需要访问原始的`ResponseWriter`或`Request`: + +```go +func handler(h *commonhttp.Handler) { + // 获取原始ResponseWriter + w := h.ResponseWriter() + + // 获取原始Request + r := h.Request() + + // 获取Context + ctx := h.Context() +} ``` ## 完整示例 @@ -197,118 +270,159 @@ http.WriteJSON(w, http.StatusOK, 0, "success", data) package main import ( + "log" "net/http" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" ) +// 用户结构 +type User struct { + ID int64 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + // 用户列表接口 -func GetUserList(w http.ResponseWriter, r *http.Request) { +func GetUserList(h *commonhttp.Handler) { // 获取分页参数 - page, pageSize := http.GetPaginationParams(r) + pagination := h.ParsePaginationRequest() + page := pagination.GetPage() + pageSize := pagination.GetSize() // 获取查询参数 - keyword := http.GetQuery(r, "keyword", "") + keyword := h.GetQuery("keyword", "") // 查询数据 users, total := queryUsers(keyword, page, pageSize) // 返回分页响应 - http.SuccessPage(w, users, total, page, pageSize) + h.SuccessPage(users, total, page, pageSize) } // 创建用户接口 -func CreateUser(w http.ResponseWriter, r *http.Request) { +func CreateUser(h *commonhttp.Handler) { // 解析请求体 var req struct { Name string `json:"name"` Email string `json:"email"` } - if err := http.ParseJSON(r, &req); err != nil { - http.BadRequest(w, "请求参数解析失败") + if err := h.ParseJSON(&req); err != nil { + h.WriteJSON(http.StatusBadRequest, 400, "请求参数解析失败", nil) return } // 参数验证 if req.Name == "" { - http.Error(w, 1001, "用户名不能为空") + h.Error(1001, "用户名不能为空") return } // 创建用户 user, err := createUser(req.Name, req.Email) if err != nil { - http.SystemError(w, "创建用户失败") + h.SystemError("创建用户失败") return } // 返回成功响应 - http.SuccessWithMessage(w, "创建成功", user) + h.SuccessWithMessage("创建成功", user) } // 获取用户详情接口 -func GetUser(w http.ResponseWriter, r *http.Request) { - // 获取路径参数(需要配合路由框架使用) - id := http.GetQueryInt64(r, "id", 0) +func GetUser(h *commonhttp.Handler) { + // 获取查询参数 + id := h.GetQueryInt64("id", 0) if id == 0 { - http.BadRequest(w, "用户ID不能为空") + h.WriteJSON(http.StatusBadRequest, 400, "用户ID不能为空", nil) return } // 查询用户 user, err := getUserByID(id) if err != nil { - http.SystemError(w, "查询用户失败") + h.SystemError("查询用户失败") return } if user == nil { - http.Error(w, 1002, "用户不存在") + h.Error(1002, "用户不存在") return } - http.Success(w, user) + h.Success(user) +} + +func main() { + // 使用HandleFunc包装器(推荐) + http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { + switch h.Request().Method { + case http.MethodGet: + GetUserList(h) + case http.MethodPost: + CreateUser(h) + default: + h.WriteJSON(http.StatusMethodNotAllowed, 405, "方法不支持", nil) + } + })) + + http.HandleFunc("/user", commonhttp.HandleFunc(GetUser)) + + log.Println("Server started on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) } ``` ## API 参考 -### 响应方法 +### Handler结构 -#### Success(w http.ResponseWriter, data interface{}) +Handler封装了`ResponseWriter`和`Request`,提供更简洁的API。 + +```go +type Handler struct { + w http.ResponseWriter + r *http.Request +} +``` + +### 创建Handler + +#### NewHandler(w http.ResponseWriter, r *http.Request) *Handler + +创建Handler实例。 + +#### HandleFunc(fn func(*Handler)) http.HandlerFunc + +将Handler函数转换为标准的http.HandlerFunc(便捷包装器)。 + +**示例:** +```go +http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { + h.Success(data) +})) +``` + +### Handler响应方法 + +#### (h *Handler) Success(data interface{}) 成功响应,HTTP 200,业务code 0。 -#### SuccessWithMessage(w http.ResponseWriter, message string, data interface{}) +#### (h *Handler) SuccessWithMessage(message string, data interface{}) 带消息的成功响应。 -#### Error(w http.ResponseWriter, code int, message string) +#### (h *Handler) Error(code int, message string) 业务错误响应,HTTP 200,业务code非0。 -#### SystemError(w http.ResponseWriter, message string) +#### (h *Handler) SystemError(message string) 系统错误响应,HTTP 500,业务code 500。 -#### BadRequest(w http.ResponseWriter, message string) - -请求错误响应,HTTP 400。 - -#### Unauthorized(w http.ResponseWriter, message string) - -未授权响应,HTTP 401。 - -#### Forbidden(w http.ResponseWriter, message string) - -禁止访问响应,HTTP 403。 - -#### NotFound(w http.ResponseWriter, message string) - -未找到响应,HTTP 404。 - -#### WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{}) +#### (h *Handler) WriteJSON(httpCode, code int, message string, data interface{}) 写入JSON响应(自定义)。 @@ -318,69 +432,113 @@ func GetUser(w http.ResponseWriter, r *http.Request) { - `message`: 响应消息 - `data`: 响应数据 -#### SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int) +#### (h *Handler) SuccessPage(list interface{}, total int64, page, pageSize int, message ...string) 分页成功响应。 -#### SuccessPageWithMessage(w http.ResponseWriter, message string, list interface{}, total int64, page, pageSize int) +**参数:** +- `list`: 数据列表 +- `total`: 总记录数 +- `page`: 当前页码 +- `pageSize`: 每页大小 +- `message`: 响应消息(可选,如果为空则使用默认消息 "success") -带消息的分页成功响应。 +### Handler请求解析方法 -### 请求方法 - -#### ParseJSON(r *http.Request, v interface{}) error +#### (h *Handler) ParseJSON(v interface{}) error 解析JSON请求体。 -#### GetQuery(r *http.Request, key, defaultValue string) string +#### (h *Handler) GetQuery(key, defaultValue string) string 获取查询参数(字符串)。 -#### GetQueryInt(r *http.Request, key string, defaultValue int) int +#### (h *Handler) GetQueryInt(key string, defaultValue int) int 获取查询参数(整数)。 -#### GetQueryInt64(r *http.Request, key string, defaultValue int64) int64 +#### (h *Handler) GetQueryInt64(key string, defaultValue int64) int64 获取查询参数(int64)。 -#### GetQueryBool(r *http.Request, key string, defaultValue bool) bool +#### (h *Handler) GetQueryBool(key string, defaultValue bool) bool 获取查询参数(布尔值)。 -#### GetQueryFloat64(r *http.Request, key string, defaultValue float64) float64 +#### (h *Handler) GetQueryFloat64(key string, defaultValue float64) float64 获取查询参数(浮点数)。 -#### GetFormValue(r *http.Request, key, defaultValue string) string +#### (h *Handler) GetFormValue(key, defaultValue string) string 获取表单值(字符串)。 -#### GetFormInt(r *http.Request, key string, defaultValue int) int +#### (h *Handler) GetFormInt(key string, defaultValue int) int 获取表单值(整数)。 -#### GetFormInt64(r *http.Request, key string, defaultValue int64) int64 +#### (h *Handler) GetFormInt64(key string, defaultValue int64) int64 获取表单值(int64)。 -#### GetFormBool(r *http.Request, key string, defaultValue bool) bool +#### (h *Handler) GetFormBool(key string, defaultValue bool) bool 获取表单值(布尔值)。 -#### GetHeader(r *http.Request, key, defaultValue string) string +#### (h *Handler) GetHeader(key, defaultValue string) string 获取请求头。 -#### GetPaginationParams(r *http.Request) (page, pageSize int) +#### (h *Handler) ParsePaginationRequest() *PaginationRequest -获取分页参数。 +从请求中解析分页参数。 -**返回:** page(页码,最小1),pageSize(每页大小,最小1,最大1000) +**说明:** +- 支持从查询参数和form表单中解析 +- 优先级:查询参数 > form表单 +- 如果请求体是JSON格式且包含分页字段,建议先使用`ParseJSON`解析完整请求体到包含`PaginationRequest`的结构体中 -#### GetOffset(page, pageSize int) int +#### (h *Handler) GetTimezone() string -根据页码和每页大小计算偏移量。 +从请求的context中获取时区。 + +**说明:** +- 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 +- 如果未设置,返回默认时区 AsiaShanghai + +### Handler访问原始对象 + +#### (h *Handler) ResponseWriter() http.ResponseWriter + +获取原始的ResponseWriter(需要时使用)。 + +#### (h *Handler) Request() *http.Request + +获取原始的Request(需要时使用)。 + +#### (h *Handler) Context() context.Context + +获取请求的Context。 + +### 分页请求结构 + +#### PaginationRequest + +分页请求结构,支持从JSON和form中解析分页参数。 + +**字段:** +- `Page`: 页码(默认1) +- `Size`: 每页数量(兼容旧版本) +- `PageSize`: 每页数量(推荐使用,优先于Size) + +**方法:** +- `GetPage() int`: 获取页码,如果未设置则返回默认值1 +- `GetSize() int`: 获取每页数量,优先使用PageSize,如果未设置则使用Size,默认20,最大100 +- `GetOffset() int`: 计算数据库查询的偏移量 + +#### ParsePaginationRequest(r *http.Request) *PaginationRequest + +从请求中解析分页参数(内部函数,Handler内部使用)。 ## 状态码说明 @@ -406,18 +564,21 @@ func GetUser(w http.ResponseWriter, r *http.Request) { 2. **分页参数限制**: - page最小值为1 - - pageSize最小值为1,最大值为1000 + - pageSize最小值为1,最大值为100 3. **响应格式统一**: - 所有响应都遵循标准结构 - timestamp为Unix时间戳(秒) 4. **错误处理**: - - 使用Error方法返回业务错误 - - 使用SystemError返回系统错误 - - 使用BadRequest等返回HTTP级别的错误 + - 使用`Error`方法返回业务错误(HTTP 200,业务code非0) + - 使用`SystemError`返回系统错误(HTTP 500) + - 其他HTTP错误状态码(400, 401, 403, 404等)使用`WriteJSON`方法直接指定 + +5. **黑盒模式**: + - 所有功能都通过Handler对象调用,无需传递`w`和`r`参数 + - 代码更简洁,减少调用方工作量 ## 示例 -完整示例请参考 `examples/http_example.go` - +完整示例请参考 `examples/http_handler_example.go` diff --git a/docs/middleware.md b/docs/middleware.md index e4c0679..22d355f 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -121,7 +121,7 @@ corsHandler := middleware.CORS(corsConfig)(handler) - 从请求头 `X-Timezone` 读取时区 - 如果未传递时区信息,使用默认时区 `AsiaShanghai` -- 时区信息存储到context中,可通过 `http.GetTimezone()` 获取 +- 时区信息存储到context中,可通过Handler的`GetTimezone()`方法获取 - 自动验证时区有效性,无效时区会回退到默认时区 ### 使用方法 @@ -132,19 +132,21 @@ corsHandler := middleware.CORS(corsConfig)(handler) import ( "net/http" "git.toowon.com/jimmy/go-common/middleware" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" "git.toowon.com/jimmy/go-common/datetime" ) func handler(w http.ResponseWriter, r *http.Request) { - // 从context获取时区 - timezone := http.GetTimezone(r) + h := commonhttp.NewHandler(w, r) + + // 从Handler获取时区 + timezone := h.GetTimezone() // 使用时区 now := datetime.Now(timezone) datetime.FormatDateTime(now, timezone) - http.Success(w, map[string]interface{}{ + h.Success(map[string]interface{}{ "timezone": timezone, "time": datetime.FormatDateTime(now), }) @@ -183,13 +185,15 @@ func main() { ```go import ( "net/http" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" "git.toowon.com/jimmy/go-common/datetime" ) func GetUserList(w http.ResponseWriter, r *http.Request) { - // 从请求context获取时区 - timezone := http.GetTimezone(r) + h := commonhttp.NewHandler(w, r) + + // 从Handler获取时区 + timezone := h.GetTimezone() // 使用时区进行时间处理 now := datetime.Now(timezone) @@ -199,7 +203,7 @@ func GetUserList(w http.ResponseWriter, r *http.Request) { endTime := datetime.EndOfDay(now, timezone) // 返回数据 - http.Success(w, map[string]interface{}{ + h.Success(map[string]interface{}{ "timezone": timezone, "startTime": datetime.FormatDateTime(startTime), "endTime": datetime.FormatDateTime(endTime), @@ -283,16 +287,18 @@ import ( "net/http" "git.toowon.com/jimmy/go-common/middleware" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" "git.toowon.com/jimmy/go-common/datetime" ) func apiHandler(w http.ResponseWriter, r *http.Request) { - // 获取时区 - timezone := http.GetTimezone(r) + h := commonhttp.NewHandler(w, r) + + // 从Handler获取时区 + timezone := h.GetTimezone() now := datetime.Now(timezone) - http.Success(w, map[string]interface{}{ + h.Success(map[string]interface{}{ "message": "Hello", "timezone": timezone, "time": datetime.FormatDateTime(now), @@ -332,7 +338,7 @@ import ( "net/http" "git.toowon.com/jimmy/go-common/middleware" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" ) func main() { @@ -352,15 +358,17 @@ func main() { } func getUsers(w http.ResponseWriter, r *http.Request) { - timezone := http.GetTimezone(r) + h := commonhttp.NewHandler(w, r) + timezone := h.GetTimezone() // 处理逻辑 - http.Success(w, nil) + h.Success(nil) } func getPosts(w http.ResponseWriter, r *http.Request) { - timezone := http.GetTimezone(r) + h := commonhttp.NewHandler(w, r) + timezone := h.GetTimezone() // 处理逻辑 - http.Success(w, nil) + h.Success(nil) } ``` diff --git a/examples/factory_example.go b/examples/factory_example.go index 58792be..f5acdb1 100644 --- a/examples/factory_example.go +++ b/examples/factory_example.go @@ -4,9 +4,10 @@ import ( "context" "fmt" "log" + "os" + "time" "git.toowon.com/jimmy/go-common/factory" - "github.com/redis/go-redis/v9" ) func main() { @@ -16,80 +17,148 @@ func main() { log.Fatal("Failed to create factory:", err) } - // 直接获取数据库对象(已初始化,可直接使用) + ctx := context.Background() + + // ========== 日志记录(黑盒模式,推荐) ========== + fac.LogInfo("应用启动") + fac.LogDebug("调试信息: %s", "test") + fac.LogWarn("警告信息") + fac.LogError("错误信息: %v", fmt.Errorf("test error")) + + // 带字段的日志 + fac.LogInfof(map[string]interface{}{ + "user_id": 123, + "ip": "192.168.1.1", + }, "用户登录成功") + + fac.LogErrorf(map[string]interface{}{ + "error_code": 1001, + "user_id": 123, + }, "登录失败: %v", fmt.Errorf("invalid password")) + + // ========== 邮件发送(黑盒模式,推荐) ========== + err = fac.SendEmail( + []string{"user@example.com"}, + "验证码", + "您的验证码是:123456", + ) + if err != nil { + fac.LogError("发送邮件失败: %v", err) + } else { + fac.LogInfo("邮件发送成功") + } + + // HTML邮件 + err = fac.SendEmail( + []string{"user@example.com"}, + "欢迎", + "纯文本内容", + "

HTML内容

", + ) + if err != nil { + fac.LogError("发送HTML邮件失败: %v", err) + } + + // ========== 短信发送(黑盒模式,推荐) ========== + resp, err := fac.SendSMS( + []string{"13800138000"}, + map[string]string{"code": "123456"}, + ) + if err != nil { + fac.LogError("发送短信失败: %v", err) + } else { + fac.LogInfo("短信发送成功: %s", resp.RequestID) + } + + // 指定模板代码 + resp, err = fac.SendSMS( + []string{"13800138000"}, + map[string]string{"code": "123456"}, + "SMS_123456789", // 模板代码 + ) + if err != nil { + fac.LogError("发送短信失败: %v", err) + } + + // ========== 文件上传(黑盒模式,推荐,自动选择OSS或MinIO) ========== + file, err := os.Open("test.jpg") + if err == nil { + defer file.Close() + + url, err := fac.UploadFile(ctx, "images/test.jpg", file, "image/jpeg") + if err != nil { + fac.LogError("上传文件失败: %v", err) + } else { + fac.LogInfo("文件上传成功: %s", url) + } + } + + // ========== 获取文件URL(黑盒模式) ========== + // 永久有效 + url, err := fac.GetFileURL("images/test.jpg", 0) + if err != nil { + fac.LogError("获取文件URL失败: %v", err) + } else { + fac.LogInfo("文件URL: %s", url) + } + + // 临时访问URL(1小时后过期) + url, err = fac.GetFileURL("images/test.jpg", 3600) + if err != nil { + fac.LogError("获取临时URL失败: %v", err) + } else { + fac.LogInfo("临时URL: %s", url) + } + + // ========== Redis操作(黑盒模式,推荐) ========== + // 设置值(不过期) + err = fac.RedisSet(ctx, "user:123", "value") + if err != nil { + fac.LogError("Redis设置失败: %v", err) + } + + // 设置值(带过期时间) + err = fac.RedisSet(ctx, "user:123", "value", time.Hour) + if err != nil { + fac.LogError("Redis设置失败: %v", err) + } + + // 获取值 + value, err := fac.RedisGet(ctx, "user:123") + if err != nil { + fac.LogError("Redis获取失败: %v", err) + } else { + fac.LogInfo("Redis值: %s", value) + } + + // 删除键 + err = fac.RedisDelete(ctx, "user:123", "user:456") + if err != nil { + fac.LogError("Redis删除失败: %v", err) + } + + // 检查键是否存在 + exists, err := fac.RedisExists(ctx, "user:123") + if err != nil { + fac.LogError("Redis检查失败: %v", err) + } else { + fac.LogInfo("键是否存在: %v", exists) + } + + // ========== 数据库操作 ========== db, err := fac.GetDatabase() if err != nil { - log.Printf("Database not available: %v", err) + fac.LogError("数据库连接失败: %v", err) } else { - // 直接使用数据库 + // 直接使用GORM var count int64 if err := db.Table("users").Count(&count).Error; err != nil { - log.Printf("Failed to count users: %v", err) + fac.LogError("查询用户数量失败: %v", err) } else { - fmt.Printf("User count: %d\n", count) + fac.LogInfo("用户数量: %d", count) } } - // 直接获取Redis客户端(已初始化,可直接使用) - redisClient, err := fac.GetRedisClient() - if err != nil { - log.Printf("Redis not available: %v", err) - } else { - // 直接使用Redis客户端 - ctx := context.Background() - val, err := redisClient.Get(ctx, "test_key").Result() - if err != nil && err != redis.Nil { - log.Printf("Redis error: %v", err) - } else if err == redis.Nil { - fmt.Println("Redis key not found") - } else { - fmt.Printf("Redis value: %s\n", val) - } - } - // 获取邮件客户端(已初始化,可直接使用) - emailClient, err := fac.GetEmailClient() - if err != nil { - log.Printf("Email client not available: %v", err) - } else { - // 直接使用 - err = emailClient.SendSimple( - []string{"recipient@example.com"}, - "测试邮件", - "这是测试内容", - ) - if err != nil { - log.Printf("Failed to send email: %v", err) - } else { - fmt.Println("Email sent successfully") - } - } - - // 获取短信客户端(已初始化,可直接使用) - smsClient, err := fac.GetSMSClient() - if err != nil { - log.Printf("SMS client not available: %v", err) - } else { - // 直接使用 - resp, err := smsClient.SendSimple( - []string{"13800138000"}, - map[string]string{"code": "123456"}, - ) - if err != nil { - log.Printf("Failed to send SMS: %v", err) - } else { - fmt.Printf("SMS sent: %s\n", resp.RequestID) - } - } - - // 获取日志记录器(已初始化,可直接使用) - logger, err := fac.GetLogger() - if err != nil { - log.Printf("Logger not available: %v", err) - } else { - logger.Info("Application started") - logger.Debug("Debug message") - logger.Warn("Warning message") - logger.Error("Error message") - } + fac.LogInfo("示例执行完成") } diff --git a/examples/http_example.go b/examples/http_example.go deleted file mode 100644 index 8e7d490..0000000 --- a/examples/http_example.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "log" - "net/http" - - "git.toowon.com/jimmy/go-common/http" -) - -// 用户结构 -type User struct { - ID int64 `json:"id"` - Name string `json:"name"` - Email string `json:"email"` -} - -// 获取用户列表 -func GetUserList(w http.ResponseWriter, r *http.Request) { - // 获取分页参数 - page, pageSize := http.GetPaginationParams(r) - - // 获取查询参数 - keyword := http.GetQuery(r, "keyword", "") - - // 模拟查询数据 - users := []User{ - {ID: 1, Name: "User1", Email: "user1@example.com"}, - {ID: 2, Name: "User2", Email: "user2@example.com"}, - } - total := int64(100) - - // 返回分页响应 - http.SuccessPage(w, users, total, page, pageSize) -} - -// 创建用户 -func CreateUser(w http.ResponseWriter, r *http.Request) { - // 解析请求体 - var req struct { - Name string `json:"name"` - Email string `json:"email"` - } - - if err := http.ParseJSON(r, &req); err != nil { - http.BadRequest(w, "请求参数解析失败") - return - } - - // 参数验证 - if req.Name == "" { - http.Error(w, 1001, "用户名不能为空") - return - } - - // 模拟创建用户 - user := User{ - ID: 1, - Name: req.Name, - Email: req.Email, - } - - // 返回成功响应 - http.SuccessWithMessage(w, "创建成功", user) -} - -// 获取用户详情 -func GetUser(w http.ResponseWriter, r *http.Request) { - // 获取查询参数 - id := http.GetQueryInt64(r, "id", 0) - - if id == 0 { - http.BadRequest(w, "用户ID不能为空") - return - } - - // 模拟查询用户 - if id == 1 { - user := User{ID: 1, Name: "User1", Email: "user1@example.com"} - http.Success(w, user) - } else { - http.Error(w, 1002, "用户不存在") - } -} - -func main() { - http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - GetUserList(w, r) - case http.MethodPost: - CreateUser(w, r) - default: - http.NotFound(w, "方法不支持") - } - }) - - http.HandleFunc("/user", GetUser) - - log.Println("Server started on :8080") - log.Fatal(http.ListenAndServe(":8080", nil)) -} diff --git a/examples/http_handler_example.go b/examples/http_handler_example.go new file mode 100644 index 0000000..adc0540 --- /dev/null +++ b/examples/http_handler_example.go @@ -0,0 +1,109 @@ +package main + +import ( + "log" + "net/http" + + commonhttp "git.toowon.com/jimmy/go-common/http" +) + +// 用户结构 +type User struct { + ID int64 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +// 获取用户列表(使用Handler黑盒模式) +func GetUserList(h *commonhttp.Handler) { + // 获取分页参数(简洁方式) + pagination := h.ParsePaginationRequest() + page := pagination.GetPage() + pageSize := pagination.GetSize() + + // 获取查询参数(简洁方式) + _ = h.GetQuery("keyword", "") // 示例:获取查询参数 + + // 模拟查询数据 + users := []User{ + {ID: 1, Name: "User1", Email: "user1@example.com"}, + {ID: 2, Name: "User2", Email: "user2@example.com"}, + } + total := int64(100) + + // 返回分页响应(简洁方式) + h.SuccessPage(users, total, page, pageSize) +} + +// 创建用户(使用Handler黑盒模式) +func CreateUser(h *commonhttp.Handler) { + // 解析请求体(简洁方式) + var req struct { + Name string `json:"name"` + Email string `json:"email"` + } + + if err := h.ParseJSON(&req); err != nil { + h.WriteJSON(http.StatusBadRequest, 400, "请求参数解析失败", nil) + return + } + + // 参数验证 + if req.Name == "" { + h.Error(1001, "用户名不能为空") + return + } + + // 模拟创建用户 + user := User{ + ID: 1, + Name: req.Name, + Email: req.Email, + } + + // 返回成功响应(简洁方式) + h.SuccessWithMessage("创建成功", user) +} + +// 获取用户详情(使用Handler黑盒模式) +func GetUser(h *commonhttp.Handler) { + // 获取查询参数(简洁方式) + id := h.GetQueryInt64("id", 0) + + if id == 0 { + h.WriteJSON(http.StatusBadRequest, 400, "用户ID不能为空", nil) + return + } + + // 模拟查询用户 + if id == 1 { + user := User{ID: 1, Name: "User1", Email: "user1@example.com"} + h.Success(user) + } else { + h.Error(1002, "用户不存在") + } +} + +func main() { + // 方式1:使用HandleFunc包装器(推荐,最简洁) + http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { + switch h.Request().Method { + case http.MethodGet: + GetUserList(h) + case http.MethodPost: + CreateUser(h) + default: + h.WriteJSON(http.StatusMethodNotAllowed, 405, "方法不支持", nil) + } + })) + + // 方式2:手动创建Handler(需要更多控制时) + http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { + h := commonhttp.NewHandler(w, r) + GetUser(h) + }) + + log.Println("Server started on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + diff --git a/examples/http_pagination_example.go b/examples/http_pagination_example.go new file mode 100644 index 0000000..f320042 --- /dev/null +++ b/examples/http_pagination_example.go @@ -0,0 +1,61 @@ +package main + +import ( + "log" + "net/http" + + commonhttp "git.toowon.com/jimmy/go-common/http" +) + +// ListUserRequest 用户列表请求(包含分页字段) +type ListUserRequest struct { + Keyword string `json:"keyword"` + commonhttp.PaginationRequest // 嵌入分页请求结构 +} + +// User 用户结构 +type User struct { + ID int64 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +// 获取用户列表(使用Handler和PaginationRequest) +func GetUserList(h *commonhttp.Handler) { + var req ListUserRequest + + // 方式1:从JSON请求体解析(分页字段会自动解析) + if h.Request().Method == http.MethodPost { + if err := h.ParseJSON(&req); err != nil { + h.WriteJSON(http.StatusBadRequest, 400, "请求参数解析失败", nil) + return + } + } else { + // 方式2:从查询参数解析分页 + pagination := h.ParsePaginationRequest() + req.PaginationRequest = *pagination + req.Keyword = h.GetQuery("keyword", "") + } + + // 使用分页方法 + page := req.GetPage() // 获取页码(默认1) + size := req.GetSize() // 获取每页数量(默认20,最大100) + _ = req.GetOffset() // 计算偏移量 + + // 模拟查询数据 + users := []User{ + {ID: 1, Name: "User1", Email: "user1@example.com"}, + {ID: 2, Name: "User2", Email: "user2@example.com"}, + } + total := int64(100) + + // 返回分页响应 + h.SuccessPage(users, total, page, size) +} + +func main() { + http.HandleFunc("/users", commonhttp.HandleFunc(GetUserList)) + + log.Println("Server started on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/examples/middleware_example.go b/examples/middleware_example.go index 6e6adc3..452797b 100644 --- a/examples/middleware_example.go +++ b/examples/middleware_example.go @@ -5,7 +5,7 @@ import ( "net/http" "git.toowon.com/jimmy/go-common/datetime" - "git.toowon.com/jimmy/go-common/http" + commonhttp "git.toowon.com/jimmy/go-common/http" "git.toowon.com/jimmy/go-common/middleware" ) @@ -31,8 +31,11 @@ func main() { middleware.Timezone, ) - // 定义处理器 - handler := chain.ThenFunc(apiHandler) + // 定义处理器(使用Handler模式) + handler := chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) { + h := commonhttp.NewHandler(w, r) + apiHandler(h) + }) // 注册路由 http.Handle("/api", handler) @@ -42,10 +45,10 @@ func main() { log.Fatal(http.ListenAndServe(":8080", nil)) } -// apiHandler 处理API请求 -func apiHandler(w http.ResponseWriter, r *http.Request) { - // 从context获取时区 - timezone := http.GetTimezone(r) +// apiHandler 处理API请求(使用Handler模式) +func apiHandler(h *commonhttp.Handler) { + // 从Handler获取时区 + timezone := h.GetTimezone() // 使用时区进行时间处理 now := datetime.Now(timezone) @@ -53,7 +56,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { endOfDay := datetime.EndOfDay(now, timezone) // 返回响应 - http.Success(w, map[string]interface{}{ + h.Success(map[string]interface{}{ "message": "Hello from API", "timezone": timezone, "currentTime": datetime.FormatDateTime(now), @@ -61,4 +64,3 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { "endOfDay": datetime.FormatDateTime(endOfDay), }) } - diff --git a/factory/factory.go b/factory/factory.go index 787ac3b..4087557 100644 --- a/factory/factory.go +++ b/factory/factory.go @@ -3,12 +3,14 @@ package factory import ( "context" "fmt" + "io" "time" "git.toowon.com/jimmy/go-common/config" "git.toowon.com/jimmy/go-common/email" "git.toowon.com/jimmy/go-common/logger" "git.toowon.com/jimmy/go-common/sms" + "git.toowon.com/jimmy/go-common/storage" "github.com/redis/go-redis/v9" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -18,7 +20,13 @@ import ( // Factory 工厂类,用于从配置创建各种客户端对象 type Factory struct { - cfg *config.Config + cfg *config.Config + storage storage.Storage // 存储实例(延迟初始化) + logger *logger.Logger // 日志实例(延迟初始化) + email *email.Email // 邮件客户端(延迟初始化) + sms *sms.SMS // 短信客户端(延迟初始化) + db *gorm.DB // 数据库连接(延迟初始化) + redis *redis.Client // Redis客户端(延迟初始化) } // NewFactory 创建工厂实例 @@ -38,37 +46,275 @@ func NewFactoryFromFile(filePath string) (*Factory, error) { return NewFactory(cfg), nil } -// GetEmailClient 获取邮件客户端(已初始化) -// 返回已初始化的邮件客户端对象,可直接使用 -func (f *Factory) GetEmailClient() (*email.Email, error) { +// getEmailClient 获取邮件客户端(内部方法,延迟初始化) +func (f *Factory) getEmailClient() (*email.Email, error) { + if f.email != nil { + return f.email, nil + } + if f.cfg.Email == nil { return nil, fmt.Errorf("email config is nil") } - return email.NewEmail(f.cfg.Email) + + e, err := email.NewEmail(f.cfg.Email) + if err != nil { + return nil, fmt.Errorf("failed to create email client: %w", err) + } + + f.email = e + return e, nil } -// GetSMSClient 获取短信客户端(已初始化) -// 返回已初始化的短信客户端对象,可直接使用 -func (f *Factory) GetSMSClient() (*sms.SMS, error) { +// SendEmail 发送邮件(黑盒模式) +// to: 收件人列表 +// subject: 邮件主题 +// body: 邮件正文(纯文本) +// htmlBody: HTML正文(可选,如果设置了会优先使用) +func (f *Factory) SendEmail(to []string, subject, body string, htmlBody ...string) error { + e, err := f.getEmailClient() + if err != nil { + return err + } + + msg := &email.Message{ + To: to, + Subject: subject, + Body: body, + } + + if len(htmlBody) > 0 && htmlBody[0] != "" { + msg.HTMLBody = htmlBody[0] + } + + return e.Send(msg) +} + +// getSMSClient 获取短信客户端(内部方法,延迟初始化) +func (f *Factory) getSMSClient() (*sms.SMS, error) { + if f.sms != nil { + return f.sms, nil + } + if f.cfg.SMS == nil { return nil, fmt.Errorf("SMS config is nil") } - return sms.NewSMS(f.cfg.SMS) + + s, err := sms.NewSMS(f.cfg.SMS) + if err != nil { + return nil, fmt.Errorf("failed to create SMS client: %w", err) + } + + f.sms = s + return s, nil } -// GetLogger 获取日志记录器(已初始化) -// 返回已初始化的日志记录器对象,可直接使用 -func (f *Factory) GetLogger() (*logger.Logger, error) { +// SendSMS 发送短信(黑盒模式) +// phoneNumbers: 手机号列表 +// templateParam: 模板参数(map或JSON字符串) +// templateCode: 模板代码(可选,如果为空使用配置中的模板代码) +func (f *Factory) SendSMS(phoneNumbers []string, templateParam interface{}, templateCode ...string) (*sms.SendResponse, error) { + s, err := f.getSMSClient() + if err != nil { + return nil, err + } + + req := &sms.SendRequest{ + PhoneNumbers: phoneNumbers, + TemplateParam: templateParam, + } + + if len(templateCode) > 0 && templateCode[0] != "" { + req.TemplateCode = templateCode[0] + } + + return s.Send(req) +} + +// getLogger 获取日志记录器(内部方法,延迟初始化) +func (f *Factory) getLogger() (*logger.Logger, error) { + if f.logger != nil { + return f.logger, nil + } + + var l *logger.Logger + var err error if f.cfg.Logger == nil { // 如果没有配置,使用默认配置创建 - return logger.NewLogger(nil) + l, err = logger.NewLogger(nil) + } else { + l, err = logger.NewLogger(f.cfg.Logger) } - return logger.NewLogger(f.cfg.Logger) + + if err != nil { + return nil, fmt.Errorf("failed to create logger: %w", err) + } + + f.logger = l + return l, nil } -// GetDatabase 获取数据库连接对象(已初始化) -// 返回已初始化的GORM数据库对象,可直接使用 -func (f *Factory) GetDatabase() (*gorm.DB, error) { +// LogDebug 记录调试日志 +// message: 日志消息 +// args: 格式化参数(可选) +func (f *Factory) LogDebug(message string, args ...interface{}) { + l, err := f.getLogger() + if err != nil { + // 如果日志初始化失败,使用标准输出 + if len(args) > 0 { + fmt.Printf("[DEBUG] "+message+"\n", args...) + } else { + fmt.Printf("[DEBUG] %s\n", message) + } + return + } + if len(args) > 0 { + l.Debug(message, args...) + } else { + l.Debug(message) + } +} + +// LogDebugf 记录调试日志(带字段) +// fields: 日志字段 +// message: 日志消息 +// args: 格式化参数(可选) +func (f *Factory) LogDebugf(fields map[string]interface{}, message string, args ...interface{}) { + l, err := f.getLogger() + if err != nil { + // 如果日志初始化失败,使用标准输出 + if len(args) > 0 { + fmt.Printf("[DEBUG] "+message+"\n", args...) + } else { + fmt.Printf("[DEBUG] %s\n", message) + } + return + } + l.Debugf(fields, message, args...) +} + +// LogInfo 记录信息日志 +// message: 日志消息 +// args: 格式化参数(可选) +func (f *Factory) LogInfo(message string, args ...interface{}) { + l, err := f.getLogger() + if err != nil { + // 如果日志初始化失败,使用标准输出 + if len(args) > 0 { + fmt.Printf("[INFO] "+message+"\n", args...) + } else { + fmt.Printf("[INFO] %s\n", message) + } + return + } + if len(args) > 0 { + l.Info(message, args...) + } else { + l.Info(message) + } +} + +// LogInfof 记录信息日志(带字段) +// fields: 日志字段 +// message: 日志消息 +// args: 格式化参数(可选) +func (f *Factory) LogInfof(fields map[string]interface{}, message string, args ...interface{}) { + l, err := f.getLogger() + if err != nil { + // 如果日志初始化失败,使用标准输出 + if len(args) > 0 { + fmt.Printf("[INFO] "+message+"\n", args...) + } else { + fmt.Printf("[INFO] %s\n", message) + } + return + } + l.Infof(fields, message, args...) +} + +// LogWarn 记录警告日志 +// message: 日志消息 +// args: 格式化参数(可选) +func (f *Factory) LogWarn(message string, args ...interface{}) { + l, err := f.getLogger() + if err != nil { + // 如果日志初始化失败,使用标准输出 + if len(args) > 0 { + fmt.Printf("[WARN] "+message+"\n", args...) + } else { + fmt.Printf("[WARN] %s\n", message) + } + return + } + if len(args) > 0 { + l.Warn(message, args...) + } else { + l.Warn(message) + } +} + +// LogWarnf 记录警告日志(带字段) +// fields: 日志字段 +// message: 日志消息 +// args: 格式化参数(可选) +func (f *Factory) LogWarnf(fields map[string]interface{}, message string, args ...interface{}) { + l, err := f.getLogger() + if err != nil { + // 如果日志初始化失败,使用标准输出 + if len(args) > 0 { + fmt.Printf("[WARN] "+message+"\n", args...) + } else { + fmt.Printf("[WARN] %s\n", message) + } + return + } + l.Warnf(fields, message, args...) +} + +// LogError 记录错误日志 +// message: 日志消息 +// args: 格式化参数(可选) +func (f *Factory) LogError(message string, args ...interface{}) { + l, err := f.getLogger() + if err != nil { + // 如果日志初始化失败,使用标准输出 + if len(args) > 0 { + fmt.Printf("[ERROR] "+message+"\n", args...) + } else { + fmt.Printf("[ERROR] %s\n", message) + } + return + } + if len(args) > 0 { + l.Error(message, args...) + } else { + l.Error(message) + } +} + +// LogErrorf 记录错误日志(带字段) +// fields: 日志字段 +// message: 日志消息 +// args: 格式化参数(可选) +func (f *Factory) LogErrorf(fields map[string]interface{}, message string, args ...interface{}) { + l, err := f.getLogger() + if err != nil { + // 如果日志初始化失败,使用标准输出 + if len(args) > 0 { + fmt.Printf("[ERROR] "+message+"\n", args...) + } else { + fmt.Printf("[ERROR] %s\n", message) + } + return + } + l.Errorf(fields, message, args...) +} + +// getDatabase 获取数据库连接对象(内部方法,延迟初始化) +func (f *Factory) getDatabase() (*gorm.DB, error) { + if f.db != nil { + return f.db, nil + } + if f.cfg.Database == nil { return nil, fmt.Errorf("database config is nil") } @@ -112,12 +358,23 @@ func (f *Factory) GetDatabase() (*gorm.DB, error) { sqlDB.SetConnMaxLifetime(time.Duration(f.cfg.Database.ConnMaxLifetime) * time.Second) } + f.db = db return db, nil } -// GetRedisClient 获取Redis客户端对象(已初始化) -// 返回已初始化的Redis客户端对象,可直接使用 -func (f *Factory) GetRedisClient() (*redis.Client, error) { +// GetDatabase 获取数据库连接对象(已初始化) +// 返回已初始化的GORM数据库对象,可直接使用 +// 注意:数据库保持返回GORM对象,因为GORM已经提供了很好的抽象 +func (f *Factory) GetDatabase() (*gorm.DB, error) { + return f.getDatabase() +} + +// getRedisClient 获取Redis客户端对象(内部方法,延迟初始化) +func (f *Factory) getRedisClient() (*redis.Client, error) { + if f.redis != nil { + return f.redis, nil + } + if f.cfg.Redis == nil { return nil, fmt.Errorf("redis config is nil") } @@ -169,17 +426,155 @@ func (f *Factory) GetRedisClient() (*redis.Client, error) { return nil, fmt.Errorf("failed to connect to redis: %w", err) } + f.redis = client return client, nil } -// GetRedisConfig 获取Redis配置(用于创建Redis客户端) -// 返回Redis配置对象,调用方可以使用此配置创建Redis客户端 -// 注意:推荐使用 GetRedisClient 方法直接获取已初始化的客户端 -func (f *Factory) GetRedisConfig() *config.RedisConfig { - return f.cfg.Redis +// RedisGet 获取Redis值(黑盒模式) +// key: Redis键 +func (f *Factory) RedisGet(ctx context.Context, key string) (string, error) { + client, err := f.getRedisClient() + if err != nil { + return "", err + } + + result, err := client.Get(ctx, key).Result() + if err == redis.Nil { + return "", nil // key不存在,返回空字符串 + } + if err != nil { + return "", fmt.Errorf("failed to get redis key: %w", err) + } + + return result, nil +} + +// RedisSet 设置Redis值(黑盒模式) +// key: Redis键 +// value: Redis值 +// expiration: 过期时间(可选,0表示不过期) +func (f *Factory) RedisSet(ctx context.Context, key string, value interface{}, expiration ...time.Duration) error { + client, err := f.getRedisClient() + if err != nil { + return err + } + + var exp time.Duration + if len(expiration) > 0 { + exp = expiration[0] + } + + err = client.Set(ctx, key, value, exp).Err() + if err != nil { + return fmt.Errorf("failed to set redis key: %w", err) + } + + return nil +} + +// RedisDelete 删除Redis键(黑盒模式) +// keys: Redis键列表 +func (f *Factory) RedisDelete(ctx context.Context, keys ...string) error { + if len(keys) == 0 { + return nil + } + + client, err := f.getRedisClient() + if err != nil { + return err + } + + err = client.Del(ctx, keys...).Err() + if err != nil { + return fmt.Errorf("failed to delete redis keys: %w", err) + } + + return nil +} + +// RedisExists 检查Redis键是否存在(黑盒模式) +// key: Redis键 +func (f *Factory) RedisExists(ctx context.Context, key string) (bool, error) { + client, err := f.getRedisClient() + if err != nil { + return false, err + } + + count, err := client.Exists(ctx, key).Result() + if err != nil { + return false, fmt.Errorf("failed to check redis key existence: %w", err) + } + + return count > 0, nil } // GetConfig 获取配置对象 func (f *Factory) GetConfig() *config.Config { return f.cfg } + +// getStorage 获取存储实例(内部方法,延迟初始化) +func (f *Factory) getStorage() (storage.Storage, error) { + if f.storage != nil { + return f.storage, nil + } + + // 根据配置自动选择存储类型 + // 优先级:MinIO > OSS + var storageType storage.StorageType + if f.cfg.MinIO != nil { + storageType = storage.StorageTypeMinIO + } else if f.cfg.OSS != nil { + storageType = storage.StorageTypeOSS + } else { + return nil, fmt.Errorf("no storage config found (OSS or MinIO)") + } + + // 创建存储实例 + s, err := storage.NewStorage(storageType, f.cfg) + if err != nil { + return nil, fmt.Errorf("failed to create storage: %w", err) + } + + f.storage = s + return s, nil +} + +// UploadFile 上传文件 +// ctx: 上下文 +// objectKey: 对象键(文件路径) +// reader: 文件内容 +// contentType: 文件类型(可选) +// 返回文件访问URL和错误 +func (f *Factory) UploadFile(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) (string, error) { + s, err := f.getStorage() + if err != nil { + return "", err + } + + // 上传文件 + err = s.Upload(ctx, objectKey, reader, contentType...) + if err != nil { + return "", fmt.Errorf("failed to upload file: %w", err) + } + + // 获取文件URL + url, err := s.GetURL(objectKey, 0) + if err != nil { + return "", fmt.Errorf("failed to get file URL: %w", err) + } + + return url, nil +} + +// GetFileURL 获取文件访问URL(Show方法) +// objectKey: 对象键 +// expires: 过期时间(秒),0表示永久有效 +func (f *Factory) GetFileURL(objectKey string, expires int64) (string, error) { + s, err := f.getStorage() + if err != nil { + return "", err + } + + return s.GetURL(objectKey, expires) +} diff --git a/go.mod b/go.mod index ce83448..751bea9 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,12 @@ module git.toowon.com/jimmy/go-common -go 1.21 +go 1.23.0 + +toolchain go1.24.10 require ( + github.com/minio/minio-go/v7 v7.0.97 + github.com/redis/go-redis/v9 v9.17.1 gorm.io/driver/mysql v1.5.2 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 @@ -12,16 +16,31 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect - github.com/redis/go-redis/v9 v9.17.1 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect + github.com/minio/crc64nvme v1.1.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.26.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 71b0e3d..15e0ee8 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,24 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -20,24 +31,55 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= +github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ= +github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.17.1 h1:7tl732FjYPRT9H9aNfyTwKg9iTETjWjGKEJ2t/5iWTs= github.com/redis/go-redis/v9 v9.17.1/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http/handler.go b/http/handler.go new file mode 100644 index 0000000..18df8a7 --- /dev/null +++ b/http/handler.go @@ -0,0 +1,279 @@ +package http + +import ( + "context" + "encoding/json" + "io" + "net/http" + "strconv" + + "git.toowon.com/jimmy/go-common/middleware" +) + +// Handler HTTP处理器包装器,封装ResponseWriter和Request,提供简洁的API +type Handler struct { + w http.ResponseWriter + r *http.Request +} + +// NewHandler 创建Handler实例 +func NewHandler(w http.ResponseWriter, r *http.Request) *Handler { + return &Handler{ + w: w, + r: r, + } +} + +// ResponseWriter 获取原始的ResponseWriter(需要时使用) +func (h *Handler) ResponseWriter() http.ResponseWriter { + return h.w +} + +// Request 获取原始的Request(需要时使用) +func (h *Handler) Request() *http.Request { + return h.r +} + +// Context 获取请求的Context +func (h *Handler) Context() context.Context { + return h.r.Context() +} + +// ========== 响应方法(黑盒模式) ========== + +// Success 成功响应 +// data: 响应数据,可以为nil +func (h *Handler) Success(data interface{}) { + writeJSON(h.w, http.StatusOK, 0, "success", data) +} + +// SuccessWithMessage 带消息的成功响应 +func (h *Handler) SuccessWithMessage(message string, data interface{}) { + writeJSON(h.w, http.StatusOK, 0, message, data) +} + +// Error 错误响应 +// code: 业务错误码,非0表示业务错误 +// message: 错误消息 +func (h *Handler) Error(code int, message string) { + writeJSON(h.w, http.StatusOK, code, message, nil) +} + +// SystemError 系统错误响应(返回HTTP 500) +// message: 错误消息 +func (h *Handler) SystemError(message string) { + writeJSON(h.w, http.StatusInternalServerError, 500, message, nil) +} + +// WriteJSON 写入JSON响应(自定义HTTP状态码和业务状态码) +// httpCode: HTTP状态码(200表示正常,500表示系统错误等) +// code: 业务状态码(0表示成功,非0表示业务错误) +// message: 响应消息 +// data: 响应数据 +func (h *Handler) WriteJSON(httpCode, code int, message string, data interface{}) { + writeJSON(h.w, httpCode, code, message, data) +} + +// SuccessPage 分页成功响应 +// list: 数据列表 +// total: 总记录数 +// page: 当前页码 +// pageSize: 每页大小 +// message: 响应消息(可选,如果为空则使用默认消息 "success") +func (h *Handler) SuccessPage(list interface{}, total int64, page, pageSize int, message ...string) { + msg := "success" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + + pageData := &PageData{ + List: list, + Total: total, + Page: page, + PageSize: pageSize, + } + + writeJSON(h.w, http.StatusOK, 0, msg, pageData) +} + +// ========== 请求解析方法(黑盒模式) ========== + +// ParseJSON 解析JSON请求体 +// v: 目标结构体指针 +func (h *Handler) ParseJSON(v interface{}) error { + body, err := io.ReadAll(h.r.Body) + if err != nil { + return err + } + defer h.r.Body.Close() + + if len(body) == 0 { + return nil + } + + return json.Unmarshal(body, v) +} + +// GetQuery 获取查询参数 +// key: 参数名 +// defaultValue: 默认值 +func (h *Handler) GetQuery(key, defaultValue string) string { + value := h.r.URL.Query().Get(key) + if value == "" { + return defaultValue + } + return value +} + +// GetQueryInt 获取整数查询参数 +// key: 参数名 +// defaultValue: 默认值 +func (h *Handler) GetQueryInt(key string, defaultValue int) int { + value := h.r.URL.Query().Get(key) + if value == "" { + return defaultValue + } + + intValue, err := strconv.Atoi(value) + if err != nil { + return defaultValue + } + + return intValue +} + +// GetQueryInt64 获取int64查询参数 +func (h *Handler) GetQueryInt64(key string, defaultValue int64) int64 { + value := h.r.URL.Query().Get(key) + if value == "" { + return defaultValue + } + + intValue, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return defaultValue + } + + return intValue +} + +// GetQueryBool 获取布尔查询参数 +func (h *Handler) GetQueryBool(key string, defaultValue bool) bool { + value := h.r.URL.Query().Get(key) + if value == "" { + return defaultValue + } + + boolValue, err := strconv.ParseBool(value) + if err != nil { + return defaultValue + } + + return boolValue +} + +// GetQueryFloat64 获取float64查询参数 +func (h *Handler) GetQueryFloat64(key string, defaultValue float64) float64 { + value := h.r.URL.Query().Get(key) + if value == "" { + return defaultValue + } + + floatValue, err := strconv.ParseFloat(value, 64) + if err != nil { + return defaultValue + } + + return floatValue +} + +// GetFormValue 获取表单值 +func (h *Handler) GetFormValue(key, defaultValue string) string { + value := h.r.FormValue(key) + if value == "" { + return defaultValue + } + return value +} + +// GetFormInt 获取表单整数 +func (h *Handler) GetFormInt(key string, defaultValue int) int { + value := h.r.FormValue(key) + if value == "" { + return defaultValue + } + + intValue, err := strconv.Atoi(value) + if err != nil { + return defaultValue + } + + return intValue +} + +// GetFormInt64 获取表单int64 +func (h *Handler) GetFormInt64(key string, defaultValue int64) int64 { + value := h.r.FormValue(key) + if value == "" { + return defaultValue + } + + intValue, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return defaultValue + } + + return intValue +} + +// GetFormBool 获取表单布尔值 +func (h *Handler) GetFormBool(key string, defaultValue bool) bool { + value := h.r.FormValue(key) + if value == "" { + return defaultValue + } + + boolValue, err := strconv.ParseBool(value) + if err != nil { + return defaultValue + } + + return boolValue +} + +// GetHeader 获取请求头 +func (h *Handler) GetHeader(key, defaultValue string) string { + value := h.r.Header.Get(key) + if value == "" { + return defaultValue + } + return value +} + +// ParsePaginationRequest 从请求中解析分页参数 +// 支持从查询参数和form表单中解析 +// 优先级:查询参数 > form表单 +func (h *Handler) ParsePaginationRequest() *PaginationRequest { + return ParsePaginationRequest(h.r) +} + +// GetTimezone 从请求的context中获取时区 +// 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 +// 如果未设置,返回默认时区 AsiaShanghai +func (h *Handler) GetTimezone() string { + return middleware.GetTimezoneFromContext(h.r.Context()) +} + +// HandleFunc 将Handler函数转换为标准的http.HandlerFunc +// 这样可以将Handler函数直接用于http.HandleFunc +// 示例: +// +// http.HandleFunc("/users", http.HandleFunc(func(h *http.Handler) { +// h.Success(data) +// })) +func HandleFunc(fn func(*Handler)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h := NewHandler(w, r) + fn(h) + } +} diff --git a/http/request.go b/http/request.go index 15d0811..bc14f4a 100644 --- a/http/request.go +++ b/http/request.go @@ -1,49 +1,12 @@ package http import ( - "context" - "encoding/json" - "io" "net/http" "strconv" - - "git.toowon.com/jimmy/go-common/middleware" ) -// ParseJSON 解析JSON请求体 -// r: HTTP请求 -// v: 目标结构体指针 -func ParseJSON(r *http.Request, v interface{}) error { - body, err := io.ReadAll(r.Body) - if err != nil { - return err - } - defer r.Body.Close() - - if len(body) == 0 { - return nil - } - - return json.Unmarshal(body, v) -} - -// GetQuery 获取查询参数 -// r: HTTP请求 -// key: 参数名 -// defaultValue: 默认值 -func GetQuery(r *http.Request, key, defaultValue string) string { - value := r.URL.Query().Get(key) - if value == "" { - return defaultValue - } - return value -} - -// GetQueryInt 获取整数查询参数 -// r: HTTP请求 -// key: 参数名 -// defaultValue: 默认值 -func GetQueryInt(r *http.Request, key string, defaultValue int) int { +// getQueryInt 获取整数查询参数(内部方法,供ParsePaginationRequest使用) +func getQueryInt(r *http.Request, key string, defaultValue int) int { value := r.URL.Query().Get(key) if value == "" { return defaultValue @@ -57,62 +20,8 @@ func GetQueryInt(r *http.Request, key string, defaultValue int) int { return intValue } -// GetQueryInt64 获取int64查询参数 -func GetQueryInt64(r *http.Request, key string, defaultValue int64) int64 { - value := r.URL.Query().Get(key) - if value == "" { - return defaultValue - } - - intValue, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return defaultValue - } - - return intValue -} - -// GetQueryBool 获取布尔查询参数 -func GetQueryBool(r *http.Request, key string, defaultValue bool) bool { - value := r.URL.Query().Get(key) - if value == "" { - return defaultValue - } - - boolValue, err := strconv.ParseBool(value) - if err != nil { - return defaultValue - } - - return boolValue -} - -// GetQueryFloat64 获取float64查询参数 -func GetQueryFloat64(r *http.Request, key string, defaultValue float64) float64 { - value := r.URL.Query().Get(key) - if value == "" { - return defaultValue - } - - floatValue, err := strconv.ParseFloat(value, 64) - if err != nil { - return defaultValue - } - - return floatValue -} - -// GetFormValue 获取表单值 -func GetFormValue(r *http.Request, key, defaultValue string) string { - value := r.FormValue(key) - if value == "" { - return defaultValue - } - return value -} - -// GetFormInt 获取表单整数 -func GetFormInt(r *http.Request, key string, defaultValue int) int { +// getFormInt 获取表单整数(内部方法,供ParsePaginationRequest使用) +func getFormInt(r *http.Request, key string, defaultValue int) int { value := r.FormValue(key) if value == "" { return defaultValue @@ -126,82 +35,82 @@ func GetFormInt(r *http.Request, key string, defaultValue int) int { return intValue } -// GetFormInt64 获取表单int64 -func GetFormInt64(r *http.Request, key string, defaultValue int64) int64 { - value := r.FormValue(key) - if value == "" { - return defaultValue - } - - intValue, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return defaultValue - } - - return intValue +// PaginationRequest 分页请求结构 +// 支持从JSON和form中解析分页参数 +type PaginationRequest struct { + Page int `json:"page" form:"page"` // 页码,默认1 + Size int `json:"size" form:"size"` // 每页数量(兼容旧版本) + PageSize int `json:"page_size" form:"page_size"` // 每页数量(推荐使用) } -// GetFormBool 获取表单布尔值 -func GetFormBool(r *http.Request, key string, defaultValue bool) bool { - value := r.FormValue(key) - if value == "" { - return defaultValue +// GetPage 获取页码,如果未设置则返回默认值1 +func (p *PaginationRequest) GetPage() int { + if p.Page <= 0 { + return 1 } - - boolValue, err := strconv.ParseBool(value) - if err != nil { - return defaultValue - } - - return boolValue + return p.Page } -// GetHeader 获取请求头 -func GetHeader(r *http.Request, key, defaultValue string) string { - value := r.Header.Get(key) - if value == "" { - return defaultValue +// GetSize 获取每页数量,如果未设置则返回默认值20,最大限制100 +// 优先使用 PageSize 字段,如果未设置则使用 Size 字段(兼容旧版本) +func (p *PaginationRequest) GetSize() int { + size := p.PageSize + if size <= 0 { + size = p.Size // 兼容旧版本的 Size 字段 } - return value + if size <= 0 { + return 20 // 默认20条 + } + if size > 100 { + return 100 // 最大100条 + } + return size } -// GetPaginationParams 获取分页参数 -// 返回 page, pageSize -// 默认 page=1, pageSize=10 -func GetPaginationParams(r *http.Request) (page, pageSize int) { - page = GetQueryInt(r, "page", 1) - pageSize = GetQueryInt(r, "pageSize", 10) - - // 参数校验 - if page < 1 { - page = 1 - } - if pageSize < 1 { - pageSize = 10 - } - if pageSize > 1000 { - pageSize = 1000 // 限制最大页面大小 - } - - return page, pageSize +// GetOffset 计算数据库查询的偏移量 +func (p *PaginationRequest) GetOffset() int { + return (p.GetPage() - 1) * p.GetSize() } -// GetOffset 根据页码和每页大小计算偏移量 -func GetOffset(page, pageSize int) int { - if page < 1 { - page = 1 +// getPaginationFromQuery 从查询参数获取分页参数(内部辅助方法) +func getPaginationFromQuery(r *http.Request) (page, size, pageSize int) { + page = getQueryInt(r, "page", 0) + size = getQueryInt(r, "size", 0) + pageSize = getQueryInt(r, "page_size", 0) + return +} + +// getPaginationFromForm 从form表单获取分页参数(内部辅助方法) +func getPaginationFromForm(r *http.Request) (page, size, pageSize int) { + page = getFormInt(r, "page", 0) + size = getFormInt(r, "size", 0) + pageSize = getFormInt(r, "page_size", 0) + return +} + +// ParsePaginationRequest 从请求中解析分页参数 +// 支持从查询参数和form表单中解析 +// 优先级:查询参数 > form表单 +// 注意:如果请求体是JSON格式且包含分页字段,建议先使用ParseJSON解析完整请求体到包含PaginationRequest的结构体中 +func ParsePaginationRequest(r *http.Request) *PaginationRequest { + req := &PaginationRequest{} + + // 1. 从查询参数解析(优先级最高) + req.Page, req.Size, req.PageSize = getPaginationFromQuery(r) + + // 2. 如果查询参数中没有,尝试从form表单解析 + if req.Page == 0 || (req.Size == 0 && req.PageSize == 0) { + page, size, pageSize := getPaginationFromForm(r) + if req.Page == 0 && page != 0 { + req.Page = page + } + if req.Size == 0 && size != 0 { + req.Size = size + } + if req.PageSize == 0 && pageSize != 0 { + req.PageSize = pageSize + } } - return (page - 1) * pageSize -} -// GetTimezone 从请求的context中获取时区 -// 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 -// 如果未设置,返回默认时区 AsiaShanghai -func GetTimezone(r *http.Request) string { - return middleware.GetTimezoneFromContext(r.Context()) -} - -// GetTimezoneFromContext 从context中获取时区 -func GetTimezoneFromContext(ctx context.Context) string { - return middleware.GetTimezoneFromContext(ctx) + return req } diff --git a/http/response.go b/http/response.go index c9dca1b..bddc9b9 100644 --- a/http/response.go +++ b/http/response.go @@ -30,56 +30,12 @@ type PageData struct { PageSize int `json:"pageSize"` // 每页大小 } -// Success 成功响应 -// data: 响应数据,可以为nil -func Success(w http.ResponseWriter, data interface{}) { - WriteJSON(w, http.StatusOK, 0, "success", data) -} - -// SuccessWithMessage 带消息的成功响应 -func SuccessWithMessage(w http.ResponseWriter, message string, data interface{}) { - WriteJSON(w, http.StatusOK, 0, message, data) -} - -// Error 错误响应 -// code: 业务错误码,非0表示业务错误 -// message: 错误消息 -func Error(w http.ResponseWriter, code int, message string) { - WriteJSON(w, http.StatusOK, code, message, nil) -} - -// SystemError 系统错误响应(返回HTTP 500) -// message: 错误消息 -func SystemError(w http.ResponseWriter, message string) { - WriteJSON(w, http.StatusInternalServerError, 500, message, nil) -} - -// BadRequest 请求错误响应(HTTP 400) -func BadRequest(w http.ResponseWriter, message string) { - WriteJSON(w, http.StatusBadRequest, 400, message, nil) -} - -// Unauthorized 未授权响应(HTTP 401) -func Unauthorized(w http.ResponseWriter, message string) { - WriteJSON(w, http.StatusUnauthorized, 401, message, nil) -} - -// Forbidden 禁止访问响应(HTTP 403) -func Forbidden(w http.ResponseWriter, message string) { - WriteJSON(w, http.StatusForbidden, 403, message, nil) -} - -// NotFound 未找到响应(HTTP 404) -func NotFound(w http.ResponseWriter, message string) { - WriteJSON(w, http.StatusNotFound, 404, message, nil) -} - -// WriteJSON 写入JSON响应 +// writeJSON 写入JSON响应(内部方法) // httpCode: HTTP状态码(200表示正常,500表示系统错误等) // code: 业务状态码(0表示成功,非0表示业务错误) // message: 响应消息 // data: 响应数据 -func WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{}) { +func writeJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{}) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(httpCode) @@ -92,47 +48,3 @@ func WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data i json.NewEncoder(w).Encode(response) } - -// SuccessPage 分页成功响应 -// list: 数据列表 -// total: 总记录数 -// page: 当前页码 -// pageSize: 每页大小 -func SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusOK) - - response := PageResponse{ - Code: 0, - Message: "success", - Timestamp: time.Now().Unix(), - Data: &PageData{ - List: list, - Total: total, - Page: page, - PageSize: pageSize, - }, - } - - json.NewEncoder(w).Encode(response) -} - -// SuccessPageWithMessage 带消息的分页成功响应 -func SuccessPageWithMessage(w http.ResponseWriter, message string, list interface{}, total int64, page, pageSize int) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusOK) - - response := PageResponse{ - Code: 0, - Message: message, - Timestamp: time.Now().Unix(), - Data: &PageData{ - List: list, - Total: total, - Page: page, - PageSize: pageSize, - }, - } - - json.NewEncoder(w).Encode(response) -} diff --git a/storage/handler.go b/storage/handler.go index 7e7d8cc..f0ad866 100644 --- a/storage/handler.go +++ b/storage/handler.go @@ -43,29 +43,31 @@ func NewUploadHandler(cfg UploadHandlerConfig) *UploadHandler { // 表单字段: file (文件) // 可选字段: prefix (对象键前缀,会覆盖配置中的前缀) func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + handler := commonhttp.NewHandler(w, r) + if r.Method != http.MethodPost { - commonhttp.NotFound(w, "Method not allowed") + handler.Error(4001, "Method not allowed") return } // 解析multipart表单 err := r.ParseMultipartForm(h.maxFileSize) if err != nil { - commonhttp.BadRequest(w, fmt.Sprintf("Failed to parse form: %v", err)) + handler.Error(4002, fmt.Sprintf("Failed to parse form: %v", err)) return } // 获取文件 file, header, err := r.FormFile("file") if err != nil { - commonhttp.BadRequest(w, fmt.Sprintf("Failed to get file: %v", err)) + handler.Error(4003, fmt.Sprintf("Failed to get file: %v", err)) return } defer file.Close() // 检查文件大小 if h.maxFileSize > 0 && header.Size > h.maxFileSize { - commonhttp.Error(w, 1001, fmt.Sprintf("File size exceeds limit: %d bytes", h.maxFileSize)) + handler.Error(1001, fmt.Sprintf("File size exceeds limit: %d bytes", h.maxFileSize)) return } @@ -80,7 +82,7 @@ func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } if !allowed { - commonhttp.Error(w, 1002, fmt.Sprintf("File extension not allowed. Allowed: %v", h.allowedExts)) + handler.Error(1002, fmt.Sprintf("File extension not allowed. Allowed: %v", h.allowedExts)) return } } @@ -108,14 +110,14 @@ func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() err = h.storage.Upload(ctx, objectKey, file, contentType) if err != nil { - commonhttp.SystemError(w, fmt.Sprintf("Failed to upload file: %v", err)) + handler.SystemError(fmt.Sprintf("Failed to upload file: %v", err)) return } // 获取文件URL fileURL, err := h.storage.GetURL(objectKey, 0) if err != nil { - commonhttp.SystemError(w, fmt.Sprintf("Failed to get file URL: %v", err)) + handler.SystemError(fmt.Sprintf("Failed to get file URL: %v", err)) return } @@ -128,7 +130,7 @@ func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { UploadTime: time.Now(), } - commonhttp.SuccessWithMessage(w, "Upload successful", result) + handler.SuccessWithMessage("Upload successful", result) } // generateUniqueFilename 生成唯一文件名 @@ -154,15 +156,17 @@ func NewProxyHandler(storage Storage) *ProxyHandler { // ServeHTTP 处理文件查看请求 // URL参数: key (对象键) func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + handler := commonhttp.NewHandler(w, r) + if r.Method != http.MethodGet { - commonhttp.NotFound(w, "Method not allowed") + handler.Error(4001, "Method not allowed") return } // 获取对象键 - objectKey := r.URL.Query().Get("key") + objectKey := handler.GetQuery("key", "") if objectKey == "" { - commonhttp.BadRequest(w, "Missing parameter: key") + handler.Error(4004, "Missing parameter: key") return } @@ -170,19 +174,19 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() exists, err := h.storage.Exists(ctx, objectKey) if err != nil { - commonhttp.SystemError(w, fmt.Sprintf("Failed to check file existence: %v", err)) + handler.SystemError(fmt.Sprintf("Failed to check file existence: %v", err)) return } if !exists { - commonhttp.NotFound(w, "File not found") + handler.Error(4005, "File not found") return } // 获取文件内容 reader, err := h.storage.GetObject(ctx, objectKey) if err != nil { - commonhttp.SystemError(w, fmt.Sprintf("Failed to get file: %v", err)) + handler.SystemError(fmt.Sprintf("Failed to get file: %v", err)) return } defer reader.Close() @@ -206,7 +210,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 复制文件内容到响应 _, err = io.Copy(w, reader) if err != nil { - commonhttp.SystemError(w, fmt.Sprintf("Failed to write response: %v", err)) + handler.SystemError(fmt.Sprintf("Failed to write response: %v", err)) return } } diff --git a/storage/minio.go b/storage/minio.go index 18e1aca..2a14d46 100644 --- a/storage/minio.go +++ b/storage/minio.go @@ -5,16 +5,20 @@ import ( "fmt" "io" "strings" + "time" "git.toowon.com/jimmy/go-common/config" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" ) // MinIOStorage MinIO存储实现 type MinIOStorage struct { - config *config.MinIOConfig - // client 存储MinIO客户端(实际使用时需要根据具体的MinIO SDK实现) - // 这里使用interface{},实际使用时需要替换为具体的客户端类型 - client interface{} + config *config.MinIOConfig + client *minio.Client + bucket string + domain string + protocol string } // NewMinIOStorage 创建MinIO存储实例 @@ -23,122 +27,138 @@ func NewMinIOStorage(cfg *config.MinIOConfig) (*MinIOStorage, error) { return nil, fmt.Errorf("MinIO config is nil") } - storage := &MinIOStorage{ - config: cfg, + // 创建MinIO客户端 + client, err := minio.New(cfg.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""), + Secure: cfg.UseSSL, + }) + if err != nil { + return nil, fmt.Errorf("failed to create MinIO client: %w", err) } - // 初始化MinIO客户端 - // 注意:这里需要根据实际的MinIO SDK实现 - // 例如使用MinIO Go SDK: - // client, err := minio.New(cfg.Endpoint, &minio.Options{ - // Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""), - // Secure: cfg.UseSSL, - // }) - // if err != nil { - // return nil, fmt.Errorf("failed to create MinIO client: %w", err) - // } - // storage.client = client + // 检查bucket是否存在,不存在则创建 + ctx := context.Background() + exists, err := client.BucketExists(ctx, cfg.Bucket) + if err != nil { + // 如果检查失败,可能是网络问题,但不阻止客户端创建 + // 继续创建客户端,后续上传时会再次检查 + } else if !exists { + err = client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{}) + if err != nil { + // 不阻止客户端创建,后续上传时会再次尝试 + } + } - return storage, nil + protocol := "http" + if cfg.UseSSL { + protocol = "https" + } + + return &MinIOStorage{ + config: cfg, + client: client, + bucket: cfg.Bucket, + domain: cfg.Domain, + protocol: protocol, + }, nil } // Upload 上传文件到MinIO func (s *MinIOStorage) Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error { - // 实现MinIO上传逻辑 - // 注意:这里需要根据实际的MinIO SDK实现 - // 示例(使用MinIO Go SDK): - // ct := "application/octet-stream" - // if len(contentType) > 0 && contentType[0] != "" { - // ct = contentType[0] - // } - // - // _, err := s.client.PutObject(ctx, s.config.Bucket, objectKey, reader, -1, minio.PutObjectOptions{ - // ContentType: ct, - // }) - // if err != nil { - // return fmt.Errorf("failed to upload object: %w", err) - // } + if s.client == nil { + return fmt.Errorf("MinIO client is not initialized") + } - // 当前实现返回错误,提示需要实现具体的MinIO SDK - return fmt.Errorf("MinIO upload not implemented, please implement with actual MinIO SDK") + ct := "application/octet-stream" + if len(contentType) > 0 && contentType[0] != "" { + ct = contentType[0] + } + + opts := minio.PutObjectOptions{ + ContentType: ct, + } + + _, err := s.client.PutObject(ctx, s.bucket, objectKey, reader, -1, opts) + if err != nil { + return fmt.Errorf("failed to upload object: %w", err) + } + + return nil } // GetURL 获取MinIO文件访问URL func (s *MinIOStorage) GetURL(objectKey string, expires int64) (string, error) { - if s.config.Domain != "" { - // 使用自定义域名 - if strings.HasSuffix(s.config.Domain, "/") { - return s.config.Domain + objectKey, nil + if s.client == nil { + return "", fmt.Errorf("MinIO client is not initialized") + } + + // 如果设置了过期时间,生成预签名URL + if expires > 0 { + ctx := context.Background() + expiry := time.Duration(expires) * time.Second + presignedURL, err := s.client.PresignedGetObject(ctx, s.bucket, objectKey, expiry, nil) + if err != nil { + return "", fmt.Errorf("failed to generate presigned URL: %w", err) } - return s.config.Domain + "/" + objectKey, nil + return presignedURL.String(), nil + } + + // 使用自定义域名或默认域名 + if s.domain != "" { + // 使用自定义域名 + if strings.HasSuffix(s.domain, "/") { + return fmt.Sprintf("%s://%s%s/%s", s.protocol, s.domain, s.bucket, objectKey), nil + } + return fmt.Sprintf("%s://%s/%s/%s", s.protocol, s.domain, s.bucket, objectKey), nil } // 使用MinIO默认域名 - protocol := "http" - if s.config.UseSSL { - protocol = "https" - } - - // 构建MinIO URL // 格式: http://endpoint/bucket/objectKey - url := fmt.Sprintf("%s://%s/%s/%s", protocol, s.config.Endpoint, s.config.Bucket, objectKey) - - // 如果设置了过期时间,需要生成预签名URL - // 注意:这里需要根据实际的MinIO SDK实现 - // 示例(使用MinIO Go SDK): - // if expires > 0 { - // expiry := time.Duration(expires) * time.Second - // presignedURL, err := s.client.PresignedGetObject(ctx, s.config.Bucket, objectKey, expiry, nil) - // if err != nil { - // return "", err - // } - // return presignedURL.String(), nil - // } - - return url, nil + return fmt.Sprintf("%s://%s/%s/%s", s.protocol, s.config.Endpoint, s.bucket, objectKey), nil } // Delete 删除MinIO文件 func (s *MinIOStorage) Delete(ctx context.Context, objectKey string) error { - // 实现MinIO删除逻辑 - // 注意:这里需要根据实际的MinIO SDK实现 - // 示例(使用MinIO Go SDK): - // err := s.client.RemoveObject(ctx, s.config.Bucket, objectKey, minio.RemoveObjectOptions{}) - // if err != nil { - // return fmt.Errorf("failed to delete object: %w", err) - // } + if s.client == nil { + return fmt.Errorf("MinIO client is not initialized") + } - return fmt.Errorf("MinIO delete not implemented, please implement with actual MinIO SDK") + err := s.client.RemoveObject(ctx, s.bucket, objectKey, minio.RemoveObjectOptions{}) + if err != nil { + return fmt.Errorf("failed to delete object: %w", err) + } + + return nil } // Exists 检查MinIO文件是否存在 func (s *MinIOStorage) Exists(ctx context.Context, objectKey string) (bool, error) { - // 实现MinIO存在性检查逻辑 - // 注意:这里需要根据实际的MinIO SDK实现 - // 示例(使用MinIO Go SDK): - // _, err := s.client.StatObject(ctx, s.config.Bucket, objectKey, minio.StatObjectOptions{}) - // if err != nil { - // if minio.ToErrorResponse(err).Code == "NoSuchKey" { - // return false, nil - // } - // return false, fmt.Errorf("failed to check object existence: %w", err) - // } - // return true, nil + if s.client == nil { + return false, fmt.Errorf("MinIO client is not initialized") + } - return false, fmt.Errorf("MinIO exists check not implemented, please implement with actual MinIO SDK") + _, err := s.client.StatObject(ctx, s.bucket, objectKey, minio.StatObjectOptions{}) + if err != nil { + errResp := minio.ToErrorResponse(err) + if errResp.Code == "NoSuchKey" { + return false, nil + } + return false, fmt.Errorf("failed to check object existence: %w", err) + } + + return true, nil } // GetObject 获取MinIO文件内容 func (s *MinIOStorage) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, error) { - // 实现MinIO获取对象逻辑 - // 注意:这里需要根据实际的MinIO SDK实现 - // 示例(使用MinIO Go SDK): - // obj, err := s.client.GetObject(ctx, s.config.Bucket, objectKey, minio.GetObjectOptions{}) - // if err != nil { - // return nil, fmt.Errorf("failed to get object: %w", err) - // } - // return obj, nil + if s.client == nil { + return nil, fmt.Errorf("MinIO client is not initialized") + } - return nil, fmt.Errorf("MinIO get object not implemented, please implement with actual MinIO SDK") + obj, err := s.client.GetObject(ctx, s.bucket, objectKey, minio.GetObjectOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get object: %w", err) + } + + return obj, nil } -