522 lines
20 KiB
Markdown
522 lines
20 KiB
Markdown
# GoChat 阶段二 P0-6:环境配置管理改进方案
|
||
|
||
## 一、现状分析
|
||
|
||
### 1.1 配置文件清单
|
||
|
||
| 文件 | 位置 | 说明 |
|
||
|------|------|------|
|
||
| config.go | `internal/config/config.go` | 配置结构体定义 + Load() 函数 |
|
||
| validator.go | `internal/config/validator.go` | 配置校验逻辑 |
|
||
| config_test.go | `internal/config/config_test.go` | 配置校验测试 |
|
||
| config.yaml | `configs/config.yaml` | 主配置文件(开发默认) |
|
||
| config.dev.yaml | `configs/config.dev.yaml` | 开发环境覆盖 |
|
||
| config.prod.yaml | `configs/config.prod.yaml` | 生产环境覆盖 |
|
||
| .env.example | `.env.example` | 114行,环境变量模板 |
|
||
| prometheus_alerts.yml | `configs/prometheus_alerts.yml` | 监控告警规则 |
|
||
| fluentd.conf | `configs/fluentd.conf` | 日志收集配置 |
|
||
| redis.conf | `configs/redis.conf` | Redis 服务配置 |
|
||
|
||
### 1.2 当前 Viper 使用方式
|
||
|
||
`Load()` 函数(`config.go:210-255`)的配置加载流程:
|
||
|
||
```
|
||
1. viper.SetConfigName("config") → 固定文件名 "config"
|
||
2. viper.SetConfigType("yaml") → YAML格式
|
||
3. viper.AddConfigPath("./configs") → 搜索路径1
|
||
4. viper.AddConfigPath("./") → 搜索路径2
|
||
5. viper.AddConfigPath("/etc/gochat/") → 搜索路径3
|
||
6. viper.SetEnvPrefix("GOCHAT") → 环境变量前缀 GOCHAT_
|
||
7. viper.AutomaticEnv() → 自动映射环境变量
|
||
8. viper.SetDefault(...) → 设置默认值(rate_limit + saml)
|
||
9. viper.ReadInConfig() → 读取配置文件
|
||
10. viper.Unmarshal(&cfg) → 反序列化到结构体
|
||
11. 手动补零值默认 → RateLimit零值兜底
|
||
```
|
||
|
||
### 1.3 配置结构体(13个)
|
||
|
||
```go
|
||
Config → {
|
||
Server ServerConfig (host, port, mode, cors)
|
||
Database DatabaseConfig (host, port, user, password, dbname/Name, sslmode, 连接池参数, migration)
|
||
Redis RedisConfig (host, port, password, db, url)
|
||
JWT JWTConfig (secret, expiry, refresh, access, audience, issuer)
|
||
Log LogConfig (level, format)
|
||
Captain CaptainConfig (enabled, llm_provider, llm_model, llm_api_key, llm_base_url, embedding, max_tokens, temperature)
|
||
Worker WorkerConfig (concurrency)
|
||
OAuth OAuthConfig (google, github → OAuthProviderConfig)
|
||
RateLimit RateLimitConfig (enabled, requests_per_minute, window_seconds)
|
||
SAML SAMLConfig (enabled, idp_metadata_url, idp_metadata_xml, sp_entity_id, acs_url, cert/key paths, attribute_map, clock_drift_tolerance)
|
||
}
|
||
+ LLMConfig(从CaptainConfig派生)
|
||
```
|
||
|
||
### 1.4 多环境配置现状
|
||
|
||
已有 `config.dev.yaml` 和 `config.prod.yaml`,但 **未被 Load() 函数使用**!Load 只读取单一的 `config.yaml`,没有环境合并逻辑。
|
||
|
||
docker-compose 文件中设置 `GOCHAT_ENV=development/production/test`,但这个变量仅用于 Docker 容器标识,**不被 Viper 用于选择环境配置文件**。
|
||
|
||
Helm values 中有 `values-staging.yaml`,设置了 `GOCHAT_ENV: "staging"`,但同样只是 ConfigMap 中的标识变量。
|
||
|
||
### 1.5 环境变量命名问题
|
||
|
||
**重大缺陷**:`.env.example` 中存在命名不一致:
|
||
|
||
| .env.example 变量 | Viper期望的变量名(GOCHAT_前缀+自动映射) | 问题 |
|
||
|---|---|---|
|
||
| `POSTGRES_HOST` | `GOCHAT_DATABASE_HOST` | ❌ 前缀不一致 |
|
||
| `POSTGRES_PORT` | `GOCHAT_DATABASE_PORT` | ❌ 前缀不一致 |
|
||
| `POSTGRES_USER` | `GOCHAT_DATABASE_USER` | ❌ 前缀不一致 |
|
||
| `POSTGRES_PASSWORD` | `GOCHAT_DATABASE_PASSWORD` | ❌ 前缀不一致 |
|
||
| `POSTGRES_DB` | `GOCHAT_DATABASE_DBNAME` | ❌ 前缀+键名不一致 |
|
||
| `POSTGRES_SSLMODE` | `GOCHAT_DATABASE_SSLMODE` | ❌ 前缀不一致 |
|
||
| `REDIS_HOST` | `GOCHAT_REDIS_HOST` | ❌ 前缀不一致 |
|
||
| `REDIS_PORT` | `GOCHAT_REDIS_PORT` | ❌ 前缀不一致 |
|
||
| `REDIS_PASSWORD` | `GOCHAT_REDIS_PASSWORD` | ❌ 前缀不一致 |
|
||
| `REDIS_DB` | `GOCHAT_REDIS_DB` | ❌ 前缀不一致 |
|
||
| `SMTP_*` | `GOCHAT_SMTP_*`(SMTP未在Config结构体中) | ❌ 缺少SMTP配置 |
|
||
| `FRONTEND_URL` | `GOCHAT_FRONTEND_URL`(未在Config结构体中) | ❌ 缺少Frontend配置 |
|
||
| `OPENAI_API_KEY` | `GOCHAT_CAPTAIN_LLM_API_KEY` | ❌ 前缀不一致 |
|
||
| `GOCHAT_METRICS_*` | `GOCHAT_METRICS_*`(Metrics未在Config结构体中) | ❌ 缺少Metrics配置 |
|
||
| `GOCHAT_WS_*` | `GOCHAT_WS_*`(WebSocket未在Config结构体中) | ❌ 缺少WebSocket配置 |
|
||
| `GOCHAT_ANALYTICS_*` | `GOCHAT_ANALYTICS_*`(Analytics未在Config结构体中) | ❌ 缺少Analytics配置 |
|
||
| `GOCHAT_FEATURE_*` | `GOCHAT_FEATURE_*`(FeatureFlags未在Config结构体中) | ❌ 缺少FeatureFlags配置 |
|
||
|
||
**核心问题**:`viper.AutomaticEnv()` + `viper.SetEnvPrefix("GOCHAT")` 只能自动映射 `GOCHAT_` 前缀的环境变量到嵌套键。没有 `EnvKeyReplacer` 设置(`.` → `_` 的映射),也没有 `BindEnv()` 显式绑定非标准命名的变量。所以大量 `POSTGRES_*`、`REDIS_*`、`OPENAI_*` 等环境变量**实际上不会被 Viper 读取**!
|
||
|
||
### 1.6 其他问题汇总
|
||
|
||
| 问题 | 说明 |
|
||
|------|------|
|
||
| 无环境选择逻辑 | config.dev.yaml/config.prod.yaml 存在但未被使用 |
|
||
| 无配置热加载 | 没有 WatchConfig/OnConfigChange |
|
||
| 无 .env 文件加载 | Viper 不自动读取 .env 文件,需 godotenv 或手动处理 |
|
||
| 环境变量映射断裂 | POSTGRES_*/REDIS_* 等前缀与 GOCHAT_ 不匹配,AutomaticEnv无法映射 |
|
||
| 无 EnvKeyReplacer | 未设置 viper.RegisterAlias 或 EnvKeyReplacer 来处理嵌套键 |
|
||
| 缺少多个配置结构体 | SMTP/Frontend/Metrics/WebSocket/Analytics/FeatureFlags/SMTP 在.env中但不在Config结构体中 |
|
||
| DatabaseConfig 双字段 | Name 和 DBName 同时存在,语义混乱 |
|
||
| 无配置版本号 | 配置文件没有版本标记,难以追踪兼容性变化 |
|
||
| 无 staging 环境文件 | 只有 dev/prod,缺少 config.staging.yaml |
|
||
| 配置默认值分散 | 部分在 viper.SetDefault,部分在 YAML,部分在代码中硬编码兜底 |
|
||
|
||
---
|
||
|
||
## 二、改进方案
|
||
|
||
### 2.1 配置加载优先级(从低到高)
|
||
|
||
```
|
||
优先级(低→高,后者覆盖前者):
|
||
|
||
1. 代码默认值(viper.SetDefault)
|
||
2. 基础配置文件(configs/config.yaml)
|
||
3. 环境配置文件(configs/config.{env}.yaml)
|
||
4. 本地覆盖文件(configs/config.local.yaml,不提交到git)
|
||
5. 环境变量(GOCHAT_ 前缀)
|
||
6. 命令行参数(--server.port 等,可选)
|
||
```
|
||
|
||
### 2.2 多环境配置方案
|
||
|
||
#### 环境选择逻辑
|
||
|
||
通过 `GOCHAT_ENV` 环境变量选择环境(默认 `development`):
|
||
|
||
```
|
||
GOCHAT_ENV=development → 加载 config.yaml + config.dev.yaml
|
||
GOCHAT_ENV=staging → 加载 config.yaml + config.staging.yaml
|
||
GOCHAT_ENV=production → 加载 config.yaml + config.prod.yaml
|
||
GOCHAT_ENV=test → 加载 config.yaml(无环境覆盖,测试用单独配置)
|
||
```
|
||
|
||
#### configs/ 目录结构
|
||
|
||
```
|
||
configs/
|
||
├── config.yaml # 基础配置(所有环境共享)
|
||
├── config.dev.yaml # 开发环境覆盖(已存在,需完善)
|
||
├── config.staging.yaml # 预发布环境覆盖(新增)
|
||
├── config.prod.yaml # 生产环境覆盖(已存在,需完善)
|
||
├── config.local.yaml # 本地开发覆盖(新增,加入.gitignore)
|
||
├── config.test.yaml # 测试环境覆盖(新增)
|
||
├── prometheus_alerts.yml # 监控告警(保持不变)
|
||
├── fluentd.conf # 日志收集(保持不变)
|
||
└── redis.conf # Redis配置(保持不变)
|
||
```
|
||
|
||
#### Load() 函数改进伪代码
|
||
|
||
```go
|
||
func Load() (*Config, error) {
|
||
// 1. 设置基础配置
|
||
viper.SetConfigName("config")
|
||
viper.SetConfigType("yaml")
|
||
viper.AddConfigPath("./configs")
|
||
viper.AddConfigPath("./")
|
||
viper.AddConfigPath("/etc/gochat/")
|
||
|
||
// 2. 环境变量映射
|
||
viper.SetEnvPrefix("GOCHAT")
|
||
viper.AutomaticEnv()
|
||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||
|
||
// 3. 显式绑定非标准环境变量(兼容旧命名)
|
||
bindLegacyEnvVars() // POSTGRES_* → database.*, REDIS_* → redis.*
|
||
|
||
// 4. 设置默认值(集中管理)
|
||
setDefaults()
|
||
|
||
// 5. 读取基础配置
|
||
if err := viper.ReadInConfig(); err != nil {
|
||
return nil, fmt.Errorf("read base config: %w", err)
|
||
}
|
||
|
||
// 6. 合理环境配置
|
||
env := viper.GetString("env") // 从 GOCHAT_ENV 或 config.yaml 读取
|
||
if env == "" {
|
||
env = "development"
|
||
}
|
||
envFile := fmt.Sprintf("config.%s", env)
|
||
viper.SetConfigName(envFile)
|
||
if err := viper.MergeInConfig(); err != nil {
|
||
// 环境配置可选,不存在时不报错
|
||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||
return nil, fmt.Errorf("merge env config: %w", err)
|
||
}
|
||
}
|
||
|
||
// 7. 合并本地覆盖(可选,开发用)
|
||
viper.SetConfigName("config.local")
|
||
if err := viper.MergeInConfig(); err != nil {
|
||
// local配置可选,忽略任何错误
|
||
}
|
||
|
||
// 8. 反序列化
|
||
var cfg Config
|
||
if err := viper.Unmarshal(&cfg); err != nil {
|
||
return nil, fmt.Errorf("unmarshal config: %w", err)
|
||
}
|
||
|
||
// 9. 设置运行时环境标识
|
||
cfg.Env = env
|
||
|
||
// 10. 校验
|
||
if err := Validate(&cfg); err != nil {
|
||
return nil, fmt.Errorf("validate config: %w", err)
|
||
}
|
||
|
||
return &cfg, nil
|
||
}
|
||
```
|
||
|
||
### 2.3 配置热加载方案
|
||
|
||
#### 设计思路
|
||
|
||
Go 服务通常不需要像 Rails 那样频繁热加载,但以下场景有价值:
|
||
- 开发环境:修改日志级别、限流参数后无需重启
|
||
- 生产环境:调整 RateLimit、Worker并发数、Captain开关等
|
||
|
||
#### 实现方案
|
||
|
||
```go
|
||
// ConfigWatcher 提供配置热加载能力
|
||
type ConfigWatcher struct {
|
||
cfg *Config
|
||
mu sync.RWMutex
|
||
onChange func(oldCfg, newCfg *Config)
|
||
}
|
||
|
||
// WatchConfig 启动配置文件监控
|
||
func WatchConfig(initial *Config, onChange func(old, new *Config)) (*ConfigWatcher, error) {
|
||
watcher := &ConfigWatcher{
|
||
cfg: initial,
|
||
onChange: onChange,
|
||
}
|
||
|
||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||
newCfg, err := Reload()
|
||
if err != nil {
|
||
log.Printf("config reload failed: %v", err)
|
||
return
|
||
}
|
||
|
||
oldCfg := watcher.Get()
|
||
watcher.mu.Lock()
|
||
watcher.cfg = newCfg
|
||
watcher.mu.Unlock()
|
||
|
||
if watcher.onChange != nil {
|
||
watcher.onChange(oldCfg, newCfg)
|
||
}
|
||
})
|
||
viper.WatchConfig()
|
||
|
||
return watcher, nil
|
||
}
|
||
|
||
// Get 安全获取当前配置(RLock保护)
|
||
func (w *ConfigWatcher) Get() *Config {
|
||
w.mu.RLock()
|
||
defer w.mu.RUnlock()
|
||
return w.cfg
|
||
}
|
||
|
||
// Reload 重新加载配置(不启动watch)
|
||
func Reload() (*Config, error) {
|
||
return Load() // 重新走完整的加载流程
|
||
}
|
||
```
|
||
|
||
#### 安全的配置引用方式
|
||
|
||
需要修改各模块从 `cfg *Config`(一次性指针)改为 `cfgProvider func() *Config`(动态获取):
|
||
|
||
```go
|
||
// 在 server/router 等模块中使用
|
||
type ConfigProvider func() *Config
|
||
|
||
// 初始化时:
|
||
// router.Setup(cfgWatcher.Get) // 传入 getter 函数
|
||
// 而不是 router.Setup(cfg) // 传入静态指针
|
||
```
|
||
|
||
#### 热加载安全策略
|
||
|
||
| 配置项 | 可热加载 | 需重启 | 说明 |
|
||
|--------|---------|--------|------|
|
||
| log.level/format | ✅ | | 日志级别即时生效 |
|
||
| rate_limit.* | ✅ | | 限流参数即时生效 |
|
||
| worker.concurrency | ✅ | | Worker池大小动态调整 |
|
||
| captain.enabled | ✅ | | AI功能开关 |
|
||
| server.port | ❌ | ✅ | 端口变更需重启 |
|
||
| database.* | ❌ | ✅ | 数据库连接变更需重启 |
|
||
| redis.url | ❌ | ✅ | Redis连接变更需重启 |
|
||
| jwt.secret | ❌ | ✅ | JWT密钥变更需重启(安全) |
|
||
| saml.* | ❌ | ✅ | SAML配置变更需重启 |
|
||
|
||
### 2.4 环境变量映射修复
|
||
|
||
#### 方案A:显式 BindEnv(推荐)
|
||
|
||
为所有非标准命名环境变量显式绑定:
|
||
|
||
```go
|
||
func bindLegacyEnvVars() {
|
||
// Database - 兼容 POSTGRES_* 前缀
|
||
viper.BindEnv("database.host", "POSTGRES_HOST", "GOCHAT_DATABASE_HOST")
|
||
viper.BindEnv("database.port", "POSTGRES_PORT", "GOCHAT_DATABASE_PORT")
|
||
viper.BindEnv("database.user", "POSTGRES_USER", "GOCHAT_DATABASE_USER")
|
||
viper.BindEnv("database.password", "POSTGRES_PASSWORD", "GOCHAT_DATABASE_PASSWORD")
|
||
viper.BindEnv("database.dbname", "POSTGRES_DB", "GOCHAT_DATABASE_DBNAME")
|
||
viper.BindEnv("database.sslmode", "POSTGRES_SSLMODE", "GOCHAT_DATABASE_SSLMODE")
|
||
|
||
// Redis - 兼容 REDIS_* 前缀
|
||
viper.BindEnv("redis.host", "REDIS_HOST", "GOCHAT_REDIS_HOST")
|
||
viper.BindEnv("redis.port", "REDIS_PORT", "GOCHAT_REDIS_PORT")
|
||
viper.BindEnv("redis.password", "REDIS_PASSWORD", "GOCHAT_REDIS_PASSWORD")
|
||
viper.BindEnv("redis.db", "REDIS_DB", "GOCHAT_REDIS_DB")
|
||
|
||
// JWT - 兼容非标准命名
|
||
viper.BindEnv("jwt.secret", "GOCHAT_JWT_SECRET", "JWT_SECRET")
|
||
viper.BindEnv("jwt.expiry_hours", "GOCHAT_JWT_EXPIRY_HOURS")
|
||
|
||
// Captain - 兼容 OPENAI_* 遗留命名
|
||
viper.BindEnv("captain.llm_api_key", "GOCHAT_CAPTAIN_LLM_API_KEY", "OPENAI_API_KEY")
|
||
viper.BindEnv("captain.llm_model", "GOCHAT_CAPTAIN_LLM_MODEL", "OPENAI_MODEL")
|
||
}
|
||
```
|
||
|
||
#### 方案B:EnvKeyReplacer(辅助)
|
||
|
||
```go
|
||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||
// 使 GOCHAT_SERVER_HOST → server.host 自动映射生效
|
||
```
|
||
|
||
### 2.5 新增配置结构体
|
||
|
||
当前 `.env.example` 中有多个配置域未纳入 Config 结构体,需要补充:
|
||
|
||
```go
|
||
type Config struct {
|
||
Env string `mapstructure:"env"` // 新增:当前运行环境标识
|
||
Server ServerConfig `mapstructure:"server"`
|
||
Database DatabaseConfig `mapstructure:"database"`
|
||
Redis RedisConfig `mapstructure:"redis"`
|
||
JWT JWTConfig `mapstructure:"jwt"`
|
||
Log LogConfig `mapstructure:"log"`
|
||
Captain CaptainConfig `mapstructure:"captain"`
|
||
Worker WorkerConfig `mapstructure:"worker"`
|
||
OAuth OAuthConfig `mapstructure:"oauth"`
|
||
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
|
||
SAML SAMLConfig `mapstructure:"saml"`
|
||
SMTP SMTPConfig `mapstructure:"smtp"` // 新增
|
||
Frontend FrontendConfig `mapstructure:"frontend"` // 新增
|
||
Metrics MetricsConfig `mapstructure:"metrics"` // 新增
|
||
WebSocket WebSocketConfig `mapstructure:"websocket"` // 新增
|
||
Analytics AnalyticsConfig `mapstructure:"analytics"` // 新增
|
||
Feature FeatureConfig `mapstructure:"feature"` // 新增
|
||
}
|
||
|
||
// 新增结构体定义
|
||
type SMTPConfig struct {
|
||
Host string `mapstructure:"host"`
|
||
Port int `mapstructure:"port"`
|
||
Username string `mapstructure:"username"`
|
||
Password string `mapstructure:"password"`
|
||
FromEmail string `mapstructure:"from_email"`
|
||
}
|
||
|
||
type FrontendConfig struct {
|
||
URL string `mapstructure:"url"`
|
||
}
|
||
|
||
type MetricsConfig struct {
|
||
Enabled bool `mapstructure:"enabled"`
|
||
Port int `mapstructure:"port"`
|
||
}
|
||
|
||
type WebSocketConfig struct {
|
||
MaxMessageSize int `mapstructure:"max_message_size"`
|
||
ReadBufferSize int `mapstructure:"read_buffer_size"`
|
||
WriteBufferSize int `mapstructure:"write_buffer_size"`
|
||
PingInterval int `mapstructure:"ping_interval"`
|
||
PongTimeout int `mapstructure:"pong_timeout"`
|
||
MaxSubscriptions int `mapstructure:"max_subscriptions"`
|
||
}
|
||
|
||
type AnalyticsConfig struct {
|
||
Enabled bool `mapstructure:"enabled"`
|
||
RetentionDays int `mapstructure:"retention_days"`
|
||
FlushInterval int `mapstructure:"flush_interval"`
|
||
}
|
||
|
||
type FeatureConfig struct {
|
||
CaptainAI bool `mapstructure:"captain_ai"`
|
||
AutoAssignment bool `mapstructure:"auto_assignment"`
|
||
CSAT bool `mapstructure:"csat"`
|
||
Campaigns bool `mapstructure:"campaigns"`
|
||
MFA bool `mapstructure:"mfa"`
|
||
}
|
||
```
|
||
|
||
### 2.6 DatabaseConfig 清理
|
||
|
||
`Name` 和 `DBName` 双字段需要统一:
|
||
|
||
```go
|
||
type DatabaseConfig struct {
|
||
Host string `mapstructure:"host"`
|
||
Port int `mapstructure:"port"`
|
||
User string `mapstructure:"user"`
|
||
Password string `mapstructure:"password"`
|
||
DBName string `mapstructure:"dbname"` // 统一用 dbname
|
||
SSLMode string `mapstructure:"sslmode"`
|
||
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"`
|
||
RunMigrations bool `mapstructure:"run_migrations"`
|
||
MigrationsPath string `mapstructure:"migrations_path"`
|
||
LogLevel string `mapstructure:"log_level"` // 新增:数据库日志级别
|
||
}
|
||
// Name 字段废弃 → DSN() 和 MigrateDSN() 只使用 DBName
|
||
```
|
||
|
||
### 2.7 配置版本标记
|
||
|
||
在 config.yaml 中加入版本号:
|
||
|
||
```yaml
|
||
# config.yaml
|
||
version: "2.0" # 配置格式版本,用于兼容性检查
|
||
|
||
server:
|
||
host: "0.0.0.0"
|
||
port: 3000
|
||
...
|
||
```
|
||
|
||
Load() 中加入版本检查:
|
||
```go
|
||
cfgVersion := viper.GetString("version")
|
||
if cfgVersion != "" && cfgVersion != "2.0" {
|
||
return nil, fmt.Errorf("unsupported config version: %s (expected 2.0)", cfgVersion)
|
||
}
|
||
```
|
||
|
||
### 2.8 Redis URL 构建优化
|
||
|
||
当前 RedisConfig 有 Host/Port/Password/DB 和 URL 双模式,建议增加 `BuildURL()` 方法:
|
||
|
||
```go
|
||
func (r RedisConfig) BuildURL() string {
|
||
if r.URL != "" {
|
||
return r.URL
|
||
}
|
||
auth := ""
|
||
if r.Password != "" {
|
||
auth = fmt.Sprintf(":%s@", r.Password)
|
||
}
|
||
return fmt.Sprintf("redis://%slocalhost:%d/%d", auth, r.Port, r.DB)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 三、实施计划
|
||
|
||
### 阶段一:核心改进(1天)
|
||
|
||
1. **修改 Load() 函数**:加入环境选择逻辑、MergeInConfig 环境合并
|
||
2. **加入 EnvKeyReplacer**:`strings.NewReplacer(".", "_")`
|
||
3. **加入 BindEnv**:显式绑定所有非标准环境变量
|
||
4. **创建 config.staging.yaml 和 config.test.yaml**
|
||
5. **Config 结构体新增 Env 字段**
|
||
|
||
### 阶段二:配置补全(0.5天)
|
||
|
||
1. **新增配置结构体**:SMTP/Frontend/Metrics/WebSocket/Analytics/Feature
|
||
2. **清理 DatabaseConfig**:统一 DBName,废弃 Name
|
||
3. **更新 config.yaml**:加入 version 标记 + 新配置域
|
||
4. **更新 config.dev.yaml / config.prod.yaml**:补全新配置域覆盖
|
||
|
||
### 阶段三:热加载(0.5天)
|
||
|
||
1. **实现 ConfigWatcher**:WatchConfig + OnConfigChange + sync.RWMutex
|
||
2. **引入 ConfigProvider 模式**:关键模块使用 `func() *Config` 获取动态配置
|
||
3. **热加载安全策略**:标记哪些配置可热加载,哪些需重启
|
||
|
||
### 阶段四:配置本地覆盖(0.5天)
|
||
|
||
1. **创建 config.local.yaml 模板**
|
||
2. **更新 .gitignore**:加入 config.local.yaml
|
||
3. **更新 validator.go**:新增配置域校验
|
||
4. **更新 config_test.go**:新增加载逻辑测试
|
||
|
||
### 总预估工时:2.5天(含测试)
|
||
|
||
---
|
||
|
||
## 四、风险与注意事项
|
||
|
||
| 风险 | 对策 |
|
||
|------|------|
|
||
| MergeInConfig 环境文件不存在时报错 | 检查 ConfigFileNotFoundError,容错处理 |
|
||
| 配置热加载与数据库连接池冲突 | 不可热加载的配置项标记为 "需重启",onChange 回调中只更新安全配置 |
|
||
| 环境变量命名兼容性 | BindEnv 支持多别名,同时支持旧命名(POSTGRES_*)和新命名(GOCHAT_DATABASE_*) |
|
||
| config.local.yaml 误提交 | 加入 .gitignore,并在 Makefile 中添加 check-git-clean 目标 |
|
||
| DatabaseConfig Name→DBName 迁移 | DSN() 方法中保留 Name 兜底兼容(`if dbname == "" { dbname = d.Name }`) |
|
||
| viper.Unmarshal 对零值字段不设置默认 | 继续使用 viper.SetDefault + 代码兜底的双重策略 |
|
||
|
||
---
|
||
|
||
## 五、参考
|
||
|
||
- Chatwoot 配置管理:`config/environments/{development,staging,production}.rb` + `.env` 文件
|
||
- Viper 文档:https://github.com/spf13/viper#working-with-environment-variables
|
||
- fsnotify:https://github.com/fsnotify/fsnotify(Viper WatchConfig 底层依赖)
|
||
- 12-Factor App:https://12factor.net/config(环境变量优先原则) |