Your Name 6cac41f1c0
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
接入DuluPay充值闭环并统一升级弹窗比例
2026-02-14 15:25:36 +08:00

188 lines
5.0 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { and, eq } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth";
import { db } from "@/lib/db";
import { paymentOrder, systemSettings } from "@/lib/db/schema";
import {
DULUPAY_CONFIG_KEY,
normalizeDulupayConfig,
} from "@/lib/payment/dulupay-config";
import {
normalizeDulupayBaseUrl,
signDulupayV1,
} from "@/lib/payment/dulupay";
import {
POINTS_PACKAGES_KEY,
normalizePointsPackages,
} from "@/lib/points/config";
export const runtime = "nodejs";
const createSchema = z.object({
packageId: z.string().trim().min(1).max(64),
payType: z.string().trim().min(1).max(32).optional(),
});
function buildOutTradeNo() {
const random = Math.random().toString(36).slice(2, 8);
return `DP${Date.now()}${random}`.slice(0, 64);
}
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = createSchema.parse(await request.json());
const [dulupaySetting, pointsPackagesSetting] = await Promise.all([
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, DULUPAY_CONFIG_KEY))
.limit(1)
.then((rows) => rows[0]),
db
.select()
.from(systemSettings)
.where(eq(systemSettings.key, POINTS_PACKAGES_KEY))
.limit(1)
.then((rows) => rows[0]),
]);
const dulupayConfig = normalizeDulupayConfig(dulupaySetting?.value);
if (!dulupayConfig.enabled) {
return NextResponse.json(
{ error: "DuluPay 未启用" },
{ status: 400 }
);
}
if (dulupayConfig.version !== "v1") {
return NextResponse.json(
{ error: "当前仅支持 DuluPay V1 支付" },
{ status: 400 }
);
}
if (!dulupayConfig.pid || !dulupayConfig.key) {
return NextResponse.json(
{ error: "DuluPay 配置不完整,请补充 PID 和 Key" },
{ status: 400 }
);
}
const packages = normalizePointsPackages(pointsPackagesSetting?.value);
const selectedPackage = packages.find(
(item) => item.id === body.packageId && item.isEnabled !== false
);
if (!selectedPackage) {
return NextResponse.json(
{ error: "积分套餐不存在或未启用" },
{ status: 404 }
);
}
if (selectedPackage.price <= 0) {
return NextResponse.json(
{ error: "该套餐金额无效" },
{ status: 400 }
);
}
const outTradeNo = buildOutTradeNo();
const amount = Number(selectedPackage.price.toFixed(2));
const amountCents = Math.round(amount * 100);
const payType = body.payType || dulupayConfig.defaultType || "alipay";
const defaultOrigin = new URL(request.url).origin;
const notifyUrl =
dulupayConfig.notifyUrl || `${defaultOrigin}/api/payment/dulupay/notify`;
const returnUrl = dulupayConfig.returnUrl || `${defaultOrigin}/`;
const baseParams = {
pid: dulupayConfig.pid,
type: payType,
out_trade_no: outTradeNo,
notify_url: notifyUrl,
return_url: returnUrl,
name: `积分充值-${selectedPackage.name}`,
money: amount.toFixed(2),
param: `${session.user.id}:${selectedPackage.id}`,
};
const sign = signDulupayV1(baseParams, dulupayConfig.key);
const params = new URLSearchParams({
...baseParams,
sign,
sign_type: "MD5",
});
const paymentUrl = `${normalizeDulupayBaseUrl(
dulupayConfig.baseUrl
)}/submit.php?${params.toString()}`;
const existing = await db
.select({ id: paymentOrder.id })
.from(paymentOrder)
.where(
and(
eq(paymentOrder.outTradeNo, outTradeNo),
eq(paymentOrder.provider, "dulupay")
)
)
.limit(1)
.then((rows) => rows[0]);
if (existing) {
return NextResponse.json({ error: "订单创建失败,请重试" }, { status: 500 });
}
const [created] = await db
.insert(paymentOrder)
.values({
userId: session.user.id,
provider: "dulupay",
outTradeNo,
packageId: selectedPackage.id,
points: selectedPackage.points,
amountCents,
currency: selectedPackage.currency,
payType,
status: "pending",
paymentUrl,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning({
id: paymentOrder.id,
});
return NextResponse.json({
orderId: created.id,
outTradeNo,
paymentUrl,
amount,
currency: selectedPackage.currency,
points: selectedPackage.points,
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: "请求参数错误",
details: error.errors,
},
{ status: 400 }
);
}
console.error("[DuluPay Create] Error:", error);
return NextResponse.json({ error: "创建支付订单失败" }, { status: 500 });
}
}