2026-03-01 03:04:24 +08:00
|
|
|
import { prisma } from '@/lib/db';
|
2026-03-01 17:58:08 +08:00
|
|
|
import { initPaymentProviders, paymentRegistry } from '@/lib/payment';
|
|
|
|
|
import type { PaymentType } from '@/lib/payment';
|
|
|
|
|
import { confirmPayment } from './service';
|
2026-03-01 03:04:24 +08:00
|
|
|
|
|
|
|
|
const INTERVAL_MS = 30_000; // 30 seconds
|
|
|
|
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
|
|
|
|
|
export async function expireOrders(): Promise<number> {
|
2026-03-01 17:58:08 +08:00
|
|
|
const orders = await prisma.order.findMany({
|
2026-03-01 03:04:24 +08:00
|
|
|
where: {
|
|
|
|
|
status: 'PENDING',
|
|
|
|
|
expiresAt: { lt: new Date() },
|
|
|
|
|
},
|
2026-03-01 17:58:08 +08:00
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
paymentTradeNo: true,
|
|
|
|
|
paymentType: true,
|
|
|
|
|
},
|
2026-03-01 03:04:24 +08:00
|
|
|
});
|
|
|
|
|
|
2026-03-01 17:58:08 +08:00
|
|
|
if (orders.length === 0) return 0;
|
|
|
|
|
|
|
|
|
|
let expiredCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const order of orders) {
|
|
|
|
|
try {
|
|
|
|
|
// If order has a payment on the platform, check its actual status
|
|
|
|
|
if (order.paymentTradeNo && order.paymentType) {
|
|
|
|
|
try {
|
|
|
|
|
initPaymentProviders();
|
|
|
|
|
const provider = paymentRegistry.getProvider(order.paymentType as PaymentType);
|
|
|
|
|
|
|
|
|
|
// Query the real payment status before expiring
|
|
|
|
|
const queryResult = await provider.queryOrder(order.paymentTradeNo);
|
|
|
|
|
|
|
|
|
|
if (queryResult.status === 'paid') {
|
|
|
|
|
// User already paid — process as success instead of expiring
|
|
|
|
|
await confirmPayment({
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
tradeNo: order.paymentTradeNo,
|
|
|
|
|
paidAmount: queryResult.amount,
|
|
|
|
|
providerName: provider.name,
|
|
|
|
|
});
|
|
|
|
|
console.log(`Order ${order.id} was paid during timeout, processed as success`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Not paid — cancel on the platform
|
|
|
|
|
if (provider.cancelPayment) {
|
|
|
|
|
try {
|
|
|
|
|
await provider.cancelPayment(order.paymentTradeNo);
|
|
|
|
|
} catch (cancelErr) {
|
|
|
|
|
// Cancel may fail if session already expired on platform side — that's fine
|
|
|
|
|
console.warn(`Failed to cancel payment for order ${order.id}:`, cancelErr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (platformErr) {
|
|
|
|
|
// Platform unreachable — still expire the order locally
|
|
|
|
|
console.warn(`Platform check failed for order ${order.id}, expiring anyway:`, platformErr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark as expired in database (WHERE status='PENDING' ensures idempotency)
|
|
|
|
|
const result = await prisma.order.updateMany({
|
|
|
|
|
where: { id: order.id, status: 'PENDING' },
|
|
|
|
|
data: { status: 'EXPIRED' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.count > 0) expiredCount++;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(`Error expiring order ${order.id}:`, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (expiredCount > 0) {
|
|
|
|
|
console.log(`Expired ${expiredCount} orders`);
|
2026-03-01 03:04:24 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-01 17:58:08 +08:00
|
|
|
return expiredCount;
|
2026-03-01 03:04:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function startTimeoutScheduler(): void {
|
|
|
|
|
if (timer) return;
|
|
|
|
|
|
|
|
|
|
// Run immediately on startup
|
|
|
|
|
expireOrders().catch(console.error);
|
|
|
|
|
|
|
|
|
|
// Then run every 30 seconds
|
|
|
|
|
timer = setInterval(() => {
|
|
|
|
|
expireOrders().catch(console.error);
|
|
|
|
|
}, INTERVAL_MS);
|
|
|
|
|
|
|
|
|
|
console.log('Order timeout scheduler started');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function stopTimeoutScheduler(): void {
|
|
|
|
|
if (timer) {
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
timer = null;
|
|
|
|
|
console.log('Order timeout scheduler stopped');
|
|
|
|
|
}
|
|
|
|
|
}
|