Compare commits

...

44 Commits

Author SHA1 Message Date
张成
7ef0c68ad1 1 2026-04-10 18:45:10 +08:00
张成
37daa2f99f 1 2026-04-10 18:35:13 +08:00
张成
51bbdacdda 1 2026-04-08 17:27:40 +08:00
张成
f2a8e61016 1 2026-04-08 16:39:27 +08:00
张成
048c40d802 1 2026-04-08 15:28:02 +08:00
张成
bfd39eddcf 1 2026-04-08 15:00:49 +08:00
张成
21fe005c19 1 2026-04-08 14:09:26 +08:00
张成
e3d14dd637 1 2026-04-08 13:30:50 +08:00
张成
ca8bbcd9cd 1 2026-04-08 13:29:22 +08:00
张成
820e437729 1 2026-04-03 17:24:38 +08:00
张成
39a5b49213 1 2026-03-17 15:10:27 +08:00
张成
f071215ad5 1 2026-03-17 10:50:49 +08:00
张成
f5082c157c 1 2026-03-13 18:24:41 +08:00
张成
6b371148af 1 2026-03-13 17:06:02 +08:00
张成
6aeee136c4 1 2026-03-13 16:53:26 +08:00
张成
2c021c24ef 1 2026-03-13 15:32:28 +08:00
张成
8a953eb769 1 2026-02-28 17:58:03 +08:00
张成
a40219c7e4 1 2026-02-28 17:38:45 +08:00
张成
5ec4e7f440 1 2026-02-28 16:28:28 +08:00
张成
96da90daa8 11 2026-02-28 15:21:59 +08:00
张成
e44ffba1ef 1 2026-02-28 14:15:49 +08:00
张成
0483d6d023 1 2026-02-28 13:51:17 +08:00
张成
58c9d64e55 1 2026-02-28 13:31:32 +08:00
张成
dfd3119163 1 2026-02-28 10:38:28 +08:00
张成
1a011bcc01 1 2026-02-27 17:33:39 +08:00
张成
c1d812a80e Merge branch 'dev2' into dev 2026-02-27 16:47:40 +08:00
张成
f57bb2767d 1 2026-02-27 16:46:38 +08:00
张成
d9d277fe59 1 2025-12-30 18:27:26 +08:00
张成
d2ae741b9e 1 2025-12-30 17:47:07 +08:00
张成
52876229a8 1 2025-12-30 17:06:14 +08:00
张成
914999c9fc 1 2025-12-30 16:45:09 +08:00
张成
fb9aa5b155 1 2025-12-30 16:36:46 +08:00
张成
b49bd658a6 1 2025-12-30 16:30:37 +08:00
张成
956cfe88f8 1 2025-12-30 16:23:45 +08:00
张成
c45ea21c83 1 2025-12-30 16:18:28 +08:00
张成
fa2dea3f04 1 2025-12-30 15:49:51 +08:00
张成
dd7373c0b8 1 2025-12-30 15:48:41 +08:00
张成
65833dd32d 11 2025-12-30 15:46:18 +08:00
张成
d14f89e008 1 2025-12-30 14:51:33 +08:00
张成
dcaf0cb428 1 2025-12-30 14:37:33 +08:00
张成
6d73a80e50 1 2025-12-30 10:06:09 +08:00
张成
6b5e409b6b ignore app directory 2025-12-30 10:06:02 +08:00
张成
5035b9aa72 1 2025-12-29 21:07:52 +08:00
张成
8fa06435a9 1 2025-12-29 18:35:57 +08:00
131 changed files with 8333 additions and 21001 deletions

View File

@@ -10,7 +10,17 @@
"Bash(mkdir:*)",
"Bash(findstr:*)",
"Bash(cat:*)",
"Bash(npm run restart:*)"
"Bash(npm run restart:*)",
"Bash(del scheduledJobs.js)",
"Bash(ls:*)",
"Bash(wc:*)",
"Bash(for:*)",
"Bash(done)",
"Bash(npm start)",
"Bash(timeout 10 npm start)",
"Bash(timeout 15 npm start)",
"Bash(del apiservicesconfigaiConfig.js)",
"Bash(grep:*)"
],
"deny": [],
"ask": []

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ node_modules.*
dist.zip
dist/
admin/node_modules/
app/
app/*
app/*

View File

@@ -1,140 +0,0 @@
# AI 功能禁用说明
## 📋 概述
根据需求AI 接入功能暂时禁用,作为二期规划。当前使用简单的文本匹配来实现职位过滤功能。
## ✅ 已完成的修改
### 1. 创建文本匹配过滤服务
- ✅ 创建了 `api/middleware/job/job_filter_service.js`
- 实现基于文本匹配的职位分析
- 支持技能匹配、经验匹配、薪资匹配
- 支持外包检测和关键词过滤
### 2. 禁用 AI 服务调用
#### 2.1 jobManager.js
- ✅ 注释掉 `aiService` 引用
- ✅ 禁用 `analyzeResumeWithAI()` 调用
- ✅ 修改 `analyzeResume()` 使用文本匹配
#### 2.2 resumeManager.js
- ✅ 注释掉 `aiService` 引用
- ✅ 修改 `analyzeResume()` 使用文本匹配
- ✅ 修改 `calculateMatchScore()` 使用 `jobFilterService.analyzeJobMatch()`
#### 2.3 chatManager.js
- ✅ 注释掉 `aiService` 引用
- ✅ 修改 `generateChatContent()` 使用默认模板
- ✅ 修改 `generateInterviewInvitation()` 使用默认模板
- ✅ 添加 `generateDefaultChatContent()` 方法
- ✅ 添加 `generateDefaultInterviewInvitation()` 方法
## 🔧 文本匹配过滤功能
### 功能特性
1. **技能匹配度计算**
- 从职位描述中提取技能关键词
- 与简历技能进行匹配
- 计算匹配百分比0-100分
2. **经验匹配度计算**
- 从职位描述中提取经验要求
- 与简历工作经验进行匹配
- 计算匹配分数
3. **薪资合理性计算**
- 解析职位薪资范围
- 与期望薪资进行对比
- 计算匹配分数
4. **外包检测**
- 检测职位描述中的外包关键词
- 标记是否为外包岗位
5. **关键词过滤**
- 支持包含关键词过滤
- 支持排除关键词过滤
- 支持自定义排除关键词列表
### 使用示例
```javascript
const jobFilterService = require('./job_filter_service');
// 分析职位匹配度
const analysis = jobFilterService.analyzeJobMatch(jobInfo, resumeInfo);
console.log('综合分数:', analysis.overallScore);
console.log('技能匹配:', analysis.skillMatch);
console.log('是否为外包:', analysis.isOutsourcing);
// 过滤职位列表
const filteredJobs = jobFilterService.filterJobs(jobs, {
minScore: 60, // 最低匹配分数
excludeOutsourcing: true, // 排除外包
excludeKeywords: ['销售', '客服'] // 排除关键词
}, resumeInfo);
```
## 📝 默认模板
### 聊天内容模板
- **greeting**: "您好,我对这个岗位很感兴趣,希望能进一步了解。"
- **interview**: "感谢您的回复,我很期待与您进一步沟通。"
- **followup**: "您好,想了解一下这个岗位的最新进展。"
### 面试邀约模板
- "感谢您的邀请,我很期待与您面谈。请问方便的时间是什么时候?"
## ⚠️ 注意事项
1. **AI 服务文件保留**
- `api/middleware/job/aiService.js` 文件保留,但不再被调用
- 二期规划时可以重新启用
3. **日志提示**
- 所有禁用 AI 的地方都有日志提示
- 明确标注"AI分析已禁用二期规划"
## 🔄 二期规划恢复步骤
当需要恢复 AI 功能时:
1. 取消注释所有 `aiService` 引用
2. 恢复 AI 方法调用
3. 移除或注释文本匹配的替代代码
4. 测试 AI 服务连接和功能
## 📊 当前功能对比
| 功能 | AI 版本 | 文本匹配版本 |
|------|---------|-------------|
| 简历分析 | AI 智能分析 | 技能关键词提取 |
| 职位匹配 | AI 深度分析 | 文本匹配评分 |
| 聊天生成 | AI 个性化生成 | 固定模板 |
| 面试邀约 | AI 个性化生成 | 固定模板 |
| 外包检测 | AI 判断 | 关键词匹配 |
## 🎯 后续优化建议
1. **增强文本匹配**
- 添加更多技能关键词
- 优化匹配算法
- 支持同义词匹配
2. **规则配置化**
- 将过滤规则配置化
- 支持用户自定义规则
- 支持规则优先级
3. **匹配度优化**
- 优化评分算法
- 添加更多匹配维度
- 支持权重配置

View File

@@ -1,968 +0,0 @@
# 自动找工作系统 - MQTT指令列表
> 版本: v1.0 | 更新日期: 2025-12-25
## 文档说明
本文档定义了服务端通过MQTT向客户端下发的所有指令格式和规范。所有操作都通过任务和指令的方式异步执行。
---
## 一、MQTT通信架构
### 1.1 通信流程
```
┌──────────────┐ ┌──────────────┐
│ 服务端 │ │ 客户端 │
│ (Node.js) │ │ (设备端) │
└──────┬───────┘ └──────┬───────┘
│ │
│ ① 创建任务(task_status表) │
│ ② 生成指令(task_commands表) │
│ │
│ ③ MQTT Publish │
│ Topic: {sn_code}/command │
│ ─────────────────────────────> │
│ │
│ ④ 执行指令 │
│ ⑤ 生成结果 │
│ │
│ ⑥ MQTT Publish │
│ Topic: response │
│ <───────────────────────────── │
│ │
│ ⑦ 更新指令状态(task_commands) │
│ ⑧ 更新任务状态(task_status) │
│ │
```
### 1.2 MQTT配置
- **Broker地址**: `mqtt://192.144.167.231:1883`
- **订阅主题**:
- `heartbeat` - 设备心跳信息
- `response` - 设备响应信息
- **发布主题**:
- `{sn_code}/command` - 向指定设备发送指令
### 1.3 消息格式
**服务端 → 客户端 (指令)**
```json
{
"commandId": "uuid",
"taskId": "uuid",
"platform": "boss",
"action": "search_jobs",
"data": {
"keyword": "全栈工程师",
"city": "101020100",
"page": 1
}
}
```
**客户端 → 服务端 (响应)**
```json
{
"commandId": "uuid",
"taskId": "uuid",
"code": 200,
"message": "执行成功",
"data": {
// 返回数据
}
}
```
**客户端 → 服务端 (心跳)**
```json
{
"sn_code": "device001",
"platform": "boss",
"timestamp": 1672531200000,
"status": "online",
"version": "1.0.0"
}
```
---
## 二、已实现指令列表
### 2.1 用户登录指令
#### get_login_qr_code - 获取登录二维码
**指令格式**
```json
{
"action": "get_login_qr_code",
"platform": "boss",
"data": {}
}
```
**返回格式**
```json
{
"code": 200,
"message": "二维码获取成功",
"data": {
"qrCode": "https://example.com/qrcode.png",
"qr_code_url": "https://example.com/qrcode.png",
"expire_time": 300
}
}
```
**说明**: 获取登录二维码,用户扫码登录后客户端需要保存cookies/token
---
#### get_user_info - 获取用户信息
**指令格式**
```json
{
"action": "get_user_info",
"platform": "boss",
"data": {}
}
```
**返回格式**
```json
{
"code": 200,
"message": "获取成功",
"data": {
"userId": "123456",
"userName": "张三",
"phone": "138****5678",
"isLoggedIn": true
}
}
```
**说明**: 获取当前登录用户的基本信息,验证登录状态
---
### 2.2 简历管理指令
#### get_online_resume - 获取在线简历
**指令格式**
```json
{
"action": "get_online_resume",
"platform": "boss",
"data": {}
}
```
**返回格式**
```json
{
"code": 200,
"message": "获取成功",
"data": {
"baseInfo": {
"name": "张三",
"gender": 1,
"age": 28,
"account": "138****5678",
"emailBlur": "zhang***@qq.com",
"workYears": 5,
"workYearDesc": "5年",
"degreeCategory": "本科"
},
"expectList": [{
"positionName": "全栈工程师",
"locationName": "上海",
"salaryDesc": "20-30K",
"industryDesc": "互联网"
}],
"workExpList": [{
"companyName": "XX科技公司",
"positionName": "高级前端工程师",
"startDate": "2020-01",
"endDate": "2023-12",
"workContent": "负责前端架构设计和开发..."
}],
"projectExpList": [{
"name": "电商平台项目",
"roleName": "技术负责人",
"startDate": "2022-01",
"endDate": "2023-06",
"projectDesc": "项目描述...",
"performance": "项目成果..."
}],
"educationExpList": [{
"school": "XX大学",
"major": "计算机科学与技术",
"degreeName": "本科",
"endYear": 2018
}],
"userDesc": "熟悉Vue、React、Node.js等技术栈...",
"certificationList": []
}
}
```
**说明**: 获取用户在招聘平台上的完整简历信息
---
### 2.3 岗位搜索指令
#### search_jobs - 搜索岗位 (已实现但需完善)
**指令格式**
```json
{
"action": "search_jobs",
"platform": "boss",
"data": {
"keyword": "全栈工程师",
"city": "101020100",
"page": 1
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "搜索成功",
"data": {
"total": 150,
"page": 1,
"jobList": [{
"jobId": "job123456",
"jobTitle": "全栈工程师",
"companyName": "XX科技公司",
"companySize": "100-499人",
"salary": "20-30K",
"location": "上海·浦东新区",
"experience": "3-5年",
"education": "本科",
"jobRequirements": "1. 熟悉Vue/React...",
"jobDescription": "岗位职责...",
"bossName": "张经理",
"bossTitle": "技术总监"
}]
}
}
```
**说明**: 当前实现基础,需要扩展支持更多搜索条件
---
#### get_job_list - 获取岗位列表
**指令格式**
```json
{
"action": "get_job_list",
"platform": "boss",
"data": {
"page": 1,
"pageSize": 20
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "获取成功",
"data": {
"total": 50,
"page": 1,
"jobList": [
// 同 search_jobs 的 jobList 格式
]
}
}
```
**说明**: 获取推荐岗位列表
---
### 2.4 投递管理指令
#### apply_job - 投递岗位 (基础实现)
**指令格式**
```json
{
"action": "apply_job",
"platform": "boss",
"data": {
"jobId": "job123456",
"expectSalary": "20-30K"
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "投递成功",
"data": {
"applyId": "apply123456",
"jobId": "job123456",
"applyTime": "2025-12-25 10:30:00",
"status": "success"
}
}
```
**错误码**
- `400` - 参数错误
- `403` - 已投递过该岗位
- `429` - 投递次数达到上限
- `500` - 投递失败
**说明**: 向指定岗位投递简历
---
### 2.5 聊天管理指令
#### get_chat_list - 获取聊天列表
**指令格式**
```json
{
"action": "get_chat_list",
"platform": "boss",
"data": {
"page": 1,
"pageSize": 20
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "获取成功",
"data": {
"total": 15,
"chatList": [{
"conversationId": "conv123456",
"jobId": "job123456",
"jobTitle": "全栈工程师",
"companyName": "XX科技",
"bossName": "张经理",
"lastMessage": "您好,请问...",
"lastMessageTime": "2025-12-25 10:30:00",
"unreadCount": 2,
"hasInterview": false
}]
}
}
```
**说明**: 获取与HR的聊天会话列表
---
#### send_chat_message - 发送聊天消息
**指令格式**
```json
{
"action": "send_chat_message",
"platform": "boss",
"data": {
"conversationId": "conv123456",
"jobId": "job123456",
"content": "您好,我对这个岗位很感兴趣..."
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "发送成功",
"data": {
"messageId": "msg123456",
"sendTime": "2025-12-25 10:30:00"
}
}
```
**说明**: 向HR发送聊天消息
---
### 2.6 测试和调试指令
#### open_bot_detection - 打开测试页
**指令格式**
```json
{
"action": "open_bot_detection",
"platform": "boss",
"data": {}
}
```
**返回格式**
```json
{
"code": 200,
"message": "测试页已打开",
"data": {}
}
```
**说明**: 打开测试页面,用于调试
---
## 三、待开发指令列表
### 3.1 搜索投递增强指令 (优先级: HIGH)
#### search_jobs_enhanced - 增强搜索岗位 ⭐⭐⭐⭐⭐
**指令格式**
```json
{
"action": "search_jobs_enhanced",
"platform": "boss",
"data": {
"keyword": "全栈工程师",
"city": "101020100",
"page": 1,
"pageSize": 20,
// 新增搜索条件
"experience": "3",
"degree": "203",
"salary": "406",
"scale": "303",
"stage": "807",
"position": "100109",
// 滚动加载方式
"scrollLoadType": "auto",
"maxScrollPages": 5
}
}
```
**参数说明**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| keyword | 搜索关键词 | "全栈工程师" |
| city | 城市代码 | "101020100" (上海) |
| page | 页码 | 1 |
| pageSize | 每页数量 | 20 |
| experience | 工作经验 | "1"=1年以下, "3"=1-3年, "4"=3-5年, "5"=5-10年, "6"=10年以上 |
| degree | 学历要求 | "202"=不限, "203"=大专, "204"=本科, "205"=硕士, "206"=博士 |
| salary | 薪资范围 | "402"=3-5K, "403"=5-10K, "404"=10-15K, "405"=15-20K, "406"=20-30K, "407"=30-50K, "408"=50K以上 |
| scale | 公司规模 | "302"=0-20人, "303"=20-99人, "304"=100-499人, "305"=500-999人, "306"=1000人以上 |
| stage | 融资阶段 | "801"=未融资, "802"=天使轮, "803"=A轮, "804"=B轮, "805"=C轮, "806"=D轮及以上, "807"=已上市, "808"=不需要融资 |
| position | 职位类型 | "100109"=全栈, "100110"=前端, "100111"=后端, "100112"=移动端 |
| scrollLoadType | 加载方式 | "auto"=自动滚动, "manual"=手动翻页 |
| maxScrollPages | 最大滚动页数 | 5 |
**返回格式**
```json
{
"code": 200,
"message": "搜索成功",
"data": {
"total": 150,
"page": 1,
"hasMore": true,
"jobList": [{
"jobId": "job123456",
"jobTitle": "全栈工程师",
"companyName": "XX科技公司",
"companySize": "100-499人",
"companyIndustry": "互联网",
"companyStage": "已上市",
"salary": "20-30K",
"salaryMonth": "14薪",
"location": "上海·浦东新区",
"longitude": 121.5273,
"latitude": 31.2172,
"experience": "3-5年",
"education": "本科",
"skills": ["Vue", "React", "Node.js"],
"jobRequirements": "1. 熟悉Vue/React...",
"jobDescription": "岗位职责...",
"welfare": ["五险一金", "带薪年假", "弹性工作"],
"bossName": "张经理",
"bossTitle": "技术总监",
"bossActiveStatus": "刚刚活跃",
"publishTime": "2025-12-25",
"viewCount": 150,
"applyCount": 30,
"isOutsourcing": false,
"jobLink": "https://www.zhipin.com/job_detail/xxx"
}]
}
}
```
**说明**:
- 支持Boss直聘完整的搜索筛选条件
- 支持自动滚动加载更多岗位
- 返回更详细的岗位信息
---
#### search_by_url - 通过URL搜索岗位 ⭐⭐⭐⭐⭐
**指令格式**
```json
{
"action": "search_by_url",
"platform": "boss",
"data": {
"url": "https://www.zhipin.com/web/geek/jobs?city=101020100&query=%E5%85%A8%E6%A0%88%E5%B7%A5%E7%A8%8B%E5%B8%88",
"scrollLoadType": "auto",
"maxScrollPages": 5
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "搜索成功",
"data": {
// 同 search_jobs_enhanced 返回格式
}
}
```
**说明**:
- 直接使用Boss直聘的搜索URL
- 自动解析URL参数
- 支持所有筛选条件
---
### 3.2 批量投递指令 (优先级: HIGH)
#### batch_apply_jobs - 批量投递岗位 ⭐⭐⭐⭐⭐
**指令格式**
```json
{
"action": "batch_apply_jobs",
"platform": "boss",
"data": {
"jobIds": ["job001", "job002", "job003"],
"expectSalary": "20-30K",
"applyInterval": 30,
"maxApplyCount": 10
}
}
```
**参数说明**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| jobIds | 岗位ID数组 | ["job001", "job002"] |
| expectSalary | 期望薪资 | "20-30K" |
| applyInterval | 投递间隔(秒) | 30 |
| maxApplyCount | 最大投递数量 | 10 |
**返回格式**
```json
{
"code": 200,
"message": "批量投递完成",
"data": {
"total": 10,
"success": 8,
"failed": 2,
"results": [{
"jobId": "job001",
"status": "success",
"applyId": "apply001",
"message": "投递成功"
}, {
"jobId": "job002",
"status": "failed",
"message": "已投递过该岗位"
}]
}
}
```
**说明**:
- 批量投递多个岗位
- 控制投递间隔避免被限制
- 返回每个岗位的投递结果
---
### 3.3 简历刷新指令 (优先级: HIGH)
#### refresh_resume - 刷新简历 ⭐⭐⭐⭐
**指令格式**
```json
{
"action": "refresh_resume",
"platform": "boss",
"data": {}
}
```
**返回格式**
```json
{
"code": 200,
"message": "简历刷新成功",
"data": {
"refreshTime": "2025-12-25 10:30:00",
"nextRefreshTime": "2025-12-25 12:30:00"
}
}
```
**说明**:
- 刷新简历提升排名
- 每2小时可刷新一次
---
### 3.4 账号保活指令 (优先级: HIGH)
#### auto_active - 自动活跃账号 ⭐⭐⭐⭐
**指令格式**
```json
{
"action": "auto_active",
"platform": "boss",
"data": {
"actionType": "random",
"actions": ["browse_jobs", "view_company", "search_keyword", "update_visibility"]
}
}
```
**参数说明**
| 参数 | 说明 | 可选值 |
|------|------|--------|
| actionType | 动作类型 | "random"=随机, "sequence"=顺序 |
| actions | 动作列表 | ["browse_jobs", "view_company", "search_keyword", "update_visibility"] |
**动作说明**
- `browse_jobs` - 浏览岗位(随机点击5-10个岗位)
- `view_company` - 查看公司主页
- `search_keyword` - 搜索关键词(随机关键词)
- `update_visibility` - 修改简历可见性
**返回格式**
```json
{
"code": 200,
"message": "活跃操作完成",
"data": {
"executedActions": ["browse_jobs", "view_company"],
"duration": 120,
"timestamp": "2025-12-25 10:30:00"
}
}
```
**说明**:
- 模拟真实用户行为
- 随机时间间隔
- 避免账号被标记为机器人
---
### 3.5 聊天增强指令 (优先级: MEDIUM)
#### get_chat_detail - 获取聊天详情 ⭐⭐⭐
**指令格式**
```json
{
"action": "get_chat_detail",
"platform": "boss",
"data": {
"conversationId": "conv123456",
"page": 1,
"pageSize": 50
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "获取成功",
"data": {
"conversationId": "conv123456",
"jobId": "job123456",
"messages": [{
"messageId": "msg001",
"senderId": "boss123",
"senderType": "boss",
"content": "您好,请问什么时候方便面试?",
"sendTime": "2025-12-25 10:30:00",
"isRead": true,
"messageType": "text",
"isInterviewInvitation": true
}]
}
}
```
**说明**: 获取完整的聊天历史记录
---
#### send_greeting - 发送打招呼 ⭐⭐⭐
**指令格式**
```json
{
"action": "send_greeting",
"platform": "boss",
"data": {
"jobId": "job123456",
"content": "您好,我对这个岗位很感兴趣,期待能有机会详聊。"
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "打招呼成功",
"data": {
"conversationId": "conv123456",
"messageId": "msg001",
"sendTime": "2025-12-25 10:30:00"
}
}
```
**说明**: 主动向HR发起沟通
---
### 3.6 数据采集指令 (优先级: MEDIUM)
#### get_job_detail - 获取岗位详情 ⭐⭐⭐
**指令格式**
```json
{
"action": "get_job_detail",
"platform": "boss",
"data": {
"jobId": "job123456"
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "获取成功",
"data": {
"jobId": "job123456",
// 完整的岗位详情(同search_jobs_enhanced中的jobList项)
"companyDetail": {
"companyId": "company123",
"companyName": "XX科技公司",
"companyLogo": "https://...",
"companySize": "100-499人",
"companyIndustry": "互联网",
"companyStage": "已上市",
"companyAddress": "上海市浦东新区...",
"companyDesc": "公司介绍..."
}
}
}
```
**说明**: 获取岗位的完整详情信息
---
#### get_company_info - 获取公司信息 ⭐⭐⭐
**指令格式**
```json
{
"action": "get_company_info",
"platform": "boss",
"data": {
"companyId": "company123"
}
}
```
**返回格式**
```json
{
"code": 200,
"message": "获取成功",
"data": {
"companyId": "company123",
"companyName": "XX科技公司",
"companyLogo": "https://...",
"companySize": "100-499人",
"companyIndustry": "互联网",
"companyStage": "已上市",
"companyAddress": "上海市浦东新区...",
"companyDesc": "公司介绍...",
"companyBenefit": ["五险一金", "带薪年假"],
"companyPhotos": ["https://...", "https://..."],
"jobCount": 50,
"isVerified": true
}
}
```
**说明**: 获取公司的详细信息
---
## 四、指令执行规范
### 4.1 指令生命周期
```
1. 创建 (pending)
2. 下发 (sent)
3. 执行中 (executing)
4. 完成 (completed) / 失败 (failed) / 超时 (timeout)
```
### 4.2 超时设置
| 指令类型 | 超时时间 | 重试次数 |
|---------|----------|----------|
| 登录类指令 | 60秒 | 1次 |
| 简历获取 | 30秒 | 2次 |
| 岗位搜索 | 60秒 | 2次 |
| 岗位投递 | 30秒 | 1次 |
| 聊天消息 | 30秒 | 2次 |
| 保活操作 | 120秒 | 0次 |
### 4.3 错误码规范
| 错误码 | 说明 | 处理方式 |
|--------|------|----------|
| 200 | 成功 | - |
| 400 | 参数错误 | 不重试 |
| 401 | 未登录 | 触发重新登录 |
| 403 | 无权限/已操作 | 不重试 |
| 429 | 请求过于频繁 | 延迟后重试 |
| 500 | 服务器错误 | 重试 |
| 503 | 服务不可用 | 延迟后重试 |
| 600 | 网络超时 | 重试 |
| 700 | 客户端错误 | 记录日志,不重试 |
### 4.4 重试策略
- **指数退避**: `delay = min(1000 * 2^(retryCount-1), 30000ms)`
- **最大重试次数**: 根据指令类型决定(见4.2表格)
- **可重试错误**: 429, 500, 503, 600
- **不可重试错误**: 400, 401, 403, 700
---
## 五、客户端实现要求
### 5.1 MQTT客户端
- **连接保持**: 断线自动重连
- **心跳间隔**: 10秒
- **订阅主题**: `{sn_code}/command`
- **发布主题**: `response`, `heartbeat`
### 5.2 指令处理
1. **接收指令**
- 解析JSON格式
- 验证必需字段
- 记录指令日志
2. **执行指令**
- 根据action分发到对应处理器
- 更新执行状态
- 捕获异常错误
3. **返回响应**
- 统一响应格式
- 包含commandId用于追踪
- 返回详细的执行结果
### 5.3 异常处理
- **网络异常**: 自动重试
- **登录过期**: 通知服务端重新登录
- **页面加载失败**: 刷新页面重试
- **元素定位失败**: 记录截图,返回错误
### 5.4 日志记录
- **请求日志**: 记录所有接收到的指令
- **响应日志**: 记录所有返回的响应
- **错误日志**: 记录所有异常和错误
- **操作日志**: 记录关键操作步骤
---
## 六、开发优先级
### P0 - 立即开发 (投递核心功能)
1.`search_jobs_enhanced` - 增强搜索
2.`search_by_url` - URL搜索
3.`batch_apply_jobs` - 批量投递
4.`refresh_resume` - 简历刷新
### P1 - 短期开发 (保活和聊天)
5.`auto_active` - 账号保活
6.`send_greeting` - 发送打招呼
7.`get_chat_detail` - 聊天详情
### P2 - 中期开发 (数据采集)
8.`get_job_detail` - 岗位详情
9.`get_company_info` - 公司信息
---
**文档维护**: 开发团队
**最后更新**: 2025-12-25

View File

@@ -1,20 +0,0 @@
# handleAutoDeliverTask ,自动投递岗位
1. 如果 2 小时之内没有获取在线简历 ,则重新获取一下在线简历,没有创建,有则更新
2. 获取职位列表, 按照用户 简历的信息resume_info 中的 skills expectedLocation expectedSalary expectedPosition workYears education location 和 职位类型 job_types 中的 年龄薪资距离职位的位置commonSkillsexcludeKeywords
user_longitude
user_longitude
job_postings和 经纬度 做距离匹配 ,按照 用户中可以配置 is_salary_priority 优先级 按照权重 占比 过滤
defaultValue: [ { "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20} ]
3.投递合适匹配的岗位

View File

@@ -1,354 +0,0 @@
# 任务与指令的区别说明
## 📋 概述
在调度系统中,**任务Task** 和 **指令Command** 是两个不同层次的概念,它们的关系是:**一个任务可以包含多个指令**。
### ⚠️ 重要说明
**当前系统实际情况**
- **真正的任务**:目前只有 `auto_deliver`(自动投递任务)是真正的任务,它包含多个步骤和指令
- **伪任务**:虽然代码中有 `get_resume``get_job_list``send_chat``apply_job` 等任务处理器,但它们实际上只是包装了单个指令,本质上就是直接执行指令
**为什么会有伪任务**
1. 统一的任务追踪和日志记录
2. 保持接口的一致性
3. 未来可能扩展为真正的任务(包含多个步骤)
## 🔄 层级关系
```
任务Task
├── 指令1Command
├── 指令2Command
└── 指令3Command
```
## 📊 详细对比
| 维度 | 任务Task | 指令Command |
|------|------------|----------------|
| **概念层级** | 业务层 | 执行层 |
| **数据库表** | `task_status` | `task_commands` |
| **管理模块** | TaskQueue任务队列 | CommandManager指令管理器 |
| **处理模块** | TaskHandlers任务处理器 | jobManager业务管理器 |
| **粒度** | 粗粒度(业务流程) | 细粒度(具体操作) |
| **包含关系** | 包含多个指令 | 属于某个任务 |
| **执行方式** | 由任务队列调度 | 由指令管理器执行 |
| **通信方式** | 内部调度 | 通过 MQTT 发送到客户端 |
## 🎯 任务Task
### 定义
任务是业务层面的概念,代表一个完整的业务流程或工作单元。
### 特点
- **业务导向**:代表一个完整的业务目标
- **可包含多个步骤**:一个任务可以包含多个指令
- **有生命周期**pending → running → completed/failed
- **有优先级**:可以设置任务优先级
- **有超时机制**:任务级别有超时保护
### 任务类型示例
**真正的任务(包含多个步骤)**
- `auto_deliver` - 自动投递任务(包含多个子操作:获取简历、获取岗位列表、筛选职位、批量投递)
- `auto_chat` - 自动沟通任务待实现自动与HR进行沟通回复消息等
- `auto_active_account` - 自动活跃账号任务(待实现:自动执行操作保持账号活跃度)
**注意**:目前系统中只有 `auto_deliver` 是已实现的真正任务,`auto_chat``auto_active_account` 是待实现的任务框架。
### 任务表结构task_status
```javascript
{
id: 1,
sn_code: 'GHJU',
taskType: 'auto_deliver',
taskName: '自动投递 - 前端开发',
status: 'running',
priority: 7,
taskParams: { keyword: '前端', platform: 'boss' },
result: {},
startTime: '2024-01-01 10:00:00',
endTime: null,
duration: 0
}
```
### 任务执行流程
```javascript
// 1. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: 'auto_deliver',
taskName: '自动投递',
taskParams: { keyword: '前端' }
});
// 2. 任务队列调度执行
// 3. 任务处理器处理任务
// 4. 任务处理器创建并执行指令
```
## ⚙️ 指令Command
### 定义
指令是执行层面的概念,代表一个具体的操作,通过 MQTT 发送到客户端执行。
### 特点
- **执行导向**:代表一个具体的操作
- **原子性**:一个指令是一个不可分割的操作
- **有执行顺序**:指令可以按顺序执行
- **有超时机制**:指令级别有超时保护
- **MQTT 通信**:通过 MQTT 发送到客户端
### 指令类型示例
- `getOnlineResume` - 获取在线简历
- `getJobList` - 获取岗位列表
- `applyJob` - 投递简历
- `sendChatMessage` - 发送聊天消息
- `getLoginQrCode` - 获取登录二维码
### 指令表结构task_commands
```javascript
{
id: 1,
task_id: 100, // 关联的任务ID
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: '{"sn_code":"GHJU"}',
status: 'completed',
sequence: 1,
priority: 9,
start_time: '2024-01-01 10:00:00',
end_time: '2024-01-01 10:00:30',
duration: 30000
}
```
### 指令执行流程
```javascript
// 1. 任务处理器创建指令
const commands = [{
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code })
}];
// 2. 指令管理器执行指令
await command.executeCommands(taskId, commands, mqttClient);
// 3. 通过 MQTT 发送到客户端
// 4. 客户端执行并返回结果
```
## 🔗 关系示例
### 示例1自动投递任务
**任务**`auto_deliver`(自动投递任务)
**包含的指令**
1. `getOnlineResume` - 获取在线简历
2. `getJobList` - 获取岗位列表
3. `applyJob` - 投递简历(可能多个)
```javascript
// 任务处理器创建多个指令
async handleAutoDeliverTask(task) {
// 1. 获取简历指令
const getResumeCommand = {
command_type: 'getOnlineResume',
command_name: '获取在线简历',
...
};
// 2. 获取岗位列表指令
const getJobListCommand = {
command_type: 'getJobList',
command_name: '获取岗位列表',
...
};
// 3. 投递指令(可能多个)
const applyCommands = jobs.map(job => ({
command_type: 'applyJob',
command_name: `投递简历 - ${job.jobTitle}`,
...
}));
// 执行所有指令
await command.executeCommands(task.id, [
getResumeCommand,
getJobListCommand,
...applyCommands
], mqttClient);
}
```
### 示例2获取简历伪任务实际是指令
**说明**:虽然代码中有 `get_resume` 任务处理器,但它实际上只是包装了单个指令,本质上就是直接执行指令。
**任务**`get_resume`(获取简历任务)
**包含的指令**
1. `getOnlineResume` - 获取在线简历
```javascript
async handleGetResumeTask(task) {
// 实际上只是创建一个指令并执行
const commands = [{
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code: task.sn_code })
}];
await command.executeCommands(task.id, commands, this.mqttClient);
}
```
**注意**:这种"任务"实际上可以直接作为指令执行,不需要通过任务队列。它们存在的原因可能是为了:
1. 统一的任务追踪和日志记录
2. 未来可能扩展为真正的任务(包含多个步骤)
3. 保持接口的一致性
## 📈 执行流程图
```
┌─────────────────┐
│ 任务队列 │
│ (TaskQueue) │
└────────┬────────┘
│ 调度任务
┌─────────────────┐
│ 任务处理器 │
│ (TaskHandlers) │
└────────┬────────┘
│ 创建指令
┌─────────────────┐
│ 指令管理器 │
│ (CommandManager)│
└────────┬────────┘
│ 执行指令
┌─────────────────┐
│ 业务管理器 │
│ (jobManager) │
└────────┬────────┘
│ MQTT 发送
┌─────────────────┐
│ 客户端设备 │
│ (Python Client)│
└─────────────────┘
```
## 🎨 设计优势
### 1. **职责分离**
- **任务层**:负责业务逻辑和流程编排
- **指令层**:负责具体操作和 MQTT 通信
### 2. **灵活性**
- 一个任务可以包含不同数量的指令
- 可以根据业务需求动态创建指令
### 3. **可追踪性**
- 任务级别:可以追踪整个业务流程
- 指令级别:可以追踪每个具体操作
### 4. **错误处理**
- 任务级别:处理业务逻辑错误
- 指令级别:处理执行错误和超时
## 📝 代码示例
### 任务处理器创建指令
```javascript
// api/middleware/schedule/taskHandlers.js
async handleAutoDeliverTask(task) {
const { sn_code, taskParams } = task;
// 1. 创建获取简历指令
const getResumeCommand = {
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code, platform: 'boss' })
};
// 2. 创建获取岗位列表指令
const getJobListCommand = {
command_type: 'getJobList',
command_name: '获取岗位列表',
command_params: JSON.stringify({
sn_code,
keyword: taskParams.keyword,
platform: 'boss'
})
};
// 3. 执行指令序列
const result = await command.executeCommands(
task.id,
[getResumeCommand, getJobListCommand],
this.mqttClient
);
return result;
}
```
### 指令管理器执行指令
```javascript
// api/middleware/schedule/command.js
async executeCommand(taskId, command, mqttClient) {
// 1. 创建指令记录
const commandRecord = await db.getModel('task_commands').create({
task_id: taskId,
command_type: command.command_type,
command_name: command.command_name,
status: 'pending'
});
// 2. 调用业务管理器执行
const result = await jobManager[commandType](
sn_code,
mqttClient,
commandParams
);
// 3. 更新指令状态
await this.updateCommandStatus(commandId, 'completed', result);
return result;
}
```
## 🔍 总结
- **任务Task**:业务层面的工作单元,代表一个完整的业务流程
- **真正的任务**:包含多个步骤/指令,如 `auto_deliver`
- **伪任务**:虽然叫任务,但实际只是包装了单个指令,如 `get_resume``get_job_list`
- **指令Command**:执行层面的操作单元,代表一个具体的操作
- 通过 MQTT 发送到客户端执行
- 如:`getOnlineResume``getJobList``applyJob`
- **关系**
- 真正的任务包含多个指令,指令按顺序执行
- 伪任务只是指令的包装,本质上就是直接执行指令
- **管理**:任务由任务队列管理,指令由指令管理器管理
- **通信**:任务在服务端内部调度,指令通过 MQTT 发送到客户端
- **当前状态**
- 目前系统中只有 `auto_deliver` 是真正的任务(包含多个步骤)
- 其他如 `get_resume``get_job_list``send_chat``apply_job` 虽然叫任务,但实际只是指令的包装
这种设计实现了业务逻辑和执行逻辑的分离,提高了系统的灵活性和可维护性。伪任务的存在可能是为了统一的任务追踪和未来扩展。

View File

@@ -1,874 +0,0 @@
# 自动找工作系统 - 功能规划文档
> 版本: v1.0 | 规划日期: 2025-12-25 | 状态: 待开发
## 文档说明
本文档规划了自动找工作系统的未开发功能,按优先级分为4个方向共20项功能。每项功能包含现状分析、待开发内容和预期价值,可直接转化为开发任务。
## 优先级说明
- **HIGH**: 核心功能,对系统价值提升明显,建议优先开发
- **MEDIUM**: 重要优化,提升用户体验和系统性能,可分阶段实施
- **LOW**: 未来规划,可根据实际需求决定是否开发
---
## 第一部分: 功能完善和补充
**优先级: HIGH** | **预计工期: 4-6周**
### 1.1 自动聊天功能完善 ⭐⭐⭐⭐⭐
**现状分析**
- ✅ 聊天记录表结构完整 (`chat_records`)
- ✅ AI聊天内容生成基础框架 (`aiService.generateChatContent`)
- ✅ 聊天类型分类 (greeting/followup/interview/reply)
- ❌ AI生成器未完整实现
- ❌ 聊天时机判断逻辑缺失
- ❌ 多轮对话上下文管理未实现
**待开发内容**
1. **AI聊天内容生成器完整实现**
- 完善 Prompt 模板(不同场景)
- 集成简历信息和岗位描述
- 个性化内容生成(根据HR回复调整策略)
- 长度和语气控制
2. **聊天时机智能判断**
- HR查看后多久发消息(规则+AI预测)
- 避免过于频繁或过晚联系
- 根据不同平台特性调整策略
- 工作时间优先发送
3. **多轮对话上下文管理**
- 记录对话历史
- 上下文理解(避免重复询问)
- 话题延续和自然过渡
- 面试邀约智能识别和响应
4. **情感分析和回复策略调整**
- 分析HR回复的情感倾向
- 根据情感调整后续策略
- 识别拒绝信号(及时停止沟通)
- 识别兴趣信号(加大沟通力度)
**技术实现**
- 文件路径: `api/middleware/job/chatManager.js`
- 依赖: `aiService.js`, `chat_records表`
- 预计工期: 2周
**预期价值**
- 📈 HR回复率提升 30%+
- 📈 面试邀约率提升 20%+
- 💡 减少人工沟通成本 80%+
- ✨ 提供24小时自动化沟通能力
---
### 1.2 账号保活任务 ⭐⭐⭐⭐⭐
**现状分析**
- ✅ 配置项已有 (`pla_account.auto_active`, `active_interval`, `active_actions_json`)
- ✅ 任务类型定义 (`auto_active_account`)
- ❌ 执行逻辑未实现
- ❌ 行为模拟策略缺失
**待开发内容**
1. **定时浏览岗位模拟真实用户**
- 随机浏览岗位详情
- 模拟点击、滚动行为
- 页面停留时间随机化(10-60秒)
- 每日浏览次数控制(5-20次)
2. **随机时间间隔访问**
- 避免固定时间访问(容易被识别)
- 工作时间随机分布
- 模拟午休和下班后的访问
- 周末降低活跃频率
3. **多样化操作行为**
- 搜索岗位(随机关键词)
- 查看推荐岗位
- 浏览公司主页
- 修改简历可见性
- 更新最后活跃时间
4. **避免账号被标记为机器人**
- 行为模式随机化
- 添加鼠标轨迹模拟
- 操作速度人性化(不要太快)
- 避免连续大量操作
**技术实现**
- 文件路径: `api/middleware/schedule/taskHandlers.js` (新增 `handleAutoActiveTask`)
- MQTT指令: 新增 `auto_active` 操作类型
- 预计工期: 1周
**预期价值**
- 📉 账号封禁风险降低 70%+
- 📈 简历曝光率提升 40%+
- 🔒 账号在线状态保持稳定
- ✨ 自动维护账号活跃度
---
### 1.3 简历自动更新 ⭐⭐⭐⭐
**现状分析**
- ✅ 简历同步功能完整
- ✅ 简历信息存储完善
- ❌ 简历刷新逻辑未实现
- ❌ 简历优化建议缺失
**待开发内容**
1. **定时刷新简历排名**
- 每天自动刷新简历(提升排名)
- 最佳刷新时间智能选择(如早上9点)
- 通过MQTT下发刷新指令
- 记录刷新历史和效果
2. **简历内容优化建议**
- AI分析当前简历不足
- 给出具体优化建议(哪些技能需要补充)
- 对比同类岗位的简历特征
- 建议调整项目经验描述
3. **A/B测试不同简历版本效果**
- 支持多个简历版本
- 自动切换测试
- 统计不同版本的查看率和回复率
- 推荐最优版本
**技术实现**
- 文件路径: `api/middleware/job/resumeManager.js` (新增刷新方法)
- MQTT指令: 新增 `refresh_resume` 操作
- 数据库: `resume_info` 新增 `last_refresh_time` 字段
- 预计工期: 1周
**预期价值**
- 📈 简历曝光率提升 50%+
- 📈 查看率提升 30%+
- 💡 简历质量持续优化
- ✨ 自动维护简历新鲜度
---
### 1.4 岗位黑名单和收藏 ⭐⭐⭐⭐
**现状分析**
- ❌ 黑名单功能未实现
- ❌ 收藏功能未实现
- ❌ 岗位对比功能缺失
**待开发内容**
1. **公司黑名单**
- 不再投递某公司的岗位
- 黑名单原因记录(薪资虚标、工作内容不符等)
- 支持批量添加
- 黑名单导入导出
2. **岗位类型黑名单**
- 不再投递某类岗位(如外包、销售)
- 支持自定义黑名单关键词
- 黑名单优先级高于匹配规则
3. **收藏感兴趣岗位**
- 标记收藏岗位
- 收藏原因备注
- 收藏夹分类管理
- 收藏岗位状态跟踪(是否还在招聘)
4. **岗位对比功能**
- 多个岗位并排对比
- 对比维度: 薪资、技能要求、公司、地点、福利
- AI给出推荐意见
- 导出对比报告
**技术实现**
- 数据库: 新增 `job_blacklist`, `job_favorites`
- 文件路径: `api/controller_admin/` 新增相关API
- 前端: `admin/src/views/work/` 新增黑名单和收藏页面
- 预计工期: 1周
**预期价值**
- 📈 投递精准度提升 40%+
- 📉 无效投递减少 60%+
- 💡 提供个性化岗位管理
- ✨ 提高求职效率
---
### 1.5 多轮面试跟踪 ⭐⭐⭐⭐
**现状分析**
-`apply_records` 表有 `hasInterview` 字段
- ❌ 只记录是否有面试,未细分轮次
- ❌ Offer管理功能缺失
- ❌ 入职状态未追踪
**待开发内容**
1. **一面/二面/三面状态追踪**
- 新增面试轮次字段 (`interview_round`)
- 每轮面试时间、地点、面试官记录
- 面试类型(电话、视频、现场)
- 面试状态(待面试、已面试、通过、淘汰)
2. **面试反馈记录**
- 面试官反馈内容
- 面试问题记录
- 自我评价
- 改进建议
3. **Offer管理**
- Offer详情(薪资、福利、入职时间)
- 接受/拒绝/谈薪状态
- 谈薪记录(几轮谈判,最终结果)
- Offer对比(如有多个Offer)
4. **入职状态追踪**
- 入职日期
- 试用期状态
- 转正时间
- 离职时间(如有)
**技术实现**
- 数据库: `apply_records` 新增字段或新增 `interview_records`, `offer_records`
- 文件路径: `api/controller_admin/apply_records.js` 扩展
- 前端: `admin/src/views/work/apply_records.vue` 新增详情面板
- 预计工期: 1.5周
**预期价值**
- 📊 完整的求职生命周期管理
- 📈 面试准备更充分
- 💡 Offer决策更科学
- ✨ 提供长期职业数据积累
---
## 第二部分: AI能力增强
**优先级: HIGH** | **预计工期: 5-7周**
### 2.1 简历智能优化 ⭐⭐⭐⭐⭐
**现状分析**
- ✅ 简历AI分析基础功能 (`aiService.analyzeResume`)
- ✅ 竞争力评分、技能提取、优劣势分析
- ❌ 优化建议深度不足
- ❌ 针对性改进方案缺失
**待开发内容**
1. **AI简历分析深度提升**
- 细粒度分析(每个项目、每段经历)
- 识别简历中的弱项和亮点
- 对比行业优秀简历
- 生成详细分析报告
2. **简历改进建议(针对性)**
- 针对目标岗位给出定制化建议
- 建议补充哪些技能关键词
- 建议如何重写工作描述
- 建议哪些内容需要精简
3. **技能关键词优化建议**
- 分析热门技能关键词
- 建议替换为更专业的术语
- 建议技能顺序排列
- 建议补充相关技能
4. **项目经验描述优化**
- 使用STAR法则重写项目描述
- 量化项目成果(如提升XX%性能)
- 突出个人贡献和技术难点
- 精简冗长描述
**技术实现**
- 文件路径: `api/middleware/job/aiService.js` 新增 `optimizeResume` 方法
- Prompt工程: 设计专业的简历优化Prompt
- 前端: `resume_info_detail.vue` 新增优化建议面板
- 预计工期: 2周
**预期价值**
- 📈 简历竞争力提升 20%+
- 📈 查看率提升 35%+
- 📈 回复率提升 25%+
- 💡 简历质量专业化
---
### 2.2 面试问题预测 ⭐⭐⭐⭐
**待开发内容**
1. **基于岗位描述预测面试问题**
- 分析岗位职责和要求
- 预测技术问题(如React性能优化、数据库索引等)
- 预测行为问题(如团队合作、项目经验)
- 预测HR问题(如离职原因、职业规划)
2. **提供参考答案**
- 给出专业、结构化的答案
- 提供多种回答思路
- 标注答案亮点和注意事项
- 面试官可能的追问
3. **根据简历生成个性化回答**
- 结合简历中的项目经验
- 使用简历中的真实案例
- 避免空洞的回答
- 突出个人优势
**技术实现**
- 文件路径: `api/middleware/job/aiService.js` 新增 `predictInterviewQuestions` 方法
- 数据来源: `job_postings.jobDescription`, `resume_info`
- 前端: 新增 `interview_prep.vue` 面试准备页面
- 预计工期: 1.5周
**预期价值**
- 📈 面试通过率提升 30%+
- ⏱️ 面试准备时间减少 70%+
- 💡 回答更专业、更自信
- ✨ 提供全方位面试辅导
---
### 2.3 薪资谈判策略 ⭐⭐⭐⭐
**待开发内容**
1. **AI分析岗位薪资合理范围**
- 根据岗位要求和地区计算合理薪资
- 参考行业薪资数据(如拉勾、Boss薪资报告)
- 考虑公司规模和融资阶段
- 给出薪资范围建议(如18-22K)
2. **给出谈薪建议和话术**
- 什么时候开始谈薪(面试哪个阶段)
- 如何提薪资要求(不卑不亢)
- 谈判策略(如先等对方报价)
- 具体话术模板
3. **根据市场行情评估Offer价值**
- 对比市场平均薪资
- 综合评估(薪资+福利+发展空间)
- 识别低于市场价的Offer
- 给出接受/拒绝建议
**技术实现**
- 文件路径: `api/middleware/job/aiService.js` 新增 `analyzeSalary` 方法
- 数据源: 岗位描述、地区、公司、市场数据
- 前端: `offer_analysis.vue` Offer分析页面
- 预计工期: 1周
**预期价值**
- 💰 薪资平均提升 10-15%
- 📈 谈判成功率提升 40%+
- 💡 避免接受低薪Offer
- ✨ 提供科学的薪资决策
---
### 2.4 公司背景调查 ⭐⭐⭐⭐
**待开发内容**
1. **整合企查查/天眼查数据**
- 调用企查查API获取公司信息
- 公司基本信息(注册资本、成立时间、法人)
- 融资情况(融资轮次、投资方)
- 诉讼记录、欠薪记录
2. **AI分析公司发展前景**
- 根据融资情况评估发展阶段
- 根据招聘规模判断业务状态
- 根据行业趋势预测前景
- 给出公司评级(A/B/C/D)
3. **风险预警**
- 识别高风险公司(如多次欠薪、频繁裁员)
- 识别虚假招聘(如招聘周期异常长)
- 识别外包公司(尽管岗位描述未标注)
- 给出风险提示和建议
**技术实现**
- 文件路径: `api/services/company_background_service.js`
- 数据源: 企查查API、`company_info`
- 前端: `job_postings.vue` 新增公司背调入口
- 预计工期: 1.5周
**预期价值**
- 📉 入职风险降低 80%+
- 💡 避免进入高风险公司
- ✨ 提供全面的公司情报
- 🔍 识别隐藏的问题公司
---
### 2.5 职业发展路径 ⭐⭐⭐
**待开发内容**
1. **基于简历和目标生成职业规划**
- 分析当前职业阶段(初级/中级/高级)
- 根据目标岗位生成发展路径
- 列出需要补充的技能和经验
- 给出时间线规划(1年/3年/5年)
2. **技能提升建议**
- 推荐学习资源(课程、书籍、开源项目)
- 建议考取的证书
- 建议参加的技术社区
- 建议做的练手项目
3. **转行可行性分析**
- 评估转行难度
- 分析已有技能的可迁移性
- 给出转行路径建议
- 预测转行后的薪资变化
**技术实现**
- 文件路径: `api/middleware/job/aiService.js` 新增 `generateCareerPath` 方法
- 数据源: `resume_info`, 目标岗位描述
- 前端: 新增 `career_plan.vue` 职业规划页面
- 预计工期: 1.5周
**预期价值**
- 📊 提供长期职业指导
- 💡 明确发展方向和目标
- 📈 技能提升更有针对性
- ✨ 降低职业迷茫感
---
## 第三部分: 用户体验优化
**优先级: MEDIUM** | **预计工期: 4-5周**
### 3.1 数据可视化增强 ⭐⭐⭐⭐
**待开发内容**
1. **投递转化漏斗图**
- 投递数 → 查看数 → 回复数 → 面试数 → Offer数
- 每个环节的转化率
- 识别漏斗中的薄弱环节
- 对比不同平台的漏斗差异
2. **面试成功率趋势**
- 按时间展示面试通过率变化
- 分析成功率提升/下降原因
- 识别面试表现最好的时间段
- 给出改进建议
3. **薪资分布统计**
- 投递岗位的薪资分布
- Offer薪资分布
- 对比期望薪资和实际薪资
- 不同技能栈的薪资对比
4. **不同平台效果对比**
- Boss、猎聘等平台的效果对比
- 投递量、回复率、面试率对比
- 平台特点分析
- 推荐最优平台
**技术实现**
- 前端: `admin/src/views/statistics/` 新增统计页面
- ECharts图表: 漏斗图、折线图、柱状图、饼图
- API: `statistics_server.js` 新增统计接口
- 预计工期: 1周
**预期价值**
- 📊 数据洞察更直观
- 💡 快速发现问题环节
- 📈 数据驱动优化决策
- ✨ 提供专业的数据分析
---
### 3.2 实时通知系统 ⭐⭐⭐⭐
**待开发内容**
1. **浏览器通知**
- 面试邀约即时通知
- Offer通知
- 重要聊天消息通知
- 任务执行状态通知
2. **邮件通知**
- 每日投递报告
- 面试提醒(提前1天)
- 重要事件邮件
- 周报月报
3. **企业微信/钉钉集成**
- 通过企业微信机器人推送
- 支持快捷操作(如快速回复)
- 群组通知
- @指定人员
4. **关键事件提醒**
- 面试邀约(立即通知)
- Offer(立即通知)
- 简历被查看(可配置)
- 任务执行失败(立即通知)
**技术实现**
- 文件路径: `api/services/notification_service.js`
- 浏览器通知: Web Notification API
- 邮件: nodemailer
- 企业微信: 企业微信Webhook
- 预计工期: 1.5周
**预期价值**
- ⏱️ 响应时间缩短 90%+
- 📲 不错过任何重要消息
- 💡 多渠道及时触达
- ✨ 提供主动式消息推送
---
### 3.3 批量操作功能 ⭐⭐⭐
**待开发内容**
1. **批量启用/禁用账号**
- 勾选多个账号
- 一键启用/禁用
- 批量设置自动化开关
- 操作日志记录
2. **批量设置投递策略**
- 批量修改投递时间范围
- 批量修改每日上限
- 批量设置关键词过滤
- 应用模板到多个账号
3. **批量导出数据**
- 勾选导出字段
- 导出为CSV/Excel
- 定时导出任务
- 导出历史管理
**技术实现**
- 前端: 各列表页面新增批量操作工具栏
- API: 各模块新增批量操作接口
- 预计工期: 1周
**预期价值**
- ⏱️ 管理效率提升 80%+
- 💡 降低重复操作
- ✨ 提供便捷的批量工具
---
### 3.4 移动端适配 ⭐⭐⭐
**待开发内容**
1. **响应式布局优化**
- 适配手机、平板屏幕
- 菜单改为抽屉式
- 表格改为卡片式
- 图表自适应尺寸
2. **移动端专属操作界面**
- 大图标按钮
- 手势操作(滑动、长按)
- 底部操作栏
- 快捷入口
3. **快捷操作入口**
- 快捷查看今日统计
- 快捷查看任务状态
- 快捷回复聊天
- 快捷查看面试安排
**技术实现**
- 前端: 响应式CSS
- 使用 iView 的响应式组件
- 新增移动端专属组件
- 预计工期: 1.5周
**预期价值**
- 📱 随时随地管理
- 💡 提升移动端体验
- ✨ 扩大使用场景
---
### 3.5 智能推荐 ⭐⭐⭐
**待开发内容**
1. **推荐最适合的岗位**
- 基于简历和历史投递记录
- AI预测岗位匹配度
- 推荐优先投递的岗位
- 推荐理由说明
2. **推荐最佳投递时间**
- 分析不同时间投递的效果
- 推荐最佳投递时段
- 避开竞争激烈的时段
- 根据平台特性调整
3. **推荐优化策略**
- 分析数据找出问题
- 推荐具体改进措施
- 预测改进后的效果
- 跟踪改进效果
**技术实现**
- 文件路径: `api/middleware/job/recommendService.js`
- AI模型: 基于历史数据训练
- 前端: 首页新增推荐面板
- 预计工期: 1周
**预期价值**
- 📈 投递效果提升 25%+
- 💡 降低决策成本
- ✨ 提供智能化建议
---
## 第四部分: 系统性能提升
**优先级: MEDIUM** | **预计工期: 3-4周**
### 4.1 缓存策略优化 ⭐⭐⭐⭐
**待开发内容**
1. **Redis缓存热点数据**
- 缓存职位类型配置(5分钟→实时)
- 缓存统计数据(避免重复计算)
- 缓存简历信息(减少数据库查询)
- 缓存设备状态
2. **职位类型配置缓存**
- 当前5分钟缓存改为即时失效
- 配置更新时清除缓存
- 预加载常用配置
3. **简历信息缓存**
- 缓存最近查询的简历
- LRU淘汰策略
- 简历更新时清除缓存
**技术实现**
- Redis集成: `ioredis`
- 文件路径: `api/middleware/cache/cacheManager.js`
- 缓存策略: LRU, TTL
- 预计工期: 1周
**预期价值**
- 📈 响应速度提升 50%+
- 📉 数据库压力降低 60%+
- 💡 提升系统吞吐量
- ✨ 提供更快的用户体验
---
### 4.2 数据库查询优化 ⭐⭐⭐⭐
**待开发内容**
1. **慢查询分析和优化**
- 开启MySQL慢查询日志
- 分析TOP 10慢查询
- 优化SQL语句
- 添加必要索引
2. **索引优化**
- 分析现有索引使用情况
- 添加复合索引
- 删除冗余索引
- 定期索引维护
3. **分表分库策略**
- 大表分表(如 `chat_records` 按月分表)
- 历史数据归档
- 读写分离(可选)
**技术实现**
- 使用 `EXPLAIN` 分析查询
- Sequelize 索引配置优化
- 数据库迁移脚本
- 预计工期: 1周
**预期价值**
- 📈 查询性能提升 3-5倍
- 📉 慢查询数量减少 80%+
- 💡 数据库可支撑更大数据量
- ✨ 提升系统稳定性
---
### 4.3 任务队列扩展 ⭐⭐⭐
**待开发内容**
1. **支持更多任务类型**
- 定时报告生成
- 数据清理任务
- 批量操作任务
- 自定义任务
2. **任务优先级动态调整**
- 根据紧急程度调整优先级
- VIP用户任务优先执行
- 失败任务降低优先级
- 长时间等待的任务提升优先级
3. **任务失败自动重试优化**
- 更智能的重试策略
- 不同错误类型不同重试间隔
- 重试次数动态调整
- 重试失败后的降级处理
**技术实现**
- 文件路径: `api/middleware/schedule/taskQueue.js` 优化
- 新增任务类型处理器
- 优化优先级算法
- 预计工期: 1周
**预期价值**
- 📈 任务处理更稳定
- 💡 支持更复杂的任务场景
- ✨ 提供更灵活的任务管理
---
### 4.4 并发控制优化 ⭐⭐⭐
**待开发内容**
1. **增加并发设备数**
- 当前最多5个设备
- 支持动态配置并发数
- 根据服务器性能自动调整
- 支持分布式部署
2. **更精细的限流策略**
- 不同任务类型不同限流
- 不同平台不同限流
- 根据时间段动态调整
- API请求限流
3. **分布式锁机制**
- 避免多实例冲突
- Redis分布式锁
- 锁超时自动释放
- 死锁检测
**技术实现**
- Redis分布式锁: `redlock`
- 配置动态化
- 负载均衡策略
- 预计工期: 1周
**预期价值**
- 📈 系统吞吐量提升 2-3倍
- 💡 支持更大规模部署
- ✨ 提供企业级并发控制
---
### 4.5 日志和监控 ⭐⭐⭐⭐
**待开发内容**
1. **完善日志记录**
- 统一日志格式(JSON)
- 日志级别分级(DEBUG/INFO/WARN/ERROR)
- 敏感信息脱敏
- 日志文件按日期切割
2. **性能监控面板**
- API响应时间监控
- 数据库查询时间监控
- 任务执行时间监控
- 内存和CPU监控
3. **异常告警机制**
- 错误率超过阈值告警
- 任务失败立即告警
- 系统资源不足告警
- 钉钉/企业微信告警
4. **操作审计日志**
- 记录所有关键操作
- 操作人、操作时间、操作内容
- 敏感操作二次确认
- 审计日志导出
**技术实现**
- 日志库: `winston`
- 监控: `prometheus` + `grafana` (可选)
- 告警: `api/services/alert_service.js`
- 预计工期: 1.5周
**预期价值**
- 🔍 问题定位效率提升 80%+
- 📊 系统运行状态可视化
- 🚨 及时发现和处理异常
- ✨ 提供运维级别的监控
---
## 附录: 开发优先级建议
### 短期(1-2个月)
**优先开发高价值、低成本功能**
| 功能 | 优先级 | 预计工期 | 价值 |
|------|--------|----------|------|
| 自动聊天功能完善 | ⭐⭐⭐⭐⭐ | 2周 | HR回复率+30% |
| 账号保活任务 | ⭐⭐⭐⭐⭐ | 1周 | 封禁风险-70% |
| 简历智能优化 | ⭐⭐⭐⭐⭐ | 2周 | 竞争力+20% |
| 缓存策略优化 | ⭐⭐⭐⭐ | 1周 | 响应速度+50% |
**预计总工期: 6周**
### 中期(3-4个月)
**完善核心功能和AI能力**
| 功能 | 优先级 | 预计工期 | 价值 |
|------|--------|----------|------|
| 简历自动更新 | ⭐⭐⭐⭐ | 1周 | 曝光率+50% |
| 岗位黑名单和收藏 | ⭐⭐⭐⭐ | 1周 | 精准度+40% |
| 多轮面试跟踪 | ⭐⭐⭐⭐ | 1.5周 | 完整生命周期 |
| 面试问题预测 | ⭐⭐⭐⭐ | 1.5周 | 通过率+30% |
| 数据可视化增强 | ⭐⭐⭐⭐ | 1周 | 数据洞察更直观 |
**预计总工期: 6周**
### 长期(5-6个月)
**提升体验和系统性能**
| 功能 | 优先级 | 预计工期 | 价值 |
|------|--------|----------|------|
| 薪资谈判策略 | ⭐⭐⭐⭐ | 1周 | 薪资+10-15% |
| 公司背景调查 | ⭐⭐⭐⭐ | 1.5周 | 风险-80% |
| 实时通知系统 | ⭐⭐⭐⭐ | 1.5周 | 响应时间-90% |
| 日志和监控 | ⭐⭐⭐⭐ | 1.5周 | 定位效率+80% |
| 数据库查询优化 | ⭐⭐⭐⭐ | 1周 | 性能+3-5倍 |
**预计总工期: 6.5周**
---
## 总结
本规划文档共列出 **20项待开发功能**,分为4个优先级方向:
- **功能完善和补充** (5项, HIGH): 完善核心业务流程
- **AI能力增强** (5项, HIGH): 提升智能化水平
- **用户体验优化** (5项, MEDIUM): 改善交互和便捷性
- **系统性能提升** (5项, MEDIUM): 优化性能和稳定性
**预期开发周期**:
- 短期(1-2月): 6周
- 中期(3-4月): 6周
- 长期(5-6月): 6.5周
- **总计**: 约4.5个月
**预期收益**:
- 📈 整体求职成功率提升 **50%+**
- 📈 用户使用效率提升 **80%+**
- 📈 系统性能提升 **3-5倍**
- 💰 用户平均薪资提升 **10-15%**
---
**文档维护**: 开发团队
**最后更新**: 2025-12-25

View File

@@ -1,574 +0,0 @@
# 发布脚本使用说明
## 📋 概述
发布脚本 (`scripts/publish.js`) 用于自动化应用的构建和发布流程,包括:
1. 清理构建目录
2. 构建应用NSIS 或便携版)
3. 调用接口创建版本记录
4. 上传压缩包到 OSS
## 🚀 快速开始
### 安装依赖
确保已安装 `form-data` 依赖:
```bash
npm install form-data --save-dev
```
### 基本使用
```bash
# 发布 NSIS 安装包(默认)
npm run publish
# 或直接运行脚本
node scripts/publish.js
```
## 📝 命令选项
### 构建类型
```bash
# 发布 NSIS 安装包
npm run publish:nsis
# 或
node scripts/publish.js --type nsis
# 发布便携版
npm run publish:portable
# 或
node scripts/publish.js --type portable
```
### 发布说明
```bash
node scripts/publish.js --notes "修复了若干bug优化了性能"
```
### 强制更新
```bash
node scripts/publish.js --force
```
### 跳过步骤
```bash
# 仅上传已构建的文件(跳过构建)
node scripts/publish.js --skip-build
# 仅构建不上传
node scripts/publish.js --skip-upload
```
### 查看帮助
```bash
node scripts/publish.js --help
```
## 🔧 配置要求
### 1. 配置文件
确保 `config/appConfig.js` 中包含有效的配置:
```javascript
module.exports = {
api_urls: {
dev: "http://work.light120.com/api",
prod: "http://work.light120.com/api"
},
token: "your-token-here", // 必须配置有效的 token
// ...
};
```
### 2. API 接口
脚本需要以下 API 接口:
#### 接口 1创建版本记录
- **接口地址**: `POST /api/version/create`
- **请求头**:
```
Content-Type: application/json
Authorization: ${token}
```
- **请求参数**:
```json
{
"version": "1.0.0", // 必填版本号x.y.z 格式)
"platform": "win32", // 必填平台类型win32/darwin/linux
"arch": "x64", // 必填架构类型x64/ia32/arm64
"download_url": "https://...", // 必填:下载地址(上传后更新)
"file_path": "/path/to/file", // 必填:服务器文件路径
"file_size": 12345678, // 可选:文件大小(字节),不提供会自动计算
"file_hash": "sha256-hash", // 可选SHA256 哈希值,不提供会自动计算
"release_notes": "发布说明", // 可选:更新日志
"force_update": 0, // 可选是否强制更新1:是 0:否),默认 0
"status": 1 // 可选状态1:启用 0:禁用),默认 1
}
```
- **响应示例**:
```json
{
"code": 0,
"message": "版本创建成功",
"data": {
"id": 123,
"version": "1.0.0",
"platform": "win32",
"arch": "x64",
"download_url": "https://oss.example.com/path/to/file.exe",
"file_path": "/path/to/file.exe",
"file_size": 12345678,
"file_hash": "sha256-hash-value",
"release_notes": "发布说明",
"force_update": 0,
"status": 1,
"create_time": "2024-01-01 12:00:00"
}
}
```
- **错误响应**:
```json
{
"code": 400,
"message": "版本号不能为空" // 或其他错误信息
}
```
- **注意事项**:
- 版本号格式必须为 `x.y.z`1.0.0
- 平台类型必须为:`win32`、`darwin` 或 `linux`
- 架构类型必须为:`x64`、`ia32` 或 `arm64`
- 如果版本已存在(相同 version + platform + arch会返回错误
- 如果提供了 `file_path` 但未提供 `file_size` 或 `file_hash`,接口会自动计算
#### 接口 2上传文件到 OSS
- **接口地址**: `POST /api/file/upload_version`
- **请求头**:
```
Content-Type: multipart/form-data
Authorization: ${token}
```
- **请求参数**Form Data:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `file` | File | 是 | 文件内容(二进制) |
| `version` | String | 是 | 版本号1.0.0 |
| `platform` | String | 是 | 平台类型win32/darwin/linux |
| `arch` | String | 是 | 架构类型x64/ia32/arm64 |
| `file_hash` | String | 是 | SHA256 哈希值 |
| `file_size` | Number | 是 | 文件大小(字节) |
| `version_id` | Number | 否 | 版本记录 ID如果已创建版本记录 |
| `build_type` | String | 否 | 构建类型nsis/portable |
- **请求示例**(使用 form-data:
```javascript
const FormData = require('form-data');
const fs = require('fs');
const form = new FormData();
form.append('file', fs.createReadStream('./dist/app.exe'));
form.append('version', '1.0.0');
form.append('platform', 'win32');
form.append('arch', 'x64');
form.append('file_hash', 'sha256-hash-value');
form.append('file_size', 12345678);
form.append('version_id', 123); // 可选
// 发送请求
form.submit('http://api.example.com/api/file/upload_version', {
headers: {
'Authorization': `${token}`
}
}, callback);
```
- **响应示例**:
```json
{
"code": 0,
"message": "文件上传成功",
"data": {
"download_url": "https://oss.example.com/versions/win32/x64/app-1.0.0.exe",
"file_path": "versions/win32/x64/app-1.0.0.exe",
"oss_path": "https://oss.example.com/versions/win32/x64/app-1.0.0.exe",
"file_size": 12345678,
"file_hash": "sha256-hash-value"
}
}
```
- **错误响应**:
```json
{
"code": 400,
"message": "文件上传失败:文件大小不匹配" // 或其他错误信息
}
```
- **注意事项**:
- 文件会按照 `versions/{platform}/{arch}/{filename}` 的路径结构上传到 OSS
- 上传前会验证文件哈希值,确保文件完整性
- 上传成功后,建议调用 `/version/update` 接口更新版本记录的下载地址
- 大文件上传建议设置较长的超时时间(建议 10 分钟以上)
#### 接口 3更新版本下载地址可选
- **接口地址**: `POST /api/version/update`
- **请求头**:
```
Content-Type: application/json
Authorization: ${token}
```
- **请求参数**:
```json
{
"id": 123, // 必填:版本记录 ID
"download_url": "https://...", // 可选:下载地址
"file_path": "/path/to/file", // 可选:文件路径
"file_hash": "sha256-hash", // 可选:文件哈希值
"file_size": 12345678, // 可选:文件大小
"release_notes": "更新说明", // 可选:更新日志
"force_update": 1, // 可选:是否强制更新
"status": 1 // 可选:状态
}
```
- **响应示例**:
```json
{
"code": 0,
"message": "版本更新成功",
"data": {
"id": 123,
"version": "1.0.0",
"download_url": "https://oss.example.com/path/to/file.exe",
// ... 其他字段
}
}
```
- **使用场景**:
- 文件上传成功后,更新版本记录的下载地址
- 修改版本的发布说明或强制更新标志
- 启用或禁用某个版本
## 📦 发布流程
1. **清理构建目录**
- 删除 `dist` 目录及其所有内容
2. **构建应用**
- 根据构建类型执行 `electron-builder`
- NSIS: `electron-builder --win nsis`
- Portable: `electron-builder --win portable`
3. **查找构建产物**
- 在 `dist` 目录中查找 `.exe` 文件
- 按文件大小排序,优先处理主安装包
4. **创建版本记录**
- 调用 `POST /api/version/create` 接口
- 传递版本信息、平台、架构等
- 获取版本记录 ID用于后续更新
5. **上传文件到 OSS**
- 计算文件 SHA256 哈希值
- 使用 `multipart/form-data` 格式调用 `POST /api/file/upload_version` 接口
- 传递文件、版本信息、哈希值等参数
- 获取上传后的下载地址
6. **更新版本记录**
- 使用获取到的下载地址调用 `POST /api/version/update` 接口
- 更新版本记录的 `download_url` 和 `file_path` 字段
## ⚠️ 注意事项
1. **Token 配置**
- 确保 `config/appConfig.js` 中有有效的 `token`
- Token 用于 API 认证,格式为 `${token}`
- 所有接口请求都需要在请求头中包含 `Authorization: ${token}`
2. **接口路径**
- 所有接口路径前缀为 `/api`
- 完整路径示例:
- 创建版本:`POST http://api.example.com/api/version/create`
- 上传文件:`POST http://api.example.com/api/file/upload_version`
- 更新版本:`POST http://api.example.com/api/version/update`
3. **文件大小**
- 大文件上传可能需要较长时间
- 脚本默认超时时间为 10 分钟600000ms
- 建议大文件(>100MB增加超时时间到 30 分钟
4. **网络连接**
- 确保能够访问 API 服务器和 OSS
- 上传大文件时建议使用稳定的网络连接
- 建议在网络稳定的环境下执行发布流程
5. **版本号**
- 版本号从 `package.json` 的 `version` 字段读取
- 发布前确保版本号已更新
- 版本号格式必须符合 `x.y.z` 格式1.0.0
6. **构建产物**
- 脚本会自动查找 `dist` 目录中的 `.exe` 文件
- 排除 `builder` 和 `helper` 相关的辅助文件
- 确保构建产物文件名清晰,便于识别
7. **文件哈希验证**
- 上传前必须计算文件的 SHA256 哈希值
- 上传接口会验证文件哈希,确保文件完整性
- 哈希值不匹配会导致上传失败
8. **接口调用顺序**
- 建议先创建版本记录(获取 version_id
- 然后上传文件(可传递 version_id
- 最后更新版本记录的下载地址
- 也可以先上传文件,再创建版本记录并更新下载地址
## 🔍 故障排查
### 问题:上传失败
**可能原因**:
- Token 无效或过期
- API 接口地址不正确
- 网络连接问题
- 文件过大,超时
**解决方法**:
1. 检查 `config/appConfig.js` 中的 `token` 是否有效
2. 检查 API 地址是否正确
3. 检查网络连接
4. 如果文件很大,可以增加超时时间
### 问题:构建失败
**可能原因**:
- 缺少依赖
- electron-builder 配置错误
- 磁盘空间不足
**解决方法**:
1. 运行 `npm install` 安装所有依赖
2. 检查 `package.json` 中的 `build` 配置
3. 确保有足够的磁盘空间
### 问题:找不到构建产物
**可能原因**:
- 构建未成功完成
- 构建产物在其他位置
**解决方法**:
1. 检查构建日志,确认构建成功
2. 手动检查 `dist` 目录
3. 使用 `--skip-build` 选项,手动指定文件路径(需要修改脚本)
## 📝 示例
### 完整发布流程
```bash
# 1. 更新版本号(在 package.json 中)
# "version": "1.0.1"
# 2. 发布
npm run publish -- --notes "新功能:支持自动投递简历" --force
```
### 仅上传已构建的文件
```bash
# 1. 手动构建
npm run build:nsis
# 2. 仅上传
node scripts/publish.js --skip-build
```
### 测试发布(不上传)
```bash
# 构建并创建版本记录,但不上传文件
node scripts/publish.js --skip-upload
```
## 💻 接口调用示例
### Node.js 示例代码
#### 1. 创建版本记录
```javascript
const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs');
async function createVersion() {
const filePath = './dist/app-1.0.0.exe';
const stats = fs.statSync(filePath);
const fileSize = stats.size;
// 计算文件哈希
const fileBuffer = fs.readFileSync(filePath);
const fileHash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
const response = await axios.post('http://api.example.com/api/version/create', {
version: '1.0.0',
platform: 'win32',
arch: 'x64',
download_url: '', // 上传后更新
file_path: filePath,
file_size: fileSize,
file_hash: fileHash,
release_notes: '修复了若干bug优化了性能',
force_update: 0,
status: 1
}, {
headers: {
'Authorization': `${token}`,
'Content-Type': 'application/json'
}
});
return response.data.data; // 返回版本记录,包含 id
}
```
#### 2. 上传文件到 OSS
```javascript
const FormData = require('form-data');
const axios = require('axios');
const fs = require('fs');
const crypto = require('crypto');
async function uploadVersionFile(versionId) {
const filePath = './dist/app-1.0.0.exe';
const stats = fs.statSync(filePath);
const fileSize = stats.size;
// 计算文件哈希
const fileBuffer = fs.readFileSync(filePath);
const fileHash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
// 创建 FormData
const form = new FormData();
form.append('file', fs.createReadStream(filePath));
form.append('version', '1.0.0');
form.append('platform', 'win32');
form.append('arch', 'x64');
form.append('file_hash', fileHash);
form.append('file_size', fileSize);
if (versionId) {
form.append('version_id', versionId);
}
const response = await axios.post('http://api.example.com/api/file/upload_version', form, {
headers: {
'Authorization': `${token}`,
...form.getHeaders()
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
timeout: 600000 // 10 分钟超时
});
return response.data.data; // 返回上传结果,包含 download_url
}
```
#### 3. 更新版本下载地址
```javascript
async function updateVersionDownloadUrl(versionId, downloadUrl, fileHash) {
const response = await axios.post('http://api.example.com/api/version/update', {
id: versionId,
download_url: downloadUrl,
file_hash: fileHash
}, {
headers: {
'Authorization': `${token}`,
'Content-Type': 'application/json'
}
});
return response.data.data;
}
```
#### 4. 完整发布流程示例
```javascript
async function publishVersion() {
try {
// 1. 创建版本记录
console.log('创建版本记录...');
const version = await createVersion();
console.log('版本记录创建成功ID:', version.id);
// 2. 上传文件
console.log('上传文件到 OSS...');
const uploadResult = await uploadVersionFile(version.id);
console.log('文件上传成功,下载地址:', uploadResult.download_url);
// 3. 更新版本下载地址
console.log('更新版本下载地址...');
await updateVersionDownloadUrl(version.id, uploadResult.download_url, uploadResult.file_hash);
console.log('版本发布完成!');
} catch (error) {
console.error('发布失败:', error.response?.data || error.message);
throw error;
}
}
```
### cURL 示例
#### 创建版本记录
```bash
curl -X POST http://api.example.com/api/version/create \
-H "Authorization: your-token-here" \
-H "Content-Type: application/json" \
-d '{
"version": "1.0.0",
"platform": "win32",
"arch": "x64",
"download_url": "",
"file_path": "/path/to/file.exe",
"file_size": 12345678,
"file_hash": "sha256-hash-value",
"release_notes": "发布说明",
"force_update": 0,
"status": 1
}'
```
#### 上传文件
```bash
curl -X POST http://api.example.com/api/file/upload_version \
-H "Authorization: your-token-here" \
-F "file=@./dist/app-1.0.0.exe" \
-F "version=1.0.0" \
-F "platform=win32" \
-F "arch=x64" \
-F "file_hash=sha256-hash-value" \
-F "file_size=12345678" \
-F "version_id=123"
```
## 🔗 相关文档
- [API 配置说明](./API_CONFIG.md)
- [打包说明](./BUILD.md)
- [更新逻辑检查报告](./更新逻辑检查报告.md)

View File

@@ -1,100 +0,0 @@
# 命名规范统一方案
## 📋 命名规范标准
### 1. 文件命名规范
**统一使用下划线命名snake_case**
-`ai_service.js`
-`job_service.js`
-`pla_account_service.js`
-`job_manager_service.js`
-`chat_manager_service.js`
-`resume_manager_service.js`
-`aiService.js``ai_service.js`
-`jobManager.js``job_manager_service.js`
-`chatManager.js``chat_manager_service.js`
-`resumeManager.js``resume_manager_service.js`
### 2. 类命名规范
**统一使用大驼峰命名PascalCase**
-`AIService`
-`JobService`
-`JobManagerService`
-`ChatManagerService`
-`ResumeManagerService`
-`aiService``AIService`
-`JobManager``JobManagerService`
### 3. 目录命名规范
**统一使用小写+下划线snake_case**
-`services/` - 业务服务层
-`middleware/` - 中间件层
-`middleware/schedule/` - 调度系统
-`middleware/mqtt/` - MQTT通信
-`middleware/job/` → 移到 `services/` 并重命名
### 4. 服务文件命名规范
**所有服务文件统一以 `_service.js` 结尾**
-`ai_service.js`
-`job_service.js`
-`job_manager_service.js`
-`chat_manager_service.js`
-`resume_manager_service.js`
-`pla_account_service.js`
-`oss_tool_service.js` (重命名 `ossTool.js`)
## 🔄 需要重命名的文件
### services/ 目录
1. `ossTool.js``oss_tool_service.js`
2. `task_scheduler.js` → 标记为废弃或删除
### middleware/job/ 目录(移到 services/
1. `jobManager.js``services/job_manager_service.js`
2. `chatManager.js``services/chat_manager_service.js`
3. `resumeManager.js``services/resume_manager_service.js`
4. `aiService.js` → 合并到 `services/ai_service.js` 后删除
## 📁 整理后的目录结构
```
api/
├── services/ # 业务服务层
│ ├── index.js # 服务管理器
│ ├── ai_service.js # AI服务合并后
│ ├── job_service.js # 职位服务
│ ├── job_manager_service.js # 工作管理服务
│ ├── chat_manager_service.js # 聊天管理服务
│ ├── resume_manager_service.js # 简历管理服务
│ ├── pla_account_service.js # 账号服务
│ └── oss_tool_service.js # OSS服务
└── middleware/ # 中间件层
├── schedule/ # 调度系统
├── mqtt/ # MQTT通信
├── dbProxy.js # 数据库代理
└── logProxy.js # 日志代理
```
## 🎯 执行步骤
1. **重命名现有文件**
- `ossTool.js``oss_tool_service.js`
2. **移动并重命名业务服务**
- `middleware/job/jobManager.js``services/job_manager_service.js`
- `middleware/job/chatManager.js``services/chat_manager_service.js`
- `middleware/job/resumeManager.js``services/resume_manager_service.js`
3. **合并AI服务**
-`middleware/job/aiService.js` 的功能合并到 `services/ai_service.js`
- 删除 `middleware/job/aiService.js`
4. **更新所有引用**
- 更新 `command.js` 中的引用
- 更新其他文件中的引用
5. **统一类命名**
- 确保所有类都使用 PascalCase
- 确保所有服务类都以 Service 结尾

View File

@@ -1,68 +0,0 @@
# 命名规范统一进度
## ✅ 已完成
### 1. OSS服务重命名
-`ossTool.js``oss_tool_service.js`
-`OSSTool``OSSToolService`
- ✅ 更新引用:`api/controller_front/file.js`
## 📋 待完成
### 2. 移动并重命名业务服务middleware/job/ → services/
-`jobManager.js``services/job_manager_service.js`
- 类名:`JobManager``JobManagerService`
- 更新引用:`api/middleware/schedule/command.js`
-`chatManager.js``services/chat_manager_service.js`
- 类名:`ChatManager``ChatManagerService`
- 更新引用:`api/middleware/schedule/command.js`
-`resumeManager.js``services/resume_manager_service.js`
- 类名:`ResumeManager``ResumeManagerService`
### 3. 合并AI服务
- ⏳ 将 `middleware/job/aiService.js` 的功能合并到 `services/ai_service.js`
- ⏳ 统一类名为 `AIService`
- ⏳ 删除 `middleware/job/aiService.js`
- ⏳ 更新所有引用
### 4. 处理废弃文件
-`services/task_scheduler.js` - 添加废弃标记或删除
## 📝 命名规范总结
### 文件命名规范
- ✅ 统一使用 `snake_case` + `_service.js` 后缀
- ✅ 示例:`oss_tool_service.js`, `job_manager_service.js`
### 类命名规范
- ✅ 统一使用 `PascalCase` + `Service` 后缀
- ✅ 示例:`OSSToolService`, `JobManagerService`
### 目录结构
```
api/
├── services/ # 业务服务层
│ ├── ai_service.js
│ ├── job_service.js
│ ├── job_manager_service.js # 待移动
│ ├── chat_manager_service.js # 待移动
│ ├── resume_manager_service.js # 待移动
│ ├── pla_account_service.js
│ └── oss_tool_service.js # ✅ 已完成
└── middleware/ # 中间件层
├── schedule/
├── mqtt/
└── job/ # 待删除文件移到services后
```
## 🔄 下一步操作
1. 移动并重命名 `middleware/job/` 下的文件
2. 合并AI服务
3. 更新所有引用
4. 统一导出方式
5. 删除废弃文件

View File

@@ -1,860 +0,0 @@
{
"baseInfo": {
"name": "张成",
"nickName": "张成",
"tiny": "https://img.bosszhipin.com/beijin/upload/avatar/20250211/607f1f3d68754fd006d510844c0273d99b737108b9d73a664006f977785f3a6a694eb527b0e564d8_s.png.webp",
"birthday": "19930612",
"age": "32岁",
"gender": 1,
"degree": 203,
"degreeCategory": "本科",
"account": "193******69",
"emailBlur": "978****03@qq.com",
"weixinBlur": "z56***01",
"freshGraduate": 0,
"workYears": 11,
"nameShowType": 0,
"bossCert": 0,
"userCert": 2,
"certGender": true,
"certBirth": true,
"startWorkDate": 20141201,
"applyStatus": 0,
"workYearDesc": "10年以上经验",
"resumeStatus": 0,
"resumeCount": 0,
"complete": false,
"weiXinSecurityUid": null,
"garbageFieldList": null,
"hometown": 0,
"hometownName": null,
"studyAbroadCertPass": 0,
"highestEduExp": null,
"showF3Optimize": 0,
"startWorkDateDesc": "2014-12",
"birthdayDesc": "1993-06"
},
"userDesc": "拥有10年深厚行业经验的资深前端架构师专注前沿技术与业务融合推动数字化产品创新升级。\n \n1. 技术栈与架构熟练运用Vue、React等主流框架搭配Webpack、Vite等构建工具进行高效开发。精通TypeScript优化代码结构与维护性。擅长使用Redux、MobX管理复杂应用状态搭建稳定架构。\n \n2. 跨端与组件化掌握Flutter、React Native等跨端技术实现多平台无缝运行。主导设计高复用组件库利用Storybook管理组件使组件复用率达70%开发周期缩短40%。\n \n3. AI集成积极探索AI与前端融合集成GPT等大模型实现智能客服、内容生成引入机器学习算法实现用户行为分析、个性化推荐大幅提升用户参与度。\n \n4. 音视频处理具备音视频处理能力使用WebRTC实现实时通信结合FFmpeg进行格式转换、剪辑。利用Media Source Extensions实现自适应码率播放优化视频加载与播放体验。\n\n 5. 后端及多元开发擅长使用Node.js搭配Express、Koa框架搭建高性能后端服务优化接口响应速度。熟练运用Python进行数据处理、自动化脚本编写结合Django、Flask框架开发后端应用在数据挖掘与分析领域成果显著。掌握C#语言,基于.NET平台进行Windows桌面应用开发具备丰富的Windows Forms、WPF项目经验实现全栈技术链路的打通。",
"applyStatus": 0,
"lastlast_modify_time": "2025.11.05 13:41",
"workEduDesc": "宝尊电子商务·架构师",
"expectList": [
{
"id": "dfc1777a2703c9071nVz3d-0GVpYxA~~",
"expectType": 0,
"position": 100123,
"positionName": "全栈工程师",
"customPositionId": "",
"positionType": 0,
"location": 101020100,
"locationName": "上海",
"subLocation": 0,
"subLocationName": null,
"lowSalary": 20,
"highSalary": 30,
"salaryDesc": "20-30K",
"salaryDescNew": "20-30K",
"industryList": [],
"industryDesc": "行业不限",
"suggestPosition": "",
"applyStatus": 0,
"freshGraduate": 0,
"doneWorkPositionList": null,
"garbageFieldList": null,
"interestPositionList": null,
"interestLocationList": null,
"industryExpect": false,
"tagName": null
}
],
"workExpList": [
{
"id": "9bd3116c0333c52d1nJ_09y-EldZx465V_-X",
"companyName": "上海宝尊电子商务有限公司",
"industry": {
"code": 100020,
"name": "互联网"
},
"department": "",
"customPositionName": "架构师",
"position": 100704,
"positionName": "架构师",
"positionLv2": 100700,
"startDate": "2019.06",
"startDateStr": "2019.06",
"endDate": "2024.12",
"endDateStr": "2024.12",
"emphasis": [],
"workContent": "智能视频剪辑系统(2021.07-2024.12)\n技术栈:Vue2+Element UI+WebAssembly+Canvas+Konva.js+WebSocket+GSAP\n核心功能:\n1.视频处理:基于WebAssembly解析视频信息,实现大文件分片上传,支持200G+视频处理\n2.创意编辑:使用Konva.js开发多层级编辑器,实现图片分层、文字动画、贴片特效\n3.动画系统:基于GSAP开发文字特效、Logo动画,支持动态片头片尾制作\n4.预览系统:使用Canvas实现视频片段预览,支持时间轴精确定位\n5.任务管理:基于WebSocket实现批量任务进度实时通知\n项目成果:视频处理效率提升300%|日均处理1000+视频|压缩率达97%|任务效率提升200%|降低\n人工成本60%",
"workPerformance": "",
"isPublic": 1,
"workType": 1,
"suggestToDel": 0,
"garbageFieldList": null
},
{
"id": "283b1abcae6041e41nJ_0969ElpVy423Vvic",
"companyName": "上海航天动力科技工程有限公司",
"industry": {
"code": 100021,
"name": "计算机软件"
},
"department": "",
"customPositionName": "架构师",
"position": 100704,
"positionName": "架构师",
"positionLv2": 100700,
"startDate": "2018.06",
"startDateStr": "2018.06",
"endDate": "2019.06",
"endDateStr": "2019.06",
"emphasis": [],
"workContent": "技术栈:Vue2+iView+OpenLayers+Cesium+WebSocket+Less\n核心功能:\n1.GIS可视化:基于OpenLayers实现管网GIS展示,支持多图层管理\n2.实时监控:使用WebSocket推送报警信息,实现管道水流方向动画\n3.数据分析:集成ECharts开发运营分析、报表统计功能\n4.空间分析:使用Turf.js实现等差线绘制,可视化爆管位置\n项目成果:地图加载提升200%|漏损检测准确率95%|节省成本300万+|服务10+水务公司|覆盖管\n网1000km+",
"workPerformance": "",
"isPublic": 1,
"workType": 1,
"suggestToDel": 0,
"garbageFieldList": null
},
{
"id": "d18fc7f74a6479ee1nJ_0969ElpVy423Vvid",
"companyName": "上海开澜软件有限公司",
"industry": {
"code": 100021,
"name": "计算机软件"
},
"department": "",
"customPositionName": ".NET",
"position": 100107,
"positionName": ".NET",
"positionLv2": 100100,
"startDate": "2016.06",
"startDateStr": "2016.06",
"endDate": "2018.06",
"endDateStr": "2018.06",
"emphasis": [
"HTML"
],
"workContent": "滩涂造地BIM管理系统(2017.01-2018.05)\n技术栈:jQuery+EasyUI+BIMViz+百度图+WebAppOffice\n核心功能:\n1.BIM可视化:集成BIMViz实现模型在线预览,支持构件查询和联动\n2.地图集成:基于百度地图API实现工程位置展示和空间分析\n3.文档管理:使用WebAppOffice实现在线预览,支持多格式文档\n4.工作流程:开发OA审批流程,实现物料申请和人员管理\n项目成果:BIM性能提升200%|审批效率提升150%|支持50+文档格式|日均处理500+工单|管理效\n率提升80%",
"workPerformance": "",
"isPublic": 1,
"workType": 1,
"suggestToDel": 0,
"garbageFieldList": null
},
{
"id": "c07196b9b117210f1nJ_0969ElpVy423Vvie",
"companyName": "上海加谷网络科技有限公司",
"industry": {
"code": 100002,
"name": "游戏"
},
"department": "",
"customPositionName": ".NET",
"position": 100107,
"positionName": ".NET",
"positionLv2": 100100,
"startDate": "2014.06",
"startDateStr": "2014.06",
"endDate": "2016.05",
"endDateStr": "2016.05",
"emphasis": [
"HTML"
],
"workContent": "H5营销平台开发(2014.06-2016.05)\n技术栈:jQuery+Canvas+CSS3+HTML5+微信JSSDK\n核心功能:\n1.互动游戏:开发大转盘抽奖、刮刮卡、砸金蛋等H5游戏\n2.动画特效:自研Canvas动画框架,支持Flash动画转换\n3.低代码平台:开发可视化搭建工具,支持营销活动快速生成\n4.社交功能:集成微信分享、支付、授权等功能项目成果:服务200+品牌|开发效率提升300%|上线周期缩短80%|转化率提升150%|支持百万级访问",
"workPerformance": "",
"isPublic": 1,
"workType": 1,
"suggestToDel": 0,
"garbageFieldList": null
}
],
"projectExpList": [
{
"id": "903b8e07eb7f44ca1nx50t-6EFZQw4i8UvKY",
"name": "AI智能视频剪辑系统",
"roleName": "全栈工程师",
"url": "",
"startDate": "2024.01",
"endDate": "",
"projectDesc": "系统简介\nai智能视频剪辑系统,可通过淘宝直播间,天猫,淘宝商品链接,自动从直播间中按照商品切片,并输出到天猫商品详情页主视频,发布到小红书,淘宝微淘等平台,可批量生成视频,处理视频,前端技术栈:vue全家桶+elemeui+浏览器插件\n后端技术栈:Node.js+Koa2+Vue2+FFmpeg+Redis\n功能模块:\n1.内容采集:开发Chrome浏览器插件自动获取直播回放、商品时间点智能识别、多平台视频源导入本地大视频文件上传,支持200g左右大文件批量上传,断点续传等功能\n2.任务管理:用户可以批量创建任务,任务实时状态使用WebSocket更新并通知\n3.模版管理:可基于execl文件批量创建任务,动态生成片尾图和首图\n4.智能生成:基于GPT的视频内容理解、关键片段提取和智能场景组合、60秒精华自动生成\n5.内容编辑:多轨道编辑和场景识别分割、转场特效库和字幕样式编辑、贴纸滤镜和音频处理、GSAP文字动画和Logo动效\n6.深度剪辑:用户可以拖动句子,自动以切片输出内容\n7.发布管理:多格式导出和自定义分辨率码率、批量导出队列和云端渲染、进度实时展示\n8.视频特效开发:如:文字抖动,花字,特效,特殊字幕,贴片动画等工作成果:\n1.使用FFmpeg开发视频处理引擎,支持多种视频格式转码,压缩进度回调\n2.设计任务队列系统,服务器CPU使用率控制在70%以下,使用负载均衡分摊压力,实现任务批量处理\n3.集成GPT模型,视频内容理解准确率达85%,精华片段提取效率提升200%\n4.实现大文件分片上传,支持断点续传,上传成功率99%\n5.开发完整的日志追踪系统,问题定位时间缩短,使用sse日志推送至前端页面可实时观察任务处理情况,接入报警机器人报警推送至企业微信群\n6.开发前端视频渲染引擎,图片编辑器,js逆向破解支持数据采集成功\n7.封装常用前端组件库,前端框架搭建与维护,指导其他组员开发,解决问题",
"performance": "",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2024.01",
"endDateStr": "至今"
},
{
"id": "2a6ee534976187341nx50t-6EVNWyoq-Vf-e",
"name": "水务DMA分区计量管理系统",
"roleName": "全栈工程师",
"url": "",
"startDate": "2023.07",
"endDate": "",
"projectDesc": "DMA (DistrictMetered Area)分区计量管理系统是一种先进的、专门应用于供水管网精细化管理的综合性系统。\n通过将供水管网划分成多个相对独立的计量区域(即DMA分区),在每个分区的进水口和出水口等关键节点安装高精度的计量设备,精确监测和记录水流的流入、流出情况,实现对各个分区内水量的实时计量与分析,其核心原理是基于封闭区域水量平衡理论,通过对比流入和流出水量等数据,精准定位可能存在的管网漏损点以及评估管网运行状态,助力供水企业实现科学管理、高效节水以及提升供水服务质量.\n前端技术栈:vue全家桶+elemeui,c#+wpf\n后端技术栈:Node.js+Koa2+mysq\n功能模块:\n1.实时数据处理:对DMA分区计量设备的水流数据进行清洗、校验和初步分析\n2.GIS数据管理:提供接口实现与地理信息系统的数据交互,管理管网地理空间数据。\n3.数据存储:基于MongoDB设计数据存储架构,进行数据备份与恢复。\n4.管网可视化:通过地图展示供水管网地理空间数据,支持多种地图操作\n5.空间分析:实现管网连通性、最短路径、缓冲区等空间分析功能。\n6.地图交互:提供地图标注和测距工具。\n7.实时数据推送:利用WebSocket实时推送DMA分区计量和设备状态数据。\n8.智能报警:实时监测管网异常,生成报警信息并通知相关人员,对报警信息分类统计\n9.设备状态监控:实时获取计量设备工作状态、电量等信息,分析运行历史数据数据采集功能\n10.数据采集程序:使用C#的WPF开发可与多种RTU设备通信的采集程序\n11.动态解析:服务端动态接收并自动解析不同RTU设备的数据。\n12.数据展示与导出:以图表和表格展示采集数据,支持CSV格式动态导出。\n项目成果:支持10000+设备并发查询性能提升150%系统稳定性99.99",
"performance": "",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2023.07",
"endDateStr": "至今"
},
{
"id": "6aa05589bc43d7711nx50t-6EVNWyoq-Vf-d",
"name": "bb物语小程序商城",
"roleName": "全栈工程师",
"url": "",
"startDate": "2023.05",
"endDate": "",
"projectDesc": "技术架构:\n前端:Vue2+mpVue\n后端:node.js+koa2+sequelize\n数据库:MySQL+Redis\n功能模块:\n1.商品管理:商品分类、商品上架、库存管理、价格管理、商品搜索\n2.订单管理:订单创建、订单支付、订单取消、订单查询,订单统计\n3.用户管理:用户注册、用户登录、用户信息管理、用户积分、用户等级\n4.营销管理:优惠券管理、满减活动、限时抢购、拼团活动、积分兑换\n5.支付系统:微信支付、支付宝支付\n4.设计用户管理系统,支持用户注册、登录、信息管理和积分系统\n5.开发营销管理模块,支持优惠券、满减活动、限时抢购和拼团活动\n6.实现数据分析功能,支持销售数据统计,用户行为分析、商品热度分析、营销效果分析\n工作成果:\n1.实现商品管理系统,支持商品的分类、上架、库存和价格管理\n2.开发订单管理模块,支持订单的创建、支付、取消和查询\n3.集成多种支付方式,支持微信支付、支付宝支付\n4.设计用户管理系统,支持用户注册、登录、信息管理和积分系统\n5.开发营销管理模块,支持优惠券、满减活动、限时抢购和拼团活动\n6.实现数据分析功能,支持销售数据统计,用户行为分析和商品热度分析\n7.支持三级分销,以及销售人员工资统计",
"performance": "",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2023.05",
"endDateStr": "至今"
},
{
"id": "32010e34944e04851nJ_0969E1JQx4q-Vfuc",
"name": "AI智能写手系统",
"roleName": "全栈开发工程师",
"url": "",
"startDate": "2020.03",
"endDate": "2021.07",
"projectDesc": "技术栈:Vue2+Element Ul+WebSocket+jWT+Less+Webpack+Node.js\n核心功能:\n1.平台管理:基于IWT的权限控制,实现品牌管理、角色权限、秀场墙功能\n2.内容制作:开发智能语句生成,作品库管理,多元化批量处理功能\n3.数据采集:Chrome插件开发,实现多平台商品信息自动采集\n4.多端适配:使用Rem+Flex实现响应式布局,支持不同屏幕自适应\n5.自动发布:开发多平台发布插件,支持淘宝天猫京东等平台内自动发布\n6.定时任务:基于Node.js实现定时发布、数据同步、内容更新等动化任务\n项目成果:开发效率提升200%|首屏加载800ms|数据处理效率提升300%|服务10+品牌|运营效率\n提升400%|任务成功率99.5%",
"performance": "开发效率提升200%首屏加载800ms数据处理效率提升300%服务10+品牌运营效率\n提升400%任务成功率99.5%",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2020.03",
"endDateStr": "2021.07"
},
{
"id": "1dc51326ce11bc601nJ_0969E1JQx4q-Vfud",
"name": "易分析取数工具",
"roleName": "全栈开发工程师",
"url": "",
"startDate": "2019.08",
"endDate": "2020.03",
"projectDesc": "系统简介\n获取天猫,淘宝,腾讯,小红书蒲公英商家后台数据,用与大数据做营销精细化运营,支持人群画像,地域,人群,粉丝,购买,等一些列数据,以及达人数据粉丝数,带货数,直播场次,直播效果,商品评论,数据做分析\n前端技术栈:vue全家桶+elemeui+浏览器插件\n后端技术栈:Node.js+Koa2+mysql,Pythopn+Selenium+Flask\n功能模块:\n1.前端数据采集:淘宝数据银行API对接,策略中心数据抓取,生意参谋数据同步,营销数据实时获取\n2.后端数据采集:\n3.插件功能:多平台数据采集插件、自定义数据抓取规则、请求拦截和数据过滤、离线数据缓存\n4.数据处理:多维度数据整合分析、数据清洗和结构化、自定义数据导出、实时数据同步\n5.监控预警:数据采集任务监控、异常采集预警,数据质量监控、采集额度管理\n工作成果:\n1.支持淘宝全站数据采集,日均处理5000+数据\n2.开发通用数据采集引擎,支持自定义采集规则\n3.实现数据实时同步,延迟<500ms\n4.分布式爬虫系统架构,支持分布式抓取",
"performance": "1. 设计插件热更新方案,实现核心模块动态替换和状态保持\n2. 开发多浏览器兼容层,解决不同浏览器API差异问题实现插件配置动态更新和按需加载机制\n3. 使用Sequelize设计数据模型,处理复杂的表关联和数据同步重写浏览器原生Ajax,实现智能请求拦截和缓存策略\n4. 开发分布式日志系统,支持问题快速定位",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2019.08",
"endDateStr": "2020.03"
},
{
"id": "e4294f91116b15dc1nJ_0969E1JQx4q-Vfue",
"name": "全工况智能终端采集系统",
"roleName": ".net软件工程师",
"url": "",
"startDate": "2018.05",
"endDate": "2018.08",
"projectDesc": "技术栈:WPF+Socket+InfluxDB+PostgreSQL\n核心功能:\n1.数据采集:实现Socket高并发通信,支持多协议解析\n2.实时监控:开发设备状态实时监控界面\n3.数据存储:设计混合数据库方案,优化查询性能\n4.可视化:实现数据实时展示和趋势分析\n项目成果:支持10000+设备并发|查询性能提升150%|系统稳定性99.99%|支持亿级数据毫秒级查询",
"performance": "1.实现百万级数据快速查询,平均查询时间<500ms\n2.设计实时数据动态渲染方案\n3.优化数据库性能,批量写入效率提升50%\n4.实现数据自动分级存储和归档\n5.开发设备状态实时监控系统",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2018.05",
"endDateStr": "2018.08"
},
{
"id": "96160107ede0ae851nJ_0969E1JQx4q-Vfuf",
"name": "宝山排水证管理系统",
"roleName": ".net软件工程师",
"url": "",
"startDate": "2017.01",
"endDate": "2017.07",
"projectDesc": "技术栈:AngularjS+KendoUI+lonic+TypeScript+MongoDE\n核心功能:\n1.PC端开发:基于KendoUI快速构建后台管理界面,实现许可证全流程管理\n2.移动端开发:使用lonic+Aneular开发跨平台App,支持iOS和Androic\n3.表单设计:实现动态表单配置,支持多类型数据录入和自定义校验规则\n4.流程管理:开发审批流程引擎,支持条件分支、并行审批、委托授权\n5.地图功能:集成百度地图,实现排水户分布展示和空间位置选择\n6.统计分析:开发数据可视化大屏,展示许可证办理情况和区域分布移动端技术难点:\n基于lonic+Cordova实现原生功能调用,如相机、定位、文件系统等解决Android返回键监听和iOS手势返冲突问题\n实现大文件分片上传和断点续传,支持现场照片批量上传\n开发自定义相机组件,支持照片压缩和水印添加使用SOLite实现本地数据存储,解决断网环境下的数据同光\n优化Scroll性能,解决长列表滚动卡顿问题\n处理键盘弹出时的页面布局自适应\n解决iOS和Android平台字体、样式兼容性问题\n实现应用内检查更新和增量更新功能\n优化首屏加载速度,实现资源按需加载\n项目成果:办理时间缩短80%|代码复用率80%|响应时间300ms|审批效率提升200%",
"performance": "1. 实现大文件分片上传和断点续传,支持现场照片批量上传开发自定义相机组件,支持照片压缩和水印添加使用SOLite实现本地数据存储,解决断网环境下的数据同光\n2. 优化Scroll性能,解决长列表滚动卡顿问题处理键盘弹出时的页面布局自适应解决iOS和Android平台字体、样式兼容性问题实现应用内检查更新和增量更新功能\n3. 优化首屏加载速度,实现资源按需加载",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2017.01",
"endDateStr": "2017.07"
},
{
"id": "c06dfe8c3678cf231nJ90tq7EVpQxo6_V_qW",
"name": "H5营销平台",
"roleName": ".net软件工程师",
"url": "",
"startDate": "2014.06",
"endDate": "2016.05",
"projectDesc": "技术栈:Vue2+Canvas+CSS3+微信ISSDK+.Net\n功能模块:\n1.游戏开发:开发大转盘抽奖、刮刮卡等H5游戏、自研Canvas动画框架、支持Flash动画转换\n2.内容管理:开发可视化搭建工具、支持营销活动快速生成、实现模板在线编辑\n3.社交功能:集成微信分享、支付、授权、开发用户数据分析、实现活动数据统计\n项目成果:服务200+品牌开发效率提升300%转化率提升150%",
"performance": "",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2014.06",
"endDateStr": "2016.05"
},
{
"id": "4e09afd8b41347c61nJ_0969E1JQx4q-VfqW",
"name": "物料ERP管理系统",
"roleName": "前端开发工程师",
"url": "",
"startDate": "2014.08",
"endDate": "2015.06",
"projectDesc": "技术栈:jQuery+Bootstrap+EasyUl+WebSocket+SQL Server\n核心功能:\n1.库存管理:实现物料库、出库、调拨、盘点等完整业务流程\n2.采购管理:开发供应商管理、询价比价、采购计划、订单跟踪功能\n3.生产管理:实现BOM管理、生产计划、物料需求计划(MRP)功能\n4.报表分析:开发库存周转率、采购分析、成本核算等统计报表\n5.预警提醒:设置库存预警、采购超期、价格异常等自动提醒\n技术难点:\n开发物料编码生成器,支持多级分类和自定义规则\n实现基于WebSocket的实时库存变更提醒\n设计MRP运算引擎,优化大批量数据处理性能\n开发报表导出功能,支持复杂表头和数据汇总项目成果:库存周转提升40%|采购成本降低15%|支持100+用户并发|日均处理3000+笔业务",
"performance": "1. 开发物料编码生成器,支持多级分类和自定义规则\n2. 实现基于WebSocket的实时库存变更提醒\n3. 设计MRP运算引擎,优化大批量数据处理性能\n4. 开发报表导出功能,支持复杂表头和数据汇",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2014.08",
"endDateStr": "2015.06"
}
],
"educationExpList": [
{
"id": "ee34188f32f9cecc1XF52NW5F1dT",
"schoolId": 2811,
"school": "武汉工程大学",
"major": "计算机应用技术(大数据方向)",
"degree": 203,
"eduType": 2,
"degreeName": "本科",
"startYear": "2021",
"endYear": "2024",
"educationDesc": "",
"country": "",
"tags": [
"卓越工程师计划"
],
"schoolType": 0,
"suggestToDel": 0,
"thesisTitle": "",
"thesisDesc": "",
"majorRanking": 0,
"courseList": [],
"badge": "https://img.bosszhipin.com/beijin/icon/bed7df948518127f74daa2ee178c44fc6bb61e3b7bce0931da574d19d1d82c88.jpg",
"certified": 0,
"garbageFieldList": null,
"startYearStr": "2021",
"endYearStr": "2024"
},
{
"id": "e095a1ceefdd0cc31XF52NW6FVZY",
"schoolId": 2831,
"school": "武汉软件工程职业学院",
"major": "软件技术",
"degree": 202,
"eduType": 1,
"degreeName": "大专",
"startYear": "2011",
"endYear": "2014",
"educationDesc": "",
"country": "",
"tags": [],
"schoolType": 0,
"suggestToDel": 0,
"thesisTitle": "",
"thesisDesc": "",
"majorRanking": 0,
"courseList": [],
"badge": "https://img.bosszhipin.com/beijin/icon/18282111c2fc8e191c5b6aedcece5a956bb61e3b7bce0931da574d19d1d82c88.jpg",
"certified": 0,
"garbageFieldList": null,
"startYearStr": "2011",
"endYearStr": "2014"
}
],
"socialContactList": null,
"volunteerExpList": null,
"certificationList": null,
"trainingExpList": null,
"designWorksImage": null,
"designWorksVideo": null,
"personalImage": null,
"deliciousFoodImage": null,
"garbage": {
"status": 0,
"reasonCode": 0,
"resumeDetailStatus": 0,
"garbageBaseInfo": null,
"baseInfo": null,
"garbageUserDesc": null,
"personalAdvantages": null,
"garbageWorkExp": null,
"workExp": null,
"garbageEduExp": null,
"eduExp": null,
"garbageProjectExp": null,
"projectExp": null,
"garbageVolunteerExp": null,
"volunteerExp": null,
"garbageExpectList": null,
"expect": null,
"garbageCertificationList": null,
"certification": null,
"designWorks": null,
"designWorksImageList": null,
"designWorksVideoList": null,
"garbageHandicapped": null,
"handicapped": null,
"trainingExp": null,
"clubExp": null,
"professionalSkill": null,
"honor": null
},
"editStatus": {
"canAddExpect": true,
"canAddWorkExp": true,
"canAddProjectExp": true,
"canAddEduExp": true,
"canAddSocialContact": true
},
"doneWorkPositionList": [],
"handicappedInfo": null,
"postExpList": [],
"myLabels": null,
"clubExpList": [],
"professionalSkill": null,
"honorList": [],
"feature": {
"showNewPositionStyle": 1,
"showHandicappedModule": 0,
"showResumeImportBtn": 1,
"showResumeImportBtnRedDot": false,
"showResumeFillHelper": 1,
"showTrainingExpModule": 0,
"showF3Optimize": 1,
"showPostExpModule": 0,
"webResumeLabelModule": 0,
"expectLocationInterestCombine": 0,
"stuMultiExpectChoose": 0,
"useNewStruct": 1
},
"virtualPartTimeCombineExpect": null,
"extendInfo": {
"shareUrl": "https://m.zhipin.com/mpa/html/resume-detail?sid=self&securityId=I8Nn8H-vv1Tt2-x1OxkE4U0hH697NrOJFEJF_pkEXnMTR6gLbbUXGWy9pFLFnOy9YeLxI-31CLT9aCmPeQ_YSVDoSj1AMLuej3IhRhkUgQzm98k4pG1F7XVZdNphlh2Mc8Wr2PltQNmRB2eJHwcx4j338ACBezr_YAvcjOQ~",
"shareText": "{\"showQQShare\":false,\"smsShareTitle\":\"牛人张成 10年以上工作经验目标 全栈工程师职位求靠谱Boss带走。迅速进入@Boss直聘把TA带走下载链接奉上https://m.zhipin.com/download?from=duanxin\",\"wbShareTitle\":\"#招聘#牛人张成 10年以上工作经验目标 全栈工程师职位求靠谱Boss带走。迅速进入@Boss直聘把TA带走下载链接奉上https://m.zhipin.com/download?from=weibo\",\"wxShareDesc\":\"经验10年以上工作经验 期望薪资20-30K\",\"wxShareTitle\":\"【Boss直聘】张成正在求职全栈工程师\"}"
},
"moduleList": [
{
"moduleType": 10,
"moduleName": "个人信息",
"customConfig": {
"showType": 0,
"desc": null,
"tag": null
},
"data": {
"name": "张成",
"nickName": "张成",
"tiny": "https://img.bosszhipin.com/beijin/upload/avatar/20250211/607f1f3d68754fd006d510844c0273d99b737108b9d73a664006f977785f3a6a694eb527b0e564d8_s.png.webp",
"birthday": "1993-06",
"age": "32岁",
"gender": 1,
"degree": 203,
"degreeCategory": "本科",
"account": "193******69",
"emailBlur": "978****03@qq.com",
"weixinBlur": "z56***01",
"freshGraduate": 0,
"workYears": 11,
"nameShowType": 0,
"bossCert": 0,
"userCert": 2,
"certGender": true,
"certBirth": true,
"startWorkDate": 20141201,
"applyStatus": 0,
"workYearDesc": "10年以上经验",
"resumeStatus": 0,
"resumeCount": 0,
"complete": false,
"weiXinSecurityUid": null,
"garbageFieldList": null,
"hometown": 0,
"hometownName": null,
"studyAbroadCertPass": 0,
"highestEduExp": null,
"showF3Optimize": 0,
"startWorkDateDesc": "2014-12",
"birthdayDesc": "1993-06"
},
"dataList": null
},
{
"moduleType": 11,
"moduleName": "个人优势",
"customConfig": {
"showType": 0,
"desc": null,
"tag": null
},
"data": {
"userDesc": "拥有10年深厚行业经验的资深前端架构师专注前沿技术与业务融合推动数字化产品创新升级。\n \n1. 技术栈与架构熟练运用Vue、React等主流框架搭配Webpack、Vite等构建工具进行高效开发。精通TypeScript优化代码结构与维护性。擅长使用Redux、MobX管理复杂应用状态搭建稳定架构。\n \n2. 跨端与组件化掌握Flutter、React Native等跨端技术实现多平台无缝运行。主导设计高复用组件库利用Storybook管理组件使组件复用率达70%开发周期缩短40%。\n \n3. AI集成积极探索AI与前端融合集成GPT等大模型实现智能客服、内容生成引入机器学习算法实现用户行为分析、个性化推荐大幅提升用户参与度。\n \n4. 音视频处理具备音视频处理能力使用WebRTC实现实时通信结合FFmpeg进行格式转换、剪辑。利用Media Source Extensions实现自适应码率播放优化视频加载与播放体验。\n\n 5. 后端及多元开发擅长使用Node.js搭配Express、Koa框架搭建高性能后端服务优化接口响应速度。熟练运用Python进行数据处理、自动化脚本编写结合Django、Flask框架开发后端应用在数据挖掘与分析领域成果显著。掌握C#语言,基于.NET平台进行Windows桌面应用开发具备丰富的Windows Forms、WPF项目经验实现全栈技术链路的打通。"
},
"dataList": null
},
{
"moduleType": 24,
"moduleName": "求职期望",
"customConfig": {
"showType": 0,
"desc": null,
"tag": null
},
"data": null,
"dataList": [
{
"id": "dfc1777a2703c9071nVz3d-0GVpYxA~~",
"expectType": 0,
"position": 100123,
"positionName": "全栈工程师",
"customPositionId": "",
"positionType": 0,
"location": 101020100,
"locationName": "上海",
"subLocation": 0,
"subLocationName": null,
"lowSalary": 20,
"highSalary": 30,
"salaryDesc": "20-30K",
"salaryDescNew": "20-30K",
"industryList": [],
"industryDesc": "行业不限",
"suggestPosition": "",
"applyStatus": 0,
"freshGraduate": 0,
"doneWorkPositionList": null,
"garbageFieldList": null,
"interestPositionList": null,
"interestLocationList": null,
"industryExpect": false,
"tagName": null
}
]
},
{
"moduleType": 12,
"moduleName": "工作经历",
"customConfig": {
"showType": 0,
"desc": null,
"tag": null
},
"data": null,
"dataList": [
{
"id": "9bd3116c0333c52d1nJ_09y-EldZx465V_-X",
"companyName": "上海宝尊电子商务有限公司",
"industry": {
"code": 100020,
"name": "互联网"
},
"department": "",
"customPositionName": "架构师",
"position": 100704,
"positionName": "架构师",
"positionLv2": 100700,
"startDate": "2019.06",
"startDateStr": "2019.06",
"endDate": "2024.12",
"endDateStr": "2024.12",
"emphasis": [],
"workContent": "智能视频剪辑系统(2021.07-2024.12)\n技术栈:Vue2+Element UI+WebAssembly+Canvas+Konva.js+WebSocket+GSAP\n核心功能:\n1.视频处理:基于WebAssembly解析视频信息,实现大文件分片上传,支持200G+视频处理\n2.创意编辑:使用Konva.js开发多层级编辑器,实现图片分层、文字动画、贴片特效\n3.动画系统:基于GSAP开发文字特效、Logo动画,支持动态片头片尾制作\n4.预览系统:使用Canvas实现视频片段预览,支持时间轴精确定位\n5.任务管理:基于WebSocket实现批量任务进度实时通知\n项目成果:视频处理效率提升300%|日均处理1000+视频|压缩率达97%|任务效率提升200%|降低\n人工成本60%",
"workPerformance": "",
"isPublic": 1,
"workType": 1,
"suggestToDel": 0,
"garbageFieldList": null
},
{
"id": "283b1abcae6041e41nJ_0969ElpVy423Vvic",
"companyName": "上海航天动力科技工程有限公司",
"industry": {
"code": 100021,
"name": "计算机软件"
},
"department": "",
"customPositionName": "架构师",
"position": 100704,
"positionName": "架构师",
"positionLv2": 100700,
"startDate": "2018.06",
"startDateStr": "2018.06",
"endDate": "2019.06",
"endDateStr": "2019.06",
"emphasis": [],
"workContent": "技术栈:Vue2+iView+OpenLayers+Cesium+WebSocket+Less\n核心功能:\n1.GIS可视化:基于OpenLayers实现管网GIS展示,支持多图层管理\n2.实时监控:使用WebSocket推送报警信息,实现管道水流方向动画\n3.数据分析:集成ECharts开发运营分析、报表统计功能\n4.空间分析:使用Turf.js实现等差线绘制,可视化爆管位置\n项目成果:地图加载提升200%|漏损检测准确率95%|节省成本300万+|服务10+水务公司|覆盖管\n网1000km+",
"workPerformance": "",
"isPublic": 1,
"workType": 1,
"suggestToDel": 0,
"garbageFieldList": null
},
{
"id": "d18fc7f74a6479ee1nJ_0969ElpVy423Vvid",
"companyName": "上海开澜软件有限公司",
"industry": {
"code": 100021,
"name": "计算机软件"
},
"department": "",
"customPositionName": ".NET",
"position": 100107,
"positionName": ".NET",
"positionLv2": 100100,
"startDate": "2016.06",
"startDateStr": "2016.06",
"endDate": "2018.06",
"endDateStr": "2018.06",
"emphasis": [
"HTML"
],
"workContent": "滩涂造地BIM管理系统(2017.01-2018.05)\n技术栈:jQuery+EasyUI+BIMViz+百度图+WebAppOffice\n核心功能:\n1.BIM可视化:集成BIMViz实现模型在线预览,支持构件查询和联动\n2.地图集成:基于百度地图API实现工程位置展示和空间分析\n3.文档管理:使用WebAppOffice实现在线预览,支持多格式文档\n4.工作流程:开发OA审批流程,实现物料申请和人员管理\n项目成果:BIM性能提升200%|审批效率提升150%|支持50+文档格式|日均处理500+工单|管理效\n率提升80%",
"workPerformance": "",
"isPublic": 1,
"workType": 1,
"suggestToDel": 0,
"garbageFieldList": null
},
{
"id": "c07196b9b117210f1nJ_0969ElpVy423Vvie",
"companyName": "上海加谷网络科技有限公司",
"industry": {
"code": 100002,
"name": "游戏"
},
"department": "",
"customPositionName": ".NET",
"position": 100107,
"positionName": ".NET",
"positionLv2": 100100,
"startDate": "2014.06",
"startDateStr": "2014.06",
"endDate": "2016.05",
"endDateStr": "2016.05",
"emphasis": [
"HTML"
],
"workContent": "H5营销平台开发(2014.06-2016.05)\n技术栈:jQuery+Canvas+CSS3+HTML5+微信JSSDK\n核心功能:\n1.互动游戏:开发大转盘抽奖、刮刮卡、砸金蛋等H5游戏\n2.动画特效:自研Canvas动画框架,支持Flash动画转换\n3.低代码平台:开发可视化搭建工具,支持营销活动快速生成\n4.社交功能:集成微信分享、支付、授权等功能项目成果:服务200+品牌|开发效率提升300%|上线周期缩短80%|转化率提升150%|支持百万级访问",
"workPerformance": "",
"isPublic": 1,
"workType": 1,
"suggestToDel": 0,
"garbageFieldList": null
}
]
},
{
"moduleType": 13,
"moduleName": "项目经历",
"customConfig": {
"showType": 0,
"desc": null,
"tag": null
},
"data": null,
"dataList": [
{
"id": "903b8e07eb7f44ca1nx50t-6EFZQw4i8UvKY",
"name": "AI智能视频剪辑系统",
"roleName": "全栈工程师",
"url": "",
"startDate": "2024.01",
"endDate": "",
"projectDesc": "系统简介\nai智能视频剪辑系统,可通过淘宝直播间,天猫,淘宝商品链接,自动从直播间中按照商品切片,并输出到天猫商品详情页主视频,发布到小红书,淘宝微淘等平台,可批量生成视频,处理视频,前端技术栈:vue全家桶+elemeui+浏览器插件\n后端技术栈:Node.js+Koa2+Vue2+FFmpeg+Redis\n功能模块:\n1.内容采集:开发Chrome浏览器插件自动获取直播回放、商品时间点智能识别、多平台视频源导入本地大视频文件上传,支持200g左右大文件批量上传,断点续传等功能\n2.任务管理:用户可以批量创建任务,任务实时状态使用WebSocket更新并通知\n3.模版管理:可基于execl文件批量创建任务,动态生成片尾图和首图\n4.智能生成:基于GPT的视频内容理解、关键片段提取和智能场景组合、60秒精华自动生成\n5.内容编辑:多轨道编辑和场景识别分割、转场特效库和字幕样式编辑、贴纸滤镜和音频处理、GSAP文字动画和Logo动效\n6.深度剪辑:用户可以拖动句子,自动以切片输出内容\n7.发布管理:多格式导出和自定义分辨率码率、批量导出队列和云端渲染、进度实时展示\n8.视频特效开发:如:文字抖动,花字,特效,特殊字幕,贴片动画等工作成果:\n1.使用FFmpeg开发视频处理引擎,支持多种视频格式转码,压缩进度回调\n2.设计任务队列系统,服务器CPU使用率控制在70%以下,使用负载均衡分摊压力,实现任务批量处理\n3.集成GPT模型,视频内容理解准确率达85%,精华片段提取效率提升200%\n4.实现大文件分片上传,支持断点续传,上传成功率99%\n5.开发完整的日志追踪系统,问题定位时间缩短,使用sse日志推送至前端页面可实时观察任务处理情况,接入报警机器人报警推送至企业微信群\n6.开发前端视频渲染引擎,图片编辑器,js逆向破解支持数据采集成功\n7.封装常用前端组件库,前端框架搭建与维护,指导其他组员开发,解决问题",
"performance": "",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2024.01",
"endDateStr": "至今"
},
{
"id": "2a6ee534976187341nx50t-6EVNWyoq-Vf-e",
"name": "水务DMA分区计量管理系统",
"roleName": "全栈工程师",
"url": "",
"startDate": "2023.07",
"endDate": "",
"projectDesc": "DMA (DistrictMetered Area)分区计量管理系统是一种先进的、专门应用于供水管网精细化管理的综合性系统。\n通过将供水管网划分成多个相对独立的计量区域(即DMA分区),在每个分区的进水口和出水口等关键节点安装高精度的计量设备,精确监测和记录水流的流入、流出情况,实现对各个分区内水量的实时计量与分析,其核心原理是基于封闭区域水量平衡理论,通过对比流入和流出水量等数据,精准定位可能存在的管网漏损点以及评估管网运行状态,助力供水企业实现科学管理、高效节水以及提升供水服务质量.\n前端技术栈:vue全家桶+elemeui,c#+wpf\n后端技术栈:Node.js+Koa2+mysq\n功能模块:\n1.实时数据处理:对DMA分区计量设备的水流数据进行清洗、校验和初步分析\n2.GIS数据管理:提供接口实现与地理信息系统的数据交互,管理管网地理空间数据。\n3.数据存储:基于MongoDB设计数据存储架构,进行数据备份与恢复。\n4.管网可视化:通过地图展示供水管网地理空间数据,支持多种地图操作\n5.空间分析:实现管网连通性、最短路径、缓冲区等空间分析功能。\n6.地图交互:提供地图标注和测距工具。\n7.实时数据推送:利用WebSocket实时推送DMA分区计量和设备状态数据。\n8.智能报警:实时监测管网异常,生成报警信息并通知相关人员,对报警信息分类统计\n9.设备状态监控:实时获取计量设备工作状态、电量等信息,分析运行历史数据数据采集功能\n10.数据采集程序:使用C#的WPF开发可与多种RTU设备通信的采集程序\n11.动态解析:服务端动态接收并自动解析不同RTU设备的数据。\n12.数据展示与导出:以图表和表格展示采集数据,支持CSV格式动态导出。\n项目成果:支持10000+设备并发查询性能提升150%系统稳定性99.99",
"performance": "",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2023.07",
"endDateStr": "至今"
},
{
"id": "6aa05589bc43d7711nx50t-6EVNWyoq-Vf-d",
"name": "bb物语小程序商城",
"roleName": "全栈工程师",
"url": "",
"startDate": "2023.05",
"endDate": "",
"projectDesc": "技术架构:\n前端:Vue2+mpVue\n后端:node.js+koa2+sequelize\n数据库:MySQL+Redis\n功能模块:\n1.商品管理:商品分类、商品上架、库存管理、价格管理、商品搜索\n2.订单管理:订单创建、订单支付、订单取消、订单查询,订单统计\n3.用户管理:用户注册、用户登录、用户信息管理、用户积分、用户等级\n4.营销管理:优惠券管理、满减活动、限时抢购、拼团活动、积分兑换\n5.支付系统:微信支付、支付宝支付\n4.设计用户管理系统,支持用户注册、登录、信息管理和积分系统\n5.开发营销管理模块,支持优惠券、满减活动、限时抢购和拼团活动\n6.实现数据分析功能,支持销售数据统计,用户行为分析、商品热度分析、营销效果分析\n工作成果:\n1.实现商品管理系统,支持商品的分类、上架、库存和价格管理\n2.开发订单管理模块,支持订单的创建、支付、取消和查询\n3.集成多种支付方式,支持微信支付、支付宝支付\n4.设计用户管理系统,支持用户注册、登录、信息管理和积分系统\n5.开发营销管理模块,支持优惠券、满减活动、限时抢购和拼团活动\n6.实现数据分析功能,支持销售数据统计,用户行为分析和商品热度分析\n7.支持三级分销,以及销售人员工资统计",
"performance": "",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2023.05",
"endDateStr": "至今"
},
{
"id": "32010e34944e04851nJ_0969E1JQx4q-Vfuc",
"name": "AI智能写手系统",
"roleName": "全栈开发工程师",
"url": "",
"startDate": "2020.03",
"endDate": "2021.07",
"projectDesc": "技术栈:Vue2+Element Ul+WebSocket+jWT+Less+Webpack+Node.js\n核心功能:\n1.平台管理:基于IWT的权限控制,实现品牌管理、角色权限、秀场墙功能\n2.内容制作:开发智能语句生成,作品库管理,多元化批量处理功能\n3.数据采集:Chrome插件开发,实现多平台商品信息自动采集\n4.多端适配:使用Rem+Flex实现响应式布局,支持不同屏幕自适应\n5.自动发布:开发多平台发布插件,支持淘宝天猫京东等平台内自动发布\n6.定时任务:基于Node.js实现定时发布、数据同步、内容更新等动化任务\n项目成果:开发效率提升200%|首屏加载800ms|数据处理效率提升300%|服务10+品牌|运营效率\n提升400%|任务成功率99.5%",
"performance": "开发效率提升200%首屏加载800ms数据处理效率提升300%服务10+品牌运营效率\n提升400%任务成功率99.5%",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2020.03",
"endDateStr": "2021.07"
},
{
"id": "1dc51326ce11bc601nJ_0969E1JQx4q-Vfud",
"name": "易分析取数工具",
"roleName": "全栈开发工程师",
"url": "",
"startDate": "2019.08",
"endDate": "2020.03",
"projectDesc": "系统简介\n获取天猫,淘宝,腾讯,小红书蒲公英商家后台数据,用与大数据做营销精细化运营,支持人群画像,地域,人群,粉丝,购买,等一些列数据,以及达人数据粉丝数,带货数,直播场次,直播效果,商品评论,数据做分析\n前端技术栈:vue全家桶+elemeui+浏览器插件\n后端技术栈:Node.js+Koa2+mysql,Pythopn+Selenium+Flask\n功能模块:\n1.前端数据采集:淘宝数据银行API对接,策略中心数据抓取,生意参谋数据同步,营销数据实时获取\n2.后端数据采集:\n3.插件功能:多平台数据采集插件、自定义数据抓取规则、请求拦截和数据过滤、离线数据缓存\n4.数据处理:多维度数据整合分析、数据清洗和结构化、自定义数据导出、实时数据同步\n5.监控预警:数据采集任务监控、异常采集预警,数据质量监控、采集额度管理\n工作成果:\n1.支持淘宝全站数据采集,日均处理5000+数据\n2.开发通用数据采集引擎,支持自定义采集规则\n3.实现数据实时同步,延迟<500ms\n4.分布式爬虫系统架构,支持分布式抓取",
"performance": "1. 设计插件热更新方案,实现核心模块动态替换和状态保持\n2. 开发多浏览器兼容层,解决不同浏览器API差异问题实现插件配置动态更新和按需加载机制\n3. 使用Sequelize设计数据模型,处理复杂的表关联和数据同步重写浏览器原生Ajax,实现智能请求拦截和缓存策略\n4. 开发分布式日志系统,支持问题快速定位",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2019.08",
"endDateStr": "2020.03"
},
{
"id": "e4294f91116b15dc1nJ_0969E1JQx4q-Vfue",
"name": "全工况智能终端采集系统",
"roleName": ".net软件工程师",
"url": "",
"startDate": "2018.05",
"endDate": "2018.08",
"projectDesc": "技术栈:WPF+Socket+InfluxDB+PostgreSQL\n核心功能:\n1.数据采集:实现Socket高并发通信,支持多协议解析\n2.实时监控:开发设备状态实时监控界面\n3.数据存储:设计混合数据库方案,优化查询性能\n4.可视化:实现数据实时展示和趋势分析\n项目成果:支持10000+设备并发|查询性能提升150%|系统稳定性99.99%|支持亿级数据毫秒级查询",
"performance": "1.实现百万级数据快速查询,平均查询时间<500ms\n2.设计实时数据动态渲染方案\n3.优化数据库性能,批量写入效率提升50%\n4.实现数据自动分级存储和归档\n5.开发设备状态实时监控系统",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2018.05",
"endDateStr": "2018.08"
},
{
"id": "96160107ede0ae851nJ_0969E1JQx4q-Vfuf",
"name": "宝山排水证管理系统",
"roleName": ".net软件工程师",
"url": "",
"startDate": "2017.01",
"endDate": "2017.07",
"projectDesc": "技术栈:AngularjS+KendoUI+lonic+TypeScript+MongoDE\n核心功能:\n1.PC端开发:基于KendoUI快速构建后台管理界面,实现许可证全流程管理\n2.移动端开发:使用lonic+Aneular开发跨平台App,支持iOS和Androic\n3.表单设计:实现动态表单配置,支持多类型数据录入和自定义校验规则\n4.流程管理:开发审批流程引擎,支持条件分支、并行审批、委托授权\n5.地图功能:集成百度地图,实现排水户分布展示和空间位置选择\n6.统计分析:开发数据可视化大屏,展示许可证办理情况和区域分布移动端技术难点:\n基于lonic+Cordova实现原生功能调用,如相机、定位、文件系统等解决Android返回键监听和iOS手势返冲突问题\n实现大文件分片上传和断点续传,支持现场照片批量上传\n开发自定义相机组件,支持照片压缩和水印添加使用SOLite实现本地数据存储,解决断网环境下的数据同光\n优化Scroll性能,解决长列表滚动卡顿问题\n处理键盘弹出时的页面布局自适应\n解决iOS和Android平台字体、样式兼容性问题\n实现应用内检查更新和增量更新功能\n优化首屏加载速度,实现资源按需加载\n项目成果:办理时间缩短80%|代码复用率80%|响应时间300ms|审批效率提升200%",
"performance": "1. 实现大文件分片上传和断点续传,支持现场照片批量上传开发自定义相机组件,支持照片压缩和水印添加使用SOLite实现本地数据存储,解决断网环境下的数据同光\n2. 优化Scroll性能,解决长列表滚动卡顿问题处理键盘弹出时的页面布局自适应解决iOS和Android平台字体、样式兼容性问题实现应用内检查更新和增量更新功能\n3. 优化首屏加载速度,实现资源按需加载",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2017.01",
"endDateStr": "2017.07"
},
{
"id": "c06dfe8c3678cf231nJ90tq7EVpQxo6_V_qW",
"name": "H5营销平台",
"roleName": ".net软件工程师",
"url": "",
"startDate": "2014.06",
"endDate": "2016.05",
"projectDesc": "技术栈:Vue2+Canvas+CSS3+微信ISSDK+.Net\n功能模块:\n1.游戏开发:开发大转盘抽奖、刮刮卡等H5游戏、自研Canvas动画框架、支持Flash动画转换\n2.内容管理:开发可视化搭建工具、支持营销活动快速生成、实现模板在线编辑\n3.社交功能:集成微信分享、支付、授权、开发用户数据分析、实现活动数据统计\n项目成果:服务200+品牌开发效率提升300%转化率提升150%",
"performance": "",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2014.06",
"endDateStr": "2016.05"
},
{
"id": "4e09afd8b41347c61nJ_0969E1JQx4q-VfqW",
"name": "物料ERP管理系统",
"roleName": "前端开发工程师",
"url": "",
"startDate": "2014.08",
"endDate": "2015.06",
"projectDesc": "技术栈:jQuery+Bootstrap+EasyUl+WebSocket+SQL Server\n核心功能:\n1.库存管理:实现物料库、出库、调拨、盘点等完整业务流程\n2.采购管理:开发供应商管理、询价比价、采购计划、订单跟踪功能\n3.生产管理:实现BOM管理、生产计划、物料需求计划(MRP)功能\n4.报表分析:开发库存周转率、采购分析、成本核算等统计报表\n5.预警提醒:设置库存预警、采购超期、价格异常等自动提醒\n技术难点:\n开发物料编码生成器,支持多级分类和自定义规则\n实现基于WebSocket的实时库存变更提醒\n设计MRP运算引擎,优化大批量数据处理性能\n开发报表导出功能,支持复杂表头和数据汇总项目成果:库存周转提升40%|采购成本降低15%|支持100+用户并发|日均处理3000+笔业务",
"performance": "1. 开发物料编码生成器,支持多级分类和自定义规则\n2. 实现基于WebSocket的实时库存变更提醒\n3. 设计MRP运算引擎,优化大批量数据处理性能\n4. 开发报表导出功能,支持复杂表头和数据汇",
"suggestToDel": 0,
"garbageFieldList": null,
"startDateStr": "2014.08",
"endDateStr": "2015.06"
}
]
},
{
"moduleType": 14,
"moduleName": "教育经历",
"customConfig": {
"showType": 0,
"desc": null,
"tag": null
},
"data": null,
"dataList": [
{
"id": "ee34188f32f9cecc1XF52NW5F1dT",
"schoolId": 2811,
"school": "武汉工程大学",
"major": "计算机应用技术(大数据方向)",
"degree": 203,
"eduType": 2,
"degreeName": "本科",
"startYear": "2021",
"endYear": "2024",
"educationDesc": "",
"country": "",
"tags": [
"卓越工程师计划"
],
"schoolType": 0,
"suggestToDel": 0,
"thesisTitle": "",
"thesisDesc": "",
"majorRanking": 0,
"courseList": [],
"badge": "https://img.bosszhipin.com/beijin/icon/bed7df948518127f74daa2ee178c44fc6bb61e3b7bce0931da574d19d1d82c88.jpg",
"certified": 0,
"garbageFieldList": null,
"startYearStr": "2021",
"endYearStr": "2024"
},
{
"id": "e095a1ceefdd0cc31XF52NW6FVZY",
"schoolId": 2831,
"school": "武汉软件工程职业学院",
"major": "软件技术",
"degree": 202,
"eduType": 1,
"degreeName": "大专",
"startYear": "2011",
"endYear": "2014",
"educationDesc": "",
"country": "",
"tags": [],
"schoolType": 0,
"suggestToDel": 0,
"thesisTitle": "",
"thesisDesc": "",
"majorRanking": 0,
"courseList": [],
"badge": "https://img.bosszhipin.com/beijin/icon/18282111c2fc8e191c5b6aedcece5a956bb61e3b7bce0931da574d19d1d82c88.jpg",
"certified": 0,
"garbageFieldList": null,
"startYearStr": "2011",
"endYearStr": "2014"
}
]
},
{
"moduleType": 16,
"moduleName": "资格证书",
"customConfig": {
"showType": 1,
"desc": null,
"tag": null
},
"data": null,
"dataList": null
},
{
"moduleType": 21,
"moduleName": "志愿服务经历",
"customConfig": {
"showType": 1,
"desc": null,
"tag": null
},
"data": null,
"dataList": null
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +0,0 @@
# 已删除文件清单
## ✅ 已删除的文件
### 1. 废弃的服务文件
-`api/services/task_scheduler.js` - 未使用的任务调度器
- **原因**:实际系统使用 `middleware/schedule/` 中的调度系统
- **替代方案**:使用 `middleware/schedule/index.js` 中的 `ScheduleManager`
### 2. 已合并的服务文件
-`api/services/job_service.js` - 职位服务(只有一个方法)
- **原因**:只有一个 `jobGreet` 方法,已合并到 `middleware/job/jobManager.js`
- **新位置**`middleware/job/jobManager.js``job_greet()` 方法
### 3. 重命名的文件
-`api/services/ossTool.js``api/services/oss_tool_service.js`
- **原因**统一命名规范snake_case + _service.js
## 📝 清理说明
### services/index.js 清理
- 移除了对 `TaskScheduler` 的引用(已废弃)
- 移除了对 `MQTTHandler` 的引用(文件不存在)
- 移除了对 `JobService` 的引用(已合并)
- 保留了 `AIService``PlaAccountService` 的引用
## ⚠️ 注意事项
1. **TaskScheduler 已废弃**
- 实际调度系统:`middleware/schedule/index.js` (ScheduleManager)
- 任务队列:`middleware/schedule/taskQueue.js`
2. **MQTT 管理**
- 实际使用:`middleware/mqtt/mqttManager.js`
- 不是 `services/mqtt_handler.js`(文件不存在)
3. **工作管理**
- 实际使用:`middleware/job/jobManager.js`
- 已包含 `job_greet` 方法
## 🔄 后续工作
继续完成命名规范统一:
- 移动并重命名 `middleware/job/` 下的文件到 `services/`
- 合并AI服务
- 统一类命名

View File

View File

@@ -1,123 +0,0 @@
# 指令和任务模式适配检查报告
## 📋 检查范围
检查 `api/middleware` 目录下的代码是否适用于新的指令和任务模式。
## ✅ 已适配的部分
### 1. **任务处理器 (taskHandlers.js)**
- ✅ 正确使用 `command.executeCommands()` 执行指令
- ✅ 在 `handleAutoDeliverTask` 中创建指令并执行
- ✅ 指令类型使用驼峰命名(`getOnlineResume`, `getJobList`, `applyJob`
### 2. **指令管理器 (command.js)**
- ✅ 已重构完成,统一封装指令执行
- ✅ 统一处理成功、失败、超时
- ✅ 统一记录数据库
- ✅ 支持驼峰转下划线的命名转换
### 3. **任务队列 (taskQueue.js)**
- ✅ 正确使用任务处理器
- ✅ 通过 `taskHandlers` 执行任务
## ⚠️ 需要修复的问题
### 1. **方法命名不一致**
**问题描述**
- 指令类型使用驼峰命名:`getOnlineResume`, `getJobList`, `applyJob`
- 大部分方法使用下划线命名:`get_online_resume`, `get_job_list`
-`applyJob` 方法名是驼峰命名,与指令类型一致
**当前转换逻辑**
```javascript
// command.js 中的转换
const to_snake_case = (str) => {
if (str.includes('_')) return str;
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
};
// getOnlineResume -> get_online_resume ✓
// getJobList -> get_job_list ✓
// applyJob -> apply_job ✗ (但实际方法名是 applyJob)
```
**解决方案**
1. **方案1推荐**:统一使用下划线命名,将 `applyJob` 改为 `apply_job`
2. **方案2**:保持现状,`command.js` 中已支持两种命名方式(先尝试下划线,再尝试原名称)
**当前状态**方案2已实现代码可以正常工作但命名不统一。
### 2. **sendChatMessage 方法**
**问题描述**
- `chatManager.js` 中的方法是 `sendChatMessage`(驼峰命名)
- 如果指令类型是 `sendChatMessage`,转换后会变成 `send_chat_message`,但实际方法名是 `sendChatMessage`
**当前状态**`command.js` 中已支持回退机制,如果下划线命名找不到,会尝试原名称,所以可以正常工作。
## 📊 方法命名对照表
| 指令类型 (command_type) | 转换后方法名 | 实际方法名 | 状态 |
|------------------------|-------------|-----------|------|
| `getOnlineResume` | `get_online_resume` | `get_online_resume` | ✅ 匹配 |
| `getJobList` | `get_job_list` | `get_job_list` | ✅ 匹配 |
| `applyJob` | `apply_job` | `applyJob` | ⚠️ 不匹配(但可工作) |
| `sendChatMessage` | `send_chat_message` | `sendChatMessage` | ⚠️ 不匹配(但可工作) |
## 🔧 建议修复
### 方案1统一使用下划线命名推荐
**修改文件**
1. `api/middleware/job/jobManager.js`:将 `applyJob` 改为 `apply_job`
2. `api/middleware/job/chatManager.js`:将 `sendChatMessage` 改为 `send_chat_message`
3. `api/middleware/schedule/taskHandlers.js`:将指令类型改为下划线命名
**优点**
- 命名统一,符合项目规范
- 代码更清晰,减少混淆
**缺点**
- 需要修改多个文件
- 可能影响其他调用方
### 方案2保持现状当前方案
**优点**
- 不需要修改现有代码
- `command.js` 已支持两种命名方式
**缺点**
- 命名不统一,容易混淆
- 代码可读性稍差
## 📝 其他检查项
### 1. **deviceManager.js**
- ✅ 不直接涉及指令和任务,主要用于设备状态管理
- ✅ 与任务系统配合良好
### 2. **job/index.js**
- ✅ 正确导出所有方法
- ✅ 支持下划线命名规范
### 3. **MQTT 相关**
- ✅ 通过 `mqttClient.publishAndWait` 发送指令
- ✅ 与指令系统配合良好
## ✅ 总结
**整体适配情况****良好** ✅
1. ✅ 核心功能已正确适配新的指令和任务模式
2. ✅ 指令执行统一封装,处理逻辑完善
3. ⚠️ 存在命名不一致问题,但不影响功能(有回退机制)
4. 💡 建议统一命名规范,提高代码可维护性
## 🎯 下一步行动
1. **可选**:统一方法命名规范(下划线命名)
2. **可选**:添加单元测试验证指令执行流程
3. **可选**:完善错误处理和日志记录

View File

@@ -1,529 +0,0 @@
# Boss直聘搜索列表和投递功能开发规划
## 📋 功能概述
基于Boss直聘Web端职位搜索页面`https://www.zhipin.com/web/geek/jobs`),完善搜索列表获取和职位投递功能,包括服务端任务创建、指令生成和完整流程实现。
## 🎯 目标功能
### 1. 搜索列表功能
- 支持多条件搜索(关键词、城市、薪资、经验、学历等)
- 支持分页获取职位列表
- 自动保存职位到数据库
- 支持职位去重和更新
### 2. 投递功能
- 单个职位投递
- 批量职位投递
- 投递状态跟踪
- 投递记录管理
## 📊 功能架构
```
用户/系统触发
创建任务 (task_status)
生成指令序列 (task_commands)
执行指令 (通过MQTT发送到设备)
设备执行并返回结果
保存数据到数据库
更新任务和指令状态
```
## 🔧 技术实现
### 一、搜索列表功能完善
#### 1.1 指令参数扩展
**文件**: `api/middleware/job/jobManager.js`
**方法**: `get_job_list()`
**需要支持的参数**:
```javascript
{
keyword: '全栈工程师', // 搜索关键词
city: '101020100', // 城市代码(上海)
cityName: '上海', // 城市名称
salary: '20-30K', // 薪资范围
experience: '3-5年', // 工作经验
education: '本科', // 学历要求
industry: '互联网', // 公司行业
companySize: '100-499人', // 公司规模
financingStage: 'B轮', // 融资阶段
page: 1, // 页码
pageSize: 20, // 每页数量
pageCount: 3 // 获取页数(用于批量获取)
}
```
#### 1.2 任务创建接口
**文件**: `api/services/pla_account_service.js`
**新增方法**: `createSearchJobListTask()`
```javascript
/**
* 创建搜索职位列表任务
* @param {Object} params - 任务参数
* @param {number} params.id - 账号ID
* @param {string} params.keyword - 搜索关键词
* @param {string} params.city - 城市代码
* @param {Object} params.searchParams - 搜索条件(薪资、经验、学历等)
* @param {number} params.pageCount - 获取页数
* @returns {Promise<Object>} 任务创建结果
*/
async createSearchJobListTask(params) {
// 1. 验证账号和授权
// 2. 创建任务记录
// 3. 生成搜索指令
// 4. 执行指令
// 5. 返回任务ID
}
```
#### 1.3 指令生成逻辑
**文件**: `api/middleware/schedule/taskHandlers.js`
**需要完善**: `handleAutoDeliverTask()` 中的搜索指令生成
**当前实现**:
```javascript
const getJobListCommand = {
command_type: 'getJobList',
command_name: '获取职位列表',
command_params: JSON.stringify({
sn_code: sn_code,
keyword: keyword || accountConfig.keyword || '',
platform: platform || 'boss',
pageCount: pageCount || 3
}),
priority: config.getTaskPriority('search_jobs') || 5
};
```
**需要扩展为**:
```javascript
const getJobListCommand = {
command_type: 'get_job_list', // 统一使用下划线命名
command_name: '获取职位列表',
command_params: JSON.stringify({
sn_code: sn_code,
platform: platform || 'boss',
keyword: keyword || accountConfig.keyword || '',
city: city || accountConfig.city || '101020100', // 默认上海
cityName: cityName || accountConfig.cityName || '上海',
salary: searchParams?.salary || '',
experience: searchParams?.experience || '',
education: searchParams?.education || '',
industry: searchParams?.industry || '',
companySize: searchParams?.companySize || '',
financingStage: searchParams?.financingStage || '',
page: 1,
pageSize: 20,
pageCount: pageCount || 3
}),
priority: config.getTaskPriority('get_job_list') || 5,
sequence: 1
};
```
#### 1.4 职位数据保存优化
**文件**: `api/middleware/job/jobManager.js`
**方法**: `saveJobsToDatabase()`
**需要完善**:
- 支持更多字段映射从Boss直聘响应数据
- 优化位置解析逻辑
- 支持职位状态更新(已投递、已查看等)
- 添加职位匹配度计算
### 二、投递功能完善
#### 2.1 单个职位投递
**文件**: `api/middleware/job/jobManager.js`
**方法**: `applyJob()`
**当前状态**: ✅ 已实现基础功能
**需要完善**:
- 支持更多投递参数(期望薪资、求职信等)
- 优化错误处理
- 添加投递前检查(是否已投递、是否满足条件等)
#### 2.2 批量职位投递任务
**文件**: `api/middleware/schedule/taskHandlers.js`
**方法**: `handleAutoDeliverTask()`
**当前状态**: ✅ 已实现基础功能
**需要完善**:
1. **搜索条件完善**
- 支持城市、薪资、经验、学历等筛选条件
- 从账号配置中读取默认搜索条件
- 支持任务参数覆盖账号配置
2. **职位匹配算法优化**
- 完善距离计算(基于经纬度)
- 完善薪资匹配(解析薪资范围字符串)
- 完善工作年限匹配
- 完善学历匹配
- 完善权重评分系统
3. **投递指令生成**
- 为每个匹配的职位生成投递指令
- 支持批量投递(一次任务投递多个职位)
- 添加投递间隔控制(避免频繁投递)
#### 2.3 投递任务创建接口
**文件**: `api/services/pla_account_service.js`
**新增方法**: `createDeliverTask()`
```javascript
/**
* 创建投递任务
* @param {Object} params - 任务参数
* @param {number} params.id - 账号ID
* @param {string} params.keyword - 搜索关键词
* @param {Object} params.searchParams - 搜索条件
* @param {Object} params.filterRules - 过滤规则
* @param {number} params.maxCount - 最大投递数量
* @returns {Promise<Object>} 任务创建结果
*/
async createDeliverTask(params) {
// 1. 验证账号和授权
// 2. 创建任务记录
// 3. 生成搜索指令(获取职位列表)
// 4. 生成投递指令序列(根据匹配结果)
// 5. 执行任务
// 6. 返回任务ID
}
```
## 📝 具体开发任务
### 任务1: 完善搜索参数支持
**文件**: `api/middleware/job/jobManager.js`
**修改方法**: `get_job_list()`
**任务内容**:
1. 扩展参数支持(城市、薪资、经验、学历等)
2. 构建完整的搜索参数对象
3. 传递给MQTT指令
**代码位置**: 第153-206行
**预计工作量**: 2小时
---
### 任务2: 优化职位数据保存
**文件**: `api/middleware/job/jobManager.js`
**修改方法**: `saveJobsToDatabase()`
**任务内容**:
1. 完善字段映射从Boss直聘响应数据提取更多字段
2. 优化位置解析减少API调用添加缓存
3. 添加职位状态管理
4. 添加职位匹配度字段
**代码位置**: 第215-308行
**预计工作量**: 3小时
---
### 任务3: 完善自动投递任务搜索条件
**文件**: `api/middleware/schedule/taskHandlers.js`
**修改方法**: `handleAutoDeliverTask()`
**任务内容**:
1. 从账号配置读取默认搜索条件
2. 支持任务参数覆盖
3. 构建完整的搜索参数
4. 传递给搜索指令
**代码位置**: 第220-233行
**预计工作量**: 2小时
---
### 任务4: 优化职位匹配算法
**文件**: `api/middleware/schedule/taskHandlers.js`
**修改方法**: `handleAutoDeliverTask()`
**任务内容**:
1. 完善距离计算(使用经纬度计算实际距离)
2. 完善薪资匹配(解析"20-30K"格式,转换为数值范围)
3. 完善工作年限匹配(解析"3-5年"格式)
4. 完善学历匹配(学历等级映射)
5. 优化权重评分计算
**代码位置**: 第255-400行职位评分和过滤逻辑
**预计工作量**: 4小时
---
### 任务5: 创建搜索任务接口
**文件**: `api/services/pla_account_service.js`
**新增方法**: `createSearchJobListTask()`
**任务内容**:
1. 验证账号和授权
2. 创建任务记录
3. 生成搜索指令
4. 执行指令
5. 返回任务信息
**代码位置**: 在 `runCommand()` 方法后添加
**预计工作量**: 3小时
---
### 任务6: 创建投递任务接口
**文件**: `api/services/pla_account_service.js`
**新增方法**: `createDeliverTask()`
**任务内容**:
1. 验证账号和授权
2. 创建任务记录
3. 生成搜索指令(获取职位列表)
4. 等待搜索完成
5. 获取匹配的职位
6. 生成投递指令序列
7. 执行投递指令
8. 返回任务信息
**代码位置**: 在 `createSearchJobListTask()` 方法后添加
**预计工作量**: 4小时
---
### 任务7: 完善指令类型映射
**文件**: `api/middleware/schedule/command.js`
**修改位置**: 指令执行逻辑
**任务内容**:
1. 确保 `get_job_list` 指令类型正确映射到 `jobManager.get_job_list()`
2. 确保 `search_jobs` 指令类型正确映射到 `jobManager.search_jobs()`
3. 确保 `apply_job` 指令类型正确映射到 `jobManager.applyJob()`
**代码位置**: 第150-250行指令执行逻辑
**预计工作量**: 1小时
---
### 任务8: 添加搜索条件配置管理
**文件**: `api/model/pla_account.js`
**任务内容**:
1. 添加搜索条件配置字段(如果不存在)
2. 支持在账号配置中保存默认搜索条件
3. 支持在任务参数中覆盖搜索条件
**相关字段**:
- `search_config` (JSON): 搜索条件配置
```json
{
"city": "101020100",
"cityName": "上海",
"defaultSalary": "20-30K",
"defaultExperience": "3-5年",
"defaultEducation": "本科"
}
```
**预计工作量**: 1小时
---
## 🔄 工作流程
### 搜索职位列表流程
```
1. 用户/系统调用 createSearchJobListTask()
2. 创建任务记录 (task_status)
3. 生成搜索指令 (task_commands)
- command_type: 'get_job_list'
- command_params: { keyword, city, salary, ... }
4. 执行指令 (通过MQTT发送到设备)
5. 设备执行搜索并返回职位列表
6. 保存职位到数据库 (job_postings)
- 去重处理
- 位置解析
- 字段映射
7. 更新指令状态为完成
8. 更新任务状态为完成
```
### 投递职位流程
```
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. 更新任务状态为完成
```
## 📊 数据库字段说明
### job_postings 表需要完善的字段
| 字段名 | 类型 | 说明 | 状态 |
|--------|------|------|------|
| `city` | VARCHAR | 城市代码 | 待添加 |
| `cityName` | VARCHAR | 城市名称 | 待添加 |
| `salaryMin` | INT | 最低薪资(元) | 待添加 |
| `salaryMax` | INT | 最高薪资(元) | 待添加 |
| `experienceMin` | INT | 最低工作年限 | 待添加 |
| `experienceMax` | INT | 最高工作年限 | 待添加 |
| `educationLevel` | VARCHAR | 学历等级 | 待添加 |
| `matchScore` | DECIMAL | 匹配度评分 | 待添加 |
### pla_account 表需要添加的字段
| 字段名 | 类型 | 说明 | 状态 |
|--------|------|------|------|
| `search_config` | JSON | 搜索条件配置 | 待添加 |
| `city` | VARCHAR | 默认城市代码 | 待添加 |
| `cityName` | VARCHAR | 默认城市名称 | 待添加 |
## 🧪 测试计划
### 单元测试
1. 测试搜索参数构建
2. 测试职位数据保存
3. 测试职位匹配算法
4. 测试投递指令生成
### 集成测试
1. 测试完整搜索流程
2. 测试完整投递流程
3. 测试任务创建和执行
4. 测试MQTT通信
### 功能测试
1. 测试多条件搜索
2. 测试分页获取
3. 测试批量投递
4. 测试错误处理
## 📅 开发时间估算
| 任务 | 预计时间 | 优先级 |
|------|----------|--------|
| 任务1: 完善搜索参数支持 | 2小时 | 高 |
| 任务2: 优化职位数据保存 | 3小时 | 高 |
| 任务3: 完善自动投递任务搜索条件 | 2小时 | 高 |
| 任务4: 优化职位匹配算法 | 4小时 | 高 |
| 任务5: 创建搜索任务接口 | 3小时 | 中 |
| 任务6: 创建投递任务接口 | 4小时 | 中 |
| 任务7: 完善指令类型映射 | 1小时 | 中 |
| 任务8: 添加搜索条件配置管理 | 1小时 | 低 |
**总计**: 约20小时
## 🚀 开发优先级
### 第一阶段(核心功能)
1. 任务1: 完善搜索参数支持
2. 任务2: 优化职位数据保存
3. 任务3: 完善自动投递任务搜索条件
4. 任务4: 优化职位匹配算法
### 第二阶段(接口完善)
5. 任务5: 创建搜索任务接口
6. 任务6: 创建投递任务接口
7. 任务7: 完善指令类型映射
### 第三阶段(配置管理)
8. 任务8: 添加搜索条件配置管理
## 📌 注意事项
1. **命名规范**: 统一使用下划线命名(`get_job_list` 而不是 `getJobList`
2. **错误处理**: 所有方法都需要完善的错误处理和日志记录
3. **数据验证**: 所有输入参数都需要验证
4. **性能优化**: 批量操作需要考虑性能,避免阻塞
5. **MQTT通信**: 确保指令参数格式正确,与客户端协议一致
6. **数据库事务**: 批量操作需要使用事务保证数据一致性
## 🔗 相关文件
- `api/middleware/job/jobManager.js` - 工作管理核心逻辑
- `api/middleware/schedule/taskHandlers.js` - 任务处理器
- `api/middleware/schedule/command.js` - 指令管理器
- `api/services/pla_account_service.js` - 账号服务
- `api/model/job_postings.js` - 职位数据模型
- `api/model/pla_account.js` - 账号数据模型

View File

@@ -1,210 +0,0 @@
# resume_info 表同步指南
## ❌ 错误信息
```
Unknown column 'sn_code' in 'field list'
```
这个错误表示数据库中的 `resume_info` 表缺少 `sn_code` 字段。
## 🔧 解决方案
### 方案1: 使用同步脚本(推荐)
运行以下命令同步表结构:
```bash
node scripts/sync_resume_table.js
```
这个脚本会:
- ✅ 使用 `alter: true` 模式同步表(保留现有数据)
- ✅ 显示当前表结构
- ✅ 检查所有必需字段是否存在
- ✅ 提示缺少的字段
### 方案2: 手动添加字段
如果同步脚本无法运行可以手动执行以下SQL
```sql
-- 添加 sn_code 字段
ALTER TABLE `resume_info`
ADD COLUMN `sn_code` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '设备SN码' AFTER `id`;
-- 添加 account_id 字段
ALTER TABLE `resume_info`
ADD COLUMN `account_id` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '用户ID' AFTER `sn_code`;
-- 添加索引
ALTER TABLE `resume_info`
ADD INDEX `idx_sn_code` (`sn_code`);
```
### 方案3: 重建表(会删除现有数据!)
⚠️ **警告:此操作会删除表中所有数据!**
如果表中没有重要数据,可以删除表让系统重新创建:
```sql
DROP TABLE IF EXISTS `resume_info`;
```
然后重启应用Sequelize 会自动创建表(因为模型中有 `sync({ force: true })`)。
## 📋 必需字段列表
`resume_info` 表必须包含以下字段:
### 核心字段
-`id` - 主键VARCHAR/UUID
-`sn_code` - 设备SN码VARCHAR(50),必填)
-`account_id` - 用户IDVARCHAR(50),必填)
-`platform` - 平台VARCHAR(20),默认'boss'
### 个人信息
-`fullName` - 姓名
-`gender` - 性别
-`age` - 年龄
-`phone` - 电话
-`email` - 邮箱
-`location` - 所在地
### 教育背景
-`education` - 学历
-`major` - 专业
-`school` - 毕业院校
-`graduationYear` - 毕业年份
### 工作信息
-`workYears` - 工作年限
-`currentPosition` - 当前职位
-`currentCompany` - 当前公司
-`currentSalary` - 当前薪资
### 期望信息
-`expectedPosition` - 期望职位
-`expectedSalary` - 期望薪资
-`expectedLocation` - 期望地点
-`expectedIndustry` - 期望行业
### 技能和经验TEXT类型
-`skills` - 技能标签JSON
-`skillDescription` - 技能描述
-`certifications` - 证书资质JSON
-`projectExperience` - 项目经验JSON
-`workExperience` - 工作经历JSON
### AI分析字段TEXT类型
-`aiSkillTags` - AI技能标签JSON
-`aiStrengths` - AI优势分析
-`aiWeaknesses` - AI劣势分析
-`aiCareerSuggestion` - AI职业建议
-`aiCompetitiveness` - AI竞争力评分INT
### 其他字段
-`resumeContent` - 简历内容TEXT
-`originalData` - 原始数据TEXT/JSON
-`isActive` - 是否活跃BOOLEAN
-`isPublic` - 是否公开BOOLEAN
-`syncTime` - 同步时间DATETIME
## 🔍 验证表结构
运行以下SQL查看表结构
```sql
DESCRIBE resume_info;
```
或者查看完整的建表语句:
```sql
SHOW CREATE TABLE resume_info;
```
## 📝 完整建表SQL参考
```sql
CREATE TABLE `resume_info` (
`id` varchar(255) NOT NULL,
`sn_code` varchar(50) NOT NULL DEFAULT '' COMMENT '设备SN码',
`account_id` varchar(50) NOT NULL DEFAULT '' COMMENT '用户ID',
`platform` varchar(20) NOT NULL DEFAULT 'boss' COMMENT '平台',
`fullName` varchar(50) DEFAULT '' COMMENT '姓名',
`gender` varchar(10) DEFAULT '' COMMENT '性别',
`age` int(11) DEFAULT 0 COMMENT '年龄',
`phone` varchar(20) DEFAULT '' COMMENT '电话',
`email` varchar(100) DEFAULT '' COMMENT '邮箱',
`location` varchar(100) DEFAULT '' COMMENT '所在地',
`education` varchar(50) DEFAULT '' COMMENT '学历',
`major` varchar(100) DEFAULT '' COMMENT '专业',
`school` varchar(200) DEFAULT '' COMMENT '毕业院校',
`graduationYear` int(11) DEFAULT 0 COMMENT '毕业年份',
`workYears` varchar(50) DEFAULT '' COMMENT '工作年限',
`currentPosition` varchar(100) DEFAULT '' COMMENT '当前职位',
`currentCompany` varchar(200) DEFAULT '' COMMENT '当前公司',
`currentSalary` varchar(50) DEFAULT '' COMMENT '当前薪资',
`expectedPosition` varchar(100) DEFAULT '' COMMENT '期望职位',
`expectedSalary` varchar(50) DEFAULT '' COMMENT '期望薪资',
`expectedLocation` varchar(100) DEFAULT '' COMMENT '期望地点',
`expectedIndustry` varchar(100) DEFAULT '' COMMENT '期望行业',
`skills` text COMMENT '技能标签(JSON)',
`skillDescription` text COMMENT '技能描述',
`certifications` text COMMENT '证书资质(JSON)',
`projectExperience` text COMMENT '项目经验(JSON)',
`workExperience` text COMMENT '工作经历(JSON)',
`aiSkillTags` text COMMENT 'AI技能标签(JSON)',
`aiStrengths` text COMMENT 'AI优势分析',
`aiWeaknesses` text COMMENT 'AI劣势分析',
`aiCareerSuggestion` text COMMENT 'AI职业建议',
`aiCompetitiveness` int(11) DEFAULT 0 COMMENT 'AI竞争力评分',
`resumeContent` text COMMENT '简历内容',
`originalData` text COMMENT '原始数据(JSON)',
`isActive` tinyint(1) DEFAULT 1 COMMENT '是否活跃',
`isPublic` tinyint(1) DEFAULT 1 COMMENT '是否公开',
`syncTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '同步时间',
PRIMARY KEY (`id`),
KEY `idx_sn_code` (`sn_code`),
KEY `idx_platform` (`platform`),
KEY `idx_isActive` (`isActive`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='简历信息表';
```
## ✅ 验证修复
修复后,运行以下代码验证:
```javascript
const db = require('./api/middleware/dbProxy');
const resume_info = db.getModel('resume_info');
// 测试创建记录
const testResume = await resume_info.create({
id: 'test-uuid-123',
sn_code: 'TEST001',
account_id: 'user123',
platform: 'boss',
fullName: '测试用户',
isActive: true
});
console.log('✅ 创建成功:', testResume.id);
```
## 🚀 下一步
表结构同步完成后,就可以正常使用简历存储功能了:
```javascript
const jobManager = require('./api/middleware/job/jobManager');
const resumeData = await jobManager.get_online_resume(
'GHJU',
mqttClient,
{ platform: 'boss' }
);
```

View File

@@ -1,57 +0,0 @@
# 文件清理总结
## ✅ 已删除的文件
### 1. 废弃的服务文件
-`api/services/task_scheduler.js`
- **删除原因**:未使用,实际系统使用 `middleware/schedule/` 中的调度系统
- **替代方案**:使用 `middleware/schedule/index.js` 中的 `ScheduleManager`
### 2. 已合并的服务文件
-`api/services/job_service.js`
- **删除原因**:只有一个方法,已合并到 `middleware/job/jobManager.js`
- **新位置**`middleware/job/jobManager.js``job_greet()` 方法
### 3. 重命名的文件
-`api/services/ossTool.js``api/services/oss_tool_service.js`
- **原因**:统一命名规范
## 🔧 已清理的引用
### services/index.js
- ✅ 移除了对 `TaskScheduler` 的引用(已删除)
- ✅ 移除了对 `MQTTHandler` 的引用(文件不存在)
- ✅ 移除了对 `JobService` 的引用(已合并)
- ✅ 移除了相关的初始化代码和监听器设置
- ✅ 保留了 `AIService``PlaAccountService` 的引用
## 📋 当前 services/ 目录结构
```
api/services/
├── index.js # 服务管理器(已清理)
├── ai_service.js # AI服务
├── pla_account_service.js # 账号服务
└── oss_tool_service.js # OSS服务已重命名
```
## ⚠️ 注意事项
1. **调度系统**
- 实际使用:`middleware/schedule/index.js` (ScheduleManager)
- 不要使用:`services/task_scheduler.js`(已删除)
2. **MQTT管理**
- 实际使用:`middleware/mqtt/mqttManager.js`
- 不要使用:`services/mqtt_handler.js`(文件不存在)
3. **工作管理**
- 实际使用:`middleware/job/jobManager.js`
- 包含 `job_greet` 方法(原 `job_service.js` 的方法)
## 🎯 清理效果
- **减少文件数量**删除了2个不需要的文件
- **代码更清晰**:移除了无效引用
- **结构更合理**services 目录只保留实际使用的服务

View File

@@ -1,54 +0,0 @@
# 服务合并完成说明
## ✅ 已完成
### 1. job_service.js 合并
- ✅ 将 `job_service.js``jobGreet` 方法合并到 `jobManager.js`
- ✅ 方法重命名为 `job_greet`(统一使用下划线命名)
- ✅ 更新了 `api/controller_admin/job_postings.js` 中的引用
- ✅ 更新了 `api/services/index.js`,移除了 `JobService` 的引用
- ✅ 删除了 `job_service.js` 文件
### 2. 方法改进
-`job_greet` 方法支持可选的 `mqttClient` 参数
- ✅ 修复了 `getResumeAnalysis` 方法的 `mqttClient` 参数问题
## 📝 变更详情
### 方法位置变更
- **原位置**`api/services/job_service.js``JobService.jobGreet()`
- **新位置**`api/middleware/job/jobManager.js``JobManager.job_greet()`
### 方法签名变更
```javascript
// 旧方法
async jobGreet(params) {
// ...
}
// 新方法
async job_greet(params) {
const { sn_code, encryptJobId, securityId, brandName, platform = 'boss', mqttClient } = params;
// 支持可选的 mqttClient 参数
// ...
}
```
### 引用更新
- `api/controller_admin/job_postings.js`
- `jobService.jobGreet()``jobManager.job_greet()`
## 🎯 优势
1. **代码更集中**:所有工作管理相关的方法都在 `jobManager.js`
2. **减少文件数量**:删除了只有一个方法的服务文件
3. **命名统一**:使用下划线命名 `job_greet`,与其他方法一致
4. **更好的复用性**:支持可选的 `mqttClient` 参数
## 📋 后续工作
继续完成命名规范统一:
- 移动并重命名 `middleware/job/` 下的文件到 `services/`
- 合并AI服务
- 统一类命名

View File

@@ -1,320 +0,0 @@
# 服务端升级功能逻辑说明
## 概述
本文档说明服务端需要实现的升级功能逻辑,配合客户端的自动升级功能。
## API 接口要求
### GET /version/check
**功能**:检查是否有新版本可用
**请求参数**Query 参数):
- `current_version`: 当前版本号x.y.z 格式,如 "1.0.0"
- `platform`: 平台类型(如 "win32", "darwin", "linux"
- `arch`: 架构类型(如 "x64", "ia32", "arm64"
- `sn_code`: 设备序列号(可选,用于权限控制)
**响应格式**
```json
{
"code": 0,
"message": "success",
"data": {
"version": "1.1.0",
"download_url": "http://work.light120.com/downloads/app-1.1.0.exe",
"release_notes": "修复了一些bug新增了xxx功能",
"force_update": false,
"file_size": 52428800,
"file_hash": "abc123def456..."
}
}
```
**字段说明**
- `version`: 最新版本号x.y.z 格式)
- `download_url`: 更新包下载地址(完整的 HTTP/HTTPS URL
- `release_notes`: 更新日志(可选,字符串)
- `force_update`: 是否强制更新(可选,布尔值,默认 false
- `file_size`: 文件大小(字节,可选)
- `file_hash`: 文件 SHA256 哈希值(可选,用于校验文件完整性)
**业务逻辑**
1. **版本比较**
- 从数据库查询最新版本信息(根据 platform 和 arch
- 比较请求中的 `current_version` 与数据库中的最新版本
- 如果最新版本 > 当前版本,返回更新信息
- 如果版本相同或更旧,返回 `code: 0, data: null` 或提示"已是最新版本"
2. **版本号格式**
- 格式x.y.z如 1.0.0, 1.1.0, 2.0.0
- 比较规则:主版本号 > 次版本号 > 修订号
- 示例1.1.0 > 1.0.0, 2.0.0 > 1.9.9
3. **平台和架构过滤**
- 只返回匹配 platform 和 arch 的版本信息
- 如果某个平台没有新版本,返回空结果
4. **序列号验证**(可选):
- 可以根据 sn_code 验证设备权限
- 如果启用权限控制,未授权的设备返回错误
5. **错误处理**
- 参数缺失:返回 `code: 400, message: "参数错误"`
- 服务器错误:返回 `code: 500, message: "服务器错误"`
- 无新版本:返回 `code: 0, data: null``has_update: false`
## 数据库设计建议
### 版本信息表version_info
| 字段名 | 类型 | 说明 | 约束 |
|--------|------|------|------|
| id | INT/BIGINT | 主键 | PRIMARY KEY, AUTO_INCREMENT |
| version | VARCHAR(20) | 版本号x.y.z 格式) | NOT NULL, UNIQUE |
| platform | VARCHAR(20) | 平台类型win32/darwin/linux | NOT NULL |
| arch | VARCHAR(20) | 架构类型x64/ia32/arm64 | NOT NULL |
| download_url | VARCHAR(500) | 下载地址 | NOT NULL |
| file_path | VARCHAR(500) | 服务器文件路径 | NOT NULL |
| file_size | BIGINT | 文件大小(字节) | DEFAULT 0 |
| file_hash | VARCHAR(64) | SHA256 哈希值 | |
| release_notes | TEXT | 更新日志 | |
| force_update | TINYINT(1) | 是否强制更新 | DEFAULT 0 |
| status | TINYINT(1) | 状态1:启用 0:禁用) | DEFAULT 1 |
| create_time | DATETIME | 创建时间 | NOT NULL, DEFAULT CURRENT_TIMESTAMP |
| last_modify_time | DATETIME | 最后修改时间 | NOT NULL, DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |
**索引建议**
- PRIMARY KEY (id)
- UNIQUE KEY (version, platform, arch)
- INDEX (platform, arch, status)
### 版本发布历史表version_history可选
用于记录版本发布历史,便于管理:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | INT/BIGINT | 主键 |
| version_id | INT | 关联 version_info.id |
| release_type | VARCHAR(20) | 发布类型stable/beta/alpha |
| create_time | DATETIME | 发布时间 |
| last_modify_time | DATETIME | 最后修改时间 |
## 业务逻辑流程
### 1. 版本检查流程
```
客户端请求
接收参数current_version, platform, arch, sn_code
验证参数有效性
查询数据库最新版本(按 platform + arch + status=1
版本号比较
有更新?
├─ 是 → 构建返回数据(包含文件信息)
└─ 否 → 返回空结果或提示"已是最新版本"
返回响应
```
### 2. 版本管理流程
**新增版本**
1. 上传安装包文件到服务器指定目录
2. 计算文件 SHA256 哈希值
3. 获取文件大小
4. 插入数据库记录status=1 表示启用)
**禁用版本**
1. 更新 status=0不删除记录保留历史
**删除版本**
1. 删除数据库记录
2. 删除服务器上的文件
### 3. 文件存储建议
**目录结构**
```
/uploads/
└── versions/
├── win32/
│ ├── x64/
│ │ ├── app-1.0.0.exe
│ │ └── app-1.1.0.exe
│ └── ia32/
└── darwin/
└── x64/
└── app-1.1.0.dmg
```
**文件命名规则**
- 格式:`app-{version}.{ext}`
- 示例:`app-1.1.0.exe`, `app-1.1.0.dmg`
**下载 URL 生成**
- 基础 URL`http://work.light120.com/downloads/`
- 完整 URL`http://work.light120.com/downloads/app-1.1.0.exe`
## 关键逻辑说明
### 1. 版本号比较逻辑
**字符串比较规则**
- 将版本号按 "." 分割成数组:["1", "1", "0"]
- 逐位比较数字大小
- 主版本号优先级最高,依次递减
**示例**
```javascript
// 伪代码
compareVersion("1.1.0", "1.0.0") true // 1.1.0 > 1.0.0
compareVersion("2.0.0", "1.9.9") true // 2.0.0 > 1.9.9
compareVersion("1.0.0", "1.0.0") false // 相等
```
### 2. 文件哈希计算
**计算方法**
- 使用 SHA256 算法
- 读取文件内容,计算哈希值
- 返回小写的十六进制字符串
**用途**
- 客户端下载后校验文件完整性
- 防止文件被篡改
- 确保文件下载完整
### 3. 强制更新逻辑
**force_update 字段**
- `true`: 强制更新,客户端必须更新才能使用
- `false`: 可选更新,客户端可以选择稍后更新
**业务场景**
- 安全漏洞修复 → 强制更新
- 重大功能更新 → 强制更新
- 小版本更新 → 可选更新
### 4. 平台和架构支持
**支持列表**
- Windows: `win32` + `x64` / `ia32`
- macOS: `darwin` + `x64` / `arm64`
- Linux: `linux` + `x64` / `arm64`
**查询逻辑**
- 必须同时匹配 platform 和 arch
- 如果某个组合没有版本,返回空结果
## 安全考虑
### 1. 文件下载安全
- **HTTPS 下载**:优先使用 HTTPS 协议
- **文件校验**:提供 SHA256 哈希值供客户端校验
- **文件大小验证**:返回文件大小,客户端可以验证下载完整性
### 2. 权限控制
- **序列号验证**:可以根据 sn_code 控制哪些设备可以更新
- **版本状态**:使用 status 字段控制版本是否可用
- **IP 限制**:可以限制下载 IP 范围(可选)
### 3. 防止恶意更新
- **文件类型验证**:只允许上传 .exe、.dmg、.AppImage 等安装包
- **文件大小限制**:设置最大文件大小限制
- **版本号验证**:验证版本号格式是否正确
## 返回状态码说明
| code | 说明 | 处理方式 |
|------|------|----------|
| 0 | 成功 | 检查 data 是否为空判断是否有更新 |
| 400 | 参数错误 | 客户端提示参数错误 |
| 401 | 未授权 | 客户端提示未授权 |
| 404 | 未找到版本 | 客户端提示版本不存在 |
| 500 | 服务器错误 | 客户端提示服务器错误,稍后重试 |
## 示例场景
### 场景1有新版本
**请求**
```
GET /version/check?current_version=1.0.0&platform=win32&arch=x64&sn_code=GHJU
```
**响应**
```json
{
"code": 0,
"message": "success",
"data": {
"version": "1.1.0",
"download_url": "http://work.light120.com/downloads/app-1.1.0.exe",
"release_notes": "1. 修复了登录问题\n2. 新增自动升级功能",
"force_update": false,
"file_size": 52428800,
"file_hash": "a1b2c3d4e5f6..."
}
}
```
### 场景2已是最新版本
**请求**
```
GET /version/check?current_version=1.1.0&platform=win32&arch=x64&sn_code=GHJU
```
**响应**
```json
{
"code": 0,
"message": "已是最新版本",
"data": null
}
```
### 场景3参数错误
**请求**
```
GET /version/check?current_version=1.0.0
```
**响应**
```json
{
"code": 400,
"message": "缺少必要参数platform, arch",
"data": null
}
```
## 注意事项
1. **日期字段命名**:统一使用 `create_time``last_modify_time`
2. **版本号格式**:严格按照 x.y.z 格式,便于版本比较
3. **文件存储**:建议使用 CDN 或静态文件服务器,提高下载速度
4. **日志记录**:记录版本检查请求,便于统计和分析
5. **缓存策略**:可以考虑缓存最新版本信息,减少数据库查询
6. **灰度发布**:可以通过 sn_code 控制部分设备先更新(可选)
## 实现优先级
1. **基础功能**:版本检查、版本比较、返回下载信息
2. **文件管理**:文件上传、文件存储、哈希计算
3. **安全功能**:文件校验、权限控制
4. **管理功能**:版本管理后台、历史记录

544
_doc/消息.md Normal file
View File

@@ -0,0 +1,544 @@
messages[
{
"uncount": 1,
"flag": 0,
"bizType": 21050003,
"mid": 306388872406018,
"received": true,
"securityId": "cVB1gt2iMS_FV-B1EuTtuprq9nPmk046hua_alhFbU9irJdmTCNqWxiCzyABGAJxRhHJ19omwFtTF62aNLx0Kb-jEBjLnK5zAvB5QJQ74XNKfm3GvYbAw6Hat_UPGUYrWuoC78uf6-i_4mQDfnri1cgi33PGu4t76fHCBmlQgeyNQYZtvlemjQ~~",
"cmid": 0,
"type": 3,
"body": {
"type": 8,
"templateId": 1,
"jobDesc": {
"education": "本科",
"boss": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"distance": "",
"city": "上海 浦东新区 北蔡",
"lid": "",
"partTimeDesc": "",
"expectId": 1421763565,
"title": "信息系统产品经理",
"salary": "20-30K",
"experience": "3-5年",
"bottomText": "2月1日 18:19 由你发起的沟通",
"content": "",
"jobLabel": "",
"latlon": "",
"expectPosition": "",
"company": "中建八局上海公司",
"url": "bosszp://bosszhipin.app/openwith?type=jobview&jid=510160286&uid=631237132&securityId=_gWIU90mUoEub-b1Uw8xol50QDeii-lYj5J6gGaUDpLqCFr6_7TAHagl3UwLBh3S8tgBUY_91xh-LMPqDo56Bhg2tM5ptshBG4OSYvjb2szm9uGWm64E8GFe2pIXtMoJJW53RY_enX-0S41IAQyZSwBHGDLKYC-ajb88mwLIgFzHv26-aCA6uQXewrLCx0JKQZdIcGdUYOLlbbjVOXgCwPV04Ih_o6_BUwTq8iZjpMMBSuMa_abda3A_rjsOeY_qaN0tPcB92T2m3a0xMseYDQyJQj6x8CqTN3Wn8NhF3RuYC3KvDRGYC22HGNfGIHRHIbXlOIm5N6HK6UFRkfQMoYIl6KIJOm9-q3mHf4exItvw1_K8ArABDBF2koQ0lyCMu0bvrscAuUZCY4ggK_r4iUNaJz-r7mvO5JLona10LTHVmZ5gRGXej4kIPT9F2lj02KdK0nKWG8ilYQ2AzD0FbUI52lYa5CgAu5hhO9_kHcsJdfhxKCaSbiM_To6iTrJtfEksQnmqwMLU6tpTw-N3SA-UJsXfhwJRV3uhDsKGJeIFDpJjZ6IFr3qVVIDzwROyUZlK6Tu8Y8_nnKHxrtuIKfQPU8mFrWf02LtWWJg~",
"extend": "{\"jobType\":0}",
"jobId": 510160286,
"stage": "未融资",
"geek": {
"uid": 546224865,
"headImg": 0,
"name": "",
"company": "",
"avatar": "",
"source": 0,
"certification": 0
},
"iconFlag": 0,
"positionCategory": "产品经理",
"bossTitle": "招聘主管",
"expectSalary": ""
},
"headTitle": "您正在与Boss韩先生直接沟通如下职位"
},
"offline": false,
"pushSound": 0,
"from": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"time": 1769941171000,
"taskId": 0,
"status": 2
},
{
"uncount": 0,
"flag": 0,
"bizType": 101,
"mid": 306388872410117,
"received": true,
"securityId": "HGFvURTQnW2V8-_1M1FQeOyKfMqBVGmx4iYXvV-ja6Ax2OHah-vLgPuQbHIZIxOV9DTQVpbE4IDFvjkbMEeBVFun5rsz1N-55pIQUpuqzGCHJTPKTSH6pYKpFoUtSueouw04tw~~",
"cmid": 0,
"type": 3,
"body": {
"extend": "",
"text": "您正在招的信息系统产品经理我很有兴趣,如果方便,希望可以和您进一步沟通,谢谢!",
"type": 1,
"templateId": 1,
"headTitle": ""
},
"offline": false,
"pushSound": 0,
"bizId": "42",
"from": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"time": 1769941171000,
"pushText": "肖伟民:您正在招的信息系统产品经理我很有兴趣,如果方便,希望可以和您进一步沟通,谢谢!",
"taskId": 0,
"status": 2
},
{
"uncount": 1,
"flag": 0,
"bizType": 317,
"mid": 306388872438784,
"received": true,
"securityId": "scWxOY-tkn0ih-A1bkgY7cd1djlTDxgd7J24wQXQqZvi8wn_teicgd2zAj7dz74o5LYrEdUJsKDHzdyUzdqrWs55p9cUQXsroGgVJrQlUspiITmPBY23-nVeei7VytBBuFATuw~~",
"cmid": 0,
"type": 4,
"body": {
"style": 3,
"type": 16,
"templateId": 1,
"articles": [
{
"extend": "{\"descriptionHighParts\":[{\"endIndex\":8,\"startIndex\":6},{\"endIndex\":14,\"startIndex\":12}],\"avatarList\":[\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09095058edb5e6d5d9273d3250d5a0c4c56bb61e3b7bce0931da574d19d1d82c88.png\",\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09ba9684830b5f873d7f98f2d5757b12c86bb61e3b7bce0931da574d19d1d82c88.png\",\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09f663dc109c9552b92d74732363d6a4056bb61e3b7bce0931da574d19d1d82c88.png\"]}",
"picUrl": "https://img.bosszhipin.com/beijin/icon/bed51f39faf420a15620181baffee482f7aba6f40b0808dd3b1d96fc3abbc5af.png",
"highlightParts": [
{
"startIndex": 1,
"endIndex": 3
},
{
"startIndex": 10,
"endIndex": 12
}
],
"subTitle": "共**人投递,你超过**竞争者",
"description": "优秀竞争者会**,建议你**",
"statisticParameters": "",
"title": "你与该职位竞争者PK情况",
"templateId": 5,
"url": "https://m.zhipin.com/mpa/html/props/transit?targetId=a875d648fa6db1490nV62tu9ElpW&sendNum=1&bossId=5e86870fd010f0eb0Xd72d66EVFS",
"bottomText": "查看详细分析",
"timeout": 0
}
],
"headTitle": ""
},
"offline": false,
"pushSound": 0,
"from": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"time": 1769941171000,
"pushText": "你与该职位竞争者PK情况",
"taskId": 0,
"status": 2
},
{
"uncount": 0,
"flag": 0,
"bizType": 110,
"mid": 306962385814530,
"received": true,
"securityId": "2Qs1rcvXn2axz-Z1F_qA9Hr2oJm3oy4XqZZxP3u_iQc1b13a9hYHxYzEYHGRR5iffvX9fV3HQ02WePxjcHPZ3H2gHkLE2kI8aSRvEYf18TcPjNkA7nR8bBL75uun6ErmddoTvQ~~",
"cmid": 0,
"type": 3,
"body": {
"extend": "",
"text": "我们感谢您的投递但您的专业技能与我们目前的职位需求并不完全吻合。祝您在BOSS直聘找到更适合您的工作",
"type": 1,
"templateId": 1,
"headTitle": ""
},
"offline": false,
"pushSound": 0,
"from": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"time": 1770081189000,
"pushText": "韩先生:我们感谢您的投递但您的专业技能与我们目前的职位需求并不完全吻合。祝您在BOSS直聘找到更适合您的工作",
"taskId": 0,
"status": 2
},
{
"uncount": 0,
"flag": 0,
"mid": 307012846076929,
"received": true,
"securityId": "G91RCDcRlfd6n-Y1v0PcuW2JmcEY6RpuBJKIAI3ODWIfRD2XePO7K6i_APp7VZlOzCXxU_Scck1oUQ9TfKG1gGr0ym8Ar0yFmsCud7dQJOCe6v9PnHym2ahnoRR9Z0DILSr7XA~~",
"cmid": 4611687788520895950,
"type": 1,
"body": {
"extend": "",
"text": "好的,谢谢您的时间!",
"type": 1,
"templateId": 1,
"headTitle": ""
},
"offline": false,
"pushSound": 0,
"from": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"time": 1770093508000,
"pushText": "肖伟民:好的,谢谢您的时间!",
"taskId": 0,
"status": 1
}
]
[
{
"uncount": 1,
"flag": 0,
"bizType": 21050003,
"mid": 306388872406018,
"received": true,
"securityId": "l6FAuQDH5EgEB-Z1zJPUxoiSUg2Z9523Jp_in3wnvBpa97RjDD49bO-Acv9BDLEXoAlWVRufC9SHOXAYZJooF7jwLsgm5Lv3ez-EX_utlkxCIW6po1_ncoGdjyT9qh7wajk-5UjNNTGpCuszVaoArX4VKt6S1O-YlMotVz0eILByZhvb1DBbmA~~",
"cmid": 0,
"type": 3,
"body": {
"type": 8,
"templateId": 1,
"jobDesc": {
"education": "本科",
"boss": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"distance": "",
"city": "上海 浦东新区 北蔡",
"lid": "",
"partTimeDesc": "",
"expectId": 1421763565,
"title": "信息系统产品经理",
"salary": "20-30K",
"experience": "3-5年",
"bottomText": "2月1日 18:19 由你发起的沟通",
"content": "",
"jobLabel": "",
"latlon": "",
"expectPosition": "",
"company": "中建八局上海公司",
"url": "bosszp://bosszhipin.app/openwith?type=jobview&jid=510160286&uid=631237132&securityId=_gWIU90mUoEub-b1Uw8xol50QDeii-lYj5J6gGaUDpLqCFr6_7TAHagl3UwLBh3S8tgBUY_91xh-LMPqDo56Bhg2tM5ptshBG4OSYvjb2szm9uGWm64E8GFe2pIXtMoJJW53RY_enX-0S41IAQyZSwBHGDLKYC-ajb88mwLIgFzHv26-aCA6uQXewrLCx0JKQZdIcGdUYOLlbbjVOXgCwPV04Ih_o6_BUwTq8iZjpMMBSuMa_abda3A_rjsOeY_qaN0tPcB92T2m3a0xMseYDQyJQj6x8CqTN3Wn8NhF3RuYC3KvDRGYC22HGNfGIHRHIbXlOIm5N6HK6UFRkfQMoYIl6KIJOm9-q3mHf4exItvw1_K8ArABDBF2koQ0lyCMu0bvrscAuUZCY4ggK_r4iUNaJz-r7mvO5JLona10LTHVmZ5gRGXej4kIPT9F2lj02KdK0nKWG8ilYQ2AzD0FbUI52lYa5CgAu5hhO9_kHcsJdfhxKCaSbiM_To6iTrJtfEksQnmqwMLU6tpTw-N3SA-UJsXfhwJRV3uhDsKGJeIFDpJjZ6IFr3qVVIDzwROyUZlK6Tu8Y8_nnKHxrtuIKfQPU8mFrWf02LtWWJg~",
"extend": "{\"jobType\":0}",
"jobId": 510160286,
"stage": "未融资",
"geek": {
"uid": 546224865,
"headImg": 0,
"name": "",
"company": "",
"avatar": "",
"source": 0,
"certification": 0
},
"iconFlag": 0,
"positionCategory": "产品经理",
"bossTitle": "招聘主管",
"expectSalary": ""
},
"headTitle": "您正在与Boss韩先生直接沟通如下职位"
},
"offline": false,
"pushSound": 0,
"from": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"time": 1769941171000,
"taskId": 0,
"status": 2
},
{
"uncount": 0,
"flag": 0,
"bizType": 101,
"mid": 306388872410117,
"received": true,
"securityId": "ICqTpLTj9ZNwJ-z1EH0JV5QdmFoLScV7hG92WmVbwh8OOUMHjLT1wI49GP2NbxQ0XgOl0BWmE32TShuCnB7aBYL2Tmu5w_OLGAZWHy4iwex-v68JL6m90raPWD-xJK7PFUshRA~~",
"cmid": 0,
"type": 3,
"body": {
"extend": "",
"text": "您正在招的信息系统产品经理我很有兴趣,如果方便,希望可以和您进一步沟通,谢谢!",
"type": 1,
"templateId": 1,
"headTitle": ""
},
"offline": false,
"pushSound": 0,
"bizId": "42",
"from": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"time": 1769941171000,
"pushText": "肖伟民:您正在招的信息系统产品经理我很有兴趣,如果方便,希望可以和您进一步沟通,谢谢!",
"taskId": 0,
"status": 2
},
{
"uncount": 1,
"flag": 0,
"bizType": 317,
"mid": 306388872438784,
"received": true,
"securityId": "hzKRrkb1wHEAg-p1U4iTIhg_RchIKhXZ3wxOSwcIOUeWiGyipwyvAYQhZ_2yYcMrYEcZnDiGkOFlXW23Z4qMV8SxSbBx3UlHZtnh1OQLLCnzC8BOANRVYRHOieH82O44gBThQw~~",
"cmid": 0,
"type": 4,
"body": {
"style": 3,
"type": 16,
"templateId": 1,
"articles": [
{
"extend": "{\"descriptionHighParts\":[{\"endIndex\":8,\"startIndex\":6},{\"endIndex\":14,\"startIndex\":12}],\"avatarList\":[\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09095058edb5e6d5d9273d3250d5a0c4c56bb61e3b7bce0931da574d19d1d82c88.png\",\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09ba9684830b5f873d7f98f2d5757b12c86bb61e3b7bce0931da574d19d1d82c88.png\",\"https://img.bosszhipin.com/beijin/upload/item/20221117/e84be512208dcc09f663dc109c9552b92d74732363d6a4056bb61e3b7bce0931da574d19d1d82c88.png\"]}",
"picUrl": "https://img.bosszhipin.com/beijin/icon/bed51f39faf420a15620181baffee482f7aba6f40b0808dd3b1d96fc3abbc5af.png",
"highlightParts": [
{
"startIndex": 1,
"endIndex": 3
},
{
"startIndex": 10,
"endIndex": 12
}
],
"subTitle": "共**人投递,你超过**竞争者",
"description": "优秀竞争者会**,建议你**",
"statisticParameters": "",
"title": "你与该职位竞争者PK情况",
"templateId": 5,
"url": "https://m.zhipin.com/mpa/html/props/transit?targetId=a875d648fa6db1490nV62tu9ElpW&sendNum=1&bossId=5e86870fd010f0eb0Xd72d66EVFS",
"bottomText": "查看详细分析",
"timeout": 0
}
],
"headTitle": ""
},
"offline": false,
"pushSound": 0,
"from": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"time": 1769941171000,
"pushText": "你与该职位竞争者PK情况",
"taskId": 0,
"status": 2
},
{
"uncount": 0,
"flag": 0,
"bizType": 110,
"mid": 306962385814530,
"received": true,
"securityId": "6NQl3ioceNxKx-G1nOrvVjeUEQLy_S6KIErL7eOTqON2kEHVzdEw36KXlKU6wBXG4VTP227-XF9M5Ge8BKcS7BDkxqAzgrfNBxKsCZhXOlsiGlCe-SshPg43KJ1SmrpXrJa3ow~~",
"cmid": 0,
"type": 3,
"body": {
"extend": "",
"text": "我们感谢您的投递但您的专业技能与我们目前的职位需求并不完全吻合。祝您在BOSS直聘找到更适合您的工作",
"type": 1,
"templateId": 1,
"headTitle": ""
},
"offline": false,
"pushSound": 0,
"from": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"time": 1770081189000,
"pushText": "韩先生:我们感谢您的投递但您的专业技能与我们目前的职位需求并不完全吻合。祝您在BOSS直聘找到更适合您的工作",
"taskId": 0,
"status": 2
},
{
"uncount": 0,
"flag": 0,
"mid": 307012846076929,
"received": true,
"securityId": "HOZrJc7V5lAgt-I1xBVW4AIzOdmJyVezMBCstpqbYRgw8N23mSYYlslQXE15U9gbK7-EBHJcUbnhQSRF6VIrp4rZlOUgB2cMB9qz09C4DXUYv7SVELEU8i89_0YZqXlDdMq8pg~~",
"cmid": 4611687788520895950,
"type": 1,
"body": {
"extend": "",
"text": "好的,谢谢您的时间!",
"type": 1,
"templateId": 1,
"headTitle": ""
},
"offline": false,
"pushSound": 0,
"from": {
"uid": 546224865,
"headImg": 15,
"name": "肖伟民",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_15.png",
"source": 0,
"certification": 0
},
"to": {
"uid": 631237132,
"headImg": 7,
"name": "韩先生",
"company": "",
"avatar": "https://img.bosszhipin.com/boss/avatar/avatar_7.png",
"source": 0,
"certification": 0
},
"time": 1770093508000,
"pushText": "肖伟民:好的,谢谢您的时间!",
"taskId": 0,
"status": 1
}
]

View File

@@ -1,26 +0,0 @@
# 目录整理执行计划
## 📋 整理步骤
### 第一步合并AI服务
-`middleware/job/aiService.js` 的功能合并到 `services/ai_service.js`
- 保留更完整的功能middleware/job/aiService.js 功能更全)
- 删除 `middleware/job/aiService.js`
### 第二步:移动业务服务
- `middleware/job/jobManager.js``services/job_manager_service.js`
- `middleware/job/chatManager.js``services/chat_manager_service.js`
- `middleware/job/resumeManager.js``services/resume_manager_service.js`
### 第三步:更新引用
- 更新 `command.js` 中的引用
- 更新所有其他文件中的引用
### 第四步:处理废弃文件
- `services/task_scheduler.js` 标记为废弃(添加注释说明)
## ⚠️ 注意事项
- 更新所有 require 路径
- 测试确保功能正常

View File

@@ -1,60 +0,0 @@
# 目录结构整理方案
## 📋 职责划分
### services/ - 业务服务层
**职责**:对外提供业务逻辑服务,处理业务相关的操作
- 职位服务job_service.js
- 账号服务pla_account_service.js
- AI服务ai_service.js
- OSS服务ossTool.js
- 工作管理服务jobManager.js
- 聊天管理服务chatManager.js
- 简历管理服务resumeManager.js
### middleware/ - 中间件层
**职责**:系统级功能,基础设施服务
- 调度系统schedule/
- MQTT通信mqtt/
- 数据库代理dbProxy.js
- 日志代理logProxy.js
## 🔄 需要移动的文件
### 1. 从 middleware/job/ 移到 services/
- `jobManager.js``services/job_manager_service.js`
- `chatManager.js``services/chat_manager_service.js`
- `resumeManager.js``services/resume_manager_service.js`
### 2. 合并重复的AI服务
- `middleware/job/aiService.js``services/ai_service.js` 合并
- 保留 `services/ai_service.js`,删除 `middleware/job/aiService.js`
### 3. 处理未使用的文件
- `services/task_scheduler.js` - 标记为废弃或删除(实际未使用)
## 📁 整理后的目录结构
```
api/
├── services/ # 业务服务层
│ ├── index.js # 服务管理器
│ ├── ai_service.js # AI服务合并后
│ ├── job_service.js # 职位服务
│ ├── pla_account_service.js # 账号服务
│ ├── ossTool.js # OSS服务
│ ├── job_manager_service.js # 工作管理服务从middleware/job/移入)
│ ├── chat_manager_service.js # 聊天管理服务从middleware/job/移入)
│ └── resume_manager_service.js # 简历管理服务从middleware/job/移入)
└── middleware/ # 中间件层
├── schedule/ # 调度系统
│ ├── index.js
│ ├── taskQueue.js
│ ├── command.js
│ └── ...
├── mqtt/ # MQTT通信
├── dbProxy.js # 数据库代理
└── logProxy.js # 日志代理
```

View File

@@ -1,239 +0,0 @@
# 简历存储功能 - 前置条件和使用说明
## ⚠️ 重要前置条件
在使用简历存储功能之前,必须确保以下条件已满足:
### 1. 数据库表已创建
#### ✅ `pla_account` 表(平台账户表)
此表存储设备与平台账户的绑定关系,**必须先有记录**才能存储简历。
**表结构**:
```sql
CREATE TABLE `pla_account` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '账户名',
`sn_code` varchar(50) NOT NULL DEFAULT '' COMMENT '设备SN码',
`platform_type` varchar(50) NOT NULL DEFAULT '' COMMENT '平台类型(boss/liepin)',
`login_name` varchar(50) NOT NULL DEFAULT '' COMMENT '登录名',
`pwd` varchar(50) NOT NULL DEFAULT '' COMMENT '密码',
`keyword` varchar(50) NOT NULL DEFAULT '' COMMENT '关键词',
`search_url` varchar(50) NOT NULL DEFAULT '' COMMENT '搜索页网址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
**示例数据**:
```sql
INSERT INTO `pla_account` (`name`, `sn_code`, `platform_type`, `login_name`, `pwd`)
VALUES ('张三的Boss账号', 'GHJU', 'boss', '13800138000', 'password123');
```
#### ✅ `resume_info` 表(简历信息表)
此表存储简历详细信息,会自动创建(通过 Sequelize sync
**关键字段**:
- `id` - 简历UUID主键
- `sn_code` - 设备SN码关联设备
- `account_id` - 账户ID**关联 pla_account.id**
- `platform` - 平台类型boss/liepin
### 2. 数据关联关系
```
┌─────────────────┐ ┌──────────────────┐
│ pla_account │ │ resume_info │
├─────────────────┤ ├──────────────────┤
│ id (自增) │◄────────│ account_id │
│ sn_code │ │ sn_code │
│ platform_type │ │ platform │
│ login_name │ │ fullName │
│ pwd │ │ ... │
└─────────────────┘ └──────────────────┘
```
**查询逻辑**:
1. 通过 `sn_code` + `platform` 查询 `pla_account`
2. 获取 `pla_account.id` 作为 `account_id`
3.`account_id` 存入 `resume_info`
### 3. 环境配置
#### 数据库连接
确保数据库连接配置正确(`config/config.js`
#### AI服务配置可选
如需AI分析功能需配置 DeepSeek API
```env
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxx
DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
DEEPSEEK_MODEL=deepseek-chat
```
## 🚀 使用流程
### 步骤1: 创建平台账户记录
在调用简历存储功能之前,必须先在 `pla_account` 表中创建账户记录:
```javascript
const db = require('./api/middleware/dbProxy');
const pla_account = db.getModel('pla_account');
// 创建账户记录
await pla_account.create({
name: '张三的Boss账号',
sn_code: 'GHJU',
platform_type: 'boss',
login_name: '13800138000',
pwd: 'password123',
keyword: '前端工程师',
search_url: 'https://www.zhipin.com/web/geek/job'
});
```
### 步骤2: 同步数据库表结构
运行同步脚本确保表结构正确:
```bash
node scripts/sync_resume_table.js
```
### 步骤3: 调用简历存储功能
```javascript
const jobManager = require('./api/middleware/job/jobManager');
// 获取在线简历(自动存储)
const resumeData = await jobManager.get_online_resume(
'GHJU', // 设备SN码必须在 pla_account 中存在)
mqttClient, // MQTT客户端
{ platform: 'boss' } // 平台类型(必须与 pla_account.platform_type 匹配)
);
```
## ❌ 常见错误
### 错误1: "未找到设备 GHJU 在平台 boss 的账户信息"
**原因**: `pla_account` 表中没有对应的记录
**解决方案**:
```javascript
// 检查是否存在账户记录
const account = await pla_account.findOne({
where: { sn_code: 'GHJU', platform_type: 'boss' }
});
if (!account) {
// 创建账户记录
await pla_account.create({
name: '账户名称',
sn_code: 'GHJU',
platform_type: 'boss',
login_name: '登录名',
pwd: '密码'
});
}
```
### 错误2: "Unknown column 'sn_code' in 'field list'"
**原因**: 数据库表结构未同步
**解决方案**:
```bash
# 运行同步脚本
node scripts/sync_resume_table.js
# 或手动执行SQL
ALTER TABLE `resume_info`
ADD COLUMN `sn_code` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '设备SN码',
ADD COLUMN `account_id` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '用户ID';
```
### 错误3: "account_id 不能为空"
**原因**: `pla_account` 查询失败或返回 null
**解决方案**:
1. 确认 `sn_code``platform_type` 匹配
2. 检查 `pla_account` 表中是否有对应记录
3. 确认 `platform` 参数正确('boss' 不是 'Boss'
## ✅ 验证清单
使用简历存储功能前,请确认:
- [ ] `pla_account` 表已创建
- [ ] `pla_account` 表中有对应设备的记录
- [ ] `sn_code``platform_type` 匹配
- [ ] `resume_info` 表已创建
- [ ] `resume_info` 表包含 `sn_code``account_id` 字段
- [ ] 数据库连接正常
- [ ] MQTT 客户端可用
- [ ] 可选DeepSeek API 配置正确
## 📝 完整示例
```javascript
const db = require('./api/middleware/dbProxy');
const jobManager = require('./api/middleware/job/jobManager');
async function setupAndGetResume() {
const pla_account = db.getModel('pla_account');
// 1. 检查或创建账户记录
let account = await pla_account.findOne({
where: { sn_code: 'GHJU', platform_type: 'boss' }
});
if (!account) {
console.log('创建账户记录...');
account = await pla_account.create({
name: '测试账号',
sn_code: 'GHJU',
platform_type: 'boss',
login_name: '13800138000',
pwd: 'password123'
});
console.log('账户创建成功ID:', account.id);
} else {
console.log('账户已存在ID:', account.id);
}
// 2. 获取简历(自动存储)
console.log('获取在线简历...');
const resumeData = await jobManager.get_online_resume(
'GHJU',
mqttClient,
{ platform: 'boss' }
);
console.log('简历获取成功!');
console.log('姓名:', resumeData.baseInfo?.name);
// 3. 验证存储结果
const resume_info = db.getModel('resume_info');
const savedResume = await resume_info.findOne({
where: { sn_code: 'GHJU', platform: 'boss', isActive: true }
});
console.log('简历已保存ID:', savedResume.id);
console.log('关联账户ID:', savedResume.account_id);
console.log('竞争力评分:', savedResume.aiCompetitiveness);
}
```
## 🔗 相关文档
- 详细功能说明: `_doc/简历存储和分析功能说明.md`
- 数据库同步指南: `_doc/数据库表同步指南.md`
- 快速参考: `_doc/简历功能快速参考.md`
- 示例代码: `examples/resume_storage_example.js`

View File

@@ -1,238 +0,0 @@
# 简历存储和AI分析功能实现总结
## ✅ 已完成的工作
### 1. 核心功能实现
#### 📝 文件修改
**文件**: `api/middleware/job/jobManager.js`
**新增依赖**:
```javascript
const db = require('../dbProxy');
const { v4: uuidv4 } = require('uuid');
```
**新增/修改的方法**:
1. **`get_online_resume(sn_code, mqttClient, params)`** ✅
- 从MQTT获取在线简历数据
- 自动调用存储方法保存到数据库
- 支持平台参数配置默认boss
- 容错处理:存储失败不影响数据返回
2. **`saveResumeToDatabase(sn_code, platform, resumeData)`** ✅ 新增
- 解析Boss直聘响应数据
- 映射到resume_info模型字段
- 自动提取技能标签
- 处理项目经验和工作经历JSON格式
- 支持创建/更新简历(去重机制)
- 自动触发AI分析
3. **`extractSkillsFromDesc(description)`** ✅ 新增
- 从简历描述中自动提取技能标签
- 支持40+常见技术栈识别
- 自动去重
4. **`analyzeResumeWithAI(resumeId, resumeInfo)`** ✅ 新增
- 调用AI服务分析简历
- 生成专业的分析提示词
- 解析AI返回结果
- 更新AI分析字段到数据库
- 失败时使用默认分析
5. **`parseAIAnalysis(aiResponse, resumeInfo)`** ✅ 新增
- 智能解析AI返回的JSON或文本格式
- 支持中英文字段识别
- 正则表达式提取关键信息
- 容错处理
6. **`getDefaultAnalysis(resumeInfo)`** ✅ 新增
- 基于规则的默认分析算法
- 工作年限评分
- 技能数量评分
- 学历评分
- 综合竞争力计算0-100分
### 2. 数据映射实现
#### Boss直聘 → resume_info 字段映射
| 数据类型 | 映射字段数 | 状态 |
|---------|-----------|------|
| 基本信息 | 6个字段 | ✅ |
| 教育背景 | 4个字段 | ✅ |
| 工作经验 | 4个字段 | ✅ |
| 期望信息 | 4个字段 | ✅ |
| 技能专长 | 3个字段 | ✅ |
| 项目经验 | JSON数组 | ✅ |
| 工作经历 | JSON数组 | ✅ |
| AI分析 | 5个字段 | ✅ |
| 原始数据 | 完整JSON | ✅ |
**总计**: 30+ 字段完整映射
### 3. AI分析功能
#### 分析维度
- ✅ 技能标签提取5-10个
- ✅ 优势分析100字以内
- ✅ 劣势分析100字以内
- ✅ 职业建议150字以内
- ✅ 竞争力评分0-100分
#### 评分算法
```
基础分: 50分
+ 工作年限: 10年以上(+20) | 5-10年(+15) | 3-5年(+10)
+ 技能数量: 10个以上(+15) | 5-10个(+10)
+ 学历: 硕士(+10) | 本科(+5)
= 最终竞争力评分 (0-100)
```
### 4. 文档和示例
#### 📚 创建的文档
1. **`_doc/简历存储和分析功能说明.md`** ✅
- 功能概述
- 数据映射表
- 使用示例
- 注意事项
2. **`_doc/简历功能实现总结.md`** ✅
- 实现总结
- 技术细节
- 测试指南
#### 💻 创建的示例代码
**`examples/resume_storage_example.js`** ✅
- 示例1: 获取在线简历并自动存储
- 示例2: 查询已存储的简历
- 示例3: 查看简历的项目经验
- 示例4: 统计简历数据
## 🎯 功能特性
### 核心特性
-**自动存储**: 获取简历后自动保存到数据库
-**智能去重**: 同设备同平台只保留一份活跃简历
-**AI分析**: 自动调用AI服务进行简历分析
-**容错处理**: 存储或分析失败不影响主流程
-**完整数据**: 保留原始JSON数据便于追溯
-**技能提取**: 自动识别40+常见技术栈
### 技术亮点
- 🔹 使用UUID作为简历唯一标识
- 🔹 JSON格式存储复杂数据项目、工作经历
- 🔹 智能解析AI返回的多种格式
- 🔹 基于规则的默认分析作为降级方案
- 🔹 完善的日志输出便于调试
## 📊 数据流程图
```
┌─────────────┐
│ MQTT请求 │
│ get_online_ │
│ resume │
└──────┬──────┘
┌─────────────┐
│ 获取简历数据 │
│ (Boss直聘) │
└──────┬──────┘
┌─────────────┐
│ 解析数据 │
│ 字段映射 │
└──────┬──────┘
┌─────────────┐
│ 保存到数据库 │
│ resume_info │
└──────┬──────┘
┌─────────────┐
│ AI分析简历 │
│ (DeepSeek) │
└──────┬──────┘
┌─────────────┐
│ 更新AI字段 │
│ 完成存储 │
└─────────────┘
```
## 🧪 测试建议
### 单元测试
```bash
# 运行示例代码
node examples/resume_storage_example.js
```
### 集成测试
1. 确保数据库连接正常
2. 确保MQTT服务可用
3. 确保DeepSeek API配置正确
4. 调用 `get_online_resume` 方法
5. 检查数据库中的记录
6. 验证AI分析字段
### 测试用例
- ✅ 新简历创建
- ✅ 已有简历更新
- ✅ 技能标签提取
- ✅ AI分析成功
- ✅ AI分析失败降级
- ✅ 数据库存储失败容错
## 🔧 配置要求
### 环境变量
```env
# DeepSeek AI配置用于简历分析
DEEPSEEK_API_KEY=your_api_key_here
DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
DEEPSEEK_MODEL=deepseek-chat
```
### 数据库
- 表: `resume_info`
- 引擎: MySQL/MariaDB
- 字符集: UTF8MB4
## 📈 性能指标
- **数据获取**: ~2-5秒取决于MQTT响应
- **数据存储**: ~100-300ms
- **AI分析**: ~3-10秒取决于API响应
- **总耗时**: ~5-15秒
## 🚀 后续优化方向
1. **性能优化**
- 异步AI分析不阻塞主流程
- 批量处理多份简历
- 缓存AI分析结果
2. **功能增强**
- 支持更多招聘平台
- 简历版本管理
- 简历对比功能
- 导出PDF/Word
3. **AI优化**
- 优化提示词模板
- 增加更多分析维度
- 训练专用模型
## ✨ 总结
本次实现完成了从在线简历获取、数据存储到AI智能分析的完整闭环为自动化求职系统提供了坚实的数据基础。所有核心功能已实现并经过测试可以投入使用。

View File

@@ -1,251 +0,0 @@
# 简历存储和分析功能 - 快速参考
## 🚀 快速开始
### 1. 基本使用
```javascript
const jobManager = require('./api/middleware/job/jobManager');
// 获取在线简历(自动存储和分析)
const resumeData = await jobManager.get_online_resume(
'GHJU', // 设备SN码
mqttClient, // MQTT客户端
{ platform: 'boss' } // 平台可选默认boss
);
```
### 2. 查询已存储的简历
```javascript
const db = require('./api/middleware/dbProxy');
const resume_info = db.getModel('resume_info');
// 查询指定设备的简历
const resume = await resume_info.findOne({
where: {
sn_code: 'GHJU',
platform: 'boss',
isActive: true
}
});
console.log('姓名:', resume.fullName);
console.log('竞争力评分:', resume.aiCompetitiveness);
```
## 📋 主要字段说明
### 基本信息
- `fullName` - 姓名
- `gender` - 性别
- `age` - 年龄
- `phone` - 电话
- `email` - 邮箱
### 工作信息
- `workYears` - 工作年限
- `currentPosition` - 当前职位
- `currentCompany` - 当前公司
- `expectedPosition` - 期望职位
- `expectedSalary` - 期望薪资
### AI分析字段
- `aiSkillTags` - AI提取的技能标签JSON数组
- `aiStrengths` - 优势分析
- `aiWeaknesses` - 劣势分析
- `aiCareerSuggestion` - 职业建议
- `aiCompetitiveness` - 竞争力评分0-100
### 复杂数据JSON格式
- `skills` - 技能标签数组
- `projectExperience` - 项目经验数组
- `workExperience` - 工作经历数组
- `originalData` - 完整原始数据
## 🔍 常用查询示例
### 查询高竞争力简历
```javascript
const highScoreResumes = await resume_info.findAll({
where: {
aiCompetitiveness: { [db.models.op.gte]: 80 }
},
order: [['aiCompetitiveness', 'DESC']]
});
```
### 按技能搜索
```javascript
const vueResumes = await resume_info.findAll({
where: {
skills: { [db.models.op.like]: '%Vue%' }
}
});
```
### 统计数据
```javascript
// 总数
const total = await resume_info.count();
// 按平台统计
const bossCount = await resume_info.count({
where: { platform: 'boss' }
});
// 平均竞争力
const avgScore = await resume_info.findAll({
attributes: [
[db.models.sequelize.fn('AVG',
db.models.sequelize.col('aiCompetitiveness')),
'avgScore']
]
});
```
## 🎯 数据处理技巧
### 解析JSON字段
```javascript
// 解析技能标签
const skills = JSON.parse(resume.skills || '[]');
console.log('技能:', skills.join(', '));
// 解析项目经验
const projects = JSON.parse(resume.projectExperience || '[]');
projects.forEach(p => {
console.log(`项目: ${p.name} - ${p.role}`);
});
// 解析工作经历
const workExp = JSON.parse(resume.workExperience || '[]');
workExp.forEach(w => {
console.log(`${w.company} - ${w.position}`);
});
```
### 获取原始数据
```javascript
const originalData = JSON.parse(resume.originalData);
console.log('完整Boss直聘数据:', originalData);
```
## ⚙️ 配置说明
### 环境变量(.env
```env
# DeepSeek AI配置
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxx
DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
DEEPSEEK_MODEL=deepseek-chat
```
### 数据库配置
确保 `resume_info` 表已创建,字段定义参考:
`api/model/resume_info.js`
## 🐛 常见问题
### Q1: 简历保存失败怎么办?
A: 系统有容错机制,保存失败不会影响数据返回。检查日志:
```
[工作管理] 保存简历数据失败: [错误信息]
```
### Q2: AI分析失败怎么办
A: 系统会自动使用基于规则的默认分析。检查:
- DeepSeek API配置是否正确
- API密钥是否有效
- 网络连接是否正常
### Q3: 如何更新已有简历?
A: 再次调用 `get_online_resume`,系统会自动检测并更新:
```javascript
// 同一设备同一平台会自动更新
await jobManager.get_online_resume('GHJU', mqttClient);
```
### Q4: 如何查看详细日志?
A: 查看控制台输出:
```
[工作管理] 开始获取设备 GHJU 的在线简历
[工作管理] 成功获取简历数据
[工作管理] 简历已创建/更新 - ID: xxx
[工作管理] AI分析完成 - 竞争力评分: 85
```
## 📊 性能优化建议
### 1. 批量查询
```javascript
// 使用 findAll 而不是多次 findOne
const resumes = await resume_info.findAll({
where: { sn_code: { [db.models.op.in]: ['GHJU', 'ABCD'] } }
});
```
### 2. 选择性字段
```javascript
// 只查询需要的字段
const resumes = await resume_info.findAll({
attributes: ['id', 'fullName', 'aiCompetitiveness'],
where: { isActive: true }
});
```
### 3. 分页查询
```javascript
const resumes = await resume_info.findAndCountAll({
limit: 20,
offset: 0,
order: [['aiCompetitiveness', 'DESC']]
});
```
## 🔗 相关文档
- 详细说明: `_doc/简历存储和分析功能说明.md`
- 实现总结: `_doc/简历功能实现总结.md`
- 示例代码: `examples/resume_storage_example.js`
- 模型定义: `api/model/resume_info.js`
- 响应示例: `_doc/在线简历响应文本.json`
## 💡 最佳实践
1. **总是检查返回值**
```javascript
const resume = await resume_info.findOne({...});
if (resume) {
// 处理简历数据
}
```
2. **安全解析JSON**
```javascript
try {
const skills = JSON.parse(resume.skills || '[]');
} catch (e) {
console.error('解析失败:', e);
}
```
3. **使用事务处理批量操作**
```javascript
const t = await db.models.sequelize.transaction();
try {
// 批量操作
await t.commit();
} catch (error) {
await t.rollback();
}
```
4. **定期清理旧数据**
```javascript
// 删除非活跃简历
await resume_info.destroy({
where: { isActive: false }
});
```

View File

@@ -1,169 +0,0 @@
# 简历存储和分析功能说明
## 📋 功能概述
本功能实现了从在线平台Boss直聘获取用户简历数据并自动存储到数据库同时使用AI进行智能分析的完整流程。
## 🔗 数据关联
- **`pla_account`** 表:存储平台账户信息(设备与平台的绑定关系)
- **`resume_info`** 表:存储简历详细信息
- **关联关系**`resume_info.account_id` = `pla_account.id`自增ID
- **查询逻辑**:通过 `sn_code` + `platform` 查询 `pla_account` 获取 `account_id`
## 🔧 核心功能
### 1. 简历数据获取与存储
**位置**: `api/middleware/job/jobManager.js`
**主要方法**:
- `get_online_resume(sn_code, mqttClient, params)` - 获取在线简历
- `saveResumeToDatabase(sn_code, platform, resumeData)` - 保存简历到数据库
**数据流程**:
```
MQTT请求 → 获取简历数据 → 解析数据 → 存储到resume_info表 → AI分析 → 更新AI分析字段
```
### 2. 数据映射关系
#### 从Boss直聘响应到数据库字段的映射
| Boss直聘字段 | 数据库字段 | 说明 |
|-------------|-----------|------|
| `baseInfo.name` | `fullName` | 姓名 |
| `baseInfo.gender` | `gender` | 性别1=男0=女) |
| `baseInfo.age` | `age` | 年龄 |
| `baseInfo.account` | `phone` | 电话 |
| `baseInfo.emailBlur` | `email` | 邮箱 |
| `expectList[0].locationName` | `location` | 所在地 |
| `educationExpList[0].degreeName` | `education` | 学历 |
| `educationExpList[0].major` | `major` | 专业 |
| `educationExpList[0].school` | `school` | 毕业院校 |
| `educationExpList[0].endYear` | `graduationYear` | 毕业年份 |
| `baseInfo.workYearDesc` | `workYears` | 工作年限 |
| `workExpList[0].positionName` | `currentPosition` | 当前职位 |
| `workExpList[0].companyName` | `currentCompany` | 当前公司 |
| `expectList[0].positionName` | `expectedPosition` | 期望职位 |
| `expectList[0].salaryDesc` | `expectedSalary` | 期望薪资 |
| `expectList[0].locationName` | `expectedLocation` | 期望地点 |
| `expectList[0].industryDesc` | `expectedIndustry` | 期望行业 |
| `userDesc` | `skillDescription` | 技能描述 |
| `projectExpList` | `projectExperience` | 项目经验JSON |
| `workExpList` | `workExperience` | 工作经历JSON |
### 3. AI智能分析
**分析维度**:
1. **技能标签提取** - 从简历描述中自动提取技术栈
2. **优势分析** - 分析候选人的核心优势
3. **劣势分析** - 指出需要改进的方面
4. **职业建议** - 提供职业发展建议
5. **竞争力评分** - 0-100分的综合评分
**评分规则**(默认分析):
- 基础分50分
- 工作年限10年以上+20分5-10年+15分3-5年+10分
- 技能数量10个以上+15分5-10个+10分
- 学历:硕士+10分本科+5分
### 4. 技能标签自动提取
系统会自动从简历描述中提取以下技能标签:
**前端技术**:
- Vue, React, Angular, JavaScript, TypeScript
- Webpack, Vite, Redux, MobX
- jQuery, Bootstrap, Element UI, Ant Design
**后端技术**:
- Node.js, Python, Java, C#, .NET
- Express, Koa, Django, Flask
**数据库**:
- MySQL, MongoDB, Redis
**其他技术**:
- WebRTC, FFmpeg, Canvas, WebSocket
- Git, Docker, Kubernetes, AWS, Azure
- Selenium, Jest, Mocha, Cypress
## 📊 数据库表结构
**表名**: `resume_info`
**主要字段**:
```sql
- id: IDUUID
- sn_code: SN码
- platform: boss/liepin
- fullName:
- gender:
- age:
- phone:
- email:
- education:
- workYears:
- expectedPosition:
- expectedSalary:
- skills: JSON
- projectExperience: JSON
- workExperience: JSON
- aiSkillTags: AI提取的技能标签JSON
- aiStrengths: AI分析的优势
- aiWeaknesses: AI分析的劣势
- aiCareerSuggestion: AI职业建议
- aiCompetitiveness: AI竞争力评分
- originalData: JSON
- isActive:
- syncTime:
```
## 🚀 使用示例
### 调用方式
```javascript
const jobManager = require('./api/middleware/job/jobManager');
// 获取在线简历(自动存储和分析)
const resumeData = await jobManager.get_online_resume(
'GHJU', // 设备SN码
mqttClient, // MQTT客户端实例
{ platform: 'boss' } // 参数(可选)
);
```
### 响应数据示例
参考文件: `_doc/在线简历响应文本.json`
## 🔍 日志输出
系统会输出以下日志信息:
```
[工作管理] 开始获取设备 GHJU 的在线简历
[工作管理] 成功获取简历数据: {...}
[工作管理] 简历已创建 - ID: xxx-xxx-xxx
[工作管理] 开始AI分析简历 - ID: xxx-xxx-xxx
[工作管理] AI分析完成 - 竞争力评分: 85
[工作管理] 简历数据已保存到数据库
```
## ⚠️ 注意事项
1. **数据安全**: 原始简历数据会完整保存在 `originalData` 字段中
2. **去重机制**: 同一设备同一平台只保留一份活跃简历
3. **容错处理**: 如果AI分析失败会使用基于规则的默认分析
4. **异步处理**: 简历保存失败不会影响数据返回
## 📝 后续优化建议
1. 增加更多平台支持(猎聘、拉勾等)
2. 优化AI提示词提高分析准确度
3. 添加简历版本管理功能
4. 实现简历对比功能
5. 增加简历导出功能PDF、Word等

View File

@@ -1,281 +0,0 @@
# 聊天列表功能说明
## 功能概述
聊天列表模块实现了管理后台的实时聊天功能,包括:
- 会话列表展示
- 实时消息收发
- 消息历史记录
- 轮询机制接收新消息
## 功能特性
### 1. 会话列表
- **按会话分组**: 自动按照 `conversationId``jobId + sn_code` 组合进行分组
- **最新消息展示**: 显示每个会话的最新一条消息
- **未读消息标记**: 显示未读消息数量(开发中)
- **平台过滤**: 支持按 Boss直聘/猎聘 平台筛选
- **搜索功能**: 支持按公司名称/职位名称搜索
### 2. 聊天窗口
- **消息列表**: 按时间顺序展示所有聊天消息
- **消息方向**: 区分发送和接收的消息,不同样式展示
- **AI标记**: 显示AI生成的消息标记
- **面试邀约**: 特殊样式展示面试邀约消息
- **实时刷新**: 自动轮询获取新消息(默认5秒)
### 3. 消息发送
- **快速回复**: 输入框支持快速发送消息
- **Enter发送**: 支持回车键发送消息
- **发送状态**: 显示消息发送中的加载状态
- **AI生成**: 预留AI消息生成功能接口(开发中)
### 4. 定时刷新机制
- **自动刷新**: 使用 setInterval 定时刷新消息
- **可配置间隔**: 默认10秒,可自定义刷新间隔
- **资源释放**: 页面销毁时自动清除定时器
- **简单高效**: 使用Ajax轮询,无需WebSocket
## 文件结构
```
admin/src/
├── views/chat/
│ ├── chat_list.vue # 聊天列表页面(新增)
│ └── chat_records.vue # 聊天记录管理页面(原有)
├── api/operation/
│ └── chat_records_server.js # 聊天API服务
└── router/
└── component-map.js # 路由组件映射
api/
├── controller_admin/
│ └── chat_records.js # 聊天记录后端控制器(已扩展)
└── model/
└── chat_records.js # 聊天记录数据模型
```
## API 接口
### 前端API服务 (chat_records_server.js)
| 方法 | 说明 | 参数 |
|------|------|------|
| `page(param)` | 分页查询聊天记录 | seachOption, pageOption |
| `getByJobId(params)` | 获取指定职位的聊天记录 | jobId, sn_code |
| `sendMessage(data)` | 发送聊天消息 | sn_code, jobId, content, chatType, platform |
| `getUnreadCount(params)` | 获取未读消息数量 | sn_code |
| `markAsRead(data)` | 标记消息为已读 | chatId |
| `getStatistics()` | 获取聊天统计数据 | - |
### 后端API接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/admin_api/chat/list` | POST | 获取聊天记录列表 |
| `/admin_api/chat/by-job` | GET | 获取指定职位的聊天记录 |
| `/admin_api/chat/send` | POST | 发送聊天消息 |
| `/admin_api/chat/unread-count` | GET | 获取未读消息数量 |
| `/admin_api/chat/mark-read` | POST | 标记消息为已读 |
| `/admin_api/chat/statistics` | GET | 获取聊天统计数据 |
| `/admin_api/chat/detail` | GET | 获取聊天记录详情 |
| `/admin_api/chat/delete` | POST | 删除聊天记录 |
## 使用说明
### 1. 访问聊天列表页面
在后台菜单中添加聊天列表页面的路由配置:
```javascript
{
name: '聊天列表',
path: '/chat/chat_list',
component: 'chat/chat_list'
}
```
### 2. 查看会话列表
- 左侧显示所有会话列表
- 每个会话显示公司名称、职位名称、最新消息和时间
- 点击会话可在右侧查看完整的聊天记录
### 3. 发送消息
1. 在左侧选择一个会话
2. 在右侧聊天窗口底部的输入框中输入消息
3. 点击"发送"按钮或按回车键发送
4. 消息发送成功后会自动刷新聊天记录
### 4. 筛选和搜索
- **平台筛选**: 在顶部选择 Boss直聘 或 猎聘 进行筛选
- **关键词搜索**: 在搜索框输入公司名称或职位名称进行搜索
- 筛选和搜索会实时更新会话列表
## 数据模型
### 聊天记录模型 (chat_records)
主要字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| id | INTEGER | 主键ID |
| sn_code | STRING | 设备SN码 |
| platform | STRING | 平台(boss/liepin) |
| jobId | STRING | 职位ID |
| companyName | STRING | 公司名称 |
| jobTitle | STRING | 职位名称 |
| hrName | STRING | HR姓名 |
| content | TEXT | 消息内容 |
| direction | STRING | 消息方向(sent/received) |
| chatType | STRING | 聊天类型(greeting/reply/interview) |
| sendStatus | STRING | 发送状态(pending/sent/failed) |
| sendTime | DATE | 发送时间 |
| receiveTime | DATE | 接收时间 |
| hasReply | BOOLEAN | 是否有回复 |
| conversationId | STRING | 会话ID |
| isAiGenerated | BOOLEAN | 是否AI生成 |
| isInterviewInvitation | BOOLEAN | 是否面试邀约 |
## 技术实现
### 1. 会话分组逻辑
```javascript
// 按 conversationId 或 jobId+sn_code 分组
const convId = record.conversationId || `${record.jobId}_${record.sn_code}`
```
### 2. 定时刷新机制
```javascript
// 启动定时刷新
startAutoRefresh() {
this.refreshTimer = setInterval(() => {
// 如果有选中的会话,刷新消息
if (this.activeConversation) {
this.loadChatMessages()
}
// 刷新会话列表
this.loadConversations()
}, this.refreshInterval) // 默认10秒
}
// 停止定时刷新
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
this.refreshTimer = null
}
}
```
### 3. 消息发送流程
1. 前端调用 `chatRecordsServer.sendMessage()`
2. 后端创建聊天记录,状态为 `pending`
3. 后端通过MQTT发送消息到设备(待实现)
4. 更新聊天记录状态为 `sent`
5. 前端刷新消息列表
## 待开发功能
### 1. 优化刷新机制(可选)
当前使用简单的定时刷新,如果需要更高的实时性,可以考虑:
- WebSocket实时推送(需要服务端支持)
- 长轮询(Long Polling)
- Server-Sent Events (SSE)
- 智能刷新间隔(根据活跃度动态调整)
### 2. AI消息生成
集成AI服务生成智能回复:
- 根据聊天上下文生成合适的回复
- 支持不同的回复风格
- 提高回复效率
### 3. 富文本消息
支持更丰富的消息类型:
- 图片消息
- 文件消息
- 表情包
- Markdown格式
### 4. 消息状态管理
完善消息状态:
- 已读/未读状态
- 消息撤回
- 消息编辑
- 消息引用回复
### 5. 会话管理
增强会话管理功能:
- 会话置顶
- 会话静音
- 会话归档
- 会话标签
## 注意事项
### 1. MQTT集成
当前消息发送功能需要集成MQTT客户端才能真正发送到设备。在 `chat_records.js` 控制器中有TODO标记:
```javascript
// TODO: 这里需要通过MQTT发送消息到设备
// 目前先简单返回成功,实际需要集成MQTT客户端
```
### 2. 数据同步
- 刷新间隔不宜过短,避免服务器压力(建议10秒以上)
- 可根据实际需求调整刷新间隔
- 考虑添加手动刷新按钮,让用户主动刷新
### 3. 性能优化
- 会话列表分页加载
- 消息列表虚拟滚动
- 图片懒加载
- 消息缓存机制
### 4. 安全性
- 消息内容过滤和验证
- 防止XSS攻击
- 消息发送频率限制
- 敏感信息加密
## 测试建议
### 1. 功能测试
- 测试会话列表加载
- 测试消息发送和接收
- 测试筛选和搜索功能
- 测试定时刷新机制
### 2. 边界测试
- 测试空会话列表
- 测试空消息列表
- 测试网络异常情况
- 测试大量消息加载
### 3. 性能测试
- 测试大量会话的加载速度
- 测试长时间运行的内存占用
- 测试频繁切换会话的响应速度
## 更新日志
### v1.0.0 (2025-01-XX)
- ✅ 创建聊天列表页面
- ✅ 实现会话分组和展示
- ✅ 实现消息发送功能
- ✅ 实现定时刷新接收新消息(Ajax轮询)
- ✅ 添加后端API接口
- ✅ 支持平台筛选和搜索
### 计划中
- ⏳ AI消息生成
- ⏳ 富文本消息支持
- ⏳ 完善消息状态管理
- ⏳ 增强会话管理功能
- ⏳ 优化刷新机制(WebSocket/长轮询等)

View File

@@ -1,151 +0,0 @@
# 聊天功能快速开始
## 快速配置
### 1. 添加菜单路由
在后台管理系统的菜单配置中添加以下菜单项:
```json
{
"name": "聊天列表",
"path": "/chat/chat_list",
"component": "chat/chat_list",
"icon": "md-chatbubbles"
}
```
### 2. 启动项目
```bash
# 启动后端API服务
cd api
npm install
npm start
# 启动前端管理后台
cd admin
npm install
npm run dev
```
### 3. 访问聊天列表
在浏览器中访问: `http://localhost:8080/#/chat/chat_list`
## 功能演示
### 查看会话列表
1. 左侧显示所有聊天会话
2. 每个会话显示:
- 公司名称
- 职位名称
- 最新消息内容
- 消息时间
- 平台标签(Boss/猎聘)
### 查看聊天记录
1. 点击左侧的任意会话
2. 右侧显示完整的聊天历史记录
3. 消息按时间顺序排列
4. 区分发送和接收的消息
### 发送消息
1. 选择一个会话
2. 在底部输入框输入消息
3. 点击"发送"按钮或按回车键
4. 消息发送成功后会自动刷新
### 筛选和搜索
- **平台筛选**: 选择Boss直聘或猎聘
- **关键词搜索**: 输入公司名称或职位名称
## API测试
### 测试获取聊天列表
```bash
curl -X POST http://localhost:3000/admin_api/chat/list \
-H "Content-Type: application/json" \
-d '{
"page": 1,
"pageSize": 20
}'
```
### 测试发送消息
```bash
curl -X POST http://localhost:3000/admin_api/chat/send \
-H "Content-Type: application/json" \
-d '{
"sn_code": "GHJU",
"jobId": "12345",
"content": "您好,我对这个职位很感兴趣",
"platform": "boss",
"chatType": "reply"
}'
```
### 测试获取指定职位的聊天记录
```bash
curl -X GET "http://localhost:3000/admin_api/chat/by-job?jobId=12345&sn_code=GHJU"
```
## 常见问题
### 1. 页面显示空白?
- 检查API服务是否正常运行
- 检查浏览器控制台是否有错误
- 确认数据库中是否有聊天记录数据
### 2. 消息发送失败?
- 检查设备SN码是否正确
- 检查职位ID是否存在
- 查看后端日志确认错误原因
- 注意: 当前MQTT集成待完成,消息会保存但不会真正发送到设备
### 3. 轮询不工作?
- 检查浏览器控制台是否有网络错误
- 确认轮询定时器是否正常启动
- 可以调整轮询间隔 (默认5秒)
### 4. 会话列表为空?
- 检查筛选条件是否过于严格
- 尝试清空搜索关键词
- 确认数据库中有聊天记录
## 下一步
### 功能扩展
- 集成MQTT实现真实消息发送
- 添加WebSocket实现实时推送
- 集成AI生成智能回复
- 支持富文本和文件消息
### 性能优化
- 实现消息虚拟滚动
- 添加消息缓存机制
- 优化大量会话的加载性能
### 用户体验
- 添加消息已读状态
- 支持消息撤回
- 添加消息搜索功能
- 支持会话置顶和归档
## 技术支持
如有问题,请查看:
- [聊天列表功能说明.md](./聊天列表功能说明.md) - 完整的功能文档
- 项目代码中的注释
- 后端API的Swagger文档
## 更新记录
- **2025-01-XX**: 初始版本发布
- 实现基础聊天列表功能
- 支持消息发送和接收
- 添加轮询机制
- 支持平台筛选和搜索

File diff suppressed because it is too large Load Diff

View File

@@ -1,299 +0,0 @@
# autoAiWorkSys 调度架构分析与优化建议
## 📋 目录
1. [架构概览](#架构概览)
2. [核心问题分析](#核心问题分析)
3. [优化建议](#优化建议)
4. [重构方案](#重构方案)
---
## 架构概览
### 当前架构层次
```
应用入口 (app.js)
└─> ScheduleManager (middleware/schedule/index.js)
├─> TaskQueue (taskQueue.js) - 设备级任务队列
├─> Strategy (strategy.js) - 调度策略
├─> Monitor (monitor.js) - 监控系统
├─> Command (command.js) - 指令执行
└─> MQTT Client - 设备通信
ServiceManager (services/index.js)
├─> TaskScheduler (task_scheduler.js) - 通用任务调度器(未使用)
├─> JobService (job_service.js) - 职位服务
└─> JobManager (job/jobManager.js) - 工作管理
```
### 任务执行流程
```
任务创建 → TaskQueue.addTask()
保存到数据库 (task_status)
processQueue() - 单设备串行执行
executeTask() - 执行任务
getTaskCommands() - 生成指令序列
Command.executeCommands() - 执行指令
MQTT.publishAndWait() - 发送到设备
更新任务状态
```
---
## 核心问题分析
### 🔴 问题1: 架构层次混乱,职责不清
**问题描述:**
- 存在两套调度系统:`TaskScheduler` (services层) 和 `ScheduleManager` (middleware层)
- `TaskScheduler` 定义了完整的调度功能但未被使用
- `TaskQueue``TaskScheduler` 功能重叠(都有优先级队列、重试机制)
- `ServiceManager``ScheduleManager` 职责边界模糊
**影响:**
- 代码维护困难,新人难以理解
- 功能重复,增加维护成本
- 扩展性差,难以统一优化
---
### 🔴 问题2: 任务执行效率低
**问题描述:**
- 每个设备单线程串行执行(`TaskQueue.processQueue()`
- 优先级队列使用简单数组,插入效率 O(n)
- 无法充分利用多核CPU资源
- 设备间无法并行执行
**影响:**
- 设备资源利用率低
- 任务执行延迟高
- 无法横向扩展
---
### 🔴 问题3: 重试机制分散,可能导致重复重试
**问题描述:**
- `TaskScheduler` 有重试机制maxRetries, retryDelay
- `TaskQueue` 有重试机制retryCount, maxRetries
- `Command` 也有重试机制maxRetries, retryDelay
- 三层重试可能导致总重试次数超出预期
**影响:**
- 重试次数不可控
- 资源浪费
- 错误处理逻辑复杂
---
### 🔴 问题4: 状态管理分散,可能不一致
**问题描述:**
- 内存状态:`TaskQueue.deviceQueues``TaskQueue.deviceStatus`
- 数据库状态:`task_status`
- 监控状态:`Monitor.deviceOnlineStatus`
- 策略状态:`Strategy.deviceTimestamps``Strategy.dailyCounters`
**影响:**
- 服务重启后状态丢失
- 内存和数据库状态可能不一致
- 难以追踪任务真实状态
---
### 🔴 问题5: 优先级队列实现效率低
**问题描述:**
- 使用简单数组 + `sort()` 实现优先级队列
- 每次插入都需要排序,时间复杂度 O(n log n)
- 应该使用堆Heap数据结构
**影响:**
- 队列操作性能差
- 任务数量多时性能下降明显
---
### 🔴 问题6: MQTT客户端获取方式不统一
**问题描述:**
- `ScheduleManager` 初始化时创建 MQTT 客户端
- `TaskQueue` 通过 `getMqttClient()` 动态获取
- `JobService` 直接从 `scheduleManager` 获取
- 可能导致多个MQTT连接或连接丢失
**影响:**
- 资源管理混乱
- 连接状态不可控
- 难以监控和调试
---
### 🔴 问题7: 错误处理不完善
**问题描述:**
- 部分异步操作缺少 try-catch
- 错误信息记录不完整
- 错误恢复机制缺失
**影响:**
- 错误难以追踪
- 系统稳定性差
- 调试困难
---
## 优化建议
### ✅ 优化1: 统一调度架构
**建议:**
1. **移除未使用的 `TaskScheduler`**,统一使用 `ScheduleManager` + `TaskQueue`
2. **明确职责划分**
- `ScheduleManager`: 系统初始化、组件协调、定时任务
- `TaskQueue`: 任务队列管理、执行调度
- `Command`: 指令执行、MQTT通信
- `Strategy`: 调度策略、频率控制
- `Monitor`: 监控、统计、告警
---
### ✅ 优化2: 提升任务执行效率
**建议:**
1. **使用工作池模式**:允许设备间并行执行
2. **优化优先级队列**使用堆Heap数据结构
3. **支持任务并发控制**:每个设备可配置最大并发数
---
### ✅ 优化3: 统一重试机制
**建议:**
1. **只在 TaskQueue 层实现重试**,移除 Command 层的重试
2. **使用指数退避策略**
3. **记录重试原因和次数**
---
### ✅ 优化4: 统一状态管理
**建议:**
1. **使用数据库作为唯一数据源**Single Source of Truth
2. **内存状态仅作为缓存**,定期同步到数据库
3. **服务启动时从数据库恢复状态**
---
### ✅ 优化5: 优化优先级队列
**建议:**
使用堆Heap数据结构实现优先级队列
---
### ✅ 优化6: 统一MQTT客户端管理
**建议:**
1. **使用单例模式**统一管理MQTT客户端
2. **实现连接池**(如果需要多个连接)
3. **添加连接状态监控和自动重连**
---
### ✅ 优化7: 完善错误处理
**建议:**
1. **统一错误处理中间件**
2. **完善错误日志记录**(包含上下文信息)
3. **实现错误恢复机制**
---
## 重构方案
### 阶段1: 架构清理(优先级:高)
1. **移除未使用的代码**
- 删除或标记 `TaskScheduler`(如果确实未使用)
- 清理重复功能
2. **统一MQTT管理**
- 实现统一的MQTT客户端管理器
- 所有模块通过统一接口获取客户端
3. **统一错误处理**
- 实现错误处理中间件
- 完善错误日志
### 阶段2: 性能优化(优先级:高)
1. **优化优先级队列**
- 使用堆数据结构
- 提升插入和删除效率
2. **实现工作池模式**
- 允许设备间并行执行
- 支持并发控制
3. **优化数据库操作**
- 批量更新任务状态
- 使用事务保证一致性
### 阶段3: 状态管理优化(优先级:中)
1. **统一状态管理**
- 数据库作为唯一数据源
- 内存状态作为缓存
2. **实现状态同步**
- 定期同步内存状态到数据库
- 服务启动时恢复状态
### 阶段4: 监控和可观测性(优先级:中)
1. **完善监控指标**
- 任务执行时间分布
- 错误率统计
- 资源使用情况
2. **实现告警机制**
- 任务失败率告警
- 设备离线告警
- 系统资源告警
---
## 总结
### 关键优化点
1.**统一架构**:移除冗余,明确职责
2.**提升性能**:工作池模式、堆队列、并发控制
3.**统一重试**:避免重复重试,使用指数退避
4.**状态管理**:数据库为主,内存为缓存
5.**资源管理**统一MQTT客户端管理
6.**错误处理**:完善错误处理和恢复机制
### 预期收益
- **性能提升**:任务执行效率提升 50-100%
- **稳定性提升**:错误处理更完善,系统更稳定
- **可维护性提升**:代码结构更清晰,易于维护
- **可扩展性提升**:支持更多设备和任务类型
---
*文档生成时间2024年*
*分析范围autoAiWorkSys 调度架构*

View File

@@ -1,255 +0,0 @@
# 调度架构重构完成说明
## ✅ 已完成的优化
### 1. 优先级队列优化PriorityQueue.js
**实现内容:**
- 使用堆Heap数据结构实现优先级队列
- 时间复杂度:插入 O(log n),删除 O(log n)
- 支持按优先级和创建时间排序
**性能提升:**
- 队列操作性能提升 10-100 倍(取决于队列大小)
- 任务数量多时性能优势明显
**使用方式:**
```javascript
const queue = new PriorityQueue();
queue.push({ priority: 10, createdAt: Date.now(), ...task });
const task = queue.pop(); // 获取优先级最高的任务
```
---
### 2. 工作池模式实现
**实现内容:**
- **设备内串行执行**:每个设备的任务按顺序执行(`deviceMaxConcurrency = 1`
- **设备间并行执行**:不同设备可以同时执行任务
- **全局并发控制**:通过 `maxConcurrency` 控制全局最大并发设备数默认5
**配置说明:**
```javascript
const taskQueue = new TaskQueue({
maxConcurrency: 5, // 全局最大并发设备数
deviceMaxConcurrency: 1 // 每个设备最大并发数(保持串行)
});
```
**执行流程:**
```
设备A: 任务1 → 任务2 → 任务3 (串行)
设备B: 任务1 → 任务2 → 任务3 (串行)
设备C: 任务1 → 任务2 → 任务3 (串行)
并行执行最多5个设备同时执行
```
---
### 3. 统一重试机制
**实现内容:**
- 移除了 Command 层的重试逻辑
- 统一在 TaskQueue 层实现重试
- 使用指数退避策略
**重试策略:**
- 基础延迟1000ms
- 最大延迟30000ms
- 计算公式:`delay = min(1000 * 2^(retryCount-1), 30000)`
**重试次数:**
- 第1次重试延迟 1000ms
- 第2次重试延迟 2000ms
- 第3次重试延迟 4000ms
- 第4次重试延迟 8000ms
- ...最大30000ms
---
### 4. 统一错误处理ErrorHandler.js
**实现内容:**
- 统一错误分类(可重试/不可重试)
- 自动记录错误日志到数据库
- 错误上下文信息完整记录
**可重试错误类型:**
- 网络错误ETIMEDOUT, ECONNRESET, ENOTFOUND
- MQTT连接错误
- 设备离线错误
- 超时错误
**使用方式:**
```javascript
const errorInfo = await ErrorHandler.handleError(error, {
taskId: task.id,
sn_code: task.sn_code,
taskType: task.taskType
});
if (ErrorHandler.isRetryableError(error)) {
// 可重试
}
```
---
### 5. 统一MQTT客户端管理
**实现内容:**
- 优先使用 ScheduleManager 初始化的 MQTT 客户端
- 避免重复创建连接
- 统一获取接口
**获取方式:**
```javascript
// TaskQueue 内部自动获取
const mqttClient = await this.getMqttClient();
```
---
### 6. 状态管理优化
**实现内容:**
- 服务启动时从数据库恢复未完成任务
- 内存状态作为缓存,数据库为主
- 定期同步状态到数据库
**恢复机制:**
- 启动时自动加载 `pending``running` 状态的任务
- `running` 状态的任务自动重置为 `pending`
- 确保服务重启后任务不丢失
---
## 📊 性能对比
### 优化前
- 队列插入O(n log n) - 每次插入都要排序
- 任务执行:完全串行,设备间无法并行
- 重试机制:三层重试,可能重复重试
- 错误处理:分散,难以追踪
### 优化后
- 队列插入O(log n) - 堆插入
- 任务执行设备间并行最多5个设备同时执行
- 重试机制:统一重试,指数退避
- 错误处理:统一处理,完整记录
### 预期性能提升
- **队列操作性能**:提升 10-100 倍
- **任务执行效率**:提升 50-100%(设备间并行)
- **错误恢复能力**:提升 80%(统一错误处理)
- **系统稳定性**:显著提升(状态恢复机制)
---
## 🔧 使用说明
### 1. 初始化
TaskQueue 会在 ScheduleManager 初始化时自动初始化:
```javascript
// 在 schedule/index.js 中
await this.components.taskQueue.init?.();
```
### 2. 添加任务
```javascript
const taskId = await taskQueue.addTask(sn_code, {
taskType: 'get_job_list',
taskName: '获取岗位列表',
taskParams: { keyword: '前端', platform: 'boss' },
priority: 7,
maxRetries: 3
});
```
### 3. 获取状态
```javascript
// 获取设备状态
const status = taskQueue.getDeviceStatus(sn_code);
// 获取全局统计
const stats = taskQueue.getStatistics();
```
### 4. 配置并发数
```javascript
// 在创建 TaskQueue 实例时配置
const taskQueue = new TaskQueue({
maxConcurrency: 10, // 全局最大并发设备数
deviceMaxConcurrency: 1 // 每个设备最大并发数(保持串行)
});
```
---
## 📝 代码变更说明
### 新增文件
1. `PriorityQueue.js` - 优先级队列实现
2. `ErrorHandler.js` - 统一错误处理
### 修改文件
1. `taskQueue.js` - 完全重构
- 使用 PriorityQueue 替代数组
- 实现工作池模式
- 统一重试机制
- 集成错误处理
### 兼容性
- ✅ 保持原有 API 接口不变
- ✅ 数据库结构不变
---
## 🚀 后续优化建议
### 1. 监控和告警
- 添加任务执行时间监控
- 实现失败率告警
- 资源使用监控
### 2. 性能优化
- 批量更新数据库状态
- 使用 Redis 缓存热点数据
- 实现任务预取机制
### 3. 扩展功能
- 支持任务依赖关系
- 实现任务优先级动态调整
- 支持任务暂停/恢复
---
## ⚠️ 注意事项
1. **设备内串行执行**:每个设备仍然保持串行执行,确保任务顺序
2. **全局并发控制**默认最多5个设备同时执行可根据服务器性能调整
3. **状态恢复**:服务重启后会自动恢复未完成任务
4. **错误处理**:不可重试的错误会立即标记为失败,不会重试
---
## 📞 问题反馈
如遇到问题,请检查:
1. 数据库连接是否正常
2. MQTT 客户端是否初始化
3. 任务状态是否正确更新
4. 错误日志中的详细信息
---
*重构完成时间2024年*
*重构版本v2.0*

View File

@@ -1,387 +0,0 @@
# 自动找工作系统 - 项目功能总结
> 版本: v1.0 | 更新日期: 2025-12-25
## 一、项目概述
自动找工作系统(autoAiWorkSys)是一个基于AI的智能求职助手平台,通过自动化技术帮助求职者高效管理多个招聘平台账号、智能筛选匹配岗位、自动投递简历,并提供全流程的求职数据分析。系统集成Qwen 2.5 AI模型,实现简历智能分析、岗位匹配度评分、聊天内容生成等功能,大幅提升求职效率和成功率。
## 二、技术栈
### 前端技术
- **框架**: Vue 2.6.14 + Vuex 3.6.2 + Vue Router 3.5.3
- **UI组件**: View Design (iView) 4.7.0
- **构建工具**: Webpack 5
- **图表库**: ECharts
- **HTTP客户端**: 自定义 framework.http
### 后端技术
- **运行时**: Node.js
- **Web框架**: Koa 2.16.3
- **ORM**: Sequelize 5.22.5
- **数据库**: MySQL 8.0
- **消息队列**: MQTT (mqtt://192.144.167.231:1883)
- **AI模型**: Qwen 2.5 (阿里云DashScope)
- **缓存**: Redis (规划中)
- **存储**: Ali OSS
### 核心框架
- **Node Core Framework**: 自研框架,提供统一的API路由、数据库管理、日志管理
## 三、核心特性
1. **多平台账号管理** - 支持Boss直聘、猎聘等多个招聘平台,统一管理账号和授权
2. **智能简历分析** - AI评估简历竞争力(0-100分),提取技能标签,给出优势劣势和职业建议
3. **自动岗位投递** - 基于技能匹配和AI评分自动筛选岗位并投递,支持每日上限和时间范围控制
4. **AI岗位匹配** - 多维度评分(技能、经验、薪资、公司质量),自动识别外包岗位
5. **任务调度系统** - 优先级队列+MQTT通信,设备内串行、设备间并行执行
6. **数据可视化统计** - 投递成功率、面试转化率、不同平台效果对比等多维度分析
7. **设备实时监控** - 在线状态、健康度、错误信息、心跳检测
8. **完整审计日志** - 任务执行、投递记录、聊天记录全链路追踪
## 四、功能模块一览
### 4.1 前端功能模块
| 模块 | 页面路径 | 主要功能 |
|------|---------|----------|
| **首页/仪表板** | `/home` | 设备选择、账户信息卡片、今日统计、当前任务列表、近7天趋势图 |
| **账号管理** | `/account/pla_account` | 账号列表、新增/编辑、授权管理、批量位置解析、停止任务 |
| **账号详情** | `/account/pla_account_detail` | 账号基本信息、任务历史、自动化配置、运行操作面板 |
| **简历管理** | `/account/resume_info` | 简历列表、查看详情、AI分析结果展示、删除 |
| **简历详情** | `/account/resume_info_detail` | 个人信息、教育背景、工作经验、期望信息、AI评分和建议 |
| **岗位管理** | `/work/job_postings` | 岗位列表、过滤查询、打招呼、查看详情 |
| **投递记录** | `/work/apply_records` | 投递状态追踪、反馈状态、面试/Offer信息 |
| **职位类型** | `/work/job_types` | 职位类型配置、技能关键词、排除关键词 |
| **任务管理** | `/task/task_status` | 任务列表、指令详情、取消/重试操作 |
| **聊天管理** | `/chat/chat_list` | 双面板聊天界面、会话列表、AI生成回复 |
| **聊天记录** | `/chat/chat_records` | 聊天历史记录、消息类型、发送状态 |
| **系统配置** | `/system/system_config` | 系统参数配置、AI服务配置、MQTT配置 |
### 4.2 后端API模块
| 模块 | 接口前缀 | 主要功能 |
|------|---------|----------|
| **账号管理** | `/admin_api/account` | 列表、详情、新增、更新、删除、授权、停止任务、位置解析 |
| **简历管理** | `/admin_api/resume` | 列表、详情、统计、删除、AI分析、按设备获取 |
| **岗位管理** | `/admin_api/job` | 列表、详情、统计、删除、打招呼 |
| **投递记录** | `/admin_api/apply` | 列表、详情、统计、删除 |
| **聊天记录** | `/admin_api/chat` | 列表、详情、统计、删除 |
| **设备监控** | `/admin_api/device` | 列表、概览、配置更新、错误重置 |
| **任务状态** | `/admin_api/task` | 列表、详情、统计、更新、删除 |
| **数据统计** | `/admin_api/dashboard` | 综合统计、投递转化率、平台对比、设备排名 |
| **系统配置** | `/admin_api/system` | 配置列表、新增、更新、删除 |
### 4.3 核心业务流程
```
┌─────────────────────┐
│ 账号配置和授权 │
└──────────┬──────────┘
┌─────────────────────┐
│ 获取在线简历(MQTT) │ → AI分析 → 竞争力评分、技能提取、优劣势分析
└──────────┬──────────┘
┌─────────────────────┐
│ 创建自动投递任务 │
└──────────┬──────────┘
┌─────────────────────┐
│ 搜索岗位(MQTT) │ → 保存到job_postings表
└──────────┬──────────┘
┌─────────────────────┐
│ 岗位过滤和匹配 │ → 技能匹配+AI评分+外包识别
└──────────┬──────────┘
┌─────────────────────┐
│ 自动投递(MQTT) │ → 记录apply_records
└──────────┬──────────┘
┌─────────────────────┐
│ 聊天和面试跟踪 │ → AI生成聊天内容
└──────────┬──────────┘
┌─────────────────────┐
│ 数据统计和分析 │
└─────────────────────┘
```
## 五、已实现功能清单
### 5.1 账号管理模块 ✅
- ✅ 多平台账号绑定(Boss直聘、猎聘)
- ✅ 账号状态管理(启用/禁用、在线/离线、登录状态)
- ✅ 自动化开关(自动投递、自动聊天、自动活跃)
- ✅ 授权管理(设置授权日期、天数、过期时间计算)
- ✅ 职位类型配置(关联job_types)
- ✅ 投递配置(时间范围、每日上限、薪资范围、关键词过滤)
- ✅ 沟通配置(时间范围、是否沟通外包岗位)
- ✅ 活跃配置(活跃间隔、活跃动作)
- ✅ 位置解析(单个/批量,经纬度获取)
- ✅ 停止任务(取消该账号所有运行中任务)
### 5.2 简历管理模块 ✅
- ✅ 简历信息存储(个人信息、教育背景、工作经验、期望信息)
- ✅ 简历获取(通过MQTT从设备获取在线简历)
- ✅ AI竞争力评分(0-100分)
- ✅ AI技能标签提取
- ✅ AI优势劣势分析
- ✅ AI职业建议生成
- ✅ 简历与岗位匹配度计算
- ✅ 简历统计(平均竞争力、工作年限分布、竞争力分布)
- ✅ 按设备和平台查询简历
### 5.3 岗位管理模块 ✅
- ✅ 岗位信息存储(基本信息、要求、描述、薪资、地点)
- ✅ 岗位搜索(通过MQTT下发搜索指令)
- ✅ 岗位列表获取(通过MQTT获取列表)
- ✅ AI岗位匹配评分(技能、经验、薪资、公司质量)
- ✅ 外包岗位识别(规则+AI双层识别)
- ✅ 岗位过滤(技能关键词匹配、排除关键词检测)
- ✅ 综合评分计算(多维度权重评分)
- ✅ 岗位统计(总数、平均匹配度、外包比例)
- ✅ 打招呼功能(初次沟通)
### 5.4 自动投递模块 ✅
- ✅ 自动投递任务创建和调度
- ✅ 每日投递上限控制
- ✅ 投递时间范围控制(工作日/周末)
- ✅ 简历刷新检查(2小时内刷新)
- ✅ 岗位过滤和排序
- ✅ 投递指令下发(MQTT)
- ✅ 投递状态追踪(待投递、投递中、成功、失败、重复)
- ✅ 反馈状态管理(无反馈、已查看、感兴趣、不合适、面试邀约)
- ✅ 投递统计(成功率、面试转化率、Offer转化率)
### 5.5 AI分析模块 ✅
- ✅ Qwen 2.5模型集成(阿里云DashScope)
- ✅ 简历智能分析
- ✅ 技能标签提取
- ✅ 竞争力评分(0-100)
- ✅ 优势劣势分析
- ✅ 职业发展建议
- ✅ 岗位智能匹配
- ✅ 技能匹配度(0-100)
- ✅ 经验匹配度(0-100)
- ✅ 薪资合理性(0-100)
- ✅ 公司质量评分(0-100)
- ✅ 外包岗位识别
- ✅ 聊天内容生成(基础框架)
### 5.6 任务调度模块 ✅
- ✅ 优先级任务队列(堆实现,O(log n)性能)
- ✅ 并发控制(全局5设备,每设备1任务)
- ✅ 设备内串行、设备间并行执行
- ✅ 任务状态管理(待执行、执行中、成功、失败、超时、取消)
- ✅ 指数退避重试机制(最多3次)
- ✅ 任务超时检测(10分钟)
- ✅ 错误分类(可重试/不可重试)
- ✅ 任务恢复(启动时恢复未完成任务)
- ✅ 任务统计(岗位搜索数、过滤数、投递数、聊天数)
### 5.7 设备监控模块 ✅
- ✅ 设备状态追踪(在线/离线)
- ✅ 心跳检测(通过MQTT)
- ✅ 健康度评分
- ✅ 错误信息记录
- ✅ 最后心跳时间
- ✅ 设备配置更新
- ✅ 设备错误重置
- ✅ 设备概览统计(在线数、离线数、健康度排名)
### 5.8 数据统计模块 ✅
- ✅ 投递成功率统计
- ✅ 面试转化率统计
- ✅ Offer转化率统计
- ✅ 不同平台数据对比
- ✅ 设备活跃度排名
- ✅ 简历竞争力分布
- ✅ 岗位外包比例统计
- ✅ 近7天趋势图(投递、搜索、聊天)
- ✅ 今日统计(实时刷新)
### 5.9 聊天管理模块 ✅
- ✅ 聊天记录存储
- ✅ 聊天类型分类(打招呼、跟进、面试、回复)
- ✅ 发送状态追踪
- ✅ 回复检测和记录
- ✅ 回复时长统计
- ✅ 面试邀约识别
- ✅ 情感分析(积极/中性/消极)
- ✅ 效果评分
- ✅ AI生成标记
- ✅ 双面板聊天界面
### 5.10 其他功能 ✅
- ✅ 用户邀请和推广系统
- ✅ 公司信息库(上市公司数据)
- ✅ 版本管理
- ✅ 邮件服务集成
- ✅ OSS存储集成
- ✅ 地理位置服务(百度地图API)
- ✅ Swagger API文档
- ✅ 数据导出(CSV)
## 六、数据模型
### 6.1 核心表结构
| 表名 | 说明 | 关键字段 |
|------|------|---------|
| **pla_account** | 平台账号表 | sn_code, platform_type, is_online, auto_deliver, deliver_config |
| **resume_info** | 简历信息表 | resumeId, account_id, aiCompetitiveness, aiSkillTags, aiStrengths |
| **job_postings** | 岗位信息表 | jobId, platform, aiMatchScore, isOutsourcing, applyStatus |
| **apply_records** | 投递记录表 | resumeId, jobId, applyStatus, feedbackStatus, hasInterview, hasOffer |
| **chat_records** | 聊天记录表 | conversationId, chatType, isAiGenerated, hasReply, sentiment |
| **task_status** | 任务状态表 | taskType, status, priority, retryCount, progress |
| **task_commands** | 任务指令表 | taskId, commandType, commandData, response, executeTime |
| **job_types** | 职位类型表 | name, commonSkills, excludeKeywords, sortOrder |
| **device_monitor** | 设备监控表 | sn_code, is_online, health_score, last_heartbeat_time |
| **company_info** | 公司信息表 | company_name, is_listed, market_value, risk_level |
### 6.2 表关联关系
```
pla_account (1) ──→ (N) resume_info
├─→ (N) apply_records ←── (1) job_postings
│ │
│ └─→ (N) chat_records
└─→ (N) task_status ──→ (N) task_commands
pla_account (1) ──→ (1) job_types (职位类型配置)
pla_account (1) ──→ (N) device_monitor (设备监控)
```
## 七、快速开始
### 7.1 环境要求
- Node.js >= 14.x
- MySQL >= 8.0
- Redis (可选,用于缓存)
- MQTT Broker (已配置: mqtt://192.144.167.231:1883)
### 7.2 安装步骤
```bash
# 1. 克隆项目
git clone <repository-url>
cd autoAiWorkSys
# 2. 安装后端依赖
npm install
# 3. 安装前端依赖
cd admin
npm install
# 4. 配置数据库
# 编辑 config/config.js
# 设置MySQL连接信息
# 5. 初始化数据库
# 执行 _sql 目录下的SQL脚本
# 6. 启动后端服务
npm run dev
# 7. 启动前端服务(新终端)
cd admin
npm run dev
```
### 7.3 核心配置
**config/config.js** - 主配置文件
```javascript
{
db: {
host: 'localhost',
port: 3306,
database: 'auto_job',
username: 'root',
password: 'your_password'
},
mqtt: {
host: 'mqtt://192.144.167.231:1883',
clientId: 'autoAiWorkSys_server'
},
ai: {
apiKey: 'your_dashscope_api_key',
model: 'qwen-turbo' // qwen-turbo/qwen-plus/qwen-max
}
}
```
### 7.4 访问地址
- 前端管理后台: http://localhost:8080
- 后端API: http://localhost:3000/admin_api
- API文档: http://localhost:3000/api/docs
## 八、项目文件结构
```
autoAiWorkSys/
├── admin/ # 前端管理后台
│ ├── src/
│ │ ├── views/ # 页面组件
│ │ ├── api/ # API调用
│ │ ├── router/ # 路由配置
│ │ └── store/ # Vuex状态管理
│ └── package.json
├── api/ # 后端服务
│ ├── controller_admin/ # 后台管理API
│ ├── middleware/ # 核心业务逻辑
│ │ ├── job/ # 岗位、简历、聊天管理
│ │ ├── schedule/ # 任务调度系统
│ │ └── mqtt/ # MQTT通信
│ ├── model/ # 数据库模型
│ ├── services/ # 业务服务层
│ └── utils/ # 工具函数
├── config/ # 配置文件
├── framework/ # 核心框架
├── _doc/ # 文档目录
├── _sql/ # 数据库脚本
└── package.json
```
## 九、技术亮点
1. **优先级队列** - 堆实现,O(log n)性能,比数组提升10-100倍
2. **双层过滤** - 规则过滤+AI评分,平衡性能和准确性
3. **智能重试** - 指数退避策略,区分可重试和不可重试错误
4. **MQTT通信** - 异步消息队列,高效的设备指令下发和响应
5. **AI多场景应用** - 简历分析、岗位匹配、聊天生成、外包识别
6. **完整审计** - 任务、投递、聊天全链路追踪
7. **模块化设计** - 清晰的分层架构,易于扩展和维护
## 十、性能指标
- 任务调度延迟: < 100ms
- 数据库查询: 95%在100ms内
- MQTT消息延迟: < 50ms
- 前端页面加载: < 2s
- 并发支持: 最多5个设备同时执行
---
**文档维护**: 开发团队
**最后更新**: 2025-12-25
**联系方式**: 项目Issues

View File

@@ -34,7 +34,6 @@ async function syncAllModels() {
// 执行同步
await model.sync({ alter: true });
console.log(`${modelName} 同步完成`);
return { modelName, success: true };
} catch (error) {

View File

@@ -0,0 +1,66 @@
-- 为 pla_account 表添加自动搜索相关字段
-- 执行时间2025-01-XX
-- 说明:添加自动搜索开关和搜索配置字段
-- ============================================
-- 添加自动搜索开关字段auto_search
-- ============================================
ALTER TABLE `pla_account`
ADD COLUMN `auto_search` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '自动搜索开关1=启用0=禁用)'
AFTER `auto_deliver`;
-- ============================================
-- 添加自动搜索配置字段search_config
-- ============================================
ALTER TABLE `pla_account`
ADD COLUMN `search_config` JSON COMMENT '自动搜索配置JSON对象包含search_interval-搜索间隔分钟数, city-城市, cityName-城市名称, salary-薪资, experience-经验, education-学历)'
AFTER `auto_search`;
-- ============================================
-- 为已有账号设置默认配置
-- ============================================
-- 为所有账号设置默认的 search_config如果为 NULL
UPDATE `pla_account`
SET `search_config` = JSON_OBJECT(
'search_interval', 30,
'city', '',
'cityName', '',
'salary', '',
'experience', '',
'education', ''
)
WHERE `search_config` IS NULL;
-- ============================================
-- 验证字段是否添加成功
-- ============================================
SELECT
COLUMN_NAME AS '字段名',
COLUMN_TYPE AS '字段类型',
IS_NULLABLE AS '允许空',
COLUMN_DEFAULT AS '默认值',
COLUMN_COMMENT AS '注释'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'pla_account'
AND COLUMN_NAME IN ('auto_search', 'search_config')
ORDER BY ORDINAL_POSITION;
-- ============================================
-- 注意事项
-- ============================================
-- 1. auto_search 使用 TINYINT(1) 类型,默认值为 0关闭
-- 2. search_config 使用 JSON 类型MySQL 5.7+
-- 3. 如果 MySQL 版本低于 5.7,请将 JSON 类型改为 TEXT 类型
-- 4. 执行前建议先备份数据库
-- 5. 如果字段已存在会报错,请先删除字段再执行:
-- ALTER TABLE `pla_account` DROP COLUMN `auto_search`;
-- ALTER TABLE `pla_account` DROP COLUMN `search_config`;
-- 6. search_config 默认值包含以下字段:
-- - search_interval: 30搜索间隔单位分钟
-- - city: ''(城市代码)
-- - cityName: ''(城市名称)
-- - salary: ''(薪资范围)
-- - experience: ''(工作经验要求)
-- - education: ''(学历要求)

View File

@@ -26,11 +26,11 @@ const baseConfig = {
// 开发环境配置
const developmentConfig = {
...baseConfig,
// apiUrl: 'http://localhost:9097/admin_api/',
// uploadUrl: 'http://localhost:9097/admin_api/upload',
apiUrl: 'http://localhost:9097/admin_api/',
uploadUrl: 'http://localhost:9097/admin_api/upload',
apiUrl: 'https://work.light120.com/admin_api/',
uploadUrl: 'https://work.light120.com/admin_api/upload',
// apiUrl: 'https://work.light120.com/admin_api/',
// uploadUrl: 'https://work.light120.com/admin_api/upload',
// 开发环境显示更多调试信息
debug: true
}

View File

@@ -87,7 +87,9 @@ class TaskStatusServer {
* @returns {Promise}
*/
cancel(row) {
return window.framework.http.post(`task_status/${row.taskId}/cancel`)
return window.framework.http.post('task/cancel', {
taskId: row.taskId || row.id
})
}
/**
@@ -96,7 +98,9 @@ class TaskStatusServer {
* @returns {Promise}
*/
retry(row) {
return window.framework.http.post(`task_status/${row.taskId}/retry`)
return window.framework.http.post('task/retry', {
taskId: row.taskId || row.id
})
}
/**

View File

@@ -190,8 +190,8 @@
<div class="detail-item">
<span class="label">自动投递</span>
<span class="value">
<Tag :color="deliverConfig.auto_deliver ? 'success' : 'default'">
{{ deliverConfig.auto_deliver ? '开启' : '关闭' }}
<Tag :color="deliverConfig.auto_deliver ? 'success' : 'default'">
{{ deliverConfig.auto_deliver ? '开启' : '关闭' }}
</Tag>
</span>
</div>
@@ -229,6 +229,12 @@
</div>
</Col>
<Col span="8">
<div class="detail-item">
<span class="label">同公司重复投递间隔()</span>
<span class="value">{{ deliverConfig.repeat_deliver_days != null ? deliverConfig.repeat_deliver_days : '-' }}</span>
</div>
</Col>
<Col span="8">
<div class="detail-item">
<span class="label">过滤关键词</span>
<span class="value">{{ deliverConfig.filter_keywords || '-' }}</span>
@@ -360,15 +366,9 @@
<div class="tab-body">
<Table :columns="taskColumns" :data="tasksData" :loading="tasksLoading" border>
</Table>
<Page
:current="tasksPageOption.page"
:total="tasksPageOption.total"
:page-size="tasksPageOption.pageSize"
show-total
show-elevator
show-sizer
@on-change="queryTasks"
@on-page-size-change="handleTasksPageSizeChange"
<Page :current="tasksPageOption.page" :total="tasksPageOption.total"
:page-size="tasksPageOption.pageSize" show-total show-elevator show-sizer
@on-change="queryTasks" @on-page-size-change="handleTasksPageSizeChange"
style="margin-top: 16px; text-align: right;">
</Page>
</div>
@@ -380,15 +380,9 @@
<div class="tab-body">
<Table :columns="commandColumns" :data="commandsData" :loading="commandsLoading" border>
</Table>
<Page
:current="commandsPageOption.page"
:total="commandsPageOption.total"
:page-size="commandsPageOption.pageSize"
show-total
show-elevator
show-sizer
@on-change="queryCommands"
@on-page-size-change="handleCommandsPageSizeChange"
<Page :current="commandsPageOption.page" :total="commandsPageOption.total"
:page-size="commandsPageOption.pageSize" show-total show-elevator show-sizer
@on-change="queryCommands" @on-page-size-change="handleCommandsPageSizeChange"
style="margin-top: 16px; text-align: right;">
</Page>
</div>
@@ -497,6 +491,10 @@
</div>
<div slot="footer">
<Button @click="closeCommandDetail">关闭</Button>
<Button v-if="currentCommandDetail && currentCommandDetail.status === 'failed'" type="primary"
:loading="retryCommandLoading" @click="retryCommand(currentCommandDetail)">
重试
</Button>
</div>
</Modal>
@@ -568,6 +566,7 @@ export default {
max_salary: 0,
page_count: 3,
max_deliver: 10,
repeat_deliver_days: 30,
filter_keywords: '',
exclude_keywords: ''
},
@@ -708,13 +707,13 @@ export default {
width: 150,
render: (h, params) => {
const btns = []
if (params.row.status === 'failed') {
btns.push({
title: '重试',
type: 'primary',
click: () => this.retryTask(params.row)
})
}
btns.push({
title: '重试',
type: 'primary',
click: () => this.retryTask(params.row)
})
if (params.row.status === 'pending' || params.row.status === 'running') {
btns.push({
title: '取消',
@@ -748,6 +747,7 @@ export default {
},
// 指令详情弹窗
commandDetailVisible: false,
retryCommandLoading: false,
currentCommandDetail: null,
// 二维码弹窗
@@ -825,13 +825,13 @@ export default {
click: () => this.showCommandDetail(params.row)
})
// 重试按钮(只在失败状态时显示)
if (params.row.status === 'failed') {
btns.push({
title: '重试',
type: 'warning',
click: () => this.retryCommand(params.row)
})
}
btns.push({
title: '重试',
type: 'warning',
click: () => this.retryCommand(params.row)
})
return h('div', btns.map(btn =>
h('Button', {
props: {
@@ -915,6 +915,7 @@ export default {
max_salary: deliverConfig.max_salary || 0,
page_count: deliverConfig.page_count || 3,
max_deliver: deliverConfig.max_deliver || 10,
repeat_deliver_days: deliverConfig.repeat_deliver_days != null ? deliverConfig.repeat_deliver_days : 30,
filter_keywords: Array.isArray(deliverConfig.filter_keywords)
? deliverConfig.filter_keywords.join(',')
: (deliverConfig.filter_keywords || ''),
@@ -933,6 +934,7 @@ export default {
max_salary: 0,
page_count: 3,
max_deliver: 10,
repeat_deliver_days: 30,
filter_keywords: '',
exclude_keywords: '',
deliver_start_time: '09:00',
@@ -1031,7 +1033,7 @@ export default {
const res = await plaAccountServer.getTasks(this.accountId, param)
console.log('res', res);
this.tasksData = res.data.rows || []
this.tasksPageOption.total = res.data.count || 0
@@ -1081,23 +1083,37 @@ export default {
}
},
// 重试指令
// 重试指令(指令列表与指令详情弹窗共用,成功后刷新列表与详情)
async retryCommand(command) {
this.$Modal.confirm({
title: '确认重试',
content: `确定要重试指令"${command.command_name}"吗?`,
onOk: async () => {
try {
await plaAccountServer.retryCommand(command.id)
this.$Message.success('重试指令成功')
// 刷新指令列表
} catch (error) {
console.error('重试指令失败:', error)
this.$Message.error(error.message || '重试指令失败')
const isFromDetail = this.commandDetailVisible && this.currentCommandDetail && this.currentCommandDetail.id === command.id
if (!isFromDetail) {
this.$Modal.confirm({
title: '确认重试',
content: `确定要重试指令"${command.command_name}"吗?`,
onOk: async () => {
await this.doRetryCommand(command)
}
})
return
}
await this.doRetryCommand(command)
},
async doRetryCommand(command) {
this.retryCommandLoading = true
try {
await plaAccountServer.retryCommand(command.id)
this.$Message.success('重试指令成功')
this.queryCommands(this.commandsPageOption.page)
if (this.commandDetailVisible && this.currentCommandDetail && this.currentCommandDetail.id === command.id) {
const res = await plaAccountServer.getCommandDetail(this.accountId, command.id)
this.currentCommandDetail = res.data || {}
}
})
} catch (error) {
console.error('重试指令失败:', error)
this.$Message.error(error.message || '重试指令失败')
} finally {
this.retryCommandLoading = false
}
},
// 取消任务

View File

@@ -148,6 +148,9 @@
<FormItem label="每次最多投递数">
<InputNumber v-model="formData.max_deliver" :min="1" placeholder="默认10个" style="width: 100%;" />
</FormItem>
<FormItem label="同公司重复投递间隔(天)">
<InputNumber v-model="formData.repeat_deliver_days" :min="1" :max="365" placeholder="默认30天N天内投过的公司跳过" style="width: 100%;" />
</FormItem>
<FormItem label="过滤关键词">
<Input
v-model="formData.filter_keywords"
@@ -268,6 +271,7 @@ export default {
max_salary: 0,
page_count: 3,
max_deliver: 10,
repeat_deliver_days: 30,
filter_keywords: '',
exclude_keywords: '',
deliver_start_time: '09:00',
@@ -386,6 +390,7 @@ export default {
this.formData.max_salary = deliverConfig.max_salary || 0
this.formData.page_count = deliverConfig.page_count || 3
this.formData.max_deliver = deliverConfig.max_deliver || 10
this.formData.repeat_deliver_days = deliverConfig.repeat_deliver_days != null ? deliverConfig.repeat_deliver_days : 30
this.formData.filter_keywords = Array.isArray(deliverConfig.filter_keywords)
? deliverConfig.filter_keywords.join('\n')
: (deliverConfig.filter_keywords || '')
@@ -447,6 +452,7 @@ export default {
max_salary: 0,
page_count: 3,
max_deliver: 10,
repeat_deliver_days: 30,
filter_keywords: '',
exclude_keywords: '',
auto_chat: 0,
@@ -600,6 +606,9 @@ export default {
if (saveData.max_deliver !== undefined) {
deliverConfig.max_deliver = Number(saveData.max_deliver) || 10
}
if (saveData.repeat_deliver_days !== undefined) {
deliverConfig.repeat_deliver_days = Number(saveData.repeat_deliver_days) || 30
}
// 解析过滤关键词:支持换行和逗号分隔
if (saveData.filter_keywords !== undefined) {
deliverConfig.filter_keywords = this.parseKeywords(saveData.filter_keywords)
@@ -623,6 +632,7 @@ export default {
delete saveData.max_salary
delete saveData.page_count
delete saveData.max_deliver
delete saveData.repeat_deliver_days
delete saveData.filter_keywords
delete saveData.exclude_keywords
delete saveData.deliver_start_time

View File

@@ -28,13 +28,14 @@
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
@changePage="query"></tables>
</div>
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules" @on-save="handleSaveSuccess">
<editModal ref="editModal" :columns="editFormColumns" :rules="gridOption.rules" @on-save="handleSaveSuccess">
</editModal>
</div>
</template>
<script>
import jobTypesServer from '@/api/work/job_types_server.js'
import plaAccountServer from '@/api/profile/pla_account_server.js'
export default {
data() {
@@ -44,8 +45,10 @@ export default {
return {
seachTypes: [
{ key: 'name', value: '职位类型名称' },
{ key: 'description', value: '描述' }
{ key: 'description', value: '描述' },
{ key: 'pla_account_id', value: '关联账户ID' }
],
plaAccountOptions: [],
gridOption: {
param: {
seachOption: {
@@ -63,6 +66,23 @@ export default {
},
listColumns: [
{ title: 'ID', key: 'id', minWidth: 80 },
{
title: '关联账户',
key: 'pla_account',
minWidth: 200,
render: (h, params) => {
const id = params.row.pla_account_id
const pa = params.row.pla_account
if (id == null || id === '') {
return h('span', { style: { color: '#999' } }, '-')
}
if (pa && (pa.name || pa.sn_code)) {
const txt = `${pa.name || ''} (SN:${pa.sn_code || '-'})`
return h('span', { attrs: { title: `ID:${id} ${txt}` } }, txt)
}
return h('span', { attrs: { title: '仅ID账户可能已删除' } }, `ID:${id}`)
}
},
{ title: '职位类型名称', key: 'name', minWidth: 150 },
{ title: '描述', key: 'description', minWidth: 200 },
{
@@ -77,6 +97,11 @@ export default {
{
title: '常见技能关键词', key: 'commonSkills', minWidth: 200,
},
{
title: '标题须含关键词',
key: 'titleIncludeKeywords',
minWidth: 200
},
{
title: '排除关键词', key: 'excludeKeywords', minWidth: 200
},
@@ -147,6 +172,22 @@ export default {
placeholder: '请输入JSON数组格式例如["外包", "销售", "客服"]',
tooltip: '排除关键词列表JSON数组格式'
},
{
title: '标题须含关键词',
key: 'titleIncludeKeywords',
com: 'TextArea',
required: false,
placeholder: '请输入JSON数组格式例如["售前", "工程师"]',
tooltip: 'JSON数组格式仅匹配岗位标题须同时包含每一项与「常见技能关键词」无关'
},
{
title: '关联账户',
key: 'pla_account_id',
type: 'select',
required: false,
tooltip: '可选;与设备/账号绑定AI 同步 Tab 时会写入',
options: []
},
{
title: '是否启用',
key: 'is_enabled',
@@ -171,12 +212,35 @@ export default {
seachTypePlaceholder() {
const item = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
return item ? `请输入${item.value}` : '请选择'
},
editFormColumns() {
const accOpts = [{ value: '', label: '不关联' }, ...this.plaAccountOptions]
return this.editColumns.map((col) => {
if (col.key === 'pla_account_id') {
return { ...col, options: accOpts }
}
return col
})
}
},
mounted() {
this.loadPlaAccountOptions()
this.query(1)
},
methods: {
loadPlaAccountOptions() {
plaAccountServer.page({
pageOption: { page: 1, pageSize: 999 },
seachOption: {}
}).then((res) => {
if (res.code === 0 && res.data && res.data.rows) {
this.plaAccountOptions = res.data.rows.map((r) => ({
value: r.id,
label: `${r.name || ''} (SN:${r.sn_code || r.id})`
}))
}
}).catch(() => {})
},
query(page) {
if (page) {
this.gridOption.param.pageOption.page = page
@@ -217,6 +281,8 @@ export default {
description: '',
commonSkills: '[]',
excludeKeywords: '[]',
titleIncludeKeywords: '[]',
pla_account_id: '',
is_enabled: 1,
sort_order: 0
})
@@ -249,12 +315,26 @@ export default {
excludeKeywords = JSON.stringify(excludeKeywords, null, 2)
}
let titleIncludeKeywords = row.titleIncludeKeywords || '[]'
if (typeof titleIncludeKeywords === 'string') {
try {
const parsed = JSON.parse(titleIncludeKeywords)
titleIncludeKeywords = JSON.stringify(parsed, null, 2)
} catch (e) {
// 保持原样
}
} else {
titleIncludeKeywords = JSON.stringify(titleIncludeKeywords, null, 2)
}
this.$refs.editModal.editShow({
id: row.id,
name: row.name,
description: row.description || '',
commonSkills: commonSkills,
excludeKeywords: excludeKeywords,
titleIncludeKeywords: titleIncludeKeywords,
pla_account_id: row.pla_account_id != null && row.pla_account_id !== '' ? row.pla_account_id : '',
is_enabled: row.is_enabled,
sort_order: row.sort_order || 0
})
@@ -281,7 +361,7 @@ export default {
// 处理 JSON 字段
const formData = { ...data }
// 处理 commonSkills
// 处理 commonSkillsJSON 数组)
if (formData.commonSkills) {
try {
const parsed = typeof formData.commonSkills === 'string'
@@ -294,7 +374,7 @@ export default {
}
}
// 处理 excludeKeywords
// 处理 excludeKeywordsJSON 数组)
if (formData.excludeKeywords) {
try {
const parsed = typeof formData.excludeKeywords === 'string'
@@ -307,6 +387,28 @@ export default {
}
}
// 处理 titleIncludeKeywordsJSON 数组,与上两项一致)
if (formData.titleIncludeKeywords) {
try {
const parsed = typeof formData.titleIncludeKeywords === 'string'
? JSON.parse(formData.titleIncludeKeywords)
: formData.titleIncludeKeywords
formData.titleIncludeKeywords = Array.isArray(parsed) ? parsed : []
} catch (e) {
this.$Message.warning('标题须含关键词格式错误,将使用空数组')
formData.titleIncludeKeywords = []
}
} else {
formData.titleIncludeKeywords = []
}
if (formData.pla_account_id === undefined || formData.pla_account_id === '') {
formData.pla_account_id = null
} else if (formData.pla_account_id != null) {
const n = parseInt(formData.pla_account_id, 10)
formData.pla_account_id = Number.isNaN(n) ? null : n
}
const apiMethod = formData.id ? jobTypesServer.update : jobTypesServer.add
apiMethod(formData).then(res => {
if (res.code === 0) {

View File

@@ -277,7 +277,7 @@ module.exports = {
const records = await chat_records.findAll({
where: { jobId, sn_code },
order: [['sendTime', 'ASC'], ['receiveTime', 'ASC'], ['id', 'ASC']]
order: [['updateTime', 'ASC'], ['id', 'ASC']]
});
return ctx.success(records);
@@ -338,8 +338,7 @@ module.exports = {
content,
chatType,
direction: 'sent',
sendStatus: 'pending',
sendTime: new Date()
// 对话会话表当前不再区分单条消息时间,这里仅保留必要字段
});
console.log(`[聊天管理] 消息待发送到设备 ${sn_code}:`, content);
@@ -482,8 +481,8 @@ module.exports = {
record.direction || '',
record.chatType || '',
`"${(record.content || '').replace(/"/g, '""')}"`,
record.sendTime || '',
record.receiveTime || '',
'',
'',
record.hasReply ? '是' : '否'
];
csvContent += row.join(',') + '\n';

View File

@@ -165,7 +165,7 @@ switch (type) {
break;
case 'chat':
Model = models.chat_records;
dateField = 'sendTime';
dateField = 'updateTime';
break;
default:
return ctx.fail('无效的统计类型');

View File

@@ -4,7 +4,7 @@
*/
const Framework = require("../../framework/node-core-framework.js");
const jobManager = require("../middleware/job/jobManager.js");
const jobManager = require("../middleware/job/index.js");
module.exports = {
/**

View File

@@ -5,6 +5,57 @@
const Framework = require("../../framework/node-core-framework.js");
/**
* 为 job_types 行批量附加 pla_account列表/详情展示)
* @param {import('sequelize').Model[]} rowInstances
* @param {object} models
* @returns {Promise<object[]>}
*/
async function attachPlaAccountToJobTypeRows(rowInstances, models) {
const { pla_account, op } = models;
const plain = (rowInstances || []).map((r) => (r && typeof r.toJSON === 'function' ? r.toJSON() : r));
const ids = [...new Set(plain.map((row) => row.pla_account_id).filter((id) => id != null && id !== ''))];
if (!pla_account || ids.length === 0) {
return plain.map((row) => ({ ...row, pla_account: null }));
}
const accounts = await pla_account.findAll({
where: { id: { [op.in]: ids } },
attributes: ['id', 'name', 'sn_code', 'login_name']
});
const map = {};
accounts.forEach((a) => {
const j = a.toJSON();
map[j.id] = j;
});
return plain.map((row) => ({
...row,
pla_account: row.pla_account_id != null ? map[row.pla_account_id] || null : null
}));
}
/**
* @param {*} raw
* @param {object} pla_account
* @returns {Promise<{ ok: boolean, value?: number|null, message?: string }>}
*/
async function normalizePlaAccountId(raw, pla_account) {
if (raw === undefined) {
return { ok: true, skip: true };
}
if (raw === null || raw === '') {
return { ok: true, value: null };
}
const n = parseInt(raw, 10);
if (Number.isNaN(n) || n < 1) {
return { ok: false, message: '关联账户ID无效' };
}
const acc = await pla_account.findByPk(n);
if (!acc) {
return { ok: false, message: '关联账户不存在' };
}
return { ok: true, value: n };
}
module.exports = {
/**
* @swagger
@@ -37,14 +88,26 @@ module.exports = {
const models = Framework.getModels();
const { job_types, op } = models;
const body = ctx.getBody();
const { name } = body;
const seachOption = body.seachOption || {};
// 获取分页参数
const { limit, offset } = ctx.getPageSize();
const where = {};
if (name) {
where.name = { [op.like]: `%${name}%` };
const key = seachOption.key || body.key;
const value = seachOption.value !== undefined && seachOption.value !== null ? seachOption.value : body.value;
if (key && value !== undefined && value !== null && String(value).trim() !== '') {
const v = String(value).trim();
if (key === 'pla_account_id') {
const n = parseInt(v, 10);
if (!Number.isNaN(n)) {
where.pla_account_id = n;
}
} else if (key === 'name' || key === 'description') {
where[key] = { [op.like]: `%${v}%` };
}
}
if (seachOption.is_enabled !== undefined && seachOption.is_enabled !== null) {
where.is_enabled = seachOption.is_enabled;
}
const result = await job_types.findAndCountAll({
@@ -54,7 +117,8 @@ module.exports = {
order: [['sort_order', 'ASC'], ['id', 'ASC']]
});
return ctx.success(result);
const rows = await attachPlaAccountToJobTypeRows(result.rows, models);
return ctx.success({ rows, count: result.count });
},
/**
@@ -89,7 +153,8 @@ module.exports = {
return ctx.fail('职位类型不存在');
}
return ctx.success(jobType);
const [enriched] = await attachPlaAccountToJobTypeRows([jobType], models);
return ctx.success(enriched);
},
/**
@@ -120,6 +185,9 @@ module.exports = {
* excludeKeywords:
* type: array
* description: 排除关键词JSON数组
* titleIncludeKeywords:
* type: array
* description: 职位标题须同时包含的子串JSON数组仅匹配岗位标题
* is_enabled:
* type: integer
* description: 是否启用1=启用0=禁用)
@@ -132,9 +200,9 @@ module.exports = {
*/
'POST /job_type/create': async (ctx) => {
const models = Framework.getModels();
const { job_types } = models;
const { job_types, pla_account } = models;
const body = ctx.getBody();
const { name, description, commonSkills, excludeKeywords, is_enabled, sort_order } = body;
const { name, description, commonSkills, excludeKeywords, titleIncludeKeywords, is_enabled, sort_order } = body;
if (!name) {
return ctx.fail('职位类型名称不能为空');
@@ -146,16 +214,30 @@ module.exports = {
return ctx.fail('职位类型名称已存在');
}
let pla_account_id = null;
const paResolved = await normalizePlaAccountId(body.pla_account_id, pla_account);
if (!paResolved.ok) {
return ctx.fail(paResolved.message || '关联账户校验失败');
}
if (!paResolved.skip) {
pla_account_id = paResolved.value === undefined ? null : paResolved.value;
}
const jobType = await job_types.create({
name,
description: description || '',
commonSkills: Array.isArray(commonSkills) ? JSON.stringify(commonSkills) : (commonSkills || '[]'),
excludeKeywords: Array.isArray(excludeKeywords) ? JSON.stringify(excludeKeywords) : (excludeKeywords || '[]'),
titleIncludeKeywords: Array.isArray(titleIncludeKeywords)
? JSON.stringify(titleIncludeKeywords)
: (titleIncludeKeywords || '[]'),
pla_account_id,
is_enabled: is_enabled !== undefined ? is_enabled : 1,
sort_order: sort_order || 0
});
return ctx.success(jobType);
const [enriched] = await attachPlaAccountToJobTypeRows([jobType], models);
return ctx.success(enriched);
},
/**
@@ -189,6 +271,9 @@ module.exports = {
* excludeKeywords:
* type: array
* description: 排除关键词JSON数组
* titleIncludeKeywords:
* type: array
* description: 职位标题须同时包含的子串JSON数组
* is_enabled:
* type: integer
* description: 是否启用1=启用0=禁用)
@@ -201,9 +286,9 @@ module.exports = {
*/
'POST /job_type/update': async (ctx) => {
const models = Framework.getModels();
const { job_types } = models;
const { job_types, pla_account } = models;
const body = ctx.getBody();
const { id, name, description, commonSkills, excludeKeywords, is_enabled, sort_order } = body;
const { id, name, description, commonSkills, excludeKeywords, titleIncludeKeywords, is_enabled, sort_order } = body;
if (!id) {
return ctx.fail('职位类型ID不能为空');
@@ -231,16 +316,32 @@ module.exports = {
if (excludeKeywords !== undefined) {
updateData.excludeKeywords = Array.isArray(excludeKeywords) ? JSON.stringify(excludeKeywords) : excludeKeywords;
}
if (titleIncludeKeywords !== undefined) {
updateData.titleIncludeKeywords = Array.isArray(titleIncludeKeywords)
? JSON.stringify(titleIncludeKeywords)
: titleIncludeKeywords;
}
if (body.pla_account_id !== undefined) {
const paResolved = await normalizePlaAccountId(body.pla_account_id, pla_account);
if (!paResolved.ok) {
return ctx.fail(paResolved.message || '关联账户校验失败');
}
if (!paResolved.skip) {
updateData.pla_account_id = paResolved.value === undefined ? null : paResolved.value;
}
}
if (is_enabled !== undefined) updateData.is_enabled = is_enabled;
if (sort_order !== undefined) updateData.sort_order = sort_order;
await job_types.update(updateData, { where: { id } });
// 清除缓存
const jobFilterService = require('../middleware/job/job_filter_service.js');
const jobFilterService = require('../middleware/job/services/jobFilterService.js');
jobFilterService.clearCache(id);
return ctx.success({ message: '职位类型更新成功' });
const updated = await job_types.findByPk(id);
const [enriched] = await attachPlaAccountToJobTypeRows([updated], models);
return ctx.success(enriched);
},
/**
@@ -289,7 +390,7 @@ module.exports = {
await job_types.destroy({ where: { id } });
// 清除缓存
const jobFilterService = require('../middleware/job/job_filter_service.js');
const jobFilterService = require('../middleware/job/services/jobFilterService.js');
jobFilterService.clearCache(id);
return ctx.success({ message: '职位类型删除成功' });

View File

@@ -363,7 +363,7 @@ module.exports = {
* description: 账号ID
* taskType:
* type: string
* description: 指令类型: get_login_qr_code-登录检查, get_resume-获取简历, search_jobs-搜索岗位
* description: 任务类型: get_login_qr_code-登录检查, get_resume-获取简历, auto_search-搜索岗位
* taskName:
* type: string
* description: 指令名称

View File

@@ -69,12 +69,7 @@ module.exports = {
}),
chat_records.count({
where: {
sn_code: deviceSn,
direction: 'sent',
sendTime: {
[op.gte]: todayStart,
[op.lte]: todayEnd
}
sn_code: deviceSn
}
}).catch(err => {
console.error('[统计] 查询聊天数量失败:', err);
@@ -189,14 +184,9 @@ module.exports = {
}),
chat_records.findAll({
where: {
sn_code: deviceSn,
direction: 'sent',
sendTime: {
[op.gte]: start,
[op.lte]: end
}
sn_code: deviceSn
},
attributes: ['sendTime'],
attributes: ['updateTime'],
raw: true
}).catch(err => {
console.error('[统计] 查询聊天记录失败:', err);
@@ -226,7 +216,7 @@ module.exports = {
}).length;
const chatCount = allChats.filter(item => {
const itemDate = dayjs(item.sendTime);
const itemDate = dayjs(item.updateTime);
return !itemDate.isBefore(dayStart, 'day') && !itemDate.isAfter(dayEnd, 'day');
}).length;
@@ -499,14 +489,9 @@ module.exports = {
const allChats = await chat_records.findAll({
where: {
sn_code: deviceSn,
direction: 'sent',
sendTime: {
[op.gte]: start,
[op.lte]: end
}
sn_code: deviceSn
},
attributes: ['sendTime'],
attributes: ['updateTime'],
raw: true
}).catch(err => {
console.error('[统计] 查询聊天记录失败:', err);
@@ -523,7 +508,7 @@ module.exports = {
const dayEnd = currentDate.endOf('day');
const count = allChats.filter(item => {
const itemDate = dayjs(item.sendTime);
const itemDate = dayjs(item.updateTime);
return !itemDate.isBefore(dayStart, 'day') && !itemDate.isAfter(dayEnd, 'day');
}).length;

View File

@@ -439,9 +439,7 @@ return ctx.success({
return ctx.fail('任务不存在');
}
if (task.status !== 'failed') {
return ctx.fail('只能重试失败的任务');
}
await task_status.update({
status: 'pending',

View File

@@ -0,0 +1,93 @@
const fs = require('fs');
const path = require('path');
const axios = require('axios');
/**
* 静态 JS 代理/缓存
*
* 规则:
* - 前端请求GET /static/bossheader 里带 path例如
* path: https://static.zhipin.com/fe-zhipin-geek/web/chat-new/v5410/static/js/app.4e199352.js
* - 从 URL 中取 pathname/fe-zhipin-geek/web/chat-new/v5410/static/js/app.4e199352.js
* - 去掉开头的 /,中间的 / 全部替换为 _得到本地文件名
* fe-zhipin-geek_web-chat-new_v5410_static_js_app.4e199352.js
* - 在项目根目录下的 js 目录保存/读取该文件:./js/<文件名>
* - 如果已存在:直接返回本地文件
* - 如果不存在:从远程 URL 下载,保存后返回
*/
module.exports = {
'GET /static/boss': async (ctx) => {
// 1. 获取原始 URL优先从 header兼容 query/body
const urlStr =
ctx.get('path') ||
ctx.query.path ||
(ctx.request.body && ctx.request.body.path);
if (!urlStr) {
ctx.status = 400;
ctx.body = { code: 400, message: '缺少 path 参数' };
return;
}
let urlObj = new URL(urlStr);
// 2. 生成本地文件名:去掉开头的 /,中间 / 替换为 _
const remotePath = urlObj.pathname || '/';
const fileName = remotePath
.replace(/^\/+/, '')
.replace(/\//g, '_');
// 根目录下 js 目录
const jsRootDir = path.join(process.cwd(), 'static/boss');
const localFilePath = path.join(jsRootDir, fileName);
// 钩子注入:在 JS 中注入自定义 onMessageArrived 钩子
const injectOnMessageArrivedHook = (buffer) => {
try {
let js = buffer.toString('utf8');
const needle = 'onMessageArrived:function(e){try{var t=e.payloadBytes,n=S.decode(t);';
if (js.includes(needle)) {
const hook = `${needle}if(window.Function&&window.Function.__proto__&&typeof window.Function.__proto__.$onMessageArrived==="function"){try{window.Function.__proto__.$onMessageArrived(n);}catch(e){}}`;
js = js.replace(needle, hook);
return Buffer.from(js, 'utf8');
}
return buffer;
} catch (e) {
return buffer;
}
};
// 确保目录存在
if (!fs.existsSync(jsRootDir)) {
fs.mkdirSync(jsRootDir, { recursive: true });
}
// 3. 如果文件已存在,直接返回本地文件(文件内容已是替换后的,无需再次注入)
if (fs.existsSync(localFilePath)) {
ctx.type = 'application/javascript; charset=utf-8';
ctx.body = fs.createReadStream(localFilePath);
return;
}
// 4. 文件不存在:从远程下载并保存(带钩子注入)
const response = await axios.get(urlStr, {
responseType: 'arraybuffer',
timeout: 15000,
});
if (response.status !== 200) {
ctx.status = 502;
ctx.body = { code: 502, message: '下载远程 JS 失败' };
return;
}
const patched = injectOnMessageArrivedHook(Buffer.from(response.data));
fs.writeFileSync(localFilePath, patched);
ctx.type = 'application/javascript; charset=utf-8';
ctx.body = patched;
},
}

View File

@@ -75,7 +75,7 @@ module.exports = {
* example: '用户不存在或密码错误'
*/
"POST /user/login": async (ctx) => {
const { login_name:email, password, device_id: client_device_id } = ctx.getBody();
const { login_name: email, password, device_id: client_device_id } = ctx.getBody();
const dayjs = require('dayjs');
const { verifyPassword, validateDeviceId, maskEmail } = require('../utils/crypto_utils');
@@ -87,16 +87,6 @@ module.exports = {
// 统一邮箱地址为小写
const email_normalized = email.toLowerCase().trim();
// 验证邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email_normalized)) {
return ctx.fail('邮箱格式不正确');
}
// 验证密码长度
if (password.length < 6 || password.length > 50) {
return ctx.fail('密码长度不正确');
}
const { pla_account } = await Framework.getModels();
@@ -404,17 +394,17 @@ module.exports = {
// 获取原有配置
const original_deliver_config = user.deliver_config || {};
// 深度合并配置(只覆盖传入的字段,保留原有的其他字段)
const deepMerge = (target, source) => {
const result = { ...target };
Object.keys(source).forEach(key => {
const sourceValue = source[key];
const targetValue = target[key];
// 如果源值是对象且目标值也是对象,递归合并(排除数组)
if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue) &&
targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue)) {
targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue)) {
result[key] = deepMerge(targetValue, sourceValue);
} else {
// 否则直接覆盖
@@ -423,7 +413,7 @@ module.exports = {
});
return result;
};
const new_deliver_config = deepMerge(original_deliver_config, deliver_config);
// 处理 auto_deliver 字段(支持 auto_deliver 和 auto_delivery 两种字段名)
@@ -447,7 +437,7 @@ module.exports = {
console.log('[保存投递配置成功]', {
sn_code,
deliver_config:new_deliver_config,
deliver_config: new_deliver_config,
auto_deliver: auto_deliver_value,
timestamp: new Date().toISOString()
});
@@ -460,5 +450,182 @@ module.exports = {
});
return ctx.fail('保存投递配置失败');
}
},
/**
* 根据设备SN码获取账号全部功能配置投递、沟通、活跃
*/
'POST /user/account-config/get': async (ctx) => {
try {
const body = ctx.getBody();
const { sn_code } = body;
if (!sn_code) return ctx.fail('请提供设备SN码');
const { pla_account, resume_info } = await Framework.getModels();
const user = await pla_account.findOne({ where: { sn_code } });
if (!user) return ctx.fail('用户不存在');
const u = user.toJSON ? user.toJSON() : user;
const result = {
deliver_config: u.deliver_config || null,
chat_strategy: u.chat_strategy || null,
active_actions: u.active_actions || null,
auto_chat: u.auto_chat != null ? !!u.auto_chat : false,
auto_active: u.auto_active != null ? !!u.auto_active : false
};
const platform = u.platform_type || 'boss';
const resume = await resume_info.findOne({
where: { sn_code, platform, isActive: true },
order: [['last_modify_time', 'DESC']]
});
if (resume) {
const r = resume.toJSON ? resume.toJSON() : resume;
result.job_listings = Array.isArray(r.job_listings) ? r.job_listings : [];
result.deliver_tab_label = r.deliver_tab_label != null ? String(r.deliver_tab_label) : '';
} else {
result.job_listings = [];
result.deliver_tab_label = '';
}
return ctx.success(result);
} catch (error) {
console.error('[获取账号配置失败]', error);
return ctx.fail('获取账号配置失败');
}
},
/**
* 仅保存投递标签:标签列表(来自 get_job_listings+ 当前选中的标签
* 更新 resume_info 的 job_listings、deliver_tab_label并同步 pla_account.keyword与推荐/期望职位一致)
*/
'POST /user/deliver-tab-label/save': async (ctx) => {
try {
const body = ctx.getBody();
const { sn_code, job_listings, deliver_tab_label } = body;
if (!sn_code) return ctx.fail('请提供设备SN码');
const { pla_account, resume_info } = await Framework.getModels();
const user = await pla_account.findOne({ where: { sn_code } });
if (!user) return ctx.fail('用户不存在');
const platform = user.platform_type || 'boss';
const account_id = user.account_id != null ? String(user.account_id) : (user.id != null ? String(user.id) : '');
const list = Array.isArray(job_listings) ? job_listings : [];
const label = deliver_tab_label != null ? String(deliver_tab_label) : '';
let resume = await resume_info.findOne({
where: { sn_code, platform },
order: [['last_modify_time', 'DESC']]
});
if (resume) {
await resume_info.update(
{ job_listings: list, deliver_tab_label: label },
{ where: { id: resume.id } }
);
} else {
await resume_info.create({
sn_code,
account_id: account_id || '',
platform,
resumeId: '',
job_listings: list,
deliver_tab_label: label,
isActive: true
});
}
const keywordSync = label.trim().slice(0, 50);
await pla_account.update({ keyword: keywordSync }, { where: { id: user.id } });
return ctx.success({ message: '投递标签已保存', job_listings: list, deliver_tab_label: label });
} catch (error) {
console.error('[保存投递标签失败]', error);
return ctx.fail('保存投递标签失败');
}
},
/**
* 保存账号功能配置可只传需要更新的部分deliver_config / chat_strategy / active_actions
*/
'POST /user/account-config/save': async (ctx) => {
try {
const body = ctx.getBody();
const { sn_code, deliver_config, chat_strategy, active_actions, deliver_tab_label, job_listings } = body;
if (!sn_code) return ctx.fail('请提供设备SN码');
const { pla_account, resume_info } = await Framework.getModels();
const user = await pla_account.findOne({ where: { sn_code } });
if (!user) return ctx.fail('用户不存在');
const updateData = {};
if (deliver_config !== undefined) {
const original = user.deliver_config || {};
const deepMerge = (t, s) => {
const r = { ...t };
Object.keys(s).forEach(k => {
const sv = s[k], tv = t[k];
if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
r[k] = deepMerge(tv, sv);
} else {
r[k] = sv;
}
});
return r;
};
updateData.deliver_config = deepMerge(original, deliver_config);
if (deliver_config.auto_deliver !== undefined) updateData.auto_deliver = deliver_config.auto_deliver ? 1 : 0;
else if (deliver_config.auto_delivery !== undefined) updateData.auto_deliver = deliver_config.auto_delivery ? 1 : 0;
}
if (chat_strategy !== undefined) {
updateData.chat_strategy = chat_strategy;
if (chat_strategy.auto_chat !== undefined) updateData.auto_chat = chat_strategy.auto_chat ? 1 : 0;
}
if (active_actions !== undefined) {
updateData.active_actions = active_actions;
if (active_actions.auto_active !== undefined) updateData.auto_active = active_actions.auto_active ? 1 : 0;
}
if (deliver_tab_label !== undefined || job_listings !== undefined) {
const platform = user.platform_type || 'boss';
const account_id = user.account_id != null ? String(user.account_id) : (user.id != null ? String(user.id) : '');
let resume = await resume_info.findOne({
where: { sn_code, platform },
order: [['last_modify_time', 'DESC']]
});
const resumePayload = {};
if (deliver_tab_label !== undefined) resumePayload.deliver_tab_label = deliver_tab_label;
if (job_listings !== undefined) resumePayload.job_listings = Array.isArray(job_listings) ? job_listings : [];
if (resume) {
await resume_info.update(resumePayload, { where: { id: resume.id } });
} else {
await resume_info.create({
sn_code,
account_id: account_id || '',
platform,
resumeId: '',
deliver_tab_label: resumePayload.deliver_tab_label != null ? resumePayload.deliver_tab_label : '',
job_listings: resumePayload.job_listings || [],
isActive: true
});
}
if (deliver_tab_label !== undefined) {
const keywordSync = (deliver_tab_label != null ? String(deliver_tab_label) : '')
.trim()
.slice(0, 50);
await pla_account.update({ keyword: keywordSync }, { where: { id: user.id } });
}
}
if (Object.keys(updateData).length === 0 && deliver_tab_label === undefined && job_listings === undefined) {
return ctx.success({ message: '无更新' });
}
if (Object.keys(updateData).length > 0) {
await pla_account.update(updateData, { where: { id: user.id } });
}
return ctx.success({ message: '配置保存成功' });
} catch (error) {
console.error('[保存账号配置失败]', error);
return ctx.fail('保存账号配置失败');
}
}
}

View File

@@ -1,421 +0,0 @@
// const aiService = require('./aiService'); // 二期规划AI 服务暂时禁用
const logs = require('../logProxy');
/**
* 智能聊天管理模块
* 负责聊天内容生成、发送策略和效果监控
*/
class ChatManager {
constructor() {
this.chatHistory = new Map(); // 聊天历史记录
this.chatStrategies = new Map(); // 聊天策略配置
this.effectStats = new Map(); // 聊天效果统计
this.initDefaultStrategies();
}
/**
* 初始化默认聊天策略
*/
initDefaultStrategies() {
// 初次打招呼策略
this.chatStrategies.set('greeting', {
name: '初次打招呼',
description: '向HR发送初次打招呼消息',
template: 'greeting',
timing: 'immediate',
retryCount: 1,
retryInterval: 300000 // 5分钟
});
// 面试邀约策略
this.chatStrategies.set('interview', {
name: '面试邀约',
description: '发送面试邀约消息',
template: 'interview',
timing: 'after_greeting',
retryCount: 2,
retryInterval: 600000 // 10分钟
});
// 跟进沟通策略
this.chatStrategies.set('followup', {
name: '跟进沟通',
description: '跟进之前的沟通',
template: 'followup',
timing: 'after_interview',
retryCount: 1,
retryInterval: 86400000 // 24小时
});
}
/**
* 生成聊天内容
* @param {string} sn_code - 设备SN码
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @param {string} chatType - 聊天类型
* @param {object} context - 聊天上下文
* @returns {Promise<object>} 聊天内容
*/
async generateChatContent(sn_code, jobInfo, resumeInfo, chatType = 'greeting', context = {}) {
console.log(`[聊天管理] 开始生成设备 ${sn_code} 的聊天内容,类型: ${chatType}`);
// 获取聊天策略
const strategy = this.chatStrategies.get(chatType);
if (!strategy) {
throw new Error(`未找到聊天类型 ${chatType} 的策略配置`);
}
// 二期规划AI 生成聊天内容暂时禁用,使用默认模板
// const chatContent = await aiService.generateChatContent(jobInfo, resumeInfo, chatType);
// if (!chatContent.success) {
// console.error(`[聊天管理] AI生成聊天内容失败:`, chatContent.error);
// throw new Error(chatContent.error);
// }
console.log(`[聊天管理] AI生成已禁用二期规划使用默认聊天模板`);
const chatContent = this.generateDefaultChatContent(jobInfo, resumeInfo, chatType);
const result = {
sn_code: sn_code,
jobInfo: jobInfo,
chatType: chatType,
strategy: strategy,
content: chatContent.content,
context: context,
timestamp: Date.now()
};
// 记录聊天历史
this.recordChatHistory(sn_code, result);
console.log(`[聊天管理] 聊天内容生成成功:`, result);
return result;
}
/**
* 发送聊天消息
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} chatData - 聊天数据
* @returns {Promise<object>} 发送结果
*/
async sendChatMessage(sn_code, mqttClient, chatData) {
console.log(`[聊天管理] 开始发送聊天消息到设备 ${sn_code}`);
// 构建发送指令
const sendData = {
platform: 'boss',
action: 'send_chat_message',
data: {
jobId: chatData.jobInfo.jobId,
companyId: chatData.jobInfo.companyId,
message: chatData.content,
chatType: chatData.chatType
}
};
// 发送MQTT指令
const response = await mqttClient.publishAndWait(sn_code, sendData);
if (!response || response.code !== 200) {
// 更新聊天状态
this.updateChatStatus(sn_code, chatData, 'failed', response);
console.error(`[聊天管理] 聊天消息发送失败:`, response);
throw new Error(response?.message || '聊天消息发送失败');
}
// 更新聊天状态
this.updateChatStatus(sn_code, chatData, 'sent', response);
// 记录效果统计
this.recordChatEffect(sn_code, chatData, 'sent');
console.log(`[聊天管理] 聊天消息发送成功:`, response);
return response;
}
/**
* 生成面试邀约
* @param {string} sn_code - 设备SN码
* @param {object} jobInfo - 岗位信息
* @param {object} chatHistory - 聊天历史
* @returns {Promise<object>} 面试邀约内容
*/
async generateInterviewInvitation(sn_code, jobInfo, chatHistory) {
console.log(`[聊天管理] 开始生成设备 ${sn_code} 的面试邀约`);
console.log(`[聊天管理] AI生成已禁用二期规划使用默认模板`);
// 二期规划AI 生成面试邀约暂时禁用,使用默认模板
// const invitation = await aiService.generateInterviewInvitation(jobInfo, chatHistory);
// if (!invitation.success) {
// console.error(`[聊天管理] AI生成面试邀约失败:`, invitation.error);
// throw new Error(invitation.error);
// }
const invitation = this.generateDefaultInterviewInvitation(jobInfo);
const result = {
sn_code: sn_code,
jobInfo: jobInfo,
chatType: 'interview',
content: invitation.content,
timestamp: Date.now()
};
// 记录聊天历史
this.recordChatHistory(sn_code, result);
console.log(`[聊天管理] 面试邀约生成成功:`, result);
return result;
}
/**
* 记录聊天历史
* @param {string} sn_code - 设备SN码
* @param {object} chatData - 聊天数据
*/
recordChatHistory(sn_code, chatData) {
if (!this.chatHistory.has(sn_code)) {
this.chatHistory.set(sn_code, []);
}
const history = this.chatHistory.get(sn_code);
history.push({
...chatData,
id: Date.now() + Math.random(),
status: 'generated'
});
// 限制历史记录数量
if (history.length > 100) {
history.splice(0, history.length - 100);
}
}
/**
* 更新聊天状态
* @param {string} sn_code - 设备SN码
* @param {object} chatData - 聊天数据
* @param {string} status - 新状态
* @param {object} response - 响应数据
*/
updateChatStatus(sn_code, chatData, status, response = {}) {
if (!this.chatHistory.has(sn_code)) {
return;
}
const history = this.chatHistory.get(sn_code);
const chatRecord = history.find(record =>
record.timestamp === chatData.timestamp &&
record.chatType === chatData.chatType
);
if (chatRecord) {
chatRecord.status = status;
chatRecord.response = response;
}
}
/**
* 记录聊天效果
* @param {string} sn_code - 设备SN码
* @param {object} chatData - 聊天数据
* @param {string} action - 动作类型
*/
recordChatEffect(sn_code, chatData, action) {
if (!this.effectStats.has(sn_code)) {
this.effectStats.set(sn_code, {
totalSent: 0,
totalReplied: 0,
totalInterview: 0,
replyRate: 0,
interviewRate: 0,
lastUpdate: Date.now()
});
}
const stats = this.effectStats.get(sn_code);
if (action === 'sent') {
stats.totalSent++;
} else if (action === 'replied') {
stats.totalReplied++;
} else if (action === 'interview') {
stats.totalInterview++;
}
// 计算比率
if (stats.totalSent > 0) {
stats.replyRate = (stats.totalReplied / stats.totalSent * 100).toFixed(2);
stats.interviewRate = (stats.totalInterview / stats.totalSent * 100).toFixed(2);
}
stats.lastUpdate = Date.now();
}
/**
* 获取聊天历史
* @param {string} sn_code - 设备SN码
* @param {object} filters - 过滤条件
* @returns {Array} 聊天历史
*/
getChatHistory(sn_code, filters = {}) {
if (!this.chatHistory.has(sn_code)) {
return [];
}
let history = this.chatHistory.get(sn_code);
// 应用过滤条件
if (filters.chatType) {
history = history.filter(record => record.chatType === filters.chatType);
}
if (filters.status) {
history = history.filter(record => record.status === filters.status);
}
if (filters.startTime) {
history = history.filter(record => record.timestamp >= filters.startTime);
}
if (filters.endTime) {
history = history.filter(record => record.timestamp <= filters.endTime);
}
// 按时间倒序排列
return history.sort((a, b) => b.timestamp - a.timestamp);
}
/**
* 获取聊天效果统计
* @param {string} sn_code - 设备SN码
* @returns {object} 效果统计
*/
getChatEffectStats(sn_code) {
if (!this.effectStats.has(sn_code)) {
return {
totalSent: 0,
totalReplied: 0,
totalInterview: 0,
replyRate: 0,
interviewRate: 0,
lastUpdate: Date.now()
};
}
return this.effectStats.get(sn_code);
}
/**
* 设置聊天策略
* @param {string} chatType - 聊天类型
* @param {object} strategy - 策略配置
*/
setChatStrategy(chatType, strategy) {
this.chatStrategies.set(chatType, {
...this.chatStrategies.get(chatType),
...strategy
});
console.log(`[聊天管理] 更新聊天策略 ${chatType}:`, strategy);
}
/**
* 清理过期数据
*/
cleanup() {
const now = Date.now();
const expireTime = 30 * 24 * 3600000; // 30天
// 清理过期的聊天历史
for (const [sn_code, history] of this.chatHistory.entries()) {
const filteredHistory = history.filter(record =>
now - record.timestamp < expireTime
);
if (filteredHistory.length === 0) {
this.chatHistory.delete(sn_code);
} else {
this.chatHistory.set(sn_code, filteredHistory);
}
}
// 清理过期的效果统计
for (const [sn_code, stats] of this.effectStats.entries()) {
if (now - stats.lastUpdate > expireTime) {
this.effectStats.delete(sn_code);
}
}
console.log(`[聊天管理] 数据清理完成`);
}
/**
* 生成默认聊天内容(替代 AI 生成)
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @param {string} chatType - 聊天类型
* @returns {object} 聊天内容
*/
generateDefaultChatContent(jobInfo, resumeInfo, chatType) {
const templates = {
greeting: '您好,我对这个岗位很感兴趣,希望能进一步了解。',
interview: '感谢您的回复,我很期待与您进一步沟通。',
followup: '您好,想了解一下这个岗位的最新进展。'
};
const content = templates[chatType] || templates.greeting;
return {
success: true,
content: content,
chatType: chatType
};
}
/**
* 生成默认面试邀约(替代 AI 生成)
* @param {object} jobInfo - 岗位信息
* @returns {object} 面试邀约内容
*/
generateDefaultInterviewInvitation(jobInfo) {
return {
success: true,
content: '感谢您的邀请,我很期待与您面谈。请问方便的时间是什么时候?',
jobTitle: jobInfo.jobTitle || '该岗位',
companyName: jobInfo.companyName || '贵公司'
};
}
/**
* 获取聊天列表
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数
* @returns {Promise<object>} 聊天列表
*/
async get_chat_list(sn_code, mqttClient, params = {}) {
const { platform = 'boss', pageCount = 3 } = params;
console.log(`[聊天管理] 开始获取设备 ${sn_code} 的聊天列表`);
// 通过MQTT指令获取聊天列表
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: "get_chat_list",
data: { pageCount }
});
if (!response || response.code !== 200) {
console.error(`[聊天管理] 获取聊天列表失败:`, response);
throw new Error('获取聊天列表失败');
}
console.log(`[聊天管理] 成功获取聊天列表`);
return response.data;
}
}
module.exports = new ChatManager();

View File

@@ -3,10 +3,7 @@
* 聚合所有 job 相关模块的方法,提供统一的对外接口
*/
const jobManager = require('./jobManager');
const resumeManager = require('./resumeManager');
const chatManager = require('./chatManager');
const { jobManager, resumeManager, chatManager } = require('./managers');
const pack = (instance) => {
const proto = Object.getPrototypeOf(instance);
@@ -23,7 +20,6 @@ const pack = (instance) => {
/**
* 便捷方法:直接导出常用方法
* 使用下划线命名规范
*/
module.exports = {
...pack(jobManager),

View File

@@ -1,651 +0,0 @@
# 职位过滤服务文档
## 概述
`job_filter_service.js` 是一个职位文本匹配过滤服务,使用简单的文本匹配规则来过滤和分析职位信息。该服务支持从数据库动态获取职位类型的技能关键词和排除关键词,能够分析职位与简历的匹配度,并提供过滤和评分功能。
## 主要功能
1. **职位类型配置管理**:从数据库获取或使用默认配置的技能关键词和排除关键词
2. **匹配度分析**:分析职位与简历的匹配度(技能、经验、薪资)
3. **职位过滤**:根据匹配分数、外包标识、排除关键词等条件过滤职位列表
4. **自定义权重评分**:支持根据自定义权重配置计算职位评分(距离、薪资、工作年限、学历、技能)
## 类结构
```javascript
class JobFilterService {
// 默认技能关键词
defaultCommonSkills: Array<string>
// 默认排除关键词
defaultExcludeKeywords: Array<string>
// 职位类型配置缓存
jobTypeCache: Map<number, Object>
}
```
## API 文档
### 1. getJobTypeConfig(jobTypeId)
根据职位类型ID获取技能关键词和排除关键词配置。
**参数:**
- `jobTypeId` (number, 可选): 职位类型ID
**返回值:**
```javascript
{
commonSkills: Array<string>, // 技能关键词列表
excludeKeywords: Array<string> // 排除关键词列表
}
```
**说明:**
- 如果未提供 `jobTypeId` 或配置获取失败,返回默认配置
- 配置结果会缓存5分钟避免频繁查询数据库
-`job_types` 表读取 `commonSkills``excludeKeywords` 字段JSON格式
**使用示例:**
```javascript
const config = await jobFilterService.getJobTypeConfig(1);
console.log(config.commonSkills); // ['Vue', 'React', ...]
console.log(config.excludeKeywords); // ['外包', '外派', ...]
```
---
### 2. clearCache(jobTypeId)
清除职位类型配置缓存。
**参数:**
- `jobTypeId` (number, 可选): 职位类型ID不传则清除所有缓存
**使用示例:**
```javascript
// 清除特定职位类型的缓存
jobFilterService.clearCache(1);
// 清除所有缓存
jobFilterService.clearCache();
```
---
### 3. analyzeJobMatch(jobInfo, resumeInfo, jobTypeId)
使用文本匹配分析职位与简历的匹配度。
**参数:**
- `jobInfo` (object, 必需): 职位信息对象
- `jobTitle` (string): 职位名称
- `companyName` (string): 公司名称
- `description` (string): 职位描述
- `skills` (string): 技能要求
- `requirements` (string): 职位要求
- `salary` (string): 薪资范围
- `experience` (string): 经验要求
- `education` (string): 学历要求
- `longitude` (number): 经度
- `latitude` (number): 纬度
- `resumeInfo` (object, 可选): 简历信息对象
- `skills` (string|Array): 技能列表
- `skillDescription` (string): 技能描述
- `currentPosition` (string): 当前职位
- `expectedPosition` (string): 期望职位
- `workYears` (number|string): 工作年限
- `expectedSalary` (string): 期望薪资
- `education` (string): 学历
- `jobTypeId` (number, 可选): 职位类型ID
**返回值:**
```javascript
{
skillMatch: number, // 技能匹配度0-100
experienceMatch: number, // 经验匹配度0-100
salaryMatch: number, // 薪资匹配度0-100
isOutsourcing: boolean, // 是否为外包岗位
overallScore: number, // 综合推荐指数0-100
matchReasons: Array<string>, // 匹配原因列表
concerns: Array<string>, // 关注点列表
suggestion: string, // 投递建议
analysis: Object // 完整的分析结果(同上)
}
```
**评分规则:**
1. **综合推荐指数计算**
- 技能匹配度 × 40% + 经验匹配度 × 30% + 薪资匹配度 × 30%
2. **技能匹配度**
- 从职位描述中提取技能关键词
- 计算简历中匹配的技能数量占比
- 无简历信息时,基于职位关键词数量评分
3. **经验匹配度**
- 从职位描述中提取经验要求应届、1年、2年、3年、5年、10年
- 根据简历工作年限与要求的匹配程度评分
4. **薪资匹配度**
- 解析职位薪资范围和期望薪资
- 期望薪资低于职位薪资时得高分,高于职位薪资时得低分
5. **外包检测**
- 检测职位描述中是否包含:外包、外派、驻场、人力外包、项目外包
**使用示例:**
```javascript
const jobInfo = {
jobTitle: '前端开发工程师',
description: '要求3年以上Vue开发经验熟悉React',
salary: '15-25K',
experience: '3-5年'
};
const resumeInfo = {
skills: 'Vue, React, JavaScript',
workYears: 4,
expectedSalary: '20K'
};
const result = await jobFilterService.analyzeJobMatch(jobInfo, resumeInfo, 1);
console.log(result.overallScore); // 85
console.log(result.matchReasons); // ['技能匹配度高', '工作经验符合要求', ...]
console.log(result.suggestion); // '强烈推荐投递:匹配度很高'
```
---
### 4. filterJobs(jobs, filterRules, resumeInfo, jobTypeId)
过滤职位列表,返回匹配的职位(带匹配分数)。
**参数:**
- `jobs` (Array, 必需): 职位列表(可以是 Sequelize 模型实例或普通对象)
- `filterRules` (object, 可选): 过滤规则
- `minScore` (number, 默认60): 最低匹配分数
- `excludeOutsourcing` (boolean, 默认true): 是否排除外包岗位
- `excludeKeywords` (Array<string>, 默认[]): 额外排除关键词列表
- `resumeInfo` (object, 可选): 简历信息
- `jobTypeId` (number, 可选): 职位类型ID
**返回值:**
```javascript
Array<{
...jobData, // 原始职位数据
matchScore: number, // 匹配分数
matchAnalysis: Object // 完整的匹配分析结果
}>
```
**过滤逻辑:**
1. 对每个职位进行匹配度分析
2. 过滤掉匹配分数低于 `minScore` 的职位
3. 如果 `excludeOutsourcing` 为 true过滤掉外包岗位
4. 过滤掉包含排除关键词的职位
5. 按匹配分数降序排序
**使用示例:**
```javascript
const jobs = [
{ jobTitle: '前端开发', description: 'Vue开发...', salary: '20K' },
{ jobTitle: '后端开发', description: 'Java开发...', salary: '25K' }
];
const resumeInfo = {
skills: 'Vue, JavaScript',
workYears: 3
};
const filterRules = {
minScore: 70,
excludeOutsourcing: true,
excludeKeywords: ['销售']
};
const filteredJobs = await jobFilterService.filterJobs(
jobs,
filterRules,
resumeInfo,
1
);
console.log(filteredJobs.length); // 过滤后的职位数量
console.log(filteredJobs[0].matchScore); // 第一个职位的匹配分数
```
---
### 5. calculateJobScoreWithWeights(jobData, resumeInfo, accountConfig, jobTypeConfig, priorityWeights)
根据自定义权重配置计算职位评分。
**参数:**
- `jobData` (object, 必需): 职位数据
- `longitude` (number): 经度
- `latitude` (number): 纬度
- `salary` (string): 薪资范围
- `experience` (string): 经验要求
- `education` (string): 学历要求
- `resumeInfo` (object, 必需): 简历信息
- `expectedSalary` (string): 期望薪资
- `workYears` (string|number): 工作年限
- `education` (string): 学历
- `skills` (string|Array): 技能列表
- `accountConfig` (object, 必需): 账号配置
- `user_longitude` (number): 用户经度
- `user_latitude` (number): 用户纬度
- `jobTypeConfig` (object, 可选): 职位类型配置(包含 commonSkills
- `priorityWeights` (Array, 必需): 权重配置
```javascript
[
{ key: 'distance', weight: 30 }, // 距离权重0-100
{ key: 'salary', weight: 40 }, // 薪资权重
{ key: 'work_years', weight: 20 }, // 工作年限权重
{ key: 'education', weight: 10 } // 学历权重
]
```
**返回值:**
```javascript
{
totalScore: number, // 总分0-100+,技能评分作为额外加分项)
scores: {
distance: number, // 距离评分
salary: number, // 薪资评分
work_years: number, // 工作年限评分
education: number, // 学历评分
skills: number // 技能评分(如果有 jobTypeConfig
}
}
```
**评分规则:**
1. **距离评分**
- 0-5km: 100分
- 5-10km: 90分
- 10-20km: 80分
- 20-50km: 60分
- 50km以上: 30分
2. **薪资评分**
- 职位薪资 ≥ 期望薪资: 100分
- 职位薪资 ≥ 期望薪资 × 0.8: 80分
- 职位薪资 ≥ 期望薪资 × 0.6: 60分
- 其他: 40分
3. **工作年限评分**
- 简历年限 ≥ 职位要求: 100分
- 简历年限 ≥ 职位要求 × 0.8: 80分
- 简历年限 ≥ 职位要求 × 0.6: 60分
- 其他: 40分
4. **学历评分**
- 简历学历 ≥ 职位要求: 100分
- 简历学历 = 职位要求 - 1级: 70分
- 其他: 40分
5. **技能评分**
- 计算简历技能与职位类型配置的技能关键词匹配度
- 作为额外加分项固定权重10%
**使用示例:**
```javascript
const jobData = {
longitude: 116.3974,
latitude: 39.9093,
salary: '20-30K',
experience: '3-5年',
education: '本科'
};
const resumeInfo = {
expectedSalary: '25K',
workYears: 4,
education: '本科',
skills: ['Vue', 'React', 'JavaScript']
};
const accountConfig = {
user_longitude: 116.4074,
user_latitude: 39.9042
};
const jobTypeConfig = {
commonSkills: ['Vue', 'React', 'JavaScript', 'Node.js']
};
const priorityWeights = [
{ key: 'distance', weight: 30 },
{ key: 'salary', weight: 40 },
{ key: 'work_years', weight: 20 },
{ key: 'education', weight: 10 }
];
const result = jobFilterService.calculateJobScoreWithWeights(
jobData,
resumeInfo,
accountConfig,
jobTypeConfig,
priorityWeights
);
console.log(result.totalScore); // 85
console.log(result.scores); // { distance: 100, salary: 90, work_years: 100, ... }
```
---
### 6. parseSalaryRange(salaryDesc)
解析薪资范围字符串。
**参数:**
- `salaryDesc` (string): 薪资描述字符串
- 支持格式:`15-25K`、`25K`、`5000-6000元/月`、`2-3万`、`20000-30000` 等
**返回值:**
```javascript
{
min: number, // 最低薪资(元)
max: number // 最高薪资(元)
}
```
或 `null`(解析失败时)
**使用示例:**
```javascript
const range1 = jobFilterService.parseSalaryRange('15-25K');
console.log(range1); // { min: 15000, max: 25000 }
const range2 = jobFilterService.parseSalaryRange('2-3万');
console.log(range2); // { min: 20000, max: 30000 }
```
---
### 7. parseExpectedSalary(expectedSalary)
解析期望薪资字符串,返回平均值。
**参数:**
- `expectedSalary` (string): 期望薪资描述字符串
**返回值:**
- `number`: 期望薪资数值(元),如果是范围则返回平均值
- `null`: 解析失败时
**使用示例:**
```javascript
const salary1 = jobFilterService.parseExpectedSalary('20-30K');
console.log(salary1); // 25000平均值
const salary2 = jobFilterService.parseExpectedSalary('25K');
console.log(salary2); // 25000
```
---
### 8. parseWorkYears(workYearsStr)
解析工作年限字符串为数字。
**参数:**
- `workYearsStr` (string): 工作年限字符串(如 "3年"、"5年以上"
**返回值:**
- `number`: 工作年限数字
- `null`: 解析失败时
**使用示例:**
```javascript
const years = jobFilterService.parseWorkYears('3-5年');
console.log(years); // 3提取第一个数字
```
---
## 辅助方法
### buildJobText(jobInfo)
构建职位文本(用于匹配)。
**参数:**
- `jobInfo` (object): 职位信息对象
**返回值:**
- `string`: 合并后的职位文本(小写)
---
### buildResumeText(resumeInfo)
构建简历文本(用于匹配)。
**参数:**
- `resumeInfo` (object): 简历信息对象
**返回值:**
- `string`: 合并后的简历文本(小写)
---
### calculateSkillMatch(jobText, resumeText, commonSkills)
计算技能匹配度0-100分
**参数:**
- `jobText` (string): 职位文本
- `resumeText` (string): 简历文本
- `commonSkills` (Array<string>): 技能关键词列表
**返回值:**
- `number`: 匹配度分数0-100
---
### calculateExperienceMatch(jobInfo, resumeInfo)
计算经验匹配度0-100分
**参数:**
- `jobInfo` (object): 职位信息
- `resumeInfo` (object): 简历信息
**返回值:**
- `number`: 匹配度分数0-100
---
### calculateSalaryMatch(jobInfo, resumeInfo)
计算薪资合理性0-100分
**参数:**
- `jobInfo` (object): 职位信息
- `resumeInfo` (object): 简历信息
**返回值:**
- `number`: 匹配度分数0-100
---
### checkOutsourcing(jobText)
检查是否为外包岗位。
**参数:**
- `jobText` (string): 职位文本
**返回值:**
- `boolean`: 是否为外包
---
## 默认配置
### 默认技能关键词
```javascript
[
'Vue', 'React', 'Angular', 'JavaScript', 'TypeScript', 'Node.js',
'Python', 'Java', 'C#', '.NET', 'Flutter', 'React Native',
'Webpack', 'Vite', 'Redux', 'MobX', 'Express', 'Koa',
'Django', 'Flask', 'MySQL', 'MongoDB', 'Redis',
'WebRTC', 'FFmpeg', 'Canvas', 'WebSocket', 'HTML5', 'CSS3',
'jQuery', 'Bootstrap', 'Element UI', 'Ant Design',
'Git', 'Docker', 'Kubernetes', 'AWS', 'Azure',
'Selenium', 'Jest', 'Mocha', 'Cypress'
]
```
### 默认排除关键词
```javascript
[
'外包', '外派', '驻场', '销售', '客服', '电话销售',
'地推', '推广', '市场', '运营', '行政', '文员'
]
```
## 数据库依赖
该服务依赖以下数据库表:
### job_types 表
存储职位类型配置,需要包含以下字段:
- `id` (number): 职位类型ID
- `is_enabled` (number): 是否启用1=启用0=禁用)
- `commonSkills` (string|JSON): 技能关键词列表JSON数组字符串
- `excludeKeywords` (string|JSON): 排除关键词列表JSON数组字符串
**示例数据:**
```json
{
"id": 1,
"is_enabled": 1,
"commonSkills": "[\"Vue\", \"React\", \"JavaScript\"]",
"excludeKeywords": "[\"外包\", \"外派\"]"
}
```
## 使用示例
### 完整使用流程
```javascript
const jobFilterService = require('./job_filter_service');
// 1. 分析单个职位的匹配度
const jobInfo = {
jobTitle: '高级前端开发工程师',
companyName: 'XX科技有限公司',
description: '负责前端架构设计要求5年以上Vue/React开发经验',
skills: 'Vue, React, TypeScript, Node.js',
requirements: '本科及以上学历,有大型项目经验',
salary: '25-40K·14薪'
};
const resumeInfo = {
skills: 'Vue, React, JavaScript, TypeScript, Node.js, Webpack',
skillDescription: '精通Vue生态熟悉React有5年+前端开发经验',
currentPosition: '高级前端开发工程师',
expectedPosition: '前端架构师',
workYears: 6,
expectedSalary: '30K',
education: '本科'
};
const analysis = await jobFilterService.analyzeJobMatch(
jobInfo,
resumeInfo,
1 // 职位类型ID
);
console.log('匹配分析结果:');
console.log('综合评分:', analysis.overallScore);
console.log('技能匹配度:', analysis.skillMatch);
console.log('经验匹配度:', analysis.experienceMatch);
console.log('薪资匹配度:', analysis.salaryMatch);
console.log('是否外包:', analysis.isOutsourcing);
console.log('匹配原因:', analysis.matchReasons);
console.log('关注点:', analysis.concerns);
console.log('投递建议:', analysis.suggestion);
// 2. 过滤职位列表
const jobs = await Job.findAll({ where: { status: 1 } });
const filteredJobs = await jobFilterService.filterJobs(
jobs,
{
minScore: 70,
excludeOutsourcing: true,
excludeKeywords: ['销售', '客服']
},
resumeInfo,
1
);
console.log(`共找到 ${filteredJobs.length} 个匹配的职位`);
filteredJobs.forEach((job, index) => {
console.log(`${index + 1}. ${job.jobTitle} - 匹配分数:${job.matchScore}`);
});
// 3. 自定义权重评分
const accountConfig = {
user_longitude: 116.4074,
user_latitude: 39.9042
};
const priorityWeights = [
{ key: 'distance', weight: 25 },
{ key: 'salary', weight: 35 },
{ key: 'work_years', weight: 25 },
{ key: 'education', weight: 15 }
];
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
jobInfo,
resumeInfo,
accountConfig,
{ commonSkills: ['Vue', 'React', 'TypeScript'] },
priorityWeights
);
console.log('自定义权重评分:', scoreResult.totalScore);
console.log('各项评分:', scoreResult.scores);
```
## 注意事项
1. **缓存机制**职位类型配置会缓存5分钟修改数据库配置后需要调用 `clearCache()` 清除缓存
2. **性能优化**
- 大量职位过滤时,建议分批处理
- 避免在循环中频繁调用 `getJobTypeConfig()`,配置会被自动缓存
3. **文本匹配**
- 所有文本匹配均为小写匹配,不区分大小写
- 匹配逻辑较为简单,如需更精确的匹配,建议使用 AI 分析(二期规划)
4. **薪资解析**
- 支持多种薪资格式,但可能无法解析所有格式
- 解析失败时返回默认分数或 null
5. **错误处理**
- 所有方法都包含错误处理,失败时返回默认值或空结果
- 建议在生产环境中监控日志输出
## 版本历史
- **v1.0.0**: 初始版本,支持基础文本匹配和过滤功能
- 支持从数据库动态获取职位类型配置
- 支持自定义权重评分计算

View File

@@ -0,0 +1,406 @@
const ai_service = require('../../../services/ai_service');
const db = require('../../dbProxy');
/**
* 聊天管理模块
* 负责沟通列表、沟通详情、发送消息等与设备端的 MQTT 指令对接
*/
class ChatManager {
/**
* 解析沟通列表返回值,统一为 { friendList, foldText, ... }
* 只支持新的结构:
* response.data = { success, apiData: [ { response: { code, zpData:{...} } } ] }
* @private
*/
_parse_chat_list_response(response) {
const outerData = response && response.data;
if (!outerData || !Array.isArray(outerData.apiData) || outerData.apiData.length === 0) {
return { friendList: [], foldText: '', filterEncryptIdList: [], filterBossIdList: [] };
}
const firstApi = outerData.apiData[0] || {};
const innerResp = firstApi.response || firstApi.data || null;
const raw = innerResp && (innerResp.zpData != null ? innerResp.zpData : innerResp.data);
if (!raw) {
return { friendList: [], foldText: '', filterEncryptIdList: [], filterBossIdList: [] };
}
return {
friendList: Array.isArray(raw.friendList) ? raw.friendList : [],
foldText: raw.foldText || '',
filterEncryptIdList: Array.isArray(raw.filterEncryptIdList) ? raw.filterEncryptIdList : [],
filterBossIdList: Array.isArray(raw.filterBossIdList) ? raw.filterBossIdList : []
};
}
/**
* 获取聊天列表
* 返回值结构: { friendList, foldText, filterEncryptIdList, filterBossIdList }
* friendList 每项: friendId, encryptFriendId, name, updateTime, brandName, jobName, jobCity, positionName, bossTitle 等
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数
* @returns {Promise<object>} { friendList, foldText, filterEncryptIdList, filterBossIdList }
*/
async get_chat_list(sn_code, mqttClient, params = {}) {
const { platform = 'boss', pageCount = 3 } = params;
console.log(`[聊天管理] 开始获取设备 ${sn_code} 的聊天列表`);
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: 'get_chat_list',
data: { pageCount }
});
// 只认新结构data.success === true
const ok = !!response && response.data && response.data.success === true;
if (!ok) {
console.error(`[聊天管理] 获取聊天列表失败:`, response);
throw new Error(response?.message || '获取聊天列表失败');
}
const parsed = this._parse_chat_list_response(response);
// 存储数据库
console.log(`[聊天管理] 成功获取聊天列表,共 ${parsed.friendList.length} 个联系人`);
return parsed;
}
/**
* 解析 get_chat_detail 设备端返回格式
* 格式: { type, code, message, data: { success, apiData: { response: { zpData } }, getBossData: { response: { zpData } } } }
* apiData.response.zpData = 消息列表 hasMore/messages/type/minMsgId
* getBossData.response.zpData = 会话 data+job
* @private
*/
_parse_chat_detail_response(response) {
if (!response) return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] };
const d = response.data;
const api_data = d && d.apiData;
const get_boss_data = d && d.getBossData;
const msg_zp = api_data && api_data.response && api_data.response.zpData;
const boss_zp = get_boss_data && get_boss_data.response && get_boss_data.response.zpData;
if (msg_zp && Array.isArray(msg_zp.messages)) {
return {
variant: 'messages',
hasMore: !!msg_zp.hasMore,
messages: msg_zp.messages,
type: msg_zp.type,
minMsgId: msg_zp.minMsgId,
data: (boss_zp && boss_zp.data) || null,
job: (boss_zp && boss_zp.job) || null
};
}
if (boss_zp && (boss_zp.data != null || boss_zp.job != null)) {
return {
variant: 'session',
data: boss_zp.data || null,
job: boss_zp.job || null,
hasMore: false,
messages: [],
type: null,
minMsgId: null
};
}
return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] };
}
/**
* 解析详情,统一返回 { variant, hasMore, minMsgId, messages, data, job }
* 入参可为设备端完整返回data.apiData/data.getBossData或已解析对象直接返回
*/
parseDetailResponse(apiResponse) {
if (apiResponse && (apiResponse.variant === 'messages' || apiResponse.variant === 'session' || apiResponse.variant === 'unknown')) {
return apiResponse;
}
return this._parse_chat_detail_response(apiResponse);
}
/**
* 获取沟通详情(会话信息或聊天消息列表)
* 返回值: { variant: 'session'|'messages', ... }
* - session: data(boss/会话信息), job(职位信息)
* - messages: hasMore, messages[], type, minMsgId
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数,如 friendId/encryptBossId/encryptJobId 等,由设备端约定
* @returns {Promise<object>}
*/
async get_chat_detail(sn_code, mqttClient, params = {}) {
const { platform = 'boss', ...rest } = params;
const friendId = rest.friendId != null ? Number(rest.friendId) : NaN;
if (!Number.isFinite(friendId) || friendId <= 0) {
throw new Error('缺少必要参数friendId需为有效正数');
}
console.log(`[聊天管理] 开始获取设备 ${sn_code} 的沟通详情`, { friendId });
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: 'get_chat_detail',
data: { ...rest, friendId }
});
const ok = response && (response.code === 200 || response.code === 0);
if (!ok) {
console.error(`[聊天管理] 获取沟通详情失败:`, response);
throw new Error(response?.message || '获取沟通详情失败');
}
const parsed = this._parse_chat_detail_response(response);
const logExtra = parsed.variant === 'session'
? '会话'
: parsed.variant === 'messages'
? `消息 ${parsed.messages.length}`
: '未知';
console.log(`[聊天管理] 成功获取沟通详情 (${logExtra})`);
return parsed;
}
/**
* 发送聊天消息(支持多条 + 文本/发简历/换电话/换微信)
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - friendId(必填), messages(数组), chatType, use_real_type, platform
* @param {string} params.friendId - 好友ID用于打开该好友的聊天面板
* @param {Array} params.messages - 每项为 string 或 { type: 'text'|'send_resume'|'exchange_phone'|'exchange_wechat', content?: string }
* @param {boolean} params.use_real_type - 是否模拟真实打字,默认 false
* @returns {Promise<object>} 发送结果
*/
async send_chat_message(sn_code, mqttClient, params) {
const { friendId, messages, chatType, use_real_type = false, platform = 'boss' } = params || {};
if (!friendId) throw new Error('friendId 不能为空');
if (!Array.isArray(messages) || messages.length === 0) throw new Error('messages 必须是非空数组');
const normalized_messages = messages.map((item) => {
if (typeof item === 'string') return { type: 'text', content: item };
return { type: item.type || 'text', content: item.content || '' };
});
console.log(`[聊天管理] 设备 ${sn_code} 发送聊天消息friendId=${friendId},条数=${normalized_messages.length}`);
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: 'send_chat_message',
data: { friendId, messages: normalized_messages, chatType, use_real_type: !!use_real_type }
});
if (!response || (response.code !== 0 && response.code !== 200)) {
console.error(`[聊天管理] 聊天消息发送失败:`, response);
throw new Error(response?.message || '聊天消息发送失败');
}
console.log(`[聊天管理] 聊天消息发送成功`);
return response;
}
/** 是否为系统/模板消息竞争者PK、拒绝模板、系统卡片等不参与回复判断 */
_isSystemMessage(msg) {
const body = msg.body || {};
if (msg.bizType === 317 || msg.bizType === 21050003) return true;
if (msg.type === 4) return true;
if (body.type === 16) return true;
return false;
}
/** 统一 uid 为可比较的字符串(支持 number 或 { low, high } */
_normalizeUid(uid) {
if (uid == null) return null;
if (typeof uid === 'number' || typeof uid === 'string') return String(uid);
if (typeof uid === 'object' && typeof uid.low === 'number') return String(uid.low);
return null;
}
/** 过滤出 HR 发的、非系统、可回复的消息列表(已排除自己发的) */
_filterHrReplyableMessages(messages, geek_uid) {
if (!geek_uid || !Array.isArray(messages)) return [];
const geekStr = this._normalizeUid(geek_uid);
if (!geekStr) return [];
const list = messages.filter(msg => {
if (!msg.from) return false;
const fromStr = this._normalizeUid(msg.from.uid);
if (fromStr === geekStr) return false; // 自己发的,排除
if (this._isSystemMessage(msg)) return false;
return true;
});
return list;
}
/** AI 回复后写入 chat_reply_intent_logoptions 含 sn_code/platform/friendId/encryptFriendIdsecurityId 为 HR 消息唯一 id */
_saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, replied, reason, securityId) {
if (!options || options.sn_code == null) return;
try {
const model = db.getModel('chat_reply_intent_log');
model.create({
sn_code: options.sn_code || '',
platform: options.platform || 'boss',
friendId: options.friendId ?? null,
encrypt_friend_id: options.encryptFriendId || '',
security_id: securityId || null,
hr_message_text: hr_message_text || null,
action: action || '',
reply_content: reply_content || null,
replied: !!replied,
reason: reason || null,
job_name: (jobInfo && (jobInfo.jobName || jobInfo.title)) || null,
create_time: new Date()
}).catch(e => console.warn('[聊天管理] 写入 chat_reply_intent_log 失败:', e.message));
} catch (e) {
console.warn('[聊天管理] 写入 chat_reply_intent_log 失败:', e.message);
}
}
/**
* 根据沟通详情判断是否需回复,并用 AI 判断意图(文字回复 / 发简历)及生成内容
* @param {object} detail - 沟通详情,含 variant、messages、job 等
* @param {object} options - 可选,落库用 { sn_code, platform, friendId, encryptFriendId }
* @returns {Promise<object>} { replied, action?, reply_content?, hr_message_text?, reason? }
*/
async getReplyContentFromDetail(detail, options) {
if (!detail || detail.variant !== 'messages' || !Array.isArray(detail.messages) || detail.messages.length === 0) {
return { replied: false, reason: '无可用消息' };
}
const messages = detail.messages;
let hr_uid = null;
let geek_uid = null;
for (const msg of messages) {
const body = msg.body || {};
const jobDesc = body.jobDesc || body.job_desc || null;
if (jobDesc) {
if (jobDesc.boss && jobDesc.boss.uid && !hr_uid) hr_uid = jobDesc.boss.uid;
if (jobDesc.geek && jobDesc.geek.uid && !geek_uid) geek_uid = jobDesc.geek.uid;
}
if (hr_uid && geek_uid) break;
}
const lastRaw = messages[messages.length - 1];
if (lastRaw && lastRaw.from && lastRaw.to) {
hr_uid = hr_uid || lastRaw.from.uid;
geek_uid = geek_uid || lastRaw.to.uid;
}
const jobInfo = detail.job || {};
// 用 messages 判定整条对话最后一条是否来自 HR只有最后一条是 HR 才需要回复
const last_in_messages = messages[messages.length - 1];
const last_from_uid = last_in_messages && last_in_messages.from ? this._normalizeUid(last_in_messages.from.uid) : null;
const hr_uid_str = this._normalizeUid(hr_uid);
const is_last_from_hr = hr_uid_str && last_from_uid === hr_uid_str && !this._isSystemMessage(last_in_messages);
if (!is_last_from_hr) {
this._saveReplyIntentLog(options, '', jobInfo, '', '', false, '最后一条消息不是HR发的', null);
return { replied: false, reason: '最后一条消息不是HR发的' };
}
const hrList = this._filterHrReplyableMessages(messages, geek_uid);
if (hrList.length === 0) {
this._saveReplyIntentLog(options, '', jobInfo, '', '', false, '无HR可回复消息已过滤系统与己方', null);
return { replied: false, reason: '无HR可回复消息已过滤系统与己方' };
}
const last = hrList[hrList.length - 1];
const body = last.body || {};
const hr_message_text =
(typeof body.text === 'string' && body.text) ||
(typeof last.pushText === 'string' && last.pushText) ||
'';
const security_id = last.securityId || last.security_id || '';
if (security_id && options) {
try {
const logModel = db.getModel('chat_reply_intent_log');
const existing = await logModel.findOne({ where: { security_id } });
if (existing) {
// 已回复过的 HR 消息:不再重复发,避免每次扫描都发一条
if (existing.replied) {
return { replied: false, reason: '该条HR消息已回复过跳过' };
}
// 之前记录为不回复:直接沿用,不再调 AI
return {
replied: false,
reason: existing.reason || '已记录为不回复'
};
}
} catch (e) {
console.warn('[聊天管理] 查询 chat_reply_intent_log 失败:', e.message);
}
}
const { action, reply_content } = await ai_service.replyIntentAndContent({
jobInfo,
hrMessage: hr_message_text,
previousMessages: hrList.slice(-5).map(m => (m.body && m.body.text) || m.pushText || '')
});
if (action === 'no_reply') {
this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, false, 'HR表示暂不匹配/无需回复', security_id || null);
return { replied: false, reason: 'HR表示暂不匹配/无需回复' };
}
const needContent = action === 'text';
if (needContent && (!reply_content || !reply_content.trim())) {
this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, false, 'AI 未生成有效回复文案', security_id || null);
return { replied: false, reason: 'AI 未生成有效回复文案' };
}
this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, true, null, security_id || null);
return {
replied: true,
action: action || 'text',
reply_content: reply_content || '',
hr_message_text
};
}
/**
* 使用 AI 自动决定是否回复,并发送回复(内部会先获取详情,再调用 getReplyContentFromDetail再发送
* 单条指令场景用;任务 auto_chat 已改为下发 get_chat_list / get_chat_detail / send_chat_message 多条指令。
*
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 包含 friendId + 获取详情所需参数
* @returns {Promise<object>} { replied, reply_content?, hr_message_text?, reason? }
*/
async auto_reply_with_ai(sn_code, mqttClient, params = {}) {
const { friendId, platform = 'boss', ...detailParams } = params;
if (!friendId) throw new Error('friendId 不能为空');
const parsed = await this.get_chat_detail(sn_code, mqttClient, { platform, ...detailParams });
const decision = await this.getReplyContentFromDetail(parsed, {
sn_code,
platform,
friendId,
encryptFriendId: detailParams.encryptFriendId || ''
});
if (!decision.replied) return decision;
const action = decision.action || 'text';
const content = decision.reply_content || '';
const actionMessages = {
send_resume: [{ type: 'send_resume', content }],
exchange_wechat: [{ type: 'exchange_wechat', content }],
exchange_phone: [{ type: 'exchange_phone', content }]
};
const messages = actionMessages[action] || [{ type: 'text', content }];
await this.send_chat_message(sn_code, mqttClient, {
friendId,
messages,
chatType: 'reply',
platform
});
return {
replied: true,
reply_content: decision.reply_content,
hr_message_text: decision.hr_message_text
};
}
}
module.exports = new ChatManager();

View File

@@ -0,0 +1,13 @@
/**
* Managers 模块统一导出
*/
const jobManager = require('./jobManager');
const resumeManager = require('./resumeManager');
const chatManager = require('./chatManager');
module.exports = {
jobManager,
resumeManager,
chatManager
};

View File

@@ -1,10 +1,12 @@
// const aiService = require('./aiService'); // 二期规划AI 服务暂时禁用
const jobFilterService = require('./job_filter_service'); // 使用文本匹配过滤服务
const locationService = require('../../services/locationService'); // 位置服务
const logs = require('../logProxy');
const db = require('../dbProxy');
const { Op } = require('sequelize');
const aiService = require('../../../services/ai_service');
const { jobFilterService } = require('../services');
const locationService = require('../../../services/locationService');
const logs = require('../../logProxy');
const db = require('../../dbProxy');
const { v4: uuidv4 } = require('uuid');
/**
* 工作管理模块
* 负责简历获取分析存储和匹配度计算
@@ -13,6 +15,25 @@ class JobManager {
constructor() {
}
/**
* 解析设备 get_job_list / search_job_list 等返回的 response.data
* 实际形态为多页 XHR 监听结果数组与客户端一致例如
* `[{ url, method, status, data: { code: 0, message, zpData: { hasMore, jobList: [...] } } }, ...]`
* pageCount=3 时通常 3 每页一次 list.json本方法将所有页的 jobList 合并为一维数组
*/
_jobListFromRecommendMonitorData(responseData) {
if (!Array.isArray(responseData)) return [];
const jobs = [];
for (const item of responseData) {
const inner = item?.data;
if (!inner || typeof inner !== 'object') continue;
if (inner.code !== undefined && inner.code !== 0) continue;
const list = inner.zpData?.jobList;
if (Array.isArray(list)) jobs.push(...list);
}
return jobs;
}
// 启动客户端那个平台 用户信息,心跳机制
async set_user_info(sn_code, mqttClient, user_info) {
const response = await mqttClient.publishAndWait(sn_code, {
@@ -144,21 +165,320 @@ 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('多条件搜索职位失败');
}
const jobs = this._jobListFromRecommendMonitorData(response.data);
console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`);
// 保存职位到数据库
await this.saveJobsToDatabase(sn_code, platform, keyword, jobs);
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
});
// 未通过规则/评分的待投递记录标记为 filtered避免长期 pending
const passedIds = new Set(matchedJobs.map((j) => j.id).filter((id) => id != null));
const notPassedIds = searchedJobs
.map((row) => (row.toJSON ? row.toJSON() : row))
.map((j) => j.id)
.filter((id) => id != null && !passedIds.has(id));
if (notPassedIds.length > 0) {
try {
await job_postings.update(
{ applyStatus: 'filtered' },
{
where: {
id: { [Op.in]: notPassedIds },
sn_code,
platform,
applyStatus: 'pending'
}
}
);
console.log(`[工作管理] 搜索并投递:不符合条件已标记 filtered ${notPassedIds.length}`);
} catch (e) {
console.warn('[工作管理] 标记 filtered 失败:', e.message);
}
}
// 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}`
};
}
/**
* 获取期望 tab 列表投递用标签 推荐前端开发工程师
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数 { platform }
* @returns {Promise<Array<{ index: number, text: string }>>} tab 列表
*/
async get_job_listings(sn_code, mqttClient, params = {}) {
const { platform = 'boss' } = params;
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: 'get_job_listings',
data: {}
});
if (!response || response.code !== 200) {
console.error(`[工作管理] 获取 job_listings 失败:`, response);
throw new Error(response?.message || '获取 job_listings 失败');
}
const list = Array.isArray(response.data) ? response.data : [];
console.log(`[工作管理] 获取 job_listings 成功,共 ${list.length} 个 tab`);
try {
const jobTypeAiSyncService = require('../../../services/job_type_ai_sync_service');
await jobTypeAiSyncService.maybeSyncAfterListings(sn_code, list, platform);
} catch (syncErr) {
console.warn('[工作管理] job_types AI 同步失败:', syncErr.message);
}
return list;
}
/**
* 获取岗位列表与客户端/Electron 约定一致
* @param {string} sn_code - 设备 SN
* @param {object} mqttClient - MQTT 客户端
* @param {object} params - { platform?, pageCount?, tabLabel?, keyword? }keyword 仅服务端入库用下发设备只有 pageCount + tabLabel
* @returns {Promise<object>} 岗位列表
*/
async get_job_list(sn_code, mqttClient, params = {}) {
const { keyword = '前端', platform = 'boss', pageCount = 3 } = params;
console.log(`[工作管理] 开始获取设备 ${sn_code} 的岗位列表,关键词: ${keyword}`);
const { platform = 'boss', pageCount = 3, tabLabel } = params;
const keyword = String(params.keyword || '').trim() || String(tabLabel || '').trim();
const data = {
pageCount,
...(String(tabLabel || '').trim() ? { tabLabel: String(tabLabel).trim() } : {})
};
console.log(`[工作管理] get_job_list ${sn_code} → 设备`, data, `入库 keyword=${keyword}`);
// 通过MQTT指令获取岗位列表
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: "get_job_list",
data: { keyword, pageCount }
action: 'get_job_list',
data
});
if (!response || response.code !== 200) {
@@ -166,43 +486,19 @@ class JobManager {
throw new Error('获取岗位列表失败');
}
// 处理职位列表数据response.data 可能是数组(职位列表.json 格式)或单个对象
let jobs = [];
if (Array.isArray(response.data)) {
// 如果是数组格式(职位列表.json遍历每个元素提取岗位数据
for (const item of response.data) {
if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) {
jobs = jobs.concat(item.data.zpData.jobList);
}
}
console.log(`[工作管理] 从 ${response.data.length} 个响应中提取岗位数据`);
} else if (response.data?.data?.zpData?.jobList) {
// 如果是单个对象格式,从 data.zpData.jobList 获取
jobs = response.data.data.zpData.jobList || [];
} else if (response.data?.zpData?.jobList) {
// 兼容旧格式:直接从 zpData.jobList 获取
jobs = response.data.zpData.jobList || [];
}
const jobs = this._jobListFromRecommendMonitorData(response.data);
console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`);
// 保存职位到数据库
try {
await this.saveJobsToDatabase(sn_code, platform, keyword, jobs);
} catch (error) {
console.error(`[工作管理] 保存职位到数据库失败:`, error);
// 不影响主流程,继续返回数据
}
const result = {
jobs: jobs,
keyword: keyword,
platform: platform,
await this.saveJobsToDatabase(sn_code, platform, keyword, jobs);
return {
jobs,
keyword,
platform,
count: jobs.length
};
return result;
}
/**
@@ -218,89 +514,93 @@ class JobManager {
console.log(`[工作管理] 开始保存 ${jobs.length} 个职位到数据库`);
for (const job of jobs) {
try {
// 构建职位信息对象
const jobInfo = {
sn_code,
platform,
keyword,
// Boss直聘字段映射
encryptBossId: job.encryptBossId || '',
jobId: job.encryptJobId || '',
jobTitle: job.jobName || '',
companyId: job.encryptBrandId || '',
companyName: job.brandName || '',
companySize: job.brandScaleName || '',
companyIndustry: job.brandIndustry || '',
salary: job.salaryDesc || '',
// 构建职位信息对象
const jobInfo = {
sn_code,
platform,
keyword,
// 岗位要求(从 jobLabels 和 skills 提取)
jobRequirements: JSON.stringify({
experience: job.jobExperience || '',
education: job.jobDegree || '',
labels: job.jobLabels || [],
skills: job.skills || []
}),
// 工作地点
location: [job.cityName, job.areaDistrict, job.businessDistrict]
.filter(Boolean).join(' '),
// Boss直聘字段映射
encryptBossId: job.encryptBossId || '',
jobId: job.encryptJobId || '',
jobTitle: job.jobName || '',
companyId: job.encryptBrandId || '',
companyName: job.brandName || '',
companySize: job.brandScaleName || '',
companyIndustry: job.brandIndustry || '',
salary: job.salaryDesc || '',
// 岗位要求(从 jobLabels 和 skills 提取)
jobRequirements: JSON.stringify({
experience: job.jobExperience || '',
education: job.jobDegree || '',
labels: job.jobLabels || [],
skills: job.skills || []
}),
// 原始数据
originalData: JSON.stringify(job),
// 工作地点
location: [job.cityName, job.areaDistrict, job.businessDistrict]
.filter(Boolean).join(' '),
// 默认状态
applyStatus: 'pending',
chatStatus: 'none'
};
experience: job.jobExperience || '',
education: job.jobDegree || '',
// 调用位置服务解析 location + companyName 获取坐标
if (jobInfo.location && jobInfo.companyName) {
const addressToParse = `${jobInfo.location.replaceAll(' ', '')}${jobInfo.companyName}`;
// 原始数据
originalData: JSON.stringify(job),
// 默认状态
applyStatus: 'pending',
chatStatus: 'none'
};
// 调用位置服务解析 location + companyName 获取坐标
if (jobInfo.location && jobInfo.companyName) {
const addressToParse = `${jobInfo.location.replaceAll(' ', '')}${jobInfo.companyName}`;
// 等待 1秒
await new Promise(resolve => setTimeout(resolve, 1000));
// 等待 1秒
// await new Promise(resolve => setTimeout(resolve, 1000));
// const location = await locationService.getLocationByAddress(addressToParse).catch(error => {
// console.error(`[工作管理] 获取位置失败:`, error);
// });
const location = await locationService.getLocationByAddress(addressToParse).catch(error => {
console.error(`[工作管理] 获取位置失败:`, error);
});
// if (location) {
// jobInfo.latitude = String(location.lat);
// jobInfo.longitude = String(location.lng);
// }
}
if (location) {
jobInfo.latitude = String(location.lat);
jobInfo.longitude = String(location.lng);
// 创建新职位 重复投递时间 从 pla_account 中获取pla_account 列为 platform_type不是 platform
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
platform_type: platform
}
});
let repeatDeliverDays = 30;
if (account) {
let dc = account.deliver_config?.repeat_deliver_days||30;
repeatDeliverDays = Number(dc);
}
const existingJob = await job_postings.findOne({
where: {
jobId: jobInfo.jobId,
sn_code: sn_code,
last_modify_time: {
[Op.gte]: new Date(Date.now() - repeatDeliverDays * 24 * 60 * 60 * 1000)
}
}
});
// 检查是否已存在(根据 jobId 和 sn_code
const existingJob = await job_postings.findOne({
where: {
jobId: jobInfo.jobId,
sn_code: sn_code
}
});
if (existingJob) {
// 更新现有职位
await job_postings.update(jobInfo, {
where: {
jobId: jobInfo.jobId,
sn_code: sn_code
}
});
console.log(`[工作管理] 职位已更新 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`);
} else {
// 创建新职位
await job_postings.create(jobInfo);
console.log(`[工作管理] 职位已创建 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`);
}
} catch (error) {
console.error(`[工作管理] 保存职位失败:`, error, job);
// 继续处理下一个职位
if (existingJob) {
await job_postings.update(jobInfo, { where: { id: existingJob.id } });
} else {
await job_postings.create(jobInfo);
console.log(`[工作管理] 职位已创建 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`);
}
}
@@ -320,10 +620,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');
@@ -365,9 +666,9 @@ class JobManager {
};
}
// 检查该公司是否在一个月内已投递过(避免连续投递同一公司
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
// 检查该公司是否在 30 天内已投递过(超过 30 天可再投该公司其它岗位
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const Sequelize = require('sequelize');
const recentCompanyApply = await apply_records.findOne({
@@ -375,7 +676,7 @@ class JobManager {
sn_code: sn_code,
companyName: jobData.companyName,
applyTime: {
[Sequelize.Op.gte]: oneMonthAgo
[Sequelize.Op.gte]: thirtyDaysAgo
}
},
order: [['applyTime', 'DESC']]
@@ -383,28 +684,28 @@ class JobManager {
if (recentCompanyApply) {
const daysAgo = Math.floor((new Date() - new Date(recentCompanyApply.applyTime)) / (1000 * 60 * 60 * 24));
console.log(`[工作管理] 跳过一个月内已投递的公司: ${jobData.companyName} (${daysAgo}天前投递过)`);
console.log(`[工作管理] 跳过30天内已投递的公司: ${jobData.companyName} (${daysAgo}天前投递过)`);
return {
success: false,
deliveredCount: 0,
failedCount: 1,
message: `该公司在${daysAgo}天前已投递过,一个月内不重复投递`,
message: `该公司在${daysAgo}天前已投递过,30天内不重复投递`,
deliveredJobs: [],
failedJobs: [{
jobId: jobData.jobId,
jobTitle: jobData.jobTitle,
companyName: jobData.companyName,
error: `该公司在${daysAgo}天前已投递过,一个月内不重复投递`
error: `该公司在${daysAgo}天前已投递过,30天内不重复投递`
}]
};
}
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 || '',

View File

@@ -1,7 +1,7 @@
const aiService = require('./aiService');
const jobFilterService = require('./job_filter_service');
const logs = require('../logProxy');
const db = require('../dbProxy');
const aiService = require('../../../services/ai_service');
const { jobFilterService } = require('../services');
const logs = require('../../logProxy');
const db = require('../../dbProxy');
const { v4: uuidv4 } = require('uuid');
/**

View File

@@ -0,0 +1,9 @@
/**
* Services 模块统一导出
*/
const jobFilterService = require('./jobFilterService');
module.exports = {
jobFilterService
};

View File

@@ -4,8 +4,8 @@
* 支持从数据库动态获取职位类型的技能关键词和排除关键词
*/
const db = require('../dbProxy.js');
const locationService = require('../../services/locationService');
const db = require('../../dbProxy.js');
const locationService = require('../../../services/locationService');
class JobFilterService {
constructor() {
@@ -32,15 +32,16 @@ class JobFilterService {
}
/**
* 根据职位类型ID获取技能关键词排除关键词
* 根据职位类型ID获取技能关键词排除关键词标题须含词
* @param {number} jobTypeId - 职位类型ID
* @returns {Promise<{commonSkills: Array, excludeKeywords: Array}>}
* @returns {Promise<{commonSkills: Array, excludeKeywords: Array, titleIncludeKeywords: Array}>}
*/
async getJobTypeConfig(jobTypeId) {
if (!jobTypeId) {
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
excludeKeywords: this.defaultExcludeKeywords,
titleIncludeKeywords: []
};
}
@@ -55,7 +56,8 @@ class JobFilterService {
console.warn('[职位过滤服务] job_types 模型不存在,使用默认配置');
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
excludeKeywords: this.defaultExcludeKeywords,
titleIncludeKeywords: []
};
}
@@ -67,7 +69,8 @@ class JobFilterService {
console.warn(`[职位过滤服务] 职位类型 ${jobTypeId} 不存在或已禁用,使用默认配置`);
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
excludeKeywords: this.defaultExcludeKeywords,
titleIncludeKeywords: []
};
}
@@ -76,6 +79,7 @@ class JobFilterService {
// 解析 JSON 字段
let commonSkills = this.defaultCommonSkills;
let excludeKeywords = this.defaultExcludeKeywords;
let titleIncludeKeywords = [];
if (jobTypeData.commonSkills) {
try {
@@ -103,9 +107,23 @@ class JobFilterService {
}
}
if (jobTypeData.titleIncludeKeywords) {
try {
const parsed = typeof jobTypeData.titleIncludeKeywords === 'string'
? JSON.parse(jobTypeData.titleIncludeKeywords)
: jobTypeData.titleIncludeKeywords;
if (Array.isArray(parsed)) {
titleIncludeKeywords = parsed.map((k) => String(k || '').trim()).filter(Boolean);
}
} catch (e) {
console.warn(`[职位过滤服务] 解析 titleIncludeKeywords 失败:`, e);
}
}
const config = {
commonSkills,
excludeKeywords
excludeKeywords,
titleIncludeKeywords
};
// 缓存配置缓存5分钟
@@ -119,7 +137,8 @@ class JobFilterService {
console.error(`[职位过滤服务] 获取职位类型配置失败:`, error);
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
excludeKeywords: this.defaultExcludeKeywords,
titleIncludeKeywords: []
};
}
}
@@ -655,71 +674,6 @@ class JobFilterService {
return null;
}
/**
* 过滤职位列表基于文本匹配
* @param {Array} jobs - 职位列表
* @param {object} filterRules - 过滤规则
* @param {object} resumeInfo - 简历信息可选
* @param {number} jobTypeId - 职位类型ID可选
* @param {object} options - 选项
* @param {boolean} options.autoSave - 是否自动保存评分结果到数据库默认false
* @returns {Promise<Array>} 过滤后的职位列表带匹配分数
*/
async filterJobs(jobs, filterRules = {}, resumeInfo = {}, jobTypeId = null, options = {}) {
const {
minScore = 60, // 最低匹配分数
excludeOutsourcing = true, // 是否排除外包
excludeKeywords = [] // 额外排除关键词
} = filterRules;
const { autoSave = false } = options;
// 获取职位类型配置
const { excludeKeywords: typeExcludeKeywords } = await this.getJobTypeConfig(jobTypeId);
const allExcludeKeywords = [...typeExcludeKeywords, ...excludeKeywords];
const results = [];
for (const job of jobs) {
const jobData = job.toJSON ? job.toJSON() : job;
// 分析匹配度(如果 autoSave 为 true 且 job 有 id则自动保存
const analysisOptions = autoSave && jobData.id ? {
jobPostingId: jobData.id,
autoSave: true
} : {};
const analysis = await this.analyzeJobMatch(jobData, resumeInfo, jobTypeId, analysisOptions);
results.push({
...jobData,
matchScore: analysis.overallScore,
matchAnalysis: analysis
});
}
return results
.filter(job => {
// 1. 最低分数过滤
if (job.matchScore < minScore) {
return false;
}
// 2. 外包过滤
if (excludeOutsourcing && job.matchAnalysis.isOutsourcing) {
return false;
}
// 3. 排除关键词过滤
const jobText = this.buildJobText(job);
if (allExcludeKeywords.some(kw => jobText.includes(kw.toLowerCase()))) {
return false;
}
return true;
})
.sort((a, b) => b.matchScore - a.matchScore); // 按匹配分数降序排序
}
/**
* 根据自定义权重配置计算职位评分
* @param {Object} jobData - 职位数据

View File

@@ -0,0 +1,7 @@
/**
* Utils 模块统一导出
*/
module.exports = {
// 工具函数将在需要时添加
};

View File

@@ -8,15 +8,45 @@ class MqttSyncClient {
constructor(brokerUrl, options = {}) {
this.client = mqtt.connect(brokerUrl, options)
this.isConnected = false
/** @type {string[]} 需在每次 connect含重连后向 Broker 幂等订阅的主题 */
this._maintainedTopics = []
/** 最近一次收到任意 `response` 主题消息的时间(用于超时日志关联) */
this.lastResponseAt = null
// 使用 Map 结构优化消息监听器,按 topic 分组
this.messageListeners = new Map(); // Map<topic, Set<listener>>
this.globalListeners = new Set(); // 全局监听器(监听所有 topic
const ts = () => new Date().toISOString()
const markDisconnected = (reason) => {
this.isConnected = false
console.warn(`[MQTT] ${ts()} 连接不可用 reason=${reason}`)
}
this.client.on('connect', () => {
this.isConnected = true
console.log(`[MQTT] ${ts()} 服务端已连接(含重连后的 connect`)
this._resubscribeMaintainedTopics()
})
console.log('MQTT 服务端已连接')
this.client.on('reconnect', () => {
console.log(`[MQTT] ${ts()} 正在重连 Broker...`)
})
this.client.on('offline', () => {
markDisconnected('offline')
})
this.client.on('disconnect', () => {
markDisconnected('disconnect')
})
this.client.on('close', () => {
markDisconnected('close')
})
this.client.on('end', () => {
markDisconnected('end')
})
this.client.on('message', (topic, message) => {
@@ -29,12 +59,10 @@ class MqttSyncClient {
return;
}
// 记录日志但不包含敏感信息
const { maskSensitiveData } = require('../../utils/crypto_utils');
const safeMessage = maskSensitiveData(messageObj, ['password', 'pwd', 'token', 'secret', 'key', 'cookie']);
console.log('[MQTT] 收到消息', topic, '类型:', messageObj.action || messageObj.type || 'unknown');
if (topic === 'response') {
this.lastResponseAt = Date.now()
}
// 优化:只通知相关 topic 的监听器,而不是所有监听器
// 1. 触发该 topic 的专用监听器
const topicListeners = this.messageListeners.get(topic);
if (topicListeners && topicListeners.size > 0) {
@@ -61,18 +89,52 @@ class MqttSyncClient {
})
this.client.on('error', (err) => {
console.warn('[MQTT] Error:', err.message)
console.warn(`[MQTT] ${ts()} Error:`, err && err.message ? err.message : err)
})
}
/**
* 与 mqtt.js 原生 connected 对齐,供单例健康检查
*/
isBrokerConnected() {
return !!(this.client && this.client.connected)
}
/**
* 注册需在每次 connect 后向 Broker 重新声明订阅的主题(不重复注册消息监听器)
* @param {string[]} topics
*/
setMaintainedTopics(topics) {
this._maintainedTopics = Array.isArray(topics) ? [...topics] : []
}
_resubscribeMaintainedTopics() {
if (!this._maintainedTopics.length) return
if (!this.client || !this.client.connected) return
const ts = new Date().toISOString()
for (const topic of this._maintainedTopics) {
this.client.subscribe(topic, { qos: 0 }, (err, granted) => {
if (err) {
console.warn(`[MQTT] ${ts} ensureSubscriptions 订阅失败 topic=${topic}`, err.message || err)
} else {
console.log(`[MQTT] ${ts} ensureSubscriptions 已订阅 topic=${topic}`, granted)
}
})
}
}
waitForConnect(timeout = 5000) {
return new Promise((resolve, reject) => {
if (this.isConnected) return resolve()
if (this.isBrokerConnected()) {
this.isConnected = true
return resolve()
}
const timer = setTimeout(() => {
reject(new Error('MQTT connect timeout'))
}, timeout)
const check = () => {
if (this.isConnected) {
if (this.isBrokerConnected()) {
this.isConnected = true
clearTimeout(timer)
resolve()
} else {
@@ -118,7 +180,6 @@ class MqttSyncClient {
resolve(granted)
}
}
1
})
})
}
@@ -148,7 +209,12 @@ class MqttSyncClient {
const timer = setTimeout(() => {
this.removeMessageListener(onMessage);
reject(new Error('Timeout waiting for response'));
const last = this.lastResponseAt
const extra = last
? ` lastResponseAt=${new Date(last).toISOString()} brokerConnected=${this.isBrokerConnected()}`
: ` brokerConnected=${this.isBrokerConnected()}`
console.warn(`[MQTT] ${new Date().toISOString()} publishAndWait 超时 uuid=${uuid} topic=request_${sn_code}${extra}`)
reject(new Error('Timeout waiting for response' + (last ? `; lastResponseAt=${new Date(last).toISOString()}` : '')));
}, timeout);
const onMessage = (topic, message) => {
@@ -247,6 +313,7 @@ class MqttSyncClient {
}
end(force = false) {
this.isConnected = false
this.client.end(force)
}
}

View File

@@ -1,6 +1,7 @@
const db = require('../dbProxy.js');
const logProxy = require('../logProxy.js');
const deviceManager = require('../schedule/deviceManager.js');
const deviceManager = require('../schedule/core/deviceManager.js');
const chatManager = require('../job/managers/chatManager');
/**
* MQTT 消息分发器
@@ -12,6 +13,8 @@ class MqttDispatcher {
this.mqttClient = mqttClient;
this.actionHandlers = new Map();
this.subscribedTopics = new Set();
// 去重防抖:记录最近处理过的 Boss 消息 securityId -> timestamp
this.bossMessageDedupMap = new Map();
}
/**
@@ -212,8 +215,6 @@ class MqttDispatcher {
return;
}
console.log(`[MQTT心跳] 收到设备 ${sn_code} 的心跳消息`);
// 移除 device_status 模型依赖
// const device_status = db.getModel('device_status');
// let device = await device_status.findByPk(sn_code);
@@ -286,7 +287,6 @@ class MqttDispatcher {
},
{ where: { sn_code } }
);
console.log(`[MQTT心跳] 设备 ${sn_code} 状态已更新到数据库 - 在线: true, 登录: ${updateData.isLoggedIn || false}`);
} catch (error) {
console.error(`[MQTT心跳] 更新数据库状态失败:`, error);
}
@@ -303,6 +303,136 @@ class MqttDispatcher {
}
}
/**
* 处理来自 boss-automation-nodejs 的 Boss 聊天消息
* @param {object|string} message - Boss 消息对象或 JSON 字符串
*/
async handleBossMessage(message) {
try {
let data = message;
if (typeof message === 'string') {
try {
data = JSON.parse(message);
} catch (e) {
console.warn('[MQTT Boss 消息] JSON 解析失败,按原始字符串处理');
}
}
const sn_code = data && data.sn_code ? data.sn_code : null;
const payload = data && data.payload ? data.payload : null;
if (!sn_code || !payload) {
console.warn('[MQTT Boss 消息] sn_code 或 payload 缺失,忽略:', data);
return;
}
// 按你给的结构解析:取第一条消息的文本等关键信息
const firstMsg = Array.isArray(payload.messages) && payload.messages.length > 0
? payload.messages[0]
: null;
const fromUidObj = firstMsg && firstMsg.from && firstMsg.from.uid;
const toUidObj = firstMsg && firstMsg.to && firstMsg.to.uid;
const text = firstMsg && firstMsg.body && typeof firstMsg.body.text === 'string'
? firstMsg.body.text
: null;
// 兼容 uid 为数字或 { low, high } 两种格式
const toUidStr = (uid) => {
if (uid == null) return null;
if (typeof uid === 'number' && !Number.isNaN(uid)) return String(uid);
if (typeof uid === 'object' && typeof uid.low === 'number') return String(uid.low);
return null;
};
const normalized = {
sn_code,
type: payload.type || null,
version: payload.version || null,
from_uid: toUidStr(fromUidObj),
to_uid: toUidStr(toUidObj),
text,
raw: payload
};
// 去重防抖:按 securityId或 cmid在一定时间窗口内只处理一次
const securityId = firstMsg && firstMsg.securityId;
const cmidObj = firstMsg && firstMsg.cmid;
const cmid = cmidObj && typeof cmidObj.low === 'number' ? `${cmidObj.high}:${cmidObj.low}` : null;
const dedupKey = securityId || cmid;
const now = Date.now();
const windowMs = 2 * 60 * 1000; // 2 分钟内视为重复
if (dedupKey) {
const lastTs = this.bossMessageDedupMap.get(dedupKey);
if (lastTs && now - lastTs < windowMs) {
console.log('[MQTT Boss 消息] 检测到重复消息,跳过处理:', { sn_code, dedupKey });
return;
}
this.bossMessageDedupMap.set(dedupKey, now);
}
console.log('[MQTT Boss 消息] 解析结果:', {
sn_code: normalized.sn_code,
type: normalized.type,
version: normalized.version,
from_uid: normalized.from_uid,
to_uid: normalized.to_uid,
text: normalized.text
});
// 落库到 chat_message 表(与自动沟通的落库格式保持一致)
try {
const chatMessageModel = db.getModel('chat_message');
const platform = 'boss';
const friendId = normalized.from_uid ? Number(normalized.from_uid) : 0;
const mid =
firstMsg && firstMsg.mid && typeof firstMsg.mid.low === 'number'
? firstMsg.mid.low
: 0;
const base = {
sn_code,
platform,
friendId,
encryptFriendId: '',
fetch_time: new Date()
};
if (friendId && chatMessageModel) {
const existing = await chatMessageModel.findOne({
where: { sn_code, platform, friendId, mid }
});
const row = { ...base, mid, message_data: firstMsg };
if (existing) {
await existing.update(row);
} else {
await chatMessageModel.create(row);
}
}
} catch (e) {
console.warn('[MQTT Boss 消息] 写入 chat_message 失败:', e.message);
}
// 调用现有 AI 自动回复流程(基于 get_chat_detail + getReplyContentFromDetail
try {
const friendIdNum = normalized.from_uid != null ? Number(normalized.from_uid) : 0;
const hasValidFriendId = friendIdNum > 0 && Number.isFinite(friendIdNum);
if (hasValidFriendId && this.mqttClient) {
const result = await chatManager.auto_reply_with_ai(sn_code, this.mqttClient, {
friendId: friendIdNum,
platform: 'boss'
});
console.log('[MQTT Boss 消息] AI 自动回复结果:', result);
} else if (!hasValidFriendId && normalized.from_uid != null) {
console.warn('[MQTT Boss 消息] 跳过 AI 回复friendId 无效或为 0', { from_uid: normalized.from_uid, friendIdNum });
}
} catch (e) {
console.warn('[MQTT Boss 消息] AI 自动回复失败:', e.message);
}
} catch (error) {
console.error('[MQTT Boss 消息] 处理 Boss 消息失败:', error);
}
}
/**
* 处理响应消息
* @param {object|string} message - 响应消息对象或JSON字符串

View File

@@ -1,8 +1,30 @@
const MqttSyncClient = require('./mqttClient');
const Framework = require('../../../framework/node-core-framework');
const logs = require('../logProxy');
const appConfig = require('../../../config/config.js');
// action.js 已合并到 mqttDispatcher.js不再需要单独引入
function buildMqttManagerConfig() {
const mqttCfg = appConfig.mqtt || {};
const brokerUrl = (mqttCfg.brokerUrl && String(mqttCfg.brokerUrl).trim())
? mqttCfg.brokerUrl.trim()
: `mqtt://${mqttCfg.host || '192.144.167.231'}:${mqttCfg.port != null ? mqttCfg.port : 1883}`;
const options = {
clientId: mqttCfg.clientId || `mqtt_server_${Math.random().toString(16).substr(2, 8)}`,
clean: mqttCfg.clean !== false,
connectTimeout: mqttCfg.connectTimeout != null ? mqttCfg.connectTimeout : 5000,
reconnectPeriod: mqttCfg.reconnectPeriod != null ? mqttCfg.reconnectPeriod : 5000,
keepalive: mqttCfg.keepalive != null ? mqttCfg.keepalive : 60
};
if (mqttCfg.username) {
options.username = mqttCfg.username;
}
if (mqttCfg.password) {
options.password = mqttCfg.password;
}
return { brokerUrl, options };
}
/**
* MQTT管理器 - 单例模式
* 负责管理MQTT连接确保全局只有一个MQTT客户端实例
@@ -11,16 +33,7 @@ class MqttManager {
constructor() {
this.client = null;
this.isInitialized = false;
this.config = {
brokerUrl: 'mqtt://192.144.167.231:1883', // MQTT Broker地址
options: {
clientId: `mqtt_server_${Math.random().toString(16).substr(2, 8)}`,
clean: true,
connectTimeout: 5000,
reconnectPeriod: 5000, // 自动重连间隔
keepalive: 10
}
};
this.config = buildMqttManagerConfig();
}
/**
@@ -30,8 +43,16 @@ class MqttManager {
*/
async getInstance(config = {}) {
if (this.client && this.isInitialized) {
console.log('[MQTT管理器] 返回已存在的MQTT客户端实例');
return this.client;
const brokerOk = typeof this.client.isBrokerConnected === 'function'
? this.client.isBrokerConnected()
: this.client.isConnected;
if (!brokerOk) {
console.warn('[MQTT管理器] 单例已初始化但 Broker 未连接,重置并重建');
await this.reset();
} else {
console.log('[MQTT管理器] 返回已存在的MQTT客户端实例');
return this.client;
}
}
// 合并配置
@@ -91,7 +112,13 @@ class MqttManager {
* @returns {boolean}
*/
isReady() {
return this.isInitialized && this.client && this.client.isConnected;
if (!this.isInitialized || !this.client) {
return false;
}
if (typeof this.client.isBrokerConnected === 'function') {
return this.client.isBrokerConnected();
}
return !!this.client.isConnected;
}
/**

View File

@@ -1,9 +1,9 @@
const logs = require('../logProxy');
const db = require('../dbProxy');
const jobManager = require('../job/index');
const ScheduleUtils = require('./utils');
const ScheduleConfig = require('./config');
const authorizationService = require('../../services/authorization_service');
const logs = require('../../logProxy');
const db = require('../../dbProxy');
const jobManager = require('../../job/index');
const ScheduleUtils = require('../utils/scheduleUtils');
const ScheduleConfig = require('../infrastructure/config');
const authorizationService = require('../../../services/authorization_service');
/**
@@ -84,10 +84,11 @@ class CommandManager {
const start_time = new Date();
let command_id = null;
let command_record = null;
let task = null;
try {
// 1. 获取任务信息
const task = await db.getModel('task_status').findByPk(task_id);
task = await db.getModel('task_status').findByPk(task_id);
if (!task) {
throw new Error(`任务不存在: ${task_id}`);
}
@@ -129,7 +130,7 @@ class CommandManager {
// 4.5 推送指令开始执行状态
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const taskQueue = require('./taskQueue');
const summary = await taskQueue.getTaskStatusSummary(task.sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary, {
@@ -163,7 +164,7 @@ class CommandManager {
// 6.5 推送指令完成状态
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const taskQueue = require('./taskQueue');
const summary = await taskQueue.getTaskStatusSummary(task.sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary);
@@ -191,15 +192,16 @@ class CommandManager {
start_time
);
// 推送指令失败状态
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
const taskQueue = require('./taskQueue');
const summary = await taskQueue.getTaskStatusSummary(task.sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary);
} catch (pushError) {
// 推送失败不影响错误处理
console.warn(`[指令管理] 推送设备工作状态失败:`, pushError.message);
// 推送指令失败状态(需有 task 或从 DB 取 sn_code
if (task && task.sn_code) {
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const taskQueue = require('./taskQueue');
const summary = await taskQueue.getTaskStatusSummary(task.sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary);
} catch (pushError) {
console.warn(`[指令管理] 推送设备工作状态失败:`, pushError.message);
}
}
}
@@ -210,33 +212,18 @@ class CommandManager {
/**
* 执行指令带超时保护
* command_type job 层方法名一致统一使用下划线命名
* @private
*/
async _execute_command_with_timeout(command_id, command_type, command_name, command_params, sn_code, mqttClient, start_time) {
// 将驼峰命名转换为下划线命名
const to_snake_case = (str) => {
if (str.includes('_')) {
return str;
}
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
};
const timeout = ScheduleConfig.taskTimeouts[command_type] || 5 * 60 * 1000;
const method_name = to_snake_case(command_type);
// 获取指令超时时间从配置中获取默认5分钟
const timeout = ScheduleConfig.taskTimeouts[command_type] ||
ScheduleConfig.taskTimeouts[method_name] ||
5 * 60 * 1000;
// 构建指令执行 Promise
const command_promise = (async () => {
if (command_type && jobManager[method_name]) {
return await jobManager[method_name](sn_code, mqttClient, command_params);
} else if (jobManager[command_type]) {
return await jobManager[command_type](sn_code, mqttClient, command_params);
} else {
throw new Error(`未知的指令类型: ${command_type} (尝试的方法名: ${method_name})`);
const fn = jobManager[command_type];
if (!fn) {
throw new Error(`未知的指令类型: ${command_type}, jobManager 中不存在对应方法`);
}
return await fn(sn_code, mqttClient, command_params);
})();
// 使用超时机制包装

View File

@@ -1,8 +1,8 @@
const dayjs = require('dayjs');
const Sequelize = require('sequelize');
const db = require('../dbProxy');
const config = require('./config');
const utils = require('./utils');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
const utils = require('../utils/scheduleUtils');
/**
* 设备管理器简化版
@@ -77,7 +77,6 @@ class DeviceManager {
// 更新登录状态
if (heartbeatData.isLoggedIn !== undefined) {
device.isLoggedIn = heartbeatData.isLoggedIn;
console.log(`[设备管理器] 设备 ${sn_code} 登录状态更新 - isLoggedIn: ${device.isLoggedIn}`);
}
}

View File

@@ -0,0 +1,16 @@
/**
* Core 模块导出
* 统一导出核心模块,简化引用路径
*/
const deviceManager = require('./deviceManager');
const taskQueue = require('./taskQueue');
const command = require('./command');
const scheduledJobs = require('./scheduledJobs');
module.exports = {
deviceManager,
taskQueue,
command,
scheduledJobs
};

View File

@@ -0,0 +1,701 @@
const node_schedule = require("node-schedule");
const dayjs = require('dayjs');
const config = require('../infrastructure/config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const db = require('../../dbProxy');
// 引入新的任务模块
const tasks = require('../tasks');
const { autoSearchTask, autoDeliverTask, autoChatTask, autoActiveTask } = tasks;
const Framework = require("../../../../framework/node-core-framework.js");
/**
* 定时任务管理器(重构版)
* 使用独立的任务模块,职责更清晰,易于维护和扩展
*/
class ScheduledJobs {
constructor(components, taskHandlers) {
this.taskQueue = components.taskQueue;
this.taskHandlers = taskHandlers;
this.jobs = [];
// 业务任务防重入标记(按任务类型存)
this._runningFlags = {
auto_search: false,
auto_deliver: false,
auto_chat: false,
auto_active: false,
job_type_listings_ai: false
};
}
/**
* 启动所有定时任务
*/
start() {
console.log('[定时任务] 开始启动所有定时任务...');
// ==================== 系统维护任务 ====================
// 每天凌晨重置统计数据
const resetJob = node_schedule.scheduleJob(config.schedules.dailyReset, () => {
this.resetDailyStats();
});
this.jobs.push(resetJob);
console.log('[定时任务] ✓ 已启动每日统计重置任务');
// 启动心跳检查定时任务(每分钟检查一次)
const monitoringJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await deviceManager.checkHeartbeatStatus().catch(error => {
console.error('[定时任务] 检查心跳状态失败:', error);
});
});
this.jobs.push(monitoringJob);
console.log('[定时任务] ✓ 已启动心跳检查任务');
// 启动离线设备任务清理定时任务(每分钟检查一次)
const cleanupOfflineTasksJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.cleanupOfflineDeviceTasks().catch(error => {
console.error('[定时任务] 清理离线设备任务失败:', error);
});
});
this.jobs.push(cleanupOfflineTasksJob);
console.log('[定时任务] ✓ 已启动离线设备任务清理任务');
// 启动任务超时检查定时任务(每分钟检查一次)
const timeoutCheckJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.checkTaskTimeouts().catch(error => {
console.error('[定时任务] 检查任务超时失败:', error);
});
});
this.jobs.push(timeoutCheckJob);
console.log('[定时任务] ✓ 已启动任务超时检查任务');
// 启动任务状态摘要同步定时任务每10秒发送一次
const taskSummaryJob = node_schedule.scheduleJob('*/10 * * * * *', async () => {
await this.syncTaskStatusSummary().catch(error => {
console.error('[定时任务] 同步任务状态摘要失败:', error);
});
});
this.jobs.push(taskSummaryJob);
console.log('[定时任务] ✓ 已启动任务状态摘要同步任务');
// ==================== 业务任务(使用新的任务模块) ====================
// 1. 自动搜索任务 - 每60分钟执行一次
const autoSearchJob = node_schedule.scheduleJob(config.schedules.autoSearch || '0 0 */1 * * *', () => {
this.runAutoSearchTask();
});
this.jobs.push(autoSearchJob);
console.log('[定时任务] ✓ 已启动自动搜索任务 (每60分钟)');
// 2. 自动投递任务 - 每1分钟检查一次
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
this.runAutoDeliverTask();
});
this.jobs.push(autoDeliverJob);
console.log('[定时任务] ✓ 已启动自动投递任务 (每1分钟)');
// 3. 自动沟通任务 - 每15分钟执行一次
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */1 * * * *', () => {
this.runAutoChatTask();
});
this.jobs.push(autoChatJob);
console.log('[定时任务] ✓ 已启动自动沟通任务 (每15分钟)');
// 4. 自动活跃任务 - 每2小时执行一次
const autoActiveJob = node_schedule.scheduleJob(config.schedules.autoActive || '0 0 */2 * * *', () => {
this.runAutoActiveTask();
});
this.jobs.push(autoActiveJob);
console.log('[定时任务] ✓ 已启动自动活跃任务 (每2小时)');
// 5. 每日拉取 get_job_listings 并用 AI 更新 job_typesdescription / excludeKeywords / commonSkills / titleIncludeKeywords
const jobTypeListingsAiJob = node_schedule.scheduleJob(config.schedules.jobTypeListingsAi || '0 0 4 * * *', () => {
this.runDailyJobTypeListingsAiSync().catch((err) => {
console.error('[定时任务] 每日 job_types AI 同步失败:', err);
});
});
this.jobs.push(jobTypeListingsAiJob);
console.log('[定时任务] ✓ 已启动每日 job_types AI 同步 (每天 04:00)');
// 立即执行一次业务任务(可选)
setTimeout(() => {
console.log('[定时任务] 立即执行一次初始化任务...');
this.runAutoDeliverTask();
this.runAutoChatTask();
this.runDailyJobTypeListingsAiSync().catch((err) => {
console.error('[定时任务] 启动时 job_types AI 同步失败:', err);
});
}, 10000); // 延迟10秒,等待系统初始化完成和设备心跳
console.log('[定时任务] 所有定时任务启动完成!');
}
// ==================== 业务任务执行方法(使用新的任务模块) ====================
/**
* 运行自动搜索任务
* 为所有启用自动搜索的账号添加搜索任务
*/
async runAutoSearchTask() {
const key = 'auto_search';
if (this._runningFlags[key]) {
console.log('[自动搜索调度] 上一次执行尚未完成,本次跳过');
return;
}
this._runningFlags[key] = true;
try {
const accounts = await this.getEnabledAccounts('auto_search');
if (accounts.length === 0) {
return;
}
console.log(`[自动搜索调度] 找到 ${accounts.length} 个启用自动搜索的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoSearchTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动搜索调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动搜索调度] 执行失败:', error);
} finally {
this._runningFlags[key] = false;
}
}
/**
* 运行自动投递任务
* 为所有启用自动投递的账号添加投递任务
*/
async runAutoDeliverTask() {
const key = 'auto_deliver';
if (this._runningFlags[key]) {
console.log('[自动投递调度] 上一次执行尚未完成,本次跳过');
return;
}
this._runningFlags[key] = true;
try {
const accounts = await this.getEnabledAccounts('auto_deliver');
if (accounts.length === 0) {
return;
}
console.log(`[自动投递调度] 找到 ${accounts.length} 个启用自动投递的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoDeliverTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动投递调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动投递调度] 执行失败:', error);
} finally {
this._runningFlags[key] = false;
}
}
/**
* 运行自动沟通任务
* 为所有启用自动沟通的账号添加沟通任务
*/
async runAutoChatTask() {
const key = 'auto_chat';
if (this._runningFlags[key]) {
console.log('[自动沟通调度] 上一次执行尚未完成,本次跳过');
return;
}
this._runningFlags[key] = true;
try {
const accounts = await this.getEnabledAccounts('auto_chat');
if (accounts.length === 0) {
return;
}
console.log(`[自动沟通调度] 找到 ${accounts.length} 个启用自动沟通的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoChatTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动沟通调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动沟通调度] 执行失败:', error);
} finally {
this._runningFlags[key] = false;
}
}
/**
* 运行自动活跃任务
* 为所有启用自动活跃的账号添加活跃任务
*/
async runAutoActiveTask() {
const key = 'auto_active';
if (this._runningFlags[key]) {
console.log('[自动活跃调度] 上一次执行尚未完成,本次跳过');
return;
}
this._runningFlags[key] = true;
try {
const accounts = await this.getEnabledAccounts('auto_active');
if (accounts.length === 0) {
return;
}
console.log(`[自动活跃调度] 找到 ${accounts.length} 个启用自动活跃的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoActiveTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动活跃调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动活跃调度] 执行失败:', error);
} finally {
this._runningFlags[key] = false;
}
}
/**
* 每日一次:对已绑定 job_type_id 且设备在线的账号下发 get_job_listings成功后在 jobManager 内触发 AI 更新 job_types
*/
async runDailyJobTypeListingsAiSync() {
const key = 'job_type_listings_ai';
if (this._runningFlags[key]) {
console.log('[job_type_listings_ai] 上一次执行尚未完成,本次跳过');
return;
}
this._runningFlags[key] = true;
try {
const Sequelize = require('sequelize');
const { Op } = Sequelize;
const scheduleManager = require('../index');
const jobApi = require('../../job/index');
const mqtt = scheduleManager.mqttClient;
if (!mqtt) {
console.warn('[job_type_listings_ai] MQTT 未初始化,跳过');
return;
}
const { pla_account } = db.models;
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1,
job_type_id: { [Op.ne]: null }
},
attributes: ['id', 'sn_code', 'job_type_id', 'platform_type']
});
if (!accounts || accounts.length === 0) {
return;
}
const now = Date.now();
const offlineThreshold = 3 * 60 * 1000;
let ok = 0;
let skipped = 0;
for (const acc of accounts) {
const sn_code = acc.sn_code;
const device = deviceManager.devices.get(sn_code);
const lastHb = device && device.lastHeartbeat ? device.lastHeartbeat : 0;
const isOnline = device && device.isOnline && now - lastHb < offlineThreshold;
if (!isOnline) {
skipped++;
continue;
}
const platform =
acc.platform_type ||
(typeof acc.getDataValue === 'function' && acc.getDataValue('platform_type')) ||
'boss';
try {
await jobApi.get_job_listings(sn_code, mqtt, { platform });
ok++;
} catch (err) {
console.warn(`[job_type_listings_ai] 设备 ${sn_code} 失败:`, err.message);
skipped++;
}
}
if (ok > 0 || skipped > 0) {
console.log(`[job_type_listings_ai] 完成: 成功 ${ok},跳过/失败 ${skipped},共 ${accounts.length} 个账号`);
}
} catch (error) {
console.error('[job_type_listings_ai] 执行失败:', error);
} finally {
this._runningFlags[key] = false;
}
}
/**
* 获取启用指定功能的账号列表
* @param {string} featureType - 功能类型: auto_search, auto_deliver, auto_chat, auto_active
*/
async getEnabledAccounts(featureType) {
try {
const { pla_account } = db.models;
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1,
[featureType]: 1
},
attributes: ['sn_code', 'name', 'keyword', 'platform_type']
});
return accounts.map(acc => acc.toJSON());
} catch (error) {
console.error(`[获取账号列表] 失败 (${featureType}):`, error);
return [];
}
}
// ==================== 系统维护方法(保留原有逻辑) ====================
/**
* 重置每日统计
*/
resetDailyStats() {
console.log('[定时任务] 重置每日统计数据');
try {
deviceManager.resetAllDailyCounters();
console.log('[定时任务] 每日统计重置完成');
} catch (error) {
console.error('[定时任务] 重置统计失败:', error);
}
}
/**
* 清理过期数据
*/
cleanupCaches() {
console.log('[定时任务] 开始清理过期数据');
try {
deviceManager.cleanupOfflineDevices(config.monitoring.offlineThreshold);
command.cleanupExpiredCommands(30);
console.log('[定时任务] 数据清理完成');
} catch (error) {
console.error('[定时任务] 数据清理失败:', error);
}
}
/**
* 清理离线设备任务
* 检查离线超过10分钟的设备取消其所有pending/running状态的任务
*/
async cleanupOfflineDeviceTasks() {
try {
// 离线阈值10分钟
const offlineThreshold = 10 * 60 * 1000;
const now = Date.now();
const thresholdTime = now - offlineThreshold;
// 获取所有启用的账号
const pla_account = db.getModel('pla_account');
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 通过 deviceManager 检查哪些设备离线超过10分钟
const offlineSnCodes = [];
const offlineDevicesInfo = [];
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: null
});
} else {
const lastHeartbeat = device.lastHeartbeat || 0;
if (lastHeartbeat < thresholdTime || !device.isOnline) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: lastHeartbeat ? new Date(lastHeartbeat) : null
});
}
}
}
if (offlineSnCodes.length === 0) {
return;
}
console.log(`[清理离线任务] 发现 ${offlineSnCodes.length} 个离线超过10分钟的设备`);
let totalCancelled = 0;
const task_status = db.getModel('task_status');
for (const sn_code of offlineSnCodes) {
try {
const deviceInfo = offlineDevicesInfo.find(d => d.sn_code === sn_code);
const updateResult = await task_status.update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({
reason: '设备离线超过10分钟任务已自动取消',
offlineTime: deviceInfo?.lastHeartbeatTime
})
},
{
where: {
sn_code: sn_code,
status: ['pending', 'running']
}
}
);
const cancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
totalCancelled += cancelledCount;
if (this.taskQueue && typeof this.taskQueue.cancelDeviceTasks === 'function') {
await this.taskQueue.cancelDeviceTasks(sn_code);
}
if (cancelledCount > 0) {
console.log(`[清理离线任务] 设备 ${sn_code} 已取消 ${cancelledCount} 个任务`);
}
} catch (error) {
console.error(`[清理离线任务] 取消设备 ${sn_code} 的任务失败:`, error);
}
}
if (totalCancelled > 0) {
console.log(`[清理离线任务] 共取消 ${totalCancelled} 个离线设备的任务`);
}
} catch (error) {
console.error('[清理离线任务] 执行失败:', error);
}
}
/**
* 同步任务状态摘要到客户端
*/
async syncTaskStatusSummary() {
try {
const { pla_account } = await Framework.getModels();
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
const offlineThreshold = 3 * 60 * 1000;
const now = Date.now();
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
continue;
}
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
continue;
}
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const summary = await this.taskQueue.getTaskStatusSummary(sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, {
currentCommand: summary.currentCommand || null
});
} catch (error) {
console.error(`[设备工作状态同步] 设备 ${sn_code} 同步失败:`, error.message);
}
}
} catch (error) {
console.error('[任务状态同步] 执行失败:', error);
}
}
/**
* 检查任务超时并强制标记为失败
*/
async checkTaskTimeouts() {
try {
const Sequelize = require('sequelize');
const { task_status, op } = db.models;
const runningTasks = await task_status.findAll({
where: {
status: 'running'
},
attributes: ['id', 'sn_code', 'taskType', 'taskName', 'startTime', 'create_time']
});
if (!runningTasks || runningTasks.length === 0) {
return;
}
const now = new Date();
let timeoutCount = 0;
for (const task of runningTasks) {
const taskData = task.toJSON();
const startTime = taskData.startTime ? new Date(taskData.startTime) : (taskData.create_time ? new Date(taskData.create_time) : null);
if (!startTime) {
continue;
}
const taskTimeout = config.getTaskTimeout(taskData.taskType) || 10 * 60 * 1000;
const maxAllowedTime = taskTimeout * 1.2;
const elapsedTime = now.getTime() - startTime.getTime();
if (elapsedTime > maxAllowedTime) {
try {
await task_status.update(
{
status: 'failed',
endTime: now,
duration: elapsedTime,
result: JSON.stringify({
error: `任务执行超时(运行时间: ${Math.round(elapsedTime / 1000)}秒,超时限制: ${Math.round(maxAllowedTime / 1000)}秒)`,
timeout: true,
taskType: taskData.taskType,
startTime: startTime.toISOString()
}),
progress: 0
},
{
where: { id: taskData.id }
}
);
timeoutCount++;
console.warn(`[任务超时检查] 任务 ${taskData.id} (${taskData.taskName}) 运行时间过长,已强制标记为失败`);
if (this.taskQueue && typeof this.taskQueue.deviceStatus !== 'undefined') {
const deviceStatus = this.taskQueue.deviceStatus.get(taskData.sn_code);
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.id === taskData.id) {
deviceStatus.isRunning = false;
deviceStatus.currentTask = null;
deviceStatus.runningCount = Math.max(0, deviceStatus.runningCount - 1);
this.taskQueue.globalRunningCount = Math.max(0, this.taskQueue.globalRunningCount - 1);
console.log(`[任务超时检查] 已重置设备 ${taskData.sn_code} 的状态`);
setTimeout(() => {
this.taskQueue.processQueue(taskData.sn_code).catch(error => {
console.error(`[任务超时检查] 继续处理队列失败:`, error);
});
}, 100);
}
}
} catch (error) {
console.error(`[任务超时检查] 更新超时任务状态失败:`, error);
}
}
}
if (timeoutCount > 0) {
console.log(`[任务超时检查] 共检测到 ${timeoutCount} 个超时任务`);
}
} catch (error) {
console.error('[任务超时检查] 执行失败:', error);
}
}
/**
* 停止所有定时任务
*/
stop() {
console.log('[定时任务] 停止所有定时任务...');
for (const job of this.jobs) {
if (job) {
job.cancel();
}
}
this.jobs = [];
console.log('[定时任务] 所有定时任务已停止');
}
}
module.exports = ScheduledJobs;

View File

@@ -1,14 +1,14 @@
const { v4: uuidv4 } = require('uuid');
const Sequelize = require('sequelize');
const logs = require('../logProxy');
const db = require('../dbProxy');
const logs = require('../../logProxy');
const db = require('../../dbProxy');
const command = require('./command');
const PriorityQueue = require('./PriorityQueue');
const ErrorHandler = require('./ErrorHandler');
const PriorityQueue = require('../infrastructure/PriorityQueue');
const ErrorHandler = require('../infrastructure/ErrorHandler');
const deviceManager = require('./deviceManager');
const ScheduleUtils = require('./utils');
const ScheduleConfig = require('./config');
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
const ScheduleUtils = require('../utils/scheduleUtils');
const ScheduleConfig = require('../infrastructure/config');
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
/**
* 任务队列管理器重构版
@@ -222,7 +222,6 @@ class TaskQueue {
// 移除 device_status 依赖,不再检查设备在线状态
// 如果需要在线状态检查,可以从 deviceManager 获取
const deviceManager = require('./deviceManager');
const deviceStatus = deviceManager.getAllDevicesStatus();
const onlineSnCodes = new Set(
Object.entries(deviceStatus)
@@ -230,24 +229,7 @@ class TaskQueue {
.map(([sn_code]) => sn_code)
);
// 原有代码已移除,改为使用 deviceManager
/*
const device_status = db.getModel('device_status');
const heartbeatTimeout = require('./config.js').monitoring.heartbeatTimeout;
const now = new Date();
const heartbeatThreshold = new Date(now.getTime() - heartbeatTimeout);
const onlineDevices = await device_status.findAll({
where: {
isOnline: true,
lastHeartbeatTime: {
[Sequelize.Op.gte]: heartbeatThreshold // 心跳时间在阈值内
}
},
attributes: ['sn_code']
});
const onlineSnCodes = new Set(onlineDevices.map(dev => dev.sn_code));
*/
let processedCount = 0;
let queuedCount = 0;
@@ -406,7 +388,7 @@ class TaskQueue {
const authDays = accountData.authorization_days || 0;
// 使用工具函数计算剩余天数
const { calculateRemainingDays } = require('../../utils/account_utils');
const { calculateRemainingDays } = require('../../../utils/account_utils');
const remaining_days = calculateRemainingDays(authDate, authDays);
// 如果没有授权信息或剩余天数 <= 0不允许创建任务
@@ -1065,13 +1047,13 @@ class TaskQueue {
async getMqttClient() {
try {
// 首先尝试从调度系统获取已初始化的MQTT客户端
const scheduleManager = require('./index');
const scheduleManager = require('../index');
if (scheduleManager.mqttClient) {
return scheduleManager.mqttClient;
}
// 如果调度系统没有初始化,则直接创建
const mqttManager = require('../mqtt/mqttManager');
const mqttManager = require('../../mqtt/mqttManager');
console.log('[任务队列] 创建新的MQTT客户端');
return await mqttManager.getInstance();
} catch (error) {

View File

@@ -0,0 +1,89 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
/**
* 自动活跃处理器
* 负责保持账户活跃度
*/
class ActiveHandler extends BaseHandler {
/**
* 处理自动活跃任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handle(task) {
return await this.execute(task, async () => {
return await this.doActive(task);
}, {
checkAuth: true,
checkOnline: true,
recordDeviceMetrics: true
});
}
/**
* 执行活跃逻辑
*/
async doActive(task) {
const { sn_code, taskParams } = task;
const { platform = 'boss' } = taskParams;
console.log(`[自动活跃] 开始 - 设备: ${sn_code}`);
// 1. 获取账户配置
const accountConfig = await this.getAccountConfig(sn_code, ['platform_type', 'active_strategy']);
if (!accountConfig) {
return {
activeCount: 0,
message: '未找到账户配置'
};
}
// 2. 解析活跃策略配置
const activeStrategy = ConfigManager.parseActiveStrategy(accountConfig.active_strategy);
// 3. 检查活跃时间范围
const timeRange = ConfigManager.getTimeRange(activeStrategy);
if (timeRange) {
const timeRangeValidator = require('../services/timeRangeValidator');
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
if (!timeCheck.allowed) {
return {
activeCount: 0,
message: timeCheck.reason
};
}
}
// 4. 创建活跃指令
const actionNameMap = { view_jobs: '浏览职位', browse_jobs: '浏览职位', refresh_resume: '刷新简历', check_notifications: '查看通知' };
const actions = activeStrategy.actions || ['view_jobs'];
const activeCommands = actions.map(action => ({
command_type: `active_${action}`,
command_name: actionNameMap[action] || `活跃-${action}`,
command_params: JSON.stringify({
sn_code,
platform: platform || accountConfig.platform_type || 'boss',
action
}),
priority: config.getTaskPriority('auto_active') || 5
}));
// 5. 执行活跃指令
const result = await command.executeCommands(task.id, activeCommands, this.mqttClient);
console.log(`[自动活跃] 完成 - 设备: ${sn_code}, 执行动作: ${actions.join(', ')}`);
return {
activeCount: actions.length,
actions,
message: '活跃完成'
};
}
}
module.exports = ActiveHandler;

View File

@@ -0,0 +1,250 @@
const deviceManager = require('../core/deviceManager');
const accountValidator = require('../services/accountValidator');
const db = require('../../dbProxy');
/**
* 任务处理器基类
* 提供通用的授权检查、计时、错误处理、设备记录等功能
*/
class BaseHandler {
constructor(mqttClient) {
this.mqttClient = mqttClient;
}
/**
* 执行任务(带授权检查和错误处理)
* @param {object} task - 任务对象
* @param {Function} businessLogic - 业务逻辑函数
* @param {object} options - 选项
* @returns {Promise<object>} 执行结果
*/
async execute(task, businessLogic, options = {}) {
const {
checkAuth = true, // 是否检查授权
checkOnline = true, // 是否检查在线状态
recordDeviceMetrics = true // 是否记录设备指标
} = options;
const { sn_code, taskName } = task;
const startTime = Date.now();
try {
// 1. 验证账户(启用 + 授权 + 在线)
if (checkAuth || checkOnline) {
const validation = await accountValidator.validate(sn_code, {
checkEnabled: true,
checkAuth,
checkOnline,
offlineThreshold: 3 * 60 * 1000 // 3分钟
});
if (!validation.valid) {
throw new Error(`设备 ${sn_code} 验证失败: ${validation.reason}`);
}
}
// 2. 记录任务开始
if (recordDeviceMetrics) {
deviceManager.recordTaskStart(sn_code, task);
}
// 3. 执行业务逻辑
const result = await businessLogic();
// 4. 记录任务成功
const duration = Date.now() - startTime;
if (recordDeviceMetrics) {
deviceManager.recordTaskComplete(sn_code, task, true, duration);
}
return {
success: true,
duration,
...result
};
} catch (error) {
// 5. 记录任务失败
const duration = Date.now() - startTime;
if (recordDeviceMetrics) {
deviceManager.recordTaskComplete(sn_code, task, false, duration);
}
console.error(`[${taskName}] 执行失败 (设备: ${sn_code}):`, error.message);
return {
success: false,
error: error.message,
duration
};
}
}
/**
* 检查每日操作限制
* @param {string} sn_code - 设备序列号
* @param {string} operation - 操作类型 (search, deliver, chat)
* @param {string} platform - 平台类型
* @returns {Promise<{allowed: boolean, count?: number, limit?: number, reason?: string}>}
*/
async checkDailyLimit(sn_code, operation, platform = 'boss') {
try {
const today = new Date().toISOString().split('T')[0];
const task_status = db.getModel('task_status');
// 查询今日该操作的完成次数
const count = await task_status.count({
where: {
sn_code,
taskType: `auto_${operation}`,
status: 'completed',
endTime: {
[db.models.op.gte]: new Date(today)
}
}
});
// 获取每日限制(从 deviceManager 或配置)
const limit = deviceManager.canExecuteOperation(sn_code, operation);
if (!limit.allowed) {
return {
allowed: false,
count,
reason: limit.reason
};
}
return {
allowed: true,
count,
limit: limit.max || 999
};
} catch (error) {
console.error(`[每日限制检查] 失败 (${sn_code}, ${operation}):`, error);
return { allowed: true }; // 检查失败时默认允许
}
}
/**
* 检查执行间隔时间
* @param {string} sn_code - 设备序列号
* @param {string} taskType - 任务类型
* @param {number} intervalMinutes - 间隔时间(分钟)
* @returns {Promise<{allowed: boolean, elapsed?: number, remaining?: number, reason?: string}>}
*/
async checkInterval(sn_code, taskType, intervalMinutes) {
try {
const task_status = db.getModel('task_status');
// 查询最近一次成功完成的任务
const lastTask = await task_status.findOne({
where: {
sn_code,
taskType,
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
if (!lastTask || !lastTask.endTime) {
return { allowed: true, elapsed: null };
}
const now = Date.now();
const lastTime = new Date(lastTask.endTime).getTime();
const elapsed = now - lastTime;
const intervalMs = intervalMinutes * 60 * 1000;
if (elapsed < intervalMs) {
const remainingMinutes = Math.ceil((intervalMs - elapsed) / (60 * 1000));
const elapsedMinutes = Math.floor(elapsed / (60 * 1000));
return {
allowed: false,
elapsed: elapsedMinutes,
remaining: remainingMinutes,
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟`
};
}
return {
allowed: true,
elapsed: Math.floor(elapsed / (60 * 1000))
};
} catch (error) {
console.error(`[间隔检查] 失败 (${sn_code}, ${taskType}):`, error);
return { allowed: true }; // 检查失败时默认允许
}
}
/**
* 获取账户配置
* @param {string} sn_code - 设备序列号
* @param {string[]} fields - 需要的字段
* @returns {Promise<object|null>}
*/
async getAccountConfig(sn_code, fields = ['*']) {
try {
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: { sn_code, is_delete: 0 },
attributes: fields
});
return account ? account.toJSON() : null;
} catch (error) {
console.error(`[获取账户配置] 失败 (${sn_code}):`, error);
return null;
}
}
/**
* 推送设备工作状态(可选的通知)
* @param {string} sn_code - 设备序列号
* @param {object} status - 状态信息
*/
async notifyDeviceStatus(sn_code, status) {
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, status);
} catch (error) {
console.warn(`[状态推送] 失败 (${sn_code}):`, error.message);
}
}
/**
* 标准化错误响应
* @param {Error} error - 错误对象
* @param {string} sn_code - 设备序列号
* @returns {object} 标准化的错误响应
*/
formatError(error, sn_code) {
return {
success: false,
error: error.message || '未知错误',
sn_code,
timestamp: new Date().toISOString()
};
}
/**
* 标准化成功响应
* @param {object} data - 响应数据
* @param {string} sn_code - 设备序列号
* @returns {object} 标准化的成功响应
*/
formatSuccess(data, sn_code) {
return {
success: true,
sn_code,
timestamp: new Date().toISOString(),
...data
};
}
}
module.exports = BaseHandler;

View File

@@ -0,0 +1,234 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
const chatManager = require('../../job/managers/chatManager');
const db = require('../../dbProxy');
/**
* 自动沟通处理器
* 负责自动回复 HR 消息。auto_chat 是任务,其下按指令执行:获取列表 → 获取详情 →(若需回复)发送消息
*/
class ChatHandler extends BaseHandler {
/**
* 处理自动沟通任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handle(task) {
return await this.execute(task, async () => {
return await this.doChat(task);
}, {
checkAuth: true,
checkOnline: true,
recordDeviceMetrics: true
});
}
/**
* 执行沟通逻辑:先下发「获取列表」指令,再对每个会话下发「获取详情」→(若需回复)「发送消息」指令
*/
async doChat(task) {
const { sn_code, taskParams } = task;
const platform = taskParams.platform || 'boss';
console.log(`[自动沟通] 开始 - 设备: ${sn_code}`);
const accountConfig = await this.getAccountConfig(sn_code, ['platform_type', 'chat_strategy']);
if (!accountConfig) {
return { chatCount: 0, message: '未找到账户配置' };
}
const chatStrategy = ConfigManager.parseChatStrategy(accountConfig.chat_strategy);
const timeRange = ConfigManager.getTimeRange(chatStrategy);
if (timeRange) {
const timeRangeValidator = require('../services/timeRangeValidator');
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
if (!timeCheck.allowed) {
return { chatCount: 0, message: timeCheck.reason };
}
}
const platform_type = platform || accountConfig.platform_type || 'boss';
const page_count = chatStrategy.page_count || 3;
// 1. 下发「获取列表」指令
const list_command = {
command_type: 'get_chat_list',
command_name: '获取聊天列表',
command_params: { platform: platform_type, pageCount: page_count },
priority: config.getTaskPriority('auto_chat') || 6
};
const list_exec = await command.executeCommands(task.id, [list_command], this.mqttClient);
const list_result = list_exec?.results?.[0]?.result;
const friend_list = Array.isArray(list_result?.friendList) ? list_result.friendList : [];
if (friend_list.length === 0) {
console.log(`[自动沟通] 完成 - 设备: ${sn_code},无会话`);
return { chatCount: 0, message: '没有可沟通的会话', detail: { total_contacts: 0 } };
}
this._syncFriendListToChatRecords(sn_code, platform_type, friend_list);
let replied_count = 0;
const details = [];
for (const friend of friend_list) {
if (!friend.friendId) continue;
const item = await this._processOneFriend(task, friend, sn_code, platform_type);
details.push(item);
if (item.replied) replied_count++;
}
console.log(`[自动沟通] 完成 - 设备: ${sn_code},会话 ${friend_list.length},回复 ${replied_count}`);
return {
chatCount: replied_count,
message: '自动沟通完成',
detail: {
total_contacts: friend_list.length,
replied_count,
details
}
};
}
/** 将会话列表同步到 chat_records内部 catch 仅打日志 */
async _syncFriendListToChatRecords(sn_code, platform_type, friend_list) {
try {
const chatRecordsModel = db.getModel('chat_records');
for (const friend of friend_list) {
const friend_id = friend.friendId;
if (friend_id == null) continue;
const encryptId = friend.encryptFriendId || '';
const existing = await chatRecordsModel.findOne({
where: { sn_code, platform: platform_type, encryptFriendId: encryptId }
});
const baseData = {
sn_code,
platform: platform_type,
friendId: friend_id,
encryptFriendId: encryptId,
name: friend.name || '',
updateTime: friend.updateTime != null ? friend.updateTime : null,
brandName: friend.brandName || '',
jobName: friend.jobName || '',
jobCity: friend.jobCity || '',
positionName: friend.positionName || '',
bossTitle: friend.bossTitle || '',
friendSource: friend.friendSource != null ? friend.friendSource : 0,
jobTypeDesc: friend.jobTypeDesc || '',
waterLevel: friend.waterLevel != null ? friend.waterLevel : 0
};
if (existing) await existing.update(baseData);
else await chatRecordsModel.create(baseData);
}
console.log(`[自动沟通] 已同步 ${friend_list.length} 条会话到 chat_records`);
} catch (e) {
console.warn('[自动沟通] 同步聊天会话列表到 chat_records 失败:', e.message);
}
}
/**
* 落库 chat_message入参为解析后格式 { variant, messages, data, job }
* 有 messages 则每条一条记录;仅会话时 mid=0 存 { data, job }
*/
async _saveChatMessagesToDb(parsed, friend, sn_code, platform_type) {
if (!parsed) return;
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
const has_session = parsed.data != null || parsed.job != null;
if (messages.length === 0 && !has_session) return;
try {
const chatMessageModel = db.getModel('chat_message');
const friend_id = friend.friendId;
const encrypt_id = friend.encryptFriendId || '';
const fetch_time = new Date();
const base = { sn_code, platform: platform_type, friendId: friend_id, encryptFriendId: encrypt_id, fetch_time };
if (messages.length > 0) {
for (const msg of messages) {
const mid = msg.mid != null ? msg.mid : 0;
const existing = await chatMessageModel.findOne({
where: { sn_code, platform: platform_type, friendId: friend_id, mid }
});
const row = { ...base, mid, message_data: msg };
if (existing) await existing.update(row);
else await chatMessageModel.create(row);
}
} else {
const boss_payload = { data: parsed.data || null, job: parsed.job || null };
const existing = await chatMessageModel.findOne({
where: { sn_code, platform: platform_type, friendId: friend_id, mid: 0 }
});
const row = { ...base, mid: 0, message_data: boss_payload };
if (existing) await existing.update(row);
else await chatMessageModel.create(row);
}
} catch (e) {
console.warn('[自动沟通] 写入 chat_message 失败:', e.message);
}
}
/**
* 处理单个会话:获取详情 → 落库 → 判断是否回复 → 若需则发送消息
* @returns {{ friendId: number, replied: boolean, reason: string|null }}
*/
async _processOneFriend(task, friend, sn_code, platform_type) {
const friend_id = friend.friendId;
try {
const detail_command = {
command_type: 'get_chat_detail',
command_name: '获取聊天详情',
command_params: { platform: platform_type, friendId: friend_id },
priority: config.getTaskPriority('auto_chat') || 6
};
const detail_exec = await command.executeCommands(task.id, [detail_command], this.mqttClient);
const result = detail_exec?.results?.[0]?.result;
const parsed = chatManager.parseDetailResponse(result || {});
await this._saveChatMessagesToDb(parsed, friend, sn_code, platform_type);
const decision = await chatManager.getReplyContentFromDetail(parsed || {}, {
sn_code,
platform: platform_type,
friendId: friend_id,
encryptFriendId: friend.encryptFriendId || ''
});
if (decision.replied) {
const action = decision.action || 'text';
const content = decision.reply_content || '';
const actionMessages = {
send_resume: [{ type: 'send_resume', content }],
exchange_wechat: [{ type: 'exchange_wechat', content }],
exchange_phone: [{ type: 'exchange_phone', content }]
};
const messages = actionMessages[action] || [{ type: 'text', content }];
const actionNames = { send_resume: '发送简历', exchange_wechat: '换微信', exchange_phone: '换电话' };
const send_command = {
command_type: 'send_chat_message',
command_name: actionNames[action] || '发送聊天消息',
command_params: {
platform: platform_type,
friendId: friend_id,
messages,
chatType: 'reply'
},
priority: config.getTaskPriority('auto_chat') || 6
};
await command.executeCommands(task.id, [send_command], this.mqttClient);
}
return {
friendId: friend_id,
replied: !!decision.replied,
reason: decision.reason || null
};
} catch (err) {
return { friendId: friend_id, replied: false, reason: err.message || '处理失败' };
}
}
}
module.exports = ChatHandler;

View File

@@ -0,0 +1,454 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const jobFilterEngine = require('../services/jobFilterEngine');
const command = require('../core/command');
const config = require('../infrastructure/config');
const db = require('../../dbProxy');
/**
* 自动投递处理器
* 负责职位搜索、过滤、评分和自动投递
*/
class DeliverHandler extends BaseHandler {
/**
* 处理自动投递任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handle(task) {
return await this.execute(task, async () => {
return await this.doDeliver(task);
}, {
checkAuth: true,
checkOnline: true,
recordDeviceMetrics: true
});
}
/**
* 执行投递逻辑
*/
async doDeliver(task) {
const { sn_code, taskParams } = task;
const { keyword, platform = 'boss', pageCount = 3, maxCount = 10 } = taskParams;
console.log(`[自动投递] 开始 - 设备: ${sn_code}, 关键词: ${keyword}`);
// 1. 检查每日投递限制
const dailyCheck = await this.checkDailyDeliverLimit(sn_code, platform);
if (!dailyCheck.allowed) {
return {
deliveredCount: 0,
message: dailyCheck.message
};
}
const actualMaxCount = dailyCheck.actualMaxCount;
// 2. 检查并获取简历
const resume = await this.getOrRefreshResume(sn_code, platform, task.id);
if (!resume) {
return {
deliveredCount: 0,
message: '未找到简历信息'
};
}
// 3. 获取账户配置
const accountConfig = await this.getAccountConfig(sn_code, [
'keyword', 'platform_type', 'deliver_config', 'job_type_id', 'is_salary_priority'
]);
if (!accountConfig) {
return {
deliveredCount: 0,
message: '未找到账户配置'
};
}
// 4. 检查投递时间范围
const deliverConfig = ConfigManager.parseDeliverConfig(accountConfig.deliver_config);
const timeRange = ConfigManager.getTimeRange(deliverConfig);
if (timeRange) {
const timeRangeValidator = require('../services/timeRangeValidator');
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
if (!timeCheck.allowed) {
return {
deliveredCount: 0,
message: timeCheck.reason
};
}
}
// 5. 获取职位类型配置(同时下发 get_job_listings 并保存到 resume_info.job_listings
const jobTypeConfig = await this.getJobTypeConfig(accountConfig.job_type_id, {
sn_code,
platform,
taskId: task.id,
mqttClient: this.mqttClient
});
// 6. 下发 get_job_list与前端一致command 只带 pageCount + tabLabel设备端不接收 keyword/job_type_id
const tabLabel = resume.deliver_tab_label || '';
await this.getJobList(sn_code, platform, pageCount, task.id, tabLabel);
// 7. 从数据库获取待投递职位
const pendingJobs = await this.getPendingJobs(sn_code, platform, actualMaxCount * 3);
if (!pendingJobs || pendingJobs.length === 0) {
return {
deliveredCount: 0,
message: '没有待投递的职位'
};
}
// 8. 过滤配置仅来自职位类型 job_types排除词 / 标题须含词等),不与账号投递配置、任务参数混用
const filterConfig = this.mergeFilterConfig(jobTypeConfig);
// 9. 过滤已投递的公司repeat_deliver_days 由投递配置给出,缺省 30上限 365
const repeatDeliverDays = Math.min(365, Math.max(1, Number(deliverConfig.repeat_deliver_days) || 30));
const recentCompanies = await this.getRecentDeliveredCompanies(sn_code, repeatDeliverDays);
// 10. 过滤 + 评分 + 按 60 分阈值筛(入口在 jobFilterEngine便于阅读
const filteredJobs = await jobFilterEngine.filterAndScoreJobsForDeliver(
pendingJobs,
filterConfig,
resume,
accountConfig,
jobTypeConfig,
recentCompanies
);
// 本轮未进入「可投递」列表的待投递记录,标记为已过滤,避免长期停留在 pending
await this.markFilteredJobsNotPassed(pendingJobs, filteredJobs, sn_code, platform);
const jobsToDeliver = filteredJobs.slice(0, actualMaxCount);
console.log(`[自动投递] 职位筛选完成 - 原始: ${pendingJobs.length}, 符合条件: ${filteredJobs.length}, 将投递: ${jobsToDeliver.length}`);
if (jobsToDeliver.length === 0) {
return {
deliveredCount: 0,
message: '没有符合条件的职位'
};
}
// 11. 创建投递指令并执行
const deliverCommands = this.createDeliverCommands(jobsToDeliver, sn_code, platform);
const result = await command.executeCommands(task.id, deliverCommands, this.mqttClient);
console.log(`[自动投递] 完成 - 设备: ${sn_code}, 投递: ${deliverCommands.length} 个职位`);
return {
deliveredCount: deliverCommands.length,
...result
};
}
/**
* 检查每日投递限制
*/
async checkDailyDeliverLimit(sn_code, platform) {
const apply_records = db.getModel('apply_records');
const dailyLimit = config.getDailyLimit('apply', platform);
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayApplyCount = await apply_records.count({
where: {
sn_code,
platform,
applyTime: {
[db.models.op.gte]: today
}
}
});
console.log(`[自动投递] 今日已投递: ${todayApplyCount}/${dailyLimit}`);
if (todayApplyCount >= dailyLimit) {
return {
allowed: false,
message: `已达到每日投递上限(${dailyLimit}次)`
};
}
const remainingQuota = dailyLimit - todayApplyCount;
return {
allowed: true,
actualMaxCount: remainingQuota,
todayCount: todayApplyCount,
limit: dailyLimit
};
}
/**
* 获取或刷新简历
*/
async getOrRefreshResume(sn_code, platform, taskId) {
const resume_info = db.getModel('resume_info');
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
let resume = await resume_info.findOne({
where: {
sn_code,
platform,
isActive: true
},
order: [['last_modify_time', 'DESC']]
});
const needRefresh = !resume ||
!resume.last_modify_time ||
new Date(resume.last_modify_time) < twoHoursAgo;
if (needRefresh) {
console.log(`[自动投递] 简历超过2小时未更新重新获取`);
try {
await command.executeCommands(taskId, [{
command_type: 'get_online_resume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code, platform }),
priority: config.getTaskPriority('get_resume') || 5
}], this.mqttClient);
// 重新查询
resume = await resume_info.findOne({
where: { sn_code, platform, isActive: true },
order: [['last_modify_time', 'DESC']]
});
} catch (error) {
console.warn(`[自动投递] 获取在线简历失败:`, error.message);
}
}
return resume ? resume.toJSON() : null;
}
/**
* 获取职位类型配置;若传入 options先下发 get_job_listings 获取 tab 列表并写入 resume_info.job_listings
* @param {number} jobTypeId - 职位类型 ID
* @param {object} options - 可选 { sn_code, platform, taskId, mqttClient },用于下发 get_job_listings 并保存
*/
async getJobTypeConfig(jobTypeId, options = {}) {
const { sn_code, platform = 'boss', taskId, mqttClient } = options;
if (sn_code && taskId && mqttClient) {
try {
const getListingsCommand = {
command_type: 'get_job_listings',
command_name: '获取投递标签列表',
command_params: JSON.stringify({ sn_code, platform }),
priority: config.getTaskPriority('auto_deliver') || 7
};
const ret = await command.executeCommands(taskId, [getListingsCommand], mqttClient);
const firstResult = ret.results && ret.results[0];
const list = firstResult && firstResult.result && Array.isArray(firstResult.result)
? firstResult.result
: [];
const job_listings = list.map((item) => (item && item.text != null ? String(item.text).trim() : '')).filter(Boolean);
if (job_listings.length > 0) {
const resume_info = db.getModel('resume_info');
const [updated] = await resume_info.update(
{ job_listings },
{ where: { sn_code, platform } }
);
if (updated) {
console.log(`[自动投递] job_listings 已保存,共 ${job_listings.length}`);
}
}
} catch (err) {
console.warn(`[自动投递] 下发 get_job_listings 或保存失败:`, err.message);
}
}
if (!jobTypeId) return null;
try {
const job_types = db.getModel('job_types');
const jobType = await job_types.findByPk(jobTypeId);
return jobType ? jobType.toJSON() : null;
} catch (error) {
console.error(`[自动投递] 获取职位类型配置失败:`, error);
return null;
}
}
/**
* 下发 get_job_list 命令拉取职位列表command_params 与前端约定pageCount、tabLabel + sn_code、platform
*/
async getJobList(sn_code, platform, pageCount, taskId, tabLabel = '') {
const label = tabLabel != null && String(tabLabel).trim() !== '' ? String(tabLabel).trim() : '';
const params = {
sn_code,
platform,
pageCount,
...(label ? { tabLabel: label } : {})
};
const getJobListCommand = {
command_type: 'get_job_list',
command_name: '获取职位列表',
command_params: JSON.stringify(params),
priority: config.getTaskPriority('auto_deliver') || 7
};
await command.executeCommands(taskId, [getJobListCommand], this.mqttClient);
}
/**
* 将本批中未通过过滤/评分的职位从 pending 更新为 filtered仍 pending 的仅为通过筛选且等待下轮投递的)
* @param {Array} pendingJobs - 本批拉取的待投递
* @param {Array} filteredJobs - filterAndScoreJobsForDeliver 通过的结果(含 matchScore
*/
async markFilteredJobsNotPassed(pendingJobs, filteredJobs, sn_code, platform) {
if (!pendingJobs || pendingJobs.length === 0) {
return;
}
const passedIds = new Set(
(filteredJobs || []).map((j) => j.id).filter((id) => id != null)
);
const notPassedIds = pendingJobs
.map((j) => (j && j.id != null ? j.id : null))
.filter((id) => id != null && !passedIds.has(id));
if (notPassedIds.length === 0) {
return;
}
const job_postings = db.getModel('job_postings');
const { op } = db.models;
try {
const [n] = await job_postings.update(
{ applyStatus: 'filtered' },
{
where: {
id: { [op.in]: notPassedIds },
sn_code,
platform,
applyStatus: 'pending'
}
}
);
console.log(`[自动投递] 不符合条件已标记 filtered: ${notPassedIds.length} 条(更新行数 ${n}`);
} catch (e) {
console.warn('[自动投递] 标记 filtered 失败:', e.message);
}
}
/**
* 获取待投递职位
*/
async getPendingJobs(sn_code, platform, limit) {
const job_postings = db.getModel('job_postings');
const jobs = await job_postings.findAll({
where: {
sn_code,
platform,
applyStatus: 'pending',
create_time: {
[db.models.op.gte]: new Date(Date.now() - 1000 * 60 * 60 * 24)
}
},
order: [['create_time', 'DESC']],
limit
});
return jobs.map(job => job.toJSON ? job.toJSON() : job);
}
/**
* 自动投递过滤配置:仅使用 job_typesexcludeKeywords、titleIncludeKeywords
* 薪资筛选不在此合并min/max 为 0 表示不做薪资过滤);评分权重仍走 accountConfig.is_salary_priority
*/
mergeFilterConfig(jobTypeConfig) {
const base = {
exclude_keywords: [],
filter_keywords: [],
title_include_keywords: [],
min_salary: 0,
max_salary: 0,
priority_weights: []
};
if (!jobTypeConfig) {
return base;
}
if (jobTypeConfig.excludeKeywords) {
try {
const raw = jobTypeConfig.excludeKeywords;
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
base.exclude_keywords = Array.isArray(parsed) ? parsed.map((k) => String(k || '').trim()).filter(Boolean) : [];
} catch (e) {
base.exclude_keywords = [];
}
}
if (jobTypeConfig.titleIncludeKeywords != null) {
const v = jobTypeConfig.titleIncludeKeywords;
if (Array.isArray(v)) {
base.title_include_keywords = v.map((k) => String(k || '').trim()).filter(Boolean);
} else if (typeof v === 'string' && v.trim()) {
try {
const p = JSON.parse(v);
if (Array.isArray(p)) {
base.title_include_keywords = p.map((k) => String(k || '').trim()).filter(Boolean);
}
} catch (e) {
/* ignore */
}
}
}
return base;
}
/**
* 获取近期已投递的公司
*/
async getRecentDeliveredCompanies(sn_code, days = 30) {
const apply_records = db.getModel('apply_records');
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - days);
const recentApplies = await apply_records.findAll({
where: {
sn_code,
applyTime: {
[db.models.op.gte]: daysAgo
}
},
attributes: ['companyName'],
group: ['companyName']
});
return new Set(recentApplies.map(apply => apply.companyName).filter(Boolean));
}
/**
* 创建投递指令
*/
createDeliverCommands(jobs, sn_code, platform) {
return jobs.map(job => ({
command_type: 'deliver_resume',
command_name: '投递简历',
command_params: JSON.stringify({
sn_code,
platform,
jobId: job.jobId,
encryptBossId: job.encryptBossId || '',
securityId: job.securityId || '',
brandName: job.companyName,
jobTitle: job.jobTitle,
companyName: job.companyName,
matchScore: job.matchScore,
scoreDetails: job.scoreDetails
}),
priority: config.getTaskPriority('apply') || 6
}));
}
}
module.exports = DeliverHandler;

View File

@@ -0,0 +1,18 @@
/**
* 处理器模块导出
* 统一导出所有任务处理器
*/
const BaseHandler = require('./baseHandler');
const SearchHandler = require('./searchHandler');
const DeliverHandler = require('./deliverHandler');
const ChatHandler = require('./chatHandler');
const ActiveHandler = require('./activeHandler');
module.exports = {
BaseHandler,
SearchHandler,
DeliverHandler,
ChatHandler,
ActiveHandler
};

View File

@@ -0,0 +1,106 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
/**
* 自动搜索处理器
* 负责搜索职位列表
*/
class SearchHandler extends BaseHandler {
/**
* 处理自动搜索任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handle(task) {
return await this.execute(task, async () => {
return await this.doSearch(task);
}, {
checkAuth: true,
checkOnline: true,
recordDeviceMetrics: true
});
}
/**
* 执行搜索逻辑
*/
async doSearch(task) {
const { sn_code, taskParams } = task;
const { keyword, platform = 'boss', pageCount = 3 } = taskParams;
console.log(`[自动搜索] 开始 - 设备: ${sn_code}, 关键词: ${keyword}`);
// 1. 获取账户配置
const accountConfig = await this.getAccountConfig(sn_code, ['keyword', 'platform_type', 'search_config']);
if (!accountConfig) {
return {
jobsFound: 0,
message: '未找到账户配置'
};
}
// 2. 解析搜索配置
const searchConfig = ConfigManager.parseSearchConfig(accountConfig.search_config);
// 3. 检查搜索时间范围
const timeRange = ConfigManager.getTimeRange(searchConfig);
if (timeRange) {
const timeRangeValidator = require('../services/timeRangeValidator');
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
if (!timeCheck.allowed) {
return {
jobsFound: 0,
message: timeCheck.reason
};
}
}
// 4. 从 resume_info 取 deliver_tab_label下发给 get_job_list 用于切换期望 tab
const platformType = platform || accountConfig.platform_type || 'boss';
let tabLabel = '';
try {
const db = require('../../dbProxy');
const resume_info = db.getModel('resume_info');
const resume = await resume_info.findOne({
where: { sn_code, platform: platformType, isActive: true },
order: [['last_modify_time', 'DESC']],
attributes: ['deliver_tab_label']
});
if (resume && resume.deliver_tab_label) {
tabLabel = String(resume.deliver_tab_label).trim();
}
} catch (e) {
console.warn('[自动搜索] 读取 resume_info.deliver_tab_label 失败:', e.message);
}
const commandParams = {
sn_code,
platform: platformType,
pageCount: pageCount || searchConfig.page_count || 3,
...(tabLabel ? { tabLabel } : {})
};
const searchCommand = {
command_type: 'get_job_list',
command_name: '获取职位列表',
command_params: JSON.stringify(commandParams),
priority: config.getTaskPriority('auto_search') || 8
};
// 5. 执行搜索指令
const result = await command.executeCommands(task.id, [searchCommand], this.mqttClient);
console.log(`[自动搜索] 完成 - 设备: ${sn_code}, 结果: ${JSON.stringify(result)}`);
return {
jobsFound: result.jobCount || 0,
message: '搜索完成'
};
}
}
module.exports = SearchHandler;

View File

@@ -1,17 +1,23 @@
const mqttManager = require("../mqtt/mqttManager.js");
// 导入调度模块(简化版)
const TaskQueue = require('./taskQueue.js');
const Command = require('./command.js');
const deviceManager = require('./deviceManager.js');
const config = require('./config.js');
const utils = require('./utils.js');
// 导入核心模块
const TaskQueue = require('./core/taskQueue.js');
const Command = require('./core/command.js');
const deviceManager = require('./core/deviceManager.js');
const ScheduledJobs = require('./core/scheduledJobs.js');
// 导入新的模块
// 导入基础设施模块
const config = require('./infrastructure/config.js');
const utils = require('./utils/scheduleUtils.js');
// 导入任务处理器
const TaskHandlers = require('./taskHandlers.js');
// 导入MQTT模块
const MqttDispatcher = require('../mqtt/mqttDispatcher.js');
const ScheduledJobs = require('./scheduledJobs.js');
const DeviceWorkStatusNotifier = require('./deviceWorkStatusNotifier.js');
// 导入通知器
const DeviceWorkStatusNotifier = require('./notifiers/deviceWorkStatusNotifier.js');
/**
* 调度系统管理器
@@ -22,7 +28,7 @@ class ScheduleManager {
this.mqttClient = null;
this.isInitialized = false;
this.startTime = new Date();
// 子模块
this.taskHandlers = null;
this.mqttDispatcher = null;
@@ -80,9 +86,9 @@ class ScheduleManager {
async initComponents() {
// 初始化设备管理器
await deviceManager.init();
// 初始化任务队列
await TaskQueue.init?.();
await TaskQueue.init();
}
/**
@@ -124,16 +130,25 @@ class ScheduleManager {
console.error('[调度管理器] 处理响应消息失败:', error);
}
});
// 订阅 Boss 聊天消息主题,将 boss-automation-nodejs 转发过来的新消息交给 mqttDispatcher 处理
this.mqttClient.subscribe("boss/message", async (topic, message) => {
try {
if (this.mqttDispatcher && typeof this.mqttDispatcher.handleBossMessage === 'function') {
await this.mqttDispatcher.handleBossMessage(message);
}
} catch (error) {
console.error('[调度管理器] 处理 Boss 消息失败:', error);
}
});
// 重连后向 Broker 幂等声明订阅(监听器仅在上方注册一次,不重复添加)
if (typeof this.mqttClient.setMaintainedTopics === 'function') {
this.mqttClient.setMaintainedTopics(['heartbeat', 'response', 'boss/message']);
}
}
/**
* 手动执行找工作流程已废弃full_flow 不再使用)
* @deprecated 请使用其他任务类型,如 auto_deliver
*/
async manualExecuteJobFlow(sn_code, keyword = '前端') {
console.warn(`[手动执行] manualExecuteJobFlow 已废弃full_flow 不再使用`);
throw new Error('full_flow 任务类型已废弃,请使用其他任务类型');
}
/**
* 获取系统状态
@@ -142,7 +157,11 @@ class ScheduleManager {
const status = this.mqttDispatcher ? this.mqttDispatcher.getSystemStatus() : {};
return {
isInitialized: this.isInitialized,
mqttConnected: this.mqttClient && this.mqttClient.isConnected,
mqttConnected: this.mqttClient && (
typeof this.mqttClient.isBrokerConnected === 'function'
? this.mqttClient.isBrokerConnected()
: this.mqttClient.isConnected
),
systemStats: deviceManager.getSystemStats(),
allDevices: deviceManager.getAllDevicesStatus(),
taskQueues: TaskQueue.getAllDeviceStatus(),
@@ -178,28 +197,18 @@ class ScheduleManager {
}
}
// 创建调度管理器实例
// 创建并导出调度管理器实例
const scheduleManager = new ScheduleManager();
// 导出兼容接口,保持与原有代码的一致性
// 导出兼容接口(简化版)
module.exports = {
// 初始化方法
init: () => scheduleManager.init(),
// 手动执行任务
manualExecuteJobFlow: (sn_code, keyword) => scheduleManager.manualExecuteJobFlow(sn_code, keyword),
// 获取系统状态
getSystemStatus: () => scheduleManager.getSystemStatus(),
// 停止系统
stop: () => scheduleManager.stop(),
// 访问各个组件(为了兼容性
// 直接暴露属性(使用 getter 保持动态访问)
get mqttClient() { return scheduleManager.mqttClient; },
get isInitialized() { return scheduleManager.isInitialized; },
// 访问各个组件实例(简化版)
get taskQueue() { return TaskQueue; },
get command() { return Command; },
get deviceManager() { return deviceManager; }

View File

@@ -1,4 +1,4 @@
const db = require('../dbProxy');
const db = require('../../dbProxy');
/**
* 统一错误处理模块

View File

@@ -23,6 +23,7 @@ class ScheduleConfig {
// 任务超时配置(毫秒)
this.taskTimeouts = {
auto_search: 20 * 60 * 1000, // 自动搜索任务20分钟
auto_deliver: 30 * 60 * 1000, // 自动投递任务30分钟包含多个子任务
auto_chat: 15 * 60 * 1000, // 自动沟通任务15分钟
auto_active_account: 10 * 60 * 1000 // 自动活跃账号任务10分钟
@@ -30,6 +31,7 @@ class ScheduleConfig {
// 任务优先级配置
this.taskPriorities = {
auto_search: 8, // 自动搜索任务(最高优先级,先搜索后投递)
auto_deliver: 7, // 自动投递任务
auto_chat: 6, // 自动沟通任务
auto_active_account: 5, // 自动活跃账号任务
@@ -44,10 +46,13 @@ class ScheduleConfig {
// 定时任务配置
this.schedules = {
dailyReset: '0 0 * * *', // 每天凌晨重置统计
monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟
autoDeliver: '0 */1 * * * *', // 自动投递任务每1分钟执行一次
autoChat: '0 */15 * * * *' // 自动沟通任务:每15分钟执行一次
dailyReset: '0 0 * * *', // 每天凌晨重置统计
monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟
autoSearch: '0 0 */1 * * *', // 自动搜索任务每1小时执行一次
autoDeliver: '0 */2 * * * *', // 自动投递任务:每2分钟执行一次
autoChat: '0 */1 * * * *', // 自动沟通任务每1分钟执行一次
autoActive: '0 0 */2 * * *', // 自动活跃任务每2小时执行一次
jobTypeListingsAi: '0 0 4 * * *' // 每天 04:00 对有 job_type_id 的在线设备拉取 get_job_listings 并 AI 更新 job_types
};
}

View File

@@ -0,0 +1,14 @@
/**
* Infrastructure 模块导出
* 统一导出基础设施模块
*/
const PriorityQueue = require('./PriorityQueue');
const ErrorHandler = require('./ErrorHandler');
const config = require('./config');
module.exports = {
PriorityQueue,
ErrorHandler,
config
};

View File

@@ -3,7 +3,7 @@
* 负责向客户端推送设备当前工作状态任务指令等
*/
const db = require('../dbProxy');
const db = require('../../dbProxy');
class DeviceWorkStatusNotifier {
constructor() {
@@ -186,44 +186,33 @@ class DeviceWorkStatusNotifier {
}
/**
* 格式化指令描述
* 格式化指令描述与前端/后端/下发统一只用一个名字 command_type不做映射
* @private
*/
_formatCommandDescription(command) {
const params = command.command_params || command.params || {};
let parsedParams = {};
if (typeof params === 'string') {
try {
parsedParams = JSON.parse(params);
} catch (e) {
// 解析失败,忽略
}
} catch (e) {}
} else {
parsedParams = params;
}
// 根据指令类型格式化描述
const commandType = command.command_type || command.type || '';
const commandName = command.command_name || command.name || '';
const command_type = command.command_type || command.type || '';
if (parsedParams.jobTitle && parsedParams.companyName) {
const companyName = parsedParams.companyName.length > 20
? parsedParams.companyName.substring(0, 20) + '...'
const companyName = parsedParams.companyName.length > 20
? parsedParams.companyName.substring(0, 20) + '...'
: parsedParams.companyName;
return `投递职位: ${parsedParams.jobTitle} @ ${companyName}`;
} else if (parsedParams.jobTitle) {
return `投递职位: ${parsedParams.jobTitle}`;
} else if (commandType === 'applyJob' || commandName.includes('投递')) {
return '投递简历';
} else if (commandType === 'searchJobs' || commandName.includes('搜索')) {
return `搜索职位: ${parsedParams.keyword || ''}`;
} else if (commandType === 'sendChatMessage' || commandName.includes('沟通')) {
return '发送消息';
} else if (commandName) {
return commandName;
return `${command_type}: ${parsedParams.jobTitle} @ ${companyName}`;
}
return '执行指令';
if (parsedParams.jobTitle) {
return `${command_type}: ${parsedParams.jobTitle}`;
}
if (parsedParams.keyword) {
return `${command_type}: ${parsedParams.keyword}`;
}
return command_type || command.command_name || '执行指令';
}
/**

View File

@@ -0,0 +1,9 @@
/**
* Notifiers 模块导出
*/
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
module.exports = {
deviceWorkStatusNotifier
};

View File

@@ -1,779 +0,0 @@
const node_schedule = require("node-schedule");
const dayjs = require('dayjs');
const config = require('./config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const db = require('../dbProxy');
const authorizationService = require('../../services/authorization_service.js');
const Framework = require("../../../framework/node-core-framework.js");
/**
* 检查当前时间是否在指定的时间范围内
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
* @returns {Object} {allowed: boolean, reason: string}
*/
function checkTimeRange(timeRange) {
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
return { allowed: true, reason: '未配置时间范围' };
}
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute; // 转换为分钟数
// 解析开始时间和结束时间
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
// 检查是否仅工作日(使用宽松比较,兼容字符串和数字)
if (timeRange.workdays_only == 1) { // 使用 == 而不是 ===
const dayOfWeek = now.getDay(); // 0=周日, 1=周一, ..., 6=周六
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
}
}
// 检查当前时间是否在时间范围内
if (startTime <= endTime) {
// 正常情况09:00 - 18:00
if (currentTime < startTime || currentTime >= endTime) {
return { allowed: false, reason: `当前时间 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` };
}
} else {
// 跨天情况22:00 - 06:00
if (currentTime < startTime && currentTime >= endTime) {
return { allowed: false, reason: `当前时间 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` };
}
}
return { allowed: true, reason: '在允许的时间范围内' };
}
/**
* 定时任务管理器(简化版)
* 管理所有定时任务的创建和销毁
*/
class ScheduledJobs {
constructor(components, taskHandlers) {
this.taskQueue = components.taskQueue;
this.taskHandlers = taskHandlers;
this.jobs = [];
}
/**
* 启动所有定时任务
*/
start() {
// 每天凌晨重置统计数据
const resetJob = node_schedule.scheduleJob(config.schedules.dailyReset, () => {
this.resetDailyStats();
});
this.jobs.push(resetJob);
// 启动心跳检查定时任务(每分钟检查一次)
const monitoringJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await deviceManager.checkHeartbeatStatus().catch(error => {
console.error('[定时任务] 检查心跳状态失败:', error);
});
});
this.jobs.push(monitoringJob);
// 启动离线设备任务清理定时任务(每分钟检查一次)
const cleanupOfflineTasksJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.cleanupOfflineDeviceTasks().catch(error => {
console.error('[定时任务] 清理离线设备任务失败:', error);
});
});
this.jobs.push(cleanupOfflineTasksJob);
console.log('[定时任务] 已启动离线设备任务清理任务');
// 启动任务超时检查定时任务(每分钟检查一次)
const timeoutCheckJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.checkTaskTimeouts().catch(error => {
console.error('[定时任务] 检查任务超时失败:', error);
});
});
this.jobs.push(timeoutCheckJob);
console.log('[定时任务] 已启动任务超时检查任务');
// 启动任务状态摘要同步定时任务每10秒发送一次
const taskSummaryJob = node_schedule.scheduleJob('*/10 * * * * *', async () => {
await this.syncTaskStatusSummary().catch(error => {
console.error('[定时任务] 同步任务状态摘要失败:', error);
});
});
this.jobs.push(taskSummaryJob);
console.log('[定时任务] 已启动任务状态摘要同步任务');
// 执行自动投递任务
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
this.autoDeliverTask();
});
// 立即执行一次自动投递任务
this.autoDeliverTask();
this.jobs.push(autoDeliverJob);
console.log('[定时任务] 已启动自动投递任务');
// 执行自动沟通任务
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */15 * * * *', () => {
this.autoChatTask();
});
// 立即执行一次自动沟通任务
this.autoChatTask();
this.jobs.push(autoChatJob);
console.log('[定时任务] 已启动自动沟通任务');
}
/**
* 重置每日统计
*/
resetDailyStats() {
console.log('[定时任务] 重置每日统计数据');
try {
deviceManager.resetAllDailyCounters();
console.log('[定时任务] 每日统计重置完成');
} catch (error) {
console.error('[定时任务] 重置统计失败:', error);
}
}
/**
* 清理过期数据
*/
cleanupCaches() {
console.log('[定时任务] 开始清理过期数据');
try {
deviceManager.cleanupOfflineDevices(config.monitoring.offlineThreshold);
command.cleanupExpiredCommands(30);
console.log('[定时任务] 数据清理完成');
} catch (error) {
console.error('[定时任务] 数据清理失败:', error);
}
}
/**
* 清理离线设备任务
* 检查离线超过10分钟的设备取消其所有pending/running状态的任务
*/
async cleanupOfflineDeviceTasks() {
try {
// 离线阈值10分钟
const offlineThreshold = 10 * 60 * 1000; // 10分钟
const now = Date.now();
const thresholdTime = now - offlineThreshold;
// 获取所有启用的账号
const pla_account = db.getModel('pla_account');
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 通过 deviceManager 检查哪些设备离线超过10分钟
const offlineSnCodes = [];
const offlineDevicesInfo = [];
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: null
});
} else {
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
if (lastHeartbeat < thresholdTime || !device.isOnline) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: lastHeartbeat ? new Date(lastHeartbeat) : null
});
}
}
}
if (offlineSnCodes.length === 0) {
return;
}
console.log(`[清理离线任务] 发现 ${offlineSnCodes.length} 个离线超过10分钟的设备: ${offlineSnCodes.join(', ')}`);
let totalCancelled = 0;
// 为每个离线设备取消任务
const task_status = db.getModel('task_status');
for (const sn_code of offlineSnCodes) {
try {
// 查询该设备的所有pending/running任务
const pendingTasks = await task_status.findAll({
where: {
sn_code: sn_code,
status: ['pending', 'running']
},
attributes: ['id']
});
if (pendingTasks.length === 0) {
continue;
}
const deviceInfo = offlineDevicesInfo.find(d => d.sn_code === sn_code);
// 更新任务状态为cancelled
const updateResult = await task_status.update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({
reason: '设备离线超过10分钟任务已自动取消',
offlineTime: deviceInfo?.lastHeartbeatTime
})
},
{
where: {
sn_code: sn_code,
status: ['pending', 'running']
}
}
);
const cancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
totalCancelled += cancelledCount;
// 从内存队列中移除任务
if (this.taskQueue && typeof this.taskQueue.cancelDeviceTasks === 'function') {
await this.taskQueue.cancelDeviceTasks(sn_code);
}
console.log(`[清理离线任务] 设备 ${sn_code} 已取消 ${cancelledCount} 个任务`);
} catch (error) {
console.error(`[清理离线任务] 取消设备 ${sn_code} 的任务失败:`, error);
}
}
if (totalCancelled > 0) {
console.log(`[清理离线任务] 共取消 ${totalCancelled} 个离线设备的任务`);
}
} catch (error) {
console.error('[清理离线任务] 执行失败:', error);
}
}
/**
* 同步任务状态摘要到客户端
* 定期向所有在线设备发送任务状态摘要(当前任务、待执行任务、下次执行时间等)
*/
async syncTaskStatusSummary() {
try {
const { pla_account } = await Framework.getModels();
// 获取所有启用的账号
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
// 为每个在线设备发送任务状态摘要
for (const account of accounts) {
const sn_code = account.sn_code;
// 检查设备是否在线
const device = deviceManager.devices.get(sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线,跳过
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
// 设备离线,跳过
continue;
}
// 设备在线,推送设备工作状态
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
const summary = await this.taskQueue.getTaskStatusSummary(sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, {
currentCommand: summary.currentCommand || null
});
} catch (error) {
console.error(`[设备工作状态同步] 设备 ${sn_code} 同步失败:`, error.message);
}
}
} catch (error) {
console.error('[任务状态同步] 执行失败:', error);
}
}
/**
* 检查任务超时并强制标记为失败
* 检测长时间运行的任务(可能是卡住的),强制标记为失败,释放资源
*/
async checkTaskTimeouts() {
try {
const Sequelize = require('sequelize');
const { task_status, op } = db.models;
// 查询所有运行中的任务
const runningTasks = await task_status.findAll({
where: {
status: 'running'
},
attributes: ['id', 'sn_code', 'taskType', 'taskName', 'startTime', 'create_time']
});
if (!runningTasks || runningTasks.length === 0) {
return;
}
const now = new Date();
let timeoutCount = 0;
for (const task of runningTasks) {
const taskData = task.toJSON();
const startTime = taskData.startTime ? new Date(taskData.startTime) : (taskData.create_time ? new Date(taskData.create_time) : null);
if (!startTime) {
continue;
}
// 获取任务类型的超时时间默认10分钟
const taskTimeout = config.getTaskTimeout(taskData.taskType) || 10 * 60 * 1000;
// 允许额外20%的缓冲时间
const maxAllowedTime = taskTimeout * 1.2;
const elapsedTime = now.getTime() - startTime.getTime();
// 如果任务运行时间超过最大允许时间,标记为超时失败
if (elapsedTime > maxAllowedTime) {
try {
await task_status.update(
{
status: 'failed',
endTime: now,
duration: elapsedTime,
result: JSON.stringify({
error: `任务执行超时(运行时间: ${Math.round(elapsedTime / 1000)}秒,超时限制: ${Math.round(maxAllowedTime / 1000)}秒)`,
timeout: true,
taskType: taskData.taskType,
startTime: startTime.toISOString()
}),
progress: 0
},
{
where: { id: taskData.id }
}
);
timeoutCount++;
console.warn(`[任务超时检查] 任务 ${taskData.id} (${taskData.taskName}) 运行时间过长,已强制标记为失败`, {
task_id: taskData.id,
sn_code: taskData.sn_code,
taskType: taskData.taskType,
elapsedTime: Math.round(elapsedTime / 1000) + '秒',
maxAllowedTime: Math.round(maxAllowedTime / 1000) + '秒'
});
// 如果任务队列中有这个任务,也需要从内存中清理
if (this.taskQueue && typeof this.taskQueue.deviceStatus !== 'undefined') {
const deviceStatus = this.taskQueue.deviceStatus.get(taskData.sn_code);
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.id === taskData.id) {
// 重置设备状态,允许继续执行下一个任务
deviceStatus.isRunning = false;
deviceStatus.currentTask = null;
deviceStatus.runningCount = Math.max(0, deviceStatus.runningCount - 1);
this.taskQueue.globalRunningCount = Math.max(0, this.taskQueue.globalRunningCount - 1);
console.log(`[任务超时检查] 已重置设备 ${taskData.sn_code} 的状态,可以继续执行下一个任务`);
// 尝试继续处理该设备的队列
setTimeout(() => {
this.taskQueue.processQueue(taskData.sn_code).catch(error => {
console.error(`[任务超时检查] 继续处理队列失败 (设备: ${taskData.sn_code}):`, error);
});
}, 100);
}
}
} catch (error) {
console.error(`[任务超时检查] 更新超时任务状态失败 (任务ID: ${taskData.id}):`, error);
}
}
}
if (timeoutCount > 0) {
console.log(`[任务超时检查] 共检测到 ${timeoutCount} 个超时任务,已强制标记为失败`);
}
} catch (error) {
console.error('[任务超时检查] 执行失败:', error);
}
}
/**
* 自动投递任务
*/
async autoDeliverTask() {
const now = new Date();
console.log(`[自动投递] ${now.toLocaleString()} 开始执行自动投递任务`);
try {
// 移除 device_status 依赖,改为直接从 pla_account 查询启用且开启自动投递的账号
const models = db.models;
const { pla_account, op } = models;
// 直接从 pla_account 查询启用且开启自动投递的账号
// 注意:不再检查在线状态,因为 device_status 已移除
const pla_users = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1, // 只获取启用的账号
auto_deliver: 1
}
});
if (!pla_users || pla_users.length === 0) {
console.log('[自动投递] 没有启用且开启自动投递的账号');
return;
}
console.log(`[自动投递] 找到 ${pla_users.length} 个可用账号`);
// 获取 task_status 模型用于查询上次投递时间
const { task_status } = models;
// 为每个设备添加自动投递任务到队列
for (const pl_user of pla_users) {
const userData = pl_user.toJSON();
// 检查设备是否在线离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const device = deviceManager.devices.get(userData.sn_code);
// 检查用户授权天数 是否够
const authorization = await authorizationService.checkAuthorization(userData.sn_code);
if (!authorization.is_authorized) {
console.log(`[自动投递] 设备 ${userData.sn_code} 授权天数不足,跳过添加任务`);
continue;
}
if (!device) {
// 设备从未发送过心跳,视为离线
console.log(`[自动投递] 设备 ${userData.sn_code} 离线(从未发送心跳),跳过添加任务`);
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
console.log(`[自动投递] 设备 ${userData.sn_code} 离线(最后心跳: ${offlineMinutes}分钟前),跳过添加任务`);
continue;
}
// 检查设备调度策略
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'deliver');
if (!canExecute.allowed) {
console.log(`[自动投递] 设备 ${userData.sn_code} 不满足执行条件: ${canExecute.reason}`);
continue;
}
// 获取投递配置,如果不存在则使用默认值
let deliver_config = userData.deliver_config;
if (typeof deliver_config === 'string') {
try {
deliver_config = JSON.parse(deliver_config);
} catch (e) {
deliver_config = {};
}
}
deliver_config = deliver_config || {
deliver_interval: 30,
min_salary: 0,
max_salary: 0,
page_count: 3,
max_deliver: 10,
filter_keywords: [],
exclude_keywords: []
};
// 检查投递时间范围
if (deliver_config.time_range) {
const timeCheck = checkTimeRange(deliver_config.time_range);
if (!timeCheck.allowed) {
console.log(`[自动投递] 设备 ${userData.sn_code} ${timeCheck.reason}`);
continue;
}
}
// 检查投递间隔时间
const deliver_interval = deliver_config.deliver_interval || 30; // 默认30分钟
const interval_ms = deliver_interval * 60 * 1000; // 转换为毫秒
// 查询该账号最近一次成功完成的自动投递任务
const lastDeliverTask = await task_status.findOne({
where: {
sn_code: userData.sn_code,
taskType: 'auto_deliver',
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
// 如果存在上次投递记录,检查是否已经过了间隔时间
if (lastDeliverTask && lastDeliverTask.endTime) {
const lastDeliverTime = new Date(lastDeliverTask.endTime);
const elapsedTime = new Date().getTime() - lastDeliverTime.getTime();
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
const message = `距离上次投递仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${deliver_interval} 分钟)`;
console.log(`[自动投递] 设备 ${userData.sn_code} ${message}`);
// 推送等待状态到客户端
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
// 获取当前任务状态摘要
const taskStatusSummary = this.taskQueue ? this.taskQueue.getTaskStatusSummary(userData.sn_code) : {
sn_code: userData.sn_code,
pendingCount: 0,
totalPendingCount: 0,
pendingTasks: []
};
// 添加等待消息到工作状态
await deviceWorkStatusNotifier.sendDeviceWorkStatus(userData.sn_code, taskStatusSummary, {
waitingMessage: {
type: 'deliver_interval',
message: message,
remainingMinutes: remainingMinutes,
nextDeliverTime: new Date(lastDeliverTime.getTime() + interval_ms).toISOString()
}
});
} catch (pushError) {
console.warn(`[自动投递] 推送等待消息失败:`, pushError.message);
}
continue;
}
}
// 添加自动投递任务到队列
await this.taskQueue.addTask(userData.sn_code, {
taskType: 'auto_deliver',
taskName: `自动投递 - ${userData.keyword || ''}`,
taskParams: {
keyword: userData.keyword || '',
platform: userData.platform_type || 'boss',
pageCount: deliver_config.page_count || 3,
maxCount: deliver_config.max_deliver || 10,
filterRules: {
minSalary: deliver_config.min_salary || 0,
maxSalary: deliver_config.max_salary || 0,
keywords: deliver_config.filter_keywords || [],
excludeKeywords: deliver_config.exclude_keywords || []
}
},
priority: config.getTaskPriority('auto_deliver') || 6
});
console.log(`[自动投递] 已为设备 ${userData.sn_code} 添加自动投递任务,关键词: ${userData.keyword || '默认'},投递间隔: ${deliver_interval} 分钟`);
}
console.log('[自动投递] 任务添加完成');
} catch (error) {
console.error('[自动投递] 执行失败:', error);
}
}
/**
* 自动沟通任务
*/
async autoChatTask() {
const now = new Date();
console.log(`[自动沟通] ${now.toLocaleString()} 开始执行自动沟通任务`);
try {
// 移除 device_status 依赖,改为直接从 pla_account 查询启用且开启自动沟通的账号
const models = db.models;
const { pla_account, op } = models;
// 直接从 pla_account 查询启用且开启自动沟通的账号
// 注意:不再检查在线状态,因为 device_status 已移除
const pla_users = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1, // 只获取启用的账号
auto_chat: 1
}
});
if (!pla_users || pla_users.length === 0) {
console.log('[自动沟通] 没有启用且开启自动沟通的账号');
return;
}
console.log(`[自动沟通] 找到 ${pla_users.length} 个可用账号`);
// 获取 task_status 模型用于查询上次沟通时间
const { task_status } = models;
// 为每个设备添加自动沟通任务到队列
for (const pl_user of pla_users) {
const userData = pl_user.toJSON();
// 检查设备是否在线离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const device = deviceManager.devices.get(userData.sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线
console.log(`[自动沟通] 设备 ${userData.sn_code} 离线(从未发送心跳),跳过添加任务`);
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
console.log(`[自动沟通] 设备 ${userData.sn_code} 离线(最后心跳: ${offlineMinutes}分钟前),跳过添加任务`);
continue;
}
// 检查设备调度策略
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'chat');
if (!canExecute.allowed) {
console.log(`[自动沟通] 设备 ${userData.sn_code} 不满足执行条件: ${canExecute.reason}`);
continue;
}
// 获取沟通策略配置
let chatStrategy = {};
if (userData.chat_strategy) {
chatStrategy = typeof userData.chat_strategy === 'string'
? JSON.parse(userData.chat_strategy)
: userData.chat_strategy;
}
// 检查沟通时间范围
if (chatStrategy.time_range) {
const timeCheck = checkTimeRange(chatStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动沟通] 设备 ${userData.sn_code} ${timeCheck.reason}`);
continue;
}
}
// 检查沟通间隔时间
const chat_interval = chatStrategy.chat_interval || 30; // 默认30分钟
const interval_ms = chat_interval * 60 * 1000; // 转换为毫秒
// 查询该账号最近一次成功完成的自动沟通任务
const lastChatTask = await task_status.findOne({
where: {
sn_code: userData.sn_code,
taskType: 'auto_chat',
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
// 如果存在上次沟通记录,检查是否已经过了间隔时间
if (lastChatTask && lastChatTask.endTime) {
const lastChatTime = new Date(lastChatTask.endTime);
const elapsedTime = now.getTime() - lastChatTime.getTime();
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
console.log(`[自动沟通] 设备 ${userData.sn_code} 距离上次沟通仅 ${Math.round(elapsedTime / (60 * 1000))} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${chat_interval} 分钟)`);
continue;
}
}
// 添加自动沟通任务到队列
await this.taskQueue.addTask(userData.sn_code, {
taskType: 'auto_chat',
taskName: `自动沟通 - ${userData.name || '默认'}`,
taskParams: {
platform: userData.platform_type || 'boss'
},
priority: config.getTaskPriority('auto_chat') || 6
});
console.log(`[自动沟通] 已为设备 ${userData.sn_code} 添加自动沟通任务,沟通间隔: ${chat_interval} 分钟`);
}
console.log('[自动沟通] 任务添加完成');
} catch (error) {
console.error('[自动沟通] 执行失败:', error);
}
}
}
module.exports = ScheduledJobs;

View File

@@ -0,0 +1,199 @@
const db = require('../../dbProxy');
const authorizationService = require('../../../services/authorization_service');
const deviceManager = require('../core/deviceManager');
/**
* 账户验证服务
* 统一处理账户启用状态、授权状态、在线状态的检查
*/
class AccountValidator {
/**
* 检查账户是否启用
* @param {string} sn_code - 设备序列号
* @returns {Promise<{enabled: boolean, reason?: string}>}
*/
async checkEnabled(sn_code) {
try {
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: { sn_code, is_delete: 0 },
attributes: ['is_enabled', 'name']
});
if (!account) {
return { enabled: false, reason: '账户不存在' };
}
if (!account.is_enabled) {
return { enabled: false, reason: '账户未启用' };
}
return { enabled: true };
} catch (error) {
console.error(`[账户验证] 检查启用状态失败 (${sn_code}):`, error);
return { enabled: false, reason: '检查失败' };
}
}
/**
* 检查账户授权状态
* @param {string} sn_code - 设备序列号
* @returns {Promise<{authorized: boolean, days?: number, reason?: string}>}
*/
async checkAuthorization(sn_code) {
try {
const result = await authorizationService.checkAuthorization(sn_code);
if (!result.is_authorized) {
return {
authorized: false,
days: result.days_remaining || 0,
reason: result.message || '授权已过期'
};
}
return {
authorized: true,
days: result.days_remaining
};
} catch (error) {
console.error(`[账户验证] 检查授权状态失败 (${sn_code}):`, error);
return { authorized: false, reason: '授权检查失败' };
}
}
/**
* 检查设备是否在线
* @param {string} sn_code - 设备序列号
* @param {number} offlineThreshold - 离线阈值(毫秒)
* @returns {{online: boolean, lastHeartbeat?: number, reason?: string}}
*/
checkOnline(sn_code, offlineThreshold = 3 * 60 * 1000) {
const device = deviceManager.devices.get(sn_code);
if (!device) {
return { online: false, reason: '设备从未发送心跳' };
}
const now = Date.now();
const lastHeartbeat = device.lastHeartbeat || 0;
const elapsed = now - lastHeartbeat;
if (elapsed > offlineThreshold) {
const minutes = Math.round(elapsed / (60 * 1000));
return {
online: false,
lastHeartbeat,
reason: `设备离线(最后心跳: ${minutes}分钟前)`
};
}
if (!device.isOnline) {
return { online: false, lastHeartbeat, reason: '设备标记为离线' };
}
return { online: true, lastHeartbeat };
}
/**
* 综合验证(启用 + 授权 + 在线)
* @param {string} sn_code - 设备序列号
* @param {object} options - 验证选项
* @param {boolean} options.checkEnabled - 是否检查启用状态(默认 true
* @param {boolean} options.checkAuth - 是否检查授权(默认 true
* @param {boolean} options.checkOnline - 是否检查在线(默认 true
* @param {number} options.offlineThreshold - 离线阈值(默认 3分钟
* @returns {Promise<{valid: boolean, reason?: string, details?: object}>}
*/
async validate(sn_code, options = {}) {
const {
checkEnabled = true,
checkAuth = true,
checkOnline = true,
offlineThreshold = 3 * 60 * 1000
} = options;
const details = {};
// 检查启用状态
if (checkEnabled) {
const enabledResult = await this.checkEnabled(sn_code);
details.enabled = enabledResult;
if (!enabledResult.enabled) {
return {
valid: false,
reason: enabledResult.reason,
details
};
}
}
// 检查授权状态
if (checkAuth) {
const authResult = await this.checkAuthorization(sn_code);
details.authorization = authResult;
if (!authResult.authorized) {
return {
valid: false,
reason: authResult.reason,
details
};
}
}
// 检查在线状态
if (checkOnline) {
const onlineResult = this.checkOnline(sn_code, offlineThreshold);
details.online = onlineResult;
if (!onlineResult.online) {
return {
valid: false,
reason: onlineResult.reason,
details
};
}
}
return { valid: true, details };
}
/**
* 批量验证多个账户
* @param {string[]} sn_codes - 设备序列号数组
* @param {object} options - 验证选项
* @returns {Promise<{valid: string[], invalid: Array<{sn_code: string, reason: string}>}>}
*/
async validateBatch(sn_codes, options = {}) {
const valid = [];
const invalid = [];
for (const sn_code of sn_codes) {
const result = await this.validate(sn_code, options);
if (result.valid) {
valid.push(sn_code);
} else {
invalid.push({ sn_code, reason: result.reason });
}
}
return { valid, invalid };
}
/**
* 检查账户是否已登录(通过心跳数据)
* @param {string} sn_code - 设备序列号
* @returns {boolean}
*/
checkLoggedIn(sn_code) {
const device = deviceManager.devices.get(sn_code);
return device?.isLoggedIn || false;
}
}
// 导出单例
const accountValidator = new AccountValidator();
module.exports = accountValidator;

View File

@@ -0,0 +1,230 @@
/**
* 配置管理服务
* 统一处理账户配置的解析和验证
*/
class ConfigManager {
/**
* 解析 JSON 配置字符串
* @param {string|object} config - 配置字符串或对象
* @param {object} defaultValue - 默认值
* @returns {object} 解析后的配置对象
*/
static parseConfig(config, defaultValue = {}) {
if (!config) {
return defaultValue;
}
if (typeof config === 'object') {
return { ...defaultValue, ...config };
}
if (typeof config === 'string') {
try {
const parsed = JSON.parse(config);
return { ...defaultValue, ...parsed };
} catch (error) {
console.warn('[配置管理] JSON 解析失败:', error.message);
return defaultValue;
}
}
return defaultValue;
}
/**
* 解析投递配置
* @param {string|object} deliverConfig - 投递配置
* @returns {object} 标准化的投递配置
*/
static parseDeliverConfig(deliverConfig) {
const defaultConfig = {
deliver_interval: 30, // 投递间隔(分钟)
min_salary: 0, // 最低薪资
max_salary: 0, // 最高薪资
page_count: 3, // 搜索页数
max_deliver: 10, // 最大投递数
repeat_deliver_days: 30, // 多少天内已投递过的公司不再投递(与 getRecentDeliveredCompanies 一致)
filter_keywords: [], // 过滤关键词
exclude_keywords: [], // 排除关键词
time_range: null, // 时间范围
priority_weights: null // 优先级权重
};
return this.parseConfig(deliverConfig, defaultConfig);
}
/**
* 解析搜索配置
* @param {string|object} searchConfig - 搜索配置
* @returns {object} 标准化的搜索配置
*/
static parseSearchConfig(searchConfig) {
const defaultConfig = {
search_interval: 60, // 搜索间隔(分钟)
page_count: 3, // 搜索页数
keywords: [], // 搜索关键词
exclude_keywords: [], // 排除关键词
time_range: null, // 时间范围
city: '', // 城市
salary_range: '', // 薪资范围
experience: '', // 经验
education: '' // 学历
};
return this.parseConfig(searchConfig, defaultConfig);
}
/**
* 解析沟通配置
* @param {string|object} chatStrategy - 沟通策略
* @returns {object} 标准化的沟通配置
*/
static parseChatStrategy(chatStrategy) {
const defaultConfig = {
chat_interval: 30, // 沟通间隔(分钟)
auto_reply: false, // 是否自动回复
reply_template: '', // 回复模板
time_range: null // 时间范围
};
return this.parseConfig(chatStrategy, defaultConfig);
}
/**
* 解析活跃配置
* @param {string|object} activeStrategy - 活跃策略
* @returns {object} 标准化的活跃配置
*/
static parseActiveStrategy(activeStrategy) {
const defaultConfig = {
active_interval: 120, // 活跃间隔(分钟)
actions: ['view_jobs'], // 活跃动作
time_range: null // 时间范围
};
return this.parseConfig(activeStrategy, defaultConfig);
}
/**
* 获取优先级权重配置
* @param {object} config - 投递配置
* @returns {object} 优先级权重
*/
static getPriorityWeights(config) {
const defaultWeights = {
salary: 0.4, // 薪资匹配度
keyword: 0.3, // 关键词匹配度
company: 0.2, // 公司活跃度
distance: 0.1 // 距离(未来)
};
if (!config.priority_weights) {
return defaultWeights;
}
return { ...defaultWeights, ...config.priority_weights };
}
/**
* 获取排除关键词列表
* @param {object} config - 配置对象
* @returns {string[]} 排除关键词数组
*/
static getExcludeKeywords(config) {
if (!config.exclude_keywords) {
return [];
}
if (Array.isArray(config.exclude_keywords)) {
return config.exclude_keywords.filter(k => k && k.trim());
}
if (typeof config.exclude_keywords === 'string') {
return config.exclude_keywords
.split(/[,,、]/)
.map(k => k.trim())
.filter(k => k);
}
return [];
}
/**
* 获取过滤关键词列表
* @param {object} config - 配置对象
* @returns {string[]} 过滤关键词数组
*/
static getFilterKeywords(config) {
if (!config.filter_keywords) {
return [];
}
if (Array.isArray(config.filter_keywords)) {
return config.filter_keywords.filter(k => k && k.trim());
}
if (typeof config.filter_keywords === 'string') {
return config.filter_keywords
.split(/[,,、]/)
.map(k => k.trim())
.filter(k => k);
}
return [];
}
/**
* 获取薪资范围
* @param {object} config - 配置对象
* @returns {{min: number, max: number}} 薪资范围
*/
static getSalaryRange(config) {
return {
min: parseInt(config.min_salary) || 0,
max: parseInt(config.max_salary) || 0
};
}
/**
* 获取时间范围
* @param {object} config - 配置对象
* @returns {object|null} 时间范围配置
*/
static getTimeRange(config) {
return config.time_range || null;
}
/**
* 验证配置完整性
* @param {object} config - 配置对象
* @param {string[]} requiredFields - 必需字段
* @returns {{valid: boolean, missing?: string[]}} 验证结果
*/
static validateConfig(config, requiredFields = []) {
const missing = [];
for (const field of requiredFields) {
if (config[field] === undefined || config[field] === null) {
missing.push(field);
}
}
if (missing.length > 0) {
return { valid: false, missing };
}
return { valid: true };
}
/**
* 合并配置(用于覆盖默认配置)
* @param {object} defaultConfig - 默认配置
* @param {object} userConfig - 用户配置
* @returns {object} 合并后的配置
*/
static mergeConfig(defaultConfig, userConfig) {
return { ...defaultConfig, ...userConfig };
}
}
module.exports = ConfigManager;

View File

@@ -0,0 +1,479 @@
const SalaryParser = require('../utils/salaryParser');
const KeywordMatcher = require('../utils/keywordMatcher');
const db = require('../../dbProxy');
/**
* 职位过滤引擎schedule 自动投递用)
* 本文件集中:过滤(薪资/关键词/活跃度/去重)+ 评分(权重分+关键词奖励)+ 按分数阈值筛。
* 自动投递只需调 filterAndScoreJobsForDeliver 一个方法。
*/
class JobFilterEngine {
getJobKey(job) {
return String(job.id || job.jobId || `${job.companyName || ''}|${job.jobTitle || ''}`);
}
getRemovedTitles(beforeJobs, afterJobs, limit = 5) {
const keptKeySet = new Set(afterJobs.map((job) => this.getJobKey(job)));
return beforeJobs
.filter((job) => !keptKeySet.has(this.getJobKey(job)))
.map((job) => job.jobTitle || '')
.filter(Boolean)
.slice(0, limit);
}
/**
* 过滤职位列表(薪资 → 标题须含词 → 关键词 → 活跃度 → 去重)
* @param {Array} jobs - 职位列表
* @param {object} config - 过滤配置
* @param {object} resumeInfo - 简历信息(未使用,兼容签名)
* @returns {Promise<Array>} 过滤后的职位列表
*/
async filterJobs(jobs, config) {
if (!jobs || jobs.length === 0) {
return [];
}
let filtered = [...jobs];
// 1. 薪资过滤
const beforeSalaryJobs = [...filtered];
filtered = this.filterBySalary(filtered, config);
const salaryRemoved = beforeSalaryJobs.length - filtered.length;
if (salaryRemoved > 0) {
const removedTitles = this.getRemovedTitles(beforeSalaryJobs, filtered);
console.log(`[jobFilterEngine] 步骤1-薪资过滤: 范围=${config.min_salary ?? 0}-${config.max_salary ?? 0}K 剔除标题=${removedTitles.join(' | ') || '无'}`);
}
// 2. 职位标题须包含job_types.titleIncludeKeywords仅 jobTitle与 commonSkills 无关)
const beforeTitleFilterJobs = [...filtered];
filtered = this.filterByTitleIncludeKeywords(filtered, config);
const titleKwRemoved = beforeTitleFilterJobs.length - filtered.length;
if (titleKwRemoved > 0) {
const removedTitles = this.getRemovedTitles(beforeTitleFilterJobs, filtered);
console.log(`[jobFilterEngine] 步骤2-标题须含: 关键词=[${(config.title_include_keywords || []).join('、') || '无'}] 剔除标题=${removedTitles.join(' | ') || '无'}`);
}
// 3. 关键词过滤(排除词 + filter_keywords匹配标题与行业等
const beforeKeywordFilterJobs = [...filtered];
filtered = this.filterByKeywords(filtered, config);
const keywordsRemoved = beforeKeywordFilterJobs.length - filtered.length;
if (keywordsRemoved > 0) {
const removedTitles = this.getRemovedTitles(beforeKeywordFilterJobs, filtered);
console.log(`[jobFilterEngine] 步骤3-关键词过滤: 排除=[${(config.exclude_keywords || []).join('、') || '无'}] 包含=[${(config.filter_keywords || []).join('、') || '无'}] 剔除标题=${removedTitles.join(' | ') || '无'}`);
}
// 4. 公司活跃度过滤
if (config.filter_inactive_companies) {
const beforeActivityJobs = [...filtered];
filtered = await this.filterByCompanyActivity(filtered, config.company_active_days || 7);
const activityRemoved = beforeActivityJobs.length - filtered.length;
if (activityRemoved > 0) {
const removedTitles = this.getRemovedTitles(beforeActivityJobs, filtered);
console.log(`[jobFilterEngine] 步骤4-公司活跃度过滤: 剔除标题=${removedTitles.join(' | ') || '无'}`);
}
}
// 5. 去重(同一公司、同一职位名称)
if (config.deduplicate) {
const beforeDedupJobs = [...filtered];
filtered = this.deduplicateJobs(filtered);
const dedupRemoved = beforeDedupJobs.length - filtered.length;
if (dedupRemoved > 0) {
const removedTitles = this.getRemovedTitles(beforeDedupJobs, filtered);
console.log(`[jobFilterEngine] 步骤5-去重: 剔除标题=${removedTitles.join(' | ') || '无'}`);
}
}
const keptTitles = filtered.map((j) => j.jobTitle || '').filter(Boolean).slice(0, 5);
console.log(`[jobFilterEngine] filterJobs 结束: 通过标题=${keptTitles.join(' | ') || '无'}`);
return filtered;
}
/**
* 自动投递用:过滤 + 评分 + 按 60 分阈值筛,一次调用完成(便于阅读与维护)
*/
async filterAndScoreJobsForDeliver(jobs, filterConfig, resume, accountConfig, jobTypeConfig, recentCompanies) {
const scored = [];
const jobDesc = (j) => `${j.companyName || '?'} / ${j.jobTitle || '?'}`;
const { jobFilterService } = require('../../job/services');
console.log(`[jobFilterEngine] filterAndScoreJobsForDeliver 开始,待处理: ${jobs.length}`);
for (const job of jobs) {
if (job.companyName && recentCompanies.has(job.companyName)) {
console.log(`[jobFilterEngine] 已投递公司 剔除: ${jobDesc(job)}`);
continue;
}
const filtered = await this.filterJobs([job], filterConfig, resume);
if (filtered.length === 0) {
console.log(`[jobFilterEngine] 过滤条件不通过 剔除: ${jobDesc(job)}`);
continue;
}
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
job,
resume,
accountConfig,
jobTypeConfig,
accountConfig.is_salary_priority || []
);
const keywordBonus = KeywordMatcher.calculateBonus(
`${job.jobTitle || ''} ${job.companyName || ''} ${job.jobDescription || ''}`,
filterConfig.filter_keywords || [],
{ baseScore: 5, maxBonus: 20 }
);
const finalScore = scoreResult.totalScore + keywordBonus.score;
if (finalScore < 60) {
console.log(`[jobFilterEngine] 评分不足(>=60) 剔除: ${jobDesc(job)} 总分=${finalScore.toFixed(1)}`);
continue;
}
scored.push({
...job,
matchScore: finalScore,
scoreDetails: { ...scoreResult.scores, keywordBonus: keywordBonus.score }
});
}
scored.sort((a, b) => b.matchScore - a.matchScore);
console.log(`[jobFilterEngine] filterAndScoreJobsForDeliver 结束: 原始${jobs.length} 通过${scored.length}`);
return scored;
}
/**
* 按薪资过滤
* @param {Array} jobs - 职位列表
* @param {object} config - 配置
* @returns {Array} 过滤后的职位
*/
filterBySalary(jobs, config) {
const { min_salary = 0, max_salary = 0 } = config;
if (min_salary === 0 && max_salary === 0) {
return jobs; // 无薪资限制
}
return jobs.filter(job => {
const jobSalary = SalaryParser.parse(job.salary || job.salaryDesc || '');
return SalaryParser.isWithinRange(jobSalary, min_salary, max_salary);
});
}
/**
* 职位标题须包含配置中的关键词(命中任意一个即通过),不扫描描述/公司名/commonSkills
* @param {Array} jobs
* @param {object} config
* @returns {Array}
*/
filterByTitleIncludeKeywords(jobs, config) {
const kws = config.title_include_keywords;
if (!Array.isArray(kws) || kws.length === 0) {
return jobs;
}
return jobs.filter((job) => {
const title = `${job.jobTitle || ''}`.toLowerCase();
return kws.some((kw) => {
const k = String(kw || '').toLowerCase().trim();
if (!k) {
return false;
}
return title.includes(k);
});
});
}
/**
* 按关键词过滤
* @param {Array} jobs - 职位列表
* @param {object} config - 配置
* @returns {Array} 过滤后的职位
*/
filterByKeywords(jobs, config) {
const {
exclude_keywords = [],
filter_keywords = []
} = config;
if (exclude_keywords.length === 0 && filter_keywords.length === 0) {
return jobs;
}
return KeywordMatcher.filterJobs(jobs, {
excludeKeywords: exclude_keywords,
filterKeywords: filter_keywords
});
}
/**
* 按公司活跃度过滤
* @param {Array} jobs - 职位列表
* @param {number} activeDays - 活跃天数阈值
* @returns {Promise<Array>} 过滤后的职位
*/
async filterByCompanyActivity(jobs, activeDays = 7) {
try {
const task_status = db.getModel('task_status');
const thresholdDate = new Date(Date.now() - activeDays * 24 * 60 * 60 * 1000);
// 查询近期已投递的公司
const recentCompanies = await task_status.findAll({
where: {
taskType: 'auto_deliver',
status: 'completed',
endTime: {
[db.models.op.gte]: thresholdDate
}
},
attributes: ['result'],
raw: true
});
// 提取公司名称
const deliveredCompanies = new Set();
for (const task of recentCompanies) {
try {
const result = JSON.parse(task.result || '{}');
if (result.deliveredJobs) {
result.deliveredJobs.forEach(job => {
if (job.company) {
deliveredCompanies.add(job.company.toLowerCase());
}
});
}
} catch (e) {
// 忽略解析错误
}
}
// 过滤掉近期已投递的公司
return jobs.filter(job => {
const company = (job.company || job.companyName || '').toLowerCase().trim();
return !deliveredCompanies.has(company);
});
} catch (error) {
console.error('[职位过滤] 公司活跃度过滤失败:', error);
return jobs; // 失败时返回原列表
}
}
/**
* 去重职位
* @param {Array} jobs - 职位列表
* @returns {Array} 去重后的职位
*/
deduplicateJobs(jobs) {
const seen = new Set();
const unique = [];
for (const job of jobs) {
const company = (job.company || job.companyName || '').toLowerCase().trim();
const jobName = (job.jobTitle || '').toLowerCase().trim();
const key = `${company}||${jobName}`;
if (!seen.has(key)) {
seen.add(key);
unique.push(job);
}
}
return unique;
}
/**
* 为职位打分
* @param {Array} jobs - 职位列表
* @param {object} resumeInfo - 简历信息
* @param {object} config - 配置(包含权重)
* @returns {Array} 带分数的职位列表
*/
scoreJobs(jobs, resumeInfo = {}, config = {}) {
const weights = config.priority_weights || {
salary: 0.4,
keyword: 0.3,
company: 0.2,
freshness: 0.1
};
return jobs.map(job => {
const scores = {
salary: this.scoreSalary(job, resumeInfo),
keyword: this.scoreKeywords(job, config),
company: this.scoreCompany(job),
freshness: this.scoreFreshness(job)
};
// 加权总分
const totalScore = (
scores.salary * weights.salary +
scores.keyword * weights.keyword +
scores.company * weights.company +
scores.freshness * weights.freshness
);
return {
...job,
_scores: scores,
_totalScore: totalScore
};
});
}
/**
* 薪资匹配度评分 (0-100)
* @param {object} job - 职位信息
* @param {object} resumeInfo - 简历信息
* @returns {number} 分数
*/
scoreSalary(job, resumeInfo) {
const jobSalary = SalaryParser.parse(job.salary || job.salaryDesc || '');
const expectedSalary = SalaryParser.parse(resumeInfo.expected_salary || '');
if (jobSalary.min === 0 || expectedSalary.min === 0) {
return 50; // 无法判断时返回中性分
}
const matchScore = SalaryParser.calculateMatch(jobSalary, expectedSalary);
return matchScore * 100;
}
/**
* 关键词匹配度评分 (0-100)
* @param {object} job - 职位信息
* @param {object} config - 配置
* @returns {number} 分数
*/
scoreKeywords(job, config) {
const bonusKeywords = config.filter_keywords || [];
if (bonusKeywords.length === 0) {
return 50; // 无关键词时返回中性分
}
const jobText = [
job.jobTitle || '',
job.jobDescription || '',
job.skills || ''
].join(' ');
const bonusResult = KeywordMatcher.calculateBonus(jobText, bonusKeywords, {
baseScore: 10,
maxBonus: 100
});
return Math.min(bonusResult.score, 100);
}
/**
* 公司评分 (0-100)
* @param {object} job - 职位信息
* @returns {number} 分数
*/
scoreCompany(job) {
let score = 50; // 基础分
// 融资阶段加分
const fundingStage = (job.financingStage || job.financing || '').toLowerCase();
const fundingBonus = {
'已上市': 30,
'上市公司': 30,
'd轮': 25,
'c轮': 20,
'b轮': 15,
'a轮': 10,
'天使轮': 5
};
for (const [stage, bonus] of Object.entries(fundingBonus)) {
if (fundingStage.includes(stage.toLowerCase())) {
score += bonus;
break;
}
}
// 公司规模加分
const scale = (job.scale || job.companyScale || '').toLowerCase();
if (scale.includes('10000') || scale.includes('万人')) {
score += 15;
} else if (scale.includes('1000-9999') || scale.includes('千人')) {
score += 10;
} else if (scale.includes('500-999')) {
score += 5;
}
return Math.min(score, 100);
}
/**
* 新鲜度评分 (0-100)
* @param {object} job - 职位信息
* @returns {number} 分数
*/
scoreFreshness(job) {
const publishTime = job.publishTime || job.createTime;
if (!publishTime) {
return 50; // 无时间信息时返回中性分
}
try {
const now = Date.now();
const pubTime = new Date(publishTime).getTime();
const hoursAgo = (now - pubTime) / (1000 * 60 * 60);
// 越新鲜分数越高
if (hoursAgo < 1) return 100;
if (hoursAgo < 24) return 90;
if (hoursAgo < 72) return 70;
if (hoursAgo < 168) return 50; // 一周内
return 30;
} catch (error) {
return 50;
}
}
/**
* 排序职位
* @param {Array} jobs - 职位列表(带分数)
* @param {string} sortBy - 排序方式: score, salary, freshness
* @returns {Array} 排序后的职位
*/
sortJobs(jobs, sortBy = 'score') {
const sorted = [...jobs];
switch (sortBy) {
case 'score':
sorted.sort((a, b) => (b._totalScore || 0) - (a._totalScore || 0));
break;
case 'salary':
sorted.sort((a, b) => {
const salaryA = SalaryParser.parse(a.salary || '');
const salaryB = SalaryParser.parse(b.salary || '');
return (salaryB.max || 0) - (salaryA.max || 0);
});
break;
case 'freshness':
sorted.sort((a, b) => {
const timeA = new Date(a.publishTime || a.createTime || 0).getTime();
const timeB = new Date(b.publishTime || b.createTime || 0).getTime();
return timeB - timeA;
});
break;
default:
// 默认按分数排序
sorted.sort((a, b) => (b._totalScore || 0) - (a._totalScore || 0));
}
return sorted;
}
}
// 导出单例
const jobFilterEngine = new JobFilterEngine();
module.exports = jobFilterEngine;

View File

@@ -0,0 +1,158 @@
/**
* 时间范围验证器
* 检查当前时间是否在指定的时间范围内(支持工作日限制)
*/
class TimeRangeValidator {
/**
* 检查当前时间是否在指定的时间范围内
* @param {object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
* @returns {{allowed: boolean, reason: string}} 检查结果
*/
static checkTimeRange(timeRange) {
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
return { allowed: true, reason: '未配置时间范围' };
}
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute; // 转换为分钟数
// 解析开始时间和结束时间
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
// 检查是否仅工作日(使用宽松比较,兼容字符串和数字)
if (timeRange.workdays_only == 1) {
const dayOfWeek = now.getDay(); // 0=周日, 1=周一, ..., 6=周六
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
}
}
// 检查当前时间是否在时间范围内
if (startTime <= endTime) {
// 正常情况09:00 - 18:00
if (currentTime < startTime || currentTime >= endTime) {
const currentTimeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
return {
allowed: false,
reason: `当前时间 ${currentTimeStr} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
} else {
// 跨天情况22:00 - 06:00
if (currentTime < startTime && currentTime >= endTime) {
const currentTimeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
return {
allowed: false,
reason: `当前时间 ${currentTimeStr} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
}
return { allowed: true, reason: '在允许的时间范围内' };
}
/**
* 检查是否在工作时间内
* @param {string} startTime - 开始时间 '09:00'
* @param {string} endTime - 结束时间 '18:00'
* @returns {boolean}
*/
static isWithinWorkingHours(startTime = '09:00', endTime = '18:00') {
const result = this.checkTimeRange({
start_time: startTime,
end_time: endTime,
workdays_only: 0
});
return result.allowed;
}
/**
* 检查是否是工作日
* @returns {boolean}
*/
static isWorkingDay() {
const dayOfWeek = new Date().getDay();
return dayOfWeek !== 0 && dayOfWeek !== 6; // 非周六周日
}
/**
* 获取下一个可操作时间
* @param {object} timeRange - 时间范围配置
* @returns {Date|null} 下一个可操作时间,如果当前可操作则返回 null
*/
static getNextAvailableTime(timeRange) {
const check = this.checkTimeRange(timeRange);
if (check.allowed) {
return null; // 当前可操作
}
if (!timeRange || !timeRange.start_time) {
return null;
}
const now = new Date();
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
// 如果是工作日限制且当前是周末
if (timeRange.workdays_only == 1) {
const dayOfWeek = now.getDay();
if (dayOfWeek === 0) {
// 周日,下一个可操作时间是周一
const nextTime = new Date(now);
nextTime.setDate(now.getDate() + 1);
nextTime.setHours(startHour, startMinute, 0, 0);
return nextTime;
} else if (dayOfWeek === 6) {
// 周六,下一个可操作时间是下周一
const nextTime = new Date(now);
nextTime.setDate(now.getDate() + 2);
nextTime.setHours(startHour, startMinute, 0, 0);
return nextTime;
}
}
// 计算下一个开始时间
const nextTime = new Date(now);
nextTime.setHours(startHour, startMinute, 0, 0);
// 如果已经过了今天的开始时间,则设置为明天
if (nextTime <= now) {
nextTime.setDate(now.getDate() + 1);
}
return nextTime;
}
/**
* 格式化剩余时间
* @param {object} timeRange - 时间范围配置
* @returns {string} 剩余时间描述
*/
static formatRemainingTime(timeRange) {
const nextTime = this.getNextAvailableTime(timeRange);
if (!nextTime) {
return '当前可操作';
}
const now = Date.now();
const diff = nextTime.getTime() - now;
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 24) {
const days = Math.floor(hours / 24);
return `需要等待 ${days}${hours % 24} 小时`;
} else if (hours > 0) {
return `需要等待 ${hours} 小时 ${minutes} 分钟`;
} else {
return `需要等待 ${minutes} 分钟`;
}
}
}
module.exports = TimeRangeValidator;

View File

@@ -1,803 +1,96 @@
const db = require('../dbProxy.js');
const config = require('./config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const jobFilterService = require('../job/job_filter_service.js');
const { SearchHandler, DeliverHandler, ChatHandler, ActiveHandler } = require('./handlers');
/**
* 任务处理器(简化版)
* 处理各种类型的任务
* 任务处理器工厂(重构版)
* 使用独立的处理器类替代原有的内嵌处理方法
*
* 重构说明:
* - 原 taskHandlers.js: 1045 行,包含所有业务逻辑
* - 新 taskHandlers.js: 95 行,仅作为处理器工厂
* - 业务逻辑已分离到 handlers/ 目录下的独立处理器
*/
class TaskHandlers {
constructor(mqttClient) {
this.mqttClient = mqttClient;
// 初始化各个处理器
this.searchHandler = new SearchHandler(mqttClient);
this.deliverHandler = new DeliverHandler(mqttClient);
this.chatHandler = new ChatHandler(mqttClient);
this.activeHandler = new ActiveHandler(mqttClient);
console.log('[任务处理器] 已初始化所有处理器实例');
}
/**
* 注册任务处理器到任务队列
* @param {object} taskQueue - 任务队列实例
*/
* 注册任务处理器到任务队列
* @param {object} taskQueue - 任务队列实例
*/
register(taskQueue) {
// 自动投递任务
console.log('[任务处理器] 开始注册处理器...');
// 注册自动搜索处理器(唯一搜索任务类型)
taskQueue.registerHandler('auto_search', async (task) => {
return await this.handleAutoSearchTask(task);
});
// 注册自动投递处理器
taskQueue.registerHandler('auto_deliver', async (task) => {
return await this.handleAutoDeliverTask(task);
});
// 自动沟通任务(待实现)
// 注册自动沟通处理器
taskQueue.registerHandler('auto_chat', async (task) => {
return await this.handleAutoChatTask(task);
});
// 自动活跃账号任务(待实现)
// 注册自动活跃账户处理器
taskQueue.registerHandler('auto_active_account', async (task) => {
return await this.handleAutoActiveAccountTask(task);
});
console.log('[任务处理器] 所有处理器已注册完成');
}
/**
* 处理自动搜索任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handleAutoSearchTask(task) {
console.log(`[任务处理器] 调度自动搜索任务 - 设备: ${task.sn_code}`);
return await this.searchHandler.handle(task);
}
/**
* 处理自动投递任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handleAutoDeliverTask(task) {
const { sn_code, taskParams } = task;
const { keyword, platform, pageCount, maxCount, filterRules = {} } = taskParams;
console.log(`[任务处理器] 自动投递任务 - 设备: ${sn_code}, 关键词: ${keyword}`);
// 检查授权状态
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,
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 job_types = db.getModel('job_types');
const apply_records = db.getModel('apply_records');
const Sequelize = require('sequelize');
const { Op } = Sequelize;
// 检查今日投递次数限制
const currentPlatform = platform || 'boss';
const dailyLimit = config.getDailyLimit('apply', currentPlatform);
// 获取今日开始时间00:00:00
const today = new Date();
today.setHours(0, 0, 0, 0);
// 查询今日已投递次数
const todayApplyCount = await apply_records.count({
where: {
sn_code: sn_code,
platform: currentPlatform,
applyTime: {
[Op.gte]: today
}
}
});
console.log(`[任务处理器] 今日已投递 ${todayApplyCount} 次,限制: ${dailyLimit}`);
// 如果已达到每日投递上限,则跳过
if (todayApplyCount >= dailyLimit) {
console.log(`[任务处理器] 已达到每日投递上限(${dailyLimit}次),跳过投递`);
return {
success: false,
deliveredCount: 0,
message: `已达到每日投递上限(${dailyLimit}次),今日已投递 ${todayApplyCount}`
};
}
// 计算本次可投递的数量(不超过剩余限额)
const remainingQuota = dailyLimit - todayApplyCount;
const actualMaxCount = Math.min(maxCount || 10, remainingQuota);
if (actualMaxCount < (maxCount || 10)) {
console.log(`[任务处理器] 受每日投递上限限制,本次最多投递 ${actualMaxCount} 个职位(剩余限额: ${remainingQuota}`);
}
// 1. 检查并获取在线简历如果2小时内没有获取
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
let resume = await resume_info.findOne({
where: {
sn_code,
platform: platform || 'boss',
isActive: true
},
order: [['last_modify_time', 'DESC']]
});
const needRefreshResume = !resume ||
!resume.last_modify_time ||
new Date(resume.last_modify_time) < twoHoursAgo;
if (needRefreshResume) {
console.log(`[任务处理器] 简历超过2小时未更新重新获取在线简历`);
try {
// 通过 command 系统获取在线简历,而不是直接调用 jobManager
const getResumeCommand = {
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code, platform: platform || 'boss' }),
priority: config.getTaskPriority('get_resume') || 5
};
await command.executeCommands(task.id, [getResumeCommand], this.mqttClient);
// 重新查询简历
resume = await resume_info.findOne({
where: {
sn_code,
platform: platform || 'boss',
isActive: true
},
order: [['last_modify_time', 'DESC']]
});
} catch (error) {
console.warn(`[任务处理器] 获取在线简历失败,使用已有简历:`, error.message);
}
}
if (!resume) {
console.log(`[任务处理器] 未找到简历信息,无法进行自动投递`);
return {
success: false,
deliveredCount: 0,
message: '未找到简历信息'
};
}
// 2. 获取账号配置和职位类型配置
const account = await pla_account.findOne({
where: { sn_code, platform_type: platform || 'boss' }
});
if (!account) {
console.log(`[任务处理器] 未找到账号配置`);
return {
success: false,
deliveredCount: 0,
message: '未找到账号配置'
};
}
const accountConfig = account.toJSON();
const resumeInfo = resume.toJSON();
// 检查投递时间范围
if (accountConfig.deliver_config) {
const deliverConfig = typeof accountConfig.deliver_config === 'string'
? JSON.parse(accountConfig.deliver_config)
: accountConfig.deliver_config;
if (deliverConfig.time_range) {
const timeCheck = this.checkTimeRange(deliverConfig.time_range);
if (!timeCheck.allowed) {
console.log(`[任务处理器] 自动投递任务 - ${timeCheck.reason}`);
return {
success: true,
deliveredCount: 0,
message: timeCheck.reason
};
}
}
}
// 获取职位类型配置
let jobTypeConfig = null;
if (accountConfig.job_type_id) {
const jobType = await job_types.findByPk(accountConfig.job_type_id);
if (jobType) {
jobTypeConfig = jobType.toJSON();
}
}
// 获取优先级权重配置
let priorityWeights = accountConfig.is_salary_priority;
if (!Array.isArray(priorityWeights) || priorityWeights.length === 0) {
priorityWeights = [
{ key: "distance", weight: 50 },
{ key: "salary", weight: 20 },
{ key: "work_years", weight: 10 },
{ key: "education", weight: 20 }
];
}
// 3. 先获取职位列表
const getJobListCommand = {
command_type: 'getJobList',
command_name: '获取职位列表',
command_params: JSON.stringify({
sn_code: sn_code,
keyword: keyword || accountConfig.keyword || '',
platform: platform || 'boss',
pageCount: pageCount || 3
}),
priority: config.getTaskPriority('search_jobs') || 5
};
await command.executeCommands(task.id, [getJobListCommand], this.mqttClient);
// 4. 从数据库获取待投递的职位
const pendingJobs = await job_postings.findAll({
where: {
sn_code: sn_code,
platform: platform || 'boss',
applyStatus: 'pending'
},
order: [['create_time', 'DESC']],
limit: actualMaxCount * 3 // 获取更多职位用于筛选(受每日投递上限限制)
});
if (!pendingJobs || pendingJobs.length === 0) {
console.log(`[任务处理器] 没有待投递的职位`);
return {
success: true,
deliveredCount: 0,
message: '没有待投递的职位'
};
}
// 5. 根据简历信息、职位类型配置和权重配置进行评分和过滤
const scoredJobs = [];
// 合并排除关键词:从职位类型配置和任务参数中获取
const jobTypeExcludeKeywords = jobTypeConfig && jobTypeConfig.excludeKeywords
? (typeof jobTypeConfig.excludeKeywords === 'string'
? JSON.parse(jobTypeConfig.excludeKeywords)
: jobTypeConfig.excludeKeywords)
: [];
let taskExcludeKeywords = filterRules.excludeKeywords || [];
// 如果 filterRules 中没有,尝试从 accountConfig.deliver_config 获取
if ((!taskExcludeKeywords || taskExcludeKeywords.length === 0) && accountConfig.deliver_config) {
const deliverConfig = typeof accountConfig.deliver_config === 'string'
? JSON.parse(accountConfig.deliver_config)
: accountConfig.deliver_config;
if (deliverConfig.exclude_keywords) {
taskExcludeKeywords = Array.isArray(deliverConfig.exclude_keywords)
? deliverConfig.exclude_keywords
: (typeof deliverConfig.exclude_keywords === 'string'
? JSON.parse(deliverConfig.exclude_keywords)
: []);
}
}
const excludeKeywords = [...jobTypeExcludeKeywords, ...taskExcludeKeywords];
// 获取过滤关键词(用于优先匹配或白名单过滤)
let filterKeywords = filterRules.keywords || [];
// 如果 filterRules 中没有,尝试从 accountConfig.deliver_config 获取
if ((!filterKeywords || filterKeywords.length === 0) && accountConfig.deliver_config) {
const deliverConfig = typeof accountConfig.deliver_config === 'string'
? JSON.parse(accountConfig.deliver_config)
: accountConfig.deliver_config;
if (deliverConfig.filter_keywords) {
filterKeywords = Array.isArray(deliverConfig.filter_keywords)
? deliverConfig.filter_keywords
: (typeof deliverConfig.filter_keywords === 'string'
? JSON.parse(deliverConfig.filter_keywords)
: []);
}
}
console.log(`[任务处理器] 过滤关键词配置 - 包含关键词: ${JSON.stringify(filterKeywords)}, 排除关键词: ${JSON.stringify(excludeKeywords)}`);
// 获取薪资范围过滤(优先从 filterRules如果没有则从 accountConfig.deliver_config 获取)
let minSalary = filterRules.minSalary || 0;
let maxSalary = filterRules.maxSalary || 0;
// 如果 filterRules 中没有,尝试从 accountConfig.deliver_config 获取
if (minSalary === 0 && maxSalary === 0 && accountConfig.deliver_config) {
const deliverConfig = typeof accountConfig.deliver_config === 'string'
? JSON.parse(accountConfig.deliver_config)
: accountConfig.deliver_config;
minSalary = deliverConfig.min_salary || 0;
maxSalary = deliverConfig.max_salary || 0;
}
console.log(`[任务处理器] 薪资过滤配置 - 最低: ${minSalary}元, 最高: ${maxSalary}`);
// 获取一个月内已投递的公司列表(用于过滤)
// 注意apply_records 和 Sequelize 已在方法开头定义
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
const recentApplies = await apply_records.findAll({
where: {
sn_code: sn_code,
applyTime: {
[Sequelize.Op.gte]: oneMonthAgo
}
},
attributes: ['companyName'],
group: ['companyName']
});
const recentCompanyNames = new Set(recentApplies.map(apply => apply.companyName).filter(Boolean));
for (const job of pendingJobs) {
const jobData = job.toJSON ? job.toJSON() : job;
// 薪资范围过滤
if (minSalary > 0 || maxSalary > 0) {
// 解析职位薪资字符串(如 "20-30K"
const jobSalaryRange = this.parseSalaryRange(jobData.salary || '');
const jobSalaryMin = jobSalaryRange.min || 0;
const jobSalaryMax = jobSalaryRange.max || 0;
// 如果职位没有薪资信息,跳过
if (jobSalaryMin === 0 && jobSalaryMax === 0) {
console.log(`[任务处理器] 跳过无薪资信息的职位: ${jobData.jobTitle} @ ${jobData.companyName}`);
continue;
}
// 如果职位薪资范围与过滤范围没有交集,则跳过
if (minSalary > 0 && jobSalaryMax > 0 && minSalary > jobSalaryMax) {
console.log(`[任务处理器] 跳过薪资过低职位: ${jobData.jobTitle} @ ${jobData.companyName}, 职位薪资: ${jobData.salary}, 要求最低: ${minSalary}`);
continue;
}
if (maxSalary > 0 && jobSalaryMin > 0 && maxSalary < jobSalaryMin) {
console.log(`[任务处理器] 跳过薪资过高职位: ${jobData.jobTitle} @ ${jobData.companyName}, 职位薪资: ${jobData.salary}, 要求最高: ${maxSalary}`);
continue;
}
}
// 如果配置了简历期望薪资,也要与职位薪资进行比较
if (resumeInfo && resumeInfo.expectedSalary) {
const expectedSalaryRange = this.parseExpectedSalary(resumeInfo.expectedSalary);
if (expectedSalaryRange) {
const jobSalaryRange = this.parseSalaryRange(jobData.salary || '');
const jobSalaryMin = jobSalaryRange.min || 0;
const jobSalaryMax = jobSalaryRange.max || 0;
// 如果职位薪资明显低于期望薪资范围,跳过
// 期望薪资是 "20-30K",职位薪资应该至少接近或高于期望薪资的最低值
if (jobSalaryMax > 0 && expectedSalaryRange.min > 0 && jobSalaryMax < expectedSalaryRange.min * 0.8) {
console.log(`[任务处理器] 跳过薪资低于期望的职位: ${jobData.jobTitle} @ ${jobData.companyName}, 职位薪资: ${jobData.salary}, 期望薪资: ${resumeInfo.expectedSalary}`);
continue;
}
}
}
// 排除关键词过滤
if (Array.isArray(excludeKeywords) && excludeKeywords.length > 0) {
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase();
const matchedExcludeKeywords = excludeKeywords.filter(kw => {
const keyword = kw ? kw.toLowerCase().trim() : '';
return keyword && jobText.includes(keyword);
});
if (matchedExcludeKeywords.length > 0) {
console.log(`[任务处理器] 跳过包含排除关键词的职位: ${jobData.jobTitle} @ ${jobData.companyName}, 匹配: ${matchedExcludeKeywords.join(', ')}`);
continue;
}
}
// 过滤关键词(白名单模式):如果设置了过滤关键词,只投递包含这些关键词的职位
if (Array.isArray(filterKeywords) && filterKeywords.length > 0) {
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase();
const matchedKeywords = filterKeywords.filter(kw => {
const keyword = kw ? kw.toLowerCase().trim() : '';
return keyword && jobText.includes(keyword);
});
if (matchedKeywords.length === 0) {
// 如果没有匹配到任何过滤关键词,跳过该职位(白名单模式)
console.log(`[任务处理器] 跳过未匹配过滤关键词的职位: ${jobData.jobTitle} @ ${jobData.companyName}, 过滤关键词: ${filterKeywords.join(', ')}`);
continue;
} else {
console.log(`[任务处理器] 职位匹配过滤关键词: ${jobData.jobTitle} @ ${jobData.companyName}, 匹配: ${matchedKeywords.join(', ')}`);
}
}
// 检查该公司是否在一个月内已投递过
if (jobData.companyName && recentCompanyNames.has(jobData.companyName)) {
console.log(`[任务处理器] 跳过一个月内已投递的公司: ${jobData.companyName}`);
continue;
}
// 使用 job_filter_service 计算评分
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
jobData,
resumeInfo,
accountConfig,
jobTypeConfig,
priorityWeights
);
// 如果配置了过滤关键词,给包含这些关键词的职位加分(额外奖励)
let keywordBonus = 0;
if (Array.isArray(filterKeywords) && filterKeywords.length > 0) {
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase();
const matchedKeywords = filterKeywords.filter(kw => {
const keyword = kw ? kw.toLowerCase().trim() : '';
return keyword && jobText.includes(keyword);
});
if (matchedKeywords.length > 0) {
// 每匹配一个关键词加5分最多加20分
keywordBonus = Math.min(matchedKeywords.length * 5, 20);
}
}
const finalScore = scoreResult.totalScore + keywordBonus;
// 只保留总分 >= 60 的职位
if (finalScore >= 60) {
scoredJobs.push({
...jobData,
matchScore: finalScore,
scoreDetails: {
...scoreResult.scores,
keywordBonus: keywordBonus
}
});
}
}
// 按总分降序排序
scoredJobs.sort((a, b) => b.matchScore - a.matchScore);
// 取前 actualMaxCount 个职位(受每日投递上限限制)
const jobsToDeliver = scoredJobs.slice(0, actualMaxCount);
console.log(`[任务处理器] 职位评分完成,共 ${pendingJobs.length} 个职位,评分后 ${scoredJobs.length} 个符合条件,将投递 ${jobsToDeliver.length}`);
if (jobsToDeliver.length === 0) {
return {
success: true,
deliveredCount: 0,
message: '没有符合条件的职位'
};
}
// 6. 为每个职位创建一条独立的投递指令
const deliverCommands = [];
for (const jobData of jobsToDeliver) {
console.log(`[任务处理器] 准备投递职位: ${jobData.jobTitle} @ ${jobData.companyName}, 评分: ${jobData.matchScore}`, jobData.scoreDetails);
deliverCommands.push({
command_type: 'applyJob',
command_name: `投递简历 - ${jobData.jobTitle} @ ${jobData.companyName} (评分:${jobData.matchScore})`,
command_params: JSON.stringify({
sn_code: sn_code,
platform: platform || 'boss',
jobId: jobData.jobId,
encryptBossId: jobData.encryptBossId || '',
securityId: jobData.securityId || '',
brandName: jobData.companyName,
jobTitle: jobData.jobTitle,
companyName: jobData.companyName,
matchScore: jobData.matchScore,
scoreDetails: jobData.scoreDetails
}),
priority: config.getTaskPriority('apply') || 6
});
}
// 7. 执行所有投递指令
const result = await command.executeCommands(task.id, deliverCommands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
console.log(`[任务处理器] 自动投递任务完成 - 设备: ${sn_code}, 创建了 ${deliverCommands.length} 条投递指令, 耗时: ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
console.error(`[任务处理器] 自动投递任务失败 - 设备: ${sn_code}:`, error);
throw error;
}
console.log(`[任务处理器] 调度自动投递任务 - 设备: ${task.sn_code}`);
return await this.deliverHandler.handle(task);
}
/**
* 检查当前时间是否在指定的时间范围内
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
* @returns {Object} {allowed: boolean, reason: string}
*/
checkTimeRange(timeRange) {
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
return { allowed: true, reason: '未配置时间范围' };
}
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute; // 转换为分钟数
// 解析开始时间和结束时间
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
// 检查是否仅工作日(使用宽松比较,兼容字符串和数字)
if (timeRange.workdays_only == 1) { // 使用 == 而不是 ===
const dayOfWeek = now.getDay(); // 0=周日, 1=周一, ..., 6=周六
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
}
}
// 检查当前时间是否在时间范围内
if (startTime <= endTime) {
// 正常情况09:00 - 18:00
if (currentTime < startTime || currentTime >= endTime) {
return { allowed: false, reason: `当前时间 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` };
}
} else {
// 跨天情况22:00 - 06:00
if (currentTime < startTime && currentTime >= endTime) {
return { allowed: false, reason: `当前时间 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` };
}
}
return { allowed: true, reason: '在允许的时间范围内' };
}
/**
* 处理自动沟通任务(待实现)
* 功能自动与HR进行沟通回复消息等
* 处理自动沟通任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handleAutoChatTask(task) {
const { sn_code, taskParams } = task;
console.log(`[任务处理器] 自动沟通任务 - 设备: ${sn_code}`);
// 检查授权状态
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,
chatCount: 0,
message: authCheck.message
};
}
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
// 获取账号配置
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: { sn_code: sn_code }
});
if (!account) {
throw new Error(`账号不存在: ${sn_code}`);
}
const accountData = account.toJSON();
// 检查是否开启自动沟通
if (!accountData.auto_chat) {
console.log(`[任务处理器] 设备 ${sn_code} 未开启自动沟通`);
return {
success: true,
message: '未开启自动沟通',
chatCount: 0
};
}
// 解析沟通策略配置
let chatStrategy = {};
if (accountData.chat_strategy) {
chatStrategy = typeof accountData.chat_strategy === 'string'
? JSON.parse(accountData.chat_strategy)
: accountData.chat_strategy;
}
// 检查沟通时间范围
if (chatStrategy.time_range) {
const timeCheck = this.checkTimeRange(chatStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[任务处理器] 自动沟通任务 - ${timeCheck.reason}`);
return {
success: true,
message: timeCheck.reason,
chatCount: 0
};
}
}
// TODO: 实现自动沟通逻辑
// 1. 获取待回复的聊天列表
// 2. 根据消息内容生成回复
// 3. 发送回复消息
// 4. 记录沟通结果
console.log(`[任务处理器] 自动沟通任务 - 逻辑待实现`);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return {
success: true,
message: '自动沟通任务框架已就绪,逻辑待实现',
chatCount: 0
};
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
console.log(`[任务处理器] 调度自动沟通任务 - 设备: ${task.sn_code}`);
return await this.chatHandler.handle(task);
}
/**
* 处理自动活跃账任务(待实现)
* 功能:自动执行一些操作来保持账号活跃度,如浏览职位、搜索等
* 处理自动活跃账任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
async handleAutoActiveAccountTask(task) {
const { sn_code, taskParams } = task;
console.log(`[任务处理器] 自动活跃账号任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
// TODO: 实现自动活跃账号逻辑
// 1. 随机搜索一些职位
// 2. 浏览职位详情
// 3. 查看公司信息
// 4. 执行一些模拟用户行为
console.log(`[任务处理器] 自动活跃账号任务 - 逻辑待实现`);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return {
success: true,
message: '自动活跃账号任务框架已就绪,逻辑待实现',
actionCount: 0
};
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 解析职位薪资范围
* @param {string} salaryDesc - 薪资描述(如 "20-30K"、"30-40K·18薪"、"5000-6000元/月"
* @returns {object} 薪资范围 { min, max },单位:元
*/
parseSalaryRange(salaryDesc) {
if (!salaryDesc) return { min: 0, max: 0 };
// 1. 匹配K格式40-60K, 30-40K·18薪忽略后面的薪数
const kMatch = salaryDesc.match(/(\d+)[-~](\d+)[kK千]/);
if (kMatch) {
return {
min: parseInt(kMatch[1]) * 1000,
max: parseInt(kMatch[2]) * 1000
};
}
// 2. 匹配单个K值25K
const singleKMatch = salaryDesc.match(/(\d+)[kK千]/);
if (singleKMatch) {
const value = parseInt(singleKMatch[1]) * 1000;
return { min: value, max: value };
}
// 3. 匹配元/月格式5000-6000元/月
const yuanMatch = salaryDesc.match(/(\d+)[-~](\d+)[元万]/);
if (yuanMatch) {
const min = parseInt(yuanMatch[1]);
const max = parseInt(yuanMatch[2]);
// 判断单位(万或元)
if (salaryDesc.includes('万')) {
return {
min: min * 10000,
max: max * 10000
};
} else {
return { min, max };
}
}
// 4. 匹配单个元/月值5000元/月
const singleYuanMatch = salaryDesc.match(/(\d+)[元万]/);
if (singleYuanMatch) {
const value = parseInt(singleYuanMatch[1]);
if (salaryDesc.includes('万')) {
return { min: value * 10000, max: value * 10000 };
} else {
return { min: value, max: value };
}
}
// 5. 匹配纯数字格式20000-30000
const numMatch = salaryDesc.match(/(\d+)[-~](\d+)/);
if (numMatch) {
return {
min: parseInt(numMatch[1]),
max: parseInt(numMatch[2])
};
}
return { min: 0, max: 0 };
}
/**
* 解析期望薪资范围
* @param {string} expectedSalary - 期望薪资描述(如 "20-30K"、"5000-6000元/月"
* @returns {object|null} 期望薪资范围 { min, max },单位:元
*/
parseExpectedSalary(expectedSalary) {
if (!expectedSalary) return null;
// 1. 匹配K格式20-30K
const kMatch = expectedSalary.match(/(\d+)[-~](\d+)[kK千]/);
if (kMatch) {
return {
min: parseInt(kMatch[1]) * 1000,
max: parseInt(kMatch[2]) * 1000
};
}
// 2. 匹配单个K值25K
const singleKMatch = expectedSalary.match(/(\d+)[kK千]/);
if (singleKMatch) {
const value = parseInt(singleKMatch[1]) * 1000;
return { min: value, max: value };
}
// 3. 匹配元/月格式5000-6000元/月
const yuanMatch = expectedSalary.match(/(\d+)[-~](\d+)[元万]/);
if (yuanMatch) {
const min = parseInt(yuanMatch[1]);
const max = parseInt(yuanMatch[2]);
// 判断单位(万或元)
if (expectedSalary.includes('万')) {
return {
min: min * 10000,
max: max * 10000
};
} else {
return { min, max };
}
}
// 4. 匹配单个元/月值5000元/月
const singleYuanMatch = expectedSalary.match(/(\d+)[元万]/);
if (singleYuanMatch) {
const value = parseInt(singleYuanMatch[1]);
if (expectedSalary.includes('万')) {
return { min: value * 10000, max: value * 10000 };
} else {
return { min: value, max: value };
}
}
// 5. 匹配纯数字格式20000-30000
const numMatch = expectedSalary.match(/(\d+)[-~](\d+)/);
if (numMatch) {
return {
min: parseInt(numMatch[1]),
max: parseInt(numMatch[2])
};
}
return null;
console.log(`[任务处理器] 调度自动活跃任务 - 设备: ${task.sn_code}`);
return await this.activeHandler.handle(task);
}
}
module.exports = TaskHandlers;

View File

@@ -0,0 +1,182 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
/**
* 自动活跃账号任务
* 定期浏览职位、刷新简历、查看通知等,保持账号活跃度
*/
class AutoActiveTask extends BaseTask {
constructor() {
super('auto_active_account', {
defaultInterval: 120, // 默认2小时
defaultPriority: 5, // 较低优先级
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_deliver', // 投递任务
'auto_search' // 搜索任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.platform) {
return {
valid: false,
reason: '缺少必要参数: platform'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动活跃账号 - ${params.platform || 'boss'}`;
}
/**
* 执行自动活跃任务
*/
async execute(sn_code, params) {
console.log(`[自动活跃] 设备 ${sn_code} 开始执行活跃任务`);
const actions = [];
// 1. 浏览推荐职位
actions.push({
action: 'browse_jobs',
count: Math.floor(Math.random() * 5) + 3 // 3-7个职位
});
// 2. 刷新简历
actions.push({
action: 'refresh_resume',
success: true
});
// 3. 查看通知
actions.push({
action: 'check_notifications',
count: Math.floor(Math.random() * 3)
});
// 4. 浏览公司主页
actions.push({
action: 'browse_companies',
count: Math.floor(Math.random() * 3) + 1
});
console.log(`[自动活跃] 设备 ${sn_code} 完成 ${actions.length} 个活跃操作`);
return {
success: true,
actions: actions,
message: `完成 ${actions.length} 个活跃操作`
};
}
/**
* 添加活跃任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动活跃] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
if (!account) {
console.log(`[自动活跃] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
const accountData = account.toJSON();
// 2. 检查是否开启了自动活跃
if (!accountData.auto_active) {
console.log(`[自动活跃] 设备 ${sn_code} 未开启自动活跃`);
return { success: false, reason: '未开启自动活跃' };
}
// 3. 获取活跃策略配置
let activeStrategy = {};
if (accountData.active_strategy) {
activeStrategy = typeof accountData.active_strategy === 'string'
? JSON.parse(accountData.active_strategy)
: accountData.active_strategy;
}
// 4. 检查时间范围
if (activeStrategy.time_range) {
const timeCheck = this.checkTimeRange(activeStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动活跃] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查活跃间隔
const active_interval = activeStrategy.active_interval || this.config.defaultInterval;
const intervalCheck = await this.checkExecutionInterval(sn_code, active_interval);
if (!intervalCheck.allowed) {
console.log(`[自动活跃] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
platform: accountData.platform_type || 'boss',
actions: activeStrategy.actions || ['browse_jobs', 'refresh_resume', 'check_notifications'],
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动活跃] 已为设备 ${sn_code} 添加活跃任务,间隔: ${active_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动活跃] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoActiveTask();

View File

@@ -0,0 +1,181 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
/**
* 自动沟通任务
* 自动回复HR消息,保持活跃度
*/
class AutoChatTask extends BaseTask {
constructor() {
super('auto_chat', {
defaultInterval: 15, // 默认15分钟
defaultPriority: 6, // 中等优先级
requiresLogin: true, // 需要登录
conflictsWith: [] // 不与其他任务冲突(可以在投递/搜索间隙执行)
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.platform) {
return {
valid: false,
reason: '缺少必要参数: platform'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动沟通 - ${params.name || '默认'}`;
}
/**
* 执行自动沟通任务
*/
async execute(sn_code, params) {
console.log(`[自动沟通] 设备 ${sn_code} 开始执行沟通任务`);
// 1. 获取未读消息列表
const unreadMessages = await this.getUnreadMessages(sn_code, params.platform);
if (!unreadMessages || unreadMessages.length === 0) {
console.log(`[自动沟通] 设备 ${sn_code} 没有未读消息`);
return {
success: true,
repliedCount: 0,
message: '没有未读消息'
};
}
console.log(`[自动沟通] 设备 ${sn_code} 找到 ${unreadMessages.length} 条未读消息`);
// 2. 智能回复(这里需要调用实际的AI回复逻辑)
const replyResult = {
success: true,
repliedCount: unreadMessages.length,
messages: unreadMessages.map(m => ({
id: m.id,
from: m.hr_name,
company: m.company_name
}))
};
return replyResult;
}
/**
* 获取未读消息
*/
async getUnreadMessages(sn_code, platform) {
// TODO: 从数据库或缓存获取未读消息
// 这里返回空数组作为示例
return [];
}
/**
* 添加沟通任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动沟通] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
if (!account) {
console.log(`[自动沟通] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
const accountData = account.toJSON();
// 2. 检查是否开启了自动沟通
if (!accountData.auto_chat) {
console.log(`[自动沟通] 设备 ${sn_code} 未开启自动沟通`);
return { success: false, reason: '未开启自动沟通' };
}
// 3. 获取沟通策略配置
let chatStrategy = {};
if (accountData.chat_strategy) {
chatStrategy = typeof accountData.chat_strategy === 'string'
? JSON.parse(accountData.chat_strategy)
: accountData.chat_strategy;
}
// 4. 检查时间范围
if (chatStrategy.time_range) {
const timeCheck = this.checkTimeRange(chatStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动沟通] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查沟通间隔
const chat_interval = chatStrategy.chat_interval || this.config.defaultInterval;
const intervalCheck = await this.checkExecutionInterval(sn_code, chat_interval);
if (!intervalCheck.allowed) {
console.log(`[自动沟通] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
platform: accountData.platform_type || 'boss',
name: accountData.name || '默认',
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动沟通] 已为设备 ${sn_code} 添加沟通任务,间隔: ${chat_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动沟通] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoChatTask();

View File

@@ -0,0 +1,297 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
const authorizationService = require('../../../services/authorization_service');
const ConfigManager = require('../services/configManager');
/**
* 自动投递任务
* 从数据库读取职位列表并进行自动投递
*/
class AutoDeliverTask extends BaseTask {
constructor() {
super('auto_deliver', {
defaultInterval: 30, // 默认30分钟
defaultPriority: 7, // 高优先级
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_search', // 搜索任务
'auto_active_account' // 活跃账号任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动投递 - ${params.keyword || '指定职位'}`;
}
/**
* 执行自动投递任务
*/
async execute(sn_code, params) {
console.log(`[自动投递] 设备 ${sn_code} 开始执行投递任务`);
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
throw new Error(`账号 ${sn_code} 不存在`);
}
// 2. 检查授权
const authorization = await authorizationService.checkAuthorization(sn_code);
if (!authorization.is_authorized) {
throw new Error('授权天数不足');
}
// 3. 获取投递配置
const deliverConfig = ConfigManager.parseDeliverConfig(account.deliver_config);
// 4. 检查日投递限制
const dailyLimit = config.platformDailyLimits[account.platform_type] || 50;
const todayDelivered = await this.getTodayDeliveredCount(sn_code);
if (todayDelivered >= dailyLimit) {
throw new Error(`今日投递已达上限 (${todayDelivered}/${dailyLimit})`);
}
// 5. 获取可投递的职位列表
const jobs = await this.getDeliverableJobs(sn_code, account, deliverConfig);
if (!jobs || jobs.length === 0) {
console.log(`[自动投递] 设备 ${sn_code} 没有可投递的职位`);
return {
success: true,
delivered: 0,
message: '没有可投递的职位'
};
}
console.log(`[自动投递] 设备 ${sn_code} 找到 ${jobs.length} 个可投递职位`);
// 6. 执行投递(这里需要调用实际的投递逻辑)
const deliverResult = {
success: true,
delivered: jobs.length,
jobs: jobs.map(j => ({
id: j.id,
title: j.job_title,
company: j.company_name
}))
};
return deliverResult;
}
/**
* 获取账号信息
*/
async getAccountInfo(sn_code) {
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
return account ? account.toJSON() : null;
}
/**
* 获取今日已投递数量
*/
async getTodayDeliveredCount(sn_code) {
const { task_status } = db.models;
const Sequelize = require('sequelize');
const today = new Date();
today.setHours(0, 0, 0, 0);
const count = await task_status.count({
where: {
sn_code: sn_code,
taskType: 'auto_deliver',
status: 'completed',
endTime: {
[Sequelize.Op.gte]: today
}
}
});
return count;
}
/**
* 获取可投递的职位列表
*/
async getDeliverableJobs(sn_code, account, deliverConfig) {
const { job_postings } = db.models;
const Sequelize = require('sequelize');
// 构建查询条件
const where = {
sn_code: sn_code,
platform: account.platform_type,
is_delivered: 0, // 未投递
is_filtered: 0 // 未被过滤
};
// 薪资范围过滤
if (deliverConfig.min_salary > 0) {
where.salary_min = {
[Sequelize.Op.gte]: deliverConfig.min_salary
};
}
if (deliverConfig.max_salary > 0) {
where.salary_max = {
[Sequelize.Op.lte]: deliverConfig.max_salary
};
}
// 查询职位
const jobs = await job_postings.findAll({
where: where,
limit: deliverConfig.max_deliver,
order: [['create_time', 'DESC']]
});
return jobs.map(j => j.toJSON());
}
/**
* 添加投递任务到队列
* 这是外部调用的入口,会进行所有冲突检查
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动投递] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
console.log(`[自动投递] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
// 2. 检查是否开启了自动投递
if (!account.auto_deliver) {
console.log(`[自动投递] 设备 ${sn_code} 未开启自动投递`);
return { success: false, reason: '未开启自动投递' };
}
// 3. 获取投递配置
const deliverConfig = ConfigManager.parseDeliverConfig(account.deliver_config);
// 4. 检查时间范围
if (deliverConfig.time_range) {
const timeCheck = this.checkTimeRange(deliverConfig.time_range);
if (!timeCheck.allowed) {
console.log(`[自动投递] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查投递间隔
const intervalCheck = await this.checkExecutionInterval(
sn_code,
deliverConfig.deliver_interval
);
if (!intervalCheck.allowed) {
console.log(`[自动投递] 设备 ${sn_code} ${intervalCheck.reason}`);
// 推送等待状态到客户端
await this.notifyWaitingStatus(sn_code, intervalCheck, taskQueue);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
keyword: account.keyword || '',
platform: account.platform_type || 'boss',
pageCount: deliverConfig.page_count,
maxCount: deliverConfig.max_deliver,
filterRules: {
minSalary: deliverConfig.min_salary,
maxSalary: deliverConfig.max_salary,
keywords: deliverConfig.filter_keywords,
excludeKeywords: deliverConfig.exclude_keywords
},
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动投递] 已为设备 ${sn_code} 添加投递任务,间隔: ${deliverConfig.deliver_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动投递] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
/**
* 推送等待状态到客户端
*/
async notifyWaitingStatus(sn_code, intervalCheck, taskQueue) {
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
// 获取当前任务状态摘要
const taskStatusSummary = taskQueue.getTaskStatusSummary(sn_code);
// 添加等待消息到工作状态
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, taskStatusSummary, {
waitingMessage: {
type: 'deliver_interval',
message: intervalCheck.reason,
remainingMinutes: intervalCheck.remainingMinutes,
nextDeliverTime: intervalCheck.nextExecutionTime?.toISOString()
}
});
} catch (error) {
console.warn(`[自动投递] 推送等待消息失败:`, error.message);
}
}
}
// 导出单例
module.exports = new AutoDeliverTask();

View File

@@ -0,0 +1,211 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
const ConfigManager = require('../services/configManager');
/**
* 自动搜索职位任务
* 定期搜索符合条件的职位并保存到数据库
*/
class AutoSearchTask extends BaseTask {
constructor() {
super('auto_search', {
defaultInterval: 60, // 默认60分钟
defaultPriority: 8, // 高优先级(比投递高,先搜索后投递)
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_deliver', // 投递任务
'auto_active_account' // 活跃账号任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.keyword && !params.jobType) {
return {
valid: false,
reason: '缺少必要参数: keyword 或 jobType'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动搜索 - ${params.keyword || params.jobType || '默认'}`;
}
/**
* 执行自动搜索任务
*/
async execute(sn_code, params) {
console.log(`[自动搜索] 设备 ${sn_code} 开始执行搜索任务`);
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
throw new Error(`账号 ${sn_code} 不存在`);
}
// 2. 获取搜索配置
const searchConfig = ConfigManager.parseSearchConfig(account.search_config);
// 3. 检查日搜索限制
const dailyLimit = config.dailyLimits.maxSearch || 20;
const todaySearched = await this.getTodaySearchCount(sn_code);
if (todaySearched >= dailyLimit) {
throw new Error(`今日搜索已达上限 (${todaySearched}/${dailyLimit})`);
}
// 4. 执行搜索(这里需要调用实际的搜索逻辑)
const searchResult = {
success: true,
keyword: params.keyword || account.keyword,
pageCount: searchConfig.page_count || 3,
jobsFound: 0, // 实际搜索到的职位数
jobsSaved: 0 // 保存到数据库的职位数
};
console.log(`[自动搜索] 设备 ${sn_code} 搜索完成,找到 ${searchResult.jobsFound} 个职位`);
return searchResult;
}
/**
* 获取账号信息
*/
async getAccountInfo(sn_code) {
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
return account ? account.toJSON() : null;
}
/**
* 获取今日已搜索数量
*/
async getTodaySearchCount(sn_code) {
const { task_status } = db.models;
const Sequelize = require('sequelize');
const today = new Date();
today.setHours(0, 0, 0, 0);
const count = await task_status.count({
where: {
sn_code: sn_code,
taskType: 'auto_search',
status: 'completed',
endTime: {
[Sequelize.Op.gte]: today
}
}
});
return count;
}
/**
* 添加搜索任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动搜索] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
console.log(`[自动搜索] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
// 2. 检查是否开启了自动搜索
if (!account.auto_search) {
console.log(`[自动搜索] 设备 ${sn_code} 未开启自动搜索`);
return { success: false, reason: '未开启自动搜索' };
}
// 3. 获取搜索配置
const searchConfig = ConfigManager.parseSearchConfig(account.search_config);
// 4. 检查时间范围
if (searchConfig.time_range) {
const timeCheck = this.checkTimeRange(searchConfig.time_range);
if (!timeCheck.allowed) {
console.log(`[自动搜索] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查搜索间隔
const intervalCheck = await this.checkExecutionInterval(
sn_code,
searchConfig.search_interval
);
if (!intervalCheck.allowed) {
console.log(`[自动搜索] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
keyword: account.keyword || '',
jobType: account.job_type || '',
platform: account.platform_type || 'boss',
pageCount: searchConfig.page_count || 3,
city: searchConfig.city || '',
salaryRange: searchConfig.salary_range || '',
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动搜索] 已为设备 ${sn_code} 添加搜索任务,间隔: ${searchConfig.search_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动搜索] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoSearchTask();

View File

@@ -0,0 +1,405 @@
const dayjs = require('dayjs');
const deviceManager = require('../core/deviceManager');
const db = require('../../dbProxy');
/**
* 任务基类
* 提供所有任务的通用功能和冲突检测机制
*/
class BaseTask {
constructor(taskType, config = {}) {
this.taskType = taskType;
this.config = {
// 默认配置
defaultInterval: 30, // 默认间隔30分钟
defaultPriority: 5,
requiresLogin: true, // 是否需要登录状态
conflictsWith: [], // 与哪些任务类型冲突
...config
};
// 任务执行锁 { sn_code: timestamp }
this.taskLocks = new Map();
// 最后执行时间缓存 { sn_code: timestamp }
this.lastExecutionCache = new Map();
}
/**
* Layer 1: 任务类型互斥锁检查
* 防止同一设备同时添加相同类型的任务
*/
acquireTaskLock(sn_code) {
const lockKey = `${sn_code}:${this.taskType}`;
const now = Date.now();
const existingLock = this.taskLocks.get(lockKey);
// 如果存在锁且未超时(5分钟),返回false
if (existingLock && (now - existingLock) < 5 * 60 * 1000) {
const remainingTime = Math.ceil((5 * 60 * 1000 - (now - existingLock)) / 1000);
return {
allowed: false,
reason: `任务 ${this.taskType} 正在添加中,请等待 ${remainingTime}`
};
}
// 获取锁
this.taskLocks.set(lockKey, now);
return { allowed: true };
}
/**
* 释放任务锁
*/
releaseTaskLock(sn_code) {
const lockKey = `${sn_code}:${this.taskType}`;
this.taskLocks.delete(lockKey);
}
/**
* Layer 2: 设备状态检查
* 检查设备是否在线、是否登录、是否忙碌
*/
async checkDeviceStatus(sn_code) {
// 1. 优先检查内存中的设备状态
let device = deviceManager.devices.get(sn_code);
// 2. 如果内存中没有,降级到数据库查询(可能是刚启动还没收到心跳)
if (!device) {
try {
const pla_account = db.getModel('pla_account');
const dbDevice = await pla_account.findOne({
where: { sn_code, is_delete: 0 },
attributes: ['sn_code', 'is_online', 'is_logged_in']
});
if (!dbDevice) {
return {
allowed: false,
reason: `设备 ${sn_code} 不存在`
};
}
// 检查数据库中的在线状态
if (!dbDevice.is_online) {
return {
allowed: false,
reason: `设备 ${sn_code} 离线(数据库状态)`
};
}
// 检查数据库中的登录状态
if (this.config.requiresLogin && !dbDevice.is_logged_in) {
return {
allowed: false,
reason: `设备 ${sn_code} 未登录平台账号(数据库状态)`
};
}
// 数据库检查通过,允许执行
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 查询设备状态失败:`, error);
return {
allowed: false,
reason: `设备 ${sn_code} 状态查询失败`
};
}
}
// 3. 检查心跳超时
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
return {
allowed: false,
reason: `设备 ${sn_code} 离线(最后心跳: ${offlineMinutes}分钟前)`
};
}
// 4. 检查登录状态(如果任务需要)
if (this.config.requiresLogin && !device.isLoggedIn) {
return {
allowed: false,
reason: `设备 ${sn_code} 未登录平台账号`
};
}
return { allowed: true };
}
/**
* Layer 3: 检查任务队列状态
* 防止队列中已有相同任务
*/
async checkTaskQueue(sn_code, taskQueue) {
// 获取设备队列
const deviceQueue = taskQueue.deviceQueues.get(sn_code);
if (!deviceQueue) {
return { allowed: true };
}
// 检查队列中是否有相同类型的待执行任务
const tasks = deviceQueue.toArray();
const hasSameTypeTask = tasks.some(task =>
task.taskType === this.taskType &&
task.status === 'pending'
);
if (hasSameTypeTask) {
return {
allowed: false,
reason: `队列中已存在待执行的 ${this.taskType} 任务`
};
}
return { allowed: true };
}
/**
* Layer 4: 检查任务去重
* 查询数据库中是否有重复的待执行任务
*/
async checkDuplicateTask(sn_code) {
try {
const { task_status } = db.models;
// 查询该设备是否有相同类型的pending/running任务
const existingTask = await task_status.findOne({
where: {
sn_code: sn_code,
taskType: this.taskType,
status: ['pending', 'running']
},
attributes: ['id', 'status', 'taskName']
});
if (existingTask) {
return {
allowed: false,
reason: `已存在 ${existingTask.status} 状态的任务: ${existingTask.taskName}`
};
}
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 检查重复任务失败:`, error);
// 出错时允许继续,避免阻塞
return { allowed: true };
}
}
/**
* Layer 5: 操作类型冲突检测
* 某些操作类型不能同时执行
*/
async checkOperationConflict(sn_code, taskQueue) {
// 如果没有配置冲突类型,直接通过
if (!this.config.conflictsWith || this.config.conflictsWith.length === 0) {
return { allowed: true };
}
// 检查当前是否有冲突的任务正在执行
const deviceStatus = taskQueue.deviceStatus.get(sn_code);
if (deviceStatus && deviceStatus.currentTask) {
const currentTaskType = deviceStatus.currentTask.taskType;
if (this.config.conflictsWith.includes(currentTaskType)) {
return {
allowed: false,
reason: `与正在执行的任务 ${currentTaskType} 冲突`
};
}
}
return { allowed: true };
}
/**
* 检查执行间隔
* 从数据库查询上次成功执行时间,判断是否满足间隔要求
*/
async checkExecutionInterval(sn_code, intervalMinutes) {
try {
const { task_status } = db.models;
// 先从缓存检查
const cachedLastExecution = this.lastExecutionCache.get(sn_code);
const now = Date.now();
if (cachedLastExecution) {
const elapsedTime = now - cachedLastExecution;
const interval_ms = intervalMinutes * 60 * 1000;
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
return {
allowed: false,
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
remainingMinutes,
elapsedMinutes
};
}
}
// 从数据库查询最近一次成功完成的任务
const lastTask = await task_status.findOne({
where: {
sn_code: sn_code,
taskType: this.taskType,
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
// 如果存在上次执行记录,检查是否已经过了间隔时间
if (lastTask && lastTask.endTime) {
const lastExecutionTime = new Date(lastTask.endTime).getTime();
const elapsedTime = now - lastExecutionTime;
const interval_ms = intervalMinutes * 60 * 1000;
// 更新缓存
this.lastExecutionCache.set(sn_code, lastExecutionTime);
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
return {
allowed: false,
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
remainingMinutes,
elapsedMinutes,
nextExecutionTime: new Date(lastExecutionTime + interval_ms)
};
}
}
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 检查执行间隔失败:`, error);
// 出错时允许继续,避免阻塞
return { allowed: true };
}
}
/**
* 检查时间范围限制
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
*/
checkTimeRange(timeRange) {
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
return { allowed: true, reason: '未配置时间范围' };
}
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute;
// 解析开始时间和结束时间
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
// 检查是否仅工作日
if (timeRange.workdays_only == 1) {
const dayOfWeek = now.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
}
}
// 检查当前时间是否在时间范围内
if (startTime <= endTime) {
// 正常情况: 09:00 - 18:00
if (currentTime < startTime || currentTime >= endTime) {
return {
allowed: false,
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
} else {
// 跨天情况: 22:00 - 06:00
if (currentTime < startTime && currentTime >= endTime) {
return {
allowed: false,
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
}
return { allowed: true, reason: '在允许的时间范围内' };
}
/**
* 综合检查 - 执行所有层级的检查
* @param {string} sn_code - 设备SN码
* @param {Object} taskQueue - 任务队列实例
* @param {Object} options - 额外选项
* @returns {Object} { allowed: boolean, reason: string }
*/
async canExecuteTask(sn_code, taskQueue, options = {}) {
const checks = [
{ name: 'Layer1-任务锁', fn: () => this.acquireTaskLock(sn_code) },
{ name: 'Layer2-设备状态', fn: () => this.checkDeviceStatus(sn_code) },
{ name: 'Layer3-队列检查', fn: () => this.checkTaskQueue(sn_code, taskQueue) },
{ name: 'Layer4-任务去重', fn: () => this.checkDuplicateTask(sn_code) },
{ name: 'Layer5-操作冲突', fn: () => this.checkOperationConflict(sn_code, taskQueue) }
];
// 逐层检查
for (const check of checks) {
const result = await check.fn();
if (!result.allowed) {
console.log(`[${this.taskType}] ${check.name} 未通过: ${result.reason}`);
return result;
}
}
return { allowed: true };
}
/**
* 清理任务锁(定期清理过期锁)
*/
cleanupExpiredLocks() {
const now = Date.now();
const timeout = 5 * 60 * 1000; // 5分钟超时
for (const [lockKey, timestamp] of this.taskLocks.entries()) {
if (now - timestamp > timeout) {
this.taskLocks.delete(lockKey);
}
}
}
/**
* 获取任务名称(子类可覆盖)
*/
getTaskName(params) {
return `${this.taskType} 任务`;
}
/**
* 验证任务参数(子类必须实现)
*/
validateParams(params) {
throw new Error('子类必须实现 validateParams 方法');
}
/**
* 执行任务的具体逻辑(子类必须实现)
*/
async execute(sn_code, params) {
throw new Error('子类必须实现 execute 方法');
}
}
module.exports = BaseTask;

View File

@@ -0,0 +1,16 @@
/**
* 任务模块索引
* 统一导出所有任务类型
*/
const autoSearchTask = require('./autoSearchTask');
const autoDeliverTask = require('./autoDeliverTask');
const autoChatTask = require('./autoChatTask');
const autoActiveTask = require('./autoActiveTask');
module.exports = {
autoSearchTask,
autoDeliverTask,
autoChatTask,
autoActiveTask
};

View File

@@ -0,0 +1,14 @@
/**
* Utils 模块导出
* 统一导出工具类模块
*/
const SalaryParser = require('./salaryParser');
const KeywordMatcher = require('./keywordMatcher');
const ScheduleUtils = require('./scheduleUtils');
module.exports = {
SalaryParser,
KeywordMatcher,
ScheduleUtils
};

View File

@@ -0,0 +1,227 @@
/**
* 关键词匹配工具
* 提供职位描述的关键词匹配和评分功能
*/
class KeywordMatcher {
/**
* 检查是否包含排除关键词
* @param {string} text - 待检查的文本
* @param {string[]} excludeKeywords - 排除关键词列表
* @returns {{matched: boolean, keywords: string[]}} 匹配结果
*/
static matchExcludeKeywords(text, excludeKeywords = []) {
if (!text || !excludeKeywords || excludeKeywords.length === 0) {
return { matched: false, keywords: [] };
}
const matched = [];
const lowerText = text.toLowerCase();
for (const keyword of excludeKeywords) {
if (!keyword || !keyword.trim()) continue;
const lowerKeyword = keyword.toLowerCase().trim();
if (lowerText.includes(lowerKeyword)) {
matched.push(keyword);
}
}
return {
matched: matched.length > 0,
keywords: matched
};
}
/**
* 检查是否包含过滤关键词(必须匹配)
* @param {string} text - 待检查的文本
* @param {string[]} filterKeywords - 过滤关键词列表
* @returns {{matched: boolean, keywords: string[], matchCount: number}} 匹配结果
*/
static matchFilterKeywords(text, filterKeywords = []) {
if (!text) {
return { matched: false, keywords: [], matchCount: 0 };
}
if (!filterKeywords || filterKeywords.length === 0) {
return { matched: true, keywords: [], matchCount: 0 };
}
const matched = [];
const lowerText = text.toLowerCase();
for (const keyword of filterKeywords) {
if (!keyword || !keyword.trim()) continue;
const lowerKeyword = keyword.toLowerCase().trim();
if (lowerText.includes(lowerKeyword)) {
matched.push(keyword);
}
}
// 只要匹配到至少一个过滤关键词即可通过
return {
matched: matched.length > 0,
keywords: matched,
matchCount: matched.length
};
}
/**
* 计算关键词匹配奖励分数
* @param {string} text - 待检查的文本
* @param {string[]} keywords - 关键词列表
* @param {object} options - 选项
* @returns {{score: number, matchedKeywords: string[], matchCount: number}}
*/
static calculateBonus(text, keywords = [], options = {}) {
const {
baseScore = 10, // 每个关键词的基础分
maxBonus = 50, // 最大奖励分
caseSensitive = false // 是否区分大小写
} = options;
if (!text || !keywords || keywords.length === 0) {
return { score: 0, matchedKeywords: [], matchCount: 0 };
}
const matched = [];
const searchText = caseSensitive ? text : text.toLowerCase();
for (const keyword of keywords) {
if (!keyword || !keyword.trim()) continue;
const searchKeyword = caseSensitive ? keyword.trim() : keyword.toLowerCase().trim();
if (searchText.includes(searchKeyword)) {
matched.push(keyword);
}
}
const score = Math.min(matched.length * baseScore, maxBonus);
return {
score,
matchedKeywords: matched,
matchCount: matched.length
};
}
/**
* 高亮匹配的关键词(用于展示)
* @param {string} text - 原始文本
* @param {string[]} keywords - 关键词列表
* @param {string} prefix - 前缀标记(默认 <mark>
* @param {string} suffix - 后缀标记(默认 </mark>
* @returns {string} 高亮后的文本
*/
static highlight(text, keywords = [], prefix = '<mark>', suffix = '</mark>') {
if (!text || !keywords || keywords.length === 0) {
return text;
}
let result = text;
for (const keyword of keywords) {
if (!keyword || !keyword.trim()) continue;
const regex = new RegExp(`(${this.escapeRegex(keyword.trim())})`, 'gi');
result = result.replace(regex, `${prefix}$1${suffix}`);
}
return result;
}
/**
* 转义正则表达式特殊字符
* @param {string} str - 待转义的字符串
* @returns {string} 转义后的字符串
*/
static escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* 综合匹配(排除 + 过滤 + 奖励)
* @param {string} text - 待检查的文本
* @param {object} config - 配置
* @param {string[]} config.excludeKeywords - 排除关键词
* @param {string[]} config.filterKeywords - 过滤关键词
* @param {string[]} config.bonusKeywords - 奖励关键词
* @returns {{pass: boolean, reason?: string, score: number, details: object}}
*/
static match(text, config = {}) {
const {
excludeKeywords = [],
filterKeywords = [],
bonusKeywords = []
} = config;
// 1. 检查排除关键词
const excludeResult = this.matchExcludeKeywords(text, excludeKeywords);
if (excludeResult.matched) {
return {
pass: false,
reason: `包含排除关键词: ${excludeResult.keywords.join(', ')}`,
score: 0,
details: { exclude: excludeResult }
};
}
// 2. 检查过滤关键词(必须匹配)
const filterResult = this.matchFilterKeywords(text, filterKeywords);
if (filterKeywords.length > 0 && !filterResult.matched) {
return {
pass: false,
reason: '不包含任何必需关键词',
score: 0,
details: { filter: filterResult }
};
}
// 3. 计算奖励分数
const bonusResult = this.calculateBonus(text, bonusKeywords);
return {
pass: true,
score: bonusResult.score,
details: {
exclude: excludeResult,
filter: filterResult,
bonus: bonusResult
}
};
}
/**
* 批量匹配职位列表
* @param {Array} jobs - 职位列表
* @param {object} config - 匹配配置
* @param {Function} textExtractor - 文本提取函数 (job) => string
* @returns {Array} 匹配通过的职位(带匹配信息)
*/
static filterJobs(jobs, config) {
if (!jobs || jobs.length === 0) {
return [];
}
const textExtractor=(job) => `${job.jobTitle || ''} ${job.companyIndustry || ''}`;
const filtered = [];
for (const job of jobs) {
const text = textExtractor(job);
const matchResult = this.match(text, config);
if (matchResult.pass) {
filtered.push({
...job,
_matchInfo: matchResult
});
}
}
return filtered;
}
}
module.exports = KeywordMatcher;

View File

@@ -0,0 +1,126 @@
/**
* 薪资解析工具
* 统一处理职位薪资和期望薪资的解析逻辑
*/
class SalaryParser {
/**
* 解析薪资范围字符串
* @param {string} salaryDesc - 薪资描述 (如 "15-20K", "8000-12000元")
* @returns {{ min: number, max: number }} 薪资范围(单位:元)
*/
static parse(salaryDesc) {
if (!salaryDesc || typeof salaryDesc !== 'string') {
return { min: 0, max: 0 };
}
// 尝试各种格式
return this.parseK(salaryDesc)
|| this.parseYuan(salaryDesc)
|| this.parseMixed(salaryDesc)
|| { min: 0, max: 0 };
}
/**
* 解析 K 格式薪资 (如 "15-20K", "8-12k")
*/
static parseK(desc) {
const kMatch = desc.match(/(\d+)[-~](\d+)[kK千]/);
if (kMatch) {
return {
min: parseInt(kMatch[1]) * 1000,
max: parseInt(kMatch[2]) * 1000
};
}
return null;
}
/**
* 解析元格式薪资 (如 "8000-12000元", "15000-20000")
*/
static parseYuan(desc) {
const yuanMatch = desc.match(/(\d+)[-~](\d+)元?/);
if (yuanMatch) {
return {
min: parseInt(yuanMatch[1]),
max: parseInt(yuanMatch[2])
};
}
return null;
}
/**
* 解析混合格式 (如 "8k-12000元")
*/
static parseMixed(desc) {
const mixedMatch = desc.match(/(\d+)[kK千][-~](\d+)元?/);
if (mixedMatch) {
return {
min: parseInt(mixedMatch[1]) * 1000,
max: parseInt(mixedMatch[2])
};
}
return null;
}
/**
* 检查职位薪资是否在期望范围内
* @param {object} jobSalary - 职位薪资 { min, max }
* @param {number} minExpected - 期望最低薪资
* @param {number} maxExpected - 期望最高薪资
*/
static isWithinRange(jobSalary, minExpected, maxExpected) {
if (!jobSalary || jobSalary.min === 0) {
return true; // 无法判断时默认通过
}
// 职位最高薪资 >= 期望最低薪资
if (minExpected > 0 && jobSalary.max < minExpected) {
return false;
}
// 职位最低薪资 <= 期望最高薪资
if (maxExpected > 0 && jobSalary.min > maxExpected) {
return false;
}
return true;
}
/**
* 计算薪资匹配度(用于职位评分)
* @param {object} jobSalary - 职位薪资
* @param {object} expectedSalary - 期望薪资
* @returns {number} 匹配度 0-1
*/
static calculateMatch(jobSalary, expectedSalary) {
if (!jobSalary || !expectedSalary || jobSalary.min === 0 || expectedSalary.min === 0) {
return 0.5; // 无法判断时返回中性值
}
const jobAvg = (jobSalary.min + jobSalary.max) / 2;
const expectedAvg = (expectedSalary.min + expectedSalary.max) / 2;
const diff = Math.abs(jobAvg - expectedAvg);
const range = (jobSalary.max - jobSalary.min + expectedSalary.max - expectedSalary.min) / 2;
// 差距越小,匹配度越高
return Math.max(0, 1 - diff / (range || 1));
}
/**
* 格式化薪资显示
* @param {object} salary - 薪资对象 { min, max }
* @returns {string} 格式化字符串
*/
static format(salary) {
if (!salary || salary.min === 0) {
return '面议';
}
const minK = (salary.min / 1000).toFixed(0);
const maxK = (salary.max / 1000).toFixed(0);
return `${minK}-${maxK}K`;
}
}
module.exports = SalaryParser;

62
api/model/chat_message.js Normal file
View File

@@ -0,0 +1,62 @@
const Sequelize = require('sequelize');
/**
* 聊天消息表
* 每句话一条记录;会话形态(仅 data+job用 mid=0 存一条
*/
module.exports = (db) => {
const chat_message = db.define('chat_message', {
sn_code: {
comment: '设备SN码',
type: Sequelize.STRING(50),
allowNull: false,
defaultValue: ''
},
platform: {
comment: '平台: boss / liepin',
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: 'boss'
},
friendId: {
comment: '好友/会话ID',
type: Sequelize.BIGINT,
allowNull: false
},
encryptFriendId: {
comment: '好友加密ID',
type: Sequelize.STRING(100),
allowNull: true,
defaultValue: ''
},
mid: {
comment: '消息ID接口返回',
type: Sequelize.BIGINT,
allowNull: false
},
message_data: {
comment: '单条消息原始数据 JSON',
type: Sequelize.JSON,
allowNull: true,
defaultValue: null
},
fetch_time: {
comment: '拉取时间',
type: Sequelize.DATE,
allowNull: true,
defaultValue: Sequelize.NOW
}
}, {
timestamps: false,
indexes: [
{ unique: true, fields: ['sn_code', 'platform', 'friendId', 'mid'] },
{ unique: false, fields: ['sn_code', 'platform', 'friendId'] },
{ unique: false, fields: ['fetch_time'] }
]
});
// chat_message.sync({ force: true });
return chat_message;
};

View File

@@ -1,12 +1,12 @@
const Sequelize = require('sequelize');
/**
* 聊天记录表模型
* 记录与HR的聊天内容和效果
* 聊天会话列表表模型
* 按 Boss 「好友/会话」列表维度存储,会话摘要信息
*/
module.exports = (db) => {
const chat_records = db.define("chat_records", {
// 聊天基本信息
const chat_records = db.define('chat_records', {
// 设备与平台
sn_code: {
comment: '设备SN码',
type: Sequelize.STRING(50),
@@ -19,270 +19,91 @@ module.exports = (db) => {
allowNull: false,
defaultValue: 'boss'
},
// 岗位关联
jobId: {
comment: '岗位ID',
// Boss 会话列表字段(与接口 friend 对象对应)
friendId: {
comment: '好友ID',
type: Sequelize.BIGINT,
allowNull: false
},
friendSource: {
comment: '好友来源',
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: 0
},
encryptFriendId: {
comment: '好友加密ID',
type: Sequelize.STRING(100),
allowNull: true,
allowNull: false,
defaultValue: ''
},
encryptBossId: {
comment: 'Boss加密ID',
type: Sequelize.STRING(100),
allowNull: true,
defaultValue: ''
},
jobTitle: {
comment: '岗位名称',
type: Sequelize.STRING(200),
allowNull: true,
defaultValue: ''
},
companyName: {
comment: '公司名称',
type: Sequelize.STRING(200),
allowNull: true,
defaultValue: ''
},
// HR信息
hrName: {
name: {
comment: 'HR姓名',
type: Sequelize.STRING(50),
allowNull: true,
defaultValue: ''
},
hrTitle: {
comment: 'HR职位',
type: Sequelize.STRING(50),
allowNull: true,
defaultValue: ''
},
hrId: {
comment: 'HR ID',
type: Sequelize.STRING(100),
allowNull: true,
defaultValue: ''
},
// 聊天内容
chatType: {
comment: '聊天类型: greeting-打招呼, follow_up-跟进, interview-面试邀约, reply-回复',
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: 'greeting'
},
direction: {
comment: '消息方向: sent-发送, received-接收',
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: 'sent'
},
content: {
comment: '聊天内容',
type: Sequelize.TEXT,
allowNull: false,
defaultValue: ''
},
contentType: {
comment: '内容类型: text-文本, image-图片, file-文件',
type: Sequelize.STRING(20),
allowNull: true,
defaultValue: 'text'
},
// AI生成信息
isAiGenerated: {
comment: '是否AI生成',
type: Sequelize.BOOLEAN,
allowNull: true,
defaultValue: false
},
aiPrompt: {
comment: 'AI生成的提示词',
type: Sequelize.TEXT,
allowNull: true,
defaultValue: ''
},
aiModel: {
comment: 'AI模型名称',
type: Sequelize.STRING(50),
allowNull: true,
defaultValue: ''
},
// 发送状态
sendStatus: {
comment: '发送状态: pending-待发送, sending-发送中, sent-已发送, failed-发送失败',
type: Sequelize.STRING(20),
allowNull: true,
defaultValue: 'pending'
},
sendTime: {
comment: '发送时间',
type: Sequelize.DATE,
updateTime: {
comment: '最后更新时间(毫秒时间戳)',
type: Sequelize.BIGINT,
allowNull: true
},
receiveTime: {
comment: '接收时间',
type: Sequelize.DATE,
allowNull: true
},
// 回复信息
hasReply: {
comment: '是否有回复',
type: Sequelize.BOOLEAN,
allowNull: true,
defaultValue: false
},
replyTime: {
comment: '回复时间',
type: Sequelize.DATE,
allowNull: true
},
replyContent: {
comment: '回复内容',
type: Sequelize.TEXT,
allowNull: true,
defaultValue: ''
},
replyDuration: {
comment: '回复时长(分钟)',
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: 0
},
// 面试邀约信息
isInterviewInvitation: {
comment: '是否包含面试邀约',
type: Sequelize.BOOLEAN,
allowNull: true,
defaultValue: false
},
interviewTime: {
comment: '面试时间',
type: Sequelize.DATE,
allowNull: true
},
interviewLocation: {
comment: '面试地点',
brandName: {
comment: '公司名称',
type: Sequelize.STRING(200),
allowNull: true,
defaultValue: ''
},
interviewType: {
comment: '面试类型: online-线上, offline-线下',
type: Sequelize.STRING(20),
jobName: {
comment: '职位名称',
type: Sequelize.STRING(200),
allowNull: true,
defaultValue: ''
},
interviewStatus: {
comment: '面试状态: pending-待确认, confirmed-已确认, completed-已完成, cancelled-已取消',
type: Sequelize.STRING(20),
jobTypeDesc: {
comment: '职位类型描述',
type: Sequelize.STRING(100),
allowNull: true,
defaultValue: ''
},
// 效果评估
effectScore: {
comment: '效果评分(0-100)',
jobCity: {
comment: '职位城市',
type: Sequelize.STRING(100),
allowNull: true,
defaultValue: ''
},
positionName: {
comment: '岗位名称/方向',
type: Sequelize.STRING(200),
allowNull: true,
defaultValue: ''
},
bossTitle: {
comment: 'Boss/HR 职位头衔',
type: Sequelize.STRING(100),
allowNull: true,
defaultValue: ''
},
waterLevel: {
comment: '水位Boss 优先级标记)',
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: 0
},
sentiment: {
comment: '情感倾向: positive-正面, neutral-中性, negative-负面',
type: Sequelize.STRING(20),
allowNull: true,
defaultValue: 'neutral'
},
// 会话信息
conversationId: {
comment: '会话ID(同一个岗位的聊天属于一个会话)',
type: Sequelize.STRING(50),
allowNull: true,
defaultValue: ''
},
messageIndex: {
comment: '消息序号(会话内的消息顺序)',
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: 0
},
// 关联信息
taskId: {
comment: '关联任务ID',
type: Sequelize.STRING(50),
allowNull: true,
defaultValue: ''
},
// 其他信息
originalData: {
comment: '原始数据(JSON)',
type: Sequelize.TEXT,
allowNull: true,
defaultValue: ''
},
errorMessage: {
comment: '错误信息',
type: Sequelize.TEXT,
allowNull: true,
defaultValue: ''
},
notes: {
comment: '备注',
type: Sequelize.TEXT,
allowNull: true,
defaultValue: ''
},
}
}, {
timestamps: false,
indexes: [
{
unique: false,
fields: ['sn_code']
},
{
unique: false,
fields: ['jobId']
},
{
unique: false,
fields: ['encryptBossId']
},
{
unique: false,
fields: ['conversationId']
},
{
unique: false,
fields: ['chatType']
},
{
unique: false,
fields: ['sendStatus']
},
{
unique: false,
fields: ['hasReply']
},
{ unique: false, fields: ['sn_code'] },
{ unique: false, fields: ['platform'] },
{ unique: false, fields: ['friendId'] },
{ unique: false, fields: ['encryptFriendId'] }
]
});
//chat_records.sync({ force: true });
// chat_records.sync({ force: true });
return chat_records
return chat_records;
};

View File

@@ -0,0 +1,87 @@
const Sequelize = require('sequelize');
/**
* 沟通回复意图 AI 调用记录
* 记录 getReplyContentFromDetail 中 replyIntentAndContent 的入参与结果,便于排查与统计
*/
module.exports = (db) => {
const chat_reply_intent_log = db.define('chat_reply_intent_log', {
sn_code: {
comment: '设备SN码',
type: Sequelize.STRING(50),
allowNull: true,
defaultValue: ''
},
platform: {
comment: '平台: boss / liepin',
type: Sequelize.STRING(20),
allowNull: true,
defaultValue: 'boss'
},
friendId: {
comment: '好友/会话ID',
type: Sequelize.BIGINT,
allowNull: true
},
encrypt_friend_id: {
comment: '好友加密ID',
type: Sequelize.STRING(100),
allowNull: true,
defaultValue: ''
},
security_id: {
comment: 'HR 消息唯一 id用于去重同一消息只调用一次 AI',
type: Sequelize.STRING(500),
allowNull: true
},
hr_message_text: {
comment: 'HR 最新消息原文AI 入参)',
type: Sequelize.TEXT,
allowNull: true
},
action: {
comment: 'AI 返回意图: no_reply/text/send_resume/exchange_wechat/exchange_phone',
type: Sequelize.STRING(30),
allowNull: true,
defaultValue: ''
},
reply_content: {
comment: 'AI 返回的回复文案',
type: Sequelize.TEXT,
allowNull: true
},
replied: {
comment: '是否执行了回复',
type: Sequelize.BOOLEAN,
allowNull: true,
defaultValue: false
},
reason: {
comment: '未回复时的原因',
type: Sequelize.STRING(200),
allowNull: true
},
job_name: {
comment: '职位名称(便于排查)',
type: Sequelize.STRING(200),
allowNull: true
},
create_time: {
comment: '创建时间',
type: Sequelize.DATE,
allowNull: true,
defaultValue: Sequelize.NOW
}
}, {
timestamps: false,
indexes: [
{ unique: true, fields: ['security_id'], name: 'uk_security_id' },
{ unique: false, fields: ['sn_code', 'platform', 'friendId'] },
{ unique: false, fields: ['create_time'] }
]
});
// chat_reply_intent_log.sync({ force: true });
return chat_reply_intent_log;
};

View File

@@ -190,7 +190,7 @@ module.exports = (db) => {
},
// 投递状态
applyStatus: {
comment: '投递状态: pending-待投递, applied-已投递, rejected-被拒绝, accepted-已接受',
comment: '投递状态: pending-待投递, filtered-已过滤(不符合规则未投递), applied-已投递, rejected-被拒绝, accepted-已接受, success/failed-见业务',
type: Sequelize.STRING(20),
allowNull: true,
defaultValue: 'pending'

Some files were not shown because too many files have changed in this diff Show More