diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java index dddc697..2024178 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java @@ -21,6 +21,8 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; import java.util.Date; import java.util.UUID; @@ -44,6 +46,8 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ public TeachingClassStudentMapper teachingClassStudentMapper; @Autowired public QwenChatClient qwenChatClient; + @Autowired + public PlatformTransactionManager assessmentCompletionTransactionManager; @Override public AiTrainingEvaluationView get(String taskKey, JwtUser user) { @@ -116,8 +120,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ completed.setAssessmentModel("qwen"); completed.setAssessmentCompletedAt(completedAt); completed.setUpdateTime(completedAt); - aiTrainingEvaluationMapper.completeAssessment(completed); - studentTrainingAnswerMapper.updateAiAssessmentScore(context.answer.getId(), score, completedAt); + completeAssessmentAndWriteScore(completed, context.answer.getId(), score, completedAt); completed.setHelpStatus(evaluation.getHelpStatus()); completed.setAssessmentStatus(SUCCEEDED); return toView(completed); @@ -142,6 +145,22 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ 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) { AiTrainingEvaluation evaluation = aiTrainingEvaluationMapper.findByStudentClassAndTask( user.getUserId(), context.teachingClassId, context.task.getId()); diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java index ecaeec1..d87dd0a 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java @@ -14,6 +14,10 @@ import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.security.JwtUser; import org.junit.jupiter.api.Test; 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.nio.charset.StandardCharsets; @@ -72,6 +76,8 @@ class AiTrainingEvaluationServiceImplTest { .thenReturn(null, evaluation); when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1); 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()); @@ -79,6 +85,43 @@ class AiTrainingEvaluationServiceImplTest { 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 void assessmentRejectsInvalidCriteriaTotal() { AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); @@ -216,6 +259,9 @@ class AiTrainingEvaluationServiceImplTest { service.studentTrainingAnswerMapper = mock(StudentTrainingAnswerMapper.class); service.teachingClassStudentMapper = mock(TeachingClassStudentMapper.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()); TeachingClassStudent member = new TeachingClassStudent(); member.setTeachingClassId("class-1"); @@ -224,6 +270,34 @@ class AiTrainingEvaluationServiceImplTest { 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() { TrainingTask task = new TrainingTask(); task.setId("task-1");