import { NextRequest, NextResponse } from "next/server"; import { getZPayConfig } from "@/lib/sdk"; import { SITE_URL } from "@/app/lib/env"; const ZPAY_REQUIRED = [ "pid", "type", "out_trade_no", "notify_url", "return_url", "name", "money", "sign", "sign_type", ]; function escapeAttr(s: string): string { return String(s) .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } function buildPayHtml(payUrl: string, params: Record): string { const flat: Record = {}; for (const [k, v] of Object.entries(params)) { if (v == null) continue; const s = String(v).trim(); if (s === "") continue; flat[k] = s; } const inputs = Object.entries(flat) .map(([k, v]) => ``) .join("\n"); return ` 跳转支付...

正在跳转到支付页面...

${inputs}
`; } function detectDevice(request: NextRequest): "pc" | "h5" | "wechat" { const ua = request.headers.get("user-agent") || ""; if (/MicroMessenger/i.test(ua)) return "wechat"; if (/Mobile|Android|iPhone|iPad/i.test(ua)) return "h5"; return "pc"; } export async function POST(request: NextRequest) { try { let body: Record; const contentType = request.headers.get("content-type") || ""; if (contentType.includes("application/json")) { body = await request.json(); } else { const form = await request.formData(); body = Object.fromEntries(form.entries()) as Record; } const payConfig = getZPayConfig(); const user_id = String(body?.user_id || `user_${Date.now()}`).trim(); const return_url = String(body?.return_url || "").trim(); const total_fee = Number(body?.total_fee) || payConfig.defaultAmount; const type = String(body?.type || payConfig.defaultType); const channel = String(body?.channel || "alipay"); const device = (body?.device as string) || detectDevice(request); const origin = request.headers.get("x-forwarded-host") ? `${request.headers.get("x-forwarded-proto") || "https"}://${request.headers.get("x-forwarded-host")}` : request.headers.get("origin") || SITE_URL; const finalReturnUrl = return_url || `${origin}/pay/paid`; const payload = { user_id, total_fee, type, channel: device === "wechat" ? "wxpay" : channel, return_url: finalReturnUrl, device, }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); const redirectRes = await fetch(`${payConfig.apiUrl}/payh5/redirect`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...payload, client_ip: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "", }), redirect: "manual", signal: controller.signal, }).finally(() => clearTimeout(timeout)); const orderId = redirectRes.headers.get("x-pay-order-id")?.trim(); if (redirectRes.status >= 301 && redirectRes.status <= 308) { const location = redirectRes.headers.get("location"); if (location) { const res = NextResponse.redirect(location); if (orderId) { res.cookies.set("pay_order", `${orderId}|${Date.now()}`, { path: "/", maxAge: 900 }); } return res; } } const resData = await redirectRes.json().catch(() => ({})); if (redirectRes.status === 200 && resData?.use_form) { const { createPayOrder } = await import("@/lib/sdk"); const { params } = await createPayOrder({ user_id, return_url: finalReturnUrl, total_fee, type, channel: "wxpay", }); const html = buildPayHtml(payConfig.zpaySubmitUrl, params); const res = new NextResponse(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" }, }); if (orderId) { res.cookies.set("pay_order", `${orderId}|${Date.now()}`, { path: "/", maxAge: 900 }); } return res; } if (redirectRes.status === 200 && resData?.qrcode_mode && resData?.img) { const returnUrl = String(resData.return_url || finalReturnUrl).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c ); const imgSrc = String(resData.img).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c ); const ordId = String(resData.order_id || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c ); const ch = String(resData.channel || "alipay").toLowerCase(); const isWx = ch === "wxpay" || ch === "wx" || ch === "wechat"; const title = isWx ? "微信扫码支付" : "支付宝扫码支付"; const hint = isWx ? "请使用微信扫描下方二维码完成支付" : "请使用支付宝扫描下方二维码完成支付"; const esc = (s: string) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c); const html = ` ${esc(title)}

${esc(title)}

${esc(hint)}

支付二维码

支付成功后将自动跳转...

`; return new NextResponse(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" }, }); } if (redirectRes.status === 400 || redirectRes.status === 500) { return NextResponse.json( { ok: false, error: resData?.detail || resData?.msg || "支付跳转失败" }, { status: redirectRes.status } ); } return NextResponse.json( { ok: false, error: "支付服务暂不可用,请稍后重试" }, { status: 500 } ); } catch (e) { const err = e instanceof Error ? e : new Error(String(e)); const msg = err.message || ""; const hint = msg.includes("fetch") || msg.includes("ECONNREFUSED") ? "请确认 payjsapi 已启动:cd C:\\project\\payjsapi && python run.py" : (err as Error).name === "AbortError" ? "请求超时,请检查 payjsapi 是否正常运行" : msg; console.error("Pay API error:", err); return NextResponse.json( { ok: false, error: `支付请求失败: ${hint}` }, { status: 500 } ); } }