Files
autoAiWorkSys/api/controller_admin/dashboard.js
张成 891dfc5777 1
2025-12-18 18:32:11 +08:00

388 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 totalDevices = await pla_account.count({ where: { is_delete: 0 } });
const onlineDevices = await pla_account.count({ where: { is_delete: 0, is_online: 1 } });
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;
// 从 pla_account 获取所有账号
const accounts = await pla_account.findAll({
where: { is_delete: 0 },
attributes: ['id', 'sn_code', 'name', 'is_online'],
limit: 20
});
// 为每个账号统计任务、岗位、投递、聊天数据
const performanceData = await Promise.all(accounts.map(async (account) => {
const snCode = account.sn_code;
const isOnline = account.is_online === 1;
// 统计任务
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: 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
});
}
};