初始版本,工具基础类
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.cursor
|
||||
167
README.md
Normal file
167
README.md
Normal 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
466
config/config.go
Normal 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
74
config/example.json
Normal 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
358
datetime/datetime.go
Normal 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
100
docs/README.md
Normal 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
500
docs/config.md
Normal 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
461
docs/datetime.md
Normal 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))
|
||||
}
|
||||
```
|
||||
|
||||
### 示例2:UTC转换(数据库存储场景)
|
||||
|
||||
```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
332
docs/email.md
Normal 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端口:通常使用TLS(STARTTLS)
|
||||
- 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
423
docs/http.md
Normal 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(页码,最小1),pageSize(每页大小,最小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
438
docs/middleware.md
Normal 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)
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 示例1:CORS + 时区中间件
|
||||
|
||||
```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
242
docs/migration.md
Normal 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
370
docs/sms.md
Normal 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
496
docs/storage.md
Normal 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
306
email/email.go
Normal 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
124
examples/config_example.go
Normal 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)")
|
||||
}
|
||||
}
|
||||
56
examples/datetime_example.go
Normal file
56
examples/datetime_example.go
Normal 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)
|
||||
}
|
||||
|
||||
74
examples/datetime_utc_example.go
Normal file
74
examples/datetime_utc_example.go
Normal 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
121
examples/email_example.go
Normal 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
101
examples/http_example.go
Normal 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))
|
||||
}
|
||||
64
examples/middleware_example.go
Normal file
64
examples/middleware_example.go
Normal 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),
|
||||
})
|
||||
}
|
||||
|
||||
58
examples/migration_example.go
Normal file
58
examples/migration_example.go
Normal 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
102
examples/sms_example.go
Normal 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")
|
||||
}
|
||||
|
||||
71
examples/storage_example.go
Normal file
71
examples/storage_example.go
Normal 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
14
go.mod
Normal 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
12
go.sum
Normal 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
207
http/request.go
Normal 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
138
http/response.go
Normal 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
36
middleware/chain.go
Normal 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
207
middleware/cors.go
Normal 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
82
middleware/timezone.go
Normal 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
373
migration/migration.go
Normal 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
290
sms/sms.go
Normal 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
212
storage/handler.go
Normal 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
144
storage/minio.go
Normal 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
155
storage/oss.go
Normal 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
105
storage/storage.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user