feat: 添加数据库索引优化查询性能
refactor(前端): 使用useMemo和useCallback优化性能 perf(后端): 优化统计查询性能 style: 统一前端样式定义 build: 添加创建索引脚本
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
const { sequelize } = require('./db');
|
||||
|
||||
const createIndexes = async () => {
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
console.log('开始创建数据库索引...');
|
||||
|
||||
try {
|
||||
// Device 表索引
|
||||
console.log('创建 devices 表索引...');
|
||||
await queryInterface.addIndex('devices', ['status']);
|
||||
await queryInterface.addIndex('devices', ['type']);
|
||||
await queryInterface.addIndex('devices', ['rackId']);
|
||||
await queryInterface.addIndex('devices', ['createdAt']);
|
||||
await queryInterface.addIndex('devices', ['status', 'type']);
|
||||
await queryInterface.addIndex('devices', ['name']);
|
||||
console.log(' ✓ devices 表索引创建完成');
|
||||
|
||||
// User 表索引
|
||||
console.log('创建 users 表索引...');
|
||||
await queryInterface.addIndex('users', ['status']);
|
||||
await queryInterface.addIndex('users', ['username']);
|
||||
await queryInterface.addIndex('users', ['email']);
|
||||
console.log(' ✓ users 表索引创建完成');
|
||||
|
||||
// Consumable 表索引
|
||||
console.log('创建 consumables 表索引...');
|
||||
await queryInterface.addIndex('consumables', ['category']);
|
||||
await queryInterface.addIndex('consumables', ['status']);
|
||||
await queryInterface.addIndex('consumables', ['category', 'status']);
|
||||
console.log(' ✓ consumables 表索引创建完成');
|
||||
|
||||
// ConsumableRecord 表索引
|
||||
console.log('创建 consumable_records 表索引...');
|
||||
await queryInterface.addIndex('consumable_records', ['consumableId']);
|
||||
await queryInterface.addIndex('consumable_records', ['type']);
|
||||
await queryInterface.addIndex('consumable_records', ['createdAt']);
|
||||
console.log(' ✓ consumable_records 表索引创建完成');
|
||||
|
||||
// ConsumableLog 表索引
|
||||
console.log('创建 consumable_logs 表索引...');
|
||||
await queryInterface.addIndex('consumable_logs', ['consumableId']);
|
||||
await queryInterface.addIndex('consumable_logs', ['operationType']);
|
||||
await queryInterface.addIndex('consumable_logs', ['createdAt']);
|
||||
await queryInterface.addIndex('consumable_logs', ['consumableId', 'createdAt']);
|
||||
console.log(' ✓ consumable_logs 表索引创建完成');
|
||||
|
||||
// OperationLog 表索引
|
||||
console.log('创建 operation_logs 表索引...');
|
||||
await queryInterface.addIndex('operation_logs', ['userId']);
|
||||
await queryInterface.addIndex('operation_logs', ['action']);
|
||||
await queryInterface.addIndex('operation_logs', ['module']);
|
||||
await queryInterface.addIndex('operation_logs', ['createdAt']);
|
||||
await queryInterface.addIndex('operation_logs', ['userId', 'createdAt']);
|
||||
console.log(' ✓ operation_logs 表索引创建完成');
|
||||
|
||||
// LoginHistory 表索引
|
||||
console.log('创建 login_histories 表索引...');
|
||||
await queryInterface.addIndex('login_histories', ['userId']);
|
||||
await queryInterface.addIndex('login_histories', ['loginTime']);
|
||||
await queryInterface.addIndex('login_histories', ['loginType']);
|
||||
await queryInterface.addIndex('login_histories', ['userId', 'loginTime']);
|
||||
console.log(' ✓ login_histories 表索引创建完成');
|
||||
|
||||
// Rack 表索引
|
||||
console.log('创建 racks 表索引...');
|
||||
await queryInterface.addIndex('racks', ['roomId']);
|
||||
await queryInterface.addIndex('racks', ['status']);
|
||||
await queryInterface.addIndex('racks', ['roomId', 'status']);
|
||||
console.log(' ✓ racks 表索引创建完成');
|
||||
|
||||
// Room 表索引
|
||||
console.log('创建 rooms 表索引...');
|
||||
await queryInterface.addIndex('rooms', ['status']);
|
||||
await queryInterface.addIndex('rooms', ['name']);
|
||||
console.log(' ✓ rooms 表索引创建完成');
|
||||
|
||||
console.log('\n✅ 所有索引创建完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建索引失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const checkIndexes = async () => {
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
const tables = [
|
||||
'devices', 'users', 'consumables', 'consumable_records',
|
||||
'consumable_logs', 'operation_logs', 'login_histories',
|
||||
'racks', 'rooms'
|
||||
];
|
||||
|
||||
console.log('\n检查现有索引...');
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const indexes = await queryInterface.showIndex(table);
|
||||
console.log(`\n${table} 表索引:`);
|
||||
indexes.forEach(idx => {
|
||||
console.log(` - ${idx.name}: [${idx.fields.join(', ')}]`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ${table} 表检查失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dropIndexes = async () => {
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
console.log('开始删除自定义索引...');
|
||||
|
||||
try {
|
||||
const indexDefinitions = [
|
||||
{ table: 'devices', indexes: ['status', 'type', 'rackId', 'createdAt', 'status_type', 'name'] },
|
||||
{ table: 'users', indexes: ['status', 'username', 'email'] },
|
||||
{ table: 'consumables', indexes: ['category', 'status', 'category_status'] },
|
||||
{ table: 'consumable_records', indexes: ['consumableId', 'type', 'createdAt'] },
|
||||
{ table: 'consumable_logs', indexes: ['consumableId', 'operationType', 'createdAt', 'consumableId_createdAt'] },
|
||||
{ table: 'operation_logs', indexes: ['userId', 'action', 'module', 'createdAt', 'userId_createdAt'] },
|
||||
{ table: 'login_histories', indexes: ['userId', 'loginTime', 'loginType', 'userId_loginTime'] },
|
||||
{ table: 'racks', indexes: ['roomId', 'status', 'roomId_status'] },
|
||||
{ table: 'rooms', indexes: ['status', 'name'] }
|
||||
];
|
||||
|
||||
for (const def of indexDefinitions) {
|
||||
console.log(`处理 ${def.table} 表...`);
|
||||
const existingIndexes = await queryInterface.showIndex(def.table);
|
||||
for (const existing of existingIndexes) {
|
||||
if (def.indexes.includes(existing.name)) {
|
||||
await queryInterface.removeIndex(def.table, existing.name);
|
||||
console.log(` ✓ 删除索引: ${existing.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ 索引删除完成!');
|
||||
} catch (error) {
|
||||
console.error('删除索引失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { createIndexes, checkIndexes, dropIndexes };
|
||||
|
||||
if (require.main === module) {
|
||||
const command = process.argv[2] || 'create';
|
||||
|
||||
sequelize.authenticate()
|
||||
.then(async () => {
|
||||
console.log('数据库连接成功\n');
|
||||
if (command === 'check') {
|
||||
await checkIndexes();
|
||||
} else if (command === 'drop') {
|
||||
await dropIndexes();
|
||||
} else {
|
||||
await createIndexes();
|
||||
}
|
||||
await sequelize.close();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('数据库连接失败:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -57,7 +57,12 @@ const Consumable = sequelize.define('Consumable', {
|
||||
}
|
||||
}, {
|
||||
tableName: 'consumables',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['category'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['category', 'status'] }
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Consumable;
|
||||
|
||||
@@ -57,7 +57,13 @@ const ConsumableLog = sequelize.define('ConsumableLog', {
|
||||
}, {
|
||||
tableName: 'consumable_logs',
|
||||
timestamps: true,
|
||||
comment: '耗材操作日志表'
|
||||
comment: '耗材操作日志表',
|
||||
indexes: [
|
||||
{ fields: ['consumableId'] },
|
||||
{ fields: ['operationType'] },
|
||||
{ fields: ['createdAt'] },
|
||||
{ fields: ['consumableId', 'createdAt'] }
|
||||
]
|
||||
});
|
||||
|
||||
ConsumableLog.belongsTo(Consumable, {
|
||||
|
||||
@@ -49,7 +49,12 @@ const ConsumableRecord = sequelize.define('ConsumableRecord', {
|
||||
}
|
||||
}, {
|
||||
tableName: 'consumable_records',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['consumableId'] },
|
||||
{ fields: ['type'] },
|
||||
{ fields: ['createdAt'] }
|
||||
]
|
||||
});
|
||||
|
||||
ConsumableRecord.belongsTo(Consumable, {
|
||||
|
||||
@@ -72,7 +72,15 @@ const Device = sequelize.define('Device', {
|
||||
}
|
||||
}, {
|
||||
tableName: 'devices',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['type'] },
|
||||
{ fields: ['rackId'] },
|
||||
{ fields: ['createdAt'] },
|
||||
{ fields: ['status', 'type'] },
|
||||
{ fields: ['name'] }
|
||||
]
|
||||
});
|
||||
|
||||
// 关联关系
|
||||
|
||||
@@ -51,7 +51,13 @@ const LoginHistory = sequelize.define('LoginHistory', {
|
||||
tableName: 'login_histories',
|
||||
timestamps: true,
|
||||
createdAt: 'loginTime',
|
||||
updatedAt: false
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ fields: ['userId'] },
|
||||
{ fields: ['loginTime'] },
|
||||
{ fields: ['loginType'] },
|
||||
{ fields: ['userId', 'loginTime'] }
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = LoginHistory;
|
||||
|
||||
@@ -76,7 +76,14 @@ const OperationLog = sequelize.define('OperationLog', {
|
||||
tableName: 'operation_logs',
|
||||
timestamps: true,
|
||||
createdAt: 'operateTime',
|
||||
updatedAt: false
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ fields: ['userId'] },
|
||||
{ fields: ['action'] },
|
||||
{ fields: ['module'] },
|
||||
{ fields: ['createdAt'] },
|
||||
{ fields: ['userId', 'createdAt'] }
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = OperationLog;
|
||||
|
||||
@@ -40,7 +40,12 @@ const Rack = sequelize.define('Rack', {
|
||||
}
|
||||
}, {
|
||||
tableName: 'racks',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['roomId'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['roomId', 'status'] }
|
||||
]
|
||||
});
|
||||
|
||||
// 关联关系
|
||||
|
||||
@@ -33,7 +33,11 @@ const Room = sequelize.define('Room', {
|
||||
}
|
||||
}, {
|
||||
tableName: 'rooms',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['name'] }
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = Room;
|
||||
@@ -57,7 +57,12 @@ const User = sequelize.define('User', {
|
||||
}
|
||||
}, {
|
||||
tableName: 'users',
|
||||
timestamps: true
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['username'] },
|
||||
{ fields: ['email'] }
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = User;
|
||||
|
||||
Generated
+49
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"compression": "^1.7.5",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parser": "^3.2.0",
|
||||
"csv-writer": "^1.6.0",
|
||||
@@ -2646,6 +2647,45 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/negotiator": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -5771,6 +5811,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"description": "IDC设备管理系统后端",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
"dev": "nodemon server.js",
|
||||
"create-indexes": "node create_indexes.js",
|
||||
"check-indexes": "node create_indexes.js check",
|
||||
"drop-indexes": "node create_indexes.js drop"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
|
||||
@@ -124,40 +124,42 @@ router.post('/', async (req, res) => {
|
||||
router.get('/statistics', async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
|
||||
const dateWhere = {};
|
||||
if (startDate && endDate) {
|
||||
dateWhere.createdAt = {
|
||||
[Op.between]: [new Date(startDate), new Date(endDate)]
|
||||
};
|
||||
}
|
||||
|
||||
const inCount = await ConsumableRecord.count({
|
||||
where: { ...dateWhere, type: 'in' }
|
||||
});
|
||||
|
||||
const outCount = await ConsumableRecord.count({
|
||||
where: { ...dateWhere, type: 'out' }
|
||||
});
|
||||
|
||||
const inQuantity = await ConsumableRecord.sum('quantity', {
|
||||
where: { ...dateWhere, type: 'in' }
|
||||
}) || 0;
|
||||
|
||||
const outQuantity = await ConsumableRecord.sum('quantity', {
|
||||
where: { ...dateWhere, type: 'out' }
|
||||
}) || 0;
|
||||
|
||||
const byType = await ConsumableRecord.findAll({
|
||||
|
||||
const records = await ConsumableRecord.findAll({
|
||||
where: dateWhere,
|
||||
attributes: [
|
||||
'type',
|
||||
[sequelize.fn('SUM', sequelize.col('quantity')), 'totalQuantity'],
|
||||
[sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['type']
|
||||
attributes: ['type', 'quantity']
|
||||
});
|
||||
|
||||
|
||||
let inCount = 0;
|
||||
let outCount = 0;
|
||||
let inQuantity = 0;
|
||||
let outQuantity = 0;
|
||||
const typeMap = {};
|
||||
|
||||
records.forEach(item => {
|
||||
const qty = parseFloat(item.quantity) || 0;
|
||||
if (item.type === 'in') {
|
||||
inCount++;
|
||||
inQuantity += qty;
|
||||
} else if (item.type === 'out') {
|
||||
outCount++;
|
||||
outQuantity += qty;
|
||||
}
|
||||
|
||||
if (!typeMap[item.type]) {
|
||||
typeMap[item.type] = { count: 0, totalQuantity: 0 };
|
||||
}
|
||||
typeMap[item.type].count++;
|
||||
typeMap[item.type].totalQuantity += qty;
|
||||
});
|
||||
|
||||
const recentRecords = await ConsumableRecord.findAll({
|
||||
where: dateWhere,
|
||||
include: [
|
||||
@@ -166,17 +168,17 @@ router.get('/statistics', async (req, res) => {
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: 10
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
inCount,
|
||||
outCount,
|
||||
inQuantity,
|
||||
outQuantity,
|
||||
netQuantity: inQuantity - outQuantity,
|
||||
byType: byType.map(item => ({
|
||||
type: item.type,
|
||||
totalQuantity: item.dataValues.totalQuantity,
|
||||
count: item.dataValues.count
|
||||
byType: Object.entries(typeMap).map(([type, data]) => ({
|
||||
type,
|
||||
totalQuantity: data.totalQuantity,
|
||||
count: data.count
|
||||
})),
|
||||
recentRecords
|
||||
});
|
||||
|
||||
@@ -179,34 +179,43 @@ router.get('/low-stock', async (req, res) => {
|
||||
|
||||
router.get('/statistics/summary', async (req, res) => {
|
||||
try {
|
||||
const total = await Consumable.count();
|
||||
const lowStock = await Consumable.count({
|
||||
where: sequelize.where(sequelize.col('currentStock'), {
|
||||
[Op.lte]: sequelize.col('minStock')
|
||||
})
|
||||
});
|
||||
|
||||
const consumables = await Consumable.findAll({
|
||||
attributes: ['currentStock', 'unitPrice']
|
||||
attributes: ['currentStock', 'unitPrice', 'category']
|
||||
});
|
||||
|
||||
const totalValue = consumables.reduce((sum, item) => {
|
||||
return sum + (parseFloat(item.currentStock) || 0) * (parseFloat(item.unitPrice) || 0);
|
||||
}, 0);
|
||||
|
||||
const byCategory = await Consumable.findAll({
|
||||
attributes: ['category', [sequelize.fn('COUNT', '*'), 'count']],
|
||||
group: ['category']
|
||||
|
||||
let total = consumables.length;
|
||||
let lowStock = 0;
|
||||
let totalValue = 0;
|
||||
const categoryMap = {};
|
||||
|
||||
consumables.forEach(item => {
|
||||
const currentStock = parseFloat(item.currentStock) || 0;
|
||||
const unitPrice = parseFloat(item.unitPrice) || 0;
|
||||
|
||||
totalValue += currentStock * unitPrice;
|
||||
|
||||
if (currentStock <= (parseFloat(item.minStock) || 0)) {
|
||||
lowStock++;
|
||||
}
|
||||
|
||||
if (item.category) {
|
||||
if (!categoryMap[item.category]) {
|
||||
categoryMap[item.category] = 0;
|
||||
}
|
||||
categoryMap[item.category]++;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const byCategory = Object.entries(categoryMap).map(([category, count]) => ({
|
||||
category,
|
||||
count
|
||||
}));
|
||||
|
||||
res.json({
|
||||
total,
|
||||
lowStock,
|
||||
totalValue: totalValue.toFixed(2),
|
||||
byCategory: byCategory.map(item => ({
|
||||
category: item.category,
|
||||
count: item.dataValues.count
|
||||
}))
|
||||
byCategory
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
+53
-64
@@ -19,48 +19,60 @@ router.get('/stats', async (req, res) => {
|
||||
if (endDate) where.createdAt[Op.lte] = new Date(endDate + ' 23:59:59');
|
||||
}
|
||||
|
||||
const total = await Ticket.count({ where });
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
const statusStats = await Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'status',
|
||||
[require('sequelize').fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['status']
|
||||
});
|
||||
|
||||
const priorityStats = await Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'priority',
|
||||
[require('sequelize').fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['priority']
|
||||
});
|
||||
|
||||
const categoryStats = await Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'faultCategory',
|
||||
[require('sequelize').fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['faultCategory']
|
||||
});
|
||||
|
||||
const monthlyStats = await Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
[dbDialect === 'mysql'
|
||||
? require('sequelize').fn('DATE_FORMAT', require('sequelize').col('createdAt'), '%Y-%m')
|
||||
: require('sequelize').fn('strftime', '%Y-%m', require('sequelize').col('createdAt')),
|
||||
'month'],
|
||||
[require('sequelize').fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['month'],
|
||||
order: [['month', 'DESC']],
|
||||
limit: 12
|
||||
});
|
||||
const [total, statusStats, priorityStats, categoryStats, monthlyStats, deviceStats, dailyStats] = await Promise.all([
|
||||
Ticket.count({ where }),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: ['status', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||
group: ['status']
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: ['priority', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||
group: ['priority']
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: ['faultCategory', [Sequelize.fn('COUNT', '*'), 'count']],
|
||||
group: ['faultCategory']
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
[dbDialect === 'mysql'
|
||||
? Sequelize.fn('DATE_FORMAT', Sequelize.col('createdAt'), '%Y-%m')
|
||||
: Sequelize.fn('strftime', '%Y-%m', Sequelize.col('createdAt')),
|
||||
'month'],
|
||||
[Sequelize.fn('COUNT', '*'), 'count']
|
||||
],
|
||||
group: ['month'],
|
||||
order: [['month', 'DESC']],
|
||||
limit: 12
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'deviceId',
|
||||
'deviceName',
|
||||
[Sequelize.fn('COUNT', '*'), 'count'],
|
||||
[Sequelize.fn('MAX', Sequelize.col('createdAt')), 'lastFaultTime']
|
||||
],
|
||||
group: ['deviceId', 'deviceName'],
|
||||
order: [[Sequelize.fn('COUNT', '*'), 'DESC']],
|
||||
limit: 10
|
||||
}),
|
||||
Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
[Sequelize.fn('DATE', Sequelize.col('createdAt')), 'date'],
|
||||
[Sequelize.fn('COUNT', '*'), 'created']
|
||||
],
|
||||
group: ['date'],
|
||||
order: [['date', 'ASC']]
|
||||
})
|
||||
]);
|
||||
|
||||
const statusData = statusStats.map(s => s.dataValues);
|
||||
const pending = statusData.find(s => s.status === 'pending')?.count || 0;
|
||||
@@ -88,19 +100,6 @@ router.get('/stats', async (req, res) => {
|
||||
avgTime: 0
|
||||
}));
|
||||
|
||||
const deviceStats = await Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'deviceId',
|
||||
'deviceName',
|
||||
[require('sequelize').fn('COUNT', '*'), 'count'],
|
||||
[require('sequelize').fn('MAX', require('sequelize').col('createdAt')), 'lastFaultTime']
|
||||
],
|
||||
group: ['deviceId', 'deviceName'],
|
||||
order: [[require('sequelize').fn('COUNT', '*'), 'DESC']],
|
||||
limit: 10
|
||||
});
|
||||
|
||||
const byDevice = deviceStats.map(d => ({
|
||||
deviceId: d.deviceId,
|
||||
deviceName: d.deviceName,
|
||||
@@ -109,16 +108,6 @@ router.get('/stats', async (req, res) => {
|
||||
deviceType: ''
|
||||
}));
|
||||
|
||||
const dailyStats = await Ticket.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
[require('sequelize').fn('DATE', require('sequelize').col('createdAt')), 'date'],
|
||||
[require('sequelize').fn('COUNT', '*'), 'created']
|
||||
],
|
||||
group: ['date'],
|
||||
order: [['date', 'ASC']]
|
||||
});
|
||||
|
||||
const trend = dailyStats.map(d => ({
|
||||
date: d.dataValues.date,
|
||||
created: d.dataValues.created,
|
||||
|
||||
+28
-10
@@ -51,20 +51,38 @@ router.get('/', authMiddleware, async (req, res) => {
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
for (const user of users) {
|
||||
const userRoles = await UserRole.findAll({
|
||||
if (users.length > 0) {
|
||||
const userIds = users.map(u => u.userId);
|
||||
const allUserRoles = await UserRole.findAll({
|
||||
include: [{
|
||||
model: Role,
|
||||
where: { status: 'active' }
|
||||
where: { status: 'active' },
|
||||
attributes: ['roleId', 'roleName', 'roleCode']
|
||||
}],
|
||||
where: { UserId: user.userId }
|
||||
where: {
|
||||
UserId: { [Op.in]: userIds }
|
||||
}
|
||||
});
|
||||
|
||||
const userRolesMap = {};
|
||||
allUserRoles.forEach(ur => {
|
||||
if (!userRolesMap[ur.UserId]) {
|
||||
userRolesMap[ur.UserId] = [];
|
||||
}
|
||||
userRolesMap[ur.UserId].push({
|
||||
roleId: ur.Role.roleId,
|
||||
roleName: ur.Role.roleName,
|
||||
roleCode: ur.Role.roleCode
|
||||
});
|
||||
});
|
||||
|
||||
users.forEach(user => {
|
||||
user.dataValues.roles = userRolesMap[user.userId] || [];
|
||||
});
|
||||
} else {
|
||||
users.forEach(user => {
|
||||
user.dataValues.roles = [];
|
||||
});
|
||||
|
||||
user.dataValues.roles = userRoles.map(ur => ({
|
||||
roleId: ur.Role.roleId,
|
||||
roleName: ur.Role.roleName,
|
||||
roleCode: ur.Role.roleCode
|
||||
}));
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
||||
Reference in New Issue
Block a user