import { Router } from 'express' import fs from 'fs' import path from 'path' import pool from '../db/connection' import { requireAdmin } from '../middleware/requireAdmin' const router = Router() const LOG_DIR = path.resolve(process.env.LOG_DIR || './logs') /** 获取今日日期字符串 YYYY-MM-DD(服务器本地时间) */ function getTodayStr(): string { const d = new Date() return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` } /** 获取日志列表(最近7天) */ router.get('/files', requireAdmin, (_req, res) => { try { if (!fs.existsSync(LOG_DIR)) { return res.json({ code: 0, data: [] }) } const files = fs.readdirSync(LOG_DIR) .filter(f => f.endsWith('.log')) .sort() .reverse() .slice(0, 7) // 最多返回7天 const fileList = files.map(f => { const filePath = path.join(LOG_DIR, f) const stat = fs.statSync(filePath) return { name: f, date: f.replace('.log', ''), size: stat.size, modified: stat.mtime.toISOString() } }) res.json({ code: 0, data: fileList }) } catch (err) { console.error('[Logs] List error:', err) res.status(500).json({ code: 50000, message: '获取日志列表失败' }) } }) /** 获取日志统计 */ router.get('/stats', requireAdmin, (_req, res) => { try { if (!fs.existsSync(LOG_DIR)) { return res.json({ code: 0, data: { today: { total: 0, errors: 0, slow: 0 }, files: [] } }) } const dateStr = getTodayStr() const todayFile = path.join(LOG_DIR, `${dateStr}.log`) let todayStats = { total: 0, errors: 0, slow: 0, warns: 0 } if (fs.existsSync(todayFile)) { const content = fs.readFileSync(todayFile, 'utf-8') const lines = content.split('\n').filter(l => l.trim()) todayStats.total = lines.length todayStats.errors = lines.filter(l => l.includes(' ERROR ')).length todayStats.slow = lines.filter(l => l.includes('Slow request')).length todayStats.warns = lines.filter(l => l.includes(' WARN ')).length } // 最近7天统计 const files = fs.readdirSync(LOG_DIR) .filter(f => f.endsWith('.log')) .sort() .reverse() .slice(0, 7) const dailyStats = files.map(f => { const filePath = path.join(LOG_DIR, f) const content = fs.readFileSync(filePath, 'utf-8') const lines = content.split('\n').filter(l => l.trim()) return { date: f.replace('.log', ''), total: lines.length, errors: lines.filter(l => l.includes(' ERROR ')).length, slow: lines.filter(l => l.includes('Slow request')).length } }) res.json({ code: 0, data: { today: todayStats, daily: dailyStats } }) } catch (err) { console.error('[Logs] Stats error:', err) res.status(500).json({ code: 50000, message: '获取日志统计失败' }) } }) /** 获取埋点统计(必须在 /:date 之前注册) */ router.get('/track/stats', requireAdmin, async (_req, res) => { try { // 今日统计 const [todayRows] = await pool.query( `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.query( `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.query( `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.query( `SELECT COUNT(*) as total FROM track_events t WHERE ${where}`, params ) const total = (countResult as any[])[0].total const [rows] = await pool.query( `SELECT t.*, u.nickname FROM track_events t LEFT JOIN users u ON t.user_id = u.id AND u.deleted_at IS NULL 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 const { level, keyword, page = '1', pageSize = '100' } = req.query // 日期格式校验 if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { return res.status(400).json({ code: 40001, message: '日期格式无效' }) } const logFile = path.join(LOG_DIR, `${date}.log`) if (!fs.existsSync(logFile)) { return res.json({ code: 0, data: { lines: [], total: 0 } }) } const content = fs.readFileSync(logFile, 'utf-8') let lines = content.split('\n').filter(l => l.trim()) // 按级别过滤 const levelStr = typeof level === 'string' ? level : '' if (levelStr && levelStr !== 'all') { lines = lines.filter(l => l.includes(` ${levelStr.toUpperCase()} `)) } // 按关键词过滤 if (keyword && typeof keyword === 'string') { const kw = keyword.toLowerCase() lines = lines.filter(l => l.toLowerCase().includes(kw)) } const total = lines.length const pNum = Math.max(1, parseInt(page as string) || 1) const pSize = Math.min(500, Math.max(1, parseInt(pageSize as string) || 100)) const offset = (pNum - 1) * pSize const paged = lines.slice(offset, offset + pSize) res.json({ code: 0, data: { lines: paged, total, page: pNum, pageSize: pSize } }) } catch (err) { console.error('[Logs] Query error:', err) res.status(500).json({ code: 50000, message: '查询日志失败' }) } }) export default router