's'
This commit is contained in:
@@ -114,9 +114,9 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
const return_url = String(body?.return_url || "").trim();
|
||||
const total_fee = Number(body?.total_fee) || 180;
|
||||
const total_fee = Number(body?.total_fee) || 8800;
|
||||
const type = String(body?.type || "vip");
|
||||
const channel = String(body?.channel || "alipay");
|
||||
const channel = String(body?.channel || "wxpay");
|
||||
const device = String(body?.device || "pc");
|
||||
|
||||
if (!user_id) {
|
||||
@@ -178,10 +178,65 @@ export async function POST(request: NextRequest) {
|
||||
if (redirectRes.status >= 301 && redirectRes.status <= 308) {
|
||||
const location = redirectRes.headers.get("location");
|
||||
if (location) {
|
||||
const res = NextResponse.redirect(location);
|
||||
const orderId =
|
||||
redirectRes.headers.get("x-pay-order-id")?.trim() ||
|
||||
String(params?.out_trade_no || "").trim();
|
||||
if (device === "h5") {
|
||||
const returnUrl = finalReturnUrl.replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c
|
||||
);
|
||||
const payUrl = location.replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c
|
||||
);
|
||||
const isWx = ["wxpay", "wx", "wechat"].includes(channel.toLowerCase());
|
||||
const title = isWx ? "微信支付" : "支付宝支付";
|
||||
const hint = isWx
|
||||
? "点击下方按钮跳转至支付页面,支付完成后将自动返回"
|
||||
: "点击下方按钮跳转至支付页面,支付完成后将自动返回";
|
||||
const esc = (s: string) =>
|
||||
s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
|
||||
const html = `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>${esc(title)}</title>
|
||||
<style>body{font-family:system-ui;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0f4f8;padding:20px;}h2{color:#333;margin-bottom:8px;}p{color:#64748b;font-size:14px;text-align:center;}.btn{display:block;width:100%;max-width:280px;margin:24px auto;padding:14px 24px;font-size:16px;font-weight:600;color:#fff;background:#07c160;border:none;border-radius:8px;cursor:pointer;text-align:center;text-decoration:none;}.btn:active{opacity:0.9;}.tip{font-size:12px;color:#94a3b8;margin-top:16px;}</style></head>
|
||||
<body>
|
||||
<h2>${esc(title)}</h2>
|
||||
<p>${esc(hint)}</p>
|
||||
<a href="${payUrl}" class="btn">跳转支付</a>
|
||||
<p class="tip" id="tip">支付成功后将自动跳转,1 分钟内未支付将返回首页</p>
|
||||
<script>
|
||||
(function(){
|
||||
var orderId="${String(orderId).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}";
|
||||
var returnUrl="${returnUrl}";
|
||||
if(!orderId){return;}
|
||||
var tip=document.getElementById("tip");
|
||||
var t=0;
|
||||
var maxT=30;
|
||||
function check(){
|
||||
t++;
|
||||
if(t>maxT){tip.textContent="支付超时,返回首页";window.location.href="/";return;}
|
||||
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId))
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(d){
|
||||
if(d.ok&&d.paid){window.location.href=returnUrl;return;}
|
||||
setTimeout(check,2000);
|
||||
})
|
||||
.catch(function(){setTimeout(check,2000);});
|
||||
}
|
||||
setTimeout(check,2000);
|
||||
})();
|
||||
</script>
|
||||
</body></html>`;
|
||||
const res = new NextResponse(html, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
});
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
return res;
|
||||
}
|
||||
const res = NextResponse.redirect(location);
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
return res;
|
||||
}
|
||||
@@ -229,7 +284,7 @@ export async function POST(request: NextRequest) {
|
||||
<h2>${esc(title)}</h2>
|
||||
<p>${esc(hint)}</p>
|
||||
<img src="${imgSrc}" alt="支付二维码" />
|
||||
<p class="tip" id="tip">支付成功后将自动跳转...</p>
|
||||
<p class="tip" id="tip">支付成功后将自动跳转,1 分钟内未支付将返回首页</p>
|
||||
<script>
|
||||
(function(){
|
||||
var orderId="${orderId}";
|
||||
@@ -237,10 +292,10 @@ export async function POST(request: NextRequest) {
|
||||
if(!orderId){return;}
|
||||
var tip=document.getElementById("tip");
|
||||
var t=0;
|
||||
var maxT=300;
|
||||
var maxT=30;
|
||||
function check(){
|
||||
t++;
|
||||
if(t>maxT){tip.textContent="轮询超时";return;}
|
||||
if(t>maxT){tip.textContent="支付超时,返回首页";window.location.href="/";return;}
|
||||
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId))
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(d){
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const COOKIE_NAME = "nomadvip_pay_order";
|
||||
const POLL_INTERVAL = 2000;
|
||||
const MAX_POLLS = 150;
|
||||
const MAX_POLLS = 30;
|
||||
const MAX_AGE_MS = 15 * 60 * 1000;
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
@@ -31,10 +31,12 @@ function parsePayOrderCookie(raw: string | null): [string | null, boolean] {
|
||||
return [orderId, true];
|
||||
}
|
||||
|
||||
export function usePayStatusPoll(onPaid: () => void): boolean {
|
||||
export function usePayStatusPoll(onPaid: () => void, onTimeout?: () => void): boolean {
|
||||
const [polling, setPolling] = useState(false);
|
||||
const onPaidRef = useRef(onPaid);
|
||||
const onTimeoutRef = useRef(onTimeout);
|
||||
onPaidRef.current = onPaid;
|
||||
onTimeoutRef.current = onTimeout;
|
||||
|
||||
useEffect(() => {
|
||||
const raw = getCookie(COOKIE_NAME);
|
||||
@@ -52,6 +54,7 @@ export function usePayStatusPoll(onPaid: () => void): boolean {
|
||||
clearInterval(timer);
|
||||
clearCookie(COOKIE_NAME);
|
||||
setPolling(false);
|
||||
onTimeoutRef.current?.();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
12
app/page.tsx
12
app/page.tsx
@@ -6,12 +6,7 @@ import { marked } from "marked";
|
||||
import PocketBase from "pocketbase";
|
||||
import { indexMd } from "./index-content";
|
||||
import styles from "./index.module.css";
|
||||
import {
|
||||
getPayEnv,
|
||||
getRecommendedChannel,
|
||||
mustUseWxPay,
|
||||
getDeviceFromEnv,
|
||||
} from "@/app/lib/payEnv";
|
||||
import { getPayEnv, getDeviceFromEnv } from "@/app/lib/payEnv";
|
||||
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
|
||||
|
||||
const PB_URL =
|
||||
@@ -152,14 +147,13 @@ export default function Home() {
|
||||
const userId = getOrCreateUserId();
|
||||
const returnUrl = `${window.location.origin}/paid`;
|
||||
const env = getPayEnv();
|
||||
const channel = mustUseWxPay(env) ? "wxpay" : getRecommendedChannel(env);
|
||||
const device = getDeviceFromEnv(env);
|
||||
redirectToPay({
|
||||
user_id: userId,
|
||||
return_url: returnUrl,
|
||||
total_fee: 180,
|
||||
total_fee: 8800,
|
||||
type: "vip",
|
||||
channel,
|
||||
channel: "wxpay",
|
||||
device,
|
||||
});
|
||||
}, [getOrCreateUserId]);
|
||||
|
||||
@@ -2,13 +2,41 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
|
||||
|
||||
function SuccessWithRedirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => router.replace("/"), 2000);
|
||||
return () => clearTimeout(t);
|
||||
}, [router]);
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
|
||||
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-emerald-50 text-5xl">
|
||||
✅
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">支付成功</h2>
|
||||
<p className="mt-3 text-slate-500">感谢您的支持,VIP 权益已开通</p>
|
||||
<p className="mt-2 text-xs text-slate-400">2 秒后自动返回首页</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-full bg-sky-500 px-8 py-3 font-semibold text-white transition-colors hover:bg-sky-600"
|
||||
>
|
||||
← 立即返回首页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PaidPage() {
|
||||
const [status, setStatus] = useState<"pending" | "success" | "fail">("pending");
|
||||
const [fromUrl, setFromUrl] = useState<string | null>(null);
|
||||
|
||||
usePayStatusPoll(() => setStatus("success"));
|
||||
const router = useRouter();
|
||||
usePayStatusPoll(() => setStatus("success"), () => router.replace("/"));
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -29,7 +57,7 @@ export default function PaidPage() {
|
||||
<p className="mt-3 text-sm text-slate-500">
|
||||
正在查询支付状态,请稍候…
|
||||
<br />
|
||||
(支付完成后将自动跳转)
|
||||
(支付完成后将自动跳转,1 分钟内未支付将返回首页)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,21 +66,7 @@ export default function PaidPage() {
|
||||
|
||||
if (status === "success") {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
|
||||
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-emerald-50 text-5xl">
|
||||
✅
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">支付成功</h2>
|
||||
<p className="mt-3 text-slate-500">感谢您的支持,VIP 权益已开通</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-full bg-sky-500 px-8 py-3 font-semibold text-white transition-colors hover:bg-sky-600"
|
||||
>
|
||||
← 返回首页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<SuccessWithRedirect />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user