mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-16 07:33:06 +08:00
feat(settings): 新增「站点类型」三态开关,关闭订阅后用户端收起所有「订阅」入口与文案
后台功能开关页新增「站点类型」单选:充值 & 订阅(默认,即现状)/ 仅充值 / 仅订阅。 它由两个后端开关派生并在保存时同时写回: - 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 覆盖开关双向切换。
This commit is contained in:
@@ -383,6 +383,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
GrokDefaultBaseURLMode: settings.GrokDefaultBaseURLMode,
|
||||
|
||||
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
|
||||
SubscriptionEnabled: settings.SubscriptionEnabled,
|
||||
|
||||
ModelPlazaEnabled: settings.ModelPlazaEnabled,
|
||||
ModelPlazaRequireAuth: settings.ModelPlazaRequireAuth,
|
||||
|
||||
@@ -579,6 +579,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if before.AvailableChannelsEnabled != after.AvailableChannelsEnabled {
|
||||
changed = append(changed, "available_channels_enabled")
|
||||
}
|
||||
if before.SubscriptionEnabled != after.SubscriptionEnabled {
|
||||
changed = append(changed, "subscription_enabled")
|
||||
}
|
||||
if before.ModelPlazaEnabled != after.ModelPlazaEnabled {
|
||||
changed = append(changed, "model_plaza_enabled")
|
||||
}
|
||||
|
||||
@@ -186,3 +186,20 @@ func TestUpdateSettingsValidatesTencentCaptchaAppIDWhenEnabledFlagIsOmitted(t *t
|
||||
require.Equal(t, http.StatusBadRequest, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "positive integer")
|
||||
}
|
||||
|
||||
// subscription_enabled is an opt-out switch: an explicit false is written as-is,
|
||||
// and a later payload that omits the field keeps the stored value.
|
||||
func TestUpdateSettingsSubscriptionEnabledIsWritableAndKeptWhenOmitted(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeySubscriptionEnabled: "true",
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{"subscription_enabled": false}, nil)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "false", repo.values[service.SettingKeySubscriptionEnabled])
|
||||
|
||||
rec = doUpdateSettings(t, h, map[string]any{"site_name": "Example Gateway"}, nil)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "false", repo.values[service.SettingKeySubscriptionEnabled],
|
||||
"a payload without subscription_enabled must not flip the stored value back to true")
|
||||
}
|
||||
|
||||
@@ -344,6 +344,9 @@ type UpdateSettingsRequest struct {
|
||||
// Available Channels feature switch (user-facing)
|
||||
AvailableChannelsEnabled *bool `json:"available_channels_enabled"`
|
||||
|
||||
// Subscription feature switch (user-facing subscription surface; see SettingKeySubscriptionEnabled)
|
||||
SubscriptionEnabled *bool `json:"subscription_enabled"`
|
||||
|
||||
// Model Plaza feature switches + description
|
||||
ModelPlazaEnabled *bool `json:"model_plaza_enabled"`
|
||||
ModelPlazaRequireAuth *bool `json:"model_plaza_require_auth"`
|
||||
@@ -1937,6 +1940,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
return previousSettings.AvailableChannelsEnabled
|
||||
}(),
|
||||
SubscriptionEnabled: func() bool {
|
||||
if req.SubscriptionEnabled != nil {
|
||||
return *req.SubscriptionEnabled
|
||||
}
|
||||
return previousSettings.SubscriptionEnabled
|
||||
}(),
|
||||
ModelPlazaEnabled: func() bool {
|
||||
if req.ModelPlazaEnabled != nil {
|
||||
return *req.ModelPlazaEnabled
|
||||
@@ -2380,6 +2389,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
GrokDefaultBaseURLMode: updatedSettings.GrokDefaultBaseURLMode,
|
||||
|
||||
AvailableChannelsEnabled: updatedSettings.AvailableChannelsEnabled,
|
||||
SubscriptionEnabled: updatedSettings.SubscriptionEnabled,
|
||||
|
||||
ModelPlazaEnabled: updatedSettings.ModelPlazaEnabled,
|
||||
ModelPlazaRequireAuth: updatedSettings.ModelPlazaRequireAuth,
|
||||
|
||||
@@ -317,6 +317,10 @@ type SystemSettings struct {
|
||||
// Available Channels feature switch (user-facing aggregate view)
|
||||
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||
|
||||
// Subscription feature switch: gates the whole user-facing subscription surface
|
||||
// (sidebar entries, purchase-page subscription tab, header badge, /subscriptions route).
|
||||
SubscriptionEnabled bool `json:"subscription_enabled"`
|
||||
|
||||
// Model Plaza feature (public group/model pricing showcase)
|
||||
ModelPlazaEnabled bool `json:"model_plaza_enabled"`
|
||||
ModelPlazaRequireAuth bool `json:"model_plaza_require_auth"`
|
||||
@@ -403,7 +407,11 @@ type PublicSettings struct {
|
||||
GoogleOAuthEnabled bool `json:"google_oauth_enabled"`
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
PaymentEnabled bool `json:"payment_enabled"`
|
||||
Version string `json:"version"`
|
||||
// PaymentBalanceDisabled mirrors the payment-config BALANCE_PAYMENT_DISABLED switch so the
|
||||
// user shell can derive the site billing mode (recharge & subscription / recharge only /
|
||||
// subscription only) before any authenticated checkout call.
|
||||
PaymentBalanceDisabled bool `json:"payment_balance_disabled"`
|
||||
Version string `json:"version"`
|
||||
// 服务器全局时区(IANA 名称与当前 UTC 偏移,如 "Asia/Shanghai" / "+08:00")。
|
||||
// 高峰时段等按服务器本地时间判定的窗口,前端展示时据此标注,避免用户按浏览器本地时间误读。
|
||||
ServerTimezone string `json:"server_timezone"`
|
||||
@@ -422,6 +430,8 @@ type PublicSettings struct {
|
||||
|
||||
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||
|
||||
SubscriptionEnabled bool `json:"subscription_enabled"`
|
||||
|
||||
ModelPlazaEnabled bool `json:"model_plaza_enabled"`
|
||||
ModelPlazaRequireAuth bool `json:"model_plaza_require_auth"`
|
||||
PluginManagementEnabled bool `json:"plugin_management_enabled"`
|
||||
|
||||
@@ -95,6 +95,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
||||
GoogleOAuthEnabled: settings.GoogleOAuthEnabled,
|
||||
BackendModeEnabled: settings.BackendModeEnabled,
|
||||
PaymentEnabled: settings.PaymentEnabled,
|
||||
PaymentBalanceDisabled: settings.PaymentBalanceDisabled,
|
||||
Version: h.version,
|
||||
ServerTimezone: timezone.Name(),
|
||||
ServerUTCOffset: timezone.UTCOffset(),
|
||||
@@ -111,6 +112,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
||||
ChannelMonitorHideUserRanking: settings.ChannelMonitorHideUserRanking,
|
||||
|
||||
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
|
||||
SubscriptionEnabled: settings.SubscriptionEnabled,
|
||||
|
||||
ModelPlazaEnabled: settings.ModelPlazaEnabled,
|
||||
ModelPlazaRequireAuth: settings.ModelPlazaRequireAuth,
|
||||
|
||||
Reference in New Issue
Block a user