diff --git a/src/main/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationController.java b/src/main/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationController.java index e808330..f7bbf51 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationController.java @@ -3,6 +3,7 @@ package com.sztzjy.linkCommerce.controller.stu; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.config.security.TokenProvider; +import com.sztzjy.linkCommerce.entity.dto.AiTrainingHelpRequest; import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView; import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService; import com.sztzjy.linkCommerce.util.ResultEntity; @@ -13,6 +14,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -38,10 +40,12 @@ public class AiTrainingEvaluationController { @PostMapping("/help") @ApiOperation("Generate AI learning help") - public ResultEntity help(@PathVariable String taskKey, HttpServletRequest request) { + public ResultEntity help(@PathVariable String taskKey, + @RequestBody(required = false) AiTrainingHelpRequest body, + HttpServletRequest request) { try { return new ResultEntity<>(HttpStatus.OK, "AI help generated", - aiTrainingEvaluationService.generateHelp(taskKey, currentUser(request))); + aiTrainingEvaluationService.generateHelp(taskKey, currentUser(request), body == null ? null : body.getQuestion())); } catch (ServiceException e) { return new ResultEntity<>(e.getCode(), e.getMessage()); } diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/AiTrainingHelpRequest.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/AiTrainingHelpRequest.java new file mode 100644 index 0000000..b8ca7ab --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/AiTrainingHelpRequest.java @@ -0,0 +1,20 @@ +package com.sztzjy.linkCommerce.entity.dto; + +public class AiTrainingHelpRequest { + private String question; + + public AiTrainingHelpRequest() { + } + + public AiTrainingHelpRequest(String question) { + this.question = question; + } + + public String getQuestion() { + return question; + } + + public void setQuestion(String question) { + this.question = question; + } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/AiTrainingEvaluationService.java b/src/main/java/com/sztzjy/linkCommerce/service/AiTrainingEvaluationService.java index 67394e7..115a778 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/AiTrainingEvaluationService.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/AiTrainingEvaluationService.java @@ -6,7 +6,7 @@ import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView; public interface AiTrainingEvaluationService { AiTrainingEvaluationView get(String taskKey, JwtUser user); - AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user); + AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user, String question); AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user); } 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 54318b4..290f4e4 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java @@ -33,6 +33,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ private static final String PROCESSING = "PROCESSING"; private static final String SUCCEEDED = "SUCCEEDED"; private static final int MAX_ERROR_LENGTH = 1000; + private static final int MAX_HELP_QUESTION_LENGTH = 500; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -58,10 +59,11 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ } @Override - public AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user) { + public AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user, String question) { + String normalizedQuestion = normalizeHelpQuestion(question); EvaluationContext context = resolveContext(taskKey, user, true); AiTrainingEvaluation evaluation = getOrCreate(context, user); - if (isTerminalOrProcessing(evaluation.getHelpStatus())) { + if (PROCESSING.equals(evaluation.getHelpStatus())) { return toView(evaluation); } if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) { @@ -69,9 +71,9 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ } String taskSnapshot = taskSnapshot(context.task); - String answerSnapshot = answerSnapshot(context.answer); + String answerSnapshot = helpAnswerSnapshot(normalizedQuestion, context.answer); try { - String rawResponse = qwenChatClient.completeJson(helpSystemPrompt(), helpUserPrompt(taskSnapshot, answerSnapshot)); + String rawResponse = qwenChatClient.completeJson(helpSystemPrompt(), helpUserPrompt(taskSnapshot, answerSnapshot, normalizedQuestion)); validateHelp(rawResponse); Date completedAt = new Date(); AiTrainingEvaluation completed = new AiTrainingEvaluation(); @@ -230,6 +232,17 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ return SUCCEEDED.equals(status) || PROCESSING.equals(status); } + private String normalizeHelpQuestion(String question) { + String normalized = StringUtils.trimToEmpty(question); + if (StringUtils.isBlank(normalized)) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "AI help question is required"); + } + if (normalized.length() > MAX_HELP_QUESTION_LENGTH) { + throw new ServiceException(HttpStatus.BAD_REQUEST, "AI help question is too long"); + } + return normalized; + } + private String taskSnapshot(TrainingTask task) { ObjectNode node = objectMapper.createObjectNode(); node.put("background", task.getBackground()); @@ -248,6 +261,13 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ return json(node); } + private String helpAnswerSnapshot(String question, StudentTrainingAnswer answer) { + ObjectNode node = objectMapper.createObjectNode(); + node.put("studentQuestion", question); + node.put("studentAnswers", answerSnapshot(answer)); + return json(node); + } + private String json(JsonNode node) { try { return objectMapper.writeValueAsString(node); @@ -331,8 +351,8 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ return "You are a training learning assistant. Return only JSON with overallDiagnosis, strengths, improvementAreas, and recommendedActions. Do not include a score."; } - private String helpUserPrompt(String taskSnapshot, String answerSnapshot) { - return "Task snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot; + private String helpUserPrompt(String taskSnapshot, String answerSnapshot, String question) { + return "Student question: " + question + "\nTask snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot; } private String assessmentSystemPrompt() { diff --git a/src/main/resources/mappers/AiTrainingEvaluationMapper.xml b/src/main/resources/mappers/AiTrainingEvaluationMapper.xml index f4d3b9d..59f1f62 100644 --- a/src/main/resources/mappers/AiTrainingEvaluationMapper.xml +++ b/src/main/resources/mappers/AiTrainingEvaluationMapper.xml @@ -61,7 +61,7 @@ UPDATE ai_training_evaluation SET help_status = 'PROCESSING', help_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP} WHERE id = #{id,jdbcType=VARCHAR} - AND help_status IN ('NOT_STARTED', 'FAILED') + AND help_status IN ('NOT_STARTED', 'FAILED', 'SUCCEEDED') diff --git a/src/test/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationControllerTest.java b/src/test/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationControllerTest.java index 50228e8..c7e8335 100644 --- a/src/test/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationControllerTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationControllerTest.java @@ -2,6 +2,7 @@ package com.sztzjy.linkCommerce.controller.stu; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.dto.AiTrainingHelpRequest; import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView; import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService; import com.sztzjy.linkCommerce.util.ResultEntity; @@ -23,13 +24,13 @@ class AiTrainingEvaluationControllerTest { controller.aiTrainingEvaluationService = mock(AiTrainingEvaluationService.class); AiTrainingEvaluationView view = new AiTrainingEvaluationView(); view.setHelpStatus("SUCCEEDED"); - when(controller.aiTrainingEvaluationService.generateHelp("task-key", controller.user)).thenReturn(view); + when(controller.aiTrainingEvaluationService.generateHelp("task-key", controller.user, "如何区分核心层和有形层?")).thenReturn(view); - ResultEntity result = controller.help("task-key", mock(HttpServletRequest.class)); + ResultEntity result = controller.help("task-key", new AiTrainingHelpRequest("如何区分核心层和有形层?"), mock(HttpServletRequest.class)); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals("SUCCEEDED", result.getBody().getData().getHelpStatus()); - verify(controller.aiTrainingEvaluationService).generateHelp("task-key", controller.user); + verify(controller.aiTrainingEvaluationService).generateHelp("task-key", controller.user, "如何区分核心层和有形层?"); } @Test 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 0df2bc4..1f578ca 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java @@ -42,7 +42,16 @@ class AiTrainingEvaluationServiceImplTest { void rejectsBlankAllStepAnswers() { AiTrainingEvaluationServiceImpl service = serviceWithContext(answer(null, " ", "", "\t", null)); - assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", student())); + assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", student(), "请给我建议")); + + verify(service.qwenChatClient, never()).completeJson(any(), any()); + } + + @Test + void rejectsBlankHelpQuestionBeforeCallingQwen() { + AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); + + assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", student(), " ")); verify(service.qwenChatClient, never()).completeJson(any(), any()); } @@ -56,7 +65,7 @@ class AiTrainingEvaluationServiceImplTest { when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1); when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson()); - AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student()); + AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student(), "如何完善调研证据?"); ArgumentCaptor saved = ArgumentCaptor.forClass(AiTrainingEvaluation.class); verify(service.aiTrainingEvaluationMapper).completeHelp(saved.capture()); @@ -65,9 +74,26 @@ class AiTrainingEvaluationServiceImplTest { assertTrue(saved.getValue().getHelpTaskSnapshot().contains("objective one")); assertTrue(saved.getValue().getHelpTaskSnapshot().contains("task requirement")); assertTrue(saved.getValue().getHelpAnswerSnapshot().contains("step four")); + assertTrue(saved.getValue().getHelpAnswerSnapshot().contains("如何完善调研证据?")); assertEquals(helpJson(), saved.getValue().getHelpReportJson()); } + @Test + void helpPromptIncludesStudentQuestion() { + AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); + AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED"); + when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1")) + .thenReturn(null, evaluation); + when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1); + when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson()); + + service.generateHelp("new-product-survey", student(), "如何区分核心层和有形层?"); + + ArgumentCaptor userPrompt = ArgumentCaptor.forClass(String.class); + verify(service.qwenChatClient).completeJson(any(), userPrompt.capture()); + assertTrue(userPrompt.getValue().contains("Student question: 如何区分核心层和有形层?")); + } + @Test void assessmentSuccessWritesScoreToAnswer() { AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); @@ -138,17 +164,20 @@ class AiTrainingEvaluationServiceImplTest { } @Test - void succeededHelpReturnsStoredReportWithoutCallingQwenAgain() { + void succeededHelpCanBeRegeneratedForANewQuestion() { AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); AiTrainingEvaluation evaluation = evaluation("SUCCEEDED", "NOT_STARTED"); evaluation.setHelpReportJson(helpJson()); when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1")).thenReturn(evaluation); - AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student()); + when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1); + when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson()); + + AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student(), "请根据新问题给建议"); assertEquals("SUCCEEDED", view.getHelpStatus()); - assertEquals(helpJson(), view.getHelpReportJson()); - verify(service.qwenChatClient, never()).completeJson(any(), any()); + verify(service.qwenChatClient).completeJson(any(), any()); + verify(service.aiTrainingEvaluationMapper).completeHelp(any(AiTrainingEvaluation.class)); } @Test @@ -159,7 +188,7 @@ class AiTrainingEvaluationServiceImplTest { when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1); when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson()); - service.generateHelp("new-product-survey", student()); + service.generateHelp("new-product-survey", student(), "请给出改进建议"); verify(service.qwenChatClient).completeJson(any(), any()); verify(service.aiTrainingEvaluationMapper).completeHelp(any(AiTrainingEvaluation.class)); @@ -195,7 +224,7 @@ class AiTrainingEvaluationServiceImplTest { demo.setDemoMode(true); demo.setDemoTeachingClassId("class-1"); - assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", demo)); + assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", demo, "请给建议")); verify(service.qwenChatClient, never()).completeJson(any(), any()); verify(service.aiTrainingEvaluationMapper, never()).insertIgnore(any()); @@ -248,7 +277,7 @@ class AiTrainingEvaluationServiceImplTest { String xml = readResource("src/main/resources/mappers/AiTrainingEvaluationMapper.xml"); assertTrue(xml.contains("INSERT IGNORE INTO ai_training_evaluation")); - assertTrue(xml.contains("help_status IN ('NOT_STARTED', 'FAILED')")); + assertTrue(xml.contains("help_status IN ('NOT_STARTED', 'FAILED', 'SUCCEEDED')")); assertTrue(xml.contains("assessment_status IN ('NOT_STARTED', 'FAILED')")); assertTrue(xml.contains("help_status = 'SUCCEEDED'")); assertTrue(xml.contains("assessment_status = 'SUCCEEDED'"));