Forráskód Böngészése

修正了以下内容:
1.AIEvaluationService.getSentenceList方法的实现
原实现太过粗糙,粗暴的使用句号作为分隔符
2.调整了AIEvaluationService.evaluation方法的实现,提高了并发性能

Jiang Pengyu 6 hónapja
szülő
commit
a1443c51d0

+ 1 - 1
src/main/java/com/njuzr/eaibackend/controller/AIController.java

@@ -67,7 +67,7 @@ public class AIController {
      * @return
      */
     @PreAuthorize("hasRole('ROLE_STUDENT')")
-    @PostMapping
+    @PostMapping //这个接口实际上没用过,因为创建会话的操作已经放在了AssignmentController里面的参加作业接口里了
     public MyResponse createDialogue(
             @RequestParam Long assignmentId,
             @AuthenticationPrincipal(expression = "id") Long userId

+ 1 - 1
src/main/java/com/njuzr/eaibackend/controller/AssignmentController.java

@@ -222,7 +222,7 @@ public class AssignmentController {
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
-    }
+    }//这段代码写的挺烂的,不知道为什么写成这种古早风格,写个EngagementStageDTO就可以了,有空再改吧
 
     @PreAuthorize("hasRole('TEACHER')")
     @PutMapping("/engage/correct")

+ 8 - 0
src/main/java/com/njuzr/eaibackend/mapper/EvaluationMapperServiceImpl.java

@@ -14,4 +14,12 @@ public class EvaluationMapperServiceImpl extends ServiceImpl<EvaluationMapper, E
                 .eq(Evaluation::getVersion, version)
                 .one();
     }
+
+    public boolean deleteByVersion(Long assignmentId, Long studentId, Integer version) {
+    return this.lambdaUpdate()
+            .eq(Evaluation::getAssignmentId, assignmentId)
+            .eq(Evaluation::getStudentId, studentId)
+            .eq(Evaluation::getVersion, version)
+            .remove();
+}
 }

+ 3 - 3
src/main/java/com/njuzr/eaibackend/service/AIDialogueService.java

@@ -124,10 +124,10 @@ public class AIDialogueService {
 
         if (start > sortedDialogues.size()) {
             start = end = sortedDialogues.size();
-        }
+        } //防止越界
 
         List<AIDialogue.DialogueEntry> pagedDialogues = sortedDialogues.subList(start, end);
-        pagedDialogues.sort(Comparator.comparing(AIDialogue.DialogueEntry::getTimestamp));
+        pagedDialogues.sort(Comparator.comparing(AIDialogue.DialogueEntry::getTimestamp)); //倒序分页 + 正序展示
 
         List<AIEntry> entries = pagedDialogues.stream().map(entry -> ModelMapperUtil.map(entry, AIEntry.class)).collect(Collectors.toList());
 
@@ -151,7 +151,7 @@ public class AIDialogueService {
         }
         return targetRecord.getRecords();
 
-    }
+    }   
 
 
 

+ 101 - 12
src/main/java/com/njuzr/eaibackend/service/AIEvaluationService.java

@@ -19,6 +19,7 @@ import org.springframework.transaction.PlatformTransactionManager;
 import org.springframework.transaction.support.TransactionTemplate;
 import org.springframework.transaction.TransactionDefinition;
 
