Files
nxxmdata/backend/services/notificationService.js
2025-09-12 20:08:42 +08:00

532 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 通知服务
* @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;