Your Name 8386755e6d
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat(admin): include video generation records in image history
2026-02-20 21:14:05 +08:00

316 lines
10 KiB
TypeScript

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(/<video[^>]+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 '%<video%'`,
sql`${message.parts}::text ILIKE '%.mp4%'`
),
];
if (search) {
// 搜索用户名或邮箱
const matchingUsers = await db
.select({ id: user.id })
.from(user)
.where(or(
like(user.name, `%${search}%`),
like(user.email, `%${search}%`)
));
if (matchingUsers.length > 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<number>`count(*)::int` })
.from(generatedImage)
.where(imageWhereClause);
// 查询视频总数
const videoCountResult = await db
.select({ count: sql<number>`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<string | null>`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<string, any> | 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<typeof item> => 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<string, unknown> = {};
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 });
}
}