+import java.text.BreakIterator;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
@@ -98,12 +99,55 @@ public class AIEvaluationService {
         if(engagement == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业参与不存在");
         Assignment assignment = assignmentMapper.selectById(assignmentId);
         if (assignment == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业不存在");
-        Evaluation evaluationJudge = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, engagement.getVersion());
-        if(evaluationJudge != null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业已进行过AI分析");
-        // 先在独立事务中初始化记录,确保其他线程能立即查询到
-        initEvaluationRecord(assignmentId, studentId, engagement);
-        try{
-            // 发起LLM请求
+        /**
+         * AI 作业评估主流程。
+         *
+         * 并发控制说明:
+         * 1. 通过数据库唯一索引 (assignment_id, student_id, version) 保证同一作业版本只能存在一条 evaluation 记录。
+         * 2. 首先调用 initEvaluationRecord() 插入一条占位记录(overall=0),用于抢占执行权限。
+         *    - 插入成功:当前线程获得评估执行权。
+         *    - 插入失败(DuplicateKeyException):说明已有评估任务存在。
+         *
+         * 重复请求处理:
+         * - 若已有记录且 overall=0:
+         *      表示正在进行 AI 分析。
+         *      若时间超过设定阈值(如 10 分钟),视为任务卡死,
+         *      删除占位记录并提示用户重新发起请求。
+         * - 若已有记录且 overall=1:
+         *      表示已完成评估,拒绝重复分析。
+         *
+         * 异常处理:
+         * - 若 AI 请求过程中发生异常,
+         *      删除占位记录以释放执行权,避免残留脏数据导致后续无法重新发起。
+         *
+         * 设计目的:
+         * - 防止用户重复点击或并发请求导致重复执行。
+         * - 防止系统异常导致任务永久卡死。
+         * - 保证 evaluation 表始终只存在一条有效执行记录。
+         */
+        try {
+            initEvaluationRecord(assignmentId, studentId, engagement);
+        } catch (org.springframework.dao.DuplicateKeyException ex) {
+            Evaluation exist = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, engagement.getVersion());
+            if (exist == null) {
+                throw MyException.create(HttpStatus.BAD_REQUEST, "作业正在进行AI分析,请稍后重试");
+            }
+            if (exist.getOverall() == 0) {
+
+                long timeoutMs = 10L * 60 * 1000;
+                Date expireBefore = new Date(System.currentTimeMillis() - timeoutMs);
+
+                if (exist.getTime() != null && exist.getTime().before(expireBefore)){
+                    evaluationMapperServiceImpl.deleteByVersion(assignmentId, studentId, engagement.getVersion());
+                   throw MyException.create(HttpStatus.BAD_REQUEST, "之前的 AI 分析已超时,已为您清理,请重新发起");
+
+                } else {
+                    throw MyException.create(HttpStatus.BAD_REQUEST, "作业正在进行AI分析,请勿重复发起请求");
+                }
+            }
+            throw MyException.create(HttpStatus.BAD_REQUEST, "作业已进行过AI分析");
+        }
+        try {
             String prompt = String.format(PromptConstant.REWRITE_PROMPT_TEMPLATE, assignment.getDescription(), engagement.getTextContent());
             AIRequestService.AIResponse response = requestAIWithRetry(prompt, 0);
             String retContent = response.getChoices().get(0).getMessage().getContent();
@@ -114,10 +158,34 @@ public class AIEvaluationService {
 
             log.info("evaluation cost: {}", sw.formatTime());
             return new AIEntry(role, retContent);
+
         } catch (Exception e) {
-            log.error("evaluation requestAI error: {}", e.getMessage());
-            throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()+":"+"请求失败");
+            log.error("evaluation requestAI error: {}", e.getMessage(), e);
+            evaluationMapperServiceImpl.deleteByVersion(assignmentId, studentId, engagement.getVersion());
+            throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(),HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + ":" + "请求失败,请重新尝试"
+            );
         }
+        // Evaluation evaluationJudge = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, engagement.getVersion()); //如果连点两次,可能会重复
+        // if(evaluationJudge != null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业已进行过AI分析");
+        // 先在独立事务中初始化记录,确保其他线程能立即查询到
+        // initEvaluationRecord(assignmentId, studentId, engagement);
+        // try{
+        //     // 发起LLM请求
+        //     String prompt = String.format(PromptConstant.REWRITE_PROMPT_TEMPLATE, assignment.getDescription(), engagement.getTextContent());
+        //     AIRequestService.AIResponse response = requestAIWithRetry(prompt, 0);
+        //     String retContent = response.getChoices().get(0).getMessage().getContent();
+        //     String role = response.getChoices().get(0).getMessage().getRole();
+
+        //     // 保存评估结果
+        //     saveOverallEvaluationResult(assignmentId, studentId, engagement, retContent);
+
+        //     log.info("evaluation cost: {}", sw.formatTime());
+        //     return new AIEntry(role, retContent);
+        // } catch (Exception e) {
+        //     log.error("evaluation requestAI error: {}", e.getMessage());
+        //     evaluationMapperServiceImpl.deleteByVersion(assignmentId,studentId,engagement.getVersion()); // 删除初始化的记录,避免脏数据
+        //     throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()+":"+"请求失败,请重新尝试");
+        // }
 
     }
 
@@ -221,7 +289,7 @@ public class AIEvaluationService {
 
         for (int i = 0; i < sentences.length; i++) {
             final int sentenceNo = i;
-            final String sentence = sentences[i] + ".";
+            final String sentence = sentences[i];
             futures[i] = CompletableFuture.supplyAsync(() ->
                     processSingleSentenceWithRetry(assignmentId, studentId, content, assignment.getDescription(), sentence, sentenceNo, engagement.getVersion()),
                     aiPerSentenceExecutor);
@@ -268,11 +336,32 @@ public class AIEvaluationService {
         log.info("rewritePerSentence cost: {}", sw.formatTime());
         return sentenceEvaluationsList;
     }
-
+    //原来的getSentenceList方法存在问题,无法正确处理多种标点符号和不同语言的句子分割,这里改用BreakIterator来实现更准确的句子分割
     private static String[] getSentenceList(String content) {
-        String[] sentences = content.split("\\.");
-        return sentences;
+    if (content == null || content.isBlank()) {
+        return new String[0];
+    }
+
+    BreakIterator iterator = BreakIterator.getSentenceInstance(Locale.US);
+    iterator.setText(content);
+
+    List<String> sentences = new ArrayList<>();
+    int start = iterator.first();
+    int end;
+
+    while ((end = iterator.next()) != BreakIterator.DONE) {
+        String sentence = content.substring(start, end).trim();
+        if (!sentence.isEmpty()) {
+            sentences.add(sentence);
+        }
+        start = end;
+    }
+        return sentences.toArray(new String[0]);
     }
+    // private static String[] getSentenceList(String content) {
+    //     String[] sentences = content.split("\\.");
+    //     return sentences;
+    // }
 
     private SentenceEvaluation processSingleSentenceWithRetry(Long assignmentId, Long studentId, String content, String description, String sentence, int sentenceNo, int version) {
         int retryTime = 0;

+ 1 - 1
src/main/java/com/njuzr/eaibackend/service/impl/CourseServiceImpl.java

@@ -231,7 +231,7 @@ public class CourseServiceImpl implements CourseService {
             throw MyException.create(HttpStatus.BAD_REQUEST, "课程不存在");
 
         if (targetUser.getRole() != Role.ADMIN) {
-            List<String> teacherIds = Collections.singletonList(Optional.ofNullable(targetCourse.getTeacherIds())
+            List<String> teacherIds = Collections.singletonList(Optional.ofNullable(targetCourse.getTeacherIds()) //只满足任课老师只有一个的情况
                     .orElse(Collections.emptyList().toString()));
             String userIdStr = String.valueOf(targetUser.getId());
             boolean hasTeacherId = teacherIds.contains(userIdStr);