feat(tickets): 增强统计接口功能并修复状态流转
This commit is contained in:
+148
-28
@@ -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: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user