Compare commits
20 Commits
7ee92b8905
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2786202212 | ||
|
|
517a320627 | ||
|
|
eed08a30fb | ||
|
|
1d02702e96 | ||
|
|
547f1eaec2 | ||
|
|
43382668a3 | ||
|
|
6e38ba6b38 | ||
|
|
b17d08ffa8 | ||
|
|
54644dbb72 | ||
|
|
1d8d2ea6e8 | ||
|
|
3f4acc5e1d | ||
|
|
69f2f87f4b | ||
|
|
0cfff98edf | ||
|
|
77789446f3 | ||
|
|
2530f25b86 | ||
|
|
6efd77d2b5 | ||
|
|
6253abc617 | ||
|
|
933f1618ca | ||
|
|
c43bf79e2d | ||
|
|
55ef28440a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ node_modules.*
|
|||||||
dist.zip
|
dist.zip
|
||||||
dist/
|
dist/
|
||||||
admin/node_modules/
|
admin/node_modules/
|
||||||
|
app/
|
||||||
968
_doc/MQTT指令列表.md
Normal file
968
_doc/MQTT指令列表.md
Normal file
@@ -0,0 +1,968 @@
|
|||||||
|
# 自动找工作系统 - 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
|
||||||
874
_doc/功能规划文档.md
Normal file
874
_doc/功能规划文档.md
Normal file
@@ -0,0 +1,874 @@
|
|||||||
|
# 自动找工作系统 - 功能规划文档
|
||||||
|
|
||||||
|
> 版本: 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
|
||||||
1174
_doc/客户端待开发功能.md
Normal file
1174
_doc/客户端待开发功能.md
Normal file
File diff suppressed because it is too large
Load Diff
529
_doc/搜索列表和投递功能开发规划.md
Normal file
529
_doc/搜索列表和投递功能开发规划.md
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
# Boss直聘搜索列表和投递功能开发规划
|
||||||
|
|
||||||
|
## 📋 功能概述
|
||||||
|
|
||||||
|
基于Boss直聘Web端职位搜索页面(`https://www.zhipin.com/web/geek/jobs`),完善搜索列表获取和职位投递功能,包括服务端任务创建、指令生成和完整流程实现。
|
||||||
|
|
||||||
|
## 🎯 目标功能
|
||||||
|
|
||||||
|
### 1. 搜索列表功能
|
||||||
|
- 支持多条件搜索(关键词、城市、薪资、经验、学历等)
|
||||||
|
- 支持分页获取职位列表
|
||||||
|
- 自动保存职位到数据库
|
||||||
|
- 支持职位去重和更新
|
||||||
|
|
||||||
|
### 2. 投递功能
|
||||||
|
- 单个职位投递
|
||||||
|
- 批量职位投递
|
||||||
|
- 投递状态跟踪
|
||||||
|
- 投递记录管理
|
||||||
|
|
||||||
|
## 📊 功能架构
|
||||||
|
|
||||||
|
```
|
||||||
|
用户/系统触发
|
||||||
|
↓
|
||||||
|
创建任务 (task_status)
|
||||||
|
↓
|
||||||
|
生成指令序列 (task_commands)
|
||||||
|
↓
|
||||||
|
执行指令 (通过MQTT发送到设备)
|
||||||
|
↓
|
||||||
|
设备执行并返回结果
|
||||||
|
↓
|
||||||
|
保存数据到数据库
|
||||||
|
↓
|
||||||
|
更新任务和指令状态
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
### 一、搜索列表功能完善
|
||||||
|
|
||||||
|
#### 1.1 指令参数扩展
|
||||||
|
|
||||||
|
**文件**: `api/middleware/job/jobManager.js`
|
||||||
|
|
||||||
|
**方法**: `get_job_list()`
|
||||||
|
|
||||||
|
**需要支持的参数**:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
keyword: '全栈工程师', // 搜索关键词
|
||||||
|
city: '101020100', // 城市代码(上海)
|
||||||
|
cityName: '上海', // 城市名称
|
||||||
|
salary: '20-30K', // 薪资范围
|
||||||
|
experience: '3-5年', // 工作经验
|
||||||
|
education: '本科', // 学历要求
|
||||||
|
industry: '互联网', // 公司行业
|
||||||
|
companySize: '100-499人', // 公司规模
|
||||||
|
financingStage: 'B轮', // 融资阶段
|
||||||
|
page: 1, // 页码
|
||||||
|
pageSize: 20, // 每页数量
|
||||||
|
pageCount: 3 // 获取页数(用于批量获取)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 任务创建接口
|
||||||
|
|
||||||
|
**文件**: `api/services/pla_account_service.js`
|
||||||
|
|
||||||
|
**新增方法**: `createSearchJobListTask()`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 创建搜索职位列表任务
|
||||||
|
* @param {Object} params - 任务参数
|
||||||
|
* @param {number} params.id - 账号ID
|
||||||
|
* @param {string} params.keyword - 搜索关键词
|
||||||
|
* @param {string} params.city - 城市代码
|
||||||
|
* @param {Object} params.searchParams - 搜索条件(薪资、经验、学历等)
|
||||||
|
* @param {number} params.pageCount - 获取页数
|
||||||
|
* @returns {Promise<Object>} 任务创建结果
|
||||||
|
*/
|
||||||
|
async createSearchJobListTask(params) {
|
||||||
|
// 1. 验证账号和授权
|
||||||
|
// 2. 创建任务记录
|
||||||
|
// 3. 生成搜索指令
|
||||||
|
// 4. 执行指令
|
||||||
|
// 5. 返回任务ID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 指令生成逻辑
|
||||||
|
|
||||||
|
**文件**: `api/middleware/schedule/taskHandlers.js`
|
||||||
|
|
||||||
|
**需要完善**: `handleAutoDeliverTask()` 中的搜索指令生成
|
||||||
|
|
||||||
|
**当前实现**:
|
||||||
|
```javascript
|
||||||
|
const getJobListCommand = {
|
||||||
|
command_type: 'getJobList',
|
||||||
|
command_name: '获取职位列表',
|
||||||
|
command_params: JSON.stringify({
|
||||||
|
sn_code: sn_code,
|
||||||
|
keyword: keyword || accountConfig.keyword || '',
|
||||||
|
platform: platform || 'boss',
|
||||||
|
pageCount: pageCount || 3
|
||||||
|
}),
|
||||||
|
priority: config.getTaskPriority('search_jobs') || 5
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**需要扩展为**:
|
||||||
|
```javascript
|
||||||
|
const getJobListCommand = {
|
||||||
|
command_type: 'get_job_list', // 统一使用下划线命名
|
||||||
|
command_name: '获取职位列表',
|
||||||
|
command_params: JSON.stringify({
|
||||||
|
sn_code: sn_code,
|
||||||
|
platform: platform || 'boss',
|
||||||
|
keyword: keyword || accountConfig.keyword || '',
|
||||||
|
city: city || accountConfig.city || '101020100', // 默认上海
|
||||||
|
cityName: cityName || accountConfig.cityName || '上海',
|
||||||
|
salary: searchParams?.salary || '',
|
||||||
|
experience: searchParams?.experience || '',
|
||||||
|
education: searchParams?.education || '',
|
||||||
|
industry: searchParams?.industry || '',
|
||||||
|
companySize: searchParams?.companySize || '',
|
||||||
|
financingStage: searchParams?.financingStage || '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
pageCount: pageCount || 3
|
||||||
|
}),
|
||||||
|
priority: config.getTaskPriority('get_job_list') || 5,
|
||||||
|
sequence: 1
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4 职位数据保存优化
|
||||||
|
|
||||||
|
**文件**: `api/middleware/job/jobManager.js`
|
||||||
|
|
||||||
|
**方法**: `saveJobsToDatabase()`
|
||||||
|
|
||||||
|
**需要完善**:
|
||||||
|
- 支持更多字段映射(从Boss直聘响应数据)
|
||||||
|
- 优化位置解析逻辑
|
||||||
|
- 支持职位状态更新(已投递、已查看等)
|
||||||
|
- 添加职位匹配度计算
|
||||||
|
|
||||||
|
### 二、投递功能完善
|
||||||
|
|
||||||
|
#### 2.1 单个职位投递
|
||||||
|
|
||||||
|
**文件**: `api/middleware/job/jobManager.js`
|
||||||
|
|
||||||
|
**方法**: `applyJob()`
|
||||||
|
|
||||||
|
**当前状态**: ✅ 已实现基础功能
|
||||||
|
|
||||||
|
**需要完善**:
|
||||||
|
- 支持更多投递参数(期望薪资、求职信等)
|
||||||
|
- 优化错误处理
|
||||||
|
- 添加投递前检查(是否已投递、是否满足条件等)
|
||||||
|
|
||||||
|
#### 2.2 批量职位投递任务
|
||||||
|
|
||||||
|
**文件**: `api/middleware/schedule/taskHandlers.js`
|
||||||
|
|
||||||
|
**方法**: `handleAutoDeliverTask()`
|
||||||
|
|
||||||
|
**当前状态**: ✅ 已实现基础功能
|
||||||
|
|
||||||
|
**需要完善**:
|
||||||
|
1. **搜索条件完善**
|
||||||
|
- 支持城市、薪资、经验、学历等筛选条件
|
||||||
|
- 从账号配置中读取默认搜索条件
|
||||||
|
- 支持任务参数覆盖账号配置
|
||||||
|
|
||||||
|
2. **职位匹配算法优化**
|
||||||
|
- 完善距离计算(基于经纬度)
|
||||||
|
- 完善薪资匹配(解析薪资范围字符串)
|
||||||
|
- 完善工作年限匹配
|
||||||
|
- 完善学历匹配
|
||||||
|
- 完善权重评分系统
|
||||||
|
|
||||||
|
3. **投递指令生成**
|
||||||
|
- 为每个匹配的职位生成投递指令
|
||||||
|
- 支持批量投递(一次任务投递多个职位)
|
||||||
|
- 添加投递间隔控制(避免频繁投递)
|
||||||
|
|
||||||
|
#### 2.3 投递任务创建接口
|
||||||
|
|
||||||
|
**文件**: `api/services/pla_account_service.js`
|
||||||
|
|
||||||
|
**新增方法**: `createDeliverTask()`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 创建投递任务
|
||||||
|
* @param {Object} params - 任务参数
|
||||||
|
* @param {number} params.id - 账号ID
|
||||||
|
* @param {string} params.keyword - 搜索关键词
|
||||||
|
* @param {Object} params.searchParams - 搜索条件
|
||||||
|
* @param {Object} params.filterRules - 过滤规则
|
||||||
|
* @param {number} params.maxCount - 最大投递数量
|
||||||
|
* @returns {Promise<Object>} 任务创建结果
|
||||||
|
*/
|
||||||
|
async createDeliverTask(params) {
|
||||||
|
// 1. 验证账号和授权
|
||||||
|
// 2. 创建任务记录
|
||||||
|
// 3. 生成搜索指令(获取职位列表)
|
||||||
|
// 4. 生成投递指令序列(根据匹配结果)
|
||||||
|
// 5. 执行任务
|
||||||
|
// 6. 返回任务ID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 具体开发任务
|
||||||
|
|
||||||
|
### 任务1: 完善搜索参数支持
|
||||||
|
|
||||||
|
**文件**: `api/middleware/job/jobManager.js`
|
||||||
|
|
||||||
|
**修改方法**: `get_job_list()`
|
||||||
|
|
||||||
|
**任务内容**:
|
||||||
|
1. 扩展参数支持(城市、薪资、经验、学历等)
|
||||||
|
2. 构建完整的搜索参数对象
|
||||||
|
3. 传递给MQTT指令
|
||||||
|
|
||||||
|
**代码位置**: 第153-206行
|
||||||
|
|
||||||
|
**预计工作量**: 2小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 任务2: 优化职位数据保存
|
||||||
|
|
||||||
|
**文件**: `api/middleware/job/jobManager.js`
|
||||||
|
|
||||||
|
**修改方法**: `saveJobsToDatabase()`
|
||||||
|
|
||||||
|
**任务内容**:
|
||||||
|
1. 完善字段映射(从Boss直聘响应数据提取更多字段)
|
||||||
|
2. 优化位置解析(减少API调用,添加缓存)
|
||||||
|
3. 添加职位状态管理
|
||||||
|
4. 添加职位匹配度字段
|
||||||
|
|
||||||
|
**代码位置**: 第215-308行
|
||||||
|
|
||||||
|
**预计工作量**: 3小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 任务3: 完善自动投递任务搜索条件
|
||||||
|
|
||||||
|
**文件**: `api/middleware/schedule/taskHandlers.js`
|
||||||
|
|
||||||
|
**修改方法**: `handleAutoDeliverTask()`
|
||||||
|
|
||||||
|
**任务内容**:
|
||||||
|
1. 从账号配置读取默认搜索条件
|
||||||
|
2. 支持任务参数覆盖
|
||||||
|
3. 构建完整的搜索参数
|
||||||
|
4. 传递给搜索指令
|
||||||
|
|
||||||
|
**代码位置**: 第220-233行
|
||||||
|
|
||||||
|
**预计工作量**: 2小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 任务4: 优化职位匹配算法
|
||||||
|
|
||||||
|
**文件**: `api/middleware/schedule/taskHandlers.js`
|
||||||
|
|
||||||
|
**修改方法**: `handleAutoDeliverTask()`
|
||||||
|
|
||||||
|
**任务内容**:
|
||||||
|
1. 完善距离计算(使用经纬度计算实际距离)
|
||||||
|
2. 完善薪资匹配(解析"20-30K"格式,转换为数值范围)
|
||||||
|
3. 完善工作年限匹配(解析"3-5年"格式)
|
||||||
|
4. 完善学历匹配(学历等级映射)
|
||||||
|
5. 优化权重评分计算
|
||||||
|
|
||||||
|
**代码位置**: 第255-400行(职位评分和过滤逻辑)
|
||||||
|
|
||||||
|
**预计工作量**: 4小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 任务5: 创建搜索任务接口
|
||||||
|
|
||||||
|
**文件**: `api/services/pla_account_service.js`
|
||||||
|
|
||||||
|
**新增方法**: `createSearchJobListTask()`
|
||||||
|
|
||||||
|
**任务内容**:
|
||||||
|
1. 验证账号和授权
|
||||||
|
2. 创建任务记录
|
||||||
|
3. 生成搜索指令
|
||||||
|
4. 执行指令
|
||||||
|
5. 返回任务信息
|
||||||
|
|
||||||
|
**代码位置**: 在 `runCommand()` 方法后添加
|
||||||
|
|
||||||
|
**预计工作量**: 3小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 任务6: 创建投递任务接口
|
||||||
|
|
||||||
|
**文件**: `api/services/pla_account_service.js`
|
||||||
|
|
||||||
|
**新增方法**: `createDeliverTask()`
|
||||||
|
|
||||||
|
**任务内容**:
|
||||||
|
1. 验证账号和授权
|
||||||
|
2. 创建任务记录
|
||||||
|
3. 生成搜索指令(获取职位列表)
|
||||||
|
4. 等待搜索完成
|
||||||
|
5. 获取匹配的职位
|
||||||
|
6. 生成投递指令序列
|
||||||
|
7. 执行投递指令
|
||||||
|
8. 返回任务信息
|
||||||
|
|
||||||
|
**代码位置**: 在 `createSearchJobListTask()` 方法后添加
|
||||||
|
|
||||||
|
**预计工作量**: 4小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 任务7: 完善指令类型映射
|
||||||
|
|
||||||
|
**文件**: `api/middleware/schedule/command.js`
|
||||||
|
|
||||||
|
**修改位置**: 指令执行逻辑
|
||||||
|
|
||||||
|
**任务内容**:
|
||||||
|
1. 确保 `get_job_list` 指令类型正确映射到 `jobManager.get_job_list()`
|
||||||
|
2. 确保 `search_jobs` 指令类型正确映射到 `jobManager.search_jobs()`
|
||||||
|
3. 确保 `apply_job` 指令类型正确映射到 `jobManager.applyJob()`
|
||||||
|
|
||||||
|
**代码位置**: 第150-250行(指令执行逻辑)
|
||||||
|
|
||||||
|
**预计工作量**: 1小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 任务8: 添加搜索条件配置管理
|
||||||
|
|
||||||
|
**文件**: `api/model/pla_account.js`
|
||||||
|
|
||||||
|
**任务内容**:
|
||||||
|
1. 添加搜索条件配置字段(如果不存在)
|
||||||
|
2. 支持在账号配置中保存默认搜索条件
|
||||||
|
3. 支持在任务参数中覆盖搜索条件
|
||||||
|
|
||||||
|
**相关字段**:
|
||||||
|
- `search_config` (JSON): 搜索条件配置
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"city": "101020100",
|
||||||
|
"cityName": "上海",
|
||||||
|
"defaultSalary": "20-30K",
|
||||||
|
"defaultExperience": "3-5年",
|
||||||
|
"defaultEducation": "本科"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**预计工作量**: 1小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 工作流程
|
||||||
|
|
||||||
|
### 搜索职位列表流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户/系统调用 createSearchJobListTask()
|
||||||
|
↓
|
||||||
|
2. 创建任务记录 (task_status)
|
||||||
|
↓
|
||||||
|
3. 生成搜索指令 (task_commands)
|
||||||
|
- command_type: 'get_job_list'
|
||||||
|
- command_params: { keyword, city, salary, ... }
|
||||||
|
↓
|
||||||
|
4. 执行指令 (通过MQTT发送到设备)
|
||||||
|
↓
|
||||||
|
5. 设备执行搜索并返回职位列表
|
||||||
|
↓
|
||||||
|
6. 保存职位到数据库 (job_postings)
|
||||||
|
- 去重处理
|
||||||
|
- 位置解析
|
||||||
|
- 字段映射
|
||||||
|
↓
|
||||||
|
7. 更新指令状态为完成
|
||||||
|
↓
|
||||||
|
8. 更新任务状态为完成
|
||||||
|
```
|
||||||
|
|
||||||
|
### 投递职位流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 用户/系统调用 createDeliverTask()
|
||||||
|
↓
|
||||||
|
2. 创建任务记录 (task_status)
|
||||||
|
↓
|
||||||
|
3. 生成搜索指令 (获取职位列表)
|
||||||
|
- command_type: 'get_job_list'
|
||||||
|
↓
|
||||||
|
4. 执行搜索指令
|
||||||
|
↓
|
||||||
|
5. 获取职位列表并保存到数据库
|
||||||
|
↓
|
||||||
|
6. 根据简历信息和过滤规则匹配职位
|
||||||
|
- 距离匹配
|
||||||
|
- 薪资匹配
|
||||||
|
- 工作年限匹配
|
||||||
|
- 学历匹配
|
||||||
|
- 权重评分
|
||||||
|
↓
|
||||||
|
7. 为每个匹配的职位生成投递指令
|
||||||
|
- command_type: 'apply_job'
|
||||||
|
- command_params: { jobId, encryptBossId, ... }
|
||||||
|
↓
|
||||||
|
8. 批量执行投递指令(带间隔控制)
|
||||||
|
↓
|
||||||
|
9. 保存投递记录 (apply_records)
|
||||||
|
↓
|
||||||
|
10. 更新职位状态 (job_postings.applyStatus)
|
||||||
|
↓
|
||||||
|
11. 更新任务状态为完成
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 数据库字段说明
|
||||||
|
|
||||||
|
### job_postings 表需要完善的字段
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 说明 | 状态 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `city` | VARCHAR | 城市代码 | 待添加 |
|
||||||
|
| `cityName` | VARCHAR | 城市名称 | 待添加 |
|
||||||
|
| `salaryMin` | INT | 最低薪资(元) | 待添加 |
|
||||||
|
| `salaryMax` | INT | 最高薪资(元) | 待添加 |
|
||||||
|
| `experienceMin` | INT | 最低工作年限 | 待添加 |
|
||||||
|
| `experienceMax` | INT | 最高工作年限 | 待添加 |
|
||||||
|
| `educationLevel` | VARCHAR | 学历等级 | 待添加 |
|
||||||
|
| `matchScore` | DECIMAL | 匹配度评分 | 待添加 |
|
||||||
|
|
||||||
|
### pla_account 表需要添加的字段
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 说明 | 状态 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `search_config` | JSON | 搜索条件配置 | 待添加 |
|
||||||
|
| `city` | VARCHAR | 默认城市代码 | 待添加 |
|
||||||
|
| `cityName` | VARCHAR | 默认城市名称 | 待添加 |
|
||||||
|
|
||||||
|
## 🧪 测试计划
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
1. 测试搜索参数构建
|
||||||
|
2. 测试职位数据保存
|
||||||
|
3. 测试职位匹配算法
|
||||||
|
4. 测试投递指令生成
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
1. 测试完整搜索流程
|
||||||
|
2. 测试完整投递流程
|
||||||
|
3. 测试任务创建和执行
|
||||||
|
4. 测试MQTT通信
|
||||||
|
|
||||||
|
### 功能测试
|
||||||
|
1. 测试多条件搜索
|
||||||
|
2. 测试分页获取
|
||||||
|
3. 测试批量投递
|
||||||
|
4. 测试错误处理
|
||||||
|
|
||||||
|
## 📅 开发时间估算
|
||||||
|
|
||||||
|
| 任务 | 预计时间 | 优先级 |
|
||||||
|
|------|----------|--------|
|
||||||
|
| 任务1: 完善搜索参数支持 | 2小时 | 高 |
|
||||||
|
| 任务2: 优化职位数据保存 | 3小时 | 高 |
|
||||||
|
| 任务3: 完善自动投递任务搜索条件 | 2小时 | 高 |
|
||||||
|
| 任务4: 优化职位匹配算法 | 4小时 | 高 |
|
||||||
|
| 任务5: 创建搜索任务接口 | 3小时 | 中 |
|
||||||
|
| 任务6: 创建投递任务接口 | 4小时 | 中 |
|
||||||
|
| 任务7: 完善指令类型映射 | 1小时 | 中 |
|
||||||
|
| 任务8: 添加搜索条件配置管理 | 1小时 | 低 |
|
||||||
|
|
||||||
|
**总计**: 约20小时
|
||||||
|
|
||||||
|
## 🚀 开发优先级
|
||||||
|
|
||||||
|
### 第一阶段(核心功能)
|
||||||
|
1. 任务1: 完善搜索参数支持
|
||||||
|
2. 任务2: 优化职位数据保存
|
||||||
|
3. 任务3: 完善自动投递任务搜索条件
|
||||||
|
4. 任务4: 优化职位匹配算法
|
||||||
|
|
||||||
|
### 第二阶段(接口完善)
|
||||||
|
5. 任务5: 创建搜索任务接口
|
||||||
|
6. 任务6: 创建投递任务接口
|
||||||
|
7. 任务7: 完善指令类型映射
|
||||||
|
|
||||||
|
### 第三阶段(配置管理)
|
||||||
|
8. 任务8: 添加搜索条件配置管理
|
||||||
|
|
||||||
|
## 📌 注意事项
|
||||||
|
|
||||||
|
1. **命名规范**: 统一使用下划线命名(`get_job_list` 而不是 `getJobList`)
|
||||||
|
2. **错误处理**: 所有方法都需要完善的错误处理和日志记录
|
||||||
|
3. **数据验证**: 所有输入参数都需要验证
|
||||||
|
4. **性能优化**: 批量操作需要考虑性能,避免阻塞
|
||||||
|
5. **MQTT通信**: 确保指令参数格式正确,与客户端协议一致
|
||||||
|
6. **数据库事务**: 批量操作需要使用事务保证数据一致性
|
||||||
|
|
||||||
|
## 🔗 相关文件
|
||||||
|
|
||||||
|
- `api/middleware/job/jobManager.js` - 工作管理核心逻辑
|
||||||
|
- `api/middleware/schedule/taskHandlers.js` - 任务处理器
|
||||||
|
- `api/middleware/schedule/command.js` - 指令管理器
|
||||||
|
- `api/services/pla_account_service.js` - 账号服务
|
||||||
|
- `api/model/job_postings.js` - 职位数据模型
|
||||||
|
- `api/model/pla_account.js` - 账号数据模型
|
||||||
|
|
||||||
387
_doc/项目功能总结.md
Normal file
387
_doc/项目功能总结.md
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
# 自动找工作系统 - 项目功能总结
|
||||||
|
|
||||||
|
> 版本: 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
|
||||||
36
_sql/add_pricing_plans_menu.sql
Normal file
36
_sql/add_pricing_plans_menu.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- 在用户管理菜单下添加"价格套餐管理"菜单项
|
||||||
|
-- 参考其他菜单项的配置格式
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (
|
||||||
|
name,
|
||||||
|
parent_id,
|
||||||
|
model_id,
|
||||||
|
form_id,
|
||||||
|
icon,
|
||||||
|
path,
|
||||||
|
component,
|
||||||
|
api_path,
|
||||||
|
is_show_menu,
|
||||||
|
is_show,
|
||||||
|
type,
|
||||||
|
sort,
|
||||||
|
create_time,
|
||||||
|
last_modify_time,
|
||||||
|
is_delete
|
||||||
|
) VALUES (
|
||||||
|
'价格套餐管理', -- 菜单名称
|
||||||
|
120, -- parent_id: 用户管理菜单的ID
|
||||||
|
0, -- model_id
|
||||||
|
0, -- form_id
|
||||||
|
'md-pricetags', -- icon: 价格标签图标
|
||||||
|
'pricing_plans', -- path: 路由路径
|
||||||
|
'system/pricing_plans.vue', -- component: 组件路径(已在 component-map.js 中定义)
|
||||||
|
'system/pricing_plans_server.js', -- api_path: API 服务文件路径
|
||||||
|
1, -- is_show_menu: 1=显示在菜单栏
|
||||||
|
1, -- is_show: 1=启用
|
||||||
|
'页面', -- type: 页面类型
|
||||||
|
3, -- sort: 排序(可根据实际情况调整)
|
||||||
|
NOW(), -- create_time: 创建时间
|
||||||
|
NOW(), -- last_modify_time: 最后修改时间
|
||||||
|
0 -- is_delete: 0=未删除
|
||||||
|
);
|
||||||
24
_sql/create_pricing_plans_table.sql
Normal file
24
_sql/create_pricing_plans_table.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-- 创建价格套餐表
|
||||||
|
-- 用于存储各种价格套餐的配置信息
|
||||||
|
|
||||||
|
CREATE TABLE `pricing_plans` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '价格套餐ID',
|
||||||
|
`name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '套餐名称(如:体验套餐、月度套餐等)',
|
||||||
|
`duration` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '时长描述(如:7天、30天、永久)',
|
||||||
|
`days` INT(11) NOT NULL DEFAULT 0 COMMENT '天数(-1表示永久,0表示无限制)',
|
||||||
|
`price` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '售价(元)',
|
||||||
|
`original_price` DECIMAL(10,2) NULL DEFAULT NULL COMMENT '原价(元),可为空表示无原价',
|
||||||
|
`unit` VARCHAR(20) NOT NULL DEFAULT '元' COMMENT '价格单位',
|
||||||
|
`discount` VARCHAR(50) NULL DEFAULT NULL COMMENT '折扣描述(如:8.3折、超值)',
|
||||||
|
`features` TEXT NOT NULL COMMENT '功能特性列表(JSON字符串数组)',
|
||||||
|
`featured` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为推荐套餐(1=推荐,0=普通)',
|
||||||
|
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用(1=启用,0=禁用)',
|
||||||
|
`sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT '排序顺序(越小越靠前)',
|
||||||
|
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
|
||||||
|
`is_delete` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除(1=已删除,0=未删除)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_is_active` (`is_active`),
|
||||||
|
INDEX `idx_is_delete` (`is_delete`),
|
||||||
|
INDEX `idx_sort_order` (`sort_order`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='价格套餐表';
|
||||||
89
_sql/insert_pricing_plans_data.sql
Normal file
89
_sql/insert_pricing_plans_data.sql
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
-- 插入初始价格套餐数据
|
||||||
|
-- 基于原有前端接口 /config/pricing-plans 中的硬编码数据
|
||||||
|
|
||||||
|
INSERT INTO `pricing_plans` (
|
||||||
|
`name`,
|
||||||
|
`duration`,
|
||||||
|
`days`,
|
||||||
|
`price`,
|
||||||
|
`original_price`,
|
||||||
|
`unit`,
|
||||||
|
`discount`,
|
||||||
|
`features`,
|
||||||
|
`featured`,
|
||||||
|
`is_active`,
|
||||||
|
`sort_order`,
|
||||||
|
`create_time`,
|
||||||
|
`last_modify_time`,
|
||||||
|
`is_delete`
|
||||||
|
) VALUES
|
||||||
|
(
|
||||||
|
'体验套餐',
|
||||||
|
'7天',
|
||||||
|
7,
|
||||||
|
28.00,
|
||||||
|
28.00,
|
||||||
|
'元',
|
||||||
|
NULL,
|
||||||
|
'["7天使用权限", "全功能体验", "技术支持"]',
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'月度套餐',
|
||||||
|
'30天',
|
||||||
|
30,
|
||||||
|
99.00,
|
||||||
|
120.00,
|
||||||
|
'元',
|
||||||
|
'约8.3折',
|
||||||
|
'["30天使用权限", "全功能使用", "优先技术支持", "性价比最高"]',
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'季度套餐',
|
||||||
|
'90天',
|
||||||
|
90,
|
||||||
|
269.00,
|
||||||
|
360.00,
|
||||||
|
'元',
|
||||||
|
'7.5折',
|
||||||
|
'["90天使用权限", "全功能使用", "优先技术支持", "更优惠价格"]',
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'终生套餐',
|
||||||
|
'永久',
|
||||||
|
-1,
|
||||||
|
888.00,
|
||||||
|
NULL,
|
||||||
|
'元',
|
||||||
|
'超值',
|
||||||
|
'["永久使用权限", "全功能使用", "终身技术支持", "一次购买,终身使用", "最划算选择"]',
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 查询验证插入结果
|
||||||
|
SELECT id, name, duration, price, original_price, featured, is_active, sort_order
|
||||||
|
FROM pricing_plans
|
||||||
|
WHERE is_delete = 0
|
||||||
|
ORDER BY sort_order ASC;
|
||||||
@@ -26,11 +26,11 @@ const baseConfig = {
|
|||||||
// 开发环境配置
|
// 开发环境配置
|
||||||
const developmentConfig = {
|
const developmentConfig = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
apiUrl: 'http://localhost:9097/admin_api/',
|
// apiUrl: 'http://localhost:9097/admin_api/',
|
||||||
uploadUrl: 'http://localhost:9097/admin_api/upload',
|
// uploadUrl: 'http://localhost:9097/admin_api/upload',
|
||||||
|
|
||||||
// apiUrl: 'https://work.light120.com/admin_api/',
|
apiUrl: 'https://work.light120.com/admin_api/',
|
||||||
// uploadUrl: 'https://work.light120.com/admin_api/upload',
|
uploadUrl: 'https://work.light120.com/admin_api/upload',
|
||||||
// 开发环境显示更多调试信息
|
// 开发环境显示更多调试信息
|
||||||
debug: true
|
debug: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ class ResumeInfoServer {
|
|||||||
analyzeWithAI(resumeId) {
|
analyzeWithAI(resumeId) {
|
||||||
return window.framework.http.post('/resume/analyze-with-ai', { resumeId })
|
return window.framework.http.post('/resume/analyze-with-ai', { resumeId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步在线简历
|
||||||
|
* @param {String} resumeId - 简历ID
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
syncOnline(resumeId) {
|
||||||
|
return window.framework.http.post('/resume/sync-online', { resumeId })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ResumeInfoServer()
|
export default new ResumeInfoServer()
|
||||||
|
|||||||
54
admin/src/api/system/pricing_plans_server.js
Normal file
54
admin/src/api/system/pricing_plans_server.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* 价格套餐 API 服务
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PricingPlansServer {
|
||||||
|
/**
|
||||||
|
* 分页查询价格套餐
|
||||||
|
* @param {Object} param - 查询参数
|
||||||
|
* @param {Object} param.seachOption - 搜索条件
|
||||||
|
* @param {Object} param.pageOption - 分页选项
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
page(param) {
|
||||||
|
return window.framework.http.post('/pricing_plans/list', param)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取价格套餐详情
|
||||||
|
* @param {Number|String} id - 价格套餐ID
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
getById(id) {
|
||||||
|
return window.framework.http.get('/pricing_plans/detail', { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增价格套餐
|
||||||
|
* @param {Object} row - 价格套餐数据
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
add(row) {
|
||||||
|
return window.framework.http.post('/pricing_plans/create', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新价格套餐信息
|
||||||
|
* @param {Object} row - 价格套餐数据
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
update(row) {
|
||||||
|
return window.framework.http.post('/pricing_plans/update', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除价格套餐
|
||||||
|
* @param {Object} row - 价格套餐数据(包含id)
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
del(row) {
|
||||||
|
return window.framework.http.post('/pricing_plans/delete', { id: row.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new PricingPlansServer()
|
||||||
9
admin/src/framework/node-core-framework.js
Normal file
9
admin/src/framework/node-core-framework.js
Normal file
File diff suppressed because one or more lines are too long
@@ -21,6 +21,7 @@ import TaskStatus from '@/views/task/task_status.vue'
|
|||||||
import SystemConfig from '@/views/system/system_config.vue'
|
import SystemConfig from '@/views/system/system_config.vue'
|
||||||
import Version from '@/views/system/version.vue'
|
import Version from '@/views/system/version.vue'
|
||||||
import JobTypes from '@/views/work/job_types.vue'
|
import JobTypes from '@/views/work/job_types.vue'
|
||||||
|
import PricingPlans from '@/views/system/pricing_plans.vue'
|
||||||
|
|
||||||
// 首页模块
|
// 首页模块
|
||||||
import HomeIndex from '@/views/home/index.vue'
|
import HomeIndex from '@/views/home/index.vue'
|
||||||
@@ -53,6 +54,8 @@ const componentMap = {
|
|||||||
'system/system_config': SystemConfig,
|
'system/system_config': SystemConfig,
|
||||||
'system/version': Version,
|
'system/version': Version,
|
||||||
'work/job_types': JobTypes,
|
'work/job_types': JobTypes,
|
||||||
|
'system/pricing_plans': PricingPlans,
|
||||||
|
'system/pricing_plans.vue': PricingPlans,
|
||||||
'home/index': HomeIndex,
|
'home/index': HomeIndex,
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -349,12 +349,12 @@ export default {
|
|||||||
{ title: '密码', key: 'pwd', com: 'Password', required: true },
|
{ title: '密码', key: 'pwd', com: 'Password', required: true },
|
||||||
],
|
],
|
||||||
listColumns: [
|
listColumns: [
|
||||||
{ title: 'ID', key: 'id' },
|
{ title: '账户名', key: 'name', minWidth: 120 },
|
||||||
{ title: '账户名', key: 'name' },
|
{ title: '设备SN码', key: 'sn_code', minWidth: 150 },
|
||||||
{ title: '设备SN码', key: 'sn_code'},
|
|
||||||
{
|
{
|
||||||
title: '平台',
|
title: '平台',
|
||||||
key: 'platform_type',
|
key: 'platform_type',
|
||||||
|
minWidth: 100,
|
||||||
render: (h, params) => {
|
render: (h, params) => {
|
||||||
const platformMap = {
|
const platformMap = {
|
||||||
'boss': { text: 'Boss直聘', color: 'blue' },
|
'boss': { text: 'Boss直聘', color: 'blue' },
|
||||||
@@ -364,26 +364,10 @@ export default {
|
|||||||
return h('Tag', { props: { color: platform.color } }, platform.text)
|
return h('Tag', { props: { color: platform.color } }, platform.text)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ title: '登录名', key: 'login_name'},
|
|
||||||
{ title: '搜索关键词', key: 'keyword' },
|
|
||||||
{ title: '用户地址', key: 'user_address' },
|
|
||||||
{
|
|
||||||
title: '经纬度',
|
|
||||||
key: 'location',
|
|
||||||
|
|
||||||
render: (h, params) => {
|
|
||||||
const lon = params.row.user_longitude;
|
|
||||||
const lat = params.row.user_latitude;
|
|
||||||
if (lon && lat) {
|
|
||||||
return h('span', `${lat}, ${lon}`)
|
|
||||||
}
|
|
||||||
return h('span', { style: { color: '#999' } }, '未设置')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '在线状态',
|
title: '在线状态',
|
||||||
key: 'is_online',
|
key: 'is_online',
|
||||||
|
minWidth: 90,
|
||||||
render: (h, params) => {
|
render: (h, params) => {
|
||||||
return h('Tag', {
|
return h('Tag', {
|
||||||
props: { color: params.row.is_online ? 'success' : 'default' }
|
props: { color: params.row.is_online ? 'success' : 'default' }
|
||||||
@@ -393,12 +377,7 @@ export default {
|
|||||||
{
|
{
|
||||||
title: '自动投递',
|
title: '自动投递',
|
||||||
key: 'auto_deliver',
|
key: 'auto_deliver',
|
||||||
com: "Radio",
|
minWidth: 90,
|
||||||
|
|
||||||
options: [
|
|
||||||
{ value: 1, label: '开启' },
|
|
||||||
{ value: 0, label: '关闭' }
|
|
||||||
],
|
|
||||||
render: (h, params) => {
|
render: (h, params) => {
|
||||||
return h('Tag', {
|
return h('Tag', {
|
||||||
props: { color: params.row.auto_deliver ? 'success' : 'default' }
|
props: { color: params.row.auto_deliver ? 'success' : 'default' }
|
||||||
@@ -408,11 +387,7 @@ export default {
|
|||||||
{
|
{
|
||||||
title: '自动沟通',
|
title: '自动沟通',
|
||||||
key: 'auto_chat',
|
key: 'auto_chat',
|
||||||
"com": "Radio",
|
minWidth: 90,
|
||||||
options: [
|
|
||||||
{ value: 1, label: '开启' },
|
|
||||||
{ value: 0, label: '关闭' }
|
|
||||||
],
|
|
||||||
render: (h, params) => {
|
render: (h, params) => {
|
||||||
return h('Tag', {
|
return h('Tag', {
|
||||||
props: { color: params.row.auto_chat ? 'success' : 'default' }
|
props: { color: params.row.auto_chat ? 'success' : 'default' }
|
||||||
@@ -422,7 +397,7 @@ export default {
|
|||||||
{
|
{
|
||||||
title: '剩余天数',
|
title: '剩余天数',
|
||||||
key: 'remaining_days',
|
key: 'remaining_days',
|
||||||
|
minWidth: 100,
|
||||||
render: (h, params) => {
|
render: (h, params) => {
|
||||||
const remainingDays = params.row.remaining_days || 0
|
const remainingDays = params.row.remaining_days || 0
|
||||||
let color = 'success'
|
let color = 'success'
|
||||||
@@ -436,52 +411,10 @@ export default {
|
|||||||
}, remainingDays > 0 ? `${remainingDays} 天` : '已过期')
|
}, remainingDays > 0 ? `${remainingDays} 天` : '已过期')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '授权日期',
|
|
||||||
key: 'authorization_date',
|
|
||||||
|
|
||||||
render: (h, params) => {
|
|
||||||
if (!params.row.authorization_date) {
|
|
||||||
return h('span', { style: { color: '#999' } }, '未授权')
|
|
||||||
}
|
|
||||||
const date = new Date(params.row.authorization_date)
|
|
||||||
return h('span', this.formatDate(date))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '过期时间',
|
|
||||||
key: 'expire_date',
|
|
||||||
|
|
||||||
render: (h, params) => {
|
|
||||||
if (!params.row.authorization_date || !params.row.authorization_days) {
|
|
||||||
return h('span', { style: { color: '#999' } }, '未授权')
|
|
||||||
}
|
|
||||||
const authDate = new Date(params.row.authorization_date)
|
|
||||||
const expireDate = new Date(authDate.getTime() + params.row.authorization_days * 24 * 60 * 60 * 1000)
|
|
||||||
const remainingDays = params.row.remaining_days || 0
|
|
||||||
return h('span', {
|
|
||||||
style: { color: remainingDays <= 0 ? '#ed4014' : remainingDays <= 7 ? '#ff9900' : '#515a6e' }
|
|
||||||
}, this.formatDate(expireDate))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '自动活跃',
|
|
||||||
key: 'auto_active',
|
|
||||||
"com": "Radio",
|
|
||||||
options: [
|
|
||||||
{ value: 1, label: '开启' },
|
|
||||||
{ value: 0, label: '关闭' }
|
|
||||||
],
|
|
||||||
|
|
||||||
render: (h, params) => {
|
|
||||||
return h('Tag', {
|
|
||||||
props: { color: params.row.auto_active ? 'success' : 'default' }
|
|
||||||
}, params.row.auto_active ? '开启' : '关闭')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '启用状态',
|
title: '启用状态',
|
||||||
key: 'is_enabled',
|
key: 'is_enabled',
|
||||||
|
minWidth: 100,
|
||||||
render: (h, params) => {
|
render: (h, params) => {
|
||||||
return h('i-switch', {
|
return h('i-switch', {
|
||||||
props: {
|
props: {
|
||||||
@@ -496,7 +429,6 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ title: '创建时间', key: 'create_time', },
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
@@ -640,18 +572,21 @@ export default {
|
|||||||
}
|
}
|
||||||
this.query(1)
|
this.query(1)
|
||||||
},
|
},
|
||||||
async handleSaveSuccess({ data }) {
|
async handleSaveSuccess({ data, isEdit } = {}) {
|
||||||
try {
|
try {
|
||||||
// 如果是新增(来自 editModal),data 只包含必填字段,直接保存
|
// 如果是新增(来自 editModal),data 只包含必填字段,直接保存
|
||||||
if (data && !data.id) {
|
if (data && !data.id && !isEdit) {
|
||||||
await plaAccountServer.add(data)
|
await plaAccountServer.add(data)
|
||||||
this.$Message.success('保存成功!')
|
this.$Message.success('保存成功!')
|
||||||
}
|
}
|
||||||
// 编辑时由 FloatPanel 组件(PlaAccountEdit)处理保存,这里只刷新列表
|
// 编辑时由 FloatPanel 组件(PlaAccountEdit)处理保存,这里只刷新列表
|
||||||
this.query(this.gridOption.param.pageOption.page)
|
// 刷新列表,保持当前页码
|
||||||
|
this.query(this.gridOption.param.pageOption.page || 1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存失败:', error)
|
console.error('保存失败:', error)
|
||||||
this.$Message.error('保存失败:' + (error.message || '请稍后重试'))
|
// 优先从 error.response.data.message 获取,然后是 error.message
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '请稍后重试'
|
||||||
|
this.$Message.error('保存失败:' + errorMsg)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 显示账号详情
|
// 显示账号详情
|
||||||
@@ -861,7 +796,9 @@ export default {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('位置解析失败:', error)
|
console.error('位置解析失败:', error)
|
||||||
this.$Message.error('位置解析失败:' + (error.message || '请稍后重试'))
|
// 优先从 error.response.data.message 获取,然后是 error.message
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '请稍后重试'
|
||||||
|
this.$Message.error(errorMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -908,7 +845,9 @@ export default {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量位置解析失败:', error)
|
console.error('批量位置解析失败:', error)
|
||||||
this.$Message.error('批量位置解析失败:' + (error.message || '请稍后重试'))
|
// 优先从 error.response.data.message 获取,然后是 error.message
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '请稍后重试'
|
||||||
|
this.$Message.error(errorMsg)
|
||||||
} finally {
|
} finally {
|
||||||
this.batchParseLoading = false
|
this.batchParseLoading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,10 @@
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="平台" prop="platform_type">
|
<FormItem label="平台" prop="platform_type">
|
||||||
<Select v-model="formData.platform_type" placeholder="请选择平台">
|
<Select v-model="formData.platform_type" placeholder="请选择平台">
|
||||||
<Option value="1">Boss直聘</Option>
|
<Option value="boss">Boss直聘</Option>
|
||||||
<Option value="2">猎聘</Option>
|
<Option value="liepin">猎聘</Option>
|
||||||
|
<Option value="zhipin">智联招聘</Option>
|
||||||
|
<Option value="58">58同城</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="登录名" prop="login_name">
|
<FormItem label="登录名" prop="login_name">
|
||||||
@@ -545,7 +547,8 @@ export default {
|
|||||||
|
|
||||||
this.$Message.success('保存成功!')
|
this.$Message.success('保存成功!')
|
||||||
this.$refs.floatPanel.hide()
|
this.$refs.floatPanel.hide()
|
||||||
this.$emit('on-save')
|
// 触发保存成功事件,通知父组件刷新列表
|
||||||
|
this.$emit('on-save', { isEdit: this.isEdit, data: saveData })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存失败:', error)
|
console.error('保存失败:', error)
|
||||||
this.$Message.error('保存失败:' + (error.message || '请稍后重试'))
|
this.$Message.error('保存失败:' + (error.message || '请稍后重试'))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
@back="handleBack"
|
@back="handleBack"
|
||||||
>
|
>
|
||||||
<template #header-right>
|
<template #header-right>
|
||||||
|
<Button type="info" @click="handleSyncOnline" :loading="syncing" style="margin-right: 8px;">同步在线简历</Button>
|
||||||
<Button type="primary" @click="handleAnalyzeAI" :loading="analyzing">AI 分析</Button>
|
<Button type="primary" @click="handleAnalyzeAI" :loading="analyzing">AI 分析</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -272,6 +273,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
analyzing: false,
|
analyzing: false,
|
||||||
|
syncing: false,
|
||||||
resumeData: null,
|
resumeData: null,
|
||||||
skillTags: [],
|
skillTags: [],
|
||||||
workExperience: [],
|
workExperience: [],
|
||||||
@@ -317,6 +319,32 @@ export default {
|
|||||||
}
|
}
|
||||||
return field
|
return field
|
||||||
},
|
},
|
||||||
|
async handleSyncOnline() {
|
||||||
|
if (!this.resumeData || !this.resumeData.resumeId) {
|
||||||
|
this.$Message.warning('简历ID不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.resumeData.sn_code) {
|
||||||
|
this.$Message.warning('该简历未绑定设备,无法同步在线简历')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncing = true
|
||||||
|
try {
|
||||||
|
const res = await resumeInfoServer.syncOnline(this.resumeData.resumeId)
|
||||||
|
this.$Message.success(res.message || '同步在线简历成功')
|
||||||
|
// 重新加载数据
|
||||||
|
await this.loadResumeData(this.resumeData.resumeId)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步在线简历失败:', error)
|
||||||
|
// 优先从 error.response.data.message 获取,然后是 error.message
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '请稍后重试'
|
||||||
|
this.$Message.error(errorMsg)
|
||||||
|
} finally {
|
||||||
|
this.syncing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
async handleAnalyzeAI() {
|
async handleAnalyzeAI() {
|
||||||
if (!this.resumeData || !this.resumeData.resumeId) {
|
if (!this.resumeData || !this.resumeData.resumeId) {
|
||||||
this.$Message.warning('简历ID不存在')
|
this.$Message.warning('简历ID不存在')
|
||||||
@@ -331,7 +359,9 @@ export default {
|
|||||||
await this.loadResumeData(this.resumeData.resumeId)
|
await this.loadResumeData(this.resumeData.resumeId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI 分析失败:', error)
|
console.error('AI 分析失败:', error)
|
||||||
this.$Message.error('AI 分析失败: ' + (error.message || '请稍后重试'))
|
// 优先从 error.response.data.message 获取,然后是 error.message
|
||||||
|
const errorMsg = error.response?.data?.message || error.message || '请稍后重试'
|
||||||
|
this.$Message.error(errorMsg)
|
||||||
} finally {
|
} finally {
|
||||||
this.analyzing = false
|
this.analyzing = false
|
||||||
}
|
}
|
||||||
|
|||||||
392
admin/src/views/system/pricing_plans.vue
Normal file
392
admin/src/views/system/pricing_plans.vue
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-view">
|
||||||
|
<div class="table-head-tool">
|
||||||
|
<Button type="primary" @click="showAddWarp">新增价格套餐</Button>
|
||||||
|
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||||
|
<FormItem :label-width="20" class="flex">
|
||||||
|
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||||
|
:placeholder="seachTypePlaceholder">
|
||||||
|
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||||
|
</Select>
|
||||||
|
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||||
|
placeholder="请输入关键字" @on-search="query(1)" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="状态">
|
||||||
|
<Select v-model="gridOption.param.seachOption.is_active" style="width: 120px" clearable
|
||||||
|
@on-change="query(1)">
|
||||||
|
<Option :value="1">启用</Option>
|
||||||
|
<Option :value="0">禁用</Option>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem>
|
||||||
|
<Button type="primary" @click="query(1)">查询</Button>
|
||||||
|
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<div class="table-body">
|
||||||
|
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||||
|
@changePage="query"></tables>
|
||||||
|
</div>
|
||||||
|
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules" >
|
||||||
|
</editModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import pricingPlansServer from '@/api/system/pricing_plans_server.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
let rules = {}
|
||||||
|
rules["name"] = [{ required: true, message: '请填写套餐名称', trigger: 'blur' }]
|
||||||
|
rules["duration"] = [{ required: true, message: '请填写时长描述', trigger: 'blur' }]
|
||||||
|
rules["days"] = [{ required: true, type: 'number', message: '请填写天数', trigger: 'blur' }]
|
||||||
|
rules["price"] = [{ required: true, message: '请填写价格', trigger: 'blur' }]
|
||||||
|
|
||||||
|
return {
|
||||||
|
seachTypes: [
|
||||||
|
{ key: 'name', value: '套餐名称' },
|
||||||
|
{ key: 'duration', value: '时长' }
|
||||||
|
],
|
||||||
|
gridOption: {
|
||||||
|
param: {
|
||||||
|
seachOption: {
|
||||||
|
key: 'name',
|
||||||
|
value: '',
|
||||||
|
is_active: null
|
||||||
|
},
|
||||||
|
pageOption: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
rules: rules
|
||||||
|
},
|
||||||
|
listColumns: [
|
||||||
|
{ title: 'ID', key: 'id', minWidth: 60 },
|
||||||
|
{ title: '套餐名称', key: 'name', minWidth: 120 },
|
||||||
|
{ title: '时长', key: 'duration', minWidth: 100 },
|
||||||
|
{ title: '天数', key: 'days', minWidth: 80 },
|
||||||
|
{
|
||||||
|
title: '价格',
|
||||||
|
key: 'price',
|
||||||
|
minWidth: 100,
|
||||||
|
render: (h, params) => {
|
||||||
|
return h('span', `¥${params.row.price}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '原价',
|
||||||
|
key: 'original_price',
|
||||||
|
minWidth: 100,
|
||||||
|
render: (h, params) => {
|
||||||
|
return h('span', params.row.original_price ? `¥${params.row.original_price}` : '-')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ title: '折扣', key: 'discount', minWidth: 100 },
|
||||||
|
{
|
||||||
|
title: '推荐',
|
||||||
|
key: 'featured',
|
||||||
|
minWidth: 80,
|
||||||
|
render: (h, params) => {
|
||||||
|
return h('Tag', {
|
||||||
|
props: { color: params.row.featured === 1 ? 'warning' : 'default' }
|
||||||
|
}, params.row.featured === 1 ? '推荐' : '普通')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'is_active',
|
||||||
|
minWidth: 80,
|
||||||
|
render: (h, params) => {
|
||||||
|
const status = params.row.is_active === 1
|
||||||
|
return h('Tag', {
|
||||||
|
props: { color: status ? 'success' : 'default' }
|
||||||
|
}, status ? '启用' : '禁用')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ title: '排序', key: 'sort_order', minWidth: 80 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 200,
|
||||||
|
align: 'center',
|
||||||
|
render: (h, params) => {
|
||||||
|
return h('div', [
|
||||||
|
h('Button', {
|
||||||
|
props: {
|
||||||
|
type: 'primary',
|
||||||
|
size: 'small'
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
marginRight: '5px'
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
click: () => {
|
||||||
|
this.edit(params.row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, '编辑'),
|
||||||
|
h('Button', {
|
||||||
|
props: {
|
||||||
|
type: 'error',
|
||||||
|
size: 'small'
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
click: () => {
|
||||||
|
this.del(params.row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, '删除')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
editColumns: [
|
||||||
|
{
|
||||||
|
title: '套餐名称',
|
||||||
|
key: 'name',
|
||||||
|
type: 'input',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时长描述',
|
||||||
|
key: 'duration',
|
||||||
|
type: 'input',
|
||||||
|
required: true,
|
||||||
|
placeholder: '如:7天、30天、永久'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '天数',
|
||||||
|
key: 'days',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
tooltip: '-1表示永久,0表示无限制'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '价格',
|
||||||
|
key: 'price',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '原价',
|
||||||
|
key: 'original_price',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
placeholder: '可留空,表示无原价'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '单位',
|
||||||
|
key: 'unit',
|
||||||
|
type: 'input',
|
||||||
|
required: false,
|
||||||
|
defaultValue: '元'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '折扣描述',
|
||||||
|
key: 'discount',
|
||||||
|
type: 'input',
|
||||||
|
required: false,
|
||||||
|
placeholder: '如:8.3折、超值'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '功能特性',
|
||||||
|
key: 'features',
|
||||||
|
com: 'TextArea',
|
||||||
|
required: false,
|
||||||
|
placeholder: '请输入JSON数组格式,例如:["功能1", "功能2", "功能3"]',
|
||||||
|
tooltip: '功能特性列表,JSON数组格式'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否推荐',
|
||||||
|
key: 'featured',
|
||||||
|
com: 'Radio',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 1, label: '推荐' },
|
||||||
|
{ value: 0, label: '普通' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否启用',
|
||||||
|
key: 'is_active',
|
||||||
|
com: 'Radio',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: 1, label: '启用' },
|
||||||
|
{ value: 0, label: '禁用' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '排序',
|
||||||
|
key: 'sort_order',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
tooltip: '数字越小越靠前'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
seachTypePlaceholder() {
|
||||||
|
const item = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||||
|
return item ? `请输入${item.value}` : '请选择'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.query(1)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
query(page) {
|
||||||
|
if (page) {
|
||||||
|
this.gridOption.param.pageOption.page = page
|
||||||
|
}
|
||||||
|
const param = {
|
||||||
|
pageOption: this.gridOption.param.pageOption,
|
||||||
|
seachOption: {}
|
||||||
|
}
|
||||||
|
if (this.gridOption.param.seachOption.key && this.gridOption.param.seachOption.value) {
|
||||||
|
param.seachOption[this.gridOption.param.seachOption.key] = this.gridOption.param.seachOption.value
|
||||||
|
}
|
||||||
|
if (this.gridOption.param.seachOption.is_active !== null) {
|
||||||
|
param.seachOption.is_active = this.gridOption.param.seachOption.is_active
|
||||||
|
}
|
||||||
|
pricingPlansServer.page(param).then(res => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
const data = res.data
|
||||||
|
this.gridOption.data = data.rows
|
||||||
|
this.gridOption.param.pageOption.total = data.count || data.total || 0
|
||||||
|
} else {
|
||||||
|
this.$Message.error(res.message || '查询失败')
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
this.$Message.error('查询失败:' + (err.message || '未知错误'))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.gridOption.param.seachOption = {
|
||||||
|
key: 'name',
|
||||||
|
value: '',
|
||||||
|
is_active: null
|
||||||
|
}
|
||||||
|
this.query(1)
|
||||||
|
},
|
||||||
|
showAddWarp() {
|
||||||
|
|
||||||
|
let editRow={
|
||||||
|
name: '',
|
||||||
|
duration: '',
|
||||||
|
days: 0,
|
||||||
|
price: 0,
|
||||||
|
original_price: null,
|
||||||
|
unit: '元',
|
||||||
|
discount: '',
|
||||||
|
features: '[]',
|
||||||
|
featured: 0,
|
||||||
|
is_active: 1,
|
||||||
|
sort_order: 0
|
||||||
|
}
|
||||||
|
this.$refs.editModal.show(editRow)
|
||||||
|
},
|
||||||
|
edit(row) {
|
||||||
|
// 解析 JSON 字段
|
||||||
|
let features = row.features || '[]'
|
||||||
|
|
||||||
|
// 如果是字符串,尝试解析并格式化
|
||||||
|
if (typeof features === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(features)
|
||||||
|
features = JSON.stringify(parsed, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
// 保持原样
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
features = JSON.stringify(features, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
let editRow={
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
duration: row.duration || '',
|
||||||
|
days: row.days,
|
||||||
|
price: row.price,
|
||||||
|
original_price: row.original_price,
|
||||||
|
unit: row.unit || '元',
|
||||||
|
discount: row.discount || '',
|
||||||
|
features: features,
|
||||||
|
featured: row.featured,
|
||||||
|
is_active: row.is_active,
|
||||||
|
sort_order: row.sort_order || 0
|
||||||
|
}
|
||||||
|
this.$refs.editModal.editShow(editRow,(newRow)=>{
|
||||||
|
debugger
|
||||||
|
this.handleSaveSuccess(newRow)
|
||||||
|
|
||||||
|
})
|
||||||
|
},
|
||||||
|
del(row) {
|
||||||
|
this.$Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除价格套餐"${row.name}"吗?`,
|
||||||
|
onOk: () => {
|
||||||
|
pricingPlansServer.del(row).then(res => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$Message.success('删除成功')
|
||||||
|
this.query(this.gridOption.param.pageOption.page)
|
||||||
|
} else {
|
||||||
|
this.$Message.error(res.message || '删除失败')
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
this.$Message.error('删除失败:' + (err.message || '未知错误'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleSaveSuccess(data) {
|
||||||
|
// 处理 JSON 字段
|
||||||
|
const formData = { ...data }
|
||||||
|
|
||||||
|
// 处理 features
|
||||||
|
if (formData.features) {
|
||||||
|
try {
|
||||||
|
const parsed = typeof formData.features === 'string'
|
||||||
|
? JSON.parse(formData.features)
|
||||||
|
: formData.features
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
this.$Message.warning('功能特性必须是数组格式,将使用空数组')
|
||||||
|
formData.features = []
|
||||||
|
} else {
|
||||||
|
formData.features = parsed
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$Message.warning('功能特性格式错误,将使用空数组')
|
||||||
|
formData.features = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiMethod = formData.id ? pricingPlansServer.update : pricingPlansServer.add
|
||||||
|
apiMethod(formData).then(res => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$Message.success(formData.id ? '更新成功' : '添加成功')
|
||||||
|
this.$refs.editModal.hide()
|
||||||
|
this.query(this.gridOption.param.pageOption.page)
|
||||||
|
} else {
|
||||||
|
this.$Message.error(res.message || (formData.id ? '更新失败' : '添加失败'))
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
this.$Message.error((formData.id ? '更新失败' : '添加失败') + ':' + (err.message || '未知错误'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-view {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
455
api/controller_admin/pricing_plans.js
Normal file
455
api/controller_admin/pricing_plans.js
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
/**
|
||||||
|
* 价格套餐管理API - 后台管理
|
||||||
|
* 提供价格套餐的增删改查功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Framework = require("../../framework/node-core-framework.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /admin_api/pricing_plans/list:
|
||||||
|
* post:
|
||||||
|
* summary: 获取价格套餐列表
|
||||||
|
* description: 分页获取所有价格套餐
|
||||||
|
* tags: [后台-价格套餐管理]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* pageOption:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* page:
|
||||||
|
* type: integer
|
||||||
|
* description: 页码
|
||||||
|
* pageSize:
|
||||||
|
* type: integer
|
||||||
|
* description: 每页数量
|
||||||
|
* seachOption:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* key:
|
||||||
|
* type: string
|
||||||
|
* description: 搜索字段
|
||||||
|
* value:
|
||||||
|
* type: string
|
||||||
|
* description: 搜索值
|
||||||
|
* is_active:
|
||||||
|
* type: integer
|
||||||
|
* description: 状态筛选(1=启用,0=禁用)
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 获取成功
|
||||||
|
*/
|
||||||
|
'POST /pricing_plans/list': async (ctx) => {
|
||||||
|
try {
|
||||||
|
const models = Framework.getModels();
|
||||||
|
const { pricing_plans, op } = models;
|
||||||
|
const body = ctx.getBody();
|
||||||
|
|
||||||
|
// 获取分页参数
|
||||||
|
const { limit, offset } = ctx.getPageSize();
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
const where = { is_delete: 0 };
|
||||||
|
|
||||||
|
// 搜索条件
|
||||||
|
if (body.seachOption) {
|
||||||
|
const { key, value, is_active } = body.seachOption;
|
||||||
|
|
||||||
|
if (value && key) {
|
||||||
|
if (key === 'name') {
|
||||||
|
where.name = { [op.like]: `%${value}%` };
|
||||||
|
} else if (key === 'duration') {
|
||||||
|
where.duration = { [op.like]: `%${value}%` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if (is_active !== undefined && is_active !== null && is_active !== '') {
|
||||||
|
where.is_active = is_active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pricing_plans.findAndCountAll({
|
||||||
|
where,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
order: [['sort_order', 'ASC'], ['id', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx.success(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取价格套餐列表失败:', error);
|
||||||
|
return ctx.fail('获取价格套餐列表失败: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /admin_api/pricing_plans/detail:
|
||||||
|
* get:
|
||||||
|
* summary: 获取价格套餐详情
|
||||||
|
* description: 根据ID获取价格套餐详细信息
|
||||||
|
* tags: [后台-价格套餐管理]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: 价格套餐ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 获取成功
|
||||||
|
*/
|
||||||
|
'GET /pricing_plans/detail': async (ctx) => {
|
||||||
|
try {
|
||||||
|
const models = Framework.getModels();
|
||||||
|
const { pricing_plans } = models;
|
||||||
|
const { id } = ctx.getQuery();
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return ctx.fail('价格套餐ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await pricing_plans.findOne({
|
||||||
|
where: { id, is_delete: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
|
return ctx.fail('价格套餐不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.success(plan);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取价格套餐详情失败:', error);
|
||||||
|
return ctx.fail('获取价格套餐详情失败: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /admin_api/pricing_plans/create:
|
||||||
|
* post:
|
||||||
|
* summary: 创建价格套餐
|
||||||
|
* description: 创建新的价格套餐
|
||||||
|
* tags: [后台-价格套餐管理]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - name
|
||||||
|
* - duration
|
||||||
|
* - days
|
||||||
|
* - price
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* description: 套餐名称
|
||||||
|
* duration:
|
||||||
|
* type: string
|
||||||
|
* description: 时长描述
|
||||||
|
* days:
|
||||||
|
* type: integer
|
||||||
|
* description: 天数(-1表示永久)
|
||||||
|
* price:
|
||||||
|
* type: number
|
||||||
|
* description: 售价
|
||||||
|
* original_price:
|
||||||
|
* type: number
|
||||||
|
* description: 原价
|
||||||
|
* unit:
|
||||||
|
* type: string
|
||||||
|
* description: 单位
|
||||||
|
* discount:
|
||||||
|
* type: string
|
||||||
|
* description: 折扣描述
|
||||||
|
* features:
|
||||||
|
* type: array
|
||||||
|
* description: 功能特性列表
|
||||||
|
* featured:
|
||||||
|
* type: integer
|
||||||
|
* description: 是否推荐(1=推荐,0=普通)
|
||||||
|
* is_active:
|
||||||
|
* type: integer
|
||||||
|
* description: 是否启用(1=启用,0=禁用)
|
||||||
|
* sort_order:
|
||||||
|
* type: integer
|
||||||
|
* description: 排序顺序
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 创建成功
|
||||||
|
*/
|
||||||
|
'POST /pricing_plans/create': async (ctx) => {
|
||||||
|
try {
|
||||||
|
const models = Framework.getModels();
|
||||||
|
const { pricing_plans } = models;
|
||||||
|
const body = ctx.getBody();
|
||||||
|
const { name, duration, days, price, original_price, unit, discount, features, featured, is_active, sort_order } = body;
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if (!name) {
|
||||||
|
return ctx.fail('套餐名称不能为空');
|
||||||
|
}
|
||||||
|
if (!duration) {
|
||||||
|
return ctx.fail('时长描述不能为空');
|
||||||
|
}
|
||||||
|
if (days === undefined || days === null) {
|
||||||
|
return ctx.fail('天数不能为空');
|
||||||
|
}
|
||||||
|
if (!price && price !== 0) {
|
||||||
|
return ctx.fail('价格不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证价格
|
||||||
|
if (price < 0) {
|
||||||
|
return ctx.fail('价格不能为负数');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证天数
|
||||||
|
if (days < -1) {
|
||||||
|
return ctx.fail('天数不能小于-1(-1表示永久)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 features 字段:转换为 JSON 字符串
|
||||||
|
let featuresStr = '[]';
|
||||||
|
if (features) {
|
||||||
|
try {
|
||||||
|
if (Array.isArray(features)) {
|
||||||
|
featuresStr = JSON.stringify(features);
|
||||||
|
} else if (typeof features === 'string') {
|
||||||
|
// 验证是否为有效 JSON
|
||||||
|
const parsed = JSON.parse(features);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return ctx.fail('功能特性必须是数组格式');
|
||||||
|
}
|
||||||
|
featuresStr = features;
|
||||||
|
} else {
|
||||||
|
return ctx.fail('功能特性格式错误');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return ctx.fail('功能特性JSON格式错误');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建套餐
|
||||||
|
const plan = await pricing_plans.create({
|
||||||
|
name,
|
||||||
|
duration,
|
||||||
|
days,
|
||||||
|
price,
|
||||||
|
original_price: original_price !== undefined ? original_price : null,
|
||||||
|
unit: unit || '元',
|
||||||
|
discount: discount || null,
|
||||||
|
features: featuresStr,
|
||||||
|
featured: featured !== undefined ? featured : 0,
|
||||||
|
is_active: is_active !== undefined ? is_active : 1,
|
||||||
|
sort_order: sort_order !== undefined ? sort_order : 0,
|
||||||
|
is_delete: 0,
|
||||||
|
create_time: new Date(),
|
||||||
|
last_modify_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx.success(plan);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建价格套餐失败:', error);
|
||||||
|
return ctx.fail('创建价格套餐失败: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /admin_api/pricing_plans/update:
|
||||||
|
* post:
|
||||||
|
* summary: 更新价格套餐
|
||||||
|
* description: 更新价格套餐信息
|
||||||
|
* tags: [后台-价格套餐管理]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - id
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 价格套餐ID
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* description: 套餐名称
|
||||||
|
* duration:
|
||||||
|
* type: string
|
||||||
|
* description: 时长描述
|
||||||
|
* days:
|
||||||
|
* type: integer
|
||||||
|
* description: 天数
|
||||||
|
* price:
|
||||||
|
* type: number
|
||||||
|
* description: 售价
|
||||||
|
* original_price:
|
||||||
|
* type: number
|
||||||
|
* description: 原价
|
||||||
|
* unit:
|
||||||
|
* type: string
|
||||||
|
* description: 单位
|
||||||
|
* discount:
|
||||||
|
* type: string
|
||||||
|
* description: 折扣描述
|
||||||
|
* features:
|
||||||
|
* type: array
|
||||||
|
* description: 功能特性列表
|
||||||
|
* featured:
|
||||||
|
* type: integer
|
||||||
|
* description: 是否推荐
|
||||||
|
* is_active:
|
||||||
|
* type: integer
|
||||||
|
* description: 是否启用
|
||||||
|
* sort_order:
|
||||||
|
* type: integer
|
||||||
|
* description: 排序顺序
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 更新成功
|
||||||
|
*/
|
||||||
|
'POST /pricing_plans/update': async (ctx) => {
|
||||||
|
try {
|
||||||
|
const models = Framework.getModels();
|
||||||
|
const { pricing_plans } = models;
|
||||||
|
const body = ctx.getBody();
|
||||||
|
const { id, name, duration, days, price, original_price, unit, discount, features, featured, is_active, sort_order } = body;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return ctx.fail('价格套餐ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await pricing_plans.findOne({
|
||||||
|
where: { id, is_delete: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
|
return ctx.fail('价格套餐不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建更新数据
|
||||||
|
const updateData = {
|
||||||
|
last_modify_time: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (name !== undefined) updateData.name = name;
|
||||||
|
if (duration !== undefined) updateData.duration = duration;
|
||||||
|
if (days !== undefined) {
|
||||||
|
if (days < -1) {
|
||||||
|
return ctx.fail('天数不能小于-1(-1表示永久)');
|
||||||
|
}
|
||||||
|
updateData.days = days;
|
||||||
|
}
|
||||||
|
if (price !== undefined) {
|
||||||
|
if (price < 0) {
|
||||||
|
return ctx.fail('价格不能为负数');
|
||||||
|
}
|
||||||
|
updateData.price = price;
|
||||||
|
}
|
||||||
|
if (original_price !== undefined) updateData.original_price = original_price;
|
||||||
|
if (unit !== undefined) updateData.unit = unit;
|
||||||
|
if (discount !== undefined) updateData.discount = discount;
|
||||||
|
if (featured !== undefined) updateData.featured = featured;
|
||||||
|
if (is_active !== undefined) updateData.is_active = is_active;
|
||||||
|
if (sort_order !== undefined) updateData.sort_order = sort_order;
|
||||||
|
|
||||||
|
// 处理 features 字段
|
||||||
|
if (features !== undefined) {
|
||||||
|
try {
|
||||||
|
if (Array.isArray(features)) {
|
||||||
|
updateData.features = JSON.stringify(features);
|
||||||
|
} else if (typeof features === 'string') {
|
||||||
|
const parsed = JSON.parse(features);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return ctx.fail('功能特性必须是数组格式');
|
||||||
|
}
|
||||||
|
updateData.features = features;
|
||||||
|
} else {
|
||||||
|
return ctx.fail('功能特性格式错误');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return ctx.fail('功能特性JSON格式错误');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await pricing_plans.update(updateData, {
|
||||||
|
where: { id, is_delete: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx.success({ message: '价格套餐更新成功' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新价格套餐失败:', error);
|
||||||
|
return ctx.fail('更新价格套餐失败: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /admin_api/pricing_plans/delete:
|
||||||
|
* post:
|
||||||
|
* summary: 删除价格套餐
|
||||||
|
* description: 软删除指定的价格套餐
|
||||||
|
* tags: [后台-价格套餐管理]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - id
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 价格套餐ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 删除成功
|
||||||
|
*/
|
||||||
|
'POST /pricing_plans/delete': async (ctx) => {
|
||||||
|
try {
|
||||||
|
const models = Framework.getModels();
|
||||||
|
const { pricing_plans } = models;
|
||||||
|
const { id } = ctx.getBody();
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return ctx.fail('价格套餐ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await pricing_plans.findOne({
|
||||||
|
where: { id, is_delete: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
|
return ctx.fail('价格套餐不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 软删除
|
||||||
|
await pricing_plans.update(
|
||||||
|
{
|
||||||
|
is_delete: 1,
|
||||||
|
last_modify_time: new Date()
|
||||||
|
},
|
||||||
|
{ where: { id } }
|
||||||
|
);
|
||||||
|
|
||||||
|
return ctx.success({ message: '价格套餐删除成功' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除价格套餐失败:', error);
|
||||||
|
return ctx.fail('删除价格套餐失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -358,6 +358,100 @@ return ctx.success({ message: '简历删除成功' });
|
|||||||
console.error('AI 分析失败:', error);
|
console.error('AI 分析失败:', error);
|
||||||
return ctx.fail('AI 分析失败: ' + error.message);
|
return ctx.fail('AI 分析失败: ' + error.message);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /admin_api/resume/sync-online:
|
||||||
|
* post:
|
||||||
|
* summary: 同步在线简历
|
||||||
|
* description: 通过MQTT指令获取用户在线简历并更新到数据库
|
||||||
|
* tags: [后台-简历管理]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - resumeId
|
||||||
|
* properties:
|
||||||
|
* resumeId:
|
||||||
|
* type: string
|
||||||
|
* description: 简历ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 同步成功
|
||||||
|
*/
|
||||||
|
'POST /resume/sync-online': async (ctx) => {
|
||||||
|
const models = Framework.getModels();
|
||||||
|
const { resume_info } = models;
|
||||||
|
const { resumeId } = ctx.getBody();
|
||||||
|
|
||||||
|
if (!resumeId) {
|
||||||
|
return ctx.fail('简历ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resume = await resume_info.findOne({ where: { resumeId } });
|
||||||
|
|
||||||
|
if (!resume) {
|
||||||
|
return ctx.fail('简历不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sn_code, platform } = resume;
|
||||||
|
|
||||||
|
if (!sn_code) {
|
||||||
|
return ctx.fail('该简历未绑定设备SN码');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scheduleManager = require('../middleware/schedule');
|
||||||
|
const resumeManager = require('../middleware/job/resumeManager');
|
||||||
|
|
||||||
|
// 检查 MQTT 客户端是否已初始化
|
||||||
|
if (!scheduleManager.mqttClient) {
|
||||||
|
return ctx.fail('MQTT客户端未初始化,请检查调度系统是否正常启动');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查设备是否在线
|
||||||
|
// const deviceManager = require('../middleware/schedule/deviceManager');
|
||||||
|
// if (!deviceManager.isDeviceOnline(sn_code)) {
|
||||||
|
// return ctx.fail('设备离线,无法同步在线简历');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 调用简历管理器获取并保存简历
|
||||||
|
const resumeData = await resumeManager.get_online_resume(sn_code, scheduleManager.mqttClient, {
|
||||||
|
platform: platform || 'boss'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重新获取更新后的简历数据
|
||||||
|
const updatedResume = await resume_info.findOne({ where: { resumeId } });
|
||||||
|
if (!updatedResume) {
|
||||||
|
return ctx.fail('同步成功但未找到更新后的简历记录');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resumeDetail = updatedResume.toJSON();
|
||||||
|
|
||||||
|
// 解析 JSON 字段
|
||||||
|
const jsonFields = ['skills', 'certifications', 'projectExperience', 'workExperience', 'aiSkillTags'];
|
||||||
|
jsonFields.forEach(field => {
|
||||||
|
if (resumeDetail[field]) {
|
||||||
|
try {
|
||||||
|
resumeDetail[field] = JSON.parse(resumeDetail[field]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`解析字段 ${field} 失败:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx.success({
|
||||||
|
message: '同步在线简历成功',
|
||||||
|
data: resumeDetail
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步在线简历失败:', error);
|
||||||
|
return ctx.fail('同步在线简历失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,20 @@ module.exports = {
|
|||||||
where.feedbackStatus = seachOption.feedbackStatus;
|
where.feedbackStatus = seachOption.feedbackStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
console.log(seachOption.startTime, seachOption.endTime);
|
||||||
|
if (seachOption.startTime || seachOption.endTime) {
|
||||||
|
where.create_time = {};
|
||||||
|
if (seachOption.startTime) {
|
||||||
|
where.create_time[op.gte] = new Date(seachOption.startTime);
|
||||||
|
}
|
||||||
|
if (seachOption.endTime) {
|
||||||
|
const endTime = new Date(seachOption.endTime);
|
||||||
|
endTime.setHours(23, 59, 59, 999); // 设置为当天的最后一刻
|
||||||
|
where.create_time[op.lte] = endTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索:岗位名称、公司名称
|
// 搜索:岗位名称、公司名称
|
||||||
if (seachOption.key && seachOption.value) {
|
if (seachOption.key && seachOption.value) {
|
||||||
const key = seachOption.key;
|
const key = seachOption.key;
|
||||||
@@ -93,7 +107,7 @@ module.exports = {
|
|||||||
where,
|
where,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
order: [['applyTime', 'DESC']]
|
order: [['create_time', 'DESC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
return ctx.success({
|
return ctx.success({
|
||||||
@@ -109,7 +123,7 @@ module.exports = {
|
|||||||
* /api/apply/statistics:
|
* /api/apply/statistics:
|
||||||
* get:
|
* get:
|
||||||
* summary: 获取投递统计
|
* summary: 获取投递统计
|
||||||
* description: 根据设备SN码获取投递统计数据(包含今日、本周、本月统计)
|
* description: 根据设备SN码获取投递统计数据(包含今日、本周、本月统计),支持时间范围筛选
|
||||||
* tags: [前端-投递管理]
|
* tags: [前端-投递管理]
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: query
|
* - in: query
|
||||||
@@ -118,21 +132,50 @@ module.exports = {
|
|||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* description: 设备SN码
|
* description: 设备SN码
|
||||||
|
* - in: query
|
||||||
|
* name: startTime
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 开始时间(可选)
|
||||||
|
* - in: query
|
||||||
|
* name: endTime
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 结束时间(可选)
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: 获取成功
|
* description: 获取成功
|
||||||
*/
|
*/
|
||||||
'GET /apply/statistics': async (ctx) => {
|
'POST /apply/statistics': async (ctx) => {
|
||||||
const models = Framework.getModels();
|
const models = Framework.getModels();
|
||||||
const { apply_records, op } = models;
|
const { apply_records, op, job_postings } = models;
|
||||||
const { sn_code } = ctx.query;
|
const { sn_code, startTime, endTime } = ctx.getBody();
|
||||||
|
console.log(startTime, endTime);
|
||||||
const final_sn_code = sn_code;
|
const final_sn_code = sn_code;
|
||||||
|
|
||||||
if (!final_sn_code) {
|
if (!final_sn_code) {
|
||||||
return ctx.fail('请提供设备SN码');
|
return ctx.fail('请提供设备SN码');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算时间范围
|
// 构建基础查询条件
|
||||||
|
const baseWhere = { sn_code: final_sn_code };
|
||||||
|
|
||||||
|
// 如果提供了时间范围,则添加到查询条件中
|
||||||
|
if (startTime || endTime) {
|
||||||
|
baseWhere.create_time = {};
|
||||||
|
if (startTime) {
|
||||||
|
baseWhere.create_time[op.gte] = new Date(startTime);
|
||||||
|
}
|
||||||
|
if (endTime) {
|
||||||
|
const endTimeDate = new Date(endTime);
|
||||||
|
endTimeDate.setHours(23, 59, 59, 999); // 设置为当天的最后一刻
|
||||||
|
baseWhere.create_time[op.lte] = endTimeDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算时间范围(如果未提供时间范围,则使用默认的今日、本周、本月)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// 今天的开始时间(00:00:00)
|
// 今天的开始时间(00:00:00)
|
||||||
@@ -150,6 +193,8 @@ module.exports = {
|
|||||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
monthStart.setHours(0, 0, 0, 0);
|
monthStart.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
totalCount,
|
totalCount,
|
||||||
successCount,
|
successCount,
|
||||||
@@ -158,35 +203,44 @@ module.exports = {
|
|||||||
interviewCount,
|
interviewCount,
|
||||||
todayCount,
|
todayCount,
|
||||||
weekCount,
|
weekCount,
|
||||||
monthCount
|
monthCount,
|
||||||
|
totalJobCount
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// 总计
|
// 总计(如果提供了时间范围,则只统计该范围内的)
|
||||||
apply_records.count({ where: { sn_code: final_sn_code } }),
|
apply_records.count({ where: baseWhere }),
|
||||||
apply_records.count({ where: { sn_code: final_sn_code, applyStatus: 'success' } }),
|
apply_records.count({ where: { ...baseWhere, applyStatus: 'success' } }),
|
||||||
apply_records.count({ where: { sn_code: final_sn_code, applyStatus: 'failed' } }),
|
apply_records.count({ where: { ...baseWhere, applyStatus: 'failed' } }),
|
||||||
apply_records.count({ where: { sn_code: final_sn_code, applyStatus: 'pending' } }),
|
apply_records.count({ where: { ...baseWhere, applyStatus: 'pending' } }),
|
||||||
apply_records.count({ where: { sn_code: final_sn_code, feedbackStatus: 'interview' } }),
|
apply_records.count({ where: { ...baseWhere, feedbackStatus: 'interview' } }),
|
||||||
// 今日
|
|
||||||
apply_records.count({
|
// 今日(如果提供了时间范围,则返回0,否则统计今日)
|
||||||
|
startTime || endTime ? 0 : apply_records.count({
|
||||||
where: {
|
where: {
|
||||||
sn_code: final_sn_code,
|
sn_code: final_sn_code,
|
||||||
applyTime: { [op.gte]: todayStart }
|
create_time: { [op.gte]: todayStart }
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// 本周
|
// 本周(如果提供了时间范围,则返回0,否则统计本周)
|
||||||
apply_records.count({
|
startTime || endTime ? 0 : apply_records.count({
|
||||||
where: {
|
where: {
|
||||||
sn_code: final_sn_code,
|
sn_code: final_sn_code,
|
||||||
applyTime: { [op.gte]: weekStart }
|
create_time: { [op.gte]: weekStart }
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// 本月
|
// 本月(如果提供了时间范围,则返回0,否则统计本月)
|
||||||
apply_records.count({
|
startTime || endTime ? 0 : apply_records.count({
|
||||||
where: {
|
where: {
|
||||||
sn_code: final_sn_code,
|
sn_code: final_sn_code,
|
||||||
applyTime: { [op.gte]: monthStart }
|
create_time: { [op.gte]: monthStart }
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
// 总职位数
|
||||||
|
job_postings.count({
|
||||||
|
where: {
|
||||||
|
sn_code: final_sn_code,
|
||||||
|
create_time: { [op.gte]: todayStart }
|
||||||
|
}
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return ctx.success({
|
return ctx.success({
|
||||||
@@ -198,6 +252,7 @@ module.exports = {
|
|||||||
todayCount,
|
todayCount,
|
||||||
weekCount,
|
weekCount,
|
||||||
monthCount,
|
monthCount,
|
||||||
|
totalJobCount,
|
||||||
successRate: totalCount > 0 ? ((successCount / totalCount) * 100).toFixed(2) : 0,
|
successRate: totalCount > 0 ? ((successCount / totalCount) * 100).toFixed(2) : 0,
|
||||||
interviewRate: totalCount > 0 ? ((interviewCount / totalCount) * 100).toFixed(2) : 0
|
interviewRate: totalCount > 0 ? ((interviewCount / totalCount) * 100).toFixed(2) : 0
|
||||||
});
|
});
|
||||||
@@ -279,12 +334,12 @@ module.exports = {
|
|||||||
const records = await apply_records.findAll({
|
const records = await apply_records.findAll({
|
||||||
where: {
|
where: {
|
||||||
sn_code: sn_code,
|
sn_code: sn_code,
|
||||||
applyTime: {
|
create_time: {
|
||||||
[op.gte]: sevenDaysAgo,
|
[op.gte]: sevenDaysAgo,
|
||||||
[op.lte]: today
|
[op.lte]: today
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
attributes: ['applyTime'],
|
attributes: ['create_time'],
|
||||||
raw: true
|
raw: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -299,7 +354,7 @@ module.exports = {
|
|||||||
|
|
||||||
// 统计当天的投递数量
|
// 统计当天的投递数量
|
||||||
const count = records.filter(record => {
|
const count = records.filter(record => {
|
||||||
const recordDate = new Date(record.applyTime);
|
const recordDate = new Date(record.create_time);
|
||||||
recordDate.setHours(0, 0, 0, 0);
|
recordDate.setHours(0, 0, 0, 0);
|
||||||
return recordDate.getTime() === date.getTime();
|
return recordDate.getTime() === date.getTime();
|
||||||
}).length;
|
}).length;
|
||||||
|
|||||||
@@ -88,84 +88,47 @@ module.exports = {
|
|||||||
* description: 获取成功
|
* description: 获取成功
|
||||||
*/
|
*/
|
||||||
'GET /config/pricing-plans': async (ctx) => {
|
'GET /config/pricing-plans': async (ctx) => {
|
||||||
try {
|
|
||||||
// 写死4条价格套餐数据
|
const models = Framework.getModels();
|
||||||
// 价格计算规则:2小时 = 1天
|
const { pricing_plans } = models;
|
||||||
const pricingPlans = [
|
|
||||||
{
|
// 查询所有启用且未删除的套餐,按排序顺序返回
|
||||||
id: 1,
|
const plans = await pricing_plans.findAll({
|
||||||
name: '体验套餐',
|
where: {
|
||||||
duration: '7天',
|
is_active: 1,
|
||||||
days: 7,
|
is_delete: 0
|
||||||
price: 28,
|
|
||||||
originalPrice: 28,
|
|
||||||
unit: '元',
|
|
||||||
features: [
|
|
||||||
'7天使用权限',
|
|
||||||
'全功能体验',
|
|
||||||
'技术支持'
|
|
||||||
],
|
|
||||||
featured: false
|
|
||||||
},
|
},
|
||||||
{
|
order: [['sort_order', 'ASC'], ['id', 'ASC']],
|
||||||
id: 2,
|
attributes: [
|
||||||
name: '月度套餐',
|
'id', 'name', 'duration', 'days', 'price',
|
||||||
duration: '30天',
|
'original_price', 'unit', 'discount', 'features', 'featured'
|
||||||
days: 30,
|
]
|
||||||
price: 99,
|
});
|
||||||
originalPrice: 120,
|
|
||||||
unit: '元',
|
// 转换数据格式以匹配前端期望
|
||||||
discount: '约8.3折',
|
const pricingPlans = plans.map(plan => {
|
||||||
features: [
|
const planData = plan.toJSON();
|
||||||
'30天使用权限',
|
|
||||||
'全功能使用',
|
// 重命名字段以匹配前端期望(camelCase)
|
||||||
'优先技术支持',
|
if (planData.original_price !== null) {
|
||||||
'性价比最高'
|
planData.originalPrice = planData.original_price;
|
||||||
],
|
|
||||||
featured: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '季度套餐',
|
|
||||||
duration: '90天',
|
|
||||||
days: 90,
|
|
||||||
price: 269,
|
|
||||||
originalPrice: 360,
|
|
||||||
unit: '元',
|
|
||||||
discount: '7.5折',
|
|
||||||
features: [
|
|
||||||
'90天使用权限',
|
|
||||||
'全功能使用',
|
|
||||||
'优先技术支持',
|
|
||||||
'更优惠价格'
|
|
||||||
],
|
|
||||||
featured: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: '终生套餐',
|
|
||||||
duration: '永久',
|
|
||||||
days: -1,
|
|
||||||
price: 888,
|
|
||||||
originalPrice: null,
|
|
||||||
unit: '元',
|
|
||||||
discount: '超值',
|
|
||||||
features: [
|
|
||||||
'永久使用权限',
|
|
||||||
'全功能使用',
|
|
||||||
'终身技术支持',
|
|
||||||
'一次购买,终身使用',
|
|
||||||
'最划算选择'
|
|
||||||
],
|
|
||||||
featured: false
|
|
||||||
}
|
}
|
||||||
];
|
delete planData.original_price;
|
||||||
|
|
||||||
|
if (planData.features) {
|
||||||
|
planData.features = JSON.parse(planData.features);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换 featured 为布尔值
|
||||||
|
planData.featured = planData.featured === 1;
|
||||||
|
|
||||||
|
return planData;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return ctx.success(pricingPlans);
|
return ctx.success(pricingPlans);
|
||||||
} catch (error) {
|
|
||||||
console.error('获取价格套餐失败:', error);
|
|
||||||
return ctx.fail('获取价格套餐失败: ' + error.message);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -316,41 +316,118 @@ class ResumeManager {
|
|||||||
|
|
||||||
const resume_info = db.getModel('resume_info');
|
const resume_info = db.getModel('resume_info');
|
||||||
|
|
||||||
|
// 解析 JSON 字段
|
||||||
|
let skillsArray = [];
|
||||||
|
let workExpArray = [];
|
||||||
|
let projectExpArray = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (resumeInfo.skills) {
|
||||||
|
skillsArray = typeof resumeInfo.skills === 'string' ? JSON.parse(resumeInfo.skills) : resumeInfo.skills;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[简历管理] 解析技能字段失败:`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (resumeInfo.workExperience) {
|
||||||
|
workExpArray = typeof resumeInfo.workExperience === 'string' ? JSON.parse(resumeInfo.workExperience) : resumeInfo.workExperience;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[简历管理] 解析工作经历字段失败:`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (resumeInfo.projectExperience) {
|
||||||
|
projectExpArray = typeof resumeInfo.projectExperience === 'string' ? JSON.parse(resumeInfo.projectExperience) : resumeInfo.projectExperience;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[简历管理] 解析项目经历字段失败:`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建工作经历文本
|
||||||
|
const workExpText = workExpArray.map((work, index) => {
|
||||||
|
return `${index + 1}. ${work.company || ''} - ${work.position || ''} (${work.startDate || ''} ~ ${work.endDate || '至今'})
|
||||||
|
工作内容:${work.content || ''}
|
||||||
|
行业:${work.industry || ''}`;
|
||||||
|
}).join('\n\n');
|
||||||
|
|
||||||
|
// 构建项目经历文本
|
||||||
|
const projectExpText = projectExpArray.map((project, index) => {
|
||||||
|
return `${index + 1}. ${project.name || ''} - ${project.role || ''} (${project.startDate || ''} ~ ${project.endDate || '至今'})
|
||||||
|
项目描述:${project.description || ''}
|
||||||
|
项目成果:${project.performance || ''}`;
|
||||||
|
}).join('\n\n');
|
||||||
|
|
||||||
// 构建分析提示词
|
// 构建分析提示词
|
||||||
const prompt = `请分析以下简历,提供专业的评估:
|
const prompt = `请分析以下简历,提供专业的评估:
|
||||||
|
|
||||||
姓名:${resumeInfo.fullName}
|
【基本信息】
|
||||||
工作年限:${resumeInfo.workYears}
|
姓名:${resumeInfo.fullName || ''}
|
||||||
当前职位:${resumeInfo.currentPosition}
|
性别:${resumeInfo.gender || ''}
|
||||||
期望职位:${resumeInfo.expectedPosition}
|
年龄:${resumeInfo.age || ''}
|
||||||
期望薪资:${resumeInfo.expectedSalary}
|
所在地:${resumeInfo.location || ''}
|
||||||
学历:${resumeInfo.education}
|
工作年限:${resumeInfo.workYears || ''}
|
||||||
技能:${resumeInfo.skills}
|
|
||||||
|
|
||||||
个人优势:
|
【教育背景】
|
||||||
${resumeInfo.skillDescription}
|
学历:${resumeInfo.education || ''}
|
||||||
|
专业:${resumeInfo.major || ''}
|
||||||
|
毕业院校:${resumeInfo.school || ''}
|
||||||
|
毕业年份:${resumeInfo.graduationYear || ''}
|
||||||
|
|
||||||
请从以下几个方面进行分析:
|
【当前状态】
|
||||||
1. 核心技能标签(提取5-10个关键技能)
|
当前职位:${resumeInfo.currentPosition || ''}
|
||||||
2. 优势分析(100字以内)
|
当前公司:${resumeInfo.currentCompany || ''}
|
||||||
3. 劣势分析(100字以内)
|
当前薪资:${resumeInfo.currentSalary || ''}
|
||||||
4. 职业建议(150字以内)
|
|
||||||
5. 竞争力评分(0-100分)`;
|
【求职期望】
|
||||||
|
期望职位:${resumeInfo.expectedPosition || ''}
|
||||||
|
期望薪资:${resumeInfo.expectedSalary || ''}
|
||||||
|
期望地点:${resumeInfo.expectedLocation || ''}
|
||||||
|
期望行业:${resumeInfo.expectedIndustry || ''}
|
||||||
|
|
||||||
|
【技能标签】
|
||||||
|
${skillsArray.length > 0 ? skillsArray.join('、') : '无'}
|
||||||
|
|
||||||
|
【个人优势描述】
|
||||||
|
${resumeInfo.skillDescription || ''}
|
||||||
|
|
||||||
|
【工作经历】
|
||||||
|
${workExpText || '无'}
|
||||||
|
|
||||||
|
【项目经历】
|
||||||
|
${projectExpText || '无'}
|
||||||
|
|
||||||
|
请从以下几个方面进行专业分析,并返回 JSON 格式结果:
|
||||||
|
1. skillTags: 核心技能标签数组(提取5-10个最关键的技术技能,如:Vue、React、Node.js等)
|
||||||
|
2. strengths: 优势分析(100字以内,基于工作经历、项目经历、技能水平等综合评估)
|
||||||
|
3. weaknesses: 劣势分析(100字以内,指出需要改进的地方或不足)
|
||||||
|
4. careerSuggestion: 职业建议(150字以内,基于期望职位和当前能力给出职业发展建议)
|
||||||
|
5. competitiveness: 竞争力评分(0-100的整数,综合考虑工作年限、技能深度、项目经验、学历等因素)
|
||||||
|
|
||||||
|
要求:
|
||||||
|
- 竞争力评分要客观公正,基于实际能力评估
|
||||||
|
- 优势分析要突出核心亮点和技术能力
|
||||||
|
- 劣势分析要指出真实存在的问题
|
||||||
|
- 职业建议要具有针对性和可操作性`;
|
||||||
|
|
||||||
try {
|
|
||||||
// 调用AI服务进行分析
|
// 调用AI服务进行分析
|
||||||
const aiAnalysis = await aiService.analyzeResume(prompt);
|
const aiAnalysis = await aiService.analyzeResume(prompt);
|
||||||
|
|
||||||
// 解析AI返回的结果
|
// 解析AI返回的结果,如果AI没返回有效数据,直接抛出错误
|
||||||
const analysis = this.parse_ai_analysis(aiAnalysis, resumeInfo);
|
const analysis = this.parse_ai_analysis(aiAnalysis, resumeInfo);
|
||||||
|
|
||||||
|
if (!analysis) {
|
||||||
|
throw new Error('AI分析结果为空,无法处理');
|
||||||
|
}
|
||||||
|
|
||||||
// 确保所有字段都有值
|
// 确保所有字段都有值
|
||||||
const updateData = {
|
const updateData = {
|
||||||
aiSkillTags: JSON.stringify(analysis.skillTags || []),
|
aiSkillTags: JSON.stringify(analysis.skillTags || []),
|
||||||
aiStrengths: analysis.strengths || '',
|
aiStrengths: analysis.strengths || '',
|
||||||
aiWeaknesses: analysis.weaknesses || '',
|
aiWeaknesses: analysis.weaknesses || '',
|
||||||
aiCareerSuggestion: analysis.careerSuggestion || '',
|
aiCareerSuggestion: analysis.careerSuggestion || '',
|
||||||
aiCompetitiveness: parseInt(analysis.competitiveness || 70, 10)
|
aiCompetitiveness: parseInt(analysis.competitiveness || 0, 10)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确保竞争力评分在 0-100 范围内
|
// 确保竞争力评分在 0-100 范围内
|
||||||
@@ -363,105 +440,44 @@ ${resumeInfo.skillDescription}
|
|||||||
console.log(`[简历管理] AI分析完成 - 竞争力评分: ${updateData.aiCompetitiveness}, 技能标签: ${updateData.aiSkillTags}`);
|
console.log(`[简历管理] AI分析完成 - 竞争力评分: ${updateData.aiCompetitiveness}, 技能标签: ${updateData.aiSkillTags}`);
|
||||||
|
|
||||||
return analysis;
|
return analysis;
|
||||||
} catch (error) {
|
|
||||||
console.error(`[简历管理] AI分析失败:`, error, {
|
|
||||||
resumeId: resumeId,
|
|
||||||
fullName: resumeInfo.fullName
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果AI分析失败,使用基于规则的默认分析,并保存到数据库
|
|
||||||
const defaultAnalysis = this.get_default_analysis(resumeInfo);
|
|
||||||
|
|
||||||
// 保存默认分析结果到数据库
|
|
||||||
const updateData = {
|
|
||||||
aiSkillTags: JSON.stringify(defaultAnalysis.skillTags || []),
|
|
||||||
aiStrengths: defaultAnalysis.strengths || '',
|
|
||||||
aiWeaknesses: defaultAnalysis.weaknesses || '',
|
|
||||||
aiCareerSuggestion: defaultAnalysis.careerSuggestion || '',
|
|
||||||
aiCompetitiveness: parseInt(defaultAnalysis.competitiveness || 70, 10)
|
|
||||||
};
|
|
||||||
|
|
||||||
// 确保竞争力评分在 0-100 范围内
|
|
||||||
if (updateData.aiCompetitiveness < 0) updateData.aiCompetitiveness = 0;
|
|
||||||
if (updateData.aiCompetitiveness > 100) updateData.aiCompetitiveness = 100;
|
|
||||||
|
|
||||||
await resume_info.update(updateData, { where: { resumeId: resumeId } });
|
|
||||||
|
|
||||||
console.log(`[简历管理] 使用默认分析结果 - 竞争力评分: ${updateData.aiCompetitiveness}`);
|
|
||||||
|
|
||||||
return defaultAnalysis;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析AI分析结果
|
* 解析AI分析结果
|
||||||
* @param {object} aiResponse - AI响应对象
|
* @param {object} aiResponse - AI响应对象
|
||||||
* @param {object} resumeInfo - 简历信息
|
* @param {object} resumeInfo - 简历信息
|
||||||
* @returns {object} 解析后的分析结果
|
* @returns {object|null} 解析后的分析结果,如果AI没返回有效数据则返回null
|
||||||
*/
|
*/
|
||||||
parse_ai_analysis(aiResponse, resumeInfo) {
|
parse_ai_analysis(aiResponse, resumeInfo) {
|
||||||
try {
|
|
||||||
// aiService.analyzeResume 返回格式: { analysis: {...} } 或 { analysis: { content: "...", parseError: true } }
|
// aiService.analyzeResume 返回格式: { analysis: {...} } 或 { analysis: { content: "...", parseError: true } }
|
||||||
const analysis = aiResponse.analysis;
|
const analysis = aiResponse?.analysis;
|
||||||
|
|
||||||
// 如果解析失败,analysis 会有 parseError 标记
|
// 如果解析失败,analysis 会有 parseError 标记,直接返回 null
|
||||||
if (analysis && analysis.parseError) {
|
if (!analysis || analysis.parseError) {
|
||||||
console.warn(`[简历管理] AI分析结果解析失败,使用默认分析`);
|
console.warn(`[简历管理] AI分析结果解析失败或为空`);
|
||||||
return this.get_default_analysis(resumeInfo);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果解析成功,analysis 直接是解析后的对象
|
// 如果解析成功,analysis 直接是解析后的对象
|
||||||
if (analysis && typeof analysis === 'object' && !analysis.parseError) {
|
if (typeof analysis === 'object' && !analysis.parseError) {
|
||||||
|
// 检查是否有必要的字段
|
||||||
|
if (analysis.competitiveness === undefined && analysis.竞争力评分 === undefined) {
|
||||||
|
console.warn(`[简历管理] AI分析结果缺少竞争力评分字段`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
skillTags: analysis.skillTags || analysis.技能标签 || [],
|
skillTags: analysis.skillTags || analysis.技能标签 || [],
|
||||||
strengths: analysis.strengths || analysis.优势 || analysis.优势分析 || '',
|
strengths: analysis.strengths || analysis.优势 || analysis.优势分析 || '',
|
||||||
weaknesses: analysis.weaknesses || analysis.劣势 || analysis.劣势分析 || '',
|
weaknesses: analysis.weaknesses || analysis.劣势 || analysis.劣势分析 || '',
|
||||||
careerSuggestion: analysis.careerSuggestion || analysis.职业建议 || '',
|
careerSuggestion: analysis.careerSuggestion || analysis.职业建议 || '',
|
||||||
competitiveness: parseInt(analysis.competitiveness || analysis.竞争力评分 || 70, 10)
|
competitiveness: parseInt(analysis.competitiveness || analysis.竞争力评分 || 0, 10)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果 analysis 是字符串,尝试解析
|
// 如果格式不对,返回 null
|
||||||
const content = analysis?.content || analysis || '';
|
console.warn(`[简历管理] AI分析结果格式不正确`);
|
||||||
if (typeof content === 'string' && content.includes('{') && content.includes('}')) {
|
return null;
|
||||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
||||||
if (jsonMatch) {
|
|
||||||
const parsed = JSON.parse(jsonMatch[0]);
|
|
||||||
return {
|
|
||||||
skillTags: parsed.skillTags || parsed.技能标签 || [],
|
|
||||||
strengths: parsed.strengths || parsed.优势 || parsed.优势分析 || '',
|
|
||||||
weaknesses: parsed.weaknesses || parsed.劣势 || parsed.劣势分析 || '',
|
|
||||||
careerSuggestion: parsed.careerSuggestion || parsed.职业建议 || '',
|
|
||||||
competitiveness: parseInt(parsed.competitiveness || parsed.竞争力评分 || 70, 10)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果无法解析JSON,尝试从文本中提取信息
|
|
||||||
if (typeof content === 'string') {
|
|
||||||
const skillTagsMatch = content.match(/技能标签[::](.*?)(?:\n|$)/);
|
|
||||||
const strengthsMatch = content.match(/优势[分析]*[::](.*?)(?:\n|劣势)/s);
|
|
||||||
const weaknessesMatch = content.match(/劣势[分析]*[::](.*?)(?:\n|职业)/s);
|
|
||||||
const suggestionMatch = content.match(/职业建议[::](.*?)(?:\n|竞争力)/s);
|
|
||||||
const scoreMatch = content.match(/竞争力评分[::](\d+)/);
|
|
||||||
|
|
||||||
return {
|
|
||||||
skillTags: skillTagsMatch ? skillTagsMatch[1].split(/[,,、]/).map(s => s.trim()) : [],
|
|
||||||
strengths: strengthsMatch ? strengthsMatch[1].trim() : '',
|
|
||||||
weaknesses: weaknessesMatch ? weaknessesMatch[1].trim() : '',
|
|
||||||
careerSuggestion: suggestionMatch ? suggestionMatch[1].trim() : '',
|
|
||||||
competitiveness: scoreMatch ? parseInt(scoreMatch[1], 10) : 70
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果所有解析都失败,使用默认分析
|
|
||||||
console.warn(`[简历管理] 无法解析AI分析结果,使用默认分析`);
|
|
||||||
return this.get_default_analysis(resumeInfo);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[简历管理] 解析AI分析结果失败:`, error);
|
|
||||||
// 解析失败时使用默认分析
|
|
||||||
return this.get_default_analysis(resumeInfo);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -156,23 +156,6 @@ class DeviceWorkStatusNotifier {
|
|||||||
await mqttClient.publish(topic, message);
|
await mqttClient.publish(topic, message);
|
||||||
|
|
||||||
|
|
||||||
if (workStatus.currentActivity) {
|
|
||||||
const activity = workStatus.currentActivity;
|
|
||||||
const activityInfo = activity.type === 'command'
|
|
||||||
? `指令[${activity.name}]`
|
|
||||||
: `任务[${activity.name}]`;
|
|
||||||
console.log(` - 当前活动: ${activityInfo}`);
|
|
||||||
} else {
|
|
||||||
console.log(` - 当前活动: 无`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workStatus.waitingMessage) {
|
|
||||||
console.log(` - 等待消息: ${workStatus.waitingMessage.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` - 待执行: ${workStatus.pendingQueue.totalCount}个`);
|
|
||||||
console.log(` - 下次执行: ${workStatus.pendingQueue.nextExecuteTimeText}`);
|
|
||||||
console.log(` - 消息大小: ${message.length} 字节`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 通知失败不影响任务执行,只记录日志
|
// 通知失败不影响任务执行,只记录日志
|
||||||
console.warn(`[设备工作状态] 推送失败:`, error.message);
|
console.warn(`[设备工作状态] 推送失败:`, error.message);
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ class ScheduleManager {
|
|||||||
console.log('[调度管理器] 心跳监听已启动');
|
console.log('[调度管理器] 心跳监听已启动');
|
||||||
|
|
||||||
// 5. 启动定时任务
|
// 5. 启动定时任务
|
||||||
// this.scheduledJobs.start();
|
this.scheduledJobs.start();
|
||||||
// console.log('[调度管理器] 定时任务已启动');
|
console.log('[调度管理器] 定时任务已启动');
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
|
|
||||||
|
|||||||
97
api/model/pricing_plans.js
Normal file
97
api/model/pricing_plans.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
const Sequelize = require('sequelize');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 价格套餐表模型
|
||||||
|
* 存储各种价格套餐的配置信息,支持管理员在后台配置和管理
|
||||||
|
*/
|
||||||
|
module.exports = (db) => {
|
||||||
|
const pricing_plans = db.define("pricing_plans", {
|
||||||
|
name: {
|
||||||
|
comment: '套餐名称(如:体验套餐、月度套餐等)',
|
||||||
|
type: Sequelize.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
comment: '时长描述(如:7天、30天、永久)',
|
||||||
|
type: Sequelize.STRING(50),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: ''
|
||||||
|
},
|
||||||
|
days: {
|
||||||
|
comment: '天数(-1表示永久,0表示无限制)',
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
comment: '售价(元)',
|
||||||
|
type: Sequelize.DECIMAL(10, 2),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0.00
|
||||||
|
},
|
||||||
|
original_price: {
|
||||||
|
comment: '原价(元),可为空表示无原价',
|
||||||
|
type: Sequelize.DECIMAL(10, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
},
|
||||||
|
unit: {
|
||||||
|
comment: '价格单位',
|
||||||
|
type: Sequelize.STRING(20),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: '元'
|
||||||
|
},
|
||||||
|
discount: {
|
||||||
|
comment: '折扣描述(如:8.3折、超值)',
|
||||||
|
type: Sequelize.STRING(50),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
comment: '功能特性列表(JSON字符串数组)',
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: '[]'
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
comment: '是否为推荐套餐(1=推荐,0=普通)',
|
||||||
|
type: Sequelize.TINYINT(1),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
is_active: {
|
||||||
|
comment: '是否启用(1=启用,0=禁用)',
|
||||||
|
type: Sequelize.TINYINT(1),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 1
|
||||||
|
},
|
||||||
|
sort_order: {
|
||||||
|
comment: '排序顺序(越小越靠前)',
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
}, {
|
||||||
|
timestamps: false,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
unique: false,
|
||||||
|
fields: ['is_active']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unique: false,
|
||||||
|
fields: ['is_delete']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unique: false,
|
||||||
|
fields: ['sort_order']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// pricing_plans.sync({ force: true });
|
||||||
|
|
||||||
|
return pricing_plans;
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
import{a as t}from"./index-CMiak5Ls.js";class s{async getConfig(r){try{return await t.post("/user/delivery-config/get",{sn_code:r})}catch(e){throw console.error("获取投递配置失败:",e),e}}async saveConfig(r,e){try{return await t.post("/user/delivery-config/save",{sn_code:r,deliver_config:e})}catch(o){throw console.error("保存投递配置失败:",o),o}}}const i=new s;export{i as default};
|
import{a as t}from"./index-CsHwYKwf.js";class s{async getConfig(r){try{return await t.post("/user/delivery-config/get",{sn_code:r})}catch(e){throw console.error("获取投递配置失败:",e),e}}async saveConfig(r,e){try{return await t.post("/user/delivery-config/save",{sn_code:r,deliver_config:e})}catch(o){throw console.error("保存投递配置失败:",o),o}}}const i=new s;export{i as default};
|
||||||
1
app/assets/index-BUzIVj1g.css
Normal file
1
app/assets/index-BUzIVj1g.css
Normal file
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
@@ -1 +0,0 @@
|
|||||||
import{a as t}from"./index-Cia_UppJ.js";class s{async getConfig(r){try{return await t.post("/user/delivery-config/get",{sn_code:r})}catch(e){throw console.error("获取投递配置失败:",e),e}}async saveConfig(r,e){try{return await t.post("/user/delivery-config/save",{sn_code:r,deliver_config:e})}catch(o){throw console.error("保存投递配置失败:",o),o}}}const i=new s;export{i as default};
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 334 KiB |
Binary file not shown.
Binary file not shown.
@@ -1,32 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>boss - 远程监听服务</title>
|
|
||||||
<script type="module" crossorigin src="/app/assets/index-Cia_UppJ.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/app/assets/index--P_P-eHg.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<!-- 启动加载动画 -->
|
|
||||||
<div id="loading-screen" class="loading-screen">
|
|
||||||
<div class="loading-content">
|
|
||||||
<div class="loading-logo">
|
|
||||||
<div class="logo-circle"></div>
|
|
||||||
</div>
|
|
||||||
<div class="loading-text">正在启动...</div>
|
|
||||||
<div class="loading-progress">
|
|
||||||
<div class="progress-bar-animated"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Vue 应用挂载点 -->
|
|
||||||
<div id="app" ></div>
|
|
||||||
|
|
||||||
<!-- 在 body 底部加载 Vue 应用脚本 -->
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>boss - 远程监听服务</title>
|
<title>boss - 远程监听服务</title>
|
||||||
<script type="module" crossorigin src="/app/assets/index-CMiak5Ls.js"></script>
|
<script type="module" crossorigin src="/app/assets/index-CsHwYKwf.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/app/assets/index-DDAU-aR1.css">
|
<link rel="stylesheet" crossorigin href="/app/assets/index-BUzIVj1g.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ module.exports = {
|
|||||||
acquire: 30000,
|
acquire: 30000,
|
||||||
idle: 10000
|
idle: 10000
|
||||||
},
|
},
|
||||||
logging: false
|
logging: true
|
||||||
},
|
},
|
||||||
|
|
||||||
// API 路径配置(必需)
|
// API 路径配置(必需)
|
||||||
|
|||||||
138
scripts/add_pricing_plans_menu.js
Normal file
138
scripts/add_pricing_plans_menu.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 添加"价格套餐管理"菜单项到用户管理菜单下
|
||||||
|
* 执行 SQL 插入操作
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Framework = require('../framework/node-core-framework.js');
|
||||||
|
const frameworkConfig = require('../config/framework.config.js');
|
||||||
|
|
||||||
|
async function addPricingPlansMenu() {
|
||||||
|
console.log('🔄 开始添加"价格套餐管理"菜单项...\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 初始化框架
|
||||||
|
console.log('正在初始化框架...');
|
||||||
|
const framework = await Framework.init(frameworkConfig);
|
||||||
|
const models = Framework.getModels();
|
||||||
|
|
||||||
|
if (!models) {
|
||||||
|
throw new Error('无法获取模型列表');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从任意模型获取 sequelize 实例
|
||||||
|
const Sequelize = require('sequelize');
|
||||||
|
const firstModel = Object.values(models)[0];
|
||||||
|
if (!firstModel || !firstModel.sequelize) {
|
||||||
|
throw new Error('无法获取数据库连接');
|
||||||
|
}
|
||||||
|
const sequelize = firstModel.sequelize;
|
||||||
|
|
||||||
|
// 查找用户管理菜单的ID
|
||||||
|
const [userMenu] = await sequelize.query(
|
||||||
|
`SELECT id FROM sys_menu WHERE name = '用户管理' AND parent_id = 0 AND is_delete = 0 LIMIT 1`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
let parentId = 120; // 默认 fallback 值
|
||||||
|
if (userMenu && userMenu.id) {
|
||||||
|
parentId = userMenu.id;
|
||||||
|
console.log(`找到用户管理菜单,ID: ${parentId}`);
|
||||||
|
} else {
|
||||||
|
console.log(`未找到用户管理菜单,使用默认 parent_id: ${parentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
const [existing] = await sequelize.query(
|
||||||
|
`SELECT id, name FROM sys_menu WHERE path = 'pricing_plans' AND is_delete = 0`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
console.log(`⚠️ 菜单项已存在 (ID: ${existing.id}, 名称: ${existing.name})`);
|
||||||
|
console.log('✅ 无需重复添加\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最大排序值
|
||||||
|
const [maxSort] = await sequelize.query(
|
||||||
|
`SELECT MAX(sort) as maxSort FROM sys_menu WHERE parent_id = ${parentId} AND is_delete = 0`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const nextSort = (maxSort && maxSort.maxSort ? maxSort.maxSort : 0) + 1;
|
||||||
|
|
||||||
|
// 执行插入
|
||||||
|
await sequelize.query(
|
||||||
|
`INSERT INTO sys_menu (
|
||||||
|
name,
|
||||||
|
parent_id,
|
||||||
|
model_id,
|
||||||
|
form_id,
|
||||||
|
icon,
|
||||||
|
path,
|
||||||
|
component,
|
||||||
|
api_path,
|
||||||
|
is_show_menu,
|
||||||
|
is_show,
|
||||||
|
type,
|
||||||
|
sort,
|
||||||
|
create_time,
|
||||||
|
last_modify_time,
|
||||||
|
is_delete
|
||||||
|
) VALUES (
|
||||||
|
'价格套餐管理',
|
||||||
|
${parentId},
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'md-pricetags',
|
||||||
|
'pricing_plans',
|
||||||
|
'system/pricing_plans.vue',
|
||||||
|
'system/pricing_plans_server.js',
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'页面',
|
||||||
|
${nextSort},
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
0
|
||||||
|
)`,
|
||||||
|
{ type: Sequelize.QueryTypes.INSERT }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('✅ "价格套餐管理"菜单项添加成功!\n');
|
||||||
|
|
||||||
|
// 验证插入结果
|
||||||
|
const [menu] = await sequelize.query(
|
||||||
|
`SELECT id, name, parent_id, path, component, api_path, sort
|
||||||
|
FROM sys_menu
|
||||||
|
WHERE path = 'pricing_plans' AND is_delete = 0`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (menu) {
|
||||||
|
console.log('📋 菜单项详情:');
|
||||||
|
console.log(` ID: ${menu.id}`);
|
||||||
|
console.log(` 名称: ${menu.name}`);
|
||||||
|
console.log(` 父菜单ID: ${menu.parent_id}`);
|
||||||
|
console.log(` 路由路径: ${menu.path}`);
|
||||||
|
console.log(` 组件路径: ${menu.component}`);
|
||||||
|
console.log(` API路径: ${menu.api_path}`);
|
||||||
|
console.log(` 排序: ${menu.sort}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 添加失败:', error.message);
|
||||||
|
console.error('\n详细错误:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行添加
|
||||||
|
addPricingPlansMenu()
|
||||||
|
.then(() => {
|
||||||
|
console.log('✨ 操作完成!');
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('\n💥 执行失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
135
scripts/migrate_pricing_plans_data.js
Normal file
135
scripts/migrate_pricing_plans_data.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* 迁移现有价格套餐数据到数据库
|
||||||
|
* 将 config.js 中硬编码的 4 个套餐数据导入到 pricing_plans 表
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Framework = require('../framework/node-core-framework.js');
|
||||||
|
const frameworkConfig = require('../config/framework.config.js');
|
||||||
|
|
||||||
|
async function migratePricingPlans() {
|
||||||
|
console.log('🔄 开始迁移价格套餐数据...\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 初始化框架
|
||||||
|
console.log('正在初始化框架...');
|
||||||
|
const framework = await Framework.init(frameworkConfig);
|
||||||
|
const models = Framework.getModels();
|
||||||
|
|
||||||
|
if (!models || !models.pricing_plans) {
|
||||||
|
throw new Error('无法获取 pricing_plans 模型');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pricing_plans } = models;
|
||||||
|
|
||||||
|
// 检查是否已有数据
|
||||||
|
const existingCount = await pricing_plans.count({ where: { is_delete: 0 } });
|
||||||
|
if (existingCount > 0) {
|
||||||
|
console.log(`⚠️ 已存在 ${existingCount} 条套餐数据,跳过迁移\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 现有的4个套餐数据(来自 api/controller_front/config.js)
|
||||||
|
const plans = [
|
||||||
|
{
|
||||||
|
name: '体验套餐',
|
||||||
|
duration: '7天',
|
||||||
|
days: 7,
|
||||||
|
price: 28.00,
|
||||||
|
original_price: 28.00,
|
||||||
|
unit: '元',
|
||||||
|
discount: null,
|
||||||
|
features: JSON.stringify(['7天使用权限', '全功能体验', '技术支持']),
|
||||||
|
featured: 0,
|
||||||
|
is_active: 1,
|
||||||
|
sort_order: 1,
|
||||||
|
is_delete: 0,
|
||||||
|
create_time: new Date(),
|
||||||
|
last_modify_time: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '月度套餐',
|
||||||
|
duration: '30天',
|
||||||
|
days: 30,
|
||||||
|
price: 99.00,
|
||||||
|
original_price: 120.00,
|
||||||
|
unit: '元',
|
||||||
|
discount: '约8.3折',
|
||||||
|
features: JSON.stringify(['30天使用权限', '全功能使用', '优先技术支持', '性价比最高']),
|
||||||
|
featured: 1,
|
||||||
|
is_active: 1,
|
||||||
|
sort_order: 2,
|
||||||
|
is_delete: 0,
|
||||||
|
create_time: new Date(),
|
||||||
|
last_modify_time: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '季度套餐',
|
||||||
|
duration: '90天',
|
||||||
|
days: 90,
|
||||||
|
price: 269.00,
|
||||||
|
original_price: 360.00,
|
||||||
|
unit: '元',
|
||||||
|
discount: '7.5折',
|
||||||
|
features: JSON.stringify(['90天使用权限', '全功能使用', '优先技术支持', '更优惠价格']),
|
||||||
|
featured: 0,
|
||||||
|
is_active: 1,
|
||||||
|
sort_order: 3,
|
||||||
|
is_delete: 0,
|
||||||
|
create_time: new Date(),
|
||||||
|
last_modify_time: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '终生套餐',
|
||||||
|
duration: '永久',
|
||||||
|
days: -1,
|
||||||
|
price: 888.00,
|
||||||
|
original_price: null,
|
||||||
|
unit: '元',
|
||||||
|
discount: '超值',
|
||||||
|
features: JSON.stringify(['永久使用权限', '全功能使用', '终身技术支持', '一次购买,终身使用', '最划算选择']),
|
||||||
|
featured: 0,
|
||||||
|
is_active: 1,
|
||||||
|
sort_order: 4,
|
||||||
|
is_delete: 0,
|
||||||
|
create_time: new Date(),
|
||||||
|
last_modify_time: new Date()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 批量插入
|
||||||
|
await pricing_plans.bulkCreate(plans);
|
||||||
|
console.log('✅ 成功迁移 4 条价格套餐数据!\n');
|
||||||
|
|
||||||
|
// 验证插入结果
|
||||||
|
const result = await pricing_plans.findAll({
|
||||||
|
where: { is_delete: 0 },
|
||||||
|
order: [['sort_order', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📋 已迁移的套餐:');
|
||||||
|
result.forEach(plan => {
|
||||||
|
const planData = plan.toJSON();
|
||||||
|
console.log(` ${planData.id}. ${planData.name} - ${planData.duration} - ¥${planData.price}`);
|
||||||
|
if (planData.featured === 1) {
|
||||||
|
console.log(` [推荐套餐]`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 迁移失败:', error.message);
|
||||||
|
console.error('\n详细错误:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行迁移
|
||||||
|
migratePricingPlans()
|
||||||
|
.then(() => {
|
||||||
|
console.log('✨ 迁移完成!');
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('\n💥 执行失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user