178 lines
5.3 KiB
TypeScript
178 lines
5.3 KiB
TypeScript
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;
|
||
total_fee?: number;
|
||
type?: string;
|
||
channel: PayChannel;
|
||
device: "pc" | "h5" | "wechat";
|
||
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", params.device],
|
||
["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();
|
||
}
|
||
|
||
/** 微信内:同步表单 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";
|
||
}
|