mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
后台功能开关页新增「站点类型」单选:充值 & 订阅(默认,即现状)/ 仅充值 / 仅订阅。 它由两个后端开关派生并在保存时同时写回: - subscription_enabled(新增;默认 true,缺省/空值视为开启,仅显式 false 关闭),按既有 开关模式贯通常量、公共设置、SSR 注入载荷、默认值、更新写入、DTO、审计 diff 与前端注册表; - payment_balance_disabled(支付配置里既有的 BALANCE_PAYMENT_DISABLED,此前只能走接口/数据库 改),补进公共设置与注入载荷,让用户端在任何鉴权请求之前就能推导站点类型。 两者都关闭是接口层可表达但 UI 不提供的组合,前端归为「仅充值」,后台保存时会把余额充值重新打开。 「仅充值」时的表现: - 用户侧边栏隐藏「我的订阅」,「充值/订阅」入口收窄为「充值」;管理端侧边栏隐藏「订阅管理」 入口(页面仍可通过地址访问); - 购买页不再渲染「订阅」tab,只剩单个 tab 时顶部切换器随之隐藏;tab 列表变化时当前 tab 自动收敛到可用项(关闭订阅回到充值、仅订阅站点重新打开订阅进入订阅),列表为空时展示 「充值与订阅均暂未开放」而不是一个必定被后端拒绝的充值表单; - /purchase 的页头标题/描述与 document.title 改走 resolveRouteMetaKeys 按站点类型切换; - /subscriptions 路由新增 requiresSubscription 守卫,直达时重定向到仪表盘; - 顶栏订阅进度徽章不再挂载,App.vue、PaymentView 与徽章组件都不再预加载/轮询订阅接口; - 用量页隐藏「计费类型(余额/订阅)」筛选,Key 用量公开页「订阅类型」行改为「计费方式」。 「仅订阅」时侧边栏入口与页头显示为「订阅」,购买页只剩订阅 tab(余额下单由后端既有逻辑拒绝)。 购买订阅接口、已有订阅的计费、兑换码发放订阅与管理端订阅管理页面本身均不受影响。 featureFlags 抽出纯函数 resolveFeatureFlag,新增 utils/siteBillingMode 承载三态派生与反解, PaymentView 测试改用响应式 store mock 覆盖开关双向切换。
161 lines
7.7 KiB
Go
161 lines
7.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"html"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SettingHandler 公开设置处理器(无需认证)
|
|
type SettingHandler struct {
|
|
settingService *service.SettingService
|
|
notificationEmailService *service.NotificationEmailService
|
|
version string
|
|
}
|
|
|
|
// NewSettingHandler 创建公开设置处理器
|
|
func NewSettingHandler(settingService *service.SettingService, version string) *SettingHandler {
|
|
return &SettingHandler{
|
|
settingService: settingService,
|
|
version: version,
|
|
}
|
|
}
|
|
|
|
// SetNotificationEmailService attaches the public notification email service without
|
|
// changing the constructor signature used by existing tests.
|
|
func (h *SettingHandler) SetNotificationEmailService(notificationEmailService *service.NotificationEmailService) {
|
|
h.notificationEmailService = notificationEmailService
|
|
}
|
|
|
|
// GetPublicSettings 获取公开设置
|
|
// GET /api/v1/settings/public
|
|
func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetPublicSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.PublicSettings{
|
|
RegistrationEnabled: settings.RegistrationEnabled,
|
|
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
|
ForceEmailOnThirdPartySignup: settings.ForceEmailOnThirdPartySignup,
|
|
RegistrationEmailSuffixWhitelist: settings.RegistrationEmailSuffixWhitelist,
|
|
RegistrationEmailDomainQuotaEnabled: settings.RegistrationEmailDomainQuotaEnabled,
|
|
PromoCodeEnabled: settings.PromoCodeEnabled,
|
|
PasswordResetEnabled: settings.PasswordResetEnabled,
|
|
InvitationCodeEnabled: settings.InvitationCodeEnabled,
|
|
TotpEnabled: settings.TotpEnabled,
|
|
PasskeyEnabled: settings.PasskeyEnabled,
|
|
LoginAgreementEnabled: settings.LoginAgreementEnabled,
|
|
LoginAgreementMode: settings.LoginAgreementMode,
|
|
LoginAgreementUpdatedAt: settings.LoginAgreementUpdatedAt,
|
|
LoginAgreementRevision: settings.LoginAgreementRevision,
|
|
LoginAgreementDocuments: publicLoginAgreementDocumentsToDTO(settings.LoginAgreementDocuments),
|
|
TurnstileEnabled: settings.TurnstileEnabled,
|
|
TurnstileSiteKey: settings.TurnstileSiteKey,
|
|
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
|
|
TencentCaptchaAppID: settings.TencentCaptchaAppID,
|
|
TencentCaptchaRegion: settings.TencentCaptchaRegion,
|
|
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
|
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
|
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
|
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
|
SiteName: settings.SiteName,
|
|
SiteLogo: settings.SiteLogo,
|
|
SiteSubtitle: settings.SiteSubtitle,
|
|
APIBaseURL: settings.APIBaseURL,
|
|
ContactInfo: settings.ContactInfo,
|
|
DocURL: settings.DocURL,
|
|
HomeContent: settings.HomeContent,
|
|
CompactHomeEnabled: settings.CompactHomeEnabled,
|
|
HideCcsImportButton: settings.HideCcsImportButton,
|
|
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
|
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
|
TableDefaultPageSize: settings.TableDefaultPageSize,
|
|
TablePageSizeOptions: settings.TablePageSizeOptions,
|
|
CustomMenuItems: dto.ParseUserVisibleMenuItems(settings.CustomMenuItems),
|
|
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
|
|
DingTalkOAuthEnabled: settings.DingTalkOAuthEnabled,
|
|
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
|
WeChatOAuthEnabled: settings.WeChatOAuthEnabled,
|
|
WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled,
|
|
WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled,
|
|
WeChatOAuthMobileEnabled: settings.WeChatOAuthMobileEnabled,
|
|
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
|
|
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
|
|
GitHubOAuthEnabled: settings.GitHubOAuthEnabled,
|
|
GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
|
|
BackendModeEnabled: settings.BackendModeEnabled,
|
|
PaymentEnabled: settings.PaymentEnabled,
|
|
PaymentBalanceDisabled: settings.PaymentBalanceDisabled,
|
|
Version: h.version,
|
|
ServerTimezone: timezone.Name(),
|
|
ServerUTCOffset: timezone.UTCOffset(),
|
|
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
|
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
|
|
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
|
BalanceLowNotifyRechargeURL: settings.BalanceLowNotifyRechargeURL,
|
|
|
|
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
|
ChannelMonitorMode: settings.ChannelMonitorMode,
|
|
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
|
|
ChannelMonitorHideThroughput: settings.ChannelMonitorHideThroughput,
|
|
ChannelMonitorShowQuota: settings.ChannelMonitorShowQuota,
|
|
ChannelMonitorHideUserRanking: settings.ChannelMonitorHideUserRanking,
|
|
|
|
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
|
|
SubscriptionEnabled: settings.SubscriptionEnabled,
|
|
|
|
ModelPlazaEnabled: settings.ModelPlazaEnabled,
|
|
ModelPlazaRequireAuth: settings.ModelPlazaRequireAuth,
|
|
PluginManagementEnabled: settings.PluginManagementEnabled,
|
|
|
|
AffiliateEnabled: settings.AffiliateEnabled,
|
|
|
|
RiskControlEnabled: settings.RiskControlEnabled,
|
|
|
|
AllowUserViewErrorRequests: settings.AllowUserViewErrorRequests,
|
|
})
|
|
}
|
|
|
|
// UnsubscribeNotificationEmail handles optional notification email opt-outs.
|
|
// GET /api/v1/settings/email-unsubscribe?token=...
|
|
func (h *SettingHandler) UnsubscribeNotificationEmail(c *gin.Context) {
|
|
if h.notificationEmailService == nil {
|
|
response.InternalError(c, "notification email service is not configured")
|
|
return
|
|
}
|
|
token := strings.TrimSpace(c.Query("token"))
|
|
if token == "" {
|
|
response.BadRequest(c, "token is required")
|
|
return
|
|
}
|
|
result, err := h.notificationEmailService.Unsubscribe(c.Request.Context(), token)
|
|
if err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
body := "<!doctype html><html><head><meta charset=\"utf-8\"><title>Unsubscribed</title></head><body style=\"font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;padding:32px;\"><h1>Unsubscribed</h1><p>You have unsubscribed <strong>" + html.EscapeString(result.Email) + "</strong> from <strong>" + html.EscapeString(result.Event) + "</strong> emails.</p></body></html>"
|
|
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(body))
|
|
}
|
|
|
|
func publicLoginAgreementDocumentsToDTO(items []service.LoginAgreementDocument) []dto.LoginAgreementDocument {
|
|
result := make([]dto.LoginAgreementDocument, 0, len(items))
|
|
for _, item := range items {
|
|
result = append(result, dto.LoginAgreementDocument{
|
|
ID: item.ID,
|
|
Title: item.Title,
|
|
ContentMD: item.ContentMD,
|
|
})
|
|
}
|
|
return result
|
|
}
|