Files
xlgo-core/config/config.go
T
杭州明婳科技 dcfd24b624 release: v1.0.3 bug fix release
v1.0.3 定位为 bug fix release,收口 v1.0.2 引入的破坏性清理并修复
4 个轻量 bug + 依赖复查 + 版本号治理 + 文档对齐。同时包含此前未提交
的 v1.0.2 全部工作。

v1.0.3 变更:

Fixed:
- generateJTI 忽略 rand.Read 错误(jwt)— 改为 (string, error) 并传播
- QueryBuilder.Page Count 受残留 Limit 截断(repository)— countDB 加 Limit(-1).Offset(-1)
- OSS/本地存储文件名冲突(storage)— 新增 uniqueFilename 加 8 字节随机后缀
- 数据库重试策略对不可恢复错误无效(database)— 新增 isTransientDBError

Dependencies:
- go mod tidy 补全 postgres 方言传递依赖;安全补丁升级
  x/crypto v0.49→v0.53、golang-jwt/jwt/v5 v5.2.1→v5.3.1、gorilla/websocket v1.5.1→v1.5.3

Removed:
- Breaking: 清理 v1.0.2 兼容别名(InitMySQL* / driverName)
- 死代码 database.DBResolver
- 代码中 292 行"评分/理由"自夸注释(#26,23 个文件)

Changed:
- Breaking: 错误码体系重构 CodeSuccess 1→0、CodeFail 0→1,删除 CodeInvalidParams,加编译期防撞码
- database/mysql.go → manager.go;Logger 拆分三独立 core 修复 Tee 重复写入
- 版本号常量化:app.go 新增 const Version 作为唯一来源,CLI/脚手架模板引用之,不再散落字面量

Security:
- CORS 中间件按 W3C 规范修复 Allow-Credentials / Vary: Origin / 非白名单不回显

Added:
- console 包显式 level 控制(SetLevel/WithLevel/LevelSilent,atomic.Int32 并发安全)
- 新增测试 jwt/repository/storage/database/router/middleware/app
- 文档:新增 CHANGELOG.md、Version_v1.0.2_report.md;更新 README/GUIDE 顶部更新日志
- 文档对齐:删除对不存在的 config.DefaultManager() getter 的描述(保持 API 与文档一致)

详见 CHANGELOG.md 与 README.md 更新日志。

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

627 lines
16 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"
"github.com/fsnotify/fsnotify"
"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
TokenExpire int `mapstructure:"token_expire"` // Token过期时间(秒)
}
// 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"
}
// ServerConfig 服务配置
type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"` // development 或 production
}
// 数据库驱动常量
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"`
}
// 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 int `mapstructure:"expire"` // 过期时间(秒)
}
// 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
}
// 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 := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", 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 := v.Unmarshal(&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 := v.Unmarshal(&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()
}