feat(ai): 集成DeepSeek AI优化简历功能
- 配置DeepSeek AI相关依赖和配置项 - 创建ChatController提供AI聊天测试接口 - 实现AiService和AiServiceImpl提供AI优化服务 - 添加ResumeAiOptimizeRequest和ResumeAiOptimizeVO数据传输对象 - 在ResumeController中添加AI优化简历的API端点 - 前端ResumeEdit页面集成AI优化按钮和进度显示 - 实现前端调用AI优化接口并更新表单内容 - 添加AI优化相关的样式和交互效果
This commit is contained in:
@@ -48,6 +48,7 @@
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- knife4j接口文档-->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
@@ -87,6 +88,13 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--Ai-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-deepseek</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- mybatis-plus-->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
@@ -131,6 +139,18 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
@@ -169,4 +189,15 @@
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.zds.boss.controller;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/chat")
|
||||
public class ChatController {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
private final ChatModel chatModel;
|
||||
|
||||
public ChatController(ChatClient.Builder chatClientBuilder, ChatModel chatModel) {
|
||||
this.chatClient = chatClientBuilder.build();
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
@GetMapping("/test")
|
||||
public String completion(@RequestParam String message) {
|
||||
try {
|
||||
return chatClient.prompt()
|
||||
.user(message)
|
||||
.call()
|
||||
.content();
|
||||
} catch (Exception e) {
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/model")
|
||||
public String model(@RequestParam String message) {
|
||||
try {
|
||||
Prompt prompt = new Prompt(message);
|
||||
ChatResponse response = chatModel.call(prompt);
|
||||
return response.getResult().getOutput().getText();
|
||||
} catch (Exception e) {
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,15 @@ import com.zds.boss.exception.BusinessException;
|
||||
import com.zds.boss.exception.ErrorCode;
|
||||
import com.zds.boss.exception.ThrowUtils;
|
||||
import com.zds.boss.model.dto.resume.ResumeAddRequest;
|
||||
import com.zds.boss.model.dto.resume.ResumeAiOptimizeRequest;
|
||||
import com.zds.boss.model.dto.resume.ResumeQueryRequest;
|
||||
import com.zds.boss.model.dto.resume.ResumeUpdateRequest;
|
||||
import com.zds.boss.model.entity.Resume;
|
||||
import com.zds.boss.model.entity.User;
|
||||
import com.zds.boss.model.enums.UserRoleEnum;
|
||||
import com.zds.boss.model.vo.ResumeAiOptimizeVO;
|
||||
import com.zds.boss.model.vo.ResumeVO;
|
||||
import com.zds.boss.service.AiService;
|
||||
import com.zds.boss.service.CosService;
|
||||
import com.zds.boss.service.FileService;
|
||||
import com.zds.boss.service.ResumeAddressService;
|
||||
@@ -51,6 +54,9 @@ public class ResumeController {
|
||||
@Resource
|
||||
private ResumeAddressService resumeAddressService;
|
||||
|
||||
@Resource
|
||||
private AiService aiService;
|
||||
|
||||
@Value("${file.max-size-mb:10}")
|
||||
private long maxSizeMb;
|
||||
|
||||
@@ -303,4 +309,37 @@ public class ResumeController {
|
||||
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI优化简历
|
||||
*
|
||||
* @param request AI优化请求
|
||||
* @param httpRequest HTTP请求
|
||||
* @return 优化后的简历内容
|
||||
*/
|
||||
@PostMapping("/ai/optimize")
|
||||
public BaseResponse<ResumeAiOptimizeVO> aiOptimizeResume(
|
||||
@RequestBody ResumeAiOptimizeRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
if (request == null) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
|
||||
}
|
||||
|
||||
// 验证用户登录
|
||||
userService.getLoginUser(httpRequest);
|
||||
|
||||
// 检查是否有内容需要优化
|
||||
boolean hasContent = (request.getResumeTitle() != null && !request.getResumeTitle().trim().isEmpty())
|
||||
|| (request.getSummary() != null && !request.getSummary().trim().isEmpty())
|
||||
|| (request.getContent() != null && !request.getContent().trim().isEmpty());
|
||||
|
||||
if (!hasContent) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "请至少填写一项内容后再进行AI优化");
|
||||
}
|
||||
|
||||
// 调用AI服务优化简历
|
||||
ResumeAiOptimizeVO result = aiService.optimizeResume(request);
|
||||
|
||||
return ResultUtils.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.zds.boss.model.dto.resume;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* AI简历优化请求
|
||||
*/
|
||||
@Data
|
||||
public class ResumeAiOptimizeRequest implements Serializable {
|
||||
|
||||
/**
|
||||
* 简历标题
|
||||
*/
|
||||
private String resumeTitle;
|
||||
|
||||
/**
|
||||
* 个人摘要
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 详细内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.zds.boss.model.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* AI简历优化响应
|
||||
*/
|
||||
@Data
|
||||
public class ResumeAiOptimizeVO implements Serializable {
|
||||
|
||||
/**
|
||||
* 优化后的简历标题
|
||||
*/
|
||||
private String resumeTitle;
|
||||
|
||||
/**
|
||||
* 优化后的个人摘要
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 优化后的详细内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.zds.boss.service;
|
||||
|
||||
import com.zds.boss.model.dto.resume.ResumeAiOptimizeRequest;
|
||||
import com.zds.boss.model.vo.ResumeAiOptimizeVO;
|
||||
|
||||
/**
|
||||
* AI服务接口
|
||||
*/
|
||||
public interface AiService {
|
||||
|
||||
/**
|
||||
* 使用AI优化简历
|
||||
*
|
||||
* @param request 简历优化请求
|
||||
* @return 优化后的简历内容
|
||||
*/
|
||||
ResumeAiOptimizeVO optimizeResume(ResumeAiOptimizeRequest request);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.zds.boss.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zds.boss.exception.BusinessException;
|
||||
import com.zds.boss.exception.ErrorCode;
|
||||
import com.zds.boss.model.dto.resume.ResumeAiOptimizeRequest;
|
||||
import com.zds.boss.model.vo.ResumeAiOptimizeVO;
|
||||
import com.zds.boss.service.AiService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* AI服务实现类
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiServiceImpl implements AiService {
|
||||
|
||||
@Resource
|
||||
private DeepSeekChatModel deepSeekChatModel;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 系统提示词 - 专业简历分析师
|
||||
*/
|
||||
private static final String SYSTEM_PROMPT = """
|
||||
你是一位专业的简历分析师和职业规划顾问,拥有10年以上的人力资源和招聘经验。
|
||||
你的任务是帮助用户优化他们的简历,使其更加专业、吸引人,并能更好地展示他们的技能和经验。
|
||||
|
||||
优化原则:
|
||||
1. 简历标题:简洁有力,突出核心职位和关键技能,格式如"职位名称-姓名"或"职位名称 | 核心优势"
|
||||
2. 个人摘要:控制在100-200字,突出核心竞争力、关键成就和职业目标,使用有力的动词开头
|
||||
3. 详细内容:使用STAR法则(情境-任务-行动-结果)描述经历,量化成就,突出与目标职位相关的技能
|
||||
|
||||
优化要求:
|
||||
- 语言简洁专业,避免口语化表达
|
||||
- 突出数据和成果,使用具体数字量化成就
|
||||
- 使用行业关键词,提高ATS(简历筛选系统)通过率
|
||||
- 保持内容真实,在原有基础上进行润色和优化
|
||||
- 如果原内容较少,可以适当扩展,但要基于原有信息合理推断
|
||||
|
||||
你必须严格按照以下JSON格式返回结果,不要包含任何其他内容:
|
||||
{
|
||||
"resumeTitle": "优化后的简历标题",
|
||||
"summary": "优化后的个人摘要",
|
||||
"content": "优化后的详细内容"
|
||||
}
|
||||
""";
|
||||
|
||||
@Override
|
||||
public ResumeAiOptimizeVO optimizeResume(ResumeAiOptimizeRequest request) {
|
||||
log.info("开始AI简历优化,标题: {}", request.getResumeTitle());
|
||||
|
||||
// 构建用户消息
|
||||
String userMessage = String.format("""
|
||||
请优化以下简历内容:
|
||||
|
||||
【简历标题】
|
||||
%s
|
||||
|
||||
【个人摘要】
|
||||
%s
|
||||
|
||||
【详细内容】
|
||||
%s
|
||||
|
||||
请按照JSON格式返回优化后的结果。
|
||||
""",
|
||||
nullToEmpty(request.getResumeTitle()),
|
||||
nullToEmpty(request.getSummary()),
|
||||
nullToEmpty(request.getContent())
|
||||
);
|
||||
|
||||
try {
|
||||
// 调用DeepSeek API
|
||||
ChatClient chatClient = ChatClient.builder(deepSeekChatModel).build();
|
||||
String response = chatClient.prompt()
|
||||
.system(SYSTEM_PROMPT)
|
||||
.user(userMessage)
|
||||
.call()
|
||||
.content();
|
||||
|
||||
log.info("AI返回原始响应: {}", response);
|
||||
|
||||
// 解析JSON响应
|
||||
ResumeAiOptimizeVO result = parseResponse(response);
|
||||
log.info("AI简历优化完成");
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("AI简历优化失败: {}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI优化失败,请稍后重试: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析AI响应的JSON
|
||||
*/
|
||||
private ResumeAiOptimizeVO parseResponse(String response) {
|
||||
try {
|
||||
// 尝试提取JSON部分(处理可能的markdown代码块)
|
||||
String jsonStr = extractJson(response);
|
||||
|
||||
JsonNode jsonNode = objectMapper.readTree(jsonStr);
|
||||
|
||||
ResumeAiOptimizeVO vo = new ResumeAiOptimizeVO();
|
||||
vo.setResumeTitle(getTextValue(jsonNode, "resumeTitle"));
|
||||
vo.setSummary(getTextValue(jsonNode, "summary"));
|
||||
vo.setContent(getTextValue(jsonNode, "content"));
|
||||
|
||||
return vo;
|
||||
} catch (Exception e) {
|
||||
log.error("解析AI响应失败: {}", e.getMessage());
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI响应解析失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中提取JSON字符串
|
||||
*/
|
||||
private String extractJson(String response) {
|
||||
if (response == null || response.isEmpty()) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI响应为空");
|
||||
}
|
||||
|
||||
// 处理markdown代码块
|
||||
if (response.contains("```json")) {
|
||||
int start = response.indexOf("```json") + 7;
|
||||
int end = response.lastIndexOf("```");
|
||||
if (end > start) {
|
||||
return response.substring(start, end).trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (response.contains("```")) {
|
||||
int start = response.indexOf("```") + 3;
|
||||
int end = response.lastIndexOf("```");
|
||||
if (end > start) {
|
||||
return response.substring(start, end).trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试找到JSON对象
|
||||
int start = response.indexOf("{");
|
||||
int end = response.lastIndexOf("}");
|
||||
if (start >= 0 && end > start) {
|
||||
return response.substring(start, end + 1);
|
||||
}
|
||||
|
||||
return response.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取JSON节点的文本值
|
||||
*/
|
||||
private String getTextValue(JsonNode node, String field) {
|
||||
if (node.has(field) && !node.get(field).isNull()) {
|
||||
return node.get(field).asText();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将null转换为空字符串
|
||||
*/
|
||||
private String nullToEmpty(String str) {
|
||||
return str == null ? "" : str;
|
||||
}
|
||||
}
|
||||
1
BOSSBackEnd/src/main/resources/application.properties
Normal file
1
BOSSBackEnd/src/main/resources/application.properties
Normal file
@@ -0,0 +1 @@
|
||||
spring.profiles.active=dev
|
||||
@@ -19,6 +19,14 @@ spring:
|
||||
application:
|
||||
name: BOSS
|
||||
|
||||
ai:
|
||||
deepseek:
|
||||
api-key:
|
||||
base-url: https://api.deepseek.com
|
||||
chat:
|
||||
options:
|
||||
model: deepseek-chat
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://127.0.0.1:3306/boss?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
@@ -34,7 +42,7 @@ spring:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
database: 0
|
||||
password: "" # ?????????
|
||||
password: ""
|
||||
timeout: 5s
|
||||
lettuce:
|
||||
pool:
|
||||
|
||||
Reference in New Issue
Block a user