532 lines
18 KiB
JavaScript
532 lines
18 KiB
JavaScript
/**
|
||
* 通知服务
|
||
* @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;
|