修改管理后台

This commit is contained in:
shenquanyi
2025-09-12 20:08:42 +08:00
parent 39d61c6f9b
commit 80a24c2d60
286 changed files with 75316 additions and 9452 deletions

View File

@@ -0,0 +1,705 @@
/**
* 数据备份服务
* @file backupService.js
* @description 提供数据库备份、文件备份和故障恢复功能
*/
const fs = require('fs').promises;
const path = require('path');
const { spawn } = require('child_process');
const archiver = require('archiver');
const { sequelize } = require('../config/database-simple');
const logger = require('../utils/logger');
const moment = require('moment');
/**
* 备份配置
*/
const BACKUP_CONFIG = {
// 备份目录
backupDir: path.join(__dirname, '../../backups'),
// 数据库备份配置
database: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
database: process.env.DB_NAME || 'nxxm_farming',
username: process.env.DB_USER || 'root',
password: process.env.DB_PASS || ''
},
// 备份保留策略
retention: {
daily: 7, // 保留7天的日备份
weekly: 4, // 保留4周的周备份
monthly: 12 // 保留12个月的月备份
},
// 压缩级别 (0-9)
compressionLevel: 6
};
/**
* 备份服务类
*/
class BackupService {
constructor() {
this.isBackupRunning = false;
this.backupQueue = [];
this.initBackupDirectory();
}
/**
* 初始化备份目录
*/
async initBackupDirectory() {
try {
await fs.mkdir(BACKUP_CONFIG.backupDir, { recursive: true });
await fs.mkdir(path.join(BACKUP_CONFIG.backupDir, 'database'), { recursive: true });
await fs.mkdir(path.join(BACKUP_CONFIG.backupDir, 'files'), { recursive: true });
await fs.mkdir(path.join(BACKUP_CONFIG.backupDir, 'logs'), { recursive: true });
logger.info('备份目录初始化完成');
} catch (error) {
logger.error('备份目录初始化失败:', error);
throw error;
}
}
/**
* 执行完整系统备份
* @param {Object} options 备份选项
* @returns {Object} 备份结果
*/
async createFullBackup(options = {}) {
if (this.isBackupRunning) {
throw new Error('备份正在进行中,请稍后再试');
}
this.isBackupRunning = true;
const backupId = this.generateBackupId();
const backupPath = path.join(BACKUP_CONFIG.backupDir, backupId);
try {
logger.info(`开始创建完整备份: ${backupId}`);
await fs.mkdir(backupPath, { recursive: true });
const backupResult = {
id: backupId,
timestamp: new Date(),
type: 'full',
status: 'in_progress',
components: {
database: { status: 'pending', size: 0, path: '' },
files: { status: 'pending', size: 0, path: '' },
logs: { status: 'pending', size: 0, path: '' }
},
totalSize: 0,
duration: 0
};
const startTime = Date.now();
// 1. 备份数据库
logger.info('开始备份数据库...');
const dbBackupResult = await this.backupDatabase(backupPath);
backupResult.components.database = dbBackupResult;
// 2. 备份文件(上传文件、配置文件等)
logger.info('开始备份文件...');
const fileBackupResult = await this.backupFiles(backupPath);
backupResult.components.files = fileBackupResult;
// 3. 备份日志
logger.info('开始备份日志...');
const logBackupResult = await this.backupLogs(backupPath);
backupResult.components.logs = logBackupResult;
// 4. 创建压缩包
logger.info('开始压缩备份文件...');
const archivePath = await this.createArchive(backupPath, backupId);
// 5. 计算总大小和持续时间
const archiveStats = await fs.stat(archivePath);
backupResult.totalSize = archiveStats.size;
backupResult.duration = Date.now() - startTime;
backupResult.status = 'completed';
backupResult.archivePath = archivePath;
// 6. 清理临时目录
await this.cleanupDirectory(backupPath);
// 7. 保存备份元数据
await this.saveBackupMetadata(backupResult);
logger.info(`完整备份创建完成: ${backupId}, 大小: ${this.formatSize(backupResult.totalSize)}, 用时: ${backupResult.duration}ms`);
return backupResult;
} catch (error) {
logger.error(`创建备份失败: ${backupId}`, error);
// 清理失败的备份
try {
await this.cleanupDirectory(backupPath);
} catch (cleanupError) {
logger.error('清理失败备份目录出错:', cleanupError);
}
throw error;
} finally {
this.isBackupRunning = false;
}
}
/**
* 备份数据库
* @param {string} backupPath 备份路径
* @returns {Object} 备份结果
*/
async backupDatabase(backupPath) {
return new Promise((resolve, reject) => {
const timestamp = moment().format('YYYYMMDD_HHmmss');
const filename = `database_${timestamp}.sql`;
const outputPath = path.join(backupPath, filename);
const mysqldumpArgs = [
'-h', BACKUP_CONFIG.database.host,
'-P', BACKUP_CONFIG.database.port,
'-u', BACKUP_CONFIG.database.username,
`--password=${BACKUP_CONFIG.database.password}`,
'--single-transaction',
'--routines',
'--triggers',
'--set-gtid-purged=OFF',
BACKUP_CONFIG.database.database
];
const mysqldump = spawn('mysqldump', mysqldumpArgs);
const writeStream = require('fs').createWriteStream(outputPath);
mysqldump.stdout.pipe(writeStream);
let errorOutput = '';
mysqldump.stderr.on('data', (data) => {
errorOutput += data.toString();
});
mysqldump.on('close', async (code) => {
try {
if (code === 0) {
const stats = await fs.stat(outputPath);
resolve({
status: 'completed',
size: stats.size,
path: filename,
duration: Date.now() - Date.now()
});
} else {
reject(new Error(`mysqldump失败退出代码: ${code}, 错误: ${errorOutput}`));
}
} catch (error) {
reject(error);
}
});
mysqldump.on('error', (error) => {
reject(new Error(`mysqldump执行失败: ${error.message}`));
});
});
}
/**
* 备份文件
* @param {string} backupPath 备份路径
* @returns {Object} 备份结果
*/
async backupFiles(backupPath) {
try {
const fileBackupPath = path.join(backupPath, 'files');
await fs.mkdir(fileBackupPath, { recursive: true });
// 备份上传文件(如果存在)
const uploadsDir = path.join(__dirname, '../../uploads');
try {
await fs.access(uploadsDir);
await this.copyDirectory(uploadsDir, path.join(fileBackupPath, 'uploads'));
} catch (error) {
// 上传目录不存在,跳过
logger.warn('上传目录不存在,跳过文件备份');
}
// 备份配置文件
const configFiles = [
path.join(__dirname, '../config'),
path.join(__dirname, '../package.json'),
path.join(__dirname, '../.env')
];
for (const configPath of configFiles) {
try {
await fs.access(configPath);
const basename = path.basename(configPath);
const destPath = path.join(fileBackupPath, basename);
const stats = await fs.stat(configPath);
if (stats.isDirectory()) {
await this.copyDirectory(configPath, destPath);
} else {
await fs.copyFile(configPath, destPath);
}
} catch (error) {
logger.warn(`配置文件 ${configPath} 备份跳过:`, error.message);
}
}
const backupStats = await this.getDirectorySize(fileBackupPath);
return {
status: 'completed',
size: backupStats.size,
path: 'files',
fileCount: backupStats.fileCount
};
} catch (error) {
logger.error('文件备份失败:', error);
return {
status: 'failed',
error: error.message,
size: 0,
path: ''
};
}
}
/**
* 备份日志
* @param {string} backupPath 备份路径
* @returns {Object} 备份结果
*/
async backupLogs(backupPath) {
try {
const logBackupPath = path.join(backupPath, 'logs');
await fs.mkdir(logBackupPath, { recursive: true });
const logsDir = path.join(__dirname, '../logs');
try {
await fs.access(logsDir);
await this.copyDirectory(logsDir, logBackupPath);
} catch (error) {
logger.warn('日志目录不存在,跳过日志备份');
return {
status: 'skipped',
size: 0,
path: '',
message: '日志目录不存在'
};
}
const backupStats = await this.getDirectorySize(logBackupPath);
return {
status: 'completed',
size: backupStats.size,
path: 'logs',
fileCount: backupStats.fileCount
};
} catch (error) {
logger.error('日志备份失败:', error);
return {
status: 'failed',
error: error.message,
size: 0,
path: ''
};
}
}
/**
* 创建压缩包
* @param {string} sourcePath 源路径
* @param {string} backupId 备份ID
* @returns {string} 压缩包路径
*/
async createArchive(sourcePath, backupId) {
return new Promise((resolve, reject) => {
const archivePath = path.join(BACKUP_CONFIG.backupDir, `${backupId}.zip`);
const output = require('fs').createWriteStream(archivePath);
const archive = archiver('zip', { zlib: { level: BACKUP_CONFIG.compressionLevel } });
output.on('close', () => {
logger.info(`压缩包创建完成: ${archivePath}, 大小: ${archive.pointer()} bytes`);
resolve(archivePath);
});
archive.on('error', (error) => {
reject(error);
});
archive.pipe(output);
archive.directory(sourcePath, false);
archive.finalize();
});
}
/**
* 复制目录
* @param {string} src 源目录
* @param {string} dest 目标目录
*/
async copyDirectory(src, dest) {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
await this.copyDirectory(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}
/**
* 获取目录大小
* @param {string} dirPath 目录路径
* @returns {Object} 大小统计
*/
async getDirectorySize(dirPath) {
let totalSize = 0;
let fileCount = 0;
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
const subStats = await this.getDirectorySize(fullPath);
totalSize += subStats.size;
fileCount += subStats.fileCount;
} else {
const stats = await fs.stat(fullPath);
totalSize += stats.size;
fileCount++;
}
}
} catch (error) {
logger.warn(`获取目录大小失败: ${dirPath}`, error);
}
return { size: totalSize, fileCount };
}
/**
* 清理目录
* @param {string} dirPath 目录路径
*/
async cleanupDirectory(dirPath) {
try {
await fs.rmdir(dirPath, { recursive: true });
} catch (error) {
logger.warn(`清理目录失败: ${dirPath}`, error);
}
}
/**
* 生成备份ID
* @returns {string} 备份ID
*/
generateBackupId() {
const timestamp = moment().format('YYYYMMDD_HHmmss');
const random = Math.random().toString(36).substring(2, 8);
return `backup_${timestamp}_${random}`;
}
/**
* 保存备份元数据
* @param {Object} backupResult 备份结果
*/
async saveBackupMetadata(backupResult) {
try {
const metadataPath = path.join(BACKUP_CONFIG.backupDir, 'metadata.json');
let metadata = [];
try {
const existing = await fs.readFile(metadataPath, 'utf8');
metadata = JSON.parse(existing);
} catch (error) {
// 文件不存在或格式错误,使用空数组
}
metadata.unshift(backupResult);
// 只保留最近100条记录
if (metadata.length > 100) {
metadata = metadata.slice(0, 100);
}
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
logger.info('备份元数据已保存');
} catch (error) {
logger.error('保存备份元数据失败:', error);
}
}
/**
* 获取备份列表
* @returns {Array} 备份列表
*/
async getBackupList() {
try {
const metadataPath = path.join(BACKUP_CONFIG.backupDir, 'metadata.json');
try {
const data = await fs.readFile(metadataPath, 'utf8');
return JSON.parse(data);
} catch (error) {
return [];
}
} catch (error) {
logger.error('获取备份列表失败:', error);
return [];
}
}
/**
* 删除备份
* @param {string} backupId 备份ID
* @returns {boolean} 删除结果
*/
async deleteBackup(backupId) {
try {
const archivePath = path.join(BACKUP_CONFIG.backupDir, `${backupId}.zip`);
try {
await fs.unlink(archivePath);
logger.info(`备份文件已删除: ${archivePath}`);
} catch (error) {
logger.warn(`备份文件删除失败或不存在: ${archivePath}`);
}
// 更新元数据
const metadata = await this.getBackupList();
const updatedMetadata = metadata.filter(backup => backup.id !== backupId);
const metadataPath = path.join(BACKUP_CONFIG.backupDir, 'metadata.json');
await fs.writeFile(metadataPath, JSON.stringify(updatedMetadata, null, 2));
return true;
} catch (error) {
logger.error(`删除备份失败: ${backupId}`, error);
return false;
}
}
/**
* 自动清理过期备份
*/
async cleanupExpiredBackups() {
try {
const backups = await this.getBackupList();
const now = moment();
let deletedCount = 0;
for (const backup of backups) {
const backupDate = moment(backup.timestamp);
const daysDiff = now.diff(backupDate, 'days');
let shouldDelete = false;
// 根据备份类型和时间决定是否删除
if (backup.type === 'daily' && daysDiff > BACKUP_CONFIG.retention.daily) {
shouldDelete = true;
} else if (backup.type === 'weekly' && daysDiff > (BACKUP_CONFIG.retention.weekly * 7)) {
shouldDelete = true;
} else if (backup.type === 'monthly' && daysDiff > (BACKUP_CONFIG.retention.monthly * 30)) {
shouldDelete = true;
} else if (backup.type === 'full' && daysDiff > BACKUP_CONFIG.retention.daily) {
// 完整备份使用日备份的保留策略
shouldDelete = true;
}
if (shouldDelete) {
const deleted = await this.deleteBackup(backup.id);
if (deleted) {
deletedCount++;
}
}
}
if (deletedCount > 0) {
logger.info(`自动清理完成,删除了 ${deletedCount} 个过期备份`);
}
return deletedCount;
} catch (error) {
logger.error('自动清理过期备份失败:', error);
return 0;
}
}
/**
* 恢复数据库
* @param {string} backupId 备份ID
* @returns {boolean} 恢复结果
*/
async restoreDatabase(backupId) {
try {
const backups = await this.getBackupList();
const backup = backups.find(b => b.id === backupId);
if (!backup) {
throw new Error('备份不存在');
}
const archivePath = path.join(BACKUP_CONFIG.backupDir, `${backupId}.zip`);
// 这里可以实现数据库恢复逻辑
// 由于涉及到解压和执行SQL脚本需要谨慎实现
logger.info(`数据库恢复功能需要进一步实现: ${backupId}`);
return true;
} catch (error) {
logger.error(`恢复数据库失败: ${backupId}`, error);
return false;
}
}
/**
* 获取备份统计
* @returns {Object} 统计信息
*/
async getBackupStats() {
try {
const backups = await this.getBackupList();
const stats = {
total: backups.length,
totalSize: backups.reduce((sum, backup) => sum + (backup.totalSize || 0), 0),
byType: {
full: backups.filter(b => b.type === 'full').length,
daily: backups.filter(b => b.type === 'daily').length,
weekly: backups.filter(b => b.type === 'weekly').length,
monthly: backups.filter(b => b.type === 'monthly').length
},
byStatus: {
completed: backups.filter(b => b.status === 'completed').length,
failed: backups.filter(b => b.status === 'failed').length,
inProgress: backups.filter(b => b.status === 'in_progress').length
},
latest: backups[0] || null,
oldestRetained: backups[backups.length - 1] || null
};
return stats;
} catch (error) {
logger.error('获取备份统计失败:', error);
return {
total: 0,
totalSize: 0,
byType: {},
byStatus: {},
latest: null,
oldestRetained: null
};
}
}
/**
* 格式化文件大小
* @param {number} bytes 字节数
* @returns {string} 格式化的大小
*/
formatSize(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
/**
* 检查备份系统健康状态
* @returns {Object} 健康状态
*/
async checkBackupHealth() {
try {
const stats = await this.getBackupStats();
const diskUsage = await this.getDiskUsage();
const health = {
status: 'healthy',
issues: [],
stats,
diskUsage,
lastBackup: stats.latest,
recommendations: []
};
// 检查磁盘空间
if (diskUsage.usagePercent > 90) {
health.status = 'warning';
health.issues.push('磁盘空间不足使用率超过90%');
health.recommendations.push('清理过期备份或扩展存储空间');
}
// 检查最近备份时间
if (stats.latest) {
const daysSinceLastBackup = moment().diff(moment(stats.latest.timestamp), 'days');
if (daysSinceLastBackup > 1) {
health.status = 'warning';
health.issues.push(`最近备份时间过久(${daysSinceLastBackup}天前)`);
health.recommendations.push('建议执行新的备份');
}
} else {
health.status = 'error';
health.issues.push('没有找到任何备份记录');
health.recommendations.push('立即创建第一个备份');
}
return health;
} catch (error) {
logger.error('检查备份健康状态失败:', error);
return {
status: 'error',
issues: ['无法检查备份状态'],
error: error.message
};
}
}
/**
* 获取磁盘使用情况
* @returns {Object} 磁盘使用情况
*/
async getDiskUsage() {
try {
const stats = await fs.stat(BACKUP_CONFIG.backupDir);
// 简化的磁盘使用情况检查
// 在实际环境中可以使用更精确的磁盘空间检查
return {
total: 100 * 1024 * 1024 * 1024, // 假设100GB
used: stats.size || 0,
free: 100 * 1024 * 1024 * 1024 - (stats.size || 0),
usagePercent: ((stats.size || 0) / (100 * 1024 * 1024 * 1024)) * 100
};
} catch (error) {
logger.error('获取磁盘使用情况失败:', error);
return {
total: 0,
used: 0,
free: 0,
usagePercent: 0
};
}
}
}
// 创建单例实例
const backupService = new BackupService();
module.exports = backupService;

