Compare commits

..

2 Commits

Author SHA1 Message Date
张成
5035b9aa72 1 2025-12-29 21:07:52 +08:00
张成
8fa06435a9 1 2025-12-29 18:35:57 +08:00
6 changed files with 990 additions and 103 deletions

View File

@@ -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直聘响应数据提取更多字段 1. 完善字段映射从Boss直聘响应数据提取更多字段
2. 优化位置解析减少API调用,添加缓存 2. 优化位置解析(优先使用响应中的gps字段减少API调用
3. 添加职位状态管理 3. 解析薪资范围从salaryDesc提取min/max
4. 添加职位匹配度字段 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行 **代码位置**: 第215-308行
**预计工作量**: 3小时 **预计工作量**: 4小时(增加字段解析逻辑)
--- ---
@@ -291,48 +428,67 @@ async createDeliverTask(params) {
--- ---
### 任务5: 创建搜索任务接口 ### 任务5: 创建搜索任务接口(支持可选投递)
**文件**: `api/services/pla_account_service.js` **文件**: `api/services/pla_account_service.js`
**新增方法**: `createSearchJobListTask()` **新增方法**: `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<Object>} 任务创建结果 { taskId, message, jobCount, deliveredCount }
*/
async createSearchJobListTask(params) {
// 1. 验证账号和授权
// 2. 创建任务记录 (taskType: 'search_jobs' 或 'auto_deliver')
// 3. 生成搜索指令
// 4. 执行搜索指令
// 5. 等待搜索完成(职位会自动保存到数据库)
// 6. 如果 autoDeliver=true:
// - 从数据库获取刚搜索到的职位列表
// - 根据简历信息和过滤规则匹配职位
// - 生成投递指令序列
// - 执行投递指令(带间隔控制)
// - 保存投递记录
// - 更新职位状态
// 7. 返回任务信息
}
```
**任务内容**: **任务内容**:
1. 验证账号和授权 1. 验证账号和授权
2. 创建任务记录 2. 创建任务记录根据autoDeliver参数设置taskType: 'search_jobs' 或 'auto_deliver'
3. 生成搜索指令 3. 生成搜索指令command_type: 'get_job_list'
4. 执行指令 4. 执行搜索指令通过MQTT发送到设备
5. 返回任务信息 5. 等待搜索完成(职位会自动保存到数据库)
6. 如果 `autoDeliver=true`,继续执行投递流程:
- 从数据库获取刚搜索到的职位列表applyStatus = 'pending'
- 根据简历信息和过滤规则匹配职位(距离、薪资、工作年限、学历等)
- 为每个匹配的职位生成投递指令command_type: 'apply_job'
- 批量执行投递指令(带间隔控制,避免频繁投递)
- 保存投递记录 (apply_records)
- 更新职位状态 (job_postings.applyStatus = 'applied')
7. 返回任务信息(包含搜索到的职位数量和投递数量)
**代码位置**: 在 `runCommand()` 方法后添加 **代码位置**: 在 `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` **文件**: `api/middleware/schedule/command.js`
@@ -349,7 +505,7 @@ async createDeliverTask(params) {
--- ---
### 任务8: 添加搜索条件配置管理 ### 任务7: 添加搜索条件配置管理
**文件**: `api/model/pla_account.js` **文件**: `api/model/pla_account.js`
@@ -376,16 +532,18 @@ async createDeliverTask(params) {
## 🔄 工作流程 ## 🔄 工作流程
### 搜索职位列表流程 ### 搜索职位列表流程(支持可选投递)
``` ```
1. 用户/系统调用 createSearchJobListTask() 1. 用户/系统调用 createSearchJobListTask()
- 参数: { id, keyword, searchParams, pageCount, autoDeliver: true/false, filterRules, maxCount }
2. 创建任务记录 (task_status) 2. 创建任务记录 (task_status)
- taskType: 'search_jobs' 或 'auto_deliver'根据autoDeliver参数
3. 生成搜索指令 (task_commands) 3. 生成搜索指令 (task_commands)
- command_type: 'get_job_list' - command_type: 'get_job_list'
- command_params: { keyword, city, salary, ... } - command_params: { keyword, city, salary, experience, education, ... }
4. 执行指令 (通过MQTT发送到设备) 4. 执行指令 (通过MQTT发送到设备)
@@ -393,62 +551,79 @@ async createDeliverTask(params) {
6. 保存职位到数据库 (job_postings) 6. 保存职位到数据库 (job_postings)
- 去重处理 - 去重处理
- 位置解析 - 位置解析优先使用gps字段
- 字段映射 - 字段映射
- 状态: applyStatus = 'pending'(待投递)
7. 更新指令状态为完成 7. 更新搜索指令状态为完成
8. 更新任务状态为完成 8. 如果 autoDeliver=true继续执行投递流程:
```
### 投递职位流程
```
1. 用户/系统调用 createDeliverTask()
2. 创建任务记录 (task_status) 8.1 从数据库获取刚搜索到的职位列表
- 筛选条件: applyStatus = 'pending', sn_code = 账号SN码
3. 生成搜索指令 (获取职位列表) 8.2 根据简历信息和过滤规则匹配职位
- command_type: 'get_job_list' - 距离匹配(基于经纬度)
- 薪资匹配解析salaryDesc
4. 执行搜索指令 - 工作年限匹配解析jobExperience
- 学历匹配解析jobDegree
5. 获取职位列表并保存到数据库
6. 根据简历信息和过滤规则匹配职位
- 距离匹配
- 薪资匹配
- 工作年限匹配
- 学历匹配
- 权重评分 - 权重评分
7. 为每个匹配的职位生成投递指令 8.3 为每个匹配的职位生成投递指令
- command_type: 'apply_job' - command_type: 'apply_job'
- command_params: { jobId, encryptBossId, ... } - command_params: {
jobId: job.encryptJobId, // 职位ID必需
encryptBossId: job.encryptBossId, // Boss ID必需
securityId: job.securityId, // 安全ID必需从最新搜索结果获取
brandName: job.brandName, // 公司名称(可选)
jobTitle: job.jobName // 职位名称(可选)
}
8. 批量执行投递指令(带间隔控制) 8.4 批量执行投递指令(带间隔控制,避免频繁投递
9. 保存投递记录 (apply_records) 8.5 保存投递记录 (apply_records)
10. 更新职位状态 (job_postings.applyStatus) 8.6 更新职位状态 (job_postings.applyStatus = 'applied')
11. 更新任务状态为完成 9. 更新任务状态为完成
10. 返回任务信息(包含搜索到的职位数量和投递数量)
``` ```
**说明**:
- 此接口支持两种模式:
- `autoDeliver=false`: 仅搜索,不投递。职位保存到数据库,状态为'pending'
- `autoDeliver=true`: 搜索完成后立即投递匹配的职位
- **重要**: 投递必须在搜索完成后立即执行,因为 `securityId` 等字段可能有时效性,前端页面变化后这些字段可能失效
- 不支持从已保存的职位中选择投递,因为职位信息可能已过期
## 📊 数据库字段说明 ## 📊 数据库字段说明
### job_postings 表需要完善的字段 ### job_postings 表需要完善的字段
| 字段名 | 类型 | 说明 | 状态 | | 字段名 | 类型 | 说明 | 状态 | 数据来源 |
|--------|------|------|------| |--------|------|------|------|----------|
| `city` | VARCHAR | 城市代码 | 待添加 | | `city` | VARCHAR | 城市代码 | 待添加 | `job.city` |
| `cityName` | VARCHAR | 城市名称 | 待添加 | | `cityName` | VARCHAR | 城市名称 | 待添加 | `job.cityName` |
| `salaryMin` | INT | 最低薪资(元) | 待添加 | | `areaDistrict` | VARCHAR | 区域 | 待添加 | `job.areaDistrict` |
| `salaryMax` | INT | 最高薪资(元) | 待添加 | | `businessDistrict` | VARCHAR | 商圈 | 待添加 | `job.businessDistrict` |
| `experienceMin` | INT | 最低工作年限 | 待添加 | | `salaryMin` | INT | 最低薪资(元) | 待添加 | 从 `salaryDesc` 解析 |
| `experienceMax` | INT | 最高工作年限 | 待添加 | | `salaryMax` | INT | 最高薪资(元) | 待添加 | 从 `salaryDesc` 解析 |
| `educationLevel` | VARCHAR | 学历等级 | 待添加 | | `experienceMin` | INT | 最低工作年限 | 待添加 | 从 `jobExperience` 解析 |
| `matchScore` | DECIMAL | 匹配度评分 | 待添加 | | `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 表需要添加的字段 ### pla_account 表需要添加的字段
@@ -483,15 +658,14 @@ async createDeliverTask(params) {
| 任务 | 预计时间 | 优先级 | | 任务 | 预计时间 | 优先级 |
|------|----------|--------| |------|----------|--------|
| 任务1: 完善搜索参数支持 | 2小时 | 高 | | 任务1: 完善搜索参数支持 | 2小时 | 高 |
| 任务2: 优化职位数据保存 | 3小时 | 高 | | 任务2: 优化职位数据保存 | 4小时 | 高 |
| 任务3: 完善自动投递任务搜索条件 | 2小时 | 高 | | 任务3: 完善自动投递任务搜索条件 | 2小时 | 高 |
| 任务4: 优化职位匹配算法 | 4小时 | 高 | | 任务4: 优化职位匹配算法 | 4小时 | 高 |
| 任务5: 创建搜索任务接口 | 3小时 | | | 任务5: 创建搜索任务接口(支持可选投递) | 5小时 | |
| 任务6: 创建投递任务接口 | 4小时 | 中 | | 任务6: 完善指令类型映射 | 1小时 | 中 |
| 任务7: 完善指令类型映射 | 1小时 | | | 任务7: 添加搜索条件配置管理 | 1小时 | |
| 任务8: 添加搜索条件配置管理 | 1小时 | 低 |
**总计**: 约20小时 **总计**: 约19小时
## 🚀 开发优先级 ## 🚀 开发优先级
@@ -500,14 +674,70 @@ async createDeliverTask(params) {
2. 任务2: 优化职位数据保存 2. 任务2: 优化职位数据保存
3. 任务3: 完善自动投递任务搜索条件 3. 任务3: 完善自动投递任务搜索条件
4. 任务4: 优化职位匹配算法 4. 任务4: 优化职位匹配算法
5. 任务5: 创建搜索任务接口(支持可选投递)
### 第二阶段(接口完善) ### 第二阶段(接口完善)
5. 任务5: 创建搜索任务接口 6. 任务6: 完善指令类型映射
6. 任务6: 创建投递任务接口
7. 任务7: 完善指令类型映射
### 第三阶段(配置管理) ### 第三阶段(配置管理)
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. **性能优化**: 批量操作需要考虑性能,避免阻塞 4. **性能优化**: 批量操作需要考虑性能,避免阻塞
5. **MQTT通信**: 确保指令参数格式正确,与客户端协议一致 5. **MQTT通信**: 确保指令参数格式正确,与客户端协议一致
6. **数据库事务**: 批量操作需要使用事务保证数据一致性 6. **数据库事务**: 批量操作需要使用事务保证数据一致性
7. **投递时机**: 投递必须在搜索完成后立即执行,因为 `securityId` 等字段可能有时效性,前端页面变化后这些字段可能失效
8. **职位状态验证**: 投递前必须验证职位状态applyStatus = 'pending'),避免重复投递
9. **投递必需字段**: 投递时需要 `encryptJobId`、`encryptBossId` 和 `securityId`,这些字段必须从最新搜索结果中获取
10. **位置信息**: 优先使用响应中的 `gps` 字段避免不必要的API调用
11. **接口设计**: 搜索和投递在同一接口中完成,不支持单独的投递接口,因为已保存的职位信息可能已过期
## 🔗 相关文件 ## 🔗 相关文件

View File

@@ -144,14 +144,350 @@ class JobManager {
} }
/** /**
* 获取岗位列表 * 多条件搜索职位列表新指令使用新的MQTT action
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 搜索参数
* @returns {Promise<object>} 搜索结果
*/
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<object>} 执行结果
*/
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. 先执行搜索使用search_jobs_with_params新的搜索指令
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);
}
// 执行投递使用新的deliver_resume_search action
const deliverResult = await this.deliver_resume(sn_code, mqttClient, {
jobId: jobData.jobId,
encryptBossId: jobData.encryptBossId || '',
securityId: securityId,
brandName: jobData.companyName || '',
jobTitle: jobData.jobTitle || '',
companyName: jobData.companyName || '',
platform: platform,
action: 'deliver_resume_search' // 搜索并投递使用新的action
});
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码 * @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端 * @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数 * @param {object} params - 参数
* @returns {Promise<object>} 岗位列表 * @returns {Promise<object>} 岗位列表
*/ */
async get_job_list(sn_code, mqttClient, params = {}) { async get_job_list(sn_code, mqttClient, params = {}) {
const { keyword = '前端', platform = 'boss', pageCount = 3 } = params; const {
keyword = '前端',
platform = 'boss',
pageCount = 3,
city = '',
cityName = '',
salary = '',
experience = '',
education = '',
industry = '',
companySize = '',
financingStage = '',
page = 1,
pageSize = 20
} = params;
// 判断是否是多条件搜索(如果包含多条件参数,使用多条件搜索逻辑)
const hasMultiParams = city || cityName || salary || experience || education ||
industry || companySize || financingStage || page || pageSize;
if (hasMultiParams) {
// 使用多条件搜索逻辑
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: "get_job_list", // 保持与原有get_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
};
}
// 简单搜索逻辑(保持原有逻辑)
console.log(`[工作管理] 开始获取设备 ${sn_code} 的岗位列表,关键词: ${keyword}`); console.log(`[工作管理] 开始获取设备 ${sn_code} 的岗位列表,关键词: ${keyword}`);
// 通过MQTT指令获取岗位列表 // 通过MQTT指令获取岗位列表
@@ -320,10 +656,11 @@ class JobManager {
* @param {string} params.brandName - 公司名称(可选) * @param {string} params.brandName - 公司名称(可选)
* @param {string} params.jobTitle - 职位标题(可选) * @param {string} params.jobTitle - 职位标题(可选)
* @param {string} params.companyName - 公司名称(可选) * @param {string} params.companyName - 公司名称(可选)
* @param {string} params.action - MQTT Action默认deliver_resume可选deliver_resume_search
* @returns {Promise<object>} 投递结果 * @returns {Promise<object>} 投递结果
*/ */
async applyJob(sn_code, mqttClient, params = {}) { async deliver_resume(sn_code, mqttClient, params = {}) {
const { platform = 'boss', jobId, encryptBossId, securityId, brandName, jobTitle, companyName } = params; const { platform = 'boss', jobId, encryptBossId, securityId, brandName, jobTitle, companyName, action = 'deliver_resume' } = params;
if (!jobId) { if (!jobId) {
throw new Error('jobId 参数不能为空请指定要投递的职位ID'); throw new Error('jobId 参数不能为空请指定要投递的职位ID');
@@ -401,10 +738,10 @@ class JobManager {
console.log(`[工作管理] 投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`); console.log(`[工作管理] 投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`);
// 通过MQTT指令投递简历 // 通过MQTT指令投递简历支持自定义action
const response = await mqttClient.publishAndWait(sn_code, { const response = await mqttClient.publishAndWait(sn_code, {
platform, platform,
action: "deliver_resume", action: action, // 使用传入的action参数默认为"deliver_resume"
data: { data: {
encryptJobId: jobData.jobId, encryptJobId: jobData.jobId,
securityId: jobData.securityId || securityId || '', securityId: jobData.securityId || securityId || '',

View File

@@ -230,12 +230,41 @@ class CommandManager {
// 构建指令执行 Promise // 构建指令执行 Promise
const command_promise = (async () => { const command_promise = (async () => {
// 指令类型映射表(内部指令类型 -> jobManager方法名
const commandMethodMap = {
// get_job_list 指令对应MQTT Action: "get_job_list"
'get_job_list': 'get_job_list',
'getJobList': 'get_job_list',
// search_jobs_with_params 指令对应MQTT Action: "search_job_list"
'search_jobs_with_params': 'search_jobs_with_params',
'searchJobsWithParams': 'search_jobs_with_params',
// search_and_deliver 指令内部调用search_jobs_with_params和deliver_resume
'search_and_deliver': 'search_and_deliver',
'searchAndDeliver': 'search_and_deliver',
// deliver_resume 指令对应MQTT Action: "deliver_resume"
'deliver_resume': 'deliver_resume',
'deliverResume': 'deliver_resume'
// search_jobs 指令对应MQTT Action: "search_jobs"
'search_jobs': 'search_jobs',
'searchJobs': 'search_jobs'
};
// 优先使用映射表
const mappedMethod = commandMethodMap[command_type] || commandMethodMap[method_name];
if (mappedMethod && jobManager[mappedMethod]) {
return await jobManager[mappedMethod](sn_code, mqttClient, command_params);
}
// 其次尝试转换后的方法名
if (command_type && jobManager[method_name]) { if (command_type && jobManager[method_name]) {
return await jobManager[method_name](sn_code, mqttClient, command_params); return await jobManager[method_name](sn_code, mqttClient, command_params);
} else if (jobManager[command_type]) { }
// 最后尝试原始指令类型
if (jobManager[command_type]) {
return await jobManager[command_type](sn_code, mqttClient, command_params); return await jobManager[command_type](sn_code, mqttClient, command_params);
} else { } else {
throw new Error(`未知的指令类型: ${command_type} (尝试的方法名: ${method_name})`); throw new Error(`未知的指令类型: ${command_type} (尝试的方法名: ${method_name}, 映射方法: ${mappedMethod})`);
} }
})(); })();

View File

@@ -214,7 +214,7 @@ class DeviceWorkStatusNotifier {
return `投递职位: ${parsedParams.jobTitle} @ ${companyName}`; return `投递职位: ${parsedParams.jobTitle} @ ${companyName}`;
} else if (parsedParams.jobTitle) { } else if (parsedParams.jobTitle) {
return `投递职位: ${parsedParams.jobTitle}`; return `投递职位: ${parsedParams.jobTitle}`;
} else if (commandType === 'applyJob' || commandName.includes('投递')) { } else if (commandType === 'deliver_resume' || commandName.includes('投递')) {
return '投递简历'; return '投递简历';
} else if (commandType === 'searchJobs' || commandName.includes('搜索')) { } else if (commandType === 'searchJobs' || commandName.includes('搜索')) {
return `搜索职位: ${parsedParams.keyword || ''}`; return `搜索职位: ${parsedParams.keyword || ''}`;

View File

@@ -24,6 +24,11 @@ class TaskHandlers {
return await this.handleAutoDeliverTask(task); return await this.handleAutoDeliverTask(task);
}); });
// 搜索职位列表任务(新功能)
taskQueue.registerHandler('search_jobs', async (task) => {
return await this.handleSearchJobListTask(task);
});
// 自动沟通任务(待实现) // 自动沟通任务(待实现)
taskQueue.registerHandler('auto_chat', async (task) => { taskQueue.registerHandler('auto_chat', async (task) => {
return await this.handleAutoChatTask(task); return await this.handleAutoChatTask(task);
@@ -469,7 +474,7 @@ class TaskHandlers {
for (const jobData of jobsToDeliver) { for (const jobData of jobsToDeliver) {
console.log(`[任务处理器] 准备投递职位: ${jobData.jobTitle} @ ${jobData.companyName}, 评分: ${jobData.matchScore}`, jobData.scoreDetails); console.log(`[任务处理器] 准备投递职位: ${jobData.jobTitle} @ ${jobData.companyName}, 评分: ${jobData.matchScore}`, jobData.scoreDetails);
deliverCommands.push({ deliverCommands.push({
command_type: 'applyJob', command_type: 'deliver_resume', // 与MQTT Action保持一致
command_name: `投递简历 - ${jobData.jobTitle} @ ${jobData.companyName} (评分:${jobData.matchScore})`, command_name: `投递简历 - ${jobData.jobTitle} @ ${jobData.companyName} (评分:${jobData.matchScore})`,
command_params: JSON.stringify({ command_params: JSON.stringify({
sn_code: sn_code, sn_code: sn_code,
@@ -547,6 +552,180 @@ class TaskHandlers {
return { allowed: true, reason: '在允许的时间范围内' }; return { allowed: true, reason: '在允许的时间范围内' };
} }
/**
* 处理搜索职位列表任务(新功能)
* 支持多条件搜索和可选投递
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
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 {
// 使用多条件搜索指令新的指令类型使用新的MQTT action
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进行沟通回复消息等 * 功能自动与HR进行沟通回复消息等

View File

@@ -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 { return {
message: '任务已添加到队列', 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<Object>} 任务创建结果 { 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
};
} }
/** /**