diff --git a/_doc/搜索列表和投递功能开发规划.md b/_doc/搜索列表和投递功能开发规划.md index 32a286d..f59c7bc 100644 --- a/_doc/搜索列表和投递功能开发规划.md +++ b/_doc/搜索列表和投递功能开发规划.md @@ -18,6 +18,75 @@ - 投递状态跟踪 - 投递记录管理 +## 📊 Boss直聘响应数据结构 + +### 响应格式示例 + +Boss直聘搜索职位列表的响应数据结构如下: + +```json +{ + "code": 0, + "message": "Success", + "zpData": { + "resCount": 450, // 搜索结果总数 + "hasMore": true, // 是否还有更多 + "totalCount": 300, // 总职位数 + "jobList": [ // 职位列表 + { + "encryptJobId": "5ae70dfe114c23ab0nR-2ti-FFpU", // 职位ID(投递必需) + "encryptBossId": "b55854108ac215180XZ62N-_FlNT", // Boss ID(投递必需) + "securityId": "HP23zbQfaslvy-c1...", // 安全ID(投递必需) + "jobName": "全栈软件工程师", // 职位名称 + "salaryDesc": "25-50K·19薪", // 薪资描述(需解析) + "jobExperience": "在校/应届", // 工作经验(需解析) + "jobDegree": "学历不限", // 学历要求 + "city": 101020100, // 城市代码 + "cityName": "上海", // 城市名称 + "areaDistrict": "长宁区", // 区域 + "businessDistrict": "新华路", // 商圈 + "gps": { // 位置信息(优先使用) + "longitude": 121.41902537687392, + "latitude": 31.210308153576566 + }, + "encryptBrandId": "d283b66de3cefd891H1529q5Flc~", // 公司ID + "brandName": "上海大裂谷智能科技", // 公司名称 + "brandScaleName": "100-499人", // 公司规模 + "brandIndustry": "人工智能", // 公司行业 + "brandStageName": "天使轮", // 融资阶段 + "bossName": "杨明雨", // Boss姓名 + "bossTitle": "HR", // Boss职位 + "bossOnline": true, // Boss是否在线 + "jobLabels": ["在校/应届", "学历不限"], // 职位标签 + "skills": [], // 技能要求 + "welfareList": ["带薪年假", "五险一金"], // 福利列表 + "proxyJob": 0, // 是否外包(0否1是) + "industry": 100028 // 行业代码 + } + ] + } +} +``` + +### 关键字段说明 + +1. **投递必需字段**: + - `encryptJobId`: 职位ID,投递时必须 + - `encryptBossId`: Boss ID,投递时必须 + - `securityId`: 安全ID,投递时必须(每次搜索可能不同) + +2. **位置信息**: + - `gps.longitude` 和 `gps.latitude`: 直接使用,无需调用位置服务API + - 如果没有gps字段,再使用 `cityName + areaDistrict + businessDistrict + brandName` 调用位置服务 + +3. **薪资解析**: + - `salaryDesc` 格式多样:`"25-50K·19薪"`、`"20-30K"`、`"面议"` 等 + - 需要解析出 `salaryMin` 和 `salaryMax`(单位:元) + +4. **工作年限解析**: + - `jobExperience` 可能为:`"在校/应届"`、`"3-5年"`、`"1-3年"` 等 + - 需要解析出 `experienceMin` 和 `experienceMax` + ## 📊 功能架构 ``` @@ -244,13 +313,81 @@ async createDeliverTask(params) { **任务内容**: 1. 完善字段映射(从Boss直聘响应数据提取更多字段) -2. 优化位置解析(减少API调用,添加缓存) -3. 添加职位状态管理 -4. 添加职位匹配度字段 +2. 优化位置解析(优先使用响应中的gps字段,减少API调用) +3. 解析薪资范围(从salaryDesc提取min/max) +4. 解析工作年限(从jobExperience提取min/max) +5. 添加职位状态管理 +6. 添加职位匹配度字段 + +**Boss直聘响应数据字段映射表**: + +| 响应字段 | 数据库字段 | 说明 | 示例值 | +|---------|-----------|------|--------| +| `encryptJobId` | `jobId` | 职位ID(加密) | "5ae70dfe114c23ab0nR-2ti-FFpU" | +| `jobName` | `jobTitle` | 职位名称 | "全栈软件工程师" | +| `encryptBrandId` | `companyId` | 公司ID(加密) | "d283b66de3cefd891H1529q5Flc~" | +| `brandName` | `companyName` | 公司名称 | "上海大裂谷智能科技" | +| `brandScaleName` | `companySize` | 公司规模 | "100-499人" | +| `brandIndustry` | `companyIndustry` | 公司行业 | "人工智能" | +| `brandStageName` | `brandStage` | 融资阶段 | "天使轮" | +| `salaryDesc` | `salary` | 薪资描述 | "25-50K·19薪" | +| `salaryDesc` | `salaryMin`, `salaryMax` | 薪资范围(需解析) | 25000, 50000 | +| `jobExperience` | `experience` | 工作经验 | "在校/应届" | +| `jobExperience` | `experienceMin`, `experienceMax` | 工作年限范围(需解析) | - | +| `jobDegree` | `education` | 学历要求 | "学历不限" | +| `jobDegree` | `educationLevel` | 学历等级(需映射) | - | +| `city` | `city` | 城市代码 | 101020100 | +| `cityName` | `cityName` | 城市名称 | "上海" | +| `areaDistrict` | `areaDistrict` | 区域 | "长宁区" | +| `businessDistrict` | `businessDistrict` | 商圈 | "新华路" | +| `gps.longitude` | `longitude` | 经度(优先使用) | 121.41902537687392 | +| `gps.latitude` | `latitude` | 纬度(优先使用) | 31.210308153576566 | +| `encryptBossId` | `encryptBossId` | Boss ID(投递需要) | "b55854108ac215180XZ62N-_FlNT" | +| `securityId` | `securityId` | 安全ID(投递需要) | "HP23zbQfaslvy-c1..." | +| `bossName` | `bossName` | Boss姓名 | "杨明雨" | +| `bossTitle` | `bossTitle` | Boss职位 | "HR" | +| `bossOnline` | `bossOnline` | Boss是否在线 | true | +| `jobLabels` | `jobLabels` | 职位标签(JSON) | ["在校/应届", "学历不限"] | +| `skills` | `skills` | 技能要求(JSON) | ["Java", "MySQL"] | +| `welfareList` | `welfareList` | 福利列表(JSON) | ["带薪年假", "五险一金"] | +| `proxyJob` | `isOutsourcing` | 是否外包 | 0/1 | +| `industry` | `industry` | 行业代码 | 100028 | + +**关键优化点**: + +1. **位置信息**: 优先使用响应中的 `gps.longitude` 和 `gps.latitude`,避免调用位置服务API + - 如果 `gps` 字段存在,直接使用 + - 如果不存在,再使用 `cityName + areaDistrict + businessDistrict + brandName` 调用位置服务 + +2. **薪资解析**: 从 `salaryDesc` 解析薪资范围 + - 格式示例:`"25-50K·19薪"` → min: 25000, max: 50000 + - 格式示例:`"20-30K"` → min: 20000, max: 30000 + - 格式示例:`"面议"` → min: 0, max: 0 + - 格式示例:`"15K以上"` → min: 15000, max: 999999 + - 需要处理:K(千)、W(万)、薪(年终奖倍数) + +3. **工作年限解析**: 从 `jobExperience` 解析年限范围 + - `"在校/应届"` → min: 0, max: 0 + - `"1-3年"` → min: 1, max: 3 + - `"3-5年"` → min: 3, max: 5 + - `"5-10年"` → min: 5, max: 10 + - `"10年以上"` → min: 10, max: 99 + +4. **学历映射**: 将学历描述映射为等级 + - `"学历不限"` → `"unlimited"` + - `"高中"` → `"high_school"` + - `"大专"` → `"college"` + - `"本科"` → `"bachelor"` + - `"硕士"` → `"master"` + - `"博士"` → `"doctor"` + +5. **投递必需字段**: 确保保存 `encryptJobId`、`encryptBossId` 和 `securityId` + - 这些字段在投递时必须使用 + - `securityId` 每次搜索可能不同,需要实时保存 **代码位置**: 第215-308行 -**预计工作量**: 3小时 +**预计工作量**: 4小时(增加字段解析逻辑) --- @@ -291,48 +428,67 @@ async createDeliverTask(params) { --- -### 任务5: 创建搜索任务接口 +### 任务5: 创建搜索任务接口(支持可选投递) **文件**: `api/services/pla_account_service.js` **新增方法**: `createSearchJobListTask()` +**方法签名**: +```javascript +/** + * 创建搜索职位列表任务(支持可选投递) + * @param {Object} params - 任务参数 + * @param {number} params.id - 账号ID + * @param {string} params.keyword - 搜索关键词 + * @param {Object} params.searchParams - 搜索条件(城市、薪资、经验、学历等) + * @param {number} params.pageCount - 获取页数 + * @param {boolean} params.autoDeliver - 是否自动投递(默认false) + * @param {Object} params.filterRules - 过滤规则(autoDeliver=true时使用) + * @param {number} params.maxCount - 最大投递数量(autoDeliver=true时使用) + * @returns {Promise} 任务创建结果 { taskId, message, jobCount, deliveredCount } + */ +async createSearchJobListTask(params) { + // 1. 验证账号和授权 + // 2. 创建任务记录 (taskType: 'search_jobs' 或 'auto_deliver') + // 3. 生成搜索指令 + // 4. 执行搜索指令 + // 5. 等待搜索完成(职位会自动保存到数据库) + // 6. 如果 autoDeliver=true: + // - 从数据库获取刚搜索到的职位列表 + // - 根据简历信息和过滤规则匹配职位 + // - 生成投递指令序列 + // - 执行投递指令(带间隔控制) + // - 保存投递记录 + // - 更新职位状态 + // 7. 返回任务信息 +} +``` + **任务内容**: 1. 验证账号和授权 -2. 创建任务记录 -3. 生成搜索指令 -4. 执行指令 -5. 返回任务信息 +2. 创建任务记录(根据autoDeliver参数设置taskType: 'search_jobs' 或 'auto_deliver') +3. 生成搜索指令(command_type: 'get_job_list') +4. 执行搜索指令(通过MQTT发送到设备) +5. 等待搜索完成(职位会自动保存到数据库) +6. 如果 `autoDeliver=true`,继续执行投递流程: + - 从数据库获取刚搜索到的职位列表(applyStatus = 'pending') + - 根据简历信息和过滤规则匹配职位(距离、薪资、工作年限、学历等) + - 为每个匹配的职位生成投递指令(command_type: 'apply_job') + - 批量执行投递指令(带间隔控制,避免频繁投递) + - 保存投递记录 (apply_records) + - 更新职位状态 (job_postings.applyStatus = 'applied') +7. 返回任务信息(包含搜索到的职位数量和投递数量) **代码位置**: 在 `runCommand()` 方法后添加 -**预计工作量**: 3小时 +**预计工作量**: 5小时(增加投递逻辑) --- -### 任务6: 创建投递任务接口 - -**文件**: `api/services/pla_account_service.js` - -**新增方法**: `createDeliverTask()` - -**任务内容**: -1. 验证账号和授权 -2. 创建任务记录 -3. 生成搜索指令(获取职位列表) -4. 等待搜索完成 -5. 获取匹配的职位 -6. 生成投递指令序列 -7. 执行投递指令 -8. 返回任务信息 - -**代码位置**: 在 `createSearchJobListTask()` 方法后添加 - -**预计工作量**: 4小时 - --- -### 任务7: 完善指令类型映射 +### 任务6: 完善指令类型映射 **文件**: `api/middleware/schedule/command.js` @@ -349,7 +505,7 @@ async createDeliverTask(params) { --- -### 任务8: 添加搜索条件配置管理 +### 任务7: 添加搜索条件配置管理 **文件**: `api/model/pla_account.js` @@ -376,16 +532,18 @@ async createDeliverTask(params) { ## 🔄 工作流程 -### 搜索职位列表流程 +### 搜索职位列表流程(支持可选投递) ``` 1. 用户/系统调用 createSearchJobListTask() + - 参数: { id, keyword, searchParams, pageCount, autoDeliver: true/false, filterRules, maxCount } ↓ 2. 创建任务记录 (task_status) + - taskType: 'search_jobs' 或 'auto_deliver'(根据autoDeliver参数) ↓ 3. 生成搜索指令 (task_commands) - command_type: 'get_job_list' - - command_params: { keyword, city, salary, ... } + - command_params: { keyword, city, salary, experience, education, ... } ↓ 4. 执行指令 (通过MQTT发送到设备) ↓ @@ -393,62 +551,79 @@ async createDeliverTask(params) { ↓ 6. 保存职位到数据库 (job_postings) - 去重处理 - - 位置解析 + - 位置解析(优先使用gps字段) - 字段映射 + - 状态: applyStatus = 'pending'(待投递) ↓ -7. 更新指令状态为完成 +7. 更新搜索指令状态为完成 ↓ -8. 更新任务状态为完成 +8. 如果 autoDeliver=true,继续执行投递流程: + ↓ + 8.1 从数据库获取刚搜索到的职位列表 + - 筛选条件: applyStatus = 'pending', sn_code = 账号SN码 + ↓ + 8.2 根据简历信息和过滤规则匹配职位 + - 距离匹配(基于经纬度) + - 薪资匹配(解析salaryDesc) + - 工作年限匹配(解析jobExperience) + - 学历匹配(解析jobDegree) + - 权重评分 + ↓ + 8.3 为每个匹配的职位生成投递指令 + - command_type: 'apply_job' + - command_params: { + jobId: job.encryptJobId, // 职位ID(必需) + encryptBossId: job.encryptBossId, // Boss ID(必需) + securityId: job.securityId, // 安全ID(必需,从最新搜索结果获取) + brandName: job.brandName, // 公司名称(可选) + jobTitle: job.jobName // 职位名称(可选) + } + ↓ + 8.4 批量执行投递指令(带间隔控制,避免频繁投递) + ↓ + 8.5 保存投递记录 (apply_records) + ↓ + 8.6 更新职位状态 (job_postings.applyStatus = 'applied') + ↓ +9. 更新任务状态为完成 + ↓ +10. 返回任务信息(包含搜索到的职位数量和投递数量) ``` -### 投递职位流程 - -``` -1. 用户/系统调用 createDeliverTask() - ↓ -2. 创建任务记录 (task_status) - ↓ -3. 生成搜索指令 (获取职位列表) - - command_type: 'get_job_list' - ↓ -4. 执行搜索指令 - ↓ -5. 获取职位列表并保存到数据库 - ↓ -6. 根据简历信息和过滤规则匹配职位 - - 距离匹配 - - 薪资匹配 - - 工作年限匹配 - - 学历匹配 - - 权重评分 - ↓ -7. 为每个匹配的职位生成投递指令 - - command_type: 'apply_job' - - command_params: { jobId, encryptBossId, ... } - ↓ -8. 批量执行投递指令(带间隔控制) - ↓ -9. 保存投递记录 (apply_records) - ↓ -10. 更新职位状态 (job_postings.applyStatus) - ↓ -11. 更新任务状态为完成 -``` +**说明**: +- 此接口支持两种模式: + - `autoDeliver=false`: 仅搜索,不投递。职位保存到数据库,状态为'pending' + - `autoDeliver=true`: 搜索完成后立即投递匹配的职位 +- **重要**: 投递必须在搜索完成后立即执行,因为 `securityId` 等字段可能有时效性,前端页面变化后这些字段可能失效 +- 不支持从已保存的职位中选择投递,因为职位信息可能已过期 ## 📊 数据库字段说明 ### job_postings 表需要完善的字段 -| 字段名 | 类型 | 说明 | 状态 | -|--------|------|------|------| -| `city` | VARCHAR | 城市代码 | 待添加 | -| `cityName` | VARCHAR | 城市名称 | 待添加 | -| `salaryMin` | INT | 最低薪资(元) | 待添加 | -| `salaryMax` | INT | 最高薪资(元) | 待添加 | -| `experienceMin` | INT | 最低工作年限 | 待添加 | -| `experienceMax` | INT | 最高工作年限 | 待添加 | -| `educationLevel` | VARCHAR | 学历等级 | 待添加 | -| `matchScore` | DECIMAL | 匹配度评分 | 待添加 | +| 字段名 | 类型 | 说明 | 状态 | 数据来源 | +|--------|------|------|------|----------| +| `city` | VARCHAR | 城市代码 | 待添加 | `job.city` | +| `cityName` | VARCHAR | 城市名称 | 待添加 | `job.cityName` | +| `areaDistrict` | VARCHAR | 区域 | 待添加 | `job.areaDistrict` | +| `businessDistrict` | VARCHAR | 商圈 | 待添加 | `job.businessDistrict` | +| `salaryMin` | INT | 最低薪资(元) | 待添加 | 从 `salaryDesc` 解析 | +| `salaryMax` | INT | 最高薪资(元) | 待添加 | 从 `salaryDesc` 解析 | +| `experienceMin` | INT | 最低工作年限 | 待添加 | 从 `jobExperience` 解析 | +| `experienceMax` | INT | 最高工作年限 | 待添加 | 从 `jobExperience` 解析 | +| `educationLevel` | VARCHAR | 学历等级 | 待添加 | 从 `jobDegree` 映射 | +| `matchScore` | DECIMAL | 匹配度评分 | 待添加 | 计算得出 | +| `encryptBossId` | VARCHAR | Boss ID | 已有 | `job.encryptBossId` | +| `securityId` | VARCHAR | 安全ID | 待添加 | `job.securityId`(投递必需) | +| `bossName` | VARCHAR | Boss姓名 | 待添加 | `job.bossName` | +| `bossTitle` | VARCHAR | Boss职位 | 待添加 | `job.bossTitle` | +| `bossOnline` | TINYINT | Boss是否在线 | 待添加 | `job.bossOnline` | +| `brandStage` | VARCHAR | 融资阶段 | 待添加 | `job.brandStageName` | +| `jobLabels` | JSON | 职位标签 | 待添加 | `job.jobLabels` | +| `skills` | JSON | 技能要求 | 待添加 | `job.skills` | +| `welfareList` | JSON | 福利列表 | 待添加 | `job.welfareList` | +| `isOutsourcing` | TINYINT | 是否外包 | 待添加 | `job.proxyJob` | +| `industry` | INT | 行业代码 | 待添加 | `job.industry` | ### pla_account 表需要添加的字段 @@ -483,15 +658,14 @@ async createDeliverTask(params) { | 任务 | 预计时间 | 优先级 | |------|----------|--------| | 任务1: 完善搜索参数支持 | 2小时 | 高 | -| 任务2: 优化职位数据保存 | 3小时 | 高 | +| 任务2: 优化职位数据保存 | 4小时 | 高 | | 任务3: 完善自动投递任务搜索条件 | 2小时 | 高 | | 任务4: 优化职位匹配算法 | 4小时 | 高 | -| 任务5: 创建搜索任务接口 | 3小时 | 中 | -| 任务6: 创建投递任务接口 | 4小时 | 中 | -| 任务7: 完善指令类型映射 | 1小时 | 中 | -| 任务8: 添加搜索条件配置管理 | 1小时 | 低 | +| 任务5: 创建搜索任务接口(支持可选投递) | 5小时 | 高 | +| 任务6: 完善指令类型映射 | 1小时 | 中 | +| 任务7: 添加搜索条件配置管理 | 1小时 | 低 | -**总计**: 约20小时 +**总计**: 约19小时 ## 🚀 开发优先级 @@ -500,14 +674,70 @@ async createDeliverTask(params) { 2. 任务2: 优化职位数据保存 3. 任务3: 完善自动投递任务搜索条件 4. 任务4: 优化职位匹配算法 +5. 任务5: 创建搜索任务接口(支持可选投递) ### 第二阶段(接口完善) -5. 任务5: 创建搜索任务接口 -6. 任务6: 创建投递任务接口 -7. 任务7: 完善指令类型映射 +6. 任务6: 完善指令类型映射 ### 第三阶段(配置管理) -8. 任务8: 添加搜索条件配置管理 +7. 任务7: 添加搜索条件配置管理 + +## 💡 使用场景说明 + +### 场景1: 仅搜索职位列表 +```javascript +// 只搜索职位,不投递 +const result = await plaAccountService.createSearchJobListTask({ + id: accountId, + keyword: '全栈工程师', + searchParams: { + city: '101020100', + cityName: '上海', + salary: '20-30K', + experience: '3-5年', + education: '本科' + }, + pageCount: 3, + autoDeliver: false // 不自动投递 +}); + +// 返回: { taskId: 123, message: '搜索任务已创建', jobCount: 45 } +// 职位会自动保存到数据库,状态为 'pending'(待投递) +``` + +### 场景2: 搜索并自动投递(推荐) +```javascript +// 搜索职位并自动投递匹配的职位 +const result = await plaAccountService.createSearchJobListTask({ + id: accountId, + keyword: '全栈工程师', + searchParams: { + city: '101020100', + cityName: '上海', + salary: '20-30K', + experience: '3-5年', + education: '本科' + }, + pageCount: 3, + autoDeliver: true, // 自动投递 + filterRules: { + minSalary: 20000, + maxSalary: 30000, + keywords: ['Vue', 'React'], + excludeKeywords: ['外包', '外派'] + }, + maxCount: 10 // 最多投递10个职位 +}); + +// 返回: { taskId: 123, message: '搜索并投递任务已创建', jobCount: 45, deliveredCount: 8 } +``` + +**重要说明**: +- **投递必须在搜索完成后立即执行**,因为 `securityId` 等字段可能有时效性 +- 前端页面变化后,已保存的职位信息中的 `securityId` 可能失效,无法用于投递 +- 因此不支持从已保存的职位中选择投递,必须在搜索后立即投递 +- 如果只需要搜索不投递,设置 `autoDeliver: false` +- 如果需要搜索并投递,设置 `autoDeliver: true`,系统会根据匹配规则自动投递 ## 📌 注意事项 @@ -517,6 +747,11 @@ async createDeliverTask(params) { 4. **性能优化**: 批量操作需要考虑性能,避免阻塞 5. **MQTT通信**: 确保指令参数格式正确,与客户端协议一致 6. **数据库事务**: 批量操作需要使用事务保证数据一致性 +7. **投递时机**: 投递必须在搜索完成后立即执行,因为 `securityId` 等字段可能有时效性,前端页面变化后这些字段可能失效 +8. **职位状态验证**: 投递前必须验证职位状态(applyStatus = 'pending'),避免重复投递 +9. **投递必需字段**: 投递时需要 `encryptJobId`、`encryptBossId` 和 `securityId`,这些字段必须从最新搜索结果中获取 +10. **位置信息**: 优先使用响应中的 `gps` 字段,避免不必要的API调用 +11. **接口设计**: 搜索和投递在同一接口中完成,不支持单独的投递接口,因为已保存的职位信息可能已过期 ## 🔗 相关文件 diff --git a/api/middleware/job/jobManager.js b/api/middleware/job/jobManager.js index aa36a13..ded0ead 100644 --- a/api/middleware/job/jobManager.js +++ b/api/middleware/job/jobManager.js @@ -143,6 +143,256 @@ class JobManager { return response.data; } + /** + * 多条件搜索职位列表(新指令) + * @param {string} sn_code - 设备SN码 + * @param {object} mqttClient - MQTT客户端 + * @param {object} params - 搜索参数 + * @returns {Promise} 搜索结果 + */ + async search_jobs_with_params(sn_code, mqttClient, params = {}) { + const { + keyword = '前端', + platform = 'boss', + city = '', + cityName = '', + salary = '', + experience = '', + education = '', + industry = '', + companySize = '', + financingStage = '', + page = 1, + pageSize = 20, + pageCount = 3 + } = params; + + console.log(`[工作管理] 开始多条件搜索设备 ${sn_code} 的职位,关键词: ${keyword}, 城市: ${cityName || city}`); + + // 构建完整的搜索参数对象 + const searchData = { + keyword, + pageCount + }; + + // 添加可选搜索条件 + if (city) searchData.city = city; + if (cityName) searchData.cityName = cityName; + if (salary) searchData.salary = salary; + if (experience) searchData.experience = experience; + if (education) searchData.education = education; + if (industry) searchData.industry = industry; + if (companySize) searchData.companySize = companySize; + if (financingStage) searchData.financingStage = financingStage; + if (page) searchData.page = page; + if (pageSize) searchData.pageSize = pageSize; + + // 通过MQTT指令获取岗位列表(使用新的action) + const response = await mqttClient.publishAndWait(sn_code, { + platform, + action: "search_job_list", // 新的搜索action + data: searchData + }); + + if (!response || response.code !== 200) { + console.error(`[工作管理] 多条件搜索职位失败:`, response); + throw new Error('多条件搜索职位失败'); + } + + // 处理职位列表数据 + let jobs = []; + if (Array.isArray(response.data)) { + for (const item of response.data) { + if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) { + jobs = jobs.concat(item.data.zpData.jobList); + } + } + } else if (response.data?.data?.zpData?.jobList) { + jobs = response.data.data.zpData.jobList || []; + } else if (response.data?.zpData?.jobList) { + jobs = response.data.zpData.jobList || []; + } + + console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`); + + // 保存职位到数据库 + try { + await this.saveJobsToDatabase(sn_code, platform, keyword, jobs); + } catch (error) { + console.error(`[工作管理] 保存职位到数据库失败:`, error); + } + + return { + jobs: jobs, + keyword: keyword, + platform: platform, + count: jobs.length + }; + } + + /** + * 搜索并投递职位(新指令) + * @param {string} sn_code - 设备SN码 + * @param {object} mqttClient - MQTT客户端 + * @param {object} params - 参数 + * @returns {Promise} 执行结果 + */ + async search_and_deliver(sn_code, mqttClient, params = {}) { + const { + keyword, + searchParams = {}, + pageCount = 3, + filterRules = {}, + maxCount = 10, + platform = 'boss' + } = params; + + console.log(`[工作管理] 开始搜索并投递职位,设备: ${sn_code}, 关键词: ${keyword}`); + + // 1. 先执行搜索 + const searchResult = await this.search_jobs_with_params(sn_code, mqttClient, { + keyword, + platform, + ...searchParams, + pageCount + }); + + if (!searchResult || searchResult.count === 0) { + return { + success: true, + jobCount: 0, + deliveredCount: 0, + message: '未找到职位' + }; + } + + // 2. 等待数据保存完成 + await new Promise(resolve => setTimeout(resolve, 2000)); + + // 3. 从数据库获取刚搜索到的职位 + const job_postings = db.getModel('job_postings'); + const searchedJobs = await job_postings.findAll({ + where: { + sn_code: sn_code, + platform: platform, + applyStatus: 'pending', + keyword: keyword + }, + order: [['create_time', 'DESC']], + limit: 1000 + }); + + if (searchedJobs.length === 0) { + return { + success: true, + jobCount: searchResult.count, + deliveredCount: 0, + message: '未找到待投递的职位' + }; + } + + // 4. 获取简历信息用于匹配 + const resume_info = db.getModel('resume_info'); + const resume = await resume_info.findOne({ + where: { + sn_code: sn_code, + platform: platform, + isActive: true + }, + order: [['last_modify_time', 'DESC']] + }); + + if (!resume) { + return { + success: true, + jobCount: searchResult.count, + deliveredCount: 0, + message: '未找到活跃简历,无法投递' + }; + } + + // 5. 获取账号配置 + const pla_account = db.getModel('pla_account'); + const account = await pla_account.findOne({ + where: { sn_code, platform_type: platform } + }); + + if (!account) { + throw new Error('账号不存在'); + } + + const accountConfig = account.toJSON(); + const resumeData = resume.toJSON(); + + // 6. 使用过滤方法进行职位匹配 + const matchedJobs = await this.filter_jobs_by_rules(searchedJobs, { + minSalary: filterRules.minSalary || 0, + maxSalary: filterRules.maxSalary || 0, + keywords: filterRules.keywords || [], + excludeKeywords: filterRules.excludeKeywords || [], + accountConfig: accountConfig, + resumeInfo: resumeData + }); + + // 7. 限制投递数量 + const jobsToDeliver = matchedJobs.slice(0, maxCount); + console.log(`[工作管理] 匹配到 ${matchedJobs.length} 个职位,将投递 ${jobsToDeliver.length} 个`); + + // 8. 执行投递 + let deliveredCount = 0; + const apply_records = db.getModel('apply_records'); + + for (let i = 0; i < jobsToDeliver.length; i++) { + const job = jobsToDeliver[i]; + const jobData = job.toJSON ? job.toJSON() : job; + + try { + // 从原始数据中获取 securityId + let securityId = jobData.securityId || ''; + try { + if (jobData.originalData) { + const originalData = typeof jobData.originalData === 'string' + ? JSON.parse(jobData.originalData) + : jobData.originalData; + securityId = originalData.securityId || securityId; + } + } catch (e) { + console.warn(`[工作管理] 解析职位原始数据失败:`, e); + } + + // 执行投递 + const deliverResult = await this.applyJob(sn_code, mqttClient, { + jobId: jobData.jobId, + encryptBossId: jobData.encryptBossId || '', + securityId: securityId, + brandName: jobData.companyName || '', + jobTitle: jobData.jobTitle || '', + companyName: jobData.companyName || '', + platform: platform + }); + + if (deliverResult && deliverResult.success) { + deliveredCount++; + } + + // 投递间隔控制 + if (i < jobsToDeliver.length - 1) { + await new Promise(resolve => setTimeout(resolve, 3000)); + } + } catch (error) { + console.error(`[工作管理] 投递职位失败:`, error); + // 继续投递下一个职位 + } + } + + return { + success: true, + jobCount: searchResult.count, + deliveredCount: deliveredCount, + message: `搜索完成,找到 ${searchResult.count} 个职位,成功投递 ${deliveredCount} 个` + }; + } + /** * 获取岗位列表 * @param {string} sn_code - 设备SN码 diff --git a/api/middleware/schedule/taskHandlers.js b/api/middleware/schedule/taskHandlers.js index 40300e6..f654b1b 100644 --- a/api/middleware/schedule/taskHandlers.js +++ b/api/middleware/schedule/taskHandlers.js @@ -24,6 +24,11 @@ class TaskHandlers { return await this.handleAutoDeliverTask(task); }); + // 搜索职位列表任务(新功能) + taskQueue.registerHandler('search_jobs', async (task) => { + return await this.handleSearchJobListTask(task); + }); + // 自动沟通任务(待实现) taskQueue.registerHandler('auto_chat', async (task) => { return await this.handleAutoChatTask(task); @@ -547,6 +552,180 @@ class TaskHandlers { return { allowed: true, reason: '在允许的时间范围内' }; } + /** + * 处理搜索职位列表任务(新功能) + * 支持多条件搜索和可选投递 + * @param {object} task - 任务对象 + * @returns {Promise} 执行结果 + */ + async handleSearchJobListTask(task) { + const { sn_code, taskParams } = task; + const { + keyword, + searchParams = {}, + pageCount = 3, + autoDeliver = false, + filterRules = {}, + maxCount = 10 + } = taskParams; + + console.log(`[任务处理器] 搜索职位列表任务 - 设备: ${sn_code}, 关键词: ${keyword}, 自动投递: ${autoDeliver}`); + + // 检查授权状态 + const authorizationService = require('../../services/authorization_service'); + const authCheck = await authorizationService.checkAuthorization(sn_code, 'sn_code'); + if (!authCheck.is_authorized) { + console.log(`[任务处理器] 搜索职位列表任务 - 设备: ${sn_code} 授权检查失败: ${authCheck.message}`); + return { + success: false, + jobCount: 0, + deliveredCount: 0, + message: authCheck.message + }; + } + + deviceManager.recordTaskStart(sn_code, task); + const startTime = Date.now(); + + try { + const job_postings = db.getModel('job_postings'); + const pla_account = db.getModel('pla_account'); + const resume_info = db.getModel('resume_info'); + const apply_records = db.getModel('apply_records'); + const Sequelize = require('sequelize'); + + // 1. 获取账号配置 + const account = await pla_account.findOne({ + where: { sn_code, platform_type: taskParams.platform || 'boss' } + }); + + if (!account) { + throw new Error('账号不存在'); + } + + const accountConfig = account.toJSON(); + + // 2. 从账号配置中读取搜索条件 + const searchConfig = accountConfig.search_config + ? (typeof accountConfig.search_config === 'string' + ? JSON.parse(accountConfig.search_config) + : accountConfig.search_config) + : {}; + + // 3. 构建完整的搜索参数(任务参数优先,其次账号配置) + const searchCommandParams = { + sn_code: sn_code, + platform: taskParams.platform || accountConfig.platform_type || 'boss', + keyword: keyword || accountConfig.keyword || searchConfig.keyword || '', + city: searchParams.city || accountConfig.city || searchConfig.city || '', + cityName: searchParams.cityName || accountConfig.cityName || searchConfig.cityName || '', + salary: searchParams.salary || searchConfig.defaultSalary || '', + experience: searchParams.experience || searchConfig.defaultExperience || '', + education: searchParams.education || searchConfig.defaultEducation || '', + industry: searchParams.industry || searchConfig.industry || '', + companySize: searchParams.companySize || searchConfig.companySize || '', + financingStage: searchParams.financingStage || searchConfig.financingStage || '', + page: 1, + pageSize: 20, + pageCount: pageCount + }; + + // 4. 根据是否投递选择不同的指令 + let searchCommand; + if (autoDeliver) { + // 使用搜索并投递指令 + searchCommand = { + command_type: 'search_and_deliver', + command_name: '搜索并投递职位', + command_params: JSON.stringify({ + keyword: searchCommandParams.keyword, + searchParams: { + city: searchCommandParams.city, + cityName: searchCommandParams.cityName, + salary: searchCommandParams.salary, + experience: searchCommandParams.experience, + education: searchCommandParams.education, + industry: searchCommandParams.industry, + companySize: searchCommandParams.companySize, + financingStage: searchCommandParams.financingStage, + page: searchCommandParams.page, + pageSize: searchCommandParams.pageSize, + pageCount: searchCommandParams.pageCount + }, + filterRules: filterRules, + maxCount: maxCount, + platform: searchCommandParams.platform + }), + priority: config.getTaskPriority('search_and_deliver') || 5, + sequence: 1 + }; + } else { + // 使用多条件搜索指令 + searchCommand = { + command_type: 'search_jobs_with_params', + command_name: '多条件搜索职位列表', + command_params: JSON.stringify(searchCommandParams), + priority: config.getTaskPriority('search_jobs_with_params') || 5, + sequence: 1 + }; + } + + // 5. 执行指令 + const commandResult = await command.executeCommands(task.id, [searchCommand], this.mqttClient); + + // 6. 处理执行结果 + let jobCount = 0; + let deliveredCount = 0; + + if (autoDeliver) { + // 如果使用 search_and_deliver 指令,结果中已包含投递信息 + if (commandResult && commandResult.results && commandResult.results.length > 0) { + const result = commandResult.results[0].result; + if (result) { + jobCount = result.jobCount || 0; + deliveredCount = result.deliveredCount || 0; + } + } + } else { + // 如果使用 search_jobs_with_params 指令,等待搜索完成并从数据库获取结果 + await new Promise(resolve => setTimeout(resolve, 2000)); + + const searchedJobs = await job_postings.findAll({ + where: { + sn_code: sn_code, + platform: searchCommandParams.platform, + applyStatus: 'pending', + keyword: searchCommandParams.keyword + }, + order: [['create_time', 'DESC']], + limit: 1000 + }); + + jobCount = searchedJobs.length; + } + + const duration = Date.now() - startTime; + deviceManager.recordTaskComplete(sn_code, task, true, duration); + + console.log(`[任务处理器] 搜索职位列表任务完成 - 设备: ${sn_code}, 找到 ${jobCount} 个职位, 投递 ${deliveredCount} 个, 耗时: ${duration}ms`); + + return { + success: true, + jobCount: jobCount, + deliveredCount: deliveredCount, + message: autoDeliver + ? `搜索完成,找到 ${jobCount} 个职位,成功投递 ${deliveredCount} 个` + : `搜索完成,找到 ${jobCount} 个职位` + }; + + } catch (error) { + const duration = Date.now() - startTime; + deviceManager.recordTaskComplete(sn_code, task, false, duration); + console.error(`[任务处理器] 搜索职位列表任务失败 - 设备: ${sn_code}:`, error); + throw error; + } + } + /** * 处理自动沟通任务(待实现) * 功能:自动与HR进行沟通,回复消息等 diff --git a/api/services/pla_account_service.js b/api/services/pla_account_service.js index a6deb6e..c40e5fc 100644 --- a/api/services/pla_account_service.js +++ b/api/services/pla_account_service.js @@ -680,13 +680,120 @@ class PlaAccountService { } }); + const taskId = await scheduleManager.taskQueue.addTask(account.sn_code, { + taskType: taskType, + taskName: `手动任务 - ${taskName}`, + taskParams: { + keyword: account.keyword, + platform: account.platform_type + } + }); + return { message: '任务已添加到队列', - taskId: task.id + taskId: taskId }; + } - - + /** + * 创建搜索职位列表任务(支持可选投递) + * @param {Object} params - 任务参数 + * @param {number} params.id - 账号ID + * @param {string} params.keyword - 搜索关键词 + * @param {Object} params.searchParams - 搜索条件(城市、薪资、经验、学历等) + * @param {number} params.pageCount - 获取页数 + * @param {boolean} params.autoDeliver - 是否自动投递(默认false) + * @param {Object} params.filterRules - 过滤规则(autoDeliver=true时使用) + * @param {number} params.maxCount - 最大投递数量(autoDeliver=true时使用) + * @returns {Promise} 任务创建结果 { taskId, message, jobCount, deliveredCount } + */ + async createSearchJobListTask(params) { + const pla_account = db.getModel('pla_account'); + const task_status = db.getModel('task_status'); + + const { + id, + keyword, + searchParams = {}, + pageCount = 3, + autoDeliver = false, + filterRules = {}, + maxCount = 10 + } = params; + + // 1. 验证账号和授权 + if (!id) { + throw new Error('账号ID不能为空'); + } + + const account = await pla_account.findByPk(id); + if (!account) { + throw new Error('账号不存在'); + } + + // 检查账号是否启用 + if (!account.is_enabled) { + throw new Error('账号未启用,无法执行任务'); + } + + // 检查授权状态 + const authCheck = await authorizationService.checkAuthorization(id, 'id'); + if (!authCheck.is_authorized) { + throw new Error(authCheck.message); + } + + // 检查MQTT客户端 + if (!scheduleManager.mqttClient) { + throw new Error('MQTT客户端未初始化'); + } + + const sn_code = account.sn_code; + const platform = account.platform_type || 'boss'; + + // 2. 创建任务记录(使用新的搜索任务类型) + const taskType = 'search_jobs'; + const taskName = autoDeliver ? '搜索并投递职位' : '搜索职位列表'; + + const task = await task_status.create({ + sn_code: sn_code, + taskType: taskType, + taskName: taskName, + taskParams: JSON.stringify({ + keyword: keyword || account.keyword || '', + searchParams: searchParams, + pageCount: pageCount, + autoDeliver: autoDeliver, + filterRules: filterRules, + maxCount: maxCount, + platform: platform + }), + status: 'pending', + progress: 0 + }); + + console.log(`[账号服务] 创建搜索任务: ${taskName} (ID: ${task.id}, 设备: ${sn_code})`); + + // 3. 将任务添加到队列,由 handleSearchJobListTask 处理 + const taskId = await scheduleManager.taskQueue.addTask(sn_code, { + taskType: taskType, + taskName: taskName, + taskParams: { + keyword: keyword || account.keyword || '', + searchParams: searchParams, + pageCount: pageCount, + autoDeliver: autoDeliver, + filterRules: filterRules, + maxCount: maxCount, + platform: platform + } + }); + + return { + taskId: taskId, + message: `搜索任务已创建,任务ID: ${taskId}`, + jobCount: 0, + deliveredCount: 0 + }; } /**