2026-03-01 21:53:09 +08:00
|
|
|
|
import { prisma } from '@/lib/db';
|
|
|
|
|
|
import { getEnv } from '@/lib/config';
|
2026-03-01 22:51:09 +08:00
|
|
|
|
import { initPaymentProviders, paymentRegistry } from '@/lib/payment';
|
2026-03-03 22:00:44 +08:00
|
|
|
|
import { getMethodFeeRate } from './fee';
|
2026-03-01 21:53:09 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取指定支付渠道的每日全平台限额(0 = 不限制)。
|
2026-03-01 22:51:09 +08:00
|
|
|
|
* 优先级:环境变量显式配置 > provider 默认值 > process.env 兜底 > 0
|
2026-03-01 21:53:09 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function getMethodDailyLimit(paymentType: string): number {
|
|
|
|
|
|
const env = getEnv();
|
|
|
|
|
|
const key = `MAX_DAILY_AMOUNT_${paymentType.toUpperCase()}` as keyof typeof env;
|
|
|
|
|
|
const val = env[key];
|
2026-03-01 22:51:09 +08:00
|
|
|
|
if (typeof val === 'number') return val; // 明确配置(含 0)
|
2026-03-01 21:53:09 +08:00
|
|
|
|
|
2026-03-01 22:51:09 +08:00
|
|
|
|
// 尝试从已注册的 provider 取默认值
|
|
|
|
|
|
initPaymentProviders();
|
|
|
|
|
|
const providerDefault = paymentRegistry.getDefaultLimit(paymentType);
|
|
|
|
|
|
if (providerDefault?.dailyMax !== undefined) return providerDefault.dailyMax;
|
|
|
|
|
|
|
|
|
|
|
|
// 兜底:process.env(支持未在 schema 中声明的动态渠道)
|
2026-03-01 21:53:09 +08:00
|
|
|
|
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; // 默认不限制
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 22:51:09 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取指定支付渠道的单笔限额(0 = 使用全局 MAX_RECHARGE_AMOUNT)。
|
|
|
|
|
|
* 优先级:process.env MAX_SINGLE_AMOUNT_* > provider 默认值 > 0
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getMethodSingleLimit(paymentType: string): number {
|
|
|
|
|
|
const raw = process.env[`MAX_SINGLE_AMOUNT_${paymentType.toUpperCase()}`];
|
|
|
|
|
|
if (raw !== undefined) {
|
|
|
|
|
|
const num = Number(raw);
|
|
|
|
|
|
if (Number.isFinite(num) && num >= 0) return num;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
initPaymentProviders();
|
|
|
|
|
|
const providerDefault = paymentRegistry.getDefaultLimit(paymentType);
|
|
|
|
|
|
if (providerDefault?.singleMax !== undefined) return providerDefault.singleMax;
|
|
|
|
|
|
|
|
|
|
|
|
return 0; // 使用全局 MAX_RECHARGE_AMOUNT
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 21:53:09 +08:00
|
|
|
|
export interface MethodLimitStatus {
|
|
|
|
|
|
/** 每日限额,0 = 不限 */
|
|
|
|
|
|
dailyLimit: number;
|
|
|
|
|
|
/** 今日已使用金额 */
|
|
|
|
|
|
used: number;
|
2026-03-01 22:51:09 +08:00
|
|
|
|
/** 剩余每日额度,null = 不限 */
|
2026-03-01 21:53:09 +08:00
|
|
|
|
remaining: number | null;
|
|
|
|
|
|
/** 是否还可使用(false = 今日额度已满) */
|
|
|
|
|
|
available: boolean;
|
2026-03-01 22:51:09 +08:00
|
|
|
|
/** 单笔限额,0 = 使用全局配置 MAX_RECHARGE_AMOUNT */
|
|
|
|
|
|
singleMax: number;
|
2026-03-03 22:00:44 +08:00
|
|
|
|
/** 手续费率百分比,0 = 无手续费 */
|
|
|
|
|
|
feeRate: number;
|
2026-03-01 21:53:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量查询多个支付渠道的今日使用情况。
|
|
|
|
|
|
* 一次 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);
|
2026-03-01 22:51:09 +08:00
|
|
|
|
const singleMax = getMethodSingleLimit(type);
|
2026-03-03 22:00:44 +08:00
|
|
|
|
const feeRate = getMethodFeeRate(type);
|
2026-03-01 21:53:09 +08:00
|
|
|
|
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,
|
2026-03-01 22:51:09 +08:00
|
|
|
|
singleMax,
|
2026-03-03 22:00:44 +08:00
|
|
|
|
feeRate,
|
2026-03-01 21:53:09 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|