Files
2026-03-16 09:32:35 -05:00

95 lines
3.5 KiB
TypeScript

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 = 15000;
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,
options?: { timeoutMs?: number }
): Promise<{ status: number; data: unknown }> {
const candidates = getApiCandidates(request);
if (!candidates.length) throw new Error("PAYMENT_API_URL 未配置");
const timeoutMs = options?.timeoutMs ?? TIMEOUT_MS;
let lastError: unknown = null;
for (const apiUrl of candidates) {
const url = `${apiUrl}${path}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
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 请求失败");
}