优化代码
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -43,6 +43,11 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- aop和aspect -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* <h1>公共常量</h1>
|
||||
* @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");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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)) {
|
||||
@@ -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<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.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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,15 @@ public class InterviewQuestionProgress {
|
||||
@TableField("question_content")
|
||||
private String questionContent;
|
||||
|
||||
/** 问题序号 */
|
||||
private Integer questionIndex;
|
||||
|
||||
/** 答题耗时(秒) */
|
||||
private Long timeTaken;
|
||||
|
||||
/** 详细评估信息 */
|
||||
private String evaluationDetails;
|
||||
|
||||
/**
|
||||
* 面试会话ID
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
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. 解析简历
|
||||
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<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 {
|
||||
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<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.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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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