269 lines
7.6 KiB
TypeScript
269 lines
7.6 KiB
TypeScript
import { and, desc, eq } from "drizzle-orm";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import { db } from "@/lib/db";
|
|
import { canvasProject, model3dAsset } from "@/lib/db/schema";
|
|
|
|
type Model3DTaskStatus =
|
|
| "queued"
|
|
| "running"
|
|
| "success"
|
|
| "failed"
|
|
| "banned"
|
|
| "expired"
|
|
| "cancelled"
|
|
| "unknown";
|
|
|
|
type Model3DTaskMode =
|
|
| "text"
|
|
| "image"
|
|
| "multiview"
|
|
| "batch"
|
|
| "convert"
|
|
| "refine"
|
|
| "texture"
|
|
| "stylize"
|
|
| "rigcheck"
|
|
| "rig"
|
|
| "retarget"
|
|
| "segment"
|
|
| "complete"
|
|
| "lowpoly";
|
|
|
|
const FINAL_TASK_STATUS = new Set<Model3DTaskStatus>([
|
|
"queued",
|
|
"running",
|
|
"success",
|
|
"failed",
|
|
"banned",
|
|
"expired",
|
|
"cancelled",
|
|
"unknown",
|
|
]);
|
|
|
|
const TASK_MODE_SET = new Set<Model3DTaskMode>([
|
|
"text",
|
|
"image",
|
|
"multiview",
|
|
"batch",
|
|
"convert",
|
|
"refine",
|
|
"texture",
|
|
"stylize",
|
|
"rigcheck",
|
|
"rig",
|
|
"retarget",
|
|
"segment",
|
|
"complete",
|
|
"lowpoly",
|
|
]);
|
|
|
|
const TASK_LIST_LIMIT = 120;
|
|
|
|
function normalizeTaskStatus(input: unknown): Model3DTaskStatus {
|
|
const value = String(input || "").trim() as Model3DTaskStatus;
|
|
return FINAL_TASK_STATUS.has(value) ? value : "unknown";
|
|
}
|
|
|
|
function normalizeTaskMode(input: unknown): Model3DTaskMode {
|
|
const value = String(input || "").trim() as Model3DTaskMode;
|
|
return TASK_MODE_SET.has(value) ? value : "text";
|
|
}
|
|
|
|
function readOptionalText(input: unknown): string | null {
|
|
const normalized = String(input || "").trim();
|
|
return normalized ? normalized : null;
|
|
}
|
|
|
|
function readProgress(input: unknown): number | null {
|
|
if (input === undefined || input === null || input === "") return null;
|
|
const value = Number(input);
|
|
if (!Number.isFinite(value)) return null;
|
|
return Math.max(0, Math.min(100, Math.round(value)));
|
|
}
|
|
|
|
function readDate(input: unknown): Date | null {
|
|
if (!input) return null;
|
|
if (input instanceof Date && !Number.isNaN(input.getTime())) return input;
|
|
if (typeof input === "number" && Number.isFinite(input)) {
|
|
const fromNumber = new Date(input);
|
|
if (!Number.isNaN(fromNumber.getTime())) return fromNumber;
|
|
}
|
|
if (typeof input === "string" && input.trim()) {
|
|
const fromText = new Date(input);
|
|
if (!Number.isNaN(fromText.getTime())) return fromText;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function ensureProjectOwnership(projectId: string, userId: string) {
|
|
const [project] = await db
|
|
.select({ id: canvasProject.id })
|
|
.from(canvasProject)
|
|
.where(and(eq(canvasProject.id, projectId), eq(canvasProject.userId, userId)))
|
|
.limit(1);
|
|
return Boolean(project);
|
|
}
|
|
|
|
export async function GET(
|
|
_request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const { id: projectId } = await params;
|
|
const ownsProject = await ensureProjectOwnership(projectId, session.user.id);
|
|
if (!ownsProject) {
|
|
return NextResponse.json({ success: false, error: "画布不存在或无权限" }, { status: 404 });
|
|
}
|
|
|
|
const assets = await db
|
|
.select()
|
|
.from(model3dAsset)
|
|
.where(
|
|
and(
|
|
eq(model3dAsset.projectId, projectId),
|
|
eq(model3dAsset.userId, session.user.id)
|
|
)
|
|
)
|
|
.orderBy(desc(model3dAsset.updatedAt))
|
|
.limit(TASK_LIST_LIMIT);
|
|
|
|
return NextResponse.json({ success: true, data: assets });
|
|
} catch (error) {
|
|
console.error("[Model3DAssets] GET failed:", error);
|
|
return NextResponse.json({ success: false, error: "获取 3D 资产失败" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const { id: projectId } = await params;
|
|
const ownsProject = await ensureProjectOwnership(projectId, session.user.id);
|
|
if (!ownsProject) {
|
|
return NextResponse.json({ success: false, error: "画布不存在或无权限" }, { status: 404 });
|
|
}
|
|
|
|
const body = await request.json().catch(() => ({}));
|
|
const rawAsset = ((body as any)?.asset || body || {}) as Record<string, unknown>;
|
|
const taskId = String(rawAsset.taskId || "").trim();
|
|
if (!taskId) {
|
|
return NextResponse.json({ success: false, error: "缺少 taskId" }, { status: 400 });
|
|
}
|
|
|
|
const status = normalizeTaskStatus(rawAsset.status);
|
|
const mode = normalizeTaskMode(rawAsset.mode);
|
|
const prompt = String(rawAsset.prompt || "").trim();
|
|
const modelVersion = String(rawAsset.modelVersion || "").trim();
|
|
const progress = readProgress(rawAsset.progress);
|
|
const modelUrl = readOptionalText(rawAsset.modelUrl);
|
|
const previewImageUrl = readOptionalText(rawAsset.previewImageUrl);
|
|
const error = readOptionalText(rawAsset.error);
|
|
const sourceTaskId = readOptionalText(rawAsset.sourceTaskId);
|
|
const exportFormat = readOptionalText(rawAsset.exportFormat);
|
|
const createdAt = readDate(rawAsset.createdAt);
|
|
|
|
const now = new Date();
|
|
const [saved] = await db
|
|
.insert(model3dAsset)
|
|
.values({
|
|
userId: session.user.id,
|
|
projectId,
|
|
taskId,
|
|
status,
|
|
mode,
|
|
prompt,
|
|
modelVersion,
|
|
progress,
|
|
modelUrl,
|
|
previewImageUrl,
|
|
error,
|
|
sourceTaskId,
|
|
exportFormat,
|
|
createdAt: createdAt || now,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: [model3dAsset.projectId, model3dAsset.taskId],
|
|
set: {
|
|
userId: session.user.id,
|
|
status,
|
|
mode,
|
|
prompt,
|
|
modelVersion,
|
|
progress,
|
|
modelUrl,
|
|
previewImageUrl,
|
|
error,
|
|
sourceTaskId,
|
|
exportFormat,
|
|
updatedAt: now,
|
|
},
|
|
})
|
|
.returning();
|
|
|
|
return NextResponse.json({ success: true, data: saved });
|
|
} catch (error) {
|
|
console.error("[Model3DAssets] POST failed:", error);
|
|
return NextResponse.json({ success: false, error: "保存 3D 资产失败" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function DELETE(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const { id: projectId } = await params;
|
|
const ownsProject = await ensureProjectOwnership(projectId, session.user.id);
|
|
if (!ownsProject) {
|
|
return NextResponse.json({ success: false, error: "画布不存在或无权限" }, { status: 404 });
|
|
}
|
|
|
|
const searchTaskId = request.nextUrl.searchParams.get("taskId");
|
|
let bodyTaskId = "";
|
|
if (!searchTaskId) {
|
|
const body = await request.json().catch(() => ({}));
|
|
bodyTaskId = String((body as any)?.taskId || "").trim();
|
|
}
|
|
const taskId = String(searchTaskId || bodyTaskId || "").trim();
|
|
|
|
const conditions = [
|
|
eq(model3dAsset.projectId, projectId),
|
|
eq(model3dAsset.userId, session.user.id),
|
|
];
|
|
if (taskId) {
|
|
conditions.push(eq(model3dAsset.taskId, taskId));
|
|
}
|
|
|
|
const deleted = await db
|
|
.delete(model3dAsset)
|
|
.where(and(...conditions))
|
|
.returning({ id: model3dAsset.id });
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: { deletedCount: deleted.length },
|
|
});
|
|
} catch (error) {
|
|
console.error("[Model3DAssets] DELETE failed:", error);
|
|
return NextResponse.json({ success: false, error: "清理 3D 资产失败" }, { status: 500 });
|
|
}
|
|
}
|