'data'
This commit is contained in:
151
app/api/pay/complete-order/route.ts
Normal file
151
app/api/pay/complete-order/route.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 本地直接确认支付:检测到 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user