394 lines
12 KiB
JavaScript
394 lines
12 KiB
JavaScript
/**
|
|
* 数据统计和报表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 {
|
|
pla_account,
|
|
task_status,
|
|
job_postings,
|
|
apply_records,
|
|
chat_records,
|
|
op
|
|
} = models;
|
|
const deviceManager = require('../middleware/schedule/deviceManager');
|
|
|
|
// 设备统计(从 pla_account 和 deviceManager 获取)
|
|
const totalDevices = await pla_account.count({ where: { is_delete: 0 } });
|
|
const deviceStatus = deviceManager.getAllDevicesStatus();
|
|
const onlineDevices = Object.values(deviceStatus).filter(d => d.isOnline).length;
|
|
const runningDevices = 0; // 不再维护运行状态
|
|
|
|
// 任务统计
|
|
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 { pla_account, task_status, job_postings, apply_records, chat_records } = models;
|
|
const deviceManager = require('../middleware/schedule/deviceManager');
|
|
|
|
// 从 pla_account 获取所有账号
|
|
const accounts = await pla_account.findAll({
|
|
where: { is_delete: 0 },
|
|
attributes: ['id', 'sn_code', 'name'],
|
|
limit: 20
|
|
});
|
|
|
|
// 获取设备在线状态
|
|
const deviceStatus = deviceManager.getAllDevicesStatus();
|
|
|
|
// 为每个账号统计任务、岗位、投递、聊天数据
|
|
const performanceData = await Promise.all(accounts.map(async (account) => {
|
|
const snCode = account.sn_code;
|
|
const status = deviceStatus[snCode] || { isOnline: false };
|
|
|
|
// 统计任务
|
|
const [completedTasks, failedTasks] = await Promise.all([
|
|
task_status.count({ where: { sn_code: snCode, status: 'completed' } }),
|
|
task_status.count({ where: { sn_code: snCode, status: 'failed' } })
|
|
]);
|
|
|
|
// 统计岗位、投递、聊天(如果有相关字段)
|
|
const [jobsSearched, applies, chats] = await Promise.all([
|
|
job_postings.count({ where: { sn_code: snCode } }).catch(() => 0),
|
|
apply_records.count({ where: { sn_code: snCode } }).catch(() => 0),
|
|
chat_records.count({ where: { sn_code: snCode } }).catch(() => 0)
|
|
]);
|
|
|
|
const total = completedTasks + failedTasks;
|
|
const successRate = total > 0 ? ((completedTasks / total) * 100).toFixed(2) : 0;
|
|
|
|
return {
|
|
sn_code: snCode,
|
|
deviceName: account.name || snCode,
|
|
tasksCompleted: completedTasks,
|
|
tasksFailed: failedTasks,
|
|
jobsSearched,
|
|
applies,
|
|
chats,
|
|
successRate,
|
|
healthScore: status.isOnline ? 100 : 0,
|
|
onlineDuration: 0 // 不再维护在线时长
|
|
};
|
|
}));
|
|
|
|
// 按完成任务数排序
|
|
performanceData.sort((a, b) => b.tasksCompleted - a.tasksCompleted);
|
|
|
|
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
|
|
});
|
|
|
|
}
|
|
};
|
|
|