feat: 优化接口速度

This commit is contained in:
feie9454 2026-04-24 22:02:53 +08:00
parent fd20a4411b
commit d1c5b818d9
5 changed files with 148 additions and 60 deletions

View File

@ -12,6 +12,30 @@ import { ScreenRecord, TimeDistributionPoint, Segment, Marker } from '../types';
import { formatMemory, formatDate } from '../utils'; import { formatMemory, formatDate } from '../utils';
import { useStarToggle } from '../hooks/useStarToggle'; import { useStarToggle } from '../hooks/useStarToggle';
const inFlightJsonRequests = new Map<string, Promise<unknown>>();
const fetchJsonOnce = async <T,>(url: string): Promise<T> => {
const existingRequest = inFlightJsonRequests.get(url);
if (existingRequest) {
return existingRequest as Promise<T>;
}
const request = fetch(url, { cache: 'no-store' })
.then(async (response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json() as Promise<T>;
})
.finally(() => {
inFlightJsonRequests.delete(url);
});
inFlightJsonRequests.set(url, request);
return request as Promise<T>;
};
interface ScreenshotsTabProps { interface ScreenshotsTabProps {
hostname: string; hostname: string;
selectedDate: string | null; selectedDate: string | null;
@ -27,6 +51,9 @@ export default function ScreenshotsTab({
jumpRequest, jumpRequest,
onLastUpdateChange onLastUpdateChange
}: ScreenshotsTabProps) { }: ScreenshotsTabProps) {
const timeDistributionRequestIdRef = useRef(0);
const hourlyRecordsRequestIdRef = useRef(0);
// 状态管理 // 状态管理
const [timeDistribution, setTimeDistribution] = useState<TimeDistributionPoint[]>([]); const [timeDistribution, setTimeDistribution] = useState<TimeDistributionPoint[]>([]);
const [records, setRecords] = useState<ScreenRecord[]>([]); const [records, setRecords] = useState<ScreenRecord[]>([]);
@ -58,30 +85,50 @@ export default function ScreenshotsTab({
// 获取时间分布数据 // 获取时间分布数据
const fetchTimeDistribution = async () => { const fetchTimeDistribution = async () => {
const requestId = ++timeDistributionRequestIdRef.current;
try { try {
setLoadingDistribution(true); setLoadingDistribution(true);
const response = await fetch(`/hosts/${hostname}/time-distribution`);
if (!response.ok) throw new Error('获取时间分布数据失败'); const data = await fetchJsonOnce<{ distribution: TimeDistributionPoint[] }>(
const data = await response.json(); `/hosts/${hostname}/time-distribution`
);
if (requestId !== timeDistributionRequestIdRef.current) {
return;
}
setTimeDistribution(data.distribution); setTimeDistribution(data.distribution);
} catch (error) { } catch (error) {
console.error('获取时间分布数据失败:', error); if (requestId === timeDistributionRequestIdRef.current) {
console.error('获取时间分布数据失败:', error);
}
} finally { } finally {
setLoadingDistribution(false); if (requestId === timeDistributionRequestIdRef.current) {
setLoadingDistribution(false);
}
} }
}; };
// 获取小时记录 // 获取小时记录
const fetchHourlyRecords = async (startTime: number, endTime: number, options?: { targetRecordId?: string, keepSelection?: boolean }) => { const fetchHourlyRecords = async (startTime: number, endTime: number, options?: { targetRecordId?: string, keepSelection?: boolean }) => {
const requestId = ++hourlyRecordsRequestIdRef.current;
const requestUrl = `/hosts/${hostname}/screenshots?startTime=${startTime}&endTime=${endTime}`;
try { try {
setLoadingRecords(true); setLoadingRecords(true);
setShowDetailTimeline(true); setShowDetailTimeline(true);
const response = await fetch(
`/hosts/${hostname}/screenshots?startTime=${startTime}&endTime=${endTime}` const data = await fetchJsonOnce<{
); lastUpdate: string | null;
if (!response.ok) throw new Error('获取记录数据失败'); records: ScreenRecord[];
const data = await response.json(); }>(requestUrl);
const newRecords = data.records.reverse();
if (requestId !== hourlyRecordsRequestIdRef.current) {
return;
}
const newRecords = data.records;
setRecords(newRecords); setRecords(newRecords);
onLastUpdateChange(data.lastUpdate); onLastUpdateChange(data.lastUpdate);
setTimeRange({ min: startTime * 1000, max: endTime * 1000 }); setTimeRange({ min: startTime * 1000, max: endTime * 1000 });
@ -104,9 +151,13 @@ export default function ScreenshotsTab({
} }
}); });
} catch (error) { } catch (error) {
console.error('获取记录数据失败:', error); if (requestId === hourlyRecordsRequestIdRef.current) {
console.error('获取记录数据失败:', error);
}
} finally { } finally {
setLoadingRecords(false); if (requestId === hourlyRecordsRequestIdRef.current) {
setLoadingRecords(false);
}
} }
}; };

