From 1cc6bf1b45c0ca4dd95201772982faada22dd901 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sat, 7 Feb 2026 00:57:36 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20chore:=20Improve=20subscription?= =?UTF-8?q?=20billing=20fallback=20and=20UI=20states?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a lightweight active-subscription check to skip subscription pre-consume when none exist, reducing unnecessary transactions and locks. In the subscription UI, disable subscription-first options when no active plan is available, show the effective fallback to wallet with a clear notice, and distinguish “invalidated” from “expired” states. Update i18n strings across supported locales to reflect the new messages and status labels. --- model/subscription.go | 16 +++++ service/billing_session.go | 7 +++ .../topup/SubscriptionPlansCard.jsx | 63 +++++++++++++++---- web/src/i18n/locales/en.json | 3 + web/src/i18n/locales/fr.json | 3 + web/src/i18n/locales/ja.json | 3 + web/src/i18n/locales/ru.json | 3 + web/src/i18n/locales/vi.json | 3 + web/src/i18n/locales/zh.json | 3 + 9 files changed, 92 insertions(+), 12 deletions(-) diff --git a/model/subscription.go b/model/subscription.go index 6d8d21306..2d23a8b5b 100644 --- a/model/subscription.go +++ b/model/subscription.go @@ -666,6 +666,22 @@ func GetAllActiveUserSubscriptions(userId int) ([]SubscriptionSummary, error) { return buildSubscriptionSummaries(subs), nil } +// HasActiveUserSubscription returns whether the user has any active subscription. +// This is a lightweight existence check to avoid heavy pre-consume transactions. +func HasActiveUserSubscription(userId int) (bool, error) { + if userId <= 0 { + return false, errors.New("invalid userId") + } + now := common.GetTimestamp() + var count int64 + if err := DB.Model(&UserSubscription{}). + Where("user_id = ? AND status = ? AND end_time > ?", userId, "active", now). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + // GetAllUserSubscriptions returns all subscriptions (active and expired) for a user. func GetAllUserSubscriptions(userId int) ([]SubscriptionSummary, error) { if userId <= 0 { diff --git a/service/billing_session.go b/service/billing_session.go index 04fce6136..00da526bd 100644 --- a/service/billing_session.go +++ b/service/billing_session.go @@ -323,6 +323,13 @@ func NewBillingSession(c *gin.Context, relayInfo *relaycommon.RelayInfo, preCons case "subscription_first": fallthrough default: + hasSub, err := model.HasActiveUserSubscription(relayInfo.UserId) + if err != nil { + return nil, types.NewError(err, types.ErrorCodeQueryDataError, types.ErrOptionWithSkipRetry()) + } + if !hasSub { + return tryWallet() + } session, err := trySubscription() if err != nil { if err.GetErrorCode() == types.ErrorCodeInsufficientUserQuota { diff --git a/web/src/components/topup/SubscriptionPlansCard.jsx b/web/src/components/topup/SubscriptionPlansCard.jsx index 54b4506ba..a619c7450 100644 --- a/web/src/components/topup/SubscriptionPlansCard.jsx +++ b/web/src/components/topup/SubscriptionPlansCard.jsx @@ -201,6 +201,16 @@ const SubscriptionPlansCard = ({ // 当前订阅信息 - 支持多个订阅 const hasActiveSubscription = activeSubscriptions.length > 0; const hasAnySubscription = allSubscriptions.length > 0; + const disableSubscriptionPreference = !hasActiveSubscription; + const isSubscriptionPreference = + billingPreference === 'subscription_first' || + billingPreference === 'subscription_only'; + const displayBillingPreference = + disableSubscriptionPreference && isSubscriptionPreference + ? 'wallet_first' + : billingPreference; + const subscriptionPreferenceLabel = + billingPreference === 'subscription_only' ? t('仅用订阅') : t('优先订阅'); const planPurchaseCountMap = useMemo(() => { const map = new Map(); @@ -319,13 +329,25 @@ const SubscriptionPlansCard = ({