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

249 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }
);
}
let payjsResult: PayjsVipResult | null = null;
let vip = false;
let email: string | null = null;
let linkedUserId: string | undefined;
let vipFromSite = false;
let vipFromPayments = false;
let vipSource = vip ? "payjsapi" : "none";
try {
const adminToken = await getAdminToken().catch((error) => {
console.error("[check-by-user] getAdminToken failed:", error);
return null;
});
if (!adminToken) {
payjsResult = await checkVipFromPayjsapi(request, userId);
vip = !!payjsResult?.vip;
email = payjsResult?.email ?? null;
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
);
}
// PB 未命中再请求 payjsapi避免每次都先走远端网络
if (!vip) {
payjsResult = await checkVipFromPayjsapi(request, userId);
if (payjsResult?.vip) {
vip = true;
email = payjsResult.email ?? null;
vipSource = "payjsapi";
}
}
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" });
}
}