初始版本,工具基础类

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

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.cursor

167
README.md Normal file
View File

@@ -0,0 +1,167 @@
# GoCommon - Go通用工具类库
这是一个Go语言开发的通用工具类库为其他Go项目提供常用的工具方法集合。
## 功能模块
### 1. 数据库迁移工具 (migration)
提供数据库迁移功能支持MySQL、PostgreSQL、SQLite等数据库。
### 2. 日期转换工具 (datetime)
提供日期时间转换功能,支持时区设定和多种格式转换。
### 3. HTTP Restful工具 (http)
提供HTTP请求/响应处理工具包含标准化的响应结构、分页支持和HTTP状态码与业务状态码的分离。
### 4. 中间件工具 (middleware)
提供常用的HTTP中间件包括CORS处理和时区管理。
### 5. 配置工具 (config)
提供从外部文件加载配置的功能支持数据库、OSS、Redis、CORS、MinIO等配置。
### 6. 存储工具 (storage)
提供文件上传和查看功能支持OSS和MinIO两种存储方式并提供HTTP处理器。
### 7. 邮件工具 (email)
提供SMTP邮件发送功能支持纯文本和HTML邮件使用Go标准库实现。
### 8. 短信工具 (sms)
提供阿里云短信发送功能支持模板短信和批量发送使用Go标准库实现。
## 安装
```bash
go get github.com/go-common
```
## 使用示例
详细的使用说明请参考各模块的文档:
- [数据库迁移工具文档](./docs/migration.md)
- [日期转换工具文档](./docs/datetime.md)
- [HTTP Restful工具文档](./docs/http.md)
- [中间件工具文档](./docs/middleware.md)
- [配置工具文档](./docs/config.md)
- [存储工具文档](./docs/storage.md)
- [邮件工具文档](./docs/email.md)
- [短信工具文档](./docs/sms.md)
### 快速示例
#### 数据库迁移
```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()
```
#### 文件上传和查看
```go
import "github.com/go-common/storage"
// 创建存储实例
storage, _ := storage.NewStorage(storage.StorageTypeOSS, cfg)
// 创建上传处理器
uploadHandler := storage.NewUploadHandler(storage.UploadHandlerConfig{
Storage: storage,
MaxFileSize: 10 * 1024 * 1024,
AllowedExts: []string{".jpg", ".png"},
})
// 创建代理查看处理器
proxyHandler := storage.NewProxyHandler(storage)
```
#### 邮件发送
```go
import "github.com/go-common/email"
// 从配置创建邮件发送器
mailer, _ := email.NewEmail(cfg.GetEmail())
// 发送邮件
mailer.SendSimple(
[]string{"recipient@example.com"},
"主题",
"正文",
)
```
#### 短信发送
```go
import "github.com/go-common/sms"
// 从配置创建短信发送器
smsClient, _ := sms.NewSMS(cfg.GetSMS())
// 发送短信
smsClient.SendSimple(
[]string{"13800138000"},
map[string]string{"code": "123456"},
)
```
更多示例请查看 [examples](./examples/) 目录。
## 版本
v1.0.0

466
config/config.go Normal file
View File

@@ -0,0 +1,466 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/go-common/middleware"
)
// Config 应用配置
type Config struct {
Database *DatabaseConfig `json:"database"`
OSS *OSSConfig `json:"oss"`
Redis *RedisConfig `json:"redis"`
CORS *CORSConfig `json:"cors"`
MinIO *MinIOConfig `json:"minio"`
Email *EmailConfig `json:"email"`
SMS *SMSConfig `json:"sms"`
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
// Type 数据库类型: mysql, postgres, sqlite
Type string `json:"type"`
// Host 数据库主机
Host string `json:"host"`
// Port 数据库端口
Port int `json:"port"`
// User 数据库用户名
User string `json:"user"`
// Password 数据库密码
Password string `json:"password"`
// Database 数据库名称
Database string `json:"database"`
// Charset 字符集MySQL使用
Charset string `json:"charset"`
// MaxOpenConns 最大打开连接数
MaxOpenConns int `json:"maxOpenConns"`
// MaxIdleConns 最大空闲连接数
MaxIdleConns int `json:"maxIdleConns"`
// ConnMaxLifetime 连接最大生存时间(秒)
ConnMaxLifetime int `json:"connMaxLifetime"`
// DSN 数据库连接字符串(如果设置了,会优先使用)
DSN string `json:"dsn"`
}
// OSSConfig OSS对象存储配置
type OSSConfig struct {
// Provider 提供商: aliyun, tencent, aws, qiniu
Provider string `json:"provider"`
// Endpoint 端点地址
Endpoint string `json:"endpoint"`
// AccessKeyID 访问密钥ID
AccessKeyID string `json:"accessKeyId"`
// AccessKeySecret 访问密钥
AccessKeySecret string `json:"accessKeySecret"`
// Bucket 存储桶名称
Bucket string `json:"bucket"`
// Region 区域
Region string `json:"region"`
// UseSSL 是否使用SSL
UseSSL bool `json:"useSSL"`
// Domain 自定义域名CDN域名
Domain string `json:"domain"`
}
// RedisConfig Redis配置
type RedisConfig struct {
// Host Redis主机
Host string `json:"host"`
// Port Redis端口
Port int `json:"port"`
// Password Redis密码
Password string `json:"password"`
// Database Redis数据库编号
Database int `json:"database"`
// MaxRetries 最大重试次数
MaxRetries int `json:"maxRetries"`
// PoolSize 连接池大小
PoolSize int `json:"poolSize"`
// MinIdleConns 最小空闲连接数
MinIdleConns int `json:"minIdleConns"`
// DialTimeout 连接超时时间(秒)
DialTimeout int `json:"dialTimeout"`
// ReadTimeout 读取超时时间(秒)
ReadTimeout int `json:"readTimeout"`
// WriteTimeout 写入超时时间(秒)
WriteTimeout int `json:"writeTimeout"`
// Addr Redis地址如果设置了会优先使用格式: host:port
Addr string `json:"addr"`
}
// CORSConfig CORS配置与middleware.CORSConfig兼容
type CORSConfig struct {
// AllowedOrigins 允许的源
AllowedOrigins []string `json:"allowedOrigins"`
// AllowedMethods 允许的HTTP方法
AllowedMethods []string `json:"allowedMethods"`
// AllowedHeaders 允许的请求头
AllowedHeaders []string `json:"allowedHeaders"`
// ExposedHeaders 暴露给客户端的响应头
ExposedHeaders []string `json:"exposedHeaders"`
// AllowCredentials 是否允许发送凭证
AllowCredentials bool `json:"allowCredentials"`
// MaxAge 预检请求的缓存时间(秒)
MaxAge int `json:"maxAge"`
}
// MinIOConfig MinIO配置
type MinIOConfig struct {
// Endpoint MinIO端点地址
Endpoint string `json:"endpoint"`
// AccessKeyID 访问密钥ID
AccessKeyID string `json:"accessKeyId"`
// SecretAccessKey 密钥
SecretAccessKey string `json:"secretAccessKey"`
// UseSSL 是否使用SSL
UseSSL bool `json:"useSSL"`
// Bucket 存储桶名称
Bucket string `json:"bucket"`
// Region 区域
Region string `json:"region"`
// Domain 自定义域名
Domain string `json:"domain"`
}
// EmailConfig 邮件配置
type EmailConfig struct {
// Host SMTP服务器地址
Host string `json:"host"`
// Port SMTP服务器端口
Port int `json:"port"`
// Username 发件人邮箱
Username string `json:"username"`
// Password 邮箱密码或授权码
Password string `json:"password"`
// From 发件人邮箱地址如果为空使用Username
From string `json:"from"`
// FromName 发件人名称
FromName string `json:"fromName"`
// UseTLS 是否使用TLS
UseTLS bool `json:"useTLS"`
// UseSSL 是否使用SSL
UseSSL bool `json:"useSSL"`
// Timeout 连接超时时间(秒)
Timeout int `json:"timeout"`
}
// SMSConfig 短信配置(阿里云短信)
type SMSConfig struct {
// AccessKeyID 阿里云AccessKey ID
AccessKeyID string `json:"accessKeyId"`
// AccessKeySecret 阿里云AccessKey Secret
AccessKeySecret string `json:"accessKeySecret"`
// Region 区域cn-hangzhou
Region string `json:"region"`
// SignName 短信签名
SignName string `json:"signName"`
// TemplateCode 短信模板代码
TemplateCode string `json:"templateCode"`
// Endpoint 服务端点(可选,默认使用区域端点)
Endpoint string `json:"endpoint"`
// Timeout 请求超时时间(秒)
Timeout int `json:"timeout"`
}
// LoadFromFile 从文件加载配置
// filePath: 配置文件路径(支持绝对路径和相对路径)
func LoadFromFile(filePath string) (*Config, error) {
// 转换为绝对路径
absPath, err := filepath.Abs(filePath)
if err != nil {
return nil, fmt.Errorf("failed to get absolute path: %w", err)
}
// 读取文件
data, err := os.ReadFile(absPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// 解析JSON
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
// 设置默认值
config.setDefaults()
return &config, nil
}
// LoadFromBytes 从字节数组加载配置
func LoadFromBytes(data []byte) (*Config, error) {
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
// 设置默认值
config.setDefaults()
return &config, nil
}
// setDefaults 设置默认值
func (c *Config) setDefaults() {
// 数据库默认值
if c.Database != nil {
if c.Database.Charset == "" {
c.Database.Charset = "utf8mb4"
}
if c.Database.MaxOpenConns == 0 {
c.Database.MaxOpenConns = 100
}
if c.Database.MaxIdleConns == 0 {
c.Database.MaxIdleConns = 10
}
if c.Database.ConnMaxLifetime == 0 {
c.Database.ConnMaxLifetime = 3600
}
}
// Redis默认值
if c.Redis != nil {
if c.Redis.Port == 0 {
c.Redis.Port = 6379
}
if c.Redis.Database == 0 {
c.Redis.Database = 0
}
if c.Redis.MaxRetries == 0 {
c.Redis.MaxRetries = 3
}
if c.Redis.PoolSize == 0 {
c.Redis.PoolSize = 10
}
if c.Redis.MinIdleConns == 0 {
c.Redis.MinIdleConns = 5
}
if c.Redis.DialTimeout == 0 {
c.Redis.DialTimeout = 5
}
if c.Redis.ReadTimeout == 0 {
c.Redis.ReadTimeout = 3
}
if c.Redis.WriteTimeout == 0 {
c.Redis.WriteTimeout = 3
}
}
// CORS默认值
if c.CORS != nil {
if len(c.CORS.AllowedOrigins) == 0 {
c.CORS.AllowedOrigins = []string{"*"}
}
if len(c.CORS.AllowedMethods) == 0 {
c.CORS.AllowedMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"}
}
if len(c.CORS.AllowedHeaders) == 0 {
c.CORS.AllowedHeaders = []string{"Content-Type", "Authorization", "X-Requested-With", "X-Timezone"}
}
if c.CORS.MaxAge == 0 {
c.CORS.MaxAge = 86400
}
}
// 邮件默认值
if c.Email != nil {
if c.Email.Port == 0 {
c.Email.Port = 587 // 默认使用587端口TLS
}
if c.Email.From == "" {
c.Email.From = c.Email.Username
}
if c.Email.Timeout == 0 {
c.Email.Timeout = 30
}
}
// 短信默认值
if c.SMS != nil {
if c.SMS.Region == "" {
c.SMS.Region = "cn-hangzhou"
}
if c.SMS.Timeout == 0 {
c.SMS.Timeout = 10
}
}
}
// GetDatabase 获取数据库配置
func (c *Config) GetDatabase() *DatabaseConfig {
return c.Database
}
// GetOSS 获取OSS配置
func (c *Config) GetOSS() *OSSConfig {
return c.OSS
}
// GetRedis 获取Redis配置
func (c *Config) GetRedis() *RedisConfig {
return c.Redis
}
// GetCORS 获取CORS配置并转换为middleware.CORSConfig
func (c *Config) GetCORS() *middleware.CORSConfig {
if c.CORS == nil {
return middleware.DefaultCORSConfig()
}
return &middleware.CORSConfig{
AllowedOrigins: c.CORS.AllowedOrigins,
AllowedMethods: c.CORS.AllowedMethods,
AllowedHeaders: c.CORS.AllowedHeaders,
ExposedHeaders: c.CORS.ExposedHeaders,
AllowCredentials: c.CORS.AllowCredentials,
MaxAge: c.CORS.MaxAge,
}
}
// GetMinIO 获取MinIO配置
func (c *Config) GetMinIO() *MinIOConfig {
return c.MinIO
}
// GetEmail 获取邮件配置
func (c *Config) GetEmail() *EmailConfig {
return c.Email
}
// GetSMS 获取短信配置
func (c *Config) GetSMS() *SMSConfig {
return c.SMS
}
// GetDatabaseDSN 获取数据库连接字符串
func (c *Config) GetDatabaseDSN() (string, error) {
if c.Database == nil {
return "", fmt.Errorf("database config is nil")
}
// 如果已经设置了DSN直接返回
if c.Database.DSN != "" {
return c.Database.DSN, nil
}
// 根据数据库类型生成DSN
switch c.Database.Type {
case "mysql":
return c.buildMySQLDSN(), nil
case "postgres":
return c.buildPostgresDSN(), nil
case "sqlite":
return c.Database.Database, nil
default:
return "", fmt.Errorf("unsupported database type: %s", c.Database.Type)
}
}
// buildMySQLDSN 构建MySQL连接字符串
// 注意数据库时间统一使用UTC时间不设置时区
func (c *Config) buildMySQLDSN() string {
db := c.Database
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", db.User, db.Password, db.Host, db.Port, db.Database)
params := []string{}
if db.Charset != "" {
params = append(params, "charset="+db.Charset)
}
params = append(params, "parseTime=True")
params = append(params, "loc=UTC") // 统一使用UTC时区
if len(params) > 0 {
dsn += "?" + params[0]
for i := 1; i < len(params); i++ {
dsn += "&" + params[i]
}
}
return dsn
}
// buildPostgresDSN 构建PostgreSQL连接字符串
// 注意数据库时间统一使用UTC时间不设置时区
func (c *Config) buildPostgresDSN() string {
db := c.Database
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s",
db.Host, db.Port, db.User, db.Password, db.Database)
// 统一使用UTC时区
dsn += " timezone=UTC"
dsn += " sslmode=disable"
return dsn
}
// GetRedisAddr 获取Redis地址
func (c *Config) GetRedisAddr() string {
if c.Redis == nil {
return ""
}
// 如果已经设置了Addr直接返回
if c.Redis.Addr != "" {
return c.Redis.Addr
}
// 构建地址
return fmt.Sprintf("%s:%d", c.Redis.Host, c.Redis.Port)
}

