diff --git a/README.md b/README.md index 50f237f..1091236 100644 --- a/README.md +++ b/README.md @@ -295,12 +295,24 @@ http.HandleFunc("/user", commonhttp.HandleFunc(GetUser)) ``` ### 日期时间 -```go -import "git.toowon.com/jimmy/go-common/datetime" +**推荐方式:通过 factory 使用(黑盒模式)** -datetime.SetDefaultTimeZone(datetime.AsiaShanghai) -now := datetime.Now() -str := datetime.FormatDateTime(now) +```go +import "git.toowon.com/jimmy/go-common/factory" + +fac, _ := factory.NewFactoryFromFile("config.json") +now := fac.Now("Asia/Shanghai") +str := fac.FormatDateTime(now) +``` + +**或者直接使用 tools 包:** + +```go +import "git.toowon.com/jimmy/go-common/tools" + +tools.SetDefaultTimeZone(tools.AsiaShanghai) +now := tools.Now() +str := tools.FormatDateTime(now) ``` 更多示例:[examples目录](./examples/) diff --git a/docs/README.md b/docs/README.md index df2d012..4481aea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,12 +57,24 @@ CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT, ...); #### 日期转换 -```go -import "git.toowon.com/jimmy/go-common/datetime" +**推荐方式:通过 factory 使用(黑盒模式)** -datetime.SetDefaultTimeZone(datetime.AsiaShanghai) -now := datetime.Now() -str := datetime.FormatDateTime(now) +```go +import "git.toowon.com/jimmy/go-common/factory" + +fac, _ := factory.NewFactoryFromFile("config.json") +now := fac.Now("Asia/Shanghai") +str := fac.FormatDateTime(now) +``` + +**或者直接使用 tools 包:** + +```go +import "git.toowon.com/jimmy/go-common/tools" + +tools.SetDefaultTimeZone(tools.AsiaShanghai) +now := tools.Now() +str := tools.FormatDateTime(now) ``` #### HTTP响应(Handler黑盒模式) diff --git a/docs/datetime.md b/docs/datetime.md index 2a5510e..809f910 100644 --- a/docs/datetime.md +++ b/docs/datetime.md @@ -4,6 +4,8 @@ 日期转换工具提供了丰富的日期时间处理功能,支持时区设定、格式转换、时间计算等常用操作。 +**重要说明**:日期时间功能位于 `tools` 包中,推荐通过 `factory` 包使用(黑盒模式),也可以直接使用 `tools` 包。 + ## 功能特性 - 支持时区设定和转换 @@ -16,13 +18,34 @@ ## 使用方法 -### 1. 设置默认时区 +### 方式1:通过 factory 使用(推荐,黑盒模式) ```go -import "git.toowon.com/jimmy/go-common/datetime" +import "git.toowon.com/jimmy/go-common/factory" + +// 创建工厂 +fac, _ := factory.NewFactoryFromFile("config.json") + +// 获取当前时间 +now := fac.Now("Asia/Shanghai") + +// 格式化时间 +str := fac.FormatDateTime(now) + +// 解析时间 +t, _ := fac.ParseDateTime("2024-01-01 12:00:00", "Asia/Shanghai") + +// 时间计算 +tomorrow := fac.AddDays(now, 1) +``` + +### 方式2:直接使用 tools 包 + +```go +import "git.toowon.com/jimmy/go-common/tools" // 设置默认时区为上海时区 -err := datetime.SetDefaultTimeZone(datetime.AsiaShanghai) +err := tools.SetDefaultTimeZone(tools.AsiaShanghai) if err != nil { log.Fatal(err) } @@ -30,166 +53,284 @@ if err != nil { ### 2. 获取当前时间 +**通过 factory:** ```go // 使用默认时区 -now := datetime.Now() +now := fac.Now() // 使用指定时区 -now := datetime.Now(datetime.AsiaShanghai) -now := datetime.Now("America/New_York") +now := fac.Now("Asia/Shanghai") +now := fac.Now("America/New_York") +``` + +**直接使用 tools:** +```go +// 使用默认时区 +now := tools.Now() + +// 使用指定时区 +now := tools.Now(tools.AsiaShanghai) +now := tools.Now("America/New_York") ``` ### 3. 解析时间字符串 +**通过 factory:** ```go // 使用默认时区解析 -t, err := datetime.Parse("2006-01-02 15:04:05", "2024-01-01 12:00:00") -if err != nil { - log.Fatal(err) -} +t, err := fac.ParseDateTime("2024-01-01 12:00:00") // 使用指定时区解析 -t, err := datetime.Parse("2006-01-02 15:04:05", "2024-01-01 12:00:00", datetime.AsiaShanghai) +t, err := fac.ParseDateTime("2024-01-01 12:00:00", "Asia/Shanghai") + +// 解析日期 +t, err := fac.ParseDate("2024-01-01") +``` + +**直接使用 tools:** +```go +// 使用默认时区解析 +t, err := tools.Parse("2006-01-02 15:04:05", "2024-01-01 12:00:00") + +// 使用指定时区解析 +t, err := tools.Parse("2006-01-02 15:04:05", "2024-01-01 12:00:00", tools.AsiaShanghai) // 使用常用格式解析 -t, err := datetime.ParseDateTime("2024-01-01 12:00:00") -t, err := datetime.ParseDate("2024-01-01") +t, err := tools.ParseDateTime("2024-01-01 12:00:00") +t, err := tools.ParseDate("2024-01-01") ``` ### 4. 格式化时间 +**通过 factory:** ```go t := time.Now() // 使用默认时区格式化 -str := datetime.Format(t, "2006-01-02 15:04:05") +str := fac.FormatDateTime(t) +str := fac.FormatDate(t) +str := fac.FormatTime(t) // 使用指定时区格式化 -str := datetime.Format(t, "2006-01-02 15:04:05", datetime.AsiaShanghai) +str := fac.FormatDateTime(t, "Asia/Shanghai") +``` + +**直接使用 tools:** +```go +t := time.Now() + +// 使用默认时区格式化 +str := tools.Format(t, "2006-01-02 15:04:05") + +// 使用指定时区格式化 +str := tools.Format(t, "2006-01-02 15:04:05", tools.AsiaShanghai) // 使用常用格式 -str := datetime.FormatDateTime(t) // "2006-01-02 15:04:05" -str := datetime.FormatDate(t) // "2006-01-02" -str := datetime.FormatTime(t) // "15:04:05" +str := tools.FormatDateTime(t) // "2006-01-02 15:04:05" +str := tools.FormatDate(t) // "2006-01-02" +str := tools.FormatTime(t) // "15:04:05" ``` ### 5. 时区转换 +**通过 factory:** ```go t := time.Now() +t2, err := tools.ToTimezone(t, "Asia/Shanghai") +``` -// 转换到指定时区 -t2, err := datetime.ToTimezone(t, datetime.AsiaShanghai) -if err != nil { - log.Fatal(err) -} +**直接使用 tools:** +```go +t := time.Now() +t2, err := tools.ToTimezone(t, tools.AsiaShanghai) ``` ### 6. Unix时间戳转换 +**通过 factory:** ```go t := time.Now() // 转换为Unix时间戳(秒) -unix := datetime.ToUnix(t) +unix := fac.ToUnix(t) // 从Unix时间戳创建时间 -t2 := datetime.FromUnix(unix) +t2 := fac.FromUnix(unix) // 转换为Unix毫秒时间戳 -unixMilli := datetime.ToUnixMilli(t) +unixMilli := fac.ToUnixMilli(t) // 从Unix毫秒时间戳创建时间 -t3 := datetime.FromUnixMilli(unixMilli) +t3 := fac.FromUnixMilli(unixMilli) +``` + +**直接使用 tools:** +```go +t := time.Now() + +// 转换为Unix时间戳(秒) +unix := tools.ToUnix(t) + +// 从Unix时间戳创建时间 +t2 := tools.FromUnix(unix) + +// 转换为Unix毫秒时间戳 +unixMilli := tools.ToUnixMilli(t) + +// 从Unix毫秒时间戳创建时间 +t3 := tools.FromUnixMilli(unixMilli) ``` ### 7. 时间计算 +**通过 factory:** ```go t := time.Now() // 添加天数 -t1 := datetime.AddDays(t, 7) +t1 := fac.AddDays(t, 7) // 添加月数 -t2 := datetime.AddMonths(t, 1) +t2 := fac.AddMonths(t, 1) // 添加年数 -t3 := datetime.AddYears(t, 1) +t3 := fac.AddYears(t, 1) +``` + +**直接使用 tools:** +```go +t := time.Now() + +// 添加天数 +t1 := tools.AddDays(t, 7) + +// 添加月数 +t2 := tools.AddMonths(t, 1) + +// 添加年数 +t3 := tools.AddYears(t, 1) ``` ### 8. 时间范围获取 +**通过 factory:** ```go t := time.Now() // 获取一天的开始时间(00:00:00) -start := datetime.StartOfDay(t) +start := fac.StartOfDay(t) // 获取一天的结束时间(23:59:59.999999999) -end := datetime.EndOfDay(t) +end := fac.EndOfDay(t) // 获取月份的开始时间 -monthStart := datetime.StartOfMonth(t) +monthStart := fac.StartOfMonth(t) // 获取月份的结束时间 -monthEnd := datetime.EndOfMonth(t) +monthEnd := fac.EndOfMonth(t) // 获取年份的开始时间 -yearStart := datetime.StartOfYear(t) +yearStart := fac.StartOfYear(t) // 获取年份的结束时间 -yearEnd := datetime.EndOfYear(t) +yearEnd := fac.EndOfYear(t) +``` + +**直接使用 tools:** +```go +t := time.Now() + +// 获取一天的开始时间(00:00:00) +start := tools.StartOfDay(t) + +// 获取一天的结束时间(23:59:59.999999999) +end := tools.EndOfDay(t) + +// 获取月份的开始时间 +monthStart := tools.StartOfMonth(t) + +// 获取月份的结束时间 +monthEnd := tools.EndOfMonth(t) + +// 获取年份的开始时间 +yearStart := tools.StartOfYear(t) + +// 获取年份的结束时间 +yearEnd := tools.EndOfYear(t) ``` ### 9. 时间差计算 +**通过 factory:** ```go t1 := time.Now() t2 := time.Now().Add(24 * time.Hour) // 计算天数差 -days := datetime.DiffDays(t1, t2) +days := fac.DiffDays(t1, t2) // 计算小时差 -hours := datetime.DiffHours(t1, t2) +hours := fac.DiffHours(t1, t2) // 计算分钟差 -minutes := datetime.DiffMinutes(t1, t2) +minutes := fac.DiffMinutes(t1, t2) // 计算秒数差 -seconds := datetime.DiffSeconds(t1, t2) +seconds := fac.DiffSeconds(t1, t2) +``` + +**直接使用 tools:** +```go +t1 := time.Now() +t2 := time.Now().Add(24 * time.Hour) + +// 计算天数差 +days := tools.DiffDays(t1, t2) + +// 计算小时差 +hours := tools.DiffHours(t1, t2) + +// 计算分钟差 +minutes := tools.DiffMinutes(t1, t2) + +// 计算秒数差 +seconds := tools.DiffSeconds(t1, t2) ``` ### 10. 转换为UTC时间(用于数据库存储) +**直接使用 tools(factory 暂未提供):** ```go // 将任意时区的时间转换为UTC t := time.Now() // 当前时区的时间 -utcTime := datetime.ToUTC(t) +utcTime := tools.ToUTC(t) // 从指定时区转换为UTC -t, _ := datetime.ParseDateTime("2024-01-01 12:00:00", datetime.AsiaShanghai) -utcTime, err := datetime.ToUTCFromTimezone(t, datetime.AsiaShanghai) +t, _ := tools.ParseDateTime("2024-01-01 12:00:00", tools.AsiaShanghai) +utcTime, err := tools.ToUTCFromTimezone(t, tools.AsiaShanghai) // 解析时间字符串并直接转换为UTC -utcTime, err := datetime.ParseDateTimeToUTC("2024-01-01 12:00:00", datetime.AsiaShanghai) +utcTime, err := tools.ParseDateTimeToUTC("2024-01-01 12:00:00", tools.AsiaShanghai) // 解析日期并转换为UTC(当天的00:00:00 UTC) -utcTime, err := datetime.ParseDateToUTC("2024-01-01", datetime.AsiaShanghai) +utcTime, err := tools.ParseDateToUTC("2024-01-01", tools.AsiaShanghai) ``` ## API 参考 ### 时区常量 +**通过 tools 包使用:** + ```go +import "git.toowon.com/jimmy/go-common/tools" + const ( - UTC = "UTC" - AsiaShanghai = "Asia/Shanghai" - AmericaNewYork = "America/New_York" - EuropeLondon = "Europe/London" - AsiaTokyo = "Asia/Tokyo" + tools.UTC = "UTC" + tools.AsiaShanghai = "Asia/Shanghai" + tools.AmericaNewYork = "America/New_York" + tools.EuropeLondon = "Europe/London" + tools.AsiaTokyo = "Asia/Tokyo" ) ``` @@ -208,15 +349,22 @@ CommonLayouts.RFC3339Nano = time.RFC3339Nano ### 主要函数 +**注意**:以下函数可以通过 `factory` 或 `tools` 包调用。推荐使用 `factory` 的黑盒模式。 + #### SetDefaultTimeZone(timezone string) error -设置默认时区。 +设置默认时区(仅 tools 包提供)。 **参数:** - `timezone`: 时区字符串,如 "Asia/Shanghai" **返回:** 错误信息 +**使用方式:** +```go +tools.SetDefaultTimeZone(tools.AsiaShanghai) +``` + #### Now(timezone ...string) time.Time 获取当前时间。 @@ -226,105 +374,54 @@ CommonLayouts.RFC3339Nano = time.RFC3339Nano **返回:** 时间对象 -#### Parse(layout, value string, timezone ...string) (time.Time, error) +**使用方式:** +```go +// 通过 factory +now := fac.Now("Asia/Shanghai") -解析时间字符串。 +// 直接使用 tools +now := tools.Now(tools.AsiaShanghai) +``` + +#### ParseDateTime(value string, timezone ...string) (time.Time, error) + +解析日期时间字符串(2006-01-02 15:04:05)。 **参数:** -- `layout`: 时间格式,如 "2006-01-02 15:04:05" - `value`: 时间字符串 - `timezone`: 可选,时区字符串 **返回:** 时间对象和错误信息 -#### Format(t time.Time, layout string, timezone ...string) string +**使用方式:** +```go +// 通过 factory +t, err := fac.ParseDateTime("2024-01-01 12:00:00", "Asia/Shanghai") -格式化时间。 +// 直接使用 tools +t, err := tools.ParseDateTime("2024-01-01 12:00:00", tools.AsiaShanghai) +``` + +#### FormatDateTime(t time.Time, timezone ...string) string + +格式化日期时间(2006-01-02 15:04:05)。 **参数:** - `t`: 时间对象 -- `layout`: 时间格式 - `timezone`: 可选,时区字符串 **返回:** 格式化后的时间字符串 -#### ToTimezone(t time.Time, timezone string) (time.Time, error) +**使用方式:** +```go +// 通过 factory +str := fac.FormatDateTime(t, "Asia/Shanghai") -转换时区。 +// 直接使用 tools +str := tools.FormatDateTime(t, tools.AsiaShanghai) +``` -**参数:** -- `t`: 时间对象 -- `timezone`: 目标时区 - -**返回:** 转换后的时间对象和错误信息 - -#### ToUnix(t time.Time) int64 - -转换为Unix时间戳(秒)。 - -#### FromUnix(sec int64, timezone ...string) time.Time - -从Unix时间戳创建时间。 - -#### ToUnixMilli(t time.Time) int64 - -转换为Unix毫秒时间戳。 - -#### FromUnixMilli(msec int64, timezone ...string) time.Time - -从Unix毫秒时间戳创建时间。 - -#### AddDays(t time.Time, days int) time.Time - -添加天数。 - -#### AddMonths(t time.Time, months int) time.Time - -添加月数。 - -#### AddYears(t time.Time, years int) time.Time - -添加年数。 - -#### StartOfDay(t time.Time, timezone ...string) time.Time - -获取一天的开始时间。 - -#### EndOfDay(t time.Time, timezone ...string) time.Time - -获取一天的结束时间。 - -#### StartOfMonth(t time.Time, timezone ...string) time.Time - -获取月份的开始时间。 - -#### EndOfMonth(t time.Time, timezone ...string) time.Time - -获取月份的结束时间。 - -#### StartOfYear(t time.Time, timezone ...string) time.Time - -获取年份的开始时间。 - -#### EndOfYear(t time.Time, timezone ...string) time.Time - -获取年份的结束时间。 - -#### DiffDays(t1, t2 time.Time) int - -计算两个时间之间的天数差。 - -#### DiffHours(t1, t2 time.Time) int64 - -计算两个时间之间的小时差。 - -#### DiffMinutes(t1, t2 time.Time) int64 - -计算两个时间之间的分钟差。 - -#### DiffSeconds(t1, t2 time.Time) int64 - -计算两个时间之间的秒数差。 +更多函数请参考 `factory` 包或 `tools` 包的 API 文档。 ### UTC转换函数 @@ -391,7 +488,7 @@ CommonLayouts.RFC3339Nano = time.RFC3339Nano ## 完整示例 -### 示例1:基本使用 +### 示例1:通过 factory 使用(推荐) ```go package main @@ -399,27 +496,67 @@ package main import ( "fmt" "log" - "time" - "git.toowon.com/jimmy/go-common/datetime" + "git.toowon.com/jimmy/go-common/factory" +) + +func main() { + // 创建工厂 + fac, err := factory.NewFactoryFromFile("config.json") + if err != nil { + log.Fatal(err) + } + + // 获取当前时间 + now := fac.Now("Asia/Shanghai") + fmt.Printf("Current time: %s\n", fac.FormatDateTime(now)) + + // 解析时间 + t, err := fac.ParseDateTime("2024-01-01 12:00:00", "Asia/Shanghai") + if err != nil { + log.Fatal(err) + } + + // 格式化时间 + fmt.Printf("Parsed time: %s\n", fac.FormatDateTime(t)) + + // 时间计算 + tomorrow := fac.AddDays(now, 1) + fmt.Printf("Tomorrow: %s\n", fac.FormatDate(tomorrow)) +} +``` + +### 示例2:直接使用 tools 包 + +```go +package main + +import ( + "fmt" + "log" + + "git.toowon.com/jimmy/go-common/tools" ) func main() { // 设置默认时区 - datetime.SetDefaultTimeZone(datetime.AsiaShanghai) + err := tools.SetDefaultTimeZone(tools.AsiaShanghai) + if err != nil { + log.Fatal(err) + } // 获取当前时间 - now := datetime.Now() - fmt.Printf("Current time: %s\n", datetime.FormatDateTime(now)) + now := tools.Now() + fmt.Printf("Current time: %s\n", tools.FormatDateTime(now)) // 时区转换 - t, _ := datetime.ParseDateTime("2024-01-01 12:00:00") - t2, _ := datetime.ToTimezone(t, datetime.AmericaNewYork) - fmt.Printf("Time in New York: %s\n", datetime.FormatDateTime(t2)) + t, _ := tools.ParseDateTime("2024-01-01 12:00:00") + t2, _ := tools.ToTimezone(t, tools.AmericaNewYork) + fmt.Printf("Time in New York: %s\n", tools.FormatDateTime(t2)) } ``` -### 示例2:UTC转换(数据库存储场景) +### 示例3:UTC转换(数据库存储场景) ```go package main @@ -428,34 +565,32 @@ import ( "fmt" "log" - "git.toowon.com/jimmy/go-common/datetime" + "git.toowon.com/jimmy/go-common/tools" ) func main() { // 从请求中获取时间(假设是上海时区) requestTimeStr := "2024-01-01 12:00:00" - requestTimezone := datetime.AsiaShanghai + requestTimezone := tools.AsiaShanghai // 转换为UTC时间(用于数据库存储) - dbTime, err := datetime.ParseDateTimeToUTC(requestTimeStr, requestTimezone) + dbTime, err := tools.ParseDateTimeToUTC(requestTimeStr, requestTimezone) if err != nil { log.Fatal(err) } fmt.Printf("Request time (Shanghai): %s\n", requestTimeStr) - fmt.Printf("Database time (UTC): %s\n", datetime.FormatDateTime(dbTime, datetime.UTC)) + fmt.Printf("Database time (UTC): %s\n", tools.FormatDateTime(dbTime, tools.UTC)) // 从数据库读取UTC时间,转换为用户时区显示 - userTimezone := datetime.AsiaShanghai - displayTime, err := datetime.ToTimezone(dbTime, userTimezone) + userTimezone := tools.AsiaShanghai + displayTime, err := tools.ToTimezone(dbTime, userTimezone) if err != nil { log.Fatal(err) } - fmt.Printf("Display time (Shanghai): %s\n", datetime.FormatDateTime(displayTime, userTimezone)) + fmt.Printf("Display time (Shanghai): %s\n", tools.FormatDateTime(displayTime, userTimezone)) } ``` -完整示例请参考: -- `examples/datetime_example.go` - 基本使用示例 -- `examples/datetime_utc_example.go` - UTC转换示例 +完整示例请参考 `factory` 包中的 datetime 相关方法,通过 `factory` 调用 `tools` 包中的 datetime 功能。 diff --git a/docs/factory.md b/docs/factory.md index 57fae8e..9865a97 100644 --- a/docs/factory.md +++ b/docs/factory.md @@ -26,6 +26,11 @@ | **邮件** | `SendEmail()` | `fac.SendEmail(to, subject, body)` | | **短信** | `SendSMS()` | `fac.SendSMS(phones, params)` | | **存储** | `UploadFile()`, `GetFileURL()` | `fac.UploadFile(ctx, key, file)` | +| **日期时间** | `Now()`, `ParseDateTime()`, `FormatDateTime()` 等 | `fac.Now("Asia/Shanghai")` | +| **金额计算** | `YuanToCents()`, `CentsToYuan()`, `FormatYuan()` | `fac.YuanToCents(100.5)` | +| **版本信息** | `GetVersion()` | `fac.GetVersion()` | +| **HTTP响应** | `Success()`, `Error()`, `SuccessPage()` | `fac.Success(w, data)` | +| **HTTP请求** | `ParseJSON()`, `GetQuery()`, `GetTimezone()` 等 | `fac.ParseJSON(r, &req)` | ### 🔧 高级功能:Get方法(仅在必要时使用) @@ -176,7 +181,83 @@ db.Find(&users) db.Create(&user) ``` -### 8. Redis操作(获取客户端对象) +### 8. 日期时间操作(黑盒模式) + +```go +// 获取当前时间 +now := fac.Now("Asia/Shanghai") + +// 解析时间 +t, _ := fac.ParseDateTime("2024-01-01 12:00:00", "Asia/Shanghai") + +// 格式化时间 +str := fac.FormatDateTime(now) + +// 时间计算 +tomorrow := fac.AddDays(now, 1) +startOfDay := fac.StartOfDay(now, "Asia/Shanghai") +endOfDay := fac.EndOfDay(now, "Asia/Shanghai") + +// Unix时间戳 +unix := fac.ToUnix(now) +t2 := fac.FromUnix(unix, "Asia/Shanghai") +``` + +### 9. 金额计算(黑盒模式) + +```go +// 元转分 +cents := fac.YuanToCents(100.5) // 10050 + +// 分转元 +yuan := fac.CentsToYuan(10050) // 100.5 + +// 格式化显示 +str := fac.FormatYuan(10050) // "100.50" +``` + +### 10. 版本信息(黑盒模式) + +```go +version := fac.GetVersion() +fmt.Println("当前版本:", version) +``` + +### 11. HTTP响应(黑盒模式) + +```go +import "net/http" + +// 成功响应 +fac.Success(w, data) +fac.Success(w, data, "操作成功") + +// 分页响应 +fac.SuccessPage(w, users, total, page, pageSize) + +// 错误响应 +fac.Error(w, 1001, "用户不存在") +fac.SystemError(w, "系统错误") +``` + +### 12. HTTP请求解析(黑盒模式) + +```go +import "net/http" + +// 解析JSON +var req UserRequest +fac.ParseJSON(r, &req) + +// 获取查询参数 +id := fac.GetQueryInt64(r, "id", 0) +keyword := fac.GetQuery(r, "keyword", "") + +// 获取时区(需要配合middleware.Timezone使用) +timezone := fac.GetTimezone(r) +``` + +### 13. Redis操作(获取客户端对象) ```go import ( @@ -481,6 +562,222 @@ func main() { **返回:** 配置对象 +### HTTP响应方法(黑盒模式) + +#### Success(w http.ResponseWriter, data interface{}, message ...string) + +发送成功响应。 + +**参数:** +- `w`: HTTP响应写入器 +- `data`: 响应数据 +- `message`: 可选,成功消息(如果不提供,使用默认消息) + +**示例:** +```go +fac.Success(w, user) // 使用默认消息 +fac.Success(w, user, "获取成功") // 自定义消息 +``` + +#### Error(w http.ResponseWriter, code int, message string) + +发送错误响应。 + +**参数:** +- `w`: HTTP响应写入器 +- `code`: 业务错误码 +- `message`: 错误消息 + +#### SystemError(w http.ResponseWriter, message string) + +发送系统错误响应(HTTP 500)。 + +**参数:** +- `w`: HTTP响应写入器 +- `message`: 错误消息 + +#### SuccessPage(w http.ResponseWriter, data interface{}, total int64, page, pageSize int, message ...string) + +发送分页成功响应。 + +**参数:** +- `w`: HTTP响应写入器 +- `data`: 响应数据列表 +- `total`: 总记录数 +- `page`: 当前页码 +- `pageSize`: 每页大小 +- `message`: 可选,成功消息 + +### HTTP请求方法(黑盒模式) + +#### ParseJSON(r *http.Request, v interface{}) error + +解析JSON请求体。 + +**参数:** +- `r`: HTTP请求 +- `v`: 目标结构体指针 + +#### GetQuery(r *http.Request, key, defaultValue string) string + +获取查询参数。 + +#### GetQueryInt(r *http.Request, key string, defaultValue int) int + +获取查询整数参数。 + +#### GetQueryInt64(r *http.Request, key string, defaultValue int64) int64 + +获取查询64位整数参数。 + +#### GetFormValue(r *http.Request, key, defaultValue string) string + +获取表单值。 + +#### GetTimezone(r *http.Request) string + +从请求的context中获取时区(需要配合middleware.Timezone使用)。 + +### 日期时间工具方法(黑盒模式) + +#### Now(timezone ...string) time.Time + +获取当前时间。 + +**参数:** +- `timezone`: 可选,时区字符串(如 "Asia/Shanghai"),不指定则使用默认时区 + +#### ParseDateTime(value string, timezone ...string) (time.Time, error) + +解析日期时间字符串(格式:2006-01-02 15:04:05)。 + +#### ParseDate(value string, timezone ...string) (time.Time, error) + +解析日期字符串(格式:2006-01-02)。 + +#### FormatDateTime(t time.Time, timezone ...string) string + +格式化日期时间(格式:2006-01-02 15:04:05)。 + +#### FormatDate(t time.Time, timezone ...string) string + +格式化日期(格式:2006-01-02)。 + +#### FormatTime(t time.Time, timezone ...string) string + +格式化时间(格式:15:04:05)。 + +#### ToUnix(t time.Time) int64 + +转换为Unix时间戳(秒)。 + +#### FromUnix(sec int64, timezone ...string) time.Time + +从Unix时间戳创建时间。 + +#### ToUnixMilli(t time.Time) int64 + +转换为Unix毫秒时间戳。 + +#### FromUnixMilli(msec int64, timezone ...string) time.Time + +从Unix毫秒时间戳创建时间。 + +#### AddDays(t time.Time, days int) time.Time + +添加天数。 + +#### AddMonths(t time.Time, months int) time.Time + +添加月数。 + +#### AddYears(t time.Time, years int) time.Time + +添加年数。 + +#### StartOfDay(t time.Time, timezone ...string) time.Time + +获取一天的开始时间(00:00:00)。 + +#### EndOfDay(t time.Time, timezone ...string) time.Time + +获取一天的结束时间(23:59:59.999999999)。 + +#### StartOfMonth(t time.Time, timezone ...string) time.Time + +获取月份的开始时间。 + +#### EndOfMonth(t time.Time, timezone ...string) time.Time + +获取月份的结束时间。 + +#### StartOfYear(t time.Time, timezone ...string) time.Time + +获取年份的开始时间。 + +#### EndOfYear(t time.Time, timezone ...string) time.Time + +获取年份的结束时间。 + +#### DiffDays(t1, t2 time.Time) int + +计算两个时间之间的天数差。 + +#### DiffHours(t1, t2 time.Time) int64 + +计算两个时间之间的小时差。 + +#### DiffMinutes(t1, t2 time.Time) int64 + +计算两个时间之间的分钟差。 + +#### DiffSeconds(t1, t2 time.Time) int64 + +计算两个时间之间的秒数差。 + +### 金额工具方法(黑盒模式) + +#### GetMoneyCalculator() *tools.MoneyCalculator + +获取金额计算器实例。 + +#### YuanToCents(yuan float64) int64 + +元转分。 + +**示例:** +```go +cents := fac.YuanToCents(100.5) // 返回 10050 +``` + +#### CentsToYuan(cents int64) float64 + +分转元。 + +**示例:** +```go +yuan := fac.CentsToYuan(10050) // 返回 100.5 +``` + +#### FormatYuan(cents int64) string + +格式化显示金额(分转元,保留2位小数)。 + +**示例:** +```go +str := fac.FormatYuan(10050) // 返回 "100.50" +``` + +### 版本工具方法(黑盒模式) + +#### GetVersion() string + +获取版本号。 + +**说明:** +- 优先从环境变量 `DOCKER_TAG` 或 `VERSION` 中读取 +- 如果没有设置环境变量,则使用默认版本号 + ## 设计优势 ### 优势总结 diff --git a/docs/http.md b/docs/http.md index 653f67a..e99b1fa 100644 --- a/docs/http.md +++ b/docs/http.md @@ -2,16 +2,17 @@ ## 概述 -HTTP Restful工具提供了标准化的HTTP请求和响应处理功能,采用Handler黑盒模式,封装了`ResponseWriter`和`Request`,提供简洁的API,无需每次都传递这两个参数。 +HTTP Restful工具提供了标准化的HTTP请求和响应处理功能,提供公共方法供外部调用,保持低耦合。 ## 功能特性 -- **黑盒模式**:封装`ResponseWriter`和`Request`,提供简洁的API +- **低耦合设计**:提供公共方法,不封装Handler结构 - **标准化的响应结构**:`{code, message, timestamp, data}` - **分离HTTP状态码和业务状态码** - **支持分页响应** - **提供便捷的请求参数解析方法** - **支持JSON请求体解析** +- **Factory黑盒模式**:推荐使用 `factory.Success()` 等方法 ## 响应结构 @@ -26,6 +27,18 @@ HTTP Restful工具提供了标准化的HTTP请求和响应处理功能,采用H } ``` +**结构体类型(暴露在 factory 中):** + +```go +// 在 factory 包中可以直接使用 +type Response struct { + Code int `json:"code"` // 业务状态码,0表示成功 + Message string `json:"message"` // 响应消息 + Timestamp int64 `json:"timestamp"` // 时间戳 + Data interface{} `json:"data"` // 响应数据 +} +``` + ### 分页响应结构 ```json @@ -42,9 +55,116 @@ HTTP Restful工具提供了标准化的HTTP请求和响应处理功能,采用H } ``` +**结构体类型(暴露在 factory 中):** + +```go +// 在 factory 包中可以直接使用 +type PageData struct { + List interface{} `json:"list"` // 数据列表 + Total int64 `json:"total"` // 总记录数 + Page int `json:"page"` // 当前页码 + PageSize int `json:"pageSize"` // 每页大小 +} + +type PageResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Timestamp int64 `json:"timestamp"` + Data *PageData `json:"data"` +} +``` + +### 使用暴露的结构体 + +外部项目可以直接使用 `factory.Response`、`factory.PageData` 等类型: + +```go +import "git.toowon.com/jimmy/go-common/factory" + +// 创建标准响应对象 +response := factory.Response{ + Code: 0, + Message: "success", + Data: userData, +} + +// 创建分页数据对象 +pageData := &factory.PageData{ + List: users, + Total: 100, + Page: 1, + PageSize: 20, +} + +// 传递给 Success 方法 +fac.Success(w, pageData) +``` + ## 使用方法 -### 1. 创建Handler +### 方式一:使用Factory黑盒方法(推荐)⭐ + +这是最简单的方式,直接使用 `factory.Success()` 等方法: + +```go +import ( + "net/http" + "git.toowon.com/jimmy/go-common/factory" + commonhttp "git.toowon.com/jimmy/go-common/http" +) + +func GetUser(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 获取查询参数(使用公共方法) + id := commonhttp.GetQueryInt64(r, "id", 0) + + // 返回成功响应(使用factory方法) + fac.Success(w, data) +} + +func CreateUser(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 解析JSON(使用公共方法) + var req struct { + Name string `json:"name"` + } + if err := commonhttp.ParseJSON(r, &req); err != nil { + commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil) + return + } + + // 返回成功响应(使用factory方法) + fac.Success(w, data, "创建成功") +} + +func GetUserList(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 获取分页参数(使用factory方法,推荐) + pagination := fac.ParsePaginationRequest(r) + page := pagination.GetPage() + pageSize := pagination.GetSize() + + // 获取查询参数(使用factory方法,推荐) + keyword := fac.GetQuery(r, "keyword", "") + + // 查询数据 + list, total := getDataList(keyword, page, pageSize) + + // 返回分页响应(使用factory方法) + fac.SuccessPage(w, list, total, page, pageSize) +} + +// 注册路由 +http.HandleFunc("/user", GetUser) +http.HandleFunc("/users", GetUserList) +``` + +### 方式二:直接使用公共方法 + +如果不想使用Factory,可以直接使用 `http` 包的公共方法: ```go import ( @@ -52,154 +172,110 @@ import ( commonhttp "git.toowon.com/jimmy/go-common/http" ) -// 方式1:使用HandleFunc包装器(推荐,最简洁) -func GetUser(h *commonhttp.Handler) { - id := h.GetQueryInt64("id", 0) - h.Success(data) +func GetUser(w http.ResponseWriter, r *http.Request) { + // 获取查询参数 + id := commonhttp.GetQueryInt64(r, "id", 0) + + // 返回成功响应 + commonhttp.Success(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. 成功响应 - -```go -func handler(h *commonhttp.Handler) { - // 简单成功响应(data为nil) - h.Success(nil) - - // 带数据的成功响应 - data := map[string]interface{}{ - "id": 1, - "name": "test", +func CreateUser(w http.ResponseWriter, r *http.Request) { + // 解析JSON + var req struct { + Name string `json:"name"` + } + if err := commonhttp.ParseJSON(r, &req); err != nil { + commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil) + return } - h.Success(data) - // 带消息的成功响应 - h.SuccessWithMessage("操作成功", data) + // 返回成功响应 + commonhttp.Success(w, data, "创建成功") } ``` -### 3. 错误响应 +### 成功响应 ```go -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) -} +// 使用Factory(推荐) +fac.Success(w, data) // 只有数据,使用默认消息 "success" +fac.Success(w, data, "操作成功") // 数据+消息 + +// 或直接使用公共方法 +commonhttp.Success(w, data) // 只有数据 +commonhttp.Success(w, data, "操作成功") // 数据+消息 ``` -### 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, "查询成功") -} +// 使用Factory(推荐) +fac.Error(w, 1001, "用户不存在") // 业务错误(HTTP 200,业务code非0) +fac.SystemError(w, "服务器内部错误") // 系统错误(HTTP 500) + +// 或直接使用公共方法 +commonhttp.Error(w, 1001, "用户不存在") +commonhttp.SystemError(w, "服务器内部错误") +commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数错误", nil) // 自定义HTTP状态码(仅公共方法) ``` -### 5. 解析请求 +### 分页响应 + +```go +// 使用Factory(推荐) +fac.SuccessPage(w, list, total, page, pageSize) +fac.SuccessPage(w, list, total, page, pageSize, "查询成功") + +// 或直接使用公共方法 +commonhttp.SuccessPage(w, list, total, page, pageSize) +commonhttp.SuccessPage(w, list, total, page, pageSize, "查询成功") +``` + +### 解析请求 #### 解析JSON请求体 ```go -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... +// 使用公共方法 +var req struct { + Name string `json:"name"` + Email string `json:"email"` +} + +if err := commonhttp.ParseJSON(r, &req); err != nil { + commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil) + return } ``` #### 获取查询参数 ```go -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) -} +// 使用公共方法 +name := commonhttp.GetQuery(r, "name", "") +id := commonhttp.GetQueryInt(r, "id", 0) +userId := commonhttp.GetQueryInt64(r, "userId", 0) +isActive := commonhttp.GetQueryBool(r, "isActive", false) +price := commonhttp.GetQueryFloat64(r, "price", 0.0) ``` #### 获取表单参数 ```go -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) -} +// 使用公共方法 +name := commonhttp.GetFormValue(r, "name", "") +age := commonhttp.GetFormInt(r, "age", 0) +userId := commonhttp.GetFormInt64(r, "userId", 0) +isActive := commonhttp.GetFormBool(r, "isActive", false) ``` #### 获取请求头 ```go -func handler(h *commonhttp.Handler) { - token := h.GetHeader("Authorization", "") - contentType := h.GetHeader("Content-Type", "application/json") -} +// 使用公共方法 +token := commonhttp.GetHeader(r, "Authorization", "") +contentType := commonhttp.GetHeader(r, "Content-Type", "application/json") ``` #### 获取分页参数 @@ -207,71 +283,55 @@ func handler(h *commonhttp.Handler) { **方式1:使用 PaginationRequest 结构(推荐)** ```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() // 计算偏移量 +// 定义请求结构(包含分页字段) +type ListUserRequest struct { + Keyword string `json:"keyword"` + commonhttp.PaginationRequest // 嵌入分页请求结构 } -// 或者从查询参数/form解析分页 -func handler(h *commonhttp.Handler) { - pagination := h.ParsePaginationRequest() - page := pagination.GetPage() - size := pagination.GetSize() - offset := pagination.GetOffset() +// 从JSON请求体解析(分页字段会自动解析) +var req ListUserRequest +if err := commonhttp.ParseJSON(r, &req); err != nil { + commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil) + return } + +// 使用分页方法 +page := req.GetPage() // 获取页码(默认1) +size := req.GetSize() // 获取每页数量(默认20,最大100,优先使用page_size) +offset := req.GetOffset() // 计算偏移量 +``` + +**方式2:从查询参数/form解析分页** + +```go +// 使用公共方法 +pagination := commonhttp.ParsePaginationRequest(r) +page := pagination.GetPage() +size := pagination.GetSize() +offset := pagination.GetOffset() ``` #### 获取时区 ```go -func handler(h *commonhttp.Handler) { - // 从请求的context中获取时区 - // 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 - // 如果未设置,返回默认时区 AsiaShanghai - timezone := h.GetTimezone() -} -``` - -### 6. 访问原始对象 - -如果需要访问原始的`ResponseWriter`或`Request`: - -```go -func handler(h *commonhttp.Handler) { - // 获取原始ResponseWriter - w := h.ResponseWriter() - - // 获取原始Request - r := h.Request() - - // 获取Context - ctx := h.Context() -} +// 使用公共方法 +// 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 +// 如果未设置,返回默认时区 AsiaShanghai +timezone := commonhttp.GetTimezone(r) ``` ## 完整示例 +### 使用Factory(推荐) + ```go package main import ( "log" "net/http" + "git.toowon.com/jimmy/go-common/factory" commonhttp "git.toowon.com/jimmy/go-common/http" ) @@ -283,91 +343,97 @@ type User struct { } // 用户列表接口 -func GetUserList(h *commonhttp.Handler) { - // 获取分页参数 - pagination := h.ParsePaginationRequest() +func GetUserList(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 获取分页参数(使用公共方法) + pagination := commonhttp.ParsePaginationRequest(r) page := pagination.GetPage() pageSize := pagination.GetSize() - // 获取查询参数 - keyword := h.GetQuery("keyword", "") + // 获取查询参数(使用公共方法) + keyword := commonhttp.GetQuery(r, "keyword", "") // 查询数据 users, total := queryUsers(keyword, page, pageSize) - // 返回分页响应 - h.SuccessPage(users, total, page, pageSize) + // 返回分页响应(使用factory方法) + fac.SuccessPage(w, users, total, page, pageSize) } // 创建用户接口 -func CreateUser(h *commonhttp.Handler) { - // 解析请求体 +func CreateUser(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 解析请求体(使用公共方法) var req struct { Name string `json:"name"` Email string `json:"email"` } - if err := h.ParseJSON(&req); err != nil { - h.WriteJSON(http.StatusBadRequest, 400, "请求参数解析失败", nil) + if err := commonhttp.ParseJSON(r, &req); err != nil { + fac.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil) return } // 参数验证 if req.Name == "" { - h.Error(1001, "用户名不能为空") + fac.Error(w, 1001, "用户名不能为空") return } // 创建用户 user, err := createUser(req.Name, req.Email) if err != nil { - h.SystemError("创建用户失败") + fac.SystemError(w, "创建用户失败") return } - // 返回成功响应 - h.SuccessWithMessage("创建成功", user) + // 返回成功响应(使用factory方法) + fac.Success(w, user, "创建成功") } // 获取用户详情接口 -func GetUser(h *commonhttp.Handler) { - // 获取查询参数 - id := h.GetQueryInt64("id", 0) +func GetUser(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 获取查询参数(使用factory方法,推荐) + id := fac.GetQueryInt64(r, "id", 0) if id == 0 { - h.WriteJSON(http.StatusBadRequest, 400, "用户ID不能为空", nil) + commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "用户ID不能为空", nil) return } // 查询用户 user, err := getUserByID(id) if err != nil { - h.SystemError("查询用户失败") + fac.SystemError(w, "查询用户失败") return } if user == nil { - h.Error(1002, "用户不存在") + fac.Error(w, 1002, "用户不存在") return } - h.Success(user) + // 返回成功响应(使用factory方法) + fac.Success(w, user) } func main() { - // 使用HandleFunc包装器(推荐) - http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { - switch h.Request().Method { + http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { case http.MethodGet: - GetUserList(h) + GetUserList(w, r) case http.MethodPost: - CreateUser(h) + CreateUser(w, r) default: - h.WriteJSON(http.StatusMethodNotAllowed, 405, "方法不支持", nil) + commonhttp.WriteJSON(w, http.StatusMethodNotAllowed, 405, "方法不支持", nil) } - })) + }) - http.HandleFunc("/user", commonhttp.HandleFunc(GetUser)) + http.HandleFunc("/user", GetUser) log.Println("Server started on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) @@ -376,53 +442,93 @@ func main() { ## API 参考 -### Handler结构 +### Factory HTTP响应结构体(暴露给外部项目使用) -Handler封装了`ResponseWriter`和`Request`,提供更简洁的API。 +#### Response -```go -type Handler struct { - w http.ResponseWriter - r *http.Request -} -``` +标准响应结构体,外部项目可以直接使用 `factory.Response`。 -### 创建Handler - -#### NewHandler(w http.ResponseWriter, r *http.Request) *Handler - -创建Handler实例。 - -#### HandleFunc(fn func(*Handler)) http.HandlerFunc - -将Handler函数转换为标准的http.HandlerFunc(便捷包装器)。 +**字段:** +- `Code`: 业务状态码,0表示成功 +- `Message`: 响应消息 +- `Timestamp`: 时间戳(Unix时间戳,秒) +- `Data`: 响应数据 **示例:** ```go -http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { - h.Success(data) -})) +response := factory.Response{ + Code: 0, + Message: "success", + Data: userData, +} ``` -### Handler响应方法 +#### PageData -#### (h *Handler) Success(data interface{}) +分页数据结构体,外部项目可以直接使用 `factory.PageData`。 + +**字段:** +- `List`: 数据列表 +- `Total`: 总记录数 +- `Page`: 当前页码 +- `PageSize`: 每页大小 + +**示例:** +```go +pageData := &factory.PageData{ + List: users, + Total: 100, + Page: 1, + PageSize: 20, +} +fac.Success(w, pageData) +``` + +#### PageResponse + +分页响应结构体,外部项目可以直接使用 `factory.PageResponse`。 + +**字段:** +- `Code`: 业务状态码 +- `Message`: 响应消息 +- `Timestamp`: 时间戳 +- `Data`: 分页数据(*PageData) + +### Factory HTTP响应方法(推荐使用) + +#### (f *Factory) Success(w http.ResponseWriter, data interface{}, message ...string) 成功响应,HTTP 200,业务code 0。 -#### (h *Handler) SuccessWithMessage(message string, data interface{}) +**参数:** +- `data`: 响应数据,可以为nil +- `message`: 响应消息(可选),如果为空则使用默认消息 "success" -带消息的成功响应。 +**示例:** +```go +fac.Success(w, data) // 只有数据,使用默认消息 "success" +fac.Success(w, data, "操作成功") // 数据+消息 +``` -#### (h *Handler) Error(code int, message string) +#### (f *Factory) Error(w http.ResponseWriter, code int, message string) 业务错误响应,HTTP 200,业务code非0。 -#### (h *Handler) SystemError(message string) +**示例:** +```go +fac.Error(w, 1001, "用户不存在") +``` + +#### (f *Factory) SystemError(w http.ResponseWriter, message string) 系统错误响应,HTTP 500,业务code 500。 -#### (h *Handler) WriteJSON(httpCode, code int, message string, data interface{}) +**示例:** +```go +fac.SystemError(w, "服务器内部错误") +``` + +#### (f *Factory) WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{}) 写入JSON响应(自定义)。 @@ -432,7 +538,17 @@ http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { - `message`: 响应消息 - `data`: 响应数据 -#### (h *Handler) SuccessPage(list interface{}, total int64, page, pageSize int, message ...string) +**说明:** +- 此方法不在 Factory 中,直接使用 `commonhttp.WriteJSON()` +- 用于需要自定义HTTP状态码的场景(如 400, 401, 403, 404 等) + +**示例:** +```go +commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数错误", nil) +commonhttp.WriteJSON(w, http.StatusUnauthorized, 401, "未登录", nil) +``` + +#### (f *Factory) SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int, message ...string) 分页成功响应。 @@ -443,53 +559,81 @@ http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { - `pageSize`: 每页大小 - `message`: 响应消息(可选,如果为空则使用默认消息 "success") -### Handler请求解析方法 +**示例:** +```go +fac.SuccessPage(w, users, total, page, pageSize) +fac.SuccessPage(w, users, total, page, pageSize, "查询成功") +``` -#### (h *Handler) ParseJSON(v interface{}) error +### HTTP公共方法(直接使用) + +#### WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{}) + +写入JSON响应(自定义HTTP状态码和业务状态码)。 + +**说明:** +- 此方法不在 Factory 中,直接使用 `commonhttp.WriteJSON()` +- 用于需要自定义HTTP状态码的场景(如 400, 401, 403, 404 等) + +**示例:** +```go +commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数错误", nil) +commonhttp.WriteJSON(w, http.StatusUnauthorized, 401, "未登录", nil) +``` + +#### ParseJSON(r *http.Request, v interface{}) error 解析JSON请求体。 -#### (h *Handler) GetQuery(key, defaultValue string) string +**示例:** +```go +var req CreateUserRequest +if err := commonhttp.ParseJSON(r, &req); err != nil { + // 处理错误 +} +``` + +#### GetQuery(r *http.Request, key, defaultValue string) string 获取查询参数(字符串)。 -#### (h *Handler) GetQueryInt(key string, defaultValue int) int +#### GetQueryInt(r *http.Request, key string, defaultValue int) int 获取查询参数(整数)。 -#### (h *Handler) GetQueryInt64(key string, defaultValue int64) int64 +#### GetQueryInt64(r *http.Request, key string, defaultValue int64) int64 获取查询参数(int64)。 -#### (h *Handler) GetQueryBool(key string, defaultValue bool) bool +#### GetQueryBool(r *http.Request, key string, defaultValue bool) bool 获取查询参数(布尔值)。 -#### (h *Handler) GetQueryFloat64(key string, defaultValue float64) float64 +#### GetQueryFloat64(r *http.Request, key string, defaultValue float64) float64 获取查询参数(浮点数)。 -#### (h *Handler) GetFormValue(key, defaultValue string) string +#### GetFormValue(r *http.Request, key, defaultValue string) string 获取表单值(字符串)。 -#### (h *Handler) GetFormInt(key string, defaultValue int) int +#### GetFormInt(r *http.Request, key string, defaultValue int) int 获取表单值(整数)。 -#### (h *Handler) GetFormInt64(key string, defaultValue int64) int64 +#### GetFormInt64(r *http.Request, key string, defaultValue int64) int64 获取表单值(int64)。 -#### (h *Handler) GetFormBool(key string, defaultValue bool) bool +#### GetFormBool(r *http.Request, key string, defaultValue bool) bool 获取表单值(布尔值)。 -#### (h *Handler) GetHeader(key, defaultValue string) string +#### GetHeader(r *http.Request, key, defaultValue string) string 获取请求头。 -#### (h *Handler) ParsePaginationRequest() *PaginationRequest +#### ParsePaginationRequest(r *http.Request) *PaginationRequest 从请求中解析分页参数。 @@ -498,7 +642,7 @@ http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { - 优先级:查询参数 > form表单 - 如果请求体是JSON格式且包含分页字段,建议先使用`ParseJSON`解析完整请求体到包含`PaginationRequest`的结构体中 -#### (h *Handler) GetTimezone() string +#### GetTimezone(r *http.Request) string 从请求的context中获取时区。 @@ -506,19 +650,39 @@ http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { - 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 - 如果未设置,返回默认时区 AsiaShanghai -### Handler访问原始对象 +#### Success(w http.ResponseWriter, data interface{}, message ...string) -#### (h *Handler) ResponseWriter() http.ResponseWriter +成功响应(公共方法)。 -获取原始的ResponseWriter(需要时使用)。 +**参数:** +- `data`: 响应数据,可以为nil +- `message`: 响应消息(可选),如果为空则使用默认消息 "success" -#### (h *Handler) Request() *http.Request +**示例:** +```go +commonhttp.Success(w, data) // 只有数据 +commonhttp.Success(w, data, "操作成功") // 数据+消息 +``` -获取原始的Request(需要时使用)。 +#### Error(w http.ResponseWriter, code int, message string) -#### (h *Handler) Context() context.Context +错误响应(公共方法)。 -获取请求的Context。 +#### SystemError(w http.ResponseWriter, message string) + +系统错误响应(公共方法)。 + +#### WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{}) + +写入JSON响应(公共方法,不在Factory中)。 + +**说明:** +- 用于需要自定义HTTP状态码的场景 +- 直接使用 `commonhttp.WriteJSON()`,不在 Factory 中 + +#### SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int, message ...string) + +分页成功响应(公共方法)。 ### 分页请求结构 @@ -575,10 +739,14 @@ http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { - 使用`SystemError`返回系统错误(HTTP 500) - 其他HTTP错误状态码(400, 401, 403, 404等)使用`WriteJSON`方法直接指定 -5. **黑盒模式**: - - 所有功能都通过Handler对象调用,无需传递`w`和`r`参数 - - 代码更简洁,减少调用方工作量 +5. **推荐使用Factory**: + - 使用 `factory.Success()` 等方法,代码更简洁 + - 直接使用 `http` 包的公共方法,保持低耦合 + - 不需要Handler结构,减少不必要的封装 ## 示例 -完整示例请参考 `examples/http_handler_example.go` +完整示例请参考: +- `examples/http_handler_example.go` - 使用Factory和公共方法 +- `examples/http_pagination_example.go` - 分页示例 +- `examples/factory_blackbox_example.go` - Factory黑盒模式示例 diff --git a/docs/middleware.md b/docs/middleware.md index bb6b4f6..057333a 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -136,7 +136,7 @@ import ( "net/http" "git.toowon.com/jimmy/go-common/middleware" commonhttp "git.toowon.com/jimmy/go-common/http" - "git.toowon.com/jimmy/go-common/datetime" + "git.toowon.com/jimmy/go-common/tools" ) func handler(w http.ResponseWriter, r *http.Request) { @@ -146,12 +146,12 @@ func handler(w http.ResponseWriter, r *http.Request) { timezone := h.GetTimezone() // 使用时区 - now := datetime.Now(timezone) - datetime.FormatDateTime(now, timezone) + now := tools.Now(timezone) + tools.FormatDateTime(now, timezone) h.Success(map[string]interface{}{ "timezone": timezone, - "time": datetime.FormatDateTime(now), + "time": tools.FormatDateTime(now), }) } @@ -168,7 +168,7 @@ func main() { import ( "net/http" "git.toowon.com/jimmy/go-common/middleware" - "git.toowon.com/jimmy/go-common/datetime" + "git.toowon.com/jimmy/go-common/tools" ) func handler(w http.ResponseWriter, r *http.Request) { @@ -177,7 +177,7 @@ func handler(w http.ResponseWriter, r *http.Request) { func main() { // 使用自定义默认时区 - handler := middleware.TimezoneWithDefault(datetime.UTC)(http.HandlerFunc(handler)) + handler := middleware.TimezoneWithDefault(tools.UTC)(http.HandlerFunc(handler)) http.Handle("/api", handler) http.ListenAndServe(":8080", nil) } @@ -185,11 +185,43 @@ func main() { #### 在业务代码中使用时区 +**推荐方式:通过 factory 使用(黑盒模式)** + +```go +import ( + "net/http" + "git.toowon.com/jimmy/go-common/factory" +) + +func GetUserList(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 从请求中获取时区 + timezone := fac.GetTimezone(r) + + // 使用时区进行时间处理 + now := fac.Now(timezone) + + // 查询数据时使用时区 + startTime := fac.StartOfDay(now, timezone) + endTime := fac.EndOfDay(now, timezone) + + // 返回数据 + fac.Success(w, map[string]interface{}{ + "timezone": timezone, + "startTime": fac.FormatDateTime(startTime), + "endTime": fac.FormatDateTime(endTime), + }) +} +``` + +**或者直接使用 tools 包:** + ```go import ( "net/http" commonhttp "git.toowon.com/jimmy/go-common/http" - "git.toowon.com/jimmy/go-common/datetime" + "git.toowon.com/jimmy/go-common/tools" ) func GetUserList(w http.ResponseWriter, r *http.Request) { @@ -199,17 +231,17 @@ func GetUserList(w http.ResponseWriter, r *http.Request) { timezone := h.GetTimezone() // 使用时区进行时间处理 - now := datetime.Now(timezone) + now := tools.Now(timezone) // 查询数据时使用时区 - startTime := datetime.StartOfDay(now, timezone) - endTime := datetime.EndOfDay(now, timezone) + startTime := tools.StartOfDay(now, timezone) + endTime := tools.EndOfDay(now, timezone) // 返回数据 h.Success(map[string]interface{}{ "timezone": timezone, - "startTime": datetime.FormatDateTime(startTime), - "endTime": datetime.FormatDateTime(endTime), + "startTime": tools.FormatDateTime(startTime), + "endTime": tools.FormatDateTime(endTime), }) } ``` @@ -658,7 +690,7 @@ import ( "git.toowon.com/jimmy/go-common/middleware" "git.toowon.com/jimmy/go-common/logger" commonhttp "git.toowon.com/jimmy/go-common/http" - "git.toowon.com/jimmy/go-common/datetime" + "git.toowon.com/jimmy/go-common/tools" ) func apiHandler(w http.ResponseWriter, r *http.Request) { @@ -666,12 +698,12 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { // 从Handler获取时区 timezone := h.GetTimezone() - now := datetime.Now(timezone) + now := tools.Now(timezone) h.Success(map[string]interface{}{ "message": "Hello", "timezone": timezone, - "time": datetime.FormatDateTime(now), + "time": tools.FormatDateTime(now), }) } @@ -756,7 +788,7 @@ import ( "git.toowon.com/jimmy/go-common/middleware" commonhttp "git.toowon.com/jimmy/go-common/http" - "git.toowon.com/jimmy/go-common/datetime" + "git.toowon.com/jimmy/go-common/tools" ) func apiHandler(w http.ResponseWriter, r *http.Request) { @@ -764,12 +796,12 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { // 从Handler获取时区 timezone := h.GetTimezone() - now := datetime.Now(timezone) + now := tools.Now(timezone) h.Success(map[string]interface{}{ "message": "Hello", "timezone": timezone, - "time": datetime.FormatDateTime(now), + "time": tools.FormatDateTime(now), }) } diff --git a/email/email.go b/email/email.go index a703e9d..111b4a5 100644 --- a/email/email.go +++ b/email/email.go @@ -17,37 +17,43 @@ type Email struct { } // NewEmail 创建邮件发送器 -func NewEmail(cfg *config.EmailConfig) (*Email, error) { - if cfg == nil { +func NewEmail(cfg *config.Config) *Email { + if cfg == nil || cfg.Email == nil { + return &Email{config: nil} + } + return &Email{config: cfg.Email} +} + +// getEmailConfig 获取邮件配置(内部方法) +func (e *Email) getEmailConfig() (*config.EmailConfig, error) { + if e.config == nil { return nil, fmt.Errorf("email config is nil") } - if cfg.Host == "" { + if e.config.Host == "" { return nil, fmt.Errorf("email host is required") } - if cfg.Username == "" { + if e.config.Username == "" { return nil, fmt.Errorf("email username is required") } - if cfg.Password == "" { + if e.config.Password == "" { return nil, fmt.Errorf("email password is required") } // 设置默认值 - if cfg.Port == 0 { - cfg.Port = 587 + if e.config.Port == 0 { + e.config.Port = 587 } - if cfg.From == "" { - cfg.From = cfg.Username + if e.config.From == "" { + e.config.From = e.config.Username } - if cfg.Timeout == 0 { - cfg.Timeout = 30 + if e.config.Timeout == 0 { + e.config.Timeout = 30 } - return &Email{ - config: cfg, - }, nil + return e.config, nil } // Message 邮件消息 @@ -69,66 +75,91 @@ type Message struct { // HTMLBody HTML正文(可选,如果设置了会优先使用) HTMLBody string - - // Attachments 附件列表(可选) - Attachments []Attachment } -// Attachment 附件 -type Attachment struct { - // Filename 文件名 - Filename string +// SendEmail 发送邮件 +// to: 收件人列表 +// subject: 邮件主题 +// body: 邮件正文(纯文本) +// htmlBody: HTML正文(可选,如果设置了会优先使用) +func (e *Email) SendEmail(to []string, subject, body string, htmlBody ...string) error { + cfg, err := e.getEmailConfig() + if err != nil { + return err + } - // Content 文件内容 - Content []byte + msg := &Message{ + To: to, + Subject: subject, + Body: body, + } - // ContentType 文件类型(如:application/pdf) - ContentType string + if len(htmlBody) > 0 && htmlBody[0] != "" { + msg.HTMLBody = htmlBody[0] + } + + return e.send(msg, cfg) } -// SendRaw 发送原始邮件内容 -// recipients: 收件人列表(To、Cc、Bcc的合并列表) -// body: 完整的邮件内容(MIME格式),由外部构建 -func (e *Email) SendRaw(recipients []string, body []byte) error { - if len(recipients) == 0 { +// send 发送邮件(内部方法) +func (e *Email) send(msg *Message, cfg *config.EmailConfig) error { + if msg == nil { + return fmt.Errorf("message is nil") + } + + if len(msg.To) == 0 { return fmt.Errorf("recipients are required") } - if len(body) == 0 { - return fmt.Errorf("email body is required") + if msg.Subject == "" { + return fmt.Errorf("subject is required") } + if msg.Body == "" && msg.HTMLBody == "" { + return fmt.Errorf("body or HTMLBody is required") + } + + // 构建邮件内容 + emailBody, err := e.buildEmailBody(msg, cfg) + if err != nil { + return fmt.Errorf("failed to build email body: %w", err) + } + + // 合并收件人列表 + recipients := append(msg.To, msg.Cc...) + recipients = append(recipients, msg.Bcc...) + // 连接SMTP服务器 - addr := fmt.Sprintf("%s:%d", e.config.Host, e.config.Port) - auth := smtp.PlainAuth("", e.config.Username, e.config.Password, e.config.Host) + addr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port)) + auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host) // 创建连接 - conn, err := net.DialTimeout("tcp", addr, time.Duration(e.config.Timeout)*time.Second) + conn, err := net.DialTimeout("tcp", addr, time.Duration(cfg.Timeout)*time.Second) if err != nil { return fmt.Errorf("failed to connect to SMTP server: %w", err) } defer conn.Close() // 创建SMTP客户端 - client, err := smtp.NewClient(conn, e.config.Host) + client, err := smtp.NewClient(conn, cfg.Host) if err != nil { return fmt.Errorf("failed to create SMTP client: %w", err) } defer client.Close() // TLS/SSL处理 - if e.config.UseSSL { + if cfg.UseSSL { // SSL模式(端口通常是465) tlsConfig := &tls.Config{ - ServerName: e.config.Host, + ServerName: cfg.Host, } if err := client.StartTLS(tlsConfig); err != nil { return fmt.Errorf("failed to start TLS: %w", err) } - } else if e.config.UseTLS { + } else if cfg.UseTLS { // TLS模式(STARTTLS,端口通常是587) tlsConfig := &tls.Config{ - ServerName: e.config.Host, + ServerName: cfg.Host, } if err := client.StartTLS(tlsConfig); err != nil { return fmt.Errorf("failed to start TLS: %w", err) @@ -141,7 +172,7 @@ func (e *Email) SendRaw(recipients []string, body []byte) error { } // 设置发件人 - if err := client.Mail(e.config.From); err != nil { + if err := client.Mail(cfg.From); err != nil { return fmt.Errorf("failed to set sender: %w", err) } @@ -158,7 +189,7 @@ func (e *Email) SendRaw(recipients []string, body []byte) error { return fmt.Errorf("failed to get data writer: %w", err) } - _, err = writer.Write(body) + _, err = writer.Write(emailBody) if err != nil { writer.Close() return fmt.Errorf("failed to write email body: %w", err) @@ -177,47 +208,14 @@ func (e *Email) SendRaw(recipients []string, body []byte) error { return nil } -// Send 发送邮件(使用Message结构,内部会构建邮件内容) -// 注意:如果需要完全控制邮件内容,请使用SendRaw方法 -func (e *Email) Send(msg *Message) error { - if msg == nil { - return fmt.Errorf("message is nil") - } - - if len(msg.To) == 0 { - return fmt.Errorf("recipients are required") - } - - if msg.Subject == "" { - return fmt.Errorf("subject is required") - } - - if msg.Body == "" && msg.HTMLBody == "" { - return fmt.Errorf("body or HTMLBody is required") - } - - // 构建邮件内容 - emailBody, err := e.buildEmailBody(msg) - if err != nil { - return fmt.Errorf("failed to build email body: %w", err) - } - - // 合并收件人列表 - recipients := append(msg.To, msg.Cc...) - recipients = append(recipients, msg.Bcc...) - - // 使用SendRaw发送 - return e.SendRaw(recipients, emailBody) -} - // buildEmailBody 构建邮件内容 -func (e *Email) buildEmailBody(msg *Message) ([]byte, error) { +func (e *Email) buildEmailBody(msg *Message, cfg *config.EmailConfig) ([]byte, error) { var buf bytes.Buffer // 邮件头 - from := e.config.From - if e.config.FromName != "" { - from = fmt.Sprintf("%s <%s>", e.config.FromName, e.config.From) + from := cfg.From + if cfg.FromName != "" { + from = fmt.Sprintf("%s <%s>", cfg.FromName, cfg.From) } buf.WriteString(fmt.Sprintf("From: %s\r\n", from)) @@ -281,26 +279,3 @@ func joinEmails(emails []string) string { return result } -// SendSimple 发送简单邮件(便捷方法) -// to: 收件人 -// subject: 主题 -// body: 正文 -func (e *Email) SendSimple(to []string, subject, body string) error { - return e.Send(&Message{ - To: to, - Subject: subject, - Body: body, - }) -} - -// SendHTML 发送HTML邮件(便捷方法) -// to: 收件人 -// subject: 主题 -// htmlBody: HTML正文 -func (e *Email) SendHTML(to []string, subject, htmlBody string) error { - return e.Send(&Message{ - To: to, - Subject: subject, - HTMLBody: htmlBody, - }) -} diff --git a/examples/datetime_example.go b/examples/datetime_example.go deleted file mode 100644 index a8119e0..0000000 --- a/examples/datetime_example.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" - - "git.toowon.com/jimmy/go-common/datetime" -) - -func main() { - // 设置默认时区 - err := datetime.SetDefaultTimeZone(datetime.AsiaShanghai) - if err != nil { - log.Fatal(err) - } - - // 获取当前时间 - now := datetime.Now() - fmt.Printf("Current time: %s\n", datetime.FormatDateTime(now)) - - // 解析时间字符串 - t, err := datetime.ParseDateTime("2024-01-01 12:00:00") - if err != nil { - log.Fatal(err) - } - fmt.Printf("Parsed time: %s\n", datetime.FormatDateTime(t)) - - // 时区转换 - t2, err := datetime.ToTimezone(t, datetime.AmericaNewYork) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Time in New York: %s\n", datetime.FormatDateTime(t2)) - - // Unix时间戳 - unix := datetime.ToUnix(now) - fmt.Printf("Unix timestamp: %d\n", unix) - t3 := datetime.FromUnix(unix) - fmt.Printf("From Unix: %s\n", datetime.FormatDateTime(t3)) - - // 时间计算 - tomorrow := datetime.AddDays(now, 1) - fmt.Printf("Tomorrow: %s\n", datetime.FormatDate(tomorrow)) - - // 时间范围 - startOfDay := datetime.StartOfDay(now) - endOfDay := datetime.EndOfDay(now) - fmt.Printf("Start of day: %s\n", datetime.FormatDateTime(startOfDay)) - fmt.Printf("End of day: %s\n", datetime.FormatDateTime(endOfDay)) - - // 时间差 - diff := datetime.DiffDays(now, tomorrow) - fmt.Printf("Days difference: %d\n", diff) -} - diff --git a/examples/datetime_utc_example.go b/examples/datetime_utc_example.go deleted file mode 100644 index 5547781..0000000 --- a/examples/datetime_utc_example.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" - - "git.toowon.com/jimmy/go-common/datetime" -) - -func main() { - // 示例1:将当前时间转换为UTC - fmt.Println("=== Example 1: Convert Current Time to UTC ===") - now := time.Now() - utcTime := datetime.ToUTC(now) - fmt.Printf("Local time: %s\n", datetime.FormatDateTime(now)) - fmt.Printf("UTC time: %s\n", datetime.FormatDateTime(utcTime)) - - // 示例2:从指定时区转换为UTC - fmt.Println("\n=== Example 2: Convert from Specific Timezone to UTC ===") - // 解析上海时区的时间 - shanghaiTime, err := datetime.ParseDateTime("2024-01-01 12:00:00", datetime.AsiaShanghai) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Shanghai time: %s\n", datetime.FormatDateTime(shanghaiTime, datetime.AsiaShanghai)) - - // 转换为UTC - utcTime2, err := datetime.ToUTCFromTimezone(shanghaiTime, datetime.AsiaShanghai) - if err != nil { - log.Fatal(err) - } - fmt.Printf("UTC time: %s\n", datetime.FormatDateTime(utcTime2, datetime.UTC)) - - // 示例3:解析时间字符串并直接转换为UTC - fmt.Println("\n=== Example 3: Parse and Convert to UTC ===") - utcTime3, err := datetime.ParseDateTimeToUTC("2024-01-01 12:00:00", datetime.AsiaShanghai) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Parsed from Shanghai timezone, UTC: %s\n", datetime.FormatDateTime(utcTime3, datetime.UTC)) - - // 示例4:解析日期并转换为UTC - fmt.Println("\n=== Example 4: Parse Date and Convert to UTC ===") - utcTime4, err := datetime.ParseDateToUTC("2024-01-01", datetime.AsiaShanghai) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Date parsed from Shanghai timezone, UTC: %s\n", datetime.FormatDateTime(utcTime4, datetime.UTC)) - - // 示例5:数据库存储场景 - fmt.Println("\n=== Example 5: Database Storage Scenario ===") - // 从请求中获取时间(假设是上海时区) - requestTimeStr := "2024-01-01 12:00:00" - requestTimezone := datetime.AsiaShanghai - - // 转换为UTC时间(用于数据库存储) - dbTime, err := datetime.ParseDateTimeToUTC(requestTimeStr, requestTimezone) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Request time (Shanghai): %s\n", requestTimeStr) - fmt.Printf("Database time (UTC): %s\n", datetime.FormatDateTime(dbTime, datetime.UTC)) - - // 从数据库读取UTC时间,转换为用户时区显示 - userTimezone := datetime.AsiaShanghai - displayTime, err := datetime.ToTimezone(dbTime, userTimezone) - if err != nil { - log.Fatal(err) - } - fmt.Printf("Display time (Shanghai): %s\n", datetime.FormatDateTime(displayTime, userTimezone)) -} - diff --git a/examples/factory_blackbox_example.go b/examples/factory_blackbox_example.go index b324d5e..2910b18 100644 --- a/examples/factory_blackbox_example.go +++ b/examples/factory_blackbox_example.go @@ -7,7 +7,6 @@ import ( "time" "git.toowon.com/jimmy/go-common/factory" - commonhttp "git.toowon.com/jimmy/go-common/http" ) // 示例:Factory黑盒模式 - 最简化的使用方式 @@ -44,8 +43,6 @@ func main() { // 用户列表 func handleUsers(w http.ResponseWriter, r *http.Request) { - h := commonhttp.NewHandler(w, r) - // 创建工厂(在处理器中也可以复用) fac, _ := factory.NewFactoryFromFile("config.json") ctx := context.Background() @@ -59,7 +56,7 @@ func handleUsers(w http.ResponseWriter, r *http.Request) { cacheKey := "users:list" cached, _ := fac.RedisGet(ctx, cacheKey) if cached != "" { - h.Success(cached) + fac.Success(w, cached) return } @@ -72,12 +69,11 @@ func handleUsers(w http.ResponseWriter, r *http.Request) { // 4. 缓存结果 fac.RedisSet(ctx, cacheKey, users, 5*time.Minute) - h.Success(users) + fac.Success(w, users) } // 文件上传 func handleUpload(w http.ResponseWriter, r *http.Request) { - h := commonhttp.NewHandler(w, r) fac, _ := factory.NewFactoryFromFile("config.json") ctx := context.Background() @@ -85,7 +81,7 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { file, header, err := r.FormFile("file") if err != nil { fac.LogError("文件上传失败: %v", err) - h.Error(400, "文件上传失败") + fac.Error(w, 400, "文件上传失败") return } defer file.Close() @@ -95,7 +91,7 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { url, err := fac.UploadFile(ctx, objectKey, file, header.Header.Get("Content-Type")) if err != nil { fac.LogError("文件上传到存储失败: %v", err) - h.Error(500, "文件上传失败") + fac.Error(w, 500, "文件上传失败") return } @@ -106,7 +102,7 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { "url": url, }, "文件上传成功") - h.Success(map[string]interface{}{ + fac.Success(w, map[string]interface{}{ "url": url, }) } @@ -122,16 +118,14 @@ func authMiddleware(next http.Handler) http.Handler { // 获取token token := r.Header.Get("Authorization") if token == "" { - h := commonhttp.NewHandler(w, r) - h.Error(401, "未授权") + fac.Error(w, 401, "未授权") return } // 从Redis验证token(黑盒方法) userID, err := fac.RedisGet(ctx, "token:"+token) if err != nil || userID == "" { - h := commonhttp.NewHandler(w, r) - h.Error(401, "token无效") + fac.Error(w, 401, "token无效") return } diff --git a/examples/http_handler_example.go b/examples/http_handler_example.go index adc0540..c589a1b 100644 --- a/examples/http_handler_example.go +++ b/examples/http_handler_example.go @@ -5,6 +5,7 @@ import ( "net/http" commonhttp "git.toowon.com/jimmy/go-common/http" + "git.toowon.com/jimmy/go-common/factory" ) // 用户结构 @@ -14,15 +15,17 @@ type User struct { Email string `json:"email"` } -// 获取用户列表(使用Handler黑盒模式) -func GetUserList(h *commonhttp.Handler) { - // 获取分页参数(简洁方式) - pagination := h.ParsePaginationRequest() +// 获取用户列表(使用公共方法和factory) +func GetUserList(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 获取分页参数(使用公共方法) + pagination := commonhttp.ParsePaginationRequest(r) page := pagination.GetPage() pageSize := pagination.GetSize() - // 获取查询参数(简洁方式) - _ = h.GetQuery("keyword", "") // 示例:获取查询参数 + // 获取查询参数(使用公共方法) + _ = commonhttp.GetQuery(r, "keyword", "") // 示例:获取查询参数 // 模拟查询数据 users := []User{ @@ -31,26 +34,28 @@ func GetUserList(h *commonhttp.Handler) { } total := int64(100) - // 返回分页响应(简洁方式) - h.SuccessPage(users, total, page, pageSize) + // 返回分页响应(使用factory方法) + fac.SuccessPage(w, users, total, page, pageSize) } -// 创建用户(使用Handler黑盒模式) -func CreateUser(h *commonhttp.Handler) { - // 解析请求体(简洁方式) +// 创建用户(使用公共方法和factory) +func CreateUser(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 解析请求体(使用公共方法) var req struct { Name string `json:"name"` Email string `json:"email"` } - if err := h.ParseJSON(&req); err != nil { - h.WriteJSON(http.StatusBadRequest, 400, "请求参数解析失败", nil) + if err := commonhttp.ParseJSON(r, &req); err != nil { + commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil) return } // 参数验证 if req.Name == "" { - h.Error(1001, "用户名不能为空") + fac.Error(w, 1001, "用户名不能为空") return } @@ -61,49 +66,46 @@ func CreateUser(h *commonhttp.Handler) { Email: req.Email, } - // 返回成功响应(简洁方式) - h.SuccessWithMessage("创建成功", user) + // 返回成功响应(使用factory方法,统一Success方法) + fac.Success(w, user, "创建成功") } -// 获取用户详情(使用Handler黑盒模式) -func GetUser(h *commonhttp.Handler) { - // 获取查询参数(简洁方式) - id := h.GetQueryInt64("id", 0) +// 获取用户详情(使用公共方法和factory) +func GetUser(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") + + // 获取查询参数(使用公共方法) + id := commonhttp.GetQueryInt64(r, "id", 0) if id == 0 { - h.WriteJSON(http.StatusBadRequest, 400, "用户ID不能为空", nil) + commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "用户ID不能为空", nil) return } // 模拟查询用户 if id == 1 { user := User{ID: 1, Name: "User1", Email: "user1@example.com"} - h.Success(user) + fac.Success(w, user) } else { - h.Error(1002, "用户不存在") + fac.Error(w, 1002, "用户不存在") } } func main() { - // 方式1:使用HandleFunc包装器(推荐,最简洁) - http.HandleFunc("/users", commonhttp.HandleFunc(func(h *commonhttp.Handler) { - switch h.Request().Method { + // 使用标准http.HandleFunc + http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { case http.MethodGet: - GetUserList(h) + GetUserList(w, r) case http.MethodPost: - CreateUser(h) + CreateUser(w, r) default: - h.WriteJSON(http.StatusMethodNotAllowed, 405, "方法不支持", nil) + commonhttp.WriteJSON(w, http.StatusMethodNotAllowed, 405, "方法不支持", nil) } - })) - - // 方式2:手动创建Handler(需要更多控制时) - http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { - h := commonhttp.NewHandler(w, r) - GetUser(h) }) + http.HandleFunc("/user", GetUser) + 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 index f320042..6c53026 100644 --- a/examples/http_pagination_example.go +++ b/examples/http_pagination_example.go @@ -5,6 +5,7 @@ import ( "net/http" commonhttp "git.toowon.com/jimmy/go-common/http" + "git.toowon.com/jimmy/go-common/factory" ) // ListUserRequest 用户列表请求(包含分页字段) @@ -20,21 +21,22 @@ type User struct { Email string `json:"email"` } -// 获取用户列表(使用Handler和PaginationRequest) -func GetUserList(h *commonhttp.Handler) { +// 获取用户列表(使用公共方法和factory) +func GetUserList(w http.ResponseWriter, r *http.Request) { + fac, _ := factory.NewFactoryFromFile("config.json") var req ListUserRequest // 方式1:从JSON请求体解析(分页字段会自动解析) - if h.Request().Method == http.MethodPost { - if err := h.ParseJSON(&req); err != nil { - h.WriteJSON(http.StatusBadRequest, 400, "请求参数解析失败", nil) + if r.Method == http.MethodPost { + if err := commonhttp.ParseJSON(r, &req); err != nil { + commonhttp.WriteJSON(w, http.StatusBadRequest, 400, "请求参数解析失败", nil) return } } else { // 方式2:从查询参数解析分页 - pagination := h.ParsePaginationRequest() + pagination := commonhttp.ParsePaginationRequest(r) req.PaginationRequest = *pagination - req.Keyword = h.GetQuery("keyword", "") + req.Keyword = commonhttp.GetQuery(r, "keyword", "") } // 使用分页方法 @@ -49,12 +51,12 @@ func GetUserList(h *commonhttp.Handler) { } total := int64(100) - // 返回分页响应 - h.SuccessPage(users, total, page, size) + // 返回分页响应(使用factory方法) + fac.SuccessPage(w, users, total, page, size) } func main() { - http.HandleFunc("/users", commonhttp.HandleFunc(GetUserList)) + http.HandleFunc("/users", GetUserList) log.Println("Server started on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) diff --git a/examples/middleware_simple_example.go b/examples/middleware_simple_example.go index c70d555..3d74c14 100644 --- a/examples/middleware_simple_example.go +++ b/examples/middleware_simple_example.go @@ -21,13 +21,11 @@ func main() { // 定义API处理器 http.Handle("/api/hello", chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) { - h := commonhttp.NewHandler(w, r) + // 获取时区(使用公共方法) + timezone := commonhttp.GetTimezone(r) - // 获取时区 - timezone := h.GetTimezone() - - // 返回响应 - h.Success(map[string]interface{}{ + // 返回响应(使用公共方法) + commonhttp.Success(w, map[string]interface{}{ "message": "Hello, World!", "timezone": timezone, }) diff --git a/factory/factory.go b/factory/factory.go index 6ced0ab..3a9adfc 100644 --- a/factory/factory.go +++ b/factory/factory.go @@ -9,11 +9,13 @@ import ( "git.toowon.com/jimmy/go-common/config" "git.toowon.com/jimmy/go-common/email" + commonhttp "git.toowon.com/jimmy/go-common/http" "git.toowon.com/jimmy/go-common/logger" "git.toowon.com/jimmy/go-common/middleware" "git.toowon.com/jimmy/go-common/migration" "git.toowon.com/jimmy/go-common/sms" "git.toowon.com/jimmy/go-common/storage" + "git.toowon.com/jimmy/go-common/tools" "github.com/redis/go-redis/v9" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -21,6 +23,51 @@ import ( "gorm.io/gorm" ) +// ========== HTTP响应结构体(暴露给外部项目使用) ========== + +// Response 标准响应结构(暴露给外部项目使用) +// 外部项目可以直接使用 factory.Response 创建响应对象 +// +// 示例: +// +// response := factory.Response{ +// Code: 0, +// Message: "success", +// Data: data, +// } +type Response = commonhttp.Response + +// PageResponse 分页响应结构(暴露给外部项目使用) +// 外部项目可以直接使用 factory.PageResponse 创建分页响应对象 +type PageResponse = commonhttp.PageResponse + +// PageData 分页数据(暴露给外部项目使用) +// 外部项目可以直接使用 factory.PageData 创建分页数据对象 +// +// 示例: +// +// pageData := &factory.PageData{ +// List: users, +// Total: 100, +// Page: 1, +// PageSize: 20, +// } +// fac.Success(w, pageData) +type PageData = commonhttp.PageData + +// ========== HTTP请求结构体(暴露给外部项目使用) ========== + +// PaginationRequest 分页请求结构(暴露给外部项目使用) +// 外部项目可以直接使用 factory.PaginationRequest 创建分页请求对象 +// +// 示例: +// +// type ListUserRequest struct { +// Keyword string `json:"keyword"` +// factory.PaginationRequest // 嵌入分页请求结构 +// } +type PaginationRequest = commonhttp.PaginationRequest + // Factory 工厂类 - 黑盒模式设计 // // 核心理念: @@ -90,17 +137,8 @@ func (f *Factory) getEmailClient() (*email.Email, error) { return f.email, nil } - if f.cfg.Email == nil { - return nil, fmt.Errorf("email config is nil") - } - - 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 + f.email = email.NewEmail(f.cfg) + return f.email, nil } // SendEmail 发送邮件(黑盒模式,推荐使用) @@ -114,18 +152,7 @@ func (f *Factory) SendEmail(to []string, subject, body string, htmlBody ...strin 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) + return e.SendEmail(to, subject, body, htmlBody...) } // getSMSClient 获取短信客户端(内部方法,延迟初始化) @@ -134,17 +161,8 @@ func (f *Factory) getSMSClient() (*sms.SMS, error) { return f.sms, nil } - if f.cfg.SMS == nil { - return nil, fmt.Errorf("SMS config is nil") - } - - 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 + f.sms = sms.NewSMS(f.cfg) + return f.sms, nil } // SendSMS 发送短信(黑盒模式,推荐使用) @@ -157,17 +175,7 @@ func (f *Factory) SendSMS(phoneNumbers []string, templateParam interface{}, temp 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) + return s.SendSMS(phoneNumbers, templateParam, templateCode...) } // getLogger 获取日志记录器(内部方法,延迟初始化) @@ -862,3 +870,328 @@ func (f *Factory) GetMigrationStatus(migrationsDir string) ([]migration.Migratio return status, nil } + +// ========== HTTP响应方法(黑盒模式,推荐使用) ========== +// +// 这些方法直接调用 http 包的公共方法,保持低耦合。 +// 推荐直接使用 factory.Success() 等方法,而不是通过 handler。 + +// Success 成功响应(黑盒模式,推荐使用) +// w: ResponseWriter +// data: 响应数据,可以为nil +// message: 响应消息(可选),如果为空则使用默认消息 "success" +// +// 示例: +// +// fac, _ := factory.NewFactoryFromFile("config.json") +// http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { +// fac.Success(w, user) // 只有数据 +// fac.Success(w, user, "查询成功") // 数据+消息 +// }) +func (f *Factory) Success(w http.ResponseWriter, data interface{}, message ...string) { + commonhttp.Success(w, data, message...) +} + +// SuccessPage 分页成功响应(黑盒模式,推荐使用) +// w: ResponseWriter +// list: 数据列表 +// total: 总记录数 +// page: 当前页码 +// pageSize: 每页大小 +// message: 响应消息(可选),如果为空则使用默认消息 "success" +func (f *Factory) SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int, message ...string) { + commonhttp.SuccessPage(w, list, total, page, pageSize, message...) +} + +// Error 错误响应(黑盒模式,推荐使用) +// w: ResponseWriter +// code: 业务错误码,非0表示业务错误 +// message: 错误消息 +func (f *Factory) Error(w http.ResponseWriter, code int, message string) { + commonhttp.Error(w, code, message) +} + +// SystemError 系统错误响应(返回HTTP 500)(黑盒模式,推荐使用) +// w: ResponseWriter +// message: 错误消息 +func (f *Factory) SystemError(w http.ResponseWriter, message string) { + commonhttp.SystemError(w, message) +} + +// ========== HTTP请求解析方法(黑盒模式,推荐使用) ========== +// +// 这些方法直接调用 http 包的公共方法,保持低耦合。 +// 推荐直接使用 factory.ParseJSON()、factory.GetQuery() 等方法。 + +// ParseJSON 解析JSON请求体(黑盒模式,推荐使用) +// r: HTTP请求 +// v: 目标结构体指针 +// +// 示例: +// +// var req struct { +// Name string `json:"name"` +// } +// if err := fac.ParseJSON(r, &req); err != nil { +// fac.Error(w, 400, "请求参数解析失败") +// return +// } +func (f *Factory) ParseJSON(r *http.Request, v interface{}) error { + return commonhttp.ParseJSON(r, v) +} + +// ParsePaginationRequest 从请求中解析分页参数(黑盒模式,推荐使用) +// r: HTTP请求 +// 支持从查询参数和form表单中解析 +// 优先级:查询参数 > form表单 +// +// 示例: +// +// pagination := fac.ParsePaginationRequest(r) +// page := pagination.GetPage() +// pageSize := pagination.GetSize() +func (f *Factory) ParsePaginationRequest(r *http.Request) *PaginationRequest { + return commonhttp.ParsePaginationRequest(r) +} + +// GetQuery 获取查询参数(字符串)(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetQuery(r *http.Request, key, defaultValue string) string { + return commonhttp.GetQuery(r, key, defaultValue) +} + +// GetQueryInt 获取整数查询参数(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetQueryInt(r *http.Request, key string, defaultValue int) int { + return commonhttp.GetQueryInt(r, key, defaultValue) +} + +// GetQueryInt64 获取int64查询参数(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetQueryInt64(r *http.Request, key string, defaultValue int64) int64 { + return commonhttp.GetQueryInt64(r, key, defaultValue) +} + +// GetQueryBool 获取布尔查询参数(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetQueryBool(r *http.Request, key string, defaultValue bool) bool { + return commonhttp.GetQueryBool(r, key, defaultValue) +} + +// GetQueryFloat64 获取float64查询参数(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetQueryFloat64(r *http.Request, key string, defaultValue float64) float64 { + return commonhttp.GetQueryFloat64(r, key, defaultValue) +} + +// GetFormValue 获取表单值(字符串)(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetFormValue(r *http.Request, key, defaultValue string) string { + return commonhttp.GetFormValue(r, key, defaultValue) +} + +// GetFormInt 获取表单整数(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetFormInt(r *http.Request, key string, defaultValue int) int { + return commonhttp.GetFormInt(r, key, defaultValue) +} + +// GetFormInt64 获取表单int64(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetFormInt64(r *http.Request, key string, defaultValue int64) int64 { + return commonhttp.GetFormInt64(r, key, defaultValue) +} + +// GetFormBool 获取表单布尔值(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func (f *Factory) GetFormBool(r *http.Request, key string, defaultValue bool) bool { + return commonhttp.GetFormBool(r, key, defaultValue) +} + +// GetHeader 获取请求头(黑盒模式,推荐使用) +// r: HTTP请求 +// key: 请求头名称 +// defaultValue: 默认值 +func (f *Factory) GetHeader(r *http.Request, key, defaultValue string) string { + return commonhttp.GetHeader(r, key, defaultValue) +} + +// GetTimezone 从请求的context中获取时区(黑盒模式,推荐使用) +// r: HTTP请求 +// 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 +// 如果未设置,返回默认时区 AsiaShanghai +func (f *Factory) GetTimezone(r *http.Request) string { + return commonhttp.GetTimezone(r) +} + +// ========== Tools工具方法(黑盒模式,推荐使用) ========== +// +// 这些方法直接调用 tools 包的公共方法,保持低耦合。 +// factory 只负责方法暴露,具体业务在 tools 包中实现。 + +// ========== Version 版本工具 ========== + +// GetVersion 获取版本号(黑盒模式,推荐使用) +// 优先从环境变量 DOCKER_TAG 或 VERSION 中读取 +// 如果没有设置环境变量,则使用默认版本号 +func (f *Factory) GetVersion() string { + return tools.GetVersion() +} + +// ========== Money 金额工具 ========== + +// GetMoneyCalculator 获取金额计算器(黑盒模式,推荐使用) +// 返回金额计算器实例,可用于金额计算操作 +func (f *Factory) GetMoneyCalculator() *tools.MoneyCalculator { + return tools.NewMoneyCalculator() +} + +// YuanToCents 元转分(黑盒模式,推荐使用) +func (f *Factory) YuanToCents(yuan float64) int64 { + return tools.YuanToCents(yuan) +} + +// CentsToYuan 分转元(黑盒模式,推荐使用) +func (f *Factory) CentsToYuan(cents int64) float64 { + return tools.CentsToYuan(cents) +} + +// FormatYuan 格式化显示金额(分转元,保留2位小数)(黑盒模式,推荐使用) +func (f *Factory) FormatYuan(cents int64) string { + return tools.FormatYuan(cents) +} + +// ========== DateTime 日期时间工具 ========== + +// Now 获取当前时间(使用指定时区)(黑盒模式,推荐使用) +func (f *Factory) Now(timezone ...string) time.Time { + return tools.Now(timezone...) +} + +// ParseDateTime 解析日期时间字符串(2006-01-02 15:04:05)(黑盒模式,推荐使用) +func (f *Factory) ParseDateTime(value string, timezone ...string) (time.Time, error) { + return tools.ParseDateTime(value, timezone...) +} + +// ParseDate 解析日期字符串(2006-01-02)(黑盒模式,推荐使用) +func (f *Factory) ParseDate(value string, timezone ...string) (time.Time, error) { + return tools.ParseDate(value, timezone...) +} + +// FormatDateTime 格式化日期时间(2006-01-02 15:04:05)(黑盒模式,推荐使用) +func (f *Factory) FormatDateTime(t time.Time, timezone ...string) string { + return tools.FormatDateTime(t, timezone...) +} + +// FormatDate 格式化日期(2006-01-02)(黑盒模式,推荐使用) +func (f *Factory) FormatDate(t time.Time, timezone ...string) string { + return tools.FormatDate(t, timezone...) +} + +// FormatTime 格式化时间(15:04:05)(黑盒模式,推荐使用) +func (f *Factory) FormatTime(t time.Time, timezone ...string) string { + return tools.FormatTime(t, timezone...) +} + +// ToUnix 转换为Unix时间戳(黑盒模式,推荐使用) +func (f *Factory) ToUnix(t time.Time) int64 { + return tools.ToUnix(t) +} + +// FromUnix 从Unix时间戳创建时间(黑盒模式,推荐使用) +func (f *Factory) FromUnix(sec int64, timezone ...string) time.Time { + return tools.FromUnix(sec, timezone...) +} + +// ToUnixMilli 转换为Unix毫秒时间戳(黑盒模式,推荐使用) +func (f *Factory) ToUnixMilli(t time.Time) int64 { + return tools.ToUnixMilli(t) +} + +// FromUnixMilli 从Unix毫秒时间戳创建时间(黑盒模式,推荐使用) +func (f *Factory) FromUnixMilli(msec int64, timezone ...string) time.Time { + return tools.FromUnixMilli(msec, timezone...) +} + +// AddDays 添加天数(黑盒模式,推荐使用) +func (f *Factory) AddDays(t time.Time, days int) time.Time { + return tools.AddDays(t, days) +} + +// AddMonths 添加月数(黑盒模式,推荐使用) +func (f *Factory) AddMonths(t time.Time, months int) time.Time { + return tools.AddMonths(t, months) +} + +// AddYears 添加年数(黑盒模式,推荐使用) +func (f *Factory) AddYears(t time.Time, years int) time.Time { + return tools.AddYears(t, years) +} + +// StartOfDay 获取一天的开始时间(00:00:00)(黑盒模式,推荐使用) +func (f *Factory) StartOfDay(t time.Time, timezone ...string) time.Time { + return tools.StartOfDay(t, timezone...) +} + +// EndOfDay 获取一天的结束时间(23:59:59.999999999)(黑盒模式,推荐使用) +func (f *Factory) EndOfDay(t time.Time, timezone ...string) time.Time { + return tools.EndOfDay(t, timezone...) +} + +// StartOfMonth 获取月份的开始时间(黑盒模式,推荐使用) +func (f *Factory) StartOfMonth(t time.Time, timezone ...string) time.Time { + return tools.StartOfMonth(t, timezone...) +} + +// EndOfMonth 获取月份的结束时间(黑盒模式,推荐使用) +func (f *Factory) EndOfMonth(t time.Time, timezone ...string) time.Time { + return tools.EndOfMonth(t, timezone...) +} + +// StartOfYear 获取年份的开始时间(黑盒模式,推荐使用) +func (f *Factory) StartOfYear(t time.Time, timezone ...string) time.Time { + return tools.StartOfYear(t, timezone...) +} + +// EndOfYear 获取年份的结束时间(黑盒模式,推荐使用) +func (f *Factory) EndOfYear(t time.Time, timezone ...string) time.Time { + return tools.EndOfYear(t, timezone...) +} + +// DiffDays 计算两个时间之间的天数差(黑盒模式,推荐使用) +func (f *Factory) DiffDays(t1, t2 time.Time) int { + return tools.DiffDays(t1, t2) +} + +// DiffHours 计算两个时间之间的小时差(黑盒模式,推荐使用) +func (f *Factory) DiffHours(t1, t2 time.Time) int64 { + return tools.DiffHours(t1, t2) +} + +// DiffMinutes 计算两个时间之间的分钟差(黑盒模式,推荐使用) +func (f *Factory) DiffMinutes(t1, t2 time.Time) int64 { + return tools.DiffMinutes(t1, t2) +} + +// DiffSeconds 计算两个时间之间的秒数差(黑盒模式,推荐使用) +func (f *Factory) DiffSeconds(t1, t2 time.Time) int64 { + return tools.DiffSeconds(t1, t2) +} diff --git a/http/handler.go b/http/handler.go deleted file mode 100644 index 18df8a7..0000000 --- a/http/handler.go +++ /dev/null @@ -1,279 +0,0 @@ -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 bc14f4a..ad69c6b 100644 --- a/http/request.go +++ b/http/request.go @@ -1,8 +1,12 @@ package http import ( + "encoding/json" + "io" "net/http" "strconv" + + "git.toowon.com/jimmy/go-common/middleware" ) // getQueryInt 获取整数查询参数(内部方法,供ParsePaginationRequest使用) @@ -114,3 +118,192 @@ func ParsePaginationRequest(r *http.Request) *PaginationRequest { return req } + +// ========== 请求解析公共方法 ========== + +// 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 { + value := r.URL.Query().Get(key) + if value == "" { + return defaultValue + } + + intValue, err := strconv.Atoi(value) + if err != nil { + return defaultValue + } + + return intValue +} + +// GetQueryInt64 获取int64查询参数(公共方法) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +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 获取布尔查询参数(公共方法) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +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查询参数(公共方法) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +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 获取表单值(公共方法) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func GetFormValue(r *http.Request, key, defaultValue string) string { + value := r.FormValue(key) + if value == "" { + return defaultValue + } + return value +} + +// GetFormInt 获取表单整数(公共方法) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func GetFormInt(r *http.Request, key string, defaultValue int) int { + value := r.FormValue(key) + if value == "" { + return defaultValue + } + + intValue, err := strconv.Atoi(value) + if err != nil { + return defaultValue + } + + return intValue +} + +// GetFormInt64 获取表单int64(公共方法) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +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 +} + +// GetFormBool 获取表单布尔值(公共方法) +// r: HTTP请求 +// key: 参数名 +// defaultValue: 默认值 +func GetFormBool(r *http.Request, key string, defaultValue bool) bool { + value := r.FormValue(key) + if value == "" { + return defaultValue + } + + boolValue, err := strconv.ParseBool(value) + if err != nil { + return defaultValue + } + + return boolValue +} + +// GetHeader 获取请求头(公共方法) +// r: HTTP请求 +// key: 请求头名称 +// defaultValue: 默认值 +func GetHeader(r *http.Request, key, defaultValue string) string { + value := r.Header.Get(key) + if value == "" { + return defaultValue + } + return value +} + +// GetTimezone 从请求的context中获取时区(公共方法) +// r: HTTP请求 +// 如果使用了middleware.Timezone中间件,可以从context中获取时区信息 +// 如果未设置,返回默认时区 AsiaShanghai +func GetTimezone(r *http.Request) string { + return middleware.GetTimezoneFromContext(r.Context()) +} diff --git a/http/response.go b/http/response.go index bddc9b9..434bb9c 100644 --- a/http/response.go +++ b/http/response.go @@ -30,7 +30,7 @@ type PageData struct { PageSize int `json:"pageSize"` // 每页大小 } -// writeJSON 写入JSON响应(内部方法) +// writeJSON 写入JSON响应(公共方法) // httpCode: HTTP状态码(200表示正常,500表示系统错误等) // code: 业务状态码(0表示成功,非0表示业务错误) // message: 响应消息 @@ -48,3 +48,58 @@ func writeJSON(w http.ResponseWriter, httpCode, code int, message string, data i json.NewEncoder(w).Encode(response) } + +// Success 成功响应(公共方法) +// w: ResponseWriter +// data: 响应数据,可以为nil +// message: 响应消息(可选),如果为空则使用默认消息 "success" +// +// 使用方式: +// +// Success(w, data) // 只有数据,使用默认消息 "success" +// Success(w, data, "查询成功") // 数据+消息 +func Success(w http.ResponseWriter, data interface{}, message ...string) { + msg := "success" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + writeJSON(w, http.StatusOK, 0, msg, data) +} + +// SuccessPage 分页成功响应(公共方法) +// w: ResponseWriter +// list: 数据列表 +// total: 总记录数 +// page: 当前页码 +// pageSize: 每页大小 +// message: 响应消息(可选),如果为空则使用默认消息 "success" +func SuccessPage(w http.ResponseWriter, 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(w, http.StatusOK, 0, msg, pageData) +} + +// Error 错误响应(公共方法) +// w: ResponseWriter +// code: 业务错误码,非0表示业务错误 +// message: 错误消息 +func Error(w http.ResponseWriter, code int, message string) { + writeJSON(w, http.StatusOK, code, message, nil) +} + +// SystemError 系统错误响应(返回HTTP 500)(公共方法) +// w: ResponseWriter +// message: 错误消息 +func SystemError(w http.ResponseWriter, message string) { + writeJSON(w, http.StatusInternalServerError, 500, message, nil) +} diff --git a/middleware/timezone.go b/middleware/timezone.go index d561c40..97725d4 100644 --- a/middleware/timezone.go +++ b/middleware/timezone.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "git.toowon.com/jimmy/go-common/datetime" + "git.toowon.com/jimmy/go-common/tools" ) // TimezoneKey context中存储时区的key @@ -14,7 +14,7 @@ type timezoneKey struct{} const TimezoneHeaderName = "X-Timezone" // DefaultTimezone 默认时区 -const DefaultTimezone = datetime.AsiaShanghai +const DefaultTimezone = tools.AsiaShanghai // GetTimezoneFromContext 从context中获取时区 func GetTimezoneFromContext(ctx context.Context) string { @@ -38,7 +38,7 @@ func Timezone(next http.Handler) http.Handler { } // 验证时区是否有效 - if _, err := datetime.GetLocation(timezone); err != nil { + if _, err := tools.GetLocation(timezone); err != nil { // 如果时区无效,使用默认时区 timezone = DefaultTimezone } @@ -53,7 +53,7 @@ func Timezone(next http.Handler) http.Handler { // defaultTimezone: 默认时区,如果未指定则使用 AsiaShanghai func TimezoneWithDefault(defaultTimezone string) func(http.Handler) http.Handler { // 验证默认时区是否有效 - if _, err := datetime.GetLocation(defaultTimezone); err != nil { + if _, err := tools.GetLocation(defaultTimezone); err != nil { defaultTimezone = DefaultTimezone } @@ -68,7 +68,7 @@ func TimezoneWithDefault(defaultTimezone string) func(http.Handler) http.Handler } // 验证时区是否有效 - if _, err := datetime.GetLocation(timezone); err != nil { + if _, err := tools.GetLocation(timezone); err != nil { // 如果时区无效,使用默认时区 timezone = defaultTimezone } @@ -79,4 +79,3 @@ func TimezoneWithDefault(defaultTimezone string) func(http.Handler) http.Handler }) } } - diff --git a/sms/sms.go b/sms/sms.go index fe89ca0..7c6188e 100644 --- a/sms/sms.go +++ b/sms/sms.go @@ -16,59 +16,6 @@ import ( "git.toowon.com/jimmy/go-common/config" ) -// SMS 短信发送器 -type SMS struct { - config *config.SMSConfig -} - -// NewSMS 创建短信发送器 -func NewSMS(cfg *config.SMSConfig) (*SMS, error) { - if cfg == nil { - return nil, fmt.Errorf("SMS config is nil") - } - - if cfg.AccessKeyID == "" { - return nil, fmt.Errorf("AccessKeyID is required") - } - - if cfg.AccessKeySecret == "" { - return nil, fmt.Errorf("AccessKeySecret is required") - } - - if cfg.SignName == "" { - return nil, fmt.Errorf("SignName is required") - } - - // 设置默认值 - if cfg.Region == "" { - cfg.Region = "cn-hangzhou" - } - if cfg.Timeout == 0 { - cfg.Timeout = 10 - } - - return &SMS{ - config: cfg, - }, nil -} - -// SendRequest 发送短信请求 -type SendRequest struct { - // PhoneNumbers 手机号列表 - PhoneNumbers []string - - // TemplateCode 模板代码(如果为空,使用配置中的模板代码) - TemplateCode string - - // TemplateParam 模板参数(可以是map或JSON字符串) - // 如果是map,会自动转换为JSON字符串 - // 如果是string,直接使用(必须是有效的JSON字符串) - TemplateParam interface{} - - // SignName 签名(如果为空,使用配置中的签名) - SignName string -} - // SendResponse 发送短信响应 type SendResponse struct { // RequestID 请求ID @@ -84,48 +31,123 @@ type SendResponse struct { BizID string `json:"BizId"` } -// SendRaw 发送原始请求(允许外部完全控制请求参数) -// params: 请求参数map,工具只负责添加必要的系统参数(如签名、时间戳等)并发送 -func (s *SMS) SendRaw(params map[string]string) (*SendResponse, error) { - if params == nil { - params = make(map[string]string) +// SMS 短信发送器 +type SMS struct { + config *config.SMSConfig +} + +// NewSMS 创建短信发送器 +func NewSMS(cfg *config.Config) *SMS { + if cfg == nil || cfg.SMS == nil { + return &SMS{config: nil} + } + return &SMS{config: cfg.SMS} +} + +// getSMSConfig 获取短信配置(内部方法) +func (s *SMS) getSMSConfig() (*config.SMSConfig, error) { + if s.config == nil { + return nil, fmt.Errorf("SMS config is nil") } - // 确保必要的系统参数存在 - if params["Action"] == "" { - params["Action"] = "SendSms" + if s.config.AccessKeyID == "" { + return nil, fmt.Errorf("AccessKeyID is required") } - if params["Version"] == "" { - params["Version"] = "2017-05-25" + + if s.config.AccessKeySecret == "" { + return nil, fmt.Errorf("AccessKeySecret is required") } - if params["RegionId"] == "" { - params["RegionId"] = s.config.Region + + if s.config.SignName == "" { + return nil, fmt.Errorf("SignName is required") } - if params["AccessKeyId"] == "" { - params["AccessKeyId"] = s.config.AccessKeyID + + // 设置默认值 + if s.config.Region == "" { + s.config.Region = "cn-hangzhou" } - if params["Format"] == "" { - params["Format"] = "JSON" + if s.config.Timeout == 0 { + s.config.Timeout = 10 } - if params["SignatureMethod"] == "" { - params["SignatureMethod"] = "HMAC-SHA1" + + return s.config, nil +} + +// SendSMS 发送短信 +// phoneNumbers: 手机号列表 +// templateParam: 模板参数(map或JSON字符串) +// templateCode: 模板代码(可选,如果为空使用配置中的模板代码) +func (s *SMS) SendSMS(phoneNumbers []string, templateParam interface{}, templateCode ...string) (*SendResponse, error) { + cfg, err := s.getSMSConfig() + if err != nil { + return nil, err } - if params["SignatureVersion"] == "" { - params["SignatureVersion"] = "1.0" + + if len(phoneNumbers) == 0 { + return nil, fmt.Errorf("phone numbers are required") } - if params["SignatureNonce"] == "" { - params["SignatureNonce"] = fmt.Sprint(time.Now().UnixNano()) + + // 使用配置中的模板代码(如果请求中未指定) + templateCodeValue := "" + if len(templateCode) > 0 && templateCode[0] != "" { + templateCodeValue = templateCode[0] + } else { + templateCodeValue = cfg.TemplateCode } - if params["Timestamp"] == "" { - params["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05Z") + if templateCodeValue == "" { + return nil, fmt.Errorf("template code is required") } + signName := cfg.SignName + + // 处理模板参数 + var templateParamJSON string + if templateParam != nil { + switch v := templateParam.(type) { + case string: + // 直接使用字符串(必须是有效的JSON) + templateParamJSON = v + case map[string]string: + // 转换为JSON字符串 + paramBytes, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("failed to marshal template param: %w", err) + } + templateParamJSON = string(paramBytes) + default: + // 尝试JSON序列化 + paramBytes, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("failed to marshal template param: %w", err) + } + templateParamJSON = string(paramBytes) + } + } else { + templateParamJSON = "{}" + } + + // 构建请求参数 + params := make(map[string]string) + params["Action"] = "SendSms" + params["Version"] = "2017-05-25" + params["RegionId"] = cfg.Region + params["AccessKeyId"] = cfg.AccessKeyID + params["Format"] = "JSON" + params["SignatureMethod"] = "HMAC-SHA1" + params["SignatureVersion"] = "1.0" + params["SignatureNonce"] = fmt.Sprint(time.Now().UnixNano()) + params["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05Z") + params["PhoneNumbers"] = strings.Join(phoneNumbers, ",") + params["SignName"] = signName + params["TemplateCode"] = templateCodeValue + params["TemplateParam"] = templateParamJSON + // 计算签名 - signature := s.calculateSignature(params, "POST") + signature := s.calculateSignature(params, "POST", cfg.AccessKeySecret) params["Signature"] = signature // 构建请求URL - endpoint := s.config.Endpoint + endpoint := cfg.Endpoint if endpoint == "" { endpoint = "https://dysmsapi.aliyuncs.com" } @@ -145,7 +167,7 @@ func (s *SMS) SendRaw(params map[string]string) (*SendResponse, error) { httpReq.Header.Set("Accept", "application/json") client := &http.Client{ - Timeout: time.Duration(s.config.Timeout) * time.Second, + Timeout: time.Duration(cfg.Timeout) * time.Second, } resp, err := client.Do(httpReq) @@ -174,70 +196,8 @@ func (s *SMS) SendRaw(params map[string]string) (*SendResponse, error) { return &sendResp, nil } -// Send 发送短信(使用SendRequest结构) -// 注意:如果需要完全控制请求参数,请使用SendRaw方法 -func (s *SMS) Send(req *SendRequest) (*SendResponse, error) { - if req == nil { - return nil, fmt.Errorf("request is nil") - } - - if len(req.PhoneNumbers) == 0 { - return nil, fmt.Errorf("phone numbers are required") - } - - // 使用配置中的模板代码和签名(如果请求中未指定) - templateCode := req.TemplateCode - if templateCode == "" { - templateCode = s.config.TemplateCode - } - if templateCode == "" { - return nil, fmt.Errorf("template code is required") - } - - signName := req.SignName - if signName == "" { - signName = s.config.SignName - } - - // 处理模板参数 - var templateParamJSON string - if req.TemplateParam != nil { - switch v := req.TemplateParam.(type) { - case string: - // 直接使用字符串(必须是有效的JSON) - templateParamJSON = v - case map[string]string: - // 转换为JSON字符串 - paramBytes, err := json.Marshal(v) - if err != nil { - return nil, fmt.Errorf("failed to marshal template param: %w", err) - } - templateParamJSON = string(paramBytes) - default: - // 尝试JSON序列化 - paramBytes, err := json.Marshal(v) - if err != nil { - return nil, fmt.Errorf("failed to marshal template param: %w", err) - } - templateParamJSON = string(paramBytes) - } - } else { - templateParamJSON = "{}" - } - - // 构建请求参数 - params := make(map[string]string) - params["PhoneNumbers"] = strings.Join(req.PhoneNumbers, ",") - params["SignName"] = signName - params["TemplateCode"] = templateCode - params["TemplateParam"] = templateParamJSON - - // 使用SendRaw发送 - return s.SendRaw(params) -} - // calculateSignature 计算签名 -func (s *SMS) calculateSignature(params map[string]string, method string) string { +func (s *SMS) calculateSignature(params map[string]string, method, accessKeySecret string) string { // 对参数进行排序 keys := make([]string, 0, len(params)) for k := range params { @@ -260,31 +220,10 @@ func (s *SMS) calculateSignature(params map[string]string, method string) string stringToSign := method + "&" + url.QueryEscape("/") + "&" + url.QueryEscape(queryString) // 计算HMAC-SHA1签名 - mac := hmac.New(sha1.New, []byte(s.config.AccessKeySecret+"&")) + mac := hmac.New(sha1.New, []byte(accessKeySecret+"&")) mac.Write([]byte(stringToSign)) signature := base64.StdEncoding.EncodeToString(mac.Sum(nil)) return signature } -// SendSimple 发送简单短信(便捷方法) -// phoneNumbers: 手机号列表 -// templateParam: 模板参数 -func (s *SMS) SendSimple(phoneNumbers []string, templateParam map[string]string) (*SendResponse, error) { - return s.Send(&SendRequest{ - PhoneNumbers: phoneNumbers, - TemplateParam: templateParam, - }) -} - -// SendWithTemplate 使用指定模板发送短信(便捷方法) -// phoneNumbers: 手机号列表 -// templateCode: 模板代码 -// templateParam: 模板参数 -func (s *SMS) SendWithTemplate(phoneNumbers []string, templateCode string, templateParam map[string]string) (*SendResponse, error) { - return s.Send(&SendRequest{ - PhoneNumbers: phoneNumbers, - TemplateCode: templateCode, - TemplateParam: templateParam, - }) -} diff --git a/storage/handler.go b/storage/handler.go index 9bf9528..7b36a12 100644 --- a/storage/handler.go +++ b/storage/handler.go @@ -43,31 +43,29 @@ 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 { - handler.Error(4001, "Method not allowed") + commonhttp.Error(w, 4001, "Method not allowed") return } // 解析multipart表单 err := r.ParseMultipartForm(h.maxFileSize) if err != nil { - handler.Error(4002, fmt.Sprintf("Failed to parse form: %v", err)) + commonhttp.Error(w, 4002, fmt.Sprintf("Failed to parse form: %v", err)) return } // 获取文件 file, header, err := r.FormFile("file") if err != nil { - handler.Error(4003, fmt.Sprintf("Failed to get file: %v", err)) + commonhttp.Error(w, 4003, fmt.Sprintf("Failed to get file: %v", err)) return } defer file.Close() // 检查文件大小 if h.maxFileSize > 0 && header.Size > h.maxFileSize { - handler.Error(1001, fmt.Sprintf("File size exceeds limit: %d bytes", h.maxFileSize)) + commonhttp.Error(w, 1001, fmt.Sprintf("File size exceeds limit: %d bytes", h.maxFileSize)) return } @@ -82,7 +80,7 @@ func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } if !allowed { - handler.Error(1002, fmt.Sprintf("File extension not allowed. Allowed: %v", h.allowedExts)) + commonhttp.Error(w, 1002, fmt.Sprintf("File extension not allowed. Allowed: %v", h.allowedExts)) return } } @@ -110,14 +108,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 { - handler.SystemError(fmt.Sprintf("Failed to upload file: %v", err)) + commonhttp.SystemError(w, fmt.Sprintf("Failed to upload file: %v", err)) return } // 获取文件URL fileURL, err := h.storage.GetURL(objectKey, 0) if err != nil { - handler.SystemError(fmt.Sprintf("Failed to get file URL: %v", err)) + commonhttp.SystemError(w, fmt.Sprintf("Failed to get file URL: %v", err)) return } @@ -130,7 +128,7 @@ func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { UploadTime: time.Now(), } - handler.SuccessWithMessage("Upload successful", result) + commonhttp.Success(w, result, "Upload successful") } // generateUniqueFilename 生成唯一文件名 diff --git a/datetime/datetime.go b/tools/datetime.go similarity index 99% rename from datetime/datetime.go rename to tools/datetime.go index 364bf81..f6c15f5 100644 --- a/datetime/datetime.go +++ b/tools/datetime.go @@ -1,4 +1,4 @@ -package datetime +package tools import ( "fmt" diff --git a/tools/money.go b/tools/money.go new file mode 100644 index 0000000..294e999 --- /dev/null +++ b/tools/money.go @@ -0,0 +1,173 @@ +package tools + +import ( + "fmt" + "math" +) + +// MoneyCalculator 金额计算工具(以分为单位) +type MoneyCalculator struct{} + +// NewMoneyCalculator 创建金额计算器 +func NewMoneyCalculator() *MoneyCalculator { + return &MoneyCalculator{} +} + +// YuanToCents 元转分 +func (m *MoneyCalculator) YuanToCents(yuan float64) int64 { + return int64(math.Round(yuan * 100)) +} + +// CentsToYuan 分转元 +func (m *MoneyCalculator) CentsToYuan(cents int64) float64 { + return float64(cents) / 100 +} + +// FormatYuan 格式化显示金额(分转元,保留2位小数) +func (m *MoneyCalculator) FormatYuan(cents int64) string { + return fmt.Sprintf("%.2f", m.CentsToYuan(cents)) +} + +// Add 金额相加 +func (m *MoneyCalculator) Add(amount1, amount2 int64) int64 { + return amount1 + amount2 +} + +// Subtract 金额相减 +func (m *MoneyCalculator) Subtract(amount1, amount2 int64) int64 { + return amount1 - amount2 +} + +// Multiply 金额乘法(金额 * 数量) +func (m *MoneyCalculator) Multiply(amount int64, quantity int) int64 { + return amount * int64(quantity) +} + +// MultiplyFloat 金额乘法(金额 * 浮点数,如折扣) +func (m *MoneyCalculator) MultiplyFloat(amount int64, rate float64) int64 { + return int64(math.Round(float64(amount) * rate)) +} + +// Divide 金额除法(平均分配) +func (m *MoneyCalculator) Divide(amount int64, count int) int64 { + if count <= 0 { + return 0 + } + return amount / int64(count) +} + +// CalculateDiscount 计算折扣金额 +func (m *MoneyCalculator) CalculateDiscount(originalAmount int64, discountRate float64) int64 { + if discountRate <= 0 || discountRate >= 1 { + return 0 + } + return int64(math.Round(float64(originalAmount) * discountRate)) +} + +// CalculatePercentage 计算百分比金额 +func (m *MoneyCalculator) CalculatePercentage(amount int64, percentage float64) int64 { + return int64(math.Round(float64(amount) * percentage / 100)) +} + +// Max 取最大金额 +func (m *MoneyCalculator) Max(amounts ...int64) int64 { + if len(amounts) == 0 { + return 0 + } + max := amounts[0] + for _, amount := range amounts[1:] { + if amount > max { + max = amount + } + } + return max +} + +// Min 取最小金额 +func (m *MoneyCalculator) Min(amounts ...int64) int64 { + if len(amounts) == 0 { + return 0 + } + min := amounts[0] + for _, amount := range amounts[1:] { + if amount < min { + min = amount + } + } + return min +} + +// IsPositive 判断是否为正数 +func (m *MoneyCalculator) IsPositive(amount int64) bool { + return amount > 0 +} + +// IsZero 判断是否为零 +func (m *MoneyCalculator) IsZero(amount int64) bool { + return amount == 0 +} + +// IsNegative 判断是否为负数 +func (m *MoneyCalculator) IsNegative(amount int64) bool { + return amount < 0 +} + +// Equal 判断金额是否相等 +func (m *MoneyCalculator) Equal(amount1, amount2 int64) bool { + return amount1 == amount2 +} + +// Greater 判断第一个金额是否大于第二个 +func (m *MoneyCalculator) Greater(amount1, amount2 int64) bool { + return amount1 > amount2 +} + +// Less 判断第一个金额是否小于第二个 +func (m *MoneyCalculator) Less(amount1, amount2 int64) bool { + return amount1 < amount2 +} + +// Sum 计算金额总和 +func (m *MoneyCalculator) Sum(amounts ...int64) int64 { + var total int64 + for _, amount := range amounts { + total += amount + } + return total +} + +// SplitAmount 金额分摊(处理分摊不均的情况) +func (m *MoneyCalculator) SplitAmount(totalAmount int64, count int) []int64 { + if count <= 0 { + return []int64{} + } + + baseAmount := totalAmount / int64(count) + remainder := totalAmount % int64(count) + + amounts := make([]int64, count) + for i := 0; i < count; i++ { + amounts[i] = baseAmount + if int64(i) < remainder { + amounts[i]++ + } + } + + return amounts +} + +// 全局金额计算器实例 +var Money = NewMoneyCalculator() + +// 便捷函数 +func YuanToCents(yuan float64) int64 { + return Money.YuanToCents(yuan) +} + +func CentsToYuan(cents int64) float64 { + return Money.CentsToYuan(cents) +} + +func FormatYuan(cents int64) string { + return Money.FormatYuan(cents) +} diff --git a/tools/version.go b/tools/version.go new file mode 100644 index 0000000..6bdb1ee --- /dev/null +++ b/tools/version.go @@ -0,0 +1,26 @@ +package tools + +import ( + "os" +) + +// Version 版本号 - 在这里修改版本号(默认值) +const DefaultVersion = "1.0.1" + +// GetVersion 获取版本号 +// 优先从环境变量 DOCKER_TAG 或 VERSION 中读取 +// 如果没有设置环境变量,则使用默认版本号 +func GetVersion() string { + // 优先从 Docker 标签环境变量读取 + if dockerTag := os.Getenv("DOCKER_TAG"); dockerTag != "" { + return dockerTag + } + + // 从通用版本环境变量读取 + if version := os.Getenv("VERSION"); version != "" { + return version + } + + // 使用默认版本号 + return DefaultVersion +}