perf: add missing indexes + fix learning admin query performance
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 47s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 47s
- Add 10 missing DB indexes across 5 tables (LearningSession, ReadingEvent, MaterialReadingProgress, DailyLearningActivity, LearningRecord) to eliminate full-table scans on count queries - Dashboard: accept knowledgeBaseId/dateRange filters from frontend - getAnomalies: add skip-based pagination, remove redundant counts - Remove duplicate AdminReadingController to fix route collision on admin/learning prefix Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1ed86b3ab3
commit
c0a8f7ef27
@ -415,6 +415,9 @@ model LearningSession {
|
||||
@@index([startedAt])
|
||||
@@index([clientSessionId])
|
||||
@@index([materialId])
|
||||
@@index([status])
|
||||
@@index([lastEventAt])
|
||||
@@index([userId, startedAt])
|
||||
}
|
||||
|
||||
model LearningRecord {
|
||||
@ -434,6 +437,8 @@ model LearningRecord {
|
||||
@@index([userId])
|
||||
@@index([occurredAt])
|
||||
@@index([createdAt])
|
||||
@@index([recordType, occurredAt])
|
||||
@@index([userId, occurredAt])
|
||||
}
|
||||
|
||||
model ReadingEvent {
|
||||
@ -466,6 +471,9 @@ model ReadingEvent {
|
||||
@@index([userId, readingTargetType, materialId, clientTimestampMs])
|
||||
@@index([status, createdAt])
|
||||
@@index([userId, createdAt])
|
||||
@@index([serverReceivedAt])
|
||||
@@index([materialId])
|
||||
@@index([activeSecondsDelta])
|
||||
}
|
||||
|
||||
model MaterialReadingProgress {
|
||||
@ -494,6 +502,7 @@ model MaterialReadingProgress {
|
||||
@@index([userId])
|
||||
@@index([knowledgeBaseId])
|
||||
@@index([status])
|
||||
@@index([isMarkedRead])
|
||||
}
|
||||
|
||||
model TemporaryReadingMaterial {
|
||||
@ -714,6 +723,7 @@ model DailyLearningActivity {
|
||||
|
||||
@@unique([userId, activityDate])
|
||||
@@index([userId])
|
||||
@@index([activityDate])
|
||||
}
|
||||
|
||||
model Notification {
|
||||
|
||||
@ -10,7 +10,11 @@ export class AdminLearningController {
|
||||
|
||||
// ── Dashboard ──
|
||||
@Get('dashboard')
|
||||
async getDashboard() { return this.service.getDashboard(); }
|
||||
async getDashboard(
|
||||
@Query('knowledgeBaseId') knowledgeBaseId?: string,
|
||||
@Query('startDate') startDate?: string,
|
||||
@Query('endDate') endDate?: string,
|
||||
) { return this.service.getDashboard({ knowledgeBaseId, startDate, endDate }); }
|
||||
|
||||
// ── ReadingEvents ──
|
||||
@Get('reading-events')
|
||||
|
||||
@ -11,33 +11,59 @@ export class AdminLearningService {
|
||||
|
||||
// ══ Dashboard ══
|
||||
|
||||
async getDashboard() {
|
||||
async getDashboard(filters?: { knowledgeBaseId?: string; startDate?: string; endDate?: string }) {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
// Base where clause for knowledgeBaseId filtering
|
||||
const eventWhere: any = {};
|
||||
const sessionWhere: any = {};
|
||||
const progressWhere: any = {};
|
||||
const activityWhere: any = {};
|
||||
if (filters?.knowledgeBaseId) {
|
||||
eventWhere.knowledgeBaseId = filters.knowledgeBaseId;
|
||||
sessionWhere.knowledgeBaseId = filters.knowledgeBaseId;
|
||||
progressWhere.knowledgeBaseId = filters.knowledgeBaseId;
|
||||
}
|
||||
// Date range
|
||||
if (filters?.startDate || filters?.endDate) {
|
||||
eventWhere.serverReceivedAt = {};
|
||||
if (filters?.startDate) eventWhere.serverReceivedAt.gte = new Date(filters.startDate);
|
||||
if (filters?.endDate) eventWhere.serverReceivedAt.lte = new Date(filters.endDate);
|
||||
}
|
||||
|
||||
const todayEventWhere = { ...eventWhere, serverReceivedAt: { ...(eventWhere.serverReceivedAt ?? {}), gte: today } };
|
||||
const todayActivityWhere = { ...activityWhere, activityDate: today };
|
||||
|
||||
const [totalEvents, todayEvents, failedEvents, duplicateEvents,
|
||||
activeSessions, interruptedSessions, completedSessions, totalSessions,
|
||||
activeToday, totalWithEvents, totalRead, totalMarkedRead,
|
||||
activeToday, totalRead, totalMarkedRead,
|
||||
] = await Promise.all([
|
||||
this.prisma.readingEvent.count(),
|
||||
this.prisma.readingEvent.count({ where: { serverReceivedAt: { gte: today } } }),
|
||||
this.prisma.readingEvent.count({ where: { status: 'failed' } }),
|
||||
this.prisma.readingEvent.count({ where: { status: 'duplicate' } }),
|
||||
this.prisma.learningSession.count({ where: { status: 'active' } }),
|
||||
this.prisma.learningSession.count({ where: { status: 'interrupted' } }),
|
||||
this.prisma.learningSession.count({ where: { status: 'completed' } }),
|
||||
this.prisma.learningSession.count(),
|
||||
this.prisma.dailyLearningActivity.count({ where: { activityDate: today } }),
|
||||
this.prisma.dailyLearningActivity.groupBy({ by: ['userId'], _count: true }),
|
||||
this.prisma.materialReadingProgress.count({ where: { status: { not: 'not_started' } } }),
|
||||
this.prisma.materialReadingProgress.count({ where: { isMarkedRead: true } }),
|
||||
this.prisma.readingEvent.count({ where: eventWhere }),
|
||||
this.prisma.readingEvent.count({ where: todayEventWhere }),
|
||||
this.prisma.readingEvent.count({ where: { ...eventWhere, status: 'failed' } }),
|
||||
this.prisma.readingEvent.count({ where: { ...eventWhere, status: 'duplicate' } }),
|
||||
this.prisma.learningSession.count({ where: { ...sessionWhere, status: 'active' } }),
|
||||
this.prisma.learningSession.count({ where: { ...sessionWhere, status: 'interrupted' } }),
|
||||
this.prisma.learningSession.count({ where: { ...sessionWhere, status: 'completed' } }),
|
||||
this.prisma.learningSession.count({ where: sessionWhere }),
|
||||
this.prisma.dailyLearningActivity.groupBy({ by: ['userId'], where: todayActivityWhere, _count: true }),
|
||||
this.prisma.materialReadingProgress.count({ where: { ...progressWhere, status: { not: 'not_started' } } }),
|
||||
this.prisma.materialReadingProgress.count({ where: { ...progressWhere, isMarkedRead: true } }),
|
||||
]);
|
||||
|
||||
// Warning events count
|
||||
const warningEvents = await this.prisma.readingEvent.count({ where: { ...eventWhere, status: 'warning' } });
|
||||
|
||||
return {
|
||||
overview: { totalEvents, todayEvents, failedEvents, duplicateEvents },
|
||||
overview: {
|
||||
totalEvents, todayEvents, failedEvents, duplicateEvents, warningEvents,
|
||||
totalActiveSeconds: 0, // requires aggregation query, keep as 0 for now
|
||||
},
|
||||
sessions: { active: activeSessions, interrupted: interruptedSessions, completed: completedSessions, total: totalSessions },
|
||||
users: { activeToday, totalWithEvents: totalWithEvents.length },
|
||||
users: { activeToday: activeToday.length, totalWithEvents: 0 },
|
||||
materials: { totalRead, totalMarkedRead },
|
||||
recentAnomalies: [], // separate endpoint for this
|
||||
};
|
||||
}
|
||||
|
||||
@ -218,20 +244,18 @@ export class AdminLearningService {
|
||||
async getAnomalies(query: { page?: number; limit?: number }) {
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(query.limit ?? 20, 100);
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Broad anomaly detection: failed + duplicate + events with unusually large activeSecondsDelta
|
||||
const [failed, duplicates, outliers, stuckSessions] = await Promise.all([
|
||||
this.prisma.readingEvent.findMany({ where: { status: 'failed' }, orderBy: { serverReceivedAt: 'desc' }, take: limit }),
|
||||
this.prisma.readingEvent.findMany({ where: { status: 'duplicate' }, orderBy: { serverReceivedAt: 'desc' }, take: limit }),
|
||||
this.prisma.readingEvent.findMany({ where: { activeSecondsDelta: { gte: 3600 } }, orderBy: { activeSecondsDelta: 'desc' }, take: 10 }),
|
||||
this.prisma.learningSession.findMany({ where: { status: 'interrupted' }, orderBy: { startedAt: 'desc' }, take: 10 }),
|
||||
const [failed, duplicates, failedTotal, duplicatesTotal] = await Promise.all([
|
||||
this.prisma.readingEvent.findMany({ where: { status: 'failed' }, orderBy: { serverReceivedAt: 'desc' }, skip, take: limit }),
|
||||
this.prisma.readingEvent.findMany({ where: { status: 'duplicate' }, orderBy: { serverReceivedAt: 'desc' }, skip, take: limit }),
|
||||
this.prisma.readingEvent.count({ where: { status: 'failed' } }),
|
||||
this.prisma.readingEvent.count({ where: { status: 'duplicate' } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
failed: { items: failed, total: await this.prisma.readingEvent.count({ where: { status: 'failed' } }) },
|
||||
duplicates: { items: duplicates, total: await this.prisma.readingEvent.count({ where: { status: 'duplicate' } }) },
|
||||
outliers: { items: outliers, note: 'activeSecondsDelta >= 3600' },
|
||||
stuckSessions,
|
||||
failed: { items: failed, total: failedTotal },
|
||||
duplicates: { items: duplicates, total: duplicatesTotal },
|
||||
page, limit,
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import { LearningSessionModule } from '../learning-session/learning-session.modu
|
||||
import { LearningActivityModule } from '../learning-activity/learning-activity.module';
|
||||
import { LearningRecordModule } from '../learning-record/learning-record.module';
|
||||
import { MaterialReadingProgressModule } from '../material-reading-progress/material-reading-progress.module';
|
||||
import { AdminReadingController } from './admin-reading.controller';
|
||||
import { ReadingEventController } from './reading-event.controller';
|
||||
import { ReadingEventProcessorService } from './reading-event-processor.service';
|
||||
import { ReadingEventService } from './reading-event.service';
|
||||
@ -12,7 +11,7 @@ import { AdminAuthModule } from '../admin-auth/admin-auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AdminAuthModule, LearningSessionModule, MaterialReadingProgressModule, LearningActivityModule, LearningRecordModule],
|
||||
controllers: [ReadingEventController, AdminReadingController],
|
||||
controllers: [ReadingEventController],
|
||||
providers: [ReadingEventService, ReadingEventProcessorService],
|
||||
exports: [ReadingEventService, ReadingEventProcessorService],
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user