/**
* 共享支付触发逻辑 - 与首页「立即加入」效果一致
* 供首页、课程页、云手机页等所有「立即报名」按钮使用,直接发起支付,不跳转首页
*
* 校验逻辑:
* - 页面有本地存储(登录资料 + paidType=vip)时,优先使用本地存储并立即展示;后台悄悄校验,若 user 被删或 VIP 被撤则更新本地状态
* - 无身份标识和 VIP 权限等存储时,点击按钮直接进入支付流程,不校验本地 userId
*/
import { getPayEnv, getDeviceFromEnv } from "@/app/lib/payEnv";
import { getPaymentConfig } from "@/config/services.config";
const ANONYMOUS_USER_COOKIE = "nomadvip_anon_uid";
const ANONYMOUS_USER_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
const PAY_ORDER_COOKIE = "nomadvip_pay_order";
const PAY_ORDER_MAX_AGE = 60 * 15;
const VIP_PAY_TEST_MODE_KEY = "vipPayTestMode";
const PAY_LAUNCH_OVERLAY_ID = "nomadvip-pay-launch-overlay";
function getStorage(key: string): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
}
function setStorage(key: string, value: string): void {
if (typeof window === "undefined") return;
localStorage.setItem(key, value);
}
function getCookie(name: string): string | null {
if (typeof document === "undefined") return null;
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
function setAnonymousUserCookie(userId: string): void {
if (typeof document === "undefined") return;
if (!/^user\d+$/.test(userId)) return;
const secure = window.location.protocol === "https:" ? "; Secure" : "";
document.cookie = `${ANONYMOUS_USER_COOKIE}=${encodeURIComponent(
userId
)}; Path=/; Max-Age=${ANONYMOUS_USER_COOKIE_MAX_AGE}; SameSite=Lax${secure}`;
}
function setPayOrderCookie(orderId: string): void {
if (typeof document === "undefined") return;
const normalizedOrderId = orderId.trim();
if (!normalizedOrderId) return;
const secure = window.location.protocol === "https:" ? "; Secure" : "";
document.cookie = `${PAY_ORDER_COOKIE}=${encodeURIComponent(
`${normalizedOrderId}|${Date.now()}`
)}; Path=/; Max-Age=${PAY_ORDER_MAX_AGE}; SameSite=Lax${secure}`;
}
function getDisplayProfile() {
const wechatNick = (getStorage("wechatNick") || "").trim();
const userName = (getStorage("userName") || "").trim();
const userEmail = (getStorage("userEmail") || "").trim();
const wechatAvatar = (getStorage("wechatAvatar") || "").trim();
const displayName = wechatNick || userName || userEmail || "当前用户";
return { displayName, avatar: wechatAvatar || "" };
}
function showPayLaunchOverlay(displayName: string, avatar?: string): void {
if (typeof document === "undefined") return;
if (!(avatar || "").trim()) return;
const prev = document.getElementById(PAY_LAUNCH_OVERLAY_ID);
if (prev) prev.remove();
const safeName = displayName.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
const safeAvatar = (avatar || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
const node = document.createElement("div");
node.id = PAY_LAUNCH_OVERLAY_ID;
node.style.cssText = "position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,.28);pointer-events:none;";
node.innerHTML = `
开通对象:${safeName}
正在为你升级 VIP,马上进入支付...
`;
document.body.appendChild(node);
window.setTimeout(() => {
const current = document.getElementById(PAY_LAUNCH_OVERLAY_ID);
if (current) current.remove();
}, 2400);
}
function savePendingOrder(orderId: string): void {
const normalizedOrderId = orderId.trim();
if (!normalizedOrderId) return;
setPayOrderCookie(normalizedOrderId);
setStorage(PAY_ORDER_COOKIE, `${normalizedOrderId}|${Date.now()}`);
}
function getStoredAnonymousUserId(): string | null {
const memosAccount = getStorage("memosAccount");
if (memosAccount && /^user\d+$/.test(memosAccount)) return memosAccount;
const userId = getStorage("userId");
if (userId && /^user\d+$/.test(userId)) return userId;
const cookieUserId = getCookie(ANONYMOUS_USER_COOKIE);
if (cookieUserId && /^user\d+$/.test(cookieUserId)) {
setStorage("memosAccount", cookieUserId);
return cookieUserId;
}
return null;
}
/** 同步获取 userId,用于微信内立即表单提交,避免异步导致手势失效 */
function getOrCreateUserIdSync(): string {
const anonymousUserId = getStoredAnonymousUserId();
if (anonymousUserId) {
setStorage("userId", anonymousUserId);
setAnonymousUserCookie(anonymousUserId);
return anonymousUserId;
}
let userId = getStorage("userId");
if (userId?.startsWith("{") && userId.includes("data")) {
try {
const parsed = JSON.parse(userId) as { data?: string };
if (parsed?.data) {
userId = parsed.data;
setStorage("userId", userId);
}
} catch {
userId = null;
}
}
if (!userId) {
userId = `user${Date.now()}`;
setStorage("userId", userId);
}
return userId;
}
async function getOrCreateUserId(): Promise {
const syncId = getOrCreateUserIdSync();
if (syncId && !syncId.startsWith("user")) {
return syncId;
}
try {
const meRes = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
const meData = await meRes.json();
if (meData?.user?.id) {
setStorage("userId", meData.user.id);
return meData.user.id;
}
} catch {
/* ignore */
}
return syncId;
}
function redirectToPayZpay(params: {
user_id: string;
wechat_userid?: string;
wechat_nick?: string;
wechat_avatar?: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
device: "pc" | "h5" | "wechat";
}) {
const form = document.createElement("form");
form.method = "POST";
form.action = "/api/pay";
form.target = "_self";
form.style.display = "none";
const fields: [string, string][] = [
["user_id", params.user_id],
...(params.wechat_userid ? [["wechat_userid", params.wechat_userid] as [string, string]] : []),
...(params.wechat_nick ? [["wechat_nick", params.wechat_nick] as [string, string]] : []),
...(params.wechat_avatar ? [["wechat_avatar", params.wechat_avatar] as [string, string]] : []),
["return_url", params.return_url],
["total_fee", String(params.total_fee)],
["type", params.type],
["channel", params.channel],
["device", params.device],
["provider", "zpay"],
["html", "1"],
];
fields.forEach(([k, v]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = v;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
async function redirectToPayZpayH5(
params: {
user_id: string;
wechat_userid?: string;
wechat_nick?: string;
wechat_avatar?: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
},
onPendingOrder?: (orderId: string) => void
): Promise {
const res = await fetch("/api/pay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...params,
device: "h5",
provider: "zpay",
html: "0",
}),
});
const data = await res.json().catch(() => ({}));
if (!data?.ok) {
alert(data?.error || "支付发起失败");
return null;
}
const hasRedirect = !!data?.redirect_url;
const hasParams = !!data?.params && !!data?.payUrl;
if (!hasRedirect && !hasParams) {
alert(data?.error || "支付发起失败");
return null;
}
const orderId = data?.order_id?.trim() || null;
if (orderId) {
savePendingOrder(orderId);
onPendingOrder?.(orderId);
}
if (data?.redirect_url) {
window.location.href = data.redirect_url;
return orderId;
}
const form = document.createElement("form");
form.method = "POST";
form.action = data.payUrl;
form.target = "_self";
form.style.display = "none";
for (const [k, v] of Object.entries(data.params as Record)) {
if (v == null) continue;
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = String(v);
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
return orderId;
}
/** 微信内:同步表单 POST 到 /api/pay,避免异步 fetch 后 form.submit 被微信拦截 */
function payWechatXorpaySync(params: {
user_id: string;
wechat_userid?: string;
wechat_nick?: string;
wechat_avatar?: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
}) {
const form = document.createElement("form");
form.method = "POST";
form.action = "/api/pay";
form.target = "_self";
form.style.display = "none";
const fields: [string, string][] = [
["user_id", params.user_id],
...(params.wechat_userid ? [["wechat_userid", params.wechat_userid] as [string, string]] : []),
...(params.wechat_nick ? [["wechat_nick", params.wechat_nick] as [string, string]] : []),
...(params.wechat_avatar ? [["wechat_avatar", params.wechat_avatar] as [string, string]] : []),
["return_url", params.return_url],
["total_fee", String(params.total_fee)],
["type", params.type],
["channel", params.channel],
["device", "wechat"],
["provider", "xorpay"],
["html", "1"],
];
fields.forEach(([k, v]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = v;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
/**
* 触发支付 - 与首页「立即加入」效果一致,直接发起支付流程
* @param onPendingOrder 可选,H5 支付时收到 order_id 的回调(用于轮询)
*/
export async function triggerPay(
onPendingOrder?: (orderId: string) => void
): Promise {
const userId =
typeof navigator !== "undefined" && /MicroMessenger/i.test(navigator.userAgent)
? getOrCreateUserIdSync()
: await getOrCreateUserId();
if (/^user\d+$/.test(userId)) {
setStorage("memosAccount", userId);
setStorage("userId", userId);
setAnonymousUserCookie(userId);
}
const returnUrl = `${window.location.origin}/paid`;
const env = getPayEnv();
const device = getDeviceFromEnv(env);
const wechatUserId =
(getStorage("wechatUserId") || "").trim() ||
(getStorage("userid") || "").trim();
const isPayTestMode = getStorage(VIP_PAY_TEST_MODE_KEY) === "1";
const totalFee = isPayTestMode ? 100 : getPaymentConfig().defaultAmount ?? 8800;
const profile = getDisplayProfile();
const shouldShowLaunchOverlay =
device !== "wechat" && !!(profile.avatar || "").trim();
if (shouldShowLaunchOverlay) {
showPayLaunchOverlay(profile.displayName, profile.avatar);
}
const base = {
user_id: userId,
wechat_userid: wechatUserId || undefined,
wechat_nick: profile.displayName,
wechat_avatar: profile.avatar || undefined,
return_url: returnUrl,
total_fee: totalFee,
type: "vip",
channel: "wxpay" as const,
};
if (device === "wechat") {
payWechatXorpaySync(base);
} else if (device === "h5") {
await redirectToPayZpayH5(base, onPendingOrder);
} else {
redirectToPayZpay({ ...base, device });
}
}