feat(admin): 添加系统日志功能
## 新功能 1. **系统日志数据库** - 新增 system_logs 表 - 支持日志级别:debug/info/warn/error - 支持日志分类:auth/user/file/share/system/security - 记录用户ID、用户名、IP地址、User-Agent 2. **日志记录** - 用户注册成功/失败 - 用户登录成功/失败(密码错误) - 系统操作(日志清理等) 3. **管理员API** - GET /api/admin/logs - 查询日志(支持分页和筛选) - GET /api/admin/logs/stats - 获取日志统计 - POST /api/admin/logs/cleanup - 清理旧日志 4. **前端界面** - 日志列表展示(时间、级别、分类、内容、用户、IP) - 筛选功能(级别、分类、关键词搜索) - 分页导航 - 清理旧日志功能 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,32 @@ function initDatabase() {
|
|||||||
console.error('数据库迁移(密码重置Token)失败:', error);
|
console.error('数据库迁移(密码重置Token)失败:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统日志表
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS system_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
level TEXT NOT NULL DEFAULT 'info',
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
username TEXT,
|
||||||
|
ip_address TEXT,
|
||||||
|
user_agent TEXT,
|
||||||
|
details TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 日志表索引
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_logs_created_at ON system_logs(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_logs_category ON system_logs(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_logs_level ON system_logs(level);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_logs_user_id ON system_logs(user_id);
|
||||||
|
`);
|
||||||
|
|
||||||
console.log('数据库初始化完成');
|
console.log('数据库初始化完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,6 +636,141 @@ function migrateToV2() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统日志操作
|
||||||
|
const SystemLogDB = {
|
||||||
|
// 日志级别常量
|
||||||
|
LEVELS: {
|
||||||
|
DEBUG: 'debug',
|
||||||
|
INFO: 'info',
|
||||||
|
WARN: 'warn',
|
||||||
|
ERROR: 'error'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 日志分类常量
|
||||||
|
CATEGORIES: {
|
||||||
|
AUTH: 'auth', // 认证相关(登录、登出、注册)
|
||||||
|
USER: 'user', // 用户管理(创建、修改、删除、封禁)
|
||||||
|
FILE: 'file', // 文件操作(上传、下载、删除、重命名)
|
||||||
|
SHARE: 'share', // 分享操作(创建、删除、访问)
|
||||||
|
SYSTEM: 'system', // 系统操作(设置修改、服务启动)
|
||||||
|
SECURITY: 'security' // 安全事件(登录失败、暴力破解、异常访问)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 写入日志
|
||||||
|
log({ level = 'info', category, action, message, userId = null, username = null, ipAddress = null, userAgent = null, details = null }) {
|
||||||
|
try {
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT INTO system_logs (level, category, action, message, user_id, username, ip_address, user_agent, details)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
const detailsStr = details ? (typeof details === 'string' ? details : JSON.stringify(details)) : null;
|
||||||
|
|
||||||
|
stmt.run(level, category, action, message, userId, username, ipAddress, userAgent, detailsStr);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('写入日志失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询日志(支持分页和筛选)
|
||||||
|
query({ page = 1, pageSize = 50, level = null, category = null, userId = null, startDate = null, endDate = null, keyword = null }) {
|
||||||
|
let sql = 'SELECT * FROM system_logs WHERE 1=1';
|
||||||
|
let countSql = 'SELECT COUNT(*) as total FROM system_logs WHERE 1=1';
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (level) {
|
||||||
|
sql += ' AND level = ?';
|
||||||
|
countSql += ' AND level = ?';
|
||||||
|
params.push(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
sql += ' AND category = ?';
|
||||||
|
countSql += ' AND category = ?';
|
||||||
|
params.push(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
sql += ' AND user_id = ?';
|
||||||
|
countSql += ' AND user_id = ?';
|
||||||
|
params.push(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
sql += ' AND created_at >= ?';
|
||||||
|
countSql += ' AND created_at >= ?';
|
||||||
|
params.push(startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
sql += ' AND created_at <= ?';
|
||||||
|
countSql += ' AND created_at <= ?';
|
||||||
|
params.push(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword) {
|
||||||
|
sql += ' AND (message LIKE ? OR username LIKE ? OR action LIKE ?)';
|
||||||
|
countSql += ' AND (message LIKE ? OR username LIKE ? OR action LIKE ?)';
|
||||||
|
const kw = `%${keyword}%`;
|
||||||
|
params.push(kw, kw, kw);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
const totalResult = db.prepare(countSql).get(...params);
|
||||||
|
const total = totalResult ? totalResult.total : 0;
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
const logs = db.prepare(sql).all(...params, pageSize, offset);
|
||||||
|
|
||||||
|
return {
|
||||||
|
logs,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages: Math.ceil(total / pageSize)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取最近的日志
|
||||||
|
getRecent(limit = 100) {
|
||||||
|
return db.prepare('SELECT * FROM system_logs ORDER BY created_at DESC LIMIT ?').all(limit);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 按分类统计
|
||||||
|
getStatsByCategory() {
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT category, COUNT(*) as count
|
||||||
|
FROM system_logs
|
||||||
|
GROUP BY category
|
||||||
|
ORDER BY count DESC
|
||||||
|
`).all();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 按日期统计(最近7天)
|
||||||
|
getStatsByDate(days = 7) {
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT DATE(created_at) as date, COUNT(*) as count
|
||||||
|
FROM system_logs
|
||||||
|
WHERE created_at >= datetime('now', '-' || ? || ' days')
|
||||||
|
GROUP BY DATE(created_at)
|
||||||
|
ORDER BY date DESC
|
||||||
|
`).all(days);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清理旧日志(保留指定天数)
|
||||||
|
cleanup(keepDays = 90) {
|
||||||
|
const result = db.prepare(`
|
||||||
|
DELETE FROM system_logs
|
||||||
|
WHERE created_at < datetime('now', '-' || ? || ' days')
|
||||||
|
`).run(keepDays);
|
||||||
|
|
||||||
|
return result.changes;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化数据库
|
// 初始化数据库
|
||||||
initDatabase();
|
initDatabase();
|
||||||
createDefaultAdmin();
|
createDefaultAdmin();
|
||||||
@@ -622,5 +783,6 @@ module.exports = {
|
|||||||
ShareDB,
|
ShareDB,
|
||||||
SettingsDB,
|
SettingsDB,
|
||||||
VerificationDB,
|
VerificationDB,
|
||||||
PasswordResetTokenDB
|
PasswordResetTokenDB,
|
||||||
|
SystemLogDB
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const util = require('util');
|
|||||||
const execAsync = util.promisify(exec);
|
const execAsync = util.promisify(exec);
|
||||||
const execFileAsync = util.promisify(execFile);
|
const execFileAsync = util.promisify(execFile);
|
||||||
|
|
||||||
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB } = require('./database');
|
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB } = require('./database');
|
||||||
const { generateToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth');
|
const { generateToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -359,6 +359,96 @@ function getProtocol(req) {
|
|||||||
return req.protocol || (req.secure ? 'https' : 'http');
|
return req.protocol || (req.secure ? 'https' : 'http');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 系统日志工具函数 =====
|
||||||
|
|
||||||
|
// 从请求中提取日志信息
|
||||||
|
function getLogInfoFromReq(req) {
|
||||||
|
return {
|
||||||
|
ipAddress: req.ip || req.socket?.remoteAddress || 'unknown',
|
||||||
|
userAgent: req.get('User-Agent') || 'unknown',
|
||||||
|
userId: req.user?.id || null,
|
||||||
|
username: req.user?.username || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录认证日志
|
||||||
|
function logAuth(req, action, message, details = null, level = 'info') {
|
||||||
|
const info = getLogInfoFromReq(req);
|
||||||
|
SystemLogDB.log({
|
||||||
|
level,
|
||||||
|
category: 'auth',
|
||||||
|
action,
|
||||||
|
message,
|
||||||
|
...info,
|
||||||
|
details
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录用户管理日志
|
||||||
|
function logUser(req, action, message, details = null, level = 'info') {
|
||||||
|
const info = getLogInfoFromReq(req);
|
||||||
|
SystemLogDB.log({
|
||||||
|
level,
|
||||||
|
category: 'user',
|
||||||
|
action,
|
||||||
|
message,
|
||||||
|
...info,
|
||||||
|
details
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录文件操作日志
|
||||||
|
function logFile(req, action, message, details = null, level = 'info') {
|
||||||
|
const info = getLogInfoFromReq(req);
|
||||||
|
SystemLogDB.log({
|
||||||
|
level,
|
||||||
|
category: 'file',
|
||||||
|
action,
|
||||||
|
message,
|
||||||
|
...info,
|
||||||
|
details
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录分享操作日志
|
||||||
|
function logShare(req, action, message, details = null, level = 'info') {
|
||||||
|
const info = getLogInfoFromReq(req);
|
||||||
|
SystemLogDB.log({
|
||||||
|
level,
|
||||||
|
category: 'share',
|
||||||
|
action,
|
||||||
|
message,
|
||||||
|
...info,
|
||||||
|
details
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录系统操作日志
|
||||||
|
function logSystem(req, action, message, details = null, level = 'info') {
|
||||||
|
const info = req ? getLogInfoFromReq(req) : {};
|
||||||
|
SystemLogDB.log({
|
||||||
|
level,
|
||||||
|
category: 'system',
|
||||||
|
action,
|
||||||
|
message,
|
||||||
|
...info,
|
||||||
|
details
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录安全事件日志
|
||||||
|
function logSecurity(req, action, message, details = null, level = 'warn') {
|
||||||
|
const info = getLogInfoFromReq(req);
|
||||||
|
SystemLogDB.log({
|
||||||
|
level,
|
||||||
|
category: 'security',
|
||||||
|
action,
|
||||||
|
message,
|
||||||
|
...info,
|
||||||
|
details
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 文件上传配置(临时存储)
|
// 文件上传配置(临时存储)
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
dest: path.join(__dirname, 'uploads'),
|
dest: path.join(__dirname, 'uploads'),
|
||||||
@@ -1148,6 +1238,9 @@ app.post('/api/register',
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录注册日志
|
||||||
|
logAuth(req, 'register', `新用户注册: ${username}`, { userId, email });
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '注册成功,请查收邮箱完成验证',
|
message: '注册成功,请查收邮箱完成验证',
|
||||||
@@ -1155,6 +1248,7 @@ app.post('/api/register',
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('注册失败:', error);
|
console.error('注册失败:', error);
|
||||||
|
logAuth(req, 'register_failed', `用户注册失败: ${req.body.username || 'unknown'}`, { error: error.message }, 'error');
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '注册失败: ' + error.message
|
message: '注册失败: ' + error.message
|
||||||
@@ -1448,6 +1542,9 @@ app.post('/api/login',
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!UserDB.verifyPassword(password, user.password)) {
|
if (!UserDB.verifyPassword(password, user.password)) {
|
||||||
|
// 记录登录失败安全日志
|
||||||
|
logSecurity(req, 'login_failed', `登录失败(密码错误): ${username}`, { userId: user.id });
|
||||||
|
|
||||||
// 记录失败尝试
|
// 记录失败尝试
|
||||||
if (req.rateLimitKeys) {
|
if (req.rateLimitKeys) {
|
||||||
const result = loginLimiter.recordFailure(req.rateLimitKeys.ipKey);
|
const result = loginLimiter.recordFailure(req.rateLimitKeys.ipKey);
|
||||||
@@ -1487,6 +1584,9 @@ app.post('/api/login',
|
|||||||
path: '/' // 限制Cookie作用域
|
path: '/' // 限制Cookie作用域
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 记录登录成功日志
|
||||||
|
logAuth(req, 'login', `用户登录成功: ${user.username}`, { userId: user.id, isAdmin: user.is_admin });
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: '登录成功',
|
message: '登录成功',
|
||||||
@@ -1506,6 +1606,7 @@ app.post('/api/login',
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('登录失败:', error);
|
||||||
|
logAuth(req, 'login_error', `登录异常: ${req.body.username || 'unknown'}`, { error: error.message }, 'error');
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '登录失败: ' + error.message
|
message: '登录失败: ' + error.message
|
||||||
@@ -3720,6 +3821,91 @@ app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取系统日志
|
||||||
|
app.get('/api/admin/logs', authMiddleware, adminMiddleware, (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
pageSize = 50,
|
||||||
|
level,
|
||||||
|
category,
|
||||||
|
userId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
keyword
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
const result = SystemLogDB.query({
|
||||||
|
page: parseInt(page),
|
||||||
|
pageSize: Math.min(parseInt(pageSize) || 50, 200), // 限制最大每页200条
|
||||||
|
level: level || null,
|
||||||
|
category: category || null,
|
||||||
|
userId: userId ? parseInt(userId) : null,
|
||||||
|
startDate: startDate || null,
|
||||||
|
endDate: endDate || null,
|
||||||
|
keyword: keyword || null
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
...result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取系统日志失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取系统日志失败: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取日志统计
|
||||||
|
app.get('/api/admin/logs/stats', authMiddleware, adminMiddleware, (req, res) => {
|
||||||
|
try {
|
||||||
|
const categoryStats = SystemLogDB.getStatsByCategory();
|
||||||
|
const dateStats = SystemLogDB.getStatsByDate(7);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
stats: {
|
||||||
|
byCategory: categoryStats,
|
||||||
|
byDate: dateStats
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取日志统计失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取日志统计失败: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理旧日志
|
||||||
|
app.post('/api/admin/logs/cleanup', authMiddleware, adminMiddleware, (req, res) => {
|
||||||
|
try {
|
||||||
|
const { keepDays = 90 } = req.body;
|
||||||
|
const days = Math.max(7, Math.min(parseInt(keepDays) || 90, 365)); // 最少保留7天,最多365天
|
||||||
|
|
||||||
|
const deletedCount = SystemLogDB.cleanup(days);
|
||||||
|
|
||||||
|
// 记录清理操作
|
||||||
|
logSystem(req, 'logs_cleanup', `管理员清理了 ${deletedCount} 条日志(保留 ${days} 天)`, { deletedCount, keepDays: days });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `已清理 ${deletedCount} 条日志`,
|
||||||
|
deletedCount
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清理日志失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '清理日志失败: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 封禁/解封用户
|
// 封禁/解封用户
|
||||||
app.post('/api/admin/users/:id/ban', authMiddleware, adminMiddleware, (req, res) => {
|
app.post('/api/admin/users/:id/ban', authMiddleware, adminMiddleware, (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1874,6 +1874,118 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统日志 -->
|
||||||
|
<div class="card" style="margin-bottom: 30px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||||
|
<h3 style="margin: 0;">
|
||||||
|
<i class="fas fa-clipboard-list"></i> 系统日志
|
||||||
|
</h3>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<button class="btn btn-secondary" @click="cleanupLogs" title="清理90天前的日志">
|
||||||
|
<i class="fas fa-trash"></i> 清理旧日志
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" @click="loadSystemLogs(1)" :disabled="systemLogs.loading">
|
||||||
|
<i class="fas" :class="systemLogs.loading ? 'fa-spinner fa-spin' : 'fa-sync'"></i>
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选器 -->
|
||||||
|
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<label style="font-size: 13px; color: #666;">级别:</label>
|
||||||
|
<select v-model="systemLogs.filters.level" @change="filterLogs" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<option value="info">信息</option>
|
||||||
|
<option value="warn">警告</option>
|
||||||
|
<option value="error">错误</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<label style="font-size: 13px; color: #666;">分类:</label>
|
||||||
|
<select v-model="systemLogs.filters.category" @change="filterLogs" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<option value="auth">认证</option>
|
||||||
|
<option value="user">用户</option>
|
||||||
|
<option value="file">文件</option>
|
||||||
|
<option value="share">分享</option>
|
||||||
|
<option value="system">系统</option>
|
||||||
|
<option value="security">安全</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 200px;">
|
||||||
|
<label style="font-size: 13px; color: #666;">搜索:</label>
|
||||||
|
<input type="text" v-model="systemLogs.filters.keyword" @keyup.enter="filterLogs"
|
||||||
|
placeholder="搜索日志内容..." style="flex: 1; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" @click="clearLogFilters" style="padding: 6px 12px;">
|
||||||
|
<i class="fas fa-times"></i> 清除筛选
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志统计 -->
|
||||||
|
<div v-if="systemLogs.total > 0" style="margin-bottom: 15px; font-size: 13px; color: #666;">
|
||||||
|
共 {{ systemLogs.total }} 条日志,第 {{ systemLogs.page }}/{{ systemLogs.totalPages }} 页
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志列表 -->
|
||||||
|
<div v-if="systemLogs.logs.length > 0" style="max-height: 500px; overflow-y: auto;">
|
||||||
|
<div v-for="log in systemLogs.logs" :key="log.id"
|
||||||
|
style="display: flex; gap: 12px; padding: 12px; border-bottom: 1px solid #eee; align-items: flex-start;">
|
||||||
|
<!-- 时间 -->
|
||||||
|
<div style="width: 140px; flex-shrink: 0; font-size: 12px; color: #888;">
|
||||||
|
{{ formatLogTime(log.created_at) }}
|
||||||
|
</div>
|
||||||
|
<!-- 级别标签 -->
|
||||||
|
<div style="width: 50px; flex-shrink: 0;">
|
||||||
|
<span :style="getLogLevelColor(log.level)" style="padding: 2px 8px; border-radius: 4px; font-size: 11px;">
|
||||||
|
{{ getLogLevelText(log.level) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 分类图标 -->
|
||||||
|
<div style="width: 70px; flex-shrink: 0; display: flex; align-items: center; gap: 4px; font-size: 12px; color: #666;">
|
||||||
|
<i class="fas" :class="getLogCategoryIcon(log.category)"></i>
|
||||||
|
{{ getLogCategoryText(log.category) }}
|
||||||
|
</div>
|
||||||
|
<!-- 内容 -->
|
||||||
|
<div style="flex: 1; min-width: 0;">
|
||||||
|
<div style="font-weight: 500; margin-bottom: 4px;">{{ log.action }}</div>
|
||||||
|
<div style="font-size: 13px; color: #555;">{{ log.message }}</div>
|
||||||
|
<div v-if="log.username || log.ip_address" style="font-size: 11px; color: #888; margin-top: 4px;">
|
||||||
|
<span v-if="log.username"><i class="fas fa-user"></i> {{ log.username }}</span>
|
||||||
|
<span v-if="log.ip_address" style="margin-left: 10px;"><i class="fas fa-globe"></i> {{ log.ip_address }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else-if="!systemLogs.loading" style="text-align: center; padding: 40px; color: #888;">
|
||||||
|
<i class="fas fa-clipboard" style="font-size: 48px; margin-bottom: 15px;"></i>
|
||||||
|
<p>暂无日志记录</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="systemLogs.loading" style="text-align: center; padding: 40px; color: #888;">
|
||||||
|
<i class="fas fa-spinner fa-spin" style="font-size: 24px;"></i>
|
||||||
|
<p>加载中...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div v-if="systemLogs.totalPages > 1" style="display: flex; justify-content: center; gap: 8px; margin-top: 15px;">
|
||||||
|
<button class="btn btn-secondary" @click="loadSystemLogs(systemLogs.page - 1)" :disabled="systemLogs.page <= 1" style="padding: 6px 12px;">
|
||||||
|
<i class="fas fa-chevron-left"></i> 上一页
|
||||||
|
</button>
|
||||||
|
<span style="display: flex; align-items: center; padding: 0 15px; color: #666;">
|
||||||
|
{{ systemLogs.page }} / {{ systemLogs.totalPages }}
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-secondary" @click="loadSystemLogs(systemLogs.page + 1)" :disabled="systemLogs.page >= systemLogs.totalPages" style="padding: 6px 12px;">
|
||||||
|
下一页 <i class="fas fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 style="margin-bottom: 20px;">用户管理</h3>
|
<h3 style="margin-bottom: 20px;">用户管理</h3>
|
||||||
<div style="overflow-x: auto;">
|
<div style="overflow-x: auto;">
|
||||||
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px;">
|
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px;">
|
||||||
|
|||||||
137
frontend/app.js
137
frontend/app.js
@@ -157,6 +157,21 @@ createApp({
|
|||||||
checks: []
|
checks: []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 系统日志
|
||||||
|
systemLogs: {
|
||||||
|
loading: false,
|
||||||
|
logs: [],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 30,
|
||||||
|
totalPages: 0,
|
||||||
|
filters: {
|
||||||
|
level: '',
|
||||||
|
category: '',
|
||||||
|
keyword: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Toast通知
|
// Toast通知
|
||||||
toasts: [],
|
toasts: [],
|
||||||
toastIdCounter: 0,
|
toastIdCounter: 0,
|
||||||
@@ -2050,11 +2065,12 @@ handleDragLeave(e) {
|
|||||||
this.loadShares();
|
this.loadShares();
|
||||||
break;
|
break;
|
||||||
case 'admin':
|
case 'admin':
|
||||||
// 切换到管理后台时,重新加载用户列表和健康检测
|
// 切换到管理后台时,重新加载用户列表、健康检测和系统日志
|
||||||
if (this.user && this.user.is_admin) {
|
if (this.user && this.user.is_admin) {
|
||||||
this.loadUsers();
|
this.loadUsers();
|
||||||
this.loadServerStorageStats();
|
this.loadServerStorageStats();
|
||||||
this.loadHealthCheck();
|
this.loadHealthCheck();
|
||||||
|
this.loadSystemLogs(1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
@@ -2331,6 +2347,125 @@ handleDragLeave(e) {
|
|||||||
return texts[status] || '未知';
|
return texts[status] || '未知';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ===== 系统日志 =====
|
||||||
|
|
||||||
|
async loadSystemLogs(page = 1) {
|
||||||
|
this.systemLogs.loading = true;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: page,
|
||||||
|
pageSize: this.systemLogs.pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.systemLogs.filters.level) {
|
||||||
|
params.append('level', this.systemLogs.filters.level);
|
||||||
|
}
|
||||||
|
if (this.systemLogs.filters.category) {
|
||||||
|
params.append('category', this.systemLogs.filters.category);
|
||||||
|
}
|
||||||
|
if (this.systemLogs.filters.keyword) {
|
||||||
|
params.append('keyword', this.systemLogs.filters.keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get(`${this.apiBase}/api/admin/logs?${params}`, {
|
||||||
|
headers: { Authorization: `Bearer ${this.token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
this.systemLogs.logs = response.data.logs;
|
||||||
|
this.systemLogs.total = response.data.total;
|
||||||
|
this.systemLogs.page = response.data.page;
|
||||||
|
this.systemLogs.totalPages = response.data.totalPages;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载系统日志失败:', error);
|
||||||
|
this.showToast('error', '错误', '加载系统日志失败');
|
||||||
|
} finally {
|
||||||
|
this.systemLogs.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
filterLogs() {
|
||||||
|
this.loadSystemLogs(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearLogFilters() {
|
||||||
|
this.systemLogs.filters = { level: '', category: '', keyword: '' };
|
||||||
|
this.loadSystemLogs(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
getLogLevelColor(level) {
|
||||||
|
const colors = {
|
||||||
|
debug: 'background: #6c757d; color: white;',
|
||||||
|
info: 'background: #17a2b8; color: white;',
|
||||||
|
warn: 'background: #ffc107; color: black;',
|
||||||
|
error: 'background: #dc3545; color: white;'
|
||||||
|
};
|
||||||
|
return colors[level] || 'background: #6c757d; color: white;';
|
||||||
|
},
|
||||||
|
|
||||||
|
getLogLevelText(level) {
|
||||||
|
const texts = { debug: '调试', info: '信息', warn: '警告', error: '错误' };
|
||||||
|
return texts[level] || level;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLogCategoryText(category) {
|
||||||
|
const texts = {
|
||||||
|
auth: '认证',
|
||||||
|
user: '用户',
|
||||||
|
file: '文件',
|
||||||
|
share: '分享',
|
||||||
|
system: '系统',
|
||||||
|
security: '安全'
|
||||||
|
};
|
||||||
|
return texts[category] || category;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLogCategoryIcon(category) {
|
||||||
|
const icons = {
|
||||||
|
auth: 'fa-key',
|
||||||
|
user: 'fa-user',
|
||||||
|
file: 'fa-file',
|
||||||
|
share: 'fa-share-alt',
|
||||||
|
system: 'fa-cog',
|
||||||
|
security: 'fa-shield-alt'
|
||||||
|
};
|
||||||
|
return icons[category] || 'fa-info';
|
||||||
|
},
|
||||||
|
|
||||||
|
formatLogTime(timestamp) {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async cleanupLogs() {
|
||||||
|
if (!confirm('确定要清理90天前的日志吗?此操作不可恢复。')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.apiBase}/api/admin/logs/cleanup`,
|
||||||
|
{ keepDays: 90 },
|
||||||
|
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
this.showToast('success', '成功', response.data.message);
|
||||||
|
this.loadSystemLogs(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清理日志失败:', error);
|
||||||
|
this.showToast('error', '错误', '清理日志失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ===== 上传工具管理 =====
|
// ===== 上传工具管理 =====
|
||||||
|
|
||||||
// 检测上传工具是否存在
|
// 检测上传工具是否存在
|
||||||
|
|||||||
Reference in New Issue
Block a user