74
config/example.json Normal file
View File

@@ -0,0 +1,74 @@
{
"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
}
}

358
datetime/datetime.go Normal file
View File

@@ -0,0 +1,358 @@
package datetime
import (
"fmt"
"time"
)
// TimeZone 时区常量
const (
UTC = "UTC"
AsiaShanghai = "Asia/Shanghai"
AmericaNewYork = "America/New_York"
EuropeLondon = "Europe/London"
AsiaTokyo = "Asia/Tokyo"
)
// DefaultTimeZone 默认时区
var DefaultTimeZone = UTC
// SetDefaultTimeZone 设置默认时区
func SetDefaultTimeZone(timezone string) error {
_, err := time.LoadLocation(timezone)
if err != nil {
return fmt.Errorf("invalid timezone: %w", err)
}
DefaultTimeZone = timezone
return nil
}
// GetLocation 获取时区Location对象
func GetLocation(timezone string) (*time.Location, error) {
if timezone == "" {
timezone = DefaultTimeZone
}
return time.LoadLocation(timezone)
}
// Now 获取当前时间(使用指定时区)
func Now(timezone ...string) time.Time {
tz := DefaultTimeZone
if len(timezone) > 0 && timezone[0] != "" {
tz = timezone[0]
}
loc, err := GetLocation(tz)
if err != nil {
// 如果时区无效使用UTC
loc, _ = time.LoadLocation(UTC)
}
return time.Now().In(loc)
}
// Parse 解析时间字符串
// layout: 时间格式,如 "2006-01-02 15:04:05"
// value: 时间字符串
// timezone: 时区,如果为空则使用默认时区
func Parse(layout, value string, timezone ...string) (time.Time, error) {
tz := DefaultTimeZone
if len(timezone) > 0 && timezone[0] != "" {
tz = timezone[0]
}
loc, err := GetLocation(tz)
if err != nil {
return time.Time{}, fmt.Errorf("invalid timezone: %w", err)
}
t, err := time.ParseInLocation(layout, value, loc)
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse time: %w", err)
}
return t, nil
}
// Format 格式化时间
// t: 时间对象
// layout: 时间格式,如 "2006-01-02 15:04:05"
// timezone: 时区,如果为空则使用时间对象本身的时区
func Format(t time.Time, layout string, timezone ...string) string {
if len(timezone) > 0 && timezone[0] != "" {
loc, err := GetLocation(timezone[0])
if err == nil {
t = t.In(loc)
}
}
return t.Format(layout)
}
// ToTimezone 将时间转换到指定时区
func ToTimezone(t time.Time, timezone string) (time.Time, error) {
loc, err := GetLocation(timezone)
if err != nil {
return time.Time{}, fmt.Errorf("invalid timezone: %w", err)
}
return t.In(loc), nil
}
// CommonLayouts 常用时间格式
var CommonLayouts = struct {
DateTime string
DateTimeSec string
Date string
Time string
TimeSec string
ISO8601 string
RFC3339 string
RFC3339Nano string
Unix string
}{
DateTime: "2006-01-02 15:04",
DateTimeSec: "2006-01-02 15:04:05",
Date: "2006-01-02",
Time: "15:04",
TimeSec: "15:04:05",
ISO8601: "2006-01-02T15:04:05Z07:00",
RFC3339: time.RFC3339,
RFC3339Nano: time.RFC3339Nano,
Unix: "unix",
}
// FormatDateTime 格式化日期时间2006-01-02 15:04:05
func FormatDateTime(t time.Time, timezone ...string) string {
return Format(t, CommonLayouts.DateTimeSec, timezone...)
}
// FormatDate 格式化日期2006-01-02
func FormatDate(t time.Time, timezone ...string) string {
return Format(t, CommonLayouts.Date, timezone...)
}
// FormatTime 格式化时间15:04:05
func FormatTime(t time.Time, timezone ...string) string {
return Format(t, CommonLayouts.TimeSec, timezone...)
}
// ParseDateTime 解析日期时间字符串2006-01-02 15:04:05
func ParseDateTime(value string, timezone ...string) (time.Time, error) {
return Parse(CommonLayouts.DateTimeSec, value, timezone...)
}
// ParseDate 解析日期字符串2006-01-02
func ParseDate(value string, timezone ...string) (time.Time, error) {
return Parse(CommonLayouts.Date, value, timezone...)
}
// ToUnix 转换为Unix时间戳
func ToUnix(t time.Time) int64 {
return t.Unix()
}
// FromUnix 从Unix时间戳创建时间
func FromUnix(sec int64, timezone ...string) time.Time {
t := time.Unix(sec, 0)
if len(timezone) > 0 && timezone[0] != "" {
loc, err := GetLocation(timezone[0])
if err == nil {
t = t.In(loc)
}
}
return t
}
// ToUnixMilli 转换为Unix毫秒时间戳
func ToUnixMilli(t time.Time) int64 {
return t.UnixMilli()
}
// FromUnixMilli 从Unix毫秒时间戳创建时间
func FromUnixMilli(msec int64, timezone ...string) time.Time {
t := time.UnixMilli(msec)
if len(timezone) > 0 && timezone[0] != "" {
loc, err := GetLocation(timezone[0])
if err == nil {
t = t.In(loc)
}
}
return t
}
// AddDays 添加天数
func AddDays(t time.Time, days int) time.Time {
return t.AddDate(0, 0, days)
}
// AddMonths 添加月数
func AddMonths(t time.Time, months int) time.Time {
return t.AddDate(0, months, 0)
}
// AddYears 添加年数
func AddYears(t time.Time, years int) time.Time {
return t.AddDate(years, 0, 0)
}
// StartOfDay 获取一天的开始时间00:00:00
func StartOfDay(t time.Time, timezone ...string) time.Time {
tz := DefaultTimeZone
if len(timezone) > 0 && timezone[0] != "" {
tz = timezone[0]
}
loc, err := GetLocation(tz)
if err != nil {
loc, _ = time.LoadLocation(UTC)
}
t = t.In(loc)
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, loc)
}
// EndOfDay 获取一天的结束时间23:59:59.999999999
func EndOfDay(t time.Time, timezone ...string) time.Time {
tz := DefaultTimeZone
if len(timezone) > 0 && timezone[0] != "" {
tz = timezone[0]
}
loc, err := GetLocation(tz)
if err != nil {
loc, _ = time.LoadLocation(UTC)
}
t = t.In(loc)
year, month, day := t.Date()
return time.Date(year, month, day, 23, 59, 59, 999999999, loc)
}
// StartOfMonth 获取月份的开始时间
func StartOfMonth(t time.Time, timezone ...string) time.Time {
tz := DefaultTimeZone
if len(timezone) > 0 && timezone[0] != "" {
tz = timezone[0]
}
loc, err := GetLocation(tz)
if err != nil {
loc, _ = time.LoadLocation(UTC)
}
t = t.In(loc)
year, month, _ := t.Date()
return time.Date(year, month, 1, 0, 0, 0, 0, loc)
}
// EndOfMonth 获取月份的结束时间
func EndOfMonth(t time.Time, timezone ...string) time.Time {
tz := DefaultTimeZone
if len(timezone) > 0 && timezone[0] != "" {
tz = timezone[0]
}
loc, err := GetLocation(tz)
if err != nil {
loc, _ = time.LoadLocation(UTC)
}
t = t.In(loc)
year, month, _ := t.Date()
// 获取下个月的第一天然后减去1纳秒
nextMonth := time.Date(year, month+1, 1, 0, 0, 0, 0, loc)
return nextMonth.Add(-time.Nanosecond)
}
// StartOfYear 获取年份的开始时间
func StartOfYear(t time.Time, timezone ...string) time.Time {
tz := DefaultTimeZone
if len(timezone) > 0 && timezone[0] != "" {
tz = timezone[0]
}
loc, err := GetLocation(tz)
if err != nil {
loc, _ = time.LoadLocation(UTC)
}
t = t.In(loc)
year, _, _ := t.Date()
return time.Date(year, 1, 1, 0, 0, 0, 0, loc)
}
// EndOfYear 获取年份的结束时间
func EndOfYear(t time.Time, timezone ...string) time.Time {
tz := DefaultTimeZone
if len(timezone) > 0 && timezone[0] != "" {
tz = timezone[0]
}
loc, err := GetLocation(tz)
if err != nil {
loc, _ = time.LoadLocation(UTC)
}
t = t.In(loc)
year, _, _ := t.Date()
return time.Date(year, 12, 31, 23, 59, 59, 999999999, loc)
}
// DiffDays 计算两个时间之间的天数差
func DiffDays(t1, t2 time.Time) int {
return int(t2.Sub(t1).Hours() / 24)
}
// DiffHours 计算两个时间之间的小时差
func DiffHours(t1, t2 time.Time) int64 {
return int64(t2.Sub(t1).Hours())
}
// DiffMinutes 计算两个时间之间的分钟差
func DiffMinutes(t1, t2 time.Time) int64 {
return int64(t2.Sub(t1).Minutes())
}
// DiffSeconds 计算两个时间之间的秒数差
func DiffSeconds(t1, t2 time.Time) int64 {
return int64(t2.Sub(t1).Seconds())
}
// ToUTC 将时间转换为UTC时间
// t: 时间对象(可以是任意时区)
// 返回: UTC时间
func ToUTC(t time.Time) time.Time {
return t.UTC()
}
// ToUTCFromTimezone 从指定时区转换为UTC时间
// t: 时间对象(会被视为指定时区的时间)
// timezone: 源时区
// 返回: UTC时间
// 注意此方法假设时间对象t表示的是指定时区的本地时间然后转换为UTC
func ToUTCFromTimezone(t time.Time, timezone string) (time.Time, error) {
loc, err := GetLocation(timezone)
if err != nil {
return time.Time{}, fmt.Errorf("invalid timezone: %w", err)
}
// 将时间视为指定时区的本地时间然后转换为UTC
// 首先将时间转换到指定时区然后转换为UTC
localTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc)
return localTime.UTC(), nil
}
// ParseToUTC 解析时间字符串并转换为UTC时间
// layout: 时间格式,如 "2006-01-02 15:04:05"
// value: 时间字符串
// timezone: 源时区,如果为空则使用默认时区
// 返回: UTC时间
func ParseToUTC(layout, value string, timezone ...string) (time.Time, error) {
t, err := Parse(layout, value, timezone...)
if err != nil {
return time.Time{}, err
}
return t.UTC(), nil
}
// ParseDateTimeToUTC 解析日期时间字符串并转换为UTC时间便捷方法
// value: 时间字符串(格式: 2006-01-02 15:04:05
// timezone: 源时区,如果为空则使用默认时区
// 返回: UTC时间
func ParseDateTimeToUTC(value string, timezone ...string) (time.Time, error) {
return ParseToUTC(CommonLayouts.DateTimeSec, value, timezone...)
}
// ParseDateToUTC 解析日期字符串并转换为UTC时间便捷方法
// value: 日期字符串(格式: 2006-01-02
// timezone: 源时区,如果为空则使用默认时区
// 返回: UTC时间当天的00:00:00 UTC时间
func ParseDateToUTC(value string, timezone ...string) (time.Time, error) {
return ParseToUTC(CommonLayouts.Date, value, timezone...)
}

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`

306
email/email.go Normal file
View File

@@ -0,0 +1,306 @@
package email
import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/smtp"
"time"
"github.com/go-common/config"
)
// Email 邮件发送器
type Email struct {
config *config.EmailConfig
}
// NewEmail 创建邮件发送器
func NewEmail(cfg *config.EmailConfig) (*Email, error) {
if cfg == nil {
return nil, fmt.Errorf("email config is nil")
}
if cfg.Host == "" {
return nil, fmt.Errorf("email host is required")
}
if cfg.Username == "" {
return nil, fmt.Errorf("email username is required")
}
if cfg.Password == "" {
return nil, fmt.Errorf("email password is required")
}
// 设置默认值
if cfg.Port == 0 {
cfg.Port = 587
}
if cfg.From == "" {
cfg.From = cfg.Username
}
if cfg.Timeout == 0 {
cfg.Timeout = 30
}
return &Email{
config: cfg,
}, nil
}
// Message 邮件消息
type Message struct {
// To 收件人列表
To []string
// Cc 抄送列表(可选)
Cc []string
// Bcc 密送列表(可选)
Bcc []string
// Subject 主题
Subject string
// Body 正文(纯文本)
Body string
// HTMLBody HTML正文可选如果设置了会优先使用
HTMLBody string
// Attachments 附件列表(可选)
Attachments []Attachment
}
// Attachment 附件
type Attachment struct {
// Filename 文件名
Filename string
// Content 文件内容
Content []byte
// ContentType 文件类型application/pdf
ContentType string
}
// SendRaw 发送原始邮件内容
// recipients: 收件人列表To、Cc、Bcc的合并列表
// body: 完整的邮件内容MIME格式由外部构建
func (e *Email) SendRaw(recipients []string, body []byte) error {
if len(recipients) == 0 {
return fmt.Errorf("recipients are required")
}
if len(body) == 0 {
return fmt.Errorf("email body is required")
}
// 连接SMTP服务器
addr := fmt.Sprintf("%s:%d", e.config.Host, e.config.Port)
auth := smtp.PlainAuth("", e.config.Username, e.config.Password, e.config.Host)
// 创建连接
conn, err := net.DialTimeout("tcp", addr, time.Duration(e.config.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)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %w", err)
}
defer client.Close()
// TLS/SSL处理
if e.config.UseSSL {
// SSL模式端口通常是465
tlsConfig := &tls.Config{
ServerName: e.config.Host,
}
if err := client.StartTLS(tlsConfig); err != nil {
return fmt.Errorf("failed to start TLS: %w", err)
}
} else if e.config.UseTLS {
// TLS模式STARTTLS端口通常是587
tlsConfig := &tls.Config{
ServerName: e.config.Host,
}
if err := client.StartTLS(tlsConfig); err != nil {
return fmt.Errorf("failed to start TLS: %w", err)
}
}
// 认证
if err := client.Auth(auth); err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
// 设置发件人
if err := client.Mail(e.config.From); err != nil {
return fmt.Errorf("failed to set sender: %w", err)
}
// 设置收件人
for _, to := range recipients {
if err := client.Rcpt(to); err != nil {
return fmt.Errorf("failed to set recipient %s: %w", to, err)
}
}
// 发送邮件内容
writer, err := client.Data()
if err != nil {
return fmt.Errorf("failed to get data writer: %w", err)
}
_, err = writer.Write(body)
if err != nil {
writer.Close()
return fmt.Errorf("failed to write email body: %w", err)
}
err = writer.Close()
if err != nil {
return fmt.Errorf("failed to close writer: %w", err)
}
// 退出
if err := client.Quit(); err != nil {
return fmt.Errorf("failed to quit: %w", err)
}
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) {
var buf bytes.Buffer
// 邮件头
from := e.config.From
if e.config.FromName != "" {
from = fmt.Sprintf("%s <%s>", e.config.FromName, e.config.From)
}
buf.WriteString(fmt.Sprintf("From: %s\r\n", from))
// 收件人
buf.WriteString(fmt.Sprintf("To: %s\r\n", joinEmails(msg.To)))
// 抄送
if len(msg.Cc) > 0 {
buf.WriteString(fmt.Sprintf("Cc: %s\r\n", joinEmails(msg.Cc)))
}
// 主题
buf.WriteString(fmt.Sprintf("Subject: %s\r\n", msg.Subject))
// 内容类型
if msg.HTMLBody != "" {
// 多部分邮件HTML + 纯文本)
boundary := "----=_Part_" + fmt.Sprint(time.Now().UnixNano())
buf.WriteString("MIME-Version: 1.0\r\n")
buf.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=\"%s\"\r\n", boundary))
buf.WriteString("\r\n")
// 纯文本部分
buf.WriteString("--" + boundary + "\r\n")
buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
buf.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
buf.WriteString("\r\n")
buf.WriteString(msg.Body)
buf.WriteString("\r\n")
// HTML部分
buf.WriteString("--" + boundary + "\r\n")
buf.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
buf.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
buf.WriteString("\r\n")
buf.WriteString(msg.HTMLBody)
buf.WriteString("\r\n")
buf.WriteString("--" + boundary + "--\r\n")
} else {
// 纯文本邮件
buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
buf.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
buf.WriteString("\r\n")
buf.WriteString(msg.Body)
buf.WriteString("\r\n")
}
return buf.Bytes(), nil
}
// joinEmails 连接邮箱地址
func joinEmails(emails []string) string {
if len(emails) == 0 {
return ""
}
result := emails[0]
for i := 1; i < len(emails); i++ {
result += ", " + emails[i]
}
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,
})
}

124
examples/config_example.go Normal file
View File

@@ -0,0 +1,124 @@
package main
import (
"fmt"
"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/example.json")
if err != nil {
log.Fatal("Failed to load config:", err)
}
// 1. 使用数据库配置
fmt.Println("=== Database Config ===")
dbConfig := cfg.GetDatabase()
if dbConfig != nil {
fmt.Printf("Type: %s\n", dbConfig.Type)
fmt.Printf("Host: %s:%d\n", dbConfig.Host, dbConfig.Port)
fmt.Printf("Database: %s\n", dbConfig.Database)
fmt.Printf("User: %s\n", dbConfig.User)
}
// 获取数据库连接字符串
dsn, err := cfg.GetDatabaseDSN()
if err != nil {
log.Printf("Failed to get DSN: %v", err)
} else {
fmt.Printf("DSN: %s\n", dsn)
// 实际使用时可以这样连接数据库
// db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// if err != nil {
// log.Fatal(err)
// }
}
// 2. 使用OSS配置
fmt.Println("\n=== OSS Config ===")
ossConfig := cfg.GetOSS()
if ossConfig != nil {
fmt.Printf("Provider: %s\n", ossConfig.Provider)
fmt.Printf("Endpoint: %s\n", ossConfig.Endpoint)
fmt.Printf("Bucket: %s\n", ossConfig.Bucket)
fmt.Printf("Region: %s\n", ossConfig.Region)
}
// 3. 使用Redis配置
fmt.Println("\n=== Redis Config ===")
redisConfig := cfg.GetRedis()
if redisConfig != nil {
fmt.Printf("Host: %s\n", redisConfig.Host)
fmt.Printf("Port: %d\n", redisConfig.Port)
fmt.Printf("Database: %d\n", redisConfig.Database)
}
// 获取Redis地址
redisAddr := cfg.GetRedisAddr()
fmt.Printf("Redis Address: %s\n", redisAddr)
// 4. 使用CORS配置
fmt.Println("\n=== CORS Config ===")
corsConfig := cfg.GetCORS()
if corsConfig != nil {
fmt.Printf("Allowed Origins: %v\n", corsConfig.AllowedOrigins)
fmt.Printf("Allowed Methods: %v\n", corsConfig.AllowedMethods)
fmt.Printf("Max Age: %d\n", corsConfig.MaxAge)
}
// 使用CORS配置创建中间件
chain := middleware.NewChain(
middleware.CORS(corsConfig),
)
fmt.Printf("CORS middleware created: %v\n", chain != nil)
// 5. 使用MinIO配置
fmt.Println("\n=== MinIO Config ===")
minioConfig := cfg.GetMinIO()
if minioConfig != nil {
fmt.Printf("Endpoint: %s\n", minioConfig.Endpoint)
fmt.Printf("Bucket: %s\n", minioConfig.Bucket)
fmt.Printf("UseSSL: %v\n", minioConfig.UseSSL)
}
// 6. 使用邮件配置
fmt.Println("\n=== Email Config ===")
emailConfig := cfg.GetEmail()
if emailConfig != nil {
fmt.Printf("Host: %s:%d\n", emailConfig.Host, emailConfig.Port)
fmt.Printf("From: %s <%s>\n", emailConfig.FromName, emailConfig.From)
fmt.Printf("UseTLS: %v\n", emailConfig.UseTLS)
}
// 7. 使用短信配置
fmt.Println("\n=== SMS Config ===")
smsConfig := cfg.GetSMS()
if smsConfig != nil {
fmt.Printf("Region: %s\n", smsConfig.Region)
fmt.Printf("Sign Name: %s\n", smsConfig.SignName)
fmt.Printf("Template Code: %s\n", smsConfig.TemplateCode)
}
// 示例:实际使用配置连接数据库
fmt.Println("\n=== Example: Connect to Database ===")
if dbConfig != nil && dbConfig.Type == "mysql" {
// 注意:这里只是示例,实际使用时需要确保数据库服务正在运行
// db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// if err != nil {
// log.Printf("Failed to connect to database: %v", err)
// } else {
// fmt.Println("Database connected successfully")
// sqlDB, _ := db.DB()
// sqlDB.SetMaxOpenConns(dbConfig.MaxOpenConns)
// sqlDB.SetMaxIdleConns(dbConfig.MaxIdleConns)
// sqlDB.SetConnMaxLifetime(time.Duration(dbConfig.ConnMaxLifetime) * time.Second)
// }
fmt.Println("Database connection example (commented out)")
}
}

View File

@@ -0,0 +1,56 @@
package main
import (
"fmt"
"log"
"time"
"github.com/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)
}

View File

@@ -0,0 +1,74 @@
package main
import (
"fmt"
"log"
"time"
"github.com/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))
}

121
examples/email_example.go Normal file
View File

@@ -0,0 +1,121 @@
package main
import (
"fmt"
"log"
"github.com/go-common/config"
"github.com/go-common/email"
)
func main() {
// 加载配置
cfg, err := config.LoadFromFile("./config/example.json")
if err != nil {
log.Fatal("Failed to load config:", err)
}
// 创建邮件发送器
emailConfig := cfg.GetEmail()
if emailConfig == nil {
log.Fatal("Email config is nil")
}
mailer, err := email.NewEmail(emailConfig)
if err != nil {
log.Fatal("Failed to create email client:", err)
}
// 示例1发送原始邮件内容推荐最灵活
fmt.Println("=== Example 1: Send Raw Email Content ===")
// 外部构建完整的邮件内容MIME格式
emailBody := []byte(`From: ` + emailConfig.From + `
To: recipient@example.com
Subject: 原始邮件测试
Content-Type: text/html; charset=UTF-8
<html>
<body>
<h1>这是原始邮件内容</h1>
<p>由外部完全控制邮件格式和内容</p>
</body>
</html>
`)
err = mailer.SendRaw(
[]string{"recipient@example.com"},
emailBody,
)
if err != nil {
log.Printf("Failed to send raw email: %v", err)
} else {
fmt.Println("Raw email sent successfully")
}
// 示例2发送简单邮件便捷方法
fmt.Println("\n=== Example 2: Send Simple Email ===")
err = mailer.SendSimple(
[]string{"recipient@example.com"},
"测试邮件",
"这是一封测试邮件使用Go标准库发送。",
)
if err != nil {
log.Printf("Failed to send email: %v", err)
} else {
fmt.Println("Email sent successfully")
}
// 示例3发送HTML邮件
fmt.Println("\n=== Example 3: Send HTML Email ===")
htmlBody := `
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>欢迎使用邮件服务</h1>
<p>这是一封HTML格式的邮件。</p>
<p>支持<strong>富文本</strong>格式。</p>
</body>
</html>
`
err = mailer.SendHTML(
[]string{"recipient@example.com"},
"HTML邮件测试",
htmlBody,
)
if err != nil {
log.Printf("Failed to send HTML email: %v", err)
} else {
fmt.Println("HTML email sent successfully")
}
// 示例4发送完整邮件包含抄送、密送
fmt.Println("\n=== Example 4: Send Full 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>
<p>支持同时发送纯文本和HTML版本。</p>
</body>
</html>
`,
}
err = mailer.Send(msg)
if err != nil {
log.Printf("Failed to send full email: %v", err)
} else {
fmt.Println("Full email sent successfully")
}
fmt.Println("\nNote: Make sure your email configuration is correct and SMTP service is enabled.")
}

