chore: Improve subscription billing fallback and UI states

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.
This commit is contained in:
t0ng7u
2026-02-07 00:57:36 +08:00
parent e8177efee9
commit 4fd8d033cd
9 changed files with 92 additions and 12 deletions

View File

@@ -666,6 +666,22 @@ func GetAllActiveUserSubscriptions(userId int) ([]SubscriptionSummary, error) {
return buildSubscriptionSummaries(subs), nil 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. // GetAllUserSubscriptions returns all subscriptions (active and expired) for a user.
func GetAllUserSubscriptions(userId int) ([]SubscriptionSummary, error) { func GetAllUserSubscriptions(userId int) ([]SubscriptionSummary, error) {
if userId <= 0 { if userId <= 0 {

View File

@@ -323,6 +323,13 @@ func NewBillingSession(c *gin.Context, relayInfo *relaycommon.RelayInfo, preCons
case "subscription_first": case "subscription_first":
fallthrough fallthrough
default: 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() session, err := trySubscription()
if err != nil { if err != nil {
if err.GetErrorCode() == types.ErrorCodeInsufficientUserQuota { if err.GetErrorCode() == types.ErrorCodeInsufficientUserQuota {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3254,9 +3254,12 @@
"我的订阅": "Đăng ký của tôi", "我的订阅": "Đăng ký của tôi",
"个生效中": "gói đăng ký đang hiệu lực", "个生效中": "gói đăng ký đang hiệu lực",
"无生效": "Không có gói đăng ký 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", "个已过期": "gói đăng ký đã hết hạn",
"订阅": "Đăng ký", "订阅": "Đăng ký",
"过期于": "Hết hạn vào", "过期于": "Hết hạn vào",
"作废于": "Vô hiệu vào",
"购买套餐后即可享受模型权益": "Mua gói để nhận quyền lợi mô hình", "购买套餐后即可享受模型权益": "Mua gói để nhận quyền lợi mô hình",
"限购": "Giới hạn mua", "限购": "Giới hạn mua",
"推荐": "Đề xuất", "推荐": "Đề xuất",

View File

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