业务逻辑修改以及完善

This commit is contained in:
2026-01-11 23:31:20 +08:00
parent 6edf7a4958
commit 85294a917a
17 changed files with 1014 additions and 378 deletions

View File

@@ -366,6 +366,13 @@
<version>1.0.10</version>
</dependency>
<!-- WebSocket Client -->
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-client</artifactId>
<version>2.1.3</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -91,6 +91,12 @@
<artifactId>concentus</artifactId>
</dependency>
<!-- WebSocket Client -->
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-client</artifactId>
</dependency>
</dependencies>

View File

@@ -1,173 +1,80 @@
package com.vetti.socket.agents;
import okhttp3.*;
import okio.ByteString;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import javax.websocket.ClientEndpointConfig;
import javax.websocket.ContainerProvider;
import javax.websocket.WebSocketContainer;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
*
*/
public class ElevenLabsAgentClient extends TextWebSocketHandler{
@Slf4j
public class ElevenLabsAgentClient {
// 存储Vue会话与ElevenLabs WebSocket的映射多客户端隔离
private static final Map<WebSocketSession, WebSocket> SESSION_MAP = new ConcurrentHashMap<>();
// ElevenLabs配置
private static final String ELEVEN_LABS_API_KEY = "你的ElevenLabs API Key";
private static final String AGENT_ID = "你的ElevenLabs Agent ID";
private static final String ELEVEN_LABS_WSS_URL = "wss://api.elevenlabs.io/v1/agents/" + AGENT_ID + "/stream";
// OkHttp客户端
private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) // WebSocket长连接取消读超时
private static final String AGENT_WS_URL =
"wss://api.elevenlabs.io/v1/agents/%s/stream";
private final ElevenLabsAgentEndpoint endpoint;
public ElevenLabsAgentClient(String traceId, WebSocketSession frontendSession) {
this.endpoint = new ElevenLabsAgentEndpoint();
connect(traceId, frontendSession);
}
private void connect(String traceId, WebSocketSession frontendSession) {
try {
log.info("[traceId={}] Connecting to ElevenLabs Agent...", traceId);
WebSocketContainer container =
ContainerProvider.getWebSocketContainer();
ClientEndpointConfig config =
ClientEndpointConfig.Builder.create()
.configurator(new ClientEndpointConfig.Configurator() {
@Override
public void beforeRequest(
Map<String, List<String>> headers
) {
headers.put(
"xi-api-key",
List.of("sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116")
);
}
})
.build();
// ==================== 1. 处理Vue前端连接 ====================
@Override
public void afterConnectionEstablished(WebSocketSession vueSession) throws Exception {
super.afterConnectionEstablished(vueSession);
System.out.println("Vue前端连接成功" + vueSession.getId());
config.getUserProperties().put("traceId", traceId);
config.getUserProperties().put("frontendSession", frontendSession);
// 建立与ElevenLabs Agents的WSS连接
buildElevenLabsWssConnection(vueSession);
}
container.connectToServer(
endpoint,
config,
URI.create(
String.format(
AGENT_WS_URL,
"9c5cb2f7ba9efb61d0f0eee01427b6e00c6abe92d4754cfb794884ac4d73c79d"
)
)
);
// ==================== 2. 处理Vue前端发送的文本消息如语音指令、配置信息 ====================
@Override
protected void handleTextMessage(WebSocketSession vueSession, TextMessage message) throws Exception {
super.handleTextMessage(vueSession, message);
System.out.println("接收Vue文本消息" + message.getPayload());
// 获取对应ElevenLabs WebSocket连接转发消息
WebSocket elevenLabsWs = SESSION_MAP.get(vueSession);
if (elevenLabsWs != null && elevenLabsWs.queueSize() == 0) {
elevenLabsWs.send(message.getPayload());
System.out.println("转发文本消息到ElevenLabs成功");
}
}
// ==================== 3. 处理Vue前端发送的二进制消息核心语音流数据 ====================
@Override
protected void handleBinaryMessage(WebSocketSession vueSession, BinaryMessage message) {
super.handleBinaryMessage(vueSession, message);
byte[] voiceData = message.getPayload().array();
System.out.println("接收Vue语音流数据字节长度" + voiceData.length);
// 获取对应ElevenLabs WebSocket连接转发语音流二进制
WebSocket elevenLabsWs = SESSION_MAP.get(vueSession);
if (elevenLabsWs != null && elevenLabsWs.queueSize() == 0) {
elevenLabsWs.send(ByteString.of(voiceData));
System.out.println("转发语音流到ElevenLabs成功");
}
// 释放二进制消息资源
// message.isLast();
}
// ==================== 4. 处理Vue前端连接关闭 ====================
@Override
public void afterConnectionClosed(WebSocketSession vueSession, org.springframework.web.socket.CloseStatus status) throws Exception {
super.afterConnectionClosed(vueSession, status);
System.out.println("Vue前端连接关闭" + vueSession.getId() + ",原因:" + status.getReason());
// 关闭对应的ElevenLabs WSS连接
WebSocket elevenLabsWs = SESSION_MAP.remove(vueSession);
if (elevenLabsWs != null) {
elevenLabsWs.close(1000, "Vue客户端断开连接");
System.out.println("关闭ElevenLabs WSS连接成功");
}
}
// ==================== 5. 建立与ElevenLabs Agents的WSS连接 ====================
private void buildElevenLabsWssConnection(WebSocketSession vueSession) {
// 1. 构建ElevenLabs WSS请求携带认证头
Request request = new Request.Builder()
.url(ELEVEN_LABS_WSS_URL)
.header("xi-api-key", ELEVEN_LABS_API_KEY) // 必选认证头
.build();
// 2. 构建ElevenLabs WSS监听器处理响应并回流到Vue
WebSocketListener elevenLabsListener = new WebSocketListener() {
// ElevenLabs WSS连接建立
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
System.out.println("与ElevenLabs Agents WSS连接成功");
// 存储Vue会话与ElevenLabs WS的映射
SESSION_MAP.put(vueSession, webSocket);
}
// 接收ElevenLabs文本消息如状态、错误提示回流到Vue
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
System.out.println("接收ElevenLabs文本消息" + text);
try {
// 回流到Vue前端
if (vueSession.isOpen()) {
vueSession.sendMessage(new TextMessage(text));
}
} catch (Exception e) {
System.err.println("回流文本消息到Vue失败" + e.getMessage());
throw new RuntimeException("Connect ElevenLabs failed", e);
}
}
// 接收ElevenLabs二进制消息核心音频流响应回流到Vue
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
byte[] audioData = bytes.toByteArray();
System.out.println("接收ElevenLabs音频流数据字节长度" + audioData.length);
try {
// 回流二进制音频流到Vue前端
if (vueSession.isOpen()) {
vueSession.sendMessage(new BinaryMessage(audioData));
}
} catch (Exception e) {
System.err.println("回流音频流到Vue失败" + e.getMessage());
}
public void sendAudio(java.nio.ByteBuffer buffer) {
endpoint.sendAudio(buffer);
}
// ElevenLabs WSS连接关闭
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
System.out.println("ElevenLabs WSS连接关闭" + reason + ",状态码:" + code);
// 移除映射关闭Vue连接
SESSION_MAP.remove(vueSession);
try {
if (vueSession.isOpen()) {
vueSession.close(org.springframework.web.socket.CloseStatus.NORMAL);
}
} catch (Exception e) {
e.printStackTrace();
}
public void sendText(String text) {
endpoint.sendText(text);
}
// ElevenLabs WSS连接异常
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
System.err.println("ElevenLabs WSS连接异常" + t.getMessage());
// 移除映射关闭Vue连接
SESSION_MAP.remove(vueSession);
try {
if (vueSession.isOpen()) {
vueSession.close(org.springframework.web.socket.CloseStatus.SERVER_ERROR);
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
// 3. 建立ElevenLabs WSS连接
OK_HTTP_CLIENT.newWebSocket(request, elevenLabsListener);
public void close() {
endpoint.close();
}
}

View File

@@ -0,0 +1,88 @@
package com.vetti.socket.agents;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import javax.websocket.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class ElevenLabsAgentEndpoint extends Endpoint {
private Session agentSession;
private String traceId;
private WebSocketSession frontendSession;
private final AtomicInteger audioCount = new AtomicInteger();
@Override
public void onOpen(Session session, EndpointConfig config) {
log.info("客户端链接啦:{}", traceId);
this.agentSession = session;
this.traceId = (String) config.getUserProperties().get("traceId");
this.frontendSession =
(WebSocketSession) config.getUserProperties().get("frontendSession");
log.info("[traceId={}] ElevenLabs Agent CONNECTED", traceId);
session.addMessageHandler(String.class, this::onText);
session.addMessageHandler(ByteBuffer.class, this::onAudio);
}
private void onText(String message) {
try {
log.info("[traceId={}] Agent → TEXT {}", traceId, message);
frontendSession.sendMessage(new TextMessage(message));
} catch (IOException e) {
log.error("[traceId={}] Send text failed", traceId, e);
}
}
private void onAudio(ByteBuffer buffer) {
try {
int count = audioCount.incrementAndGet();
if (count == 1) {
log.info(
"[traceId={}] Agent → AUDIO FIRST packet size={} bytes",
traceId,
buffer.remaining()
);
}
frontendSession.sendMessage(new BinaryMessage(buffer));
} catch (IOException e) {
log.error("[traceId={}] Send audio failed", traceId, e);
}
}
@Override
public void onClose(Session session, CloseReason closeReason) {
log.info("[traceId={}] ElevenLabs Agent CLOSED {}", traceId, closeReason);
}
public void sendAudio(ByteBuffer buffer) {
if (agentSession != null && agentSession.isOpen()) {
agentSession.getAsyncRemote().sendBinary(buffer);
}
}
public void sendText(String text) {
if (agentSession != null && agentSession.isOpen()) {
agentSession.getAsyncRemote().sendText(text);
}
}
public void close() {
try {
if (agentSession != null) {
agentSession.close();
}
} catch (Exception ignored) {}
}
}

View File

@@ -0,0 +1,17 @@
package com.vetti.socket.agents;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
@Data
public class ElevenLabsConfig {
@Value("${elevenLabs.agent-id}")
private String agentId;
@Value("${elevenLabs.api-key}")
private String apiKey;
}

View File

@@ -0,0 +1,36 @@
package com.vetti.socket.agents;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.BinaryWebSocketHandler;
@Slf4j
public class FrontendWebSocketHandler extends BinaryWebSocketHandler {
private ElevenLabsAgentClient agentClient;
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String traceId = session.getId();
log.info("[traceId={}] Vue WebSocket CONNECTED", traceId);
agentClient = new ElevenLabsAgentClient(traceId, session);
}
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
agentClient.sendAudio(message.getPayload());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
agentClient.sendText(message.getPayload());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
log.info("[traceId={}] Vue WebSocket CLOSED", session.getId());
agentClient.close();
}
}

View File

@@ -0,0 +1,16 @@
package com.vetti.socket.agents;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new FrontendWebSocketHandler(), "/voice-websocket/elevenLabsAgent/{clientId}")
.setAllowedOrigins("*");
}
}

View File

