优化代码
This commit is contained in:
@@ -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;
|
||||
|
||||
/**
|
||||
* <h1>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 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<AiSessionLog>> list(@PathVariable String sessionId) {
|
||||
return R.success(service.list(
|
||||
new LambdaQueryWrapper<AiSessionLog>()
|
||||
.eq(AiSessionLog::getToken, sessionId)
|
||||
.ne(AiSessionLog::getRole, Role.SYSTEM.getValue())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <h1></h1>
|
||||
*
|
||||
@@ -37,15 +38,15 @@ public class InterviewController {
|
||||
@PostMapping("/start")
|
||||
public R<InterviewSession> 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<InterviewQuestionProgress> getNextQuestion(@PathVariable String sessionId) {
|
||||
@GetMapping("/next-question/{sessionId}/{progressId}")
|
||||
public R<InterviewMessage> 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<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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ public class SubmitAnswerDTO implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 当前问题的进度ID (interview_question_progress.id)
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,4 +15,6 @@ import com.qingqiu.interview.entity.InterviewQuestionProgress;
|
||||
*/
|
||||
public interface IInterviewQuestionProgressService extends IService<InterviewQuestionProgress> {
|
||||
Page<InterviewQuestionProgress> pageList(QuestionProgressPageParams params);
|
||||
|
||||
InterviewQuestionProgress getNextQuestion(String sessionId);
|
||||
}
|
||||
|
||||
@@ -55,4 +55,7 @@ public interface InterviewAiService {
|
||||
* @return 包含最终报告的JSON对象
|
||||
*/
|
||||
JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList);
|
||||
|
||||
String generateFirstQuestion(String sessionId, String candidateName, String questionContent);
|
||||
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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<InterviewSession> {
|
||||
* @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<InterviewSession> {
|
||||
* @return 包含最终报告的面试会话信息
|
||||
*/
|
||||
InterviewSession endInterview(String sessionId);
|
||||
|
||||
/**
|
||||
* 获取面试报告
|
||||
* @param sessionId
|
||||
* @return
|
||||
*/
|
||||
InterviewReportResponse getInterviewReport(String sessionId);
|
||||
}
|
||||
|
||||
@@ -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<Message> 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<Message> finalMessage = new ArrayList<>();
|
||||
// 剪切 10%的消息
|
||||
|
||||
@@ -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<InterviewQuestionProgress> 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<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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -40,4 +42,37 @@ public class InterviewQuestionProgressServiceImpl extends ServiceImpl<InterviewQ
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <h1></h1>
|
||||
@@ -48,10 +51,12 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
|
||||
private final QuestionService questionService;
|
||||
|
||||
private final InterviewQuestionProgressMapper progressMapper;
|
||||
private final IInterviewQuestionProgressService progressService;
|
||||
|
||||
private final InterviewEvaluationMapper evaluationMapper;
|
||||
|
||||
private final InterviewMessageMapper messageMapper;
|
||||
|
||||
private final InterviewAiService aiService;
|
||||
|
||||
private final DocumentParserManager documentParserManager;
|
||||
@@ -70,6 +75,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
session.setAiModel(dto.getAiModel());
|
||||
session.setStatus(InterviewSession.Status.ACTIVE.name());
|
||||
session.setTotalQuestions(dto.getTotalQuestions());
|
||||
session.setModel(dto.getModel());
|
||||
this.baseMapper.insert(session); // 先插入以获取ID
|
||||
|
||||
// 2. 调用AI服务从简历提取技能
|
||||
@@ -87,6 +93,14 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
|
||||
// 4. 更新会话信息
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -115,7 +129,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
}
|
||||
// 批量保存问题进度
|
||||
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)) {
|
||||
progressList.forEach(progressMapper::insert);
|
||||
progressList.forEach(progressService::save);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -177,39 +191,66 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public InterviewQuestionProgress getNextQuestion(String sessionId) {
|
||||
// 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);
|
||||
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<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());
|
||||
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<InterviewQuestionProgress> context = progressMapper.selectList(
|
||||
List<InterviewQuestionProgress> context = progressService.list(
|
||||
new LambdaQueryWrapper<InterviewQuestionProgress>()
|
||||
.eq(InterviewQuestionProgress::getSessionId, currentProgress.getSessionId())
|
||||
.eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name())
|
||||
@@ -228,11 +269,12 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
currentProgress.setAiAnswer(evalResult.getString("aiAnswer"));
|
||||
currentProgress.setScore(evalResult.getBigDecimal("score"));
|
||||
currentProgress.setStatus(InterviewQuestionProgress.Status.COMPLETED.name());
|
||||
progressMapper.updateById(currentProgress);
|
||||
progressService.updateById(currentProgress);
|
||||
|
||||
// 4. 将单题评估结果存入 evaluation 表用于分析
|
||||
saveEvaluationRecord(currentProgress, evalResult);
|
||||
|
||||
|
||||
// 5. ---> 解析AI的是否追问判断,并处理追问逻辑 <---
|
||||
if (evalResult.getBooleanValue("continueAsking", false)) {
|
||||
// 创建一个新的、状态为ACTIVE的追问问题
|
||||
@@ -241,7 +283,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
followUp.setQuestionId(0L); // 追问问题没有本地ID
|
||||
followUp.setQuestionContent(evalResult.getString("followUpQuestion"));
|
||||
followUp.setStatus(InterviewQuestionProgress.Status.ACTIVE.name()); // 直接设为激活状态,作为下一个问题
|
||||
progressMapper.insert(followUp);
|
||||
progressService.save(followUp);
|
||||
return followUp; // 将这个新的追问问题返回给前端
|
||||
}
|
||||
|
||||
@@ -265,7 +307,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
.eq(InterviewSession::getSessionId, sessionId));
|
||||
if (session == null) throw new RuntimeException("会话不存在");
|
||||
|
||||
List<InterviewQuestionProgress> completedProgresses = progressMapper.selectList(
|
||||
List<InterviewQuestionProgress> completedProgresses = progressService.list(
|
||||
new LambdaQueryWrapper<InterviewQuestionProgress>()
|
||||
.eq(InterviewQuestionProgress::getSessionId, sessionId)
|
||||
.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 {
|
||||
// 获取文件扩展名
|
||||
String extName = FileNameUtil.extName(resume.getOriginalFilename());
|
||||
@@ -297,4 +395,20 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
||||
// 2. 解析简历
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user