From 0650feb0d217e142802c5ae34e29beba4297dd4b Mon Sep 17 00:00:00 2001 From: Jimmy Xue Date: Thu, 4 Dec 2025 22:30:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=B7=A5=E5=85=B7=E7=B1=BB?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E6=B3=95=EF=BC=8C=E4=BC=98=E5=8C=96=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E8=B0=83=E7=94=A8=E5=8F=8A=E5=A2=9E=E5=8A=A0=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=B7=A5=E5=85=B7=E5=8F=8A=E5=85=B6=E7=94=A8=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MIGRATION.md | 583 +++++++++++++++++ QUICKSTART.md | 310 +++++++++ README.md | 406 +++++++++++- config/config.go | 21 +- docs/README.md | 56 +- docs/config.md | 36 +- docs/middleware.md | 617 +++++++++++++++++- docs/storage.md | 14 +- examples/config_example.go | 23 +- examples/middleware_example.go | 66 -- examples/middleware_full_example.go | 154 +++++ examples/middleware_ratelimit_example.go | 89 +++ examples/middleware_simple_example.go | 42 ++ examples/migrations/README.md | 134 ++++ ...20240101000001_create_users_table.down.sql | 4 + .../20240101000001_create_users_table.sql | 14 + examples/storage_example.go | 14 +- logger/logger.go | 104 +++ middleware/cors.go | 30 + middleware/logging.go | 221 +++++++ middleware/ratelimit.go | 286 ++++++++ middleware/recovery.go | 149 +++++ migration/helper.go | 216 ++++++ templates/Dockerfile.example | 39 ++ templates/Makefile.example | 78 +++ templates/README.md | 40 ++ templates/docker-compose.example.yml | 22 + templates/migrate/main.go | 147 +++++ 28 files changed, 3753 insertions(+), 162 deletions(-) create mode 100644 MIGRATION.md create mode 100644 QUICKSTART.md delete mode 100644 examples/middleware_example.go create mode 100644 examples/middleware_full_example.go create mode 100644 examples/middleware_ratelimit_example.go create mode 100644 examples/middleware_simple_example.go create mode 100644 examples/migrations/README.md create mode 100644 examples/migrations/migrations/20240101000001_create_users_table.down.sql create mode 100644 examples/migrations/migrations/20240101000001_create_users_table.sql create mode 100644 middleware/logging.go create mode 100644 middleware/ratelimit.go create mode 100644 middleware/recovery.go create mode 100644 migration/helper.go create mode 100644 templates/Dockerfile.example create mode 100644 templates/Makefile.example create mode 100644 templates/README.md create mode 100644 templates/docker-compose.example.yml create mode 100644 templates/migrate/main.go 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)") +} +