feat: 添加复查功能和批量审核操作

- 复查功能: PUT /assessment/:id/review
  * 支持调整最终总分
  * 记录复查历史(reviewHistory)
  * 保存原始分数(originalScore)
  * 保留复查人、复查时间、复查意见

- 批量审核: POST /question-banks/:bankId/items/batch-review
  * 支持批量通过/拒绝题目
  * 可添加审核意见

- AssessmentSession实体: 添加复查相关字段
This commit is contained in:
Developer
2026-05-13 23:06:40 +08:00
parent 332b14454b
commit 649844a657
7 changed files with 149 additions and 0 deletions
@@ -158,4 +158,21 @@ export class AssessmentController {
knowledgeGroupId,
);
}
@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,
);
}
}
@@ -1438,4 +1438,53 @@ const initialState: Partial<EvaluationState> = {
recentRecords,
};
}
async reviewAssessment(
sessionId: string,
newScore: number,
comment: string | undefined,
reviewerId: string,
tenantId: string,
): Promise<AssessmentSession> {
const session = await this.sessionRepository.findOne({
where: { id: sessionId },
});
if (!session) {
throw new NotFoundException('Assessment session not found');
}
if (session.status !== AssessmentStatus.COMPLETED) {
throw new ForbiddenException('Can only review completed assessments');
}
const reviewRecord = {
reviewedBy: reviewerId,
reviewedAt: new Date().toISOString(),
originalScore: session.finalScore,
newScore: newScore,
comment: comment || '',
};
const reviewHistory = session.reviewHistory || [];
reviewHistory.push(reviewRecord);
if (!session.originalScore) {
session.originalScore = session.finalScore;
}
session.finalScore = newScore;
session.reviewedBy = reviewerId;
session.reviewedAt = new Date();
session.reviewComment = comment || null;
session.reviewHistory = reviewHistory;
await this.sessionRepository.save(session);
this.logger.log(
`[reviewAssessment] Session ${sessionId} reviewed by ${reviewerId}, score changed from ${reviewRecord.originalScore} to ${newScore}`,
);
return session;
}
}
@@ -133,4 +133,18 @@ export class QuestionBankController {
req.user.tenantId,
);
}
@Post(':bankId/items/batch-review')
async batchReviewItems(
@Param('bankId') bankId: string,
@Body() body: { itemIds: string[]; approved: boolean; comment?: string },
) {
this.logger.log(`[batchReview] Reviewing ${body.itemIds.length} items, approved: ${body.approved}`);
return this.questionBankService.batchReviewItems(
bankId,
body.itemIds,
body.approved,
body.comment,
);
}
}
@@ -61,6 +61,9 @@ export class AssessmentSession {
@Column({ type: 'float', name: 'final_score', nullable: true })
finalScore: number;
@Column({ type: 'float', name: 'original_score', nullable: true })
originalScore: number;
@Column({ type: 'text', name: 'final_report', nullable: true })
finalReport: string;
@@ -70,6 +73,18 @@ export class AssessmentSession {
@Column({ type: 'simple-json', name: 'feedback_history', nullable: true })
feedbackHistory: any[];
@Column({ name: 'reviewed_by', nullable: true, type: 'text' })
reviewedBy: string | null;
@Column({ name: 'reviewed_at', nullable: true, type: 'datetime' })
reviewedAt: Date | null;
@Column({ name: 'review_comment', nullable: true, type: 'text' })
reviewComment: string | null;
@Column({ type: 'simple-json', name: 'review_history', nullable: true })
reviewHistory: any[];
@Column({ type: 'int', name: 'current_question_index', default: 0 })
currentQuestionIndex: number;
@@ -406,4 +406,38 @@ export class QuestionBankService {
);
return selected;
}
async batchReviewItems(
bankId: string,
itemIds: string[],
approved: boolean,
comment?: string,
): Promise<QuestionBankItem[]> {
await this.findOne(bankId);
const items = await this.itemRepository.find({
where: itemIds.map(id => ({ id, bankId })),
});
if (items.length !== itemIds.length) {
throw new NotFoundException('Some items not found');
}
const newStatus = approved
? QuestionBankItemStatus.PUBLISHED
: QuestionBankItemStatus.PENDING_REVIEW;
for (const item of items) {
item.status = newStatus;
if (comment) {
item.basis = item.basis
? `${item.basis}\n[审核意见]: ${comment}`
: `[审核意见]: ${comment}`;
}
}
await this.itemRepository.save(items);
this.logger.log(`[batchReview] ${items.length} items ${approved ? 'approved' : 'rejected'}`);
return items;
}
}
+10
View File
@@ -149,4 +149,14 @@ export const questionBankService = {
if (!response.ok) throw new Error('Failed to generate questions');
return await response.json();
},
async batchReviewItems(bankId: string, itemIds: string[], approved: boolean, comment?: string): Promise<QuestionBankItem[]> {
const response = await apiClient.request(`/question-banks/${bankId}/items/batch-review`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemIds, approved, comment }),
});
if (!response.ok) throw new Error('Failed to batch review items');
return await response.json();
},
};
@@ -37,6 +37,16 @@ export class AssessmentStatsService {
const { data } = await apiClient.get<AssessmentStats>(url);
return data;
}
async reviewAssessment(sessionId: string, newScore: number, comment?: string): Promise<any> {
const response = await apiClient.request(`/assessment/${sessionId}/review`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newScore, comment }),
});
if (!response.ok) throw new Error('Failed to review assessment');
return await response.json();
}
}
export const assessmentStatsService = new AssessmentStatsService();