'
This commit is contained in:
@@ -16,6 +16,30 @@ type PayjsVipResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const inflightUserChecks = new Map<string, Promise<PayjsVipResult | null>>();
|
const inflightUserChecks = new Map<string, Promise<PayjsVipResult | null>>();
|
||||||
|
const inflightUserRequests = new Map<
|
||||||
|
string,
|
||||||
|
Promise<{
|
||||||
|
ok: true;
|
||||||
|
vip: boolean;
|
||||||
|
alreadyLinked: boolean;
|
||||||
|
email?: string;
|
||||||
|
linkedUserId?: string;
|
||||||
|
}>
|
||||||
|
>();
|
||||||
|
const userCheckResultCache = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
expiresAt: number;
|
||||||
|
payload: {
|
||||||
|
ok: true;
|
||||||
|
vip: boolean;
|
||||||
|
alreadyLinked: boolean;
|
||||||
|
email?: string;
|
||||||
|
linkedUserId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
const USER_CHECK_CACHE_TTL_MS = Number(process.env.CHECK_BY_USER_CACHE_TTL_MS) || 10_000;
|
||||||
|
|
||||||
async function fetchPayjsVipWithRetry(
|
async function fetchPayjsVipWithRetry(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -126,139 +150,167 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let payjsResult: PayjsVipResult | null = null;
|
const cached = userCheckResultCache.get(userId);
|
||||||
let vip = false;
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
let email: string | null = null;
|
return NextResponse.json(cached.payload);
|
||||||
let linkedUserId: string | undefined;
|
}
|
||||||
let vipFromSite = false;
|
if (cached) {
|
||||||
let vipFromPayments = false;
|
userCheckResultCache.delete(userId);
|
||||||
let vipSource = vip ? "payjsapi" : "none";
|
}
|
||||||
|
|
||||||
try {
|
const inflight = inflightUserRequests.get(userId);
|
||||||
const adminToken = await getAdminToken().catch((error) => {
|
if (inflight) {
|
||||||
console.error("[check-by-user] getAdminToken failed:", error);
|
const payload = await inflight;
|
||||||
return null;
|
return NextResponse.json(payload);
|
||||||
});
|
}
|
||||||
if (!adminToken) {
|
|
||||||
payjsResult = await checkVipFromPayjsapi(request, userId);
|
const task = (async () => {
|
||||||
vip = !!payjsResult?.vip;
|
let payjsResult: PayjsVipResult | null = null;
|
||||||
email = payjsResult?.email ?? null;
|
let vip = false;
|
||||||
console.error("[check-by-user] missing PocketBase admin token, fallback to payjs result");
|
let email: string | null = null;
|
||||||
return NextResponse.json({
|
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 {
|
||||||
|
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 {
|
||||||
ok: true,
|
ok: true,
|
||||||
vip,
|
vip,
|
||||||
alreadyLinked: !!email,
|
alreadyLinked: !!email,
|
||||||
email: email ?? undefined,
|
email: email ?? undefined,
|
||||||
});
|
linkedUserId,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[check-by-user] failed:", error);
|
||||||
|
// 接口容错:即使 PB 回退失败,也不抛 500 给前端
|
||||||
|
return { ok: true, vip: false, alreadyLinked: false };
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
if (!vip) {
|
inflightUserRequests.set(userId, task);
|
||||||
vipFromSite = await checkSiteVipByUserId(adminToken, userId);
|
try {
|
||||||
vip = vipFromSite;
|
const payload = await task;
|
||||||
if (!vip) {
|
userCheckResultCache.set(userId, {
|
||||||
vipFromPayments = await checkPaymentsByUserId(adminToken, userId);
|
payload,
|
||||||
vip = vipFromPayments;
|
expiresAt: Date.now() + USER_CHECK_CACHE_TTL_MS,
|
||||||
}
|
|
||||||
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) {
|
return NextResponse.json(payload);
|
||||||
console.error("[check-by-user] failed:", error);
|
} finally {
|
||||||
// 接口容错:即使 PB 回退失败,也不抛 500 给前端
|
inflightUserRequests.delete(userId);
|
||||||
return NextResponse.json({ ok: true, vip: false, error: "check_failed_degraded" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user