优化代码

This commit is contained in:
2025-09-19 15:19:41 +08:00
parent a384bbfd16
commit 8b357fbb93
30 changed files with 585 additions and 182 deletions

View File

@@ -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 -->

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {
}

View File

@@ -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;
}
}

View File

@@ -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");
}

View File

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

View File

@@ -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)) {

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View 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;
}

View File

@@ -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;
}

View File

@@ -33,6 +33,15 @@ public class InterviewQuestionProgress {
@TableField("question_content")
private String questionContent;
/** 问题序号 */
private Integer questionIndex;
/** 答题耗时(秒) */
private Long timeTaken;
/** 详细评估信息 */
private String evaluationDetails;
/**
* 面试会话ID
*/

View File

@@ -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;

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

View File

@@ -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;
}

View File

@@ -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); // 解析失败则直接结束面试
}
}
/**
* 获取所有面试会话列表

View File

@@ -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 "";
}
}

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -1,5 +1,7 @@
package com.qingqiu.interview.service.parser;
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;
@@ -9,7 +11,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
@Service("mdParser")
public class MarkdownParserService implements DocumentParser {
public class MarkdownParserServiceImpl implements DocumentParser {
private final Parser parser = Parser.builder().build();
private final TextContentRenderer renderer = TextContentRenderer.builder().build();
@@ -28,5 +30,10 @@ public class MarkdownParserService implements DocumentParser {
public String getSupportedType() {
return "md";
}
@Override
public DocumentParserProvider getSupportedProvider() {
return DocumentParserProvider.MARKDOWN;
}
}

View File

@@ -1,10 +1,10 @@
package com.qingqiu.interview.service.parser;
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.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;
@@ -12,7 +12,7 @@ import java.io.InputStream;
import java.util.Objects;
@Service("pdfParser")
public class PdfParserService implements DocumentParser {
public class PdfParserServiceImpl implements DocumentParser {
/**
* 解析 PDF 文档内容
@@ -53,5 +53,10 @@ public class PdfParserService implements DocumentParser {
public String getSupportedType() {
return "pdf"; // 返回支持的文档类型
}
@Override
public DocumentParserProvider getSupportedProvider() {
return DocumentParserProvider.PDF;
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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();
}

View File

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

View 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;
}