/** * 统计数据管理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 }); } };