From 8b357fbb933080bd15f0ce06425978147e2985d9 Mon Sep 17 00:00:00 2001
From: huangpeng <1764183241@qq.com>
Date: Fri, 19 Sep 2025 15:19:41 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 5 +
.../interview/ai/factory/AIClientFactory.java | 2 +-
.../interview/ai/factory/AIClientManager.java | 2 +-
.../ai/factory/DeepSeekClientFactory.java | 2 +-
.../ai/factory/QwenClientFactory.java | 2 +-
.../interview/annotation/AiChatLog.java | 15 +++
.../interview/aspect/AiChatLogAspect.java | 34 +++++
.../common/constants/CommonConstant.java | 4 +
.../common/enums/DocumentParserProvider.java | 33 +++++
.../{ai => common}/enums/LLMProvider.java | 9 +-
.../interview/common/utils/AIUtils.java | 15 +++
.../interview/controller/ChatController.java | 36 ++++++
.../com/qingqiu/interview/dto/ChatDTO.java | 26 ++++
.../interview/entity/AiSessionLog.java | 10 ++
.../entity/InterviewQuestionProgress.java | 9 ++
.../interview/entity/InterviewSession.java | 8 ++
.../interview/service/ChatService.java | 29 +++++
.../service/InterviewChatService.java | 17 +++
.../interview/service/InterviewService.java | 87 +------------
.../service/impl/ChatServiceImpl.java | 96 ++++++++++++++
.../impl/InterviewChatServiceImpl.java | 50 ++++++++
.../service/impl/QuestionServiceImpl.java | 2 +-
.../parser/DocumentParserManagerImpl.java | 40 ++++++
.../parser/MarkdownParserServiceImpl.java} | 71 ++++++-----
.../parser/PdfParserServiceImpl.java} | 119 +++++++++---------
.../interview/service/llm/LlmService.java | 2 +-
.../service/llm/qwen/QwenService.java | 2 +-
.../service/parser/DocumentParser.java | 4 +
.../service/parser/DocumentParserManager.java | 14 +++
.../java/com/qingqiu/interview/vo/ChatVO.java | 22 ++++
30 files changed, 585 insertions(+), 182 deletions(-)
create mode 100644 src/main/java/com/qingqiu/interview/annotation/AiChatLog.java
create mode 100644 src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java
create mode 100644 src/main/java/com/qingqiu/interview/common/enums/DocumentParserProvider.java
rename src/main/java/com/qingqiu/interview/{ai => common}/enums/LLMProvider.java (85%)
create mode 100644 src/main/java/com/qingqiu/interview/controller/ChatController.java
create mode 100644 src/main/java/com/qingqiu/interview/dto/ChatDTO.java
create mode 100644 src/main/java/com/qingqiu/interview/service/ChatService.java
create mode 100644 src/main/java/com/qingqiu/interview/service/InterviewChatService.java
create mode 100644 src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java
create mode 100644 src/main/java/com/qingqiu/interview/service/impl/InterviewChatServiceImpl.java
create mode 100644 src/main/java/com/qingqiu/interview/service/impl/parser/DocumentParserManagerImpl.java
rename src/main/java/com/qingqiu/interview/service/{parser/MarkdownParserService.java => impl/parser/MarkdownParserServiceImpl.java} (69%)
rename src/main/java/com/qingqiu/interview/service/{parser/PdfParserService.java => impl/parser/PdfParserServiceImpl.java} (82%)
create mode 100644 src/main/java/com/qingqiu/interview/service/parser/DocumentParserManager.java
create mode 100644 src/main/java/com/qingqiu/interview/vo/ChatVO.java
diff --git a/pom.xml b/pom.xml
index 7e2ea8d..891d80f 100755
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,11 @@
org.springframework.boot
spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
org.springframework.boot
spring-boot-starter-webflux
diff --git a/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
index aea32c2..a7c737a 100755
--- a/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
+++ b/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
@@ -1,6 +1,6 @@
package com.qingqiu.interview.ai.factory;
-import com.qingqiu.interview.ai.enums.LLMProvider;
+import com.qingqiu.interview.common.enums.LLMProvider;
import com.qingqiu.interview.ai.service.AIClientService;
public interface AIClientFactory {
diff --git a/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java b/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
index 263c79f..fb79371 100755
--- a/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
+++ b/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
@@ -1,6 +1,6 @@
package com.qingqiu.interview.ai.factory;
-import com.qingqiu.interview.ai.enums.LLMProvider;
+import com.qingqiu.interview.common.enums.LLMProvider;
import com.qingqiu.interview.ai.service.AIClientService;
import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
index 73ffce2..c60d5e4 100755
--- a/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
+++ b/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
@@ -1,6 +1,6 @@
package com.qingqiu.interview.ai.factory;
-import com.qingqiu.interview.ai.enums.LLMProvider;
+import com.qingqiu.interview.common.enums.LLMProvider;
import com.qingqiu.interview.ai.service.AIClientService;
import com.qingqiu.interview.ai.service.impl.DeepSeekClientServiceImpl;
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
diff --git a/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
index 6d6a5d8..aeca885 100755
--- a/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
+++ b/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
@@ -1,6 +1,6 @@
package com.qingqiu.interview.ai.factory;
-import com.qingqiu.interview.ai.enums.LLMProvider;
+import com.qingqiu.interview.common.enums.LLMProvider;
import com.qingqiu.interview.ai.service.AIClientService;
import com.qingqiu.interview.ai.service.impl.QwenClientServiceImpl;
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
diff --git a/src/main/java/com/qingqiu/interview/annotation/AiChatLog.java b/src/main/java/com/qingqiu/interview/annotation/AiChatLog.java
new file mode 100644
index 0000000..fb32e51
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/annotation/AiChatLog.java
@@ -0,0 +1,15 @@
+package com.qingqiu.interview.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 12:58
+ */
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AiChatLog {
+}
diff --git a/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java b/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java
new file mode 100644
index 0000000..13451ed
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java
@@ -0,0 +1,34 @@
+package com.qingqiu.interview.aspect;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * ai聊天的切面
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 13:00
+ */
+@Aspect
+@Component
+public class AiChatLogAspect {
+
+ public AiChatLogAspect() {
+
+ }
+
+ @Pointcut("@annotation(com.qingqiu.interview.annotation.AiChatLog)")
+ public void logPointCut() {
+ }
+
+ @Around("logPointCut()")
+ public Object around(ProceedingJoinPoint point) throws Throwable {
+ Object result = point.proceed();
+ return result;
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java b/src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java
index 5fc459b..b2a9316 100755
--- a/src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java
+++ b/src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java
@@ -1,5 +1,7 @@
package com.qingqiu.interview.common.constants;
+import java.math.BigDecimal;
+
/**
* 公共常量
* @author huangpeng
@@ -10,4 +12,6 @@ public class CommonConstant {
public static final Integer ZERO = 0;
public static final Integer ONE = 1;
public static final Long ROOT_PARENT_ID = 0L;
+ public static final Integer MAX_TOKEN = 64000;
+ public static final BigDecimal DEFAULT_TRUNCATE_RATIO = new BigDecimal("0.1");
}
diff --git a/src/main/java/com/qingqiu/interview/common/enums/DocumentParserProvider.java b/src/main/java/com/qingqiu/interview/common/enums/DocumentParserProvider.java
new file mode 100644
index 0000000..18d6f6a
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/common/enums/DocumentParserProvider.java
@@ -0,0 +1,33 @@
+package com.qingqiu.interview.common.enums;
+
+import lombok.Getter;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 16:43
+ */
+@Getter
+public enum DocumentParserProvider {
+
+ PDF("pdf"),
+ MARKDOWN("md"),
+
+ ;
+
+ private final String code;
+
+ DocumentParserProvider(String code) {
+ this.code = code;
+ }
+
+ public static DocumentParserProvider fromCode(String code) {
+ for (DocumentParserProvider provider : values()) {
+ if (provider.getCode().equals(code)) {
+ return provider;
+ }
+ }
+ throw new IllegalArgumentException("Unknown provider: " + code);
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/ai/enums/LLMProvider.java b/src/main/java/com/qingqiu/interview/common/enums/LLMProvider.java
similarity index 85%
rename from src/main/java/com/qingqiu/interview/ai/enums/LLMProvider.java
rename to src/main/java/com/qingqiu/interview/common/enums/LLMProvider.java
index 82aaa77..17379f4 100644
--- a/src/main/java/com/qingqiu/interview/ai/enums/LLMProvider.java
+++ b/src/main/java/com/qingqiu/interview/common/enums/LLMProvider.java
@@ -1,5 +1,8 @@
-package com.qingqiu.interview.ai.enums;
+package com.qingqiu.interview.common.enums;
+import lombok.Getter;
+
+@Getter
public enum LLMProvider {
OPEN_AI("openai"),
@@ -16,10 +19,6 @@ public enum LLMProvider {
this.code = code;
}
- public String getCode() {
- return code;
- }
-
public static LLMProvider fromCode(String code) {
for (LLMProvider provider : values()) {
if (provider.getCode().equals(code)) {
diff --git a/src/main/java/com/qingqiu/interview/common/utils/AIUtils.java b/src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
index 35b8e66..d59b1f7 100755
--- a/src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
+++ b/src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
@@ -2,6 +2,10 @@ package com.qingqiu.interview.common.utils;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
+import com.alibaba.dashscope.tokenizers.Tokenizer;
+import com.alibaba.dashscope.tokenizers.TokenizerFactory;
+
+import java.util.List;
public class AIUtils {
@@ -23,4 +27,15 @@ public class AIUtils {
public static Message createSystemMessage(String prompt) {
return createMessage(Role.SYSTEM.getValue(), prompt);
}
+
+ /**
+ * 获取prompt的token数
+ * @param prompt 输入
+ * @return tokens
+ */
+ public static Integer getPromptTokens(String prompt) {
+ Tokenizer tokenizer = TokenizerFactory.qwen();
+ List integers = tokenizer.encodeOrdinary(prompt);
+ return integers.size();
+ }
}
diff --git a/src/main/java/com/qingqiu/interview/controller/ChatController.java b/src/main/java/com/qingqiu/interview/controller/ChatController.java
new file mode 100644
index 0000000..bf00c50
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/controller/ChatController.java
@@ -0,0 +1,36 @@
+package com.qingqiu.interview.controller;
+
+import com.qingqiu.interview.common.res.R;
+import com.qingqiu.interview.dto.ChatDTO;
+import com.qingqiu.interview.dto.InterviewStartRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * AI聊天控制器
+ *
+ * @author qingqiu
+ * @date 2025/9/18 12:11
+ */
+@RestController
+@RequestMapping("/chat")
+@RequiredArgsConstructor
+public class ChatController {
+
+ /**
+ * 创建聊天
+ * @return
+ */
+ @PostMapping("/send")
+ public R> createChat(@RequestBody ChatDTO dto) {
+ return R.success();
+ }
+
+ @PostMapping("/interview/create")
+ public R> createInterview(@RequestParam("resume") MultipartFile resume,
+ @Validated @ModelAttribute InterviewStartRequest request) {
+ return R.success();
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/dto/ChatDTO.java b/src/main/java/com/qingqiu/interview/dto/ChatDTO.java
new file mode 100644
index 0000000..c12e04f
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/dto/ChatDTO.java
@@ -0,0 +1,26 @@
+package com.qingqiu.interview.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 12:54
+ */
+@Data
+@Accessors(chain = true)
+public class ChatDTO {
+
+ /** 会话id */
+ private String sessionId;
+ /** 调用模型 */
+ private String aiModel;
+ /** 输入内容 */
+ private String content;
+ /** 0 普通会话 1 面试会话 */
+ private Integer dataType;
+ /** 角色类型:user/assistant/system */
+ private String role;
+}
diff --git a/src/main/java/com/qingqiu/interview/entity/AiSessionLog.java b/src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
index 9de8909..ee6b34d 100755
--- a/src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
+++ b/src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
@@ -5,6 +5,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
+import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
@@ -22,6 +23,7 @@ import java.time.LocalDateTime;
@TableName("ai_session_log")
public class AiSessionLog implements Serializable {
+ @Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@@ -32,6 +34,11 @@ public class AiSessionLog implements Serializable {
*/
private String role;
+ /**
+ * 数据类型 0 普通会话 1 面试会话
+ */
+ private Integer dataType;
+
/**
* 输入内容
*/
@@ -54,5 +61,8 @@ public class AiSessionLog implements Serializable {
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
+ @TableLogic
+ private Integer deleted;
+
}
diff --git a/src/main/java/com/qingqiu/interview/entity/InterviewQuestionProgress.java b/src/main/java/com/qingqiu/interview/entity/InterviewQuestionProgress.java
index 8fa8358..6c9a382 100755
--- a/src/main/java/com/qingqiu/interview/entity/InterviewQuestionProgress.java
+++ b/src/main/java/com/qingqiu/interview/entity/InterviewQuestionProgress.java
@@ -33,6 +33,15 @@ public class InterviewQuestionProgress {
@TableField("question_content")
private String questionContent;
+ /** 问题序号 */
+ private Integer questionIndex;
+
+ /** 答题耗时(秒) */
+ private Long timeTaken;
+
+ /** 详细评估信息 */
+ private String evaluationDetails;
+
/**
* 面试会话ID
*/
diff --git a/src/main/java/com/qingqiu/interview/entity/InterviewSession.java b/src/main/java/com/qingqiu/interview/entity/InterviewSession.java
index d7db6f0..c5a623f 100755
--- a/src/main/java/com/qingqiu/interview/entity/InterviewSession.java
+++ b/src/main/java/com/qingqiu/interview/entity/InterviewSession.java
@@ -33,6 +33,14 @@ public class InterviewSession implements Serializable {
@TableField("extracted_skills")
private String extractedSkills;
+ @TableField("interview_type")
+ private String interviewType;
+
+ @TableField("estimated_duration")
+ private Integer estimatedDuration;
+
+ @TableField("current_question_id")
+ private Long currentQuestionId;
@TableField("ai_model")
private String aiModel;
diff --git a/src/main/java/com/qingqiu/interview/service/ChatService.java b/src/main/java/com/qingqiu/interview/service/ChatService.java
new file mode 100644
index 0000000..90d7241
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/service/ChatService.java
@@ -0,0 +1,29 @@
+package com.qingqiu.interview.service;
+
+import com.qingqiu.interview.dto.ChatDTO;
+import com.qingqiu.interview.dto.InterviewStartRequest;
+import com.qingqiu.interview.vo.ChatVO;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 12:45
+ */
+public interface ChatService {
+
+ /**
+ * 创建普通会话
+ * @return sessionId
+ */
+ ChatVO createChat(ChatDTO dto);
+
+ /**
+ * 创建面试会话
+ * @param resume 简历
+ * @param request 面试信息
+ * @return sessionId
+ */
+ String createInterviewChat(MultipartFile resume, InterviewStartRequest request);
+}
diff --git a/src/main/java/com/qingqiu/interview/service/InterviewChatService.java b/src/main/java/com/qingqiu/interview/service/InterviewChatService.java
new file mode 100644
index 0000000..b57f420
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/service/InterviewChatService.java
@@ -0,0 +1,17 @@
+package com.qingqiu.interview.service;
+
+import com.qingqiu.interview.dto.InterviewStartRequest;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 16:37
+ */
+public interface InterviewChatService {
+
+ void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException;
+}
diff --git a/src/main/java/com/qingqiu/interview/service/InterviewService.java b/src/main/java/com/qingqiu/interview/service/InterviewService.java
index f81beec..2dcfb1b 100755
--- a/src/main/java/com/qingqiu/interview/service/InterviewService.java
+++ b/src/main/java/com/qingqiu/interview/service/InterviewService.java
@@ -63,6 +63,12 @@ public class InterviewService {
// 1. 解析简历
String resumeContent = parseResume(resume);
+ // 判断是否AI出题
+ if (request.getModel().equals("local")) {
+ if (CollectionUtil.isEmpty(request.getSelectedNodes())) {
+
+ }
+ }
// 2. 创建会话 并发送AI请求 让其从题库中智能抽题
@@ -212,40 +218,8 @@ public class InterviewService {
}
- /**
- * 导入题库(使用AI自动分类)
- */
- /**
- * 获取会话历史
- */
- public SessionHistoryResponse getSessionHistory(String sessionId) {
- InterviewSession session = sessionMapper.selectBySessionId(sessionId);
- if (session == null) {
- throw new IllegalArgumentException("会话不存在: " + sessionId);
- }
-
- List messages = messageMapper.selectBySessionIdOrderByOrder(sessionId);
- List messageDtos = messages.stream()
- .map(msg -> new SessionHistoryResponse.MessageDto()
- .setMessageType(msg.getMessageType())
- .setSender(msg.getSender())
- .setContent(msg.getContent())
- .setMessageOrder(msg.getMessageOrder())
- .setCreatedTime(msg.getCreatedTime()))
- .collect(Collectors.toList());
-
- return new SessionHistoryResponse()
- .setSessionId(sessionId)
- .setCandidateName(session.getCandidateName())
- .setAiModel(session.getAiModel())
- .setStatus(session.getStatus())
- .setTotalQuestions(session.getTotalQuestions())
- .setCurrentQuestionIndex(session.getCurrentQuestionIndex())
- .setCreatedTime(session.getCreatedTime())
- .setMessages(messageDtos);
- }
private String parseResume(MultipartFile resume) throws IOException {
String fileExtension = getFileExtension(resume.getOriginalFilename());
@@ -539,55 +513,6 @@ public class InterviewService {
""", session.getResumeContent(), historyBuilder.toString());
}
- private InterviewResponse generateNextQuestion(InterviewSession session) {
- try {
- // 1. 解析出AI选择的题目ID列表
- List selectedQuestionIds = objectMapper.readValue(session.getSelectedQuestionIds(), new com.fasterxml.jackson.core.type.TypeReference>() {
- });
-
- // 2. 获取下一个问题的索引
- int nextQuestionIndex = session.getCurrentQuestionIndex(); // 数据库中存的是已回答问题的数量
- if (nextQuestionIndex >= selectedQuestionIds.size()) {
- return finishInterview(session); // 如果没有更多问题,则结束面试
- }
-
- // 3. 获取下一个问题的ID并从数据库查询
- Long nextQuestionId = selectedQuestionIds.get(nextQuestionIndex);
- Question nextQuestion = questionMapper.selectById(nextQuestionId);
- if (nextQuestion == null) {
- log.error("无法找到ID为 {} 的问题,跳过此问题。", nextQuestionId);
- // 更新会话状态并尝试下一个问题
- session.setCurrentQuestionIndex(nextQuestionIndex + 1);
- sessionMapper.updateById(session);
- return generateNextQuestion(session); // 递归调用以获取再下一个问题
- }
-
- // 4. 更新会话状态(当前问题索引+1)
- session.setCurrentQuestionIndex(nextQuestionIndex + 1);
- sessionMapper.updateById(session);
-
- // 5. 生成并保存AI的提问消息
- String questionContent = String.format("好的,下一个问题是:%s", nextQuestion.getContent());
- int messageOrder = messageMapper.selectMaxOrderBySessionId(session.getSessionId()) + 1;
- saveMessage(session.getSessionId(), InterviewMessage.MessageType.QUESTION.name(),
- InterviewMessage.Sender.AI.name(), questionContent, nextQuestion.getId(), messageOrder);
-
- // 6. 返回响应
- return new InterviewResponse()
- .setSessionId(session.getSessionId())
- .setMessage(questionContent)
- .setMessageType(InterviewMessage.MessageType.QUESTION.name())
- .setSender(InterviewMessage.Sender.AI.name())
- .setCurrentQuestionIndex(session.getCurrentQuestionIndex())
- .setTotalQuestions(session.getTotalQuestions())
- .setStatus(InterviewSession.Status.ACTIVE.name());
-
- } catch (JsonProcessingException e) {
- log.error("解析会话中的题目ID列表失败", e);
- return finishInterview(session); // 解析失败则直接结束面试
- }
- }
-
/**
* 获取所有面试会话列表
diff --git a/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java
new file mode 100644
index 0000000..5ee2841
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java
@@ -0,0 +1,96 @@
+package com.qingqiu.interview.service.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.alibaba.dashscope.common.Message;
+import com.alibaba.dashscope.common.Role;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.qingqiu.interview.common.enums.LLMProvider;
+import com.qingqiu.interview.ai.factory.AIClientManager;
+import com.qingqiu.interview.common.utils.AIUtils;
+import com.qingqiu.interview.dto.ChatDTO;
+import com.qingqiu.interview.dto.InterviewStartRequest;
+import com.qingqiu.interview.entity.AiSessionLog;
+import com.qingqiu.interview.service.ChatService;
+import com.qingqiu.interview.service.IAiSessionLogService;
+import com.qingqiu.interview.vo.ChatVO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.qingqiu.interview.common.constants.CommonConstant.DEFAULT_TRUNCATE_RATIO;
+import static com.qingqiu.interview.common.constants.CommonConstant.MAX_TOKEN;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 12:56
+ */
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ChatServiceImpl implements ChatService {
+
+ private final AIClientManager aiClientManager;
+
+ private IAiSessionLogService aiSessionLogService;
+
+ @Override
+ public ChatVO createChat(ChatDTO dto) {
+ LLMProvider llmProvider = LLMProvider.fromCode(dto.getAiModel());
+ List messages = new ArrayList<>();
+ AtomicInteger tokens = new AtomicInteger();
+ // 如果会话id不为空 则从数据库中获取会话记录
+ if (dto.getSessionId() != null) {
+ List list = aiSessionLogService.list(
+ new LambdaQueryWrapper()
+ .eq(AiSessionLog::getToken, dto.getSessionId())
+ .eq(AiSessionLog::getDataType, dto.getDataType())
+ .orderByAsc(AiSessionLog::getCreatedTime)
+ );
+ if (CollectionUtil.isNotEmpty(list)) {
+ messages = list.stream().map(data -> {
+ tokens.getAndAdd(AIUtils.getPromptTokens(data.getContent()));
+ return AIUtils.createMessage(data.getRole(), data.getContent());
+ }).toList();
+ }
+
+ }
+ if (CollectionUtil.isEmpty( messages)) {
+ messages = new ArrayList<>();
+ }
+ messages.add(AIUtils.createMessage(dto.getRole(), dto.getContent()));
+ List finalMessage = new ArrayList<>();
+ // 剪切 10%的消息
+ if (tokens.get() > MAX_TOKEN) {
+ BigDecimal size = new BigDecimal(String.valueOf(messages.size()));
+ size = size.multiply(DEFAULT_TRUNCATE_RATIO).setScale(0, RoundingMode.HALF_UP);
+ for (int i = size.intValue(); i < messages.size(); i++) {
+ finalMessage.add(messages.get(i));
+ }
+ } else {
+ finalMessage = messages;
+ }
+ String res = aiClientManager.getClient(llmProvider).chatCompletion(finalMessage);
+
+
+ return ChatVO.builder()
+ .role(Role.ASSISTANT.getValue())
+ .sessionId(dto.getSessionId())
+ .content(res)
+ .build();
+ }
+
+ @Override
+ public String createInterviewChat(MultipartFile resume, InterviewStartRequest request) {
+ return "";
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewChatServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewChatServiceImpl.java
new file mode 100644
index 0000000..7363cd1
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewChatServiceImpl.java
@@ -0,0 +1,50 @@
+package com.qingqiu.interview.service.impl;
+
+import cn.hutool.core.io.file.FileNameUtil;
+import com.qingqiu.interview.common.constants.AIStrategyConstant;
+import com.qingqiu.interview.common.enums.DocumentParserProvider;
+import com.qingqiu.interview.dto.InterviewStartRequest;
+import com.qingqiu.interview.service.InterviewChatService;
+import com.qingqiu.interview.service.parser.DocumentParser;
+import com.qingqiu.interview.service.parser.DocumentParserManager;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 16:38
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
+public class InterviewChatServiceImpl implements InterviewChatService {
+
+ private final DocumentParserManager documentParserManager;
+ @Override
+ public void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException {
+ log.info("开始新面试会话,当前模式: {}, 候选人: {}, 默认AI模型: {}", request.getModel(), request.getCandidateName(), AIStrategyConstant.QWEN);
+ // 1. 解析简历
+ String resumeContent = parseResume(resume);
+ // 判断是否使用本地题库
+ if (request.getModel().equals("local")) {
+
+ }
+ }
+
+ private String parseResume(MultipartFile resume) throws IOException {
+ // 获取文件扩展名
+ String extName = FileNameUtil.extName(resume.getOriginalFilename());
+ // 1. 获取简历解析器
+ DocumentParser parser = documentParserManager.getParser(DocumentParserProvider.fromCode(extName));
+ // 2. 解析简历
+ return parser.parse(resume.getInputStream());
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java
index 69241be..e645539 100644
--- a/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java
+++ b/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java
@@ -6,7 +6,7 @@ import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.qingqiu.interview.ai.enums.LLMProvider;
+import com.qingqiu.interview.common.enums.LLMProvider;
import com.qingqiu.interview.common.constants.CommonConstant;
import com.qingqiu.interview.common.utils.TreeUtil;
import com.qingqiu.interview.dto.QuestionOptionsDTO;
diff --git a/src/main/java/com/qingqiu/interview/service/impl/parser/DocumentParserManagerImpl.java b/src/main/java/com/qingqiu/interview/service/impl/parser/DocumentParserManagerImpl.java
new file mode 100644
index 0000000..5527b7a
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/service/impl/parser/DocumentParserManagerImpl.java
@@ -0,0 +1,40 @@
+package com.qingqiu.interview.service.impl.parser;
+
+import com.qingqiu.interview.common.enums.DocumentParserProvider;
+import com.qingqiu.interview.service.parser.DocumentParser;
+import com.qingqiu.interview.service.parser.DocumentParserManager;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 16:45
+ */
+@Service
+public class DocumentParserManagerImpl implements DocumentParserManager {
+ private final Map factories;
+
+ public DocumentParserManagerImpl(List strategies) {
+ this.factories = strategies.stream()
+ .collect(Collectors.toMap(
+ DocumentParser::getSupportedProvider,
+ Function.identity()
+ ));
+ }
+
+
+ @Override
+ public DocumentParser getParser(DocumentParserProvider provider) {
+ DocumentParser parser = factories.get(provider);
+ if (parser == null) {
+ throw new IllegalArgumentException("不支持的AI type: " + provider);
+ }
+ return parser;
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/service/parser/MarkdownParserService.java b/src/main/java/com/qingqiu/interview/service/impl/parser/MarkdownParserServiceImpl.java
similarity index 69%
rename from src/main/java/com/qingqiu/interview/service/parser/MarkdownParserService.java
rename to src/main/java/com/qingqiu/interview/service/impl/parser/MarkdownParserServiceImpl.java
index e917acf..b855d18 100755
--- a/src/main/java/com/qingqiu/interview/service/parser/MarkdownParserService.java
+++ b/src/main/java/com/qingqiu/interview/service/impl/parser/MarkdownParserServiceImpl.java
@@ -1,32 +1,39 @@
-package com.qingqiu.interview.service.parser;
-
-import org.commonmark.node.Node;
-import org.commonmark.parser.Parser;
-import org.commonmark.renderer.text.TextContentRenderer;
-import org.springframework.stereotype.Service;
-
-import java.io.InputStream;
-import java.io.InputStreamReader;
-
-@Service("mdParser")
-public class MarkdownParserService implements DocumentParser {
-
- private final Parser parser = Parser.builder().build();
- private final TextContentRenderer renderer = TextContentRenderer.builder().build();
-
- @Override
- public String parse(InputStream inputStream) {
- try (InputStreamReader reader = new InputStreamReader(inputStream)) {
- Node document = parser.parseReader(reader);
- return renderer.render(document);
- } catch (Exception e) {
- throw new RuntimeException("Failed to parse Markdown document", e);
- }
- }
-
- @Override
- public String getSupportedType() {
- return "md";
- }
-}
-
+package com.qingqiu.interview.service.impl.parser;
+
+import com.qingqiu.interview.common.enums.DocumentParserProvider;
+import com.qingqiu.interview.service.parser.DocumentParser;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.text.TextContentRenderer;
+import org.springframework.stereotype.Service;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+@Service("mdParser")
+public class MarkdownParserServiceImpl implements DocumentParser {
+
+ private final Parser parser = Parser.builder().build();
+ private final TextContentRenderer renderer = TextContentRenderer.builder().build();
+
+ @Override
+ public String parse(InputStream inputStream) {
+ try (InputStreamReader reader = new InputStreamReader(inputStream)) {
+ Node document = parser.parseReader(reader);
+ return renderer.render(document);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to parse Markdown document", e);
+ }
+ }
+
+ @Override
+ public String getSupportedType() {
+ return "md";
+ }
+
+ @Override
+ public DocumentParserProvider getSupportedProvider() {
+ return DocumentParserProvider.MARKDOWN;
+ }
+}
+
diff --git a/src/main/java/com/qingqiu/interview/service/parser/PdfParserService.java b/src/main/java/com/qingqiu/interview/service/impl/parser/PdfParserServiceImpl.java
similarity index 82%
rename from src/main/java/com/qingqiu/interview/service/parser/PdfParserService.java
rename to src/main/java/com/qingqiu/interview/service/impl/parser/PdfParserServiceImpl.java
index 9f1b022..57ea707 100755
--- a/src/main/java/com/qingqiu/interview/service/parser/PdfParserService.java
+++ b/src/main/java/com/qingqiu/interview/service/impl/parser/PdfParserServiceImpl.java
@@ -1,57 +1,62 @@
-package com.qingqiu.interview.service.parser;
-
-
-import org.apache.pdfbox.Loader;
-import org.apache.pdfbox.cos.COSDocument;
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.pdmodel.fdf.FDFDocument;
-import org.apache.pdfbox.text.PDFTextStripper;
-import org.springframework.stereotype.Service;
-
-import java.io.InputStream;
-import java.util.Objects;
-
-@Service("pdfParser")
-public class PdfParserService implements DocumentParser {
-
- /**
- * 解析 PDF 文档内容
- * @param inputStream PDF 文件输入流
- * @return 提取的文本内容
- */
- @Override
- public String parse(InputStream inputStream) {
- // 检查输入流是否为 null,避免空指针异常
- Objects.requireNonNull(inputStream, "PDF文件输入流不能为空");
-
- // 使用 try-with-resources 确保 PDDocument 资源自动关闭
-
- try (PDDocument document = Loader.loadPDF(inputStream.readAllBytes())) {
-
- // 创建 PDF 文本提取器
- PDFTextStripper pdfStripper = new PDFTextStripper();
-
- // 配置提取参数
- pdfStripper.setSortByPosition(true); // 按位置排序,保持原始布局
- pdfStripper.setAddMoreFormatting(true); // 保留更多格式信息
- pdfStripper.setSuppressDuplicateOverlappingText(true); // 去除重复文本
-
- // 执行文本提取并返回结果
- return pdfStripper.getText(document);
-
- } catch (Exception e) {
- // 处理其他未知异常
- throw new RuntimeException("解析PDF时发生未知错误", e);
- }
- }
-
- /**
- * 获取该解析器支持的文档类型
- * @return 支持的文档类型标识(此处为"pdf")
- */
- @Override
- public String getSupportedType() {
- return "pdf"; // 返回支持的文档类型
- }
-}
-
+package com.qingqiu.interview.service.impl.parser;
+
+
+import com.qingqiu.interview.common.enums.DocumentParserProvider;
+import com.qingqiu.interview.service.parser.DocumentParser;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
+import org.springframework.stereotype.Service;
+
+import java.io.InputStream;
+import java.util.Objects;
+
+@Service("pdfParser")
+public class PdfParserServiceImpl implements DocumentParser {
+
+ /**
+ * 解析 PDF 文档内容
+ * @param inputStream PDF 文件输入流
+ * @return 提取的文本内容
+ */
+ @Override
+ public String parse(InputStream inputStream) {
+ // 检查输入流是否为 null,避免空指针异常
+ Objects.requireNonNull(inputStream, "PDF文件输入流不能为空");
+
+ // 使用 try-with-resources 确保 PDDocument 资源自动关闭
+
+ try (PDDocument document = Loader.loadPDF(inputStream.readAllBytes())) {
+
+ // 创建 PDF 文本提取器
+ PDFTextStripper pdfStripper = new PDFTextStripper();
+
+ // 配置提取参数
+ pdfStripper.setSortByPosition(true); // 按位置排序,保持原始布局
+ pdfStripper.setAddMoreFormatting(true); // 保留更多格式信息
+ pdfStripper.setSuppressDuplicateOverlappingText(true); // 去除重复文本
+
+ // 执行文本提取并返回结果
+ return pdfStripper.getText(document);
+
+ } catch (Exception e) {
+ // 处理其他未知异常
+ throw new RuntimeException("解析PDF时发生未知错误", e);
+ }
+ }
+
+ /**
+ * 获取该解析器支持的文档类型
+ * @return 支持的文档类型标识(此处为"pdf")
+ */
+ @Override
+ public String getSupportedType() {
+ return "pdf"; // 返回支持的文档类型
+ }
+
+ @Override
+ public DocumentParserProvider getSupportedProvider() {
+ return DocumentParserProvider.PDF;
+ }
+}
+
diff --git a/src/main/java/com/qingqiu/interview/service/llm/LlmService.java b/src/main/java/com/qingqiu/interview/service/llm/LlmService.java
index 21cf61a..afa630d 100755
--- a/src/main/java/com/qingqiu/interview/service/llm/LlmService.java
+++ b/src/main/java/com/qingqiu/interview/service/llm/LlmService.java
@@ -1,6 +1,6 @@
package com.qingqiu.interview.service.llm;
-import com.qingqiu.interview.ai.enums.LLMProvider;
+import com.qingqiu.interview.common.enums.LLMProvider;
public interface LlmService {
diff --git a/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java b/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java
index a9cf224..a7a6ef2 100755
--- a/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java
+++ b/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java
@@ -7,7 +7,7 @@ import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.tokenizers.Tokenizer;
import com.alibaba.dashscope.tokenizers.TokenizerFactory;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.qingqiu.interview.ai.enums.LLMProvider;
+import com.qingqiu.interview.common.enums.LLMProvider;
import com.qingqiu.interview.ai.factory.AIClientManager;
import com.qingqiu.interview.entity.AiSessionLog;
import com.qingqiu.interview.mapper.AiSessionLogMapper;
diff --git a/src/main/java/com/qingqiu/interview/service/parser/DocumentParser.java b/src/main/java/com/qingqiu/interview/service/parser/DocumentParser.java
index c47d5f8..a26be4f 100755
--- a/src/main/java/com/qingqiu/interview/service/parser/DocumentParser.java
+++ b/src/main/java/com/qingqiu/interview/service/parser/DocumentParser.java
@@ -1,5 +1,7 @@
package com.qingqiu.interview.service.parser;
+import com.qingqiu.interview.common.enums.DocumentParserProvider;
+
import java.io.InputStream;
public interface DocumentParser {
@@ -15,5 +17,7 @@ public interface DocumentParser {
* @return "pdf", "md", etc.
*/
String getSupportedType();
+
+ DocumentParserProvider getSupportedProvider();
}
diff --git a/src/main/java/com/qingqiu/interview/service/parser/DocumentParserManager.java b/src/main/java/com/qingqiu/interview/service/parser/DocumentParserManager.java
new file mode 100644
index 0000000..f962a19
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/service/parser/DocumentParserManager.java
@@ -0,0 +1,14 @@
+package com.qingqiu.interview.service.parser;
+
+import com.qingqiu.interview.common.enums.DocumentParserProvider;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 16:42
+ */
+public interface DocumentParserManager {
+
+ DocumentParser getParser(DocumentParserProvider provider);
+}
diff --git a/src/main/java/com/qingqiu/interview/vo/ChatVO.java b/src/main/java/com/qingqiu/interview/vo/ChatVO.java
new file mode 100644
index 0000000..1d97b1e
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/vo/ChatVO.java
@@ -0,0 +1,22 @@
+package com.qingqiu.interview.vo;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/18 12:56
+ */
+@Data
+@Accessors
+@Builder
+public class ChatVO {
+
+ private String sessionId;
+ private String content;
+ /** 角色 */
+ private String role;
+}