Commit 14c56833 by zwb

新增岗位智推

parent 01791790
Showing with 3469 additions and 53 deletions
...@@ -138,6 +138,20 @@ ...@@ -138,6 +138,20 @@
<artifactId>fastjson2</artifactId> <artifactId>fastjson2</artifactId>
<version>2.0.43</version> <version>2.0.43</version>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.29.1</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>0.29.1</version>
</dependency>
</dependencies> </dependencies>
</project> </project>
package org.dromara.common.core.config;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
/**
* @author jiangxiaoge
* @description
* @data 2025/2/13
**/
public class LlmEmbeddingConfig {
// 使用 volatile 保证在多线程环境下的正确性
private static volatile EmbeddingModel instance;
// 私有构造方法,防止外部实例化
private LlmEmbeddingConfig() {
}
public static EmbeddingModel getEmbeddingModel() {
// 双重检查锁定实现单例
if (instance == null) {
synchronized (LlmEmbeddingConfig.class) {
if (instance == null) {
try {
// 初始化 ChatLanguageModel 实例
instance = OpenAiEmbeddingModel
.builder()
//.baseUrl("https://api.openai.com/v1")
.baseUrl("http://47.253.179.191:6666/v1")
.modelName("text-embedding-3-small")
.apiKey("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")
.build();
} catch (Exception e) {
// 错误处理
e.printStackTrace();
return null;
}
}
}
}
return instance;
}
}
package org.dromara.common.core.config;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import okhttp3.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class QwenClientSingleton {
// 单例 OkHttpClient
private static final OkHttpClient client;
static {
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(100); // 全局最大并发请求数
dispatcher.setMaxRequestsPerHost(50); // 单host最大并发请求数
client = new OkHttpClient.Builder()
.dispatcher(dispatcher)
.connectionPool(new ConnectionPool(32, 30, java.util.concurrent.TimeUnit.MINUTES)) // 连接池配置
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) // 连接超时
.readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) // 读取超时
.writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS) // 写入超时
.retryOnConnectionFailure(true) // 连接失败时重试
.build();
}
// 配置参数
private final String baseUrl;
private final String apiKey;
private final String model;
// 单例
private static volatile QwenClientSingleton instance;
private QwenClientSingleton(String baseUrl, String apiKey, String model) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.model = model;
}
public static QwenClientSingleton getInstance(String baseUrl, String apiKey, String model) {
if (instance == null) {
synchronized (QwenClientSingleton.class) {
if (instance == null) {
instance = new QwenClientSingleton(baseUrl, apiKey, model);
}
}
}
return instance;
}
// 核心调用方法
public String chat(String systemMessage, String userMessage) throws IOException {
JSONObject bodyJson = new JSONObject(Map.of(
"model", model,
"messages", List.of(
Map.of("role", "system", "content", systemMessage),
Map.of("role", "user", "content", userMessage)
),
"temperature", 0.0,
"top_p", 1.0,
"chat_template_kwargs", Map.of("enable_thinking", false)
));
Request request = new Request.Builder()
.url(baseUrl + "/v1/chat/completions")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer " + apiKey)
.post(RequestBody.create(bodyJson.toJSONString(), MediaType.get("application/json; charset=utf-8")))
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
JSONObject respJson = JSON.parseObject(response.body().string());
return respJson.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content");
}
}
public static void main(String[] args) throws IOException {
QwenClientSingleton instance = QwenClientSingleton.getInstance("https://xzt-llm-dev.jinsehuaqin.com",
"token-abc123",
"/mnt/app/llm/Qwen3/Qwen/Qwen3-8B");
String llmRes = instance.chat(SYSTEM_MESSAGE, USER_MESSAGE);
System.out.println(llmRes);
}
static String USER_MESSAGE = """
[
{
"id": 37856490,
"COMPANY_ID": 175721181,
"COMPANY_NAME": "北京博远育学文化发展有限公司",
"COMPANY_SHORT_NAME": null,
"INDUSTRY_CODE": null,
"INDUSTRY_NAME": null,
"PUBLISH_DATE": "2025-09-16",
"START_DATE": null,
"END_DATE": null,
"COUNTRY": "中国",
"CITY_CODE": "101251100",
"CITY_NAME": "湖南省|张家界市",
"POSITION_CODE": null,
"POSITION_NAME": null,
"POSITION_NAME1": null,
"POSITION_NAME2": null,
"POSITION_NAME3": null,
"JOB_NAME": "远程兼职,单单奖,初高中线上解题",
"JOB_SALARY": "40-50/",
"JOB_EDUCATION": "大专",
"JOB_EXPERIENCE": "经验不限",
"JOB_LABEL": null,
"JOB_DESCRIPTION": "【工作内容】工作周期长期兼职每周工期无要求工作时间不限工作时段不限结算方式月结招聘截止时间202603071工作内容在app上录制你所学学科的解题视频题目随机目前k12全科都在招2硬性要求需要有教师资格证语数英物化生科目不必须对应学科有就行2001年及以前出生有设备电脑+手写板或者平板+电容笔3基本薪资上传成功一题6.810+单单奖综合1030/题按月结算4工作时间不限制有时间就可以做没时间就可以不做5工作地点不限制但是工作环境需要安静录制不需要露脸【任职要求】",
"JOB_NUMBER": null,
"COMPANY_ZONE": "湖南省|张家界市",
"COMPANY_DETAIL_INFO": null,
"COMPANY_WEBSITE": null,
"COMPANY_LOCATION": "张家界桑植县桑植县德兴高级中学1",
"COMPANY_LOGO_ADDRESS": null,
"DATA_SOURCES": "boss直聘",
"URL_LINK": "https://www.zhipin.com/job_detail/bdeef9f132b6182903Fy39u0EVNR.html",
"CONTACT": null,
"STAFF_SIZE": null,
"JOB_NATURE": null,
"JOB_AGE": null,
"JOB_SEX": null,
"JOB_TIME": null,
"JOB_WELFARE": null,
"REMARK": null,
"DESC01": "boss直聘",
"DESC02": null,
"UPDATE_DATE": "2025-09-16",
"ORIG_ID": 791561870,
"USE_FLAG": 0,
"PUBLISH_TIME": "2025-09-14 00:00:00",
"COLLECTOR_NAME": null,
"COLLECT_TIME": null,
"COLLECT_FLAG": 0,
"CLEANER_NAME": null,
"CLEAN_TIME": null,
"CLEAN_FLAG": 0,
"UPDATE_REASON": null,
"TIME_STAMP": "2025-09-18 07:33:21.373032",
"UNIQUE_MD5": "5909dca2fb38d68bec8a8a4cb1d17f8f",
"VERSION_ID": 0
}
]
""";
static String SYSTEM_MESSAGE = """
- Carefully consider the user's question to ensure your answer is logical and makes sense.
- Make sure your explanation is concise and easy to understand, not verbose.
- Strictly return the answer in json format.
- Strictly Ensure that the following answer is in a valid JSON format.
- The output should be formatted as a JSON instance that conforms to the JSON schema below and do not add comments.
Here is the output schema:
'''
{
"jobName": string, // 岗位名称
"standardPosition": string, // 岗位标准名称
"standardJobDesc": string, // 岗位标准描述
"workPlace": List[string], // 工作地点。只允许省-市,直辖市-直辖市,全国-全国这三种格式,禁止到市以下级别。
"isIntern": string, // 是否是实习岗位。只能是"是"或者"否"。
"salaryConversionProcess": string, // 薪资转换为月薪的过程。转换时,如果没有说明工作时间,按照默认每天工作8小时,每月工作24天进行计算。即时薪需要*8*24计算,日薪需要*24进行计算。年薪需要÷12进行计算。
"salaryRange": string, // 薪资范围,需将日薪、年薪等转换为月薪,并按从小到大的自然数格式解析,例如“8000-12000”;无法解析则标记为“未知”
"graduate": string, // 分析整理岗位是否适合应届生。只能是"合适"或者"不合适"。
"graduateContent": string, // 分析整理岗位是否适合应届生的内容描述。例如:如果是合适,则说明为什么合适;如果不合适,则说明为什么不合适
"sketch": string, // 岗位语义描述,例如:工作内容、任职资格等。
"experienceType": string, // 分析岗位要求的经验最低年限要求的经验年数的最低值<=3年为“否”,>3年为“是”,仅允许“是”或“否”两种取值,不能为空;若无经验要求,则设为“否”。只能输出"是"或者"否"。
"welfare": string // 岗位福利,例如五险一金、餐补等。
}
'''
# 技能
## 技能1:理解岗位内容
当用户告诉你岗位相关内容时,可以使用此技能对岗位数据进行解析理解。
- 用户可能通过对话或者传入json文件的方式告诉给你岗位相关的数据信息,你应该都能理解他的意思。为了达到这样一个效果,我会告诉你,他使用的数据字段对应的释义,这样无论他是使用对话还是使用json格式数据文件,你都能理解他的意思。
- 具体的字段释义如下:
id: 主键 ID
company_id: 企业 ID
company_name: 企业名称
company_short_name: 企业简称
industry_code: 行业编码
industry_name: 行业名称
publish_date: 发布日期
start_date: 开始时间
end_date: 结束时间
country: 国家
city_code: 城市代码
city_name: 城市名称
position_code: 职位代码
position_name: 职位名称
job_name: 岗位名称
job_salary: 工资范围
job_education: 学历要求
job_experience: 工作经验要求
job_label: 工作标签(技能、岗位特性等)
job_description: 岗位描述
job_number: 工作编号
company_zone: 公司所在区域
company_detail_info: 公司详细信息
company_website: 公司官网网址
company_location: 公司地址
company_logo_address: 公司 logo 地址
data_sources: 数据来源
url_link: 岗位链接地址
remark: 备注
update_date: 更新时间
orig_id: 来源 ID
use_flag: 使用标记(是否有效)
publish_time: 发布时间
collector_name: 采集人姓名
collect_time: 采集时间
collect_flag: 采集标记
cleaner_name: 清理人姓名
clean_time: 清理时间
clean_flag: 清理标记
update_reason: 更新原因
time_stamp: 时间戳
version_id: 版本 ID
portrayal_type: 画像状态(0-未生成,1-已生成)
# 确定岗位名称jobName
岗位名称,直接读取输入数据中的jobname即可
# 确定标准岗位分类standardPosition
从输入信息中的岗位名称以及岗位工作内容描述综合判断,给出一个一般性描述的具体岗位名称。
# 标准岗位分类描述standardJobDesc
标准岗位分类描述,请根据给理解到的的岗位名称(jobName)和岗位相关信息(例如:job_labeljob_description),生成标准化的岗位语义描述。 岗位语义描述应**清晰概括岗位的核心职责和必备技能**,并确保表述通用性,适用于我们理解对该岗位的需求。 描述整体情注意不超过50字。其中对于技能的要求要精确到具体的技能名称,岗位描述中可能会缺少这部分具体技能名称的描述,你可以根据你自己的能力(泛化的经验)去自动联想一些**经验要求和技能要求的内容**
例如:
"jobName":"业务数据分析专家"
"standardJobDesc":"该岗位主要负责业务数据的处理、建模、报表可视化,使用 Python,SQL,BI工具支持业务增长。"
# 判断是否是实习岗位isIntern
判断岗位是否为实习岗位,如果是则返回“是”,否则返回“否”。
只有当岗位名称中含有“实习”二字,或者岗位描述中提及了类似“实习生”、“实习期”等字眼时才认为是实习岗位并返回“是”。否则认为该岗位不是实习岗位,返回“否”。
# 确定工作地城市信息workPlace
请结合cityname以及你理解的岗位整体数据信息去理解工作城市所在信息。
这里应该是一个数组,因为工作城市可能有多个,数组中的每项内容的格式应为“省-市”(直辖市格式为“市-市”、全国范围则为“全国-全国”)。
**请注意,这里的数据格式请使用AA-BB,连接符不要变化,同时,请注意,不需要解析到区县级,只需要省-市,直辖市-直辖市,全国-全国这3个中的一个**
# 薪资转换过程salaryConversionProcess
薪资转换过程,请将日薪、年薪等转换为月薪的过程记录在此字段中。例如:
"salaryConversionProcess": "原始薪资范围为 300/天 至 400/天,转换为月薪即 (300*30)至(400*30),得到结果为9000到12000。"
"salaryRange": "15000-20000",
1. 当原始薪资是日薪时,按照工作时间进行计算,未明确工作时间时,按照每月24天进行计算。
2. 当原始薪资是年薪时,按照每年12个月进行计算。
3. 当原始薪资是时薪时,按照工作时间进行计算,未明确工作时间时,按照每天工作8小时,每月工作24天进行计算。
# 确定薪资范围salaryRange
将日薪、年薪等转换为月薪,并按从小到大的自然数格式解析,例如“8000-12000”;无法解析则标记为“未知”
**请注意,不管接收到的数据有没有其他的表达方式如7k1w这种,都请按照要求转化为具体数值-具体数值,连接符也不要进行转化**
**同时,薪资范围必须是整千数字,非整千的数字向下取整**
# 技能2:分析整理岗位要求技能&经验
分析岗位对应要求的技能和经验。
请你完整的理解岗位整体意思,并按照**工作经验要求****工作技能要求** 2个维度去提取对应的内容。
当你得到的岗位信息特别少时,你可以根据你自己的能力(泛化的经验)去自动联想一些**经验要求和技能要求的内容****同时,为了确保不要过度联想导致后期的分析内容失真**,请你确保仅在如下几个情况时才发挥你的聪明才智去进行信息联想补充:
- 岗位描述缺乏具体技能要求
- 没有提及经验要求(如“XX年经验”)
- 岗位职责含糊,不明确具体做什么
# 分析整理岗位是否适合应届生graduate
不适合应届毕业生的工作岗位通常具有以下特征或以下特征之一:
1. **经验门槛过高**
- 明确要求3-5年工作经验或独立负责核心业务(如高级项目经理)。
2. **高压或高风险岗位**
- 业绩考核严苛(如销售岗末位淘汰制)、法律/财务责任重大(如风控总监)。
3. **缺乏成长性**
- 重复性劳动(如纯数据录入)、无技能提升空间。
4. **职业路径模糊**
- 岗位职责混乱(如“全能助理”兼保洁、司机),无明确发展方向。
5. **短期无保障**
- 岗位描述中出现“日结”“兼职”“短期”等表示该岗位非长期职业。
适合应届毕业生的工作岗位通常具有以下特征:
1. **学习与成长空间**
- 提供系统培训或 mentorship(导师制),帮助新人快速适应职场。
- 工作内容包含基础技能锻炼(如沟通、项目管理),并有明确的晋升路径。
2. **行业与岗位特性**
- 对经验要求较低:如行政助理、市场专员、初级程序员、管培生等。
- 新兴行业(如互联网、新能源、AI)更愿意接纳新人,包容试错。
2. **专业对口度**
- 与大学专业相关,能快速应用理论知识(如医学、工程类、技术类岗位)。
# 给出是否适合应届生的理由graduateContent
适合或不适合应届生的理由,如果合适,需要说明原因且需要说出明确的晋升途径。
# 语义化描述工作岗位sketch
你要理解,我们之所以要进行这块的内容,是因为我们最终是需要将这份岗位转义成一段语义内容,能够被向量模型所理解,这样才能让我们后续的工作能继续下去。所以我们**绝不仅仅只是将岗位拆分成技能和经验的关键词**!!!同时,通过语义的描述可以让模型能够更精准的理解这个岗位的要求,提高后续工作的准确性。
请注意,你需要使用“这个岗位需要xxxx技能,需要xxx经验” 这种语义描述方式来进行语义转化。**需要注意的事情是,我不需要多段落的内容,应该是将其整合在一起。**
为了确保你能理解我的意思,并输出我想要的内容,你可以参考下我给你的例子:
示例:
背景:
岗位名称:电子工程师
得到的岗位数据理解后内容如下:
1 熟练电路设计,懂得信号处理,
2 精通c,c++,具有单片机,smt32开发经验,
3 有独立设计和测试项目经验
分析后内容如下:
工作技能要求
电路设计(模拟电路、数字电路)
信号处理(滤波、采样、信号放大)
嵌入式开发(C/C++、单片机、STM32RTOS
硬件测试(示波器、逻辑分析仪、JTAG 调试)
PCB 设计(Altium DesignerKiCadOrCAD
通信协议(UARTSPII2CCAN
硬件调试(KeilIARSWD
工作经验要求
电路设计经验
2-3 年嵌入式开发经验
单片机(STM32)开发
独立设计和测试项目
硬件产品研发经验
硬件调试经验
输出:
sketch
这个岗位需要熟练的电路设计能力,包括模拟电路和数字电路的设计,能够进行信号处理(滤波、采样、放大)。候选人需要精通 C C++ 语言,具备单片机开发经验,特别是 STM32 平台的开发。需要有嵌入式开发经验,包括 RTOS 和硬件驱动编写。候选人还需要具备硬件测试能力,能够使用示波器、逻辑分析仪进行调试,并有 2-3 年的相关工作经验,能够独立设计和测试嵌入式电子项目。
# 分析是否需要3年以上工作经验experienceType
jobExperience中要求的经验年数的最低值<=3年为“否”,>3年为“是”,仅允许“是”或“否”两种取值,不能为空;若无经验要求,则设为“否”)
# 分析该岗位是否有额外福利benefits
jobDescription字段中提取除薪资、奖金外其他的福利,例如法定假日外额外假期、下午茶、节日礼品等,总结简化输出为字符串数组。
**需要注意,这里只允许总结原始数据中的真实数据,请你不要做任何添加和调整,不存在额外福利或不能判断时将此字段空置。**
## 输出要求
- 请直接分析以下岗位信息并输出标准格式的JSON结果,不要包含任何解释、推理过程或思考步骤,直接给出最终结果。
- 请严格遵循你在最后阶段以json格式输出对应的内容,且无需添加多余信息,请一定记住,你更多是以api形式与后端应用发出的请求进行交互,高质量的稳定输出json格式的岗位画像是最合适的。
- 禁止在JSON外输出任何解释性文字。
## 输出示例
示例一:
```json
{
"jobName": "物流员",
"standardPosition": "物流客服",
"standardJobDesc": "该岗位负责处理物流订单查询、异常跟踪及客户沟通,需具备良好的服务意识和物流系统操作能力。",
"workPlace": [
"广东省-广州市",
"广东省-佛山市",
"全国-全国"
],
"isIntern": "否",
"salaryConversionProcess": "年薪10万转换为月薪8333元",
"salaryRange": "4000-7000",
"requirement": "无要求",
"graduate": "不合适",
"graduateContent": "岗位缺乏成长性,属于重复性劳动,且职业路径模糊,无明确发展方向。",
"sketch": "这里是转化的语义描述",
"experienceType": "是",
"welfare": [
"下午茶",
"节假日礼品市",
"五天以上年假"
]
}
```
示例二:
```json
{
"jobName": "速派出行招聘网约车司机 8000-1.2万元",
"standardPosition": "网约车司机",
"standardJobDesc": "该岗位负责安全驾驶、接送乘客,需熟悉导航系统,具备良好服务意识和驾驶技能。",
"workPlace": [
"广东省-佛山市"
],
"isIntern": "否",
"salaryConversionProcess": "日薪800-1200转换为月薪24000元",
"salaryRange": "24000-36000",
"graduate": "不合适",
"graduateContent": "岗位要求高中及以上学历,1-3年驾龄即可,工作时间自由安排,但岗位缺乏成长性,属于重复性劳动,且职业路径模糊,无明确发展方向。",
"sketch": "这个岗位需要持有C1驾驶证和网络预约出租汽车驾驶员证,驾龄3年以上,近3个记分周期内未一次性扣满12分。候选人需身体健康,无犯罪、酒驾、吸毒记录。工作内容为网约车驾驶服务,薪酬按每日流水500-800元计算,多劳多得,工资日结。工作时间自由安排,可兼职或全职。",
"experienceType": "否",
"welfare": [
"工资日结",
"工作时间自由安排",
"可兼职/全职"
]
}
```
""";
}
...@@ -85,4 +85,7 @@ public interface CacheConstants { ...@@ -85,4 +85,7 @@ public interface CacheConstants {
* 下载使用简历对应模版缓存pdf * 下载使用简历对应模版缓存pdf
*/ */
String RESUME_TEMPLATE_PDF_ID = "template:resume:pdf:download:%s"; String RESUME_TEMPLATE_PDF_ID = "template:resume:pdf:download:%s";
/**分析职业期望数据*/
String REDIS_USER_ANALYSIS_KEY = "user:analysis:%s";
} }
...@@ -82,8 +82,5 @@ public interface Constants { ...@@ -82,8 +82,5 @@ public interface Constants {
/** 全字段时间格式化字符串模式:yyyy-MM-dd HH:mm:ss */ /** 全字段时间格式化字符串模式:yyyy-MM-dd HH:mm:ss */
public static final String DATE_FULL_FORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String DATE_FULL_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**分析职业期望数据*/
String REDIS_USER_ANALYSIS_KEY = "user:analysis:%s";
} }
...@@ -151,6 +151,19 @@ ...@@ -151,6 +151,19 @@
<artifactId>fastjson2</artifactId> <artifactId>fastjson2</artifactId>
<version>2.0.43</version> <version>2.0.43</version>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.29.1</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>0.29.1</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
......
package com.bkty.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用 StringRedisSerializer 处理 Key 和 Value
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(stringRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
\ No newline at end of file
...@@ -12,7 +12,11 @@ import lombok.RequiredArgsConstructor; ...@@ -12,7 +12,11 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.JxgException; import org.dromara.common.core.exception.JxgException;
import org.dromara.common.core.exception.WarnException;
import org.dromara.common.core.utils.DateTimeWrapper;
import org.dromara.common.core.utils.SecurityUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
...@@ -79,4 +83,20 @@ public class JobRecommendController { ...@@ -79,4 +83,20 @@ public class JobRecommendController {
public R<List<Level1City>> getAllCitys() { public R<List<Level1City>> getAllCitys() {
return new R<>(categoryCacheManager.getCityCache()); return new R<>(categoryCacheManager.getCityCache());
} }
@Operation(summary = "岗位智推")
@GetMapping("/position")
public R<Void> recommendPosition(@RequestParam(value = "analysisId") Long analysisId,
@RequestParam("resumeId") Long resumeId) {
if (analysisId == null || resumeId == null){
throw new WarnException("分析数据不能为空");
}
if (resumeId == null){
throw new WarnException("简历id不能为空");
}
Long userId = LoginHelper.getLoginUser().getUserId();
jobRecommendService.recommendPosition(analysisId,userId,resumeId);
return R.ok();
}
} }
...@@ -5,12 +5,14 @@ import cn.hutool.core.collection.CollectionUtil; ...@@ -5,12 +5,14 @@ import cn.hutool.core.collection.CollectionUtil;
import com.bkty.system.api.model.LoginUser; import com.bkty.system.api.model.LoginUser;
import com.bkty.system.domain.dto.*; import com.bkty.system.domain.dto.*;
import com.bkty.system.domain.entity.FunctionResumeBaseTag; import com.bkty.system.domain.entity.FunctionResumeBaseTag;
import com.bkty.system.domain.entity.FunctionResumeSketch;
import com.bkty.system.domain.vo.ResumeBase; import com.bkty.system.domain.vo.ResumeBase;
import com.bkty.system.domain.vo.ResumeByPdfVo; import com.bkty.system.domain.vo.ResumeByPdfVo;
import com.bkty.system.domain.vo.ResumeModelVo; import com.bkty.system.domain.vo.ResumeModelVo;
import com.bkty.system.domain.vo.ResumeVo; import com.bkty.system.domain.vo.ResumeVo;
import com.bkty.system.service.resume.NewEditionResumeService; import com.bkty.system.service.resume.NewEditionResumeService;
import com.bkty.system.service.resume.ResumeCacheService; import com.bkty.system.service.resume.ResumeCacheService;
import com.bkty.system.service.resume.ResumeMakeService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
...@@ -44,6 +46,8 @@ public class NewEditionResumeController { ...@@ -44,6 +46,8 @@ public class NewEditionResumeController {
private final ResumeCacheService resumeCacheService; private final ResumeCacheService resumeCacheService;
private final ResumeMakeService resumeMakeService;
/** /**
* 新版导入简历 * 新版导入简历
* @param file 文件 * @param file 文件
...@@ -256,4 +260,15 @@ public class NewEditionResumeController { ...@@ -256,4 +260,15 @@ public class NewEditionResumeController {
this.newEditionResumeService.reversalExperience(dto); this.newEditionResumeService.reversalExperience(dto);
return R.ok(); return R.ok();
} }
/**
* 查询简历分析详情
* @param resumeId
* @return
*/
@GetMapping("/query-resume-sketch")
public R<FunctionResumeSketch> queryResumeSketch(@RequestParam("resumeId") Long resumeId){
FunctionResumeSketch vo = this.resumeMakeService.queryResumeSketch(resumeId);
return new R<>(vo);
}
} }
package com.bkty.system.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.util.Date;
/**
* @author jiangxiaoge
* @description 岗位推荐记录
* @data 2025/2/18
**/
@Data
@TableName("ai_position_recommend_record")
public class AiPositionRecommendRecord extends BaseEntity {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
private Long userId;
private Long positionId;
/**
* 是否收藏 0.否 1.是 2.不喜欢
*/
private Integer treasuresType;
/**
* 是否已读 0.否 1.是
*/
private Integer alreadyType;
private Integer deliverType;
private Integer operationType;
private Integer rScore;
private String rDetails;
private Date recommendTime;
private Long resumeId;
private String cityCode;
private String analyzeJson;
private String evaluationJson;
private String jobTitle;
private String jobProfession;
private String standardPosition;
private Integer companySize;
private Integer companyNature;
private String expDemand;
private String eduDemand;
private String publicPlatform;
private Integer maxPay;
private String companyName;
private Integer positionScore;
private String jdMd;
/**
* 首次评分
*/
private Integer firstScore;
//岗位优势分析
private String advantageAnalysis;
//岗位匹配度
private String matchRate;
//投递准备
private String deliverPrepare;
//自我介绍
private String selfIntroduction;
//公司规模
private String companyScale;
//公司行业
private String companyIndustry;
// 分析id
private Long analysisId;
}
\ No newline at end of file
package com.bkty.system.domain.entity;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.Date;
import java.util.List;
/**
* @author jiangxiaoge
* @description 岗位画像实体类
* @data 2025/2/7
**/
@Data
//@Document(indexName = "function_position_portrait_test") // 使用前缀
@Document(indexName = "#{@environment.getProperty('elasticsearch.index-name-prefix')}") // 使用前缀
public class FunctionPositionPortraitV2 {
@Id
private Long positionDataId;
//岗位名称
@Field(type = FieldType.Keyword)
private String jobTitle;
//公司行业
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String industry;
//标准职位
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String standardPosition;
//城市信息
@Field(type = FieldType.Keyword)
private List<String> workPlace;
//全职、实习、灵活工作
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String natureOfPost;
//薪资范围
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String salaryRange;
//薪资(查询用)
@Field(type = FieldType.Integer)
private Integer pay;
//公司名称
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String companyName;
//公司性质
@Field(type = FieldType.Integer)
private Integer companyNature;
//公司规模
@Field(type = FieldType.Integer)
private Integer companySize;
//经验要求
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String expDemand;
//学历要求
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String eduDemand;
//发布渠道
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String publicPlatform;
//发布链接
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String platformUrl;
//JD信息
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String jdHtml;
//专业技能
@Field(type = FieldType.Keyword)
private List<String> skill;
//福利
@Field(type = FieldType.Keyword)
private List<String> welfare;
//晋升路径
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String promote;
//软技能
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String mild;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String graduate;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String graduateContent;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String graduatePercentage;
@Field(type = FieldType.Dense_Vector, dims = 1536, similarity = "cosine")
private List<Float> featureVector;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String sketch;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String experienceType;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String publicTime;
@Field(type = FieldType.Date)
private Date queryDate;
private String standardJobDesc;
//是否实习
@Field(type = FieldType.Text)
private String isIntern;
}
\ No newline at end of file
package com.bkty.system.domain.entity;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import java.util.List;
@Data
public class JobEvaluation {
@JSONField(name = "competitiveness")
private Competitiveness competitiveness;
@JSONField(name = "jobFit")
private JobFit jobFit;
@JSONField(name = "resumeTips")
private ResumeTips resumeTips;
@JSONField(name = "prepGrowth")
private PrepGrowth prepGrowth;
@Data
public static class Competitiveness {
private List<String> advantages;
private List<String> weaknesses;
private List<String> suggestions;
}
@Data
public static class JobFit {
private List<String> requirements;
private MatchDetails matchDetails;
}
@Data
public static class MatchDetails {
private List<MatchedSkill> matched;
private List<MissingSkill> missing;
}
@Data
public static class ResumeTips {
private List<String> improvements;
}
@Data
public static class MatchedSkill {
private String skill;
private String status;
private String analysis;
}
@Data
public static class MissingSkill {
private String skill;
private String importance;
private String suggestion;
}
@Data
public static class PrepGrowth {
private List<String> shortTerm;
private List<String> longTerm;
}
}
\ No newline at end of file
package com.bkty.system.domain.entity;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import java.util.List;
@Data
public class MatchResult {
@JSONField(name = "comprehensiveMatchScore")
private Object comprehensiveMatchScore; // 处理数字和字符串两种情况
private String comprehensiveMatchRecommendation;
private List<String> jobSeekingAdvice;
private MatchDimensionAnalysis matchDimensionAnalysis;
public int getComprehensiveMatchScoreAsInt() {
if (comprehensiveMatchScore instanceof Number) {
return ((Number) comprehensiveMatchScore).intValue();
} else if (comprehensiveMatchScore instanceof String) {
try {
return Integer.parseInt((String) comprehensiveMatchScore);
} catch (NumberFormatException e) {
return 0; // 处理异常情况
}
}
return 0;
}
@Data
public static class MatchDimensionAnalysis {
private MatchDetail education;
private MatchDetail professionalSkills;
private MatchDetail salaryExpectation;
private MatchDetail workLocation;
private MatchDetail practicalExperience;
private MatchDetail softSkills;
}
@Data
public static class MatchDetail {
private String matchDegree;
private String detailedAnalysis;
private String matchPoint;
private String improvementSuggestion;
}
}
\ No newline at end of file
package com.bkty.system.domain.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author jiangxiaoge
* @description 推送消息状态封装
* @data 2025/6/17
**/
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ProcessItem {
private String status;
private String text;
private double percent;
private String time;
}
\ No newline at end of file
package com.bkty.system.domain.vo;
import com.bkty.system.domain.entity.AiPositionRecommendRecord;
import com.bkty.system.domain.entity.FunctionPositionPortraitV2;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* @author jiangxiaoge
* @description
* @data 2025/2/18
**/
@Data
public class AiPositionRecommendRecordVo extends AiPositionRecommendRecord {
private String resumeName;
private FunctionPositionPortraitV2 portraitV2;
private List<String> kitIds;
private Map<String, String> levelMap;
}
package com.bkty.system.domain.vo;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* @author Zhang Wenbiao
* @description 职业需求分析数据vo
* @datetime 2025/12/4 12:08
*/
@Data
public class AiRecommendVo {
private String id;
/**期望职业*/
private List<String> career;
/**单位性质*/
private List<String> companyNature;
/**目标城市*/
private List<String> targetCity;
/**期望薪资*/
private String pay;
private String mbti;
private Long resumeCount;
private List<Integer> companySize;
private Integer workTime;
private Integer internTime;
/**求职性质:1-实习,2-全职*/
private String jobType;
/**自动推荐状态 0.已经自动推荐*/
private Integer authRecommend;
/**推荐状态 0.未推荐 1.推荐中 2.推荐完成*/
private Integer recommendStatus;
/**发送给智能体的岗位列表*/
private List<Map<String, String>> jobList;
}
package com.bkty.system.domain.vo;
import com.bkty.system.domain.entity.JobEvaluation;
import com.bkty.system.domain.entity.MatchResult;
import com.bkty.system.domain.entity.FunctionPositionPortraitV2;
import lombok.Data;
/**
* @author jiangxiaoge
* @description
* @data 2025/2/18
**/
@Data
public class FunctionPositionPortraitVo extends FunctionPositionPortraitV2 {
private String score;
private String details;
private String resumeId;
private String cityCode;
private String whetherItMatches;
private String resumeSketch;
private Float scoreFloat;
private MatchResult matchResult;
private JobEvaluation jobEvaluation;
private String jdMd;
private Float rerankScore;
private String cozeResult;
}
\ No newline at end of file
package com.bkty.system.domain.vo;
import lombok.Data;
import java.util.List;
/**
* @author jiangxiaoge
* @description 岗位推荐列表item
* @data 2025/3/28
**/
@Data
public class RecommendItem {
private Long id;
private String jobTitle;
private String standardPosition;
private List<String> welfare;
private String salaryRange;
private Integer maxPay;
private String rDetails;
private String companyName;
private Integer companyNature;
private String city;
private String createTime;
private String resumeName;
private Integer positionScore;
private String jdMd;
}
\ No newline at end of file
package com.bkty.system.domain.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @author jiangxiaoge
* @description 重排序vo
* @data 2025/7/1
**/
@Data
@AllArgsConstructor
public class RerankVo {
private FunctionPositionPortraitVo positionData;
private Float score;
}
\ No newline at end of file
...@@ -4,6 +4,8 @@ import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; ...@@ -4,6 +4,8 @@ import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
/** /**
...@@ -13,6 +15,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; ...@@ -13,6 +15,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
*/ */
@EnableDubbo @EnableDubbo
@EnableScheduling @EnableScheduling
@EnableCaching
@SpringBootApplication @SpringBootApplication
public class employmentBusinessPCSystemApplication { public class employmentBusinessPCSystemApplication {
public static void main(String[] args) { public static void main(String[] args) {
......
...@@ -64,7 +64,43 @@ public class CategoryCacheManager { ...@@ -64,7 +64,43 @@ public class CategoryCacheManager {
public Level1Group getPositionDataByName(String level1Name) { public Level1Group getPositionDataByName(String level1Name) {
return positionNameMap.get(level1Name); return positionNameMap.get(level1Name);
} }
/**
* 根据三级分类名称获取二级分类下所有三级分类名称 (更新后的方法)
*/
public List<String> getLevel2NameByLevel3Name(String level3Name){
for (Level1Group level1Group : positionCache) {
for (Level2Group level2Group : level1Group.getLevel2Groups()) {
for (Level3Group level3Group : level2Group.getLevel3Groups()) {
if (level3Group.getName().equals(level3Name)) {
return level2Group.getLevel3Groups().stream().map(Level3Group::getName).toList();
}
}
}
}
return null;
}
/**
* 根据三级分类名称获取每一级分类的名称Map
*/
public Map<String, String> getLevelNameMapByLevel3Name(String level3Name) {
Map<String, String> levelNameMap = new ConcurrentHashMap<>();
for (Level1Group level1Group : positionCache) {
for (Level2Group level2Group : level1Group.getLevel2Groups()) {
for (Level3Group level3Group : level2Group.getLevel3Groups()) {
if (level3Group.getName().equals(level3Name)) {
levelNameMap.put("level1", level1Group.getName());
levelNameMap.put("level2", level2Group.getName());
levelNameMap.put("level3", level3Group.getName());
return levelNameMap;
}
}
}
}
return null;
}
/** /**
* 根据一级分类获取数据 * 根据一级分类获取数据
*/ */
......
package com.bkty.system.llm;
import dev.langchain4j.service.SystemMessage;
/**
* @author jiangxiaoge
* @description
* @data 2025/6/10
**/
public interface JdWriter {
String PAY_MESSAGE = """
- Carefully consider the user's question to ensure your answer is logical and makes sense.
- Make sure your explanation is concise and easy to understand, not verbose.
- Strictly return the answer in json format.
- Strictly Ensure that the following answer is in a valid JSON format.
- The output should be formatted as a JSON instance that conforms to the JSON schema below and do not add comments.
Here is the output schema:
'''
{
"salaryConversionProcess": string, // 薪资转换为月薪的过程。转换时,如果没有说明工作时间,按照默认每天工作8小时,每月工作24天进行计算。即时薪需要*8*24计算,日薪需要*24进行计算。年薪需要÷12进行计算。
"salaryRange": string, // 薪资范围,需将日薪、年薪等转换为月薪,并按从小到大的自然数格式解析,例如“8000-12000”;无法解析则标记为“未知”
}
'''
# 技能
## 技能1:理解岗位内容
当用户告诉你岗位相关内容时,可以使用此技能对岗位数据进行解析理解,并计算该岗位提供的月薪标准。
- 用户可能通过对话或者传入json文件的方式告诉给你岗位相关的数据信息,你应该都能理解他的意思。为了达到这样一个效果,我会告诉你,他使用的数据字段对应的释义,这样无论他是使用对话还是使用json格式数据文件,你都能理解他的意思。
- 具体的字段释义如下:
id: 主键 ID
company_id: 企业 ID
company_name: 企业名称
company_short_name: 企业简称
industry_code: 行业编码
industry_name: 行业名称
publish_date: 发布日期
start_date: 开始时间
end_date: 结束时间
country: 国家
city_code: 城市代码
city_name: 城市名称
position_code: 职位代码
position_name: 职位名称
job_name: 岗位名称
job_salary: 工资范围
job_education: 学历要求
job_experience: 工作经验要求
job_label: 工作标签(技能、岗位特性等)
job_description: 岗位描述
job_number: 工作编号
company_zone: 公司所在区域
company_detail_info: 公司详细信息
company_website: 公司官网网址
company_location: 公司地址
company_logo_address: 公司 logo 地址
data_sources: 数据来源
url_link: 岗位链接地址
remark: 备注
update_date: 更新时间
orig_id: 来源 ID
use_flag: 使用标记(是否有效)
publish_time: 发布时间
collector_name: 采集人姓名
collect_time: 采集时间
collect_flag: 采集标记
cleaner_name: 清理人姓名
clean_time: 清理时间
clean_flag: 清理标记
update_reason: 更新原因
time_stamp: 时间戳
version_id: 版本 ID
portrayal_type: 画像状态(0-未生成,1-已生成)
# 薪资转换过程salaryConversionProcess
薪资转换过程,请将日薪、年薪等转换为月薪的过程记录在此字段中。例如:
"salaryConversionProcess": "原始薪资范围为 300/ 400/天,转换为月薪即 (300*30)(400*30),得到结果为900012000"
"salaryRange": "15000-20000",
1. 当原始薪资是日薪时,按照工作时间进行计算,未明确工作时间时,按照每月24天进行计算。
2. 当原始薪资是年薪时,按照每年12个月进行计算。
3. 当原始薪资是时薪时,按照工作时间进行计算,未明确工作时间时,按照每天工作8小时,每月工作24天进行计算。
# 确定薪资范围salaryRange
将日薪、年薪等转换为月薪,并按从小到大的自然数格式解析,例如“8000-12000”;无法解析则标记为“未知”
**请注意,不管接收到的数据有没有其他的表达方式如7k,1w这种,都请按照要求转化为具体数值-具体数值,连接符也不要进行转化**
**同时,薪资范围必须是整千数字,非整千的数字向下取整**
## 输出要求
- 请直接分析以下岗位信息并输出标准格式的JSON结果,不要包含任何解释、推理过程或思考步骤,直接给出最终结果。
- 请严格遵循你在最后阶段以json格式输出对应的内容,且无需添加多余信息,请一定记住,你更多是以api形式与后端应用发出的请求进行交互,高质量的稳定输出json格式的岗位画像是最合适的。
- 禁止在JSON外输出任何解释性文字。
## 输出示例
示例一:
```json
{
"salaryConversionProcess": "年薪10万转换为月薪8333",
"salaryRange": "4000-7000"
}
```
示例二:
```json
{
"salaryConversionProcess": "日薪800-1200转换为月薪24000",
"salaryRange": "24000-36000"
}
```
""";
String SYSTEM_MESSAGE = """
# 角色
文本格式转换专家,擅长将纯文本转化为合适的Markdown格式文本。
# 目标
1. 将输入进来的纯文本转化为合适的Markdown格式文本。
2. 输出转换后的Markdown格式文本。
# 技能
1. 熟悉Markdown格式中的列表标签。
2. 能够准确识别纯文本中的内容结构以进行格式转换。
# 工作流程
1. 接收输入的纯文本。
2. 分析纯文本中的内容结构,判断哪些部分适合使列表标签。
3. 使用加粗标签和列表标签将纯文本转换为Markdown格式文本。
4. 检查转换后的文本,确保没有改动文本内容且未添加其他文字说明。
5. 输出转换后的Markdown格式文本。
# 约束
1. 必须把输入的纯文本转化为Markdown格式文本。
2. 必须仅使用正文格式和列表标签进行转换。
3. 禁止添加任何其他的文字说明。
4. 禁止改动文本内容。
# 输出格式
输出为符合Markdown语法的文本,使用无序列表标签(- )以及有序列表标签(1. ),文字风格保持与输入文本一致,简洁明了。
# 示例
示例1:
输入:
1.负责分析对比市场各车型车机及手机APP车控功能等特点及发展趋势,定期输出竟品分析报告:2.负责跟踪和推进在研车型车机及手机APP车控功能定义的开发落地;3.参与规划和设计在研车型车机及手机APP车控功能的原型图和功能定义:4.参与竞品与本品的动静态体验测评,并编制测评报告,跟踪测评问题的改进,5.开展用户需求研究,协助开展新车型开发的竞争策略的制定和维护。1.硕士研究生及以上学历,车辆工程、机..
输出:
1. 负责分析对比市场各车型车机及手机APP车控功能等特点及发展趋势,定期输出竟品分析报告。
2. 负责跟踪和推进在研车型车机及手机APP车控功能定义的开发落地。
3. 参与规划和设计在研车型车机及手机APP车控功能的原型图和功能定义。
4. 参与竞品与本品的动静态体验测评,并编制测评报告,跟踪测评问题的改进。
5. 开展用户需求研究,协助开展新车型开发的竞争策略的制定和维护。
6. 硕士研究生及以上学历,车辆工程、机..
示例2:
输入:
岗位职责1.负责公司市场运营线上及线上平面设计的需求,制作相关物料的设计及质量把控。2.协助完成AIGC项目产品的平面设计需求。任职要求1.在校生,绘画、设计类专业本科及以上学历。2.视觉表现能力出色,不拘泥于单一的风格表现,具有丰富的想象力与创意。3.有活跃的创意思维,出色的审美和理解能力,重视细节。4.较强的沟通能力和团队精神,思维逻辑清晰,能够从用户角度去思考设计。5.熟练使用Photoshop,Al,Figma,Sketch,Axure 等相关设计软件;有 Cinema 4D 使用经验者更佳,
输出:
岗位职责
1. 负责公司市场运营线上及线上平面设计的需求,制作相关物料的设计及质量把控。
2. 协助完成AIGC项目产品的平面设计需求。
任职要求
1. 在校生,绘画、设计类专业本科及以上学历。
2. 视觉表现能力出色,不拘泥于单一的风格表现,具有丰富的想象力与创意。
3. 有活跃的创意思维,出色的审美和理解能力,重视细节。
4. 较强的沟通能力和团队精神,思维逻辑清晰,能够从用户角度去思考设计。
5. 熟练使用Photoshop,Al,Figma,Sketch,Axure 等相关设计软件;有 Cinema 4D 使用经验者更佳。
""";
@SystemMessage(SYSTEM_MESSAGE)
String writer(String prompt);
}
package com.bkty.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bkty.system.domain.entity.AiRecommendBase;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface AiAnalysisMapper extends BaseMapper<AiRecommendBase> {
void updateAnalysis(@Param("analysisId") Long analysisId,
@Param("rType") Integer rType,
@Param("userId") Long userId,
@Param("authRecommend") Integer authRecommend);
}
...@@ -3,7 +3,14 @@ package com.bkty.system.mapper; ...@@ -3,7 +3,14 @@ package com.bkty.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bkty.system.domain.entity.CityData; import com.bkty.system.domain.entity.CityData;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper @Mapper
public interface CityDataMapper extends BaseMapper<CityData> { public interface CityDataMapper extends BaseMapper<CityData> {
@Select("""
select city_code from city_data
where city_name = #{city}
""")
String getCityCodeByName(@Param("city") String city);
} }
package com.bkty.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bkty.system.domain.entity.AiPositionRecommendRecord;
import com.bkty.system.domain.vo.AiPositionRecommendRecordVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author jiangxiaoge
* @description 岗位推荐mapper
* @data 2025/2/18
**/
@Mapper
public interface PositionRecommendMapper extends BaseMapper<AiPositionRecommendRecord> {
@Select("""
SELECT position_id from ai_position_recommend_record where is_deleted = 0 and analysis_id = #{analysisId}
""")
List<Long> selectPositionIdList(@Param("analysisId") Long analysisId);
List<AiPositionRecommendRecordVo> selectPositionList(@Param("analysisId") Long analysisId,
@Param("queryType") Integer queryType,
@Param("recommendTime") String recommendTime,
@Param("pId") Long pId,
@Param("start") Long start,
@Param("size") Long size,
@Param("sortField") String sortField,
@Param("sortType") String sortType,
@Param("deliverType") Integer deliverType);
}
...@@ -13,4 +13,12 @@ public interface CozeApiService { ...@@ -13,4 +13,12 @@ public interface CozeApiService {
* @return * @return
*/ */
String getCozeTokenCn(String cozeSource) throws Exception; String getCozeTokenCn(String cozeSource) throws Exception;
/**
* 获取cozeToken
* @return
*/
String getCozeToken() throws Exception;
String getCozeDomainUri();
} }
...@@ -14,11 +14,9 @@ import org.apache.commons.collections4.MapUtils; ...@@ -14,11 +14,9 @@ import org.apache.commons.collections4.MapUtils;
import org.dromara.common.core.constant.CacheConstants; import org.dromara.common.core.constant.CacheConstants;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.CozeConstsnts; import org.dromara.common.core.constant.CozeConstsnts;
import org.dromara.common.core.exception.JxgException;
import org.dromara.common.core.exception.WarnException; import org.dromara.common.core.exception.WarnException;
import org.dromara.common.core.utils.AESUtil; import org.dromara.common.core.utils.*;
import org.dromara.common.core.utils.JWTGenerator;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.TimeTool;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
...@@ -107,4 +105,91 @@ public class CozeApiServiceImpl implements CozeApiService { ...@@ -107,4 +105,91 @@ public class CozeApiServiceImpl implements CozeApiService {
} }
throw new WarnException("获取coze令牌失败"); throw new WarnException("获取coze令牌失败");
} }
@Override
public String getCozeToken() throws Exception {
HttpServletRequest request = WebUtils.getRequest();
String redisKey = CacheConstants.REDIS_CN_COZE_TOKEN_KEY;
String publicKey = CozeConstsnts.COZE_CN_OAUTH_PUBLIC_KEY_STR;
String oauthId = CozeConstsnts.COZE_CN_OAUTH_ID;
String apiDomainName = CozeConstsnts.COZE_CN_API_DOMAIN_NAME;
String privateKey = CozeConstsnts.COZE_CN_OAUTH_PRIVATE_KEY_STR;
if (request != null){
/*log.info("request不为空");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
log.info("{}:{}", headerName, headerValue);
}*/
String cozeSource = request.getHeader(CozeConstsnts.COZE_HEADER_TOKEN_NAME);
//log.info("cozeSource:{}", cozeSource);
if (StringUtils.isNotBlank(cozeSource) && cozeSource.equals(CozeConstsnts.COZE_SOURCE_NAME)){
//log.info("进入国内访问");
redisKey = CacheConstants.REDIS_COZE_TOKEN_KEY;
publicKey = CozeConstsnts.COZE_OAUTH_PUBLIC_KEY_STR;
oauthId = CozeConstsnts.COZE_OAUTH_ID;
apiDomainName = CozeConstsnts.COZE_API_DOMAIN_NAME;
privateKey = CozeConstsnts.COZE_OAUTH_PRIVATE_KEY_STR;
}
}
try {
if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))){
return redisTemplate.opsForValue().get(redisKey);
}
} catch (Exception e) {
log.warn("Redis操作超时或异常,将直接获取新token: {}", e.getMessage());
// 继续执行后续代码获取新token
}
Map<String, Object> headerMap = Map.of("alg", "RS256",
"typ", "JWT",
"kid", publicKey);
//String headerString = Base64.getEncoder().encodeToString(JSON.toJSONString(headerMap).getBytes());
long iat = TimeTool.nowMilli();
long expiredTime = TimeTool.nowMilli() + TimeUnit.MINUTES.toMillis(10);
Map<String, Object> payloadMap = Map.of("iss", oauthId,
"aud", apiDomainName,
"iat", iat,
"exp", expiredTime,
"jti", UUID.randomUUID());
String payloadString = JSON.toJSONString(payloadMap);
String jwtString = JWTGenerator.generatorJwtString(headerMap, payloadString, privateKey);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(HttpHeaders.AUTHORIZATION, CozeConstsnts.COZE_TOKEN_BEARER + jwtString);
// 创建请求体(示例 JSON 数据)
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("duration_seconds", 86399);
requestBody.put("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
// 发送 POST 请求并获取响应
ResponseEntity<String> response = restTemplate.exchange(
getCozeDomainUri() + "/api/permission/oauth2/token", HttpMethod.POST, requestEntity, String.class);
com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(response.getBody());
String accessToken;
if (jsonObject != null) {
accessToken = jsonObject.getString("access_token");
redisTemplate.opsForValue().set(redisKey, accessToken, 86399 - 60, TimeUnit.SECONDS);
return accessToken;
}
throw new JxgException("获取coze令牌失败");
}
@Override
public String getCozeDomainUri(){
HttpServletRequest request = WebUtils.getRequest();
if (request != null){
String header = request.getHeader(CozeConstsnts.COZE_HEADER_TOKEN_NAME);
if (StringUtils.isNotBlank(header) && CozeConstsnts.COZE_SOURCE_NAME.equals(header)){
return Constants.HTTPS + CozeConstsnts.COZE_API_DOMAIN_NAME;
}
}
return Constants.HTTPS + CozeConstsnts.COZE_CN_API_DOMAIN_NAME;
}
} }
...@@ -3,6 +3,7 @@ package com.bkty.system.service.jobRecommend; ...@@ -3,6 +3,7 @@ package com.bkty.system.service.jobRecommend;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.bkty.system.domain.dto.AnalysisCareerDto; import com.bkty.system.domain.dto.AnalysisCareerDto;
import com.bkty.system.domain.entity.AiRecommendBase; import com.bkty.system.domain.entity.AiRecommendBase;
import com.bkty.system.domain.vo.AiRecommendVo;
public interface AiAnalysisService extends IService<AiRecommendBase> { public interface AiAnalysisService extends IService<AiRecommendBase> {
...@@ -11,4 +12,9 @@ public interface AiAnalysisService extends IService<AiRecommendBase> { ...@@ -11,4 +12,9 @@ public interface AiAnalysisService extends IService<AiRecommendBase> {
* @param dto * @param dto
*/ */
String saveAnalysisData(AnalysisCareerDto dto); String saveAnalysisData(AnalysisCareerDto dto);
/**
* 查询个人分析推荐数据
*/
AiRecommendVo queryAnalysisData(Long analysisId);
} }
...@@ -14,4 +14,12 @@ public interface JobRecommendService { ...@@ -14,4 +14,12 @@ public interface JobRecommendService {
* @return * @return
*/ */
List<Level1Group> getLevel1Groups(String level1); List<Level1Group> getLevel1Groups(String level1);
/**
* 岗位智推
* @param analysisId
* @param userId
* @param resumeId
*/
void recommendPosition(Long analysisId, Long userId, Long resumeId);
} }
package com.bkty.system.service.jobRecommend;
import com.bkty.system.domain.entity.FunctionPositionPortraitV2;
import com.bkty.system.domain.vo.AiPositionRecommendRecordVo;
import java.util.Map;
/**
* @author jiangxiaoge
* @description 岗位推荐查询redisService
* @data 2025/3/28
**/
public interface PositionRecommendRedisService {
/**
* 初始化redis数据
*/
public boolean initRedisData(Long analysisId);
/**
* 保存岗位到redis
* @param portraitV2
* @param position
*/
void savePositionToRedis(FunctionPositionPortraitV2 portraitV2, AiPositionRecommendRecordVo position, Long userId);
}
\ No newline at end of file
package com.bkty.system.service.jobRecommend;
import com.bkty.system.domain.entity.FunctionPositionPortraitV2;
import java.util.Collection;
import java.util.List;
/**
* 岗位推荐service
*/
public interface RecommendPositionService {
/**
* 根据求职意向岗位推荐
* @param analysisId
*/
void recommendPosition(Long analysisId, Long userId, Long resumeId);
List<FunctionPositionPortraitV2> queryPositionList(String cityCode, List<Long> positionIdList);
}
...@@ -6,8 +6,10 @@ import com.alibaba.nacos.common.utils.StringUtils; ...@@ -6,8 +6,10 @@ import com.alibaba.nacos.common.utils.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bkty.system.domain.dto.AnalysisCareerDto; import com.bkty.system.domain.dto.AnalysisCareerDto;
import com.bkty.system.domain.dto.ResumeMakeDto;
import com.bkty.system.domain.entity.AiRecommendBase; import com.bkty.system.domain.entity.AiRecommendBase;
import com.bkty.system.domain.entity.FunctionResumeBase; import org.dromara.common.core.constant.CacheConstants;
import com.bkty.system.domain.vo.AiRecommendVo;
import com.bkty.system.mapper.AiRecommendBaseMapper; import com.bkty.system.mapper.AiRecommendBaseMapper;
import com.bkty.system.mapper.FunctionResumeBaseMapper; import com.bkty.system.mapper.FunctionResumeBaseMapper;
import com.bkty.system.service.coze.CozeApiService; import com.bkty.system.service.coze.CozeApiService;
...@@ -48,63 +50,54 @@ public class AiAnalysisServiceImpl extends ServiceImpl<AiRecommendBaseMapper, Ai ...@@ -48,63 +50,54 @@ public class AiAnalysisServiceImpl extends ServiceImpl<AiRecommendBaseMapper, Ai
@Override @Override
public String saveAnalysisData(AnalysisCareerDto dto) { public String saveAnalysisData(AnalysisCareerDto dto) {
AiRecommendBase aiRecommendBase = new AiRecommendBase(); AiRecommendBase aiRecommendBase = new AiRecommendBase();
// if (StringUtils.isNotBlank(dto.getAnalysisId())){ aiRecommendBase.setResumeId(Long.valueOf(dto.getResumeId())); //简历id
// //String json = redisTemplate.opsForValue().get(CacheConstants.REDIS_USER_ANALYSIS_KEY.formatted(user.getId())); aiRecommendBase.setPay(dto.getPay()); //期望薪资
// aiRecommendBase = this.baseMapper.selectById(dto.getAnalysisId()); aiRecommendBase.setCareer(JSON.toJSONString(dto.getCareer())); //目标岗位
// } aiRecommendBase.setTargetCity(JSON.toJSONString(dto.getTargetCity())); //目标城市
aiRecommendBase.setAuthRecommend(0); //自动推荐
/* if (CollectionUtil.isNotEmpty(dto.getCareer())){ aiRecommendBase.setRecommendStatus(0); //未推荐
aiRecommendBase.setCareer(JSON.toJSONString(dto.getCareer())); aiRecommendBase.setUserId(Objects.requireNonNull(LoginHelper.getLoginUser()).getUserId());
} this.baseMapper.insert(aiRecommendBase);
redisTemplate.opsForValue().set(CacheConstants.REDIS_USER_ANALYSIS_KEY.formatted(aiRecommendBase.getId()), JSON.toJSONString(aiRecommendBase));
if (CollectionUtil.isNotEmpty(dto.getTargetCity())){ return String.valueOf(aiRecommendBase.getId());
aiRecommendBase.setTargetCity(JSON.toJSONString(dto.getTargetCity())); }
}
if (StringUtils.isNotBlank(dto.getPay())){ @Override
aiRecommendBase.setPay(dto.getPay()); public AiRecommendVo queryAnalysisData(Long analysisId) {
}
if (CollectionUtil.isNotEmpty(dto.getCompanyNature())){ AiRecommendVo vo = new AiRecommendVo();
aiRecommendBase.setCompanyNature(JSON.toJSONString(dto.getCompanyNature())); AiRecommendBase recommendBase = null;
String recommendJson = redisTemplate.opsForValue().get(CacheConstants.REDIS_USER_ANALYSIS_KEY.formatted(analysisId));
if (StringUtils.isBlank(recommendJson)){
recommendBase = this.baseMapper.selectById(analysisId);
}else {
recommendBase = JSON.parseObject(recommendJson, AiRecommendBase.class);
} }
if (StringUtils.isNotBlank(recommendBase.getCareer())){
if (StringUtils.isNotBlank(dto.getMbti())){ List<String> list = JSON.parseObject(recommendBase.getCareer(), List.class);
aiRecommendBase.setMbti(dto.getMbti()); vo.setCareer(list);
} }
if (null != dto.getWorkTime()){ if (StringUtils.isNotBlank(recommendBase.getTargetCity())){
aiRecommendBase.setWorkTime(dto.getWorkTime()); List<String> list = JSON.parseObject(recommendBase.getTargetCity(), List.class);
vo.setTargetCity(list);
} }
if (null != dto.getInternTime()){ if (StringUtils.isNotBlank(recommendBase.getPay())){
aiRecommendBase.setInternTime(dto.getInternTime()); vo.setPay(recommendBase.getPay());
} }
if (CollectionUtil.isNotEmpty(dto.getCompanySize())){ if (null != recommendBase.getAuthRecommend()){
aiRecommendBase.setCompanySize(JSON.toJSONString(dto.getCompanySize())); vo.setAuthRecommend(recommendBase.getAuthRecommend());
} }
if (null != dto.getJobType()){ if (null != recommendBase.getRecommendStatus()){
aiRecommendBase.setJobType(dto.getJobType()); vo.setRecommendStatus(recommendBase.getRecommendStatus());
} }
if (null != dto.getAuthRecommend()){ vo.setId(String.valueOf(recommendBase.getId()));
aiRecommendBase.setAuthRecommend(dto.getAuthRecommend());
}
if (null != dto.getRecommendStatus()){ return vo;
aiRecommendBase.setRecommendStatus(dto.getRecommendStatus());
}*/
aiRecommendBase.setResumeId(Long.valueOf(dto.getResumeId())); //简历id
aiRecommendBase.setPay(dto.getPay()); //期望薪资
aiRecommendBase.setCareer(JSON.toJSONString(dto.getCareer())); //目标岗位
aiRecommendBase.setTargetCity(JSON.toJSONString(dto.getTargetCity())); //目标城市
aiRecommendBase.setAuthRecommend(0); //自动推荐
aiRecommendBase.setRecommendStatus(0); //未推荐
aiRecommendBase.setUserId(Objects.requireNonNull(LoginHelper.getLoginUser()).getUserId());
this.baseMapper.insert(aiRecommendBase);
redisTemplate.opsForValue().set(Constants.REDIS_USER_ANALYSIS_KEY.formatted(aiRecommendBase.getId()), JSON.toJSONString(aiRecommendBase));
return String.valueOf(aiRecommendBase.getId());
} }
} }
package com.bkty.system.service.jobRecommend.impl; package com.bkty.system.service.jobRecommend.impl;
import com.bkty.system.domain.entity.AiRecommendBase;
import com.bkty.system.init.CategoryCacheManager; import com.bkty.system.init.CategoryCacheManager;
import com.bkty.system.init.Level1Group; import com.bkty.system.init.Level1Group;
import com.bkty.system.init.Level2Group; import com.bkty.system.init.Level2Group;
import com.bkty.system.init.Level3Group; import com.bkty.system.init.Level3Group;
import com.bkty.system.mapper.AiAnalysisMapper;
import com.bkty.system.service.jobRecommend.AiAnalysisService;
import com.bkty.system.service.jobRecommend.JobRecommendService; import com.bkty.system.service.jobRecommend.JobRecommendService;
import com.bkty.system.service.jobRecommend.RecommendPositionService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.DateTimeWrapper;
import org.dromara.common.core.utils.SecurityUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -28,6 +36,11 @@ public class JobRecommendServiceImpl implements JobRecommendService { ...@@ -28,6 +36,11 @@ public class JobRecommendServiceImpl implements JobRecommendService {
@Autowired @Autowired
private CategoryCacheManager categoryCacheManager; private CategoryCacheManager categoryCacheManager;
@Autowired
private AiAnalysisMapper aiAnalysisMapper;
private final RecommendPositionService recommendPositionService;
@Override @Override
public List<Level1Group> getLevel1Groups(String level1) { public List<Level1Group> getLevel1Groups(String level1) {
...@@ -60,4 +73,40 @@ public class JobRecommendServiceImpl implements JobRecommendService { ...@@ -60,4 +73,40 @@ public class JobRecommendServiceImpl implements JobRecommendService {
} }
return result; return result;
} }
@Override
public void recommendPosition(Long analysisId, Long userId, Long resumeId) {
log.info("开始推荐岗位");
if (userId == null) {
userId = LoginHelper.getLoginUser().getUserId();
}
final Long thisUserId = userId;
AiRecommendBase aiRecommendBase = aiAnalysisMapper.selectById(analysisId);
if (aiRecommendBase.getRecommendStatus() != null && aiRecommendBase.getRecommendStatus() == 1) {
log.info("用户数据推送中");
return;
}
Integer orlStatus = aiRecommendBase.getRecommendStatus();
log.info("修改岗位意向数据为推送中");
aiAnalysisMapper.updateAnalysis(analysisId, 1, thisUserId, 1);
//开启一个异步线程调用方法
new Thread(new Runnable() {
@Override
public void run() {
//异步调用推荐岗位
log.info("开始推荐岗位");
log.info("推荐岗位参数:analysisId:{},userId:{}", analysisId, thisUserId);
try {
recommendPositionService.recommendPosition(analysisId, thisUserId, resumeId);
} catch (Exception e) {
log.info("恢复岗位意向数据,错误信息:{}", e.toString());
aiAnalysisMapper.updateAnalysis(analysisId, orlStatus, thisUserId, 1);
}
}
}).start();
}
} }
package com.bkty.system.service.jobRecommend.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.nacos.common.utils.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bkty.system.domain.entity.AiPositionRecommendRecord;
import com.bkty.system.domain.entity.FunctionPositionPortraitV2;
import com.bkty.system.domain.vo.AiPositionRecommendRecordVo;
import com.bkty.system.domain.vo.RecommendItem;
import com.bkty.system.mapper.PositionRecommendMapper;
import com.bkty.system.service.jobRecommend.PositionRecommendRedisService;
import com.bkty.system.service.jobRecommend.RecommendPositionService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.utils.DateTimeWrapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author jiangxiaoge
* @description 岗位推荐Redis实现类
* @data 2025/3/28
**/
@Slf4j
@Service
@AllArgsConstructor
public class PositionRecommendRedisServiceImpl implements PositionRecommendRedisService {
private final PositionRecommendMapper positionRecommendMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final RecommendPositionService recommendPositionService;
private static final String ALL_KEY = "recommend:%d:all";
private static final String NOT_READ_KEY = "recommend:%d:not_read";
private static final String FAVORITE_KEY = "recommend:%d:favorite";
private static final String NOT_SUITABLE_KEY = "recommend:%d:not_suitable";
private static final String DELIVERED_KEY = "recommend:%d:delivered";
private static final String RECOMMEND_ITEM_KEY = "recommend:%d:item:%d";
private static final String RECOMMEND_COUNT_KEY = "recommend:count:%d";//推荐次数
@Override
public boolean initRedisData(Long analysisId) {
// 1. 查询所有岗位推荐数据
QueryWrapper<AiPositionRecommendRecord> queryWrapper = Wrappers.query();
queryWrapper.select("id", "resume_id", "operation_type",
"treasures_type", "deliver_type", "city_code", "r_details", "position_score", "jd_md");
queryWrapper.eq("analysis_id", analysisId).eq("is_deleted", false);
List<AiPositionRecommendRecordVo> positions = positionRecommendMapper.selectPositionList(analysisId, null,null, null , null, null, null, null, null);
if (CollectionUtil.isEmpty(positions)){
//无岗位推荐数据
return false;
}
Map<String, List<AiPositionRecommendRecordVo>> queryGroup = positions.stream().collect(Collectors.groupingBy(AiPositionRecommendRecordVo::getCityCode));
List<FunctionPositionPortraitV2> portraitV2s = new ArrayList<>();
for (Map.Entry<String, List<AiPositionRecommendRecordVo>> entry : queryGroup.entrySet()) {
String cityCode = entry.getKey();
portraitV2s.addAll(recommendPositionService.queryPositionList(cityCode, entry.getValue().stream().map(AiPositionRecommendRecordVo::getPositionId).toList()));
}
Map<Long, FunctionPositionPortraitV2> collect = portraitV2s.stream().collect(Collectors.toMap(FunctionPositionPortraitV2::getPositionDataId, p -> p));
// 构造 Redis Key
String allKey = String.format(ALL_KEY, analysisId);
String notReadKey = String.format(NOT_READ_KEY, analysisId);
String favoriteKey = String.format(FAVORITE_KEY, analysisId);
String notSuitableKey = String.format(NOT_SUITABLE_KEY, analysisId);
String deliveredKey = String.format(DELIVERED_KEY, analysisId);
delRedisKey(allKey, notReadKey, favoriteKey, notSuitableKey, deliveredKey);
// 2. 遍历数据,将其存入 Redis
for (AiPositionRecommendRecordVo position : positions) {
savePositionToRedis(collect.get(position.getPositionId()), position, analysisId);
}
return true;
}
private void delRedisKey(String... keys){
redisTemplate.delete(List.of(keys));
}
@Override
public void savePositionToRedis(FunctionPositionPortraitV2 portraitV2, AiPositionRecommendRecordVo position, Long analysisId) {
// 构造 Redis Key
//String allKey = String.format(ALL_KEY, userId);
String notReadKey = String.format(NOT_READ_KEY, analysisId);
String favoriteKey = String.format(FAVORITE_KEY, analysisId);
String notSuitableKey = String.format(NOT_SUITABLE_KEY, analysisId);
String deliveredKey = String.format(DELIVERED_KEY, analysisId);
Long resumeId = position.getResumeId();
Long positionId = position.getId();
RecommendItem item = new RecommendItem();
item.setId(position.getId());
item.setCity(position.getCityCode());
item.setStandardPosition(portraitV2.getStandardPosition());
item.setWelfare(portraitV2.getWelfare());
item.setSalaryRange(portraitV2.getSalaryRange());
item.setCompanyName(portraitV2.getCompanyName());
item.setCompanyNature(portraitV2.getCompanyNature());
item.setCreateTime(DateTimeWrapper.parseFromDate(position.getRecommendTime(), null).toString(Constants.DATE_FULL_FORMAT));
item.setRDetails(position.getRDetails());
item.setPositionScore(position.getPositionScore());
//R<String> stringR = remoteResumeService.queryResumeName(resumeId);
item.setResumeName(position.getResumeName());
item.setJobTitle(position.getJobTitle());
item.setJdMd(position.getJdMd());
try {
item.setMaxPay(Integer.valueOf(portraitV2.getSalaryRange().split("-")[1]));
} catch (Exception e){
item.setMaxPay(0);
}
if (resumeId == null || positionId == null) {
return;
}
// 2.1 存入 "全部" 集合
long createTimeTimestamp = position.getCreateTime().getTime();
//redisTemplate.opsForZSet().add(allKey, position.getId(), createTimeTimestamp);
// 2.2 根据岗位状态存入对应的 Redis 集合
if (position.getPositionScore() == null || position.getPositionScore() == 0) {
redisTemplate.opsForZSet().add(notReadKey, position.getId(), createTimeTimestamp);
}
if (position.getPositionScore() != null && position.getPositionScore() > 4) {
redisTemplate.opsForZSet().add(favoriteKey, position.getId(), createTimeTimestamp);
}
if (position.getPositionScore() != null && position.getPositionScore() < 5) {
redisTemplate.opsForZSet().add(notSuitableKey, position.getId(), createTimeTimestamp);
}
if (position.getDeliverType() != null && position.getDeliverType() == 1) {
redisTemplate.opsForZSet().add(deliveredKey, position.getId(), createTimeTimestamp);
}
// 2.3 将 RecommendItem 对象转换为 JSON 字符串并存储到 Redis 中
try {
String jsonString = JSON.toJSONString(item);
redisTemplate.opsForValue().set(String.format(RECOMMEND_ITEM_KEY, analysisId, positionId), jsonString);
} catch (Exception e) {
e.printStackTrace();
}
}
}
\ No newline at end of file
package com.bkty.system.service.jobRecommend.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.http.HttpUtil;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.KnnQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
import co.elastic.clients.json.JsonData;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
import com.bkty.system.domain.entity.*;
import com.bkty.system.domain.vo.AiRecommendVo;
import com.bkty.system.domain.vo.FunctionPositionPortraitVo;
import com.bkty.system.domain.vo.RerankVo;
import com.bkty.system.domain.vo.ResumeVo;
import com.bkty.system.init.CategoryCacheManager;
import com.bkty.system.llm.JdWriter;
import com.bkty.system.mapper.AiAnalysisMapper;
import com.bkty.system.mapper.CityDataMapper;
import com.bkty.system.mapper.PositionRecommendMapper;
import com.bkty.system.service.coze.CozeApiService;
import com.bkty.system.service.jobRecommend.AiAnalysisService;
import com.bkty.system.service.jobRecommend.PositionRecommendRedisService;
import com.bkty.system.service.jobRecommend.RecommendPositionService;
import com.bkty.system.service.resume.NewEditionResumeService;
import com.bkty.system.service.resume.ResumeMakeService;
import com.bkty.system.utils.MariaDbAdtUtil;
import dev.ai4j.openai4j.OpenAiHttpException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.config.QwenClientSingleton;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.CozeConstsnts;
import org.dromara.common.core.utils.DateTimeWrapper;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;
/**
* @author Zhang Wenbiao
* @description 岗位推荐serviceImpl
* @datetime 2025/12/4 11:59
*/
@Slf4j
@AllArgsConstructor
@Service
public class RecommendPositionServiceImpl implements RecommendPositionService {
private final RedisTemplate<String, String> redisTemplate;
private final AiAnalysisMapper aiAnalysisMapper;
private final AiAnalysisService aiAnalysisService;
private final CityDataMapper cityDataMapper;
private final NewEditionResumeService newEditionResumeService;
private final ResumeMakeService resumeMakeService;
private final PositionRecommendMapper positionRecommendMapper;
private final ElasticsearchOperations elasticsearchOperations;
private final CategoryCacheManager categoryCacheManager;
private final CozeApiService cozeApiService;
private final ApplicationContext applicationContext;
@Override
public void recommendPosition(Long analysisId, Long userId, Long resumeId) {
try {
delProcess(userId);
// 1. 数据准备阶段
AiRecommendVo aiRecommendVo = prepareRecommendationData(analysisId, userId, resumeId);
if (aiRecommendVo == null) return;
// 2. 检查配额限制
// if (!checkQuotaLimit(analysisId, userId)) return;
// 3. 获取简历信息
ResumeVo resumeVo = getResumeInfo(analysisId, userId, resumeId);
if (resumeVo == null) return;
// 4. 获取简历画像
List<FunctionResumeSketch> resumeSketchList = getResumeSketch(analysisId, userId, resumeId);
if (resumeSketchList == null) return;
// 5. 获取历史推荐岗位ID
List<Long> positionIdList = getHistoryPositionIds(analysisId);
// 6. 构建推荐参数
List<RcommendParam> rcommendParamList = buildRecommendParams(aiRecommendVo, resumeSketchList);
List<RcommendParam> qgRcommendParamList = buildNationalRecommendParams(aiRecommendVo, resumeSketchList);
// 7. 执行岗位推荐
List<FunctionPositionPortraitVo> positionPortraitV2List = executePositionRecommend(
rcommendParamList, qgRcommendParamList, positionIdList, userId);
if (CollectionUtil.isEmpty(positionPortraitV2List)) {
handleEmptyRecommendation(analysisId, userId);
return;
}
// 8. 处理推荐结果
handleRecommendResults(positionPortraitV2List, analysisId, userId, resumeId, resumeVo, aiRecommendVo);
} catch (Exception e) {
log.error("岗位推荐过程中发生异常", e);
addProcess(userId, new ProcessItem("error", "岗位推荐过程中发生异常", 0, DateTimeWrapper.now().toString()));
aiAnalysisMapper.updateAnalysis(analysisId, 0, userId, 0);
}
}
public List<FunctionPositionPortraitV2> queryPositionList(String cityCode, List<Long> positionIdList) {
//查询岗位
BoolQuery.Builder bool = QueryBuilders.bool().boost(1.0f);
Query multiMatch = QueryBuilders.terms()
.field("positionDataId")
.terms(termsBuilder -> termsBuilder
.value(positionIdList.stream()
.map(FieldValue::of).toList()))
.build()
._toQuery();
bool.must(multiMatch);
Query bQuery = new Query(bool.build());
NativeQuery build = NativeQuery.builder()
.withQuery(bQuery)
.withSourceFilter(new SourceFilter() {
@Override
public String[] getIncludes() {
return new String[]{"positionDataId", "jobTitle", "industry", "standardPosition", "workPlace", "natureOfPost", "salaryRange", "companyName", "companyNature", "companySize", "expDemand", "eduDemand", "publicPlatform", "platformUrl", "publicTime", "skill", "promote", "mild", "jdHtml"};
}
@Override
public String[] getExcludes() {
return new String[0];
}
})
.withFields("positionDataId", "jobTitle", "industry", "standardPosition", "workPlace", "natureOfPost", "salaryRange", "companyName", "companyNature", "companySize", "expDemand", "eduDemand", "publicPlatform", "platformUrl", "publicTime", "skill", "promote", "mild", "jdHtml")
.withTrackTotalHits(true)
.withSearchType(null)
.withPageable(Pageable.ofSize(positionIdList.size()))
.build();
SearchHits<FunctionPositionPortraitV2> search = elasticsearchOperations.search(build, FunctionPositionPortraitV2.class, IndexCoordinates.of(Constants.ES_INDEX_POSITION + cityCode));
List<FunctionPositionPortraitV2> dataList = new ArrayList<>();
for (SearchHit<FunctionPositionPortraitV2> hit : search) {
FunctionPositionPortraitV2 content = hit.getContent();
content.setFeatureVector(null);
content.setSketch(null);
dataList.add(content);
}
return dataList;
}
/**
* 处理推荐结果
*/
private void handleRecommendResults(List<FunctionPositionPortraitVo> positionPortraitV2List,
Long analysisId, Long userId, Long resumeId,ResumeVo resumeVo,
AiRecommendVo aiRecommendVo) throws InterruptedException {
positionPortraitV2List.sort((o1, o2) -> o2.getRerankScore().compareTo(o1.getRerankScore()));
// 分批处理
List<FunctionPositionPortraitVo> firstBatch = new ArrayList<>();
List<FunctionPositionPortraitVo> secondBatch = new ArrayList<>();
if (positionPortraitV2List.size() > 50) {
firstBatch = new ArrayList<>(positionPortraitV2List.subList(0, 50));
secondBatch = new ArrayList<>(positionPortraitV2List.subList(50, positionPortraitV2List.size()));
} else {
firstBatch = new ArrayList<>(positionPortraitV2List);
}
// 通过coze平台获取分析信息
List<FunctionPositionPortraitVo> matchResults = processPositionAnalysis(
firstBatch, secondBatch, userId, aiRecommendVo, resumeVo);
if (matchResults.isEmpty()) {
handleEmptyAnalysisResults(analysisId, userId);
return;
}
// 处理推荐数据插入
processRecommendationInsert(matchResults, analysisId, userId, resumeId, resumeVo, aiRecommendVo);
}
/**
* 处理推荐数据插入(使用多线程)
*/
private void processRecommendationInsert(List<FunctionPositionPortraitVo> matchResults,
Long analysisId, Long userId, Long resumeId,ResumeVo resumeVo,
AiRecommendVo aiRecommendVo) throws InterruptedException {
List<FunctionPositionPortraitVo> addDataList = filterRecommendData(matchResults, userId);
if (addDataList.isEmpty()) {
handleEmptyRecommendation(analysisId, userId);
return;
}
// 使用多线程处理数据插入
insertRecommendationDataConcurrently(addDataList, userId, resumeVo, aiRecommendVo,analysisId);
// 更新分析状态和缓存
aiAnalysisMapper.updateAnalysis(analysisId, 2, userId, 0);
PositionRecommendRedisService recommendRedisService = applicationContext.getBean(PositionRecommendRedisService.class);
recommendRedisService.initRedisData(analysisId);
}
/**
* 使用多线程插入推荐数据
*/
private void insertRecommendationDataConcurrently(List<FunctionPositionPortraitVo> addDataList,
Long userId, ResumeVo resumeVo, AiRecommendVo aiRecommendVo, Long analysisId)
throws InterruptedException {
ExecutorService insertExecutor = Executors.newFixedThreadPool(10);
List<CompletableFuture<Void>> insertFutures = new ArrayList<>();
Date date = new Date();
for (FunctionPositionPortraitVo portraitVo : addDataList) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
AiPositionRecommendRecord recommendRecord = buildRecommendRecord(portraitVo, userId, date,analysisId);
// generateJobApplicationAdviceInBatches(resumeVo, aiRecommendVo, portraitVo, recommendRecord);
// generateCompanyScaleAndIndustry(recommendRecord);
positionRecommendMapper.insert(recommendRecord);
} catch (Exception e) {
log.error("插入推荐数据失败,岗位ID: {}", portraitVo.getPositionDataId(), e);
}
}, insertExecutor);
insertFutures.add(future);
}
CompletableFuture.allOf(insertFutures.toArray(new CompletableFuture[0])).join();
insertExecutor.shutdown();
if (!insertExecutor.awaitTermination(10, TimeUnit.MINUTES)) {
insertExecutor.shutdownNow();
}
}
/**
* 构建推荐记录
*/
private AiPositionRecommendRecord buildRecommendRecord(FunctionPositionPortraitVo portraitVo, Long userId, Date date, Long analysisId) {
AiPositionRecommendRecord recommendRecord = new AiPositionRecommendRecord();
recommendRecord.setUserId(userId);
recommendRecord.setPositionId(portraitVo.getPositionDataId());
recommendRecord.setTreasuresType(0);
recommendRecord.setAlreadyType(0);
recommendRecord.setDeliverType(0);
recommendRecord.setOperationType(0);
recommendRecord.setRScore(Integer.valueOf(portraitVo.getScore()));
recommendRecord.setRDetails(portraitVo.getDetails());
recommendRecord.setRecommendTime(date);
recommendRecord.setResumeId(Long.valueOf(portraitVo.getResumeId()));
recommendRecord.setCityCode(portraitVo.getCityCode());
recommendRecord.setAnalyzeJson(JSON.toJSONString(portraitVo.getMatchResult()));
recommendRecord.setEvaluationJson(JSON.toJSONString(portraitVo.getJobEvaluation()));
recommendRecord.setJobTitle(portraitVo.getJobTitle());
recommendRecord.setJobProfession(portraitVo.getIndustry());
recommendRecord.setStandardPosition(portraitVo.getStandardPosition());
recommendRecord.setCompanySize(portraitVo.getCompanySize());
recommendRecord.setCompanyNature(portraitVo.getCompanyNature());
recommendRecord.setExpDemand(portraitVo.getExpDemand());
recommendRecord.setEduDemand(portraitVo.getEduDemand());
recommendRecord.setPublicPlatform(portraitVo.getPublicPlatform());
recommendRecord.setCompanyName(portraitVo.getCompanyName());
recommendRecord.setJdMd(portraitVo.getJdMd());
recommendRecord.setMaxPay(portraitVo.getPay());
recommendRecord.setCreateTime(date);
recommendRecord.setUpdateTime(date);
recommendRecord.setAnalysisId(analysisId);
return recommendRecord;
}
/**
* 过滤推荐数据
*/
private List<FunctionPositionPortraitVo> filterRecommendData(List<FunctionPositionPortraitVo> matchResults, Long userId) {
List<FunctionPositionPortraitVo> addDataList = matchResults;
if (matchResults.size() > 10) {
addDataList = matchResults.subList(0, 10);
}
return addDataList;
}
/**
* 处理空分析结果
*/
private void handleEmptyAnalysisResults(Long analysisId, Long userId) {
addProcess(userId, new ProcessItem("failure", NOTIFICATION_TITLE_ERROR, 100, DateTimeWrapper.now().toString()));
aiAnalysisMapper.updateAnalysis(analysisId, 2, userId, 0);
}
/**
* 处理岗位分析
*/
private List<FunctionPositionPortraitVo> processPositionAnalysis(
List<FunctionPositionPortraitVo> firstBatch,
List<FunctionPositionPortraitVo> secondBatch,
Long userId, AiRecommendVo aiRecommendVo, ResumeVo resumeVo) {
ExecutorService executor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
List<CompletableFuture<Void>> futures = new ArrayList<>();
List<FunctionPositionPortraitVo> matchResults = Collections.synchronizedList(new ArrayList<>());
int totalJobCount = firstBatch.size() + secondBatch.size();
AtomicInteger processedCount = new AtomicInteger(0);
int baseProgress = 50;
double progressPerJob = (double) 50 / totalJobCount;
AtomicLong lastPushTime = new AtomicLong(0);
// 处理第一批
for (FunctionPositionPortraitVo resultVo : firstBatch) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
sendCozeByPositionData(userId, resultVo, aiRecommendVo, resumeVo, processedCount,
baseProgress, progressPerJob, lastPushTime, matchResults);
} catch (Exception e) {
log.error("处理岗位分析失败", e);
log.error("处理岗位分析失败的id:{}", resultVo.getPositionDataId());
}
}, executor);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
executor.shutdown();
try {
if (!executor.awaitTermination(50, TimeUnit.MINUTES)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
matchResults.sort((o1, o2) -> o2.getScore().compareTo(o1.getScore()));
// 如果结果不足10个,处理第二批
if (matchResults.size() < 10 && CollectionUtil.isNotEmpty(secondBatch)) {
processSecondBatch(secondBatch, matchResults, userId, aiRecommendVo, resumeVo,
processedCount, baseProgress, progressPerJob, lastPushTime);
}
return matchResults;
}
/**
* 处理第二批岗位分析
*/
private void processSecondBatch(List<FunctionPositionPortraitVo> secondBatch,
List<FunctionPositionPortraitVo> matchResults,
Long userId, AiRecommendVo aiRecommendVo, ResumeVo resumeVo,
AtomicInteger processedCount, int baseProgress,
double progressPerJob, AtomicLong lastPushTime) {
ExecutorService executor2 = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
List<CompletableFuture<Void>> futuresSecond = new ArrayList<>();
for (FunctionPositionPortraitVo batch : secondBatch) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
if (matchResults.size() < 10) {
sendCozeByPositionData(userId, batch, aiRecommendVo, resumeVo, processedCount,
baseProgress, progressPerJob, lastPushTime, matchResults);
}
} catch (Exception e) {
log.error("处理岗位分析失败", e);
log.error("处理岗位分析失败的id:{}", batch.getPositionDataId());
}
}, executor2);
futuresSecond.add(future);
}
if (!futuresSecond.isEmpty()) {
try {
CompletableFuture.allOf(futuresSecond.toArray(new CompletableFuture[0])).get(5, TimeUnit.MINUTES);
} catch (Exception e) {
log.warn("处理secondBatch时超时或发生异常", e);
}
}
executor2.shutdown();
try {
if (!executor2.awaitTermination(50, TimeUnit.MINUTES)) {
executor2.shutdownNow();
}
} catch (InterruptedException e) {
executor2.shutdownNow();
Thread.currentThread().interrupt();
}
}
private void sendCozeByPositionData(Long userId, FunctionPositionPortraitVo resultVo, AiRecommendVo aiRecommendVo, ResumeVo resumeVo, AtomicInteger processedCount, int baseProgress, double progressPerJob, AtomicLong lastPushTime, List<FunctionPositionPortraitVo> matchResults) throws Exception {
if (resultVo.getPay() != null && resultVo.getPay() == 0){
//薪资未知,数据二次确认
Map<String, Object> stringObjectMap = MariaDbAdtUtil.queryRecruitQqxbById(resultVo.getPositionDataId());
try {
//res = writer.writer(promptTemplate.template());
QwenClientSingleton instance = QwenClientSingleton.getInstance("https://xzt-llm-dev.jinsehuaqin.com",
"token-abc123",
"/mnt/app/llm/Qwen3/Qwen/Qwen3-8B");
String chat = instance.chat(JdWriter.PAY_MESSAGE, JSON.toJSONString(stringObjectMap));
JSONObject jsonObject = JSON.parseObject(chat);
String salaryRange = jsonObject.getString("salaryRange");
if (salaryRange.contains("-") &&
!"未知".equals(salaryRange) &&
!salaryRange.contains("面议")) {
resultVo.setPay(Integer.valueOf(salaryRange.split("-")[0]));
resultVo.setStandardPosition(salaryRange);
//修改es数据
updatePositionSalaryInfoWithClient(resultVo.getPositionDataId(), resultVo.getCityCode(), salaryRange, resultVo.getPay());
}
} catch (OpenAiHttpException | IOException e) {
throw new RuntimeException(e);
} catch (Exception e) {
log.error("薪资确认错误", e);
}
}
JSONObject workflowObj = sendCozeWorkflow(resultVo, aiRecommendVo, resumeVo);
System.out.println("工作流返回信息:" + workflowObj);
if (workflowObj == null) {
log.error("工作流返回错误");
}
JobEvaluation strategy = workflowObj.getObject("Strategy", JobEvaluation.class);
MatchResult matching = workflowObj.getObject("matching", MatchResult.class);
String jdMd = setJdMd(resultVo.getJdHtml());
resultVo.setJdMd(jdMd);
resultVo.setScore(Objects.toString(matching.getComprehensiveMatchScore(), "0"));
resultVo.setDetails(matching.getComprehensiveMatchRecommendation());
resultVo.setMatchResult(matching);
resultVo.setJobEvaluation(strategy);
// 更新进度
int currentProcessed = processedCount.incrementAndGet();
int progress = baseProgress + (int) Math.min(50, currentProcessed * progressPerJob);
//String progressTip = String.format("已完成岗位匹配分析:%d%%", progress);
// 新增:Redis 推送逻辑
long currentTime = System.currentTimeMillis();
if (currentTime - lastPushTime.get() > 3000) { // 大于3秒才发送
String companyName = resultVo.getCompanyName(); // 获取公司名称
String jobTitle = resultVo.getJobTitle(); // 获取岗位名称
String tip = String.format("小职正通过智能模型评估您与 %s · %s 的匹配度…", companyName, jobTitle);
addProcess(userId, new ProcessItem("delta", tip, progress, DateTimeWrapper.now().toString())); // 调用你原来的 Redis 推送方法
lastPushTime.set(currentTime); // 更新上次发送时间
}
if (Integer.parseInt(resultVo.getScore()) > 59) {
matchResults.add(resultVo);
}
}
public String setJdMd(String jdHtml) {
//ChatLanguageModel chatModel = LlmModelConfig.getChatLanguageModel();
//JdWriter writer = AiServices.create(JdWriter.class, chatModel);
//PromptTemplate promptTemplate = new PromptTemplate(jdHtml);
String res = null;
try {
//res = writer.writer(promptTemplate.template());
QwenClientSingleton instance = QwenClientSingleton.getInstance("https://xzt-llm-dev.jinsehuaqin.com",
"token-abc123",
"/mnt/app/llm/Qwen3/Qwen/Qwen3-8B");
res = instance.chat(JdWriter.SYSTEM_MESSAGE, jdHtml);
} catch (OpenAiHttpException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return res;
}
private JSONObject sendCozeWorkflow(FunctionPositionPortraitVo resultVo, AiRecommendVo aiRecommendVo, ResumeVo resumeVo) throws Exception {
String cozeToken = cozeApiService.getCozeToken();
// 构建请求头
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put(HttpHeaders.AUTHORIZATION, CozeConstsnts.COZE_TOKEN_BEARER + cozeToken);
List<Map<String, String>> jobList = new ArrayList<>();
for (String s : aiRecommendVo.getCareer()) {
Map<String, String> levelNameMapByLevel3Name = categoryCacheManager.getLevelNameMapByLevel3Name(s);
jobList.add(levelNameMapByLevel3Name);
}
aiRecommendVo.setJobList(jobList);
Map<String, Object> paramMap = Map.of("input_job_details", JSON.toJSONString(resultVo),
"input_user_resume", JSON.toJSONString(resumeVo),
"input_job_willing", JSON.toJSONString(aiRecommendVo));
/*Map<String, Object> body = Map.of("workflow_id", "7513774076784312335",
"is_async", false,
"parameters", paramMap);*/
Map<String, Object> body = Map.of("workflow_id", "7553630384651632680",
"is_async", false,
"parameters", paramMap);
String uri = "https://api.coze.cn/v1/workflow/run";
// 使用 Hutool 的 HttpUtil 发送 POST 请求
try {
// 将Map转换为JSON字符串
String jsonBody = JSON.toJSONString(body);
cn.hutool.http.HttpRequest request = cn.hutool.http.HttpRequest.post(uri)
.header("Content-Type", "application/json")
.header(HttpHeaders.AUTHORIZATION, CozeConstsnts.COZE_TOKEN_BEARER + cozeToken)
.body(jsonBody)
.timeout(30000); // 设置超时时间 30 秒
cn.hutool.http.HttpResponse response = request.execute();
String responseBody = response.body();
JSONObject jsonObject = JSON.parseObject(responseBody);
if (jsonObject != null && jsonObject.getInteger("code") == 0) {
return jsonObject.getJSONObject("data");
} else {
log.error("调用岗位智推分析工作流失败:\n" + responseBody);
}
} catch (Exception e) {
log.error("发送HTTP请求时发生异常: ", e);
throw new Exception("请求发送失败: " + e.getMessage(), e);
}
return null;
}
/**
* 使用Elasticsearch Client更新岗位薪资信息(支持动态索引)
*
* @param positionDataId 岗位数据ID
* @param cityCode 城市代码,用于构建索引名称
* @param salaryRange 薪资范围
* @param pay 薪资数值
*/
public void updatePositionSalaryInfoWithClient(Long positionDataId, String cityCode, String salaryRange, Integer pay) {
try {
// 构建索引坐标
IndexCoordinates indexCoordinates = IndexCoordinates.of(Constants.ES_INDEX_POSITION + cityCode);
// 构建更新脚本
org.springframework.data.elasticsearch.core.document.Document document =
org.springframework.data.elasticsearch.core.document.Document.create();
document.put("salaryRange", salaryRange);
document.put("pay", pay);
// 构建更新请求
org.springframework.data.elasticsearch.core.query.UpdateQuery updateQuery =
org.springframework.data.elasticsearch.core.query.UpdateQuery.builder(String.valueOf(positionDataId))
.withDocument(document)
.withDocAsUpsert(true)
.build();
// 执行更新操作
elasticsearchOperations.update(updateQuery, indexCoordinates);
log.info("成功更新岗位薪资信息, positionDataId: {}, cityCode: {}", positionDataId, cityCode);
} catch (Exception e) {
log.error("更新岗位薪资信息失败, positionDataId: {}, cityCode: {}", positionDataId, cityCode, e);
throw new RuntimeException("更新ES文档失败: " + e.getMessage(), e);
}
}
/**
* 处理空推荐结果
*/
private void handleEmptyRecommendation(Long analysisId, Long userId) {
addProcess(userId, new ProcessItem("failure", NOTIFICATION_TITLE_ERROR, 100, DateTimeWrapper.now().toString()));
aiAnalysisMapper.updateAnalysis(analysisId, 2, userId, 0);
String nowString = DateTimeWrapper.now().toString();
}
private final static String NOTIFICATION_TITLE_ERROR = "岗位智推未成功";
private static final List<String> PLATFORM_TIPS = Arrays.asList(
"小职正在通过智能算法,从猎聘网为您搜寻最合适的岗位中...",
"小职正在通过智能算法,从boss直聘为您搜寻最合适的岗位中...",
"小职正在通过智能算法,从智联招聘为您搜寻最合适的岗位中...",
"小职正在通过智能算法,从前程无忧为您搜寻最合适的岗位中...",
"小职正在通过智能算法,从实习僧网为您搜寻最合适的岗位中...",
"小职正在通过智能算法,从应届生求职为您搜寻最合适的岗位中...",
"小职正在通过智能算法,从牛客网为您搜寻最合适的岗位中..."
);
/**
* 执行岗位推荐
*/
private List<FunctionPositionPortraitVo> executePositionRecommend(
List<RcommendParam> rcommendParamList,
List<RcommendParam> qgRcommendParamList,
List<Long> positionIdList, Long userId) {
// 显示平台提示信息
for (String platformTip : PLATFORM_TIPS) {
addProcess(userId, new ProcessItem("delta", platformTip, 50, DateTimeWrapper.now().toString()));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
List<FunctionPositionPortraitVo> positionPortraitV2List = new ArrayList<>();
try {
positionPortraitV2List = recommend(rcommendParamList, positionIdList);
// 处理全国岗位查询时需要剔除的岗位
if (positionIdList == null && CollectionUtil.isNotEmpty(positionPortraitV2List)) {
positionIdList = positionPortraitV2List.stream().map(FunctionPositionPortraitV2::getPositionDataId).toList();
} else if (CollectionUtil.isNotEmpty(positionIdList)) {
positionIdList.addAll(positionPortraitV2List.stream().map(FunctionPositionPortraitV2::getPositionDataId).toList());
}
positionPortraitV2List.addAll(recommend(qgRcommendParamList, positionIdList));
} catch (Exception e) {
log.error("岗位推荐过程中发生异常", e);
}
return positionPortraitV2List;
}
/**
* 岗位推荐
*
* @return
*/
private List<FunctionPositionPortraitVo> recommend(List<RcommendParam> param, List<Long> positionIdList) {
List<FunctionPositionPortraitVo> result = new CopyOnWriteArrayList<>();
Set<Long> pIdSet = ConcurrentHashMap.newKeySet();
// 创建线程池
ExecutorService executor = new ThreadPoolExecutor(
16, // 核心线程数
32, // 最大线程数
0L, // 线程空闲时间
TimeUnit.MILLISECONDS, // 时间单位
new LinkedBlockingQueue<Runnable>() // 任务队列
);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (RcommendParam rcommendParam : param) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
List<FunctionPositionPortraitVo> vos = queryRecommendPositionByCityAndJobTitle(rcommendParam.cityCode,
rcommendParam.jobTitle, rcommendParam.pay,
rcommendParam.resumeId, positionIdList, rcommendParam.floatList, rcommendParam.sketch, rcommendParam.isIntern);
//使用重排序模型排序
List<RerankVo> rerank = rerank(vos, rcommendParam.sketch);
for (RerankVo vo : rerank) {
synchronized (pIdSet) {
if (!pIdSet.contains(vo.getPositionData().getPositionDataId())) {
vo.getPositionData().setRerankScore(vo.getScore());
result.add(vo.getPositionData());
pIdSet.add(vo.getPositionData().getPositionDataId());
}
}
}
}, executor);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
executor.shutdown();
try {
// 等待所有任务完成,最多等待5分钟
if (!executor.awaitTermination(5, TimeUnit.MINUTES)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
//单线程调用
/*List<FunctionPositionPortraitVo> result = new ArrayList<>();
Set<Long> pIdSet = new HashSet<>();
for (RcommendParam rcommendParam : param) {
List<FunctionPositionPortraitVo> vos = queryRecommendPositionByCityAndJobTitle(rcommendParam.cityCode, rcommendParam.jobTitle, rcommendParam.pay, rcommendParam.resumeId, positionIdList);
for (FunctionPositionPortraitVo vo : vos) {
if (!pIdSet.contains(vo.getPositionDataId())){
result.add(vo);
pIdSet.add(vo.getPositionDataId());
}
}
if (matchingSize(result) > 19){
List<FunctionPositionPortraitVo> list = result.stream().filter((functionPositionPortraitVo -> "匹配".equals(functionPositionPortraitVo.getWhetherItMatches()))).toList();
return list.subList(0, 20);
}
}
Map<Boolean, List<FunctionPositionPortraitVo>> collect = result.stream().collect(Collectors.groupingBy(vo->vo.getWhetherItMatches().equals("匹配")));
List<FunctionPositionPortraitVo> matchingList = new ArrayList<>();
if (collect.containsKey(true)){
matchingList.addAll(collect.get(true));
}
int size = 20 - matchingList.size();
for (int i = 0; i < size; i++) {
matchingList.add(collect.get(false).get(i));
}*/
return result;
}
private List<RerankVo> rerank(List<FunctionPositionPortraitVo> positionPortraitV2s, String resumeSketch) {
List<RerankVo> rerankVos = new ArrayList<>();
for (List<FunctionPositionPortraitVo> portraitV2s : Lists.partition(positionPortraitV2s, 5)) {
Map<String, Object> bodyMap = constructionBody(portraitV2s, resumeSketch);
// 使用 Hutool 发送 POST 请求
String url = "https://xzt-rma-dev.jinsehuaqin.com/score";
String jsonBody = JSON.toJSONString(bodyMap);
try {
String response = HttpUtil.post(url, jsonBody);
log.info("重排序接口访问结果:{}", response);
// 解析响应为 Map
Map<String, List<Float>> result = JSON.parseObject(response, new TypeReference<Map<String, List<Float>>>() {
});
List<Float> scores = result.get("scores");
for (int i = 0; i < scores.size(); i++) {
Float score = scores.get(i);
rerankVos.add(new RerankVo(portraitV2s.get(i), score));
}
} catch (Exception e) {
log.error("调用重排序接口失败:{}", e.getMessage(), e);
}
}
rerankVos.sort((o1, o2) -> o2.getScore().compareTo(o1.getScore()));
return rerankVos;
}
private final static String RERANK_INSTRUCTION = """
检索最符合求职者专业以及技能的岗位。
判断条件:求职者信息中不具备岗位要求的技能则不合适,求职者信息中不具备岗位要求的经验则不合适。
求职者信息:%s
""";
private Map<String, Object> constructionBody(List<FunctionPositionPortraitVo> portraitV2s, String resumeSketch) {
Map<String, Object> body = new HashMap<>();
body.put("instruction", RERANK_INSTRUCTION + resumeSketch);
List<Map<String, Object>> pairs = new ArrayList<>(portraitV2s.size());
for (FunctionPositionPortraitV2 portraitV2 : portraitV2s) {
Map<String, Object> pair = new HashMap<>();
Map<String, Object> query = new HashMap<>();
query.put("jobTitle", portraitV2.getJobTitle());
query.put("standardPosition", portraitV2.getStandardPosition());
query.put("sketch", portraitV2.getSketch());
pair.put("query", JSON.toJSONString(query));
Map<String, Object> document = new HashMap<>();
document.put("jdHtml", portraitV2.getJdHtml());
pair.put("document", JSON.toJSONString(document));
pairs.add(pair);
}
body.put("pairs", pairs);
return body;
}
/**
*
* @param cityCode 城市代码
* @param jobTitle 岗位名称
* @param pay 薪资
* @param resumeId 简历id
* @param positionIdList 岗位id
* @param titleVector 岗位向量
* @param resumeSketch 简历简述
* @param isIntern 是否实习
* @return
*/
private List<FunctionPositionPortraitVo> queryRecommendPositionByCityAndJobTitle(String cityCode,
String jobTitle,
String pay,
Long resumeId,
List<Long> positionIdList,
List<Float> titleVector,
String resumeSketch,
String isIntern) {
List<FunctionPositionPortraitVo> resultList = new ArrayList<>();
//JobTitleVector titleVector = positionSynService.queryJobTitleVector(jobTitle);
//生成查询词向量
//ChatLanguageModel chatModel = LlmModelConfig.getChatLanguageModel();
if (titleVector != null) {
BoolQuery.Builder bool = QueryBuilders.bool().boost(1.0f);
Query multiMatch = QueryBuilders.matchPhrase()
.field("standardPosition")
.query(jobTitle)
.build()
._toQuery();
bool.must(multiMatch);
if (CollectionUtil.isNotEmpty(positionIdList)) {
Query notIdQuery = QueryBuilders.terms()
.field("positionDataId")
.terms(termsBuilder -> termsBuilder
.value(positionIdList.stream()
.map(FieldValue::of)
.toList()))
.build()
._toQuery();
bool.mustNot(notIdQuery);
}
if (StringUtils.isNotBlank(isIntern)) {
Query isInternMatch = QueryBuilders.matchPhrase()
.field("isIntern")
.query(isIntern)
.build()
._toQuery();
bool.must(isInternMatch);
}
if (cityCode.equals("0000")) {
//查询全国岗位
Query match = QueryBuilders.match().field("workPlace").query("全国").build()._toQuery();
bool.must(match);
}
//需要修改画像信息金额字段要分开
BoolQuery.Builder salaryBool = QueryBuilders.bool().boost(1.0f);
int payInt = Integer.parseInt(pay);
Query query1 = QueryBuilders.range()
.field("pay")
.gte(JsonData.of(payInt - 1500)).lte(JsonData.of(payInt + 5000))
.build()._toQuery();
// 薪资为"面议"的条件
Query negotiationQuery = QueryBuilders.term()
.field("salaryRange")
.value("面议")
.build()._toQuery();
salaryBool.should(query1);
salaryBool.should(negotiationQuery);
salaryBool.minimumShouldMatch("1");
bool.filter(salaryBool.build()._toQuery());
//bool.filter(query1);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 获取近20天的日期列表(从新到旧)
List<String> last20Days = IntStream.range(0, 30)
.mapToObj(i -> LocalDate.now().minusDays(i))
.map(date -> date.format(formatter))
.toList();
Query publicTimeQuery = QueryBuilders.terms()
.field("publicTime")
.terms(termsBuilder -> termsBuilder
.value(last20Days.stream()
.map(FieldValue::of)
.toList()))
.build()
._toQuery();
bool.must(publicTimeQuery);
KnnQuery query = KnnQuery.of(k -> k.field("featureVector")
.boost(0.5f)
.k(100)
.numCandidates(6000)
.queryVector(titleVector).filter(bool.build()._toQuery()));
NativeQuery build = NativeQuery.builder()
//.withQuery(bool.build()._toQuery())
//.withQuery(functionScoreQuery)
.withKnnQuery(query)
.withSourceFilter(new SourceFilter() {
@Override
public String[] getIncludes() {
return new String[]{"positionDataId", "jobTitle", "industry", "standardPosition", "workPlace", "natureOfPost", "salaryRange", "companyName", "companyNature", "companySize", "expDemand", "eduDemand", "publicPlatform", "platformUrl", "duty", "skill", "promote", "mild", "jdHtml", "sketch", "pay"};
}
@Override
public String[] getExcludes() {
return new String[0];
}
})
.withTrackTotalHits(true)
.withSearchType(null)
.withPageable(Pageable.ofSize(100))
.build();
SearchHits<FunctionPositionPortraitV2> search = elasticsearchOperations.search(build, FunctionPositionPortraitV2.class, IndexCoordinates.of(Constants.ES_INDEX_POSITION + cityCode));
//获取查询的数据
for (SearchHit<FunctionPositionPortraitV2> hit : search) {
FunctionPositionPortraitVo vo = new FunctionPositionPortraitVo();
BeanUtils.copyProperties(hit.getContent(), vo);
vo.setFeatureVector(null);
//vo.setSketch(null);
vo.setResumeId(String.valueOf(resumeId));
vo.setResumeSketch(resumeSketch);
vo.setScoreFloat(hit.getScore());
vo.setCityCode(cityCode);
resultList.add(vo);
}
}
log.info("推荐岗位数据完成:{}条", resultList.size());
if (resultList.size() < 100) {
List<FunctionPositionPortraitVo> functionPositionPortraitVos = queryRecommendPositionByLevel2(cityCode,
jobTitle,
pay,
resumeId,
positionIdList,
titleVector,
resumeSketch,
isIntern);
//将resultList使用functionPositionPortraitVos补齐到100个
resultList.addAll(functionPositionPortraitVos);
}
return resultList.size() > 100 ? resultList.subList(0, 100) : resultList;
}
/**
* 普通查询岗位数量不足时使用关联岗位查询
*
* @param cityCode
* @param jobTitle
* @param pay
* @param resumeId
* @param positionIdList
* @param titleVector
* @param resumeSketch
* @return
*/
private List<FunctionPositionPortraitVo> queryRecommendPositionByLevel2(String cityCode,
String jobTitle,
String pay,
Long resumeId,
List<Long> positionIdList,
List<Float> titleVector,
String resumeSketch,
String isIntern) {
List<FunctionPositionPortraitVo> resultList = new ArrayList<>();
BoolQuery.Builder bool = QueryBuilders.bool().boost(1.0f);
List<String> byLevel3Name = categoryCacheManager.getLevel2NameByLevel3Name(jobTitle);
BoolQuery.Builder byLevel3NameBool = QueryBuilders.bool().boost(1.0f);
;
for (String level3Name : byLevel3Name) {
if (!level3Name.equals(jobTitle)) {
Query match = QueryBuilders.matchPhrase()
.field("standardPosition")
.query(level3Name)
.build()
._toQuery();
byLevel3NameBool.should(match); // should = OR
}
}
byLevel3NameBool.minimumShouldMatch("1");
bool.must(byLevel3NameBool.build()._toQuery());
Query multiMatch = QueryBuilders.terms()
.field("standardPosition")
.terms(termsBuilder -> termsBuilder
.value(byLevel3Name.stream()
.map(FieldValue::of)
.toList()))
.build()
._toQuery();
bool.must(multiMatch);
if (CollectionUtil.isNotEmpty(positionIdList)) {
Query notIdQuery = QueryBuilders.terms()
.field("positionDataId")
.terms(termsBuilder -> termsBuilder
.value(positionIdList.stream()
.map(FieldValue::of)
.toList()))
.build()
._toQuery();
bool.mustNot(notIdQuery);
}
if (StringUtils.isNotBlank(isIntern)){
Query isInternMatch = QueryBuilders.matchPhrase()
.field("isIntern")
.query(isIntern)
.build()
._toQuery();
bool.must(isInternMatch);
}
if (cityCode.equals("0000")) {
//查询全国岗位
Query match = QueryBuilders.match().field("workPlace").query("全国").build()._toQuery();
bool.must(match);
}
//需要修改画像信息金额字段要分开
BoolQuery.Builder salaryBool = QueryBuilders.bool().boost(1.0f);
int payInt = Integer.parseInt(pay);
Query query1 = QueryBuilders.range()
.field("pay")
.gte(JsonData.of(payInt - 1500)).lte(JsonData.of(payInt + 5000))
.build()._toQuery();
// 薪资为"面议"的条件
Query negotiationQuery = QueryBuilders.term()
.field("salaryRange")
.value("面议")
.build()._toQuery();
salaryBool.should(query1);
salaryBool.should(negotiationQuery);
salaryBool.minimumShouldMatch("1");
bool.filter(salaryBool.build()._toQuery());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 获取近20天的日期列表(从新到旧)
List<String> last20Days = IntStream.range(0, 30)
.mapToObj(i -> LocalDate.now().minusDays(i))
.map(date -> date.format(formatter))
.toList();
Query publicTimeQuery = QueryBuilders.terms()
.field("publicTime")
.terms(termsBuilder -> termsBuilder
.value(last20Days.stream()
.map(FieldValue::of)
.toList()))
.build()
._toQuery();
bool.must(publicTimeQuery);
KnnQuery query = KnnQuery.of(k -> k.field("featureVector")
.boost(0.5f)
.k(100)
.numCandidates(6000)
.queryVector(titleVector).filter(bool.build()._toQuery()));
NativeQuery build = NativeQuery.builder()
//.withQuery(bool.build()._toQuery())
//.withQuery(functionScoreQuery)
.withKnnQuery(query)
.withSourceFilter(new SourceFilter() {
@Override
public String[] getIncludes() {
return new String[]{"positionDataId", "jobTitle", "industry", "standardPosition", "workPlace", "natureOfPost", "salaryRange", "companyName", "companyNature", "companySize", "expDemand", "eduDemand", "publicPlatform", "platformUrl", "duty", "skill", "promote", "mild", "jdHtml", "sketch", "pay"};
}
@Override
public String[] getExcludes() {
return new String[0];
}
})
.withTrackTotalHits(true)
.withSearchType(null)
.withPageable(Pageable.ofSize(100))
.build();
SearchHits<FunctionPositionPortraitV2> search = elasticsearchOperations.search(build, FunctionPositionPortraitV2.class, IndexCoordinates.of(Constants.ES_INDEX_POSITION + cityCode));
//获取查询的数据
for (SearchHit<FunctionPositionPortraitV2> hit : search) {
FunctionPositionPortraitVo vo = new FunctionPositionPortraitVo();
BeanUtils.copyProperties(hit.getContent(), vo);
vo.setFeatureVector(null);
//vo.setSketch(null);
vo.setResumeId(String.valueOf(resumeId));
vo.setResumeSketch(resumeSketch);
vo.setScoreFloat(hit.getScore());
vo.setCityCode(cityCode);
resultList.add(vo);
}
return resultList;
}
/**
* 构建全国推荐参数
*/
private List<RcommendParam> buildNationalRecommendParams(AiRecommendVo aiRecommendVo, List<FunctionResumeSketch> resumeSketchList) {
String jobTypeString = convertJobType(aiRecommendVo.getJobType());
String finalJobTypeString = jobTypeString;
List<RcommendParam> qgRcommendParamList = new ArrayList<>();
for (String jobTitle : aiRecommendVo.getCareer()) {
for (FunctionResumeSketch resumeSketch : resumeSketchList) {
RcommendParam rcommendParam = new RcommendParam();
rcommendParam.cityCode = "0000";
rcommendParam.jobTitle = jobTitle;
rcommendParam.pay = aiRecommendVo.getPay();
rcommendParam.isIntern = finalJobTypeString;
rcommendParam.resumeId = resumeSketch.getResumeId();
rcommendParam.floatList = JSON.parseObject(resumeSketch.getVectorQuantity(), new TypeReference<List<Float>>() {});
rcommendParam.sketch = resumeSketch.getSketch();
qgRcommendParamList.add(rcommendParam);
}
}
return qgRcommendParamList;
}
/**
* 构建推荐参数
*/
private List<RcommendParam> buildRecommendParams(AiRecommendVo aiRecommendVo, List<FunctionResumeSketch> resumeSketchList) {
Map<String, String> cityMap = extractProfileData(aiRecommendVo);
String jobTypeString = convertJobType(aiRecommendVo.getJobType());
List<RcommendParam> rcommendParamList = new ArrayList<>();
String finalJobTypeString = jobTypeString;
cityMap.forEach((k, v) -> {
for (String jobTitle : aiRecommendVo.getCareer()) {
for (FunctionResumeSketch resumeSketch : resumeSketchList) {
RcommendParam rcommendParam = new RcommendParam();
rcommendParam.cityCode = v;
rcommendParam.jobTitle = jobTitle;
rcommendParam.pay = aiRecommendVo.getPay();
rcommendParam.isIntern = finalJobTypeString;
rcommendParam.resumeId = resumeSketch.getResumeId();
rcommendParam.floatList = JSON.parseObject(resumeSketch.getVectorQuantity(), new TypeReference<List<Float>>() {});
rcommendParam.sketch = resumeSketch.getSketch();
rcommendParamList.add(rcommendParam);
}
}
});
return rcommendParamList;
}
/**
* 转换工作类型
*/
private String convertJobType(String jobType) {
if (StringUtils.isNotBlank(jobType)) {
if (jobType.equals("1")) {
return "是";
}
if (jobType.equals("2")) {
return "否";
}
}
return null;
}
/**
* 获取历史推荐岗位ID
*/
private List<Long> getHistoryPositionIds(Long analysisId) {
List<Long> positionIdList = this.positionRecommendMapper.selectPositionIdList(analysisId);
if (CollectionUtil.isEmpty(positionIdList)) {
positionIdList = null;
}
return positionIdList;
}
/**
* 获取简历画像
*/
private List<FunctionResumeSketch> getResumeSketch(Long analysisId, Long userId, Long resumeId) {
addProcess(userId, new ProcessItem("delta", "小职正在通过智能分析,为您构建简历画像中...", 12, DateTimeWrapper.now().toString()));
FunctionResumeSketch resumeSketchR = null;
try {
resumeSketchR = resumeMakeService.queryResumeSketch(resumeId);
} catch (Exception e) {
addProcess(userId, new ProcessItem("error", "获取简历失败,结束推荐岗位", 0, DateTimeWrapper.now().toString()));
log.error("获取简历向量失败");
aiAnalysisMapper.updateAnalysis(analysisId, 0, userId, 0);
return null;
}
List<FunctionResumeSketch> resumeSketchList = new ArrayList<>();
resumeSketchList.add(resumeSketchR);
return resumeSketchList;
}
/**
* 获取简历信息
*/
private ResumeVo getResumeInfo(Long analysisId, Long userId, Long resumeId) {
ResumeVo resumeVo = null;
try {
resumeVo = newEditionResumeService.queryNewEditionResumeId(String.valueOf(resumeId)).join();
} catch (Exception e) {
addProcess(userId, new ProcessItem("error", "获取简历失败,结束推荐岗位", 0, DateTimeWrapper.now().toString()));
log.error("获取简历数据失败");
aiAnalysisMapper.updateAnalysis(analysisId, 0, userId, 0);
return null;
}
addProcess(userId, new ProcessItem("delta", "小职正在智能分析您的简历内容中...", 3, DateTimeWrapper.now().toString()));
return resumeVo;
}
/**
* 准备推荐数据
*/
private AiRecommendVo prepareRecommendationData(Long analysisId, Long userId, Long resumeId) {
// 获取求职意向
AiRecommendVo aiRecommendVo = aiAnalysisService.queryAnalysisData(analysisId);
// 获取城市信息
Map<String, String> cityMap = extractProfileData(aiRecommendVo);
if (CollectionUtil.isEmpty(cityMap)) {
addProcess(userId, new ProcessItem("error", "获取城市失败,结束推荐岗位", 0, DateTimeWrapper.now().toString()));
log.error("获取城市失败,结束推荐岗位");
aiAnalysisMapper.updateAnalysis(analysisId, 0, userId, 0);
return null;
}
return aiRecommendVo;
}
/**
* 添加推荐过程
*
* @param userId
* @param item
*/
private void addProcess(Long userId, ProcessItem item) {
redisTemplate.opsForList().rightPush(RECOMMEND_POSITION_PROCESS.formatted(userId), JSON.toJSONString(item));
//设置10天过期
redisTemplate.expire(RECOMMEND_POSITION_PROCESS.formatted(userId), 10, TimeUnit.DAYS);
}
/**
* 提取求职意向的城市信息
*
* @param aiRecommendVo
* @return
*/
private Map<String, String> extractProfileData(AiRecommendVo aiRecommendVo) {
Map<String, String> cityMap = new HashMap<>();
for (String city : aiRecommendVo.getTargetCity()) {
String cityCode = cityDataMapper.getCityCodeByName(city);
if (cityCode == null) {
log.error("{}获取城市编码失败", city);
continue;
}
cityMap.put(city, cityCode);
}
return cityMap;
}
/**
* 岗位智能推过程key
*/
private final static String RECOMMEND_POSITION_PROCESS = "recommend:position:process:%s";
/**
* 删除推荐过程
*
* @param userId
*/
private void delProcess(Long userId) {
redisTemplate.delete(RECOMMEND_POSITION_PROCESS.formatted(userId));
}
class RcommendParam {
//String cityCode, String jobTitle, String pay, String resumeId
String cityCode;
String jobTitle;
String pay;
String isIntern;
Long resumeId;
Integer internTime;
private Integer jobType;
private List<Integer> companySize;
private Integer workTime;
private List<Float> floatList;
private String sketch;
}
}
package com.bkty.system.service.resume; package com.bkty.system.service.resume;
import com.bkty.system.domain.dto.*; import com.bkty.system.domain.dto.*;
import com.bkty.system.domain.entity.FunctionResumeSketch;
import com.bkty.system.domain.vo.ResumeByPdfVo; import com.bkty.system.domain.vo.ResumeByPdfVo;
import com.bkty.system.domain.vo.ResumeModelVo; import com.bkty.system.domain.vo.ResumeModelVo;
import com.bkty.system.domain.vo.ResumeVo; import com.bkty.system.domain.vo.ResumeVo;
...@@ -120,4 +121,5 @@ public interface NewEditionResumeService { ...@@ -120,4 +121,5 @@ public interface NewEditionResumeService {
*/ */
void reversalExperience(ResumeExpChangeDto dto) throws Exception; void reversalExperience(ResumeExpChangeDto dto) throws Exception;
} }
package com.bkty.system.service.resume;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bkty.system.domain.entity.FunctionResumeBase;
import com.bkty.system.domain.entity.FunctionResumeSketch;
/**
* @author jiangxiaoge
* @description 简历制作Service
* @data 2024/12/16
**/
public interface ResumeMakeService extends IService<FunctionResumeBase> {
/**
* 查询简历分析详情
* @param resumeId
* @return
*/
FunctionResumeSketch queryResumeSketch(Long resumeId);
}
package com.bkty.system.service.resume.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bkty.system.domain.entity.FunctionResumeBase;
import com.bkty.system.domain.entity.FunctionResumeSketch;
import com.bkty.system.domain.vo.ResumeVo;
import com.bkty.system.mapper.FunctionResumeBaseMapper;
import com.bkty.system.mapper.FunctionResumeSketchMapper;
import com.bkty.system.service.coze.CozeApiService;
import com.bkty.system.service.resume.NewEditionResumeService;
import com.bkty.system.service.resume.ResumeMakeService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import dev.langchain4j.model.embedding.EmbeddingModel;
import org.dromara.common.core.config.LlmEmbeddingConfig;
import org.dromara.common.core.constant.CozeConstsnts;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* @author Zhang Wenbiao
* @description 简历制作service
* @datetime 2025/12/4 16:25
*/
@Slf4j
@AllArgsConstructor
@Service
public class ResumeMakeServiceImpl extends ServiceImpl<FunctionResumeBaseMapper, FunctionResumeBase> implements ResumeMakeService {
private final NewEditionResumeService newEditionResumeService;
private final CozeApiService cozeApiService;
private final FunctionResumeSketchMapper resumeSketchMapper;
@Override
public FunctionResumeSketch queryResumeSketch(Long resumeId) {
try {
ResumeVo join = newEditionResumeService.queryNewEditionResumeId(String.valueOf(resumeId)).join();
String resumeDescByCoze = getResumeDescByCoze(com.alibaba.fastjson2.JSON.toJSONString(join), "7506433655737630720");
EmbeddingModel embeddingModel = LlmEmbeddingConfig.getEmbeddingModel();
List<Float> dutyVector = embeddingModel.embed(resumeDescByCoze).content().vectorAsList();
QueryWrapper<FunctionResumeSketch> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("resume_id", resumeId).eq("is_deleted", false);
FunctionResumeSketch resumeSketch = new FunctionResumeSketch();
resumeSketch.setResumeId(resumeId);
resumeSketch.setSketch(resumeDescByCoze);
resumeSketch.setVectorQuantity(com.alibaba.fastjson2.JSON.toJSONString(dutyVector));
if (resumeSketchMapper.exists(queryWrapper)) {
UpdateWrapper<FunctionResumeSketch> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("resume_id", resumeId).eq("is_deleted", false)
.set("sketch", resumeDescByCoze).set("vector_quantity", com.alibaba.fastjson2.JSON.toJSONString(dutyVector));
resumeSketchMapper.update(null, updateWrapper);
} else {
resumeSketchMapper.insert(resumeSketch);
}
return resumeSketch;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private String getResumeDescByCoze(String sendData, String botId) throws Exception{
String cozeToken = cozeApiService.getCozeToken();
// 构建请求头
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put(HttpHeaders.AUTHORIZATION, CozeConstsnts.COZE_TOKEN_BEARER + cozeToken);
List<Map<String, Object>> additionalMessages = new ArrayList<>();
Map<String, Object> content = Map.of("content_type", "text", "content", sendData, "role", "user");
additionalMessages.add(content);
Map<String, Object> body = Map.of("bot_id", botId,
"user_id", "9527",
"stream", false,
"additional_messages", additionalMessages);
String uri = cozeApiService.getCozeDomainUri() + "/v3/chat";
// 使用 Hutool 发送 POST 请求
try {
String jsonBody = com.alibaba.fastjson2.JSON.toJSONString(body);
cn.hutool.http.HttpRequest request = cn.hutool.http.HttpRequest.post(uri)
.addHeaders(headers)
.body(jsonBody)
.timeout(30000); // 设置超时时间 30 秒
cn.hutool.http.HttpResponse response = request.execute();
String responseBody = response.body();
Map<String, Object> map = com.alibaba.fastjson2.JSON.parseObject(responseBody, Map.class);
String code = Objects.toString(map.get("code"), "");
if (code.equals("0")) {
Map<String, Object> data = (Map<String, Object>) map.get("data");
String status = Objects.toString(data.get("status"), "");
boolean inProgress = status.equals("in_progress");
while (inProgress) {
String retrieveUri = cozeApiService.getCozeDomainUri() + "/v3/chat/retrieve?chat_id=%s&conversation_id=%s".formatted(data.get("id").toString(), data.get("conversation_id").toString());
// 使用 Hutool 发送 GET 请求
cn.hutool.http.HttpRequest retrieveRequest = cn.hutool.http.HttpRequest.get(retrieveUri)
.addHeaders(headers)
.timeout(30000);
cn.hutool.http.HttpResponse retrieveResponse = retrieveRequest.execute();
String retrieveResponseBody = retrieveResponse.body();
System.out.println(retrieveResponseBody);
Map<String, Object> mapData = com.alibaba.fastjson2.JSON.parseObject(retrieveResponseBody, Map.class);
if (!"0".equals(mapData.get("code").toString())) {
break;
}
Map<String, Object> rData = (Map<String, Object>) mapData.get("data");
String status1 = rData.get("status").toString();
inProgress = status1.equals("in_progress");
if (status1.equals("completed")) {
String messageListUri = cozeApiService.getCozeDomainUri() + "/v3/chat/message/list?chat_id=%s&conversation_id=%s".formatted(data.get("id").toString(), data.get("conversation_id").toString());
// 使用 Hutool 发送 GET 请求
cn.hutool.http.HttpRequest messageListRequest = cn.hutool.http.HttpRequest.get(messageListUri)
.addHeaders(headers)
.timeout(30000);
cn.hutool.http.HttpResponse messageListResponse = messageListRequest.execute();
String messageListResponseBody = messageListResponse.body();
com.alibaba.fastjson2.JSONObject jsonObject = com.alibaba.fastjson2.JSON.parseObject(messageListResponseBody);
if (jsonObject != null) {
if (jsonObject.getInteger("code") == 0) {
com.alibaba.fastjson2.JSONArray jsonArray = jsonObject.getJSONArray("data");
for (int i = 0; i < jsonArray.size(); i++) {
com.alibaba.fastjson2.JSONObject thisJsonObject = jsonArray.getJSONObject(i);
if (thisJsonObject.getString("type").equals("answer")) {
String contentString = thisJsonObject.getString("content");
contentString = contentString.replace("```json","" );
contentString = contentString.replace("```", "");
log.info("执行成功,简历信息:{}\n描述信息:{}", data, contentString);
return contentString;
}
}
}
}
}
Thread.sleep(1000);
}
}
}catch (Exception e) {
log.error("发送HTTP请求时发生异常: ", e);
throw new Exception("请求发送失败: " + e.getMessage(), e);
}
return null;
}
}
package com.bkty.system.utils;
import java.sql.*;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.HashMap;
public class MariaDbAdtUtil {
// 数据库连接配置
//private static final String JDBC_URL = "jdbc:mariadb://127.0.0.1:33066/gov_datacenter";
private static final String JDBC_URL = "jdbc:mariadb://192.168.1.218:3306/gov_datacenter";
private static final String JDBC_USERNAME = "wangwenjiang";
private static final String JDBC_PASSWORD = "Q8$l63I6-iijE3^9";
/**
* 获取 MariaDB 数据库连接
*/
private static Connection getConnection() throws SQLException {
return DriverManager.getConnection(JDBC_URL, JDBC_USERNAME, JDBC_PASSWORD);
}
/**
* 根据ID查询recruit_qqxb表,返回单条记录的Map表示
*
* @param id 要查询的记录ID
* @return 包含记录字段的Map,如果未找到记录则返回null
*/
public static Map<String, Object> queryRecruitQqxbById(long id) {
String query = "SELECT * FROM recruit_qqxb WHERE id = ?";
int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try (Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(query)) {
statement.setLong(1, id);
statement.setQueryTimeout(10); // 设置查询超时为10秒
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
return resultSetToMap(resultSet);
}
return null;
}
} catch (SQLException e) {
if (attempt == maxRetries - 1) {
throw new RuntimeException("查询recruit_qqxb表 id:"+ id +"失败: " + e.getMessage(), e);
}
try {
Thread.sleep(1000); // 等待1秒后重试
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("查询被中断", ie);
}
}
}
return null;
}
/**
* 将ResultSet当前行转换为Map<String, Object>
*
* @param resultSet 查询结果集
* @return 包含当前行数据的Map
* @throws SQLException SQL异常
*/
private static Map<String, Object> resultSetToMap(ResultSet resultSet) throws SQLException {
Map<String, Object> resultMap = new HashMap<>();
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i);
Object value = resultSet.getObject(i);
resultMap.put(columnName, value);
}
return resultMap;
}
/**
* 查询并返回游标迭代器
*/
public static Iterable<Map.Entry<Integer, String>> queryWithCursor(String query) {
return () -> new Iterator<>() {
private Connection connection;
private PreparedStatement statement;
private ResultSet resultSet;
private boolean isNextAvailable;
{
try {
// 初始化数据库连接和查询
connection = getConnection();
statement = connection.prepareStatement(query, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
statement.setFetchSize(Integer.MIN_VALUE); // 启用游标模式
resultSet = statement.executeQuery();
isNextAvailable = resultSet.next(); // 移动到第一条记录
} catch (SQLException e) {
close(); // 出现异常时清理资源
throw new RuntimeException("查询执行失败: " + e.getMessage(), e);
}
}
@Override
public boolean hasNext() {
return isNextAvailable;
}
@Override
public Map.Entry<Integer, String> next() {
if (!isNextAvailable) {
throw new NoSuchElementException("没有更多数据");
}
try {
// 读取 position_id 和 position_portrayal 字段
int positionId = resultSet.getInt("position_id");
String positionPortrayal = resultSet.getString("position_portrayal");
isNextAvailable = resultSet.next(); // 移动到下一条记录
return new HashMap.SimpleEntry<>(positionId, positionPortrayal);
} catch (SQLException e) {
throw new RuntimeException("获取下一条数据失败: " + e.getMessage(), e);
}
}
/**
* 关闭资源
*/
private void close() {
try {
if (resultSet != null) resultSet.close();
if (statement != null) statement.close();
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
protected void finalize() throws Throwable {
close();
super.finalize();
}
};
}
public static void main(String[] args) {
/*String query = "SELECT position_id, position_portrayal FROM function_position_portrayal WHERE is_deleted = false";
for (Map.Entry<Integer, String> entry : queryWithCursor(query)) {
System.out.println("Position ID: " + entry.getKey() + ", Position Portrayal: " + entry.getValue());
// 在这里处理每条数据
}*/
// 新增功能测试
long idToQuery = 38243537L;
Map<String, Object> result = queryRecruitQqxbById(idToQuery);
if (result != null) {
System.out.println("查询结果:");
result.forEach((key, value) -> System.out.println(key + ": " + value));
} else {
System.out.println("未找到ID为 " + idToQuery + " 的记录");
}
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bkty.system.mapper.AiAnalysisMapper">
<update id="updateAnalysis">
update ai_recommend_base set
recommend_status = #{rType}
<if test="authRecommend != null">
, auth_recommend = #{authRecommend}
</if>
where id = #{analysisId}
</update>
</mapper>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bkty.system.mapper.PositionRecommendMapper">
<select id="selectPositionList" resultType="com.bkty.system.domain.vo.AiPositionRecommendRecordVo">
SELECT
t1.*, t2.resume_name as resumeName
FROM
ai_position_recommend_record t1, function_resume_base t2
WHERE
t1.is_deleted = 0 and
t1.analysis_id = #{analysisId} and
t1.resume_id = t2.id
<if test="queryType == 1 and recommendTime == null">
AND t1.recommend_time = (SELECT MAX(recommend_time) FROM ai_position_recommend_record WHERE analysis_id = #{analysisId} AND is_deleted = 0)
ORDER BY t1.r_score DESC
</if>
<if test="queryType == 1 and recommendTime != null">
AND t1.recommend_time = #{recommendTime}
ORDER BY t1.r_score DESC
</if>
<if test="queryType == 0">
AND t1.operation_type = 1
<if test="deliverType != null">
AND t1.treasures_type != 2
</if>
<choose>
<when test="sortField != null and sortField != ''">
ORDER BY t1.${sortField} ${sortType}
</when>
<otherwise>
ORDER BY t1.update_time DESC
</otherwise>
</choose>
</if>
<if test="queryType == 2">
AND t1.treasures_type = 1
order by t1.create_time desc
</if>
<if test="queryType == 3">
AND t1.treasures_type = 2
order by t1.create_time desc
</if>
<if test="queryType == 4">
AND t1.deliver_type = 1
order by t1.create_time desc
</if>
<if test="pId != null">
AND t1.id = #{pId}
</if>
<if test="start != null">
limit #{start}, #{size}
</if>
</select>
</mapper>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment