优化代码

This commit is contained in:
2025-09-21 21:19:44 +08:00
parent df5aa0b9c6
commit d3b5ca0033
16 changed files with 470 additions and 72 deletions

View File

@@ -1,10 +1,17 @@
package com.qingqiu.interview.aspect; 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.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/** /**
* <h1> * <h1>
@@ -18,6 +25,9 @@ import org.springframework.stereotype.Component;
@Component @Component
public class AiChatLogAspect { public class AiChatLogAspect {
@Resource
private IAiSessionLogService aiSessionLogService;
public AiChatLogAspect() { public AiChatLogAspect() {
} }
@@ -27,8 +37,35 @@ public class AiChatLogAspect {
} }
@Around("logPointCut()") @Around("logPointCut()")
@Transactional(rollbackFor = Exception.class)
public Object around(ProceedingJoinPoint point) throws Throwable { 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(); 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; return result;
} }
} }

View File

@@ -1,10 +1,22 @@
package com.qingqiu.interview.controller; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* <p> * <p>
* ai会话记录 前端控制器 * ai会话记录 前端控制器
@@ -13,8 +25,22 @@ import org.springframework.web.bind.annotation.RestController;
* @author huangpeng * @author huangpeng
* @since 2025-08-30 * @since 2025-08-30
*/ */
@Slf4j
@RestController @RestController
@RequestMapping("/ai-session-log") @RequestMapping("/ai-session-log")
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class AiSessionLogController { public class AiSessionLogController {
private final IAiSessionLogService service;
@GetMapping("/list-by-session-id/{sessionId}")
public R<List<AiSessionLog>> list(@PathVariable String sessionId) {
return R.success(service.list(
new LambdaQueryWrapper<AiSessionLog>()
.eq(AiSessionLog::getToken, sessionId)
.ne(AiSessionLog::getRole, Role.SYSTEM.getValue())
));
}
} }

View File

@@ -1,9 +1,8 @@
package com.qingqiu.interview.controller; package com.qingqiu.interview.controller;
import com.alibaba.fastjson2.JSONObject;
import com.qingqiu.interview.common.res.R; import com.qingqiu.interview.common.res.R;
import com.qingqiu.interview.dto.InterviewStartRequest; import com.qingqiu.interview.dto.*;
import com.qingqiu.interview.dto.SubmitAnswerDTO; import com.qingqiu.interview.entity.InterviewMessage;
import com.qingqiu.interview.entity.InterviewQuestionProgress; import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession; import com.qingqiu.interview.entity.InterviewSession;
import com.qingqiu.interview.service.InterviewService; 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.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/** /**
* <h1></h1> * <h1></h1>
* *
@@ -37,15 +38,15 @@ public class InterviewController {
@PostMapping("/start") @PostMapping("/start")
public R<InterviewSession> start(@RequestPart("resume") MultipartFile resume, public R<InterviewSession> start(@RequestPart("resume") MultipartFile resume,
@RequestPart("interviewStartDto") InterviewStartRequest request) { @RequestPart("interviewStartDto") InterviewStartRequest request) {
log.info("接受的数据: {}", JSONObject.toJSONString(request)); // log.info("接受的数据: {}", JSONObject.toJSONString(request));
return R.success(); // return R.success();
// try { try {
// InterviewSession session = interviewService.startInterview(resume, request); InterviewSession session = interviewService.startInterview(resume, request);
// return R.success(session); return R.success(session);
// } catch (Exception e) { } catch (Exception e) {
// // log.error("开始面试失败", e); log.error("开始面试失败", e);
// return R.error("开始面试失败:" + e.getMessage()); return R.error("开始面试失败:" + e.getMessage());
// } }
} }
/** /**
@@ -54,10 +55,11 @@ public class InterviewController {
* @param sessionId 会话ID * @param sessionId 会话ID
* @return 下一个问题 * @return 下一个问题
*/ */
@GetMapping("/{sessionId}/next-question") @GetMapping("/next-question/{sessionId}/{progressId}")
public R<InterviewQuestionProgress> getNextQuestion(@PathVariable String sessionId) { public R<InterviewMessage> getNextQuestion(@PathVariable String sessionId,
@PathVariable Long progressId) {
try { try {
InterviewQuestionProgress nextQuestion = interviewService.getNextQuestion(sessionId); InterviewMessage nextQuestion = interviewService.getNextQuestion(sessionId, progressId);
if (nextQuestion == null) { if (nextQuestion == null) {
return R.success(null, "所有问题已回答完毕!"); return R.success(null, "所有问题已回答完毕!");
} }
@@ -101,4 +103,23 @@ public class InterviewController {
return R.error("结束面试失败:" + e.getMessage()); return R.error("结束面试失败:" + e.getMessage());
} }
} }
@PostMapping("/get-history-list")
public R<List<InterviewSession>> getHistoryList() {
try {
List<InterviewSession> 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<InterviewReportResponse> getInterviewReportDetail(@PathVariable String sessionId) {
return R.success(interviewService.getInterviewReport(sessionId));
}
} }

View File

@@ -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;
/**
* <h1></h1>
*
* @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<List<InterviewMessage>> listBySessionId(@PathVariable String sessionId) {
return R.success(
service.list(
new LambdaQueryWrapper<InterviewMessage>()
.eq(InterviewMessage::getSessionId, sessionId)
.orderByAsc(InterviewMessage::getCreatedTime)
)
);
}
}

View File

@@ -19,6 +19,8 @@ public class SubmitAnswerDTO implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private String sessionId;
/** /**
* 当前问题的进度ID (interview_question_progress.id) * 当前问题的进度ID (interview_question_progress.id)
*/ */

View File

@@ -28,8 +28,8 @@ public class InterviewMessage {
@TableField("content") @TableField("content")
private String content; private String content;
@TableField("question_id") @TableField("question_progress_id")
private Long questionId; private Long questionProgressId;
@TableField("message_order") @TableField("message_order")
private Integer messageOrder; private Integer messageOrder;

View File

@@ -44,6 +44,8 @@ public class InterviewSession implements Serializable {
@TableField("ai_model") @TableField("ai_model")
private String aiModel; private String aiModel;
@TableField("model")
private String model;
@TableField("status") @TableField("status")
private String status; private String status;

View File

@@ -15,4 +15,6 @@ import com.qingqiu.interview.entity.InterviewQuestionProgress;
*/ */
public interface IInterviewQuestionProgressService extends IService<InterviewQuestionProgress> { public interface IInterviewQuestionProgressService extends IService<InterviewQuestionProgress> {
Page<InterviewQuestionProgress> pageList(QuestionProgressPageParams params); Page<InterviewQuestionProgress> pageList(QuestionProgressPageParams params);
InterviewQuestionProgress getNextQuestion(String sessionId);
} }

View File

@@ -55,4 +55,7 @@ public interface InterviewAiService {
* @return 包含最终报告的JSON对象 * @return 包含最终报告的JSON对象
*/ */
JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList); JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList);
String generateFirstQuestion(String sessionId, String candidateName, String questionContent);
} }

View File

@@ -0,0 +1,13 @@
package com.qingqiu.interview.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.qingqiu.interview.entity.InterviewMessage;
/**
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/21 12:00
*/
public interface InterviewMessageService extends IService<InterviewMessage> {
}

View File

@@ -1,8 +1,10 @@
package com.qingqiu.interview.service; package com.qingqiu.interview.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.qingqiu.interview.dto.InterviewReportResponse;
import com.qingqiu.interview.dto.InterviewStartRequest; import com.qingqiu.interview.dto.InterviewStartRequest;
import com.qingqiu.interview.dto.SubmitAnswerDTO; import com.qingqiu.interview.dto.SubmitAnswerDTO;
import com.qingqiu.interview.entity.InterviewMessage;
import com.qingqiu.interview.entity.InterviewQuestionProgress; import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession; import com.qingqiu.interview.entity.InterviewSession;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -32,7 +34,7 @@ public interface InterviewService extends IService<InterviewSession> {
* @param sessionId 会话ID * @param sessionId 会话ID
* @return 下一个问题 或 null如果没有更多问题 * @return 下一个问题 或 null如果没有更多问题
*/ */
InterviewQuestionProgress getNextQuestion(String sessionId); InterviewMessage getNextQuestion(String sessionId, Long progressId);
/** /**
* 提交答案并获取AI评估 * 提交答案并获取AI评估
@@ -49,4 +51,11 @@ public interface InterviewService extends IService<InterviewSession> {
* @return 包含最终报告的面试会话信息 * @return 包含最终报告的面试会话信息
*/ */
InterviewSession endInterview(String sessionId); InterviewSession endInterview(String sessionId);
/**
* 获取面试报告
* @param sessionId
* @return
*/
InterviewReportResponse getInterviewReport(String sessionId);
} }

