/** * 数据统计和报表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 }); } };