/** * 支付确认:对齐 nomadvip 标准。payjsapi 异步通知未到达时,由前端直接写入 PocketBase site_vip */ import { NextRequest, NextResponse } from "next/server"; import { getApiUrlFromRequest, getPocketBaseConfig, getPaymentConfig, getJoinPaymentConfig, SITE_ID, } from "@/config/services.config"; import { queryXorPayOrderStatus } from "@/app/lib/payment/xorpayStatus"; import { queryZPayOrderStatus } from "@/app/lib/payment/zpayStatus"; function parseUserIdFromOrderId(orderId: string): string | null { if (!orderId || !orderId.includes("_order_")) return null; const prefix = orderId.split("_order_")[0]; if (!prefix || !prefix.includes("_")) return prefix || null; const parts = prefix.split("_"); const type = parts[parts.length - 1]; if (["vip", "video", "meetup", "book"].includes(type?.toLowerCase() || "")) { return parts.slice(0, -1).join("_") || null; } return prefix; } async function getAdminToken(): Promise { const email = process.env.POCKETBASE_EMAIL || process.env.PB_ADMIN_EMAIL; const password = process.env.POCKETBASE_PASSWORD || process.env.PB_ADMIN_PASSWORD; if (!email || !password) return null; const pb = getPocketBaseConfig(); const paths = ["/api/admins/auth-with-password", "/api/collections/_superusers/auth-with-password"]; for (const path of paths) { const res = await fetch(`${pb.url}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identity: email, password }), }); if (res.ok) { const data = await res.json(); if (data?.token) return data.token; } } return null; } function resolvePayApiUrl(request: NextRequest): string { const payConfig = getPaymentConfig(); const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host"); return ( (hostHeader ? getApiUrlFromRequest(hostHeader) : null) || payConfig.apiUrl ).replace(/\/$/, ""); } async function verifyOrderPaid(request: NextRequest, orderId: string): Promise { const apiUrl = resolvePayApiUrl(request); const url = `${apiUrl}/digital/order_status?order_id=${encodeURIComponent(orderId)}`; try { const res = await fetch(url, { cache: "no-store" }); const data = await res.json().catch(() => ({})); if (data?.paid) return true; } catch (e) { console.error("[pay/confirm] verifyOrderPaid fetch error:", e); } if (await queryXorPayOrderStatus(orderId, 5000).then((r) => r?.paid)) return true; if (await queryZPayOrderStatus(orderId, 5000).then((r) => r?.paid)) return true; return false; } export async function POST(request: NextRequest) { try { const body = await request.json().catch(() => ({})); let orderId = String(body?.order_id ?? body?.orderId ?? "").trim(); let userId = String(body?.user_id ?? body?.userId ?? "").trim(); if (!orderId) { return NextResponse.json({ ok: false, error: "缺少 order_id" }, { status: 400 }); } if (!userId) { userId = parseUserIdFromOrderId(orderId) || ""; } if (!userId) { return NextResponse.json({ ok: false, error: "缺少 user_id 且无法从 order_id 解析" }, { status: 400 }); } const paid = await verifyOrderPaid(request, orderId); if (!paid) { return NextResponse.json({ ok: false, error: "订单未支付" }, { status: 400 }); } const adminToken = await getAdminToken(); if (!adminToken) { return NextResponse.json({ ok: false, error: "PocketBase 未配置" }, { status: 500 }); } const pb = getPocketBaseConfig(); const joinConfig = getJoinPaymentConfig(); const totalFee = Number(joinConfig?.amount ?? 8800); const checkPayments = await fetch( `${pb.url}/api/collections/payments/records?filter=${encodeURIComponent(`order_id="${orderId}"`)}&perPage=1`, { headers: { Authorization: `Bearer ${adminToken}` } } ); const paymentsData = await checkPayments.json().catch(() => ({})); if ((paymentsData?.items?.length ?? 0) === 0) { await fetch(`${pb.url}/api/collections/payments/records`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${adminToken}`, }, body: JSON.stringify({ user_id: userId, type: "video", order_id: orderId, total_fee: totalFee, amount: totalFee, channel: "wxpay", }), }); } const siteVipCheck = await fetch( `${pb.url}/api/collections/site_vip/records?filter=${encodeURIComponent(`user_id="${userId}" && site_id="${SITE_ID}"`)}&perPage=1`, { headers: { Authorization: `Bearer ${adminToken}` } } ); const siteVipData = await siteVipCheck.json().catch(() => ({})); const siteVipItems = siteVipData?.items ?? []; if (siteVipItems.length > 0) { await fetch(`${pb.url}/api/collections/site_vip/records/${siteVipItems[0].id}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${adminToken}`, }, body: JSON.stringify({ order_id: orderId }), }); } else { const siteVipRes = await fetch(`${pb.url}/api/collections/site_vip/records`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${adminToken}`, }, body: JSON.stringify({ user_id: userId, site_id: SITE_ID, order_id: orderId, }), }); if (!siteVipRes.ok) { const err = await siteVipRes.json().catch(() => ({})); if (!err?.data?.order_id?.message?.includes("unique") && !err?.message?.includes("unique")) { console.error("[pay/confirm] site_vip create failed:", await siteVipRes.text()); } } } return NextResponse.json({ ok: true }); } catch (e) { console.error("[pay/confirm] error:", e); return NextResponse.json( { ok: false, error: e instanceof Error ? e.message : "确认失败" }, { status: 500 } ); } }