fix: atomically persist AI assessment score

main
chenyuan 2 months ago
parent 2d6efbe9ea
commit c6eebbfd63

@ -21,6 +21,8 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
@ -44,6 +46,8 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
public TeachingClassStudentMapper teachingClassStudentMapper; public TeachingClassStudentMapper teachingClassStudentMapper;
@Autowired @Autowired
public QwenChatClient qwenChatClient; public QwenChatClient qwenChatClient;
@Autowired
public PlatformTransactionManager assessmentCompletionTransactionManager;
@Override @Override
public AiTrainingEvaluationView get(String taskKey, JwtUser user) { public AiTrainingEvaluationView get(String taskKey, JwtUser user) {
@ -116,8 +120,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
completed.setAssessmentModel("qwen"); completed.setAssessmentModel("qwen");
completed.setAssessmentCompletedAt(completedAt); completed.setAssessmentCompletedAt(completedAt);
completed.setUpdateTime(completedAt); completed.setUpdateTime(completedAt);
aiTrainingEvaluationMapper.completeAssessment(completed); completeAssessmentAndWriteScore(completed, context.answer.getId(), score, completedAt);
studentTrainingAnswerMapper.updateAiAssessmentScore(context.answer.getId(), score, completedAt);
completed.setHelpStatus(evaluation.getHelpStatus()); completed.setHelpStatus(evaluation.getHelpStatus());
completed.setAssessmentStatus(SUCCEEDED); completed.setAssessmentStatus(SUCCEEDED);
return toView(completed); return toView(completed);
@ -142,6 +145,22 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return new EvaluationContext(task, member.getTeachingClassId(), answer); return new EvaluationContext(task, member.getTeachingClassId(), answer);
} }
private void completeAssessmentAndWriteScore(AiTrainingEvaluation completed,
String answerId,
int score,
Date completedAt) {
TransactionTemplate transactionTemplate = new TransactionTemplate(assessmentCompletionTransactionManager);
transactionTemplate.execute(status -> {
if (aiTrainingEvaluationMapper.completeAssessment(completed) != 1) {
throw new IllegalStateException("Could not complete AI assessment");
}
if (studentTrainingAnswerMapper.updateAiAssessmentScore(answerId, score, completedAt) != 1) {
throw new IllegalStateException("Could not save AI assessment score");
}
return null;
});
}
private AiTrainingEvaluation getOrCreate(EvaluationContext context, JwtUser user) { private AiTrainingEvaluation getOrCreate(EvaluationContext context, JwtUser user) {
AiTrainingEvaluation evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask( AiTrainingEvaluation evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask(
user.getUserId(), context.teachingClassId, context.task.getId()); user.getUserId(), context.teachingClassId, context.task.getId());

@ -14,6 +14,10 @@ import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.config.security.JwtUser;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.SimpleTransactionStatus;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -72,6 +76,8 @@ class AiTrainingEvaluationServiceImplTest {
.thenReturn(null, evaluation); .thenReturn(null, evaluation);
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1); when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(assessmentJson()); when(service.qwenChatClient.completeJson(any(), any())).thenReturn(assessmentJson());
when(service.aiTrainingEvaluationMapper.completeAssessment(any(AiTrainingEvaluation.class))).thenReturn(1);
when(service.studentTrainingAnswerMapper.updateAiAssessmentScore(eq("answer-1"), eq(86), any(Date.class))).thenReturn(1);
AiTrainingEvaluationView view = service.generateAssessment("new-product-survey", student()); AiTrainingEvaluationView view = service.generateAssessment("new-product-survey", student());
@ -79,6 +85,43 @@ class AiTrainingEvaluationServiceImplTest {
verify(service.studentTrainingAnswerMapper).updateAiAssessmentScore(eq("answer-1"), eq(86), any(Date.class)); verify(service.studentTrainingAnswerMapper).updateAiAssessmentScore(eq("answer-1"), eq(86), any(Date.class));
} }
@Test
void assessmentScoreWriteFailureRollsBackCompletionAndLeavesEvaluationRetryable() throws Exception {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
RecordingTransactionManager transactionManager = new RecordingTransactionManager(evaluation);
service.assessmentCompletionTransactionManager = transactionManager;
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
.thenReturn(null, evaluation);
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenAnswer(invocation -> {
evaluation.setAssessmentStatus("PROCESSING");
return 1;
});
when(service.qwenChatClient.completeJson(any(), any())).thenAnswer(invocation -> {
assertTrue(!transactionManager.transactionActive);
return assessmentJson();
});
when(service.aiTrainingEvaluationMapper.completeAssessment(any(AiTrainingEvaluation.class))).thenAnswer(invocation -> {
evaluation.setAssessmentStatus("SUCCEEDED");
return 1;
});
when(service.studentTrainingAnswerMapper.updateAiAssessmentScore(eq("answer-1"), eq(86), any(Date.class)))
.thenThrow(new IllegalStateException("score write failed"));
when(service.aiTrainingEvaluationMapper.failAssessment(eq("evaluation-1"), any(), any(Date.class))).thenAnswer(invocation -> {
if ("PROCESSING".equals(evaluation.getAssessmentStatus())) {
evaluation.setAssessmentStatus("FAILED");
return 1;
}
return 0;
});
assertThrows(ServiceException.class, () -> service.generateAssessment("new-product-survey", student()));
assertEquals("FAILED", evaluation.getAssessmentStatus());
assertTrue(!"SUCCEEDED".equals(evaluation.getAssessmentStatus()));
assertTrue(transactionManager.rollbackCalled);
}
@Test @Test
void assessmentRejectsInvalidCriteriaTotal() { void assessmentRejectsInvalidCriteriaTotal() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
@ -216,6 +259,9 @@ class AiTrainingEvaluationServiceImplTest {
service.studentTrainingAnswerMapper = mock(StudentTrainingAnswerMapper.class); service.studentTrainingAnswerMapper = mock(StudentTrainingAnswerMapper.class);
service.teachingClassStudentMapper = mock(TeachingClassStudentMapper.class); service.teachingClassStudentMapper = mock(TeachingClassStudentMapper.class);
service.qwenChatClient = mock(QwenChatClient.class); service.qwenChatClient = mock(QwenChatClient.class);
service.assessmentCompletionTransactionManager = mock(PlatformTransactionManager.class);
when(service.assessmentCompletionTransactionManager.getTransaction(any(TransactionDefinition.class)))
.thenReturn(new SimpleTransactionStatus());
when(service.trainingTaskMapper.selectByTaskKey("new-product-survey")).thenReturn(task()); when(service.trainingTaskMapper.selectByTaskKey("new-product-survey")).thenReturn(task());
TeachingClassStudent member = new TeachingClassStudent(); TeachingClassStudent member = new TeachingClassStudent();
member.setTeachingClassId("class-1"); member.setTeachingClassId("class-1");
@ -224,6 +270,34 @@ class AiTrainingEvaluationServiceImplTest {
return service; return service;
} }
private static class RecordingTransactionManager implements PlatformTransactionManager {
private final AiTrainingEvaluation evaluation;
private boolean transactionActive;
private boolean rollbackCalled;
private RecordingTransactionManager(AiTrainingEvaluation evaluation) {
this.evaluation = evaluation;
}
@Override
public TransactionStatus getTransaction(TransactionDefinition definition) {
transactionActive = true;
return new SimpleTransactionStatus();
}
@Override
public void commit(TransactionStatus status) {
transactionActive = false;
}
@Override
public void rollback(TransactionStatus status) {
transactionActive = false;
rollbackCalled = true;
evaluation.setAssessmentStatus("PROCESSING");
}
}
private TrainingTask task() { private TrainingTask task() {
TrainingTask task = new TrainingTask(); TrainingTask task = new TrainingTask();
task.setId("task-1"); task.setId("task-1");

Loading…
Cancel
Save