View File

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
import { win32 } from 'path' import { win32 } from 'path'
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { withCors } from '@/lib/middleware' import { withCors } from '@/lib/middleware'
import { Prisma } from '@prisma/client'
const MAX_GAP_MS = 5 * 60 * 1000 const MAX_GAP_MS = 5 * 60 * 1000
const DEFAULT_SAMPLE_MS = 30 * 1000 const DEFAULT_SAMPLE_MS = 30 * 1000
@ -36,6 +37,12 @@ interface HourBucket {
appDurations: Map<string, number> appDurations: Map<string, number>
} }
interface ScreenTimeRecordRow {
timestamp: Date
title: string | null
path: string | null
}
const cleanTitle = (title: string) => title.replace(/\s+/g, ' ').trim() const cleanTitle = (title: string) => title.replace(/\s+/g, ' ').trim()
const trimLabelSeparators = (label: string) => label const trimLabelSeparators = (label: string) => label
@ -304,27 +311,36 @@ async function handleScreenTime(req: NextRequest) {
const aggregateEndMs = unit === 'week' ? weekEndMs : selectedDayEndMs const aggregateEndMs = unit === 'week' ? weekEndMs : selectedDayEndMs
const aggregateEndDate = addDays(aggregateStartDate, unit === 'week' ? 7 : 1) const aggregateEndDate = addDays(aggregateStartDate, unit === 'week' ? 7 : 1)
const records = await prisma.record.findMany({ const rangeStart = new Date(weekStartMs - MAX_GAP_MS)
where: { const rangeEnd = new Date(weekEndMs + MAX_GAP_MS)
hostname,
timestamp: { const records = await prisma.$queryRaw<ScreenTimeRecordRow[]>(Prisma.sql`
gte: new Date(weekStartMs - MAX_GAP_MS), WITH scoped_records AS (
lte: new Date(weekEndMs + MAX_GAP_MS) SELECT "id", "timestamp"
} FROM "records"
}, WHERE "hostname" = ${hostname}
select: { AND "timestamp" >= ${rangeStart}
timestamp: true, AND "timestamp" <= ${rangeEnd}
windows: { ),
select: { ranked_windows AS (
title: true, SELECT
path: true w."recordId",
} w."title",
} w."path",
}, ROW_NUMBER() OVER (PARTITION BY w."recordId" ORDER BY w."id") AS row_number
orderBy: { FROM "windows" w
timestamp: 'asc' INNER JOIN scoped_records r ON r."id" = w."recordId"
} )
}) SELECT
r."timestamp",
w."title",
w."path"
FROM scoped_records r
LEFT JOIN ranked_windows w
ON w."recordId" = r."id"
AND w.row_number = 1
ORDER BY r."timestamp" ASC
`)
const validDiffs = records const validDiffs = records
.slice(0, -1) .slice(0, -1)
@ -346,8 +362,7 @@ async function handleScreenTime(req: NextRequest) {
})) }))
records.forEach((record, index) => { records.forEach((record, index) => {
const activeWindow = record.windows[0] if (record.title === null && record.path === null) return
if (!activeWindow) return
const currentMs = record.timestamp.getTime() const currentMs = record.timestamp.getTime()
const nextMs = records[index + 1]?.timestamp.getTime() const nextMs = records[index + 1]?.timestamp.getTime()
@ -359,10 +374,10 @@ async function handleScreenTime(req: NextRequest) {
const intervalStartMs = currentMs const intervalStartMs = currentMs
const intervalEndMs = currentMs + intervalMs const intervalEndMs = currentMs + intervalMs
const title = cleanTitle(activeWindow.title) const title = cleanTitle(record.title ?? '')
|| stripExtension(win32.basename(activeWindow.path || '')) || stripExtension(win32.basename(record.path ?? ''))
|| '未知窗口' || '未知窗口'
const process = normalizeProcess(activeWindow.path, title) const process = normalizeProcess(record.path ?? '', title)
const aggregateContributionMs = Math.max(0, Math.min(intervalEndMs, aggregateEndMs) - Math.max(intervalStartMs, aggregateStartMs)) const aggregateContributionMs = Math.max(0, Math.min(intervalEndMs, aggregateEndMs) - Math.max(intervalStartMs, aggregateStartMs))
addDurationToUsageMap( addDurationToUsageMap(

View File

@ -218,26 +218,40 @@ async function handleGetScreenshots(req: NextRequest) {
if (endTime) whereClause.timestamp.lte = endTime if (endTime) whereClause.timestamp.lte = endTime
} }
// Get records const [records, host] = await Promise.all([
const records = await prisma.record.findMany({ prisma.record.findMany({
where: whereClause, where: whereClause,
include: { select: {
windows: true, id: true,
screenshots: true timestamp: true,
}, isStarred: true,
orderBy: { windows: {
timestamp: 'desc' select: {
} title: true,
}) path: true,
memory: true
// Get host info }
const host = await prisma.host.findUnique({ },
where: { hostname }, screenshots: {
select: { select: {
hostname: true, fileId: true,
lastUpdate: true filename: true,
} monitorName: true
}) }
}
},
orderBy: {
timestamp: 'asc'
}
}),
prisma.host.findUnique({
where: { hostname },
select: {
hostname: true,
lastUpdate: true
}
})
])
if (!host) { if (!host) {
return NextResponse.json({ error: '未找到主机记录' }, { status: 404 }) return NextResponse.json({ error: '未找到主机记录' }, { status: 404 })

View File

@ -0,0 +1,5 @@
-- CreateIndex
CREATE INDEX "windows_recordId_idx" ON "windows"("recordId");
-- CreateIndex
CREATE INDEX "screenshots_recordId_idx" ON "screenshots"("recordId");

View File

@ -56,6 +56,8 @@ model Window {
// Relations // Relations
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade) record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
@@index([recordId])
@@map("windows") @@map("windows")
} }
@ -72,6 +74,7 @@ model Screenshot {
// Relations // Relations
record Record @relation(fields: [recordId], references: [id], onDelete: Cascade) record Record @relation(fields: [recordId], references: [id], onDelete: Cascade)
@@index([recordId])
@@index([objectName]) @@index([objectName])
@@map("screenshots") @@map("screenshots")
} }