This commit is contained in:
张成
2025-11-24 13:23:42 +08:00
commit 5d7444cd65
156 changed files with 50653 additions and 0 deletions

View File

@@ -0,0 +1,321 @@
# 后台管理 API 接口文档
## 📋 接口概览
本目录包含所有后台管理系统的 API 接口,每个业务模块对应一个控制器文件。
### 🗂️ 控制器列表
| 文件名 | 模块 | 路由前缀 | 说明 |
|--------|------|----------|------|
| `apply_records.js` | 投递记录管理 | `/admin_api/apply` | 投递记录的查询、统计、删除 |
| `job_postings.js` | 岗位信息管理 | `/admin_api/job` | 岗位信息的查询、统计、删除 |
| `pla_account.js` | 平台账号管理 | `/admin_api/account` | 平台账号的查询、更新、删除 |
| `resume_info.js` | 简历信息管理 | `/admin_api/resume` | 简历信息的查询、统计、删除 |
| `device_monitor.js` | 设备状态管理 | `/admin_api/device` | 设备状态的查询、监控、配置 |
| `chat_records.js` | 聊天记录管理 | `/admin_api/chat` | 聊天记录的查询、统计、删除 |
| `task_status.js` | 任务状态管理 | `/admin_api/task` | 任务状态的查询、更新、删除 |
| `system_config.js` | 系统配置管理 | `/admin_api/config` | 系统配置的CRUD操作 |
| `dashboard.js` | 数据统计面板 | `/admin_api/dashboard` | 综合数据统计 |
| `sys_user.js` | 系统用户管理 | `/admin_api/user` | 用户管理 |
---
## 🔌 接口详情
### 1⃣ 投递记录管理 (`apply_records.js`)
**路由前缀**: `/admin_api/apply`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/list` | POST | 分页获取投递记录列表 |
| `/statistics` | GET | 获取投递统计数据 |
| `/detail` | GET | 获取投递记录详情 |
| `/delete` | POST | 删除投递记录 |
**主要功能**
- ✅ 按状态、反馈、面试、Offer 筛选
- ✅ 支持搜索岗位名称/公司名称
- ✅ 统计成功率、面试率、Offer率
- ✅ 按平台和状态统计
---
### 2⃣ 岗位信息管理 (`job_postings.js`)
**路由前缀**: `/admin_api/job`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/list` | POST | 分页获取岗位列表 |
| `/statistics` | GET | 获取岗位统计数据 |
| `/detail` | GET | 获取岗位详情 |
| `/delete` | POST | 删除岗位信息 |
**主要功能**
- ✅ 按平台、外包、薪资范围筛选
- ✅ 支持搜索岗位名称/公司名称
- ✅ AI 匹配度排序
- ✅ 统计外包率、平均匹配度、薪资分布
---
### 3⃣ 平台账号管理 (`pla_account.js`)
**路由前缀**: `/admin_api/account`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/list` | POST | 分页获取账号列表 |
| `/statistics` | GET | 获取账号统计数据 |
| `/detail` | GET | 获取账号详情 |
| `/update` | POST | 更新账号信息 |
| `/delete` | POST | 删除平台账号 |
**主要功能**
- ✅ 按平台、激活状态筛选
- ✅ 支持搜索账号/设备SN码
- ✅ 统计激活率、平台分布
- ✅ 更新 cookies/token
---
### 4⃣ 简历信息管理 (`resume_info.js`)
**路由前缀**: `/admin_api/resume`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/list` | POST | 分页获取简历列表 |
| `/statistics` | GET | 获取简历统计数据 |
| `/detail` | GET | 获取简历详情 |
| `/delete` | POST | 删除简历信息 |
**主要功能**
- ✅ 按竞争力评分筛选
- ✅ 支持搜索姓名/技能
- ✅ 竞争力分布统计
- ✅ 工作年限统计
---
### 5⃣ 设备状态管理 (`device_monitor.js`)
**路由前缀**: `/admin_api/device`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/list` | POST | 分页获取设备列表 |
| `/overview` | GET | 获取设备概览统计 |
| `/update-config` | POST | 更新设备配置 |
| `/reset-error` | POST | 重置设备错误 |
| `/delete` | POST | 删除设备记录 |
**主要功能**
- ✅ 按在线、健康状态筛选
- ✅ 实时监控设备状态
- ✅ 统计在线率、健康率
- ✅ 最近离线设备列表
---
### 6⃣ 聊天记录管理 (`chat_records.js`)
**路由前缀**: `/admin_api/chat`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/list` | POST | 分页获取聊天记录 |
| `/statistics` | GET | 获取聊天统计数据 |
| `/detail` | GET | 获取聊天详情 |
| `/delete` | POST | 删除聊天记录 |
**主要功能**
- ✅ 按类型、回复状态筛选
- ✅ 支持搜索聊天内容
- ✅ 统计回复率
- ✅ 按类型和平台统计
---
### 7⃣ 任务状态管理 (`task_status.js`)
**路由前缀**: `/admin_api/task`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/list` | POST | 分页获取任务列表 |
| `/statistics` | GET | 获取任务统计数据 |
| `/detail` | GET | 获取任务详情 |
| `/update` | POST | 更新任务状态 |
| `/delete` | POST | 删除任务记录 |
**主要功能**
- ✅ 按类型、状态筛选
- ✅ 支持搜索任务名称
- ✅ 统计成功率
- ✅ 按类型和状态统计
---
### 8⃣ 系统配置管理 (`system_config.js`)
**路由前缀**: `/admin_api/config`
| 接口 | 方法 | 说明 |
|------|------|------|
| `/list` | POST | 分页获取配置列表 |
| `/get` | GET | 获取配置详情 |
| `/add` | POST | 添加配置 |
| `/update` | POST | 更新配置 |
| `/delete` | POST | 删除配置 |
| `/categories` | GET | 获取配置分类 |
| `/reset` | POST | 重置配置为默认值 |
| `/batch-update` | POST | 批量更新配置 |
**主要功能**
- ✅ 完整的 CRUD 操作
- ✅ 支持多种配置类型string/number/boolean/json
- ✅ 配置加密存储
- ✅ 批量更新
---
## 📊 统一响应格式
### 成功响应
```json
{
"code": 0,
"message": "success",
"data": {
// 响应数据
}
}
```
### 错误响应
```json
{
"code": -1,
"message": "错误信息",
"data": null
}
```
### 分页列表响应
```json
{
"code": 0,
"message": "success",
"data": {
"total": 100,
"page": 1,
"pageSize": 20,
"list": [
// 数据列表
]
}
}
```
---
## 🔍 通用查询参数
所有 `POST /list` 接口都支持以下通用参数:
| 参数 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `page` | Integer | 页码 | 1 |
| `pageSize` | Integer | 每页数量 | 20 |
| `searchText` | String | 搜索关键词 | - |
---
## 🎯 接口特点
### ✅ 已实现功能
- 📋 **分页查询**:所有列表接口都支持分页
- 🔍 **搜索过滤**:支持关键词搜索和多条件筛选
- 📊 **统计分析**:每个模块都提供详细的统计数据
- 🔐 **错误处理**:统一的错误处理和日志记录
- 📝 **Swagger文档**:所有接口都有完整的 Swagger 注释
### 🚀 接口亮点
1. **统一规范**:所有接口遵循相同的命名和响应格式
2. **丰富统计**:提供多维度的数据统计和分析
3. **灵活查询**:支持多种筛选条件和排序方式
4. **安全可靠**:完整的参数校验和错误处理
---
## 📝 使用示例
### 获取投递记录列表
```bash
POST /admin_api/apply/list
Content-Type: application/json
{
"page": 1,
"pageSize": 20,
"applyStatus": "success",
"hasOffer": true,
"searchText": "前端"
}
```
### 获取设备概览
```bash
GET /admin_api/device/overview
```
### 更新系统配置
```bash
POST /admin_api/config/update
Content-Type: application/json
{
"configKey": "ai.provider",
"configValue": "openai",
"description": "AI服务提供商"
}
```
---
## 🔗 相关文件
- **前端页面**`admin/src/views/` - 各模块的 Vue 页面
- **API服务**`admin/src/api/` - 前端 API 调用封装
- **数据模型**`api/model/` - 数据库表模型定义
- **路由配置**`admin/src/router/` - 前端路由配置
---
## 📌 注意事项
1. **权限控制**:生产环境需要添加权限验证中间件
2. **数据安全**敏感信息如密码、token需要加密处理
3. **性能优化**:大数据量查询需要添加索引和分页限制
4. **日志记录**:所有操作都会记录日志便于追踪
5. **错误处理**:统一使用 `ctx.success()``ctx.fail()` 方法
---
## 🎉 总结
所有 8 个业务模块的后台管理接口已完整实现,涵盖:
- ✅ 投递记录管理
- ✅ 岗位信息管理
- ✅ 平台账号管理
- ✅ 简历信息管理
- ✅ 设备状态管理
- ✅ 聊天记录管理
- ✅ 任务状态管理
- ✅ 系统配置管理
每个模块都提供了完整的 CRUD 操作和统计分析功能!🚀

View File

@@ -0,0 +1,268 @@
/**
* 投递记录管理API - 后台管理
* 提供投递记录的查询和管理功能
*/
const Framework = require("../../framework/node-core-framework.js");
module.exports = {
/**
* @swagger
* /admin_api/apply/list:
* post:
* summary: 获取投递记录列表
* description: 分页获取所有投递记录
* tags: [后台-投递管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* seachOption:
* type: object
* description: 搜索条件
* properties:
* key:
* type: string
* description: 搜索字段(jobTitle/companyName/sn_code)
* value:
* type: string
* description: 搜索值
* platform:
* type: string
* description: 平台筛选(boss/liepin)
* applyStatus:
* type: string
* description: 投递状态筛选
* feedbackStatus:
* type: string
* description: 反馈状态筛选
* pageOption:
* type: object
* description: 分页选项
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* responses:
* 200:
* description: 获取成功
*/
'POST /apply/list': async (ctx) => {
const models = Framework.getModels();
const { apply_records, op } = models;
const body = ctx.getBody();
const seachOption = body.seachOption || {};
const pageOption = body.pageOption || {};
// 获取分页参数
const page = pageOption.page || 1;
const pageSize = pageOption.pageSize || 20;
const limit = pageSize;
const offset = (page - 1) * pageSize;
const where = {};
// 平台筛选
if (seachOption.platform) {
where.platform = seachOption.platform;
}
// 投递状态筛选
if (seachOption.applyStatus) {
where.applyStatus = seachOption.applyStatus;
}
// 反馈状态筛选
if (seachOption.feedbackStatus) {
where.feedbackStatus = seachOption.feedbackStatus;
}
// 搜索岗位名称、公司名称、设备SN码
if (seachOption.key && seachOption.value) {
const key = seachOption.key;
const value = seachOption.value;
if (key === 'jobTitle') {
where.jobTitle = { [op.like]: `%${value}%` };
} else if (key === 'companyName') {
where.companyName = { [op.like]: `%${value}%` };
} else if (key === 'sn_code') {
where.sn_code = { [op.like]: `%${value}%` };
} else {
// 默认搜索岗位名称或公司名称
where[op.or] = [
{ jobTitle: { [op.like]: `%${value}%` } },
{ companyName: { [op.like]: `%${value}%` } }
];
}
}
const result = await apply_records.findAndCountAll({
where,
limit,
offset,
order: [['applyTime', 'DESC']]
});
return ctx.success(result);
},
/**
* @swagger
* /admin_api/apply/statistics:
* get:
* summary: 获取投递统计
* description: 获取投递记录的统计数据
* tags: [后台-投递管理]
* responses:
* 200:
* description: 获取成功
*/
'GET /apply/statistics': async (ctx) => {
const models = Framework.getModels();
const { apply_records } = models;
const [
totalApply,
pendingApply,
successApply,
failedApply,
hasInterviewCount,
hasOfferCount
] = await Promise.all([
apply_records.count(),
apply_records.count({ where: { applyStatus: 'pending' } }),
apply_records.count({ where: { applyStatus: 'success' } }),
apply_records.count({ where: { applyStatus: 'failed' } }),
apply_records.count({ where: { hasInterview: true } }),
apply_records.count({ where: { hasOffer: true } })
]);
// 按平台统计
const platformStats = await apply_records.findAll({
attributes: [
'platform',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['platform'],
raw: true
});
// 按状态统计
const statusStats = await apply_records.findAll({
attributes: [
'applyStatus',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['applyStatus'],
raw: true
});
return ctx.success({
totalApply,
pendingApply,
successApply,
failedApply,
hasInterviewCount,
hasOfferCount,
successRate: totalApply > 0 ? ((successApply / totalApply) * 100).toFixed(2) : 0,
interviewRate: totalApply > 0 ? ((hasInterviewCount / totalApply) * 100).toFixed(2) : 0,
offerRate: totalApply > 0 ? ((hasOfferCount / totalApply) * 100).toFixed(2) : 0,
platformStats,
statusStats
});
},
/**
* @swagger
* /admin_api/apply/detail:
* get:
* summary: 获取投递记录详情
* description: 根据投递ID获取详细信息
* tags: [后台-投递管理]
* parameters:
* - in: query
* name: applyId
* required: true
* schema:
* type: string
* description: 投递记录ID
* responses:
* 200:
* description: 获取成功
*/
'GET /apply/detail': async (ctx) => {
const models = Framework.getModels();
const { apply_records } = models;
const { applyId } = ctx.query;
if (!applyId) {
return ctx.fail('投递记录ID不能为空');
}
const record = await apply_records.findOne({ where: { applyId } });
if (!record) {
return ctx.fail('投递记录不存在');
}
return ctx.success(record);
},
/**
* @swagger
* /admin_api/apply/delete:
* post:
* summary: 删除投递记录
* description: 删除指定的投递记录
* tags: [后台-投递管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - applyId
* properties:
* applyId:
* type: string
* description: 投递记录ID
* responses:
* 200:
* description: 删除成功
*/
'POST /apply/delete': async (ctx) => {
const models = Framework.getModels();
const { apply_records } = models;
const body = ctx.getBody();
const { applyId } = body;
if (!applyId) {
return ctx.fail('投递记录ID不能为空');
}
const result = await apply_records.destroy({ where: { applyId } });
if (result === 0) {
return ctx.fail('投递记录不存在');
}
return ctx.success({ message: '投递记录删除成功' });
}
};

View File

@@ -0,0 +1,499 @@
/**
* 聊天记录管理API - 后台管理
* 提供聊天记录的查询和管理功能
*/
const Framework = require("../../framework/node-core-framework.js");
module.exports = {
/**
* @swagger
* /admin_api/chat/list:
* post:
* summary: 获取聊天记录列表
* description: 分页获取所有聊天记录
* tags: [后台-聊天管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* chatType:
* type: string
* description: 聊天类型(可选)
* hasReply:
* type: boolean
* description: 是否有回复(可选)
* responses:
* 200:
* description: 获取成功
*/
'POST /chat/list': async (ctx) => {
const models = Framework.getModels();
const { chat_records, op } = models;
const body = ctx.getBody();
// 支持两种参数格式:直接传参或通过 seachOption 传递
const seachOption = body.seachOption || {};
const pageOption = body.pageOption || {};
// 获取分页参数
const page = pageOption.page || body.page || 1;
const pageSize = pageOption.pageSize || body.pageSize || 20;
const limit = pageSize;
const offset = (page - 1) * pageSize;
const where = {};
// 支持平台筛选
if (seachOption.platform || body.platform) {
where.platform = seachOption.platform || body.platform;
}
// 支持聊天类型筛选
if (seachOption.chatType || body.chatType) {
where.chatType = seachOption.chatType || body.chatType;
}
// 支持回复状态筛选
if (seachOption.hasReply !== undefined || body.hasReply !== undefined) {
where.hasReply = seachOption.hasReply !== undefined ? seachOption.hasReply : body.hasReply;
}
// 支持搜索:公司名称、职位名称、消息内容
if (seachOption.key && seachOption.value) {
const key = seachOption.key;
const value = seachOption.value;
if (key === 'companyName') {
where.companyName = { [op.like]: `%${value}%` };
} else if (key === 'jobTitle') {
where.jobTitle = { [op.like]: `%${value}%` };
} else if (key === 'content') {
where.content = { [op.like]: `%${value}%` };
} else if (key === 'sn_code') {
where.sn_code = { [op.like]: `%${value}%` };
} else {
// 默认搜索内容
where.content = { [op.like]: `%${value}%` };
}
}
// 支持直接搜索文本
if (body.searchText) {
where[op.or] = [
{ companyName: { [op.like]: `%${body.searchText}%` } },
{ jobTitle: { [op.like]: `%${body.searchText}%` } },
{ content: { [op.like]: `%${body.searchText}%` } }
];
}
const result = await chat_records.findAndCountAll({
where,
limit,
offset,
order: [['id', 'DESC']]
});
return ctx.success({
count: result.count,
total: result.count,
rows: result.rows,
list: result.rows
});
},
/**
* @swagger
* /admin_api/chat/statistics:
* get:
* summary: 获取聊天统计
* description: 获取聊天记录的统计数据
* tags: [后台-聊天管理]
* responses:
* 200:
* description: 获取成功
*/
'GET /chat/statistics': async (ctx) => {
const models = Framework.getModels();
const { chat_records } = models;
const [
totalChats,
hasReplyCount,
noReplyCount
] = await Promise.all([
chat_records.count(),
chat_records.count({ where: { hasReply: true } }),
chat_records.count({ where: { hasReply: false } })
]);
const typeStats = await chat_records.findAll({
attributes: [
'chatType',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['chatType'],
raw: true
});
const platformStats = await chat_records.findAll({
attributes: [
'platform',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['platform'],
raw: true
});
return ctx.success({
totalChats,
hasReplyCount,
noReplyCount,
replyRate: totalChats > 0 ? ((hasReplyCount / totalChats) * 100).toFixed(2) : 0,
typeStats,
platformStats
});
},
/**
* @swagger
* /admin_api/chat/detail:
* get:
* summary: 获取聊天记录详情
* description: 根据聊天ID获取详细信息
* tags: [后台-聊天管理]
* parameters:
* - in: query
* name: chatId
* required: true
* schema:
* type: string
* description: 聊天记录ID
* responses:
* 200:
* description: 获取成功
*/
'GET /chat/detail': async (ctx) => {
const models = Framework.getModels();
const { chat_records } = models;
const { chatId } = ctx.query;
const body = ctx.getBody();
const id = chatId || body.chatId;
if (!id) {
return ctx.fail('聊天记录ID不能为空');
}
const chat = await chat_records.findOne({ where: { id } });
if (!chat) {
return ctx.fail('聊天记录不存在');
}
return ctx.success(chat);
},
/**
* @swagger
* /admin_api/chat/delete:
* post:
* summary: 删除聊天记录
* description: 删除指定的聊天记录
* tags: [后台-聊天管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - chatId
* properties:
* chatId:
* type: string
* description: 聊天记录ID
* responses:
* 200:
* description: 删除成功
*/
'POST /chat/delete': async (ctx) => {
const models = Framework.getModels();
const { chat_records } = models;
const body = ctx.getBody();
const chatId = body.chatId || body.id;
if (!chatId) {
return ctx.fail('聊天记录ID不能为空');
}
const result = await chat_records.destroy({ where: { id: chatId } });
if (result === 0) {
return ctx.fail('聊天记录不存在');
}
return ctx.success({ message: '聊天记录删除成功' });
},
/**
* @swagger
* /admin_api/chat/by-job:
* get:
* summary: 获取指定职位的聊天记录
* description: 根据职位ID和设备SN码获取聊天记录
* tags: [后台-聊天管理]
* parameters:
* - in: query
* name: jobId
* required: true
* schema:
* type: string
* description: 职位ID
* - in: query
* name: sn_code
* required: true
* schema:
* type: string
* description: 设备SN码
* responses:
* 200:
* description: 获取成功
*/
'GET /chat/by-job': async (ctx) => {
const models = Framework.getModels();
const { chat_records } = models;
const { jobId, sn_code } = ctx.query;
if (!jobId || !sn_code) {
return ctx.fail('职位ID和设备SN码不能为空');
}
const records = await chat_records.findAll({
where: { jobId, sn_code },
order: [['sendTime', 'ASC'], ['receiveTime', 'ASC'], ['id', 'ASC']]
});
return ctx.success(records);
},
/**
* @swagger
* /admin_api/chat/send:
* post:
* summary: 发送聊天消息
* description: 向指定职位发送聊天消息
* tags: [后台-聊天管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sn_code
* - jobId
* - content
* - platform
* properties:
* sn_code:
* type: string
* description: 设备SN码
* jobId:
* type: string
* description: 职位ID
* content:
* type: string
* description: 消息内容
* chatType:
* type: string
* description: 聊天类型
* platform:
* type: string
* description: 平台(boss/liepin)
* responses:
* 200:
* description: 发送成功
*/
'POST /chat/send': async (ctx) => {
const models = Framework.getModels();
const { chat_records } = models;
const body = ctx.getBody();
const { sn_code, jobId, content, chatType = 'reply', platform } = body;
if (!sn_code || !jobId || !content || !platform) {
return ctx.fail('设备SN码、职位ID、消息内容和平台不能为空');
}
const chatRecord = await chat_records.create({
sn_code,
jobId,
platform,
content,
chatType,
direction: 'sent',
sendStatus: 'pending',
sendTime: new Date()
});
console.log(`[聊天管理] 消息待发送到设备 ${sn_code}:`, content);
await chatRecord.update({
sendStatus: 'sent'
});
return ctx.success({
message: '消息发送成功',
chatRecord
});
},
/**
* @swagger
* /admin_api/chat/unread-count:
* get:
* summary: 获取未读消息数量
* description: 获取指定设备的未读消息数量
* tags: [后台-聊天管理]
* parameters:
* - in: query
* name: sn_code
* required: true
* schema:
* type: string
* description: 设备SN码
* responses:
* 200:
* description: 获取成功
*/
'GET /chat/unread-count': async (ctx) => {
const models = Framework.getModels();
const { chat_records } = models;
const { sn_code } = ctx.query;
if (!sn_code) {
return ctx.fail('设备SN码不能为空');
}
const count = await chat_records.count({
where: {
sn_code,
direction: 'received',
hasReply: false
}
});
return ctx.success({ unreadCount: count });
},
/**
* @swagger
* /admin_api/chat/mark-read:
* post:
* summary: 标记消息为已读
* description: 将指定的聊天消息标记为已读
* tags: [后台-聊天管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - chatId
* properties:
* chatId:
* type: string
* description: 聊天记录ID
* responses:
* 200:
* description: 标记成功
*/
'POST /chat/mark-read': async (ctx) => {
const models = Framework.getModels();
const { chat_records } = models;
const body = ctx.getBody();
const chatId = body.chatId || body.id;
if (!chatId) {
return ctx.fail('聊天记录ID不能为空');
}
const result = await chat_records.update(
{ hasReply: true, replyTime: new Date() },
{ where: { id: chatId } }
);
if (result[0] === 0) {
return ctx.fail('聊天记录不存在');
}
return ctx.success({ message: '标记成功' });
},
/**
* @swagger
* /admin_api/chat/export:
* post:
* summary: 导出聊天记录
* description: 导出聊天记录为CSV文件
* tags: [后台-聊天管理]
* responses:
* 200:
* description: 导出成功
*/
'POST /chat/export': async (ctx) => {
const models = Framework.getModels();
const { chat_records, op } = models;
const body = ctx.getBody();
const where = {};
if (body.platform) where.platform = body.platform;
if (body.chatType) where.chatType = body.chatType;
if (body.hasReply !== undefined) where.hasReply = body.hasReply;
const records = await chat_records.findAll({
where,
order: [['id', 'DESC']],
limit: 10000
});
const headers = ['ID', '设备SN码', '平台', '职位ID', '公司名称', '职位名称', 'HR姓名',
'消息方向', '聊天类型', '消息内容', '发送时间', '接收时间', '是否有回复'];
let csvContent = '\uFEFF' + headers.join(',') + '\n';
records.forEach(record => {
const row = [
record.id || '',
record.sn_code || '',
record.platform || '',
record.jobId || '',
`"${(record.companyName || '').replace(/"/g, '""')}"`,
`"${(record.jobTitle || '').replace(/"/g, '""')}"`,
`"${(record.hrName || '').replace(/"/g, '""')}"`,
record.direction || '',
record.chatType || '',
`"${(record.content || '').replace(/"/g, '""')}"`,
record.sendTime || '',
record.receiveTime || '',
record.hasReply ? '是' : '否'
];
csvContent += row.join(',') + '\n';
});
ctx.set('Content-Type', 'text/csv;charset=utf-8');
ctx.set('Content-Disposition', `attachment; filename="chat_records_${Date.now()}.csv"`);
ctx.body = csvContent;
}
};

