2026-05-29 15:11:52 +08:00

330 lines
10 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 { 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<string, RuntimeModelMeta & { score: number }>();
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<string, RuntimeModelMeta & { score: number }>,
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<number>`count(*)::int`,
totalUsers: sql<number>`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<number>`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<string>`date(${generatedImage.createdAt})::text`,
count: sql<number>`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<number>`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<number>`count(*)::int`,
userCount: sql<number>`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<string>`date(${generatedImage.createdAt})::text`,
modelId: generatedImage.modelId,
count: sql<number>`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<string>`to_char(${generatedImage.createdAt}, 'YYYY-MM')`,
count: sql<number>`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<number>`extract(hour from ${generatedImage.createdAt})::int`,
count: sql<number>`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<number>`count(*)::int`,
users: sql<number>`count(distinct ${generatedImage.userId})::int`,
})
.from(generatedImage)
.where(sql`date(${generatedImage.createdAt}) = current_date`);
// 9. 本周统计
const weekStats = await db
.select({
count: sql<number>`count(*)::int`,
users: sql<number>`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<number>`count(*)::int`,
users: sql<number>`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 });
}
}