初始版本,工具基础类

This commit is contained in:
2025-11-30 13:02:34 +08:00
commit ea4e2e305d
37 changed files with 7480 additions and 0 deletions

100
docs/README.md Normal file
View File

@@ -0,0 +1,100 @@
# GoCommon 工具类库文档
## 目录
- [数据库迁移工具](./migration.md) - 数据库版本管理和迁移
- [日期转换工具](./datetime.md) - 日期时间处理和时区转换
- [HTTP Restful工具](./http.md) - HTTP请求响应处理和分页
- [中间件工具](./middleware.md) - CORS和时区处理中间件
- [配置工具](./config.md) - 外部配置文件加载和管理
- [存储工具](./storage.md) - 文件上传和查看OSS、MinIO
- [邮件工具](./email.md) - SMTP邮件发送
- [短信工具](./sms.md) - 阿里云短信发送
## 快速开始
### 安装
```bash
go get github.com/go-common
```
### 使用示例
#### 数据库迁移
```go
import "github.com/go-common/migration"
migrator := migration.NewMigrator(db)
migrator.AddMigration(migration.Migration{
Version: "20240101000001",
Description: "create_users_table",
Up: func(db *gorm.DB) error {
return db.Exec("CREATE TABLE users ...").Error
},
})
migrator.Up()
```
#### 日期转换
```go
import "github.com/go-common/datetime"
datetime.SetDefaultTimeZone(datetime.AsiaShanghai)
now := datetime.Now()
str := datetime.FormatDateTime(now)
```
#### HTTP响应
```go
import "github.com/go-common/http"
http.Success(w, data)
http.SuccessPage(w, list, total, page, pageSize)
http.Error(w, 1001, "业务错误")
```
#### 中间件
```go
import (
"github.com/go-common/middleware"
"github.com/go-common/http"
)
// CORS + 时区中间件
chain := middleware.NewChain(
middleware.CORS(),
middleware.Timezone,
)
handler := chain.ThenFunc(yourHandler)
// 在处理器中获取时区
timezone := http.GetTimezone(r)
```
#### 配置管理
```go
import "github.com/go-common/config"
// 从文件加载配置
cfg, err := config.LoadFromFile("./config.json")
// 获取各种配置
dsn, _ := cfg.GetDatabaseDSN()
redisAddr := cfg.GetRedisAddr()
corsConfig := cfg.GetCORS()
```
## 版本
v1.0.0
## 许可证
MIT

500
docs/config.md Normal file
View File

@@ -0,0 +1,500 @@
# 配置工具文档
## 概述
配置工具提供了从外部文件加载和管理应用配置的功能支持数据库、OSS、Redis、CORS、MinIO、邮件、短信等常用服务的配置。
## 功能特性
- 支持从外部JSON文件加载配置
- 支持数据库配置MySQL、PostgreSQL、SQLite
- 支持OSS对象存储配置阿里云、腾讯云、AWS、七牛云等
- 支持Redis配置
- 支持CORS配置与middleware包集成
- 支持MinIO配置
- 支持邮件配置SMTP
- 支持短信配置(阿里云短信)
- 自动设置默认值
- 自动生成数据库连接字符串DSN
- 自动生成Redis地址
## 配置文件格式
配置文件采用JSON格式支持以下配置项
```json
{
"database": {
"type": "mysql",
"host": "localhost",
"port": 3306,
"user": "root",
"password": "password",
"database": "testdb",
"charset": "utf8mb4",
"maxOpenConns": 100,
"maxIdleConns": 10,
"connMaxLifetime": 3600
},
"oss": {
"provider": "aliyun",
"endpoint": "oss-cn-hangzhou.aliyuncs.com",
"accessKeyId": "your-access-key-id",
"accessKeySecret": "your-access-key-secret",
"bucket": "your-bucket-name",
"region": "cn-hangzhou",
"useSSL": true,
"domain": "https://cdn.example.com"
},
"redis": {
"host": "localhost",
"port": 6379,
"password": "",
"database": 0,
"maxRetries": 3,
"poolSize": 10,
"minIdleConns": 5,
"dialTimeout": 5,
"readTimeout": 3,
"writeTimeout": 3
},
"cors": {
"allowedOrigins": ["*"],
"allowedMethods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
"allowedHeaders": ["Content-Type", "Authorization", "X-Requested-With", "X-Timezone"],
"exposedHeaders": [],
"allowCredentials": false,
"maxAge": 86400
},
"minio": {
"endpoint": "localhost:9000",
"accessKeyId": "minioadmin",
"secretAccessKey": "minioadmin",
"useSSL": false,
"bucket": "test-bucket",
"region": "us-east-1",
"domain": "http://localhost:9000"
},
"email": {
"host": "smtp.example.com",
"port": 587,
"username": "your-email@example.com",
"password": "your-email-password",
"from": "your-email@example.com",
"fromName": "Your App Name",
"useTLS": true,
"useSSL": false,
"timeout": 30
},
"sms": {
"accessKeyId": "your-aliyun-access-key-id",
"accessKeySecret": "your-aliyun-access-key-secret",
"region": "cn-hangzhou",
"signName": "Your Sign Name",
"templateCode": "SMS_123456789",
"endpoint": "",
"timeout": 10
}
}
```
## 使用方法
### 1. 加载配置文件
```go
import "github.com/go-common/config"
// 从文件加载配置(支持绝对路径和相对路径)
config, err := config.LoadFromFile("/path/to/config.json")
if err != nil {
log.Fatal(err)
}
// 或者从字节数组加载
data := []byte(`{"database": {...}}`)
config, err := config.LoadFromBytes(data)
```
### 2. 获取数据库配置
```go
// 获取数据库配置对象
dbConfig := config.GetDatabase()
if dbConfig != nil {
fmt.Printf("Database: %s@%s:%d/%s\n",
dbConfig.User, dbConfig.Host, dbConfig.Port, dbConfig.Database)
}
// 获取数据库连接字符串DSN
dsn, err := config.GetDatabaseDSN()
if err != nil {
log.Fatal(err)
}
// MySQL: "root:password@tcp(localhost:3306)/testdb?charset=utf8mb4&parseTime=True&loc=UTC"
// PostgreSQL: "host=localhost port=5432 user=root password=password dbname=testdb timezone=UTC sslmode=disable"
// 注意数据库时间统一使用UTC时间
```
### 3. 获取OSS配置
```go
ossConfig := config.GetOSS()
if ossConfig != nil {
fmt.Printf("OSS Provider: %s\n", ossConfig.Provider)
fmt.Printf("Endpoint: %s\n", ossConfig.Endpoint)
fmt.Printf("Bucket: %s\n", ossConfig.Bucket)
}
```
### 4. 获取Redis配置
```go
redisConfig := config.GetRedis()
if redisConfig != nil {
fmt.Printf("Redis: %s:%d\n", redisConfig.Host, redisConfig.Port)
}
// 获取Redis地址格式: host:port
addr := config.GetRedisAddr()
// 输出: "localhost:6379"
```
### 5. 获取CORS配置
```go
// 获取CORS配置返回middleware.CORSConfig类型可直接用于中间件
corsConfig := config.GetCORS()
// 使用CORS中间件
import "github.com/go-common/middleware"
chain := middleware.NewChain(
middleware.CORS(corsConfig),
)
```
### 6. 获取MinIO配置
```go
minioConfig := config.GetMinIO()
if minioConfig != nil {
fmt.Printf("MinIO Endpoint: %s\n", minioConfig.Endpoint)
fmt.Printf("Bucket: %s\n", minioConfig.Bucket)
}
```
## 配置项说明
### DatabaseConfig 数据库配置
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| Type | string | 数据库类型: mysql, postgres, sqlite | - |
| Host | string | 数据库主机 | - |
| Port | int | 数据库端口 | - |
| User | string | 数据库用户名 | - |
| Password | string | 数据库密码 | - |
| Database | string | 数据库名称 | - |
| Charset | string | 字符集MySQL使用 | utf8mb4 |
| MaxOpenConns | int | 最大打开连接数 | 100 |
| MaxIdleConns | int | 最大空闲连接数 | 10 |
| ConnMaxLifetime | int | 连接最大生存时间(秒) | 3600 |
| DSN | string | 数据库连接字符串(如果设置,优先使用) | - |
### OSSConfig OSS配置
| 字段 | 类型 | 说明 |
|------|------|------|
| Provider | string | 提供商: aliyun, tencent, aws, qiniu |
| Endpoint | string | 端点地址 |
| AccessKeyID | string | 访问密钥ID |
| AccessKeySecret | string | 访问密钥 |
| Bucket | string | 存储桶名称 |
| Region | string | 区域 |
| UseSSL | bool | 是否使用SSL |
| Domain | string | 自定义域名CDN域名 |
### RedisConfig Redis配置
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| Host | string | Redis主机 | - |
| Port | int | Redis端口 | 6379 |
| Password | string | Redis密码 | - |
| Database | int | Redis数据库编号 | 0 |
| MaxRetries | int | 最大重试次数 | 3 |
| PoolSize | int | 连接池大小 | 10 |
| MinIdleConns | int | 最小空闲连接数 | 5 |
| DialTimeout | int | 连接超时时间(秒) | 5 |
| ReadTimeout | int | 读取超时时间(秒) | 3 |
| WriteTimeout | int | 写入超时时间(秒) | 3 |
| Addr | string | Redis地址如果设置优先使用 | - |
### CORSConfig CORS配置
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| AllowedOrigins | []string | 允许的源 | ["*"] |
| AllowedMethods | []string | 允许的HTTP方法 | ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] |
| AllowedHeaders | []string | 允许的请求头 | ["Content-Type", "Authorization", "X-Requested-With", "X-Timezone"] |
| ExposedHeaders | []string | 暴露给客户端的响应头 | [] |
| AllowCredentials | bool | 是否允许发送凭证 | false |
| MaxAge | int | 预检请求的缓存时间(秒) | 86400 |
### MinIOConfig MinIO配置
| 字段 | 类型 | 说明 |
|------|------|------|
| Endpoint | string | MinIO端点地址 |
| AccessKeyID | string | 访问密钥ID |
| SecretAccessKey | string | 密钥 |
| UseSSL | bool | 是否使用SSL |
| Bucket | string | 存储桶名称 |
| Region | string | 区域 |
| Domain | string | 自定义域名 |
### EmailConfig 邮件配置
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| Host | string | SMTP服务器地址 | - |
| Port | int | SMTP服务器端口 | 587 |
| Username | string | 发件人邮箱 | - |
| Password | string | 邮箱密码或授权码 | - |
| From | string | 发件人邮箱地址如果为空使用Username | Username |
| FromName | string | 发件人名称 | - |
| UseTLS | bool | 是否使用TLS | false |
| UseSSL | bool | 是否使用SSL | false |
| Timeout | int | 连接超时时间(秒) | 30 |
### SMSConfig 短信配置(阿里云短信)
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| AccessKeyID | string | 阿里云AccessKey ID | - |
| AccessKeySecret | string | 阿里云AccessKey Secret | - |
| Region | string | 区域cn-hangzhou | cn-hangzhou |
| SignName | string | 短信签名 | - |
| TemplateCode | string | 短信模板代码 | - |
| Endpoint | string | 服务端点(可选,默认使用区域端点) | - |
| Timeout | int | 请求超时时间(秒) | 10 |
## 完整示例
### 示例1加载配置并使用
```go
package main
import (
"log"
"github.com/go-common/config"
"github.com/go-common/middleware"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 加载配置
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
log.Fatal(err)
}
// 使用数据库配置
dsn, err := cfg.GetDatabaseDSN()
if err != nil {
log.Fatal(err)
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 使用Redis配置
redisAddr := cfg.GetRedisAddr()
fmt.Printf("Redis Address: %s\n", redisAddr)
// 使用CORS配置
corsConfig := cfg.GetCORS()
chain := middleware.NewChain(
middleware.CORS(corsConfig),
)
// 使用OSS配置
ossConfig := cfg.GetOSS()
if ossConfig != nil {
fmt.Printf("OSS Provider: %s\n", ossConfig.Provider)
}
// 使用MinIO配置
minioConfig := cfg.GetMinIO()
if minioConfig != nil {
fmt.Printf("MinIO Endpoint: %s\n", minioConfig.Endpoint)
}
}
```
### 示例2部分配置
配置文件可以只包含需要的配置项:
```json
{
"database": {
"type": "mysql",
"host": "localhost",
"port": 3306,
"user": "root",
"password": "password",
"database": "testdb"
},
"redis": {
"host": "localhost",
"port": 6379
}
}
```
未配置的部分会返回nil需要在使用前检查
```go
cfg, _ := config.LoadFromFile("./config.json")
dbConfig := cfg.GetDatabase()
if dbConfig != nil {
// 使用数据库配置
}
ossConfig := cfg.GetOSS()
if ossConfig == nil {
// OSS未配置
}
```
### 示例3使用DSN字段
如果配置文件中直接提供了DSN会优先使用
```json
{
"database": {
"type": "mysql",
"dsn": "root:password@tcp(localhost:3306)/testdb?charset=utf8mb4&parseTime=True"
}
}
```
```go
dsn, err := cfg.GetDatabaseDSN()
// 直接返回配置中的DSN不会重新构建
```
## API 参考
### LoadFromFile(filePath string) (*Config, error)
从文件加载配置。
**参数:**
- `filePath`: 配置文件路径(支持绝对路径和相对路径)
**返回:** 配置对象和错误信息
### LoadFromBytes(data []byte) (*Config, error)
从字节数组加载配置。
**参数:**
- `data`: JSON格式的配置数据
**返回:** 配置对象和错误信息
### (c *Config) GetDatabase() *DatabaseConfig
获取数据库配置。
**返回:** 数据库配置对象可能为nil
### (c *Config) GetDatabaseDSN() (string, error)
获取数据库连接字符串。
**返回:** DSN字符串和错误信息
### (c *Config) GetOSS() *OSSConfig
获取OSS配置。
**返回:** OSS配置对象可能为nil
### (c *Config) GetRedis() *RedisConfig
获取Redis配置。
**返回:** Redis配置对象可能为nil
### (c *Config) GetRedisAddr() string
获取Redis地址格式: host:port
**返回:** Redis地址字符串
### (c *Config) GetCORS() *middleware.CORSConfig
获取CORS配置并转换为middleware.CORSConfig类型。
**返回:** CORS配置对象如果配置为nil返回默认配置
### (c *Config) GetMinIO() *MinIOConfig
获取MinIO配置。
**返回:** MinIO配置对象可能为nil
### (c *Config) GetEmail() *EmailConfig
获取邮件配置。
**返回:** 邮件配置对象可能为nil
### (c *Config) GetSMS() *SMSConfig
获取短信配置。
**返回:** 短信配置对象可能为nil
## 注意事项
1. **配置文件路径**
- 支持绝对路径和相对路径
- 相对路径基于当前工作目录
2. **默认值**
- 配置加载时会自动设置默认值
- 如果配置项为nil对应的Get方法会返回nil
3. **DSN优先级**
- 如果配置中设置了DSN字段会优先使用
- 否则会根据配置项自动构建DSN
4. **配置验证**
- 当前版本不进行配置验证,请确保配置正确
- 建议在生产环境中添加配置验证逻辑
5. **安全性**
- 配置文件可能包含敏感信息(密码、密钥等)
- 建议将配置文件放在安全的位置,不要提交到版本控制系统
- 可以使用环境变量或配置管理服务
6. **数据库时区**
- 数据库时间统一使用UTC时间存储
- DSN中会自动设置UTC时区MySQL: loc=UTC, PostgreSQL: timezone=UTC
- 时区转换应在应用层处理使用datetime工具包进行时区转换
## 配置文件示例
完整配置文件示例请参考 `config/example.json`

