1
This commit is contained in:
@@ -147,6 +147,8 @@ module.exports = {
|
||||
'POST /account/update': async (ctx) => {
|
||||
const body = ctx.getBody();
|
||||
const { id, ...updateData } = body;
|
||||
|
||||
|
||||
await plaAccountService.updateAccount(id, updateData);
|
||||
return ctx.success({ message: '账号信息更新成功' });
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const dayjs = require('dayjs');
|
||||
const config = require('../../../config/config');
|
||||
|
||||
/**
|
||||
* 调度系统配置中心
|
||||
@@ -44,46 +43,16 @@ class ScheduleConfig {
|
||||
|
||||
// 监控配置
|
||||
this.monitoring = {
|
||||
heartbeatTimeout: 3 * 60 * 1000, // 心跳超时:5分钟
|
||||
taskFailureRate: 0.5, // 任务失败率:50%
|
||||
consecutiveFailures: 3, // 连续失败次数:3次
|
||||
alertCooldown: 5 * 60 * 1000, // 告警冷却:5分钟
|
||||
heartbeatTimeout: 3 * 60 * 1000, // 心跳超时:3分钟
|
||||
offlineThreshold: 24 * 60 * 60 * 1000 // 离线设备清理:24小时
|
||||
};
|
||||
|
||||
// 定时任务配置
|
||||
this.schedules = {
|
||||
dailyReset: '0 0 * * *', // 每天凌晨重置统计
|
||||
jobFlowInterval: '0 */5 * * * *', // 每10秒执行一次找工作流程
|
||||
monitoringInterval: '*/1 * * * *', // 监控检查间隔:1分钟
|
||||
autoDeliver: '0 */5 * * * *', // 自动投递任务:每5分钟执行一次
|
||||
// 监控检查间隔:1分钟
|
||||
autoDeliver: '0 */1 * * * *' // 自动投递任务:每5分钟执行一次
|
||||
};
|
||||
|
||||
// 测试配置覆盖
|
||||
if (config.test) {
|
||||
this.applyTestConfig(config.test);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用测试配置
|
||||
* @param {object} testConfig - 测试配置
|
||||
*/
|
||||
applyTestConfig(testConfig) {
|
||||
if (testConfig.skipWorkStartHour) {
|
||||
this.workHours.start = 0;
|
||||
this.workHours.end = 24;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 测试模式下缩短所有间隔时间
|
||||
if (testConfig.fastMode) {
|
||||
this.rateLimits.search = 10 * 1000; // 10秒
|
||||
this.rateLimits.apply = 5 * 1000; // 5秒
|
||||
this.rateLimits.chat = 2 * 1000; // 2秒
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const node_schedule = require("node-schedule");
|
||||
const dayjs = require('dayjs');
|
||||
const config = require('./config.js');
|
||||
const deviceManager = require('./deviceManager.js');
|
||||
const command = require('./command.js');
|
||||
@@ -331,6 +332,8 @@ class ScheduledJobs {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (!pla_users || pla_users.length === 0) {
|
||||
console.log('[自动投递] 没有启用且开启自动投递的账号');
|
||||
return;
|
||||
@@ -338,11 +341,13 @@ class ScheduledJobs {
|
||||
|
||||
console.log(`[自动投递] 找到 ${pla_users.length} 个可用账号`);
|
||||
|
||||
// 获取 task_status 模型用于查询上次投递时间
|
||||
const { task_status } = models;
|
||||
|
||||
// 为每个设备添加自动投递任务到队列
|
||||
for (const pl_user of pla_users) {
|
||||
const userData = pl_user.toJSON();
|
||||
|
||||
|
||||
// 检查设备调度策略
|
||||
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'deliver');
|
||||
if (!canExecute.allowed) {
|
||||
@@ -350,6 +355,44 @@ class ScheduledJobs {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取投递配置,如果不存在则使用默认值
|
||||
const deliver_config = userData.deliver_config || {
|
||||
deliver_interval: 30,
|
||||
min_salary: 0,
|
||||
max_salary: 0,
|
||||
page_count: 3,
|
||||
max_deliver: 10,
|
||||
filter_keywords: [],
|
||||
exclude_keywords: []
|
||||
};
|
||||
|
||||
// 检查投递间隔时间
|
||||
const deliver_interval = deliver_config.deliver_interval || 30; // 默认30分钟
|
||||
const interval_ms = deliver_interval * 60 * 1000; // 转换为毫秒
|
||||
|
||||
// 查询该账号最近一次成功完成的自动投递任务
|
||||
const lastDeliverTask = await task_status.findOne({
|
||||
where: {
|
||||
sn_code: userData.sn_code,
|
||||
taskType: 'auto_deliver',
|
||||
status: 'completed'
|
||||
},
|
||||
order: [['endTime', 'DESC']],
|
||||
attributes: ['endTime']
|
||||
});
|
||||
|
||||
// 如果存在上次投递记录,检查是否已经过了间隔时间
|
||||
if (lastDeliverTask && lastDeliverTask.endTime) {
|
||||
const lastDeliverTime = new Date(lastDeliverTask.endTime);
|
||||
const elapsedTime = now.getTime() - lastDeliverTime.getTime();
|
||||
|
||||
if (elapsedTime < interval_ms) {
|
||||
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
|
||||
console.log(`[自动投递] 设备 ${userData.sn_code} 距离上次投递仅 ${Math.round(elapsedTime / (60 * 1000))} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${deliver_interval} 分钟)`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自动投递任务到队列
|
||||
await this.taskQueue.addTask(userData.sn_code, {
|
||||
taskType: 'auto_deliver',
|
||||
@@ -357,19 +400,19 @@ class ScheduledJobs {
|
||||
taskParams: {
|
||||
keyword: userData.keyword || '',
|
||||
platform: userData.platform_type || 'boss',
|
||||
pageCount: 3, // 默认值
|
||||
maxCount: 10, // 默认值
|
||||
pageCount: deliver_config.page_count || 3,
|
||||
maxCount: deliver_config.max_deliver || 10,
|
||||
filterRules: {
|
||||
minSalary: userData.min_salary || 0,
|
||||
maxSalary: userData.max_salary || 0,
|
||||
keywords: [],
|
||||
excludeKeywords: []
|
||||
minSalary: deliver_config.min_salary || 0,
|
||||
maxSalary: deliver_config.max_salary || 0,
|
||||
keywords: deliver_config.filter_keywords || [],
|
||||
excludeKeywords: deliver_config.exclude_keywords || []
|
||||
}
|
||||
},
|
||||
priority: config.getTaskPriority('auto_deliver') || 6
|
||||
});
|
||||
|
||||
console.log(`[自动投递] 已为设备 ${userData.sn_code} 添加自动投递任务,关键词: ${userData.keyword || '默认'}`);
|
||||
console.log(`[自动投递] 已为设备 ${userData.sn_code} 添加自动投递任务,关键词: ${userData.keyword || '默认'},投递间隔: ${deliver_interval} 分钟`);
|
||||
}
|
||||
|
||||
console.log('[自动投递] 任务添加完成');
|
||||
|
||||
@@ -44,7 +44,7 @@ class TaskHandlers {
|
||||
|
||||
async handleAutoDeliverTask(task) {
|
||||
const { sn_code, taskParams } = task;
|
||||
const { keyword, platform, pageCount, maxCount } = taskParams;
|
||||
const { keyword, platform, pageCount, maxCount, filterRules = {} } = taskParams;
|
||||
|
||||
console.log(`[任务处理器] 自动投递任务 - 设备: ${sn_code}, 关键词: ${keyword}`);
|
||||
|
||||
@@ -181,11 +181,22 @@ class TaskHandlers {
|
||||
|
||||
// 5. 根据简历信息、职位类型配置和权重配置进行评分和过滤
|
||||
const scoredJobs = [];
|
||||
const excludeKeywords = jobTypeConfig && jobTypeConfig.excludeKeywords
|
||||
|
||||
// 合并排除关键词:从职位类型配置和任务参数中获取
|
||||
const jobTypeExcludeKeywords = jobTypeConfig && jobTypeConfig.excludeKeywords
|
||||
? (typeof jobTypeConfig.excludeKeywords === 'string'
|
||||
? JSON.parse(jobTypeConfig.excludeKeywords)
|
||||
: jobTypeConfig.excludeKeywords)
|
||||
: [];
|
||||
const taskExcludeKeywords = filterRules.excludeKeywords || [];
|
||||
const excludeKeywords = [...jobTypeExcludeKeywords, ...taskExcludeKeywords];
|
||||
|
||||
// 获取过滤关键词(用于优先匹配)
|
||||
const filterKeywords = filterRules.keywords || [];
|
||||
|
||||
// 获取薪资范围过滤
|
||||
const minSalary = filterRules.minSalary || 0;
|
||||
const maxSalary = filterRules.maxSalary || 0;
|
||||
|
||||
// 获取一个月内已投递的公司列表(用于过滤)
|
||||
const apply_records = db.getModel('apply_records');
|
||||
@@ -209,6 +220,20 @@ class TaskHandlers {
|
||||
for (const job of pendingJobs) {
|
||||
const jobData = job.toJSON ? job.toJSON() : job;
|
||||
|
||||
// 薪资范围过滤
|
||||
if (minSalary > 0 || maxSalary > 0) {
|
||||
const jobSalaryMin = jobData.salaryMin || 0;
|
||||
const jobSalaryMax = jobData.salaryMax || 0;
|
||||
|
||||
// 如果职位薪资范围与过滤范围没有交集,则跳过
|
||||
if (minSalary > 0 && jobSalaryMax > 0 && minSalary > jobSalaryMax) {
|
||||
continue;
|
||||
}
|
||||
if (maxSalary > 0 && jobSalaryMin > 0 && maxSalary < jobSalaryMin) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 排除关键词过滤
|
||||
if (Array.isArray(excludeKeywords) && excludeKeywords.length > 0) {
|
||||
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase();
|
||||
@@ -233,12 +258,28 @@ class TaskHandlers {
|
||||
priorityWeights
|
||||
);
|
||||
|
||||
// 如果配置了过滤关键词,给包含这些关键词的职位加分
|
||||
let keywordBonus = 0;
|
||||
if (Array.isArray(filterKeywords) && filterKeywords.length > 0) {
|
||||
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase();
|
||||
const matchedKeywords = filterKeywords.filter(kw => jobText.includes(kw.toLowerCase()));
|
||||
if (matchedKeywords.length > 0) {
|
||||
// 每匹配一个关键词加5分,最多加20分
|
||||
keywordBonus = Math.min(matchedKeywords.length * 5, 20);
|
||||
}
|
||||
}
|
||||
|
||||
const finalScore = scoreResult.totalScore + keywordBonus;
|
||||
|
||||
// 只保留总分 >= 60 的职位
|
||||
if (scoreResult.totalScore >= 60) {
|
||||
if (finalScore >= 60) {
|
||||
scoredJobs.push({
|
||||
...jobData,
|
||||
matchScore: scoreResult.totalScore,
|
||||
scoreDetails: scoreResult.scores
|
||||
matchScore: finalScore,
|
||||
scoreDetails: {
|
||||
...scoreResult.scores,
|
||||
keywordBonus: keywordBonus
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +59,31 @@ module.exports = (db) => {
|
||||
type: Sequelize.JSON(),
|
||||
allowNull: false,
|
||||
get: function () {
|
||||
return JSON.parse(this.getDataValue('is_salary_priority'));
|
||||
const value = this.getDataValue('is_salary_priority');
|
||||
if (!value) {
|
||||
return [{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}];
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return [{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set: function (value) {
|
||||
this.setDataValue('is_salary_priority', JSON.stringify(value));
|
||||
if (value === null || value === undefined) {
|
||||
this.setDataValue('is_salary_priority', JSON.stringify([{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}]));
|
||||
} else if (typeof value === 'string') {
|
||||
// 如果已经是字符串,直接使用
|
||||
this.setDataValue('is_salary_priority', value);
|
||||
} else {
|
||||
// 如果是对象/数组,序列化为字符串
|
||||
this.setDataValue('is_salary_priority', JSON.stringify(value));
|
||||
}
|
||||
},
|
||||
defaultValue: [ { "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20} ]
|
||||
defaultValue: JSON.stringify([{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}])
|
||||
},
|
||||
|
||||
|
||||
@@ -97,17 +116,56 @@ module.exports = (db) => {
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
min_salary: {
|
||||
comment: '最低薪资(单位:元)',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
max_salary: {
|
||||
comment: '最高薪资(单位:元)',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
// 自动投递配置(JSON格式,包含:deliver_interval-投递间隔分钟数, min_salary-最低薪资, max_salary-最高薪资, page_count-滚动获取职位列表次数, max_deliver-每次最多投递数量, filter_keywords-过滤关键词, exclude_keywords-排除关键词)
|
||||
deliver_config: {
|
||||
comment: '自动投递配置(JSON对象)',
|
||||
type: Sequelize.JSON(),
|
||||
allowNull: true,
|
||||
get: function () {
|
||||
const value = this.getDataValue('deliver_config');
|
||||
if (!value) return null;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set: function (value) {
|
||||
if (value === null || value === undefined) {
|
||||
this.setDataValue('deliver_config', null);
|
||||
} else if (typeof value === 'string') {
|
||||
// 如果已经是字符串,直接使用
|
||||
this.setDataValue('deliver_config', value);
|
||||
} else {
|
||||
// 如果是对象,序列化为字符串
|
||||
this.setDataValue('deliver_config', JSON.stringify(value));
|
||||
}
|
||||
},
|
||||
// 默认值说明:
|
||||
// deliver_interval: 30 - 投递间隔时间,单位:分钟,默认30分钟执行一次自动投递
|
||||
// min_salary: 0 - 最低薪资(单位:元),0表示不限制
|
||||
// max_salary: 0 - 最高薪资(单位:元),0表示不限制
|
||||
// page_count: 3 - 滚动获取职位列表次数,默认3次
|
||||
// max_deliver: 10 - 每次最多投递数量,默认10个
|
||||
// filter_keywords: [] - 过滤关键词数组,包含这些关键词的职位会被优先考虑
|
||||
// exclude_keywords: [] - 排除关键词数组,包含这些关键词的职位会被排除
|
||||
defaultValue: JSON.stringify({
|
||||
deliver_interval: 30,
|
||||
min_salary: 0,
|
||||
max_salary: 0,
|
||||
page_count: 3,
|
||||
max_deliver: 10,
|
||||
time_range: {
|
||||
start_time: '09:00',
|
||||
end_time: '18:00',
|
||||
workdays_only: 1
|
||||
},
|
||||
filter_keywords: [],
|
||||
exclude_keywords: []
|
||||
})
|
||||
},
|
||||
// 自动沟通相关配置
|
||||
auto_chat: {
|
||||
@@ -116,18 +174,52 @@ module.exports = (db) => {
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
chat_interval: {
|
||||
comment: '沟通间隔(单位:分钟)',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 30
|
||||
},
|
||||
auto_reply: {
|
||||
comment: '自动回复开关',
|
||||
type: Sequelize.TINYINT(1),
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
// 自动沟通配置(JSON格式,包含:chat_interval-沟通间隔分钟数, is_chat_outsourcing-是否沟通外包岗位, time_range-沟通时间段)
|
||||
chat_strategy: {
|
||||
comment: '自动沟通策略配置(JSON对象)',
|
||||
type: Sequelize.JSON(),
|
||||
allowNull: true,
|
||||
get: function () {
|
||||
const value = this.getDataValue('chat_strategy');
|
||||
if (!value) return null;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set: function (value) {
|
||||
if (value === null || value === undefined) {
|
||||
this.setDataValue('chat_strategy', null);
|
||||
} else if (typeof value === 'string') {
|
||||
// 如果已经是字符串,直接使用
|
||||
this.setDataValue('chat_strategy', value);
|
||||
} else {
|
||||
// 如果是对象,序列化为字符串
|
||||
this.setDataValue('chat_strategy', JSON.stringify(value));
|
||||
}
|
||||
},
|
||||
// 默认值说明:
|
||||
// chat_interval: 30 - 沟通间隔时间,单位:分钟,默认30分钟执行一次自动沟通
|
||||
// is_chat_outsourcing: 0 - 是否沟通外包岗位,0=不沟通外包岗位,1=沟通外包岗位
|
||||
// time_range: 沟通时间段配置
|
||||
// - start_time: 开始时间,格式:HH:mm,默认 "09:00"
|
||||
// - end_time: 结束时间,格式:HH:mm,默认 "18:00"
|
||||
// - workdays_only: 是否仅工作日,0=包含周末,1=仅工作日,默认1
|
||||
defaultValue: JSON.stringify({
|
||||
chat_interval: 30,
|
||||
is_chat_outsourcing: 0,
|
||||
time_range: {
|
||||
start_time: '09:00',
|
||||
end_time: '18:00',
|
||||
workdays_only: 1
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 自动活跃相关配置
|
||||
auto_active: {
|
||||
comment: '自动活跃开关',
|
||||
@@ -135,18 +227,47 @@ module.exports = (db) => {
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
// 是否沟通外包岗位
|
||||
is_chat_outsourcing: {
|
||||
comment: '是否沟通外包岗位',
|
||||
type: Sequelize.TINYINT(1),
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
// 自动活跃配置(JSON格式,包含:active_interval-活跃间隔分钟数, actions-活跃动作列表)
|
||||
active_actions: {
|
||||
comment: '自动活跃动作配置(JSON对象)',
|
||||
type: Sequelize.JSON(),
|
||||
allowNull: true,
|
||||
get: function () {
|
||||
const value = this.getDataValue('active_actions');
|
||||
if (!value) return null;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set: function (value) {
|
||||
if (value === null || value === undefined) {
|
||||
this.setDataValue('active_actions', null);
|
||||
} else if (typeof value === 'string') {
|
||||
// 如果已经是字符串,直接使用
|
||||
this.setDataValue('active_actions', value);
|
||||
} else {
|
||||
// 如果是对象,序列化为字符串
|
||||
this.setDataValue('active_actions', JSON.stringify(value));
|
||||
}
|
||||
},
|
||||
// 默认值说明:
|
||||
// active_interval: 60 - 活跃间隔时间,单位:分钟,默认60分钟执行一次活跃动作
|
||||
// actions: [] - 活跃动作列表,数组格式,可包含多个活跃动作配置对象
|
||||
defaultValue: JSON.stringify({
|
||||
active_interval: 60,
|
||||
actions: []
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
|
||||
//pla_account.sync({ force: true });
|
||||
// pla_account.sync({ force: true });
|
||||
return pla_account
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user