feat: system logs - email log + system log with admin UI

- db: email_logs (sent/failed) + system_logs (info/warn/error) tables,
  auto-migrated for existing installs
- backend/logger.js: logEmail/logSystem with try/catch (never breaks flow)
- mailer: log send result (SMTP unconfigured/sent/failed + error msg)
- webhook: log blocked-internal, non-2xx response, send failure
- server: error middleware records 500 errors to system_logs
- routes/logs.js: GET /logs/emails + /logs/system (admin/owner, filters)
- UI: 系统日志 page (owner/admin) with system/email tabs + filters
- API docs updated
This commit is contained in:
2026-08-17 05:14:41 +08:00
parent 8d6c3c4a8d
commit a11df22600
10 changed files with 241 additions and 8 deletions

View File

@@ -1,4 +1,5 @@
const { query, getRow } = require('./db');
const { logSystem } = require('./logger');
const EVENT_LABELS = { ticket_created:'工单创建', ticket_claimed:'工单认领', ticket_transferred:'工单转交', ticket_updated:'工单更新' };
@@ -24,14 +25,20 @@ async function sendWebhook(event, data) {
for (const cfg of configs) {
const events = cfg.events || 'all';
if (events !== 'all' && !events.split(',').map(s=>s.trim()).includes(event)) continue;
if (cfg.webhook_url && await isPrivateUrl(cfg.webhook_url)) { console.error('[Webhook] blocked internal URL:', cfg.name); continue; }
if (cfg.webhook_url && await isPrivateUrl(cfg.webhook_url)) { console.error('[Webhook] blocked internal URL:', cfg.name); await logSystem('warn', 'webhook', `拦截内网地址: ${cfg.name}`, { url: cfg.webhook_url }); continue; }
const embed = buildEmbed(event, data);
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 10000);
await fetch(cfg.webhook_url, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(embed), signal: ctrl.signal });
const ctl = new AbortController();
const t = setTimeout(() => ctl.abort(), 10000);
const resp = await fetch(cfg.webhook_url, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(embed), signal: ctl.signal });
clearTimeout(t);
} catch (e) { console.error('[Webhook]', cfg.name, e.message); }
if (resp.status < 200 || resp.status >= 300) {
await logSystem('warn', 'webhook', `Webhook ${cfg.name} 返回 ${resp.status}`, { event, url: cfg.webhook_url });
}
} catch (e) {
console.error('[Webhook]', cfg.name, e.message);
await logSystem('error', 'webhook', `Webhook ${cfg.name} 发送失败: ${e.message}`, { event, url: cfg.webhook_url });
}
}
} catch {}
}