diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 0000000..8e50433
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,583 @@
+# 数据库迁移工具 - 完整指南
+
+## 📌 核心特点
+
+- ✅ **独立工具,零耦合** - 与应用代码完全分离
+- ✅ **生产就绪** - 编译成二进制,无需Go环境
+- ✅ **灵活配置** - 支持命令行参数、环境变量、配置文件
+- ✅ **Docker友好** - 挂载配置,修改无需重启容器
+
+---
+
+## 🚀 快速开始(3步)
+
+> **黑盒模式**:迁移工具内部调用 `migration.RunMigrationsFromConfig()` 方法,自动处理配置加载、数据库连接、迁移执行等所有细节。你只需要提供配置文件和SQL文件即可。
+
+### 1. 复制迁移工具模板
+
+```bash
+mkdir -p cmd/migrate
+cp /path/to/go-common/templates/migrate/main.go cmd/migrate/
+```
+
+### 2. 创建迁移文件
+
+```bash
+mkdir -p migrations
+```
+
+创建 `migrations/20240101000001_create_users.sql`:
+
+```sql
+CREATE TABLE users (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ username VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+```
+
+### 3. 编译和使用
+
+```bash
+# 编译(生产环境推荐)
+go build -o bin/migrate cmd/migrate/main.go
+
+# 使用
+./bin/migrate up # 使用默认配置
+./bin/migrate up -config /path/to/config.json # 指定配置
+./bin/migrate status # 查看状态
+./bin/migrate -help # 查看帮助
+```
+
+---
+
+## 💻 本地使用
+
+### 开发环境
+
+```bash
+# 直接运行(需要Go环境)
+go run cmd/migrate/main.go up
+go run cmd/migrate/main.go up -config dev.json
+
+# 编译后运行(推荐)
+go build -o bin/migrate cmd/migrate/main.go
+./bin/migrate up
+./bin/migrate up -config config.prod.json
+./bin/migrate up -c prod.json -d db/migrations
+```
+
+### 命令说明
+
+```bash
+./bin/migrate -help
+
+# 输出:
+# 用法: migrate [命令] [选项]
+#
+# 命令:
+# up 执行所有待执行的迁移(默认)
+# down 回滚最后一个迁移
+# status 查看迁移状态
+#
+# 选项:
+# -config, -c 配置文件路径(默认: config.json)
+# -dir, -d 迁移文件目录(默认: migrations)
+# -help, -h 显示帮助信息
+```
+
+### 配置方式
+
+#### 方式1:配置文件(推荐开发环境)
+
+`config.json`:
+```json
+{
+ "database": {
+ "type": "mysql",
+ "host": "localhost",
+ "port": 3306,
+ "user": "root",
+ "password": "password",
+ "database": "mydb"
+ }
+}
+```
+
+#### 方式2:环境变量(推荐生产环境)
+
+```bash
+# 使用 DATABASE_URL(最简单)
+export DATABASE_URL="mysql://root:password@localhost:3306/mydb"
+./bin/migrate up
+
+# 覆盖配置路径
+export CONFIG_FILE="/etc/app/config.json"
+export MIGRATIONS_DIR="/opt/app/migrations"
+./bin/migrate up
+```
+
+#### 配置优先级(从高到低)
+
+1. 命令行参数 `-config` 和 `-dir`(最高)
+2. 环境变量 `CONFIG_FILE` 和 `MIGRATIONS_DIR`
+3. 环境变量 `DATABASE_URL`
+4. 默认值 `config.json` 和 `migrations`
+
+---
+
+## 🐳 Docker 使用
+
+### 方式1:挂载配置文件(推荐)⭐
+
+**优势**:修改配置无需重启容器!
+
+#### docker-compose.yml
+
+```yaml
+version: '3.8'
+
+services:
+ app:
+ build: .
+ ports:
+ - "8080:8080"
+ volumes:
+ # 挂载配置文件(推荐:修改配置无需重启容器)
+ - ./config.json:/app/config.json:ro
+ # 启动时先执行迁移,再启动应用
+ command: sh -c "./migrate up && ./server"
+```
+
+#### 使用方式
+
+```bash
+# 1. 启动服务
+docker-compose up -d
+
+# 2. 修改配置文件
+vim config.json
+
+# 3. 手动执行迁移(无需重启容器!)
+docker-compose exec app ./migrate up
+
+# 4. 查看状态
+docker-compose exec app ./migrate status
+```
+
+### 方式2:指定配置文件路径
+
+适用于多环境部署:
+
+```yaml
+services:
+ app:
+ build: .
+ volumes:
+ # 挂载不同环境的配置文件
+ - ./config.prod.json:/app/config.json:ro
+ command: sh -c "./migrate up -config /app/config.json && ./server"
+```
+
+```bash
+# 手动切换环境(修改挂载的配置文件后)
+docker-compose exec app ./migrate up
+```
+
+### 方式3:使用环境变量
+
+无需配置文件(适用于简单场景):
+
+```yaml
+services:
+ app:
+ build: .
+ environment:
+ DATABASE_URL: mysql://root:password@db:3306/mydb
+ command: sh -c "./migrate up && ./server"
+
+# 注意:DATABASE_URL 的值应该指向你的数据库服务
+# 例如:mysql://user:pass@your-db-host:3306/dbname
+```
+
+### Dockerfile
+
+```dockerfile
+FROM golang:1.21 as builder
+WORKDIR /app
+COPY go.mod go.sum ./
+RUN go mod download
+COPY . .
+
+# 编译应用和迁移工具
+RUN go build -o bin/server cmd/server/main.go
+RUN go build -o bin/migrate cmd/migrate/main.go
+
+FROM debian:bookworm-slim
+WORKDIR /app
+RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
+
+# 复制二进制文件和迁移文件
+COPY --from=builder /app/bin/migrate .
+COPY --from=builder /app/bin/server .
+COPY --from=builder /app/migrations ./migrations
+
+EXPOSE 8080
+
+# 启动:先迁移,再启动应用
+CMD ["sh", "-c", "./migrate up && ./server"]
+
+# 注意:配置文件通过 volumes 挂载,不打包进镜像
+```
+
+---
+
+## ☸️ Kubernetes 部署
+
+### 使用 Job 执行迁移
+
+```yaml
+# k8s-job-migrate.yaml
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: db-migrate
+spec:
+ template:
+ spec:
+ containers:
+ - name: migrate
+ image: myapp:latest
+ command: ["./migrate", "up", "-config", "/etc/config/database.json"]
+ volumeMounts:
+ - name: config
+ mountPath: /etc/config
+ readOnly: true
+ volumes:
+ - name: config
+ configMap:
+ name: app-config
+ restartPolicy: OnFailure
+```
+
+### 部署流程
+
+```bash
+# 1. 创建 ConfigMap
+kubectl create configmap app-config --from-file=config.json
+
+# 2. 执行迁移
+kubectl apply -f k8s-job-migrate.yaml
+kubectl wait --for=condition=complete job/db-migrate
+
+# 3. 部署应用
+kubectl apply -f k8s-deployment.yaml
+```
+
+---
+
+## 🔧 CI/CD 集成
+
+### GitLab CI
+
+```yaml
+# .gitlab-ci.yml
+stages:
+ - build
+ - migrate
+ - deploy
+
+build:
+ stage: build
+ script:
+ - go build -o bin/migrate cmd/migrate/main.go
+ - go build -o bin/server cmd/server/main.go
+ artifacts:
+ paths:
+ - bin/
+
+migrate:
+ stage: migrate
+ script:
+ - ./bin/migrate up -config config.prod.json
+ environment:
+ name: production
+
+deploy:
+ stage: deploy
+ script:
+ - ./bin/server
+```
+
+### GitHub Actions
+
+```yaml
+# .github/workflows/deploy.yml
+name: Deploy
+
+on:
+ push:
+ branches: [main]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: '1.21'
+
+ - name: Build
+ run: |
+ go build -o bin/migrate cmd/migrate/main.go
+ go build -o bin/server cmd/server/main.go
+
+ - name: Run Migrations
+ env:
+ DATABASE_URL: ${{ secrets.DATABASE_URL }}
+ run: ./bin/migrate up
+
+ - name: Deploy
+ run: ./bin/server
+```
+
+---
+
+## 📁 迁移文件管理
+
+### 文件命名规则
+
+```
+migrations/
+├── 20240101000001_create_users.sql # Up 迁移
+├── 20240101000001_create_users.down.sql # Down 回滚(可选)
+├── 20240102000001_add_posts.sql
+└── 20240102000001_add_posts.down.sql
+```
+
+格式:`{时间戳}_{描述}.sql`
+
+### 创建迁移文件
+
+```bash
+# 获取时间戳
+date +%Y%m%d%H%M%S
+# 输出:20240101120000
+
+# 创建迁移文件
+vim migrations/20240101120000_create_posts.sql
+```
+
+### 迁移文件示例
+
+**Up 文件**:
+```sql
+-- migrations/20240101000001_create_users.sql
+CREATE TABLE users (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ username VARCHAR(255) NOT NULL UNIQUE,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX idx_users_email ON users(email);
+```
+
+**Down 文件**(可选):
+```sql
+-- migrations/20240101000001_create_users.down.sql
+DROP INDEX idx_users_email ON users;
+DROP TABLE IF EXISTS users;
+```
+
+### 兼容性建议
+
+使用条件语句确保迁移可重复执行:
+
+```sql
+-- 创建表
+CREATE TABLE IF NOT EXISTS users (...);
+
+-- 添加列
+ALTER TABLE posts ADD COLUMN IF NOT EXISTS author_id BIGINT;
+
+-- 创建索引
+CREATE INDEX IF NOT EXISTS idx_posts_author ON posts(author_id);
+```
+
+---
+
+## 🔍 常见问题
+
+### Q: 为什么不直接在应用代码中调用?
+
+**A**: **耦合度太高!** 独立工具的优势:
+- ✅ 应用和迁移完全解耦
+- ✅ 可以独立部署和执行
+- ✅ 更灵活的部署策略
+- ✅ 符合单一职责原则
+
+### Q: 生产环境没有Go怎么办?
+
+**A**: **编译成二进制文件**!
+
+```bash
+# 本地或CI中编译
+go build -o bin/migrate cmd/migrate/main.go
+
+# 部署二进制文件(不需要Go环境)
+scp bin/migrate server:/app/
+ssh server "/app/migrate up"
+```
+
+### Q: Docker中修改配置需要重启吗?
+
+**A**: **不需要!** 使用挂载方式:
+
+```yaml
+volumes:
+ - ./config.json:/app/config.json:ro
+```
+
+修改后直接执行:
+```bash
+docker-compose exec app ./migrate up
+```
+
+### Q: 如何指定不同的配置文件?
+
+**A**: 使用命令行参数:
+
+```bash
+# 开发环境
+./migrate up -config config.dev.json
+
+# 测试环境
+./migrate up -config config.test.json
+
+# 生产环境
+./migrate up -config /etc/app/config.prod.json
+```
+
+### Q: 多个实例同时启动会有问题吗?
+
+**A**: 不会。数据库会保证只有一个实例能执行迁移(通过版本号主键)。
+
+### Q: Docker连不上数据库?
+
+**A**: 注意主机名:
+- ❌ `localhost`(容器内无法访问宿主机)
+- ✅ `db`(docker-compose 服务名)
+- ✅ `host.docker.internal`(Mac/Windows 访问宿主机)
+
+---
+
+## 💡 最佳实践
+
+### 1. 开发环境
+
+- 使用 `go run` 快速迭代
+- 使用配置文件管理不同环境
+
+```bash
+go run cmd/migrate/main.go up -config config.dev.json
+```
+
+### 2. 测试环境
+
+- 编译后部署,模拟生产环境
+- 使用独立的数据库
+
+```bash
+go build -o bin/migrate cmd/migrate/main.go
+./bin/migrate up -config config.test.json
+```
+
+### 3. 生产环境
+
+- 编译后部署,先执行迁移再启动应用
+- 使用环境变量管理敏感信息
+
+```bash
+go build -o bin/migrate cmd/migrate/main.go
+DATABASE_URL="mysql://..." ./bin/migrate up
+./bin/server
+```
+
+### 4. Docker 部署
+
+- 多阶段构建,只包含二进制文件
+- 挂载配置文件,灵活修改
+
+```dockerfile
+FROM golang:1.21 as builder
+RUN go build -o bin/migrate cmd/migrate/main.go
+
+FROM debian:bookworm-slim
+COPY --from=builder /app/bin/migrate .
+CMD ["sh", "-c", "./migrate up && ./server"]
+```
+
+### 5. CI/CD
+
+- 在构建阶段编译
+- 部署前执行迁移
+
+```yaml
+- build: go build -o bin/migrate cmd/migrate/main.go
+- migrate: ./bin/migrate up
+- deploy: ./bin/server
+```
+
+---
+
+## 📊 推荐的项目结构
+
+```
+your-project/
+├── cmd/
+│ ├── migrate/
+│ │ └── main.go # 迁移工具(独立)
+│ └── server/
+│ └── main.go # 应用主程序
+├── migrations/ # 迁移SQL文件
+│ ├── 20240101000001_create_users.sql
+│ └── 20240101000001_create_users.down.sql
+├── config.json # 配置文件
+├── Dockerfile
+├── docker-compose.yml
+├── Makefile # 常用命令
+└── go.mod
+```
+
+---
+
+## 📚 更多资源
+
+- [模板文件](./templates/) - 可直接复制的模板
+- [完整文档](./docs/migration.md) - 详细功能文档
+- [配置文档](./docs/config.md) - 配置说明
+
+---
+
+## 🎯 总结
+
+使用 GoCommon 的迁移工具,你可以:
+
+1. ✅ 复制一个模板文件到 `cmd/migrate/main.go`
+2. ✅ 创建 SQL 迁移文件到 `migrations/`
+3. ✅ 编译:`go build -o bin/migrate cmd/migrate/main.go`
+4. ✅ 使用:`./bin/migrate up`
+
+**核心优势**:
+- 独立工具,零耦合
+- 生产就绪,无需Go环境
+- 灵活配置,支持多环境
+- Docker友好,修改配置无需重启
+
+**开箱即用,灵活强大!** 🎉
+
diff --git a/QUICKSTART.md b/QUICKSTART.md
new file mode 100644
index 0000000..c4d0b78
--- /dev/null
+++ b/QUICKSTART.md
@@ -0,0 +1,310 @@
+# 快速开始指南
+
+5分钟快速上手 GoCommon 工具库。
+
+## 1. 安装
+
+```bash
+# 配置私有仓库
+go env -w GOPRIVATE=git.toowon.com
+
+# 安装最新版本
+go get git.toowon.com/jimmy/go-common@latest
+```
+
+## 2. 创建配置文件
+
+创建 `config.json`:
+
+```json
+{
+ "database": {
+ "type": "mysql",
+ "host": "localhost",
+ "port": 3306,
+ "user": "root",
+ "password": "password",
+ "database": "mydb"
+ },
+ "redis": {
+ "host": "localhost",
+ "port": 6379
+ },
+ "logger": {
+ "level": "info",
+ "output": "stdout",
+ "async": true
+ }
+}
+```
+
+## 3. 创建主程序
+
+创建 `main.go`:
+
+```go
+package main
+
+import (
+ "net/http"
+ "time"
+
+ "git.toowon.com/jimmy/go-common/factory"
+ "git.toowon.com/jimmy/go-common/middleware"
+ commonhttp "git.toowon.com/jimmy/go-common/http"
+)
+
+func main() {
+ // 从配置文件创建工厂(黑盒模式)
+ fac, err := factory.NewFactoryFromFile("./config.json")
+ if err != nil {
+ panic(err)
+ }
+
+ // 获取logger
+ logger, _ := fac.GetLogger()
+ defer logger.Close()
+
+ // 配置中间件链
+ chain := middleware.NewChain(
+ middleware.Recovery(&middleware.RecoveryConfig{Logger: logger}),
+ middleware.Logging(&middleware.LoggingConfig{Logger: logger}),
+ middleware.RateLimitByIP(100, time.Minute),
+ middleware.CORS(nil),
+ middleware.Timezone,
+ )
+
+ // 注册路由
+ http.Handle("/api/hello", chain.ThenFunc(handleHello))
+ http.Handle("/api/users", chain.ThenFunc(handleUsers))
+
+ // 启动服务
+ logger.Info("Server started on :8080")
+ http.ListenAndServe(":8080", nil)
+}
+
+// API处理器 - 问候接口
+func handleHello(w http.ResponseWriter, r *http.Request) {
+ h := commonhttp.NewHandler(w, r)
+
+ h.Success(map[string]interface{}{
+ "message": "Hello, World!",
+ "timezone": h.GetTimezone(),
+ })
+}
+
+// API处理器 - 用户列表(带分页)
+func handleUsers(w http.ResponseWriter, r *http.Request) {
+ h := commonhttp.NewHandler(w, r)
+
+ // 解析分页参数
+ pagination := h.ParsePaginationRequest()
+ page := pagination.GetPage()
+ size := pagination.GetSize()
+
+ // 模拟数据
+ users := []map[string]interface{}{
+ {"id": 1, "name": "Alice"},
+ {"id": 2, "name": "Bob"},
+ }
+ total := int64(100)
+
+ // 返回分页数据
+ h.SuccessPage(users, total, page, size)
+}
+```
+
+## 4. 运行
+
+```bash
+go run main.go
+```
+
+## 5. 测试
+
+```bash
+# 测试问候接口
+curl http://localhost:8080/api/hello
+
+# 测试分页接口
+curl "http://localhost:8080/api/users?page=1&page_size=10"
+
+# 测试时区
+curl -H "X-Timezone: America/New_York" http://localhost:8080/api/hello
+
+# 测试限流(快速请求多次)
+for i in {1..150}; do curl http://localhost:8080/api/hello; done
+```
+
+## 6. 常见使用场景
+
+### 场景1:使用数据库
+
+```go
+// 获取数据库连接
+db, _ := fac.GetDatabase()
+
+// 使用GORM查询
+var users []User
+db.Find(&users)
+```
+
+### 场景2:使用Redis
+
+```go
+ctx := context.Background()
+
+// 设置值
+fac.RedisSet(ctx, "key", "value", time.Hour)
+
+// 获取值
+value, _ := fac.RedisGet(ctx, "key")
+
+// 删除值
+fac.RedisDelete(ctx, "key")
+```
+
+### 场景3:发送邮件
+
+```go
+// 发送简单邮件
+fac.SendEmail(
+ []string{"user@example.com"},
+ "测试邮件",
+ "这是邮件正文",
+)
+
+// 发送HTML邮件
+fac.SendEmail(
+ []string{"user@example.com"},
+ "测试邮件",
+ "纯文本内容",
+ "
HTML内容
",
+)
+```
+
+### 场景4:上传文件
+
+```go
+ctx := context.Background()
+
+// 打开文件
+file, _ := os.Open("test.jpg")
+defer file.Close()
+
+// 上传文件(自动选择OSS或MinIO)
+url, _ := fac.UploadFile(ctx, "images/test.jpg", file, "image/jpeg")
+
+// 获取文件URL
+url, _ := fac.GetFileURL("images/test.jpg", 3600) // 1小时后过期
+```
+
+### 场景5:记录日志
+
+```go
+// 简单日志
+fac.LogInfo("用户登录成功")
+fac.LogError("登录失败: %v", err)
+
+// 带字段的日志
+fac.LogInfof(map[string]interface{}{
+ "user_id": 123,
+ "action": "login",
+}, "用户操作")
+```
+
+### 场景6:时间处理
+
+```go
+import "git.toowon.com/jimmy/go-common/datetime"
+
+// 获取当前时间(带时区)
+timezone := h.GetTimezone()
+now := datetime.Now(timezone)
+
+// 格式化时间
+str := datetime.FormatDateTime(now)
+
+// 解析时间
+t, _ := datetime.ParseDateTime("2024-01-01 00:00:00", timezone)
+
+// 时间计算
+tomorrow := datetime.AddDays(now, 1)
+startOfDay := datetime.StartOfDay(now, timezone)
+```
+
+### 场景7:数据库迁移(独立工具)⭐
+
+```bash
+# 1. 复制模板到项目
+cp templates/migrate/main.go cmd/migrate/
+
+# 2. 创建迁移文件 migrations/20240101000001_create_users.sql
+
+# 3. 编译并使用
+go build -o bin/migrate cmd/migrate/main.go
+./bin/migrate up # 执行迁移
+./bin/migrate status # 查看状态
+```
+
+**特点**:独立工具,零耦合,生产就绪
+
+完整指南:[MIGRATION.md](./MIGRATION.md)
+
+## 7. 更多文档
+
+- [完整文档](./docs/README.md)
+- [中间件文档](./docs/middleware.md)
+- [工厂模式文档](./docs/factory.md)
+- [HTTP工具文档](./docs/http.md)
+- [配置文档](./docs/config.md)
+
+## 常见问题
+
+### Q: 如何自定义中间件配置?
+
+查看 [中间件文档](./docs/middleware.md) 了解详细配置选项。
+
+### Q: 如何使用数据库迁移?
+
+查看 [迁移工具文档](./docs/migration.md) 了解数据库版本管理。
+
+### Q: 支持哪些数据库?
+
+支持 MySQL、PostgreSQL、SQLite。
+
+### Q: 日志如何配置异步模式?
+
+在配置文件中设置 `"async": true`,或通过代码配置:
+
+```go
+loggerConfig := &config.LoggerConfig{
+ Async: true,
+ BufferSize: 1000,
+}
+```
+
+### Q: 如何按用户ID限流?
+
+```go
+limiter := middleware.NewTokenBucketLimiter(100, time.Minute)
+rateLimitConfig := &middleware.RateLimitConfig{
+ Limiter: limiter,
+ KeyFunc: func(r *http.Request) string {
+ return r.Header.Get("X-User-ID")
+ },
+}
+chain := middleware.NewChain(
+ middleware.RateLimit(rateLimitConfig),
+)
+```
+
+## 下一步
+
+恭喜!你已经掌握了 GoCommon 的基本使用。
+
+建议阅读:
+1. [中间件文档](./docs/middleware.md) - 了解更多中间件配置
+2. [工厂模式文档](./docs/factory.md) - 深入了解黑盒模式
+3. [示例代码](./examples/) - 查看更多实际示例
+
diff --git a/README.md b/README.md
index 84d6cf8..b5c4cf7 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,44 @@
这是一个Go语言开发的通用工具类库,为其他Go项目提供常用的工具方法集合。
+**📖 快速链接**:
+- [5分钟快速开始](./QUICKSTART.md)
+- [数据库迁移指南](./MIGRATION.md) ⭐ 独立工具,零耦合,Docker友好
+- [完整文档](./docs/README.md)
+
+## 🌟 核心特性
+
+### 🎯 **极简调用,减少80%重复代码**
+- **工厂黑盒模式**:一个配置文件,搞定所有服务初始化
+- **Handler黑盒模式**:统一的HTTP请求处理,无需重复传递 `w` 和 `r`
+- **中间件链式调用**:一行代码组合多个中间件
+
+### 🚀 **生产级特性,开箱即用**
+- **异步日志**:不阻塞请求,高并发性能
+- **Panic恢复**:自动捕获panic,防止服务崩溃
+- **令牌桶限流**:保护API,防止滥用
+- **时区自动处理**:统一管理时区,避免时间错乱
+
+### 🔧 **灵活可扩展**
+- **默认配置即可用**:传 `nil` 使用默认配置
+- **完全可定制**:每个功能都支持自定义配置
+- **无侵入设计**:可以独立使用任何模块
+
+### 📦 **零外部依赖(核心功能)**
+- email、sms 使用 Go 标准库实现
+- 可选依赖:gorm(数据库)、redis、minio
+
## 功能模块
### 1. 数据库迁移工具 (migration)
提供数据库迁移功能,支持MySQL、PostgreSQL、SQLite等数据库。
+**🎯 独立工具,零耦合**:
+- ✅ 编译成独立二进制:`go build -o bin/migrate cmd/migrate/main.go`
+- ✅ 生产环境无需Go环境,只需二进制文件
+- ✅ 与应用代码完全解耦,可独立部署和执行
+- ✅ 支持宿主机和Docker,零额外配置
+
### 2. 日期转换工具 (datetime)
提供日期时间转换功能,支持时区设定和多种格式转换。
@@ -14,7 +47,13 @@
提供HTTP请求/响应处理工具,包含标准化的响应结构、分页支持和HTTP状态码与业务状态码的分离。
### 4. 中间件工具 (middleware)
-提供常用的HTTP中间件,包括CORS处理和时区管理。
+提供生产级HTTP中间件,包括:
+- **CORS** - 跨域资源共享
+- **Timezone** - 时区处理
+- **Logging** - 请求日志记录(支持异步)
+- **Recovery** - Panic恢复,防止服务崩溃
+- **RateLimit** - 请求限流(令牌桶算法)
+- **Chain** - 中间件链式组合
### 5. 配置工具 (config)
提供从外部文件加载配置的功能,支持数据库、OSS、Redis、CORS、MinIO等配置。
@@ -64,9 +103,107 @@ go get git.toowon.com/jimmy/go-common@v1.0.0
**版本管理说明请参考 [VERSION.md](./VERSION.md)**
+---
+
+## 📚 文档导航
+
+- **[快速开始指南](./QUICKSTART.md)** ⭐ - 5分钟快速上手
+- [完整文档](./docs/README.md) - 所有模块详细文档
+- [故障排除](./TROUBLESHOOTING.md) - 常见问题解决
+- [版本管理](./VERSION.md) - 版本发布说明
+
+---
+
+## 快速开始
+
+### 1. 创建配置文件 `config.json`
+
+```json
+{
+ "database": {
+ "type": "mysql",
+ "host": "localhost",
+ "port": 3306,
+ "user": "root",
+ "password": "password",
+ "database": "mydb"
+ },
+ "redis": {
+ "host": "localhost",
+ "port": 6379
+ },
+ "logger": {
+ "level": "info",
+ "output": "both",
+ "filePath": "./logs/app.log",
+ "async": true
+ }
+}
+```
+
+### 2. 使用工厂模式(最简单,推荐)
+
+```go
+package main
+
+import (
+ "net/http"
+ "time"
+
+ "git.toowon.com/jimmy/go-common/factory"
+ "git.toowon.com/jimmy/go-common/middleware"
+ commonhttp "git.toowon.com/jimmy/go-common/http"
+)
+
+func main() {
+ // 从配置文件创建工厂
+ fac, _ := factory.NewFactoryFromFile("./config.json")
+
+ // 获取logger
+ logger, _ := fac.GetLogger()
+ defer logger.Close()
+
+ // 配置中间件
+ chain := middleware.NewChain(
+ middleware.Recovery(&middleware.RecoveryConfig{Logger: logger}),
+ middleware.Logging(&middleware.LoggingConfig{Logger: logger}),
+ middleware.RateLimitByIP(100, time.Minute),
+ middleware.CORS(nil),
+ middleware.Timezone,
+ )
+
+ // 注册API路由
+ http.Handle("/api/hello", chain.ThenFunc(handleHello))
+
+ // 启动服务
+ http.ListenAndServe(":8080", nil)
+}
+
+func handleHello(w http.ResponseWriter, r *http.Request) {
+ h := commonhttp.NewHandler(w, r)
+
+ // 使用工厂记录日志
+ // fac.LogInfo("Hello API called")
+
+ // 返回响应
+ h.Success(map[string]interface{}{
+ "message": "Hello, World!",
+ "timezone": h.GetTimezone(),
+ })
+}
+```
+
+### 3. 运行项目
+
+```bash
+go run main.go
+# 访问 http://localhost:8080/api/hello
+```
+
## 使用示例
详细的使用说明请参考各模块的文档:
+- **[数据库迁移完整指南](./MIGRATION.md)** ⭐ - 独立工具,零耦合
- [数据库迁移工具文档](./docs/migration.md)
- [日期转换工具文档](./docs/datetime.md)
- [HTTP Restful工具文档](./docs/http.md)
@@ -80,21 +217,28 @@ go get git.toowon.com/jimmy/go-common@v1.0.0
### 快速示例
-#### 数据库迁移
-```go
-import "git.toowon.com/jimmy/go-common/migration"
+#### 数据库迁移(独立工具,零耦合)⭐
+```bash
+# 1. 复制模板:templates/migrate/main.go -> cmd/migrate/main.go
+# 2. 编译(生产环境推荐)
+go build -o bin/migrate cmd/migrate/main.go
-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()
+# 3. 使用
+./bin/migrate up # 使用默认配置
+./bin/migrate up -config /path/to/config.json # 指定配置
+./bin/migrate status # 查看状态
+
+# 迁移文件:migrations/20240101000001_create_users.sql
+# CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT, ...);
+
+# Docker 中使用(挂载配置,修改无需重启)
+# volumes:
+# - ./config.json:/app/config.json:ro
+# command: sh -c "./migrate up && ./server"
```
+**详细说明**:[数据库迁移完整指南](./MIGRATION.md) 📖
+
#### 日期转换
```go
import "git.toowon.com/jimmy/go-common/datetime"
@@ -120,23 +264,53 @@ func GetUser(h *commonhttp.Handler) {
http.HandleFunc("/user", commonhttp.HandleFunc(GetUser))
```
-#### 中间件
+#### 中间件(完整的生产级配置)
```go
import (
+ "time"
+ "git.toowon.com/jimmy/go-common/config"
+ "git.toowon.com/jimmy/go-common/logger"
"git.toowon.com/jimmy/go-common/middleware"
commonhttp "git.toowon.com/jimmy/go-common/http"
)
-// CORS + 时区中间件
+// 方式1:简单配置(使用默认设置)
chain := middleware.NewChain(
- middleware.CORS(),
+ middleware.Recovery(nil), // Panic恢复
+ middleware.Logging(nil), // 请求日志
+ middleware.RateLimit(nil), // 限流(100请求/分钟)
+ middleware.CORS(nil), // CORS(允许所有源)
+ middleware.Timezone, // 时区处理
+)
+http.Handle("/api", chain.ThenFunc(yourHandler))
+
+// 方式2:生产级配置(推荐)
+// 创建异步logger
+myLogger, _ := logger.NewLogger(&config.LoggerConfig{
+ Level: "info",
+ Output: "both",
+ FilePath: "./logs/app.log",
+ Async: true, // 异步模式,不阻塞请求
+})
+
+chain := middleware.NewChain(
+ middleware.Recovery(&middleware.RecoveryConfig{
+ Logger: myLogger,
+ EnableStackTrace: true,
+ }),
+ middleware.Logging(&middleware.LoggingConfig{
+ Logger: myLogger,
+ SkipPaths: []string{"/health"},
+ }),
+ middleware.RateLimitByIP(100, time.Minute), // 100请求/分钟
+ middleware.CORS(nil),
middleware.Timezone,
)
-handler := chain.ThenFunc(yourHandler)
-// 在Handler中获取时区
+// 在Handler中使用
func handler(h *commonhttp.Handler) {
- timezone := h.GetTimezone()
+ timezone := h.GetTimezone() // 获取时区
+ h.Success(data)
}
```
@@ -276,3 +450,197 @@ go get git.toowon.com/jimmy/go-common@v1.0.0
**详细版本管理说明请参考 [VERSION.md](./VERSION.md)**
+## 设计理念
+
+### 1. 黑盒模式 - 减少重复代码
+
+**问题**:传统方式需要在每个项目中重复编写初始化代码
+
+```go
+// ❌ 传统方式 - 需要在每个项目中重复
+db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
+sqlDB, _ := db.DB()
+sqlDB.SetMaxOpenConns(100)
+sqlDB.SetMaxIdleConns(10)
+// ... 更多配置
+```
+
+**解决**:工厂黑盒模式 - 配置文件搞定一切
+
+```go
+// ✅ 黑盒模式 - 一行代码搞定
+db, _ := factory.NewFactoryFromFile("config.json").GetDatabase()
+```
+
+### 2. Handler模式 - 统一请求处理
+
+**问题**:每个处理器都要传递 `w` 和 `r`
+
+```go
+// ❌ 传统方式
+func GetUser(w http.ResponseWriter, r *http.Request) {
+ id := r.URL.Query().Get("id")
+ json.NewEncoder(w).Encode(data)
+}
+```
+
+**解决**:Handler封装 - 简洁优雅
+
+```go
+// ✅ Handler模式
+func GetUser(h *commonhttp.Handler) {
+ id := h.GetQueryInt64("id", 0)
+ h.Success(data)
+}
+```
+
+### 3. 中间件链 - 灵活组合
+
+**问题**:中间件嵌套难以维护
+
+```go
+// ❌ 传统方式 - 嵌套地狱
+handler := corsMiddleware(
+ timezoneMiddleware(
+ loggingMiddleware(
+ yourHandler
+ )
+ )
+)
+```
+
+**解决**:链式调用 - 清晰明了
+
+```go
+// ✅ 链式组合
+chain := middleware.NewChain(
+ middleware.CORS(),
+ middleware.Timezone,
+ middleware.Logging(nil),
+)
+handler := chain.ThenFunc(yourHandler)
+```
+
+## 最佳实践
+
+### ✅ 推荐做法
+
+1. **使用工厂模式**:通过配置文件统一管理所有服务
+2. **使用异步日志**:生产环境开启 `Async: true`
+3. **配置Recovery中间件**:防止panic导致服务崩溃
+4. **合理设置限流**:根据实际业务设置限流阈值
+5. **使用时区中间件**:统一管理时区,避免时间错乱
+
+### ❌ 避免做法
+
+1. **不要在循环中创建logger**:使用全局logger或工厂模式
+2. **不要跳过健康检查日志**:高频接口应该配置 `SkipPaths`
+3. **不要使用同步日志记录大量日志**:高并发场景使用异步模式
+4. **不要在生产环境使用 `CORS: *`**:明确指定允许的源
+
+## 性能优化建议
+
+### 日志优化
+
+```go
+// ✅ 异步模式 - 高并发场景
+loggerConfig := &config.LoggerConfig{
+ Async: true, // 异步写入
+ BufferSize: 1000, // 缓冲区大小
+}
+
+// ✅ 跳过高频接口
+loggingConfig := &middleware.LoggingConfig{
+ SkipPaths: []string{"/health", "/metrics", "/ping"},
+}
+```
+
+### 中间件顺序优化
+
+```go
+// ✅ 推荐顺序(从外到内)
+chain := middleware.NewChain(
+ middleware.Recovery(cfg), // 1. 最外层捕获panic
+ middleware.Logging(cfg), // 2. 记录所有请求
+ middleware.RateLimit(cfg), // 3. 限流保护
+ middleware.CORS(cfg), // 4. CORS处理
+ middleware.Timezone, // 5. 时区处理
+)
+```
+
+### 数据库连接池优化
+
+```json
+{
+ "database": {
+ "maxOpenConns": 100, // 最大连接数
+ "maxIdleConns": 10, // 最大空闲连接
+ "connMaxLifetime": 3600 // 连接最大生存时间(秒)
+ }
+}
+```
+
+## 故障排除
+
+### 问题1:循环导入错误
+
+**错误**:`import cycle not allowed`
+
+**解决**:使用 `middleware.NewCORSConfig()` 转换配置
+
+```go
+configCORS := cfg.GetCORS()
+middlewareCORS := middleware.NewCORSConfig(
+ configCORS.AllowedOrigins,
+ configCORS.AllowedMethods,
+ configCORS.AllowedHeaders,
+ configCORS.ExposedHeaders,
+ configCORS.AllowCredentials,
+ configCORS.MaxAge,
+)
+```
+
+### 问题2:IDE显示导入错误
+
+**错误**:`could not import git.toowon.com/jimmy/go-common/logger`
+
+**解决**:重置Go模块缓存
+
+```bash
+go clean -modcache
+go mod download
+# 重启IDE的Language Server
+```
+
+### 问题3:限流不生效
+
+**原因**:分布式部署下,内存存储只在单机生效
+
+**解决**:分布式场景建议使用Redis实现限流
+
+更多问题请查看 [TROUBLESHOOTING.md](./TROUBLESHOOTING.md)
+
+## 贡献指南
+
+欢迎贡献代码!请遵循以下步骤:
+
+1. Fork 本仓库
+2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 创建 Pull Request
+
+## 许可证
+
+MIT License
+
+## 联系方式
+
+- 作者:Jimmy
+- 邮箱:jimmy@toowon.com
+- 项目地址:git.toowon.com/jimmy/go-common
+
+---
+
+⭐ 如果这个项目对你有帮助,请给个 Star!
+
diff --git a/config/config.go b/config/config.go
index ebe17c1..4fda174 100644
--- a/config/config.go
+++ b/config/config.go
@@ -5,8 +5,6 @@ import (
"fmt"
"os"
"path/filepath"
-
- "git.toowon.com/jimmy/go-common/middleware"
)
// Config 应用配置
@@ -397,20 +395,11 @@ 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,
- }
+// GetCORS 获取CORS配置
+// 返回的是 config.CORSConfig,需要转换为 middleware.CORSConfig 时
+// 可以使用 middleware.CORSFromConfig() 函数
+func (c *Config) GetCORS() *CORSConfig {
+ return c.CORS
}
// GetMinIO 获取MinIO配置
diff --git a/docs/README.md b/docs/README.md
index 9baf764..df2d012 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -3,9 +3,10 @@
## 目录
- [数据库迁移工具](./migration.md) - 数据库版本管理和迁移
+ - [完整使用指南](../MIGRATION.md) ⭐ - 独立工具,零耦合,Docker友好
- [日期转换工具](./datetime.md) - 日期时间处理和时区转换
- [HTTP Restful工具](./http.md) - HTTP请求响应处理和分页
-- [中间件工具](./middleware.md) - CORS和时区处理中间件
+- [中间件工具](./middleware.md) - 生产级HTTP中间件(CORS、时区、日志、Recovery、限流)
- [配置工具](./config.md) - 外部配置文件加载和管理
- [存储工具](./storage.md) - 文件上传和查看(OSS、MinIO)
- [邮件工具](./email.md) - SMTP邮件发送
@@ -23,22 +24,37 @@ go get git.toowon.com/jimmy/go-common
### 使用示例
-#### 数据库迁移
+#### 数据库迁移(独立工具,零耦合)
-```go
-import "git.toowon.com/jimmy/go-common/migration"
+```bash
+# 1. 复制模板:templates/migrate/main.go -> cmd/migrate/main.go
+# 2. 创建迁移文件:migrations/20240101000001_create_users.sql
-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()
+# 3. 开发环境
+go run cmd/migrate/main.go up
+
+# 4. 生产环境(编译后使用,推荐)
+go build -o bin/migrate cmd/migrate/main.go
+./bin/migrate up # 使用默认配置
+./bin/migrate up -config /path/to/config.json # 指定配置
+./bin/migrate status # 查看状态
+
+# 5. Docker(挂载配置,修改无需重启)
+# docker-compose.yml:
+# volumes:
+# - ./config.json:/app/config.json:ro
+# command: sh -c "./migrate up && ./server"
```
+**迁移文件示例**(`migrations/20240101000001_create_users.sql`):
+```sql
+CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT, ...);
+```
+
+**优势**:独立工具,零耦合,支持命令行参数,Docker友好
+
+详细说明:[MIGRATION.md](../MIGRATION.md)
+
#### 日期转换
```go
@@ -65,24 +81,26 @@ func GetUser(h *commonhttp.Handler) {
http.HandleFunc("/user", commonhttp.HandleFunc(GetUser))
```
-#### 中间件
+#### 中间件(生产级配置)
```go
import (
- "net/http"
+ "time"
"git.toowon.com/jimmy/go-common/middleware"
commonhttp "git.toowon.com/jimmy/go-common/http"
)
-// CORS + 时区中间件
+// 完整的中间件链
chain := middleware.NewChain(
- middleware.CORS(),
- middleware.Timezone,
+ middleware.Recovery(nil), // Panic恢复
+ middleware.Logging(nil), // 请求日志
+ middleware.RateLimitByIP(100, time.Minute), // 限流
+ middleware.CORS(nil), // CORS
+ middleware.Timezone, // 时区
)
handler := chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
h := commonhttp.NewHandler(w, r)
- // 在Handler中获取时区
timezone := h.GetTimezone()
h.Success(data)
})
diff --git a/docs/config.md b/docs/config.md
index 5dfb998..a382deb 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -163,13 +163,26 @@ addr := config.GetRedisAddr()
### 5. 获取CORS配置
```go
-// 获取CORS配置(返回middleware.CORSConfig类型,可直接用于中间件)
-corsConfig := config.GetCORS()
+// 获取CORS配置(返回config.CORSConfig类型)
+configCORS := config.GetCORS()
-// 使用CORS中间件
+// 转换为middleware.CORSConfig并使用CORS中间件
import "git.toowon.com/jimmy/go-common/middleware"
+
+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),
)
```
@@ -328,9 +341,20 @@ func main() {
fmt.Printf("Redis Address: %s\n", redisAddr)
// 使用CORS配置
- corsConfig := cfg.GetCORS()
+ configCORS := cfg.GetCORS()
+ 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),
)
// 使用OSS配置
diff --git a/docs/middleware.md b/docs/middleware.md
index 22d355f..bb6b4f6 100644
--- a/docs/middleware.md
+++ b/docs/middleware.md
@@ -2,12 +2,15 @@
## 概述
-中间件工具提供了常用的HTTP中间件功能,包括CORS处理和时区管理。
+中间件工具提供了常用的HTTP中间件功能,包括CORS处理、时区管理、请求日志、Panic恢复和限流等。
## 功能特性
- **CORS中间件**:支持跨域资源共享配置
- **时区中间件**:从请求头读取时区信息,支持默认时区设置
+- **日志中间件**:自动记录每个HTTP请求的详细信息
+- **Recovery中间件**:捕获panic并恢复,防止服务崩溃
+- **限流中间件**:基于令牌桶算法的请求限流
- **中间件链**:提供便捷的中间件链式调用
## CORS中间件
@@ -233,6 +236,371 @@ X-Timezone: Asia/Shanghai
3. 时区信息存储在context中,可以在整个请求生命周期中使用
4. 建议在CORS配置中包含 `X-Timezone` 请求头
+## 日志中间件
+
+### 功能说明
+
+日志中间件用于自动记录每个HTTP请求的详细信息,帮助监控和调试应用。
+
+记录内容包括:
+- 请求方法、路径、查询参数
+- 响应状态码、响应大小
+- 请求处理时间(毫秒)
+- 客户端IP地址(支持X-Forwarded-For)
+- User-Agent、Referer等信息
+
+### 使用方法
+
+#### 基本使用(使用默认logger)
+
+```go
+import (
+ "net/http"
+ "git.toowon.com/jimmy/go-common/middleware"
+)
+
+func main() {
+ chain := middleware.NewChain(
+ middleware.Logging(nil), // 使用默认配置
+ middleware.CORS(),
+ middleware.Timezone,
+ )
+
+ handler := chain.ThenFunc(apiHandler)
+ http.Handle("/api", handler)
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+#### 使用自定义logger
+
+```go
+import (
+ "git.toowon.com/jimmy/go-common/middleware"
+ "git.toowon.com/jimmy/go-common/logger"
+)
+
+func main() {
+ // 创建自定义logger(异步模式,输出到文件)
+ loggerConfig := &logger.LoggerConfig{
+ Level: "info",
+ Output: "file",
+ FilePath: "./logs/app.log",
+ Async: true, // 异步模式,不阻塞请求
+ BufferSize: 1000,
+ }
+ myLogger, _ := logger.NewLogger(loggerConfig)
+
+ // 配置日志中间件
+ loggingConfig := &middleware.LoggingConfig{
+ Logger: myLogger,
+ SkipPaths: []string{"/health", "/metrics"}, // 跳过健康检查接口
+ }
+
+ chain := middleware.NewChain(
+ middleware.Logging(loggingConfig),
+ middleware.CORS(),
+ middleware.Timezone,
+ )
+
+ handler := chain.ThenFunc(apiHandler)
+ http.Handle("/api", handler)
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+### LoggingConfig 配置说明
+
+| 字段 | 类型 | 说明 | 默认值 |
+|------|------|------|--------|
+| Logger | *logger.Logger | 日志记录器 | 创建默认logger(stdout) |
+| SkipPaths | []string | 跳过记录的路径列表 | [] |
+| LogRequestBody | bool | 是否记录请求体(慎用) | false |
+| LogResponseBody | bool | 是否记录响应体(慎用) | false |
+
+### 日志输出示例
+
+```
+[INFO] HTTP Request method=GET path=/api/users query= status=200 size=1024 duration=45 ip=192.168.1.100 user_agent=Mozilla/5.0 referer=
+[WARN] HTTP Request method=POST path=/api/users query= status=400 size=128 duration=12 ip=192.168.1.100 user_agent=PostmanRuntime/7.29
+[ERROR] HTTP Request method=GET path=/api/error query= status=500 size=256 duration=89 ip=192.168.1.100 user_agent=curl/7.64.1 referer=
+```
+
+### 注意事项
+
+1. **异步模式推荐**:生产环境建议使用异步logger,避免日志写入阻塞请求
+2. **跳过路径**:健康检查、监控接口等高频接口建议跳过日志记录
+3. **日志级别**:根据状态码自动选择日志级别(5xx=ERROR, 4xx=WARN, 2xx-3xx=INFO)
+4. **客户端IP**:自动从X-Forwarded-For、X-Real-IP或RemoteAddr获取真实IP
+
+## Recovery中间件
+
+### 功能说明
+
+Recovery中间件用于捕获HTTP处理过程中的panic,防止panic导致整个服务崩溃。
+
+功能包括:
+- 捕获panic并恢复服务
+- 记录panic信息和堆栈跟踪
+- 返回500错误响应
+- 支持自定义错误处理
+
+### 使用方法
+
+#### 基本使用(使用默认配置)
+
+```go
+import (
+ "net/http"
+ "git.toowon.com/jimmy/go-common/middleware"
+)
+
+func main() {
+ chain := middleware.NewChain(
+ middleware.Recovery(nil), // 使用默认配置
+ middleware.Logging(nil),
+ middleware.CORS(),
+ middleware.Timezone,
+ )
+
+ handler := chain.ThenFunc(apiHandler)
+ http.Handle("/api", handler)
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+#### 使用自定义logger
+
+```go
+import (
+ "git.toowon.com/jimmy/go-common/middleware"
+ "git.toowon.com/jimmy/go-common/logger"
+)
+
+func main() {
+ myLogger, _ := logger.NewLogger(nil)
+
+ recoveryConfig := &middleware.RecoveryConfig{
+ Logger: myLogger,
+ EnableStackTrace: true, // 启用堆栈跟踪
+ }
+
+ chain := middleware.NewChain(
+ middleware.Recovery(recoveryConfig),
+ middleware.Logging(nil),
+ )
+
+ handler := chain.ThenFunc(apiHandler)
+ http.Handle("/api", handler)
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+#### 自定义错误响应
+
+```go
+import (
+ "net/http"
+ commonhttp "git.toowon.com/jimmy/go-common/http"
+ "git.toowon.com/jimmy/go-common/middleware"
+)
+
+func main() {
+ recoveryConfig := &middleware.RecoveryConfig{
+ EnableStackTrace: true,
+ CustomHandler: func(w http.ResponseWriter, r *http.Request, err interface{}) {
+ // 使用统一的JSON响应格式
+ h := commonhttp.NewHandler(w, r)
+ h.SystemError("服务器内部错误")
+ },
+ }
+
+ chain := middleware.NewChain(
+ middleware.Recovery(recoveryConfig),
+ )
+
+ handler := chain.ThenFunc(apiHandler)
+ http.Handle("/api", handler)
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+### RecoveryConfig 配置说明
+
+| 字段 | 类型 | 说明 | 默认值 |
+|------|------|------|--------|
+| Logger | *logger.Logger | 日志记录器 | 创建默认logger |
+| EnableStackTrace | bool | 是否记录堆栈跟踪 | true |
+| CustomHandler | func(...) | 自定义错误处理函数 | nil(返回500文本) |
+
+### 注意事项
+
+1. **放在最外层**:Recovery中间件应该放在中间件链的最前面,以捕获所有panic
+2. **日志记录**:建议配置logger,确保panic信息被记录下来
+3. **堆栈跟踪**:生产环境建议启用,方便排查问题
+4. **自定义响应**:可以自定义错误响应格式,统一错误处理
+
+## 限流中间件
+
+### 功能说明
+
+限流中间件用于限制请求频率,防止API被滥用或遭受攻击。
+
+特性:
+- 基于令牌桶算法
+- 支持按IP、用户ID等维度限流
+- 自动设置限流响应头
+- 内存存储,自动清理过期数据
+
+### 使用方法
+
+#### 基本使用(默认配置:100请求/分钟)
+
+```go
+import (
+ "net/http"
+ "git.toowon.com/jimmy/go-common/middleware"
+)
+
+func main() {
+ chain := middleware.NewChain(
+ middleware.Recovery(nil),
+ middleware.Logging(nil),
+ middleware.RateLimit(nil), // 默认100请求/分钟,按IP限流
+ middleware.CORS(),
+ )
+
+ handler := chain.ThenFunc(apiHandler)
+ http.Handle("/api", handler)
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+#### 自定义限流规则
+
+```go
+import (
+ "time"
+ "git.toowon.com/jimmy/go-common/middleware"
+)
+
+func main() {
+ // 创建限流器:10请求/分钟
+ limiter := middleware.NewTokenBucketLimiter(10, time.Minute)
+
+ rateLimitConfig := &middleware.RateLimitConfig{
+ Limiter: limiter,
+ }
+
+ chain := middleware.NewChain(
+ middleware.RateLimit(rateLimitConfig),
+ )
+
+ handler := chain.ThenFunc(apiHandler)
+ http.Handle("/api", handler)
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+#### 按用户ID限流
+
+```go
+import (
+ "net/http"
+ "time"
+ "git.toowon.com/jimmy/go-common/middleware"
+)
+
+func main() {
+ limiter := middleware.NewTokenBucketLimiter(100, time.Minute)
+
+ rateLimitConfig := &middleware.RateLimitConfig{
+ Limiter: limiter,
+ KeyFunc: func(r *http.Request) string {
+ // 从请求头或JWT token中获取用户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) {
+ // 记录限流事件
+ println("Rate limit exceeded for:", key)
+ },
+ }
+
+ chain := middleware.NewChain(
+ middleware.RateLimit(rateLimitConfig),
+ )
+
+ handler := chain.ThenFunc(apiHandler)
+ http.Handle("/api", handler)
+ http.ListenAndServe(":8080", nil)
+}
+```
+
+#### 便捷函数
+
+```go
+// 按IP限流:10请求/分钟
+chain := middleware.NewChain(
+ middleware.RateLimitByIP(10, time.Minute),
+)
+
+// 或使用自定义速率
+chain := middleware.NewChain(
+ middleware.RateLimitWithRate(50, time.Minute),
+)
+```
+
+### RateLimitConfig 配置说明
+
+| 字段 | 类型 | 说明 | 默认值 |
+|------|------|------|--------|
+| Limiter | RateLimiter | 限流器实例 | 100请求/分钟 |
+| KeyFunc | func(*http.Request) string | 生成限流键的函数 | 使用客户端IP |
+| OnRateLimitExceeded | func(...) | 限流触发回调 | nil |
+
+### 响应头说明
+
+限流中间件会自动设置以下响应头:
+
+| 响应头 | 说明 |
+|--------|------|
+| X-RateLimit-Limit | 窗口期内允许的请求数 |
+| X-RateLimit-Remaining | 当前窗口剩余配额 |
+| X-RateLimit-Reset | 配额重置时间(Unix时间戳) |
+| Retry-After | 限流时,建议重试的等待时间(秒) |
+
+### 响应示例
+
+正常请求:
+```
+HTTP/1.1 200 OK
+X-RateLimit-Limit: 100
+X-RateLimit-Remaining: 95
+X-RateLimit-Reset: 1640000000
+```
+
+触发限流:
+```
+HTTP/1.1 429 Too Many Requests
+X-RateLimit-Limit: 100
+X-RateLimit-Remaining: 0
+X-RateLimit-Reset: 1640000000
+Retry-After: 45
+Too Many Requests
+```
+
+### 注意事项
+
+1. **内存存储**:当前实现使用内存存储,适用于单机部署。如需分布式限流,建议使用Redis
+2. **键设计**:合理设计限流键,可以按IP、用户、API等维度限流
+3. **清理机制**:自动清理过期的限流数据,避免内存泄漏
+4. **算法选择**:使用令牌桶算法,支持突发流量
+
## 中间件链
### 功能说明
@@ -277,7 +645,107 @@ handler := chain.ThenFunc(handler)
## 完整示例
-### 示例1:CORS + 时区中间件
+### 示例1:完整的生产级中间件配置
+
+```go
+package main
+
+import (
+ "log"
+ "net/http"
+ "time"
+
+ "git.toowon.com/jimmy/go-common/middleware"
+ "git.toowon.com/jimmy/go-common/logger"
+ commonhttp "git.toowon.com/jimmy/go-common/http"
+ "git.toowon.com/jimmy/go-common/datetime"
+)
+
+func apiHandler(w http.ResponseWriter, r *http.Request) {
+ h := commonhttp.NewHandler(w, r)
+
+ // 从Handler获取时区
+ timezone := h.GetTimezone()
+ now := datetime.Now(timezone)
+
+ h.Success(map[string]interface{}{
+ "message": "Hello",
+ "timezone": timezone,
+ "time": datetime.FormatDateTime(now),
+ })
+}
+
+func main() {
+ // 1. 配置logger(异步模式,输出到文件)
+ loggerConfig := &logger.LoggerConfig{
+ Level: "info",
+ Output: "both", // 同时输出到stdout和文件
+ FilePath: "./logs/app.log",
+ Async: true,
+ BufferSize: 1000,
+ }
+ myLogger, err := logger.NewLogger(loggerConfig)
+ if err != nil {
+ log.Fatal(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"},
+ }
+
+ // 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{}) {
+ 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,
+ }, "Rate limit exceeded")
+ },
+ }
+
+ // 6. 创建中间件链(顺序很重要!)
+ chain := middleware.NewChain(
+ middleware.Recovery(recoveryConfig), // 最外层:捕获panic
+ middleware.Logging(loggingConfig), // 日志记录
+ middleware.RateLimit(rateLimitConfig), // 限流
+ middleware.CORS(corsConfig), // CORS处理
+ middleware.Timezone, // 时区处理
+ )
+
+ // 7. 应用中间件
+ http.Handle("/api", chain.ThenFunc(apiHandler))
+ http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("OK"))
+ })
+
+ log.Println("Server started on :8080")
+ log.Fatal(http.ListenAndServe(":8080", nil))
+}
+```
+
+### 示例2:基础的CORS + 时区中间件
```go
package main
@@ -306,23 +774,13 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
}
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,
+ middleware.CORS(nil), // 使用默认CORS配置
+ middleware.Timezone, // 使用默认时区
)
- // 应用中间件
- handler := chain.ThenFunc(apiHandler)
-
- http.Handle("/api", handler)
+ http.Handle("/api", chain.ThenFunc(apiHandler))
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
@@ -408,6 +866,65 @@ func getPosts(w http.ResponseWriter, r *http.Request) {
从context中获取时区。
+### 日志中间件
+
+#### Logging(config *LoggingConfig) func(http.Handler) http.Handler
+
+创建日志中间件。
+
+**参数:**
+- `config`: 日志配置,nil则使用默认配置
+
+**返回:** 中间件函数
+
+### Recovery中间件
+
+#### Recovery(config *RecoveryConfig) func(http.Handler) http.Handler
+
+创建Recovery中间件。
+
+**参数:**
+- `config`: Recovery配置,nil则使用默认配置
+
+**返回:** 中间件函数
+
+#### RecoveryWithLogger(log *logger.Logger) func(http.Handler) http.Handler
+
+使用指定logger的Recovery中间件(便捷函数)。
+
+#### RecoveryWithCustomHandler(customHandler func(...)) func(http.Handler) http.Handler
+
+使用自定义错误处理的Recovery中间件(便捷函数)。
+
+### 限流中间件
+
+#### RateLimit(config *RateLimitConfig) func(http.Handler) http.Handler
+
+创建限流中间件。
+
+**参数:**
+- `config`: 限流配置,nil则使用默认配置(100请求/分钟)
+
+**返回:** 中间件函数
+
+#### NewTokenBucketLimiter(rate int, windowSize time.Duration) RateLimiter
+
+创建令牌桶限流器。
+
+**参数:**
+- `rate`: 每个窗口期允许的请求数
+- `windowSize`: 窗口大小
+
+**返回:** 限流器实例
+
+#### RateLimitWithRate(rate int, windowSize time.Duration) func(http.Handler) http.Handler
+
+使用指定速率创建限流中间件(便捷函数)。
+
+#### RateLimitByIP(rate int, windowSize time.Duration) func(http.Handler) http.Handler
+
+按IP限流(便捷函数)。
+
### 中间件链
#### NewChain(middlewares ...func(http.Handler) http.Handler) *Chain
@@ -428,19 +945,63 @@ func getPosts(w http.ResponseWriter, r *http.Request) {
## 注意事项
-1. **CORS配置**:
- - 生产环境建议明确指定允许的源,避免使用 "*"
- - 如果使用凭证(cookies),必须明确指定源,不能使用 "*"
+### 1. CORS配置
+- 生产环境建议明确指定允许的源,避免使用 "*"
+- 如果使用凭证(cookies),必须明确指定源,不能使用 "*"
+- CORS中间件应该在Recovery和Logging之后,以便正确处理预检请求
-2. **时区处理**:
- - 时区信息存储在context中,确保中间件在处理器之前执行
- - 时区验证失败时会自动回退到默认时区,不会返回错误
+### 2. 时区处理
+- 时区信息存储在context中,确保中间件在处理器之前执行
+- 时区验证失败时会自动回退到默认时区,不会返回错误
+- 建议在CORS配置中包含 `X-Timezone` 请求头
-3. **中间件顺序**:
- - CORS中间件应该放在最外层,以便处理预检请求
- - 时区中间件可以放在CORS之后
+### 3. 日志记录
+- **生产环境推荐异步模式**:避免日志写入阻塞请求,提升性能
+- **跳过高频接口**:健康检查、监控接口等高频接口建议跳过日志
+- **日志轮转**:使用文件输出时,建议配合日志轮转工具(如logrotate)
+- **敏感信息**:不要记录请求体和响应体,避免泄露敏感信息
-4. **性能考虑**:
- - CORS预检请求会被缓存,减少重复请求
- - 时区验证只在请求头存在时进行,性能影响很小
+### 4. Panic恢复
+- **放在最外层**:Recovery中间件应该放在中间件链的最前面
+- **记录日志**:务必配置logger,确保panic信息被记录
+- **监控告警**:建议将panic事件接入监控系统,及时发现问题
+- **堆栈跟踪**:生产环境建议启用,方便排查问题
+
+### 5. 限流配置
+- **合理设置阈值**:根据实际业务需求设置限流阈值
+- **分布式部署**:当前实现使用内存存储,适用于单机。分布式部署建议使用Redis
+- **键设计**:合理设计限流键,可以按IP、用户、API等维度限流
+- **响应头**:客户端可以根据X-RateLimit-*响应头实现智能重试
+
+### 6. 中间件顺序(推荐)
+建议的中间件顺序(从外到内):
+1. **Recovery** - 最外层,捕获所有panic
+2. **Logging** - 记录所有请求(包括限流的请求)
+3. **RateLimit** - 限流保护
+4. **CORS** - 处理跨域
+5. **Timezone** - 时区处理
+6. **业务中间件** - 认证、授权等
+
+```go
+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. 性能考虑
+- **异步日志**:使用异步logger,避免IO阻塞
+- **限流算法**:令牌桶算法支持突发流量
+- **自动清理**:限流数据会自动清理,避免内存泄漏
+- **跳过路径**:合理使用SkipPaths,减少不必要的处理
+
+### 8. 生产环境建议
+- 使用异步logger,配置日志文件和轮转
+- 启用Recovery中间件,配置告警
+- 根据业务设置合理的限流阈值
+- 配置监控指标(请求量、错误率、限流触发次数等)
+- 定期review日志,优化性能瓶颈
diff --git a/docs/storage.md b/docs/storage.md
index c8fa664..25ac3d1 100644
--- a/docs/storage.md
+++ b/docs/storage.md
@@ -325,8 +325,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,
)
diff --git a/examples/config_example.go b/examples/config_example.go
index 853b9d7..4b6b531 100644
--- a/examples/config_example.go
+++ b/examples/config_example.go
@@ -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)
diff --git a/examples/middleware_example.go b/examples/middleware_example.go
deleted file mode 100644
index 452797b..0000000
--- a/examples/middleware_example.go
+++ /dev/null
@@ -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),
- })
-}
diff --git a/examples/middleware_full_example.go b/examples/middleware_full_example.go
new file mode 100644
index 0000000..46c4cf8
--- /dev/null
+++ b/examples/middleware_full_example.go
@@ -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"))
+}
+
diff --git a/examples/middleware_ratelimit_example.go b/examples/middleware_ratelimit_example.go
new file mode 100644
index 0000000..93663c4
--- /dev/null
+++ b/examples/middleware_ratelimit_example.go
@@ -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))
+}
+
diff --git a/examples/middleware_simple_example.go b/examples/middleware_simple_example.go
new file mode 100644
index 0000000..c70d555
--- /dev/null
+++ b/examples/middleware_simple_example.go
@@ -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))
+}
+
diff --git a/examples/migrations/README.md b/examples/migrations/README.md
new file mode 100644
index 0000000..0fc2251
--- /dev/null
+++ b/examples/migrations/README.md
@@ -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)
diff --git a/examples/migrations/migrations/20240101000001_create_users_table.down.sql b/examples/migrations/migrations/20240101000001_create_users_table.down.sql
new file mode 100644
index 0000000..b9026b3
--- /dev/null
+++ b/examples/migrations/migrations/20240101000001_create_users_table.down.sql
@@ -0,0 +1,4 @@
+-- Rollback: Drop users table
+
+DROP TABLE IF EXISTS users;
+
diff --git a/examples/migrations/migrations/20240101000001_create_users_table.sql b/examples/migrations/migrations/20240101000001_create_users_table.sql
new file mode 100644
index 0000000..7a824e9
--- /dev/null
+++ b/examples/migrations/migrations/20240101000001_create_users_table.sql
@@ -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;
+
diff --git a/examples/storage_example.go b/examples/storage_example.go
index 4ea7ecd..4858846 100644
--- a/examples/storage_example.go
+++ b/examples/storage_example.go
@@ -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,
)
diff --git a/logger/logger.go b/logger/logger.go
index 5358076..a9acd55 100644
--- a/logger/logger.go
+++ b/logger/logger.go
@@ -11,6 +11,23 @@ import (
"git.toowon.com/jimmy/go-common/config"
)
+var (
+ // defaultLogger 全局默认日志记录器
+ // 用于中间件和其他需要快速日志记录的场景
+ defaultLogger *Logger
+ defaultMux sync.RWMutex
+)
+
+func init() {
+ // 初始化默认logger(同步模式,输出到stdout)
+ var err error
+ defaultLogger, err = NewLogger(nil)
+ if err != nil {
+ // 如果初始化失败,使用nil,后续会降级到标准输出
+ defaultLogger = nil
+ }
+}
+
// logMessage 异步日志消息结构
type logMessage struct {
level string // debug, info, warn, error
@@ -357,3 +374,90 @@ func (l *Logger) Close() error {
return nil
}
+
+// ========== 全局默认Logger相关方法 ==========
+
+// SetDefaultLogger 设置全局默认logger
+// 用于在应用启动时统一配置logger
+func SetDefaultLogger(log *Logger) {
+ defaultMux.Lock()
+ defer defaultMux.Unlock()
+
+ // 如果之前有logger,先关闭它
+ if defaultLogger != nil {
+ defaultLogger.Close()
+ }
+
+ defaultLogger = log
+}
+
+// GetDefaultLogger 获取全局默认logger
+func GetDefaultLogger() *Logger {
+ defaultMux.RLock()
+ defer defaultMux.RUnlock()
+ return defaultLogger
+}
+
+// Default 全局日志方法 - Debug
+func Default() *Logger {
+ return GetDefaultLogger()
+}
+
+// ========== 全局便捷日志方法 ==========
+// 以下方法使用全局默认logger,方便快速记录日志
+
+// Debug 使用全局logger记录调试日志
+func Debug(format string, v ...interface{}) {
+ if log := GetDefaultLogger(); log != nil {
+ log.Debug(format, v...)
+ }
+}
+
+// Info 使用全局logger记录信息日志
+func Info(format string, v ...interface{}) {
+ if log := GetDefaultLogger(); log != nil {
+ log.Info(format, v...)
+ }
+}
+
+// Warn 使用全局logger记录警告日志
+func Warn(format string, v ...interface{}) {
+ if log := GetDefaultLogger(); log != nil {
+ log.Warn(format, v...)
+ }
+}
+
+// Error 使用全局logger记录错误日志
+func Error(format string, v ...interface{}) {
+ if log := GetDefaultLogger(); log != nil {
+ log.Error(format, v...)
+ }
+}
+
+// Debugf 使用全局logger记录调试日志(带字段)
+func Debugf(fields map[string]interface{}, format string, v ...interface{}) {
+ if log := GetDefaultLogger(); log != nil {
+ log.Debugf(fields, format, v...)
+ }
+}
+
+// Infof 使用全局logger记录信息日志(带字段)
+func Infof(fields map[string]interface{}, format string, v ...interface{}) {
+ if log := GetDefaultLogger(); log != nil {
+ log.Infof(fields, format, v...)
+ }
+}
+
+// Warnf 使用全局logger记录警告日志(带字段)
+func Warnf(fields map[string]interface{}, format string, v ...interface{}) {
+ if log := GetDefaultLogger(); log != nil {
+ log.Warnf(fields, format, v...)
+ }
+}
+
+// Errorf 使用全局logger记录错误日志(带字段)
+func Errorf(fields map[string]interface{}, format string, v ...interface{}) {
+ if log := GetDefaultLogger(); log != nil {
+ log.Errorf(fields, format, v...)
+ }
+}
diff --git a/middleware/cors.go b/middleware/cors.go
index 4d56835..86472e8 100644
--- a/middleware/cors.go
+++ b/middleware/cors.go
@@ -43,6 +43,36 @@ func DefaultCORSConfig() *CORSConfig {
}
}
+// NewCORSConfig 从配置参数创建 CORSConfig
+// 用于从 config 包的 CORSConfig 转换为 middleware 的 CORSConfig
+// 避免循环依赖
+func NewCORSConfig(allowedOrigins, allowedMethods, allowedHeaders, exposedHeaders []string, allowCredentials bool, maxAge int) *CORSConfig {
+ cfg := &CORSConfig{
+ AllowedOrigins: allowedOrigins,
+ AllowedMethods: allowedMethods,
+ AllowedHeaders: allowedHeaders,
+ ExposedHeaders: exposedHeaders,
+ AllowCredentials: allowCredentials,
+ MaxAge: maxAge,
+ }
+
+ // 设置默认值(如果为空)
+ if len(cfg.AllowedOrigins) == 0 {
+ cfg.AllowedOrigins = []string{"*"}
+ }
+ if len(cfg.AllowedMethods) == 0 {
+ cfg.AllowedMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"}
+ }
+ if len(cfg.AllowedHeaders) == 0 {
+ cfg.AllowedHeaders = []string{"Content-Type", "Authorization", "X-Requested-With", "X-Timezone"}
+ }
+ if cfg.MaxAge == 0 {
+ cfg.MaxAge = 86400
+ }
+
+ return cfg
+}
+
// CORS CORS中间件
func CORS(config ...*CORSConfig) func(http.Handler) http.Handler {
var cfg *CORSConfig
diff --git a/middleware/logging.go b/middleware/logging.go
new file mode 100644
index 0000000..5699287
--- /dev/null
+++ b/middleware/logging.go
@@ -0,0 +1,221 @@
+package middleware
+
+import (
+ "net/http"
+ "strconv"
+ "time"
+
+ "git.toowon.com/jimmy/go-common/logger"
+)
+
+// responseWriter 包装 http.ResponseWriter 以捕获状态码和响应大小
+type responseWriter struct {
+ http.ResponseWriter
+ statusCode int
+ size int
+}
+
+func (rw *responseWriter) WriteHeader(statusCode int) {
+ rw.statusCode = statusCode
+ rw.ResponseWriter.WriteHeader(statusCode)
+}
+
+func (rw *responseWriter) Write(b []byte) (int, error) {
+ size, err := rw.ResponseWriter.Write(b)
+ rw.size += size
+ return size, err
+}
+
+// LoggingConfig 日志中间件配置
+type LoggingConfig struct {
+ // Logger 日志记录器(可选,如果为nil则使用默认logger)
+ Logger *logger.Logger
+
+ // SkipPaths 跳过记录的路径列表(如健康检查接口)
+ SkipPaths []string
+
+ // LogRequestBody 是否记录请求体(谨慎使用,可能影响性能)
+ LogRequestBody bool
+
+ // LogResponseBody 是否记录响应体(谨慎使用,可能影响性能和内存)
+ LogResponseBody bool
+}
+
+// Logging HTTP请求日志中间件
+// 记录每个HTTP请求的详细信息,包括:
+// - 请求方法、路径、IP、User-Agent
+// - 响应状态码、响应大小
+// - 请求处理时间
+//
+// 使用方式1:使用默认logger
+//
+// chain := middleware.NewChain(
+// middleware.Logging(nil),
+// )
+//
+// 使用方式2:使用自定义logger
+//
+// myLogger, _ := logger.NewLogger(loggerConfig)
+// chain := middleware.NewChain(
+// middleware.Logging(&middleware.LoggingConfig{
+// Logger: myLogger,
+// SkipPaths: []string{"/health", "/metrics"},
+// }),
+// )
+func Logging(config *LoggingConfig) func(http.Handler) http.Handler {
+ // 如果没有配置,使用默认配置
+ if config == nil {
+ config = &LoggingConfig{}
+ }
+
+ // 如果没有提供logger,创建一个默认的
+ if config.Logger == nil {
+ // 使用默认配置创建logger(输出到stdout,info级别)
+ defaultLogger, err := logger.NewLogger(nil)
+ if err != nil {
+ // 如果创建失败,使用nil,后面会降级处理
+ config.Logger = nil
+ } else {
+ config.Logger = defaultLogger
+ }
+ }
+
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // 检查是否跳过此路径
+ if shouldSkipPath(r.URL.Path, config.SkipPaths) {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ // 记录开始时间
+ startTime := time.Now()
+
+ // 包装 ResponseWriter 以捕获状态码和响应大小
+ rw := &responseWriter{
+ ResponseWriter: w,
+ statusCode: http.StatusOK, // 默认200
+ size: 0,
+ }
+
+ // 处理请求
+ next.ServeHTTP(rw, r)
+
+ // 计算处理时间
+ duration := time.Since(startTime)
+
+ // 记录日志
+ logHTTPRequest(config.Logger, r, rw, duration)
+ })
+ }
+}
+
+// shouldSkipPath 检查是否应该跳过该路径
+func shouldSkipPath(path string, skipPaths []string) bool {
+ for _, skipPath := range skipPaths {
+ if path == skipPath {
+ return true
+ }
+ }
+ return false
+}
+
+// logHTTPRequest 记录HTTP请求日志
+func logHTTPRequest(log *logger.Logger, r *http.Request, rw *responseWriter, duration time.Duration) {
+ // 获取客户端IP
+ clientIP := getClientIP(r)
+
+ // 构建日志字段
+ fields := map[string]interface{}{
+ "method": r.Method,
+ "path": r.URL.Path,
+ "query": r.URL.RawQuery,
+ "status": rw.statusCode,
+ "size": rw.size,
+ "duration": duration.Milliseconds(), // 毫秒
+ "ip": clientIP,
+ "user_agent": r.UserAgent(),
+ "referer": r.Referer(),
+ }
+
+ // 构建日志消息
+ message := "HTTP Request"
+
+ // 根据状态码选择日志级别
+ if log != nil {
+ // 使用提供的logger
+ if rw.statusCode >= 500 {
+ log.Errorf(fields, message)
+ } else if rw.statusCode >= 400 {
+ log.Warnf(fields, message)
+ } else {
+ log.Infof(fields, message)
+ }
+ } else {
+ // 降级处理:使用标准输出
+ // 注意:这是同步的,不会有性能问题
+ if rw.statusCode >= 500 {
+ logToStdout("ERROR", fields, message)
+ } else if rw.statusCode >= 400 {
+ logToStdout("WARN", fields, message)
+ } else {
+ logToStdout("INFO", fields, message)
+ }
+ }
+}
+
+// logToStdout 降级处理:输出到标准输出(当logger不可用时)
+func logToStdout(level string, fields map[string]interface{}, message string) {
+ // 简单的标准输出日志
+ var fieldStr string
+ for k, v := range fields {
+ fieldStr += " " + k + "=" + formatValue(v)
+ }
+ println("[" + level + "] " + message + fieldStr)
+}
+
+// formatValue 格式化值(用于日志输出)
+func formatValue(v interface{}) string {
+ switch val := v.(type) {
+ case string:
+ return val
+ case int:
+ return strconv.Itoa(val)
+ case int64:
+ return strconv.FormatInt(val, 10)
+ default:
+ return ""
+ }
+}
+
+// getClientIP 获取客户端真实IP
+// 优先级:X-Forwarded-For > X-Real-IP > RemoteAddr
+func getClientIP(r *http.Request) string {
+ // 尝试从 X-Forwarded-For 获取
+ xff := r.Header.Get("X-Forwarded-For")
+ if xff != "" {
+ // X-Forwarded-For 可能包含多个IP,取第一个
+ for idx := 0; idx < len(xff); idx++ {
+ if xff[idx] == ',' {
+ return xff[:idx]
+ }
+ }
+ return xff
+ }
+
+ // 尝试从 X-Real-IP 获取
+ xri := r.Header.Get("X-Real-IP")
+ if xri != "" {
+ return xri
+ }
+
+ // 使用 RemoteAddr
+ remoteAddr := r.RemoteAddr
+ // 移除端口号
+ for i := len(remoteAddr) - 1; i >= 0; i-- {
+ if remoteAddr[i] == ':' {
+ return remoteAddr[:i]
+ }
+ }
+ return remoteAddr
+}
diff --git a/middleware/ratelimit.go b/middleware/ratelimit.go
new file mode 100644
index 0000000..5639b74
--- /dev/null
+++ b/middleware/ratelimit.go
@@ -0,0 +1,286 @@
+package middleware
+
+import (
+ "net/http"
+ "sync"
+ "time"
+)
+
+// RateLimiter 限流器接口
+type RateLimiter interface {
+ // Allow 检查是否允许请求
+ // key: 限流键(如IP地址、用户ID等)
+ // 返回: 是否允许, 剩余配额, 重置时间
+ Allow(key string) (allowed bool, remaining int, resetTime time.Time)
+}
+
+// tokenBucketLimiter 令牌桶限流器
+type tokenBucketLimiter struct {
+ rate int // 每个窗口期允许的请求数
+ windowSize time.Duration // 窗口大小
+ buckets map[string]*bucket
+ mu sync.RWMutex
+ cleanupTicker *time.Ticker
+ stopCleanup chan struct{}
+}
+
+// bucket 令牌桶
+type bucket struct {
+ tokens int // 当前令牌数
+ lastRefill time.Time // 上次填充时间
+ mu sync.Mutex
+}
+
+// NewTokenBucketLimiter 创建令牌桶限流器
+func NewTokenBucketLimiter(rate int, windowSize time.Duration) RateLimiter {
+ limiter := &tokenBucketLimiter{
+ rate: rate,
+ windowSize: windowSize,
+ buckets: make(map[string]*bucket),
+ stopCleanup: make(chan struct{}),
+ }
+
+ // 启动清理goroutine,定期清理过期的bucket
+ limiter.cleanupTicker = time.NewTicker(windowSize * 2)
+ go limiter.cleanup()
+
+ return limiter
+}
+
+// cleanup 定期清理过期的bucket
+func (l *tokenBucketLimiter) cleanup() {
+ for {
+ select {
+ case <-l.cleanupTicker.C:
+ l.mu.Lock()
+ now := time.Now()
+ for key, bkt := range l.buckets {
+ bkt.mu.Lock()
+ // 如果bucket超过2个窗口期没有使用,删除它
+ if now.Sub(bkt.lastRefill) > l.windowSize*2 {
+ delete(l.buckets, key)
+ }
+ bkt.mu.Unlock()
+ }
+ l.mu.Unlock()
+ case <-l.stopCleanup:
+ l.cleanupTicker.Stop()
+ return
+ }
+ }
+}
+
+// Allow 检查是否允许请求
+func (l *tokenBucketLimiter) Allow(key string) (bool, int, time.Time) {
+ now := time.Now()
+
+ // 获取或创建bucket
+ l.mu.Lock()
+ bkt, exists := l.buckets[key]
+ if !exists {
+ bkt = &bucket{
+ tokens: l.rate,
+ lastRefill: now,
+ }
+ l.buckets[key] = bkt
+ }
+ l.mu.Unlock()
+
+ // 尝试消费令牌
+ bkt.mu.Lock()
+ defer bkt.mu.Unlock()
+
+ // 计算需要填充的令牌数
+ elapsed := now.Sub(bkt.lastRefill)
+ if elapsed >= l.windowSize {
+ // 窗口期已过,重新填充
+ bkt.tokens = l.rate
+ bkt.lastRefill = now
+ }
+
+ // 检查是否有可用令牌
+ if bkt.tokens > 0 {
+ bkt.tokens--
+ resetTime := bkt.lastRefill.Add(l.windowSize)
+ return true, bkt.tokens, resetTime
+ }
+
+ // 没有可用令牌
+ resetTime := bkt.lastRefill.Add(l.windowSize)
+ return false, 0, resetTime
+}
+
+// RateLimitConfig 限流中间件配置
+type RateLimitConfig struct {
+ // Limiter 限流器(必需)
+ // 如果为nil,会使用默认的令牌桶限流器(100请求/分钟)
+ Limiter RateLimiter
+
+ // KeyFunc 生成限流键的函数(可选)
+ // 默认使用客户端IP作为键
+ // 可以自定义为用户ID、API Key等
+ KeyFunc func(r *http.Request) string
+
+ // OnRateLimitExceeded 当限流被触发时的回调(可选)
+ // 可以用于记录日志、发送告警等
+ OnRateLimitExceeded func(w http.ResponseWriter, r *http.Request, key string)
+}
+
+// RateLimit 限流中间件
+// 实现基于令牌桶算法的请求限流
+//
+// 使用方式1:使用默认配置(100请求/分钟,按IP限流)
+//
+// chain := middleware.NewChain(
+// middleware.RateLimit(nil),
+// )
+//
+// 使用方式2:自定义限流规则
+//
+// limiter := middleware.NewTokenBucketLimiter(10, time.Minute) // 10请求/分钟
+// chain := middleware.NewChain(
+// middleware.RateLimit(&middleware.RateLimitConfig{
+// Limiter: limiter,
+// }),
+// )
+//
+// 使用方式3:按用户ID限流
+//
+// chain := middleware.NewChain(
+// middleware.RateLimit(&middleware.RateLimitConfig{
+// Limiter: limiter,
+// KeyFunc: func(r *http.Request) string {
+// // 从请求头或token中获取用户ID
+// return r.Header.Get("X-User-ID")
+// },
+// }),
+// )
+func RateLimit(config *RateLimitConfig) func(http.Handler) http.Handler {
+ // 如果没有配置,使用默认配置
+ if config == nil {
+ config = &RateLimitConfig{}
+ }
+
+ // 如果没有提供限流器,创建默认的(100请求/分钟)
+ if config.Limiter == nil {
+ config.Limiter = NewTokenBucketLimiter(100, time.Minute)
+ }
+
+ // 如果没有提供KeyFunc,使用默认的(客户端IP)
+ if config.KeyFunc == nil {
+ config.KeyFunc = func(r *http.Request) string {
+ return getClientIP(r)
+ }
+ }
+
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // 生成限流键
+ key := config.KeyFunc(r)
+ if key == "" {
+ // 如果无法生成键,允许请求通过
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ // 检查是否允许请求
+ allowed, remaining, resetTime := config.Limiter.Allow(key)
+
+ // 设置限流相关的响应头
+ w.Header().Set("X-RateLimit-Limit", formatInt(config.Limiter.(*tokenBucketLimiter).rate))
+ w.Header().Set("X-RateLimit-Remaining", formatInt(remaining))
+ w.Header().Set("X-RateLimit-Reset", formatInt64(resetTime.Unix()))
+
+ if !allowed {
+ // 触发限流回调
+ if config.OnRateLimitExceeded != nil {
+ config.OnRateLimitExceeded(w, r, key)
+ }
+
+ // 返回429错误
+ w.Header().Set("Retry-After", formatInt64(int64(time.Until(resetTime).Seconds())))
+ http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
+ return
+ }
+
+ // 允许请求通过
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+// RateLimitWithRate 使用指定速率创建限流中间件(便捷函数)
+// rate: 每个窗口期允许的请求数
+// windowSize: 窗口大小
+func RateLimitWithRate(rate int, windowSize time.Duration) func(http.Handler) http.Handler {
+ return RateLimit(&RateLimitConfig{
+ Limiter: NewTokenBucketLimiter(rate, windowSize),
+ })
+}
+
+// RateLimitByIP 按IP限流(便捷函数)
+func RateLimitByIP(rate int, windowSize time.Duration) func(http.Handler) http.Handler {
+ return RateLimit(&RateLimitConfig{
+ Limiter: NewTokenBucketLimiter(rate, windowSize),
+ KeyFunc: func(r *http.Request) string {
+ return getClientIP(r)
+ },
+ })
+}
+
+// formatInt 格式化int为字符串
+func formatInt(n int) string {
+ if n == 0 {
+ return "0"
+ }
+
+ // 简单的int转字符串
+ var buf [20]byte
+ i := len(buf) - 1
+ negative := n < 0
+ if negative {
+ n = -n
+ }
+
+ for n > 0 {
+ buf[i] = byte('0' + n%10)
+ n /= 10
+ i--
+ }
+
+ if negative {
+ buf[i] = '-'
+ i--
+ }
+
+ return string(buf[i+1:])
+}
+
+// formatInt64 格式化int64为字符串
+func formatInt64(n int64) string {
+ if n == 0 {
+ return "0"
+ }
+
+ // 简单的int64转字符串
+ var buf [20]byte
+ i := len(buf) - 1
+ negative := n < 0
+ if negative {
+ n = -n
+ }
+
+ for n > 0 {
+ buf[i] = byte('0' + n%10)
+ n /= 10
+ i--
+ }
+
+ if negative {
+ buf[i] = '-'
+ i--
+ }
+
+ return string(buf[i+1:])
+}
+
diff --git a/middleware/recovery.go b/middleware/recovery.go
new file mode 100644
index 0000000..c95c952
--- /dev/null
+++ b/middleware/recovery.go
@@ -0,0 +1,149 @@
+package middleware
+
+import (
+ "fmt"
+ "net/http"
+ "runtime/debug"
+
+ "git.toowon.com/jimmy/go-common/logger"
+)
+
+// RecoveryConfig Recovery中间件配置
+type RecoveryConfig struct {
+ // Logger 日志记录器(可选,如果为nil则使用默认logger)
+ Logger *logger.Logger
+
+ // EnableStackTrace 是否在日志中包含堆栈跟踪
+ EnableStackTrace bool
+
+ // CustomHandler 自定义错误处理函数(可选)
+ // 如果设置了,会在记录日志后调用此函数
+ // 可以用于自定义错误响应格式
+ CustomHandler func(w http.ResponseWriter, r *http.Request, err interface{})
+}
+
+// Recovery Panic恢复中间件
+// 捕获HTTP处理过程中的panic,记录错误日志并返回500错误
+// 防止panic导致整个服务崩溃
+//
+// 使用方式1:使用默认配置
+//
+// chain := middleware.NewChain(
+// middleware.Recovery(nil),
+// )
+//
+// 使用方式2:使用自定义配置
+//
+// myLogger, _ := logger.NewLogger(loggerConfig)
+// chain := middleware.NewChain(
+// middleware.Recovery(&middleware.RecoveryConfig{
+// Logger: myLogger,
+// EnableStackTrace: true,
+// }),
+// )
+//
+// 使用方式3:自定义错误响应
+//
+// chain := middleware.NewChain(
+// middleware.Recovery(&middleware.RecoveryConfig{
+// Logger: myLogger,
+// CustomHandler: func(w http.ResponseWriter, r *http.Request, err interface{}) {
+// // 自定义JSON响应
+// w.Header().Set("Content-Type", "application/json")
+// w.WriteHeader(http.StatusInternalServerError)
+// w.Write([]byte(`{"code":500,"message":"Internal Server Error"}`))
+// },
+// }),
+// )
+func Recovery(config *RecoveryConfig) func(http.Handler) http.Handler {
+ // 如果没有配置,使用默认配置
+ if config == nil {
+ config = &RecoveryConfig{
+ EnableStackTrace: true, // 默认启用堆栈跟踪
+ }
+ }
+
+ // 如果没有提供logger,创建一个默认的
+ if config.Logger == nil {
+ defaultLogger, err := logger.NewLogger(nil)
+ if err != nil {
+ config.Logger = nil
+ } else {
+ config.Logger = defaultLogger
+ }
+ }
+
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer func() {
+ if err := recover(); err != nil {
+ // 记录panic信息
+ logPanic(config.Logger, r, err, config.EnableStackTrace)
+
+ // 如果提供了自定义处理函数,调用它
+ if config.CustomHandler != nil {
+ config.CustomHandler(w, r, err)
+ return
+ }
+
+ // 默认错误响应
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ }
+ }()
+
+ next.ServeHTTP(w, r)
+ })
+ }
+}
+
+// logPanic 记录panic日志
+func logPanic(log *logger.Logger, r *http.Request, err interface{}, enableStackTrace bool) {
+ // 获取堆栈跟踪
+ var stack string
+ if enableStackTrace {
+ stack = string(debug.Stack())
+ }
+
+ // 构建日志字段
+ fields := map[string]interface{}{
+ "method": r.Method,
+ "path": r.URL.Path,
+ "query": r.URL.RawQuery,
+ "ip": getClientIP(r),
+ "error": fmt.Sprintf("%v", err),
+ }
+
+ // 构建日志消息
+ message := "Panic recovered"
+ if enableStackTrace && stack != "" {
+ message += "\n" + stack
+ }
+
+ // 记录错误日志
+ if log != nil {
+ log.Errorf(fields, message)
+ } else {
+ // 降级处理:输出到标准错误
+ fmt.Printf("[ERROR] %s\n", message)
+ for k, v := range fields {
+ fmt.Printf(" %s: %v\n", k, v)
+ }
+ }
+}
+
+// RecoveryWithLogger 使用指定logger的Recovery中间件(便捷函数)
+func RecoveryWithLogger(log *logger.Logger) func(http.Handler) http.Handler {
+ return Recovery(&RecoveryConfig{
+ Logger: log,
+ EnableStackTrace: true,
+ })
+}
+
+// RecoveryWithCustomHandler 使用自定义错误处理的Recovery中间件(便捷函数)
+func RecoveryWithCustomHandler(customHandler func(w http.ResponseWriter, r *http.Request, err interface{})) func(http.Handler) http.Handler {
+ return Recovery(&RecoveryConfig{
+ EnableStackTrace: true,
+ CustomHandler: customHandler,
+ })
+}
+
diff --git a/migration/helper.go b/migration/helper.go
new file mode 100644
index 0000000..4cb77f1
--- /dev/null
+++ b/migration/helper.go
@@ -0,0 +1,216 @@
+package migration
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "git.toowon.com/jimmy/go-common/config"
+ "gorm.io/driver/mysql"
+ "gorm.io/driver/postgres"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+)
+
+// RunMigrationsFromConfig 从配置文件运行迁移(便捷方法)
+//
+// 注意:推荐使用独立的迁移工具(templates/migrate/main.go),而不是在应用代码中直接调用。
+// 独立工具可以实现零耦合、独立部署。
+//
+// 此方法主要用于:
+// 1. 独立迁移工具内部调用(推荐)
+// 2. 简单场景下在应用启动时调用(不推荐,会导致耦合)
+//
+// 用法:
+//
+// import "git.toowon.com/jimmy/go-common/migration"
+// migration.RunMigrationsFromConfig("config.json", "migrations")
+func RunMigrationsFromConfig(configFile, migrationsDir string) error {
+ // 加载配置
+ cfg, err := loadConfigFromFileOrEnv(configFile)
+ if err != nil {
+ return fmt.Errorf("加载配置失败: %w", err)
+ }
+
+ // 连接数据库
+ db, err := connectDB(cfg)
+ if err != nil {
+ return fmt.Errorf("连接数据库失败: %w", err)
+ }
+
+ // 创建迁移器
+ migrator := NewMigrator(db)
+
+ // 加载迁移文件
+ migrations, err := LoadMigrationsFromFiles(migrationsDir, "*.sql")
+ if err != nil {
+ return fmt.Errorf("加载迁移文件失败: %w", err)
+ }
+
+ if len(migrations) == 0 {
+ fmt.Printf("在目录 '%s' 中没有找到迁移文件\n", migrationsDir)
+ return nil
+ }
+
+ migrator.AddMigrations(migrations...)
+
+ // 执行迁移
+ if err := migrator.Up(); err != nil {
+ return fmt.Errorf("执行迁移失败: %w", err)
+ }
+
+ fmt.Println("✓ 迁移执行成功")
+ return nil
+}
+
+// RunMigrationsFromConfigWithCommand 从配置文件运行迁移(支持命令)
+// command: "up", "down", "status"
+func RunMigrationsFromConfigWithCommand(configFile, migrationsDir, command string) error {
+ // 加载配置
+ cfg, err := loadConfigFromFileOrEnv(configFile)
+ if err != nil {
+ return fmt.Errorf("加载配置失败: %w", err)
+ }
+
+ // 连接数据库
+ db, err := connectDB(cfg)
+ if err != nil {
+ return fmt.Errorf("连接数据库失败: %w", err)
+ }
+
+ // 创建迁移器
+ migrator := NewMigrator(db)
+
+ // 加载迁移文件
+ migrations, err := LoadMigrationsFromFiles(migrationsDir, "*.sql")
+ if err != nil {
+ return fmt.Errorf("加载迁移文件失败: %w", err)
+ }
+
+ if len(migrations) == 0 {
+ fmt.Printf("在目录 '%s' 中没有找到迁移文件\n", migrationsDir)
+ return nil
+ }
+
+ migrator.AddMigrations(migrations...)
+
+ // 执行命令
+ switch command {
+ case "up":
+ if err := migrator.Up(); err != nil {
+ return fmt.Errorf("执行迁移失败: %w", err)
+ }
+ fmt.Println("✓ 迁移执行成功")
+
+ case "down":
+ if err := migrator.Down(); err != nil {
+ return fmt.Errorf("回滚迁移失败: %w", err)
+ }
+ fmt.Println("✓ 迁移回滚成功")
+
+ case "status":
+ status, err := migrator.Status()
+ if err != nil {
+ return fmt.Errorf("获取迁移状态失败: %w", err)
+ }
+ printMigrationStatus(status)
+
+ default:
+ return fmt.Errorf("未知命令: %s (支持: up, down, status)", command)
+ }
+
+ return nil
+}
+
+// loadConfigFromFileOrEnv 从文件或环境变量加载配置
+func loadConfigFromFileOrEnv(configFile string) (*config.Config, error) {
+ // 优先从环境变量加载
+ if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" {
+ return &config.Config{
+ Database: &config.DatabaseConfig{
+ DSN: dbURL,
+ },
+ }, nil
+ }
+
+ // 尝试从配置文件加载
+ if configFile != "" {
+ if _, err := os.Stat(configFile); err == nil {
+ return config.LoadFromFile(configFile)
+ }
+ }
+
+ // 尝试默认路径
+ defaultPaths := []string{"config.json", "../config.json"}
+ for _, path := range defaultPaths {
+ if _, err := os.Stat(path); err == nil {
+ return config.LoadFromFile(path)
+ }
+ }
+
+ return nil, fmt.Errorf("未找到配置文件,也未设置环境变量 DATABASE_URL")
+}
+
+// connectDB 连接数据库
+func connectDB(cfg *config.Config) (*gorm.DB, error) {
+ if cfg.Database == nil {
+ return nil, fmt.Errorf("数据库配置为空")
+ }
+
+ dsn, err := cfg.GetDatabaseDSN()
+ if err != nil {
+ return nil, err
+ }
+
+ var db *gorm.DB
+ switch cfg.Database.Type {
+ case "mysql":
+ db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
+ case "postgres":
+ db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
+ case "sqlite":
+ db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
+ default:
+ return nil, fmt.Errorf("不支持的数据库类型: %s", cfg.Database.Type)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ // 配置连接池
+ sqlDB, err := db.DB()
+ if err != nil {
+ return nil, err
+ }
+
+ sqlDB.SetMaxOpenConns(10)
+ sqlDB.SetMaxIdleConns(5)
+ sqlDB.SetConnMaxLifetime(time.Hour)
+
+ return db, nil
+}
+
+// printMigrationStatus 打印迁移状态
+func printMigrationStatus(status []MigrationStatus) {
+ if len(status) == 0 {
+ fmt.Println("没有找到迁移")
+ return
+ }
+
+ fmt.Println("\n迁移状态:")
+ fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+ fmt.Printf("%-20s %-40s %-10s\n", "版本", "描述", "状态")
+ fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+
+ for _, s := range status {
+ statusText := "待执行"
+ if s.Applied {
+ statusText = "✓ 已应用"
+ }
+ fmt.Printf("%-20s %-40s %-10s\n", s.Version, s.Description, statusText)
+ }
+ fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+ fmt.Println()
+}
+
diff --git a/templates/Dockerfile.example b/templates/Dockerfile.example
new file mode 100644
index 0000000..14ab207
--- /dev/null
+++ b/templates/Dockerfile.example
@@ -0,0 +1,39 @@
+# Dockerfile 示例
+# 展示如何构建包含迁移工具的 Go 应用
+
+# ===== 构建阶段 =====
+FROM golang:1.21 as builder
+
+WORKDIR /app
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+
+# 编译应用和迁移工具
+RUN go build -o bin/server cmd/server/main.go
+RUN go build -o bin/migrate cmd/migrate/main.go
+
+# ===== 运行阶段 =====
+FROM debian:bookworm-slim
+
+WORKDIR /app
+
+# 安装运行时依赖
+RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
+
+# 复制二进制文件和迁移文件
+COPY --from=builder /app/bin/server .
+COPY --from=builder /app/bin/migrate .
+COPY --from=builder /app/migrations ./migrations
+
+EXPOSE 8080
+
+# 启动:先执行迁移,再启动应用
+CMD ["sh", "-c", "./migrate up && ./server"]
+
+# 注意:
+# 1. 配置文件通过 docker-compose volumes 挂载,不打包进镜像
+# 2. 镜像只包含二进制文件和迁移SQL文件
+# 3. 配置文件在运行时提供,更安全、更灵活
+
diff --git a/templates/Makefile.example b/templates/Makefile.example
new file mode 100644
index 0000000..d1c8fb2
--- /dev/null
+++ b/templates/Makefile.example
@@ -0,0 +1,78 @@
+# 示例 Makefile
+# 提供常用的开发和部署命令
+
+.PHONY: help build run migrate-up migrate-down migrate-status docker-build docker-up clean
+
+# 默认目标:显示帮助
+help:
+ @echo "可用命令:"
+ @echo " make build - 编译应用和迁移工具"
+ @echo " make run - 运行应用(先执行迁移)"
+ @echo " make migrate-up - 执行数据库迁移"
+ @echo " make migrate-down - 回滚数据库迁移"
+ @echo " make migrate-status - 查看迁移状态"
+ @echo " make docker-build - 构建 Docker 镜像"
+ @echo " make docker-up - 启动 Docker 服务"
+ @echo " make clean - 清理编译文件"
+
+# 编译
+build:
+ @echo "编译应用..."
+ @mkdir -p bin
+ @go build -o bin/server cmd/server/main.go
+ @go build -o bin/migrate cmd/migrate/main.go
+ @echo "✓ 编译完成"
+
+# 运行应用
+run: migrate-up
+ @echo "启动应用..."
+ @./bin/server
+
+# 执行迁移
+migrate-up: build
+ @echo "执行数据库迁移..."
+ @./bin/migrate up
+
+# 回滚迁移
+migrate-down: build
+ @echo "回滚数据库迁移..."
+ @./bin/migrate down
+
+# 查看迁移状态
+migrate-status: build
+ @./bin/migrate status
+
+# 构建 Docker 镜像
+docker-build:
+ @echo "构建 Docker 镜像..."
+ @docker build -t myapp:latest .
+
+# 启动 Docker 服务
+docker-up:
+ @echo "启动 Docker 服务..."
+ @docker-compose up --build
+
+# 清理
+clean:
+ @echo "清理编译文件..."
+ @rm -rf bin
+ @echo "✓ 清理完成"
+
+# 开发环境:直接运行(不编译)
+dev-migrate-up:
+ @go run cmd/migrate/main.go up
+
+dev-migrate-down:
+ @go run cmd/migrate/main.go down
+
+dev-migrate-status:
+ @go run cmd/migrate/main.go status
+
+# 交叉编译(Linux)
+build-linux:
+ @echo "交叉编译 Linux 版本..."
+ @mkdir -p bin
+ @GOOS=linux GOARCH=amd64 go build -o bin/server-linux cmd/server/main.go
+ @GOOS=linux GOARCH=amd64 go build -o bin/migrate-linux cmd/migrate/main.go
+ @echo "✓ Linux 版本编译完成"
+
diff --git a/templates/README.md b/templates/README.md
new file mode 100644
index 0000000..64f7c8b
--- /dev/null
+++ b/templates/README.md
@@ -0,0 +1,40 @@
+# 模板文件
+
+这个目录包含了可以直接复制到你项目中使用的模板文件。
+
+## 包含的模板
+
+- `migrate/main.go` - 数据库迁移工具模板 ⭐
+- `Dockerfile.example` - Docker 构建示例
+- `docker-compose.example.yml` - Docker Compose 示例
+- `Makefile.example` - Makefile 常用命令示例
+
+## 快速使用
+
+### 迁移工具模板
+
+```bash
+# 1. 复制到你的项目
+mkdir -p cmd/migrate
+cp templates/migrate/main.go cmd/migrate/
+
+# 2. 编译
+go build -o bin/migrate cmd/migrate/main.go
+
+# 3. 使用
+./bin/migrate up
+./bin/migrate -help
+```
+
+### Docker 模板
+
+```bash
+# 复制到你的项目根目录
+cp templates/Dockerfile.example Dockerfile
+cp templates/docker-compose.example.yml docker-compose.yml
+cp templates/Makefile.example Makefile
+```
+
+## 完整文档
+
+详细使用说明请查看:[MIGRATION.md](../MIGRATION.md)
diff --git a/templates/docker-compose.example.yml b/templates/docker-compose.example.yml
new file mode 100644
index 0000000..939777f
--- /dev/null
+++ b/templates/docker-compose.example.yml
@@ -0,0 +1,22 @@
+# docker-compose.yml 示例
+# 展示如何在 docker-compose 中使用迁移工具
+
+version: '3.8'
+
+services:
+ app:
+ build: .
+ ports:
+ - "8080:8080"
+ volumes:
+ # 挂载配置文件(推荐:修改配置无需重启容器)
+ - ./config.json:/app/config.json:ro
+ # 启动时先执行迁移,再启动应用
+ command: sh -c "./migrate up && ./server"
+
+# 使用说明:
+# 1. 将此配置添加到你的 docker-compose.yml 中
+# 2. 确保你的配置文件(config.json)包含数据库连接信息
+# 3. 修改配置后,手动执行迁移:docker-compose exec app ./migrate up
+# 4. 无需重启容器!
+
diff --git a/templates/migrate/main.go b/templates/migrate/main.go
new file mode 100644
index 0000000..5399322
--- /dev/null
+++ b/templates/migrate/main.go
@@ -0,0 +1,147 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "git.toowon.com/jimmy/go-common/migration"
+)
+
+// 数据库迁移工具(黑盒模式)
+//
+// 工作原理:
+// 此工具调用 migration.RunMigrationsFromConfigWithCommand() 方法,
+// 内部自动处理配置加载、数据库连接、迁移执行等所有细节。
+// 你只需要提供配置文件和SQL迁移文件即可。
+//
+// 使用方式:
+// 基本用法:
+// ./migrate up # 使用默认配置
+// ./migrate up -config /path/to/config.json # 指定配置文件
+// ./migrate up -config config.json -dir db/migrations # 指定配置和迁移目录
+// ./migrate status # 查看迁移状态
+// ./migrate down # 回滚最后一个迁移
+//
+// Docker 中使用:
+// # 方式1:挂载配置文件
+// docker run -v /host/config.json:/app/config.json myapp ./migrate up
+//
+// # 方式2:使用环境变量
+// docker run -e DATABASE_URL="mysql://..." myapp ./migrate up
+//
+// # 方式3:指定容器内的配置文件路径
+// docker run myapp ./migrate up -config /etc/app/config.json
+//
+// 支持的命令:
+// up - 执行所有待执行的迁移
+// down - 回滚最后一个迁移
+// status - 查看迁移状态
+//
+// 配置优先级(从高到低):
+// 1. 命令行参数 -config 和 -dir
+// 2. 环境变量 CONFIG_FILE 和 MIGRATIONS_DIR
+// 3. 环境变量 DATABASE_URL(直接连接,无需配置文件)
+// 4. 默认值(config.json 和 migrations)
+
+var (
+ configFile string
+ migrationsDir string
+ showHelp bool
+)
+
+func init() {
+ flag.StringVar(&configFile, "config", "", "配置文件路径(默认:config.json 或环境变量 CONFIG_FILE)")
+ flag.StringVar(&configFile, "c", "", "配置文件路径(简写)")
+ flag.StringVar(&migrationsDir, "dir", "", "迁移文件目录(默认:migrations 或环境变量 MIGRATIONS_DIR)")
+ flag.StringVar(&migrationsDir, "d", "", "迁移文件目录(简写)")
+ flag.BoolVar(&showHelp, "help", false, "显示帮助信息")
+ flag.BoolVar(&showHelp, "h", false, "显示帮助信息(简写)")
+}
+
+func main() {
+ flag.Parse()
+
+ // 显示帮助
+ if showHelp {
+ printHelp()
+ os.Exit(0)
+ }
+
+ // 获取命令(默认up)
+ command := "up"
+ args := flag.Args()
+ if len(args) > 0 {
+ command = args[0]
+ }
+
+ // 验证命令
+ if command != "up" && command != "down" && command != "status" {
+ fmt.Fprintf(os.Stderr, "错误:未知命令 '%s'\n\n", command)
+ printHelp()
+ os.Exit(1)
+ }
+
+ // 获取配置文件路径(优先级:命令行 > 环境变量 > 默认值)
+ if configFile == "" {
+ configFile = getEnv("CONFIG_FILE", "config.json")
+ }
+
+ // 获取迁移目录(优先级:命令行 > 环境变量 > 默认值)
+ if migrationsDir == "" {
+ migrationsDir = getEnv("MIGRATIONS_DIR", "migrations")
+ }
+
+ // 执行迁移
+ if err := migration.RunMigrationsFromConfigWithCommand(configFile, migrationsDir, command); err != nil {
+ fmt.Fprintf(os.Stderr, "错误: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func getEnv(key, defaultValue string) string {
+ if value := os.Getenv(key); value != "" {
+ return value
+ }
+ return defaultValue
+}
+
+func printHelp() {
+ fmt.Println("数据库迁移工具")
+ fmt.Println()
+ fmt.Println("用法:")
+ fmt.Println(" migrate [命令] [选项]")
+ fmt.Println()
+ fmt.Println("命令:")
+ fmt.Println(" up 执行所有待执行的迁移(默认)")
+ fmt.Println(" down 回滚最后一个迁移")
+ fmt.Println(" status 查看迁移状态")
+ fmt.Println()
+ fmt.Println("选项:")
+ fmt.Println(" -config, -c 配置文件路径(默认: config.json)")
+ fmt.Println(" -dir, -d 迁移文件目录(默认: migrations)")
+ fmt.Println(" -help, -h 显示帮助信息")
+ fmt.Println()
+ fmt.Println("示例:")
+ fmt.Println(" # 使用默认配置")
+ fmt.Println(" migrate up")
+ fmt.Println()
+ fmt.Println(" # 指定配置文件")
+ fmt.Println(" migrate up -config /etc/app/config.json")
+ fmt.Println()
+ fmt.Println(" # 指定配置和迁移目录")
+ fmt.Println(" migrate up -c config.json -d db/migrations")
+ fmt.Println()
+ fmt.Println(" # 使用环境变量")
+ fmt.Println(" DATABASE_URL='mysql://...' migrate up")
+ fmt.Println()
+ fmt.Println(" # Docker 中使用")
+ fmt.Println(" docker run -v /host/config.json:/app/config.json myapp migrate up")
+ fmt.Println()
+ fmt.Println("配置优先级(从高到低):")
+ fmt.Println(" 1. 命令行参数 -config 和 -dir")
+ fmt.Println(" 2. 环境变量 CONFIG_FILE 和 MIGRATIONS_DIR")
+ fmt.Println(" 3. 环境变量 DATABASE_URL")
+ fmt.Println(" 4. 默认值(config.json 和 migrations)")
+}
+