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