diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 7dcf02e..d2136c4 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -21,6 +21,7 @@ export async function GET(request: NextRequest) { enabledPaymentTypes: env.ENABLED_PAYMENT_TYPES, minAmount: env.MIN_RECHARGE_AMOUNT, maxAmount: env.MAX_RECHARGE_AMOUNT, + maxDailyAmount: env.MAX_DAILY_RECHARGE_AMOUNT, }, }); } catch (error) { diff --git a/src/app/pay/page.tsx b/src/app/pay/page.tsx index 7f7d332..93f4cc1 100644 --- a/src/app/pay/page.tsx +++ b/src/app/pay/page.tsx @@ -24,6 +24,7 @@ interface AppConfig { enabledPaymentTypes: string[]; minAmount: number; maxAmount: number; + maxDailyAmount: number; } function PayContent() { @@ -51,6 +52,7 @@ function PayContent() { enabledPaymentTypes: ['alipay', 'wxpay', 'stripe'], minAmount: 1, maxAmount: 10000, + maxDailyAmount: 0, }); const effectiveUserId = resolvedUserId || userId; @@ -178,6 +180,7 @@ function PayContent() { USER_INACTIVE: '账户已被禁用,无法充值,请联系管理员', TOO_MANY_PENDING: '您有过多待支付订单,请先完成或取消现有订单后再试', USER_NOT_FOUND: '用户不存在,请检查链接是否正确', + DAILY_LIMIT_EXCEEDED: data.error, }; setError(codeMessages[data.code] || data.error || '创建订单失败'); return; @@ -349,6 +352,9 @@ function PayContent() { diff --git a/src/components/PaymentForm.tsx b/src/components/PaymentForm.tsx index a2a7966..23606aa 100644 --- a/src/components/PaymentForm.tsx +++ b/src/components/PaymentForm.tsx @@ -143,7 +143,7 @@ export default function PaymentForm({ 充值金额
- {QUICK_AMOUNTS.map((val) => ( + {QUICK_AMOUNTS.filter((val) => val <= maxAmount).map((val) => (
- {customAmount !== '' && !isValid && ( -
- { - '\u91D1\u989D\u9700\u5728\u8303\u56F4\u5185\uFF0C\u4E14\u6700\u591A\u652F\u6301 2 \u4F4D\u5C0F\u6570\uFF08\u7CBE\u786E\u5230\u5206\uFF09' - } -
- )} + {customAmount !== '' && !isValid && (() => { + const num = parseFloat(customAmount); + let msg = '金额需在范围内,且最多支持 2 位小数(精确到分)'; + if (!isNaN(num)) { + if (num < minAmount) msg = `单笔最低充值 ¥${minAmount}`; + else if (num > maxAmount) msg = `单笔最高充值 ¥${maxAmount}`; + } + return ( +
+ {msg} +
+ ); + })()} {/* Payment Type */}
diff --git a/src/lib/config.ts b/src/lib/config.ts index b68a1d8..99e143a 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -33,7 +33,9 @@ const envSchema = z.object({ ORDER_TIMEOUT_MINUTES: z.string().default('5').transform(Number).pipe(z.number().int().positive()), MIN_RECHARGE_AMOUNT: z.string().default('1').transform(Number).pipe(z.number().positive()), - MAX_RECHARGE_AMOUNT: z.string().default('10000').transform(Number).pipe(z.number().positive()), + MAX_RECHARGE_AMOUNT: z.string().default('1000').transform(Number).pipe(z.number().positive()), + // 每日每用户最大累计充值额,0 = 不限制 + MAX_DAILY_RECHARGE_AMOUNT: z.string().default('0').transform(Number).pipe(z.number().min(0)), PRODUCT_NAME: z.string().default('Sub2API Balance Recharge'), ADMIN_TOKEN: z.string().min(1), diff --git a/src/lib/order/service.ts b/src/lib/order/service.ts index 5735ca6..7c39a15 100644 --- a/src/lib/order/service.ts +++ b/src/lib/order/service.ts @@ -44,6 +44,29 @@ export async function createOrder(input: CreateOrderInput): Promise 0) { + const todayStart = new Date(); + todayStart.setUTCHours(0, 0, 0, 0); + const dailyAgg = await prisma.order.aggregate({ + where: { + userId: input.userId, + status: { in: ['PAID', 'RECHARGING', 'COMPLETED'] }, + paidAt: { gte: todayStart }, + }, + _sum: { amount: true }, + }); + const alreadyPaid = Number(dailyAgg._sum.amount ?? 0); + if (alreadyPaid + input.amount > env.MAX_DAILY_RECHARGE_AMOUNT) { + const remaining = Math.max(0, env.MAX_DAILY_RECHARGE_AMOUNT - alreadyPaid); + throw new OrderError( + 'DAILY_LIMIT_EXCEEDED', + `今日累计充值已达上限,剩余可充值 ${remaining.toFixed(2)} 元`, + 429, + ); + } + } + const expiresAt = new Date(Date.now() + env.ORDER_TIMEOUT_MINUTES * 60 * 1000); const order = await prisma.order.create({ data: {