This commit is contained in:
张成
2025-11-26 18:39:15 +08:00
parent 771dc60607
commit d5d8069573
15 changed files with 2346 additions and 1593 deletions

View File

@@ -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: '账号信息更新成功' });
},

View File

@@ -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秒
}
}
/**

View File

@@ -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('[自动投递] 任务添加完成');

View File

@@ -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
}
});
}
}

View File

@@ -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