diff --git a/server/src/assessment/assessment.controller.ts b/server/src/assessment/assessment.controller.ts index 2a9650e..6266829 100644 --- a/server/src/assessment/assessment.controller.ts +++ b/server/src/assessment/assessment.controller.ts @@ -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, + ); + } } diff --git a/server/src/assessment/assessment.service.ts b/server/src/assessment/assessment.service.ts index 977ce75..9c34b23 100644 --- a/server/src/assessment/assessment.service.ts +++ b/server/src/assessment/assessment.service.ts @@ -1438,4 +1438,53 @@ const initialState: Partial = { recentRecords, }; } + + async reviewAssessment( + sessionId: string, + newScore: number, + comment: string | undefined, + reviewerId: string, + tenantId: string, + ): Promise { + 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; + } } diff --git a/server/src/assessment/controllers/question-bank.controller.ts b/server/src/assessment/controllers/question-bank.controller.ts index 6dcbc1a..05bcc4f 100644 --- a/server/src/assessment/controllers/question-bank.controller.ts +++ b/server/src/assessment/controllers/question-bank.controller.ts @@ -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, + ); + } } \ No newline at end of file diff --git a/server/src/assessment/entities/assessment-session.entity.ts b/server/src/assessment/entities/assessment-session.entity.ts index 2410cb5..3420481 100644 --- a/server/src/assessment/entities/assessment-session.entity.ts +++ b/server/src/assessment/entities/assessment-session.entity.ts @@ -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; diff --git a/server/src/assessment/services/question-bank.service.ts b/server/src/assessment/services/question-bank.service.ts index 69cba61..fd51485 100644 --- a/server/src/assessment/services/question-bank.service.ts +++ b/server/src/assessment/services/question-bank.service.ts @@ -406,4 +406,38 @@ export class QuestionBankService { ); return selected; } + + async batchReviewItems( + bankId: string, + itemIds: string[], + approved: boolean, + comment?: string, + ): Promise { + 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; + } } \ No newline at end of file diff --git a/web/services/questionBankService.ts b/web/services/questionBankService.ts index be2882d..b659a7a 100644 --- a/web/services/questionBankService.ts +++ b/web/services/questionBankService.ts @@ -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 { + 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(); + }, }; \ No newline at end of file diff --git a/web/src/services/assessmentStatsService.ts b/web/src/services/assessmentStatsService.ts index 2aa1ba2..80cbe27 100644 --- a/web/src/services/assessmentStatsService.ts +++ b/web/src/services/assessmentStatsService.ts @@ -37,6 +37,16 @@ export class AssessmentStatsService { const { data } = await apiClient.get(url); return data; } + + async reviewAssessment(sessionId: string, newScore: number, comment?: string): Promise { + 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(); \ No newline at end of file