diff --git a/app/api/pay/route.ts b/app/api/pay/route.ts index 2eedb0f..b7ecc71 100644 --- a/app/api/pay/route.ts +++ b/app/api/pay/route.ts @@ -3,7 +3,7 @@ * PC: 二维码 | H5: 302 跳转 | 微信内: 收银台表单 */ import { NextRequest, NextResponse } from "next/server"; -import { getPaymentConfig, getApiUrlFromRequest } from "@/config/services.config"; +import { getPaymentConfig } from "@/config/services.config"; const ZPAY_REQUIRED = ["pid", "type", "out_trade_no", "notify_url", "return_url", "name", "money", "sign", "sign_type"]; @@ -15,7 +15,11 @@ function escapeAttr(s: string): string { .replace(/>/g, ">"); } -function buildPayHtml(payUrl: string, params: Record): string { +function buildPayHtml( + payUrl: string, + params: Record, + options?: { directToCashier?: boolean } +): string { const flat: Record = {}; for (const [k, v] of Object.entries(params)) { if (v == null) continue; @@ -26,7 +30,10 @@ function buildPayHtml(payUrl: string, params: Record): string { const inputs = Object.entries(flat) .map(([k, v]) => ``) .join("\n"); - return ` + const direct = options?.directToCashier; + return direct + ? `支付
${inputs}
` + : ` @@ -48,6 +55,42 @@ function setPayOrderCookie(res: NextResponse, orderId: string) { res.cookies.set("nomadvip_pay_order", `${orderId}|${Date.now()}`, { path: "/", maxAge: 900 }); } +async function createPayOrder( + apiUrl: string, + user_id: string, + return_url: string, + total_fee: number, + type: string, + channel: string, + clientIp?: string +) { + const payload: Record = { + user_id, + total_fee, + type, + channel, + return_url, + ...(clientIp && { client_ip: clientIp }), + }; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 15000); + const res = await fetch(`${apiUrl}/nomadvip/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 || "支付创建失败"); + } + const params = data.params || {}; + if (!params || typeof params !== "object") { + throw new Error("payjsapi 未返回支付参数"); + } + return { params: params as Record, payUrl: data.pay_url || "" }; +} + export async function POST(request: NextRequest) { try { let body: Record; @@ -60,9 +103,7 @@ export async function POST(request: NextRequest) { } const payConfig = getPaymentConfig(); - const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host"); - const envApiUrl = process.env.PAYMENT_API_URL || process.env.NEXT_PUBLIC_API_URL; - const apiUrl = (envApiUrl ? payConfig.apiUrl : getApiUrlFromRequest(hostHeader)).replace(/\/$/, ""); + const apiUrl = payConfig.apiUrl.replace(/\/$/, ""); const user_id = String(body?.user_id || "").trim(); const return_url = String(body?.return_url || "").trim(); const total_fee = Number(body?.total_fee) || 18800; @@ -80,7 +121,8 @@ export async function POST(request: NextRequest) { : request.headers.get("origin") || "http://localhost:3000"; const finalReturnUrl = return_url || `${origin}/paid`; - const wantHtml = String(body?.html) === "1"; + const accept = request.headers.get("accept") || ""; + const wantHtml = String(body?.html) === "1" || accept.includes("text/html"); if (!wantHtml) { return NextResponse.json({ ok: false, error: "请传 html=1" }, { status: 400 }); @@ -91,6 +133,20 @@ export async function POST(request: NextRequest) { request.headers.get("x-real-ip") || ""; + const { params } = await createPayOrder( + apiUrl, + user_id, + finalReturnUrl, + total_fee, + type, + channel, + clientIp + ); + + if (!params || Object.keys(params).length === 0) { + return NextResponse.json({ ok: false, error: "支付参数为空" }, { status: 500 }); + } + const redirectPayload = { user_id, total_fee, @@ -102,7 +158,7 @@ export async function POST(request: NextRequest) { }; const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 15000); + const timeout = setTimeout(() => controller.abort(), 45000); const redirectRes = await fetch(`${apiUrl}/nomadvip/payh5/redirect`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -115,7 +171,9 @@ export async function POST(request: NextRequest) { const location = redirectRes.headers.get("location"); if (location) { const res = NextResponse.redirect(location); - const orderId = redirectRes.headers.get("x-pay-order-id")?.trim(); + const orderId = + redirectRes.headers.get("x-pay-order-id")?.trim() || + String(params?.out_trade_no || "").trim(); if (orderId) setPayOrderCookie(res, orderId); return res; } @@ -124,19 +182,10 @@ export async function POST(request: NextRequest) { const resData = await redirectRes.json().catch(() => ({})); if (redirectRes.status === 200 && resData?.use_form) { - // 微信内:直接用 payh5 获取 params,构建收银台表单(不调用 redirect 避免重复创建订单) - const createRes = await fetch(`${apiUrl}/nomadvip/payh5`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ user_id, total_fee, type, channel }), - }); - const createData = await createRes.json().catch(() => ({})); - const params = createData?.params || {}; const orderId = String(params?.out_trade_no || "").trim(); - if (!params || Object.keys(params).length === 0) { - return NextResponse.json({ ok: false, error: "支付参数为空" }, { status: 500 }); - } - const html = buildPayHtml(payConfig.zpaySubmitUrl, params as Record); + const html = buildPayHtml(payConfig.zpaySubmitUrl, params as Record, { + directToCashier: device === "wechat", + }); const res = new NextResponse(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" }, diff --git a/app/api/pay/status/route.ts b/app/api/pay/status/route.ts index 969d249..3161fa1 100644 --- a/app/api/pay/status/route.ts +++ b/app/api/pay/status/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { getPaymentConfig, getApiUrlFromRequest } from "@/config/services.config"; +import { getPaymentConfig } from "@/config/services.config"; export async function GET(request: NextRequest) { const orderId = request.nextUrl.searchParams.get("order_id"); @@ -7,17 +7,17 @@ export async function GET(request: NextRequest) { return NextResponse.json({ ok: false, error: "缺少 order_id" }, { status: 400 }); } const payConfig = getPaymentConfig(); - const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host"); - const envApiUrl = process.env.PAYMENT_API_URL || process.env.NEXT_PUBLIC_API_URL; - const apiUrl = (envApiUrl ? payConfig.apiUrl : getApiUrlFromRequest(hostHeader)).replace(/\/$/, ""); + const apiUrl = payConfig.apiUrl.replace(/\/$/, ""); try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 25000); const res = await fetch( `${apiUrl}/nomadvip/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`, - { cache: "no-store" } - ); + { cache: "no-store", signal: controller.signal } + ).finally(() => clearTimeout(timeout)); const data = await res.json().catch(() => ({})); return NextResponse.json({ ok: true, paid: !!data?.paid }); } catch (e) { - return NextResponse.json({ ok: false, paid: false, error: String(e) }, { status: 500 }); + return NextResponse.json({ ok: true, paid: false }); } }