Просмотр исходного кода

Merge branch 'lll_deepseek' into refactor

lalala 4 месяцев назад
Родитель
Сommit
cd09aacfae
19 измененных файлов с 1238 добавлено и 80 удалено
  1. 7 0
      application.yaml
  2. 30 3
      eai.sql
  3. 20 0
      src/main/java/com/njuzr/eaibackend/config/DeepSeekConfig.java
  4. 1 0
      src/main/java/com/njuzr/eaibackend/config/SecurityConfig.java
  5. 65 0
      src/main/java/com/njuzr/eaibackend/constant/EvaluationPromptConstant.java
  6. 44 2
      src/main/java/com/njuzr/eaibackend/controller/AIController.java
  7. 13 0
      src/main/java/com/njuzr/eaibackend/controller/EvaluationController.java
  8. 28 0
      src/main/java/com/njuzr/eaibackend/dto/deepseek/DeepSeekMessage.java
  9. 23 0
      src/main/java/com/njuzr/eaibackend/dto/deepseek/DeepSeekRequest.java
  10. 48 0
      src/main/java/com/njuzr/eaibackend/dto/deepseek/DeepSeekResponse.java
  11. 57 0
      src/main/java/com/njuzr/eaibackend/dto/deepseek/DeepSeekStreamResponse.java
  12. 7 3
      src/main/java/com/njuzr/eaibackend/exception/GlobalExceptionHandler.java
  13. 119 1
      src/main/java/com/njuzr/eaibackend/service/AIDialogueService.java
  14. 244 55
      src/main/java/com/njuzr/eaibackend/service/AIEvaluationService.java
  15. 60 0
      src/main/java/com/njuzr/eaibackend/service/DeepSeekService.java
  16. 5 16
      src/main/java/com/njuzr/eaibackend/service/impl/AssignmentServiceImpl.java
  17. 262 0
      src/main/java/com/njuzr/eaibackend/service/impl/DeepSeekServiceImpl.java
  18. 21 0
      src/main/java/com/njuzr/eaibackend/vo/EvaluationCurrentVO.java
  19. 184 0
      src/test/java/com/njuzr/eaibackend/service/DeepSeekServiceTest.java

+ 7 - 0
application.yaml

@@ -59,6 +59,13 @@ jwt:
     enabled: true
     throttle-seconds: 60
 
+# DeepSeek AI 配置
+deepseek:
+  api-key: ${DEEPSEEK_API_KEY:sk-994409a3b69648de8171be819274fc98}
+  base-url: https://api.deepseek.com/v1
+  model: deepseek-chat
+  timeout: 60000
+
 
 aliyun:
   oss:

+ 30 - 3
eai.sql

