Files
junhong_cmp_fiber/pkg/config/config.go
huang b0da71bd25 refactor: 清理 YAML 支付配置遗留代码,重命名 Card* 常量为 Asset*,新增支付配置相关错误码
- 删除 PaymentConfig 结构体和 WechatConfig.Payment 字段(YAML 方案已废弃)
- 删除 wechat.payment 配置节和 NewPaymentApp() 函数
- 删除 validateWechatConfig 中所有 wechatCfg.Payment.* 校验代码
- pkg/constants/wallet.go: Card* 前缀统一重命名为 Asset*,旧名保留废弃别名
- pkg/constants/redis.go: 新增 RedisWechatConfigActiveKey()
- pkg/errors/codes.go: 新增错误码 1170-1175
- go.mod: 新增 golang.org/x/text 依赖(富友支付 GBK 编解码)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-16 23:28:29 +08:00

332 lines
15 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 (
"errors"
"fmt"
"strings"
"sync/atomic"
"time"
)
// globalConfig 保存当前配置,支持原子访问
var globalConfig atomic.Pointer[Config]
// Config 应用配置
type Config struct {
Server ServerConfig `mapstructure:"server"`
Redis RedisConfig `mapstructure:"redis"`
Database DatabaseConfig `mapstructure:"database"`
Queue QueueConfig `mapstructure:"queue"`
Logging LoggingConfig `mapstructure:"logging"`
Middleware MiddlewareConfig `mapstructure:"middleware"`
SMS SMSConfig `mapstructure:"sms"`
JWT JWTConfig `mapstructure:"jwt"`
DefaultAdmin DefaultAdminConfig `mapstructure:"default_admin"`
Storage StorageConfig `mapstructure:"storage"`
Gateway GatewayConfig `mapstructure:"gateway"`
Wechat WechatConfig `mapstructure:"wechat"`
}
// 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"
}
// DatabaseConfig 数据库连接配置
type DatabaseConfig struct {
Host string `mapstructure:"host"` // 数据库主机地址
Port int `mapstructure:"port"` // 数据库端口
User string `mapstructure:"user"` // 数据库用户名
Password string `mapstructure:"password"` // 数据库密码(明文存储)
DBName string `mapstructure:"dbname"` // 数据库名称
SSLMode string `mapstructure:"sslmode"` // SSL 模式disable, require, verify-ca, verify-full
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数默认25
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数默认10
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生命周期默认5m
}
// QueueConfig 任务队列配置
type QueueConfig struct {
Concurrency int `mapstructure:"concurrency"` // Worker 并发数默认10
Queues map[string]int `mapstructure:"queues"` // 队列优先级配置(队列名 -> 权重)
RetryMax int `mapstructure:"retry_max"` // 最大重试次数默认5
Timeout time.Duration `mapstructure:"timeout"` // 任务超时时间默认10m
}
// 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 {
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"
}
// SMSConfig 短信服务配置
type SMSConfig struct {
GatewayURL string `mapstructure:"gateway_url"` // 短信网关地址
Username string `mapstructure:"username"` // 账号用户名
Password string `mapstructure:"password"` // 账号密码
Signature string `mapstructure:"signature"` // 短信签名(例如:【签名】)
Timeout time.Duration `mapstructure:"timeout"` // 请求超时时间
}
// JWTConfig JWT 认证配置
type JWTConfig struct {
SecretKey string `mapstructure:"secret_key"` // JWT 签名密钥
TokenDuration time.Duration `mapstructure:"token_duration"` // Token 有效期C 端 JWT
AccessTokenTTL time.Duration `mapstructure:"access_token_ttl"` // 访问令牌有效期B 端 Redis Token
RefreshTokenTTL time.Duration `mapstructure:"refresh_token_ttl"` // 刷新令牌有效期B 端 Redis Token
}
// DefaultAdminConfig 默认超级管理员配置
type DefaultAdminConfig struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Phone string `mapstructure:"phone"`
}
// StorageConfig 对象存储配置
type StorageConfig struct {
Provider string `mapstructure:"provider"` // 存储提供商s3
S3 S3Config `mapstructure:"s3"` // S3 兼容存储配置
Presign PresignConfig `mapstructure:"presign"` // 预签名 URL 配置
TempDir string `mapstructure:"temp_dir"` // 临时文件目录
}
// GatewayConfig Gateway 服务配置
type GatewayConfig struct {
BaseURL string `mapstructure:"base_url"` // Gateway API 基础 URL
AppID string `mapstructure:"app_id"` // 应用 ID
AppSecret string `mapstructure:"app_secret"` // 应用密钥
Timeout int `mapstructure:"timeout"` // 超时时间(秒)
}
// S3Config S3 兼容存储配置
type S3Config struct {
Endpoint string `mapstructure:"endpoint"` // 服务端点http://obs-helf.cucloud.cn
Region string `mapstructure:"region"` // 区域cn-langfang-2
Bucket string `mapstructure:"bucket"` // 存储桶名称
AccessKeyID string `mapstructure:"access_key_id"` // 访问密钥 ID
SecretAccessKey string `mapstructure:"secret_access_key"` // 访问密钥
UseSSL bool `mapstructure:"use_ssl"` // 是否使用 SSL
PathStyle bool `mapstructure:"path_style"` // 是否使用路径风格(兼容性)
}
// PresignConfig 预签名 URL 配置
type PresignConfig struct {
UploadExpires time.Duration `mapstructure:"upload_expires"` // 上传 URL 有效期默认15m
DownloadExpires time.Duration `mapstructure:"download_expires"` // 下载 URL 有效期默认24h
}
// WechatConfig 微信配置
type WechatConfig struct {
OfficialAccount OfficialAccountConfig `mapstructure:"official_account"`
}
// OfficialAccountConfig 微信公众号配置
type OfficialAccountConfig struct {
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
Token string `mapstructure:"token"`
AESKey string `mapstructure:"aes_key"`
OAuthRedirectURL string `mapstructure:"oauth_redirect_url"`
}
type requiredField struct {
value string
name string
envName string
}
func (c *Config) ValidateRequired() error {
fields := []requiredField{
{c.Database.Host, "database.host", "JUNHONG_DATABASE_HOST"},
{c.Database.User, "database.user", "JUNHONG_DATABASE_USER"},
{c.Database.Password, "database.password", "JUNHONG_DATABASE_PASSWORD"},
{c.Database.DBName, "database.dbname", "JUNHONG_DATABASE_DBNAME"},
{c.Redis.Address, "redis.address", "JUNHONG_REDIS_ADDRESS"},
{c.JWT.SecretKey, "jwt.secret_key", "JUNHONG_JWT_SECRET_KEY"},
}
var missing []string
for _, f := range fields {
if f.value == "" {
missing = append(missing, fmt.Sprintf(" - %s (环境变量: %s)", f.name, f.envName))
}
}
if len(missing) > 0 {
return fmt.Errorf("缺少必填配置项:\n%s", strings.Join(missing, "\n"))
}
return nil
}
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)")
}
// Port 验证(独立字段)
if c.Redis.Port <= 0 || c.Redis.Port > 65535 {
return fmt.Errorf("invalid configuration: redis.port: port number out of range (current value: %d, expected: 1-65535)", c.Redis.Port)
}
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)
}
// 短信服务验证(可选,配置 GatewayURL 时才验证其他字段)
if c.SMS.GatewayURL != "" {
if c.SMS.Username == "" {
return fmt.Errorf("invalid configuration: sms.username: must be non-empty when gateway_url is configured")
}
if c.SMS.Password == "" {
return fmt.Errorf("invalid configuration: sms.password: must be non-empty when gateway_url is configured")
}
if c.SMS.Signature == "" {
return fmt.Errorf("invalid configuration: sms.signature: must be non-empty when gateway_url is configured")
}
if c.SMS.Timeout > 0 && (c.SMS.Timeout < 5*time.Second || c.SMS.Timeout > 60*time.Second) {
return fmt.Errorf("invalid configuration: sms.timeout: duration out of range (current value: %s, expected: 5s-60s)", c.SMS.Timeout)
}
}
// JWT 验证SecretKey 必填验证在 ValidateRequired 中处理)
if len(c.JWT.SecretKey) > 0 && len(c.JWT.SecretKey) < 32 {
return fmt.Errorf("invalid configuration: jwt.secret_key: secret key too short (current length: %d, expected: >= 32)", len(c.JWT.SecretKey))
}
if c.JWT.TokenDuration < 1*time.Hour || c.JWT.TokenDuration > 720*time.Hour {
return fmt.Errorf("invalid configuration: jwt.token_duration: duration out of range (current value: %s, expected: 1h-720h)", c.JWT.TokenDuration)
}
if c.JWT.AccessTokenTTL < 1*time.Hour || c.JWT.AccessTokenTTL > 168*time.Hour {
return fmt.Errorf("invalid configuration: jwt.access_token_ttl: duration out of range (current value: %s, expected: 1h-168h)", c.JWT.AccessTokenTTL)
}
if c.JWT.RefreshTokenTTL < 24*time.Hour || c.JWT.RefreshTokenTTL > 720*time.Hour {
return fmt.Errorf("invalid configuration: jwt.refresh_token_ttl: duration out of range (current value: %s, expected: 24h-720h)", c.JWT.RefreshTokenTTL)
}
// Gateway 验证(可选,配置 BaseURL 时才验证其他字段)
if c.Gateway.BaseURL != "" {
if !strings.HasPrefix(c.Gateway.BaseURL, "http://") && !strings.HasPrefix(c.Gateway.BaseURL, "https://") {
return fmt.Errorf("invalid configuration: gateway.base_url: must start with http:// or https:// (current value: %s)", c.Gateway.BaseURL)
}
if c.Gateway.AppID == "" {
return fmt.Errorf("invalid configuration: gateway.app_id: must be non-empty when base_url is configured")
}
if c.Gateway.AppSecret == "" {
return fmt.Errorf("invalid configuration: gateway.app_secret: must be non-empty when base_url is configured")
}
if c.Gateway.Timeout < 5 || c.Gateway.Timeout > 300 {
return fmt.Errorf("invalid configuration: gateway.timeout: timeout out of range (current value: %d, expected: 5-300 seconds)", c.Gateway.Timeout)
}
}
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
}