101
examples/http_example.go Normal file
View File

@@ -0,0 +1,101 @@
package main
import (
"log"
"net/http"
"github.com/go-common/http"
)
// 用户结构
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// 获取用户列表
func GetUserList(w http.ResponseWriter, r *http.Request) {
// 获取分页参数
page, pageSize := http.GetPaginationParams(r)
// 获取查询参数
keyword := http.GetQuery(r, "keyword", "")
// 模拟查询数据
users := []User{
{ID: 1, Name: "User1", Email: "user1@example.com"},
{ID: 2, Name: "User2", Email: "user2@example.com"},
}
total := int64(100)
// 返回分页响应
http.SuccessPage(w, users, total, page, pageSize)
}
// 创建用户
func CreateUser(w http.ResponseWriter, r *http.Request) {
// 解析请求体
var req struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := http.ParseJSON(r, &req); err != nil {
http.BadRequest(w, "请求参数解析失败")
return
}
// 参数验证
if req.Name == "" {
http.Error(w, 1001, "用户名不能为空")
return
}
// 模拟创建用户
user := User{
ID: 1,
Name: req.Name,
Email: req.Email,
}
// 返回成功响应
http.SuccessWithMessage(w, "创建成功", user)
}
// 获取用户详情
func GetUser(w http.ResponseWriter, r *http.Request) {
// 获取查询参数
id := http.GetQueryInt64(r, "id", 0)
if id == 0 {
http.BadRequest(w, "用户ID不能为空")
return
}
// 模拟查询用户
if id == 1 {
user := User{ID: 1, Name: "User1", Email: "user1@example.com"}
http.Success(w, user)
} else {
http.Error(w, 1002, "用户不存在")
}
}
func main() {
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
GetUserList(w, r)
case http.MethodPost:
CreateUser(w, r)
default:
http.NotFound(w, "方法不支持")
}
})
http.HandleFunc("/user", GetUser)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

