package admin import ( "log/slog" "github.com/Wei-Shaw/sub2api/internal/handler/dto" "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) func (h *SettingHandler) auditSettingsUpdate(c *gin.Context, before *service.SystemSettings, after *service.SystemSettings, beforeAuthSourceDefaults *service.AuthSourceDefaultSettings, afterAuthSourceDefaults *service.AuthSourceDefaultSettings, req UpdateSettingsRequest) { if before == nil || after == nil { return } changed := diffSettings(before, after, beforeAuthSourceDefaults, afterAuthSourceDefaults, req) if len(changed) == 0 { return } subject, _ := middleware.GetAuthSubjectFromContext(c) role, _ := middleware.GetUserRoleFromContext(c) slog.Info("settings updated", "audit", true, "user_id", subject.UserID, "role", role, "changed", changed, ) } func diffSettings(before *service.SystemSettings, after *service.SystemSettings, beforeAuthSourceDefaults *service.AuthSourceDefaultSettings, afterAuthSourceDefaults *service.AuthSourceDefaultSettings, req UpdateSettingsRequest) []string { changed := make([]string, 0, 20) if before.RegistrationEnabled != after.RegistrationEnabled { changed = append(changed, "registration_enabled") } if before.EmailVerifyEnabled != after.EmailVerifyEnabled { changed = append(changed, "email_verify_enabled") } if !equalStringSlice(before.RegistrationEmailSuffixWhitelist, after.RegistrationEmailSuffixWhitelist) { changed = append(changed, "registration_email_suffix_whitelist") } if before.RegistrationEmailDomainQuotaEnabled != after.RegistrationEmailDomainQuotaEnabled { changed = append(changed, "registration_email_domain_quota_enabled") } if before.PromoCodeEnabled != after.PromoCodeEnabled { changed = append(changed, "promo_code_enabled") } if before.InvitationCodeEnabled != after.InvitationCodeEnabled { changed = append(changed, "invitation_code_enabled") } if before.PasswordResetEnabled != after.PasswordResetEnabled { changed = append(changed, "password_reset_enabled") } if before.FrontendURL != after.FrontendURL { changed = append(changed, "frontend_url") } if before.TotpEnabled != after.TotpEnabled { changed = append(changed, "totp_enabled") } if before.PasskeyEnabled != after.PasskeyEnabled { changed = append(changed, "passkey_enabled") } if before.SessionBindingEnabled != after.SessionBindingEnabled { changed = append(changed, "session_binding_enabled") } if before.StepUpEnabled != after.StepUpEnabled { changed = append(changed, "step_up_enabled") } if before.LoginAgreementEnabled != after.LoginAgreementEnabled { changed = append(changed, "login_agreement_enabled") } if before.LoginAgreementMode != after.LoginAgreementMode { changed = append(changed, "login_agreement_mode") } if before.LoginAgreementUpdatedAt != after.LoginAgreementUpdatedAt { changed = append(changed, "login_agreement_updated_at") } if !equalLoginAgreementDocuments(before.LoginAgreementDocuments, after.LoginAgreementDocuments) { changed = append(changed, "login_agreement_documents") } if before.SMTPHost != after.SMTPHost { changed = append(changed, "smtp_host") } if before.SMTPPort != after.SMTPPort { changed = append(changed, "smtp_port") } if before.SMTPUsername != after.SMTPUsername { changed = append(changed, "smtp_username") } if req.SMTPPassword != "" { changed = append(changed, "smtp_password") } if before.SMTPFrom != after.SMTPFrom { changed = append(changed, "smtp_from_email") } if before.SMTPFromName != after.SMTPFromName { changed = append(changed, "smtp_from_name") } if before.SMTPUseTLS != after.SMTPUseTLS { changed = append(changed, "smtp_use_tls") } if before.TurnstileEnabled != after.TurnstileEnabled { changed = append(changed, "turnstile_enabled") } if before.TurnstileSiteKey != after.TurnstileSiteKey { changed = append(changed, "turnstile_site_key") } if req.TurnstileSecretKey != "" { changed = append(changed, "turnstile_secret_key") } if before.TencentCaptchaEnabled != after.TencentCaptchaEnabled { changed = append(changed, "tencent_captcha_enabled") } if before.TencentCaptchaAppID != after.TencentCaptchaAppID { changed = append(changed, "tencent_captcha_app_id") } if req.TencentCaptchaAppSecretKey != "" { changed = append(changed, "tencent_captcha_app_secret_key") } if req.TencentCaptchaCloudSecretID != "" { changed = append(changed, "tencent_captcha_cloud_secret_id") } if req.TencentCaptchaCloudSecretKey != "" { changed = append(changed, "tencent_captcha_cloud_secret_key") } if before.TencentCaptchaRegion != after.TencentCaptchaRegion { changed = append(changed, "tencent_captcha_region") } if before.AliyunCaptchaEnabled != after.AliyunCaptchaEnabled { changed = append(changed, "aliyun_captcha_enabled") } if before.AliyunCaptchaAccessKeyID != after.AliyunCaptchaAccessKeyID { changed = append(changed, "aliyun_captcha_access_key_id") } if req.AliyunCaptchaAccessKeySecret != "" { changed = append(changed, "aliyun_captcha_access_key_secret") } if before.AliyunCaptchaSceneID != after.AliyunCaptchaSceneID { changed = append(changed, "aliyun_captcha_scene_id") } if before.AliyunCaptchaPrefix != after.AliyunCaptchaPrefix { changed = append(changed, "aliyun_captcha_prefix") } if before.AliyunCaptchaRegion != after.AliyunCaptchaRegion { changed = append(changed, "aliyun_captcha_region") } if before.APIKeyACLTrustForwardedIP != after.APIKeyACLTrustForwardedIP { changed = append(changed, "api_key_acl_trust_forwarded_ip") } if !equalStringSlice(before.ForwardedClientIPHeaders, after.ForwardedClientIPHeaders) { changed = append(changed, "forwarded_client_ip_headers") } if before.LinuxDoConnectEnabled != after.LinuxDoConnectEnabled { changed = append(changed, "linuxdo_connect_enabled") } if before.LinuxDoConnectClientID != after.LinuxDoConnectClientID { changed = append(changed, "linuxdo_connect_client_id") } if req.LinuxDoConnectClientSecret != "" { changed = append(changed, "linuxdo_connect_client_secret") } if before.LinuxDoConnectRedirectURL != after.LinuxDoConnectRedirectURL { changed = append(changed, "linuxdo_connect_redirect_url") } if before.DingTalkConnectEnabled != after.DingTalkConnectEnabled { changed = append(changed, "dingtalk_connect_enabled") } if before.DingTalkConnectClientID != after.DingTalkConnectClientID { changed = append(changed, "dingtalk_connect_client_id") } if req.DingTalkConnectClientSecret != "" { changed = append(changed, "dingtalk_connect_client_secret") } if before.DingTalkConnectRedirectURL != after.DingTalkConnectRedirectURL { changed = append(changed, "dingtalk_connect_redirect_url") } if before.DingTalkConnectCorpRestrictionPolicy != after.DingTalkConnectCorpRestrictionPolicy { changed = append(changed, "dingtalk_connect_corp_restriction_policy") } if before.DingTalkConnectInternalCorpID != after.DingTalkConnectInternalCorpID { changed = append(changed, "dingtalk_connect_internal_corp_id") } if before.DingTalkConnectBypassRegistration != after.DingTalkConnectBypassRegistration { changed = append(changed, "dingtalk_connect_bypass_registration") } if before.DingTalkConnectSyncCorpEmail != after.DingTalkConnectSyncCorpEmail { changed = append(changed, "dingtalk_connect_sync_corp_email") } if before.DingTalkConnectSyncDisplayName != after.DingTalkConnectSyncDisplayName { changed = append(changed, "dingtalk_connect_sync_display_name") } if before.DingTalkConnectSyncDept != after.DingTalkConnectSyncDept { changed = append(changed, "dingtalk_connect_sync_dept") } if before.DingTalkConnectSyncCorpEmailAttrKey != after.DingTalkConnectSyncCorpEmailAttrKey { changed = append(changed, "dingtalk_connect_sync_corp_email_attr_key") } if before.DingTalkConnectSyncDisplayNameAttrKey != after.DingTalkConnectSyncDisplayNameAttrKey { changed = append(changed, "dingtalk_connect_sync_display_name_attr_key") } if before.DingTalkConnectSyncDeptAttrKey != after.DingTalkConnectSyncDeptAttrKey { changed = append(changed, "dingtalk_connect_sync_dept_attr_key") } if before.WeChatConnectEnabled != after.WeChatConnectEnabled { changed = append(changed, "wechat_connect_enabled") } if before.WeChatConnectAppID != after.WeChatConnectAppID { changed = append(changed, "wechat_connect_app_id") } if req.WeChatConnectAppSecret != "" { changed = append(changed, "wechat_connect_app_secret") } if before.WeChatConnectOpenAppID != after.WeChatConnectOpenAppID { changed = append(changed, "wechat_connect_open_app_id") } if req.WeChatConnectOpenAppSecret != "" { changed = append(changed, "wechat_connect_open_app_secret") } if before.WeChatConnectMPAppID != after.WeChatConnectMPAppID { changed = append(changed, "wechat_connect_mp_app_id") } if req.WeChatConnectMPAppSecret != "" { changed = append(changed, "wechat_connect_mp_app_secret") } if before.WeChatConnectMobileAppID != after.WeChatConnectMobileAppID { changed = append(changed, "wechat_connect_mobile_app_id") } if req.WeChatConnectMobileAppSecret != "" { changed = append(changed, "wechat_connect_mobile_app_secret") } if before.WeChatConnectOpenEnabled != after.WeChatConnectOpenEnabled { changed = append(changed, "wechat_connect_open_enabled") } if before.WeChatConnectMPEnabled != after.WeChatConnectMPEnabled { changed = append(changed, "wechat_connect_mp_enabled") } if before.WeChatConnectMobileEnabled != after.WeChatConnectMobileEnabled { changed = append(changed, "wechat_connect_mobile_enabled") } if before.WeChatConnectMode != after.WeChatConnectMode { changed = append(changed, "wechat_connect_mode") } if before.WeChatConnectScopes != after.WeChatConnectScopes { changed = append(changed, "wechat_connect_scopes") } if before.WeChatConnectRedirectURL != after.WeChatConnectRedirectURL { changed = append(changed, "wechat_connect_redirect_url") } if before.WeChatConnectFrontendRedirectURL != after.WeChatConnectFrontendRedirectURL { changed = append(changed, "wechat_connect_frontend_redirect_url") } if before.OIDCConnectEnabled != after.OIDCConnectEnabled { changed = append(changed, "oidc_connect_enabled") } if before.OIDCConnectProviderName != after.OIDCConnectProviderName { changed = append(changed, "oidc_connect_provider_name") } if before.OIDCConnectClientID != after.OIDCConnectClientID { changed = append(changed, "oidc_connect_client_id") } if req.OIDCConnectClientSecret != "" { changed = append(changed, "oidc_connect_client_secret") } if before.OIDCConnectIssuerURL != after.OIDCConnectIssuerURL { changed = append(changed, "oidc_connect_issuer_url") } if before.OIDCConnectDiscoveryURL != after.OIDCConnectDiscoveryURL { changed = append(changed, "oidc_connect_discovery_url") } if before.OIDCConnectAuthorizeURL != after.OIDCConnectAuthorizeURL { changed = append(changed, "oidc_connect_authorize_url") } if before.OIDCConnectTokenURL != after.OIDCConnectTokenURL { changed = append(changed, "oidc_connect_token_url") } if before.OIDCConnectUserInfoURL != after.OIDCConnectUserInfoURL { changed = append(changed, "oidc_connect_userinfo_url") } if before.OIDCConnectJWKSURL != after.OIDCConnectJWKSURL { changed = append(changed, "oidc_connect_jwks_url") } if before.OIDCConnectScopes != after.OIDCConnectScopes { changed = append(changed, "oidc_connect_scopes") } if before.OIDCConnectRedirectURL != after.OIDCConnectRedirectURL { changed = append(changed, "oidc_connect_redirect_url") } if before.OIDCConnectFrontendRedirectURL != after.OIDCConnectFrontendRedirectURL { changed = append(changed, "oidc_connect_frontend_redirect_url") } if before.OIDCConnectTokenAuthMethod != after.OIDCConnectTokenAuthMethod { changed = append(changed, "oidc_connect_token_auth_method") } if before.OIDCConnectUsePKCE != after.OIDCConnectUsePKCE { changed = append(changed, "oidc_connect_use_pkce") } if before.OIDCConnectValidateIDToken != after.OIDCConnectValidateIDToken { changed = append(changed, "oidc_connect_validate_id_token") } if before.OIDCConnectAllowedSigningAlgs != after.OIDCConnectAllowedSigningAlgs { changed = append(changed, "oidc_connect_allowed_signing_algs") } if before.OIDCConnectClockSkewSeconds != after.OIDCConnectClockSkewSeconds { changed = append(changed, "oidc_connect_clock_skew_seconds") } if before.OIDCConnectRequireEmailVerified != after.OIDCConnectRequireEmailVerified { changed = append(changed, "oidc_connect_require_email_verified") } if before.OIDCConnectUserInfoEmailPath != after.OIDCConnectUserInfoEmailPath { changed = append(changed, "oidc_connect_userinfo_email_path") } if before.OIDCConnectUserInfoIDPath != after.OIDCConnectUserInfoIDPath { changed = append(changed, "oidc_connect_userinfo_id_path") } if before.OIDCConnectUserInfoUsernamePath != after.OIDCConnectUserInfoUsernamePath { changed = append(changed, "oidc_connect_userinfo_username_path") } if before.SiteName != after.SiteName { changed = append(changed, "site_name") } if before.SiteLogo != after.SiteLogo { changed = append(changed, "site_logo") } if before.SiteSubtitle != after.SiteSubtitle { changed = append(changed, "site_subtitle") } if before.APIBaseURL != after.APIBaseURL { changed = append(changed, "api_base_url") } if before.ContactInfo != after.ContactInfo { changed = append(changed, "contact_info") } if before.DocURL != after.DocURL { changed = append(changed, "doc_url") } if before.HomeContent != after.HomeContent { changed = append(changed, "home_content") } if before.CompactHomeEnabled != after.CompactHomeEnabled { changed = append(changed, "compact_home_enabled") } if before.HideCcsImportButton != after.HideCcsImportButton { changed = append(changed, "hide_ccs_import_button") } if before.DefaultConcurrency != after.DefaultConcurrency { changed = append(changed, "default_concurrency") } if before.DefaultBalance != after.DefaultBalance { changed = append(changed, "default_balance") } if before.AffiliateRebateRate != after.AffiliateRebateRate { changed = append(changed, "affiliate_rebate_rate") } if before.AffiliateRebateFreezeHours != after.AffiliateRebateFreezeHours { changed = append(changed, "affiliate_rebate_freeze_hours") } if before.AffiliateRebateDurationDays != after.AffiliateRebateDurationDays { changed = append(changed, "affiliate_rebate_duration_days") } if before.AffiliateRebatePerInviteeCap != after.AffiliateRebatePerInviteeCap { changed = append(changed, "affiliate_rebate_per_invitee_cap") } if before.AdminRechargeRebateEnabled != after.AdminRechargeRebateEnabled { changed = append(changed, "affiliate_admin_recharge_enabled") } if !equalDefaultSubscriptions(before.DefaultSubscriptions, after.DefaultSubscriptions) { changed = append(changed, "default_subscriptions") } if before.EnableModelFallback != after.EnableModelFallback { changed = append(changed, "enable_model_fallback") } if before.FallbackModelAnthropic != after.FallbackModelAnthropic { changed = append(changed, "fallback_model_anthropic") } if before.FallbackModelOpenAI != after.FallbackModelOpenAI { changed = append(changed, "fallback_model_openai") } if before.FallbackModelGemini != after.FallbackModelGemini { changed = append(changed, "fallback_model_gemini") } if before.FallbackModelAntigravity != after.FallbackModelAntigravity { changed = append(changed, "fallback_model_antigravity") } if before.EnableIdentityPatch != after.EnableIdentityPatch { changed = append(changed, "enable_identity_patch") } if before.IdentityPatchPrompt != after.IdentityPatchPrompt { changed = append(changed, "identity_patch_prompt") } if before.OpsMonitoringEnabled != after.OpsMonitoringEnabled { changed = append(changed, "ops_monitoring_enabled") } if before.OpsRealtimeMonitoringEnabled != after.OpsRealtimeMonitoringEnabled { changed = append(changed, "ops_realtime_monitoring_enabled") } if before.OpsQueryModeDefault != after.OpsQueryModeDefault { changed = append(changed, "ops_query_mode_default") } if before.OpsMetricsIntervalSeconds != after.OpsMetricsIntervalSeconds { changed = append(changed, "ops_metrics_interval_seconds") } if before.MinClaudeCodeVersion != after.MinClaudeCodeVersion { changed = append(changed, "min_claude_code_version") } if before.MaxClaudeCodeVersion != after.MaxClaudeCodeVersion { changed = append(changed, "max_claude_code_version") } if before.MinCodexVersion != after.MinCodexVersion { changed = append(changed, "min_codex_version") } if before.MaxCodexVersion != after.MaxCodexVersion { changed = append(changed, "max_codex_version") } if before.CodexCLIOnlyAllowAppServerClients != after.CodexCLIOnlyAllowAppServerClients { changed = append(changed, "codex_cli_only_allow_app_server_clients") } if before.CodexCLIOnlyEngineFingerprintSignals != after.CodexCLIOnlyEngineFingerprintSignals { changed = append(changed, "codex_cli_only_engine_fingerprint_signals") } if before.CodexCLIOnlyBlacklist != after.CodexCLIOnlyBlacklist { changed = append(changed, "codex_cli_only_blacklist") } if before.CodexCLIOnlyWhitelist != after.CodexCLIOnlyWhitelist { changed = append(changed, "codex_cli_only_whitelist") } if before.AllowUngroupedKeyScheduling != after.AllowUngroupedKeyScheduling { changed = append(changed, "allow_ungrouped_key_scheduling") } if before.BackendModeEnabled != after.BackendModeEnabled { changed = append(changed, "backend_mode_enabled") } if before.PurchaseSubscriptionEnabled != after.PurchaseSubscriptionEnabled { changed = append(changed, "purchase_subscription_enabled") } if before.PurchaseSubscriptionURL != after.PurchaseSubscriptionURL { changed = append(changed, "purchase_subscription_url") } if before.TableDefaultPageSize != after.TableDefaultPageSize { changed = append(changed, "table_default_page_size") } if !equalIntSlice(before.TablePageSizeOptions, after.TablePageSizeOptions) { changed = append(changed, "table_page_size_options") } if before.CustomMenuItems != after.CustomMenuItems { changed = append(changed, "custom_menu_items") } if before.CustomEndpoints != after.CustomEndpoints { changed = append(changed, "custom_endpoints") } if before.EnableFingerprintUnification != after.EnableFingerprintUnification { changed = append(changed, "enable_fingerprint_unification") } if before.EnableMetadataPassthrough != after.EnableMetadataPassthrough { changed = append(changed, "enable_metadata_passthrough") } if before.EnableCCHSigning != after.EnableCCHSigning { changed = append(changed, "enable_cch_signing") } if before.EnableClaudeOAuthSystemPromptInjection != after.EnableClaudeOAuthSystemPromptInjection { changed = append(changed, "enable_claude_oauth_system_prompt_injection") } if before.ClaudeOAuthSystemPrompt != after.ClaudeOAuthSystemPrompt { changed = append(changed, "claude_oauth_system_prompt") } if before.ClaudeOAuthSystemPromptBlocks != after.ClaudeOAuthSystemPromptBlocks { changed = append(changed, "claude_oauth_system_prompt_blocks") } if before.EnableAnthropicCacheTTL1hInjection != after.EnableAnthropicCacheTTL1hInjection { changed = append(changed, "enable_anthropic_cache_ttl_1h_injection") } if before.RewriteMessageCacheControl != after.RewriteMessageCacheControl { changed = append(changed, "rewrite_message_cache_control") } if before.EnableClientDatelineNormalization != after.EnableClientDatelineNormalization { changed = append(changed, "enable_client_dateline_normalization") } if before.AntigravityUserAgentVersion != after.AntigravityUserAgentVersion { changed = append(changed, "antigravity_user_agent_version") } if before.OpenAICodexUserAgent != after.OpenAICodexUserAgent { changed = append(changed, "openai_codex_user_agent") } if before.OpenAICodexClientVersion != after.OpenAICodexClientVersion { changed = append(changed, "openai_codex_client_version") } if before.OpenAICodexVersionAutoSyncEnabled != after.OpenAICodexVersionAutoSyncEnabled { changed = append(changed, "openai_codex_version_auto_sync_enabled") } if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource { changed = append(changed, "payment_visible_method_alipay_source") } if before.PaymentVisibleMethodWxpaySource != after.PaymentVisibleMethodWxpaySource { changed = append(changed, "payment_visible_method_wxpay_source") } if before.PaymentVisibleMethodAlipayEnabled != after.PaymentVisibleMethodAlipayEnabled { changed = append(changed, "payment_visible_method_alipay_enabled") } if before.PaymentVisibleMethodWxpayEnabled != after.PaymentVisibleMethodWxpayEnabled { changed = append(changed, "payment_visible_method_wxpay_enabled") } if before.OpenAILowUpstreamRatePriorityEnabled != after.OpenAILowUpstreamRatePriorityEnabled { changed = append(changed, "openai_low_upstream_rate_priority_enabled") } if before.OpenAIOAuthSchedulingRateMultiplier != after.OpenAIOAuthSchedulingRateMultiplier { changed = append(changed, "openai_oauth_scheduling_rate_multiplier") } if before.OpenAIAdvancedSchedulerEnabled != after.OpenAIAdvancedSchedulerEnabled { changed = append(changed, "openai_advanced_scheduler_enabled") } if before.OpenAIAdvancedSchedulerStickyWeightedEnabled != after.OpenAIAdvancedSchedulerStickyWeightedEnabled { changed = append(changed, "openai_advanced_scheduler_sticky_weighted_enabled") } if before.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled != after.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled { changed = append(changed, "openai_advanced_scheduler_subscription_priority_enabled") } if before.OpenAIAdvancedSchedulerLBTopK != after.OpenAIAdvancedSchedulerLBTopK { changed = append(changed, "openai_advanced_scheduler_lb_top_k") } if before.OpenAIAdvancedSchedulerWeightPriority != after.OpenAIAdvancedSchedulerWeightPriority { changed = append(changed, "openai_advanced_scheduler_weight_priority") } if before.OpenAIAdvancedSchedulerWeightLoad != after.OpenAIAdvancedSchedulerWeightLoad { changed = append(changed, "openai_advanced_scheduler_weight_load") } if before.OpenAIAdvancedSchedulerWeightQueue != after.OpenAIAdvancedSchedulerWeightQueue { changed = append(changed, "openai_advanced_scheduler_weight_queue") } if before.OpenAIAdvancedSchedulerWeightErrorRate != after.OpenAIAdvancedSchedulerWeightErrorRate { changed = append(changed, "openai_advanced_scheduler_weight_error_rate") } if before.OpenAIAdvancedSchedulerWeightTTFT != after.OpenAIAdvancedSchedulerWeightTTFT { changed = append(changed, "openai_advanced_scheduler_weight_ttft") } if before.OpenAIAdvancedSchedulerWeightReset != after.OpenAIAdvancedSchedulerWeightReset { changed = append(changed, "openai_advanced_scheduler_weight_reset") } if before.OpenAIAdvancedSchedulerWeightQuotaHeadroom != after.OpenAIAdvancedSchedulerWeightQuotaHeadroom { changed = append(changed, "openai_advanced_scheduler_weight_quota_headroom") } if before.OpenAIAdvancedSchedulerWeightUpstreamCost != after.OpenAIAdvancedSchedulerWeightUpstreamCost { changed = append(changed, "openai_advanced_scheduler_weight_upstream_cost") } if before.OpenAIAdvancedSchedulerWeightPreviousResponse != after.OpenAIAdvancedSchedulerWeightPreviousResponse { changed = append(changed, "openai_advanced_scheduler_weight_previous_response") } if before.OpenAIAdvancedSchedulerWeightSessionSticky != after.OpenAIAdvancedSchedulerWeightSessionSticky { changed = append(changed, "openai_advanced_scheduler_weight_session_sticky") } // 余额、订阅到期与账号限额通知 if before.BalanceLowNotifyEnabled != after.BalanceLowNotifyEnabled { changed = append(changed, "balance_low_notify_enabled") } if before.BalanceLowNotifyThreshold != after.BalanceLowNotifyThreshold { changed = append(changed, "balance_low_notify_threshold") } if before.BalanceLowNotifyRechargeURL != after.BalanceLowNotifyRechargeURL { changed = append(changed, "balance_low_notify_recharge_url") } if before.SubscriptionExpiryNotifyEnabled != after.SubscriptionExpiryNotifyEnabled { changed = append(changed, "subscription_expiry_notify_enabled") } if before.AccountQuotaNotifyEnabled != after.AccountQuotaNotifyEnabled { changed = append(changed, "account_quota_notify_enabled") } if !equalNotifyEmailEntries(before.AccountQuotaNotifyEmails, after.AccountQuotaNotifyEmails) { changed = append(changed, "account_quota_notify_emails") } if before.ChannelMonitorEnabled != after.ChannelMonitorEnabled { changed = append(changed, "channel_monitor_enabled") } if before.ChannelMonitorDefaultIntervalSeconds != after.ChannelMonitorDefaultIntervalSeconds { changed = append(changed, "channel_monitor_default_interval_seconds") } if before.AvailableChannelsEnabled != after.AvailableChannelsEnabled { changed = append(changed, "available_channels_enabled") } if before.ModelPlazaEnabled != after.ModelPlazaEnabled { changed = append(changed, "model_plaza_enabled") } if before.ModelPlazaRequireAuth != after.ModelPlazaRequireAuth { changed = append(changed, "model_plaza_require_auth") } if before.ModelPlazaDescription != after.ModelPlazaDescription { changed = append(changed, "model_plaza_description") } if before.AffiliateEnabled != after.AffiliateEnabled { changed = append(changed, "affiliate_enabled") } if before.RiskControlEnabled != after.RiskControlEnabled { changed = append(changed, "risk_control_enabled") } if before.CyberSessionBlockEnabled != after.CyberSessionBlockEnabled { changed = append(changed, "cyber_session_block_enabled") } if before.CyberSessionBlockTTLSeconds != after.CyberSessionBlockTTLSeconds { changed = append(changed, "cyber_session_block_ttl_seconds") } // Default platform quotas(JSON map,整体比较) if !equalPlatformQuotaSettings(before.DefaultPlatformQuotas, after.DefaultPlatformQuotas) { changed = append(changed, service.SettingKeyDefaultPlatformQuotas) } if !equalAccountSchedulingThresholds(before.AccountSchedulingThresholds, after.AccountSchedulingThresholds) { changed = append(changed, service.SettingKeyAccountSchedulingThresholds) } changed = appendAuthSourceDefaultChanges(changed, beforeAuthSourceDefaults, afterAuthSourceDefaults) return changed } func appendAuthSourceDefaultChanges(changed []string, before *service.AuthSourceDefaultSettings, after *service.AuthSourceDefaultSettings) []string { if before == nil { before = &service.AuthSourceDefaultSettings{} } if after == nil { after = &service.AuthSourceDefaultSettings{} } type providerDefaultGrantField struct { name string before service.ProviderDefaultGrantSettings after service.ProviderDefaultGrantSettings } fields := []providerDefaultGrantField{ {name: "email", before: before.Email, after: after.Email}, {name: "linuxdo", before: before.LinuxDo, after: after.LinuxDo}, {name: "oidc", before: before.OIDC, after: after.OIDC}, {name: "wechat", before: before.WeChat, after: after.WeChat}, {name: "github", before: before.GitHub, after: after.GitHub}, {name: "google", before: before.Google, after: after.Google}, {name: "dingtalk", before: before.DingTalk, after: after.DingTalk}, } for _, field := range fields { if field.before.Balance != field.after.Balance { changed = append(changed, "auth_source_default_"+field.name+"_balance") } if field.before.Concurrency != field.after.Concurrency { changed = append(changed, "auth_source_default_"+field.name+"_concurrency") } if !equalDefaultSubscriptions(field.before.Subscriptions, field.after.Subscriptions) { changed = append(changed, "auth_source_default_"+field.name+"_subscriptions") } if field.before.GrantOnSignup != field.after.GrantOnSignup { changed = append(changed, "auth_source_default_"+field.name+"_grant_on_signup") } if field.before.GrantOnFirstBind != field.after.GrantOnFirstBind { changed = append(changed, "auth_source_default_"+field.name+"_grant_on_first_bind") } // Platform quotas diff:整体替换语义,发单个 JSON key。 if !equalPlatformQuotaSettings(field.before.PlatformQuotas, field.after.PlatformQuotas) { changed = append(changed, service.SettingKeyAuthSourcePlatformQuotas(field.name)) } } if before.ForceEmailOnThirdPartySignup != after.ForceEmailOnThirdPartySignup { changed = append(changed, "force_email_on_third_party_signup") } return changed } func normalizeDefaultSubscriptions(input []dto.DefaultSubscriptionSetting) []dto.DefaultSubscriptionSetting { if len(input) == 0 { return nil } normalized := make([]dto.DefaultSubscriptionSetting, 0, len(input)) for _, item := range input { if item.GroupID <= 0 || item.ValidityDays <= 0 { continue } if item.ValidityDays > service.MaxValidityDays { item.ValidityDays = service.MaxValidityDays } normalized = append(normalized, item) } return normalized } func normalizeOptionalDefaultSubscriptions(input *[]dto.DefaultSubscriptionSetting) *[]dto.DefaultSubscriptionSetting { if input == nil { return nil } normalized := normalizeDefaultSubscriptions(*input) return &normalized } func float64ValueOrDefault(value *float64, fallback float64) float64 { if value == nil { return fallback } return *value } func intValueOrDefault(value *int, fallback int) int { if value == nil { return fallback } return *value } func boolValueOrDefault(value *bool, fallback bool) bool { if value == nil { return fallback } return *value } func defaultSubscriptionsValueOrDefault(input *[]dto.DefaultSubscriptionSetting, fallback []service.DefaultSubscriptionSetting) []service.DefaultSubscriptionSetting { if input == nil { return fallback } result := make([]service.DefaultSubscriptionSetting, 0, len(*input)) for _, item := range *input { result = append(result, service.DefaultSubscriptionSetting{ GroupID: item.GroupID, ValidityDays: item.ValidityDays, }) } return result } // platformQuotasValueOrDefault 处理 auth-source platform quota 的 nil 语义: // nil = 请求未包含该字段(保留 fallback),non-nil(含 empty map)= 整体覆盖。 // 注意:JSON null 与字段省略等价——两者均反序列化为 nil map,因此都保留旧值; // 若要清空某 source 的所有 quota 配置,须显式发空对象 {}。 func platformQuotasValueOrDefault(value, fallback map[string]*service.DefaultPlatformQuotaSetting) map[string]*service.DefaultPlatformQuotaSetting { if value == nil { return fallback } return value } func equalStringSlice(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func equalDefaultSubscriptions(a, b []service.DefaultSubscriptionSetting) bool { if len(a) != len(b) { return false } for i := range a { if a[i].GroupID != b[i].GroupID || a[i].ValidityDays != b[i].ValidityDays { return false } } return true } func equalLoginAgreementDocuments(a, b []service.LoginAgreementDocument) bool { if len(a) != len(b) { return false } for i := range a { if a[i].ID != b[i].ID || a[i].Title != b[i].Title || a[i].ContentMD != b[i].ContentMD { return false } } return true } func equalIntSlice(a, b []int) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func equalNotifyEmailEntries(a, b []service.NotifyEmailEntry) bool { if len(a) != len(b) { return false } for i := range a { if a[i].Email != b[i].Email || a[i].Verified != b[i].Verified || a[i].Disabled != b[i].Disabled { return false } } return true } // equalNullableFloat compares two *float64 values treating nil as a distinct case. func equalNullableFloat(a, b *float64) bool { if a == nil && b == nil { return true } if a == nil || b == nil { return false } return *a == *b } // slotOf returns the *float64 for the given window from a DefaultPlatformQuotaSetting. func slotOf(s *service.DefaultPlatformQuotaSetting, win string) *float64 { if s == nil { return nil } switch win { case "daily": return s.DailyLimitUSD case "weekly": return s.WeeklyLimitUSD case "monthly": return s.MonthlyLimitUSD } return nil } // equalPlatformQuotaSettings reports whether two platform-quota maps are identical across all allowed slots. func equalAccountSchedulingThresholds(before, after map[string]int) bool { for _, platform := range service.AllowedSchedulingThresholdPlatforms { beforeValue := 100 if before != nil { if value, ok := before[platform]; ok { beforeValue = value } } afterValue := 100 if after != nil { if value, ok := after[platform]; ok { afterValue = value } } if beforeValue != afterValue { return false } } return true } func equalPlatformQuotaSettings(before, after map[string]*service.DefaultPlatformQuotaSetting) bool { for _, platform := range service.AllowedQuotaPlatforms { b := before[platform] a := after[platform] if !equalNullableFloat(slotOf(b, "daily"), slotOf(a, "daily")) { return false } if !equalNullableFloat(slotOf(b, "weekly"), slotOf(a, "weekly")) { return false } if !equalNullableFloat(slotOf(b, "monthly"), slotOf(a, "monthly")) { return false } } return true } func stringSetting(value *string, fallback string) string { if value == nil { return fallback } return *value }