做完了一部分,备份一下,防止以外删除

This commit is contained in:
2025-11-11 15:16:38 +08:00
parent 9600e5b6e0
commit e98dd4d725
39 changed files with 2423 additions and 183 deletions

167
pkg/config/config.go Normal file
View File

@@ -0,0 +1,167 @@
package config
import (
"errors"
"fmt"
"strings"
"sync/atomic"
"time"
"unsafe"
)
// globalConfig 保存当前配置,支持原子访问
var globalConfig atomic.Pointer[Config]
// Config 应用配置
type Config struct {
Server ServerConfig `mapstructure:"server"`
Redis RedisConfig `mapstructure:"redis"`
Logging LoggingConfig `mapstructure:"logging"`
Middleware MiddlewareConfig `mapstructure:"middleware"`
}
// ServerConfig HTTP 服务器配置
type ServerConfig struct {
Address string `mapstructure:"address"` // 例如 ":3000"
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 例如 "10s"
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 例如 "10s"
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` // 例如 "30s"
Prefork bool `mapstructure:"prefork"` // 多进程模式
}
// RedisConfig Redis 连接配置
type RedisConfig struct {
Address string `mapstructure:"address"` // 例如 "localhost"
Port int `mapstructure:"port"` // 例如 "6379"
Password string `mapstructure:"password"` // 无认证时留空
DB int `mapstructure:"db"` // 数据库编号0-15
PoolSize int `mapstructure:"pool_size"` // 最大连接数
MinIdleConns int `mapstructure:"min_idle_conns"` // 保活连接数
DialTimeout time.Duration `mapstructure:"dial_timeout"` // 例如 "5s"
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 例如 "3s"
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 例如 "3s"
}
// LoggingConfig 日志配置
type LoggingConfig struct {
Level string `mapstructure:"level"` // debug, info, warn, error
Development bool `mapstructure:"development"` // 启用开发模式(美化输出)
AppLog LogRotationConfig `mapstructure:"app_log"` // 应用日志配置
AccessLog LogRotationConfig `mapstructure:"access_log"` // HTTP 访问日志配置
}
// LogRotationConfig Lumberjack 日志轮转配置
type LogRotationConfig struct {
Filename string `mapstructure:"filename"` // 日志文件路径
MaxSize int `mapstructure:"max_size"` // 轮转前的最大大小MB
MaxBackups int `mapstructure:"max_backups"` // 保留的旧文件最大数量
MaxAge int `mapstructure:"max_age"` // 保留旧文件的最大天数
Compress bool `mapstructure:"compress"` // 压缩轮转的文件
}
// MiddlewareConfig 中间件配置
type MiddlewareConfig struct {
EnableAuth bool `mapstructure:"enable_auth"` // 启用 keyauth 中间件
EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // 启用限流器默认false
RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // 限流器配置
}
// RateLimiterConfig 限流器配置
type RateLimiterConfig struct {
Max int `mapstructure:"max"` // 时间窗口内的最大请求数
Expiration time.Duration `mapstructure:"expiration"` // 时间窗口(例如 "1m"
Storage string `mapstructure:"storage"` // "memory" 或 "redis"
}
// Validate 验证配置值
func (c *Config) Validate() error {
// 服务器验证
if c.Server.Address == "" {
return fmt.Errorf("invalid configuration: server.address: must be non-empty (current value: empty)")
}
if c.Server.ReadTimeout < 5*time.Second || c.Server.ReadTimeout > 300*time.Second {
return fmt.Errorf("invalid configuration: server.read_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.ReadTimeout)
}
if c.Server.WriteTimeout < 5*time.Second || c.Server.WriteTimeout > 300*time.Second {
return fmt.Errorf("invalid configuration: server.write_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.WriteTimeout)
}
if c.Server.ShutdownTimeout < 10*time.Second || c.Server.ShutdownTimeout > 120*time.Second {
return fmt.Errorf("invalid configuration: server.shutdown_timeout: duration out of range (current value: %s, expected: 10s-120s)", c.Server.ShutdownTimeout)
}
// Redis 验证
if c.Redis.Address == "" {
return fmt.Errorf("invalid configuration: redis.address: must be non-empty (current value: empty)")
}
if !strings.Contains(c.Redis.Address, ":") {
return fmt.Errorf("invalid configuration: redis.address: invalid format (current value: %s, expected: HOST:PORT)", c.Redis.Address)
}
if c.Redis.DB < 0 || c.Redis.DB > 15 {
return fmt.Errorf("invalid configuration: redis.db: database number out of range (current value: %d, expected: 0-15)", c.Redis.DB)
}
if c.Redis.PoolSize <= 0 || c.Redis.PoolSize > 1000 {
return fmt.Errorf("invalid configuration: redis.pool_size: pool size out of range (current value: %d, expected: 1-1000)", c.Redis.PoolSize)
}
if c.Redis.MinIdleConns < 0 || c.Redis.MinIdleConns > c.Redis.PoolSize {
return fmt.Errorf("invalid configuration: redis.min_idle_conns: must be 0 to pool_size (current value: %d, pool_size: %d)", c.Redis.MinIdleConns, c.Redis.PoolSize)
}
// 日志验证
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
if !validLevels[c.Logging.Level] {
return fmt.Errorf("invalid configuration: logging.level: invalid log level (current value: %s, expected: debug, info, warn, error)", c.Logging.Level)
}
if c.Logging.AppLog.Filename == "" {
return fmt.Errorf("invalid configuration: logging.app_log.filename: must be non-empty valid file path")
}
if c.Logging.AccessLog.Filename == "" {
return fmt.Errorf("invalid configuration: logging.access_log.filename: must be non-empty valid file path")
}
if c.Logging.AppLog.MaxSize < 1 || c.Logging.AppLog.MaxSize > 1000 {
return fmt.Errorf("invalid configuration: logging.app_log.max_size: size out of range (current value: %d, expected: 1-1000 MB)", c.Logging.AppLog.MaxSize)
}
if c.Logging.AccessLog.MaxSize < 1 || c.Logging.AccessLog.MaxSize > 1000 {
return fmt.Errorf("invalid configuration: logging.access_log.max_size: size out of range (current value: %d, expected: 1-1000 MB)", c.Logging.AccessLog.MaxSize)
}
// 中间件验证
if c.Middleware.RateLimiter.Max <= 0 {
c.Middleware.RateLimiter.Max = 100 // 默认值
}
if c.Middleware.RateLimiter.Max > 10000 {
return fmt.Errorf("invalid configuration: middleware.rate_limiter.max: request limit out of range (current value: %d, expected: 1-10000)", c.Middleware.RateLimiter.Max)
}
validStorage := map[string]bool{"memory": true, "redis": true}
if c.Middleware.RateLimiter.Storage != "" && !validStorage[c.Middleware.RateLimiter.Storage] {
return fmt.Errorf("invalid configuration: middleware.rate_limiter.storage: invalid storage type (current value: %s, expected: memory or redis)", c.Middleware.RateLimiter.Storage)
}
return nil
}
// Get 返回当前配置
func Get() *Config {
return globalConfig.Load()
}
// Set 原子地更新全局配置
func Set(cfg *Config) error {
if cfg == nil {
return errors.New("config cannot be nil")
}
if err := cfg.Validate(); err != nil {
return err
}
globalConfig.Store(cfg)
return nil
}
// unsafeSet 无验证设置配置(仅供热重载内部使用)
func unsafeSet(cfg *Config) {
globalConfig.Store(cfg)
}
// atomicSwap 原子地交换配置
func atomicSwap(new *Config) *Config {
return (*Config)(atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&globalConfig)), unsafe.Pointer(new)))
}

93
pkg/config/loader.go Normal file
View File

@@ -0,0 +1,93 @@
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/spf13/viper"
)
// Load 从文件和环境变量加载配置
func Load() (*Config, error) {
// 确定配置路径
configPath := os.Getenv(constants.EnvConfigPath)
if configPath == "" {
configPath = constants.DefaultConfigPath
}
// 检查环境特定配置dev, staging, prod
configEnv := os.Getenv(constants.EnvConfigEnv)
if configEnv != "" {
// 优先尝试环境特定配置
envConfigPath := fmt.Sprintf("configs/config.%s.yaml", configEnv)
if _, err := os.Stat(envConfigPath); err == nil {
configPath = envConfigPath
}
}
// 设置 Viper
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
// 启用环境变量覆盖
viper.AutomaticEnv()
viper.SetEnvPrefix("APP")
// 读取配置文件
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// 反序列化到 Config 结构体
cfg := &Config{}
if err := viper.Unmarshal(cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// 验证配置
if err := cfg.Validate(); err != nil {
return nil, err
}
// 设为全局配置
globalConfig.Store(cfg)
return cfg, nil
}
// Reload 重新加载当前配置文件
func Reload() (*Config, error) {
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to reload config: %w", err)
}
cfg := &Config{}
if err := viper.Unmarshal(cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// 设置前验证
if err := cfg.Validate(); err != nil {
return nil, err
}
// 原子交换
globalConfig.Store(cfg)
return cfg, nil
}
// GetConfigPath 返回当前已加载配置文件的绝对路径
func GetConfigPath() string {
configFile := viper.ConfigFileUsed()
if configFile == "" {
return ""
}
absPath, err := filepath.Abs(configFile)
if err != nil {
return configFile
}
return absPath
}

43
pkg/config/watcher.go Normal file
View File

@@ -0,0 +1,43 @@
package config
import (
"context"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap"
)
// Watch 监听配置文件变化
// 运行直到上下文被取消
func Watch(ctx context.Context, logger *zap.Logger) {
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
select {
case <-ctx.Done():
return // 如果上下文被取消则停止处理
default:
logger.Info("配置文件已更改", zap.String("file", e.Name))
// 尝试重新加载
newConfig, err := Reload()
if err != nil {
logger.Error("重新加载配置失败,保留先前配置",
zap.Error(err),
zap.String("file", e.Name),
)
return
}
logger.Info("配置重新加载成功",
zap.String("file", e.Name),
zap.String("server_address", newConfig.Server.Address),
zap.String("log_level", newConfig.Logging.Level),
)
}
})
// 阻塞直到上下文被取消
<-ctx.Done()
logger.Info("配置监听器已停止")
}