Files
xlgo-core/config/config.go
T
杭州明婳科技 18e4f7e891 fix: ServerConfig 加 Host 字段 + 面向用户文案统一中文
- ServerConfig 新增 Host 字段:空=监听所有接口(兼容),设值=绑定指定地址
  (127.0.0.1 仅本机 / 内网IP 绑定指定网卡),避免生产暴露 0.0.0.0
- app.go 启动地址改 host:port,日志区分"所有接口"/指定地址
- 面向用户/调用的文案统一中文:
  - middleware/recover.go: "Panic recovered"→"panic 已恢复","Panic: %v"→"服务器内部错误: %v"
  - middleware/logger.go: 5 处日志消息改中文
  - middleware/metrics.go: 3 个 Prometheus Help 改中文
  - app.go/database/manager.go/logger.go: 英文 error 改中文
- 保留英文:JSON 协议字段名、Prometheus metric Name、MySQL 错误串匹配、技术专有名词
- CLI 模板与 example 配置加 host 注释

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:12:40 +08:00

711 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"fmt"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
)
// 配置错误
var (
ErrConfigNotLoaded = fmt.Errorf("配置未加载")
)
// Config 全局配置结构体
type Config struct {
App AppConfig `mapstructure:"app"`
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
JWT JWTConfig `mapstructure:"jwt"`
SMS SMSConfig `mapstructure:"sms"`
Storage StorageConfig `mapstructure:"storage"`
Upload UploadConfig `mapstructure:"upload"`
Log LogConfig `mapstructure:"log"`
CORS CORSConfig `mapstructure:"cors"`
}
// AppConfig 应用配置
// 使用场景:
// - 缓存键名前缀: cache:{site_name}:user:1
// - 日志标识: [site_a] 2024-01-01 10:00:00 ...
// - 站点追踪: Request-ID 带站点标识
// - 分布式锁: lock:{site_name}:order:123
type AppConfig struct {
Name string `mapstructure:"name"` // 应用名称,如 "用户管理系统"
SiteName string `mapstructure:"site_name"` // 站点别名,如 "site_a"、"user_api"
Version string `mapstructure:"version"` // 应用版本
Env string `mapstructure:"env"` // 运行环境: dev/test/prod
Debug bool `mapstructure:"debug"` // 是否开启调试模式
BaseURL string `mapstructure:"base_url"` // 应用基础URL
}
// GetSiteName 获取站点别名,如果未设置则返回空字符串
func (c *AppConfig) GetSiteName() string {
if c == nil {
return ""
}
return c.SiteName
}
// GetCachePrefix 获取缓存键名前缀
func (c *AppConfig) GetCachePrefix() string {
return c.GetSiteName()
}
// IsDebug 是否调试模式
func (c *AppConfig) IsDebug() bool {
if c == nil {
return false
}
return c.Debug
}
// IsDev 是否开发环境
func (c *AppConfig) IsDev() bool {
if c == nil {
return false
}
return c.Env == "dev" || c.Env == "development"
}
// IsProd 是否生产环境
func (c *AppConfig) IsProd() bool {
if c == nil {
return false
}
return c.Env == "prod" || c.Env == "production"
}
// TLSConfig HTTPS/TLS 配置
type TLSConfig struct {
Enabled bool `mapstructure:"enabled"`
CertFile string `mapstructure:"cert_file"`
KeyFile string `mapstructure:"key_file"`
}
// ServerConfig 服务配置
type ServerConfig struct {
Host string `mapstructure:"host"` // 绑定地址,空=监听所有接口(0.0.0.0)"127.0.0.1"=仅本机;内网IP=绑定指定网卡
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"` // development 或 production
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 读超时,如 "15s"
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 写超时,如 "30s"
IdleTimeout time.Duration `mapstructure:"idle_timeout"` // 空闲超时,如 "60s"
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` // 优雅关闭超时,如 "30s"
MaxHeaderBytes int `mapstructure:"max_header_bytes"` // 最大请求头字节数
TLS TLSConfig `mapstructure:"tls"`
UnixSocket string `mapstructure:"unix_socket"` // 非空时优先于 Port,监听 unix socket
ResponseMode string `mapstructure:"response_mode"` // business(默认) 或 rest,见 response.SetMode
}
// 默认值常量(ServerConfig 字段为零值时回退使用)
const (
defaultReadTimeout = 15 * time.Second
defaultWriteTimeout = 30 * time.Second
defaultIdleTimeout = 60 * time.Second
defaultShutdownTimeout = 30 * time.Second
defaultMaxHeaderBytes = 1 << 20 // 1MB
)
// EffectiveReadTimeout 返回生效的读超时(零值回退默认)
func (c ServerConfig) EffectiveReadTimeout() time.Duration {
if c.ReadTimeout > 0 {
return c.ReadTimeout
}
return defaultReadTimeout
}
// EffectiveWriteTimeout 返回生效的写超时(零值回退默认)
func (c ServerConfig) EffectiveWriteTimeout() time.Duration {
if c.WriteTimeout > 0 {
return c.WriteTimeout
}
return defaultWriteTimeout
}
// EffectiveIdleTimeout 返回生效的空闲超时(零值回退默认)
func (c ServerConfig) EffectiveIdleTimeout() time.Duration {
if c.IdleTimeout > 0 {
return c.IdleTimeout
}
return defaultIdleTimeout
}
// EffectiveShutdownTimeout 返回生效的关闭超时(零值回退默认)
func (c ServerConfig) EffectiveShutdownTimeout() time.Duration {
if c.ShutdownTimeout > 0 {
return c.ShutdownTimeout
}
return defaultShutdownTimeout
}
// EffectiveMaxHeaderBytes 返回生效的最大请求头字节数(零值回退默认)
func (c ServerConfig) EffectiveMaxHeaderBytes() int {
if c.MaxHeaderBytes > 0 {
return c.MaxHeaderBytes
}
return defaultMaxHeaderBytes
}
// 数据库驱动常量
const (
DriverMySQL = "mysql"
DriverPostgres = "postgres"
)
// DSNBuilder 根据 DatabaseConfig 生成连接字符串
type DSNBuilder func(*DatabaseConfig) string
var (
dsnBuildersMu sync.RWMutex
dsnBuilders = map[string]DSNBuilder{}
)
// RegisterDSNBuilder 为指定驱动注册 DSN 构建器(驱动名大小写不敏感)。
// aliases 用于注册同一驱动的别名,例如 postgres 的 "postgresql"、"pg"。
// 通常由 database 包通过 database.RegisterDialect 间接调用,
// 应用代码也可直接使用以扩展自定义驱动。
func RegisterDSNBuilder(name string, builder DSNBuilder, aliases ...string) {
if builder == nil {
return
}
dsnBuildersMu.Lock()
defer dsnBuildersMu.Unlock()
for _, n := range append([]string{name}, aliases...) {
key := strings.ToLower(strings.TrimSpace(n))
if key != "" {
dsnBuilders[key] = builder
}
}
}
// LookupDSNBuilder 查找已注册的 DSN 构建器
func LookupDSNBuilder(name string) (DSNBuilder, bool) {
key := strings.ToLower(strings.TrimSpace(name))
dsnBuildersMu.RLock()
defer dsnBuildersMu.RUnlock()
b, ok := dsnBuilders[key]
return b, ok
}
// RegisteredDrivers 返回所有已注册 DSN 构建器的驱动名(用于诊断)
func RegisteredDrivers() []string {
dsnBuildersMu.RLock()
defer dsnBuildersMu.RUnlock()
names := make([]string, 0, len(dsnBuilders))
for k := range dsnBuilders {
names = append(names, k)
}
return names
}
func init() {
// 内置 MySQL / PostgreSQL 的 DSN 构建器
RegisterDSNBuilder(DriverMySQL, func(c *DatabaseConfig) string { return c.MySQLDSN() })
RegisterDSNBuilder(DriverPostgres, func(c *DatabaseConfig) string { return c.PostgresDSN() }, "postgresql", "pg")
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
// Driver 数据库驱动,支持 mysql(默认)与 postgres
Driver string `mapstructure:"driver"`
// Host 数据库主机
Host string `mapstructure:"host"`
// Port 数据库端口
Port int `mapstructure:"port"`
// User 数据库用户名
User string `mapstructure:"user"`
// Password 数据库密码
Password string `mapstructure:"password"`
// Name 数据库名
Name string `mapstructure:"name"`
// CustomDSN 自定义连接字符串,设置后优先于由 Host/Port 等字段生成的 DSN
CustomDSN string `mapstructure:"dsn"`
// MaxIdleConns 最大空闲连接数
MaxIdleConns int `mapstructure:"max_idle_conns"`
// MaxOpenConns 最大打开连接数
MaxOpenConns int `mapstructure:"max_open_conns"`
// ConnMaxIdleTime 连接最大空闲时间,如 "5m"(#21)。0 表示用驱动默认
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`
// HealthCheckInterval 主库探活间隔,如 "30s"#21)。0 表示用默认 30s
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"`
// HealthCheckFailureThreshold 连续探活失败多少次标记不健康(#21)。0 表示用默认 3
HealthCheckFailureThreshold int `mapstructure:"health_check_failure_threshold"`
}
// DSN 根据驱动返回连接字符串。设置了 CustomDSN 时优先返回 CustomDSN
// 未指定 Driver 时按 MySQL 处理(向后兼容)。
// 若驱动通过 RegisterDSNBuilder 注册过自定义构建器,则使用注册的构建器。
func (c *DatabaseConfig) DSN() string {
if c.CustomDSN != "" {
return c.CustomDSN
}
if builder, ok := LookupDSNBuilder(c.Driver); ok {
return builder(c)
}
// 未注册时回退到 MySQL(保持向后兼容)
return c.MySQLDSN()
}
// MySQLDSN 返回 MySQL 连接字符串
func (c *DatabaseConfig) MySQLDSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
c.User, c.Password, c.Host, c.Port, c.Name)
}
// PostgresDSN 返回 PostgreSQL 连接字符串
func (c *DatabaseConfig) PostgresDSN() string {
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
c.Host, c.Port, c.User, c.Password, c.Name)
}
// RedisConfig Redis 配置
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// Addr 返回 Redis 地址
func (c *RedisConfig) Addr() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
// JWTConfig JWT 配置
type JWTConfig struct {
Secret string `mapstructure:"secret"`
Expire time.Duration `mapstructure:"expire"` // 过期时间,如 "24h"time.Duration
RefreshExpire time.Duration `mapstructure:"refresh_expire"` // 刷新 token 过期时间,如 "168h"
Issuer string `mapstructure:"issuer"` // 签发者
Algorithm string `mapstructure:"algorithm"` // 签名算法:HS256(默认)/RS256
}
// SMSConfig 短信配置
type SMSConfig struct {
Enabled bool `mapstructure:"enabled"`
Provider string `mapstructure:"provider"`
AccessKeyID string `mapstructure:"access_key_id"`
AccessKeySecret string `mapstructure:"access_key_secret"`
SignName string `mapstructure:"sign_name"`
TemplateCode string `mapstructure:"template_code"`
}
// StorageConfig 文件存储配置
type StorageConfig struct {
Driver string `mapstructure:"driver"` // local 或 oss
Local LocalStorageConfig `mapstructure:"local"`
OSS OSSStorageConfig `mapstructure:"oss"`
}
// LocalStorageConfig 本地存储配置
type LocalStorageConfig struct {
Path string `mapstructure:"path"`
BaseURL string `mapstructure:"base_url"`
}
// OSSStorageConfig OSS 存储配置
type OSSStorageConfig struct {
Endpoint string `mapstructure:"endpoint"`
Bucket string `mapstructure:"bucket"`
AccessKeyID string `mapstructure:"access_key_id"`
AccessKeySecret string `mapstructure:"access_key_secret"`
BaseURL string `mapstructure:"base_url"`
}
// LogConfig 日志配置
type LogConfig struct {
Dir string `mapstructure:"dir"`
MaxSize int `mapstructure:"max_size"` // MB
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"` // 天
Compress bool `mapstructure:"compress"`
}
// UploadConfig 上传配置
type UploadConfig struct {
MaxFileSize int `mapstructure:"max_file_size"` // 最大图片大小(MB
MaxVideoSize int `mapstructure:"max_video_size"` // 最大视频大小(MB
MaxAvatarSize int `mapstructure:"max_avatar_size"` // 最大头像大小(MB
AllowedImageTypes []string `mapstructure:"allowed_image_types"` // 允许的图片 MIME 类型
AllowedVideoTypes []string `mapstructure:"allowed_video_types"` // 允许的视频 MIME 类型
}
// CORSConfig CORS 跨域配置
type CORSConfig struct {
AllowedOrigins []string `mapstructure:"allowed_origins"` // 允许的域名列表
AllowedMethods []string `mapstructure:"allowed_methods"` // 允许的方法
AllowedHeaders []string `mapstructure:"allowed_headers"` // 允许的请求头
ExposedHeaders []string `mapstructure:"exposed_headers"` // 暴露的响应头
AllowCredentials bool `mapstructure:"allow_credentials"` // 是否允许携带凭证
MaxAge int `mapstructure:"max_age"` // 预检请求缓存时间(秒)
}
// GetAllowedOrigins 获取允许的域名列表
func (c *CORSConfig) GetAllowedOrigins() []string {
if c == nil || len(c.AllowedOrigins) == 0 {
return []string{}
}
return c.AllowedOrigins
}
// GetAllowedMethods 获取允许的方法列表
func (c *CORSConfig) GetAllowedMethods() []string {
if c == nil || len(c.AllowedMethods) == 0 {
return []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}
}
return c.AllowedMethods
}
// GetAllowedHeaders 获取允许的请求头列表
func (c *CORSConfig) GetAllowedHeaders() []string {
if c == nil || len(c.AllowedHeaders) == 0 {
return []string{"Origin", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "X-Requested-With"}
}
return c.AllowedHeaders
}
// GetExposedHeaders 获取暴露的响应头列表
func (c *CORSConfig) GetExposedHeaders() []string {
if c == nil || len(c.ExposedHeaders) == 0 {
return []string{"Content-Length", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Content-Type"}
}
return c.ExposedHeaders
}
// GetMaxAge 获取预检请求缓存时间
func (c *CORSConfig) GetMaxAge() int {
if c == nil || c.MaxAge <= 0 {
return 86400 // 默认 24 小时
}
return c.MaxAge
}
// Manager 配置管理器
type Manager struct {
mu sync.RWMutex
path string
v *viper.Viper
cfg *Config
callbacks []func(*Config)
}
var defaultManager = NewManager("")
// NewManager 创建配置管理器
func NewManager(configPath string) *Manager {
return &Manager{path: configPath}
}
func newViper(configPath string) *viper.Viper {
v := viper.New()
v.SetConfigFile(configPath)
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
return v
}
// unmarshalConfig 将 viper 解析到 Config,启用 string→time.Duration decode hook
// 使 ServerConfig/JWTConfig 的 Duration 字段可写 "24h"/"15s" 等字符串。
func unmarshalConfig(v *viper.Viper, cfg *Config) error {
return v.Unmarshal(cfg, viper.DecodeHook(mapstructure.StringToTimeDurationHookFunc()))
}
// Load 加载配置文件
func (m *Manager) Load() (*Config, error) {
if m == nil || m.path == "" {
return nil, ErrConfigNotLoaded
}
v := newViper(m.path)
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var cfg Config
if err := unmarshalConfig(v, &cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
if err := cfg.Validate(); err != nil {
return nil, err
}
m.mu.Lock()
m.v = v
m.cfg = &cfg
m.mu.Unlock()
return &cfg, nil
}
// LoadWithWatch 加载配置文件并启用热更新
func (m *Manager) LoadWithWatch(onChange func(*Config)) (*Config, error) {
cfg, err := m.Load()
if err != nil {
return nil, err
}
if onChange != nil {
m.RegisterCallback(onChange)
}
if err := m.StartWatcher(); err != nil {
return nil, fmt.Errorf("启动配置监听失败: %w", err)
}
return cfg, nil
}
// RegisterCallback 注册配置变更回调
func (m *Manager) RegisterCallback(cb func(*Config)) {
if m == nil || cb == nil {
return
}
m.mu.Lock()
defer m.mu.Unlock()
m.callbacks = append(m.callbacks, cb)
}
// StartWatcher 启动配置文件监听
func (m *Manager) StartWatcher() error {
if m == nil {
return ErrConfigNotLoaded
}
m.mu.RLock()
v := m.v
m.mu.RUnlock()
if v == nil {
return ErrConfigNotLoaded
}
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
var newCfg Config
if err := unmarshalConfig(v, &newCfg); err != nil {
return
}
m.mu.Lock()
m.cfg = &newCfg
cbs := make([]func(*Config), len(m.callbacks))
copy(cbs, m.callbacks)
m.mu.Unlock()
for _, cb := range cbs {
cb(&newCfg)
}
})
return nil
}
// StopWatcher 停止配置文件监听
func (m *Manager) StopWatcher() {}
// Get 获取配置
func (m *Manager) Get() *Config {
if m == nil {
return nil
}
m.mu.RLock()
defer m.mu.RUnlock()
return m.cfg
}
// GetViper 获取 viper 实例
func (m *Manager) GetViper() *viper.Viper {
if m == nil {
return nil
}
m.mu.RLock()
defer m.mu.RUnlock()
return m.v
}
// Set 手动设置配置
func (m *Manager) Set(cfg *Config) {
if m == nil {
return
}
m.mu.Lock()
defer m.mu.Unlock()
m.cfg = cfg
if cfg == nil {
m.v = nil
}
}
// Reload 重新加载配置文件
func (m *Manager) Reload() error {
if m == nil {
return ErrConfigNotLoaded
}
m.mu.RLock()
v := m.v
m.mu.RUnlock()
if v == nil {
return ErrConfigNotLoaded
}
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
var newCfg Config
if err := unmarshalConfig(v, &newCfg); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
m.mu.Lock()
m.cfg = &newCfg
cbs := make([]func(*Config), len(m.callbacks))
copy(cbs, m.callbacks)
m.mu.Unlock()
for _, cb := range cbs {
cb(&newCfg)
}
return nil
}
// Load 加载配置文件
func Load(configPath string) (*Config, error) {
defaultManager = NewManager(configPath)
return defaultManager.Load()
}
// LoadWithWatch 加载配置文件并启用热更新
func LoadWithWatch(configPath string, onChange func(*Config)) (*Config, error) {
defaultManager = NewManager(configPath)
return defaultManager.LoadWithWatch(onChange)
}
// RegisterCallback 注册配置变更回调
func RegisterCallback(cb func(*Config)) {
defaultManager.RegisterCallback(cb)
}
// StartWatcher 启动配置文件监听
func StartWatcher() error {
return defaultManager.StartWatcher()
}
// StopWatcher 停止配置文件监听
func StopWatcher() {
defaultManager.StopWatcher()
}
// Get 获取全局配置
func Get() *Config {
return defaultManager.Get()
}
// GetViper 获取 viper 实例(用于扩展配置)
func GetViper() *viper.Viper {
return defaultManager.GetViper()
}
// Set 手动设置配置(用于测试或动态修改)
func Set(cfg *Config) {
defaultManager.Set(cfg)
}
// Reload 重新加载配置文件
func Reload() error {
return defaultManager.Reload()
}
// SetDefaultManager 替换全局默认配置管理器。
// 主要供应用层(如 App)在持有自己的 Manager 时使用,
// 使 config.Get / config.GetString 等便捷函数仍然能取到正确的配置。
// 传入 nil 表示重置为空管理器。
func SetDefaultManager(m *Manager) {
if m == nil {
defaultManager = NewManager("")
return
}
defaultManager = m
}
// GetString 获取字符串配置
func GetString(key string) string {
v := GetViper()
if v == nil {
return ""
}
return v.GetString(key)
}
// GetInt 获取整数配置
func GetInt(key string) int {
v := GetViper()
if v == nil {
return 0
}
return v.GetInt(key)
}
// GetBool 获取布尔配置
func GetBool(key string) bool {
v := GetViper()
if v == nil {
return false
}
return v.GetBool(key)
}
// GetStringMap 获取字符串映射配置
func GetStringMap(key string) map[string]any {
v := GetViper()
if v == nil {
return nil
}
return v.GetStringMap(key)
}
// IsDevelopment 是否开发环境
func (c *Config) IsDevelopment() bool {
if c == nil {
return false
}
// 优先使用 App.Env
if c.App.Env != "" {
return c.App.IsDev()
}
return c.Server.Mode == "development"
}
// IsProduction 是否生产环境
func (c *Config) IsProduction() bool {
if c == nil {
return false
}
// 优先使用 App.Env
if c.App.Env != "" {
return c.App.IsProd()
}
return c.Server.Mode == "production"
}
// GetAppName 获取应用名称
func (c *Config) GetAppName() string {
if c == nil {
return ""
}
return c.App.Name
}
// GetSiteName 获取站点别名
func (c *Config) GetSiteName() string {
if c == nil {
return ""
}
return c.App.GetSiteName()
}