From ceb951f50e716ee24c66babf75ad42173535299d Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 10 Mar 2026 08:11:06 -0500 Subject: [PATCH] 's' --- app/api/pay/route.ts | 31 +++++++++++++++++------ app/page.tsx | 59 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/app/api/pay/route.ts b/app/api/pay/route.ts index c3feaef..150268f 100644 --- a/app/api/pay/route.ts +++ b/app/api/pay/route.ts @@ -37,10 +37,10 @@ function buildPayHtml( .map(([k, v]) => ``) .join("\n"); const direct = options?.directToCashier; - // 手机/微信:无中转页,直接提交表单跳转收银台 + // direct:微信内无中转,直接提交 | 非 direct:PC/H5 有中转页提示 return direct ? `支付
${inputs}
` - : `支付
${inputs}
`; + : `跳转支付

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

${inputs}
`; } function setPayOrderCookie(res: NextResponse, orderId: string) { @@ -54,7 +54,8 @@ async function createPayOrder( total_fee: number, type: string, channel: string, - clientIp?: string + clientIp?: string, + provider?: string ) { const payload: Record = { user_id, @@ -63,6 +64,7 @@ async function createPayOrder( channel, return_url, ...(clientIp && { client_ip: clientIp }), + ...(provider && { provider }), }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); @@ -116,6 +118,7 @@ export async function POST(request: NextRequest) { const type = String(body?.type || "vip"); const channel = String(body?.channel || "wxpay"); const device = String(body?.device || "pc"); + const reqProvider = String(body?.provider || "").trim().toLowerCase() || undefined; const accept = request.headers.get("accept") || ""; const wantHtml = String(body?.html) === "1" || accept.includes("text/html"); @@ -135,10 +138,6 @@ export async function POST(request: NextRequest) { : request.headers.get("origin") || "http://localhost:3000"; const finalReturnUrl = return_url || `${origin}/paid`; - if (!wantHtml) { - return NextResponse.json({ ok: false, error: "请传 html=1" }, { status: 400 }); - } - const clientIp = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || @@ -151,7 +150,8 @@ export async function POST(request: NextRequest) { total_fee, type, channel, - clientIp + clientIp, + reqProvider ); if (!params || Object.keys(params).length === 0) { @@ -164,6 +164,20 @@ export async function POST(request: NextRequest) { return NextResponse.json({ ok: false, error: "支付参数为空" }, { status: 500 }); } + // wantHtml=0:返回 JSON,供微信 fetch 后直接提交表单(无中转页) + if (!wantHtml) { + const orderId = String(params?.order_id || params?.out_trade_no || "").trim(); + const res = NextResponse.json({ + ok: true, + params, + payUrl: provider === "xorpay" ? payUrl : payConfig.zpaySubmitUrl, + provider, + order_id: orderId, + }); + if (orderId) setPayOrderCookie(res, orderId); + return res; + } + // xorpay:直接 payh5 返回收银台参数,无需 redirect 中转 const submitUrl = provider === "xorpay" ? payUrl : payConfig.zpaySubmitUrl; if (provider === "xorpay") { @@ -187,6 +201,7 @@ export async function POST(request: NextRequest) { channel, return_url: finalReturnUrl, device, + provider: "zpay", ...(clientIp && { client_ip: clientIp }), }; diff --git a/app/page.tsx b/app/page.tsx index 799babf..ce221db 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -13,7 +13,8 @@ import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll"; const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn"; -function redirectToPay(params: { +/** PC/H5:form POST 到 /api/pay,有中转页,默认 zpay */ +function redirectToPayZpay(params: { user_id: string; return_url: string; total_fee: number; @@ -33,6 +34,7 @@ function redirectToPay(params: { ["type", params.type], ["channel", params.channel], ["device", params.device], + ["provider", "zpay"], ["html", "1"], ]; fields.forEach(([k, v]) => { @@ -46,6 +48,47 @@ function redirectToPay(params: { form.submit(); } +/** 微信内:fetch 获取 xorpay 参数,直接提交表单到收银台,无中转页 */ +async function payWechatXorpay(params: { + user_id: string; + return_url: string; + total_fee: number; + type: string; + channel: "alipay" | "wxpay"; +}) { + const res = await fetch("/api/pay", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ...params, + total_fee: params.total_fee, + device: "wechat", + provider: "xorpay", + html: "0", + }), + }); + const data = await res.json().catch(() => ({})); + if (!data?.ok || !data?.params || !data?.payUrl) { + alert(data?.error || "支付发起失败"); + return; + } + const form = document.createElement("form"); + form.method = "POST"; + form.action = data.payUrl; + form.target = "_self"; + form.style.display = "none"; + for (const [k, v] of Object.entries(data.params as Record)) { + if (v == null) continue; + const input = document.createElement("input"); + input.type = "hidden"; + input.name = k; + input.value = String(v); + form.appendChild(input); + } + document.body.appendChild(form); + form.submit(); +} + function getStorage(key: string): string | null { if (typeof window === "undefined") return null; return localStorage.getItem(key); @@ -162,14 +205,12 @@ export default function Home() { const env = getPayEnv(); const device = getDeviceFromEnv(env); const totalFee = getPaymentConfig().defaultAmount ?? 880; - redirectToPay({ - user_id: userId, - return_url: returnUrl, - total_fee: totalFee, - type: "vip", - channel: "wxpay", - device, - }); + const base = { user_id: userId, return_url: returnUrl, total_fee: totalFee, type: "vip", channel: "wxpay" as const }; + if (device === "wechat") { + payWechatXorpay(base); + } else { + redirectToPayZpay({ ...base, device }); + } }, [getOrCreateUserId]); const handleCopyUsername = useCallback(() => {