feat(tickets): 增强统计接口功能并修复状态流转

This commit is contained in:
zhang1106
2026-04-17 11:13:25 +08:00
parent 7df6c73d7b
commit 75071ea382
+148 -28
View File
@@ -32,6 +32,11 @@ router.get('/stats', async (req, res) => {
const Sequelize = require('sequelize'); 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 [ const [
total, total,
statusStats, statusStats,
@@ -41,30 +46,35 @@ router.get('/stats', async (req, res) => {
deviceStats, deviceStats,
dailyCreatedStats, dailyCreatedStats,
dailyCompletedStats, dailyCompletedStats,
dailyClosedStats,
dailyInProgressStats,
dailyPendingStats,
priorityCompletedStats,
categoryCompletedStats,
] = await Promise.all([ ] = await Promise.all([
Ticket.count({ where }), Ticket.count({ where }),
Ticket.findAll({ Ticket.findAll({
where, where,
attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']], attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['status'], group: [Sequelize.col('Ticket.status')],
}), }),
Ticket.findAll({ Ticket.findAll({
where, where,
attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']], attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['priority'], group: [Sequelize.col('Ticket.priority')],
}), }),
Ticket.findAll({ Ticket.findAll({
where, where,
attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']], attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']],
group: ['faultCategory'], group: [Sequelize.col('Ticket.faultCategory')],
}), }),
Ticket.findAll({ Ticket.findAll({
where, where,
attributes: [ attributes: [
[ [
dbDialect === 'mysql' dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m') ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.createdAt'), '%Y-%m')
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')), : Sequelize.fn('strftime', '%Y-%m', Sequelize.col('Ticket.createdAt')),
'month', 'month',
], ],
[Sequelize.fn('COUNT', '*'), 'count'], [Sequelize.fn('COUNT', '*'), 'count'],
@@ -79,19 +89,20 @@ router.get('/stats', async (req, res) => {
'deviceId', 'deviceId',
'deviceName', 'deviceName',
[Sequelize.fn('COUNT', '*'), 'count'], [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']], order: [[Sequelize.fn('COUNT', '*'), 'DESC']],
limit: 10, limit: 10,
include: [{ model: Device, attributes: ['type'] }],
}), }),
Ticket.findAll({ Ticket.findAll({
where, where,
attributes: [ attributes: [
[ [
dbDialect === 'mysql' dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m-%d') ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.createdAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('createdAt')), : Sequelize.fn('date', Sequelize.col('Ticket.createdAt')),
'date', 'date',
], ],
[Sequelize.fn('COUNT', '*'), 'created'], [Sequelize.fn('COUNT', '*'), 'created'],
@@ -107,8 +118,8 @@ router.get('/stats', async (req, res) => {
attributes: [ attributes: [
[ [
dbDialect === 'mysql' dbDialect === 'mysql'
? Sequelize.fn('DATE_FORMAT', Sequelize.col('updatedAt'), '%Y-%m-%d') ? Sequelize.fn('DATE_FORMAT', Sequelize.col('Ticket.updatedAt'), '%Y-%m-%d')
: Sequelize.fn('date', Sequelize.col('updatedAt')), : Sequelize.fn('date', Sequelize.col('Ticket.updatedAt')),
'date', 'date',
], ],
[Sequelize.fn('COUNT', '*'), 'completed'], [Sequelize.fn('COUNT', '*'), 'completed'],
@@ -116,6 +127,81 @@ router.get('/stats', async (req, res) => {
group: ['date'], group: ['date'],
order: [['date', 'ASC']], 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); 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, percentage: total > 0 ? (item.count / total) * 100 : 0,
})); }));
const byPriority = priorityStats.map(p => ({ const byPriority = priorityStats.map(p => {
priority: p.dataValues.priority, const priority = p.dataValues.priority;
const completedData = priorityCompletedStats.find(
pc => pc.dataValues.priority === priority
);
return {
priority,
count: p.dataValues.count, count: p.dataValues.count,
completed: 0, completed: completedData?.dataValues?.completed || 0,
avgTime: 0, avgTime: completedData?.dataValues?.avgTime
})); ? parseFloat(completedData.dataValues.avgTime)
: 0,
};
});
const byCategory = categoryStats.map(c => ({ const byCategory = categoryStats.map(c => {
category: c.dataValues.faultCategory, const category = c.dataValues.faultCategory;
const completedData = categoryCompletedStats.find(
cc => cc.dataValues.faultCategory === category
);
return {
category,
count: c.dataValues.count, count: c.dataValues.count,
completed: 0, completed: completedData?.dataValues?.completed || 0,
avgTime: 0, avgTime: completedData?.dataValues?.avgTime
})); ? parseFloat(completedData.dataValues.avgTime)
: 0,
};
});
const byDevice = deviceStats.map(d => ({ const byDevice = deviceStats.map(d => ({
deviceId: d.deviceId, deviceId: d.deviceId,
deviceName: d.deviceName, deviceName: d.deviceName,
count: d.dataValues.count, count: d.dataValues.count,
lastFaultTime: d.dataValues.lastFaultTime, lastFaultTime: d.dataValues.lastFaultTime,
deviceType: '', deviceType: d.Device?.type || '',
})); }));
const createdMap = {}; const createdMap = {};
@@ -160,17 +262,35 @@ router.get('/stats', async (req, res) => {
dailyCompletedStats.forEach(d => { dailyCompletedStats.forEach(d => {
completedMap[d.dataValues.date] = d.dataValues.completed; 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 = [ 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(); ].sort();
const trend = allDates.map(date => ({ const trend = allDates.map(date => ({
date, date,
created: createdMap[date] || 0, created: createdMap[date] || 0,
completed: completedMap[date] || 0, completed: completedMap[date] || 0,
closed: 0, closed: closedMap[date] || 0,
inProgress: 0, inProgress: inProgressMap[date] || 0,
pending: 0, pending: pendingMap[date] || 0,
})); }));
const completedTickets = await Ticket.findAll({ const completedTickets = await Ticket.findAll({
@@ -696,8 +816,8 @@ router.put('/:ticketId/status', async (req, res) => {
// 状态机校验:定义合法的状态流转 // 状态机校验:定义合法的状态流转
const STATUS_TRANSITIONS = { const STATUS_TRANSITIONS = {
pending: ['processing', 'closed'], pending: ['in_progress', 'closed'],
processing: ['completed', 'closed'], in_progress: ['completed', 'closed'],
completed: ['closed'], completed: ['closed'],
closed: [], closed: [],
}; };