Compare commits

...

13 Commits

Author SHA1 Message Date
张成
d9d277fe59 1 2025-12-30 18:27:26 +08:00
张成
d2ae741b9e 1 2025-12-30 17:47:07 +08:00
张成
52876229a8 1 2025-12-30 17:06:14 +08:00
张成
914999c9fc 1 2025-12-30 16:45:09 +08:00
张成
fb9aa5b155 1 2025-12-30 16:36:46 +08:00
张成
b49bd658a6 1 2025-12-30 16:30:37 +08:00
张成
956cfe88f8 1 2025-12-30 16:23:45 +08:00
张成
c45ea21c83 1 2025-12-30 16:18:28 +08:00
张成
fa2dea3f04 1 2025-12-30 15:49:51 +08:00
张成
dd7373c0b8 1 2025-12-30 15:48:41 +08:00
张成
65833dd32d 11 2025-12-30 15:46:18 +08:00
张成
d14f89e008 1 2025-12-30 14:51:33 +08:00
张成
dcaf0cb428 1 2025-12-30 14:37:33 +08:00
55 changed files with 4601 additions and 2850 deletions

View File

@@ -10,7 +10,17 @@
"Bash(mkdir:*)", "Bash(mkdir:*)",
"Bash(findstr:*)", "Bash(findstr:*)",
"Bash(cat:*)", "Bash(cat:*)",
"Bash(npm run restart:*)" "Bash(npm run restart:*)",
"Bash(del scheduledJobs.js)",
"Bash(ls:*)",
"Bash(wc:*)",
"Bash(for:*)",
"Bash(done)",
"Bash(npm start)",
"Bash(timeout 10 npm start)",
"Bash(timeout 15 npm start)",
"Bash(del apiservicesconfigaiConfig.js)",
"Bash(grep:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -34,7 +34,6 @@ async function syncAllModels() {
// 执行同步 // 执行同步
await model.sync({ alter: true }); await model.sync({ alter: true });
console.log(`${modelName} 同步完成`);
return { modelName, success: true }; return { modelName, success: true };
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,66 @@
-- 为 pla_account 表添加自动搜索相关字段
-- 执行时间2025-01-XX
-- 说明:添加自动搜索开关和搜索配置字段
-- ============================================
-- 添加自动搜索开关字段auto_search
-- ============================================
ALTER TABLE `pla_account`
ADD COLUMN `auto_search` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '自动搜索开关1=启用0=禁用)'
AFTER `auto_deliver`;
-- ============================================
-- 添加自动搜索配置字段search_config
-- ============================================
ALTER TABLE `pla_account`
ADD COLUMN `search_config` JSON COMMENT '自动搜索配置JSON对象包含search_interval-搜索间隔分钟数, city-城市, cityName-城市名称, salary-薪资, experience-经验, education-学历)'
AFTER `auto_search`;
-- ============================================
-- 为已有账号设置默认配置
-- ============================================
-- 为所有账号设置默认的 search_config如果为 NULL
UPDATE `pla_account`
SET `search_config` = JSON_OBJECT(
'search_interval', 30,
'city', '',
'cityName', '',
'salary', '',
'experience', '',
'education', ''
)
WHERE `search_config` IS NULL;
-- ============================================
-- 验证字段是否添加成功
-- ============================================
SELECT
COLUMN_NAME AS '字段名',
COLUMN_TYPE AS '字段类型',
IS_NULLABLE AS '允许空',
COLUMN_DEFAULT AS '默认值',
COLUMN_COMMENT AS '注释'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'pla_account'
AND COLUMN_NAME IN ('auto_search', 'search_config')
ORDER BY ORDINAL_POSITION;
-- ============================================
-- 注意事项
-- ============================================
-- 1. auto_search 使用 TINYINT(1) 类型,默认值为 0关闭
-- 2. search_config 使用 JSON 类型MySQL 5.7+
-- 3. 如果 MySQL 版本低于 5.7,请将 JSON 类型改为 TEXT 类型
-- 4. 执行前建议先备份数据库
-- 5. 如果字段已存在会报错,请先删除字段再执行:
-- ALTER TABLE `pla_account` DROP COLUMN `auto_search`;
-- ALTER TABLE `pla_account` DROP COLUMN `search_config`;
-- 6. search_config 默认值包含以下字段:
-- - search_interval: 30搜索间隔单位分钟
-- - city: ''(城市代码)
-- - cityName: ''(城市名称)
-- - salary: ''(薪资范围)
-- - experience: ''(工作经验要求)
-- - education: ''(学历要求)

View File

