diff --git a/app/api/pay/route.ts b/app/api/pay/route.ts
index c3feaef..150268f 100644
--- a/app/api/pay/route.ts
+++ b/app/api/pay/route.ts
@@ -37,10 +37,10 @@ function buildPayHtml(
.map(([k, v]) => ``)
.join("\n");
const direct = options?.directToCashier;
- // 手机/微信:无中转页,直接提交表单跳转收银台
+ // direct:微信内无中转,直接提交 | 非 direct:PC/H5 有中转页提示
return direct
? `
支付`
- : `支付`;
+ : `跳转支付正在跳转到支付页面...
`;
}
function setPayOrderCookie(res: NextResponse, orderId: string) {
@@ -54,7 +54,8 @@ async function createPayOrder(
total_fee: number,
type: string,
channel: string,
- clientIp?: string
+ clientIp?: string,
+ provider?: string
) {
const payload: Record = {
user_id,
@@ -63,6 +64,7 @@ async function createPayOrder(
channel,
return_url,
...(clientIp && { client_ip: clientIp }),
+ ...(provider && { provider }),
};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
@@ -116,6 +118,7 @@ export async function POST(request: NextRequest) {
const type = String(body?.type || "vip");
const channel = String(body?.channel || "wxpay");
const device = String(body?.device || "pc");
+ const reqProvider = String(body?.provider || "").trim().toLowerCase() || undefined;
const accept = request.headers.get("accept") || "";
const wantHtml = String(body?.html) === "1" || accept.includes("text/html");
@@ -135,10 +138,6 @@ export async function POST(request: NextRequest) {
: request.headers.get("origin") || "http://localhost:3000";
const finalReturnUrl = return_url || `${origin}/paid`;
- if (!wantHtml) {
- return NextResponse.json({ ok: false, error: "请传 html=1" }, { status: 400 });
- }
-
const clientIp =
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
request.headers.get("x-real-ip") ||
@@ -151,7 +150,8 @@ export async function POST(request: NextRequest) {
total_fee,
type,
channel,
- clientIp
+ clientIp,
+ reqProvider
);
if (!params || Object.keys(params).length === 0) {
@@ -164,6 +164,20 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ ok: false, error: "支付参数为空" }, { status: 500 });
}
+ // wantHtml=0:返回 JSON,供微信 fetch 后直接提交表单(无中转页)
+ if (!wantHtml) {
+ const orderId = String(params?.order_id || params?.out_trade_no || "").trim();
+ const res = NextResponse.json({
+ ok: true,
+ params,
+ payUrl: provider === "xorpay" ? payUrl : payConfig.zpaySubmitUrl,
+ provider,
+ order_id: orderId,
+ });
+ if (orderId) setPayOrderCookie(res, orderId);
+ return res;
+ }
+
// xorpay:直接 payh5 返回收银台参数,无需 redirect 中转
const submitUrl = provider === "xorpay" ? payUrl : payConfig.zpaySubmitUrl;
if (provider === "xorpay") {
@@ -187,6 +201,7 @@ export async function POST(request: NextRequest) {
channel,
return_url: finalReturnUrl,
device,
+ provider: "zpay",
...(clientIp && { client_ip: clientIp }),
};
diff --git a/app/page.tsx b/app/page.tsx
index 799babf..ce221db 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -13,7 +13,8 @@ import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
const PB_URL =
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
-function redirectToPay(params: {
+/** PC/H5:form POST 到 /api/pay,有中转页,默认 zpay */
+function redirectToPayZpay(params: {
user_id: string;
return_url: string;
total_fee: number;
@@ -33,6 +34,7 @@ function redirectToPay(params: {
["type", params.type],
["channel", params.channel],
["device", params.device],
+ ["provider", "zpay"],
["html", "1"],
];
fields.forEach(([k, v]) => {
@@ -46,6 +48,47 @@ function redirectToPay(params: {
form.submit();
}
+/** 微信内:fetch 获取 xorpay 参数,直接提交表单到收银台,无中转页 */
+async function payWechatXorpay(params: {
+ user_id: string;
+ return_url: string;
+ total_fee: number;
+ type: string;
+ channel: "alipay" | "wxpay";
+}) {
+ const res = await fetch("/api/pay", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ ...params,
+ total_fee: params.total_fee,
+ device: "wechat",
+ provider: "xorpay",
+ html: "0",
+ }),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!data?.ok || !data?.params || !data?.payUrl) {
+ alert(data?.error || "支付发起失败");
+ return;
+ }
+ 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();
+}
+
function getStorage(key: string): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
@@ -162,14 +205,12 @@ export default function Home() {
const env = getPayEnv();
const device = getDeviceFromEnv(env);
const totalFee = getPaymentConfig().defaultAmount ?? 880;
- redirectToPay({
- user_id: userId,
- return_url: returnUrl,
- total_fee: totalFee,
- type: "vip",
- channel: "wxpay",
- device,
- });
+ const base = { user_id: userId, return_url: returnUrl, total_fee: totalFee, type: "vip", channel: "wxpay" as const };
+ if (device === "wechat") {
+ payWechatXorpay(base);
+ } else {
+ redirectToPayZpay({ ...base, device });
+ }
}, [getOrCreateUserId]);
const handleCopyUsername = useCallback(() => {