import { NextRequest } from "next/server"; import { getPaymentConfig } from "@/config/services.config"; function isPrivateHost(hostname: string): boolean { if (!hostname) return false; if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return true; if (/^10\./.test(hostname) || /^192\.168\./.test(hostname)) return true; const match = hostname.match(/^172\.(\d{1,2})\./); if (!match) return false; return Number(match[1]) >= 16 && Number(match[1]) <= 31; } function getRequestHostname(request: NextRequest): string { const host = request.headers.get("x-forwarded-host") || request.headers.get("host") || ""; return host.split(",")[0].trim().split(":")[0]; } /** 与 cnomadcna 一致:多 URL 回退,开发/内网优先本地 */ function getApiCandidates(request: NextRequest): string[] { const config = getPaymentConfig(); const hostname = getRequestHostname(request); const port = process.env.PAYJSAPI_PORT || "8007"; const localUrl = `http://${hostname || "127.0.0.1"}:${port}`; const configuredUrl = config.apiUrl.replace(/\/$/, ""); const preferLocal = process.env.NODE_ENV === "development" || isPrivateHost(hostname); const urls = preferLocal ? [localUrl, configuredUrl] : [configuredUrl, localUrl]; return urls.filter((u, i) => u && urls.indexOf(u) === i); } const TIMEOUT_MS = 20000; export async function postSalonApi( request: NextRequest, path: string, body: unknown ): Promise<{ status: number; data: unknown }> { const candidates = getApiCandidates(request); if (!candidates.length) throw new Error("PAYMENT_API_URL 未配置"); let lastError: unknown = null; for (const apiUrl of candidates) { const url = `${apiUrl}${path}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); try { 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)) { return { status: res.status, data }; } lastError = data?.detail || data?.error || `HTTP ${res.status}`; } catch (error) { clearTimeout(timeoutId); lastError = error; } } throw lastError instanceof Error ? lastError : new Error("payjsapi 请求失败"); } export async function getSalonApi( request: NextRequest, path: string ): Promise<{ status: number; data: unknown }> { const candidates = getApiCandidates(request); if (!candidates.length) throw new Error("PAYMENT_API_URL 未配置"); let lastError: unknown = null; for (const apiUrl of candidates) { const url = `${apiUrl}${path}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); try { const res = await fetch(url, { cache: "no-store", signal: controller.signal }); clearTimeout(timeoutId); const data = await res.json().catch(() => ({})); if (res.ok) return { status: res.status, data }; lastError = data?.detail || data?.error || `HTTP ${res.status}`; } catch (error) { clearTimeout(timeoutId); lastError = error; } } throw lastError instanceof Error ? lastError : new Error("payjsapi 请求失败"); }