This commit is contained in:
eric
2026-03-13 03:43:50 -05:00
parent e5d080c202
commit 857380aa01
29 changed files with 3595 additions and 580 deletions

View File

@@ -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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] || c);
const returnUrl = String(resData.return_url || finalReturnUrl).replace(
/[&<>"']/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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}`;