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); } function normalizeBaseUrl(value?: string | null) { const raw = String(value || "").trim(); if (!raw) return ""; try { const parsed = new URL(raw); return `${parsed.protocol}//${parsed.host}`; } catch { return ""; } } function resolvePublicOrigin(request: NextRequest) { const fromEnv = normalizeBaseUrl(process.env.AUTH_URL) || normalizeBaseUrl(process.env.NEXTAUTH_URL); if (fromEnv) return fromEnv; const forwardedHost = request.headers.get("x-forwarded-host"); const host = request.headers.get("host"); const resolvedHost = String(forwardedHost || host || "").trim(); if (!resolvedHost) { return new URL(request.url).origin; } const forwardedProto = request.headers.get("x-forwarded-proto"); const protocol = String(forwardedProto || "").trim() || "https"; return `${protocol}://${resolvedHost}`; } export async function POST(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id || session.user.type === "guest") { return NextResponse.json({ error: "请先登录正式账号后再充值" }, { 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 = resolvePublicOrigin(request); 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 }); } }