461
docs/datetime.md Normal file
View File

@@ -0,0 +1,461 @@
# 日期转换工具文档
## 概述
日期转换工具提供了丰富的日期时间处理功能,支持时区设定、格式转换、时间计算等常用操作。
## 功能特性
- 支持时区设定和转换
- 支持多种时间格式的解析和格式化
- 提供常用时间格式常量
- 支持Unix时间戳转换
- 提供时间计算功能(添加天数、月数、年数等)
- 提供时间范围获取功能(开始/结束时间)
- 支持将任意时区时间转换为UTC时间用于数据库存储
## 使用方法
### 1. 设置默认时区
```go
import "github.com/go-common/datetime"
// 设置默认时区为上海时区
err := datetime.SetDefaultTimeZone(datetime.AsiaShanghai)
if err != nil {
log.Fatal(err)
}
```
### 2. 获取当前时间
```go
// 使用默认时区
now := datetime.Now()
// 使用指定时区
now := datetime.Now(datetime.AsiaShanghai)
now := datetime.Now("America/New_York")
```
### 3. 解析时间字符串
```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 := datetime.Parse("2006-01-02 15:04:05", "2024-01-01 12:00:00", datetime.AsiaShanghai)
// 使用常用格式解析
t, err := datetime.ParseDateTime("2024-01-01 12:00:00")
t, err := datetime.ParseDate("2024-01-01")
```
### 4. 格式化时间
```go
t := time.Now()
// 使用默认时区格式化
str := datetime.Format(t, "2006-01-02 15:04:05")
// 使用指定时区格式化
str := datetime.Format(t, "2006-01-02 15:04:05", datetime.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"
```
### 5. 时区转换
```go
t := time.Now()
// 转换到指定时区
t2, err := datetime.ToTimezone(t, datetime.AsiaShanghai)
if err != nil {
log.Fatal(err)
}
```
### 6. Unix时间戳转换
```go
t := time.Now()
// 转换为Unix时间戳
unix := datetime.ToUnix(t)
// 从Unix时间戳创建时间
t2 := datetime.FromUnix(unix)
// 转换为Unix毫秒时间戳
unixMilli := datetime.ToUnixMilli(t)
// 从Unix毫秒时间戳创建时间
t3 := datetime.FromUnixMilli(unixMilli)
```
### 7. 时间计算
```go
t := time.Now()
// 添加天数
t1 := datetime.AddDays(t, 7)
// 添加月数
t2 := datetime.AddMonths(t, 1)
// 添加年数
t3 := datetime.AddYears(t, 1)
```
### 8. 时间范围获取
```go
t := time.Now()
// 获取一天的开始时间00:00:00
start := datetime.StartOfDay(t)
// 获取一天的结束时间23:59:59.999999999
end := datetime.EndOfDay(t)
// 获取月份的开始时间
monthStart := datetime.StartOfMonth(t)
// 获取月份的结束时间
monthEnd := datetime.EndOfMonth(t)
// 获取年份的开始时间
yearStart := datetime.StartOfYear(t)
// 获取年份的结束时间
yearEnd := datetime.EndOfYear(t)
```
### 9. 时间差计算
```go
t1 := time.Now()
t2 := time.Now().Add(24 * time.Hour)
// 计算天数差
days := datetime.DiffDays(t1, t2)
// 计算小时差
hours := datetime.DiffHours(t1, t2)
// 计算分钟差
minutes := datetime.DiffMinutes(t1, t2)
// 计算秒数差
seconds := datetime.DiffSeconds(t1, t2)
```
### 10. 转换为UTC时间用于数据库存储
```go
// 将任意时区的时间转换为UTC
t := time.Now() // 当前时区的时间
utcTime := datetime.ToUTC(t)
// 从指定时区转换为UTC
t, _ := datetime.ParseDateTime("2024-01-01 12:00:00", datetime.AsiaShanghai)
utcTime, err := datetime.ToUTCFromTimezone(t, datetime.AsiaShanghai)
// 解析时间字符串并直接转换为UTC
utcTime, err := datetime.ParseDateTimeToUTC("2024-01-01 12:00:00", datetime.AsiaShanghai)
// 解析日期并转换为UTC当天的00:00:00 UTC
utcTime, err := datetime.ParseDateToUTC("2024-01-01", datetime.AsiaShanghai)
```
## API 参考
### 时区常量
```go
const (
UTC = "UTC"
AsiaShanghai = "Asia/Shanghai"
AmericaNewYork = "America/New_York"
EuropeLondon = "Europe/London"
AsiaTokyo = "Asia/Tokyo"
)
```
### 常用时间格式
```go
CommonLayouts.DateTime = "2006-01-02 15:04"
CommonLayouts.DateTimeSec = "2006-01-02 15:04:05"
CommonLayouts.Date = "2006-01-02"
CommonLayouts.Time = "15:04"
CommonLayouts.TimeSec = "15:04:05"
CommonLayouts.ISO8601 = "2006-01-02T15:04:05Z07:00"
CommonLayouts.RFC3339 = time.RFC3339
CommonLayouts.RFC3339Nano = time.RFC3339Nano
```
### 主要函数
#### SetDefaultTimeZone(timezone string) error
设置默认时区。
**参数:**
- `timezone`: 时区字符串,如 "Asia/Shanghai"
**返回:** 错误信息
#### Now(timezone ...string) time.Time
获取当前时间。
**参数:**
- `timezone`: 可选,时区字符串,不指定则使用默认时区
**返回:** 时间对象
#### Parse(layout, value string, timezone ...string) (time.Time, error)
解析时间字符串。
**参数:**
- `layout`: 时间格式,如 "2006-01-02 15:04:05"
- `value`: 时间字符串
- `timezone`: 可选,时区字符串
**返回:** 时间对象和错误信息
#### Format(t time.Time, layout string, timezone ...string) string
格式化时间。
**参数:**
- `t`: 时间对象
- `layout`: 时间格式
- `timezone`: 可选,时区字符串
**返回:** 格式化后的时间字符串
#### ToTimezone(t time.Time, timezone string) (time.Time, error)
转换时区。
**参数:**
- `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
计算两个时间之间的秒数差。
### UTC转换函数
#### ToUTC(t time.Time) time.Time
将时间转换为UTC时间。
**参数:**
- `t`: 时间对象(可以是任意时区)
**返回:** UTC时间
#### ToUTCFromTimezone(t time.Time, timezone string) (time.Time, error)
从指定时区转换为UTC时间。
**参数:**
- `t`: 时间对象(会被视为指定时区的时间)
- `timezone`: 源时区
**返回:** UTC时间和错误信息
#### ParseToUTC(layout, value string, timezone ...string) (time.Time, error)
解析时间字符串并转换为UTC时间。
**参数:**
- `layout`: 时间格式,如 "2006-01-02 15:04:05"
- `value`: 时间字符串
- `timezone`: 源时区,如果为空则使用默认时区
**返回:** UTC时间和错误信息
#### ParseDateTimeToUTC(value string, timezone ...string) (time.Time, error)
解析日期时间字符串并转换为UTC时间便捷方法
**参数:**
- `value`: 时间字符串(格式: 2006-01-02 15:04:05
- `timezone`: 源时区,如果为空则使用默认时区
**返回:** UTC时间和错误信息
#### ParseDateToUTC(value string, timezone ...string) (time.Time, error)
解析日期字符串并转换为UTC时间便捷方法
**参数:**
- `value`: 日期字符串(格式: 2006-01-02
- `timezone`: 源时区,如果为空则使用默认时区
**返回:** UTC时间当天的00:00:00 UTC时间和错误信息
## 注意事项
1. **时区字符串**必须符合IANA时区数据库格式
2. **默认时区**默认时区为UTC建议在应用启动时设置合适的默认时区
3. **时间格式**时间格式字符串必须使用Go的特定时间2006-01-02 15:04:05
4. **时间范围函数**所有时间范围函数StartOfDay、EndOfDay等都会考虑时区
5. **数据库存储**
- 数据库时间统一使用UTC时间存储
- 使用`ToUTC``ParseToUTC`等方法将时间转换为UTC后存储到数据库
- 从数据库读取UTC时间后使用`ToTimezone`转换为用户时区显示
## 完整示例
### 示例1基本使用
```go
package main
import (
"fmt"
"log"
"time"
"github.com/go-common/datetime"
)
func main() {
// 设置默认时区
datetime.SetDefaultTimeZone(datetime.AsiaShanghai)
// 获取当前时间
now := datetime.Now()
fmt.Printf("Current time: %s\n", datetime.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))
}
```
### 示例2UTC转换数据库存储场景
```go
package main
import (
"fmt"
"log"
"github.com/go-common/datetime"
)
func main() {
// 从请求中获取时间(假设是上海时区)
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))
}
```
完整示例请参考:
- `examples/datetime_example.go` - 基本使用示例
- `examples/datetime_utc_example.go` - UTC转换示例

332
docs/email.md Normal file
View File

@@ -0,0 +1,332 @@
# 邮件工具文档
## 概述
邮件工具提供了SMTP邮件发送功能使用Go标准库实现无需第三方依赖。
## 功能特性
- 支持SMTP邮件发送
- 支持TLS/SSL加密
- 支持发送原始邮件内容(完全由外部控制)
- 支持便捷方法发送简单邮件
- 使用配置工具统一管理配置
## 使用方法
### 1. 创建邮件发送器
```go
import (
"github.com/go-common/config"
"github.com/go-common/email"
)
// 从配置加载
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
log.Fatal(err)
}
emailConfig := cfg.GetEmail()
if emailConfig == nil {
log.Fatal("email config is nil")
}
// 创建邮件发送器
mailer, err := email.NewEmail(emailConfig)
if err != nil {
log.Fatal(err)
}
```
### 2. 发送原始邮件内容(推荐,最灵活)
```go
// 外部构建完整的邮件内容MIME格式
emailBody := []byte(`From: sender@example.com
To: recipient@example.com
Subject: 邮件主题
Content-Type: text/html; charset=UTF-8
<html>
<body>
<h1>邮件内容</h1>
<p>这是由外部构建的完整邮件内容</p>
</body>
</html>
`)
// 发送邮件工具只负责SMTP发送不构建内容
err := mailer.SendRaw(
[]string{"recipient@example.com"}, // 收件人列表
emailBody, // 完整的邮件内容
)
if err != nil {
log.Fatal(err)
}
```
### 3. 发送简单邮件(便捷方法)
```go
// 发送纯文本邮件(内部会构建邮件内容)
err := mailer.SendSimple(
[]string{"recipient@example.com"},
"邮件主题",
"邮件正文内容",
)
if err != nil {
log.Fatal(err)
}
```
### 4. 发送HTML邮件便捷方法
```go
// 发送HTML邮件内部会构建邮件内容
htmlBody := `
<html>
<body>
<h1>欢迎</h1>
<p>这是一封HTML邮件</p>
</body>
</html>
`
err := mailer.SendHTML(
[]string{"recipient@example.com"},
"邮件主题",
htmlBody,
)
if err != nil {
log.Fatal(err)
}
```
### 5. 使用Message结构发送便捷方法
```go
import "github.com/go-common/email"
msg := &email.Message{
To: []string{"to@example.com"},
Cc: []string{"cc@example.com"},
Bcc: []string{"bcc@example.com"},
Subject: "邮件主题",
Body: "纯文本正文",
HTMLBody: "<html><body><h1>HTML正文</h1></body></html>",
}
err := mailer.Send(msg)
if err != nil {
log.Fatal(err)
}
```
## API 参考
### NewEmail(cfg *config.EmailConfig) (*Email, error)
创建邮件发送器。
**参数:**
- `cfg`: 邮件配置对象
**返回:** 邮件发送器实例和错误信息
### (e *Email) SendRaw(recipients []string, body []byte) error
发送原始邮件内容(推荐使用,最灵活)。
**参数:**
- `recipients`: 收件人列表To、Cc、Bcc的合并列表
- `body`: 完整的邮件内容MIME格式由外部构建
**返回:** 错误信息
**说明:** 此方法允许外部完全控制邮件内容工具只负责SMTP发送。
### (e *Email) Send(msg *Message) error
发送邮件使用Message结构内部会构建邮件内容
**参数:**
- `msg`: 邮件消息对象
**返回:** 错误信息
**说明:** 如果需要完全控制邮件内容请使用SendRaw方法。
### (e *Email) SendSimple(to []string, subject, body string) error
发送简单邮件(便捷方法)。
**参数:**
- `to`: 收件人列表
- `subject`: 主题
- `body`: 正文
### (e *Email) SendHTML(to []string, subject, htmlBody string) error
发送HTML邮件便捷方法
**参数:**
- `to`: 收件人列表
- `subject`: 主题
- `htmlBody`: HTML正文
### Message 结构体
```go
type Message struct {
To []string // 收件人列表
Cc []string // 抄送列表(可选)
Bcc []string // 密送列表(可选)
Subject string // 主题
Body string // 正文(纯文本)
HTMLBody string // HTML正文可选
Attachments []Attachment // 附件列表(可选)
}
```
### Attachment 结构体
```go
type Attachment struct {
Filename string // 文件名
Content []byte // 文件内容
ContentType string // 文件类型
}
```
## 配置说明
邮件配置通过 `config.EmailConfig` 提供:
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| Host | string | SMTP服务器地址 | - |
| Port | int | SMTP服务器端口 | 587 |
| Username | string | 发件人邮箱 | - |
| Password | string | 邮箱密码或授权码 | - |
| From | string | 发件人邮箱地址 | Username |
| FromName | string | 发件人名称 | - |
| UseTLS | bool | 是否使用TLS | false |
| UseSSL | bool | 是否使用SSL | false |
| Timeout | int | 连接超时时间(秒) | 30 |
## 常见SMTP服务器配置
### Gmail
```json
{
"host": "smtp.gmail.com",
"port": 587,
"useTLS": true,
"useSSL": false
}
```
### QQ邮箱
```json
{
"host": "smtp.qq.com",
"port": 587,
"useTLS": true,
"useSSL": false
}
```
### 163邮箱
```json
{
"host": "smtp.163.com",
"port": 25,
"useTLS": false,
"useSSL": false
}
```
### 企业邮箱SSL
```json
{
"host": "smtp.exmail.qq.com",
"port": 465,
"useTLS": false,
"useSSL": true
}
```
## 注意事项
1. **推荐使用SendRaw方法**
- `SendRaw`方法允许外部完全控制邮件内容
- 可以构建任意格式的MIME邮件包括复杂附件、多部分内容等
- 工具只负责SMTP发送不构建内容
2. **邮件内容构建**
- 使用`SendRaw`需要外部构建完整的MIME格式邮件内容
- 可以参考RFC 5322标准构建邮件内容
- 便捷方法(`Send``SendSimple``SendHTML`)内部会构建简单格式的邮件内容
3. **密码/授权码**
- 很多邮箱服务商需要使用授权码而不是登录密码
- Gmail、QQ邮箱等需要开启SMTP服务并获取授权码
4. **端口选择**
- 587端口通常使用TLSSTARTTLS
- 465端口通常使用SSL
- 25端口通常不使用加密不推荐
5. **TLS vs SSL**
- UseTLS=true使用STARTTLS推荐端口587
- UseSSL=true使用SSL端口465
6. **错误处理**
- 所有操作都应该进行错误处理
- 建议记录详细的错误日志
## 完整示例
```go
package main
import (
"log"
"github.com/go-common/config"
"github.com/go-common/email"
)
func main() {
// 加载配置
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
log.Fatal(err)
}
// 创建邮件发送器
mailer, err := email.NewEmail(cfg.GetEmail())
if err != nil {
log.Fatal(err)
}
// 发送邮件
err = mailer.SendSimple(
[]string{"recipient@example.com"},
"测试邮件",
"这是一封测试邮件",
)
if err != nil {
log.Fatal(err)
}
log.Println("邮件发送成功")
}
```
## 示例
完整示例请参考 `examples/email_example.go`

423
docs/http.md Normal file
View File

@@ -0,0 +1,423 @@
# HTTP Restful工具文档
## 概述
HTTP Restful工具提供了标准化的HTTP请求和响应处理功能包含统一的响应结构、分页支持和HTTP状态码与业务状态码的分离。
## 功能特性
- 标准化的响应结构:`{code, message, timestamp, data}`
- 分离HTTP状态码和业务状态码
- 支持分页响应
- 提供便捷的请求参数解析方法
- 支持JSON请求体解析
- 提供常用的HTTP错误响应方法
## 响应结构
### 标准响应结构
```json
{
"code": 0,
"message": "success",
"timestamp": 1704067200,
"data": {}
}
```
### 分页响应结构
```json
{
"code": 0,
"message": "success",
"timestamp": 1704067200,
"data": {
"list": [],
"total": 100,
"page": 1,
"pageSize": 10
}
}
```
## 使用方法
### 1. 成功响应
```go
import (
"net/http"
"github.com/go-common/http"
)
// 简单成功响应data为nil
http.Success(w, nil)
// 带数据的成功响应
data := map[string]interface{}{
"id": 1,
"name": "test",
}
http.Success(w, data)
// 带消息的成功响应
http.SuccessWithMessage(w, "操作成功", data)
```
### 2. 错误响应
```go
// 业务错误HTTP 200业务code非0
http.Error(w, 1001, "用户不存在")
// 系统错误HTTP 500
http.SystemError(w, "服务器内部错误")
// 请求错误HTTP 400
http.BadRequest(w, "请求参数错误")
// 未授权HTTP 401
http.Unauthorized(w, "未登录")
// 禁止访问HTTP 403
http.Forbidden(w, "无权限访问")
// 未找到HTTP 404
http.NotFound(w, "资源不存在")
```
### 3. 分页响应
```go
// 获取分页参数
page, pageSize := http.GetPaginationParams(r)
// 查询数据(示例)
list, total := getDataList(page, pageSize)
// 返回分页响应
http.SuccessPage(w, list, total, page, pageSize)
// 带消息的分页响应
http.SuccessPageWithMessage(w, "查询成功", list, total, page, pageSize)
```
### 4. 解析请求
#### 解析JSON请求体
```go
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
var req CreateUserRequest
err := http.ParseJSON(r, &req)
if err != nil {
http.BadRequest(w, "请求参数解析失败")
return
}
```
#### 获取查询参数
```go
// 获取字符串参数
name := http.GetQuery(r, "name", "")
email := http.GetQuery(r, "email", "default@example.com")
// 获取整数参数
id := http.GetQueryInt(r, "id", 0)
age := http.GetQueryInt(r, "age", 18)
// 获取int64参数
userId := http.GetQueryInt64(r, "userId", 0)
// 获取布尔参数
isActive := http.GetQueryBool(r, "isActive", false)
// 获取浮点数参数
price := http.GetQueryFloat64(r, "price", 0.0)
```
#### 获取表单参数
```go
// 获取表单字符串
name := http.GetFormValue(r, "name", "")
// 获取表单整数
age := http.GetFormInt(r, "age", 0)
// 获取表单int64
userId := http.GetFormInt64(r, "userId", 0)
// 获取表单布尔值
isActive := http.GetFormBool(r, "isActive", false)
```
#### 获取请求头
```go
token := http.GetHeader(r, "Authorization", "")
contentType := http.GetHeader(r, "Content-Type", "application/json")
```
#### 获取分页参数
```go
// 自动解析page和pageSize参数
// 默认: page=1, pageSize=10
// 限制: pageSize最大1000
page, pageSize := http.GetPaginationParams(r)
// 计算数据库查询偏移量
offset := http.GetOffset(page, pageSize)
```
### 5. 自定义响应
```go
// 使用WriteJSON自定义响应
http.WriteJSON(w, http.StatusOK, 0, "success", data)
// 参数说明:
// - httpCode: HTTP状态码200, 400, 500等
// - code: 业务状态码0表示成功非0表示业务错误
// - message: 响应消息
// - data: 响应数据
```
## 完整示例
```go
package main
import (
"net/http"
"github.com/go-common/http"
)
// 用户列表接口
func GetUserList(w http.ResponseWriter, r *http.Request) {
// 获取分页参数
page, pageSize := http.GetPaginationParams(r)
// 获取查询参数
keyword := http.GetQuery(r, "keyword", "")
// 查询数据
users, total := queryUsers(keyword, page, pageSize)
// 返回分页响应
http.SuccessPage(w, users, total, page, pageSize)
}
// 创建用户接口
func CreateUser(w http.ResponseWriter, r *http.Request) {
// 解析请求体
var req struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := http.ParseJSON(r, &req); err != nil {
http.BadRequest(w, "请求参数解析失败")
return
}
// 参数验证
if req.Name == "" {
http.Error(w, 1001, "用户名不能为空")
return
}
// 创建用户
user, err := createUser(req.Name, req.Email)
if err != nil {
http.SystemError(w, "创建用户失败")
return
}
// 返回成功响应
http.SuccessWithMessage(w, "创建成功", user)
}
// 获取用户详情接口
func GetUser(w http.ResponseWriter, r *http.Request) {
// 获取路径参数(需要配合路由框架使用)
id := http.GetQueryInt64(r, "id", 0)
if id == 0 {
http.BadRequest(w, "用户ID不能为空")
return
}
// 查询用户
user, err := getUserByID(id)
if err != nil {
http.SystemError(w, "查询用户失败")
return
}
if user == nil {
http.Error(w, 1002, "用户不存在")
return
}
http.Success(w, user)
}
```
## API 参考
### 响应方法
#### Success(w http.ResponseWriter, data interface{})
成功响应HTTP 200业务code 0。
#### SuccessWithMessage(w http.ResponseWriter, message string, data interface{})
带消息的成功响应。
#### Error(w http.ResponseWriter, code int, message string)
业务错误响应HTTP 200业务code非0。
#### SystemError(w http.ResponseWriter, message string)
系统错误响应HTTP 500业务code 500。
#### BadRequest(w http.ResponseWriter, message string)
请求错误响应HTTP 400。
#### Unauthorized(w http.ResponseWriter, message string)
未授权响应HTTP 401。
#### Forbidden(w http.ResponseWriter, message string)
禁止访问响应HTTP 403。
#### NotFound(w http.ResponseWriter, message string)
未找到响应HTTP 404。
#### WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{})
写入JSON响应自定义
**参数:**
- `httpCode`: HTTP状态码
- `code`: 业务状态码
- `message`: 响应消息
- `data`: 响应数据
#### SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int)
分页成功响应。
#### SuccessPageWithMessage(w http.ResponseWriter, message string, list interface{}, total int64, page, pageSize int)
带消息的分页成功响应。
### 请求方法
#### ParseJSON(r *http.Request, v interface{}) error
解析JSON请求体。
#### 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
获取查询参数int64
#### GetQueryBool(r *http.Request, key string, defaultValue bool) bool
获取查询参数(布尔值)。
#### GetQueryFloat64(r *http.Request, key string, defaultValue float64) float64
获取查询参数(浮点数)。
#### GetFormValue(r *http.Request, key, defaultValue string) string
获取表单值(字符串)。
#### GetFormInt(r *http.Request, key string, defaultValue int) int
获取表单值(整数)。
#### GetFormInt64(r *http.Request, key string, defaultValue int64) int64
获取表单值int64
#### GetFormBool(r *http.Request, key string, defaultValue bool) bool
获取表单值(布尔值)。
#### GetHeader(r *http.Request, key, defaultValue string) string
获取请求头。
#### GetPaginationParams(r *http.Request) (page, pageSize int)
获取分页参数。
**返回:** page页码最小1pageSize每页大小最小1最大1000
#### GetOffset(page, pageSize int) int
根据页码和每页大小计算偏移量。
## 状态码说明
### HTTP状态码
- `200`: 正常响应(包括业务错误)
- `400`: 请求参数错误
- `401`: 未授权
- `403`: 禁止访问
- `404`: 资源不存在
- `500`: 系统内部错误
### 业务状态码
- `0`: 成功
- `非0`: 业务错误(具体错误码由业务定义)
## 注意事项
1. **HTTP状态码与业务状态码分离**
- 业务错误如用户不存在、参数验证失败等返回HTTP 200业务code非0
- 只有系统异常如数据库连接失败、程序panic等才返回HTTP 500
2. **分页参数限制**
- page最小值为1
- pageSize最小值为1最大值为1000
3. **响应格式统一**
- 所有响应都遵循标准结构
- timestamp为Unix时间戳
4. **错误处理**
- 使用Error方法返回业务错误
- 使用SystemError返回系统错误
- 使用BadRequest等返回HTTP级别的错误
## 示例
完整示例请参考 `examples/http_example.go`

438
docs/middleware.md Normal file
View File

@@ -0,0 +1,438 @@
# 中间件工具文档
## 概述
中间件工具提供了常用的HTTP中间件功能包括CORS处理和时区管理。
## 功能特性
- **CORS中间件**:支持跨域资源共享配置
- **时区中间件**:从请求头读取时区信息,支持默认时区设置
- **中间件链**:提供便捷的中间件链式调用
## CORS中间件
### 功能说明
CORS中间件用于处理跨域资源共享支持
- 配置允许的源(支持通配符)
- 配置允许的HTTP方法
- 配置允许的请求头
- 配置暴露的响应头
- 支持凭证传递
- 预检请求缓存时间设置
### 使用方法
#### 基本使用(默认配置)
```go
import (
"net/http"
"github.com/go-common/middleware"
)
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 处理请求
})
// 使用默认CORS配置
corsHandler := middleware.CORS()(handler)
http.Handle("/api", corsHandler)
http.ListenAndServe(":8080", nil)
}
```
#### 自定义配置
```go
import (
"net/http"
"github.com/go-common/middleware"
)
func main() {
// 自定义CORS配置
corsConfig := &middleware.CORSConfig{
AllowedOrigins: []string{
"https://example.com",
"https://app.example.com",
"*.example.com", // 支持通配符
},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{
"Content-Type",
"Authorization",
"X-Requested-With",
"X-Timezone",
},
ExposedHeaders: []string{"X-Total-Count"},
AllowCredentials: true,
MaxAge: 3600, // 1小时
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 处理请求
})
corsHandler := middleware.CORS(corsConfig)(handler)
http.Handle("/api", corsHandler)
http.ListenAndServe(":8080", nil)
}
```
#### 允许所有源(开发环境)
```go
corsConfig := &middleware.CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"*"},
}
corsHandler := middleware.CORS(corsConfig)(handler)
```
### CORSConfig 配置说明
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| AllowedOrigins | []string | 允许的源,支持 "*" 和 "*.example.com" | ["*"] |
| AllowedMethods | []string | 允许的HTTP方法 | ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] |
| AllowedHeaders | []string | 允许的请求头 | ["Content-Type", "Authorization", "X-Requested-With", "X-Timezone"] |
| ExposedHeaders | []string | 暴露给客户端的响应头 | [] |
| AllowCredentials | bool | 是否允许发送凭证 | false |
| MaxAge | int | 预检请求缓存时间(秒) | 86400 |
### 注意事项
1. 如果 `AllowCredentials``true``AllowedOrigins` 不能使用 "*",必须指定具体的源
2. 通配符支持:`"*.example.com"` 会匹配 `"https://app.example.com"` 等子域名
3. 预检请求OPTIONS会自动处理无需在业务代码中处理
## 时区中间件
### 功能说明
时区中间件用于从请求头读取时区信息并存储到context中方便后续使用。
- 从请求头 `X-Timezone` 读取时区
- 如果未传递时区信息,使用默认时区 `AsiaShanghai`
- 时区信息存储到context中可通过 `http.GetTimezone()` 获取
- 自动验证时区有效性,无效时区会回退到默认时区
### 使用方法
#### 基本使用(默认时区 AsiaShanghai
```go
import (
"net/http"
"github.com/go-common/middleware"
"github.com/go-common/http"
"github.com/go-common/datetime"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 从context获取时区
timezone := http.GetTimezone(r)
// 使用时区
now := datetime.Now(timezone)
datetime.FormatDateTime(now, timezone)
http.Success(w, map[string]interface{}{
"timezone": timezone,
"time": datetime.FormatDateTime(now),
})
}
func main() {
handler := middleware.Timezone(http.HandlerFunc(handler))
http.Handle("/api", handler)
http.ListenAndServe(":8080", nil)
}
```
#### 自定义默认时区
```go
import (
"net/http"
"github.com/go-common/middleware"
"github.com/go-common/datetime"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 处理请求
}
func main() {
// 使用自定义默认时区
handler := middleware.TimezoneWithDefault(datetime.UTC)(http.HandlerFunc(handler))
http.Handle("/api", handler)
http.ListenAndServe(":8080", nil)
}
```
#### 在业务代码中使用时区
```go
import (
"net/http"
"github.com/go-common/http"
"github.com/go-common/datetime"
)
func GetUserList(w http.ResponseWriter, r *http.Request) {
// 从请求context获取时区
timezone := http.GetTimezone(r)
// 使用时区进行时间处理
now := datetime.Now(timezone)
// 查询数据时使用时区
startTime := datetime.StartOfDay(now, timezone)
endTime := datetime.EndOfDay(now, timezone)
// 返回数据
http.Success(w, map[string]interface{}{
"timezone": timezone,
"startTime": datetime.FormatDateTime(startTime),
"endTime": datetime.FormatDateTime(endTime),
})
}
```
### 请求头格式
客户端需要在请求头中传递时区信息:
```
X-Timezone: Asia/Shanghai
```
支持的时区格式IANA时区数据库
- `Asia/Shanghai`
- `America/New_York`
- `Europe/London`
- `UTC`
- 等等
### 注意事项
1. 如果请求头中未传递 `X-Timezone`,默认使用 `AsiaShanghai`
2. 如果传递的时区无效,会自动回退到默认时区
3. 时区信息存储在context中可以在整个请求生命周期中使用
4. 建议在CORS配置中包含 `X-Timezone` 请求头
## 中间件链
### 功能说明
中间件链提供便捷的中间件组合方式,支持链式调用。
### 使用方法
```go
import (
"net/http"
"github.com/go-common/middleware"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 处理请求
}
func main() {
// 创建中间件链
chain := middleware.NewChain(
middleware.CORS(),
middleware.Timezone,
)
// 应用到处理器
handler := chain.ThenFunc(handler)
http.Handle("/api", handler)
http.ListenAndServe(":8080", nil)
}
```
#### 链式追加中间件
```go
chain := middleware.NewChain(middleware.CORS())
chain.Append(middleware.Timezone)
handler := chain.ThenFunc(handler)
```
## 完整示例
### 示例1CORS + 时区中间件
```go
package main
import (
"log"
"net/http"
"github.com/go-common/middleware"
"github.com/go-common/http"
"github.com/go-common/datetime"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
// 获取时区
timezone := http.GetTimezone(r)
now := datetime.Now(timezone)
http.Success(w, map[string]interface{}{
"message": "Hello",
"timezone": timezone,
"time": datetime.FormatDateTime(now),
})
}
func main() {
// 配置CORS
corsConfig := &middleware.CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Timezone"},
}
// 创建中间件链
chain := middleware.NewChain(
middleware.CORS(corsConfig),
middleware.Timezone,
)
// 应用中间件
handler := chain.ThenFunc(apiHandler)
http.Handle("/api", handler)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
### 示例2与路由框架集成
```go
package main
import (
"net/http"
"github.com/go-common/middleware"
"github.com/go-common/http"
)
func main() {
mux := http.NewServeMux()
// 创建中间件链
chain := middleware.NewChain(
middleware.CORS(),
middleware.Timezone,
)
// 应用中间件到所有路由
mux.Handle("/api/users", chain.ThenFunc(getUsers))
mux.Handle("/api/posts", chain.ThenFunc(getPosts))
http.ListenAndServe(":8080", mux)
}
func getUsers(w http.ResponseWriter, r *http.Request) {
timezone := http.GetTimezone(r)
// 处理逻辑
http.Success(w, nil)
}
func getPosts(w http.ResponseWriter, r *http.Request) {
timezone := http.GetTimezone(r)
// 处理逻辑
http.Success(w, nil)
}
```
## API 参考
### CORS中间件
#### CORS(config ...*CORSConfig) func(http.Handler) http.Handler
创建CORS中间件。
**参数:**
- `config`: 可选的CORS配置不指定则使用默认配置
**返回:** 中间件函数
#### DefaultCORSConfig() *CORSConfig
返回默认的CORS配置。
### 时区中间件
#### Timezone(next http.Handler) http.Handler
时区处理中间件(默认时区为 AsiaShanghai
#### TimezoneWithDefault(defaultTimezone string) func(http.Handler) http.Handler
时区处理中间件(可自定义默认时区)。
**参数:**
- `defaultTimezone`: 默认时区字符串
**返回:** 中间件函数
#### GetTimezoneFromContext(ctx context.Context) string
从context中获取时区。
### 中间件链
#### NewChain(middlewares ...func(http.Handler) http.Handler) *Chain
创建新的中间件链。
#### (c *Chain) Then(handler http.Handler) http.Handler
将中间件链应用到处理器。
#### (c *Chain) ThenFunc(handler http.HandlerFunc) http.Handler
将中间件链应用到处理器函数。
#### (c *Chain) Append(middlewares ...func(http.Handler) http.Handler) *Chain
追加中间件到链中。
## 注意事项
1. **CORS配置**
- 生产环境建议明确指定允许的源,避免使用 "*"
- 如果使用凭证cookies必须明确指定源不能使用 "*"
2. **时区处理**
- 时区信息存储在context中确保中间件在处理器之前执行
- 时区验证失败时会自动回退到默认时区,不会返回错误
3. **中间件顺序**
- CORS中间件应该放在最外层以便处理预检请求
- 时区中间件可以放在CORS之后
4. **性能考虑**
- CORS预检请求会被缓存减少重复请求
- 时区验证只在请求头存在时进行,性能影响很小

242
docs/migration.md Normal file
View File

@@ -0,0 +1,242 @@
# 数据库迁移工具文档
## 概述
数据库迁移工具提供了数据库版本管理和迁移功能支持MySQL、PostgreSQL、SQLite等数据库。使用GORM作为数据库操作库可以方便地进行数据库结构的版本控制。
## 功能特性
- 支持迁移版本管理
- 支持迁移和回滚操作
- 支持从文件系统加载迁移文件
- 支持迁移状态查询
- 自动创建迁移记录表
- 事务支持,确保迁移的原子性
## 使用方法
### 1. 创建迁移器
```go
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"github.com/go-common/migration"
)
// 初始化数据库连接
dsn := "user:password@tcp(localhost:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// 创建迁移器
migrator := migration.NewMigrator(db)
// 或者指定自定义的迁移记录表名
migrator := migration.NewMigrator(db, "my_migrations")
```
### 2. 添加迁移
#### 方式一:代码方式添加迁移
```go
migrator.AddMigration(migration.Migration{
Version: "20240101000001",
Description: "create_users_table",
Up: func(db *gorm.DB) error {
return db.Exec(`
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`).Error
},
Down: func(db *gorm.DB) error {
return db.Exec("DROP TABLE IF EXISTS users").Error
},
})
```
#### 方式二:批量添加迁移
```go
migrations := []migration.Migration{
{
Version: "20240101000001",
Description: "create_users_table",
Up: func(db *gorm.DB) error {
return db.Exec("CREATE TABLE users ...").Error
},
Down: func(db *gorm.DB) error {
return db.Exec("DROP TABLE users").Error
},
},
{
Version: "20240101000002",
Description: "add_index_to_users",
Up: func(db *gorm.DB) error {
return db.Exec("CREATE INDEX idx_email ON users(email)").Error
},
Down: func(db *gorm.DB) error {
return db.Exec("DROP INDEX idx_email ON users").Error
},
},
}
migrator.AddMigrations(migrations...)
```
#### 方式三:从文件加载迁移
```go
// 文件命名格式: {version}_{description}.sql 或 {version}_{description}.up.sql
// 例如: 20240101000001_create_users_table.up.sql
// 对应的回滚文件: 20240101000001_create_users_table.down.sql
migrations, err := migration.LoadMigrationsFromFiles("./migrations", "*.up.sql")
if err != nil {
log.Fatal(err)
}
migrator.AddMigrations(migrations...)
```
### 3. 执行迁移
```go
// 执行所有未应用的迁移
err := migrator.Up()
if err != nil {
log.Fatal(err)
}
```
### 4. 回滚迁移
```go
// 回滚最后一个迁移
err := migrator.Down()
if err != nil {
log.Fatal(err)
}
```
### 5. 查看迁移状态
```go
status, err := migrator.Status()
if err != nil {
log.Fatal(err)
}
for _, s := range status {
fmt.Printf("Version: %s, Description: %s, Applied: %v\n",
s.Version, s.Description, s.Applied)
}
```
### 6. 生成迁移版本号
```go
// 生成基于时间戳的版本号
version := migration.GenerateVersion()
// 输出: 1704067200 (Unix时间戳)
```
## API 参考
### Migration 结构体
```go
type Migration struct {
Version string // 迁移版本号(必须唯一)
Description string // 迁移描述
Up func(*gorm.DB) error // 升级函数
Down func(*gorm.DB) error // 回滚函数(可选)
}
```
### Migrator 方法
#### NewMigrator(db *gorm.DB, tableName ...string) *Migrator
创建新的迁移器。
**参数:**
- `db`: GORM数据库连接
- `tableName`: 可选,迁移记录表名,默认为 "schema_migrations"
**返回:** 迁移器实例
#### AddMigration(migration Migration)
添加单个迁移。
#### AddMigrations(migrations ...Migration)
批量添加迁移。
#### Up() error
执行所有未应用的迁移。按版本号升序执行。
**返回:** 错误信息
#### Down() error
回滚最后一个已应用的迁移。
**返回:** 错误信息
#### Status() ([]MigrationStatus, error)
查看所有迁移的状态。
**返回:** 迁移状态列表和错误信息
### MigrationStatus 结构体
```go
type MigrationStatus struct {
Version string // 版本号
Description string // 描述
Applied bool // 是否已应用
}
```
### 辅助函数
#### LoadMigrationsFromFiles(dir string, pattern string) ([]Migration, error)
从文件系统加载迁移文件。
**参数:**
- `dir`: 迁移文件目录
- `pattern`: 文件匹配模式,如 "*.up.sql"
**返回:** 迁移列表和错误信息
**文件命名格式:** `{version}_{description}.up.sql`
**示例:**
- `20240101000001_create_users_table.up.sql` - 升级文件
- `20240101000001_create_users_table.down.sql` - 回滚文件(可选)
#### GenerateVersion() string
生成基于时间戳的迁移版本号。
**返回:** Unix时间戳字符串
## 注意事项
1. 迁移版本号必须唯一,建议使用时间戳格式
2. 迁移操作在事务中执行,失败会自动回滚
3. 迁移记录表会自动创建,无需手动创建
4. 如果迁移文件没有对应的down文件回滚操作会失败
5. 迁移按版本号升序执行,确保顺序正确
## 示例
完整示例请参考 `examples/migration_example.go`

370
docs/sms.md Normal file
View File

@@ -0,0 +1,370 @@
# 短信工具文档
## 概述
短信工具提供了阿里云短信发送功能使用Go标准库实现无需第三方依赖。
## 功能特性
- 支持阿里云短信服务
- 支持发送原始请求(完全由外部控制请求参数)
- 支持模板短信发送
- 支持批量发送
- 自动签名计算
- 使用配置工具统一管理配置
## 使用方法
### 1. 创建短信发送器
```go
import (
"github.com/go-common/config"
"github.com/go-common/sms"
)
// 从配置加载
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
log.Fatal(err)
}
smsConfig := cfg.GetSMS()
if smsConfig == nil {
log.Fatal("SMS config is nil")
}
// 创建短信发送器
smsClient, err := sms.NewSMS(smsConfig)
if err != nil {
log.Fatal(err)
}
```
### 2. 发送原始请求(推荐,最灵活)
```go
// 外部构建完整的请求参数
params := map[string]string{
"PhoneNumbers": "13800138000,13900139000",
"SignName": "我的签名",
"TemplateCode": "SMS_123456789",
"TemplateParam": `{"code":"123456","expire":"5"}`,
}
// 发送短信(工具只负责添加系统参数、计算签名并发送)
resp, err := smsClient.SendRaw(params)
if err != nil {
log.Fatal(err)
}
fmt.Printf("发送成功RequestID: %s\n", resp.RequestID)
```
### 3. 发送简单短信(便捷方法)
```go
// 使用配置中的模板代码发送短信
templateParam := map[string]string{
"code": "123456",
}
resp, err := smsClient.SendSimple(
[]string{"13800138000"},
templateParam,
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("发送成功RequestID: %s\n", resp.RequestID)
```
### 4. 使用指定模板发送短信(便捷方法)
```go
// 使用指定的模板代码发送短信
templateParam := map[string]string{
"code": "123456",
"expire": "5",
}
resp, err := smsClient.SendWithTemplate(
[]string{"13800138000"},
"SMS_123456789", // 模板代码
templateParam,
)
if err != nil {
log.Fatal(err)
}
```
### 5. 使用SendRequest结构发送便捷方法
```go
import "github.com/go-common/sms"
req := &sms.SendRequest{
PhoneNumbers: []string{"13800138000", "13900139000"},
TemplateCode: "SMS_123456789",
TemplateParam: map[string]string{
"code": "123456",
},
SignName: "我的签名", // 可选,如果为空使用配置中的签名
}
resp, err := smsClient.Send(req)
if err != nil {
log.Fatal(err)
}
```
### 6. 使用JSON字符串作为模板参数
```go
// TemplateParam可以是JSON字符串
req := &sms.SendRequest{
PhoneNumbers: []string{"13800138000"},
TemplateCode: "SMS_123456789",
TemplateParam: `{"code":"123456","expire":"5"}`, // 直接使用JSON字符串
}
resp, err := smsClient.Send(req)
```
## API 参考
### NewSMS(cfg *config.SMSConfig) (*SMS, error)
创建短信发送器。
**参数:**
- `cfg`: 短信配置对象
**返回:** 短信发送器实例和错误信息
### (s *SMS) SendRaw(params map[string]string) (*SendResponse, error)
发送原始请求(推荐使用,最灵活)。
**参数:**
- `params`: 请求参数map工具只负责添加必要的系统参数如签名、时间戳等并发送
**返回:** 发送响应和错误信息
**说明:** 此方法允许外部完全控制请求参数,工具只负责添加系统参数、计算签名并发送。
### (s *SMS) Send(req *SendRequest) (*SendResponse, error)
发送短信使用SendRequest结构
**参数:**
- `req`: 发送请求对象
**返回:** 发送响应和错误信息
**说明:** 如果需要完全控制请求参数请使用SendRaw方法。
### (s *SMS) SendSimple(phoneNumbers []string, templateParam map[string]string) (*SendResponse, error)
发送简单短信(便捷方法,使用配置中的模板代码)。
**参数:**
- `phoneNumbers`: 手机号列表
- `templateParam`: 模板参数
### (s *SMS) SendWithTemplate(phoneNumbers []string, templateCode string, templateParam map[string]string) (*SendResponse, error)
使用指定模板发送短信(便捷方法)。
**参数:**
- `phoneNumbers`: 手机号列表
- `templateCode`: 模板代码
- `templateParam`: 模板参数
### SendRequest 结构体
```go
type SendRequest struct {
PhoneNumbers []string // 手机号列表
TemplateCode string // 模板代码(可选,如果为空使用配置中的)
TemplateParam interface{} // 模板参数可以是map[string]string或JSON字符串
SignName string // 签名(可选,如果为空使用配置中的)
}
```
### SendResponse 结构体
```go
type SendResponse struct {
RequestID string // 请求ID
Code string // 响应码OK表示成功
Message string // 响应消息
BizID string // 业务ID
}
```
## 配置说明
短信配置通过 `config.SMSConfig` 提供:
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| AccessKeyID | string | 阿里云AccessKey ID | - |
| AccessKeySecret | string | 阿里云AccessKey Secret | - |
| Region | string | 区域cn-hangzhou | cn-hangzhou |
| SignName | string | 短信签名 | - |
| TemplateCode | string | 短信模板代码 | - |
| Endpoint | string | 服务端点(可选) | - |
| Timeout | int | 请求超时时间(秒) | 10 |
## 阿里云短信配置步骤
1. **开通阿里云短信服务**
- 登录阿里云控制台
- 开通短信服务
2. **创建AccessKey**
- 在AccessKey管理页面创建AccessKey
- 保存AccessKey ID和Secret
3. **申请短信签名**
- 在短信服务控制台申请签名
- 等待审核通过
4. **创建短信模板**
- 在短信服务控制台创建模板
- 模板格式示例:`您的验证码是${code},有效期${expire}分钟`
- 等待审核通过获取模板代码SMS_123456789
5. **配置参数**
```json
{
"sms": {
"accessKeyId": "your-access-key-id",
"accessKeySecret": "your-access-key-secret",
"region": "cn-hangzhou",
"signName": "您的签名",
"templateCode": "SMS_123456789"
}
}
```
## 模板参数示例
### 验证码模板
模板内容:`您的验证码是${code},有效期${expire}分钟`
```go
templateParam := map[string]string{
"code": "123456",
"expire": "5",
}
```
### 通知模板
模板内容:`您的订单${orderNo}已发货,物流单号:${trackingNo}`
```go
templateParam := map[string]string{
"orderNo": "ORD123456",
"trackingNo": "SF1234567890",
}
```
## 响应码说明
| Code | 说明 |
|------|------|
| OK | 发送成功 |
| InvalidSignName | 签名不存在 |
| InvalidTemplateCode | 模板不存在 |
| InvalidPhoneNumbers | 手机号格式错误 |
| Throttling | 请求被限流 |
| 其他 | 参考阿里云短信服务错误码文档 |
## 注意事项
1. **推荐使用SendRaw方法**
- `SendRaw`方法允许外部完全控制请求参数
- 可以发送任意阿里云短信API支持的请求
- 工具只负责添加系统参数、计算签名并发送
2. **模板参数格式**
- `TemplateParam`可以是`map[string]string`或JSON字符串
- 如果使用JSON字符串必须是有效的JSON格式
- 模板参数必须与模板中定义的变量匹配
3. **AccessKey安全**
- AccessKey具有账户权限请妥善保管
- 建议使用子账户AccessKey并限制权限
4. **签名和模板**
- 签名和模板需要先申请并审核通过
- 模板参数必须与模板中定义的变量匹配
5. **手机号格式**
- 支持国内手机号11位数字
- 支持国际手机号(需要加国家代码)
6. **发送频率**
- 注意阿里云的发送频率限制
- 建议实现发送频率控制
7. **错误处理**
- 所有操作都应该进行错误处理
- 建议记录详细的错误日志
- 注意区分业务错误和系统错误
8. **批量发送**
- 支持一次发送给多个手机号
- 注意批量发送的数量限制
## 完整示例
```go
package main
import (
"fmt"
"log"
"github.com/go-common/config"
"github.com/go-common/sms"
)
func main() {
// 加载配置
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
log.Fatal(err)
}
// 创建短信发送器
smsClient, err := sms.NewSMS(cfg.GetSMS())
if err != nil {
log.Fatal(err)
}
// 发送验证码短信
templateParam := map[string]string{
"code": "123456",
"expire": "5",
}
resp, err := smsClient.SendSimple(
[]string{"13800138000"},
templateParam,
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("发送成功RequestID: %s\n", resp.RequestID)
}
```
## 示例
完整示例请参考 `examples/sms_example.go`

496
docs/storage.md Normal file
View File

@@ -0,0 +1,496 @@
# 存储工具文档
## 概述
存储工具提供了文件上传和查看功能支持OSS和MinIO两种存储方式并提供HTTP处理器用于文件上传和代理查看。
## 功能特性
- 支持OSS对象存储阿里云、腾讯云、AWS、七牛云等
- 支持MinIO对象存储
- 提供统一的存储接口
- 支持文件上传HTTP处理器
- 支持文件代理查看HTTP处理器
- 支持文件大小和扩展名限制
- 自动生成唯一文件名
- 支持自定义对象键前缀
## 使用方法
### 1. 创建存储实例
```go
import (
"github.com/go-common/config"
"github.com/go-common/storage"
)
// 加载配置
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
log.Fatal(err)
}
// 创建OSS存储实例
ossStorage, err := storage.NewStorage(storage.StorageTypeOSS, cfg)
if err != nil {
log.Fatal(err)
}
// 创建MinIO存储实例
minioStorage, err := storage.NewStorage(storage.StorageTypeMinIO, cfg)
if err != nil {
log.Fatal(err)
}
```
### 2. 上传文件
```go
import (
"context"
"os"
"github.com/go-common/storage"
)
// 打开文件
file, err := os.Open("test.jpg")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 上传文件
ctx := context.Background()
objectKey := "images/test.jpg"
err = ossStorage.Upload(ctx, objectKey, file, "image/jpeg")
if err != nil {
log.Fatal(err)
}
// 获取文件URL
url, err := ossStorage.GetURL(objectKey, 0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("File URL: %s\n", url)
```
### 3. 使用HTTP处理器上传文件
```go
import (
"net/http"
"github.com/go-common/storage"
)
// 创建上传处理器
uploadHandler := storage.NewUploadHandler(storage.UploadHandlerConfig{
Storage: ossStorage,
MaxFileSize: 10 * 1024 * 1024, // 10MB
AllowedExts: []string{".jpg", ".jpeg", ".png", ".gif"},
ObjectPrefix: "images/",
})
// 注册路由
http.Handle("/upload", uploadHandler)
http.ListenAndServe(":8080", nil)
```
**上传请求示例:**
```bash
curl -X POST http://localhost:8080/upload \
-F "file=@test.jpg" \
-F "prefix=images/"
```
**响应示例:**
```json
{
"code": 0,
"message": "Upload successful",
"timestamp": 1704067200,
"data": {
"objectKey": "images/test_1704067200000000000.jpg",
"url": "https://bucket.oss-cn-hangzhou.aliyuncs.com/images/test_1704067200000000000.jpg",
"size": 102400,
"contentType": "image/jpeg",
"uploadTime": "2024-01-01T12:00:00Z"
}
}
```
### 4. 使用HTTP处理器查看文件
```go
import (
"net/http"
"github.com/go-common/storage"
)
// 创建代理查看处理器
proxyHandler := storage.NewProxyHandler(ossStorage)
// 注册路由
http.Handle("/file", proxyHandler)
http.ListenAndServe(":8080", nil)
```
**查看请求示例:**
```
GET /file?key=images/test.jpg
```
### 5. 生成对象键
```go
import "github.com/go-common/storage"
// 生成简单对象键
objectKey := storage.GenerateObjectKey("images/", "test.jpg")
// 输出: "images/test.jpg"
// 生成带日期的对象键
objectKey := storage.GenerateObjectKeyWithDate("images", "test.jpg")
// 输出: "images/2024/01/01/test.jpg"
```
### 6. 删除文件
```go
ctx := context.Background()
err := ossStorage.Delete(ctx, "images/test.jpg")
if err != nil {
log.Fatal(err)
}
```
### 7. 检查文件是否存在
```go
ctx := context.Background()
exists, err := ossStorage.Exists(ctx, "images/test.jpg")
if err != nil {
log.Fatal(err)
}
if exists {
fmt.Println("File exists")
}
```
## API 参考
### Storage 接口
```go
type Storage interface {
// Upload 上传文件
Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error
// GetURL 获取文件访问URL
GetURL(objectKey string, expires int64) (string, error)
// Delete 删除文件
Delete(ctx context.Context, objectKey string) error
// Exists 检查文件是否存在
Exists(ctx context.Context, objectKey string) (bool, error)
// GetObject 获取文件内容
GetObject(ctx context.Context, objectKey string) (io.ReadCloser, error)
}
```
### NewStorage(storageType StorageType, cfg *config.Config) (Storage, error)
创建存储实例。
**参数:**
- `storageType`: 存储类型(`storage.StorageTypeOSS``storage.StorageTypeMinIO`
- `cfg`: 配置对象
**返回:** 存储实例和错误信息
### UploadHandler
文件上传HTTP处理器。
#### NewUploadHandler(cfg UploadHandlerConfig) *UploadHandler
创建上传处理器。
**配置参数:**
- `Storage`: 存储实例
- `MaxFileSize`: 最大文件大小字节0表示不限制
- `AllowedExts`: 允许的文件扩展名,空表示不限制
- `ObjectPrefix`: 对象键前缀
#### 请求格式
- **方法**: POST
- **表单字段**:
- `file`: 文件(必需)
- `prefix`: 对象键前缀(可选,会覆盖配置中的前缀)
#### 响应格式
```json
{
"code": 0,
"message": "Upload successful",
"timestamp": 1704067200,
"data": {
"objectKey": "images/test.jpg",
"url": "https://...",
"size": 102400,
"contentType": "image/jpeg",
"uploadTime": "2024-01-01T12:00:00Z"
}
}
```
### ProxyHandler
文件代理查看HTTP处理器。
#### NewProxyHandler(storage Storage) *ProxyHandler
创建代理查看处理器。
#### 请求格式
- **方法**: GET
- **URL参数**:
- `key`: 对象键(必需)
#### 响应
直接返回文件内容设置适当的Content-Type。
### 辅助函数
#### GenerateObjectKey(prefix, filename string) string
生成对象键。
#### GenerateObjectKeyWithDate(prefix, filename string) string
生成带日期的对象键(格式: prefix/YYYY/MM/DD/filename
## 完整示例
### 示例1文件上传和查看
```go
package main
import (
"log"
"net/http"
"github.com/go-common/config"
"github.com/go-common/middleware"
"github.com/go-common/storage"
)
func main() {
// 加载配置
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
log.Fatal(err)
}
// 创建存储实例使用OSS
ossStorage, err := storage.NewStorage(storage.StorageTypeOSS, cfg)
if err != nil {
log.Fatal(err)
}
// 创建上传处理器
uploadHandler := storage.NewUploadHandler(storage.UploadHandlerConfig{
Storage: ossStorage,
MaxFileSize: 10 * 1024 * 1024, // 10MB
AllowedExts: []string{".jpg", ".jpeg", ".png", ".gif", ".pdf"},
ObjectPrefix: "uploads/",
})
// 创建代理查看处理器
proxyHandler := storage.NewProxyHandler(ossStorage)
// 创建中间件链
chain := middleware.NewChain(
middleware.CORS(cfg.GetCORS()),
middleware.Timezone,
)
// 注册路由
mux := http.NewServeMux()
mux.Handle("/upload", chain.Then(uploadHandler))
mux.Handle("/file", chain.Then(proxyHandler))
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
```
### 示例2直接使用存储接口
```go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/go-common/config"
"github.com/go-common/storage"
)
func main() {
// 加载配置
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
log.Fatal(err)
}
// 创建存储实例
s, err := storage.NewStorage(storage.StorageTypeMinIO, cfg)
if err != nil {
log.Fatal(err)
}
// 打开文件
file, err := os.Open("test.jpg")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 生成对象键
objectKey := storage.GenerateObjectKeyWithDate("images", "test.jpg")
// 上传文件
ctx := context.Background()
err = s.Upload(ctx, objectKey, file, "image/jpeg")
if err != nil {
log.Fatal(err)
}
// 获取文件URL
url, err := s.GetURL(objectKey, 0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("File uploaded: %s\n", url)
}
```
## 注意事项
1. **OSS和MinIO SDK实现**
- 当前实现提供了接口和框架但具体的OSS和MinIO SDK集成需要根据实际使用的SDK实现
- 需要在`oss.go``minio.go`中实现具体的SDK调用
2. **文件大小限制**
- 建议设置合理的文件大小限制
- 大文件上传可能需要分片上传
3. **文件扩展名验证**
- 建议限制允许的文件类型,防止上传恶意文件
- 仅验证扩展名不够安全,建议结合文件内容验证
4. **安全性**
- 上传接口应该添加身份验证
- 代理查看接口可以添加访问控制
5. **性能优化**
- 对于大文件,考虑使用分片上传
- 代理查看可以添加缓存机制
6. **错误处理**
- 所有操作都应该进行错误处理
- 建议记录详细的错误日志
## 实现OSS和MinIO SDK集成
由于不同的OSS提供商和MinIO有不同的SDK当前实现提供了框架需要根据实际情况集成
### OSS SDK集成示例阿里云OSS
```go
import (
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
func NewOSSStorage(cfg *config.OSSConfig) (*OSSStorage, error) {
client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
if err != nil {
return nil, err
}
storage := &OSSStorage{
config: cfg,
client: client,
}
return storage, nil
}
func (s *OSSStorage) Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error {
bucket, err := s.client.Bucket(s.config.Bucket)
if err != nil {
return err
}
options := []oss.Option{}
if len(contentType) > 0 && contentType[0] != "" {
options = append(options, oss.ContentType(contentType[0]))
}
return bucket.PutObject(objectKey, reader, options...)
}
```
### MinIO SDK集成示例
```go
import (
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
func NewMinIOStorage(cfg *config.MinIOConfig) (*MinIOStorage, error) {
client, err := minio.New(cfg.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""),
Secure: cfg.UseSSL,
})
if err != nil {
return nil, err
}
storage := &MinIOStorage{
config: cfg,
client: client,
}
return storage, nil
}
func (s *MinIOStorage) Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error {
ct := "application/octet-stream"
if len(contentType) > 0 && contentType[0] != "" {
ct = contentType[0]
}
_, err := s.client.PutObject(ctx, s.config.Bucket, objectKey, reader, -1, minio.PutObjectOptions{
ContentType: ct,
})
return err
}
```
## 示例
完整示例请参考 `examples/storage_example.go`