3 Commits
xdev ... test

Author SHA1 Message Date
eric
88aa96a2a1 'env' 2026-03-13 10:58:51 -05:00
eric
9d593c77bf s'' 2026-03-13 06:16:57 -05:00
eric
15dba690d3 's' 2026-03-13 05:51:29 -05:00
16 changed files with 161 additions and 37 deletions

30
.env.example Normal file
View File

@@ -0,0 +1,30 @@
# ========== 与 payjsapi/digital/nomadvip 同源 ==========
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.hackrobot.cn
POCKETBASE_JOIN_COLLECTION=solan
POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com
POCKETBASE_PASSWORD=Xiao4669805@
# ZPay 支付 (payjsapi)
PAYMENT_API_URL=http://127.0.0.1:8007
PAYMENT_PROVIDER=zpay
PAYMENT_DEFAULT_AMOUNT=80
PAYMENT_DEFAULT_TYPE=meetup
PAYMENT_JOIN_AMOUNT=80
PAYMENT_JOIN_TYPE=meetup
ZPAY_SUBMIT_URL=https://zpayz.cn/submit.php
# MinIO 存储
MINIO_ENDPOINT=minioweb.hackrobot.cn
MINIO_PORT=9035
MINIO_USE_SSL=false
MINIO_BUCKET=hackrobot
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_REGION=china
MINIO_UPLOAD_PREFIX=joins
# 站点(本地调试可改为 http://192.168.41.222:3001
NEXT_PUBLIC_SITE_URL=http://localhost:3001
# 生产环境跨站 SSO本地不设置
# AUTH_COOKIE_DOMAIN=.hackrobot.cn

12
.env.local Normal file
View File

@@ -0,0 +1,12 @@
# 本地调试 - 支付测试 1 元、zpay 渠道
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.hackrobot.cn
PAYMENT_API_URL=http://127.0.0.1:8007
PAYMENT_PROVIDER=zpay
PAYMENT_DEFAULT_AMOUNT=100
PAYMENT_JOIN_AMOUNT=100
NEXT_PUBLIC_SITE_ID=meetup
NEXT_PUBLIC_SITE_URL=http://localhost:3001
# PocketBase 管理员check-user、ensure-user、vip 需要)
POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com
POCKETBASE_PASSWORD=Xiao4669805@

2
.gitignore vendored
View File

@@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* # .env*
# vercel # vercel
.vercel .vercel

View File

