mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 00:27:02 +00:00
✨ feat: harden subscription billing and improve UI consistency
Improve subscription payment safety and data integrity by handling user/URL lookup failures, fixing Stripe subscription mode, persisting quota reset fields, and correcting subscription delta accounting and DB timestamp casting. Refine the UI with stricter custom duration validation, accurate currency rounding, conditional Epay labeling, rollback on preference update failure, and shared subscription formatting helpers plus clearer component naming.
This commit is contained in:
@@ -207,21 +207,23 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) {
|
||||
err := model.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// update plan (allow zero values updates with map)
|
||||
updateMap := map[string]interface{}{
|
||||
"title": req.Plan.Title,
|
||||
"subtitle": req.Plan.Subtitle,
|
||||
"price_amount": req.Plan.PriceAmount,
|
||||
"currency": req.Plan.Currency,
|
||||
"duration_unit": req.Plan.DurationUnit,
|
||||
"duration_value": req.Plan.DurationValue,
|
||||
"custom_seconds": req.Plan.CustomSeconds,
|
||||
"enabled": req.Plan.Enabled,
|
||||
"sort_order": req.Plan.SortOrder,
|
||||
"stripe_price_id": req.Plan.StripePriceId,
|
||||
"creem_product_id": req.Plan.CreemProductId,
|
||||
"max_purchase_per_user": req.Plan.MaxPurchasePerUser,
|
||||
"total_amount": req.Plan.TotalAmount,
|
||||
"upgrade_group": req.Plan.UpgradeGroup,
|
||||
"updated_at": common.GetTimestamp(),
|
||||
"title": req.Plan.Title,
|
||||
"subtitle": req.Plan.Subtitle,
|
||||
"price_amount": req.Plan.PriceAmount,
|
||||
"currency": req.Plan.Currency,
|
||||
"duration_unit": req.Plan.DurationUnit,
|
||||
"duration_value": req.Plan.DurationValue,
|
||||
"custom_seconds": req.Plan.CustomSeconds,
|
||||
"enabled": req.Plan.Enabled,
|
||||
"sort_order": req.Plan.SortOrder,
|
||||
"stripe_price_id": req.Plan.StripePriceId,
|
||||
"creem_product_id": req.Plan.CreemProductId,
|
||||
"max_purchase_per_user": req.Plan.MaxPurchasePerUser,
|
||||
"total_amount": req.Plan.TotalAmount,
|
||||
"upgrade_group": req.Plan.UpgradeGroup,
|
||||
"quota_reset_period": req.Plan.QuotaResetPeriod,
|
||||
"quota_reset_custom_seconds": req.Plan.QuotaResetCustomSeconds,
|
||||
"updated_at": common.GetTimestamp(),
|
||||
}
|
||||
if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -54,7 +54,15 @@ func SubscriptionRequestCreemPay(c *gin.Context) {
|
||||
}
|
||||
|
||||
userId := c.GetInt("id")
|
||||
user, _ := model.GetUserById(userId, false)
|
||||
user, err := model.GetUserById(userId, false)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
common.ApiErrorMsg(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if plan.MaxPurchasePerUser > 0 {
|
||||
count, err := model.CountUserSubscriptionsByPlan(userId, plan.Id)
|
||||
|
||||
@@ -61,8 +61,16 @@ func SubscriptionRequestEpay(c *gin.Context) {
|
||||
}
|
||||
|
||||
callBackAddress := service.GetCallbackAddress()
|
||||
returnUrl, _ := url.Parse(callBackAddress + "/api/subscription/epay/return")
|
||||
notifyUrl, _ := url.Parse(callBackAddress + "/api/subscription/epay/notify")
|
||||
returnUrl, err := url.Parse(callBackAddress + "/api/subscription/epay/return")
|
||||
if err != nil {
|
||||
common.ApiErrorMsg(c, "回调地址配置错误")
|
||||
return
|
||||
}
|
||||
notifyUrl, err := url.Parse(callBackAddress + "/api/subscription/epay/notify")
|
||||
if err != nil {
|
||||
common.ApiErrorMsg(c, "回调地址配置错误")
|
||||
return
|
||||
}
|
||||
|
||||
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
|
||||
tradeNo = fmt.Sprintf("SUBUSR%dNO%s", userId, tradeNo)
|
||||
|
||||
@@ -51,7 +51,15 @@ func SubscriptionRequestStripePay(c *gin.Context) {
|
||||
}
|
||||
|
||||
userId := c.GetInt("id")
|
||||
user, _ := model.GetUserById(userId, false)
|
||||
user, err := model.GetUserById(userId, false)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
common.ApiErrorMsg(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if plan.MaxPurchasePerUser > 0 {
|
||||
count, err := model.CountUserSubscriptionsByPlan(userId, plan.Id)
|
||||
@@ -110,7 +118,7 @@ func genStripeSubscriptionLink(referenceId string, customerId string, email stri
|
||||
Quantity: stripe.Int64(1),
|
||||
},
|
||||
},
|
||||
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
|
||||
Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)),
|
||||
}
|
||||
|
||||
if "" == customerId {
|
||||
|
||||
@@ -9,7 +9,7 @@ func GetDBTimestamp() int64 {
|
||||
var err error
|
||||
switch {
|
||||
case common.UsingPostgreSQL:
|
||||
err = DB.Raw("SELECT EXTRACT(EPOCH FROM NOW())").Scan(&ts).Error
|
||||
err = DB.Raw("SELECT EXTRACT(EPOCH FROM NOW())::bigint").Scan(&ts).Error
|
||||
case common.UsingSQLite:
|
||||
err = DB.Raw("SELECT strftime('%s','now')").Scan(&ts).Error
|
||||
default:
|
||||
|
||||
@@ -508,7 +508,7 @@ func PostConsumeQuota(relayInfo *relaycommon.RelayInfo, quota int, preConsumedQu
|
||||
if relayInfo.SubscriptionId == 0 {
|
||||
return errors.New("subscription id is missing")
|
||||
}
|
||||
delta := int64(quota) - relayInfo.SubscriptionPreConsumed
|
||||
delta := int64(quota)
|
||||
if delta != 0 {
|
||||
if err := model.PostConsumeUserSubscriptionDelta(relayInfo.SubscriptionId, delta); err != nil {
|
||||
return err
|
||||
|
||||
@@ -202,9 +202,10 @@ const renderResetPeriod = (text, record, t) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderPaymentConfig = (text, record, t) => {
|
||||
const renderPaymentConfig = (text, record, t, enableEpay) => {
|
||||
const hasStripe = !!record?.plan?.stripe_price_id;
|
||||
const hasCreem = !!record?.plan?.creem_product_id;
|
||||
const hasEpay = !!enableEpay;
|
||||
|
||||
return (
|
||||
<Space spacing={4}>
|
||||
@@ -218,9 +219,11 @@ const renderPaymentConfig = (text, record, t) => {
|
||||
Creem
|
||||
</Tag>
|
||||
)}
|
||||
<Tag color='light-green' shape='circle'>
|
||||
{t('易支付')}
|
||||
</Tag>
|
||||
{hasEpay && (
|
||||
<Tag color='light-green' shape='circle'>
|
||||
{t('易支付')}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
@@ -274,7 +277,12 @@ const renderOperations = (text, record, { openEdit, setPlanEnabled, t }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getSubscriptionsColumns = ({ t, openEdit, setPlanEnabled }) => {
|
||||
export const getSubscriptionsColumns = ({
|
||||
t,
|
||||
openEdit,
|
||||
setPlanEnabled,
|
||||
enableEpay,
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
title: 'ID',
|
||||
@@ -324,7 +332,8 @@ export const getSubscriptionsColumns = ({ t, openEdit, setPlanEnabled }) => {
|
||||
{
|
||||
title: t('支付渠道'),
|
||||
width: 180,
|
||||
render: (text, record) => renderPaymentConfig(text, record, t),
|
||||
render: (text, record) =>
|
||||
renderPaymentConfig(text, record, t, enableEpay),
|
||||
},
|
||||
{
|
||||
title: t('总额度'),
|
||||
|
||||
@@ -27,16 +27,24 @@ import {
|
||||
import { getSubscriptionsColumns } from './SubscriptionsColumnDefs';
|
||||
|
||||
const SubscriptionsTable = (subscriptionsData) => {
|
||||
const { plans, loading, compactMode, openEdit, setPlanEnabled, t } =
|
||||
subscriptionsData;
|
||||
const {
|
||||
plans,
|
||||
loading,
|
||||
compactMode,
|
||||
openEdit,
|
||||
setPlanEnabled,
|
||||
t,
|
||||
enableEpay,
|
||||
} = subscriptionsData;
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return getSubscriptionsColumns({
|
||||
t,
|
||||
openEdit,
|
||||
setPlanEnabled,
|
||||
enableEpay,
|
||||
});
|
||||
}, [t, openEdit, setPlanEnabled]);
|
||||
}, [t, openEdit, setPlanEnabled, enableEpay]);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
return compactMode
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { Banner } from '@douyinfe/semi-ui';
|
||||
import CardPro from '../../common/ui/CardPro';
|
||||
import SubscriptionsTable from './SubscriptionsTable';
|
||||
@@ -27,10 +27,13 @@ import AddEditSubscriptionModal from './modals/AddEditSubscriptionModal';
|
||||
import { useSubscriptionsData } from '../../../hooks/subscriptions/useSubscriptionsData';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
import { createCardProPagination } from '../../../helpers/utils';
|
||||
import { StatusContext } from '../../../context/Status';
|
||||
|
||||
const SubscriptionsPage = () => {
|
||||
const subscriptionsData = useSubscriptionsData();
|
||||
const isMobile = useIsMobile();
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const enableEpay = !!statusState?.status?.enable_online_topup;
|
||||
|
||||
const {
|
||||
showEdit,
|
||||
@@ -91,7 +94,7 @@ const SubscriptionsPage = () => {
|
||||
})}
|
||||
t={t}
|
||||
>
|
||||
<SubscriptionsTable {...subscriptionsData} />
|
||||
<SubscriptionsTable {...subscriptionsData} enableEpay={enableEpay} />
|
||||
</CardPro>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -423,7 +423,7 @@ const AddEditSubscriptionModal = ({
|
||||
field='custom_seconds'
|
||||
label={t('自定义秒数')}
|
||||
required
|
||||
min={0}
|
||||
min={1}
|
||||
precision={0}
|
||||
rules={[{ required: true, message: t('请输入秒数') }]}
|
||||
style={{ width: '100%' }}
|
||||
|
||||
@@ -35,45 +35,13 @@ import { API, showError, showSuccess, renderQuota } from '../../helpers';
|
||||
import { getCurrencyConfig } from '../../helpers/render';
|
||||
import { Crown, RefreshCw, Sparkles } from 'lucide-react';
|
||||
import SubscriptionPurchaseModal from './modals/SubscriptionPurchaseModal';
|
||||
import {
|
||||
formatSubscriptionDuration,
|
||||
formatSubscriptionResetPeriod,
|
||||
} from '../../helpers/subscriptionFormat';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 格式化有效期显示
|
||||
function formatDuration(plan, t) {
|
||||
const unit = plan?.duration_unit || 'month';
|
||||
const value = plan?.duration_value || 1;
|
||||
const unitLabels = {
|
||||
year: t('年'),
|
||||
month: t('个月'),
|
||||
day: t('天'),
|
||||
hour: t('小时'),
|
||||
custom: t('自定义'),
|
||||
};
|
||||
if (unit === 'custom') {
|
||||
const seconds = plan?.custom_seconds || 0;
|
||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
||||
return `${seconds} ${t('秒')}`;
|
||||
}
|
||||
return `${value} ${unitLabels[unit] || unit}`;
|
||||
}
|
||||
|
||||
function formatResetPeriod(plan, t) {
|
||||
const period = plan?.quota_reset_period || 'never';
|
||||
if (period === 'never') return t('不重置');
|
||||
if (period === 'daily') return t('每天');
|
||||
if (period === 'weekly') return t('每周');
|
||||
if (period === 'monthly') return t('每月');
|
||||
if (period === 'custom') {
|
||||
const seconds = Number(plan?.quota_reset_custom_seconds || 0);
|
||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
||||
if (seconds >= 60) return `${Math.floor(seconds / 60)} ${t('分钟')}`;
|
||||
return `${seconds} ${t('秒')}`;
|
||||
}
|
||||
return t('不重置');
|
||||
}
|
||||
|
||||
// 过滤易支付方式
|
||||
function getEpayMethods(payMethods = []) {
|
||||
return (payMethods || []).filter(
|
||||
@@ -473,8 +441,9 @@ const SubscriptionPlansCard = ({
|
||||
const totalAmount = Number(plan?.total_amount || 0);
|
||||
const { symbol, rate } = getCurrencyConfig();
|
||||
const price = Number(plan?.price_amount || 0);
|
||||
const displayPrice = (price * rate).toFixed(
|
||||
price % 1 === 0 ? 0 : 2,
|
||||
const convertedPrice = price * rate;
|
||||
const displayPrice = convertedPrice.toFixed(
|
||||
Number.isInteger(convertedPrice) ? 0 : 2,
|
||||
);
|
||||
const isPopular = index === 0 && plans.length > 1;
|
||||
const limit = Number(plan?.max_purchase_per_user || 0);
|
||||
@@ -487,11 +456,13 @@ const SubscriptionPlansCard = ({
|
||||
? `${t('升级分组')}: ${plan.upgrade_group}`
|
||||
: null;
|
||||
const resetLabel =
|
||||
formatResetPeriod(plan, t) === t('不重置')
|
||||
formatSubscriptionResetPeriod(plan, t) === t('不重置')
|
||||
? null
|
||||
: `${t('额度重置')}: ${formatResetPeriod(plan, t)}`;
|
||||
: `${t('额度重置')}: ${formatSubscriptionResetPeriod(plan, t)}`;
|
||||
const planBenefits = [
|
||||
{ label: `${t('有效期')}: ${formatDuration(plan, t)}` },
|
||||
{
|
||||
label: `${t('有效期')}: ${formatSubscriptionDuration(plan, t)}`,
|
||||
},
|
||||
resetLabel ? { label: resetLabel } : null,
|
||||
totalAmount > 0
|
||||
? {
|
||||
|
||||
@@ -356,6 +356,7 @@ const TopUp = () => {
|
||||
};
|
||||
|
||||
const updateBillingPreference = async (pref) => {
|
||||
const previousPref = billingPreference;
|
||||
setBillingPreference(pref);
|
||||
try {
|
||||
const res = await API.put('/api/subscription/self/preference', {
|
||||
@@ -363,11 +364,16 @@ const TopUp = () => {
|
||||
});
|
||||
if (res.data?.success) {
|
||||
showSuccess(t('更新成功'));
|
||||
const normalizedPref =
|
||||
res.data?.data?.billing_preference || pref || previousPref;
|
||||
setBillingPreference(normalizedPref);
|
||||
} else {
|
||||
showError(res.data?.message || t('更新失败'));
|
||||
setBillingPreference(previousPref);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(t('请求失败'));
|
||||
setBillingPreference(previousPref);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -33,45 +33,13 @@ import { SiStripe } from 'react-icons/si';
|
||||
import { IconCreditCard } from '@douyinfe/semi-icons';
|
||||
import { renderQuota } from '../../../helpers';
|
||||
import { getCurrencyConfig } from '../../../helpers/render';
|
||||
import {
|
||||
formatSubscriptionDuration,
|
||||
formatSubscriptionResetPeriod,
|
||||
} from '../../../helpers/subscriptionFormat';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// 格式化有效期显示
|
||||
function formatDuration(plan, t) {
|
||||
const unit = plan?.duration_unit || 'month';
|
||||
const value = plan?.duration_value || 1;
|
||||
const unitLabels = {
|
||||
year: t('年'),
|
||||
month: t('个月'),
|
||||
day: t('天'),
|
||||
hour: t('小时'),
|
||||
custom: t('自定义'),
|
||||
};
|
||||
if (unit === 'custom') {
|
||||
const seconds = plan?.custom_seconds || 0;
|
||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
||||
return `${seconds} ${t('秒')}`;
|
||||
}
|
||||
return `${value} ${unitLabels[unit] || unit}`;
|
||||
}
|
||||
|
||||
function formatResetPeriod(plan, t) {
|
||||
const period = plan?.quota_reset_period || 'never';
|
||||
if (period === 'never') return t('不重置');
|
||||
if (period === 'daily') return t('每天');
|
||||
if (period === 'weekly') return t('每周');
|
||||
if (period === 'monthly') return t('每月');
|
||||
if (period === 'custom') {
|
||||
const seconds = Number(plan?.quota_reset_custom_seconds || 0);
|
||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
||||
if (seconds >= 60) return `${Math.floor(seconds / 60)} ${t('分钟')}`;
|
||||
return `${seconds} ${t('秒')}`;
|
||||
}
|
||||
return t('不重置');
|
||||
}
|
||||
|
||||
const SubscriptionPurchaseModal = ({
|
||||
t,
|
||||
visible,
|
||||
@@ -93,7 +61,10 @@ const SubscriptionPurchaseModal = ({
|
||||
const totalAmount = Number(plan?.total_amount || 0);
|
||||
const { symbol, rate } = getCurrencyConfig();
|
||||
const price = plan ? Number(plan.price_amount || 0) : 0;
|
||||
const displayPrice = (price * rate).toFixed(price % 1 === 0 ? 0 : 2);
|
||||
const convertedPrice = price * rate;
|
||||
const displayPrice = convertedPrice.toFixed(
|
||||
Number.isInteger(convertedPrice) ? 0 : 2,
|
||||
);
|
||||
// 只有当管理员开启支付网关 AND 套餐配置了对应的支付ID时才显示
|
||||
const hasStripe = enableStripeTopUp && !!plan?.stripe_price_id;
|
||||
const hasCreem = enableCreemTopUp && !!plan?.creem_product_id;
|
||||
@@ -142,17 +113,17 @@ const SubscriptionPurchaseModal = ({
|
||||
<div className='flex items-center'>
|
||||
<CalendarClock size={14} className='mr-1 text-slate-500' />
|
||||
<Text className='text-slate-900 dark:text-slate-100'>
|
||||
{formatDuration(plan, t)}
|
||||
{formatSubscriptionDuration(plan, t)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{formatResetPeriod(plan, t) !== t('不重置') && (
|
||||
{formatSubscriptionResetPeriod(plan, t) !== t('不重置') && (
|
||||
<div className='flex justify-between items-center'>
|
||||
<Text strong className='text-slate-700 dark:text-slate-200'>
|
||||
{t('重置周期')}:
|
||||
</Text>
|
||||
<Text className='text-slate-900 dark:text-slate-100'>
|
||||
{formatResetPeriod(plan, t)}
|
||||
{formatSubscriptionResetPeriod(plan, t)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
34
web/src/helpers/subscriptionFormat.js
Normal file
34
web/src/helpers/subscriptionFormat.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export function formatSubscriptionDuration(plan, t) {
|
||||
const unit = plan?.duration_unit || 'month';
|
||||
const value = plan?.duration_value || 1;
|
||||
const unitLabels = {
|
||||
year: t('年'),
|
||||
month: t('个月'),
|
||||
day: t('天'),
|
||||
hour: t('小时'),
|
||||
custom: t('自定义'),
|
||||
};
|
||||
if (unit === 'custom') {
|
||||
const seconds = plan?.custom_seconds || 0;
|
||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
||||
return `${seconds} ${t('秒')}`;
|
||||
}
|
||||
return `${value} ${unitLabels[unit] || unit}`;
|
||||
}
|
||||
|
||||
export function formatSubscriptionResetPeriod(plan, t) {
|
||||
const period = plan?.quota_reset_period || 'never';
|
||||
if (period === 'never') return t('不重置');
|
||||
if (period === 'daily') return t('每天');
|
||||
if (period === 'weekly') return t('每周');
|
||||
if (period === 'monthly') return t('每月');
|
||||
if (period === 'custom') {
|
||||
const seconds = Number(plan?.quota_reset_custom_seconds || 0);
|
||||
if (seconds >= 86400) return `${Math.floor(seconds / 86400)} ${t('天')}`;
|
||||
if (seconds >= 3600) return `${Math.floor(seconds / 3600)} ${t('小时')}`;
|
||||
if (seconds >= 60) return `${Math.floor(seconds / 60)} ${t('分钟')}`;
|
||||
return `${seconds} ${t('秒')}`;
|
||||
}
|
||||
return t('不重置');
|
||||
}
|
||||
@@ -2660,7 +2660,7 @@
|
||||
"推荐": "おすすめ",
|
||||
"已达到购买上限": "購入上限に達しました",
|
||||
"已达上限": "上限に達しました",
|
||||
"立即订阅": "今すぐ購読",
|
||||
"立即订阅": "今すぐサブスクリプション",
|
||||
"暂无可购买套餐": "購入可能なプランがありません",
|
||||
"该套餐未配置 Stripe": "このプランには Stripe が設定されていません",
|
||||
"已打开支付页面": "決済ページを開きました",
|
||||
|
||||
@@ -2616,7 +2616,7 @@
|
||||
"格式化 JSON": "Форматировать JSON",
|
||||
"关闭提示": "Закрыть уведомление",
|
||||
"说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "Примечание: тесты на этой странице используют нестриминговые запросы. Если канал поддерживает только стриминговые ответы, тест может завершиться неудачей. Ориентируйтесь на реальное использование.",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Примечание: сопоставление endpoint'ов используется только для отображения в «Маркетплейсе моделей» и не влияет на реальный вызов. Чтобы настроить реальное поведение вызовов, перейдите в «Управление каналами».",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Примечание: сопоставление эндпоинтов используется только для отображения в «Маркетплейсе моделей» и не влияет на реальный вызов. Чтобы настроить реальное поведение вызовов, перейдите в «Управление каналами».",
|
||||
"Stripe/Creem 需在第三方平台创建商品并填入 ID": "Товары Stripe/Creem нужно создать на сторонней платформе и указать их ID",
|
||||
"暂无订阅套餐": "Нет тарифных планов",
|
||||
"订阅管理": "Управление подписками",
|
||||
|
||||
@@ -3228,9 +3228,9 @@
|
||||
"仅用订阅": "Chỉ dùng đăng ký",
|
||||
"仅用钱包": "Chỉ dùng ví",
|
||||
"我的订阅": "Đăng ký của tôi",
|
||||
"个生效中": "đang hiệu lực",
|
||||
"无生效": "Không có hiệu lực",
|
||||
"个已过期": "đã hết hạn",
|
||||
"个生效中": "gói đăng ký đang hiệu lực",
|
||||
"无生效": "Không có gói đăng ký hiệu lực",
|
||||
"个已过期": "gói đăng ký đã hết hạn",
|
||||
"订阅": "Đăng ký",
|
||||
"过期于": "Hết hạn vào",
|
||||
"购买套餐后即可享受模型权益": "Mua gói để nhận quyền lợi mô hình",
|
||||
|
||||
@@ -18,12 +18,12 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import SubscriptionsTable from '../../components/table/subscriptions';
|
||||
import SubscriptionsPage from '../../components/table/subscriptions';
|
||||
|
||||
const Subscription = () => {
|
||||
return (
|
||||
<div className='mt-[60px] px-2'>
|
||||
<SubscriptionsTable />
|
||||
<SubscriptionsPage />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user