@@ -3,6 +3,7 @@ package com.vetti.web.controller.ai;
import com.vetti.common.core.controller.BaseController;
import com.vetti.common.core.domain.R;
import com.vetti.hotake.domain.HotakeInitialScreeningQuestionsInfo;
import com.vetti.hotake.domain.HotakeRolesInfo;
import com.vetti.hotake.domain.dto.*;
import com.vetti.hotake.domain.vo.*;
import com.vetti.hotake.service.IHotakeAiCommonToolsService;
@@ -160,4 +161,24 @@ public class HotakeAiCommonToolsController extends BaseController {
return R.ok(hotakeAiCommonToolsService.getRoleLinkAnalysis(roleLinkAnalysisVo));
}
/**
* 招聘信息分析补全
*/
@ApiOperation("招聘信息分析补全(按照步骤生成)")
@PostMapping(value = "/roleInfoAnalysisSetUp")
public R<HotakeRolesInfoDto> handleRoleInfoAnalysisSetUp(@RequestBody HotakeRolesInfoDto rolesInfoDto)
{
return R.ok(hotakeAiCommonToolsService.getRoleInfoAnalysisSetUp(rolesInfoDto));
}
/**
* AI面试问题生成
*/
@ApiOperation("AI面试问题生成")
@PostMapping(value = "/aiInterviewQuestions")
public R<?> handleAiInterviewQuestions(@RequestBody HotakeRolesInfo rolesInfo)
{
return R.ok(hotakeAiCommonToolsService.getAiInterviewQuestions(rolesInfo));
}
}

View File

@@ -155,6 +155,8 @@ elevenLabs:
apiKey: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
# apiKey: sk_88f5a560e1bbde0e5b8b6b6eb1812163a98bfb98554acbec
modelId: eleven_turbo_v2_5
agent-id: 9c5cb2f7ba9efb61d0f0eee01427b6e00c6abe92d4754cfb794884ac4d73c79d
api-key: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
# 语音转文本
whisper:

View File

@@ -155,6 +155,8 @@ elevenLabs:
apiKey: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
# apiKey: sk_88f5a560e1bbde0e5b8b6b6eb1812163a98bfb98554acbec
modelId: eleven_turbo_v2_5
agent-id: 9c5cb2f7ba9efb61d0f0eee01427b6e00c6abe92d4754cfb794884ac4d73c79d
api-key: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
# 语音转文本
whisper:

View File