@@ -1,307 +0,0 @@
const axios = require('axios');
const config = require('../../../config/config');
const logs = require('../logProxy');
/**
* Qwen 2.5 大模型服务
* 集成阿里云 DashScope API提供智能化的岗位筛选、聊天生成、简历分析等功能
*/
class aiService {
constructor() {
this.apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY;
// 使用 DashScope 兼容 OpenAI 格式的接口
this.apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
// Qwen 2.5 模型qwen-turbo快速、qwen-plus增强、qwen-max最强
this.model = config.ai?.model || 'qwen-turbo';
this.maxRetries = 3;
}
/**
* 调用 Qwen 2.5 API
* @param {string} prompt - 提示词
* @param {object} options - 配置选项
* @returns {Promise<object>} API响应结果
*/
async callAPI(prompt, options = {}) {
const requestData = {
model: this.model,
messages: [
{
role: 'system',
content: options.systemPrompt || '你是一个专业的招聘顾问,擅长分析岗位信息、生成聊天内容和分析简历匹配度。'
},
{
role: 'user',
content: prompt
}
],
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 2000,
top_p: options.topP || 0.9
};
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const response = await axios.post(this.apiUrl, requestData, {
headers: {
'Authorization': `Bearer ${this.apiKey}`, // DashScope 使用 Bearer token 格式
'Content-Type': 'application/json'
},
timeout: 30000
});
return {
data: response.data,
content: response.data.choices?.[0]?.message?.content || ''
};
} catch (error) {
console.log(`Qwen 2.5 API调用失败 (尝试 ${attempt}/${this.maxRetries}): ${error.message}`);
if (attempt === this.maxRetries) {
throw new Error(error.message);
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
/**
* 岗位智能筛选
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @returns {Promise<object>} 筛选结果
*/
async analyzeJob(jobInfo, resumeInfo) {
const prompt = `
请分析以下岗位信息,并给出详细的评估结果:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 薪资范围:${jobInfo.salary || '未知'}
- 工作地点:${jobInfo.location || '未知'}
- 岗位描述:${jobInfo.description || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
简历信息:
- 技能标签:${resumeInfo.skills || '未知'}
- 工作经验:${resumeInfo.experience || '未知'}
- 教育背景:${resumeInfo.education || '未知'}
- 期望薪资:${resumeInfo.expectedSalary || '未知'}
请从以下维度进行分析:
1. 技能匹配度0-100分
2. 经验匹配度0-100分
3. 薪资合理性0-100分
4. 公司质量评估0-100分
5. 是否为外包岗位(是/否)
6. 综合推荐指数0-100分
7. 详细分析说明
8. 投递建议
请以JSON格式返回结果。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的招聘分析师,擅长评估岗位与简历的匹配度。请提供客观、专业的分析结果。',
temperature: 0.3
});
try {
// 尝试解析JSON响应
const analysis = JSON.parse(result.content);
return {
analysis: analysis
};
} catch (parseError) {
// 如果解析失败,返回原始内容
return {
analysis: {
content: result.content,
parseError: true
}
};
}
}
/**
* 生成个性化聊天内容
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @param {string} chatType - 聊天类型 (greeting/interview/followup)
* @returns {Promise<object>} 聊天内容
*/
async generateChatContent(jobInfo, resumeInfo, chatType = 'greeting') {
const chatTypeMap = {
'greeting': '初次打招呼',
'interview': '面试邀约',
'followup': '跟进沟通'
};
const prompt = `
请为以下场景生成个性化的聊天内容:
聊天类型:${chatTypeMap[chatType] || chatType}
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
简历信息:
- 技能标签:${resumeInfo.skills || '未知'}
- 工作经验:${resumeInfo.experience || '未知'}
- 项目经验:${resumeInfo.projects || '未知'}
要求:
1. 内容要自然、专业、个性化
2. 突出简历与岗位的匹配点
3. 避免过于机械化的表达
4. 长度控制在100-200字
5. 体现求职者的诚意和热情
请直接返回聊天内容,不需要其他格式。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的招聘沟通专家,擅长生成自然、专业的求职聊天内容。',
temperature: 0.8
});
return result;
}
/**
* 分析简历要素
* @param {string} resumeText - 简历文本内容
* @returns {Promise<object>} 简历分析结果
*/
async analyzeResume(resumeText) {
const prompt = `
请分析以下简历内容,并返回 JSON 格式的分析结果:
简历内容:
${resumeText}
请按以下格式返回 JSON 结果:
{
"skillTags": ["技能1", "技能2", "技能3"], // 技能标签数组(编程语言、框架、工具等)
"strengths": "核心优势描述", // 简历的优势和亮点
"weaknesses": "不足之处描述", // 简历的不足或需要改进的地方
"careerSuggestion": "职业发展建议", // 针对该简历的职业发展方向和建议
"competitiveness": 75 // 竞争力评分0-100的整数综合考虑工作年限、技能、经验等因素
}
要求:
1. skillTags 必须是字符串数组
2. strengths、weaknesses、careerSuggestion 是字符串描述
3. competitiveness 必须是 0-100 之间的整数
4. 所有字段都必须返回,如果没有相关信息,使用空数组或空字符串
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的简历分析师,擅长分析简历的核心要素、优势劣势、竞争力评分和职业发展建议。请以 JSON 格式返回分析结果,确保格式正确。',
temperature: 0.3,
maxTokens: 1500
});
try {
// 尝试从返回内容中提取 JSON
let content = result.content.trim();
// 如果返回内容被代码块包裹,提取其中的 JSON
const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || content.match(/(\{[\s\S]*\})/);
if (jsonMatch) {
content = jsonMatch[1];
}
const analysis = JSON.parse(content);
return {
analysis: analysis
};
} catch (parseError) {
console.error(`[AI服务] 简历分析结果解析失败:`, parseError);
console.error(`[AI服务] 原始内容:`, result.content);
return {
analysis: {
content: result.content,
parseError: true
}
};
}
}
/**
* 生成面试邀约内容
* @param {object} jobInfo - 岗位信息
* @param {object} chatHistory - 聊天历史
* @returns {Promise<object>} 面试邀约内容
*/
async generateInterviewInvitation(jobInfo, chatHistory) {
const prompt = `
请基于以下信息生成面试邀约内容:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 工作地点:${jobInfo.location || '未知'}
聊天历史:
${chatHistory || '无'}
要求:
1. 表达面试邀约的诚意
2. 提供灵活的时间选择
3. 说明面试形式和地点
4. 体现对候选人的重视
5. 语言自然、专业
请直接返回面试邀约内容。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的HR擅长生成面试邀约内容。',
temperature: 0.6
});
return result;
}
/**
* 识别外包岗位
* @param {object} jobInfo - 岗位信息
* @returns {Promise<object>} 外包识别结果
*/
async identifyOutsourcingJob(jobInfo) {
const prompt = `
请分析以下岗位信息,判断是否为外包岗位:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 岗位描述:${jobInfo.description || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
- 工作地点:${jobInfo.location || '未知'}
外包岗位特征:
1. 公司名称包含"外包"、"派遣"、"人力"等关键词
2. 岗位描述提到"项目外包"、"驻场开发"等
3. 技能要求过于宽泛或具体
4. 工作地点频繁变动
5. 薪资结构不明确
请判断是否为外包岗位,并给出详细分析。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的岗位分析师,擅长识别外包岗位的特征。',
temperature: 0.3
});
return result;
}
}
module.exports = new aiService();

View File

@@ -3,10 +3,7 @@
* 聚合所有 job 相关模块的方法,提供统一的对外接口 * 聚合所有 job 相关模块的方法,提供统一的对外接口
*/ */
const jobManager = require('./jobManager'); const { jobManager, resumeManager, chatManager } = require('./managers');
const resumeManager = require('./resumeManager');
const chatManager = require('./chatManager');
const pack = (instance) => { const pack = (instance) => {
const proto = Object.getPrototypeOf(instance); const proto = Object.getPrototypeOf(instance);
@@ -23,7 +20,6 @@ const pack = (instance) => {
/** /**
* 便捷方法:直接导出常用方法 * 便捷方法:直接导出常用方法
* 使用下划线命名规范
*/ */
module.exports = { module.exports = {
...pack(jobManager), ...pack(jobManager),

View File

@@ -1,651 +0,0 @@
# 职位过滤服务文档
## 概述
`job_filter_service.js` 是一个职位文本匹配过滤服务,使用简单的文本匹配规则来过滤和分析职位信息。该服务支持从数据库动态获取职位类型的技能关键词和排除关键词,能够分析职位与简历的匹配度,并提供过滤和评分功能。
## 主要功能
1. **职位类型配置管理**:从数据库获取或使用默认配置的技能关键词和排除关键词
2. **匹配度分析**:分析职位与简历的匹配度(技能、经验、薪资)
3. **职位过滤**:根据匹配分数、外包标识、排除关键词等条件过滤职位列表
4. **自定义权重评分**:支持根据自定义权重配置计算职位评分(距离、薪资、工作年限、学历、技能)
## 类结构
```javascript
class JobFilterService {
// 默认技能关键词
defaultCommonSkills: Array<string>
// 默认排除关键词
defaultExcludeKeywords: Array<string>
// 职位类型配置缓存
jobTypeCache: Map<number, Object>
}
```
## API 文档
### 1. getJobTypeConfig(jobTypeId)
根据职位类型ID获取技能关键词和排除关键词配置。
**参数:**
- `jobTypeId` (number, 可选): 职位类型ID
**返回值:**
```javascript
{
commonSkills: Array<string>, // 技能关键词列表
excludeKeywords: Array<string> // 排除关键词列表
}
```
**说明:**
- 如果未提供 `jobTypeId` 或配置获取失败,返回默认配置
- 配置结果会缓存5分钟避免频繁查询数据库
-`job_types` 表读取 `commonSkills``excludeKeywords` 字段JSON格式
**使用示例:**
```javascript
const config = await jobFilterService.getJobTypeConfig(1);
console.log(config.commonSkills); // ['Vue', 'React', ...]
console.log(config.excludeKeywords); // ['外包', '外派', ...]
```
---
### 2. clearCache(jobTypeId)
清除职位类型配置缓存。
**参数:**
- `jobTypeId` (number, 可选): 职位类型ID不传则清除所有缓存
**使用示例:**
```javascript
// 清除特定职位类型的缓存
jobFilterService.clearCache(1);
// 清除所有缓存
jobFilterService.clearCache();
```
---
### 3. analyzeJobMatch(jobInfo, resumeInfo, jobTypeId)
使用文本匹配分析职位与简历的匹配度。
**参数:**
- `jobInfo` (object, 必需): 职位信息对象
- `jobTitle` (string): 职位名称
- `companyName` (string): 公司名称
- `description` (string): 职位描述
- `skills` (string): 技能要求
- `requirements` (string): 职位要求
- `salary` (string): 薪资范围
- `experience` (string): 经验要求
- `education` (string): 学历要求
- `longitude` (number): 经度
- `latitude` (number): 纬度
- `resumeInfo` (object, 可选): 简历信息对象
- `skills` (string|Array): 技能列表
- `skillDescription` (string): 技能描述
- `currentPosition` (string): 当前职位
- `expectedPosition` (string): 期望职位
- `workYears` (number|string): 工作年限
- `expectedSalary` (string): 期望薪资
- `education` (string): 学历
- `jobTypeId` (number, 可选): 职位类型ID
**返回值:**
```javascript
{
skillMatch: number, // 技能匹配度0-100
experienceMatch: number, // 经验匹配度0-100
salaryMatch: number, // 薪资匹配度0-100
isOutsourcing: boolean, // 是否为外包岗位
overallScore: number, // 综合推荐指数0-100
matchReasons: Array<string>, // 匹配原因列表
concerns: Array<string>, // 关注点列表
suggestion: string, // 投递建议
analysis: Object // 完整的分析结果(同上)
}
```
**评分规则:**
1. **综合推荐指数计算**
- 技能匹配度 × 40% + 经验匹配度 × 30% + 薪资匹配度 × 30%
2. **技能匹配度**
- 从职位描述中提取技能关键词
- 计算简历中匹配的技能数量占比
- 无简历信息时,基于职位关键词数量评分
3. **经验匹配度**
- 从职位描述中提取经验要求应届、1年、2年、3年、5年、10年
- 根据简历工作年限与要求的匹配程度评分
4. **薪资匹配度**
- 解析职位薪资范围和期望薪资
- 期望薪资低于职位薪资时得高分,高于职位薪资时得低分
5. **外包检测**
- 检测职位描述中是否包含:外包、外派、驻场、人力外包、项目外包
**使用示例:**
```javascript
const jobInfo = {
jobTitle: '前端开发工程师',
description: '要求3年以上Vue开发经验熟悉React',
salary: '15-25K',
experience: '3-5年'
};
const resumeInfo = {
skills: 'Vue, React, JavaScript',
workYears: 4,
expectedSalary: '20K'
};
const result = await jobFilterService.analyzeJobMatch(jobInfo, resumeInfo, 1);
console.log(result.overallScore); // 85
console.log(result.matchReasons); // ['技能匹配度高', '工作经验符合要求', ...]
console.log(result.suggestion); // '强烈推荐投递:匹配度很高'
```
---
### 4. filterJobs(jobs, filterRules, resumeInfo, jobTypeId)
过滤职位列表,返回匹配的职位(带匹配分数)。
**参数:**
- `jobs` (Array, 必需): 职位列表(可以是 Sequelize 模型实例或普通对象)
- `filterRules` (object, 可选): 过滤规则
- `minScore` (number, 默认60): 最低匹配分数
- `excludeOutsourcing` (boolean, 默认true): 是否排除外包岗位
- `excludeKeywords` (Array<string>, 默认[]): 额外排除关键词列表
- `resumeInfo` (object, 可选): 简历信息
- `jobTypeId` (number, 可选): 职位类型ID
**返回值:**
```javascript
Array<{
...jobData, // 原始职位数据
matchScore: number, // 匹配分数
matchAnalysis: Object // 完整的匹配分析结果
}>
```
**过滤逻辑:**
1. 对每个职位进行匹配度分析
2. 过滤掉匹配分数低于 `minScore` 的职位
3. 如果 `excludeOutsourcing` 为 true过滤掉外包岗位
4. 过滤掉包含排除关键词的职位
5. 按匹配分数降序排序
**使用示例:**
```javascript
const jobs = [
{ jobTitle: '前端开发', description: 'Vue开发...', salary: '20K' },
{ jobTitle: '后端开发', description: 'Java开发...', salary: '25K' }
];
const resumeInfo = {
skills: 'Vue, JavaScript',
workYears: 3
};
const filterRules = {
minScore: 70,
excludeOutsourcing: true,
excludeKeywords: ['销售']
};
const filteredJobs = await jobFilterService.filterJobs(
jobs,
filterRules,
resumeInfo,
1
);
console.log(filteredJobs.length); // 过滤后的职位数量
console.log(filteredJobs[0].matchScore); // 第一个职位的匹配分数
```
---
### 5. calculateJobScoreWithWeights(jobData, resumeInfo, accountConfig, jobTypeConfig, priorityWeights)
根据自定义权重配置计算职位评分。
**参数:**
- `jobData` (object, 必需): 职位数据
- `longitude` (number): 经度
- `latitude` (number): 纬度
- `salary` (string): 薪资范围
- `experience` (string): 经验要求
- `education` (string): 学历要求
- `resumeInfo` (object, 必需): 简历信息
- `expectedSalary` (string): 期望薪资
- `workYears` (string|number): 工作年限
- `education` (string): 学历
- `skills` (string|Array): 技能列表
- `accountConfig` (object, 必需): 账号配置
- `user_longitude` (number): 用户经度
- `user_latitude` (number): 用户纬度
- `jobTypeConfig` (object, 可选): 职位类型配置(包含 commonSkills
- `priorityWeights` (Array, 必需): 权重配置
```javascript
[
{ key: 'distance', weight: 30 }, // 距离权重0-100
{ key: 'salary', weight: 40 }, // 薪资权重
{ key: 'work_years', weight: 20 }, // 工作年限权重
{ key: 'education', weight: 10 } // 学历权重
]
```
**返回值:**
```javascript
{
totalScore: number, // 总分0-100+,技能评分作为额外加分项)
scores: {
distance: number, // 距离评分
salary: number, // 薪资评分
work_years: number, // 工作年限评分
education: number, // 学历评分
skills: number // 技能评分(如果有 jobTypeConfig
}
}
```
**评分规则:**
1. **距离评分**
- 0-5km: 100分
- 5-10km: 90分
- 10-20km: 80分
- 20-50km: 60分
- 50km以上: 30分
2. **薪资评分**
- 职位薪资 ≥ 期望薪资: 100分
- 职位薪资 ≥ 期望薪资 × 0.8: 80分
- 职位薪资 ≥ 期望薪资 × 0.6: 60分
- 其他: 40分
3. **工作年限评分**
- 简历年限 ≥ 职位要求: 100分
- 简历年限 ≥ 职位要求 × 0.8: 80分
- 简历年限 ≥ 职位要求 × 0.6: 60分
- 其他: 40分
4. **学历评分**
- 简历学历 ≥ 职位要求: 100分
- 简历学历 = 职位要求 - 1级: 70分
- 其他: 40分
5. **技能评分**
- 计算简历技能与职位类型配置的技能关键词匹配度
- 作为额外加分项固定权重10%
**使用示例:**
```javascript
const jobData = {
longitude: 116.3974,
latitude: 39.9093,
salary: '20-30K',
experience: '3-5年',
education: '本科'
};
const resumeInfo = {
expectedSalary: '25K',
workYears: 4,
education: '本科',
skills: ['Vue', 'React', 'JavaScript']
};
const accountConfig = {
user_longitude: 116.4074,
user_latitude: 39.9042
};
const jobTypeConfig = {
commonSkills: ['Vue', 'React', 'JavaScript', 'Node.js']
};
const priorityWeights = [
{ key: 'distance', weight: 30 },
{ key: 'salary', weight: 40 },
{ key: 'work_years', weight: 20 },
{ key: 'education', weight: 10 }
];
const result = jobFilterService.calculateJobScoreWithWeights(
jobData,
resumeInfo,
accountConfig,
jobTypeConfig,
priorityWeights
);
console.log(result.totalScore); // 85
console.log(result.scores); // { distance: 100, salary: 90, work_years: 100, ... }
```
---
### 6. parseSalaryRange(salaryDesc)
解析薪资范围字符串。
**参数:**
- `salaryDesc` (string): 薪资描述字符串
- 支持格式:`15-25K`、`25K`、`5000-6000元/月`、`2-3万`、`20000-30000` 等
**返回值:**
```javascript
{
min: number, // 最低薪资(元)
max: number // 最高薪资(元)
}
```
或 `null`(解析失败时)
**使用示例:**
```javascript
const range1 = jobFilterService.parseSalaryRange('15-25K');
console.log(range1); // { min: 15000, max: 25000 }
const range2 = jobFilterService.parseSalaryRange('2-3万');
console.log(range2); // { min: 20000, max: 30000 }
```
---
### 7. parseExpectedSalary(expectedSalary)
解析期望薪资字符串,返回平均值。
**参数:**
- `expectedSalary` (string): 期望薪资描述字符串
**返回值:**
- `number`: 期望薪资数值(元),如果是范围则返回平均值
- `null`: 解析失败时
**使用示例:**
```javascript
const salary1 = jobFilterService.parseExpectedSalary('20-30K');
console.log(salary1); // 25000平均值
const salary2 = jobFilterService.parseExpectedSalary('25K');
console.log(salary2); // 25000
```
---
### 8. parseWorkYears(workYearsStr)
解析工作年限字符串为数字。
**参数:**
- `workYearsStr` (string): 工作年限字符串(如 "3年"、"5年以上"
**返回值:**
- `number`: 工作年限数字
- `null`: 解析失败时
**使用示例:**
```javascript
const years = jobFilterService.parseWorkYears('3-5年');
console.log(years); // 3提取第一个数字
```
---
## 辅助方法
### buildJobText(jobInfo)
构建职位文本(用于匹配)。
**参数:**
- `jobInfo` (object): 职位信息对象
**返回值:**
- `string`: 合并后的职位文本(小写)
---
### buildResumeText(resumeInfo)
构建简历文本(用于匹配)。
**参数:**
- `resumeInfo` (object): 简历信息对象
**返回值:**
- `string`: 合并后的简历文本(小写)
---
### calculateSkillMatch(jobText, resumeText, commonSkills)
计算技能匹配度0-100分
**参数:**
- `jobText` (string): 职位文本
- `resumeText` (string): 简历文本
- `commonSkills` (Array<string>): 技能关键词列表
**返回值:**
- `number`: 匹配度分数0-100
---
### calculateExperienceMatch(jobInfo, resumeInfo)
计算经验匹配度0-100分
**参数:**
- `jobInfo` (object): 职位信息
- `resumeInfo` (object): 简历信息
**返回值:**
- `number`: 匹配度分数0-100
---
### calculateSalaryMatch(jobInfo, resumeInfo)
计算薪资合理性0-100分
**参数:**
- `jobInfo` (object): 职位信息
- `resumeInfo` (object): 简历信息
**返回值:**
- `number`: 匹配度分数0-100
---
### checkOutsourcing(jobText)
检查是否为外包岗位。
**参数:**
- `jobText` (string): 职位文本
**返回值:**
- `boolean`: 是否为外包
---
## 默认配置
### 默认技能关键词
```javascript
[
'Vue', 'React', 'Angular', 'JavaScript', 'TypeScript', 'Node.js',
'Python', 'Java', 'C#', '.NET', 'Flutter', 'React Native',
'Webpack', 'Vite', 'Redux', 'MobX', 'Express', 'Koa',
'Django', 'Flask', 'MySQL', 'MongoDB', 'Redis',
'WebRTC', 'FFmpeg', 'Canvas', 'WebSocket', 'HTML5', 'CSS3',
'jQuery', 'Bootstrap', 'Element UI', 'Ant Design',
'Git', 'Docker', 'Kubernetes', 'AWS', 'Azure',
'Selenium', 'Jest', 'Mocha', 'Cypress'
]
```
### 默认排除关键词
```javascript
[
'外包', '外派', '驻场', '销售', '客服', '电话销售',
'地推', '推广', '市场', '运营', '行政', '文员'
]
```
## 数据库依赖
该服务依赖以下数据库表:
### job_types 表
存储职位类型配置,需要包含以下字段:
- `id` (number): 职位类型ID
- `is_enabled` (number): 是否启用1=启用0=禁用)
- `commonSkills` (string|JSON): 技能关键词列表JSON数组字符串
- `excludeKeywords` (string|JSON): 排除关键词列表JSON数组字符串
**示例数据:**
```json
{
"id": 1,
"is_enabled": 1,
"commonSkills": "[\"Vue\", \"React\", \"JavaScript\"]",
"excludeKeywords": "[\"外包\", \"外派\"]"
}
```
## 使用示例
### 完整使用流程
```javascript
const jobFilterService = require('./job_filter_service');
// 1. 分析单个职位的匹配度
const jobInfo = {
jobTitle: '高级前端开发工程师',
companyName: 'XX科技有限公司',
description: '负责前端架构设计要求5年以上Vue/React开发经验',
skills: 'Vue, React, TypeScript, Node.js',
requirements: '本科及以上学历,有大型项目经验',
salary: '25-40K·14薪'
};
const resumeInfo = {
skills: 'Vue, React, JavaScript, TypeScript, Node.js, Webpack',
skillDescription: '精通Vue生态熟悉React有5年+前端开发经验',
currentPosition: '高级前端开发工程师',
expectedPosition: '前端架构师',
workYears: 6,
expectedSalary: '30K',
education: '本科'
};
const analysis = await jobFilterService.analyzeJobMatch(
jobInfo,
resumeInfo,
1 // 职位类型ID
);
console.log('匹配分析结果:');
console.log('综合评分:', analysis.overallScore);
console.log('技能匹配度:', analysis.skillMatch);
console.log('经验匹配度:', analysis.experienceMatch);
console.log('薪资匹配度:', analysis.salaryMatch);
console.log('是否外包:', analysis.isOutsourcing);
console.log('匹配原因:', analysis.matchReasons);
console.log('关注点:', analysis.concerns);
console.log('投递建议:', analysis.suggestion);
// 2. 过滤职位列表
const jobs = await Job.findAll({ where: { status: 1 } });
const filteredJobs = await jobFilterService.filterJobs(
jobs,
{
minScore: 70,
excludeOutsourcing: true,
excludeKeywords: ['销售', '客服']
},
resumeInfo,
1
);
console.log(`共找到 ${filteredJobs.length} 个匹配的职位`);
filteredJobs.forEach((job, index) => {
console.log(`${index + 1}. ${job.jobTitle} - 匹配分数:${job.matchScore}`);
});
// 3. 自定义权重评分
const accountConfig = {
user_longitude: 116.4074,
user_latitude: 39.9042
};
const priorityWeights = [
{ key: 'distance', weight: 25 },
{ key: 'salary', weight: 35 },
{ key: 'work_years', weight: 25 },
{ key: 'education', weight: 15 }
];
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
jobInfo,
resumeInfo,
accountConfig,
{ commonSkills: ['Vue', 'React', 'TypeScript'] },
priorityWeights
);
console.log('自定义权重评分:', scoreResult.totalScore);
console.log('各项评分:', scoreResult.scores);
```
## 注意事项
1. **缓存机制**职位类型配置会缓存5分钟修改数据库配置后需要调用 `clearCache()` 清除缓存
2. **性能优化**
- 大量职位过滤时,建议分批处理
- 避免在循环中频繁调用 `getJobTypeConfig()`,配置会被自动缓存
3. **文本匹配**
- 所有文本匹配均为小写匹配,不区分大小写
- 匹配逻辑较为简单,如需更精确的匹配,建议使用 AI 分析(二期规划)
4. **薪资解析**
- 支持多种薪资格式,但可能无法解析所有格式
- 解析失败时返回默认分数或 null
5. **错误处理**
- 所有方法都包含错误处理,失败时返回默认值或空结果
- 建议在生产环境中监控日志输出
## 版本历史
- **v1.0.0**: 初始版本,支持基础文本匹配和过滤功能
- 支持从数据库动态获取职位类型配置
- 支持自定义权重评分计算

View File

@@ -1,5 +1,8 @@
// const aiService = require('./aiService'); // 二期规划AI 服务暂时禁用 const aiServiceModule = require('../../../services/ai_service');
const logs = require('../logProxy'); const logs = require('../../logProxy');
// 实例化AI服务
const aiService = aiServiceModule.getInstance();
/** /**
* 智能聊天管理模块 * 智能聊天管理模块

View File

@@ -0,0 +1,13 @@
/**
* Managers 模块统一导出
*/
const jobManager = require('./jobManager');
const resumeManager = require('./resumeManager');
const chatManager = require('./chatManager');
module.exports = {
jobManager,
resumeManager,
chatManager
};

View File

@@ -1,10 +1,13 @@
// const aiService = require('./aiService'); // 二期规划AI 服务暂时禁用 const aiServiceModule = require('../../../services/ai_service');
const jobFilterService = require('./job_filter_service'); // 使用文本匹配过滤服务 const { jobFilterService } = require('../services');
const locationService = require('../../services/locationService'); // 位置服务 const locationService = require('../../../services/locationService');
const logs = require('../logProxy'); const logs = require('../../logProxy');
const db = require('../dbProxy'); const db = require('../../dbProxy');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
// 实例化AI服务
const aiService = aiServiceModule.getInstance();
/** /**
* 工作管理模块 * 工作管理模块
* 负责简历获取分析存储和匹配度计算 * 负责简历获取分析存储和匹配度计算

View File

@@ -1,9 +1,12 @@
const aiService = require('./aiService'); const aiServiceModule = require('../../../services/ai_service');
const jobFilterService = require('./job_filter_service'); const { jobFilterService } = require('../services');
const logs = require('../logProxy'); const logs = require('../../logProxy');
const db = require('../dbProxy'); const db = require('../../dbProxy');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
// 实例化AI服务
const aiService = aiServiceModule.getInstance();
/** /**
* 简历管理模块 * 简历管理模块
* 负责简历获取分析存储和匹配度计算 * 负责简历获取分析存储和匹配度计算

View File

@@ -0,0 +1,9 @@
/**
* Services 模块统一导出
*/
const jobFilterService = require('./jobFilterService');
module.exports = {
jobFilterService
};

View File

@@ -4,8 +4,8 @@
* 支持从数据库动态获取职位类型的技能关键词和排除关键词 * 支持从数据库动态获取职位类型的技能关键词和排除关键词
*/ */
const db = require('../dbProxy.js'); const db = require('../../dbProxy.js');
const locationService = require('../../services/locationService'); const locationService = require('../../../services/locationService');
class JobFilterService { class JobFilterService {
constructor() { constructor() {

View File

@@ -0,0 +1,7 @@
/**
* Utils 模块统一导出
*/
module.exports = {
// 工具函数将在需要时添加
};

View File

@@ -29,12 +29,7 @@ class MqttSyncClient {
return; return;
} }
// 记录日志但不包含敏感信息
const { maskSensitiveData } = require('../../utils/crypto_utils');
const safeMessage = maskSensitiveData(messageObj, ['password', 'pwd', 'token', 'secret', 'key', 'cookie']);
console.log('[MQTT] 收到消息', topic, '类型:', messageObj.action || messageObj.type || 'unknown');
// 优化:只通知相关 topic 的监听器,而不是所有监听器
// 1. 触发该 topic 的专用监听器 // 1. 触发该 topic 的专用监听器
const topicListeners = this.messageListeners.get(topic); const topicListeners = this.messageListeners.get(topic);
if (topicListeners && topicListeners.size > 0) { if (topicListeners && topicListeners.size > 0) {

View File

@@ -1,6 +1,6 @@
const db = require('../dbProxy.js'); const db = require('../dbProxy.js');
const logProxy = require('../logProxy.js'); const logProxy = require('../logProxy.js');
const deviceManager = require('../schedule/deviceManager.js'); const deviceManager = require('../schedule/core/deviceManager.js');
/** /**
* MQTT 消息分发器 * MQTT 消息分发器
@@ -212,8 +212,6 @@ class MqttDispatcher {
return; return;
} }
console.log(`[MQTT心跳] 收到设备 ${sn_code} 的心跳消息`);
// 移除 device_status 模型依赖 // 移除 device_status 模型依赖
// const device_status = db.getModel('device_status'); // const device_status = db.getModel('device_status');
// let device = await device_status.findByPk(sn_code); // let device = await device_status.findByPk(sn_code);
@@ -286,7 +284,6 @@ class MqttDispatcher {
}, },
{ where: { sn_code } } { where: { sn_code } }
); );
console.log(`[MQTT心跳] 设备 ${sn_code} 状态已更新到数据库 - 在线: true, 登录: ${updateData.isLoggedIn || false}`);
} catch (error) { } catch (error) {
console.error(`[MQTT心跳] 更新数据库状态失败:`, error); console.error(`[MQTT心跳] 更新数据库状态失败:`, error);
} }

View File

@@ -1,9 +1,9 @@
const logs = require('../logProxy'); const logs = require('../../logProxy');
const db = require('../dbProxy'); const db = require('../../dbProxy');
const jobManager = require('../job/index'); const jobManager = require('../../job/index');
const ScheduleUtils = require('./utils'); const ScheduleUtils = require('../utils/scheduleUtils');
const ScheduleConfig = require('./config'); const ScheduleConfig = require('../infrastructure/config');
const authorizationService = require('../../services/authorization_service'); const authorizationService = require('../../../services/authorization_service');
/** /**
@@ -129,7 +129,7 @@ class CommandManager {
// 4.5 推送指令开始执行状态 // 4.5 推送指令开始执行状态
try { try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const taskQueue = require('./taskQueue'); const taskQueue = require('./taskQueue');
const summary = await taskQueue.getTaskStatusSummary(task.sn_code); const summary = await taskQueue.getTaskStatusSummary(task.sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary, { await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary, {
@@ -163,7 +163,7 @@ class CommandManager {
// 6.5 推送指令完成状态 // 6.5 推送指令完成状态
try { try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const taskQueue = require('./taskQueue'); const taskQueue = require('./taskQueue');
const summary = await taskQueue.getTaskStatusSummary(task.sn_code); const summary = await taskQueue.getTaskStatusSummary(task.sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary); await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary);
@@ -193,7 +193,7 @@ class CommandManager {
// 推送指令失败状态 // 推送指令失败状态
try { try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const taskQueue = require('./taskQueue'); const taskQueue = require('./taskQueue');
const summary = await taskQueue.getTaskStatusSummary(task.sn_code); const summary = await taskQueue.getTaskStatusSummary(task.sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary); await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary);
@@ -213,58 +213,17 @@ class CommandManager {
* @private * @private
*/ */
async _execute_command_with_timeout(command_id, command_type, command_name, command_params, sn_code, mqttClient, start_time) { async _execute_command_with_timeout(command_id, command_type, command_name, command_params, sn_code, mqttClient, start_time) {
// 将驼峰命名转换为下划线命名
const to_snake_case = (str) => {
if (str.includes('_')) {
return str;
}
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
};
const method_name = to_snake_case(command_type);
// 获取指令超时时间从配置中获取默认5分钟 // 获取指令超时时间从配置中获取默认5分钟
const timeout = ScheduleConfig.taskTimeouts[command_type] || const timeout = ScheduleConfig.taskTimeouts[command_type] || 5 * 60 * 1000;
ScheduleConfig.taskTimeouts[method_name] ||
5 * 60 * 1000;
// 构建指令执行 Promise // 构建指令执行 Promise
const command_promise = (async () => { const command_promise = (async () => {
// 指令类型映射表(内部指令类型 -> jobManager方法名) // 直接使用 command_type 调用 jobManager 的方法,不做映射
const commandMethodMap = { // command_type 和 jobManager 的方法名保持一致
// get_job_list 指令对应MQTT Action: "get_job_list"
'get_job_list': 'get_job_list',
'getJobList': 'get_job_list',
// search_jobs_with_params 指令对应MQTT Action: "search_job_list"
'search_jobs_with_params': 'search_jobs_with_params',
'searchJobsWithParams': 'search_jobs_with_params',
// search_and_deliver 指令内部调用search_jobs_with_params和deliver_resume
'search_and_deliver': 'search_and_deliver',
'searchAndDeliver': 'search_and_deliver',
// deliver_resume 指令对应MQTT Action: "deliver_resume"
'deliver_resume': 'deliver_resume',
'deliverResume': 'deliver_resume'
// search_jobs 指令对应MQTT Action: "search_jobs"
'search_jobs': 'search_jobs',
'searchJobs': 'search_jobs'
};
// 优先使用映射表
const mappedMethod = commandMethodMap[command_type] || commandMethodMap[method_name];
if (mappedMethod && jobManager[mappedMethod]) {
return await jobManager[mappedMethod](sn_code, mqttClient, command_params);
}
// 其次尝试转换后的方法名
if (command_type && jobManager[method_name]) {
return await jobManager[method_name](sn_code, mqttClient, command_params);
}
// 最后尝试原始指令类型
if (jobManager[command_type]) { if (jobManager[command_type]) {
return await jobManager[command_type](sn_code, mqttClient, command_params); return await jobManager[command_type](sn_code, mqttClient, command_params);
} else { } else {
throw new Error(`未知的指令类型: ${command_type} (尝试的方法名: ${method_name}, 映射方法: ${mappedMethod})`); throw new Error(`未知的指令类型: ${command_type}, jobManager 中不存在对应方法`);
} }
})(); })();

View File

@@ -1,8 +1,8 @@
const dayjs = require('dayjs'); const dayjs = require('dayjs');
const Sequelize = require('sequelize'); const Sequelize = require('sequelize');
const db = require('../dbProxy'); const db = require('../../dbProxy');
const config = require('./config'); const config = require('../infrastructure/config');
const utils = require('./utils'); const utils = require('../utils/scheduleUtils');
/** /**
* 设备管理器简化版 * 设备管理器简化版
@@ -77,7 +77,6 @@ class DeviceManager {
// 更新登录状态 // 更新登录状态
if (heartbeatData.isLoggedIn !== undefined) { if (heartbeatData.isLoggedIn !== undefined) {
device.isLoggedIn = heartbeatData.isLoggedIn; device.isLoggedIn = heartbeatData.isLoggedIn;
console.log(`[设备管理器] 设备 ${sn_code} 登录状态更新 - isLoggedIn: ${device.isLoggedIn}`);
} }
} }

View File

@@ -0,0 +1,16 @@
/**
* Core 模块导出
* 统一导出核心模块,简化引用路径
*/
const deviceManager = require('./deviceManager');
const taskQueue = require('./taskQueue');
const command = require('./command');
const scheduledJobs = require('./scheduledJobs');
module.exports = {
deviceManager,
taskQueue,
command,
scheduledJobs
};

View File

@@ -0,0 +1,570 @@
const node_schedule = require("node-schedule");
const dayjs = require('dayjs');
const config = require('../infrastructure/config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const db = require('../../dbProxy');
// 引入新的任务模块
const tasks = require('../tasks');
const { autoSearchTask, autoDeliverTask, autoChatTask, autoActiveTask } = tasks;
const Framework = require("../../../../framework/node-core-framework.js");
/**
* 定时任务管理器(重构版)
* 使用独立的任务模块,职责更清晰,易于维护和扩展
*/
class ScheduledJobs {
constructor(components, taskHandlers) {
this.taskQueue = components.taskQueue;
this.taskHandlers = taskHandlers;
this.jobs = [];
}
/**
* 启动所有定时任务
*/
start() {
console.log('[定时任务] 开始启动所有定时任务...');
// ==================== 系统维护任务 ====================
// 每天凌晨重置统计数据
const resetJob = node_schedule.scheduleJob(config.schedules.dailyReset, () => {
this.resetDailyStats();
});
this.jobs.push(resetJob);
console.log('[定时任务] ✓ 已启动每日统计重置任务');
// 启动心跳检查定时任务(每分钟检查一次)
const monitoringJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await deviceManager.checkHeartbeatStatus().catch(error => {
console.error('[定时任务] 检查心跳状态失败:', error);
});
});
this.jobs.push(monitoringJob);
console.log('[定时任务] ✓ 已启动心跳检查任务');
// 启动离线设备任务清理定时任务(每分钟检查一次)
const cleanupOfflineTasksJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.cleanupOfflineDeviceTasks().catch(error => {
console.error('[定时任务] 清理离线设备任务失败:', error);
});
});
this.jobs.push(cleanupOfflineTasksJob);
console.log('[定时任务] ✓ 已启动离线设备任务清理任务');
// 启动任务超时检查定时任务(每分钟检查一次)
const timeoutCheckJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.checkTaskTimeouts().catch(error => {
console.error('[定时任务] 检查任务超时失败:', error);
});
});
this.jobs.push(timeoutCheckJob);
console.log('[定时任务] ✓ 已启动任务超时检查任务');
// 启动任务状态摘要同步定时任务每10秒发送一次
const taskSummaryJob = node_schedule.scheduleJob('*/10 * * * * *', async () => {
await this.syncTaskStatusSummary().catch(error => {
console.error('[定时任务] 同步任务状态摘要失败:', error);
});
});
this.jobs.push(taskSummaryJob);
console.log('[定时任务] ✓ 已启动任务状态摘要同步任务');
// ==================== 业务任务(使用新的任务模块) ====================
// 1. 自动搜索任务 - 每60分钟执行一次
const autoSearchJob = node_schedule.scheduleJob(config.schedules.autoSearch || '0 0 */1 * * *', () => {
this.runAutoSearchTask();
});
this.jobs.push(autoSearchJob);
console.log('[定时任务] ✓ 已启动自动搜索任务 (每60分钟)');
// 2. 自动投递任务 - 每1分钟检查一次
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
this.runAutoDeliverTask();
});
this.jobs.push(autoDeliverJob);
console.log('[定时任务] ✓ 已启动自动投递任务 (每1分钟)');
// 3. 自动沟通任务 - 每15分钟执行一次
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */15 * * * *', () => {
this.runAutoChatTask();
});
this.jobs.push(autoChatJob);
console.log('[定时任务] ✓ 已启动自动沟通任务 (每15分钟)');
// 4. 自动活跃任务 - 每2小时执行一次
const autoActiveJob = node_schedule.scheduleJob(config.schedules.autoActive || '0 0 */2 * * *', () => {
this.runAutoActiveTask();
});
this.jobs.push(autoActiveJob);
console.log('[定时任务] ✓ 已启动自动活跃任务 (每2小时)');
// 立即执行一次业务任务(可选)
setTimeout(() => {
console.log('[定时任务] 立即执行一次初始化任务...');
this.runAutoDeliverTask();
this.runAutoChatTask();
}, 10000); // 延迟10秒,等待系统初始化完成和设备心跳
console.log('[定时任务] 所有定时任务启动完成!');
}
// ==================== 业务任务执行方法(使用新的任务模块) ====================
/**
* 运行自动搜索任务
* 为所有启用自动搜索的账号添加搜索任务
*/
async runAutoSearchTask() {
try {
const accounts = await this.getEnabledAccounts('auto_search');
if (accounts.length === 0) {
return;
}
console.log(`[自动搜索调度] 找到 ${accounts.length} 个启用自动搜索的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoSearchTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动搜索调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动搜索调度] 执行失败:', error);
}
}
/**
* 运行自动投递任务
* 为所有启用自动投递的账号添加投递任务
*/
async runAutoDeliverTask() {
try {
const accounts = await this.getEnabledAccounts('auto_deliver');
if (accounts.length === 0) {
return;
}
console.log(`[自动投递调度] 找到 ${accounts.length} 个启用自动投递的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoDeliverTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动投递调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动投递调度] 执行失败:', error);
}
}
/**
* 运行自动沟通任务
* 为所有启用自动沟通的账号添加沟通任务
*/
async runAutoChatTask() {
try {
const accounts = await this.getEnabledAccounts('auto_chat');
if (accounts.length === 0) {
return;
}
console.log(`[自动沟通调度] 找到 ${accounts.length} 个启用自动沟通的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoChatTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动沟通调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动沟通调度] 执行失败:', error);
}
}
/**
* 运行自动活跃任务
* 为所有启用自动活跃的账号添加活跃任务
*/
async runAutoActiveTask() {
try {
const accounts = await this.getEnabledAccounts('auto_active');
if (accounts.length === 0) {
return;
}
console.log(`[自动活跃调度] 找到 ${accounts.length} 个启用自动活跃的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoActiveTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动活跃调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动活跃调度] 执行失败:', error);
}
}
/**
* 获取启用指定功能的账号列表
* @param {string} featureType - 功能类型: auto_search, auto_deliver, auto_chat, auto_active
*/
async getEnabledAccounts(featureType) {
try {
const { pla_account } = db.models;
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1,
[featureType]: 1
},
attributes: ['sn_code', 'name', 'keyword', 'platform_type']
});
return accounts.map(acc => acc.toJSON());
} catch (error) {
console.error(`[获取账号列表] 失败 (${featureType}):`, error);
return [];
}
}
// ==================== 系统维护方法(保留原有逻辑) ====================
/**
* 重置每日统计
*/
resetDailyStats() {
console.log('[定时任务] 重置每日统计数据');
try {
deviceManager.resetAllDailyCounters();
console.log('[定时任务] 每日统计重置完成');
} catch (error) {
console.error('[定时任务] 重置统计失败:', error);
}
}
/**
* 清理过期数据
*/
cleanupCaches() {
console.log('[定时任务] 开始清理过期数据');
try {
deviceManager.cleanupOfflineDevices(config.monitoring.offlineThreshold);
command.cleanupExpiredCommands(30);
console.log('[定时任务] 数据清理完成');
} catch (error) {
console.error('[定时任务] 数据清理失败:', error);
}
}
/**
* 清理离线设备任务
* 检查离线超过10分钟的设备取消其所有pending/running状态的任务
*/
async cleanupOfflineDeviceTasks() {
try {
// 离线阈值10分钟
const offlineThreshold = 10 * 60 * 1000;
const now = Date.now();
const thresholdTime = now - offlineThreshold;
// 获取所有启用的账号
const pla_account = db.getModel('pla_account');
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 通过 deviceManager 检查哪些设备离线超过10分钟
const offlineSnCodes = [];
const offlineDevicesInfo = [];
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: null
});
} else {
const lastHeartbeat = device.lastHeartbeat || 0;
if (lastHeartbeat < thresholdTime || !device.isOnline) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: lastHeartbeat ? new Date(lastHeartbeat) : null
});
}
}
}
if (offlineSnCodes.length === 0) {
return;
}
console.log(`[清理离线任务] 发现 ${offlineSnCodes.length} 个离线超过10分钟的设备`);
let totalCancelled = 0;
const task_status = db.getModel('task_status');
for (const sn_code of offlineSnCodes) {
try {
const deviceInfo = offlineDevicesInfo.find(d => d.sn_code === sn_code);
const updateResult = await task_status.update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({
reason: '设备离线超过10分钟任务已自动取消',
offlineTime: deviceInfo?.lastHeartbeatTime
})
},
{
where: {
sn_code: sn_code,
status: ['pending', 'running']
}
}
);
const cancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
totalCancelled += cancelledCount;
if (this.taskQueue && typeof this.taskQueue.cancelDeviceTasks === 'function') {
await this.taskQueue.cancelDeviceTasks(sn_code);
}
if (cancelledCount > 0) {
console.log(`[清理离线任务] 设备 ${sn_code} 已取消 ${cancelledCount} 个任务`);
}
} catch (error) {
console.error(`[清理离线任务] 取消设备 ${sn_code} 的任务失败:`, error);
}
}
if (totalCancelled > 0) {
console.log(`[清理离线任务] 共取消 ${totalCancelled} 个离线设备的任务`);
}
} catch (error) {
console.error('[清理离线任务] 执行失败:', error);
}
}
/**
* 同步任务状态摘要到客户端
*/
async syncTaskStatusSummary() {
try {
const { pla_account } = await Framework.getModels();
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
const offlineThreshold = 3 * 60 * 1000;
const now = Date.now();
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
continue;
}
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
continue;
}
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const summary = await this.taskQueue.getTaskStatusSummary(sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, {
currentCommand: summary.currentCommand || null
});
} catch (error) {
console.error(`[设备工作状态同步] 设备 ${sn_code} 同步失败:`, error.message);
}
}
} catch (error) {
console.error('[任务状态同步] 执行失败:', error);
}
}
/**
* 检查任务超时并强制标记为失败
*/
async checkTaskTimeouts() {
try {
const Sequelize = require('sequelize');
const { task_status, op } = db.models;
const runningTasks = await task_status.findAll({
where: {
status: 'running'
},
attributes: ['id', 'sn_code', 'taskType', 'taskName', 'startTime', 'create_time']
});
if (!runningTasks || runningTasks.length === 0) {
return;
}
const now = new Date();
let timeoutCount = 0;
for (const task of runningTasks) {
const taskData = task.toJSON();
const startTime = taskData.startTime ? new Date(taskData.startTime) : (taskData.create_time ? new Date(taskData.create_time) : null);
if (!startTime) {
continue;
}
const taskTimeout = config.getTaskTimeout(taskData.taskType) || 10 * 60 * 1000;
const maxAllowedTime = taskTimeout * 1.2;
const elapsedTime = now.getTime() - startTime.getTime();
if (elapsedTime > maxAllowedTime) {
try {
await task_status.update(
{
status: 'failed',
endTime: now,
duration: elapsedTime,
result: JSON.stringify({
error: `任务执行超时(运行时间: ${Math.round(elapsedTime / 1000)}秒,超时限制: ${Math.round(maxAllowedTime / 1000)}秒)`,
timeout: true,
taskType: taskData.taskType,
startTime: startTime.toISOString()
}),
progress: 0
},
{
where: { id: taskData.id }
}
);
timeoutCount++;
console.warn(`[任务超时检查] 任务 ${taskData.id} (${taskData.taskName}) 运行时间过长,已强制标记为失败`);
if (this.taskQueue && typeof this.taskQueue.deviceStatus !== 'undefined') {
const deviceStatus = this.taskQueue.deviceStatus.get(taskData.sn_code);
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.id === taskData.id) {
deviceStatus.isRunning = false;
deviceStatus.currentTask = null;
deviceStatus.runningCount = Math.max(0, deviceStatus.runningCount - 1);
this.taskQueue.globalRunningCount = Math.max(0, this.taskQueue.globalRunningCount - 1);
console.log(`[任务超时检查] 已重置设备 ${taskData.sn_code} 的状态`);
setTimeout(() => {
this.taskQueue.processQueue(taskData.sn_code).catch(error => {
console.error(`[任务超时检查] 继续处理队列失败:`, error);
});
}, 100);
}
}
} catch (error) {
console.error(`[任务超时检查] 更新超时任务状态失败:`, error);
}
}
}
if (timeoutCount > 0) {
console.log(`[任务超时检查] 共检测到 ${timeoutCount} 个超时任务`);
}
} catch (error) {
console.error('[任务超时检查] 执行失败:', error);
}
}
/**
* 停止所有定时任务
*/
stop() {
console.log('[定时任务] 停止所有定时任务...');
for (const job of this.jobs) {
if (job) {
job.cancel();
}
}
this.jobs = [];
console.log('[定时任务] 所有定时任务已停止');
}
}
module.exports = ScheduledJobs;

View File

@@ -1,14 +1,14 @@
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const Sequelize = require('sequelize'); const Sequelize = require('sequelize');
const logs = require('../logProxy'); const logs = require('../../logProxy');
const db = require('../dbProxy'); const db = require('../../dbProxy');
const command = require('./command'); const command = require('./command');
const PriorityQueue = require('./PriorityQueue'); const PriorityQueue = require('../infrastructure/PriorityQueue');
const ErrorHandler = require('./ErrorHandler'); const ErrorHandler = require('../infrastructure/ErrorHandler');
const deviceManager = require('./deviceManager'); const deviceManager = require('./deviceManager');
const ScheduleUtils = require('./utils'); const ScheduleUtils = require('../utils/scheduleUtils');
const ScheduleConfig = require('./config'); const ScheduleConfig = require('../infrastructure/config');
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
/** /**
* 任务队列管理器重构版 * 任务队列管理器重构版
@@ -222,7 +222,6 @@ class TaskQueue {
// 移除 device_status 依赖,不再检查设备在线状态 // 移除 device_status 依赖,不再检查设备在线状态
// 如果需要在线状态检查,可以从 deviceManager 获取 // 如果需要在线状态检查,可以从 deviceManager 获取
const deviceManager = require('./deviceManager');
const deviceStatus = deviceManager.getAllDevicesStatus(); const deviceStatus = deviceManager.getAllDevicesStatus();
const onlineSnCodes = new Set( const onlineSnCodes = new Set(
Object.entries(deviceStatus) Object.entries(deviceStatus)
@@ -230,24 +229,7 @@ class TaskQueue {
.map(([sn_code]) => sn_code) .map(([sn_code]) => sn_code)
); );
// 原有代码已移除,改为使用 deviceManager
/*
const device_status = db.getModel('device_status');
const heartbeatTimeout = require('./config.js').monitoring.heartbeatTimeout;
const now = new Date();
const heartbeatThreshold = new Date(now.getTime() - heartbeatTimeout);
const onlineDevices = await device_status.findAll({
where: {
isOnline: true,
lastHeartbeatTime: {
[Sequelize.Op.gte]: heartbeatThreshold // 心跳时间在阈值内
}
},
attributes: ['sn_code']
});
const onlineSnCodes = new Set(onlineDevices.map(dev => dev.sn_code));
*/
let processedCount = 0; let processedCount = 0;
let queuedCount = 0; let queuedCount = 0;
@@ -1065,13 +1047,13 @@ class TaskQueue {
async getMqttClient() { async getMqttClient() {
try { try {
// 首先尝试从调度系统获取已初始化的MQTT客户端 // 首先尝试从调度系统获取已初始化的MQTT客户端
const scheduleManager = require('./index'); const scheduleManager = require('../index');
if (scheduleManager.mqttClient) { if (scheduleManager.mqttClient) {
return scheduleManager.mqttClient; return scheduleManager.mqttClient;
} }
// 如果调度系统没有初始化,则直接创建 // 如果调度系统没有初始化,则直接创建
const mqttManager = require('../mqtt/mqttManager'); const mqttManager = require('../../mqtt/mqttManager');
console.log('[任务队列] 创建新的MQTT客户端'); console.log('[任务队列] 创建新的MQTT客户端');
return await mqttManager.getInstance(); return await mqttManager.getInstance();
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,88 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
/**
* 自动活跃处理器
* 负责保持账户活跃度
*/
class ActiveHandler extends BaseHandler {
/**
* 处理自动活跃任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handle(task) {
return await this.execute(task, async () => {
return await this.doActive(task);
}, {
checkAuth: true,
checkOnline: true,
recordDeviceMetrics: true
});
}
/**
* 执行活跃逻辑
*/
async doActive(task) {
const { sn_code, taskParams } = task;
const { platform = 'boss' } = taskParams;
console.log(`[自动活跃] 开始 - 设备: ${sn_code}`);
// 1. 获取账户配置
const accountConfig = await this.getAccountConfig(sn_code, ['platform_type', 'active_strategy']);
if (!accountConfig) {
return {
activeCount: 0,
message: '未找到账户配置'
};
}
// 2. 解析活跃策略配置
const activeStrategy = ConfigManager.parseActiveStrategy(accountConfig.active_strategy);
// 3. 检查活跃时间范围
const timeRange = ConfigManager.getTimeRange(activeStrategy);
if (timeRange) {
const timeRangeValidator = require('../services/timeRangeValidator');
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
if (!timeCheck.allowed) {
return {
activeCount: 0,
message: timeCheck.reason
};
}
}
// 4. 创建活跃指令
const actions = activeStrategy.actions || ['view_jobs'];
const activeCommands = actions.map(action => ({
command_type: `active_${action}`,
command_name: `自动活跃 - ${action}`,
command_params: JSON.stringify({
sn_code,
platform: platform || accountConfig.platform_type || 'boss',
action
}),
priority: config.getTaskPriority('auto_active') || 5
}));
// 5. 执行活跃指令
const result = await command.executeCommands(task.id, activeCommands, this.mqttClient);
console.log(`[自动活跃] 完成 - 设备: ${sn_code}, 执行动作: ${actions.join(', ')}`);
return {
activeCount: actions.length,
actions,
message: '活跃完成'
};
}
}
module.exports = ActiveHandler;

View File

@@ -0,0 +1,250 @@
const deviceManager = require('../core/deviceManager');
const accountValidator = require('../services/accountValidator');
const db = require('../../dbProxy');
/**
* 任务处理器基类
* 提供通用的授权检查、计时、错误处理、设备记录等功能
*/
class BaseHandler {
constructor(mqttClient) {
this.mqttClient = mqttClient;
}
/**
* 执行任务(带授权检查和错误处理)
* @param {object} task - 任务对象
* @param {Function} businessLogic - 业务逻辑函数
* @param {object} options - 选项
* @returns {Promise<object>} 执行结果
*/
async execute(task, businessLogic, options = {}) {
const {
checkAuth = true, // 是否检查授权
checkOnline = true, // 是否检查在线状态
recordDeviceMetrics = true // 是否记录设备指标
} = options;
const { sn_code, taskName } = task;
const startTime = Date.now();
try {
// 1. 验证账户(启用 + 授权 + 在线)
if (checkAuth || checkOnline) {
const validation = await accountValidator.validate(sn_code, {
checkEnabled: true,
checkAuth,
checkOnline,
offlineThreshold: 3 * 60 * 1000 // 3分钟
});
if (!validation.valid) {
throw new Error(`设备 ${sn_code} 验证失败: ${validation.reason}`);
}
}
// 2. 记录任务开始
if (recordDeviceMetrics) {
deviceManager.recordTaskStart(sn_code, task);
}
// 3. 执行业务逻辑
const result = await businessLogic();
// 4. 记录任务成功
const duration = Date.now() - startTime;
if (recordDeviceMetrics) {
deviceManager.recordTaskComplete(sn_code, task, true, duration);
}
return {
success: true,
duration,
...result
};
} catch (error) {
// 5. 记录任务失败
const duration = Date.now() - startTime;
if (recordDeviceMetrics) {
deviceManager.recordTaskComplete(sn_code, task, false, duration);
}
console.error(`[${taskName}] 执行失败 (设备: ${sn_code}):`, error.message);
return {
success: false,
error: error.message,
duration
};
}
}
/**
* 检查每日操作限制
* @param {string} sn_code - 设备序列号
* @param {string} operation - 操作类型 (search, deliver, chat)
* @param {string} platform - 平台类型
* @returns {Promise<{allowed: boolean, count?: number, limit?: number, reason?: string}>}
*/
async checkDailyLimit(sn_code, operation, platform = 'boss') {
try {
const today = new Date().toISOString().split('T')[0];
const task_status = db.getModel('task_status');
// 查询今日该操作的完成次数
const count = await task_status.count({
where: {
sn_code,
taskType: `auto_${operation}`,
status: 'completed',
endTime: {
[db.models.op.gte]: new Date(today)
}
}
});
// 获取每日限制(从 deviceManager 或配置)
const limit = deviceManager.canExecuteOperation(sn_code, operation);
if (!limit.allowed) {
return {
allowed: false,
count,
reason: limit.reason
};
}
return {
allowed: true,
count,
limit: limit.max || 999
};
} catch (error) {
console.error(`[每日限制检查] 失败 (${sn_code}, ${operation}):`, error);
return { allowed: true }; // 检查失败时默认允许
}
}
/**
* 检查执行间隔时间
* @param {string} sn_code - 设备序列号
* @param {string} taskType - 任务类型
* @param {number} intervalMinutes - 间隔时间(分钟)
* @returns {Promise<{allowed: boolean, elapsed?: number, remaining?: number, reason?: string}>}
*/
async checkInterval(sn_code, taskType, intervalMinutes) {
try {
const task_status = db.getModel('task_status');
// 查询最近一次成功完成的任务
const lastTask = await task_status.findOne({
where: {
sn_code,
taskType,
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
if (!lastTask || !lastTask.endTime) {
return { allowed: true, elapsed: null };
}
const now = Date.now();
const lastTime = new Date(lastTask.endTime).getTime();
const elapsed = now - lastTime;
const intervalMs = intervalMinutes * 60 * 1000;
if (elapsed < intervalMs) {
const remainingMinutes = Math.ceil((intervalMs - elapsed) / (60 * 1000));
const elapsedMinutes = Math.floor(elapsed / (60 * 1000));
return {
allowed: false,
elapsed: elapsedMinutes,
remaining: remainingMinutes,
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟`
};
}
return {
allowed: true,
elapsed: Math.floor(elapsed / (60 * 1000))
};
} catch (error) {
console.error(`[间隔检查] 失败 (${sn_code}, ${taskType}):`, error);
return { allowed: true }; // 检查失败时默认允许
}
}
/**
* 获取账户配置
* @param {string} sn_code - 设备序列号
* @param {string[]} fields - 需要的字段
* @returns {Promise<object|null>}
*/
async getAccountConfig(sn_code, fields = ['*']) {
try {
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: { sn_code, is_delete: 0 },
attributes: fields
});
return account ? account.toJSON() : null;
} catch (error) {
console.error(`[获取账户配置] 失败 (${sn_code}):`, error);
return null;
}
}
/**
* 推送设备工作状态(可选的通知)
* @param {string} sn_code - 设备序列号
* @param {object} status - 状态信息
*/
async notifyDeviceStatus(sn_code, status) {
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, status);
} catch (error) {
console.warn(`[状态推送] 失败 (${sn_code}):`, error.message);
}
}
/**
* 标准化错误响应
* @param {Error} error - 错误对象
* @param {string} sn_code - 设备序列号
* @returns {object} 标准化的错误响应
*/
formatError(error, sn_code) {
return {
success: false,
error: error.message || '未知错误',
sn_code,
timestamp: new Date().toISOString()
};
}
/**
* 标准化成功响应
* @param {object} data - 响应数据
* @param {string} sn_code - 设备序列号
* @returns {object} 标准化的成功响应
*/
formatSuccess(data, sn_code) {
return {
success: true,
sn_code,
timestamp: new Date().toISOString(),
...data
};
}
}
module.exports = BaseHandler;

