feat(config): implement layered configuration using config.yaml and .env

This commit is contained in:
2026-02-12 14:41:49 +08:00
parent e01cdc9889
commit aed996f409
6 changed files with 228 additions and 87 deletions

View File

@@ -17,4 +17,4 @@ POSTGRES_SSLMODE=disable
# JWT Configuration
JWT_SECRET=your-secret-key-here-change-in-production
JWT_EXPIRE=7200
JWT_EXPIRE=7200

112
README.md
View File

@@ -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. 克隆项目

22
config.yaml Normal file
View File

@@ -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 # 过期时间(秒)

View File

@@ -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 获取应用程序配置

5
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=