diff --git a/BOSSBackEnd/pom.xml b/BOSSBackEnd/pom.xml index 35390fc..da4dd3a 100644 --- a/BOSSBackEnd/pom.xml +++ b/BOSSBackEnd/pom.xml @@ -48,6 +48,7 @@ mysql-connector-j runtime + com.github.xiaoymin @@ -87,6 +88,13 @@ org.springframework.boot spring-boot-starter-json + + + + org.springframework.ai + spring-ai-starter-model-deepseek + + com.baomidou @@ -131,6 +139,18 @@ + + + + org.springframework.ai + spring-ai-bom + 1.0.0 + pom + import + + + + @@ -169,4 +189,15 @@ + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/controller/ChatController.java b/BOSSBackEnd/src/main/java/com/zds/boss/controller/ChatController.java new file mode 100644 index 0000000..a2c3eda --- /dev/null +++ b/BOSSBackEnd/src/main/java/com/zds/boss/controller/ChatController.java @@ -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(); + } + } +} diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/controller/ResumeController.java b/BOSSBackEnd/src/main/java/com/zds/boss/controller/ResumeController.java index 834ea2f..f8b437c 100644 --- a/BOSSBackEnd/src/main/java/com/zds/boss/controller/ResumeController.java +++ b/BOSSBackEnd/src/main/java/com/zds/boss/controller/ResumeController.java @@ -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 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); + } } diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/model/dto/resume/ResumeAiOptimizeRequest.java b/BOSSBackEnd/src/main/java/com/zds/boss/model/dto/resume/ResumeAiOptimizeRequest.java new file mode 100644 index 0000000..52bd994 --- /dev/null +++ b/BOSSBackEnd/src/main/java/com/zds/boss/model/dto/resume/ResumeAiOptimizeRequest.java @@ -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; +} diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/model/vo/ResumeAiOptimizeVO.java b/BOSSBackEnd/src/main/java/com/zds/boss/model/vo/ResumeAiOptimizeVO.java new file mode 100644 index 0000000..c2ee630 --- /dev/null +++ b/BOSSBackEnd/src/main/java/com/zds/boss/model/vo/ResumeAiOptimizeVO.java @@ -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; +} diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/service/AiService.java b/BOSSBackEnd/src/main/java/com/zds/boss/service/AiService.java new file mode 100644 index 0000000..ddd4fa9 --- /dev/null +++ b/BOSSBackEnd/src/main/java/com/zds/boss/service/AiService.java @@ -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); +} diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/service/impl/AiServiceImpl.java b/BOSSBackEnd/src/main/java/com/zds/boss/service/impl/AiServiceImpl.java new file mode 100644 index 0000000..e9751fd --- /dev/null +++ b/BOSSBackEnd/src/main/java/com/zds/boss/service/impl/AiServiceImpl.java @@ -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; + } +} diff --git a/BOSSBackEnd/src/main/resources/application.properties b/BOSSBackEnd/src/main/resources/application.properties new file mode 100644 index 0000000..cbb42d2 --- /dev/null +++ b/BOSSBackEnd/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.profiles.active=dev diff --git a/BOSSBackEnd/src/main/resources/application.yml b/BOSSBackEnd/src/main/resources/application.yml index 9021d7c..b211fc4 100644 --- a/BOSSBackEnd/src/main/resources/application.yml +++ b/BOSSBackEnd/src/main/resources/application.yml @@ -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: diff --git a/BOSSFrontEnd/src/api/api/resumeController.ts b/BOSSFrontEnd/src/api/api/resumeController.ts index 94d0f25..7a7d10d 100644 --- a/BOSSFrontEnd/src/api/api/resumeController.ts +++ b/BOSSFrontEnd/src/api/api/resumeController.ts @@ -96,3 +96,32 @@ export async function deleteResumeFile( ...(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>('/resume/ai/optimize', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }) +} diff --git a/BOSSFrontEnd/src/views/MyResumes.vue b/BOSSFrontEnd/src/views/MyResumes.vue index 2d25ce2..3ffd1de 100644 --- a/BOSSFrontEnd/src/views/MyResumes.vue +++ b/BOSSFrontEnd/src/views/MyResumes.vue @@ -35,26 +35,6 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/BOSSFrontEnd/src/views/ResumeEdit.vue b/BOSSFrontEnd/src/views/ResumeEdit.vue index ee343fc..f6c16ee 100644 --- a/BOSSFrontEnd/src/views/ResumeEdit.vue +++ b/BOSSFrontEnd/src/views/ResumeEdit.vue @@ -69,6 +69,37 @@ + + + + + + {{ aiOptimizing ? 'AI优化中...' : 'DeepSeek AI 智能优化' }} + + +
+ 使用 DeepSeek AI 智能优化您的简历内容,让简历更专业、更有吸引力 +
+
+
+ 保存 取消 @@ -82,9 +113,9 @@ import NavBar from '@/components/NavBar.vue'; import { reactive, ref, onMounted, computed } from 'vue'; 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 { UploadOutlined, FilePdfOutlined } from '@ant-design/icons-vue'; +import { UploadOutlined, FilePdfOutlined, RobotOutlined } from '@ant-design/icons-vue'; const route = useRoute(); const router = useRouter(); @@ -95,6 +126,11 @@ const uploading = ref(false); const uploadProgress = ref(0); const uploadFileName = ref(''); +// AI优化相关状态 +const aiOptimizing = ref(false); +const aiProgress = ref(0); +let aiProgressTimer: ReturnType | null = null; + const formState = reactive({ resumeTitle: '', summary: '', @@ -229,6 +265,86 @@ const clearAttachment = async () => { 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) => { loading.value = true; try { @@ -266,4 +382,25 @@ onMounted(() => { background-color: #f0f2f5; 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; +}