| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218 |
- 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))
- }
- func (s *SettingService) GetOpenAIImagesOAuthUnavailableCooldownSettings(ctx context.Context) (*OpenAIImagesOAuthUnavailableCooldownSettings, error) {
- value, err := s.settingRepo.GetValue(ctx, SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings)
- if err != nil {
- if errors.Is(err, ErrSettingNotFound) {
- return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
- }
- return nil, fmt.Errorf("get OpenAI images OAuth unavailable cooldown settings: %w", err)
- }
- if value == "" {
- return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
- }
- var settings OpenAIImagesOAuthUnavailableCooldownSettings
- if err := json.Unmarshal([]byte(value), &settings); err != nil ||
- settings.CooldownMinutes <= 0 || settings.CooldownMinutes > openAIImagesOAuthUnavailableMaxCooldownMinutes {
- return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
- }
- return &settings, nil
- }
- func (s *SettingService) SetOpenAIImagesOAuthUnavailableCooldownSettings(ctx context.Context, settings *OpenAIImagesOAuthUnavailableCooldownSettings) error {
- if settings == nil {
- return fmt.Errorf("settings cannot be nil")
- }
- if settings.CooldownMinutes <= 0 || settings.CooldownMinutes > openAIImagesOAuthUnavailableMaxCooldownMinutes {
- return fmt.Errorf("cooldown_minutes must be between 1-%d", openAIImagesOAuthUnavailableMaxCooldownMinutes)
- }
- data, err := json.Marshal(settings)
- if err != nil {
- return fmt.Errorf("marshal OpenAI images OAuth unavailable cooldown settings: %w", err)
- }
- return s.settingRepo.Set(ctx, SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings, 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, OpenAIFastTierUltrafast: 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
- }
- }
|