171 lines
6.0 KiB
TypeScript
171 lines
6.0 KiB
TypeScript
/**
|
|
* 支付确认:对齐 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<string | null> {
|
|
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) ||
|
|
process.env.PAYMENT_API_URL ||
|
|
payConfig.apiUrl
|
|
).replace(/\/$/, "");
|
|
}
|
|
|
|
async function verifyOrderPaid(request: NextRequest, orderId: string): Promise<boolean> {
|
|
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 ?? 100);
|
|
|
|
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 }
|
|
);
|
|
}
|
|
}
|