Ver Fonte

Merge pull request #2879 from QuantumNous/fix/subscription-preference-fallback

✨ chore: Improve subscription billing fallback and UI states
Calcium-Ion há 6 dias atrás
pai
commit
aa31b9c77c

+ 16 - 0
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 {

+ 11 - 4
service/billing_session.go

@@ -323,12 +323,19 @@ func NewBillingSession(c *gin.Context, relayInfo *relaycommon.RelayInfo, preCons
 	case "subscription_first":
 		fallthrough
 	default:
-		session, err := trySubscription()
-		if err != nil {
-			if err.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {
+		hasSub, subCheckErr := model.HasActiveUserSubscription(relayInfo.UserId)
+		if subCheckErr != nil {
+			return nil, types.NewError(subCheckErr, types.ErrorCodeQueryDataError, types.ErrOptionWithSkipRetry())
+		}
+		if !hasSub {
+			return tryWallet()
+		}
+		session, apiErr := trySubscription()
+		if apiErr != nil {
+			if apiErr.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {
 				return tryWallet()
 			}
-			return nil, err
+			return nil, apiErr
 		}
 		return session, nil
 	}

+ 51 - 12
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 = ({
               </div>
               <div className='flex items-center gap-2'>
                 <Select
-                  value={billingPreference}
+                  value={displayBillingPreference}
                   onChange={onChangeBillingPreference}
                   size='small'
                   optionList={[
-                    { value: 'subscription_first', label: t('优先订阅') },
+                    {
+                      value: 'subscription_first',
+                      label: disableSubscriptionPreference
+                        ? `${t('优先订阅')} (${t('无生效')})`
+                        : t('优先订阅'),
+                      disabled: disableSubscriptionPreference,
+                    },
                     { value: 'wallet_first', label: t('优先钱包') },
-                    { value: 'subscription_only', label: t('仅用订阅') },
+                    {
+                      value: 'subscription_only',
+                      label: disableSubscriptionPreference
+                        ? `${t('仅用订阅')} (${t('无生效')})`
+                        : t('仅用订阅'),
+                      disabled: disableSubscriptionPreference,
+                    },
                     { value: 'wallet_only', label: t('仅用钱包') },
                   ]}
                 />
@@ -344,6 +366,13 @@ const SubscriptionPlansCard = ({
                 />
               </div>
             </div>
+            {disableSubscriptionPreference && isSubscriptionPreference && (
+              <Text type='tertiary' size='small'>
+                {t('已保存偏好为')}
+                {subscriptionPreferenceLabel}
+                {t(',当前无生效订阅,将自动使用钱包')}
+              </Text>
+            )}
 
             {hasAnySubscription ? (
               <>
@@ -364,6 +393,7 @@ const SubscriptionPlansCard = ({
                     const usagePercent = getUsagePercent(sub);
                     const now = Date.now() / 1000;
                     const isExpired = (subscription?.end_time || 0) < now;
+                    const isCancelled = subscription?.status === 'cancelled';
                     const isActive =
                       subscription?.status === 'active' && !isExpired;
 
@@ -386,6 +416,10 @@ const SubscriptionPlansCard = ({
                               >
                                 {t('生效')}
                               </Tag>
+                            ) : isCancelled ? (
+                              <Tag color='white' size='small' shape='circle'>
+                                {t('已作废')}
+                              </Tag>
                             ) : (
                               <Tag color='white' size='small' shape='circle'>
                                 {t('已过期')}
@@ -399,7 +433,11 @@ const SubscriptionPlansCard = ({
                           )}
                         </div>
                         <div className='text-xs text-gray-500 mb-2'>
-                          {isActive ? t('至') : t('过期于')}{' '}
+                          {isActive
+                            ? t('至')
+                            : isCancelled
+                              ? t('作废于')
+                              : t('过期于')}{' '}
                           {new Date(
                             (subscription?.end_time || 0) * 1000,
                           ).toLocaleString()}
@@ -471,9 +509,9 @@ const SubscriptionPlansCard = ({
                   resetLabel ? { label: resetLabel } : null,
                   totalAmount > 0
                     ? {
-                      label: totalLabel,
-                      tooltip: `${t('原生额度')}:${totalAmount}`,
-                    }
+                        label: totalLabel,
+                        tooltip: `${t('原生额度')}:${totalAmount}`,
+                      }
                     : { label: totalLabel },
                   limitLabel ? { label: limitLabel } : null,
                   upgradeLabel ? { label: upgradeLabel } : null,
@@ -482,8 +520,9 @@ const SubscriptionPlansCard = ({
                 return (
                   <Card
                     key={plan?.id}
-                    className={`!rounded-xl transition-all hover:shadow-lg w-full h-full ${isPopular ? 'ring-2 ring-purple-500' : ''
-                      }`}
+                    className={`!rounded-xl transition-all hover:shadow-lg w-full h-full ${
+                      isPopular ? 'ring-2 ring-purple-500' : ''
+                    }`}
                     bodyStyle={{ padding: 0 }}
                   >
                     <div className='p-4 h-full flex flex-col'>
@@ -629,9 +668,9 @@ const SubscriptionPlansCard = ({
         purchaseLimitInfo={
           selectedPlan?.plan?.id
             ? {
-              limit: Number(selectedPlan?.plan?.max_purchase_per_user || 0),
-              count: getPlanPurchaseCount(selectedPlan?.plan?.id),
-            }
+                limit: Number(selectedPlan?.plan?.max_purchase_per_user || 0),
+                count: getPlanPurchaseCount(selectedPlan?.plan?.id),
+              }
             : null
         }
         onPayStripe={payStripe}

+ 3 - 0
web/src/i18n/locales/en.json

@@ -2729,10 +2729,13 @@
     "我的订阅": "My Subscriptions",
     "个生效中": "active",
     "无生效": "No active",
+    "已保存偏好为": "Saved preference: ",
+    ",当前无生效订阅,将自动使用钱包": ", no active subscription. Wallet will be used automatically.",
     "个已过期": "expired",
     "订阅": "Subscription",
     "至": "until",
     "过期于": "Expires at",
+    "作废于": "Invalidated at",
     "购买套餐后即可享受模型权益": "Enjoy model benefits after purchasing a plan",
     "限购": "Limit",
     "推荐": "Recommended",

+ 3 - 0
web/src/i18n/locales/fr.json

@@ -2692,10 +2692,13 @@
     "我的订阅": "Mes abonnements",
     "个生效中": "actifs",
     "无生效": "Aucun actif",
+    "已保存偏好为": "Préférence enregistrée : ",
+    ",当前无生效订阅,将自动使用钱包": ", aucun abonnement actif, le portefeuille sera utilisé automatiquement.",
     "个已过期": "expirés",
     "订阅": "Abonnement",
     "至": "jusqu'à",
     "过期于": "Expire le",
+    "作废于": "Invalidé le",
     "购买套餐后即可享受模型权益": "Profitez des avantages du modèle après l'achat d'un plan",
     "限购": "Limite",
     "推荐": "Recommandé",

+ 3 - 0
web/src/i18n/locales/ja.json

@@ -2675,10 +2675,13 @@
     "我的订阅": "私のサブスクリプション",
     "个生效中": "件有効中",
     "无生效": "有効なし",
+    "已保存偏好为": "保存された設定は",
+    ",当前无生效订阅,将自动使用钱包": "、有効なサブスクリプションがないため、自動的にウォレットを使用します",
     "个已过期": "件期限切れ",
     "订阅": "サブスクリプション",
     "至": "まで",
     "过期于": "有効期限",
+    "作废于": "無効化日",
     "购买套餐后即可享受模型权益": "プラン購入後にモデル特典を利用できます",
     "限购": "購入制限",
     "推荐": "おすすめ",

+ 3 - 0
web/src/i18n/locales/ru.json

@@ -2705,10 +2705,13 @@
     "我的订阅": "Мои подписки",
     "个生效中": "активных",
     "无生效": "Нет активных",
+    "已保存偏好为": "Сохранённая настройка: ",
+    ",当前无生效订阅,将自动使用钱包": ", нет активной подписки, автоматически будет использоваться кошелек.",
     "个已过期": "истекших",
     "订阅": "Подписка",
     "至": "до",
     "过期于": "Истекает",
+    "作废于": "Аннулировано",
     "购买套餐后即可享受模型权益": "После покупки плана доступны преимущества моделей",
     "限购": "Лимит",
     "推荐": "Рекомендуется",

+ 3 - 0
web/src/i18n/locales/vi.json

@@ -3254,9 +3254,12 @@
     "我的订阅": "Đăng ký của tôi",
     "个生效中": "gói đăng ký đang hiệu lực",
     "无生效": "Không có gói đăng ký hiệu lực",
+    "已保存偏好为": "Đã lưu tùy chọn: ",
+    ",当前无生效订阅,将自动使用钱包": ", hiện không có gói đăng ký hiệu lực, sẽ tự động dùng ví.",
     "个已过期": "gói đăng ký đã hết hạn",
     "订阅": "Đăng ký",
     "过期于": "Hết hạn vào",
+    "作废于": "Vô hiệu vào",
     "购买套餐后即可享受模型权益": "Mua gói để nhận quyền lợi mô hình",
     "限购": "Giới hạn mua",
     "推荐": "Đề xuất",

+ 3 - 0
web/src/i18n/locales/zh.json

@@ -2714,10 +2714,13 @@
     "我的订阅": "我的订阅",
     "个生效中": "个生效中",
     "无生效": "无生效",
+    "已保存偏好为": "已保存偏好为",
+    ",当前无生效订阅,将自动使用钱包": ",当前无生效订阅,将自动使用钱包",
     "个已过期": "个已过期",
     "订阅": "订阅",
     "至": "至",
     "过期于": "过期于",
+    "作废于": "作废于",
     "购买套餐后即可享受模型权益": "购买套餐后即可享受模型权益",
     "限购": "限购",
     "推荐": "推荐",