This commit is contained in:
eric
2026-03-16 11:45:38 -05:00
parent 3e73917ad1
commit c5e057f0ed
9 changed files with 482 additions and 72 deletions

View File

@@ -2,6 +2,18 @@ import { getPaymentConfig, getJoinPaymentConfig } from "@/config/services.config
export type PayChannel = "wxpay" | "alipay";
const ORDER_COOKIE = "join_pay_order";
function savePendingOrder(orderId: string): void {
if (typeof document === "undefined") return;
try {
const v = `${orderId}|${Date.now()}`;
document.cookie = `${ORDER_COOKIE}=${encodeURIComponent(v)}; path=/; max-age=900`;
localStorage.setItem(ORDER_COOKIE, v);
} catch {}
}
/** 同步表单 POST,用于 PC 和微信内(微信内避免异步 fetch 导致手势失效) */
export function redirectToPay(params: {
user_id: string;
return_url: string;
@@ -44,6 +56,121 @@ export function redirectToPay(params: {
payForm.submit();
}
/** 微信内:同步表单 POST 到 /api/pay,避免异步 fetch 后 form.submit 被微信拦截(参考 nomadvip) */
export function payWechatXorpaySync(params: {
user_id: string;
return_url: string;
total_fee?: number;
type?: string;
channel: PayChannel;
useJoinConfig?: boolean;
}): void {
const baseConfig = getPaymentConfig();
const joinConfig = params.useJoinConfig ? getJoinPaymentConfig() : null;
const total_fee = params.total_fee ?? joinConfig?.amount ?? baseConfig.defaultAmount;
const type = params.type ?? joinConfig?.type ?? baseConfig.defaultType;
const payForm = document.createElement("form");
payForm.method = "POST";
payForm.action = "/api/pay";
payForm.target = "_self";
payForm.style.display = "none";
const fields: [string, string][] = [
["user_id", params.user_id],
["return_url", params.return_url],
["total_fee", String(total_fee)],
["type", type],
["channel", params.channel],
["device", "wechat"],
["html", "1"],
];
fields.forEach(([k, v]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = v;
payForm.appendChild(input);
});
document.body.appendChild(payForm);
payForm.submit();
}
/** 手机浏览器 H5:异步 fetch 获取 redirect_url 后跳转,支持 ZPAY 微信 H5 唤醒(参考 nomadvip + payjsapi) */
export async function redirectToPayH5(params: {
user_id: string;
return_url: string;
total_fee?: number;
type?: string;
channel: PayChannel;
useJoinConfig?: boolean;
}): Promise<void> {
const baseConfig = getPaymentConfig();
const joinConfig = params.useJoinConfig ? getJoinPaymentConfig() : null;
const total_fee = params.total_fee ?? joinConfig?.amount ?? baseConfig.defaultAmount;
const type = params.type ?? joinConfig?.type ?? baseConfig.defaultType;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 28000);
const res = await fetch("/api/pay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_id: params.user_id,
return_url: params.return_url,
total_fee,
type,
channel: params.channel,
device: "h5",
html: "0",
}),
redirect: "manual",
signal: controller.signal,
}).finally(() => clearTimeout(timeoutId));
if (res.status >= 301 && res.status <= 308) {
const location = res.headers.get("location");
if (location) {
const orderId = res.headers.get("x-pay-order-id")?.trim();
if (orderId) savePendingOrder(orderId);
window.location.href = location;
return;
}
}
const data = await res.json().catch(() => ({}));
if (!data?.ok) {
alert(data?.error || "支付发起失败");
return;
}
if (data?.order_id) savePendingOrder(data.order_id);
if (data?.redirect_url) {
window.location.href = data.redirect_url;
return;
}
if (data?.params && data?.payUrl) {
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<string, string>)) {
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();
return;
}
alert("支付发起失败");
}
export function getDeviceFromEnv(env: "pc" | "h5" | "wechat"): "pc" | "h5" | "wechat" {
if (env === "wechat") return "wechat";
return env === "pc" ? "pc" : "h5";

View File

@@ -1,4 +1,9 @@
export { redirectToPay, getDeviceFromEnv } from "./client";
export {
redirectToPay,
redirectToPayH5,
payWechatXorpaySync,
getDeviceFromEnv,
} from "./client";
export { getPayEnv } from "../payEnv";
export { usePayStatusPoll } from "./usePayStatusPoll";
export type { PayChannel } from "./client";