diff --git a/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java b/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java
index 13451ed..07f0e05 100644
--- a/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java
+++ b/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java
@@ -1,10 +1,17 @@
package com.qingqiu.interview.aspect;
+import com.qingqiu.interview.dto.ChatDTO;
+import com.qingqiu.interview.entity.AiSessionLog;
+import com.qingqiu.interview.service.IAiSessionLogService;
+import com.qingqiu.interview.vo.ChatVO;
+import jakarta.annotation.Resource;
+import org.apache.commons.lang3.StringUtils;
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;
+import org.springframework.transaction.annotation.Transactional;
/**
*
@@ -18,6 +25,9 @@ import org.springframework.stereotype.Component;
@Component
public class AiChatLogAspect {
+ @Resource
+ private IAiSessionLogService aiSessionLogService;
+
public AiChatLogAspect() {
}
@@ -27,8 +37,35 @@ public class AiChatLogAspect {
}
@Around("logPointCut()")
+ @Transactional(rollbackFor = Exception.class)
public Object around(ProceedingJoinPoint point) throws Throwable {
+
+ Object[] args = point.getArgs();
+ ChatDTO arg = (ChatDTO) args[0];
+ if (StringUtils.isNoneBlank(arg.getSessionId())) {
+ AiSessionLog userSessionLog = new AiSessionLog();
+ userSessionLog
+ .setRole(arg.getRole())
+ .setDataType(arg.getDataType())
+ .setContent(arg.getContent())
+ .setToken(arg.getSessionId())
+ ;
+ aiSessionLogService.save(userSessionLog);
+ }
+
+
Object result = point.proceed();
+
+ ChatVO chatVO = (ChatVO) result;
+ if (StringUtils.isNotBlank(chatVO.getSessionId())) {
+ AiSessionLog aiSessionLog = new AiSessionLog();
+ aiSessionLog
+ .setRole(chatVO.getRole())
+ .setContent(chatVO.getContent())
+ .setToken(chatVO.getSessionId())
+ ;
+ aiSessionLogService.save(aiSessionLog);
+ }
return result;
}
}
diff --git a/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java b/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java
index 269ceca..4935faf 100755
--- a/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java
+++ b/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java
@@ -1,10 +1,22 @@
package com.qingqiu.interview.controller;
+import com.alibaba.dashscope.common.Role;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.qingqiu.interview.common.res.R;
+import com.qingqiu.interview.entity.AiSessionLog;
+import com.qingqiu.interview.service.IAiSessionLogService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
-
import org.springframework.web.bind.annotation.RestController;
+import java.util.List;
+
/**
*
* ai会话记录 前端控制器
@@ -13,8 +25,22 @@ import org.springframework.web.bind.annotation.RestController;
* @author huangpeng
* @since 2025-08-30
*/
+@Slf4j
@RestController
@RequestMapping("/ai-session-log")
+@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class AiSessionLogController {
+ private final IAiSessionLogService service;
+
+ @GetMapping("/list-by-session-id/{sessionId}")
+ public R> list(@PathVariable String sessionId) {
+ return R.success(service.list(
+ new LambdaQueryWrapper()
+ .eq(AiSessionLog::getToken, sessionId)
+ .ne(AiSessionLog::getRole, Role.SYSTEM.getValue())
+ ));
+ }
+
+
}
diff --git a/src/main/java/com/qingqiu/interview/controller/InterviewController.java b/src/main/java/com/qingqiu/interview/controller/InterviewController.java
index 31e0ed2..863b78d 100644
--- a/src/main/java/com/qingqiu/interview/controller/InterviewController.java
+++ b/src/main/java/com/qingqiu/interview/controller/InterviewController.java
@@ -1,9 +1,8 @@
package com.qingqiu.interview.controller;
-import com.alibaba.fastjson2.JSONObject;
import com.qingqiu.interview.common.res.R;
-import com.qingqiu.interview.dto.InterviewStartRequest;
-import com.qingqiu.interview.dto.SubmitAnswerDTO;
+import com.qingqiu.interview.dto.*;
+import com.qingqiu.interview.entity.InterviewMessage;
import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession;
import com.qingqiu.interview.service.InterviewService;
@@ -14,6 +13,8 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
+import java.util.List;
+
/**
*
*
@@ -37,15 +38,15 @@ public class InterviewController {
@PostMapping("/start")
public R start(@RequestPart("resume") MultipartFile resume,
@RequestPart("interviewStartDto") InterviewStartRequest request) {
- log.info("接受的数据: {}", JSONObject.toJSONString(request));
- return R.success();
-// try {
-// InterviewSession session = interviewService.startInterview(resume, request);
-// return R.success(session);
-// } catch (Exception e) {
-// // log.error("开始面试失败", e);
-// return R.error("开始面试失败:" + e.getMessage());
-// }
+// log.info("接受的数据: {}", JSONObject.toJSONString(request));
+// return R.success();
+ try {
+ InterviewSession session = interviewService.startInterview(resume, request);
+ return R.success(session);
+ } catch (Exception e) {
+ log.error("开始面试失败", e);
+ return R.error("开始面试失败:" + e.getMessage());
+ }
}
/**
@@ -54,10 +55,11 @@ public class InterviewController {
* @param sessionId 会话ID
* @return 下一个问题
*/
- @GetMapping("/{sessionId}/next-question")
- public R getNextQuestion(@PathVariable String sessionId) {
+ @GetMapping("/next-question/{sessionId}/{progressId}")
+ public R getNextQuestion(@PathVariable String sessionId,
+ @PathVariable Long progressId) {
try {
- InterviewQuestionProgress nextQuestion = interviewService.getNextQuestion(sessionId);
+ InterviewMessage nextQuestion = interviewService.getNextQuestion(sessionId, progressId);
if (nextQuestion == null) {
return R.success(null, "所有问题已回答完毕!");
}
@@ -101,4 +103,23 @@ public class InterviewController {
return R.error("结束面试失败:" + e.getMessage());
}
}
+
+ @PostMapping("/get-history-list")
+ public R> getHistoryList() {
+ try {
+ List historyList = interviewService.list();
+ return R.success(historyList);
+ } catch (Exception e) {
+ // log.error("获取面试历史列表失败", e);
+ return R.error("获取面试历史列表失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 获取单次面试的详细复盘报告
+ */
+ @PostMapping("/get-report-detail/{sessionId}")
+ public R getInterviewReportDetail(@PathVariable String sessionId) {
+ return R.success(interviewService.getInterviewReport(sessionId));
+ }
}
diff --git a/src/main/java/com/qingqiu/interview/controller/InterviewMessageController.java b/src/main/java/com/qingqiu/interview/controller/InterviewMessageController.java
new file mode 100644
index 0000000..1741433
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/controller/InterviewMessageController.java
@@ -0,0 +1,42 @@
+package com.qingqiu.interview.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.qingqiu.interview.common.res.R;
+import com.qingqiu.interview.entity.InterviewMessage;
+import com.qingqiu.interview.service.InterviewMessageService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/21 11:59
+ */
+@Slf4j
+@RestController
+@RequestMapping("/interview-message")
+@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
+public class InterviewMessageController {
+
+ public final InterviewMessageService service;
+
+ @GetMapping("/list-by-session-id/{sessionId}")
+ public R> listBySessionId(@PathVariable String sessionId) {
+ return R.success(
+ service.list(
+ new LambdaQueryWrapper()
+ .eq(InterviewMessage::getSessionId, sessionId)
+ .orderByAsc(InterviewMessage::getCreatedTime)
+ )
+ );
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/dto/SubmitAnswerDTO.java b/src/main/java/com/qingqiu/interview/dto/SubmitAnswerDTO.java
index 5454cee..3b5332a 100644
--- a/src/main/java/com/qingqiu/interview/dto/SubmitAnswerDTO.java
+++ b/src/main/java/com/qingqiu/interview/dto/SubmitAnswerDTO.java
@@ -19,6 +19,8 @@ public class SubmitAnswerDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
+ private String sessionId;
+
/**
* 当前问题的进度ID (interview_question_progress.id)
*/
diff --git a/src/main/java/com/qingqiu/interview/entity/InterviewMessage.java b/src/main/java/com/qingqiu/interview/entity/InterviewMessage.java
index 6edcd2e..d531f21 100755
--- a/src/main/java/com/qingqiu/interview/entity/InterviewMessage.java
+++ b/src/main/java/com/qingqiu/interview/entity/InterviewMessage.java
@@ -28,8 +28,8 @@ public class InterviewMessage {
@TableField("content")
private String content;
- @TableField("question_id")
- private Long questionId;
+ @TableField("question_progress_id")
+ private Long questionProgressId;
@TableField("message_order")
private Integer messageOrder;
diff --git a/src/main/java/com/qingqiu/interview/entity/InterviewSession.java b/src/main/java/com/qingqiu/interview/entity/InterviewSession.java
index c5a623f..2b3a3a5 100755
--- a/src/main/java/com/qingqiu/interview/entity/InterviewSession.java
+++ b/src/main/java/com/qingqiu/interview/entity/InterviewSession.java
@@ -44,6 +44,8 @@ public class InterviewSession implements Serializable {
@TableField("ai_model")
private String aiModel;
+ @TableField("model")
+ private String model;
@TableField("status")
private String status;
diff --git a/src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java b/src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java
index 208699b..4d59dbb 100755
--- a/src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java
+++ b/src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java
@@ -15,4 +15,6 @@ import com.qingqiu.interview.entity.InterviewQuestionProgress;
*/
public interface IInterviewQuestionProgressService extends IService {
Page pageList(QuestionProgressPageParams params);
+
+ InterviewQuestionProgress getNextQuestion(String sessionId);
}
diff --git a/src/main/java/com/qingqiu/interview/service/InterviewAiService.java b/src/main/java/com/qingqiu/interview/service/InterviewAiService.java
index 56f528b..d0b4724 100644
--- a/src/main/java/com/qingqiu/interview/service/InterviewAiService.java
+++ b/src/main/java/com/qingqiu/interview/service/InterviewAiService.java
@@ -55,4 +55,7 @@ public interface InterviewAiService {
* @return 包含最终报告的JSON对象
*/
JSONObject generateFinalReport(InterviewSession session, List progressList);
+
+ String generateFirstQuestion(String sessionId, String candidateName, String questionContent);
+
}
diff --git a/src/main/java/com/qingqiu/interview/service/InterviewMessageService.java b/src/main/java/com/qingqiu/interview/service/InterviewMessageService.java
new file mode 100644
index 0000000..008f0ab
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/service/InterviewMessageService.java
@@ -0,0 +1,13 @@
+package com.qingqiu.interview.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.qingqiu.interview.entity.InterviewMessage;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/21 12:00
+ */
+public interface InterviewMessageService extends IService {
+}
diff --git a/src/main/java/com/qingqiu/interview/service/InterviewService.java b/src/main/java/com/qingqiu/interview/service/InterviewService.java
index 7d5223e..10ed8d2 100644
--- a/src/main/java/com/qingqiu/interview/service/InterviewService.java
+++ b/src/main/java/com/qingqiu/interview/service/InterviewService.java
@@ -1,8 +1,10 @@
package com.qingqiu.interview.service;
import com.baomidou.mybatisplus.extension.service.IService;
+import com.qingqiu.interview.dto.InterviewReportResponse;
import com.qingqiu.interview.dto.InterviewStartRequest;
import com.qingqiu.interview.dto.SubmitAnswerDTO;
+import com.qingqiu.interview.entity.InterviewMessage;
import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession;
import org.springframework.web.multipart.MultipartFile;
@@ -32,7 +34,7 @@ public interface InterviewService extends IService {
* @param sessionId 会话ID
* @return 下一个问题 或 null(如果没有更多问题)
*/
- InterviewQuestionProgress getNextQuestion(String sessionId);
+ InterviewMessage getNextQuestion(String sessionId, Long progressId);
/**
* 提交答案并获取AI评估
@@ -49,4 +51,11 @@ public interface InterviewService extends IService {
* @return 包含最终报告的面试会话信息
*/
InterviewSession endInterview(String sessionId);
+
+ /**
+ * 获取面试报告
+ * @param sessionId
+ * @return
+ */
+ InterviewReportResponse getInterviewReport(String sessionId);
}
diff --git a/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java
index 5ee2841..31d7020 100644
--- a/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java
+++ b/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java
@@ -4,8 +4,9 @@ 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.annotation.AiChatLog;
+import com.qingqiu.interview.common.enums.LLMProvider;
import com.qingqiu.interview.common.utils.AIUtils;
import com.qingqiu.interview.dto.ChatDTO;
import com.qingqiu.interview.dto.InterviewStartRequest;
@@ -41,9 +42,10 @@ public class ChatServiceImpl implements ChatService {
private final AIClientManager aiClientManager;
- private IAiSessionLogService aiSessionLogService;
+ private final IAiSessionLogService aiSessionLogService;
@Override
+ @AiChatLog
public ChatVO createChat(ChatDTO dto) {
LLMProvider llmProvider = LLMProvider.fromCode(dto.getAiModel());
List messages = new ArrayList<>();
@@ -57,16 +59,13 @@ public class ChatServiceImpl implements ChatService {
.orderByAsc(AiSessionLog::getCreatedTime)
);
if (CollectionUtil.isNotEmpty(list)) {
- messages = list.stream().map(data -> {
+ messages.addAll(list.stream().map(data -> {
tokens.getAndAdd(AIUtils.getPromptTokens(data.getContent()));
return AIUtils.createMessage(data.getRole(), data.getContent());
- }).toList();
+ }).toList());
}
}
- if (CollectionUtil.isEmpty( messages)) {
- messages = new ArrayList<>();
- }
messages.add(AIUtils.createMessage(dto.getRole(), dto.getContent()));
List finalMessage = new ArrayList<>();
// 剪切 10%的消息
diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java
index 0a9dc6b..d05bc91 100644
--- a/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java
+++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java
@@ -43,7 +43,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
ChatDTO chatDTO = new ChatDTO()
.setContent(prompt)
- .setRole(Role.SYSTEM.name())
+ .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO);
@@ -67,7 +67,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
ChatDTO chatDTO = new ChatDTO()
.setSessionId(sessionId)
.setContent(prompt)
- .setRole(Role.SYSTEM.name())
+ .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent());
@@ -99,7 +99,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
ChatDTO chatDTO = new ChatDTO()
.setSessionId(sessionId)
.setContent(prompt)
- .setRole(Role.SYSTEM.name())
+ .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent());
@@ -114,10 +114,11 @@ public class InterviewAiServiceImpl implements InterviewAiService {
String prompt = "你是一位资深的技术面试官,以严格和深入著称。" +
"你需要评估候选人对以下问题的回答。请注意:\n" +
- "1. 如果回答模糊、不完整或有错误,你必须提出一个具体的追问问题(followUpQuestion)来深入考察,此时'continueAsking'应为true。\n" +
+ "1. 如果回答模糊、不完整或有错误,你可以提出一个具体的追问问题(followUpQuestion)来深入考察,此时'continueAsking'应为true。\n" +
"2. 如果回答得很好,则'continueAsking'为false,'followUpQuestion'为空字符串。\n" +
"3. 'score'范围为0-100分。\n" +
"4. 'feedback'和'suggestions'需要给出专业、有建设性的意见。\n" +
+ "5. 追问最好有限制,不要无限制的向下追问,注意追问是支线而非主线!追问至多3个问题,之后必须切回主线\n" +
"请严格按照以下JSON格式返回,不要有任何额外说明:\n" +
"{\"feedback\": \"...\", \"suggestions\": \"...\", \"aiAnswer\": \"...\", \"score\": 85.5, \"continueAsking\": false, \"followUpQuestion\": \"...\"}\n\n" +
"面试历史上下文:\n" + history + "\n\n" +
@@ -125,8 +126,9 @@ public class InterviewAiServiceImpl implements InterviewAiService {
"候选人回答:\n" + userAnswer;
ChatDTO chatDTO = new ChatDTO()
+ .setSessionId(sessionId)
.setContent(prompt)
- .setRole(Role.SYSTEM.name())
+ .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent());
@@ -134,24 +136,92 @@ public class InterviewAiServiceImpl implements InterviewAiService {
@Override
public JSONObject generateFinalReport(InterviewSession session, List progressList) {
- String transcript = progressList.stream()
- .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n",
- p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback()))
- .collect(Collectors.joining("\n-----------------\n"));
+// String transcript = progressList.stream()
+// .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n",
+// p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback()))
+// .collect(Collectors.joining("\n-----------------\n"));
- String prompt = "你是一位经验丰富的招聘经理。" +
- "请根据以下完整的面试记录,为候选人生成一份综合评估报告。" +
- "报告需要包括一个总分(overallScore),简明扼要的总结(summary),以及候选人的优点(strengths)和待提升点(weaknesses)。" +
- "请严格按照以下JSON格式返回:\n" +
- "{\"overallScore\": 88.0, \"summary\": \"...\", \"strengths\": [\"...\"], \"weaknesses\": [\"...\"]}\n\n" +
- "候选人姓名:" + session.getCandidateName() + "\n" +
- "面试完整记录:\n" + transcript;
+// String prompt = "你是一位经验丰富的招聘经理。" +
+// "请根据以下完整的面试记录,为候选人生成一份综合评估报告。" +
+// "报告需要包括一个总分(overallScore),简明扼要的总结(summary),以及候选人的优点(strengths)和待提升点(weaknesses)。" +
+// "请严格按照以下JSON格式返回:\n" +
+// "{\"overallScore\": 88.0, \"summary\": \"...\", \"strengths\": [\"...\"], \"weaknesses\": [\"...\"]}\n\n" +
+// "候选人姓名:" + session.getCandidateName() + "\n" +
+// "面试完整记录:\n" + transcript;
+
+ String prompt = buildFinalReportPrompt(session, progressList);
ChatDTO chatDTO = new ChatDTO()
- .setRole(Role.SYSTEM.name())
+ .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE)
.setContent(prompt);
ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent());
}
+
+ private String buildFinalReportPrompt(InterviewSession session, List progressList) {
+ StringBuilder historyBuilder = new StringBuilder();
+ for (InterviewQuestionProgress progress : progressList) {
+ historyBuilder.append(
+ String.format("\n【问题】: %s\n【回答】: %s\n【AI单题反馈】: %s\n【AI单题建议】: %s\n【AI单题评分】: %s/5.0\n",
+ progress.getQuestionContent(),
+ progress.getUserAnswer(),
+ progress.getFeedback(),
+ progress.getSuggestions(),
+ progress.getScore()
+ )
+ );
+ }
+
+ return String.format("""
+ 你是一位资深的HR和技术总监。请根据以下候选人的简历、完整的面试问答历史和AI对每一题的初步评估,给出一份全面、专业、有深度的最终面试报告。
+
+ 要求:
+ 1. **综合评价**: 对候选人的整体表现给出一个总结性的评语,点出其核心亮点和主要不足。
+ 2. **技术能力评估**: 分点阐述候选人在不同技术领域(如Java基础, Spring, 数据库等)的掌握程度。
+ 3. **改进建议**: 给出3-5条具体的、可操作的学习和改进建议。
+ 4. **综合得分**: 给出一个1-100分的最终综合得分。
+ 5. **录用建议**: 给出明确的录用建议(如:强烈推荐、推荐、待考虑、不推荐)。
+ 6. 以严格的JSON格式返回,不要包含任何额外的解释文字。格式如下:
+ {
+ "overallScore": 85,
+ "overallFeedback": "候选人Java基础扎实,但在高并发场景下的经验有所欠缺...",
+ "technicalAssessment": {
+ "Java基础": "掌握良好,对集合框架理解深入。",
+ "Spring框架": "熟悉基本使用,但对底层原理理解不足。",
+ "数据库": "能够编写常规SQL,但在索引优化方面知识欠缺。"
+ },
+ "suggestions": [
+ "深入学习Spring AOP和事务管理的实现原理。",
+ "系统学习MySQL索引优化和查询性能分析。",
+ "通过实际项目积累高并发处理经验。"
+ ],
+ "hiringRecommendation": "推荐"
+ }
+
+ 【候选人简历摘要】:
+ %s
+
+ 【面试问答与评估历史】:
+ %s
+ """, session.getResumeContent(), historyBuilder.toString());
+ }
+
+ @Override
+ public String generateFirstQuestion(String sessionId, String candidateName, String questionContent) {
+ String prompt = String.format("""
+ 你是一位专业的技术面试官。现在要开始面试,候选人是 %s。
+
+ 第一个问题是:%s
+
+ 请以友好但专业的语气提出这个问题,可以适当添加一些引导性的话语。
+ """, candidateName, questionContent);
+ ChatDTO chatDTO = new ChatDTO()
+ .setSessionId(sessionId)
+ .setRole(Role.SYSTEM.getValue())
+ .setDataType(CommonConstant.ONE)
+ .setContent(prompt);
+ ChatVO chatVO = chatService.createChat(chatDTO);
+ return chatVO.getContent();
+ }
}
diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewMessageServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewMessageServiceImpl.java
new file mode 100644
index 0000000..e71c24a
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewMessageServiceImpl.java
@@ -0,0 +1,23 @@
+package com.qingqiu.interview.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qingqiu.interview.entity.InterviewMessage;
+import com.qingqiu.interview.mapper.InterviewMessageMapper;
+import com.qingqiu.interview.service.InterviewMessageService;
+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;
+
+/**
+ *
+ *
+ * @author qingqiu
+ * @date 2025/9/21 12:00
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
+public class InterviewMessageServiceImpl extends ServiceImpl implements InterviewMessageService {
+}
diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java
index ff21eca..a94893d 100755
--- a/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java
+++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java
@@ -9,8 +9,10 @@ import com.qingqiu.interview.mapper.InterviewQuestionProgressMapper;
import com.qingqiu.interview.service.IInterviewQuestionProgressService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
+import java.util.Objects;
/**
*
@@ -40,4 +42,37 @@ public class InterviewQuestionProgressServiceImpl extends ServiceImpl()
+ .eq(InterviewQuestionProgress::getSessionId, sessionId)
+ .eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.ACTIVE.name())
+ .orderByAsc(InterviewQuestionProgress::getId)
+ .last("LIMIT 1")
+ );
+ if (Objects.nonNull(activeQuestion)) {
+ return activeQuestion;
+ }
+ // 1. 查找第一个处于“默认”状态的问题
+ LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+ queryWrapper.eq(InterviewQuestionProgress::getSessionId, sessionId)
+ .eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.DEFAULT.name())
+ .orderByAsc(InterviewQuestionProgress::getId) // 按插入顺序
+ .last("LIMIT 1");
+ InterviewQuestionProgress nextQuestion = baseMapper.selectOne(queryWrapper);
+
+ if (nextQuestion == null) {
+ // 没有更多的问题了
+ return null;
+ }
+
+ // 2. 将问题状态更新为“进行中”
+ nextQuestion.setStatus(InterviewQuestionProgress.Status.ACTIVE.name());
+ baseMapper.updateById(nextQuestion);
+ return nextQuestion;
+ }
}
diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewServiceImpl.java
index b1393e9..595b83d 100644
--- a/src/main/java/com/qingqiu/interview/service/impl/InterviewServiceImpl.java
+++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewServiceImpl.java
@@ -7,15 +7,15 @@ import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qingqiu.interview.common.enums.DocumentParserProvider;
+import com.qingqiu.interview.common.ex.ApiException;
+import com.qingqiu.interview.dto.InterviewReportResponse;
import com.qingqiu.interview.dto.InterviewStartRequest;
import com.qingqiu.interview.dto.SubmitAnswerDTO;
-import com.qingqiu.interview.entity.InterviewEvaluation;
-import com.qingqiu.interview.entity.InterviewQuestionProgress;
-import com.qingqiu.interview.entity.InterviewSession;
-import com.qingqiu.interview.entity.Question;
+import com.qingqiu.interview.entity.*;
import com.qingqiu.interview.mapper.InterviewEvaluationMapper;
-import com.qingqiu.interview.mapper.InterviewQuestionProgressMapper;
+import com.qingqiu.interview.mapper.InterviewMessageMapper;
import com.qingqiu.interview.mapper.InterviewSessionMapper;
+import com.qingqiu.interview.service.IInterviewQuestionProgressService;
import com.qingqiu.interview.service.InterviewAiService;
import com.qingqiu.interview.service.InterviewService;
import com.qingqiu.interview.service.QuestionService;
@@ -24,6 +24,7 @@ import com.qingqiu.interview.service.parser.DocumentParserManager;
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -33,7 +34,9 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.UUID;
+import java.util.stream.Collectors;
/**
*
@@ -48,10 +51,12 @@ public class InterviewServiceImpl extends ServiceImpl queryWrapper = new LambdaQueryWrapper<>();
- queryWrapper.eq(InterviewQuestionProgress::getSessionId, sessionId)
- .eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.DEFAULT.name())
- .orderByAsc(InterviewQuestionProgress::getId) // 按插入顺序
- .last("LIMIT 1");
- InterviewQuestionProgress nextQuestion = progressMapper.selectOne(queryWrapper);
+ public InterviewMessage getNextQuestion(String sessionId, Long progressId) {
- if (nextQuestion == null) {
- // 没有更多的问题了
+ // 获取下一个问题
+ InterviewQuestionProgress nextQuestion = progressService.getNextQuestion(sessionId);
+ if (Objects.isNull(nextQuestion)) {
return null;
}
+ // 判断是否在interview_message中存在
+ InterviewMessage interviewMessage = messageMapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(InterviewMessage::getQuestionProgressId, nextQuestion.getId())
+ .orderByAsc(InterviewMessage::getId)
+ .last("LIMIT 1")
+ );
+ if (Objects.isNull(interviewMessage)) {
+ InterviewQuestionProgress prevQuestion = progressService.getById(progressId);
- // 2. 将问题状态更新为“进行中”
- nextQuestion.setStatus(InterviewQuestionProgress.Status.ACTIVE.name());
- progressMapper.updateById(nextQuestion);
+ // 格式化返回的内容
+ StringBuilder sb = new StringBuilder();
+ if (StringUtils.isNotBlank(prevQuestion.getFeedback())) {
+ sb.append(prevQuestion.getFeedback()).append("\n");
+ }
+ if (StringUtils.isNotBlank(prevQuestion.getSuggestions())) {
+ sb.append(prevQuestion.getSuggestions()).append("\n");
+ }
+ if (StringUtils.isNotBlank(prevQuestion.getAiAnswer())) {
+ sb.append(prevQuestion.getAiAnswer()).append("\n");
+ }
+ sb.append(nextQuestion.getQuestionContent());
- return nextQuestion;
+ interviewMessage = saveMessage(sessionId,
+ InterviewMessage.MessageType.QUESTION.name(),
+ InterviewMessage.Sender.AI.name(),
+ sb.toString(),
+ nextQuestion.getId()
+ );
+ }
+
+ return interviewMessage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public InterviewQuestionProgress submitAnswer(SubmitAnswerDTO dto) {
// 1. 查询当前正在进行的这个问题
- InterviewQuestionProgress currentProgress = progressMapper.selectById(dto.getProgressId());
- if (currentProgress == null || !InterviewQuestionProgress.Status.ACTIVE.name().equals(currentProgress.getStatus())) {
- throw new RuntimeException("问题进度不存在或已处理");
+ InterviewQuestionProgress currentProgress = progressService.getById(dto.getProgressId());
+ if (Objects.isNull(currentProgress) || !InterviewQuestionProgress.Status.ACTIVE.name().equals(currentProgress.getStatus())) {
+ throw new ApiException("问题进度不存在或已处理");
}
currentProgress.setUserAnswer(dto.getAnswer());
+ // 存储消息
+ saveMessage(dto.getSessionId(),
+ InterviewMessage.MessageType.ANSWER.name(),
+ InterviewMessage.Sender.USER.name(),
+ dto.getAnswer(),
+ currentProgress.getId()
+ );
// 2. 调用AI服务评估回答
- List context = progressMapper.selectList(
+ List context = progressService.list(
new LambdaQueryWrapper()
.eq(InterviewQuestionProgress::getSessionId, currentProgress.getSessionId())
.eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name())
@@ -228,11 +269,12 @@ public class InterviewServiceImpl extends ServiceImpl 解析AI的是否追问判断,并处理追问逻辑 <---
if (evalResult.getBooleanValue("continueAsking", false)) {
// 创建一个新的、状态为ACTIVE的追问问题
@@ -241,7 +283,7 @@ public class InterviewServiceImpl extends ServiceImpl completedProgresses = progressMapper.selectList(
+ List completedProgresses = progressService.list(
new LambdaQueryWrapper()
.eq(InterviewQuestionProgress::getSessionId, sessionId)
.eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name())
@@ -289,6 +331,62 @@ public class InterviewServiceImpl extends ServiceImpl()
+ .eq(InterviewSession::getSessionId, sessionId)
+ .last("LIMIT 1")
+ );
+ if (session == null) {
+ throw new IllegalArgumentException("找不到ID为 " + sessionId + " 的面试会话。");
+ }
+ List progressList = progressService.list(
+ new LambdaQueryWrapper()
+ .eq(InterviewQuestionProgress::getSessionId, sessionId)
+ .orderByAsc(InterviewQuestionProgress::getUpdatedTime)
+ );
+
+
+ List questionDetails = progressList.stream().map(progress -> {
+ InterviewReportResponse.QuestionDetail detail = new InterviewReportResponse.QuestionDetail();
+ detail.setQuestionId(progress.getQuestionId());
+ detail.setQuestionContent(progress.getQuestionContent());
+ detail.setUserAnswer(progress.getUserAnswer());
+ detail.setAiFeedback(progress.getFeedback());
+ detail.setSuggestions(progress.getSuggestions());
+ detail.setScore(progress.getScore());
+ return detail;
+ }).collect(Collectors.toList());
+
+ InterviewReportResponse report = new InterviewReportResponse();
+ report.setSessionDetails(session);
+ report.setQuestionDetails(questionDetails);
+ List interviewMessages = messageMapper.selectList(
+ new LambdaQueryWrapper()
+ .eq(InterviewMessage::getSessionId, sessionId)
+ );
+ // 获取当前面试的 问题
+ InterviewQuestionProgress progress = progressService.getOne(
+ new LambdaQueryWrapper()
+ .eq(InterviewQuestionProgress::getSessionId, sessionId)
+ .eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.ACTIVE.name())
+ .last("LIMIT 1")
+ );
+ if (Objects.nonNull(progress)) {
+ report.setCurrentQuestionId(progress.getQuestionId());
+ }
+ report.setMessages(interviewMessages);
+
+ return report;
+ }
+
+
private String parseResume(MultipartFile resume) throws IOException {
// 获取文件扩展名
String extName = FileNameUtil.extName(resume.getOriginalFilename());
@@ -297,4 +395,20 @@ public class InterviewServiceImpl extends ServiceImpl