package admin import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "log/slog" "regexp" "strings" "github.com/Wei-Shaw/sub2api/internal/handler/dto" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) // semverPattern 预编译 semver 格式校验正则 var semverPattern = regexp.MustCompile(`^\d+\.\d+\.\d+$`) // menuItemIDPattern validates custom menu item IDs: alphanumeric, hyphens, underscores only. var menuItemIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) // generateMenuItemID generates a short random hex ID for a custom menu item. func generateMenuItemID() (string, error) { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("generate menu item ID: %w", err) } return hex.EncodeToString(b), nil } func scopesContainOpenID(scopes string) bool { for _, scope := range strings.Fields(strings.ToLower(strings.TrimSpace(scopes))) { if scope == "openid" { return true } } return false } func firstNonEmpty(values ...string) string { for _, value := range values { if trimmed := strings.TrimSpace(value); trimmed != "" { return trimmed } } return "" } // SettingHandler 系统设置处理器 type SettingHandler struct { settingService *service.SettingService emailService *service.EmailService turnstileService *service.TurnstileService aliyunCaptchaService *service.AliyunCaptchaService opsService *service.OpsService paymentConfigService *service.PaymentConfigService paymentService *service.PaymentService userAttributeService *service.UserAttributeService notificationEmailService *service.NotificationEmailService totpService *service.TotpService userService *service.UserService } // NewSettingHandler 创建系统设置处理器 func NewSettingHandler(settingService *service.SettingService, emailService *service.EmailService, turnstileService *service.TurnstileService, opsService *service.OpsService, paymentConfigService *service.PaymentConfigService, paymentService *service.PaymentService, userAttributeService *service.UserAttributeService) *SettingHandler { return &SettingHandler{ settingService: settingService, emailService: emailService, turnstileService: turnstileService, opsService: opsService, paymentConfigService: paymentConfigService, paymentService: paymentService, userAttributeService: userAttributeService, } } // SetNotificationEmailService attaches the notification template service without changing // the constructor signature used by existing unit tests. func (h *SettingHandler) SetNotificationEmailService(notificationEmailService *service.NotificationEmailService) { h.notificationEmailService = notificationEmailService } // SetAliyunCaptchaService attaches the Aliyun captcha credential validator without // changing the constructor signature used by existing unit tests. func (h *SettingHandler) SetAliyunCaptchaService(aliyunCaptchaService *service.AliyunCaptchaService) { h.aliyunCaptchaService = aliyunCaptchaService } // SetStepUpDeps attaches the services backing the step-up switch preconditions // (enable requires the acting admin to have TOTP enabled; disable is itself a // step-up gated operation), without changing the constructor signature used by // existing unit tests. func (h *SettingHandler) SetStepUpDeps(totpService *service.TotpService, userService *service.UserService) { h.totpService = totpService h.userService = userService } // GetSettings 获取所有系统设置 // GET /api/v1/admin/settings func (h *SettingHandler) GetSettings(c *gin.Context) { settings, err := h.settingService.GetAllSettings(c.Request.Context()) if err != nil { response.ErrorFrom(c, err) return } authSourceDefaults, err := h.settingService.GetAuthSourceDefaultSettings(c.Request.Context()) if err != nil { response.ErrorFrom(c, err) return } // Check if ops monitoring is enabled (respects config.ops.enabled) opsEnabled := h.opsService != nil && h.opsService.IsMonitoringEnabled(c.Request.Context()) defaultSubscriptions := make([]dto.DefaultSubscriptionSetting, 0, len(settings.DefaultSubscriptions)) for _, sub := range settings.DefaultSubscriptions { defaultSubscriptions = append(defaultSubscriptions, dto.DefaultSubscriptionSetting{ GroupID: sub.GroupID, ValidityDays: sub.ValidityDays, }) } // Load payment config var paymentCfg *service.PaymentConfig if h.paymentConfigService != nil { paymentCfg, _ = h.paymentConfigService.GetPaymentConfig(c.Request.Context()) } if paymentCfg == nil { paymentCfg = &service.PaymentConfig{} } passkeyConfigured, passkeyRPID, passkeyRPOrigins := h.settingService.PasskeyConfiguration() payload := dto.SystemSettings{ RegistrationEnabled: settings.RegistrationEnabled, EmailVerifyEnabled: settings.EmailVerifyEnabled, RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist, RegistrationEmailDomainQuotaEnabled: settings.RegistrationEmailDomainQuotaEnabled, PromoCodeEnabled: settings.PromoCodeEnabled, PasswordResetEnabled: settings.PasswordResetEnabled, FrontendURL: settings.FrontendURL, InvitationCodeEnabled: settings.InvitationCodeEnabled, TotpEnabled: settings.TotpEnabled, TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(), PasskeyEnabled: settings.PasskeyEnabled, PasskeyConfigured: passkeyConfigured, PasskeyRPID: passkeyRPID, PasskeyRPOrigins: passkeyRPOrigins, SessionBindingEnabled: settings.SessionBindingEnabled, StepUpEnabled: settings.StepUpEnabled, AuditLogRetentionDays: settings.AuditLogRetentionDays, LoginAgreementEnabled: settings.LoginAgreementEnabled, LoginAgreementMode: settings.LoginAgreementMode, LoginAgreementUpdatedAt: settings.LoginAgreementUpdatedAt, LoginAgreementDocuments: loginAgreementDocumentsToDTO(settings.LoginAgreementDocuments), SMTPHost: settings.SMTPHost, SMTPPort: settings.SMTPPort, SMTPUsername: settings.SMTPUsername, SMTPPasswordConfigured: settings.SMTPPasswordConfigured, SMTPFrom: settings.SMTPFrom, SMTPFromName: settings.SMTPFromName, SMTPUseTLS: settings.SMTPUseTLS, TurnstileEnabled: settings.TurnstileEnabled, TurnstileSiteKey: settings.TurnstileSiteKey, TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured, TencentCaptchaEnabled: settings.TencentCaptchaEnabled, TencentCaptchaAppID: settings.TencentCaptchaAppID, TencentCaptchaAppSecretKeyConfigured: settings.TencentCaptchaAppSecretKeyConfigured, TencentCaptchaCloudSecretIDConfigured: settings.TencentCaptchaCloudSecretIDConfigured, TencentCaptchaCloudSecretKeyConfigured: settings.TencentCaptchaCloudSecretKeyConfigured, TencentCaptchaRegion: settings.TencentCaptchaRegion, AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled, AliyunCaptchaAccessKeyID: settings.AliyunCaptchaAccessKeyID, AliyunCaptchaAccessKeySecretConfigured: settings.AliyunCaptchaAccessKeySecretConfigured, AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID, AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix, AliyunCaptchaRegion: settings.AliyunCaptchaRegion, APIKeyACLTrustForwardedIP: settings.APIKeyACLTrustForwardedIP, ForwardedClientIPHeaders: settings.ForwardedClientIPHeaders, LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled, LinuxDoConnectClientID: settings.LinuxDoConnectClientID, LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured, LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL, DingTalkConnectEnabled: settings.DingTalkConnectEnabled, DingTalkConnectClientID: settings.DingTalkConnectClientID, DingTalkConnectClientSecretConfigured: settings.DingTalkConnectClientSecretConfigured, DingTalkConnectRedirectURL: settings.DingTalkConnectRedirectURL, DingTalkConnectCorpRestrictionPolicy: settings.DingTalkConnectCorpRestrictionPolicy, DingTalkConnectInternalCorpID: settings.DingTalkConnectInternalCorpID, DingTalkConnectBypassRegistration: settings.DingTalkConnectBypassRegistration, DingTalkConnectSyncCorpEmail: settings.DingTalkConnectSyncCorpEmail, DingTalkConnectSyncDisplayName: settings.DingTalkConnectSyncDisplayName, DingTalkConnectSyncDept: settings.DingTalkConnectSyncDept, DingTalkConnectSyncCorpEmailAttrKey: settings.DingTalkConnectSyncCorpEmailAttrKey, DingTalkConnectSyncDisplayNameAttrKey: settings.DingTalkConnectSyncDisplayNameAttrKey, DingTalkConnectSyncDeptAttrKey: settings.DingTalkConnectSyncDeptAttrKey, DingTalkConnectSyncCorpEmailAttrName: settings.DingTalkConnectSyncCorpEmailAttrName, DingTalkConnectSyncDisplayNameAttrName: settings.DingTalkConnectSyncDisplayNameAttrName, DingTalkConnectSyncDeptAttrName: settings.DingTalkConnectSyncDeptAttrName, WeChatConnectEnabled: settings.WeChatConnectEnabled, WeChatConnectAppID: settings.WeChatConnectAppID, WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured, WeChatConnectOpenAppID: settings.WeChatConnectOpenAppID, WeChatConnectOpenAppSecretConfigured: settings.WeChatConnectOpenAppSecretConfigured, WeChatConnectMPAppID: settings.WeChatConnectMPAppID, WeChatConnectMPAppSecretConfigured: settings.WeChatConnectMPAppSecretConfigured, WeChatConnectMobileAppID: settings.WeChatConnectMobileAppID, WeChatConnectMobileAppSecretConfigured: settings.WeChatConnectMobileAppSecretConfigured, WeChatConnectOpenEnabled: settings.WeChatConnectOpenEnabled, WeChatConnectMPEnabled: settings.WeChatConnectMPEnabled, WeChatConnectMobileEnabled: settings.WeChatConnectMobileEnabled, WeChatConnectMode: settings.WeChatConnectMode, WeChatConnectScopes: settings.WeChatConnectScopes, WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL, WeChatConnectFrontendRedirectURL: settings.WeChatConnectFrontendRedirectURL, OIDCConnectEnabled: settings.OIDCConnectEnabled, OIDCConnectProviderName: settings.OIDCConnectProviderName, OIDCConnectClientID: settings.OIDCConnectClientID, OIDCConnectClientSecretConfigured: settings.OIDCConnectClientSecretConfigured, OIDCConnectIssuerURL: settings.OIDCConnectIssuerURL, OIDCConnectDiscoveryURL: settings.OIDCConnectDiscoveryURL, OIDCConnectAuthorizeURL: settings.OIDCConnectAuthorizeURL, OIDCConnectTokenURL: settings.OIDCConnectTokenURL, OIDCConnectUserInfoURL: settings.OIDCConnectUserInfoURL, OIDCConnectJWKSURL: settings.OIDCConnectJWKSURL, OIDCConnectScopes: settings.OIDCConnectScopes, OIDCConnectRedirectURL: settings.OIDCConnectRedirectURL, OIDCConnectFrontendRedirectURL: settings.OIDCConnectFrontendRedirectURL, OIDCConnectTokenAuthMethod: settings.OIDCConnectTokenAuthMethod, OIDCConnectUsePKCE: settings.OIDCConnectUsePKCE, OIDCConnectValidateIDToken: settings.OIDCConnectValidateIDToken, OIDCConnectAllowedSigningAlgs: settings.OIDCConnectAllowedSigningAlgs, OIDCConnectClockSkewSeconds: settings.OIDCConnectClockSkewSeconds, OIDCConnectRequireEmailVerified: settings.OIDCConnectRequireEmailVerified, OIDCConnectUserInfoEmailPath: settings.OIDCConnectUserInfoEmailPath, OIDCConnectUserInfoIDPath: settings.OIDCConnectUserInfoIDPath, OIDCConnectUserInfoUsernamePath: settings.OIDCConnectUserInfoUsernamePath, GitHubOAuthEnabled: settings.GitHubOAuthEnabled, GitHubOAuthClientID: settings.GitHubOAuthClientID, GitHubOAuthClientSecretConfigured: settings.GitHubOAuthClientSecretConfigured, GitHubOAuthRedirectURL: settings.GitHubOAuthRedirectURL, GitHubOAuthFrontendRedirectURL: settings.GitHubOAuthFrontendRedirectURL, GoogleOAuthEnabled: settings.GoogleOAuthEnabled, GoogleOAuthClientID: settings.GoogleOAuthClientID, GoogleOAuthClientSecretConfigured: settings.GoogleOAuthClientSecretConfigured, GoogleOAuthRedirectURL: settings.GoogleOAuthRedirectURL, GoogleOAuthFrontendRedirectURL: settings.GoogleOAuthFrontendRedirectURL, SiteName: settings.SiteName, SiteLogo: settings.SiteLogo, SiteSubtitle: settings.SiteSubtitle, APIBaseURL: settings.APIBaseURL, ContactInfo: settings.ContactInfo, DocURL: settings.DocURL, HomeContent: settings.HomeContent, CompactHomeEnabled: settings.CompactHomeEnabled, HideCcsImportButton: settings.HideCcsImportButton, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, TableDefaultPageSize: settings.TableDefaultPageSize, TablePageSizeOptions: settings.TablePageSizeOptions, CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems), CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints), DefaultConcurrency: settings.DefaultConcurrency, DefaultBalance: settings.DefaultBalance, RiskControlEnabled: settings.RiskControlEnabled, CyberSessionBlockEnabled: settings.CyberSessionBlockEnabled, CyberSessionBlockTTLSeconds: settings.CyberSessionBlockTTLSeconds, AffiliateRebateRate: settings.AffiliateRebateRate, AffiliateRebateFreezeHours: settings.AffiliateRebateFreezeHours, AffiliateRebateDurationDays: settings.AffiliateRebateDurationDays, AffiliateRebatePerInviteeCap: settings.AffiliateRebatePerInviteeCap, AdminRechargeRebateEnabled: settings.AdminRechargeRebateEnabled, DefaultUserRPMLimit: settings.DefaultUserRPMLimit, DefaultSubscriptions: defaultSubscriptions, EnableModelFallback: settings.EnableModelFallback, FallbackModelAnthropic: settings.FallbackModelAnthropic, FallbackModelOpenAI: settings.FallbackModelOpenAI, FallbackModelGemini: settings.FallbackModelGemini, FallbackModelAntigravity: settings.FallbackModelAntigravity, EnableIdentityPatch: settings.EnableIdentityPatch, IdentityPatchPrompt: settings.IdentityPatchPrompt, OpsMonitoringEnabled: opsEnabled && settings.OpsMonitoringEnabled, OpsRealtimeMonitoringEnabled: settings.OpsRealtimeMonitoringEnabled, OpsQueryModeDefault: settings.OpsQueryModeDefault, OpsMetricsIntervalSeconds: settings.OpsMetricsIntervalSeconds, MinClaudeCodeVersion: settings.MinClaudeCodeVersion, MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion, AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling, BackendModeEnabled: settings.BackendModeEnabled, EnableFingerprintUnification: settings.EnableFingerprintUnification, EnableMetadataPassthrough: settings.EnableMetadataPassthrough, EnableCCHSigning: settings.EnableCCHSigning, EnableClaudeOAuthSystemPromptInjection: settings.EnableClaudeOAuthSystemPromptInjection, ClaudeOAuthSystemPrompt: settings.ClaudeOAuthSystemPrompt, ClaudeOAuthSystemPromptBlocks: settings.ClaudeOAuthSystemPromptBlocks, EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection, RewriteMessageCacheControl: settings.RewriteMessageCacheControl, EnableClientDatelineNormalization: settings.EnableClientDatelineNormalization, AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion, OpenAICodexUserAgent: settings.OpenAICodexUserAgent, OpenAICodexClientVersion: settings.OpenAICodexClientVersion, OpenAICodexClientVersionSynced: settings.OpenAICodexClientVersionSynced, OpenAICodexVersionAutoSyncEnabled: settings.OpenAICodexVersionAutoSyncEnabled, MinCodexVersion: settings.MinCodexVersion, MaxCodexVersion: settings.MaxCodexVersion, CodexCLIOnlyBlacklist: settings.CodexCLIOnlyBlacklist, CodexCLIOnlyWhitelist: settings.CodexCLIOnlyWhitelist, CodexCLIOnlyAllowAppServerClients: settings.CodexCLIOnlyAllowAppServerClients, CodexCLIOnlyEngineFingerprintSignals: settings.CodexCLIOnlyEngineFingerprintSignals, WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled, PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource, PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource, PaymentVisibleMethodAlipayEnabled: settings.PaymentVisibleMethodAlipayEnabled, PaymentVisibleMethodWxpayEnabled: settings.PaymentVisibleMethodWxpayEnabled, OpenAILowUpstreamRatePriorityEnabled: settings.OpenAILowUpstreamRatePriorityEnabled, OpenAIOAuthSchedulingRateMultiplier: settings.OpenAIOAuthSchedulingRateMultiplier, OpenAIAdvancedSchedulerEnabled: settings.OpenAIAdvancedSchedulerEnabled, OpenAIAdvancedSchedulerStickyWeightedEnabled: settings.OpenAIAdvancedSchedulerStickyWeightedEnabled, OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled, OpenAIAdvancedSchedulerLBTopK: settings.OpenAIAdvancedSchedulerLBTopK, OpenAIAdvancedSchedulerWeightPriority: settings.OpenAIAdvancedSchedulerWeightPriority, OpenAIAdvancedSchedulerWeightLoad: settings.OpenAIAdvancedSchedulerWeightLoad, OpenAIAdvancedSchedulerWeightQueue: settings.OpenAIAdvancedSchedulerWeightQueue, OpenAIAdvancedSchedulerWeightErrorRate: settings.OpenAIAdvancedSchedulerWeightErrorRate, OpenAIAdvancedSchedulerWeightTTFT: settings.OpenAIAdvancedSchedulerWeightTTFT, OpenAIAdvancedSchedulerWeightReset: settings.OpenAIAdvancedSchedulerWeightReset, OpenAIAdvancedSchedulerWeightQuotaHeadroom: settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom, OpenAIAdvancedSchedulerWeightUpstreamCost: settings.OpenAIAdvancedSchedulerWeightUpstreamCost, OpenAIAdvancedSchedulerWeightPreviousResponse: settings.OpenAIAdvancedSchedulerWeightPreviousResponse, OpenAIAdvancedSchedulerWeightSessionSticky: settings.OpenAIAdvancedSchedulerWeightSessionSticky, OpenAIAdvancedSchedulerEffectiveLBTopK: settings.OpenAIAdvancedSchedulerEffectiveLBTopK, OpenAIAdvancedSchedulerEffectiveWeightPriority: settings.OpenAIAdvancedSchedulerEffectiveWeightPriority, OpenAIAdvancedSchedulerEffectiveWeightLoad: settings.OpenAIAdvancedSchedulerEffectiveWeightLoad, OpenAIAdvancedSchedulerEffectiveWeightQueue: settings.OpenAIAdvancedSchedulerEffectiveWeightQueue, OpenAIAdvancedSchedulerEffectiveWeightErrorRate: settings.OpenAIAdvancedSchedulerEffectiveWeightErrorRate, OpenAIAdvancedSchedulerEffectiveWeightTTFT: settings.OpenAIAdvancedSchedulerEffectiveWeightTTFT, OpenAIAdvancedSchedulerEffectiveWeightReset: settings.OpenAIAdvancedSchedulerEffectiveWeightReset, OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom: settings.OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom, OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost: settings.OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost, OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse: settings.OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse, OpenAIAdvancedSchedulerEffectiveWeightSessionSticky: settings.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky, BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled, BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold, BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL, SubscriptionExpiryNotifyEnabled: settings.SubscriptionExpiryNotifyEnabled, AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled, AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(settings.AccountQuotaNotifyEmails), PaymentEnabled: paymentCfg.Enabled, PaymentMinAmount: paymentCfg.MinAmount, PaymentMaxAmount: paymentCfg.MaxAmount, PaymentDailyLimit: paymentCfg.DailyLimit, PaymentOrderTimeoutMin: paymentCfg.OrderTimeoutMin, PaymentMaxPendingOrders: paymentCfg.MaxPendingOrders, PaymentEnabledTypes: paymentCfg.EnabledTypes, PaymentBalanceDisabled: paymentCfg.BalanceDisabled, PaymentBalanceRechargeMultiplier: paymentCfg.BalanceRechargeMultiplier, PaymentSubscriptionUSDToCNYRate: paymentCfg.SubscriptionUSDToCNYRate, PaymentRechargeFeeRate: paymentCfg.RechargeFeeRate, PaymentLoadBalanceStrat: paymentCfg.LoadBalanceStrategy, PaymentProductNamePrefix: paymentCfg.ProductNamePrefix, PaymentProductNameSuffix: paymentCfg.ProductNameSuffix, PaymentHelpImageURL: paymentCfg.HelpImageURL, PaymentHelpText: paymentCfg.HelpText, PaymentCancelRateLimitEnabled: paymentCfg.CancelRateLimitEnabled, PaymentCancelRateLimitMax: paymentCfg.CancelRateLimitMax, PaymentCancelRateLimitWindow: paymentCfg.CancelRateLimitWindow, PaymentCancelRateLimitUnit: paymentCfg.CancelRateLimitUnit, PaymentCancelRateLimitMode: paymentCfg.CancelRateLimitMode, PaymentAlipayForceQRCode: paymentCfg.AlipayForceQRCode, PaymentAlipayMobilePrecreateDeepLink: paymentCfg.AlipayMobilePrecreateDeepLink, ChannelMonitorEnabled: settings.ChannelMonitorEnabled, ChannelMonitorMode: settings.ChannelMonitorMode, ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds, ChannelMonitorHideThroughput: settings.ChannelMonitorHideThroughput, ChannelMonitorShowQuota: settings.ChannelMonitorShowQuota, GrokDefaultTextModel: settings.GrokDefaultTextModel, GrokCrossClientModelMapEnabled: settings.GrokCrossClientModelMapEnabled, GrokDefaultBaseURLMode: settings.GrokDefaultBaseURLMode, AvailableChannelsEnabled: settings.AvailableChannelsEnabled, ModelPlazaEnabled: settings.ModelPlazaEnabled, ModelPlazaRequireAuth: settings.ModelPlazaRequireAuth, ModelPlazaDescription: settings.ModelPlazaDescription, AffiliateEnabled: settings.AffiliateEnabled, AccountSchedulingThresholds: settings.AccountSchedulingThresholds, AllowUserViewErrorRequests: settings.AllowUserViewErrorRequests, } // OpenAI fast policy (stored under a dedicated setting key) if fastPolicy, err := h.settingService.GetOpenAIFastPolicySettings(c.Request.Context()); err != nil { slog.Error("openai_fast_policy_settings_get_failed", "error", err) } else if fastPolicy != nil { payload.OpenAIFastPolicySettings = openaiFastPolicySettingsToDTO(fastPolicy) } // Default platform quotas(JSON map) if platformQuotas, err := h.settingService.GetDefaultPlatformQuotas(c.Request.Context()); err != nil { slog.Error("default_platform_quotas_get_failed", "error", err) } else { payload.DefaultPlatformQuotas = platformQuotas } response.Success(c, systemSettingsResponseData(payload, authSourceDefaults)) } // openaiFastPolicySettingsToDTO converts service -> dto for OpenAI fast policy. func openaiFastPolicySettingsToDTO(s *service.OpenAIFastPolicySettings) *dto.OpenAIFastPolicySettings { if s == nil { return nil } rules := make([]dto.OpenAIFastPolicyRule, len(s.Rules)) for i, r := range s.Rules { rules[i] = dto.OpenAIFastPolicyRule(r) } return &dto.OpenAIFastPolicySettings{Rules: rules} } // openaiFastPolicySettingsFromDTO converts dto -> service for OpenAI fast policy. // // 规范化 ServiceTier:在 DTO 进入 service 层之前统一把空字符串归一为 // service.OpenAIFastTierAny ("all"),避免管理员保存时空串与 "all" 同时 // 表达"匹配任意 tier"造成数据库取值的二义性。其它非空值原样透传,由 // service.SetOpenAIFastPolicySettings 负责合法值校验。 func openaiFastPolicySettingsFromDTO(s *dto.OpenAIFastPolicySettings) *service.OpenAIFastPolicySettings { if s == nil { return nil } rules := make([]service.OpenAIFastPolicyRule, len(s.Rules)) for i, r := range s.Rules { rules[i] = service.OpenAIFastPolicyRule(r) tier := strings.ToLower(strings.TrimSpace(rules[i].ServiceTier)) if tier == "" { tier = service.OpenAIFastTierAny } rules[i].ServiceTier = tier } return &service.OpenAIFastPolicySettings{Rules: rules} } func loginAgreementDocumentsToDTO(items []service.LoginAgreementDocument) []dto.LoginAgreementDocument { result := make([]dto.LoginAgreementDocument, 0, len(items)) for _, item := range items { result = append(result, dto.LoginAgreementDocument{ ID: item.ID, Title: item.Title, ContentMD: item.ContentMD, }) } return result } func loginAgreementDocumentsToService(items []dto.LoginAgreementDocument) []service.LoginAgreementDocument { result := make([]service.LoginAgreementDocument, 0, len(items)) for _, item := range items { title := strings.TrimSpace(item.Title) content := strings.TrimSpace(item.ContentMD) if title == "" && content == "" { continue } result = append(result, service.LoginAgreementDocument{ ID: strings.TrimSpace(item.ID), Title: title, ContentMD: content, }) } return result } func systemSettingsResponseData(settings dto.SystemSettings, authSourceDefaults *service.AuthSourceDefaultSettings) map[string]any { data := make(map[string]any) raw, err := json.Marshal(settings) if err == nil { _ = json.Unmarshal(raw, &data) } if authSourceDefaults == nil { authSourceDefaults = &service.AuthSourceDefaultSettings{} } data["auth_source_default_email_balance"] = authSourceDefaults.Email.Balance data["auth_source_default_email_concurrency"] = authSourceDefaults.Email.Concurrency data["auth_source_default_email_subscriptions"] = authSourceDefaults.Email.Subscriptions data["auth_source_default_email_grant_on_signup"] = authSourceDefaults.Email.GrantOnSignup data["auth_source_default_email_grant_on_first_bind"] = authSourceDefaults.Email.GrantOnFirstBind data["auth_source_default_linuxdo_balance"] = authSourceDefaults.LinuxDo.Balance data["auth_source_default_linuxdo_concurrency"] = authSourceDefaults.LinuxDo.Concurrency data["auth_source_default_linuxdo_subscriptions"] = authSourceDefaults.LinuxDo.Subscriptions data["auth_source_default_linuxdo_grant_on_signup"] = authSourceDefaults.LinuxDo.GrantOnSignup data["auth_source_default_linuxdo_grant_on_first_bind"] = authSourceDefaults.LinuxDo.GrantOnFirstBind data["auth_source_default_dingtalk_balance"] = authSourceDefaults.DingTalk.Balance data["auth_source_default_dingtalk_concurrency"] = authSourceDefaults.DingTalk.Concurrency data["auth_source_default_dingtalk_subscriptions"] = authSourceDefaults.DingTalk.Subscriptions data["auth_source_default_dingtalk_grant_on_signup"] = authSourceDefaults.DingTalk.GrantOnSignup data["auth_source_default_dingtalk_grant_on_first_bind"] = authSourceDefaults.DingTalk.GrantOnFirstBind data["auth_source_default_oidc_balance"] = authSourceDefaults.OIDC.Balance data["auth_source_default_oidc_concurrency"] = authSourceDefaults.OIDC.Concurrency data["auth_source_default_oidc_subscriptions"] = authSourceDefaults.OIDC.Subscriptions data["auth_source_default_oidc_grant_on_signup"] = authSourceDefaults.OIDC.GrantOnSignup data["auth_source_default_oidc_grant_on_first_bind"] = authSourceDefaults.OIDC.GrantOnFirstBind data["auth_source_default_wechat_balance"] = authSourceDefaults.WeChat.Balance data["auth_source_default_wechat_concurrency"] = authSourceDefaults.WeChat.Concurrency data["auth_source_default_wechat_subscriptions"] = authSourceDefaults.WeChat.Subscriptions data["auth_source_default_wechat_grant_on_signup"] = authSourceDefaults.WeChat.GrantOnSignup data["auth_source_default_wechat_grant_on_first_bind"] = authSourceDefaults.WeChat.GrantOnFirstBind data["auth_source_default_github_balance"] = authSourceDefaults.GitHub.Balance data["auth_source_default_github_concurrency"] = authSourceDefaults.GitHub.Concurrency data["auth_source_default_github_subscriptions"] = authSourceDefaults.GitHub.Subscriptions data["auth_source_default_github_grant_on_signup"] = authSourceDefaults.GitHub.GrantOnSignup data["auth_source_default_github_grant_on_first_bind"] = authSourceDefaults.GitHub.GrantOnFirstBind data["auth_source_default_google_balance"] = authSourceDefaults.Google.Balance data["auth_source_default_google_concurrency"] = authSourceDefaults.Google.Concurrency data["auth_source_default_google_subscriptions"] = authSourceDefaults.Google.Subscriptions data["auth_source_default_google_grant_on_signup"] = authSourceDefaults.Google.GrantOnSignup data["auth_source_default_google_grant_on_first_bind"] = authSourceDefaults.Google.GrantOnFirstBind data["auth_source_default_email_platform_quotas"] = authSourceDefaults.Email.PlatformQuotas data["auth_source_default_linuxdo_platform_quotas"] = authSourceDefaults.LinuxDo.PlatformQuotas data["auth_source_default_oidc_platform_quotas"] = authSourceDefaults.OIDC.PlatformQuotas data["auth_source_default_wechat_platform_quotas"] = authSourceDefaults.WeChat.PlatformQuotas data["auth_source_default_github_platform_quotas"] = authSourceDefaults.GitHub.PlatformQuotas data["auth_source_default_google_platform_quotas"] = authSourceDefaults.Google.PlatformQuotas data["auth_source_default_dingtalk_platform_quotas"] = authSourceDefaults.DingTalk.PlatformQuotas data["force_email_on_third_party_signup"] = authSourceDefaults.ForceEmailOnThirdPartySignup return data }