import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db'; import { ORDER_STATUS } from '@/lib/constants'; import { getEnv } from '@/lib/config'; import { buildAlipayPaymentUrl } from '@/lib/alipay/provider'; import { deriveOrderState, getOrderDisplayState, type OrderStatusLike } from '@/lib/order/status'; import { buildOrderResultUrl } from '@/lib/order/status-access'; export const dynamic = 'force-dynamic'; const MOBILE_UA_PATTERN = /AlipayClient|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i; const ALIPAY_APP_UA_PATTERN = /AlipayClient/i; type ShortLinkOrderStatus = OrderStatusLike & { id: string }; function getUserAgent(request: NextRequest): string { return request.headers.get('user-agent') || ''; } function isMobileRequest(request: NextRequest): boolean { return MOBILE_UA_PATTERN.test(getUserAgent(request)); } function isAlipayAppRequest(request: NextRequest): boolean { return ALIPAY_APP_UA_PATTERN.test(getUserAgent(request)); } function escapeHtml(value: string): string { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function buildAppUrl(pathname = '/'): string { return new URL(pathname, getEnv().NEXT_PUBLIC_APP_URL).toString(); } function buildResultUrl(orderId: string): string { return buildOrderResultUrl(getEnv().NEXT_PUBLIC_APP_URL, orderId); } function serializeScriptString(value: string): string { return JSON.stringify(value).replace(/ ${escapeHtml(title)} ${headExtra} ${body} `; } function renderErrorPage(title: string, message: string, orderId?: string, status = 400): NextResponse { const html = renderHtml( title, `
!

${escapeHtml(title)}

${escapeHtml(message)}

${orderId ? `
订单号:${escapeHtml(orderId)}
` : ''} 返回支付首页
`, ); return new NextResponse(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store, no-cache, must-revalidate', }, }); } function renderStatusPage(order: ShortLinkOrderStatus): NextResponse { const display = getStatusDisplay(order); const html = renderHtml( display.label, `
${escapeHtml(display.icon)}

${escapeHtml(display.label)}

${escapeHtml(display.message)}

订单号:${escapeHtml(order.id)}
查看订单结果
`, ); return new NextResponse(html, { headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store, no-cache, must-revalidate', }, }); } function renderRedirectPage(orderId: string, payUrl: string): NextResponse { const html = renderHtml( '正在跳转支付宝', `

正在拉起支付宝

请稍候,系统正在自动跳转到支付宝完成支付。

订单号:${escapeHtml(orderId)}

如未自动拉起支付宝,请返回原充值页后重新发起支付。

已支付?查看订单结果
`, ``, ); return new NextResponse(html, { headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store, no-cache, must-revalidate', }, }); } export async function GET(request: NextRequest, { params }: { params: Promise<{ orderId: string }> }) { const { orderId } = await params; const order = await prisma.order.findUnique({ where: { id: orderId }, select: { id: true, amount: true, payAmount: true, paymentType: true, status: true, expiresAt: true, paidAt: true, completedAt: true, }, }); if (!order) { return renderErrorPage('订单不存在', '未找到对应订单,请确认二维码是否正确', orderId, 404); } if (order.paymentType !== 'alipay_direct') { return renderErrorPage('支付方式不匹配', '该订单不是支付宝直连订单,无法通过当前链接支付', orderId, 400); } if (order.status !== ORDER_STATUS.PENDING) { return renderStatusPage(order); } if (order.expiresAt.getTime() <= Date.now()) { return renderStatusPage({ id: order.id, status: ORDER_STATUS.EXPIRED, paidAt: order.paidAt, completedAt: order.completedAt, }); } const payAmount = Number(order.payAmount ?? order.amount); if (!Number.isFinite(payAmount) || payAmount <= 0) { return renderErrorPage('订单金额异常', '订单金额无效,请返回原页面重新发起支付', order.id, 500); } const env = getEnv(); const payUrl = buildAlipayPaymentUrl({ orderId: order.id, amount: payAmount, subject: `${env.PRODUCT_NAME} ${payAmount.toFixed(2)} CNY`, notifyUrl: env.ALIPAY_NOTIFY_URL, returnUrl: isAlipayAppRequest(request) ? null : buildResultUrl(order.id), isMobile: isMobileRequest(request), }); return renderRedirectPage(order.id, payUrl); }