diff --git a/app/api/payment/dulupay/create/route.ts b/app/api/payment/dulupay/create/route.ts
new file mode 100644
index 000000000..08ba734d9
--- /dev/null
+++ b/app/api/payment/dulupay/create/route.ts
@@ -0,0 +1,187 @@
+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 });
+ }
+}
diff --git a/app/api/payment/dulupay/notify/route.ts b/app/api/payment/dulupay/notify/route.ts
new file mode 100644
index 000000000..bf5095d42
--- /dev/null
+++ b/app/api/payment/dulupay/notify/route.ts
@@ -0,0 +1,215 @@
+import { NextRequest, NextResponse } from "next/server";
+import { and, eq, ne, sql } from "drizzle-orm";
+import { db } from "@/lib/db";
+import { paymentOrder, pointsLedger, systemSettings, user } from "@/lib/db/schema";
+import {
+ DULUPAY_CONFIG_KEY,
+ normalizeDulupayConfig,
+} from "@/lib/payment/dulupay-config";
+import { verifyDulupayV1Sign } from "@/lib/payment/dulupay";
+
+export const runtime = "nodejs";
+
+function plainText(text: string, status = 200) {
+ return new NextResponse(text, {
+ status,
+ headers: {
+ "Content-Type": "text/plain; charset=utf-8",
+ "Cache-Control": "no-store",
+ },
+ });
+}
+
+async function readNotifyParams(request: NextRequest) {
+ const url = new URL(request.url);
+ const record: Record
= {};
+
+ url.searchParams.forEach((value, key) => {
+ record[key] = value;
+ });
+
+ if (request.method === "GET") {
+ return record;
+ }
+
+ const contentType = request.headers.get("content-type") || "";
+
+ if (contentType.includes("application/json")) {
+ const data = (await request.json().catch(() => null)) as
+ | Record
+ | null;
+ if (data) {
+ for (const [key, value] of Object.entries(data)) {
+ if (value !== undefined && value !== null) {
+ record[key] = String(value);
+ }
+ }
+ }
+ return record;
+ }
+
+ const text = await request.text();
+ if (!text) return record;
+
+ const formParams = new URLSearchParams(text);
+ formParams.forEach((value, key) => {
+ record[key] = value;
+ });
+
+ return record;
+}
+
+async function handleNotify(request: NextRequest) {
+ const params = await readNotifyParams(request);
+ const outTradeNo = String(params.out_trade_no || "").trim();
+ const tradeNo = String(params.trade_no || "").trim();
+ const tradeStatus = String(params.trade_status || "").trim();
+ const moneyRaw = String(params.money || "").trim();
+
+ if (!outTradeNo) {
+ return plainText("fail", 400);
+ }
+
+ const setting = await db
+ .select()
+ .from(systemSettings)
+ .where(eq(systemSettings.key, DULUPAY_CONFIG_KEY))
+ .limit(1)
+ .then((rows) => rows[0]);
+
+ const config = normalizeDulupayConfig(setting?.value);
+ if (!config.enabled || config.version !== "v1" || !config.key) {
+ return plainText("fail", 400);
+ }
+
+ if (!verifyDulupayV1Sign(params, config.key)) {
+ return plainText("fail", 400);
+ }
+
+ const order = await db
+ .select({
+ id: paymentOrder.id,
+ userId: paymentOrder.userId,
+ status: paymentOrder.status,
+ points: paymentOrder.points,
+ amountCents: paymentOrder.amountCents,
+ })
+ .from(paymentOrder)
+ .where(
+ and(
+ eq(paymentOrder.outTradeNo, outTradeNo),
+ eq(paymentOrder.provider, "dulupay")
+ )
+ )
+ .limit(1)
+ .then((rows) => rows[0]);
+
+ if (!order) {
+ return plainText("fail", 404);
+ }
+
+ const callbackAmountCents = Number.isFinite(Number(moneyRaw))
+ ? Math.round(Number(moneyRaw) * 100)
+ : null;
+
+ if (
+ callbackAmountCents !== null &&
+ callbackAmountCents > 0 &&
+ callbackAmountCents !== order.amountCents
+ ) {
+ await db
+ .update(paymentOrder)
+ .set({
+ status: "failed",
+ tradeNo: tradeNo || null,
+ errorMessage: `回调金额不一致: ${moneyRaw}`,
+ rawCallback: params,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(paymentOrder.id, order.id), ne(paymentOrder.status, "paid")));
+
+ return plainText("fail", 400);
+ }
+
+ if (tradeStatus === "TRADE_SUCCESS") {
+ await db.transaction(async (tx) => {
+ const updated = await tx
+ .update(paymentOrder)
+ .set({
+ status: "paid",
+ tradeNo: tradeNo || null,
+ rawCallback: params,
+ errorMessage: null,
+ paidAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(
+ and(eq(paymentOrder.id, order.id), ne(paymentOrder.status, "paid"))
+ )
+ .returning({
+ id: paymentOrder.id,
+ userId: paymentOrder.userId,
+ points: paymentOrder.points,
+ outTradeNo: paymentOrder.outTradeNo,
+ });
+
+ if (updated.length === 0) {
+ return;
+ }
+
+ const paidOrder = updated[0];
+
+ if (paidOrder.points > 0) {
+ await tx
+ .update(user)
+ .set({
+ pointsBalance: sql`${user.pointsBalance} + ${paidOrder.points}`,
+ updatedAt: new Date(),
+ })
+ .where(eq(user.id, paidOrder.userId));
+
+ await tx.insert(pointsLedger).values({
+ userId: paidOrder.userId,
+ amount: paidOrder.points,
+ reason: "recharge",
+ note: `DuluPay 充值 (${paidOrder.outTradeNo})`,
+ });
+ }
+ });
+
+ return plainText("success");
+ }
+
+ const mappedStatus = tradeStatus === "TRADE_CLOSED" ? "closed" : "failed";
+
+ await db
+ .update(paymentOrder)
+ .set({
+ status: mappedStatus,
+ tradeNo: tradeNo || null,
+ errorMessage: tradeStatus ? `trade_status=${tradeStatus}` : "支付失败",
+ rawCallback: params,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(paymentOrder.id, order.id), ne(paymentOrder.status, "paid")));
+
+ return plainText("success");
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ return await handleNotify(request);
+ } catch (error) {
+ console.error("[DuluPay Notify] GET error:", error);
+ return plainText("fail", 500);
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ return await handleNotify(request);
+ } catch (error) {
+ console.error("[DuluPay Notify] POST error:", error);
+ return plainText("fail", 500);
+ }
+}
diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts
index 9f10bb220..c50ff8ef8 100644
--- a/app/api/site-settings/route.ts
+++ b/app/api/site-settings/route.ts
@@ -18,6 +18,12 @@ import {
POINTS_PACKAGES_KEY,
POINTS_SYSTEM_CONFIG_KEY,
} from "@/lib/points/config";
+import {
+ defaultDulupayConfig,
+ DULUPAY_CONFIG_KEY,
+ normalizeDulupayConfig,
+ normalizeDulupayForPublic,
+} from "@/lib/payment/dulupay-config";
const HOME_VARIANT_KEY = "home_variant";
// Historically stored as "home_surprise_categories" but used for the featured gallery categories.
@@ -85,6 +91,8 @@ function normalizeLovartComposerPrefix(value: unknown): string {
// GET /api/site-settings - 获取网站设置 (公开)
export async function GET() {
try {
+ const session = await auth().catch(() => null);
+ const isAdmin = session?.user?.type === "admin";
const [
settings,
notificationSetting,
@@ -96,6 +104,7 @@ export async function GET() {
membershipOverridesSetting,
pointsSystemConfigSetting,
pointsPackagesSetting,
+ dulupayConfigSetting,
] = await Promise.all([
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
db
@@ -152,6 +161,12 @@ export async function GET() {
.where(eq(systemSettings.key, POINTS_PACKAGES_KEY))
.limit(1)
.then(rows => rows[0]),
+ db
+ .select()
+ .from(systemSettings)
+ .where(eq(systemSettings.key, DULUPAY_CONFIG_KEY))
+ .limit(1)
+ .then(rows => rows[0]),
]);
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
@@ -171,6 +186,9 @@ export async function GET() {
pointsSystemConfigSetting?.value
);
const pointsPackages = normalizePointsPackages(pointsPackagesSetting?.value);
+ const dulupayConfig = isAdmin
+ ? normalizeDulupayConfig(dulupayConfigSetting?.value)
+ : normalizeDulupayForPublic(dulupayConfigSetting?.value);
// 如果没有设置记录,返回默认值
const baseSettings = settings || {
@@ -209,6 +227,7 @@ export async function GET() {
membershipUserOverrides,
pointsSystemConfig,
pointsPackages,
+ dulupayConfig,
};
const response = NextResponse.json({
@@ -228,6 +247,7 @@ export async function GET() {
membershipUserOverrides,
pointsSystemConfig,
pointsPackages,
+ dulupayConfig,
});
// 禁用缓存,确保总是获取最新设置
@@ -281,6 +301,7 @@ const updateSchema = z.object({
membershipUserOverrides: z.unknown().optional(),
pointsSystemConfig: z.unknown().optional(),
pointsPackages: z.unknown().optional(),
+ dulupayConfig: z.unknown().optional(),
});
// PUT /api/site-settings - 更新网站设置(仅管理员)
@@ -307,6 +328,7 @@ export async function PUT(request: NextRequest) {
membershipUserOverrides,
pointsSystemConfig,
pointsPackages,
+ dulupayConfig,
...siteSettingsPayload
} = data;
@@ -484,6 +506,26 @@ export async function PUT(request: NextRequest) {
});
}
+ if (dulupayConfig !== undefined) {
+ const normalized = normalizeDulupayConfig(dulupayConfig);
+ await db
+ .insert(systemSettings)
+ .values({
+ key: DULUPAY_CONFIG_KEY,
+ value: normalized,
+ updatedAt: new Date(),
+ updatedBy: session.user.id,
+ })
+ .onConflictDoUpdate({
+ target: systemSettings.key,
+ set: {
+ value: normalized,
+ updatedAt: new Date(),
+ updatedBy: session.user.id,
+ },
+ });
+ }
+
return NextResponse.json({
...updated,
homeVariant: homeVariant ?? normalizeHomeVariant(undefined),
@@ -515,6 +557,10 @@ export async function PUT(request: NextRequest) {
pointsPackages !== undefined
? normalizePointsPackages(pointsPackages)
: defaultPointsPackages,
+ dulupayConfig:
+ dulupayConfig !== undefined
+ ? normalizeDulupayConfig(dulupayConfig)
+ : defaultDulupayConfig,
});
} else {
// 创建新设置
@@ -713,6 +759,26 @@ export async function PUT(request: NextRequest) {
});
}
+ if (dulupayConfig !== undefined) {
+ const normalized = normalizeDulupayConfig(dulupayConfig);
+ await db
+ .insert(systemSettings)
+ .values({
+ key: DULUPAY_CONFIG_KEY,
+ value: normalized,
+ updatedAt: new Date(),
+ updatedBy: session.user.id,
+ })
+ .onConflictDoUpdate({
+ target: systemSettings.key,
+ set: {
+ value: normalized,
+ updatedAt: new Date(),
+ updatedBy: session.user.id,
+ },
+ });
+ }
+
return NextResponse.json({
...created,
homeVariant: homeVariant ?? normalizeHomeVariant(undefined),
@@ -744,6 +810,10 @@ export async function PUT(request: NextRequest) {
pointsPackages !== undefined
? normalizePointsPackages(pointsPackages)
: defaultPointsPackages,
+ dulupayConfig:
+ dulupayConfig !== undefined
+ ? normalizeDulupayConfig(dulupayConfig)
+ : defaultDulupayConfig,
});
}
} catch (error) {
diff --git a/app/api/user/account-center/route.ts b/app/api/user/account-center/route.ts
index 89d675944..e9344d7df 100644
--- a/app/api/user/account-center/route.ts
+++ b/app/api/user/account-center/route.ts
@@ -1,8 +1,8 @@
import { NextResponse } from "next/server";
-import { eq } from "drizzle-orm";
+import { desc, eq } from "drizzle-orm";
import { auth } from "@/app/(auth)/auth";
import { db } from "@/lib/db";
-import { systemSettings } from "@/lib/db/schema";
+import { paymentOrder, systemSettings } from "@/lib/db/schema";
import {
getPointsLedgerByUserId,
getPointsSystemConfig,
@@ -35,11 +35,29 @@ export async function GET() {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
- const [dbUser, pointsConfig, ledger, configSetting, overridesSetting] =
+ const [
+ dbUser,
+ pointsConfig,
+ ledger,
+ paymentOrders,
+ configSetting,
+ overridesSetting,
+ ] =
await Promise.all([
getUserById({ id: session.user.id }),
getPointsSystemConfig(),
getPointsLedgerByUserId({ userId: session.user.id, limit: 200 }),
+ db
+ .select({
+ id: paymentOrder.id,
+ amountCents: paymentOrder.amountCents,
+ status: paymentOrder.status,
+ createdAt: paymentOrder.createdAt,
+ })
+ .from(paymentOrder)
+ .where(eq(paymentOrder.userId, session.user.id))
+ .orderBy(desc(paymentOrder.createdAt))
+ .limit(100),
db
.select()
.from(systemSettings)
@@ -76,17 +94,35 @@ export async function GET() {
createdAt: item.createdAt?.toISOString() || new Date().toISOString(),
}));
- const billingRecords = ledger
- .filter((item) => item.reason === "recharge" || item.reason === "gift")
+ const rechargeRecords = paymentOrders.map((order) => ({
+ id: order.id,
+ date: order.createdAt?.toISOString() || new Date().toISOString(),
+ category: "积分充值",
+ amount: Number((order.amountCents / 100).toFixed(2)),
+ status:
+ order.status === "paid"
+ ? "completed"
+ : order.status === "pending"
+ ? "pending"
+ : "failed",
+ invoice: "-",
+ }));
+
+ const giftedRecords = ledger
+ .filter((item) => item.reason === "gift")
.map((item) => ({
id: item.id,
date: item.createdAt?.toISOString() || new Date().toISOString(),
- category: item.reason === "recharge" ? "积分充值" : "积分赠送",
+ category: "积分赠送",
amount: item.amount,
status: "completed",
invoice: "-",
}));
+ const billingRecords = [...rechargeRecords, ...giftedRecords].sort((a, b) => {
+ return new Date(b.date).getTime() - new Date(a.date).getTime();
+ });
+
return NextResponse.json({
profile: {
name: dbUser?.name || session.user.name || "",
@@ -110,4 +146,3 @@ export async function GET() {
);
}
}
-
diff --git a/app/api/user/payment-orders/route.ts b/app/api/user/payment-orders/route.ts
new file mode 100644
index 000000000..485ab6979
--- /dev/null
+++ b/app/api/user/payment-orders/route.ts
@@ -0,0 +1,44 @@
+import { NextResponse } from "next/server";
+import { desc, eq } from "drizzle-orm";
+import { auth } from "@/app/(auth)/auth";
+import { db } from "@/lib/db";
+import { paymentOrder } from "@/lib/db/schema";
+
+export async function GET() {
+ try {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const orders = await db
+ .select({
+ id: paymentOrder.id,
+ provider: paymentOrder.provider,
+ outTradeNo: paymentOrder.outTradeNo,
+ tradeNo: paymentOrder.tradeNo,
+ packageId: paymentOrder.packageId,
+ points: paymentOrder.points,
+ amountCents: paymentOrder.amountCents,
+ currency: paymentOrder.currency,
+ payType: paymentOrder.payType,
+ status: paymentOrder.status,
+ paidAt: paymentOrder.paidAt,
+ createdAt: paymentOrder.createdAt,
+ })
+ .from(paymentOrder)
+ .where(eq(paymentOrder.userId, session.user.id))
+ .orderBy(desc(paymentOrder.createdAt))
+ .limit(200);
+
+ return NextResponse.json({
+ orders: orders.map((order) => ({
+ ...order,
+ amount: Number((order.amountCents / 100).toFixed(2)),
+ })),
+ });
+ } catch (error) {
+ console.error("[Payment Orders API] Error:", error);
+ return NextResponse.json({ error: "Failed to fetch payment orders" }, { status: 500 });
+ }
+}
diff --git a/components/home/lovart-home.tsx b/components/home/lovart-home.tsx
index e92c81903..3b651d759 100644
--- a/components/home/lovart-home.tsx
+++ b/components/home/lovart-home.tsx
@@ -133,6 +133,15 @@ type MembershipSummaryResponse = {
};
};
+type PointsPackage = {
+ id: string;
+ name: string;
+ points: number;
+ price: number;
+ currency: string;
+ isEnabled: boolean;
+};
+
type AccountCenterResponse = {
profile: {
name: string;
@@ -300,6 +309,7 @@ export function LovartHome({
);
const [usageFilter, setUsageFilter] = useState<"all" | "consumed" | "earned">("all");
const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">("monthly");
+ const [creatingPaymentPackageId, setCreatingPaymentPackageId] = useState(null);
const [selectedModelId, setSelectedModelId] = useState("");
const [imageParams, setImageParams] = useState({
aspectRatio: "auto",
@@ -367,6 +377,11 @@ export function LovartHome({
toolbox1Enabled?: boolean;
toolbox2Name?: string | null;
toolbox2Enabled?: boolean;
+ pointsPackages?: PointsPackage[];
+ dulupayConfig?: {
+ enabled?: boolean;
+ defaultType?: string;
+ };
}>("/api/site-settings", fetcher);
const { data: authSettingsData } = useSWR(
@@ -505,6 +520,12 @@ export function LovartHome({
return records.filter((item) => item.status === usageFilter);
}, [accountCenterData?.usageRecords, usageFilter]);
+ const enabledPointsPackages = useMemo(() => {
+ return (siteSettingsData?.pointsPackages || []).filter(
+ (item) => item?.isEnabled !== false && item.price > 0 && item.points > 0
+ );
+ }, [siteSettingsData?.pointsPackages]);
+
const toolbox1Links = useMemo(
() =>
(navLinksData?.links || [])
@@ -768,6 +789,38 @@ export function LovartHome({
window.location.href = "/login";
};
+ const handleCreateDulupayOrder = async (packageId: string) => {
+ if (!isAuthenticated) {
+ openLoginDialog();
+ return;
+ }
+
+ if (creatingPaymentPackageId) return;
+
+ setCreatingPaymentPackageId(packageId);
+ try {
+ const response = await fetch("/api/payment/dulupay/create", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ packageId,
+ payType: siteSettingsData?.dulupayConfig?.defaultType || "alipay",
+ }),
+ });
+ const result = await response.json();
+
+ if (!response.ok || !result?.paymentUrl) {
+ throw new Error(result?.error || "创建支付订单失败");
+ }
+
+ window.location.href = result.paymentUrl;
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : "创建支付订单失败");
+ } finally {
+ setCreatingPaymentPackageId(null);
+ }
+ };
+
const createCanvasFromComposer = async () => {
if (!isAuthenticated) {
openLoginDialog();
@@ -1580,29 +1633,29 @@ export function LovartHome({
) : null}