forked from hangshuo652/aurak
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>
This commit is contained in:
@@ -119,6 +119,14 @@ export class AssessmentController {
|
||||
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) {
|
||||
|
||||
@@ -427,6 +427,36 @@ private async getModel(tenantId: string): Promise<ChatOpenAI> {
|
||||
this.logger.debug(
|
||||
`[startSession] Found template: ${template?.name}, linked group: ${template?.knowledgeGroupId}`,
|
||||
);
|
||||
|
||||
// P2: Check attempt limit
|
||||
if (template.attemptLimit > 0) {
|
||||
const attemptCount = await this.sessionRepository.count({
|
||||
where: { userId, templateId, status: AssessmentStatus.COMPLETED },
|
||||
});
|
||||
if (attemptCount >= template.attemptLimit) {
|
||||
throw new BadRequestException(
|
||||
`已达到最大尝试次数 ${template.attemptLimit}/${template.attemptLimit}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// P2: Check scheduled window
|
||||
if (template.scheduledStart) {
|
||||
const start = new Date(template.scheduledStart);
|
||||
if (Date.now() < start.getTime()) {
|
||||
throw new BadRequestException(
|
||||
`考试尚未开始,预定时间: ${start.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (template.scheduledEnd) {
|
||||
const end = new Date(template.scheduledEnd);
|
||||
if (Date.now() > end.getTime()) {
|
||||
throw new BadRequestException(
|
||||
'考试已结束,超过预定截止时间',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use kbId if provided, otherwise fall back to template's group ID
|
||||
@@ -497,6 +527,11 @@ private async getModel(tenantId: string): Promise<ChatOpenAI> {
|
||||
style: template.style,
|
||||
dimensions: template.dimensions,
|
||||
linkedGroupIds: template.linkedGroupIds,
|
||||
attemptLimit: template.attemptLimit,
|
||||
reviewMode: template.reviewMode,
|
||||
shuffleQuestions: template.shuffleQuestions,
|
||||
scheduledStart: template.scheduledStart,
|
||||
scheduledEnd: template.scheduledEnd,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -572,6 +607,14 @@ private async getModel(tenantId: string): Promise<ChatOpenAI> {
|
||||
templateData.questionAnswerKey = answerKey;
|
||||
}
|
||||
|
||||
// P2: Shuffle questions per candidate
|
||||
if (template?.shuffleQuestions !== false && questionsFromBank.length > 1) {
|
||||
for (let i = questionsFromBank.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[questionsFromBank[i], questionsFromBank[j]] = [questionsFromBank[j], questionsFromBank[i]];
|
||||
}
|
||||
}
|
||||
|
||||
questionSource = 'bank';
|
||||
this.logger.log(
|
||||
`[startSession] Selected ${questionsFromBank.length} questions from question bank`,
|
||||
@@ -1252,10 +1295,48 @@ const initialState: Partial<EvaluationState> = {
|
||||
values.feedbackHistory = this.mapMessages(values.feedbackHistory);
|
||||
}
|
||||
|
||||
return this.sanitizeStateForClient(
|
||||
values,
|
||||
session.status !== AssessmentStatus.COMPLETED,
|
||||
);
|
||||
// Determine stripAnswers: strip if in-progress, or if completed but reviewMode is 'none'
|
||||
let stripAnswers = session.status !== AssessmentStatus.COMPLETED;
|
||||
if (session.status === AssessmentStatus.COMPLETED) {
|
||||
const templateData = session.templateJson as any;
|
||||
const reviewMode = templateData?.reviewMode || 'none';
|
||||
if (reviewMode === 'none') {
|
||||
stripAnswers = true;
|
||||
}
|
||||
}
|
||||
return this.sanitizeStateForClient(values, stripAnswers);
|
||||
}
|
||||
|
||||
/**
|
||||
* P2: Get completed session review with correct answers.
|
||||
* Requires reviewMode != 'none' on the template.
|
||||
*/
|
||||
async getSessionReview(sessionId: string, userId: string): Promise<any> {
|
||||
this.logger.log(`getSessionReview: session=${sessionId}, user=${userId}`);
|
||||
const session = await this.sessionRepository.findOne({
|
||||
where: { id: sessionId, userId },
|
||||
});
|
||||
if (!session) throw new NotFoundException('Session not found');
|
||||
if (session.status !== AssessmentStatus.COMPLETED) {
|
||||
throw new BadRequestException('只能在考核完成后查看回顾');
|
||||
}
|
||||
|
||||
const templateData = session.templateJson as any;
|
||||
const reviewMode = templateData?.reviewMode || 'none';
|
||||
if (reviewMode === 'none') {
|
||||
throw new BadRequestException('当前模板未开启答题回顾功能');
|
||||
}
|
||||
|
||||
// Return state with answers visible
|
||||
await this.ensureGraphState(sessionId, session);
|
||||
const state = await this.graph.getState({
|
||||
configurable: { thread_id: sessionId },
|
||||
});
|
||||
const values = { ...state.values };
|
||||
if (values.messages) values.messages = this.mapMessages(values.messages);
|
||||
if (values.feedbackHistory) values.feedbackHistory = this.mapMessages(values.feedbackHistory);
|
||||
|
||||
return this.sanitizeStateForClient(values, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,4 +107,31 @@ export class CreateTemplateDto {
|
||||
@Min(30)
|
||||
@Max(3600)
|
||||
perQuestionTimeLimit?: number;
|
||||
|
||||
/** P2: Max attempts (0=unlimited) */
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(99)
|
||||
@IsOptional()
|
||||
attemptLimit?: number = 1;
|
||||
|
||||
/** P2: Scheduled window start */
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
scheduledStart?: string | null;
|
||||
|
||||
/** P2: Scheduled window end */
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
scheduledEnd?: string | null;
|
||||
|
||||
/** P2: Review mode */
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
reviewMode?: string = 'none';
|
||||
|
||||
/** P2: Shuffle questions */
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
shuffleQuestions?: boolean = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user