227 lines
8.3 KiB
TypeScript
227 lines
8.3 KiB
TypeScript
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<boolean> {
|
|
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<boolean> {
|
|
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<string | null> {
|
|
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 });
|
|
}
|
|
}
|