This commit is contained in:
eric
2026-03-27 19:07:24 -05:00
parent 759875ec36
commit 3eb0334114
8 changed files with 49 additions and 31 deletions

View File

@@ -1,13 +1,16 @@
# ========== 与 payjsapi/digital/cnomadcna 同源 ========== # ========== 与 payjsapi/digital/cnomadcna 同源 ==========
# 复制为 .env.localPB/Memos 与 payjsapi 保持一致 # 复制为 .env.localPB/Memos 与 payjsapi 保持一致
# #
# 本地无 PocketBase仓库已提供写好的 .env.local与 payjsapi/.env.local 配套) # 本地无 PocketBasenext dev 下可用 POCKETBASE_INTERNAL_URL 指远程 PB
# payjsapi 侧需 PB_INTERNAL_URLNext 侧需 POCKETBASE_INTERNAL_URL勿只配 NEXT_PUBLIC_* # next start 生产同机部署:服务端只用 POCKETBASE_URL默认 http://127.0.0.1:8090不会读取 POCKETBASE_INTERNAL_URL避免误配公网域名拖慢 API
# payjsapi 生产请配本机 PB如 PB_INTERNAL_URL=http://127.0.0.1:8090勿让 payjs 再绕公网打 pocketbase 域名。
# ========== PocketBase ========== # ========== PocketBase ==========
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.hackrobot.cn NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.hackrobot.cn
# 服务端直连 PB本地未起 8090 时必填线上域名) # 仅 next dev 服务端生效:远程 PB
POCKETBASE_INTERNAL_URL=https://pocketbase.hackrobot.cn POCKETBASE_INTERNAL_URL=https://pocketbase.hackrobot.cn
# 仅 next start 生产服务端生效(同机示例)
# POCKETBASE_URL=http://127.0.0.1:8090
POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com
POCKETBASE_PASSWORD=Xiao4669805@ POCKETBASE_PASSWORD=Xiao4669805@

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { HomeBootOverlay } from "@/app/components/HomeBootOverlay";
import { triggerPay } from "@/app/lib/triggerPay"; import { triggerPay } from "@/app/lib/triggerPay";
import { getPayEnv, getDeviceFromEnv } from "@/app/lib/payEnv"; import { getPayEnv, getDeviceFromEnv } from "@/app/lib/payEnv";
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll"; import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
@@ -692,9 +691,6 @@ export default function HomeClient() {
return ( return (
<> <>
{!initDone && !isLoggedIn ? (
<HomeBootOverlay visible subtitle="正在同步会员状态…" />
) : null}
<VipLayout isLoggedIn={isLoggedIn} userName={userName} userEmail={userEmail} onLogout={handleLogout}> <VipLayout isLoggedIn={isLoggedIn} userName={userName} userEmail={userEmail} onLogout={handleLogout}>
{isLoggedIn ? ( {isLoggedIn ? (
<DashboardPage userName={userName} userEmail={userEmail} /> <DashboardPage userName={userName} userEmail={userEmail} />

View File

@@ -1,15 +1,10 @@
"use client"; "use client";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { HomeBootOverlay } from "@/app/components/HomeBootOverlay";
const HomeClient = dynamic(() => import("./HomeClient"), { const HomeClient = dynamic(() => import("./HomeClient"), {
ssr: false, ssr: false,
loading: () => ( loading: () => <div className="min-h-screen bg-[var(--background)]" aria-hidden />,
<div className="min-h-screen bg-[var(--background)]">
<HomeBootOverlay visible subtitle="加载中…" />
</div>
),
}); });
export function HomeClientLoader() { export function HomeClientLoader() {

View File

@@ -1,7 +1,9 @@
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_FETCH_TIMEOUT =
Number(process.env.POCKETBASE_FETCH_TIMEOUT) ||
(process.env.NODE_ENV === "development" ? 30_000 : 15_000);
const PB_ADMIN_TOKEN_TTL_MS = Number(process.env.POCKETBASE_ADMIN_TOKEN_TTL_MS) || 55_000; const PB_ADMIN_TOKEN_TTL_MS = Number(process.env.POCKETBASE_ADMIN_TOKEN_TTL_MS) || 55_000;
let adminTokenCache: { token: string; expiresAt: number } | null = null; let adminTokenCache: { token: string; expiresAt: number } | null = null;

View File

@@ -38,10 +38,14 @@ type RecoveryResult = {
const inflightEmailChecks = new Map<string, Promise<PayjsVipEmailResult | null>>(); const inflightEmailChecks = new Map<string, Promise<PayjsVipEmailResult | null>>();
const PAYJS_CHECK_BY_EMAIL_TIMEOUT = const PAYJS_CHECK_BY_EMAIL_TIMEOUT =
Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT) || 20000; Number(process.env.PAYJSAPI_CHECK_BY_EMAIL_TIMEOUT) || 20_000;
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);
const PB_USER_LOGIN_TIMEOUT_MS =
Number(process.env.POCKETBASE_USER_LOGIN_TIMEOUT_MS) ||
(process.env.NODE_ENV === "development" ? 30_000 : 15_000);
function resolvePayApiUrl(request: NextRequest): string { function resolvePayApiUrl(request: NextRequest): string {
const payConfig = getPaymentConfig(); const payConfig = getPaymentConfig();
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host"); const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
@@ -154,6 +158,7 @@ async function loginPocketBaseUser(
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identity: email, password }), body: JSON.stringify({ identity: email, password }),
cache: "no-store", cache: "no-store",
signal: AbortSignal.timeout(PB_USER_LOGIN_TIMEOUT_MS),
}); });
if (!res.ok) { if (!res.ok) {
@@ -301,6 +306,11 @@ export async function POST(request: NextRequest) {
} }
try { try {
const adminTokenPromise = getAdminToken().catch((error) => {
console.error("[check-by-email] getAdminToken failed:", error);
return null;
});
let authData: { token: string; record: PBAuthRecord } | null = null; let authData: { token: string; record: PBAuthRecord } | null = null;
let loginError: string | null = null; let loginError: string | null = null;
try { try {
@@ -339,10 +349,7 @@ export async function POST(request: NextRequest) {
); );
} }
const adminToken = await getAdminToken().catch((error) => { const adminToken = await adminTokenPromise;
console.error("[check-by-email] getAdminToken failed:", error);
return null;
});
if (!adminToken) { if (!adminToken) {
// 管理员 token 获取失败时,降级到 payjsapi 结果,避免直接 500 // 管理员 token 获取失败时,降级到 payjsapi 结果,避免直接 500
if (!payjsResult) { if (!payjsResult) {

View File

@@ -84,7 +84,7 @@ async function checkVipFromPayjsapi(
userId userId
)}`; )}`;
console.log("[check-by-user] payjsapi request user_id=%s url=%s", userId, url); console.log("[check-by-user] payjsapi request user_id=%s url=%s", userId, url);
const checkTimeout = Number(process.env.PAYJSAPI_CHECK_TIMEOUT) || 15000; const checkTimeout = Number(process.env.PAYJSAPI_CHECK_TIMEOUT) || 15_000;
const retries = Math.max(0, Number(process.env.PAYJSAPI_CHECK_RETRIES) || 1); const retries = Math.max(0, Number(process.env.PAYJSAPI_CHECK_RETRIES) || 1);
const res = await fetchPayjsVipWithRetry(url, checkTimeout, retries); const res = await fetchPayjsVipWithRetry(url, checkTimeout, retries);
if (!res) { if (!res) {
@@ -166,7 +166,7 @@ export async function POST(request: NextRequest) {
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 = "none";
try { try {
const adminToken = await getAdminToken().catch((error) => { const adminToken = await getAdminToken().catch((error) => {
@@ -206,7 +206,7 @@ export async function POST(request: NextRequest) {
); );
} }
// PB 命中再请求 payjsapi,避免每次都先走远端网络 // 同机 PB 命中则不再打 payjsapi;仅未命中时再走 payjs与线上极致路径一致
if (!vip) { if (!vip) {
payjsResult = await checkVipFromPayjsapi(request, userId); payjsResult = await checkVipFromPayjsapi(request, userId);
if (payjsResult?.vip) { if (payjsResult?.vip) {

View File

@@ -90,7 +90,7 @@ export function LoginModal({ open, onClose, onVerify, onVerifyByEmail, onSuccess
</div> </div>
</div> </div>
{verifyOverlay ? ( {verifyOverlay ? (
<HomeBootOverlay visible subtitle="正在验证并解锁会员…" /> <HomeBootOverlay visible subtitle="正在验证并登录…" />
) : null} ) : null}
</> </>
); );

View File

@@ -30,23 +30,38 @@ export interface MinioConfig {
publicUrl: string; publicUrl: string;
} }
/**
* 服务端 PocketBase 基址:
* - 生产next start只认 `POCKETBASE_URL`,否则本机 8090。忽略 `POCKETBASE_INTERNAL_URL`,避免误配公网域名把同机 API 拖慢。
* - 本地 next dev可用 `POCKETBASE_INTERNAL_URL` 指向远程 PB本机未起 8090
*/
export function getPocketBaseConfig(): PocketBaseConfig { export function getPocketBaseConfig(): PocketBaseConfig {
const serverSide = typeof window === "undefined"; const serverSide = typeof window === "undefined";
const internal = process.env.POCKETBASE_INTERNAL_URL?.trim();
if (serverSide && internal) {
return { url: internal.replace(/\/$/, "") };
}
// 生产同机部署默认走本机 PocketBase避免 DNS/公网抖动导致 fetch failed
if (serverSide) { if (serverSide) {
if (isDev) {
const devServerUrl =
process.env.POCKETBASE_INTERNAL_URL?.trim() ||
process.env.POCKETBASE_URL?.trim();
if (devServerUrl) {
return { url: devServerUrl.replace(/\/$/, "") };
}
return { url: "http://127.0.0.1:8090" };
}
return { return {
url: (process.env.POCKETBASE_URL || "http://127.0.0.1:8090").replace(/\/$/, ""), url: (process.env.POCKETBASE_URL || "http://127.0.0.1:8090").replace(
/\/$/,
""
),
}; };
} }
return { return {
url: url: (
process.env.NEXT_PUBLIC_POCKETBASE_URL || process.env.NEXT_PUBLIC_POCKETBASE_URL ||
process.env.POCKETBASE_URL || process.env.POCKETBASE_URL ||
"https://pocketbase.hackrobot.cn", "https://pocketbase.hackrobot.cn"
).replace(/\/$/, ""),
}; };
} }