feat: 增加 GitHub 和 Google 邮箱快捷登录

This commit is contained in:
lyen1688
2026-05-06 16:06:11 +08:00
parent a1106e8167
commit af550fa64e
35 changed files with 2656 additions and 74 deletions
+267 -1
View File
@@ -129,6 +129,8 @@ type AuthSourceDefaultSettings struct {
LinuxDo ProviderDefaultGrantSettings
OIDC ProviderDefaultGrantSettings
WeChat ProviderDefaultGrantSettings
GitHub ProviderDefaultGrantSettings
Google ProviderDefaultGrantSettings
ForceEmailOnThirdPartySignup bool
}
@@ -169,6 +171,20 @@ var (
grantOnSignup: SettingKeyAuthSourceDefaultWeChatGrantOnSignup,
grantOnFirstBind: SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind,
}
gitHubAuthSourceDefaultKeys = authSourceDefaultKeySet{
balance: SettingKeyAuthSourceDefaultGitHubBalance,
concurrency: SettingKeyAuthSourceDefaultGitHubConcurrency,
subscriptions: SettingKeyAuthSourceDefaultGitHubSubscriptions,
grantOnSignup: SettingKeyAuthSourceDefaultGitHubGrantOnSignup,
grantOnFirstBind: SettingKeyAuthSourceDefaultGitHubGrantOnFirstBind,
}
googleAuthSourceDefaultKeys = authSourceDefaultKeySet{
balance: SettingKeyAuthSourceDefaultGoogleBalance,
concurrency: SettingKeyAuthSourceDefaultGoogleConcurrency,
subscriptions: SettingKeyAuthSourceDefaultGoogleSubscriptions,
grantOnSignup: SettingKeyAuthSourceDefaultGoogleGrantOnSignup,
grantOnFirstBind: SettingKeyAuthSourceDefaultGoogleGrantOnFirstBind,
}
)
const (
@@ -177,6 +193,17 @@ const (
defaultWeChatConnectMode = "open"
defaultWeChatConnectScopes = "snsapi_login"
defaultWeChatConnectFrontend = "/auth/wechat/callback"
defaultGitHubOAuthAuthorize = "https://github.com/login/oauth/authorize"
defaultGitHubOAuthToken = "https://github.com/login/oauth/access_token"
defaultGitHubOAuthUserInfo = "https://api.github.com/user"
defaultGitHubOAuthEmails = "https://api.github.com/user/emails"
defaultGitHubOAuthScopes = "read:user user:email"
defaultGitHubOAuthFrontend = "/auth/oauth/callback"
defaultGoogleOAuthAuthorize = "https://accounts.google.com/o/oauth2/v2/auth"
defaultGoogleOAuthToken = "https://oauth2.googleapis.com/token"
defaultGoogleOAuthUserInfo = "https://openidconnect.googleapis.com/v1/userinfo"
defaultGoogleOAuthScopes = "openid email profile"
defaultGoogleOAuthFrontend = "/auth/oauth/callback"
)
func normalizeWeChatConnectModeSetting(raw string) string {
@@ -448,6 +475,12 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingPaymentEnabled,
SettingKeyOIDCConnectEnabled,
SettingKeyOIDCConnectProviderName,
SettingKeyGitHubOAuthEnabled,
SettingKeyGitHubOAuthClientID,
SettingKeyGitHubOAuthClientSecret,
SettingKeyGoogleOAuthEnabled,
SettingKeyGoogleOAuthClientID,
SettingKeyGoogleOAuthClientSecret,
SettingKeyBalanceLowNotifyEnabled,
SettingKeyBalanceLowNotifyThreshold,
SettingKeyBalanceLowNotifyRechargeURL,
@@ -482,6 +515,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
if oidcProviderName == "" {
oidcProviderName = "OIDC"
}
gitHubEnabled := s.emailOAuthPublicEnabled(settings, "github")
googleEnabled := s.emailOAuthPublicEnabled(settings, "google")
weChatEnabled, weChatOpenEnabled, weChatMPEnabled, weChatMobileEnabled := s.weChatOAuthCapabilitiesFromSettings(settings)
// Password reset requires email verification to be enabled
@@ -534,6 +569,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
PaymentEnabled: settings[SettingPaymentEnabled] == "true",
OIDCOAuthEnabled: oidcEnabled,
OIDCOAuthProviderName: oidcProviderName,
GitHubOAuthEnabled: gitHubEnabled,
GoogleOAuthEnabled: googleEnabled,
BalanceLowNotifyEnabled: settings[SettingKeyBalanceLowNotifyEnabled] == "true",
AccountQuotaNotifyEnabled: settings[SettingKeyAccountQuotaNotifyEnabled] == "true",
BalanceLowNotifyThreshold: balanceLowNotifyThreshold,
@@ -677,6 +714,8 @@ type PublicSettingsInjectionPayload struct {
WeChatOAuthMobileEnabled bool `json:"wechat_oauth_mobile_enabled"`
OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"`
OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"`
GitHubOAuthEnabled bool `json:"github_oauth_enabled"`
GoogleOAuthEnabled bool `json:"google_oauth_enabled"`
BackendModeEnabled bool `json:"backend_mode_enabled"`
PaymentEnabled bool `json:"payment_enabled"`
Version string `json:"version"`
@@ -733,6 +772,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
WeChatOAuthMobileEnabled: settings.WeChatOAuthMobileEnabled,
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
GitHubOAuthEnabled: settings.GitHubOAuthEnabled,
GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
BackendModeEnabled: settings.BackendModeEnabled,
PaymentEnabled: settings.PaymentEnabled,
Version: s.version,
@@ -806,6 +847,98 @@ func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string
return openReady || mpReady, openReady, mpReady, mobileReady
}
func (s *SettingService) emailOAuthBaseConfig(provider string) config.EmailOAuthProviderConfig {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "github":
cfg := config.EmailOAuthProviderConfig{
AuthorizeURL: defaultGitHubOAuthAuthorize,
TokenURL: defaultGitHubOAuthToken,
UserInfoURL: defaultGitHubOAuthUserInfo,
EmailsURL: defaultGitHubOAuthEmails,
Scopes: defaultGitHubOAuthScopes,
FrontendRedirectURL: defaultGitHubOAuthFrontend,
}
if s != nil && s.cfg != nil {
cfg = mergeEmailOAuthBaseConfig(cfg, s.cfg.GitHubOAuth)
}
return cfg
case "google":
cfg := config.EmailOAuthProviderConfig{
AuthorizeURL: defaultGoogleOAuthAuthorize,
TokenURL: defaultGoogleOAuthToken,
UserInfoURL: defaultGoogleOAuthUserInfo,
Scopes: defaultGoogleOAuthScopes,
FrontendRedirectURL: defaultGoogleOAuthFrontend,
}
if s != nil && s.cfg != nil {
cfg = mergeEmailOAuthBaseConfig(cfg, s.cfg.GoogleOAuth)
}
return cfg
default:
return config.EmailOAuthProviderConfig{}
}
}
func mergeEmailOAuthBaseConfig(base, override config.EmailOAuthProviderConfig) config.EmailOAuthProviderConfig {
base.Enabled = override.Enabled
if strings.TrimSpace(override.ClientID) != "" {
base.ClientID = strings.TrimSpace(override.ClientID)
}
if strings.TrimSpace(override.ClientSecret) != "" {
base.ClientSecret = strings.TrimSpace(override.ClientSecret)
}
if strings.TrimSpace(override.AuthorizeURL) != "" {
base.AuthorizeURL = strings.TrimSpace(override.AuthorizeURL)
}
if strings.TrimSpace(override.TokenURL) != "" {
base.TokenURL = strings.TrimSpace(override.TokenURL)
}
if strings.TrimSpace(override.UserInfoURL) != "" {
base.UserInfoURL = strings.TrimSpace(override.UserInfoURL)
}
if strings.TrimSpace(override.EmailsURL) != "" {
base.EmailsURL = strings.TrimSpace(override.EmailsURL)
}
if strings.TrimSpace(override.Scopes) != "" {
base.Scopes = strings.TrimSpace(override.Scopes)
}
if strings.TrimSpace(override.RedirectURL) != "" {
base.RedirectURL = strings.TrimSpace(override.RedirectURL)
}
if strings.TrimSpace(override.FrontendRedirectURL) != "" {
base.FrontendRedirectURL = strings.TrimSpace(override.FrontendRedirectURL)
}
return base
}
func (s *SettingService) emailOAuthPublicEnabled(settings map[string]string, provider string) bool {
cfg := s.effectiveEmailOAuthConfig(settings, provider)
return cfg.Enabled && strings.TrimSpace(cfg.ClientID) != "" && strings.TrimSpace(cfg.ClientSecret) != ""
}
func (s *SettingService) effectiveEmailOAuthConfig(settings map[string]string, provider string) config.EmailOAuthProviderConfig {
cfg := s.emailOAuthBaseConfig(provider)
switch strings.ToLower(strings.TrimSpace(provider)) {
case "github":
if raw, ok := settings[SettingKeyGitHubOAuthEnabled]; ok {
cfg.Enabled = raw == "true"
}
cfg.ClientID = firstNonEmpty(settings[SettingKeyGitHubOAuthClientID], cfg.ClientID)
cfg.ClientSecret = firstNonEmpty(settings[SettingKeyGitHubOAuthClientSecret], cfg.ClientSecret)
cfg.RedirectURL = firstNonEmpty(settings[SettingKeyGitHubOAuthRedirectURL], cfg.RedirectURL)
cfg.FrontendRedirectURL = firstNonEmpty(settings[SettingKeyGitHubOAuthFrontendRedirectURL], cfg.FrontendRedirectURL, defaultGitHubOAuthFrontend)
case "google":
if raw, ok := settings[SettingKeyGoogleOAuthEnabled]; ok {
cfg.Enabled = raw == "true"
}
cfg.ClientID = firstNonEmpty(settings[SettingKeyGoogleOAuthClientID], cfg.ClientID)
cfg.ClientSecret = firstNonEmpty(settings[SettingKeyGoogleOAuthClientSecret], cfg.ClientSecret)
cfg.RedirectURL = firstNonEmpty(settings[SettingKeyGoogleOAuthRedirectURL], cfg.RedirectURL)
cfg.FrontendRedirectURL = firstNonEmpty(settings[SettingKeyGoogleOAuthFrontendRedirectURL], cfg.FrontendRedirectURL, defaultGoogleOAuthFrontend)
}
return cfg
}
// filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON
// array string, returning only items with visibility != "admin".
func filterUserVisibleMenuItems(raw string) json.RawMessage {
@@ -1052,6 +1185,16 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
if settings.WeChatConnectFrontendRedirectURL == "" {
settings.WeChatConnectFrontendRedirectURL = defaultWeChatConnectFrontend
}
settings.GitHubOAuthRedirectURL = strings.TrimSpace(settings.GitHubOAuthRedirectURL)
settings.GitHubOAuthFrontendRedirectURL = strings.TrimSpace(settings.GitHubOAuthFrontendRedirectURL)
if settings.GitHubOAuthFrontendRedirectURL == "" {
settings.GitHubOAuthFrontendRedirectURL = defaultGitHubOAuthFrontend
}
settings.GoogleOAuthRedirectURL = strings.TrimSpace(settings.GoogleOAuthRedirectURL)
settings.GoogleOAuthFrontendRedirectURL = strings.TrimSpace(settings.GoogleOAuthFrontendRedirectURL)
if settings.GoogleOAuthFrontendRedirectURL == "" {
settings.GoogleOAuthFrontendRedirectURL = defaultGoogleOAuthFrontend
}
updates := make(map[string]string)
@@ -1121,6 +1264,22 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingKeyOIDCConnectClientSecret] = settings.OIDCConnectClientSecret
}
// GitHub / Google 邮箱快捷登录
updates[SettingKeyGitHubOAuthEnabled] = strconv.FormatBool(settings.GitHubOAuthEnabled)
updates[SettingKeyGitHubOAuthClientID] = strings.TrimSpace(settings.GitHubOAuthClientID)
updates[SettingKeyGitHubOAuthRedirectURL] = settings.GitHubOAuthRedirectURL
updates[SettingKeyGitHubOAuthFrontendRedirectURL] = settings.GitHubOAuthFrontendRedirectURL
if settings.GitHubOAuthClientSecret != "" {
updates[SettingKeyGitHubOAuthClientSecret] = strings.TrimSpace(settings.GitHubOAuthClientSecret)
}
updates[SettingKeyGoogleOAuthEnabled] = strconv.FormatBool(settings.GoogleOAuthEnabled)
updates[SettingKeyGoogleOAuthClientID] = strings.TrimSpace(settings.GoogleOAuthClientID)
updates[SettingKeyGoogleOAuthRedirectURL] = settings.GoogleOAuthRedirectURL
updates[SettingKeyGoogleOAuthFrontendRedirectURL] = settings.GoogleOAuthFrontendRedirectURL
if settings.GoogleOAuthClientSecret != "" {
updates[SettingKeyGoogleOAuthClientSecret] = strings.TrimSpace(settings.GoogleOAuthClientSecret)
}
// WeChat Connect OAuth 登录
updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled)
updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID
@@ -1273,17 +1432,21 @@ func (s *SettingService) buildAuthSourceDefaultUpdates(ctx context.Context, sett
settings.LinuxDo.Subscriptions,
settings.OIDC.Subscriptions,
settings.WeChat.Subscriptions,
settings.GitHub.Subscriptions,
settings.Google.Subscriptions,
} {
if err := s.validateDefaultSubscriptionGroups(ctx, subscriptions); err != nil {
return nil, err
}
}
updates := make(map[string]string, 21)
updates := make(map[string]string, 31)
writeProviderDefaultGrantUpdates(updates, emailAuthSourceDefaultKeys, settings.Email)
writeProviderDefaultGrantUpdates(updates, linuxDoAuthSourceDefaultKeys, settings.LinuxDo)
writeProviderDefaultGrantUpdates(updates, oidcAuthSourceDefaultKeys, settings.OIDC)
writeProviderDefaultGrantUpdates(updates, weChatAuthSourceDefaultKeys, settings.WeChat)
writeProviderDefaultGrantUpdates(updates, gitHubAuthSourceDefaultKeys, settings.GitHub)
writeProviderDefaultGrantUpdates(updates, googleAuthSourceDefaultKeys, settings.Google)
updates[SettingKeyForceEmailOnThirdPartySignup] = strconv.FormatBool(settings.ForceEmailOnThirdPartySignup)
return updates, nil
}
@@ -1362,6 +1525,61 @@ func (s *SettingService) validateDefaultSubscriptionGroups(ctx context.Context,
return nil
}
func (s *SettingService) GetEmailOAuthProviderConfig(ctx context.Context, provider string) (config.EmailOAuthProviderConfig, error) {
provider = strings.ToLower(strings.TrimSpace(provider))
if provider != "github" && provider != "google" {
return config.EmailOAuthProviderConfig{}, infraerrors.NotFound("OAUTH_PROVIDER_NOT_FOUND", "oauth provider not found")
}
keys := []string{
SettingKeyGitHubOAuthEnabled,
SettingKeyGitHubOAuthClientID,
SettingKeyGitHubOAuthClientSecret,
SettingKeyGitHubOAuthRedirectURL,
SettingKeyGitHubOAuthFrontendRedirectURL,
SettingKeyGoogleOAuthEnabled,
SettingKeyGoogleOAuthClientID,
SettingKeyGoogleOAuthClientSecret,
SettingKeyGoogleOAuthRedirectURL,
SettingKeyGoogleOAuthFrontendRedirectURL,
}
settings, err := s.settingRepo.GetMultiple(ctx, keys)
if err != nil {
return config.EmailOAuthProviderConfig{}, fmt.Errorf("get email oauth settings: %w", err)
}
cfg := s.effectiveEmailOAuthConfig(settings, provider)
if !cfg.Enabled {
return config.EmailOAuthProviderConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
}
if strings.TrimSpace(cfg.ClientID) == "" {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client id not configured")
}
if strings.TrimSpace(cfg.ClientSecret) == "" {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client secret not configured")
}
for label, rawURL := range map[string]string{
"authorize": cfg.AuthorizeURL,
"token": cfg.TokenURL,
"userinfo": cfg.UserInfoURL,
"redirect": cfg.RedirectURL,
} {
if strings.TrimSpace(rawURL) == "" {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth "+label+" url not configured")
}
if err := config.ValidateAbsoluteHTTPURL(rawURL); err != nil {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth "+label+" url invalid")
}
}
if strings.TrimSpace(cfg.EmailsURL) != "" {
if err := config.ValidateAbsoluteHTTPURL(cfg.EmailsURL); err != nil {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth emails url invalid")
}
}
if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
return config.EmailOAuthProviderConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url invalid")
}
return cfg, nil
}
// IsRegistrationEnabled 检查是否开放注册
func (s *SettingService) IsRegistrationEnabled(ctx context.Context) bool {
value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEnabled)
@@ -1711,6 +1929,16 @@ func (s *SettingService) GetAuthSourceDefaultSettings(ctx context.Context) (*Aut
SettingKeyAuthSourceDefaultWeChatSubscriptions,
SettingKeyAuthSourceDefaultWeChatGrantOnSignup,
SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind,
SettingKeyAuthSourceDefaultGitHubBalance,
SettingKeyAuthSourceDefaultGitHubConcurrency,
SettingKeyAuthSourceDefaultGitHubSubscriptions,
SettingKeyAuthSourceDefaultGitHubGrantOnSignup,
SettingKeyAuthSourceDefaultGitHubGrantOnFirstBind,
SettingKeyAuthSourceDefaultGoogleBalance,
SettingKeyAuthSourceDefaultGoogleConcurrency,
SettingKeyAuthSourceDefaultGoogleSubscriptions,
SettingKeyAuthSourceDefaultGoogleGrantOnSignup,
SettingKeyAuthSourceDefaultGoogleGrantOnFirstBind,
SettingKeyForceEmailOnThirdPartySignup,
}
@@ -1724,6 +1952,8 @@ func (s *SettingService) GetAuthSourceDefaultSettings(ctx context.Context) (*Aut
LinuxDo: parseProviderDefaultGrantSettings(settings, linuxDoAuthSourceDefaultKeys),
OIDC: parseProviderDefaultGrantSettings(settings, oidcAuthSourceDefaultKeys),
WeChat: parseProviderDefaultGrantSettings(settings, weChatAuthSourceDefaultKeys),
GitHub: parseProviderDefaultGrantSettings(settings, gitHubAuthSourceDefaultKeys),
Google: parseProviderDefaultGrantSettings(settings, googleAuthSourceDefaultKeys),
ForceEmailOnThirdPartySignup: settings[SettingKeyForceEmailOnThirdPartySignup] == "true",
}, nil
}
@@ -1824,6 +2054,16 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyWeChatConnectScopes: "snsapi_login",
SettingKeyWeChatConnectRedirectURL: "",
SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend,
SettingKeyGitHubOAuthEnabled: "false",
SettingKeyGitHubOAuthClientID: "",
SettingKeyGitHubOAuthClientSecret: "",
SettingKeyGitHubOAuthRedirectURL: "",
SettingKeyGitHubOAuthFrontendRedirectURL: defaultGitHubOAuthFrontend,
SettingKeyGoogleOAuthEnabled: "false",
SettingKeyGoogleOAuthClientID: "",
SettingKeyGoogleOAuthClientSecret: "",
SettingKeyGoogleOAuthRedirectURL: "",
SettingKeyGoogleOAuthFrontendRedirectURL: defaultGoogleOAuthFrontend,
SettingKeyOIDCConnectEnabled: "false",
SettingKeyOIDCConnectProviderName: "OIDC",
SettingKeyOIDCConnectClientID: "",
@@ -1874,6 +2114,16 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyAuthSourceDefaultWeChatSubscriptions: "[]",
SettingKeyAuthSourceDefaultWeChatGrantOnSignup: "false",
SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind: "false",
SettingKeyAuthSourceDefaultGitHubBalance: "0",
SettingKeyAuthSourceDefaultGitHubConcurrency: "5",
SettingKeyAuthSourceDefaultGitHubSubscriptions: "[]",
SettingKeyAuthSourceDefaultGitHubGrantOnSignup: "false",
SettingKeyAuthSourceDefaultGitHubGrantOnFirstBind: "false",
SettingKeyAuthSourceDefaultGoogleBalance: "0",
SettingKeyAuthSourceDefaultGoogleConcurrency: "5",
SettingKeyAuthSourceDefaultGoogleSubscriptions: "[]",
SettingKeyAuthSourceDefaultGoogleGrantOnSignup: "false",
SettingKeyAuthSourceDefaultGoogleGrantOnFirstBind: "false",
SettingKeyForceEmailOnThirdPartySignup: "false",
SettingKeySMTPPort: "587",
SettingKeySMTPUseTLS: "false",
@@ -2173,6 +2423,22 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
}
result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != ""
gitHubEffective := s.effectiveEmailOAuthConfig(settings, "github")
result.GitHubOAuthEnabled = gitHubEffective.Enabled
result.GitHubOAuthClientID = strings.TrimSpace(gitHubEffective.ClientID)
result.GitHubOAuthClientSecret = strings.TrimSpace(gitHubEffective.ClientSecret)
result.GitHubOAuthClientSecretConfigured = result.GitHubOAuthClientSecret != ""
result.GitHubOAuthRedirectURL = strings.TrimSpace(gitHubEffective.RedirectURL)
result.GitHubOAuthFrontendRedirectURL = strings.TrimSpace(gitHubEffective.FrontendRedirectURL)
googleEffective := s.effectiveEmailOAuthConfig(settings, "google")
result.GoogleOAuthEnabled = googleEffective.Enabled
result.GoogleOAuthClientID = strings.TrimSpace(googleEffective.ClientID)
result.GoogleOAuthClientSecret = strings.TrimSpace(googleEffective.ClientSecret)
result.GoogleOAuthClientSecretConfigured = result.GoogleOAuthClientSecret != ""
result.GoogleOAuthRedirectURL = strings.TrimSpace(googleEffective.RedirectURL)
result.GoogleOAuthFrontendRedirectURL = strings.TrimSpace(googleEffective.FrontendRedirectURL)
// WeChat Connect 设置:
// - 优先读取 DB 系统设置
// - 缺失时回退到 config/env,保持升级兼容