View File

@@ -0,0 +1,87 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
/**
* 自动沟通处理器
* 负责自动回复HR消息
*/
class ChatHandler extends BaseHandler {
/**
* 处理自动沟通任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handle(task) {
return await this.execute(task, async () => {
return await this.doChat(task);
}, {
checkAuth: true,
checkOnline: true,
recordDeviceMetrics: true
});
}
/**
* 执行沟通逻辑
*/
async doChat(task) {
const { sn_code, taskParams } = task;
const { platform = 'boss' } = taskParams;
console.log(`[自动沟通] 开始 - 设备: ${sn_code}`);
// 1. 获取账户配置
const accountConfig = await this.getAccountConfig(sn_code, ['platform_type', 'chat_strategy']);
if (!accountConfig) {
return {
chatCount: 0,
message: '未找到账户配置'
};
}
// 2. 解析沟通策略配置
const chatStrategy = ConfigManager.parseChatStrategy(accountConfig.chat_strategy);
// 3. 检查沟通时间范围
const timeRange = ConfigManager.getTimeRange(chatStrategy);
if (timeRange) {
const timeRangeValidator = require('../services/timeRangeValidator');
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
if (!timeCheck.allowed) {
return {
chatCount: 0,
message: timeCheck.reason
};
}
}
// 4. 创建沟通指令
const chatCommand = {
command_type: 'autoChat',
command_name: '自动沟通',
command_params: JSON.stringify({
sn_code,
platform: platform || accountConfig.platform_type || 'boss',
autoReply: chatStrategy.auto_reply || false,
replyTemplate: chatStrategy.reply_template || ''
}),
priority: config.getTaskPriority('auto_chat') || 6
};
// 5. 执行沟通指令
const result = await command.executeCommands(task.id, [chatCommand], this.mqttClient);
console.log(`[自动沟通] 完成 - 设备: ${sn_code}`);
return {
chatCount: result.chatCount || 0,
message: '沟通完成'
};
}
}
module.exports = ChatHandler;

