1289 lines
48 KiB
JavaScript
1289 lines
48 KiB
JavaScript
const aiServiceModule = require('../../../services/ai_service');
|
||
const { jobFilterService } = require('../services');
|
||
const locationService = require('../../../services/locationService');
|
||
const logs = require('../../logProxy');
|
||
const db = require('../../dbProxy');
|
||
const { v4: uuidv4 } = require('uuid');
|
||
|
||
// 实例化AI服务
|
||
const aiService = aiServiceModule.getInstance();
|
||
|
||
/**
|
||
* 工作管理模块
|
||
* 负责简历获取、分析、存储和匹配度计算
|
||
*/
|
||
class JobManager {
|
||
constructor() {
|
||
}
|
||
|
||
// 启动客户端那个平台 用户信息,心跳机制
|
||
async set_user_info(sn_code, mqttClient, user_info) {
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform: platform,
|
||
action: 'set_user_info',
|
||
data: {
|
||
user_info: user_info
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取登录二维码
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {object} mqttClient - MQTT客户端
|
||
* @param {object} params - 参数对象
|
||
* @returns {Promise<object>} 二维码信息
|
||
*/
|
||
async get_login_qr_code(sn_code, mqttClient, params = {}) {
|
||
console.log(`[工作管理] 开始获取设备 ${sn_code} 的登录二维码`);
|
||
|
||
const { platform = 'boss' } = params;
|
||
|
||
// 通过MQTT指令获取登录二维码
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform: platform,
|
||
action: 'get_login_qr_code',
|
||
data: {}
|
||
});
|
||
|
||
if (!response || response.code !== 200) {
|
||
console.error(`[工作管理] 获取登录二维码失败:`, response);
|
||
throw new Error(response?.message || '获取登录二维码失败');
|
||
}
|
||
|
||
const qrCodeData = response.data;
|
||
console.log(`[工作管理] 成功获取登录二维码数据:`, qrCodeData);
|
||
|
||
return qrCodeData;
|
||
}
|
||
|
||
/**
|
||
* 打开机器人检测测试页
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {object} mqttClient - MQTT客户端
|
||
* @param {object} params - 参数对象
|
||
* @returns {Promise<object>} 执行结果
|
||
*/
|
||
async open_bot_detection(sn_code, mqttClient, params = {}) {
|
||
console.log(`[工作管理] 开始打开设备 ${sn_code} 的机器人检测测试页`);
|
||
|
||
const { platform = 'boss' } = params;
|
||
|
||
// 通过MQTT指令打开机器人检测测试页
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform: platform,
|
||
action: 'open_bot_detection',
|
||
data: {}
|
||
});
|
||
|
||
if (!response || response.code !== 200) {
|
||
console.error(`[工作管理] 打开机器人检测测试页失败:`, response);
|
||
throw new Error(response?.message || '打开机器人检测测试页失败');
|
||
}
|
||
|
||
const result = response.data;
|
||
console.log(`[工作管理] 成功打开机器人检测测试页:`, result);
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 获取用户信息
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {object} mqttClient - MQTT客户端
|
||
* @param {object} params - 参数对象
|
||
* @returns {Promise<object>} 用户信息
|
||
*/
|
||
async get_user_info(sn_code, mqttClient, params = {}) {
|
||
console.log(`[工作管理] 开始获取设备 ${sn_code} 的用户信息`);
|
||
|
||
const { platform = 'boss' } = params;
|
||
|
||
// 通过MQTT指令获取用户信息
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform: platform,
|
||
action: 'get_user_info',
|
||
data: {}
|
||
});
|
||
|
||
if (!response || response.code !== 200) {
|
||
console.error(`[工作管理] 获取用户信息失败:`, response);
|
||
throw new Error(response?.message || '获取用户信息失败');
|
||
}
|
||
|
||
const userInfo = response.data;
|
||
console.log(`[工作管理] 成功获取用户信息:`, userInfo);
|
||
|
||
return userInfo;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 搜索岗位
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {object} mqttClient - MQTT客户端
|
||
* @param {object} params - 参数
|
||
* @returns {Promise<object>} 搜索结果
|
||
*/
|
||
async search_jobs(sn_code, mqttClient, params = {}) {
|
||
const { keyword = '前端', platform = 'boss' } = params;
|
||
console.log(`[工作管理] 开始搜索设备 ${sn_code} 的岗位,关键词: ${keyword}`);
|
||
|
||
// 通过MQTT指令搜索岗位
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform,
|
||
action: "search_jobs",
|
||
data: { keyword }
|
||
});
|
||
|
||
if (!response || response.code !== 200) {
|
||
console.error(`[工作管理] 搜索岗位失败:`, response);
|
||
throw new Error('搜索岗位失败');
|
||
}
|
||
|
||
console.log(`[工作管理] 成功搜索岗位`);
|
||
return response.data;
|
||
}
|
||
|
||
/**
|
||
* 多条件搜索职位列表(新指令,使用新的MQTT action)
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {object} mqttClient - MQTT客户端
|
||
* @param {object} params - 搜索参数
|
||
* @returns {Promise<object>} 搜索结果
|
||
*/
|
||
async search_jobs_with_params(sn_code, mqttClient, params = {}) {
|
||
const {
|
||
keyword = '前端',
|
||
platform = 'boss',
|
||
city = '',
|
||
cityName = '',
|
||
salary = '',
|
||
experience = '',
|
||
education = '',
|
||
industry = '',
|
||
companySize = '',
|
||
financingStage = '',
|
||
page = 1,
|
||
pageSize = 20,
|
||
pageCount = 3
|
||
} = params;
|
||
|
||
console.log(`[工作管理] 开始多条件搜索设备 ${sn_code} 的职位,关键词: ${keyword}, 城市: ${cityName || city}`);
|
||
|
||
// 构建完整的搜索参数对象
|
||
const searchData = {
|
||
keyword,
|
||
pageCount
|
||
};
|
||
|
||
// 添加可选搜索条件
|
||
if (city) searchData.city = city;
|
||
if (cityName) searchData.cityName = cityName;
|
||
if (salary) searchData.salary = salary;
|
||
if (experience) searchData.experience = experience;
|
||
if (education) searchData.education = education;
|
||
if (industry) searchData.industry = industry;
|
||
if (companySize) searchData.companySize = companySize;
|
||
if (financingStage) searchData.financingStage = financingStage;
|
||
if (page) searchData.page = page;
|
||
if (pageSize) searchData.pageSize = pageSize;
|
||
|
||
// 通过MQTT指令获取岗位列表(使用新的action)
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform,
|
||
action: "search_job_list", // 新的搜索action
|
||
data: searchData
|
||
});
|
||
|
||
if (!response || response.code !== 200) {
|
||
console.error(`[工作管理] 多条件搜索职位失败:`, response);
|
||
throw new Error('多条件搜索职位失败');
|
||
}
|
||
|
||
// 处理职位列表数据
|
||
let jobs = [];
|
||
if (Array.isArray(response.data)) {
|
||
for (const item of response.data) {
|
||
if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) {
|
||
jobs = jobs.concat(item.data.zpData.jobList);
|
||
}
|
||
}
|
||
} else if (response.data?.data?.zpData?.jobList) {
|
||
jobs = response.data.data.zpData.jobList || [];
|
||
} else if (response.data?.zpData?.jobList) {
|
||
jobs = response.data.zpData.jobList || [];
|
||
}
|
||
|
||
console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`);
|
||
|
||
// 保存职位到数据库
|
||
try {
|
||
await this.saveJobsToDatabase(sn_code, platform, keyword, jobs);
|
||
} catch (error) {
|
||
console.error(`[工作管理] 保存职位到数据库失败:`, error);
|
||
}
|
||
|
||
return {
|
||
jobs: jobs,
|
||
keyword: keyword,
|
||
platform: platform,
|
||
count: jobs.length
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 搜索并投递职位(新指令)
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {object} mqttClient - MQTT客户端
|
||
* @param {object} params - 参数
|
||
* @returns {Promise<object>} 执行结果
|
||
*/
|
||
async search_and_deliver(sn_code, mqttClient, params = {}) {
|
||
const {
|
||
keyword,
|
||
searchParams = {},
|
||
pageCount = 3,
|
||
filterRules = {},
|
||
maxCount = 10,
|
||
platform = 'boss'
|
||
} = params;
|
||
|
||
console.log(`[工作管理] 开始搜索并投递职位,设备: ${sn_code}, 关键词: ${keyword}`);
|
||
|
||
// 1. 先执行搜索(使用search_jobs_with_params,新的搜索指令)
|
||
const searchResult = await this.search_jobs_with_params(sn_code, mqttClient, {
|
||
keyword,
|
||
platform,
|
||
...searchParams,
|
||
pageCount
|
||
});
|
||
|
||
if (!searchResult || searchResult.count === 0) {
|
||
return {
|
||
success: true,
|
||
jobCount: 0,
|
||
deliveredCount: 0,
|
||
message: '未找到职位'
|
||
};
|
||
}
|
||
|
||
// 2. 等待数据保存完成
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
|
||
// 3. 从数据库获取刚搜索到的职位
|
||
const job_postings = db.getModel('job_postings');
|
||
const searchedJobs = await job_postings.findAll({
|
||
where: {
|
||
sn_code: sn_code,
|
||
platform: platform,
|
||
applyStatus: 'pending',
|
||
keyword: keyword
|
||
},
|
||
order: [['create_time', 'DESC']],
|
||
limit: 1000
|
||
});
|
||
|
||
if (searchedJobs.length === 0) {
|
||
return {
|
||
success: true,
|
||
jobCount: searchResult.count,
|
||
deliveredCount: 0,
|
||
message: '未找到待投递的职位'
|
||
};
|
||
}
|
||
|
||
// 4. 获取简历信息用于匹配
|
||
const resume_info = db.getModel('resume_info');
|
||
const resume = await resume_info.findOne({
|
||
where: {
|
||
sn_code: sn_code,
|
||
platform: platform,
|
||
isActive: true
|
||
},
|
||
order: [['last_modify_time', 'DESC']]
|
||
});
|
||
|
||
if (!resume) {
|
||
return {
|
||
success: true,
|
||
jobCount: searchResult.count,
|
||
deliveredCount: 0,
|
||
message: '未找到活跃简历,无法投递'
|
||
};
|
||
}
|
||
|
||
// 5. 获取账号配置
|
||
const pla_account = db.getModel('pla_account');
|
||
const account = await pla_account.findOne({
|
||
where: { sn_code, platform_type: platform }
|
||
});
|
||
|
||
if (!account) {
|
||
throw new Error('账号不存在');
|
||
}
|
||
|
||
const accountConfig = account.toJSON();
|
||
const resumeData = resume.toJSON();
|
||
|
||
// 6. 使用过滤方法进行职位匹配
|
||
const matchedJobs = await this.filter_jobs_by_rules(searchedJobs, {
|
||
minSalary: filterRules.minSalary || 0,
|
||
maxSalary: filterRules.maxSalary || 0,
|
||
keywords: filterRules.keywords || [],
|
||
excludeKeywords: filterRules.excludeKeywords || [],
|
||
accountConfig: accountConfig,
|
||
resumeInfo: resumeData
|
||
});
|
||
|
||
// 7. 限制投递数量
|
||
const jobsToDeliver = matchedJobs.slice(0, maxCount);
|
||
console.log(`[工作管理] 匹配到 ${matchedJobs.length} 个职位,将投递 ${jobsToDeliver.length} 个`);
|
||
|
||
// 8. 执行投递
|
||
let deliveredCount = 0;
|
||
const apply_records = db.getModel('apply_records');
|
||
|
||
for (let i = 0; i < jobsToDeliver.length; i++) {
|
||
const job = jobsToDeliver[i];
|
||
const jobData = job.toJSON ? job.toJSON() : job;
|
||
|
||
try {
|
||
// 从原始数据中获取 securityId
|
||
let securityId = jobData.securityId || '';
|
||
try {
|
||
if (jobData.originalData) {
|
||
const originalData = typeof jobData.originalData === 'string'
|
||
? JSON.parse(jobData.originalData)
|
||
: jobData.originalData;
|
||
securityId = originalData.securityId || securityId;
|
||
}
|
||
} catch (e) {
|
||
console.warn(`[工作管理] 解析职位原始数据失败:`, e);
|
||
}
|
||
|
||
// 执行投递(使用新的deliver_resume_search action)
|
||
const deliverResult = await this.deliver_resume(sn_code, mqttClient, {
|
||
jobId: jobData.jobId,
|
||
encryptBossId: jobData.encryptBossId || '',
|
||
securityId: securityId,
|
||
brandName: jobData.companyName || '',
|
||
jobTitle: jobData.jobTitle || '',
|
||
companyName: jobData.companyName || '',
|
||
platform: platform,
|
||
action: 'deliver_resume_search' // 搜索并投递使用新的action
|
||
});
|
||
|
||
if (deliverResult && deliverResult.success) {
|
||
deliveredCount++;
|
||
}
|
||
|
||
// 投递间隔控制
|
||
if (i < jobsToDeliver.length - 1) {
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
}
|
||
} catch (error) {
|
||
console.error(`[工作管理] 投递职位失败:`, error);
|
||
// 继续投递下一个职位
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
jobCount: searchResult.count,
|
||
deliveredCount: deliveredCount,
|
||
message: `搜索完成,找到 ${searchResult.count} 个职位,成功投递 ${deliveredCount} 个`
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取岗位列表(支持多条件搜索)
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {object} mqttClient - MQTT客户端
|
||
* @param {object} params - 参数
|
||
* @returns {Promise<object>} 岗位列表
|
||
*/
|
||
async get_job_list(sn_code, mqttClient, params = {}) {
|
||
const {
|
||
keyword = '前端',
|
||
platform = 'boss',
|
||
pageCount = 3,
|
||
city = '',
|
||
cityName = '',
|
||
salary = '',
|
||
experience = '',
|
||
education = '',
|
||
industry = '',
|
||
companySize = '',
|
||
financingStage = '',
|
||
page = 1,
|
||
pageSize = 20
|
||
} = params;
|
||
|
||
// 判断是否是多条件搜索(如果包含多条件参数,使用多条件搜索逻辑)
|
||
const hasMultiParams = city || cityName || salary || experience || education ||
|
||
industry || companySize || financingStage || page || pageSize;
|
||
|
||
if (hasMultiParams) {
|
||
// 使用多条件搜索逻辑
|
||
console.log(`[工作管理] 开始多条件搜索设备 ${sn_code} 的职位,关键词: ${keyword}, 城市: ${cityName || city}`);
|
||
|
||
// 构建完整的搜索参数对象
|
||
const searchData = {
|
||
keyword,
|
||
pageCount
|
||
};
|
||
|
||
// 添加可选搜索条件
|
||
if (city) searchData.city = city;
|
||
if (cityName) searchData.cityName = cityName;
|
||
if (salary) searchData.salary = salary;
|
||
if (experience) searchData.experience = experience;
|
||
if (education) searchData.education = education;
|
||
if (industry) searchData.industry = industry;
|
||
if (companySize) searchData.companySize = companySize;
|
||
if (financingStage) searchData.financingStage = financingStage;
|
||
if (page) searchData.page = page;
|
||
if (pageSize) searchData.pageSize = pageSize;
|
||
|
||
// 通过MQTT指令获取岗位列表(保持action不变,前端已使用)
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform,
|
||
action: "get_job_list", // 保持与原有get_job_list相同的action,前端已使用
|
||
data: searchData
|
||
});
|
||
|
||
if (!response || response.code !== 200) {
|
||
console.error(`[工作管理] 多条件搜索职位失败:`, response);
|
||
throw new Error('多条件搜索职位失败');
|
||
}
|
||
|
||
// 处理职位列表数据
|
||
let jobs = [];
|
||
if (Array.isArray(response.data)) {
|
||
for (const item of response.data) {
|
||
if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) {
|
||
jobs = jobs.concat(item.data.zpData.jobList);
|
||
}
|
||
}
|
||
} else if (response.data?.data?.zpData?.jobList) {
|
||
jobs = response.data.data.zpData.jobList || [];
|
||
} else if (response.data?.zpData?.jobList) {
|
||
jobs = response.data.zpData.jobList || [];
|
||
}
|
||
|
||
console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`);
|
||
|
||
// 保存职位到数据库
|
||
try {
|
||
await this.saveJobsToDatabase(sn_code, platform, keyword, jobs);
|
||
} catch (error) {
|
||
console.error(`[工作管理] 保存职位到数据库失败:`, error);
|
||
}
|
||
|
||
return {
|
||
jobs: jobs,
|
||
keyword: keyword,
|
||
platform: platform,
|
||
count: jobs.length
|
||
};
|
||
}
|
||
|
||
// 简单搜索逻辑(保持原有逻辑)
|
||
console.log(`[工作管理] 开始获取设备 ${sn_code} 的岗位列表,关键词: ${keyword}`);
|
||
|
||
// 通过MQTT指令获取岗位列表
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform,
|
||
action: "get_job_list",
|
||
data: { keyword, pageCount }
|
||
});
|
||
|
||
if (!response || response.code !== 200) {
|
||
console.error(`[工作管理] 获取岗位列表失败:`, response);
|
||
throw new Error('获取岗位列表失败');
|
||
}
|
||
|
||
// 处理职位列表数据:response.data 可能是数组(职位列表.json 格式)或单个对象
|
||
let jobs = [];
|
||
|
||
if (Array.isArray(response.data)) {
|
||
// 如果是数组格式(职位列表.json),遍历每个元素提取岗位数据
|
||
for (const item of response.data) {
|
||
if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) {
|
||
jobs = jobs.concat(item.data.zpData.jobList);
|
||
}
|
||
}
|
||
console.log(`[工作管理] 从 ${response.data.length} 个响应中提取岗位数据`);
|
||
} else if (response.data?.data?.zpData?.jobList) {
|
||
// 如果是单个对象格式,从 data.zpData.jobList 获取
|
||
jobs = response.data.data.zpData.jobList || [];
|
||
} else if (response.data?.zpData?.jobList) {
|
||
// 兼容旧格式:直接从 zpData.jobList 获取
|
||
jobs = response.data.zpData.jobList || [];
|
||
}
|
||
|
||
console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`);
|
||
|
||
// 保存职位到数据库
|
||
try {
|
||
await this.saveJobsToDatabase(sn_code, platform, keyword, jobs);
|
||
} catch (error) {
|
||
console.error(`[工作管理] 保存职位到数据库失败:`, error);
|
||
// 不影响主流程,继续返回数据
|
||
}
|
||
|
||
const result = {
|
||
jobs: jobs,
|
||
keyword: keyword,
|
||
platform: platform,
|
||
count: jobs.length
|
||
};
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 保存职位列表到数据库
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {string} platform - 平台
|
||
* @param {string} keyword - 搜索关键词
|
||
* @param {Array} jobs - 职位列表
|
||
*/
|
||
async saveJobsToDatabase(sn_code, platform, keyword, jobs) {
|
||
const job_postings = db.getModel('job_postings');
|
||
|
||
console.log(`[工作管理] 开始保存 ${jobs.length} 个职位到数据库`);
|
||
|
||
for (const job of jobs) {
|
||
try {
|
||
// 构建职位信息对象
|
||
const jobInfo = {
|
||
sn_code,
|
||
platform,
|
||
keyword,
|
||
|
||
// Boss直聘字段映射
|
||
encryptBossId: job.encryptBossId || '',
|
||
jobId: job.encryptJobId || '',
|
||
jobTitle: job.jobName || '',
|
||
companyId: job.encryptBrandId || '',
|
||
companyName: job.brandName || '',
|
||
companySize: job.brandScaleName || '',
|
||
companyIndustry: job.brandIndustry || '',
|
||
salary: job.salaryDesc || '',
|
||
|
||
// 岗位要求(从 jobLabels 和 skills 提取)
|
||
jobRequirements: JSON.stringify({
|
||
experience: job.jobExperience || '',
|
||
education: job.jobDegree || '',
|
||
labels: job.jobLabels || [],
|
||
skills: job.skills || []
|
||
}),
|
||
|
||
// 工作地点
|
||
location: [job.cityName, job.areaDistrict, job.businessDistrict]
|
||
.filter(Boolean).join(' '),
|
||
|
||
experience: job.jobExperience || '',
|
||
education: job.jobDegree || '',
|
||
|
||
// 原始数据
|
||
originalData: JSON.stringify(job),
|
||
|
||
// 默认状态
|
||
applyStatus: 'pending',
|
||
chatStatus: 'none'
|
||
};
|
||
|
||
// 调用位置服务解析 location + companyName 获取坐标
|
||
if (jobInfo.location && jobInfo.companyName) {
|
||
const addressToParse = `${jobInfo.location.replaceAll(' ', '')}${jobInfo.companyName}`;
|
||
|
||
|
||
// 等待 1秒
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
||
const location = await locationService.getLocationByAddress(addressToParse).catch(error => {
|
||
console.error(`[工作管理] 获取位置失败:`, error);
|
||
});
|
||
|
||
if (location) {
|
||
jobInfo.latitude = String(location.lat);
|
||
jobInfo.longitude = String(location.lng);
|
||
}
|
||
}
|
||
|
||
// 检查是否已存在(根据 jobId 和 sn_code)
|
||
const existingJob = await job_postings.findOne({
|
||
where: {
|
||
jobId: jobInfo.jobId,
|
||
sn_code: sn_code
|
||
}
|
||
});
|
||
|
||
if (existingJob) {
|
||
// 更新现有职位
|
||
await job_postings.update(jobInfo, {
|
||
where: {
|
||
jobId: jobInfo.jobId,
|
||
sn_code: sn_code
|
||
}
|
||
});
|
||
console.log(`[工作管理] 职位已更新 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`);
|
||
} else {
|
||
// 创建新职位
|
||
await job_postings.create(jobInfo);
|
||
console.log(`[工作管理] 职位已创建 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`);
|
||
}
|
||
} catch (error) {
|
||
console.error(`[工作管理] 保存职位失败:`, error, job);
|
||
// 继续处理下一个职位
|
||
}
|
||
}
|
||
|
||
console.log(`[工作管理] 职位保存完成`);
|
||
}
|
||
|
||
|
||
/**
|
||
* 投递简历(单个职位)
|
||
* @param {string} sn_code - 设备SN码
|
||
* @param {object} mqttClient - MQTT客户端
|
||
* @param {object} params - 参数
|
||
* @param {string} params.jobId - 职位ID(必需)
|
||
* @param {string} params.platform - 平台(默认boss)
|
||
* @param {string} params.encryptBossId - 加密的Boss ID(可选)
|
||
* @param {string} params.securityId - 安全ID(可选)
|
||
* @param {string} params.brandName - 公司名称(可选)
|
||
* @param {string} params.jobTitle - 职位标题(可选)
|
||
* @param {string} params.companyName - 公司名称(可选)
|
||
* @param {string} params.action - MQTT Action(默认:deliver_resume,可选:deliver_resume_search)
|
||
* @returns {Promise<object>} 投递结果
|
||
*/
|
||
async deliver_resume(sn_code, mqttClient, params = {}) {
|
||
const { platform = 'boss', jobId, encryptBossId, securityId, brandName, jobTitle, companyName, action = 'deliver_resume' } = params;
|
||
|
||
if (!jobId) {
|
||
throw new Error('jobId 参数不能为空,请指定要投递的职位ID');
|
||
}
|
||
|
||
console.log(`[工作管理] 开始投递单个职位,设备: ${sn_code}, 职位: ${jobTitle || jobId}`);
|
||
|
||
const job_postings = db.getModel('job_postings');
|
||
const apply_records = db.getModel('apply_records');
|
||
|
||
try {
|
||
// 从数据库获取职位信息
|
||
const where = { sn_code, jobId, platform };
|
||
if (encryptBossId) where.encryptBossId = encryptBossId;
|
||
|
||
const job = await job_postings.findOne({ where });
|
||
if (!job) {
|
||
throw new Error(`未找到职位记录: ${jobId}`);
|
||
}
|
||
|
||
const jobData = job.toJSON();
|
||
|
||
// 检查是否已存在投递记录(避免重复投递同一职位)
|
||
const existingApply = await apply_records.findOne({ where: { sn_code, jobId: jobData.jobId } });
|
||
if (existingApply) {
|
||
console.log(`[工作管理] 跳过已投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`);
|
||
return {
|
||
success: false,
|
||
deliveredCount: 0,
|
||
failedCount: 1,
|
||
message: '该岗位已投递过',
|
||
deliveredJobs: [],
|
||
failedJobs: [{
|
||
jobId: jobData.jobId,
|
||
jobTitle: jobData.jobTitle,
|
||
companyName: jobData.companyName,
|
||
error: '该岗位已投递过'
|
||
}]
|
||
};
|
||
}
|
||
|
||
// 检查该公司是否在一个月内已投递过(避免连续投递同一公司)
|
||
const oneMonthAgo = new Date();
|
||
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
||
|
||
const Sequelize = require('sequelize');
|
||
const recentCompanyApply = await apply_records.findOne({
|
||
where: {
|
||
sn_code: sn_code,
|
||
companyName: jobData.companyName,
|
||
applyTime: {
|
||
[Sequelize.Op.gte]: oneMonthAgo
|
||
}
|
||
},
|
||
order: [['applyTime', 'DESC']]
|
||
});
|
||
|
||
if (recentCompanyApply) {
|
||
const daysAgo = Math.floor((new Date() - new Date(recentCompanyApply.applyTime)) / (1000 * 60 * 60 * 24));
|
||
console.log(`[工作管理] 跳过一个月内已投递的公司: ${jobData.companyName} (${daysAgo}天前投递过)`);
|
||
return {
|
||
success: false,
|
||
deliveredCount: 0,
|
||
failedCount: 1,
|
||
message: `该公司在${daysAgo}天前已投递过,一个月内不重复投递`,
|
||
deliveredJobs: [],
|
||
failedJobs: [{
|
||
jobId: jobData.jobId,
|
||
jobTitle: jobData.jobTitle,
|
||
companyName: jobData.companyName,
|
||
error: `该公司在${daysAgo}天前已投递过,一个月内不重复投递`
|
||
}]
|
||
};
|
||
}
|
||
|
||
console.log(`[工作管理] 投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`);
|
||
|
||
// 通过MQTT指令投递简历(支持自定义action)
|
||
const response = await mqttClient.publishAndWait(sn_code, {
|
||
platform,
|
||
action: action, // 使用传入的action参数,默认为"deliver_resume"
|
||
data: {
|
||
encryptJobId: jobData.jobId,
|
||
securityId: jobData.securityId || securityId || '',
|
||
brandName: jobData.companyName || brandName || ''
|
||
}
|
||
});
|
||
|
||
if (response && response.code === 200) {
|
||
// 投递成功
|
||
await job_postings.update(
|
||
{ applyStatus: 'applied', applyTime: new Date() },
|
||
{ where: { id: jobData.id } }
|
||
);
|
||
|
||
// 计算距离和获取评分信息
|
||
let distance = null;
|
||
let locationScore = jobData.scoreDetails?.locationScore || 0;
|
||
|
||
try {
|
||
// 获取账号的经纬度
|
||
const pla_account = db.getModel('pla_account');
|
||
const account = await pla_account.findOne({
|
||
where: { sn_code, platform_type: platform }
|
||
});
|
||
|
||
// 如果账号和职位都有经纬度,计算实际距离
|
||
if (account && account.user_latitude && account.user_longitude &&
|
||
jobData.latitude && jobData.longitude) {
|
||
const userLat = parseFloat(account.user_latitude);
|
||
const userLng = parseFloat(account.user_longitude);
|
||
const jobLat = parseFloat(jobData.latitude);
|
||
const jobLng = parseFloat(jobData.longitude);
|
||
|
||
if (!isNaN(userLat) && !isNaN(userLng) && !isNaN(jobLat) && !isNaN(jobLng)) {
|
||
distance = locationService.calculateDistance(userLat, userLng, jobLat, jobLng);
|
||
console.log(`[工作管理] 计算距离: ${distance} 公里`);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn(`[工作管理] 计算距离失败:`, error.message);
|
||
// 距离计算失败不影响投递记录保存
|
||
}
|
||
|
||
await apply_records.create({
|
||
sn_code: sn_code,
|
||
platform: platform,
|
||
jobId: jobData.jobId,
|
||
encryptBossId: jobData.encryptBossId || '',
|
||
jobTitle: jobData.jobTitle,
|
||
companyName: jobData.companyName,
|
||
companyId: jobData.companyId || '',
|
||
salary: jobData.salary || '',
|
||
location: jobData.location || '',
|
||
resumeId: jobData.resumeId || '',
|
||
resumeName: jobData.resumeName || '',
|
||
matchScore: jobData.matchScore || jobData.scoreDetails?.totalScore || 0,
|
||
isOutsourcing: jobData.isOutsourcing || false,
|
||
priority: jobData.priority || 5,
|
||
isAutoApply: true,
|
||
applyStatus: 'success',
|
||
applyTime: new Date(),
|
||
feedbackStatus: 'none',
|
||
taskId: jobData.taskId || '',
|
||
keyword: jobData.keyword || '',
|
||
originalData: JSON.stringify({
|
||
...(response.data || {}),
|
||
scoreDetails: {
|
||
...(jobData.scoreDetails || {}),
|
||
locationScore: locationScore,
|
||
distance: distance // 实际距离(公里)
|
||
}
|
||
})
|
||
});
|
||
|
||
console.log(`[工作管理] 投递成功: ${jobData.jobTitle}`);
|
||
|
||
return {
|
||
success: true,
|
||
deliveredCount: 1,
|
||
failedCount: 0,
|
||
deliveredJobs: [{
|
||
jobId: jobData.jobId,
|
||
jobTitle: jobData.jobTitle,
|
||
companyName: jobData.companyName
|
||
}],
|
||
failedJobs: []
|
||
};
|
||
} else {
|
||
// 投递失败
|
||
await job_postings.update(
|
||
{ applyStatus: 'failed' },
|
||
{ where: { id: jobData.id } }
|
||
);
|
||
|
||
|
||
await apply_records.create({
|
||
sn_code: sn_code,
|
||
platform: platform,
|
||
jobId: jobData.jobId,
|
||
encryptBossId: jobData.encryptBossId || '',
|
||
jobTitle: jobData.jobTitle,
|
||
companyName: jobData.companyName,
|
||
companyId: jobData.companyId || '',
|
||
salary: jobData.salary || '',
|
||
location: jobData.location || '',
|
||
resumeId: jobData.resumeId || '',
|
||
resumeName: jobData.resumeName || '',
|
||
matchScore: jobData.matchScore || 0,
|
||
isOutsourcing: jobData.isOutsourcing || false,
|
||
priority: jobData.priority || 5,
|
||
isAutoApply: true,
|
||
applyStatus: 'failed',
|
||
applyTime: new Date(),
|
||
feedbackStatus: 'none',
|
||
taskId: jobData.taskId || '',
|
||
keyword: jobData.keyword || '',
|
||
errorMessage: response?.message || '投递失败',
|
||
originalData: JSON.stringify(response || {})
|
||
});
|
||
|
||
throw new Error(response?.message || '投递失败');
|
||
}
|
||
} catch (error) {
|
||
console.error(`[工作管理] 投递异常: ${jobTitle || jobId}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据规则过滤职位并评分
|
||
* 评分规则:距离30% + 关键字10% + 薪资20% + 公司规模20% = 总分
|
||
* 只有总分 >= 60 的职位才会被投递
|
||
* @param {Array} jobs - 职位列表
|
||
* @param {Object} filterRules - 过滤规则(包含账号配置和简历信息)
|
||
* @returns {Promise<Array>} 过滤并评分后的职位列表(按总分降序排序)
|
||
*/
|
||
async filter_jobs_by_rules(jobs, filterRules) {
|
||
const {
|
||
minSalary = 0,
|
||
maxSalary = 0,
|
||
keywords = [],
|
||
excludeKeywords = [],
|
||
accountConfig = {}, // 账号配置
|
||
resumeInfo = {} // 简历信息
|
||
} = filterRules;
|
||
|
||
// 关键词从职位类型配置中获取,不再从账号配置中获取
|
||
let filterKeywords = keywords || [];
|
||
let excludeKeywordsList = excludeKeywords || [];
|
||
|
||
// 使用账号配置的薪资范围
|
||
const accountMinSalary = accountConfig.min_salary || minSalary || 0;
|
||
const accountMaxSalary = accountConfig.max_salary || maxSalary || 0;
|
||
|
||
// 对每个职位进行评分
|
||
const scoredJobs = jobs.map(job => {
|
||
const jobData = job.toJSON ? job.toJSON() : job;
|
||
|
||
// 1. 距离分数(30%)
|
||
const locationScore = this.calculate_location_score(
|
||
jobData.location,
|
||
resumeInfo.expectedLocation
|
||
);
|
||
|
||
// 2. 关键字分数(10%)
|
||
const keywordScore = this.calculate_keyword_score(
|
||
jobData,
|
||
filterKeywords
|
||
);
|
||
|
||
// 3. 薪资分数(20%)
|
||
const salaryScore = this.calculate_salary_score(
|
||
jobData.salary,
|
||
resumeInfo.expectedSalary,
|
||
accountMinSalary,
|
||
accountMaxSalary
|
||
);
|
||
|
||
// 4. 公司规模分数(20%)
|
||
const companySizeScore = this.calculate_company_size_score(
|
||
jobData.companySize
|
||
);
|
||
|
||
// 计算总分(累加)
|
||
const totalScore = Math.round(
|
||
locationScore * 0.3 +
|
||
keywordScore * 0.1 +
|
||
salaryScore * 0.2 +
|
||
companySizeScore * 0.2
|
||
);
|
||
|
||
return {
|
||
...jobData,
|
||
matchScore: totalScore,
|
||
scoreDetails: {
|
||
locationScore,
|
||
keywordScore,
|
||
salaryScore,
|
||
companySizeScore,
|
||
totalScore
|
||
}
|
||
};
|
||
});
|
||
|
||
// 过滤:排除关键词、最低分数、按总分排序
|
||
return scoredJobs
|
||
.filter(job => {
|
||
// 1. 排除关键词过滤
|
||
if (excludeKeywordsList && excludeKeywordsList.length > 0) {
|
||
const jobText = `${job.jobTitle} ${job.companyName} ${job.jobDescription || ''}`.toLowerCase();
|
||
const hasExcluded = excludeKeywordsList.some(kw => jobText.includes(kw.toLowerCase()));
|
||
if (hasExcluded) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 2. 最低分数过滤(总分 >= 60 才投递)
|
||
if (job.matchScore < 60) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
})
|
||
.sort((a, b) => b.matchScore - a.matchScore); // 按总分降序排序
|
||
}
|
||
|
||
/**
|
||
* 计算距离分数(0-100分)
|
||
* @param {string} jobLocation - 职位工作地点
|
||
* @param {string} expectedLocation - 期望工作地点
|
||
* @returns {number} 距离分数
|
||
*/
|
||
calculate_location_score(jobLocation, expectedLocation) {
|
||
if (!jobLocation) return 50; // 没有地点信息,给中等分数
|
||
if (!expectedLocation) return 70; // 没有期望地点,给较高分数
|
||
|
||
const jobLoc = jobLocation.toLowerCase().trim();
|
||
const expectedLoc = expectedLocation.toLowerCase().trim();
|
||
|
||
// 完全匹配
|
||
if (jobLoc === expectedLoc) {
|
||
return 100;
|
||
}
|
||
|
||
// 包含匹配(如:期望"北京",职位"北京朝阳区")
|
||
if (jobLoc.includes(expectedLoc) || expectedLoc.includes(jobLoc)) {
|
||
return 90;
|
||
}
|
||
|
||
// 城市匹配(提取城市名)
|
||
const jobCity = this.extract_city(jobLoc);
|
||
const expectedCity = this.extract_city(expectedLoc);
|
||
if (jobCity && expectedCity && jobCity === expectedCity) {
|
||
return 80;
|
||
}
|
||
|
||
// 部分匹配(如:都包含"北京")
|
||
if (jobLoc.includes('北京') && expectedLoc.includes('北京')) {
|
||
return 70;
|
||
}
|
||
if (jobLoc.includes('上海') && expectedLoc.includes('上海')) {
|
||
return 70;
|
||
}
|
||
if (jobLoc.includes('深圳') && expectedLoc.includes('深圳')) {
|
||
return 70;
|
||
}
|
||
if (jobLoc.includes('广州') && expectedLoc.includes('广州')) {
|
||
return 70;
|
||
}
|
||
|
||
// 不匹配
|
||
return 30;
|
||
}
|
||
|
||
/**
|
||
* 提取城市名
|
||
* @param {string} location - 地点字符串
|
||
* @returns {string|null} 城市名
|
||
*/
|
||
extract_city(location) {
|
||
const cities = ['北京', '上海', '深圳', '广州', '杭州', '成都', '南京', '武汉', '西安', '苏州'];
|
||
for (const city of cities) {
|
||
if (location.includes(city)) {
|
||
return city;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 计算关键字分数(0-100分)
|
||
* @param {object} jobData - 职位数据
|
||
* @param {Array} filterKeywords - 过滤关键词列表
|
||
* @returns {number} 关键字分数
|
||
*/
|
||
calculate_keyword_score(jobData, filterKeywords) {
|
||
if (!filterKeywords || filterKeywords.length === 0) {
|
||
return 70; // 没有关键词要求,给较高分数
|
||
}
|
||
|
||
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''} ${jobData.keyword || ''}`.toLowerCase();
|
||
|
||
// 计算匹配的关键词数量
|
||
let matchedCount = 0;
|
||
filterKeywords.forEach(kw => {
|
||
if (jobText.includes(kw.toLowerCase())) {
|
||
matchedCount++;
|
||
}
|
||
});
|
||
|
||
// 计算匹配度百分比
|
||
const matchRate = matchedCount / filterKeywords.length;
|
||
return Math.round(matchRate * 100);
|
||
}
|
||
|
||
/**
|
||
* 计算薪资分数(0-100分)
|
||
* @param {string} jobSalary - 职位薪资
|
||
* @param {string} expectedSalary - 期望薪资
|
||
* @param {number} accountMinSalary - 账号配置最低薪资
|
||
* @param {number} accountMaxSalary - 账号配置最高薪资
|
||
* @returns {number} 薪资分数
|
||
*/
|
||
calculate_salary_score(jobSalary, expectedSalary, accountMinSalary, accountMaxSalary) {
|
||
if (!jobSalary) return 50; // 没有薪资信息,给中等分数
|
||
|
||
const salaryRange = this.parse_salary_range(jobSalary);
|
||
if (!salaryRange || salaryRange.min === 0) return 50;
|
||
|
||
const avgJobSalary = (salaryRange.min + salaryRange.max) / 2;
|
||
|
||
// 优先使用账号配置的薪资范围
|
||
if (accountMinSalary > 0 || accountMaxSalary > 0) {
|
||
if (accountMinSalary > 0 && salaryRange.max < accountMinSalary) {
|
||
return 20; // 职位最高薪资低于账号最低要求
|
||
}
|
||
if (accountMaxSalary > 0 && salaryRange.min > accountMaxSalary) {
|
||
return 20; // 职位最低薪资高于账号最高要求
|
||
}
|
||
// 在范围内,根据薪资水平给分
|
||
if (avgJobSalary >= accountMinSalary && (accountMaxSalary === 0 || avgJobSalary <= accountMaxSalary)) {
|
||
return 100; // 完全符合要求
|
||
}
|
||
}
|
||
|
||
// 如果有期望薪资,与期望薪资比较
|
||
if (expectedSalary) {
|
||
const expected = this.parse_expected_salary(expectedSalary);
|
||
if (expected) {
|
||
const ratio = expected / avgJobSalary;
|
||
if (ratio <= 0.8) {
|
||
return 100; // 期望薪资低于职位薪资,完全匹配
|
||
} else if (ratio <= 1.0) {
|
||
return 90; // 期望薪资略低于或等于职位薪资
|
||
} else if (ratio <= 1.2) {
|
||
return 70; // 期望薪资略高于职位薪资
|
||
} else {
|
||
return 50; // 期望薪资明显高于职位薪资
|
||
}
|
||
}
|
||
}
|
||
|
||
// 没有期望薪资和账号配置,给中等分数
|
||
return 60;
|
||
}
|
||
|
||
/**
|
||
* 解析期望薪资(返回平均值)
|
||
* @param {string} expectedSalary - 期望薪资描述(如 "20-30K"、"5000-6000元/月")
|
||
* @returns {number|null} 期望薪资数值(元),如果是范围则返回平均值
|
||
*/
|
||
parse_expected_salary(expectedSalary) {
|
||
if (!expectedSalary) return null;
|
||
|
||
// 1. 匹配K格式范围:20-30K
|
||
const kRangeMatch = expectedSalary.match(/(\d+)[-~](\d+)[kK千]/);
|
||
if (kRangeMatch) {
|
||
const min = parseInt(kRangeMatch[1]) * 1000;
|
||
const max = parseInt(kRangeMatch[2]) * 1000;
|
||
return (min + max) / 2; // 返回平均值
|
||
}
|
||
|
||
// 2. 匹配单个K值:25K
|
||
const kMatch = expectedSalary.match(/(\d+)[kK千]/);
|
||
if (kMatch) {
|
||
return parseInt(kMatch[1]) * 1000;
|
||
}
|
||
|
||
// 3. 匹配元/月格式范围:5000-6000元/月
|
||
const yuanRangeMatch = expectedSalary.match(/(\d+)[-~](\d+)[元万]/);
|
||
if (yuanRangeMatch) {
|
||
const min = parseInt(yuanRangeMatch[1]);
|
||
const max = parseInt(yuanRangeMatch[2]);
|
||
if (expectedSalary.includes('万')) {
|
||
return ((min + max) / 2) * 10000;
|
||
} else {
|
||
return (min + max) / 2; // 返回平均值
|
||
}
|
||
}
|
||
|
||
// 4. 匹配单个元/月值:5000元/月
|
||
const yuanMatch = expectedSalary.match(/(\d+)[元万]/);
|
||
if (yuanMatch) {
|
||
const value = parseInt(yuanMatch[1]);
|
||
if (expectedSalary.includes('万')) {
|
||
return value * 10000;
|
||
} else {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
// 5. 匹配纯数字范围(如:20000-30000)
|
||
const numRangeMatch = expectedSalary.match(/(\d+)[-~](\d+)/);
|
||
if (numRangeMatch) {
|
||
return (parseInt(numRangeMatch[1]) + parseInt(numRangeMatch[2])) / 2; // 返回平均值
|
||
}
|
||
|
||
// 6. 匹配纯数字(如:20000)
|
||
const numMatch = expectedSalary.match(/(\d+)/);
|
||
if (numMatch) {
|
||
return parseInt(numMatch[1]);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 计算公司规模分数(0-100分)
|
||
* @param {string} companySize - 公司规模
|
||
* @returns {number} 公司规模分数
|
||
*/
|
||
calculate_company_size_score(companySize) {
|
||
if (!companySize) return 60; // 没有规模信息,给中等分数
|
||
|
||
const size = companySize.toLowerCase();
|
||
|
||
// 大型公司(1000人以上)给高分
|
||
if (size.includes('1000') || size.includes('1000+') || size.includes('1000人以上')) {
|
||
return 100;
|
||
}
|
||
if (size.includes('500-1000') || size.includes('500人以上')) {
|
||
return 90;
|
||
}
|
||
|
||
// 中型公司(100-500人)给中高分
|
||
if (size.includes('100-500') || size.includes('100人以上')) {
|
||
return 80;
|
||
}
|
||
if (size.includes('50-100')) {
|
||
return 70;
|
||
}
|
||
|
||
// 小型公司(50人以下)给中低分
|
||
if (size.includes('20-50') || size.includes('20人以上')) {
|
||
return 60;
|
||
}
|
||
if (size.includes('0-20') || size.includes('20人以下')) {
|
||
return 50;
|
||
}
|
||
|
||
// 默认中等分数
|
||
return 60;
|
||
}
|
||
|
||
/**
|
||
* 解析薪资范围
|
||
* @param {string} salaryDesc - 薪资描述(如 "15-25K·14薪"、"5000-6000元/月")
|
||
* @returns {object} 薪资范围 { min, max },单位:元
|
||
*/
|
||
parse_salary_range(salaryDesc) {
|
||
if (!salaryDesc) return { min: 0, max: 0 };
|
||
|
||
// 1. 匹配K格式:40-60K, 30-40K·18薪(忽略后面的薪数)
|
||
const kMatch = salaryDesc.match(/(\d+)[-~](\d+)[kK千]/);
|
||
if (kMatch) {
|
||
return {
|
||
min: parseInt(kMatch[1]) * 1000,
|
||
max: parseInt(kMatch[2]) * 1000
|
||
};
|
||
}
|
||
|
||
// 2. 匹配单个K值:25K
|
||
const singleKMatch = salaryDesc.match(/(\d+)[kK千]/);
|
||
if (singleKMatch) {
|
||
const value = parseInt(singleKMatch[1]) * 1000;
|
||
return { min: value, max: value };
|
||
}
|
||
|
||
// 3. 匹配元/月格式:5000-6000元/月
|
||
const yuanMatch = salaryDesc.match(/(\d+)[-~](\d+)[元万]/);
|
||
if (yuanMatch) {
|
||
const min = parseInt(yuanMatch[1]);
|
||
const max = parseInt(yuanMatch[2]);
|
||
// 判断单位(万或元)
|
||
if (salaryDesc.includes('万')) {
|
||
return {
|
||
min: min * 10000,
|
||
max: max * 10000
|
||
};
|
||
} else {
|
||
return { min, max };
|
||
}
|
||
}
|
||
|
||
// 4. 匹配单个元/月值:5000元/月
|
||
const singleYuanMatch = salaryDesc.match(/(\d+)[元万]/);
|
||
if (singleYuanMatch) {
|
||
const value = parseInt(singleYuanMatch[1]);
|
||
if (salaryDesc.includes('万')) {
|
||
return { min: value * 10000, max: value * 10000 };
|
||
} else {
|
||
return { min: value, max: value };
|
||
}
|
||
}
|
||
|
||
// 5. 匹配纯数字格式(如:20000-30000)
|
||
const numMatch = salaryDesc.match(/(\d+)[-~](\d+)/);
|
||
if (numMatch) {
|
||
return {
|
||
min: parseInt(numMatch[1]),
|
||
max: parseInt(numMatch[2])
|
||
};
|
||
}
|
||
|
||
return { min: 0, max: 0 };
|
||
}
|
||
|
||
/**
|
||
* 延迟函数
|
||
* @param {number} ms - 毫秒数
|
||
* @returns {Promise<void>}
|
||
*/
|
||
sleep(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
|
||
|
||
|
||
}
|
||
|
||
module.exports = new JobManager();
|