From 1a5992845aefa3b5310a2346610161c1b3ab4efc Mon Sep 17 00:00:00 2001 From: stringadmin Date: Wed, 3 Jun 2026 10:52:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E9=94=99=E8=AF=AF=E6=94=B6=E9=9B=86=E4=B8=8Eadmin?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/client-errors: 批量存储客户端错误(10分钟去重合并) - GET /api/client-errors: admin分页查看错误列表 - DELETE /api/client-errors: admin清空错误记录 - 72小时自动清理旧数据 Co-Authored-By: Claude Opus 4.7 --- src/routes/clientErrors.js | 94 ++++++++++++++++++++++++++++++++++++++ src/routes/index.js | 3 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/routes/clientErrors.js diff --git a/src/routes/clientErrors.js b/src/routes/clientErrors.js new file mode 100644 index 0000000..c60c822 --- /dev/null +++ b/src/routes/clientErrors.js @@ -0,0 +1,94 @@ +const crypto = require('node:crypto'); +const { pool, requireAuth, requireAdmin } = require('./context'); + +const MAX_ERRORS_PER_PAGE = 50; +const ERROR_RETENTION_HOURS = 72; + +async function initTable(client) { + await client.query(` + CREATE TABLE IF NOT EXISTS client_errors ( + id SERIAL PRIMARY KEY, + message TEXT NOT NULL, + stack TEXT, + source VARCHAR(32), + url TEXT, + user_agent TEXT, + session_id TEXT, + user_id INTEGER, + count INTEGER DEFAULT 1, + first_seen TIMESTAMP DEFAULT NOW(), + last_seen TIMESTAMP DEFAULT NOW() + ); + CREATE INDEX IF NOT EXISTS idx_client_errors_last_seen ON client_errors(last_seen DESC); + CREATE INDEX IF NOT EXISTS idx_client_errors_user ON client_errors(user_id); + `); +} + +async function storeError(client, error) { + const normalizedMessage = error.message.slice(0, 1000); + const { rows } = await client.query( + `SELECT id, count FROM client_errors WHERE message = $1 AND url = $2 AND source = $3 AND last_seen > NOW() - INTERVAL '10 minutes'`, + [normalizedMessage, error.url || '', error.source || 'manual'] + ); + if (rows.length > 0) { + await client.query( + `UPDATE client_errors SET count = count + 1, last_seen = NOW(), stack = COALESCE($1, stack) WHERE id = $2`, + [error.stack, rows[0].id] + ); + } else { + await client.query( + `INSERT INTO client_errors (message, stack, source, url, user_agent, session_id, user_id) VALUES ($1,$2,$3,$4,$5,$6,$7)`, + [normalizedMessage, error.stack, error.source, error.url, error.userAgent, error.sessionId, error.userId || null] + ); + } +} + +module.exports = function mountClientErrorRoutes(router) { + router.post('/client-errors', requireAuth, async (req, res) => { + try { + const { errors } = req.body; + if (!Array.isArray(errors) || !errors.length) return res.json({ ok: true }); + const { pool: db } = require('./context'); + const client = await db.connect(); + try { + await initTable(client); + for (const err of errors.slice(0, 10)) { + await storeError(client, { ...err, userId: req.user?.id }); + } + } finally { client.release(); } + res.json({ ok: true }); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + router.get('/client-errors', requireAuth, requireAdmin, async (req, res) => { + try { + const page = Math.max(1, parseInt(req.query.page) || 1); + const { pool: db } = require('./context'); + const client = await db.connect(); + try { + await initTable(client); + await client.query(`DELETE FROM client_errors WHERE last_seen < NOW() - INTERVAL '$1 hours'`, [ERROR_RETENTION_HOURS]); + const { rows } = await client.query( + `SELECT * FROM client_errors ORDER BY last_seen DESC LIMIT $1 OFFSET $2`, + [MAX_ERRORS_PER_PAGE, (page - 1) * MAX_ERRORS_PER_PAGE] + ); + const { rows: countRows } = await client.query('SELECT COUNT(*) as total FROM client_errors'); + res.json({ items: rows, total: parseInt(countRows[0]?.total) || 0, page, pageSize: MAX_ERRORS_PER_PAGE }); + } finally { client.release(); } + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + + router.delete('/client-errors', requireAuth, requireAdmin, async (req, res) => { + try { + const { pool: db } = require('./context'); + await db.query('DELETE FROM client_errors'); + res.json({ ok: true }); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); +}; diff --git a/src/routes/index.js b/src/routes/index.js index 57bd418..8129e36 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -17,7 +17,8 @@ const { registerConversationRoutes } = require('./conversations') const { registerReportRoutes } = require('./reports') const { registerAssetRoutes } = require('./assets') const { registerNotificationRoutes } = require('./notifications') -const { registerDraftRoutes } = require('./drafts') +const { registerDraftRoutes } = require('./drafts'); +const mountClientErrorRoutes = require('./clientErrors') const router = express.Router()