@@ -1701,95 +1701,105 @@ public class AiCommonPromptConstants {
* @return
*/
public static String initializationRoleLinkAnalysisPrompt(){
String promptStr = "你是一位专业的招聘信息补全专家专门根据已提取的岗位信息进行AI智能补全严格遵守业务约束规则生成标准API格式响应。\n" +
String promptStr = "You are a professional job posting information completion expert, specializing in AI intelligent completion based on extracted job information, strictly following business constraint rules to generate standard API format responses.\n" +
"\n" +
"## 核心任务\n" +
"根据用户提供的「已提取岗位信息」生成完整的、符合业务约束规则的标准API格式岗位数据。\n" +
"## Core Task\n" +
"Generate complete job data that complies with business constraint rules in standard API format based on the \"extracted job information\" provided by the user.\n" +
"\n" +
"## 输入信息说明\n" +
"用户将提供一个JSON对象包含从招聘页面提取的基础信息其中\n" +
"- **有值的字段**:表示从页面准确提取的信息,请保留原值\n" +
"- **null值字段**表示页面未找到的信息需要AI智能补全\n" +
"- **非标准格式字段**:需要转换为业务规则要求的标准格式\n" +
"## Input Information Description\n" +
"The user will provide a JSON object containing basic information extracted from job posting pages, where:\n" +
"- **Fields with values**: Represent information accurately extracted from the page, please retain original values\n" +
"- **null value fields**: Represent information not found on the page, requiring AI intelligent completion\n" +
"- **Non-standard format fields**: Need to be converted to standard format required by business rules\n" +
"\n" +
"## 信息处理原则\n" +
"1. **保留准确信息**:对于已从页面准确提取的信息(如jobTitlecompanyNamelocation),请直接保留原值\n" +
"2. **格式标准化**:对于格式不标准的信息,请转换为业务规则要求的标准格式\n" +
" - `experience: \"3-5年工作经验\"` → `jobExperience: \"2-3 Years\"`\n" +
" - `education: \"本科及以上学历\"` → 标准教育要求格式\n" +
" - `skills: [\"Java\", \"Spring\"]` → 规则允许的技能格式\n" +
"## Information Processing Principles\n" +
"1. **Preserve Accurate Information**: For information accurately extracted from the page (such as jobTitle, companyName, location), please retain the original values directly\n" +
"2. **Format Standardization**: For non-standard format information, please convert to standard format required by business rules\n" +
" - `experience: \"3-5 years experience\"` → `jobExperience: 4`\n" +
" - `education: \"Bachelor's degree or above\"` → Standard education requirements format\n" +
" - `skills: [\"Java\", \"Spring\"]` → Skill format allowed by rules\n" +
" - `salaryRange: \"15K-25K\"` → `salaryStart: 90000, salaryEnd: 150000`\n" +
"3. **智能补全**对于标记为null的缺失信息请根据岗位特点和行业标准进行合理补全\n" +
"4. **严格合规**:所有字段必须遵守业务约束规则表,禁止生成规则外的值\n" +
"3. **Intelligent Completion**: For missing information marked as null, please reasonably complete based on job characteristics and industry standards\n" +
"4. **Strict Compliance**: All fields must comply with business constraint rules, and values outside the rules are prohibited\n" +
"\n" +
"## 业务约束规则表\n" +
"| 字段名 | 约束类型 | 业务约束值/规则 |\n" +
"|----------------------------|----------------|--------------------------------------------------------------------------------|\n" +
"| jobType | 可选值集合 | 仅允许:Full-time/Part-time/Contract/Temporary/Internship |\n" +
"| jobLevel | 可选值集合 | 仅允许:Entry/Junior/Mid/Senior/Lead/Manager/Director |\n" +
"| jobExperience | 文本输入 | 自由文本描述工作经验要求,如\"3-5年相关工作经验\"\"2年以上Java开发经验\" |\n" +
"| locationType | 可选值集合 | 仅允许On-site/Remote/Hybrid |\n" +
"| status | 可选值集合 | 仅允许pause/archived/open/editing |\n" +
"| dataType | 可选值集合 | 仅允许release/draft/template |\n" +
"| salaryStart/salaryEnd | 数值范围+类型 | 类型数字BigDecimal范围salaryStart≥50000salaryEnd≤200000且salaryEnd≥salaryStart |\n" +
"| descriptionTone | 可选值集合 | 仅允许professional/friendly/innovative/dynamic四选一数组格式 |\n" +
"| aboutRole | 文本输入 | 关于职位的详细描述,包含岗位背景、重要性、发展前景等 |\n" +
"| responsibilities | 文本输入 | 详细的岗位职责描述,包含主要工作内容和具体任务 |\n" +
"| roleBenefitsList | 结构+取值规则 | 1. 结构:数组,每个元素仅含 keyValue 字段;<br>2. 取值:自由文本,如\"五险一金\"\"年终奖金\"\"带薪年假\"等;<br>3. 数量3-8项 |\n" +
"| requiredSkillsList | 结构+取值规则 | 1. 结构:数组,每个元素仅含 keyValue 字段;<br>2. 取值:必须是\"Angular 4+ & RxJS\"/\"Financial Services Industry Experience\"/\"Health & Travel Account Modification\"/\"Information Technology\"<br>3. 数量1-5项 |\n" +
"| niceToHaveSkillsList | 结构+取值规则 | 1. 结构:数组,每个元素仅含 keyValue 字段;<br>2. 取值:必须是\"Financial Services Industry Experience\"/\"Health & Travel Account Modification\"/\"Information Technology\"<br>3. 数量0-3项 |\n" +
"| educationRequirements | 结构+可选值 | 1. 结构:对象,含 academicMajor、degree<br>2. academicMajor仅允许\"Computer Science\"<br>3. degree仅允许\"Bachelor's Degree\"/\"Bachelor's degree or equivalent\" |\n" +
"| certificationsLicensesList | 结构+取值规则 | 1. 结构:数组,每个元素含 type、val<br>2. type仅允许\"standard\"/\"customize\"<br>3. valstandard仅允许\"AWS Certified Solutions Architect\"/\"CISSP Certified Information Systems Security Professional\"/\"PMP Certified\"<br>4. 数量0-3项 |\n" +
"## Business Constraint Rules Table\n" +
"| Field Name | Constraint Type | Business Constraint Values/Rules |\n" +
"|----------------------------|-------------------|--------------------------------------------------------------------------------|\n" +
"| jobType | Optional Value Set | Only allow: Full-time/Part-time/Contract/Temporary/Internship |\n" +
"| jobLevel | Optional Value Set | Only allow: Entry/Junior/Mid/Senior/Lead/Manager/Director |\n" +
"| jobExperience | Integer | Work experience years required, integer value 1-10, e.g., 1 for 1 year, 5 for 5 years, 10 for 10+ years |\n" +
"\n" +
"## 核心要求\n" +
"1. 所有字段必须遵守「业务约束规则表」,禁止生成规则外的值;\n" +
"2. 数值字段必须匹配类型如salaryStart为数字非字符串\n" +
"3. 必须包含requiredSkillsList、niceToHaveSkillsList、educationRequirements、certificationsLicensesList、descriptionTone、aboutRole、responsibilities、roleBenefitsList八个核心字段不得遗漏\n" +
"4. 去掉以下字段createTime、updateTime、uuid、sysUserType、speechSpeed、acceptEquivalentWorkFlag生成结果中不得出现这些字段\n" +
"5. requiredSkillsList/niceToHaveSkillsList/educationRequirements/certificationsLicensesList的取值必须完全匹配业务约束规则\n" +
"6. jobExperience字段为自由文本输入不限制于预定义选项\n" +
"7. descriptionTone必须从四个选项中选择一个以数组格式返回\n" +
"8. aboutRole和responsibilities为文本字段需要详细描述\n" +
"9. roleBenefitsList为数组格式包含3-8项岗位福利。\n" +
"**jobExperience Conversion Rules**:\n" +
"- \"0-1 years\" or \"Fresh graduate\" → 1\n" +
"- \"1-2 years\" → 2\n" +
"- \"2-3 years\" → 3 \n" +
"- \"3-5 years\" → 4\n" +
"- \"5-8 years\" → 6\n" +
"- \"8-10 years\" → 8\n" +
"- \"10+ years\" or \"Senior level\" → 10\n" +
"- If description is vague, infer from job level: Entry→1-2, Junior→2-3, Mid→3-5, Senior→5-8, Lead/Manager→8-10\n" +
"| locationType | Optional Value Set | Only allow: On-site/Remote/Hybrid |\n" +
"| status | Optional Value Set | Only allow: pause/archived/open/editing |\n" +
"| dataType | Optional Value Set | Only allow: release/draft/template |\n" +
"| salaryStart/salaryEnd | Numeric Range+Type | Type: Number (BigDecimal); Range: salaryStart≥50000, salaryEnd≤200000, and salaryEnd≥salaryStart |\n" +
"| descriptionTone | Optional Value Set | Only allow: professional/friendly/innovative/dynamic (choose one, array format) |\n" +
"| aboutRole | Text Input | Detailed description about the role, including background, importance, and development prospects |\n" +
"| responsibilities | Text Input | Detailed job responsibilities description, including main work content and specific tasks |\n" +
"| roleBenefitsList | Structure+Value Rules | 1. Structure: Array, each element contains only keyValue field;<br>2. Values: Free text such as \"Health Insurance\", \"Annual Bonus\", \"Paid Time Off\", etc.;<br>3. Quantity: 3-8 items |\n" +
"| requiredSkillsList | Structure+Value Rules | 1. Structure: Array, each element contains only keyValue field;<br>2. Values: Must be \"Angular 4+ & RxJS\"/\"Financial Services Industry Experience\"/\"Health & Travel Account Modification\"/\"Information Technology\";<br>3. Quantity: 1-5 items |\n" +
"| niceToHaveSkillsList | Structure+Value Rules | 1. Structure: Array, each element contains only keyValue field;<br>2. Values: Must be \"Financial Services Industry Experience\"/\"Health & Travel Account Modification\"/\"Information Technology\";<br>3. Quantity: 0-3 items |\n" +
"| educationRequirements | Structure+Optional Values | 1. Structure: Object containing academicMajor, degree;<br>2. academicMajor: Only allow \"Computer Science\";<br>3. degree: Only allow \"Bachelor's Degree\"/\"Bachelor's degree or equivalent\" |\n" +
"| certificationsLicensesList | Structure+Value Rules | 1. Structure: Array, each element contains type, val;<br>2. type: Only allow \"standard\"/\"customize\";<br>3. val (standard): Only allow \"AWS Certified Solutions Architect\"/\"CISSP Certified Information Systems Security Professional\"/\"PMP Certified\";<br>4. Quantity: 0-3 items |\n" +
"\n" +
"## API响应格式要求\n" +
"- 顶层字段code固定0、dataHotakeRolesInfoDtoRes、message空字符串、timestamp当前时间格式YYYY-MM-DDTHH:MM:SS\n" +
"- 严格排除createTime、updateTime、uuid、sysUserType、speechSpeed、acceptEquivalentWorkFlag\n" +
"- 严格匹配字段类型(如数组字段不得为单个值,数字字段不得为字符串)。\n" +
"## Core Requirements\n" +
"1. All fields must comply with the \"Business Constraint Rules Table\", and values outside the rules are prohibited;\n" +
"2. Numeric fields must match the type (e.g., salaryStart is a number, not a string);\n" +
"3. Must include the eight core fields: requiredSkillsList, niceToHaveSkillsList, educationRequirements, certificationsLicensesList, descriptionTone, aboutRole, responsibilities, roleBenefitsList, and must not be omitted;\n" +
"4. Remove the following fields: createTime, updateTime, uuid, sysUserType, speechSpeed, acceptEquivalentWorkFlag (these fields must not appear in the generated results);\n" +
"5. The values of requiredSkillsList/niceToHaveSkillsList/educationRequirements/certificationsLicensesList must completely match the business constraint rules;\n" +
"6. jobExperience field is integer type, value range 1-10, representing required years of work experience;\n" +
"7. descriptionTone must choose one from four options, returned in array format;\n" +
"8. aboutRole and responsibilities are text fields that require detailed descriptions;\n" +
"9. roleBenefitsList is in array format, containing 3-8 role benefits.\n" +
"\n" +
"## 输出格式\n" +
"请以JSON格式返回补全结果确保\n" +
"1. 结构完整,包含所有必需字段\n" +
"2. 数据类型正确(数字、字符串、数组、对象)\n" +
"3. 取值符合业务约束规则\n" +
"4. 不包含禁止字段\n" +
"## API Response Format Requirements\n" +
"- Top-level fields: code (fixed 0), data (HotakeRolesInfoDtoRes), message (empty string), timestamp (current time, format YYYY-MM-DDTHH:MM:SS);\n" +
"- Strictly exclude: createTime, updateTime, uuid, sysUserType, speechSpeed, acceptEquivalentWorkFlag;\n" +
"- Strictly match field types (e.g., array fields must not be single values, numeric fields must not be strings).\n" +
"\n" +
"## 示例输出格式\n" +
"## Output Format\n" +
"Please return the completion results in JSON format, ensuring:\n" +
"1. Complete structure with all required fields\n" +
"2. Correct data types (numbers, strings, arrays, objects)\n" +
"3. Values comply with business constraint rules\n" +
"4. Does not contain prohibited fields\n" +
"\n" +
"## Example Output Format\n" +
"```json\n" +
"{\n" +
" \"code\": 0,\n" +
" \"data\": {\n" +
" \"jobTitle\": \"岗位名称\",\n" +
" \"companyName\": \"公司名称\",\n" +
" \"location\": \"工作地点\",\n" +
" \"jobTitle\": \"Job Title\",\n" +
" \"companyName\": \"Company Name\",\n" +
" \"location\": \"Work Location\",\n" +
" \"salaryStart\": 80000,\n" +
" \"salaryEnd\": 120000,\n" +
" \"jobType\": \"Full-time\",\n" +
" \"jobLevel\": \"Senior\",\n" +
" \"jobExperience\": \"3-5年相关工作经验熟悉Java开发\",\n" +
" \"jobExperience\": 4,\n" +
" \"locationType\": \"On-site\",\n" +
" \"status\": \"open\",\n" +
" \"dataType\": \"release\",\n" +
" \"descriptionTone\": [\"professional\"],\n" +
" \"aboutRole\": \"这是一个关键的技术岗位,负责公司核心业务系统的开发和维护。该职位将直接参与产品架构设计,是技术团队的重要组成部分,具有良好的职业发展前景。\",\n" +
" \"responsibilities\": \"1. 负责后端系统的设计、开发和维护2. 参与系统架构设计和技术方案制定3. 编写高质量、可维护的代码4. 与前端团队协作完成产品功能5. 参与代码审查和技术分享6. 解决系统性能和稳定性问题。\",\n" +
" \"aboutRole\": \"This is a key technical position responsible for developing and maintaining the company's core business systems. The role will directly participate in product architecture design and is an important part of the technical team with excellent career development prospects.\",\n" +
" \"responsibilities\": \"1. Design, develop and maintain backend systems; 2. Participate in system architecture design and technical solution formulation; 3. Write high-quality, maintainable code; 4. Collaborate with frontend teams to complete product features; 5. Participate in code reviews and technical sharing; 6. Solve system performance and stability issues.\",\n" +
" \"roleBenefitsList\": [\n" +
" {\"keyValue\": \"五险一金\"},\n" +
" {\"keyValue\": \"年终奖金\"},\n" +
" {\"keyValue\": \"带薪年假\"},\n" +
" {\"keyValue\": \"弹性工作时间\"},\n" +
" {\"keyValue\": \"技能培训\"},\n" +
" {\"keyValue\": \"团队建设活动\"}\n" +
" {\"keyValue\": \"Health Insurance\"},\n" +
" {\"keyValue\": \"Annual Bonus\"},\n" +
" {\"keyValue\": \"Paid Time Off\"},\n" +
" {\"keyValue\": \"Flexible Working Hours\"},\n" +
" {\"keyValue\": \"Skills Training\"},\n" +
" {\"keyValue\": \"Team Building Activities\"}\n" +
" ],\n" +
" \"requiredSkillsList\": [\n" +
" {\"keyValue\": \"Information Technology\"},\n" +
@@ -1814,12 +1824,255 @@ public class AiCommonPromptConstants {
"}\n" +
"```\n" +
"\n" +
"## 质量要求\n" +
"1. 信息保留性:准确保留第一阶段提取的有效信息\n" +
"2. 补全合理性AI补全的信息应符合行业标准和岗位特点\n" +
"3. 规则严格性100%遵守业务约束规则,不得有任何例外\n" +
"4. 格式标准性严格按照API格式要求输出\n" +
"5. 完整性验证:确保所有必需字段都已包含";
"## Quality Requirements\n" +
"1. Information Retention: Accurately retain valid information extracted in the first stage\n" +
"2. Completion Reasonableness: AI-completed information should comply with industry standards and job characteristics\n" +
"3. Rule Strictness: 100% compliance with business constraint rules, no exceptions allowed\n" +
"4. Format Standardization: Strictly output according to API format requirements\n" +
"5. Completeness Verification: Ensure all required fields are included";
return promptStr;
}
/**
* 初始化AI生成问题提示词
* @return
*/
public static String initializationAiInterviewQuestionsPrompt(){
String promptStr = "You are a senior AI interviewer and HR expert, specializing in designing high-quality standardized interview questions for AI voice interview systems.\n" +
"\n" +
"## Core Mission\n" +
"Based on the provided job description, generate a set of standardized interview questions for a 15-25 minute AI voice interview. All candidates applying for the same position will answer the same set of questions.\n" +
"\n" +
"## AI Interview Characteristics\n" +
"\n" +
"### 1. Standardization Principle\n" +
"- All candidates face the same questions\n" +
"- Questions based on job requirements, not individual resumes\n" +
"- Ensure fairness and consistency\n" +
"\n" +
"### 2. Time Control\n" +
"- Total interview duration strictly controlled within 15-25 minutes\n" +
"- Each question estimated response time 2-4 minutes\n" +
"- Include thinking time and expression time\n" +
"\n" +
"### 3. AI Voice Adaptation\n" +
"- Questions clearly and unambiguously stated\n" +
"- Suitable for voice interaction, not dependent on visual materials\n" +
"- Easy for AI systems to understand and evaluate responses\n" +
"\n" +
"## Question Category Requirements\n" +
"\n" +
"### 1. Technical Skills\n" +
"**Objective**: Assess candidate's professional technical capabilities\n" +
"\n" +
"**Question Characteristics**:\n" +
"- Based on tech stack and requirements in job description\n" +
"- Test theoretical knowledge and practical experience\n" +
"- Include scenario-based technical questions\n" +
"- Evaluate learning ability and technical depth\n" +
"\n" +
"**Example Areas**:\n" +
"- Programming languages and framework usage\n" +
"- System design and architectural thinking\n" +
"- Tools and platform experience\n" +
"- Best practices and code quality\n" +
"- New technology learning and application\n" +
"\n" +
"### 2. Experience Level\n" +
"**Objective**: Understand candidate's work experience and project background\n" +
"\n" +
"**Question Characteristics**:\n" +
"- Don't involve specific company or project names\n" +
"- Focus on depth and breadth of experience\n" +
"- Evaluate problem-solving capabilities\n" +
"- Understand career development trajectory\n" +
"\n" +
"**Example Content**:\n" +
"- Years of experience and experience level\n" +
"- Project complexity and scale\n" +
"- Team collaboration experience\n" +
"- Challenge handling ability\n" +
"- Growth and learning experiences\n" +
"\n" +
"### 3. Problem Solving\n" +
"**Objective**: Assess candidate's analytical and problem-solving abilities\n" +
"\n" +
"**Question Characteristics**:\n" +
"- Scenario-based hypothetical questions\n" +
"- Test logical thinking and analytical skills\n" +
"- Evaluate innovation and optimization thinking\n" +
"- Understand decision-making process\n" +
"\n" +
"**Example Scenarios**:\n" +
"- Technical problem resolution\n" +
"- Performance optimization challenges\n" +
"- System failure handling\n" +
"- Requirement change response\n" +
"- Solutions under resource constraints\n" +
"\n" +
"### 4. Communication\n" +
"**Objective**: Assess candidate's expression and communication abilities\n" +
"\n" +
"**Question Characteristics**:\n" +
"- Test technical concept explanation ability\n" +
"- Evaluate cross-team collaboration communication\n" +
"- Understand conflict resolution approaches\n" +
"- Verify knowledge transfer capabilities\n" +
"\n" +
"### 5. Cultural Fit\n" +
"**Objective**: Assess candidate's compatibility with company culture\n" +
"\n" +
"**Question Characteristics**:\n" +
"- Based on company culture and values\n" +
"- Understand work attitude and values\n" +
"- Evaluate team collaboration awareness\n" +
"- Verify career development expectations\n" +
"\n" +
"## Time Allocation Recommendations\n" +
"\n" +
"### 15-minute Interview (6-7 questions)\n" +
"- Technical Skills: 3-4 questions (8-10 minutes)\n" +
"- Experience Level: 1-2 questions (3-4 minutes)\n" +
"- Problem Solving: 1 question (2-3 minutes)\n" +
"- Cultural Fit: 1 question (2 minutes)\n" +
"\n" +
"### 20-minute Interview (8-9 questions)\n" +
"- Technical Skills: 4-5 questions (10-12 minutes)\n" +
"- Experience Level: 2 questions (4-5 minutes)\n" +
"- Problem Solving: 1-2 questions (3-4 minutes)\n" +
"- Communication: 1 question (2-3 minutes)\n" +
"\n" +
"### 25-minute Interview (10-11 questions)\n" +
"- Technical Skills: 5-6 questions (12-15 minutes)\n" +
"- Experience Level: 2-3 questions (5-6 minutes)\n" +
"- Problem Solving: 2 questions (4-5 minutes)\n" +
"- Communication: 1 question (2-3 minutes)\n" +
"- Cultural Fit: 1 question (2 minutes)\n" +
"\n" +
"## Question Design Principles\n" +
"\n" +
"### 1. Open-ended Principle\n" +
"- Avoid simple yes/no questions\n" +
"- Encourage detailed explanations and examples\n" +
"- Allow multiple correct answers\n" +
"- Provide ample expression space\n" +
"\n" +
"### 2. Relevance Principle\n" +
"- Strictly based on job description and requirements\n" +
"- Related to actual work scenarios\n" +
"- Match position level and experience requirements\n" +
"- Reflect core skills and abilities\n" +
"\n" +
"### 3. Fairness Principle\n" +
"- Don't involve personal background and privacy\n" +
"- Avoid cultural bias and discrimination\n" +
"- Give all candidates equal opportunities\n" +
"- Assess based on ability, not background\n" +
"\n" +
"### 4. Assessability Principle\n" +
"- Easy for AI systems to analyze and score\n" +
"- Have clear evaluation dimensions\n" +
"- Quantifiable assessment criteria\n" +
"- Support objective comparative analysis\n" +
"\n" +
"## Output Format Requirements\n" +
"\n" +
"Please return AI interview questions in JSON format with the following structure:\n" +
"\n" +
"```json\n" +
"{\n" +
" \"interview_questions\": [\n" +
" {\n" +
" \"question_id\": \"Q001\",\n" +
" \"question_text\": \"Please describe your experience with [relevant technology] and how you learned and mastered this technology?\",\n" +
" \"category\": \"technical_skills|experience_level|problem_solving|communication|cultural_fit\",\n" +
" \"estimated_time\": 3.0,\n" +
" \"difficulty_level\": \"basic|intermediate|advanced\",\n" +
" \"evaluation_criteria\": \"Assess technical depth, learning ability, practical experience\",\n" +
" \"follow_up_hints\": [\"Can ask about specific technical details\", \"Understand challenges encountered and solutions\"],\n" +
" \"expected_answer_points\": [\"Technical understanding level\", \"Learning methods\", \"Practical application\", \"Continuous improvement\"],\n" +
" \"scoring_weight\": 1-5\n" +
" }\n" +
" ],\n" +
" \"interview_summary\": {\n" +
" \"total_questions\": 8,\n" +
" \"estimated_total_time\": 20,\n" +
" \"category_distribution\": {\n" +
" \"technical_skills\": 4,\n" +
" \"experience_level\": 2,\n" +
" \"problem_solving\": 1,\n" +
" \"cultural_fit\": 1\n" +
" },\n" +
" \"difficulty_distribution\": {\n" +
" \"basic\": 2,\n" +
" \"intermediate\": 5,\n" +
" \"advanced\": 1\n" +
" }\n" +
" },\n" +
" \"interview_guidelines\": {\n" +
" \"introduction\": \"Interview opening statement suggestion\",\n" +
" \"transition_phrases\": [\"Transition phrase 1\", \"Transition phrase 2\"],\n" +
" \"closing_statement\": \"Interview closing statement\",\n" +
" \"time_management_tips\": [\"Time control tip 1\", \"Time control tip 2\"]\n" +
" },\n" +
" \"evaluation_framework\": {\n" +
" \"scoring_method\": \"Scoring method description\",\n" +
" \"key_indicators\": [\"Key evaluation indicator 1\", \"Key evaluation indicator 2\"],\n" +
" \"red_flags\": [\"Red flag to watch for 1\", \"Red flag to watch for 2\"],\n" +
" \"positive_signals\": [\"Positive signal 1\", \"Positive signal 2\"]\n" +
" }\n" +
"}\n" +
"```\n" +
"\n" +
"## Quality Standards\n" +
"\n" +
"### 1. Question Quality\n" +
"- Clear and accurate expression, unambiguous\n" +
"- Suitable for voice interaction scenarios\n" +
"- Match job requirements and level\n" +
"- Have good discriminative power\n" +
"\n" +
"### 2. Time Control\n" +
"- Total duration strictly controlled within specified range\n" +
"- Reasonable time allocation for each question\n" +
"- Consider thinking and expression time\n" +
"- Leave appropriate buffer time\n" +
"\n" +
"### 3. Structural Completeness\n" +
"- Include all required fields\n" +
"- Balanced and reasonable question distribution\n" +
"- Appropriate difficulty gradient design\n" +
"- Complete evaluation framework\n" +
"\n" +
"### 4. AI Adaptability\n" +
"- Easy for AI systems to process and analyze\n" +
"- Support automated evaluation\n" +
"- Suitable for speech recognition and understanding\n" +
"- Easy to quantify and compare results\n" +
"\n" +
"## Special Requirements\n" +
"\n" +
"### Technical Question Design\n" +
"1. **Based on Job Tech Stack**: Strictly based on technical requirements in job description\n" +
"2. **Scenario Application**: Design questions combining actual work scenarios\n" +
"3. **Progressive Levels**: Gradually deepen from basic concepts to advanced applications\n" +
"4. **Practice-Oriented**: Emphasize practical application ability over pure theoretical knowledge\n" +
"\n" +
"### Non-Technical Question Design\n" +
"1. **Cultural Fit**: Design based on company culture and values\n" +
"2. **Universal Applicability**: Suitable for candidates from different backgrounds\n" +
"3. **Behavioral Assessment**: Evaluate behavioral patterns through hypothetical scenarios\n" +
"4. **Development Potential**: Assess learning ability and growth potential\n" +
"\n" +
"### Things to Avoid\n" +
"1. Must not involve personal privacy and sensitive information\n" +
"2. Avoid questions requiring specific tools or environments\n" +
"3. Don't use overly complex or academic expressions\n" +
"4. Avoid cultural bias and discriminatory content\n" +
"5. Don't design complex algorithm questions requiring long thinking time\n" +
"\n" +
"Please generate a high-quality, standardized set of AI interview questions based on the provided job information.\n";
return promptStr;
}

View File

@@ -0,0 +1,56 @@
package com.vetti.common.enums;
public enum RoleBenefitsEnum {
health_insurance("health_insurance", "health insurance"),
paid_time_off("paid_time_off", "paid time off"),
flexible_work_schedule("flexible_work_schedule", "flexible work schedule"),
professional_development_budget("professional_development_budget", "professional development budget"),
performance_bonuses("performance_bonuses", "performance bonuses"),
wellness_program("wellness_program", "wellness program"),
stock_options("stock_options", "stock options/esop"),
relocation_support("relocation_support", "relocation support"),
parental_leave("parental_leave", "parental leave"),
team_events_corporate_activities("team_events_corporate_activities", "team events & corporate activities"),
;
private final String code;
private final String info;
RoleBenefitsEnum(String code, String info)
{
this.code = code;
this.info = info;
}
public String getCode()
{
return code;
}
public String getInfo()
{
return info;
}
public static RoleBenefitsEnum getByInfo(String code) {
RoleBenefitsEnum[] entityEnums = values();
for (RoleBenefitsEnum entityEnum : entityEnums) {
if (entityEnum.getCode().equals(code)) {
return entityEnum;
}
}
return null;
}
public static RoleBenefitsEnum getByCode(String info) {
RoleBenefitsEnum[] entityEnums = values();
for (RoleBenefitsEnum entityEnum : entityEnums) {
if (entityEnum.getInfo().equals(info.toLowerCase())) {
return entityEnum;
}
}
return null;
}
}

View File

@@ -6,6 +6,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import cn.hutool.core.util.ObjectUtil;
import org.springframework.util.AntPathMatcher;
import com.vetti.common.constant.Constants;
import com.vetti.common.core.text.StrFormatter;
@@ -719,4 +721,11 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
}
return sb.toString();
}
public static String getObjectStr(Object obj){
if(ObjectUtil.isNotEmpty(obj)){
return obj.toString();
}
return "";
}
}

