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
}
-