diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a6c4401..c22f40c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] diff --git a/api/middleware/job/aiService.js b/api/middleware/job/aiService.js deleted file mode 100644 index f53a631..0000000 --- a/api/middleware/job/aiService.js +++ /dev/null @@ -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} 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} 筛选结果 - */ - 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} 聊天内容 - */ - 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} 简历分析结果 - */ - 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} 面试邀约内容 - */ - 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} 外包识别结果 - */ - 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(); \ No newline at end of file diff --git a/api/middleware/job/index.js b/api/middleware/job/index.js index dbc01c0..e17c52c 100644 --- a/api/middleware/job/index.js +++ b/api/middleware/job/index.js @@ -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), diff --git a/api/middleware/job/job_filter_service.md b/api/middleware/job/job_filter_service.md deleted file mode 100644 index b399783..0000000 --- a/api/middleware/job/job_filter_service.md +++ /dev/null @@ -1,651 +0,0 @@ -# 职位过滤服务文档 - -## 概述 - -`job_filter_service.js` 是一个职位文本匹配过滤服务,使用简单的文本匹配规则来过滤和分析职位信息。该服务支持从数据库动态获取职位类型的技能关键词和排除关键词,能够分析职位与简历的匹配度,并提供过滤和评分功能。 - -## 主要功能 - -1. **职位类型配置管理**:从数据库获取或使用默认配置的技能关键词和排除关键词 -2. **匹配度分析**:分析职位与简历的匹配度(技能、经验、薪资) -3. **职位过滤**:根据匹配分数、外包标识、排除关键词等条件过滤职位列表 -4. **自定义权重评分**:支持根据自定义权重配置计算职位评分(距离、薪资、工作年限、学历、技能) - -## 类结构 - -```javascript -class JobFilterService { - // 默认技能关键词 - defaultCommonSkills: Array - - // 默认排除关键词 - defaultExcludeKeywords: Array - - // 职位类型配置缓存 - jobTypeCache: Map -} -``` - -## API 文档 - -### 1. getJobTypeConfig(jobTypeId) - -根据职位类型ID获取技能关键词和排除关键词配置。 - -**参数:** -- `jobTypeId` (number, 可选): 职位类型ID - -**返回值:** -```javascript -{ - commonSkills: Array, // 技能关键词列表 - excludeKeywords: Array // 排除关键词列表 -} -``` - -**说明:** -- 如果未提供 `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, // 匹配原因列表 - concerns: Array, // 关注点列表 - 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, 默认[]): 额外排除关键词列表 -- `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): 技能关键词列表 - -**返回值:** -- `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**: 初始版本,支持基础文本匹配和过滤功能 -- 支持从数据库动态获取职位类型配置 -- 支持自定义权重评分计算 - diff --git a/api/middleware/job/chatManager.js b/api/middleware/job/managers/chatManager.js similarity index 98% rename from api/middleware/job/chatManager.js rename to api/middleware/job/managers/chatManager.js index af9af00..d5bcf3b 100644 --- a/api/middleware/job/chatManager.js +++ b/api/middleware/job/managers/chatManager.js @@ -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(); /** * 智能聊天管理模块 diff --git a/api/middleware/job/managers/index.js b/api/middleware/job/managers/index.js new file mode 100644 index 0000000..5c3e6af --- /dev/null +++ b/api/middleware/job/managers/index.js @@ -0,0 +1,13 @@ +/** + * Managers 模块统一导出 + */ + +const jobManager = require('./jobManager'); +const resumeManager = require('./resumeManager'); +const chatManager = require('./chatManager'); + +module.exports = { + jobManager, + resumeManager, + chatManager +}; diff --git a/api/middleware/job/jobManager.js b/api/middleware/job/managers/jobManager.js similarity index 99% rename from api/middleware/job/jobManager.js rename to api/middleware/job/managers/jobManager.js index 218de29..ad79076 100644 --- a/api/middleware/job/jobManager.js +++ b/api/middleware/job/managers/jobManager.js @@ -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(); + /** * 工作管理模块 * 负责简历获取、分析、存储和匹配度计算 diff --git a/api/middleware/job/resumeManager.js b/api/middleware/job/managers/resumeManager.js similarity index 99% rename from api/middleware/job/resumeManager.js rename to api/middleware/job/managers/resumeManager.js index 3b6e446..6424f44 100644 --- a/api/middleware/job/resumeManager.js +++ b/api/middleware/job/managers/resumeManager.js @@ -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(); + /** * 简历管理模块 * 负责简历获取、分析、存储和匹配度计算 diff --git a/api/middleware/job/services/index.js b/api/middleware/job/services/index.js new file mode 100644 index 0000000..2756d8a --- /dev/null +++ b/api/middleware/job/services/index.js @@ -0,0 +1,9 @@ +/** + * Services 模块统一导出 + */ + +const jobFilterService = require('./jobFilterService'); + +module.exports = { + jobFilterService +}; diff --git a/api/middleware/job/job_filter_service.js b/api/middleware/job/services/jobFilterService.js similarity index 99% rename from api/middleware/job/job_filter_service.js rename to api/middleware/job/services/jobFilterService.js index 29e0743..edf6d6a 100644 --- a/api/middleware/job/job_filter_service.js +++ b/api/middleware/job/services/jobFilterService.js @@ -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() { diff --git a/api/middleware/job/utils/index.js b/api/middleware/job/utils/index.js new file mode 100644 index 0000000..67be924 --- /dev/null +++ b/api/middleware/job/utils/index.js @@ -0,0 +1,7 @@ +/** + * Utils 模块统一导出 + */ + +module.exports = { + // 工具函数将在需要时添加 +}; diff --git a/api/middleware/schedule/infrastructure/ErrorHandler.js b/api/middleware/schedule/infrastructure/ErrorHandler.js index 7e13e63..8c1f1a1 100644 --- a/api/middleware/schedule/infrastructure/ErrorHandler.js +++ b/api/middleware/schedule/infrastructure/ErrorHandler.js @@ -1,4 +1,4 @@ -const db = require('../dbProxy'); +const db = require('../../dbProxy'); /** * 统一错误处理模块 diff --git a/api/middleware/schedule/notifiers/deviceWorkStatusNotifier.js b/api/middleware/schedule/notifiers/deviceWorkStatusNotifier.js index 7daa5d4..6b35c4e 100644 --- a/api/middleware/schedule/notifiers/deviceWorkStatusNotifier.js +++ b/api/middleware/schedule/notifiers/deviceWorkStatusNotifier.js @@ -3,7 +3,7 @@ * 负责向客户端推送设备当前工作状态(任务、指令等) */ -const db = require('../dbProxy'); +const db = require('../../dbProxy'); class DeviceWorkStatusNotifier { constructor() { diff --git a/api/services/ai_service.js b/api/services/ai_service.js index 4d66fed..f4f656f 100644 --- a/api/services/ai_service.js +++ b/api/services/ai_service.js @@ -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({