Files
gitlab-instance-0a899031_no…/app/api/vip/check-by-user/route.ts
2026-03-26 10:57:26 -05:00

250 lines
7.2 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getApiUrlFromRequest, getPaymentConfig } from "@/config/services.config";
import {
checkPaymentsByUserId,
checkSiteVipByUserId,
findLinkedMemberFromOrder,
findVipEmailLinkByAnonymousUserId,
getAdminToken,
isAnonymousUserId,
upsertVipEmailLink,
} from "@/app/api/vip/_helpers";
type PayjsVipResult = {
vip: boolean;
email?: string;
};
async function fetchPayjsVipWithRetry(
url: string,
timeoutMs: number,
retries: number
): Promise<Response | null> {
let lastError: unknown = null;
for (let i = 0; i <= retries; i += 1) {
try {
const res = await fetch(url, {
cache: "no-store",
signal: AbortSignal.timeout(timeoutMs),
});
return res;
} catch (error) {
lastError = error;
if (i < retries) {
await new Promise((resolve) => setTimeout(resolve, 250 * (i + 1)));
}
}
}
throw lastError instanceof Error ? lastError : new Error("payjsapi fetch failed");
}
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) ||
payConfig.apiUrl
).replace(/\/$/, "");
}
async function checkVipFromPayjsapi(
request: NextRequest,
userId: string
): Promise<PayjsVipResult | null> {
try {
const apiUrl = resolvePayApiUrl(request);
const url = `${apiUrl}/nomadvip/check_vip?user_id=${encodeURIComponent(
userId
)}`;
console.log("[check-by-user] payjsapi request user_id=%s url=%s", userId, url);
const checkTimeout = Number(process.env.PAYJSAPI_CHECK_TIMEOUT) || 15000;
const retries = Math.max(0, Number(process.env.PAYJSAPI_CHECK_RETRIES) || 1);
const res = await fetchPayjsVipWithRetry(url, checkTimeout, retries);
if (!res) {
return null;
}
if (!res.ok) {
console.log(
"[check-by-user] payjsapi non-ok user_id=%s status=%s",
userId,
res.status
);
return null;
}
const data = await res.json().catch(() => ({}));
const result = {
vip: !!data?.vip,
email:
typeof data?.email === "string" && data.email.trim()
? data.email.trim().toLowerCase()
: undefined,
};
console.log(
"[check-by-user] payjsapi response user_id=%s vip=%s email=%s",
userId,
result.vip,
result.email ?? "(none)"
);
return result;
} catch (error) {
console.log(
"[check-by-user] payjsapi request failed user_id=%s error=%s",
userId,
error instanceof Error ? error.message : String(error)
);
return null;
}
}
export async function POST(request: NextRequest) {
const body = await request.json().catch(() => ({}));
const userId = String(body?.user_id ?? body?.userId ?? "").trim();
console.log("[check-by-user] request user_id=%s", userId || "(empty)");
if (!userId) {
return NextResponse.json(
{ ok: false, vip: false, error: "缺少 user_id" },
{ status: 400 }
);
}
const payjsResult = await checkVipFromPayjsapi(request, userId);
let vip = !!payjsResult?.vip;
let email = payjsResult?.email ?? null;
let linkedUserId: string | undefined;
let vipFromSite = false;
let vipFromPayments = false;
let vipSource = vip ? "payjsapi" : "none";
try {
if (vip) {
console.log(
"[check-by-user] hit payjsapi user_id=%s alreadyLinked=%s",
userId,
!!email
);
return NextResponse.json({
ok: true,
vip: true,
alreadyLinked: !!email,
email: email ?? undefined,
});
}
const adminToken = await getAdminToken().catch((error) => {
console.error("[check-by-user] getAdminToken failed:", error);
return null;
});
if (!adminToken) {
console.error("[check-by-user] missing PocketBase admin token, fallback to payjs result");
return NextResponse.json({
ok: true,
vip,
alreadyLinked: !!email,
email: email ?? undefined,
});
}
if (!vip) {
vipFromSite = await checkSiteVipByUserId(adminToken, userId);
vip = vipFromSite;
if (!vip) {
vipFromPayments = await checkPaymentsByUserId(adminToken, userId);
vip = vipFromPayments;
}
if (vipFromSite) {
vipSource = "pocketbase:site_vip";
} else if (vipFromPayments) {
vipSource = "pocketbase:payments";
}
console.log(
"[check-by-user] pb fallback user_id=%s site_vip=%s payments=%s",
userId,
vipFromSite,
vipFromPayments
);
}
if (isAnonymousUserId(userId)) {
const linkRecord = await findVipEmailLinkByAnonymousUserId(adminToken, userId);
const linkEmail =
typeof linkRecord?.email === "string"
? linkRecord.email.trim().toLowerCase()
: "";
const linkUserId =
typeof linkRecord?.user_id === "string" ? linkRecord.user_id.trim() : "";
// 匿名 user_id 本身可能已迁移到真实用户,需补查映射用户的 VIP
if (!vip && linkUserId && linkUserId !== userId) {
const linkedVip = await checkSiteVipByUserId(adminToken, linkUserId);
const linkedPaymentVip = linkedVip
? true
: await checkPaymentsByUserId(adminToken, linkUserId);
if (linkedVip || linkedPaymentVip) {
vip = true;
linkedUserId = linkUserId;
vipSource = linkedVip ? "vip_email_link:site_vip" : "vip_email_link:payments";
}
}
if (vip && linkEmail) {
email = linkEmail;
if (linkUserId && !isAnonymousUserId(linkUserId)) {
linkedUserId = linkUserId;
}
if (!vipSource || vipSource === "none") {
vipSource = "vip_email_link";
}
console.log(
"[check-by-user] linked by vip_email_link user_id=%s linkedUserId=%s email=%s",
userId,
linkedUserId || "(none)",
email
);
} else if (vip) {
const linkedMember = await findLinkedMemberFromOrder(adminToken, userId);
if (linkedMember) {
email = linkedMember.email;
linkedUserId = linkedMember.userId;
await upsertVipEmailLink(
adminToken,
linkedMember.email,
linkedMember.userId,
userId
);
vipSource = "linked_from_order";
console.log(
"[check-by-user] linked by order user_id=%s linkedUserId=%s email=%s",
userId,
linkedUserId,
email
);
}
}
}
console.log(
"[check-by-user] result user_id=%s vip=%s source=%s alreadyLinked=%s linkedUserId=%s",
userId,
vip,
vipSource,
!!email,
linkedUserId || "(none)"
);
return NextResponse.json({
ok: true,
vip,
alreadyLinked: !!email,
email: email ?? undefined,
linkedUserId,
});
} catch (error) {
console.error("[check-by-user] failed:", error);
// 接口容错:即使 PB 回退失败,也不抛 500 给前端
return NextResponse.json({ ok: true, vip: false, error: "check_failed_degraded" });
}
}