's'
This commit is contained in:
253
app/api/pay/confirm/route.ts
Normal file
253
app/api/pay/confirm/route.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 支付确认:payjsapi 未写入时,由 nomadvip 直接写入 PocketBase + 创建 Memos
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
getApiUrlFromRequest,
|
||||
getPocketBaseConfig,
|
||||
getPaymentConfig,
|
||||
SITE_ID,
|
||||
} from "@/config/services.config";
|
||||
import { queryXorPayOrderStatus } from "@/app/lib/xorpayStatus";
|
||||
import { queryZPayOrderStatus } from "@/app/lib/zpayStatus";
|
||||
|
||||
const getTotalFee = () =>
|
||||
getPaymentConfig().defaultAmount ?? Number(process.env.PAYMENT_DEFAULT_AMOUNT) ?? 8800;
|
||||
|
||||
async function getAdminToken(): Promise<string | null> {
|
||||
const email = process.env.POCKETBASE_EMAIL;
|
||||
const password = process.env.POCKETBASE_PASSWORD;
|
||||
if (!email || !password) return null;
|
||||
const pb = getPocketBaseConfig();
|
||||
const paths = ["/api/admins/auth-with-password", "/api/collections/_superusers/auth-with-password"];
|
||||
for (const path of paths) {
|
||||
const res = await fetch(`${pb.url}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: email, password }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data?.token) return data.token;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePayApiUrl(request: NextRequest): string {
|
||||
const payConfig = getPaymentConfig();
|
||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
return (
|
||||
(hostHeader ? getApiUrlFromRequest(hostHeader) : null) ||
|
||||
process.env.PAYMENT_API_URL ||
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
payConfig.apiUrl
|
||||
).replace(/\/$/, "");
|
||||
}
|
||||
|
||||
async function verifyOrderPaid(
|
||||
request: NextRequest,
|
||||
orderId: string
|
||||
): Promise<boolean> {
|
||||
const apiUrl = resolvePayApiUrl(request);
|
||||
const url = `${apiUrl}/nomadvip/order_status?order_id=${encodeURIComponent(orderId)}`;
|
||||
console.log("[pay/confirm] verify order_status url=%s", url);
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
const res = await fetch(url, {
|
||||
cache: "no-store",
|
||||
signal: controller.signal,
|
||||
}).finally(() => clearTimeout(timeout));
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const paid = !!data?.paid;
|
||||
if (!paid && !res.ok) {
|
||||
console.warn("[pay/confirm] order_status non-ok:", res.status, url, data);
|
||||
}
|
||||
if (paid) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[pay/confirm] verifyOrderPaid fetch error:", e);
|
||||
}
|
||||
|
||||
const xorpay = await queryXorPayOrderStatus(orderId, 5000);
|
||||
console.log(
|
||||
"[pay/confirm] xorpay fallback order_id=%s paid=%s status=%s error=%s url=%s",
|
||||
orderId,
|
||||
!!xorpay?.paid,
|
||||
xorpay?.status || "",
|
||||
xorpay?.error || "",
|
||||
xorpay?.url || ""
|
||||
);
|
||||
if (xorpay?.paid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const zpay = await queryZPayOrderStatus(orderId, 5000);
|
||||
console.log(
|
||||
"[pay/confirm] zpay fallback order_id=%s paid=%s status=%s error=%s url=%s",
|
||||
orderId,
|
||||
!!zpay?.paid,
|
||||
zpay?.status || "",
|
||||
zpay?.error || "",
|
||||
zpay?.url || ""
|
||||
);
|
||||
return !!zpay?.paid;
|
||||
}
|
||||
|
||||
async function createMemosUser(username: string, password: string): Promise<boolean> {
|
||||
const baseUrl = process.env.MEMOS_API_URL || "https://qun.hackrobot.cn";
|
||||
const token = process.env.MEMOS_ACCESS_TOKEN;
|
||||
if (!token) return false;
|
||||
const url = `${baseUrl.replace(/\/$/, "")}/api/v1/users`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
role: "USER",
|
||||
state: "NORMAL",
|
||||
}),
|
||||
});
|
||||
if (res.ok) return true;
|
||||
const err = await res.json().catch(() => ({}));
|
||||
const alreadyExists =
|
||||
err?.code === 6 ||
|
||||
err?.code === 13 ||
|
||||
err?.message?.includes("already exists") ||
|
||||
err?.message?.includes("UNIQUE") ||
|
||||
err?.message?.includes("constraint");
|
||||
if (alreadyExists) return true;
|
||||
console.error("Memos create user failed:", res.status, err);
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error("Memos create user error:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const orderId = String(body?.order_id ?? body?.orderId ?? "").trim();
|
||||
const userId = String(body?.user_id ?? body?.userId ?? "").trim();
|
||||
|
||||
console.log("[pay/confirm] 请求 order_id=%s user_id=%s", orderId || "(空)", userId || "(空)");
|
||||
|
||||
if (!orderId || !userId) {
|
||||
return NextResponse.json({ ok: false, error: "缺少 order_id 或 user_id" }, { status: 400 });
|
||||
}
|
||||
|
||||
const paid = await verifyOrderPaid(request, orderId);
|
||||
if (!paid) {
|
||||
console.error("[pay/confirm] 订单未支付 order_id=%s user_id=%s (请检查 payjsapi order_status 接口)", orderId, userId);
|
||||
return NextResponse.json({ ok: false, error: "订单未支付" }, { status: 400 });
|
||||
}
|
||||
|
||||
const adminToken = await getAdminToken();
|
||||
if (!adminToken) {
|
||||
return NextResponse.json({ ok: false, error: "PocketBase 未配置" }, { status: 500 });
|
||||
}
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const totalFee = getTotalFee();
|
||||
|
||||
const checkExisting = await fetch(
|
||||
`${pb.url}/api/collections/payments/records?filter=${encodeURIComponent(`order_id="${orderId}"`)}&perPage=1`,
|
||||
{ headers: { Authorization: `Bearer ${adminToken}` } }
|
||||
);
|
||||
const existingData = await checkExisting.json().catch(() => ({}));
|
||||
const paymentsExist = (existingData?.items?.length ?? 0) > 0;
|
||||
|
||||
if (!paymentsExist) {
|
||||
const paymentsRes = await fetch(`${pb.url}/api/collections/payments/records`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
type: "vip",
|
||||
order_id: orderId,
|
||||
total_fee: totalFee,
|
||||
amount: totalFee,
|
||||
channel: "wxpay",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!paymentsRes.ok) {
|
||||
const err = await paymentsRes.json().catch(() => ({}));
|
||||
if (err?.data?.order_id?.message?.includes("unique") || err?.message?.includes("unique")) {
|
||||
// 已存在(并发),继续
|
||||
} else {
|
||||
const msg = err?.message || err?.data ? JSON.stringify(err) : await paymentsRes.text();
|
||||
console.error("[pay/confirm] payments create failed:", paymentsRes.status, msg);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "写入 payments 失败", detail: msg },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const siteVipCheck = await fetch(
|
||||
`${pb.url}/api/collections/site_vip/records?filter=${encodeURIComponent(`user_id="${userId}" && site_id="${SITE_ID}"`)}&perPage=1`,
|
||||
{ headers: { Authorization: `Bearer ${adminToken}` } }
|
||||
);
|
||||
const siteVipData = await siteVipCheck.json().catch(() => ({}));
|
||||
const siteVipItems = siteVipData?.items ?? [];
|
||||
|
||||
if (siteVipItems.length > 0) {
|
||||
await fetch(`${pb.url}/api/collections/site_vip/records/${siteVipItems[0].id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body: JSON.stringify({ order_id: orderId }),
|
||||
});
|
||||
} else {
|
||||
const siteVipRes = await fetch(`${pb.url}/api/collections/site_vip/records`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
site_id: SITE_ID,
|
||||
order_id: orderId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!siteVipRes.ok) {
|
||||
const err = await siteVipRes.json().catch(() => ({}));
|
||||
if (err?.data?.order_id?.message?.includes("unique") || err?.message?.includes("unique")) {
|
||||
// 已存在,继续
|
||||
} else {
|
||||
console.error("site_vip create failed:", await siteVipRes.text());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userId.startsWith("user") && /^user\d+$/.test(userId)) {
|
||||
await createMemosUser(userId, "12345678");
|
||||
}
|
||||
|
||||
console.log("[pay/confirm] 成功 order_id=%s user_id=%s", orderId, userId);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (e) {
|
||||
console.error("pay/confirm error:", e);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: e instanceof Error ? e.message : "确认失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,11 @@
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPaymentConfig, getApiUrlFromRequest } from "@/config/services.config";
|
||||
import { getCookieOptions, getHostFromRequest } from "@/app/lib/auth-cookie";
|
||||
|
||||
const ZPAY_REQUIRED = ["pid", "type", "out_trade_no", "notify_url", "return_url", "name", "money", "sign", "sign_type"];
|
||||
const ANONYMOUS_USER_COOKIE_NAME = "nomadvip_anon_uid";
|
||||
const ANONYMOUS_USER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
||||
|
||||
function escapeAttr(s: string): string {
|
||||
return String(s)
|
||||
@@ -47,6 +50,25 @@ function setPayOrderCookie(res: NextResponse, orderId: string) {
|
||||
res.cookies.set("nomadvip_pay_order", `${orderId}|${Date.now()}`, { path: "/", maxAge: 900 });
|
||||
}
|
||||
|
||||
function setAnonymousUserCookie(
|
||||
res: NextResponse,
|
||||
request: NextRequest,
|
||||
userId: string
|
||||
) {
|
||||
if (!/^user\d+$/.test(userId)) {
|
||||
return;
|
||||
}
|
||||
const options = {
|
||||
...(getCookieOptions(false, getHostFromRequest(request)) as Record<
|
||||
string,
|
||||
string | number | boolean
|
||||
>),
|
||||
httpOnly: false,
|
||||
maxAge: ANONYMOUS_USER_COOKIE_MAX_AGE,
|
||||
};
|
||||
res.cookies.set(ANONYMOUS_USER_COOKIE_NAME, userId, options);
|
||||
}
|
||||
|
||||
async function createPayOrder(
|
||||
apiUrl: string,
|
||||
user_id: string,
|
||||
@@ -67,15 +89,18 @@ async function createPayOrder(
|
||||
...(clientIp && { client_ip: clientIp }),
|
||||
...(provider && { provider }),
|
||||
};
|
||||
const payh5Url = `${apiUrl}/nomadvip/payh5`;
|
||||
console.log("[pay] createPayOrder 请求 payjsapi url=%s", payh5Url);
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 30000);
|
||||
const res = await fetch(`${apiUrl}/nomadvip/payh5`, {
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
const res = await fetch(payh5Url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
}).finally(() => clearTimeout(timeout));
|
||||
const data = await res.json().catch(() => ({}));
|
||||
console.log("[pay] createPayOrder 响应 status=%s ok=%s", res.status, data?.status);
|
||||
if (!res.ok || data?.status !== "ok") {
|
||||
throw new Error(data?.detail || data?.msg || "支付创建失败");
|
||||
}
|
||||
@@ -105,7 +130,15 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const payConfig = getPaymentConfig();
|
||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
const apiUrl = (getApiUrlFromRequest(hostHeader) || payConfig.apiUrl).replace(/\/$/, "");
|
||||
// 优先用请求 Host(等同 window.location.host)推断同机 payjsapi,访问 192.168.41.222:3002 即用 192.168.41.222:8007
|
||||
const apiUrl = (
|
||||
(hostHeader ? getApiUrlFromRequest(hostHeader) : null) ||
|
||||
process.env.PAYMENT_API_URL ||
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
payConfig.apiUrl
|
||||
)
|
||||
.replace(/\/$/, "");
|
||||
console.log("[pay] 立即加入 apiUrl=%s host=%s", apiUrl, hostHeader);
|
||||
let user_id = String(body?.user_id || "").trim();
|
||||
if (user_id.startsWith("{") && user_id.includes("data")) {
|
||||
try {
|
||||
@@ -178,6 +211,7 @@ export async function POST(request: NextRequest) {
|
||||
order_id: orderId,
|
||||
});
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
setAnonymousUserCookie(res, request, user_id);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -193,6 +227,7 @@ export async function POST(request: NextRequest) {
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
});
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
setAnonymousUserCookie(res, request, user_id);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -210,7 +245,7 @@ export async function POST(request: NextRequest) {
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 45000);
|
||||
const timeout = setTimeout(() => controller.abort(), 20000);
|
||||
const redirectRes = await fetch(`${apiUrl}/nomadvip/payh5/redirect`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -224,7 +259,7 @@ export async function POST(request: NextRequest) {
|
||||
if (location) {
|
||||
const orderId =
|
||||
redirectRes.headers.get("x-pay-order-id")?.trim() ||
|
||||
String(params?.out_trade_no || "").trim();
|
||||
String(params?.order_id || params?.out_trade_no || "").trim();
|
||||
if (preferJson && orderId) {
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
@@ -235,6 +270,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
const res = NextResponse.redirect(location);
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
setAnonymousUserCookie(res, request, user_id);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -251,10 +287,12 @@ export async function POST(request: NextRequest) {
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
});
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
setAnonymousUserCookie(res, request, user_id);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (redirectRes.status === 200 && resData?.qrcode_mode && resData?.img) {
|
||||
const userId = String(user_id || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
|
||||
const returnUrl = String(resData.return_url || finalReturnUrl).replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c
|
||||
@@ -337,6 +375,7 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
(function(){
|
||||
var orderId="${orderId}";
|
||||
var returnUrl="${returnUrl}";
|
||||
var userId="${userId}";
|
||||
if(!orderId){return;}
|
||||
var waitWrap=document.getElementById("waitWrap");
|
||||
var successWrap=document.getElementById("successWrap");
|
||||
@@ -362,26 +401,39 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
clearInterval(countdownTimer);
|
||||
waitWrap.classList.add("hide");
|
||||
successWrap.classList.add("show");
|
||||
var s=10;
|
||||
var s=2;
|
||||
var t=setInterval(function(){
|
||||
s--;
|
||||
successCountdown.textContent=s+" 秒后返回首页";
|
||||
successCountdown.textContent=s>0?s+" 秒后返回首页":"返回首页中…";
|
||||
if(s<=0){
|
||||
clearInterval(t);
|
||||
window.location.href=returnUrl;
|
||||
try{localStorage.setItem("userId",userId);if(/^user\\d+$/.test(userId))localStorage.setItem("memosAccount",userId);localStorage.setItem("paidType","vip");if(!localStorage.getItem("emailLinked")&&!localStorage.getItem("userEmail"))localStorage.setItem("userName",userId);}catch(e){}
|
||||
window.location.href="/";
|
||||
}
|
||||
},1000);
|
||||
}
|
||||
function doConfirm(attempt){
|
||||
if(!userId||attempt>=3){showSuccess();return;}
|
||||
fetch("/api/pay/confirm",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({order_id:orderId,user_id:userId})})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(cd){
|
||||
if(cd&&cd.ok){showSuccess();}
|
||||
else{setTimeout(function(){doConfirm(attempt+1);},800);}
|
||||
})
|
||||
.catch(function(){setTimeout(function(){doConfirm(attempt+1);},800);});
|
||||
}
|
||||
function check(){
|
||||
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId))
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(d){
|
||||
if(d.ok&&d.paid){showSuccess();return;}
|
||||
setTimeout(check,1000);
|
||||
if(d.ok&&d.paid){
|
||||
doConfirm(0);return;
|
||||
}
|
||||
setTimeout(check,500);
|
||||
})
|
||||
.catch(function(){setTimeout(check,1000);});
|
||||
.catch(function(){setTimeout(check,500);});
|
||||
}
|
||||
setTimeout(check,1000);
|
||||
setTimeout(check,500);
|
||||
})();
|
||||
</script>
|
||||
</body></html>`;
|
||||
@@ -390,6 +442,7 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
});
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
setAnonymousUserCookie(res, request, user_id);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -415,11 +468,12 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
const msg = err.message || "";
|
||||
const payjsapiHint = "请确认 payjsapi 已启动 (npm run dev 或 python run.py,端口 8007)";
|
||||
const hint =
|
||||
msg.includes("fetch") || msg.includes("ECONNREFUSED")
|
||||
? "请确认 payjsapi 已启动"
|
||||
? payjsapiHint
|
||||
: err.name === "AbortError"
|
||||
? "请求超时"
|
||||
? `请求超时,${payjsapiHint}`
|
||||
: msg;
|
||||
console.error("Pay API error:", err);
|
||||
const errorMsg = `支付请求失败: ${hint}`;
|
||||
|
||||
@@ -1,24 +1,80 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPaymentConfig } from "@/config/services.config";
|
||||
import { getApiUrlFromRequest, getPaymentConfig } from "@/config/services.config";
|
||||
import { queryXorPayOrderStatus } from "@/app/lib/xorpayStatus";
|
||||
import { queryZPayOrderStatus } from "@/app/lib/zpayStatus";
|
||||
|
||||
function resolvePayApiUrl(request: NextRequest): string {
|
||||
const payConfig = getPaymentConfig();
|
||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
return (
|
||||
(hostHeader ? getApiUrlFromRequest(hostHeader) : null) ||
|
||||
process.env.PAYMENT_API_URL ||
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
payConfig.apiUrl
|
||||
).replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const orderId = request.nextUrl.searchParams.get("order_id");
|
||||
if (!orderId) {
|
||||
return NextResponse.json({ ok: false, error: "缺少 order_id" }, { status: 400 });
|
||||
return NextResponse.json({ ok: false, error: "missing order_id" }, { status: 400 });
|
||||
}
|
||||
const payConfig = getPaymentConfig();
|
||||
const apiUrl = payConfig.apiUrl.replace(/\/$/, "");
|
||||
|
||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
const apiUrl = resolvePayApiUrl(request);
|
||||
const url = `${apiUrl}/nomadvip/order_status?order_id=${encodeURIComponent(orderId)}`;
|
||||
console.log("[pay/status] request order_id=%s host=%s url=%s", orderId, hostHeader, url);
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 25000);
|
||||
// 使用 order_status 通用接口(xorpay 查 PocketBase,zpay 查 ZPAY)
|
||||
const res = await fetch(
|
||||
`${apiUrl}/nomadvip/order_status?order_id=${encodeURIComponent(orderId)}`,
|
||||
{ cache: "no-store", signal: controller.signal }
|
||||
).finally(() => clearTimeout(timeout));
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
const res = await fetch(url, {
|
||||
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: true, paid: false });
|
||||
const paid = !!data?.paid;
|
||||
console.log(
|
||||
"[pay/status] result order_id=%s paid=%s status=%s",
|
||||
orderId,
|
||||
paid,
|
||||
res.status
|
||||
);
|
||||
if (paid) {
|
||||
return NextResponse.json({ ok: true, paid: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"[pay/status] error order_id=%s error=%s",
|
||||
orderId,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
|
||||
const xorpay = await queryXorPayOrderStatus(orderId, 5000);
|
||||
console.log(
|
||||
"[pay/status] xorpay fallback order_id=%s paid=%s status=%s error=%s url=%s",
|
||||
orderId,
|
||||
!!xorpay?.paid,
|
||||
xorpay?.status || "",
|
||||
xorpay?.error || "",
|
||||
xorpay?.url || ""
|
||||
);
|
||||
if (xorpay?.paid) {
|
||||
return NextResponse.json({ ok: true, paid: true });
|
||||
}
|
||||
|
||||
const zpay = await queryZPayOrderStatus(orderId, 5000);
|
||||
console.log(
|
||||
"[pay/status] zpay fallback order_id=%s paid=%s status=%s error=%s url=%s",
|
||||
orderId,
|
||||
!!zpay?.paid,
|
||||
zpay?.status || "",
|
||||
zpay?.error || "",
|
||||
zpay?.url || ""
|
||||
);
|
||||
if (zpay?.paid) {
|
||||
return NextResponse.json({ ok: true, paid: true });
|
||||
}
|
||||
return NextResponse.json({ ok: true, paid: false });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user