package service import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "strconv" "github.com/Wei-Shaw/sub2api/internal/config" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" ) var ( ErrRegistrationDisabled = infraerrors.Forbidden("REGISTRATION_DISABLED", "registration is currently disabled") ErrSettingNotFound = infraerrors.NotFound("SETTING_NOT_FOUND", "setting not found") ) type SettingRepository interface { Get(ctx context.Context, key string) (*Setting, error) GetValue(ctx context.Context, key string) (string, error) Set(ctx context.Context, key, value string) error GetMultiple(ctx context.Context, keys []string) (map[string]string, error) SetMultiple(ctx context.Context, settings map[string]string) error GetAll(ctx context.Context) (map[string]string, error) Delete(ctx context.Context, key string) error } // SettingService 系统设置服务 type SettingService struct { settingRepo SettingRepository cfg *config.Config } // NewSettingService 创建系统设置服务实例 func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService { return &SettingService{ settingRepo: settingRepo, cfg: cfg, } } // GetAllSettings 获取所有系统设置 func (s *SettingService) GetAllSettings(ctx context.Context) (*SystemSettings, error) { settings, err := s.settingRepo.GetAll(ctx) if err != nil { return nil, fmt.Errorf("get all settings: %w", err) } return s.parseSettings(settings), nil } // GetPublicSettings 获取公开设置(无需登录) func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings, error) { keys := []string{ SettingKeyRegistrationEnabled, SettingKeyEmailVerifyEnabled, SettingKeyTurnstileEnabled, SettingKeyTurnstileSiteKey, SettingKeySiteName, SettingKeySiteLogo, SettingKeySiteSubtitle, SettingKeyAPIBaseURL, SettingKeyContactInfo, SettingKeyDocURL, } settings, err := s.settingRepo.GetMultiple(ctx, keys) if err != nil { return nil, fmt.Errorf("get public settings: %w", err) } return &PublicSettings{ RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true", EmailVerifyEnabled: settings[SettingKeyEmailVerifyEnabled] == "true", TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true", TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey], SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"), SiteLogo: settings[SettingKeySiteLogo], SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"), APIBaseURL: settings[SettingKeyAPIBaseURL], ContactInfo: settings[SettingKeyContactInfo], DocURL: settings[SettingKeyDocURL], LinuxDoOAuthEnabled: s.cfg != nil && s.cfg.LinuxDo.Enabled, }, nil } // UpdateSettings 更新系统设置 func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSettings) error { updates := make(map[string]string) // 注册设置 updates[SettingKeyRegistrationEnabled] = strconv.FormatBool(settings.RegistrationEnabled) updates[SettingKeyEmailVerifyEnabled] = strconv.FormatBool(settings.EmailVerifyEnabled) // 邮件服务设置(只有非空才更新密码) updates[SettingKeySMTPHost] = settings.SMTPHost updates[SettingKeySMTPPort] = strconv.Itoa(settings.SMTPPort) updates[SettingKeySMTPUsername] = settings.SMTPUsername if settings.SMTPPassword != "" { updates[SettingKeySMTPPassword] = settings.SMTPPassword } updates[SettingKeySMTPFrom] = settings.SMTPFrom updates[SettingKeySMTPFromName] = settings.SMTPFromName updates[SettingKeySMTPUseTLS] = strconv.FormatBool(settings.SMTPUseTLS) // Cloudflare Turnstile 设置(只有非空才更新密钥) updates[SettingKeyTurnstileEnabled] = strconv.FormatBool(settings.TurnstileEnabled) updates[SettingKeyTurnstileSiteKey] = settings.TurnstileSiteKey if settings.TurnstileSecretKey != "" { updates[SettingKeyTurnstileSecretKey] = settings.TurnstileSecretKey } // OEM设置 updates[SettingKeySiteName] = settings.SiteName updates[SettingKeySiteLogo] = settings.SiteLogo updates[SettingKeySiteSubtitle] = settings.SiteSubtitle updates[SettingKeyAPIBaseURL] = settings.APIBaseURL updates[SettingKeyContactInfo] = settings.ContactInfo updates[SettingKeyDocURL] = settings.DocURL // 默认配置 updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency) updates[SettingKeyDefaultBalance] = strconv.FormatFloat(settings.DefaultBalance, 'f', 8, 64) // Model fallback configuration updates[SettingKeyEnableModelFallback] = strconv.FormatBool(settings.EnableModelFallback) updates[SettingKeyFallbackModelAnthropic] = settings.FallbackModelAnthropic updates[SettingKeyFallbackModelOpenAI] = settings.FallbackModelOpenAI updates[SettingKeyFallbackModelGemini] = settings.FallbackModelGemini updates[SettingKeyFallbackModelAntigravity] = settings.FallbackModelAntigravity // Identity patch configuration (Claude -> Gemini) updates[SettingKeyEnableIdentityPatch] = strconv.FormatBool(settings.EnableIdentityPatch) updates[SettingKeyIdentityPatchPrompt] = settings.IdentityPatchPrompt return s.settingRepo.SetMultiple(ctx, updates) } // IsRegistrationEnabled 检查是否开放注册 func (s *SettingService) IsRegistrationEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEnabled) if err != nil { // 安全默认:如果设置不存在或查询出错,默认关闭注册 return false } return value == "true" } // IsEmailVerifyEnabled 检查是否开启邮件验证 func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyEmailVerifyEnabled) if err != nil { return false } return value == "true" } // GetSiteName 获取网站名称 func (s *SettingService) GetSiteName(ctx context.Context) string { value, err := s.settingRepo.GetValue(ctx, SettingKeySiteName) if err != nil || value == "" { return "Sub2API" } return value } // GetDefaultConcurrency 获取默认并发量 func (s *SettingService) GetDefaultConcurrency(ctx context.Context) int { value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultConcurrency) if err != nil { return s.cfg.Default.UserConcurrency } if v, err := strconv.Atoi(value); err == nil && v > 0 { return v } return s.cfg.Default.UserConcurrency } // GetDefaultBalance 获取默认余额 func (s *SettingService) GetDefaultBalance(ctx context.Context) float64 { value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultBalance) if err != nil { return s.cfg.Default.UserBalance } if v, err := strconv.ParseFloat(value, 64); err == nil && v >= 0 { return v } return s.cfg.Default.UserBalance } // InitializeDefaultSettings 初始化默认设置 func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { // 检查是否已有设置 _, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEnabled) if err == nil { // 已有设置,不需要初始化 return nil } if !errors.Is(err, ErrSettingNotFound) { return fmt.Errorf("check existing settings: %w", err) } // 初始化默认设置 defaults := map[string]string{ SettingKeyRegistrationEnabled: "true", SettingKeyEmailVerifyEnabled: "false", SettingKeySiteName: "Sub2API", SettingKeySiteLogo: "", SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency), SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64), SettingKeySMTPPort: "587", SettingKeySMTPUseTLS: "false", // Model fallback defaults SettingKeyEnableModelFallback: "false", SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022", SettingKeyFallbackModelOpenAI: "gpt-4o", SettingKeyFallbackModelGemini: "gemini-2.5-pro", SettingKeyFallbackModelAntigravity: "gemini-2.5-pro", // Identity patch defaults SettingKeyEnableIdentityPatch: "true", SettingKeyIdentityPatchPrompt: "", } return s.settingRepo.SetMultiple(ctx, defaults) } // parseSettings 解析设置到结构体 func (s *SettingService) parseSettings(settings map[string]string) *SystemSettings { result := &SystemSettings{ RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true", EmailVerifyEnabled: settings[SettingKeyEmailVerifyEnabled] == "true", SMTPHost: settings[SettingKeySMTPHost], SMTPUsername: settings[SettingKeySMTPUsername], SMTPFrom: settings[SettingKeySMTPFrom], SMTPFromName: settings[SettingKeySMTPFromName], SMTPUseTLS: settings[SettingKeySMTPUseTLS] == "true", SMTPPasswordConfigured: settings[SettingKeySMTPPassword] != "", TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true", TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey], TurnstileSecretKeyConfigured: settings[SettingKeyTurnstileSecretKey] != "", SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"), SiteLogo: settings[SettingKeySiteLogo], SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"), APIBaseURL: settings[SettingKeyAPIBaseURL], ContactInfo: settings[SettingKeyContactInfo], DocURL: settings[SettingKeyDocURL], } // 解析整数类型 if port, err := strconv.Atoi(settings[SettingKeySMTPPort]); err == nil { result.SMTPPort = port } else { result.SMTPPort = 587 } if concurrency, err := strconv.Atoi(settings[SettingKeyDefaultConcurrency]); err == nil { result.DefaultConcurrency = concurrency } else { result.DefaultConcurrency = s.cfg.Default.UserConcurrency } // 解析浮点数类型 if balance, err := strconv.ParseFloat(settings[SettingKeyDefaultBalance], 64); err == nil { result.DefaultBalance = balance } else { result.DefaultBalance = s.cfg.Default.UserBalance } // 敏感信息直接返回,方便测试连接时使用 result.SMTPPassword = settings[SettingKeySMTPPassword] result.TurnstileSecretKey = settings[SettingKeyTurnstileSecretKey] // Model fallback settings result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true" result.FallbackModelAnthropic = s.getStringOrDefault(settings, SettingKeyFallbackModelAnthropic, "claude-3-5-sonnet-20241022") result.FallbackModelOpenAI = s.getStringOrDefault(settings, SettingKeyFallbackModelOpenAI, "gpt-4o") result.FallbackModelGemini = s.getStringOrDefault(settings, SettingKeyFallbackModelGemini, "gemini-2.5-pro") result.FallbackModelAntigravity = s.getStringOrDefault(settings, SettingKeyFallbackModelAntigravity, "gemini-2.5-pro") // Identity patch settings (default: enabled, to preserve existing behavior) if v, ok := settings[SettingKeyEnableIdentityPatch]; ok && v != "" { result.EnableIdentityPatch = v == "true" } else { result.EnableIdentityPatch = true } result.IdentityPatchPrompt = settings[SettingKeyIdentityPatchPrompt] return result } // getStringOrDefault 获取字符串值或默认值 func (s *SettingService) getStringOrDefault(settings map[string]string, key, defaultValue string) string { if value, ok := settings[key]; ok && value != "" { return value } return defaultValue } // IsTurnstileEnabled 检查是否启用 Turnstile 验证 func (s *SettingService) IsTurnstileEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyTurnstileEnabled) if err != nil { return false } return value == "true" } // GetTurnstileSecretKey 获取 Turnstile Secret Key func (s *SettingService) GetTurnstileSecretKey(ctx context.Context) string { value, err := s.settingRepo.GetValue(ctx, SettingKeyTurnstileSecretKey) if err != nil { return "" } return value } // IsIdentityPatchEnabled 检查是否启用身份补丁(Claude -> Gemini systemInstruction 注入) func (s *SettingService) IsIdentityPatchEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyEnableIdentityPatch) if err != nil { // 默认开启,保持兼容 return true } return value == "true" } // GetIdentityPatchPrompt 获取自定义身份补丁提示词(为空表示使用内置默认模板) func (s *SettingService) GetIdentityPatchPrompt(ctx context.Context) string { value, err := s.settingRepo.GetValue(ctx, SettingKeyIdentityPatchPrompt) if err != nil { return "" } return value } // GenerateAdminAPIKey 生成新的管理员 API Key func (s *SettingService) GenerateAdminAPIKey(ctx context.Context) (string, error) { // 生成 32 字节随机数 = 64 位十六进制字符 bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", fmt.Errorf("generate random bytes: %w", err) } key := AdminAPIKeyPrefix + hex.EncodeToString(bytes) // 存储到 settings 表 if err := s.settingRepo.Set(ctx, SettingKeyAdminAPIKey, key); err != nil { return "", fmt.Errorf("save admin api key: %w", err) } return key, nil } // GetAdminAPIKeyStatus 获取管理员 API Key 状态 // 返回脱敏的 key、是否存在、错误 func (s *SettingService) GetAdminAPIKeyStatus(ctx context.Context) (maskedKey string, exists bool, err error) { key, err := s.settingRepo.GetValue(ctx, SettingKeyAdminAPIKey) if err != nil { if errors.Is(err, ErrSettingNotFound) { return "", false, nil } return "", false, err } if key == "" { return "", false, nil } // 脱敏:显示前 10 位和后 4 位 if len(key) > 14 { maskedKey = key[:10] + "..." + key[len(key)-4:] } else { maskedKey = key } return maskedKey, true, nil } // GetAdminAPIKey 获取完整的管理员 API Key(仅供内部验证使用) // 如果未配置返回空字符串和 nil 错误,只有数据库错误时才返回 error func (s *SettingService) GetAdminAPIKey(ctx context.Context) (string, error) { key, err := s.settingRepo.GetValue(ctx, SettingKeyAdminAPIKey) if err != nil { if errors.Is(err, ErrSettingNotFound) { return "", nil // 未配置,返回空字符串 } return "", err // 数据库错误 } return key, nil } // DeleteAdminAPIKey 删除管理员 API Key func (s *SettingService) DeleteAdminAPIKey(ctx context.Context) error { return s.settingRepo.Delete(ctx, SettingKeyAdminAPIKey) } // IsModelFallbackEnabled 检查是否启用模型兜底机制 func (s *SettingService) IsModelFallbackEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyEnableModelFallback) if err != nil { return false // Default: disabled } return value == "true" } // GetFallbackModel 获取指定平台的兜底模型 func (s *SettingService) GetFallbackModel(ctx context.Context, platform string) string { var key string var defaultModel string switch platform { case PlatformAnthropic: key = SettingKeyFallbackModelAnthropic defaultModel = "claude-3-5-sonnet-20241022" case PlatformOpenAI: key = SettingKeyFallbackModelOpenAI defaultModel = "gpt-4o" case PlatformGemini: key = SettingKeyFallbackModelGemini defaultModel = "gemini-2.5-pro" case PlatformAntigravity: key = SettingKeyFallbackModelAntigravity defaultModel = "gemini-2.5-pro" default: return "" } value, err := s.settingRepo.GetValue(ctx, key) if err != nil || value == "" { return defaultModel } return value }