mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 15:57:53 +08:00
refactor(handler): 纯移动拆分 setting_handler.go(3957→468行)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,758 @@
|
||||
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.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.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.APIKeyACLTrustForwardedIP != after.APIKeyACLTrustForwardedIP {
|
||||
changed = append(changed, "api_key_acl_trust_forwarded_ip")
|
||||
}
|
||||
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.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 !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.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.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.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.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)
|
||||
}
|
||||
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 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
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// TestSMTPRequest 测试SMTP连接请求
|
||||
type TestSMTPRequest struct {
|
||||
SMTPHost string `json:"smtp_host"`
|
||||
SMTPPort int `json:"smtp_port"`
|
||||
SMTPUsername string `json:"smtp_username"`
|
||||
SMTPPassword string `json:"smtp_password"`
|
||||
SMTPUseTLS bool `json:"smtp_use_tls"`
|
||||
}
|
||||
|
||||
// TestSMTPConnection 测试SMTP连接
|
||||
// POST /api/v1/admin/settings/test-smtp
|
||||
func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
|
||||
var req TestSMTPRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
|
||||
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
|
||||
|
||||
var savedConfig *service.SMTPConfig
|
||||
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
|
||||
savedConfig = cfg
|
||||
}
|
||||
|
||||
if req.SMTPHost == "" && savedConfig != nil {
|
||||
req.SMTPHost = savedConfig.Host
|
||||
}
|
||||
if req.SMTPPort <= 0 {
|
||||
if savedConfig != nil && savedConfig.Port > 0 {
|
||||
req.SMTPPort = savedConfig.Port
|
||||
} else {
|
||||
req.SMTPPort = 587
|
||||
}
|
||||
}
|
||||
if req.SMTPUsername == "" && savedConfig != nil {
|
||||
req.SMTPUsername = savedConfig.Username
|
||||
}
|
||||
password := strings.TrimSpace(req.SMTPPassword)
|
||||
if password == "" && savedConfig != nil {
|
||||
password = savedConfig.Password
|
||||
}
|
||||
if req.SMTPHost == "" {
|
||||
response.BadRequest(c, "SMTP host is required")
|
||||
return
|
||||
}
|
||||
|
||||
config := &service.SMTPConfig{
|
||||
Host: req.SMTPHost,
|
||||
Port: req.SMTPPort,
|
||||
Username: req.SMTPUsername,
|
||||
Password: password,
|
||||
UseTLS: req.SMTPUseTLS,
|
||||
}
|
||||
|
||||
err := h.emailService.TestSMTPConnectionWithConfig(config)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "SMTP connection test failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "SMTP connection successful"})
|
||||
}
|
||||
|
||||
// SendTestEmailRequest 发送测试邮件请求
|
||||
type SendTestEmailRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
SMTPHost string `json:"smtp_host"`
|
||||
SMTPPort int `json:"smtp_port"`
|
||||
SMTPUsername string `json:"smtp_username"`
|
||||
SMTPPassword string `json:"smtp_password"`
|
||||
SMTPFrom string `json:"smtp_from_email"`
|
||||
SMTPFromName string `json:"smtp_from_name"`
|
||||
SMTPUseTLS bool `json:"smtp_use_tls"`
|
||||
}
|
||||
|
||||
// SendTestEmail 发送测试邮件
|
||||
// POST /api/v1/admin/settings/send-test-email
|
||||
func (h *SettingHandler) SendTestEmail(c *gin.Context) {
|
||||
var req SendTestEmailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
req.SMTPHost = strings.TrimSpace(req.SMTPHost)
|
||||
req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
|
||||
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
|
||||
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
|
||||
|
||||
var savedConfig *service.SMTPConfig
|
||||
if cfg, err := h.emailService.GetSMTPConfig(c.Request.Context()); err == nil && cfg != nil {
|
||||
savedConfig = cfg
|
||||
}
|
||||
|
||||
if req.SMTPHost == "" && savedConfig != nil {
|
||||
req.SMTPHost = savedConfig.Host
|
||||
}
|
||||
if req.SMTPPort <= 0 {
|
||||
if savedConfig != nil && savedConfig.Port > 0 {
|
||||
req.SMTPPort = savedConfig.Port
|
||||
} else {
|
||||
req.SMTPPort = 587
|
||||
}
|
||||
}
|
||||
if req.SMTPUsername == "" && savedConfig != nil {
|
||||
req.SMTPUsername = savedConfig.Username
|
||||
}
|
||||
password := strings.TrimSpace(req.SMTPPassword)
|
||||
if password == "" && savedConfig != nil {
|
||||
password = savedConfig.Password
|
||||
}
|
||||
if req.SMTPFrom == "" && savedConfig != nil {
|
||||
req.SMTPFrom = savedConfig.From
|
||||
}
|
||||
if req.SMTPFromName == "" && savedConfig != nil {
|
||||
req.SMTPFromName = savedConfig.FromName
|
||||
}
|
||||
if req.SMTPHost == "" {
|
||||
response.BadRequest(c, "SMTP host is required")
|
||||
return
|
||||
}
|
||||
|
||||
config := &service.SMTPConfig{
|
||||
Host: req.SMTPHost,
|
||||
Port: req.SMTPPort,
|
||||
Username: req.SMTPUsername,
|
||||
Password: password,
|
||||
From: req.SMTPFrom,
|
||||
FromName: req.SMTPFromName,
|
||||
UseTLS: req.SMTPUseTLS,
|
||||
}
|
||||
|
||||
siteName := h.settingService.GetSiteName(c.Request.Context())
|
||||
subject := "[" + siteName + "] Test Email"
|
||||
body := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; }
|
||||
.container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }
|
||||
.content { padding: 40px 30px; text-align: center; }
|
||||
.success { color: #10b981; font-size: 48px; margin-bottom: 20px; }
|
||||
.footer { background-color: #f8f9fa; padding: 20px; text-align: center; color: #999; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>` + siteName + `</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="success">✓</div>
|
||||
<h2>Email Configuration Successful!</h2>
|
||||
<p>This is a test email to verify your SMTP settings are working correctly.</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This is an automated test message.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
if err := h.emailService.SendEmailWithConfig(config, req.Email, subject, body); err != nil {
|
||||
response.BadRequest(c, "Failed to send test email: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Test email sent successfully"})
|
||||
}
|
||||
|
||||
// ListEmailTemplates returns all editable notification email templates.
|
||||
// GET /api/v1/admin/settings/email-templates
|
||||
func (h *SettingHandler) ListEmailTemplates(c *gin.Context) {
|
||||
if h.notificationEmailService == nil {
|
||||
response.InternalError(c, "notification email service is not configured")
|
||||
return
|
||||
}
|
||||
events := h.notificationEmailService.ListEventInfos()
|
||||
templates, err := h.notificationEmailService.ListTemplates(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, dto.EmailTemplateListResponse{
|
||||
Events: emailTemplateEventOptionsToDTO(events),
|
||||
Locales: h.notificationEmailService.SupportedLocales(),
|
||||
Templates: emailTemplateSummariesToDTO(templates),
|
||||
Placeholders: emailTemplatePlaceholderUnion(events),
|
||||
})
|
||||
}
|
||||
|
||||
// GetEmailTemplate returns one editable notification email template.
|
||||
// GET /api/v1/admin/settings/email-templates/:event/:locale
|
||||
func (h *SettingHandler) GetEmailTemplate(c *gin.Context) {
|
||||
if h.notificationEmailService == nil {
|
||||
response.InternalError(c, "notification email service is not configured")
|
||||
return
|
||||
}
|
||||
tmpl, err := h.notificationEmailService.GetTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.Success(c, emailTemplateDetailToDTO(tmpl))
|
||||
}
|
||||
|
||||
// UpdateEmailTemplate saves an override for one event/locale template.
|
||||
// PUT /api/v1/admin/settings/email-templates/:event/:locale
|
||||
func (h *SettingHandler) UpdateEmailTemplate(c *gin.Context) {
|
||||
if h.notificationEmailService == nil {
|
||||
response.InternalError(c, "notification email service is not configured")
|
||||
return
|
||||
}
|
||||
var req dto.UpdateEmailTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
tmpl, err := h.notificationEmailService.UpdateTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"), req.Subject, req.HTML)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.Success(c, emailTemplateDetailToDTO(tmpl))
|
||||
}
|
||||
|
||||
// RestoreOfficialEmailTemplate removes an override and returns the built-in template.
|
||||
// POST /api/v1/admin/settings/email-templates/:event/:locale/restore-official
|
||||
func (h *SettingHandler) RestoreOfficialEmailTemplate(c *gin.Context) {
|
||||
if h.notificationEmailService == nil {
|
||||
response.InternalError(c, "notification email service is not configured")
|
||||
return
|
||||
}
|
||||
tmpl, err := h.notificationEmailService.RestoreOfficialTemplate(c.Request.Context(), c.Param("event"), c.Param("locale"))
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.Success(c, emailTemplateDetailToDTO(tmpl))
|
||||
}
|
||||
|
||||
// PreviewEmailTemplate renders a template with safe sample variables without saving it.
|
||||
// POST /api/v1/admin/settings/email-templates/preview
|
||||
func (h *SettingHandler) PreviewEmailTemplate(c *gin.Context) {
|
||||
if h.notificationEmailService == nil {
|
||||
response.InternalError(c, "notification email service is not configured")
|
||||
return
|
||||
}
|
||||
var req dto.PreviewEmailTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
preview, err := h.notificationEmailService.PreviewTemplate(c.Request.Context(), service.NotificationEmailPreviewInput{
|
||||
Event: req.Event,
|
||||
Locale: req.Locale,
|
||||
Subject: req.Subject,
|
||||
HTML: req.HTML,
|
||||
Variables: req.Variables,
|
||||
})
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.Success(c, dto.EmailTemplatePreviewResponse{Subject: preview.Subject, HTML: preview.HTML})
|
||||
}
|
||||
|
||||
func emailTemplateEventOptionsToDTO(events []service.NotificationEmailEventInfo) []dto.EmailTemplateEventOption {
|
||||
items := make([]dto.EmailTemplateEventOption, 0, len(events))
|
||||
for _, event := range events {
|
||||
items = append(items, dto.EmailTemplateEventOption{
|
||||
Value: event.Event,
|
||||
Label: event.Label,
|
||||
Description: event.Description,
|
||||
Category: event.Category,
|
||||
Optional: event.Optional,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func emailTemplateSummariesToDTO(templates []service.NotificationEmailTemplate) []dto.EmailTemplateSummary {
|
||||
items := make([]dto.EmailTemplateSummary, 0, len(templates))
|
||||
for _, tmpl := range templates {
|
||||
items = append(items, dto.EmailTemplateSummary{
|
||||
Event: tmpl.Event,
|
||||
Locale: tmpl.Locale,
|
||||
Subject: tmpl.Subject,
|
||||
IsCustom: tmpl.IsCustom,
|
||||
UpdatedAt: emailTemplateUpdatedAt(tmpl),
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func emailTemplateDetailToDTO(tmpl service.NotificationEmailTemplate) dto.EmailTemplateDetail {
|
||||
return dto.EmailTemplateDetail{
|
||||
Event: tmpl.Event,
|
||||
Locale: tmpl.Locale,
|
||||
Subject: tmpl.Subject,
|
||||
HTML: tmpl.HTML,
|
||||
IsCustom: tmpl.IsCustom,
|
||||
UpdatedAt: emailTemplateUpdatedAt(tmpl),
|
||||
Placeholders: tmpl.Placeholders,
|
||||
}
|
||||
}
|
||||
|
||||
func emailTemplateUpdatedAt(tmpl service.NotificationEmailTemplate) string {
|
||||
if tmpl.UpdatedAt == nil {
|
||||
return ""
|
||||
}
|
||||
return tmpl.UpdatedAt.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
|
||||
func emailTemplatePlaceholderUnion(events []service.NotificationEmailEventInfo) []string {
|
||||
seen := make(map[string]struct{})
|
||||
placeholders := make([]string, 0)
|
||||
for _, event := range events {
|
||||
for _, placeholder := range event.Placeholders {
|
||||
if _, ok := seen[placeholder]; ok {
|
||||
continue
|
||||
}
|
||||
seen[placeholder] = struct{}{}
|
||||
placeholders = append(placeholders, placeholder)
|
||||
}
|
||||
}
|
||||
return placeholders
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// GetAdminAPIKey 获取管理员 API Key 状态
|
||||
// GET /api/v1/admin/settings/admin-api-key
|
||||
func (h *SettingHandler) GetAdminAPIKey(c *gin.Context) {
|
||||
maskedKey, exists, err := h.settingService.GetAdminAPIKeyStatus(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"exists": exists,
|
||||
"masked_key": maskedKey,
|
||||
})
|
||||
}
|
||||
|
||||
// RegenerateAdminAPIKey 生成/重新生成管理员 API Key
|
||||
// POST /api/v1/admin/settings/admin-api-key/regenerate
|
||||
func (h *SettingHandler) RegenerateAdminAPIKey(c *gin.Context) {
|
||||
key, err := h.settingService.GenerateAdminAPIKey(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"key": key, // 完整 key 只在生成时返回一次
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAdminAPIKey 删除管理员 API Key
|
||||
// DELETE /api/v1/admin/settings/admin-api-key
|
||||
func (h *SettingHandler) DeleteAdminAPIKey(c *gin.Context) {
|
||||
if err := h.settingService.DeleteAdminAPIKey(c.Request.Context()); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Admin API key deleted"})
|
||||
}
|
||||
|
||||
// GetOverloadCooldownSettings 获取529过载冷却配置
|
||||
// GET /api/v1/admin/settings/overload-cooldown
|
||||
func (h *SettingHandler) GetOverloadCooldownSettings(c *gin.Context) {
|
||||
settings, err := h.settingService.GetOverloadCooldownSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.OverloadCooldownSettings{
|
||||
Enabled: settings.Enabled,
|
||||
CooldownMinutes: settings.CooldownMinutes,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateOverloadCooldownSettingsRequest 更新529过载冷却配置请求
|
||||
type UpdateOverloadCooldownSettingsRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CooldownMinutes int `json:"cooldown_minutes"`
|
||||
}
|
||||
|
||||
// UpdateOverloadCooldownSettings 更新529过载冷却配置
|
||||
// PUT /api/v1/admin/settings/overload-cooldown
|
||||
func (h *SettingHandler) UpdateOverloadCooldownSettings(c *gin.Context) {
|
||||
var req UpdateOverloadCooldownSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
settings := &service.OverloadCooldownSettings{
|
||||
Enabled: req.Enabled,
|
||||
CooldownMinutes: req.CooldownMinutes,
|
||||
}
|
||||
|
||||
if err := h.settingService.SetOverloadCooldownSettings(c.Request.Context(), settings); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updatedSettings, err := h.settingService.GetOverloadCooldownSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.OverloadCooldownSettings{
|
||||
Enabled: updatedSettings.Enabled,
|
||||
CooldownMinutes: updatedSettings.CooldownMinutes,
|
||||
})
|
||||
}
|
||||
|
||||
// GetRateLimit429CooldownSettings 获取429默认回避配置
|
||||
// GET /api/v1/admin/settings/rate-limit-429-cooldown
|
||||
func (h *SettingHandler) GetRateLimit429CooldownSettings(c *gin.Context) {
|
||||
settings, err := h.settingService.GetRateLimit429CooldownSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.RateLimit429CooldownSettings{
|
||||
Enabled: settings.Enabled,
|
||||
CooldownSeconds: settings.CooldownSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRateLimit429CooldownSettingsRequest 更新429默认回避配置请求
|
||||
type UpdateRateLimit429CooldownSettingsRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
CooldownSeconds int `json:"cooldown_seconds"`
|
||||
}
|
||||
|
||||
// UpdateRateLimit429CooldownSettings 更新429默认回避配置
|
||||
// PUT /api/v1/admin/settings/rate-limit-429-cooldown
|
||||
func (h *SettingHandler) UpdateRateLimit429CooldownSettings(c *gin.Context) {
|
||||
var req UpdateRateLimit429CooldownSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
settings := &service.RateLimit429CooldownSettings{
|
||||
Enabled: req.Enabled,
|
||||
CooldownSeconds: req.CooldownSeconds,
|
||||
}
|
||||
|
||||
if err := h.settingService.SetRateLimit429CooldownSettings(c.Request.Context(), settings); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updatedSettings, err := h.settingService.GetRateLimit429CooldownSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.RateLimit429CooldownSettings{
|
||||
Enabled: updatedSettings.Enabled,
|
||||
CooldownSeconds: updatedSettings.CooldownSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStreamTimeoutSettings 获取流超时处理配置
|
||||
// GET /api/v1/admin/settings/stream-timeout
|
||||
func (h *SettingHandler) GetStreamTimeoutSettings(c *gin.Context) {
|
||||
settings, err := h.settingService.GetStreamTimeoutSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.StreamTimeoutSettings{
|
||||
Enabled: settings.Enabled,
|
||||
Action: settings.Action,
|
||||
TempUnschedMinutes: settings.TempUnschedMinutes,
|
||||
ThresholdCount: settings.ThresholdCount,
|
||||
ThresholdWindowMinutes: settings.ThresholdWindowMinutes,
|
||||
})
|
||||
}
|
||||
|
||||
// GetRectifierSettings 获取请求整流器配置
|
||||
// GET /api/v1/admin/settings/rectifier
|
||||
func (h *SettingHandler) GetRectifierSettings(c *gin.Context) {
|
||||
settings, err := h.settingService.GetRectifierSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
patterns := settings.APIKeySignaturePatterns
|
||||
if patterns == nil {
|
||||
patterns = []string{}
|
||||
}
|
||||
response.Success(c, dto.RectifierSettings{
|
||||
Enabled: settings.Enabled,
|
||||
ThinkingSignatureEnabled: settings.ThinkingSignatureEnabled,
|
||||
ThinkingBudgetEnabled: settings.ThinkingBudgetEnabled,
|
||||
APIKeySignatureEnabled: settings.APIKeySignatureEnabled,
|
||||
APIKeySignaturePatterns: patterns,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRectifierSettingsRequest 更新整流器配置请求
|
||||
type UpdateRectifierSettingsRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"`
|
||||
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
|
||||
APIKeySignatureEnabled bool `json:"apikey_signature_enabled"`
|
||||
APIKeySignaturePatterns []string `json:"apikey_signature_patterns"`
|
||||
}
|
||||
|
||||
// UpdateRectifierSettings 更新请求整流器配置
|
||||
// PUT /api/v1/admin/settings/rectifier
|
||||
func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) {
|
||||
var req UpdateRectifierSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 校验并清理自定义匹配关键词
|
||||
const maxPatterns = 50
|
||||
const maxPatternLen = 500
|
||||
if len(req.APIKeySignaturePatterns) > maxPatterns {
|
||||
response.BadRequest(c, "Too many signature patterns (max 50)")
|
||||
return
|
||||
}
|
||||
var cleanedPatterns []string
|
||||
for _, p := range req.APIKeySignaturePatterns {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if len(p) > maxPatternLen {
|
||||
response.BadRequest(c, "Signature pattern too long (max 500 characters)")
|
||||
return
|
||||
}
|
||||
cleanedPatterns = append(cleanedPatterns, p)
|
||||
}
|
||||
|
||||
settings := &service.RectifierSettings{
|
||||
Enabled: req.Enabled,
|
||||
ThinkingSignatureEnabled: req.ThinkingSignatureEnabled,
|
||||
ThinkingBudgetEnabled: req.ThinkingBudgetEnabled,
|
||||
APIKeySignatureEnabled: req.APIKeySignatureEnabled,
|
||||
APIKeySignaturePatterns: cleanedPatterns,
|
||||
}
|
||||
|
||||
if err := h.settingService.SetRectifierSettings(c.Request.Context(), settings); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 重新获取设置返回
|
||||
updatedSettings, err := h.settingService.GetRectifierSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedPatterns := updatedSettings.APIKeySignaturePatterns
|
||||
if updatedPatterns == nil {
|
||||
updatedPatterns = []string{}
|
||||
}
|
||||
response.Success(c, dto.RectifierSettings{
|
||||
Enabled: updatedSettings.Enabled,
|
||||
ThinkingSignatureEnabled: updatedSettings.ThinkingSignatureEnabled,
|
||||
ThinkingBudgetEnabled: updatedSettings.ThinkingBudgetEnabled,
|
||||
APIKeySignatureEnabled: updatedSettings.APIKeySignatureEnabled,
|
||||
APIKeySignaturePatterns: updatedPatterns,
|
||||
})
|
||||
}
|
||||
|
||||
// GetBetaPolicySettings 获取 Beta 策略配置
|
||||
// GET /api/v1/admin/settings/beta-policy
|
||||
func (h *SettingHandler) GetBetaPolicySettings(c *gin.Context) {
|
||||
settings, err := h.settingService.GetBetaPolicySettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
rules := make([]dto.BetaPolicyRule, len(settings.Rules))
|
||||
for i, r := range settings.Rules {
|
||||
rules[i] = dto.BetaPolicyRule(r)
|
||||
}
|
||||
response.Success(c, dto.BetaPolicySettings{Rules: rules})
|
||||
}
|
||||
|
||||
// UpdateBetaPolicySettingsRequest 更新 Beta 策略配置请求
|
||||
type UpdateBetaPolicySettingsRequest struct {
|
||||
Rules []dto.BetaPolicyRule `json:"rules"`
|
||||
}
|
||||
|
||||
// UpdateBetaPolicySettings 更新 Beta 策略配置
|
||||
// PUT /api/v1/admin/settings/beta-policy
|
||||
func (h *SettingHandler) UpdateBetaPolicySettings(c *gin.Context) {
|
||||
var req UpdateBetaPolicySettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rules := make([]service.BetaPolicyRule, len(req.Rules))
|
||||
for i, r := range req.Rules {
|
||||
rules[i] = service.BetaPolicyRule(r)
|
||||
}
|
||||
|
||||
settings := &service.BetaPolicySettings{Rules: rules}
|
||||
if err := h.settingService.SetBetaPolicySettings(c.Request.Context(), settings); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Re-fetch to return updated settings
|
||||
updated, err := h.settingService.GetBetaPolicySettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
outRules := make([]dto.BetaPolicyRule, len(updated.Rules))
|
||||
for i, r := range updated.Rules {
|
||||
outRules[i] = dto.BetaPolicyRule(r)
|
||||
}
|
||||
response.Success(c, dto.BetaPolicySettings{Rules: outRules})
|
||||
}
|
||||
|
||||
// UpdateStreamTimeoutSettingsRequest 更新流超时配置请求
|
||||
type UpdateStreamTimeoutSettingsRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Action string `json:"action"`
|
||||
TempUnschedMinutes int `json:"temp_unsched_minutes"`
|
||||
ThresholdCount int `json:"threshold_count"`
|
||||
ThresholdWindowMinutes int `json:"threshold_window_minutes"`
|
||||
}
|
||||
|
||||
// UpdateStreamTimeoutSettings 更新流超时处理配置
|
||||
// PUT /api/v1/admin/settings/stream-timeout
|
||||
func (h *SettingHandler) UpdateStreamTimeoutSettings(c *gin.Context) {
|
||||
var req UpdateStreamTimeoutSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
settings := &service.StreamTimeoutSettings{
|
||||
Enabled: req.Enabled,
|
||||
Action: req.Action,
|
||||
TempUnschedMinutes: req.TempUnschedMinutes,
|
||||
ThresholdCount: req.ThresholdCount,
|
||||
ThresholdWindowMinutes: req.ThresholdWindowMinutes,
|
||||
}
|
||||
|
||||
if err := h.settingService.SetStreamTimeoutSettings(c.Request.Context(), settings); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 重新获取设置返回
|
||||
updatedSettings, err := h.settingService.GetStreamTimeoutSettings(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.StreamTimeoutSettings{
|
||||
Enabled: updatedSettings.Enabled,
|
||||
Action: updatedSettings.Action,
|
||||
TempUnschedMinutes: updatedSettings.TempUnschedMinutes,
|
||||
ThresholdCount: updatedSettings.ThresholdCount,
|
||||
ThresholdWindowMinutes: updatedSettings.ThresholdWindowMinutes,
|
||||
})
|
||||
}
|
||||
|
||||
// GetWebSearchEmulationConfig 获取 Web Search 模拟配置
|
||||
// GET /api/v1/admin/settings/web-search-emulation
|
||||
func (h *SettingHandler) GetWebSearchEmulationConfig(c *gin.Context) {
|
||||
cfg, err := h.settingService.GetWebSearchEmulationConfig(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, service.PopulateWebSearchUsage(c.Request.Context(), cfg))
|
||||
}
|
||||
|
||||
// UpdateWebSearchEmulationConfig 更新 Web Search 模拟配置
|
||||
// PUT /api/v1/admin/settings/web-search-emulation
|
||||
func (h *SettingHandler) UpdateWebSearchEmulationConfig(c *gin.Context) {
|
||||
var cfg service.WebSearchEmulationConfig
|
||||
if err := c.ShouldBindJSON(&cfg); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.settingService.SaveWebSearchEmulationConfig(c.Request.Context(), &cfg); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-read (with sanitized api keys) to return current state
|
||||
updated, err := h.settingService.GetWebSearchEmulationConfig(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, service.PopulateWebSearchUsage(c.Request.Context(), updated))
|
||||
}
|
||||
|
||||
// ResetWebSearchUsage 重置指定 provider 的配额用量
|
||||
// POST /api/v1/admin/settings/web-search-emulation/reset-usage
|
||||
func (h *SettingHandler) ResetWebSearchUsage(c *gin.Context) {
|
||||
var req struct {
|
||||
ProviderType string `json:"provider_type"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
if req.ProviderType == "" {
|
||||
response.BadRequest(c, "provider_type is required")
|
||||
return
|
||||
}
|
||||
if err := service.ResetWebSearchUsage(c.Request.Context(), req.ProviderType); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// TestWebSearchEmulation 测试 Web Search 搜索
|
||||
// POST /api/v1/admin/settings/web-search-emulation/test
|
||||
func (h *SettingHandler) TestWebSearchEmulation(c *gin.Context) {
|
||||
var req struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Query) == "" {
|
||||
req.Query = "搜索今年世界大事件"
|
||||
}
|
||||
|
||||
result, err := service.TestWebSearch(c.Request.Context(), req.Query)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user