View File

@@ -4,8 +4,9 @@ import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.dashscope.common.Message; import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role; import com.alibaba.dashscope.common.Role;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.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.common.utils.AIUtils;
import com.qingqiu.interview.dto.ChatDTO; import com.qingqiu.interview.dto.ChatDTO;
import com.qingqiu.interview.dto.InterviewStartRequest; import com.qingqiu.interview.dto.InterviewStartRequest;
@@ -41,9 +42,10 @@ public class ChatServiceImpl implements ChatService {
private final AIClientManager aiClientManager; private final AIClientManager aiClientManager;
private IAiSessionLogService aiSessionLogService; private final IAiSessionLogService aiSessionLogService;
@Override @Override
@AiChatLog
public ChatVO createChat(ChatDTO dto) { public ChatVO createChat(ChatDTO dto) {
LLMProvider llmProvider = LLMProvider.fromCode(dto.getAiModel()); LLMProvider llmProvider = LLMProvider.fromCode(dto.getAiModel());
List<Message> messages = new ArrayList<>(); List<Message> messages = new ArrayList<>();
@@ -57,16 +59,13 @@ public class ChatServiceImpl implements ChatService {
.orderByAsc(AiSessionLog::getCreatedTime) .orderByAsc(AiSessionLog::getCreatedTime)
); );
if (CollectionUtil.isNotEmpty(list)) { if (CollectionUtil.isNotEmpty(list)) {
messages = list.stream().map(data -> { messages.addAll(list.stream().map(data -> {
tokens.getAndAdd(AIUtils.getPromptTokens(data.getContent())); tokens.getAndAdd(AIUtils.getPromptTokens(data.getContent()));
return AIUtils.createMessage(data.getRole(), 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())); messages.add(AIUtils.createMessage(dto.getRole(), dto.getContent()));
List<Message> finalMessage = new ArrayList<>(); List<Message> finalMessage = new ArrayList<>();
// 剪切 10%的消息 // 剪切 10%的消息

View File

@@ -43,7 +43,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
ChatDTO chatDTO = new ChatDTO() ChatDTO chatDTO = new ChatDTO()
.setContent(prompt) .setContent(prompt)
.setRole(Role.SYSTEM.name()) .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE); .setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO); ChatVO chatVO = chatService.createChat(chatDTO);
@@ -67,7 +67,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
ChatDTO chatDTO = new ChatDTO() ChatDTO chatDTO = new ChatDTO()
.setSessionId(sessionId) .setSessionId(sessionId)
.setContent(prompt) .setContent(prompt)
.setRole(Role.SYSTEM.name()) .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE); .setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO); ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent()); return JSON.parseObject(chatVO.getContent());
@@ -99,7 +99,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
ChatDTO chatDTO = new ChatDTO() ChatDTO chatDTO = new ChatDTO()
.setSessionId(sessionId) .setSessionId(sessionId)
.setContent(prompt) .setContent(prompt)
.setRole(Role.SYSTEM.name()) .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE); .setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO); ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent()); return JSON.parseObject(chatVO.getContent());
@@ -114,10 +114,11 @@ public class InterviewAiServiceImpl implements InterviewAiService {
String prompt = "你是一位资深的技术面试官,以严格和深入著称。" + String prompt = "你是一位资深的技术面试官,以严格和深入著称。" +
"你需要评估候选人对以下问题的回答。请注意:\n" + "你需要评估候选人对以下问题的回答。请注意:\n" +
"1. 如果回答模糊、不完整或有错误,你必须提出一个具体的追问问题followUpQuestion来深入考察此时'continueAsking'应为true。\n" + "1. 如果回答模糊、不完整或有错误,你可以提出一个具体的追问问题followUpQuestion来深入考察此时'continueAsking'应为true。\n" +
"2. 如果回答得很好,则'continueAsking'为false'followUpQuestion'为空字符串。\n" + "2. 如果回答得很好,则'continueAsking'为false'followUpQuestion'为空字符串。\n" +
"3. 'score'范围为0-100分。\n" + "3. 'score'范围为0-100分。\n" +
"4. 'feedback'和'suggestions'需要给出专业、有建设性的意见。\n" + "4. 'feedback'和'suggestions'需要给出专业、有建设性的意见。\n" +
"5. 追问最好有限制不要无限制的向下追问注意追问是支线而非主线追问至多3个问题之后必须切回主线\n" +
"请严格按照以下JSON格式返回不要有任何额外说明\n" + "请严格按照以下JSON格式返回不要有任何额外说明\n" +
"{\"feedback\": \"...\", \"suggestions\": \"...\", \"aiAnswer\": \"...\", \"score\": 85.5, \"continueAsking\": false, \"followUpQuestion\": \"...\"}\n\n" + "{\"feedback\": \"...\", \"suggestions\": \"...\", \"aiAnswer\": \"...\", \"score\": 85.5, \"continueAsking\": false, \"followUpQuestion\": \"...\"}\n\n" +
"面试历史上下文:\n" + history + "\n\n" + "面试历史上下文:\n" + history + "\n\n" +
@@ -125,8 +126,9 @@ public class InterviewAiServiceImpl implements InterviewAiService {
"候选人回答:\n" + userAnswer; "候选人回答:\n" + userAnswer;
ChatDTO chatDTO = new ChatDTO() ChatDTO chatDTO = new ChatDTO()
.setSessionId(sessionId)
.setContent(prompt) .setContent(prompt)
.setRole(Role.SYSTEM.name()) .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE); .setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO); ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent()); return JSON.parseObject(chatVO.getContent());
@@ -134,24 +136,92 @@ public class InterviewAiServiceImpl implements InterviewAiService {
@Override @Override
public JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList) { public JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList) {
String transcript = progressList.stream() // String transcript = progressList.stream()
.map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n", // .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n",
p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback())) // p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback()))
.collect(Collectors.joining("\n-----------------\n")); // .collect(Collectors.joining("\n-----------------\n"));
String prompt = "你是一位经验丰富的招聘经理。" + // String prompt = "你是一位经验丰富的招聘经理。" +
"请根据以下完整的面试记录,为候选人生成一份综合评估报告。" + // "请根据以下完整的面试记录,为候选人生成一份综合评估报告。" +
"报告需要包括一个总分overallScore简明扼要的总结summary以及候选人的优点strengths和待提升点weaknesses" + // "报告需要包括一个总分overallScore简明扼要的总结summary以及候选人的优点strengths和待提升点weaknesses" +
"请严格按照以下JSON格式返回\n" + // "请严格按照以下JSON格式返回\n" +
"{\"overallScore\": 88.0, \"summary\": \"...\", \"strengths\": [\"...\"], \"weaknesses\": [\"...\"]}\n\n" + // "{\"overallScore\": 88.0, \"summary\": \"...\", \"strengths\": [\"...\"], \"weaknesses\": [\"...\"]}\n\n" +
"候选人姓名:" + session.getCandidateName() + "\n" + // "候选人姓名:" + session.getCandidateName() + "\n" +
"面试完整记录:\n" + transcript; // "面试完整记录:\n" + transcript;
String prompt = buildFinalReportPrompt(session, progressList);
ChatDTO chatDTO = new ChatDTO() ChatDTO chatDTO = new ChatDTO()
.setRole(Role.SYSTEM.name()) .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE) .setDataType(CommonConstant.ONE)
.setContent(prompt); .setContent(prompt);
ChatVO chatVO = chatService.createChat(chatDTO); ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent()); return JSON.parseObject(chatVO.getContent());
} }
private String buildFinalReportPrompt(InterviewSession session, List<InterviewQuestionProgress> 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();
}
} }

