import { NextRequest, NextResponse } from "next/server"; import { db } from "@/lib/db"; import { chat, generatedImage, message, user } from "@/lib/db/schema"; import { eq, desc, sql, and, gte, lte, like, or, isNotNull } from "drizzle-orm"; import { auth } from "@/app/(auth)/auth"; function extractVideoUrlFromParts(parts: unknown): string | null { const text = Array.isArray(parts) ? parts .map((part) => (part && typeof part === "object" && "text" in part ? String((part as any).text || "") : "")) .join("\n") : typeof parts === "string" ? parts : ""; if (!text) return null; const htmlMatch = text.match(/]+src="([^"]+)"/i); if (htmlMatch?.[1]) return htmlMatch[1]; const markdownMatch = text.match(/\[[^\]]*]\((https?:\/\/[^)\s]+\.mp4(?:\?[^)\s]*)?)\)/i); if (markdownMatch?.[1]) return markdownMatch[1]; const plainMatch = text.match(/(https?:\/\/[^\s"'<>]+\.mp4(?:\?[^\s"'<>]+)?)/i); return plainMatch?.[1] || null; } export async function GET(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // 检查是否为管理员 const currentUser = await db.query.user.findFirst({ where: eq(user.id, session.user.id), }); if (currentUser?.role !== "admin") { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } // 获取查询参数 const searchParams = request.nextUrl.searchParams; const page = Number.parseInt(searchParams.get("page") || "1"); const pageSize = Number.parseInt(searchParams.get("pageSize") || "200"); const offset = (page - 1) * pageSize; const search = searchParams.get("search") || ""; const startDate = searchParams.get("startDate"); const endDate = searchParams.get("endDate"); // 构建查询条件 const imageConditions = []; const videoConditions = [ eq(message.role, "assistant"), or( isNotNull(message.temporaryVideoUrl), isNotNull(message.permanentVideoUrl), sql`${message.parts}::text ILIKE '% 0) { const matchedUserIds = matchingUsers.map((u) => u.id); imageConditions.push( sql`${generatedImage.userId} IN (${sql.join(matchedUserIds.map((id) => sql`${id}`), sql`, `)})` ); videoConditions.push( sql`${chat.userId} IN (${sql.join(matchedUserIds.map((id) => sql`${id}`), sql`, `)})` ); } else { // 没有匹配的用户,返回空结果 return NextResponse.json({ images: [], pagination: { page, pageSize, total: 0, totalPages: 0 } }); } } if (startDate) { const start = new Date(startDate); imageConditions.push(gte(generatedImage.createdAt, start)); videoConditions.push(gte(message.createdAt, start)); } if (endDate) { const end = new Date(endDate); end.setHours(23, 59, 59, 999); imageConditions.push(lte(generatedImage.createdAt, end)); videoConditions.push(lte(message.createdAt, end)); } const imageWhereClause = imageConditions.length > 0 ? and(...imageConditions) : undefined; const videoWhereClause = videoConditions.length > 0 ? and(...videoConditions) : undefined; // 查询图片总数 const imageCountResult = await db .select({ count: sql`count(*)::int` }) .from(generatedImage) .where(imageWhereClause); // 查询视频总数 const videoCountResult = await db .select({ count: sql`count(*)::int` }) .from(message) .innerJoin(chat, eq(message.chatId, chat.id)) .where(videoWhereClause); const totalImages = imageCountResult[0]?.count || 0; const totalVideos = videoCountResult[0]?.count || 0; const total = totalImages + totalVideos; const totalPages = Math.ceil(total / pageSize); const fetchLimit = Math.max(page * pageSize, pageSize); // 查询图片列表(包含用户信息) const imageRows = await db .select({ id: generatedImage.id, userId: generatedImage.userId, userName: user.name, userEmail: user.email, userDepartment: user.department, thumbnailUrl: generatedImage.thumbnailUrl, originalUrl: generatedImage.originalUrl, imageSize: generatedImage.imageSize, prompt: generatedImage.prompt, modelId: generatedImage.modelId, isFeatured: generatedImage.isFeatured, featuredCategory: generatedImage.featuredCategory, featuredAt: generatedImage.featuredAt, createdAt: generatedImage.createdAt, chatId: generatedImage.chatId, recordType: sql<"image">`'image'`, videoUrl: sql`null`, }) .from(generatedImage) .leftJoin(user, eq(generatedImage.userId, user.id)) .where(imageWhereClause) .orderBy(desc(generatedImage.createdAt)) .limit(fetchLimit); // 查询视频列表(来自消息表) const videoRows = await db .select({ id: message.id, userId: chat.userId, userName: user.name, userEmail: user.email, userDepartment: user.department, temporaryVideoUrl: message.temporaryVideoUrl, permanentVideoUrl: message.permanentVideoUrl, parts: message.parts, videoMetadata: message.videoMetadata, createdAt: message.createdAt, chatId: message.chatId, }) .from(message) .innerJoin(chat, eq(message.chatId, chat.id)) .leftJoin(user, eq(chat.userId, user.id)) .where(videoWhereClause) .orderBy(desc(message.createdAt)) .limit(fetchLimit); const normalizedVideos = videoRows .map((item) => { const metadata = (item.videoMetadata as Record | null) || {}; const videoUrl = item.permanentVideoUrl || item.temporaryVideoUrl || extractVideoUrlFromParts(item.parts); if (!videoUrl) return null; const resolution = typeof metadata.resolution === "string" && metadata.resolution.trim() ? metadata.resolution.trim() : null; const prompt = typeof metadata.prompt === "string" && metadata.prompt.trim() ? metadata.prompt.trim() : null; const modelId = typeof metadata.model === "string" && metadata.model.trim() ? metadata.model.trim() : null; return { id: `video-${item.id}`, sourceMessageId: item.id, userId: item.userId, userName: item.userName, userEmail: item.userEmail, userDepartment: item.userDepartment, thumbnailUrl: videoUrl, originalUrl: videoUrl, videoUrl, imageSize: resolution, prompt, modelId, isFeatured: false, featuredCategory: null, featuredAt: null, createdAt: item.createdAt, chatId: item.chatId, recordType: "video" as const, }; }) .filter((item): item is NonNullable => Boolean(item)); const merged = [...imageRows, ...normalizedVideos].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); const images = merged.slice(offset, offset + pageSize); return NextResponse.json({ images, pagination: { page, pageSize, total, totalPages, totalImages, totalVideos } }); } catch (error) { console.error("[Admin Images API] Error:", error); return NextResponse.json({ error: "Failed to fetch images" }, { status: 500 }); } } // PATCH: 设置/取消图片精选 export async function PATCH(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // 检查是否为管理员 const currentUser = await db.query.user.findFirst({ where: eq(user.id, session.user.id), }); if (currentUser?.role !== "admin") { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const body = await request.json(); const { imageId, isFeatured, featuredCategory } = body; if (!imageId) { return NextResponse.json({ error: "Image ID is required" }, { status: 400 }); } const updateData: Record = {}; const shouldUpdateFeaturedState = typeof isFeatured === "boolean"; const shouldUpdateCategory = Object.prototype.hasOwnProperty.call(body, "featuredCategory"); if (!shouldUpdateFeaturedState && !shouldUpdateCategory) { return NextResponse.json({ error: "No valid fields to update" }, { status: 400 }); } // 更新精选状态 if (shouldUpdateFeaturedState) { updateData.isFeatured = isFeatured; if (isFeatured) { updateData.featuredAt = new Date(); updateData.featuredBy = session.user.id; } else { updateData.featuredAt = null; updateData.featuredBy = null; } } if (shouldUpdateCategory) { const normalizedCategory = typeof featuredCategory === "string" && featuredCategory.trim().length > 0 ? featuredCategory.trim().slice(0, 64) : null; updateData.featuredCategory = normalizedCategory; } await db .update(generatedImage) .set(updateData) .where(eq(generatedImage.id, imageId)); console.log( `[Admin Images API] Image ${imageId} updated by ${session.user.id}, featured=${String( isFeatured )}` ); return NextResponse.json({ success: true, message: shouldUpdateFeaturedState ? isFeatured ? "已添加到精选" : "已取消精选" : "精选分类已更新", }); } catch (error) { console.error("[Admin Images API] PATCH Error:", error); return NextResponse.json({ error: "Failed to update featured status" }, { status: 500 }); } }