feat: 支付渠道每日限额(渠道维度全平台统计)
- config.ts:新增 MAX_DAILY_AMOUNT_ALIPAY/WXPAY/STRIPE(默认 alipay/wxpay 各 1w,stripe 不限) - lib/order/limits.ts:getMethodDailyLimit + queryMethodLimits 共用工具,支持动态渠道兜底 - order/service.ts:createOrder 校验渠道限额,超限抛 METHOD_DAILY_LIMIT_EXCEEDED - api/limits/route.ts:公开接口 GET /api/limits,返回各渠道今日用量/剩余/是否可用 - api/user/route.ts:config 响应中加入 methodLimits,前端一次请求即可获取限额状态 - PaymentForm.tsx:额度已满的渠道置灰 + 标注「今日额度已满」,无法选择 - pay/page.tsx:AppConfig 加 methodLimits,传给 PaymentForm,新增错误码映射
This commit is contained in:
71
src/lib/order/limits.ts
Normal file
71
src/lib/order/limits.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getEnv } from '@/lib/config';
|
||||
|
||||
/**
|
||||
* 获取指定支付渠道的每日全平台限额(0 = 不限制)。
|
||||
* 优先读 config(Zod 验证),兜底读 process.env,适配未来动态注册的新渠道。
|
||||
*/
|
||||
export function getMethodDailyLimit(paymentType: string): number {
|
||||
const env = getEnv();
|
||||
const key = `MAX_DAILY_AMOUNT_${paymentType.toUpperCase()}` as keyof typeof env;
|
||||
const val = env[key];
|
||||
if (typeof val === 'number') return val;
|
||||
|
||||
// 兜底:支持动态渠道(未在 schema 中声明的 MAX_DAILY_AMOUNT_* 变量)
|
||||
const raw = process.env[`MAX_DAILY_AMOUNT_${paymentType.toUpperCase()}`];
|
||||
if (raw !== undefined) {
|
||||
const num = Number(raw);
|
||||
return Number.isFinite(num) && num >= 0 ? num : 0;
|
||||
}
|
||||
return 0; // 默认不限制
|
||||
}
|
||||
|
||||
export interface MethodLimitStatus {
|
||||
/** 每日限额,0 = 不限 */
|
||||
dailyLimit: number;
|
||||
/** 今日已使用金额 */
|
||||
used: number;
|
||||
/** 剩余额度,null = 不限 */
|
||||
remaining: number | null;
|
||||
/** 是否还可使用(false = 今日额度已满) */
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询多个支付渠道的今日使用情况。
|
||||
* 一次 DB groupBy 完成,调用方按需传入渠道列表。
|
||||
*/
|
||||
export async function queryMethodLimits(
|
||||
paymentTypes: string[],
|
||||
): Promise<Record<string, MethodLimitStatus>> {
|
||||
const todayStart = new Date();
|
||||
todayStart.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const usageRows = await prisma.order.groupBy({
|
||||
by: ['paymentType'],
|
||||
where: {
|
||||
paymentType: { in: paymentTypes },
|
||||
status: { in: ['PAID', 'RECHARGING', 'COMPLETED'] },
|
||||
paidAt: { gte: todayStart },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
});
|
||||
|
||||
const usageMap = Object.fromEntries(
|
||||
usageRows.map((r) => [r.paymentType, Number(r._sum.amount ?? 0)]),
|
||||
);
|
||||
|
||||
const result: Record<string, MethodLimitStatus> = {};
|
||||
for (const type of paymentTypes) {
|
||||
const dailyLimit = getMethodDailyLimit(type);
|
||||
const used = usageMap[type] ?? 0;
|
||||
const remaining = dailyLimit > 0 ? Math.max(0, dailyLimit - used) : null;
|
||||
result[type] = {
|
||||
dailyLimit,
|
||||
used,
|
||||
remaining,
|
||||
available: dailyLimit === 0 || used < dailyLimit,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user