301 lines
10 KiB
TypeScript
301 lines
10 KiB
TypeScript
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<string, string>;
|
||
}) {
|
||
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<string, string>();
|
||
|
||
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 }
|
||
);
|
||
}
|
||
}
|