From 75071ea38278f2842513bec18eb667de7bf1a66c Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Fri, 17 Apr 2026 11:13:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(tickets):=20=E5=A2=9E=E5=BC=BA=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8E=A5=E5=8F=A3=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=8A=B6=E6=80=81=E6=B5=81=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/tickets.js | 180 +++++++++++++++++++++++++++++++------- 1 file changed, 150 insertions(+), 30 deletions(-) diff --git a/backend/routes/tickets.js b/backend/routes/tickets.js index fd0eb5d..7199a8e 100644 --- a/backend/routes/tickets.js +++ b/backend/routes/tickets.js @@ -32,6 +32,11 @@ router.get('/stats', async (req, res) => { const Sequelize = require('sequelize'); + const avgTimeExpr = + dbDialect === 'mysql' + ? Sequelize.literal('AVG(TIMESTAMPDIFF(HOUR, Ticket.createdAt, Ticket.updatedAt))') + : Sequelize.literal('AVG((julianday(Ticket.updatedAt) - julianday(Ticket.createdAt)) * 24)'); + const [ total, statusStats, @@ -41,30 +46,35 @@ router.get('/stats', async (req, res) => { deviceStats, dailyCreatedStats, dailyCompletedStats, + dailyClosedStats, + dailyInProgressStats, + dailyPendingStats, + priorityCompletedStats, + categoryCompletedStats, ] = await Promise.all([ Ticket.count({ where }), Ticket.findAll({ where, attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']], - group: ['status'], + group: [Sequelize.col('Ticket.status')], }), Ticket.findAll({ where, attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']], - group: ['priority'], + group: [Sequelize.col('Ticket.priority')], }), Ticket.findAll({ where, attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']], - group: ['faultCategory'], + group: [Sequelize.col('Ticket.faultCategory')], }), Ticket.findAll({ where, attributes: [ [ dbDialect === 'mysql' - ? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m') - : Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')), + ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.createdAt'), '%Y-%m') + : Sequelize.fn('strftime', '%Y-%m', Sequelize.col('Ticket.createdAt')), 'month', ], [Sequelize.fn('COUNT', '*'), 'count'], @@ -79,19 +89,20 @@ router.get('/stats', async (req, res) => { 'deviceId', 'deviceName', [Sequelize.fn('COUNT', '*'), 'count'], - [Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime'], + [Sequelize.fn('MAX', Sequelize.col('Ticket.createdAt')), 'lastFaultTime'], ], - group: ['deviceId', 'deviceName'], + group: [Sequelize.col('Ticket.deviceId'), 'deviceName'], order: [[Sequelize.fn('COUNT', '*'), 'DESC']], limit: 10, + include: [{ model: Device, attributes: ['type'] }], }), Ticket.findAll({ where, attributes: [ [ dbDialect === 'mysql' - ? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d') - : Sequelize.fn('date', Sequelize.col('createdAt')), + ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.createdAt'), '%Y-%m-%d') + : Sequelize.fn('date', Sequelize.col('Ticket.createdAt')), 'date', ], [Sequelize.fn('COUNT', '*'), 'created'], @@ -107,8 +118,8 @@ router.get('/stats', async (req, res) => { attributes: [ [ dbDialect === 'mysql' - ? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d') - : Sequelize.fn('date', Sequelize.col('updatedAt')), + ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.updatedAt'), '%Y-%m-%d') + : Sequelize.fn('date', Sequelize.col('Ticket.updatedAt')), 'date', ], [Sequelize.fn('COUNT', '*'), 'completed'], @@ -116,6 +127,81 @@ router.get('/stats', async (req, res) => { group: ['date'], order: [['date', 'ASC']], }), + Ticket.findAll({ + where: { + ...where, + status: 'closed', + }, + attributes: [ + [ + dbDialect === 'mysql' + ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.updatedAt'), '%Y-%m-%d') + : Sequelize.fn('date', Sequelize.col('Ticket.updatedAt')), + 'date', + ], + [Sequelize.fn('COUNT', '*'), 'closed'], + ], + group: ['date'], + order: [['date', 'ASC']], + }), + Ticket.findAll({ + where: { + ...where, + status: 'in_progress', + }, + attributes: [ + [ + dbDialect === 'mysql' + ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.updatedAt'), '%Y-%m-%d') + : Sequelize.fn('date', Sequelize.col('Ticket.updatedAt')), + 'date', + ], + [Sequelize.fn('COUNT', '*'), 'inProgress'], + ], + group: ['date'], + order: [['date', 'ASC']], + }), + Ticket.findAll({ + where: { + ...where, + status: 'pending', + }, + attributes: [ + [ + dbDialect === 'mysql' + ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.createdAt'), '%Y-%m-%d') + : Sequelize.fn('date', Sequelize.col('Ticket.createdAt')), + 'date', + ], + [Sequelize.fn('COUNT', '*'), 'pending'], + ], + group: ['date'], + order: [['date', 'ASC']], + }), + Ticket.findAll({ + where: { + ...where, + status: 'completed', + }, + attributes: [ + 'priority', + [Sequelize.fn('COUNT', '*'), 'completed'], + [avgTimeExpr, 'avgTime'], + ], + group: [Sequelize.col('Ticket.priority')], + }), + Ticket.findAll({ + where: { + ...where, + status: 'completed', + }, + attributes: [ + 'faultCategory', + [Sequelize.fn('COUNT', '*'), 'completed'], + [avgTimeExpr, 'avgTime'], + ], + group: [Sequelize.col('Ticket.faultCategory')], + }), ]); const statusData = statusStats.map(s => s.dataValues); @@ -130,26 +216,42 @@ router.get('/stats', async (req, res) => { percentage: total > 0 ? (item.count / total) * 100 : 0, })); - const byPriority = priorityStats.map(p => ({ - priority: p.dataValues.priority, - count: p.dataValues.count, - completed: 0, - avgTime: 0, - })); + const byPriority = priorityStats.map(p => { + const priority = p.dataValues.priority; + const completedData = priorityCompletedStats.find( + pc => pc.dataValues.priority === priority + ); + return { + priority, + count: p.dataValues.count, + completed: completedData?.dataValues?.completed || 0, + avgTime: completedData?.dataValues?.avgTime + ? parseFloat(completedData.dataValues.avgTime) + : 0, + }; + }); - const byCategory = categoryStats.map(c => ({ - category: c.dataValues.faultCategory, - count: c.dataValues.count, - completed: 0, - avgTime: 0, - })); + const byCategory = categoryStats.map(c => { + const category = c.dataValues.faultCategory; + const completedData = categoryCompletedStats.find( + cc => cc.dataValues.faultCategory === category + ); + return { + category, + count: c.dataValues.count, + completed: completedData?.dataValues?.completed || 0, + avgTime: completedData?.dataValues?.avgTime + ? parseFloat(completedData.dataValues.avgTime) + : 0, + }; + }); const byDevice = deviceStats.map(d => ({ deviceId: d.deviceId, deviceName: d.deviceName, count: d.dataValues.count, lastFaultTime: d.dataValues.lastFaultTime, - deviceType: '', + deviceType: d.Device?.type || '', })); const createdMap = {}; @@ -160,17 +262,35 @@ router.get('/stats', async (req, res) => { dailyCompletedStats.forEach(d => { completedMap[d.dataValues.date] = d.dataValues.completed; }); + const closedMap = {}; + dailyClosedStats.forEach(d => { + closedMap[d.dataValues.date] = d.dataValues.closed; + }); + const inProgressMap = {}; + dailyInProgressStats.forEach(d => { + inProgressMap[d.dataValues.date] = d.dataValues.inProgress; + }); + const pendingMap = {}; + dailyPendingStats.forEach(d => { + pendingMap[d.dataValues.date] = d.dataValues.pending; + }); const allDates = [ - ...new Set([...Object.keys(createdMap), ...Object.keys(completedMap)]), + ...new Set([ + ...Object.keys(createdMap), + ...Object.keys(completedMap), + ...Object.keys(closedMap), + ...Object.keys(inProgressMap), + ...Object.keys(pendingMap), + ]), ].sort(); const trend = allDates.map(date => ({ date, created: createdMap[date] || 0, completed: completedMap[date] || 0, - closed: 0, - inProgress: 0, - pending: 0, + closed: closedMap[date] || 0, + inProgress: inProgressMap[date] || 0, + pending: pendingMap[date] || 0, })); const completedTickets = await Ticket.findAll({ @@ -696,8 +816,8 @@ router.put('/:ticketId/status', async (req, res) => { // 状态机校验:定义合法的状态流转 const STATUS_TRANSITIONS = { - pending: ['processing', 'closed'], - processing: ['completed', 'closed'], + pending: ['in_progress', 'closed'], + in_progress: ['completed', 'closed'], completed: ['closed'], closed: [], };