Browse Source

feat:将对话部分改为流式

lalala 6 months ago
parent
commit
20fb0e755b

+ 7 - 0
application.yaml

@@ -46,6 +46,13 @@ jwt:
   issuer: eai
   expiration: 86400
 
+# DeepSeek AI 配置
+deepseek:
+  api-key: ${DEEPSEEK_API_KEY:sk-994409a3b69648de8171be819274fc98}
+  base-url: https://api.deepseek.com/v1
+  model: deepseek-chat
+  timeout: 60000
+
 
 aliyun:
   oss:

+ 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() // 其他所有请求需要认证
                 )

+ 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,

+ 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,获取当前文件链接

+ 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);
+}

+ 0 - 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, "更新文件版本失败");

+ 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);
+    }
+}

+ 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);
+    }
+}