/** * 本地直接确认支付:检测到 paid 后立即写入 PocketBase,不依赖 salonapi * 用于扫码页 doConfirm,避免「检测到支付,正在确认…」等待过久 */ import { NextRequest, NextResponse } from "next/server"; import { getAdminToken } from "@/app/lib/pocketbase/admin"; import { getPocketBaseConfig, SITE_ID } from "@/config/services.config"; import { queryZPayOrderStatus } from "@/app/lib/zpayStatus"; import { queryXorPayOrderStatus } from "@/app/lib/xorpayStatus"; function parseOrderId(orderId: string): { userId: string; payType: string } { const id = (orderId || "").trim(); if (!id.includes("_order_")) return { userId: "", payType: "unknown" }; const prefix = id.split("_order_")[0]; const lastUnderscore = prefix.lastIndexOf("_"); if (lastUnderscore > 0) { return { userId: prefix.substring(0, lastUnderscore), payType: prefix.substring(lastUnderscore + 1).toLowerCase(), }; } return { userId: prefix, payType: "unknown" }; } 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: "缺少 order_id" }, { status: 400 }); } const { userId, payType } = parseOrderId(orderId); if (!userId || payType !== "salon") { return NextResponse.json({ ok: false, error: "订单格式无效" }, { status: 400 }); } const [zpay, xorpay] = await Promise.all([ queryZPayOrderStatus(orderId, 5000), queryXorPayOrderStatus(orderId, 5000), ]); const paid = zpay?.paid || xorpay?.paid; if (!paid) { return NextResponse.json({ ok: false, error: "订单未支付" }, { status: 400 }); } const moneyYuan = parseFloat(zpay?.money || xorpay?.payPrice || "0") || 0; const amountCents = moneyYuan > 0 ? Math.round(moneyYuan * 100) : 100; const token = await getAdminToken(); if (!token) { return NextResponse.json({ ok: false, error: "PocketBase 未配置" }, { status: 503 }); } const pb = getPocketBaseConfig(); const base = pb.url.replace(/\/$/, ""); const escapedUserId = userId.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); const filter = encodeURIComponent(`user_id = "${escapedUserId}"`); const listRes = await fetch( `${base}/api/collections/${pb.joinCollection}/records?filter=${filter}&sort=-created&perPage=1`, { headers: { Authorization: `Bearer ${token}` }, cache: "no-store", } ); if (!listRes.ok) { return NextResponse.json({ ok: false, error: "查询记录失败" }, { status: 500 }); } const listData = await listRes.json().catch(() => ({})); const items = Array.isArray(listData?.items) ? listData.items : []; const rec = items[0] ?? null; if (rec?.id) { const patchRes = await fetch( `${base}/api/collections/${pb.joinCollection}/records/${rec.id}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ order_id: orderId, amount: amountCents, vip: true, }), } ); if (!patchRes.ok) { const err = await patchRes.json().catch(() => ({})); console.warn("[pay/complete-order] solanRed patch failed:", patchRes.status, err); } } const siteVipFilter = encodeURIComponent( `user_id = "${escapedUserId}" && site_id = "${SITE_ID}"` ); const siteVipList = await fetch( `${base}/api/collections/site_vip/records?filter=${siteVipFilter}&perPage=1`, { headers: { Authorization: `Bearer ${token}` }, cache: "no-store", } ); if (siteVipList.ok) { const svData = await siteVipList.json().catch(() => ({})); const svItems = svData?.items ?? []; if (svItems.length > 0) { await fetch( `${base}/api/collections/site_vip/records/${svItems[0].id}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ order_id: orderId }), } ); } else { await fetch(`${base}/api/collections/site_vip/records`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ user_id: userId, site_id: SITE_ID, order_id: orderId, expires_at: "", }), }); } } return NextResponse.json({ ok: true }); } catch (error) { console.error("[pay/complete-order] error:", error); return NextResponse.json( { ok: false, error: error instanceof Error ? error.message : "操作失败" }, { status: 500 } ); } }