's'
This commit is contained in:
@@ -37,10 +37,10 @@ function buildPayHtml(
|
|||||||
.map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}" />`)
|
.map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}" />`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const direct = options?.directToCashier;
|
const direct = options?.directToCashier;
|
||||||
// 手机/微信:无中转页,直接提交表单跳转收银台
|
// direct:微信内无中转,直接提交 | 非 direct:PC/H5 有中转页提示
|
||||||
return direct
|
return direct
|
||||||
? `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>支付</title><style>body{margin:0;overflow:hidden}</style></head><body><form id="f" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">${inputs}</form><script>document.getElementById("f").submit()</script></body></html>`
|
? `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>支付</title><style>body{margin:0;overflow:hidden}</style></head><body><form id="f" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">${inputs}</form><script>document.getElementById("f").submit()</script></body></html>`
|
||||||
: `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>支付</title><style>body{margin:0;overflow:hidden}</style></head><body><form id="zpayForm" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">${inputs}</form><script>document.getElementById("zpayForm").submit();</script></body></html>`;
|
: `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>跳转支付</title><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f0f4f8;}p{color:#64748b;}</style></head><body><p>正在跳转到支付页面...</p><form id="zpayForm" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">${inputs}</form><script>document.getElementById("zpayForm").submit();</script></body></html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPayOrderCookie(res: NextResponse, orderId: string) {
|
function setPayOrderCookie(res: NextResponse, orderId: string) {
|
||||||
@@ -54,7 +54,8 @@ async function createPayOrder(
|
|||||||
total_fee: number,
|
total_fee: number,
|
||||||
type: string,
|
type: string,
|
||||||
channel: string,
|
channel: string,
|
||||||
clientIp?: string
|
clientIp?: string,
|
||||||
|
provider?: string
|
||||||
) {
|
) {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
user_id,
|
user_id,
|
||||||
@@ -63,6 +64,7 @@ async function createPayOrder(
|
|||||||
channel,
|
channel,
|
||||||
return_url,
|
return_url,
|
||||||
...(clientIp && { client_ip: clientIp }),
|
...(clientIp && { client_ip: clientIp }),
|
||||||
|
...(provider && { provider }),
|
||||||
};
|
};
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||||
@@ -116,6 +118,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const type = String(body?.type || "vip");
|
const type = String(body?.type || "vip");
|
||||||
const channel = String(body?.channel || "wxpay");
|
const channel = String(body?.channel || "wxpay");
|
||||||
const device = String(body?.device || "pc");
|
const device = String(body?.device || "pc");
|
||||||
|
const reqProvider = String(body?.provider || "").trim().toLowerCase() || undefined;
|
||||||
const accept = request.headers.get("accept") || "";
|
const accept = request.headers.get("accept") || "";
|
||||||
const wantHtml = String(body?.html) === "1" || accept.includes("text/html");
|
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";
|
: request.headers.get("origin") || "http://localhost:3000";
|
||||||
const finalReturnUrl = return_url || `${origin}/paid`;
|
const finalReturnUrl = return_url || `${origin}/paid`;
|
||||||
|
|
||||||
if (!wantHtml) {
|
|
||||||
return NextResponse.json({ ok: false, error: "请传 html=1" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientIp =
|
const clientIp =
|
||||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||||
request.headers.get("x-real-ip") ||
|
request.headers.get("x-real-ip") ||
|
||||||
@@ -151,7 +150,8 @@ export async function POST(request: NextRequest) {
|
|||||||
total_fee,
|
total_fee,
|
||||||
type,
|
type,
|
||||||
channel,
|
channel,
|
||||||
clientIp
|
clientIp,
|
||||||
|
reqProvider
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!params || Object.keys(params).length === 0) {
|
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 });
|
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 中转
|
// xorpay:直接 payh5 返回收银台参数,无需 redirect 中转
|
||||||
const submitUrl = provider === "xorpay" ? payUrl : payConfig.zpaySubmitUrl;
|
const submitUrl = provider === "xorpay" ? payUrl : payConfig.zpaySubmitUrl;
|
||||||
if (provider === "xorpay") {
|
if (provider === "xorpay") {
|
||||||
@@ -187,6 +201,7 @@ export async function POST(request: NextRequest) {
|
|||||||
channel,
|
channel,
|
||||||
return_url: finalReturnUrl,
|
return_url: finalReturnUrl,
|
||||||
device,
|
device,
|
||||||
|
provider: "zpay",
|
||||||
...(clientIp && { client_ip: clientIp }),
|
...(clientIp && { client_ip: clientIp }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
59
app/page.tsx
59
app/page.tsx
@@ -13,7 +13,8 @@ import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
|
|||||||
const PB_URL =
|
const PB_URL =
|
||||||
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
|
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;
|
user_id: string;
|
||||||
return_url: string;
|
return_url: string;
|
||||||
total_fee: number;
|
total_fee: number;
|
||||||
@@ -33,6 +34,7 @@ function redirectToPay(params: {
|
|||||||
["type", params.type],
|
["type", params.type],
|
||||||
["channel", params.channel],
|
["channel", params.channel],
|
||||||
["device", params.device],
|
["device", params.device],
|
||||||
|
["provider", "zpay"],
|
||||||
["html", "1"],
|
["html", "1"],
|
||||||
];
|
];
|
||||||
fields.forEach(([k, v]) => {
|
fields.forEach(([k, v]) => {
|
||||||
@@ -46,6 +48,47 @@ function redirectToPay(params: {
|
|||||||
form.submit();
|
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<string, string>)) {
|
||||||
|
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 {
|
function getStorage(key: string): string | null {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
return localStorage.getItem(key);
|
return localStorage.getItem(key);
|
||||||
@@ -162,14 +205,12 @@ export default function Home() {
|
|||||||
const env = getPayEnv();
|
const env = getPayEnv();
|
||||||
const device = getDeviceFromEnv(env);
|
const device = getDeviceFromEnv(env);
|
||||||
const totalFee = getPaymentConfig().defaultAmount ?? 880;
|
const totalFee = getPaymentConfig().defaultAmount ?? 880;
|
||||||
redirectToPay({
|
const base = { user_id: userId, return_url: returnUrl, total_fee: totalFee, type: "vip", channel: "wxpay" as const };
|
||||||
user_id: userId,
|
if (device === "wechat") {
|
||||||
return_url: returnUrl,
|
payWechatXorpay(base);
|
||||||
total_fee: totalFee,
|
} else {
|
||||||
type: "vip",
|
redirectToPayZpay({ ...base, device });
|
||||||
channel: "wxpay",
|
}
|
||||||
device,
|
|
||||||
});
|
|
||||||
}, [getOrCreateUserId]);
|
}, [getOrCreateUserId]);
|
||||||
|
|
||||||
const handleCopyUsername = useCallback(() => {
|
const handleCopyUsername = useCallback(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user