's'
This commit is contained in:
@@ -1,13 +1,16 @@
|
|||||||
# ========== 与 payjsapi/digital/cnomadcna 同源 ==========
|
# ========== 与 payjsapi/digital/cnomadcna 同源 ==========
|
||||||
# 复制为 .env.local,PB/Memos 与 payjsapi 保持一致
|
# 复制为 .env.local,PB/Memos 与 payjsapi 保持一致
|
||||||
#
|
#
|
||||||
# 本地无 PocketBase:仓库已提供写好的 .env.local(与 payjsapi/.env.local 配套)。
|
# 本地无 PocketBase:next dev 下可用 POCKETBASE_INTERNAL_URL 指远程 PB。
|
||||||
# payjsapi 侧需 PB_INTERNAL_URL;Next 侧需 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@
|
||||||
|
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(/\/$/, ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user