670 lines
17 KiB
JavaScript
670 lines
17 KiB
JavaScript
/**
|
|
* 统计控制器
|
|
* @file statsController.js
|
|
* @description 处理数据统计相关的请求
|
|
*/
|
|
|
|
const { Farm, Animal, Device, Alert, SensorData } = require('../models');
|
|
const { sequelize } = require('../config/database-simple');
|
|
const { Op } = require('sequelize');
|
|
|
|
/**
|
|
* 获取仪表盘统计数据
|
|
* @param {Object} req - 请求对象
|
|
* @param {Object} res - 响应对象
|
|
*/
|
|
exports.getDashboardStats = async (req, res) => {
|
|
try {
|
|
// 检查是否需要模拟500错误
|
|
if (req.query.testError === '500') {
|
|
throw new Error('模拟服务器错误');
|
|
}
|
|
|
|
// 检查是否需要模拟401错误
|
|
if (req.query.testError === '401') {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: '未授权'
|
|
});
|
|
}
|
|
|
|
// 从数据库获取真实统计数据
|
|
const [farmCount, animalCount, deviceCount, alertCount, onlineDeviceCount, alertsByLevel] = await Promise.all([
|
|
Farm.count(),
|
|
Animal.sum('count') || 0,
|
|
Device.count(),
|
|
Alert.count(),
|
|
Device.count({ where: { status: 'online' } }),
|
|
Alert.findAll({
|
|
attributes: [
|
|
'level',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
|
],
|
|
group: ['level'],
|
|
raw: true
|
|
})
|
|
]);
|
|
|
|
// 计算设备在线率
|
|
const deviceOnlineRate = deviceCount > 0 ? (onlineDeviceCount / deviceCount) : 0;
|
|
|
|
// 格式化预警级别统计
|
|
const alertLevels = { low: 0, medium: 0, high: 0, critical: 0 };
|
|
alertsByLevel.forEach(item => {
|
|
alertLevels[item.level] = parseInt(item.count);
|
|
});
|
|
|
|
const stats = {
|
|
farmCount: farmCount || 0,
|
|
animalCount: animalCount || 0,
|
|
deviceCount: deviceCount || 0,
|
|
alertCount: alertCount || 0,
|
|
deviceOnlineRate: Math.round(deviceOnlineRate * 100) / 100,
|
|
alertsByLevel: alertLevels
|
|
};
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: stats
|
|
});
|
|
} catch (error) {
|
|
console.error('获取仪表盘统计数据失败:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取统计数据失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取养殖场统计数据
|
|
* @param {Object} req - 请求对象
|
|
* @param {Object} res - 响应对象
|
|
*/
|
|
exports.getFarmStats = async (req, res) => {
|
|
try {
|
|
// 检查是否需要模拟500错误
|
|
if (req.query.testError === '500') {
|
|
throw new Error('模拟服务器错误');
|
|
}
|
|
|
|
// 检查是否需要模拟401错误
|
|
if (req.query.testError === '401') {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: '未授权'
|
|
});
|
|
}
|
|
|
|
// 从数据库获取真实养殖场统计数据
|
|
const [totalFarms, farmsByType, farmsByStatus] = await Promise.all([
|
|
Farm.count(),
|
|
Farm.findAll({
|
|
attributes: [
|
|
'type',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
|
],
|
|
group: ['type'],
|
|
raw: true
|
|
}),
|
|
Farm.findAll({
|
|
attributes: [
|
|
'status',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
|
],
|
|
group: ['status'],
|
|
raw: true
|
|
})
|
|
]);
|
|
|
|
// 格式化数据
|
|
const formattedFarmsByType = farmsByType.map(item => ({
|
|
type: item.type,
|
|
count: parseInt(item.count)
|
|
}));
|
|
|
|
const formattedFarmsByStatus = farmsByStatus.map(item => ({
|
|
status: item.status,
|
|
count: parseInt(item.count)
|
|
}));
|
|
|
|
const stats = {
|
|
totalFarms: totalFarms || 0,
|
|
farmsByType: formattedFarmsByType,
|
|
farmsByStatus: formattedFarmsByStatus
|
|
};
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: stats
|
|
});
|
|
} catch (error) {
|
|
console.error('获取养殖场统计数据失败:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取养殖场统计数据失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取动物统计数据
|
|
* @param {Object} req - 请求对象
|
|
* @param {Object} res - 响应对象
|
|
*/
|
|
exports.getAnimalStats = async (req, res) => {
|
|
try {
|
|
// 检查是否需要模拟500错误
|
|
if (req.query.testError === '500') {
|
|
throw new Error('模拟服务器错误');
|
|
}
|
|
|
|
// 检查是否需要模拟401错误
|
|
if (req.query.testError === '401') {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: '未授权'
|
|
});
|
|
}
|
|
|
|
// 从数据库获取真实动物统计数据
|
|
const [totalAnimals, animalsByType, animalsByHealth] = await Promise.all([
|
|
Animal.sum('count') || 0,
|
|
Animal.findAll({
|
|
attributes: [
|
|
'type',
|
|
[sequelize.fn('SUM', sequelize.col('count')), 'total_count']
|
|
],
|
|
group: ['type'],
|
|
raw: true
|
|
}),
|
|
Animal.findAll({
|
|
attributes: [
|
|
'health_status',
|
|
[sequelize.fn('SUM', sequelize.col('count')), 'total_count']
|
|
],
|
|
group: ['health_status'],
|
|
raw: true
|
|
})
|
|
]);
|
|
|
|
// 格式化数据
|
|
const formattedAnimalsByType = animalsByType.map(item => ({
|
|
type: item.type,
|
|
count: parseInt(item.total_count) || 0
|
|
}));
|
|
|
|
const formattedAnimalsByHealth = animalsByHealth.map(item => ({
|
|
health_status: item.health_status,
|
|
count: parseInt(item.total_count) || 0
|
|
}));
|
|
|
|
const stats = {
|
|
totalAnimals: totalAnimals || 0,
|
|
animalsByType: formattedAnimalsByType,
|
|
animalsByHealth: formattedAnimalsByHealth
|
|
};
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: stats
|
|
});
|
|
} catch (error) {
|
|
console.error('获取动物统计数据失败:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取动物统计数据失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取设备统计数据
|
|
* @param {Object} req - 请求对象
|
|
* @param {Object} res - 响应对象
|
|
*/
|
|
exports.getDeviceStats = async (req, res) => {
|
|
try {
|
|
// 检查是否需要模拟500错误
|
|
if (req.query.testError === '500') {
|
|
throw new Error('模拟服务器错误');
|
|
}
|
|
|
|
// 检查是否需要模拟401错误
|
|
if (req.query.testError === '401') {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: '未授权'
|
|
});
|
|
}
|
|
|
|
// 从数据库获取真实设备统计数据
|
|
const [totalDevices, devicesByType, devicesByStatus] = await Promise.all([
|
|
Device.count(),
|
|
Device.findAll({
|
|
attributes: [
|
|
'type',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'device_count']
|
|
],
|
|
group: ['type'],
|
|
raw: true
|
|
}),
|
|
Device.findAll({
|
|
attributes: [
|
|
'status',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'device_count']
|
|
],
|
|
group: ['status'],
|
|
raw: true
|
|
})
|
|
]);
|
|
|
|
// 格式化数据
|
|
const formattedDevicesByType = devicesByType.map(item => ({
|
|
type: item.type,
|
|
count: parseInt(item.device_count) || 0
|
|
}));
|
|
|
|
const formattedDevicesByStatus = devicesByStatus.map(item => ({
|
|
status: item.status,
|
|
count: parseInt(item.device_count) || 0
|
|
}));
|
|
|
|
// 计算在线率
|
|
const onlineDevices = formattedDevicesByStatus.find(item => item.status === 'online')?.count || 0;
|
|
const onlineRate = totalDevices > 0 ? (onlineDevices / totalDevices) : 0;
|
|
|
|
const stats = {
|
|
totalDevices: totalDevices || 0,
|
|
devicesByType: formattedDevicesByType,
|
|
devicesByStatus: formattedDevicesByStatus,
|
|
onlineRate: parseFloat(onlineRate.toFixed(2))
|
|
};
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: stats
|
|
});
|
|
} catch (error) {
|
|
console.error('获取设备统计数据失败:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取设备统计数据失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取预警统计数据
|
|
* @param {Object} req - 请求对象
|
|
* @param {Object} res - 响应对象
|
|
*/
|
|
exports.getAlertStats = async (req, res) => {
|
|
try {
|
|
const { testError } = req.query;
|
|
|
|
// 模拟401错误
|
|
if (testError === '401') {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: '未授权'
|
|
});
|
|
}
|
|
|
|
// 模拟500错误
|
|
if (testError === '500') {
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: '服务器错误'
|
|
});
|
|
}
|
|
|
|
// 获取预警总数
|
|
const totalAlerts = await Alert.count();
|
|
|
|
// 按类型统计预警
|
|
const alertsByType = await Alert.findAll({
|
|
attributes: [
|
|
'type',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
|
],
|
|
group: ['type']
|
|
});
|
|
|
|
// 按级别统计预警
|
|
const alertsByLevel = await Alert.findAll({
|
|
attributes: [
|
|
'level',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
|
],
|
|
group: ['level']
|
|
});
|
|
|
|
// 按状态统计预警
|
|
const alertsByStatus = await Alert.findAll({
|
|
attributes: [
|
|
'status',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
|
],
|
|
group: ['status']
|
|
});
|
|
|
|
// 获取最近的预警
|
|
const recentAlerts = await Alert.findAll({
|
|
limit: 10,
|
|
order: [['created_at', 'DESC']],
|
|
attributes: ['id', 'type', 'level', 'message', 'created_at']
|
|
});
|
|
|
|
// 格式化数据
|
|
const formattedAlertsByType = alertsByType.map(item => ({
|
|
type: item.type,
|
|
count: parseInt(item.dataValues.count) || 0
|
|
}));
|
|
|
|
const formattedAlertsByLevel = alertsByLevel.map(item => ({
|
|
level: item.level,
|
|
count: parseInt(item.dataValues.count) || 0
|
|
}));
|
|
|
|
const formattedAlertsByStatus = alertsByStatus.map(item => ({
|
|
status: item.status,
|
|
count: parseInt(item.dataValues.count) || 0
|
|
}));
|
|
|
|
const stats = {
|
|
totalAlerts: totalAlerts || 0,
|
|
alertsByType: formattedAlertsByType,
|
|
alertsByLevel: formattedAlertsByLevel,
|
|
alertsByStatus: formattedAlertsByStatus,
|
|
recentAlerts: recentAlerts
|
|
};
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: stats
|
|
});
|
|
} catch (error) {
|
|
console.error('获取预警统计数据失败:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取预警统计数据失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取实时监控数据
|
|
* @param {Object} req - 请求对象
|
|
* @param {Object} res - 响应对象
|
|
*/
|
|
exports.getMonitorData = async (req, res) => {
|
|
try {
|
|
const { testError } = req.query;
|
|
|
|
// 模拟401错误
|
|
if (testError === '401') {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: '未授权'
|
|
});
|
|
}
|
|
|
|
// 模拟500错误
|
|
if (testError === '500') {
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: '服务器错误'
|
|
});
|
|
}
|
|
|
|
// 获取设备状态统计
|
|
const devicesByStatus = await Device.findAll({
|
|
attributes: [
|
|
'status',
|
|
[sequelize.fn('COUNT', sequelize.col('id')), 'device_count']
|
|
],
|
|
group: ['status']
|
|
});
|
|
|
|
// 格式化设备状态数据
|
|
const deviceStatus = {};
|
|
devicesByStatus.forEach(item => {
|
|
deviceStatus[item.status] = parseInt(item.dataValues.device_count) || 0;
|
|
});
|
|
|
|
// 获取最近的预警
|
|
const recentAlerts = await Alert.findAll({
|
|
limit: 5,
|
|
order: [['created_at', 'DESC']],
|
|
attributes: ['id', 'type', 'level', 'message', 'created_at']
|
|
});
|
|
|
|
// 从传感器数据表获取真实环境数据
|
|
const [temperatureData, humidityData] = await Promise.all([
|
|
SensorData.findAll({
|
|
where: {
|
|
sensor_type: 'temperature'
|
|
},
|
|
order: [['recorded_at', 'DESC']],
|
|
limit: 24, // 最近24小时数据
|
|
attributes: ['value', 'recorded_at', 'unit'],
|
|
include: [{
|
|
model: Device,
|
|
as: 'device',
|
|
attributes: ['name'],
|
|
include: [{
|
|
model: Farm,
|
|
as: 'farm',
|
|
attributes: ['location']
|
|
}]
|
|
}]
|
|
}),
|
|
SensorData.findAll({
|
|
where: {
|
|
sensor_type: 'humidity'
|
|
},
|
|
order: [['recorded_at', 'DESC']],
|
|
limit: 24, // 最近24小时数据
|
|
attributes: ['value', 'recorded_at', 'unit'],
|
|
include: [{
|
|
model: Device,
|
|
as: 'device',
|
|
attributes: ['name'],
|
|
include: [{
|
|
model: Farm,
|
|
as: 'farm',
|
|
attributes: ['location']
|
|
}]
|
|
}]
|
|
})
|
|
]);
|
|
|
|
// 格式化环境数据为前端期望的结构
|
|
const temperatureHistory = temperatureData.map(item => ({
|
|
time: item.recorded_at,
|
|
value: parseFloat(item.value)
|
|
}));
|
|
|
|
const humidityHistory = humidityData.map(item => ({
|
|
time: item.recorded_at,
|
|
value: parseFloat(item.value)
|
|
}));
|
|
|
|
const environmentalData = {
|
|
temperature: {
|
|
current: temperatureHistory.length > 0 ? temperatureHistory[0].value : 25.0,
|
|
unit: '°C',
|
|
history: temperatureHistory
|
|
},
|
|
humidity: {
|
|
current: humidityHistory.length > 0 ? humidityHistory[0].value : 65.0,
|
|
unit: '%',
|
|
history: humidityHistory
|
|
}
|
|
};
|
|
|
|
// 如果没有传感器数据,提供默认值
|
|
if (temperatureHistory.length === 0) {
|
|
const now = new Date();
|
|
environmentalData.temperature.history = [{
|
|
time: now.toISOString(),
|
|
value: 25.0
|
|
}];
|
|
environmentalData.temperature.current = 25.0;
|
|
}
|
|
|
|
if (humidityHistory.length === 0) {
|
|
const now = new Date();
|
|
environmentalData.humidity.history = [{
|
|
time: now.toISOString(),
|
|
value: 65.0
|
|
}];
|
|
environmentalData.humidity.current = 65.0;
|
|
}
|
|
|
|
const monitorData = {
|
|
deviceStatus,
|
|
recentAlerts,
|
|
environmentData: environmentalData
|
|
};
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: monitorData
|
|
});
|
|
} catch (error) {
|
|
console.error('获取实时监控数据失败:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取实时监控数据失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取月度数据趋势
|
|
* @param {Object} req - 请求对象
|
|
* @param {Object} res - 响应对象
|
|
*/
|
|
exports.getMonthlyTrends = async (req, res) => {
|
|
try {
|
|
// 获取最近12个月的数据
|
|
const months = [];
|
|
const now = new Date();
|
|
|
|
for (let i = 11; i >= 0; i--) {
|
|
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
months.push({
|
|
year: date.getFullYear(),
|
|
month: date.getMonth() + 1,
|
|
label: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
|
|
});
|
|
}
|
|
|
|
// 获取每月的统计数据
|
|
const monthlyData = await Promise.all(months.map(async (monthInfo) => {
|
|
const startDate = new Date(monthInfo.year, monthInfo.month - 1, 1);
|
|
const endDate = new Date(monthInfo.year, monthInfo.month, 0, 23, 59, 59);
|
|
|
|
const [farmCount, animalCount, deviceCount, alertCount] = await Promise.all([
|
|
Farm.count({
|
|
where: {
|
|
created_at: {
|
|
[Op.lte]: endDate
|
|
}
|
|
}
|
|
}),
|
|
Animal.sum('count', {
|
|
where: {
|
|
created_at: {
|
|
[Op.lte]: endDate
|
|
}
|
|
}
|
|
}) || 0,
|
|
Device.count({
|
|
where: {
|
|
created_at: {
|
|
[Op.lte]: endDate
|
|
}
|
|
}
|
|
}),
|
|
Alert.count({
|
|
where: {
|
|
created_at: {
|
|
[Op.between]: [startDate, endDate]
|
|
}
|
|
}
|
|
})
|
|
]);
|
|
|
|
return {
|
|
month: monthInfo.label,
|
|
farmCount: farmCount || 0,
|
|
animalCount: animalCount || 0,
|
|
deviceCount: deviceCount || 0,
|
|
alertCount: alertCount || 0
|
|
};
|
|
}));
|
|
|
|
// 格式化为图表数据
|
|
const trendData = {
|
|
xAxis: monthlyData.map(item => item.month),
|
|
series: [
|
|
{
|
|
name: '养殖场数量',
|
|
type: 'line',
|
|
data: monthlyData.map(item => item.farmCount),
|
|
itemStyle: { color: '#1890ff' },
|
|
areaStyle: { opacity: 0.3 }
|
|
},
|
|
{
|
|
name: '动物数量',
|
|
type: 'line',
|
|
data: monthlyData.map(item => item.animalCount),
|
|
itemStyle: { color: '#52c41a' },
|
|
areaStyle: { opacity: 0.3 }
|
|
},
|
|
{
|
|
name: '设备数量',
|
|
type: 'line',
|
|
data: monthlyData.map(item => item.deviceCount),
|
|
itemStyle: { color: '#faad14' },
|
|
areaStyle: { opacity: 0.3 }
|
|
},
|
|
{
|
|
name: '预警数量',
|
|
type: 'line',
|
|
data: monthlyData.map(item => item.alertCount),
|
|
itemStyle: { color: '#ff4d4f' },
|
|
areaStyle: { opacity: 0.3 }
|
|
}
|
|
]
|
|
};
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: trendData
|
|
});
|
|
} catch (error) {
|
|
console.error('获取月度数据趋势失败:', error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取月度数据趋势失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
}; |