This commit is contained in:
张成
2025-12-30 15:46:18 +08:00
parent d14f89e008
commit 65833dd32d
29 changed files with 2416 additions and 1048 deletions

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

@@ -0,0 +1,265 @@
const dayjs = require('dayjs');
/**
* 调度系统工具函数
* 提供通用的辅助功能
*/
class ScheduleUtils {
/**
* 生成唯一任务ID
* @returns {string} 任务ID
*/
static generateTaskId() {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 生成唯一指令ID
* @returns {string} 指令ID
*/
static generateCommandId() {
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 格式化时间戳
* @param {number} timestamp - 时间戳
* @returns {string} 格式化的时间
*/
static formatTimestamp(timestamp) {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss');
}
/**
* 格式化持续时间
* @param {number} ms - 毫秒数
* @returns {string} 格式化的持续时间
*/
static formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60 * 1000) {
return `${(ms / 1000).toFixed(1)}s`;
} else if (ms < 60 * 60 * 1000) {
return `${(ms / (60 * 1000)).toFixed(1)}min`;
} else {
return `${(ms / (60 * 60 * 1000)).toFixed(1)}h`;
}
}
/**
* 深度克隆对象
* @param {object} obj - 要克隆的对象
* @returns {object} 克隆后的对象
*/
static deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => this.deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
}
/**
* 安全解析JSON
* @param {string} jsonString - JSON字符串
* @param {any} defaultValue - 默认值
* @returns {any} 解析结果或默认值
*/
static safeJsonParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
return defaultValue;
}
}
/**
* 安全序列化JSON
* @param {any} obj - 要序列化的对象
* @param {string} defaultValue - 默认值
* @returns {string} JSON字符串或默认值
*/
static safeJsonStringify(obj, defaultValue = '{}') {
try {
return JSON.stringify(obj);
} catch (error) {
return defaultValue;
}
}
/**
* 延迟执行
* @param {number} ms - 延迟毫秒数
* @returns {Promise} Promise对象
*/
static delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 重试执行函数
* @param {function} fn - 要执行的函数
* @param {number} maxRetries - 最大重试次数
* @param {number} delay - 重试延迟(毫秒)
* @returns {Promise} 执行结果
*/
static async retry(fn, maxRetries = 3, delay = 1000) {
let lastError;
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < maxRetries) {
console.log(`[工具函数] 执行失败,${delay}ms后重试 (${i + 1}/${maxRetries + 1}):`, error.message);
await this.delay(delay);
}
}
}
throw lastError;
}
/**
* 限制并发执行数量
* @param {Array} tasks - 任务数组
* @param {number} concurrency - 并发数量
* @returns {Promise<Array>} 执行结果数组
*/
static async limitConcurrency(tasks, concurrency = 5) {
const results = [];
const executing = [];
for (const task of tasks) {
const promise = Promise.resolve(task()).then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
if (tasks.length >= concurrency) {
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
/**
* 创建带超时的Promise
* @param {Promise} promise - 原始Promise
* @param {number} timeout - 超时时间(毫秒)
* @param {string} timeoutMessage - 超时错误消息
* @returns {Promise} 带超时的Promise
*/
static withTimeout(promise, timeout, timeoutMessage = 'Operation timed out') {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => reject(new Error(timeoutMessage)), timeout);
})
]);
}
/**
* 获取今天的日期字符串
* @returns {string} 日期字符串 YYYY-MM-DD
*/
static getTodayString() {
return dayjs().format('YYYY-MM-DD');
}
/**
* 检查日期是否为今天
* @param {string|Date} date - 日期
* @returns {boolean} 是否为今天
*/
static isToday(date) {
return dayjs(date).format('YYYY-MM-DD') === this.getTodayString();
}
/**
* 获取随机延迟时间
* @param {number} min - 最小延迟(毫秒)
* @param {number} max - 最大延迟(毫秒)
* @returns {number} 随机延迟时间
*/
static getRandomDelay(min = 1000, max = 5000) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化的文件大小
*/
static formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* 计算成功率
* @param {number} success - 成功次数
* @param {number} total - 总次数
* @returns {string} 百分比字符串
*/
static calculateSuccessRate(success, total) {
if (total === 0) return '0%';
return ((success / total) * 100).toFixed(2) + '%';
}
/**
* 验证设备SN码格式
* @param {string} sn_code - 设备SN码
* @returns {boolean} 是否有效
*/
static isValidSnCode(sn_code) {
return typeof sn_code === 'string' && sn_code.length > 0 && sn_code.length <= 50;
}
/**
* 清理对象中的空值
* @param {object} obj - 要清理的对象
* @returns {object} 清理后的对象
*/
static cleanObject(obj) {
const cleaned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] !== null && obj[key] !== undefined && obj[key] !== '') {
cleaned[key] = obj[key];
}
}
return cleaned;
}
}
module.exports = ScheduleUtils;