View File

@@ -0,0 +1,381 @@
/**
* 数据统计和报表API - 后台管理
* 提供系统数据统计和可视化报表
*/
const Framework = require("../../framework/node-core-framework.js");
module.exports = {
/**
* @swagger
* /admin_api/dashboard/overview:
* get:
* summary: 获取系统概览
* description: 获取系统整体运行状态概览
* tags: [后台-数据统计]
* responses:
* 200:
* description: 获取成功
*/
'GET /dashboard/overview': async (ctx) => {
const models = Framework.getModels();
const {
device_status,
task_status,
job_postings,
apply_records,
chat_records,
op
} = models;
// 设备统计
const [totalDevices, onlineDevices, runningDevices] = await Promise.all([
device_status.count(),
device_status.count({ where: { isOnline: true } }),
device_status.count({ where: { isRunning: true } })
]);
// 任务统计
const [totalTasks, runningTasks, completedTasks, failedTasks] = await Promise.all([
task_status.count(),
task_status.count({ where: { status: 'running' } }),
task_status.count({ where: { status: 'completed' } }),
task_status.count({ where: { status: 'failed' } })
]);
// 岗位统计
const [totalJobs, pendingJobs, appliedJobs, highQualityJobs] = await Promise.all([
job_postings.count(),
job_postings.count({ where: { applyStatus: 'pending' } }),
job_postings.count({ where: { applyStatus: 'applied' } }),
job_postings.count({ where: { aiMatchScore: { [op.gte]: 70 } } })
]);
// 投递统计
const [totalApplies, viewedApplies, interviewApplies, offerApplies] = await Promise.all([
apply_records.count(),
apply_records.count({ where: { isViewed: true } }),
apply_records.count({ where: { hasInterview: true } }),
apply_records.count({ where: { hasOffer: true } })
]);
// 聊天统计
const [totalChats, repliedChats, interviewInvitations] = await Promise.all([
chat_records.count(),
chat_records.count({ where: { hasReply: true } }),
chat_records.count({ where: { isInterviewInvitation: true } })
]);
// 计算各种率
const taskSuccessRate = totalTasks > 0 ? ((completedTasks / totalTasks) * 100).toFixed(2) : 0;
const applyViewRate = totalApplies > 0 ? ((viewedApplies / totalApplies) * 100).toFixed(2) : 0;
const interviewRate = totalApplies > 0 ? ((interviewApplies / totalApplies) * 100).toFixed(2) : 0;
const offerRate = totalApplies > 0 ? ((offerApplies / totalApplies) * 100).toFixed(2) : 0;
const chatReplyRate = totalChats > 0 ? ((repliedChats / totalChats) * 100).toFixed(2) : 0;
return ctx.success({
device: {
total: totalDevices,
online: onlineDevices,
offline: totalDevices - onlineDevices,
running: runningDevices,
onlineRate: totalDevices > 0 ? ((onlineDevices / totalDevices) * 100).toFixed(2) : 0
},
task: {
total: totalTasks,
running: runningTasks,
completed: completedTasks,
failed: failedTasks,
successRate: taskSuccessRate
},
job: {
total: totalJobs,
pending: pendingJobs,
applied: appliedJobs,
highQuality: highQualityJobs
},
apply: {
total: totalApplies,
viewed: viewedApplies,
interview: interviewApplies,
offer: offerApplies,
viewRate: applyViewRate,
interviewRate,
offerRate
},
chat: {
total: totalChats,
replied: repliedChats,
interviewInvitations,
replyRate: chatReplyRate
}
});
},
/**
* @swagger
* /admin_api/dashboard/trend:
* post:
* summary: 获取趋势统计
* description: 获取指定时间范围内的数据趋势
* tags: [后台-数据统计]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* startDate:
* type: string
* description: 开始日期
* endDate:
* type: string
* description: 结束日期
* type:
* type: string
* description: 统计类型(task/job/apply/chat)
* responses:
* 200:
* description: 获取成功
*/
'POST /dashboard/trend': async (ctx) => {
const models = Framework.getModels();
const body = ctx.getBody();
const { startDate, endDate, type = 'task' } = body;
const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 默认30天
const end = endDate ? new Date(endDate) : new Date();
let Model;
let dateField;
switch (type) {
case 'task':
Model = models.task_status;
dateField = 'id';
break;
case 'job':
Model = models.job_postings;
dateField = 'id';
break;
case 'apply':
Model = models.apply_records;
dateField = 'applyTime';
break;
case 'chat':
Model = models.chat_records;
dateField = 'sendTime';
break;
default:
return ctx.fail('无效的统计类型');
}
const data = await Model.findAll({
where: {
[dateField]: {
[models.op.between]: [start, end]
}
},
attributes: [
[models.sequelize.fn('DATE', models.sequelize.col(dateField)), 'date'],
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: [models.sequelize.fn('DATE', models.sequelize.col(dateField))],
order: [[models.sequelize.fn('DATE', models.sequelize.col(dateField)), 'ASC']],
raw: true
});
return ctx.success({
type,
startDate: start,
endDate: end,
data
});
},
/**
* @swagger
* /admin_api/dashboard/device-performance:
* get:
* summary: 获取设备性能统计
* description: 获取各设备的性能和工作量统计
* tags: [后台-数据统计]
* responses:
* 200:
* description: 获取成功
*/
'GET /dashboard/device-performance': async (ctx) => {
const models = Framework.getModels();
const { device_status } = models;
const devices = await device_status.findAll({
attributes: [
'sn_code',
'deviceName',
'totalTasksCompleted',
'totalTasksFailed',
'totalJobsSearched',
'totalApplies',
'totalChats',
'healthScore',
'onlineDuration'
],
order: [['totalTasksCompleted', 'DESC']],
limit: 20
});
const performanceData = devices.map(device => {
const total = device.totalTasksCompleted + device.totalTasksFailed;
const successRate = total > 0 ? ((device.totalTasksCompleted / total) * 100).toFixed(2) : 0;
return {
sn_code: device.sn_code,
deviceName: device.deviceName,
tasksCompleted: device.totalTasksCompleted,
tasksFailed: device.totalTasksFailed,
jobsSearched: device.totalJobsSearched,
applies: device.totalApplies,
chats: device.totalChats,
successRate,
healthScore: device.healthScore,
onlineDuration: device.onlineDuration
};
});
return ctx.success(performanceData);
},
/**
* @swagger
* /admin_api/dashboard/job-quality:
* get:
* summary: 获取岗位质量分布
* description: 获取岗位匹配度的分布统计
* tags: [后台-数据统计]
* responses:
* 200:
* description: 获取成功
*/
'GET /dashboard/job-quality': async (ctx) => {
const models = Framework.getModels();
const { job_postings, op } = models;
const [
excellent, // 90+
good, // 70-89
medium, // 50-69
low, // <50
outsourcing
] = await Promise.all([
job_postings.count({ where: { aiMatchScore: { [op.gte]: 90 } } }),
job_postings.count({ where: { aiMatchScore: { [op.between]: [70, 89] } } }),
job_postings.count({ where: { aiMatchScore: { [op.between]: [50, 69] } } }),
job_postings.count({ where: { aiMatchScore: { [op.lt]: 50 } } }),
job_postings.count({ where: { isOutsourcing: true } })
]);
const total = excellent + good + medium + low;
return ctx.success({
distribution: {
excellent: { count: excellent, percentage: total > 0 ? ((excellent / total) * 100).toFixed(2) : 0 },
good: { count: good, percentage: total > 0 ? ((good / total) * 100).toFixed(2) : 0 },
medium: { count: medium, percentage: total > 0 ? ((medium / total) * 100).toFixed(2) : 0 },
low: { count: low, percentage: total > 0 ? ((low / total) * 100).toFixed(2) : 0 }
},
outsourcing: {
count: outsourcing,
percentage: total > 0 ? ((outsourcing / total) * 100).toFixed(2) : 0
},
total
});
},
/**
* @swagger
* /admin_api/dashboard/apply-funnel:
* get:
* summary: 获取投递漏斗数据
* description: 获取从投递到Offer的转化漏斗
* tags: [后台-数据统计]
* responses:
* 200:
* description: 获取成功
*/
'GET /dashboard/apply-funnel': async (ctx) => {
const models = Framework.getModels();
const { apply_records } = models;
const [
totalApplies,
viewedApplies,
interestedApplies,
chattedApplies,
interviewApplies,
offerApplies
] = await Promise.all([
apply_records.count(),
apply_records.count({ where: { isViewed: true } }),
apply_records.count({ where: { feedbackStatus: 'interested' } }),
apply_records.count({ where: { hasChatted: true } }),
apply_records.count({ where: { hasInterview: true } }),
apply_records.count({ where: { hasOffer: true } })
]);
const funnelData = [
{
stage: '投递',
count: totalApplies,
percentage: 100,
conversionRate: 100
},
{
stage: '查看',
count: viewedApplies,
percentage: totalApplies > 0 ? ((viewedApplies / totalApplies) * 100).toFixed(2) : 0,
conversionRate: totalApplies > 0 ? ((viewedApplies / totalApplies) * 100).toFixed(2) : 0
},
{
stage: '感兴趣',
count: interestedApplies,
percentage: totalApplies > 0 ? ((interestedApplies / totalApplies) * 100).toFixed(2) : 0,
conversionRate: viewedApplies > 0 ? ((interestedApplies / viewedApplies) * 100).toFixed(2) : 0
},
{
stage: '沟通',
count: chattedApplies,
percentage: totalApplies > 0 ? ((chattedApplies / totalApplies) * 100).toFixed(2) : 0,
conversionRate: interestedApplies > 0 ? ((chattedApplies / interestedApplies) * 100).toFixed(2) : 0
},
{
stage: '面试',
count: interviewApplies,
percentage: totalApplies > 0 ? ((interviewApplies / totalApplies) * 100).toFixed(2) : 0,
conversionRate: chattedApplies > 0 ? ((interviewApplies / chattedApplies) * 100).toFixed(2) : 0
},
{
stage: 'Offer',
count: offerApplies,
percentage: totalApplies > 0 ? ((offerApplies / totalApplies) * 100).toFixed(2) : 0,
conversionRate: interviewApplies > 0 ? ((offerApplies / interviewApplies) * 100).toFixed(2) : 0
}
];
return ctx.success({
funnel: funnelData,
overallConversionRate: totalApplies > 0 ? ((offerApplies / totalApplies) * 100).toFixed(2) : 0
});
}
};

View File

@@ -0,0 +1,277 @@
/**
* 设备监控管理API - 后台管理
* 提供设备状态监控和管理功能
*/
const Framework = require("../../framework/node-core-framework.js");
module.exports = {
/**
* @swagger
* /admin_api/device/list:
* post:
* summary: 获取设备列表
* description: 分页获取所有设备列表(管理后台)
* tags: [后台-设备管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* isOnline:
* type: boolean
* description: 是否在线(可选)
* healthStatus:
* type: string
* description: 健康状态(可选)
* responses:
* 200:
* description: 获取成功
*/
'POST /device/list': async (ctx) => {
const models = Framework.getModels();
const { device_status, op } = models;
const body = ctx.getBody();
const { isOnline, healthStatus, platform, searchText} = ctx.getBody();
// 获取分页参数
const { limit, offset } = ctx.getPageSize();
const where = {};
if (isOnline !== undefined) where.isOnline = isOnline;
if (healthStatus) where.healthStatus = healthStatus;
if (platform) where.platform = platform;
// 支持搜索设备名称或SN码
if (searchText) {
where[op.or] = [
{ deviceName: { [op.like]: `%${searchText}%` } },
{ sn_code: { [op.like]: `%${searchText}%` } }
];
}
const result = await device_status.findAndCountAll({
where,
limit,
offset,
order: [
['isOnline', 'DESC'],
['last_modify_time', 'DESC']
]
});
return ctx.success({
total: result.count,
list: result.rows
});
},
/**
* @swagger
* /admin_api/device/overview:
* get:
* summary: 获取设备概览
* description: 获取所有设备的统计概览
* tags: [后台-设备管理]
* responses:
* 200:
* description: 获取成功
*/
'GET /device/overview': async (ctx) => {
const models = Framework.getModels();
const { device_status, op } = models;
const [
totalDevices,
onlineDevices,
runningDevices,
healthyDevices,
warningDevices,
errorDevices
] = await Promise.all([
device_status.count(),
device_status.count({ where: { isOnline: true } }),
device_status.count({ where: { isRunning: true } }),
device_status.count({ where: { healthStatus: 'healthy' } }),
device_status.count({ where: { healthStatus: 'warning' } }),
device_status.count({ where: { healthStatus: 'error' } })
]);
// 计算平均健康分数
const avgHealthScore = await device_status.findAll({
attributes: [
[models.sequelize.fn('AVG', models.sequelize.col('healthScore')), 'avgScore']
],
raw: true
});
// 获取最近离线的设备
const recentOffline = await device_status.findAll({
where: { isOnline: false },
limit: 5,
order: [['lastOfflineTime', 'DESC']],
attributes: ['sn_code', 'deviceName', 'lastOfflineTime', 'lastError']
});
return ctx.success({
totalDevices,
onlineDevices,
offlineDevices: totalDevices - onlineDevices,
runningDevices,
idleDevices: onlineDevices - runningDevices,
healthyDevices,
warningDevices,
errorDevices,
onlineRate: totalDevices > 0 ? ((onlineDevices / totalDevices) * 100).toFixed(2) : 0,
healthyRate: totalDevices > 0 ? ((healthyDevices / totalDevices) * 100).toFixed(2) : 0,
averageHealthScore: parseFloat(avgHealthScore[0]?.avgScore || 0).toFixed(2),
recentOffline
});
},
/**
* @swagger
* /admin_api/device/update-config:
* post:
* summary: 更新设备配置
* description: 更新指定设备的配置信息
* tags: [后台-设备管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sn_code
* - config
* properties:
* sn_code:
* type: string
* description: 设备SN码
* config:
* type: object
* description: 配置数据
* responses:
* 200:
* description: 更新成功
*/
'POST /device/update-config': async (ctx) => {
const models = Framework.getModels();
const { device_status } = models;
const body = ctx.getBody();
const { sn_code, config } = body;
if (!sn_code || !config) {
return ctx.fail('设备SN码和配置数据不能为空');
}
await device_status.update({
config: JSON.stringify(config)
}, { where: { sn_code } });
return ctx.success({ message: '设备配置更新成功' });
},
/**
* @swagger
* /admin_api/device/reset-error:
* post:
* summary: 重置设备错误
* description: 清除设备的错误信息和计数
* tags: [后台-设备管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sn_code
* properties:
* sn_code:
* type: string
* description: 设备SN码
* responses:
* 200:
* description: 重置成功
*/
'POST /device/reset-error': async (ctx) => {
const models = Framework.getModels();
const { device_status } = models;
const body = ctx.getBody();
const { sn_code } = body;
if (!sn_code) {
return ctx.fail('设备SN码不能为空');
}
await device_status.update({
lastError: '',
errorCount: 0,
healthStatus: 'healthy',
healthScore: 100
}, { where: { sn_code } });
return ctx.success({ message: '设备错误已重置' });
},
/**
* @swagger
* /admin_api/device/delete:
* post:
* summary: 删除设备
* description: 删除指定的设备记录
* tags: [后台-设备管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sn_code
* properties:
* sn_code:
* type: string
* description: 设备SN码
* responses:
* 200:
* description: 删除成功
*/
'POST /device/delete': async (ctx) => {
const models = Framework.getModels();
const { device_status } = models;
const body = ctx.getBody();
const { sn_code } = body;
if (!sn_code) {
return ctx.fail('设备SN码不能为空');
}
const result = await device_status.destroy({ where: { sn_code } });
if (result === 0) {
return ctx.fail('设备不存在');
}
return ctx.success({ message: '设备删除成功' });
}
};

View File

@@ -0,0 +1,292 @@
/**
* 岗位信息管理API - 后台管理
* 提供岗位信息的查询和管理功能
*/
const Framework = require("../../framework/node-core-framework.js");
const jobManager = require("../middleware/job/jobManager.js");
module.exports = {
/**
* @swagger
* /admin_api/job/list:
* post:
* summary: 获取岗位列表
* description: 分页获取所有岗位信息
* tags: [后台-岗位管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* platform:
* type: string
* description: 平台(可选)
* isOutsourcing:
* type: boolean
* description: 是否外包(可选)
* responses:
* 200:
* description: 获取成功
*/
'POST /job/list': async (ctx) => {
const models = Framework.getModels();
const { job_postings, op } = models;
const body = ctx.getBody();
const { platform, isOutsourcing, minSalary, maxSalary, searchText } = ctx.getBody();
// 获取分页参数
const { limit, offset } = ctx.getPageSize();
const where = {};
if (platform) {
where.platform = platform;
}
if (isOutsourcing) {
where.isOutsourcing = isOutsourcing;
}
// 薪资范围筛选
if (minSalary) {
where.salaryMin = { [op.gte]: minSalary };
}
if (maxSalary) {
where.salaryMax = { [op.lte]: maxSalary };
}
// 支持搜索岗位名称、公司名称
if (searchText) {
where[op.or] = [
{ jobTitle: { [op.like]: `%${searchText}%` } },
{ companyName: { [op.like]: `%${searchText}%` } }
];
}
const result = await job_postings.findAndCountAll({
where,
limit,
offset,
order: [
['aiMatchScore', 'DESC'],
['id', 'DESC']
]
});
return ctx.success(result);
},
/**
* @swagger
* /admin_api/job/statistics:
* get:
* summary: 获取岗位统计
* description: 获取岗位信息的统计数据
* tags: [后台-岗位管理]
* responses:
* 200:
* description: 获取成功
*/
'GET /job/statistics': async (ctx) => {
const models = Framework.getModels();
const { job_postings } = models;
const [
totalJobs,
outsourcingJobs,
directJobs,
highMatchJobs
] = await Promise.all([
job_postings.count(),
job_postings.count({ where: { isOutsourcing: true } }),
job_postings.count({ where: { isOutsourcing: false } }),
job_postings.count({ where: { aiMatchScore: { [models.op.gte]: 80 } } })
]);
// 按平台统计
const platformStats = await job_postings.findAll({
attributes: [
'platform',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['platform'],
raw: true
});
// 平均匹配度
const avgMatchScore = await job_postings.findAll({
attributes: [
[models.sequelize.fn('AVG', models.sequelize.col('aiMatchScore')), 'avgScore']
],
raw: true
});
// 薪资统计
const salaryStats = await job_postings.findAll({
attributes: [
[models.sequelize.fn('AVG', models.sequelize.col('salaryMin')), 'avgMinSalary'],
[models.sequelize.fn('AVG', models.sequelize.col('salaryMax')), 'avgMaxSalary'],
[models.sequelize.fn('MAX', models.sequelize.col('salaryMax')), 'maxSalary'],
[models.sequelize.fn('MIN', models.sequelize.col('salaryMin')), 'minSalary']
],
raw: true
});
return ctx.success({
totalJobs,
outsourcingJobs,
directJobs,
highMatchJobs,
outsourcingRate: totalJobs > 0 ? ((outsourcingJobs / totalJobs) * 100).toFixed(2) : 0,
averageMatchScore: parseFloat(avgMatchScore[0]?.avgScore || 0).toFixed(2),
platformStats,
salaryStats: salaryStats[0] || {}
});
},
/**
* @swagger
* /admin_api/job/detail:
* get:
* summary: 获取岗位详情
* description: 根据岗位ID获取详细信息
* tags: [后台-岗位管理]
* parameters:
* - in: query
* name: jobId
* required: true
* schema:
* type: string
* description: 岗位ID
* responses:
* 200:
* description: 获取成功
*/
'GET /job/detail': async (ctx) => {
const models = Framework.getModels();
const { job_postings } = models;
const { jobId } = ctx.query;
if (!jobId) {
return ctx.fail('岗位ID不能为空');
}
const job = await job_postings.findOne({ where: { jobId } });
if (!job) {
return ctx.fail('岗位不存在');
}
return ctx.success(job);
},
/**
* @swagger
* /admin_api/job/delete:
* post:
* summary: 删除岗位
* description: 删除指定的岗位信息
* tags: [后台-岗位管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - jobId
* properties:
* jobId:
* type: string
* description: 岗位ID
* responses:
* 200:
* description: 删除成功
*/
'POST /job/delete': async (ctx) => {
const models = Framework.getModels();
const { job_postings } = models;
const body = ctx.getBody();
const { jobId } = body;
if (!jobId) {
return ctx.fail('岗位ID不能为空');
}
const result = await job_postings.destroy({ where: { jobId } });
if (result === 0) {
return ctx.fail('岗位不存在');
}
return ctx.success({ message: '岗位删除成功' });
},
/**
* @swagger
* /admin_api/job/greet:
* post:
* summary: 职位打招呼
* description: 向指定职位的HR打招呼通过MQTT发送指令到boss-automation-nodejs
* tags: [后台-岗位管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - sn_code
* - encryptJobId
* properties:
* sn_code:
* type: string
* description: 设备SN码
* encryptJobId:
* type: string
* description: 加密的职位ID
* securityId:
* type: string
* description: 安全ID可选
* brandName:
* type: string
* description: 公司名称
* platform:
* type: string
* description: 平台默认boss
* responses:
* 200:
* description: 打招呼成功
*/
'POST /job/greet': async (ctx) => {
const body = ctx.getBody();
const { sn_code, encryptJobId, securityId, brandName, platform = 'boss' } = body;
const result = await jobManager.job_greet({
sn_code,
encryptJobId,
securityId,
brandName,
platform
});
return ctx.success(result);
}
};

View File

@@ -0,0 +1,322 @@
/**
* 职位类型管理API - 后台管理
* 提供职位类型的查询和管理功能
*/
const Framework = require("../../framework/node-core-framework.js");
module.exports = {
/**
* @swagger
* /admin_api/job_type/list:
* post:
* summary: 获取职位类型列表
* description: 分页获取所有职位类型
* tags: [后台-职位类型管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* name:
* type: string
* description: 职位类型名称(可选)
* responses:
* 200:
* description: 获取成功
*/
'POST /job_type/list': async (ctx) => {
const models = Framework.getModels();
const { job_types, op } = models;
const body = ctx.getBody();
const { name } = body;
// 获取分页参数
const { limit, offset } = ctx.getPageSize();
const where = {};
if (name) {
where.name = { [op.like]: `%${name}%` };
}
const result = await job_types.findAndCountAll({
where,
limit,
offset,
order: [['sort_order', 'ASC'], ['id', 'ASC']]
});
return ctx.success(result);
},
/**
* @swagger
* /admin_api/job_type/detail:
* get:
* summary: 获取职位类型详情
* description: 根据ID获取职位类型详细信息
* tags: [后台-职位类型管理]
* parameters:
* - in: query
* name: id
* required: true
* schema:
* type: integer
* description: 职位类型ID
* responses:
* 200:
* description: 获取成功
*/
'GET /job_type/detail': async (ctx) => {
const models = Framework.getModels();
const { job_types } = models;
const { id } = ctx.getQuery();
if (!id) {
return ctx.fail('职位类型ID不能为空');
}
const jobType = await job_types.findByPk(id);
if (!jobType) {
return ctx.fail('职位类型不存在');
}
return ctx.success(jobType);
},
/**
* @swagger
* /admin_api/job_type/create:
* post:
* summary: 创建职位类型
* description: 创建新的职位类型
* tags: [后台-职位类型管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* properties:
* name:
* type: string
* description: 职位类型名称
* description:
* type: string
* description: 职位类型描述
* commonSkills:
* type: array
* description: 常见技能关键词JSON数组
* excludeKeywords:
* type: array
* description: 排除关键词JSON数组
* is_enabled:
* type: integer
* description: 是否启用1=启用0=禁用)
* sort_order:
* type: integer
* description: 排序顺序
* responses:
* 200:
* description: 创建成功
*/
'POST /job_type/create': async (ctx) => {
const models = Framework.getModels();
const { job_types } = models;
const body = ctx.getBody();
const { name, description, commonSkills, excludeKeywords, is_enabled, sort_order } = body;
if (!name) {
return ctx.fail('职位类型名称不能为空');
}
// 检查名称是否已存在
const existing = await job_types.findOne({ where: { name } });
if (existing) {
return ctx.fail('职位类型名称已存在');
}
const jobType = await job_types.create({
name,
description: description || '',
commonSkills: Array.isArray(commonSkills) ? JSON.stringify(commonSkills) : (commonSkills || '[]'),
excludeKeywords: Array.isArray(excludeKeywords) ? JSON.stringify(excludeKeywords) : (excludeKeywords || '[]'),
is_enabled: is_enabled !== undefined ? is_enabled : 1,
sort_order: sort_order || 0
});
return ctx.success(jobType);
},
/**
* @swagger
* /admin_api/job_type/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: 职位类型名称
* description:
* type: string
* description: 职位类型描述
* commonSkills:
* type: array
* description: 常见技能关键词JSON数组
* excludeKeywords:
* type: array
* description: 排除关键词JSON数组
* is_enabled:
* type: integer
* description: 是否启用1=启用0=禁用)
* sort_order:
* type: integer
* description: 排序顺序
* responses:
* 200:
* description: 更新成功
*/
'POST /job_type/update': async (ctx) => {
const models = Framework.getModels();
const { job_types } = models;
const body = ctx.getBody();
const { id, name, description, commonSkills, excludeKeywords, is_enabled, sort_order } = body;
if (!id) {
return ctx.fail('职位类型ID不能为空');
}
const jobType = await job_types.findByPk(id);
if (!jobType) {
return ctx.fail('职位类型不存在');
}
// 如果更新名称,检查是否与其他记录冲突
if (name && name !== jobType.name) {
const existing = await job_types.findOne({ where: { name } });
if (existing) {
return ctx.fail('职位类型名称已存在');
}
}
const updateData = {};
if (name !== undefined) updateData.name = name;
if (description !== undefined) updateData.description = description;
if (commonSkills !== undefined) {
updateData.commonSkills = Array.isArray(commonSkills) ? JSON.stringify(commonSkills) : commonSkills;
}
if (excludeKeywords !== undefined) {
updateData.excludeKeywords = Array.isArray(excludeKeywords) ? JSON.stringify(excludeKeywords) : excludeKeywords;
}
if (is_enabled !== undefined) updateData.is_enabled = is_enabled;
if (sort_order !== undefined) updateData.sort_order = sort_order;
await job_types.update(updateData, { where: { id } });
// 清除缓存
const jobFilterService = require('../middleware/job/job_filter_service.js');
jobFilterService.clearCache(id);
return ctx.success({ message: '职位类型更新成功' });
},
/**
* @swagger
* /admin_api/job_type/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 /job_type/delete': async (ctx) => {
const models = Framework.getModels();
const { job_types, pla_account } = models;
const { id } = ctx.getBody();
if (!id) {
return ctx.fail('职位类型ID不能为空');
}
const jobType = await job_types.findByPk(id);
if (!jobType) {
return ctx.fail('职位类型不存在');
}
// 检查是否有账号使用此职位类型
const accountsUsing = await pla_account.count({ where: { job_type_id: id } });
if (accountsUsing > 0) {
return ctx.fail(`该职位类型正在被 ${accountsUsing} 个账号使用,无法删除`);
}
await job_types.destroy({ where: { id } });
// 清除缓存
const jobFilterService = require('../middleware/job/job_filter_service.js');
jobFilterService.clearCache(id);
return ctx.success({ message: '职位类型删除成功' });
},
/**
* @swagger
* /admin_api/job_type/all:
* get:
* summary: 获取所有启用的职位类型(用于下拉选择)
* description: 获取所有启用的职位类型列表
* tags: [后台-职位类型管理]
* responses:
* 200:
* description: 获取成功
*/
'GET /job_type/all': async (ctx) => {
const models = Framework.getModels();
const { job_types } = models;
const list = await job_types.findAll({
where: { is_enabled: 1 },
order: [['sort_order', 'ASC'], ['id', 'ASC']],
attributes: ['id', 'name', 'description']
});
return ctx.success(list);
}
};

View File

@@ -0,0 +1,469 @@
/**
* 平台账号管理API - 后台管理
* 提供平台账号的查询和管理功能
*/
const Framework = require("../../framework/node-core-framework.js");
const plaAccountService = require('../services/pla_account_service');
module.exports = {
'GET /pla_account/getById': async (ctx) => {
const { id } = ctx.getQuery();
const accountData = await plaAccountService.getAccountById(id);
return ctx.success(accountData);
},
'POST /account/detail': async (ctx) => {
const { id } = ctx.getBody();
const accountData = await plaAccountService.getAccountById(id);
return ctx.success(accountData);
},
/**
* @swagger
* /admin_api/account/list:
* post:
* summary: 获取账号列表
* description: 分页获取所有平台账号
* tags: [后台-账号管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* key:
* type: string
* description: 搜索字段
* value:
* type: string
* description: 搜索值
* platform_type:
* type: string
* description: 平台类型ID
* is_online:
* type: boolean
* description: 是否在线
* responses:
* 200:
* description: 获取成功
*/
'POST /account/list': async (ctx) => {
const body = ctx.getBody();
const { key, value, platform_type, is_online } = body;
const { limit, offset } = ctx.getPageSize();
const result = await plaAccountService.getAccountList({
key,
value,
platform_type,
is_online,
limit,
offset
});
return ctx.success(result);
},
/**
* @swagger
* /admin_api/account/add:
* post:
* summary: 新增账号
* description: 创建新的平台账号
* tags: [后台-账号管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - sn_code
* - platform_type
* - login_name
* properties:
* name:
* type: string
* description: 账户名
* sn_code:
* type: string
* description: 设备SN码
* platform_type:
* type: string
* description: 平台类型ID
* login_name:
* type: string
* description: 登录名
* pwd:
* type: string
* description: 密码
* keyword:
* type: string
* description: 搜索关键词
* search_url:
* type: string
* description: 搜索页网址
* responses:
* 200:
* description: 创建成功
*/
'POST /account/add': async (ctx) => {
const body = ctx.getBody();
const account = await plaAccountService.createAccount(body);
return ctx.success({ message: '账号创建成功', data: account });
},
/**
* @swagger
* /admin_api/account/update:
* 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 /account/update': async (ctx) => {
const body = ctx.getBody();
const { id, ...updateData } = body;
await plaAccountService.updateAccount(id, updateData);
return ctx.success({ message: '账号信息更新成功' });
},
/**
* @swagger
* /admin_api/account/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 /account/delete': async (ctx) => {
const body = ctx.getBody();
const { id } = body;
await plaAccountService.deleteAccount(id);
return ctx.success({ message: '账号删除成功' });
},
/**
* @swagger
* /admin_api/account/stopTasks:
* post:
* summary: 停止账号的所有任务
* description: 停止指定账号的所有待执行和正在执行的任务
* tags: [后台-账号管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - id
* properties:
* id:
* type: integer
* description: 账号ID
* sn_code:
* type: string
* description: 设备SN码可选如果提供id则不需要
* responses:
* 200:
* description: 停止成功
*/
'POST /account/stopTasks': async (ctx) => {
const body = ctx.getBody();
const result = await plaAccountService.stopTasks(body);
return ctx.success(result);
},
/**
* @swagger
* /admin_api/pla_account/tasks:
* get:
* summary: 获取账号的任务列表
* description: 根据账号ID获取该账号的所有任务列表支持分页
* tags: [后台-账号管理]
* parameters:
* - in: query
* name: id
* required: true
* schema:
* type: integer
* description: 账号ID
* - in: query
* name: page
* schema:
* type: integer
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* description: 每页数量
* responses:
* 200:
* description: 获取成功
*/
'GET /pla_account/tasks': async (ctx) => {
const { id } = ctx.getQuery();
const { limit, offset } = ctx.getPageSize();
const result = await plaAccountService.getAccountTasks({
id,
limit,
offset
});
return ctx.success(result);
},
/**
* @swagger
* /admin_api/pla_account/commands:
* get:
* summary: 获取账号的指令列表
* description: 根据账号ID获取该账号的所有指令列表支持分页
* tags: [后台-账号管理]
* parameters:
* - in: query
* name: id
* required: true
* schema:
* type: integer
* description: 账号ID
* - in: query
* name: page
* schema:
* type: integer
* description: 页码
* - in: query
* name: pageSize
* schema:
* type: integer
* description: 每页数量
* responses:
* 200:
* description: 获取成功
*/
'GET /pla_account/commands': async (ctx) => {
const { id } = ctx.getQuery();
const { limit, offset } = ctx.getPageSize();
const result = await plaAccountService.getAccountCommands({
id,
limit,
offset
});
return ctx.success(result);
},
/**
* @swagger
* /admin_api/pla_account/runTask:
* post:
* summary: 执行账号指令
* description: 为指定账号直接执行指令(如用户登录、获取简历等)
* tags: [后台-账号管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - id
* - taskType
* properties:
* id:
* type: integer
* description: 账号ID
* taskType:
* type: string
* description: 指令类型: get_login_qr_code-登录检查, get_resume-获取简历, search_jobs-搜索岗位
* taskName:
* type: string
* description: 指令名称
* responses:
* 200:
* description: 指令执行成功
*/
'POST /pla_account/runTask': async (ctx) => {
const body = ctx.getBody();
const task = await plaAccountService.runTask(body);
return ctx.success(task);
},
/**
* @swagger
* /admin_api/pla_account/runCommand:
* post:
* summary: 执行账号指令
* description: 为指定账号直接执行指令(如用户登录、获取简历等)
* tags: [后台-账号管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - id
* - commandType
* properties:
* id:
* type: integer
* description: 账号ID
* commandType:
* type: string
* description: 指令类型: get_login_qr_code-登录检查, get_resume-获取简历, search_jobs-搜索岗位
* commandName:
* type: string
* description: 指令名称
* responses:
* 200:
* description: 指令执行成功
*/
'POST /pla_account/runCommand': async (ctx) => {
const body = ctx.getBody();
const result = await plaAccountService.runCommand(body);
return ctx.success(result);
},
/**
* @swagger
* /admin_api/pla_account/commandDetail:
* get:
* summary: 获取指令详情
* description: 根据账号ID和指令ID获取指令的详细信息
* tags: [后台-账号管理]
* parameters:
* - in: query
* name: accountId
* required: true
* schema:
* type: integer
* description: 账号ID
* - in: query
* name: commandId
* required: true
* schema:
* type: integer
* description: 指令ID
* responses:
* 200:
* description: 获取成功
*/
'GET /pla_account/commandDetail': async (ctx) => {
const { accountId, commandId } = ctx.getQuery();
const commandDetail = await plaAccountService.getCommandDetail({
accountId,
commandId
});
return ctx.success(commandDetail);
},
/**
* @swagger
* /admin_api/pla_account/parseLocation:
* post:
* summary: 解析地址并更新经纬度
* description: 根据账号ID解析地址获取经纬度并更新到账号信息中
* tags: [后台-账号管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - id
* properties:
* id:
* type: integer
* description: 账号ID
* address:
* type: string
* description: 地址(可选,如果不提供则使用账号中的地址)
* responses:
* 200:
* description: 解析成功
*/
'POST /pla_account/parseLocation': async (ctx) => {
const body = ctx.getBody();
const result = await plaAccountService.parseLocation(body);
return ctx.success(result);
},
/**
* @swagger
* /admin_api/pla_account/batchParseLocation:
* post:
* summary: 批量解析地址并更新经纬度
* description: 批量解析多个账号的地址,获取经纬度并更新到账号信息中
* tags: [后台-账号管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - ids
* properties:
* ids:
* type: array
* items:
* type: integer
* description: 账号ID数组
* responses:
* 200:
* description: 批量解析完成
*/
'POST /pla_account/batchParseLocation': async (ctx) => {
const body = ctx.getBody();
const { ids } = body;
const result = await plaAccountService.batchParseLocation(ids);
return ctx.success(result);
}
};

View File

@@ -0,0 +1,274 @@
/**
* 简历信息管理API - 后台管理
* 提供简历信息的查询和管理功能
*/
const Framework = require("../../framework/node-core-framework.js");
module.exports = {
/**
* @swagger
* /admin_api/resume/list:
* post:
* summary: 获取简历列表
* description: 分页获取所有简历信息
* tags: [后台-简历管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* minCompetitiveness:
* type: integer
* description: 最小竞争力评分(可选)
* responses:
* 200:
* description: 获取成功
*/
'POST /resume/list': async (ctx) => {
const models = Framework.getModels();
const { resume_info, op } = models;
const body = ctx.getBody();
const { minCompetitiveness, searchText} = ctx.getBody();
// 获取分页参数
const { limit, offset } = ctx.getPageSize();
const where = {};
// 竞争力评分筛选
if (minCompetitiveness !== undefined) {
where.aiCompetitiveness = { [op.gte]: minCompetitiveness };
}
// 支持搜索姓名或技能
if (searchText) {
where[op.or] = [
{ fullName: { [op.like]: `%${searchText}%` } },
{ skills: { [op.like]: `%${searchText}%` } }
];
}
const result = await resume_info.findAndCountAll({
where,
limit,
offset,
order: [
['aiCompetitiveness', 'DESC'],
['id', 'DESC']
]
});
return ctx.success({
total: result.count,
list: result.rows
});
},
/**
* @swagger
* /admin_api/resume/statistics:
* get:
* summary: 获取简历统计
* description: 获取简历信息的统计数据
* tags: [后台-简历管理]
* responses:
* 200:
* description: 获取成功
*/
'GET /resume/statistics': async (ctx) => {
const models = Framework.getModels();
const { resume_info } = models;
const totalResumes = await resume_info.count();
// 平均竞争力
const avgCompetitiveness = await resume_info.findAll({
attributes: [
[models.sequelize.fn('AVG', models.sequelize.col('aiCompetitiveness')), 'avgScore']
],
raw: true
});
// 按工作年限统计
const workYearsStats = await resume_info.findAll({
attributes: [
'workYears',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['workYears'],
raw: true
});
// 竞争力分布
const [
highCompetitiveness,
mediumCompetitiveness,
lowCompetitiveness
] = await Promise.all([
resume_info.count({ where: { aiCompetitiveness: { [models.op.gte]: 80 } } }),
resume_info.count({ where: { aiCompetitiveness: { [models.op.gte]: 60, [models.op.lt]: 80 } } }),
resume_info.count({ where: { aiCompetitiveness: { [models.op.lt]: 60 } } })
]);
return ctx.success({
totalResumes,
averageCompetitiveness: parseFloat(avgCompetitiveness[0]?.avgScore || 0).toFixed(2),
workYearsStats,
competitivenessDistribution: {
high: highCompetitiveness,
medium: mediumCompetitiveness,
low: lowCompetitiveness
}
});
},
/**
* @swagger
* /admin_api/resume/detail:
* get:
* summary: 获取简历详情
* description: 根据简历ID获取详细信息
* tags: [后台-简历管理]
* parameters:
* - in: query
* name: resumeId
* required: true
* schema:
* type: string
* description: 简历ID
* responses:
* 200:
* description: 获取成功
*/
'GET /resume/detail': async (ctx) => {
const models = Framework.getModels();
const { resume_info } = models;
const { resumeId } = ctx.query;
if (!resumeId) {
return ctx.fail('简历ID不能为空');
}
const resume = await resume_info.findOne({ where: { resumeId } });
if (!resume) {
return ctx.fail('简历不存在');
}
return ctx.success(resume);
},
/**
* @swagger
* /admin_api/resume/delete:
* post:
* summary: 删除简历
* description: 删除指定的简历信息
* tags: [后台-简历管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - resumeId
* properties:
* resumeId:
* type: string
* description: 简历ID
* responses:
* 200:
* description: 删除成功
*/
'POST /resume/delete': async (ctx) => {
const models = Framework.getModels();
const { resume_info } = models;
const body = ctx.getBody();
const { resumeId } = body;
if (!resumeId) {
return ctx.fail('简历ID不能为空');
}
const result = await resume_info.destroy({ where: { resumeId } });
if (result === 0) {
return ctx.fail('简历不存在');
}
return ctx.success({ message: '简历删除成功' });
},
/**
* @swagger
* /admin_api/resume/get-by-device:
* get:
* summary: 根据设备获取简历
* description: 获取指定设备和平台的活跃简历
* tags: [后台-简历管理]
* parameters:
* - in: query
* name: sn_code
* required: true
* schema:
* type: string
* description: 设备SN码
* - in: query
* name: platform
* required: true
* schema:
* type: string
* description: 平台
* responses:
* 200:
* description: 获取成功
*/
'GET /resume/get-by-device': async (ctx) => {
const models = Framework.getModels();
const { resume_info } = models;
const { sn_code, platform } = ctx.query;
if (!sn_code || !platform) {
return ctx.fail('设备SN码和平台不能为空');
}
const resume = await resume_info.findOne({
where: { sn_code, platform, isActive: true },
order: [['syncTime', 'DESC']]
});
if (!resume) {
return ctx.fail('未找到活跃简历');
}
const resumeDetail = resume.toJSON();
const jsonFields = ['skills', 'certifications', 'projectExperience', 'workExperience', 'aiSkillTags'];
jsonFields.forEach(field => {
if (resumeDetail[field]) {
resumeDetail[field] = JSON.parse(resumeDetail[field]);
}
});
return ctx.success(resumeDetail);
}
};

View File

@@ -0,0 +1,537 @@
/**
* 统计数据管理API - 后台管理
* 提供首页统计数据的查询功能
*/
const Framework = require("../../framework/node-core-framework.js");
const dayjs = require('dayjs');
module.exports = {
/**
* @swagger
* /admin_api/statistics/overview:
* get:
* summary: 获取今日统计概览
* description: 获取指定设备今日的投递、找工作、聊天、执行中任务数量
* tags: [后台-统计管理]
* parameters:
* - in: query
* name: deviceSn
* required: true
* schema:
* type: string
* description: 设备SN码
* responses:
* 200:
* description: 获取成功
*/
'GET /statistics/overview': async (ctx) => {
const models = Framework.getModels();
const { apply_records, job_postings, chat_records, task_status, op } = models;
const { deviceSn } = ctx.getQuery();
if (!deviceSn) {
return ctx.fail('设备SN码不能为空');
}
const todayStart = dayjs().startOf('day').toDate();
const todayEnd = dayjs().endOf('day').toDate();
const [
applyCount,
jobSearchCount,
chatCount,
runningTaskCount
] = await Promise.all([
apply_records.count({
where: {
sn_code: deviceSn,
applyTime: {
[op.gte]: todayStart,
[op.lte]: todayEnd
}
}
}).catch(err => {
console.error('[统计] 查询投递数量失败:', err);
return 0;
}),
job_postings.count({
where: {
sn_code: deviceSn,
create_time: {
[op.gte]: todayStart,
[op.lte]: todayEnd
}
}
}).catch(err => {
console.error('[统计] 查询找工作数量失败:', err);
return 0;
}),
chat_records.count({
where: {
sn_code: deviceSn,
direction: 'sent',
sendTime: {
[op.gte]: todayStart,
[op.lte]: todayEnd
}
}
}).catch(err => {
console.error('[统计] 查询聊天数量失败:', err);
return 0;
}),
task_status.count({
where: {
sn_code: deviceSn,
status: 'running'
}
}).catch(err => {
console.error('[统计] 查询任务数量失败:', err);
return 0;
})
]);
return ctx.success({
applyCount,
jobSearchCount,
chatCount,
runningTaskCount
});
},
/**
* @swagger
* /admin_api/statistics/daily:
* post:
* summary: 获取按天统计的数据
* description: 获取指定设备指定时间范围内按天统计的数据,用于图表展示
* tags: [后台-统计管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - deviceSn
* properties:
* deviceSn:
* type: string
* description: 设备SN码
* days:
* type: integer
* description: 统计最近几天例如7天
* startDate:
* type: string
* description: 开始日期 (YYYY-MM-DD)
* endDate:
* type: string
* description: 结束日期 (YYYY-MM-DD)
* responses:
* 200:
* description: 获取成功
*/
'POST /statistics/daily': async (ctx) => {
const models = Framework.getModels();
const { apply_records, job_postings, chat_records, op } = models;
const { deviceSn, days, startDate, endDate } = ctx.getBody();
if (!deviceSn) {
return ctx.fail('设备SN码不能为空');
}
let start;
let end;
if (days) {
const maxDays = Math.min(days, 30);
end = dayjs().endOf('day').toDate();
start = dayjs().subtract(maxDays - 1, 'day').startOf('day').toDate();
} else if (startDate && endDate) {
start = dayjs(startDate).startOf('day').toDate();
end = dayjs(endDate).endOf('day').toDate();
const diffDays = dayjs(end).diff(dayjs(start), 'day') + 1;
if (diffDays > 30) {
end = dayjs(start).add(29, 'day').endOf('day').toDate();
}
} else {
end = dayjs().endOf('day').toDate();
start = dayjs().subtract(6, 'day').startOf('day').toDate();
}
const [allApplies, allJobs, allChats] = await Promise.all([
apply_records.findAll({
where: {
sn_code: deviceSn,
applyTime: {
[op.gte]: start,
[op.lte]: end
}
},
attributes: ['applyTime'],
raw: true
}).catch(err => {
console.error('[统计] 查询投递记录失败:', err);
return [];
}),
job_postings.findAll({
where: {
sn_code: deviceSn,
create_time: {
[op.gte]: start,
[op.lte]: end
}
},
attributes: ['create_time'],
raw: true
}).catch(err => {
console.error('[统计] 查询岗位记录失败:', err);
return [];
}),
chat_records.findAll({
where: {
sn_code: deviceSn,
direction: 'sent',
sendTime: {
[op.gte]: start,
[op.lte]: end
}
},
attributes: ['sendTime'],
raw: true
}).catch(err => {
console.error('[统计] 查询聊天记录失败:', err);
return [];
})
]);
const dates = [];
const applyData = [];
const jobSearchData = [];
const chatData = [];
let currentDate = dayjs(start);
while (currentDate.isBefore(end) || currentDate.isSame(end, 'day')) {
const dateStr = currentDate.format('YYYY-MM-DD');
const dayStart = currentDate.startOf('day');
const dayEnd = currentDate.endOf('day');
const applyCount = allApplies.filter(item => {
const itemDate = dayjs(item.applyTime);
return itemDate.isSameOrAfter(dayStart) && itemDate.isSameOrBefore(dayEnd);
}).length;
const jobCount = allJobs.filter(item => {
const itemDate = dayjs(item.create_time);
return itemDate.isSameOrAfter(dayStart) && itemDate.isSameOrBefore(dayEnd);
}).length;
const chatCount = allChats.filter(item => {
const itemDate = dayjs(item.sendTime);
return itemDate.isSameOrAfter(dayStart) && itemDate.isSameOrBefore(dayEnd);
}).length;
dates.push(dateStr);
applyData.push(applyCount);
jobSearchData.push(jobCount);
chatData.push(chatCount);
currentDate = currentDate.add(1, 'day');
}
return ctx.success({
dates,
applyData,
jobSearchData,
chatData
});
},
/**
* @swagger
* /admin_api/statistics/running-tasks:
* get:
* summary: 获取当前正在执行的任务
* description: 获取指定设备当前正在执行的任务及其命令列表
* tags: [后台-统计管理]
* parameters:
* - in: query
* name: deviceSn
* required: true
* schema:
* type: string
* description: 设备SN码
* responses:
* 200:
* description: 获取成功
*/
'GET /statistics/running-tasks': async (ctx) => {
const models = Framework.getModels();
const { task_status, task_commands } = models;
const { deviceSn } = ctx.getQuery();
if (!deviceSn) {
return ctx.fail('设备SN码不能为空');
}
// 查询正在执行的任务
const runningTasks = await task_status.findAll({
where: {
sn_code: deviceSn,
status: 'running'
},
order: [['startTime', 'DESC']],
limit: 10 // 限制最多返回10个任务
});
// 为每个任务查询其命令列表
const tasksWithCommands = await Promise.all(
runningTasks.map(async (task) => {
const commands = await task_commands.findAll({
where: { task_id: task.id },
order: [
['sequence', 'ASC'],
['create_time', 'ASC']
]
});
return {
taskId: task.id,
taskName: task.taskName || task.taskType,
taskType: task.taskType,
startTime: dayjs(task.startTime).format('YYYY-MM-DD HH:mm:ss'),
progress: task.progress || 0,
commands: commands.map(cmd => ({
commandId: cmd.id,
commandName: cmd.command_name,
status: cmd.status || 'pending'
}))
};
})
);
return ctx.success(tasksWithCommands);
},
/**
* @swagger
* /admin_api/statistics/apply:
* post:
* summary: 获取投递数量统计(按天)
* description: 获取指定设备按天的投递数量统计
* tags: [后台-统计管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - deviceSn
* properties:
* deviceSn:
* type: string
* days:
* type: integer
* responses:
* 200:
* description: 获取成功
*/
'POST /statistics/apply': async (ctx) => {
const models = Framework.getModels();
const { apply_records, op } = models;
const { deviceSn, days = 7 } = ctx.getBody();
if (!deviceSn) {
return ctx.fail('设备SN码不能为空');
}
const maxDays = Math.min(days, 30);
const end = dayjs().endOf('day').toDate();
const start = dayjs().subtract(maxDays - 1, 'day').startOf('day').toDate();
const allApplies = await apply_records.findAll({
where: {
sn_code: deviceSn,
applyTime: {
[op.gte]: start,
[op.lte]: end
}
},
attributes: ['applyTime'],
raw: true
}).catch(err => {
console.error('[统计] 查询投递记录失败:', err);
return [];
});
const dates = [];
const data = [];
let currentDate = dayjs(start);
while (currentDate.isBefore(end) || currentDate.isSame(end, 'day')) {
const dateStr = currentDate.format('YYYY-MM-DD');
const dayStart = currentDate.startOf('day');
const dayEnd = currentDate.endOf('day');
const count = allApplies.filter(item => {
const itemDate = dayjs(item.applyTime);
return itemDate.isSameOrAfter(dayStart) && itemDate.isSameOrBefore(dayEnd);
}).length;
dates.push(dateStr);
data.push(count);
currentDate = currentDate.add(1, 'day');
}
return ctx.success({ dates, data });
},
/**
* @swagger
* /admin_api/statistics/job-search:
* post:
* summary: 获取找工作数量统计(按天)
* description: 获取指定设备按天的找工作数量统计
* tags: [后台-统计管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - deviceSn
* properties:
* deviceSn:
* type: string
* days:
* type: integer
* responses:
* 200:
* description: 获取成功
*/
'POST /statistics/job-search': async (ctx) => {
const models = Framework.getModels();
const { job_postings, op } = models;
const { deviceSn, days = 7 } = ctx.getBody();
if (!deviceSn) {
return ctx.fail('设备SN码不能为空');
}
const maxDays = Math.min(days, 30);
const end = dayjs().endOf('day').toDate();
const start = dayjs().subtract(maxDays - 1, 'day').startOf('day').toDate();
const allJobs = await job_postings.findAll({
where: {
sn_code: deviceSn,
create_time: {
[op.gte]: start,
[op.lte]: end
}
},
attributes: ['create_time'],
raw: true
}).catch(err => {
console.error('[统计] 查询岗位记录失败:', err);
return [];
});
const dates = [];
const data = [];
let currentDate = dayjs(start);
while (currentDate.isBefore(end) || currentDate.isSame(end, 'day')) {
const dateStr = currentDate.format('YYYY-MM-DD');
const dayStart = currentDate.startOf('day');
const dayEnd = currentDate.endOf('day');
const count = allJobs.filter(item => {
const itemDate = dayjs(item.create_time);
return itemDate.isSameOrAfter(dayStart) && itemDate.isSameOrBefore(dayEnd);
}).length;
dates.push(dateStr);
data.push(count);
currentDate = currentDate.add(1, 'day');
}
return ctx.success({ dates, data });
},
/**
* @swagger
* /admin_api/statistics/chat:
* post:
* summary: 获取聊天/沟通数量统计(按天)
* description: 获取指定设备按天的聊天数量统计
* tags: [后台-统计管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - deviceSn
* properties:
* deviceSn:
* type: string
* days:
* type: integer
* responses:
* 200:
* description: 获取成功
*/
'POST /statistics/chat': async (ctx) => {
const models = Framework.getModels();
const { chat_records, op } = models;
const { deviceSn, days = 7 } = ctx.getBody();
if (!deviceSn) {
return ctx.fail('设备SN码不能为空');
}
const maxDays = Math.min(days, 30);
const end = dayjs().endOf('day').toDate();
const start = dayjs().subtract(maxDays - 1, 'day').startOf('day').toDate();
const allChats = await chat_records.findAll({
where: {
sn_code: deviceSn,
direction: 'sent',
sendTime: {
[op.gte]: start,
[op.lte]: end
}
},
attributes: ['sendTime'],
raw: true
}).catch(err => {
console.error('[统计] 查询聊天记录失败:', err);
return [];
});
const dates = [];
const data = [];
let currentDate = dayjs(start);
while (currentDate.isBefore(end) || currentDate.isSame(end, 'day')) {
const dateStr = currentDate.format('YYYY-MM-DD');
const dayStart = currentDate.startOf('day');
const dayEnd = currentDate.endOf('day');
const count = allChats.filter(item => {
const itemDate = dayjs(item.sendTime);
return itemDate.isSameOrAfter(dayStart) && itemDate.isSameOrBefore(dayEnd);
}).length;
dates.push(dateStr);
data.push(count);
currentDate = currentDate.add(1, 'day');
}
return ctx.success({ dates, data });
}
};

View File

@@ -0,0 +1,498 @@
/**
* 系统配置管理API - 后台管理
* 提供系统配置的CRUD操作
*/
const Framework = require("../../framework/node-core-framework.js");
const normalizeConfigValue = (config) => {
if (!config) {
return config;
}
const normalized = { ...config };
if (normalized.configType === 'json' && normalized.configValue) {
normalized.configValue = JSON.parse(normalized.configValue);
} else if (normalized.configType === 'boolean') {
normalized.configValue = normalized.configValue === 'true' || normalized.configValue === '1';
} else if (normalized.configType === 'number') {
normalized.configValue = parseFloat(normalized.configValue) || 0;
}
return normalized;
};
const formatConfigValueForSave = (configType, configValue) => {
if (configType === 'json' && typeof configValue === 'object') {
return JSON.stringify(configValue);
}
if (configType === 'boolean') {
return configValue ? 'true' : 'false';
}
return String(configValue);
};
module.exports = {
/**
* @swagger
* /admin_api/config/list:
* post:
* summary: 获取配置列表
* description: 分页获取系统配置列表
* tags: [后台-系统配置]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* category:
* type: string
* description: 配置分类(可选)
* responses:
* 200:
* description: 获取成功
*/
'POST /config/list': async (ctx) => {
const models = Framework.getModels();
const { system_config, op } = models;
const body = ctx.getBody();
const { category, searchText} = ctx.getBody();
// 获取分页参数
const { limit, offset, page, pageSize } = ctx.getPageSize();
const where = {};
if (category) where.category = category;
// 支持搜索配置键或名称
if (searchText) {
where[op.or] = [
{ configKey: { [op.like]: `%${searchText}%` } },
{ configName: { [op.like]: `%${searchText}%` } }
];
}
const result = await system_config.findAndCountAll({
where,
limit,
offset,
order: [
['category', 'ASC'],
['sortOrder', 'ASC'],
['configId', 'ASC']
]
});
const list = result.rows.map(item => normalizeConfigValue(item.toJSON()));
return ctx.success({
total: result.count,
page,
pageSize,
list
});
},
/**
* @swagger
* /admin_api/config/get:
* get:
* summary: 获取配置详情
* description: 根据配置键获取配置详情
* tags: [后台-系统配置]
* parameters:
* - in: query
* name: configKey
* required: true
* schema:
* type: string
* description: 配置键
* responses:
* 200:
* description: 获取成功
*/
'GET /config/get': async (ctx) => {
const models = Framework.getModels();
const { system_config } = models;
const { configKey } = ctx.query;
if (!configKey) {
return ctx.fail('配置键不能为空');
}
const config = await system_config.findOne({ where: { configKey } });
if (!config) {
return ctx.fail('配置不存在');
}
const configData = normalizeConfigValue(config.toJSON());
return ctx.success(configData);
},
/**
* @swagger
* /admin_api/config/add:
* post:
* summary: 添加配置
* description: 添加新的系统配置
* tags: [后台-系统配置]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - configKey
* - configValue
* - category
* properties:
* configKey:
* type: string
* description: 配置键
* configValue:
* type: string
* description: 配置值
* configType:
* type: string
* description: 配置类型
* category:
* type: string
* description: 配置分类
* configName:
* type: string
* description: 配置名称
* description:
* type: string
* description: 配置描述
* responses:
* 200:
* description: 添加成功
*/
'POST /config/add': async (ctx) => {
const models = Framework.getModels();
const { system_config } = models;
const body = ctx.getBody();
const {
configKey,
configValue,
configType = 'string',
category,
configName,
description,
defaultValue,
sortOrder = 0,
isActive = true,
isSystem = false
} = body;
if (!configKey || configValue === undefined || !category) {
return ctx.fail('配置键、配置值和分类不能为空');
}
const existingConfig = await system_config.findOne({ where: { configKey } });
if (existingConfig) {
return ctx.fail('配置键已存在');
}
const finalValue = formatConfigValueForSave(configType, configValue);
await system_config.create({
configKey,
configValue: finalValue,
configType,
category,
configName: configName || configKey,
description: description || '',
defaultValue: defaultValue || finalValue,
sortOrder,
isActive,
isSystem,
isVisible: true,
});
return ctx.success({ message: '配置添加成功' });
},
/**
* @swagger
* /admin_api/config/update:
* post:
* summary: 更新配置
* description: 更新系统配置
* tags: [后台-系统配置]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - configKey
* properties:
* configKey:
* type: string
* description: 配置键
* configValue:
* type: string
* description: 配置值
* description:
* type: string
* description: 配置描述
* responses:
* 200:
* description: 更新成功
*/
'POST /config/update': async (ctx) => {
const models = Framework.getModels();
const { system_config } = models;
const body = ctx.getBody();
const {
configKey,
configValue,
configName,
description,
sortOrder,
isActive
} = body;
if (!configKey) {
return ctx.fail('配置键不能为空');
}
const config = await system_config.findOne({ where: { configKey } });
if (!config) {
return ctx.fail('配置不存在');
}
const updateData = {};
if (configValue !== undefined) {
updateData.configValue = formatConfigValueForSave(config.configType, configValue);
}
if (configName) updateData.configName = configName;
if (description) updateData.description = description;
if (sortOrder !== undefined) updateData.sortOrder = sortOrder;
if (isActive !== undefined) updateData.isActive = isActive;
await system_config.update(updateData, { where: { configKey } });
return ctx.success({ message: '配置更新成功' });
},
/**
* @swagger
* /admin_api/config/delete:
* post:
* summary: 删除配置
* description: 删除系统配置(系统配置不可删除)
* tags: [后台-系统配置]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - configKey
* properties:
* configKey:
* type: string
* description: 配置键
* responses:
* 200:
* description: 删除成功
*/
'POST /config/delete': async (ctx) => {
const models = Framework.getModels();
const { system_config } = models;
const body = ctx.getBody();
const { configKey } = body;
if (!configKey) {
return ctx.fail('配置键不能为空');
}
const config = await system_config.findOne({ where: { configKey } });
if (!config) {
return ctx.fail('配置不存在');
}
if (config.isSystem) {
return ctx.fail('系统配置不可删除');
}
await system_config.destroy({ where: { configKey } });
return ctx.success({ message: '配置删除成功' });
},
/**
* @swagger
* /admin_api/config/categories:
* get:
* summary: 获取配置分类列表
* description: 获取所有配置分类
* tags: [后台-系统配置]
* responses:
* 200:
* description: 获取成功
*/
'GET /config/categories': async (ctx) => {
const models = Framework.getModels();
const { system_config } = models;
const categories = await system_config.findAll({
attributes: [
'category',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['category'],
raw: true
});
return ctx.success(categories);
},
/**
* @swagger
* /admin_api/config/reset:
* post:
* summary: 重置配置为默认值
* description: 将配置重置为默认值
* tags: [后台-系统配置]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - configKey
* properties:
* configKey:
* type: string
* description: 配置键
* responses:
* 200:
* description: 重置成功
*/
'POST /config/reset': async (ctx) => {
const models = Framework.getModels();
const { system_config } = models;
const body = ctx.getBody();
const { configKey } = body;
if (!configKey) {
return ctx.fail('配置键不能为空');
}
const config = await system_config.findOne({ where: { configKey } });
if (!config) {
return ctx.fail('配置不存在');
}
if (!config.defaultValue) {
return ctx.fail('该配置没有默认值');
}
await system_config.update({
configValue: config.defaultValue,
}, { where: { configKey } });
return ctx.success({ message: '配置已重置为默认值' });
},
/**
* @swagger
* /admin_api/config/batch-update:
* post:
* summary: 批量更新配置
* description: 批量更新多个配置
* tags: [后台-系统配置]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - configs
* properties:
* configs:
* type: array
* description: 配置数组
* responses:
* 200:
* description: 更新成功
*/
'POST /config/batch-update': async (ctx) => {
const models = Framework.getModels();
const { system_config } = models;
const body = ctx.getBody();
const { configs } = body;
if (!configs || !Array.isArray(configs) || configs.length === 0) {
return ctx.fail('配置数组不能为空');
}
let successCount = 0;
let failedCount = 0;
for (const item of configs) {
const { configKey, configValue } = item;
if (!configKey || configValue === undefined) {
failedCount++;
continue;
}
const config = await system_config.findOne({ where: { configKey } });
if (!config) {
failedCount++;
continue;
}
const finalValue = formatConfigValueForSave(config.configType, configValue);
await system_config.update({
configValue: finalValue,
}, { where: { configKey } });
successCount++;
}
return ctx.success({
message: '批量更新完成',
total: configs.length,
success: successCount,
failed: failedCount
});
}
};

View File

@@ -0,0 +1,509 @@
/**
* 任务状态管理API - 后台管理
* 提供任务状态的查询和管理功能
*/
const Framework = require("../../framework/node-core-framework.js");
module.exports = {
/**
* @swagger
* /admin_api/task/commands:
* post:
* summary: 获取任务的指令列表
* description: 获取指定任务的所有指令记录
* tags: [后台-任务管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* taskId:
* type: integer
* description: 任务ID
* responses:
* 200:
* description: 获取成功
*/
'POST /task/commands': async (ctx) => {
const models = Framework.getModels();
const { task_commands, op } = models;
const { taskId } = ctx.getBody();
if (!taskId) {
return ctx.error('任务ID不能为空');
}
// 查询该任务的所有指令,按执行顺序排序
const commands = await task_commands.findAll({
where: { task_id: taskId },
order: [
['sequence', 'ASC'],
['create_time', 'ASC']
]
});
return ctx.success(commands);
},
/**
* @swagger
* /admin_api/task/list:
* post:
* summary: 获取任务列表
* description: 分页获取所有任务状态
* tags: [后台-任务管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* page:
* type: integer
* description: 页码
* pageSize:
* type: integer
* description: 每页数量
* taskType:
* type: string
* description: 任务类型(可选)
* status:
* type: string
* description: 任务状态(可选)
* responses:
* 200:
* description: 获取成功
*/
'POST /task/list': async (ctx) => {
const models = Framework.getModels();
const { task_status, op } = models;
const body = ctx.getBody();
const { taskType, status, taskName, taskId, sn_code } = ctx.getBody();
// 获取分页参数
const { limit, offset } = ctx.getPageSize();
const where = {};
if (taskType) where.taskType = taskType;
if (status) where.status = status;
// 支持多种搜索字段
if (taskName) {
where.taskName = { [op.like]: `%${taskName}%` };
}
if (taskId) {
where.taskId = { [op.like]: `%${taskId}%` };
}
if (sn_code) {
where.sn_code = { [op.like]: `%${sn_code}%` };
}
const result = await task_status.findAndCountAll({
where,
limit,
offset,
order: [
['status', 'ASC'],
['startTime', 'DESC']
]
});
return ctx.success({
count: result.count,
rows: result.rows
});
},
/**
* @swagger
* /admin_api/task/statistics:
* get:
* summary: 获取任务统计
* description: 获取任务状态的统计数据
* tags: [后台-任务管理]
* responses:
* 200:
* description: 获取成功
*/
'GET /task/statistics': async (ctx) => {
const models = Framework.getModels();
const { task_status } = models;
const [
totalTasks,
runningTasks,
completedTasks,
failedTasks,
pausedTasks
] = await Promise.all([
task_status.count(),
task_status.count({ where: { status: 'running' } }),
task_status.count({ where: { status: 'completed' } }),
task_status.count({ where: { status: 'failed' } }),
task_status.count({ where: { status: 'paused' } })
]);
// 按任务类型统计
const typeStats = await task_status.findAll({
attributes: [
'taskType',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['taskType'],
raw: true
});
// 按状态统计
const statusStats = await task_status.findAll({
attributes: [
'status',
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
],
group: ['status'],
raw: true
});
// 计算成功率
const successRate = totalTasks > 0 ? ((completedTasks / totalTasks) * 100).toFixed(2) : 0;
return ctx.success({
totalTasks,
runningTasks,
completedTasks,
failedTasks,
pausedTasks,
successRate,
typeStats,
statusStats
});
},
/**
* @swagger
* /admin_api/task/detail:
* get:
* summary: 获取任务详情
* description: 根据任务ID获取详细信息
* tags: [后台-任务管理]
* parameters:
* - in: query
* name: taskId
* required: true
* schema:
* type: string
* description: 任务ID
* responses:
* 200:
* description: 获取成功
*/
'GET /task/detail': async (ctx) => {
const models = Framework.getModels();
const { task_status } = models;
const { taskId } = ctx.query;
if (!taskId) {
return ctx.fail('任务ID不能为空');
}
const task = await task_status.findOne({ where: { taskId } });
if (!task) {
return ctx.fail('任务不存在');
}
return ctx.success(task);
},
/**
* @swagger
* /admin_api/task/update:
* post:
* summary: 更新任务状态
* description: 更新任务的状态或进度
* tags: [后台-任务管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - taskId
* properties:
* taskId:
* type: string
* description: 任务ID
* status:
* type: string
* description: 任务状态
* responses:
* 200:
* description: 更新成功
*/
'POST /task/update': async (ctx) => {
const models = Framework.getModels();
const { task_status } = models;
const body = ctx.getBody();
const { taskId, status, progress, errorMessage } = body;
if (!taskId) {
return ctx.fail('任务ID不能为空');
}
const updateData = {
};
if (status) {
updateData.status = status;
if (status === 'completed') {
updateData.endTime = new Date();
updateData.progress = 100;
} else if (status === 'failed') {
updateData.endTime = new Date();
}
}
if (progress !== undefined) updateData.progress = progress;
if (errorMessage) updateData.errorMessage = errorMessage;
await task_status.update(updateData, { where: { taskId } });
return ctx.success({ message: '任务状态更新成功' });
},
/**
* @swagger
* /admin_api/task/delete:
* post:
* summary: 删除任务
* description: 删除指定的任务记录
* tags: [后台-任务管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - taskId
* properties:
* taskId:
* type: string
* description: 任务ID
* responses:
* 200:
* description: 删除成功
*/
'POST /task/delete': async (ctx) => {
const models = Framework.getModels();
const { task_status } = models;
const body = ctx.getBody();
const { taskId } = body;
if (!taskId) {
return ctx.fail('任务ID不能为空');
}
const result = await task_status.destroy({ where: { taskId } });
if (result === 0) {
return ctx.fail('任务不存在');
}
return ctx.success({ message: '任务删除成功' });
},
/**
* @swagger
* /admin_api/task/cancel:
* post:
* summary: 取消任务
* description: 取消正在执行或待执行的任务
* tags: [后台-任务管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - taskId
* properties:
* taskId:
* type: string
* description: 任务ID
* responses:
* 200:
* description: 取消成功
*/
'POST /task/cancel': async (ctx) => {
const models = Framework.getModels();
const { task_status } = models;
const body = ctx.getBody();
const { taskId } = body;
if (!taskId) {
return ctx.fail('任务ID不能为空');
}
const task = await task_status.findOne({ where: { taskId } });
if (!task) {
return ctx.fail('任务不存在');
}
if (task.status !== 'pending' && task.status !== 'running') {
return ctx.fail('只能取消待执行或执行中的任务');
}
await task_status.update({
status: 'cancelled',
endTime: new Date(),
}, { where: { taskId } });
return ctx.success({ message: '任务取消成功' });
},
/**
* @swagger
* /admin_api/task/retry:
* post:
* summary: 重试任务
* description: 重新执行失败的任务
* tags: [后台-任务管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - taskId
* properties:
* taskId:
* type: string
* description: 任务ID
* responses:
* 200:
* description: 重试成功
*/
'POST /task/retry': async (ctx) => {
const models = Framework.getModels();
const { task_status } = models;
const body = ctx.getBody();
const { id } = body;
if (!id) {
return ctx.fail('任务ID不能为空');
}
const task = await task_status.findOne({ where: { id } });
if (!task) {
return ctx.fail('任务不存在');
}
if (task.status !== 'failed') {
return ctx.fail('只能重试失败的任务');
}
await task_status.update({
status: 'pending',
progress: 0,
errorMessage: '',
errorStack: '',
startTime: null,
endTime: null
}, { where: { id } });
return ctx.success({ message: '任务重试成功' });
},
/**
* @swagger
* /admin_api/task/export:
* post:
* summary: 导出任务列表
* description: 导出任务列表为CSV文件
* tags: [后台-任务管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* taskType:
* type: string
* description: 任务类型(可选)
* status:
* type: string
* description: 任务状态(可选)
* responses:
* 200:
* description: 导出成功
*/
'POST /task/export': async (ctx) => {
const models = Framework.getModels();
const { task_status, op } = models;
const body = ctx.getBody();
const { taskType, status, taskName, id, sn_code } = body;
const where = {};
if (taskType) where.taskType = taskType;
if (status) where.status = status;
if (taskName) where.taskName = { [op.like]: `%${taskName}%` };
if (id) where.id = id;
if (sn_code) where.sn_code = { [op.like]: `%${sn_code}%` };
const tasks = await task_status.findAll({
where,
order: [
['status', 'ASC'],
['startTime', 'DESC']
]
});
// 生成CSV内容
const headers = ['任务ID', '设备SN码', '任务类型', '任务名称', '任务状态', '进度', '开始时间', '结束时间', '创建时间', '错误信息'];
const rows = tasks.map(task => [
task.id,
task.sn_code,
task.taskType,
task.taskName || '',
task.status,
task.progress || 0,
task.startTime || '',
task.endTime || '',
task.errorMessage || ''
]);
let csv = headers.join(',') + '\n';
csv += rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','))
.join('\n');
// 添加 BOM 以支持 Excel 正确识别 UTF-8
const bom = '\uFEFF';
const content = bom + csv;
ctx.set('Content-Type', 'text/csv; charset=utf-8');
ctx.set('Content-Disposition', `attachment; filename="tasks_${Date.now()}.csv"`);
return ctx.success(content);
}
};