feat: 管理后台日志查询系统
后端: - 新增 /api/logs 路由,支持日志文件列表、统计、查询 - 按日期/级别/关键词筛选日志 前端: - 新增日志管理页面,展示今日统计和7天趋势 - 支持按级别筛选和关键词搜索 - 管理后台添加「系统日志」入口
This commit is contained in:
145
server/src/routes/logs.ts
Normal file
145
server/src/routes/logs.ts
Normal 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
|
||||
Reference in New Issue
Block a user