refactor: merge admin/learning into admin-api/learning, restore API isolation
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 45s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 45s
- Move AdminLearningService + DTOs to learning-session module - Merge 21 new endpoints into existing admin-api/learning controller - Add analysis and ai-usage methods to unified service - Delete admin-learning module (no longer needed) - Revert JwtAuthGuard /api/admin bypass (was breaking isolation) - Fix: /api/* now exclusively serves user/iOS traffic again Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
04da939a22
commit
82e3a60101
@ -26,7 +26,7 @@ import { AdminEventsModule } from './modules/admin-events/admin-events.module';
|
||||
import { AdminKnowledgeModule } from './modules/admin-knowledge/admin-knowledge.module';
|
||||
import { AdminCostsModule } from './modules/admin-costs/admin-costs.module';
|
||||
import { AdminBillingModule } from './modules/admin-billing/admin-billing.module';
|
||||
import { AdminLearningModule } from './modules/admin-learning/admin-learning.module';
|
||||
|
||||
import { AdminServersModule } from './modules/admin-servers/admin-servers.module';
|
||||
import { AdminConversationModule } from './modules/admin-conversation/admin-conversation.module';
|
||||
import { AdminAiChatModule } from './modules/admin-ai-chat/admin-ai-chat.module';
|
||||
@ -139,7 +139,6 @@ import appleConfig from './config/apple.config';
|
||||
AdminCostsModule,
|
||||
AdminBillingModule,
|
||||
AdminServersModule,
|
||||
AdminLearningModule,
|
||||
AdminConversationModule,
|
||||
AdminAiChatModule,
|
||||
AdminAuditLogModule,
|
||||
|
||||
@ -33,7 +33,7 @@ export class JwtAuthGuard implements CanActivate {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
|
||||
// Admin and internal routes use their own auth guards
|
||||
if (request.path.startsWith('/api/admin') || request.path.startsWith('/admin-api') || request.path.startsWith('/internal')) {
|
||||
if (request.path.startsWith('/admin-api') || request.path.startsWith('/internal')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
import { Controller, Get, Post, Param, Query, Body, UseGuards } from '@nestjs/common';
|
||||
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||
import { AdminLearningService } from './admin-learning.service';
|
||||
import { EventFilterQuery, SessionFilterQuery, ProgressFilterQuery, RecordFilterQuery, PaginationQuery, BatchReprocessDto, RecalculateDto } from './dto/admin-learning.dto';
|
||||
|
||||
@Controller('admin/learning')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
export class AdminLearningController {
|
||||
constructor(private readonly service: AdminLearningService) {}
|
||||
|
||||
// ── Dashboard ──
|
||||
@Get('dashboard')
|
||||
async getDashboard(
|
||||
@Query('knowledgeBaseId') knowledgeBaseId?: string,
|
||||
@Query('startDate') startDate?: string,
|
||||
@Query('endDate') endDate?: string,
|
||||
) { return this.service.getDashboard({ knowledgeBaseId, startDate, endDate }); }
|
||||
|
||||
// ── Knowledge Bases ──
|
||||
@Get('knowledge-bases')
|
||||
async getKnowledgeBases() { return this.service.getKnowledgeBases(); }
|
||||
|
||||
// ── ReadingEvents ──
|
||||
@Get('reading-events')
|
||||
async getReadingEvents(@Query() query: EventFilterQuery) { return this.service.getReadingEvents(query); }
|
||||
@Get('reading-events/failed')
|
||||
async getFailedEvents(@Query() query: PaginationQuery) { return this.service.getFailedEvents(query); }
|
||||
@Get('reading-events/:id')
|
||||
async getReadingEvent(@Param('id') id: string) { return this.service.getReadingEvent(id); }
|
||||
@Post('reading-events/:id/reprocess')
|
||||
async reprocessEvent(@Param('id') id: string) { return this.service.reprocessEvent(id); }
|
||||
@Post('reading-events/reprocess-batch')
|
||||
async batchReprocess(@Body() dto: BatchReprocessDto) { return this.service.batchReprocess(dto.eventIds); }
|
||||
|
||||
// ── Sessions ──
|
||||
@Get('sessions')
|
||||
async getSessions(@Query() query: SessionFilterQuery) { return this.service.getSessions(query); }
|
||||
@Get('sessions/interrupted')
|
||||
async getInterruptedSessions(@Query() query: PaginationQuery) { return this.service.getInterruptedSessions(query); }
|
||||
@Get('sessions/:id')
|
||||
async getSession(@Param('id') id: string) { return this.service.getSession(id); }
|
||||
|
||||
// ── Progress ──
|
||||
@Get('progress')
|
||||
async getProgress(@Query() query: ProgressFilterQuery) { return this.service.getProgress(query); }
|
||||
@Get('progress/:id')
|
||||
async getProgressDetail(@Param('id') id: string) { return this.service.getProgressDetail(id); }
|
||||
|
||||
// ── DailyActivities ──
|
||||
@Get('daily-activities')
|
||||
async getDailyActivities(@Query() query: PaginationQuery & { userId?: string; startDate?: string; endDate?: string }) { return this.service.getDailyActivities(query); }
|
||||
|
||||
// ── Records ──
|
||||
@Get('records')
|
||||
async getRecords(@Query() query: RecordFilterQuery) { return this.service.getRecords(query); }
|
||||
@Get('records/:id')
|
||||
async getRecord(@Param('id') id: string) { return this.service.getRecord(id); }
|
||||
|
||||
// ── Timeline ──
|
||||
@Get('user-timeline')
|
||||
async getUserTimeline(@Query('userId') userId: string, @Query('limit') limit?: string) {
|
||||
return this.service.getUserTimeline(userId, limit ? parseInt(limit) : undefined);
|
||||
}
|
||||
|
||||
// ── Diagnose ──
|
||||
@Get('user-diagnose')
|
||||
async diagnoseUser(@Query('userId') userId: string) { return this.service.diagnoseUser(userId); }
|
||||
@Get('material-diagnose')
|
||||
async diagnoseMaterial(@Query('materialId') materialId: string) { return this.service.diagnoseMaterial(materialId); }
|
||||
|
||||
// ── Anomalies ──
|
||||
@Get('anomalies')
|
||||
async getAnomalies(@Query() query: PaginationQuery) { return this.service.getAnomalies(query); }
|
||||
|
||||
// ── Temporary Materials ──
|
||||
@Get('temporary-materials')
|
||||
async getTemporaryMaterials(@Query() query: PaginationQuery) { return this.service.getTemporaryMaterials(query); }
|
||||
|
||||
// ── Operations ──
|
||||
@Post('recalculate')
|
||||
async recalculateLearningData(@Body() dto: RecalculateDto) { return this.service.recalculateLearningData(dto.userId); }
|
||||
@Get('export')
|
||||
async export(@Query('startDate') startDate?: string, @Query('endDate') endDate?: string, @Query('type') type?: string) {
|
||||
return this.service.export({ startDate, endDate, type });
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../../infrastructure/database/prisma.module';
|
||||
import { AiRuntimeModule } from '../ai-runtime/ai-runtime.module';
|
||||
import { AdminLearningController } from './admin-learning.controller';
|
||||
import { AdminLearningService } from './admin-learning.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AiRuntimeModule],
|
||||
controllers: [AdminLearningController],
|
||||
providers: [AdminLearningService],
|
||||
})
|
||||
export class AdminLearningModule {}
|
||||
@ -1,100 +1,95 @@
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||
import { Controller, Get, Post, Param, Query, Body, UseGuards } from '@nestjs/common';
|
||||
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
|
||||
import { AdminLearningService } from './admin-learning.service';
|
||||
import { EventFilterQuery, SessionFilterQuery, ProgressFilterQuery, RecordFilterQuery, PaginationQuery, BatchReprocessDto, RecalculateDto } from './dto/admin-learning.dto';
|
||||
|
||||
@ApiTags('admin-learning')
|
||||
@ApiBearerAuth()
|
||||
@Controller('admin-api/learning')
|
||||
@UseGuards(AdminAuthGuard, AdminRolesGuard)
|
||||
export class AdminLearningController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(private readonly service: AdminLearningService) {}
|
||||
|
||||
// ── Dashboard ──
|
||||
@Get('dashboard')
|
||||
async getDashboard(
|
||||
@Query('knowledgeBaseId') knowledgeBaseId?: string,
|
||||
@Query('startDate') startDate?: string,
|
||||
@Query('endDate') endDate?: string,
|
||||
) { return this.service.getDashboard({ knowledgeBaseId, startDate, endDate }); }
|
||||
|
||||
// ── Knowledge Bases ──
|
||||
@Get('knowledge-bases')
|
||||
async getKnowledgeBases() { return this.service.getKnowledgeBases(); }
|
||||
|
||||
// ── ReadingEvents ──
|
||||
@Get('reading-events')
|
||||
async getReadingEvents(@Query() query: EventFilterQuery) { return this.service.getReadingEvents(query); }
|
||||
@Get('reading-events/:id')
|
||||
async getReadingEvent(@Param('id') id: string) { return this.service.getReadingEvent(id); }
|
||||
@Post('reading-events/:id/reprocess')
|
||||
async reprocessEvent(@Param('id') id: string) { return this.service.reprocessEvent(id); }
|
||||
@Post('reading-events/reprocess-batch')
|
||||
async batchReprocess(@Body() dto: BatchReprocessDto) { return this.service.batchReprocess(dto.eventIds); }
|
||||
|
||||
// ── Sessions ──
|
||||
@Get('sessions')
|
||||
@ApiOperation({ summary: '学习会话列表' })
|
||||
@ApiQuery({ name: 'userId', required: false })
|
||||
@ApiQuery({ name: 'page', required: false })
|
||||
@ApiQuery({ name: 'limit', required: false })
|
||||
async listSessions(
|
||||
@Query('userId') userId?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
) {
|
||||
const take = Math.min(Number(limit) || 20, 100);
|
||||
const skip = (Math.max(Number(page) || 1, 1) - 1) * take;
|
||||
const where: any = {};
|
||||
if (userId) where.userId = userId;
|
||||
async getSessions(@Query() query: SessionFilterQuery) { return this.service.getSessions(query); }
|
||||
@Get('sessions/:id')
|
||||
async getSession(@Param('id') id: string) { return this.service.getSession(id); }
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.learningSession.findMany({
|
||||
where,
|
||||
orderBy: { startedAt: 'desc' },
|
||||
take,
|
||||
skip,
|
||||
select: { id: true, userId: true, knowledgeBaseId: true, knowledgeItemId: true, mode: true, status: true, startedAt: true, endedAt: true, durationSeconds: true, focusMinutes: true },
|
||||
}),
|
||||
this.prisma.learningSession.count({ where }),
|
||||
]);
|
||||
return { items, total };
|
||||
}
|
||||
// ── Progress ──
|
||||
@Get('progress')
|
||||
async getProgress(@Query() query: ProgressFilterQuery) { return this.service.getProgress(query); }
|
||||
@Get('progress/:id')
|
||||
async getProgressDetail(@Param('id') id: string) { return this.service.getProgressDetail(id); }
|
||||
|
||||
// ── DailyActivities ──
|
||||
@Get('daily-activities')
|
||||
async getDailyActivities(@Query() query: PaginationQuery & { userId?: string; startDate?: string; endDate?: string }) { return this.service.getDailyActivities(query); }
|
||||
|
||||
// ── Records ──
|
||||
@Get('records')
|
||||
async getRecords(@Query() query: RecordFilterQuery) { return this.service.getRecords(query); }
|
||||
@Get('records/:id')
|
||||
async getRecord(@Param('id') id: string) { return this.service.getRecord(id); }
|
||||
|
||||
// ── AI Analysis ──
|
||||
@Get('analysis')
|
||||
@ApiOperation({ summary: 'AI 分析结果列表' })
|
||||
@ApiQuery({ name: 'userId', required: false })
|
||||
@ApiQuery({ name: 'page', required: false })
|
||||
@ApiQuery({ name: 'limit', required: false })
|
||||
async listAnalysis(
|
||||
@Query('userId') userId?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
) {
|
||||
const take = Math.min(Number(limit) || 20, 100);
|
||||
const skip = (Math.max(Number(page) || 1, 1) - 1) * take;
|
||||
const where: any = {};
|
||||
if (userId) where.userId = userId;
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.aiAnalysisResult.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take,
|
||||
skip,
|
||||
select: { id: true, userId: true, jobId: true, summary: true, masteryScore: true, weaknesses: true, strengths: true, createdAt: true },
|
||||
}),
|
||||
this.prisma.aiAnalysisResult.count({ where }),
|
||||
]);
|
||||
return { items, total };
|
||||
async getAnalysis(@Query('userId') userId?: string, @Query('page') page?: string, @Query('limit') limit?: string) {
|
||||
return this.service.getAnalysis({ userId, page: page ? parseInt(page) : undefined, limit: limit ? parseInt(limit) : undefined });
|
||||
}
|
||||
|
||||
// ── AI Usage ──
|
||||
@Get('ai-usage')
|
||||
@ApiOperation({ summary: 'AI 调用日志' })
|
||||
@ApiQuery({ name: 'userId', required: false })
|
||||
@ApiQuery({ name: 'model', required: false })
|
||||
@ApiQuery({ name: 'page', required: false })
|
||||
@ApiQuery({ name: 'limit', required: false })
|
||||
async listAiUsage(
|
||||
@Query('userId') userId?: string,
|
||||
@Query('model') model?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
) {
|
||||
const take = Math.min(Number(limit) || 20, 100);
|
||||
const skip = (Math.max(Number(page) || 1, 1) - 1) * take;
|
||||
const where: any = {};
|
||||
if (userId) where.userId = userId;
|
||||
if (model) where.model = { contains: model };
|
||||
async getAiUsage(@Query('userId') userId?: string, @Query('model') model?: string, @Query('page') page?: string, @Query('limit') limit?: string) {
|
||||
return this.service.getAiUsage({ userId, model, page: page ? parseInt(page) : undefined, limit: limit ? parseInt(limit) : undefined });
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.aiUsageLog.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take,
|
||||
skip,
|
||||
select: { id: true, userId: true, model: true, provider: true, inputTokens: true, outputTokens: true, estimatedCost: true, success: true, createdAt: true },
|
||||
}),
|
||||
this.prisma.aiUsageLog.count({ where }),
|
||||
]);
|
||||
return { items, total };
|
||||
// ── Timeline ──
|
||||
@Get('user-timeline')
|
||||
async getUserTimeline(@Query('userId') userId: string, @Query('limit') limit?: string) {
|
||||
return this.service.getUserTimeline(userId, limit ? parseInt(limit) : undefined);
|
||||
}
|
||||
|
||||
// ── Diagnose ──
|
||||
@Get('user-diagnose')
|
||||
async diagnoseUser(@Query('userId') userId: string) { return this.service.diagnoseUser(userId); }
|
||||
@Get('material-diagnose')
|
||||
async diagnoseMaterial(@Query('materialId') materialId: string) { return this.service.diagnoseMaterial(materialId); }
|
||||
|
||||
// ── Anomalies ──
|
||||
@Get('anomalies')
|
||||
async getAnomalies(@Query() query: PaginationQuery) { return this.service.getAnomalies(query); }
|
||||
|
||||
// ── Temporary Materials ──
|
||||
@Get('temporary-materials')
|
||||
async getTemporaryMaterials(@Query() query: PaginationQuery) { return this.service.getTemporaryMaterials(query); }
|
||||
|
||||
// ── Operations ──
|
||||
@Post('recalculate')
|
||||
async recalculateLearningData(@Body() dto: RecalculateDto) { return this.service.recalculateLearningData(dto.userId); }
|
||||
@Get('export')
|
||||
async export(@Query('startDate') startDate?: string, @Query('endDate') endDate?: string, @Query('type') type?: string) {
|
||||
return this.service.export({ startDate, endDate, type });
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ export class AdminLearningService {
|
||||
// ══ Knowledge Bases (for filter dropdown) ══
|
||||
|
||||
async getKnowledgeBases() {
|
||||
// Return knowledge bases that appear in reading_events, with real names from KB table
|
||||
const ids = await this.prisma.readingEvent.findMany({
|
||||
where: { knowledgeBaseId: { not: null } },
|
||||
select: { knowledgeBaseId: true },
|
||||
@ -37,7 +36,6 @@ export class AdminLearningService {
|
||||
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 = {};
|
||||
@ -47,7 +45,6 @@ export class AdminLearningService {
|
||||
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);
|
||||
@ -74,18 +71,14 @@ export class AdminLearningService {
|
||||
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, warningEvents,
|
||||
totalActiveSeconds: 0, // requires aggregation query, keep as 0 for now
|
||||
},
|
||||
overview: { totalEvents, todayEvents, failedEvents, duplicateEvents, warningEvents, totalActiveSeconds: 0 },
|
||||
sessions: { active: activeSessions, interrupted: interruptedSessions, completed: completedSessions, total: totalSessions },
|
||||
users: { activeToday: activeToday.length, totalWithEvents: 0 },
|
||||
materials: { totalRead, totalMarkedRead },
|
||||
recentAnomalies: [], // separate endpoint for this
|
||||
recentAnomalies: [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -93,7 +86,8 @@ export class AdminLearningService {
|
||||
|
||||
async getReadingEvents(query: {
|
||||
userId?: string; materialId?: string; readingTargetType?: string;
|
||||
eventType?: string; status?: string; startDate?: string; endDate?: string;
|
||||
eventType?: string; status?: string; errorCode?: string;
|
||||
startDate?: string; endDate?: string;
|
||||
page?: number; limit?: number; sortBy?: string; order?: 'asc' | 'desc';
|
||||
}) {
|
||||
const where: any = {};
|
||||
@ -102,6 +96,7 @@ export class AdminLearningService {
|
||||
if (query.readingTargetType) where.readingTargetType = query.readingTargetType;
|
||||
if (query.eventType) where.eventType = query.eventType;
|
||||
if (query.status) where.status = query.status;
|
||||
if (query.errorCode) where.errorCode = query.errorCode;
|
||||
if (query.startDate || query.endDate) {
|
||||
where.serverReceivedAt = {};
|
||||
if (query.startDate) where.serverReceivedAt.gte = new Date(query.startDate);
|
||||
@ -117,7 +112,6 @@ export class AdminLearningService {
|
||||
this.prisma.readingEvent.findMany({ where, orderBy, skip: (page - 1) * limit, take: limit }),
|
||||
this.prisma.readingEvent.count({ where }),
|
||||
]);
|
||||
|
||||
return { items, total, page, limit };
|
||||
}
|
||||
|
||||
@ -127,17 +121,6 @@ export class AdminLearningService {
|
||||
return event;
|
||||
}
|
||||
|
||||
async getFailedEvents(query: { page?: number; limit?: number }) {
|
||||
const where = { status: { in: ['failed', 'warning', 'duplicate'] } };
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(query.limit ?? 20, 100);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.readingEvent.findMany({ where, orderBy: { serverReceivedAt: 'desc' }, skip: (page - 1) * limit, take: limit }),
|
||||
this.prisma.readingEvent.count({ where }),
|
||||
]);
|
||||
return { items, total, page, limit };
|
||||
}
|
||||
|
||||
// ══ Sessions ══
|
||||
|
||||
async getSessions(query: { userId?: string; materialId?: string; status?: string; clientSessionId?: string; startDate?: string; endDate?: string; page?: number; limit?: number }) {
|
||||
@ -151,12 +134,10 @@ export class AdminLearningService {
|
||||
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(query.limit ?? 20, 100);
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.learningSession.findMany({ where, orderBy: { startedAt: 'desc' }, skip: (page - 1) * limit, take: limit }),
|
||||
this.prisma.learningSession.count({ where }),
|
||||
]);
|
||||
|
||||
return { items, total, page, limit };
|
||||
}
|
||||
|
||||
@ -166,17 +147,6 @@ export class AdminLearningService {
|
||||
return session;
|
||||
}
|
||||
|
||||
async getInterruptedSessions(query: { page?: number; limit?: number }) {
|
||||
const where = { status: 'interrupted' };
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(query.limit ?? 20, 100);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.learningSession.findMany({ where, orderBy: { startedAt: 'desc' }, skip: (page - 1) * limit, take: limit }),
|
||||
this.prisma.learningSession.count({ where }),
|
||||
]);
|
||||
return { items, total, page, limit };
|
||||
}
|
||||
|
||||
// ══ Progress ══
|
||||
|
||||
async getProgress(query: { userId?: string; materialId?: string; status?: string; page?: number; limit?: number }) {
|
||||
@ -241,6 +211,41 @@ export class AdminLearningService {
|
||||
return r;
|
||||
}
|
||||
|
||||
// ══ AI Analysis ══
|
||||
|
||||
async getAnalysis(query: { userId?: string; page?: number; limit?: number }) {
|
||||
const where: any = {};
|
||||
if (query.userId) where.userId = query.userId;
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(query.limit ?? 20, 100);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.aiAnalysisResult.findMany({
|
||||
where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit,
|
||||
select: { id: true, userId: true, jobId: true, summary: true, masteryScore: true, weaknesses: true, strengths: true, createdAt: true },
|
||||
}),
|
||||
this.prisma.aiAnalysisResult.count({ where }),
|
||||
]);
|
||||
return { items, total, page, limit };
|
||||
}
|
||||
|
||||
// ══ AI Usage ══
|
||||
|
||||
async getAiUsage(query: { userId?: string; model?: string; page?: number; limit?: number }) {
|
||||
const where: any = {};
|
||||
if (query.userId) where.userId = query.userId;
|
||||
if (query.model) where.model = { contains: query.model };
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(query.limit ?? 20, 100);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.aiUsageLog.findMany({
|
||||
where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit,
|
||||
select: { id: true, userId: true, model: true, provider: true, inputTokens: true, outputTokens: true, estimatedCost: true, success: true, createdAt: true },
|
||||
}),
|
||||
this.prisma.aiUsageLog.count({ where }),
|
||||
]);
|
||||
return { items, total, page, limit };
|
||||
}
|
||||
|
||||
// ══ Timeline ══
|
||||
|
||||
async getUserTimeline(userId: string, limit: number = 50) {
|
||||
@ -250,14 +255,12 @@ export class AdminLearningService {
|
||||
take: Math.min(limit, 200),
|
||||
select: { eventId: true, eventType: true, materialId: true, clientTimestampMs: true, status: true },
|
||||
});
|
||||
|
||||
const sessions = await this.prisma.learningSession.findMany({
|
||||
where: { userId },
|
||||
orderBy: { startedAt: 'desc' },
|
||||
take: Math.min(limit, 50),
|
||||
select: { id: true, mode: true, status: true, startedAt: true, endedAt: true, totalActiveSeconds: true },
|
||||
});
|
||||
|
||||
return { events, sessions };
|
||||
}
|
||||
|
||||
@ -274,12 +277,7 @@ export class AdminLearningService {
|
||||
this.prisma.readingEvent.count({ where: { status: 'failed' } }),
|
||||
this.prisma.readingEvent.count({ where: { status: 'duplicate' } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
failed: { items: failed, total: failedTotal },
|
||||
duplicates: { items: duplicates, total: duplicatesTotal },
|
||||
page, limit,
|
||||
};
|
||||
return { failed: { items: failed, total: failedTotal }, duplicates: { items: duplicates, total: duplicatesTotal }, page, limit };
|
||||
}
|
||||
|
||||
// ══ Diagnose ══
|
||||
@ -337,7 +335,6 @@ export class AdminLearningService {
|
||||
const snapshot = await this.snapshotBuilder.buildSnapshot(userId, 'user', userId);
|
||||
return { status: 'recalculated', userId, snapshotId: snapshot.id };
|
||||
}
|
||||
// Recalculate for all active users (limit to 100)
|
||||
const users = await this.prisma.dailyLearningActivity.groupBy({ by: ['userId'], _count: true, take: 100, orderBy: { _count: { userId: 'desc' } } });
|
||||
const snapshots = await Promise.all(users.map(u =>
|
||||
this.snapshotBuilder.buildSnapshot(u.userId, 'user', u.userId).catch(() => null)
|
||||
@ -1,12 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../../infrastructure/database/prisma.module';
|
||||
import { AiRuntimeModule } from '../ai-runtime/ai-runtime.module';
|
||||
import { LearningSessionController } from './learning-session.controller';
|
||||
import { AdminLearningController } from './admin-learning.controller';
|
||||
import { AdminLearningService } from './admin-learning.service';
|
||||
import { LearningSessionService } from './learning-session.service';
|
||||
import { LearningSessionRepository } from './learning-session.repository';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AiRuntimeModule],
|
||||
controllers: [LearningSessionController, AdminLearningController],
|
||||
providers: [LearningSessionService, LearningSessionRepository],
|
||||
providers: [AdminLearningService, LearningSessionService, LearningSessionRepository],
|
||||
exports: [LearningSessionService, LearningSessionRepository],
|
||||
})
|
||||
export class LearningSessionModule {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user