1
This commit is contained in:
@@ -15,7 +15,10 @@
|
||||
"Bash(ls:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(for:*)",
|
||||
"Bash(done)"
|
||||
"Bash(done)",
|
||||
"Bash(npm start)",
|
||||
"Bash(timeout 10 npm start)",
|
||||
"Bash(timeout 15 npm start)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -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();
|
||||
@@ -3,10 +3,7 @@
|
||||
* 聚合所有 job 相关模块的方法,提供统一的对外接口
|
||||
*/
|
||||
|
||||
const jobManager = require('./jobManager');
|
||||
const resumeManager = require('./resumeManager');
|
||||
const chatManager = require('./chatManager');
|
||||
|
||||
const { jobManager, resumeManager, chatManager } = require('./managers');
|
||||
|
||||
const pack = (instance) => {
|
||||
const proto = Object.getPrototypeOf(instance);
|
||||
@@ -23,7 +20,6 @@ const pack = (instance) => {
|
||||
|
||||
/**
|
||||
* 便捷方法:直接导出常用方法
|
||||
* 使用下划线命名规范
|
||||
*/
|
||||
module.exports = {
|
||||
...pack(jobManager),
|
||||
|
||||
@@ -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**: 初始版本,支持基础文本匹配和过滤功能
|
||||
- 支持从数据库动态获取职位类型配置
|
||||
- 支持自定义权重评分计算
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// const aiService = require('./aiService'); // 二期规划:AI 服务暂时禁用
|
||||
const logs = require('../logProxy');
|
||||
const aiServiceModule = require('../../../services/ai_service');
|
||||
const logs = require('../../logProxy');
|
||||
|
||||
// 实例化AI服务
|
||||
const aiService = aiServiceModule.getInstance();
|
||||
|
||||
/**
|
||||
* 智能聊天管理模块
|
||||
13
api/middleware/job/managers/index.js
Normal file
13
api/middleware/job/managers/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Managers 模块统一导出
|
||||
*/
|
||||
|
||||
const jobManager = require('./jobManager');
|
||||
const resumeManager = require('./resumeManager');
|
||||
const chatManager = require('./chatManager');
|
||||
|
||||
module.exports = {
|
||||
jobManager,
|
||||
resumeManager,
|
||||
chatManager
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
// const aiService = require('./aiService'); // 二期规划:AI 服务暂时禁用
|
||||
const jobFilterService = require('./job_filter_service'); // 使用文本匹配过滤服务
|
||||
const locationService = require('../../services/location_service'); // 位置服务
|
||||
const logs = require('../logProxy');
|
||||
const db = require('../dbProxy');
|
||||
const aiServiceModule = require('../../../services/ai_service');
|
||||
const { jobFilterService } = require('../services');
|
||||
const locationService = require('../../../services/locationService');
|
||||
const logs = require('../../logProxy');
|
||||
const db = require('../../dbProxy');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
// 实例化AI服务
|
||||
const aiService = aiServiceModule.getInstance();
|
||||
|
||||
/**
|
||||
* 工作管理模块
|
||||
* 负责简历获取、分析、存储和匹配度计算
|
||||
@@ -1,9 +1,12 @@
|
||||
const aiService = require('./aiService');
|
||||
const jobFilterService = require('./job_filter_service');
|
||||
const logs = require('../logProxy');
|
||||
const db = require('../dbProxy');
|
||||
const aiServiceModule = require('../../../services/ai_service');
|
||||
const { jobFilterService } = require('../services');
|
||||
const logs = require('../../logProxy');
|
||||
const db = require('../../dbProxy');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
// 实例化AI服务
|
||||
const aiService = aiServiceModule.getInstance();
|
||||
|
||||
/**
|
||||
* 简历管理模块
|
||||
* 负责简历获取、分析、存储和匹配度计算
|
||||
9
api/middleware/job/services/index.js
Normal file
9
api/middleware/job/services/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Services 模块统一导出
|
||||
*/
|
||||
|
||||
const jobFilterService = require('./jobFilterService');
|
||||
|
||||
module.exports = {
|
||||
jobFilterService
|
||||
};
|
||||
@@ -4,8 +4,8 @@
|
||||
* 支持从数据库动态获取职位类型的技能关键词和排除关键词
|
||||
*/
|
||||
|
||||
const db = require('../dbProxy.js');
|
||||
const locationService = require('../../services/location_service');
|
||||
const db = require('../../dbProxy.js');
|
||||
const locationService = require('../../../services/locationService');
|
||||
|
||||
class JobFilterService {
|
||||
constructor() {
|
||||
7
api/middleware/job/utils/index.js
Normal file
7
api/middleware/job/utils/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Utils 模块统一导出
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// 工具函数将在需要时添加
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const db = require('../dbProxy');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
/**
|
||||
* 统一错误处理模块
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 负责向客户端推送设备当前工作状态(任务、指令等)
|
||||
*/
|
||||
|
||||
const db = require('../dbProxy');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
class DeviceWorkStatusNotifier {
|
||||
constructor() {
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const aiConfig = require('./config/aiConfig');
|
||||
|
||||
class AIService {
|
||||
constructor(config = {}) {
|
||||
this.apiKey = config.apiKey || process.env.AI_API_KEY || '';
|
||||
this.baseURL = config.baseURL || process.env.AI_BASE_URL || 'https://api.deepseek.com';
|
||||
this.model = config.model || 'deepseek-chat';
|
||||
this.timeout = config.timeout || 30000;
|
||||
constructor() {
|
||||
this.apiKey = aiConfig.apiKey;
|
||||
this.baseURL = aiConfig.baseURL;
|
||||
this.model = aiConfig.model;
|
||||
this.timeout = aiConfig.timeout;
|
||||
|
||||
// 创建axios实例
|
||||
this.client = axios.create({
|
||||
|
||||
Reference in New Issue
Block a user