View File

@@ -0,0 +1,64 @@
package main
import (
"log"
"net/http"
"github.com/go-common/datetime"
"github.com/go-common/http"
"github.com/go-common/middleware"
)
// 示例使用CORS和时区中间件
func main() {
// 配置CORS
corsConfig := &middleware.CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{
"Content-Type",
"Authorization",
"X-Requested-With",
"X-Timezone",
},
AllowCredentials: false,
MaxAge: 3600,
}
// 创建中间件链
chain := middleware.NewChain(
middleware.CORS(corsConfig),
middleware.Timezone,
)
// 定义处理器
handler := chain.ThenFunc(apiHandler)
// 注册路由
http.Handle("/api", handler)
log.Println("Server started on :8080")
log.Println("Try: curl -H 'X-Timezone: America/New_York' http://localhost:8080/api")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// apiHandler 处理API请求
func apiHandler(w http.ResponseWriter, r *http.Request) {
// 从context获取时区
timezone := http.GetTimezone(r)
// 使用时区进行时间处理
now := datetime.Now(timezone)
startOfDay := datetime.StartOfDay(now, timezone)
endOfDay := datetime.EndOfDay(now, timezone)
// 返回响应
http.Success(w, map[string]interface{}{
"message": "Hello from API",
"timezone": timezone,
"currentTime": datetime.FormatDateTime(now),
"startOfDay": datetime.FormatDateTime(startOfDay),
"endOfDay": datetime.FormatDateTime(endOfDay),
})
}

View File

@@ -0,0 +1,58 @@
package main
import (
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"github.com/go-common/migration"
)
func main() {
// 初始化数据库连接
dsn := "user:password@tcp(localhost:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 创建迁移器
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 IF NOT EXISTS 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
},
})
// 执行迁移
if err := migrator.Up(); err != nil {
log.Fatal(err)
}
// 查看迁移状态
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)
}
}

102
examples/sms_example.go Normal file
View File

@@ -0,0 +1,102 @@
package main
import (
"fmt"
"log"
"github.com/go-common/config"
"github.com/go-common/sms"
)
func main() {
// 加载配置
cfg, err := config.LoadFromFile("./config/example.json")
if err != nil {
log.Fatal("Failed to load config:", err)
}
// 创建短信发送器
smsConfig := cfg.GetSMS()
if smsConfig == nil {
log.Fatal("SMS config is nil")
}
smsClient, err := sms.NewSMS(smsConfig)
if err != nil {
log.Fatal("Failed to create SMS client:", err)
}
// 示例1发送原始请求推荐最灵活
fmt.Println("=== Example 1: Send Raw SMS Request ===")
// 外部构建完整的请求参数
params := map[string]string{
"PhoneNumbers": "13800138000",
"SignName": smsConfig.SignName,
"TemplateCode": smsConfig.TemplateCode,
"TemplateParam": `{"code":"123456","expire":"5"}`,
}
resp, err := smsClient.SendRaw(params)
if err != nil {
log.Printf("Failed to send raw SMS: %v", err)
} else {
fmt.Printf("Raw SMS sent successfully, RequestID: %s\n", resp.RequestID)
}
// 示例2发送简单短信使用配置中的模板代码
fmt.Println("\n=== Example 2: Send Simple SMS ===")
templateParam := map[string]string{
"code": "123456",
"expire": "5",
}
resp2, err := smsClient.SendSimple(
[]string{"13800138000"},
templateParam,
)
if err != nil {
log.Printf("Failed to send SMS: %v", err)
} else {
fmt.Printf("SMS sent successfully, RequestID: %s\n", resp2.RequestID)
}
// 示例3使用指定模板发送短信
fmt.Println("\n=== Example 3: Send SMS with Template ===")
templateParam3 := map[string]string{
"code": "654321",
"expire": "10",
}
resp3, err := smsClient.SendWithTemplate(
[]string{"13800138000"},
"SMS_123456789", // 使用指定的模板代码
templateParam3,
)
if err != nil {
log.Printf("Failed to send SMS: %v", err)
} else {
fmt.Printf("SMS sent successfully, RequestID: %s\n", resp3.RequestID)
}
// 示例4使用JSON字符串作为模板参数
fmt.Println("\n=== Example 4: Send SMS with JSON String Template Param ===")
req := &sms.SendRequest{
PhoneNumbers: []string{"13800138000"},
TemplateCode: smsConfig.TemplateCode,
TemplateParam: `{"code":"888888","expire":"15"}`, // 直接使用JSON字符串
}
resp4, err := smsClient.Send(req)
if err != nil {
log.Printf("Failed to send SMS: %v", err)
} else {
fmt.Printf("SMS sent successfully, RequestID: %s\n", resp4.RequestID)
}
fmt.Println("\nNote: Make sure your Aliyun SMS service is configured correctly:")
fmt.Println("1. AccessKey ID and Secret are valid")
fmt.Println("2. Sign name is approved")
fmt.Println("3. Template code is approved")
fmt.Println("4. Template parameters match the template definition")
}

View File

