From c5e057f0ed23145c66f95943fa1e27abce51b9be Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 16 Mar 2026 11:45:38 -0500 Subject: [PATCH] 's'' --- README.md | 2 +- app/api/pay/route.ts | 110 +++++++++++------- app/components/Footer.tsx | 2 +- app/components/HomeContent.tsx | 82 ++++++++++++- app/components/JoinModal.tsx | 19 ++- app/join/page.tsx | 203 ++++++++++++++++++++++++++++++--- app/lib/payment/client.ts | 127 +++++++++++++++++++++ app/lib/payment/index.ts | 7 +- config/home.config.ts | 2 +- 9 files changed, 482 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 1d975e2..cc19995 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ PC 扫码支付后自动检测逻辑(参考 nomadvip): 2. 失败时 **直接向 zpay/xorpay 查询** 兜底(不依赖回调) 3. 检测到 paid 后调用 complete-order → 跳转报名成功页 -若仍无反应:点击「支付完成?点此手动检查」;确认 ZPAY_PID/KEY、XORPAY_AID/SECRET 与 salonapi 一致 +若仍无反应:确认 ZPAY_PID/KEY、XORPAY_AID/SECRET 与 salonapi 一致 ## Getting started diff --git a/app/api/pay/route.ts b/app/api/pay/route.ts index 4b9af83..1e32de6 100644 --- a/app/api/pay/route.ts +++ b/app/api/pay/route.ts @@ -7,6 +7,9 @@ import { } from "@/config/services.config"; import { SITE_URL } from "@/app/lib/env"; +/** ZPAY 创建订单较慢(约 6–10s),需延长超时避免手机浏览器发起支付超时 */ +export const maxDuration = 30; + const ORDER_COOKIE = "join_pay_order"; const SALON_PAY_PREFIX = "/salon"; @@ -165,23 +168,11 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;} .then(function(r){return r.json();}) .then(function(d){ if(d&&d.paid){doConfirm(0);return;} - setTimeout(check,300); + setTimeout(check,800); }) - .catch(function(){setTimeout(check,1000);}); + .catch(function(){setTimeout(check,1500);}); } - var manualBtn=document.createElement("button"); - manualBtn.textContent="支付完成?点此手动检查"; - manualBtn.style.cssText="margin-top:12px;padding:8px 16px;font-size:13px;color:#64748b;background:#f1f5f9;border:none;border-radius:8px;cursor:pointer"; - manualBtn.onclick=function(){ - manualBtn.disabled=true; - manualBtn.textContent="检查中…"; - fetch("/api/pay/status?order_id="+encodeURIComponent(orderId),{cache:"no-store",credentials:"same-origin"}) - .then(function(r){return r.json();}) - .then(function(d){if(d&&d.paid){doConfirm(0);}else{manualBtn.disabled=false;manualBtn.textContent="支付完成?点此手动检查";}}) - .catch(function(){manualBtn.disabled=false;manualBtn.textContent="检查失败,重试";}); - }; - if(tip&&tip.parentNode){tip.parentNode.appendChild(manualBtn);} - setTimeout(check,300); + setTimeout(check,500); })(); `; @@ -243,7 +234,7 @@ async function requestRedirect( payload: Record ) { const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 12000); + const timeout = setTimeout(() => controller.abort(), 25000); try { return await fetch(`${apiUrl}${path}`, { method: "POST", @@ -309,30 +300,33 @@ export async function POST(request: NextRequest) { device ); - const created = await createPayOrder( - apiUrl, - userId, - returnUrl, - totalFee, - type, - channel, - forcedProvider, - clientIp - ); + // 微信内:xorpay 收银台,需先 createPayOrder + // PC/手机浏览器:zpay,参考 nomadvip 直接走 redirect,跳过 payh5 避免重复建单 + const isWechat = device === "wechat"; + let created: Awaited> | null = null; - if (!wantHtml) { - const response = NextResponse.json({ - ok: true, - params: created.params, - payUrl: created.payUrl, - provider: created.provider, - order_id: created.orderId, - }); - if (created.orderId) setPayOrderCookie(response, created.orderId); - return response; - } - - if (created.provider === "xorpay") { + if (isWechat) { + created = await createPayOrder( + apiUrl, + userId, + returnUrl, + totalFee, + type, + channel, + "xorpay", + clientIp + ); + if (!wantHtml) { + const response = NextResponse.json({ + ok: true, + params: created.params, + payUrl: created.payUrl, + provider: created.provider, + order_id: created.orderId, + }); + if (created.orderId) setPayOrderCookie(response, created.orderId); + return response; + } const html = buildPayHtml(created.payUrl, created.params, { directToCashier: true, orderId: created.orderId, @@ -345,6 +339,7 @@ export async function POST(request: NextRequest) { return response; } + // PC/手机:zpay 直接 redirect(手机 h5 得 302 跳转支付页,PC 得二维码页) const redirectPayload: Record = { user_id: userId, site_id: SITE_ID, @@ -366,10 +361,18 @@ export async function POST(request: NextRequest) { if (redirectRes.status >= 301 && redirectRes.status <= 308) { const location = redirectRes.headers.get("location"); if (location) { + const orderId = redirectRes.headers.get("x-pay-order-id")?.trim(); + if (!wantHtml) { + const res = NextResponse.json({ + ok: true, + order_id: orderId, + redirect_url: location, + provider: "zpay", + }); + if (orderId) setPayOrderCookie(res, orderId); + return res; + } const response = NextResponse.redirect(location); - const orderId = - redirectRes.headers.get("x-pay-order-id")?.trim() || - created.orderId; if (orderId) setPayOrderCookie(response, orderId); return response; } @@ -391,7 +394,7 @@ export async function POST(request: NextRequest) { c ] || c) ); - const rawOrderId = String(resData.order_id || created.orderId).trim(); + const rawOrderId = String(resData.order_id || "").trim(); const html = buildQrHtml(imgSrc, safeReturnUrl, rawOrderId, { channel: resData.channel || "wxpay", confirmUrl: "/api/pay/complete-order", @@ -405,6 +408,16 @@ export async function POST(request: NextRequest) { } if (redirectRes.status === 200 && resData?.use_form) { + created = await createPayOrder( + apiUrl, + userId, + returnUrl, + totalFee, + type, + channel, + undefined, + clientIp + ); const html = buildPayHtml(payConfig.zpaySubmitUrl, created.params, { directToCashier: true, orderId: String(resData.order_id || created.orderId).trim(), @@ -432,6 +445,17 @@ export async function POST(request: NextRequest) { ); } + // 兜底:redirect 失败时回退到 payh5 + created = await createPayOrder( + apiUrl, + userId, + returnUrl, + totalFee, + type, + channel, + undefined, + clientIp + ); const html = buildPayHtml(created.payUrl, created.params, { orderId: created.orderId, }); diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx index 21c5d6a..bd72e9a 100644 --- a/app/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -5,7 +5,7 @@ export default function Footer() {