14 KiB
系统操作日志模块文档
概述
系统操作日志模块用于记录后台管理系统的所有操作请求,包括用户操作、API 调用、错误信息等,方便管理员进行系统监控、审计和问题排查。
技术特性
- 自动记录: 通过中间件自动记录所有请求,无需手动调用
- 详细信息: 记录用户信息、请求参数、响应结果、执行时间等
- 敏感信息保护: 自动过滤密码等敏感信息
- 性能优化: 不影响业务响应速度
- 多维度查询: 支持按用户、模块、操作、状态、时间等多维度筛选
- 数据导出: 支持导出日志数据为 Excel 文件
- 批量操作: 支持批量删除和定期清理
数据库表结构
system_logs 表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 ID |
| user_id | bigint | 用户 ID |
| username | varchar(100) | 用户名 |
| module | varchar(50) | 模块名称 |
| action | varchar(100) | 操作名称 |
| method | varchar(10) | 请求方法 (GET/POST/PUT/DELETE) |
| url | text | 请求 URL |
| ip | varchar(45) | 客户端 IP 地址 |
| user_agent | text | 用户代理 |
| params | json | 请求参数 |
| result | text | 响应结果(仅错误时记录) |
| status_code | int | HTTP 状态码 |
| status | varchar(20) | 状态 (success/error) |
| error_message | text | 错误信息 |
| execution_time | int | 执行时间(毫秒) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
核心组件
1. 中间件 (Middleware)
LogRequestMiddleware
位置: app/Http/Middleware/LogRequestMiddleware.php
功能:
- 自动拦截所有经过的请求
- 记录请求和响应信息
- 计算请求执行时间
- 提取用户信息和操作详情
- 过滤敏感参数
- 处理异常情况
使用方式:
// 在路由中应用
Route::middleware(['log.request'])->group(function () {
// 需要记录日志的路由
});
2. 服务层 (Service)
LogService
位置: app/Services/System/LogService.php
主要方法:
create(array $data): 创建日志记录getList(array $params): 获取日志列表(分页)getListQuery(array $params): 获取日志查询构建器getById(int $id): 根据 ID 获取日志详情delete(int $id): 删除单条日志batchDelete(array $ids): 批量删除日志clearLogs(string $days): 清理指定天数前的日志getStatistics(array $params): 获取日志统计信息
3. 控制器 (Controller)
Log Controller
位置: app/Http/Controllers/System/Admin/Log.php
接口列表:
GET /admin/logs: 获取日志列表GET /admin/logs/{id}: 获取日志详情GET /admin/logs/statistics: 获取日志统计POST /admin/logs/export: 导出日志DELETE /admin/logs/{id}: 删除单条日志POST /admin/logs/batch-delete: 批量删除日志POST /admin/logs/clear: 清理历史日志
4. 请求验证 (Request Validation)
LogRequest
位置: app/Http/Requests/LogRequest.php
验证规则:
user_id: 用户 ID(可选)username: 用户名(模糊查询,可选)module: 模块名称(可选)action: 操作名称(可选)status: 状态(success/error,可选)start_date: 开始日期(可选)end_date: 结束日期(可选)ip: IP 地址(可选)page: 页码(默认 1)page_size: 每页数量(默认 20,最大 100)
API 接口文档
1. 获取日志列表
接口: GET /admin/logs
请求参数:
{
"user_id": 1,
"username": "admin",
"module": "users",
"action": "创建 users",
"status": "success",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"ip": "192.168.1.1",
"page": 1,
"page_size": 20
}
响应示例:
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"id": 1,
"user_id": 1,
"username": "admin",
"module": "users",
"action": "创建 users",
"method": "POST",
"url": "http://example.com/admin/users",
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"params": {
"name": "test",
"email": "test@example.com"
},
"result": null,
"status_code": 200,
"status": "success",
"error_message": null,
"execution_time": 125,
"created_at": "2024-01-01 12:00:00",
"user": {
"id": 1,
"name": "管理员",
"username": "admin"
}
}
],
"total": 100,
"page": 1,
"page_size": 20
}
}
2. 获取日志详情
接口: GET /admin/logs/{id}
响应示例:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"user_id": 1,
"username": "admin",
"module": "users",
"action": "创建 users",
"method": "POST",
"url": "http://example.com/admin/users",
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"params": {
"name": "test",
"email": "test@example.com"
},
"result": null,
"status_code": 200,
"status": "success",
"error_message": null,
"execution_time": 125,
"created_at": "2024-01-01 12:00:00",
"user": {
"id": 1,
"name": "管理员",
"username": "admin",
"email": "admin@example.com",
"created_at": "2024-01-01 10:00:00"
}
}
}
3. 获取日志统计
接口: GET /admin/logs/statistics
请求参数:
{
"start_date": "2024-01-01",
"end_date": "2024-12-31"
}
响应示例:
{
"code": 200,
"message": "success",
"data": {
"total": 1000,
"success": 950,
"error": 50
}
}
4. 导出日志
接口: POST /admin/logs/export
请求参数: 与获取日志列表相同的查询参数
响应: Excel 文件下载
文件名格式: 系统操作日志_YYYYMMDDHHmmss.xlsx
包含字段:
- ID
- 用户名
- 模块
- 操作
- 请求方法
- URL
- IP 地址
- 状态码
- 状态
- 错误信息
- 执行时间(ms)
- 创建时间
5. 删除单条日志
接口: DELETE /admin/logs/{id}
响应示例:
{
"code": 200,
"message": "删除成功",
"data": null
}
6. 批量删除日志
接口: POST /admin/logs/batch-delete
请求参数:
{
"ids": [1, 2, 3, 4, 5]
}
响应示例:
{
"code": 200,
"message": "批量删除成功",
"data": null
}
7. 清理历史日志
接口: POST /admin/logs/clear
请求参数:
{
"days": 30
}
说明: 清理指定天数前的所有日志记录,默认清理 30 天前的数据。
响应示例:
{
"code": 200,
"message": "清理成功",
"data": null
}
日志记录规则
1. 自动记录的请求
所有经过 log.request 中间件的请求都会被自动记录,包括:
- 用户管理操作
- 角色管理操作
- 权限管理操作
- 部门管理操作
- 系统配置操作
- 其他所有后台管理操作
2. 不记录的请求
- 登录接口 (
POST /admin/auth/login) - 健康检查接口 (
GET /up) - 其他明确排除的路由
3. 敏感信息过滤
以下字段会被自动过滤,记录为 ******:
passwordpassword_confirmationtokensecretkey
4. 错误日志处理
- 成功请求 (HTTP 状态码 < 400):
status=success - 失败请求 (HTTP 状态码 >= 400):
status=error - 错误时记录响应内容和错误消息
- 同时写入 Laravel 日志文件 (
storage/logs/laravel.log)
模块和操作名称解析
模块名称
从 URL 路径中解析,例如:
/admin/users→ 模块:users/admin/roles→ 模块:roles/admin/configs→ 模块:configs
操作名称
根据 HTTP 方法和资源名称生成:
GET /admin/users→ 操作:查询 usersPOST /admin/users→ 操作:创建 usersPUT /admin/users/1→ 操作:更新 usersDELETE /admin/users/1→ 操作:删除 users
性能优化建议
1. 定期清理日志
建议使用 Laravel 任务调度器定期清理历史日志:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// 每天凌晨 2 点清理 90 天前的日志
$schedule->call(function () {
app(LogService::class)->clearLogs(90);
})->dailyAt('02:00');
}
2. 数据库索引
确保以下字段有索引:
user_idusernamemodulestatuscreated_at
3. 分页查询
列表查询必须使用分页,避免一次加载过多数据。
4. 异步记录
日志记录操作应放在请求处理后,不影响响应速度。
前端集成示例
Vue3 + Ant Design Vue
<template>
<a-card title="操作日志">
<!-- 搜索表单 -->
<a-form layout="inline" :model="searchParams">
<a-form-item label="用户名">
<a-input v-model:value="searchParams.username" placeholder="请输入用户名" />
</a-form-item>
<a-form-item label="模块">
<a-input v-model:value="searchParams.module" placeholder="请输入模块名" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchParams.status" placeholder="请选择状态">
<a-select-option value="success">成功</a-select-option>
<a-select-option value="error">失败</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
<a-button @click="handleExport">导出</a-button>
</a-form-item>
</a-form>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="logs"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<template #status="{ record }">
<a-tag :color="record.status === 'success' ? 'green' : 'red'">
{{ record.status === 'success' ? '成功' : '失败' }}
</a-tag>
</template>
<template #action="{ record }">
<a-button type="link" @click="handleView(record)">查看</a-button>
<a-button type="link" danger @click="handleDelete(record.id)">删除</a-button>
</template>
</a-table>
</a-card>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const logs = ref([])
const loading = ref(false)
const searchParams = reactive({
username: '',
module: '',
status: null,
page: 1,
page_size: 20
})
const pagination = reactive({
total: 0,
current: 1,
pageSize: 20
})
const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '用户名', dataIndex: 'username', width: 120 },
{ title: '模块', dataIndex: 'module', width: 100 },
{ title: '操作', dataIndex: 'action', width: 150 },
{ title: '请求方法', dataIndex: 'method', width: 100 },
{ title: 'IP 地址', dataIndex: 'ip', width: 150 },
{ title: '状态', dataIndex: 'status', slots: { customRender: 'status' }, width: 100 },
{ title: '执行时间', dataIndex: 'execution_time', width: 100 },
{ title: '创建时间', dataIndex: 'created_at', width: 180 },
{ title: '操作', slots: { customRender: 'action' }, width: 150, fixed: 'right' }
]
// 获取日志列表
const fetchLogs = async () => {
loading.value = true
try {
const res = await request.get('/admin/logs', { params: searchParams })
logs.value = res.data.list
pagination.total = res.data.total
pagination.current = res.data.page
pagination.pageSize = res.data.page_size
} catch (error) {
message.error('获取日志失败')
} finally {
loading.value = false
}
}
// 查询
const handleSearch = () => {
searchParams.page = 1
fetchLogs()
}
// 重置
const handleReset = () => {
searchParams.username = ''
searchParams.module = ''
searchParams.status = null
searchParams.page = 1
fetchLogs()
}
// 导出
const handleExport = async () => {
try {
const res = await request.post('/admin/logs/export', searchParams, {
responseType: 'blob'
})
const url = window.URL.createObjectURL(new Blob([res]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `操作日志_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
message.success('导出成功')
} catch (error) {
message.error('导出失败')
}
}
// 查看详情
const handleView = (record) => {
// 打开详情对话框
console.log('查看日志', record)
}
// 删除
const handleDelete = async (id) => {
try {
await request.delete(`/admin/logs/${id}`)
message.success('删除成功')
fetchLogs()
} catch (error) {
message.error('删除失败')
}
}
// 表格分页变化
const handleTableChange = (pag) => {
searchParams.page = pag.current
searchParams.page_size = pag.pageSize
fetchLogs()
}
onMounted(() => {
fetchLogs()
})
</script>
注意事项
- 权限控制: 日志管理接口需要相应的权限才能访问
- 数据安全: 敏感信息已自动过滤,但仍需注意日志数据的安全存储
- 性能影响: 虽然日志记录不影响响应速度,但大量日志会增加数据库负载
- 定期备份: 重要日志数据建议定期备份
- 日志分析: 可结合 BI 工具对日志数据进行深度分析
常见问题
Q1: 为什么某些请求没有被记录?
A: 检查路由是否应用了 log.request 中间件,或者在中间件中是否被排除了。
Q2: 日志数据过多怎么办?
A: 使用 clearLogs 方法定期清理历史日志,或设置任务调度器自动清理。
Q3: 如何自定义日志记录规则?
A: 修改 LogRequestMiddleware 中的 parseModule 和 parseAction 方法。
Q4: 日志记录会影响性能吗?
A: 日志记录在请求处理后执行,不影响响应速度。但大量日志会增加数据库写入压力。
Q5: 如何查看完整的请求参数?
A: 在日志详情接口中,params 字段包含了完整的请求参数(敏感信息已过滤)。
更新日志
v1.0.0 (2024-01-01)
- 初始版本
- 实现基础日志记录功能
- 支持多维度查询和筛选
- 支持数据导出
- 支持批量删除和清理