Your Name ce5daaea45
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: charge style pro image generation
2026-05-17 00:58:57 +08:00

286 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 === "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 === "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 }
);
}
}