package sysconfig import ( "bindbox-game/configs" "bindbox-game/internal/pkg/cryptoaes" "bindbox-game/internal/pkg/logger" "bindbox-game/internal/repository/mysql" "context" "strings" "sync" "time" "go.uber.org/zap" ) // 敏感配置 Key 后缀列表,这些 Key 的值需要加密存储 var sensitiveKeySuffixes = []string{ "secret_key", "secret_id", "app_secret", "access_key_secret", "api_v3_key", "private_key", "public_key", } // 配置 Key 常量 const ( // COS 配置 KeyCOSBucket = "cos.bucket" KeyCOSRegion = "cos.region" KeyCOSSecretID = "cos.secret_id" KeyCOSSecretKey = "cos.secret_key" KeyCOSBaseURL = "cos.base_url" // 微信小程序配置 KeyWechatAppID = "wechat.app_id" KeyWechatAppSecret = "wechat.app_secret" KeyWechatLotteryResultTemplateID = "wechat.lottery_result_template_id" // 微信支付配置 KeyWechatPayMchID = "wechatpay.mchid" KeyWechatPaySerialNo = "wechatpay.serial_no" KeyWechatPayPrivateKey = "wechatpay.private_key" KeyWechatPayApiV3Key = "wechatpay.api_v3_key" KeyWechatPayNotifyURL = "wechatpay.notify_url" KeyWechatPayPublicKeyID = "wechatpay.public_key_id" KeyWechatPayPublicKey = "wechatpay.public_key" // 阿里云短信配置 KeyAliyunSMSAccessKeyID = "aliyun_sms.access_key_id" KeyAliyunSMSAccessKeySecret = "aliyun_sms.access_key_secret" KeyAliyunSMSSignName = "aliyun_sms.sign_name" KeyAliyunSMSTemplateCode = "aliyun_sms.template_code" // 抖音小程序配置 KeyDouyinAppID = "douyin.app_id" KeyDouyinAppSecret = "douyin.app_secret" KeyDouyinNotifyURL = "douyin.notify_url" KeyDouyinPayAppID = "douyin.pay_app_id" KeyDouyinPaySecret = "douyin.pay_secret" KeyDouyinPaySalt = "douyin.pay_salt" ) // COSConfig COS 配置结构 type COSConfig struct { Bucket string Region string SecretID string SecretKey string BaseURL string } // WechatConfig 微信小程序配置结构 type WechatConfig struct { AppID string AppSecret string LotteryResultTemplateID string } // WechatPayConfig 微信支付配置结构 type WechatPayConfig struct { MchID string SerialNo string PrivateKey string // Base64 编码的私钥内容 ApiV3Key string NotifyURL string PublicKeyID string PublicKey string // Base64 编码的公钥内容 } // AliyunSMSConfig 阿里云短信配置结构 type AliyunSMSConfig struct { AccessKeyID string AccessKeySecret string SignName string TemplateCode string } // DouyinConfig 抖音小程序配置结构 type DouyinConfig struct { AppID string AppSecret string NotifyURL string PayAppID string // 支付应用ID (担保支付) PaySecret string // 支付密钥 PaySalt string // 支付盐 } // DynamicConfig 动态配置服务 type DynamicConfig struct { cache sync.Map // key -> string value repo Service logger logger.CustomLogger encKey string // 加密密钥 (32字节用于AES-256) ttl time.Duration // 缓存过期时间 loadedAt time.Time mu sync.RWMutex } // NewDynamicConfig 创建动态配置服务实例 func NewDynamicConfig(l logger.CustomLogger, db mysql.Repo) *DynamicConfig { // 使用 CommitMasterKey 的前32字节作为 AES-256 密钥 masterKey := configs.Get().Random.CommitMasterKey encKey := masterKey if len(encKey) > 32 { encKey = encKey[:32] } else if len(encKey) < 32 { // 不足32字节则填充 encKey = encKey + strings.Repeat("0", 32-len(encKey)) } return &DynamicConfig{ repo: New(l, db), logger: l, encKey: encKey, ttl: 5 * time.Minute, } } // IsSensitiveKey 判断是否为敏感配置 Key func IsSensitiveKey(key string) bool { for _, suffix := range sensitiveKeySuffixes { if strings.HasSuffix(key, suffix) { return true } } return false } // encryptValue 加密配置值 func (d *DynamicConfig) encryptValue(value string) (string, error) { return cryptoaes.Encrypt(d.encKey, value) } // decryptValue 解密配置值 func (d *DynamicConfig) decryptValue(value string) (string, error) { return cryptoaes.Decrypt(d.encKey, value) } // LoadAll 启动时预加载所有配置到缓存 func (d *DynamicConfig) LoadAll(ctx context.Context) error { items, _, err := d.repo.List(ctx, 1, 10000, "") // 获取所有配置 if err != nil { return err } for _, item := range items { value := item.ConfigValue // 敏感配置需要解密 if IsSensitiveKey(item.ConfigKey) && value != "" { if decrypted, err := d.decryptValue(value); err == nil { value = decrypted } else { d.logger.Error("解密配置失败", zap.String("key", item.ConfigKey), zap.Error(err)) // 解密失败,尝试使用原始值(可能未加密) } } d.cache.Store(item.ConfigKey, value) } d.mu.Lock() d.loadedAt = time.Now() d.mu.Unlock() d.logger.Info("动态配置加载完成", zap.Int("count", len(items))) return nil } // Refresh 刷新缓存 func (d *DynamicConfig) Refresh(ctx context.Context) error { return d.LoadAll(ctx) } // NeedsRefresh 检查是否需要刷新缓存 func (d *DynamicConfig) NeedsRefresh() bool { d.mu.RLock() defer d.mu.RUnlock() return time.Since(d.loadedAt) > d.ttl } // Get 获取配置值(带缓存) func (d *DynamicConfig) Get(ctx context.Context, key string) string { // 1. 从缓存读取 if v, ok := d.cache.Load(key); ok { return v.(string) } // 2. 从数据库读取 cfg, err := d.repo.GetByKey(ctx, key) if err == nil && cfg != nil { value := cfg.ConfigValue // 敏感配置需要解密 if IsSensitiveKey(key) && value != "" { if decrypted, err := d.decryptValue(value); err == nil { value = decrypted } } d.cache.Store(key, value) return value } // 3. 返回空字符串(调用方需要处理 fallback) return "" } // GetWithFallback 获取配置值,如果不存在则返回 fallback 值 func (d *DynamicConfig) GetWithFallback(ctx context.Context, key, fallback string) string { if v := d.Get(ctx, key); v != "" { return v } return fallback } // Set 设置配置值(自动处理加密) func (d *DynamicConfig) Set(ctx context.Context, key, value, remark string) error { storeValue := value // 敏感配置需要加密 if IsSensitiveKey(key) && value != "" { encrypted, err := d.encryptValue(value) if err != nil { return err } storeValue = encrypted } _, err := d.repo.UpsertByKey(ctx, key, storeValue, remark) if err != nil { return err } // 更新缓存(存储明文) d.cache.Store(key, value) return nil } // GetCOS 获取 COS 配置 func (d *DynamicConfig) GetCOS(ctx context.Context) COSConfig { staticCfg := configs.Get().COS return COSConfig{ Bucket: d.GetWithFallback(ctx, KeyCOSBucket, staticCfg.Bucket), Region: d.GetWithFallback(ctx, KeyCOSRegion, staticCfg.Region), SecretID: d.GetWithFallback(ctx, KeyCOSSecretID, staticCfg.SecretID), SecretKey: d.GetWithFallback(ctx, KeyCOSSecretKey, staticCfg.SecretKey), BaseURL: d.GetWithFallback(ctx, KeyCOSBaseURL, staticCfg.BaseURL), } } // GetWechat 获取微信小程序配置 func (d *DynamicConfig) GetWechat(ctx context.Context) WechatConfig { staticCfg := configs.Get().Wechat return WechatConfig{ AppID: d.GetWithFallback(ctx, KeyWechatAppID, staticCfg.AppID), AppSecret: d.GetWithFallback(ctx, KeyWechatAppSecret, staticCfg.AppSecret), LotteryResultTemplateID: d.GetWithFallback(ctx, KeyWechatLotteryResultTemplateID, staticCfg.LotteryResultTemplateID), } } // GetWechatPay 获取微信支付配置 func (d *DynamicConfig) GetWechatPay(ctx context.Context) WechatPayConfig { staticCfg := configs.Get().WechatPay return WechatPayConfig{ MchID: d.GetWithFallback(ctx, KeyWechatPayMchID, staticCfg.MchID), SerialNo: d.GetWithFallback(ctx, KeyWechatPaySerialNo, staticCfg.SerialNo), PrivateKey: d.Get(ctx, KeyWechatPayPrivateKey), // 私钥无静态 fallback,需要 Base64 存储 ApiV3Key: d.GetWithFallback(ctx, KeyWechatPayApiV3Key, staticCfg.ApiV3Key), NotifyURL: d.GetWithFallback(ctx, KeyWechatPayNotifyURL, staticCfg.NotifyURL), PublicKeyID: d.GetWithFallback(ctx, KeyWechatPayPublicKeyID, staticCfg.PublicKeyID), PublicKey: d.Get(ctx, KeyWechatPayPublicKey), // 公钥无静态 fallback,需要 Base64 存储 } } // GetAliyunSMS 获取阿里云短信配置 func (d *DynamicConfig) GetAliyunSMS(ctx context.Context) AliyunSMSConfig { staticCfg := configs.Get().AliyunSMS return AliyunSMSConfig{ AccessKeyID: d.GetWithFallback(ctx, KeyAliyunSMSAccessKeyID, staticCfg.AccessKeyID), AccessKeySecret: d.GetWithFallback(ctx, KeyAliyunSMSAccessKeySecret, staticCfg.AccessKeySecret), SignName: d.GetWithFallback(ctx, KeyAliyunSMSSignName, staticCfg.SignName), TemplateCode: d.GetWithFallback(ctx, KeyAliyunSMSTemplateCode, staticCfg.TemplateCode), } } // GetDouyin 获取抖音小程序配置 func (d *DynamicConfig) GetDouyin(ctx context.Context) DouyinConfig { return DouyinConfig{ AppID: d.Get(ctx, KeyDouyinAppID), AppSecret: d.Get(ctx, KeyDouyinAppSecret), NotifyURL: d.Get(ctx, KeyDouyinNotifyURL), PayAppID: d.Get(ctx, KeyDouyinPayAppID), PaySecret: d.Get(ctx, KeyDouyinPaySecret), PaySalt: d.Get(ctx, KeyDouyinPaySalt), } }