import { NextRequest, NextResponse } from "next/server"; import { db } from "@/lib/db"; import { generatedImage, user, aiModel, aiProvider } from "@/lib/db/schema"; import { eq, sql, desc, and, gte, lt } from "drizzle-orm"; import { auth } from "@/app/(auth)/auth"; type RuntimeModelRegistryRow = { modelId: string; displayName: string; providerName: string | null; providerType: string | null; isEnabled: boolean; isHidden: boolean; isDefault: boolean; isSubModel: boolean; sortOrder: number; updatedAt: Date; createdAt: Date; }; type RuntimeModelMeta = { modelName: string | null; providerName: string | null; providerType: string | null; }; function scoreRuntimeModelCandidate(row: RuntimeModelRegistryRow) { return ( (row.isEnabled ? 100 : 0) + (!row.isHidden ? 20 : 0) + (!row.isSubModel ? 10 : 0) + (row.isDefault ? 8 : 0) + Math.max(0, 1000 - row.sortOrder) + (row.updatedAt?.getTime() || row.createdAt?.getTime() || 0) / 1_000_000_000_000 ); } function buildRuntimeModelRegistry(rows: RuntimeModelRegistryRow[]) { const registry = new Map(); for (const row of rows) { const score = scoreRuntimeModelCandidate(row); const current = registry.get(row.modelId); if (current && current.score >= score) continue; registry.set(row.modelId, { score, modelName: row.displayName || null, providerName: row.providerName || null, providerType: row.providerType || null, }); } return registry; } function resolveRuntimeModelMeta( registry: Map, modelId: string | null ): RuntimeModelMeta { if (!modelId) { return { modelName: null, providerName: null, providerType: null, }; } const resolved = registry.get(modelId); return { modelName: resolved?.modelName || null, providerName: resolved?.providerName || null, providerType: resolved?.providerType || 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 month = searchParams.get("month"); // 格式: YYYY-MM const selectedDate = searchParams.get("date"); // 格式: YYYY-MM-DD,用于高峰时段筛选 // 1. 总体统计 const totalStats = await db .select({ totalImages: sql`count(*)::int`, totalUsers: sql`count(distinct ${generatedImage.userId})::int`, }) .from(generatedImage); // 2. 按用户统计(Top 20) const userStats = await db .select({ userId: generatedImage.userId, userName: user.name, userEmail: user.email, imageCount: sql`count(*)::int`, }) .from(generatedImage) .leftJoin(user, eq(generatedImage.userId, user.id)) .groupBy(generatedImage.userId, user.name, user.email) .orderBy(desc(sql`count(*)`)) .limit(20); // 3. 按日期统计(最近30天) const dailyStats = await db .select({ date: sql`date(${generatedImage.createdAt})::text`, count: sql`count(*)::int`, }) .from(generatedImage) .where(sql`${generatedImage.createdAt} >= now() - interval '30 days'`) .groupBy(sql`date(${generatedImage.createdAt})`) .orderBy(sql`date(${generatedImage.createdAt})`); // 4. 按图片尺寸统计 const sizeStats = await db .select({ imageSize: generatedImage.imageSize, count: sql`count(*)::int`, }) .from(generatedImage) .groupBy(generatedImage.imageSize) .orderBy(desc(sql`count(*)`)); const runtimeModelRows = await db .select({ modelId: aiModel.modelId, displayName: aiModel.displayName, providerName: aiProvider.name, providerType: aiProvider.type, isEnabled: aiModel.isEnabled, isHidden: aiModel.isHidden, isDefault: aiModel.isDefault, isSubModel: aiModel.isSubModel, sortOrder: aiModel.sortOrder, updatedAt: aiModel.updatedAt, createdAt: aiModel.createdAt, }) .from(aiModel) .leftJoin(aiProvider, eq(aiModel.providerId, aiProvider.id)); const runtimeModelRegistry = buildRuntimeModelRegistry(runtimeModelRows); // 5. 按模型统计 const aggregatedModelStats = await db .select({ modelId: generatedImage.modelId, count: sql`count(*)::int`, userCount: sql`count(distinct ${generatedImage.userId})::int`, }) .from(generatedImage) .groupBy(generatedImage.modelId) .orderBy(desc(sql`count(*)`)); const modelStats = aggregatedModelStats.slice(0, 10).map((stat) => ({ modelId: stat.modelId, count: stat.count, })); const modelDetailStats = aggregatedModelStats.slice(0, 20).map((stat) => ({ modelId: stat.modelId, count: stat.count, userCount: stat.userCount, ...resolveRuntimeModelMeta(runtimeModelRegistry, stat.modelId), })); // 5.2 按提供商类型统计 const providerTypeStatsMap = new Map< string, { providerType: string | null; providerName: string | null; count: number; } >(); for (const stat of aggregatedModelStats) { const meta = resolveRuntimeModelMeta(runtimeModelRegistry, stat.modelId); const key = `${meta.providerType || "unknown"}::${meta.providerName || "unknown"}`; const current = providerTypeStatsMap.get(key) || { providerType: meta.providerType, providerName: meta.providerName, count: 0, }; current.count += stat.count; providerTypeStatsMap.set(key, current); } const providerTypeStats = Array.from(providerTypeStatsMap.values()).sort( (left, right) => right.count - left.count ); // 5.3 模型使用趋势(最近7天,按模型分组) const rawModelTrendStats = await db .select({ date: sql`date(${generatedImage.createdAt})::text`, modelId: generatedImage.modelId, count: sql`count(*)::int`, }) .from(generatedImage) .where(sql`${generatedImage.createdAt} >= now() - interval '7 days'`) .groupBy(sql`date(${generatedImage.createdAt})`, generatedImage.modelId) .orderBy(sql`date(${generatedImage.createdAt})`); const modelTrendStats = rawModelTrendStats.map((stat) => ({ date: stat.date, modelId: stat.modelId, count: stat.count, ...resolveRuntimeModelMeta(runtimeModelRegistry, stat.modelId), })); // 6. 按月份统计 const monthlyStats = await db .select({ month: sql`to_char(${generatedImage.createdAt}, 'YYYY-MM')`, count: sql`count(*)::int`, }) .from(generatedImage) .groupBy(sql`to_char(${generatedImage.createdAt}, 'YYYY-MM')`) .orderBy(sql`to_char(${generatedImage.createdAt}, 'YYYY-MM')`); // 7. 按小时统计(用于分析高峰时段,支持按日期筛选) let hourlyWhereClause = undefined; if (selectedDate) { const dateStart = new Date(selectedDate); const dateEnd = new Date(selectedDate); dateEnd.setDate(dateEnd.getDate() + 1); hourlyWhereClause = and( gte(generatedImage.createdAt, dateStart), lt(generatedImage.createdAt, dateEnd) ); } const hourlyStats = await db .select({ hour: sql`extract(hour from ${generatedImage.createdAt})::int`, count: sql`count(*)::int`, }) .from(generatedImage) .where(hourlyWhereClause) .groupBy(sql`extract(hour from ${generatedImage.createdAt})`) .orderBy(sql`extract(hour from ${generatedImage.createdAt})`); // 8. 今日统计 const todayStats = await db .select({ count: sql`count(*)::int`, users: sql`count(distinct ${generatedImage.userId})::int`, }) .from(generatedImage) .where(sql`date(${generatedImage.createdAt}) = current_date`); // 9. 本周统计 const weekStats = await db .select({ count: sql`count(*)::int`, users: sql`count(distinct ${generatedImage.userId})::int`, }) .from(generatedImage) .where(sql`${generatedImage.createdAt} >= date_trunc('week', current_date)`); // 10. 指定月份统计(支持选择月份) let monthStart: Date; let monthEnd: Date; if (month) { // 解析指定月份 const [year, mon] = month.split("-").map(Number); monthStart = new Date(year, mon - 1, 1); monthEnd = new Date(year, mon, 1); // 下个月第一天 } else { // 默认当前月 const now = new Date(); monthStart = new Date(now.getFullYear(), now.getMonth(), 1); monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 1); } const monthStats = await db .select({ count: sql`count(*)::int`, users: sql`count(distinct ${generatedImage.userId})::int`, }) .from(generatedImage) .where(and( gte(generatedImage.createdAt, monthStart), lt(generatedImage.createdAt, monthEnd) )); return NextResponse.json({ overview: { totalImages: totalStats[0]?.totalImages || 0, totalUsers: totalStats[0]?.totalUsers || 0, todayImages: todayStats[0]?.count || 0, todayUsers: todayStats[0]?.users || 0, weekImages: weekStats[0]?.count || 0, weekUsers: weekStats[0]?.users || 0, monthImages: monthStats[0]?.count || 0, monthUsers: monthStats[0]?.users || 0, selectedMonth: month || `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`, selectedDate: selectedDate || null, }, userStats, dailyStats, sizeStats, modelStats, modelDetailStats, providerTypeStats, modelTrendStats, monthlyStats, hourlyStats, }); } catch (error) { console.error("[Admin Images Analytics API] Error:", error); return NextResponse.json({ error: "Failed to fetch analytics" }, { status: 500 }); } }