diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index ec7e1762e..ccc572573 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -1420,12 +1420,97 @@ type ImageAnalytics = { modelTrendStats: Array<{ date: string; modelId: string | null; + modelName: string | null; + providerName: string | null; + providerType: string | null; count: number; }>; monthlyStats: Array<{ month: string; count: number }>; hourlyStats: Array<{ hour: number; count: number }>; }; +type RuntimeModelMeta = { + modelName: string | null; + providerName: string | null; + providerType: string | null; +}; + +type RuntimeModelCandidate = RuntimeModelMeta & { + modelId: string | null; + isEnabled?: boolean; + isHidden?: boolean; + isDefault?: boolean; + isSubModel?: boolean; + sortOrder?: number | null; +}; + +const analyticsNumberFormatter = new Intl.NumberFormat("zh-CN"); + +function scoreRuntimeModelCandidate(candidate: RuntimeModelCandidate) { + return ( + (candidate.isEnabled ? 100 : 0) + + (!candidate.isHidden ? 20 : 0) + + (!candidate.isSubModel ? 10 : 0) + + (candidate.isDefault ? 8 : 0) + + Math.max(0, 1000 - Number(candidate.sortOrder ?? 999)) + ); +} + +function buildRuntimeModelMetaMap(candidates: RuntimeModelCandidate[]) { + const map = new Map(); + + candidates.forEach((candidate) => { + const modelId = String(candidate.modelId || "").trim(); + if (!modelId) return; + const score = scoreRuntimeModelCandidate(candidate); + const current = map.get(modelId); + if (current && current.score >= score) return; + map.set(modelId, { + score, + modelName: candidate.modelName || null, + providerName: candidate.providerName || null, + providerType: candidate.providerType || null, + }); + }); + + return map; +} + +function resolveRuntimeModelMeta( + runtimeModelMetaById: Map, + modelId: string | null +): RuntimeModelMeta { + if (!modelId) { + return { + modelName: null, + providerName: null, + providerType: null, + }; + } + + const resolved = runtimeModelMetaById.get(modelId); + return { + modelName: resolved?.modelName || null, + providerName: resolved?.providerName || null, + providerType: resolved?.providerType || null, + }; +} + +function formatAnalyticsNumber(value: number | null | undefined) { + return analyticsNumberFormatter.format(value || 0); +} + +function formatAnalyticsRatio(count: number, total: number) { + if (!total) return "0%"; + return `${((count / total) * 100).toFixed(1)}%`; +} + +function formatAnalyticsMonthLabel(value: string) { + const [year, month] = String(value || "").split("-"); + if (!year || !month) return value || "本月"; + return `${year} 年 ${month} 月`; +} + const AVAILABLE_TOOLS = [ { name: "getWeather", description: "获取天气信息" }, { name: "createDocument", description: "创建文档" }, @@ -1750,6 +1835,157 @@ export function AdminSettingsClient() { : null; const { data: analyticsData } = useSWR(analyticsUrl, fetcher); + const runtimeModelMetaById = useMemo(() => { + const providerMetaById = new Map< + string, + { providerName: string | null; providerType: string | null } + >(); + (providersData?.providers || []).forEach((provider) => { + providerMetaById.set(provider.id, { + providerName: provider.name, + providerType: provider.type, + }); + }); + + const candidates: RuntimeModelCandidate[] = []; + + (modelsData?.models || []).forEach((model) => { + const providerMeta = providerMetaById.get(model.providerId); + candidates.push({ + modelId: model.modelId, + modelName: model.displayName, + providerName: providerMeta?.providerName || null, + providerType: providerMeta?.providerType || null, + isEnabled: model.isEnabled, + isHidden: model.isHidden, + isDefault: model.isDefault, + isSubModel: model.isSubModel, + sortOrder: model.sortOrder, + }); + }); + + (analyticsData?.modelDetailStats || []).forEach((stat) => { + candidates.push({ + modelId: stat.modelId, + modelName: stat.modelName, + providerName: stat.providerName, + providerType: stat.providerType, + }); + }); + + (analyticsData?.modelTrendStats || []).forEach((stat) => { + candidates.push({ + modelId: stat.modelId, + modelName: stat.modelName, + providerName: stat.providerName, + providerType: stat.providerType, + }); + }); + + return buildRuntimeModelMetaMap(candidates); + }, [ + analyticsData?.modelDetailStats, + analyticsData?.modelTrendStats, + modelsData?.models, + providersData?.providers, + ]); + + const analyticsOverviewCards = useMemo(() => { + const currentMonth = `${new Date().getFullYear()}-${String( + new Date().getMonth() + 1 + ).padStart(2, "0")}`; + return [ + { + key: "total-images", + label: "总生成量", + value: formatAnalyticsNumber(analyticsData?.overview.totalImages), + note: "累计图片资产", + icon: ImageIcon, + tone: "from-zinc-950 via-zinc-900 to-zinc-800 text-white", + }, + { + key: "total-users", + label: "活跃用户", + value: formatAnalyticsNumber(analyticsData?.overview.totalUsers), + note: "参与生成的用户数", + icon: Users, + tone: "from-emerald-500/15 via-emerald-500/5 to-transparent text-emerald-700", + }, + { + key: "today-images", + label: "今日生成", + value: formatAnalyticsNumber(analyticsData?.overview.todayImages), + note: `${formatAnalyticsNumber(analyticsData?.overview.todayUsers)} 位用户`, + icon: TrendingUp, + tone: "from-amber-500/20 via-amber-500/5 to-transparent text-amber-700", + }, + { + key: "month-images", + label: selectedMonth === currentMonth ? "本月生成" : formatAnalyticsMonthLabel(selectedMonth), + value: formatAnalyticsNumber(analyticsData?.overview.monthImages), + note: `${formatAnalyticsNumber(analyticsData?.overview.monthUsers)} 位用户`, + icon: Calendar, + tone: "from-lime-400/25 via-lime-300/10 to-transparent text-lime-700", + }, + ]; + }, [analyticsData?.overview, selectedMonth]); + + const analyticsPeakHour = useMemo(() => { + return (analyticsData?.hourlyStats || []).reduce<{ + hour: number; + count: number; + } | null>((best, stat) => { + if (!best || stat.count > best.count) { + return { hour: stat.hour, count: stat.count }; + } + return best; + }, null); + }, [analyticsData?.hourlyStats]); + + const analyticsRecentMonths = useMemo( + () => analyticsData?.monthlyStats?.slice(-6) || [], + [analyticsData?.monthlyStats] + ); + + const analyticsTrendDays = useMemo(() => { + const grouped = new Map< + string, + { + date: string; + total: number; + models: ImageAnalytics["modelTrendStats"]; + } + >(); + + (analyticsData?.modelTrendStats || []).forEach((stat) => { + const current = grouped.get(stat.date) || { + date: stat.date, + total: 0, + models: [], + }; + current.total += stat.count; + current.models.push(stat); + grouped.set(stat.date, current); + }); + + return Array.from(grouped.values()) + .sort((left, right) => left.date.localeCompare(right.date)) + .slice(-7) + .map((item) => ({ + ...item, + models: [...item.models].sort((left, right) => right.count - left.count), + })); + }, [analyticsData?.modelTrendStats]); + + const analyticsModelTotal = useMemo( + () => + (analyticsData?.modelDetailStats || []).reduce( + (sum, stat) => sum + stat.count, + 0 + ), + [analyticsData?.modelDetailStats] + ); + const detailAgentModelOptions = useMemo( () => (modelsData?.models || []).filter( @@ -8585,9 +8821,10 @@ export function AdminSettingsClient() {

- {analyticsData?.modelDetailStats?.find( - (s) => s.modelId === selectedImage.modelId - )?.modelName || + {resolveRuntimeModelMeta( + runtimeModelMetaById, + selectedImage.modelId + ).modelName || selectedImage.modelId || "未知"}

@@ -8729,651 +8966,589 @@ export function AdminSettingsClient() { {/* 🆕 数据分析 */} - - {/* 概览卡片 */} -
- - -
-
- + +
+ + +
+
+ analytics overview
-
-

- {analyticsData?.overview.totalImages || 0} -

-

总生图数

-
-
- -
- - -
-
- -
-
-

- {analyticsData?.overview.totalUsers || 0} -

-

总用户数

-
-
-
-
- - -
-
- -
-
-

- {analyticsData?.overview.todayImages || 0} -

-

今日生图

-
-
-
-
- - -
-
-
- +
+
+ + 生成与运营总览 + + + 当前面板聚合最近 30 天趋势、当日峰值时段和模型使用分布。 + {selectedDate + ? ` 当前已锁定 ${selectedDate} 的小时分布。` + : " 点击趋势柱可联动查看某一天的小时明细。"} +
-
-

- {analyticsData?.overview.monthImages || 0} -

-

- {selectedMonth === - `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}` - ? "本月生图" - : `${selectedMonth} 生图`} -

+
+ + {formatAnalyticsMonthLabel(selectedMonth)} + {selectedDate ? ( + + {selectedDate} + + ) : null}
+
+ + +
+ {analyticsOverviewCards.map((item) => { + const Icon = item.icon; + return ( +
+
+
+
+ {item.label} +
+
+ {item.value} +
+
+
+ +
+
+
+ {item.note} +
+
+ ); + })} +
+
+ + + + +
+ filters & pulse +
+ 时间筛选与月度脉冲 + + 用月份控制本月指标卡,并通过日期联动小时分布。 + +
+ +
+ setSelectedMonth(e.target.value)} type="month" value={selectedMonth} />
+ +
+
+
+ + 高峰时段 +
+
+ {analyticsPeakHour + ? `${String(analyticsPeakHour.hour).padStart(2, "0")}:00` + : "--:--"} +
+
+ {analyticsPeakHour + ? `${formatAnalyticsNumber(analyticsPeakHour.count)} 次生成` + : "暂无小时数据"} +
+
+ +
+
+ + 领先模型 +
+
+ {resolveRuntimeModelMeta( + runtimeModelMetaById, + analyticsData?.modelDetailStats?.[0]?.modelId || null + ).modelName || + analyticsData?.modelDetailStats?.[0]?.modelId || + "暂无"} +
+
+ {analyticsData?.modelDetailStats?.[0] + ? `${formatAnalyticsNumber( + analyticsData.modelDetailStats[0].count + )} 次调用` + : "还没有模型统计结果"} +
+
+
+ +
+
+ recent months +
+
+ {analyticsRecentMonths.map((stat) => ( +
+
+ {stat.month} +
+
+ {formatAnalyticsNumber(stat.count)} +
+
+ ))} +
+
-
- {/* 用户排行 */} - - - - 用户生图排行 (Top 20) - +
+ + +
+
+ throughput +
+ 生成节奏 + + 左侧看最近 30 天总量,右侧看 + {selectedDate ? ` ${selectedDate} ` : "当前选择日期 "} + 的小时分布。 + +
+ {selectedDate ? ( + + ) : null}
- -
- {analyticsData?.userStats?.map((user, index) => ( -
-
- +
+
+ 近 30 天生成量 + 点击柱体联动小时分布 +
+
+ {analyticsData?.dailyStats?.map((stat, index) => { + const maxCount = Math.max( + ...(analyticsData.dailyStats?.map((s) => s.count) || [1]) + ); + const height = (stat.count / maxCount) * 100; + const isHovered = + hoveredBar?.type === "daily" && + hoveredBar?.index === index; + const isSelected = selectedDate === stat.date; + return ( +
+ setSelectedDate( + selectedDate === stat.date ? null : stat.date + ) + } + onMouseEnter={() => + setHoveredBar({ + type: "daily", + index, + value: stat.count, + label: stat.date, + }) + } + onMouseLeave={() => setHoveredBar(null)} + style={{ height: `${Math.max(height, 5)}%` }} > - {index + 1} - - - {user.userName || user.userEmail?.split("@")[0]} - -
- {user.imageCount} -
- ))} + {isHovered ? ( +
+ {stat.date} · {formatAnalyticsNumber(stat.count)} 次 +
+ ) : null} +
+ ); + })} +
+
+ {analyticsData?.dailyStats?.[0]?.date || "--"} + {analyticsData?.dailyStats?.at(-1)?.date || "--"} +
+
+ +
+
+ 24 小时分布 + {selectedDate || "默认最近样本"} +
+
+ {Array.from({ length: 24 }, (_, hour) => { + const stat = analyticsData?.hourlyStats?.find( + (item) => item.hour === hour + ); + const count = stat?.count || 0; + const maxCount = Math.max( + ...(analyticsData?.hourlyStats?.map((s) => s.count) || [ + 1, + ]), + 1 + ); + const height = (count / maxCount) * 100; + const isHovered = + hoveredBar?.type === "hourly" && + hoveredBar?.index === hour; + return ( +
+ setHoveredBar({ + type: "hourly", + index: hour, + value: count, + label: `${hour}:00`, + }) + } + onMouseLeave={() => setHoveredBar(null)} + style={{ height: `${Math.max(height, 4)}%` }} + > + {isHovered ? ( +
+ {String(hour).padStart(2, "0")}:00 ·{" "} + {formatAnalyticsNumber(count)} 次 +
+ ) : null} +
+ ); + })} +
+
+ 00:00 + 12:00 + 23:00 +
- {/* 图片尺寸分布 */} - - - 图片尺寸分布 - - -
+
+ + +
+ top users +
+ 高活跃用户 + 按生成次数排序,便于识别主要使用群体。 +
+ +
+ {analyticsData?.userStats?.slice(0, 8).map((user, index) => ( +
+
+
+ {index + 1} +
+
+
+ {user.userName || user.userEmail?.split("@")[0]} +
+
+ {user.userEmail} +
+
+
+
+
+ {formatAnalyticsNumber(user.imageCount)} +
+
+ 次生成 +
+
+
+ ))} + {(!analyticsData?.userStats || + analyticsData.userStats.length === 0) && ( +
+ 暂无用户排行数据 +
+ )} +
+
+
+ + + +
+ output sizes +
+ 尺寸分布 + 观察用户输出尺寸的偏好与占比。 +
+ {analyticsData?.sizeStats?.map((stat) => { - const total = analyticsData.overview.totalImages || 1; - const percentage = ((stat.count / total) * 100).toFixed(1); + const percentage = formatAnalyticsRatio( + stat.count, + analyticsData?.overview.totalImages || 0 + ); return (
-
- {stat.imageSize || "未知"} +
+ + {stat.imageSize || "未知"} + - {stat.count} ({percentage}%) + {formatAnalyticsNumber(stat.count)} · {percentage}
-
+
); })} -
- - - - {/* 每日趋势 */} - - - 最近30天趋势 - {selectedDate && ( - - )} - - -
- {analyticsData?.dailyStats?.map((stat, index) => { - const maxCount = Math.max( - ...(analyticsData.dailyStats?.map((s) => s.count) || [1]) - ); - const height = (stat.count / maxCount) * 100; - const isHovered = - hoveredBar?.type === "daily" && - hoveredBar?.index === index; - const isSelected = selectedDate === stat.date; - return ( -
- setSelectedDate( - selectedDate === stat.date ? null : stat.date - ) - } - onMouseEnter={() => - setHoveredBar({ - type: "daily", - index, - value: stat.count, - label: stat.date, - }) - } - onMouseLeave={() => setHoveredBar(null)} - style={{ height: `${Math.max(height, 2)}%` }} - > - {isHovered && ( -
- {stat.date}: {stat.count} 张 -
- 点击查看当天高峰时段 -
-
- )} -
- ); - })} -
-
- {analyticsData?.dailyStats?.[0]?.date} - {analyticsData?.dailyStats?.at(-1)?.date} -
-
-
- - {/* 高峰时段 */} - - - - 生图高峰时段 - {selectedDate && ( - - {selectedDate} - - )} - - {!selectedDate && ( - - 点击左侧日期柱形查看当天数据 - - )} - - -
- {Array.from({ length: 24 }, (_, hour) => { - const stat = analyticsData?.hourlyStats?.find( - (s) => s.hour === hour - ); - const count = stat?.count || 0; - const maxCount = Math.max( - ...(analyticsData?.hourlyStats?.map((s) => s.count) || [ - 1, - ]), - 1 - ); - const height = (count / maxCount) * 100; - const isHovered = - hoveredBar?.type === "hourly" && - hoveredBar?.index === hour; - return ( -
- setHoveredBar({ - type: "hourly", - index: hour, - value: count, - label: `${hour}:00`, - }) - } - onMouseLeave={() => setHoveredBar(null)} - style={{ height: `${Math.max(height, 2)}%` }} - > - {isHovered && ( -
- {hour}:00-{hour + 1}:00: {count} 张 -
- )} -
- ); - })} -
-
- 0:00 - 12:00 - 23:00 -
-
-
- - {/* 月度统计 */} - - - 月度统计 - - -
- {analyticsData?.monthlyStats?.slice(-6).map((stat) => ( -
-

- {stat.count} -

-

- {stat.month} -

-
- ))} -
-
-
+ + +
- {/* 🆕 模型使用分析 */} -
-

- - 模型使用分析 -

-
- {/* 模型使用排行 */} - - - 模型使用排行 - - 按生图数量排序 - - - -
- {analyticsData?.modelDetailStats - ?.slice(0, 8) - .map((stat, index) => { - const maxCount = - analyticsData?.modelDetailStats?.[0]?.count || 1; - const percentage = (stat.count / maxCount) * 100; - return ( -
-
-
- - {index + 1} - - - {stat.modelName || stat.modelId || "未知模型"} - -
-
- - {stat.count} - - - 张 - +
+ + +
+ model usage +
+ 模型使用概览 + + 修正为按 runtime modelId 聚合后再解析名称,避免跨 provider 串名。 + +
+ +
+ {(analyticsData?.modelDetailStats || []).slice(0, 6).map((stat, index) => { + const resolved = resolveRuntimeModelMeta( + runtimeModelMetaById, + stat.modelId + ); + return ( +
+
+
+
+ + {index + 1} + +
+ {resolved.modelName || stat.modelId || "未知模型"}
-
-
-
-
- {stat.providerName || "未知提供商"} - {stat.userCount} 用户 +
+ + {resolved.providerName || "未知提供商"} + + {formatAnalyticsNumber(stat.userCount)} 位用户
- ); - })} - {(!analyticsData?.modelDetailStats || - analyticsData.modelDetailStats.length === 0) && ( -
- 暂无数据 +
+
+ {formatAnalyticsNumber(stat.count)} +
+
+ {formatAnalyticsRatio(stat.count, analyticsModelTotal)} +
+
+
+
+
+
- )} -
- - + ); + })} + {(!analyticsData?.modelDetailStats || + analyticsData.modelDetailStats.length === 0) && ( +
+ 暂无模型使用数据 +
+ )} +
- {/* 提供商类型分布 */} - - - 提供商分布 - - 按 AI 提供商统计 - - - -
- {(() => { - const providerColors: Record = { - gemini: "bg-purple-500", - "bltcy-video": "bg-cyan-500", - "volcano-vision": "bg-orange-500", - "volcano-video": "bg-red-500", - "doubao-video": "bg-red-400", - "fal-video": "bg-indigo-500", - "ruxa-video": "bg-violet-500", - alibaba: "bg-blue-500", - "alibaba-image": "bg-amber-500", - "bfl-image": "bg-cyan-600", - "ruxa-image": "bg-emerald-500", - "fal-image": "bg-sky-500", - "openai-image": "bg-green-600", - "stepfun-image": "bg-indigo-600", - "grsai-gpt-image": "bg-emerald-600", - "grsai-video": "bg-orange-600", - midjourney: "bg-fuchsia-600", - "tripo-3d": "bg-teal-500", - "worldlabs-3d": "bg-sky-600", - "kittycad-cad": "bg-orange-500", - openai: "bg-green-500", - zhipu: "bg-lime-500", - "volcano-multimodal": "bg-fuchsia-500", - runway: "bg-rose-500", - krea: "bg-blue-500", - "qianwen-video": "bg-cyan-500", - custom: "bg-gray-500", - local: "bg-yellow-500", - }; +
+
+
+ model traffic +
+
+ {formatAnalyticsNumber(analyticsModelTotal)} +
+
+ 当前榜单内模型累计调用次数 +
+
+ +
+ {(analyticsData?.providerTypeStats || []).map((stat, index) => { const total = analyticsData?.providerTypeStats?.reduce( - (acc, s) => acc + s.count, + (sum, item) => sum + item.count, 0 ) || 0; - - return analyticsData?.providerTypeStats?.map( - (stat, index) => { - const percentage = - total > 0 - ? ((stat.count / total) * 100).toFixed(1) - : "0"; - const color = - providerColors[stat.providerType || ""] || - "bg-gray-400"; - return ( -
-
-
-
- - {stat.providerName || - stat.providerType || - "未知"} - - - - {stat.count} - - - ({percentage}%) - - -
-
-
-
-
-
- ); - } - ); - })()} - {(!analyticsData?.providerTypeStats || - analyticsData.providerTypeStats.length === 0) && ( -
- 暂无数据 -
- )} -
- - - - {/* 图片尺寸分布 */} - - - 图片尺寸分布 - - 按生成图片尺寸统计 - - - -
- {analyticsData?.sizeStats?.map((stat) => { - const total = - analyticsData?.sizeStats?.reduce( - (acc, s) => acc + s.count, - 0 - ) || 0; - const percentage = - total > 0 - ? ((stat.count / total) * 100).toFixed(1) - : "0"; + const width = formatAnalyticsRatio(stat.count, total); return (
-

- {stat.imageSize || "未知"} -

-

- {stat.count} 张 -

-

- {percentage}% -

+
+ + {stat.providerName || stat.providerType || "未知提供商"} + + + {formatAnalyticsNumber(stat.count)} · {width} + +
+
+
+
); })}
- - +
+
+
- {/* 模型使用趋势(最近7天) */} - - - 模型使用趋势 - - 最近7天各模型使用量 - - - -
- {(() => { - // 按日期分组 - const groupedByDate = - analyticsData?.modelTrendStats?.reduce( - (acc, stat) => { - if (!acc[stat.date]) acc[stat.date] = []; - acc[stat.date].push(stat); - return acc; - }, - {} as Record< - string, - typeof analyticsData.modelTrendStats + + +
+ recent 7 days +
+ 模型日趋势 + 每天显示总量,并直接列出当日最活跃的模型。 +
+ + {analyticsTrendDays.map((day) => { + const topModels = day.models.slice(0, 3); + return ( +
+
+
+
+ {day.date} +
+
+ 当日共 {formatAnalyticsNumber(day.total)} 次生成 +
+
+
+ {formatAnalyticsNumber(day.models.length)} 个模型活跃 +
+
+
+ {topModels.map((stat) => ( +
- ) || {}; - - const dates = Object.keys(groupedByDate).sort().slice(-7); - const maxDailyTotal = Math.max( - ...dates.map( - (date) => - groupedByDate[date]?.reduce( - (sum, s) => sum + s.count, - 0 - ) || 0 - ), - 1 - ); - - return dates.map((date) => { - const dayStats = groupedByDate[date] || []; - const dayTotal = dayStats.reduce( - (sum, s) => sum + s.count, - 0 - ); - const heightPercent = (dayTotal / maxDailyTotal) * 100; - - return ( -
- - {date.slice(5)} + + {stat.modelName || stat.modelId || "未知模型"} -
- {dayStats.map((stat, idx) => { - const statPercent = - dayTotal > 0 - ? (stat.count / dayTotal) * 100 - : 0; - const colors = [ - "bg-blue-500", - "bg-green-500", - "bg-purple-500", - "bg-orange-500", - "bg-pink-500", - ]; - return ( -
- ); - })} -
- - {dayTotal} + + {formatAnalyticsNumber(stat.count)}
- ); - }); - })()} - {(!analyticsData?.modelTrendStats || - analyticsData.modelTrendStats.length === 0) && ( -
- 暂无数据 + ))}
- )} +
+ ); + })} + {analyticsTrendDays.length === 0 ? ( +
+ 暂无趋势数据
- - -
+ ) : null} + +
diff --git a/app/api/admin/images/analytics/route.ts b/app/api/admin/images/analytics/route.ts index 48e493554..b5731d7d5 100644 --- a/app/api/admin/images/analytics/route.ts +++ b/app/api/admin/images/analytics/route.ts @@ -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(); + + 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(); @@ -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`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`count(*)::int`, userCount: sql`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`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`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 }); } } -