s''
This commit is contained in:
@@ -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 默认 15s,payh5/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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user