View File

@@ -114,7 +114,7 @@ public class SecurityConfig
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage","/aiCommon/**","/voice-websocket/multiple/**",
"/voice-websocket/**","/voice-websocket/multiplePcm/**","/verification/email/send","/verification/email/verify","/verification/phone/send",
"/forgotPassword","/verification/email/register/send").permitAll()
"/forgotPassword","/verification/email/register/send","/voice-websocket/elevenLabsAgent/**").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()

View File

@@ -2,6 +2,7 @@ package com.vetti.hotake.service;
import com.vetti.hotake.domain.HotakeAiInterviewQuestionsInfo;
import com.vetti.hotake.domain.HotakeInitialScreeningQuestionsInfo;
import com.vetti.hotake.domain.HotakeRolesInfo;
import com.vetti.hotake.domain.dto.*;
import com.vetti.hotake.domain.vo.*;
@@ -111,4 +112,20 @@ public interface IHotakeAiCommonToolsService {
public HotakeRolesInfoDto getRoleLinkAnalysis(HotakeRoleLinkAnalysisVo roleLinkAnalysisVo);
/**
* 招聘链接信息分析补全(按照步骤生成)
* @param rolesInfoDto 岗位信息
* @return
*/
public HotakeRolesInfoDto getRoleInfoAnalysisSetUp(HotakeRolesInfoDto rolesInfoDto);
/**
* AI面试问题生成
* @param rolesInfo 岗位信息
* @return
*/
public String getAiInterviewQuestions(HotakeRolesInfo rolesInfo);
}

View File

