diff --git a/src/app/admin/subscriptions/page.tsx b/src/app/admin/subscriptions/page.tsx index 1b1fedc..efd8606 100644 --- a/src/app/admin/subscriptions/page.tsx +++ b/src/app/admin/subscriptions/page.tsx @@ -21,6 +21,12 @@ interface SubscriptionPlan { sortOrder: number; enabled: boolean; groupExists: boolean; + groupPlatform: string | null; + groupRateMultiplier: number | null; + groupDailyLimit: number | null; + groupWeeklyLimit: number | null; + groupMonthlyLimit: number | null; + groupModelScopes: string[] | null; } interface Sub2ApiGroup { @@ -129,6 +135,14 @@ function buildText(locale: Locale) { unlimited: 'Unlimited', resetIn: 'Reset in', noGroup: 'Unknown Group', + groupInfo: 'Sub2API Group Info', + groupInfoReadonly: '(read-only, from Sub2API)', + platform: 'Platform', + rateMultiplier: 'Rate', + dailyLimit: 'Daily Limit', + weeklyLimit: 'Weekly Limit', + monthlyLimit: 'Monthly Limit', + modelScopes: 'Models', } : { missingToken: '缺少管理员凭证', @@ -201,6 +215,14 @@ function buildText(locale: Locale) { unlimited: '无限制', resetIn: '重置于', noGroup: '未知分组', + groupInfo: 'Sub2API 分组信息', + groupInfoReadonly: '(只读,来自 Sub2API)', + platform: '平台', + rateMultiplier: '倍率', + dailyLimit: '日限额', + weeklyLimit: '周限额', + monthlyLimit: '月限额', + modelScopes: '模型', }; } @@ -679,115 +701,182 @@ function SubscriptionsContent() { - {/* Plans table */} -
- {plansLoading ? ( -
{t.loading}
- ) : plans.length === 0 ? ( -
{t.noPlans}
- ) : ( - - - - - - - - - - - - - - - {plans.map((plan) => ( - - - - - - - - - - - ))} - -
{t.colName}{t.colGroup}{t.colPrice}{t.colOriginalPrice}{t.colValidDays}{t.colEnabled}{t.colGroupStatus}{t.colActions}
{plan.name} - {plan.groupId} - {plan.groupName && ( - - ({plan.groupName}) - - )} - {plan.price.toFixed(2)} - {plan.originalPrice != null ? plan.originalPrice.toFixed(2) : '-'} - - {plan.validDays}{' '} - {plan.validityUnit === 'month' - ? t.unitMonth - : plan.validityUnit === 'week' - ? t.unitWeek - : t.unitDay} - - - + {/* Plans cards */} + {plansLoading ? ( +
{t.loading}
+ ) : plans.length === 0 ? ( +
{t.noPlans}
+ ) : ( +
+ {plans.map((plan) => ( +
+ {/* ── 套餐配置(上半部分) ── */} +
+
+
+

+ {plan.name} +

{plan.groupExists ? t.groupExists : t.groupMissing} -
-
+
+
+ {/* Toggle */} +
+ + {t.colEnabled} + -
-
- )} -
+ {/* Actions */} + + + + + + {/* Plan fields grid */} +
+
+ {t.colGroup} +
+ {plan.groupId} + {plan.groupName && ({plan.groupName})} +
+
+
+ {t.colPrice} +
+ ¥{plan.price.toFixed(2)} + {plan.originalPrice != null && ( + + ¥{plan.originalPrice.toFixed(2)} + + )} +
+
+
+ {t.colValidDays} +
+ {plan.validDays} {plan.validityUnit === 'month' ? t.unitMonth : plan.validityUnit === 'week' ? t.unitWeek : t.unitDay} +
+
+
+ {t.fieldSortOrder} +
{plan.sortOrder}
+
+
+ + + {/* ── Sub2API 分组信息(嵌套只读区域) ── */} + {plan.groupExists && ( +
+
+ + {t.groupInfo} + + + {t.groupInfoReadonly} + +
+
+ {plan.groupPlatform && ( +
+ {t.platform} +
{plan.groupPlatform}
+
+ )} + {plan.groupRateMultiplier != null && ( +
+ {t.rateMultiplier} +
{plan.groupRateMultiplier}x
+
+ )} +
+ {t.dailyLimit} +
+ {plan.groupDailyLimit != null ? `$${plan.groupDailyLimit}` : t.unlimited} +
+
+
+ {t.weeklyLimit} +
+ {plan.groupWeeklyLimit != null ? `$${plan.groupWeeklyLimit}` : t.unlimited} +
+
+
+ {t.monthlyLimit} +
+ {plan.groupMonthlyLimit != null ? `$${plan.groupMonthlyLimit}` : t.unlimited} +
+
+ {plan.groupModelScopes && plan.groupModelScopes.length > 0 && ( +
+ {t.modelScopes} +
+ {plan.groupModelScopes.map((m) => ( + + {m} + + ))} +
+
+ )} +
+
+ )} + + ))} + + )} )} @@ -1035,7 +1124,8 @@ function SubscriptionsContent() { setFormPrice(e.target.value)} className={inputCls} @@ -1047,7 +1137,8 @@ function SubscriptionsContent() { setFormOriginalPrice(e.target.value)} className={inputCls} diff --git a/src/app/api/admin/subscription-plans/[id]/route.ts b/src/app/api/admin/subscription-plans/[id]/route.ts index 1e869b9..23b18fd 100644 --- a/src/app/api/admin/subscription-plans/[id]/route.ts +++ b/src/app/api/admin/subscription-plans/[id]/route.ts @@ -27,8 +27,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ } } - if (body.price !== undefined && (typeof body.price !== 'number' || body.price <= 0)) { - return NextResponse.json({ error: 'price 必须是正数' }, { status: 400 }); + if (body.price !== undefined && (typeof body.price !== 'number' || body.price <= 0 || body.price > 99999999.99)) { + return NextResponse.json({ error: 'price 必须是 0.01 ~ 99999999.99 之间的数值' }, { status: 400 }); + } + if (body.original_price !== undefined && body.original_price !== null && (typeof body.original_price !== 'number' || body.original_price <= 0 || body.original_price > 99999999.99)) { + return NextResponse.json({ error: 'original_price 必须是 0.01 ~ 99999999.99 之间的数值' }, { status: 400 }); } if (body.validity_days !== undefined && (!Number.isInteger(body.validity_days) || body.validity_days <= 0)) { return NextResponse.json({ error: 'validity_days 必须是正整数' }, { status: 400 }); diff --git a/src/app/api/admin/subscription-plans/route.ts b/src/app/api/admin/subscription-plans/route.ts index 205eae1..6ce1aa2 100644 --- a/src/app/api/admin/subscription-plans/route.ts +++ b/src/app/api/admin/subscription-plans/route.ts @@ -16,8 +16,9 @@ export async function GET(request: NextRequest) { plans.map(async (plan) => { let groupExists = false; let groupName: string | null = null; + let group: Awaited> | null = null; try { - const group = await getGroup(plan.groupId); + group = await getGroup(plan.groupId); groupExists = group !== null; groupName = group?.name ?? null; } catch { @@ -37,6 +38,12 @@ export async function GET(request: NextRequest) { sortOrder: plan.sortOrder, enabled: plan.forSale, groupExists, + groupPlatform: group?.platform ?? null, + groupRateMultiplier: group?.rate_multiplier ?? null, + groupDailyLimit: group?.daily_limit_usd ?? null, + groupWeeklyLimit: group?.weekly_limit_usd ?? null, + groupMonthlyLimit: group?.monthly_limit_usd ?? null, + groupModelScopes: group?.supported_model_scopes ?? null, createdAt: plan.createdAt, updatedAt: plan.updatedAt, }; @@ -61,8 +68,11 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: '缺少必填字段: group_id, name, price' }, { status: 400 }); } - if (typeof price !== 'number' || price <= 0) { - return NextResponse.json({ error: 'price 必须是正数' }, { status: 400 }); + if (typeof price !== 'number' || price <= 0 || price > 99999999.99) { + return NextResponse.json({ error: 'price 必须是 0.01 ~ 99999999.99 之间的数值' }, { status: 400 }); + } + if (original_price !== undefined && original_price !== null && (typeof original_price !== 'number' || original_price <= 0 || original_price > 99999999.99)) { + return NextResponse.json({ error: 'original_price 必须是 0.01 ~ 99999999.99 之间的数值' }, { status: 400 }); } if (validity_days !== undefined && (!Number.isInteger(validity_days) || validity_days <= 0)) { return NextResponse.json({ error: 'validity_days 必须是正整数' }, { status: 400 }); diff --git a/src/app/api/orders/route.ts b/src/app/api/orders/route.ts index ee51af7..a093955 100644 --- a/src/app/api/orders/route.ts +++ b/src/app/api/orders/route.ts @@ -8,7 +8,7 @@ import { handleApiError } from '@/lib/utils/api'; const createOrderSchema = z.object({ token: z.string().min(1), - amount: z.number().positive(), + amount: z.number().positive().max(99999999.99), payment_type: z.string().min(1), src_host: z.string().max(253).optional(), src_url: z.string().max(2048).optional(), diff --git a/src/app/api/subscription-plans/route.ts b/src/app/api/subscription-plans/route.ts index 9122f59..c3c5c36 100644 --- a/src/app/api/subscription-plans/route.ts +++ b/src/app/api/subscription-plans/route.ts @@ -24,9 +24,10 @@ export async function GET(request: NextRequest) { const results = await Promise.all( plans.map(async (plan) => { let groupActive = false; + let group: Awaited> = null; let groupInfo: { daily_limit_usd: number | null; weekly_limit_usd: number | null; monthly_limit_usd: number | null } | null = null; try { - const group = await getGroup(plan.groupId); + group = await getGroup(plan.groupId); groupActive = group !== null && group.status === 'active'; if (group) { groupInfo = { @@ -44,6 +45,7 @@ export async function GET(request: NextRequest) { return { id: plan.id, groupId: plan.groupId, + groupName: group?.name ?? null, name: plan.name, description: plan.description, price: Number(plan.price), @@ -51,6 +53,8 @@ export async function GET(request: NextRequest) { validityDays: plan.validityDays, validityUnit: plan.validityUnit, features: plan.features ? JSON.parse(plan.features) : [], + platform: group?.platform ?? null, + rateMultiplier: group?.rate_multiplier ?? null, limits: groupInfo, }; }), diff --git a/src/components/SubscriptionConfirm.tsx b/src/components/SubscriptionConfirm.tsx index 5f37652..0acc781 100644 --- a/src/components/SubscriptionConfirm.tsx +++ b/src/components/SubscriptionConfirm.tsx @@ -62,11 +62,11 @@ export default function SubscriptionConfirm({ {/* Plan info card */}
-
+
{plan.name} @@ -81,6 +81,24 @@ export default function SubscriptionConfirm({
+ + {/* Platform & Rate tags */} + {(plan.platform || plan.rateMultiplier != null) && ( +
+ {plan.platform && ( + + {pickLocaleText(locale, '平台', 'Platform')}: {plan.platform} + + )} + {plan.rateMultiplier != null && ( + + {pickLocaleText(locale, '倍率', 'Rate')}: {plan.rateMultiplier}x + + )} +
+ )} + + {/* Features */} {plan.features.length > 0 && (
    {plan.features.map((feature) => ( @@ -93,6 +111,24 @@ export default function SubscriptionConfirm({ ))}
)} + + {/* Usage limits */} + {plan.limits && (plan.limits.daily_limit_usd != null || plan.limits.weekly_limit_usd != null || plan.limits.monthly_limit_usd != null) && ( +
+

{pickLocaleText(locale, '用量限制', 'Usage Limits')}

+
+ {plan.limits.daily_limit_usd != null && ( +

{pickLocaleText(locale, `每日: $${plan.limits.daily_limit_usd}`, `Daily: $${plan.limits.daily_limit_usd}`)}

+ )} + {plan.limits.weekly_limit_usd != null && ( +

{pickLocaleText(locale, `每周: $${plan.limits.weekly_limit_usd}`, `Weekly: $${plan.limits.weekly_limit_usd}`)}

+ )} + {plan.limits.monthly_limit_usd != null && ( +

{pickLocaleText(locale, `每月: $${plan.limits.monthly_limit_usd}`, `Monthly: $${plan.limits.monthly_limit_usd}`)}

+ )} +
+
+ )}
{/* Payment method selector */} diff --git a/src/components/SubscriptionPlanCard.tsx b/src/components/SubscriptionPlanCard.tsx index d0004db..bfe70d1 100644 --- a/src/components/SubscriptionPlanCard.tsx +++ b/src/components/SubscriptionPlanCard.tsx @@ -8,6 +8,7 @@ import { formatValidityLabel, formatValiditySuffix, type ValidityUnit } from '@/ export interface PlanInfo { id: string; groupId: number; + groupName: string | null; name: string; price: number; originalPrice: number | null; @@ -15,6 +16,8 @@ export interface PlanInfo { validityUnit?: ValidityUnit; features: string[]; description: string | null; + platform: string | null; + rateMultiplier: number | null; limits: { daily_limit_usd: number | null; weekly_limit_usd: number | null; @@ -76,6 +79,22 @@ export default function SubscriptionPlanCard({ plan, onSubscribe, isDark, locale

)} + {/* Platform & Rate */} + {(plan.platform || plan.rateMultiplier != null) && ( +
+ {plan.platform && ( + + {pickLocaleText(locale, '平台', 'Platform')}: {plan.platform} + + )} + {plan.rateMultiplier != null && ( + + {pickLocaleText(locale, '倍率', 'Rate')}: {plan.rateMultiplier}x + + )} +
+ )} + {/* Features */} {plan.features.length > 0 && (
    diff --git a/src/lib/order/service.ts b/src/lib/order/service.ts index 4d59f2b..4b2db00 100644 --- a/src/lib/order/service.ts +++ b/src/lib/order/service.ts @@ -15,6 +15,8 @@ import { getBizDayStartUTC } from '@/lib/time/biz-day'; import { buildOrderResultUrl, createOrderStatusAccessToken } from '@/lib/order/status-access'; const MAX_PENDING_ORDERS = 3; +/** Decimal(10,2) 允许的最大金额 */ +export const MAX_AMOUNT = 99999999.99; function message(locale: Locale, zh: string, en: string): string { return pickLocaleText(locale, zh, en); @@ -58,6 +60,7 @@ export async function createOrder(input: CreateOrderInput): Promise