Files
aurak/server/src/assessment/assessment.controller.ts
T
Developer 46a10ba091 P2全部完成: 尝试限制/预约时段/题目回顾/随机排序
后端:
- assessment-template entity: attemptLimit/scheduledStart/End/reviewMode/shuffleQuestions
- DTO 更新: 新增 P2 字段验证
- startSession: 尝试次数检查、预约时段检查、题目随机排序
- getSessionState: reviewMode 控制答案可见性
- 新增 GET /assessment/:id/review 回顾端点

前端:
- AssessmentTemplateManager: 新增尝试次数/答题回顾/题目排序/预约时段配置
- AssessmentView: 答题回顾按钮(完成页)+提交确认弹窗+标记回头功能
- types.ts: 新增 P2 字段类型
- assessmentService: 新增 getReview 方法
- 进度导航点: 可视化题序+标记状态

测试 20项全部通过 + 系统测试 142项全部通过 

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:57:32 +08:00

326 lines
11 KiB
TypeScript

import {
Controller,
Post,
Body,
Get,
Param,
UseGuards,
Request,
Req,
Sse,
MessageEvent,
Query,
Delete,
Put,
ForbiddenException,
Logger,
} from '@nestjs/common';
import { map } from 'rxjs/operators';
import { AssessmentService } from './assessment.service';
import { ExportService } from './services/export.service';
import { AuditLogService } from './services/audit-log.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 {
private readonly logger = new Logger(AssessmentController.name);
constructor(
private readonly assessmentService: AssessmentService,
private readonly exportService: ExportService,
private readonly auditLog: AuditLogService,
) {}
@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;
this.logger.log(
`startSession: user=${userId}, tenant=${tenantId}, templateId=${body.templateId}, kbId=${body.knowledgeBaseId}`,
);
const session = await this.assessmentService.startSession(
userId,
body.knowledgeBaseId,
tenantId,
body.language,
body.templateId,
);
this.auditLog.log({ userId, tenantId, action: 'session.start', resourceType: 'assessment_session', resourceId: session.id });
return session;
}
@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, tenantId } = req.user;
this.logger.log(
`submitAnswer: user=${userId}, session=${sessionId}, answerLen=${body.answer?.length}`,
);
const result = await this.assessmentService.submitAnswer(
sessionId,
userId,
body.answer,
body.language,
);
this.auditLog.log({ userId, tenantId, action: 'session.answer', resourceType: 'assessment_session', resourceId: sessionId, details: { answerLength: body.answer?.length } });
return result;
}
@Sse(':id/start-stream')
@ApiOperation({ summary: 'Stream initial session generation' })
startSessionStream(@Request() req: any, @Param('id') sessionId: string) {
const { id: userId } = req.user;
this.logger.log(
`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;
this.logger.log(
`submitAnswerStream: 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;
this.logger.log(
`getSessionState: user=${userId}, session=${sessionId}`,
);
return this.assessmentService.getSessionState(sessionId, userId);
}
@Get(':id/review')
@ApiOperation({ summary: 'Get review data for a completed assessment (shows correct answers)' })
async getReview(@Request() req: any, @Param('id') sessionId: string) {
const { id: userId } = req.user;
this.logger.log(`getReview: user=${userId}, session=${sessionId}`);
return this.assessmentService.getSessionReview(sessionId, userId);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete an assessment session' })
async deleteSession(@Request() req: any, @Param('id') sessionId: string) {
const user = req.user;
this.logger.log(
`deleteSession: user=${user.id}, role=${user.role}, session=${sessionId}`,
);
await this.assessmentService.deleteSession(sessionId, user);
this.auditLog.log({ userId: user.id, tenantId: user.tenantId, action: 'session.delete', resourceType: 'assessment_session', resourceId: sessionId });
return { success: true };
}
@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;
this.logger.log(
`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;
this.logger.log(
`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,
);
}
@Post('batch-delete')
@ApiOperation({ summary: 'Batch delete assessment sessions (admin only)' })
async batchDelete(@Request() req: any, @Body() body: { ids: string[] }) {
const user = req.user;
const isAdmin = user.role?.toLowerCase() === 'super_admin' || user.role?.toLowerCase() === 'admin';
if (!isAdmin) {
throw new ForbiddenException('Only admin can batch delete');
}
const count = await this.assessmentService.batchDeleteSessions(body.ids, user);
this.auditLog.log({ userId: user.id, tenantId: user.tenantId, action: 'session.batch_delete', resourceType: 'assessment_session', details: { count, ids: body.ids } });
return { deleted: count };
}
@Post('batch-export')
@ApiOperation({ summary: 'Batch export assessments as JSON array' })
async batchExport(@Request() req: any, @Body() body: { ids: string[] }) {
const { id: userId } = req.user;
return this.assessmentService.batchExportSessions(body.ids, userId);
}
@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;
const result = await this.assessmentService.reviewAssessment(
sessionId,
body.newScore,
body.comment,
userId,
tenantId,
);
this.auditLog.log({ userId, tenantId, action: 'session.review', resourceType: 'assessment_session', resourceId: sessionId, details: { newScore: body.newScore, comment: body.comment } });
return result;
}
@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 { id: userId, tenantId, role } = req.user;
const isAdmin = role?.toLowerCase() === 'super_admin' || role?.toLowerCase() === 'admin';
if (!isAdmin) {
throw new ForbiddenException('Only admin can force end assessment');
}
const result = await this.assessmentService.forceEndAssessment(sessionId);
this.auditLog.log({ userId, tenantId, action: 'session.force_end', resourceType: 'assessment_session', resourceId: sessionId });
return result;
}
@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 HTML report' })
async exportPdf(@Param('id') sessionId: string) {
const buffer = await this.exportService.exportToPdf(sessionId);
return {
filename: `assessment-${sessionId}.html`,
buffer: buffer.toString('base64'),
};
}
}