@@ -84,12 +84,13 @@ export default function JoinPage() {
checkExisting(); checkExisting();
}, []); }, []);
// 轮询到支付成功时跳转到 /join/paid避免在 /join 重复显示成功页 // 轮询到支付成功时跳转到 /join/paid携带 order_id 供 complete-order 使用
usePayStatusPoll(() => { usePayStatusPoll((orderId) => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
const path = window.location.pathname; const path = window.location.pathname;
const paidPath = path.replace(/\/join\/?$/, "") + "/join/paid"; const paidPath = path.replace(/\/join\/?$/, "") + "/join/paid";
window.location.replace(paidPath); const url = orderId ? `${paidPath}?order_id=${encodeURIComponent(orderId)}` : paidPath;
window.location.replace(url);
}); });
useEffect(() => { useEffect(() => {

View File

@@ -53,18 +53,32 @@ export default function JoinPaidPage() {
pbSaveAuth(d.token, d.record); pbSaveAuth(d.token, d.record);
} }
window.dispatchEvent(new CustomEvent("auth:updated")); window.dispatchEvent(new CustomEvent("auth:updated"));
setTimeout(() => window.dispatchEvent(new CustomEvent("auth:updated")), 500); window.dispatchEvent(new CustomEvent("vip:updated"));
setTimeout(() => {
window.dispatchEvent(new CustomEvent("auth:updated"));
window.dispatchEvent(new CustomEvent("vip:updated"));
}, 400);
if (d.is_new) { if (d.is_new) {
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" }); await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
if (!cancelled) { if (!cancelled) {
setNeedPasswordChange(true); setNeedPasswordChange(true);
setShowPasswordModal(true); setShowPasswordModal(true);
} }
} else {
const needRes = await fetch("/api/meetup/need-password-change", { credentials: "include" });
const needData = await needRes.json().catch(() => ({}));
if (!cancelled && needData?.need === true) {
setNeedPasswordChange(true);
setShowPasswordModal(true);
}
} }
return { ok: true }; return { ok: true };
}; };
const run = async () => { const run = async () => {
const urlOrderId = typeof window !== "undefined"
? new URLSearchParams(window.location.search).get("order_id") || ""
: "";
const cookie = document.cookie.split(";").find((c) => c.trim().startsWith("join_pay_order=")); const cookie = document.cookie.split(";").find((c) => c.trim().startsWith("join_pay_order="));
const rawValue = cookie?.split("=")[1]?.trim(); const rawValue = cookie?.split("=")[1]?.trim();
const storageValue = (() => { const storageValue = (() => {
@@ -75,7 +89,7 @@ export default function JoinPaidPage() {
} }
})(); })();
const decoded = rawValue ? decodeURIComponent(rawValue) : storageValue; const decoded = rawValue ? decodeURIComponent(rawValue) : storageValue;
const orderId = decoded.split("|")[0]?.trim() || ""; const orderId = urlOrderId || decoded.split("|")[0]?.trim() || "";
if (!orderId) { if (!orderId) {
clearPendingOrder(); clearPendingOrder();
window.location.replace(`/${window.location.pathname.split("/")[1] || "zh"}/join`); window.location.replace(`/${window.location.pathname.split("/")[1] || "zh"}/join`);

View File

@@ -39,7 +39,6 @@ async function verifyOrderPaid(request: NextRequest, orderId: string): Promise<b
request.headers.get("host") || request.headers.get("x-forwarded-host"); request.headers.get("host") || request.headers.get("x-forwarded-host");
const apiUrl = ( const apiUrl = (
getApiUrlFromRequest(hostHeader) || getApiUrlFromRequest(hostHeader) ||
process.env.PAYMENT_API_URL ||
config.apiUrl config.apiUrl
).replace(/\/$/, ""); ).replace(/\/$/, "");

View File

@@ -13,11 +13,7 @@ function resolvePayApiUrl(request: NextRequest): string {
const config = getPaymentConfig(); const config = getPaymentConfig();
const hostHeader = const hostHeader =
request.headers.get("host") || request.headers.get("x-forwarded-host"); request.headers.get("host") || request.headers.get("x-forwarded-host");
return ( return (getApiUrlFromRequest(hostHeader) || config.apiUrl).replace(/\/$/, "");
getApiUrlFromRequest(hostHeader) ||
process.env.PAYMENT_API_URL ||
config.apiUrl
).replace(/\/$/, "");
} }
function setPayOrderCookie(response: NextResponse, orderId: string) { function setPayOrderCookie(response: NextResponse, orderId: string) {

View File

@@ -10,11 +10,7 @@ function resolvePayApiUrl(request: NextRequest): string {
const config = getPaymentConfig(); const config = getPaymentConfig();
const hostHeader = const hostHeader =
request.headers.get("host") || request.headers.get("x-forwarded-host"); request.headers.get("host") || request.headers.get("x-forwarded-host");
return ( return (getApiUrlFromRequest(hostHeader) || config.apiUrl).replace(/\/$/, "");
getApiUrlFromRequest(hostHeader) ||
process.env.PAYMENT_API_URL ||
config.apiUrl
).replace(/\/$/, "");
} }
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useCallback, useEffect } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import { Link, useLocale } from "@/i18n/navigation"; import { Link, useLocale } from "@/i18n/navigation";
import { useTranslation } from "@/i18n/navigation"; import { useTranslation } from "@/i18n/navigation";
import JoinModal from "./JoinModal"; import JoinModal from "./JoinModal";
@@ -85,6 +85,9 @@ export default function HeroSection() {
return () => window.removeEventListener("auth:updated", onAuth); return () => window.removeEventListener("auth:updated", onAuth);
}, []); }, []);
const checkUserCacheRef = useRef<{ email: string; data: { exists: boolean; vip: boolean }; ts: number } | null>(null);
const CACHE_TTL_MS = 60_000;
const handleCheckAndProceed = useCallback( const handleCheckAndProceed = useCallback(
async (em: string) => { async (em: string) => {
if (checking) return; if (checking) return;
@@ -96,6 +99,15 @@ export default function HeroSection() {
setError(null); setError(null);
setChecking(true); setChecking(true);
try { try {
const cached = checkUserCacheRef.current;
if (cached && cached.email === trimmed && Date.now() - cached.ts < CACHE_TTL_MS) {
const { exists, vip } = cached.data;
if (exists) {
setJoinModalVipOnly(!!vip);
setJoinModalOpen(true);
return;
}
}
const checkRes = await fetch("/api/meetup/check-user", { const checkRes = await fetch("/api/meetup/check-user", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -107,6 +119,7 @@ export default function HeroSection() {
throw new Error(checkData?.error || checkData?.detail || (locale === "zh" ? "操作失败" : "Operation failed")); throw new Error(checkData?.error || checkData?.detail || (locale === "zh" ? "操作失败" : "Operation failed"));
} }
if (checkData.exists) { if (checkData.exists) {
checkUserCacheRef.current = { email: trimmed, data: { exists: true, vip: !!checkData.vip }, ts: Date.now() };
if (checkData.vip) { if (checkData.vip) {
setJoinModalOpen(true); setJoinModalOpen(true);
setJoinModalVipOnly(true); setJoinModalVipOnly(true);
@@ -136,6 +149,9 @@ export default function HeroSection() {
(locale === "zh" ? "创建用户失败" : "Failed to create user") (locale === "zh" ? "创建用户失败" : "Failed to create user")
); );
} }
if (ensureData.is_new) {
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
}
user_id = String(ensureData.user_id).trim(); user_id = String(ensureData.user_id).trim();
if (ensureData?.token && ensureData?.record) { if (ensureData?.token && ensureData?.record) {
await fetch("/api/auth/sync-session", { await fetch("/api/auth/sync-session", {

View File

@@ -54,6 +54,9 @@ export default function JoinModal({ isOpen, onClose, initialEmail = "", vipOnly
if (!res.ok || !data?.ok) { if (!res.ok || !data?.ok) {
throw new Error(data?.error || (locale === "zh" ? "操作失败" : "Operation failed")); throw new Error(data?.error || (locale === "zh" ? "操作失败" : "Operation failed"));
} }
if (data.is_new) {
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
}
const { pbSaveAuth } = await import("@/app/lib/pocketbase"); const { pbSaveAuth } = await import("@/app/lib/pocketbase");
pbSaveAuth(data.token, data.record); pbSaveAuth(data.token, data.record);
await fetch("/api/auth/sync-session", { await fetch("/api/auth/sync-session", {

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { Link, useLocale, usePathname, useRouter, useTranslation } from "@/i18n/navigation"; import { Link, useLocale, usePathname, useRouter, useTranslation } from "@/i18n/navigation";
import { getStoredUserEmail, pbLogout } from "@/app/lib/pocketbase"; import { getStoredUserEmail, getStoredUser, getStoredToken, pbLogout } from "@/app/lib/pocketbase";
import { getNavLinks } from "@/config"; import { getNavLinks } from "@/config";
import AuthModal from "./AuthModal"; import AuthModal from "./AuthModal";
import ThemeToggle from "./ThemeToggle"; import ThemeToggle from "./ThemeToggle";
@@ -25,8 +25,8 @@ export default function NavbarComponent() {
const fetchAuth = useCallback(async () => { const fetchAuth = useCallback(async () => {
try { try {
const res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" }); let res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
const data = await res.json(); let data = await res.json();
if (data?.user && data?.token) { if (data?.user && data?.token) {
const { pbSaveAuth } = await import("@/app/lib/pocketbase"); const { pbSaveAuth } = await import("@/app/lib/pocketbase");
pbSaveAuth(data.token, { id: data.user.id, email: data.user.email }); pbSaveAuth(data.token, { id: data.user.id, email: data.user.email });
@@ -34,6 +34,25 @@ export default function NavbarComponent() {
setUserVip(!!data.user.vip); setUserVip(!!data.user.vip);
return; return;
} }
const token = getStoredToken();
const record = getStoredUser();
if (token && record?.id && record?.email) {
const syncRes = await fetch("/api/auth/sync-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, record }),
credentials: "include",
});
if (syncRes.ok) {
res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
data = await res.json();
if (data?.user && data?.token) {
setUserEmail(data.user.email);
setUserVip(!!data.user.vip);
return;
}
}
}
} catch { } catch {
/* ignore */ /* ignore */
} }
@@ -47,15 +66,21 @@ export default function NavbarComponent() {
useEffect(() => { useEffect(() => {
const onAuthUpdated = () => fetchAuth(); const onAuthUpdated = () => fetchAuth();
const onVipUpdated = () => fetchAuth();
window.addEventListener("auth:updated", onAuthUpdated); window.addEventListener("auth:updated", onAuthUpdated);
return () => window.removeEventListener("auth:updated", onAuthUpdated); window.addEventListener("vip:updated", onVipUpdated);
return () => {
window.removeEventListener("auth:updated", onAuthUpdated);
window.removeEventListener("vip:updated", onVipUpdated);
};
}, [fetchAuth]); }, [fetchAuth]);
const handleLogout = useCallback(async () => { const handleLogout = useCallback(async () => {
await pbLogout(); await pbLogout();
setUserEmail(null); setUserEmail(null);
setUserVip(false); setUserVip(false);
}, []); router.replace("/");
}, [router]);
const isActive = useCallback((href: string) => { const isActive = useCallback((href: string) => {
if (href.startsWith("/#")) return false; if (href.startsWith("/#")) return false;
@@ -124,7 +149,7 @@ export default function NavbarComponent() {
<span className="hidden sm:block max-w-[120px] truncate text-sm text-gray-600 dark:text-gray-400"> <span className="hidden sm:block max-w-[120px] truncate text-sm text-gray-600 dark:text-gray-400">
{userEmail} {userEmail}
</span> </span>
<UserAvatar email={userEmail} isVip={userVip} onLogout={() => { setUserEmail(null); setUserVip(false); }} /> <UserAvatar email={userEmail} isVip={userVip} onLogout={handleLogout} />
</div> </div>
) : null} ) : null}
<button <button

View File

@@ -54,18 +54,27 @@ function parsePendingOrder(raw: string | null): [string | null, boolean] {
return [orderId, true]; return [orderId, true];
} }
export function usePayStatusPoll(onPaid: () => void): boolean { export function usePayStatusPoll(onPaid: (orderId: string) => void): boolean {
const [polling, setPolling] = useState(false); const [polling, setPolling] = useState(false);
const onPaidRef = useRef(onPaid); const onPaidRef = useRef(onPaid);
onPaidRef.current = onPaid; onPaidRef.current = onPaid;
useEffect(() => { useEffect(() => {
const urlOrderId =
typeof window !== "undefined"
? new URLSearchParams(window.location.search).get("order_id") || ""
: "";
const cookieRaw = getCookie(COOKIE_NAME); const cookieRaw = getCookie(COOKIE_NAME);
const [cookieOrderId, cookieValid] = parsePendingOrder(cookieRaw); const [cookieOrderId, cookieValid] = parsePendingOrder(cookieRaw);
const storageRaw = getStorage(STORAGE_NAME); const storageRaw = getStorage(STORAGE_NAME);
const [storageOrderId, storageValid] = parsePendingOrder(storageRaw); const [storageOrderId, storageValid] = parsePendingOrder(storageRaw);
const orderId = cookieValid ? cookieOrderId : storageValid ? storageOrderId : null; let orderId = cookieValid ? cookieOrderId : storageValid ? storageOrderId : null;
const activeRaw = cookieValid ? cookieRaw : storageValid ? storageRaw : null; let activeRaw = cookieValid ? cookieRaw : storageValid ? storageRaw : null;
if (!orderId && urlOrderId.trim()) {
orderId = urlOrderId.trim();
activeRaw = `${orderId}|${Date.now()}`;
persistPendingOrder(activeRaw);
}
if (!orderId) { if (!orderId) {
if (cookieRaw || storageRaw) { if (cookieRaw || storageRaw) {
@@ -120,7 +129,7 @@ export function usePayStatusPoll(onPaid: () => void): boolean {
stop(); stop();
clearPendingOrder(); clearPendingOrder();
setPolling(false); setPolling(false);
onPaidRef.current(); onPaidRef.current(orderId);
return; return;
} }
} catch { } catch {

View File

@@ -67,11 +67,17 @@ export function pbSaveAuth(token: string, record: AuthUser): void {
localStorage.setItem(PB_STORAGE_KEYS.user, JSON.stringify(record)); localStorage.setItem(PB_STORAGE_KEYS.user, JSON.stringify(record));
} }
/** 登出 - 清除本地存储根域 Cookie */ /** 登出 - 清除本地存储、支付订单、根域 Cookie */
export async function pbLogout(): Promise<void> { export async function pbLogout(): Promise<void> {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
localStorage.removeItem(PB_STORAGE_KEYS.token); localStorage.removeItem(PB_STORAGE_KEYS.token);
localStorage.removeItem(PB_STORAGE_KEYS.user); localStorage.removeItem(PB_STORAGE_KEYS.user);
localStorage.removeItem("join_pay_order");
try {
document.cookie = "join_pay_order=; path=/; max-age=0";
} catch {
/* ignore */
}
try { try {
await fetch("/api/auth/logout", { method: "POST", credentials: "include" }); await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
} catch { } catch {
@@ -80,18 +86,24 @@ export async function pbLogout(): Promise<void> {
window.dispatchEvent(new CustomEvent("auth:updated")); window.dispatchEvent(new CustomEvent("auth:updated"));
} }
export function getStoredUserEmail(): string | null { export function getStoredUser(): AuthUser | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
try { try {
const raw = localStorage.getItem(PB_STORAGE_KEYS.user); const raw = localStorage.getItem(PB_STORAGE_KEYS.user);
if (!raw) return null; if (!raw) return null;
const user = JSON.parse(raw); const user = JSON.parse(raw);
return user?.email ?? null; if (!user?.id || !user?.email) return null;
return user;
} catch { } catch {
return null; return null;
} }
} }
export function getStoredUserEmail(): string | null {
const user = getStoredUser();
return user?.email ?? null;
}
export function getStoredToken(): string | null { export function getStoredToken(): string | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
return localStorage.getItem(PB_STORAGE_KEYS.token); return localStorage.getItem(PB_STORAGE_KEYS.token);

View File

@@ -3,6 +3,7 @@ export {
pbRegister, pbRegister,
pbSaveAuth, pbSaveAuth,
pbLogout, pbLogout,
getStoredUser,
getStoredUserEmail, getStoredUserEmail,
getStoredToken, getStoredToken,
} from "./auth"; } from "./auth";

View File

@@ -8,11 +8,20 @@ const isDev = process.env.NODE_ENV === "development";
export const ROOT_DOMAIN = export const ROOT_DOMAIN =
process.env.ROOT_DOMAIN || process.env.NEXT_PUBLIC_ROOT_DOMAIN || "hackrobot.cn"; process.env.ROOT_DOMAIN || process.env.NEXT_PUBLIC_ROOT_DOMAIN || "hackrobot.cn";
/** 支付 API 默认地址(本地可用 PAYMENT_API_URL 覆盖) */ /** 支付 API 默认地址。生产环境固定为 api 域名,避免 .env 中 127.0.0.1 导致支付失败 */
export const DEFAULT_PAYMENT_API_URL = isDev export const DEFAULT_PAYMENT_API_URL = isDev
? "http://127.0.0.1:8007" ? "http://127.0.0.1:8007"
: `https://api.${ROOT_DOMAIN}`; : `https://api.${ROOT_DOMAIN}`;
/** 生产环境若 PAYMENT_API_URL 指向 localhost则忽略并使用 api 域名 */
export function resolvePaymentApiUrl(): string {
const url = process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL;
if (!isDev && /^(https?:\/\/)?(127\.0\.0\.1|localhost)(:\d+)?(\/|$)/i.test(url)) {
return `https://api.${ROOT_DOMAIN}`;
}
return url;
}
export const DEFAULT_POCKETBASE_URL = `https://pocketbase.${ROOT_DOMAIN}`; export const DEFAULT_POCKETBASE_URL = `https://pocketbase.${ROOT_DOMAIN}`;
export const DEFAULT_MINIO_HOST = `minioweb.${ROOT_DOMAIN}`; export const DEFAULT_MINIO_HOST = `minioweb.${ROOT_DOMAIN}`;

View File

@@ -7,6 +7,7 @@ import {
DEFAULT_PAYMENT_API_URL, DEFAULT_PAYMENT_API_URL,
DEFAULT_POCKETBASE_URL, DEFAULT_POCKETBASE_URL,
DEFAULT_MINIO_HOST, DEFAULT_MINIO_HOST,
resolvePaymentApiUrl,
} from "./domain.config"; } from "./domain.config";
export interface PocketBaseConfig { export interface PocketBaseConfig {
@@ -44,12 +45,12 @@ const PAYJSAPI_PORT = process.env.PAYJSAPI_PORT || "8007";
/** 站点标识,用于 VIP 按站点隔离 */ /** 站点标识,用于 VIP 按站点隔离 */
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "meetup"; export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "meetup";
/** meetup 加入游牧中国 价格(分),测试用1元 */ /** meetup 加入游牧中国 价格(分),88元=8800 */
export const MEETUP_JOIN_AMOUNT = 100; export const MEETUP_JOIN_AMOUNT = 8800;
export function getApiUrlFromRequest(hostHeader: string | null): string | null { export function getApiUrlFromRequest(hostHeader: string | null): string | null {
if (!hostHeader) return null; if (!hostHeader) return null;
if (!isDev) return process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL; if (!isDev) return resolvePaymentApiUrl();
const hostname = hostHeader.split(":")[0]; const hostname = hostHeader.split(":")[0];
return `http://${hostname}:${PAYJSAPI_PORT}`; return `http://${hostname}:${PAYJSAPI_PORT}`;
} }
@@ -69,9 +70,9 @@ export function getPocketBaseConfig(): PocketBaseConfig {
export function getPaymentConfig(): PaymentConfig { export function getPaymentConfig(): PaymentConfig {
return { return {
enabled: true, enabled: true,
apiUrl: process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL, apiUrl: resolvePaymentApiUrl(),
provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay", provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay",
defaultAmount: Number(process.env.PAYMENT_DEFAULT_AMOUNT) || 100, defaultAmount: Number(process.env.PAYMENT_DEFAULT_AMOUNT) || 8800,
defaultType: process.env.PAYMENT_DEFAULT_TYPE || "meetup", defaultType: process.env.PAYMENT_DEFAULT_TYPE || "meetup",
zpaySubmitUrl: process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php", zpaySubmitUrl: process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php",
}; };