382 lines
11 KiB
Go
Executable File
382 lines
11 KiB
Go
Executable File
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]
|
||
}
|