feat(config): implement layered configuration using config.yaml and .env
This commit is contained in:
@@ -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
112
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. 克隆项目
|
||||
|
||||
22
config.yaml
Normal file
22
config.yaml
Normal 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 # 过期时间(秒)
|
||||
172
config/config.go
172
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 获取应用程序配置
|
||||
|
||||
5
go.mod
5
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
Reference in New Issue
Block a user