190 lines
5.2 KiB
JavaScript
190 lines
5.2 KiB
JavaScript
// 通知服务 - 支持多渠道通知
|
||
const axios = require('axios');
|
||
|
||
class NotificationService {
|
||
constructor() {
|
||
// 企业微信
|
||
this.weixinCorpId = process.env.WEIXIN_CORP_ID;
|
||
this.weixinAgentId = process.env.WEIXIN_AGENT_ID;
|
||
this.weixinSecret = process.env.WEIXIN_SECRET;
|
||
this.weixinToken = null;
|
||
this.weixinTokenExpiry = null;
|
||
|
||
// 钉钉
|
||
this.dingtalkToken = process.env.DINGTALK_TOKEN;
|
||
|
||
// 邮件
|
||
this.smtpConfig = {
|
||
host: process.env.SMTP_HOST,
|
||
port: process.env.SMTP_PORT,
|
||
user: process.env.SMTP_USER,
|
||
password: process.env.SMTP_PASSWORD,
|
||
};
|
||
}
|
||
|
||
// 获取企业微信 Access Token
|
||
async getWeChatToken() {
|
||
if (this.weixinToken && this.weixinTokenExpiry > Date.now()) {
|
||
return this.weixinToken;
|
||
}
|
||
|
||
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
|
||
const params = {
|
||
corpid: this.weixinCorpId,
|
||
corpsecret: this.weixinSecret,
|
||
};
|
||
|
||
const response = await axios.get(url, { params });
|
||
const { access_token, expires_in } = response.data;
|
||
|
||
if (response.data.errcode !== 0) {
|
||
throw new Error(`企业微信 Token 获取失败: ${response.data.errmsg}`);
|
||
}
|
||
|
||
this.weixinToken = access_token;
|
||
this.weixinTokenExpiry = Date.now() + (expires_in - 60) * 1000;
|
||
|
||
return access_token;
|
||
}
|
||
|
||
// 发送企业微信消息
|
||
async sendWeChat(userIds, message) {
|
||
if (!this.weixinCorpId || !this.weixinAgentId) {
|
||
console.log('[通知] 企业微信未配置,跳过');
|
||
return { success: false, reason: 'not_configured' };
|
||
}
|
||
|
||
try {
|
||
const token = await this.getWeChatToken();
|
||
const url = `https://qyapi.weixin.qq.com/cgi-bin/message/send`;
|
||
|
||
const response = await axios.post(
|
||
url,
|
||
{
|
||
touser: userIds.join('|'),
|
||
msgtype: 'text',
|
||
agentid: this.weixinAgentId,
|
||
text: { content: message },
|
||
},
|
||
{ params: { access_token: token } }
|
||
);
|
||
|
||
if (response.data.errcode !== 0) {
|
||
throw new Error(response.data.errmsg);
|
||
}
|
||
|
||
return { success: true, messageId: response.data.msgid };
|
||
} catch (error) {
|
||
console.error('[通知] 企业微信发送失败:', error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 发送钉钉消息
|
||
async sendDingTalk(userIds, message) {
|
||
if (!this.dingtalkToken) {
|
||
console.log('[通知] 钉钉未配置,跳过');
|
||
return { success: false, reason: 'not_configured' };
|
||
}
|
||
|
||
try {
|
||
const url = `https://oapi.dingtalk.com/robot/send`;
|
||
const response = await axios.post(
|
||
url,
|
||
{
|
||
msgtype: 'text',
|
||
text: { content: message },
|
||
at: {
|
||
atUserIds: userIds,
|
||
},
|
||
},
|
||
{ params: { access_token: this.dingtalkToken } }
|
||
);
|
||
|
||
if (response.data.errcode !== 0) {
|
||
throw new Error(response.data.errmsg);
|
||
}
|
||
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('[通知] 钉钉发送失败:', error.message);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
// 发送邮件
|
||
async sendEmail(recipients, subject, htmlBody) {
|
||
if (!this.smtpConfig.host) {
|
||
console.log('[通知] 邮件 SMTP 未配置,跳过');
|
||
return { success: false, reason: 'not_configured' };
|
||
}
|
||
|
||
// 实际项目中应使用 nodemailer 或邮件服务
|
||
console.log(`[通知] 发送邮件给 ${recipients.join(', ')}: ${subject}`);
|
||
return { success: true };
|
||
}
|
||
|
||
// 发送短信(预留接口)
|
||
async sendSMS(phoneNumbers, content) {
|
||
console.log(`[通知] 发送短信给 ${phoneNumbers.join(', ')}: ${content}`);
|
||
return { success: true };
|
||
}
|
||
|
||
// 统一发送接口
|
||
async sendToUsers(userIds, message, channels) {
|
||
const results = {};
|
||
|
||
for (const channel of channels) {
|
||
const userIdsForChannel = userIds; // 可以根据用户偏好筛选
|
||
|
||
switch (channel) {
|
||
case 'wechat':
|
||
results.wechat = await this.sendWeChat(userIdsForChannel, message);
|
||
break;
|
||
case 'dingtalk':
|
||
results.dingtalk = await this.sendDingTalk(userIdsForChannel, message);
|
||
break;
|
||
case 'email':
|
||
results.email = await this.sendEmail(userIdsForChannel, '预警通知', message);
|
||
break;
|
||
case 'sms':
|
||
results.sms = await this.sendSMS(userIdsForChannel, message);
|
||
break;
|
||
default:
|
||
results[channel] = { success: false, reason: 'unknown_channel' };
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
// 格式化预警消息
|
||
formatAlertMessage(rule, value, threshold) {
|
||
const severityEmoji = {
|
||
low: 'ℹ️',
|
||
medium: '⚠️',
|
||
high: '🔴',
|
||
critical: '🚨',
|
||
};
|
||
|
||
const severityZh = {
|
||
low: '低',
|
||
medium: '中',
|
||
high: '高',
|
||
critical: '严重',
|
||
};
|
||
|
||
return `
|
||
${severityEmoji[rule.severity] || '⚠️'} 【${severityZh[rule.severity] || '中'}级预警】${rule.name}
|
||
|
||
📊 当前值: ${value}
|
||
🎯 阈值: ${rule.condition === 'gt' ? '大于' : rule.condition === 'lt' ? '小于' : rule.condition} ${threshold}
|
||
⏰ 时间: ${new Date().toLocaleString()}
|
||
|
||
请及时处理!
|
||
`.trim();
|
||
}
|
||
}
|
||
|
||
module.exports = new NotificationService();
|