1
This commit is contained in:
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
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user