优化代码
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -43,6 +43,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- aop和aspect -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.qingqiu.interview.ai.factory;
|
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.AIClientService;
|
||||||
|
|
||||||
public interface AIClientFactory {
|
public interface AIClientFactory {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.qingqiu.interview.ai.factory;
|
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.AIClientService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.qingqiu.interview.ai.factory;
|
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.AIClientService;
|
||||||
import com.qingqiu.interview.ai.service.impl.DeepSeekClientServiceImpl;
|
import com.qingqiu.interview.ai.service.impl.DeepSeekClientServiceImpl;
|
||||||
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.qingqiu.interview.ai.factory;
|
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.AIClientService;
|
||||||
import com.qingqiu.interview.ai.service.impl.QwenClientServiceImpl;
|
import com.qingqiu.interview.ai.service.impl.QwenClientServiceImpl;
|
||||||
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.qingqiu.interview.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 12:58
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface AiChatLog {
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>
|
||||||
|
* ai聊天的切面
|
||||||
|
* </h1>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.qingqiu.interview.common.constants;
|
package com.qingqiu.interview.common.constants;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <h1>公共常量</h1>
|
* <h1>公共常量</h1>
|
||||||
* @author huangpeng
|
* @author huangpeng
|
||||||
@@ -10,4 +12,6 @@ public class CommonConstant {
|
|||||||
public static final Integer ZERO = 0;
|
public static final Integer ZERO = 0;
|
||||||
public static final Integer ONE = 1;
|
public static final Integer ONE = 1;
|
||||||
public static final Long ROOT_PARENT_ID = 0L;
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.qingqiu.interview.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.qingqiu.interview.ai.enums;
|
package com.qingqiu.interview.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
public enum LLMProvider {
|
public enum LLMProvider {
|
||||||
|
|
||||||
OPEN_AI("openai"),
|
OPEN_AI("openai"),
|
||||||
@@ -16,10 +19,6 @@ public enum LLMProvider {
|
|||||||
this.code = code;
|
this.code = code;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LLMProvider fromCode(String code) {
|
public static LLMProvider fromCode(String code) {
|
||||||
for (LLMProvider provider : values()) {
|
for (LLMProvider provider : values()) {
|
||||||
if (provider.getCode().equals(code)) {
|
if (provider.getCode().equals(code)) {
|
||||||
@@ -2,6 +2,10 @@ package com.qingqiu.interview.common.utils;
|
|||||||
|
|
||||||
import com.alibaba.dashscope.common.Message;
|
import com.alibaba.dashscope.common.Message;
|
||||||
import com.alibaba.dashscope.common.Role;
|
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 {
|
public class AIUtils {
|
||||||
|
|
||||||
@@ -23,4 +27,15 @@ public class AIUtils {
|
|||||||
public static Message createSystemMessage(String prompt) {
|
public static Message createSystemMessage(String prompt) {
|
||||||
return createMessage(Role.SYSTEM.getValue(), prompt);
|
return createMessage(Role.SYSTEM.getValue(), prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取prompt的token数
|
||||||
|
* @param prompt 输入
|
||||||
|
* @return tokens
|
||||||
|
*/
|
||||||
|
public static Integer getPromptTokens(String prompt) {
|
||||||
|
Tokenizer tokenizer = TokenizerFactory.qwen();
|
||||||
|
List<Integer> integers = tokenizer.encodeOrdinary(prompt);
|
||||||
|
return integers.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>AI聊天控制器</h1>
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/java/com/qingqiu/interview/dto/ChatDTO.java
Normal file
26
src/main/java/com/qingqiu/interview/dto/ChatDTO.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.qingqiu.interview.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ import java.time.LocalDateTime;
|
|||||||
@TableName("ai_session_log")
|
@TableName("ai_session_log")
|
||||||
public class AiSessionLog implements Serializable {
|
public class AiSessionLog implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
@@ -32,6 +34,11 @@ public class AiSessionLog implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据类型 0 普通会话 1 面试会话
|
||||||
|
*/
|
||||||
|
private Integer dataType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 输入内容
|
* 输入内容
|
||||||
*/
|
*/
|
||||||
@@ -54,5 +61,8 @@ public class AiSessionLog implements Serializable {
|
|||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
private LocalDateTime updatedTime;
|
private LocalDateTime updatedTime;
|
||||||
|
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ public class InterviewQuestionProgress {
|
|||||||
@TableField("question_content")
|
@TableField("question_content")
|
||||||
private String questionContent;
|
private String questionContent;
|
||||||
|
|
||||||
|
/** 问题序号 */
|
||||||
|
private Integer questionIndex;
|
||||||
|
|
||||||
|
/** 答题耗时(秒) */
|
||||||
|
private Long timeTaken;
|
||||||
|
|
||||||
|
/** 详细评估信息 */
|
||||||
|
private String evaluationDetails;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 面试会话ID
|
* 面试会话ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ public class InterviewSession implements Serializable {
|
|||||||
|
|
||||||
@TableField("extracted_skills")
|
@TableField("extracted_skills")
|
||||||
private String extractedSkills;
|
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")
|
@TableField("ai_model")
|
||||||
private String aiModel;
|
private String aiModel;
|
||||||
|
|||||||
29
src/main/java/com/qingqiu/interview/service/ChatService.java
Normal file
29
src/main/java/com/qingqiu/interview/service/ChatService.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 16:37
|
||||||
|
*/
|
||||||
|
public interface InterviewChatService {
|
||||||
|
|
||||||
|
void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException;
|
||||||
|
}
|
||||||
@@ -63,6 +63,12 @@ public class InterviewService {
|
|||||||
|
|
||||||
// 1. 解析简历
|
// 1. 解析简历
|
||||||
String resumeContent = parseResume(resume);
|
String resumeContent = parseResume(resume);
|
||||||
|
// 判断是否AI出题
|
||||||
|
if (request.getModel().equals("local")) {
|
||||||
|
if (CollectionUtil.isEmpty(request.getSelectedNodes())) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 2. 创建会话 并发送AI请求 让其从题库中智能抽题
|
// 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<InterviewMessage> messages = messageMapper.selectBySessionIdOrderByOrder(sessionId);
|
|
||||||
List<SessionHistoryResponse.MessageDto> 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 {
|
private String parseResume(MultipartFile resume) throws IOException {
|
||||||
String fileExtension = getFileExtension(resume.getOriginalFilename());
|
String fileExtension = getFileExtension(resume.getOriginalFilename());
|
||||||
@@ -539,55 +513,6 @@ public class InterviewService {
|
|||||||
""", session.getResumeContent(), historyBuilder.toString());
|
""", session.getResumeContent(), historyBuilder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private InterviewResponse generateNextQuestion(InterviewSession session) {
|
|
||||||
try {
|
|
||||||
// 1. 解析出AI选择的题目ID列表
|
|
||||||
List<Long> selectedQuestionIds = objectMapper.readValue(session.getSelectedQuestionIds(), new com.fasterxml.jackson.core.type.TypeReference<List<Long>>() {
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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); // 解析失败则直接结束面试
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有面试会话列表
|
* 获取所有面试会话列表
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @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<Message> messages = new ArrayList<>();
|
||||||
|
AtomicInteger tokens = new AtomicInteger();
|
||||||
|
// 如果会话id不为空 则从数据库中获取会话记录
|
||||||
|
if (dto.getSessionId() != null) {
|
||||||
|
List<AiSessionLog> list = aiSessionLogService.list(
|
||||||
|
new LambdaQueryWrapper<AiSessionLog>()
|
||||||
|
.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<Message> 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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
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.constants.CommonConstant;
|
||||||
import com.qingqiu.interview.common.utils.TreeUtil;
|
import com.qingqiu.interview.common.utils.TreeUtil;
|
||||||
import com.qingqiu.interview.dto.QuestionOptionsDTO;
|
import com.qingqiu.interview.dto.QuestionOptionsDTO;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 16:45
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DocumentParserManagerImpl implements DocumentParserManager {
|
||||||
|
private final Map<DocumentParserProvider, DocumentParser> factories;
|
||||||
|
|
||||||
|
public DocumentParserManagerImpl(List<DocumentParser> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,39 @@
|
|||||||
package com.qingqiu.interview.service.parser;
|
package com.qingqiu.interview.service.impl.parser;
|
||||||
|
|
||||||
import org.commonmark.node.Node;
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
import org.commonmark.parser.Parser;
|
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||||
import org.commonmark.renderer.text.TextContentRenderer;
|
import org.commonmark.node.Node;
|
||||||
import org.springframework.stereotype.Service;
|
import org.commonmark.parser.Parser;
|
||||||
|
import org.commonmark.renderer.text.TextContentRenderer;
|
||||||
import java.io.InputStream;
|
import org.springframework.stereotype.Service;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
|
import java.io.InputStream;
|
||||||
@Service("mdParser")
|
import java.io.InputStreamReader;
|
||||||
public class MarkdownParserService implements DocumentParser {
|
|
||||||
|
@Service("mdParser")
|
||||||
private final Parser parser = Parser.builder().build();
|
public class MarkdownParserServiceImpl implements DocumentParser {
|
||||||
private final TextContentRenderer renderer = TextContentRenderer.builder().build();
|
|
||||||
|
private final Parser parser = Parser.builder().build();
|
||||||
@Override
|
private final TextContentRenderer renderer = TextContentRenderer.builder().build();
|
||||||
public String parse(InputStream inputStream) {
|
|
||||||
try (InputStreamReader reader = new InputStreamReader(inputStream)) {
|
@Override
|
||||||
Node document = parser.parseReader(reader);
|
public String parse(InputStream inputStream) {
|
||||||
return renderer.render(document);
|
try (InputStreamReader reader = new InputStreamReader(inputStream)) {
|
||||||
} catch (Exception e) {
|
Node document = parser.parseReader(reader);
|
||||||
throw new RuntimeException("Failed to parse Markdown document", e);
|
return renderer.render(document);
|
||||||
}
|
} catch (Exception e) {
|
||||||
}
|
throw new RuntimeException("Failed to parse Markdown document", e);
|
||||||
|
}
|
||||||
@Override
|
}
|
||||||
public String getSupportedType() {
|
|
||||||
return "md";
|
@Override
|
||||||
}
|
public String getSupportedType() {
|
||||||
}
|
return "md";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentParserProvider getSupportedProvider() {
|
||||||
|
return DocumentParserProvider.MARKDOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,57 +1,62 @@
|
|||||||
package com.qingqiu.interview.service.parser;
|
package com.qingqiu.interview.service.impl.parser;
|
||||||
|
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
import org.apache.pdfbox.cos.COSDocument;
|
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.fdf.FDFDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service("pdfParser")
|
@Service("pdfParser")
|
||||||
public class PdfParserService implements DocumentParser {
|
public class PdfParserServiceImpl implements DocumentParser {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 PDF 文档内容
|
* 解析 PDF 文档内容
|
||||||
* @param inputStream PDF 文件输入流
|
* @param inputStream PDF 文件输入流
|
||||||
* @return 提取的文本内容
|
* @return 提取的文本内容
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String parse(InputStream inputStream) {
|
public String parse(InputStream inputStream) {
|
||||||
// 检查输入流是否为 null,避免空指针异常
|
// 检查输入流是否为 null,避免空指针异常
|
||||||
Objects.requireNonNull(inputStream, "PDF文件输入流不能为空");
|
Objects.requireNonNull(inputStream, "PDF文件输入流不能为空");
|
||||||
|
|
||||||
// 使用 try-with-resources 确保 PDDocument 资源自动关闭
|
// 使用 try-with-resources 确保 PDDocument 资源自动关闭
|
||||||
|
|
||||||
try (PDDocument document = Loader.loadPDF(inputStream.readAllBytes())) {
|
try (PDDocument document = Loader.loadPDF(inputStream.readAllBytes())) {
|
||||||
|
|
||||||
// 创建 PDF 文本提取器
|
// 创建 PDF 文本提取器
|
||||||
PDFTextStripper pdfStripper = new PDFTextStripper();
|
PDFTextStripper pdfStripper = new PDFTextStripper();
|
||||||
|
|
||||||
// 配置提取参数
|
// 配置提取参数
|
||||||
pdfStripper.setSortByPosition(true); // 按位置排序,保持原始布局
|
pdfStripper.setSortByPosition(true); // 按位置排序,保持原始布局
|
||||||
pdfStripper.setAddMoreFormatting(true); // 保留更多格式信息
|
pdfStripper.setAddMoreFormatting(true); // 保留更多格式信息
|
||||||
pdfStripper.setSuppressDuplicateOverlappingText(true); // 去除重复文本
|
pdfStripper.setSuppressDuplicateOverlappingText(true); // 去除重复文本
|
||||||
|
|
||||||
// 执行文本提取并返回结果
|
// 执行文本提取并返回结果
|
||||||
return pdfStripper.getText(document);
|
return pdfStripper.getText(document);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 处理其他未知异常
|
// 处理其他未知异常
|
||||||
throw new RuntimeException("解析PDF时发生未知错误", e);
|
throw new RuntimeException("解析PDF时发生未知错误", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取该解析器支持的文档类型
|
* 获取该解析器支持的文档类型
|
||||||
* @return 支持的文档类型标识(此处为"pdf")
|
* @return 支持的文档类型标识(此处为"pdf")
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String getSupportedType() {
|
public String getSupportedType() {
|
||||||
return "pdf"; // 返回支持的文档类型
|
return "pdf"; // 返回支持的文档类型
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public DocumentParserProvider getSupportedProvider() {
|
||||||
|
return DocumentParserProvider.PDF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.qingqiu.interview.service.llm;
|
package com.qingqiu.interview.service.llm;
|
||||||
|
|
||||||
import com.qingqiu.interview.ai.enums.LLMProvider;
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
|
|
||||||
public interface LlmService {
|
public interface LlmService {
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.alibaba.dashscope.common.Role;
|
|||||||
import com.alibaba.dashscope.tokenizers.Tokenizer;
|
import com.alibaba.dashscope.tokenizers.Tokenizer;
|
||||||
import com.alibaba.dashscope.tokenizers.TokenizerFactory;
|
import com.alibaba.dashscope.tokenizers.TokenizerFactory;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
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.ai.factory.AIClientManager;
|
||||||
import com.qingqiu.interview.entity.AiSessionLog;
|
import com.qingqiu.interview.entity.AiSessionLog;
|
||||||
import com.qingqiu.interview.mapper.AiSessionLogMapper;
|
import com.qingqiu.interview.mapper.AiSessionLogMapper;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.qingqiu.interview.service.parser;
|
package com.qingqiu.interview.service.parser;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
public interface DocumentParser {
|
public interface DocumentParser {
|
||||||
@@ -15,5 +17,7 @@ public interface DocumentParser {
|
|||||||
* @return "pdf", "md", etc.
|
* @return "pdf", "md", etc.
|
||||||
*/
|
*/
|
||||||
String getSupportedType();
|
String getSupportedType();
|
||||||
|
|
||||||
|
DocumentParserProvider getSupportedProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.qingqiu.interview.service.parser;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 16:42
|
||||||
|
*/
|
||||||
|
public interface DocumentParserManager {
|
||||||
|
|
||||||
|
DocumentParser getParser(DocumentParserProvider provider);
|
||||||
|
}
|
||||||
22
src/main/java/com/qingqiu/interview/vo/ChatVO.java
Normal file
22
src/main/java/com/qingqiu/interview/vo/ChatVO.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package com.qingqiu.interview.vo;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 12:56
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors
|
||||||
|
@Builder
|
||||||
|
public class ChatVO {
|
||||||
|
|
||||||
|
private String sessionId;
|
||||||
|
private String content;
|
||||||
|
/** 角色 */
|
||||||
|
private String role;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user