609 lines
14 KiB
Markdown
609 lines
14 KiB
Markdown
# 系统操作日志模块文档
|
||
|
||
## 概述
|
||
|
||
系统操作日志模块用于记录后台管理系统的所有操作请求,包括用户操作、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`
|
||
|
||
功能:
|
||
- 自动拦截所有经过的请求
|
||
- 记录请求和响应信息
|
||
- 计算请求执行时间
|
||
- 提取用户信息和操作详情
|
||
- 过滤敏感参数
|
||
- 处理异常情况
|
||
|
||
使用方式:
|
||
```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`
|
||
|
||
**请求参数**:
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
```
|
||
|
||
**响应示例**:
|
||
```json
|
||
{
|
||
"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}`
|
||
|
||
**响应示例**:
|
||
```json
|
||
{
|
||
"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`
|
||
|
||
**请求参数**:
|
||
```json
|
||
{
|
||
"start_date": "2024-01-01",
|
||
"end_date": "2024-12-31"
|
||
}
|
||
```
|
||
|
||
**响应示例**:
|
||
```json
|
||
{
|
||
"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}`
|
||
|
||
**响应示例**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "删除成功",
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
### 6. 批量删除日志
|
||
|
||
**接口**: `POST /admin/logs/batch-delete`
|
||
|
||
**请求参数**:
|
||
```json
|
||
{
|
||
"ids": [1, 2, 3, 4, 5]
|
||
}
|
||
```
|
||
|
||
**响应示例**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "批量删除成功",
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
### 7. 清理历史日志
|
||
|
||
**接口**: `POST /admin/logs/clear`
|
||
|
||
**请求参数**:
|
||
```json
|
||
{
|
||
"days": 30
|
||
}
|
||
```
|
||
|
||
**说明**: 清理指定天数前的所有日志记录,默认清理 30 天前的数据。
|
||
|
||
**响应示例**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "清理成功",
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
## 日志记录规则
|
||
|
||
### 1. 自动记录的请求
|
||
|
||
所有经过 `log.request` 中间件的请求都会被自动记录,包括:
|
||
- 用户管理操作
|
||
- 角色管理操作
|
||
- 权限管理操作
|
||
- 部门管理操作
|
||
- 系统配置操作
|
||
- 其他所有后台管理操作
|
||
|
||
### 2. 不记录的请求
|
||
|
||
- 登录接口 (`POST /admin/auth/login`)
|
||
- 健康检查接口 (`GET /up`)
|
||
- 其他明确排除的路由
|
||
|
||
### 3. 敏感信息过滤
|
||
|
||
以下字段会被自动过滤,记录为 `******`:
|
||
- `password`
|
||
- `password_confirmation`
|
||
- `token`
|
||
- `secret`
|
||
- `key`
|
||
|
||
### 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` → 操作: `查询 users`
|
||
- `POST /admin/users` → 操作: `创建 users`
|
||
- `PUT /admin/users/1` → 操作: `更新 users`
|
||
- `DELETE /admin/users/1` → 操作: `删除 users`
|
||
|
||
## 性能优化建议
|
||
|
||
### 1. 定期清理日志
|
||
|
||
建议使用 Laravel 任务调度器定期清理历史日志:
|
||
|
||
```php
|
||
// app/Console/Kernel.php
|
||
|
||
protected function schedule(Schedule $schedule)
|
||
{
|
||
// 每天凌晨 2 点清理 90 天前的日志
|
||
$schedule->call(function () {
|
||
app(LogService::class)->clearLogs(90);
|
||
})->dailyAt('02:00');
|
||
}
|
||
```
|
||
|
||
### 2. 数据库索引
|
||
|
||
确保以下字段有索引:
|
||
- `user_id`
|
||
- `username`
|
||
- `module`
|
||
- `status`
|
||
- `created_at`
|
||
|
||
### 3. 分页查询
|
||
|
||
列表查询必须使用分页,避免一次加载过多数据。
|
||
|
||
### 4. 异步记录
|
||
|
||
日志记录操作应放在请求处理后,不影响响应速度。
|
||
|
||
## 前端集成示例
|
||
|
||
### Vue3 + Ant Design Vue
|
||
|
||
```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>
|
||
```
|
||
|
||
## 注意事项
|
||
|
||
1. **权限控制**: 日志管理接口需要相应的权限才能访问
|
||
2. **数据安全**: 敏感信息已自动过滤,但仍需注意日志数据的安全存储
|
||
3. **性能影响**: 虽然日志记录不影响响应速度,但大量日志会增加数据库负载
|
||
4. **定期备份**: 重要日志数据建议定期备份
|
||
5. **日志分析**: 可结合 BI 工具对日志数据进行深度分析
|
||
|
||
## 常见问题
|
||
|
||
### Q1: 为什么某些请求没有被记录?
|
||
|
||
A: 检查路由是否应用了 `log.request` 中间件,或者在中间件中是否被排除了。
|
||
|
||
### Q2: 日志数据过多怎么办?
|
||
|
||
A: 使用 `clearLogs` 方法定期清理历史日志,或设置任务调度器自动清理。
|
||
|
||
### Q3: 如何自定义日志记录规则?
|
||
|
||
A: 修改 `LogRequestMiddleware` 中的 `parseModule` 和 `parseAction` 方法。
|
||
|
||
### Q4: 日志记录会影响性能吗?
|
||
|
||
A: 日志记录在请求处理后执行,不影响响应速度。但大量日志会增加数据库写入压力。
|
||
|
||
### Q5: 如何查看完整的请求参数?
|
||
|
||
A: 在日志详情接口中,`params` 字段包含了完整的请求参数(敏感信息已过滤)。
|
||
|
||
## 更新日志
|
||
|
||
### v1.0.0 (2024-01-01)
|
||
- 初始版本
|
||
- 实现基础日志记录功能
|
||
- 支持多维度查询和筛选
|
||
- 支持数据导出
|
||
- 支持批量删除和清理
|