refactor: improve analytics dashboard

This commit is contained in:
Your Name 2026-05-29 15:11:52 +08:00
parent 155539693c
commit aea1d84c88
2 changed files with 895 additions and 621 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,78 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { generatedImage, user, aiModel, aiProvider } from "@/lib/db/schema";
import { eq, sql, desc, count, and, gte, lt } from "drizzle-orm";
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();
@ -69,49 +138,74 @@ export async function GET(request: NextRequest) {
.groupBy(generatedImage.imageSize)
.orderBy(desc(sql`count(*)`));
// 5. 按模型统计(基础)
const modelStats = await db
const runtimeModelRows = await db
.select({
modelId: generatedImage.modelId,
count: sql<number>`count(*)::int`,
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(generatedImage)
.groupBy(generatedImage.modelId)
.orderBy(desc(sql`count(*)`))
.limit(10);
.from(aiModel)
.leftJoin(aiProvider, eq(aiModel.providerId, aiProvider.id));
// 5.1 按模型详细统计(包含模型名称和提供商信息)
const modelDetailStats = await db
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`,
modelName: aiModel.displayName,
providerName: aiProvider.name,
providerType: aiProvider.type,
})
.from(generatedImage)
.leftJoin(aiModel, eq(generatedImage.modelId, aiModel.modelId))
.leftJoin(aiProvider, eq(aiModel.providerId, aiProvider.id))
.groupBy(generatedImage.modelId, aiModel.displayName, aiProvider.name, aiProvider.type)
.orderBy(desc(sql`count(*)`))
.limit(20);
// 5.2 按提供商类型统计
const providerTypeStats = await db
.select({
providerType: aiProvider.type,
providerName: aiProvider.name,
count: sql<number>`count(*)::int`,
})
.from(generatedImage)
.leftJoin(aiModel, eq(generatedImage.modelId, aiModel.modelId))
.leftJoin(aiProvider, eq(aiModel.providerId, aiProvider.id))
.groupBy(aiProvider.type, aiProvider.name)
.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 modelTrendStats = await db
const rawModelTrendStats = await db
.select({
date: sql<string>`date(${generatedImage.createdAt})::text`,
modelId: generatedImage.modelId,
@ -121,6 +215,12 @@ export async function GET(request: NextRequest) {
.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
@ -227,4 +327,3 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: "Failed to fetch analytics" }, { status: 500 });
}
}