@@ -149,7 +149,8 @@ CREATE TABLE `evaluation`  (
   `overall` int NULL DEFAULT NULL COMMENT '整体评分',
   `sentence` int NULL DEFAULT NULL COMMENT '句子评分',
   PRIMARY KEY (`id`) USING BTREE,
-  INDEX `idx_student_assignment`(`student_id`, `assignment_id`) USING BTREE
+  INDEX `idx_student_assignment`(`student_id`, `assignment_id`) USING BTREE,
+  UNIQUE INDEX `uk_evaluation_assignment_student_version`(`assignment_id`, `student_id`, `version`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作业评价表' ROW_FORMAT = Dynamic;
 
 -- ----------------------------
@@ -177,7 +178,8 @@ CREATE TABLE `overall_evaluation`  (
   `version` int NOT NULL DEFAULT 1 COMMENT '批改版本(跟随作业版本)',
   `evaluation` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '整体评估结果',
   PRIMARY KEY (`id`) USING BTREE,
-  INDEX `idx_student_assignment`(`student_id`, `assignment_id`) USING BTREE
+  INDEX `idx_student_assignment`(`student_id`, `assignment_id`) USING BTREE,
+  UNIQUE INDEX `uk_overall_eval_assignment_student_version`(`assignment_id`, `student_id`, `version`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '作业整体评估表' ROW_FORMAT = Dynamic;
 
 -- ----------------------------
@@ -218,9 +220,34 @@ CREATE TABLE `sentence_evaluation`  (
   `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '句子类型',
   `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '句子类别',
   PRIMARY KEY (`id`) USING BTREE,
-  INDEX `idx_student_assignment`(`student_id`, `assignment_id`) USING BTREE
+  INDEX `idx_student_assignment`(`student_id`, `assignment_id`) USING BTREE,
+  UNIQUE INDEX `uk_sentence_eval_assignment_student_version_no`(`assignment_id`, `student_id`, `version`, `no`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 74 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '句子评估表' ROW_FORMAT = Dynamic;
 
+-- ----------------------------
+-- Migration notes (for existing databases)
+-- ----------------------------
+-- If your production DB already contains duplicate rows, the UNIQUE indexes above will fail to create.
+-- Recommended dedup strategy before adding UNIQUE indexes:
+--
+-- 1) evaluation: keep latest by time
+--   DELETE e1 FROM evaluation e1
+--   JOIN evaluation e2
+--     ON e1.assignment_id=e2.assignment_id AND e1.student_id=e2.student_id AND e1.version=e2.version
+--    AND e1.id < e2.id;
+--
+-- 2) overall_evaluation: keep latest row per (assignment_id, student_id, version)
+--   DELETE o1 FROM overall_evaluation o1
+--   JOIN overall_evaluation o2
+--     ON o1.assignment_id=o2.assignment_id AND o1.student_id=o2.student_id AND o1.version=o2.version
+--    AND o1.id < o2.id;
+--
+-- 3) sentence_evaluation: keep latest row per (assignment_id, student_id, version, no)
+--   DELETE s1 FROM sentence_evaluation s1
+--   JOIN sentence_evaluation s2
+--     ON s1.assignment_id=s2.assignment_id AND s1.student_id=s2.student_id AND s1.version=s2.version AND s1.no=s2.no
+--    AND s1.id < s2.id;
+
 -- ----------------------------
 -- Table structure for sign_records
 -- ----------------------------

+ 20 - 0
src/main/java/com/njuzr/eaibackend/config/DeepSeekConfig.java

@@ -0,0 +1,20 @@
+package com.njuzr.eaibackend.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * DeepSeek 配置类
+ * 从 application.yaml 加载配置
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "deepseek")
+public class DeepSeekConfig {
+
+    private String apiKey;
+    private String baseUrl = "https://api.deepseek.com/v1";
+    private String model = "deepseek-chat";
+    private Integer timeout = 60000;
+}

+ 1 - 0
src/main/java/com/njuzr/eaibackend/config/SecurityConfig.java

@@ -61,6 +61,7 @@ public class SecurityConfig {
                                 "/doc/**",
                                 "/api/export/**",
                                 "/api/test",
+                                "/api/ai/stream",  // SSE流式接口已通过JWT认证,跳过Security权限检查
                                 "api/auth/loginByPortal").permitAll() // 允许公开访问的路径
                         .anyRequest().authenticated() // 其他所有请求需要认证
                 )

+ 65 - 0
src/main/java/com/njuzr/eaibackend/constant/EvaluationPromptConstant.java

@@ -0,0 +1,65 @@
+package com.njuzr.eaibackend.constant;
+
+public class EvaluationPromptConstant {
+
+    public static final String OVERALL_EVALUATION_PROMPT = "你是一位专业的英语写作评分专家。请严格按照以下评分标准对作文进行评价。\n\n" +
+            "【评分维度与权重】(满分100分)\n" +
+            "- 内容与观点 (30分):主题相关性、观点明确性、论据充分性、逻辑连贯性\n" +
+            "- 词汇运用 (25分):词汇丰富度、高级词汇使用、词汇准确性、拼写正确性\n" +
+            "- 句型与语法 (25分):句型多样性、语法正确性、主谓一致、时态语态\n" +
+            "- 文章结构 (10分):开头结尾、段落衔接、过渡词使用\n" +
+            "- 语言风格 (10分):正式程度、表达流畅性、创意性\n\n" +
+            "【输出格式要求】\n" +
+            "请按以下Markdown格式输出评价结果:\n\n" +
+            "## 📊 总体评分\n" +
+            "**总分:XX分**\n\n" +
+            "| 维度 | 得分 | 评价 |\n" +
+            "|------|------|------|\n" +
+            "| 内容与观点 | XX/30 | 具体评语 |\n" +
+            "| 词汇运用 | XX/25 | 具体评语 |\n" +
+            "| 句型与语法 | XX/25 | 具体评语 |\n" +
+            "| 文章结构 | XX/10 | 具体评语 |\n" +
+            "| 语言风格 | XX/10 | 具体评语 |\n\n" +
+            "## ✨ 优点\n" +
+            "1. 优点1\n" +
+            "2. 优点2\n" +
+            "3. 优点3\n\n" +
+            "## 💡 改进建议\n" +
+            "1. 改进点1\n" +
+            "2. 改进点2\n" +
+            "3. 改进点3\n\n" +
+            "## 📝 总评\n" +
+            "一两句总体评价\n\n" +
+            "【注意事项】\n" +
+            "- 每个维度的评语要具体指出学生写得好的地方和具体问题\n" +
+            "- 优点和改进点各列出3条最具代表性的\n" +
+            "- 评分要有区分度,避免所有作文都是70-80分\n" +
+            "- 严格按照Markdown表格格式输出\n\n" +
+            "【作文信息】\n" +
+            "作业描述:%s\n\n" +
+            "学生作文:\n" +
+            "%s";
+
+    public static final String SENTENCE_EVALUATION_PROMPT = "你是一位专业的英语写作批改老师。请对以下句子进行批改。\n\n" +
+            "【批改维度】\n" +
+            "1. 词汇:拼写、词性、搭配、准确性\n" +
+            "2. 语法:时态、语态、主谓一致、句型结构\n" +
+            "3. 内容:与主题相关性、逻辑连贯性\n\n" +
+            "【输出格式】\n" +
+            "请严格按以下Markdown格式输出:\n\n" +
+            "## 句子批改\n\n" +
+            "**原句:** xxx\n\n" +
+            "**状态:** ✅ 正确 / ⚠️ 需改进\n\n" +
+            "**问题维度:** 词汇 / 语法 / 内容 / 无\n\n" +
+            "**修改建议:**\n" +
+            "修改后的句子(如果需要)\n\n" +
+            "**原因说明:**\n" +
+            "解释为什么这样改\n\n" +
+            "【作业信息】\n" +
+            "作业描述:%s\n\n" +
+            "学生作文全文:\n" +
+            "%s\n\n" +
+            "待批改的句子:\n" +
+            "%s";
+
+}

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

@@ -1,7 +1,6 @@
 package com.njuzr.eaibackend.controller;
 
 import com.njuzr.eaibackend.dto.AIDTO;
-import com.njuzr.eaibackend.po.AIDialogue;
 import com.njuzr.eaibackend.po.AIEntry;
 import com.njuzr.eaibackend.service.AIDialogueService;
 import com.njuzr.eaibackend.service.AIRequestService;
@@ -9,10 +8,11 @@ import com.njuzr.eaibackend.vo.AIDialogueVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
-import org.springframework.security.access.method.P;
+import org.springframework.http.MediaType;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 /**
  * @author: Leonezhurui
@@ -93,6 +93,48 @@ public class AIController {
         return MyResponse.success(entry);
     }
 
+    /**
+     * 学生请求AI(流式响应)- 使用 DeepSeek
+     * 返回 SSE 流式响应,实时显示AI回复内容
+     * @param dialogueId 对话ID
+     * @param aidto 对话请求DTO
+     * @return SseEmitter 流式响应
+     */
+    @PutMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter requestAIStream(
+            @RequestParam String dialogueId,
+            @RequestBody(required = false) AIDTO aidto
+    ) {
+        log.info("========== 流式接口被调用 ==========");
+        log.info("流式对话请求 - dialogueId={}, aidto={}", dialogueId, aidto);
+        
+        if (aidto == null) {
+            log.error("【错误】请求体为空");
+            SseEmitter emitter = new SseEmitter();
+            emitter.completeWithError(new RuntimeException("请求体不能为空"));
+            return emitter;
+        }
+        if (aidto.getMessages() == null) {
+            log.error("【错误】消息列表为空");
+            SseEmitter emitter = new SseEmitter();
+            emitter.completeWithError(new RuntimeException("消息列表不能为空"));
+            return emitter;
+        }
+        
+        log.info("【参数校验通过】dialogueId={}, 消息数={}", dialogueId, aidto.getMessages().size());
+        
+        try {
+            SseEmitter result = aiDialogueService.requestAIStream(dialogueId, aidto);
+            log.info("【成功】调用aiDialogueService.requestAIStream完成");
+            return result;
+        } catch (Exception e) {
+            log.error("【异常】调用aiDialogueService.requestAIStream失败: {}", e.getMessage(), e);
+            SseEmitter emitter = new SseEmitter();
+            emitter.completeWithError(e);
+            return emitter;
+        }
+    }
+
     @PostMapping("/rewrite")
     public MyResponse rewrite(
             @RequestParam Long assignmentId,

+ 13 - 0
src/main/java/com/njuzr/eaibackend/controller/EvaluationController.java

@@ -1,6 +1,7 @@
 package com.njuzr.eaibackend.controller;
 
 import com.njuzr.eaibackend.service.AIEvaluationService;
+import com.njuzr.eaibackend.vo.EvaluationCurrentVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
@@ -104,6 +105,18 @@ public class EvaluationController {
         return MyResponse.success(aiEvaluationService.getVersionList(assignmentId, studentId));
     }
 
+    /**
+     * 获取当前最新提交版本号(student_assignment.version)与该版本的批改状态。
+     */
+    @GetMapping("/current")
+    public MyResponse getCurrent(
+            @RequestParam Long assignmentId,
+            @RequestParam Long studentId
+    ) {
+        EvaluationCurrentVO vo = aiEvaluationService.getCurrentEvaluationStatus(assignmentId, studentId);
+        return MyResponse.success(vo);
+    }
+
     /**
      * 用于支持单句重试
      * @param assignmentId

+ 28 - 0
src/main/java/com/njuzr/eaibackend/dto/deepseek/DeepSeekMessage.java

@@ -0,0 +1,28 @@
+package com.njuzr.eaibackend.dto.deepseek;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * DeepSeek 消息对象
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeepSeekMessage {
+    private String role;
+    private String content;
+
+    public static DeepSeekMessage user(String content) {
+        return new DeepSeekMessage("user", content);
+    }
+
+    public static DeepSeekMessage assistant(String content) {
+        return new DeepSeekMessage("assistant", content);
+    }
+
+    public static DeepSeekMessage system(String content) {
+        return new DeepSeekMessage("system", content);
+    }
+}

+ 23 - 0
src/main/java/com/njuzr/eaibackend/dto/deepseek/DeepSeekRequest.java

@@ -0,0 +1,23 @@
+package com.njuzr.eaibackend.dto.deepseek;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * DeepSeek 请求对象
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeepSeekRequest {
+    private String model;
+    private List<DeepSeekMessage> messages;
+    private Boolean stream = false;
+    private Double temperature = 0.7;
+    private Integer maxTokens = 2048;
+}

+ 48 - 0
src/main/java/com/njuzr/eaibackend/dto/deepseek/DeepSeekResponse.java

@@ -0,0 +1,48 @@
+package com.njuzr.eaibackend.dto.deepseek;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * DeepSeek 响应对象
+ */
+@Data
+public class DeepSeekResponse {
+    private String id;
+    private String object;
+    private Long created;
+    private String model;
+    private List<Choice> choices;
+    private Usage usage;
+
+    @Data
+    public static class Choice {
+        private Integer index;
+        private DeepSeekMessage message;
+        @JsonProperty("finish_reason")
+        private String finishReason;
+        private Object logprobs;
+    }
+
+    @Data
+    public static class Usage {
+        @JsonProperty("prompt_tokens")
+        private Integer promptTokens;
+        @JsonProperty("completion_tokens")
+        private Integer completionTokens;
+        @JsonProperty("total_tokens")
+        private Integer totalTokens;
+    }
+
+    /**
+     * 获取回复内容
+     */
+    public String getContent() {
+        if (choices == null || choices.isEmpty()) {
+            return null;
+        }
+        return choices.get(0).getMessage().getContent();
+    }
+}

+ 57 - 0
src/main/java/com/njuzr/eaibackend/dto/deepseek/DeepSeekStreamResponse.java

@@ -0,0 +1,57 @@
+package com.njuzr.eaibackend.dto.deepseek;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * DeepSeek 流式响应对象
+ */
+@Data
+public class DeepSeekStreamResponse {
+    private String id;
+    private String object;
+    private Long created;
+    private String model;
+    private List<StreamChoice> choices;
+
+    @Data
+    public static class StreamChoice {
+        private Integer index;
+        private Delta delta;
+        @JsonProperty("finish_reason")
+        private String finishReason;
+        private Object logprobs;
+    }
+
+    @Data
+    public static class Delta {
+        private String role;
+        private String content;
+    }
+
+    /**
+     * 获取流式内容片段
+     */
+    public String getContent() {
+        if (choices == null || choices.isEmpty()) {
+            return null;
+        }
+        Delta delta = choices.get(0).getDelta();
+        if (delta == null) {
+            return null;
+        }
+        return delta.getContent();
+    }
+
+    /**
+     * 是否完成
+     */
+    public boolean isDone() {
+        if (choices == null || choices.isEmpty()) {
+            return false;
+        }
+        return "stop".equals(choices.get(0).getFinishReason());
+    }
+}

+ 7 - 3
src/main/java/com/njuzr/eaibackend/exception/GlobalExceptionHandler.java

@@ -27,7 +27,8 @@ public class GlobalExceptionHandler {
     // 务必注意这里的异常类型:org.springframework.security.access.AccessDeniedException.class
     @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
     public MyResponse handleAccessDeniedException(org.springframework.security.access.AccessDeniedException e) {
-        log.error("全局异常处理器捕获,访问被拒绝,错误如下:"+e.getMessage());
+        log.error("【全局异常】权限被拒绝: {}", e.getMessage());
+        log.error("【全局异常】堆栈: ", e);
         return MyResponse.error(HttpStatus.FORBIDDEN.value(), "权限不够:" + e.getMessage());
     }
 
@@ -45,7 +46,8 @@ public class GlobalExceptionHandler {
 
     @ExceptionHandler(HttpMessageNotReadableException.class)
     public MyResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
-        log.error("全局异常处理器捕获,请求体转换出错,错误如下"+":"+e.getMessage());
+        log.error("【全局异常】请求体转换出错: {}", e.getMessage());
+        log.error("【全局异常】原因: ", e.getCause());
         return MyResponse.error(HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase()+":"+"传递信息错误");
 
     }
@@ -59,7 +61,9 @@ public class GlobalExceptionHandler {
 
     @ExceptionHandler(Exception.class)
     public MyResponse handleGlobalException(Exception e) {
-        log.error("全局异常处理器捕获,错误如下:"+e.getMessage());
+        log.error("【全局异常】捕获到未处理异常: {}", e.getMessage());
+        log.error("【全局异常】异常类型: {}", e.getClass().getName());
+        log.error("【全局异常】完整堆栈: ", e);
         return MyResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器错误: " + e.getMessage());
     }
 

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

@@ -1,6 +1,7 @@
 package com.njuzr.eaibackend.service;
 
 import com.njuzr.eaibackend.dto.AIDTO;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekMessage;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.mapper.AIDialogueMapper;
 import com.njuzr.eaibackend.mapper.AIRewriteRecordMapper;
@@ -22,6 +23,7 @@ import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -48,6 +50,8 @@ public class AIDialogueService {
 
     private final AIRequestService aiRequestService;
 
+    private final DeepSeekService deepSeekService;
+
     private final StudentAssignmentMapper studentAssignmentMapper;
 
     private final AssignmentMapper assignmentMapper;
@@ -55,11 +59,12 @@ public class AIDialogueService {
     private final FileUtil fileUtil = new FileUtil();
 
     @Autowired
-    public AIDialogueService(MongoTemplate mongoTemplate, AIDialogueMapper aiDialogueMapper, AIRewriteRecordMapper aiRewriteRecordMapper, AIRequestService aiRequestService, StudentAssignmentMapper studentAssignmentMapper, AssignmentMapper assignmentMapper) {
+    public AIDialogueService(MongoTemplate mongoTemplate, AIDialogueMapper aiDialogueMapper, AIRewriteRecordMapper aiRewriteRecordMapper, AIRequestService aiRequestService, DeepSeekService deepSeekService, StudentAssignmentMapper studentAssignmentMapper, AssignmentMapper assignmentMapper) {
         this.mongoTemplate = mongoTemplate;
         this.aiDialogueMapper = aiDialogueMapper;
         this.aiRewriteRecordMapper = aiRewriteRecordMapper;
         this.aiRequestService = aiRequestService;
+        this.deepSeekService = deepSeekService;
         this.studentAssignmentMapper = studentAssignmentMapper;
         this.assignmentMapper = assignmentMapper;
     }
@@ -201,6 +206,119 @@ public class AIDialogueService {
         return new AIEntry(role, retContent);
     }
 
+    /**
+     * 系统提示词 - 简洁精炼版
+     */
+    private static final String SYSTEM_PROMPT = "你是英语写作辅导老师,用简洁的中文回复 🌟\n" +
+            "\n" +
+            "【回答风格】\n" +
+            "- 用温暖的中文和学生对话,像朋友一样耐心倾听 💬\n" +
+            "- 适当使用表情符号让交流更生动 😊✨\n" +
+            "- 先肯定学生的努力和想法,再给出建议 👍\n" +
+            "【核心要求】\n" +
+            "- 回复**简短精炼**,控制在100字以内\n" +
+            "- 重点突出,一针见血\n" +
+            "- 用表情符号增加亲切感 �\n" +
+            "\n" +
+             "【内容要求】\n" +
+            "- 详略得当:重点详细讲,次要内容简要提\n" +
+            "- 指出问题时,用'可以这样改...'代替'你错了'\n" +
+            "- 英语例句配中文翻译,让学生轻松理解\n" +
+            "- 结尾给出1条具体可行的操作建议 💡\n" +
+            "\n" +
+            "【思路拓展】当学生询问写作主题时:\n" +
+            "- 热情地提供3-5个不同角度的写作方向 🎯\n" +
+            "- 每个方向给一个生动的例子或切入点\n" +
+            "- 用'你可以试试...'、'不妨考虑...'等建议语气\n" +
+            "- 简要分析每个方向适合什么样的学生\n" ;
+
+
+    /**
+     * 使用 DeepSeek 流式对话
+     * @param dialogueId 对话ID
+     * @param aidto 对话请求DTO
+     * @return SseEmitter 流式响应
+     */
+    public SseEmitter requestAIStream(String dialogueId, AIDTO aidto) {
+        log.info("【Service层】requestAIStream开始 - dialogueId={}", dialogueId);
+        
+        try {
+            // 构造用户消息(用于存储)
+            AIEntry lastEntry = aidto.getMessages().get(aidto.getMessages().size() - 1);
+            log.info("【Service层】最后一条消息 - role={}, content长度={}", lastEntry.getRole(), 
+                    lastEntry.getContent() != null ? lastEntry.getContent().length() : 0);
+            
+            AIDialogue.DialogueEntry userEntry = new AIDialogue.DialogueEntry(
+                    lastEntry.getRole(), 
+                    lastEntry.getContent(), 
+                    System.currentTimeMillis() / 1000
+            );
+
+            // 构造 DeepSeek 消息列表(添加系统提示词)
+            List<DeepSeekMessage> messages = new ArrayList<>();
+            messages.add(DeepSeekMessage.system(SYSTEM_PROMPT));
+            
+            // 添加历史对话上下文
+            int userMsgCount = 0;
+            int assistantMsgCount = 0;
+            for (AIEntry entry : aidto.getMessages()) {
+                if ("user".equals(entry.getRole())) {
+                    messages.add(DeepSeekMessage.user(entry.getContent()));
+                    userMsgCount++;
+                } else if ("assistant".equals(entry.getRole())) {
+                    messages.add(DeepSeekMessage.assistant(entry.getContent()));
+                    assistantMsgCount++;
+                }
+            }
+            log.info("【Service层】消息统计 - user消息={}, assistant消息={}, 总消息数={}", 
+                    userMsgCount, assistantMsgCount, messages.size());
+
+            SseEmitter emitter = new SseEmitter(0L);
+            StringBuilder fullResponse = new StringBuilder();
+            
+            log.info("【Service层】开始调用deepSeekService.chatCompletionStream");
+            
+            // 使用回调方式处理流式响应
+            deepSeekService.chatCompletionStream(messages, 
+                content -> {
+                    // 流式内容回调 - 实时发送给客户端
+                    fullResponse.append(content);
+                    try {
+                        emitter.send(content);
+                    } catch (Exception e) {
+                        log.error("【Service层】发送流式响应失败: {}", e.getMessage());
+                    }
+                },
+                () -> {
+                    // 完成回调 - 保存对话记录到数据库
+                    log.info("【Service层】DeepSeek 流式响应完成,保存对话记录");
+                    AIDialogue.DialogueEntry aiEntry = new AIDialogue.DialogueEntry(
+                            "assistant", 
+                            fullResponse.toString(), 
+                            System.currentTimeMillis() / 1000
+                    );
+                    addDialogueEntry(dialogueId, userEntry, aiEntry);
+                    emitter.complete();
+                    log.info("【Service层】emitter.complete()已调用");
+                },
+                error -> {
+                    // 错误回调
+                    log.error("【Service层】DeepSeek 流式请求失败: {}", error.getMessage());
+                    emitter.completeWithError(error);
+                }
+            );
+            
+            log.info("【Service层】requestAIStream正常返回emitter");
+            return emitter;
+            
+        } catch (Exception e) {
+            log.error("【Service层】requestAIStream异常: {}", e.getMessage(), e);
+            SseEmitter errorEmitter = new SseEmitter();
+            errorEmitter.completeWithError(e);
+            return errorEmitter;
+        }
+    }
+
 
     public AIEntry rewrite(Long assignmentId, Long studentId) {
         // 1 通过assignmentId和studentId找到engagement,获取当前文件链接

+ 244 - 55
src/main/java/com/njuzr/eaibackend/service/AIEvaluationService.java

@@ -1,11 +1,16 @@
 package com.njuzr.eaibackend.service;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.njuzr.eaibackend.constant.EvaluationPromptConstant;
 import com.njuzr.eaibackend.constant.PromptConstant;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekMessage;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekResponse;
 import com.njuzr.eaibackend.exception.MyException;
 import com.njuzr.eaibackend.mapper.*;
 import com.njuzr.eaibackend.po.*;
+import com.njuzr.eaibackend.vo.EvaluationCurrentVO;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.StopWatch;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -18,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.PlatformTransactionManager;
 import org.springframework.transaction.support.TransactionTemplate;
 import org.springframework.transaction.TransactionDefinition;
+import org.springframework.dao.DuplicateKeyException;
 
 import java.text.BreakIterator;
 import java.util.*;
@@ -36,6 +42,7 @@ public class AIEvaluationService {
     private static final int RETRY_COUNT = 3;
 
     private final AIRequestService aiRequestService;
+    private final DeepSeekService deepSeekService;
 
     private final StudentAssignmentMapper studentAssignmentMapper;
 
@@ -62,12 +69,14 @@ public class AIEvaluationService {
     private SentenceEvaluationMapperServiceImpl sentenceEvaluationMapperServiceImpl;
 
     @Autowired
-    public AIEvaluationService(AIRequestService aiRequestService, StudentAssignmentMapper studentAssignmentMapper,
+    public AIEvaluationService(AIRequestService aiRequestService, DeepSeekService deepSeekService,
+            StudentAssignmentMapper studentAssignmentMapper,
             AssignmentMapper assignmentMapper, OverallEvaluationMapper overallEvaluationMapper,
             SentenceEvaluationMapper sentenceEvaluationMapper, EvaluationMapper evaluationMapper,
             EvaluationMapperServiceImpl evaluationMapperServiceImpl,
             PlatformTransactionManager transactionManager) {
         this.aiRequestService = aiRequestService;
+        this.deepSeekService = deepSeekService;
         this.studentAssignmentMapper = studentAssignmentMapper;
         this.assignmentMapper = assignmentMapper;
         this.overallEvaluationMapper = overallEvaluationMapper;
@@ -99,6 +108,10 @@ 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, "作业不存在");
+        final int version = engagement.getVersion();
+        log.info("[EVAL][OVERALL] request start assignmentId={}, studentId={}, version={}, textLen={}",
+                assignmentId, studentId, version,
+                engagement.getTextContent() == null ? -1 : engagement.getTextContent().length());
         /**
          * AI 作业评估主流程。
          *
@@ -125,43 +138,85 @@ public class AIEvaluationService {
          * - 防止系统异常导致任务永久卡死。
          * - 保证 evaluation 表始终只存在一条有效执行记录。
          */
+        // 1) 先检查当前版本是否已完成整体批改:已完成则直接返回历史结果(幂等)
+        Evaluation exist = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, version);
+        log.info("[EVAL][OVERALL] current evaluation record assignmentId={}, studentId={}, version={}, exist={}, overall={}, sentence={}, time={}",
+                assignmentId, studentId, version,
+                exist != null,
+                exist == null ? null : exist.getOverall(),
+                exist == null ? null : exist.getSentence(),
+                exist == null ? null : exist.getTime());
+        if (exist != null && exist.getOverall() != null && exist.getOverall() == 1) {
+            OverallEvaluation overall = getOverallEvaluationIfExists(assignmentId, studentId, version);
+            if (overall != null) {
+                log.info("[EVAL][OVERALL] hit cached result assignmentId={}, studentId={}, version={}, evalLen={}",
+                        assignmentId, studentId, version,
+                        overall.getEvaluation() == null ? -1 : overall.getEvaluation().length());
+                return new AIEntry("assistant", overall.getEvaluation());
+            }
+            // 理论上不应该出现:标记完成但无结果;允许重新触发
+            log.warn("evaluation overall flag=1 but overall_evaluation missing, will re-run. assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+        }
+
+        // 2) 抢占执行权:不存在则插入占位记录;并发时依赖唯一索引冲突
         try {
             initEvaluationRecord(assignmentId, studentId, engagement);
-        } catch (org.springframework.dao.DuplicateKeyException ex) {
-            Evaluation exist = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, engagement.getVersion());
-            if (exist == null) {
+            log.info("[EVAL][OVERALL] init evaluation lock success assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+        } catch (DuplicateKeyException ex) {
+            log.info("[EVAL][OVERALL] init evaluation lock duplicate assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+            Evaluation locked = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, version);
+            if (locked == null) {
                 throw MyException.create(HttpStatus.BAD_REQUEST, "作业正在进行AI分析,请稍后重试");
             }
-            if (exist.getOverall() == 0) {
 
+            // overall==0: 处理中;overall==1: 已完成(上面已处理直接返回)
+            if (locked.getOverall() != null && locked.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分析,请勿重复发起请求");
+                if (locked.getTime() != null && locked.getTime().before(expireBefore)) {
+                    evaluationMapperServiceImpl.deleteByVersion(assignmentId, studentId, version);
+                    throw MyException.create(HttpStatus.BAD_REQUEST, "之前的 AI 分析已超时,已为您清理,请重新发起");
                 }
+                throw MyException.create(HttpStatus.BAD_REQUEST, "作业正在进行AI分析,请勿重复发起请求");
+            }
+
+            OverallEvaluation overall = getOverallEvaluationIfExists(assignmentId, studentId, version);
+            if (overall != null) {
+                log.info("[EVAL][OVERALL] duplicate request but result exists assignmentId={}, studentId={}, version={}, evalLen={}",
+                        assignmentId, studentId, version,
+                        overall.getEvaluation() == null ? -1 : overall.getEvaluation().length());
+                return new AIEntry("assistant", overall.getEvaluation());
             }
             throw MyException.create(HttpStatus.BAD_REQUEST, "作业已进行过AI分析");
         }
+
         try {
-            String prompt = String.format(PromptConstant.REWRITE_PROMPT_TEMPLATE, assignment.getDescription(), engagement.getTextContent());
+            String prompt = String.format(EvaluationPromptConstant.OVERALL_EVALUATION_PROMPT,
+                    assignment.getDescription(), engagement.getTextContent());
+            log.info("[EVAL][OVERALL] llm request start assignmentId={}, studentId={}, version={}, promptLen={}",
+                    assignmentId, studentId, version, prompt.length());
             AIRequestService.AIResponse response = requestAIWithRetry(prompt, 0);
             String retContent = response.getChoices().get(0).getMessage().getContent();
             String role = response.getChoices().get(0).getMessage().getRole();
 
-            // 保存评估结果
+            // 保存Markdown格式的评估结果
             saveOverallEvaluationResult(assignmentId, studentId, engagement, retContent);
+            log.info("[EVAL][OVERALL] save success assignmentId={}, studentId={}, version={}, role={}, evalLen={}, cost={}",
+                    assignmentId, studentId, version, role,
+                    retContent == null ? -1 : retContent.length(),
+                    sw.formatTime());
 
+            log.info("【评分完成】作文长度: {}, 评价长度: {}", 
+                    engagement.getTextContent().length(), retContent.length());
             log.info("evaluation cost: {}", sw.formatTime());
             return new AIEntry(role, retContent);
 
         } catch (Exception e) {
             log.error("evaluation requestAI error: {}", e.getMessage(), e);
-            evaluationMapperServiceImpl.deleteByVersion(assignmentId, studentId, engagement.getVersion());
+            evaluationMapperServiceImpl.deleteByVersion(assignmentId, studentId, version);
             throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(),HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + ":" + "请求失败,请重新尝试"
             );
         }
@@ -207,30 +262,59 @@ public class AIEvaluationService {
 
     @Transactional(rollbackFor = Exception.class)
     public void saveOverallEvaluationResult(Long assignmentId, Long studentId, Engagement engagement, String retContent) {
+        final int version = engagement.getVersion();
         OverallEvaluation overallEvaluation = new OverallEvaluation()
             .setStudentId(studentId)
             .setAssignmentId(assignmentId)
             .setContent(engagement.getTextContent())
             .setEvaluation(retContent)
-            .setVersion(engagement.getVersion());
-        overallEvaluationMapper.insert(overallEvaluation);
+            .setVersion(version);
+        try {
+            overallEvaluationMapper.insert(overallEvaluation);
+            log.info("[EVAL][OVERALL] overall_evaluation insert ok assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+        } catch (DuplicateKeyException e) {
+            // 幂等:同版本重复写入时忽略(依赖唯一索引)
+            log.info("overall_evaluation already exists, ignore duplicate insert. assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+        }
 
         evaluationMapperServiceImpl.update(new UpdateWrapper<Evaluation>()
                 .set("overall", 1)
                 .eq("student_id", studentId)
                 .eq("assignment_id", assignmentId)
-                .eq("version", engagement.getVersion()));
+                .eq("version", version));
+        log.info("[EVAL][OVERALL] evaluation flag update overall=1 assignmentId={}, studentId={}, version={}",
+                assignmentId, studentId, version);
     }
 
     private AIRequestService.AIResponse requestAIWithRetry(String prompt, int retryTime) {
-        List<AIEntry> entry = new ArrayList<>();
-        entry.add(new AIEntry("user", prompt));
-        AIRequestService.AIResponse response = aiRequestService.requestChatGLM4(entry);
-        if(response == null && retryTime < 3) {
-            log.info("requestAI failed, retryTime: {}", retryTime + 1);
-            return requestAIWithRetry(prompt, retryTime + 1);
+        List<DeepSeekMessage> messages = new ArrayList<>();
+        messages.add(DeepSeekMessage.user(prompt));
+        try {
+            DeepSeekResponse response = deepSeekService.chatCompletion(messages);
+            if (response == null && retryTime < 3) {
+                log.info("DeepSeek request failed, retryTime: {}", retryTime + 1);
+                return requestAIWithRetry(prompt, retryTime + 1);
+            }
+            if (response != null && response.getChoices() != null && !response.getChoices().isEmpty()) {
+                AIRequestService.Choice choice = new AIRequestService.Choice();
+                AIRequestService.ResponseMessage msg = new AIRequestService.ResponseMessage();
+                msg.setRole(response.getChoices().get(0).getMessage().getRole());
+                msg.setContent(response.getChoices().get(0).getMessage().getContent());
+                choice.setMessage(msg);
+                AIRequestService.AIResponse aiResponse = new AIRequestService.AIResponse();
+                aiResponse.setChoices(List.of(choice));
+                return aiResponse;
+            }
+            return null;
+        } catch (Exception e) {
+            log.error("DeepSeek request error: {}, retryTime: {}/{}", e.getMessage(), retryTime + 1, 3);
+            if (retryTime < 3) {
+                return requestAIWithRetry(prompt, retryTime + 1);
+            }
+            return null;
         }
-        return response;
     }
 
     /**
@@ -247,41 +331,56 @@ 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, "作业不存在");
+        final int version = engagement.getVersion();
+        log.info("[EVAL][SENTENCE] request start assignmentId={}, studentId={}, version={}, textLen={}",
+                assignmentId, studentId, version,
+                engagement.getTextContent() == null ? -1 : engagement.getTextContent().length());
+
+        // 逐句批改可独立触发:确保 evaluation 占位记录存在(不依赖整体先跑)
+        Evaluation evaluationJudge = ensureEvaluationRecordExists(assignmentId, studentId, engagement);
+        log.info("[EVAL][SENTENCE] current evaluation record assignmentId={}, studentId={}, version={}, sentence={}, overall={}, time={}",
+                assignmentId, studentId, version,
+                evaluationJudge == null ? null : evaluationJudge.getSentence(),
+                evaluationJudge == null ? null : evaluationJudge.getOverall(),
+                evaluationJudge == null ? null : evaluationJudge.getTime());
+
+        // 检查是否已完成逐句批改(sentence=1表示已完成)
+        if (evaluationJudge != null && evaluationJudge.getSentence() != null && evaluationJudge.getSentence() == 1) {
+            log.info("逐句批改已完成,直接返回现有结果 - assignmentId: {}, studentId: {}, version: {}",
+                    assignmentId, studentId, version);
+            LambdaQueryWrapper<SentenceEvaluation> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(SentenceEvaluation::getAssignmentId, assignmentId)
+                    .eq(SentenceEvaluation::getStudentId, studentId)
+                    .eq(SentenceEvaluation::getVersion, version);
+            List<SentenceEvaluation> list = sentenceEvaluationMapperServiceImpl.list(queryWrapper);
+            log.info("[EVAL][SENTENCE] hit cached result assignmentId={}, studentId={}, version={}, count={}",
+                    assignmentId, studentId, version, list == null ? -1 : list.size());
+            return list;
+        }
 
-        // 没有智能评价记录时,进行重试,等待整体智能批改将记录写入数据库
-        // 使用编程式事务在新事务中查询,避免REPEATABLE READ隔离级别导致看不到其他事务提交的数据
-        int retryTime = 0;
-        Evaluation evaluationJudge = null;
-        while (retryTime < 3) {
-            final int currentVersion = engagement.getVersion();
-            evaluationJudge = requiresNewTransactionTemplate.execute(status ->
-                evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, currentVersion)
-            );
-            log.info("rewritePerSentence find evaluation record, assignmentId: {}, studentId: {}, version: {}", assignmentId, studentId, engagement.getVersion());
-            if (evaluationJudge == null){
-                try {
-                    log.info("rewritePerSentence find evaluation record failed, assignmentId: {}, studentId: {}, retryTime: {}", assignmentId, studentId, retryTime + 1);
-                    Thread.sleep(1500);
-                    retryTime++;
-                } catch (InterruptedException e) {
-                    Thread.currentThread().interrupt();
-                    throw MyException.create(HttpStatus.BAD_REQUEST, "处理被中断: " + e.getMessage());
-                }
-            } else {
-                break;
-            }
-        }   
-        if(retryTime >= 3) {
-            log.info("rewritePerSentence failed, assignmentId: {}, studentId: {}, reason: over max retry times(3)", assignmentId, studentId);
-            throw MyException.create(HttpStatus.BAD_REQUEST, "逐句批改失败,超过重试次数,请重新发起请求");
+        // sentence=2 表示处理中,直接提示前端轮询读取即可
+        if (evaluationJudge != null && evaluationJudge.getSentence() != null && evaluationJudge.getSentence() == 2) {
+            log.info("[EVAL][SENTENCE] already processing assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+            throw MyException.create(HttpStatus.BAD_REQUEST, "逐句批改正在进行中,请稍后刷新");
         }
 
+        // 设置状态为处理中(2),避免重复处理
+        evaluationMapperServiceImpl.update(new UpdateWrapper<Evaluation>()
+                .set("sentence", 2)
+                .eq("student_id", studentId)
+                .eq("assignment_id", assignmentId)
+                .eq("version", version)
+        );
+        log.info("[EVAL][SENTENCE] mark processing sentence=2 assignmentId={}, studentId={}, version={}",
+                assignmentId, studentId, version);
+
         try {
             String content = engagement.getTextContent();
             String[] sentences = getSentenceList(content);
 
             log.info("rewritePerSentence start, assignmentId: {}, studentId: {}, sentenceCount: {}, version: {}",
-                    assignmentId, studentId, sentences.length, engagement.getVersion());
+                    assignmentId, studentId, sentences.length, version);
 
             List<SentenceEvaluation> sentenceEvaluationsList = new ArrayList<>();
             @SuppressWarnings("unchecked")
@@ -296,7 +395,7 @@ public class AIEvaluationService {
                                 assignmentId, studentId,
                                 content, assignment.getDescription(),
                                 sentence, sentenceNo,
-                                engagement.getVersion()
+                                version
                         ),
                         aiPerSentenceExecutor
                 );
@@ -325,6 +424,8 @@ public class AIEvaluationService {
             // 3) 批量保存逐句结果
             if (!sentenceEvaluationsList.isEmpty()) {
                 sentenceEvaluationMapperServiceImpl.saveBatch(sentenceEvaluationsList);
+                log.info("[EVAL][SENTENCE] sentence_evaluation batch insert ok assignmentId={}, studentId={}, version={}, count={}",
+                        assignmentId, studentId, version, sentenceEvaluationsList.size());
             }
 
             // 4) 完成态:2 -> 1(只允许从处理中改成完成,避免误覆盖)
@@ -332,9 +433,11 @@ public class AIEvaluationService {
                     .set("sentence", 1)
                     .eq("student_id", studentId)
                     .eq("assignment_id", assignmentId)
-                    .eq("version", engagement.getVersion())
+                    .eq("version", version)
                     .eq("sentence", 2)
             );
+            log.info("[EVAL][SENTENCE] mark done sentence=1 assignmentId={}, studentId={}, version={}, cost={}",
+                    assignmentId, studentId, version, sw.formatTime());
 
             log.info("rewritePerSentence success, assignmentId: {}, studentId: {}, cost: {}",
                     assignmentId, studentId, sw.formatTime());
@@ -347,7 +450,7 @@ public class AIEvaluationService {
                 .set("sentence", 0)
                 .eq("student_id", studentId)
                 .eq("assignment_id", assignmentId)
-                .eq("version", engagement.getVersion())
+                .eq("version", version)
                 .eq("sentence", 2)
         );
         throw ex;
@@ -518,14 +621,19 @@ public class AIEvaluationService {
      * @return
      */
     public OverallEvaluation getRewrite(Long assignmentId, Long studentId, int version){
+        log.info("[EVAL][SELECT][OVERALL] select assignmentId={}, studentId={}, version={}", assignmentId, studentId, version);
         QueryWrapper<OverallEvaluation> queryWrapper =  new QueryWrapper<>();
         queryWrapper.eq("assignment_id", assignmentId);
         queryWrapper.eq("student_id", studentId);
         queryWrapper.eq("version", version);
         OverallEvaluation overallEvaluation = overallEvaluationMapper.selectOne(queryWrapper);
         if(overallEvaluation == null){
+            log.info("[EVAL][SELECT][OVERALL] not found assignmentId={}, studentId={}, version={}", assignmentId, studentId, version);
             throw MyException.create(HttpStatus.BAD_REQUEST, "AI批改不存在");
         }
+        log.info("[EVAL][SELECT][OVERALL] found assignmentId={}, studentId={}, version={}, evalLen={}",
+                assignmentId, studentId, version,
+                overallEvaluation.getEvaluation() == null ? -1 : overallEvaluation.getEvaluation().length());
         return overallEvaluation;
     }
 
@@ -537,15 +645,19 @@ public class AIEvaluationService {
      * @return
      */
     public List<SentenceEvaluation> getRewritePerSentence(Long assignmentId, Long studentId, int version){
+        log.info("[EVAL][SELECT][SENTENCE] select assignmentId={}, studentId={}, version={}", assignmentId, studentId, version);
         List<SentenceEvaluation> sentenceEvaluations = sentenceEvaluationMapperServiceImpl.list(new QueryWrapper<SentenceEvaluation>()
                .eq("assignment_id", assignmentId)
                .eq("student_id", studentId)
                .eq("version", version));
 
         if(sentenceEvaluations.isEmpty()){
+            log.info("[EVAL][SELECT][SENTENCE] not found assignmentId={}, studentId={}, version={}", assignmentId, studentId, version);
             throw MyException.create(HttpStatus.BAD_REQUEST, "AI批改不存在");
         }
 
+        log.info("[EVAL][SELECT][SENTENCE] found assignmentId={}, studentId={}, version={}, count={}",
+                assignmentId, studentId, version, sentenceEvaluations.size());
         return sentenceEvaluations.stream().sorted(Comparator.comparing(SentenceEvaluation::getNo)).toList();
     }
 
@@ -590,6 +702,83 @@ public class AIEvaluationService {
         return result;
     }
 
+    private OverallEvaluation getOverallEvaluationIfExists(Long assignmentId, Long studentId, int version) {
+        try {
+            QueryWrapper<OverallEvaluation> queryWrapper =  new QueryWrapper<>();
+            queryWrapper.eq("assignment_id", assignmentId);
+            queryWrapper.eq("student_id", studentId);
+            queryWrapper.eq("version", version);
+            return overallEvaluationMapper.selectOne(queryWrapper);
+        } catch (Exception e) {
+            log.warn("getOverallEvaluationIfExists failed: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    private Evaluation ensureEvaluationRecordExists(Long assignmentId, Long studentId, Engagement engagement) {
+        final int version = engagement.getVersion();
+        Evaluation existing = requiresNewTransactionTemplate.execute(status ->
+                evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, version)
+        );
+        if (existing != null) {
+            log.info("[EVAL][ENSURE] evaluation record exists assignmentId={}, studentId={}, version={}, overall={}, sentence={}",
+                    assignmentId, studentId, version, existing.getOverall(), existing.getSentence());
+            return existing;
+        }
+        try {
+            initEvaluationRecord(assignmentId, studentId, engagement);
+            log.info("[EVAL][ENSURE] init evaluation record ok assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+        } catch (DuplicateKeyException e) {
+            // 并发插入:忽略并重新查
+            log.info("[EVAL][ENSURE] init evaluation record duplicate assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+        }
+        Evaluation created = requiresNewTransactionTemplate.execute(status ->
+                evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, version)
+        );
+        if (created == null) {
+            log.warn("[EVAL][ENSURE] init evaluation record failed assignmentId={}, studentId={}, version={}",
+                    assignmentId, studentId, version);
+            throw MyException.create(HttpStatus.BAD_REQUEST, "初始化逐句批改记录失败,请重试");
+        }
+        log.info("[EVAL][ENSURE] evaluation record ready assignmentId={}, studentId={}, version={}, overall={}, sentence={}",
+                assignmentId, studentId, version, created.getOverall(), created.getSentence());
+        return created;
+    }
+
+    /**
+     * 获取当前最新提交版本号(student_assignment.version)以及该版本的整体/逐句批改状态。
+     */
+    public EvaluationCurrentVO getCurrentEvaluationStatus(Long assignmentId, Long studentId) {
+        Engagement engagement = studentAssignmentMapper.findEngagementByStudentIdAndAssignmentId(studentId, assignmentId);
+        if (engagement == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业参与不存在");
+        int version = engagement.getVersion();
+
+        Evaluation evaluation = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, version);
+        Integer overall = evaluation == null ? 0 : Optional.ofNullable(evaluation.getOverall()).orElse(0);
+        Integer sentence = evaluation == null ? 0 : Optional.ofNullable(evaluation.getSentence()).orElse(0);
+
+        OverallEvaluation overallRow = getOverallEvaluationIfExists(assignmentId, studentId, version);
+        int sentenceCount = Math.toIntExact(sentenceEvaluationMapperServiceImpl.count(new LambdaQueryWrapper<SentenceEvaluation>()
+                .eq(SentenceEvaluation::getAssignmentId, assignmentId)
+                .eq(SentenceEvaluation::getStudentId, studentId)
+                .eq(SentenceEvaluation::getVersion, version)));
+        log.info("[EVAL][CURRENT] assignmentId={}, studentId={}, version={}, evalRow={}, overallStatus={}, sentenceStatus={}, overallRow={}, sentenceCount={}",
+                assignmentId, studentId, version,
+                evaluation != null,
+                overall, sentence,
+                overallRow != null,
+                sentenceCount);
+
+        return new EvaluationCurrentVO()
+                .setAssignmentId(assignmentId)
+                .setStudentId(studentId)
+                .setVersion(version)
+                .setOverallStatus(overall)
+                .setSentenceStatus(sentence);
+    }
+
     /**
      * 双智评:同时触发整体智评和逐句智评
      * 1. 创建智评记录

+ 60 - 0
src/main/java/com/njuzr/eaibackend/service/DeepSeekService.java

@@ -0,0 +1,60 @@
+package com.njuzr.eaibackend.service;
+
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekMessage;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekResponse;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * DeepSeek 服务接口
+ */
+public interface DeepSeekService {
+
+    /**
+     * 非流式对话
+     * @param message 用户消息
+     * @return 完整响应
+     */
+    DeepSeekResponse chatCompletion(String message);
+
+    /**
+     * 非流式对话(带历史记录)
+     * @param messages 消息列表
+     * @return 完整响应
+     */
+    DeepSeekResponse chatCompletion(List<DeepSeekMessage> messages);
+
+    /**
+     * 流式对话
+     * @param message 用户消息
+     * @return SseEmitter 流式响应
+     */
+    SseEmitter chatCompletionStream(String message);
+
+    /**
+     * 流式对话(带历史记录)
+     * @param messages 消息列表
+     * @return SseEmitter 流式响应
+     */
+    SseEmitter chatCompletionStream(List<DeepSeekMessage> messages);
+
+    /**
+     * 流式对话(带回调)
+     * @param message 用户消息
+     * @param onContent 内容回调
+     * @param onComplete 完成回调
+     * @param onError 错误回调
+     */
+    void chatCompletionStream(String message, Consumer<String> onContent, Runnable onComplete, Consumer<Throwable> onError);
+
+    /**
+     * 流式对话(带历史记录和回调)
+     * @param messages 消息列表
+     * @param onContent 内容回调
+     * @param onComplete 完成回调
+     * @param onError 错误回调
+     */
+    void chatCompletionStream(List<DeepSeekMessage> messages, Consumer<String> onContent, Runnable onComplete, Consumer<Throwable> onError);
+}

+ 5 - 16
src/main/java/com/njuzr/eaibackend/service/impl/AssignmentServiceImpl.java

@@ -375,7 +375,6 @@ public class AssignmentServiceImpl implements AssignmentService {
     @Override
     @Transactional
     public void engagementSubmit(Long studentId, Long assignmentId) {
-        log.info("=== engagementSubmit 开始 ===");
         log.info("studentId: {}, assignmentId: {}", studentId, assignmentId);
 
         Engagement engagement = studentAssignmentMapper.findEngagementByStudentIdAndAssignmentId(studentId, assignmentId);
@@ -383,36 +382,21 @@ public class AssignmentServiceImpl implements AssignmentService {
             log.error("参与作业情况不存在 - studentId: {}, assignmentId: {}", studentId, assignmentId);
             throw MyException.create(HttpStatus.BAD_REQUEST, "参与作业情况不存在");
         }
-        log.info("找到参与记录 - engagementId: {}, currentStatus: {}, currentSubmitted: {}",
-                engagement.getId(), engagement.getStatus(), engagement.getSubmitted());
-
         int originalVersion = engagement.getVersion();
         engagement.setVersion(originalVersion + 1);
         engagement.setStatus(AssignmentCompletionStatus.SUBMITTED);
-        log.info("设置状态为 SUBMITTED");
 
         Assignment assignment = assignmentMapper.selectById(assignmentId);
-        log.info("作业信息 - assignmentId: {}, examMode: {}",
-                assignment != null ? assignment.getAssignmentId() : "null",
-                assignment != null ? assignment.getExamMode() : "null");
-
         if (Boolean.TRUE.equals(assignment.getExamMode())) {
-            log.info("考试模式,调用 submitExam 并设置 submitted=true");
             examTimerService.submitExam(studentId, assignmentId);
             engagement.setSubmitted(true);
-        } else {
-            log.info("非考试模式,不设置 submitted");
         }
-
         try {
             int code = studentAssignmentMapper.updateById(engagement);
-            log.info("数据库更新结果 - 影响行数: {}", code);
             if (code == 0) {
-                log.error("更新失败 - 影响行数为0");
                 throw MyException.create(HttpStatus.INTERNAL_SERVER_ERROR, "更新文件版本失败");
             }
             recordHistorySnapshot(engagement);
-            log.info("=== engagementSubmit 完成 ===");
         } catch (Exception e) {
             log.error("更新异常: {}", e.getMessage(), e);
             throw MyException.create(HttpStatus.INTERNAL_SERVER_ERROR, "更新文件版本失败");
@@ -464,6 +448,8 @@ public class AssignmentServiceImpl implements AssignmentService {
         Engagement engagement = studentAssignmentMapper.findEngagementByStudentIdAndAssignmentId(studentId, assignmentId);
         if (engagement == null)
             throw MyException.create(HttpStatus.BAD_REQUEST, "参与作业情况不存在");
+        log.info("[ENGAGE][STAGE] start assignmentId={}, studentId={}, version(before)={}, status={}",
+                assignmentId, studentId, engagement.getVersion(), engagement.getStatus());
         String quillContent = jsonObject.getString("quillContent");
         String textContent = jsonObject.getString("textContent");
         String htmlContent = jsonObject.getString("htmlContent");
@@ -474,6 +460,9 @@ public class AssignmentServiceImpl implements AssignmentService {
             int code = studentAssignmentMapper.updateById(engagement);
             if (code == 0)
                 throw MyException.create(HttpStatus.INTERNAL_SERVER_ERROR, "更新文件版本失败");
+            log.info("[ENGAGE][STAGE] success assignmentId={}, studentId={}, version(after)={}, textLen={}",
+                    assignmentId, studentId, engagement.getVersion(),
+                    textContent == null ? -1 : textContent.length());
             // 异步启动文本分析,不影响主流程返回速度
             try {
                 textAnalysisService.analyzeAndSaveTextContentAsync(studentId, assignmentId);

+ 262 - 0
src/main/java/com/njuzr/eaibackend/service/impl/DeepSeekServiceImpl.java

@@ -0,0 +1,262 @@
+package com.njuzr.eaibackend.service.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.njuzr.eaibackend.config.DeepSeekConfig;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekMessage;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekRequest;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekResponse;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekStreamResponse;
+import com.njuzr.eaibackend.service.DeepSeekService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * DeepSeek 服务实现类
+ */
+@Slf4j
+@Service
+public class DeepSeekServiceImpl implements DeepSeekService {
+
+    @Autowired
+    private DeepSeekConfig deepSeekConfig;
+
+    private final ObjectMapper objectMapper = new ObjectMapper()
+            .configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+    @Override
+    public DeepSeekResponse chatCompletion(String message) {
+        List<DeepSeekMessage> messages = Collections.singletonList(DeepSeekMessage.user(message));
+        return chatCompletion(messages);
+    }
+
+    @Override
+    public DeepSeekResponse chatCompletion(List<DeepSeekMessage> messages) {
+        try {
+            DeepSeekRequest request = DeepSeekRequest.builder()
+                    .model(deepSeekConfig.getModel())
+                    .messages(messages)
+                    .stream(false)
+                    .build();
+
+            RestTemplate restTemplate = createRestTemplate();
+            org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            headers.setBearerAuth(deepSeekConfig.getApiKey());
+
+            org.springframework.http.HttpEntity<DeepSeekRequest> entity = 
+                    new org.springframework.http.HttpEntity<>(request, headers);
+
+            String url = deepSeekConfig.getBaseUrl() + "/chat/completions";
+            log.info("DeepSeek 非流式请求: {}", url);
+
+            DeepSeekResponse response = restTemplate.postForObject(
+                    url, entity, DeepSeekResponse.class);
+
+            log.info("DeepSeek 响应成功");
+            return response;
+        } catch (Exception e) {
+            log.error("DeepSeek 请求失败: {}", e.getMessage(), e);
+            throw new RuntimeException("DeepSeek 请求失败: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public SseEmitter chatCompletionStream(String message) {
+        List<DeepSeekMessage> messages = Collections.singletonList(DeepSeekMessage.user(message));
+        return chatCompletionStream(messages);
+    }
+
+    @Override
+    public SseEmitter chatCompletionStream(List<DeepSeekMessage> messages) {
+        SseEmitter emitter = new SseEmitter(0L);
+
+        new Thread(() -> {
+            HttpURLConnection connection = null;
+            try {
+                DeepSeekRequest request = DeepSeekRequest.builder()
+                        .model(deepSeekConfig.getModel())
+                        .messages(messages)
+                        .stream(true)
+                        .build();
+
+                String url = deepSeekConfig.getBaseUrl() + "/chat/completions";
+                log.info("DeepSeek 流式请求: {}", url);
+
+                connection = (HttpURLConnection) new URL(url).openConnection();
+                connection.setRequestMethod("POST");
+                connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+                connection.setRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + deepSeekConfig.getApiKey());
+                connection.setDoOutput(true);
+                connection.setDoInput(true);
+                connection.setConnectTimeout(deepSeekConfig.getTimeout());
+                connection.setReadTimeout(deepSeekConfig.getTimeout());
+
+                String requestBody = objectMapper.writeValueAsString(request);
+                connection.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
+
+                int responseCode = connection.getResponseCode();
+                if (responseCode != 200) {
+                    throw new RuntimeException("HTTP error code: " + responseCode);
+                }
+
+                try (BufferedReader reader = new BufferedReader(
+                        new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        if (line.startsWith("data: ")) {
+                            String data = line.substring(6);
+                            if ("[DONE]".equals(data)) {
+                                emitter.complete();
+                                break;
+                            }
+                            try {
+                                DeepSeekStreamResponse streamResponse = 
+                                        objectMapper.readValue(data, DeepSeekStreamResponse.class);
+                                String content = streamResponse.getContent();
+                                if (content != null) {
+                                    emitter.send(content);
+                                }
+                                if (streamResponse.isDone()) {
+                                    emitter.complete();
+                                    break;
+                                }
+                            } catch (Exception e) {
+                                log.warn("解析流式响应失败: {}", data);
+                            }
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                log.error("DeepSeek 流式请求失败: {}", e.getMessage(), e);
+                emitter.completeWithError(e);
+            } finally {
+                if (connection != null) {
+                    connection.disconnect();
+                }
+            }
+        }).start();
+
+        return emitter;
+    }
+
+    @Override
+    public void chatCompletionStream(String message, Consumer<String> onContent, Runnable onComplete, Consumer<Throwable> onError) {
+        List<DeepSeekMessage> messages = Collections.singletonList(DeepSeekMessage.user(message));
+        chatCompletionStream(messages, onContent, onComplete, onError);
+    }
+
+    @Override
+    public void chatCompletionStream(List<DeepSeekMessage> messages, Consumer<String> onContent, Runnable onComplete, Consumer<Throwable> onError) {
+        log.info("【DeepSeekService】chatCompletionStream被调用,消息数={}", messages.size());
+        
+        new Thread(() -> {
+            HttpURLConnection connection = null;
+            try {
+                DeepSeekRequest request = DeepSeekRequest.builder()
+                        .model(deepSeekConfig.getModel())
+                        .messages(messages)
+                        .stream(true)
+                        .build();
+
+                String url = deepSeekConfig.getBaseUrl() + "/chat/completions";
+                log.info("【DeepSeekService】开始HTTP请求: {}", url);
+                log.info("【DeepSeekService】使用模型: {}", deepSeekConfig.getModel());
+
+                connection = (HttpURLConnection) new URL(url).openConnection();
+                connection.setRequestMethod("POST");
+                connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+                connection.setRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + deepSeekConfig.getApiKey());
+                connection.setDoOutput(true);
+                connection.setDoInput(true);
+                connection.setConnectTimeout(deepSeekConfig.getTimeout());
+                connection.setReadTimeout(deepSeekConfig.getTimeout());
+
+                String requestBody = objectMapper.writeValueAsString(request);
+                log.info("【DeepSeekService】请求体大小: {} bytes", requestBody.getBytes(StandardCharsets.UTF_8).length);
+                
+                connection.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
+                connection.getOutputStream().flush();
+                connection.getOutputStream().close();
+
+                int responseCode = connection.getResponseCode();
+                log.info("【DeepSeekService】收到响应码: {}", responseCode);
+                
+                if (responseCode != 200) {
+                    log.error("【DeepSeekService】HTTP错误: {}", responseCode);
+                    throw new RuntimeException("HTTP error code: " + responseCode);
+                }
+
+                log.info("【DeepSeekService】开始读取流式响应...");
+                int chunkCount = 0;
+                
+                try (BufferedReader reader = new BufferedReader(
+                        new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        if (line.startsWith("data: ")) {
+                            String data = line.substring(6);
+                            if ("[DONE]".equals(data)) {
+                                log.info("【DeepSeekService】收到[DONE],流结束");
+                                onComplete.run();
+                                break;
+                            }
+                            try {
+                                DeepSeekStreamResponse streamResponse = 
+                                        objectMapper.readValue(data, DeepSeekStreamResponse.class);
+                                String content = streamResponse.getContent();
+                                if (content != null && !content.isEmpty()) {
+                                    onContent.accept(content);
+                                    chunkCount++;
+                                    if (chunkCount % 10 == 0) {
+                                        log.info("【DeepSeekService】已发送{}个内容块", chunkCount);
+                                    }
+                                }
+                                if (streamResponse.isDone()) {
+                                    log.info("【DeepSeekService】响应标记完成");
+                                    onComplete.run();
+                                    break;
+                                }
+                            } catch (Exception e) {
+                                log.warn("【DeepSeekService】解析流式响应失败: {}", data);
+                            }
+                        }
+                    }
+                }
+                log.info("【DeepSeekService】流式响应处理完成,共{}个内容块", chunkCount);
+                
+            } catch (Exception e) {
+                log.error("【DeepSeekService】流式请求失败: {}", e.getMessage(), e);
+                onError.accept(e);
+            } finally {
+                if (connection != null) {
+                    connection.disconnect();
+                    log.info("【DeepSeekService】连接已关闭");
+                }
+            }
+        }).start();
+        
+        log.info("【DeepSeekService】新线程已启动处理流式请求");
+    }
+
+    private RestTemplate createRestTemplate() {
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(deepSeekConfig.getTimeout());
+        factory.setReadTimeout(deepSeekConfig.getTimeout());
+        return new RestTemplate(factory);
+    }
+}

+ 21 - 0
src/main/java/com/njuzr/eaibackend/vo/EvaluationCurrentVO.java

@@ -0,0 +1,21 @@
+package com.njuzr.eaibackend.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * 当前版本评价状态:
+ * - version: student_assignment.version(提交/补交时自增后的最新版本)
+ * - overallStatus: evaluation.overall(0未完成 / 1完成)
+ * - sentenceStatus: evaluation.sentence(0未开始 / 2处理中 / 1完成)
+ */
+@Data
+@Accessors(chain = true)
+public class EvaluationCurrentVO {
+    private Long assignmentId;
+    private Long studentId;
+    private Integer version;
+    private Integer overallStatus;
+    private Integer sentenceStatus;
+}
+

+ 184 - 0
src/test/java/com/njuzr/eaibackend/service/DeepSeekServiceTest.java

@@ -0,0 +1,184 @@
+package com.njuzr.eaibackend.service;
+
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekMessage;
+import com.njuzr.eaibackend.dto.deepseek.DeepSeekResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@Slf4j
+@ActiveProfiles("dev")
+@SpringBootTest
+public class DeepSeekServiceTest {
+
+    @Autowired
+    private DeepSeekService deepSeekService;
+
+    @Test
+    @DisplayName("测试1: 非流式单轮对话")
+    void testChatCompletion_SingleMessage() {
+        String message = "你好,请用一句话介绍自己";
+        log.info("测试非流式对话 - 输入: {}", message);
+
+        DeepSeekResponse response = deepSeekService.chatCompletion(message);
+
+        assertNotNull(response, "响应不应为null");
+        String content = response.getContent();
+        assertNotNull(content, "回复内容不应为null");
+        assertFalse(content.isEmpty(), "回复内容不应为空");
+
+        log.info("DeepSeek 回复: {}", content);
+        log.info("Token 使用情况 - Prompt: {}, Completion: {}, Total: {}",
+                response.getUsage() != null ? response.getUsage().getPromptTokens() : "N/A",
+                response.getUsage() != null ? response.getUsage().getCompletionTokens() : "N/A",
+                response.getUsage() != null ? response.getUsage().getTotalTokens() : "N/A");
+    }
+
+    @Test
+    @DisplayName("测试2: 非流式多轮对话(带历史记录)")
+    void testChatCompletion_WithHistory() {
+        var messages = Arrays.asList(
+                DeepSeekMessage.system("你是一个有用的AI助手,回答要简洁明了。"),
+                DeepSeekMessage.user("你好"),
+                DeepSeekMessage.assistant("你好!有什么我可以帮助你的吗?"),
+                DeepSeekMessage.user("今天天气怎么样?")
+        );
+        log.info("测试多轮对话");
+
+        DeepSeekResponse response = deepSeekService.chatCompletion(messages);
+
+        assertNotNull(response, "响应不应为null");
+        String content = response.getContent();
+        assertNotNull(content, "回复内容不应为null");
+        assertFalse(content.isEmpty(), "回复内容不应为空");
+
+        log.info("DeepSeek 回复: {}", content);
+    }
+
+    @Test
+    @DisplayName("测试3: 流式单轮对话")
+    void testChatCompletionStream_SingleMessage() throws Exception {
+        String message = "请用3句话介绍深度学习";
+        log.info("测试流式对话 - 输入: {}", message);
+
+        StringBuilder fullContent = new StringBuilder();
+        java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
+
+        // 使用带回调的方法
+        deepSeekService.chatCompletionStream(message,
+            content -> {
+                // 每收到一个片段就输出
+                fullContent.append(content);
+                System.out.print(content); // 实时输出到控制台
+            },
+            () -> {
+                // 完成时输出完整内容
+                System.out.println(); // 换行
+                log.info("流式响应完成,完整内容: {}", fullContent.toString());
+                latch.countDown();
+            },
+            error -> {
+                log.error("流式响应错误: {}", error.getMessage());
+                latch.countDown();
+            }
+        );
+
+        log.info("开始接收流式数据...");
+
+        // 等待流式响应完成(最多15秒)
+        boolean completed = latch.await(15, TimeUnit.SECONDS);
+        if (!completed) {
+            System.out.println(); // 换行
+            log.info("流式响应超时,已接收内容: {}", fullContent.toString());
+        }
+
+        log.info("流式测试完成");
+    }
+
+    @Test
+    @DisplayName("测试4: 流式多轮对话(带历史记录)")
+    void testChatCompletionStream_WithHistory() throws Exception {
+        var messages = Arrays.asList(
+                DeepSeekMessage.system("你是一个专业的程序员。"),
+                DeepSeekMessage.user("什么是Java?"),
+                DeepSeekMessage.assistant("Java是一种广泛使用的编程语言。"),
+                DeepSeekMessage.user("它有什么特点?")
+        );
+        log.info("测试流式多轮对话");
+
+        StringBuilder fullContent = new StringBuilder();
+        java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
+
+        // 使用带回调的方法
+        deepSeekService.chatCompletionStream(messages,
+            content -> {
+                // 每收到一个片段就输出
+                fullContent.append(content);
+                System.out.print(content); // 实时输出到控制台
+            },
+            () -> {
+                // 完成时输出完整内容
+                System.out.println(); // 换行
+                log.info("流式多轮对话完成,完整内容: {}", fullContent.toString());
+                latch.countDown();
+            },
+            error -> {
+                log.error("流式响应错误: {}", error.getMessage());
+                latch.countDown();
+            }
+        );
+
+        log.info("开始接收流式多轮对话数据...");
+
+        // 等待流式响应完成
+        boolean completed = latch.await(20, TimeUnit.SECONDS);
+        if (!completed) {
+            System.out.println(); // 换行
+            log.info("流式响应超时,已接收内容: {}", fullContent.toString());
+        }
+
+        log.info("流式多轮对话测试完成");
+    }
+
+    @Test
+    @DisplayName("测试5: 长文本回复测试")
+    void testChatCompletion_LongResponse() {
+        String message = "请详细介绍一下Spring Boot框架的核心特性,至少列举5个";
+        log.info("测试长文本回复");
+
+        DeepSeekResponse response = deepSeekService.chatCompletion(message);
+
+        assertNotNull(response, "响应不应为null");
+        String content = response.getContent();
+        assertNotNull(content, "回复内容不应为null");
+        assertTrue(content.length() > 100, "长文本回复应该较长");
+
+        log.info("DeepSeek 回复长度: {} 字符", content.length());
+        log.info("DeepSeek 回复: {}", content.substring(0, Math.min(200, content.length())) + "...");
+    }
+
+    @Test
+    @DisplayName("测试6: 中文对话测试")
+    void testChatCompletion_Chinese() {
+        String message = "请写一首关于春天的五言绝句";
+        log.info("测试中文对话 - 输入: {}", message);
+
+        DeepSeekResponse response = deepSeekService.chatCompletion(message);
+
+        assertNotNull(response, "响应不应为null");
+        String content = response.getContent();
+        assertNotNull(content, "回复内容不应为null");
+        assertFalse(content.isEmpty(), "回复内容不应为空");
+
+        log.info("DeepSeek 中文回复:\n{}", content);
+    }
+}