diff --git a/.env.example b/.env.example index 77b4a28..5a2418e 100644 --- a/.env.example +++ b/.env.example @@ -17,4 +17,4 @@ POSTGRES_SSLMODE=disable # JWT Configuration JWT_SECRET=your-secret-key-here-change-in-production -JWT_EXPIRE=7200 \ No newline at end of file +JWT_EXPIRE=7200 diff --git a/README.md b/README.md index 015b4e8..69bcbae 100644 --- a/README.md +++ b/README.md @@ -111,26 +111,64 @@ go run main.go 服务器将在 http://localhost:3000 上运行。 -### 5. 环境变量配置 +### 5. 配置管理 -项目支持以下环境变量(按优先级排序): +项目采用 **YAML 结构化管理 + 环境变量动态覆盖** 的方案: -| 变量名 | 说明 | 默认值 | -|--------|------|--------| -| `PORT` | 服务器端口 | 3000 | -| `ENVIRONMENT` | 运行环境 (development/production) | development | -| `JWT_SECRET` | JWT 签名密钥 | your-secret-key-here-change-in-production | -| `JWT_EXPIRE` | JWT 过期时间(秒) | 7200 | -| `DATABASE_DRIVER` | 数据库驱动 (sqlite/postgres) | sqlite | -| `DATABASE_PATH` | SQLite 数据库路径 | ./data/database.sqlite | -| `POSTGRES_HOST` | PostgreSQL 主机 | localhost | -| `POSTGRES_PORT` | PostgreSQL 端口 | 5432 | -| `POSTGRES_USER` | PostgreSQL 用户名 | trace | -| `POSTGRES_PASSWORD` | PostgreSQL 密码 | trace123 | -| `POSTGRES_DB` | PostgreSQL 数据库名 | trace | -| `POSTGRES_SSLMODE` | PostgreSQL SSL 模式 | disable | +#### 配置文件优先级(从高到低) -**注意**: 环境变量也可以通过 `.env` 文件设置。 +1. **环境变量** (最高优先级) - 格式:`APP_配置项` +2. **.env 文件** - 本地开发环境配置 +3. **config.yaml** - 默认配置文件 +4. **内置默认值** (最低优先级) + +#### 环境变量命名规则 + +所有环境变量使用 `APP_` 前缀,多级配置使用 `_` 连接: + +```bash +# 示例 +APP_SERVER_PORT=8080 # 覆盖 server.port +APP_DATABASE_DRIVER=postgres # 覆盖 database.driver +APP_DATABASE_POSTGRES_HOST=db.com # 覆盖 database.postgres.host +APP_JWT_SECRET=my-secret-key # 覆盖 jwt.secret +``` + +#### 常用配置项 + +| 环境变量 | 对应配置项 | 说明 | 默认值 | +|---------|-----------|------|--------| +| `APP_SERVER_PORT` | server.port | 服务器端口 | 3000 | +| `APP_SERVER_ENVIRONMENT` | server.environment | 运行环境 | development | +| `APP_DATABASE_DRIVER` | database.driver | 数据库驱动 | sqlite | +| `APP_DATABASE_SQLITE_PATH` | database.sqlite.path | SQLite 路径 | ./data/database.sqlite | +| `APP_DATABASE_POSTGRES_HOST` | database.postgres.host | PostgreSQL 主机 | localhost | +| `APP_DATABASE_POSTGRES_PORT` | database.postgres.port | PostgreSQL 端口 | 5432 | +| `APP_DATABASE_POSTGRES_USER` | database.postgres.user | PostgreSQL 用户名 | trace | +| `APP_DATABASE_POSTGRES_PASSWORD` | database.postgres.password | PostgreSQL 密码 | trace123 | +| `APP_DATABASE_POSTGRES_DBNAME` | database.postgres.dbname | PostgreSQL 数据库名 | trace | +| `APP_JWT_SECRET` | jwt.secret | JWT 签名密钥 | your-secret-key... | +| `APP_JWT_EXPIRE` | jwt.expire | JWT 过期时间(秒) | 7200 | + +#### 快速配置示例 + +**开发环境(.env)**: +```bash +# 使用 SQLite +DATABASE_DRIVER=sqlite +DATABASE_PATH=./data/dev.sqlite +``` + +**生产环境(环境变量)**: +```bash +# 使用 PostgreSQL +export APP_SERVER_PORT=8080 +export APP_SERVER_ENVIRONMENT=production +export APP_DATABASE_DRIVER=postgres +export APP_DATABASE_POSTGRES_HOST=prod-db.example.com +export APP_DATABASE_POSTGRES_PASSWORD=secure-password +export APP_JWT_SECRET=your-production-secret-key +``` ### 6. 测试 API @@ -243,33 +281,29 @@ CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o trace-backend ### SQLite 到 PostgreSQL 的迁移 -1. 修改配置文件 `config.yaml` 中的数据库驱动: - -```yaml -database: - driver: postgres - postgres: - host: localhost - port: 5432 - user: trace - password: trace123 - dbname: trace - sslmode: disable -``` - -2. 或者使用环境变量: +修改 `.env` 文件中的数据库配置: + +```bash +# 将数据库驱动改为 postgres +DATABASE_DRIVER=postgres + +# PostgreSQL 配置 +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=trace +POSTGRES_PASSWORD=trace123 +POSTGRES_DB=trace +POSTGRES_SSLMODE=disable +``` + +然后重新启动应用即可: ```bash -DATABASE_DRIVER=postgres \ -POSTGRES_HOST=localhost \ -POSTGRES_PORT=5432 \ -POSTGRES_USER=trace \ -POSTGRES_PASSWORD=trace123 \ -POSTGRES_DB=trace \ -POSTGRES_SSLMODE=disable \ ./trace-backend ``` +**注意**: 切换数据库后,原有 SQLite 中的数据不会自动迁移到 PostgreSQL。需要使用数据库迁移工具(如 pgloader)手动迁移数据。 + ## 贡献指南 1. 克隆项目 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..b5a2af4 --- /dev/null +++ b/config.yaml @@ -0,0 +1,22 @@ +# 服务器配置 +server: + port: "3000" + environment: "development" + +# 数据库配置 +database: + driver: "sqlite" # 可选: sqlite, postgres + sqlite: + path: "./data/database.sqlite" + postgres: + host: "localhost" + port: "5432" + user: "trace" + password: "trace123" + dbname: "trace" + sslmode: "disable" + +# JWT 配置 +jwt: + secret: "your-secret-key-here-change-in-production" + expire: 7200 # 过期时间(秒) diff --git a/config/config.go b/config/config.go index 4057789..afa03c3 100644 --- a/config/config.go +++ b/config/config.go @@ -2,9 +2,11 @@ package config import ( "fmt" + "os" + "strings" + "github.com/joho/godotenv" "github.com/spf13/viper" - "github.com/subosito/gotenv" ) // ServerConfig 服务器配置 @@ -15,18 +17,24 @@ type ServerConfig struct { // DatabaseConfig 数据库配置 type DatabaseConfig struct { - Driver string `mapstructure:"driver"` - SQLite struct { - Path string `mapstructure:"path"` - } `mapstructure:"sqlite"` - Postgres struct { - Host string `mapstructure:"host"` - Port string `mapstructure:"port"` - User string `mapstructure:"user"` - Password string `mapstructure:"password"` - DBName string `mapstructure:"dbname"` - SSLMode string `mapstructure:"sslmode"` - } `mapstructure:"postgres"` + Driver string `mapstructure:"driver"` + SQLite SQLiteConfig `mapstructure:"sqlite"` + Postgres PostgresConfig `mapstructure:"postgres"` +} + +// SQLiteConfig SQLite 配置 +type SQLiteConfig struct { + Path string `mapstructure:"path"` +} + +// PostgresConfig PostgreSQL 配置 +type PostgresConfig struct { + Host string `mapstructure:"host"` + Port string `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + DBName string `mapstructure:"dbname"` + SSLMode string `mapstructure:"sslmode"` } // JWTConfig JWT 配置 @@ -46,29 +54,70 @@ type AppConfig struct { var appConfig AppConfig // LoadConfig 加载配置 +// 优先级:环境变量 > .env 文件 > config.yaml > 默认值 func LoadConfig() { - // 使用 gotenv 加载 .env 文件 - if err := gotenv.Load(); err != nil { - fmt.Printf("警告: 未找到 .env 文件: %v\n", err) - } + // 设置默认值(最先设置,优先级最低) + setDefaults() - // 配置 Viper 以读取 YAML 配置文件 + // 1. 加载 config.yaml 配置文件 viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") viper.AddConfigPath("./config") - viper.AddConfigPath("../") - if err := viper.MergeInConfig(); err != nil { - fmt.Printf("警告: 未找到配置文件,使用默认值: %v\n", err) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + fmt.Println("提示: 未找到 config.yaml,使用默认配置") + } else { + fmt.Printf("错误: 读取配置文件失败: %v\n", err) + } + } else { + fmt.Printf("已加载配置文件: %s\n", viper.ConfigFileUsed()) } - // 环境变量前缀 - viper.SetEnvPrefix("TRACE") + // 2. 加载 .env 文件(如果存在) + // .env 文件中的变量会被加载到环境变量中 + if err := godotenv.Load(); err != nil { + // .env 文件是可选的,不存在不报错 + if os.IsNotExist(err) { + fmt.Println("提示: 未找到 .env 文件,将使用环境变量或默认值") + } else { + fmt.Printf("警告: 加载 .env 文件失败: %v\n", err) + } + } else { + fmt.Println("已加载 .env 文件") + } + + // 3. 设置环境变量支持 + // 设置环境变量前缀为 APP_ + viper.SetEnvPrefix("APP") + // 设置环境变量键名替换规则(将 _ 替换为 .) + // 例如:APP_SERVER_PORT 会映射到 server.port + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + // 启用自动环境变量读取 viper.AutomaticEnv() - // 默认配置 + // 4. 绑定特定的环境变量(确保嵌套结构体能正确映射) + bindEnvVariables() + + // 解析配置到结构体 + if err := viper.Unmarshal(&appConfig); err != nil { + fmt.Printf("错误: 配置解析失败: %v\n", err) + } + + // 验证配置 + validateConfig() + + // 打印配置信息(开发环境) + printConfig() +} + +// setDefaults 设置默认配置值 +func setDefaults() { + // Server 默认值 viper.SetDefault("server.port", "3000") viper.SetDefault("server.environment", "development") + + // Database 默认值 viper.SetDefault("database.driver", "sqlite") viper.SetDefault("database.sqlite.path", "./data/database.sqlite") viper.SetDefault("database.postgres.host", "localhost") @@ -77,36 +126,69 @@ func LoadConfig() { viper.SetDefault("database.postgres.password", "trace123") viper.SetDefault("database.postgres.dbname", "trace") viper.SetDefault("database.postgres.sslmode", "disable") + + // JWT 默认值 viper.SetDefault("jwt.secret", "your-secret-key-here-change-in-production") viper.SetDefault("jwt.expire", 7200) +} - // 绑定环境变量(支持无前缀的 .env 文件) - viper.BindEnv("server.port", "PORT", "TRACE_SERVER_PORT") - viper.BindEnv("server.environment", "ENVIRONMENT", "TRACE_SERVER_ENVIRONMENT") - viper.BindEnv("jwt.secret", "JWT_SECRET", "TRACE_JWT_SECRET") - viper.BindEnv("jwt.expire", "JWT_EXPIRE", "TRACE_JWT_EXPIRE") - viper.BindEnv("database.driver", "DATABASE_DRIVER", "TRACE_DATABASE_DRIVER") - viper.BindEnv("database.sqlite.path", "DATABASE_PATH", "TRACE_DATABASE_SQLITE_PATH") - viper.BindEnv("database.postgres.host", "POSTGRES_HOST", "TRACE_DATABASE_POSTGRES_HOST") - viper.BindEnv("database.postgres.port", "POSTGRES_PORT", "TRACE_DATABASE_POSTGRES_PORT") - viper.BindEnv("database.postgres.user", "POSTGRES_USER", "TRACE_DATABASE_POSTGRES_USER") - viper.BindEnv("database.postgres.password", "POSTGRES_PASSWORD", "TRACE_DATABASE_POSTGRES_PASSWORD") - viper.BindEnv("database.postgres.dbname", "POSTGRES_DB", "TRACE_DATABASE_POSTGRES_DBNAME") - viper.BindEnv("database.postgres.sslmode", "POSTGRES_SSLMODE", "TRACE_DATABASE_POSTGRES_SSLMODE") +// bindEnvVariables 绑定环境变量 +// 支持以下格式: +// - APP_SERVER_PORT=8080 (映射到 server.port) +// - APP_DATABASE_DRIVER=postgres (映射到 database.driver) +// - APP_DATABASE_POSTGRES_HOST=db.example.com (映射到 database.postgres.host) +func bindEnvVariables() { + // 服务器配置 + viper.BindEnv("server.port") + viper.BindEnv("server.environment") - // 解析配置 - if err := viper.Unmarshal(&appConfig); err != nil { - fmt.Printf("配置解析失败: %v\n", err) - } + // 数据库配置 + viper.BindEnv("database.driver") + viper.BindEnv("database.sqlite.path") + viper.BindEnv("database.postgres.host") + viper.BindEnv("database.postgres.port") + viper.BindEnv("database.postgres.user") + viper.BindEnv("database.postgres.password") + viper.BindEnv("database.postgres.dbname") + viper.BindEnv("database.postgres.sslmode") + // JWT 配置 + viper.BindEnv("jwt.secret") + viper.BindEnv("jwt.expire") +} + +// validateConfig 验证配置 +func validateConfig() { // 验证 JWT 密钥 if appConfig.JWT.Secret == "your-secret-key-here-change-in-production" { - fmt.Println("警告: 使用默认 JWT 密钥,请在生产环境中设置 JWT_SECRET 环境变量") + if appConfig.Server.Environment == "production" { + fmt.Println("警告: 生产环境使用了默认 JWT 密钥,请设置 APP_JWT_SECRET 环境变量") + } else { + fmt.Println("提示: 使用默认 JWT 密钥(仅适用于开发环境)") + } } - // 调试打印 - fmt.Printf("加载的配置 - 环境: %s\n", appConfig.Server.Environment) - fmt.Printf("加载的配置 - 数据库驱动: %s\n", appConfig.Database.Driver) + // 验证端口 + if appConfig.Server.Port == "" { + appConfig.Server.Port = "3000" + } +} + +// printConfig 打印配置信息(仅开发环境) +func printConfig() { + fmt.Printf("配置加载完成:\n") + fmt.Printf(" 环境: %s\n", appConfig.Server.Environment) + fmt.Printf(" 端口: %s\n", appConfig.Server.Port) + fmt.Printf(" 数据库驱动: %s\n", appConfig.Database.Driver) + + if appConfig.Database.Driver == "sqlite" { + fmt.Printf(" SQLite 路径: %s\n", appConfig.Database.SQLite.Path) + } else if appConfig.Database.Driver == "postgres" { + fmt.Printf(" PostgreSQL: %s:%s/%s\n", + appConfig.Database.Postgres.Host, + appConfig.Database.Postgres.Port, + appConfig.Database.Postgres.DBName) + } } // GetAppConfig 获取应用程序配置 diff --git a/go.mod b/go.mod index c1ad3cc..3ae6420 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,11 @@ go 1.26 require ( github.com/gin-gonic/gin v1.11.0 github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.22 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - github.com/subosito/gotenv v1.6.0 github.com/yeqown/go-qrcode/v2 v2.2.5 github.com/yeqown/go-qrcode/writer/standard v1.3.0 go.uber.org/zap v1.27.1 @@ -48,7 +49,6 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect @@ -67,6 +67,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/multierr v1.10.0 // indirect diff --git a/go.sum b/go.sum index a1f5be9..471384d 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=