View File

@@ -0,0 +1,410 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const jobFilterEngine = require('../services/jobFilterEngine');
const command = require('../core/command');
const config = require('../infrastructure/config');
const db = require('../../dbProxy');
const { jobFilterService } = require('../../job/services');
/**
* 自动投递处理器
* 负责职位搜索、过滤、评分和自动投递
*/
class DeliverHandler extends BaseHandler {
/**
* 处理自动投递任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handle(task) {
return await this.execute(task, async () => {
return await this.doDeliver(task);
}, {
checkAuth: true,
checkOnline: true,
recordDeviceMetrics: true
});
}
/**
* 执行投递逻辑
*/
async doDeliver(task) {
const { sn_code, taskParams } = task;
const { keyword, platform = 'boss', pageCount = 3, maxCount = 10, filterRules = {} } = taskParams;
console.log(`[自动投递] 开始 - 设备: ${sn_code}, 关键词: ${keyword}`);
// 1. 检查每日投递限制
const dailyCheck = await this.checkDailyDeliverLimit(sn_code, platform);
if (!dailyCheck.allowed) {
return {
deliveredCount: 0,
message: dailyCheck.message
};
}
const actualMaxCount = dailyCheck.actualMaxCount;
// 2. 检查并获取简历
const resume = await this.getOrRefreshResume(sn_code, platform, task.id);
if (!resume) {
return {
deliveredCount: 0,
message: '未找到简历信息'
};
}
// 3. 获取账户配置
const accountConfig = await this.getAccountConfig(sn_code, [
'keyword', 'platform_type', 'deliver_config', 'job_type_id', 'is_salary_priority'
]);
if (!accountConfig) {
return {
deliveredCount: 0,
message: '未找到账户配置'
};
}
// 4. 检查投递时间范围
const deliverConfig = ConfigManager.parseDeliverConfig(accountConfig.deliver_config);
const timeRange = ConfigManager.getTimeRange(deliverConfig);
if (timeRange) {
const timeRangeValidator = require('../services/timeRangeValidator');
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
if (!timeCheck.allowed) {
return {
deliveredCount: 0,
message: timeCheck.reason
};
}
}
// 5. 获取职位类型配置
const jobTypeConfig = await this.getJobTypeConfig(accountConfig.job_type_id);
// 6. 搜索职位列表
await this.searchJobs(sn_code, platform, keyword || accountConfig.keyword, pageCount, task.id);
// 7. 从数据库获取待投递职位
const pendingJobs = await this.getPendingJobs(sn_code, platform, actualMaxCount * 3);
if (!pendingJobs || pendingJobs.length === 0) {
return {
deliveredCount: 0,
message: '没有待投递的职位'
};
}
// 8. 合并过滤配置
const filterConfig = this.mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig);
// 9. 过滤已投递的公司
const recentCompanies = await this.getRecentDeliveredCompanies(sn_code, 30);
// 10. 过滤、评分、排序职位
const filteredJobs = await this.filterAndScoreJobs(
pendingJobs,
resume,
accountConfig,
jobTypeConfig,
filterConfig,
recentCompanies
);
const jobsToDeliver = filteredJobs.slice(0, actualMaxCount);
console.log(`[自动投递] 职位筛选完成 - 原始: ${pendingJobs.length}, 符合条件: ${filteredJobs.length}, 将投递: ${jobsToDeliver.length}`);
if (jobsToDeliver.length === 0) {
return {
deliveredCount: 0,
message: '没有符合条件的职位'
};
}
// 11. 创建投递指令并执行
const deliverCommands = this.createDeliverCommands(jobsToDeliver, sn_code, platform);
const result = await command.executeCommands(task.id, deliverCommands, this.mqttClient);
console.log(`[自动投递] 完成 - 设备: ${sn_code}, 投递: ${deliverCommands.length} 个职位`);
return {
deliveredCount: deliverCommands.length,
...result
};
}
/**
* 检查每日投递限制
*/
async checkDailyDeliverLimit(sn_code, platform) {
const apply_records = db.getModel('apply_records');
const dailyLimit = config.getDailyLimit('apply', platform);
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayApplyCount = await apply_records.count({
where: {
sn_code,
platform,
applyTime: {
[db.models.op.gte]: today
}
}
});
console.log(`[自动投递] 今日已投递: ${todayApplyCount}/${dailyLimit}`);
if (todayApplyCount >= dailyLimit) {
return {
allowed: false,
message: `已达到每日投递上限(${dailyLimit}次)`
};
}
const remainingQuota = dailyLimit - todayApplyCount;
return {
allowed: true,
actualMaxCount: remainingQuota,
todayCount: todayApplyCount,
limit: dailyLimit
};
}
/**
* 获取或刷新简历
*/
async getOrRefreshResume(sn_code, platform, taskId) {
const resume_info = db.getModel('resume_info');
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
let resume = await resume_info.findOne({
where: {
sn_code,
platform,
isActive: true
},
order: [['last_modify_time', 'DESC']]
});
const needRefresh = !resume ||
!resume.last_modify_time ||
new Date(resume.last_modify_time) < twoHoursAgo;
if (needRefresh) {
console.log(`[自动投递] 简历超过2小时未更新重新获取`);
try {
await command.executeCommands(taskId, [{
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code, platform }),
priority: config.getTaskPriority('get_resume') || 5
}], this.mqttClient);
// 重新查询
resume = await resume_info.findOne({
where: { sn_code, platform, isActive: true },
order: [['last_modify_time', 'DESC']]
});
} catch (error) {
console.warn(`[自动投递] 获取在线简历失败:`, error.message);
}
}
return resume ? resume.toJSON() : null;
}
/**
* 获取职位类型配置
*/
async getJobTypeConfig(jobTypeId) {
if (!jobTypeId) return null;
try {
const job_types = db.getModel('job_types');
const jobType = await job_types.findByPk(jobTypeId);
return jobType ? jobType.toJSON() : null;
} catch (error) {
console.error(`[自动投递] 获取职位类型配置失败:`, error);
return null;
}
}
/**
* 搜索职位列表
*/
async searchJobs(sn_code, platform, keyword, pageCount, taskId) {
const getJobListCommand = {
command_type: 'getJobList',
command_name: '获取职位列表',
command_params: JSON.stringify({
sn_code,
keyword,
platform,
pageCount
}),
priority: config.getTaskPriority('search_jobs') || 5
};
await command.executeCommands(taskId, [getJobListCommand], this.mqttClient);
}
/**
* 获取待投递职位
*/
async getPendingJobs(sn_code, platform, limit) {
const job_postings = db.getModel('job_postings');
const jobs = await job_postings.findAll({
where: {
sn_code,
platform,
applyStatus: 'pending'
},
order: [['create_time', 'DESC']],
limit
});
return jobs.map(job => job.toJSON ? job.toJSON() : job);
}
/**
* 合并过滤配置
*/
mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig) {
// 排除关键词
const jobTypeExclude = jobTypeConfig?.excludeKeywords
? ConfigManager.parseConfig(jobTypeConfig.excludeKeywords, [])
: [];
const deliverExclude = ConfigManager.getExcludeKeywords(deliverConfig);
const filterExclude = filterRules.excludeKeywords || [];
// 过滤关键词
const deliverFilter = ConfigManager.getFilterKeywords(deliverConfig);
const filterKeywords = filterRules.keywords || [];
// 薪资范围
const salaryRange = filterRules.minSalary || filterRules.maxSalary
? { min: filterRules.minSalary || 0, max: filterRules.maxSalary || 0 }
: ConfigManager.getSalaryRange(deliverConfig);
return {
exclude_keywords: [...jobTypeExclude, ...deliverExclude, ...filterExclude],
filter_keywords: filterKeywords.length > 0 ? filterKeywords : deliverFilter,
min_salary: salaryRange.min,
max_salary: salaryRange.max,
priority_weights: ConfigManager.getPriorityWeights(deliverConfig)
};
}
/**
* 获取近期已投递的公司
*/
async getRecentDeliveredCompanies(sn_code, days = 30) {
const apply_records = db.getModel('apply_records');
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - days);
const recentApplies = await apply_records.findAll({
where: {
sn_code,
applyTime: {
[db.models.op.gte]: daysAgo
}
},
attributes: ['companyName'],
group: ['companyName']
});
return new Set(recentApplies.map(apply => apply.companyName).filter(Boolean));
}
/**
* 过滤和评分职位
*/
async filterAndScoreJobs(jobs, resume, accountConfig, jobTypeConfig, filterConfig, recentCompanies) {
const scored = [];
for (const job of jobs) {
// 1. 过滤近期已投递的公司
if (job.companyName && recentCompanies.has(job.companyName)) {
console.log(`[自动投递] 跳过已投递公司: ${job.companyName}`);
continue;
}
// 2. 使用 jobFilterEngine 过滤和评分
const filtered = await jobFilterEngine.filterJobs([job], filterConfig, resume);
if (filtered.length === 0) {
continue; // 不符合过滤条件
}
// 3. 使用原有的评分系统job_filter_service计算详细分数
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
job,
resume,
accountConfig,
jobTypeConfig,
accountConfig.is_salary_priority || []
);
// 4. 计算关键词奖励
const KeywordMatcher = require('../utils/keywordMatcher');
const keywordBonus = KeywordMatcher.calculateBonus(
`${job.jobTitle} ${job.companyName} ${job.jobDescription || ''}`,
filterConfig.filter_keywords,
{ baseScore: 5, maxBonus: 20 }
);
const finalScore = scoreResult.totalScore + keywordBonus.score;
// 5. 只保留评分 >= 60 的职位
if (finalScore >= 60) {
scored.push({
...job,
matchScore: finalScore,
scoreDetails: {
...scoreResult.scores,
keywordBonus: keywordBonus.score
}
});
}
}
// 按评分降序排序
scored.sort((a, b) => b.matchScore - a.matchScore);
return scored;
}
/**
* 创建投递指令
*/
createDeliverCommands(jobs, sn_code, platform) {
return jobs.map(job => ({
command_type: 'deliver_resume',
command_name: `投递简历 - ${job.jobTitle} @ ${job.companyName} (评分:${job.matchScore})`,
command_params: JSON.stringify({
sn_code,
platform,
jobId: job.jobId,
encryptBossId: job.encryptBossId || '',
securityId: job.securityId || '',
brandName: job.companyName,
jobTitle: job.jobTitle,
companyName: job.companyName,
matchScore: job.matchScore,
scoreDetails: job.scoreDetails
}),
priority: config.getTaskPriority('apply') || 6
}));
}
}
module.exports = DeliverHandler;

View File

@@ -0,0 +1,18 @@
/**
* 处理器模块导出
* 统一导出所有任务处理器
*/
const BaseHandler = require('./baseHandler');
const SearchHandler = require('./searchHandler');
const DeliverHandler = require('./deliverHandler');
const ChatHandler = require('./chatHandler');
const ActiveHandler = require('./activeHandler');
module.exports = {
BaseHandler,
SearchHandler,
DeliverHandler,
ChatHandler,
ActiveHandler
};

View File

@@ -0,0 +1,87 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
/**
* 自动搜索处理器
* 负责搜索职位列表
*/
class SearchHandler extends BaseHandler {
/**
* 处理自动搜索任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handle(task) {
return await this.execute(task, async () => {
return await this.doSearch(task);
}, {
checkAuth: true,
checkOnline: true,
recordDeviceMetrics: true
});
}
/**
* 执行搜索逻辑
*/
async doSearch(task) {
const { sn_code, taskParams } = task;
const { keyword, platform = 'boss', pageCount = 3 } = taskParams;
console.log(`[自动搜索] 开始 - 设备: ${sn_code}, 关键词: ${keyword}`);
// 1. 获取账户配置
const accountConfig = await this.getAccountConfig(sn_code, ['keyword', 'platform_type', 'search_config']);
if (!accountConfig) {
return {
jobsFound: 0,
message: '未找到账户配置'
};
}
// 2. 解析搜索配置
const searchConfig = ConfigManager.parseSearchConfig(accountConfig.search_config);
// 3. 检查搜索时间范围
const timeRange = ConfigManager.getTimeRange(searchConfig);
if (timeRange) {
const timeRangeValidator = require('../services/timeRangeValidator');
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
if (!timeCheck.allowed) {
return {
jobsFound: 0,
message: timeCheck.reason
};
}
}
// 4. 创建搜索指令
const searchCommand = {
command_type: 'getJobList',
command_name: `自动搜索职位 - ${keyword || accountConfig.keyword}`,
command_params: JSON.stringify({
sn_code,
keyword: keyword || accountConfig.keyword || '',
platform: platform || accountConfig.platform_type || 'boss',
pageCount: pageCount || searchConfig.page_count || 3
}),
priority: config.getTaskPriority('search_jobs') || 8
};
// 5. 执行搜索指令
const result = await command.executeCommands(task.id, [searchCommand], this.mqttClient);
console.log(`[自动搜索] 完成 - 设备: ${sn_code}, 结果: ${JSON.stringify(result)}`);
return {
jobsFound: result.jobCount || 0,
message: '搜索完成'
};
}
}
module.exports = SearchHandler;

View File

@@ -1,17 +1,23 @@
const mqttManager = require("../mqtt/mqttManager.js"); const mqttManager = require("../mqtt/mqttManager.js");
// 导入调度模块(简化版) // 导入核心模块
const TaskQueue = require('./taskQueue.js'); const TaskQueue = require('./core/taskQueue.js');
const Command = require('./command.js'); const Command = require('./core/command.js');
const deviceManager = require('./deviceManager.js'); const deviceManager = require('./core/deviceManager.js');
const config = require('./config.js'); const ScheduledJobs = require('./core/scheduledJobs.js');
const utils = require('./utils.js');
// 导入新的模块 // 导入基础设施模块
const config = require('./infrastructure/config.js');
const utils = require('./utils/scheduleUtils.js');
// 导入任务处理器
const TaskHandlers = require('./taskHandlers.js'); const TaskHandlers = require('./taskHandlers.js');
// 导入MQTT模块
const MqttDispatcher = require('../mqtt/mqttDispatcher.js'); const MqttDispatcher = require('../mqtt/mqttDispatcher.js');
const ScheduledJobs = require('./scheduledJobs.js');
const DeviceWorkStatusNotifier = require('./deviceWorkStatusNotifier.js'); // 导入通知器
const DeviceWorkStatusNotifier = require('./notifiers/deviceWorkStatusNotifier.js');
/** /**
* 调度系统管理器 * 调度系统管理器
@@ -82,7 +88,7 @@ class ScheduleManager {
await deviceManager.init(); await deviceManager.init();
// 初始化任务队列 // 初始化任务队列
await TaskQueue.init?.(); await TaskQueue.init();
} }
/** /**
@@ -126,14 +132,7 @@ class ScheduleManager {
}); });
} }
/**
* 手动执行找工作流程已废弃full_flow 不再使用)
* @deprecated 请使用其他任务类型,如 auto_deliver
*/
async manualExecuteJobFlow(sn_code, keyword = '前端') {
console.warn(`[手动执行] manualExecuteJobFlow 已废弃full_flow 不再使用`);
throw new Error('full_flow 任务类型已废弃,请使用其他任务类型');
}
/** /**
* 获取系统状态 * 获取系统状态
@@ -178,28 +177,18 @@ class ScheduleManager {
} }
} }
// 创建调度管理器实例 // 创建并导出调度管理器实例
const scheduleManager = new ScheduleManager(); const scheduleManager = new ScheduleManager();
// 导出兼容接口,保持与原有代码的一致性 // 导出兼容接口(简化版)
module.exports = { module.exports = {
// 初始化方法
init: () => scheduleManager.init(), init: () => scheduleManager.init(),
// 手动执行任务
manualExecuteJobFlow: (sn_code, keyword) => scheduleManager.manualExecuteJobFlow(sn_code, keyword),
// 获取系统状态
getSystemStatus: () => scheduleManager.getSystemStatus(), getSystemStatus: () => scheduleManager.getSystemStatus(),
// 停止系统
stop: () => scheduleManager.stop(), stop: () => scheduleManager.stop(),
// 访问各个组件(为了兼容性 // 直接暴露属性(使用 getter 保持动态访问)
get mqttClient() { return scheduleManager.mqttClient; }, get mqttClient() { return scheduleManager.mqttClient; },
get isInitialized() { return scheduleManager.isInitialized; }, get isInitialized() { return scheduleManager.isInitialized; },
// 访问各个组件实例(简化版)
get taskQueue() { return TaskQueue; }, get taskQueue() { return TaskQueue; },
get command() { return Command; }, get command() { return Command; },
get deviceManager() { return deviceManager; } get deviceManager() { return deviceManager; }

View File

@@ -1,4 +1,4 @@
const db = require('../dbProxy'); const db = require('../../dbProxy');
/** /**
* 统一错误处理模块 * 统一错误处理模块

View File

@@ -23,6 +23,7 @@ class ScheduleConfig {
// 任务超时配置(毫秒) // 任务超时配置(毫秒)
this.taskTimeouts = { this.taskTimeouts = {
auto_search: 20 * 60 * 1000, // 自动搜索任务20分钟
auto_deliver: 30 * 60 * 1000, // 自动投递任务30分钟包含多个子任务 auto_deliver: 30 * 60 * 1000, // 自动投递任务30分钟包含多个子任务
auto_chat: 15 * 60 * 1000, // 自动沟通任务15分钟 auto_chat: 15 * 60 * 1000, // 自动沟通任务15分钟
auto_active_account: 10 * 60 * 1000 // 自动活跃账号任务10分钟 auto_active_account: 10 * 60 * 1000 // 自动活跃账号任务10分钟
@@ -30,6 +31,7 @@ class ScheduleConfig {
// 任务优先级配置 // 任务优先级配置
this.taskPriorities = { this.taskPriorities = {
auto_search: 8, // 自动搜索任务(最高优先级,先搜索后投递)
auto_deliver: 7, // 自动投递任务 auto_deliver: 7, // 自动投递任务
auto_chat: 6, // 自动沟通任务 auto_chat: 6, // 自动沟通任务
auto_active_account: 5, // 自动活跃账号任务 auto_active_account: 5, // 自动活跃账号任务
@@ -46,8 +48,10 @@ class ScheduleConfig {
this.schedules = { this.schedules = {
dailyReset: '0 0 * * *', // 每天凌晨重置统计 dailyReset: '0 0 * * *', // 每天凌晨重置统计
monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟 monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟
autoDeliver: '0 */1 * * * *', // 自动投递任务每1分钟执行一次 autoSearch: '0 0 */1 * * *', // 自动搜索任务每1小时执行一次
autoChat: '0 */15 * * * *' // 自动沟通任务:每15分钟执行一次 autoDeliver: '0 */2 * * * *', // 自动投递任务:每2分钟执行一次
autoChat: '0 */15 * * * *', // 自动沟通任务每15分钟执行一次
autoActive: '0 0 */2 * * *' // 自动活跃任务每2小时执行一次
}; };
} }

