This commit is contained in:
eric
2026-03-26 11:22:55 -05:00
parent 17768ba033
commit 8c865cf0e8
4 changed files with 89 additions and 55 deletions

View File

@@ -17,7 +17,7 @@ NEXT_PUBLIC_API_URL=http://127.0.0.1:8007
# PAYMENT_API_INTERNAL_URL=http://127.0.0.1:8007
# 内网/慢网络可适当提高超时毫秒check_vip 默认 15spayh5/redirect 默认 60s
# PAYJSAPI_CHECK_TIMEOUT=15000
# PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT=15000
# PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT=20000
# PAYJSAPI_CHECK_BY_EMAIL_RETRIES=1
# PAYJSAPI_PAYH5_TIMEOUT=60000
# PAYJSAPI_REDIRECT_TIMEOUT=60000

View File

@@ -1,6 +1,10 @@
import { SITE_ID, getPocketBaseConfig } from "@/config/services.config";
const VIP_EMAIL_LINK_COLLECTION = "vip_email_link";
const PB_FETCH_TIMEOUT = Number(process.env.POCKETBASE_FETCH_TIMEOUT) || 10000;
const PB_ADMIN_TOKEN_TTL_MS = Number(process.env.POCKETBASE_ADMIN_TOKEN_TTL_MS) || 55_000;
let adminTokenCache: { token: string; expiresAt: number } | null = null;
type PBRecord = Record<string, unknown>;
@@ -22,6 +26,11 @@ function uniqueStrings(values: Array<string | null | undefined>): string[] {
);
}
async function pbFetch(input: string, init: RequestInit = {}): Promise<Response> {
const signal = init.signal ?? AbortSignal.timeout(PB_FETCH_TIMEOUT);
return fetch(input, { ...init, signal });
}
async function listRecords(
collection: string,
filter: string,
@@ -35,7 +44,7 @@ async function listRecords(
url.searchParams.set("sort", options.sort);
}
const res = await fetch(url.toString(), {
const res = await pbFetch(url.toString(), {
headers: { Authorization: `Bearer ${adminToken}` },
cache: "no-store",
});
@@ -52,7 +61,7 @@ async function createRecord(
adminToken: string,
body: Record<string, unknown>
): Promise<void> {
await fetch(`${getPBBaseUrl()}/api/collections/${collection}/records`, {
await pbFetch(`${getPBBaseUrl()}/api/collections/${collection}/records`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -69,7 +78,7 @@ async function patchRecord(
adminToken: string,
body: Record<string, unknown>
): Promise<void> {
await fetch(
await pbFetch(
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
{
method: "PATCH",
@@ -88,7 +97,7 @@ async function deleteRecord(
recordId: string,
adminToken: string
): Promise<void> {
await fetch(
await pbFetch(
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
{
method: "DELETE",
@@ -103,7 +112,7 @@ async function getRecord(
recordId: string,
adminToken: string
): Promise<PBRecord | null> {
const res = await fetch(
const res = await pbFetch(
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
{
headers: { Authorization: `Bearer ${adminToken}` },
@@ -139,6 +148,10 @@ export async function getAdminToken(): Promise<string | null> {
return null;
}
if (adminTokenCache && adminTokenCache.expiresAt > Date.now()) {
return adminTokenCache.token;
}
const baseUrl = getPBBaseUrl();
const paths = [
"/api/admins/auth-with-password",
@@ -146,7 +159,7 @@ export async function getAdminToken(): Promise<string | null> {
];
for (const path of paths) {
const res = await fetch(`${baseUrl}${path}`, {
const res = await pbFetch(`${baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identity: email, password }),
@@ -158,6 +171,10 @@ export async function getAdminToken(): Promise<string | null> {
const data = await res.json().catch(() => ({}));
if (typeof data?.token === "string" && data.token) {
adminTokenCache = {
token: data.token,
expiresAt: Date.now() + PB_ADMIN_TOKEN_TTL_MS,
};
return data.token;
}
}

View File

@@ -36,7 +36,7 @@ type RecoveryResult = {
};
const PAYJS_CHECK_BY_EMAIL_TIMEOUT =
Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT) || 5000;
Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT) || 20000;
const PAYJS_CHECK_BY_EMAIL_RETRIES =
Math.max(0, Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_RETRIES) || 1);
@@ -49,6 +49,13 @@ function resolvePayApiUrl(request: NextRequest): string {
).replace(/\/$/, "");
}
function getLocalPayApiUrl(): string {
return (
process.env.PAYMENT_API_INTERNAL_URL?.trim() ||
"http://127.0.0.1:8007"
).replace(/\/$/, "");
}
async function checkVipByEmailFromPayjsapi(
request: NextRequest,
email: string,
@@ -57,8 +64,13 @@ async function checkVipByEmailFromPayjsapi(
): Promise<PayjsVipEmailResult | null> {
const payConfig = getPaymentConfig();
const primaryUrl = resolvePayApiUrl(request);
const localUrl = getLocalPayApiUrl();
const candidates = Array.from(
new Set([primaryUrl, payConfig.apiUrl].map((x) => (x || "").trim().replace(/\/$/, "")).filter(Boolean))
new Set(
[localUrl, primaryUrl, payConfig.apiUrl]
.map((x) => (x || "").trim().replace(/\/$/, ""))
.filter(Boolean)
)
);
for (const apiUrl of candidates) {
@@ -280,29 +292,29 @@ export async function POST(request: NextRequest) {
loginError = error instanceof Error ? error.message : "登录失败";
}
let payjsResult: PayjsVipEmailResult | null = await checkVipByEmailFromPayjsapi(
request,
email,
password,
requestedAnonymousUserId || undefined
);
// PB 登录失败时,尝试使用 payjsapi 返回的 token/record 兜底恢复
if (!authData && payjsResult?.vip && payjsResult.token && payjsResult.record?.id) {
console.log("[check-by-email] fallback by payjsapi token email=%s", email);
return NextResponse.json({
ok: true,
vip: true,
token: payjsResult.token,
record: payjsResult.record,
effectiveUserId: payjsResult.record.id,
anonymousUserId: payjsResult.anonymousUserId ?? null,
memosAccount: payjsResult.anonymousUserId ?? null,
source: "payjsapi_token_fallback",
});
}
let payjsResult: PayjsVipEmailResult | null = null;
if (!authData) {
payjsResult = await checkVipByEmailFromPayjsapi(
request,
email,
password,
requestedAnonymousUserId || undefined
);
// PB 登录失败时,尝试使用 payjsapi 返回的 token/record 兜底恢复
if (payjsResult?.vip && payjsResult.token && payjsResult.record?.id) {
console.log("[check-by-email] fallback by payjsapi token email=%s", email);
return NextResponse.json({
ok: true,
vip: true,
token: payjsResult.token,
record: payjsResult.record,
effectiveUserId: payjsResult.record.id,
anonymousUserId: payjsResult.anonymousUserId ?? null,
memosAccount: payjsResult.anonymousUserId ?? null,
source: "payjsapi_token_fallback",
});
}
const msg = loginError || "登录失败";
return NextResponse.json(
{ ok: false, vip: false, error: msg },
@@ -316,6 +328,14 @@ export async function POST(request: NextRequest) {
});
if (!adminToken) {
// 管理员 token 获取失败时,降级到 payjsapi 结果,避免直接 500
if (!payjsResult) {
payjsResult = await checkVipByEmailFromPayjsapi(
request,
email,
password,
requestedAnonymousUserId || undefined
);
}
if (payjsResult?.vip) {
return NextResponse.json({
ok: true,
@@ -349,14 +369,12 @@ export async function POST(request: NextRequest) {
);
if (!recovery.vip) {
if (!payjsResult) {
payjsResult = await checkVipByEmailFromPayjsapi(
request,
email,
password,
requestedAnonymousUserId || undefined
);
}
payjsResult = await checkVipByEmailFromPayjsapi(
request,
email,
password,
requestedAnonymousUserId || undefined
);
recovery = await resolveRecovery(
adminToken,
email,

View File

@@ -110,34 +110,23 @@ export async function POST(request: NextRequest) {
);
}
const payjsResult = await checkVipFromPayjsapi(request, userId);
let vip = !!payjsResult?.vip;
let email = payjsResult?.email ?? null;
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 {
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) {
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,
@@ -167,6 +156,16 @@ export async function POST(request: NextRequest) {
);
}
// 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 =