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}
- ) : (
-
-
-
- | {t.colName} |
- {t.colGroup} |
- {t.colPrice} |
- {t.colOriginalPrice} |
- {t.colValidDays} |
- {t.colEnabled} |
- {t.colGroupStatus} |
- {t.colActions} |
-
-
-
- {plans.map((plan) => (
-
- | {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