@@ -0,0 +1,71 @@
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/example.json")
if err != nil {
log.Fatal("Failed to load config:", err)
}
// 创建存储实例使用OSS
// 注意需要先实现OSS SDK集成
ossStorage, err := storage.NewStorage(storage.StorageTypeOSS, cfg)
if err != nil {
log.Printf("Failed to create OSS storage: %v", err)
log.Println("Note: OSS SDK integration is required")
// 继续演示其他功能
} else {
// 创建上传处理器
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("Storage server started on :8080")
log.Println("Upload: POST /upload")
log.Println("View: GET /file?key=images/test.jpg")
log.Fatal(http.ListenAndServe(":8080", mux))
}
// 演示MinIO存储
minioStorage, err := storage.NewStorage(storage.StorageTypeMinIO, cfg)
if err != nil {
log.Printf("Failed to create MinIO storage: %v", err)
log.Println("Note: MinIO SDK integration is required")
} else {
log.Printf("MinIO storage created: %v", minioStorage != nil)
}
// 演示对象键生成
objectKey1 := storage.GenerateObjectKey("images/", "test.jpg")
log.Printf("Object key 1: %s", objectKey1)
objectKey2 := storage.GenerateObjectKeyWithDate("images", "test.jpg")
log.Printf("Object key 2: %s", objectKey2)
}

14
go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/go-common
go 1.21
require (
gorm.io/driver/mysql v1.5.2
gorm.io/gorm v1.25.5
)
require (
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
)

12
go.sum Normal file
View File

@@ -0,0 +1,12 @@
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

207
http/request.go Normal file
View File

@@ -0,0 +1,207 @@
package http
import (
"context"
"encoding/json"
"io"
"net/http"
"strconv"
"github.com/go-common/middleware"
)
// ParseJSON 解析JSON请求体
// r: HTTP请求
// v: 目标结构体指针
func ParseJSON(r *http.Request, v interface{}) error {
body, err := io.ReadAll(r.Body)
if err != nil {
return err
}
defer r.Body.Close()
if len(body) == 0 {
return nil
}
return json.Unmarshal(body, v)
}
// GetQuery 获取查询参数
// r: HTTP请求
// key: 参数名
// defaultValue: 默认值
func GetQuery(r *http.Request, key, defaultValue string) string {
value := r.URL.Query().Get(key)
if value == "" {
return defaultValue
}
return value
}
// GetQueryInt 获取整数查询参数
// r: HTTP请求
// key: 参数名
// defaultValue: 默认值
func GetQueryInt(r *http.Request, key string, defaultValue int) int {
value := r.URL.Query().Get(key)
if value == "" {
return defaultValue
}
intValue, err := strconv.Atoi(value)
if err != nil {
return defaultValue
}
return intValue
}
// GetQueryInt64 获取int64查询参数
func GetQueryInt64(r *http.Request, key string, defaultValue int64) int64 {
value := r.URL.Query().Get(key)
if value == "" {
return defaultValue
}
intValue, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return defaultValue
}
return intValue
}
// GetQueryBool 获取布尔查询参数
func GetQueryBool(r *http.Request, key string, defaultValue bool) bool {
value := r.URL.Query().Get(key)
if value == "" {
return defaultValue
}
boolValue, err := strconv.ParseBool(value)
if err != nil {
return defaultValue
}
return boolValue
}
// GetQueryFloat64 获取float64查询参数
func GetQueryFloat64(r *http.Request, key string, defaultValue float64) float64 {
value := r.URL.Query().Get(key)
if value == "" {
return defaultValue
}
floatValue, err := strconv.ParseFloat(value, 64)
if err != nil {
return defaultValue
}
return floatValue
}
// GetFormValue 获取表单值
func GetFormValue(r *http.Request, key, defaultValue string) string {
value := r.FormValue(key)
if value == "" {
return defaultValue
}
return value
}
// GetFormInt 获取表单整数
func GetFormInt(r *http.Request, key string, defaultValue int) int {
value := r.FormValue(key)
if value == "" {
return defaultValue
}
intValue, err := strconv.Atoi(value)
if err != nil {
return defaultValue
}
return intValue
}
// GetFormInt64 获取表单int64
func GetFormInt64(r *http.Request, key string, defaultValue int64) int64 {
value := r.FormValue(key)
if value == "" {
return defaultValue
}
intValue, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return defaultValue
}
return intValue
}
// GetFormBool 获取表单布尔值
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 获取请求头
func GetHeader(r *http.Request, key, defaultValue string) string {
value := r.Header.Get(key)
if value == "" {
return defaultValue
}
return value
}
// GetPaginationParams 获取分页参数
// 返回 page, pageSize
// 默认 page=1, pageSize=10
func GetPaginationParams(r *http.Request) (page, pageSize int) {
page = GetQueryInt(r, "page", 1)
pageSize = GetQueryInt(r, "pageSize", 10)
// 参数校验
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 1000 {
pageSize = 1000 // 限制最大页面大小
}
return page, pageSize
}
// GetOffset 根据页码和每页大小计算偏移量
func GetOffset(page, pageSize int) int {
if page < 1 {
page = 1
}
return (page - 1) * pageSize
}
// GetTimezone 从请求的context中获取时区
// 如果使用了middleware.Timezone中间件可以从context中获取时区信息
// 如果未设置,返回默认时区 AsiaShanghai
func GetTimezone(r *http.Request) string {
return middleware.GetTimezoneFromContext(r.Context())
}
// GetTimezoneFromContext 从context中获取时区
func GetTimezoneFromContext(ctx context.Context) string {
return middleware.GetTimezoneFromContext(ctx)
}

138
http/response.go Normal file
View File

@@ -0,0 +1,138 @@
package http
import (
"encoding/json"
"net/http"
"time"
)
// Response 标准响应结构
type Response struct {
Code int `json:"code"` // 业务状态码0表示成功
Message string `json:"message"` // 响应消息
Timestamp int64 `json:"timestamp"` // 时间戳
Data interface{} `json:"data"` // 响应数据
}
// PageResponse 分页响应结构
type PageResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Timestamp int64 `json:"timestamp"`
Data *PageData `json:"data"`
}
// PageData 分页数据
type PageData struct {
List interface{} `json:"list"` // 数据列表
Total int64 `json:"total"` // 总记录数
Page int `json:"page"` // 当前页码
PageSize int `json:"pageSize"` // 每页大小
}
// Success 成功响应
// data: 响应数据可以为nil
func Success(w http.ResponseWriter, data interface{}) {
WriteJSON(w, http.StatusOK, 0, "success", data)
}
// SuccessWithMessage 带消息的成功响应
func SuccessWithMessage(w http.ResponseWriter, message string, data interface{}) {
WriteJSON(w, http.StatusOK, 0, message, data)
}
// Error 错误响应
// code: 业务错误码非0表示业务错误
// message: 错误消息
func Error(w http.ResponseWriter, code int, message string) {
WriteJSON(w, http.StatusOK, code, message, nil)
}
// SystemError 系统错误响应返回HTTP 500
// message: 错误消息
func SystemError(w http.ResponseWriter, message string) {
WriteJSON(w, http.StatusInternalServerError, 500, message, nil)
}
// BadRequest 请求错误响应HTTP 400
func BadRequest(w http.ResponseWriter, message string) {
WriteJSON(w, http.StatusBadRequest, 400, message, nil)
}
// Unauthorized 未授权响应HTTP 401
func Unauthorized(w http.ResponseWriter, message string) {
WriteJSON(w, http.StatusUnauthorized, 401, message, nil)
}
// Forbidden 禁止访问响应HTTP 403
func Forbidden(w http.ResponseWriter, message string) {
WriteJSON(w, http.StatusForbidden, 403, message, nil)
}
// NotFound 未找到响应HTTP 404
func NotFound(w http.ResponseWriter, message string) {
WriteJSON(w, http.StatusNotFound, 404, message, nil)
}
// WriteJSON 写入JSON响应
// httpCode: HTTP状态码200表示正常500表示系统错误等
// code: 业务状态码0表示成功非0表示业务错误
// message: 响应消息
// data: 响应数据
func WriteJSON(w http.ResponseWriter, httpCode, code int, message string, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(httpCode)
response := Response{
Code: code,
Message: message,
Timestamp: time.Now().Unix(),
Data: data,
}
json.NewEncoder(w).Encode(response)
}
// SuccessPage 分页成功响应
// list: 数据列表
// total: 总记录数
// page: 当前页码
// pageSize: 每页大小
func SuccessPage(w http.ResponseWriter, list interface{}, total int64, page, pageSize int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
response := PageResponse{
Code: 0,
Message: "success",
Timestamp: time.Now().Unix(),
Data: &PageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
},
}
json.NewEncoder(w).Encode(response)
}
// SuccessPageWithMessage 带消息的分页成功响应
func SuccessPageWithMessage(w http.ResponseWriter, message string, list interface{}, total int64, page, pageSize int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
response := PageResponse{
Code: 0,
Message: message,
Timestamp: time.Now().Unix(),
Data: &PageData{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
},
}
json.NewEncoder(w).Encode(response)
}

36
middleware/chain.go Normal file
View File

@@ -0,0 +1,36 @@
package middleware
import "net/http"
// Chain 中间件链
type Chain struct {
middlewares []func(http.Handler) http.Handler
}
// NewChain 创建新的中间件链
func NewChain(middlewares ...func(http.Handler) http.Handler) *Chain {
return &Chain{
middlewares: middlewares,
}
}
// Then 将中间件链应用到处理器
func (c *Chain) Then(handler http.Handler) http.Handler {
final := handler
for i := len(c.middlewares) - 1; i >= 0; i-- {
final = c.middlewares[i](final)
}
return final
}
// ThenFunc 将中间件链应用到处理器函数
func (c *Chain) ThenFunc(handler http.HandlerFunc) http.Handler {
return c.Then(handler)
}
// Append 追加中间件
func (c *Chain) Append(middlewares ...func(http.Handler) http.Handler) *Chain {
c.middlewares = append(c.middlewares, middlewares...)
return c
}

207
middleware/cors.go Normal file
View File

