调整工具类的方法,优化方法调用及增加迁移工具及其用法
This commit is contained in:
@@ -65,16 +65,27 @@ func main() {
|
||||
|
||||
// 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)
|
||||
configCORS := cfg.GetCORS()
|
||||
if configCORS != nil {
|
||||
fmt.Printf("Allowed Origins: %v\n", configCORS.AllowedOrigins)
|
||||
fmt.Printf("Allowed Methods: %v\n", configCORS.AllowedMethods)
|
||||
fmt.Printf("Max Age: %d\n", configCORS.MaxAge)
|
||||
}
|
||||
|
||||
// 使用CORS配置创建中间件
|
||||
var middlewareCORS *middleware.CORSConfig
|
||||
if configCORS != nil {
|
||||
middlewareCORS = middleware.NewCORSConfig(
|
||||
configCORS.AllowedOrigins,
|
||||
configCORS.AllowedMethods,
|
||||
configCORS.AllowedHeaders,
|
||||
configCORS.ExposedHeaders,
|
||||
configCORS.AllowCredentials,
|
||||
configCORS.MaxAge,
|
||||
)
|
||||
}
|
||||
chain := middleware.NewChain(
|
||||
middleware.CORS(corsConfig),
|
||||
middleware.CORS(middlewareCORS),
|
||||
)
|
||||
fmt.Printf("CORS middleware created: %v\n", chain != nil)
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.toowon.com/jimmy/go-common/datetime"
|
||||
commonhttp "git.toowon.com/jimmy/go-common/http"
|
||||
"git.toowon.com/jimmy/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模式)
|
||||
handler := chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := commonhttp.NewHandler(w, r)
|
||||
apiHandler(h)
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
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请求(使用Handler模式)
|
||||
func apiHandler(h *commonhttp.Handler) {
|
||||
// 从Handler获取时区
|
||||
timezone := h.GetTimezone()
|
||||
|
||||
// 使用时区进行时间处理
|
||||
now := datetime.Now(timezone)
|
||||
startOfDay := datetime.StartOfDay(now, timezone)
|
||||
endOfDay := datetime.EndOfDay(now, timezone)
|
||||
|
||||
// 返回响应
|
||||
h.Success(map[string]interface{}{
|
||||
"message": "Hello from API",
|
||||
"timezone": timezone,
|
||||
"currentTime": datetime.FormatDateTime(now),
|
||||
"startOfDay": datetime.FormatDateTime(startOfDay),
|
||||
"endOfDay": datetime.FormatDateTime(endOfDay),
|
||||
})
|
||||
}
|
||||
154
examples/middleware_full_example.go
Normal file
154
examples/middleware_full_example.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.toowon.com/jimmy/go-common/config"
|
||||
commonhttp "git.toowon.com/jimmy/go-common/http"
|
||||
"git.toowon.com/jimmy/go-common/logger"
|
||||
"git.toowon.com/jimmy/go-common/middleware"
|
||||
)
|
||||
|
||||
// 示例:完整的中间件配置
|
||||
// 包括:Recovery、Logging、RateLimit、CORS、Timezone
|
||||
func main() {
|
||||
// 1. 配置logger(异步模式,输出到文件和stdout)
|
||||
loggerConfig := &logger.LoggerConfig{
|
||||
Level: "info",
|
||||
Output: "both", // 同时输出到stdout和文件
|
||||
FilePath: "./logs/app.log",
|
||||
Async: true, // 异步模式
|
||||
BufferSize: 1000, // 缓冲区大小
|
||||
Prefix: "[API]", // 日志前缀
|
||||
}
|
||||
myLogger, err := logger.NewLogger(loggerConfig)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create logger:", err)
|
||||
}
|
||||
defer myLogger.Close() // 确保程序退出时关闭logger
|
||||
|
||||
// 2. 配置CORS
|
||||
corsConfig := &middleware.CORSConfig{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Timezone"},
|
||||
ExposedHeaders: []string{"X-Total-Count"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 3600, // 1小时
|
||||
}
|
||||
|
||||
// 3. 配置日志中间件
|
||||
loggingConfig := &middleware.LoggingConfig{
|
||||
Logger: myLogger,
|
||||
SkipPaths: []string{"/health", "/metrics"}, // 跳过健康检查和监控接口
|
||||
}
|
||||
|
||||
// 4. 配置Recovery中间件
|
||||
recoveryConfig := &middleware.RecoveryConfig{
|
||||
Logger: myLogger,
|
||||
EnableStackTrace: true, // 启用堆栈跟踪
|
||||
CustomHandler: func(w http.ResponseWriter, r *http.Request, err interface{}) {
|
||||
// 使用统一的JSON响应格式
|
||||
h := commonhttp.NewHandler(w, r)
|
||||
h.SystemError("服务器内部错误,请稍后重试")
|
||||
},
|
||||
}
|
||||
|
||||
// 5. 配置限流中间件(100请求/分钟)
|
||||
rateLimiter := middleware.NewTokenBucketLimiter(100, time.Minute)
|
||||
rateLimitConfig := &middleware.RateLimitConfig{
|
||||
Limiter: rateLimiter,
|
||||
OnRateLimitExceeded: func(w http.ResponseWriter, r *http.Request, key string) {
|
||||
// 记录限流事件
|
||||
myLogger.Warnf(map[string]interface{}{
|
||||
"key": key,
|
||||
"path": r.URL.Path,
|
||||
"ip": r.RemoteAddr,
|
||||
}, "Rate limit exceeded")
|
||||
},
|
||||
}
|
||||
|
||||
// 6. 创建中间件链(顺序很重要!)
|
||||
// 顺序:Recovery -> Logging -> RateLimit -> CORS -> Timezone
|
||||
chain := middleware.NewChain(
|
||||
middleware.Recovery(recoveryConfig), // 1. 最外层:捕获panic
|
||||
middleware.Logging(loggingConfig), // 2. 日志记录
|
||||
middleware.RateLimit(rateLimitConfig), // 3. 限流保护
|
||||
middleware.CORS(corsConfig), // 4. CORS处理
|
||||
middleware.Timezone, // 5. 时区处理
|
||||
)
|
||||
|
||||
// 7. 定义路由
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// API路由(应用所有中间件)
|
||||
mux.Handle("/api/hello", chain.ThenFunc(handleHello))
|
||||
mux.Handle("/api/panic", chain.ThenFunc(handlePanic)) // 测试panic恢复
|
||||
mux.Handle("/api/users", chain.ThenFunc(handleUsers))
|
||||
|
||||
// 健康检查(不应用中间件链,直接处理)
|
||||
mux.HandleFunc("/health", handleHealth)
|
||||
mux.HandleFunc("/metrics", handleMetrics)
|
||||
|
||||
// 8. 启动服务器
|
||||
addr := ":8080"
|
||||
log.Printf("Server starting on %s", addr)
|
||||
log.Printf("Try: http://localhost%s/api/hello", addr)
|
||||
log.Printf("Health: http://localhost%s/health", addr)
|
||||
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
log.Fatal("Server failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleHello 示例处理器:返回问候信息
|
||||
func handleHello(w http.ResponseWriter, r *http.Request) {
|
||||
h := commonhttp.NewHandler(w, r)
|
||||
|
||||
// 从Handler获取时区
|
||||
timezone := h.GetTimezone()
|
||||
|
||||
h.Success(map[string]interface{}{
|
||||
"message": "Hello, World!",
|
||||
"timezone": timezone,
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
})
|
||||
}
|
||||
|
||||
// handlePanic 示例处理器:测试panic恢复
|
||||
func handlePanic(w http.ResponseWriter, r *http.Request) {
|
||||
// 故意触发panic,测试Recovery中间件
|
||||
panic("This is a test panic!")
|
||||
}
|
||||
|
||||
// handleUsers 示例处理器:返回用户列表
|
||||
func handleUsers(w http.ResponseWriter, r *http.Request) {
|
||||
h := commonhttp.NewHandler(w, r)
|
||||
|
||||
// 模拟用户数据
|
||||
users := []map[string]interface{}{
|
||||
{"id": 1, "name": "Alice"},
|
||||
{"id": 2, "name": "Bob"},
|
||||
{"id": 3, "name": "Charlie"},
|
||||
}
|
||||
|
||||
h.Success(users)
|
||||
}
|
||||
|
||||
// handleHealth 健康检查处理器(不应用中间件)
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
// handleMetrics 监控指标处理器(不应用中间件)
|
||||
func handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("metrics: ok"))
|
||||
}
|
||||
|
||||
89
examples/middleware_ratelimit_example.go
Normal file
89
examples/middleware_ratelimit_example.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
commonhttp "git.toowon.com/jimmy/go-common/http"
|
||||
"git.toowon.com/jimmy/go-common/middleware"
|
||||
)
|
||||
|
||||
// 示例:限流中间件的使用
|
||||
// 展示不同的限流策略
|
||||
func main() {
|
||||
// 策略1:按IP限流(10请求/分钟)
|
||||
ipLimitChain := middleware.NewChain(
|
||||
middleware.RateLimitByIP(10, time.Minute),
|
||||
)
|
||||
|
||||
// 策略2:按用户ID限流(100请求/分钟)
|
||||
limiter := middleware.NewTokenBucketLimiter(100, time.Minute)
|
||||
userLimitConfig := &middleware.RateLimitConfig{
|
||||
Limiter: limiter,
|
||||
KeyFunc: func(r *http.Request) string {
|
||||
// 从请求头获取用户ID
|
||||
userID := r.Header.Get("X-User-ID")
|
||||
if userID != "" {
|
||||
return "user:" + userID
|
||||
}
|
||||
// 没有用户ID则使用IP
|
||||
return "ip:" + r.RemoteAddr
|
||||
},
|
||||
OnRateLimitExceeded: func(w http.ResponseWriter, r *http.Request, key string) {
|
||||
log.Printf("Rate limit exceeded for key: %s, path: %s", key, r.URL.Path)
|
||||
},
|
||||
}
|
||||
userLimitChain := middleware.NewChain(
|
||||
middleware.RateLimit(userLimitConfig),
|
||||
)
|
||||
|
||||
// 路由1:按IP限流的API(严格限制)
|
||||
http.Handle("/api/public", ipLimitChain.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := commonhttp.NewHandler(w, r)
|
||||
h.Success(map[string]interface{}{
|
||||
"message": "Public API - IP rate limited (10/min)",
|
||||
"tip": "Try refreshing quickly to see rate limiting",
|
||||
})
|
||||
}))
|
||||
|
||||
// 路由2:按用户ID限流的API(宽松限制)
|
||||
http.Handle("/api/private", userLimitChain.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := commonhttp.NewHandler(w, r)
|
||||
userID := r.Header.Get("X-User-ID")
|
||||
if userID == "" {
|
||||
h.Error(401, "Missing X-User-ID header")
|
||||
return
|
||||
}
|
||||
h.Success(map[string]interface{}{
|
||||
"message": "Private API - User rate limited (100/min)",
|
||||
"user_id": userID,
|
||||
})
|
||||
}))
|
||||
|
||||
// 路由3:无限流的API(用于测试对比)
|
||||
http.HandleFunc("/api/unlimited", func(w http.ResponseWriter, r *http.Request) {
|
||||
h := commonhttp.NewHandler(w, r)
|
||||
h.Success(map[string]interface{}{
|
||||
"message": "Unlimited API - No rate limiting",
|
||||
})
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
addr := ":8080"
|
||||
log.Printf("Server starting on %s", addr)
|
||||
log.Println("Test endpoints:")
|
||||
log.Printf(" - IP limited (10/min): http://localhost%s/api/public", addr)
|
||||
log.Printf(" - User limited (100/min): http://localhost%s/api/private (add X-User-ID header)", addr)
|
||||
log.Printf(" - No limit: http://localhost%s/api/unlimited", addr)
|
||||
log.Println("\nTest with curl:")
|
||||
log.Printf(" curl http://localhost%s/api/public", addr)
|
||||
log.Printf(" curl -H 'X-User-ID: user123' http://localhost%s/api/private", addr)
|
||||
log.Println("\nResponse headers:")
|
||||
log.Println(" X-RateLimit-Limit: Total allowed requests")
|
||||
log.Println(" X-RateLimit-Remaining: Remaining requests")
|
||||
log.Println(" X-RateLimit-Reset: Reset timestamp")
|
||||
|
||||
log.Fatal(http.ListenAndServe(addr, nil))
|
||||
}
|
||||
|
||||
42
examples/middleware_simple_example.go
Normal file
42
examples/middleware_simple_example.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
commonhttp "git.toowon.com/jimmy/go-common/http"
|
||||
"git.toowon.com/jimmy/go-common/middleware"
|
||||
)
|
||||
|
||||
// 示例:简单的中间件配置
|
||||
// 包括:Recovery、Logging、CORS、Timezone
|
||||
func main() {
|
||||
// 创建简单的中间件链(使用默认配置)
|
||||
chain := middleware.NewChain(
|
||||
middleware.Recovery(nil), // Panic恢复(使用默认logger)
|
||||
middleware.Logging(nil), // 请求日志(使用默认logger)
|
||||
middleware.CORS(nil), // CORS(允许所有源)
|
||||
middleware.Timezone, // 时区处理(默认AsiaShanghai)
|
||||
)
|
||||
|
||||
// 定义API处理器
|
||||
http.Handle("/api/hello", chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := commonhttp.NewHandler(w, r)
|
||||
|
||||
// 获取时区
|
||||
timezone := h.GetTimezone()
|
||||
|
||||
// 返回响应
|
||||
h.Success(map[string]interface{}{
|
||||
"message": "Hello, World!",
|
||||
"timezone": timezone,
|
||||
})
|
||||
}))
|
||||
|
||||
// 启动服务器
|
||||
addr := ":8080"
|
||||
log.Printf("Server starting on %s", addr)
|
||||
log.Printf("Try: http://localhost%s/api/hello", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, nil))
|
||||
}
|
||||
|
||||
134
examples/migrations/README.md
Normal file
134
examples/migrations/README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 数据库迁移示例
|
||||
|
||||
这个目录包含了数据库迁移的示例SQL文件。
|
||||
|
||||
## 文件说明
|
||||
|
||||
```
|
||||
examples/migrations/
|
||||
├── migrations/ # 迁移文件目录
|
||||
│ ├── 20240101000001_create_users_table.sql # 迁移SQL
|
||||
│ └── 20240101000001_create_users_table.down.sql # 回滚SQL
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 在你的项目中使用
|
||||
|
||||
#### 1. 创建迁移工具
|
||||
|
||||
在项目根目录创建 `migrate.go`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"git.toowon.com/jimmy/go-common/migration"
|
||||
)
|
||||
|
||||
func main() {
|
||||
command := "up"
|
||||
if len(os.Args) > 1 {
|
||||
command = os.Args[1]
|
||||
}
|
||||
|
||||
err := migration.RunMigrationsFromConfigWithCommand("config.json", "migrations", command)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 创建迁移文件
|
||||
|
||||
```bash
|
||||
# 创建目录
|
||||
mkdir -p migrations
|
||||
|
||||
# 创建迁移文件(使用时间戳作为版本号)
|
||||
vim migrations/20240101000001_create_users.sql
|
||||
```
|
||||
|
||||
#### 3. 编写SQL
|
||||
|
||||
```sql
|
||||
-- migrations/20240101000001_create_users.sql
|
||||
CREATE TABLE users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
#### 4. 执行迁移
|
||||
|
||||
```bash
|
||||
# 执行迁移
|
||||
go run migrate.go up
|
||||
|
||||
# 查看状态
|
||||
go run migrate.go status
|
||||
|
||||
# 回滚
|
||||
go run migrate.go down
|
||||
```
|
||||
|
||||
### 更简单:在应用启动时自动执行
|
||||
|
||||
在你的 `main.go` 中:
|
||||
|
||||
```go
|
||||
import "git.toowon.com/jimmy/go-common/migration"
|
||||
|
||||
func main() {
|
||||
// 一行代码,启动时自动迁移
|
||||
migration.RunMigrationsFromConfig("config.json", "migrations")
|
||||
|
||||
// 继续启动应用
|
||||
startServer()
|
||||
}
|
||||
```
|
||||
|
||||
## 配置方式
|
||||
|
||||
### 方式1:配置文件
|
||||
|
||||
`config.json`:
|
||||
```json
|
||||
{
|
||||
"database": {
|
||||
"type": "mysql",
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
"password": "password",
|
||||
"database": "mydb"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方式2:环境变量(Docker友好)
|
||||
|
||||
```bash
|
||||
DATABASE_URL="mysql://root:password@localhost:3306/mydb" go run migrate.go up
|
||||
```
|
||||
|
||||
**Docker 中**:
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
DATABASE_URL: mysql://root:password@db:3306/mydb
|
||||
command: sh -c "go run migrate.go up && ./app"
|
||||
```
|
||||
|
||||
**注意**:Docker 中使用服务名(`db`),不是 `localhost`
|
||||
|
||||
## 更多信息
|
||||
|
||||
- [数据库迁移完整指南](../../MIGRATION.md) ⭐
|
||||
- [详细功能文档](../../docs/migration.md)
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Rollback: Drop users table
|
||||
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Create users table
|
||||
-- Created at: 2024-01-01 00:00:01
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -36,8 +36,20 @@ func main() {
|
||||
proxyHandler := storage.NewProxyHandler(ossStorage)
|
||||
|
||||
// 创建中间件链
|
||||
var corsConfig *middleware.CORSConfig
|
||||
if cfg.GetCORS() != nil {
|
||||
c := cfg.GetCORS()
|
||||
corsConfig = middleware.NewCORSConfig(
|
||||
c.AllowedOrigins,
|
||||
c.AllowedMethods,
|
||||
c.AllowedHeaders,
|
||||
c.ExposedHeaders,
|
||||
c.AllowCredentials,
|
||||
c.MaxAge,
|
||||
)
|
||||
}
|
||||
chain := middleware.NewChain(
|
||||
middleware.CORS(cfg.GetCORS()),
|
||||
middleware.CORS(corsConfig),
|
||||
middleware.Timezone,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user