Files
aurak/server/src/assessment/assessment.controller.ts
T
Developer 68371922ca P0-1/P0-2/P1-1: dimensions form + E2E tests + PDF export
P0-1 Backend: dimensions column on template entity + validation
P0-1 Frontend: dimensions edit UI in TemplateManager
P0-2: routeAfterGrading unit tests (10 cases), service spec fix + certificate tests, jest-e2e.json
P1-1: proper PDF generation with embedded CJK font via pdf-lib low-level API
2026-05-19 08:42:03 +08:00

283 lines
8.3 KiB
TypeScript

import {
Controller,
Post,
Body,
Get,
Param,
UseGuards,
Request,
Req,
Sse,
MessageEvent,
Query,
Delete,
Put,
ForbiddenException,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
import { AssessmentService } from './assessment.service';
import { ExportService } from './services/export.service';
import { CombinedAuthGuard } from '../auth/combined-auth.guard';
import { Public } from '../auth/public.decorator';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
@ApiTags('Assessment')
@Controller('assessment')
@UseGuards(CombinedAuthGuard)
export class AssessmentController {
constructor(
private readonly assessmentService: AssessmentService,
private readonly exportService: ExportService,
) {}
@Post('start')
@ApiOperation({ summary: 'Start a new assessment session' })
async startSession(
@Request() req: any,
@Body()
body: { knowledgeBaseId?: string; language?: string; templateId?: string },
) {
const { id: userId, tenantId } = req.user;
console.log(
`[AssessmentController] startSession: user=${userId}, tenant=${tenantId}, templateId=${body.templateId}, kbId=${body.knowledgeBaseId}`,
);
return this.assessmentService.startSession(
userId,
body.knowledgeBaseId,
tenantId,
body.language,
body.templateId,
);
}
@Post(':id/answer')
@ApiOperation({ summary: 'Submit an answer to the current question' })
async submitAnswer(
@Request() req: any,
@Param('id') sessionId: string,
@Body() body: { answer: string; language?: string },
) {
const { id: userId } = req.user;
console.log(
`[AssessmentController] >>> submitAnswer CALLED: user=${userId}, session=${sessionId}, answerLen=${body.answer?.length}`,
);
return this.assessmentService.submitAnswer(
sessionId,
userId,
body.answer,
body.language,
);
}
@Sse(':id/start-stream')
@ApiOperation({ summary: 'Stream initial session generation' })
startSessionStream(@Request() req: any, @Param('id') sessionId: string) {
const { id: userId } = req.user;
console.log(
`[AssessmentController] startSessionStream: user=${userId}, session=${sessionId}`,
);
return this.assessmentService
.startSessionStream(sessionId, userId)
.pipe(map((data) => ({ data }) as MessageEvent));
}
@Sse(':id/answer-stream')
@ApiOperation({
summary: 'Stream answer evaluation and next question generation',
})
submitAnswerStream(
@Request() req: any,
@Param('id') sessionId: string,
@Query('answer') answer: string,
@Query('language') language?: string,
) {
const { id: userId } = req.user;
console.log(
`[AssessmentController] >>> submitAnswerStream CALLED: user=${userId}, session=${sessionId}, answerLen=${answer?.length}, lang=${language}`,
);
return this.assessmentService
.submitAnswerStream(sessionId, userId, answer, language)
.pipe(map((data) => ({ data }) as MessageEvent));
}
@Get(':id/state')
@ApiOperation({ summary: 'Get the current state of an assessment session' })
async getSessionState(@Request() req: any, @Param('id') sessionId: string) {
const { id: userId } = req.user;
console.log(
`[AssessmentController] getSessionState: user=${userId}, session=${sessionId}`,
);
return this.assessmentService.getSessionState(sessionId, userId);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete an assessment session' })
async deleteSession(@Request() req: any, @Param('id') sessionId: string) {
const user = req.user;
console.log(
`[AssessmentController] deleteSession: user=${user.id}, role=${user.role}, session=${sessionId}`,
);
return this.assessmentService.deleteSession(sessionId, user);
}
@Get(':id/certificate')
@ApiOperation({ summary: 'Get certificate for completed assessment' })
async getCertificate(
@Request() req: any,
@Param('id') sessionId: string,
) {
const { id: userId, tenantId } = req.user;
console.log(
`[AssessmentController] getCertificate: user=${userId}, session=${sessionId}`,
);
return this.assessmentService.generateCertificate(sessionId, userId, tenantId);
}
@Public()
@Get('certificate/verify/:certificateId')
@ApiOperation({ summary: 'Verify certificate by ID (public)' })
async verifyCertificate(
@Param('certificateId') certificateId: string,
) {
return this.assessmentService.verifyCertificate(certificateId);
}
@Public()
@Get('certificate/public/:sessionId')
@ApiOperation({ summary: 'Get public certificate info for verification' })
async getPublicCertificate(
@Param('sessionId') sessionId: string,
) {
return this.assessmentService.getPublicCertificateInfo(sessionId);
}
@Get('history')
@ApiOperation({ summary: 'Get current user assessment history (keep latest 3)' })
async getHistory(
@Request() req: any,
) {
const { id: userId } = req.user;
return this.assessmentService.getUserHistory(userId);
}
@Get('stats')
@ApiOperation({ summary: 'Get assessment statistics for admin' })
async getStats(
@Request() req: any,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
@Query('templateId') templateId?: string,
@Query('knowledgeGroupId') knowledgeGroupId?: string,
) {
const { id: userId, tenantId, role } = req.user;
console.log(
`[AssessmentController] getStats: user=${userId}, role=${role}, tenant=${tenantId}`,
);
return this.assessmentService.getStats(
userId,
tenantId,
role,
startDate,
endDate,
templateId,
knowledgeGroupId,
);
}
@Get('stats/radar')
@ApiOperation({ summary: 'Get radar chart data for dimension scores' })
async getRadarStats(
@Request() req: any,
@Query('templateId') templateId?: string,
) {
const { id: userId, tenantId, role } = req.user;
return this.assessmentService.getRadarStats(
userId,
tenantId,
role,
templateId,
);
}
@Get('stats/trend')
@ApiOperation({ summary: 'Get trend data for scores over time' })
async getTrendStats(
@Request() req: any,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
const { id: userId, tenantId, role } = req.user;
return this.assessmentService.getTrendStats(
userId,
tenantId,
role,
startDate,
endDate,
);
}
@Put(':id/review')
@ApiOperation({ summary: 'Review assessment - adjust final score' })
async review(
@Param('id') sessionId: string,
@Body() body: { newScore: number; comment?: string },
@Req() req: any,
) {
const { id: userId, tenantId } = req.user;
return this.assessmentService.reviewAssessment(
sessionId,
body.newScore,
body.comment,
userId,
tenantId,
);
}
@Get(':id/time-check')
@ApiOperation({ summary: 'Check assessment time limits' })
async checkTimeLimits(@Param('id') sessionId: string) {
return this.assessmentService.checkTimeLimits(sessionId);
}
@Post(':id/next-question')
@ApiOperation({ summary: 'Start timing for next question' })
async nextQuestion(@Param('id') sessionId: string) {
await this.assessmentService.updateQuestionStartTime(sessionId);
return { success: true };
}
@Post(':id/force-end')
@ApiOperation({ summary: 'Force end assessment (admin only)' })
async forceEnd(
@Param('id') sessionId: string,
@Request() req: any,
) {
const { role } = req.user;
const isAdmin = role === 'super_admin' || role === 'admin';
if (!isAdmin) {
throw new ForbiddenException('Only admin can force end assessment');
}
return this.assessmentService.forceEndAssessment(sessionId);
}
@Get(':id/export/excel')
@ApiOperation({ summary: 'Export assessment to Excel' })
async exportExcel(@Param('id') sessionId: string) {
const buffer = await this.exportService.exportToExcel(sessionId);
return {
filename: `assessment-${sessionId}.xlsx`,
buffer: buffer.toString('base64'),
};
}
@Get(':id/export/pdf')
@ApiOperation({ summary: 'Export assessment to PDF' })
async exportPdf(@Param('id') sessionId: string) {
const buffer = await this.exportService.exportToPdf(sessionId);
return {
filename: `assessment-${sessionId}.pdf`,
buffer: buffer.toString('base64'),
};
}
}