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>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- knife4j接口文档-->
|
<!-- knife4j接口文档-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
@@ -87,6 +88,13 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-json</artifactId>
|
<artifactId>spring-boot-starter-json</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!--Ai-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-starter-model-deepseek</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- mybatis-plus-->
|
<!-- mybatis-plus-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
@@ -131,6 +139,18 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
@@ -169,4 +189,15 @@
|
|||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</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>
|
</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.ErrorCode;
|
||||||
import com.zds.boss.exception.ThrowUtils;
|
import com.zds.boss.exception.ThrowUtils;
|
||||||
import com.zds.boss.model.dto.resume.ResumeAddRequest;
|
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.ResumeQueryRequest;
|
||||||
import com.zds.boss.model.dto.resume.ResumeUpdateRequest;
|
import com.zds.boss.model.dto.resume.ResumeUpdateRequest;
|
||||||
import com.zds.boss.model.entity.Resume;
|
import com.zds.boss.model.entity.Resume;
|
||||||
import com.zds.boss.model.entity.User;
|
import com.zds.boss.model.entity.User;
|
||||||
import com.zds.boss.model.enums.UserRoleEnum;
|
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.model.vo.ResumeVO;
|
||||||
|
import com.zds.boss.service.AiService;
|
||||||
import com.zds.boss.service.CosService;
|
import com.zds.boss.service.CosService;
|
||||||
import com.zds.boss.service.FileService;
|
import com.zds.boss.service.FileService;
|
||||||
import com.zds.boss.service.ResumeAddressService;
|
import com.zds.boss.service.ResumeAddressService;
|
||||||
@@ -51,6 +54,9 @@ public class ResumeController {
|
|||||||
@Resource
|
@Resource
|
||||||
private ResumeAddressService resumeAddressService;
|
private ResumeAddressService resumeAddressService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AiService aiService;
|
||||||
|
|
||||||
@Value("${file.max-size-mb:10}")
|
@Value("${file.max-size-mb:10}")
|
||||||
private long maxSizeMb;
|
private long maxSizeMb;
|
||||||
|
|
||||||
@@ -303,4 +309,37 @@ public class ResumeController {
|
|||||||
|
|
||||||
return ResultUtils.success(result);
|
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:
|
application:
|
||||||
name: BOSS
|
name: BOSS
|
||||||
|
|
||||||
|
ai:
|
||||||
|
deepseek:
|
||||||
|
api-key:
|
||||||
|
base-url: https://api.deepseek.com
|
||||||
|
chat:
|
||||||
|
options:
|
||||||
|
model: deepseek-chat
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
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
|
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
|
host: 127.0.0.1
|
||||||
port: 6379
|
port: 6379
|
||||||
database: 0
|
database: 0
|
||||||
password: "" # ?????????
|
password: ""
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
lettuce:
|
lettuce:
|
||||||
pool:
|
pool:
|
||||||
|
|||||||
@@ -96,3 +96,32 @@ export async function deleteResumeFile(
|
|||||||
...(options || {}),
|
...(options || {}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** AI优化简历请求 */
|
||||||
|
export interface ResumeAiOptimizeRequest {
|
||||||
|
resumeTitle?: string
|
||||||
|
summary?: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AI优化简历响应 */
|
||||||
|
export interface ResumeAiOptimizeVO {
|
||||||
|
resumeTitle?: string
|
||||||
|
summary?: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AI优化简历 POST /resume/ai/optimize */
|
||||||
|
export async function aiOptimizeResume(
|
||||||
|
body: ResumeAiOptimizeRequest,
|
||||||
|
options?: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
return request<API.BaseResponse<ResumeAiOptimizeVO>>('/resume/ai/optimize', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,26 +35,6 @@
|
|||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<!-- <a-card title="我的简历PDF" :bordered="false" style="margin-top: 16px">-->
|
|
||||||
<!-- <div v-if="addressLoading">-->
|
|
||||||
<!-- <a-spin />-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div v-else>-->
|
|
||||||
<!-- <div v-if="myAddress?.address">-->
|
|
||||||
<!-- <a-space>-->
|
|
||||||
<!-- <a :href="myAddress.address" target="_blank">查看PDF</a>-->
|
|
||||||
<!-- <a-popconfirm title="确定删除该PDF吗?" @confirm="deleteAddress">-->
|
|
||||||
<!-- <a-button danger>删除</a-button>-->
|
|
||||||
<!-- </a-popconfirm>-->
|
|
||||||
<!-- </a-space>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div v-else>-->
|
|
||||||
<!-- <a-upload :before-upload="beforeUpload" :show-upload-list="false" :custom-request="(opt:any)=>doUpload(opt.file)">-->
|
|
||||||
<!-- <a-button type="primary">上传PDF</a-button>-->
|
|
||||||
<!-- </a-upload>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </a-card>-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,37 @@
|
|||||||
<a-switch v-model:checked="isDefaultChecked" />
|
<a-switch v-model:checked="isDefaultChecked" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- AI优化区域 -->
|
||||||
|
<a-form-item label="AI优化" :wrapper-col="{ span: 16 }">
|
||||||
|
<a-space direction="vertical" style="width: 100%">
|
||||||
|
<a-button
|
||||||
|
type="default"
|
||||||
|
:loading="aiOptimizing"
|
||||||
|
@click="handleAiOptimize"
|
||||||
|
:disabled="aiOptimizing"
|
||||||
|
class="ai-optimize-btn"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<RobotOutlined />
|
||||||
|
</template>
|
||||||
|
{{ aiOptimizing ? 'AI优化中...' : 'DeepSeek AI 智能优化' }}
|
||||||
|
</a-button>
|
||||||
|
<a-progress
|
||||||
|
v-if="aiOptimizing"
|
||||||
|
:percent="Math.round(aiProgress)"
|
||||||
|
:status="aiProgress >= 100 ? 'success' : 'active'"
|
||||||
|
:stroke-color="{
|
||||||
|
'0%': '#108ee9',
|
||||||
|
'100%': '#87d068',
|
||||||
|
}"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<div v-if="!aiOptimizing" style="color: #999; font-size: 12px">
|
||||||
|
使用 DeepSeek AI 智能优化您的简历内容,让简历更专业、更有吸引力
|
||||||
|
</div>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item :wrapper-col="{ offset: 4, span: 16 }">
|
<a-form-item :wrapper-col="{ offset: 4, span: 16 }">
|
||||||
<a-button type="primary" html-type="submit" :loading="loading">保存</a-button>
|
<a-button type="primary" html-type="submit" :loading="loading">保存</a-button>
|
||||||
<a-button style="margin-left: 10px" @click="router.back()">取消</a-button>
|
<a-button style="margin-left: 10px" @click="router.back()">取消</a-button>
|
||||||
@@ -82,9 +113,9 @@
|
|||||||
import NavBar from '@/components/NavBar.vue';
|
import NavBar from '@/components/NavBar.vue';
|
||||||
import { reactive, ref, onMounted, computed } from 'vue';
|
import { reactive, ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { addResume, updateResume, getResumeVoById, deleteResumeFile } from '@/api/api/resumeController';
|
import { addResume, updateResume, getResumeVoById, deleteResumeFile, aiOptimizeResume } from '@/api/api/resumeController';
|
||||||
import { message, Upload } from 'ant-design-vue';
|
import { message, Upload } from 'ant-design-vue';
|
||||||
import { UploadOutlined, FilePdfOutlined } from '@ant-design/icons-vue';
|
import { UploadOutlined, FilePdfOutlined, RobotOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -95,6 +126,11 @@ const uploading = ref(false);
|
|||||||
const uploadProgress = ref(0);
|
const uploadProgress = ref(0);
|
||||||
const uploadFileName = ref('');
|
const uploadFileName = ref('');
|
||||||
|
|
||||||
|
// AI优化相关状态
|
||||||
|
const aiOptimizing = ref(false);
|
||||||
|
const aiProgress = ref(0);
|
||||||
|
let aiProgressTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
const formState = reactive({
|
const formState = reactive({
|
||||||
resumeTitle: '',
|
resumeTitle: '',
|
||||||
summary: '',
|
summary: '',
|
||||||
@@ -229,6 +265,86 @@ const clearAttachment = async () => {
|
|||||||
uploadFileName.value = '';
|
uploadFileName.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 开始伪进度条
|
||||||
|
const startFakeProgress = () => {
|
||||||
|
aiProgress.value = 0;
|
||||||
|
aiProgressTimer = setInterval(() => {
|
||||||
|
// 伪进度条逻辑:快速增长到30%,然后慢慢增长到90%
|
||||||
|
if (aiProgress.value < 30) {
|
||||||
|
aiProgress.value += Math.random() * 8 + 2; // 2-10%
|
||||||
|
} else if (aiProgress.value < 60) {
|
||||||
|
aiProgress.value += Math.random() * 4 + 1; // 1-5%
|
||||||
|
} else if (aiProgress.value < 90) {
|
||||||
|
aiProgress.value += Math.random() * 2 + 0.5; // 0.5-2.5%
|
||||||
|
}
|
||||||
|
// 最多到90%,等待AI返回后才变成100%
|
||||||
|
if (aiProgress.value > 90) {
|
||||||
|
aiProgress.value = 90;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 停止伪进度条
|
||||||
|
const stopFakeProgress = (success: boolean) => {
|
||||||
|
if (aiProgressTimer) {
|
||||||
|
clearInterval(aiProgressTimer);
|
||||||
|
aiProgressTimer = null;
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
aiProgress.value = 100;
|
||||||
|
// 100%后延迟重置
|
||||||
|
setTimeout(() => {
|
||||||
|
aiProgress.value = 0;
|
||||||
|
aiOptimizing.value = false;
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
aiProgress.value = 0;
|
||||||
|
aiOptimizing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// AI优化简历
|
||||||
|
const handleAiOptimize = async () => {
|
||||||
|
// 检查是否有内容可优化
|
||||||
|
if (!formState.resumeTitle?.trim() && !formState.summary?.trim() && !formState.content?.trim()) {
|
||||||
|
message.warning('请至少填写一项内容后再进行AI优化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aiOptimizing.value = true;
|
||||||
|
startFakeProgress();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await aiOptimizeResume({
|
||||||
|
resumeTitle: formState.resumeTitle,
|
||||||
|
summary: formState.summary,
|
||||||
|
content: formState.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
// 填充AI优化后的内容
|
||||||
|
if (res.data.resumeTitle) {
|
||||||
|
formState.resumeTitle = res.data.resumeTitle;
|
||||||
|
}
|
||||||
|
if (res.data.summary) {
|
||||||
|
formState.summary = res.data.summary;
|
||||||
|
}
|
||||||
|
if (res.data.content) {
|
||||||
|
formState.content = res.data.content;
|
||||||
|
}
|
||||||
|
stopFakeProgress(true);
|
||||||
|
message.success('AI优化完成!');
|
||||||
|
} else {
|
||||||
|
stopFakeProgress(false);
|
||||||
|
message.error(res.message || 'AI优化失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('AI优化失败:', error);
|
||||||
|
stopFakeProgress(false);
|
||||||
|
message.error(error.message || 'AI优化失败,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onFinish = async (values: any) => {
|
const onFinish = async (values: any) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -266,4 +382,25 @@ onMounted(() => {
|
|||||||
background-color: #f0f2f5;
|
background-color: #f0f2f5;
|
||||||
min-height: calc(100vh - 64px);
|
min-height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-optimize-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-optimize-btn:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-optimize-btn:disabled {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
opacity: 0.7;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user