131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
/**
|
||
* ZPay 支付网关 SDK - 创建订单、查询状态,随时可调用
|
||
* 复制自 digital 项目 pay API
|
||
*/
|
||
|
||
import { getZPayConfig } from "../config";
|
||
|
||
const ZPAY_REQUIRED = [
|
||
"pid",
|
||
"type",
|
||
"out_trade_no",
|
||
"notify_url",
|
||
"return_url",
|
||
"name",
|
||
"money",
|
||
"sign",
|
||
"sign_type",
|
||
];
|
||
|
||
function escapeAttr(s: string): string {
|
||
return String(s)
|
||
.replace(/&/g, "&")
|
||
.replace(/"/g, """)
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">");
|
||
}
|
||
|
||
export interface CreateOrderOptions {
|
||
user_id: string;
|
||
return_url: string;
|
||
total_fee?: number;
|
||
type?: string;
|
||
channel?: string;
|
||
}
|
||
|
||
export interface CreateOrderResult {
|
||
params: Record<string, string>;
|
||
payUrl: string;
|
||
}
|
||
|
||
/** 创建支付订单 */
|
||
export async function createPayOrder(options: CreateOrderOptions): Promise<CreateOrderResult> {
|
||
const config = getZPayConfig();
|
||
if (!config.apiUrl) throw new Error("未配置 PAYMENT_API_URL");
|
||
|
||
const { user_id, return_url, total_fee, type, channel = "alipay" } = options;
|
||
const fee = total_fee ?? config.defaultAmount;
|
||
const orderType = type ?? config.defaultType;
|
||
|
||
const payload = {
|
||
user_id,
|
||
total_fee: Number(fee) || config.defaultAmount,
|
||
type: orderType,
|
||
channel,
|
||
return_url,
|
||
};
|
||
|
||
const controller = new AbortController();
|
||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||
|
||
const res = await fetch(`${config.apiUrl}/payh5`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
signal: controller.signal,
|
||
}).finally(() => clearTimeout(timeout));
|
||
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok || data?.status !== "ok") {
|
||
throw new Error(data?.detail || data?.msg || data?.message || "支付创建失败");
|
||
}
|
||
|
||
let params = data.params || data.xorpay_params || {};
|
||
const payUrl = data.pay_url || data.xorpay_url || config.zpaySubmitUrl;
|
||
if (!params || typeof params !== "object") {
|
||
throw new Error("payjsapi 未返回支付参数");
|
||
}
|
||
params = JSON.parse(JSON.stringify(params));
|
||
if (data.provider === "xorpay") {
|
||
throw new Error("当前 payjsapi 使用 xorpay,请设置 PAYMENT_PROVIDER=zpay");
|
||
}
|
||
return { params, payUrl };
|
||
}
|
||
|
||
/** 查询订单支付状态 */
|
||
export async function getOrderStatus(orderId: string): Promise<{ paid: boolean }> {
|
||
const config = getZPayConfig();
|
||
if (!config.apiUrl) throw new Error("未配置 PAYMENT_API_URL");
|
||
|
||
const res = await fetch(
|
||
`${config.apiUrl}/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`,
|
||
{ cache: "no-store" }
|
||
);
|
||
const data = await res.json().catch(() => ({}));
|
||
return { paid: !!data?.paid };
|
||
}
|
||
|
||
/** 构建 ZPay 表单跳转 HTML */
|
||
export function buildPayHtml(payUrl: string, params: Record<string, unknown>): string {
|
||
const flat: Record<string, string> = {};
|
||
for (const [k, v] of Object.entries(params)) {
|
||
if (v == null) continue;
|
||
const s = String(v).trim();
|
||
if (s === "") continue;
|
||
flat[k] = s;
|
||
}
|
||
const inputs = Object.entries(flat)
|
||
.map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}" />`)
|
||
.join("\n");
|
||
|
||
return `<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>跳转支付...</title>
|
||
<style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f0f4f8;}p{color:#64748b;}</style>
|
||
</head>
|
||
<body>
|
||
<p>正在跳转到支付页面...</p>
|
||
<form id="zpayForm" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">
|
||
${inputs}
|
||
</form>
|
||
<script>document.getElementById("zpayForm").submit();</script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
/** 获取 ZPay 必填参数列表(用于校验) */
|
||
export { ZPAY_REQUIRED };
|