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
|
# PAYMENT_API_INTERNAL_URL=http://127.0.0.1:8007
|
||||||
# 内网/慢网络可适当提高超时(毫秒):check_vip 默认 15s,payh5/redirect 默认 60s
|
# 内网/慢网络可适当提高超时(毫秒):check_vip 默认 15s,payh5/redirect 默认 60s
|
||||||
# PAYJSAPI_CHECK_TIMEOUT=15000
|
# PAYJSAPI_CHECK_TIMEOUT=15000
|
||||||
# PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT=15000
|
# PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT=20000
|
||||||
# PAYJSAPI_CHECK_BY_EMAIL_RETRIES=1
|
# PAYJSAPI_CHECK_BY_EMAIL_RETRIES=1
|
||||||
# PAYJSAPI_PAYH5_TIMEOUT=60000
|
# PAYJSAPI_PAYH5_TIMEOUT=60000
|
||||||
# PAYJSAPI_REDIRECT_TIMEOUT=60000
|
# PAYJSAPI_REDIRECT_TIMEOUT=60000
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { SITE_ID, getPocketBaseConfig } from "@/config/services.config";
|
import { SITE_ID, getPocketBaseConfig } from "@/config/services.config";
|
||||||
|
|
||||||
const VIP_EMAIL_LINK_COLLECTION = "vip_email_link";
|
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>;
|
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(
|
async function listRecords(
|
||||||
collection: string,
|
collection: string,
|
||||||
filter: string,
|
filter: string,
|
||||||
@@ -35,7 +44,7 @@ async function listRecords(
|
|||||||
url.searchParams.set("sort", options.sort);
|
url.searchParams.set("sort", options.sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url.toString(), {
|
const res = await pbFetch(url.toString(), {
|
||||||
headers: { Authorization: `Bearer ${adminToken}` },
|
headers: { Authorization: `Bearer ${adminToken}` },
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
@@ -52,7 +61,7 @@ async function createRecord(
|
|||||||
adminToken: string,
|
adminToken: string,
|
||||||
body: Record<string, unknown>
|
body: Record<string, unknown>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await fetch(`${getPBBaseUrl()}/api/collections/${collection}/records`, {
|
await pbFetch(`${getPBBaseUrl()}/api/collections/${collection}/records`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -69,7 +78,7 @@ async function patchRecord(
|
|||||||
adminToken: string,
|
adminToken: string,
|
||||||
body: Record<string, unknown>
|
body: Record<string, unknown>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await fetch(
|
await pbFetch(
|
||||||
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@@ -88,7 +97,7 @@ async function deleteRecord(
|
|||||||
recordId: string,
|
recordId: string,
|
||||||
adminToken: string
|
adminToken: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await fetch(
|
await pbFetch(
|
||||||
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@@ -103,7 +112,7 @@ async function getRecord(
|
|||||||
recordId: string,
|
recordId: string,
|
||||||
adminToken: string
|
adminToken: string
|
||||||
): Promise<PBRecord | null> {
|
): Promise<PBRecord | null> {
|
||||||
const res = await fetch(
|
const res = await pbFetch(
|
||||||
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
`${getPBBaseUrl()}/api/collections/${collection}/records/${recordId}`,
|
||||||
{
|
{
|
||||||
headers: { Authorization: `Bearer ${adminToken}` },
|
headers: { Authorization: `Bearer ${adminToken}` },
|
||||||
@@ -139,6 +148,10 @@ export async function getAdminToken(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (adminTokenCache && adminTokenCache.expiresAt > Date.now()) {
|
||||||
|
return adminTokenCache.token;
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = getPBBaseUrl();
|
const baseUrl = getPBBaseUrl();
|
||||||
const paths = [
|
const paths = [
|
||||||
"/api/admins/auth-with-password",
|
"/api/admins/auth-with-password",
|
||||||
@@ -146,7 +159,7 @@ export async function getAdminToken(): Promise<string | null> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
const res = await fetch(`${baseUrl}${path}`, {
|
const res = await pbFetch(`${baseUrl}${path}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ identity: email, password }),
|
body: JSON.stringify({ identity: email, password }),
|
||||||
@@ -158,6 +171,10 @@ export async function getAdminToken(): Promise<string | null> {
|
|||||||
|
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (typeof data?.token === "string" && data.token) {
|
if (typeof data?.token === "string" && data.token) {
|
||||||
|
adminTokenCache = {
|
||||||
|
token: data.token,
|
||||||
|
expiresAt: Date.now() + PB_ADMIN_TOKEN_TTL_MS,
|
||||||
|
};
|
||||||
return data.token;
|
return data.token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type RecoveryResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PAYJS_CHECK_BY_EMAIL_TIMEOUT =
|
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 =
|
const PAYJS_CHECK_BY_EMAIL_RETRIES =
|
||||||
Math.max(0, Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_RETRIES) || 1);
|
Math.max(0, Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_RETRIES) || 1);
|
||||||
|
|
||||||
@@ -49,6 +49,13 @@ function resolvePayApiUrl(request: NextRequest): string {
|
|||||||
).replace(/\/$/, "");
|
).replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLocalPayApiUrl(): string {
|
||||||
|
return (
|
||||||
|
process.env.PAYMENT_API_INTERNAL_URL?.trim() ||
|
||||||
|
"http://127.0.0.1:8007"
|
||||||
|
).replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
async function checkVipByEmailFromPayjsapi(
|
async function checkVipByEmailFromPayjsapi(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
email: string,
|
email: string,
|
||||||
@@ -57,8 +64,13 @@ async function checkVipByEmailFromPayjsapi(
|
|||||||
): Promise<PayjsVipEmailResult | null> {
|
): Promise<PayjsVipEmailResult | null> {
|
||||||
const payConfig = getPaymentConfig();
|
const payConfig = getPaymentConfig();
|
||||||
const primaryUrl = resolvePayApiUrl(request);
|
const primaryUrl = resolvePayApiUrl(request);
|
||||||
|
const localUrl = getLocalPayApiUrl();
|
||||||
const candidates = Array.from(
|
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) {
|
for (const apiUrl of candidates) {
|
||||||
@@ -280,29 +292,29 @@ export async function POST(request: NextRequest) {
|
|||||||
loginError = error instanceof Error ? error.message : "登录失败";
|
loginError = error instanceof Error ? error.message : "登录失败";
|
||||||
}
|
}
|
||||||
|
|
||||||
let payjsResult: PayjsVipEmailResult | null = await checkVipByEmailFromPayjsapi(
|
let payjsResult: PayjsVipEmailResult | null = null;
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!authData) {
|
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 || "登录失败";
|
const msg = loginError || "登录失败";
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ ok: false, vip: false, error: msg },
|
{ ok: false, vip: false, error: msg },
|
||||||
@@ -316,6 +328,14 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
if (!adminToken) {
|
if (!adminToken) {
|
||||||
// 管理员 token 获取失败时,降级到 payjsapi 结果,避免直接 500
|
// 管理员 token 获取失败时,降级到 payjsapi 结果,避免直接 500
|
||||||
|
if (!payjsResult) {
|
||||||
|
payjsResult = await checkVipByEmailFromPayjsapi(
|
||||||
|
request,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
requestedAnonymousUserId || undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
if (payjsResult?.vip) {
|
if (payjsResult?.vip) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -349,14 +369,12 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!recovery.vip) {
|
if (!recovery.vip) {
|
||||||
if (!payjsResult) {
|
payjsResult = await checkVipByEmailFromPayjsapi(
|
||||||
payjsResult = await checkVipByEmailFromPayjsapi(
|
request,
|
||||||
request,
|
email,
|
||||||
email,
|
password,
|
||||||
password,
|
requestedAnonymousUserId || undefined
|
||||||
requestedAnonymousUserId || undefined
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
recovery = await resolveRecovery(
|
recovery = await resolveRecovery(
|
||||||
adminToken,
|
adminToken,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -110,34 +110,23 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payjsResult = await checkVipFromPayjsapi(request, userId);
|
let payjsResult: PayjsVipResult | null = null;
|
||||||
let vip = !!payjsResult?.vip;
|
let vip = false;
|
||||||
let email = payjsResult?.email ?? null;
|
let email: string | null = null;
|
||||||
let linkedUserId: string | undefined;
|
let linkedUserId: string | undefined;
|
||||||
let vipFromSite = false;
|
let vipFromSite = false;
|
||||||
let vipFromPayments = false;
|
let vipFromPayments = false;
|
||||||
let vipSource = vip ? "payjsapi" : "none";
|
let vipSource = vip ? "payjsapi" : "none";
|
||||||
|
|
||||||
try {
|
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) => {
|
const adminToken = await getAdminToken().catch((error) => {
|
||||||
console.error("[check-by-user] getAdminToken failed:", error);
|
console.error("[check-by-user] getAdminToken failed:", error);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (!adminToken) {
|
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");
|
console.error("[check-by-user] missing PocketBase admin token, fallback to payjs result");
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
ok: true,
|
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)) {
|
if (isAnonymousUserId(userId)) {
|
||||||
const linkRecord = await findVipEmailLinkByAnonymousUserId(adminToken, userId);
|
const linkRecord = await findVipEmailLinkByAnonymousUserId(adminToken, userId);
|
||||||
const linkEmail =
|
const linkEmail =
|
||||||
|
|||||||
Reference in New Issue
Block a user