diff --git a/controller/subscription.go b/controller/subscription.go index 577e5196..8f53e5b8 100644 --- a/controller/subscription.go +++ b/controller/subscription.go @@ -168,6 +168,9 @@ func AdminCreateSubscriptionPlan(c *gin.Context) { if req.Plan.AllowBalancePay == nil { req.Plan.AllowBalancePay = common.GetPointer(true) } + if req.Plan.AllowWalletOverflow == nil { + req.Plan.AllowWalletOverflow = common.GetPointer(true) + } if req.Plan.DurationUnit == "" { req.Plan.DurationUnit = model.SubscriptionDurationMonth } @@ -189,6 +192,13 @@ func AdminCreateSubscriptionPlan(c *gin.Context) { return } } + req.Plan.DowngradeGroup = strings.TrimSpace(req.Plan.DowngradeGroup) + if req.Plan.DowngradeGroup != "" { + if _, ok := ratio_setting.GetGroupRatioCopy()[req.Plan.DowngradeGroup]; !ok { + common.ApiErrorMsg(c, "降级分组不存在") + return + } + } req.Plan.QuotaResetPeriod = model.NormalizeResetPeriod(req.Plan.QuotaResetPeriod) if req.Plan.QuotaResetPeriod == model.SubscriptionResetCustom && req.Plan.QuotaResetCustomSeconds <= 0 { common.ApiErrorMsg(c, "自定义重置周期需大于0秒") @@ -256,6 +266,13 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) { return } } + req.Plan.DowngradeGroup = strings.TrimSpace(req.Plan.DowngradeGroup) + if req.Plan.DowngradeGroup != "" { + if _, ok := ratio_setting.GetGroupRatioCopy()[req.Plan.DowngradeGroup]; !ok { + common.ApiErrorMsg(c, "降级分组不存在") + return + } + } req.Plan.QuotaResetPeriod = model.NormalizeResetPeriod(req.Plan.QuotaResetPeriod) if req.Plan.QuotaResetPeriod == model.SubscriptionResetCustom && req.Plan.QuotaResetCustomSeconds <= 0 { common.ApiErrorMsg(c, "自定义重置周期需大于0秒") @@ -280,6 +297,7 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) { "max_purchase_per_user": req.Plan.MaxPurchasePerUser, "total_amount": req.Plan.TotalAmount, "upgrade_group": req.Plan.UpgradeGroup, + "downgrade_group": req.Plan.DowngradeGroup, "quota_reset_period": req.Plan.QuotaResetPeriod, "quota_reset_custom_seconds": req.Plan.QuotaResetCustomSeconds, "updated_at": common.GetTimestamp(), @@ -287,6 +305,9 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) { if req.Plan.AllowBalancePay != nil { updateMap["allow_balance_pay"] = *req.Plan.AllowBalancePay } + if req.Plan.AllowWalletOverflow != nil { + updateMap["allow_wallet_overflow"] = *req.Plan.AllowWalletOverflow + } if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil { return err } diff --git a/model/main.go b/model/main.go index 6d900246..39e17ff9 100644 --- a/model/main.go +++ b/model/main.go @@ -398,11 +398,13 @@ func ensureSubscriptionPlanTableSQLite() error { ` + "`enabled`" + ` numeric DEFAULT 1, ` + "`sort_order`" + ` integer DEFAULT 0, ` + "`allow_balance_pay`" + ` numeric DEFAULT 1, +` + "`allow_wallet_overflow`" + ` numeric DEFAULT 1, ` + "`stripe_price_id`" + ` varchar(128) DEFAULT '', ` + "`creem_product_id`" + ` varchar(128) DEFAULT '', ` + "`waffo_pancake_product_id`" + ` varchar(128) DEFAULT '', ` + "`max_purchase_per_user`" + ` integer DEFAULT 0, ` + "`upgrade_group`" + ` varchar(64) DEFAULT '', +` + "`downgrade_group`" + ` varchar(64) DEFAULT '', ` + "`total_amount`" + ` bigint NOT NULL DEFAULT 0, ` + "`quota_reset_period`" + ` varchar(16) DEFAULT 'never', ` + "`quota_reset_custom_seconds`" + ` bigint DEFAULT 0, @@ -433,11 +435,13 @@ PRIMARY KEY (` + "`id`" + `) {Name: "enabled", DDL: "`enabled` numeric DEFAULT 1"}, {Name: "sort_order", DDL: "`sort_order` integer DEFAULT 0"}, {Name: "allow_balance_pay", DDL: "`allow_balance_pay` numeric DEFAULT 1"}, + {Name: "allow_wallet_overflow", DDL: "`allow_wallet_overflow` numeric DEFAULT 1"}, {Name: "stripe_price_id", DDL: "`stripe_price_id` varchar(128) DEFAULT ''"}, {Name: "creem_product_id", DDL: "`creem_product_id` varchar(128) DEFAULT ''"}, {Name: "waffo_pancake_product_id", DDL: "`waffo_pancake_product_id` varchar(128) DEFAULT ''"}, {Name: "max_purchase_per_user", DDL: "`max_purchase_per_user` integer DEFAULT 0"}, {Name: "upgrade_group", DDL: "`upgrade_group` varchar(64) DEFAULT ''"}, + {Name: "downgrade_group", DDL: "`downgrade_group` varchar(64) DEFAULT ''"}, {Name: "total_amount", DDL: "`total_amount` bigint NOT NULL DEFAULT 0"}, {Name: "quota_reset_period", DDL: "`quota_reset_period` varchar(16) DEFAULT 'never'"}, {Name: "quota_reset_custom_seconds", DDL: "`quota_reset_custom_seconds` bigint DEFAULT 0"}, diff --git a/model/subscription.go b/model/subscription.go index 2dc6db1c..12e4a02c 100644 --- a/model/subscription.go +++ b/model/subscription.go @@ -162,6 +162,9 @@ type SubscriptionPlan struct { AllowBalancePay *bool `json:"allow_balance_pay" gorm:"default:true"` + // Allow falling back to wallet balance after subscription quota is exhausted (empty = true) + AllowWalletOverflow *bool `json:"allow_wallet_overflow" gorm:"default:true"` + StripePriceId string `json:"stripe_price_id" gorm:"type:varchar(128);default:''"` CreemProductId string `json:"creem_product_id" gorm:"type:varchar(128);default:''"` WaffoPancakeProductId string `json:"waffo_pancake_product_id" gorm:"type:varchar(128);default:''"` @@ -172,6 +175,9 @@ type SubscriptionPlan struct { // Upgrade user group after purchase (empty = no change) UpgradeGroup string `json:"upgrade_group" gorm:"type:varchar(64);default:''"` + // Downgrade user group on expiry (empty = revert to the group held before purchase) + DowngradeGroup string `json:"downgrade_group" gorm:"type:varchar(64);default:''"` + // Total quota (amount in quota units, 0 = unlimited) TotalAmount int64 `json:"total_amount" gorm:"type:bigint;not null;default:0"` @@ -199,6 +205,9 @@ func (p *SubscriptionPlan) NormalizeDefaults() { if p.AllowBalancePay == nil { p.AllowBalancePay = common.GetPointer(true) } + if p.AllowWalletOverflow == nil { + p.AllowWalletOverflow = common.GetPointer(true) + } } // Subscription order (payment -> webhook -> create UserSubscription) @@ -261,6 +270,12 @@ type UserSubscription struct { UpgradeGroup string `json:"upgrade_group" gorm:"type:varchar(64);default:''"` PrevUserGroup string `json:"prev_user_group" gorm:"type:varchar(64);default:''"` + // Downgrade target group on expiry (snapshot from plan; empty = revert to PrevUserGroup) + DowngradeGroup string `json:"downgrade_group" gorm:"type:varchar(64);default:''"` + + // Whether wallet fallback is allowed after this subscription's quota is exhausted (snapshot from plan) + AllowWalletOverflow bool `json:"allow_wallet_overflow" gorm:"default:true"` + CreatedAt int64 `json:"created_at" gorm:"bigint"` UpdatedAt int64 `json:"updated_at" gorm:"bigint"` } @@ -416,17 +431,17 @@ func downgradeUserGroupForSubscriptionTx(tx *gorm.DB, sub *UserSubscription, now if tx == nil || sub == nil { return "", errors.New("invalid downgrade args") } + downgradeGroup := strings.TrimSpace(sub.DowngradeGroup) upgradeGroup := strings.TrimSpace(sub.UpgradeGroup) - if upgradeGroup == "" { + // Nothing to do if neither an explicit downgrade target nor an upgrade snapshot exists. + if downgradeGroup == "" && upgradeGroup == "" { return "", nil } currentGroup, err := getUserGroupByIdTx(tx, sub.UserId) if err != nil { return "", err } - if currentGroup != upgradeGroup { - return "", nil - } + // If another active upgraded subscription exists, keep the current group. var activeSub UserSubscription activeQuery := tx.Where("user_id = ? AND status = ? AND end_time > ? AND id <> ? AND upgrade_group <> ''", sub.UserId, "active", now, sub.Id). @@ -436,15 +451,24 @@ func downgradeUserGroupForSubscriptionTx(tx *gorm.DB, sub *UserSubscription, now if activeQuery.Error == nil && activeQuery.RowsAffected > 0 { return "", nil } - prevGroup := strings.TrimSpace(sub.PrevUserGroup) - if prevGroup == "" || prevGroup == currentGroup { + // Determine the downgrade target: an explicit downgrade group takes precedence, + // otherwise revert to the group held before purchase (legacy behavior). + target := downgradeGroup + if target == "" { + // Legacy behavior: only revert when the subscription actually elevated the user. + if currentGroup != upgradeGroup { + return "", nil + } + target = strings.TrimSpace(sub.PrevUserGroup) + } + if target == "" || target == currentGroup { return "", nil } if err := tx.Model(&User{}).Where("id = ?", sub.UserId). - Update("group", prevGroup).Error; err != nil { + Update("group", target).Error; err != nil { return "", err } - return prevGroup, nil + return target, nil } func CreateUserSubscriptionFromPlanTx(tx *gorm.DB, userId int, plan *SubscriptionPlan, source string) (*UserSubscription, error) { @@ -495,21 +519,27 @@ func CreateUserSubscriptionFromPlanTx(tx *gorm.DB, userId int, plan *Subscriptio } } } + allowWalletOverflow := true + if plan.AllowWalletOverflow != nil { + allowWalletOverflow = *plan.AllowWalletOverflow + } sub := &UserSubscription{ - UserId: userId, - PlanId: plan.Id, - AmountTotal: plan.TotalAmount, - AmountUsed: 0, - StartTime: now.Unix(), - EndTime: endUnix, - Status: "active", - Source: source, - LastResetTime: lastReset, - NextResetTime: nextReset, - UpgradeGroup: upgradeGroup, - PrevUserGroup: prevGroup, - CreatedAt: common.GetTimestamp(), - UpdatedAt: common.GetTimestamp(), + UserId: userId, + PlanId: plan.Id, + AmountTotal: plan.TotalAmount, + AmountUsed: 0, + StartTime: now.Unix(), + EndTime: endUnix, + Status: "active", + Source: source, + LastResetTime: lastReset, + NextResetTime: nextReset, + UpgradeGroup: upgradeGroup, + PrevUserGroup: prevGroup, + DowngradeGroup: strings.TrimSpace(plan.DowngradeGroup), + AllowWalletOverflow: allowWalletOverflow, + CreatedAt: common.GetTimestamp(), + UpdatedAt: common.GetTimestamp(), } if err := tx.Create(sub).Error; err != nil { return nil, err @@ -811,6 +841,24 @@ func HasActiveUserSubscription(userId int) (bool, error) { return count > 0, nil } +// UserActiveSubscriptionsAllowWalletOverflow returns whether wallet balance may be used +// after the user's subscription quota is exhausted. A single active subscription that +// disallows wallet overflow (allow_wallet_overflow = false) blocks the fallback. +func UserActiveSubscriptionsAllowWalletOverflow(userId int) (bool, error) { + if userId <= 0 { + return false, errors.New("invalid userId") + } + now := common.GetTimestamp() + var strictCount int64 + if err := DB.Model(&UserSubscription{}). + Where("user_id = ? AND status = ? AND end_time > ? AND allow_wallet_overflow = ?", + userId, "active", now, false). + Count(&strictCount).Error; err != nil { + return false, err + } + return strictCount == 0, nil +} + // GetAllUserSubscriptions returns all subscriptions (active and expired) for a user. func GetAllUserSubscriptions(userId int) ([]SubscriptionSummary, error) { if userId <= 0 { @@ -982,9 +1030,10 @@ func ExpireDueSubscriptions(limit int) (int, error) { return nil } - // No active upgraded subscription, downgrade to previous group if needed. + // Find the most recently expired subscription that defines a group transition + // (an explicit downgrade target or an upgrade snapshot to revert). var lastExpired UserSubscription - expiredQuery := tx.Where("user_id = ? AND status = ? AND upgrade_group <> ''", + expiredQuery := tx.Where("user_id = ? AND status = ? AND (downgrade_group <> '' OR upgrade_group <> '')", userId, "expired"). Order("end_time desc, id desc"). Limit(1). @@ -992,23 +1041,33 @@ func ExpireDueSubscriptions(limit int) (int, error) { if expiredQuery.Error != nil || expiredQuery.RowsAffected == 0 { return nil } - upgradeGroup := strings.TrimSpace(lastExpired.UpgradeGroup) - prevGroup := strings.TrimSpace(lastExpired.PrevUserGroup) - if upgradeGroup == "" || prevGroup == "" { - return nil - } currentGroup, err := getUserGroupByIdTx(tx, userId) if err != nil { return err } - if currentGroup != upgradeGroup || currentGroup == prevGroup { + // An explicit downgrade group takes precedence; otherwise revert to the + // group held before purchase (legacy behavior, only when the subscription + // actually elevated the user). + target := strings.TrimSpace(lastExpired.DowngradeGroup) + if target == "" { + upgradeGroup := strings.TrimSpace(lastExpired.UpgradeGroup) + prevGroup := strings.TrimSpace(lastExpired.PrevUserGroup) + if upgradeGroup == "" || prevGroup == "" { + return nil + } + if currentGroup != upgradeGroup { + return nil + } + target = prevGroup + } + if target == "" || target == currentGroup { return nil } if err := tx.Model(&User{}).Where("id = ?", userId). - Update("group", prevGroup).Error; err != nil { + Update("group", target).Error; err != nil { return err } - cacheGroup = prevGroup + cacheGroup = target return nil }) if err != nil { diff --git a/service/billing_session.go b/service/billing_session.go index 4761f7a1..32344eaf 100644 --- a/service/billing_session.go +++ b/service/billing_session.go @@ -425,7 +425,15 @@ func NewBillingSession(c *gin.Context, relayInfo *relaycommon.RelayInfo, preCons session, apiErr := trySubscription() if apiErr != nil { if apiErr.GetErrorCode() == types.ErrorCodeInsufficientUserQuota { - return tryWallet() + // 仅当用户的活跃订阅允许钱包回退时才回退到钱包,否则返回订阅额度不足错误 + allowOverflow, overflowErr := model.UserActiveSubscriptionsAllowWalletOverflow(relayInfo.UserId) + if overflowErr != nil { + return nil, types.NewError(overflowErr, types.ErrorCodeQueryDataError, types.ErrOptionWithSkipRetry()) + } + if allowOverflow { + return tryWallet() + } + return nil, apiErr } return nil, apiErr } diff --git a/web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx b/web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx index 04a428d0..cfe4a994 100644 --- a/web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx +++ b/web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx @@ -297,7 +297,7 @@ export function SubscriptionPurchaseDialog(props: Props) { )}
- {t('Received amount')} + {t('Plan Quota')} diff --git a/web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx b/web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx index ea5276fa..e197452d 100644 --- a/web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx +++ b/web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx @@ -52,6 +52,7 @@ import { invalidateUserSubscription, deleteUserSubscription, } from '../../api' +import { formatQuota } from '@/lib/format' import { formatTimestamp } from '../../lib' import type { PlanRecord, UserSubscriptionRecord } from '../../types' @@ -301,7 +302,9 @@ export function UserSubscriptionsDialog(props: Props) { const sub = record.subscription const total = Number(sub.amount_total || 0) const used = Number(sub.amount_used || 0) - return total > 0 ? `${used}/${total}` : t('Unlimited') + return total > 0 + ? `${formatQuota(used)}/${formatQuota(total)}` + : t('Unlimited') }, }, { diff --git a/web/default/src/features/subscriptions/components/subscriptions-columns.tsx b/web/default/src/features/subscriptions/components/subscriptions-columns.tsx index 17bc0872..c4070472 100644 --- a/web/default/src/features/subscriptions/components/subscriptions-columns.tsx +++ b/web/default/src/features/subscriptions/components/subscriptions-columns.tsx @@ -160,7 +160,7 @@ export function useSubscriptionsColumns(): ColumnDef[] { }, { id: 'total_amount', - header: t('Received amount'), + header: t('Plan Quota'), meta: { mobileHidden: true }, cell: ({ row }) => { const total = Number(row.original.plan.total_amount || 0) diff --git a/web/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsx b/web/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsx index 9882813a..85548f09 100644 --- a/web/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsx +++ b/web/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsx @@ -66,6 +66,7 @@ import { createWaffoPancakeSubscriptionProduct, listWaffoPancakeSubscriptionProductOptions, } from '../api' +import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency' import { getDurationUnitOptions, getResetPeriodOptions } from '../constants' import { getPlanFormSchema, @@ -91,6 +92,9 @@ export function SubscriptionsMutateDrawer({ const { t } = useTranslation() const isEdit = !!currentRow?.plan?.id const { triggerRefresh } = useSubscriptions() + const { meta: currencyMeta } = getCurrencyDisplay() + const tokensOnly = currencyMeta.kind === 'tokens' + const currencyLabel = getCurrencyLabel() const [isSubmitting, setIsSubmitting] = useState(false) const [groupOptions, setGroupOptions] = useState([]) const [creatingPancakeProduct, setCreatingPancakeProduct] = useState(false) @@ -314,7 +318,7 @@ export function SubscriptionsMutateDrawer({ name='price_amount' render={({ field }) => ( - {t('Actual Amount')} + {t('Plan Price')} ( - {t('Received amount')} + + {t('Quota ({{currency}})', { currency: currencyLabel })} + field.onChange(parseFloat(e.target.value) || 0) } @@ -349,7 +363,7 @@ export function SubscriptionsMutateDrawer({ {t( - '0 means unlimited. The value is converted to quota units when saved.' + 'Total quota included in the plan, usable per billing period. 0 means unlimited.' )} @@ -398,6 +412,53 @@ export function SubscriptionsMutateDrawer({ )} /> + ( + + {t('Downgrade Group')} + + + {t('Downgrade to this group after the subscription expires')} + + + + )} + /> +
-
- ( - - {t('Sort Order')} - - - field.onChange(parseInt(e.target.value, 10) || 0) - } - /> - - - - )} - /> + ( + + {t('Sort Order')} + + + field.onChange(parseInt(e.target.value, 10) || 0) + } + /> + + + + )} + /> +
)} /> + + ( + + + {t('Allow wallet balance after quota used up')} + + + + + + )} + />
diff --git a/web/default/src/features/subscriptions/lib/plan-form.ts b/web/default/src/features/subscriptions/lib/plan-form.ts index f3a4a9dc..381f26bc 100644 --- a/web/default/src/features/subscriptions/lib/plan-form.ts +++ b/web/default/src/features/subscriptions/lib/plan-form.ts @@ -40,9 +40,11 @@ export function getPlanFormSchema(t: TFunction) { enabled: z.boolean(), sort_order: z.coerce.number(), allow_balance_pay: z.boolean(), + allow_wallet_overflow: z.boolean(), max_purchase_per_user: z.coerce.number().min(0), total_amount: z.coerce.number().min(0), upgrade_group: z.string().optional(), + downgrade_group: z.string().optional(), stripe_price_id: z.string().optional(), creem_product_id: z.string().optional(), waffo_pancake_product_id: z.string().optional(), @@ -63,9 +65,11 @@ export const PLAN_FORM_DEFAULTS: PlanFormValues = { enabled: true, sort_order: 0, allow_balance_pay: true, + allow_wallet_overflow: true, max_purchase_per_user: 0, total_amount: 0, upgrade_group: '', + downgrade_group: '', stripe_price_id: '', creem_product_id: '', waffo_pancake_product_id: '', @@ -84,9 +88,11 @@ export function planToFormValues(plan: SubscriptionPlan): PlanFormValues { enabled: plan.enabled !== false, sort_order: Number(plan.sort_order || 0), allow_balance_pay: plan.allow_balance_pay !== false, + allow_wallet_overflow: plan.allow_wallet_overflow !== false, max_purchase_per_user: Number(plan.max_purchase_per_user || 0), total_amount: quotaUnitsToDollars(Number(plan.total_amount || 0)), upgrade_group: plan.upgrade_group || '', + downgrade_group: plan.downgrade_group || '', stripe_price_id: plan.stripe_price_id || '', creem_product_id: plan.creem_product_id || '', waffo_pancake_product_id: plan.waffo_pancake_product_id || '', @@ -110,6 +116,7 @@ export function formValuesToPlanPayload(values: PlanFormValues): PlanPayload { max_purchase_per_user: Number(values.max_purchase_per_user || 0), total_amount: parseQuotaFromDollars(Number(values.total_amount || 0)), upgrade_group: values.upgrade_group || '', + downgrade_group: values.downgrade_group || '', }, } } diff --git a/web/default/src/features/subscriptions/types.ts b/web/default/src/features/subscriptions/types.ts index 10768d69..7d32c6c8 100644 --- a/web/default/src/features/subscriptions/types.ts +++ b/web/default/src/features/subscriptions/types.ts @@ -36,9 +36,11 @@ export const subscriptionPlanSchema = z.object({ enabled: z.boolean(), sort_order: z.number(), allow_balance_pay: z.boolean().optional().default(true), + allow_wallet_overflow: z.boolean().optional().default(true), max_purchase_per_user: z.number(), total_amount: z.number(), upgrade_group: z.string().optional(), + downgrade_group: z.string().optional(), stripe_price_id: z.string().optional(), creem_product_id: z.string().optional(), waffo_pancake_product_id: z.string().optional(), diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index c833aca9..a6cc429f 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -138,6 +138,9 @@ "Active models": "Active models", "active users": "active users", "Actual Amount": "Actual Amount", + "Plan Price": "Plan Price", + "Plan Quota": "Plan Quota", + "Total quota included in the plan, usable per billing period. 0 means unlimited.": "Total quota included in the plan, usable per billing period. 0 means unlimited.", "Actual Model": "Actual Model", "Actual Model:": "Actual Model:", "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.", @@ -276,6 +279,10 @@ "Allocated Memory": "Allocated Memory", "Allow accountFilter parameter": "Allow accountFilter parameter", "Allow balance redemption": "Allow balance redemption", + "Allow wallet balance after quota used up": "Allow wallet balance after quota used up", + "Downgrade Group": "Downgrade Group", + "Downgrade to pre-purchase group": "Downgrade to pre-purchase group", + "Downgrade to this group after the subscription expires": "Downgrade to this group after the subscription expires", "Allow Claude beta query passthrough": "Allow Claude beta query passthrough", "Allow clients to query configured ratios via `/api/ratio`.": "Allow clients to query configured ratios via `/api/ratio`.", "Allow HTTP image requests": "Allow HTTP image requests", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 01e5cea2..9decf342 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -138,6 +138,9 @@ "Active models": "Modèles actifs", "active users": "utilisateurs actifs", "Actual Amount": "Montant réel", + "Plan Price": "Prix du forfait", + "Plan Quota": "Quota du forfait", + "Total quota included in the plan, usable per billing period. 0 means unlimited.": "Quota total inclus dans le forfait, utilisable par période de facturation. 0 signifie illimité.", "Actual Model": "Modèle réel", "Actual Model:": "Modèle réel :", "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Adapter les requêtes avec le suffixe `-thinking` au comportement de pensée natif d’Anthropic tout en gardant une facturation prévisible.", @@ -276,6 +279,10 @@ "Allocated Memory": "Mémoire allouée", "Allow accountFilter parameter": "Autoriser le paramètre accountFilter", "Allow balance redemption": "Autoriser le paiement avec le solde", + "Allow wallet balance after quota used up": "Autoriser le solde du portefeuille une fois le quota épuisé", + "Downgrade Group": "Groupe de rétrogradation", + "Downgrade to pre-purchase group": "Rétrograder vers le groupe d'avant l'achat", + "Downgrade to this group after the subscription expires": "Rétrograder vers ce groupe après l'expiration de l'abonnement", "Allow Claude beta query passthrough": "Autoriser le passage des requêtes bêta Claude", "Allow clients to query configured ratios via `/api/ratio`.": "Autoriser les clients à interroger les ratios configurés via `/api/ratio`.", "Allow HTTP image requests": "Autoriser les requêtes d'images HTTP", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 271b758b..fdf17a21 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -138,6 +138,9 @@ "Active models": "アクティブなモデル", "active users": "アクティブユーザー", "Actual Amount": "実際の金額", + "Plan Price": "プラン価格", + "Plan Quota": "プランのクォータ", + "Total quota included in the plan, usable per billing period. 0 means unlimited.": "プランに含まれる合計クォータ。請求期間ごとに使用可能。0 は無制限を意味します。", "Actual Model": "実際のモデル", "Actual Model:": "実際のモデル:", "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "`-thinking` サフィックス付きリクエストを Anthropic ネイティブの思考動作に適配し、課金を予測可能に保ちます。", @@ -276,6 +279,10 @@ "Allocated Memory": "割り当て済みメモリ", "Allow accountFilter parameter": "accountFilter パラメータを許可", "Allow balance redemption": "残高での交換を許可", + "Allow wallet balance after quota used up": "クォータ使い切り後にウォレット残高の使用を許可", + "Downgrade Group": "ダウングレードグループ", + "Downgrade to pre-purchase group": "購入前のグループにダウングレード", + "Downgrade to this group after the subscription expires": "サブスクリプションの有効期限が切れた後、このグループにダウングレードします", "Allow Claude beta query passthrough": "Claude ベータクエリのパススルーを許可", "Allow clients to query configured ratios via `/api/ratio`.": "クライアントが `/api/ratio` 経由で設定された比率を照会できるようにします。", "Allow HTTP image requests": "HTTP画像リクエストを許可", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index cc814260..53445417 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -138,6 +138,9 @@ "Active models": "Активные модели", "active users": "активных пользователей", "Actual Amount": "Фактическая сумма", + "Plan Price": "Цена тарифа", + "Plan Quota": "Квота тарифа", + "Total quota included in the plan, usable per billing period. 0 means unlimited.": "Общая квота, включённая в тариф, доступна за каждый расчётный период. 0 означает безлимит.", "Actual Model": "Фактическая модель", "Actual Model:": "Фактическая модель:", "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Адаптировать запросы с суффиксом `-thinking` к собственному режиму размышления Anthropic, сохраняя предсказуемость биллинга.", @@ -276,6 +279,10 @@ "Allocated Memory": "Выделенная память", "Allow accountFilter parameter": "Разрешить параметр accountFilter", "Allow balance redemption": "Разрешить оплату балансом", + "Allow wallet balance after quota used up": "Разрешить использование баланса кошелька после исчерпания квоты", + "Downgrade Group": "Понизить группу", + "Downgrade to pre-purchase group": "Понизить до группы до покупки", + "Downgrade to this group after the subscription expires": "Понизить до этой группы после истечения подписки", "Allow Claude beta query passthrough": "Разрешить проброс бета-запросов Claude", "Allow clients to query configured ratios via `/api/ratio`.": "Разрешить клиентам запрашивать настроенные соотношения через `/api/ratio`.", "Allow HTTP image requests": "Разрешить HTTP-запросы изображений", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index d61691ee..4569ffa7 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -138,6 +138,9 @@ "Active models": "Mô hình đang hoạt động", "active users": "Người dùng tích cực", "Actual Amount": "Số tiền thực tế", + "Plan Price": "Giá gói", + "Plan Quota": "Hạn ngạch gói", + "Total quota included in the plan, usable per billing period. 0 means unlimited.": "Tổng hạn ngạch bao gồm trong gói, dùng được mỗi kỳ thanh toán. 0 nghĩa là không giới hạn.", "Actual Model": "Mô hình thực tế", "Actual Model:": "Mô hình thực tế:", "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "Điều chỉnh các yêu cầu có hậu tố `-thinking` sang hành vi suy luận gốc của Anthropic trong khi vẫn giữ tính phí dễ dự đoán.", @@ -276,6 +279,10 @@ "Allocated Memory": "Bộ nhớ đã cấp phát", "Allow accountFilter parameter": "Cho phép tham số accountFilter", "Allow balance redemption": "Cho phép thanh toán bằng số dư", + "Allow wallet balance after quota used up": "Cho phép dùng số dư ví sau khi dùng hết hạn ngạch", + "Downgrade Group": "Nhóm hạ cấp", + "Downgrade to pre-purchase group": "Hạ xuống nhóm trước khi mua", + "Downgrade to this group after the subscription expires": "Hạ xuống nhóm này sau khi đăng ký hết hạn", "Allow Claude beta query passthrough": "Cho phép chuyển tiếp truy vấn beta Claude", "Allow clients to query configured ratios via `/api/ratio`.": "Cho phép khách hàng truy vấn các tỷ lệ đã cấu hình thông qua `/api/ratio`.", "Allow HTTP image requests": "Cho phép yêu cầu hình ảnh HTTP", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 07f56be3..7c9b5923 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -138,6 +138,9 @@ "Active models": "活跃模型", "active users": "活跃用户", "Actual Amount": "实付金额", + "Plan Price": "套餐价格", + "Plan Quota": "套餐额度", + "Total quota included in the plan, usable per billing period. 0 means unlimited.": "套餐包含的总额度,每个计费周期可用;0 表示不限量", "Actual Model": "实际模型", "Actual Model:": "实际模型:", "Adapt `-thinking` suffix requests to Anthropic native thinking behavior while keeping billing predictable.": "将带 `-thinking` 后缀的请求适配为 Anthropic 原生思考请求,并保持计费可预测。", @@ -276,6 +279,10 @@ "Allocated Memory": "已分配内存", "Allow accountFilter parameter": "允许 accountFilter 参数", "Allow balance redemption": "允许余额兑换", + "Allow wallet balance after quota used up": "额度用尽后允许使用钱包余额", + "Downgrade Group": "降级分组", + "Downgrade to pre-purchase group": "降级到购买前分组", + "Downgrade to this group after the subscription expires": "订阅过期后降级到该分组", "Allow Claude beta query passthrough": "允许 Claude beta 查询透传", "Allow clients to query configured ratios via `/api/ratio`.": "允许客户端通过 `/api/ratio` 查询配置的比例。", "Allow HTTP image requests": "允许 HTTP 图像请求", diff --git a/web/default/src/i18n/static-keys.ts b/web/default/src/i18n/static-keys.ts index d3482b43..a9f4b188 100644 --- a/web/default/src/i18n/static-keys.ts +++ b/web/default/src/i18n/static-keys.ts @@ -350,6 +350,10 @@ export const STATIC_I18N_KEYS = [ 'Priority', 'Payment Channel', 'No Upgrade', + 'Downgrade to pre-purchase group', + 'Downgrade Group', + 'Downgrade to this group after the subscription expires', + 'Allow wallet balance after quota used up', 'Unlimited', 'Update plan info', 'Create new subscription plan', @@ -361,6 +365,9 @@ export const STATIC_I18N_KEYS = [ 'Plan Subtitle', 'e.g. Suitable for light usage', 'Actual Amount', + 'Plan Price', + 'Plan Quota', + 'Total quota included in the plan, usable per billing period. 0 means unlimited.', 'Total Quota', '0 means unlimited', 'Sort Order',