✨ 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:
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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é",
|
||||||
|
|||||||
@@ -2675,10 +2675,13 @@
|
|||||||
"我的订阅": "私のサブスクリプション",
|
"我的订阅": "私のサブスクリプション",
|
||||||
"个生效中": "件有効中",
|
"个生效中": "件有効中",
|
||||||
"无生效": "有効なし",
|
"无生效": "有効なし",
|
||||||
|
"已保存偏好为": "保存された設定は",
|
||||||
|
",当前无生效订阅,将自动使用钱包": "、有効なサブスクリプションがないため、自動的にウォレットを使用します",
|
||||||
"个已过期": "件期限切れ",
|
"个已过期": "件期限切れ",
|
||||||
"订阅": "サブスクリプション",
|
"订阅": "サブスクリプション",
|
||||||
"至": "まで",
|
"至": "まで",
|
||||||
"过期于": "有効期限",
|
"过期于": "有効期限",
|
||||||
|
"作废于": "無効化日",
|
||||||
"购买套餐后即可享受模型权益": "プラン購入後にモデル特典を利用できます",
|
"购买套餐后即可享受模型权益": "プラン購入後にモデル特典を利用できます",
|
||||||
"限购": "購入制限",
|
"限购": "購入制限",
|
||||||
"推荐": "おすすめ",
|
"推荐": "おすすめ",
|
||||||
|
|||||||
@@ -2705,10 +2705,13 @@
|
|||||||
"我的订阅": "Мои подписки",
|
"我的订阅": "Мои подписки",
|
||||||
"个生效中": "активных",
|
"个生效中": "активных",
|
||||||
"无生效": "Нет активных",
|
"无生效": "Нет активных",
|
||||||
|
"已保存偏好为": "Сохранённая настройка: ",
|
||||||
|
",当前无生效订阅,将自动使用钱包": ", нет активной подписки, автоматически будет использоваться кошелек.",
|
||||||
"个已过期": "истекших",
|
"个已过期": "истекших",
|
||||||
"订阅": "Подписка",
|
"订阅": "Подписка",
|
||||||
"至": "до",
|
"至": "до",
|
||||||
"过期于": "Истекает",
|
"过期于": "Истекает",
|
||||||
|
"作废于": "Аннулировано",
|
||||||
"购买套餐后即可享受模型权益": "После покупки плана доступны преимущества моделей",
|
"购买套餐后即可享受模型权益": "После покупки плана доступны преимущества моделей",
|
||||||
"限购": "Лимит",
|
"限购": "Лимит",
|
||||||
"推荐": "Рекомендуется",
|
"推荐": "Рекомендуется",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -2714,10 +2714,13 @@
|
|||||||
"我的订阅": "我的订阅",
|
"我的订阅": "我的订阅",
|
||||||
"个生效中": "个生效中",
|
"个生效中": "个生效中",
|
||||||
"无生效": "无生效",
|
"无生效": "无生效",
|
||||||
|
"已保存偏好为": "已保存偏好为",
|
||||||
|
",当前无生效订阅,将自动使用钱包": ",当前无生效订阅,将自动使用钱包",
|
||||||
"个已过期": "个已过期",
|
"个已过期": "个已过期",
|
||||||
"订阅": "订阅",
|
"订阅": "订阅",
|
||||||
"至": "至",
|
"至": "至",
|
||||||
"过期于": "过期于",
|
"过期于": "过期于",
|
||||||
|
"作废于": "作废于",
|
||||||
"购买套餐后即可享受模型权益": "购买套餐后即可享受模型权益",
|
"购买套餐后即可享受模型权益": "购买套餐后即可享受模型权益",
|
||||||
"限购": "限购",
|
"限购": "限购",
|
||||||
"推荐": "推荐",
|
"推荐": "推荐",
|
||||||
|
|||||||
Reference in New Issue
Block a user