's'
This commit is contained in:
@@ -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, unknown>): string {
|
||||
function buildPayHtml(
|
||||
payUrl: string,
|
||||
params: Record<string, unknown>,
|
||||
options?: { directToCashier?: boolean }
|
||||
): string {
|
||||
const flat: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v == null) continue;
|
||||
@@ -26,7 +30,10 @@ function buildPayHtml(payUrl: string, params: Record<string, unknown>): string {
|
||||
const inputs = Object.entries(flat)
|
||||
.map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}" />`)
|
||||
.join("\n");
|
||||
return `<!DOCTYPE html>
|
||||
const direct = options?.directToCashier;
|
||||
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">
|
||||
@@ -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<string, unknown> = {
|
||||
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<string, string>, payUrl: data.pay_url || "" };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
let body: Record<string, string | number>;
|
||||
@@ -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<string, unknown>);
|
||||
const html = buildPayHtml(payConfig.zpaySubmitUrl, params as Record<string, unknown>, {
|
||||
directToCashier: device === "wechat",
|
||||
});
|
||||
const res = new NextResponse(html, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user