From 6b6c9904acb8321f5cd874db988154a32e53f0b7 Mon Sep 17 00:00:00 2001 From: CaIon Date: Tue, 26 May 2026 12:03:02 +0800 Subject: [PATCH] feat(subscription): support balance purchases Refs #3071. --- controller/subscription.go | 23 ++++ model/subscription.go | 101 ++++++++++++++++++ model/topup.go | 2 + router/api-router.go | 1 + web/default/src/features/subscriptions/api.ts | 7 ++ .../dialogs/subscription-purchase-dialog.tsx | 71 ++++++++++-- .../components/subscription-plans-card.tsx | 6 ++ web/default/src/features/wallet/index.tsx | 2 + web/default/src/i18n/locales/en.json | 3 + web/default/src/i18n/locales/fr.json | 3 + web/default/src/i18n/locales/ja.json | 3 + web/default/src/i18n/locales/ru.json | 3 + web/default/src/i18n/locales/vi.json | 3 + web/default/src/i18n/locales/zh.json | 3 + 14 files changed, 222 insertions(+), 9 deletions(-) diff --git a/controller/subscription.go b/controller/subscription.go index 7dc0d9eea..699ebce82 100644 --- a/controller/subscription.go +++ b/controller/subscription.go @@ -22,6 +22,10 @@ type BillingPreferenceRequest struct { BillingPreference string `json:"billing_preference"` } +type SubscriptionBalancePayRequest struct { + PlanId int `json:"plan_id"` +} + // ---- User APIs ---- func GetSubscriptionPlans(c *gin.Context) { @@ -92,6 +96,25 @@ func UpdateSubscriptionPreference(c *gin.Context) { common.ApiSuccess(c, gin.H{"billing_preference": pref}) } +func SubscriptionRequestBalancePay(c *gin.Context) { + if !requirePaymentCompliance(c) { + return + } + + userId := c.GetInt("id") + var req SubscriptionBalancePayRequest + if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 { + common.ApiErrorMsg(c, "参数错误") + return + } + + if err := model.PurchaseSubscriptionWithBalance(userId, req.PlanId); err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, nil) +} + // ---- Admin APIs ---- func AdminListSubscriptionPlans(c *gin.Context) { diff --git a/model/subscription.go b/model/subscription.go index 4ff5a204a..0d38e1a71 100644 --- a/model/subscription.go +++ b/model/subscription.go @@ -11,6 +11,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/pkg/cachex" "github.com/samber/hot" + "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -665,6 +666,106 @@ func AdminBindSubscription(userId int, planId int, sourceNote string) (string, e return "", nil } +func calcSubscriptionBalanceQuota(priceAmount float64) (int, error) { + if priceAmount <= 0 { + return 0, nil + } + if common.QuotaPerUnit <= 0 { + return 0, errors.New("额度单位配置错误") + } + quota := decimal.NewFromFloat(priceAmount). + Mul(decimal.NewFromFloat(common.QuotaPerUnit)). + Ceil(). + IntPart() + return int(quota), nil +} + +// PurchaseSubscriptionWithBalance creates a subscription by deducting the user's wallet quota. +func PurchaseSubscriptionWithBalance(userId int, planId int) error { + if userId <= 0 || planId <= 0 { + return errors.New("invalid userId or planId") + } + + var logPlanTitle string + var logMoney float64 + var chargedQuota int + var upgradeGroup string + err := DB.Transaction(func(tx *gorm.DB) error { + plan, err := getSubscriptionPlanByIdTx(tx, planId) + if err != nil { + return err + } + if !plan.Enabled { + return errors.New("套餐未启用") + } + if plan.PriceAmount < 0 { + return errors.New("套餐价格不能为负数") + } + + requiredQuota, err := calcSubscriptionBalanceQuota(plan.PriceAmount) + if err != nil { + return err + } + + var user User + if err := tx.Set("gorm:query_option", "FOR UPDATE").Where("id = ?", userId).First(&user).Error; err != nil { + return err + } + if requiredQuota > 0 && user.Quota < requiredQuota { + return errors.New("余额不足") + } + if requiredQuota > 0 { + if err := tx.Model(&User{}).Where("id = ?", userId). + Update("quota", gorm.Expr("quota - ?", requiredQuota)).Error; err != nil { + return err + } + } + + if _, err := CreateUserSubscriptionFromPlanTx(tx, userId, plan, PaymentMethodBalance); err != nil { + return err + } + + now := common.GetTimestamp() + tradeNo := fmt.Sprintf("SUBBALUSR%dNO%s%d", userId, common.GetRandomString(6), time.Now().UnixNano()) + order := &SubscriptionOrder{ + UserId: userId, + PlanId: plan.Id, + Money: plan.PriceAmount, + TradeNo: tradeNo, + PaymentMethod: PaymentMethodBalance, + PaymentProvider: PaymentProviderBalance, + Status: common.TopUpStatusSuccess, + CreateTime: now, + CompleteTime: now, + ProviderPayload: fmt.Sprintf("charged_quota=%d", requiredQuota), + } + if err := tx.Create(order).Error; err != nil { + return err + } + + logPlanTitle = plan.Title + logMoney = plan.PriceAmount + chargedQuota = requiredQuota + upgradeGroup = strings.TrimSpace(plan.UpgradeGroup) + return nil + }) + if err != nil { + return err + } + + if chargedQuota > 0 { + if err := cacheDecrUserQuota(userId, int64(chargedQuota)); err != nil { + common.SysLog("failed to decrease user quota cache after subscription balance purchase: " + err.Error()) + } + } + if upgradeGroup != "" { + _ = UpdateUserGroupCache(userId, upgradeGroup) + } + msg := fmt.Sprintf("使用余额购买订阅成功,套餐: %s,支付金额: %.2f,扣除额度: %d", logPlanTitle, logMoney, chargedQuota) + RecordLog(userId, LogTypeTopup, msg) + return nil +} + // GetAllActiveUserSubscriptions returns all active subscriptions for a user. func GetAllActiveUserSubscriptions(userId int) ([]SubscriptionSummary, error) { if userId <= 0 { diff --git a/model/topup.go b/model/topup.go index c071b77b5..83c5990df 100644 --- a/model/topup.go +++ b/model/topup.go @@ -29,6 +29,7 @@ const ( PaymentMethodCreem = "creem" PaymentMethodWaffo = "waffo" PaymentMethodWaffoPancake = "waffo_pancake" + PaymentMethodBalance = "balance" ) const ( @@ -37,6 +38,7 @@ const ( PaymentProviderCreem = "creem" PaymentProviderWaffo = "waffo" PaymentProviderWaffoPancake = "waffo_pancake" + PaymentProviderBalance = "balance" ) var ( diff --git a/router/api-router.go b/router/api-router.go index 7dfc648ee..381d2ccd0 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -153,6 +153,7 @@ func SetApiRouter(router *gin.Engine) { subscriptionRoute.GET("/plans", controller.GetSubscriptionPlans) subscriptionRoute.GET("/self", controller.GetSubscriptionSelf) subscriptionRoute.PUT("/self/preference", controller.UpdateSubscriptionPreference) + subscriptionRoute.POST("/balance/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestBalancePay) subscriptionRoute.POST("/epay/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestEpay) subscriptionRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestStripePay) subscriptionRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestCreemPay) diff --git a/web/default/src/features/subscriptions/api.ts b/web/default/src/features/subscriptions/api.ts index 23196b4fa..fd5423fb4 100644 --- a/web/default/src/features/subscriptions/api.ts +++ b/web/default/src/features/subscriptions/api.ts @@ -129,6 +129,13 @@ export async function paySubscriptionWaffoPancake( return res.data } +export async function paySubscriptionBalance( + data: SubscriptionPayRequest +): Promise { + const res = await api.post('/api/subscription/balance/pay', data) + return res.data +} + // Mints a Pancake OnetimeProduct (see controller for the OnetimeProduct vs // SubscriptionProduct rationale) using persisted creds + StoreID. export async function createWaffoPancakeSubscriptionProduct(data: { 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 71c77dd30..acde52404 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 @@ -20,7 +20,9 @@ import { useState, useEffect } from 'react' import { Crown, CalendarClock, Package } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' +import { DEFAULT_CURRENCY_CONFIG } from '@/stores/system-config-store' import { formatQuota } from '@/lib/format' +import { useSystemConfig } from '@/hooks/use-system-config' import { Alert, AlertDescription } from '@/components/ui/alert' import { Button } from '@/components/ui/button' import { @@ -44,6 +46,7 @@ import { paySubscriptionCreem, paySubscriptionEpay, paySubscriptionWaffoPancake, + paySubscriptionBalance, } from '../../api' import { formatDuration, formatResetPeriod } from '../../lib' import type { PlanRecord } from '../../types' @@ -64,10 +67,13 @@ interface Props { epayMethods?: PaymentMethod[] purchaseLimit?: number purchaseCount?: number + userQuota?: number + onPurchaseSuccess?: () => void | Promise } export function SubscriptionPurchaseDialog(props: Props) { const { t } = useTranslation() + const { currency } = useSystemConfig() const [paying, setPaying] = useState(false) const [selectedEpayMethod, setSelectedEpayMethod] = useState('') @@ -96,6 +102,16 @@ export function SubscriptionPurchaseDialog(props: Props) { t('Select payment method') const totalAmount = Number(plan.total_amount || 0) const price = Number(plan.price_amount || 0).toFixed(2) + const quotaPerUnit = + currency?.quotaPerUnit && currency.quotaPerUnit > 0 + ? currency.quotaPerUnit + : DEFAULT_CURRENCY_CONFIG.quotaPerUnit + const balanceCost = Math.max( + 0, + Math.ceil(Number(plan.price_amount || 0) * quotaPerUnit) + ) + const userQuota = Math.max(0, Number(props.userQuota || 0)) + const insufficientBalance = userQuota < balanceCost const limitReached = (props.purchaseLimit || 0) > 0 && (props.purchaseCount || 0) >= (props.purchaseLimit || 0) @@ -215,6 +231,28 @@ export function SubscriptionPurchaseDialog(props: Props) { } } + const handlePayBalance = async () => { + setPaying(true) + try { + const res = await paySubscriptionBalance({ plan_id: plan.id }) + if (res.success) { + toast.success(t('Subscription purchased successfully')) + void props.onPurchaseSuccess?.() + props.onOpenChange(false) + } else { + toast.error( + res.message && res.message !== 'success' + ? res.message + : t('Payment request failed') + ) + } + } catch { + toast.error(t('Payment request failed')) + } finally { + setPaying(false) + } + } + return ( @@ -285,7 +323,30 @@ export function SubscriptionPurchaseDialog(props: Props) { )} - {hasAnyPayment ? ( +
+
+ {t('Required')} + {formatQuota(balanceCost)} +
+
+ {t('Available')} + {formatQuota(userQuota)} +
+ {insufficientBalance && ( + + {t('Insufficient balance')} + + )} + +
+ + {hasAnyPayment && (

{t('Select payment method')} @@ -361,14 +422,6 @@ export function SubscriptionPurchaseDialog(props: Props) {

)} - ) : ( - - - {t( - 'Online payment is not enabled. Please contact the administrator.' - )} - - )}
diff --git a/web/default/src/features/wallet/components/subscription-plans-card.tsx b/web/default/src/features/wallet/components/subscription-plans-card.tsx index 8e876664e..5d3677564 100644 --- a/web/default/src/features/wallet/components/subscription-plans-card.tsx +++ b/web/default/src/features/wallet/components/subscription-plans-card.tsx @@ -62,6 +62,8 @@ import type { PaymentMethod, TopupInfo } from '../types' interface SubscriptionPlansCardProps { topupInfo: TopupInfo | null onAvailabilityChange?: (available: boolean) => void + userQuota?: number + onPurchaseSuccess?: () => void | Promise } function getEpayMethods(payMethods: PaymentMethod[] = []): PaymentMethod[] { @@ -91,6 +93,8 @@ function getBillingPreferenceLabel( export function SubscriptionPlansCard({ topupInfo, onAvailabilityChange, + userQuota, + onPurchaseSuccess, }: SubscriptionPlansCardProps) { const { t } = useTranslation() @@ -633,6 +637,8 @@ export function SubscriptionPlansCard({ enableWaffoPancake={enableWaffoPancake} enableOnlineTopUp={enableOnlineTopUp} epayMethods={epayMethods} + userQuota={userQuota} + onPurchaseSuccess={onPurchaseSuccess} purchaseLimit={ selectedPlan?.plan?.max_purchase_per_user ? Number(selectedPlan.plan.max_purchase_per_user) diff --git a/web/default/src/features/wallet/index.tsx b/web/default/src/features/wallet/index.tsx index 4439936c9..67eb8959a 100644 --- a/web/default/src/features/wallet/index.tsx +++ b/web/default/src/features/wallet/index.tsx @@ -309,6 +309,8 @@ export function Wallet(props: WalletProps) { diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 2fdb75fde..04486dfc5 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -2035,6 +2035,7 @@ "Inspect requests, errors, and billing details": "Inspect requests, errors, and billing details", "Inspect user prompts": "Inspect user prompts", "Instance": "Instance", + "Insufficient balance": "Insufficient balance", "Integrations": "Integrations", "Inter-group overrides": "Inter-group overrides", "Inter-group ratio overrides": "Inter-group ratio overrides", @@ -2852,6 +2853,7 @@ "Path:": "Path:", "Pay": "Pay", "Pay-as-you-go with real-time usage monitoring": "Pay-as-you-go with real-time usage monitoring", + "Pay with Balance": "Pay with Balance", "Payment": "Payment", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.", "Payment Channel": "Payment Channel", @@ -3763,6 +3765,7 @@ "Subscription First": "Subscription First", "Subscription Management": "Subscription Management", "Subscription Only": "Subscription Only", + "Subscription purchased successfully": "Subscription purchased successfully", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.", "Subscription Plans": "Subscription Plans", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index e02e01ad9..2bada212b 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -2035,6 +2035,7 @@ "Inspect requests, errors, and billing details": "Inspecter les requêtes, les erreurs et les détails de facturation", "Inspect user prompts": "Inspecter les invites utilisateur", "Instance": "Instance", + "Insufficient balance": "Solde insuffisant", "Integrations": "Intégrations", "Inter-group overrides": "Dérogations inter-groupes", "Inter-group ratio overrides": "Dérogations de ratio inter-groupes", @@ -2852,6 +2853,7 @@ "Path:": "Chemin :", "Pay": "Pay", "Pay-as-you-go with real-time usage monitoring": "Paiement à l'usage avec suivi de la consommation en temps réel", + "Pay with Balance": "Payer avec le solde", "Payment": "Paiement", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Mode agrégateur de paiement — embarquez avec votre propre société enregistrée (entité offshore). Conçu pour les entreprises.", "Payment Channel": "Canal de paiement", @@ -3763,6 +3765,7 @@ "Subscription First": "Abonnement en priorité", "Subscription Management": "Gestion des abonnements", "Subscription Only": "Abonnement uniquement", + "Subscription purchased successfully": "Abonnement acheté avec succès", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "La création et la modification des forfaits d’abonnement sont verrouillées jusqu’à ce que l’administrateur confirme les conditions de conformité dans les paramètres de la passerelle de paiement.", "Subscription Plans": "Plans d'abonnement", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Les forfaits d’abonnement n’utilisent PAS le produit associé : chaque forfait dispose de son propre produit Pancake dédié, défini dans l’administration des abonnements (ou créé automatiquement via le bouton « + Créer »).", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index a2beaf96d..2c5f35ec1 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -2035,6 +2035,7 @@ "Inspect requests, errors, and billing details": "リクエスト、エラー、請求詳細を確認", "Inspect user prompts": "ユーザープロンプトの検査", "Instance": "インスタンス", + "Insufficient balance": "残高が不足しています", "Integrations": "統合", "Inter-group overrides": "グループ間上書き", "Inter-group ratio overrides": "グループ間比率上書き", @@ -2852,6 +2853,7 @@ "Path:": "パス:", "Pay": "Pay", "Pay-as-you-go with real-time usage monitoring": "リアルタイム使用量監視付き従量課金制", + "Pay with Balance": "残高で支払う", "Payment": "支払い", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "決済アグリゲーターモード — 自社の登録済み法人(オフショア法人)でオンボーディングします。エンタープライズ向けです。", "Payment Channel": "決済チャネル", @@ -3763,6 +3765,7 @@ "Subscription First": "サブスクリプション優先", "Subscription Management": "サブスクリプション管理", "Subscription Only": "サブスクリプションのみ", + "Subscription purchased successfully": "サブスクリプションを購入しました", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "管理者が支払いゲートウェイ設定でコンプライアンス条件を確認するまで、サブスクリプションプランの作成と変更はロックされます。", "Subscription Plans": "サブスクリプションプラン", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "サブスクリプションプランは紐付け済み商品を使用しません。各プランには専用の Pancake 商品があり、サブスクリプション管理画面で設定します(または「+ 作成」ボタンで自動作成します)。", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index eee01191f..6e3bb8058 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -2035,6 +2035,7 @@ "Inspect requests, errors, and billing details": "Проверяйте запросы, ошибки и детали оплаты", "Inspect user prompts": "Просмотр запросов пользователя", "Instance": "Экземпляр", + "Insufficient balance": "Недостаточно средств", "Integrations": "Интеграции", "Inter-group overrides": "Переопределения между группами", "Inter-group ratio overrides": "Переопределения соотношений между группами", @@ -2852,6 +2853,7 @@ "Path:": "Путь:", "Pay": "Pay", "Pay-as-you-go with real-time usage monitoring": "Оплата по мере использования с мониторингом в реальном времени", + "Pay with Balance": "Оплатить балансом", "Payment": "Платеж", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Режим платежного агрегатора — подключение через вашу зарегистрированную компанию (офшорное юрлицо). Создано для Enterprise.", "Payment Channel": "Платёжный канал", @@ -3763,6 +3765,7 @@ "Subscription First": "Подписка в приоритете", "Subscription Management": "Управление подписками", "Subscription Only": "Только подписка", + "Subscription purchased successfully": "Подписка успешно приобретена", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Создание и изменение планов подписки заблокированы, пока администратор не подтвердит условия соответствия в настройках платежного шлюза.", "Subscription Plans": "Планы подписки", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Планы подписки НЕ используют привязанный продукт — у каждого плана есть собственный продукт Pancake, задаваемый в администрировании подписок (или автоматически создаваемый кнопкой «+ Создать»).", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 6f7d7a498..23441a9c6 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -2035,6 +2035,7 @@ "Inspect requests, errors, and billing details": "Kiểm tra yêu cầu, lỗi và chi tiết thanh toán", "Inspect user prompts": "Kiểm tra lời nhắc của người dùng", "Instance": "Phiên bản", + "Insufficient balance": "Số dư không đủ", "Integrations": "Tích hợp", "Inter-group overrides": "Ghi đè liên nhóm", "Inter-group ratio overrides": "Tỷ lệ liên nhóm ghi đè", @@ -2852,6 +2853,7 @@ "Path:": "Đường dẫn:", "Pay": "Pay", "Pay-as-you-go with real-time usage monitoring": "Thanh toán theo mức sử dụng với theo dõi mức sử dụng theo thời gian thực", + "Pay with Balance": "Thanh toán bằng số dư", "Payment": "Thanh toán", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "Chế độ tổng hợp thanh toán — đăng ký bằng công ty đã đăng ký của bạn (pháp nhân offshore). Dành cho doanh nghiệp.", "Payment Channel": "Kênh thanh toán", @@ -3763,6 +3765,7 @@ "Subscription First": "Ưu tiên đăng ký", "Subscription Management": "Quản lý đăng ký", "Subscription Only": "Chỉ đăng ký", + "Subscription purchased successfully": "Đã mua gói đăng ký thành công", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "Việc tạo và thay đổi gói đăng ký bị khóa cho đến khi quản trị viên xác nhận điều khoản tuân thủ trong cài đặt Cổng thanh toán.", "Subscription Plans": "Gói đăng ký", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "Gói đăng ký KHÔNG dùng Sản phẩm đã liên kết — mỗi gói có một sản phẩm Pancake riêng, được đặt trong quản trị Đăng ký (hoặc tự động tạo bằng nút \"+ Create\" tại đó).", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 7e36f2a79..c567f97f9 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -2035,6 +2035,7 @@ "Inspect requests, errors, and billing details": "查看请求、错误和计费详情", "Inspect user prompts": "检查用户提示", "Instance": "实例", + "Insufficient balance": "余额不足", "Integrations": "集成", "Inter-group overrides": "分组间覆盖", "Inter-group ratio overrides": "分组间比例覆盖", @@ -2852,6 +2853,7 @@ "Path:": "路径:", "Pay": "支付", "Pay-as-you-go with real-time usage monitoring": "按量付费,实时监控使用情况", + "Pay with Balance": "使用余额支付", "Payment": "支付", "Payment aggregator mode — onboard with your own registered company (offshore entity). Built for Enterprise.": "支付聚合模式:使用你自己的注册公司(离岸实体)入驻。面向企业场景构建。", "Payment Channel": "支付渠道", @@ -3763,6 +3765,7 @@ "Subscription First": "优先订阅", "Subscription Management": "订阅管理", "Subscription Only": "仅用订阅", + "Subscription purchased successfully": "订阅购买成功", "Subscription plan creation and changes are locked until the administrator confirms compliance terms in Payment Gateway settings.": "管理员在支付网关设置中确认合规条款之前,订阅套餐的创建和修改会被锁定。", "Subscription Plans": "订阅套餐", "Subscription plans do NOT use the bound Product — each plan has its own dedicated Pancake product, set in the Subscriptions admin (or auto-minted via the \"+ Create\" button there).": "订阅套餐不会使用已绑定的产品。每个套餐都有独立的 Pancake 产品,可在订阅管理中设置,或通过其中的“+ 创建”按钮自动生成。",