@@ -1,12 +1,15 @@
package com.vetti.hotake.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.vetti.common.ai.gpt.ChatGPTClient;
import com.vetti.common.constant.AiCommonPromptConstants;
import com.vetti.common.core.domain.entity.SysUser;
import com.vetti.common.core.service.BaseServiceImpl;
import com.vetti.common.enums.FillTypeEnum;
import com.vetti.common.enums.RoleBenefitsEnum;
import com.vetti.common.utils.SecurityUtils;
import com.vetti.common.utils.html.ReadHtmlByOkHttp;
import com.vetti.hotake.domain.HotakeInitScreQuestionsReplyRecordInfo;
@@ -765,7 +768,7 @@ public class HotakeAiCommonToolsServiceImpl extends BaseServiceImpl implements I
String resultJson = "{\"jobTitle\":\"Senior Software Engineer\",\"companyName\":\"Tech Innovation Corp\",\"jobType\":null}";
log.info("招聘链接信息提取:{}",resultJson);
//处理岗位信息补充
String userPrompt_1 = "请根据以下提取的岗位信息生成完整的API格式数据\\n\\n" +resultJson;
String userPrompt_1 = "Please generate complete API-formatted data based on the extracted job information below\\n\\n" +resultJson;
List<Map<String, String>> listOne = new LinkedList();
Map<String, String> mapEntityOne = new HashMap<>();
mapEntityOne.put("role", "system");
@@ -798,25 +801,192 @@ public class HotakeAiCommonToolsServiceImpl extends BaseServiceImpl implements I
hotakeRolesInfoDto.setResponsibilities(dataMap.get("responsibilities").toString());
//岗位所需技能信息
List<RequiredSkillsDto> requiredSkillsList = (List<RequiredSkillsDto>)dataMap.get("requiredSkills");
List<RequiredSkillsDto> requiredSkillsList = (List<RequiredSkillsDto>)dataMap.get("requiredSkillsList");
hotakeRolesInfoDto.setRequiredSkillsList(requiredSkillsList);
hotakeRolesInfoDto.setRequiredSkillsJson(JSONUtil.toJsonStr(requiredSkillsList));
List<NiceToHaveSkillsDto> niceToHaveSkillsList = (List<NiceToHaveSkillsDto>)dataMap.get("niceToHaveSkillsList");
hotakeRolesInfoDto.setNiceToHaveSkillsList(niceToHaveSkillsList);
List<RoleBenefitsDto> roleBenefitsList = (List<RoleBenefitsDto>)dataMap.get("roleBenefitsList");
hotakeRolesInfoDto.setNiceToHaveSkillsJson(JSONUtil.toJsonStr(niceToHaveSkillsList));
List<Map> roleBenefitsMapList = (List<Map>)dataMap.get("roleBenefitsList");
List<RoleBenefitsDto> roleBenefitsList = new ArrayList<>();
if(CollectionUtil.isNotEmpty(roleBenefitsMapList)){
for(Map dto : roleBenefitsMapList){
//福利转换
RoleBenefitsDto dto1 = new RoleBenefitsDto();
if(ObjectUtil.isNotEmpty(dto.get("keyValue"))){
RoleBenefitsEnum benefitsEnum = RoleBenefitsEnum.getByCode(dto.get("keyValue").toString());
if(benefitsEnum != null){
dto1.setKeyValue(benefitsEnum.getCode());
roleBenefitsList.add(dto1);
}
}
}
}
hotakeRolesInfoDto.setRoleBenefitsList(roleBenefitsList);
hotakeRolesInfoDto.setRoleBenefitsJson(JSONUtil.toJsonStr(roleBenefitsList));
Map educationRequirementsMap = (Map)dataMap.get("educationRequirements");
EducationRequirementsDto educationRequirements = new EducationRequirementsDto();
educationRequirements.setAcademicMajor(educationRequirementsMap.get("academicMajor").toString());
educationRequirements.setDegree(educationRequirementsMap.get("degree").toString());
hotakeRolesInfoDto.setEducationRequirements(educationRequirements);
hotakeRolesInfoDto.setEducationRequirementsJson(JSONUtil.toJsonStr(educationRequirements));
List<CertificationsLicensesDto> certificationsLicensesList = (List<CertificationsLicensesDto>)dataMap.get("certificationsLicensesList");
hotakeRolesInfoDto.setCertificationsLicensesList(certificationsLicensesList);
hotakeRolesInfoDto.setCertificationsLicensesJson(JSONUtil.toJsonStr(certificationsLicensesList));
fill(FillTypeEnum.INSERT.getCode(),hotakeRolesInfoDto);
hotakeRolesInfoMapper.insertHotakeRolesInfo(hotakeRolesInfoDto);
return hotakeRolesInfoDto;
}
/**
* 招聘链接信息分析补全
* @param roleLinkAnalysisVo 岗位链接对象
* @return
*/
@Override
public HotakeRolesInfoDto getRoleInfoAnalysisSetUp(HotakeRolesInfoDto rolesInfoDto) {
String prompt = AiCommonPromptConstants.initializationRoleLinkAnalysisPrompt();
String resultJson = "{\n" +
" \"jobTitle\": "+rolesInfoDto.getRoleName()+",\n" +
" \"companyName\": "+rolesInfoDto.getCompanyName()+",\n" +
" \"location\": "+rolesInfoDto.getLocations()+",\n" +
" \"salaryRange\": "+rolesInfoDto.getSalaryStart()+"-"+rolesInfoDto.getSalaryEnd()+",\n" +
" \"jobType\": "+rolesInfoDto.getJobType()+",\n" +
" \"experience\": "+rolesInfoDto.getJobExperience()+",\n" +
" \"education\": "+JSONUtil.toJsonStr(rolesInfoDto.getEducationRequirements())+",\n" +
" \"skills\": "+JSONUtil.toJsonStr(rolesInfoDto.getRequiredSkillsList())+",\n" +
" \"description\": "+rolesInfoDto.getAboutRole()+"\n" +
"}";
log.info("招聘链接信息提取:{}",resultJson);
//处理岗位信息补充
String userPrompt_1 = "Please generate complete API-formatted data based on the extracted job information below\\n\\n" +resultJson;
List<Map<String, String>> listOne = new LinkedList();
Map<String, String> mapEntityOne = new HashMap<>();
mapEntityOne.put("role", "system");
mapEntityOne.put("content",prompt);
listOne.add(mapEntityOne);
Map<String, String> mapUserEntityOne = new HashMap<>();
mapUserEntityOne.put("role", "user");
mapUserEntityOne.put("content",userPrompt_1);
listOne.add(mapUserEntityOne);
String promptJsonOne = JSONUtil.toJsonStr(listOne);
String resultStrOne = chatGPTClient.handleAiChat(promptJsonOne,"RLINKAL");
String resultJsonOne = resultStrOne.replaceAll("```json","").replaceAll("```","");
log.info("招聘信息补全:{}",resultJsonOne);
HotakeRolesInfoDto hotakeRolesInfoDto = new HotakeRolesInfoDto();
Map resultMap = JSONUtil.toBean(resultJsonOne,Map.class);
Map dataMap = (Map)resultMap.get("data");
hotakeRolesInfoDto.setRoleName(dataMap.get("jobTitle").toString());
hotakeRolesInfoDto.setCompanyName(dataMap.get("companyName").toString());
hotakeRolesInfoDto.setLocations(dataMap.get("location").toString());
hotakeRolesInfoDto.setSalaryStart(new BigDecimal(dataMap.get("salaryStart").toString()));
hotakeRolesInfoDto.setSalaryEnd(new BigDecimal(dataMap.get("salaryEnd").toString()));
hotakeRolesInfoDto.setJobType(dataMap.get("jobType").toString().toLowerCase());
hotakeRolesInfoDto.setJobLevel(dataMap.get("jobLevel").toString().toLowerCase());
hotakeRolesInfoDto.setJobExperience("5");
hotakeRolesInfoDto.setLocationType(dataMap.get("locationType").toString().toLowerCase());
hotakeRolesInfoDto.setAboutRole(dataMap.get("aboutRole").toString());
hotakeRolesInfoDto.setResponsibilities(dataMap.get("responsibilities").toString());
//岗位所需技能信息
List<RequiredSkillsDto> requiredSkillsList = (List<RequiredSkillsDto>)dataMap.get("requiredSkillsList");
hotakeRolesInfoDto.setRequiredSkillsList(requiredSkillsList);
hotakeRolesInfoDto.setRequiredSkillsJson(JSONUtil.toJsonStr(requiredSkillsList));
List<NiceToHaveSkillsDto> niceToHaveSkillsList = (List<NiceToHaveSkillsDto>)dataMap.get("niceToHaveSkillsList");
hotakeRolesInfoDto.setNiceToHaveSkillsList(niceToHaveSkillsList);
hotakeRolesInfoDto.setNiceToHaveSkillsJson(JSONUtil.toJsonStr(niceToHaveSkillsList));
List<Map> roleBenefitsMapList = (List<Map>)dataMap.get("roleBenefitsList");
List<RoleBenefitsDto> roleBenefitsList = new ArrayList<>();
if(CollectionUtil.isNotEmpty(roleBenefitsMapList)){
for(Map dto : roleBenefitsMapList){
//福利转换
RoleBenefitsDto dto1 = new RoleBenefitsDto();
if(ObjectUtil.isNotEmpty(dto.get("keyValue"))){
RoleBenefitsEnum benefitsEnum = RoleBenefitsEnum.getByCode(dto.get("keyValue").toString());
if(benefitsEnum != null){
dto1.setKeyValue(benefitsEnum.getCode());
roleBenefitsList.add(dto1);
}
}
}
}
hotakeRolesInfoDto.setRoleBenefitsList(roleBenefitsList);
hotakeRolesInfoDto.setRoleBenefitsJson(JSONUtil.toJsonStr(roleBenefitsList));
Map educationRequirementsMap = (Map)dataMap.get("educationRequirements");
EducationRequirementsDto educationRequirements = new EducationRequirementsDto();
educationRequirements.setAcademicMajor(educationRequirementsMap.get("academicMajor").toString());
educationRequirements.setDegree(educationRequirementsMap.get("degree").toString());
hotakeRolesInfoDto.setEducationRequirements(educationRequirements);
hotakeRolesInfoDto.setEducationRequirementsJson(JSONUtil.toJsonStr(educationRequirements));
List<CertificationsLicensesDto> certificationsLicensesList = (List<CertificationsLicensesDto>)dataMap.get("certificationsLicensesList");
hotakeRolesInfoDto.setCertificationsLicensesList(certificationsLicensesList);
hotakeRolesInfoDto.setCertificationsLicensesJson(JSONUtil.toJsonStr(certificationsLicensesList));
return hotakeRolesInfoDto;
}
/**
* AI面试问题生成
* @param rolesInfo 岗位信息
* @return
*/
@Override
public String getAiInterviewQuestions(HotakeRolesInfo rolesInfo) {
String prompt = AiCommonPromptConstants.initializationAiInterviewQuestionsPrompt();
String resultJson = "Please generate AI interview questions based on the following job description\n" +
"\n" +
"**Job Information**\n" +
"职位名称:【】\n" +
"技术要求:【】\n" +
"经验要求:【】\n" +
"面试时长:【】\n" +
"公司文化:【】\n" +
"特殊要求:【】\n" +
"`;";
log.info("招聘链接信息提取:{}",resultJson);
//处理岗位信息补充
String userPrompt_1 = "Please generate complete API-formatted data based on the extracted job information below\\n\\n" +resultJson;
List<Map<String, String>> listOne = new LinkedList();
Map<String, String> mapEntityOne = new HashMap<>();
mapEntityOne.put("role", "system");
mapEntityOne.put("content",prompt);
listOne.add(mapEntityOne);
Map<String, String> mapUserEntityOne = new HashMap<>();
mapUserEntityOne.put("role", "user");
mapUserEntityOne.put("content",userPrompt_1);
listOne.add(mapUserEntityOne);
String promptJsonOne = JSONUtil.toJsonStr(listOne);
String resultStrOne = chatGPTClient.handleAiChat(promptJsonOne,"RLINKAL");
String resultJsonOne = resultStrOne.replaceAll("```json","").replaceAll("```","");
log.info("招聘信息补全:{}",resultJsonOne);
return "";
}
}

