'c1
This commit is contained in:
@@ -40,6 +40,9 @@ function getMeetupApiCandidates(request: NextRequest): string[] {
|
||||
return urls.filter((url, index) => url && urls.indexOf(url) === index);
|
||||
}
|
||||
|
||||
/** 上游 API 请求超时时间(毫秒),避免 serverless 超时导致 502 */
|
||||
const MEETUP_API_TIMEOUT_MS = 20000;
|
||||
|
||||
export async function postMeetupApi(
|
||||
request: NextRequest,
|
||||
path: string,
|
||||
@@ -54,13 +57,19 @@ export async function postMeetupApi(
|
||||
let lastError: unknown = null;
|
||||
|
||||
for (const apiUrl of apiCandidates) {
|
||||
const url = `${apiUrl}${path}`;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), MEETUP_API_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}${path}`, {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
cache: "no-store",
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
if (res.ok || (res.status !== 404 && res.status < 500)) {
|
||||
@@ -68,7 +77,15 @@ export async function postMeetupApi(
|
||||
}
|
||||
|
||||
lastResponse = { status: res.status, data };
|
||||
if (res.status >= 500) {
|
||||
console.error(`[meetup-api] ${path} ${apiUrl} returned ${res.status}`, data);
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
const errName = error instanceof Error && "name" in error ? (error as { name?: string }).name : "";
|
||||
const isTimeout = errName === "AbortError" || errMsg.toLowerCase().includes("abort") || errMsg.toLowerCase().includes("timeout");
|
||||
console.error(`[meetup-api] ${path} ${apiUrl} failed:`, isTimeout ? "timeout" : errMsg);
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { postMeetupApi } from "../_paymentApi";
|
||||
|
||||
/** Vercel serverless 超时时间(秒),避免上游慢导致 502 */
|
||||
export const maxDuration = 20;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
@@ -10,9 +13,30 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const { status, data } = await postMeetupApi(request, "/api/meetup/check-user", { email });
|
||||
|
||||
// 上游返回 5xx 时,统一为 503 并返回友好提示,避免直接透传 502
|
||||
if (status >= 500) {
|
||||
const payload = typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: (payload.error as string) || "服务暂时不可用,请稍后重试" },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(data, { status });
|
||||
} catch (error) {
|
||||
const isTimeout =
|
||||
error instanceof Error &&
|
||||
(error.name === "AbortError" || error.message.toLowerCase().includes("abort"));
|
||||
console.error("check-user error:", error);
|
||||
return NextResponse.json({ ok: false, error: "Operation failed" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: isTimeout
|
||||
? "请求超时,请确认 payjsapi 已启动(cd payjsapi && python run.py)"
|
||||
: "服务暂时不可用,请稍后重试",
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPocketBaseConfig, getPaymentConfig, SITE_ID } from "@/config/services.config";
|
||||
import {
|
||||
getApiUrlFromRequest,
|
||||
getPocketBaseConfig,
|
||||
getPaymentConfig,
|
||||
SITE_ID,
|
||||
} from "@/config/services.config";
|
||||
import { getAdminToken } from "@/app/lib/pocketbase/admin";
|
||||
import { queryXorPayOrderStatus } from "@/app/lib/payment/xorpayStatus";
|
||||
import { queryZPayOrderStatus } from "@/app/lib/payment/zpayStatus";
|
||||
|
||||
const DEFAULT_PASSWORD = "12345678";
|
||||
|
||||
@@ -26,27 +33,44 @@ function decodePendingEmail(userId: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
async function queryZpayPaid(orderId: string): Promise<boolean> {
|
||||
async function verifyOrderPaid(request: NextRequest, orderId: string): Promise<boolean> {
|
||||
const config = getPaymentConfig();
|
||||
if (!config.apiUrl) {
|
||||
console.error("queryZpayPaid: PAYMENT_API_URL not configured");
|
||||
return false;
|
||||
}
|
||||
const hostHeader =
|
||||
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
const apiUrl = (
|
||||
getApiUrlFromRequest(hostHeader) ||
|
||||
process.env.PAYMENT_API_URL ||
|
||||
config.apiUrl
|
||||
).replace(/\/$/, "");
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
const url = `${config.apiUrl}/cnomadcna/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`;
|
||||
const res = await fetch(url, { cache: "no-store", signal: controller.signal })
|
||||
.finally(() => clearTimeout(timeout));
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
const url = `${apiUrl}/cnomadcna/order_status?order_id=${encodeURIComponent(orderId)}`;
|
||||
console.log(`complete-order: check payjsapi order_id=${orderId} url=${url}`);
|
||||
const res = await fetch(url, { cache: "no-store", signal: controller.signal }).finally(() =>
|
||||
clearTimeout(timeout)
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!data?.paid) {
|
||||
console.warn(`queryZpayPaid: not paid yet, order_id=${orderId}, response=`, JSON.stringify(data));
|
||||
if (data?.paid) {
|
||||
console.log(`complete-order: payjsapi paid order_id=${orderId}`);
|
||||
return true;
|
||||
}
|
||||
return !!data?.paid;
|
||||
} catch (e) {
|
||||
console.error(`queryZpayPaid: fetch error, order_id=${orderId}`, e);
|
||||
return false;
|
||||
console.error(`complete-order: payjsapi fetch error order_id=${orderId}`, e);
|
||||
}
|
||||
|
||||
const xorpay = await queryXorPayOrderStatus(orderId, 5000);
|
||||
console.log(
|
||||
`complete-order: xorpay fallback order_id=${orderId} paid=${!!xorpay?.paid} status=${xorpay?.status || ""} error=${xorpay?.error || ""}`
|
||||
);
|
||||
if (xorpay?.paid) return true;
|
||||
|
||||
const zpay = await queryZPayOrderStatus(orderId, 5000);
|
||||
console.log(
|
||||
`complete-order: zpay fallback order_id=${orderId} paid=${!!zpay?.paid} status=${zpay?.status || ""} error=${zpay?.error || ""}`
|
||||
);
|
||||
return !!zpay?.paid;
|
||||
}
|
||||
|
||||
async function ensureSiteVip(
|
||||
@@ -145,8 +169,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 2. 没找到 → 查 ZPAY 确认是否已支付,已支付则直接创建 site_vip
|
||||
if (!found) {
|
||||
const zpayPaid = await queryZpayPaid(orderId);
|
||||
if (!zpayPaid) {
|
||||
const paid = await verifyOrderPaid(request, orderId);
|
||||
if (!paid) {
|
||||
console.error(`complete-order: ZPAY 未确认支付 order_id=${orderId}`);
|
||||
return NextResponse.json({ ok: false, error: "订单不存在或未支付成功" }, { status: 400 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user