import { NextRequest, NextResponse } from "next/server"; import { getPaymentConfig, getJoinPaymentConfig } from "@/config/services.config"; import { SITE_URL } from "../../lib/env"; async function createPayOrder( user_id: string, return_url: string, total_fee?: number, type?: string, channel = "alipay" ) { const joinConfig = getJoinPaymentConfig(); const fee = total_fee ?? joinConfig.amount; const orderType = type ?? joinConfig.type; const config = getPaymentConfig(); if (!config.apiUrl) throw new Error("未配置 PAYMENT_API_URL"); const payload = { user_id, total_fee: Number(fee) || joinConfig.amount, type: orderType, channel, return_url, }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); const res = await fetch(`${config.apiUrl}/payh5`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: controller.signal, }).finally(() => clearTimeout(timeout)); const data = await res.json().catch(() => ({})); if (!res.ok || data?.status !== "ok") { throw new Error(data?.detail || data?.msg || data?.message || "支付创建失败"); } let params = data.params || data.xorpay_params || {}; const payUrl = data.pay_url || data.xorpay_url || config.zpaySubmitUrl; if (!params || typeof params !== "object") { throw new Error("payjsapi 未返回支付参数"); } params = JSON.parse(JSON.stringify(params)); if (data.provider === "xorpay") { throw new Error("当前 payjsapi 使用 xorpay,请设置 PAYMENT_PROVIDER=zpay 后重启 payjsapi"); } return { params, payUrl }; } 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; } for (const key of ZPAY_REQUIRED) { if (!flat[key]) { console.error(`ZPAY 缺少必填参数: ${key}`, flat); } } const inputs = Object.entries(flat) .map(([k, v]) => ``) .join("\n"); return ` 跳转支付...

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

${inputs}
`; } 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 joinConfig = getJoinPaymentConfig(); const payConfig = getPaymentConfig(); const user_id = String(body?.user_id || "").trim(); const return_url = String(body?.return_url || "").trim(); const total_fee = Number(body?.total_fee) || joinConfig.amount; const type = String(body?.type || joinConfig.type); const channel = String(body?.channel || "alipay"); const device = String(body?.device || "pc"); // pc | h5 | wechat,微信内传 wechat if (!user_id) { return NextResponse.json({ ok: false, error: "缺少 user_id" }, { status: 400 }); } 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; // Z-Pay return_url 不支持带参数,使用 /join/paid 专用路径 const finalReturnUrl = return_url || `${origin}/zh/join/paid`; const { params, payUrl } = await createPayOrder( user_id, finalReturnUrl, total_fee, type, channel ); if (!params || Object.keys(params).length === 0) { return NextResponse.json({ ok: false, error: "支付参数为空" }, { status: 500 }); } const accept = request.headers.get("accept") || ""; const wantHtml = String(body?.html) === "1" || accept.includes("text/html"); if (wantHtml) { const clientIp = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || ""; // 使用 payjsapi /payh5/redirect:mapi.php API接口支付,按 device 返回 payurl/payurl2 const redirectPayload = { user_id, total_fee, type, channel, return_url: finalReturnUrl, device, ...(clientIp && { client_ip: clientIp }), }; 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(redirectPayload), redirect: "manual", signal: controller.signal, }).finally(() => clearTimeout(timeout)); if (redirectRes.status >= 301 && redirectRes.status <= 308) { const location = redirectRes.headers.get("location"); if (location) { const res = NextResponse.redirect(location); // payh5/redirect 创建的是新订单,必须用其返回的 order_id(非 createPayOrder 的 params) const orderId = redirectRes.headers.get("x-pay-order-id")?.trim() || String(params?.out_trade_no || "").trim(); if (orderId) { // 附带时间戳,前端只对 15 分钟内设置的 cookie 轮询,避免陈旧 cookie 误触发 const payload = `${orderId}|${Date.now()}`; res.cookies.set("join_pay_order", payload, { path: "/", maxAge: 900 }); } return res; } } const resData = await redirectRes.json().catch(() => ({})); // 微信内:payurl2 会提示「请外微信外打开」,改用收银台表单由 Z-Pay 识别 UA if (redirectRes.status === 200 && resData?.use_form) { const html = buildPayHtml(payConfig.zpaySubmitUrl, params as Record); const res = new NextResponse(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" }, }); const orderId = resData?.order_id?.trim() || String(params?.out_trade_no || "").trim(); if (orderId) { res.cookies.set("join_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 orderId = 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" }, }); } const errData = resData; if (redirectRes.status === 400 || redirectRes.status === 500) { return NextResponse.json( { ok: false, error: errData?.detail || errData?.msg || "支付跳转失败" }, { status: redirectRes.status } ); } // 降级:payjsapi 未提供 redirect 或非 zpay,使用原有表单提交逻辑 const formParams = new URLSearchParams(); const missing: string[] = []; for (const key of ZPAY_REQUIRED) { const v = params[key]; if (v == null || String(v).trim() === "") { missing.push(key); } else { formParams.append(key, String(v)); } } for (const [k, v] of Object.entries(params)) { if (!ZPAY_REQUIRED.includes(k) && v != null && String(v).trim() !== "") { formParams.append(k, String(v)); } } if (missing.length > 0) { return NextResponse.json( { ok: false, error: `payjsapi 返回参数缺少: ${missing.join(", ")}。请确认 payjsapi 已设置 PAYMENT_PROVIDER=zpay`, }, { status: 500 } ); } const zpayRes = await fetch(payConfig.zpaySubmitUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: formParams.toString(), redirect: "manual", }); const location = zpayRes.headers.get("location"); if ((zpayRes.status === 301 || zpayRes.status === 302) && location) { const res = NextResponse.redirect(location); const outTradeNo = String(params?.out_trade_no || "").trim(); if (outTradeNo) { const payload = `${outTradeNo}|${Date.now()}`; res.cookies.set("join_pay_order", payload, { path: "/", maxAge: 900 }); } return res; } const text = await zpayRes.text(); try { const errJson = JSON.parse(text); if (errJson?.code === "error") { return NextResponse.json( { ok: false, error: errJson.msg || "ZPAY 返回错误" }, { status: 500 } ); } } catch { /* ignore */ } const html = buildPayHtml(payUrl, params as Record); return new NextResponse(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" }, }); } return NextResponse.json({ ok: true, pay_url: payUrl, params, provider: "zpay", submit_method: "POST", }); } 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.name === "AbortError" ? "请求超时,请检查 payjsapi 是否正常运行" : msg; console.error("Pay API error:", err); return NextResponse.json( { ok: false, error: `支付请求失败: ${hint}` }, { status: 500 } ); } }