@@ -0,0 +1,207 @@
package middleware
import (
"net/http"
"strconv"
"strings"
)
// CORSConfig CORS配置
type CORSConfig struct {
// AllowedOrigins 允许的源,支持通配符 "*" 表示允许所有源
// 例如: []string{"*"} 或 []string{"https://example.com", "https://app.example.com"}
AllowedOrigins []string
// AllowedMethods 允许的HTTP方法
// 默认: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"}
AllowedMethods []string
// AllowedHeaders 允许的请求头
// 默认: []string{"Content-Type", "Authorization", "X-Requested-With", "X-Timezone"}
AllowedHeaders []string
// ExposedHeaders 暴露给客户端的响应头
ExposedHeaders []string
// AllowCredentials 是否允许发送凭证cookies等
AllowCredentials bool
// MaxAge 预检请求的缓存时间(秒)
// 默认: 86400 (24小时)
MaxAge int
}
// DefaultCORSConfig 返回默认的CORS配置
func DefaultCORSConfig() *CORSConfig {
return &CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Requested-With", "X-Timezone"},
ExposedHeaders: []string{},
AllowCredentials: false,
MaxAge: 86400,
}
}
// CORS CORS中间件
func CORS(config ...*CORSConfig) func(http.Handler) http.Handler {
var cfg *CORSConfig
if len(config) > 0 && config[0] != nil {
cfg = config[0]
} else {
cfg = DefaultCORSConfig()
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// 处理预检请求
if r.Method == http.MethodOptions {
handlePreflight(w, r, cfg, origin)
return
}
// 处理实际请求
handleCORSHeaders(w, r, cfg, origin)
next.ServeHTTP(w, r)
})
}
}
// handlePreflight 处理预检请求
func handlePreflight(w http.ResponseWriter, r *http.Request, cfg *CORSConfig, origin string) {
// 检查源是否允许
if !isOriginAllowed(origin, cfg.AllowedOrigins) {
w.WriteHeader(http.StatusForbidden)
return
}
// 设置CORS响应头
setCORSHeaders(w, cfg, origin)
// 检查请求的方法是否允许
requestMethod := r.Header.Get("Access-Control-Request-Method")
if requestMethod != "" && !isMethodAllowed(requestMethod, cfg.AllowedMethods) {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// 检查请求头是否允许
requestHeaders := r.Header.Get("Access-Control-Request-Headers")
if requestHeaders != "" {
headers := strings.Split(strings.ToLower(requestHeaders), ",")
for _, header := range headers {
header = strings.TrimSpace(header)
if !isHeaderAllowed(header, cfg.AllowedHeaders) {
w.WriteHeader(http.StatusForbidden)
return
}
}
}
w.WriteHeader(http.StatusNoContent)
}
// handleCORSHeaders 处理实际请求的CORS头
func handleCORSHeaders(w http.ResponseWriter, r *http.Request, cfg *CORSConfig, origin string) {
// 检查源是否允许
if !isOriginAllowed(origin, cfg.AllowedOrigins) {
return
}
// 设置CORS响应头
setCORSHeaders(w, cfg, origin)
}
// setCORSHeaders 设置CORS响应头
func setCORSHeaders(w http.ResponseWriter, cfg *CORSConfig, origin string) {
// Access-Control-Allow-Origin
if len(cfg.AllowedOrigins) == 1 && cfg.AllowedOrigins[0] == "*" {
if !cfg.AllowCredentials {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
// 如果允许凭证,不能使用 "*",必须返回具体的源
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
}
} else {
if isOriginAllowed(origin, cfg.AllowedOrigins) {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
}
// Access-Control-Allow-Methods
if len(cfg.AllowedMethods) > 0 {
w.Header().Set("Access-Control-Allow-Methods", strings.Join(cfg.AllowedMethods, ", "))
}
// Access-Control-Allow-Headers
if len(cfg.AllowedHeaders) > 0 {
w.Header().Set("Access-Control-Allow-Headers", strings.Join(cfg.AllowedHeaders, ", "))
}
// Access-Control-Expose-Headers
if len(cfg.ExposedHeaders) > 0 {
w.Header().Set("Access-Control-Expose-Headers", strings.Join(cfg.ExposedHeaders, ", "))
}
// Access-Control-Allow-Credentials
if cfg.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
// Access-Control-Max-Age
if cfg.MaxAge > 0 {
w.Header().Set("Access-Control-Max-Age", strconv.Itoa(cfg.MaxAge))
}
}
// isOriginAllowed 检查源是否允许
func isOriginAllowed(origin string, allowedOrigins []string) bool {
if origin == "" {
return false
}
for _, allowed := range allowedOrigins {
if allowed == "*" {
return true
}
if allowed == origin {
return true
}
// 支持简单的通配符匹配(如 "*.example.com"
if strings.HasPrefix(allowed, "*.") {
domain := strings.TrimPrefix(allowed, "*.")
if strings.HasSuffix(origin, domain) {
return true
}
}
}
return false
}
// isMethodAllowed 检查方法是否允许
func isMethodAllowed(method string, allowedMethods []string) bool {
method = strings.ToUpper(strings.TrimSpace(method))
for _, allowed := range allowedMethods {
if strings.ToUpper(allowed) == method {
return true
}
}
return false
}
// isHeaderAllowed 检查请求头是否允许
func isHeaderAllowed(header string, allowedHeaders []string) bool {
header = strings.ToLower(strings.TrimSpace(header))
for _, allowed := range allowedHeaders {
if strings.ToLower(allowed) == header {
return true
}
}
return false
}

82
middleware/timezone.go Normal file
View File

@@ -0,0 +1,82 @@
package middleware
import (
"context"
"net/http"
"github.com/go-common/datetime"
)
// TimezoneKey context中存储时区的key
type timezoneKey struct{}
// TimezoneHeaderName 时区请求头名称
const TimezoneHeaderName = "X-Timezone"
// DefaultTimezone 默认时区
const DefaultTimezone = datetime.AsiaShanghai
// GetTimezoneFromContext 从context中获取时区
func GetTimezoneFromContext(ctx context.Context) string {
if tz, ok := ctx.Value(timezoneKey{}).(string); ok && tz != "" {
return tz
}
return DefaultTimezone
}
// Timezone 时区处理中间件
// 从请求头 X-Timezone 读取时区信息,如果未传递则使用默认时区 AsiaShanghai
// 时区信息会存储到context中可以通过 GetTimezoneFromContext 获取
func Timezone(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求头获取时区
timezone := r.Header.Get(TimezoneHeaderName)
// 如果未传递时区信息,使用默认时区
if timezone == "" {
timezone = DefaultTimezone
}
// 验证时区是否有效
if _, err := datetime.GetLocation(timezone); err != nil {
// 如果时区无效,使用默认时区
timezone = DefaultTimezone
}
// 将时区存储到context中
ctx := context.WithValue(r.Context(), timezoneKey{}, timezone)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// TimezoneWithDefault 时区处理中间件(可自定义默认时区)
// defaultTimezone: 默认时区,如果未指定则使用 AsiaShanghai
func TimezoneWithDefault(defaultTimezone string) func(http.Handler) http.Handler {
// 验证默认时区是否有效
if _, err := datetime.GetLocation(defaultTimezone); err != nil {
defaultTimezone = DefaultTimezone
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求头获取时区
timezone := r.Header.Get(TimezoneHeaderName)
// 如果未传递时区信息,使用指定的默认时区
if timezone == "" {
timezone = defaultTimezone
}
// 验证时区是否有效
if _, err := datetime.GetLocation(timezone); err != nil {
// 如果时区无效,使用默认时区
timezone = defaultTimezone
}
// 将时区存储到context中
ctx := context.WithValue(r.Context(), timezoneKey{}, timezone)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

373
migration/migration.go Normal file
View File

@@ -0,0 +1,373 @@
package migration
import (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
// Migration 表示一个数据库迁移
type Migration struct {
Version string
Description string
Up func(*gorm.DB) error
Down func(*gorm.DB) error
}
// Migrator 数据库迁移器
type Migrator struct {
db *gorm.DB
migrations []Migration
tableName string
}
// NewMigrator 创建新的迁移器
// db: GORM数据库连接
// tableName: 存储迁移记录的表名,默认为 "schema_migrations"
func NewMigrator(db *gorm.DB, tableName ...string) *Migrator {
table := "schema_migrations"
if len(tableName) > 0 && tableName[0] != "" {
table = tableName[0]
}
return &Migrator{
db: db,
migrations: make([]Migration, 0),
tableName: table,
}
}
// AddMigration 添加迁移
func (m *Migrator) AddMigration(migration Migration) {
m.migrations = append(m.migrations, migration)
}
// AddMigrations 批量添加迁移
func (m *Migrator) AddMigrations(migrations ...Migration) {
m.migrations = append(m.migrations, migrations...)
}
// initTable 初始化迁移记录表
func (m *Migrator) initTable() error {
// 检查表是否存在
var exists bool
err := m.db.Raw(fmt.Sprintf(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = CURRENT_SCHEMA()
AND table_name = '%s'
)
`, m.tableName)).Scan(&exists).Error
if err != nil {
// 如果查询失败可能是SQLite或其他数据库尝试直接创建
exists = false
}
if !exists {
// 创建迁移记录表
err = m.db.Exec(fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
version VARCHAR(255) PRIMARY KEY,
description VARCHAR(255),
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`, m.tableName)).Error
if err != nil {
return fmt.Errorf("failed to create migration table: %w", err)
}
}
return nil
}
// getAppliedMigrations 获取已应用的迁移版本
func (m *Migrator) getAppliedMigrations() (map[string]bool, error) {
if err := m.initTable(); err != nil {
return nil, err
}
var versions []string
err := m.db.Table(m.tableName).Select("version").Pluck("version", &versions).Error
if err != nil {
return nil, fmt.Errorf("failed to get applied migrations: %w", err)
}
applied := make(map[string]bool)
for _, v := range versions {
applied[v] = true
}
return applied, nil
}
// recordMigration 记录迁移
func (m *Migrator) recordMigration(version, description string, isUp bool) error {
if err := m.initTable(); err != nil {
return err
}
if isUp {
// 记录迁移
err := m.db.Exec(fmt.Sprintf(`
INSERT INTO %s (version, description, applied_at)
VALUES (?, ?, ?)
`, m.tableName), version, description, time.Now()).Error
if err != nil {
return fmt.Errorf("failed to record migration: %w", err)
}
} else {
// 删除迁移记录
err := m.db.Exec(fmt.Sprintf("DELETE FROM %s WHERE version = ?", m.tableName), version).Error
if err != nil {
return fmt.Errorf("failed to remove migration record: %w", err)
}
}
return nil
}
// Up 执行所有未应用的迁移
func (m *Migrator) Up() error {
if err := m.initTable(); err != nil {
return err
}
applied, err := m.getAppliedMigrations()
if err != nil {
return err
}
// 排序迁移
sort.Slice(m.migrations, func(i, j int) bool {
return m.migrations[i].Version < m.migrations[j].Version
})
// 执行未应用的迁移
for _, migration := range m.migrations {
if applied[migration.Version] {
continue
}
if migration.Up == nil {
return fmt.Errorf("migration %s has no Up function", migration.Version)
}
// 开始事务
tx := m.db.Begin()
if tx.Error != nil {
return fmt.Errorf("failed to begin transaction: %w", tx.Error)
}
// 执行迁移
if err := migration.Up(tx); err != nil {
tx.Rollback()
return fmt.Errorf("failed to apply migration %s: %w", migration.Version, err)
}
// 记录迁移
if err := m.recordMigrationWithDB(tx, migration.Version, migration.Description, true); err != nil {
tx.Rollback()
return err
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit migration %s: %w", migration.Version, err)
}
fmt.Printf("Applied migration: %s - %s\n", migration.Version, migration.Description)
}
return nil
}
// Down 回滚最后一个迁移
func (m *Migrator) Down() error {
if err := m.initTable(); err != nil {
return err
}
applied, err := m.getAppliedMigrations()
if err != nil {
return err
}
// 排序迁移(倒序)
sort.Slice(m.migrations, func(i, j int) bool {
return m.migrations[i].Version > m.migrations[j].Version
})
// 找到最后一个已应用的迁移并回滚
for _, migration := range m.migrations {
if !applied[migration.Version] {
continue
}
if migration.Down == nil {
return fmt.Errorf("migration %s has no Down function", migration.Version)
}
// 开始事务
tx := m.db.Begin()
if tx.Error != nil {
return fmt.Errorf("failed to begin transaction: %w", tx.Error)
}
// 执行回滚
if err := migration.Down(tx); err != nil {
tx.Rollback()
return fmt.Errorf("failed to rollback migration %s: %w", migration.Version, err)
}
// 删除迁移记录
if err := m.recordMigrationWithDB(tx, migration.Version, migration.Description, false); err != nil {
tx.Rollback()
return err
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit rollback %s: %w", migration.Version, err)
}
fmt.Printf("Rolled back migration: %s - %s\n", migration.Version, migration.Description)
return nil
}
return fmt.Errorf("no migrations to rollback")
}
// recordMigrationWithDB 使用指定的数据库连接记录迁移
func (m *Migrator) recordMigrationWithDB(db *gorm.DB, version, description string, isUp bool) error {
if isUp {
err := db.Exec(fmt.Sprintf(`
INSERT INTO %s (version, description, applied_at)
VALUES (?, ?, ?)
`, m.tableName), version, description, time.Now()).Error
if err != nil {
return fmt.Errorf("failed to record migration: %w", err)
}
} else {
err := db.Exec(fmt.Sprintf("DELETE FROM %s WHERE version = ?", m.tableName), version).Error
if err != nil {
return fmt.Errorf("failed to remove migration record: %w", err)
}
}
return nil
}
// Status 查看迁移状态
func (m *Migrator) Status() ([]MigrationStatus, error) {
if err := m.initTable(); err != nil {
return nil, err
}
applied, err := m.getAppliedMigrations()
if err != nil {
return nil, err
}
// 排序迁移
sort.Slice(m.migrations, func(i, j int) bool {
return m.migrations[i].Version < m.migrations[j].Version
})
status := make([]MigrationStatus, 0, len(m.migrations))
for _, migration := range m.migrations {
status = append(status, MigrationStatus{
Version: migration.Version,
Description: migration.Description,
Applied: applied[migration.Version],
})
}
return status, nil
}
// MigrationStatus 迁移状态
type MigrationStatus struct {
Version string
Description string
Applied bool
}
// LoadMigrationsFromFiles 从文件系统加载迁移文件
// dir: 迁移文件目录
// pattern: 文件命名模式,例如 "*.sql" 或 "*.up.sql"
// 文件命名格式: {version}_{description}.sql 或 {version}_{description}.up.sql
func LoadMigrationsFromFiles(dir string, pattern string) ([]Migration, error) {
files, err := filepath.Glob(filepath.Join(dir, pattern))
if err != nil {
return nil, fmt.Errorf("failed to glob migration files: %w", err)
}
migrations := make([]Migration, 0)
for _, file := range files {
baseName := filepath.Base(file)
// 移除扩展名
nameWithoutExt := strings.TrimSuffix(baseName, filepath.Ext(baseName))
// 移除 .up 后缀(如果存在)
nameWithoutExt = strings.TrimSuffix(nameWithoutExt, ".up")
// 解析版本号和描述
parts := strings.SplitN(nameWithoutExt, "_", 2)
if len(parts) < 2 {
return nil, fmt.Errorf("invalid migration file name format: %s (expected: {version}_{description})", baseName)
}
version := parts[0]
description := strings.Join(parts[1:], "_")
// 读取文件内容
content, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("failed to read migration file %s: %w", file, err)
}
sqlContent := string(content)
// 查找对应的 down 文件
downFile := strings.Replace(file, ".up.sql", ".down.sql", 1)
downFile = strings.Replace(downFile, ".sql", ".down.sql", 1)
var downSQL string
if downContent, err := os.ReadFile(downFile); err == nil {
downSQL = string(downContent)
}
migration := Migration{
Version: version,
Description: description,
Up: func(db *gorm.DB) error {
return db.Exec(sqlContent).Error
},
}
if downSQL != "" {
migration.Down = func(db *gorm.DB) error {
return db.Exec(downSQL).Error
}
}
migrations = append(migrations, migration)
}
// 按版本号排序
sort.Slice(migrations, func(i, j int) bool {
return migrations[i].Version < migrations[j].Version
})
return migrations, nil
}
// GenerateVersion 生成迁移版本号(基于时间戳)
func GenerateVersion() string {
return strconv.FormatInt(time.Now().Unix(), 10)
}

290
sms/sms.go Normal file
View File

@@ -0,0 +1,290 @@
package sms
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
"github.com/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
RequestID string `json:"RequestId"`
// Code 响应码
Code string `json:"Code"`
// Message 响应消息
Message string `json:"Message"`
// BizID 业务ID
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)
}
// 确保必要的系统参数存在
if params["Action"] == "" {
params["Action"] = "SendSms"
}
if params["Version"] == "" {
params["Version"] = "2017-05-25"
}
if params["RegionId"] == "" {
params["RegionId"] = s.config.Region
}
if params["AccessKeyId"] == "" {
params["AccessKeyId"] = s.config.AccessKeyID
}
if params["Format"] == "" {
params["Format"] = "JSON"
}
if params["SignatureMethod"] == "" {
params["SignatureMethod"] = "HMAC-SHA1"
}
if params["SignatureVersion"] == "" {
params["SignatureVersion"] = "1.0"
}
if params["SignatureNonce"] == "" {
params["SignatureNonce"] = fmt.Sprint(time.Now().UnixNano())
}
if params["Timestamp"] == "" {
params["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05Z")
}
// 计算签名
signature := s.calculateSignature(params, "POST")
params["Signature"] = signature
// 构建请求URL
endpoint := s.config.Endpoint
if endpoint == "" {
endpoint = "https://dysmsapi.aliyuncs.com"
}
// 发送HTTP请求
formData := url.Values{}
for k, v := range params {
formData.Set(k, v)
}
httpReq, err := http.NewRequest("POST", endpoint, strings.NewReader(formData.Encode()))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
httpReq.Header.Set("Accept", "application/json")
client := &http.Client{
Timeout: time.Duration(s.config.Timeout) * time.Second,
}
resp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// 解析响应
var sendResp SendResponse
if err := json.Unmarshal(body, &sendResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// 检查响应码
if sendResp.Code != "OK" {
return &sendResp, fmt.Errorf("SMS send failed: Code=%s, Message=%s", sendResp.Code, sendResp.Message)
}
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 {
// 对参数进行排序
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// 构建查询字符串
var queryParts []string
for _, k := range keys {
v := params[k]
// URL编码
encodedKey := url.QueryEscape(k)
encodedValue := url.QueryEscape(v)
queryParts = append(queryParts, encodedKey+"="+encodedValue)
}
queryString := strings.Join(queryParts, "&")
// 构建待签名字符串
stringToSign := method + "&" + url.QueryEscape("/") + "&" + url.QueryEscape(queryString)
// 计算HMAC-SHA1签名
mac := hmac.New(sha1.New, []byte(s.config.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,
})
}

212
storage/handler.go Normal file
View File

@@ -0,0 +1,212 @@
package storage
import (
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"time"
commonhttp "github.com/go-common/http"
)
// UploadHandler 文件上传处理器
type UploadHandler struct {
storage Storage
maxFileSize int64 // 最大文件大小字节0表示不限制
allowedExts []string // 允许的文件扩展名,空表示不限制
objectPrefix string // 对象键前缀
}
// UploadHandlerConfig 上传处理器配置
type UploadHandlerConfig struct {
Storage Storage
MaxFileSize int64 // 最大文件大小字节0表示不限制
AllowedExts []string // 允许的文件扩展名,空表示不限制
ObjectPrefix string // 对象键前缀(如 "images/", "files/"
}
// NewUploadHandler 创建文件上传处理器
func NewUploadHandler(cfg UploadHandlerConfig) *UploadHandler {
return &UploadHandler{
storage: cfg.Storage,
maxFileSize: cfg.MaxFileSize,
allowedExts: cfg.AllowedExts,
objectPrefix: cfg.ObjectPrefix,
}
}
// ServeHTTP 处理文件上传请求
// 请求方式: POST
// 表单字段: file (文件)
// 可选字段: prefix (对象键前缀,会覆盖配置中的前缀)
func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
commonhttp.NotFound(w, "Method not allowed")
return
}
// 解析multipart表单
err := r.ParseMultipartForm(h.maxFileSize)
if err != nil {
commonhttp.BadRequest(w, fmt.Sprintf("Failed to parse form: %v", err))
return
}
// 获取文件
file, header, err := r.FormFile("file")
if err != nil {
commonhttp.BadRequest(w, fmt.Sprintf("Failed to get file: %v", err))
return
}
defer file.Close()
// 检查文件大小
if h.maxFileSize > 0 && header.Size > h.maxFileSize {
commonhttp.Error(w, 1001, fmt.Sprintf("File size exceeds limit: %d bytes", h.maxFileSize))
return
}
// 检查文件扩展名
if len(h.allowedExts) > 0 {
ext := strings.ToLower(filepath.Ext(header.Filename))
allowed := false
for _, allowedExt := range h.allowedExts {
if strings.ToLower(allowedExt) == ext {
allowed = true
break
}
}
if !allowed {
commonhttp.Error(w, 1002, fmt.Sprintf("File extension not allowed. Allowed: %v", h.allowedExts))
return
}
}
// 生成对象键
prefix := h.objectPrefix
if r.FormValue("prefix") != "" {
prefix = r.FormValue("prefix")
}
// 生成唯一文件名
filename := generateUniqueFilename(header.Filename)
objectKey := GenerateObjectKey(prefix, filename)
// 获取文件类型
contentType := header.Header.Get("Content-Type")
if contentType == "" {
contentType = mime.TypeByExtension(filepath.Ext(header.Filename))
if contentType == "" {
contentType = "application/octet-stream"
}
}
// 上传文件
ctx := r.Context()
err = h.storage.Upload(ctx, objectKey, file, contentType)
if err != nil {
commonhttp.SystemError(w, fmt.Sprintf("Failed to upload file: %v", err))
return
}
// 获取文件URL
fileURL, err := h.storage.GetURL(objectKey, 0)
if err != nil {
commonhttp.SystemError(w, fmt.Sprintf("Failed to get file URL: %v", err))
return
}
// 返回结果
result := UploadResult{
ObjectKey: objectKey,
URL: fileURL,
Size: header.Size,
ContentType: contentType,
UploadTime: time.Now(),
}
commonhttp.SuccessWithMessage(w, "Upload successful", result)
}
// generateUniqueFilename 生成唯一文件名
func generateUniqueFilename(originalFilename string) string {
ext := filepath.Ext(originalFilename)
name := strings.TrimSuffix(originalFilename, ext)
timestamp := time.Now().UnixNano()
return fmt.Sprintf("%s_%d%s", name, timestamp, ext)
}
// ProxyHandler 文件代理查看处理器
type ProxyHandler struct {
storage Storage
}
// NewProxyHandler 创建文件代理查看处理器
func NewProxyHandler(storage Storage) *ProxyHandler {
return &ProxyHandler{
storage: storage,
}
}
// ServeHTTP 处理文件查看请求
// URL参数: key (对象键)
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
commonhttp.NotFound(w, "Method not allowed")
return
}
// 获取对象键
objectKey := r.URL.Query().Get("key")
if objectKey == "" {
commonhttp.BadRequest(w, "Missing parameter: key")
return
}
// 检查文件是否存在
ctx := r.Context()
exists, err := h.storage.Exists(ctx, objectKey)
if err != nil {
commonhttp.SystemError(w, fmt.Sprintf("Failed to check file existence: %v", err))
return
}
if !exists {
commonhttp.NotFound(w, "File not found")
return
}
// 获取文件内容
reader, err := h.storage.GetObject(ctx, objectKey)
if err != nil {
commonhttp.SystemError(w, fmt.Sprintf("Failed to get file: %v", err))
return
}
defer reader.Close()
// 设置响应头
ext := filepath.Ext(objectKey)
contentType := mime.TypeByExtension(ext)
if contentType == "" {
contentType = "application/octet-stream"
}
// 如果是图片设置适当的Content-Type
if strings.HasPrefix(contentType, "image/") {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=31536000") // 缓存1年
} else {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filepath.Base(objectKey)))
}
// 复制文件内容到响应
_, err = io.Copy(w, reader)
if err != nil {
commonhttp.SystemError(w, fmt.Sprintf("Failed to write response: %v", err))
return
}
}

