setting_handler_update.go 137 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491
  1. package admin
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "log/slog"
  7. "net/http"
  8. "reflect"
  9. "strconv"
  10. "strings"
  11. "github.com/Wei-Shaw/sub2api/internal/config"
  12. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  14. "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  15. "github.com/Wei-Shaw/sub2api/internal/service"
  16. "github.com/gin-gonic/gin"
  17. "github.com/gin-gonic/gin/binding"
  18. )
  19. // UpdateSettingsRequest 更新设置请求
  20. type UpdateSettingsRequest struct {
  21. // 注册设置
  22. RegistrationEnabled bool `json:"registration_enabled"`
  23. EmailVerifyEnabled bool `json:"email_verify_enabled"`
  24. RegistrationEmailSuffixWhitelist []string `json:"registration_email_suffix_whitelist"`
  25. RegistrationEmailDomainQuotaEnabled *bool `json:"registration_email_domain_quota_enabled"` // 非白名单域名限量注册开关(省略=保持现值)
  26. PromoCodeEnabled bool `json:"promo_code_enabled"`
  27. PasswordResetEnabled bool `json:"password_reset_enabled"`
  28. FrontendURL string `json:"frontend_url"`
  29. InvitationCodeEnabled bool `json:"invitation_code_enabled"`
  30. TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
  31. PasskeyEnabled *bool `json:"passkey_enabled"` // Passkey 登录(省略=保持现值)
  32. SessionBindingEnabled *bool `json:"session_binding_enabled"` // 会话 IP/UA 绑定(省略=保持现值)
  33. StepUpEnabled *bool `json:"step_up_enabled"` // 敏感操作 step-up 2FA(省略=保持现值)
  34. AuditLogRetentionDays int `json:"audit_log_retention_days"` // 审计日志保留天数
  35. LoginAgreementEnabled bool `json:"login_agreement_enabled"`
  36. LoginAgreementMode string `json:"login_agreement_mode"`
  37. LoginAgreementUpdatedAt string `json:"login_agreement_updated_at"`
  38. LoginAgreementDocuments []dto.LoginAgreementDocument `json:"login_agreement_documents"`
  39. // 邮件服务设置
  40. SMTPHost string `json:"smtp_host"`
  41. SMTPPort int `json:"smtp_port"`
  42. SMTPUsername string `json:"smtp_username"`
  43. SMTPPassword string `json:"smtp_password"`
  44. SMTPFrom string `json:"smtp_from_email"`
  45. SMTPFromName string `json:"smtp_from_name"`
  46. SMTPUseTLS bool `json:"smtp_use_tls"`
  47. // Cloudflare Turnstile 设置
  48. TurnstileEnabled bool `json:"turnstile_enabled"`
  49. TurnstileSiteKey string `json:"turnstile_site_key"`
  50. TurnstileSecretKey string `json:"turnstile_secret_key"`
  51. // 腾讯天御验证码设置
  52. TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
  53. TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
  54. TencentCaptchaAppSecretKey string `json:"tencent_captcha_app_secret_key"`
  55. TencentCaptchaCloudSecretID string `json:"tencent_captcha_cloud_secret_id"`
  56. TencentCaptchaCloudSecretKey string `json:"tencent_captcha_cloud_secret_key"`
  57. TencentCaptchaRegion string `json:"tencent_captcha_region"`
  58. // 阿里云验证码 2.0 设置
  59. AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
  60. AliyunCaptchaAccessKeyID string `json:"aliyun_captcha_access_key_id"`
  61. AliyunCaptchaAccessKeySecret string `json:"aliyun_captcha_access_key_secret"`
  62. AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
  63. AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
  64. AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
  65. // API Key IP 访问控制设置
  66. APIKeyACLTrustForwardedIP *bool `json:"api_key_acl_trust_forwarded_ip"`
  67. ForwardedClientIPHeaders *[]string `json:"forwarded_client_ip_headers"`
  68. // LinuxDo Connect OAuth 登录
  69. LinuxDoConnectEnabled bool `json:"linuxdo_connect_enabled"`
  70. LinuxDoConnectClientID string `json:"linuxdo_connect_client_id"`
  71. LinuxDoConnectClientSecret string `json:"linuxdo_connect_client_secret"`
  72. LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
  73. // DingTalk Connect OAuth 登录
  74. DingTalkConnectEnabled bool `json:"dingtalk_connect_enabled"`
  75. DingTalkConnectClientID string `json:"dingtalk_connect_client_id"`
  76. DingTalkConnectClientSecret string `json:"dingtalk_connect_client_secret"`
  77. DingTalkConnectRedirectURL string `json:"dingtalk_connect_redirect_url"`
  78. DingTalkConnectCorpRestrictionPolicy string `json:"dingtalk_connect_corp_restriction_policy"`
  79. DingTalkConnectInternalCorpID string `json:"dingtalk_connect_internal_corp_id"`
  80. DingTalkConnectBypassRegistration bool `json:"dingtalk_connect_bypass_registration"`
  81. DingTalkConnectSyncCorpEmail bool `json:"dingtalk_connect_sync_corp_email"`
  82. DingTalkConnectSyncDisplayName bool `json:"dingtalk_connect_sync_display_name"`
  83. DingTalkConnectSyncDept bool `json:"dingtalk_connect_sync_dept"`
  84. DingTalkConnectSyncCorpEmailAttrKey string `json:"dingtalk_connect_sync_corp_email_attr_key"`
  85. DingTalkConnectSyncDisplayNameAttrKey string `json:"dingtalk_connect_sync_display_name_attr_key"`
  86. DingTalkConnectSyncDeptAttrKey string `json:"dingtalk_connect_sync_dept_attr_key"`
  87. DingTalkConnectSyncCorpEmailAttrName string `json:"dingtalk_connect_sync_corp_email_attr_name"`
  88. DingTalkConnectSyncDisplayNameAttrName string `json:"dingtalk_connect_sync_display_name_attr_name"`
  89. DingTalkConnectSyncDeptAttrName string `json:"dingtalk_connect_sync_dept_attr_name"`
  90. // WeChat Connect OAuth 登录
  91. WeChatConnectEnabled bool `json:"wechat_connect_enabled"`
  92. WeChatConnectAppID string `json:"wechat_connect_app_id"`
  93. WeChatConnectAppSecret string `json:"wechat_connect_app_secret"`
  94. WeChatConnectOpenAppID string `json:"wechat_connect_open_app_id"`
  95. WeChatConnectOpenAppSecret string `json:"wechat_connect_open_app_secret"`
  96. WeChatConnectMPAppID string `json:"wechat_connect_mp_app_id"`
  97. WeChatConnectMPAppSecret string `json:"wechat_connect_mp_app_secret"`
  98. WeChatConnectMobileAppID string `json:"wechat_connect_mobile_app_id"`
  99. WeChatConnectMobileAppSecret string `json:"wechat_connect_mobile_app_secret"`
  100. WeChatConnectOpenEnabled bool `json:"wechat_connect_open_enabled"`
  101. WeChatConnectMPEnabled bool `json:"wechat_connect_mp_enabled"`
  102. WeChatConnectMobileEnabled bool `json:"wechat_connect_mobile_enabled"`
  103. WeChatConnectMode string `json:"wechat_connect_mode"`
  104. WeChatConnectScopes string `json:"wechat_connect_scopes"`
  105. WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"`
  106. WeChatConnectFrontendRedirectURL string `json:"wechat_connect_frontend_redirect_url"`
  107. // Generic OIDC OAuth 登录
  108. OIDCConnectEnabled bool `json:"oidc_connect_enabled"`
  109. OIDCConnectProviderName string `json:"oidc_connect_provider_name"`
  110. OIDCConnectClientID string `json:"oidc_connect_client_id"`
  111. OIDCConnectClientSecret string `json:"oidc_connect_client_secret"`
  112. OIDCConnectIssuerURL string `json:"oidc_connect_issuer_url"`
  113. OIDCConnectDiscoveryURL string `json:"oidc_connect_discovery_url"`
  114. OIDCConnectAuthorizeURL string `json:"oidc_connect_authorize_url"`
  115. OIDCConnectTokenURL string `json:"oidc_connect_token_url"`
  116. OIDCConnectUserInfoURL string `json:"oidc_connect_userinfo_url"`
  117. OIDCConnectJWKSURL string `json:"oidc_connect_jwks_url"`
  118. OIDCConnectScopes string `json:"oidc_connect_scopes"`
  119. OIDCConnectRedirectURL string `json:"oidc_connect_redirect_url"`
  120. OIDCConnectFrontendRedirectURL string `json:"oidc_connect_frontend_redirect_url"`
  121. OIDCConnectTokenAuthMethod string `json:"oidc_connect_token_auth_method"`
  122. OIDCConnectUsePKCE *bool `json:"oidc_connect_use_pkce"`
  123. OIDCConnectValidateIDToken *bool `json:"oidc_connect_validate_id_token"`
  124. OIDCConnectAllowedSigningAlgs string `json:"oidc_connect_allowed_signing_algs"`
  125. OIDCConnectClockSkewSeconds int `json:"oidc_connect_clock_skew_seconds"`
  126. OIDCConnectRequireEmailVerified bool `json:"oidc_connect_require_email_verified"`
  127. OIDCConnectUserInfoEmailPath string `json:"oidc_connect_userinfo_email_path"`
  128. OIDCConnectUserInfoIDPath string `json:"oidc_connect_userinfo_id_path"`
  129. OIDCConnectUserInfoUsernamePath string `json:"oidc_connect_userinfo_username_path"`
  130. GitHubOAuthEnabled bool `json:"github_oauth_enabled"`
  131. GitHubOAuthClientID string `json:"github_oauth_client_id"`
  132. GitHubOAuthClientSecret string `json:"github_oauth_client_secret"`
  133. GitHubOAuthRedirectURL string `json:"github_oauth_redirect_url"`
  134. GitHubOAuthFrontendRedirectURL string `json:"github_oauth_frontend_redirect_url"`
  135. GoogleOAuthEnabled bool `json:"google_oauth_enabled"`
  136. GoogleOAuthClientID string `json:"google_oauth_client_id"`
  137. GoogleOAuthClientSecret string `json:"google_oauth_client_secret"`
  138. GoogleOAuthRedirectURL string `json:"google_oauth_redirect_url"`
  139. GoogleOAuthFrontendRedirectURL string `json:"google_oauth_frontend_redirect_url"`
  140. // OEM设置
  141. SiteName string `json:"site_name"`
  142. SiteLogo string `json:"site_logo"`
  143. SiteSubtitle string `json:"site_subtitle"`
  144. APIBaseURL string `json:"api_base_url"`
  145. ContactInfo string `json:"contact_info"`
  146. DocURL string `json:"doc_url"`
  147. HomeContent string `json:"home_content"`
  148. CompactHomeEnabled bool `json:"compact_home_enabled"`
  149. HideCcsImportButton bool `json:"hide_ccs_import_button"`
  150. PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"`
  151. PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
  152. TableDefaultPageSize int `json:"table_default_page_size"`
  153. TablePageSizeOptions []int `json:"table_page_size_options"`
  154. CustomMenuItems *[]dto.CustomMenuItem `json:"custom_menu_items"`
  155. CustomEndpoints *[]dto.CustomEndpoint `json:"custom_endpoints"`
  156. // 默认配置
  157. DefaultConcurrency int `json:"default_concurrency"`
  158. DefaultBalance float64 `json:"default_balance"`
  159. AffiliateRebateRate *float64 `json:"affiliate_rebate_rate"`
  160. AffiliateRebateFreezeHours *int `json:"affiliate_rebate_freeze_hours"`
  161. AffiliateRebateDurationDays *int `json:"affiliate_rebate_duration_days"`
  162. AffiliateRebatePerInviteeCap *float64 `json:"affiliate_rebate_per_invitee_cap"`
  163. AdminRechargeRebateEnabled *bool `json:"affiliate_admin_recharge_enabled"`
  164. DefaultUserRPMLimit int `json:"default_user_rpm_limit"`
  165. DefaultSubscriptions []dto.DefaultSubscriptionSetting `json:"default_subscriptions"`
  166. AuthSourceDefaultEmailBalance *float64 `json:"auth_source_default_email_balance"`
  167. AuthSourceDefaultEmailConcurrency *int `json:"auth_source_default_email_concurrency"`
  168. AuthSourceDefaultEmailSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_email_subscriptions"`
  169. AuthSourceDefaultEmailGrantOnSignup *bool `json:"auth_source_default_email_grant_on_signup"`
  170. AuthSourceDefaultEmailGrantOnFirstBind *bool `json:"auth_source_default_email_grant_on_first_bind"`
  171. AuthSourceDefaultLinuxDoBalance *float64 `json:"auth_source_default_linuxdo_balance"`
  172. AuthSourceDefaultLinuxDoConcurrency *int `json:"auth_source_default_linuxdo_concurrency"`
  173. AuthSourceDefaultLinuxDoSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_linuxdo_subscriptions"`
  174. AuthSourceDefaultLinuxDoGrantOnSignup *bool `json:"auth_source_default_linuxdo_grant_on_signup"`
  175. AuthSourceDefaultLinuxDoGrantOnFirstBind *bool `json:"auth_source_default_linuxdo_grant_on_first_bind"`
  176. AuthSourceDefaultOIDCBalance *float64 `json:"auth_source_default_oidc_balance"`
  177. AuthSourceDefaultOIDCConcurrency *int `json:"auth_source_default_oidc_concurrency"`
  178. AuthSourceDefaultOIDCSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_oidc_subscriptions"`
  179. AuthSourceDefaultOIDCGrantOnSignup *bool `json:"auth_source_default_oidc_grant_on_signup"`
  180. AuthSourceDefaultOIDCGrantOnFirstBind *bool `json:"auth_source_default_oidc_grant_on_first_bind"`
  181. AuthSourceDefaultWeChatBalance *float64 `json:"auth_source_default_wechat_balance"`
  182. AuthSourceDefaultWeChatConcurrency *int `json:"auth_source_default_wechat_concurrency"`
  183. AuthSourceDefaultWeChatSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_wechat_subscriptions"`
  184. AuthSourceDefaultWeChatGrantOnSignup *bool `json:"auth_source_default_wechat_grant_on_signup"`
  185. AuthSourceDefaultWeChatGrantOnFirstBind *bool `json:"auth_source_default_wechat_grant_on_first_bind"`
  186. AuthSourceDefaultGitHubBalance *float64 `json:"auth_source_default_github_balance"`
  187. AuthSourceDefaultGitHubConcurrency *int `json:"auth_source_default_github_concurrency"`
  188. AuthSourceDefaultGitHubSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_github_subscriptions"`
  189. AuthSourceDefaultGitHubGrantOnSignup *bool `json:"auth_source_default_github_grant_on_signup"`
  190. AuthSourceDefaultGitHubGrantOnFirstBind *bool `json:"auth_source_default_github_grant_on_first_bind"`
  191. AuthSourceDefaultGoogleBalance *float64 `json:"auth_source_default_google_balance"`
  192. AuthSourceDefaultGoogleConcurrency *int `json:"auth_source_default_google_concurrency"`
  193. AuthSourceDefaultGoogleSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_google_subscriptions"`
  194. AuthSourceDefaultGoogleGrantOnSignup *bool `json:"auth_source_default_google_grant_on_signup"`
  195. AuthSourceDefaultGoogleGrantOnFirstBind *bool `json:"auth_source_default_google_grant_on_first_bind"`
  196. AuthSourceDefaultDingTalkBalance *float64 `json:"auth_source_default_dingtalk_balance"`
  197. AuthSourceDefaultDingTalkConcurrency *int `json:"auth_source_default_dingtalk_concurrency"`
  198. AuthSourceDefaultDingTalkSubscriptions *[]dto.DefaultSubscriptionSetting `json:"auth_source_default_dingtalk_subscriptions"`
  199. AuthSourceDefaultDingTalkGrantOnSignup *bool `json:"auth_source_default_dingtalk_grant_on_signup"`
  200. AuthSourceDefaultDingTalkGrantOnFirstBind *bool `json:"auth_source_default_dingtalk_grant_on_first_bind"`
  201. ForceEmailOnThirdPartySignup *bool `json:"force_email_on_third_party_signup"`
  202. // Model fallback configuration
  203. EnableModelFallback bool `json:"enable_model_fallback"`
  204. FallbackModelAnthropic string `json:"fallback_model_anthropic"`
  205. FallbackModelOpenAI string `json:"fallback_model_openai"`
  206. FallbackModelGemini string `json:"fallback_model_gemini"`
  207. FallbackModelAntigravity string `json:"fallback_model_antigravity"`
  208. // Identity patch configuration (Claude -> Gemini)
  209. EnableIdentityPatch bool `json:"enable_identity_patch"`
  210. IdentityPatchPrompt string `json:"identity_patch_prompt"`
  211. // Ops monitoring (vNext)
  212. OpsMonitoringEnabled *bool `json:"ops_monitoring_enabled"`
  213. OpsRealtimeMonitoringEnabled *bool `json:"ops_realtime_monitoring_enabled"`
  214. OpsQueryModeDefault *string `json:"ops_query_mode_default"`
  215. OpsMetricsIntervalSeconds *int `json:"ops_metrics_interval_seconds"`
  216. MinClaudeCodeVersion string `json:"min_claude_code_version"`
  217. MaxClaudeCodeVersion string `json:"max_claude_code_version"`
  218. // 分组隔离
  219. AllowUngroupedKeyScheduling bool `json:"allow_ungrouped_key_scheduling"`
  220. // Backend Mode
  221. BackendModeEnabled bool `json:"backend_mode_enabled"`
  222. // Gateway forwarding behavior
  223. OpenAITTFTMode *string `json:"openai_ttft_mode"`
  224. EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
  225. EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
  226. EnableCCHSigning *bool `json:"enable_cch_signing"`
  227. EnableClaudeOAuthSystemPromptInjection *bool `json:"enable_claude_oauth_system_prompt_injection"`
  228. ClaudeOAuthSystemPrompt *string `json:"claude_oauth_system_prompt"`
  229. ClaudeOAuthSystemPromptBlocks *string `json:"claude_oauth_system_prompt_blocks"`
  230. EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
  231. RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
  232. EnableClientDatelineNormalization *bool `json:"enable_client_dateline_normalization"`
  233. AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
  234. OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
  235. OpenAICodexClientVersion *string `json:"openai_codex_client_version"`
  236. OpenAICodexVersionAutoSyncEnabled *bool `json:"openai_codex_version_auto_sync_enabled"`
  237. // codex_cli_only 加固(global-only)
  238. MinCodexVersion string `json:"min_codex_version"`
  239. MaxCodexVersion string `json:"max_codex_version"`
  240. CodexCLIOnlyBlacklist string `json:"codex_cli_only_blacklist"`
  241. CodexCLIOnlyWhitelist string `json:"codex_cli_only_whitelist"`
  242. CodexCLIOnlyAllowAppServerClients *bool `json:"codex_cli_only_allow_app_server_clients"`
  243. CodexCLIOnlyEngineFingerprintSignals string `json:"codex_cli_only_engine_fingerprint_signals"`
  244. // Payment visible method routing
  245. PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
  246. PaymentVisibleMethodWxpaySource *string `json:"payment_visible_method_wxpay_source"`
  247. PaymentVisibleMethodAlipayEnabled *bool `json:"payment_visible_method_alipay_enabled"`
  248. PaymentVisibleMethodWxpayEnabled *bool `json:"payment_visible_method_wxpay_enabled"`
  249. // OpenAI account scheduling
  250. OpenAILowUpstreamRatePriorityEnabled *bool `json:"openai_low_upstream_rate_priority_enabled"`
  251. OpenAIOAuthSchedulingRateMultiplier *float64 `json:"openai_oauth_scheduling_rate_multiplier"`
  252. OpenAIAdvancedSchedulerEnabled *bool `json:"openai_advanced_scheduler_enabled"`
  253. OpenAIAdvancedSchedulerStickyWeightedEnabled *bool `json:"openai_advanced_scheduler_sticky_weighted_enabled"`
  254. OpenAIAdvancedSchedulerSubscriptionPriorityEnabled *bool `json:"openai_advanced_scheduler_subscription_priority_enabled"`
  255. OpenAIAdvancedSchedulerLBTopK *string `json:"openai_advanced_scheduler_lb_top_k"`
  256. OpenAIAdvancedSchedulerWeightPriority *string `json:"openai_advanced_scheduler_weight_priority"`
  257. OpenAIAdvancedSchedulerWeightLoad *string `json:"openai_advanced_scheduler_weight_load"`
  258. OpenAIAdvancedSchedulerWeightQueue *string `json:"openai_advanced_scheduler_weight_queue"`
  259. OpenAIAdvancedSchedulerWeightErrorRate *string `json:"openai_advanced_scheduler_weight_error_rate"`
  260. OpenAIAdvancedSchedulerWeightTTFT *string `json:"openai_advanced_scheduler_weight_ttft"`
  261. OpenAIAdvancedSchedulerWeightReset *string `json:"openai_advanced_scheduler_weight_reset"`
  262. OpenAIAdvancedSchedulerWeightQuotaHeadroom *string `json:"openai_advanced_scheduler_weight_quota_headroom"`
  263. OpenAIAdvancedSchedulerWeightUpstreamCost *string `json:"openai_advanced_scheduler_weight_upstream_cost"`
  264. OpenAIAdvancedSchedulerWeightPreviousResponse *string `json:"openai_advanced_scheduler_weight_previous_response"`
  265. OpenAIAdvancedSchedulerWeightSessionSticky *string `json:"openai_advanced_scheduler_weight_session_sticky"`
  266. // 余额不足提醒
  267. BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"`
  268. BalanceLowNotifyThreshold *float64 `json:"balance_low_notify_threshold"`
  269. BalanceLowNotifyRechargeURL *string `json:"balance_low_notify_recharge_url"`
  270. SubscriptionExpiryNotifyEnabled *bool `json:"subscription_expiry_notify_enabled"`
  271. AccountQuotaNotifyEnabled *bool `json:"account_quota_notify_enabled"`
  272. AccountQuotaNotifyEmails *[]dto.NotifyEmailEntry `json:"account_quota_notify_emails"`
  273. // Payment configuration (integrated into settings, full replace)
  274. PaymentEnabled *bool `json:"payment_enabled"`
  275. PaymentMinAmount *float64 `json:"payment_min_amount"`
  276. PaymentMaxAmount *float64 `json:"payment_max_amount"`
  277. PaymentDailyLimit *float64 `json:"payment_daily_limit"`
  278. PaymentOrderTimeoutMin *int `json:"payment_order_timeout_minutes"`
  279. PaymentMaxPendingOrders *int `json:"payment_max_pending_orders"`
  280. PaymentEnabledTypes []string `json:"payment_enabled_types"`
  281. PaymentBalanceDisabled *bool `json:"payment_balance_disabled"`
  282. PaymentBalanceRechargeMultiplier *float64 `json:"payment_balance_recharge_multiplier"`
  283. PaymentSubscriptionUSDToCNYRate *float64 `json:"payment_subscription_usd_to_cny_rate"`
  284. PaymentRechargeFeeRate *float64 `json:"payment_recharge_fee_rate"`
  285. PaymentLoadBalanceStrat *string `json:"payment_load_balance_strategy"`
  286. PaymentProductNamePrefix *string `json:"payment_product_name_prefix"`
  287. PaymentProductNameSuffix *string `json:"payment_product_name_suffix"`
  288. PaymentHelpImageURL *string `json:"payment_help_image_url"`
  289. PaymentHelpText *string `json:"payment_help_text"`
  290. // Cancel rate limit
  291. PaymentCancelRateLimitEnabled *bool `json:"payment_cancel_rate_limit_enabled"`
  292. PaymentCancelRateLimitMax *int `json:"payment_cancel_rate_limit_max"`
  293. PaymentCancelRateLimitWindow *int `json:"payment_cancel_rate_limit_window"`
  294. PaymentCancelRateLimitUnit *string `json:"payment_cancel_rate_limit_unit"`
  295. PaymentCancelRateLimitMode *string `json:"payment_cancel_rate_limit_window_mode"`
  296. // Force Alipay mobile clients to use QR code payment instead of mobile redirect
  297. PaymentAlipayForceQRCode *bool `json:"payment_alipay_force_qrcode"`
  298. // Use Alipay face-to-face precreate and an app deep link on mobile clients.
  299. PaymentAlipayMobilePrecreateDeepLink *bool `json:"payment_alipay_mobile_precreate_deep_link"`
  300. // Channel Monitor feature switch
  301. ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"`
  302. ChannelMonitorMode *string `json:"channel_monitor_mode"`
  303. ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"`
  304. ChannelMonitorHideThroughput *bool `json:"channel_monitor_hide_throughput"`
  305. ChannelMonitorShowQuota *bool `json:"channel_monitor_show_quota"`
  306. ChannelMonitorHideUserRanking *bool `json:"channel_monitor_hide_user_ranking"`
  307. // Grok model mapping policy
  308. GrokDefaultTextModel *string `json:"grok_default_text_model"`
  309. GrokCrossClientModelMapEnabled *bool `json:"grok_cross_client_model_map_enabled"`
  310. GrokDefaultBaseURLMode *string `json:"grok_default_base_url_mode"`
  311. // Available Channels feature switch (user-facing)
  312. AvailableChannelsEnabled *bool `json:"available_channels_enabled"`
  313. // Model Plaza feature switches + description
  314. ModelPlazaEnabled *bool `json:"model_plaza_enabled"`
  315. ModelPlazaRequireAuth *bool `json:"model_plaza_require_auth"`
  316. ModelPlazaDescription *string `json:"model_plaza_description"`
  317. // Plugin management menu visibility switch; plugin runtime is unaffected.
  318. PluginManagementEnabled *bool `json:"plugin_management_enabled"`
  319. // Affiliate (邀请返利) feature switch
  320. AffiliateEnabled *bool `json:"affiliate_enabled"`
  321. // 风控中心功能开关
  322. RiskControlEnabled *bool `json:"risk_control_enabled"`
  323. // cyber 会话屏蔽开关 + TTL
  324. CyberSessionBlockEnabled *bool `json:"cyber_session_block_enabled"`
  325. CyberSessionBlockTTLSeconds *int `json:"cyber_session_block_ttl_seconds"`
  326. // OpenAI fast/flex policy (optional, only updated when provided)
  327. OpenAIFastPolicySettings *dto.OpenAIFastPolicySettings `json:"openai_fast_policy_settings,omitempty"`
  328. // 系统全局 platform quota 默认值(整体替换语义:nil = 不修改,non-nil = 整体覆盖)。
  329. DefaultPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"default_platform_quotas"`
  330. // 各平台账号自动停调阈值(整体替换语义:nil = 不修改,non-nil = 整体覆盖)。
  331. AccountSchedulingThresholds map[string]int `json:"account_scheduling_thresholds"`
  332. // auth-source 层 platform quota 覆盖(override 语义:nil = 不修改,non-nil = 整体覆盖该 source 的 quota 配置)。
  333. AuthSourceEmailPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_email_platform_quotas"`
  334. AuthSourceLinuxDoPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_linuxdo_platform_quotas"`
  335. AuthSourceOIDCPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_oidc_platform_quotas"`
  336. AuthSourceWeChatPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_wechat_platform_quotas"`
  337. AuthSourceGitHubPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_github_platform_quotas"`
  338. AuthSourceGooglePlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_google_platform_quotas"`
  339. AuthSourceDingTalkPlatformQuotas map[string]*service.DefaultPlatformQuotaSetting `json:"auth_source_default_dingtalk_platform_quotas"`
  340. AllowUserViewErrorRequests *bool `json:"allow_user_view_error_requests"`
  341. }
  342. // UpdateSettings 更新系统设置
  343. // PUT /api/v1/admin/settings
  344. // ensureActorTotpForStepUp 校验当前操作者具备开启 step-up 门控的条件:
  345. // 必须是真人管理员会话(admin API key 无法完成 TOTP step-up,拒绝)且本人已启用 TOTP。
  346. // 校验失败时写入错误响应并返回 false。
  347. func (h *SettingHandler) ensureActorTotpForStepUp(c *gin.Context) bool {
  348. if c.GetString("auth_method") == service.AuditAuthMethodAdminAPIKey {
  349. response.ErrorWithDetails(c, http.StatusForbidden,
  350. "Admin API key cannot enable step-up verification; use an admin session with TOTP enabled",
  351. "STEP_UP_ADMIN_API_KEY_FORBIDDEN", nil)
  352. return false
  353. }
  354. subject, ok := middleware.GetAuthSubjectFromContext(c)
  355. if !ok || subject.UserID <= 0 {
  356. response.ErrorWithDetails(c, http.StatusForbidden,
  357. "Enabling step-up verification requires an authenticated admin session",
  358. "STEP_UP_ENABLE_REQUIRES_TOTP", nil)
  359. return false
  360. }
  361. if h.userService == nil {
  362. response.InternalError(c, "Step-up precondition check unavailable")
  363. return false
  364. }
  365. user, err := h.userService.GetByID(c.Request.Context(), subject.UserID)
  366. if err != nil {
  367. response.ErrorFrom(c, err)
  368. return false
  369. }
  370. if !user.TotpEnabled {
  371. response.ErrorWithDetails(c, http.StatusBadRequest,
  372. "Enable two-factor authentication (TOTP) for your account before turning on step-up verification",
  373. "STEP_UP_ENABLE_REQUIRES_TOTP", nil)
  374. return false
  375. }
  376. return true
  377. }
  378. // settingKeyJSONAliases covers the request fields whose JSON name differs from
  379. // the setting key they persist to. Every other field of UpdateSettingsRequest
  380. // is named after its setting key.
  381. var settingKeyJSONAliases = map[string]string{
  382. "smtp_from_email": service.SettingKeySMTPFrom,
  383. }
  384. // settingKeyByJSONName maps the value-typed top-level JSON fields of
  385. // UpdateSettingsRequest to the setting key each one writes. Resolved once from
  386. // the struct tags so new fields are covered without touching this file.
  387. //
  388. // Pointer-typed fields are deliberately excluded: they already carry their own
  389. // "omitted = keep the stored value" merge in UpdateSettings, and some of them
  390. // rely on being rewritten on every save to re-normalize fail-closed security
  391. // state (see TestUpdateSettingsMalformedForwardedClientIPHeadersRemainFailClosedWhenOmitted).
  392. // Only the value-typed fields are indistinguishable from a deliberate clear.
  393. var settingKeyByJSONName = buildSettingKeyByJSONName()
  394. func buildSettingKeyByJSONName() map[string]string {
  395. t := reflect.TypeOf(UpdateSettingsRequest{})
  396. out := make(map[string]string, t.NumField())
  397. for i := 0; i < t.NumField(); i++ {
  398. field := t.Field(i)
  399. if field.Type.Kind() == reflect.Pointer {
  400. continue
  401. }
  402. name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
  403. if name == "" || name == "-" {
  404. continue
  405. }
  406. if alias, ok := settingKeyJSONAliases[name]; ok {
  407. out[name] = alias
  408. continue
  409. }
  410. out[name] = name
  411. }
  412. return out
  413. }
  414. // omittedSettingKeys reports the setting keys this payload never mentioned.
  415. // Saving settings is a whole-document PUT, so without this a client that sends
  416. // only the one field it cares about resets every other field to a zero value.
  417. func omittedSettingKeys(sentFields map[string]json.RawMessage) service.OmittedSettingKeys {
  418. omitted := make(service.OmittedSettingKeys, len(settingKeyByJSONName))
  419. for jsonName, settingKey := range settingKeyByJSONName {
  420. if _, sent := sentFields[jsonName]; !sent {
  421. omitted[settingKey] = struct{}{}
  422. }
  423. }
  424. return omitted
  425. }
  426. func settingsAuditRequest(req UpdateSettingsRequest) UpdateSettingsRequest {
  427. req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
  428. req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
  429. req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
  430. req.AliyunCaptchaAccessKeySecret = strings.TrimSpace(req.AliyunCaptchaAccessKeySecret)
  431. return req
  432. }
  433. func (h *SettingHandler) UpdateSettings(c *gin.Context) {
  434. var sentFields map[string]json.RawMessage
  435. if err := c.ShouldBindBodyWith(&sentFields, binding.JSON); err != nil {
  436. response.BadRequest(c, "Invalid request: "+err.Error())
  437. return
  438. }
  439. var req UpdateSettingsRequest
  440. if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
  441. response.BadRequest(c, "Invalid request: "+err.Error())
  442. return
  443. }
  444. auditReq := settingsAuditRequest(req)
  445. omitted := omittedSettingKeys(sentFields)
  446. previousSettings, err := h.settingService.GetAllSettings(c.Request.Context())
  447. if err != nil {
  448. response.ErrorFrom(c, err)
  449. return
  450. }
  451. previousAuthSourceDefaults, err := h.settingService.GetAuthSourceDefaultSettings(c.Request.Context())
  452. if err != nil {
  453. response.ErrorFrom(c, err)
  454. return
  455. }
  456. // 两个安全开关的请求字段为指针:省略字段=保持现值,避免旧客户端/脚本
  457. // 用不含新字段的全量 payload 保存设置时把安全开关静默重置。
  458. sessionBindingEnabled := previousSettings.SessionBindingEnabled
  459. if req.SessionBindingEnabled != nil {
  460. sessionBindingEnabled = *req.SessionBindingEnabled
  461. }
  462. stepUpEnabled := previousSettings.StepUpEnabled
  463. if req.StepUpEnabled != nil {
  464. stepUpEnabled = *req.StepUpEnabled
  465. }
  466. passkeyEnabled := previousSettings.PasskeyEnabled
  467. if req.PasskeyEnabled != nil {
  468. passkeyEnabled = *req.PasskeyEnabled
  469. }
  470. registrationEmailDomainQuotaEnabled := previousSettings.RegistrationEmailDomainQuotaEnabled
  471. if req.RegistrationEmailDomainQuotaEnabled != nil {
  472. registrationEmailDomainQuotaEnabled = *req.RegistrationEmailDomainQuotaEnabled
  473. }
  474. if passkeyEnabled {
  475. configured, _, _ := h.settingService.PasskeyConfiguration()
  476. if !configured {
  477. response.BadRequest(c, "Passkey sign-in requires a valid WebAuthn RP ID and allowed HTTPS origins in the deployment configuration")
  478. return
  479. }
  480. }
  481. forwardedClientIPHeaders := append([]string(nil), previousSettings.ForwardedClientIPHeaders...)
  482. if req.ForwardedClientIPHeaders != nil {
  483. forwardedClientIPHeaders = append([]string(nil), (*req.ForwardedClientIPHeaders)...)
  484. }
  485. // 开启敏感操作 step-up 门控属自锁风险操作:仅允许本人已启用 TOTP 的管理员会话开启,
  486. // 否则开启后操作者立即被挡在所有敏感操作之外。仅在 false→true 的开启瞬间校验,
  487. // 保持开启状态的常规设置保存不受影响。
  488. if stepUpEnabled && !previousSettings.StepUpEnabled {
  489. if !h.ensureActorTotpForStepUp(c) {
  490. return
  491. }
  492. }
  493. // 关闭 step-up 门控本身就是敏感操作:防止拿到管理员会话的攻击者先关闸再执行导出/备份。
  494. // previousSettings 已证实开关处于开启状态,使用无条件门控变体,
  495. // 避免门控内部二次读取开关时因存储故障 fail-open(前端捕获 STEP_UP_REQUIRED 弹码重试)。
  496. if !stepUpEnabled && previousSettings.StepUpEnabled {
  497. if !middleware.EnforceStepUpAlways(c, h.totpService, h.userService) {
  498. return
  499. }
  500. }
  501. // 验证参数
  502. if req.DefaultConcurrency < 1 {
  503. req.DefaultConcurrency = 1
  504. }
  505. if req.DefaultBalance < 0 {
  506. req.DefaultBalance = 0
  507. }
  508. affiliateRebateRate := previousSettings.AffiliateRebateRate
  509. if req.AffiliateRebateRate != nil {
  510. affiliateRebateRate = *req.AffiliateRebateRate
  511. }
  512. if affiliateRebateRate < service.AffiliateRebateRateMin {
  513. affiliateRebateRate = service.AffiliateRebateRateMin
  514. }
  515. if affiliateRebateRate > service.AffiliateRebateRateMax {
  516. affiliateRebateRate = service.AffiliateRebateRateMax
  517. }
  518. affiliateRebateFreezeHours := previousSettings.AffiliateRebateFreezeHours
  519. if req.AffiliateRebateFreezeHours != nil {
  520. affiliateRebateFreezeHours = *req.AffiliateRebateFreezeHours
  521. }
  522. if affiliateRebateFreezeHours < 0 {
  523. affiliateRebateFreezeHours = service.AffiliateRebateFreezeHoursDefault
  524. }
  525. if affiliateRebateFreezeHours > service.AffiliateRebateFreezeHoursMax {
  526. affiliateRebateFreezeHours = service.AffiliateRebateFreezeHoursMax
  527. }
  528. affiliateRebateDurationDays := previousSettings.AffiliateRebateDurationDays
  529. if req.AffiliateRebateDurationDays != nil {
  530. affiliateRebateDurationDays = *req.AffiliateRebateDurationDays
  531. }
  532. if affiliateRebateDurationDays < 0 {
  533. affiliateRebateDurationDays = service.AffiliateRebateDurationDaysDefault
  534. }
  535. if affiliateRebateDurationDays > service.AffiliateRebateDurationDaysMax {
  536. affiliateRebateDurationDays = service.AffiliateRebateDurationDaysMax
  537. }
  538. affiliateRebatePerInviteeCap := previousSettings.AffiliateRebatePerInviteeCap
  539. if req.AffiliateRebatePerInviteeCap != nil {
  540. affiliateRebatePerInviteeCap = *req.AffiliateRebatePerInviteeCap
  541. }
  542. if affiliateRebatePerInviteeCap < 0 {
  543. affiliateRebatePerInviteeCap = service.AffiliateRebatePerInviteeCapDefault
  544. }
  545. adminRechargeRebateEnabled := previousSettings.AdminRechargeRebateEnabled
  546. if req.AdminRechargeRebateEnabled != nil {
  547. adminRechargeRebateEnabled = *req.AdminRechargeRebateEnabled
  548. }
  549. // 通用表格配置:兼容旧客户端未传字段时保留当前值。
  550. if req.TableDefaultPageSize <= 0 {
  551. req.TableDefaultPageSize = previousSettings.TableDefaultPageSize
  552. }
  553. if req.TablePageSizeOptions == nil {
  554. req.TablePageSizeOptions = previousSettings.TablePageSizeOptions
  555. }
  556. req.SMTPHost = strings.TrimSpace(req.SMTPHost)
  557. req.SMTPUsername = strings.TrimSpace(req.SMTPUsername)
  558. req.SMTPPassword = strings.TrimSpace(req.SMTPPassword)
  559. req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
  560. req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
  561. req.TencentCaptchaAppID = strings.TrimSpace(req.TencentCaptchaAppID)
  562. req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
  563. req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
  564. req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
  565. if req.SMTPPort <= 0 {
  566. req.SMTPPort = 587
  567. }
  568. req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions)
  569. req.AuthSourceDefaultEmailSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultEmailSubscriptions)
  570. req.AuthSourceDefaultLinuxDoSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultLinuxDoSubscriptions)
  571. req.AuthSourceDefaultOIDCSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultOIDCSubscriptions)
  572. req.AuthSourceDefaultWeChatSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultWeChatSubscriptions)
  573. req.AuthSourceDefaultDingTalkSubscriptions = normalizeOptionalDefaultSubscriptions(req.AuthSourceDefaultDingTalkSubscriptions)
  574. // SMTP 配置保护:如果请求中 smtp_host 为空但数据库中已有配置,则保留已有 SMTP 配置
  575. // 防止前端加载设置失败时空表单覆盖已保存的 SMTP 配置
  576. if req.SMTPHost == "" && previousSettings.SMTPHost != "" {
  577. req.SMTPHost = previousSettings.SMTPHost
  578. req.SMTPPort = previousSettings.SMTPPort
  579. req.SMTPUsername = previousSettings.SMTPUsername
  580. req.SMTPFrom = previousSettings.SMTPFrom
  581. req.SMTPFromName = previousSettings.SMTPFromName
  582. req.SMTPUseTLS = previousSettings.SMTPUseTLS
  583. }
  584. turnstileEnabled := req.TurnstileEnabled
  585. if _, sent := sentFields["turnstile_enabled"]; !sent {
  586. turnstileEnabled = previousSettings.TurnstileEnabled
  587. }
  588. tencentCaptchaEnabled := req.TencentCaptchaEnabled
  589. if _, sent := sentFields["tencent_captcha_enabled"]; !sent {
  590. tencentCaptchaEnabled = previousSettings.TencentCaptchaEnabled
  591. }
  592. aliyunCaptchaEnabled := req.AliyunCaptchaEnabled
  593. if _, sent := sentFields["aliyun_captcha_enabled"]; !sent {
  594. aliyunCaptchaEnabled = previousSettings.AliyunCaptchaEnabled
  595. }
  596. enabledCaptchaProviders := 0
  597. for _, enabled := range []bool{turnstileEnabled, tencentCaptchaEnabled, aliyunCaptchaEnabled} {
  598. if enabled {
  599. enabledCaptchaProviders++
  600. }
  601. }
  602. if enabledCaptchaProviders > 1 {
  603. response.BadRequest(c, "Multiple captcha providers (Cloudflare Turnstile / Tencent Captcha / Aliyun Captcha) cannot be enabled at the same time")
  604. return
  605. }
  606. // 阿里云地域 normalize:未发送保留已存值,非法值一律按中国内地落库
  607. if _, sent := sentFields["aliyun_captcha_region"]; !sent {
  608. req.AliyunCaptchaRegion = previousSettings.AliyunCaptchaRegion
  609. }
  610. if req.AliyunCaptchaRegion != service.AliyunCaptchaRegionSGP {
  611. req.AliyunCaptchaRegion = service.AliyunCaptchaRegionCN
  612. }
  613. // 天御站点 normalize:未发送保留已存值,非法值一律按中国站落库
  614. if _, sent := sentFields["tencent_captcha_region"]; !sent {
  615. req.TencentCaptchaRegion = previousSettings.TencentCaptchaRegion
  616. }
  617. if req.TencentCaptchaRegion != service.TencentCaptchaRegionINTL {
  618. req.TencentCaptchaRegion = service.TencentCaptchaRegionCN
  619. }
  620. // Turnstile 参数验证
  621. if req.TurnstileEnabled {
  622. // 检查必填字段
  623. if req.TurnstileSiteKey == "" {
  624. response.BadRequest(c, "Turnstile Site Key is required when enabled")
  625. return
  626. }
  627. // 如果未提供 secret key,使用已保存的值(留空保留当前值)
  628. if req.TurnstileSecretKey == "" {
  629. if previousSettings.TurnstileSecretKey == "" {
  630. response.BadRequest(c, "Turnstile Secret Key is required when enabled")
  631. return
  632. }
  633. req.TurnstileSecretKey = previousSettings.TurnstileSecretKey
  634. }
  635. // 当 site_key 或 secret_key 任一变化时验证(避免配置错误导致无法登录)
  636. siteKeyChanged := previousSettings.TurnstileSiteKey != req.TurnstileSiteKey
  637. secretKeyChanged := previousSettings.TurnstileSecretKey != req.TurnstileSecretKey
  638. if siteKeyChanged || secretKeyChanged {
  639. if err := h.turnstileService.ValidateSecretKey(c.Request.Context(), req.TurnstileSecretKey); err != nil {
  640. response.ErrorFrom(c, err)
  641. return
  642. }
  643. }
  644. }
  645. if tencentCaptchaEnabled {
  646. if _, sent := sentFields["tencent_captcha_app_id"]; !sent {
  647. req.TencentCaptchaAppID = previousSettings.TencentCaptchaAppID
  648. }
  649. appID, err := strconv.ParseUint(req.TencentCaptchaAppID, 10, 64)
  650. if err != nil || appID == 0 {
  651. response.BadRequest(c, "Tencent Captcha CaptchaAppId must be a positive integer when enabled")
  652. return
  653. }
  654. if req.TencentCaptchaAppSecretKey == "" {
  655. req.TencentCaptchaAppSecretKey = previousSettings.TencentCaptchaAppSecretKey
  656. }
  657. if req.TencentCaptchaCloudSecretID == "" {
  658. req.TencentCaptchaCloudSecretID = previousSettings.TencentCaptchaCloudSecretID
  659. }
  660. if req.TencentCaptchaCloudSecretKey == "" {
  661. req.TencentCaptchaCloudSecretKey = previousSettings.TencentCaptchaCloudSecretKey
  662. }
  663. if req.TencentCaptchaAppSecretKey == "" {
  664. response.BadRequest(c, "Tencent Captcha AppSecretKey is required when enabled")
  665. return
  666. }
  667. if req.TencentCaptchaCloudSecretID == "" {
  668. response.BadRequest(c, "Tencent Cloud SecretId is required when Tencent Captcha is enabled")
  669. return
  670. }
  671. if req.TencentCaptchaCloudSecretKey == "" {
  672. response.BadRequest(c, "Tencent Cloud SecretKey is required when Tencent Captcha is enabled")
  673. return
  674. }
  675. }
  676. // 阿里云验证码 2.0 参数验证
  677. if aliyunCaptchaEnabled {
  678. if _, sent := sentFields["aliyun_captcha_scene_id"]; !sent {
  679. req.AliyunCaptchaSceneID = previousSettings.AliyunCaptchaSceneID
  680. }
  681. if _, sent := sentFields["aliyun_captcha_prefix"]; !sent {
  682. req.AliyunCaptchaPrefix = previousSettings.AliyunCaptchaPrefix
  683. }
  684. if _, sent := sentFields["aliyun_captcha_access_key_id"]; !sent {
  685. req.AliyunCaptchaAccessKeyID = previousSettings.AliyunCaptchaAccessKeyID
  686. }
  687. if req.AliyunCaptchaSceneID == "" {
  688. response.BadRequest(c, "Aliyun Captcha Scene ID is required when enabled")
  689. return
  690. }
  691. if req.AliyunCaptchaPrefix == "" {
  692. response.BadRequest(c, "Aliyun Captcha Prefix is required when enabled")
  693. return
  694. }
  695. if req.AliyunCaptchaAccessKeyID == "" {
  696. response.BadRequest(c, "Aliyun Captcha AccessKey ID is required when enabled")
  697. return
  698. }
  699. // 如果未提供 AccessKey Secret,使用已保存的值(留空保留当前值)
  700. if req.AliyunCaptchaAccessKeySecret == "" {
  701. if previousSettings.AliyunCaptchaAccessKeySecret == "" {
  702. response.BadRequest(c, "Aliyun Captcha AccessKey Secret is required when enabled")
  703. return
  704. }
  705. req.AliyunCaptchaAccessKeySecret = previousSettings.AliyunCaptchaAccessKeySecret
  706. }
  707. // 凭证任一变化时真实调用一次阿里云校验(避免配置错误导致无法登录)
  708. credentialsChanged := previousSettings.AliyunCaptchaAccessKeyID != req.AliyunCaptchaAccessKeyID ||
  709. previousSettings.AliyunCaptchaAccessKeySecret != req.AliyunCaptchaAccessKeySecret ||
  710. previousSettings.AliyunCaptchaSceneID != req.AliyunCaptchaSceneID ||
  711. previousSettings.AliyunCaptchaRegion != req.AliyunCaptchaRegion
  712. if credentialsChanged {
  713. if err := h.aliyunCaptchaService.ValidateCredentials(c.Request.Context(), req.AliyunCaptchaAccessKeyID, req.AliyunCaptchaAccessKeySecret, req.AliyunCaptchaSceneID, req.AliyunCaptchaRegion); err != nil {
  714. response.ErrorFrom(c, err)
  715. return
  716. }
  717. }
  718. }
  719. // TOTP 双因素认证参数验证
  720. // 只有手动配置了加密密钥才允许启用 TOTP 功能
  721. if req.TotpEnabled && !previousSettings.TotpEnabled {
  722. // 尝试启用 TOTP,检查加密密钥是否已手动配置
  723. if !h.settingService.IsTotpEncryptionKeyConfigured() {
  724. response.BadRequest(c, "Cannot enable TOTP: TOTP_ENCRYPTION_KEY environment variable must be configured first. Generate a key with 'openssl rand -hex 32' and set it in your environment.")
  725. return
  726. }
  727. }
  728. loginAgreementMode := strings.ToLower(strings.TrimSpace(req.LoginAgreementMode))
  729. if loginAgreementMode == "" {
  730. loginAgreementMode = strings.ToLower(strings.TrimSpace(previousSettings.LoginAgreementMode))
  731. }
  732. switch loginAgreementMode {
  733. case "", "modal":
  734. loginAgreementMode = "modal"
  735. case "checkbox":
  736. default:
  737. response.BadRequest(c, "Login agreement mode must be modal or checkbox")
  738. return
  739. }
  740. loginAgreementUpdatedAt := strings.TrimSpace(req.LoginAgreementUpdatedAt)
  741. if loginAgreementUpdatedAt == "" {
  742. loginAgreementUpdatedAt = strings.TrimSpace(previousSettings.LoginAgreementUpdatedAt)
  743. }
  744. loginAgreementDocuments := loginAgreementDocumentsToService(req.LoginAgreementDocuments)
  745. if len(loginAgreementDocuments) == 0 {
  746. loginAgreementDocuments = previousSettings.LoginAgreementDocuments
  747. }
  748. for _, doc := range loginAgreementDocuments {
  749. if strings.TrimSpace(doc.Title) == "" {
  750. response.BadRequest(c, "Login agreement document title is required")
  751. return
  752. }
  753. if len(doc.Title) > 80 {
  754. response.BadRequest(c, "Login agreement document title is too long (max 80 characters)")
  755. return
  756. }
  757. if len(doc.ContentMD) > 200*1024 {
  758. response.BadRequest(c, "Login agreement document content is too large (max 200KB)")
  759. return
  760. }
  761. }
  762. if req.LoginAgreementEnabled && len(loginAgreementDocuments) == 0 {
  763. response.BadRequest(c, "Login agreement documents are required when enabled")
  764. return
  765. }
  766. // LinuxDo Connect 参数验证
  767. if req.LinuxDoConnectEnabled {
  768. req.LinuxDoConnectClientID = strings.TrimSpace(req.LinuxDoConnectClientID)
  769. req.LinuxDoConnectClientSecret = strings.TrimSpace(req.LinuxDoConnectClientSecret)
  770. req.LinuxDoConnectRedirectURL = strings.TrimSpace(req.LinuxDoConnectRedirectURL)
  771. if req.LinuxDoConnectClientID == "" {
  772. response.BadRequest(c, "LinuxDo Client ID is required when enabled")
  773. return
  774. }
  775. if req.LinuxDoConnectRedirectURL == "" {
  776. response.BadRequest(c, "LinuxDo Redirect URL is required when enabled")
  777. return
  778. }
  779. if err := config.ValidateAbsoluteHTTPURL(req.LinuxDoConnectRedirectURL); err != nil {
  780. response.BadRequest(c, "LinuxDo Redirect URL must be an absolute http(s) URL")
  781. return
  782. }
  783. // 如果未提供 client_secret,则保留现有值(如有)。
  784. if req.LinuxDoConnectClientSecret == "" {
  785. if previousSettings.LinuxDoConnectClientSecret == "" {
  786. response.BadRequest(c, "LinuxDo Client Secret is required when enabled")
  787. return
  788. }
  789. req.LinuxDoConnectClientSecret = previousSettings.LinuxDoConnectClientSecret
  790. }
  791. }
  792. // DingTalk Connect 参数验证
  793. // 防御性:任何写入路径上把已废弃的 corp_restriction_policy=whitelist 入参 coerce 为 none,
  794. // 避免任何直连 admin API 的客户端把死值写回 DB(前端 UI 已无此选项)。
  795. req.DingTalkConnectCorpRestrictionPolicy = service.CoerceDingTalkCorpPolicyForWrite(req.DingTalkConnectCorpRestrictionPolicy)
  796. if req.DingTalkConnectEnabled {
  797. req.DingTalkConnectClientID = strings.TrimSpace(req.DingTalkConnectClientID)
  798. req.DingTalkConnectClientSecret = strings.TrimSpace(req.DingTalkConnectClientSecret)
  799. req.DingTalkConnectRedirectURL = strings.TrimSpace(req.DingTalkConnectRedirectURL)
  800. req.DingTalkConnectCorpRestrictionPolicy = strings.TrimSpace(req.DingTalkConnectCorpRestrictionPolicy)
  801. req.DingTalkConnectInternalCorpID = strings.TrimSpace(req.DingTalkConnectInternalCorpID)
  802. if req.DingTalkConnectClientID == "" {
  803. response.BadRequest(c, "DingTalk Client ID is required when enabled")
  804. return
  805. }
  806. if req.DingTalkConnectRedirectURL == "" {
  807. response.BadRequest(c, "DingTalk Redirect URL is required when enabled")
  808. return
  809. }
  810. if err := config.ValidateAbsoluteHTTPURL(req.DingTalkConnectRedirectURL); err != nil {
  811. response.BadRequest(c, "DingTalk Redirect URL must be an absolute http(s) URL")
  812. return
  813. }
  814. // 如果未提供 client_secret,则保留现有值(如有)。
  815. if req.DingTalkConnectClientSecret == "" {
  816. if previousSettings.DingTalkConnectClientSecret == "" {
  817. response.BadRequest(c, "DingTalk Client Secret is required when enabled")
  818. return
  819. }
  820. req.DingTalkConnectClientSecret = previousSettings.DingTalkConnectClientSecret
  821. }
  822. // Corp 策略校验(V1/V4 fail-closed)
  823. dingTalkCfg := config.DingTalkConnectConfig{
  824. Enabled: true,
  825. DingTalkAppKind: "internal_app", // 硬编码:settings 层仅支持 internal_app
  826. AppType: "internal", // 对于 internal_only 策略的默认值
  827. CorpRestrictionPolicy: req.DingTalkConnectCorpRestrictionPolicy,
  828. InternalCorpID: req.DingTalkConnectInternalCorpID,
  829. }
  830. // 若未填 corp_restriction_policy,保留已有配置
  831. if dingTalkCfg.CorpRestrictionPolicy == "" {
  832. dingTalkCfg.CorpRestrictionPolicy = previousSettings.DingTalkConnectCorpRestrictionPolicy
  833. }
  834. // 对于 internal_only 策略,app_type 必须为 internal(V1 校验)
  835. if dingTalkCfg.CorpRestrictionPolicy == "internal_only" {
  836. dingTalkCfg.AppType = "internal"
  837. } else {
  838. dingTalkCfg.AppType = "public"
  839. }
  840. if err := config.ValidateDingTalkConfig(dingTalkCfg); err != nil {
  841. response.ErrorWithDetails(c, http.StatusBadRequest, err.Error(), mapDingTalkValidateError(err), nil)
  842. return
  843. }
  844. // bypass_registration 仅在 internal_only 模式下有意义;其它策略下强制为 false,
  845. // 防止 admin 在切换 policy 时把 bypass 残留在 DB 中(前端 UI 也已隐藏该开关)。
  846. if dingTalkCfg.CorpRestrictionPolicy != "internal_only" {
  847. req.DingTalkConnectBypassRegistration = false
  848. // 身份同步三开关同理:仅 internal_only 模式下有意义,其它策略强制 false。
  849. req.DingTalkConnectSyncCorpEmail = false
  850. req.DingTalkConnectSyncDisplayName = false
  851. req.DingTalkConnectSyncDept = false
  852. }
  853. // 身份同步目标 attr key:trimSpace + 空值 fallback 到默认值
  854. req.DingTalkConnectSyncCorpEmailAttrKey = strings.TrimSpace(req.DingTalkConnectSyncCorpEmailAttrKey)
  855. if req.DingTalkConnectSyncCorpEmailAttrKey == "" {
  856. req.DingTalkConnectSyncCorpEmailAttrKey = "dingtalk_email"
  857. }
  858. req.DingTalkConnectSyncDisplayNameAttrKey = strings.TrimSpace(req.DingTalkConnectSyncDisplayNameAttrKey)
  859. if req.DingTalkConnectSyncDisplayNameAttrKey == "" {
  860. req.DingTalkConnectSyncDisplayNameAttrKey = "dingtalk_name"
  861. }
  862. req.DingTalkConnectSyncDeptAttrKey = strings.TrimSpace(req.DingTalkConnectSyncDeptAttrKey)
  863. if req.DingTalkConnectSyncDeptAttrKey == "" {
  864. req.DingTalkConnectSyncDeptAttrKey = "dingtalk_department"
  865. }
  866. // 身份同步目标 attr 显示名称:trim + 空值 fallback 到默认中文名
  867. req.DingTalkConnectSyncCorpEmailAttrName = strings.TrimSpace(req.DingTalkConnectSyncCorpEmailAttrName)
  868. if req.DingTalkConnectSyncCorpEmailAttrName == "" {
  869. req.DingTalkConnectSyncCorpEmailAttrName = "钉钉企业邮箱"
  870. }
  871. req.DingTalkConnectSyncDisplayNameAttrName = strings.TrimSpace(req.DingTalkConnectSyncDisplayNameAttrName)
  872. if req.DingTalkConnectSyncDisplayNameAttrName == "" {
  873. req.DingTalkConnectSyncDisplayNameAttrName = "钉钉姓名"
  874. }
  875. req.DingTalkConnectSyncDeptAttrName = strings.TrimSpace(req.DingTalkConnectSyncDeptAttrName)
  876. if req.DingTalkConnectSyncDeptAttrName == "" {
  877. req.DingTalkConnectSyncDeptAttrName = "钉钉部门"
  878. }
  879. }
  880. if req.WeChatConnectEnabled {
  881. req.WeChatConnectAppID = strings.TrimSpace(req.WeChatConnectAppID)
  882. req.WeChatConnectAppSecret = strings.TrimSpace(req.WeChatConnectAppSecret)
  883. req.WeChatConnectOpenAppID = strings.TrimSpace(req.WeChatConnectOpenAppID)
  884. req.WeChatConnectOpenAppSecret = strings.TrimSpace(req.WeChatConnectOpenAppSecret)
  885. req.WeChatConnectMPAppID = strings.TrimSpace(req.WeChatConnectMPAppID)
  886. req.WeChatConnectMPAppSecret = strings.TrimSpace(req.WeChatConnectMPAppSecret)
  887. req.WeChatConnectMobileAppID = strings.TrimSpace(req.WeChatConnectMobileAppID)
  888. req.WeChatConnectMobileAppSecret = strings.TrimSpace(req.WeChatConnectMobileAppSecret)
  889. req.WeChatConnectMode = strings.ToLower(strings.TrimSpace(req.WeChatConnectMode))
  890. req.WeChatConnectScopes = strings.TrimSpace(req.WeChatConnectScopes)
  891. req.WeChatConnectRedirectURL = strings.TrimSpace(req.WeChatConnectRedirectURL)
  892. req.WeChatConnectFrontendRedirectURL = strings.TrimSpace(req.WeChatConnectFrontendRedirectURL)
  893. req.WeChatConnectAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectAppID, previousSettings.WeChatConnectAppID))
  894. req.WeChatConnectRedirectURL = strings.TrimSpace(firstNonEmpty(req.WeChatConnectRedirectURL, previousSettings.WeChatConnectRedirectURL))
  895. req.WeChatConnectFrontendRedirectURL = strings.TrimSpace(firstNonEmpty(req.WeChatConnectFrontendRedirectURL, previousSettings.WeChatConnectFrontendRedirectURL))
  896. if req.WeChatConnectMode == "" {
  897. req.WeChatConnectMode = strings.ToLower(strings.TrimSpace(previousSettings.WeChatConnectMode))
  898. }
  899. if req.WeChatConnectScopes == "" {
  900. req.WeChatConnectScopes = strings.TrimSpace(previousSettings.WeChatConnectScopes)
  901. }
  902. if req.WeChatConnectMPEnabled && req.WeChatConnectMobileEnabled {
  903. response.BadRequest(c, "WeChat Official Account and Mobile App cannot be enabled at the same time")
  904. return
  905. }
  906. if req.WeChatConnectMode != "" {
  907. switch req.WeChatConnectMode {
  908. case "open", "mp", "mobile":
  909. default:
  910. response.BadRequest(c, "WeChat mode must be open, mp, or mobile")
  911. return
  912. }
  913. }
  914. if !req.WeChatConnectOpenEnabled && !req.WeChatConnectMPEnabled && !req.WeChatConnectMobileEnabled {
  915. switch req.WeChatConnectMode {
  916. case "mp":
  917. req.WeChatConnectMPEnabled = true
  918. case "mobile":
  919. req.WeChatConnectMobileEnabled = true
  920. default:
  921. req.WeChatConnectOpenEnabled = true
  922. }
  923. }
  924. if req.WeChatConnectMode == "" {
  925. if req.WeChatConnectMPEnabled {
  926. req.WeChatConnectMode = "mp"
  927. } else if req.WeChatConnectMobileEnabled {
  928. req.WeChatConnectMode = "mobile"
  929. } else {
  930. req.WeChatConnectMode = "open"
  931. }
  932. }
  933. req.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectOpenAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectOpenAppID, previousSettings.WeChatConnectAppID))
  934. req.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectMPAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectMPAppID, previousSettings.WeChatConnectAppID))
  935. req.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(req.WeChatConnectMobileAppID, req.WeChatConnectAppID, previousSettings.WeChatConnectMobileAppID, previousSettings.WeChatConnectAppID))
  936. if req.WeChatConnectOpenAppSecret == "" {
  937. req.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectOpenAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret))
  938. }
  939. if req.WeChatConnectMPAppSecret == "" {
  940. req.WeChatConnectMPAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectMPAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret))
  941. }
  942. if req.WeChatConnectMobileAppSecret == "" {
  943. req.WeChatConnectMobileAppSecret = strings.TrimSpace(firstNonEmpty(previousSettings.WeChatConnectMobileAppSecret, previousSettings.WeChatConnectAppSecret, req.WeChatConnectAppSecret))
  944. }
  945. if req.WeChatConnectAppSecret == "" {
  946. req.WeChatConnectAppSecret = strings.TrimSpace(firstNonEmpty(req.WeChatConnectOpenAppSecret, req.WeChatConnectMPAppSecret, req.WeChatConnectMobileAppSecret, previousSettings.WeChatConnectAppSecret))
  947. }
  948. if req.WeChatConnectOpenEnabled {
  949. if req.WeChatConnectOpenAppID == "" {
  950. response.BadRequest(c, "WeChat PC App ID is required when enabled")
  951. return
  952. }
  953. if req.WeChatConnectOpenAppSecret == "" {
  954. response.BadRequest(c, "WeChat PC App Secret is required when enabled")
  955. return
  956. }
  957. }
  958. if req.WeChatConnectMPEnabled {
  959. if req.WeChatConnectMPAppID == "" {
  960. response.BadRequest(c, "WeChat Official Account App ID is required when enabled")
  961. return
  962. }
  963. if req.WeChatConnectMPAppSecret == "" {
  964. response.BadRequest(c, "WeChat Official Account App Secret is required when enabled")
  965. return
  966. }
  967. }
  968. if req.WeChatConnectMobileEnabled {
  969. if req.WeChatConnectMobileAppID == "" {
  970. response.BadRequest(c, "WeChat Mobile App ID is required when enabled")
  971. return
  972. }
  973. if req.WeChatConnectMobileAppSecret == "" {
  974. response.BadRequest(c, "WeChat Mobile App Secret is required when enabled")
  975. return
  976. }
  977. }
  978. if req.WeChatConnectScopes == "" {
  979. if req.WeChatConnectMPEnabled {
  980. req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode("mp")
  981. } else {
  982. req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode(req.WeChatConnectMode)
  983. }
  984. }
  985. if req.WeChatConnectOpenEnabled || req.WeChatConnectMPEnabled {
  986. if req.WeChatConnectRedirectURL == "" {
  987. response.BadRequest(c, "WeChat Redirect URL is required when web oauth is enabled")
  988. return
  989. }
  990. if err := config.ValidateAbsoluteHTTPURL(req.WeChatConnectRedirectURL); err != nil {
  991. response.BadRequest(c, "WeChat Redirect URL must be an absolute http(s) URL")
  992. return
  993. }
  994. if req.WeChatConnectFrontendRedirectURL == "" {
  995. req.WeChatConnectFrontendRedirectURL = "/auth/wechat/callback"
  996. }
  997. if err := config.ValidateFrontendRedirectURL(req.WeChatConnectFrontendRedirectURL); err != nil {
  998. response.BadRequest(c, "WeChat Frontend Redirect URL is invalid")
  999. return
  1000. }
  1001. }
  1002. }
  1003. // Generic OIDC 参数验证
  1004. oidcUsePKCE, oidcValidateIDToken, err := h.settingService.OIDCSecurityWriteDefaults(c.Request.Context())
  1005. if err != nil {
  1006. response.ErrorFrom(c, err)
  1007. return
  1008. }
  1009. if req.OIDCConnectEnabled {
  1010. req.OIDCConnectProviderName = strings.TrimSpace(req.OIDCConnectProviderName)
  1011. req.OIDCConnectClientID = strings.TrimSpace(req.OIDCConnectClientID)
  1012. req.OIDCConnectClientSecret = strings.TrimSpace(req.OIDCConnectClientSecret)
  1013. req.OIDCConnectIssuerURL = strings.TrimSpace(req.OIDCConnectIssuerURL)
  1014. req.OIDCConnectDiscoveryURL = strings.TrimSpace(req.OIDCConnectDiscoveryURL)
  1015. req.OIDCConnectAuthorizeURL = strings.TrimSpace(req.OIDCConnectAuthorizeURL)
  1016. req.OIDCConnectTokenURL = strings.TrimSpace(req.OIDCConnectTokenURL)
  1017. req.OIDCConnectUserInfoURL = strings.TrimSpace(req.OIDCConnectUserInfoURL)
  1018. req.OIDCConnectJWKSURL = strings.TrimSpace(req.OIDCConnectJWKSURL)
  1019. req.OIDCConnectScopes = strings.TrimSpace(req.OIDCConnectScopes)
  1020. req.OIDCConnectRedirectURL = strings.TrimSpace(req.OIDCConnectRedirectURL)
  1021. req.OIDCConnectFrontendRedirectURL = strings.TrimSpace(req.OIDCConnectFrontendRedirectURL)
  1022. req.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(req.OIDCConnectTokenAuthMethod))
  1023. req.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(req.OIDCConnectAllowedSigningAlgs)
  1024. req.OIDCConnectUserInfoEmailPath = strings.TrimSpace(req.OIDCConnectUserInfoEmailPath)
  1025. req.OIDCConnectUserInfoIDPath = strings.TrimSpace(req.OIDCConnectUserInfoIDPath)
  1026. req.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(req.OIDCConnectUserInfoUsernamePath)
  1027. req.OIDCConnectProviderName = strings.TrimSpace(firstNonEmpty(req.OIDCConnectProviderName, previousSettings.OIDCConnectProviderName, "OIDC"))
  1028. req.OIDCConnectClientID = strings.TrimSpace(firstNonEmpty(req.OIDCConnectClientID, previousSettings.OIDCConnectClientID))
  1029. req.OIDCConnectIssuerURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectIssuerURL, previousSettings.OIDCConnectIssuerURL))
  1030. req.OIDCConnectDiscoveryURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectDiscoveryURL, previousSettings.OIDCConnectDiscoveryURL))
  1031. req.OIDCConnectAuthorizeURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectAuthorizeURL, previousSettings.OIDCConnectAuthorizeURL))
  1032. req.OIDCConnectTokenURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectTokenURL, previousSettings.OIDCConnectTokenURL))
  1033. req.OIDCConnectUserInfoURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoURL, previousSettings.OIDCConnectUserInfoURL))
  1034. req.OIDCConnectJWKSURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectJWKSURL, previousSettings.OIDCConnectJWKSURL))
  1035. req.OIDCConnectScopes = strings.TrimSpace(firstNonEmpty(req.OIDCConnectScopes, previousSettings.OIDCConnectScopes, "openid email profile"))
  1036. req.OIDCConnectRedirectURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectRedirectURL, previousSettings.OIDCConnectRedirectURL))
  1037. req.OIDCConnectFrontendRedirectURL = strings.TrimSpace(firstNonEmpty(req.OIDCConnectFrontendRedirectURL, previousSettings.OIDCConnectFrontendRedirectURL, "/auth/oidc/callback"))
  1038. req.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(firstNonEmpty(req.OIDCConnectTokenAuthMethod, previousSettings.OIDCConnectTokenAuthMethod, "client_secret_post")))
  1039. req.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(firstNonEmpty(req.OIDCConnectAllowedSigningAlgs, previousSettings.OIDCConnectAllowedSigningAlgs, "RS256,ES256,PS256"))
  1040. req.OIDCConnectUserInfoEmailPath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoEmailPath, previousSettings.OIDCConnectUserInfoEmailPath))
  1041. req.OIDCConnectUserInfoIDPath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoIDPath, previousSettings.OIDCConnectUserInfoIDPath))
  1042. req.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(firstNonEmpty(req.OIDCConnectUserInfoUsernamePath, previousSettings.OIDCConnectUserInfoUsernamePath))
  1043. if req.OIDCConnectUsePKCE != nil {
  1044. oidcUsePKCE = *req.OIDCConnectUsePKCE
  1045. }
  1046. if req.OIDCConnectValidateIDToken != nil {
  1047. oidcValidateIDToken = *req.OIDCConnectValidateIDToken
  1048. }
  1049. if req.OIDCConnectClockSkewSeconds == 0 {
  1050. req.OIDCConnectClockSkewSeconds = previousSettings.OIDCConnectClockSkewSeconds
  1051. if req.OIDCConnectClockSkewSeconds == 0 {
  1052. req.OIDCConnectClockSkewSeconds = 120
  1053. }
  1054. }
  1055. if req.OIDCConnectClientID == "" {
  1056. response.BadRequest(c, "OIDC Client ID is required when enabled")
  1057. return
  1058. }
  1059. if req.OIDCConnectIssuerURL == "" {
  1060. response.BadRequest(c, "OIDC Issuer URL is required when enabled")
  1061. return
  1062. }
  1063. if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectIssuerURL); err != nil {
  1064. response.BadRequest(c, "OIDC Issuer URL must be an absolute http(s) URL")
  1065. return
  1066. }
  1067. if req.OIDCConnectDiscoveryURL != "" {
  1068. if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectDiscoveryURL); err != nil {
  1069. response.BadRequest(c, "OIDC Discovery URL must be an absolute http(s) URL")
  1070. return
  1071. }
  1072. }
  1073. if req.OIDCConnectAuthorizeURL != "" {
  1074. if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectAuthorizeURL); err != nil {
  1075. response.BadRequest(c, "OIDC Authorize URL must be an absolute http(s) URL")
  1076. return
  1077. }
  1078. }
  1079. if req.OIDCConnectTokenURL != "" {
  1080. if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectTokenURL); err != nil {
  1081. response.BadRequest(c, "OIDC Token URL must be an absolute http(s) URL")
  1082. return
  1083. }
  1084. }
  1085. if req.OIDCConnectUserInfoURL != "" {
  1086. if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectUserInfoURL); err != nil {
  1087. response.BadRequest(c, "OIDC UserInfo URL must be an absolute http(s) URL")
  1088. return
  1089. }
  1090. }
  1091. if req.OIDCConnectRedirectURL == "" {
  1092. response.BadRequest(c, "OIDC Redirect URL is required when enabled")
  1093. return
  1094. }
  1095. if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectRedirectURL); err != nil {
  1096. response.BadRequest(c, "OIDC Redirect URL must be an absolute http(s) URL")
  1097. return
  1098. }
  1099. if req.OIDCConnectFrontendRedirectURL == "" {
  1100. response.BadRequest(c, "OIDC Frontend Redirect URL is required when enabled")
  1101. return
  1102. }
  1103. if err := config.ValidateFrontendRedirectURL(req.OIDCConnectFrontendRedirectURL); err != nil {
  1104. response.BadRequest(c, "OIDC Frontend Redirect URL is invalid")
  1105. return
  1106. }
  1107. if !scopesContainOpenID(req.OIDCConnectScopes) {
  1108. response.BadRequest(c, "OIDC scopes must contain openid")
  1109. return
  1110. }
  1111. switch req.OIDCConnectTokenAuthMethod {
  1112. case "", "client_secret_post", "client_secret_basic", "none":
  1113. default:
  1114. response.BadRequest(c, "OIDC Token Auth Method must be one of client_secret_post/client_secret_basic/none")
  1115. return
  1116. }
  1117. if req.OIDCConnectClockSkewSeconds < 0 || req.OIDCConnectClockSkewSeconds > 600 {
  1118. response.BadRequest(c, "OIDC clock skew seconds must be between 0 and 600")
  1119. return
  1120. }
  1121. if oidcValidateIDToken && req.OIDCConnectAllowedSigningAlgs == "" {
  1122. response.BadRequest(c, "OIDC Allowed Signing Algs is required when validate_id_token=true")
  1123. return
  1124. }
  1125. if req.OIDCConnectJWKSURL != "" {
  1126. if err := config.ValidateAbsoluteHTTPURL(req.OIDCConnectJWKSURL); err != nil {
  1127. response.BadRequest(c, "OIDC JWKS URL must be an absolute http(s) URL")
  1128. return
  1129. }
  1130. }
  1131. if req.OIDCConnectTokenAuthMethod == "" || req.OIDCConnectTokenAuthMethod == "client_secret_post" || req.OIDCConnectTokenAuthMethod == "client_secret_basic" {
  1132. if req.OIDCConnectClientSecret == "" {
  1133. if previousSettings.OIDCConnectClientSecret == "" {
  1134. response.BadRequest(c, "OIDC Client Secret is required when enabled")
  1135. return
  1136. }
  1137. req.OIDCConnectClientSecret = previousSettings.OIDCConnectClientSecret
  1138. }
  1139. }
  1140. }
  1141. // “购买订阅”页面配置验证
  1142. purchaseEnabled := previousSettings.PurchaseSubscriptionEnabled
  1143. if req.PurchaseSubscriptionEnabled != nil {
  1144. purchaseEnabled = *req.PurchaseSubscriptionEnabled
  1145. }
  1146. purchaseURL := previousSettings.PurchaseSubscriptionURL
  1147. if req.PurchaseSubscriptionURL != nil {
  1148. purchaseURL = strings.TrimSpace(*req.PurchaseSubscriptionURL)
  1149. }
  1150. // - 启用时要求 URL 合法且非空
  1151. // - 禁用时允许为空;若提供了 URL 也做基本校验,避免误配置
  1152. if purchaseEnabled {
  1153. if purchaseURL == "" {
  1154. response.BadRequest(c, "Purchase Subscription URL is required when enabled")
  1155. return
  1156. }
  1157. if err := config.ValidateAbsoluteHTTPURL(purchaseURL); err != nil {
  1158. response.BadRequest(c, "Purchase Subscription URL must be an absolute http(s) URL")
  1159. return
  1160. }
  1161. } else if purchaseURL != "" {
  1162. if err := config.ValidateAbsoluteHTTPURL(purchaseURL); err != nil {
  1163. response.BadRequest(c, "Purchase Subscription URL must be an absolute http(s) URL")
  1164. return
  1165. }
  1166. }
  1167. // Frontend URL 验证
  1168. req.FrontendURL = strings.TrimSpace(req.FrontendURL)
  1169. if req.FrontendURL != "" {
  1170. if err := config.ValidateAbsoluteHTTPURL(req.FrontendURL); err != nil {
  1171. response.BadRequest(c, "Frontend URL must be an absolute http(s) URL")
  1172. return
  1173. }
  1174. }
  1175. // 自定义菜单项验证
  1176. const (
  1177. maxCustomMenuItems = 20
  1178. maxMenuItemLabelLen = 50
  1179. maxMenuItemURLLen = 2048
  1180. maxMenuItemIconSVGLen = 10 * 1024 // 10KB
  1181. maxMenuItemIDLen = 32
  1182. )
  1183. customMenuJSON := previousSettings.CustomMenuItems
  1184. if req.CustomMenuItems != nil {
  1185. items := *req.CustomMenuItems
  1186. if len(items) > maxCustomMenuItems {
  1187. response.BadRequest(c, "Too many custom menu items (max 20)")
  1188. return
  1189. }
  1190. for i, item := range items {
  1191. if strings.TrimSpace(item.Label) == "" {
  1192. response.BadRequest(c, "Custom menu item label is required")
  1193. return
  1194. }
  1195. if len(item.Label) > maxMenuItemLabelLen {
  1196. response.BadRequest(c, "Custom menu item label is too long (max 50 characters)")
  1197. return
  1198. }
  1199. urlTrimmed := strings.TrimSpace(item.URL)
  1200. if strings.HasPrefix(urlTrimmed, "md:") {
  1201. // Markdown page mode: URL = "md:<slug>"
  1202. slug := strings.TrimPrefix(urlTrimmed, "md:")
  1203. if slug == "" {
  1204. response.BadRequest(c, "Custom menu item markdown slug cannot be empty (use md:slug format)")
  1205. return
  1206. }
  1207. } else {
  1208. if urlTrimmed == "" {
  1209. response.BadRequest(c, "Custom menu item URL is required (use md:slug for markdown pages)")
  1210. return
  1211. }
  1212. if len(item.URL) > maxMenuItemURLLen {
  1213. response.BadRequest(c, "Custom menu item URL is too long (max 2048 characters)")
  1214. return
  1215. }
  1216. if err := config.ValidateAbsoluteHTTPURL(urlTrimmed); err != nil {
  1217. response.BadRequest(c, "Custom menu item URL must be an absolute http(s) URL or md:<slug>")
  1218. return
  1219. }
  1220. }
  1221. if item.Visibility != "user" && item.Visibility != "admin" {
  1222. response.BadRequest(c, "Custom menu item visibility must be 'user' or 'admin'")
  1223. return
  1224. }
  1225. if len(item.IconSVG) > maxMenuItemIconSVGLen {
  1226. response.BadRequest(c, "Custom menu item icon SVG is too large (max 10KB)")
  1227. return
  1228. }
  1229. // Auto-generate ID if missing
  1230. if strings.TrimSpace(item.ID) == "" {
  1231. id, err := generateMenuItemID()
  1232. if err != nil {
  1233. response.Error(c, http.StatusInternalServerError, "Failed to generate menu item ID")
  1234. return
  1235. }
  1236. items[i].ID = id
  1237. } else if len(item.ID) > maxMenuItemIDLen {
  1238. response.BadRequest(c, "Custom menu item ID is too long (max 32 characters)")
  1239. return
  1240. } else if !menuItemIDPattern.MatchString(item.ID) {
  1241. response.BadRequest(c, "Custom menu item ID contains invalid characters (only a-z, A-Z, 0-9, - and _ are allowed)")
  1242. return
  1243. }
  1244. }
  1245. // ID uniqueness check
  1246. seen := make(map[string]struct{}, len(items))
  1247. for _, item := range items {
  1248. if _, exists := seen[item.ID]; exists {
  1249. response.BadRequest(c, "Duplicate custom menu item ID: "+item.ID)
  1250. return
  1251. }
  1252. seen[item.ID] = struct{}{}
  1253. }
  1254. menuBytes, err := json.Marshal(items)
  1255. if err != nil {
  1256. response.BadRequest(c, "Failed to serialize custom menu items")
  1257. return
  1258. }
  1259. customMenuJSON = string(menuBytes)
  1260. }
  1261. // 自定义端点验证
  1262. const (
  1263. maxCustomEndpoints = 10
  1264. maxEndpointNameLen = 50
  1265. maxEndpointURLLen = 2048
  1266. maxEndpointDescriptionLen = 200
  1267. )
  1268. customEndpointsJSON := previousSettings.CustomEndpoints
  1269. if req.CustomEndpoints != nil {
  1270. endpoints := *req.CustomEndpoints
  1271. if len(endpoints) > maxCustomEndpoints {
  1272. response.BadRequest(c, "Too many custom endpoints (max 10)")
  1273. return
  1274. }
  1275. for _, ep := range endpoints {
  1276. if strings.TrimSpace(ep.Name) == "" {
  1277. response.BadRequest(c, "Custom endpoint name is required")
  1278. return
  1279. }
  1280. if len(ep.Name) > maxEndpointNameLen {
  1281. response.BadRequest(c, "Custom endpoint name is too long (max 50 characters)")
  1282. return
  1283. }
  1284. if strings.TrimSpace(ep.Endpoint) == "" {
  1285. response.BadRequest(c, "Custom endpoint URL is required")
  1286. return
  1287. }
  1288. if len(ep.Endpoint) > maxEndpointURLLen {
  1289. response.BadRequest(c, "Custom endpoint URL is too long (max 2048 characters)")
  1290. return
  1291. }
  1292. if err := config.ValidateAbsoluteHTTPURL(strings.TrimSpace(ep.Endpoint)); err != nil {
  1293. response.BadRequest(c, "Custom endpoint URL must be an absolute http(s) URL")
  1294. return
  1295. }
  1296. if len(ep.Description) > maxEndpointDescriptionLen {
  1297. response.BadRequest(c, "Custom endpoint description is too long (max 200 characters)")
  1298. return
  1299. }
  1300. }
  1301. endpointBytes, err := json.Marshal(endpoints)
  1302. if err != nil {
  1303. response.BadRequest(c, "Failed to serialize custom endpoints")
  1304. return
  1305. }
  1306. customEndpointsJSON = string(endpointBytes)
  1307. }
  1308. // Ops metrics collector interval validation (seconds).
  1309. if req.OpsMetricsIntervalSeconds != nil {
  1310. v := *req.OpsMetricsIntervalSeconds
  1311. if v < 60 {
  1312. v = 60
  1313. }
  1314. if v > 3600 {
  1315. v = 3600
  1316. }
  1317. req.OpsMetricsIntervalSeconds = &v
  1318. }
  1319. defaultSubscriptions := make([]service.DefaultSubscriptionSetting, 0, len(req.DefaultSubscriptions))
  1320. for _, sub := range req.DefaultSubscriptions {
  1321. defaultSubscriptions = append(defaultSubscriptions, service.DefaultSubscriptionSetting{
  1322. GroupID: sub.GroupID,
  1323. ValidityDays: sub.ValidityDays,
  1324. })
  1325. }
  1326. // 验证最低版本号格式(空字符串=禁用,或合法 semver)
  1327. if req.MinClaudeCodeVersion != "" {
  1328. if !semverPattern.MatchString(req.MinClaudeCodeVersion) {
  1329. response.Error(c, http.StatusBadRequest, "min_claude_code_version must be empty or a valid semver (e.g. 2.1.63)")
  1330. return
  1331. }
  1332. }
  1333. // 验证最高版本号格式(空字符串=禁用,或合法 semver)
  1334. if req.MaxClaudeCodeVersion != "" {
  1335. if !semverPattern.MatchString(req.MaxClaudeCodeVersion) {
  1336. response.Error(c, http.StatusBadRequest, "max_claude_code_version must be empty or a valid semver (e.g. 3.0.0)")
  1337. return
  1338. }
  1339. }
  1340. if req.AntigravityUserAgentVersion != nil {
  1341. normalized := strings.TrimSpace(*req.AntigravityUserAgentVersion)
  1342. req.AntigravityUserAgentVersion = &normalized
  1343. if normalized != "" && !semverPattern.MatchString(normalized) {
  1344. response.Error(c, http.StatusBadRequest, "antigravity_user_agent_version must be empty or a valid semver (e.g. 1.23.2)")
  1345. return
  1346. }
  1347. }
  1348. if req.OpenAICodexUserAgent != nil {
  1349. normalized := strings.TrimSpace(*req.OpenAICodexUserAgent)
  1350. req.OpenAICodexUserAgent = &normalized
  1351. // 仅做长度上限保护,不限制具体格式(运维需要可自由调整 codex 版本号)
  1352. if len(normalized) > 512 {
  1353. response.Error(c, http.StatusBadRequest, "openai_codex_user_agent must be at most 512 characters")
  1354. return
  1355. }
  1356. }
  1357. if req.OpenAICodexClientVersion != nil {
  1358. // 该值会被拼进出站 User-Agent 与 version 头,必须是合法版本号;空串表示跟随自动同步。
  1359. normalized := strings.TrimSpace(*req.OpenAICodexClientVersion)
  1360. if normalized != "" && service.NormalizeCodexClientVersion(normalized) == "" {
  1361. response.Error(c, http.StatusBadRequest, "openai_codex_client_version must be empty or a valid version (e.g. 0.146.0)")
  1362. return
  1363. }
  1364. req.OpenAICodexClientVersion = &normalized
  1365. }
  1366. // codex_cli_only 加固:最低/最高 Codex 版本(空=禁用,或合法 semver;max>=min)
  1367. if req.MinCodexVersion != "" && !semverPattern.MatchString(req.MinCodexVersion) {
  1368. response.Error(c, http.StatusBadRequest, "min_codex_version must be empty or a valid semver (e.g. 0.141.0)")
  1369. return
  1370. }
  1371. if req.MaxCodexVersion != "" && !semverPattern.MatchString(req.MaxCodexVersion) {
  1372. response.Error(c, http.StatusBadRequest, "max_codex_version must be empty or a valid semver (e.g. 0.200.0)")
  1373. return
  1374. }
  1375. if req.MinCodexVersion != "" && req.MaxCodexVersion != "" && service.CompareVersions(req.MaxCodexVersion, req.MinCodexVersion) < 0 {
  1376. response.Error(c, http.StatusBadRequest, "max_codex_version must be greater than or equal to min_codex_version")
  1377. return
  1378. }
  1379. // codex_cli_only 黑/白名单:非空须为合法 []AllowedClientEntry JSON。
  1380. // 黑名单 OR 宽 deny(允许 originator-only);白名单双因子 AND,额外要求每条可命中(非空 originator + ua_contains)。
  1381. if err := service.ValidateCodexClientEntriesJSON(req.CodexCLIOnlyBlacklist); err != nil {
  1382. response.Error(c, http.StatusBadRequest, "codex_cli_only_blacklist "+err.Error())
  1383. return
  1384. }
  1385. if err := service.ValidateCodexWhitelistEntriesJSON(req.CodexCLIOnlyWhitelist); err != nil {
  1386. response.Error(c, http.StatusBadRequest, "codex_cli_only_whitelist "+err.Error())
  1387. return
  1388. }
  1389. if err := service.ValidateEngineFingerprintSignalsJSON(req.CodexCLIOnlyEngineFingerprintSignals); err != nil {
  1390. response.Error(c, http.StatusBadRequest, "codex_cli_only_engine_fingerprint_signals "+err.Error())
  1391. return
  1392. }
  1393. // 交叉验证:如果同时设置了最低和最高版本号,最高版本号必须 >= 最低版本号
  1394. if req.MinClaudeCodeVersion != "" && req.MaxClaudeCodeVersion != "" {
  1395. if service.CompareVersions(req.MaxClaudeCodeVersion, req.MinClaudeCodeVersion) < 0 {
  1396. response.Error(c, http.StatusBadRequest, "max_claude_code_version must be greater than or equal to min_claude_code_version")
  1397. return
  1398. }
  1399. }
  1400. // cyber 会话屏蔽 TTL 校验:提供时必须 > 0
  1401. if req.CyberSessionBlockTTLSeconds != nil && *req.CyberSessionBlockTTLSeconds <= 0 {
  1402. response.BadRequest(c, "cyber_session_block_ttl_seconds must be > 0")
  1403. return
  1404. }
  1405. settings := &service.SystemSettings{
  1406. // 系统全局 platform quota 默认值(整体替换语义)
  1407. DefaultPlatformQuotas: req.DefaultPlatformQuotas,
  1408. AccountSchedulingThresholds: req.AccountSchedulingThresholds,
  1409. RegistrationEnabled: req.RegistrationEnabled,
  1410. EmailVerifyEnabled: req.EmailVerifyEnabled,
  1411. RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist,
  1412. RegistrationEmailDomainQuotaEnabled: registrationEmailDomainQuotaEnabled,
  1413. PromoCodeEnabled: req.PromoCodeEnabled,
  1414. PasswordResetEnabled: req.PasswordResetEnabled,
  1415. FrontendURL: req.FrontendURL,
  1416. InvitationCodeEnabled: req.InvitationCodeEnabled,
  1417. TotpEnabled: req.TotpEnabled,
  1418. PasskeyEnabled: passkeyEnabled,
  1419. SessionBindingEnabled: sessionBindingEnabled,
  1420. StepUpEnabled: stepUpEnabled,
  1421. AuditLogRetentionDays: req.AuditLogRetentionDays,
  1422. LoginAgreementEnabled: req.LoginAgreementEnabled,
  1423. LoginAgreementMode: loginAgreementMode,
  1424. LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
  1425. LoginAgreementDocuments: loginAgreementDocuments,
  1426. SMTPHost: req.SMTPHost,
  1427. SMTPPort: req.SMTPPort,
  1428. SMTPUsername: req.SMTPUsername,
  1429. SMTPPassword: req.SMTPPassword,
  1430. SMTPFrom: req.SMTPFrom,
  1431. SMTPFromName: req.SMTPFromName,
  1432. SMTPUseTLS: req.SMTPUseTLS,
  1433. TurnstileEnabled: req.TurnstileEnabled,
  1434. TurnstileSiteKey: req.TurnstileSiteKey,
  1435. TurnstileSecretKey: req.TurnstileSecretKey,
  1436. TencentCaptchaEnabled: req.TencentCaptchaEnabled,
  1437. TencentCaptchaAppID: req.TencentCaptchaAppID,
  1438. TencentCaptchaAppSecretKey: req.TencentCaptchaAppSecretKey,
  1439. TencentCaptchaCloudSecretID: req.TencentCaptchaCloudSecretID,
  1440. TencentCaptchaCloudSecretKey: req.TencentCaptchaCloudSecretKey,
  1441. TencentCaptchaRegion: req.TencentCaptchaRegion,
  1442. AliyunCaptchaEnabled: req.AliyunCaptchaEnabled,
  1443. AliyunCaptchaAccessKeyID: req.AliyunCaptchaAccessKeyID,
  1444. AliyunCaptchaAccessKeySecret: req.AliyunCaptchaAccessKeySecret,
  1445. AliyunCaptchaSceneID: req.AliyunCaptchaSceneID,
  1446. AliyunCaptchaPrefix: req.AliyunCaptchaPrefix,
  1447. AliyunCaptchaRegion: req.AliyunCaptchaRegion,
  1448. APIKeyACLTrustForwardedIP: func() bool {
  1449. if req.APIKeyACLTrustForwardedIP != nil {
  1450. return *req.APIKeyACLTrustForwardedIP
  1451. }
  1452. return previousSettings.APIKeyACLTrustForwardedIP
  1453. }(),
  1454. ForwardedClientIPHeaders: forwardedClientIPHeaders,
  1455. LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
  1456. LinuxDoConnectClientID: req.LinuxDoConnectClientID,
  1457. LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
  1458. LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
  1459. DingTalkConnectEnabled: req.DingTalkConnectEnabled,
  1460. DingTalkConnectClientID: req.DingTalkConnectClientID,
  1461. DingTalkConnectClientSecret: req.DingTalkConnectClientSecret,
  1462. DingTalkConnectRedirectURL: req.DingTalkConnectRedirectURL,
  1463. DingTalkConnectCorpRestrictionPolicy: req.DingTalkConnectCorpRestrictionPolicy,
  1464. DingTalkConnectInternalCorpID: req.DingTalkConnectInternalCorpID,
  1465. DingTalkConnectBypassRegistration: req.DingTalkConnectBypassRegistration,
  1466. DingTalkConnectSyncCorpEmail: req.DingTalkConnectSyncCorpEmail,
  1467. DingTalkConnectSyncDisplayName: req.DingTalkConnectSyncDisplayName,
  1468. DingTalkConnectSyncDept: req.DingTalkConnectSyncDept,
  1469. DingTalkConnectSyncCorpEmailAttrKey: req.DingTalkConnectSyncCorpEmailAttrKey,
  1470. DingTalkConnectSyncDisplayNameAttrKey: req.DingTalkConnectSyncDisplayNameAttrKey,
  1471. DingTalkConnectSyncDeptAttrKey: req.DingTalkConnectSyncDeptAttrKey,
  1472. DingTalkConnectSyncCorpEmailAttrName: req.DingTalkConnectSyncCorpEmailAttrName,
  1473. DingTalkConnectSyncDisplayNameAttrName: req.DingTalkConnectSyncDisplayNameAttrName,
  1474. DingTalkConnectSyncDeptAttrName: req.DingTalkConnectSyncDeptAttrName,
  1475. WeChatConnectEnabled: req.WeChatConnectEnabled,
  1476. WeChatConnectAppID: req.WeChatConnectAppID,
  1477. WeChatConnectAppSecret: req.WeChatConnectAppSecret,
  1478. WeChatConnectOpenAppID: req.WeChatConnectOpenAppID,
  1479. WeChatConnectOpenAppSecret: req.WeChatConnectOpenAppSecret,
  1480. WeChatConnectMPAppID: req.WeChatConnectMPAppID,
  1481. WeChatConnectMPAppSecret: req.WeChatConnectMPAppSecret,
  1482. WeChatConnectMobileAppID: req.WeChatConnectMobileAppID,
  1483. WeChatConnectMobileAppSecret: req.WeChatConnectMobileAppSecret,
  1484. WeChatConnectOpenEnabled: req.WeChatConnectOpenEnabled,
  1485. WeChatConnectMPEnabled: req.WeChatConnectMPEnabled,
  1486. WeChatConnectMobileEnabled: req.WeChatConnectMobileEnabled,
  1487. WeChatConnectMode: req.WeChatConnectMode,
  1488. WeChatConnectScopes: req.WeChatConnectScopes,
  1489. WeChatConnectRedirectURL: req.WeChatConnectRedirectURL,
  1490. WeChatConnectFrontendRedirectURL: req.WeChatConnectFrontendRedirectURL,
  1491. OIDCConnectEnabled: req.OIDCConnectEnabled,
  1492. OIDCConnectProviderName: req.OIDCConnectProviderName,
  1493. OIDCConnectClientID: req.OIDCConnectClientID,
  1494. OIDCConnectClientSecret: req.OIDCConnectClientSecret,
  1495. OIDCConnectIssuerURL: req.OIDCConnectIssuerURL,
  1496. OIDCConnectDiscoveryURL: req.OIDCConnectDiscoveryURL,
  1497. OIDCConnectAuthorizeURL: req.OIDCConnectAuthorizeURL,
  1498. OIDCConnectTokenURL: req.OIDCConnectTokenURL,
  1499. OIDCConnectUserInfoURL: req.OIDCConnectUserInfoURL,
  1500. OIDCConnectJWKSURL: req.OIDCConnectJWKSURL,
  1501. OIDCConnectScopes: req.OIDCConnectScopes,
  1502. OIDCConnectRedirectURL: req.OIDCConnectRedirectURL,
  1503. OIDCConnectFrontendRedirectURL: req.OIDCConnectFrontendRedirectURL,
  1504. OIDCConnectTokenAuthMethod: req.OIDCConnectTokenAuthMethod,
  1505. OIDCConnectUsePKCE: oidcUsePKCE,
  1506. OIDCConnectValidateIDToken: oidcValidateIDToken,
  1507. OIDCConnectAllowedSigningAlgs: req.OIDCConnectAllowedSigningAlgs,
  1508. OIDCConnectClockSkewSeconds: req.OIDCConnectClockSkewSeconds,
  1509. OIDCConnectRequireEmailVerified: req.OIDCConnectRequireEmailVerified,
  1510. OIDCConnectUserInfoEmailPath: req.OIDCConnectUserInfoEmailPath,
  1511. OIDCConnectUserInfoIDPath: req.OIDCConnectUserInfoIDPath,
  1512. OIDCConnectUserInfoUsernamePath: req.OIDCConnectUserInfoUsernamePath,
  1513. GitHubOAuthEnabled: req.GitHubOAuthEnabled,
  1514. GitHubOAuthClientID: req.GitHubOAuthClientID,
  1515. GitHubOAuthClientSecret: req.GitHubOAuthClientSecret,
  1516. GitHubOAuthRedirectURL: req.GitHubOAuthRedirectURL,
  1517. GitHubOAuthFrontendRedirectURL: req.GitHubOAuthFrontendRedirectURL,
  1518. GoogleOAuthEnabled: req.GoogleOAuthEnabled,
  1519. GoogleOAuthClientID: req.GoogleOAuthClientID,
  1520. GoogleOAuthClientSecret: req.GoogleOAuthClientSecret,
  1521. GoogleOAuthRedirectURL: req.GoogleOAuthRedirectURL,
  1522. GoogleOAuthFrontendRedirectURL: req.GoogleOAuthFrontendRedirectURL,
  1523. SiteName: req.SiteName,
  1524. SiteLogo: req.SiteLogo,
  1525. SiteSubtitle: req.SiteSubtitle,
  1526. APIBaseURL: req.APIBaseURL,
  1527. ContactInfo: req.ContactInfo,
  1528. DocURL: req.DocURL,
  1529. HomeContent: req.HomeContent,
  1530. CompactHomeEnabled: req.CompactHomeEnabled,
  1531. HideCcsImportButton: req.HideCcsImportButton,
  1532. PurchaseSubscriptionEnabled: purchaseEnabled,
  1533. PurchaseSubscriptionURL: purchaseURL,
  1534. TableDefaultPageSize: req.TableDefaultPageSize,
  1535. TablePageSizeOptions: req.TablePageSizeOptions,
  1536. CustomMenuItems: customMenuJSON,
  1537. CustomEndpoints: customEndpointsJSON,
  1538. DefaultConcurrency: req.DefaultConcurrency,
  1539. DefaultBalance: req.DefaultBalance,
  1540. AffiliateRebateRate: affiliateRebateRate,
  1541. AffiliateRebateFreezeHours: affiliateRebateFreezeHours,
  1542. AffiliateRebateDurationDays: affiliateRebateDurationDays,
  1543. AffiliateRebatePerInviteeCap: affiliateRebatePerInviteeCap,
  1544. AdminRechargeRebateEnabled: adminRechargeRebateEnabled,
  1545. DefaultUserRPMLimit: req.DefaultUserRPMLimit,
  1546. DefaultSubscriptions: defaultSubscriptions,
  1547. EnableModelFallback: req.EnableModelFallback,
  1548. FallbackModelAnthropic: req.FallbackModelAnthropic,
  1549. FallbackModelOpenAI: req.FallbackModelOpenAI,
  1550. FallbackModelGemini: req.FallbackModelGemini,
  1551. FallbackModelAntigravity: req.FallbackModelAntigravity,
  1552. EnableIdentityPatch: req.EnableIdentityPatch,
  1553. IdentityPatchPrompt: req.IdentityPatchPrompt,
  1554. MinClaudeCodeVersion: req.MinClaudeCodeVersion,
  1555. MaxClaudeCodeVersion: req.MaxClaudeCodeVersion,
  1556. AllowUngroupedKeyScheduling: req.AllowUngroupedKeyScheduling,
  1557. BackendModeEnabled: req.BackendModeEnabled,
  1558. AllowUserViewErrorRequests: func() bool {
  1559. if req.AllowUserViewErrorRequests != nil {
  1560. return *req.AllowUserViewErrorRequests
  1561. }
  1562. return previousSettings.AllowUserViewErrorRequests
  1563. }(),
  1564. OpsMonitoringEnabled: func() bool {
  1565. if req.OpsMonitoringEnabled != nil {
  1566. return *req.OpsMonitoringEnabled
  1567. }
  1568. return previousSettings.OpsMonitoringEnabled
  1569. }(),
  1570. OpsRealtimeMonitoringEnabled: func() bool {
  1571. if req.OpsRealtimeMonitoringEnabled != nil {
  1572. return *req.OpsRealtimeMonitoringEnabled
  1573. }
  1574. return previousSettings.OpsRealtimeMonitoringEnabled
  1575. }(),
  1576. OpsQueryModeDefault: func() string {
  1577. if req.OpsQueryModeDefault != nil {
  1578. return *req.OpsQueryModeDefault
  1579. }
  1580. return previousSettings.OpsQueryModeDefault
  1581. }(),
  1582. OpsMetricsIntervalSeconds: func() int {
  1583. if req.OpsMetricsIntervalSeconds != nil {
  1584. return *req.OpsMetricsIntervalSeconds
  1585. }
  1586. return previousSettings.OpsMetricsIntervalSeconds
  1587. }(),
  1588. EnableFingerprintUnification: func() bool {
  1589. if req.EnableFingerprintUnification != nil {
  1590. return *req.EnableFingerprintUnification
  1591. }
  1592. return previousSettings.EnableFingerprintUnification
  1593. }(),
  1594. OpenAITTFTMode: func() string {
  1595. if req.OpenAITTFTMode != nil {
  1596. return *req.OpenAITTFTMode
  1597. }
  1598. return previousSettings.OpenAITTFTMode
  1599. }(),
  1600. EnableMetadataPassthrough: func() bool {
  1601. if req.EnableMetadataPassthrough != nil {
  1602. return *req.EnableMetadataPassthrough
  1603. }
  1604. return previousSettings.EnableMetadataPassthrough
  1605. }(),
  1606. EnableCCHSigning: func() bool {
  1607. if req.EnableCCHSigning != nil {
  1608. return *req.EnableCCHSigning
  1609. }
  1610. return previousSettings.EnableCCHSigning
  1611. }(),
  1612. EnableClaudeOAuthSystemPromptInjection: func() bool {
  1613. if req.EnableClaudeOAuthSystemPromptInjection != nil {
  1614. return *req.EnableClaudeOAuthSystemPromptInjection
  1615. }
  1616. return previousSettings.EnableClaudeOAuthSystemPromptInjection
  1617. }(),
  1618. ClaudeOAuthSystemPrompt: func() string {
  1619. if req.ClaudeOAuthSystemPrompt != nil {
  1620. return *req.ClaudeOAuthSystemPrompt
  1621. }
  1622. return previousSettings.ClaudeOAuthSystemPrompt
  1623. }(),
  1624. ClaudeOAuthSystemPromptBlocks: func() string {
  1625. if req.ClaudeOAuthSystemPromptBlocks != nil {
  1626. return *req.ClaudeOAuthSystemPromptBlocks
  1627. }
  1628. return previousSettings.ClaudeOAuthSystemPromptBlocks
  1629. }(),
  1630. EnableAnthropicCacheTTL1hInjection: func() bool {
  1631. if req.EnableAnthropicCacheTTL1hInjection != nil {
  1632. return *req.EnableAnthropicCacheTTL1hInjection
  1633. }
  1634. return previousSettings.EnableAnthropicCacheTTL1hInjection
  1635. }(),
  1636. RewriteMessageCacheControl: func() bool {
  1637. if req.RewriteMessageCacheControl != nil {
  1638. return *req.RewriteMessageCacheControl
  1639. }
  1640. return previousSettings.RewriteMessageCacheControl
  1641. }(),
  1642. EnableClientDatelineNormalization: func() bool {
  1643. if req.EnableClientDatelineNormalization != nil {
  1644. return *req.EnableClientDatelineNormalization
  1645. }
  1646. return previousSettings.EnableClientDatelineNormalization
  1647. }(),
  1648. AntigravityUserAgentVersion: func() string {
  1649. if req.AntigravityUserAgentVersion != nil {
  1650. return *req.AntigravityUserAgentVersion
  1651. }
  1652. return previousSettings.AntigravityUserAgentVersion
  1653. }(),
  1654. OpenAICodexUserAgent: func() string {
  1655. if req.OpenAICodexUserAgent != nil {
  1656. return *req.OpenAICodexUserAgent
  1657. }
  1658. return previousSettings.OpenAICodexUserAgent
  1659. }(),
  1660. OpenAICodexClientVersion: func() string {
  1661. if req.OpenAICodexClientVersion != nil {
  1662. return *req.OpenAICodexClientVersion
  1663. }
  1664. return previousSettings.OpenAICodexClientVersion
  1665. }(),
  1666. // 同步值由自动同步任务独占写入,面板保存时原样带回,避免被清空。
  1667. OpenAICodexClientVersionSynced: previousSettings.OpenAICodexClientVersionSynced,
  1668. OpenAICodexVersionAutoSyncEnabled: func() bool {
  1669. if req.OpenAICodexVersionAutoSyncEnabled != nil {
  1670. return *req.OpenAICodexVersionAutoSyncEnabled
  1671. }
  1672. return previousSettings.OpenAICodexVersionAutoSyncEnabled
  1673. }(),
  1674. MinCodexVersion: strings.TrimSpace(req.MinCodexVersion),
  1675. MaxCodexVersion: strings.TrimSpace(req.MaxCodexVersion),
  1676. CodexCLIOnlyBlacklist: strings.TrimSpace(req.CodexCLIOnlyBlacklist),
  1677. CodexCLIOnlyWhitelist: strings.TrimSpace(req.CodexCLIOnlyWhitelist),
  1678. CodexCLIOnlyAllowAppServerClients: func() bool {
  1679. if req.CodexCLIOnlyAllowAppServerClients != nil {
  1680. return *req.CodexCLIOnlyAllowAppServerClients
  1681. }
  1682. return previousSettings.CodexCLIOnlyAllowAppServerClients
  1683. }(),
  1684. CodexCLIOnlyEngineFingerprintSignals: strings.TrimSpace(req.CodexCLIOnlyEngineFingerprintSignals),
  1685. PaymentVisibleMethodAlipaySource: func() string {
  1686. if req.PaymentVisibleMethodAlipaySource != nil {
  1687. return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource)
  1688. }
  1689. return previousSettings.PaymentVisibleMethodAlipaySource
  1690. }(),
  1691. PaymentVisibleMethodWxpaySource: func() string {
  1692. if req.PaymentVisibleMethodWxpaySource != nil {
  1693. return strings.TrimSpace(*req.PaymentVisibleMethodWxpaySource)
  1694. }
  1695. return previousSettings.PaymentVisibleMethodWxpaySource
  1696. }(),
  1697. PaymentVisibleMethodAlipayEnabled: func() bool {
  1698. if req.PaymentVisibleMethodAlipayEnabled != nil {
  1699. return *req.PaymentVisibleMethodAlipayEnabled
  1700. }
  1701. return previousSettings.PaymentVisibleMethodAlipayEnabled
  1702. }(),
  1703. PaymentVisibleMethodWxpayEnabled: func() bool {
  1704. if req.PaymentVisibleMethodWxpayEnabled != nil {
  1705. return *req.PaymentVisibleMethodWxpayEnabled
  1706. }
  1707. return previousSettings.PaymentVisibleMethodWxpayEnabled
  1708. }(),
  1709. OpenAILowUpstreamRatePriorityEnabled: func() bool {
  1710. if req.OpenAILowUpstreamRatePriorityEnabled != nil {
  1711. return *req.OpenAILowUpstreamRatePriorityEnabled
  1712. }
  1713. return previousSettings.OpenAILowUpstreamRatePriorityEnabled
  1714. }(),
  1715. OpenAIOAuthSchedulingRateMultiplier: func() float64 {
  1716. if req.OpenAIOAuthSchedulingRateMultiplier != nil {
  1717. return *req.OpenAIOAuthSchedulingRateMultiplier
  1718. }
  1719. return previousSettings.OpenAIOAuthSchedulingRateMultiplier
  1720. }(),
  1721. OpenAIAdvancedSchedulerEnabled: func() bool {
  1722. if req.OpenAIAdvancedSchedulerEnabled != nil {
  1723. return *req.OpenAIAdvancedSchedulerEnabled
  1724. }
  1725. return previousSettings.OpenAIAdvancedSchedulerEnabled
  1726. }(),
  1727. OpenAIAdvancedSchedulerStickyWeightedEnabled: func() bool {
  1728. if req.OpenAIAdvancedSchedulerStickyWeightedEnabled != nil {
  1729. return *req.OpenAIAdvancedSchedulerStickyWeightedEnabled
  1730. }
  1731. return previousSettings.OpenAIAdvancedSchedulerStickyWeightedEnabled
  1732. }(),
  1733. OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: func() bool {
  1734. if req.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled != nil {
  1735. return *req.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled
  1736. }
  1737. return previousSettings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled
  1738. }(),
  1739. OpenAIAdvancedSchedulerLBTopK: stringSetting(req.OpenAIAdvancedSchedulerLBTopK, previousSettings.OpenAIAdvancedSchedulerLBTopK),
  1740. OpenAIAdvancedSchedulerWeightPriority: stringSetting(req.OpenAIAdvancedSchedulerWeightPriority, previousSettings.OpenAIAdvancedSchedulerWeightPriority),
  1741. OpenAIAdvancedSchedulerWeightLoad: stringSetting(req.OpenAIAdvancedSchedulerWeightLoad, previousSettings.OpenAIAdvancedSchedulerWeightLoad),
  1742. OpenAIAdvancedSchedulerWeightQueue: stringSetting(req.OpenAIAdvancedSchedulerWeightQueue, previousSettings.OpenAIAdvancedSchedulerWeightQueue),
  1743. OpenAIAdvancedSchedulerWeightErrorRate: stringSetting(req.OpenAIAdvancedSchedulerWeightErrorRate, previousSettings.OpenAIAdvancedSchedulerWeightErrorRate),
  1744. OpenAIAdvancedSchedulerWeightTTFT: stringSetting(req.OpenAIAdvancedSchedulerWeightTTFT, previousSettings.OpenAIAdvancedSchedulerWeightTTFT),
  1745. OpenAIAdvancedSchedulerWeightReset: stringSetting(req.OpenAIAdvancedSchedulerWeightReset, previousSettings.OpenAIAdvancedSchedulerWeightReset),
  1746. OpenAIAdvancedSchedulerWeightQuotaHeadroom: stringSetting(req.OpenAIAdvancedSchedulerWeightQuotaHeadroom, previousSettings.OpenAIAdvancedSchedulerWeightQuotaHeadroom),
  1747. OpenAIAdvancedSchedulerWeightUpstreamCost: stringSetting(req.OpenAIAdvancedSchedulerWeightUpstreamCost, previousSettings.OpenAIAdvancedSchedulerWeightUpstreamCost),
  1748. OpenAIAdvancedSchedulerWeightPreviousResponse: stringSetting(req.OpenAIAdvancedSchedulerWeightPreviousResponse, previousSettings.OpenAIAdvancedSchedulerWeightPreviousResponse),
  1749. OpenAIAdvancedSchedulerWeightSessionSticky: stringSetting(req.OpenAIAdvancedSchedulerWeightSessionSticky, previousSettings.OpenAIAdvancedSchedulerWeightSessionSticky),
  1750. BalanceLowNotifyEnabled: func() bool {
  1751. if req.BalanceLowNotifyEnabled != nil {
  1752. return *req.BalanceLowNotifyEnabled
  1753. }
  1754. return previousSettings.BalanceLowNotifyEnabled
  1755. }(),
  1756. BalanceLowNotifyThreshold: func() float64 {
  1757. if req.BalanceLowNotifyThreshold != nil {
  1758. return *req.BalanceLowNotifyThreshold
  1759. }
  1760. return previousSettings.BalanceLowNotifyThreshold
  1761. }(),
  1762. BalanceLowNotifyRechargeURL: func() string {
  1763. if req.BalanceLowNotifyRechargeURL != nil {
  1764. return *req.BalanceLowNotifyRechargeURL
  1765. }
  1766. return previousSettings.BalanceLowNotifyRechargeURL
  1767. }(),
  1768. SubscriptionExpiryNotifyEnabled: func() bool {
  1769. if req.SubscriptionExpiryNotifyEnabled != nil {
  1770. return *req.SubscriptionExpiryNotifyEnabled
  1771. }
  1772. return previousSettings.SubscriptionExpiryNotifyEnabled
  1773. }(),
  1774. AccountQuotaNotifyEnabled: func() bool {
  1775. if req.AccountQuotaNotifyEnabled != nil {
  1776. return *req.AccountQuotaNotifyEnabled
  1777. }
  1778. return previousSettings.AccountQuotaNotifyEnabled
  1779. }(),
  1780. AccountQuotaNotifyEmails: func() []service.NotifyEmailEntry {
  1781. if req.AccountQuotaNotifyEmails != nil {
  1782. return dto.NotifyEmailEntriesToService(*req.AccountQuotaNotifyEmails)
  1783. }
  1784. return previousSettings.AccountQuotaNotifyEmails
  1785. }(),
  1786. ChannelMonitorEnabled: func() bool {
  1787. if req.ChannelMonitorEnabled != nil {
  1788. return *req.ChannelMonitorEnabled
  1789. }
  1790. return previousSettings.ChannelMonitorEnabled
  1791. }(),
  1792. ChannelMonitorMode: func() string {
  1793. if req.ChannelMonitorMode != nil {
  1794. return *req.ChannelMonitorMode
  1795. }
  1796. return previousSettings.ChannelMonitorMode
  1797. }(),
  1798. ChannelMonitorDefaultIntervalSeconds: func() int {
  1799. if req.ChannelMonitorDefaultIntervalSeconds != nil {
  1800. return *req.ChannelMonitorDefaultIntervalSeconds
  1801. }
  1802. return previousSettings.ChannelMonitorDefaultIntervalSeconds
  1803. }(),
  1804. ChannelMonitorHideThroughput: func() bool {
  1805. if req.ChannelMonitorHideThroughput != nil {
  1806. return *req.ChannelMonitorHideThroughput
  1807. }
  1808. return previousSettings.ChannelMonitorHideThroughput
  1809. }(),
  1810. ChannelMonitorShowQuota: func() bool {
  1811. if req.ChannelMonitorShowQuota != nil {
  1812. return *req.ChannelMonitorShowQuota
  1813. }
  1814. return previousSettings.ChannelMonitorShowQuota
  1815. }(),
  1816. ChannelMonitorHideUserRanking: func() bool {
  1817. if req.ChannelMonitorHideUserRanking != nil {
  1818. return *req.ChannelMonitorHideUserRanking
  1819. }
  1820. return previousSettings.ChannelMonitorHideUserRanking
  1821. }(),
  1822. GrokDefaultTextModel: func() string {
  1823. if req.GrokDefaultTextModel != nil {
  1824. return *req.GrokDefaultTextModel
  1825. }
  1826. return previousSettings.GrokDefaultTextModel
  1827. }(),
  1828. GrokCrossClientModelMapEnabled: func() bool {
  1829. if req.GrokCrossClientModelMapEnabled != nil {
  1830. return *req.GrokCrossClientModelMapEnabled
  1831. }
  1832. return previousSettings.GrokCrossClientModelMapEnabled
  1833. }(),
  1834. GrokDefaultBaseURLMode: func() string {
  1835. if req.GrokDefaultBaseURLMode != nil {
  1836. return strings.TrimSpace(*req.GrokDefaultBaseURLMode)
  1837. }
  1838. return previousSettings.GrokDefaultBaseURLMode
  1839. }(),
  1840. AvailableChannelsEnabled: func() bool {
  1841. if req.AvailableChannelsEnabled != nil {
  1842. return *req.AvailableChannelsEnabled
  1843. }
  1844. return previousSettings.AvailableChannelsEnabled
  1845. }(),
  1846. ModelPlazaEnabled: func() bool {
  1847. if req.ModelPlazaEnabled != nil {
  1848. return *req.ModelPlazaEnabled
  1849. }
  1850. return previousSettings.ModelPlazaEnabled
  1851. }(),
  1852. ModelPlazaRequireAuth: func() bool {
  1853. if req.ModelPlazaRequireAuth != nil {
  1854. return *req.ModelPlazaRequireAuth
  1855. }
  1856. return previousSettings.ModelPlazaRequireAuth
  1857. }(),
  1858. ModelPlazaDescription: func() string {
  1859. if req.ModelPlazaDescription != nil {
  1860. return *req.ModelPlazaDescription
  1861. }
  1862. return previousSettings.ModelPlazaDescription
  1863. }(),
  1864. PluginManagementEnabled: func() bool {
  1865. if req.PluginManagementEnabled != nil {
  1866. return *req.PluginManagementEnabled
  1867. }
  1868. return previousSettings.PluginManagementEnabled
  1869. }(),
  1870. AffiliateEnabled: func() bool {
  1871. if req.AffiliateEnabled != nil {
  1872. return *req.AffiliateEnabled
  1873. }
  1874. return previousSettings.AffiliateEnabled
  1875. }(),
  1876. RiskControlEnabled: func() bool {
  1877. if req.RiskControlEnabled != nil {
  1878. return *req.RiskControlEnabled
  1879. }
  1880. return previousSettings.RiskControlEnabled
  1881. }(),
  1882. CyberSessionBlockEnabled: func() bool {
  1883. if req.CyberSessionBlockEnabled != nil {
  1884. return *req.CyberSessionBlockEnabled
  1885. }
  1886. return previousSettings.CyberSessionBlockEnabled
  1887. }(),
  1888. CyberSessionBlockTTLSeconds: func() int {
  1889. if req.CyberSessionBlockTTLSeconds != nil {
  1890. return *req.CyberSessionBlockTTLSeconds
  1891. }
  1892. return previousSettings.CyberSessionBlockTTLSeconds
  1893. }(),
  1894. }
  1895. // req.AuthSourceXxxPlatformQuotas 为 nil 表示本次请求未包含该 source 的 quota 配置(保留 previousAuthSourceDefaults 中的值);
  1896. // non-nil(含 empty map)表示整体覆盖:empty map = 清空该 source 的所有 quota 配置。
  1897. authSourceDefaults := &service.AuthSourceDefaultSettings{
  1898. Email: service.ProviderDefaultGrantSettings{
  1899. Balance: float64ValueOrDefault(req.AuthSourceDefaultEmailBalance, previousAuthSourceDefaults.Email.Balance),
  1900. Concurrency: intValueOrDefault(req.AuthSourceDefaultEmailConcurrency, previousAuthSourceDefaults.Email.Concurrency),
  1901. Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultEmailSubscriptions, previousAuthSourceDefaults.Email.Subscriptions),
  1902. GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultEmailGrantOnSignup, previousAuthSourceDefaults.Email.GrantOnSignup),
  1903. GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultEmailGrantOnFirstBind, previousAuthSourceDefaults.Email.GrantOnFirstBind),
  1904. PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceEmailPlatformQuotas, previousAuthSourceDefaults.Email.PlatformQuotas),
  1905. },
  1906. LinuxDo: service.ProviderDefaultGrantSettings{
  1907. Balance: float64ValueOrDefault(req.AuthSourceDefaultLinuxDoBalance, previousAuthSourceDefaults.LinuxDo.Balance),
  1908. Concurrency: intValueOrDefault(req.AuthSourceDefaultLinuxDoConcurrency, previousAuthSourceDefaults.LinuxDo.Concurrency),
  1909. Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultLinuxDoSubscriptions, previousAuthSourceDefaults.LinuxDo.Subscriptions),
  1910. GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultLinuxDoGrantOnSignup, previousAuthSourceDefaults.LinuxDo.GrantOnSignup),
  1911. GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultLinuxDoGrantOnFirstBind, previousAuthSourceDefaults.LinuxDo.GrantOnFirstBind),
  1912. PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceLinuxDoPlatformQuotas, previousAuthSourceDefaults.LinuxDo.PlatformQuotas),
  1913. },
  1914. OIDC: service.ProviderDefaultGrantSettings{
  1915. Balance: float64ValueOrDefault(req.AuthSourceDefaultOIDCBalance, previousAuthSourceDefaults.OIDC.Balance),
  1916. Concurrency: intValueOrDefault(req.AuthSourceDefaultOIDCConcurrency, previousAuthSourceDefaults.OIDC.Concurrency),
  1917. Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultOIDCSubscriptions, previousAuthSourceDefaults.OIDC.Subscriptions),
  1918. GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultOIDCGrantOnSignup, previousAuthSourceDefaults.OIDC.GrantOnSignup),
  1919. GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultOIDCGrantOnFirstBind, previousAuthSourceDefaults.OIDC.GrantOnFirstBind),
  1920. PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceOIDCPlatformQuotas, previousAuthSourceDefaults.OIDC.PlatformQuotas),
  1921. },
  1922. WeChat: service.ProviderDefaultGrantSettings{
  1923. Balance: float64ValueOrDefault(req.AuthSourceDefaultWeChatBalance, previousAuthSourceDefaults.WeChat.Balance),
  1924. Concurrency: intValueOrDefault(req.AuthSourceDefaultWeChatConcurrency, previousAuthSourceDefaults.WeChat.Concurrency),
  1925. Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultWeChatSubscriptions, previousAuthSourceDefaults.WeChat.Subscriptions),
  1926. GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultWeChatGrantOnSignup, previousAuthSourceDefaults.WeChat.GrantOnSignup),
  1927. GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultWeChatGrantOnFirstBind, previousAuthSourceDefaults.WeChat.GrantOnFirstBind),
  1928. PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceWeChatPlatformQuotas, previousAuthSourceDefaults.WeChat.PlatformQuotas),
  1929. },
  1930. GitHub: service.ProviderDefaultGrantSettings{
  1931. Balance: float64ValueOrDefault(req.AuthSourceDefaultGitHubBalance, previousAuthSourceDefaults.GitHub.Balance),
  1932. Concurrency: intValueOrDefault(req.AuthSourceDefaultGitHubConcurrency, previousAuthSourceDefaults.GitHub.Concurrency),
  1933. Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultGitHubSubscriptions, previousAuthSourceDefaults.GitHub.Subscriptions),
  1934. GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultGitHubGrantOnSignup, previousAuthSourceDefaults.GitHub.GrantOnSignup),
  1935. GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultGitHubGrantOnFirstBind, previousAuthSourceDefaults.GitHub.GrantOnFirstBind),
  1936. PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceGitHubPlatformQuotas, previousAuthSourceDefaults.GitHub.PlatformQuotas),
  1937. },
  1938. Google: service.ProviderDefaultGrantSettings{
  1939. Balance: float64ValueOrDefault(req.AuthSourceDefaultGoogleBalance, previousAuthSourceDefaults.Google.Balance),
  1940. Concurrency: intValueOrDefault(req.AuthSourceDefaultGoogleConcurrency, previousAuthSourceDefaults.Google.Concurrency),
  1941. Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultGoogleSubscriptions, previousAuthSourceDefaults.Google.Subscriptions),
  1942. GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultGoogleGrantOnSignup, previousAuthSourceDefaults.Google.GrantOnSignup),
  1943. GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultGoogleGrantOnFirstBind, previousAuthSourceDefaults.Google.GrantOnFirstBind),
  1944. PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceGooglePlatformQuotas, previousAuthSourceDefaults.Google.PlatformQuotas),
  1945. },
  1946. DingTalk: service.ProviderDefaultGrantSettings{
  1947. Balance: float64ValueOrDefault(req.AuthSourceDefaultDingTalkBalance, previousAuthSourceDefaults.DingTalk.Balance),
  1948. Concurrency: intValueOrDefault(req.AuthSourceDefaultDingTalkConcurrency, previousAuthSourceDefaults.DingTalk.Concurrency),
  1949. Subscriptions: defaultSubscriptionsValueOrDefault(req.AuthSourceDefaultDingTalkSubscriptions, previousAuthSourceDefaults.DingTalk.Subscriptions),
  1950. GrantOnSignup: boolValueOrDefault(req.AuthSourceDefaultDingTalkGrantOnSignup, previousAuthSourceDefaults.DingTalk.GrantOnSignup),
  1951. GrantOnFirstBind: boolValueOrDefault(req.AuthSourceDefaultDingTalkGrantOnFirstBind, previousAuthSourceDefaults.DingTalk.GrantOnFirstBind),
  1952. PlatformQuotas: platformQuotasValueOrDefault(req.AuthSourceDingTalkPlatformQuotas, previousAuthSourceDefaults.DingTalk.PlatformQuotas),
  1953. },
  1954. ForceEmailOnThirdPartySignup: boolValueOrDefault(req.ForceEmailOnThirdPartySignup, previousAuthSourceDefaults.ForceEmailOnThirdPartySignup),
  1955. }
  1956. if err := h.settingService.UpdateSettingsWithAuthSourceDefaultsOmitting(c.Request.Context(), settings, authSourceDefaults, omitted); err != nil {
  1957. response.ErrorFrom(c, err)
  1958. return
  1959. }
  1960. if h.opsService != nil {
  1961. h.opsService.SetMonitoringEnabled(settings.OpsMonitoringEnabled)
  1962. }
  1963. // Update OpenAI fast policy (stored under dedicated key, only when provided).
  1964. if req.OpenAIFastPolicySettings != nil {
  1965. if err := h.settingService.SetOpenAIFastPolicySettings(c.Request.Context(), openaiFastPolicySettingsFromDTO(req.OpenAIFastPolicySettings)); err != nil {
  1966. response.BadRequest(c, err.Error())
  1967. return
  1968. }
  1969. }
  1970. // Update payment configuration (integrated into system settings).
  1971. // Skip if no payment fields were provided (prevents accidental wipe).
  1972. if h.paymentConfigService != nil && hasPaymentFields(req) {
  1973. paymentReq := service.UpdatePaymentConfigRequest{
  1974. Enabled: req.PaymentEnabled,
  1975. MinAmount: req.PaymentMinAmount,
  1976. MaxAmount: req.PaymentMaxAmount,
  1977. DailyLimit: req.PaymentDailyLimit,
  1978. OrderTimeoutMin: req.PaymentOrderTimeoutMin,
  1979. MaxPendingOrders: req.PaymentMaxPendingOrders,
  1980. EnabledTypes: req.PaymentEnabledTypes,
  1981. BalanceDisabled: req.PaymentBalanceDisabled,
  1982. BalanceRechargeMultiplier: req.PaymentBalanceRechargeMultiplier,
  1983. SubscriptionUSDToCNYRate: req.PaymentSubscriptionUSDToCNYRate,
  1984. RechargeFeeRate: req.PaymentRechargeFeeRate,
  1985. LoadBalanceStrategy: req.PaymentLoadBalanceStrat,
  1986. ProductNamePrefix: req.PaymentProductNamePrefix,
  1987. ProductNameSuffix: req.PaymentProductNameSuffix,
  1988. HelpImageURL: req.PaymentHelpImageURL,
  1989. HelpText: req.PaymentHelpText,
  1990. CancelRateLimitEnabled: req.PaymentCancelRateLimitEnabled,
  1991. CancelRateLimitMax: req.PaymentCancelRateLimitMax,
  1992. CancelRateLimitWindow: req.PaymentCancelRateLimitWindow,
  1993. CancelRateLimitUnit: req.PaymentCancelRateLimitUnit,
  1994. CancelRateLimitMode: req.PaymentCancelRateLimitMode,
  1995. AlipayForceQRCode: req.PaymentAlipayForceQRCode,
  1996. AlipayMobilePrecreateDeepLink: req.PaymentAlipayMobilePrecreateDeepLink,
  1997. }
  1998. if err := h.paymentConfigService.UpdatePaymentConfig(c.Request.Context(), paymentReq); err != nil {
  1999. response.ErrorFrom(c, err)
  2000. return
  2001. }
  2002. // Refresh in-memory provider registry so config changes take effect immediately
  2003. if h.paymentService != nil {
  2004. h.paymentService.RefreshProviders(c.Request.Context())
  2005. }
  2006. }
  2007. h.auditSettingsUpdate(c, previousSettings, settings, previousAuthSourceDefaults, authSourceDefaults, auditReq)
  2008. // 重新获取设置返回
  2009. updatedSettings, err := h.settingService.GetAllSettings(c.Request.Context())
  2010. if err != nil {
  2011. response.ErrorFrom(c, err)
  2012. return
  2013. }
  2014. h.ensureDingTalkSyncAttributes(c.Request.Context(), updatedSettings)
  2015. updatedAuthSourceDefaults, err := h.settingService.GetAuthSourceDefaultSettings(c.Request.Context())
  2016. if err != nil {
  2017. response.ErrorFrom(c, err)
  2018. return
  2019. }
  2020. updatedDefaultSubscriptions := make([]dto.DefaultSubscriptionSetting, 0, len(updatedSettings.DefaultSubscriptions))
  2021. for _, sub := range updatedSettings.DefaultSubscriptions {
  2022. updatedDefaultSubscriptions = append(updatedDefaultSubscriptions, dto.DefaultSubscriptionSetting{
  2023. GroupID: sub.GroupID,
  2024. ValidityDays: sub.ValidityDays,
  2025. })
  2026. }
  2027. // Reload payment config for response
  2028. var updatedPaymentCfg *service.PaymentConfig
  2029. if h.paymentConfigService != nil {
  2030. updatedPaymentCfg, _ = h.paymentConfigService.GetPaymentConfig(c.Request.Context())
  2031. }
  2032. if updatedPaymentCfg == nil {
  2033. updatedPaymentCfg = &service.PaymentConfig{}
  2034. }
  2035. passkeyConfigured, passkeyRPID, passkeyRPOrigins := h.settingService.PasskeyConfiguration()
  2036. payload := dto.SystemSettings{
  2037. RegistrationEnabled: updatedSettings.RegistrationEnabled,
  2038. EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled,
  2039. RegistrationEmailSuffixWhitelist: updatedSettings.RegistrationEmailSuffixWhitelist,
  2040. RegistrationEmailDomainQuotaEnabled: updatedSettings.RegistrationEmailDomainQuotaEnabled,
  2041. PromoCodeEnabled: updatedSettings.PromoCodeEnabled,
  2042. PasswordResetEnabled: updatedSettings.PasswordResetEnabled,
  2043. FrontendURL: updatedSettings.FrontendURL,
  2044. InvitationCodeEnabled: updatedSettings.InvitationCodeEnabled,
  2045. TotpEnabled: updatedSettings.TotpEnabled,
  2046. TotpEncryptionKeyConfigured: h.settingService.IsTotpEncryptionKeyConfigured(),
  2047. PasskeyEnabled: updatedSettings.PasskeyEnabled,
  2048. PasskeyConfigured: passkeyConfigured,
  2049. PasskeyRPID: passkeyRPID,
  2050. PasskeyRPOrigins: passkeyRPOrigins,
  2051. SessionBindingEnabled: updatedSettings.SessionBindingEnabled,
  2052. StepUpEnabled: updatedSettings.StepUpEnabled,
  2053. AuditLogRetentionDays: updatedSettings.AuditLogRetentionDays,
  2054. LoginAgreementEnabled: updatedSettings.LoginAgreementEnabled,
  2055. LoginAgreementMode: updatedSettings.LoginAgreementMode,
  2056. LoginAgreementUpdatedAt: updatedSettings.LoginAgreementUpdatedAt,
  2057. LoginAgreementDocuments: loginAgreementDocumentsToDTO(updatedSettings.LoginAgreementDocuments),
  2058. SMTPHost: updatedSettings.SMTPHost,
  2059. SMTPPort: updatedSettings.SMTPPort,
  2060. SMTPUsername: updatedSettings.SMTPUsername,
  2061. SMTPPasswordConfigured: updatedSettings.SMTPPasswordConfigured,
  2062. SMTPFrom: updatedSettings.SMTPFrom,
  2063. SMTPFromName: updatedSettings.SMTPFromName,
  2064. SMTPUseTLS: updatedSettings.SMTPUseTLS,
  2065. TurnstileEnabled: updatedSettings.TurnstileEnabled,
  2066. TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
  2067. TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
  2068. TencentCaptchaEnabled: updatedSettings.TencentCaptchaEnabled,
  2069. TencentCaptchaAppID: updatedSettings.TencentCaptchaAppID,
  2070. TencentCaptchaAppSecretKeyConfigured: updatedSettings.TencentCaptchaAppSecretKeyConfigured,
  2071. TencentCaptchaCloudSecretIDConfigured: updatedSettings.TencentCaptchaCloudSecretIDConfigured,
  2072. TencentCaptchaCloudSecretKeyConfigured: updatedSettings.TencentCaptchaCloudSecretKeyConfigured,
  2073. TencentCaptchaRegion: updatedSettings.TencentCaptchaRegion,
  2074. AliyunCaptchaEnabled: updatedSettings.AliyunCaptchaEnabled,
  2075. AliyunCaptchaAccessKeyID: updatedSettings.AliyunCaptchaAccessKeyID,
  2076. AliyunCaptchaAccessKeySecretConfigured: updatedSettings.AliyunCaptchaAccessKeySecretConfigured,
  2077. AliyunCaptchaSceneID: updatedSettings.AliyunCaptchaSceneID,
  2078. AliyunCaptchaPrefix: updatedSettings.AliyunCaptchaPrefix,
  2079. AliyunCaptchaRegion: updatedSettings.AliyunCaptchaRegion,
  2080. APIKeyACLTrustForwardedIP: updatedSettings.APIKeyACLTrustForwardedIP,
  2081. ForwardedClientIPHeaders: updatedSettings.ForwardedClientIPHeaders,
  2082. LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
  2083. LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
  2084. LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
  2085. LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL,
  2086. DingTalkConnectEnabled: updatedSettings.DingTalkConnectEnabled,
  2087. DingTalkConnectClientID: updatedSettings.DingTalkConnectClientID,
  2088. DingTalkConnectClientSecretConfigured: updatedSettings.DingTalkConnectClientSecretConfigured,
  2089. DingTalkConnectRedirectURL: updatedSettings.DingTalkConnectRedirectURL,
  2090. DingTalkConnectCorpRestrictionPolicy: updatedSettings.DingTalkConnectCorpRestrictionPolicy,
  2091. DingTalkConnectInternalCorpID: updatedSettings.DingTalkConnectInternalCorpID,
  2092. DingTalkConnectBypassRegistration: updatedSettings.DingTalkConnectBypassRegistration,
  2093. DingTalkConnectSyncCorpEmail: updatedSettings.DingTalkConnectSyncCorpEmail,
  2094. DingTalkConnectSyncDisplayName: updatedSettings.DingTalkConnectSyncDisplayName,
  2095. DingTalkConnectSyncDept: updatedSettings.DingTalkConnectSyncDept,
  2096. DingTalkConnectSyncCorpEmailAttrKey: updatedSettings.DingTalkConnectSyncCorpEmailAttrKey,
  2097. DingTalkConnectSyncDisplayNameAttrKey: updatedSettings.DingTalkConnectSyncDisplayNameAttrKey,
  2098. DingTalkConnectSyncDeptAttrKey: updatedSettings.DingTalkConnectSyncDeptAttrKey,
  2099. DingTalkConnectSyncCorpEmailAttrName: updatedSettings.DingTalkConnectSyncCorpEmailAttrName,
  2100. DingTalkConnectSyncDisplayNameAttrName: updatedSettings.DingTalkConnectSyncDisplayNameAttrName,
  2101. DingTalkConnectSyncDeptAttrName: updatedSettings.DingTalkConnectSyncDeptAttrName,
  2102. WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled,
  2103. WeChatConnectAppID: updatedSettings.WeChatConnectAppID,
  2104. WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured,
  2105. WeChatConnectOpenAppID: updatedSettings.WeChatConnectOpenAppID,
  2106. WeChatConnectOpenAppSecretConfigured: updatedSettings.WeChatConnectOpenAppSecretConfigured,
  2107. WeChatConnectMPAppID: updatedSettings.WeChatConnectMPAppID,
  2108. WeChatConnectMPAppSecretConfigured: updatedSettings.WeChatConnectMPAppSecretConfigured,
  2109. WeChatConnectMobileAppID: updatedSettings.WeChatConnectMobileAppID,
  2110. WeChatConnectMobileAppSecretConfigured: updatedSettings.WeChatConnectMobileAppSecretConfigured,
  2111. WeChatConnectOpenEnabled: updatedSettings.WeChatConnectOpenEnabled,
  2112. WeChatConnectMPEnabled: updatedSettings.WeChatConnectMPEnabled,
  2113. WeChatConnectMobileEnabled: updatedSettings.WeChatConnectMobileEnabled,
  2114. WeChatConnectMode: updatedSettings.WeChatConnectMode,
  2115. WeChatConnectScopes: updatedSettings.WeChatConnectScopes,
  2116. WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL,
  2117. WeChatConnectFrontendRedirectURL: updatedSettings.WeChatConnectFrontendRedirectURL,
  2118. OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled,
  2119. OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName,
  2120. OIDCConnectClientID: updatedSettings.OIDCConnectClientID,
  2121. OIDCConnectClientSecretConfigured: updatedSettings.OIDCConnectClientSecretConfigured,
  2122. OIDCConnectIssuerURL: updatedSettings.OIDCConnectIssuerURL,
  2123. OIDCConnectDiscoveryURL: updatedSettings.OIDCConnectDiscoveryURL,
  2124. OIDCConnectAuthorizeURL: updatedSettings.OIDCConnectAuthorizeURL,
  2125. OIDCConnectTokenURL: updatedSettings.OIDCConnectTokenURL,
  2126. OIDCConnectUserInfoURL: updatedSettings.OIDCConnectUserInfoURL,
  2127. OIDCConnectJWKSURL: updatedSettings.OIDCConnectJWKSURL,
  2128. OIDCConnectScopes: updatedSettings.OIDCConnectScopes,
  2129. OIDCConnectRedirectURL: updatedSettings.OIDCConnectRedirectURL,
  2130. OIDCConnectFrontendRedirectURL: updatedSettings.OIDCConnectFrontendRedirectURL,
  2131. OIDCConnectTokenAuthMethod: updatedSettings.OIDCConnectTokenAuthMethod,
  2132. OIDCConnectUsePKCE: updatedSettings.OIDCConnectUsePKCE,
  2133. OIDCConnectValidateIDToken: updatedSettings.OIDCConnectValidateIDToken,
  2134. OIDCConnectAllowedSigningAlgs: updatedSettings.OIDCConnectAllowedSigningAlgs,
  2135. OIDCConnectClockSkewSeconds: updatedSettings.OIDCConnectClockSkewSeconds,
  2136. OIDCConnectRequireEmailVerified: updatedSettings.OIDCConnectRequireEmailVerified,
  2137. OIDCConnectUserInfoEmailPath: updatedSettings.OIDCConnectUserInfoEmailPath,
  2138. OIDCConnectUserInfoIDPath: updatedSettings.OIDCConnectUserInfoIDPath,
  2139. OIDCConnectUserInfoUsernamePath: updatedSettings.OIDCConnectUserInfoUsernamePath,
  2140. GitHubOAuthEnabled: updatedSettings.GitHubOAuthEnabled,
  2141. GitHubOAuthClientID: updatedSettings.GitHubOAuthClientID,
  2142. GitHubOAuthClientSecretConfigured: updatedSettings.GitHubOAuthClientSecretConfigured,
  2143. GitHubOAuthRedirectURL: updatedSettings.GitHubOAuthRedirectURL,
  2144. GitHubOAuthFrontendRedirectURL: updatedSettings.GitHubOAuthFrontendRedirectURL,
  2145. GoogleOAuthEnabled: updatedSettings.GoogleOAuthEnabled,
  2146. GoogleOAuthClientID: updatedSettings.GoogleOAuthClientID,
  2147. GoogleOAuthClientSecretConfigured: updatedSettings.GoogleOAuthClientSecretConfigured,
  2148. GoogleOAuthRedirectURL: updatedSettings.GoogleOAuthRedirectURL,
  2149. GoogleOAuthFrontendRedirectURL: updatedSettings.GoogleOAuthFrontendRedirectURL,
  2150. SiteName: updatedSettings.SiteName,
  2151. SiteLogo: updatedSettings.SiteLogo,
  2152. SiteSubtitle: updatedSettings.SiteSubtitle,
  2153. APIBaseURL: updatedSettings.APIBaseURL,
  2154. ContactInfo: updatedSettings.ContactInfo,
  2155. DocURL: updatedSettings.DocURL,
  2156. HomeContent: updatedSettings.HomeContent,
  2157. CompactHomeEnabled: updatedSettings.CompactHomeEnabled,
  2158. HideCcsImportButton: updatedSettings.HideCcsImportButton,
  2159. PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
  2160. PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
  2161. TableDefaultPageSize: updatedSettings.TableDefaultPageSize,
  2162. TablePageSizeOptions: updatedSettings.TablePageSizeOptions,
  2163. CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems),
  2164. CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints),
  2165. DefaultConcurrency: updatedSettings.DefaultConcurrency,
  2166. DefaultBalance: updatedSettings.DefaultBalance,
  2167. AffiliateRebateRate: updatedSettings.AffiliateRebateRate,
  2168. AffiliateRebateFreezeHours: updatedSettings.AffiliateRebateFreezeHours,
  2169. AffiliateRebateDurationDays: updatedSettings.AffiliateRebateDurationDays,
  2170. AffiliateRebatePerInviteeCap: updatedSettings.AffiliateRebatePerInviteeCap,
  2171. AdminRechargeRebateEnabled: updatedSettings.AdminRechargeRebateEnabled,
  2172. DefaultUserRPMLimit: updatedSettings.DefaultUserRPMLimit,
  2173. DefaultSubscriptions: updatedDefaultSubscriptions,
  2174. EnableModelFallback: updatedSettings.EnableModelFallback,
  2175. FallbackModelAnthropic: updatedSettings.FallbackModelAnthropic,
  2176. FallbackModelOpenAI: updatedSettings.FallbackModelOpenAI,
  2177. FallbackModelGemini: updatedSettings.FallbackModelGemini,
  2178. FallbackModelAntigravity: updatedSettings.FallbackModelAntigravity,
  2179. EnableIdentityPatch: updatedSettings.EnableIdentityPatch,
  2180. IdentityPatchPrompt: updatedSettings.IdentityPatchPrompt,
  2181. OpsMonitoringEnabled: updatedSettings.OpsMonitoringEnabled,
  2182. OpsRealtimeMonitoringEnabled: updatedSettings.OpsRealtimeMonitoringEnabled,
  2183. OpsQueryModeDefault: updatedSettings.OpsQueryModeDefault,
  2184. OpsMetricsIntervalSeconds: updatedSettings.OpsMetricsIntervalSeconds,
  2185. MinClaudeCodeVersion: updatedSettings.MinClaudeCodeVersion,
  2186. MaxClaudeCodeVersion: updatedSettings.MaxClaudeCodeVersion,
  2187. AllowUngroupedKeyScheduling: updatedSettings.AllowUngroupedKeyScheduling,
  2188. BackendModeEnabled: updatedSettings.BackendModeEnabled,
  2189. EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification,
  2190. EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
  2191. EnableCCHSigning: updatedSettings.EnableCCHSigning,
  2192. EnableClaudeOAuthSystemPromptInjection: updatedSettings.EnableClaudeOAuthSystemPromptInjection,
  2193. ClaudeOAuthSystemPrompt: updatedSettings.ClaudeOAuthSystemPrompt,
  2194. ClaudeOAuthSystemPromptBlocks: updatedSettings.ClaudeOAuthSystemPromptBlocks,
  2195. EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
  2196. RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
  2197. EnableClientDatelineNormalization: updatedSettings.EnableClientDatelineNormalization,
  2198. AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
  2199. OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
  2200. OpenAICodexClientVersion: updatedSettings.OpenAICodexClientVersion,
  2201. OpenAICodexClientVersionSynced: updatedSettings.OpenAICodexClientVersionSynced,
  2202. OpenAICodexVersionAutoSyncEnabled: updatedSettings.OpenAICodexVersionAutoSyncEnabled,
  2203. MinCodexVersion: updatedSettings.MinCodexVersion,
  2204. MaxCodexVersion: updatedSettings.MaxCodexVersion,
  2205. CodexCLIOnlyBlacklist: updatedSettings.CodexCLIOnlyBlacklist,
  2206. CodexCLIOnlyWhitelist: updatedSettings.CodexCLIOnlyWhitelist,
  2207. CodexCLIOnlyAllowAppServerClients: updatedSettings.CodexCLIOnlyAllowAppServerClients,
  2208. CodexCLIOnlyEngineFingerprintSignals: updatedSettings.CodexCLIOnlyEngineFingerprintSignals,
  2209. PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
  2210. PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
  2211. PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
  2212. PaymentVisibleMethodWxpayEnabled: updatedSettings.PaymentVisibleMethodWxpayEnabled,
  2213. OpenAILowUpstreamRatePriorityEnabled: updatedSettings.OpenAILowUpstreamRatePriorityEnabled,
  2214. OpenAIOAuthSchedulingRateMultiplier: updatedSettings.OpenAIOAuthSchedulingRateMultiplier,
  2215. OpenAIAdvancedSchedulerEnabled: updatedSettings.OpenAIAdvancedSchedulerEnabled,
  2216. OpenAIAdvancedSchedulerStickyWeightedEnabled: updatedSettings.OpenAIAdvancedSchedulerStickyWeightedEnabled,
  2217. OpenAIAdvancedSchedulerSubscriptionPriorityEnabled: updatedSettings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled,
  2218. OpenAIAdvancedSchedulerLBTopK: updatedSettings.OpenAIAdvancedSchedulerLBTopK,
  2219. OpenAIAdvancedSchedulerWeightPriority: updatedSettings.OpenAIAdvancedSchedulerWeightPriority,
  2220. OpenAIAdvancedSchedulerWeightLoad: updatedSettings.OpenAIAdvancedSchedulerWeightLoad,
  2221. OpenAIAdvancedSchedulerWeightQueue: updatedSettings.OpenAIAdvancedSchedulerWeightQueue,
  2222. OpenAIAdvancedSchedulerWeightErrorRate: updatedSettings.OpenAIAdvancedSchedulerWeightErrorRate,
  2223. OpenAIAdvancedSchedulerWeightTTFT: updatedSettings.OpenAIAdvancedSchedulerWeightTTFT,
  2224. OpenAIAdvancedSchedulerWeightReset: updatedSettings.OpenAIAdvancedSchedulerWeightReset,
  2225. OpenAIAdvancedSchedulerWeightQuotaHeadroom: updatedSettings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
  2226. OpenAIAdvancedSchedulerWeightUpstreamCost: updatedSettings.OpenAIAdvancedSchedulerWeightUpstreamCost,
  2227. OpenAIAdvancedSchedulerWeightPreviousResponse: updatedSettings.OpenAIAdvancedSchedulerWeightPreviousResponse,
  2228. OpenAIAdvancedSchedulerWeightSessionSticky: updatedSettings.OpenAIAdvancedSchedulerWeightSessionSticky,
  2229. OpenAIAdvancedSchedulerEffectiveLBTopK: updatedSettings.OpenAIAdvancedSchedulerEffectiveLBTopK,
  2230. OpenAIAdvancedSchedulerEffectiveWeightPriority: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightPriority,
  2231. OpenAIAdvancedSchedulerEffectiveWeightLoad: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightLoad,
  2232. OpenAIAdvancedSchedulerEffectiveWeightQueue: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightQueue,
  2233. OpenAIAdvancedSchedulerEffectiveWeightErrorRate: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightErrorRate,
  2234. OpenAIAdvancedSchedulerEffectiveWeightTTFT: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightTTFT,
  2235. OpenAIAdvancedSchedulerEffectiveWeightReset: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightReset,
  2236. OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom,
  2237. OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost,
  2238. OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse,
  2239. OpenAIAdvancedSchedulerEffectiveWeightSessionSticky: updatedSettings.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky,
  2240. BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
  2241. BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
  2242. BalanceLowNotifyRechargeURL: updatedSettings.BalanceLowNotifyRechargeURL,
  2243. SubscriptionExpiryNotifyEnabled: updatedSettings.SubscriptionExpiryNotifyEnabled,
  2244. AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled,
  2245. AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails),
  2246. PaymentEnabled: updatedPaymentCfg.Enabled,
  2247. PaymentMinAmount: updatedPaymentCfg.MinAmount,
  2248. PaymentMaxAmount: updatedPaymentCfg.MaxAmount,
  2249. PaymentDailyLimit: updatedPaymentCfg.DailyLimit,
  2250. PaymentOrderTimeoutMin: updatedPaymentCfg.OrderTimeoutMin,
  2251. PaymentMaxPendingOrders: updatedPaymentCfg.MaxPendingOrders,
  2252. PaymentEnabledTypes: updatedPaymentCfg.EnabledTypes,
  2253. PaymentBalanceDisabled: updatedPaymentCfg.BalanceDisabled,
  2254. PaymentBalanceRechargeMultiplier: updatedPaymentCfg.BalanceRechargeMultiplier,
  2255. PaymentSubscriptionUSDToCNYRate: updatedPaymentCfg.SubscriptionUSDToCNYRate,
  2256. PaymentRechargeFeeRate: updatedPaymentCfg.RechargeFeeRate,
  2257. PaymentLoadBalanceStrat: updatedPaymentCfg.LoadBalanceStrategy,
  2258. PaymentProductNamePrefix: updatedPaymentCfg.ProductNamePrefix,
  2259. PaymentProductNameSuffix: updatedPaymentCfg.ProductNameSuffix,
  2260. PaymentHelpImageURL: updatedPaymentCfg.HelpImageURL,
  2261. PaymentHelpText: updatedPaymentCfg.HelpText,
  2262. PaymentCancelRateLimitEnabled: updatedPaymentCfg.CancelRateLimitEnabled,
  2263. PaymentCancelRateLimitMax: updatedPaymentCfg.CancelRateLimitMax,
  2264. PaymentCancelRateLimitWindow: updatedPaymentCfg.CancelRateLimitWindow,
  2265. PaymentCancelRateLimitUnit: updatedPaymentCfg.CancelRateLimitUnit,
  2266. PaymentCancelRateLimitMode: updatedPaymentCfg.CancelRateLimitMode,
  2267. PaymentAlipayForceQRCode: updatedPaymentCfg.AlipayForceQRCode,
  2268. PaymentAlipayMobilePrecreateDeepLink: updatedPaymentCfg.AlipayMobilePrecreateDeepLink,
  2269. ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled,
  2270. ChannelMonitorMode: updatedSettings.ChannelMonitorMode,
  2271. ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
  2272. ChannelMonitorHideThroughput: updatedSettings.ChannelMonitorHideThroughput,
  2273. ChannelMonitorShowQuota: updatedSettings.ChannelMonitorShowQuota,
  2274. ChannelMonitorHideUserRanking: updatedSettings.ChannelMonitorHideUserRanking,
  2275. GrokDefaultTextModel: updatedSettings.GrokDefaultTextModel,
  2276. GrokCrossClientModelMapEnabled: updatedSettings.GrokCrossClientModelMapEnabled,
  2277. GrokDefaultBaseURLMode: updatedSettings.GrokDefaultBaseURLMode,
  2278. AvailableChannelsEnabled: updatedSettings.AvailableChannelsEnabled,
  2279. ModelPlazaEnabled: updatedSettings.ModelPlazaEnabled,
  2280. ModelPlazaRequireAuth: updatedSettings.ModelPlazaRequireAuth,
  2281. ModelPlazaDescription: updatedSettings.ModelPlazaDescription,
  2282. PluginManagementEnabled: updatedSettings.PluginManagementEnabled,
  2283. AffiliateEnabled: updatedSettings.AffiliateEnabled,
  2284. RiskControlEnabled: updatedSettings.RiskControlEnabled,
  2285. CyberSessionBlockEnabled: updatedSettings.CyberSessionBlockEnabled,
  2286. CyberSessionBlockTTLSeconds: updatedSettings.CyberSessionBlockTTLSeconds,
  2287. AccountSchedulingThresholds: updatedSettings.AccountSchedulingThresholds,
  2288. AllowUserViewErrorRequests: updatedSettings.AllowUserViewErrorRequests,
  2289. }
  2290. if fastPolicy, err := h.settingService.GetOpenAIFastPolicySettings(c.Request.Context()); err != nil {
  2291. slog.Error("openai_fast_policy_settings_get_failed", "error", err)
  2292. } else if fastPolicy != nil {
  2293. payload.OpenAIFastPolicySettings = openaiFastPolicySettingsToDTO(fastPolicy)
  2294. }
  2295. // Default platform quotas(JSON map)—— 与 GetSettings 一致,避免保存后响应缺失该字段
  2296. if platformQuotas, err := h.settingService.GetDefaultPlatformQuotas(c.Request.Context()); err != nil {
  2297. slog.Error("default_platform_quotas_get_failed", "error", err)
  2298. } else {
  2299. payload.DefaultPlatformQuotas = platformQuotas
  2300. }
  2301. response.Success(c, systemSettingsResponseData(payload, updatedAuthSourceDefaults))
  2302. }
  2303. // hasPaymentFields returns true if any payment-related field was explicitly provided.
  2304. // mapDingTalkValidateError maps ValidateDingTalkConfig errors to machine-readable reason codes.
  2305. func mapDingTalkValidateError(err error) string {
  2306. switch {
  2307. case errors.Is(err, config.ErrDingTalkV1AppTypeMismatch):
  2308. return "dingtalk_apptype_mismatch"
  2309. case errors.Is(err, config.ErrDingTalkV4InvalidAppKind):
  2310. return "dingtalk_app_kind_invalid"
  2311. default:
  2312. return "dingtalk_corp_config_invalid"
  2313. }
  2314. }
  2315. func hasPaymentFields(req UpdateSettingsRequest) bool {
  2316. return req.PaymentEnabled != nil || req.PaymentMinAmount != nil ||
  2317. req.PaymentMaxAmount != nil || req.PaymentDailyLimit != nil ||
  2318. req.PaymentOrderTimeoutMin != nil || req.PaymentMaxPendingOrders != nil ||
  2319. req.PaymentEnabledTypes != nil || req.PaymentBalanceDisabled != nil ||
  2320. req.PaymentBalanceRechargeMultiplier != nil || req.PaymentSubscriptionUSDToCNYRate != nil ||
  2321. req.PaymentRechargeFeeRate != nil ||
  2322. req.PaymentLoadBalanceStrat != nil || req.PaymentProductNamePrefix != nil ||
  2323. req.PaymentProductNameSuffix != nil || req.PaymentHelpImageURL != nil ||
  2324. req.PaymentHelpText != nil || req.PaymentCancelRateLimitEnabled != nil ||
  2325. req.PaymentCancelRateLimitMax != nil || req.PaymentCancelRateLimitWindow != nil ||
  2326. req.PaymentCancelRateLimitUnit != nil || req.PaymentCancelRateLimitMode != nil ||
  2327. req.PaymentAlipayForceQRCode != nil || req.PaymentAlipayMobilePrecreateDeepLink != nil
  2328. }
  2329. // ensureDingTalkSyncAttributes 在保存 settings 后,按 admin 配置的 (attr key, attr name)
  2330. // 兜底 upsert 对应 user attribute definition:不存在则创建;存在但 name 不同则更新 name
  2331. // (type/options/required 不变)。仅 internal_only + 对应 sync 开关开启时执行。
  2332. // 失败仅记录日志,不阻塞 settings 保存。
  2333. func (h *SettingHandler) ensureDingTalkSyncAttributes(ctx context.Context, settings *service.SystemSettings) {
  2334. if h.userAttributeService == nil || settings == nil {
  2335. return
  2336. }
  2337. if settings.DingTalkConnectCorpRestrictionPolicy != "internal_only" {
  2338. return
  2339. }
  2340. if settings.DingTalkConnectSyncDisplayName {
  2341. h.ensureUserAttributeDefinition(ctx, settings.DingTalkConnectSyncDisplayNameAttrKey, settings.DingTalkConnectSyncDisplayNameAttrName, "钉钉 internal_only 登录时同步的钉钉姓名", service.AttributeTypeText)
  2342. }
  2343. if settings.DingTalkConnectSyncCorpEmail {
  2344. h.ensureUserAttributeDefinition(ctx, settings.DingTalkConnectSyncCorpEmailAttrKey, settings.DingTalkConnectSyncCorpEmailAttrName, "钉钉 internal_only 登录时同步的企业邮箱", service.AttributeTypeEmail)
  2345. }
  2346. if settings.DingTalkConnectSyncDept {
  2347. h.ensureUserAttributeDefinition(ctx, settings.DingTalkConnectSyncDeptAttrKey, settings.DingTalkConnectSyncDeptAttrName, "钉钉 internal_only 登录时同步的完整部门路径(如:公司/研发部)", service.AttributeTypeText)
  2348. }
  2349. }
  2350. func (h *SettingHandler) ensureUserAttributeDefinition(ctx context.Context, key, name, description string, attrType service.UserAttributeType) {
  2351. key = strings.TrimSpace(key)
  2352. if key == "" {
  2353. return
  2354. }
  2355. existing, err := h.userAttributeService.GetDefinitionByKey(ctx, key)
  2356. if err == nil && existing != nil {
  2357. if strings.TrimSpace(name) != "" && existing.Name != name {
  2358. if _, err := h.userAttributeService.UpdateDefinition(ctx, existing.ID, service.UpdateAttributeDefinitionInput{
  2359. Name: &name,
  2360. }); err != nil {
  2361. slog.Warn("dingtalk: update user attribute definition name failed", "key", key, "err", err.Error())
  2362. return
  2363. }
  2364. slog.Info("dingtalk: updated user attribute definition name", "key", key, "name", name)
  2365. }
  2366. return
  2367. }
  2368. if _, err := h.userAttributeService.CreateDefinition(ctx, service.CreateAttributeDefinitionInput{
  2369. Key: key,
  2370. Name: name,
  2371. Description: description,
  2372. Type: attrType,
  2373. Enabled: true,
  2374. }); err != nil {
  2375. slog.Warn("dingtalk: ensure user attribute definition failed", "key", key, "err", err.Error())
  2376. return
  2377. }
  2378. slog.Info("dingtalk: created user attribute definition", "key", key, "name", name, "type", attrType)
  2379. }