View File

@@ -0,0 +1,14 @@
/**
* Infrastructure 模块导出
* 统一导出基础设施模块
*/
const PriorityQueue = require('./PriorityQueue');
const ErrorHandler = require('./ErrorHandler');
const config = require('./config');
module.exports = {
PriorityQueue,
ErrorHandler,
config
};

View File

@@ -3,7 +3,7 @@
* 负责向客户端推送设备当前工作状态任务指令等 * 负责向客户端推送设备当前工作状态任务指令等
*/ */
const db = require('../dbProxy'); const db = require('../../dbProxy');
class DeviceWorkStatusNotifier { class DeviceWorkStatusNotifier {
constructor() { constructor() {

View File

@@ -0,0 +1,9 @@
/**
* Notifiers 模块导出
*/
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
module.exports = {
deviceWorkStatusNotifier
};

View File

@@ -1,779 +0,0 @@
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');
const db = require('../dbProxy');
const authorizationService = require('../../services/authorization_service.js');
const Framework = require("../../../framework/node-core-framework.js");
/**
* 检查当前时间是否在指定的时间范围内
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
* @returns {Object} {allowed: boolean, reason: string}
*/
function checkTimeRange(timeRange) {
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
return { allowed: true, reason: '未配置时间范围' };
}
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute; // 转换为分钟数
// 解析开始时间和结束时间
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
// 检查是否仅工作日(使用宽松比较,兼容字符串和数字)
if (timeRange.workdays_only == 1) { // 使用 == 而不是 ===
const dayOfWeek = now.getDay(); // 0=周日, 1=周一, ..., 6=周六
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
}
}
// 检查当前时间是否在时间范围内
if (startTime <= endTime) {
// 正常情况09:00 - 18:00
if (currentTime < startTime || currentTime >= endTime) {
return { allowed: false, reason: `当前时间 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` };
}
} else {
// 跨天情况22:00 - 06:00
if (currentTime < startTime && currentTime >= endTime) {
return { allowed: false, reason: `当前时间 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` };
}
}
return { allowed: true, reason: '在允许的时间范围内' };
}
/**
* 定时任务管理器(简化版)
* 管理所有定时任务的创建和销毁
*/
class ScheduledJobs {
constructor(components, taskHandlers) {
this.taskQueue = components.taskQueue;
this.taskHandlers = taskHandlers;
this.jobs = [];
}
/**
* 启动所有定时任务
*/
start() {
// 每天凌晨重置统计数据
const resetJob = node_schedule.scheduleJob(config.schedules.dailyReset, () => {
this.resetDailyStats();
});
this.jobs.push(resetJob);
// 启动心跳检查定时任务(每分钟检查一次)
const monitoringJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await deviceManager.checkHeartbeatStatus().catch(error => {
console.error('[定时任务] 检查心跳状态失败:', error);
});
});
this.jobs.push(monitoringJob);
// 启动离线设备任务清理定时任务(每分钟检查一次)
const cleanupOfflineTasksJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.cleanupOfflineDeviceTasks().catch(error => {
console.error('[定时任务] 清理离线设备任务失败:', error);
});
});
this.jobs.push(cleanupOfflineTasksJob);
console.log('[定时任务] 已启动离线设备任务清理任务');
// 启动任务超时检查定时任务(每分钟检查一次)
const timeoutCheckJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.checkTaskTimeouts().catch(error => {
console.error('[定时任务] 检查任务超时失败:', error);
});
});
this.jobs.push(timeoutCheckJob);
console.log('[定时任务] 已启动任务超时检查任务');
// 启动任务状态摘要同步定时任务每10秒发送一次
const taskSummaryJob = node_schedule.scheduleJob('*/10 * * * * *', async () => {
await this.syncTaskStatusSummary().catch(error => {
console.error('[定时任务] 同步任务状态摘要失败:', error);
});
});
this.jobs.push(taskSummaryJob);
console.log('[定时任务] 已启动任务状态摘要同步任务');
// 执行自动投递任务
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
this.autoDeliverTask();
});
// 立即执行一次自动投递任务
this.autoDeliverTask();
this.jobs.push(autoDeliverJob);
console.log('[定时任务] 已启动自动投递任务');
// 执行自动沟通任务
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */15 * * * *', () => {
this.autoChatTask();
});
// 立即执行一次自动沟通任务
this.autoChatTask();
this.jobs.push(autoChatJob);
console.log('[定时任务] 已启动自动沟通任务');
}
/**
* 重置每日统计
*/
resetDailyStats() {
console.log('[定时任务] 重置每日统计数据');
try {
deviceManager.resetAllDailyCounters();
console.log('[定时任务] 每日统计重置完成');
} catch (error) {
console.error('[定时任务] 重置统计失败:', error);
}
}
/**
* 清理过期数据
*/
cleanupCaches() {
console.log('[定时任务] 开始清理过期数据');
try {
deviceManager.cleanupOfflineDevices(config.monitoring.offlineThreshold);
command.cleanupExpiredCommands(30);
console.log('[定时任务] 数据清理完成');
} catch (error) {
console.error('[定时任务] 数据清理失败:', error);
}
}
/**
* 清理离线设备任务
* 检查离线超过10分钟的设备取消其所有pending/running状态的任务
*/
async cleanupOfflineDeviceTasks() {
try {
// 离线阈值10分钟
const offlineThreshold = 10 * 60 * 1000; // 10分钟
const now = Date.now();
const thresholdTime = now - offlineThreshold;
// 获取所有启用的账号
const pla_account = db.getModel('pla_account');
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 通过 deviceManager 检查哪些设备离线超过10分钟
const offlineSnCodes = [];
const offlineDevicesInfo = [];
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: null
});
} else {
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
if (lastHeartbeat < thresholdTime || !device.isOnline) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: lastHeartbeat ? new Date(lastHeartbeat) : null
});
}
}
}
if (offlineSnCodes.length === 0) {
return;
}
console.log(`[清理离线任务] 发现 ${offlineSnCodes.length} 个离线超过10分钟的设备: ${offlineSnCodes.join(', ')}`);
let totalCancelled = 0;
// 为每个离线设备取消任务
const task_status = db.getModel('task_status');
for (const sn_code of offlineSnCodes) {
try {
// 查询该设备的所有pending/running任务
const pendingTasks = await task_status.findAll({
where: {
sn_code: sn_code,
status: ['pending', 'running']
},
attributes: ['id']
});
if (pendingTasks.length === 0) {
continue;
}
const deviceInfo = offlineDevicesInfo.find(d => d.sn_code === sn_code);
// 更新任务状态为cancelled
const updateResult = await task_status.update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({
reason: '设备离线超过10分钟任务已自动取消',
offlineTime: deviceInfo?.lastHeartbeatTime
})
},
{
where: {
sn_code: sn_code,
status: ['pending', 'running']
}
}
);
const cancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
totalCancelled += cancelledCount;
// 从内存队列中移除任务
if (this.taskQueue && typeof this.taskQueue.cancelDeviceTasks === 'function') {
await this.taskQueue.cancelDeviceTasks(sn_code);
}
console.log(`[清理离线任务] 设备 ${sn_code} 已取消 ${cancelledCount} 个任务`);
} catch (error) {
console.error(`[清理离线任务] 取消设备 ${sn_code} 的任务失败:`, error);
}
}
if (totalCancelled > 0) {
console.log(`[清理离线任务] 共取消 ${totalCancelled} 个离线设备的任务`);
}
} catch (error) {
console.error('[清理离线任务] 执行失败:', error);
}
}
/**
* 同步任务状态摘要到客户端
* 定期向所有在线设备发送任务状态摘要(当前任务、待执行任务、下次执行时间等)
*/
async syncTaskStatusSummary() {
try {
const { pla_account } = await Framework.getModels();
// 获取所有启用的账号
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
// 为每个在线设备发送任务状态摘要
for (const account of accounts) {
const sn_code = account.sn_code;
// 检查设备是否在线
const device = deviceManager.devices.get(sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线,跳过
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
// 设备离线,跳过
continue;
}
// 设备在线,推送设备工作状态
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
const summary = await this.taskQueue.getTaskStatusSummary(sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, {
currentCommand: summary.currentCommand || null
});
} catch (error) {
console.error(`[设备工作状态同步] 设备 ${sn_code} 同步失败:`, error.message);
}
}
} catch (error) {
console.error('[任务状态同步] 执行失败:', error);
}
}
/**
* 检查任务超时并强制标记为失败
* 检测长时间运行的任务(可能是卡住的),强制标记为失败,释放资源
*/
async checkTaskTimeouts() {
try {
const Sequelize = require('sequelize');
const { task_status, op } = db.models;
// 查询所有运行中的任务
const runningTasks = await task_status.findAll({
where: {
status: 'running'
},
attributes: ['id', 'sn_code', 'taskType', 'taskName', 'startTime', 'create_time']
});
if (!runningTasks || runningTasks.length === 0) {
return;
}
const now = new Date();
let timeoutCount = 0;
for (const task of runningTasks) {
const taskData = task.toJSON();
const startTime = taskData.startTime ? new Date(taskData.startTime) : (taskData.create_time ? new Date(taskData.create_time) : null);
if (!startTime) {
continue;
}
// 获取任务类型的超时时间默认10分钟
const taskTimeout = config.getTaskTimeout(taskData.taskType) || 10 * 60 * 1000;
// 允许额外20%的缓冲时间
const maxAllowedTime = taskTimeout * 1.2;
const elapsedTime = now.getTime() - startTime.getTime();
// 如果任务运行时间超过最大允许时间,标记为超时失败
if (elapsedTime > maxAllowedTime) {
try {
await task_status.update(
{
status: 'failed',
endTime: now,
duration: elapsedTime,
result: JSON.stringify({
error: `任务执行超时(运行时间: ${Math.round(elapsedTime / 1000)}秒,超时限制: ${Math.round(maxAllowedTime / 1000)}秒)`,
timeout: true,
taskType: taskData.taskType,
startTime: startTime.toISOString()
}),
progress: 0
},
{
where: { id: taskData.id }
}
);
timeoutCount++;
console.warn(`[任务超时检查] 任务 ${taskData.id} (${taskData.taskName}) 运行时间过长,已强制标记为失败`, {
task_id: taskData.id,
sn_code: taskData.sn_code,
taskType: taskData.taskType,
elapsedTime: Math.round(elapsedTime / 1000) + '秒',
maxAllowedTime: Math.round(maxAllowedTime / 1000) + '秒'
});
// 如果任务队列中有这个任务,也需要从内存中清理
if (this.taskQueue && typeof this.taskQueue.deviceStatus !== 'undefined') {
const deviceStatus = this.taskQueue.deviceStatus.get(taskData.sn_code);
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.id === taskData.id) {
// 重置设备状态,允许继续执行下一个任务
deviceStatus.isRunning = false;
deviceStatus.currentTask = null;
deviceStatus.runningCount = Math.max(0, deviceStatus.runningCount - 1);
this.taskQueue.globalRunningCount = Math.max(0, this.taskQueue.globalRunningCount - 1);
console.log(`[任务超时检查] 已重置设备 ${taskData.sn_code} 的状态,可以继续执行下一个任务`);
// 尝试继续处理该设备的队列
setTimeout(() => {
this.taskQueue.processQueue(taskData.sn_code).catch(error => {
console.error(`[任务超时检查] 继续处理队列失败 (设备: ${taskData.sn_code}):`, error);
});
}, 100);
}
}
} catch (error) {
console.error(`[任务超时检查] 更新超时任务状态失败 (任务ID: ${taskData.id}):`, error);
}
}
}
if (timeoutCount > 0) {
console.log(`[任务超时检查] 共检测到 ${timeoutCount} 个超时任务,已强制标记为失败`);
}
} catch (error) {
console.error('[任务超时检查] 执行失败:', error);
}
}
/**
* 自动投递任务
*/
async autoDeliverTask() {
const now = new Date();
console.log(`[自动投递] ${now.toLocaleString()} 开始执行自动投递任务`);
try {
// 移除 device_status 依赖,改为直接从 pla_account 查询启用且开启自动投递的账号
const models = db.models;
const { pla_account, op } = models;
// 直接从 pla_account 查询启用且开启自动投递的账号
// 注意:不再检查在线状态,因为 device_status 已移除
const pla_users = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1, // 只获取启用的账号
auto_deliver: 1
}
});
if (!pla_users || pla_users.length === 0) {
console.log('[自动投递] 没有启用且开启自动投递的账号');
return;
}
console.log(`[自动投递] 找到 ${pla_users.length} 个可用账号`);
// 获取 task_status 模型用于查询上次投递时间
const { task_status } = models;
// 为每个设备添加自动投递任务到队列
for (const pl_user of pla_users) {
const userData = pl_user.toJSON();
// 检查设备是否在线离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const device = deviceManager.devices.get(userData.sn_code);
// 检查用户授权天数 是否够
const authorization = await authorizationService.checkAuthorization(userData.sn_code);
if (!authorization.is_authorized) {
console.log(`[自动投递] 设备 ${userData.sn_code} 授权天数不足,跳过添加任务`);
continue;
}
if (!device) {
// 设备从未发送过心跳,视为离线
console.log(`[自动投递] 设备 ${userData.sn_code} 离线(从未发送心跳),跳过添加任务`);
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
console.log(`[自动投递] 设备 ${userData.sn_code} 离线(最后心跳: ${offlineMinutes}分钟前),跳过添加任务`);
continue;
}
// 检查设备调度策略
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'deliver');
if (!canExecute.allowed) {
console.log(`[自动投递] 设备 ${userData.sn_code} 不满足执行条件: ${canExecute.reason}`);
continue;
}
// 获取投递配置,如果不存在则使用默认值
let deliver_config = userData.deliver_config;
if (typeof deliver_config === 'string') {
try {
deliver_config = JSON.parse(deliver_config);
} catch (e) {
deliver_config = {};
}
}
deliver_config = deliver_config || {
deliver_interval: 30,
min_salary: 0,
max_salary: 0,
page_count: 3,
max_deliver: 10,
filter_keywords: [],
exclude_keywords: []
};
// 检查投递时间范围
if (deliver_config.time_range) {
const timeCheck = checkTimeRange(deliver_config.time_range);
if (!timeCheck.allowed) {
console.log(`[自动投递] 设备 ${userData.sn_code} ${timeCheck.reason}`);
continue;
}
}
// 检查投递间隔时间
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 = new Date().getTime() - lastDeliverTime.getTime();
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
const message = `距离上次投递仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${deliver_interval} 分钟)`;
console.log(`[自动投递] 设备 ${userData.sn_code} ${message}`);
// 推送等待状态到客户端
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
// 获取当前任务状态摘要
const taskStatusSummary = this.taskQueue ? this.taskQueue.getTaskStatusSummary(userData.sn_code) : {
sn_code: userData.sn_code,
pendingCount: 0,
totalPendingCount: 0,
pendingTasks: []
};
// 添加等待消息到工作状态
await deviceWorkStatusNotifier.sendDeviceWorkStatus(userData.sn_code, taskStatusSummary, {
waitingMessage: {
type: 'deliver_interval',
message: message,
remainingMinutes: remainingMinutes,
nextDeliverTime: new Date(lastDeliverTime.getTime() + interval_ms).toISOString()
}
});
} catch (pushError) {
console.warn(`[自动投递] 推送等待消息失败:`, pushError.message);
}
continue;
}
}
// 添加自动投递任务到队列
await this.taskQueue.addTask(userData.sn_code, {
taskType: 'auto_deliver',
taskName: `自动投递 - ${userData.keyword || ''}`,
taskParams: {
keyword: userData.keyword || '',
platform: userData.platform_type || 'boss',
pageCount: deliver_config.page_count || 3,
maxCount: deliver_config.max_deliver || 10,
filterRules: {
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 || '默认'},投递间隔: ${deliver_interval} 分钟`);
}
console.log('[自动投递] 任务添加完成');
} catch (error) {
console.error('[自动投递] 执行失败:', error);
}
}
/**
* 自动沟通任务
*/
async autoChatTask() {
const now = new Date();
console.log(`[自动沟通] ${now.toLocaleString()} 开始执行自动沟通任务`);
try {
// 移除 device_status 依赖,改为直接从 pla_account 查询启用且开启自动沟通的账号
const models = db.models;
const { pla_account, op } = models;
// 直接从 pla_account 查询启用且开启自动沟通的账号
// 注意:不再检查在线状态,因为 device_status 已移除
const pla_users = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1, // 只获取启用的账号
auto_chat: 1
}
});
if (!pla_users || pla_users.length === 0) {
console.log('[自动沟通] 没有启用且开启自动沟通的账号');
return;
}
console.log(`[自动沟通] 找到 ${pla_users.length} 个可用账号`);
// 获取 task_status 模型用于查询上次沟通时间
const { task_status } = models;
// 为每个设备添加自动沟通任务到队列
for (const pl_user of pla_users) {
const userData = pl_user.toJSON();
// 检查设备是否在线离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const device = deviceManager.devices.get(userData.sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线
console.log(`[自动沟通] 设备 ${userData.sn_code} 离线(从未发送心跳),跳过添加任务`);
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
console.log(`[自动沟通] 设备 ${userData.sn_code} 离线(最后心跳: ${offlineMinutes}分钟前),跳过添加任务`);
continue;
}
// 检查设备调度策略
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'chat');
if (!canExecute.allowed) {
console.log(`[自动沟通] 设备 ${userData.sn_code} 不满足执行条件: ${canExecute.reason}`);
continue;
}
// 获取沟通策略配置
let chatStrategy = {};
if (userData.chat_strategy) {
chatStrategy = typeof userData.chat_strategy === 'string'
? JSON.parse(userData.chat_strategy)
: userData.chat_strategy;
}
// 检查沟通时间范围
if (chatStrategy.time_range) {
const timeCheck = checkTimeRange(chatStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动沟通] 设备 ${userData.sn_code} ${timeCheck.reason}`);
continue;
}
}
// 检查沟通间隔时间
const chat_interval = chatStrategy.chat_interval || 30; // 默认30分钟
const interval_ms = chat_interval * 60 * 1000; // 转换为毫秒
// 查询该账号最近一次成功完成的自动沟通任务
const lastChatTask = await task_status.findOne({
where: {
sn_code: userData.sn_code,
taskType: 'auto_chat',
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
// 如果存在上次沟通记录,检查是否已经过了间隔时间
if (lastChatTask && lastChatTask.endTime) {
const lastChatTime = new Date(lastChatTask.endTime);
const elapsedTime = now.getTime() - lastChatTime.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} 分钟(间隔: ${chat_interval} 分钟)`);
continue;
}
}
// 添加自动沟通任务到队列
await this.taskQueue.addTask(userData.sn_code, {
taskType: 'auto_chat',
taskName: `自动沟通 - ${userData.name || '默认'}`,
taskParams: {
platform: userData.platform_type || 'boss'
},
priority: config.getTaskPriority('auto_chat') || 6
});
console.log(`[自动沟通] 已为设备 ${userData.sn_code} 添加自动沟通任务,沟通间隔: ${chat_interval} 分钟`);
}
console.log('[自动沟通] 任务添加完成');
} catch (error) {
console.error('[自动沟通] 执行失败:', error);
}
}
}
module.exports = ScheduledJobs;

View File

@@ -0,0 +1,199 @@
const db = require('../../dbProxy');
const authorizationService = require('../../../services/authorization_service');
const deviceManager = require('../core/deviceManager');
/**
* 账户验证服务
* 统一处理账户启用状态、授权状态、在线状态的检查
*/
class AccountValidator {
/**
* 检查账户是否启用
* @param {string} sn_code - 设备序列号
* @returns {Promise<{enabled: boolean, reason?: string}>}
*/
async checkEnabled(sn_code) {
try {
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: { sn_code, is_delete: 0 },
attributes: ['is_enabled', 'name']
});
if (!account) {
return { enabled: false, reason: '账户不存在' };
}
if (!account.is_enabled) {
return { enabled: false, reason: '账户未启用' };
}
return { enabled: true };
} catch (error) {
console.error(`[账户验证] 检查启用状态失败 (${sn_code}):`, error);
return { enabled: false, reason: '检查失败' };
}
}
/**
* 检查账户授权状态
* @param {string} sn_code - 设备序列号
* @returns {Promise<{authorized: boolean, days?: number, reason?: string}>}
*/
async checkAuthorization(sn_code) {
try {
const result = await authorizationService.checkAuthorization(sn_code);
if (!result.is_authorized) {
return {
authorized: false,
days: result.days_remaining || 0,
reason: result.message || '授权已过期'
};
}
return {
authorized: true,
days: result.days_remaining
};
} catch (error) {
console.error(`[账户验证] 检查授权状态失败 (${sn_code}):`, error);
return { authorized: false, reason: '授权检查失败' };
}
}
/**
* 检查设备是否在线
* @param {string} sn_code - 设备序列号
* @param {number} offlineThreshold - 离线阈值(毫秒)
* @returns {{online: boolean, lastHeartbeat?: number, reason?: string}}
*/
checkOnline(sn_code, offlineThreshold = 3 * 60 * 1000) {
const device = deviceManager.devices.get(sn_code);
if (!device) {
return { online: false, reason: '设备从未发送心跳' };
}
const now = Date.now();
const lastHeartbeat = device.lastHeartbeat || 0;
const elapsed = now - lastHeartbeat;
if (elapsed > offlineThreshold) {
const minutes = Math.round(elapsed / (60 * 1000));
return {
online: false,
lastHeartbeat,
reason: `设备离线(最后心跳: ${minutes}分钟前)`
};
}
if (!device.isOnline) {
return { online: false, lastHeartbeat, reason: '设备标记为离线' };
}
return { online: true, lastHeartbeat };
}
/**
* 综合验证(启用 + 授权 + 在线)
* @param {string} sn_code - 设备序列号
* @param {object} options - 验证选项
* @param {boolean} options.checkEnabled - 是否检查启用状态(默认 true
* @param {boolean} options.checkAuth - 是否检查授权(默认 true
* @param {boolean} options.checkOnline - 是否检查在线(默认 true
* @param {number} options.offlineThreshold - 离线阈值(默认 3分钟
* @returns {Promise<{valid: boolean, reason?: string, details?: object}>}
*/
async validate(sn_code, options = {}) {
const {
checkEnabled = true,
checkAuth = true,
checkOnline = true,
offlineThreshold = 3 * 60 * 1000
} = options;
const details = {};
// 检查启用状态
if (checkEnabled) {
const enabledResult = await this.checkEnabled(sn_code);
details.enabled = enabledResult;
if (!enabledResult.enabled) {
return {
valid: false,
reason: enabledResult.reason,
details
};
}
}
// 检查授权状态
if (checkAuth) {
const authResult = await this.checkAuthorization(sn_code);
details.authorization = authResult;
if (!authResult.authorized) {
return {
valid: false,
reason: authResult.reason,
details
};
}
}
// 检查在线状态
if (checkOnline) {
const onlineResult = this.checkOnline(sn_code, offlineThreshold);
details.online = onlineResult;
if (!onlineResult.online) {
return {
valid: false,
reason: onlineResult.reason,
details
};
}
}
return { valid: true, details };
}
/**
* 批量验证多个账户
* @param {string[]} sn_codes - 设备序列号数组
* @param {object} options - 验证选项
* @returns {Promise<{valid: string[], invalid: Array<{sn_code: string, reason: string}>}>}
*/
async validateBatch(sn_codes, options = {}) {
const valid = [];
const invalid = [];
for (const sn_code of sn_codes) {
const result = await this.validate(sn_code, options);
if (result.valid) {
valid.push(sn_code);
} else {
invalid.push({ sn_code, reason: result.reason });
}
}
return { valid, invalid };
}
/**
* 检查账户是否已登录(通过心跳数据)
* @param {string} sn_code - 设备序列号
* @returns {boolean}
*/
checkLoggedIn(sn_code) {
const device = deviceManager.devices.get(sn_code);
return device?.isLoggedIn || false;
}
}
// 导出单例
const accountValidator = new AccountValidator();
module.exports = accountValidator;

View File

@@ -0,0 +1,225 @@
/**
* 配置管理服务
* 统一处理账户配置的解析和验证
*/
class ConfigManager {
/**
* 解析 JSON 配置字符串
* @param {string|object} config - 配置字符串或对象
* @param {object} defaultValue - 默认值
* @returns {object} 解析后的配置对象
*/
static parseConfig(config, defaultValue = {}) {
if (!config) {
return defaultValue;
}
if (typeof config === 'object') {
return { ...defaultValue, ...config };
}
if (typeof config === 'string') {
try {
const parsed = JSON.parse(config);
return { ...defaultValue, ...parsed };
} catch (error) {
console.warn('[配置管理] JSON 解析失败:', error.message);
return defaultValue;
}
}
return defaultValue;
}
/**
* 解析投递配置
* @param {string|object} deliverConfig - 投递配置
* @returns {object} 标准化的投递配置
*/
static parseDeliverConfig(deliverConfig) {
const defaultConfig = {
deliver_interval: 30, // 投递间隔(分钟)
min_salary: 0, // 最低薪资
max_salary: 0, // 最高薪资
page_count: 3, // 搜索页数
max_deliver: 10, // 最大投递数
filter_keywords: [], // 过滤关键词
exclude_keywords: [], // 排除关键词
time_range: null, // 时间范围
priority_weights: null // 优先级权重
};
return this.parseConfig(deliverConfig, defaultConfig);
}
/**
* 解析搜索配置
* @param {string|object} searchConfig - 搜索配置
* @returns {object} 标准化的搜索配置
*/
static parseSearchConfig(searchConfig) {
const defaultConfig = {
search_interval: 60, // 搜索间隔(分钟)
page_count: 3, // 搜索页数
keywords: [], // 搜索关键词
exclude_keywords: [], // 排除关键词
time_range: null // 时间范围
};
return this.parseConfig(searchConfig, defaultConfig);
}
/**
* 解析沟通配置
* @param {string|object} chatStrategy - 沟通策略
* @returns {object} 标准化的沟通配置
*/
static parseChatStrategy(chatStrategy) {
const defaultConfig = {
chat_interval: 30, // 沟通间隔(分钟)
auto_reply: false, // 是否自动回复
reply_template: '', // 回复模板
time_range: null // 时间范围
};
return this.parseConfig(chatStrategy, defaultConfig);
}
/**
* 解析活跃配置
* @param {string|object} activeStrategy - 活跃策略
* @returns {object} 标准化的活跃配置
*/
static parseActiveStrategy(activeStrategy) {
const defaultConfig = {
active_interval: 120, // 活跃间隔(分钟)
actions: ['view_jobs'], // 活跃动作
time_range: null // 时间范围
};
return this.parseConfig(activeStrategy, defaultConfig);
}
/**
* 获取优先级权重配置
* @param {object} config - 投递配置
* @returns {object} 优先级权重
*/
static getPriorityWeights(config) {
const defaultWeights = {
salary: 0.4, // 薪资匹配度
keyword: 0.3, // 关键词匹配度
company: 0.2, // 公司活跃度
distance: 0.1 // 距离(未来)
};
if (!config.priority_weights) {
return defaultWeights;
}
return { ...defaultWeights, ...config.priority_weights };
}
/**
* 获取排除关键词列表
* @param {object} config - 配置对象
* @returns {string[]} 排除关键词数组
*/
static getExcludeKeywords(config) {
if (!config.exclude_keywords) {
return [];
}
if (Array.isArray(config.exclude_keywords)) {
return config.exclude_keywords.filter(k => k && k.trim());
}
if (typeof config.exclude_keywords === 'string') {
return config.exclude_keywords
.split(/[,,、]/)
.map(k => k.trim())
.filter(k => k);
}
return [];
}
/**
* 获取过滤关键词列表
* @param {object} config - 配置对象
* @returns {string[]} 过滤关键词数组
*/
static getFilterKeywords(config) {
if (!config.filter_keywords) {
return [];
}
if (Array.isArray(config.filter_keywords)) {
return config.filter_keywords.filter(k => k && k.trim());
}
if (typeof config.filter_keywords === 'string') {
return config.filter_keywords
.split(/[,,、]/)
.map(k => k.trim())
.filter(k => k);
}
return [];
}
/**
* 获取薪资范围
* @param {object} config - 配置对象
* @returns {{min: number, max: number}} 薪资范围
*/
static getSalaryRange(config) {
return {
min: parseInt(config.min_salary) || 0,
max: parseInt(config.max_salary) || 0
};
}
/**
* 获取时间范围
* @param {object} config - 配置对象
* @returns {object|null} 时间范围配置
*/
static getTimeRange(config) {
return config.time_range || null;
}
/**
* 验证配置完整性
* @param {object} config - 配置对象
* @param {string[]} requiredFields - 必需字段
* @returns {{valid: boolean, missing?: string[]}} 验证结果
*/
static validateConfig(config, requiredFields = []) {
const missing = [];
for (const field of requiredFields) {
if (config[field] === undefined || config[field] === null) {
missing.push(field);
}
}
if (missing.length > 0) {
return { valid: false, missing };
}
return { valid: true };
}
/**
* 合并配置(用于覆盖默认配置)
* @param {object} defaultConfig - 默认配置
* @param {object} userConfig - 用户配置
* @returns {object} 合并后的配置
*/
static mergeConfig(defaultConfig, userConfig) {
return { ...defaultConfig, ...userConfig };
}
}
module.exports = ConfigManager;

View File

@@ -0,0 +1,395 @@
const SalaryParser = require('../utils/salaryParser');
const KeywordMatcher = require('../utils/keywordMatcher');
const db = require('../../dbProxy');
/**
* 职位过滤引擎
* 综合处理职位的过滤、评分和排序
*/
class JobFilterEngine {
/**
* 过滤职位列表
* @param {Array} jobs - 职位列表
* @param {object} config - 过滤配置
* @param {object} resumeInfo - 简历信息
* @returns {Promise<Array>} 过滤后的职位列表
*/
async filterJobs(jobs, config, resumeInfo = {}) {
if (!jobs || jobs.length === 0) {
return [];
}
let filtered = [...jobs];
// 1. 薪资过滤
filtered = this.filterBySalary(filtered, config);
// 2. 关键词过滤
filtered = this.filterByKeywords(filtered, config);
// 3. 公司活跃度过滤
if (config.filter_inactive_companies) {
filtered = await this.filterByCompanyActivity(filtered, config.company_active_days || 7);
}
// 4. 去重(同一公司、同一职位名称)
if (config.deduplicate) {
filtered = this.deduplicateJobs(filtered);
}
return filtered;
}
/**
* 按薪资过滤
* @param {Array} jobs - 职位列表
* @param {object} config - 配置
* @returns {Array} 过滤后的职位
*/
filterBySalary(jobs, config) {
const { min_salary = 0, max_salary = 0 } = config;
if (min_salary === 0 && max_salary === 0) {
return jobs; // 无薪资限制
}
return jobs.filter(job => {
const jobSalary = SalaryParser.parse(job.salary || job.salaryDesc || '');
return SalaryParser.isWithinRange(jobSalary, min_salary, max_salary);
});
}
/**
* 按关键词过滤
* @param {Array} jobs - 职位列表
* @param {object} config - 配置
* @returns {Array} 过滤后的职位
*/
filterByKeywords(jobs, config) {
const {
exclude_keywords = [],
filter_keywords = []
} = config;
if (exclude_keywords.length === 0 && filter_keywords.length === 0) {
return jobs;
}
return KeywordMatcher.filterJobs(jobs, {
excludeKeywords: exclude_keywords,
filterKeywords: filter_keywords
}, (job) => {
// 组合职位名称、描述、技能要求等
return [
job.name || job.jobName || '',
job.description || job.jobDescription || '',
job.skills || '',
job.welfare || ''
].join(' ');
});
}
/**
* 按公司活跃度过滤
* @param {Array} jobs - 职位列表
* @param {number} activeDays - 活跃天数阈值
* @returns {Promise<Array>} 过滤后的职位
*/
async filterByCompanyActivity(jobs, activeDays = 7) {
try {
const task_status = db.getModel('task_status');
const thresholdDate = new Date(Date.now() - activeDays * 24 * 60 * 60 * 1000);
// 查询近期已投递的公司
const recentCompanies = await task_status.findAll({
where: {
taskType: 'auto_deliver',
status: 'completed',
endTime: {
[db.models.op.gte]: thresholdDate
}
},
attributes: ['result'],
raw: true
});
// 提取公司名称
const deliveredCompanies = new Set();
for (const task of recentCompanies) {
try {
const result = JSON.parse(task.result || '{}');
if (result.deliveredJobs) {
result.deliveredJobs.forEach(job => {
if (job.company) {
deliveredCompanies.add(job.company.toLowerCase());
}
});
}
} catch (e) {
// 忽略解析错误
}
}
// 过滤掉近期已投递的公司
return jobs.filter(job => {
const company = (job.company || job.companyName || '').toLowerCase().trim();
return !deliveredCompanies.has(company);
});
} catch (error) {
console.error('[职位过滤] 公司活跃度过滤失败:', error);
return jobs; // 失败时返回原列表
}
}
/**
* 去重职位
* @param {Array} jobs - 职位列表
* @returns {Array} 去重后的职位
*/
deduplicateJobs(jobs) {
const seen = new Set();
const unique = [];
for (const job of jobs) {
const company = (job.company || job.companyName || '').toLowerCase().trim();
const jobName = (job.name || job.jobName || '').toLowerCase().trim();
const key = `${company}||${jobName}`;
if (!seen.has(key)) {
seen.add(key);
unique.push(job);
}
}
return unique;
}
/**
* 为职位打分
* @param {Array} jobs - 职位列表
* @param {object} resumeInfo - 简历信息
* @param {object} config - 配置(包含权重)
* @returns {Array} 带分数的职位列表
*/
scoreJobs(jobs, resumeInfo = {}, config = {}) {
const weights = config.priority_weights || {
salary: 0.4,
keyword: 0.3,
company: 0.2,
freshness: 0.1
};
return jobs.map(job => {
const scores = {
salary: this.scoreSalary(job, resumeInfo),
keyword: this.scoreKeywords(job, config),
company: this.scoreCompany(job),
freshness: this.scoreFreshness(job)
};
// 加权总分
const totalScore = (
scores.salary * weights.salary +
scores.keyword * weights.keyword +
scores.company * weights.company +
scores.freshness * weights.freshness
);
return {
...job,
_scores: scores,
_totalScore: totalScore
};
});
}
/**
* 薪资匹配度评分 (0-100)
* @param {object} job - 职位信息
* @param {object} resumeInfo - 简历信息
* @returns {number} 分数
*/
scoreSalary(job, resumeInfo) {
const jobSalary = SalaryParser.parse(job.salary || job.salaryDesc || '');
const expectedSalary = SalaryParser.parse(resumeInfo.expected_salary || '');
if (jobSalary.min === 0 || expectedSalary.min === 0) {
return 50; // 无法判断时返回中性分
}
const matchScore = SalaryParser.calculateMatch(jobSalary, expectedSalary);
return matchScore * 100;
}
/**
* 关键词匹配度评分 (0-100)
* @param {object} job - 职位信息
* @param {object} config - 配置
* @returns {number} 分数
*/
scoreKeywords(job, config) {
const bonusKeywords = config.filter_keywords || [];
if (bonusKeywords.length === 0) {
return 50; // 无关键词时返回中性分
}
const jobText = [
job.name || job.jobName || '',
job.description || job.jobDescription || '',
job.skills || ''
].join(' ');
const bonusResult = KeywordMatcher.calculateBonus(jobText, bonusKeywords, {
baseScore: 10,
maxBonus: 100
});
return Math.min(bonusResult.score, 100);
}
/**
* 公司评分 (0-100)
* @param {object} job - 职位信息
* @returns {number} 分数
*/
scoreCompany(job) {
let score = 50; // 基础分
// 融资阶段加分
const fundingStage = (job.financingStage || job.financing || '').toLowerCase();
const fundingBonus = {
'已上市': 30,
'上市公司': 30,
'd轮': 25,
'c轮': 20,
'b轮': 15,
'a轮': 10,
'天使轮': 5
};
for (const [stage, bonus] of Object.entries(fundingBonus)) {
if (fundingStage.includes(stage.toLowerCase())) {
score += bonus;
break;
}
}
// 公司规模加分
const scale = (job.scale || job.companyScale || '').toLowerCase();
if (scale.includes('10000') || scale.includes('万人')) {
score += 15;
} else if (scale.includes('1000-9999') || scale.includes('千人')) {
score += 10;
} else if (scale.includes('500-999')) {
score += 5;
}
return Math.min(score, 100);
}
/**
* 新鲜度评分 (0-100)
* @param {object} job - 职位信息
* @returns {number} 分数
*/
scoreFreshness(job) {
const publishTime = job.publishTime || job.createTime;
if (!publishTime) {
return 50; // 无时间信息时返回中性分
}
try {
const now = Date.now();
const pubTime = new Date(publishTime).getTime();
const hoursAgo = (now - pubTime) / (1000 * 60 * 60);
// 越新鲜分数越高
if (hoursAgo < 1) return 100;
if (hoursAgo < 24) return 90;
if (hoursAgo < 72) return 70;
if (hoursAgo < 168) return 50; // 一周内
return 30;
} catch (error) {
return 50;
}
}
/**
* 排序职位
* @param {Array} jobs - 职位列表(带分数)
* @param {string} sortBy - 排序方式: score, salary, freshness
* @returns {Array} 排序后的职位
*/
sortJobs(jobs, sortBy = 'score') {
const sorted = [...jobs];
switch (sortBy) {
case 'score':
sorted.sort((a, b) => (b._totalScore || 0) - (a._totalScore || 0));
break;
case 'salary':
sorted.sort((a, b) => {
const salaryA = SalaryParser.parse(a.salary || '');
const salaryB = SalaryParser.parse(b.salary || '');
return (salaryB.max || 0) - (salaryA.max || 0);
});
break;
case 'freshness':
sorted.sort((a, b) => {
const timeA = new Date(a.publishTime || a.createTime || 0).getTime();
const timeB = new Date(b.publishTime || b.createTime || 0).getTime();
return timeB - timeA;
});
break;
default:
// 默认按分数排序
sorted.sort((a, b) => (b._totalScore || 0) - (a._totalScore || 0));
}
return sorted;
}
/**
* 综合处理:过滤 + 评分 + 排序
* @param {Array} jobs - 职位列表
* @param {object} config - 过滤配置
* @param {object} resumeInfo - 简历信息
* @param {object} options - 选项
* @returns {Promise<Array>} 处理后的职位列表
*/
async process(jobs, config, resumeInfo = {}, options = {}) {
const {
maxCount = 10, // 最大返回数量
sortBy = 'score' // 排序方式
} = options;
// 1. 过滤
let filtered = await this.filterJobs(jobs, config, resumeInfo);
console.log(`[职位过滤] 原始: ${jobs.length} 个,过滤后: ${filtered.length}`);
// 2. 评分
const scored = this.scoreJobs(filtered, resumeInfo, config);
// 3. 排序
const sorted = this.sortJobs(scored, sortBy);
// 4. 截取
const result = sorted.slice(0, maxCount);
console.log(`[职位过滤] 最终返回: ${result.length} 个职位`);
return result;
}
}
// 导出单例
const jobFilterEngine = new JobFilterEngine();
module.exports = jobFilterEngine;

View File

@@ -0,0 +1,158 @@
/**
* 时间范围验证器
* 检查当前时间是否在指定的时间范围内(支持工作日限制)
*/
class TimeRangeValidator {
/**
* 检查当前时间是否在指定的时间范围内
* @param {object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
* @returns {{allowed: boolean, reason: string}} 检查结果
*/
static checkTimeRange(timeRange) {
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
return { allowed: true, reason: '未配置时间范围' };
}
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute; // 转换为分钟数
// 解析开始时间和结束时间
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
// 检查是否仅工作日(使用宽松比较,兼容字符串和数字)
if (timeRange.workdays_only == 1) {
const dayOfWeek = now.getDay(); // 0=周日, 1=周一, ..., 6=周六
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
}
}
// 检查当前时间是否在时间范围内
if (startTime <= endTime) {
// 正常情况09:00 - 18:00
if (currentTime < startTime || currentTime >= endTime) {
const currentTimeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
return {
allowed: false,
reason: `当前时间 ${currentTimeStr} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
} else {
// 跨天情况22:00 - 06:00
if (currentTime < startTime && currentTime >= endTime) {
const currentTimeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
return {
allowed: false,
reason: `当前时间 ${currentTimeStr} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
}
return { allowed: true, reason: '在允许的时间范围内' };
}
/**
* 检查是否在工作时间内
* @param {string} startTime - 开始时间 '09:00'
* @param {string} endTime - 结束时间 '18:00'
* @returns {boolean}
*/
static isWithinWorkingHours(startTime = '09:00', endTime = '18:00') {
const result = this.checkTimeRange({
start_time: startTime,
end_time: endTime,
workdays_only: 0
});
return result.allowed;
}
/**
* 检查是否是工作日
* @returns {boolean}
*/
static isWorkingDay() {
const dayOfWeek = new Date().getDay();
return dayOfWeek !== 0 && dayOfWeek !== 6; // 非周六周日
}
/**
* 获取下一个可操作时间
* @param {object} timeRange - 时间范围配置
* @returns {Date|null} 下一个可操作时间,如果当前可操作则返回 null
*/
static getNextAvailableTime(timeRange) {
const check = this.checkTimeRange(timeRange);
if (check.allowed) {
return null; // 当前可操作
}
if (!timeRange || !timeRange.start_time) {
return null;
}
const now = new Date();
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
// 如果是工作日限制且当前是周末
if (timeRange.workdays_only == 1) {
const dayOfWeek = now.getDay();
if (dayOfWeek === 0) {
// 周日,下一个可操作时间是周一
const nextTime = new Date(now);
nextTime.setDate(now.getDate() + 1);
nextTime.setHours(startHour, startMinute, 0, 0);
return nextTime;
} else if (dayOfWeek === 6) {
// 周六,下一个可操作时间是下周一
const nextTime = new Date(now);
nextTime.setDate(now.getDate() + 2);
nextTime.setHours(startHour, startMinute, 0, 0);
return nextTime;
}
}
// 计算下一个开始时间
const nextTime = new Date(now);
nextTime.setHours(startHour, startMinute, 0, 0);
// 如果已经过了今天的开始时间,则设置为明天
if (nextTime <= now) {
nextTime.setDate(now.getDate() + 1);
}
return nextTime;
}
/**
* 格式化剩余时间
* @param {object} timeRange - 时间范围配置
* @returns {string} 剩余时间描述
*/
static formatRemainingTime(timeRange) {
const nextTime = this.getNextAvailableTime(timeRange);
if (!nextTime) {
return '当前可操作';
}
const now = Date.now();
const diff = nextTime.getTime() - now;
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 24) {
const days = Math.floor(hours / 24);
return `需要等待 ${days}${hours % 24} 小时`;
} else if (hours > 0) {
return `需要等待 ${hours} 小时 ${minutes} 分钟`;
} else {
return `需要等待 ${minutes} 分钟`;
}
}
}
module.exports = TimeRangeValidator;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,182 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
/**
* 自动活跃账号任务
* 定期浏览职位、刷新简历、查看通知等,保持账号活跃度
*/
class AutoActiveTask extends BaseTask {
constructor() {
super('auto_active_account', {
defaultInterval: 120, // 默认2小时
defaultPriority: 5, // 较低优先级
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_deliver', // 投递任务
'auto_search' // 搜索任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.platform) {
return {
valid: false,
reason: '缺少必要参数: platform'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动活跃账号 - ${params.platform || 'boss'}`;
}
/**
* 执行自动活跃任务
*/
async execute(sn_code, params) {
console.log(`[自动活跃] 设备 ${sn_code} 开始执行活跃任务`);
const actions = [];
// 1. 浏览推荐职位
actions.push({
action: 'browse_jobs',
count: Math.floor(Math.random() * 5) + 3 // 3-7个职位
});
// 2. 刷新简历
actions.push({
action: 'refresh_resume',
success: true
});
// 3. 查看通知
actions.push({
action: 'check_notifications',
count: Math.floor(Math.random() * 3)
});
// 4. 浏览公司主页
actions.push({
action: 'browse_companies',
count: Math.floor(Math.random() * 3) + 1
});
console.log(`[自动活跃] 设备 ${sn_code} 完成 ${actions.length} 个活跃操作`);
return {
success: true,
actions: actions,
message: `完成 ${actions.length} 个活跃操作`
};
}
/**
* 添加活跃任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动活跃] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
if (!account) {
console.log(`[自动活跃] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
const accountData = account.toJSON();
// 2. 检查是否开启了自动活跃
if (!accountData.auto_active) {
console.log(`[自动活跃] 设备 ${sn_code} 未开启自动活跃`);
return { success: false, reason: '未开启自动活跃' };
}
// 3. 获取活跃策略配置
let activeStrategy = {};
if (accountData.active_strategy) {
activeStrategy = typeof accountData.active_strategy === 'string'
? JSON.parse(accountData.active_strategy)
: accountData.active_strategy;
}
// 4. 检查时间范围
if (activeStrategy.time_range) {
const timeCheck = this.checkTimeRange(activeStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动活跃] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查活跃间隔
const active_interval = activeStrategy.active_interval || this.config.defaultInterval;
const intervalCheck = await this.checkExecutionInterval(sn_code, active_interval);
if (!intervalCheck.allowed) {
console.log(`[自动活跃] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
platform: accountData.platform_type || 'boss',
actions: activeStrategy.actions || ['browse_jobs', 'refresh_resume', 'check_notifications'],
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动活跃] 已为设备 ${sn_code} 添加活跃任务,间隔: ${active_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动活跃] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoActiveTask();

View File

@@ -0,0 +1,181 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
/**
* 自动沟通任务
* 自动回复HR消息,保持活跃度
*/
class AutoChatTask extends BaseTask {
constructor() {
super('auto_chat', {
defaultInterval: 15, // 默认15分钟
defaultPriority: 6, // 中等优先级
requiresLogin: true, // 需要登录
conflictsWith: [] // 不与其他任务冲突(可以在投递/搜索间隙执行)
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.platform) {
return {
valid: false,
reason: '缺少必要参数: platform'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动沟通 - ${params.name || '默认'}`;
}
/**
* 执行自动沟通任务
*/
async execute(sn_code, params) {
console.log(`[自动沟通] 设备 ${sn_code} 开始执行沟通任务`);
// 1. 获取未读消息列表
const unreadMessages = await this.getUnreadMessages(sn_code, params.platform);
if (!unreadMessages || unreadMessages.length === 0) {
console.log(`[自动沟通] 设备 ${sn_code} 没有未读消息`);
return {
success: true,
repliedCount: 0,
message: '没有未读消息'
};
}
console.log(`[自动沟通] 设备 ${sn_code} 找到 ${unreadMessages.length} 条未读消息`);
// 2. 智能回复(这里需要调用实际的AI回复逻辑)
const replyResult = {
success: true,
repliedCount: unreadMessages.length,
messages: unreadMessages.map(m => ({
id: m.id,
from: m.hr_name,
company: m.company_name
}))
};
return replyResult;
}
/**
* 获取未读消息
*/
async getUnreadMessages(sn_code, platform) {
// TODO: 从数据库或缓存获取未读消息
// 这里返回空数组作为示例
return [];
}
/**
* 添加沟通任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动沟通] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
if (!account) {
console.log(`[自动沟通] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
const accountData = account.toJSON();
// 2. 检查是否开启了自动沟通
if (!accountData.auto_chat) {
console.log(`[自动沟通] 设备 ${sn_code} 未开启自动沟通`);
return { success: false, reason: '未开启自动沟通' };
}
// 3. 获取沟通策略配置
let chatStrategy = {};
if (accountData.chat_strategy) {
chatStrategy = typeof accountData.chat_strategy === 'string'
? JSON.parse(accountData.chat_strategy)
: accountData.chat_strategy;
}
// 4. 检查时间范围
if (chatStrategy.time_range) {
const timeCheck = this.checkTimeRange(chatStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动沟通] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查沟通间隔
const chat_interval = chatStrategy.chat_interval || this.config.defaultInterval;
const intervalCheck = await this.checkExecutionInterval(sn_code, chat_interval);
if (!intervalCheck.allowed) {
console.log(`[自动沟通] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
platform: accountData.platform_type || 'boss',
name: accountData.name || '默认',
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动沟通] 已为设备 ${sn_code} 添加沟通任务,间隔: ${chat_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动沟通] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoChatTask();

View File

@@ -0,0 +1,320 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
const authorizationService = require('../../../services/authorization_service');
/**
* 自动投递任务
* 从数据库读取职位列表并进行自动投递
*/
class AutoDeliverTask extends BaseTask {
constructor() {
super('auto_deliver', {
defaultInterval: 30, // 默认30分钟
defaultPriority: 7, // 高优先级
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_search', // 搜索任务
'auto_active_account' // 活跃账号任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动投递 - ${params.keyword || '指定职位'}`;
}
/**
* 执行自动投递任务
*/
async execute(sn_code, params) {
console.log(`[自动投递] 设备 ${sn_code} 开始执行投递任务`);
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
throw new Error(`账号 ${sn_code} 不存在`);
}
// 2. 检查授权
const authorization = await authorizationService.checkAuthorization(sn_code);
if (!authorization.is_authorized) {
throw new Error('授权天数不足');
}
// 3. 获取投递配置
const deliverConfig = this.parseDeliverConfig(account.deliver_config);
// 4. 检查日投递限制
const dailyLimit = config.platformDailyLimits[account.platform_type] || 50;
const todayDelivered = await this.getTodayDeliveredCount(sn_code);
if (todayDelivered >= dailyLimit) {
throw new Error(`今日投递已达上限 (${todayDelivered}/${dailyLimit})`);
}
// 5. 获取可投递的职位列表
const jobs = await this.getDeliverableJobs(sn_code, account, deliverConfig);
if (!jobs || jobs.length === 0) {
console.log(`[自动投递] 设备 ${sn_code} 没有可投递的职位`);
return {
success: true,
delivered: 0,
message: '没有可投递的职位'
};
}
console.log(`[自动投递] 设备 ${sn_code} 找到 ${jobs.length} 个可投递职位`);
// 6. 执行投递(这里需要调用实际的投递逻辑)
const deliverResult = {
success: true,
delivered: jobs.length,
jobs: jobs.map(j => ({
id: j.id,
title: j.job_title,
company: j.company_name
}))
};
return deliverResult;
}
/**
* 获取账号信息
*/
async getAccountInfo(sn_code) {
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
return account ? account.toJSON() : null;
}
/**
* 解析投递配置
*/
parseDeliverConfig(deliver_config) {
if (typeof deliver_config === 'string') {
try {
deliver_config = JSON.parse(deliver_config);
} catch (e) {
deliver_config = {};
}
}
return {
deliver_interval: deliver_config?.deliver_interval || 30,
min_salary: deliver_config?.min_salary || 0,
max_salary: deliver_config?.max_salary || 0,
page_count: deliver_config?.page_count || 3,
max_deliver: deliver_config?.max_deliver || 10,
filter_keywords: deliver_config?.filter_keywords || [],
exclude_keywords: deliver_config?.exclude_keywords || [],
time_range: deliver_config?.time_range || null
};
}
/**
* 获取今日已投递数量
*/
async getTodayDeliveredCount(sn_code) {
const { task_status } = db.models;
const Sequelize = require('sequelize');
const today = new Date();
today.setHours(0, 0, 0, 0);
const count = await task_status.count({
where: {
sn_code: sn_code,
taskType: 'auto_deliver',
status: 'completed',
endTime: {
[Sequelize.Op.gte]: today
}
}
});
return count;
}
/**
* 获取可投递的职位列表
*/
async getDeliverableJobs(sn_code, account, deliverConfig) {
const { job_postings } = db.models;
const Sequelize = require('sequelize');
// 构建查询条件
const where = {
sn_code: sn_code,
platform: account.platform_type,
is_delivered: 0, // 未投递
is_filtered: 0 // 未被过滤
};
// 薪资范围过滤
if (deliverConfig.min_salary > 0) {
where.salary_min = {
[Sequelize.Op.gte]: deliverConfig.min_salary
};
}
if (deliverConfig.max_salary > 0) {
where.salary_max = {
[Sequelize.Op.lte]: deliverConfig.max_salary
};
}
// 查询职位
const jobs = await job_postings.findAll({
where: where,
limit: deliverConfig.max_deliver,
order: [['create_time', 'DESC']]
});
return jobs.map(j => j.toJSON());
}
/**
* 添加投递任务到队列
* 这是外部调用的入口,会进行所有冲突检查
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动投递] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
console.log(`[自动投递] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
// 2. 检查是否开启了自动投递
if (!account.auto_deliver) {
console.log(`[自动投递] 设备 ${sn_code} 未开启自动投递`);
return { success: false, reason: '未开启自动投递' };
}
// 3. 获取投递配置
const deliverConfig = this.parseDeliverConfig(account.deliver_config);
// 4. 检查时间范围
if (deliverConfig.time_range) {
const timeCheck = this.checkTimeRange(deliverConfig.time_range);
if (!timeCheck.allowed) {
console.log(`[自动投递] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查投递间隔
const intervalCheck = await this.checkExecutionInterval(
sn_code,
deliverConfig.deliver_interval
);
if (!intervalCheck.allowed) {
console.log(`[自动投递] 设备 ${sn_code} ${intervalCheck.reason}`);
// 推送等待状态到客户端
await this.notifyWaitingStatus(sn_code, intervalCheck, taskQueue);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
keyword: account.keyword || '',
platform: account.platform_type || 'boss',
pageCount: deliverConfig.page_count,
maxCount: deliverConfig.max_deliver,
filterRules: {
minSalary: deliverConfig.min_salary,
maxSalary: deliverConfig.max_salary,
keywords: deliverConfig.filter_keywords,
excludeKeywords: deliverConfig.exclude_keywords
},
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动投递] 已为设备 ${sn_code} 添加投递任务,间隔: ${deliverConfig.deliver_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动投递] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
/**
* 推送等待状态到客户端
*/
async notifyWaitingStatus(sn_code, intervalCheck, taskQueue) {
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
// 获取当前任务状态摘要
const taskStatusSummary = taskQueue.getTaskStatusSummary(sn_code);
// 添加等待消息到工作状态
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, taskStatusSummary, {
waitingMessage: {
type: 'deliver_interval',
message: intervalCheck.reason,
remainingMinutes: intervalCheck.remainingMinutes,
nextDeliverTime: intervalCheck.nextExecutionTime?.toISOString()
}
});
} catch (error) {
console.warn(`[自动投递] 推送等待消息失败:`, error.message);
}
}
}
// 导出单例
module.exports = new AutoDeliverTask();

View File

@@ -0,0 +1,233 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
/**
* 自动搜索职位任务
* 定期搜索符合条件的职位并保存到数据库
*/
class AutoSearchTask extends BaseTask {
constructor() {
super('auto_search', {
defaultInterval: 60, // 默认60分钟
defaultPriority: 8, // 高优先级(比投递高,先搜索后投递)
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_deliver', // 投递任务
'auto_active_account' // 活跃账号任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.keyword && !params.jobType) {
return {
valid: false,
reason: '缺少必要参数: keyword 或 jobType'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动搜索 - ${params.keyword || params.jobType || '默认'}`;
}
/**
* 执行自动搜索任务
*/
async execute(sn_code, params) {
console.log(`[自动搜索] 设备 ${sn_code} 开始执行搜索任务`);
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
throw new Error(`账号 ${sn_code} 不存在`);
}
// 2. 获取搜索配置
const searchConfig = this.parseSearchConfig(account.search_config);
// 3. 检查日搜索限制
const dailyLimit = config.dailyLimits.maxSearch || 20;
const todaySearched = await this.getTodaySearchCount(sn_code);
if (todaySearched >= dailyLimit) {
throw new Error(`今日搜索已达上限 (${todaySearched}/${dailyLimit})`);
}
// 4. 执行搜索(这里需要调用实际的搜索逻辑)
const searchResult = {
success: true,
keyword: params.keyword || account.keyword,
pageCount: searchConfig.page_count || 3,
jobsFound: 0, // 实际搜索到的职位数
jobsSaved: 0 // 保存到数据库的职位数
};
console.log(`[自动搜索] 设备 ${sn_code} 搜索完成,找到 ${searchResult.jobsFound} 个职位`);
return searchResult;
}
/**
* 获取账号信息
*/
async getAccountInfo(sn_code) {
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
return account ? account.toJSON() : null;
}
/**
* 解析搜索配置
*/
parseSearchConfig(search_config) {
if (typeof search_config === 'string') {
try {
search_config = JSON.parse(search_config);
} catch (e) {
search_config = {};
}
}
return {
search_interval: search_config?.search_interval || 60,
page_count: search_config?.page_count || 3,
city: search_config?.city || '',
salary_range: search_config?.salary_range || '',
experience: search_config?.experience || '',
education: search_config?.education || '',
time_range: search_config?.time_range || null
};
}
/**
* 获取今日已搜索数量
*/
async getTodaySearchCount(sn_code) {
const { task_status } = db.models;
const Sequelize = require('sequelize');
const today = new Date();
today.setHours(0, 0, 0, 0);
const count = await task_status.count({
where: {
sn_code: sn_code,
taskType: 'auto_search',
status: 'completed',
endTime: {
[Sequelize.Op.gte]: today
}
}
});
return count;
}
/**
* 添加搜索任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动搜索] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
console.log(`[自动搜索] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
// 2. 检查是否开启了自动搜索
if (!account.auto_search) {
console.log(`[自动搜索] 设备 ${sn_code} 未开启自动搜索`);
return { success: false, reason: '未开启自动搜索' };
}
// 3. 获取搜索配置
const searchConfig = this.parseSearchConfig(account.search_config);
// 4. 检查时间范围
if (searchConfig.time_range) {
const timeCheck = this.checkTimeRange(searchConfig.time_range);
if (!timeCheck.allowed) {
console.log(`[自动搜索] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查搜索间隔
const intervalCheck = await this.checkExecutionInterval(
sn_code,
searchConfig.search_interval
);
if (!intervalCheck.allowed) {
console.log(`[自动搜索] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
keyword: account.keyword || '',
jobType: account.job_type || '',
platform: account.platform_type || 'boss',
pageCount: searchConfig.page_count || 3,
city: searchConfig.city || '',
salaryRange: searchConfig.salary_range || '',
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动搜索] 已为设备 ${sn_code} 添加搜索任务,间隔: ${searchConfig.search_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动搜索] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoSearchTask();

View File

@@ -0,0 +1,405 @@
const dayjs = require('dayjs');
const deviceManager = require('../core/deviceManager');
const db = require('../../dbProxy');
/**
* 任务基类
* 提供所有任务的通用功能和冲突检测机制
*/
class BaseTask {
constructor(taskType, config = {}) {
this.taskType = taskType;
this.config = {
// 默认配置
defaultInterval: 30, // 默认间隔30分钟
defaultPriority: 5,
requiresLogin: true, // 是否需要登录状态
conflictsWith: [], // 与哪些任务类型冲突
...config
};
// 任务执行锁 { sn_code: timestamp }
this.taskLocks = new Map();
// 最后执行时间缓存 { sn_code: timestamp }
this.lastExecutionCache = new Map();
}
/**
* Layer 1: 任务类型互斥锁检查
* 防止同一设备同时添加相同类型的任务
*/
acquireTaskLock(sn_code) {
const lockKey = `${sn_code}:${this.taskType}`;
const now = Date.now();
const existingLock = this.taskLocks.get(lockKey);
// 如果存在锁且未超时(5分钟),返回false
if (existingLock && (now - existingLock) < 5 * 60 * 1000) {
const remainingTime = Math.ceil((5 * 60 * 1000 - (now - existingLock)) / 1000);
return {
allowed: false,
reason: `任务 ${this.taskType} 正在添加中,请等待 ${remainingTime}`
};
}
// 获取锁
this.taskLocks.set(lockKey, now);
return { allowed: true };
}
/**
* 释放任务锁
*/
releaseTaskLock(sn_code) {
const lockKey = `${sn_code}:${this.taskType}`;
this.taskLocks.delete(lockKey);
}
/**
* Layer 2: 设备状态检查
* 检查设备是否在线、是否登录、是否忙碌
*/
async checkDeviceStatus(sn_code) {
// 1. 优先检查内存中的设备状态
let device = deviceManager.devices.get(sn_code);
// 2. 如果内存中没有,降级到数据库查询(可能是刚启动还没收到心跳)
if (!device) {
try {
const pla_account = db.getModel('pla_account');
const dbDevice = await pla_account.findOne({
where: { sn_code, is_delete: 0 },
attributes: ['sn_code', 'is_online', 'is_logged_in']
});
if (!dbDevice) {
return {
allowed: false,
reason: `设备 ${sn_code} 不存在`
};
}
// 检查数据库中的在线状态
if (!dbDevice.is_online) {
return {
allowed: false,
reason: `设备 ${sn_code} 离线(数据库状态)`
};
}
// 检查数据库中的登录状态
if (this.config.requiresLogin && !dbDevice.is_logged_in) {
return {
allowed: false,
reason: `设备 ${sn_code} 未登录平台账号(数据库状态)`
};
}
// 数据库检查通过,允许执行
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 查询设备状态失败:`, error);
return {
allowed: false,
reason: `设备 ${sn_code} 状态查询失败`
};
}
}
// 3. 检查心跳超时
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
return {
allowed: false,
reason: `设备 ${sn_code} 离线(最后心跳: ${offlineMinutes}分钟前)`
};
}
// 4. 检查登录状态(如果任务需要)
if (this.config.requiresLogin && !device.isLoggedIn) {
return {
allowed: false,
reason: `设备 ${sn_code} 未登录平台账号`
};
}
return { allowed: true };
}
/**
* Layer 3: 检查任务队列状态
* 防止队列中已有相同任务
*/
async checkTaskQueue(sn_code, taskQueue) {
// 获取设备队列
const deviceQueue = taskQueue.deviceQueues.get(sn_code);
if (!deviceQueue) {
return { allowed: true };
}
// 检查队列中是否有相同类型的待执行任务
const tasks = deviceQueue.toArray();
const hasSameTypeTask = tasks.some(task =>
task.taskType === this.taskType &&
task.status === 'pending'
);
if (hasSameTypeTask) {
return {
allowed: false,
reason: `队列中已存在待执行的 ${this.taskType} 任务`
};
}
return { allowed: true };
}
/**
* Layer 4: 检查任务去重
* 查询数据库中是否有重复的待执行任务
*/
async checkDuplicateTask(sn_code) {
try {
const { task_status } = db.models;
// 查询该设备是否有相同类型的pending/running任务
const existingTask = await task_status.findOne({
where: {
sn_code: sn_code,
taskType: this.taskType,
status: ['pending', 'running']
},
attributes: ['id', 'status', 'taskName']
});
if (existingTask) {
return {
allowed: false,
reason: `已存在 ${existingTask.status} 状态的任务: ${existingTask.taskName}`
};
}
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 检查重复任务失败:`, error);
// 出错时允许继续,避免阻塞
return { allowed: true };
}
}
/**
* Layer 5: 操作类型冲突检测
* 某些操作类型不能同时执行
*/
async checkOperationConflict(sn_code, taskQueue) {
// 如果没有配置冲突类型,直接通过
if (!this.config.conflictsWith || this.config.conflictsWith.length === 0) {
return { allowed: true };
}
// 检查当前是否有冲突的任务正在执行
const deviceStatus = taskQueue.deviceStatus.get(sn_code);
if (deviceStatus && deviceStatus.currentTask) {
const currentTaskType = deviceStatus.currentTask.taskType;
if (this.config.conflictsWith.includes(currentTaskType)) {
return {
allowed: false,
reason: `与正在执行的任务 ${currentTaskType} 冲突`
};
}
}
return { allowed: true };
}
/**
* 检查执行间隔
* 从数据库查询上次成功执行时间,判断是否满足间隔要求
*/
async checkExecutionInterval(sn_code, intervalMinutes) {
try {
const { task_status } = db.models;
// 先从缓存检查
const cachedLastExecution = this.lastExecutionCache.get(sn_code);
const now = Date.now();
if (cachedLastExecution) {
const elapsedTime = now - cachedLastExecution;
const interval_ms = intervalMinutes * 60 * 1000;
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
return {
allowed: false,
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
remainingMinutes,
elapsedMinutes
};
}
}
// 从数据库查询最近一次成功完成的任务
const lastTask = await task_status.findOne({
where: {
sn_code: sn_code,
taskType: this.taskType,
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
// 如果存在上次执行记录,检查是否已经过了间隔时间
if (lastTask && lastTask.endTime) {
const lastExecutionTime = new Date(lastTask.endTime).getTime();
const elapsedTime = now - lastExecutionTime;
const interval_ms = intervalMinutes * 60 * 1000;
// 更新缓存
this.lastExecutionCache.set(sn_code, lastExecutionTime);
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
return {
allowed: false,
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
remainingMinutes,
elapsedMinutes,
nextExecutionTime: new Date(lastExecutionTime + interval_ms)
};
}
}
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 检查执行间隔失败:`, error);
// 出错时允许继续,避免阻塞
return { allowed: true };
}
}
/**
* 检查时间范围限制
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
*/
checkTimeRange(timeRange) {
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
return { allowed: true, reason: '未配置时间范围' };
}
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute;
// 解析开始时间和结束时间
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
// 检查是否仅工作日
if (timeRange.workdays_only == 1) {
const dayOfWeek = now.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
}
}
// 检查当前时间是否在时间范围内
if (startTime <= endTime) {
// 正常情况: 09:00 - 18:00
if (currentTime < startTime || currentTime >= endTime) {
return {
allowed: false,
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
} else {
// 跨天情况: 22:00 - 06:00
if (currentTime < startTime && currentTime >= endTime) {
return {
allowed: false,
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
}
return { allowed: true, reason: '在允许的时间范围内' };
}
/**
* 综合检查 - 执行所有层级的检查
* @param {string} sn_code - 设备SN码
* @param {Object} taskQueue - 任务队列实例
* @param {Object} options - 额外选项
* @returns {Object} { allowed: boolean, reason: string }
*/
async canExecuteTask(sn_code, taskQueue, options = {}) {
const checks = [
{ name: 'Layer1-任务锁', fn: () => this.acquireTaskLock(sn_code) },
{ name: 'Layer2-设备状态', fn: () => this.checkDeviceStatus(sn_code) },
{ name: 'Layer3-队列检查', fn: () => this.checkTaskQueue(sn_code, taskQueue) },
{ name: 'Layer4-任务去重', fn: () => this.checkDuplicateTask(sn_code) },
{ name: 'Layer5-操作冲突', fn: () => this.checkOperationConflict(sn_code, taskQueue) }
];
// 逐层检查
for (const check of checks) {
const result = await check.fn();
if (!result.allowed) {
console.log(`[${this.taskType}] ${check.name} 未通过: ${result.reason}`);
return result;
}
}
return { allowed: true };
}
/**
* 清理任务锁(定期清理过期锁)
*/
cleanupExpiredLocks() {
const now = Date.now();
const timeout = 5 * 60 * 1000; // 5分钟超时
for (const [lockKey, timestamp] of this.taskLocks.entries()) {
if (now - timestamp > timeout) {
this.taskLocks.delete(lockKey);
}
}
}
/**
* 获取任务名称(子类可覆盖)
*/
getTaskName(params) {
return `${this.taskType} 任务`;
}
/**
* 验证任务参数(子类必须实现)
*/
validateParams(params) {
throw new Error('子类必须实现 validateParams 方法');
}
/**
* 执行任务的具体逻辑(子类必须实现)
*/
async execute(sn_code, params) {
throw new Error('子类必须实现 execute 方法');
}
}
module.exports = BaseTask;

View File

@@ -0,0 +1,16 @@
/**
* 任务模块索引
* 统一导出所有任务类型
*/
const autoSearchTask = require('./autoSearchTask');
const autoDeliverTask = require('./autoDeliverTask');
const autoChatTask = require('./autoChatTask');
const autoActiveTask = require('./autoActiveTask');
module.exports = {
autoSearchTask,
autoDeliverTask,
autoChatTask,
autoActiveTask
};

View File

@@ -0,0 +1,14 @@
/**
* Utils 模块导出
* 统一导出工具类模块
*/
const SalaryParser = require('./salaryParser');
const KeywordMatcher = require('./keywordMatcher');
const ScheduleUtils = require('./scheduleUtils');
module.exports = {
SalaryParser,
KeywordMatcher,
ScheduleUtils
};

View File

@@ -0,0 +1,225 @@
/**
* 关键词匹配工具
* 提供职位描述的关键词匹配和评分功能
*/
class KeywordMatcher {
/**
* 检查是否包含排除关键词
* @param {string} text - 待检查的文本
* @param {string[]} excludeKeywords - 排除关键词列表
* @returns {{matched: boolean, keywords: string[]}} 匹配结果
*/
static matchExcludeKeywords(text, excludeKeywords = []) {
if (!text || !excludeKeywords || excludeKeywords.length === 0) {
return { matched: false, keywords: [] };
}
const matched = [];
const lowerText = text.toLowerCase();
for (const keyword of excludeKeywords) {
if (!keyword || !keyword.trim()) continue;
const lowerKeyword = keyword.toLowerCase().trim();
if (lowerText.includes(lowerKeyword)) {
matched.push(keyword);
}
}
return {
matched: matched.length > 0,
keywords: matched
};
}
/**
* 检查是否包含过滤关键词(必须匹配)
* @param {string} text - 待检查的文本
* @param {string[]} filterKeywords - 过滤关键词列表
* @returns {{matched: boolean, keywords: string[], matchCount: number}} 匹配结果
*/
static matchFilterKeywords(text, filterKeywords = []) {
if (!text) {
return { matched: false, keywords: [], matchCount: 0 };
}
if (!filterKeywords || filterKeywords.length === 0) {
return { matched: true, keywords: [], matchCount: 0 };
}
const matched = [];
const lowerText = text.toLowerCase();
for (const keyword of filterKeywords) {
if (!keyword || !keyword.trim()) continue;
const lowerKeyword = keyword.toLowerCase().trim();
if (lowerText.includes(lowerKeyword)) {
matched.push(keyword);
}
}
// 只要匹配到至少一个过滤关键词即可通过
return {
matched: matched.length > 0,
keywords: matched,
matchCount: matched.length
};
}
/**
* 计算关键词匹配奖励分数
* @param {string} text - 待检查的文本
* @param {string[]} keywords - 关键词列表
* @param {object} options - 选项
* @returns {{score: number, matchedKeywords: string[], matchCount: number}}
*/
static calculateBonus(text, keywords = [], options = {}) {
const {
baseScore = 10, // 每个关键词的基础分
maxBonus = 50, // 最大奖励分
caseSensitive = false // 是否区分大小写
} = options;
if (!text || !keywords || keywords.length === 0) {
return { score: 0, matchedKeywords: [], matchCount: 0 };
}
const matched = [];
const searchText = caseSensitive ? text : text.toLowerCase();
for (const keyword of keywords) {
if (!keyword || !keyword.trim()) continue;
const searchKeyword = caseSensitive ? keyword.trim() : keyword.toLowerCase().trim();
if (searchText.includes(searchKeyword)) {
matched.push(keyword);
}
}
const score = Math.min(matched.length * baseScore, maxBonus);
return {
score,
matchedKeywords: matched,
matchCount: matched.length
};
}
/**
* 高亮匹配的关键词(用于展示)
* @param {string} text - 原始文本
* @param {string[]} keywords - 关键词列表
* @param {string} prefix - 前缀标记(默认 <mark>
* @param {string} suffix - 后缀标记(默认 </mark>
* @returns {string} 高亮后的文本
*/
static highlight(text, keywords = [], prefix = '<mark>', suffix = '</mark>') {
if (!text || !keywords || keywords.length === 0) {
return text;
}
let result = text;
for (const keyword of keywords) {
if (!keyword || !keyword.trim()) continue;
const regex = new RegExp(`(${this.escapeRegex(keyword.trim())})`, 'gi');
result = result.replace(regex, `${prefix}$1${suffix}`);
}
return result;
}
/**
* 转义正则表达式特殊字符
* @param {string} str - 待转义的字符串
* @returns {string} 转义后的字符串
*/
static escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* 综合匹配(排除 + 过滤 + 奖励)
* @param {string} text - 待检查的文本
* @param {object} config - 配置
* @param {string[]} config.excludeKeywords - 排除关键词
* @param {string[]} config.filterKeywords - 过滤关键词
* @param {string[]} config.bonusKeywords - 奖励关键词
* @returns {{pass: boolean, reason?: string, score: number, details: object}}
*/
static match(text, config = {}) {
const {
excludeKeywords = [],
filterKeywords = [],
bonusKeywords = []
} = config;
// 1. 检查排除关键词
const excludeResult = this.matchExcludeKeywords(text, excludeKeywords);
if (excludeResult.matched) {
return {
pass: false,
reason: `包含排除关键词: ${excludeResult.keywords.join(', ')}`,
score: 0,
details: { exclude: excludeResult }
};
}
// 2. 检查过滤关键词(必须匹配)
const filterResult = this.matchFilterKeywords(text, filterKeywords);
if (filterKeywords.length > 0 && !filterResult.matched) {
return {
pass: false,
reason: '不包含任何必需关键词',
score: 0,
details: { filter: filterResult }
};
}
// 3. 计算奖励分数
const bonusResult = this.calculateBonus(text, bonusKeywords);
return {
pass: true,
score: bonusResult.score,
details: {
exclude: excludeResult,
filter: filterResult,
bonus: bonusResult
}
};
}
/**
* 批量匹配职位列表
* @param {Array} jobs - 职位列表
* @param {object} config - 匹配配置
* @param {Function} textExtractor - 文本提取函数 (job) => string
* @returns {Array} 匹配通过的职位(带匹配信息)
*/
static filterJobs(jobs, config, textExtractor = (job) => `${job.name || ''} ${job.description || ''}`) {
if (!jobs || jobs.length === 0) {
return [];
}
const filtered = [];
for (const job of jobs) {
const text = textExtractor(job);
const matchResult = this.match(text, config);
if (matchResult.pass) {
filtered.push({
...job,
_matchInfo: matchResult
});
}
}
return filtered;
}
}
module.exports = KeywordMatcher;

View File

@@ -0,0 +1,126 @@
/**
* 薪资解析工具
* 统一处理职位薪资和期望薪资的解析逻辑
*/
class SalaryParser {
/**
* 解析薪资范围字符串
* @param {string} salaryDesc - 薪资描述 (如 "15-20K", "8000-12000元")
* @returns {{ min: number, max: number }} 薪资范围(单位:元)
*/
static parse(salaryDesc) {
if (!salaryDesc || typeof salaryDesc !== 'string') {
return { min: 0, max: 0 };
}
// 尝试各种格式
return this.parseK(salaryDesc)
|| this.parseYuan(salaryDesc)
|| this.parseMixed(salaryDesc)
|| { min: 0, max: 0 };
}
/**
* 解析 K 格式薪资 (如 "15-20K", "8-12k")
*/
static parseK(desc) {
const kMatch = desc.match(/(\d+)[-~](\d+)[kK千]/);
if (kMatch) {
return {
min: parseInt(kMatch[1]) * 1000,
max: parseInt(kMatch[2]) * 1000
};
}
return null;
}
/**
* 解析元格式薪资 (如 "8000-12000元", "15000-20000")
*/
static parseYuan(desc) {
const yuanMatch = desc.match(/(\d+)[-~](\d+)元?/);
if (yuanMatch) {
return {
min: parseInt(yuanMatch[1]),
max: parseInt(yuanMatch[2])
};
}
return null;
}
/**
* 解析混合格式 (如 "8k-12000元")
*/
static parseMixed(desc) {
const mixedMatch = desc.match(/(\d+)[kK千][-~](\d+)元?/);
if (mixedMatch) {
return {
min: parseInt(mixedMatch[1]) * 1000,
max: parseInt(mixedMatch[2])
};
}
return null;
}
/**
* 检查职位薪资是否在期望范围内
* @param {object} jobSalary - 职位薪资 { min, max }
* @param {number} minExpected - 期望最低薪资
* @param {number} maxExpected - 期望最高薪资
*/
static isWithinRange(jobSalary, minExpected, maxExpected) {
if (!jobSalary || jobSalary.min === 0) {
return true; // 无法判断时默认通过
}
// 职位最高薪资 >= 期望最低薪资
if (minExpected > 0 && jobSalary.max < minExpected) {
return false;
}
// 职位最低薪资 <= 期望最高薪资
if (maxExpected > 0 && jobSalary.min > maxExpected) {
return false;
}
return true;
}
/**
* 计算薪资匹配度(用于职位评分)
* @param {object} jobSalary - 职位薪资
* @param {object} expectedSalary - 期望薪资
* @returns {number} 匹配度 0-1
*/
static calculateMatch(jobSalary, expectedSalary) {
if (!jobSalary || !expectedSalary || jobSalary.min === 0 || expectedSalary.min === 0) {
return 0.5; // 无法判断时返回中性值
}
const jobAvg = (jobSalary.min + jobSalary.max) / 2;
const expectedAvg = (expectedSalary.min + expectedSalary.max) / 2;
const diff = Math.abs(jobAvg - expectedAvg);
const range = (jobSalary.max - jobSalary.min + expectedSalary.max - expectedSalary.min) / 2;
// 差距越小,匹配度越高
return Math.max(0, 1 - diff / (range || 1));
}
/**
* 格式化薪资显示
* @param {object} salary - 薪资对象 { min, max }
* @returns {string} 格式化字符串
*/
static format(salary) {
if (!salary || salary.min === 0) {
return '面议';
}
const minK = (salary.min / 1000).toFixed(0);
const maxK = (salary.max / 1000).toFixed(0);
return `${minK}-${maxK}K`;
}
}
module.exports = SalaryParser;

View File

@@ -79,20 +79,20 @@ module.exports = (db) => {
get: function () { get: function () {
const value = this.getDataValue('is_salary_priority'); const value = this.getDataValue('is_salary_priority');
if (!value) { if (!value) {
return [{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}]; return [{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20 }];
} }
if (typeof value === 'string') { if (typeof value === 'string') {
try { try {
return JSON.parse(value); return JSON.parse(value);
} catch (e) { } catch (e) {
return [{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}]; return [{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20 }];
} }
} }
return value; return value;
}, },
set: function (value) { set: function (value) {
if (value === null || value === undefined) { 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}])); 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') { } else if (typeof value === 'string') {
// 如果已经是字符串,直接使用 // 如果已经是字符串,直接使用
this.setDataValue('is_salary_priority', value); this.setDataValue('is_salary_priority', value);
@@ -101,7 +101,7 @@ module.exports = (db) => {
this.setDataValue('is_salary_priority', JSON.stringify(value)); this.setDataValue('is_salary_priority', JSON.stringify(value));
} }
}, },
defaultValue: JSON.stringify([{ "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 }])
}, },
@@ -185,6 +185,57 @@ module.exports = (db) => {
exclude_keywords: [] exclude_keywords: []
}) })
}, },
// 自动搜索相关配置
auto_search: {
comment: '自动搜索开关',
type: Sequelize.TINYINT(1),
allowNull: false,
defaultValue: 0
},
// 自动搜索配置JSON格式包含search_interval-搜索间隔分钟数, page_count-滚动获取职位列表次数, city-城市, time_range-搜索时间段)
search_config: {
comment: '自动搜索配置JSON对象',
type: Sequelize.JSON(),
allowNull: true,
get: function () {
const value = this.getDataValue('search_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('search_config', null);
} else if (typeof value === 'string') {
// 如果已经是字符串,直接使用
this.setDataValue('search_config', value);
} else {
// 如果是对象,序列化为字符串
this.setDataValue('search_config', JSON.stringify(value));
}
},
// 默认值说明:
// city: '' - 城市,默认空字符串
// cityName: '' - 城市名称,默认空字符串
// salary: '' - 薪资,默认空字符串
// experience: '' - 经验,默认空字符串
// education: '' - 学历,默认空字符串
defaultValue: JSON.stringify({
search_interval: 30,
city: '',
cityName: '',
salary: '',
experience: '',
education: ''
})
},
// 自动沟通相关配置 // 自动沟通相关配置
auto_chat: { auto_chat: {
comment: '自动沟通开关', comment: '自动沟通开关',

View File

@@ -4,13 +4,14 @@
*/ */
const axios = require('axios'); const axios = require('axios');
const config = require('../../config/config');
class AIService { class AIService {
constructor(config = {}) { constructor() {
this.apiKey = config.apiKey || process.env.AI_API_KEY || ''; this.apiKey = config.ai.apiKey;
this.baseURL = config.baseURL || process.env.AI_BASE_URL || 'https://api.deepseek.com'; this.baseURL = config.ai.baseUrl;
this.model = config.model || 'deepseek-chat'; this.model = config.ai.model;
this.timeout = config.timeout || 30000; this.timeout = 30000;
// 创建axios实例 // 创建axios实例
this.client = axios.create({ this.client = axios.create({
@@ -394,23 +395,21 @@ let instance = null;
module.exports = { module.exports = {
/** /**
* 获取AI服务实例 * 获取AI服务实例
* @param {Object} config - 配置选项
* @returns {AIService} * @returns {AIService}
*/ */
getInstance(config) { getInstance() {
if (!instance) { if (!instance) {
instance = new AIService(config); instance = new AIService();
} }
return instance; return instance;
}, },
/** /**
* 创建新的AI服务实例 * 创建新的AI服务实例
* @param {Object} config - 配置选项
* @returns {AIService} * @returns {AIService}
*/ */
createInstance(config) { createInstance() {
return new AIService(config); return new AIService();
} }
}; };

View File

@@ -112,7 +112,5 @@ module.exports = {
// 导出各个服务类 // 导出各个服务类
AIService, AIService,
PlaAccountService PlaAccountService
// TaskScheduler 已废弃
// MQTTHandler 文件不存在
// JobService 已合并到 middleware/job/jobManager.js
}; };

View File

@@ -5,7 +5,7 @@
const db = require('../middleware/dbProxy'); const db = require('../middleware/dbProxy');
const scheduleManager = require('../middleware/schedule/index.js'); const scheduleManager = require('../middleware/schedule/index.js');
const locationService = require('./locationService'); const locationService = require('./location_service');
const authorizationService = require('./authorization_service'); const authorizationService = require('./authorization_service');
const { addRemainingDays, addRemainingDaysToAccounts } = require('../utils/account_utils'); const { addRemainingDays, addRemainingDaysToAccounts } = require('../utils/account_utils');

View File

@@ -25,7 +25,7 @@ module.exports = {
acquire: 30000, acquire: 30000,
idle: 10000 idle: 10000
}, },
logging: true logging: false
}, },
// API 路径配置(必需) // API 路径配置(必需)

View File

@@ -0,0 +1,59 @@
-- 任务调度系统重构 - 数据库迁移脚本
-- 为 pla_account 表添加自动搜索和自动活跃相关字段
USE autoAiWorkSys;
-- 1. 添加自动搜索开关字段
ALTER TABLE pla_account
ADD COLUMN auto_search TINYINT(1) DEFAULT 0 COMMENT '是否开启自动搜索: 0-关闭, 1-开启';
-- 2. 添加搜索配置字段
ALTER TABLE pla_account
ADD COLUMN search_config TEXT COMMENT '搜索配置(JSON): {search_interval, page_count, city, salary_range, time_range等}';
-- 3. 添加自动活跃开关字段
ALTER TABLE pla_account
ADD COLUMN auto_active TINYINT(1) DEFAULT 0 COMMENT '是否开启自动活跃: 0-关闭, 1-开启';
-- 4. 添加活跃策略配置字段
ALTER TABLE pla_account
ADD COLUMN active_strategy TEXT COMMENT '活跃策略配置(JSON): {active_interval, actions, time_range等}';
-- 5. 查看表结构验证
DESC pla_account;
-- 示例: 为已有账号设置默认配置
-- UPDATE pla_account
-- SET
-- auto_search = 0,
-- search_config = JSON_OBJECT(
-- 'search_interval', 60,
-- 'page_count', 3,
-- 'city', '',
-- 'time_range', JSON_OBJECT(
-- 'start_time', '09:00',
-- 'end_time', '18:00',
-- 'workdays_only', 1
-- )
-- ),
-- auto_active = 0,
-- active_strategy = JSON_OBJECT(
-- 'active_interval', 120,
-- 'actions', JSON_ARRAY('browse_jobs', 'refresh_resume', 'check_notifications'),
-- 'time_range', JSON_OBJECT(
-- 'start_time', '08:00',
-- 'end_time', '23:00',
-- 'workdays_only', 0
-- )
-- )
-- WHERE is_delete = 0;
-- 注意:
-- 1. 执行前请先备份数据库
-- 2. 建议在测试环境先测试
-- 3. search_config 和 active_strategy 字段存储JSON格式的配置
-- 4. 如果字段已存在会报错,可以先删除字段后再添加:
-- ALTER TABLE pla_account DROP COLUMN auto_search;
-- ALTER TABLE pla_account DROP COLUMN search_config;
-- ALTER TABLE pla_account DROP COLUMN auto_active;
-- ALTER TABLE pla_account DROP COLUMN active_strategy;