144
storage/minio.go Normal file
View File

@@ -0,0 +1,144 @@
package storage
import (
"context"
"fmt"
"io"
"strings"
"github.com/go-common/config"
)
// MinIOStorage MinIO存储实现
type MinIOStorage struct {
config *config.MinIOConfig
// client 存储MinIO客户端实际使用时需要根据具体的MinIO SDK实现
// 这里使用interface{},实际使用时需要替换为具体的客户端类型
client interface{}
}
// NewMinIOStorage 创建MinIO存储实例
func NewMinIOStorage(cfg *config.MinIOConfig) (*MinIOStorage, error) {
if cfg == nil {
return nil, fmt.Errorf("MinIO config is nil")
}
storage := &MinIOStorage{
config: cfg,
}
// 初始化MinIO客户端
// 注意这里需要根据实际的MinIO SDK实现
// 例如使用MinIO Go SDK:
// client, err := minio.New(cfg.Endpoint, &minio.Options{
// Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""),
// Secure: cfg.UseSSL,
// })
// if err != nil {
// return nil, fmt.Errorf("failed to create MinIO client: %w", err)
// }
// storage.client = client
return storage, nil
}
// Upload 上传文件到MinIO
func (s *MinIOStorage) Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error {
// 实现MinIO上传逻辑
// 注意这里需要根据实际的MinIO SDK实现
// 示例使用MinIO Go SDK:
// ct := "application/octet-stream"
// if len(contentType) > 0 && contentType[0] != "" {
// ct = contentType[0]
// }
//
// _, err := s.client.PutObject(ctx, s.config.Bucket, objectKey, reader, -1, minio.PutObjectOptions{
// ContentType: ct,
// })
// if err != nil {
// return fmt.Errorf("failed to upload object: %w", err)
// }
// 当前实现返回错误提示需要实现具体的MinIO SDK
return fmt.Errorf("MinIO upload not implemented, please implement with actual MinIO SDK")
}
// GetURL 获取MinIO文件访问URL
func (s *MinIOStorage) GetURL(objectKey string, expires int64) (string, error) {
if s.config.Domain != "" {
// 使用自定义域名
if strings.HasSuffix(s.config.Domain, "/") {
return s.config.Domain + objectKey, nil
}
return s.config.Domain + "/" + objectKey, nil
}
// 使用MinIO默认域名
protocol := "http"
if s.config.UseSSL {
protocol = "https"
}
// 构建MinIO URL
// 格式: http://endpoint/bucket/objectKey
url := fmt.Sprintf("%s://%s/%s/%s", protocol, s.config.Endpoint, s.config.Bucket, objectKey)
// 如果设置了过期时间需要生成预签名URL
// 注意这里需要根据实际的MinIO SDK实现
// 示例使用MinIO Go SDK:
// if expires > 0 {
// expiry := time.Duration(expires) * time.Second
// presignedURL, err := s.client.PresignedGetObject(ctx, s.config.Bucket, objectKey, expiry, nil)
// if err != nil {
// return "", err
// }
// return presignedURL.String(), nil
// }
return url, nil
}
// Delete 删除MinIO文件
func (s *MinIOStorage) Delete(ctx context.Context, objectKey string) error {
// 实现MinIO删除逻辑
// 注意这里需要根据实际的MinIO SDK实现
// 示例使用MinIO Go SDK:
// err := s.client.RemoveObject(ctx, s.config.Bucket, objectKey, minio.RemoveObjectOptions{})
// if err != nil {
// return fmt.Errorf("failed to delete object: %w", err)
// }
return fmt.Errorf("MinIO delete not implemented, please implement with actual MinIO SDK")
}
// Exists 检查MinIO文件是否存在
func (s *MinIOStorage) Exists(ctx context.Context, objectKey string) (bool, error) {
// 实现MinIO存在性检查逻辑
// 注意这里需要根据实际的MinIO SDK实现
// 示例使用MinIO Go SDK:
// _, err := s.client.StatObject(ctx, s.config.Bucket, objectKey, minio.StatObjectOptions{})
// if err != nil {
// if minio.ToErrorResponse(err).Code == "NoSuchKey" {
// return false, nil
// }
// return false, fmt.Errorf("failed to check object existence: %w", err)
// }
// return true, nil
return false, fmt.Errorf("MinIO exists check not implemented, please implement with actual MinIO SDK")
}
// GetObject 获取MinIO文件内容
func (s *MinIOStorage) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, error) {
// 实现MinIO获取对象逻辑
// 注意这里需要根据实际的MinIO SDK实现
// 示例使用MinIO Go SDK:
// obj, err := s.client.GetObject(ctx, s.config.Bucket, objectKey, minio.GetObjectOptions{})
// if err != nil {
// return nil, fmt.Errorf("failed to get object: %w", err)
// }
// return obj, nil
return nil, fmt.Errorf("MinIO get object not implemented, please implement with actual MinIO SDK")
}

