317 lines
9.1 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 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),
}
}