feat: 管理后台日志查询系统

后端:
- 新增 /api/logs 路由,支持日志文件列表、统计、查询
- 按日期/级别/关键词筛选日志

前端:
- 新增日志管理页面,展示今日统计和7天趋势
- 支持按级别筛选和关键词搜索
- 管理后台添加「系统日志」入口
This commit is contained in:
2026-06-08 14:33:09 +08:00
parent 0de4c9832c
commit cc09a177ef
6 changed files with 669 additions and 0 deletions

145
server/src/routes/logs.ts Normal file
View File

@@ -0,0 +1,145 @@
import { Router } from 'express'
import fs from 'fs'
import path from 'path'
import { requireAdmin } from '../middleware/requireAdmin'
const router = Router()
const LOG_DIR = path.resolve(process.env.LOG_DIR || './logs')
/** 获取日志列表最近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 today = new Date()
const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
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: '获取日志统计失败' })
}
})
/** 查询指定日期的日志 */
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())
// 按级别过滤
if (level && level !== 'all') {
lines = lines.filter(l => l.includes(` ${level.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