feat: 新增Telegram通知支持,新增手动发送日报功能
1. 新增Telegram机器人通知配置项和实现逻辑 2. 重构通知发送逻辑为统一的sendAllNotifications方法 3. 调整日报定时任务时间为23:30,优化日报内容 4. 后台页面新增手动发送日报按钮和相关接口 5. 优化通知提示文案,兼容多通知渠道
This commit is contained in:
@@ -8,7 +8,12 @@ LOGIN_PASSWORD=你的密码
|
||||
# 选填 - 企业微信群机器人 Webhook URL
|
||||
WECOM_WEBHOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的key
|
||||
|
||||
# 选填 - 电费余额预警阈值(低于此值时发送企业微信通知,设为0或留空关闭)
|
||||
# 选填 - Telegram 机器人 Token(从 @BotFather 获取)
|
||||
TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklmNOPqrstUVwxyz
|
||||
# 选填 - 接收通知的 Telegram 用户 ID(从 @userinfobot 获取)
|
||||
TELEGRAM_CHAT_ID=123456789
|
||||
|
||||
# 选填 - 电费余额预警阈值(低于此值时发送通知,设为0或留空关闭)
|
||||
ALERT_THRESHOLD=20
|
||||
|
||||
# 选填 - Cookie 签名密钥
|
||||
|
||||
@@ -86,7 +86,21 @@ export default function Dashboard({ onLogout }) {
|
||||
const res = await fetch('/api/test-notify');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
message.success('测试消息已发送,请查看企业微信');
|
||||
message.success('测试消息已发送,请检查通知渠道');
|
||||
} else {
|
||||
message.error('发送失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('发送失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendReport = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/send-report');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
message.success('日报已发送');
|
||||
} else {
|
||||
message.error('发送失败');
|
||||
}
|
||||
@@ -333,6 +347,18 @@ body { margin: 0; }
|
||||
>
|
||||
测试通知
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSendReport}
|
||||
size="small"
|
||||
style={{
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 20,
|
||||
color: 'var(--text-secondary)',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
发送日报
|
||||
</Button>
|
||||
<Button
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={handleLogout}
|
||||
|
||||
85
server.js
85
server.js
@@ -54,10 +54,21 @@ if (!WECOM_WEBHOOK_URL) {
|
||||
console.warn('警告: 环境变量 WECOM_WEBHOOK_URL 未设置,企业微信通知功能将不可用');
|
||||
}
|
||||
|
||||
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
||||
|
||||
if (TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID) {
|
||||
console.log('Telegram 机器人通知已启用');
|
||||
} else if (TELEGRAM_BOT_TOKEN || TELEGRAM_CHAT_ID) {
|
||||
console.warn('警告: 需要同时设置 TELEGRAM_BOT_TOKEN 和 TELEGRAM_CHAT_ID 才能使用 Telegram 通知');
|
||||
} else {
|
||||
console.log('Telegram 机器人通知未配置');
|
||||
}
|
||||
|
||||
const ALERT_THRESHOLD = parseFloat(process.env.ALERT_THRESHOLD) || 0;
|
||||
|
||||
if (ALERT_THRESHOLD > 0) {
|
||||
console.log(`电费预警: 当余额低于 ¥${ALERT_THRESHOLD} 时将发送企业微信通知`);
|
||||
console.log(`电费预警: 当余额低于 ¥${ALERT_THRESHOLD} 时将发送通知`);
|
||||
}
|
||||
|
||||
let lastAlertedAmount = null;
|
||||
@@ -99,6 +110,39 @@ async function sendWecomNotification(content) {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTelegramNotification(text) {
|
||||
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) return;
|
||||
const tgText = text
|
||||
.replace(/^## (.+)$/gm, '*$1*')
|
||||
.replace(/^> /gm, '')
|
||||
.trim();
|
||||
try {
|
||||
const res = await fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_id: TELEGRAM_CHAT_ID,
|
||||
text: tgText,
|
||||
parse_mode: 'Markdown',
|
||||
disable_web_page_preview: true,
|
||||
})
|
||||
});
|
||||
const body = await res.json();
|
||||
if (!body.ok) {
|
||||
console.error(`Telegram 通知发送失败: ${body.description}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Telegram 通知发送失败:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendAllNotifications(markdownContent, plainText) {
|
||||
await Promise.all([
|
||||
sendWecomNotification(markdownContent),
|
||||
sendTelegramNotification(plainText || markdownContent),
|
||||
]);
|
||||
}
|
||||
|
||||
function getLocalDateStr(d) {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
@@ -156,7 +200,7 @@ async function collectData() {
|
||||
const data = await fetchData();
|
||||
if (!data) {
|
||||
console.log(`[${new Date().toLocaleString()}] 采集失败: 接口请求异常`);
|
||||
await sendWecomNotification(
|
||||
await sendAllNotifications(
|
||||
'## 电费采集异常\n\n' +
|
||||
`> 时间:${new Date().toLocaleString('zh-CN')}\n\n` +
|
||||
'电费接口请求失败,请检查网络或 Cookie 是否过期。'
|
||||
@@ -189,7 +233,7 @@ async function checkThresholdAndAlert(amount) {
|
||||
if (lastAlertedAmount !== null && amount >= lastAlertedAmount) return;
|
||||
lastAlertedAmount = amount;
|
||||
console.log(`[预警] 余额 ¥${amount} 低于阈值 ¥${ALERT_THRESHOLD}`);
|
||||
await sendWecomNotification(
|
||||
await sendAllNotifications(
|
||||
'## 电费余额预警\n\n' +
|
||||
`> 时间:${new Date().toLocaleString('zh-CN')}\n\n` +
|
||||
`当前余额 **¥${amount.toFixed(2)}**\n\n` +
|
||||
@@ -370,16 +414,15 @@ cron.schedule('0 * * * *', async () => {
|
||||
});
|
||||
|
||||
async function sendDailyReport() {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const dateStr = getLocalDateStr(yesterday);
|
||||
const now = new Date();
|
||||
const dateStr = getLocalDateStr(now);
|
||||
|
||||
const records = getRecordsByDate(dateStr);
|
||||
if (records.length < 1) {
|
||||
await sendWecomNotification(
|
||||
await sendAllNotifications(
|
||||
'## 电费日报\n\n' +
|
||||
`> 日期:${dateStr}\n\n` +
|
||||
`昨日无数据记录`
|
||||
`今日无数据记录`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -388,7 +431,7 @@ async function sendDailyReport() {
|
||||
const first = sorted[0];
|
||||
const last = sorted[sorted.length - 1];
|
||||
|
||||
const dayBefore = new Date(yesterday);
|
||||
const dayBefore = new Date(now);
|
||||
dayBefore.setDate(dayBefore.getDate() - 1);
|
||||
const dayBeforeRecords = getRecordsByDate(getLocalDateStr(dayBefore));
|
||||
const dayBeforeLast = dayBeforeRecords.length > 0 ? dayBeforeRecords[dayBeforeRecords.length - 1] : null;
|
||||
@@ -399,14 +442,15 @@ async function sendDailyReport() {
|
||||
const cost = Math.round(Math.max(0, baseAmount - last.amount) * 100) / 100;
|
||||
|
||||
const hoursSpan = (last.timestamp - first.timestamp) / (1000 * 60 * 60);
|
||||
const avgPower = hoursSpan > 0 ? Math.round((usage / hoursSpan) * 1000) / 1000 : 0;
|
||||
const reportHours = hoursSpan > 0 ? hoursSpan : Math.max(1, (Date.now() - first.timestamp) / 3600000);
|
||||
const avgPower = Math.round((usage / reportHours) * 1000) / 1000;
|
||||
|
||||
const latest = getLatestRecord();
|
||||
|
||||
let content = '## 电费日报\n\n';
|
||||
content += `> 日期:${dateStr}\n\n`;
|
||||
content += `**昨日用电**:${usage.toFixed(2)} 度\n`;
|
||||
content += `**昨日电费**:¥${cost.toFixed(2)}\n`;
|
||||
content += `**今日用电**:${usage.toFixed(2)} 度\n`;
|
||||
content += `**今日电费**:¥${cost.toFixed(2)}\n`;
|
||||
content += `**平均功率**:${avgPower.toFixed(3)} kW\n`;
|
||||
content += `**数据记录**:${sorted.length} 条\n`;
|
||||
if (latest) {
|
||||
@@ -414,11 +458,11 @@ async function sendDailyReport() {
|
||||
content += `**当前剩余余额**:¥${latest.amount.toFixed(2)}\n`;
|
||||
}
|
||||
|
||||
await sendWecomNotification(content);
|
||||
await sendAllNotifications(content);
|
||||
}
|
||||
|
||||
cron.schedule('0 0 * * *', async () => {
|
||||
console.log(`[定时任务] 发送昨日用电报告...`);
|
||||
cron.schedule('30 23 * * *', async () => {
|
||||
console.log(`[定时任务] 发送今日用电报告...`);
|
||||
await sendDailyReport();
|
||||
});
|
||||
|
||||
@@ -428,12 +472,17 @@ app.get('/api/trigger-collect', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
app.get('/api/test-notify', requireAuth, async (req, res) => {
|
||||
await sendWecomNotification(
|
||||
await sendAllNotifications(
|
||||
'## 电费监控测试消息\n\n' +
|
||||
`> 时间:${new Date().toLocaleString('zh-CN')}\n\n` +
|
||||
'如果收到此消息,说明企业微信通知配置正常。'
|
||||
'如果收到此消息,说明推送通知配置正常。'
|
||||
);
|
||||
res.json({ success: true, message: '测试消息已发送,请查看企业微信' });
|
||||
res.json({ success: true, message: '测试消息已发送,请检查通知渠道' });
|
||||
});
|
||||
|
||||
app.get('/api/send-report', requireAuth, async (req, res) => {
|
||||
await sendDailyReport();
|
||||
res.json({ success: true, message: '日报已发送' });
|
||||
});
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user