1
This commit is contained in:
321
api/controller_admin/README.md
Normal file
321
api/controller_admin/README.md
Normal 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 操作和统计分析功能!🚀
|
||||
|
||||
268
api/controller_admin/apply_records.js
Normal file
268
api/controller_admin/apply_records.js
Normal 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: '投递记录删除成功' });
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
499
api/controller_admin/chat_records.js
Normal file
499
api/controller_admin/chat_records.js
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
381
api/controller_admin/dashboard.js
Normal file
381
api/controller_admin/dashboard.js
Normal 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
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
277
api/controller_admin/device_monitor.js
Normal file
277
api/controller_admin/device_monitor.js
Normal 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: '设备删除成功' });
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
292
api/controller_admin/job_postings.js
Normal file
292
api/controller_admin/job_postings.js
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
322
api/controller_admin/job_types.js
Normal file
322
api/controller_admin/job_types.js
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
469
api/controller_admin/pla_account.js
Normal file
469
api/controller_admin/pla_account.js
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
274
api/controller_admin/resume_info.js
Normal file
274
api/controller_admin/resume_info.js
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
537
api/controller_admin/statistics.js
Normal file
537
api/controller_admin/statistics.js
Normal 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 });
|
||||
}
|
||||
};
|
||||
498
api/controller_admin/system_config.js
Normal file
498
api/controller_admin/system_config.js
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
509
api/controller_admin/task_status.js
Normal file
509
api/controller_admin/task_status.js
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user