Unverified Commit edf215e6 authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #409 from DuckyProject/feat/purchase-subscription-iframe

feat(purchase): 增加购买订阅 iframe 页面与配置
parents e12dd079 04a509d4
...@@ -73,6 +73,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { ...@@ -73,6 +73,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
DocURL: settings.DocURL, DocURL: settings.DocURL,
HomeContent: settings.HomeContent, HomeContent: settings.HomeContent,
HideCcsImportButton: settings.HideCcsImportButton, HideCcsImportButton: settings.HideCcsImportButton,
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
DefaultConcurrency: settings.DefaultConcurrency, DefaultConcurrency: settings.DefaultConcurrency,
DefaultBalance: settings.DefaultBalance, DefaultBalance: settings.DefaultBalance,
EnableModelFallback: settings.EnableModelFallback, EnableModelFallback: settings.EnableModelFallback,
...@@ -119,14 +121,16 @@ type UpdateSettingsRequest struct { ...@@ -119,14 +121,16 @@ type UpdateSettingsRequest struct {
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"` LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
// OEM设置 // OEM设置
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
SiteLogo string `json:"site_logo"` SiteLogo string `json:"site_logo"`
SiteSubtitle string `json:"site_subtitle"` SiteSubtitle string `json:"site_subtitle"`
APIBaseURL string `json:"api_base_url"` APIBaseURL string `json:"api_base_url"`
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocURL string `json:"doc_url"` DocURL string `json:"doc_url"`
HomeContent string `json:"home_content"` HomeContent string `json:"home_content"`
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
// 默认配置 // 默认配置
DefaultConcurrency int `json:"default_concurrency"` DefaultConcurrency int `json:"default_concurrency"`
...@@ -242,6 +246,34 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -242,6 +246,34 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
} }
} }
// “购买订阅”页面配置验证
purchaseEnabled := previousSettings.PurchaseSubscriptionEnabled
if req.PurchaseSubscriptionEnabled != nil {
purchaseEnabled = *req.PurchaseSubscriptionEnabled
}
purchaseURL := previousSettings.PurchaseSubscriptionURL
if req.PurchaseSubscriptionURL != nil {
purchaseURL = strings.TrimSpace(*req.PurchaseSubscriptionURL)
}
// - 启用时要求 URL 合法且非空
// - 禁用时允许为空;若提供了 URL 也做基本校验,避免误配置
if purchaseEnabled {
if purchaseURL == "" {
response.BadRequest(c, "Purchase Subscription URL is required when enabled")
return
}
if err := config.ValidateAbsoluteHTTPURL(purchaseURL); err != nil {
response.BadRequest(c, "Purchase Subscription URL must be an absolute http(s) URL")
return
}
} else if purchaseURL != "" {
if err := config.ValidateAbsoluteHTTPURL(purchaseURL); err != nil {
response.BadRequest(c, "Purchase Subscription URL must be an absolute http(s) URL")
return
}
}
// Ops metrics collector interval validation (seconds). // Ops metrics collector interval validation (seconds).
if req.OpsMetricsIntervalSeconds != nil { if req.OpsMetricsIntervalSeconds != nil {
v := *req.OpsMetricsIntervalSeconds v := *req.OpsMetricsIntervalSeconds
...@@ -255,42 +287,44 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -255,42 +287,44 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
} }
settings := &service.SystemSettings{ settings := &service.SystemSettings{
RegistrationEnabled: req.RegistrationEnabled, RegistrationEnabled: req.RegistrationEnabled,
EmailVerifyEnabled: req.EmailVerifyEnabled, EmailVerifyEnabled: req.EmailVerifyEnabled,
PromoCodeEnabled: req.PromoCodeEnabled, PromoCodeEnabled: req.PromoCodeEnabled,
PasswordResetEnabled: req.PasswordResetEnabled, PasswordResetEnabled: req.PasswordResetEnabled,
TotpEnabled: req.TotpEnabled, TotpEnabled: req.TotpEnabled,
SMTPHost: req.SMTPHost, SMTPHost: req.SMTPHost,
SMTPPort: req.SMTPPort, SMTPPort: req.SMTPPort,
SMTPUsername: req.SMTPUsername, SMTPUsername: req.SMTPUsername,
SMTPPassword: req.SMTPPassword, SMTPPassword: req.SMTPPassword,
SMTPFrom: req.SMTPFrom, SMTPFrom: req.SMTPFrom,
SMTPFromName: req.SMTPFromName, SMTPFromName: req.SMTPFromName,
SMTPUseTLS: req.SMTPUseTLS, SMTPUseTLS: req.SMTPUseTLS,
TurnstileEnabled: req.TurnstileEnabled, TurnstileEnabled: req.TurnstileEnabled,
TurnstileSiteKey: req.TurnstileSiteKey, TurnstileSiteKey: req.TurnstileSiteKey,
TurnstileSecretKey: req.TurnstileSecretKey, TurnstileSecretKey: req.TurnstileSecretKey,
LinuxDoConnectEnabled: req.LinuxDoConnectEnabled, LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
LinuxDoConnectClientID: req.LinuxDoConnectClientID, LinuxDoConnectClientID: req.LinuxDoConnectClientID,
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret, LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL, LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
SiteName: req.SiteName, SiteName: req.SiteName,
SiteLogo: req.SiteLogo, SiteLogo: req.SiteLogo,
SiteSubtitle: req.SiteSubtitle, SiteSubtitle: req.SiteSubtitle,
APIBaseURL: req.APIBaseURL, APIBaseURL: req.APIBaseURL,
ContactInfo: req.ContactInfo, ContactInfo: req.ContactInfo,
DocURL: req.DocURL, DocURL: req.DocURL,
HomeContent: req.HomeContent, HomeContent: req.HomeContent,
HideCcsImportButton: req.HideCcsImportButton, HideCcsImportButton: req.HideCcsImportButton,
DefaultConcurrency: req.DefaultConcurrency, PurchaseSubscriptionEnabled: purchaseEnabled,
DefaultBalance: req.DefaultBalance, PurchaseSubscriptionURL: purchaseURL,
EnableModelFallback: req.EnableModelFallback, DefaultConcurrency: req.DefaultConcurrency,
FallbackModelAnthropic: req.FallbackModelAnthropic, DefaultBalance: req.DefaultBalance,
FallbackModelOpenAI: req.FallbackModelOpenAI, EnableModelFallback: req.EnableModelFallback,
FallbackModelGemini: req.FallbackModelGemini, FallbackModelAnthropic: req.FallbackModelAnthropic,
FallbackModelAntigravity: req.FallbackModelAntigravity, FallbackModelOpenAI: req.FallbackModelOpenAI,
EnableIdentityPatch: req.EnableIdentityPatch, FallbackModelGemini: req.FallbackModelGemini,
IdentityPatchPrompt: req.IdentityPatchPrompt, FallbackModelAntigravity: req.FallbackModelAntigravity,
EnableIdentityPatch: req.EnableIdentityPatch,
IdentityPatchPrompt: req.IdentityPatchPrompt,
OpsMonitoringEnabled: func() bool { OpsMonitoringEnabled: func() bool {
if req.OpsMonitoringEnabled != nil { if req.OpsMonitoringEnabled != nil {
return *req.OpsMonitoringEnabled return *req.OpsMonitoringEnabled
...@@ -360,6 +394,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -360,6 +394,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
DocURL: updatedSettings.DocURL, DocURL: updatedSettings.DocURL,
HomeContent: updatedSettings.HomeContent, HomeContent: updatedSettings.HomeContent,
HideCcsImportButton: updatedSettings.HideCcsImportButton, HideCcsImportButton: updatedSettings.HideCcsImportButton,
PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
DefaultConcurrency: updatedSettings.DefaultConcurrency, DefaultConcurrency: updatedSettings.DefaultConcurrency,
DefaultBalance: updatedSettings.DefaultBalance, DefaultBalance: updatedSettings.DefaultBalance,
EnableModelFallback: updatedSettings.EnableModelFallback, EnableModelFallback: updatedSettings.EnableModelFallback,
......
...@@ -26,14 +26,16 @@ type SystemSettings struct { ...@@ -26,14 +26,16 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured bool `json:"linuxdo_connect_client_secret_configured"` LinuxDoConnectClientSecretConfigured bool `json:"linuxdo_connect_client_secret_configured"`
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"` LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
SiteLogo string `json:"site_logo"` SiteLogo string `json:"site_logo"`
SiteSubtitle string `json:"site_subtitle"` SiteSubtitle string `json:"site_subtitle"`
APIBaseURL string `json:"api_base_url"` APIBaseURL string `json:"api_base_url"`
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocURL string `json:"doc_url"` DocURL string `json:"doc_url"`
HomeContent string `json:"home_content"` HomeContent string `json:"home_content"`
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
DefaultConcurrency int `json:"default_concurrency"` DefaultConcurrency int `json:"default_concurrency"`
DefaultBalance float64 `json:"default_balance"` DefaultBalance float64 `json:"default_balance"`
...@@ -57,23 +59,25 @@ type SystemSettings struct { ...@@ -57,23 +59,25 @@ type SystemSettings struct {
} }
type PublicSettings struct { type PublicSettings struct {
RegistrationEnabled bool `json:"registration_enabled"` RegistrationEnabled bool `json:"registration_enabled"`
EmailVerifyEnabled bool `json:"email_verify_enabled"` EmailVerifyEnabled bool `json:"email_verify_enabled"`
PromoCodeEnabled bool `json:"promo_code_enabled"` PromoCodeEnabled bool `json:"promo_code_enabled"`
PasswordResetEnabled bool `json:"password_reset_enabled"` PasswordResetEnabled bool `json:"password_reset_enabled"`
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证 TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证
TurnstileEnabled bool `json:"turnstile_enabled"` TurnstileEnabled bool `json:"turnstile_enabled"`
TurnstileSiteKey string `json:"turnstile_site_key"` TurnstileSiteKey string `json:"turnstile_site_key"`
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
SiteLogo string `json:"site_logo"` SiteLogo string `json:"site_logo"`
SiteSubtitle string `json:"site_subtitle"` SiteSubtitle string `json:"site_subtitle"`
APIBaseURL string `json:"api_base_url"` APIBaseURL string `json:"api_base_url"`
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocURL string `json:"doc_url"` DocURL string `json:"doc_url"`
HomeContent string `json:"home_content"` HomeContent string `json:"home_content"`
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
Version string `json:"version"` PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
Version string `json:"version"`
} }
// StreamTimeoutSettings 流超时处理配置 DTO // StreamTimeoutSettings 流超时处理配置 DTO
......
...@@ -32,21 +32,24 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) { ...@@ -32,21 +32,24 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
} }
response.Success(c, dto.PublicSettings{ response.Success(c, dto.PublicSettings{
RegistrationEnabled: settings.RegistrationEnabled, RegistrationEnabled: settings.RegistrationEnabled,
EmailVerifyEnabled: settings.EmailVerifyEnabled, EmailVerifyEnabled: settings.EmailVerifyEnabled,
PromoCodeEnabled: settings.PromoCodeEnabled, PromoCodeEnabled: settings.PromoCodeEnabled,
PasswordResetEnabled: settings.PasswordResetEnabled, PasswordResetEnabled: settings.PasswordResetEnabled,
TurnstileEnabled: settings.TurnstileEnabled, TotpEnabled: settings.TotpEnabled,
TurnstileSiteKey: settings.TurnstileSiteKey, TurnstileEnabled: settings.TurnstileEnabled,
SiteName: settings.SiteName, TurnstileSiteKey: settings.TurnstileSiteKey,
SiteLogo: settings.SiteLogo, SiteName: settings.SiteName,
SiteSubtitle: settings.SiteSubtitle, SiteLogo: settings.SiteLogo,
APIBaseURL: settings.APIBaseURL, SiteSubtitle: settings.SiteSubtitle,
ContactInfo: settings.ContactInfo, APIBaseURL: settings.APIBaseURL,
DocURL: settings.DocURL, ContactInfo: settings.ContactInfo,
HomeContent: settings.HomeContent, DocURL: settings.DocURL,
HideCcsImportButton: settings.HideCcsImportButton, HomeContent: settings.HomeContent,
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled, HideCcsImportButton: settings.HideCcsImportButton,
Version: h.version, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
Version: h.version,
}) })
} }
...@@ -489,7 +489,9 @@ func TestAPIContracts(t *testing.T) { ...@@ -489,7 +489,9 @@ func TestAPIContracts(t *testing.T) {
"enable_identity_patch": true, "enable_identity_patch": true,
"identity_patch_prompt": "", "identity_patch_prompt": "",
"home_content": "", "home_content": "",
"hide_ccs_import_button": false "hide_ccs_import_button": false,
"purchase_subscription_enabled": false,
"purchase_subscription_url": ""
} }
}`, }`,
}, },
......
...@@ -98,14 +98,16 @@ const ( ...@@ -98,14 +98,16 @@ const (
SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url" SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url"
// OEM设置 // OEM设置
SettingKeySiteName = "site_name" // 网站名称 SettingKeySiteName = "site_name" // 网站名称
SettingKeySiteLogo = "site_logo" // 网站Logo (base64) SettingKeySiteLogo = "site_logo" // 网站Logo (base64)
SettingKeySiteSubtitle = "site_subtitle" // 网站副标题 SettingKeySiteSubtitle = "site_subtitle" // 网站副标题
SettingKeyAPIBaseURL = "api_base_url" // API端点地址(用于客户端配置和导入) SettingKeyAPIBaseURL = "api_base_url" // API端点地址(用于客户端配置和导入)
SettingKeyContactInfo = "contact_info" // 客服联系方式 SettingKeyContactInfo = "contact_info" // 客服联系方式
SettingKeyDocURL = "doc_url" // 文档链接 SettingKeyDocURL = "doc_url" // 文档链接
SettingKeyHomeContent = "home_content" // 首页内容(支持 Markdown/HTML,或 URL 作为 iframe src) SettingKeyHomeContent = "home_content" // 首页内容(支持 Markdown/HTML,或 URL 作为 iframe src)
SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮 SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮
SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示“购买订阅”页面入口
SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // “购买订阅”页面 URL(作为 iframe src)
// 默认配置 // 默认配置
SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量 SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量
......
...@@ -73,6 +73,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ...@@ -73,6 +73,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyDocURL, SettingKeyDocURL,
SettingKeyHomeContent, SettingKeyHomeContent,
SettingKeyHideCcsImportButton, SettingKeyHideCcsImportButton,
SettingKeyPurchaseSubscriptionEnabled,
SettingKeyPurchaseSubscriptionURL,
SettingKeyLinuxDoConnectEnabled, SettingKeyLinuxDoConnectEnabled,
} }
...@@ -93,22 +95,24 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ...@@ -93,22 +95,24 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
passwordResetEnabled := emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true" passwordResetEnabled := emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true"
return &PublicSettings{ return &PublicSettings{
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true", RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
EmailVerifyEnabled: emailVerifyEnabled, EmailVerifyEnabled: emailVerifyEnabled,
PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用 PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用
PasswordResetEnabled: passwordResetEnabled, PasswordResetEnabled: passwordResetEnabled,
TotpEnabled: settings[SettingKeyTotpEnabled] == "true", TotpEnabled: settings[SettingKeyTotpEnabled] == "true",
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true", TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey], TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"), SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
SiteLogo: settings[SettingKeySiteLogo], SiteLogo: settings[SettingKeySiteLogo],
SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"), SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
APIBaseURL: settings[SettingKeyAPIBaseURL], APIBaseURL: settings[SettingKeyAPIBaseURL],
ContactInfo: settings[SettingKeyContactInfo], ContactInfo: settings[SettingKeyContactInfo],
DocURL: settings[SettingKeyDocURL], DocURL: settings[SettingKeyDocURL],
HomeContent: settings[SettingKeyHomeContent], HomeContent: settings[SettingKeyHomeContent],
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true", HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
LinuxDoOAuthEnabled: linuxDoEnabled, PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
LinuxDoOAuthEnabled: linuxDoEnabled,
}, nil }, nil
} }
...@@ -133,41 +137,45 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any ...@@ -133,41 +137,45 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
// Return a struct that matches the frontend's expected format // Return a struct that matches the frontend's expected format
return &struct { return &struct {
RegistrationEnabled bool `json:"registration_enabled"` RegistrationEnabled bool `json:"registration_enabled"`
EmailVerifyEnabled bool `json:"email_verify_enabled"` EmailVerifyEnabled bool `json:"email_verify_enabled"`
PromoCodeEnabled bool `json:"promo_code_enabled"` PromoCodeEnabled bool `json:"promo_code_enabled"`
PasswordResetEnabled bool `json:"password_reset_enabled"` PasswordResetEnabled bool `json:"password_reset_enabled"`
TotpEnabled bool `json:"totp_enabled"` TotpEnabled bool `json:"totp_enabled"`
TurnstileEnabled bool `json:"turnstile_enabled"` TurnstileEnabled bool `json:"turnstile_enabled"`
TurnstileSiteKey string `json:"turnstile_site_key,omitempty"` TurnstileSiteKey string `json:"turnstile_site_key,omitempty"`
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
SiteLogo string `json:"site_logo,omitempty"` SiteLogo string `json:"site_logo,omitempty"`
SiteSubtitle string `json:"site_subtitle,omitempty"` SiteSubtitle string `json:"site_subtitle,omitempty"`
APIBaseURL string `json:"api_base_url,omitempty"` APIBaseURL string `json:"api_base_url,omitempty"`
ContactInfo string `json:"contact_info,omitempty"` ContactInfo string `json:"contact_info,omitempty"`
DocURL string `json:"doc_url,omitempty"` DocURL string `json:"doc_url,omitempty"`
HomeContent string `json:"home_content,omitempty"` HomeContent string `json:"home_content,omitempty"`
HideCcsImportButton bool `json:"hide_ccs_import_button"` HideCcsImportButton bool `json:"hide_ccs_import_button"`
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
Version string `json:"version,omitempty"` PurchaseSubscriptionURL string `json:"purchase_subscription_url,omitempty"`
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
Version string `json:"version,omitempty"`
}{ }{
RegistrationEnabled: settings.RegistrationEnabled, RegistrationEnabled: settings.RegistrationEnabled,
EmailVerifyEnabled: settings.EmailVerifyEnabled, EmailVerifyEnabled: settings.EmailVerifyEnabled,
PromoCodeEnabled: settings.PromoCodeEnabled, PromoCodeEnabled: settings.PromoCodeEnabled,
PasswordResetEnabled: settings.PasswordResetEnabled, PasswordResetEnabled: settings.PasswordResetEnabled,
TotpEnabled: settings.TotpEnabled, TotpEnabled: settings.TotpEnabled,
TurnstileEnabled: settings.TurnstileEnabled, TurnstileEnabled: settings.TurnstileEnabled,
TurnstileSiteKey: settings.TurnstileSiteKey, TurnstileSiteKey: settings.TurnstileSiteKey,
SiteName: settings.SiteName, SiteName: settings.SiteName,
SiteLogo: settings.SiteLogo, SiteLogo: settings.SiteLogo,
SiteSubtitle: settings.SiteSubtitle, SiteSubtitle: settings.SiteSubtitle,
APIBaseURL: settings.APIBaseURL, APIBaseURL: settings.APIBaseURL,
ContactInfo: settings.ContactInfo, ContactInfo: settings.ContactInfo,
DocURL: settings.DocURL, DocURL: settings.DocURL,
HomeContent: settings.HomeContent, HomeContent: settings.HomeContent,
HideCcsImportButton: settings.HideCcsImportButton, HideCcsImportButton: settings.HideCcsImportButton,
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
Version: s.version, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
Version: s.version,
}, nil }, nil
} }
...@@ -217,6 +225,8 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet ...@@ -217,6 +225,8 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates[SettingKeyDocURL] = settings.DocURL updates[SettingKeyDocURL] = settings.DocURL
updates[SettingKeyHomeContent] = settings.HomeContent updates[SettingKeyHomeContent] = settings.HomeContent
updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton) updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton)
updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled)
updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL)
// 默认配置 // 默认配置
updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency) updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
...@@ -352,15 +362,17 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { ...@@ -352,15 +362,17 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
// 初始化默认设置 // 初始化默认设置
defaults := map[string]string{ defaults := map[string]string{
SettingKeyRegistrationEnabled: "true", SettingKeyRegistrationEnabled: "true",
SettingKeyEmailVerifyEnabled: "false", SettingKeyEmailVerifyEnabled: "false",
SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能 SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能
SettingKeySiteName: "Sub2API", SettingKeySiteName: "Sub2API",
SettingKeySiteLogo: "", SettingKeySiteLogo: "",
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency), SettingKeyPurchaseSubscriptionEnabled: "false",
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64), SettingKeyPurchaseSubscriptionURL: "",
SettingKeySMTPPort: "587", SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
SettingKeySMTPUseTLS: "false", SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
SettingKeySMTPPort: "587",
SettingKeySMTPUseTLS: "false",
// Model fallback defaults // Model fallback defaults
SettingKeyEnableModelFallback: "false", SettingKeyEnableModelFallback: "false",
SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022", SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022",
...@@ -407,6 +419,8 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin ...@@ -407,6 +419,8 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
DocURL: settings[SettingKeyDocURL], DocURL: settings[SettingKeyDocURL],
HomeContent: settings[SettingKeyHomeContent], HomeContent: settings[SettingKeyHomeContent],
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true", HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
} }
// 解析整数类型 // 解析整数类型
......
...@@ -28,14 +28,16 @@ type SystemSettings struct { ...@@ -28,14 +28,16 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured bool LinuxDoConnectClientSecretConfigured bool
LinuxDoConnectRedirectURL string LinuxDoConnectRedirectURL string
SiteName string SiteName string
SiteLogo string SiteLogo string
SiteSubtitle string SiteSubtitle string
APIBaseURL string APIBaseURL string
ContactInfo string ContactInfo string
DocURL string DocURL string
HomeContent string HomeContent string
HideCcsImportButton bool HideCcsImportButton bool
PurchaseSubscriptionEnabled bool
PurchaseSubscriptionURL string
DefaultConcurrency int DefaultConcurrency int
DefaultBalance float64 DefaultBalance float64
...@@ -74,8 +76,12 @@ type PublicSettings struct { ...@@ -74,8 +76,12 @@ type PublicSettings struct {
DocURL string DocURL string
HomeContent string HomeContent string
HideCcsImportButton bool HideCcsImportButton bool
LinuxDoOAuthEnabled bool
Version string PurchaseSubscriptionEnabled bool
PurchaseSubscriptionURL string
LinuxDoOAuthEnabled bool
Version string
} }
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制) // StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
......
...@@ -28,6 +28,8 @@ export interface SystemSettings { ...@@ -28,6 +28,8 @@ export interface SystemSettings {
doc_url: string doc_url: string
home_content: string home_content: string
hide_ccs_import_button: boolean hide_ccs_import_button: boolean
purchase_subscription_enabled: boolean
purchase_subscription_url: string
// SMTP settings // SMTP settings
smtp_host: string smtp_host: string
smtp_port: number smtp_port: number
...@@ -81,6 +83,8 @@ export interface UpdateSettingsRequest { ...@@ -81,6 +83,8 @@ export interface UpdateSettingsRequest {
doc_url?: string doc_url?: string
home_content?: string home_content?: string
hide_ccs_import_button?: boolean hide_ccs_import_button?: boolean
purchase_subscription_enabled?: boolean
purchase_subscription_url?: string
smtp_host?: string smtp_host?: string
smtp_port?: number smtp_port?: number
smtp_username?: string smtp_username?: string
......
...@@ -421,6 +421,16 @@ const userNavItems = computed(() => { ...@@ -421,6 +421,16 @@ const userNavItems = computed(() => {
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon }, { path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true }, { path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true }, { path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
...(appStore.cachedPublicSettings?.purchase_subscription_enabled
? [
{
path: '/purchase',
label: t('nav.buySubscription'),
icon: CreditCardIcon,
hideInSimpleMode: true
}
]
: []),
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true }, { path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
{ path: '/profile', label: t('nav.profile'), icon: UserIcon } { path: '/profile', label: t('nav.profile'), icon: UserIcon }
] ]
...@@ -433,6 +443,16 @@ const personalNavItems = computed(() => { ...@@ -433,6 +443,16 @@ const personalNavItems = computed(() => {
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon }, { path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true }, { path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true }, { path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
...(appStore.cachedPublicSettings?.purchase_subscription_enabled
? [
{
path: '/purchase',
label: t('nav.buySubscription'),
icon: CreditCardIcon,
hideInSimpleMode: true
}
]
: []),
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true }, { path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
{ path: '/profile', label: t('nav.profile'), icon: UserIcon } { path: '/profile', label: t('nav.profile'), icon: UserIcon }
] ]
......
...@@ -206,6 +206,7 @@ export default { ...@@ -206,6 +206,7 @@ export default {
logout: 'Logout', logout: 'Logout',
github: 'GitHub', github: 'GitHub',
mySubscriptions: 'My Subscriptions', mySubscriptions: 'My Subscriptions',
buySubscription: 'Purchase Subscription',
docs: 'Docs' docs: 'Docs'
}, },
...@@ -2894,6 +2895,17 @@ export default { ...@@ -2894,6 +2895,17 @@ export default {
hideCcsImportButton: 'Hide CCS Import Button', hideCcsImportButton: 'Hide CCS Import Button',
hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page' hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page'
}, },
purchase: {
title: 'Purchase Page',
description: 'Show a "Purchase Subscription" entry in the sidebar and open the configured URL in an iframe',
enabled: 'Show Purchase Entry',
enabledHint: 'Only shown in standard mode (not simple mode)',
url: 'Purchase URL',
urlPlaceholder: 'https://example.com/purchase',
urlHint: 'Must be an absolute http(s) URL',
iframeWarning:
'⚠️ iframe note: Some websites block embedding via X-Frame-Options or CSP (frame-ancestors). If the page is blank, provide an "Open in new tab" alternative.'
},
smtp: { smtp: {
title: 'SMTP Settings', title: 'SMTP Settings',
description: 'Configure email sending for verification codes', description: 'Configure email sending for verification codes',
...@@ -3039,6 +3051,18 @@ export default { ...@@ -3039,6 +3051,18 @@ export default {
retry: 'Retry' retry: 'Retry'
}, },
// Purchase Subscription Page
purchase: {
title: 'Purchase Subscription',
description: 'Purchase a subscription via the embedded page',
openInNewTab: 'Open in new tab',
notEnabledTitle: 'Feature not enabled',
notEnabledDesc: 'The administrator has not enabled the purchase page. Please contact admin.',
notConfiguredTitle: 'Purchase URL not configured',
notConfiguredDesc:
'The administrator enabled the entry but has not configured a purchase URL. Please contact admin.'
},
// User Subscriptions Page // User Subscriptions Page
userSubscriptions: { userSubscriptions: {
title: 'My Subscriptions', title: 'My Subscriptions',
......
...@@ -203,6 +203,7 @@ export default { ...@@ -203,6 +203,7 @@ export default {
logout: '退出登录', logout: '退出登录',
github: 'GitHub', github: 'GitHub',
mySubscriptions: '我的订阅', mySubscriptions: '我的订阅',
buySubscription: '购买订阅',
docs: '文档' docs: '文档'
}, },
...@@ -3045,6 +3046,17 @@ export default { ...@@ -3045,6 +3046,17 @@ export default {
hideCcsImportButton: '隐藏 CCS 导入按钮', hideCcsImportButton: '隐藏 CCS 导入按钮',
hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮' hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮'
}, },
purchase: {
title: '购买订阅页面',
description: '在侧边栏展示“购买订阅”入口,并在页面内通过 iframe 打开指定链接',
enabled: '显示购买订阅入口',
enabledHint: '仅在标准模式(非简单模式)下展示',
url: '购买页面 URL',
urlPlaceholder: 'https://example.com/purchase',
urlHint: '必须是完整的 http(s) 链接',
iframeWarning:
'⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用“新窗口打开”。'
},
smtp: { smtp: {
title: 'SMTP 设置', title: 'SMTP 设置',
description: '配置用于发送验证码的邮件服务', description: '配置用于发送验证码的邮件服务',
...@@ -3189,6 +3201,17 @@ export default { ...@@ -3189,6 +3201,17 @@ export default {
retry: '重试' retry: '重试'
}, },
// Purchase Subscription Page
purchase: {
title: '购买订阅',
description: '通过内嵌页面完成订阅购买',
openInNewTab: '新窗口打开',
notEnabledTitle: '该功能未开启',
notEnabledDesc: '管理员暂未开启购买订阅入口,请联系管理员。',
notConfiguredTitle: '购买链接未配置',
notConfiguredDesc: '管理员已开启入口,但尚未配置购买订阅链接,请联系管理员。'
},
// User Subscriptions Page // User Subscriptions Page
userSubscriptions: { userSubscriptions: {
title: '我的订阅', title: '我的订阅',
......
...@@ -175,6 +175,18 @@ const routes: RouteRecordRaw[] = [ ...@@ -175,6 +175,18 @@ const routes: RouteRecordRaw[] = [
descriptionKey: 'userSubscriptions.description' descriptionKey: 'userSubscriptions.description'
} }
}, },
{
path: '/purchase',
name: 'PurchaseSubscription',
component: () => import('@/views/user/PurchaseSubscriptionView.vue'),
meta: {
requiresAuth: true,
requiresAdmin: false,
title: 'Purchase Subscription',
titleKey: 'purchase.title',
descriptionKey: 'purchase.description'
}
},
// ==================== Admin Routes ==================== // ==================== Admin Routes ====================
{ {
......
...@@ -324,6 +324,8 @@ export const useAppStore = defineStore('app', () => { ...@@ -324,6 +324,8 @@ export const useAppStore = defineStore('app', () => {
doc_url: docUrl.value, doc_url: docUrl.value,
home_content: '', home_content: '',
hide_ccs_import_button: false, hide_ccs_import_button: false,
purchase_subscription_enabled: false,
purchase_subscription_url: '',
linuxdo_oauth_enabled: false, linuxdo_oauth_enabled: false,
version: siteVersion.value version: siteVersion.value
} }
......
...@@ -82,6 +82,8 @@ export interface PublicSettings { ...@@ -82,6 +82,8 @@ export interface PublicSettings {
doc_url: string doc_url: string
home_content: string home_content: string
hide_ccs_import_button: boolean hide_ccs_import_button: boolean
purchase_subscription_enabled: boolean
purchase_subscription_url: string
linuxdo_oauth_enabled: boolean linuxdo_oauth_enabled: boolean
version: string version: string
} }
......
...@@ -935,6 +935,51 @@ ...@@ -935,6 +935,51 @@
</div> </div>
</div> </div>
<!-- Purchase Subscription Page -->
<div class="card">
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t('admin.settings.purchase.title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ t('admin.settings.purchase.description') }}
</p>
</div>
<div class="space-y-6 p-6">
<!-- Enable Toggle -->
<div class="flex items-center justify-between">
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t('admin.settings.purchase.enabled')
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('admin.settings.purchase.enabledHint') }}
</p>
</div>
<Toggle v-model="form.purchase_subscription_enabled" />
</div>
<!-- URL -->
<div>
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.settings.purchase.url') }}
</label>
<input
v-model="form.purchase_subscription_url"
type="url"
class="input font-mono text-sm"
:placeholder="t('admin.settings.purchase.urlPlaceholder')"
/>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.purchase.urlHint') }}
</p>
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
{{ t('admin.settings.purchase.iframeWarning') }}
</p>
</div>
</div>
</div>
<!-- Send Test Email - Only show when email verification is enabled --> <!-- Send Test Email - Only show when email verification is enabled -->
<div v-if="form.email_verify_enabled" class="card"> <div v-if="form.email_verify_enabled" class="card">
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"> <div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
...@@ -1083,6 +1128,8 @@ const form = reactive<SettingsForm>({ ...@@ -1083,6 +1128,8 @@ const form = reactive<SettingsForm>({
doc_url: '', doc_url: '',
home_content: '', home_content: '',
hide_ccs_import_button: false, hide_ccs_import_button: false,
purchase_subscription_enabled: false,
purchase_subscription_url: '',
smtp_host: '', smtp_host: '',
smtp_port: 587, smtp_port: 587,
smtp_username: '', smtp_username: '',
...@@ -1208,6 +1255,8 @@ async function saveSettings() { ...@@ -1208,6 +1255,8 @@ async function saveSettings() {
doc_url: form.doc_url, doc_url: form.doc_url,
home_content: form.home_content, home_content: form.home_content,
hide_ccs_import_button: form.hide_ccs_import_button, hide_ccs_import_button: form.hide_ccs_import_button,
purchase_subscription_enabled: form.purchase_subscription_enabled,
purchase_subscription_url: form.purchase_subscription_url,
smtp_host: form.smtp_host, smtp_host: form.smtp_host,
smtp_port: form.smtp_port, smtp_port: form.smtp_port,
smtp_username: form.smtp_username, smtp_username: form.smtp_username,
......
<template>
<AppLayout>
<div class="purchase-page-layout">
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t('purchase.title') }}
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
{{ t('purchase.description') }}
</p>
</div>
<div class="flex items-center gap-2">
<a
v-if="isValidUrl"
:href="purchaseUrl"
target="_blank"
rel="noopener noreferrer"
class="btn btn-secondary btn-sm"
>
<Icon name="externalLink" size="sm" class="mr-1.5" :stroke-width="2" />
{{ t('purchase.openInNewTab') }}
</a>
</div>
</div>
<div class="card flex-1 min-h-0 overflow-hidden">
<div v-if="loading" class="flex h-full items-center justify-center py-12">
<div
class="h-8 w-8 animate-spin rounded-full border-2 border-primary-500 border-t-transparent"
></div>
</div>
<div
v-else-if="!purchaseEnabled"
class="flex h-full items-center justify-center p-10 text-center"
>
<div class="max-w-md">
<div
class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 dark:bg-dark-700"
>
<Icon name="creditCard" size="lg" class="text-gray-400" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t('purchase.notEnabledTitle') }}
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">
{{ t('purchase.notEnabledDesc') }}
</p>
</div>
</div>
<div
v-else-if="!isValidUrl"
class="flex h-full items-center justify-center p-10 text-center"
>
<div class="max-w-md">
<div
class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 dark:bg-dark-700"
>
<Icon name="link" size="lg" class="text-gray-400" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ t('purchase.notConfiguredTitle') }}
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">
{{ t('purchase.notConfiguredDesc') }}
</p>
</div>
</div>
<iframe v-else :src="purchaseUrl" class="h-full w-full border-0" allowfullscreen></iframe>
</div>
</div>
</AppLayout>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores'
import AppLayout from '@/components/layout/AppLayout.vue'
import Icon from '@/components/icons/Icon.vue'
const { t } = useI18n()
const appStore = useAppStore()
const loading = ref(false)
const purchaseEnabled = computed(() => {
return appStore.cachedPublicSettings?.purchase_subscription_enabled ?? false
})
const purchaseUrl = computed(() => {
return (appStore.cachedPublicSettings?.purchase_subscription_url || '').trim()
})
const isValidUrl = computed(() => {
const url = purchaseUrl.value
return url.startsWith('http://') || url.startsWith('https://')
})
onMounted(async () => {
if (appStore.publicSettingsLoaded) return
loading.value = true
try {
await appStore.fetchPublicSettings()
} finally {
loading.value = false
}
})
</script>
<style scoped>
.purchase-page-layout {
@apply flex flex-col gap-6;
height: calc(100vh - 64px - 4rem); /* 减去 header + lg:p-8 的上下padding */
}
</style>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment