's'
This commit is contained in:
@@ -11,9 +11,18 @@ import {
|
||||
import { queryXorPayOrderStatus } from "@/app/lib/xorpayStatus";
|
||||
import { queryZPayOrderStatus } from "@/app/lib/zpayStatus";
|
||||
|
||||
const getTotalFee = () =>
|
||||
const getDefaultTotalFee = () =>
|
||||
getPaymentConfig().defaultAmount ?? Number(process.env.PAYMENT_DEFAULT_AMOUNT) ?? 8800;
|
||||
|
||||
function parsePayPriceToCents(payPrice: unknown): number | null {
|
||||
if (payPrice == null) return null;
|
||||
const raw = String(payPrice).trim();
|
||||
if (!raw) return null;
|
||||
const value = Number(raw);
|
||||
if (!Number.isFinite(value) || value <= 0) return null;
|
||||
return Math.round(value * 100);
|
||||
}
|
||||
|
||||
async function getAdminToken(): Promise<string | null> {
|
||||
const email = process.env.POCKETBASE_EMAIL;
|
||||
const password = process.env.POCKETBASE_PASSWORD;
|
||||
@@ -46,7 +55,7 @@ function resolvePayApiUrl(request: NextRequest): string {
|
||||
async function verifyOrderPaid(
|
||||
request: NextRequest,
|
||||
orderId: string
|
||||
): Promise<boolean> {
|
||||
): Promise<{ paid: boolean; totalFeeCents: number | null }> {
|
||||
const apiUrl = resolvePayApiUrl(request);
|
||||
const url = `${apiUrl}/nomadvip/order_status?order_id=${encodeURIComponent(orderId)}`;
|
||||
console.log("[pay/confirm] verify order_status url=%s", url);
|
||||
@@ -59,11 +68,12 @@ async function verifyOrderPaid(
|
||||
}).finally(() => clearTimeout(timeout));
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const paid = !!data?.paid;
|
||||
const totalFeeCents = parsePayPriceToCents(data?.pay_price);
|
||||
if (!paid && !res.ok) {
|
||||
console.warn("[pay/confirm] order_status non-ok:", res.status, url, data);
|
||||
}
|
||||
if (paid) {
|
||||
return true;
|
||||
return { paid: true, totalFeeCents };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[pay/confirm] verifyOrderPaid fetch error:", e);
|
||||
@@ -79,7 +89,10 @@ async function verifyOrderPaid(
|
||||
xorpay?.url || ""
|
||||
);
|
||||
if (xorpay?.paid) {
|
||||
return true;
|
||||
return {
|
||||
paid: true,
|
||||
totalFeeCents: parsePayPriceToCents(xorpay?.pay_price),
|
||||
};
|
||||
}
|
||||
|
||||
const zpay = await queryZPayOrderStatus(orderId, 5000);
|
||||
@@ -91,7 +104,10 @@ async function verifyOrderPaid(
|
||||
zpay?.error || "",
|
||||
zpay?.url || ""
|
||||
);
|
||||
return !!zpay?.paid;
|
||||
return {
|
||||
paid: !!zpay?.paid,
|
||||
totalFeeCents: parsePayPriceToCents(zpay?.pay_price),
|
||||
};
|
||||
}
|
||||
|
||||
async function createMemosUser(username: string, password: string): Promise<boolean> {
|
||||
@@ -142,8 +158,8 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ ok: false, error: "缺少 order_id 或 user_id" }, { status: 400 });
|
||||
}
|
||||
|
||||
const paid = await verifyOrderPaid(request, orderId);
|
||||
if (!paid) {
|
||||
const verifyResult = await verifyOrderPaid(request, orderId);
|
||||
if (!verifyResult.paid) {
|
||||
console.error("[pay/confirm] 订单未支付 order_id=%s user_id=%s (请检查 payjsapi order_status 接口)", orderId, userId);
|
||||
return NextResponse.json({ ok: false, error: "订单未支付" }, { status: 400 });
|
||||
}
|
||||
@@ -154,7 +170,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const totalFee = getTotalFee();
|
||||
const totalFee = verifyResult.totalFeeCents ?? getDefaultTotalFee();
|
||||
|
||||
const checkExisting = await fetch(
|
||||
`${pb.url}/api/collections/payments/records?filter=${encodeURIComponent(`order_id="${orderId}"`)}&perPage=1`,
|
||||
|
||||
@@ -27,7 +27,7 @@ function buildErrorHtml(errorMsg: string): string {
|
||||
function buildPayHtml(
|
||||
payUrl: string,
|
||||
params: Record<string, unknown>,
|
||||
options?: { directToCashier?: boolean }
|
||||
options?: { directToCashier?: boolean; displayName?: string; avatar?: string }
|
||||
): string {
|
||||
const flat: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
@@ -40,10 +40,13 @@ function buildPayHtml(
|
||||
.map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}" />`)
|
||||
.join("\n");
|
||||
const direct = options?.directToCashier;
|
||||
const displayName = (options?.displayName || "当前用户").trim();
|
||||
const avatar = (options?.avatar || "").trim();
|
||||
const showPayerHint = !!avatar;
|
||||
// direct:微信内无中转,直接提交 | 非 direct:PC/H5 有中转页提示
|
||||
return direct
|
||||
? `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>支付</title><style>body{margin:0;overflow:hidden}</style></head><body><form id="f" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">${inputs}</form><script>document.getElementById("f").submit()</script></body></html>`
|
||||
: `<!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>`;
|
||||
: `<!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;}.wrap{background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:14px 16px;box-shadow:0 8px 26px rgba(15,23,42,.08);}.row{display:flex;align-items:center;gap:10px;}.av{width:30px;height:30px;border-radius:9999px;object-fit:cover;background:#e2e8f0;display:flex;align-items:center;justify-content:center;font-size:13px;}.t1{font-size:13px;color:#0f172a;font-weight:600;}.t2{font-size:12px;color:#64748b;}</style></head><body>${showPayerHint ? `<div class="wrap"><div class="row"><img class="av" src="${escapeAttr(avatar)}" alt="${escapeAttr(displayName)}" /><div><div class="t1">开通对象:${escapeAttr(displayName)}</div><div class="t2">正在为你升级 VIP,马上进入支付...</div></div></div></div>` : ""}<form id="zpayForm" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">${inputs}</form><script>document.getElementById("zpayForm").submit();</script></body></html>`;
|
||||
}
|
||||
|
||||
function setPayOrderCookie(res: NextResponse, orderId: string) {
|
||||
@@ -157,6 +160,8 @@ 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 wechatNick = String(body?.wechat_nick || body?.nick || body?.user_name || "").trim();
|
||||
const wechatAvatar = String(body?.wechat_avatar || body?.avatar || "").trim();
|
||||
// 渠道规则:pc/手机浏览器 -> zpay,仅微信内 -> xorpay
|
||||
const effectiveProvider =
|
||||
device === "wechat" ? "xorpay" : "zpay";
|
||||
@@ -243,6 +248,8 @@ export async function POST(request: NextRequest) {
|
||||
const submitUrl = resData?.pay_url || payConfig.zpaySubmitUrl;
|
||||
const html = buildPayHtml(submitUrl, params || {}, {
|
||||
directToCashier: device === "wechat",
|
||||
displayName: wechatNick || user_id,
|
||||
avatar: wechatAvatar,
|
||||
});
|
||||
const res = new NextResponse(html, {
|
||||
status: 200,
|
||||
@@ -255,6 +262,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (redirectRes.status === 200 && resData?.qrcode_mode && resData?.img) {
|
||||
const userId = String(user_id || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
|
||||
const userNick = String(wechatNick || userId).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
|
||||
const userAvatar = String(wechatAvatar || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
|
||||
const returnUrl = String(resData.return_url || finalReturnUrl).replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c
|
||||
@@ -293,6 +302,9 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
.countdown-num{font-size:1rem;font-weight:700;color:#1e293b;line-height:1;}
|
||||
.countdown-unit{font-size:9px;color:#94a3b8;}
|
||||
.tip{font-size:13px;color:#64748b;margin-top:16px;}
|
||||
.payer{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 14px;}
|
||||
.payer-avatar{width:30px;height:30px;border-radius:9999px;border:1px solid #e2e8f0;object-fit:cover;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:12px;}
|
||||
.payer-text{font-size:12px;color:#475569;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.success-wrap{display:none;}
|
||||
.success-wrap.show{display:block;}
|
||||
.wait-wrap.hide{display:none;}
|
||||
@@ -309,6 +321,7 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
<div class="wait-wrap" id="waitWrap">
|
||||
<h2>${esc(title)}</h2>
|
||||
<p class="hint">${esc(hint)}</p>
|
||||
${userAvatar ? `<div class="payer"><img class="payer-avatar" src="${userAvatar}" alt="${userNick}" /><span class="payer-text">开通对象:${userNick} · 正在升级 VIP</span></div>` : ""}
|
||||
<div class="wait-row">
|
||||
<div class="countdown-wrap">
|
||||
<svg class="countdown-ring" width="72" height="72" viewBox="0 0 72 72">
|
||||
@@ -473,6 +486,8 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||
const orderId = String(params?.order_id || params?.out_trade_no || "").trim();
|
||||
const html = buildPayHtml(submitUrl, params as Record<string, unknown>, {
|
||||
directToCashier: device === "wechat" || device === "h5",
|
||||
displayName: wechatNick || user_id,
|
||||
avatar: wechatAvatar,
|
||||
});
|
||||
const res = new NextResponse(html, {
|
||||
status: 200,
|
||||
|
||||
Reference in New Issue
Block a user