import { NextResponse } from "next/server"; import { and, desc, eq, inArray } from "drizzle-orm"; import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { model3dAsset, paymentOrder, systemSettings } from "@/lib/db/schema"; import { getPointsLedgerByUserId, getPointsSystemConfig, getUserById, } from "@/lib/db/queries"; import { resolveThreeDModelDisplayName } from "@/lib/model3d/model-alias-config"; import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server"; import { defaultMembershipConfig, defaultMembershipUserOverrides, getMembershipPlanById, normalizeMembershipConfig, normalizeMembershipUserOverrides, resolveMembershipPlanIdForUser, } from "@/lib/membership/config"; const MEMBERSHIP_CONFIG_KEY = "membership_config"; const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides"; const MODEL3D_LEDGER_NOTE_WITH_TASK_PATTERN = /^3D模型任务消耗积分((.+?) \/ ([^)]+))$/; const MODEL3D_LEDGER_NOTE_PATTERN = /^3D模型任务消耗积分((.+?))$/; const WORLD3D_LEDGER_NOTE_PATTERN = /^3D世界生成消耗积分((.+?))$/; function mapLedgerReasonLabel(reason: string) { if (reason === "image_generation") return "图片生成"; if (reason === "style_pro_generation") return "风格化 PRO"; if (reason === "style_pro_realistic_generation") return "风格化 PRO 实景化"; if (reason === "realtime_canvas_generation") return "实时绘画生成"; if (reason === "model3d_generation") return "3D模型生成"; if (reason === "model3d_task") return "3D模型任务"; if (reason === "model3d_task_refund") return "3D模型任务失败退回"; if (reason === "world3d_generation") return "3D世界生成"; if (reason === "video_generation") return "视频生成"; if (reason === "enhance_pro_generation") return "图像增强 Pro"; if (reason === "enhance_pro_refund") return "图像增强 Pro 失败退回"; if (reason === "deduct") return "管理员扣减"; if (reason === "gift") return "管理员赠送"; if (reason === "recharge") return "积分充值"; if (reason === "adjust") return "积分调整"; return reason || "积分变动"; } function extractWorldBillingModelId(modelId: string | null | undefined) { const normalized = typeof modelId === "string" ? modelId.trim() : ""; if (!normalized) return ""; if (!normalized.startsWith("worldlabs-3d:")) return normalized; return normalized.slice("worldlabs-3d:".length).trim(); } function formatLedgerDetail(params: { reason: string; note: string | null | undefined; modelId?: string | null; tripoModels: Array<{ id: string; displayName: string; realModelId?: string }>; worldModels: Array<{ id: string; displayName: string; realModelId?: string; billingModelId?: string; }>; tripoTaskDisplayNamesByTaskId: Map; }) { const { reason, note, modelId, tripoModels, worldModels, tripoTaskDisplayNamesByTaskId } = params; if (reason === "recharge") { return "充值积分"; } const trimmedNote = typeof note === "string" ? note.trim() : ""; if (reason === "style_pro_generation") { return "风格化 PRO 生图消耗积分"; } if (reason === "style_pro_realistic_generation") { return "风格化 PRO 实景化生图消耗积分"; } if (reason === "realtime_canvas_generation") { return "实时绘画生成消耗积分"; } if (reason === "image_angle_edit") { return "多角度编辑消耗积分"; } if (reason === "model3d_task") { const taskMatch = trimmedNote.match(MODEL3D_LEDGER_NOTE_WITH_TASK_PATTERN); if (taskMatch) { const [, rawLabel, taskId] = taskMatch; const displayName = tripoTaskDisplayNamesByTaskId.get(taskId) || resolveThreeDModelDisplayName(rawLabel, tripoModels, rawLabel); return `3D模型任务消耗积分(${displayName} / ${taskId})`; } const modelMatch = trimmedNote.match(MODEL3D_LEDGER_NOTE_PATTERN); if (modelMatch) { const [, rawLabel] = modelMatch; const displayName = resolveThreeDModelDisplayName(rawLabel, tripoModels, rawLabel); return `3D模型任务消耗积分(${displayName})`; } } if (reason === "world3d_generation") { const noteMatch = trimmedNote.match(WORLD3D_LEDGER_NOTE_PATTERN); const rawLabel = extractWorldBillingModelId(modelId) || noteMatch?.[1] || ""; const displayName = resolveThreeDModelDisplayName(rawLabel, worldModels, rawLabel); if (displayName) { return `3D世界生成消耗积分(${displayName})`; } } return trimmedNote || mapLedgerReasonLabel(reason); } export async function GET() { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } 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) .where(eq(systemSettings.key, MEMBERSHIP_CONFIG_KEY)) .limit(1) .then((rows) => rows[0]), db .select() .from(systemSettings) .where(eq(systemSettings.key, MEMBERSHIP_USER_OVERRIDES_KEY)) .limit(1) .then((rows) => rows[0]), ]); const threeDModelAliasConfig = await getThreeDModelAliasConfigForRuntime(); const model3dTaskIds = ledger .filter((item) => item.reason === "model3d_task") .map((item) => { const match = String(item.note || "").trim().match(MODEL3D_LEDGER_NOTE_WITH_TASK_PATTERN); return match?.[2] || ""; }) .filter(Boolean); const uniqueModel3dTaskIds = Array.from(new Set(model3dTaskIds)); const model3dTaskDisplayNamesByTaskId = new Map(); if (uniqueModel3dTaskIds.length > 0) { const relatedModelTasks = await db .select({ taskId: model3dAsset.taskId, modelVersion: model3dAsset.modelVersion, }) .from(model3dAsset) .where( and( eq(model3dAsset.userId, session.user.id), inArray(model3dAsset.taskId, uniqueModel3dTaskIds), ), ); relatedModelTasks.forEach((task) => { const displayName = resolveThreeDModelDisplayName( task.modelVersion, threeDModelAliasConfig.tripoModels, String(task.modelVersion || "").trim(), ); if (task.taskId && displayName) { model3dTaskDisplayNamesByTaskId.set(task.taskId, displayName); } }); } const membershipConfig = configSetting?.value ? normalizeMembershipConfig(configSetting.value) : defaultMembershipConfig; const membershipOverrides = overridesSetting?.value ? normalizeMembershipUserOverrides(overridesSetting.value) : defaultMembershipUserOverrides; const currentPlanId = resolveMembershipPlanIdForUser( session.user.id, session.user.email, membershipConfig, membershipOverrides ); const currentPlan = getMembershipPlanById(membershipConfig, currentPlanId); const usageRecords = ledger.map((item) => ({ id: item.id, detail: formatLedgerDetail({ reason: item.reason, note: item.note, modelId: item.modelId, tripoModels: threeDModelAliasConfig.tripoModels, worldModels: threeDModelAliasConfig.worldModels, tripoTaskDisplayNamesByTaskId: model3dTaskDisplayNamesByTaskId, }), status: item.amount < 0 ? "consumed" : "earned", amount: item.amount, createdAt: item.createdAt?.toISOString() || new Date().toISOString(), })); 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: "积分赠送", amount: item.amount, status: "completed", invoice: "-", })); const pointsBillingRecords = ledger .filter((item) => item.reason !== "gift" && item.reason !== "recharge") .map((item) => ({ id: item.id, date: item.createdAt?.toISOString() || new Date().toISOString(), category: formatLedgerDetail({ reason: item.reason, note: item.note, modelId: item.modelId, tripoModels: threeDModelAliasConfig.tripoModels, worldModels: threeDModelAliasConfig.worldModels, tripoTaskDisplayNamesByTaskId: model3dTaskDisplayNamesByTaskId, }), amount: item.amount, status: "completed", invoice: "-", })); const billingRecords = [ ...rechargeRecords, ...giftedRecords, ...pointsBillingRecords, ].sort((a, b) => { return new Date(b.date).getTime() - new Date(a.date).getTime(); }); return NextResponse.json({ profile: { name: dbUser?.name || session.user.name || "", email: dbUser?.email || session.user.email || "", currentPlanName: currentPlan.name, currentPlanId, availablePoints: pointsConfig.enabled ? Math.max(0, Number(dbUser?.pointsBalance || 0)) : currentPlan.monthlyCredits, monthlyCredits: currentPlan.monthlyCredits, dailyFreeCredits: currentPlan.dailyFreeCredits, }, usageRecords, billingRecords, }); } catch (error) { console.error("[Account Center API] Error:", error); return NextResponse.json( { error: "Failed to fetch account center data" }, { status: 500 } ); } }