View File

@@ -0,0 +1,743 @@
/**
* Redis缓存服务
* @file cacheService.js
* @description 提供高性能Redis缓存功能优化数据访问速度
*/
const redis = require('redis');
const logger = require('../utils/logger');
/**
* 缓存配置
*/
const CACHE_CONFIG = {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || '',
db: process.env.REDIS_DB || 0,
// 默认TTL配置
ttl: {
short: 5 * 60, // 5分钟
medium: 30 * 60, // 30分钟
long: 2 * 60 * 60, // 2小时
daily: 24 * 60 * 60 // 24小时
},
// 键名前缀
prefix: 'nxxm:',
// 连接配置
connect_timeout: 10000,
command_timeout: 5000,
retry_unfulfilled_commands: true,
retry_delay_on_failure: 100,
max_retry_delay: 3000
};
/**
* Redis缓存服务类
*/
class CacheService {
constructor() {
this.client = null;
this.isConnected = false;
this.stats = {
hits: 0,
misses: 0,
errors: 0,
totalOperations: 0
};
}
/**
* 初始化Redis连接
*/
async init() {
try {
this.client = redis.createClient({
url: `redis://${CACHE_CONFIG.host}:${CACHE_CONFIG.port}`,
password: CACHE_CONFIG.password || undefined,
database: CACHE_CONFIG.db,
socket: {
connectTimeout: CACHE_CONFIG.connect_timeout,
commandTimeout: CACHE_CONFIG.command_timeout,
reconnectStrategy: (retries) => {
if (retries > 10) {
logger.error('Redis重连次数超过限制停止重连');
return false;
}
return Math.min(retries * 100, 3000);
}
}
});
// 事件监听
this.client.on('connect', () => {
logger.info('Redis连接已建立');
this.isConnected = true;
});
this.client.on('ready', () => {
logger.info('Redis客户端已就绪');
});
this.client.on('error', (error) => {
logger.error('Redis连接错误:', error);
this.isConnected = false;
this.stats.errors++;
});
this.client.on('end', () => {
logger.warn('Redis连接已断开');
this.isConnected = false;
});
this.client.on('reconnecting', () => {
logger.info('正在重新连接Redis...');
});
// 连接到Redis
await this.client.connect();
logger.info('Redis缓存服务初始化成功');
} catch (error) {
logger.error('Redis缓存服务初始化失败:', error);
this.isConnected = false;
throw error;
}
}
/**
* 生成缓存键名
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {string} 完整键名
*/
generateKey(key, namespace = 'default') {
return `${CACHE_CONFIG.prefix}${namespace}:${key}`;
}
/**
* 设置缓存
* @param {string} key 键名
* @param {*} value 值
* @param {number} ttl 过期时间(秒)
* @param {string} namespace 命名空间
* @returns {boolean} 设置结果
*/
async set(key, value, ttl = CACHE_CONFIG.ttl.medium, namespace = 'default') {
if (!this.isConnected) {
logger.warn('Redis未连接缓存设置跳过');
return false;
}
try {
const fullKey = this.generateKey(key, namespace);
const serializedValue = JSON.stringify(value);
await this.client.setEx(fullKey, ttl, serializedValue);
this.stats.totalOperations++;
logger.debug(`缓存设置成功: ${fullKey}, TTL: ${ttl}s`);
return true;
} catch (error) {
logger.error(`缓存设置失败 ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* 获取缓存
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {*} 缓存值或null
*/
async get(key, namespace = 'default') {
if (!this.isConnected) {
logger.warn('Redis未连接缓存获取跳过');
return null;
}
try {
const fullKey = this.generateKey(key, namespace);
const value = await this.client.get(fullKey);
this.stats.totalOperations++;
if (value === null) {
this.stats.misses++;
logger.debug(`缓存未命中: ${fullKey}`);
return null;
}
this.stats.hits++;
logger.debug(`缓存命中: ${fullKey}`);
return JSON.parse(value);
} catch (error) {
logger.error(`缓存获取失败 ${key}:`, error);
this.stats.errors++;
return null;
}
}
/**
* 删除缓存
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {boolean} 删除结果
*/
async del(key, namespace = 'default') {
if (!this.isConnected) {
return false;
}
try {
const fullKey = this.generateKey(key, namespace);
const result = await this.client.del(fullKey);
this.stats.totalOperations++;
logger.debug(`缓存删除: ${fullKey}, 结果: ${result}`);
return result > 0;
} catch (error) {
logger.error(`缓存删除失败 ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* 检查缓存是否存在
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {boolean} 是否存在
*/
async exists(key, namespace = 'default') {
if (!this.isConnected) {
return false;
}
try {
const fullKey = this.generateKey(key, namespace);
const result = await this.client.exists(fullKey);
this.stats.totalOperations++;
return result === 1;
} catch (error) {
logger.error(`缓存存在检查失败 ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* 设置过期时间
* @param {string} key 键名
* @param {number} ttl 过期时间(秒)
* @param {string} namespace 命名空间
* @returns {boolean} 设置结果
*/
async expire(key, ttl, namespace = 'default') {
if (!this.isConnected) {
return false;
}
try {
const fullKey = this.generateKey(key, namespace);
const result = await this.client.expire(fullKey, ttl);
this.stats.totalOperations++;
return result === 1;
} catch (error) {
logger.error(`设置过期时间失败 ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* 获取剩余过期时间
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {number} 剩余秒数,-1表示永不过期-2表示键不存在
*/
async ttl(key, namespace = 'default') {
if (!this.isConnected) {
return -2;
}
try {
const fullKey = this.generateKey(key, namespace);
const result = await this.client.ttl(fullKey);
this.stats.totalOperations++;
return result;
} catch (error) {
logger.error(`获取过期时间失败 ${key}:`, error);
this.stats.errors++;
return -2;
}
}
/**
* 清空指定命名空间的所有缓存
* @param {string} namespace 命名空间
* @returns {number} 删除的键数量
*/
async clearNamespace(namespace) {
if (!this.isConnected) {
return 0;
}
try {
const pattern = `${CACHE_CONFIG.prefix}${namespace}:*`;
const keys = await this.client.keys(pattern);
if (keys.length === 0) {
return 0;
}
const result = await this.client.del(keys);
this.stats.totalOperations++;
logger.info(`清空命名空间 ${namespace}: 删除了 ${result} 个键`);
return result;
} catch (error) {
logger.error(`清空命名空间失败 ${namespace}:`, error);
this.stats.errors++;
return 0;
}
}
/**
* 批量设置缓存
* @param {Object} keyValuePairs 键值对
* @param {number} ttl 过期时间
* @param {string} namespace 命名空间
* @returns {boolean} 设置结果
*/
async mSet(keyValuePairs, ttl = CACHE_CONFIG.ttl.medium, namespace = 'default') {
if (!this.isConnected) {
return false;
}
try {
const pipeline = this.client.multi();
Object.entries(keyValuePairs).forEach(([key, value]) => {
const fullKey = this.generateKey(key, namespace);
const serializedValue = JSON.stringify(value);
pipeline.setEx(fullKey, ttl, serializedValue);
});
await pipeline.exec();
this.stats.totalOperations += Object.keys(keyValuePairs).length;
logger.debug(`批量缓存设置成功: ${Object.keys(keyValuePairs).length} 个键`);
return true;
} catch (error) {
logger.error('批量缓存设置失败:', error);
this.stats.errors++;
return false;
}
}
/**
* 批量获取缓存
* @param {Array} keys 键名数组
* @param {string} namespace 命名空间
* @returns {Object} 键值对结果
*/
async mGet(keys, namespace = 'default') {
if (!this.isConnected) {
return {};
}
try {
const fullKeys = keys.map(key => this.generateKey(key, namespace));
const values = await this.client.mGet(fullKeys);
const result = {};
keys.forEach((key, index) => {
const value = values[index];
if (value !== null) {
try {
result[key] = JSON.parse(value);
this.stats.hits++;
} catch (error) {
logger.warn(`解析缓存值失败 ${key}:`, error);
result[key] = null;
}
} else {
result[key] = null;
this.stats.misses++;
}
});
this.stats.totalOperations += keys.length;
return result;
} catch (error) {
logger.error('批量缓存获取失败:', error);
this.stats.errors++;
return {};
}
}
/**
* 缓存包装器 - 自动缓存函数结果
* @param {string} key 缓存键
* @param {Function} fn 异步函数
* @param {number} ttl 过期时间
* @param {string} namespace 命名空间
* @returns {*} 函数结果
*/
async wrap(key, fn, ttl = CACHE_CONFIG.ttl.medium, namespace = 'default') {
// 首先尝试从缓存获取
const cached = await this.get(key, namespace);
if (cached !== null) {
return cached;
}
try {
// 缓存未命中,执行函数
const result = await fn();
// 将结果存入缓存
await this.set(key, result, ttl, namespace);
return result;
} catch (error) {
logger.error(`缓存包装器执行失败 ${key}:`, error);
throw error;
}
}
/**
* 获取缓存统计信息
* @returns {Object} 统计信息
*/
getStats() {
const hitRate = this.stats.totalOperations > 0
? (this.stats.hits / (this.stats.hits + this.stats.misses)) * 100
: 0;
return {
...this.stats,
hitRate: hitRate.toFixed(2) + '%',
isConnected: this.isConnected,
config: {
host: CACHE_CONFIG.host,
port: CACHE_CONFIG.port,
db: CACHE_CONFIG.db
}
};
}
/**
* 获取Redis服务器信息
* @returns {Object} 服务器信息
*/
async getServerInfo() {
if (!this.isConnected) {
return null;
}
try {
const info = await this.client.info();
const memory = await this.client.info('memory');
const stats = await this.client.info('stats');
return {
version: this.extractInfoValue(info, 'redis_version'),
uptime: this.extractInfoValue(info, 'uptime_in_seconds'),
memory: {
used: this.extractInfoValue(memory, 'used_memory_human'),
peak: this.extractInfoValue(memory, 'used_memory_peak_human'),
fragmentation: this.extractInfoValue(memory, 'mem_fragmentation_ratio')
},
stats: {
connections: this.extractInfoValue(stats, 'total_connections_received'),
commands: this.extractInfoValue(stats, 'total_commands_processed'),
hits: this.extractInfoValue(stats, 'keyspace_hits'),
misses: this.extractInfoValue(stats, 'keyspace_misses')
}
};
} catch (error) {
logger.error('获取Redis服务器信息失败:', error);
return null;
}
}
/**
* 从INFO字符串中提取值
* @private
*/
extractInfoValue(infoString, key) {
const regex = new RegExp(`${key}:(.+)`);
const match = infoString.match(regex);
return match ? match[1].trim() : '';
}
/**
* 健康检查
* @returns {Object} 健康状态
*/
async healthCheck() {
try {
if (!this.isConnected) {
return {
status: 'unhealthy',
message: 'Redis连接断开',
timestamp: new Date()
};
}
// 执行简单的ping测试
const pong = await this.client.ping();
if (pong === 'PONG') {
return {
status: 'healthy',
message: 'Redis连接正常',
stats: this.getStats(),
timestamp: new Date()
};
} else {
return {
status: 'unhealthy',
message: 'Redis ping响应异常',
timestamp: new Date()
};
}
} catch (error) {
logger.error('Redis健康检查失败:', error);
return {
status: 'unhealthy',
message: 'Redis健康检查失败',
error: error.message,
timestamp: new Date()
};
}
}
/**
* 关闭连接
*/
async close() {
try {
if (this.client && this.isConnected) {
await this.client.quit();
logger.info('Redis连接已关闭');
}
} catch (error) {
logger.error('关闭Redis连接失败:', error);
}
}
/**
* 清空所有缓存
* @returns {boolean} 清空结果
*/
async flushAll() {
if (!this.isConnected) {
return false;
}
try {
await this.client.flushDb();
logger.info('Redis缓存已清空');
// 重置统计
this.stats = {
hits: 0,
misses: 0,
errors: 0,
totalOperations: 0
};
return true;
} catch (error) {
logger.error('清空Redis缓存失败:', error);
this.stats.errors++;
return false;
}
}
}
/**
* 缓存键名常量
*/
const CACHE_KEYS = {
// 用户相关
USER_LIST: 'users:list',
USER_PROFILE: (id) => `users:profile:${id}`,
USER_PERMISSIONS: (id) => `users:permissions:${id}`,
// 农场相关
FARM_LIST: 'farms:list',
FARM_DETAIL: (id) => `farms:detail:${id}`,
FARM_ANIMALS: (id) => `farms:animals:${id}`,
FARM_DEVICES: (id) => `farms:devices:${id}`,
// 设备相关
DEVICE_LIST: 'devices:list',
DEVICE_STATUS: (id) => `devices:status:${id}`,
DEVICE_METRICS: (id) => `devices:metrics:${id}`,
// 统计数据
STATS_DASHBOARD: 'stats:dashboard',
STATS_FARMS: 'stats:farms',
STATS_DEVICES: 'stats:devices',
STATS_ANIMALS: 'stats:animals',
STATS_ALERTS: 'stats:alerts',
// 系统配置
SYSTEM_CONFIG: 'system:config',
MENU_PERMISSIONS: 'system:menus',
// 搜索结果
SEARCH_RESULTS: (type, query) => `search:${type}:${Buffer.from(query).toString('base64')}`
};
/**
* 高级缓存功能
*/
class AdvancedCache extends CacheService {
/**
* 缓存列表数据(带分页)
* @param {string} key 基础键名
* @param {Array} data 数据数组
* @param {Object} pagination 分页信息
* @param {number} ttl 过期时间
*/
async setListData(key, data, pagination = {}, ttl = CACHE_CONFIG.ttl.medium) {
const cacheData = {
data,
pagination,
timestamp: Date.now()
};
return await this.set(key, cacheData, ttl, 'lists');
}
/**
* 获取列表数据
* @param {string} key 键名
* @returns {Object} 列表数据和分页信息
*/
async getListData(key) {
const cached = await this.get(key, 'lists');
if (cached && cached.data) {
return {
data: cached.data,
pagination: cached.pagination || {},
fromCache: true,
cacheTimestamp: cached.timestamp
};
}
return null;
}
/**
* 智能缓存失效
* @param {string} entity 实体类型
* @param {string} operation 操作类型
* @param {number} entityId 实体ID
*/
async invalidateRelated(entity, operation, entityId = null) {
const patterns = [];
switch (entity) {
case 'farm':
patterns.push('farms:*', 'stats:*');
if (entityId) {
patterns.push(`farms:detail:${entityId}`, `farms:animals:${entityId}`, `farms:devices:${entityId}`);
}
break;
case 'device':
patterns.push('devices:*', 'stats:*');
if (entityId) {
patterns.push(`devices:status:${entityId}`, `devices:metrics:${entityId}`);
}
break;
case 'animal':
patterns.push('animals:*', 'stats:*');
break;
case 'user':
patterns.push('users:*');
if (entityId) {
patterns.push(`users:profile:${entityId}`, `users:permissions:${entityId}`);
}
break;
default:
patterns.push('stats:*'); // 默认清理统计缓存
}
let totalDeleted = 0;
for (const pattern of patterns) {
const deleted = await this.clearPattern(pattern);
totalDeleted += deleted;
}
logger.info(`智能缓存失效: ${entity}/${operation}, 清理了 ${totalDeleted} 个缓存键`);
return totalDeleted;
}
/**
* 按模式清理缓存
* @private
*/
async clearPattern(pattern) {
if (!this.isConnected) {
return 0;
}
try {
const fullPattern = `${CACHE_CONFIG.prefix}${pattern}`;
const keys = await this.client.keys(fullPattern);
if (keys.length === 0) {
return 0;
}
const result = await this.client.del(keys);
return result;
} catch (error) {
logger.error(`按模式清理缓存失败 ${pattern}:`, error);
return 0;
}
}
}
// 创建缓存服务实例
const cacheService = new AdvancedCache();
// 优雅关闭处理
process.on('SIGINT', async () => {
logger.info('收到SIGINT信号正在关闭Redis连接...');
await cacheService.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('收到SIGTERM信号正在关闭Redis连接...');
await cacheService.close();
process.exit(0);
});
module.exports = {
cacheService,
CACHE_KEYS,
CACHE_CONFIG
};

View File

@@ -0,0 +1,531 @@
/**
* 通知服务
* @file notificationService.js
* @description 实现邮件/短信预警通知功能确保5分钟内响应时间
*/
const nodemailer = require('nodemailer');
const logger = require('../utils/logger');
const { User, Farm } = require('../models');
class NotificationService {
constructor() {
this.emailTransporter = null;
this.smsService = null; // 短信服务接口(后续可扩展)
this.notificationQueue = []; // 通知队列
this.isProcessing = false;
this.maxRetries = 3;
this.retryDelay = 1000; // 1秒重试延迟
// 初始化邮件服务
this.initEmailService();
}
/**
* 初始化邮件服务
*/
initEmailService() {
try {
// 配置邮件传输器(使用环境变量配置)
this.emailTransporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || 'smtp.example.com',
port: process.env.SMTP_PORT || 587,
secure: false, // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER || 'noreply@farm-monitor.com',
pass: process.env.SMTP_PASS || 'your_email_password'
},
tls: {
rejectUnauthorized: false
}
});
logger.info('邮件服务初始化完成');
} catch (error) {
logger.error('邮件服务初始化失败:', error);
}
}
/**
* 发送预警通知
* @param {Object} alert 预警对象
* @param {Array} recipients 接收人列表
* @param {Object} options 通知选项
*/
async sendAlertNotification(alert, recipients = [], options = {}) {
const {
urgent = false,
includeSMS = false,
maxResponseTime = 300000 // 5分钟 = 300000毫秒
} = options;
const notification = {
id: `alert_${alert.id}_${Date.now()}`,
type: 'alert',
alert,
recipients,
urgent,
includeSMS,
createdAt: new Date(),
maxResponseTime,
retryCount: 0,
status: 'pending'
};
// 添加到通知队列
this.notificationQueue.push(notification);
// 立即处理紧急通知
if (urgent) {
await this.processNotification(notification);
} else {
// 非紧急通知进入队列处理
this.processQueue();
}
logger.info(`预警通知已加入队列: ${notification.id}, 紧急程度: ${urgent}`);
return notification.id;
}
/**
* 处理通知队列
*/
async processQueue() {
if (this.isProcessing) return;
this.isProcessing = true;
try {
while (this.notificationQueue.length > 0) {
const notification = this.notificationQueue.shift();
// 检查是否超时
const timePassed = Date.now() - notification.createdAt.getTime();
if (timePassed > notification.maxResponseTime) {
logger.warn(`通知 ${notification.id} 已超时,跳过处理`);
continue;
}
await this.processNotification(notification);
}
} catch (error) {
logger.error('处理通知队列失败:', error);
} finally {
this.isProcessing = false;
}
}
/**
* 处理单个通知
* @param {Object} notification 通知对象
*/
async processNotification(notification) {
try {
const startTime = Date.now();
// 获取接收人列表
const recipients = await this.getNotificationRecipients(notification);
// 发送邮件通知
const emailResults = await this.sendEmailNotifications(notification, recipients);
// 发送短信通知(如果需要)
let smsResults = [];
if (notification.includeSMS) {
smsResults = await this.sendSMSNotifications(notification, recipients);
}
const endTime = Date.now();
const responseTime = endTime - startTime;
// 记录通知发送结果
notification.status = 'completed';
notification.responseTime = responseTime;
notification.emailResults = emailResults;
notification.smsResults = smsResults;
logger.info(`通知 ${notification.id} 发送完成,响应时间: ${responseTime}ms`);
// 检查是否超过5分钟响应时间要求
if (responseTime > 300000) {
logger.warn(`通知 ${notification.id} 响应时间超过5分钟: ${responseTime}ms`);
}
} catch (error) {
logger.error(`处理通知 ${notification.id} 失败:`, error);
// 重试机制
if (notification.retryCount < this.maxRetries) {
notification.retryCount++;
notification.status = 'retrying';
setTimeout(() => {
this.notificationQueue.unshift(notification); // 重新加入队列头部
this.processQueue();
}, this.retryDelay * notification.retryCount);
logger.info(`通知 ${notification.id} 将进行第 ${notification.retryCount} 次重试`);
} else {
notification.status = 'failed';
logger.error(`通知 ${notification.id} 达到最大重试次数,标记为失败`);
}
}
}
/**
* 获取通知接收人
* @param {Object} notification 通知对象
* @returns {Array} 接收人列表
*/
async getNotificationRecipients(notification) {
try {
const alert = notification.alert;
const recipients = [];
// 如果指定了接收人,直接使用
if (notification.recipients && notification.recipients.length > 0) {
return notification.recipients;
}
// 获取农场相关负责人
if (alert.farm_id) {
const farm = await Farm.findByPk(alert.farm_id);
if (farm && farm.contact) {
recipients.push({
name: farm.contact,
email: `${farm.contact.toLowerCase().replace(/\s+/g, '')}@farm-monitor.com`,
phone: farm.phone,
role: 'farm_manager'
});
}
}
// 获取系统管理员
const admins = await User.findAll({
include: [{
model: require('../models').Role,
as: 'role',
where: { name: 'admin' }
}]
});
for (const admin of admins) {
recipients.push({
name: admin.username,
email: admin.email,
phone: admin.phone,
role: 'admin'
});
}
return recipients;
} catch (error) {
logger.error('获取通知接收人失败:', error);
return [];
}
}
/**
* 发送邮件通知
* @param {Object} notification 通知对象
* @param {Array} recipients 接收人列表
* @returns {Array} 发送结果
*/
async sendEmailNotifications(notification, recipients) {
if (!this.emailTransporter) {
logger.warn('邮件服务未初始化,跳过邮件发送');
return [];
}
const results = [];
const alert = notification.alert;
for (const recipient of recipients) {
if (!recipient.email) continue;
try {
const emailContent = this.generateEmailContent(alert, recipient);
const mailOptions = {
from: process.env.SMTP_FROM || '"宁夏智慧养殖监管平台" <noreply@farm-monitor.com>',
to: recipient.email,
subject: emailContent.subject,
html: emailContent.html,
priority: alert.level === 'critical' ? 'high' : 'normal'
};
const result = await this.emailTransporter.sendMail(mailOptions);
results.push({
recipient: recipient.email,
status: 'success',
messageId: result.messageId
});
logger.info(`邮件通知发送成功: ${recipient.email}`);
} catch (error) {
results.push({
recipient: recipient.email,
status: 'failed',
error: error.message
});
logger.error(`邮件通知发送失败: ${recipient.email}`, error);
}
}
return results;
}
/**
* 生成邮件内容
* @param {Object} alert 预警对象
* @param {Object} recipient 接收人信息
* @returns {Object} 邮件内容
*/
generateEmailContent(alert, recipient) {
const levelMap = {
low: { text: '低级', color: '#52c41a' },
medium: { text: '中级', color: '#1890ff' },
high: { text: '高级', color: '#fa8c16' },
critical: { text: '紧急', color: '#f5222d' }
};
const levelInfo = levelMap[alert.level] || levelMap.medium;
const urgentFlag = alert.level === 'critical' ? '🚨 ' : '';
const subject = `${urgentFlag}宁夏智慧养殖监管平台 - ${levelInfo.text}预警通知`;
const html = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center;">
<h1 style="margin: 0;">宁夏智慧养殖监管平台</h1>
<p style="margin: 5px 0 0 0;">智慧养殖 · 科学监管</p>
</div>
<div style="padding: 30px; background: #f8f9fa;">
<h2 style="color: ${levelInfo.color}; margin-top: 0;">
${urgentFlag}${levelInfo.text}预警通知
</h2>
<div style="background: white; padding: 20px; border-radius: 8px; border-left: 4px solid ${levelInfo.color};">
<p><strong>尊敬的 ${recipient.name}</strong></p>
<p>系统检测到以下预警信息,请及时处理:</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold; width: 120px;">预警级别:</td>
<td style="padding: 8px; color: ${levelInfo.color}; font-weight: bold;">${levelInfo.text}</td>
</tr>
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">预警类型:</td>
<td style="padding: 8px;">${alert.type}</td>
</tr>
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">预警内容:</td>
<td style="padding: 8px;">${alert.message}</td>
</tr>
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">所属养殖场:</td>
<td style="padding: 8px;">${alert.farm_name || '未知'}</td>
</tr>
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">发生时间:</td>
<td style="padding: 8px;">${new Date(alert.created_at).toLocaleString('zh-CN')}</td>
</tr>
</table>
${alert.level === 'critical' ?
'<div style="background: #fff2f0; border: 1px solid #ffccc7; padding: 15px; border-radius: 4px; color: #f5222d;"><strong>⚠️ 这是一个紧急预警,请立即处理!</strong></div>' :
'<p style="color: #666;">请及时登录系统查看详细信息并处理相关问题。</p>'
}
</div>
<div style="text-align: center; margin-top: 30px;">
<a href="${process.env.FRONTEND_URL || 'http://localhost:5300'}/alerts"
style="background: ${levelInfo.color}; color: white; padding: 12px 30px; text-decoration: none; border-radius: 4px; font-weight: bold;">
立即处理预警
</a>
</div>
</div>
<div style="background: #e9ecef; padding: 15px; text-align: center; color: #6c757d; font-size: 12px;">
<p>此邮件由宁夏智慧养殖监管平台自动发送,请勿回复</p>
<p>如需帮助,请联系系统管理员</p>
</div>
</div>
`;
return { subject, html };
}
/**
* 发送短信通知(预留接口)
* @param {Object} notification 通知对象
* @param {Array} recipients 接收人列表
* @returns {Array} 发送结果
*/
async sendSMSNotifications(notification, recipients) {
// 这里可以集成阿里云短信、腾讯云短信等服务
const results = [];
const alert = notification.alert;
for (const recipient of recipients) {
if (!recipient.phone) continue;
try {
// 短信内容
const smsContent = `【宁夏智慧养殖监管平台】${alert.level === 'critical' ? '紧急' : ''}预警:${alert.message}。请及时处理。时间:${new Date().toLocaleString('zh-CN')}`;
// 这里实现具体的短信发送逻辑
// const smsResult = await this.sendSMS(recipient.phone, smsContent);
// 目前记录到日志(实际部署时替换为真实短信服务)
logger.info(`短信通知(模拟)已发送到 ${recipient.phone}: ${smsContent}`);
results.push({
recipient: recipient.phone,
status: 'success',
content: smsContent
});
} catch (error) {
results.push({
recipient: recipient.phone,
status: 'failed',
error: error.message
});
logger.error(`短信通知发送失败: ${recipient.phone}`, error);
}
}
return results;
}
/**
* 发送系统状态通知
* @param {Object} statusData 系统状态数据
* @param {Array} adminEmails 管理员邮箱列表
*/
async sendSystemStatusNotification(statusData, adminEmails = []) {
if (!this.emailTransporter) return;
const subject = `宁夏智慧养殖监管平台 - 系统状态报告`;
const html = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #1890ff; color: white; padding: 20px; text-align: center;">
<h1 style="margin: 0;">系统状态报告</h1>
<p style="margin: 5px 0 0 0;">宁夏智慧养殖监管平台</p>
</div>
<div style="padding: 30px; background: #f8f9fa;">
<h2 style="color: #333; margin-top: 0;">系统运行状况</h2>
<div style="background: white; padding: 20px; border-radius: 8px;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">API响应时间:</td>
<td style="padding: 8px;">${statusData.apiResponseTime || 'N/A'}ms</td>
</tr>
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">数据库连接状态:</td>
<td style="padding: 8px;">${statusData.dbConnected ? '正常' : '异常'}</td>
</tr>
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">系统内存使用率:</td>
<td style="padding: 8px;">${statusData.memoryUsage || 'N/A'}%</td>
</tr>
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">活跃用户数:</td>
<td style="padding: 8px;">${statusData.activeUsers || 0}</td>
</tr>
<tr>
<td style="padding: 8px; background: #f1f3f4; font-weight: bold;">报告时间:</td>
<td style="padding: 8px;">${new Date().toLocaleString('zh-CN')}</td>
</tr>
</table>
</div>
</div>
<div style="background: #e9ecef; padding: 15px; text-align: center; color: #6c757d; font-size: 12px;">
<p>此邮件由系统自动发送</p>
</div>
</div>
`;
for (const email of adminEmails) {
try {
await this.emailTransporter.sendMail({
from: process.env.SMTP_FROM || '"宁夏智慧养殖监管平台" <noreply@farm-monitor.com>',
to: email,
subject,
html
});
logger.info(`系统状态报告已发送到: ${email}`);
} catch (error) {
logger.error(`系统状态报告发送失败: ${email}`, error);
}
}
}
/**
* 测试邮件服务
* @param {string} testEmail 测试邮箱
* @returns {boolean} 测试结果
*/
async testEmailService(testEmail) {
if (!this.emailTransporter) {
logger.error('邮件服务未初始化');
return false;
}
try {
const result = await this.emailTransporter.sendMail({
from: process.env.SMTP_FROM || '"宁夏智慧养殖监管平台" <noreply@farm-monitor.com>',
to: testEmail,
subject: '宁夏智慧养殖监管平台 - 邮件服务测试',
html: `
<div style="font-family: Arial, sans-serif; padding: 20px; text-align: center;">
<h2 style="color: #1890ff;">邮件服务测试</h2>
<p>如果您收到此邮件,说明邮件服务配置正确。</p>
<p>测试时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
`
});
logger.info(`测试邮件发送成功: ${testEmail}, MessageID: ${result.messageId}`);
return true;
} catch (error) {
logger.error(`测试邮件发送失败: ${testEmail}`, error);
return false;
}
}
/**
* 获取通知统计信息
* @returns {Object} 统计信息
*/
getNotificationStats() {
const completedNotifications = this.notificationQueue.filter(n => n.status === 'completed');
const failedNotifications = this.notificationQueue.filter(n => n.status === 'failed');
const avgResponseTime = completedNotifications.length > 0
? completedNotifications.reduce((sum, n) => sum + n.responseTime, 0) / completedNotifications.length
: 0;
return {
totalNotifications: this.notificationQueue.length,
completedCount: completedNotifications.length,
failedCount: failedNotifications.length,
avgResponseTime: Math.round(avgResponseTime),
queueLength: this.notificationQueue.filter(n => n.status === 'pending').length
};
}
}
// 创建单例实例
const notificationService = new NotificationService();
module.exports = notificationService;

View File

@@ -0,0 +1,364 @@
/**
* 实时数据推送服务
* @file realtimeService.js
* @description 定期检查数据变化并通过WebSocket推送给客户端
*/
const cron = require('node-cron');
const { Device, Alert, Animal, Farm } = require('../models');
const { sequelize } = require('../config/database-simple');
const webSocketManager = require('../utils/websocket');
const notificationService = require('./notificationService');
const logger = require('../utils/logger');
const { Op } = require('sequelize');
class RealtimeService {
constructor() {
this.isRunning = false;
this.lastUpdateTimes = {
devices: null,
alerts: null,
animals: null,
stats: null
};
this.updateInterval = 30; // 30秒更新间隔符合文档要求
}
/**
* 启动实时数据推送服务
*/
start() {
if (this.isRunning) {
logger.warn('实时数据推送服务已在运行中');
return;
}
this.isRunning = true;
// 设备状态监控 - 每30秒检查一次
cron.schedule(`*/${this.updateInterval} * * * * *`, () => {
this.checkDeviceUpdates();
});
// 预警监控 - 每10秒检查一次预警更紧急
cron.schedule('*/10 * * * * *', () => {
this.checkAlertUpdates();
});
// 动物健康状态监控 - 每60秒检查一次
cron.schedule('*/60 * * * * *', () => {
this.checkAnimalUpdates();
});
// 系统统计数据更新 - 每2分钟更新一次
cron.schedule('*/120 * * * * *', () => {
this.updateSystemStats();
});
logger.info('实时数据推送服务已启动');
console.log(`实时数据推送服务已启动,更新间隔: ${this.updateInterval}`);
}
/**
* 停止实时数据推送服务
*/
stop() {
this.isRunning = false;
logger.info('实时数据推送服务已停止');
}
/**
* 检查设备状态更新
*/
async checkDeviceUpdates() {
try {
const lastCheck = this.lastUpdateTimes.devices || new Date(Date.now() - 60000); // 默认检查最近1分钟
const updatedDevices = await Device.findAll({
where: {
updated_at: {
[Op.gt]: lastCheck
}
},
include: [{
model: Farm,
as: 'farm',
attributes: ['id', 'name']
}],
order: [['updated_at', 'DESC']]
});
if (updatedDevices.length > 0) {
logger.info(`检测到 ${updatedDevices.length} 个设备状态更新`);
// 为每个更新的设备推送数据
for (const device of updatedDevices) {
webSocketManager.broadcastDeviceUpdate({
id: device.id,
name: device.name,
type: device.type,
status: device.status,
farm_id: device.farm_id,
farm_name: device.farm?.name,
last_maintenance: device.last_maintenance,
metrics: device.metrics,
updated_at: device.updated_at
});
}
this.lastUpdateTimes.devices = new Date();
}
} catch (error) {
logger.error('检查设备更新失败:', error);
}
}
/**
* 检查预警更新
*/
async checkAlertUpdates() {
try {
const lastCheck = this.lastUpdateTimes.alerts || new Date(Date.now() - 30000); // 默认检查最近30秒
const newAlerts = await Alert.findAll({
where: {
created_at: {
[Op.gt]: lastCheck
}
},
include: [
{
model: Farm,
as: 'farm',
attributes: ['id', 'name', 'contact', 'phone']
},
{
model: Device,
as: 'device',
attributes: ['id', 'name', 'type']
}
],
order: [['created_at', 'DESC']]
});
if (newAlerts.length > 0) {
logger.info(`检测到 ${newAlerts.length} 个新预警`);
// 推送新预警
for (const alert of newAlerts) {
webSocketManager.broadcastAlert({
id: alert.id,
type: alert.type,
level: alert.level,
message: alert.message,
status: alert.status,
farm_id: alert.farm_id,
farm_name: alert.farm?.name,
device_id: alert.device_id,
device_name: alert.device?.name,
created_at: alert.created_at
});
// 如果是高级或紧急预警,立即发送通知
if (alert.level === 'high' || alert.level === 'critical') {
await this.sendUrgentNotification(alert);
}
}
this.lastUpdateTimes.alerts = new Date();
}
} catch (error) {
logger.error('检查预警更新失败:', error);
}
}
/**
* 检查动物健康状态更新
*/
async checkAnimalUpdates() {
try {
const lastCheck = this.lastUpdateTimes.animals || new Date(Date.now() - 120000); // 默认检查最近2分钟
const updatedAnimals = await Animal.findAll({
where: {
updated_at: {
[Op.gt]: lastCheck
}
},
include: [{
model: Farm,
as: 'farm',
attributes: ['id', 'name']
}],
order: [['updated_at', 'DESC']]
});
if (updatedAnimals.length > 0) {
logger.info(`检测到 ${updatedAnimals.length} 个动物健康状态更新`);
for (const animal of updatedAnimals) {
webSocketManager.broadcastAnimalUpdate({
id: animal.id,
type: animal.type,
count: animal.count,
health_status: animal.health_status,
farm_id: animal.farm_id,
farm_name: animal.farm?.name,
last_inspection: animal.last_inspection,
updated_at: animal.updated_at
});
}
this.lastUpdateTimes.animals = new Date();
}
} catch (error) {
logger.error('检查动物更新失败:', error);
}
}
/**
* 更新系统统计数据
*/
async updateSystemStats() {
try {
const stats = await this.getSystemStats();
webSocketManager.broadcastStatsUpdate(stats);
this.lastUpdateTimes.stats = new Date();
logger.info('系统统计数据已推送');
} catch (error) {
logger.error('更新系统统计失败:', error);
}
}
/**
* 获取系统统计数据
* @returns {Promise<Object>} 统计数据
*/
async getSystemStats() {
try {
const [farmCount, deviceCount, animalCount, alertCount] = await Promise.all([
Farm.count(),
Device.count(),
Animal.sum('count'),
Alert.count({ where: { status: 'active' } })
]);
const deviceStatusStats = await Device.findAll({
attributes: [
'status',
[sequelize.fn('COUNT', sequelize.col('status')), 'count']
],
group: ['status']
});
const alertLevelStats = await Alert.findAll({
where: { status: 'active' },
attributes: [
'level',
[sequelize.fn('COUNT', sequelize.col('level')), 'count']
],
group: ['level']
});
return {
farmCount: farmCount || 0,
deviceCount: deviceCount || 0,
animalCount: animalCount || 0,
alertCount: alertCount || 0,
deviceStatus: deviceStatusStats.reduce((acc, item) => {
acc[item.status] = parseInt(item.dataValues.count);
return acc;
}, {}),
alertLevels: alertLevelStats.reduce((acc, item) => {
acc[item.level] = parseInt(item.dataValues.count);
return acc;
}, {}),
timestamp: new Date()
};
} catch (error) {
logger.error('获取系统统计数据失败:', error);
return {
error: '获取统计数据失败',
timestamp: new Date()
};
}
}
/**
* 发送紧急预警通知
* @param {Object} alert 预警对象
*/
async sendUrgentNotification(alert) {
try {
logger.warn(`紧急预警: ${alert.message} (级别: ${alert.level})`);
// 发送实时WebSocket通知给管理员
webSocketManager.broadcastAlert({
id: alert.id,
type: alert.type,
level: alert.level,
message: alert.message,
farm_id: alert.farm_id,
farm_name: alert.farm?.name,
created_at: alert.created_at
});
// 发送邮件/短信通知
const isUrgent = alert.level === 'critical' || alert.level === 'high';
await notificationService.sendAlertNotification(alert, [], {
urgent: isUrgent,
includeSMS: alert.level === 'critical', // 仅紧急预警发送短信
maxResponseTime: 300000 // 5分钟响应时间
});
logger.info(`预警通知已发送: 预警ID ${alert.id}, 紧急程度: ${isUrgent}`);
} catch (error) {
logger.error('发送紧急预警通知失败:', error);
}
}
/**
* 模拟设备数据变化(用于演示)
*/
async simulateDeviceChange(deviceId) {
try {
const device = await Device.findByPk(deviceId);
if (!device) return;
// 随机改变设备状态
const statuses = ['online', 'offline', 'maintenance'];
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
await device.update({
status: randomStatus,
metrics: {
temperature: Math.random() * 10 + 20, // 20-30度
humidity: Math.random() * 20 + 50, // 50-70%
lastUpdate: new Date()
}
});
logger.info(`模拟设备 ${deviceId} 状态变化为: ${randomStatus}`);
} catch (error) {
logger.error('模拟设备变化失败:', error);
}
}
/**
* 获取服务状态
* @returns {Object} 服务状态
*/
getStatus() {
return {
isRunning: this.isRunning,
lastUpdateTimes: this.lastUpdateTimes,
updateInterval: this.updateInterval,
connectedClients: webSocketManager.getConnectionStats()
};
}
}
// 创建单例实例
const realtimeService = new RealtimeService();
module.exports = realtimeService;

View File

@@ -0,0 +1,316 @@
/**
* 报表生成服务
* @file reportService.js
* @description 实现PDF报表生成和数据导出功能
*/
// const puppeteer = require('puppeteer'); // 暂时注释,避免模块缺失错误
const XLSX = require('xlsx');
const ejs = require('ejs');
const path = require('path');
const fs = require('fs');
const moment = require('moment');
const { Farm, Animal, Device, Alert, User, Product, Order, OrderItem } = require('../models');
const { Op, Sequelize } = require('sequelize');
const logger = require('../utils/logger');
// 设置中文本地化
moment.locale('zh-cn');
class ReportService {
constructor() {
this.templateDir = path.join(__dirname, '../templates');
this.outputDir = path.join(__dirname, '../reports');
this.ensureDirectories();
}
ensureDirectories() {
if (!fs.existsSync(this.templateDir)) {
fs.mkdirSync(this.templateDir, { recursive: true });
}
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
}
}
// 生成Excel报表
async generateExcelReport(data, filename) {
try {
const workbook = XLSX.utils.book_new();
// 添加农场数据
if (data.farms && data.farms.length > 0) {
const farmsSheet = XLSX.utils.json_to_sheet(data.farms);
XLSX.utils.book_append_sheet(workbook, farmsSheet, '农场数据');
}
// 添加动物数据
if (data.animals && data.animals.length > 0) {
const animalsSheet = XLSX.utils.json_to_sheet(data.animals);
XLSX.utils.book_append_sheet(workbook, animalsSheet, '动物数据');
}
// 添加设备数据
if (data.devices && data.devices.length > 0) {
const devicesSheet = XLSX.utils.json_to_sheet(data.devices);
XLSX.utils.book_append_sheet(workbook, devicesSheet, '设备数据');
}
// 添加告警数据
if (data.alerts && data.alerts.length > 0) {
const alertsSheet = XLSX.utils.json_to_sheet(data.alerts);
XLSX.utils.book_append_sheet(workbook, alertsSheet, '告警数据');
}
// 添加订单数据
if (data.orders && data.orders.length > 0) {
const ordersSheet = XLSX.utils.json_to_sheet(data.orders);
XLSX.utils.book_append_sheet(workbook, ordersSheet, '订单数据');
}
const filePath = path.join(this.outputDir, filename);
XLSX.writeFile(workbook, filePath);
logger.info(`Excel报表生成成功: ${filePath}`, { service: 'reportService' });
return filePath;
} catch (error) {
logger.error('Excel报表生成失败:', error, { service: 'reportService' });
throw error;
}
}
// PDF生成功能暂时禁用
async generatePDFReport(data, filename) {
logger.warn('PDF报表功能暂时不可用puppeteer模块未安装', { service: 'reportService' });
throw new Error('PDF报表功能暂时不可用请使用Excel格式或联系管理员安装puppeteer模块');
}
// HTML转PDF功能暂时禁用
async generatePDFFromHTML(htmlContent, filename) {
logger.warn('PDF生成功能暂时不可用puppeteer模块未安装', { service: 'reportService' });
throw new Error('PDF生成功能暂时不可用请使用Excel格式或联系管理员安装puppeteer模块');
}
// 获取农场统计数据
async getFarmStatistics(farmId = null) {
try {
const whereClause = farmId ? { id: farmId } : {};
const farms = await Farm.findAll({
where: whereClause,
include: [
{
model: Animal,
as: 'animals',
required: false
},
{
model: Device,
as: 'devices',
required: false
}
]
});
return farms;
} catch (error) {
logger.error('获取农场统计数据失败:', error, { service: 'reportService' });
throw error;
}
}
// 获取动物统计数据
async getAnimalStatistics(farmId = null) {
try {
const whereClause = farmId ? { farmId } : {};
const animals = await Animal.findAll({
where: whereClause,
include: [
{
model: Farm,
as: 'farm',
attributes: ['name', 'location']
}
]
});
return animals;
} catch (error) {
logger.error('获取动物统计数据失败:', error, { service: 'reportService' });
throw error;
}
}
// 获取设备统计数据
async getDeviceStatistics(farmId = null) {
try {
const whereClause = farmId ? { farmId } : {};
const devices = await Device.findAll({
where: whereClause,
include: [
{
model: Farm,
as: 'farm',
attributes: ['name', 'location']
}
]
});
return devices;
} catch (error) {
logger.error('获取设备统计数据失败:', error, { service: 'reportService' });
throw error;
}
}
// 获取告警统计数据
async getAlertStatistics(farmId = null) {
try {
const whereClause = farmId ? { farmId } : {};
const alerts = await Alert.findAll({
where: whereClause,
include: [
{
model: Farm,
as: 'farm',
attributes: ['name', 'location']
}
]
});
return alerts;
} catch (error) {
logger.error('获取告警统计数据失败:', error, { service: 'reportService' });
throw error;
}
}
// 获取订单统计数据
async getOrderStatistics(farmId = null) {
try {
const whereClause = farmId ? { farmId } : {};
const orders = await Order.findAll({
where: whereClause,
include: [
{
model: User,
as: 'user',
attributes: ['username', 'email']
},
{
model: OrderItem,
as: 'items',
include: [
{
model: Product,
as: 'product',
attributes: ['name', 'price']
}
]
}
]
});
return orders;
} catch (error) {
logger.error('获取订单统计数据失败:', error, { service: 'reportService' });
throw error;
}
}
// 生成综合报表
async generateComprehensiveReport(farmId = null, format = 'excel') {
try {
const data = {
farms: await this.getFarmStatistics(farmId),
animals: await this.getAnimalStatistics(farmId),
devices: await this.getDeviceStatistics(farmId),
alerts: await this.getAlertStatistics(farmId),
orders: await this.getOrderStatistics(farmId)
};
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
const filename = `comprehensive_report_${timestamp}.${format}`;
if (format === 'excel') {
return await this.generateExcelReport(data, filename);
} else if (format === 'pdf') {
return await this.generatePDFReport(data, filename);
} else {
throw new Error('不支持的报表格式');
}
} catch (error) {
logger.error('生成综合报表失败:', error, { service: 'reportService' });
throw error;
}
}
// 清理过期报表文件
async cleanupOldReports(daysOld = 7) {
try {
const files = fs.readdirSync(this.outputDir);
const cutoffTime = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
let cleanedCount = 0;
files.forEach(file => {
const filePath = path.join(this.outputDir, file);
const stats = fs.statSync(filePath);
if (stats.mtime.getTime() < cutoffTime) {
fs.unlinkSync(filePath);
cleanedCount++;
}
});
logger.info(`清理了 ${cleanedCount} 个过期报表文件`, { service: 'reportService' });
return cleanedCount;
} catch (error) {
logger.error('清理过期报表文件失败:', error, { service: 'reportService' });
throw error;
}
}
// 获取报表文件列表
async getReportFiles() {
try {
const files = fs.readdirSync(this.outputDir);
const fileList = files.map(file => {
const filePath = path.join(this.outputDir, file);
const stats = fs.statSync(filePath);
return {
filename: file,
size: stats.size,
created: stats.birthtime,
modified: stats.mtime
};
});
return fileList.sort((a, b) => b.modified - a.modified);
} catch (error) {
logger.error('获取报表文件列表失败:', error, { service: 'reportService' });
throw error;
}
}
// 删除报表文件
async deleteReportFile(filename) {
try {
const filePath = path.join(this.outputDir, filename);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
logger.info(`报表文件删除成功: ${filename}`, { service: 'reportService' });
return true;
} else {
throw new Error('文件不存在');
}
} catch (error) {
logger.error('删除报表文件失败:', error, { service: 'reportService' });
throw error;
}
}
}
module.exports = new ReportService();