View File

@@ -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;
/**
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/21 12:00
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class InterviewMessageServiceImpl extends ServiceImpl<InterviewMessageMapper, InterviewMessage> implements InterviewMessageService {
}

View File

@@ -9,8 +9,10 @@ import com.qingqiu.interview.mapper.InterviewQuestionProgressMapper;
import com.qingqiu.interview.service.IInterviewQuestionProgressService; import com.qingqiu.interview.service.IInterviewQuestionProgressService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
/** /**
* <p> * <p>
@@ -40,4 +42,37 @@ public class InterviewQuestionProgressServiceImpl extends ServiceImpl<InterviewQ
.orderByDesc(InterviewQuestionProgress::getCreatedTime) .orderByDesc(InterviewQuestionProgress::getCreatedTime)
); );
} }
@Override
@Transactional(rollbackFor = Exception.class)
public InterviewQuestionProgress getNextQuestion(String sessionId) {
// 查找状态为“进行中”的问题
InterviewQuestionProgress activeQuestion = baseMapper.selectOne(
new LambdaQueryWrapper<InterviewQuestionProgress>()
.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<InterviewQuestionProgress> 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;
}
} }

View File

@@ -7,15 +7,15 @@ import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qingqiu.interview.common.enums.DocumentParserProvider; 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.InterviewStartRequest;
import com.qingqiu.interview.dto.SubmitAnswerDTO; import com.qingqiu.interview.dto.SubmitAnswerDTO;
import com.qingqiu.interview.entity.InterviewEvaluation; import com.qingqiu.interview.entity.*;
import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession;
import com.qingqiu.interview.entity.Question;
import com.qingqiu.interview.mapper.InterviewEvaluationMapper; 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.mapper.InterviewSessionMapper;
import com.qingqiu.interview.service.IInterviewQuestionProgressService;
import com.qingqiu.interview.service.InterviewAiService; import com.qingqiu.interview.service.InterviewAiService;
import com.qingqiu.interview.service.InterviewService; import com.qingqiu.interview.service.InterviewService;
import com.qingqiu.interview.service.QuestionService; import com.qingqiu.interview.service.QuestionService;
@@ -24,6 +24,7 @@ import com.qingqiu.interview.service.parser.DocumentParserManager;
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO; import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -33,7 +34,9 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
/** /**
* <h1></h1> * <h1></h1>
@@ -48,10 +51,12 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
private final QuestionService questionService; private final QuestionService questionService;
private final InterviewQuestionProgressMapper progressMapper; private final IInterviewQuestionProgressService progressService;
private final InterviewEvaluationMapper evaluationMapper; private final InterviewEvaluationMapper evaluationMapper;
private final InterviewMessageMapper messageMapper;
private final InterviewAiService aiService; private final InterviewAiService aiService;
private final DocumentParserManager documentParserManager; private final DocumentParserManager documentParserManager;
@@ -70,6 +75,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
session.setAiModel(dto.getAiModel()); session.setAiModel(dto.getAiModel());
session.setStatus(InterviewSession.Status.ACTIVE.name()); session.setStatus(InterviewSession.Status.ACTIVE.name());
session.setTotalQuestions(dto.getTotalQuestions()); session.setTotalQuestions(dto.getTotalQuestions());
session.setModel(dto.getModel());
this.baseMapper.insert(session); // 先插入以获取ID this.baseMapper.insert(session); // 先插入以获取ID
// 2. 调用AI服务从简历提取技能 // 2. 调用AI服务从简历提取技能
@@ -87,6 +93,14 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
// 4. 更新会话信息 // 4. 更新会话信息
this.baseMapper.updateById(session); this.baseMapper.updateById(session);
InterviewQuestionProgress nextQuestion = progressService.getNextQuestion(sessionId);
aiService.generateFirstQuestion(session.getSessionId(), session.getCandidateName(), nextQuestion.getQuestionContent());
saveMessage(sessionId,
InterviewMessage.MessageType.QUESTION.name(),
InterviewMessage.Sender.AI.name(),
nextQuestion.getQuestionContent(),
nextQuestion.getId()
);
return session; return session;
} }
@@ -115,7 +129,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
} }
// 批量保存问题进度 // 批量保存问题进度
if (CollectionUtil.isNotEmpty(progressList)) { if (CollectionUtil.isNotEmpty(progressList)) {
progressList.forEach(progressMapper::insert); progressList.forEach(progressService::save);
} }
} }
@@ -169,7 +183,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
}); });
// 批量保存问题进度 // 批量保存问题进度
if (CollectionUtil.isNotEmpty(progressList)) { if (CollectionUtil.isNotEmpty(progressList)) {
progressList.forEach(progressMapper::insert); progressList.forEach(progressService::save);
} }
} }
@@ -177,39 +191,66 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public InterviewQuestionProgress getNextQuestion(String sessionId) { public InterviewMessage getNextQuestion(String sessionId, Long progressId) {
// 1. 查找第一个处于“默认”状态的问题
LambdaQueryWrapper<InterviewQuestionProgress> 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);
if (nextQuestion == null) { // 获取下一个问题
// 没有更多的问题了 InterviewQuestionProgress nextQuestion = progressService.getNextQuestion(sessionId);
if (Objects.isNull(nextQuestion)) {
return null; return null;
} }
// 判断是否在interview_message中存在
InterviewMessage interviewMessage = messageMapper.selectOne(
new LambdaQueryWrapper<InterviewMessage>()
.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()); StringBuilder sb = new StringBuilder();
progressMapper.updateById(nextQuestion); 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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public InterviewQuestionProgress submitAnswer(SubmitAnswerDTO dto) { public InterviewQuestionProgress submitAnswer(SubmitAnswerDTO dto) {
// 1. 查询当前正在进行的这个问题 // 1. 查询当前正在进行的这个问题
InterviewQuestionProgress currentProgress = progressMapper.selectById(dto.getProgressId()); InterviewQuestionProgress currentProgress = progressService.getById(dto.getProgressId());
if (currentProgress == null || !InterviewQuestionProgress.Status.ACTIVE.name().equals(currentProgress.getStatus())) { if (Objects.isNull(currentProgress) || !InterviewQuestionProgress.Status.ACTIVE.name().equals(currentProgress.getStatus())) {
throw new RuntimeException("问题进度不存在或已处理"); throw new ApiException("问题进度不存在或已处理");
} }
currentProgress.setUserAnswer(dto.getAnswer()); currentProgress.setUserAnswer(dto.getAnswer());
// 存储消息
saveMessage(dto.getSessionId(),
InterviewMessage.MessageType.ANSWER.name(),
InterviewMessage.Sender.USER.name(),
dto.getAnswer(),
currentProgress.getId()
);
// 2. 调用AI服务评估回答 // 2. 调用AI服务评估回答
List<InterviewQuestionProgress> context = progressMapper.selectList( List<InterviewQuestionProgress> context = progressService.list(
new LambdaQueryWrapper<InterviewQuestionProgress>() new LambdaQueryWrapper<InterviewQuestionProgress>()
.eq(InterviewQuestionProgress::getSessionId, currentProgress.getSessionId()) .eq(InterviewQuestionProgress::getSessionId, currentProgress.getSessionId())
.eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name()) .eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name())
@@ -228,11 +269,12 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
currentProgress.setAiAnswer(evalResult.getString("aiAnswer")); currentProgress.setAiAnswer(evalResult.getString("aiAnswer"));
currentProgress.setScore(evalResult.getBigDecimal("score")); currentProgress.setScore(evalResult.getBigDecimal("score"));
currentProgress.setStatus(InterviewQuestionProgress.Status.COMPLETED.name()); currentProgress.setStatus(InterviewQuestionProgress.Status.COMPLETED.name());
progressMapper.updateById(currentProgress); progressService.updateById(currentProgress);
// 4. 将单题评估结果存入 evaluation 表用于分析 // 4. 将单题评估结果存入 evaluation 表用于分析
saveEvaluationRecord(currentProgress, evalResult); saveEvaluationRecord(currentProgress, evalResult);
// 5. ---> 解析AI的是否追问判断并处理追问逻辑 <--- // 5. ---> 解析AI的是否追问判断并处理追问逻辑 <---
if (evalResult.getBooleanValue("continueAsking", false)) { if (evalResult.getBooleanValue("continueAsking", false)) {
// 创建一个新的、状态为ACTIVE的追问问题 // 创建一个新的、状态为ACTIVE的追问问题
@@ -241,7 +283,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
followUp.setQuestionId(0L); // 追问问题没有本地ID followUp.setQuestionId(0L); // 追问问题没有本地ID
followUp.setQuestionContent(evalResult.getString("followUpQuestion")); followUp.setQuestionContent(evalResult.getString("followUpQuestion"));
followUp.setStatus(InterviewQuestionProgress.Status.ACTIVE.name()); // 直接设为激活状态,作为下一个问题 followUp.setStatus(InterviewQuestionProgress.Status.ACTIVE.name()); // 直接设为激活状态,作为下一个问题
progressMapper.insert(followUp); progressService.save(followUp);
return followUp; // 将这个新的追问问题返回给前端 return followUp; // 将这个新的追问问题返回给前端
} }
@@ -265,7 +307,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
.eq(InterviewSession::getSessionId, sessionId)); .eq(InterviewSession::getSessionId, sessionId));
if (session == null) throw new RuntimeException("会话不存在"); if (session == null) throw new RuntimeException("会话不存在");
List<InterviewQuestionProgress> completedProgresses = progressMapper.selectList( List<InterviewQuestionProgress> completedProgresses = progressService.list(
new LambdaQueryWrapper<InterviewQuestionProgress>() new LambdaQueryWrapper<InterviewQuestionProgress>()
.eq(InterviewQuestionProgress::getSessionId, sessionId) .eq(InterviewQuestionProgress::getSessionId, sessionId)
.eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name()) .eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name())
@@ -289,6 +331,62 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
} }
/**
* 获取详细的面试复盘报告
*/
@Override
public InterviewReportResponse getInterviewReport(String sessionId) {
log.info("Fetching interview report for session id: {}", sessionId);
InterviewSession session = getOne(
new LambdaQueryWrapper<InterviewSession>()
.eq(InterviewSession::getSessionId, sessionId)
.last("LIMIT 1")
);
if (session == null) {
throw new IllegalArgumentException("找不到ID为 " + sessionId + " 的面试会话。");
}
List<InterviewQuestionProgress> progressList = progressService.list(
new LambdaQueryWrapper<InterviewQuestionProgress>()
.eq(InterviewQuestionProgress::getSessionId, sessionId)
.orderByAsc(InterviewQuestionProgress::getUpdatedTime)
);
List<InterviewReportResponse.QuestionDetail> 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<InterviewMessage> interviewMessages = messageMapper.selectList(
new LambdaQueryWrapper<InterviewMessage>()
.eq(InterviewMessage::getSessionId, sessionId)
);
// 获取当前面试的 问题
InterviewQuestionProgress progress = progressService.getOne(
new LambdaQueryWrapper<InterviewQuestionProgress>()
.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 { private String parseResume(MultipartFile resume) throws IOException {
// 获取文件扩展名 // 获取文件扩展名
String extName = FileNameUtil.extName(resume.getOriginalFilename()); String extName = FileNameUtil.extName(resume.getOriginalFilename());
@@ -297,4 +395,20 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
// 2. 解析简历 // 2. 解析简历
return parser.parse(resume.getInputStream()); return parser.parse(resume.getInputStream());
} }
private InterviewMessage saveMessage(String sessionId, String messageType, String sender,
String content, Long questionId) {
int nextOrder = messageMapper.selectMaxOrderBySessionId(sessionId) + 1;
InterviewMessage message = new InterviewMessage()
.setSessionId(sessionId)
.setMessageType(messageType)
.setSender(sender)
.setContent(content)
.setQuestionProgressId(questionId)
.setMessageOrder(nextOrder);
messageMapper.insert(message);
return message;
}
} }