package service import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "log/slog" "math" "strconv" "strings" "time" ) // 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" } // IsRegistrationEmailDomainQuotaEnabled 检查白名单非空时是否放行非白名单域名限量注册。 // 安全默认:设置缺失或查询出错时按关闭处理(保持白名单严格模式)。 func (s *SettingService) IsRegistrationEmailDomainQuotaEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEmailDomainQuotaEnabled) if err != nil { return false } return value == "true" } // GetRegistrationEmailSuffixWhitelist returns normalized registration email suffix whitelist. func (s *SettingService) GetRegistrationEmailSuffixWhitelist(ctx context.Context) []string { value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEmailSuffixWhitelist) if err != nil { return []string{} } return ParseRegistrationEmailSuffixWhitelist(value) } // IsPromoCodeEnabled 检查是否启用优惠码功能 func (s *SettingService) IsPromoCodeEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyPromoCodeEnabled) if err != nil { return true // 默认启用 } return value != "false" } // IsInvitationCodeEnabled 检查是否启用邀请码注册功能 func (s *SettingService) IsInvitationCodeEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyInvitationCodeEnabled) if err != nil { return false // 默认关闭 } return value == "true" } // GetCustomMenuItemsRaw returns the raw JSON string of custom_menu_items setting. func (s *SettingService) GetCustomMenuItemsRaw(ctx context.Context) string { value, err := s.settingRepo.GetValue(ctx, SettingKeyCustomMenuItems) if err != nil { return "[]" } return value } // IsAffiliateEnabled 检查是否启用邀请返利功能(总开关) func (s *SettingService) IsAffiliateEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateEnabled) if err != nil { return false // 默认关闭 } return value == "true" } // IsAffiliateAdminRechargeEnabled reports whether admin balance // deposits should participate in the affiliate rebate program. func (s *SettingService) IsAffiliateAdminRechargeEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateAdminRechargeEnabled) if err != nil { return AdminRechargeRebateEnabledDefault } return value == "true" } // GetAffiliateRebateRatePercent 读取并 clamp 全局返利比例。 // 解析失败、缺失或越界都回退到 AffiliateRebateRateDefault — 该比例从不抛错, // 调用方只关心一个可用的数值。 func (s *SettingService) GetAffiliateRebateRatePercent(ctx context.Context) float64 { raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebateRate) if err != nil { return AffiliateRebateRateDefault } rate, err := strconv.ParseFloat(strings.TrimSpace(raw), 64) if err != nil || math.IsNaN(rate) || math.IsInf(rate, 0) { return AffiliateRebateRateDefault } return clampAffiliateRebateRate(rate) } // GetAffiliateRebateFreezeHours 返回返利冻结期(小时)。 // 返回 0 表示不冻结(向后兼容)。 func (s *SettingService) GetAffiliateRebateFreezeHours(ctx context.Context) int { raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebateFreezeHours) if err != nil { return AffiliateRebateFreezeHoursDefault } hours, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil || hours < 0 { return AffiliateRebateFreezeHoursDefault } if hours > AffiliateRebateFreezeHoursMax { return AffiliateRebateFreezeHoursMax } return hours } // GetAffiliateRebateDurationDays 返回返利有效期(天)。 // 返回 0 表示永久有效。 func (s *SettingService) GetAffiliateRebateDurationDays(ctx context.Context) int { raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebateDurationDays) if err != nil { return AffiliateRebateDurationDaysDefault } days, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil || days < 0 { return AffiliateRebateDurationDaysDefault } if days > AffiliateRebateDurationDaysMax { return AffiliateRebateDurationDaysMax } return days } // GetAffiliateRebatePerInviteeCap 返回单人返利上限。 // 返回 0 表示无上限。 func (s *SettingService) GetAffiliateRebatePerInviteeCap(ctx context.Context) float64 { raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebatePerInviteeCap) if err != nil { return AffiliateRebatePerInviteeCapDefault } cap, err := strconv.ParseFloat(strings.TrimSpace(raw), 64) if err != nil || cap < 0 || math.IsNaN(cap) || math.IsInf(cap, 0) { return AffiliateRebatePerInviteeCapDefault } return cap } // IsPasswordResetEnabled 检查是否启用密码重置功能 // 要求:必须同时开启邮件验证 func (s *SettingService) IsPasswordResetEnabled(ctx context.Context) bool { // Password reset requires email verification to be enabled if !s.IsEmailVerifyEnabled(ctx) { return false } value, err := s.settingRepo.GetValue(ctx, SettingKeyPasswordResetEnabled) if err != nil { return false // 默认关闭 } return value == "true" } // IsTotpEnabled 检查是否启用 TOTP 双因素认证功能 func (s *SettingService) IsTotpEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyTotpEnabled) if err != nil { return false // 默认关闭 } return value == "true" } // PasskeyEnabled reports the effective runtime switch. WebAuthn deployment // configuration remains the security boundary; the database setting can only // disable a valid configured relying party, never replace or weaken it. func (s *SettingService) PasskeyEnabled(ctx context.Context) (bool, error) { if !s.passkeyConfigured() { return false, nil } value, err := s.settingRepo.GetValue(ctx, SettingKeyPasskeyEnabled) if errors.Is(err, ErrSettingNotFound) { return true, nil // configured deployments default to enabled until the admin persists the switch } if err != nil { return false, fmt.Errorf("read passkey setting: %w", err) } return value == "true", nil } // PasskeyConfiguration returns non-secret relying-party configuration for the // admin status UI. Enabled configurations have already passed Config.Validate. func (s *SettingService) PasskeyConfiguration() (configured bool, rpID string, origins []string) { if s == nil || s.cfg == nil { return false, "", []string{} } origins = append([]string{}, s.cfg.WebAuthn.RPOrigins...) return s.cfg.WebAuthn.Enabled, strings.TrimSpace(s.cfg.WebAuthn.RPID), origins } func (s *SettingService) passkeyConfigured() bool { return s != nil && s.cfg != nil && s.cfg.WebAuthn.Enabled } // passkeySettingEnabled must stay ANDed with passkeyConfigured: a stale // "true" row after the WebAuthn config is removed would otherwise make the // admin update gate reject every settings save while the UI toggle is locked. func (s *SettingService) passkeySettingEnabled(settings map[string]string) bool { if !s.passkeyConfigured() { return false } value, ok := settings[SettingKeyPasskeyEnabled] if !ok { return true } return value == "true" } // IsTotpEncryptionKeyConfigured 检查 TOTP 加密密钥是否已手动配置 // 只有手动配置了密钥才允许在管理后台启用 TOTP 功能 func (s *SettingService) IsTotpEncryptionKeyConfigured() bool { return s.cfg.Totp.EncryptionKeyConfigured } // IsSessionBindingEnabled 检查会话 IP/UA 绑定是否启用(默认关闭)。 // 开启时会话与登录时的 IP/User-Agent 绑定,任一变化立即失效并撤销该会话。 // 默认关闭:移动网络/多出口 IP 场景下 IP 频繁变化会导致登录后立即掉线。 func (s *SettingService) IsSessionBindingEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeySessionBindingEnabled) if err != nil { return false // 默认关闭 } return value == "true" } // IsStepUpEnabled 检查敏感操作 step-up 2FA 门控是否启用(默认关闭)。 // 开启时账号/代理导出、备份创建/下载、S3 配置修改、提升管理员等操作 // 要求当前会话在有效期内完成过 TOTP step-up 验证。 func (s *SettingService) IsStepUpEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyStepUpEnabled) if err != nil { return false // 默认关闭 } return value == "true" } // defaultAuditLogRetentionDays 审计日志默认保留天数。 const defaultAuditLogRetentionDays = 180 // GetAuditLogRetentionDays 审计日志保留天数(<=0 表示永久保留,仅支持手动清空)。 func (s *SettingService) GetAuditLogRetentionDays(ctx context.Context) int { value, err := s.settingRepo.GetValue(ctx, SettingKeyAuditLogRetentionDays) if err != nil { return defaultAuditLogRetentionDays } return parseAuditLogRetentionDays(value) } // parseAuditLogRetentionDays 解析保留天数配置,空/非法值回退默认值。 func parseAuditLogRetentionDays(value string) int { value = strings.TrimSpace(value) if value == "" { return defaultAuditLogRetentionDays } n, err := strconv.Atoi(value) if err != nil { return defaultAuditLogRetentionDays } if n < 0 { return 0 } return n } // 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 } // GetDefaultUserRPMLimit 获取新用户默认 RPM 限制(0 = 不限制)。未配置则返回 0。 func (s *SettingService) GetDefaultUserRPMLimit(ctx context.Context) int { value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultUserRPMLimit) if err != nil || value == "" { return 0 } if v, err := strconv.Atoi(value); err == nil && v >= 0 { return v } return 0 } // GetDefaultSubscriptions 获取新用户默认订阅配置列表。 func (s *SettingService) GetDefaultSubscriptions(ctx context.Context) []DefaultSubscriptionSetting { value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultSubscriptions) if err != nil { return nil } return parseDefaultSubscriptions(value) } func (s *SettingService) GetAuthSourceDefaultSettings(ctx context.Context) (*AuthSourceDefaultSettings, error) { keys := []string{ SettingKeyAuthSourceDefaultEmailBalance, SettingKeyAuthSourceDefaultEmailConcurrency, SettingKeyAuthSourceDefaultEmailSubscriptions, SettingKeyAuthSourceDefaultEmailGrantOnSignup, SettingKeyAuthSourceDefaultEmailGrantOnFirstBind, SettingKeyAuthSourceDefaultLinuxDoBalance, SettingKeyAuthSourceDefaultLinuxDoConcurrency, SettingKeyAuthSourceDefaultLinuxDoSubscriptions, SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup, SettingKeyAuthSourceDefaultLinuxDoGrantOnFirstBind, SettingKeyAuthSourceDefaultOIDCBalance, SettingKeyAuthSourceDefaultOIDCConcurrency, SettingKeyAuthSourceDefaultOIDCSubscriptions, SettingKeyAuthSourceDefaultOIDCGrantOnSignup, SettingKeyAuthSourceDefaultOIDCGrantOnFirstBind, SettingKeyAuthSourceDefaultWeChatBalance, SettingKeyAuthSourceDefaultWeChatConcurrency, SettingKeyAuthSourceDefaultWeChatSubscriptions, SettingKeyAuthSourceDefaultWeChatGrantOnSignup, SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind, SettingKeyAuthSourceDefaultGitHubBalance, SettingKeyAuthSourceDefaultGitHubConcurrency, SettingKeyAuthSourceDefaultGitHubSubscriptions, SettingKeyAuthSourceDefaultGitHubGrantOnSignup, SettingKeyAuthSourceDefaultGitHubGrantOnFirstBind, SettingKeyAuthSourceDefaultGoogleBalance, SettingKeyAuthSourceDefaultGoogleConcurrency, SettingKeyAuthSourceDefaultGoogleSubscriptions, SettingKeyAuthSourceDefaultGoogleGrantOnSignup, SettingKeyAuthSourceDefaultGoogleGrantOnFirstBind, SettingKeyAuthSourceDefaultDingTalkBalance, SettingKeyAuthSourceDefaultDingTalkConcurrency, SettingKeyAuthSourceDefaultDingTalkSubscriptions, SettingKeyAuthSourceDefaultDingTalkGrantOnSignup, SettingKeyAuthSourceDefaultDingTalkGrantOnFirstBind, SettingKeyAuthSourcePlatformQuotas("email"), SettingKeyAuthSourcePlatformQuotas("linuxdo"), SettingKeyAuthSourcePlatformQuotas("oidc"), SettingKeyAuthSourcePlatformQuotas("wechat"), SettingKeyAuthSourcePlatformQuotas("github"), SettingKeyAuthSourcePlatformQuotas("google"), SettingKeyAuthSourcePlatformQuotas("dingtalk"), SettingKeyForceEmailOnThirdPartySignup, } settings, err := s.settingRepo.GetMultiple(ctx, keys) if err != nil { return nil, fmt.Errorf("get auth source default settings: %w", err) } return &AuthSourceDefaultSettings{ Email: parseProviderDefaultGrantSettings(settings, emailAuthSourceDefaultKeys), LinuxDo: parseProviderDefaultGrantSettings(settings, linuxDoAuthSourceDefaultKeys), OIDC: parseProviderDefaultGrantSettings(settings, oidcAuthSourceDefaultKeys), WeChat: parseProviderDefaultGrantSettings(settings, weChatAuthSourceDefaultKeys), GitHub: parseProviderDefaultGrantSettings(settings, gitHubAuthSourceDefaultKeys), Google: parseProviderDefaultGrantSettings(settings, googleAuthSourceDefaultKeys), DingTalk: parseProviderDefaultGrantSettings(settings, dingTalkAuthSourceDefaultKeys), ForceEmailOnThirdPartySignup: settings[SettingKeyForceEmailOnThirdPartySignup] == "true", }, nil } func (s *SettingService) ResolveAuthSourceGrantSettings(ctx context.Context, signupSource string, firstBind bool) (ProviderDefaultGrantSettings, bool, error) { result := ProviderDefaultGrantSettings{ Balance: s.GetDefaultBalance(ctx), Concurrency: s.GetDefaultConcurrency(ctx), Subscriptions: s.GetDefaultSubscriptions(ctx), } defaults, err := s.GetAuthSourceDefaultSettings(ctx) if err != nil { return result, false, err } providerDefaults, ok := authSourceSignupSettings(defaults, signupSource) if !ok { return result, false, nil } enabled := providerDefaults.GrantOnSignup if firstBind { enabled = providerDefaults.GrantOnFirstBind } if !enabled { return result, false, nil } return mergeProviderDefaultGrantSettings(result, providerDefaults), true, nil } func (s *SettingService) UpdateAuthSourceDefaultSettings(ctx context.Context, settings *AuthSourceDefaultSettings) error { updates, err := s.buildAuthSourceDefaultUpdates(ctx, settings) if err != nil { return err } if len(updates) == 0 { return nil } if err := s.settingRepo.SetMultiple(ctx, updates); err != nil { return fmt.Errorf("update auth source default settings: %w", err) } return nil } // 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 } // TencentCaptchaConfig contains the credentials required by Tencent Cloud's // ticket verification API. It must never be returned by a public handler. type TencentCaptchaConfig struct { Enabled bool AppID string AppSecretKey string CloudSecretID string CloudSecretKey string Region string } // AliyunCaptchaConfig contains the credentials required by Aliyun Captcha 2.0's // server-side verification API. It must never be returned by a public handler. type AliyunCaptchaConfig struct { Enabled bool AccessKeyID string AccessKeySecret string SceneID string Region string } type CaptchaProviderConfig struct { TurnstileEnabled bool TurnstileSecretKey string Tencent TencentCaptchaConfig Aliyun AliyunCaptchaConfig } func (s *SettingService) GetCaptchaProviderConfig(ctx context.Context) (CaptchaProviderConfig, error) { values, err := s.settingRepo.GetMultiple(ctx, []string{ SettingKeyTurnstileEnabled, SettingKeyTurnstileSecretKey, SettingKeyTencentCaptchaEnabled, SettingKeyTencentCaptchaAppID, SettingKeyTencentCaptchaAppSecretKey, SettingKeyTencentCaptchaCloudSecretID, SettingKeyTencentCaptchaCloudSecretKey, SettingKeyTencentCaptchaRegion, SettingKeyAliyunCaptchaEnabled, SettingKeyAliyunCaptchaAccessKeyID, SettingKeyAliyunCaptchaAccessKeySecret, SettingKeyAliyunCaptchaSceneID, SettingKeyAliyunCaptchaRegion, }) if err != nil { return CaptchaProviderConfig{}, fmt.Errorf("read captcha provider settings: %w", err) } return CaptchaProviderConfig{ TurnstileEnabled: values[SettingKeyTurnstileEnabled] == "true", TurnstileSecretKey: values[SettingKeyTurnstileSecretKey], Tencent: TencentCaptchaConfig{ Enabled: values[SettingKeyTencentCaptchaEnabled] == "true", AppID: values[SettingKeyTencentCaptchaAppID], AppSecretKey: values[SettingKeyTencentCaptchaAppSecretKey], CloudSecretID: values[SettingKeyTencentCaptchaCloudSecretID], CloudSecretKey: values[SettingKeyTencentCaptchaCloudSecretKey], Region: normalizeTencentCaptchaRegion(values[SettingKeyTencentCaptchaRegion]), }, Aliyun: AliyunCaptchaConfig{ Enabled: values[SettingKeyAliyunCaptchaEnabled] == "true", AccessKeyID: values[SettingKeyAliyunCaptchaAccessKeyID], AccessKeySecret: values[SettingKeyAliyunCaptchaAccessKeySecret], SceneID: values[SettingKeyAliyunCaptchaSceneID], Region: normalizeAliyunCaptchaRegion(values[SettingKeyAliyunCaptchaRegion]), }, }, nil } func (s *SettingService) IsTencentCaptchaEnabled(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyTencentCaptchaEnabled) return err == nil && value == "true" } func (s *SettingService) GetTencentCaptchaConfig(ctx context.Context) TencentCaptchaConfig { config, err := s.GetCaptchaProviderConfig(ctx) if err != nil { return TencentCaptchaConfig{} } return config.Tencent } // 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 } // GetOverloadCooldownSettings 获取529过载冷却配置 func (s *SettingService) GetOverloadCooldownSettings(ctx context.Context) (*OverloadCooldownSettings, error) { value, err := s.settingRepo.GetValue(ctx, SettingKeyOverloadCooldownSettings) if err != nil { if errors.Is(err, ErrSettingNotFound) { return DefaultOverloadCooldownSettings(), nil } return nil, fmt.Errorf("get overload cooldown settings: %w", err) } if value == "" { return DefaultOverloadCooldownSettings(), nil } var settings OverloadCooldownSettings if err := json.Unmarshal([]byte(value), &settings); err != nil { return DefaultOverloadCooldownSettings(), nil } // 修正配置值范围 if settings.CooldownMinutes < 1 { settings.CooldownMinutes = 1 } if settings.CooldownMinutes > 120 { settings.CooldownMinutes = 120 } return &settings, nil } // SetOverloadCooldownSettings 设置529过载冷却配置 func (s *SettingService) SetOverloadCooldownSettings(ctx context.Context, settings *OverloadCooldownSettings) error { if settings == nil { return fmt.Errorf("settings cannot be nil") } // 禁用时修正为合法值即可,不拒绝请求 if settings.CooldownMinutes < 1 || settings.CooldownMinutes > 120 { if settings.Enabled { return fmt.Errorf("cooldown_minutes must be between 1-120") } settings.CooldownMinutes = 10 // 禁用状态下归一化为默认值 } data, err := json.Marshal(settings) if err != nil { return fmt.Errorf("marshal overload cooldown settings: %w", err) } return s.settingRepo.Set(ctx, SettingKeyOverloadCooldownSettings, string(data)) } // GetRateLimit429CooldownSettings 获取429默认回避配置 func (s *SettingService) GetRateLimit429CooldownSettings(ctx context.Context) (*RateLimit429CooldownSettings, error) { value, err := s.settingRepo.GetValue(ctx, SettingKeyRateLimit429CooldownSettings) if err != nil { if errors.Is(err, ErrSettingNotFound) { return DefaultRateLimit429CooldownSettings(), nil } return nil, fmt.Errorf("get 429 cooldown settings: %w", err) } if value == "" { return DefaultRateLimit429CooldownSettings(), nil } var settings RateLimit429CooldownSettings if err := json.Unmarshal([]byte(value), &settings); err != nil { return DefaultRateLimit429CooldownSettings(), nil } if settings.CooldownSeconds < 1 { settings.CooldownSeconds = 1 } if settings.CooldownSeconds > 7200 { settings.CooldownSeconds = 7200 } return &settings, nil } // SetRateLimit429CooldownSettings 设置429默认回避配置 func (s *SettingService) SetRateLimit429CooldownSettings(ctx context.Context, settings *RateLimit429CooldownSettings) error { if settings == nil { return fmt.Errorf("settings cannot be nil") } if settings.CooldownSeconds < 1 || settings.CooldownSeconds > 7200 { if settings.Enabled { return fmt.Errorf("cooldown_seconds must be between 1-7200") } settings.CooldownSeconds = 5 } data, err := json.Marshal(settings) if err != nil { return fmt.Errorf("marshal 429 cooldown settings: %w", err) } return s.settingRepo.Set(ctx, SettingKeyRateLimit429CooldownSettings, string(data)) } // GetStreamTimeoutSettings 获取流超时处理配置 func (s *SettingService) GetStreamTimeoutSettings(ctx context.Context) (*StreamTimeoutSettings, error) { value, err := s.settingRepo.GetValue(ctx, SettingKeyStreamTimeoutSettings) if err != nil { if errors.Is(err, ErrSettingNotFound) { return DefaultStreamTimeoutSettings(), nil } return nil, fmt.Errorf("get stream timeout settings: %w", err) } if value == "" { return DefaultStreamTimeoutSettings(), nil } var settings StreamTimeoutSettings if err := json.Unmarshal([]byte(value), &settings); err != nil { return DefaultStreamTimeoutSettings(), nil } // 验证并修正配置值 if settings.TempUnschedMinutes < 1 { settings.TempUnschedMinutes = 1 } if settings.TempUnschedMinutes > 60 { settings.TempUnschedMinutes = 60 } if settings.ThresholdCount < 1 { settings.ThresholdCount = 1 } if settings.ThresholdCount > 10 { settings.ThresholdCount = 10 } if settings.ThresholdWindowMinutes < 1 { settings.ThresholdWindowMinutes = 1 } if settings.ThresholdWindowMinutes > 60 { settings.ThresholdWindowMinutes = 60 } // 验证 action switch settings.Action { case StreamTimeoutActionTempUnsched, StreamTimeoutActionError, StreamTimeoutActionNone: // valid default: settings.Action = StreamTimeoutActionTempUnsched } return &settings, nil } // IsUngroupedKeySchedulingAllowed 查询是否允许未分组 Key 调度 func (s *SettingService) IsUngroupedKeySchedulingAllowed(ctx context.Context) bool { value, err := s.settingRepo.GetValue(ctx, SettingKeyAllowUngroupedKeyScheduling) if err != nil { return false // fail-closed: 查询失败时默认不允许 } return value == "true" } // GetRectifierSettings 获取请求整流器配置 func (s *SettingService) GetRectifierSettings(ctx context.Context) (*RectifierSettings, error) { value, err := s.settingRepo.GetValue(ctx, SettingKeyRectifierSettings) if err != nil { if errors.Is(err, ErrSettingNotFound) { return DefaultRectifierSettings(), nil } return nil, fmt.Errorf("get rectifier settings: %w", err) } if value == "" { return DefaultRectifierSettings(), nil } var settings RectifierSettings if err := json.Unmarshal([]byte(value), &settings); err != nil { return DefaultRectifierSettings(), nil } return &settings, nil } // SetRectifierSettings 设置请求整流器配置 func (s *SettingService) SetRectifierSettings(ctx context.Context, settings *RectifierSettings) error { if settings == nil { return fmt.Errorf("settings cannot be nil") } data, err := json.Marshal(settings) if err != nil { return fmt.Errorf("marshal rectifier settings: %w", err) } return s.settingRepo.Set(ctx, SettingKeyRectifierSettings, string(data)) } // IsSignatureRectifierEnabled 判断签名整流是否启用(总开关 && 签名子开关) func (s *SettingService) IsSignatureRectifierEnabled(ctx context.Context) bool { settings, err := s.GetRectifierSettings(ctx) if err != nil { return true // fail-open: 查询失败时默认启用 } return settings.Enabled && settings.ThinkingSignatureEnabled } // IsBudgetRectifierEnabled 判断 Budget 整流是否启用(总开关 && Budget 子开关) func (s *SettingService) IsBudgetRectifierEnabled(ctx context.Context) bool { settings, err := s.GetRectifierSettings(ctx) if err != nil { return true // fail-open: 查询失败时默认启用 } return settings.Enabled && settings.ThinkingBudgetEnabled } // GetBetaPolicySettings 获取 Beta 策略配置 func (s *SettingService) GetBetaPolicySettings(ctx context.Context) (*BetaPolicySettings, error) { value, err := s.settingRepo.GetValue(ctx, SettingKeyBetaPolicySettings) if err != nil { if errors.Is(err, ErrSettingNotFound) { return DefaultBetaPolicySettings(), nil } return nil, fmt.Errorf("get beta policy settings: %w", err) } if value == "" { return DefaultBetaPolicySettings(), nil } var settings BetaPolicySettings if err := json.Unmarshal([]byte(value), &settings); err != nil { return DefaultBetaPolicySettings(), nil } return &settings, nil } // SetBetaPolicySettings 设置 Beta 策略配置 func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *BetaPolicySettings) error { if settings == nil { return fmt.Errorf("settings cannot be nil") } validActions := map[string]bool{ BetaPolicyActionPass: true, BetaPolicyActionFilter: true, BetaPolicyActionBlock: true, } validScopes := map[string]bool{ BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, BetaPolicyScopeBedrock: true, } for i, rule := range settings.Rules { if rule.BetaToken == "" { return fmt.Errorf("rule[%d]: beta_token cannot be empty", i) } if !validActions[rule.Action] { return fmt.Errorf("rule[%d]: invalid action %q", i, rule.Action) } if !validScopes[rule.Scope] { return fmt.Errorf("rule[%d]: invalid scope %q", i, rule.Scope) } // Validate model_whitelist patterns for j, pattern := range rule.ModelWhitelist { trimmed := strings.TrimSpace(pattern) if trimmed == "" { return fmt.Errorf("rule[%d]: model_whitelist[%d] cannot be empty", i, j) } settings.Rules[i].ModelWhitelist[j] = trimmed } // Validate fallback_action if rule.FallbackAction != "" && !validActions[rule.FallbackAction] { return fmt.Errorf("rule[%d]: invalid fallback_action %q", i, rule.FallbackAction) } } data, err := json.Marshal(settings) if err != nil { return fmt.Errorf("marshal beta policy settings: %w", err) } return s.settingRepo.Set(ctx, SettingKeyBetaPolicySettings, string(data)) } // GetOpenAIFastPolicySettings 获取 OpenAI fast 策略配置 func (s *SettingService) GetOpenAIFastPolicySettings(ctx context.Context) (*OpenAIFastPolicySettings, error) { value, err := s.settingRepo.GetValue(ctx, SettingKeyOpenAIFastPolicySettings) if err != nil { if errors.Is(err, ErrSettingNotFound) { return DefaultOpenAIFastPolicySettings(), nil } return nil, fmt.Errorf("get openai fast policy settings: %w", err) } if value == "" { return DefaultOpenAIFastPolicySettings(), nil } var settings OpenAIFastPolicySettings if err := json.Unmarshal([]byte(value), &settings); err != nil { // JSON 损坏时静默 fallback 到默认配置会让策略意外失效(管理员配 // 置的 block/filter 规则被忽略)。记录 Warn 让运维能在出现异常 // 行为时定位到 settings 表里的脏数据。 slog.Warn("failed to unmarshal openai fast policy settings, falling back to defaults", "error", err, "key", SettingKeyOpenAIFastPolicySettings) return DefaultOpenAIFastPolicySettings(), nil } return &settings, nil } // SetOpenAIFastPolicySettings 设置 OpenAI fast 策略配置 func (s *SettingService) SetOpenAIFastPolicySettings(ctx context.Context, settings *OpenAIFastPolicySettings) error { if settings == nil { return fmt.Errorf("settings cannot be nil") } validActions := map[string]bool{ BetaPolicyActionPass: true, BetaPolicyActionFilter: true, BetaPolicyActionBlock: true, OpenAIFastPolicyActionForcePriority: true, } validScopes := map[string]bool{ BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, BetaPolicyScopeBedrock: true, } validTiers := map[string]bool{ OpenAIFastTierAny: true, OpenAIFastTierPriority: true, OpenAIFastTierFlex: true, } for i, rule := range settings.Rules { tier := strings.ToLower(strings.TrimSpace(rule.ServiceTier)) if tier == "" { tier = OpenAIFastTierAny } if !validTiers[tier] { return fmt.Errorf("rule[%d]: invalid service_tier %q", i, rule.ServiceTier) } settings.Rules[i].ServiceTier = tier if !validActions[rule.Action] { return fmt.Errorf("rule[%d]: invalid action %q", i, rule.Action) } if !validScopes[rule.Scope] { return fmt.Errorf("rule[%d]: invalid scope %q", i, rule.Scope) } seenUserIDs := make(map[int64]struct{}, len(rule.UserIDs)) for j, userID := range rule.UserIDs { if userID <= 0 { return fmt.Errorf("rule[%d]: user_ids[%d] must be positive", i, j) } if _, exists := seenUserIDs[userID]; exists { return fmt.Errorf("rule[%d]: user_ids[%d] duplicates user_id %d", i, j, userID) } seenUserIDs[userID] = struct{}{} } for j, pattern := range rule.ModelWhitelist { trimmed := strings.TrimSpace(pattern) if trimmed == "" { return fmt.Errorf("rule[%d]: model_whitelist[%d] cannot be empty", i, j) } settings.Rules[i].ModelWhitelist[j] = trimmed } if rule.FallbackAction != "" && !validActions[rule.FallbackAction] { return fmt.Errorf("rule[%d]: invalid fallback_action %q", i, rule.FallbackAction) } } data, err := json.Marshal(settings) if err != nil { return fmt.Errorf("marshal openai fast policy settings: %w", err) } return s.settingRepo.Set(ctx, SettingKeyOpenAIFastPolicySettings, string(data)) } // SetStreamTimeoutSettings 设置流超时处理配置 func (s *SettingService) SetStreamTimeoutSettings(ctx context.Context, settings *StreamTimeoutSettings) error { if settings == nil { return fmt.Errorf("settings cannot be nil") } // 验证配置值 if settings.TempUnschedMinutes < 1 || settings.TempUnschedMinutes > 60 { return fmt.Errorf("temp_unsched_minutes must be between 1-60") } if settings.ThresholdCount < 1 || settings.ThresholdCount > 10 { return fmt.Errorf("threshold_count must be between 1-10") } if settings.ThresholdWindowMinutes < 1 || settings.ThresholdWindowMinutes > 60 { return fmt.Errorf("threshold_window_minutes must be between 1-60") } switch settings.Action { case StreamTimeoutActionTempUnsched, StreamTimeoutActionError, StreamTimeoutActionNone: // valid default: return fmt.Errorf("invalid action: %s", settings.Action) } data, err := json.Marshal(settings) if err != nil { return fmt.Errorf("marshal stream timeout settings: %w", err) } return s.settingRepo.Set(ctx, SettingKeyStreamTimeoutSettings, string(data)) } // GetDefaultPlatformQuotas 读取系统全局 platform quota JSON key,返回全部允许平台 x 3 window 的设置。 // 永远返回包含全部允许 platform key 的 map(值可能为零值/nil 字段,表示"上层未配置 = 不限制")。 // // 使用单个 JSON key(default_platform_quotas),一次 DB roundtrip,消除旧 12-KV 格式的 N+1 问题。 // 容错语义:取值失败或 unmarshal 失败 → 返回补齐全部允许平台 key 的空 map(fail-open,注册不被阻断)。 func (s *SettingService) GetDefaultPlatformQuotas(ctx context.Context) (map[string]*DefaultPlatformQuotaSetting, error) { out := make(map[string]*DefaultPlatformQuotaSetting, len(AllowedQuotaPlatforms)) for _, platform := range AllowedQuotaPlatforms { out[platform] = &DefaultPlatformQuotaSetting{} } raw, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultPlatformQuotas) if err != nil || raw == "" { return out, nil // 无配置 = 全部不限制 } parsed := map[string]*DefaultPlatformQuotaSetting{} if err := json.Unmarshal([]byte(raw), &parsed); err != nil { slog.Warn("[Setting] unmarshal default_platform_quotas failed (fail-open)", "error", err) return out, nil } for _, platform := range AllowedQuotaPlatforms { if v := parsed[platform]; v != nil { out[platform] = v } } return out, nil // 补齐全部允许 platform key,保持与旧实现一致的下游契约 } // GetAccountSchedulingThresholds returns per-platform auto-pause thresholds (1..100). // 100 disables the threshold for that platform. Hot-path cached with singleflight. func (s *SettingService) GetAccountSchedulingThresholds(ctx context.Context) map[string]int { if s == nil || s.settingRepo == nil { return defaultAccountSchedulingThresholds() } if cached, ok := accountSchedulingThresholdsCache.Load().(*cachedAccountSchedulingThresholds); ok { if cached != nil && len(cached.thresholds) > 0 && time.Now().UnixNano() < cached.expiresAt { return cloneAccountSchedulingThresholds(cached.thresholds) } } result, err, _ := accountSchedulingThresholdsSF.Do(SettingKeyAccountSchedulingThresholds, func() (any, error) { if cached, ok := accountSchedulingThresholdsCache.Load().(*cachedAccountSchedulingThresholds); ok { if cached != nil && len(cached.thresholds) > 0 && time.Now().UnixNano() < cached.expiresAt { return cloneAccountSchedulingThresholds(cached.thresholds), nil } } thresholds := defaultAccountSchedulingThresholds() dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), accountSchedulingThresholdsDBTimeout) defer cancel() raw, err := s.settingRepo.GetValue(dbCtx, SettingKeyAccountSchedulingThresholds) if err != nil { if errors.Is(err, ErrSettingNotFound) { accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{ thresholds: cloneAccountSchedulingThresholds(thresholds), expiresAt: time.Now().Add(accountSchedulingThresholdsCacheTTL).UnixNano(), }) return cloneAccountSchedulingThresholds(thresholds), nil } slog.Warn("failed to get account scheduling thresholds, falling back to defaults", "error", err) accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{ thresholds: cloneAccountSchedulingThresholds(thresholds), expiresAt: time.Now().Add(accountSchedulingThresholdsErrorTTL).UnixNano(), }) return cloneAccountSchedulingThresholds(thresholds), nil } if trimmed := strings.TrimSpace(raw); trimmed != "" { if parsed, err := parseAccountSchedulingThresholdsSetting(trimmed); err != nil { slog.Warn("failed to parse account scheduling thresholds, falling back to defaults", "error", err) } else { thresholds = parsed } } accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{ thresholds: cloneAccountSchedulingThresholds(thresholds), expiresAt: time.Now().Add(accountSchedulingThresholdsCacheTTL).UnixNano(), }) return cloneAccountSchedulingThresholds(thresholds), nil }) if err != nil { return defaultAccountSchedulingThresholds() } if thresholds, ok := result.(map[string]int); ok { return cloneAccountSchedulingThresholds(thresholds) } return defaultAccountSchedulingThresholds() } // GetAuthSourcePlatformQuotas 读取指定 auth source 的 platform quota 覆盖(仅返回有配置的平台,override 语义)。 func (s *SettingService) GetAuthSourcePlatformQuotas(ctx context.Context, source string) map[string]*DefaultPlatformQuotaSetting { out := map[string]*DefaultPlatformQuotaSetting{} raw, err := s.settingRepo.GetValue(ctx, SettingKeyAuthSourcePlatformQuotas(source)) if err != nil || raw == "" { return out // 无 override } if err := json.Unmarshal([]byte(raw), &out); err != nil { slog.Warn("[Setting] unmarshal auth source platform quotas failed (fail-open)", "source", source, "error", err) return map[string]*DefaultPlatformQuotaSetting{} } return out // 仅含已配置平台,保持 override 语义 } // mergePlatformQuotaDefaults 按字段级 patch:src 中非 nil 字段覆盖 dst。 // 区分 nil("未配置",保留 dst)vs &0.0("显式禁用",覆盖 dst 为 0) func mergePlatformQuotaDefaults(dst, src *DefaultPlatformQuotaSetting) { if src == nil || dst == nil { return } if src.DailyLimitUSD != nil { dst.DailyLimitUSD = src.DailyLimitUSD } if src.WeeklyLimitUSD != nil { dst.WeeklyLimitUSD = src.WeeklyLimitUSD } if src.MonthlyLimitUSD != nil { dst.MonthlyLimitUSD = src.MonthlyLimitUSD } }