155
storage/oss.go Normal file
View File

@@ -0,0 +1,155 @@
package storage
import (
"context"
"fmt"
"io"
"strings"
"github.com/go-common/config"
)
// OSSStorage OSS存储实现
type OSSStorage struct {
config *config.OSSConfig
// client 存储OSS客户端实际使用时需要根据具体的OSS SDK实现
// 这里使用interface{},实际使用时需要替换为具体的客户端类型
client interface{}
}
// NewOSSStorage 创建OSS存储实例
func NewOSSStorage(cfg *config.OSSConfig) (*OSSStorage, error) {
if cfg == nil {
return nil, fmt.Errorf("OSS config is nil")
}
storage := &OSSStorage{
config: cfg,
}
// 初始化OSS客户端
// 注意这里需要根据实际的OSS SDK实现
// 例如使用阿里云OSS SDK:
// client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
// if err != nil {
// return nil, fmt.Errorf("failed to create OSS client: %w", err)
// }
// storage.client = client
return storage, nil
}
// Upload 上传文件到OSS
func (s *OSSStorage) Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error {
// 实现OSS上传逻辑
// 注意这里需要根据实际的OSS SDK实现
// 示例使用阿里云OSS SDK:
// bucket, err := s.client.Bucket(s.config.Bucket)
// if err != nil {
// return fmt.Errorf("failed to get bucket: %w", err)
// }
//
// options := []oss.Option{}
// if len(contentType) > 0 && contentType[0] != "" {
// options = append(options, oss.ContentType(contentType[0]))
// }
//
// err = bucket.PutObject(objectKey, reader, options...)
// if err != nil {
// return fmt.Errorf("failed to upload object: %w", err)
// }
// 当前实现返回错误提示需要实现具体的OSS SDK
return fmt.Errorf("OSS upload not implemented, please implement with actual OSS SDK")
}
// GetURL 获取OSS文件访问URL
func (s *OSSStorage) GetURL(objectKey string, expires int64) (string, error) {
if s.config.Domain != "" {
// 使用自定义域名
if strings.HasSuffix(s.config.Domain, "/") {
return s.config.Domain + objectKey, nil
}
return s.config.Domain + "/" + objectKey, nil
}
// 使用OSS默认域名
protocol := "http"
if s.config.UseSSL {
protocol = "https"
}
// 构建OSS URL
// 格式: https://bucket.endpoint/objectKey
url := fmt.Sprintf("%s://%s.%s/%s", protocol, s.config.Bucket, s.config.Endpoint, objectKey)
// 如果设置了过期时间需要生成签名URL
// 注意这里需要根据实际的OSS SDK实现
// 示例使用阿里云OSS SDK:
// if expires > 0 {
// bucket, err := s.client.Bucket(s.config.Bucket)
// if err != nil {
// return "", err
// }
// signedURL, err := bucket.SignURL(objectKey, oss.HTTPGet, expires)
// if err != nil {
// return "", err
// }
// return signedURL, nil
// }
return url, nil
}
// Delete 删除OSS文件
func (s *OSSStorage) Delete(ctx context.Context, objectKey string) error {
// 实现OSS删除逻辑
// 注意这里需要根据实际的OSS SDK实现
// 示例使用阿里云OSS SDK:
// bucket, err := s.client.Bucket(s.config.Bucket)
// if err != nil {
// return fmt.Errorf("failed to get bucket: %w", err)
// }
// err = bucket.DeleteObject(objectKey)
// if err != nil {
// return fmt.Errorf("failed to delete object: %w", err)
// }
return fmt.Errorf("OSS delete not implemented, please implement with actual OSS SDK")
}
// Exists 检查OSS文件是否存在
func (s *OSSStorage) Exists(ctx context.Context, objectKey string) (bool, error) {
// 实现OSS存在性检查逻辑
// 注意这里需要根据实际的OSS SDK实现
// 示例使用阿里云OSS SDK:
// bucket, err := s.client.Bucket(s.config.Bucket)
// if err != nil {
// return false, fmt.Errorf("failed to get bucket: %w", err)
// }
// exists, err := bucket.IsObjectExist(objectKey)
// if err != nil {
// return false, fmt.Errorf("failed to check object existence: %w", err)
// }
// return exists, nil
return false, fmt.Errorf("OSS exists check not implemented, please implement with actual OSS SDK")
}
// GetObject 获取OSS文件内容
func (s *OSSStorage) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, error) {
// 实现OSS获取对象逻辑
// 注意这里需要根据实际的OSS SDK实现
// 示例使用阿里云OSS SDK:
// bucket, err := s.client.Bucket(s.config.Bucket)
// if err != nil {
// return nil, fmt.Errorf("failed to get bucket: %w", err)
// }
// body, err := bucket.GetObject(objectKey)
// if err != nil {
// return nil, fmt.Errorf("failed to get object: %w", err)
// }
// return body, nil
return nil, fmt.Errorf("OSS get object not implemented, please implement with actual OSS SDK")
}

105
storage/storage.go Normal file
View File

@@ -0,0 +1,105 @@
package storage
import (
"context"
"fmt"
"io"
"time"
"github.com/go-common/config"
)
// Storage 存储接口
type Storage interface {
// Upload 上传文件
// ctx: 上下文
// objectKey: 对象键(文件路径)
// reader: 文件内容
// contentType: 文件类型(可选)
Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error
// GetURL 获取文件访问URL
// objectKey: 对象键
// expires: 过期时间0表示永久有效
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)
}
// StorageType 存储类型
type StorageType string
const (
StorageTypeOSS StorageType = "oss"
StorageTypeMinIO StorageType = "minio"
)
// NewStorage 创建存储实例
// storageType: 存储类型oss或minio
// cfg: 配置对象
func NewStorage(storageType StorageType, cfg *config.Config) (Storage, error) {
switch storageType {
case StorageTypeOSS:
ossConfig := cfg.GetOSS()
if ossConfig == nil {
return nil, fmt.Errorf("OSS config is nil")
}
return NewOSSStorage(ossConfig)
case StorageTypeMinIO:
minioConfig := cfg.GetMinIO()
if minioConfig == nil {
return nil, fmt.Errorf("MinIO config is nil")
}
return NewMinIOStorage(minioConfig)
default:
return nil, fmt.Errorf("unsupported storage type: %s", storageType)
}
}
// UploadResult 上传结果
type UploadResult struct {
// ObjectKey 对象键(文件路径)
ObjectKey string `json:"objectKey"`
// URL 文件访问URL
URL string `json:"url"`
// Size 文件大小(字节)
Size int64 `json:"size"`
// ContentType 文件类型
ContentType string `json:"contentType"`
// UploadTime 上传时间
UploadTime time.Time `json:"uploadTime"`
}
// GenerateObjectKey 生成对象键
// prefix: 前缀(如 "images/", "files/"
// filename: 文件名
func GenerateObjectKey(prefix, filename string) string {
if prefix == "" {
return filename
}
if prefix[len(prefix)-1] != '/' {
prefix += "/"
}
return prefix + filename
}
// GenerateObjectKeyWithDate 生成带日期的对象键
// prefix: 前缀
// filename: 文件名
// 格式: prefix/YYYY/MM/DD/filename
func GenerateObjectKeyWithDate(prefix, filename string) string {
now := time.Now()
datePath := fmt.Sprintf("%d/%02d/%02d", now.Year(), now.Month(), now.Day())
return GenerateObjectKey(prefix+"/"+datePath, filename)
}