mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-24 04:18:38 +00:00
✨ feat(subscription): add quota reset periods and admin configuration
- Add reset period fields on subscription plans and user items - Apply automatic quota resets during pre-consume based on plan schedule - Expose reset-period configuration in the admin plan editor - Display reset cadence in subscription cards and purchase modal - Validate custom reset seconds on plan create/update
This commit is contained in:
@@ -43,7 +43,7 @@ import {
|
||||
IconPlusCircle,
|
||||
IconSave,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Trash2, Clock, Boxes } from 'lucide-react';
|
||||
import { Trash2, Clock, Boxes, RefreshCw } from 'lucide-react';
|
||||
import { API, showError, showSuccess } from '../../../../helpers';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
|
||||
@@ -57,6 +57,14 @@ const durationUnitOptions = [
|
||||
{ value: 'custom', label: '自定义(秒)' },
|
||||
];
|
||||
|
||||
const resetPeriodOptions = [
|
||||
{ value: 'never', label: '不重置' },
|
||||
{ value: 'daily', label: '每天' },
|
||||
{ value: 'weekly', label: '每周' },
|
||||
{ value: 'monthly', label: '每月' },
|
||||
{ value: 'custom', label: '自定义(秒)' },
|
||||
];
|
||||
|
||||
const quotaTypeLabel = (quotaType) => (quotaType === 1 ? '按次' : '按量');
|
||||
|
||||
const AddEditSubscriptionModal = ({
|
||||
@@ -82,6 +90,8 @@ const AddEditSubscriptionModal = ({
|
||||
duration_unit: 'month',
|
||||
duration_value: 1,
|
||||
custom_seconds: 0,
|
||||
quota_reset_period: 'never',
|
||||
quota_reset_custom_seconds: 0,
|
||||
enabled: true,
|
||||
sort_order: 0,
|
||||
stripe_price_id: '',
|
||||
@@ -108,6 +118,8 @@ const AddEditSubscriptionModal = ({
|
||||
duration_unit: p.duration_unit || 'month',
|
||||
duration_value: Number(p.duration_value || 1),
|
||||
custom_seconds: Number(p.custom_seconds || 0),
|
||||
quota_reset_period: p.quota_reset_period || 'never',
|
||||
quota_reset_custom_seconds: Number(p.quota_reset_custom_seconds || 0),
|
||||
enabled: p.enabled !== false,
|
||||
sort_order: Number(p.sort_order || 0),
|
||||
stripe_price_id: p.stripe_price_id || '',
|
||||
@@ -264,6 +276,11 @@ const AddEditSubscriptionModal = ({
|
||||
price_amount: Number(values.price_amount || 0),
|
||||
duration_value: Number(values.duration_value || 0),
|
||||
custom_seconds: Number(values.custom_seconds || 0),
|
||||
quota_reset_period: values.quota_reset_period || 'never',
|
||||
quota_reset_custom_seconds:
|
||||
values.quota_reset_period === 'custom'
|
||||
? Number(values.quota_reset_custom_seconds || 0)
|
||||
: 0,
|
||||
sort_order: Number(values.sort_order || 0),
|
||||
},
|
||||
items: cleanedItems,
|
||||
@@ -539,6 +556,63 @@ const AddEditSubscriptionModal = ({
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 额度重置 */}
|
||||
<Card className='!rounded-2xl shadow-sm border-0 mb-4'>
|
||||
<div className='flex items-center mb-2'>
|
||||
<Avatar
|
||||
size='small'
|
||||
color='orange'
|
||||
className='mr-2 shadow-md'
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Text className='text-lg font-medium'>
|
||||
{t('额度重置')}
|
||||
</Text>
|
||||
<div className='text-xs text-gray-600'>
|
||||
{t('支持周期性重置套餐权益额度')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.Select
|
||||
field='quota_reset_period'
|
||||
label={t('重置周期')}
|
||||
>
|
||||
{resetPeriodOptions.map((o) => (
|
||||
<Select.Option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
{values.quota_reset_period === 'custom' ? (
|
||||
<Form.InputNumber
|
||||
field='quota_reset_custom_seconds'
|
||||
label={t('自定义秒数')}
|
||||
min={60}
|
||||
precision={0}
|
||||
rules={[{ required: true, message: t('请输入秒数') }]}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
<Form.InputNumber
|
||||
field='quota_reset_custom_seconds'
|
||||
label={t('自定义秒数')}
|
||||
min={0}
|
||||
precision={0}
|
||||
style={{ width: '100%' }}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 第三方支付配置 */}
|
||||
<Card className='!rounded-2xl shadow-sm border-0 mb-4'>
|
||||
<div className='flex items-center mb-2'>
|
||||
|
||||
@@ -55,6 +55,22 @@ function formatDuration(plan, 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(
|
||||
@@ -497,6 +513,9 @@ const SubscriptionPlansCard = ({
|
||||
<div className='text-sm text-gray-500 mt-1'>
|
||||
<CalendarClock size={12} className='inline mr-1' />
|
||||
{formatDuration(plan, t)}
|
||||
<span className='ml-2 text-xs text-gray-400'>
|
||||
{t('重置')}: {formatResetPeriod(plan, t)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -54,6 +54,22 @@ function formatDuration(plan, 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 getCurrencySymbol(currency) {
|
||||
const symbols = { USD: '$', EUR: '€', CNY: '¥', GBP: '£', JPY: '¥' };
|
||||
@@ -129,6 +145,14 @@ const SubscriptionPurchaseModal = ({
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<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)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className='flex justify-between items-center'>
|
||||
<Text strong className='text-slate-700 dark:text-slate-200'>
|
||||
{t('包含权益')}:
|
||||
|
||||
Reference in New Issue
Block a user