将工厂改成黑盒模式,降低用户使用成本

This commit is contained in:
2025-11-30 15:54:27 +08:00
parent 6323b49517
commit 9b9b21e5c1
18 changed files with 2075 additions and 1046 deletions

157
README.md
View File

@@ -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"}, "主题", "纯文本", "<h1>HTML内容</h1>")
// 直接获取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/) 目录。

View File

@@ -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)
})
```
#### 配置管理

View File

@@ -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"},
"验证码",
"纯文本内容",
"<h1>HTML内容</h1>",
)
```
### 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)
// 获取临时访问URL1小时后过期
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`

View File

@@ -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页码最小1pageSize每页大小最小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`

View File

@@ -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)
}
```

View File

@@ -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"},
"欢迎",
"纯文本内容",
"<h1>HTML内容</h1>",
)
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)
}
// 临时访问URL1小时后过期
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("示例执行完成")
}

View File

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

View File

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

View File

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

View File

@@ -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),
})
}

View File

@@ -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 获取文件访问URLShow方法
// 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)
}

29
go.mod
View File

@@ -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
)

58
go.sum
View File

@@ -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=

279
http/handler.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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