From d3b5ca0033e76702be5579714d50b728653603cf Mon Sep 17 00:00:00 2001 From: huangpeng <1764183241@qq.com> Date: Sun, 21 Sep 2025 21:19:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interview/aspect/AiChatLogAspect.java | 37 ++++ .../controller/AiSessionLogController.java | 28 ++- .../controller/InterviewController.java | 51 ++++-- .../InterviewMessageController.java | 42 +++++ .../interview/dto/SubmitAnswerDTO.java | 2 + .../interview/entity/InterviewMessage.java | 4 +- .../interview/entity/InterviewSession.java | 2 + .../IInterviewQuestionProgressService.java | 2 + .../interview/service/InterviewAiService.java | 3 + .../service/InterviewMessageService.java | 13 ++ .../interview/service/InterviewService.java | 11 +- .../service/impl/ChatServiceImpl.java | 13 +- .../service/impl/InterviewAiServiceImpl.java | 104 +++++++++-- .../impl/InterviewMessageServiceImpl.java | 23 +++ .../InterviewQuestionProgressServiceImpl.java | 35 ++++ .../service/impl/InterviewServiceImpl.java | 172 +++++++++++++++--- 16 files changed, 470 insertions(+), 72 deletions(-) create mode 100644 src/main/java/com/qingqiu/interview/controller/InterviewMessageController.java create mode 100644 src/main/java/com/qingqiu/interview/service/InterviewMessageService.java create mode 100644 src/main/java/com/qingqiu/interview/service/impl/InterviewMessageServiceImpl.java 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