import { NextRequest, NextResponse } from "next/server"; import { getApiUrlFromRequest, getPocketBaseConfig, getPaymentConfig, SITE_ID, } from "@/config/services.config"; import { getAdminToken } from "@/app/lib/pocketbase/admin"; import { queryXorPayOrderStatus } from "@/app/lib/payment/xorpayStatus"; import { queryZPayOrderStatus } from "@/app/lib/payment/zpayStatus"; const DEFAULT_PASSWORD = "12345678"; function parseOrderId(orderId: string): { userId: string; payType: string } { if (!orderId.includes("_order_")) return { userId: "", payType: "unknown" }; const prefix = orderId.split("_order_")[0]; if (prefix.includes("_")) { const lastUnderscore = prefix.lastIndexOf("_"); return { userId: prefix.slice(0, lastUnderscore), payType: prefix.slice(lastUnderscore + 1).toLowerCase(), }; } return { userId: prefix, payType: "unknown" }; } function decodePendingEmail(userId: string): string | null { if (!userId.startsWith("pending_")) return null; try { return Buffer.from(userId.slice(8), "hex").toString("utf-8"); } catch { return null; } } async function verifyOrderPaid(request: NextRequest, orderId: string): Promise { const config = getPaymentConfig(); const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host"); const apiUrl = ( getApiUrlFromRequest(hostHeader) || config.apiUrl ).replace(/\/$/, ""); try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); const url = `${apiUrl}/cnomadcna/order_status?order_id=${encodeURIComponent(orderId)}`; console.log(`complete-order: check payjsapi order_id=${orderId} url=${url}`); const res = await fetch(url, { cache: "no-store", signal: controller.signal }).finally(() => clearTimeout(timeout) ); const data = await res.json().catch(() => ({})); if (data?.paid) { console.log(`complete-order: payjsapi paid order_id=${orderId}`); return true; } } catch (e) { console.error(`complete-order: payjsapi fetch error order_id=${orderId}`, e); } const xorpay = await queryXorPayOrderStatus(orderId, 5000); console.log( `complete-order: xorpay fallback order_id=${orderId} paid=${!!xorpay?.paid} status=${xorpay?.status || ""} error=${xorpay?.error || ""}` ); if (xorpay?.paid) return true; const zpay = await queryZPayOrderStatus(orderId, 5000); console.log( `complete-order: zpay fallback order_id=${orderId} paid=${!!zpay?.paid} status=${zpay?.status || ""} error=${zpay?.error || ""}` ); return !!zpay?.paid; } async function ensureSiteVip( base: string, adminToken: string, userId: string, orderId: string, ): Promise { try { const filter = `user_id = "${userId}" && site_id = "${SITE_ID}"`; const checkRes = await fetch( `${base}/api/collections/site_vip/records?filter=${encodeURIComponent(filter)}&perPage=1`, { headers: { Authorization: `Bearer ${adminToken}` }, cache: "no-store" } ); if (checkRes.ok) { const checkData = await checkRes.json(); const existing = (checkData?.items ?? [])[0]; if (existing) { await fetch(`${base}/api/collections/site_vip/records/${existing.id}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${adminToken}` }, body: JSON.stringify({ order_id: orderId, expires_at: "" }), }); return true; } } const createRes = await fetch(`${base}/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, expires_at: "" }), }); return createRes.ok; } catch (e) { console.error("ensureSiteVip error:", e); return false; } } async function createUserByEmail(base: string, adminToken: string, email: string): Promise { const createRes = await fetch(`${base}/api/collections/users/records`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${adminToken}` }, body: JSON.stringify({ email, password: DEFAULT_PASSWORD, passwordConfirm: DEFAULT_PASSWORD }), }); if (createRes.ok) { const data = await createRes.json(); return data?.id ?? null; } if (createRes.status === 400) { const filter = `email="${email}"`; const getRes = await fetch( `${base}/api/collections/users/records?filter=${encodeURIComponent(filter)}&perPage=1`, { headers: { Authorization: `Bearer ${adminToken}` }, cache: "no-store" } ); if (getRes.ok) { const getData = await getRes.json(); return (getData?.items ?? [])[0]?.id ?? null; } } return null; } export async function POST(request: NextRequest) { try { const body = await request.json().catch(() => ({})); const orderId = String(body?.order_id || "").trim(); if (!orderId) { return NextResponse.json({ ok: false, error: "Missing order_id" }, { status: 400 }); } let { userId, payType } = parseOrderId(orderId); if (!userId || payType !== "meetup") { return NextResponse.json({ ok: false, error: "订单格式无效" }, { status: 400 }); } const adminToken = await getAdminToken(); if (!adminToken) { console.error("complete-order: getAdminToken failed, check POCKETBASE_EMAIL/PASSWORD"); return NextResponse.json({ ok: false, error: "服务配置错误" }, { status: 500 }); } const pb = getPocketBaseConfig(); const base = pb.url.replace(/\/$/, ""); // 1. 先查 site_vip 是否已由异步通知创建 const filter = `order_id = "${orderId}"`; const vipRes = await fetch( `${base}/api/collections/site_vip/records?filter=${encodeURIComponent(filter)}&perPage=1`, { headers: { Authorization: `Bearer ${adminToken}` }, cache: "no-store" } ); let found = false; if (vipRes.ok) { const vipData = await vipRes.json(); found = (vipData?.items ?? []).length > 0; } // 2. 没找到 → 查 ZPAY 确认是否已支付,已支付则直接创建 site_vip if (!found) { const paid = await verifyOrderPaid(request, orderId); if (!paid) { console.error(`complete-order: ZPAY 未确认支付 order_id=${orderId}`); return NextResponse.json({ ok: false, error: "订单不存在或未支付成功" }, { status: 400 }); } console.log(`complete-order: ZPAY 已确认支付,主动创建 site_vip order_id=${orderId}`); // pending_ 新用户:先创建用户 let realUserId = userId; if (userId.startsWith("pending_")) { const email = decodePendingEmail(userId); if (!email) { return NextResponse.json({ ok: false, error: "订单格式无效" }, { status: 400 }); } const newId = await createUserByEmail(base, adminToken, email); if (!newId) { return NextResponse.json({ ok: false, error: "创建用户失败" }, { status: 500 }); } realUserId = newId; } const created = await ensureSiteVip(base, adminToken, realUserId, orderId); if (!created) { return NextResponse.json({ ok: false, error: "VIP 开通失败" }, { status: 500 }); } } // 3. pending_ 新用户:登录并返回 token if (userId.startsWith("pending_")) { const email = decodePendingEmail(userId); if (!email) { return NextResponse.json({ ok: false, error: "订单格式无效" }, { status: 400 }); } const loginRes = await fetch(`${base}/api/collections/users/auth-with-password`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identity: email, password: DEFAULT_PASSWORD }), }); if (!loginRes.ok) { return NextResponse.json({ ok: false, error: "登录失败,请稍后重试" }, { status: 500 }); } const authData = await loginRes.json(); return NextResponse.json({ ok: true, token: authData.token, record: authData.record, is_new: true, }); } return NextResponse.json({ ok: true, token: null, record: null, is_new: false }); } catch (error) { console.error("complete-order error:", error); return NextResponse.json({ ok: false, error: "Operation failed" }, { status: 500 }); } }