Compare commits
22 Commits
dev2
...
e3d14dd637
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3d14dd637 | ||
|
|
ca8bbcd9cd | ||
|
|
820e437729 | ||
|
|
39a5b49213 | ||
|
|
f071215ad5 | ||
|
|
f5082c157c | ||
|
|
6b371148af | ||
|
|
6aeee136c4 | ||
|
|
2c021c24ef | ||
|
|
8a953eb769 | ||
|
|
a40219c7e4 | ||
|
|
5ec4e7f440 | ||
|
|
96da90daa8 | ||
|
|
e44ffba1ef | ||
|
|
0483d6d023 | ||
|
|
58c9d64e55 | ||
|
|
dfd3119163 | ||
|
|
1a011bcc01 | ||
|
|
c1d812a80e | ||
|
|
9ab749f0f3 | ||
|
|
63ae655b34 | ||
|
|
68b4db0aee |
140
_doc/AI禁用说明.md
140
_doc/AI禁用说明.md
@@ -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. **匹配度优化**
|
||||
- 优化评分算法
|
||||
- 添加更多匹配维度
|
||||
- 支持权重配置
|
||||
|
||||
968
_doc/MQTT指令列表.md
968
_doc/MQTT指令列表.md
@@ -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
|
||||
20
_doc/主流程.md
20
_doc/主流程.md
@@ -1,20 +0,0 @@
|
||||
|
||||
|
||||
# handleAutoDeliverTask ,自动投递岗位
|
||||
|
||||
|
||||
1. 如果 2 小时之内没有获取在线简历 ,则重新获取一下在线简历,没有创建,有则更新
|
||||
|
||||
2. 获取职位列表, 按照用户 简历的信息resume_info 中的 skills expectedLocation ,expectedSalary ,expectedPosition ,workYears ,education location 和 职位类型 job_types 中的 年龄,薪资,距离职位的位置,commonSkills,excludeKeywords ,
|
||||
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.投递合适匹配的岗位
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
# 任务与指令的区别说明
|
||||
|
||||
## 📋 概述
|
||||
|
||||
在调度系统中,**任务(Task)** 和 **指令(Command)** 是两个不同层次的概念,它们的关系是:**一个任务可以包含多个指令**。
|
||||
|
||||
### ⚠️ 重要说明
|
||||
|
||||
**当前系统实际情况**:
|
||||
- **真正的任务**:目前只有 `auto_deliver`(自动投递任务)是真正的任务,它包含多个步骤和指令
|
||||
- **伪任务**:虽然代码中有 `get_resume`、`get_job_list`、`send_chat`、`apply_job` 等任务处理器,但它们实际上只是包装了单个指令,本质上就是直接执行指令
|
||||
|
||||
**为什么会有伪任务**:
|
||||
1. 统一的任务追踪和日志记录
|
||||
2. 保持接口的一致性
|
||||
3. 未来可能扩展为真正的任务(包含多个步骤)
|
||||
|
||||
## 🔄 层级关系
|
||||
|
||||
```
|
||||
任务(Task)
|
||||
├── 指令1(Command)
|
||||
├── 指令2(Command)
|
||||
└── 指令3(Command)
|
||||
```
|
||||
|
||||
## 📊 详细对比
|
||||
|
||||
| 维度 | 任务(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` 虽然叫任务,但实际只是指令的包装
|
||||
|
||||
这种设计实现了业务逻辑和执行逻辑的分离,提高了系统的灵活性和可维护性。伪任务的存在可能是为了统一的任务追踪和未来扩展。
|
||||
|
||||
874
_doc/功能规划文档.md
874
_doc/功能规划文档.md
@@ -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
|
||||
574
_doc/发布的接口文档.md
574
_doc/发布的接口文档.md
@@ -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)
|
||||
|
||||
100
_doc/命名规范统一方案.md
100
_doc/命名规范统一方案.md
@@ -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 结尾
|
||||
|
||||
@@ -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. 删除废弃文件
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
1174
_doc/客户端待开发功能.md
1174
_doc/客户端待开发功能.md
File diff suppressed because it is too large
Load Diff
@@ -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服务
|
||||
- 统一类命名
|
||||
|
||||
@@ -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. **可选**:完善错误处理和日志记录
|
||||
|
||||
@@ -1,764 +0,0 @@
|
||||
# Boss直聘搜索列表和投递功能开发规划
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
基于Boss直聘Web端职位搜索页面(`https://www.zhipin.com/web/geek/jobs`),完善搜索列表获取和职位投递功能,包括服务端任务创建、指令生成和完整流程实现。
|
||||
|
||||
## 🎯 目标功能
|
||||
|
||||
### 1. 搜索列表功能
|
||||
- 支持多条件搜索(关键词、城市、薪资、经验、学历等)
|
||||
- 支持分页获取职位列表
|
||||
- 自动保存职位到数据库
|
||||
- 支持职位去重和更新
|
||||
|
||||
### 2. 投递功能
|
||||
- 单个职位投递
|
||||
- 批量职位投递
|
||||
- 投递状态跟踪
|
||||
- 投递记录管理
|
||||
|
||||
## 📊 Boss直聘响应数据结构
|
||||
|
||||
### 响应格式示例
|
||||
|
||||
Boss直聘搜索职位列表的响应数据结构如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "Success",
|
||||
"zpData": {
|
||||
"resCount": 450, // 搜索结果总数
|
||||
"hasMore": true, // 是否还有更多
|
||||
"totalCount": 300, // 总职位数
|
||||
"jobList": [ // 职位列表
|
||||
{
|
||||
"encryptJobId": "5ae70dfe114c23ab0nR-2ti-FFpU", // 职位ID(投递必需)
|
||||
"encryptBossId": "b55854108ac215180XZ62N-_FlNT", // Boss ID(投递必需)
|
||||
"securityId": "HP23zbQfaslvy-c1...", // 安全ID(投递必需)
|
||||
"jobName": "全栈软件工程师", // 职位名称
|
||||
"salaryDesc": "25-50K·19薪", // 薪资描述(需解析)
|
||||
"jobExperience": "在校/应届", // 工作经验(需解析)
|
||||
"jobDegree": "学历不限", // 学历要求
|
||||
"city": 101020100, // 城市代码
|
||||
"cityName": "上海", // 城市名称
|
||||
"areaDistrict": "长宁区", // 区域
|
||||
"businessDistrict": "新华路", // 商圈
|
||||
"gps": { // 位置信息(优先使用)
|
||||
"longitude": 121.41902537687392,
|
||||
"latitude": 31.210308153576566
|
||||
},
|
||||
"encryptBrandId": "d283b66de3cefd891H1529q5Flc~", // 公司ID
|
||||
"brandName": "上海大裂谷智能科技", // 公司名称
|
||||
"brandScaleName": "100-499人", // 公司规模
|
||||
"brandIndustry": "人工智能", // 公司行业
|
||||
"brandStageName": "天使轮", // 融资阶段
|
||||
"bossName": "杨明雨", // Boss姓名
|
||||
"bossTitle": "HR", // Boss职位
|
||||
"bossOnline": true, // Boss是否在线
|
||||
"jobLabels": ["在校/应届", "学历不限"], // 职位标签
|
||||
"skills": [], // 技能要求
|
||||
"welfareList": ["带薪年假", "五险一金"], // 福利列表
|
||||
"proxyJob": 0, // 是否外包(0否1是)
|
||||
"industry": 100028 // 行业代码
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键字段说明
|
||||
|
||||
1. **投递必需字段**:
|
||||
- `encryptJobId`: 职位ID,投递时必须
|
||||
- `encryptBossId`: Boss ID,投递时必须
|
||||
- `securityId`: 安全ID,投递时必须(每次搜索可能不同)
|
||||
|
||||
2. **位置信息**:
|
||||
- `gps.longitude` 和 `gps.latitude`: 直接使用,无需调用位置服务API
|
||||
- 如果没有gps字段,再使用 `cityName + areaDistrict + businessDistrict + brandName` 调用位置服务
|
||||
|
||||
3. **薪资解析**:
|
||||
- `salaryDesc` 格式多样:`"25-50K·19薪"`、`"20-30K"`、`"面议"` 等
|
||||
- 需要解析出 `salaryMin` 和 `salaryMax`(单位:元)
|
||||
|
||||
4. **工作年限解析**:
|
||||
- `jobExperience` 可能为:`"在校/应届"`、`"3-5年"`、`"1-3年"` 等
|
||||
- 需要解析出 `experienceMin` 和 `experienceMax`
|
||||
|
||||
## 📊 功能架构
|
||||
|
||||
```
|
||||
用户/系统触发
|
||||
↓
|
||||
创建任务 (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. 优化位置解析(优先使用响应中的gps字段,减少API调用)
|
||||
3. 解析薪资范围(从salaryDesc提取min/max)
|
||||
4. 解析工作年限(从jobExperience提取min/max)
|
||||
5. 添加职位状态管理
|
||||
6. 添加职位匹配度字段
|
||||
|
||||
**Boss直聘响应数据字段映射表**:
|
||||
|
||||
| 响应字段 | 数据库字段 | 说明 | 示例值 |
|
||||
|---------|-----------|------|--------|
|
||||
| `encryptJobId` | `jobId` | 职位ID(加密) | "5ae70dfe114c23ab0nR-2ti-FFpU" |
|
||||
| `jobName` | `jobTitle` | 职位名称 | "全栈软件工程师" |
|
||||
| `encryptBrandId` | `companyId` | 公司ID(加密) | "d283b66de3cefd891H1529q5Flc~" |
|
||||
| `brandName` | `companyName` | 公司名称 | "上海大裂谷智能科技" |
|
||||
| `brandScaleName` | `companySize` | 公司规模 | "100-499人" |
|
||||
| `brandIndustry` | `companyIndustry` | 公司行业 | "人工智能" |
|
||||
| `brandStageName` | `brandStage` | 融资阶段 | "天使轮" |
|
||||
| `salaryDesc` | `salary` | 薪资描述 | "25-50K·19薪" |
|
||||
| `salaryDesc` | `salaryMin`, `salaryMax` | 薪资范围(需解析) | 25000, 50000 |
|
||||
| `jobExperience` | `experience` | 工作经验 | "在校/应届" |
|
||||
| `jobExperience` | `experienceMin`, `experienceMax` | 工作年限范围(需解析) | - |
|
||||
| `jobDegree` | `education` | 学历要求 | "学历不限" |
|
||||
| `jobDegree` | `educationLevel` | 学历等级(需映射) | - |
|
||||
| `city` | `city` | 城市代码 | 101020100 |
|
||||
| `cityName` | `cityName` | 城市名称 | "上海" |
|
||||
| `areaDistrict` | `areaDistrict` | 区域 | "长宁区" |
|
||||
| `businessDistrict` | `businessDistrict` | 商圈 | "新华路" |
|
||||
| `gps.longitude` | `longitude` | 经度(优先使用) | 121.41902537687392 |
|
||||
| `gps.latitude` | `latitude` | 纬度(优先使用) | 31.210308153576566 |
|
||||
| `encryptBossId` | `encryptBossId` | Boss ID(投递需要) | "b55854108ac215180XZ62N-_FlNT" |
|
||||
| `securityId` | `securityId` | 安全ID(投递需要) | "HP23zbQfaslvy-c1..." |
|
||||
| `bossName` | `bossName` | Boss姓名 | "杨明雨" |
|
||||
| `bossTitle` | `bossTitle` | Boss职位 | "HR" |
|
||||
| `bossOnline` | `bossOnline` | Boss是否在线 | true |
|
||||
| `jobLabels` | `jobLabels` | 职位标签(JSON) | ["在校/应届", "学历不限"] |
|
||||
| `skills` | `skills` | 技能要求(JSON) | ["Java", "MySQL"] |
|
||||
| `welfareList` | `welfareList` | 福利列表(JSON) | ["带薪年假", "五险一金"] |
|
||||
| `proxyJob` | `isOutsourcing` | 是否外包 | 0/1 |
|
||||
| `industry` | `industry` | 行业代码 | 100028 |
|
||||
|
||||
**关键优化点**:
|
||||
|
||||
1. **位置信息**: 优先使用响应中的 `gps.longitude` 和 `gps.latitude`,避免调用位置服务API
|
||||
- 如果 `gps` 字段存在,直接使用
|
||||
- 如果不存在,再使用 `cityName + areaDistrict + businessDistrict + brandName` 调用位置服务
|
||||
|
||||
2. **薪资解析**: 从 `salaryDesc` 解析薪资范围
|
||||
- 格式示例:`"25-50K·19薪"` → min: 25000, max: 50000
|
||||
- 格式示例:`"20-30K"` → min: 20000, max: 30000
|
||||
- 格式示例:`"面议"` → min: 0, max: 0
|
||||
- 格式示例:`"15K以上"` → min: 15000, max: 999999
|
||||
- 需要处理:K(千)、W(万)、薪(年终奖倍数)
|
||||
|
||||
3. **工作年限解析**: 从 `jobExperience` 解析年限范围
|
||||
- `"在校/应届"` → min: 0, max: 0
|
||||
- `"1-3年"` → min: 1, max: 3
|
||||
- `"3-5年"` → min: 3, max: 5
|
||||
- `"5-10年"` → min: 5, max: 10
|
||||
- `"10年以上"` → min: 10, max: 99
|
||||
|
||||
4. **学历映射**: 将学历描述映射为等级
|
||||
- `"学历不限"` → `"unlimited"`
|
||||
- `"高中"` → `"high_school"`
|
||||
- `"大专"` → `"college"`
|
||||
- `"本科"` → `"bachelor"`
|
||||
- `"硕士"` → `"master"`
|
||||
- `"博士"` → `"doctor"`
|
||||
|
||||
5. **投递必需字段**: 确保保存 `encryptJobId`、`encryptBossId` 和 `securityId`
|
||||
- 这些字段在投递时必须使用
|
||||
- `securityId` 每次搜索可能不同,需要实时保存
|
||||
|
||||
**代码位置**: 第215-308行
|
||||
|
||||
**预计工作量**: 4小时(增加字段解析逻辑)
|
||||
|
||||
---
|
||||
|
||||
### 任务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()`
|
||||
|
||||
**方法签名**:
|
||||
```javascript
|
||||
/**
|
||||
* 创建搜索职位列表任务(支持可选投递)
|
||||
* @param {Object} params - 任务参数
|
||||
* @param {number} params.id - 账号ID
|
||||
* @param {string} params.keyword - 搜索关键词
|
||||
* @param {Object} params.searchParams - 搜索条件(城市、薪资、经验、学历等)
|
||||
* @param {number} params.pageCount - 获取页数
|
||||
* @param {boolean} params.autoDeliver - 是否自动投递(默认false)
|
||||
* @param {Object} params.filterRules - 过滤规则(autoDeliver=true时使用)
|
||||
* @param {number} params.maxCount - 最大投递数量(autoDeliver=true时使用)
|
||||
* @returns {Promise<Object>} 任务创建结果 { taskId, message, jobCount, deliveredCount }
|
||||
*/
|
||||
async createSearchJobListTask(params) {
|
||||
// 1. 验证账号和授权
|
||||
// 2. 创建任务记录 (taskType: 'search_jobs' 或 'auto_deliver')
|
||||
// 3. 生成搜索指令
|
||||
// 4. 执行搜索指令
|
||||
// 5. 等待搜索完成(职位会自动保存到数据库)
|
||||
// 6. 如果 autoDeliver=true:
|
||||
// - 从数据库获取刚搜索到的职位列表
|
||||
// - 根据简历信息和过滤规则匹配职位
|
||||
// - 生成投递指令序列
|
||||
// - 执行投递指令(带间隔控制)
|
||||
// - 保存投递记录
|
||||
// - 更新职位状态
|
||||
// 7. 返回任务信息
|
||||
}
|
||||
```
|
||||
|
||||
**任务内容**:
|
||||
1. 验证账号和授权
|
||||
2. 创建任务记录(根据autoDeliver参数设置taskType: 'search_jobs' 或 'auto_deliver')
|
||||
3. 生成搜索指令(command_type: 'get_job_list')
|
||||
4. 执行搜索指令(通过MQTT发送到设备)
|
||||
5. 等待搜索完成(职位会自动保存到数据库)
|
||||
6. 如果 `autoDeliver=true`,继续执行投递流程:
|
||||
- 从数据库获取刚搜索到的职位列表(applyStatus = 'pending')
|
||||
- 根据简历信息和过滤规则匹配职位(距离、薪资、工作年限、学历等)
|
||||
- 为每个匹配的职位生成投递指令(command_type: 'apply_job')
|
||||
- 批量执行投递指令(带间隔控制,避免频繁投递)
|
||||
- 保存投递记录 (apply_records)
|
||||
- 更新职位状态 (job_postings.applyStatus = 'applied')
|
||||
7. 返回任务信息(包含搜索到的职位数量和投递数量)
|
||||
|
||||
**代码位置**: 在 `runCommand()` 方法后添加
|
||||
|
||||
**预计工作量**: 5小时(增加投递逻辑)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
### 任务6: 完善指令类型映射
|
||||
|
||||
**文件**: `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小时
|
||||
|
||||
---
|
||||
|
||||
### 任务7: 添加搜索条件配置管理
|
||||
|
||||
**文件**: `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()
|
||||
- 参数: { id, keyword, searchParams, pageCount, autoDeliver: true/false, filterRules, maxCount }
|
||||
↓
|
||||
2. 创建任务记录 (task_status)
|
||||
- taskType: 'search_jobs' 或 'auto_deliver'(根据autoDeliver参数)
|
||||
↓
|
||||
3. 生成搜索指令 (task_commands)
|
||||
- command_type: 'get_job_list'
|
||||
- command_params: { keyword, city, salary, experience, education, ... }
|
||||
↓
|
||||
4. 执行指令 (通过MQTT发送到设备)
|
||||
↓
|
||||
5. 设备执行搜索并返回职位列表
|
||||
↓
|
||||
6. 保存职位到数据库 (job_postings)
|
||||
- 去重处理
|
||||
- 位置解析(优先使用gps字段)
|
||||
- 字段映射
|
||||
- 状态: applyStatus = 'pending'(待投递)
|
||||
↓
|
||||
7. 更新搜索指令状态为完成
|
||||
↓
|
||||
8. 如果 autoDeliver=true,继续执行投递流程:
|
||||
↓
|
||||
8.1 从数据库获取刚搜索到的职位列表
|
||||
- 筛选条件: applyStatus = 'pending', sn_code = 账号SN码
|
||||
↓
|
||||
8.2 根据简历信息和过滤规则匹配职位
|
||||
- 距离匹配(基于经纬度)
|
||||
- 薪资匹配(解析salaryDesc)
|
||||
- 工作年限匹配(解析jobExperience)
|
||||
- 学历匹配(解析jobDegree)
|
||||
- 权重评分
|
||||
↓
|
||||
8.3 为每个匹配的职位生成投递指令
|
||||
- command_type: 'apply_job'
|
||||
- command_params: {
|
||||
jobId: job.encryptJobId, // 职位ID(必需)
|
||||
encryptBossId: job.encryptBossId, // Boss ID(必需)
|
||||
securityId: job.securityId, // 安全ID(必需,从最新搜索结果获取)
|
||||
brandName: job.brandName, // 公司名称(可选)
|
||||
jobTitle: job.jobName // 职位名称(可选)
|
||||
}
|
||||
↓
|
||||
8.4 批量执行投递指令(带间隔控制,避免频繁投递)
|
||||
↓
|
||||
8.5 保存投递记录 (apply_records)
|
||||
↓
|
||||
8.6 更新职位状态 (job_postings.applyStatus = 'applied')
|
||||
↓
|
||||
9. 更新任务状态为完成
|
||||
↓
|
||||
10. 返回任务信息(包含搜索到的职位数量和投递数量)
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 此接口支持两种模式:
|
||||
- `autoDeliver=false`: 仅搜索,不投递。职位保存到数据库,状态为'pending'
|
||||
- `autoDeliver=true`: 搜索完成后立即投递匹配的职位
|
||||
- **重要**: 投递必须在搜索完成后立即执行,因为 `securityId` 等字段可能有时效性,前端页面变化后这些字段可能失效
|
||||
- 不支持从已保存的职位中选择投递,因为职位信息可能已过期
|
||||
|
||||
## 📊 数据库字段说明
|
||||
|
||||
### job_postings 表需要完善的字段
|
||||
|
||||
| 字段名 | 类型 | 说明 | 状态 | 数据来源 |
|
||||
|--------|------|------|------|----------|
|
||||
| `city` | VARCHAR | 城市代码 | 待添加 | `job.city` |
|
||||
| `cityName` | VARCHAR | 城市名称 | 待添加 | `job.cityName` |
|
||||
| `areaDistrict` | VARCHAR | 区域 | 待添加 | `job.areaDistrict` |
|
||||
| `businessDistrict` | VARCHAR | 商圈 | 待添加 | `job.businessDistrict` |
|
||||
| `salaryMin` | INT | 最低薪资(元) | 待添加 | 从 `salaryDesc` 解析 |
|
||||
| `salaryMax` | INT | 最高薪资(元) | 待添加 | 从 `salaryDesc` 解析 |
|
||||
| `experienceMin` | INT | 最低工作年限 | 待添加 | 从 `jobExperience` 解析 |
|
||||
| `experienceMax` | INT | 最高工作年限 | 待添加 | 从 `jobExperience` 解析 |
|
||||
| `educationLevel` | VARCHAR | 学历等级 | 待添加 | 从 `jobDegree` 映射 |
|
||||
| `matchScore` | DECIMAL | 匹配度评分 | 待添加 | 计算得出 |
|
||||
| `encryptBossId` | VARCHAR | Boss ID | 已有 | `job.encryptBossId` |
|
||||
| `securityId` | VARCHAR | 安全ID | 待添加 | `job.securityId`(投递必需) |
|
||||
| `bossName` | VARCHAR | Boss姓名 | 待添加 | `job.bossName` |
|
||||
| `bossTitle` | VARCHAR | Boss职位 | 待添加 | `job.bossTitle` |
|
||||
| `bossOnline` | TINYINT | Boss是否在线 | 待添加 | `job.bossOnline` |
|
||||
| `brandStage` | VARCHAR | 融资阶段 | 待添加 | `job.brandStageName` |
|
||||
| `jobLabels` | JSON | 职位标签 | 待添加 | `job.jobLabels` |
|
||||
| `skills` | JSON | 技能要求 | 待添加 | `job.skills` |
|
||||
| `welfareList` | JSON | 福利列表 | 待添加 | `job.welfareList` |
|
||||
| `isOutsourcing` | TINYINT | 是否外包 | 待添加 | `job.proxyJob` |
|
||||
| `industry` | INT | 行业代码 | 待添加 | `job.industry` |
|
||||
|
||||
### pla_account 表需要添加的字段
|
||||
|
||||
| 字段名 | 类型 | 说明 | 状态 |
|
||||
|--------|------|------|------|
|
||||
| `search_config` | JSON | 搜索条件配置 | 待添加 |
|
||||
| `city` | VARCHAR | 默认城市代码 | 待添加 |
|
||||
| `cityName` | VARCHAR | 默认城市名称 | 待添加 |
|
||||
|
||||
## 🧪 测试计划
|
||||
|
||||
### 单元测试
|
||||
1. 测试搜索参数构建
|
||||
2. 测试职位数据保存
|
||||
3. 测试职位匹配算法
|
||||
4. 测试投递指令生成
|
||||
|
||||
### 集成测试
|
||||
1. 测试完整搜索流程
|
||||
2. 测试完整投递流程
|
||||
3. 测试任务创建和执行
|
||||
4. 测试MQTT通信
|
||||
|
||||
### 功能测试
|
||||
1. 测试多条件搜索
|
||||
2. 测试分页获取
|
||||
3. 测试批量投递
|
||||
4. 测试错误处理
|
||||
|
||||
## 📅 开发时间估算
|
||||
|
||||
| 任务 | 预计时间 | 优先级 |
|
||||
|------|----------|--------|
|
||||
| 任务1: 完善搜索参数支持 | 2小时 | 高 |
|
||||
| 任务2: 优化职位数据保存 | 4小时 | 高 |
|
||||
| 任务3: 完善自动投递任务搜索条件 | 2小时 | 高 |
|
||||
| 任务4: 优化职位匹配算法 | 4小时 | 高 |
|
||||
| 任务5: 创建搜索任务接口(支持可选投递) | 5小时 | 高 |
|
||||
| 任务6: 完善指令类型映射 | 1小时 | 中 |
|
||||
| 任务7: 添加搜索条件配置管理 | 1小时 | 低 |
|
||||
|
||||
**总计**: 约19小时
|
||||
|
||||
## 🚀 开发优先级
|
||||
|
||||
### 第一阶段(核心功能)
|
||||
1. 任务1: 完善搜索参数支持
|
||||
2. 任务2: 优化职位数据保存
|
||||
3. 任务3: 完善自动投递任务搜索条件
|
||||
4. 任务4: 优化职位匹配算法
|
||||
5. 任务5: 创建搜索任务接口(支持可选投递)
|
||||
|
||||
### 第二阶段(接口完善)
|
||||
6. 任务6: 完善指令类型映射
|
||||
|
||||
### 第三阶段(配置管理)
|
||||
7. 任务7: 添加搜索条件配置管理
|
||||
|
||||
## 💡 使用场景说明
|
||||
|
||||
### 场景1: 仅搜索职位列表
|
||||
```javascript
|
||||
// 只搜索职位,不投递
|
||||
const result = await plaAccountService.createSearchJobListTask({
|
||||
id: accountId,
|
||||
keyword: '全栈工程师',
|
||||
searchParams: {
|
||||
city: '101020100',
|
||||
cityName: '上海',
|
||||
salary: '20-30K',
|
||||
experience: '3-5年',
|
||||
education: '本科'
|
||||
},
|
||||
pageCount: 3,
|
||||
autoDeliver: false // 不自动投递
|
||||
});
|
||||
|
||||
// 返回: { taskId: 123, message: '搜索任务已创建', jobCount: 45 }
|
||||
// 职位会自动保存到数据库,状态为 'pending'(待投递)
|
||||
```
|
||||
|
||||
### 场景2: 搜索并自动投递(推荐)
|
||||
```javascript
|
||||
// 搜索职位并自动投递匹配的职位
|
||||
const result = await plaAccountService.createSearchJobListTask({
|
||||
id: accountId,
|
||||
keyword: '全栈工程师',
|
||||
searchParams: {
|
||||
city: '101020100',
|
||||
cityName: '上海',
|
||||
salary: '20-30K',
|
||||
experience: '3-5年',
|
||||
education: '本科'
|
||||
},
|
||||
pageCount: 3,
|
||||
autoDeliver: true, // 自动投递
|
||||
filterRules: {
|
||||
minSalary: 20000,
|
||||
maxSalary: 30000,
|
||||
keywords: ['Vue', 'React'],
|
||||
excludeKeywords: ['外包', '外派']
|
||||
},
|
||||
maxCount: 10 // 最多投递10个职位
|
||||
});
|
||||
|
||||
// 返回: { taskId: 123, message: '搜索并投递任务已创建', jobCount: 45, deliveredCount: 8 }
|
||||
```
|
||||
|
||||
**重要说明**:
|
||||
- **投递必须在搜索完成后立即执行**,因为 `securityId` 等字段可能有时效性
|
||||
- 前端页面变化后,已保存的职位信息中的 `securityId` 可能失效,无法用于投递
|
||||
- 因此不支持从已保存的职位中选择投递,必须在搜索后立即投递
|
||||
- 如果只需要搜索不投递,设置 `autoDeliver: false`
|
||||
- 如果需要搜索并投递,设置 `autoDeliver: true`,系统会根据匹配规则自动投递
|
||||
|
||||
## 📌 注意事项
|
||||
|
||||
1. **命名规范**: 统一使用下划线命名(`get_job_list` 而不是 `getJobList`)
|
||||
2. **错误处理**: 所有方法都需要完善的错误处理和日志记录
|
||||
3. **数据验证**: 所有输入参数都需要验证
|
||||
4. **性能优化**: 批量操作需要考虑性能,避免阻塞
|
||||
5. **MQTT通信**: 确保指令参数格式正确,与客户端协议一致
|
||||
6. **数据库事务**: 批量操作需要使用事务保证数据一致性
|
||||
7. **投递时机**: 投递必须在搜索完成后立即执行,因为 `securityId` 等字段可能有时效性,前端页面变化后这些字段可能失效
|
||||
8. **职位状态验证**: 投递前必须验证职位状态(applyStatus = 'pending'),避免重复投递
|
||||
9. **投递必需字段**: 投递时需要 `encryptJobId`、`encryptBossId` 和 `securityId`,这些字段必须从最新搜索结果中获取
|
||||
10. **位置信息**: 优先使用响应中的 `gps` 字段,避免不必要的API调用
|
||||
11. **接口设计**: 搜索和投递在同一接口中完成,不支持单独的投递接口,因为已保存的职位信息可能已过期
|
||||
|
||||
## 🔗 相关文件
|
||||
|
||||
- `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` - 账号数据模型
|
||||
|
||||
210
_doc/数据库表同步指南.md
210
_doc/数据库表同步指南.md
@@ -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` - 用户ID(VARCHAR(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' }
|
||||
);
|
||||
```
|
||||
|
||||
@@ -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 目录只保留实际使用的服务
|
||||
|
||||
@@ -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服务
|
||||
- 统一类命名
|
||||
|
||||
@@ -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
544
_doc/消息.md
Normal 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
|
||||
}
|
||||
]
|
||||
@@ -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 路径
|
||||
- 测试确保功能正常
|
||||
|
||||
@@ -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 # 日志代理
|
||||
```
|
||||
|
||||
@@ -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`
|
||||
|
||||
238
_doc/简历功能实现总结.md
238
_doc/简历功能实现总结.md
@@ -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智能分析的完整闭环,为自动化求职系统提供了坚实的数据基础。所有核心功能已实现并经过测试,可以投入使用。
|
||||
|
||||
251
_doc/简历功能快速参考.md
251
_doc/简历功能快速参考.md
@@ -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 }
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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: 简历ID(UUID)
|
||||
- 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等)
|
||||
|
||||
281
_doc/聊天列表功能说明.md
281
_doc/聊天列表功能说明.md
@@ -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/长轮询等)
|
||||
151
_doc/聊天功能快速开始.md
151
_doc/聊天功能快速开始.md
@@ -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**: 初始版本发布
|
||||
- 实现基础聊天列表功能
|
||||
- 支持消息发送和接收
|
||||
- 添加轮询机制
|
||||
- 支持平台筛选和搜索
|
||||
4020
_doc/职位列表.json
4020
_doc/职位列表.json
File diff suppressed because it is too large
Load Diff
@@ -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 调度架构*
|
||||
|
||||
255
_doc/重构完成说明.md
255
_doc/重构完成说明.md
@@ -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*
|
||||
|
||||
387
_doc/项目功能总结.md
387
_doc/项目功能总结.md
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
@@ -360,15 +360,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 +374,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 +485,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>
|
||||
|
||||
@@ -708,13 +700,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 +740,7 @@ export default {
|
||||
},
|
||||
// 指令详情弹窗
|
||||
commandDetailVisible: false,
|
||||
retryCommandLoading: false,
|
||||
currentCommandDetail: null,
|
||||
|
||||
// 二维码弹窗
|
||||
@@ -825,13 +818,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: {
|
||||
@@ -1081,23 +1074,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
|
||||
}
|
||||
},
|
||||
|
||||
// 取消任务
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -165,7 +165,7 @@ switch (type) {
|
||||
break;
|
||||
case 'chat':
|
||||
Model = models.chat_records;
|
||||
dateField = 'sendTime';
|
||||
dateField = 'updateTime';
|
||||
break;
|
||||
default:
|
||||
return ctx.fail('无效的统计类型');
|
||||
|
||||
@@ -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 = {
|
||||
/**
|
||||
|
||||
@@ -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: 指令名称
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -439,9 +439,7 @@ return ctx.success({
|
||||
return ctx.fail('任务不存在');
|
||||
}
|
||||
|
||||
if (task.status !== 'failed') {
|
||||
return ctx.fail('只能重试失败的任务');
|
||||
}
|
||||
|
||||
|
||||
await task_status.update({
|
||||
status: 'pending',
|
||||
|
||||
93
api/controller_front/static.js
Normal file
93
api/controller_front/static.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
|
||||
/**
|
||||
* 静态 JS 代理/缓存
|
||||
*
|
||||
* 规则:
|
||||
* - 前端请求:GET /static/boss,header 里带 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;
|
||||
|
||||
},
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -414,7 +404,7 @@ module.exports = {
|
||||
|
||||
// 如果源值是对象且目标值也是对象,递归合并(排除数组)
|
||||
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 {
|
||||
// 否则直接覆盖
|
||||
@@ -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('保存账号配置失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
const ai_service_module = require('../../../services/ai_service');
|
||||
const ai_service = ai_service_module.getInstance();
|
||||
const ai_service = require('../../../services/ai_service');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
/**
|
||||
* 聊天管理模块
|
||||
@@ -8,13 +8,24 @@ const ai_service = ai_service_module.getInstance();
|
||||
class ChatManager {
|
||||
/**
|
||||
* 解析沟通列表返回值,统一为 { friendList, foldText, ... }
|
||||
* 设备端可能返回 code:0 + zpData 或 code:200 + data
|
||||
* 只支持新的结构:
|
||||
* response.data = { success, apiData: [ { response: { code, zpData:{...} } } ] }
|
||||
* @private
|
||||
*/
|
||||
_parse_chat_list_response(response) {
|
||||
if (!response) return null;
|
||||
const raw = response.zpData != null ? response.zpData : response.data;
|
||||
if (!raw) return { friendList: [], foldText: '', filterEncryptIdList: [], filterBossIdList: [] };
|
||||
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 || '',
|
||||
@@ -42,52 +53,76 @@ class ChatManager {
|
||||
data: { pageCount }
|
||||
});
|
||||
|
||||
// 沟通列表接口成功为 code: 0 或 code: 200
|
||||
const ok = response && (response.code === 0 || response.code === 200);
|
||||
// 只认新结构: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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析沟通详情返回值(两种形态二选一)
|
||||
* 形态1 - 会话/职位信息: zpData.data + zpData.job
|
||||
* 形态2 - 聊天消息列表: zpData.hasMore + zpData.messages
|
||||
* 解析 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 null;
|
||||
const raw = response.zpData != null ? response.zpData : response.data;
|
||||
if (!raw) return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] };
|
||||
if (!response) return { variant: 'unknown', data: null, job: null, hasMore: false, messages: [] };
|
||||
|
||||
// 形态2: 消息列表(有 messages 数组)
|
||||
if (Array.isArray(raw.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: !!raw.hasMore,
|
||||
messages: raw.messages,
|
||||
type: raw.type,
|
||||
minMsgId: raw.minMsgId
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// 形态1: 会话详情(data + job)
|
||||
if (raw.data != null || raw.job != null) {
|
||||
if (boss_zp && (boss_zp.data != null || boss_zp.job != null)) {
|
||||
return {
|
||||
variant: 'session',
|
||||
data: raw.data || null,
|
||||
job: raw.job || null
|
||||
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', ... }
|
||||
@@ -100,26 +135,29 @@ class ChatManager {
|
||||
*/
|
||||
async get_chat_detail(sn_code, mqttClient, params = {}) {
|
||||
const { platform = 'boss', ...rest } = params;
|
||||
console.log(`[聊天管理] 开始获取设备 ${sn_code} 的沟通详情`);
|
||||
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
|
||||
data: { ...rest, friendId }
|
||||
});
|
||||
|
||||
const ok = response && (response.code === 0 || response.code === 200);
|
||||
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;
|
||||
}
|
||||
@@ -162,38 +200,74 @@ class ChatManager {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 AI 自动决定是否回复,并发送回复
|
||||
* 流程:
|
||||
* 1. 根据参数获取沟通详情(消息列表)
|
||||
* 2. 如果最后一句是 HR 说的,则调用阿里云 Qwen 生成回复文案
|
||||
* 3. 通过 send_chat_message 把回复发出去
|
||||
*
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {object} mqttClient - MQTT客户端
|
||||
* @param {object} params - 包含 friendId + 获取详情所需参数(如 encryptBossId/encryptJobId 等)
|
||||
* @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;
|
||||
/** 是否为系统/模板消息(竞争者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;
|
||||
}
|
||||
|
||||
if (!friendId) {
|
||||
throw new Error('friendId 不能为空');
|
||||
}
|
||||
/** 统一 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;
|
||||
}
|
||||
|
||||
// 1. 获取沟通详情(期望拿到消息列表)
|
||||
const detail = await this.get_chat_detail(sn_code, mqttClient, {
|
||||
platform,
|
||||
...detailParams
|
||||
/** 过滤出 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_log,options 含 sn_code/platform/friendId/encryptFriendId,securityId 为 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;
|
||||
|
||||
// 2. 推断 HR 与 求职者 uid
|
||||
let hr_uid = null;
|
||||
let geek_uid = null;
|
||||
|
||||
@@ -201,143 +275,130 @@ class ChatManager {
|
||||
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 (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 last = messages[messages.length - 1];
|
||||
|
||||
// 兜底:还没有 hr_uid 时,用最后一条的 from/to 做简单推断
|
||||
if ((!hr_uid || !geek_uid) && last && last.from && last.to) {
|
||||
hr_uid = hr_uid || last.from.uid;
|
||||
geek_uid = geek_uid || last.to.uid;
|
||||
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;
|
||||
}
|
||||
|
||||
if (!last || !last.from || !hr_uid || last.from.uid !== hr_uid) {
|
||||
// 最后一条不是 HR 发的,不自动回复
|
||||
return { replied: false, reason: '最后一条不是HR消息' };
|
||||
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发的' };
|
||||
}
|
||||
|
||||
// 取 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 (!hr_message_text || !hr_message_text.trim()) {
|
||||
return { replied: false, reason: 'HR消息没有可用文本' };
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 调用阿里云 Qwen 生成回复文案(已在 config 中切换为 qwen-plus)
|
||||
const jobInfo = detail.job || {};
|
||||
|
||||
const reply_content = await ai_service.generateChatContent({
|
||||
const { action, reply_content } = await ai_service.replyIntentAndContent({
|
||||
jobInfo,
|
||||
resumeInfo: null,
|
||||
chatType: 'reply',
|
||||
hrMessage: hr_message_text,
|
||||
previousMessages: [] // 如需上下文,这里可以把 detail.messages 映射进去
|
||||
previousMessages: hrList.slice(-5).map(m => (m.body && m.body.text) || m.pushText || '')
|
||||
});
|
||||
|
||||
if (!reply_content || !reply_content.trim()) {
|
||||
return { replied: false, reason: 'AI 未生成有效回复' };
|
||||
if (action === 'no_reply') {
|
||||
this._saveReplyIntentLog(options, hr_message_text, jobInfo, action, reply_content, false, 'HR表示暂不匹配/无需回复', security_id || null);
|
||||
return { replied: false, reason: 'HR表示暂不匹配/无需回复' };
|
||||
}
|
||||
|
||||
// 4. 通过统一的 send_chat_message 下发回复
|
||||
await this.send_chat_message(sn_code, mqttClient, {
|
||||
friendId,
|
||||
messages: [{ type: 'text', content: reply_content }],
|
||||
chatType: 'reply',
|
||||
platform
|
||||
});
|
||||
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,
|
||||
reply_content,
|
||||
action: action || 'text',
|
||||
reply_content: reply_content || '',
|
||||
hr_message_text
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动获取沟通列表 + 按会话自动 AI 回复
|
||||
* 1. 调用 get_chat_list 获取会话列表
|
||||
* 2. 对每个会话按 friendId 调用 auto_reply_with_ai(内部会先获取详情,再决定是否回复)
|
||||
* 使用 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 - { platform?, pageCount? }
|
||||
* @returns {Promise<object>} { success, total_contacts, replied_count, details: [...] }
|
||||
* @param {object} params - 包含 friendId + 获取详情所需参数
|
||||
* @returns {Promise<object>} { replied, reply_content?, hr_message_text?, reason? }
|
||||
*/
|
||||
async auto_chat_ai(sn_code, mqttClient, params = {}) {
|
||||
const { platform = 'boss', pageCount = 3 } = params;
|
||||
async auto_reply_with_ai(sn_code, mqttClient, params = {}) {
|
||||
const { friendId, platform = 'boss', ...detailParams } = params;
|
||||
if (!friendId) throw new Error('friendId 不能为空');
|
||||
|
||||
// 1. 获取沟通列表
|
||||
const listResult = await this.get_chat_list(sn_code, mqttClient, {
|
||||
const parsed = await this.get_chat_detail(sn_code, mqttClient, { platform, ...detailParams });
|
||||
const decision = await this.getReplyContentFromDetail(parsed, {
|
||||
sn_code,
|
||||
platform,
|
||||
pageCount
|
||||
friendId,
|
||||
encryptFriendId: detailParams.encryptFriendId || ''
|
||||
});
|
||||
if (!decision.replied) return decision;
|
||||
|
||||
const friendList = Array.isArray(listResult.friendList) ? listResult.friendList : [];
|
||||
|
||||
if (friendList.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
total_contacts: 0,
|
||||
replied_count: 0,
|
||||
details: [],
|
||||
message: '没有可沟通的会话'
|
||||
};
|
||||
}
|
||||
|
||||
let replied_count = 0;
|
||||
const details = [];
|
||||
|
||||
// 2. 逐个会话顺序处理,避免并发下发指令
|
||||
for (const friend of friendList) {
|
||||
const friendId = friend.friendId;
|
||||
if (!friendId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await this.auto_reply_with_ai(sn_code, mqttClient, {
|
||||
platform,
|
||||
friendId
|
||||
});
|
||||
|
||||
if (r.replied) {
|
||||
replied_count++;
|
||||
}
|
||||
|
||||
details.push({
|
||||
friendId,
|
||||
replied: !!r.replied,
|
||||
reason: r.reason || null,
|
||||
reply_content: r.reply_content || null
|
||||
});
|
||||
} catch (error) {
|
||||
details.push({
|
||||
friendId,
|
||||
replied: false,
|
||||
reason: error.message || '自动回复失败',
|
||||
reply_content: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
success: true,
|
||||
total_contacts: friendList.length,
|
||||
replied_count,
|
||||
details,
|
||||
message: '自动获取列表并尝试AI回复完成'
|
||||
replied: true,
|
||||
reply_content: decision.reply_content,
|
||||
hr_message_text: decision.hr_message_text
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
const aiServiceModule = require('../../../services/ai_service');
|
||||
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');
|
||||
|
||||
// 实例化AI服务
|
||||
const aiService = aiServiceModule.getInstance();
|
||||
|
||||
/**
|
||||
* 工作管理模块
|
||||
* 负责简历获取、分析、存储和匹配度计算
|
||||
@@ -397,6 +394,29 @@ class JobManager {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取期望 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`);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取岗位列表(支持多条件搜索)
|
||||
* @param {string} sn_code - 设备SN码
|
||||
@@ -406,7 +426,7 @@ class JobManager {
|
||||
*/
|
||||
async get_job_list(sn_code, mqttClient, params = {}) {
|
||||
const {
|
||||
keyword = '前端',
|
||||
keyword: paramKeyword = '',
|
||||
platform = 'boss',
|
||||
pageCount = 3,
|
||||
city = '',
|
||||
@@ -418,22 +438,60 @@ class JobManager {
|
||||
companySize = '',
|
||||
financingStage = '',
|
||||
page = 1,
|
||||
pageSize = 20
|
||||
pageSize = 20,
|
||||
tabLabel,
|
||||
tabIndex,
|
||||
job_type_id
|
||||
} = params;
|
||||
|
||||
// 判断是否是多条件搜索(如果包含多条件参数,使用多条件搜索逻辑)
|
||||
const hasMultiParams = city || cityName || salary || experience || education ||
|
||||
industry || companySize || financingStage || page || pageSize;
|
||||
// 推荐列表实际按期望 tab 拉取,入库 keyword 应与「职位来源」一致:显式 keyword > deliver_tab_label > 空(不再默认「前端」)
|
||||
let keyword = paramKeyword != null && String(paramKeyword).trim() !== ''
|
||||
? String(paramKeyword).trim()
|
||||
: '';
|
||||
if (!keyword && tabLabel != null && String(tabLabel).trim() !== '') {
|
||||
keyword = String(tabLabel).trim();
|
||||
}
|
||||
|
||||
// 仅当调用方显式传入筛选/分页字段时才走多条件分支(避免 page/pageSize 默认值把简单拉列表永远判成「多条件」)
|
||||
const hasMultiParams = Boolean(
|
||||
(params.city != null && params.city !== '') ||
|
||||
(params.cityName != null && params.cityName !== '') ||
|
||||
(params.salary != null && params.salary !== '') ||
|
||||
(params.experience != null && params.experience !== '') ||
|
||||
(params.education != null && params.education !== '') ||
|
||||
(params.industry != null && params.industry !== '') ||
|
||||
(params.companySize != null && params.companySize !== '') ||
|
||||
(params.financingStage != null && params.financingStage !== '') ||
|
||||
params.page !== undefined ||
|
||||
params.pageSize !== undefined
|
||||
);
|
||||
|
||||
/** 期望 tab 与职位类型需原样带给设备端 get_job_list,否则不会切换 .c-expect-select 标签 */
|
||||
const appendExpectTabToData = (data) => {
|
||||
if (tabLabel != null && String(tabLabel).trim() !== '') {
|
||||
data.tabLabel = String(tabLabel).trim();
|
||||
}
|
||||
if (tabIndex !== undefined && tabIndex !== null && String(tabIndex).trim() !== '') {
|
||||
const ti = Number(tabIndex);
|
||||
if (!Number.isNaN(ti)) {
|
||||
data.tabIndex = ti;
|
||||
}
|
||||
}
|
||||
if (job_type_id != null && job_type_id !== '') {
|
||||
data.job_type_id = job_type_id;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
if (hasMultiParams) {
|
||||
// 使用多条件搜索逻辑
|
||||
console.log(`[工作管理] 开始多条件搜索设备 ${sn_code} 的职位,关键词: ${keyword}, 城市: ${cityName || city}`);
|
||||
|
||||
// 构建完整的搜索参数对象
|
||||
const searchData = {
|
||||
const searchData = appendExpectTabToData({
|
||||
keyword,
|
||||
pageCount
|
||||
};
|
||||
});
|
||||
|
||||
// 添加可选搜索条件
|
||||
if (city) searchData.city = city;
|
||||
@@ -493,11 +551,11 @@ class JobManager {
|
||||
// 简单搜索逻辑(保持原有逻辑)
|
||||
console.log(`[工作管理] 开始获取设备 ${sn_code} 的岗位列表,关键词: ${keyword}`);
|
||||
|
||||
// 通过MQTT指令获取岗位列表
|
||||
// 通过MQTT指令获取岗位列表(须带上 tabLabel/tabIndex,与 schedule/deliver 下发一致)
|
||||
const response = await mqttClient.publishAndWait(sn_code, {
|
||||
platform,
|
||||
action: "get_job_list",
|
||||
data: { keyword, pageCount }
|
||||
data: appendExpectTabToData({ keyword, pageCount })
|
||||
});
|
||||
|
||||
if (!response || response.code !== 200) {
|
||||
@@ -603,16 +661,16 @@ class JobManager {
|
||||
|
||||
|
||||
// 等待 1秒
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// 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);
|
||||
// }
|
||||
}
|
||||
|
||||
// 检查是否已存在(根据 jobId 和 sn_code)
|
||||
@@ -705,9 +763,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({
|
||||
@@ -715,7 +773,7 @@ class JobManager {
|
||||
sn_code: sn_code,
|
||||
companyName: jobData.companyName,
|
||||
applyTime: {
|
||||
[Sequelize.Op.gte]: oneMonthAgo
|
||||
[Sequelize.Op.gte]: thirtyDaysAgo
|
||||
}
|
||||
},
|
||||
order: [['applyTime', 'DESC']]
|
||||
@@ -723,18 +781,18 @@ 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天内不重复投递`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
const aiServiceModule = require('../../../services/ai_service');
|
||||
const aiService = require('../../../services/ai_service');
|
||||
const { jobFilterService } = require('../services');
|
||||
const logs = require('../../logProxy');
|
||||
const db = require('../../dbProxy');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
// 实例化AI服务
|
||||
const aiService = aiServiceModule.getInstance();
|
||||
|
||||
/**
|
||||
* 简历管理模块
|
||||
* 负责简历获取、分析、存储和匹配度计算
|
||||
|
||||
@@ -655,71 +655,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 - 职位数据
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const db = require('../dbProxy.js');
|
||||
const logProxy = require('../logProxy.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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,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字符串
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -191,15 +192,16 @@ class CommandManager {
|
||||
start_time
|
||||
);
|
||||
|
||||
// 推送指令失败状态
|
||||
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);
|
||||
// 推送指令失败状态(需有 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,21 +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) {
|
||||
// 获取指令超时时间(从配置中获取,默认5分钟)
|
||||
const timeout = ScheduleConfig.taskTimeouts[command_type] || 5 * 60 * 1000;
|
||||
|
||||
// 构建指令执行 Promise
|
||||
const command_promise = (async () => {
|
||||
// 直接使用 command_type 调用 jobManager 的方法,不做映射
|
||||
// command_type 和 jobManager 的方法名保持一致
|
||||
if (jobManager[command_type]) {
|
||||
return await jobManager[command_type](sn_code, mqttClient, command_params);
|
||||
} else {
|
||||
const fn = jobManager[command_type];
|
||||
if (!fn) {
|
||||
throw new Error(`未知的指令类型: ${command_type}, jobManager 中不存在对应方法`);
|
||||
}
|
||||
return await fn(sn_code, mqttClient, command_params);
|
||||
})();
|
||||
|
||||
// 使用超时机制包装
|
||||
|
||||
@@ -20,6 +20,14 @@ class ScheduledJobs {
|
||||
this.taskQueue = components.taskQueue;
|
||||
this.taskHandlers = taskHandlers;
|
||||
this.jobs = [];
|
||||
|
||||
// 业务任务防重入标记(按任务类型存)
|
||||
this._runningFlags = {
|
||||
auto_search: false,
|
||||
auto_deliver: false,
|
||||
auto_chat: false,
|
||||
auto_active: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,7 +98,7 @@ class ScheduledJobs {
|
||||
console.log('[定时任务] ✓ 已启动自动投递任务 (每1分钟)');
|
||||
|
||||
// 3. 自动沟通任务 - 每15分钟执行一次
|
||||
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */15 * * * *', () => {
|
||||
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */1 * * * *', () => {
|
||||
this.runAutoChatTask();
|
||||
});
|
||||
this.jobs.push(autoChatJob);
|
||||
@@ -120,6 +128,13 @@ class ScheduledJobs {
|
||||
* 为所有启用自动搜索的账号添加搜索任务
|
||||
*/
|
||||
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');
|
||||
|
||||
@@ -146,6 +161,8 @@ class ScheduledJobs {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动搜索调度] 执行失败:', error);
|
||||
} finally {
|
||||
this._runningFlags[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +171,13 @@ class ScheduledJobs {
|
||||
* 为所有启用自动投递的账号添加投递任务
|
||||
*/
|
||||
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');
|
||||
|
||||
@@ -180,6 +204,8 @@ class ScheduledJobs {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动投递调度] 执行失败:', error);
|
||||
} finally {
|
||||
this._runningFlags[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +214,13 @@ class ScheduledJobs {
|
||||
* 为所有启用自动沟通的账号添加沟通任务
|
||||
*/
|
||||
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');
|
||||
|
||||
@@ -214,6 +247,8 @@ class ScheduledJobs {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动沟通调度] 执行失败:', error);
|
||||
} finally {
|
||||
this._runningFlags[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +257,13 @@ class ScheduledJobs {
|
||||
* 为所有启用自动活跃的账号添加活跃任务
|
||||
*/
|
||||
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');
|
||||
|
||||
@@ -248,6 +290,8 @@ class ScheduledJobs {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动活跃调度] 执行失败:', error);
|
||||
} finally {
|
||||
this._runningFlags[key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -388,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,不允许创建任务
|
||||
|
||||
@@ -60,10 +60,11 @@ class ActiveHandler extends BaseHandler {
|
||||
}
|
||||
|
||||
// 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: `自动活跃 - ${action}`,
|
||||
command_name: actionNameMap[action] || `活跃-${action}`,
|
||||
command_params: JSON.stringify({
|
||||
sn_code,
|
||||
platform: platform || accountConfig.platform_type || 'boss',
|
||||
|
||||
@@ -2,10 +2,12 @@ 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消息
|
||||
* 负责自动回复 HR 消息。auto_chat 是任务,其下按指令执行:获取列表 → 获取详情 →(若需回复)发送消息
|
||||
*/
|
||||
class ChatHandler extends BaseHandler {
|
||||
/**
|
||||
@@ -24,66 +26,209 @@ class ChatHandler extends BaseHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行沟通逻辑
|
||||
* 执行沟通逻辑:先下发「获取列表」指令,再对每个会话下发「获取详情」→(若需回复)「发送消息」指令
|
||||
*/
|
||||
async doChat(task) {
|
||||
const { sn_code, taskParams } = task;
|
||||
const { platform = 'boss' } = taskParams;
|
||||
const platform = taskParams.platform || 'boss';
|
||||
|
||||
console.log(`[自动沟通] 开始 - 设备: ${sn_code}`);
|
||||
|
||||
// 1. 获取账户配置
|
||||
const accountConfig = await this.getAccountConfig(sn_code, ['platform_type', 'chat_strategy']);
|
||||
|
||||
if (!accountConfig) {
|
||||
return {
|
||||
chatCount: 0,
|
||||
message: '未找到账户配置'
|
||||
};
|
||||
return { chatCount: 0, message: '未找到账户配置' };
|
||||
}
|
||||
|
||||
// 2. 解析沟通策略配置
|
||||
const chatStrategy = ConfigManager.parseChatStrategy(accountConfig.chat_strategy);
|
||||
|
||||
// 3. 检查沟通时间范围
|
||||
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
|
||||
};
|
||||
return { chatCount: 0, message: timeCheck.reason };
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 创建自动沟通 AI 指令(内部会先获取列表,再获取详情并自动回复)
|
||||
const chatCommand = {
|
||||
command_type: 'auto_chat_ai',
|
||||
command_name: '自动沟通AI回复',
|
||||
command_params: {
|
||||
platform: platform || accountConfig.platform_type || 'boss',
|
||||
pageCount: chatStrategy.page_count || 3
|
||||
},
|
||||
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 : [];
|
||||
|
||||
// 5. 执行指令(任务队列会保证该设备内串行执行,不并发下发指令)
|
||||
const exec_result = await command.executeCommands(task.id, [chatCommand], this.mqttClient);
|
||||
const first = exec_result && Array.isArray(exec_result.results) && exec_result.results[0]
|
||||
? exec_result.results[0].result || {}
|
||||
: {};
|
||||
if (friend_list.length === 0) {
|
||||
console.log(`[自动沟通] 完成 - 设备: ${sn_code},无会话`);
|
||||
return { chatCount: 0, message: '没有可沟通的会话', detail: { total_contacts: 0 } };
|
||||
}
|
||||
|
||||
console.log(`[自动沟通] 完成 - 设备: ${sn_code}`);
|
||||
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: first.replied_count || 0,
|
||||
message: first.message || '自动沟通完成',
|
||||
detail: first
|
||||
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;
|
||||
|
||||
@@ -4,7 +4,6 @@ const jobFilterEngine = require('../services/jobFilterEngine');
|
||||
const command = require('../core/command');
|
||||
const config = require('../infrastructure/config');
|
||||
const db = require('../../dbProxy');
|
||||
const { jobFilterService } = require('../../job/services');
|
||||
|
||||
/**
|
||||
* 自动投递处理器
|
||||
@@ -82,11 +81,25 @@ class DeliverHandler extends BaseHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 获取职位类型配置
|
||||
const jobTypeConfig = await this.getJobTypeConfig(accountConfig.job_type_id);
|
||||
// 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. 搜索职位列表
|
||||
await this.searchJobs(sn_code, platform, keyword || accountConfig.keyword, pageCount, task.id);
|
||||
// 6. 下发 get_job_list 拉取职位列表(tabLabel 切换期望 tab,job_type_id 随指令下发供设备使用)
|
||||
const tabLabel = resume.deliver_tab_label || '';
|
||||
await this.getJobList(
|
||||
sn_code,
|
||||
platform,
|
||||
pageCount,
|
||||
task.id,
|
||||
tabLabel,
|
||||
accountConfig.job_type_id,
|
||||
accountConfig.keyword
|
||||
);
|
||||
|
||||
// 7. 从数据库获取待投递职位
|
||||
const pendingJobs = await this.getPendingJobs(sn_code, platform, actualMaxCount * 3);
|
||||
@@ -104,13 +117,13 @@ class DeliverHandler extends BaseHandler {
|
||||
// 9. 过滤已投递的公司
|
||||
const recentCompanies = await this.getRecentDeliveredCompanies(sn_code, 30);
|
||||
|
||||
// 10. 过滤、评分、排序职位
|
||||
const filteredJobs = await this.filterAndScoreJobs(
|
||||
// 10. 过滤 + 评分 + 按 60 分阈值筛(入口在 jobFilterEngine,便于阅读)
|
||||
const filteredJobs = await jobFilterEngine.filterAndScoreJobsForDeliver(
|
||||
pendingJobs,
|
||||
filterConfig,
|
||||
resume,
|
||||
accountConfig,
|
||||
jobTypeConfig,
|
||||
filterConfig,
|
||||
recentCompanies
|
||||
);
|
||||
|
||||
@@ -201,7 +214,7 @@ class DeliverHandler extends BaseHandler {
|
||||
|
||||
try {
|
||||
await command.executeCommands(taskId, [{
|
||||
command_type: 'getOnlineResume',
|
||||
command_type: 'get_online_resume',
|
||||
command_name: '获取在线简历',
|
||||
command_params: JSON.stringify({ sn_code, platform }),
|
||||
priority: config.getTaskPriority('get_resume') || 5
|
||||
@@ -221,11 +234,42 @@ class DeliverHandler extends BaseHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取职位类型配置
|
||||
* 获取职位类型配置;若传入 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) {
|
||||
if (!jobTypeId) return null;
|
||||
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);
|
||||
@@ -237,19 +281,33 @@ class DeliverHandler extends BaseHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索职位列表
|
||||
* 下发 get_job_list 命令拉取职位列表
|
||||
* @param {string} tabLabel - 投递用期望标签文案,对应 resume_info.deliver_tab_label,get_job_list 会按此选择 tab
|
||||
* @param {number} jobTypeId - 职位类型 ID,随指令下发供设备使用
|
||||
*/
|
||||
async searchJobs(sn_code, platform, keyword, pageCount, taskId) {
|
||||
async getJobList(sn_code, platform, pageCount, taskId, tabLabel = '', jobTypeId = null, accountKeyword = '') {
|
||||
const label = tabLabel != null && String(tabLabel).trim() !== '' ? String(tabLabel).trim() : '';
|
||||
const accKw = accountKeyword != null && String(accountKeyword).trim() !== '' ? String(accountKeyword).trim() : '';
|
||||
// 与 jobManager 一致:优先期望职位文案,其次账户搜索词,用于 job_postings.keyword
|
||||
const keyword = label || accKw;
|
||||
|
||||
const params = {
|
||||
sn_code,
|
||||
platform,
|
||||
pageCount,
|
||||
keyword
|
||||
};
|
||||
if (label) {
|
||||
params.tabLabel = label;
|
||||
}
|
||||
if (jobTypeId != null && jobTypeId !== '') {
|
||||
params.job_type_id = jobTypeId;
|
||||
}
|
||||
const getJobListCommand = {
|
||||
command_type: 'getJobList',
|
||||
command_type: 'get_job_list',
|
||||
command_name: '获取职位列表',
|
||||
command_params: JSON.stringify({
|
||||
sn_code,
|
||||
keyword,
|
||||
platform,
|
||||
pageCount
|
||||
}),
|
||||
priority: config.getTaskPriority('search_jobs') || 5
|
||||
command_params: JSON.stringify(params),
|
||||
priority: config.getTaskPriority('auto_deliver') || 7
|
||||
};
|
||||
|
||||
await command.executeCommands(taskId, [getJobListCommand], this.mqttClient);
|
||||
@@ -279,16 +337,22 @@ class DeliverHandler extends BaseHandler {
|
||||
*/
|
||||
mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig) {
|
||||
// 排除关键词
|
||||
const jobTypeExclude = jobTypeConfig?.excludeKeywords
|
||||
const rawJobTypeExclude = jobTypeConfig?.excludeKeywords
|
||||
? ConfigManager.parseConfig(jobTypeConfig.excludeKeywords, [])
|
||||
: [];
|
||||
|
||||
const deliverExclude = ConfigManager.getExcludeKeywords(deliverConfig);
|
||||
const filterExclude = filterRules.excludeKeywords || [];
|
||||
const jobTypeExclude = Array.isArray(rawJobTypeExclude) ? rawJobTypeExclude : [];
|
||||
|
||||
const deliverExcludeRaw = ConfigManager.getExcludeKeywords(deliverConfig);
|
||||
const deliverExclude = Array.isArray(deliverExcludeRaw) ? deliverExcludeRaw : [];
|
||||
const filterExcludeRaw = filterRules.excludeKeywords || [];
|
||||
const filterExclude = Array.isArray(filterExcludeRaw) ? filterExcludeRaw : [];
|
||||
|
||||
// 过滤关键词
|
||||
const deliverFilter = ConfigManager.getFilterKeywords(deliverConfig);
|
||||
const filterKeywords = filterRules.keywords || [];
|
||||
const deliverFilterRaw = ConfigManager.getFilterKeywords(deliverConfig);
|
||||
const deliverFilter = Array.isArray(deliverFilterRaw) ? deliverFilterRaw : [];
|
||||
const filterKeywordsRaw = filterRules.keywords || [];
|
||||
const filterKeywords = Array.isArray(filterKeywordsRaw) ? filterKeywordsRaw : [];
|
||||
|
||||
// 薪资范围
|
||||
const salaryRange = filterRules.minSalary || filterRules.maxSalary
|
||||
@@ -326,70 +390,13 @@ class DeliverHandler extends BaseHandler {
|
||||
return new Set(recentApplies.map(apply => apply.companyName).filter(Boolean));
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤和评分职位
|
||||
*/
|
||||
async filterAndScoreJobs(jobs, resume, accountConfig, jobTypeConfig, filterConfig, recentCompanies) {
|
||||
const scored = [];
|
||||
|
||||
for (const job of jobs) {
|
||||
// 1. 过滤近期已投递的公司
|
||||
if (job.companyName && recentCompanies.has(job.companyName)) {
|
||||
console.log(`[自动投递] 跳过已投递公司: ${job.companyName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. 使用 jobFilterEngine 过滤和评分
|
||||
const filtered = await jobFilterEngine.filterJobs([job], filterConfig, resume);
|
||||
if (filtered.length === 0) {
|
||||
continue; // 不符合过滤条件
|
||||
}
|
||||
|
||||
// 3. 使用原有的评分系统(job_filter_service)计算详细分数
|
||||
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
|
||||
job,
|
||||
resume,
|
||||
accountConfig,
|
||||
jobTypeConfig,
|
||||
accountConfig.is_salary_priority || []
|
||||
);
|
||||
|
||||
// 4. 计算关键词奖励
|
||||
const KeywordMatcher = require('../utils/keywordMatcher');
|
||||
const keywordBonus = KeywordMatcher.calculateBonus(
|
||||
`${job.jobTitle} ${job.companyName} ${job.jobDescription || ''}`,
|
||||
filterConfig.filter_keywords,
|
||||
{ baseScore: 5, maxBonus: 20 }
|
||||
);
|
||||
|
||||
const finalScore = scoreResult.totalScore + keywordBonus.score;
|
||||
|
||||
// 5. 只保留评分 >= 60 的职位
|
||||
if (finalScore >= 60) {
|
||||
scored.push({
|
||||
...job,
|
||||
matchScore: finalScore,
|
||||
scoreDetails: {
|
||||
...scoreResult.scores,
|
||||
keywordBonus: keywordBonus.score
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按评分降序排序
|
||||
scored.sort((a, b) => b.matchScore - a.matchScore);
|
||||
|
||||
return scored;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建投递指令
|
||||
*/
|
||||
createDeliverCommands(jobs, sn_code, platform) {
|
||||
return jobs.map(job => ({
|
||||
command_type: 'deliver_resume',
|
||||
command_name: `投递简历 - ${job.jobTitle} @ ${job.companyName} (评分:${job.matchScore})`,
|
||||
command_name: '投递简历',
|
||||
command_params: JSON.stringify({
|
||||
sn_code,
|
||||
platform,
|
||||
|
||||
@@ -59,17 +59,42 @@ class SearchHandler extends BaseHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 创建搜索指令
|
||||
// 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);
|
||||
}
|
||||
|
||||
let listKeyword = (keyword && String(keyword).trim()) || (accountConfig.keyword && String(accountConfig.keyword).trim()) || '';
|
||||
if (!listKeyword && tabLabel) {
|
||||
listKeyword = tabLabel;
|
||||
}
|
||||
|
||||
const commandParams = {
|
||||
sn_code,
|
||||
keyword: listKeyword,
|
||||
platform: platformType,
|
||||
pageCount: pageCount || searchConfig.page_count || 3
|
||||
};
|
||||
if (tabLabel) commandParams.tabLabel = tabLabel;
|
||||
|
||||
const searchCommand = {
|
||||
command_type: 'getJobList',
|
||||
command_name: `自动搜索职位 - ${keyword || accountConfig.keyword}`,
|
||||
command_params: JSON.stringify({
|
||||
sn_code,
|
||||
keyword: keyword || accountConfig.keyword || '',
|
||||
platform: platform || accountConfig.platform_type || 'boss',
|
||||
pageCount: pageCount || searchConfig.page_count || 3
|
||||
}),
|
||||
priority: config.getTaskPriority('search_jobs') || 8
|
||||
command_type: 'get_job_list',
|
||||
command_name: '获取职位列表',
|
||||
command_params: JSON.stringify(commandParams),
|
||||
priority: config.getTaskPriority('auto_search') || 8
|
||||
};
|
||||
|
||||
// 5. 执行搜索指令
|
||||
|
||||
@@ -130,6 +130,17 @@ 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class ScheduleConfig {
|
||||
monitoringInterval: '*/1 * * * *', // 监控检查间隔:1分钟
|
||||
autoSearch: '0 0 */1 * * *', // 自动搜索任务:每1小时执行一次
|
||||
autoDeliver: '0 */2 * * * *', // 自动投递任务:每2分钟执行一次
|
||||
autoChat: '0 */15 * * * *', // 自动沟通任务:每15分钟执行一次
|
||||
autoChat: '0 */1 * * * *', // 自动沟通任务:每1分钟执行一次
|
||||
autoActive: '0 0 */2 * * *' // 自动活跃任务:每2小时执行一次
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) + '...'
|
||||
: parsedParams.companyName;
|
||||
return `投递职位: ${parsedParams.jobTitle} @ ${companyName}`;
|
||||
} else if (parsedParams.jobTitle) {
|
||||
return `投递职位: ${parsedParams.jobTitle}`;
|
||||
} else if (commandType === 'deliver_resume' || commandName.includes('投递')) {
|
||||
return '投递简历';
|
||||
} else if (commandType === 'searchJobs' || commandName.includes('搜索')) {
|
||||
return `搜索职位: ${parsedParams.keyword || ''}`;
|
||||
} else if (commandType === 'send_chat_message' || 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 || '执行指令';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,7 +63,11 @@ class ConfigManager {
|
||||
page_count: 3, // 搜索页数
|
||||
keywords: [], // 搜索关键词
|
||||
exclude_keywords: [], // 排除关键词
|
||||
time_range: null // 时间范围
|
||||
time_range: null, // 时间范围
|
||||
city: '', // 城市
|
||||
salary_range: '', // 薪资范围
|
||||
experience: '', // 经验
|
||||
education: '' // 学历
|
||||
};
|
||||
|
||||
return this.parseConfig(searchConfig, defaultConfig);
|
||||
|
||||
@@ -3,18 +3,19 @@ const KeywordMatcher = require('../utils/keywordMatcher');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
/**
|
||||
* 职位过滤引擎
|
||||
* 综合处理职位的过滤、评分和排序
|
||||
* 职位过滤引擎(schedule 自动投递用)
|
||||
* 本文件集中:过滤(薪资/关键词/活跃度/去重)+ 评分(权重分+关键词奖励)+ 按分数阈值筛。
|
||||
* 自动投递只需调 filterAndScoreJobsForDeliver 一个方法。
|
||||
*/
|
||||
class JobFilterEngine {
|
||||
/**
|
||||
* 过滤职位列表
|
||||
* 过滤职位列表(薪资 → 关键词 → 活跃度 → 去重)
|
||||
* @param {Array} jobs - 职位列表
|
||||
* @param {object} config - 过滤配置
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @param {object} resumeInfo - 简历信息(未使用,兼容签名)
|
||||
* @returns {Promise<Array>} 过滤后的职位列表
|
||||
*/
|
||||
async filterJobs(jobs, config, resumeInfo = {}) {
|
||||
async filterJobs(jobs, config) {
|
||||
if (!jobs || jobs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -22,24 +23,98 @@ class JobFilterEngine {
|
||||
let filtered = [...jobs];
|
||||
|
||||
// 1. 薪资过滤
|
||||
const beforeSalary = filtered.length;
|
||||
filtered = this.filterBySalary(filtered, config);
|
||||
const salaryRemoved = beforeSalary - filtered.length;
|
||||
if (salaryRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤1-薪资过滤: 输入${beforeSalary} 输出${filtered.length} 剔除${salaryRemoved} (范围: ${config.min_salary ?? 0}-${config.max_salary ?? 0}K)`);
|
||||
}
|
||||
|
||||
// 2. 关键词过滤
|
||||
const beforeKeywords = filtered.length;
|
||||
filtered = this.filterByKeywords(filtered, config);
|
||||
const keywordsRemoved = beforeKeywords - filtered.length;
|
||||
if (keywordsRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤2-关键词过滤: 输入${beforeKeywords} 输出${filtered.length} 剔除${keywordsRemoved} (排除: ${(config.exclude_keywords || []).join(',') || '无'} 包含: ${(config.filter_keywords || []).join(',') || '无'})`);
|
||||
}
|
||||
|
||||
// 3. 公司活跃度过滤
|
||||
if (config.filter_inactive_companies) {
|
||||
const beforeActivity = filtered.length;
|
||||
filtered = await this.filterByCompanyActivity(filtered, config.company_active_days || 7);
|
||||
const activityRemoved = beforeActivity - filtered.length;
|
||||
if (activityRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤3-公司活跃度过滤: 输入${beforeActivity} 输出${filtered.length} 剔除${activityRemoved}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 去重(同一公司、同一职位名称)
|
||||
if (config.deduplicate) {
|
||||
const beforeDedup = filtered.length;
|
||||
filtered = this.deduplicateJobs(filtered);
|
||||
const dedupRemoved = beforeDedup - filtered.length;
|
||||
if (dedupRemoved > 0) {
|
||||
console.log(`[jobFilterEngine] 步骤4-去重: 输入${beforeDedup} 输出${filtered.length} 剔除${dedupRemoved}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[jobFilterEngine] filterJobs 结束: 原始${jobs.length} 通过${filtered.length} 总剔除${jobs.length - filtered.length}`);
|
||||
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 - 职位列表
|
||||
@@ -78,14 +153,6 @@ class JobFilterEngine {
|
||||
return KeywordMatcher.filterJobs(jobs, {
|
||||
excludeKeywords: exclude_keywords,
|
||||
filterKeywords: filter_keywords
|
||||
}, (job) => {
|
||||
// 组合职位名称、描述、技能要求等
|
||||
return [
|
||||
job.name || job.jobName || '',
|
||||
job.description || job.jobDescription || '',
|
||||
job.skills || '',
|
||||
job.welfare || ''
|
||||
].join(' ');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -355,39 +422,6 @@ class JobFilterEngine {
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合处理:过滤 + 评分 + 排序
|
||||
* @param {Array} jobs - 职位列表
|
||||
* @param {object} config - 过滤配置
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @param {object} options - 选项
|
||||
* @returns {Promise<Array>} 处理后的职位列表
|
||||
*/
|
||||
async process(jobs, config, resumeInfo = {}, options = {}) {
|
||||
const {
|
||||
maxCount = 10, // 最大返回数量
|
||||
sortBy = 'score' // 排序方式
|
||||
} = options;
|
||||
|
||||
// 1. 过滤
|
||||
let filtered = await this.filterJobs(jobs, config, resumeInfo);
|
||||
|
||||
console.log(`[职位过滤] 原始: ${jobs.length} 个,过滤后: ${filtered.length} 个`);
|
||||
|
||||
// 2. 评分
|
||||
const scored = this.scoreJobs(filtered, resumeInfo, config);
|
||||
|
||||
// 3. 排序
|
||||
const sorted = this.sortJobs(scored, sortBy);
|
||||
|
||||
// 4. 截取
|
||||
const result = sorted.slice(0, maxCount);
|
||||
|
||||
console.log(`[职位过滤] 最终返回: ${result.length} 个职位`);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
|
||||
@@ -29,7 +29,7 @@ class TaskHandlers {
|
||||
register(taskQueue) {
|
||||
console.log('[任务处理器] 开始注册处理器...');
|
||||
|
||||
// 注册自动搜索处理器
|
||||
// 注册自动搜索处理器(唯一搜索任务类型)
|
||||
taskQueue.registerHandler('auto_search', async (task) => {
|
||||
return await this.handleAutoSearchTask(task);
|
||||
});
|
||||
@@ -39,11 +39,6 @@ class TaskHandlers {
|
||||
return await this.handleAutoDeliverTask(task);
|
||||
});
|
||||
|
||||
// 注册搜索职位列表处理器(与 auto_search 相同)
|
||||
taskQueue.registerHandler('search_jobs', async (task) => {
|
||||
return await this.handleAutoSearchTask(task);
|
||||
});
|
||||
|
||||
// 注册自动沟通处理器
|
||||
taskQueue.registerHandler('auto_chat', async (task) => {
|
||||
return await this.handleAutoChatTask(task);
|
||||
|
||||
@@ -2,6 +2,7 @@ const BaseTask = require('./baseTask');
|
||||
const db = require('../../dbProxy');
|
||||
const config = require('../infrastructure/config');
|
||||
const authorizationService = require('../../../services/authorization_service');
|
||||
const ConfigManager = require('../services/configManager');
|
||||
|
||||
/**
|
||||
* 自动投递任务
|
||||
@@ -54,7 +55,7 @@ class AutoDeliverTask extends BaseTask {
|
||||
}
|
||||
|
||||
// 3. 获取投递配置
|
||||
const deliverConfig = this.parseDeliverConfig(account.deliver_config);
|
||||
const deliverConfig = ConfigManager.parseDeliverConfig(account.deliver_config);
|
||||
|
||||
// 4. 检查日投递限制
|
||||
const dailyLimit = config.platformDailyLimits[account.platform_type] || 50;
|
||||
@@ -108,30 +109,6 @@ class AutoDeliverTask extends BaseTask {
|
||||
return account ? account.toJSON() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析投递配置
|
||||
*/
|
||||
parseDeliverConfig(deliver_config) {
|
||||
if (typeof deliver_config === 'string') {
|
||||
try {
|
||||
deliver_config = JSON.parse(deliver_config);
|
||||
} catch (e) {
|
||||
deliver_config = {};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deliver_interval: deliver_config?.deliver_interval || 30,
|
||||
min_salary: deliver_config?.min_salary || 0,
|
||||
max_salary: deliver_config?.max_salary || 0,
|
||||
page_count: deliver_config?.page_count || 3,
|
||||
max_deliver: deliver_config?.max_deliver || 10,
|
||||
filter_keywords: deliver_config?.filter_keywords || [],
|
||||
exclude_keywords: deliver_config?.exclude_keywords || [],
|
||||
time_range: deliver_config?.time_range || null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日已投递数量
|
||||
*/
|
||||
@@ -217,7 +194,7 @@ class AutoDeliverTask extends BaseTask {
|
||||
}
|
||||
|
||||
// 3. 获取投递配置
|
||||
const deliverConfig = this.parseDeliverConfig(account.deliver_config);
|
||||
const deliverConfig = ConfigManager.parseDeliverConfig(account.deliver_config);
|
||||
|
||||
// 4. 检查时间范围
|
||||
if (deliverConfig.time_range) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const BaseTask = require('./baseTask');
|
||||
const db = require('../../dbProxy');
|
||||
const config = require('../infrastructure/config');
|
||||
const ConfigManager = require('../services/configManager');
|
||||
|
||||
/**
|
||||
* 自动搜索职位任务
|
||||
@@ -53,7 +54,7 @@ class AutoSearchTask extends BaseTask {
|
||||
}
|
||||
|
||||
// 2. 获取搜索配置
|
||||
const searchConfig = this.parseSearchConfig(account.search_config);
|
||||
const searchConfig = ConfigManager.parseSearchConfig(account.search_config);
|
||||
|
||||
// 3. 检查日搜索限制
|
||||
const dailyLimit = config.dailyLimits.maxSearch || 20;
|
||||
@@ -93,29 +94,6 @@ class AutoSearchTask extends BaseTask {
|
||||
return account ? account.toJSON() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析搜索配置
|
||||
*/
|
||||
parseSearchConfig(search_config) {
|
||||
if (typeof search_config === 'string') {
|
||||
try {
|
||||
search_config = JSON.parse(search_config);
|
||||
} catch (e) {
|
||||
search_config = {};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
search_interval: search_config?.search_interval || 60,
|
||||
page_count: search_config?.page_count || 3,
|
||||
city: search_config?.city || '',
|
||||
salary_range: search_config?.salary_range || '',
|
||||
experience: search_config?.experience || '',
|
||||
education: search_config?.education || '',
|
||||
time_range: search_config?.time_range || null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日已搜索数量
|
||||
*/
|
||||
@@ -162,7 +140,7 @@ class AutoSearchTask extends BaseTask {
|
||||
}
|
||||
|
||||
// 3. 获取搜索配置
|
||||
const searchConfig = this.parseSearchConfig(account.search_config);
|
||||
const searchConfig = ConfigManager.parseSearchConfig(account.search_config);
|
||||
|
||||
// 4. 检查时间范围
|
||||
if (searchConfig.time_range) {
|
||||
|
||||
@@ -199,11 +199,13 @@ class KeywordMatcher {
|
||||
* @param {Function} textExtractor - 文本提取函数 (job) => string
|
||||
* @returns {Array} 匹配通过的职位(带匹配信息)
|
||||
*/
|
||||
static filterJobs(jobs, config, textExtractor = (job) => `${job.name || ''} ${job.description || ''}`) {
|
||||
static filterJobs(jobs, config) {
|
||||
if (!jobs || jobs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const textExtractor=(job) => `${job.jobTitle || ''} ${job.companyIndustry || ''}`;
|
||||
|
||||
const filtered = [];
|
||||
|
||||
for (const job of jobs) {
|
||||
|
||||
144
api/model/ai_call_records.js
Normal file
144
api/model/ai_call_records.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const Sequelize = require('sequelize');
|
||||
const { DataTypes } = Sequelize;
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const AiCallRecords = sequelize.define('ai_call_records', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER(11),
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '主键ID'
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER(11),
|
||||
allowNull: true,
|
||||
comment: '用户ID'
|
||||
},
|
||||
sn_code: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '设备SN码'
|
||||
},
|
||||
service_type: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '服务类型:chat/completion/embedding'
|
||||
},
|
||||
model_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '模型名称'
|
||||
},
|
||||
prompt_tokens: {
|
||||
type: DataTypes.INTEGER(11),
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '输入Token数'
|
||||
},
|
||||
completion_tokens: {
|
||||
type: DataTypes.INTEGER(11),
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '输出Token数'
|
||||
},
|
||||
total_tokens: {
|
||||
type: DataTypes.INTEGER(11),
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '总Token数'
|
||||
},
|
||||
request_content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '请求内容'
|
||||
},
|
||||
response_content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '响应内容'
|
||||
},
|
||||
cost_amount: {
|
||||
type: DataTypes.DECIMAL(10, 4),
|
||||
allowNull: true,
|
||||
comment: '费用(元)'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'success',
|
||||
comment: '状态:success/failed'
|
||||
},
|
||||
error_message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '错误信息'
|
||||
},
|
||||
response_time: {
|
||||
type: DataTypes.INTEGER(11),
|
||||
allowNull: true,
|
||||
comment: '响应时间(毫秒)'
|
||||
},
|
||||
api_provider: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
defaultValue: 'qwen',
|
||||
comment: 'API提供商'
|
||||
},
|
||||
business_type: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '业务类型'
|
||||
},
|
||||
reference_id: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '关联业务ID'
|
||||
},
|
||||
create_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
comment: '创建时间'
|
||||
},
|
||||
last_modify_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
|
||||
comment: '最后修改时间'
|
||||
},
|
||||
is_delete: {
|
||||
type: DataTypes.TINYINT(1),
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '是否删除:0-否,1-是'
|
||||
}
|
||||
}, {
|
||||
tableName: 'ai_call_records',
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_user_id',
|
||||
fields: ['user_id']
|
||||
},
|
||||
{
|
||||
name: 'idx_sn_code',
|
||||
fields: ['sn_code']
|
||||
},
|
||||
{
|
||||
name: 'idx_create_time',
|
||||
fields: ['create_time']
|
||||
},
|
||||
{
|
||||
name: 'idx_is_delete',
|
||||
fields: ['is_delete']
|
||||
},
|
||||
{
|
||||
name: 'idx_business_type',
|
||||
fields: ['business_type']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return AiCallRecords;
|
||||
};
|
||||
62
api/model/chat_message.js
Normal file
62
api/model/chat_message.js
Normal 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;
|
||||
};
|
||||
@@ -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),
|
||||
@@ -20,269 +20,90 @@ module.exports = (db) => {
|
||||
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 });
|
||||
|
||||
return chat_records
|
||||
|
||||
|
||||
// chat_records.sync({ force: true });
|
||||
|
||||
return chat_records;
|
||||
};
|
||||
|
||||
|
||||
87
api/model/chat_reply_intent_log.js
Normal file
87
api/model/chat_reply_intent_log.js
Normal 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;
|
||||
};
|
||||
@@ -40,7 +40,7 @@ module.exports = (db) => {
|
||||
defaultValue: ''
|
||||
},
|
||||
keyword: {
|
||||
comment: '关键词',
|
||||
comment: '搜索/推荐职位关键词;保存投递期望标签时会与 resume_info.deliver_tab_label 同步',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
|
||||
@@ -50,9 +50,26 @@ module.exports = (db) => {
|
||||
},
|
||||
features: {
|
||||
comment: '功能特性列表(JSON字符串数组)',
|
||||
type: Sequelize.TEXT,
|
||||
type: Sequelize.JSON(),
|
||||
allowNull: false,
|
||||
defaultValue: '[]'
|
||||
defaultValue: [],
|
||||
get: function () {
|
||||
const value = this.getDataValue('features');
|
||||
if (!value) return null;
|
||||
if (typeof value === 'string') {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set: function (value) {
|
||||
if (value === null || value === undefined) {
|
||||
this.setDataValue('features', null);
|
||||
} else if (typeof value === 'string') {
|
||||
this.setDataValue('features', value);
|
||||
} else {
|
||||
this.setDataValue('features', JSON.stringify(value));
|
||||
}
|
||||
},
|
||||
},
|
||||
featured: {
|
||||
comment: '是否为推荐套餐(1=推荐,0=普通)',
|
||||
|
||||
@@ -232,6 +232,20 @@ module.exports = (db) => {
|
||||
defaultValue: ''
|
||||
},
|
||||
|
||||
// 投递用期望标签:get_job_listings 拉取的 tab 列表与当前选中的 tab 文本
|
||||
job_listings: {
|
||||
comment: '简历/期望 tab 列表(JSON数组),如 ["推荐", "前端开发工程师"]',
|
||||
type: Sequelize.JSON(),
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
},
|
||||
deliver_tab_label: {
|
||||
comment: '投递时使用的标签文本,对应 job_listings 中的某一项,如 "前端开发工程师"',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
defaultValue: ''
|
||||
},
|
||||
|
||||
// 状态信息
|
||||
isActive: {
|
||||
comment: '是否活跃简历',
|
||||
|
||||
@@ -15,7 +15,7 @@ module.exports = (db) => {
|
||||
defaultValue: ''
|
||||
},
|
||||
taskType: {
|
||||
comment: '任务类型: get_login_qr_code-登录检查, get_resume-获取简历, search_jobs-搜索岗位, get_job_list-获取岗位列表, auto_deliver-自动投递, chat-聊天, apply-投递',
|
||||
comment: '任务类型: get_login_qr_code-登录检查, get_resume-获取简历, auto_search-搜索岗位, get_job_list-获取岗位列表, auto_deliver-自动投递, chat-聊天, apply-投递',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
|
||||
168
api/services/ai_call_recorder.js
Normal file
168
api/services/ai_call_recorder.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const Framework = require('../../framework/node-core-framework');
|
||||
|
||||
/**
|
||||
* AI调用记录服务
|
||||
* 负责记录所有AI API调用的详细信息
|
||||
*/
|
||||
class AiCallRecorder {
|
||||
/**
|
||||
* 记录AI调用
|
||||
* @param {Object} params - 调用参数
|
||||
* @param {Number} params.user_id - 用户ID
|
||||
* @param {String} params.sn_code - 设备SN码
|
||||
* @param {String} params.service_type - 服务类型(chat/completion/embedding)
|
||||
* @param {String} params.model_name - 模型名称
|
||||
* @param {Number} params.prompt_tokens - 输入Token数
|
||||
* @param {Number} params.completion_tokens - 输出Token数
|
||||
* @param {Number} params.total_tokens - 总Token数
|
||||
* @param {String} params.request_content - 请求内容
|
||||
* @param {String} params.response_content - 响应内容
|
||||
* @param {Number} params.cost_amount - 费用(元)
|
||||
* @param {String} params.status - 状态(success/failed)
|
||||
* @param {String} params.error_message - 错误信息
|
||||
* @param {Number} params.response_time - 响应时间(毫秒)
|
||||
* @param {String} params.api_provider - API提供商
|
||||
* @param {String} params.business_type - 业务类型
|
||||
* @param {String} params.reference_id - 关联业务ID
|
||||
* @returns {Promise<Object>} 记录结果
|
||||
*/
|
||||
static async record(params) {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { ai_call_records } = models;
|
||||
|
||||
if (!ai_call_records) {
|
||||
console.warn('[AI记录] ai_call_records 模型未加载');
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const record = await ai_call_records.create({
|
||||
user_id: params.user_id || null,
|
||||
sn_code: params.sn_code || null,
|
||||
service_type: params.service_type,
|
||||
model_name: params.model_name,
|
||||
prompt_tokens: params.prompt_tokens || 0,
|
||||
completion_tokens: params.completion_tokens || 0,
|
||||
total_tokens: params.total_tokens || 0,
|
||||
request_content: params.request_content || null,
|
||||
response_content: params.response_content || null,
|
||||
cost_amount: params.cost_amount || null,
|
||||
status: params.status || 'success',
|
||||
error_message: params.error_message || null,
|
||||
response_time: params.response_time || null,
|
||||
api_provider: params.api_provider || 'qwen',
|
||||
business_type: params.business_type || null,
|
||||
reference_id: params.reference_id || null,
|
||||
create_time: now,
|
||||
last_modify_time: now,
|
||||
is_delete: 0
|
||||
});
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('[AI记录] 记录失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户Token使用统计
|
||||
* @param {Number} userId - 用户ID
|
||||
* @param {Object} options - 查询选项
|
||||
* @param {Date} options.startDate - 开始日期
|
||||
* @param {Date} options.endDate - 结束日期
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
static async getUserTokenStats(userId, options = {}) {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { ai_call_records } = models;
|
||||
const { Op } = Framework.getSequelize();
|
||||
|
||||
if (!ai_call_records) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const where = {
|
||||
user_id: userId,
|
||||
is_delete: 0
|
||||
};
|
||||
|
||||
if (options.startDate && options.endDate) {
|
||||
where.create_time = {
|
||||
[Op.between]: [options.startDate, options.endDate]
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await ai_call_records.findOne({
|
||||
where,
|
||||
attributes: [
|
||||
[Framework.getSequelize().fn('COUNT', Framework.getSequelize().col('id')), 'total_calls'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('prompt_tokens')), 'total_prompt_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('completion_tokens')), 'total_completion_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('total_tokens')), 'total_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('cost_amount')), 'total_cost'],
|
||||
[Framework.getSequelize().fn('AVG', Framework.getSequelize().col('response_time')), 'avg_response_time']
|
||||
],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[AI记录] 获取用户统计失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备Token使用统计
|
||||
* @param {String} snCode - 设备SN码
|
||||
* @param {Object} options - 查询选项
|
||||
* @param {Date} options.startDate - 开始日期
|
||||
* @param {Date} options.endDate - 结束日期
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
static async getDeviceTokenStats(snCode, options = {}) {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { ai_call_records } = models;
|
||||
const { Op } = Framework.getSequelize();
|
||||
|
||||
if (!ai_call_records) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const where = {
|
||||
sn_code: snCode,
|
||||
is_delete: 0
|
||||
};
|
||||
|
||||
if (options.startDate && options.endDate) {
|
||||
where.create_time = {
|
||||
[Op.between]: [options.startDate, options.endDate]
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await ai_call_records.findOne({
|
||||
where,
|
||||
attributes: [
|
||||
[Framework.getSequelize().fn('COUNT', Framework.getSequelize().col('id')), 'total_calls'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('prompt_tokens')), 'total_prompt_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('completion_tokens')), 'total_completion_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('total_tokens')), 'total_tokens'],
|
||||
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('cost_amount')), 'total_cost'],
|
||||
[Framework.getSequelize().fn('AVG', Framework.getSequelize().col('response_time')), 'avg_response_time']
|
||||
],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[AI记录] 获取设备统计失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AiCallRecorder;
|
||||
@@ -1,415 +1,452 @@
|
||||
/**
|
||||
* AI智能服务
|
||||
* 提供岗位筛选、简历分析、聊天生成等AI功能
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const config = require('../../config/config');
|
||||
const AiCallRecorder = require('./ai_call_recorder.js');
|
||||
|
||||
class AIService {
|
||||
constructor() {
|
||||
this.apiKey = config.ai.apiKey;
|
||||
this.baseURL = config.ai.baseUrl;
|
||||
this.model = config.ai.model;
|
||||
this.timeout = 30000;
|
||||
|
||||
// 创建axios实例
|
||||
this.client = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
timeout: this.timeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `${this.apiKey}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用AI接口
|
||||
* @param {Array} messages - 消息数组
|
||||
* @param {Object} options - 额外选项
|
||||
* @returns {Promise<String>} AI响应内容
|
||||
*/
|
||||
async chat(messages, options = {}) {
|
||||
try {
|
||||
const response = await this.client.post('/v1/chat/completions', {
|
||||
model: this.model,
|
||||
messages,
|
||||
temperature: options.temperature || 0.7,
|
||||
max_tokens: options.max_tokens || 2000,
|
||||
...options
|
||||
});
|
||||
|
||||
return response.data.choices[0].message.content;
|
||||
} catch (error) {
|
||||
console.warn('AI服务调用失败:', error.message);
|
||||
throw new Error(`AI服务调用失败: ${error.message}`);
|
||||
/**
|
||||
* Qwen 2.5 大模型服务
|
||||
* 集成阿里云 DashScope API,提供智能化的岗位筛选、聊天生成、简历分析等功能
|
||||
*/
|
||||
class aiService {
|
||||
constructor() {
|
||||
this.apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY;
|
||||
// 使用 DashScope 兼容 OpenAI 格式的接口
|
||||
this.apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
|
||||
// Qwen 2.5 模型:qwen-turbo(快速)、qwen-plus(增强)、qwen-max(最强)
|
||||
this.model = config.ai?.model || 'qwen-turbo';
|
||||
this.maxRetries = 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析简历竞争力
|
||||
* @param {Object} resumeData - 简历数据
|
||||
* @returns {Promise<Object>} 分析结果
|
||||
*/
|
||||
async analyzeResume(resumeData) {
|
||||
const prompt = `请分析以下简历的竞争力,并提供详细评估:
|
||||
/**
|
||||
* 调用 Qwen 2.5 API
|
||||
* @param {string} prompt - 提示词
|
||||
* @param {object} options - 配置选项
|
||||
* @returns {Promise<object>} API响应结果
|
||||
*/
|
||||
async callAPI(prompt, options = {}) {
|
||||
const startTime = Date.now();
|
||||
|
||||
const requestData = {
|
||||
model: this.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: options.systemPrompt || '你是一个专业的招聘顾问,擅长分析岗位信息、生成聊天内容和分析简历匹配度。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
temperature: options.temperature || 0.7,
|
||||
max_tokens: options.maxTokens || 2000,
|
||||
top_p: options.topP || 0.9
|
||||
};
|
||||
|
||||
const requestContent = JSON.stringify(requestData.messages);
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await axios.post(this.apiUrl, requestData, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`, // DashScope 使用 Bearer token 格式
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
const responseContent = response.data.choices?.[0]?.message?.content || '';
|
||||
const usage = response.data.usage || {};
|
||||
|
||||
// 记录AI调用(异步,不阻塞主流程)
|
||||
this.recordAiCall({
|
||||
user_id: options.user_id,
|
||||
sn_code: options.sn_code,
|
||||
service_type: options.service_type || 'completion',
|
||||
model_name: this.model,
|
||||
prompt_tokens: usage.prompt_tokens || 0,
|
||||
completion_tokens: usage.completion_tokens || 0,
|
||||
total_tokens: usage.total_tokens || 0,
|
||||
request_content: requestContent,
|
||||
response_content: responseContent,
|
||||
cost_amount: this.calculateCost(usage.total_tokens || 0),
|
||||
status: 'success',
|
||||
response_time: responseTime,
|
||||
api_provider: 'qwen',
|
||||
business_type: options.business_type,
|
||||
reference_id: options.reference_id
|
||||
}).catch(err => {
|
||||
console.warn('记录AI调用失败(不影响主流程):', err.message);
|
||||
});
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
content: responseContent
|
||||
};
|
||||
} catch (error) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
console.log(`Qwen 2.5 API调用失败 (尝试 ${attempt}/${this.maxRetries}): ${error.message}`);
|
||||
|
||||
// 记录失败的调用
|
||||
if (attempt === this.maxRetries) {
|
||||
this.recordAiCall({
|
||||
user_id: options.user_id,
|
||||
sn_code: options.sn_code,
|
||||
service_type: options.service_type || 'completion',
|
||||
model_name: this.model,
|
||||
request_content: requestContent,
|
||||
status: 'failed',
|
||||
error_message: error.message,
|
||||
response_time: responseTime,
|
||||
api_provider: 'qwen',
|
||||
business_type: options.business_type,
|
||||
reference_id: options.reference_id
|
||||
}).catch(err => {
|
||||
console.warn('记录失败调用失败:', err.message);
|
||||
});
|
||||
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
// 等待后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录AI调用
|
||||
* @param {Object} params - 调用参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async recordAiCall(params) {
|
||||
try {
|
||||
await AiCallRecorder.record(params);
|
||||
} catch (error) {
|
||||
// 记录失败不应影响主流程
|
||||
console.warn('AI调用记录失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算调用费用
|
||||
* @param {Number} totalTokens - 总Token数
|
||||
* @returns {Number} 费用(元)
|
||||
*/
|
||||
calculateCost(totalTokens) {
|
||||
// 阿里云 Qwen 价格(元/1000 tokens)
|
||||
// qwen-turbo: ¥0.003, qwen-plus: ¥0.004, qwen-max: ¥0.12
|
||||
// 这里使用 qwen-turbo 的价格作为默认值
|
||||
const pricePerThousand = 0.003;
|
||||
return (totalTokens / 1000) * pricePerThousand;
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位智能筛选
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @returns {Promise<object>} 筛选结果
|
||||
*/
|
||||
async analyzeJob(jobInfo, resumeInfo) {
|
||||
const prompt = `
|
||||
请分析以下岗位信息,并给出详细的评估结果:
|
||||
|
||||
岗位信息:
|
||||
- 公司名称:${jobInfo.companyName || '未知'}
|
||||
- 职位名称:${jobInfo.jobTitle || '未知'}
|
||||
- 薪资范围:${jobInfo.salary || '未知'}
|
||||
- 工作地点:${jobInfo.location || '未知'}
|
||||
- 岗位描述:${jobInfo.description || '未知'}
|
||||
- 技能要求:${jobInfo.skills || '未知'}
|
||||
|
||||
简历信息:
|
||||
- 姓名: ${resumeData.fullName || '未知'}
|
||||
- 工作年限: ${resumeData.workYears || '未知'}
|
||||
- 教育背景: ${resumeData.education || '未知'}
|
||||
- 期望职位: ${resumeData.expectedPosition || '未知'}
|
||||
- 期望薪资: ${resumeData.expectedSalary || '未知'}
|
||||
- 技能标签: ${Array.isArray(resumeData.skills) ? resumeData.skills.join('、') : '未知'}
|
||||
- 工作经历: ${resumeData.workExperience || '未提供'}
|
||||
- 项目经历: ${resumeData.projectExperience || '未提供'}
|
||||
- 技能标签:${resumeInfo.skills || '未知'}
|
||||
- 工作经验:${resumeInfo.experience || '未知'}
|
||||
- 教育背景:${resumeInfo.education || '未知'}
|
||||
- 期望薪资:${resumeInfo.expectedSalary || '未知'}
|
||||
|
||||
请从以下维度进行评估(1-100分):
|
||||
1. 技术能力
|
||||
2. 项目经验
|
||||
3. 教育背景
|
||||
4. 工作年限匹配度
|
||||
5. 综合竞争力
|
||||
请从以下维度进行分析:
|
||||
1. 技能匹配度(0-100分)
|
||||
2. 经验匹配度(0-100分)
|
||||
3. 薪资合理性(0-100分)
|
||||
4. 公司质量评估(0-100分)
|
||||
5. 是否为外包岗位(是/否)
|
||||
6. 综合推荐指数(0-100分)
|
||||
7. 详细分析说明
|
||||
8. 投递建议
|
||||
|
||||
返回JSON格式:
|
||||
请以JSON格式返回结果。
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的招聘分析师,擅长评估岗位与简历的匹配度。请提供客观、专业的分析结果。',
|
||||
temperature: 0.3,
|
||||
business_type: 'job_analysis',
|
||||
service_type: 'completion'
|
||||
});
|
||||
|
||||
try {
|
||||
// 尝试解析JSON响应
|
||||
const analysis = JSON.parse(result.content);
|
||||
return {
|
||||
analysis: analysis
|
||||
};
|
||||
} catch (parseError) {
|
||||
// 如果解析失败,返回原始内容
|
||||
return {
|
||||
analysis: {
|
||||
content: result.content,
|
||||
parseError: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成个性化聊天内容
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @param {string} chatType - 聊天类型 (greeting/interview/followup)
|
||||
* @returns {Promise<object>} 聊天内容
|
||||
*/
|
||||
async generateChatContent(jobInfo, resumeInfo, chatType = 'greeting') {
|
||||
const chatTypeMap = {
|
||||
'greeting': '初次打招呼',
|
||||
'interview': '面试邀约',
|
||||
'followup': '跟进沟通'
|
||||
};
|
||||
|
||||
const prompt = `
|
||||
请为以下场景生成个性化的聊天内容:
|
||||
|
||||
聊天类型:${chatTypeMap[chatType] || chatType}
|
||||
|
||||
岗位信息:
|
||||
- 公司名称:${jobInfo.companyName || '未知'}
|
||||
- 职位名称:${jobInfo.jobTitle || '未知'}
|
||||
- 技能要求:${jobInfo.skills || '未知'}
|
||||
|
||||
简历信息:
|
||||
- 技能标签:${resumeInfo.skills || '未知'}
|
||||
- 工作经验:${resumeInfo.experience || '未知'}
|
||||
- 项目经验:${resumeInfo.projects || '未知'}
|
||||
|
||||
要求:
|
||||
1. 内容要自然、专业、个性化
|
||||
2. 突出简历与岗位的匹配点
|
||||
3. 避免过于机械化的表达
|
||||
4. 长度控制在100-200字
|
||||
5. 体现求职者的诚意和热情
|
||||
|
||||
请直接返回聊天内容,不需要其他格式。
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的招聘沟通专家,擅长生成自然、专业的求职聊天内容。',
|
||||
temperature: 0.8,
|
||||
business_type: 'chat_generation',
|
||||
service_type: 'chat',
|
||||
reference_id: jobInfo.jobId || jobInfo.id
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 HR 消息判断回复意图并生成内容
|
||||
* @param {object} params - { jobInfo, hrMessage, previousMessages? }
|
||||
* @returns {Promise<{ action: 'text'|'send_resume'|'exchange_wechat'|'exchange_phone', reply_content: string }>}
|
||||
*/
|
||||
async replyIntentAndContent(params) {
|
||||
const { jobInfo = {}, hrMessage = '', previousMessages = [] } = params;
|
||||
const jobName = jobInfo.jobName || jobInfo.title || '未知职位';
|
||||
const companyName = jobInfo.brandName || jobInfo.companyName || '未知公司';
|
||||
|
||||
const prompt = `
|
||||
你正在处理 BOSS 直聘上的求职沟通。根据 HR 最新消息判断求职者应采取的回复动作。
|
||||
|
||||
【职位】${jobName}
|
||||
【公司】${companyName}
|
||||
|
||||
【HR 最新消息】
|
||||
${hrMessage || '(HR 未发文字,仅存在职位卡片等)'}
|
||||
|
||||
请严格按以下 JSON 格式返回(不要包含其他说明或换行):
|
||||
{"action":"动作","reply_content":"内容"}
|
||||
|
||||
action 仅允许以下五种之一:
|
||||
- no_reply:不需要回复(HR 明确表示暂不匹配、感谢关注、有合适机会再沟通、婉拒、不招了等,无需求职者再回复)
|
||||
- text:仅文字回复(普通聊天、打招呼、问是否考虑机会等)
|
||||
- send_resume:发简历(HR 要求发简历、看简历、投递等)
|
||||
- exchange_wechat:换微信(HR 要求加微信、留微信、发微信等)
|
||||
- exchange_phone:换电话(HR 要求留电话、发电话、联系方式等)
|
||||
|
||||
规则:
|
||||
1. 若 HR 明确表示暂不匹配、感谢关注、有合适机会再沟通、与岗位不够匹配、婉拒、不考虑、不招了、已招到 等 → action 为 no_reply,reply_content 留空。
|
||||
2. 若 HR 明确要求发简历/投递/看简历 → action 为 send_resume,reply_content 简短附言或空(10字内)。
|
||||
3. 若 HR 明确要求加微信/留微信/发微信 → action 为 exchange_wechat,reply_content 简短附言或空(10字内)。
|
||||
4. 若 HR 明确要求留电话/发电话/联系方式 → action 为 exchange_phone,reply_content 简短附言或空(10字内)。
|
||||
5. 若仅为普通聊天、打招呼 → action 为 text,reply_content 为一两句简短回复(20字以内),语气平淡、不要过于热情。
|
||||
6. reply_content 必须为字符串,不要换行;整体风格:简洁、克制、不啰嗦。
|
||||
`.trim();
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是求职沟通助手。根据 HR 消息判断动作:no_reply、text、send_resume、exchange_wechat、exchange_phone。HR 婉拒/暂不匹配时用 no_reply。回复内容务必简短(20字以内)、语气平淡、不要过于热情。输出 JSON:{"action":"五选一","reply_content":"..."}。只返回合法 JSON。',
|
||||
temperature: 0.3,
|
||||
maxTokens: 500,
|
||||
business_type: 'chat_reply_intent',
|
||||
service_type: 'completion'
|
||||
});
|
||||
|
||||
const raw = (result && result.content) ? result.content.trim() : '';
|
||||
const allowed = ['no_reply', 'text', 'send_resume', 'exchange_wechat', 'exchange_phone'];
|
||||
try {
|
||||
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
||||
const parsed = jsonMatch ? JSON.parse(jsonMatch[0]) : {};
|
||||
const action = allowed.includes(parsed.action) ? parsed.action : 'text';
|
||||
const reply_content = typeof parsed.reply_content === 'string' ? parsed.reply_content.trim() : '';
|
||||
return { action, reply_content };
|
||||
} catch (e) {
|
||||
return { action: 'text', reply_content: raw || '好的' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析简历要素
|
||||
* @param {string} resumeText - 简历文本内容
|
||||
* @returns {Promise<object>} 简历分析结果
|
||||
*/
|
||||
async analyzeResume(resumeText) {
|
||||
const prompt = `
|
||||
请分析以下简历内容,并返回 JSON 格式的分析结果:
|
||||
|
||||
简历内容:
|
||||
${resumeText}
|
||||
|
||||
请按以下格式返回 JSON 结果:
|
||||
{
|
||||
"overallScore": 总分(1-100),
|
||||
"technicalScore": 技术能力分(1-100),
|
||||
"projectScore": 项目经验分(1-100),
|
||||
"educationScore": 教育背景分(1-100),
|
||||
"experienceScore": 工作年限分(1-100),
|
||||
"strengths": ["优势1", "优势2", "优势3"],
|
||||
"weaknesses": ["不足1", "不足2"],
|
||||
"suggestions": ["建议1", "建议2", "建议3"],
|
||||
"keySkills": ["核心技能1", "核心技能2"],
|
||||
"marketCompetitiveness": "市场竞争力描述"
|
||||
}`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: '你是一个专业的HR和招聘顾问,擅长分析简历和评估候选人竞争力。' },
|
||||
{ role: 'user', content: prompt }
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await this.chat(messages, { temperature: 0.3 });
|
||||
// 提取JSON部分
|
||||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
throw new Error('AI返回格式不正确');
|
||||
} catch (error) {
|
||||
console.warn('简历分析失败:', error);
|
||||
// 返回默认值
|
||||
return {
|
||||
overallScore: 60,
|
||||
technicalScore: 60,
|
||||
projectScore: 60,
|
||||
educationScore: 60,
|
||||
experienceScore: 60,
|
||||
strengths: ['待AI分析'],
|
||||
weaknesses: ['待AI分析'],
|
||||
suggestions: ['请稍后重试'],
|
||||
keySkills: Array.isArray(resumeData.skills) ? resumeData.skills.slice(0, 5) : [],
|
||||
marketCompetitiveness: '待AI分析'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位匹配度评估
|
||||
* @param {Object} jobData - 岗位数据
|
||||
* @param {Object} resumeData - 简历数据
|
||||
* @returns {Promise<Object>} 匹配结果
|
||||
*/
|
||||
async matchJobWithResume(jobData, resumeData) {
|
||||
const prompt = `请评估以下岗位与简历的匹配度:
|
||||
|
||||
【岗位信息】
|
||||
- 职位名称: ${jobData.jobTitle || '未知'}
|
||||
- 公司名称: ${jobData.companyName || '未知'}
|
||||
- 薪资范围: ${jobData.salary || '未知'}
|
||||
- 工作地点: ${jobData.location || '未知'}
|
||||
- 工作经验要求: ${jobData.experienceRequired || '未知'}
|
||||
- 学历要求: ${jobData.educationRequired || '未知'}
|
||||
- 岗位描述: ${jobData.jobDescription || '未提供'}
|
||||
- 技能要求: ${jobData.skillsRequired || '未提供'}
|
||||
|
||||
【简历信息】
|
||||
- 工作年限: ${resumeData.workYears || '未知'}
|
||||
- 教育背景: ${resumeData.education || '未知'}
|
||||
- 技能标签: ${Array.isArray(resumeData.skills) ? resumeData.skills.join('、') : '未知'}
|
||||
- 期望职位: ${resumeData.expectedPosition || '未知'}
|
||||
- 期望薪资: ${resumeData.expectedSalary || '未知'}
|
||||
|
||||
请分析:
|
||||
1. 技能匹配度
|
||||
2. 经验匹配度
|
||||
3. 薪资匹配度
|
||||
4. 是否为外包岗位(根据公司名称、岗位描述判断)
|
||||
5. 综合推荐度
|
||||
|
||||
返回JSON格式:
|
||||
{
|
||||
"matchScore": 匹配度分数(1-100),
|
||||
"skillMatch": 技能匹配度(1-100),
|
||||
"experienceMatch": 经验匹配度(1-100),
|
||||
"salaryMatch": 薪资匹配度(1-100),
|
||||
"isOutsourcing": 是否外包(true/false),
|
||||
"outsourcingConfidence": 外包判断置信度(0-1),
|
||||
"recommendLevel": "推荐等级(excellent/good/medium/low)",
|
||||
"matchReasons": ["匹配原因1", "匹配原因2"],
|
||||
"concerns": ["顾虑点1", "顾虑点2"],
|
||||
"applyAdvice": "投递建议"
|
||||
}`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: '你是一个专业的职业规划师,擅长评估岗位与求职者的匹配度。' },
|
||||
{ role: 'user', content: prompt }
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await this.chat(messages, { temperature: 0.3 });
|
||||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
throw new Error('AI返回格式不正确');
|
||||
} catch (error) {
|
||||
console.warn('岗位匹配分析失败:', error);
|
||||
// 返回默认值
|
||||
return {
|
||||
matchScore: 50,
|
||||
skillMatch: 50,
|
||||
experienceMatch: 50,
|
||||
salaryMatch: 50,
|
||||
isOutsourcing: false,
|
||||
outsourcingConfidence: 0,
|
||||
recommendLevel: 'medium',
|
||||
matchReasons: ['待AI分析'],
|
||||
concerns: ['待AI分析'],
|
||||
applyAdvice: '建议人工审核'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量评估岗位(用于智能筛选)
|
||||
* @param {Array} jobs - 岗位列表
|
||||
* @param {Object} resumeData - 简历数据
|
||||
* @returns {Promise<Array>} 评估结果列表
|
||||
*/
|
||||
async batchMatchJobs(jobs, resumeData) {
|
||||
const results = [];
|
||||
|
||||
// 限制并发数量,避免API限流
|
||||
const concurrency = 3;
|
||||
for (let i = 0; i < jobs.length; i += concurrency) {
|
||||
const batch = jobs.slice(i, i + concurrency);
|
||||
const batchPromises = batch.map(job =>
|
||||
this.matchJobWithResume(job, resumeData).catch(err => {
|
||||
console.warn(`岗位${job.jobId}匹配失败:`, err.message);
|
||||
return {
|
||||
jobId: job.jobId,
|
||||
matchScore: 0,
|
||||
error: err.message
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
results.push(...batchResults);
|
||||
|
||||
// 避免请求过快,休眠一下
|
||||
if (i + concurrency < jobs.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成聊天内容
|
||||
* @param {Object} context - 聊天上下文
|
||||
* @returns {Promise<String>} 生成的聊天内容
|
||||
*/
|
||||
async generateChatContent(context) {
|
||||
const { jobInfo, resumeInfo, chatType = 'greeting', previousMessages = [] } = context;
|
||||
|
||||
let prompt = '';
|
||||
|
||||
switch (chatType) {
|
||||
case 'greeting':
|
||||
prompt = `作为求职者,向HR发送第一条消息表达对以下岗位的兴趣:
|
||||
岗位: ${jobInfo.jobTitle}
|
||||
公司: ${jobInfo.companyName}
|
||||
要求: 简洁、专业、突出自己的优势,不超过100字`;
|
||||
break;
|
||||
|
||||
case 'follow_up':
|
||||
prompt = `HR已查看简历但未回复,需要发送一条礼貌的跟进消息:
|
||||
岗位: ${jobInfo.jobTitle}
|
||||
要求: 礼貌、不唐突、展现持续兴趣,不超过80字`;
|
||||
break;
|
||||
|
||||
case 'interview_confirm':
|
||||
prompt = `HR发出面试邀约,需要确认并表达感谢:
|
||||
岗位: ${jobInfo.jobTitle}
|
||||
面试时间: ${context.interviewTime || '待定'}
|
||||
要求: 专业、感谢、确认参加,不超过60字`;
|
||||
break;
|
||||
|
||||
case 'reply':
|
||||
prompt = `HR说: "${context.hrMessage}"
|
||||
请作为求职者回复,要求: 自然、专业、回答问题,不超过100字`;
|
||||
break;
|
||||
|
||||
default:
|
||||
prompt = `生成一条针对${jobInfo.jobTitle}岗位的沟通消息`;
|
||||
}
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: '你是一个求职者,需要与HR进行专业、礼貌的沟通。回复要简洁、真诚、突出优势。' },
|
||||
...previousMessages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
})),
|
||||
{ role: 'user', content: prompt }
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await this.chat(messages, { temperature: 0.8, max_tokens: 200 });
|
||||
return response.trim();
|
||||
} catch (error) {
|
||||
console.warn('生成聊天内容失败:', error);
|
||||
// 返回默认模板
|
||||
switch (chatType) {
|
||||
case 'greeting':
|
||||
return `您好,我对贵司的${jobInfo.jobTitle}岗位非常感兴趣,我有相关工作经验,期待能有机会详细沟通。`;
|
||||
case 'follow_up':
|
||||
return `您好,不知道您对我的简历是否感兴趣?期待您的回复。`;
|
||||
case 'interview_confirm':
|
||||
return `好的,感谢您的面试邀约,我会准时参加。`;
|
||||
default:
|
||||
return `您好,期待与您沟通。`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为面试邀约
|
||||
* @param {String} message - HR消息内容
|
||||
* @returns {Promise<Object>} 判断结果
|
||||
*/
|
||||
async detectInterviewInvitation(message) {
|
||||
const prompt = `判断以下HR消息是否为面试邀约,并提取关键信息:
|
||||
|
||||
消息内容: "${message}"
|
||||
|
||||
返回JSON格式:
|
||||
{
|
||||
"isInterview": 是否为面试邀约(true/false),
|
||||
"confidence": 置信度(0-1),
|
||||
"interviewType": "面试类型(phone/video/onsite/unknown)",
|
||||
"interviewTime": "面试时间(如果提到)",
|
||||
"interviewLocation": "面试地点(如果提到)",
|
||||
"needReply": 是否需要回复确认(true/false)
|
||||
}`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: '你是一个文本分析专家,擅长识别面试邀约信息。' },
|
||||
{ role: 'user', content: prompt }
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await this.chat(messages, { temperature: 0.1 });
|
||||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
throw new Error('AI返回格式不正确');
|
||||
} catch (error) {
|
||||
console.warn('面试邀约判断失败:', error);
|
||||
// 简单的关键词判断作为降级方案
|
||||
const keywords = ['面试', '邀请', '来面', '约您', '见面', '电话沟通', '视频面试'];
|
||||
const isInterview = keywords.some(kw => message.includes(kw));
|
||||
|
||||
return {
|
||||
isInterview,
|
||||
confidence: isInterview ? 0.7 : 0.3,
|
||||
interviewType: 'unknown',
|
||||
interviewTime: null,
|
||||
interviewLocation: null,
|
||||
needReply: isInterview
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析HR反馈情感
|
||||
* @param {String} message - HR消息内容
|
||||
* @returns {Promise<Object>} 情感分析结果
|
||||
*/
|
||||
async analyzeSentiment(message) {
|
||||
const prompt = `分析以下HR消息的情感倾向:
|
||||
|
||||
消息: "${message}"
|
||||
|
||||
返回JSON格式:
|
||||
{
|
||||
"sentiment": "情感倾向(positive/neutral/negative)",
|
||||
"interest": "兴趣程度(high/medium/low)",
|
||||
"urgency": "紧急程度(high/medium/low)",
|
||||
"keywords": ["关键词1", "关键词2"]
|
||||
}`;
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: '你是一个情感分析专家。' },
|
||||
{ role: 'user', content: prompt }
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await this.chat(messages, { temperature: 0.1 });
|
||||
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
throw new Error('AI返回格式不正确');
|
||||
} catch (error) {
|
||||
console.warn('情感分析失败:', error);
|
||||
return {
|
||||
sentiment: 'neutral',
|
||||
interest: 'medium',
|
||||
urgency: 'low',
|
||||
keywords: []
|
||||
};
|
||||
}
|
||||
}
|
||||
"skillTags": ["技能1", "技能2", "技能3"], // 技能标签数组(编程语言、框架、工具等)
|
||||
"strengths": "核心优势描述", // 简历的优势和亮点
|
||||
"weaknesses": "不足之处描述", // 简历的不足或需要改进的地方
|
||||
"careerSuggestion": "职业发展建议", // 针对该简历的职业发展方向和建议
|
||||
"competitiveness": 75 // 竞争力评分(0-100的整数),综合考虑工作年限、技能、经验等因素
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
let instance = null;
|
||||
要求:
|
||||
1. skillTags 必须是字符串数组
|
||||
2. strengths、weaknesses、careerSuggestion 是字符串描述
|
||||
3. competitiveness 必须是 0-100 之间的整数
|
||||
4. 所有字段都必须返回,如果没有相关信息,使用空数组或空字符串
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* 获取AI服务实例
|
||||
* @returns {AIService}
|
||||
*/
|
||||
getInstance() {
|
||||
if (!instance) {
|
||||
instance = new AIService();
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的简历分析师,擅长分析简历的核心要素、优势劣势、竞争力评分和职业发展建议。请以 JSON 格式返回分析结果,确保格式正确。',
|
||||
temperature: 0.3,
|
||||
maxTokens: 1500,
|
||||
business_type: 'resume_analysis',
|
||||
service_type: 'completion'
|
||||
});
|
||||
|
||||
try {
|
||||
// 尝试从返回内容中提取 JSON
|
||||
let content = result.content.trim();
|
||||
|
||||
// 如果返回内容被代码块包裹,提取其中的 JSON
|
||||
const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || content.match(/(\{[\s\S]*\})/);
|
||||
if (jsonMatch) {
|
||||
content = jsonMatch[1];
|
||||
}
|
||||
|
||||
const analysis = JSON.parse(content);
|
||||
return {
|
||||
analysis: analysis
|
||||
};
|
||||
} catch (parseError) {
|
||||
console.error(`[AI服务] 简历分析结果解析失败:`, parseError);
|
||||
console.error(`[AI服务] 原始内容:`, result.content);
|
||||
return {
|
||||
analysis: {
|
||||
content: result.content,
|
||||
parseError: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建新的AI服务实例
|
||||
* @returns {AIService}
|
||||
*/
|
||||
createInstance() {
|
||||
return new AIService();
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 生成面试邀约内容
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @param {object} chatHistory - 聊天历史
|
||||
* @returns {Promise<object>} 面试邀约内容
|
||||
*/
|
||||
async generateInterviewInvitation(jobInfo, chatHistory) {
|
||||
const prompt = `
|
||||
请基于以下信息生成面试邀约内容:
|
||||
|
||||
岗位信息:
|
||||
- 公司名称:${jobInfo.companyName || '未知'}
|
||||
- 职位名称:${jobInfo.jobTitle || '未知'}
|
||||
- 工作地点:${jobInfo.location || '未知'}
|
||||
|
||||
聊天历史:
|
||||
${chatHistory || '无'}
|
||||
|
||||
要求:
|
||||
1. 表达面试邀约的诚意
|
||||
2. 提供灵活的时间选择
|
||||
3. 说明面试形式和地点
|
||||
4. 体现对候选人的重视
|
||||
5. 语言自然、专业
|
||||
|
||||
请直接返回面试邀约内容。
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的HR,擅长生成面试邀约内容。',
|
||||
temperature: 0.6,
|
||||
business_type: 'interview_invitation',
|
||||
service_type: 'chat'
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别外包岗位
|
||||
* @param {object} jobInfo - 岗位信息
|
||||
* @returns {Promise<object>} 外包识别结果
|
||||
*/
|
||||
async identifyOutsourcingJob(jobInfo) {
|
||||
const prompt = `
|
||||
请分析以下岗位信息,判断是否为外包岗位:
|
||||
|
||||
岗位信息:
|
||||
- 公司名称:${jobInfo.companyName || '未知'}
|
||||
- 职位名称:${jobInfo.jobTitle || '未知'}
|
||||
- 岗位描述:${jobInfo.description || '未知'}
|
||||
- 技能要求:${jobInfo.skills || '未知'}
|
||||
- 工作地点:${jobInfo.location || '未知'}
|
||||
|
||||
外包岗位特征:
|
||||
1. 公司名称包含"外包"、"派遣"、"人力"等关键词
|
||||
2. 岗位描述提到"项目外包"、"驻场开发"等
|
||||
3. 技能要求过于宽泛或具体
|
||||
4. 工作地点频繁变动
|
||||
5. 薪资结构不明确
|
||||
|
||||
请判断是否为外包岗位,并给出详细分析。
|
||||
`;
|
||||
|
||||
const result = await this.callAPI(prompt, {
|
||||
systemPrompt: '你是一个专业的岗位分析师,擅长识别外包岗位的特征。',
|
||||
temperature: 0.3,
|
||||
business_type: 'outsourcing_detection',
|
||||
service_type: 'completion'
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new aiService();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
const db = require('../middleware/dbProxy');
|
||||
const scheduleManager = require('../middleware/schedule/index.js');
|
||||
const locationService = require('./location_service');
|
||||
const locationService = require('./locationService');
|
||||
const authorizationService = require('./authorization_service');
|
||||
const { addRemainingDays, addRemainingDaysToAccounts } = require('../utils/account_utils');
|
||||
|
||||
@@ -486,10 +486,31 @@ class PlaAccountService {
|
||||
finalParams.keyword = account.keyword;
|
||||
}
|
||||
|
||||
// 构建指令对象
|
||||
// get_job_list 从 resume_info 取 deliver_tab_label 作为 tabLabel;入库 keyword 优先用期望职位而非写死
|
||||
if (commandTypeSnake === 'get_job_list') {
|
||||
try {
|
||||
const resume_info = db.getModel('resume_info');
|
||||
const resume = await resume_info.findOne({
|
||||
where: { sn_code: account.sn_code, platform: account.platform_type, isActive: true },
|
||||
order: [['last_modify_time', 'DESC']],
|
||||
attributes: ['deliver_tab_label']
|
||||
});
|
||||
if (resume && resume.deliver_tab_label) {
|
||||
const tab = String(resume.deliver_tab_label).trim();
|
||||
finalParams.tabLabel = tab;
|
||||
if (!finalParams.keyword || String(finalParams.keyword).trim() === '') {
|
||||
finalParams.keyword = tab;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[pla_account_service] 读取 resume_info.deliver_tab_label 失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建指令对象(与前端/后端/下发统一:只用一个名字 command_type)
|
||||
const command = {
|
||||
command_type: commandTypeSnake,
|
||||
command_name: commandName || commandType,
|
||||
command_name: commandTypeSnake,
|
||||
command_params: JSON.stringify(finalParams)
|
||||
};
|
||||
|
||||
@@ -497,7 +518,7 @@ class PlaAccountService {
|
||||
const task = await task_status.create({
|
||||
sn_code: account.sn_code,
|
||||
taskType: commandTypeSnake,
|
||||
taskName: commandName || commandType,
|
||||
taskName: commandTypeSnake,
|
||||
taskParams: JSON.stringify(finalParams)
|
||||
});
|
||||
|
||||
@@ -567,10 +588,6 @@ class PlaAccountService {
|
||||
throw new Error('指令不存在');
|
||||
}
|
||||
|
||||
// 检查指令状态
|
||||
if (command.status !== 'failed') {
|
||||
throw new Error('只能重试失败的指令');
|
||||
}
|
||||
|
||||
// 获取任务信息
|
||||
const task = await task_status.findByPk(command.task_id);
|
||||
@@ -627,15 +644,39 @@ class PlaAccountService {
|
||||
command_params: JSON.stringify(commandParams)
|
||||
};
|
||||
|
||||
// 执行指令
|
||||
const result = await scheduleManager.command.executeCommand(task.id, commandObj, scheduleManager.mqttClient);
|
||||
// 执行指令并同步更新当前指令记录状态
|
||||
const start_time = new Date();
|
||||
try {
|
||||
const result = await scheduleManager.command.executeCommand(task.id, commandObj, scheduleManager.mqttClient);
|
||||
const end_time = new Date();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '指令重试成功',
|
||||
commandId: command.id,
|
||||
result: result
|
||||
};
|
||||
await command.update({
|
||||
status: 'completed',
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
duration: end_time.getTime() - start_time.getTime(),
|
||||
result: JSON.stringify(result || {})
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '指令重试成功',
|
||||
commandId: command.id,
|
||||
result: result
|
||||
};
|
||||
} catch (error) {
|
||||
const end_time = new Date();
|
||||
await command.update({
|
||||
status: 'failed',
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
duration: end_time.getTime() - start_time.getTime(),
|
||||
error_message: error.message || '指令重试失败',
|
||||
error_stack: error.stack || ''
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -750,8 +791,8 @@ class PlaAccountService {
|
||||
const sn_code = account.sn_code;
|
||||
const platform = account.platform_type || 'boss';
|
||||
|
||||
// 2. 创建任务记录(使用新的搜索任务类型)
|
||||
const taskType = 'search_jobs';
|
||||
// 2. 创建任务记录(使用 auto_search,与定时任务统一)
|
||||
const taskType = 'auto_search';
|
||||
const taskName = autoDeliver ? '搜索并投递职位' : '搜索职位列表';
|
||||
|
||||
const task = await task_status.create({
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
/**
|
||||
* 账号工具函数测试
|
||||
*/
|
||||
|
||||
const {
|
||||
calculateRemainingDays,
|
||||
isAuthorizationValid,
|
||||
addRemainingDays,
|
||||
addRemainingDaysToAccounts
|
||||
} = require('../utils/account_utils');
|
||||
|
||||
// 测试剩余天数计算
|
||||
function testCalculateRemainingDays() {
|
||||
console.log('\n===== 测试剩余天数计算 =====');
|
||||
|
||||
try {
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
// 测试 1: 未来有效期
|
||||
const futureDate = dayjs().subtract(5, 'day').toDate();
|
||||
const remaining1 = calculateRemainingDays(futureDate, 30);
|
||||
console.log('✓ 未来有效期 (5天前授权30天):', remaining1, '天');
|
||||
console.assert(remaining1 === 25, `期望25天,实际${remaining1}天`);
|
||||
|
||||
// 测试 2: 已过期
|
||||
const pastDate = dayjs().subtract(40, 'day').toDate();
|
||||
const remaining2 = calculateRemainingDays(pastDate, 30);
|
||||
console.log('✓ 已过期 (40天前授权30天):', remaining2, '天');
|
||||
console.assert(remaining2 === 0, `期望0天,实际${remaining2}天`);
|
||||
|
||||
// 测试 3: 今天到期
|
||||
const todayDate = dayjs().startOf('day').toDate();
|
||||
const remaining3 = calculateRemainingDays(todayDate, 0);
|
||||
console.log('✓ 今天到期:', remaining3, '天');
|
||||
|
||||
// 测试 4: 空值处理
|
||||
const remaining4 = calculateRemainingDays(null, 30);
|
||||
console.log('✓ 空授权日期:', remaining4, '天');
|
||||
console.assert(remaining4 === 0, '空值应返回0');
|
||||
|
||||
const remaining5 = calculateRemainingDays(futureDate, 0);
|
||||
console.log('✓ 0天授权:', remaining5, '天');
|
||||
console.assert(remaining5 === 0, '0天授权应返回0');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ 剩余天数计算测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试授权有效性检查
|
||||
function testIsAuthorizationValid() {
|
||||
console.log('\n===== 测试授权有效性检查 =====');
|
||||
|
||||
try {
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
// 测试 1: 有效授权
|
||||
const validDate = dayjs().subtract(5, 'day').toDate();
|
||||
const isValid = isAuthorizationValid(validDate, 30);
|
||||
console.log('✓ 有效授权 (5天前授权30天):', isValid ? '有效' : '无效');
|
||||
console.assert(isValid === true, '应该是有效的');
|
||||
|
||||
// 测试 2: 过期授权
|
||||
const expiredDate = dayjs().subtract(40, 'day').toDate();
|
||||
const isExpired = isAuthorizationValid(expiredDate, 30);
|
||||
console.log('✓ 过期授权 (40天前授权30天):', isExpired ? '有效' : '无效');
|
||||
console.assert(isExpired === false, '应该是无效的');
|
||||
|
||||
// 测试 3: 空值处理
|
||||
const isNull = isAuthorizationValid(null, 30);
|
||||
console.log('✓ 空授权日期:', isNull ? '有效' : '无效');
|
||||
console.assert(isNull === false, '空值应该无效');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ 授权有效性检查测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试添加剩余天数
|
||||
function testAddRemainingDays() {
|
||||
console.log('\n===== 测试添加剩余天数 =====');
|
||||
|
||||
try {
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
// 测试 1: 普通对象
|
||||
const account1 = {
|
||||
id: 1,
|
||||
sn_code: 'SN001',
|
||||
authorization_date: dayjs().subtract(5, 'day').toDate(),
|
||||
authorization_days: 30
|
||||
};
|
||||
|
||||
const result1 = addRemainingDays(account1);
|
||||
console.log('✓ 普通对象添加剩余天数:', result1.remaining_days, '天');
|
||||
console.assert(result1.remaining_days === 25, `期望25天,实际${result1.remaining_days}天`);
|
||||
|
||||
// 测试 2: Sequelize实例模拟
|
||||
const account2 = {
|
||||
id: 2,
|
||||
sn_code: 'SN002',
|
||||
authorization_date: dayjs().subtract(10, 'day').toDate(),
|
||||
authorization_days: 15,
|
||||
toJSON: function() {
|
||||
return {
|
||||
id: this.id,
|
||||
sn_code: this.sn_code,
|
||||
authorization_date: this.authorization_date,
|
||||
authorization_days: this.authorization_days
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const result2 = addRemainingDays(account2);
|
||||
console.log('✓ Sequelize实例添加剩余天数:', result2.remaining_days, '天');
|
||||
console.assert(result2.remaining_days === 5, `期望5天,实际${result2.remaining_days}天`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ 添加剩余天数测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试批量添加剩余天数
|
||||
function testAddRemainingDaysToAccounts() {
|
||||
console.log('\n===== 测试批量添加剩余天数 =====');
|
||||
|
||||
try {
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
const accounts = [
|
||||
{
|
||||
id: 1,
|
||||
authorization_date: dayjs().subtract(5, 'day').toDate(),
|
||||
authorization_days: 30
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
authorization_date: dayjs().subtract(10, 'day').toDate(),
|
||||
authorization_days: 15
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
authorization_date: dayjs().subtract(50, 'day').toDate(),
|
||||
authorization_days: 30
|
||||
}
|
||||
];
|
||||
|
||||
const results = addRemainingDaysToAccounts(accounts);
|
||||
console.log('✓ 批量添加剩余天数:');
|
||||
results.forEach((acc, index) => {
|
||||
console.log(` 账号${index + 1}: ${acc.remaining_days}天`);
|
||||
});
|
||||
|
||||
console.assert(results.length === 3, '数组长度应该是3');
|
||||
console.assert(results[0].remaining_days === 25, '第1个账号剩余天数错误');
|
||||
console.assert(results[1].remaining_days === 5, '第2个账号剩余天数错误');
|
||||
console.assert(results[2].remaining_days === 0, '第3个账号剩余天数错误');
|
||||
|
||||
// 测试空数组
|
||||
const emptyResults = addRemainingDaysToAccounts([]);
|
||||
console.log('✓ 空数组处理:', emptyResults.length === 0 ? '正确' : '错误');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ 批量添加剩余天数测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试时区处理
|
||||
function testTimezoneHandling() {
|
||||
console.log('\n===== 测试时区处理 =====');
|
||||
|
||||
try {
|
||||
const dayjs = require('dayjs');
|
||||
const utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
|
||||
// 创建不同时区的日期
|
||||
const localDate = dayjs().subtract(5, 'day').toDate();
|
||||
const utcDate = dayjs().utc().subtract(5, 'day').toDate();
|
||||
|
||||
const remaining1 = calculateRemainingDays(localDate, 30);
|
||||
const remaining2 = calculateRemainingDays(utcDate, 30);
|
||||
|
||||
console.log('✓ 本地时区日期剩余天数:', remaining1, '天');
|
||||
console.log('✓ UTC时区日期剩余天数:', remaining2, '天');
|
||||
|
||||
// 剩余天数应该接近(可能相差1天因为时区转换)
|
||||
const diff = Math.abs(remaining1 - remaining2);
|
||||
console.log('✓ 时区差异:', diff, '天');
|
||||
console.assert(diff <= 1, '时区差异应该不超过1天');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ 时区处理测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
async function runAllTests() {
|
||||
console.log('\n==================== 开始测试 ====================\n');
|
||||
|
||||
const results = [];
|
||||
|
||||
results.push(testCalculateRemainingDays());
|
||||
results.push(testIsAuthorizationValid());
|
||||
results.push(testAddRemainingDays());
|
||||
results.push(testAddRemainingDaysToAccounts());
|
||||
results.push(testTimezoneHandling());
|
||||
|
||||
console.log('\n==================== 测试总结 ====================\n');
|
||||
|
||||
const passed = results.filter(r => r).length;
|
||||
const total = results.length;
|
||||
|
||||
console.log(`测试通过: ${passed}/${total}`);
|
||||
|
||||
if (passed === total) {
|
||||
console.log('\n✓ 所有测试通过!\n');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n✗ 部分测试失败\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
if (require.main === module) {
|
||||
runAllTests().catch(error => {
|
||||
console.error('测试执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testCalculateRemainingDays,
|
||||
testIsAuthorizationValid,
|
||||
testAddRemainingDays,
|
||||
testAddRemainingDaysToAccounts,
|
||||
testTimezoneHandling
|
||||
};
|
||||
@@ -1,207 +0,0 @@
|
||||
/**
|
||||
* 加密工具函数测试
|
||||
*/
|
||||
|
||||
const {
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
generateToken,
|
||||
generateDeviceId,
|
||||
validateDeviceId,
|
||||
maskPhone,
|
||||
maskEmail,
|
||||
maskSensitiveData
|
||||
} = require('../utils/crypto_utils');
|
||||
|
||||
// 测试密码加密和验证
|
||||
async function testPasswordEncryption() {
|
||||
console.log('\n===== 测试密码加密和验证 =====');
|
||||
|
||||
try {
|
||||
// 测试 1: 基本加密和验证
|
||||
const password = 'mySecurePassword123';
|
||||
const hashed = await hashPassword(password);
|
||||
console.log('✓ 密码加密成功:', hashed.substring(0, 20) + '...');
|
||||
|
||||
// 验证正确密码
|
||||
const isValid = await verifyPassword(password, hashed);
|
||||
console.log('✓ 正确密码验证:', isValid ? '通过' : '失败');
|
||||
|
||||
// 验证错误密码
|
||||
const isInvalid = await verifyPassword('wrongPassword', hashed);
|
||||
console.log('✓ 错误密码验证:', isInvalid ? '失败(不应该通过)' : '正确拒绝');
|
||||
|
||||
// 测试 2: 相同密码生成不同哈希
|
||||
const hashed2 = await hashPassword(password);
|
||||
console.log('✓ 相同密码生成不同哈希:', hashed !== hashed2 ? '是' : '否');
|
||||
|
||||
// 测试 3: 空密码处理
|
||||
try {
|
||||
await hashPassword('');
|
||||
console.log('✗ 空密码应该抛出错误');
|
||||
} catch (error) {
|
||||
console.log('✓ 空密码正确抛出错误');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ 密码加密测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试设备ID生成和验证
|
||||
function testDeviceId() {
|
||||
console.log('\n===== 测试设备ID生成和验证 =====');
|
||||
|
||||
try {
|
||||
// 测试 1: 生成设备ID
|
||||
const deviceId1 = generateDeviceId();
|
||||
console.log('✓ 生成设备ID:', deviceId1);
|
||||
|
||||
// 测试 2: 验证有效设备ID
|
||||
const isValid = validateDeviceId(deviceId1);
|
||||
console.log('✓ 验证有效设备ID:', isValid ? '通过' : '失败');
|
||||
|
||||
// 测试 3: 验证无效设备ID
|
||||
const invalidIds = [
|
||||
'invalid_id',
|
||||
'device_abc_123',
|
||||
'123456789',
|
||||
'',
|
||||
null,
|
||||
undefined
|
||||
];
|
||||
|
||||
let allInvalidRejected = true;
|
||||
for (const id of invalidIds) {
|
||||
if (validateDeviceId(id)) {
|
||||
console.log('✗ 无效ID未被拒绝:', id);
|
||||
allInvalidRejected = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (allInvalidRejected) {
|
||||
console.log('✓ 所有无效设备ID都被正确拒绝');
|
||||
}
|
||||
|
||||
// 测试 4: 生成的ID唯一性
|
||||
const deviceId2 = generateDeviceId();
|
||||
console.log('✓ 生成的ID是唯一的:', deviceId1 !== deviceId2 ? '是' : '否');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ 设备ID测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试数据脱敏
|
||||
function testDataMasking() {
|
||||
console.log('\n===== 测试数据脱敏 =====');
|
||||
|
||||
try {
|
||||
// 测试 1: 手机号脱敏
|
||||
const phone = '13800138000';
|
||||
const maskedPhone = maskPhone(phone);
|
||||
console.log('✓ 手机号脱敏:', phone, '->', maskedPhone);
|
||||
console.assert(maskedPhone === '138****8000', '手机号脱敏格式错误');
|
||||
|
||||
// 测试 2: 邮箱脱敏
|
||||
const email = 'user@example.com';
|
||||
const maskedEmail = maskEmail(email);
|
||||
console.log('✓ 邮箱脱敏:', email, '->', maskedEmail);
|
||||
|
||||
// 测试 3: 对象脱敏
|
||||
const sensitiveObj = {
|
||||
username: 'john',
|
||||
password: 'secret123',
|
||||
email: 'john@example.com',
|
||||
token: 'abc123xyz',
|
||||
normalField: 'public data'
|
||||
};
|
||||
|
||||
const masked = maskSensitiveData(sensitiveObj);
|
||||
console.log('✓ 对象脱敏:');
|
||||
console.log(' 原始:', sensitiveObj);
|
||||
console.log(' 脱敏:', masked);
|
||||
|
||||
// 验证敏感字段被屏蔽
|
||||
console.assert(masked.password === '***MASKED***', 'password未被屏蔽');
|
||||
console.assert(masked.token === '***MASKED***', 'token未被屏蔽');
|
||||
console.assert(masked.normalField === 'public data', '普通字段被修改');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ 数据脱敏测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试Token生成
|
||||
function testTokenGeneration() {
|
||||
console.log('\n===== 测试Token生成 =====');
|
||||
|
||||
try {
|
||||
// 测试 1: 生成默认长度token
|
||||
const token1 = generateToken();
|
||||
console.log('✓ 生成默认token (64字符):', token1.substring(0, 20) + '...');
|
||||
console.assert(token1.length === 64, 'Token长度错误');
|
||||
|
||||
// 测试 2: 生成指定长度token
|
||||
const token2 = generateToken(16);
|
||||
console.log('✓ 生成16字节token (32字符):', token2);
|
||||
console.assert(token2.length === 32, 'Token长度错误');
|
||||
|
||||
// 测试 3: Token唯一性
|
||||
const token3 = generateToken();
|
||||
console.log('✓ Token唯一性:', token1 !== token3 ? '是' : '否');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('✗ Token生成测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
async function runAllTests() {
|
||||
console.log('\n==================== 开始测试 ====================\n');
|
||||
|
||||
const results = [];
|
||||
|
||||
results.push(await testPasswordEncryption());
|
||||
results.push(testDeviceId());
|
||||
results.push(testDataMasking());
|
||||
results.push(testTokenGeneration());
|
||||
|
||||
console.log('\n==================== 测试总结 ====================\n');
|
||||
|
||||
const passed = results.filter(r => r).length;
|
||||
const total = results.length;
|
||||
|
||||
console.log(`测试通过: ${passed}/${total}`);
|
||||
|
||||
if (passed === total) {
|
||||
console.log('\n✓ 所有测试通过!\n');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n✗ 部分测试失败\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
if (require.main === module) {
|
||||
runAllTests().catch(error => {
|
||||
console.error('测试执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testPasswordEncryption,
|
||||
testDeviceId,
|
||||
testDataMasking,
|
||||
testTokenGeneration
|
||||
};
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* 邀请注册功能测试
|
||||
* 测试新用户注册试用期和邀请人奖励逻辑
|
||||
*/
|
||||
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
// 运行所有测试
|
||||
function runTests() {
|
||||
console.log('\n========================================');
|
||||
console.log('开始测试邀请注册功能');
|
||||
console.log('========================================');
|
||||
|
||||
const tests = [
|
||||
{ name: '新用户注册应该获得3天试用期', fn: () => {
|
||||
console.log('\n【测试1】新用户注册应该获得3天试用期');
|
||||
const newUserData = {
|
||||
authorization_date: new Date(),
|
||||
authorization_days: 3
|
||||
};
|
||||
if (!newUserData.authorization_date) throw new Error('授权日期为空');
|
||||
if (newUserData.authorization_days !== 3) throw new Error('授权天数不是3天');
|
||||
console.log('✅ 通过: 新用户获得3天试用期');
|
||||
console.log(' - 授权日期:', dayjs(newUserData.authorization_date).format('YYYY-MM-DD'));
|
||||
console.log(' - 授权天数:', newUserData.authorization_days, '天');
|
||||
}},
|
||||
{ name: '邀请人授权未过期时,应该累加3天', fn: () => {
|
||||
console.log('\n【测试2】邀请人授权未过期时,应该累加3天');
|
||||
const inviterData = {
|
||||
authorization_date: dayjs().subtract(2, 'day').toDate(),
|
||||
authorization_days: 7
|
||||
};
|
||||
const currentEndDate = dayjs(inviterData.authorization_date).add(inviterData.authorization_days, 'day');
|
||||
const now = dayjs();
|
||||
const remainingDays = currentEndDate.diff(now, 'day');
|
||||
|
||||
console.log(' 邀请人当前状态:');
|
||||
console.log(' - 授权开始日期:', dayjs(inviterData.authorization_date).format('YYYY-MM-DD'));
|
||||
console.log(' - 授权总天数:', inviterData.authorization_days, '天');
|
||||
console.log(' - 剩余天数:', remainingDays, '天');
|
||||
|
||||
if (currentEndDate.isBefore(now)) throw new Error('测试数据错误:授权应该未过期');
|
||||
const newAuthDays = inviterData.authorization_days + 3;
|
||||
if (newAuthDays !== 10) throw new Error('累加计算错误');
|
||||
console.log('✅ 通过: 未过期授权累加 7天 + 3天 = 10天');
|
||||
}},
|
||||
{ name: '邀请人授权已过期时,应该重新激活给3天', fn: () => {
|
||||
console.log('\n【测试3】邀请人授权已过期时,应该重新激活给3天');
|
||||
const inviterData = {
|
||||
authorization_date: dayjs().subtract(10, 'day').toDate(),
|
||||
authorization_days: 5
|
||||
};
|
||||
const currentEndDate = dayjs(inviterData.authorization_date).add(inviterData.authorization_days, 'day');
|
||||
const now = dayjs();
|
||||
const daysExpired = now.diff(currentEndDate, 'day');
|
||||
|
||||
console.log(' 邀请人当前状态:');
|
||||
console.log(' - 授权开始日期:', dayjs(inviterData.authorization_date).format('YYYY-MM-DD'));
|
||||
console.log(' - 授权天数:', inviterData.authorization_days, '天');
|
||||
console.log(' - 过期日期:', currentEndDate.format('YYYY-MM-DD'));
|
||||
console.log(' - 已过期天数:', daysExpired, '天');
|
||||
|
||||
if (!currentEndDate.isBefore(now)) throw new Error('测试数据错误:授权应该已过期');
|
||||
const newAuthDate = new Date();
|
||||
const newAuthDays = 3;
|
||||
if (newAuthDays !== 3) throw new Error('重新激活计算错误');
|
||||
console.log('✅ 通过: 过期授权重新激活给3天');
|
||||
console.log(' - 新授权开始日期:', dayjs(newAuthDate).format('YYYY-MM-DD'));
|
||||
}},
|
||||
{ name: '邀请人没有授权日期时,应该从今天开始累加', fn: () => {
|
||||
console.log('\n【测试4】邀请人没有授权日期时,应该从今天开始累加');
|
||||
const inviterData = {
|
||||
authorization_date: null,
|
||||
authorization_days: 0
|
||||
};
|
||||
console.log(' 邀请人当前状态:');
|
||||
console.log(' - 授权开始日期: 无');
|
||||
console.log(' - 授权天数:', inviterData.authorization_days, '天');
|
||||
|
||||
if (inviterData.authorization_date) throw new Error('测试数据错误:授权日期应该为空');
|
||||
const newAuthDate = new Date();
|
||||
const newAuthDays = 3;
|
||||
if (!newAuthDate) throw new Error('授权日期为空');
|
||||
if (newAuthDays !== 3) throw new Error('天数计算错误');
|
||||
console.log('✅ 通过: 首次授权从今天开始给3天');
|
||||
console.log(' - 新授权开始日期:', dayjs(newAuthDate).format('YYYY-MM-DD'));
|
||||
console.log(' - 新授权天数:', newAuthDays, '天');
|
||||
}},
|
||||
{ name: '邀请记录应该正确保存', fn: () => {
|
||||
console.log('\n【测试5】邀请记录应该正确保存');
|
||||
const record = {
|
||||
inviter_id: 1,
|
||||
invitee_id: 2,
|
||||
invite_code: 'INV1_ABC123',
|
||||
reward_status: 1,
|
||||
reward_type: 'trial_days',
|
||||
reward_value: 3
|
||||
};
|
||||
console.log(' 邀请记录内容:');
|
||||
console.log(' - 邀请人ID:', record.inviter_id);
|
||||
console.log(' - 被邀请人ID:', record.invitee_id);
|
||||
console.log(' - 邀请码:', record.invite_code);
|
||||
console.log(' - 奖励状态:', record.reward_status === 1 ? '已发放' : '未发放');
|
||||
console.log(' - 奖励类型:', record.reward_type);
|
||||
console.log(' - 奖励值:', record.reward_value, '天');
|
||||
|
||||
if (record.reward_status !== 1) throw new Error('奖励状态错误');
|
||||
if (record.reward_type !== 'trial_days') throw new Error('奖励类型错误');
|
||||
if (record.reward_value !== 3) throw new Error('奖励值错误');
|
||||
console.log('✅ 通过: 邀请记录字段正确');
|
||||
}}
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
tests.forEach(test => {
|
||||
try {
|
||||
test.fn();
|
||||
passed++;
|
||||
} catch (error) {
|
||||
failed++;
|
||||
console.error(`❌ 失败: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('测试完成');
|
||||
console.log('========================================');
|
||||
console.log(`✅ 通过: ${passed}/${tests.length}`);
|
||||
console.log(`❌ 失败: ${failed}/${tests.length}`);
|
||||
console.log(`成功率: ${(passed / tests.length * 100).toFixed(0)}%`);
|
||||
console.log('========================================\n');
|
||||
|
||||
return failed === 0;
|
||||
}
|
||||
|
||||
// 如果直接运行此文件,执行测试
|
||||
if (require.main === module) {
|
||||
const success = runTests();
|
||||
process.exit(success ? 0 : 1);
|
||||
}
|
||||
|
||||
module.exports = { runTests };
|
||||
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* 注册功能测试 - 验证密码加密
|
||||
*/
|
||||
|
||||
const { hashPassword, verifyPassword } = require('../utils/crypto_utils');
|
||||
|
||||
async function testRegisterPasswordEncryption() {
|
||||
console.log('\n===== 测试注册密码加密 =====\n');
|
||||
|
||||
try {
|
||||
// 模拟注册流程
|
||||
const testPassword = 'testPassword123';
|
||||
|
||||
console.log('1. 模拟用户注册...');
|
||||
console.log(' - 原始密码: ' + testPassword);
|
||||
|
||||
// 加密密码(注册时执行)
|
||||
const hashedPassword = await hashPassword(testPassword);
|
||||
console.log(' - 加密后密码: ' + hashedPassword.substring(0, 30) + '...');
|
||||
console.log(' ✓ 密码已加密并存储到数据库\n');
|
||||
|
||||
// 模拟登录验证
|
||||
console.log('2. 模拟用户登录验证...');
|
||||
console.log(' - 用户输入密码: ' + testPassword);
|
||||
|
||||
// 验证密码(登录时执行)
|
||||
const isValid = await verifyPassword(testPassword, hashedPassword);
|
||||
console.log(' - 验证结果: ' + (isValid ? '✓ 通过' : '✗ 失败'));
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('密码验证失败');
|
||||
}
|
||||
|
||||
// 测试错误密码
|
||||
console.log('\n3. 测试错误密码...');
|
||||
const wrongPassword = 'wrongPassword';
|
||||
const isWrong = await verifyPassword(wrongPassword, hashedPassword);
|
||||
console.log(' - 错误密码验证结果: ' + (isWrong ? '✗ 通过(不应该)' : '✓ 正确拒绝'));
|
||||
|
||||
if (isWrong) {
|
||||
throw new Error('错误密码不应该通过验证');
|
||||
}
|
||||
|
||||
console.log('\n✓ 注册密码加密功能测试通过!');
|
||||
console.log('✓ 新注册用户的密码会自动加密存储');
|
||||
console.log('✓ 登录时可以正确验证加密密码\n');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('\n✗ 测试失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试密码长度验证
|
||||
function testPasswordValidation() {
|
||||
console.log('\n===== 测试密码长度验证 =====\n');
|
||||
|
||||
const testCases = [
|
||||
{ password: '12345', valid: false, reason: '少于6位' },
|
||||
{ password: '123456', valid: true, reason: '等于6位' },
|
||||
{ password: 'myPassword123', valid: true, reason: '正常长度' },
|
||||
{ password: 'a'.repeat(50), valid: true, reason: '等于50位' },
|
||||
{ password: 'a'.repeat(51), valid: false, reason: '超过50位' }
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
|
||||
testCases.forEach((testCase, index) => {
|
||||
const result = testCase.password.length >= 6 && testCase.password.length <= 50;
|
||||
const passed = result === testCase.valid;
|
||||
|
||||
console.log(`测试 ${index + 1}: ${testCase.reason}`);
|
||||
console.log(` 密码长度: ${testCase.password.length}`);
|
||||
console.log(` 期望: ${testCase.valid ? '有效' : '无效'}`);
|
||||
console.log(` 结果: ${passed ? '✓ 通过' : '✗ 失败'}\n`);
|
||||
|
||||
if (!passed) {
|
||||
allPassed = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (allPassed) {
|
||||
console.log('✓ 密码长度验证测试全部通过!\n');
|
||||
} else {
|
||||
console.log('✗ 部分密码长度验证测试失败\n');
|
||||
}
|
||||
|
||||
return allPassed;
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
async function runAllTests() {
|
||||
console.log('\n==================== 注册功能安全测试 ====================\n');
|
||||
console.log('测试场景:验证注册时密码是否正确加密存储\n');
|
||||
|
||||
const results = [];
|
||||
|
||||
results.push(await testRegisterPasswordEncryption());
|
||||
results.push(testPasswordValidation());
|
||||
|
||||
console.log('\n==================== 测试总结 ====================\n');
|
||||
|
||||
const passed = results.filter(r => r).length;
|
||||
const total = results.length;
|
||||
|
||||
console.log(`测试通过: ${passed}/${total}`);
|
||||
|
||||
if (passed === total) {
|
||||
console.log('\n✓ 所有测试通过!');
|
||||
console.log('✓ 注册功能已修复,密码会自动加密存储');
|
||||
console.log('✓ 系统现在完全安全\n');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n✗ 部分测试失败\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
if (require.main === module) {
|
||||
runAllTests().catch(error => {
|
||||
console.error('测试执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testRegisterPasswordEncryption,
|
||||
testPasswordValidation
|
||||
};
|
||||
@@ -61,7 +61,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
// 白名单URL - 不需要token验证的接口
|
||||
"allowUrls": ["/admin_api/sys_user/login", "/admin_api/sys_user/authorityMenus", "/admin_api/sys_user/register", "/api/user/loginByWeixin", "/file/", "/sys_file/", "/admin_api/win_data/viewLogInfo", "/api/user/wx_auth", '/api/docs', 'api/swagger.json', 'payment/notify', 'payment/refund-notify', 'wallet/transfer_notify', 'user/sms/send', 'user/sms/verify', '/api/version/check','/api/file/upload_file_to_oss_by_auto_work','/api/version/create', 'register', 'send_email_code','/config/remote-code/version'],
|
||||
"allowUrls": ["/admin_api/sys_user/login", "/admin_api/sys_user/authorityMenus", "/admin_api/sys_user/register", "/api/user/loginByWeixin", "/file/", "/sys_file/", "/admin_api/win_data/viewLogInfo", "/api/user/wx_auth", '/api/docs', 'api/swagger.json', 'payment/notify', 'payment/refund-notify', 'wallet/transfer_notify', 'user/sms/send', 'user/sms/verify', '/api/version/check','/api/file/upload_file_to_oss_by_auto_work','/api/version/create', 'register', 'send_email_code','/config/remote-code/version','/api/static/boss'],
|
||||
|
||||
|
||||
// AI服务配置
|
||||
|
||||
145
docs/ai_service_config.md
Normal file
145
docs/ai_service_config.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# AI 服务配置说明
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
AI 服务需要通过环境变量进行配置,支持阿里云 DashScope API。
|
||||
|
||||
### 必需的环境变量
|
||||
|
||||
在 `.env` 文件或系统环境变量中配置以下参数:
|
||||
|
||||
```bash
|
||||
# AI API 密钥(阿里云 DashScope API Key)
|
||||
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# AI API 基础 URL(阿里云 DashScope 兼容 OpenAI 格式接口)
|
||||
AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
|
||||
# AI 模型名称
|
||||
# 可选值:
|
||||
# - qwen-turbo(快速,推荐日常使用)
|
||||
# - qwen-plus(增强,平衡性能和成本)
|
||||
# - qwen-max(最强,高质量输出)
|
||||
# - qwen-long(长文本,支持超长上下文)
|
||||
AI_MODEL=qwen-turbo
|
||||
```
|
||||
|
||||
### 配置示例
|
||||
|
||||
#### 1. 开发环境配置 (.env)
|
||||
|
||||
```bash
|
||||
# 阿里云 DashScope 配置
|
||||
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
AI_MODEL=qwen-turbo
|
||||
```
|
||||
|
||||
#### 2. 生产环境配置 (.env.production)
|
||||
|
||||
```bash
|
||||
# 阿里云 DashScope 配置(生产环境使用增强版)
|
||||
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
AI_MODEL=qwen-plus
|
||||
```
|
||||
|
||||
### 阿里云模型对比
|
||||
|
||||
| 模型 | 速度 | 质量 | 成本 | 适用场景 |
|
||||
|------|------|------|------|----------|
|
||||
| qwen-turbo | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 日常对话、简单分析 |
|
||||
| qwen-plus | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 复杂分析、专业任务 |
|
||||
| qwen-max | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 高质量输出、关键任务 |
|
||||
| qwen-long | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 长文本处理、文档分析 |
|
||||
|
||||
### 代码中使用
|
||||
|
||||
```javascript
|
||||
// 使用默认配置(从环境变量读取)
|
||||
const AIService = require('./services/ai_service.js');
|
||||
const aiService = AIService.getInstance();
|
||||
|
||||
// 使用自定义配置
|
||||
const aiService = AIService.createInstance({
|
||||
apiKey: 'sk-custom-key',
|
||||
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
model: 'qwen-plus',
|
||||
timeout: 60000
|
||||
});
|
||||
```
|
||||
|
||||
### API 认证格式
|
||||
|
||||
阿里云 DashScope API 使用标准的 Bearer Token 认证:
|
||||
|
||||
```
|
||||
Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **API Key 安全**:
|
||||
- 不要将 API Key 硬编码在代码中
|
||||
- 不要将 `.env` 文件提交到版本控制
|
||||
- 生产环境使用独立的 API Key
|
||||
|
||||
2. **模型选择建议**:
|
||||
- 开发/测试:使用 `qwen-turbo`(成本低)
|
||||
- 生产环境:使用 `qwen-plus`(性能平衡)
|
||||
- 关键业务:使用 `qwen-max`(质量最高)
|
||||
|
||||
3. **速率限制**:
|
||||
- 注意 API 的 QPM(每分钟请求数)限制
|
||||
- 根据套餐调整并发数量
|
||||
- 实现重试和错误处理机制
|
||||
|
||||
4. **成本控制**:
|
||||
- 监控 Token 使用量
|
||||
- 设置合理的 `max_tokens` 限制
|
||||
- 定期查看账单和用量统计
|
||||
|
||||
### 获取 API Key
|
||||
|
||||
1. 访问阿里云控制台:https://dashscope.console.aliyun.com/
|
||||
2. 进入 API-KEY 管理
|
||||
3. 创建新的 API Key
|
||||
4. 复制 API Key 并保存到环境变量
|
||||
|
||||
### 故障排查
|
||||
|
||||
#### 问题 1:Authentication Fails
|
||||
```
|
||||
错误:auth header format should be Bearer sk-...
|
||||
解决:检查 AI_API_KEY 是否正确配置
|
||||
```
|
||||
|
||||
#### 问题 2:连接超时
|
||||
```
|
||||
错误:timeout of 30000ms exceeded
|
||||
解决:
|
||||
1. 检查网络连接
|
||||
2. 增加 timeout 配置
|
||||
3. 检查 AI_BASE_URL 是否正确
|
||||
```
|
||||
|
||||
#### 问题 3:模型不存在
|
||||
```
|
||||
错误:model not found
|
||||
解决:检查 AI_MODEL 配置,确保使用支持的模型名称
|
||||
```
|
||||
|
||||
### 迁移指南
|
||||
|
||||
如果之前使用其他 AI 服务(如 DeepSeek),迁移步骤:
|
||||
|
||||
1. 更新环境变量配置
|
||||
2. 修改 API_BASE_URL
|
||||
3. 更新模型名称
|
||||
4. 测试 AI 调用功能
|
||||
5. 验证响应格式
|
||||
|
||||
---
|
||||
|
||||
**配置更新时间**: 2025-12-27
|
||||
**维护者**: 系统管理员
|
||||
364
docs/ai_service_unified.md
Normal file
364
docs/ai_service_unified.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# AI 服务统一说明
|
||||
|
||||
**更新时间**: 2025-12-27
|
||||
|
||||
---
|
||||
|
||||
## 统一后的 AI 服务架构
|
||||
|
||||
系统已完成 AI 服务的统一整理,现在只保留一个标准的 AI 服务实现。
|
||||
|
||||
---
|
||||
|
||||
## 文件位置
|
||||
|
||||
### ✅ 保留的文件(唯一 AI 服务实现)
|
||||
|
||||
**核心服务:**
|
||||
- **`api/services/ai_service.js`** - AI 服务主文件(基于阿里云 Qwen 2.5)
|
||||
- **`api/services/ai_call_recorder.js`** - AI 调用记录服务
|
||||
|
||||
**导出管理:**
|
||||
- **`api/services/index.js`** - 服务统一导出
|
||||
|
||||
**数据库层:**
|
||||
- **`api/model/ai_call_records.js`** - AI 调用记录模型
|
||||
|
||||
**后台管理:**
|
||||
- **`api/controller_admin/ai_call_records.js`** - 后台管理 API
|
||||
|
||||
**前端界面:**
|
||||
- **`admin/src/views/system/ai_call_records.vue`** - 管理界面
|
||||
- **`admin/src/api/system/ai_call_records_server.js`** - API 服务
|
||||
|
||||
### ❌ 已删除的文件
|
||||
|
||||
- ~~`api/middleware/job/aiService.js`~~ - 已删除(内容已迁移到 `services/ai_service.js`)
|
||||
|
||||
---
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 1. 直接引用(推荐)
|
||||
|
||||
```javascript
|
||||
const aiService = require('./services/ai_service');
|
||||
|
||||
// 使用 AI 服务
|
||||
const result = await aiService.analyzeJob(jobInfo, resumeInfo);
|
||||
```
|
||||
|
||||
### 2. 通过服务管理器
|
||||
|
||||
```javascript
|
||||
const { AIService } = require('./services');
|
||||
|
||||
// 使用 AI 服务
|
||||
const result = await AIService.analyzeJob(jobInfo, resumeInfo);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI 服务功能列表
|
||||
|
||||
### 核心方法
|
||||
|
||||
| 方法 | 说明 | 业务类型 |
|
||||
|------|------|---------|
|
||||
| `callAPI(prompt, options)` | 基础 API 调用 | 自定义 |
|
||||
| `analyzeJob(jobInfo, resumeInfo)` | 岗位智能筛选 | `job_analysis` |
|
||||
| `generateChatContent(jobInfo, resumeInfo, chatType)` | 生成个性化聊天 | `chat_generation` |
|
||||
| `analyzeResume(resumeText)` | 简历分析 | `resume_analysis` |
|
||||
| `generateInterviewInvitation(jobInfo, chatHistory)` | 生成面试邀约 | `interview_invitation` |
|
||||
| `identifyOutsourcingJob(jobInfo)` | 识别外包岗位 | `outsourcing_detection` |
|
||||
|
||||
### 辅助方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `recordAiCall(params)` | 记录 AI 调用 |
|
||||
| `calculateCost(totalTokens)` | 计算调用费用 |
|
||||
|
||||
---
|
||||
|
||||
## Token 自动记录
|
||||
|
||||
所有通过 `callAPI()` 方法的调用都会自动记录以下信息:
|
||||
|
||||
- **Token 使用量**:prompt_tokens, completion_tokens, total_tokens
|
||||
- **成本信息**:基于模型计算的费用
|
||||
- **性能指标**:响应时间(毫秒)
|
||||
- **状态跟踪**:成功/失败状态
|
||||
- **业务关联**:business_type, reference_id
|
||||
- **请求追踪**:完整的请求和响应内容
|
||||
|
||||
记录过程是异步非阻塞的,不会影响 AI 调用的主流程。
|
||||
|
||||
---
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
在 `.env` 文件中配置:
|
||||
|
||||
```bash
|
||||
# 阿里云 DashScope API Key
|
||||
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# 模型选择(可选)
|
||||
AI_MODEL=qwen-turbo # qwen-turbo, qwen-plus, qwen-max, qwen-long
|
||||
```
|
||||
|
||||
### 代码配置
|
||||
|
||||
```javascript
|
||||
// 在 config/config.js 中
|
||||
module.exports = {
|
||||
ai: {
|
||||
apiKey: process.env.AI_API_KEY,
|
||||
model: process.env.AI_MODEL || 'qwen-turbo'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 模型选择
|
||||
|
||||
| 模型 | 速度 | 质量 | 成本 | 价格(元/1000 tokens)|
|
||||
|------|------|------|------|---------------------|
|
||||
| qwen-turbo | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ¥0.003 |
|
||||
| qwen-plus | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ¥0.004 |
|
||||
| qwen-max | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ¥0.12 |
|
||||
| qwen-long | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | - |
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1:岗位分析
|
||||
|
||||
```javascript
|
||||
const aiService = require('./services/ai_service');
|
||||
|
||||
const jobInfo = {
|
||||
companyName: '阿里巴巴',
|
||||
jobTitle: 'Node.js 高级工程师',
|
||||
salary: '30-50K',
|
||||
location: '杭州',
|
||||
description: '负责后端服务开发...',
|
||||
skills: 'Node.js, MySQL, Redis'
|
||||
};
|
||||
|
||||
const resumeInfo = {
|
||||
skills: 'Node.js, JavaScript, MySQL',
|
||||
experience: '5年后端开发经验',
|
||||
education: '本科',
|
||||
expectedSalary: '35K'
|
||||
};
|
||||
|
||||
const result = await aiService.analyzeJob(jobInfo, resumeInfo);
|
||||
console.log(result.analysis);
|
||||
```
|
||||
|
||||
### 示例 2:生成聊天内容
|
||||
|
||||
```javascript
|
||||
const result = await aiService.generateChatContent(
|
||||
jobInfo,
|
||||
resumeInfo,
|
||||
'greeting' // greeting, interview, followup
|
||||
);
|
||||
|
||||
console.log(result.content);
|
||||
```
|
||||
|
||||
### 示例 3:简历分析
|
||||
|
||||
```javascript
|
||||
const resumeText = `
|
||||
姓名:张三
|
||||
技能:Node.js, React, MySQL
|
||||
工作经验:3年全栈开发
|
||||
...
|
||||
`;
|
||||
|
||||
const result = await aiService.analyzeResume(resumeText);
|
||||
console.log(result.analysis);
|
||||
// {
|
||||
// skillTags: ['Node.js', 'React', 'MySQL'],
|
||||
// strengths: '...',
|
||||
// weaknesses: '...',
|
||||
// careerSuggestion: '...',
|
||||
// competitiveness: 75
|
||||
// }
|
||||
```
|
||||
|
||||
### 示例 4:自定义 AI 调用
|
||||
|
||||
```javascript
|
||||
const result = await aiService.callAPI(
|
||||
'请帮我分析这个岗位的发展前景...',
|
||||
{
|
||||
systemPrompt: '你是一个职业规划专家...',
|
||||
temperature: 0.7,
|
||||
maxTokens: 2000,
|
||||
user_id: 123,
|
||||
business_type: 'career_analysis'
|
||||
}
|
||||
);
|
||||
|
||||
console.log(result.content);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
AI 服务内置了重试机制(最多 3 次)和错误处理:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const result = await aiService.analyzeJob(jobInfo, resumeInfo);
|
||||
} catch (error) {
|
||||
console.error('AI 调用失败:', error.message);
|
||||
// 错误会自动记录到 ai_call_records 表
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 监控与统计
|
||||
|
||||
### 查看调用记录
|
||||
|
||||
登录后台管理系统:**系统设置** → **AI调用记录**
|
||||
|
||||
### 统计信息
|
||||
|
||||
- 总调用次数
|
||||
- Token 总使用量
|
||||
- 总费用统计
|
||||
- 平均响应时间
|
||||
- 成功率
|
||||
|
||||
### 编程方式获取统计
|
||||
|
||||
```javascript
|
||||
const AiCallRecorder = require('./services/ai_call_recorder');
|
||||
|
||||
// 获取用户统计
|
||||
const userStats = await AiCallRecorder.getUserTokenStats(userId, {
|
||||
startDate: new Date('2025-01-01'),
|
||||
endDate: new Date('2025-12-31')
|
||||
});
|
||||
|
||||
console.log(userStats);
|
||||
// {
|
||||
// total_calls: 100,
|
||||
// total_prompt_tokens: 5000,
|
||||
// total_completion_tokens: 3000,
|
||||
// total_tokens: 8000,
|
||||
// total_cost: 24.00,
|
||||
// avg_response_time: 1500
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. API Key 安全
|
||||
|
||||
- ❌ 不要将 API Key 硬编码在代码中
|
||||
- ❌ 不要将 `.env` 文件提交到版本控制
|
||||
- ✅ 使用环境变量管理 API Key
|
||||
- ✅ 生产环境使用独立的 API Key
|
||||
|
||||
### 2. 成本控制
|
||||
|
||||
- 选择合适的模型(开发用 turbo,生产用 plus)
|
||||
- 设置合理的 `maxTokens` 限制
|
||||
- 监控 Token 使用量
|
||||
- 定期查看费用统计
|
||||
|
||||
### 3. 性能优化
|
||||
|
||||
- 重试机制已内置(3 次)
|
||||
- 超时设置为 30 秒
|
||||
- Token 记录是异步的,不阻塞主流程
|
||||
|
||||
### 4. 数据隐私
|
||||
|
||||
- 请求和响应内容会完整记录到数据库
|
||||
- 注意敏感信息的处理
|
||||
- 定期清理历史记录
|
||||
|
||||
---
|
||||
|
||||
## 迁移指南
|
||||
|
||||
如果你的代码之前引用了 `middleware/job/aiService.js`,请修改为:
|
||||
|
||||
```javascript
|
||||
// ❌ 旧代码
|
||||
const aiService = require('../middleware/job/aiService');
|
||||
|
||||
// ✅ 新代码
|
||||
const aiService = require('../services/ai_service');
|
||||
```
|
||||
|
||||
功能保持完全一致,只是路径发生了变化。
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题 1:模型未加载
|
||||
|
||||
**错误信息:** `Cannot read property 'findAll' of undefined`
|
||||
|
||||
**解决方法:**
|
||||
1. 确认已执行建表 SQL:`_sql/create_ai_call_records_table.sql`
|
||||
2. 重启 Node.js 服务
|
||||
3. 检查 `api/model/ai_call_records.js` 是否存在
|
||||
|
||||
### 问题 2:认证失败
|
||||
|
||||
**错误信息:** `auth header format should be Bearer sk-...`
|
||||
|
||||
**解决方法:**
|
||||
1. 检查 `.env` 文件中的 `AI_API_KEY`
|
||||
2. 确认 API Key 格式正确(以 `sk-` 开头)
|
||||
3. 验证 API Key 有效性
|
||||
|
||||
### 问题 3:记录失败
|
||||
|
||||
**警告信息:** `记录AI调用失败(不影响主流程)`
|
||||
|
||||
**解决方法:**
|
||||
1. 检查数据库连接
|
||||
2. 确认 `ai_call_records` 表存在
|
||||
3. 查看详细错误日志
|
||||
|
||||
### 问题 4:费用计算不准确
|
||||
|
||||
**解决方法:**
|
||||
1. 检查 `calculateCost()` 方法中的价格配置
|
||||
2. 根据实际使用的模型调整价格
|
||||
3. 定期对账单进行核对
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [AI 服务配置说明](ai_service_config.md) - 详细的环境配置指南
|
||||
- [功能实施总结](implementation_summary.md) - 完整的功能实施文档
|
||||
- [API 文档](../api/controller_admin/ai_call_records.js) - 后台 API 接口说明
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2025-12-27
|
||||
**维护者**: 开发团队
|
||||
561
docs/implementation_summary.md
Normal file
561
docs/implementation_summary.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# 自动找工作系统 - 功能实施总结
|
||||
|
||||
**更新时间**: 2025-12-27
|
||||
|
||||
---
|
||||
|
||||
## 已完成功能概览
|
||||
|
||||
本文档总结了最近完成的两个主要功能模块:
|
||||
|
||||
1. **价格套餐管理系统**
|
||||
2. **AI调用记录与Token追踪系统**
|
||||
|
||||
---
|
||||
|
||||
## 一、价格套餐管理系统
|
||||
|
||||
### 功能描述
|
||||
|
||||
将原本硬编码在 `api/controller_front/config.js` 中的价格套餐数据迁移到数据库,并提供完整的后台管理界面。
|
||||
|
||||
### 实施文件清单
|
||||
|
||||
#### 数据库层
|
||||
- ✅ `_sql/create_pricing_plans_table.sql` - 数据表创建脚本
|
||||
- ✅ `_sql/insert_pricing_plans_data.sql` - 初始数据插入脚本
|
||||
- ✅ `api/model/pricing_plans.js` - Sequelize 数据模型
|
||||
|
||||
#### 后端API层
|
||||
- ✅ `api/controller_admin/pricing_plans.js` - 后台管理API(5个端点)
|
||||
- `POST /pricing_plans/list` - 分页查询
|
||||
- `GET /pricing_plans/detail` - 获取详情
|
||||
- `POST /pricing_plans/create` - 创建套餐
|
||||
- `POST /pricing_plans/update` - 更新套餐
|
||||
- `POST /pricing_plans/delete` - 删除套餐(软删除)
|
||||
- ✅ `api/controller_front/config.js` (修改第90-136行) - 前端API改为数据库查询
|
||||
|
||||
#### 前端层
|
||||
- ✅ `admin/src/api/system/pricing_plans_server.js` - API服务层
|
||||
- ✅ `admin/src/views/system/pricing_plans.vue` - 管理界面组件
|
||||
- ✅ `admin/src/router/component-map.js` (新增映射) - 组件注册
|
||||
|
||||
### 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `pricing_plans` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '套餐名称',
|
||||
`duration` VARCHAR(50) NOT NULL COMMENT '时长描述',
|
||||
`days` INT(11) NOT NULL COMMENT '天数(-1表示永久)',
|
||||
`price` DECIMAL(10,2) NOT NULL COMMENT '价格',
|
||||
`original_price` DECIMAL(10,2) NULL COMMENT '原价',
|
||||
`unit` VARCHAR(20) NOT NULL DEFAULT '元',
|
||||
`discount` VARCHAR(50) NULL COMMENT '折扣描述',
|
||||
`features` TEXT NOT NULL COMMENT '功能列表(JSON格式)',
|
||||
`featured` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否推荐',
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT '排序顺序',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`is_delete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### 初始数据
|
||||
|
||||
系统预置了4个价格套餐:
|
||||
|
||||
1. **体验套餐** - 7天 - ¥28
|
||||
2. **月度套餐** - 30天 - ¥99(推荐)
|
||||
3. **季度套餐** - 90天 - ¥269
|
||||
4. **终生套餐** - 永久 - ¥888
|
||||
|
||||
### 菜单位置
|
||||
|
||||
**用户管理** → **价格套餐管理**
|
||||
|
||||
### 关键实现细节
|
||||
|
||||
#### 前端接口向后兼容
|
||||
|
||||
`GET /api/config/pricing-plans` 接口保持原有响应格式:
|
||||
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "体验套餐",
|
||||
"duration": "7天",
|
||||
"days": 7,
|
||||
"price": 28,
|
||||
"originalPrice": 28,
|
||||
"unit": "元",
|
||||
"features": ["7天使用权限", "全功能体验", "技术支持"],
|
||||
"featured": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 状态控制逻辑
|
||||
|
||||
- 前端API仅返回 `is_active = 1` 且 `is_delete = 0` 的套餐
|
||||
- 按照 `sort_order` ASC, `id` ASC 排序
|
||||
- Features字段从JSON字符串自动解析为数组
|
||||
|
||||
#### 表单组件修复
|
||||
|
||||
修复了单选按钮组件使用方式(重要):
|
||||
|
||||
```javascript
|
||||
// ❌ 错误写法
|
||||
{
|
||||
title: '是否推荐',
|
||||
key: 'featured',
|
||||
type: 'radio', // 错误
|
||||
options: [...]
|
||||
}
|
||||
|
||||
// ✅ 正确写法
|
||||
{
|
||||
title: '是否推荐',
|
||||
key: 'featured',
|
||||
com: 'Radio', // 正确
|
||||
options: [
|
||||
{ value: 1, label: '推荐' },
|
||||
{ value: 0, label: '普通' }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、AI调用记录与Token追踪系统
|
||||
|
||||
### 功能描述
|
||||
|
||||
为所有AI调用添加自动记录功能,追踪Token使用量、调用成本、响应时间等关键指标,并提供后台管理和统计分析界面。
|
||||
|
||||
### 实施文件清单
|
||||
|
||||
#### 数据库层
|
||||
- ✅ `_sql/create_ai_call_records_table.sql` - 数据表创建脚本
|
||||
- ✅ `api/model/ai_call_records.js` - Sequelize 数据模型
|
||||
|
||||
#### 服务层
|
||||
- ✅ `api/services/ai_call_recorder.js` - AI调用记录服务
|
||||
- ✅ `api/services/ai_service.js` - AI服务(集成Token记录)
|
||||
|
||||
#### 后端API层
|
||||
- ✅ `api/controller_admin/ai_call_records.js` - 后台管理API(5个端点)
|
||||
- `POST /ai_call_records/list` - 分页查询
|
||||
- `GET /ai_call_records/detail` - 获取详情
|
||||
- `GET /ai_call_records/stats` - 统计分析
|
||||
- `POST /ai_call_records/delete` - 删除记录
|
||||
- `POST /ai_call_records/batch_delete` - 批量删除
|
||||
|
||||
#### 前端层
|
||||
- ✅ `admin/src/api/system/ai_call_records_server.js` - API服务层
|
||||
- ✅ `admin/src/views/system/ai_call_records.vue` - 管理界面组件
|
||||
- ✅ `admin/src/router/component-map.js` (新增映射) - 组件注册
|
||||
|
||||
### 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `ai_call_records` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT(11) NULL COMMENT '用户ID',
|
||||
`sn_code` VARCHAR(100) NULL COMMENT '设备SN码',
|
||||
`service_type` VARCHAR(50) NOT NULL COMMENT '服务类型:chat/completion/embedding',
|
||||
`model_name` VARCHAR(100) NOT NULL COMMENT '模型名称',
|
||||
`prompt_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '输入Token数',
|
||||
`completion_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '输出Token数',
|
||||
`total_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '总Token数',
|
||||
`request_content` TEXT NULL COMMENT '请求内容',
|
||||
`response_content` TEXT NULL COMMENT '响应内容',
|
||||
`cost_amount` DECIMAL(10,4) NULL COMMENT '费用(元)',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'success' COMMENT '状态:success/failed',
|
||||
`error_message` TEXT NULL COMMENT '错误信息',
|
||||
`response_time` INT(11) NULL COMMENT '响应时间(毫秒)',
|
||||
`api_provider` VARCHAR(50) NULL DEFAULT 'qwen' COMMENT 'API提供商',
|
||||
`business_type` VARCHAR(50) NULL COMMENT '业务类型',
|
||||
`reference_id` VARCHAR(100) NULL COMMENT '关联业务ID',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`is_delete` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### AI服务集成
|
||||
|
||||
#### 使用的AI服务
|
||||
|
||||
系统使用 **阿里云DashScope API (Qwen 2.5)**,而非DeepSeek。
|
||||
|
||||
**关键配置:**
|
||||
|
||||
```javascript
|
||||
// api/services/ai_service.js
|
||||
class aiService {
|
||||
constructor() {
|
||||
this.apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY;
|
||||
this.apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
|
||||
this.model = config.ai?.model || 'qwen-turbo';
|
||||
this.maxRetries = 3;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 环境变量配置
|
||||
|
||||
在 `.env` 文件中配置:
|
||||
|
||||
```bash
|
||||
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
|
||||
AI_MODEL=qwen-turbo # 可选:qwen-turbo, qwen-plus, qwen-max, qwen-long
|
||||
```
|
||||
|
||||
详细配置说明见:[docs/ai_service_config.md](ai_service_config.md:1)
|
||||
|
||||
#### Token记录集成
|
||||
|
||||
AI服务的 `callAPI()` 方法自动记录所有调用:
|
||||
|
||||
```javascript
|
||||
async callAPI(prompt, options = {}) {
|
||||
const startTime = Date.now();
|
||||
// ... 调用API ...
|
||||
|
||||
// 成功时自动记录Token使用量
|
||||
this.recordAiCall({
|
||||
user_id: options.user_id,
|
||||
sn_code: options.sn_code,
|
||||
service_type: options.service_type || 'completion',
|
||||
model_name: this.model,
|
||||
prompt_tokens: usage.prompt_tokens || 0,
|
||||
completion_tokens: usage.completion_tokens || 0,
|
||||
total_tokens: usage.total_tokens || 0,
|
||||
cost_amount: this.calculateCost(usage.total_tokens || 0),
|
||||
status: 'success',
|
||||
response_time: responseTime,
|
||||
api_provider: 'qwen',
|
||||
business_type: options.business_type,
|
||||
reference_id: options.reference_id
|
||||
}).catch(err => {
|
||||
console.warn('记录AI调用失败(不影响主流程):', err.message);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务类型分类
|
||||
|
||||
系统中的AI调用按业务类型分类:
|
||||
|
||||
| 业务类型 | 说明 | 调用方法 |
|
||||
|---------|------|---------|
|
||||
| `job_analysis` | 岗位分析 | `analyzeJob()` |
|
||||
| `chat_generation` | 聊天内容生成 | `generateChatContent()` |
|
||||
| `resume_analysis` | 简历分析 | `analyzeResume()` |
|
||||
| `interview_invitation` | 面试邀约 | `generateInterviewInvitation()` |
|
||||
| `outsourcing_detection` | 外包检测 | `identifyOutsourcingJob()` |
|
||||
|
||||
#### 成本计算
|
||||
|
||||
基于Qwen模型定价:
|
||||
|
||||
```javascript
|
||||
calculateCost(totalTokens) {
|
||||
// qwen-turbo: ¥0.003/1000 tokens
|
||||
// qwen-plus: ¥0.004/1000 tokens
|
||||
// qwen-max: ¥0.12/1000 tokens
|
||||
const pricePerThousand = 0.003;
|
||||
return (totalTokens / 1000) * pricePerThousand;
|
||||
}
|
||||
```
|
||||
|
||||
### 管理界面功能
|
||||
|
||||
#### 筛选功能
|
||||
- 按用户ID搜索
|
||||
- 按设备SN码搜索
|
||||
- 按业务类型筛选
|
||||
- 按服务类型筛选
|
||||
- 按状态筛选
|
||||
- 按时间范围筛选
|
||||
|
||||
#### 统计功能
|
||||
- 总调用次数
|
||||
- Token总使用量(输入/输出/总计)
|
||||
- 总费用统计
|
||||
- 平均响应时间
|
||||
- 成功率统计
|
||||
|
||||
#### 操作功能
|
||||
- 查看详情(完整请求/响应内容)
|
||||
- 单条删除
|
||||
- 批量删除
|
||||
|
||||
### 菜单位置
|
||||
|
||||
**系统设置** → **AI调用记录**
|
||||
|
||||
---
|
||||
|
||||
## 三、关键技术点总结
|
||||
|
||||
### 1. 组件注册规范
|
||||
|
||||
前端Vue组件必须在 `admin/src/router/component-map.js` 中注册:
|
||||
|
||||
```javascript
|
||||
// 导入组件
|
||||
import PricingPlans from '@/views/system/pricing_plans.vue'
|
||||
import AiCallRecords from '@/views/system/ai_call_records.vue'
|
||||
|
||||
// 注册映射
|
||||
const componentMap = {
|
||||
'system/pricing_plans': PricingPlans,
|
||||
'system/ai_call_records': AiCallRecords,
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 表单控件使用规范
|
||||
|
||||
使用 `com` 字段而非 `type` 字段:
|
||||
|
||||
```javascript
|
||||
// 单选按钮
|
||||
{ com: 'Radio', options: [...] }
|
||||
|
||||
// 文本输入框
|
||||
{ com: 'Input' }
|
||||
|
||||
// 文本域
|
||||
{ com: 'TextArea' }
|
||||
|
||||
// 数字输入框
|
||||
{ com: 'InputNumber' }
|
||||
|
||||
// 下拉选择
|
||||
{ com: 'Select', options: [...] }
|
||||
```
|
||||
|
||||
### 3. 数据库软删除模式
|
||||
|
||||
所有查询必须包含 `is_delete = 0` 条件:
|
||||
|
||||
```javascript
|
||||
const list = await model.findAll({
|
||||
where: {
|
||||
is_delete: 0,
|
||||
// ... 其他条件
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 时间戳管理
|
||||
|
||||
使用手动管理而非Sequelize自动管理:
|
||||
|
||||
```javascript
|
||||
// 模型定义
|
||||
{
|
||||
timestamps: false, // 禁用自动时间戳
|
||||
// 手动定义时间字段
|
||||
create_time: { type: DataTypes.DATE },
|
||||
last_modify_time: { type: DataTypes.DATE }
|
||||
}
|
||||
|
||||
// 创建时手动设置
|
||||
const now = new Date();
|
||||
await model.create({
|
||||
// ... 其他字段
|
||||
create_time: now,
|
||||
last_modify_time: now
|
||||
});
|
||||
```
|
||||
|
||||
### 5. JSON字段处理
|
||||
|
||||
数据库存储为TEXT类型,应用层处理JSON序列化:
|
||||
|
||||
```javascript
|
||||
// 保存时
|
||||
const data = {
|
||||
features: JSON.stringify(['功能1', '功能2'])
|
||||
};
|
||||
|
||||
// 读取时
|
||||
const features = JSON.parse(record.features || '[]');
|
||||
```
|
||||
|
||||
### 6. 异步记录模式
|
||||
|
||||
日志/记录类操作使用异步非阻塞模式:
|
||||
|
||||
```javascript
|
||||
// 使用 .catch() 而非 try-catch,避免阻塞主流程
|
||||
this.recordAiCall(params).catch(err => {
|
||||
console.warn('记录失败(不影响主流程):', err.message);
|
||||
});
|
||||
|
||||
// 主流程继续执行
|
||||
return result;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、部署检查清单
|
||||
|
||||
### 数据库层
|
||||
- [ ] 执行 `create_pricing_plans_table.sql`
|
||||
- [ ] 执行 `insert_pricing_plans_data.sql`
|
||||
- [ ] 执行 `create_ai_call_records_table.sql`
|
||||
- [ ] 验证表创建成功
|
||||
|
||||
### 后端层
|
||||
- [ ] 重启Node.js服务以加载新模型
|
||||
- [ ] 验证模型加载:`Framework.getModels()`
|
||||
- [ ] 配置环境变量 `AI_API_KEY` 和 `AI_MODEL`
|
||||
- [ ] 测试后台API端点
|
||||
|
||||
### 前端层
|
||||
- [ ] 重新编译前端代码
|
||||
- [ ] 验证组件注册成功
|
||||
- [ ] 刷新浏览器缓存
|
||||
- [ ] 测试管理界面功能
|
||||
|
||||
### 菜单系统
|
||||
- [ ] 验证"价格套餐管理"菜单显示
|
||||
- [ ] 验证"AI调用记录"菜单显示
|
||||
- [ ] 测试菜单跳转功能
|
||||
|
||||
### 功能测试
|
||||
- [ ] 价格套餐CRUD操作
|
||||
- [ ] 前端API `/config/pricing-plans` 返回数据库数据
|
||||
- [ ] AI调用自动记录Token
|
||||
- [ ] AI调用记录管理界面
|
||||
- [ ] 统计功能准确性
|
||||
|
||||
---
|
||||
|
||||
## 五、已知问题与注意事项
|
||||
|
||||
### 1. 组件热更新
|
||||
|
||||
修改组件映射后需要:
|
||||
- 重启前端开发服务器
|
||||
- 清除浏览器缓存
|
||||
- 刷新页面
|
||||
|
||||
### 2. AI服务配置
|
||||
|
||||
**重要**:系统使用阿里云DashScope API,不是DeepSeek。
|
||||
|
||||
必须配置正确的环境变量:
|
||||
```bash
|
||||
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx # 阿里云API Key
|
||||
AI_MODEL=qwen-turbo # Qwen模型
|
||||
```
|
||||
|
||||
### 3. Token记录异步
|
||||
|
||||
Token记录失败不会影响AI调用主流程,但会打印警告日志。如果需要确保记录成功,应检查数据库连接和表结构。
|
||||
|
||||
### 4. 成本计算
|
||||
|
||||
当前成本计算使用固定价格(qwen-turbo: ¥0.003/1000 tokens),如果切换到其他模型,需要修改 `calculateCost()` 方法。
|
||||
|
||||
### 5. 数据备份
|
||||
|
||||
AI调用记录表会快速增长,建议:
|
||||
- 定期归档历史数据
|
||||
- 设置数据保留策略(如只保留最近90天)
|
||||
- 建立定期备份机制
|
||||
|
||||
---
|
||||
|
||||
## 六、文件路径索引
|
||||
|
||||
### 价格套餐系统
|
||||
|
||||
**数据库**
|
||||
- `_sql/create_pricing_plans_table.sql`
|
||||
- `_sql/insert_pricing_plans_data.sql`
|
||||
|
||||
**后端**
|
||||
- `api/model/pricing_plans.js`
|
||||
- `api/controller_admin/pricing_plans.js`
|
||||
- `api/controller_front/config.js` (修改第90-136行)
|
||||
|
||||
**前端**
|
||||
- `admin/src/api/system/pricing_plans_server.js`
|
||||
- `admin/src/views/system/pricing_plans.vue`
|
||||
|
||||
### AI调用记录系统
|
||||
|
||||
**数据库**
|
||||
- `_sql/create_ai_call_records_table.sql`
|
||||
|
||||
**后端**
|
||||
- `api/model/ai_call_records.js`
|
||||
- `api/services/ai_call_recorder.js`
|
||||
- `api/services/ai_service.js` (完全重写)
|
||||
- `api/controller_admin/ai_call_records.js`
|
||||
|
||||
**前端**
|
||||
- `admin/src/api/system/ai_call_records_server.js`
|
||||
- `admin/src/views/system/ai_call_records.vue`
|
||||
|
||||
### 公共配置
|
||||
|
||||
- `admin/src/router/component-map.js` (新增两个组件映射)
|
||||
- `config/config.js` (AI配置项)
|
||||
- `.env` (环境变量)
|
||||
|
||||
### 文档
|
||||
|
||||
- `docs/ai_service_config.md` (AI服务配置说明)
|
||||
- `docs/implementation_summary.md` (本文档)
|
||||
|
||||
---
|
||||
|
||||
## 七、维护建议
|
||||
|
||||
### 日常维护
|
||||
|
||||
1. **监控Token使用量**
|
||||
- 定期查看AI调用记录统计
|
||||
- 关注异常高额调用
|
||||
- 优化高频调用场景
|
||||
|
||||
2. **价格套餐调整**
|
||||
- 根据市场情况调整价格
|
||||
- 通过后台界面快速上下架套餐
|
||||
- 使用排序功能控制显示顺序
|
||||
|
||||
3. **数据清理**
|
||||
- 定期归档或删除历史AI调用记录
|
||||
- 清理已删除的套餐数据(物理删除)
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. **数据库索引**
|
||||
- AI调用记录表按需添加索引(user_id, sn_code, create_time)
|
||||
- 定期分析查询性能
|
||||
|
||||
2. **缓存策略**
|
||||
- 考虑对前端API `/config/pricing-plans` 添加缓存
|
||||
- 缓存有效期建议5-10分钟
|
||||
|
||||
3. **日志归档**
|
||||
- 建立AI调用记录的归档机制
|
||||
- 超过一定时间的数据转移到历史表
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2025-12-27
|
||||
**维护者**: 开发团队
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user