diff --git a/server/src/index.ts b/server/src/index.ts index a50c228..ff26d82 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -159,7 +159,7 @@ app.use('/api/filters', apiLimiter, filterRoutes) app.use('/api/admin', apiLimiter, adminRoutes) app.use('/api/feedback', apiLimiter, feedbackRoutes) app.use('/api/config', apiLimiter, configRoutes) -app.use('/api/track', trackRoutes) +app.use('/api/track', apiLimiter, trackRoutes) app.use('/api/logs', apiLimiter, logsRoutes) app.get('/api/health', async (_req, res) => { diff --git a/server/src/routes/config.ts b/server/src/routes/config.ts index 8ada268..fdb076d 100644 --- a/server/src/routes/config.ts +++ b/server/src/routes/config.ts @@ -19,6 +19,9 @@ router.get('/', async (_req, res) => { } }) +/** 允许修改的配置键白名单 */ +const ALLOWED_KEYS = ['app_name', 'app_version', 'app_slogan', 'app_description'] + /** 更新配置(管理员权限) */ router.put('/', requireAdmin, async (req, res) => { try { @@ -27,8 +30,14 @@ router.put('/', requireAdmin, async (req, res) => { return res.status(400).json({ code: 40001, message: '无效的配置数据' }) } - // 批量更新配置 + // 批量更新配置(只允许白名单内的键) for (const [key, value] of Object.entries(data)) { + if (!ALLOWED_KEYS.includes(key)) { + return res.status(400).json({ code: 40001, message: `不允许修改配置项: ${key}` }) + } + if (typeof value !== 'string' || value.length > 500) { + return res.status(400).json({ code: 40001, message: `配置值无效或过长: ${key}` }) + } await pool.query( 'INSERT INTO sys_config (config_key, config_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE config_value = ?', [key, value, value] diff --git a/server/src/routes/logs.ts b/server/src/routes/logs.ts index e285d6f..5dd7644 100644 --- a/server/src/routes/logs.ts +++ b/server/src/routes/logs.ts @@ -92,7 +92,87 @@ router.get('/stats', requireAdmin, (_req, res) => { } }) -/** 查询指定日期的日志 */ +/** 获取埋点统计(必须在 /:date 之前注册) */ +router.get('/track/stats', requireAdmin, async (_req, res) => { + try { + // 今日统计 + const [todayRows] = await pool.execute( + `SELECT event, COUNT(*) as count FROM track_events + WHERE DATE(created_at) = CURDATE() + GROUP BY event ORDER BY count DESC` + ) + + // 最近7天每日统计 + const [dailyRows] = await pool.execute( + `SELECT DATE(created_at) as date, COUNT(*) as count FROM track_events + WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY DATE(created_at) ORDER BY date DESC` + ) + + // 最近7天事件分布 + const [eventRows] = await pool.execute( + `SELECT event, COUNT(*) as count FROM track_events + WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY event ORDER BY count DESC LIMIT 10` + ) + + res.json({ + code: 0, + data: { + today: todayRows, + daily: dailyRows, + events: eventRows + } + }) + } catch (err) { + console.error('[Track] Stats error:', err) + res.status(500).json({ code: 50000, message: '获取埋点统计失败' }) + } +}) + +/** 查询埋点事件(必须在 /:date 之前注册) */ +router.get('/track/list', requireAdmin, async (req, res) => { + try { + const { event, page = '1', pageSize = '50' } = req.query + const pNum = Math.max(1, parseInt(page as string) || 1) + const pSize = Math.min(200, Math.max(1, parseInt(pageSize as string) || 50)) + const offset = (pNum - 1) * pSize + + let where = '1=1' + const params: any[] = [] + + if (event && typeof event === 'string') { + where += ' AND t.event = ?' + params.push(event) + } + + const [countResult] = await pool.execute( + `SELECT COUNT(*) as total FROM track_events t WHERE ${where}`, + params + ) + const total = (countResult as any[])[0].total + + const [rows] = await pool.execute( + `SELECT t.*, u.nickname + FROM track_events t + LEFT JOIN users u ON t.user_id = u.id + WHERE ${where} + ORDER BY t.created_at DESC + LIMIT ? OFFSET ?`, + [...params, pSize, offset] + ) + + res.json({ + code: 0, + data: { list: rows, total, page: pNum, pageSize: pSize } + }) + } catch (err) { + console.error('[Track] List error:', err) + res.status(500).json({ code: 50000, message: '查询埋点失败' }) + } +}) + +/** 查询指定日期的日志(必须在 /track/* 之后注册) */ router.get('/:date', requireAdmin, (req, res) => { try { const { date } = req.params @@ -144,84 +224,4 @@ router.get('/:date', requireAdmin, (req, res) => { } }) -/** 获取埋点统计 */ -router.get('/track/stats', requireAdmin, async (_req, res) => { - try { - // 今日统计 - const [todayRows] = await pool.execute( - `SELECT event, COUNT(*) as count FROM track_events - WHERE DATE(created_at) = CURDATE() - GROUP BY event ORDER BY count DESC` - ) - - // 最近7天每日统计 - const [dailyRows] = await pool.execute( - `SELECT DATE(created_at) as date, COUNT(*) as count FROM track_events - WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) - GROUP BY DATE(created_at) ORDER BY date DESC` - ) - - // 最近7天事件分布 - const [eventRows] = await pool.execute( - `SELECT event, COUNT(*) as count FROM track_events - WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) - GROUP BY event ORDER BY count DESC LIMIT 10` - ) - - res.json({ - code: 0, - data: { - today: todayRows, - daily: dailyRows, - events: eventRows - } - }) - } catch (err) { - console.error('[Track] Stats error:', err) - res.status(500).json({ code: 50000, message: '获取埋点统计失败' }) - } -}) - -/** 查询埋点事件 */ -router.get('/track/list', requireAdmin, async (req, res) => { - try { - const { event, page = '1', pageSize = '50' } = req.query - const pNum = Math.max(1, parseInt(page as string) || 1) - const pSize = Math.min(200, Math.max(1, parseInt(pageSize as string) || 50)) - const offset = (pNum - 1) * pSize - - let where = '1=1' - const params: any[] = [] - - if (event && typeof event === 'string') { - where += ' AND t.event = ?' - params.push(event) - } - - const [countResult] = await pool.execute( - `SELECT COUNT(*) as total FROM track_events t WHERE ${where}`, - params - ) - const total = (countResult as any[])[0].total - - const [rows] = await pool.execute( - `SELECT t.*, u.nickname - FROM track_events t - LEFT JOIN users u ON t.user_id = u.id - WHERE ${where} - ORDER BY t.created_at DESC - LIMIT ? OFFSET ?`, - [...params, pSize, offset] - ) - - res.json({ - code: 0, - data: { list: rows, total, page: pNum, pageSize: pSize } - }) - } catch (err) { - console.error('[Track] List error:', err) - res.status(500).json({ code: 50000, message: '查询埋点失败' }) - } -}) - export default router diff --git a/server/src/routes/track.ts b/server/src/routes/track.ts index 7ad3eac..1879790 100644 --- a/server/src/routes/track.ts +++ b/server/src/routes/track.ts @@ -13,11 +13,15 @@ router.post('/', async (req, res) => { return res.status(400).json({ code: 40001, message: '无效的埋点数据' }) } - // 批量插入埋点事件 - for (const event of events) { + // 批量插入埋点事件(限制每个事件的字段长度) + for (const event of events.slice(0, 50)) { // 最多 50 个事件 + const eventType = typeof event.event === 'string' ? event.event.slice(0, 50) : 'unknown' + const page = typeof event.page === 'string' ? event.page.slice(0, 200) : '' + const data = event.data && typeof event.data === 'object' ? event.data : {} + await pool.execute( 'INSERT INTO track_events (event, page, data, user_id) VALUES (?, ?, ?, ?)', - [event.event, event.page || '', JSON.stringify(event.data || {}), userId] + [eventType, page, JSON.stringify(data), userId] ) }