View File

@@ -10,10 +10,7 @@ import com.vetti.common.config.RuoYiConfig;
import com.vetti.common.core.service.BaseServiceImpl;
import com.vetti.common.enums.FillTypeEnum;
import com.vetti.common.enums.MinioBucketNameEnum;
import com.vetti.common.utils.HtmlToPdfUtil;
import com.vetti.common.utils.MarkdownUtil;
import com.vetti.common.utils.MultipartFileUtil;
import com.vetti.common.utils.SecurityUtils;
import com.vetti.common.utils.*;
import com.vetti.common.utils.readFile.FileContentUtil;
import com.vetti.hotake.domain.HotakeCvInfo;
import com.vetti.hotake.domain.HotakeProblemBaseInfo;
@@ -87,23 +84,23 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
@Override
public HotakeCvInfo selectHotakeCvInfoById(Long id) {
HotakeCvInfo cvInfo = hotakeCvInfoMapper.selectHotakeCvInfoById(id);
if(StrUtil.isNotEmpty(cvInfo.getCvTemplateJson())){
HotakeCvInfoDto cvInfoDto = JSONUtil.toBean(cvInfo.getCvTemplateJson(),HotakeCvInfoDto.class);
if (StrUtil.isNotEmpty(cvInfo.getCvTemplateJson())) {
HotakeCvInfoDto cvInfoDto = JSONUtil.toBean(cvInfo.getCvTemplateJson(), HotakeCvInfoDto.class);
cvInfo.setCvInfoDto(cvInfoDto);
}
if(StrUtil.isNotEmpty(cvInfo.getCvOptimizeJson())){
if (StrUtil.isNotEmpty(cvInfo.getCvOptimizeJson())) {
//解析优化简历
HotakeCvOptimizeDto cvOptimizeDto = JSONUtil.toBean(cvInfo.getCvOptimizeJson(),HotakeCvOptimizeDto.class);
if(cvOptimizeDto != null){
HotakeCvOptimizeDto cvOptimizeDto = JSONUtil.toBean(cvInfo.getCvOptimizeJson(), HotakeCvOptimizeDto.class);
if (cvOptimizeDto != null) {
cvInfo.setCvOptimizeDto(cvOptimizeDto);
if(cvOptimizeDto.getTextCorrections() != null && CollectionUtil.isNotEmpty(cvOptimizeDto.getTextCorrections().getFormattingIssues())){
if (cvOptimizeDto.getTextCorrections() != null && CollectionUtil.isNotEmpty(cvOptimizeDto.getTextCorrections().getFormattingIssues())) {
cvInfo.setTextCorrectionsNums(cvOptimizeDto.getTextCorrections().getFormattingIssues().size());
}else{
} else {
cvInfo.setTextCorrectionsNums(0);
}
if(cvOptimizeDto.getLogicCorrections() != null && CollectionUtil.isNotEmpty(cvOptimizeDto.getLogicCorrections().getCareerProgressionDtos())){
if (cvOptimizeDto.getLogicCorrections() != null && CollectionUtil.isNotEmpty(cvOptimizeDto.getLogicCorrections().getCareerProgressionDtos())) {
cvInfo.setLogicCorrectionsNum(cvOptimizeDto.getTextCorrections().getFormattingIssues().size());
}else{
} else {
cvInfo.setLogicCorrectionsNum(0);
}
}
@@ -111,7 +108,7 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
HotakeProblemBaseInfo query = new HotakeProblemBaseInfo();
query.setCvId(id);
List<HotakeProblemBaseInfo> problemBaseInfoList = hotakeProblemBaseInfoService.selectHotakeProblemBaseInfoList(query);
if(CollectionUtil.isNotEmpty(problemBaseInfoList)){
if (CollectionUtil.isNotEmpty(problemBaseInfoList)) {
cvInfo.setProblemBaseInfo(problemBaseInfoList.get(0));
}
return cvInfo;
@@ -127,9 +124,9 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
@Override
public List<HotakeCvInfo> selectHotakeCvInfoList(HotakeCvInfo hotakeCvInfo) {
List<HotakeCvInfo> cvInfoList = hotakeCvInfoMapper.selectHotakeCvInfoList(hotakeCvInfo);
if(CollectionUtil.isNotEmpty(cvInfoList)){
for(HotakeCvInfo cvInfo : cvInfoList){
HotakeCvInfoDto cvInfoDto = JSONUtil.toBean(cvInfo.getCvTemplateJson(),HotakeCvInfoDto.class);
if (CollectionUtil.isNotEmpty(cvInfoList)) {
for (HotakeCvInfo cvInfo : cvInfoList) {
HotakeCvInfoDto cvInfoDto = JSONUtil.toBean(cvInfo.getCvTemplateJson(), HotakeCvInfoDto.class);
cvInfo.setCvInfoDto(cvInfoDto);
//返回问题记录
}
@@ -146,9 +143,9 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
@Transactional(rollbackFor = Exception.class)
@Override
public HotakeCvInfo insertHotakeCvInfo(HotakeCvInfo hotakeCvInfo) {
if("cv".equals(hotakeCvInfo.getCvFileType())){
if ("cv".equals(hotakeCvInfo.getCvFileType())) {
//删除原先的CV简历信息
hotakeCvInfoMapper.deleteHotakeCvInfoByUserIdAndType(SecurityUtils.getUserId(),"cv");
hotakeCvInfoMapper.deleteHotakeCvInfoByUserIdAndType(SecurityUtils.getUserId(), "cv");
}
fill(FillTypeEnum.INSERT.getCode(), hotakeCvInfo);
String fileSuffix = FileUtil.getSuffix(hotakeCvInfo.getCvUrl());
@@ -156,9 +153,9 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
fileSuffix = fileSuffix.toLowerCase();
}
hotakeCvInfo.setCvFileSuffix(fileSuffix);
if(!"cv".equals(hotakeCvInfo.getCvFileType())){
if (!"cv".equals(hotakeCvInfo.getCvFileType())) {
//对附件进行基础数据的解析
String resultStr = handleAnalyzedAttachment(hotakeCvInfo.getCvUrl(),fileSuffix);
String resultStr = handleAnalyzedAttachment(hotakeCvInfo.getCvUrl(), fileSuffix);
hotakeCvInfo.setAnalyzedAttachmentJson(resultStr);
}
hotakeCvInfoMapper.insertHotakeCvInfo(hotakeCvInfo);
@@ -172,7 +169,7 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
* @return
*/
@Override
public HotakeProblemBaseInfo handleHotakeCvInfo(HotakeCvInfoDto cvInfoDto,Long cvId) {
public HotakeProblemBaseInfo handleHotakeCvInfo(HotakeCvInfoDto cvInfoDto, Long cvId) {
log.info("处理简历信息");
HotakeProblemBaseInfo problemBaseInfo = new HotakeProblemBaseInfo();
try {
@@ -203,7 +200,7 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
}
//生成预设问题记录
//先删除该用户的预设问题
hotakeProblemBaseInfoService.deleteHotakeProblemBaseInfoByUserId(SecurityUtils.getUserId(),cvId);
hotakeProblemBaseInfoService.deleteHotakeProblemBaseInfoByUserId(SecurityUtils.getUserId(), cvId);
problemBaseInfo.setUserId(SecurityUtils.getUserId());
problemBaseInfo.setContents(resultMsg);
problemBaseInfo.setQuestionNums(questionNums);
@@ -231,7 +228,6 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
//修改的时候重新生成问题和评分
hotakeCvInfoMapper.updateHotakeCvInfo(hotakeCvInfo);
return hotakeCvInfo;
@@ -275,6 +271,7 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
/**
* 简历解析以及生成问题和评分
*
* @param hotakeCvInfo 简历信息
* @return
*/
@@ -290,10 +287,10 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
.object(cvInfo.getCvUrl())
.build());
String contents = FileContentUtil.readFileContent(inputStream, cvInfo.getCvFileSuffix());
log.info("简历信息:{}",contents);
log.info("简历信息:{}", contents);
//验证文件内容是否改变,如果未改变不进行模型解析直接返回原有结果
String md5Hash = MD5.create().digestHex16(contents);
if(StrUtil.isNotEmpty(md5Hash) && md5Hash.equals(cvInfo.getCvMd5())){
if (StrUtil.isNotEmpty(md5Hash) && md5Hash.equals(cvInfo.getCvMd5())) {
//直接返回简历结果
return cvInfo;
}
@@ -302,21 +299,21 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
HotakeCvInfoDto cvInfoDto = handleAnalysisCvInfo(contents);
cvInfo.setCvInfoDto(cvInfoDto);
//对简历数据进行处理生成相应的题库数据
HotakeProblemBaseInfo problemBaseInfo = handleHotakeCvInfo(cvInfoDto,hotakeCvInfo.getId());
HotakeProblemBaseInfo problemBaseInfo = handleHotakeCvInfo(cvInfoDto, hotakeCvInfo.getId());
cvInfo.setProblemBaseInfo(problemBaseInfo);
//解析优化简历
HotakeCvOptimizeDto cvOptimizeDto = aiCommonToolsService.getResumeAnalysisOptimizer(contents);
if(cvOptimizeDto != null){
if (cvOptimizeDto != null) {
cvInfo.setCvOptimizeJson(JSONUtil.toJsonStr(cvOptimizeDto));
if(cvOptimizeDto.getTextCorrections() != null && CollectionUtil.isNotEmpty(cvOptimizeDto.getTextCorrections().getFormattingIssues())){
if (cvOptimizeDto.getTextCorrections() != null && CollectionUtil.isNotEmpty(cvOptimizeDto.getTextCorrections().getFormattingIssues())) {
cvInfo.setTextCorrectionsNums(cvOptimizeDto.getTextCorrections().getFormattingIssues().size());
}else{
} else {
cvInfo.setTextCorrectionsNums(0);
}
if(cvOptimizeDto.getLogicCorrections() != null && CollectionUtil.isNotEmpty(cvOptimizeDto.getLogicCorrections().getCareerProgressionDtos())){
if (cvOptimizeDto.getLogicCorrections() != null && CollectionUtil.isNotEmpty(cvOptimizeDto.getLogicCorrections().getCareerProgressionDtos())) {
cvInfo.setLogicCorrectionsNum(cvOptimizeDto.getTextCorrections().getFormattingIssues().size());
}else{
} else {
cvInfo.setLogicCorrectionsNum(0);
}
cvInfo.setCvOptimizeDto(cvOptimizeDto);
@@ -352,6 +349,7 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
/**
* 根据其他的附件信息生成个人简历
*
* @return
*/
@Override
@@ -365,46 +363,76 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
String basicInformation = "";
//其他的附件信息集合
String attachmentContent = "";
if(CollectionUtil.isNotEmpty(cvInfoList)){
List<HotakeCvInfo> cvInfos = cvInfoList.stream().filter(e->"letter".equals(e.getCvFileType())).toList();
if(CollectionUtil.isNotEmpty(cvInfos)){
if (CollectionUtil.isNotEmpty(cvInfoList)) {
List<HotakeCvInfo> cvInfos = cvInfoList.stream().filter(e -> "letter".equals(e.getCvFileType())).toList();
if (CollectionUtil.isNotEmpty(cvInfos)) {
basicInformation = cvInfos.get(0).getAnalyzedAttachmentJson();
}
List<HotakeCvInfo> cvInfoOnes = cvInfoList.stream().filter(e->!"letter".equals(e.getCvFileType())
&& "cv".equals(e.getCvFileType())).toList();
if(CollectionUtil.isNotEmpty(cvInfoOnes)){
List<HotakeCvInfo> cvInfoOnes = cvInfoList.stream().filter(e -> !"letter".equals(e.getCvFileType())
&& !"cv".equals(e.getCvFileType())).toList();
if (CollectionUtil.isNotEmpty(cvInfoOnes)) {
List<Map> attachmentContentList = new ArrayList<>();
for (HotakeCvInfo hotakeCvInfo : cvInfoOnes) {
if(StrUtil.isNotEmpty(hotakeCvInfo.getAnalyzedAttachmentJson())){
if (StrUtil.isNotEmpty(hotakeCvInfo.getAnalyzedAttachmentJson())) {
Map map = JSONUtil.toBean(hotakeCvInfo.getAnalyzedAttachmentJson(), Map.class);
attachmentContentList.add(map);
}
}
if(CollectionUtil.isNotEmpty(attachmentContentList)){
if (CollectionUtil.isNotEmpty(attachmentContentList)) {
attachmentContent = JSONUtil.toJsonStr(attachmentContentList);
}
}
}
String cvData = aiCommonToolsService.handleAttachmentResultMerging(basicInformation,attachmentContent);
String cvData = aiCommonToolsService.handleAttachmentResultMerging(basicInformation, attachmentContent);
//进行简历数据组合
Map dataMap = JSONUtil.toBean(cvData, Map.class);
HotakeCvInfoDto cvInfoDto = new HotakeCvInfoDto();
//个人基本信息
Map personalInfoMap = (Map)dataMap.get("personal_info");
cvInfoDto.setName(personalInfoMap.get("name").toString());
Map contactDetailsMap = (Map)personalInfoMap.get("contact_details");
cvInfoDto.setPhone(contactDetailsMap.get("phone").toString());
cvInfoDto.setEmail(contactDetailsMap.get("email").toString());
cvInfoDto.setLocation(contactDetailsMap.get("address").toString());
//个人介绍
cvInfoDto.setAbout(dataMap.get("professional_summary").toString());
Map personalInfoMap = (Map) dataMap.get("personal_info");
if (personalInfoMap != null) {
cvInfoDto.setName(StringUtils.getObjectStr(personalInfoMap.get("full_name")));
Map contactDetailsMap = (Map) personalInfoMap.get("contact_details");
if (contactDetailsMap != null) {
cvInfoDto.setPhone(StringUtils.getObjectStr(contactDetailsMap.get("phone")));
cvInfoDto.setEmail(StringUtils.getObjectStr(contactDetailsMap.get("email")));
cvInfoDto.setLocation(StringUtils.getObjectStr(contactDetailsMap.get("address")));
} else {
cvInfoDto.setPhone(StringUtils.getObjectStr(personalInfoMap.get("phone")));
cvInfoDto.setEmail(StringUtils.getObjectStr(personalInfoMap.get("email")));
cvInfoDto.setLocation(StringUtils.getObjectStr(personalInfoMap.get("address")));
}
}
//个人介绍
cvInfoDto.setAbout(StringUtils.getObjectStr(dataMap.get("professional_summary")));
try{
//技能工具
Map skillsCertificatesMap = (Map)dataMap.get("skills_certificates");
List<String> skillsList = (List<String>)skillsCertificatesMap.get("skills");
List<Map> skillsList = (List<Map>) dataMap.get("skills_certificates");
if(CollectionUtil.isNotEmpty(skillsList)){
Map map = skillsList.get(0);
if(map != null){
List<String> skillsAllList = (List<String>) map.get("skills");
//技能工具
List<VcSkillsToolsDto> skillsTools = new ArrayList<>();
if (CollectionUtil.isNotEmpty(skillsAllList)) {
for (String skill : skillsAllList) {
VcSkillsToolsDto toolsDto = new VcSkillsToolsDto();
toolsDto.setContent(skill);
skillsTools.add(toolsDto);
}
}
cvInfoDto.setSkillsTools(skillsTools);
}
}
}catch (Exception e){
//技能工具
List<String> skillsList = (List<String>) dataMap.get("skills_certificates");
if(CollectionUtil.isNotEmpty(skillsList)){
//技能工具
List<VcSkillsToolsDto> skillsTools = new ArrayList<>();
if (CollectionUtil.isNotEmpty(skillsList)) {
for (String skill : skillsList) {
VcSkillsToolsDto toolsDto = new VcSkillsToolsDto();
toolsDto.setContent(skill);
@@ -412,26 +440,27 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
}
}
cvInfoDto.setSkillsTools(skillsTools);
}
}
//工作经验集合
List<VcExperienceDto> experience = new ArrayList<>();
List<Map> workExperienceMapList = (List<Map>)dataMap.get("work_experience");
List<Map> workExperienceMapList = (List<Map>) dataMap.get("work_experience");
if (CollectionUtil.isNotEmpty(workExperienceMapList)) {
for (Map map : workExperienceMapList) {
VcExperienceDto experienceDto = new VcExperienceDto();
experienceDto.setCompany(map.get("company").toString());
experienceDto.setTitle(map.get("position").toString());
String time_period = map.get("time_period").toString();
if(StrUtil.isNotEmpty(time_period)){
String[] times = time_period.split("");
experienceDto.setDurationStart(times[0]);
if(times.length > 1){
experienceDto.setDurationEnd(times[1]);
}
}
experienceDto.setCompany(StringUtils.getObjectStr(map.get("company")));
experienceDto.setTitle(StringUtils.getObjectStr(map.get("job_title")));
experienceDto.setLocation(StringUtils.getObjectStr(map.get("location")));
String startDate = StringUtils.getObjectStr(map.get("start_date"));
experienceDto.setDurationStart(startDate);
String endDate = StringUtils.getObjectStr(map.get("end_date"));
experienceDto.setDurationEnd(endDate);
List<VcExperienceDescriptionDto> description = new ArrayList<>();
List<String> responsibilitiesList = (List<String>)map.get("responsibilities");
if(CollectionUtil.isNotEmpty(responsibilitiesList)){
List<String> responsibilitiesList = (List<String>) map.get("responsibilities");
if (CollectionUtil.isNotEmpty(responsibilitiesList)) {
for (String responsibility : responsibilitiesList) {
VcExperienceDescriptionDto descriptionDto = new VcExperienceDescriptionDto();
descriptionDto.setContent(responsibility);
@@ -445,17 +474,17 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
}
//教育经历
List<VcEducationDto> educationList = new ArrayList<>();
List<Map> educationMapList = (List<Map>)dataMap.get("education");
if(CollectionUtil.isNotEmpty(educationMapList)){
for(Map map : educationMapList){
List<Map> educationMapList = (List<Map>) dataMap.get("education");
if (CollectionUtil.isNotEmpty(educationMapList)) {
for (Map map : educationMapList) {
VcEducationDto educationDto = new VcEducationDto();
educationDto.setDegree(map.get("degree").toString());
educationDto.setInstitution(map.get("school").toString());
String time_period = map.get("time_period").toString();
if(StrUtil.isNotEmpty(time_period)){
educationDto.setDegree(StringUtils.getObjectStr(map.get("degree")));
educationDto.setInstitution(StringUtils.getObjectStr(map.get("institution")));
String time_period = StringUtils.getObjectStr(map.get("graduation_date"));
if (StrUtil.isNotEmpty(time_period)) {
String[] times = time_period.split("");
educationDto.setDurationStart(times[0]);
if(times.length > 1){
if (times.length > 1) {
educationDto.setDurationEnd(times[1]);
}
}
@@ -473,7 +502,7 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
cvInfo.setStatus("1");
//生成简历PDF数据
String markdown = aiCommonToolsService.handleGenerateMarkdown(cvData);
markdown = markdown.replaceAll("markdown","");
markdown = markdown.replaceAll("markdown", "");
try {
String html = MarkdownUtil.markdownToHtml(markdown);
// 可注入 CSS
@@ -501,14 +530,15 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
MultipartFileUtil.fileToMultipartFile(pdf);
HotakeSysFileVo fileVo = new HotakeSysFileVo();
fileVo.setMinioBucketName("cv-fs");
HotakeSysFile sysFile = sysFileService.insertHotakeSysFile(multipartFile,fileVo);
HotakeSysFile sysFile = sysFileService.insertHotakeSysFile(multipartFile, fileVo);
cvInfo.setFileSizeShow(sysFile.getFileSizeShow());
cvInfo.setCvUrl(sysFile.getStoragePath());
//创建简历对象数据
insertHotakeCvInfo(cvInfo);
//保存简历PDF附件数据
// log.info("Markdown数据为:{}",markdown);
}catch (Exception e){}
} catch (Exception e) {
}
return cvInfo;
}
@@ -522,81 +552,81 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
private HotakeCvInfoDto handleAnalysisCvInfo(String contents) {
log.info("开始简历解析");
try {
List<Map<String,String>> list = new LinkedList();
Map<String,String> entity = new HashMap<>();
entity.put("role","user");
entity.put("content","从下面提供的文本中提取所有能识别到的简历信息只提取原文中存在的内容不要补充、不推测、不总结。需要提取的字段包括name姓名或人名、phone电话号码、email电子邮件地址、experienceYear根据工作经验计算出来的工作年限、position岗位或者简历中自己期望的职位等、location地点或者地址、家庭住址、links所有链接地址、currentWork(当前工作公司)、about关于我/自我介绍、skills_tools关键资格许可证、注册/会员资格、认证、languages语言能力,主要就是Languages下面的语言、experience工作经历,除了title、company、location、durationStart、durationEnd,其他的都放到description里面,并且description里面要根据换行符分成不同的content),日期要拆分成开始时间(durationStart)和结束时间(durationEnd)、education教育经历,日期要拆分成开始时间(durationStart)和结束时间(durationEnd))。请将提取结果以结构化 JSON 格式返回,格式如下:{ \\\"name\\\": \\\"\\\", \\\"phone\\\": \\\"\\\", \\\"currentWork\\\": \\\"\\\", \\\"position\\\": \\\"\\\", \\\"location\\\": \\\"\\\", \\\"email\\\": \\\"\\\", \\\"experienceYear\\\": \\\"\\\", \\\"links\\\": [{\\\"content\\\":\\\"\\\"}], \\\"about\\\": \\\"\\\", \\\"skillsTools\\\": [{\\\"content\\\":\\\"\\\"}], \\\"languages\\\": [{\\\"content\\\":\\\"\\\"}], \\\"experience\\\": [{\\\"title\\\": \\\"\\\", \\\"company\\\": \\\"\\\",\\\"location\\\": \\\"\\\",\\\"durationStart\\\": \\\"\\\",\\\"durationEnd\\\": \\\"\\\",\\\"description\\\": [{\\\"content\\\":\\\"\\\"}]}], \\\"education\\\": [{\\\"degree\\\": \\\"\\\",\\\"institution\\\": \\\"\\\",\\\"durationStart\\\": \\\"\\\",\\\"durationEnd\\\": \\\"\\\"}] }。字段不存在则返回 null 或空数组。只返回标准可解析的 JSON结构 ,不要多余的```json等信息不要解释说明不要改写内容。以下为待处理文本"+contents);
List<Map<String, String>> list = new LinkedList();
Map<String, String> entity = new HashMap<>();
entity.put("role", "user");
entity.put("content", "从下面提供的文本中提取所有能识别到的简历信息只提取原文中存在的内容不要补充、不推测、不总结。需要提取的字段包括name姓名或人名、phone电话号码、email电子邮件地址、experienceYear根据工作经验计算出来的工作年限、position岗位或者简历中自己期望的职位等、location地点或者地址、家庭住址、links所有链接地址、currentWork(当前工作公司)、about关于我/自我介绍、skills_tools关键资格许可证、注册/会员资格、认证、languages语言能力,主要就是Languages下面的语言、experience工作经历,除了title、company、location、durationStart、durationEnd,其他的都放到description里面,并且description里面要根据换行符分成不同的content),日期要拆分成开始时间(durationStart)和结束时间(durationEnd)、education教育经历,日期要拆分成开始时间(durationStart)和结束时间(durationEnd))。请将提取结果以结构化 JSON 格式返回,格式如下:{ \\\"name\\\": \\\"\\\", \\\"phone\\\": \\\"\\\", \\\"currentWork\\\": \\\"\\\", \\\"position\\\": \\\"\\\", \\\"location\\\": \\\"\\\", \\\"email\\\": \\\"\\\", \\\"experienceYear\\\": \\\"\\\", \\\"links\\\": [{\\\"content\\\":\\\"\\\"}], \\\"about\\\": \\\"\\\", \\\"skillsTools\\\": [{\\\"content\\\":\\\"\\\"}], \\\"languages\\\": [{\\\"content\\\":\\\"\\\"}], \\\"experience\\\": [{\\\"title\\\": \\\"\\\", \\\"company\\\": \\\"\\\",\\\"location\\\": \\\"\\\",\\\"durationStart\\\": \\\"\\\",\\\"durationEnd\\\": \\\"\\\",\\\"description\\\": [{\\\"content\\\":\\\"\\\"}]}], \\\"education\\\": [{\\\"degree\\\": \\\"\\\",\\\"institution\\\": \\\"\\\",\\\"durationStart\\\": \\\"\\\",\\\"durationEnd\\\": \\\"\\\"}] }。字段不存在则返回 null 或空数组。只返回标准可解析的 JSON结构 ,不要多余的```json等信息不要解释说明不要改写内容。以下为待处理文本" + contents);
//根据AI做
list.add(entity);
String resultCv = chatGPTClient.handleAiChat(JSONUtil.toJsonStr(list), "JX");
log.info("开始返回简历解析结果:{}", resultCv);
HotakeCvInfoDto cvInfoDto = JSONUtil.toBean(resultCv, HotakeCvInfoDto.class);
//数据添加默认值
if(cvInfoDto != null){
if(StrUtil.isEmpty(cvInfoDto.getName())){
if (cvInfoDto != null) {
if (StrUtil.isEmpty(cvInfoDto.getName())) {
cvInfoDto.setName("-");
}
if(StrUtil.isEmpty(cvInfoDto.getPhone())){
if (StrUtil.isEmpty(cvInfoDto.getPhone())) {
cvInfoDto.setPhone("-");
}
if(StrUtil.isEmpty(cvInfoDto.getEmail())){
if (StrUtil.isEmpty(cvInfoDto.getEmail())) {
cvInfoDto.setEmail("-");
}
if(StrUtil.isEmpty(cvInfoDto.getPosition())){
if (StrUtil.isEmpty(cvInfoDto.getPosition())) {
cvInfoDto.setPosition("-");
}
if(StrUtil.isEmpty(cvInfoDto.getLocation())){
if (StrUtil.isEmpty(cvInfoDto.getLocation())) {
cvInfoDto.setLocation("-");
}
if(StrUtil.isEmpty(cvInfoDto.getAbout())){
if (StrUtil.isEmpty(cvInfoDto.getAbout())) {
cvInfoDto.setAbout("-");
}
if (StrUtil.isEmpty(cvInfoDto.getCurrentWork())){
if (StrUtil.isEmpty(cvInfoDto.getCurrentWork())) {
cvInfoDto.setCurrentWork("-");
}
if(CollectionUtil.isNotEmpty(cvInfoDto.getSkillsTools())){
for (VcSkillsToolsDto toolsDto :cvInfoDto.getSkillsTools()) {
if(StrUtil.isEmpty(toolsDto.getContent())){
if (CollectionUtil.isNotEmpty(cvInfoDto.getSkillsTools())) {
for (VcSkillsToolsDto toolsDto : cvInfoDto.getSkillsTools()) {
if (StrUtil.isEmpty(toolsDto.getContent())) {
toolsDto.setContent("-");
}
}
}
if(CollectionUtil.isNotEmpty(cvInfoDto.getLinks())){
for (VcLinksDto linksDto :cvInfoDto.getLinks()) {
if(StrUtil.isEmpty(linksDto.getContent())){
if (CollectionUtil.isNotEmpty(cvInfoDto.getLinks())) {
for (VcLinksDto linksDto : cvInfoDto.getLinks()) {
if (StrUtil.isEmpty(linksDto.getContent())) {
linksDto.setContent("-");
}
}
}
if(CollectionUtil.isNotEmpty(cvInfoDto.getLanguages())){
for (VcLanguagesDto languagesDto :cvInfoDto.getLanguages()) {
if(StrUtil.isEmpty(languagesDto.getContent())){
if (CollectionUtil.isNotEmpty(cvInfoDto.getLanguages())) {
for (VcLanguagesDto languagesDto : cvInfoDto.getLanguages()) {
if (StrUtil.isEmpty(languagesDto.getContent())) {
languagesDto.setContent("-");
}
}
}
if(CollectionUtil.isNotEmpty(cvInfoDto.getExperience())){
for (VcExperienceDto experienceDto :cvInfoDto.getExperience()) {
if (StrUtil.isEmpty(experienceDto.getTitle())){
if (CollectionUtil.isNotEmpty(cvInfoDto.getExperience())) {
for (VcExperienceDto experienceDto : cvInfoDto.getExperience()) {
if (StrUtil.isEmpty(experienceDto.getTitle())) {
experienceDto.setTitle("-");
}
if(StrUtil.isEmpty(experienceDto.getCompany())){
if (StrUtil.isEmpty(experienceDto.getCompany())) {
experienceDto.setCompany("-");
}
if(StrUtil.isEmpty(experienceDto.getLocation())){
if (StrUtil.isEmpty(experienceDto.getLocation())) {
experienceDto.setLocation("-");
}
if(StrUtil.isEmpty(experienceDto.getDurationStart())){
if (StrUtil.isEmpty(experienceDto.getDurationStart())) {
experienceDto.setDurationStart("-");
}
if(StrUtil.isEmpty(experienceDto.getDurationEnd())){
if (StrUtil.isEmpty(experienceDto.getDurationEnd())) {
experienceDto.setDurationEnd("-");
}
if (CollectionUtil.isNotEmpty(experienceDto.getDescription())){
for(VcExperienceDescriptionDto descriptionDto :experienceDto.getDescription()){
if(StrUtil.isEmpty(descriptionDto.getContent())){
if (CollectionUtil.isNotEmpty(experienceDto.getDescription())) {
for (VcExperienceDescriptionDto descriptionDto : experienceDto.getDescription()) {
if (StrUtil.isEmpty(descriptionDto.getContent())) {
descriptionDto.setContent("-");
}
@@ -605,31 +635,31 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
}
}
if(CollectionUtil.isNotEmpty(cvInfoDto.getEducation())){
for (VcEducationDto educationDto :cvInfoDto.getEducation()) {
if (StrUtil.isEmpty(educationDto.getDegree())){
if (CollectionUtil.isNotEmpty(cvInfoDto.getEducation())) {
for (VcEducationDto educationDto : cvInfoDto.getEducation()) {
if (StrUtil.isEmpty(educationDto.getDegree())) {
educationDto.setDegree("-");
}
if(StrUtil.isEmpty(educationDto.getInstitution())){
if (StrUtil.isEmpty(educationDto.getInstitution())) {
educationDto.setInstitution("-");
}
if(StrUtil.isEmpty(educationDto.getDurationStart())){
if (StrUtil.isEmpty(educationDto.getDurationStart())) {
educationDto.setDurationStart("-");
}
if(StrUtil.isEmpty(educationDto.getDurationEnd())){
if (StrUtil.isEmpty(educationDto.getDurationEnd())) {
educationDto.setDurationEnd("-");
}
if(educationDto.getCertificate() != null){
if(StrUtil.isEmpty(educationDto.getCertificate().getFileName())){
if (educationDto.getCertificate() != null) {
if (StrUtil.isEmpty(educationDto.getCertificate().getFileName())) {
educationDto.getCertificate().setFileName("-");
}
if (StrUtil.isEmpty(educationDto.getCertificate().getFileSuffix())){
if (StrUtil.isEmpty(educationDto.getCertificate().getFileSuffix())) {
educationDto.getCertificate().setFileSuffix("-");
}
if (StrUtil.isEmpty(educationDto.getCertificate().getFileUrl())){
if (StrUtil.isEmpty(educationDto.getCertificate().getFileUrl())) {
educationDto.getCertificate().setFileUrl("-");
}
if (StrUtil.isEmpty(educationDto.getCertificate().getFileSizeShow())){
if (StrUtil.isEmpty(educationDto.getCertificate().getFileSizeShow())) {
educationDto.getCertificate().setFileSizeShow("-");
}
}
@@ -680,21 +710,22 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
}
public static void main(String[] args){
public static void main(String[] args) {
String str = "Score: 4.8/5\n" +
"Recommendation: Highly recommended candidate - strong match for the role\n" +
"Strengths: Extensive relevant experience, Professional certifications, Strong skill set alignment\n" +
"Concerns: -";
String[] strs = str.split("\n");
System.out.println(strs[0].replaceAll("Score:","").trim().split("/")[0]);
System.out.println(strs[0].replaceAll("Score:", "").trim().split("/")[0]);
}
/**
* 处理分析附件返回对应的json 内容
*
* @return
*/
private String handleAnalyzedAttachment(String fileUrl,String fileSuffix){
private String handleAnalyzedAttachment(String fileUrl, String fileSuffix) {
log.info("开始处理附件,对附件进行解析");
try {
InputStream inputStream = minioClient.getObject(
@@ -704,14 +735,12 @@ public class HotakeCvInfoServiceImpl extends BaseServiceImpl implements IHotakeC
.build());
String contents = FileContentUtil.readFileContent(inputStream, fileSuffix);
String resultStr = aiCommonToolsService.handleAnalyzedAttachment(contents);
log.info("返回的分析附件结果数据为:{}",resultStr);
log.info("返回的分析附件结果数据为:{}", resultStr);
return resultStr;
}catch (Exception e){}
} catch (Exception e) {
}
return "";
}
}