refactor: improve analytics dashboard
This commit is contained in:
parent
155539693c
commit
aea1d84c88
File diff suppressed because it is too large
Load Diff
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user