Compare commits
4 Commits
main
...
6d73a80e50
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d73a80e50 | ||
|
|
6b5e409b6b | ||
|
|
5035b9aa72 | ||
|
|
8fa06435a9 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ node_modules.*
|
||||
dist.zip
|
||||
dist/
|
||||
admin/node_modules/
|
||||
app/
|
||||
app/*
|
||||
app/*
|
||||
@@ -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<Object>} 任务创建结果 { 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. 更新任务状态为完成
|
||||
```
|
||||
|
||||
### 投递职位流程
|
||||
|
||||
```
|
||||
1. 用户/系统调用 createDeliverTask()
|
||||
8. 如果 autoDeliver=true,继续执行投递流程:
|
||||
↓
|
||||
2. 创建任务记录 (task_status)
|
||||
8.1 从数据库获取刚搜索到的职位列表
|
||||
- 筛选条件: applyStatus = 'pending', sn_code = 账号SN码
|
||||
↓
|
||||
3. 生成搜索指令 (获取职位列表)
|
||||
- command_type: 'get_job_list'
|
||||
↓
|
||||
4. 执行搜索指令
|
||||
↓
|
||||
5. 获取职位列表并保存到数据库
|
||||
↓
|
||||
6. 根据简历信息和过滤规则匹配职位
|
||||
- 距离匹配
|
||||
- 薪资匹配
|
||||
- 工作年限匹配
|
||||
- 学历匹配
|
||||
8.2 根据简历信息和过滤规则匹配职位
|
||||
- 距离匹配(基于经纬度)
|
||||
- 薪资匹配(解析salaryDesc)
|
||||
- 工作年限匹配(解析jobExperience)
|
||||
- 学历匹配(解析jobDegree)
|
||||
- 权重评分
|
||||
↓
|
||||
7. 为每个匹配的职位生成投递指令
|
||||
8.3 为每个匹配的职位生成投递指令
|
||||
- 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 表需要完善的字段
|
||||
|
||||
| 字段名 | 类型 | 说明 | 状态 |
|
||||
|--------|------|------|------|
|
||||
| `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. **接口设计**: 搜索和投递在同一接口中完成,不支持单独的投递接口,因为已保存的职位信息可能已过期
|
||||
|
||||
## 🔗 相关文件
|
||||
|
||||
|
||||
@@ -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 {object} mqttClient - MQTT客户端
|
||||
* @param {object} params - 参数
|
||||
* @returns {Promise<object>} 岗位列表
|
||||
*/
|
||||
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}`);
|
||||
|
||||
// 通过MQTT指令获取岗位列表
|
||||
@@ -320,10 +656,11 @@ class JobManager {
|
||||
* @param {string} params.brandName - 公司名称(可选)
|
||||
* @param {string} params.jobTitle - 职位标题(可选)
|
||||
* @param {string} params.companyName - 公司名称(可选)
|
||||
* @param {string} params.action - MQTT Action(默认:deliver_resume,可选:deliver_resume_search)
|
||||
* @returns {Promise<object>} 投递结果
|
||||
*/
|
||||
async applyJob(sn_code, mqttClient, params = {}) {
|
||||
const { platform = 'boss', jobId, encryptBossId, securityId, brandName, jobTitle, companyName } = params;
|
||||
async deliver_resume(sn_code, mqttClient, params = {}) {
|
||||
const { platform = 'boss', jobId, encryptBossId, securityId, brandName, jobTitle, companyName, action = 'deliver_resume' } = params;
|
||||
|
||||
if (!jobId) {
|
||||
throw new Error('jobId 参数不能为空,请指定要投递的职位ID');
|
||||
@@ -401,10 +738,10 @@ class JobManager {
|
||||
|
||||
console.log(`[工作管理] 投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`);
|
||||
|
||||
// 通过MQTT指令投递简历
|
||||
// 通过MQTT指令投递简历(支持自定义action)
|
||||
const response = await mqttClient.publishAndWait(sn_code, {
|
||||
platform,
|
||||
action: "deliver_resume",
|
||||
action: action, // 使用传入的action参数,默认为"deliver_resume"
|
||||
data: {
|
||||
encryptJobId: jobData.jobId,
|
||||
securityId: jobData.securityId || securityId || '',
|
||||
|
||||
@@ -230,12 +230,41 @@ class CommandManager {
|
||||
|
||||
// 构建指令执行 Promise
|
||||
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]) {
|
||||
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);
|
||||
} else {
|
||||
throw new Error(`未知的指令类型: ${command_type} (尝试的方法名: ${method_name})`);
|
||||
throw new Error(`未知的指令类型: ${command_type} (尝试的方法名: ${method_name}, 映射方法: ${mappedMethod})`);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ class DeviceWorkStatusNotifier {
|
||||
return `投递职位: ${parsedParams.jobTitle} @ ${companyName}`;
|
||||
} else if (parsedParams.jobTitle) {
|
||||
return `投递职位: ${parsedParams.jobTitle}`;
|
||||
} else if (commandType === 'applyJob' || commandName.includes('投递')) {
|
||||
} else if (commandType === 'deliver_resume' || commandName.includes('投递')) {
|
||||
return '投递简历';
|
||||
} else if (commandType === 'searchJobs' || commandName.includes('搜索')) {
|
||||
return `搜索职位: ${parsedParams.keyword || ''}`;
|
||||
|
||||
@@ -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);
|
||||
@@ -469,7 +474,7 @@ class TaskHandlers {
|
||||
for (const jobData of jobsToDeliver) {
|
||||
console.log(`[任务处理器] 准备投递职位: ${jobData.jobTitle} @ ${jobData.companyName}, 评分: ${jobData.matchScore}`, jobData.scoreDetails);
|
||||
deliverCommands.push({
|
||||
command_type: 'applyJob',
|
||||
command_type: 'deliver_resume', // 与MQTT Action保持一致
|
||||
command_name: `投递简历 - ${jobData.jobTitle} @ ${jobData.companyName} (评分:${jobData.matchScore})`,
|
||||
command_params: JSON.stringify({
|
||||
sn_code: sn_code,
|
||||
@@ -547,6 +552,180 @@ class TaskHandlers {
|
||||
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进行沟通,回复消息等
|
||||
|
||||
@@ -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<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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
import{a as t}from"./index-CsHwYKwf.js";class s{async getConfig(r){try{return await t.post("/user/delivery-config/get",{sn_code:r})}catch(e){throw console.error("获取投递配置失败:",e),e}}async saveConfig(r,e){try{return await t.post("/user/delivery-config/save",{sn_code:r,deliver_config:e})}catch(o){throw console.error("保存投递配置失败:",o),o}}}const i=new s;export{i as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 334 KiB |
Binary file not shown.
Binary file not shown.
@@ -1,32 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>boss - 远程监听服务</title>
|
||||
<script type="module" crossorigin src="/app/assets/index-CsHwYKwf.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/app/assets/index-BUzIVj1g.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 启动加载动画 -->
|
||||
<div id="loading-screen" class="loading-screen">
|
||||
<div class="loading-content">
|
||||
<div class="loading-logo">
|
||||
<div class="logo-circle"></div>
|
||||
</div>
|
||||
<div class="loading-text">正在启动...</div>
|
||||
<div class="loading-progress">
|
||||
<div class="progress-bar-animated"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vue 应用挂载点 -->
|
||||
<div id="app" ></div>
|
||||
|
||||
<!-- 在 body 底部加载 Vue 应用脚本 -->
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user