382 lines
11 KiB
Go
Executable File
Raw Permalink 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"
"errors"
"fmt"
"strings"
"sync"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)
// 敏感配置 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"
)
// SystemConfig local model for raw GORM access
type SystemConfig struct {
ID int64 `gorm:"primaryKey"`
ConfigKey string `gorm:"uniqueIndex"`
ConfigValue string
Remark string
DeletedAt gorm.DeletedAt
}
func (SystemConfig) TableName() string {
return "system_configs"
}
// 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
db *gorm.DB
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{
db: db.GetDbR(),
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 {
var items []SystemConfig
// Raw GORM query
if err := d.db.WithContext(ctx).Find(&items).Error; 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.cache.Store(item.ConfigKey, value)
// DEBUG: Print keys being cached
if item.ConfigKey == "wechat.app_id" || item.ConfigKey == "wechat.app_secret" {
fmt.Printf("DEBUG LoadAll: Caching key=%s value=%s\n", 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 获取配置值(带缓存)
// Get 获取配置值(带缓存)
func (d *DynamicConfig) Get(ctx context.Context, key string) string {
// 1. 从缓存读取
if v, ok := d.cache.Load(key); ok {
return v.(string)
}
// 2. 从数据库读取 (Raw GORM)
var cfg SystemConfig
// Use WithContext and Where
err := d.db.WithContext(ctx).Where("config_key = ?", key).First(&cfg).Error
if err == nil {
value := cfg.ConfigValue
// 敏感配置需要解密
if IsSensitiveKey(key) && value != "" {
if decrypted, err := d.decryptValue(value); err == nil {
value = decrypted
} else {
d.logger.Warn("Failed to decrypt sensitive key", zap.String("key", key), zap.Error(err))
}
}
d.cache.Store(key, value)
return value
} else {
// Only log warning if not found, don't return error (return empty string)
// d.logger.Warn("Config NOT found in DB", zap.String("key", key), zap.Error(err))
}
// 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
}
// UpdateCache 手动更新缓存(用于 Admin API 调用后同步)
func (d *DynamicConfig) UpdateCache(key, value string) {
// 敏感配置需要解密后再存入缓存?
// 这里假设 Admin API 传入的是明文 value数据库存的是密文。
// 但是 LoadAll 里是从 DB 读出(可能是密文)解密后存缓存。
// cache 里存的是 明文。
// Admin API UpsertByKey 传入的是明文。
d.cache.Store(key, value)
}
// DeleteCache 删除缓存
func (d *DynamicConfig) DeleteCache(key string) {
d.cache.Delete(key)
}
// 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
}
// Upsert logic
var existing SystemConfig
err := d.db.WithContext(ctx).Where("config_key = ?", key).First(&existing).Error
if err == nil {
// Update
existing.ConfigValue = storeValue
existing.Remark = remark
return d.db.WithContext(ctx).Save(&existing).Error
}
// Create
if errors.Is(err, gorm.ErrRecordNotFound) {
newItem := SystemConfig{
ConfigKey: key,
ConfigValue: storeValue,
Remark: remark,
}
return d.db.WithContext(ctx).Create(&newItem).Error
}
return err
}
// 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), // Key content only, no fallback to file path
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), // Key content only
}
}
// 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),
}
}
// innerSubstr 截取字符串,避免越界
func innerSubstr(s string, start, length int) string {
if start >= len(s) {
return ""
}
end := start + length
if end > len(s) {
end = len(s)
}
return s[start:end]
}