格式化代码,websocket功能完善
This commit is contained in:
@@ -1,415 +0,0 @@
|
||||
# 字典缓存更新机制
|
||||
|
||||
## 概述
|
||||
|
||||
本文档说明前后端字典缓存的更新逻辑,确保在字典分类和字典项的增删改等操作后,前端字典缓存能够自动更新。
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 1. 后端实现
|
||||
|
||||
#### 1.1 DictionaryService 更新
|
||||
|
||||
在 `app/Services/System/DictionaryService.php` 中添加了 WebSocket 通知功能:
|
||||
|
||||
**依赖注入:**
|
||||
```php
|
||||
protected $webSocketService;
|
||||
|
||||
public function __construct(WebSocketService $webSocketService)
|
||||
{
|
||||
$this->webSocketService = $webSocketService;
|
||||
}
|
||||
```
|
||||
|
||||
**通知方法:**
|
||||
|
||||
1. **字典分类更新通知** (`notifyDictionaryUpdate`)
|
||||
- 触发时机:创建、更新、删除、批量删除、批量更新状态
|
||||
- 消息类型:`dictionary_update`
|
||||
|
||||
2. **字典项更新通知** (`notifyDictionaryItemUpdate`)
|
||||
- 触发时机:创建、更新、删除、批量删除、批量更新状态
|
||||
- 消息类型:`dictionary_item_update`
|
||||
|
||||
**修改的方法列表:**
|
||||
|
||||
- `create()` - 创建字典分类后发送通知
|
||||
- `update()` - 更新字典分类后发送通知
|
||||
- `delete()` - 删除字典分类后发送通知
|
||||
- `batchDelete()` - 批量删除字典分类后发送通知
|
||||
- `batchUpdateStatus()` - 批量更新状态后发送通知
|
||||
- `createItem()` - 创建字典项后发送通知
|
||||
- `updateItem()` - 更新字典项后发送通知
|
||||
- `deleteItem()` - 删除字典项后发送通知
|
||||
- `batchDeleteItems()` - 批量删除字典项后发送通知
|
||||
- `batchUpdateItemsStatus()` - 批量更新字典项状态后发送通知
|
||||
|
||||
#### 1.2 WebSocket 消息格式
|
||||
|
||||
**字典分类更新消息:**
|
||||
```json
|
||||
{
|
||||
"type": "dictionary_update",
|
||||
"data": {
|
||||
"action": "create|update|delete|batch_delete|batch_update_status",
|
||||
"resource_type": "dictionary",
|
||||
"data": {
|
||||
// 字典分类数据
|
||||
},
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字典项更新消息:**
|
||||
```json
|
||||
{
|
||||
"type": "dictionary_item_update",
|
||||
"data": {
|
||||
"action": "create|update|delete|batch_delete|batch_update_status",
|
||||
"resource_type": "dictionary_item",
|
||||
"data": {
|
||||
// 字典项数据
|
||||
},
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 前端实现
|
||||
|
||||
#### 2.1 WebSocket Composable
|
||||
|
||||
创建了 `resources/admin/src/composables/useWebSocket.js` 来处理 WebSocket 连接和消息监听:
|
||||
|
||||
**主要功能:**
|
||||
|
||||
1. **初始化 WebSocket 连接**
|
||||
- 检查用户登录状态
|
||||
- 验证用户信息完整性
|
||||
- 建立连接并注册消息处理器
|
||||
|
||||
2. **消息处理器**
|
||||
- `handleDictionaryUpdate` - 处理字典分类更新
|
||||
- `handleDictionaryItemUpdate` - 处理字典项更新
|
||||
|
||||
3. **缓存刷新**
|
||||
- 接收到更新通知后,自动刷新字典缓存
|
||||
- 显示成功提示消息
|
||||
|
||||
#### 2.2 App.vue 集成
|
||||
|
||||
在 `resources/admin/src/App.vue` 中集成了 WebSocket:
|
||||
|
||||
**生命周期钩子:**
|
||||
|
||||
```javascript
|
||||
onMounted(async () => {
|
||||
// ... 其他初始化代码
|
||||
|
||||
// 初始化 WebSocket 连接
|
||||
if (userStore.isLoggedIn()) {
|
||||
initWebSocket()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 关闭 WebSocket 连接
|
||||
closeWebSocket()
|
||||
})
|
||||
```
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 完整流程图
|
||||
|
||||
```
|
||||
用户操作(增删改字典)
|
||||
↓
|
||||
后端 Controller 调用 Service
|
||||
↓
|
||||
Service 执行数据库操作
|
||||
↓
|
||||
Service 清理后端缓存(Redis)
|
||||
↓
|
||||
Service 发送 WebSocket 广播通知
|
||||
↓
|
||||
WebSocket 推送消息到所有在线客户端
|
||||
↓
|
||||
前端接收 WebSocket 消息
|
||||
↓
|
||||
触发相应的消息处理器
|
||||
↓
|
||||
刷新前端字典缓存
|
||||
↓
|
||||
显示成功提示
|
||||
```
|
||||
|
||||
### 详细步骤
|
||||
|
||||
1. **用户操作**
|
||||
- 管理员在后台管理界面进行字典分类或字典项的增删改操作
|
||||
- 例如:创建新字典分类、修改字典项、批量删除等
|
||||
|
||||
2. **后端处理**
|
||||
- 接收请求并验证数据
|
||||
- 执行数据库操作(INSERT/UPDATE/DELETE)
|
||||
- 清理 Redis 缓存(`DictionaryService::clearCache()`)
|
||||
- 通过 WebSocket 广播更新通知
|
||||
|
||||
3. **WebSocket 通知**
|
||||
- 服务端向所有连接的 WebSocket 客户端广播消息
|
||||
- 消息包含操作类型、资源类型和更新的数据
|
||||
|
||||
4. **前端接收**
|
||||
- App.vue 在 onMounted 时初始化 WebSocket 连接
|
||||
- 注册消息处理器监听 `dictionary_update` 和 `dictionary_item_update` 事件
|
||||
- 接收到消息后调用对应的处理器
|
||||
|
||||
5. **缓存刷新**
|
||||
- 处理器调用 `dictionaryStore.refresh(true)` 强制刷新缓存
|
||||
- 从后端 API 重新加载所有字典数据
|
||||
- 更新 Pinia store 中的字典数据
|
||||
- 持久化到本地存储
|
||||
|
||||
6. **用户反馈**
|
||||
- 显示 "字典数据已更新" 的成功提示
|
||||
- 页面上的字典数据自动更新,无需手动刷新
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1:创建新字典分类
|
||||
|
||||
```php
|
||||
// 后端代码
|
||||
$dictionary = $dictionaryService->create([
|
||||
'name' => '订单状态',
|
||||
'code' => 'order_status',
|
||||
'description' => '订单状态字典',
|
||||
'value_type' => 'string',
|
||||
'sort' => 1,
|
||||
'status' => true
|
||||
]);
|
||||
|
||||
// 自动触发:
|
||||
// 1. 清理 Redis 缓存
|
||||
// 2. 广播 WebSocket 消息
|
||||
```
|
||||
|
||||
前端自动刷新缓存并显示提示。
|
||||
|
||||
### 示例 2:更新字典项
|
||||
|
||||
```php
|
||||
// 后端代码
|
||||
$item = $dictionaryService->updateItem(1, [
|
||||
'label' => '已付款',
|
||||
'value' => 'paid',
|
||||
'sort' => 2
|
||||
]);
|
||||
|
||||
// 自动触发:
|
||||
// 1. 清理对应字典的 Redis 缓存
|
||||
// 2. 广播 WebSocket 消息
|
||||
```
|
||||
|
||||
前端自动刷新缓存并显示提示。
|
||||
|
||||
### 示例 3:批量操作
|
||||
|
||||
```php
|
||||
// 后端代码
|
||||
$dictionaryService->batchUpdateStatus([1, 2, 3], false);
|
||||
|
||||
// 自动触发:
|
||||
// 1. 清理所有相关字典的 Redis 缓存
|
||||
// 2. 广播 WebSocket 消息(包含批量更新的 ID)
|
||||
```
|
||||
|
||||
前端自动刷新缓存并显示提示。
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. WebSocket 连接
|
||||
|
||||
- WebSocket 仅在用户登录后建立连接
|
||||
- 连接失败会自动重试(最多 5 次)
|
||||
- 页面卸载时会自动关闭连接
|
||||
|
||||
### 2. 缓存一致性
|
||||
|
||||
- 后端缓存使用 Redis,TTL 为 3600 秒(1 小时)
|
||||
- 前端缓存使用 Pinia + 本地存储持久化
|
||||
- WebSocket 通知确保前后端缓存同步更新
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
- WebSocket 连接失败不影响页面正常使用
|
||||
- 缓存刷新失败会在控制台输出错误日志
|
||||
- 不会阻塞用户操作
|
||||
|
||||
### 4. 性能考虑
|
||||
|
||||
- 批量操作会一次性清理相关缓存
|
||||
- WebSocket 广播向所有在线用户推送
|
||||
- 前端刷新时会重新加载所有字典数据
|
||||
|
||||
## 扩展建议
|
||||
|
||||
### 1. 细粒度缓存更新
|
||||
|
||||
当前实现是全量刷新,未来可以优化为增量更新:
|
||||
|
||||
```javascript
|
||||
// 只更新受影响的字典
|
||||
async function handleDictionaryUpdate(data) {
|
||||
const { action, data: dictData } = data
|
||||
|
||||
if (action === 'update' && dictData.code) {
|
||||
// 只更新特定的字典
|
||||
await dictionaryStore.getDictionary(dictData.code, true)
|
||||
} else {
|
||||
// 全量刷新
|
||||
await dictionaryStore.refresh(true)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 权限控制
|
||||
|
||||
可以只向有权限的用户发送通知:
|
||||
|
||||
```php
|
||||
// 后端只向有字典管理权限的用户发送
|
||||
$adminUserIds = User::whereHas('roles', function($query) {
|
||||
$query->where('name', 'admin');
|
||||
})->pluck('id')->toArray();
|
||||
|
||||
$this->webSocketService->sendToUsers($adminUserIds, $message);
|
||||
```
|
||||
|
||||
### 3. 消息队列
|
||||
|
||||
对于高并发场景,可以使用消息队列异步发送 WebSocket 通知:
|
||||
|
||||
```php
|
||||
// 使用 Laravel 队列
|
||||
UpdateDictionaryCacheJob::dispatch($action, $data);
|
||||
```
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 单元测试
|
||||
|
||||
测试后端 WebSocket 通知是否正确发送:
|
||||
|
||||
```php
|
||||
public function testDictionaryUpdateSendsWebSocketNotification()
|
||||
{
|
||||
$this->mockWebSocketService();
|
||||
|
||||
$dictionary = DictionaryService::create([
|
||||
'name' => 'Test',
|
||||
'code' => 'test'
|
||||
]);
|
||||
|
||||
// 验证 WebSocket 广播被调用
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 集成测试
|
||||
|
||||
1. 启动后端服务(Laravel-S)
|
||||
2. 启动前端开发服务器
|
||||
3. 在浏览器中登录系统
|
||||
4. 打开开发者工具的 Network -> WS 标签查看 WebSocket 消息
|
||||
5. 执行字典增删改操作
|
||||
6. 验证:
|
||||
- WebSocket 消息是否正确接收
|
||||
- 缓存是否自动刷新
|
||||
- 页面数据是否更新
|
||||
- 提示消息是否显示
|
||||
|
||||
### 3. 并发测试
|
||||
|
||||
1. 打开多个浏览器窗口并登录
|
||||
2. 在一个窗口中进行字典操作
|
||||
3. 验证所有窗口的缓存是否同步更新
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题 1:前端未收到 WebSocket 消息
|
||||
|
||||
**可能原因:**
|
||||
- WebSocket 服务未启动
|
||||
- 网络连接问题
|
||||
- 用户未登录
|
||||
- Laravel-S 未运行(使用普通 PHP 运行时)
|
||||
|
||||
**解决方法:**
|
||||
1. 检查 Laravel-S 服务是否启动:`php bin/laravels status`
|
||||
2. 检查浏览器控制台是否有 WebSocket 错误
|
||||
3. 确认用户已登录且有 token
|
||||
4. 确认是否在 Laravel-S 环境下运行(WebSocket 通知仅在 Laravel-S 环境下有效)
|
||||
|
||||
**注意:**
|
||||
- WebSocket 通知功能依赖于 Laravel-S (Swoole) 环境
|
||||
- 在普通 PHP 环境下运行时,WebSocket 通知会优雅降级(不发送通知,但不影响功能)
|
||||
- 仍需手动刷新页面或使用 API 轮询来获取最新数据
|
||||
|
||||
### 问题 2:后端 WebSocket 通知发送失败
|
||||
|
||||
**可能原因:**
|
||||
- Laravel-S 未运行
|
||||
- Swoole 服务器未启动
|
||||
- WebSocket 服务实例获取失败
|
||||
|
||||
**解决方法:**
|
||||
1. 确认在 Laravel-S 环境下运行:`php bin/laravels start`
|
||||
2. 检查 Laravel-S 配置文件 `config/laravels.php`
|
||||
3. 查看后端日志:`tail -f storage/logs/laravel.log`
|
||||
|
||||
**注意:**
|
||||
- 如果未在 Laravel-S 环境下运行,后端会记录警告日志,但不会报错
|
||||
- 字典数据仍会正常更新到数据库和 Redis 缓存
|
||||
- 只是前端不会收到自动更新通知
|
||||
|
||||
### 问题 3:缓存未更新
|
||||
|
||||
**可能原因:**
|
||||
- WebSocket 消息处理失败
|
||||
- API 请求失败
|
||||
- 前端未连接 WebSocket
|
||||
|
||||
**解决方法:**
|
||||
1. 查看浏览器控制台错误日志
|
||||
2. 检查网络请求是否成功
|
||||
3. 手动刷新页面验证 API 是否正常
|
||||
4. 确认 WebSocket 连接状态(浏览器开发者工具 Network -> WS)
|
||||
|
||||
### 问题 3:通知频繁弹出
|
||||
|
||||
**可能原因:**
|
||||
- 批量操作触发了多次通知
|
||||
|
||||
**解决方法:**
|
||||
1. 优化后端批量操作,只发送一次通知
|
||||
2. 前端添加防抖/节流逻辑
|
||||
|
||||
## 总结
|
||||
|
||||
通过 WebSocket 实现的字典缓存自动更新机制,确保了前后端数据的一致性,提升了用户体验。用户无需手动刷新页面即可获取最新的字典数据。
|
||||
|
||||
### 优势
|
||||
|
||||
- ✅ 实时更新,无需手动刷新
|
||||
- ✅ 多端同步,所有在线用户自动更新
|
||||
- ✅ 操作透明,用户有明确的反馈
|
||||
- ✅ 易于扩展,可应用于其他数据类型
|
||||
|
||||
### 限制
|
||||
|
||||
- 需要稳定的 WebSocket 连接
|
||||
- 当前实现为全量刷新,可以优化为增量更新
|
||||
- 依赖后端服务(Laravel-S)正常运行
|
||||
@@ -1,337 +0,0 @@
|
||||
# 日志模块实现总结
|
||||
|
||||
## 实现概述
|
||||
|
||||
本次优化完善了后端日志模块,实现了自动化的请求日志记录功能,所有后台管理 API 请求都会被自动记录到数据库中。
|
||||
|
||||
## 实现内容
|
||||
|
||||
### 1. 新增文件
|
||||
|
||||
#### 中间件
|
||||
- **app/Http/Middleware/LogRequestMiddleware.php**
|
||||
- 自动拦截所有经过的请求
|
||||
- 记录请求和响应信息
|
||||
- 计算请求执行时间
|
||||
- 提取用户信息和操作详情
|
||||
- 自动过滤敏感参数(密码、token等)
|
||||
- 获取客户端真实 IP(支持代理)
|
||||
|
||||
#### 请求验证
|
||||
- **app/Http/Requests/LogRequest.php**
|
||||
- 统一的请求参数验证
|
||||
- 支持列表查询、批量删除、清理等操作的参数验证
|
||||
- 自定义错误消息
|
||||
- 自动设置默认值
|
||||
|
||||
#### 文档
|
||||
- **docs/README_LOG.md**
|
||||
- 完整的模块文档
|
||||
- API 接口说明
|
||||
- 数据库表结构
|
||||
- 使用示例
|
||||
- 前端集成代码
|
||||
- 常见问题解答
|
||||
|
||||
### 2. 修改文件
|
||||
|
||||
#### 控制器
|
||||
- **app/Http/Controllers/System/Admin/Log.php**
|
||||
- 添加 `export` 方法:支持导出日志数据为 Excel
|
||||
- 使用 `LogRequest` 进行参数验证
|
||||
- 优化响应格式
|
||||
|
||||
#### 服务层
|
||||
- **app/Services/System/LogService.php**
|
||||
- 添加 `getListQuery` 方法:提供查询构建器(用于导出等场景)
|
||||
- 新增 `buildQuery` 方法:统一的查询构建逻辑
|
||||
- 代码重构,减少重复代码
|
||||
|
||||
#### 路由配置
|
||||
- **routes/admin.php**
|
||||
- 添加 `POST /admin/logs/export` 导出路由
|
||||
- 在所有需要认证的路由组中应用 `log.request` 中间件
|
||||
|
||||
#### 中间件配置
|
||||
- **bootstrap/app.php**
|
||||
- 注册 `log.request` 中间件别名
|
||||
- 创建 `admin.log` 中间件组
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 自动日志记录
|
||||
- ✅ 所有后台管理 API 请求自动记录
|
||||
- ✅ 记录用户信息(ID、用户名)
|
||||
- ✅ 记录请求信息(方法、URL、参数)
|
||||
- ✅ 记录响应信息(状态码、执行时间)
|
||||
- ✅ 记录客户端信息(IP、User-Agent)
|
||||
- ✅ 错误请求记录详细错误信息
|
||||
|
||||
### 敏感信息保护
|
||||
- ✅ 自动过滤密码字段
|
||||
- ✅ 自动过滤 token 字段
|
||||
- ✅ 自动过滤 secret 字段
|
||||
- ✅ 自动过滤 key 字段
|
||||
|
||||
### 日志管理功能
|
||||
- ✅ 多维度查询(用户、模块、操作、状态、时间、IP)
|
||||
- ✅ 分页查询
|
||||
- ✅ 日志详情查看
|
||||
- ✅ 日志统计(总数、成功数、失败数)
|
||||
- ✅ 单条删除
|
||||
- ✅ 批量删除
|
||||
- ✅ 定期清理(按天数)
|
||||
- ✅ 导出为 Excel
|
||||
|
||||
### 性能优化
|
||||
- ✅ 日志记录在请求处理后执行
|
||||
- ✅ 不影响业务响应速度
|
||||
- ✅ 异常处理,记录失败不影响业务
|
||||
- ✅ 支持分页查询,避免一次性加载过多数据
|
||||
|
||||
## API 接口列表
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/admin/logs` | GET | 获取日志列表 |
|
||||
| `/admin/logs/{id}` | GET | 获取日志详情 |
|
||||
| `/admin/logs/statistics` | GET | 获取日志统计 |
|
||||
| `/admin/logs/export` | POST | 导出日志(Excel) |
|
||||
| `/admin/logs/{id}` | DELETE | 删除单条日志 |
|
||||
| `/admin/logs/batch-delete` | POST | 批量删除日志 |
|
||||
| `/admin/logs/clear` | POST | 清理历史日志 |
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### system_logs 表
|
||||
|
||||
已存在的表结构,包含以下字段:
|
||||
- id: 主键
|
||||
- user_id: 用户 ID
|
||||
- username: 用户名
|
||||
- module: 模块名称
|
||||
- action: 操作名称
|
||||
- method: 请求方法
|
||||
- url: 请求 URL
|
||||
- ip: 客户端 IP
|
||||
- user_agent: 用户代理
|
||||
- params: 请求参数(JSON)
|
||||
- result: 响应结果
|
||||
- status_code: HTTP 状态码
|
||||
- status: 状态(success/error)
|
||||
- error_message: 错误信息
|
||||
- execution_time: 执行时间(毫秒)
|
||||
- created_at: 创建时间
|
||||
- updated_at: 更新时间
|
||||
|
||||
## 中间件应用范围
|
||||
|
||||
### 已应用的路由
|
||||
- ✅ 所有 `/admin/*` 路由(除登录接口)
|
||||
- ✅ 认证相关(登出、刷新、个人信息、修改密码)
|
||||
- ✅ 用户管理
|
||||
- ✅ 角色管理
|
||||
- ✅ 权限管理
|
||||
- ✅ 部门管理
|
||||
- ✅ 在线用户管理
|
||||
- ✅ 系统配置管理
|
||||
- ✅ 数据字典管理
|
||||
- ✅ 任务管理
|
||||
- ✅ 城市数据管理
|
||||
- ✅ 文件上传管理
|
||||
|
||||
### 未应用的路由
|
||||
- ❌ 登录接口(`POST /admin/auth/login`)
|
||||
- ❌ 健康检查接口(`GET /up`)
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 后端使用
|
||||
|
||||
中间件会自动记录所有请求,无需手动调用:
|
||||
|
||||
```php
|
||||
// 任何经过 log.request 中间件的请求都会被自动记录
|
||||
Route::middleware(['auth.check:admin', 'log.request'])->group(function () {
|
||||
Route::apiResource('users', UserController::class);
|
||||
// 其他路由...
|
||||
});
|
||||
```
|
||||
|
||||
### 前端调用示例
|
||||
|
||||
```javascript
|
||||
// 获取日志列表
|
||||
const response = await request.get('/admin/logs', {
|
||||
params: {
|
||||
username: 'admin',
|
||||
module: 'users',
|
||||
status: 'success',
|
||||
page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
})
|
||||
|
||||
// 导出日志
|
||||
await request.post('/admin/logs/export', {
|
||||
username: 'admin',
|
||||
status: 'error'
|
||||
}, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
// 批量删除
|
||||
await request.post('/admin/logs/batch-delete', {
|
||||
ids: [1, 2, 3, 4, 5]
|
||||
})
|
||||
|
||||
// 清理历史日志
|
||||
await request.post('/admin/logs/clear', {
|
||||
days: 30
|
||||
})
|
||||
```
|
||||
|
||||
## 日志记录示例
|
||||
|
||||
### 成功请求日志
|
||||
```json
|
||||
{
|
||||
"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",
|
||||
"password": "******"
|
||||
},
|
||||
"result": null,
|
||||
"status_code": 200,
|
||||
"status": "success",
|
||||
"error_message": null,
|
||||
"execution_time": 125,
|
||||
"created_at": "2024-01-01 12:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 失败请求日志
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"module": "users",
|
||||
"action": "删除 users",
|
||||
"method": "DELETE",
|
||||
"url": "http://example.com/admin/users/999",
|
||||
"ip": "192.168.1.1",
|
||||
"user_agent": "Mozilla/5.0...",
|
||||
"params": {},
|
||||
"result": "{\"code\":404,\"message\":\"用户不存在\"}",
|
||||
"status_code": 404,
|
||||
"status": "error",
|
||||
"error_message": "用户不存在",
|
||||
"execution_time": 45,
|
||||
"created_at": "2024-01-01 12:01:00"
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 性能考虑
|
||||
- 日志记录在请求处理后执行,不影响响应速度
|
||||
- 大量日志会增加数据库写入压力
|
||||
- 建议定期清理历史日志
|
||||
|
||||
### 2. 数据安全
|
||||
- 敏感信息已自动过滤
|
||||
- 日志数据应妥善保管
|
||||
- 建议定期备份重要日志
|
||||
|
||||
### 3. 权限控制
|
||||
- 日志管理接口需要相应权限
|
||||
- 建议只允许管理员查看和操作日志
|
||||
|
||||
### 4. 数据库优化
|
||||
- 确保查询字段有索引
|
||||
- 使用分页查询避免加载过多数据
|
||||
- 定期清理历史日志
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
### 1. 异步队列
|
||||
考虑使用 Laravel 队列异步处理日志记录,进一步减少对响应时间的影响。
|
||||
|
||||
### 2. 日志归档
|
||||
实现日志归档功能,将历史日志移动到归档表或文件存储。
|
||||
|
||||
### 3. 日志分析
|
||||
集成日志分析工具,提供可视化仪表盘和趋势分析。
|
||||
|
||||
### 4. 定时清理
|
||||
配置 Laravel 任务调度器,自动清理指定天数前的日志:
|
||||
|
||||
```php
|
||||
// app/Console/Kernel.php
|
||||
$schedule->call(function () {
|
||||
app(LogService::class)->clearLogs(90);
|
||||
})->dailyAt('02:00');
|
||||
```
|
||||
|
||||
### 5. 日志级别
|
||||
增加日志级别(info、warning、error、critical),便于分类管理。
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 功能测试
|
||||
1. 测试各种请求是否被正确记录
|
||||
2. 测试敏感信息是否被正确过滤
|
||||
3. 测试日志查询和筛选功能
|
||||
4. 测试日志导出功能
|
||||
5. 测试批量删除和清理功能
|
||||
|
||||
### 性能测试
|
||||
1. 测试日志记录对响应时间的影响
|
||||
2. 测试大量日志数据的查询性能
|
||||
3. 测试并发写入的性能
|
||||
|
||||
### 边界测试
|
||||
1. 测试异常情况下的日志记录
|
||||
2. 测试超长参数的处理
|
||||
3. 测试特殊字符的处理
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增文件
|
||||
```
|
||||
app/Http/Middleware/LogRequestMiddleware.php
|
||||
app/Http/Requests/LogRequest.php
|
||||
docs/README_LOG.md
|
||||
docs/LOG_IMPLEMENTATION_SUMMARY.md
|
||||
```
|
||||
|
||||
### 修改文件
|
||||
```
|
||||
app/Http/Controllers/System/Admin/Log.php
|
||||
app/Services/System/LogService.php
|
||||
routes/admin.php
|
||||
bootstrap/app.php
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
本次日志模块优化完善实现了:
|
||||
- ✅ 全自动化的请求日志记录
|
||||
- ✅ 完善的日志管理功能
|
||||
- ✅ 敏感信息保护
|
||||
- ✅ 多维度查询和筛选
|
||||
- ✅ 数据导出功能
|
||||
- ✅ 批量操作支持
|
||||
- ✅ 完整的文档说明
|
||||
|
||||
日志模块现已完全集成到项目中,所有后台管理 API 请求都会被自动记录,管理员可以通过日志管理功能进行系统监控、审计和问题排查。
|
||||
1500
docs/README_LARAVERS.md
Normal file
1500
docs/README_LARAVERS.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,608 +0,0 @@
|
||||
# 系统操作日志模块文档
|
||||
|
||||
## 概述
|
||||
|
||||
系统操作日志模块用于记录后台管理系统的所有操作请求,包括用户操作、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)
|
||||
- 初始版本
|
||||
- 实现基础日志记录功能
|
||||
- 支持多维度查询和筛选
|
||||
- 支持数据导出
|
||||
- 支持批量删除和清理
|
||||
@@ -44,6 +44,8 @@ app/Http/Controllers/System/
|
||||
- Laravel 11
|
||||
- Redis 缓存
|
||||
- Intervention Image (图像处理)
|
||||
- Laravel-S / Swoole (WebSocket 通知)
|
||||
- WebSocket (实时消息推送)
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
@@ -84,15 +86,18 @@ app/Http/Controllers/System/
|
||||
- `username`: 用户名
|
||||
- `module`: 模块
|
||||
- `action`: 操作
|
||||
- `method`: 请求方法
|
||||
- `method`: 请求方法 (GET/POST/PUT/DELETE)
|
||||
- `url`: 请求URL
|
||||
- `ip`: IP地址
|
||||
- `user_agent`: 用户代理
|
||||
- `request_data`: 请求数据(JSON)
|
||||
- `response_data`: 响应数据(JSON)
|
||||
- `duration`: 执行时间(毫秒)
|
||||
- `status_code`: 状态码
|
||||
- `params`: 请求参数(JSON)
|
||||
- `result`: 响应结果(仅错误时记录)
|
||||
- `status_code`: HTTP状态码
|
||||
- `status`: 状态 (success/error)
|
||||
- `error_message`: 错误信息
|
||||
- `execution_time`: 执行时间(毫秒)
|
||||
- `created_at`: 创建时间
|
||||
- `updated_at`: 更新时间
|
||||
|
||||
### system_tasks (任务表)
|
||||
- `id`: 主键
|
||||
@@ -197,34 +202,220 @@ app/Http/Controllers/System/
|
||||
|
||||
### 操作日志管理
|
||||
|
||||
操作日志模块通过中间件自动记录所有后台管理 API 请求,实现全自动化的日志记录功能。
|
||||
|
||||
#### 中间件说明
|
||||
|
||||
**LogRequestMiddleware**
|
||||
|
||||
位置: `app/Http/Middleware/LogRequestMiddleware.php`
|
||||
|
||||
功能:
|
||||
- 自动拦截所有经过的请求
|
||||
- 记录请求和响应信息
|
||||
- 计算请求执行时间
|
||||
- 提取用户信息和操作详情
|
||||
- 过滤敏感参数(password、token、secret、key)
|
||||
- 获取客户端真实 IP(支持代理)
|
||||
- 异常处理,记录失败不影响业务
|
||||
|
||||
使用方式:
|
||||
```php
|
||||
// 在路由中应用
|
||||
Route::middleware(['auth.check:admin', 'log.request'])->group(function () {
|
||||
// 需要记录日志的路由
|
||||
});
|
||||
```
|
||||
|
||||
#### 日志记录规则
|
||||
|
||||
**自动记录的请求:**
|
||||
所有经过 `log.request` 中间件的请求都会被自动记录,包括:
|
||||
- 用户管理操作
|
||||
- 角色管理操作
|
||||
- 权限管理操作
|
||||
- 部门管理操作
|
||||
- 系统配置操作
|
||||
- 其他所有后台管理操作
|
||||
|
||||
**不记录的请求:**
|
||||
- 登录接口 (`POST /admin/auth/login`)
|
||||
- 健康检查接口 (`GET /up`)
|
||||
- 其他明确排除的路由
|
||||
|
||||
**敏感信息过滤:**
|
||||
以下字段会被自动过滤,记录为 `******`:
|
||||
- `password`
|
||||
- `password_confirmation`
|
||||
- `token`
|
||||
- `secret`
|
||||
- `key`
|
||||
|
||||
**错误日志处理:**
|
||||
- 成功请求 (HTTP 状态码 < 400): `status` = `success`
|
||||
- 失败请求 (HTTP 状态码 >= 400): `status` = `error`
|
||||
- 错误时记录响应内容和错误消息
|
||||
- 同时写入 Laravel 日志文件 (`storage/logs/laravel.log`)
|
||||
|
||||
#### 获取日志列表
|
||||
- **接口**: `GET /admin/logs`
|
||||
- **参数**:
|
||||
- `page`, `page_size`
|
||||
- `keyword`: 搜索关键词(用户名/模块/操作)
|
||||
- `module`: 模块
|
||||
- `action`: 操作
|
||||
- `user_id`: 用户ID
|
||||
- `start_date`: 开始日期
|
||||
- `end_date`: 结束日期
|
||||
- `order_by`, `order_direction`
|
||||
```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"
|
||||
}
|
||||
],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取日志详情
|
||||
- **接口**: `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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除日志
|
||||
#### 获取日志统计
|
||||
- **接口**: `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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 导出日志
|
||||
- **接口**: `POST /admin/logs/export`
|
||||
- **参数**: 与获取日志列表相同的查询参数
|
||||
- **响应**: Excel 文件下载
|
||||
- **文件名格式**: `系统操作日志_YYYYMMDDHHmmss.xlsx`
|
||||
- **包含字段**:
|
||||
- ID
|
||||
- 用户名
|
||||
- 模块
|
||||
- 操作
|
||||
- 请求方法
|
||||
- URL
|
||||
- IP 地址
|
||||
- 状态码
|
||||
- 状态
|
||||
- 错误信息
|
||||
- 执行时间(ms)
|
||||
- 创建时间
|
||||
|
||||
#### 删除单条日志
|
||||
- **接口**: `DELETE /admin/logs/{id}`
|
||||
- **响应示例**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "删除成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 批量删除日志
|
||||
- **接口**: `POST /admin/logs/batch-delete`
|
||||
- **参数**:
|
||||
```json
|
||||
{
|
||||
"ids": [1, 2, 3]
|
||||
"ids": [1, 2, 3, 4, 5]
|
||||
}
|
||||
```
|
||||
- **响应示例**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "批量删除成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 清理日志
|
||||
#### 清理历史日志
|
||||
- **接口**: `POST /admin/logs/clear`
|
||||
- **参数**:
|
||||
```json
|
||||
@@ -232,39 +423,129 @@ app/Http/Controllers/System/
|
||||
"days": 30
|
||||
}
|
||||
```
|
||||
- **说明**: 删除指定天数之前的日志记录
|
||||
|
||||
#### 获取日志统计
|
||||
- **接口**: `GET /admin/logs/statistics`
|
||||
- **参数**:
|
||||
- `start_date`: 开始日期
|
||||
- `end_date`: 结束日期
|
||||
- **返回**:
|
||||
- **说明**: 清理指定天数前的所有日志记录,默认清理 30 天前的数据
|
||||
- **响应示例**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"total_count": 1000,
|
||||
"module_stats": [
|
||||
{
|
||||
"module": "user",
|
||||
"count": 500
|
||||
}
|
||||
],
|
||||
"user_stats": [
|
||||
{
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"count": 800
|
||||
}
|
||||
]
|
||||
}
|
||||
"message": "清理成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 数据字典管理
|
||||
|
||||
数据字典模块提供了完整的字典管理功能,包括字典分类和字典项的 CRUD 操作。通过 WebSocket 实现了前后端缓存的实时同步更新。
|
||||
|
||||
#### 字典缓存更新机制
|
||||
|
||||
**概述**
|
||||
|
||||
字典缓存更新机制通过 WebSocket 实现前后端字典缓存的实时同步,确保在字典分类和字典项的增删改等操作后,前端字典缓存能够自动更新。
|
||||
|
||||
**技术实现**
|
||||
|
||||
1. **后端实现**
|
||||
|
||||
在 `app/Services/System/DictionaryService.php` 中添加了 WebSocket 通知功能:
|
||||
|
||||
**通知方法:**
|
||||
- `notifyDictionaryUpdate` - 字典分类更新通知
|
||||
- 触发时机:创建、更新、删除、批量删除、批量更新状态
|
||||
- 消息类型:`dictionary_update`
|
||||
|
||||
- `notifyDictionaryItemUpdate` - 字典项更新通知
|
||||
- 触发时机:创建、更新、删除、批量删除、批量更新状态
|
||||
- 消息类型:`dictionary_item_update`
|
||||
|
||||
**WebSocket 消息格式:**
|
||||
|
||||
字典分类更新消息:
|
||||
```json
|
||||
{
|
||||
"type": "dictionary_update",
|
||||
"data": {
|
||||
"action": "create|update|delete|batch_delete|batch_update_status",
|
||||
"resource_type": "dictionary",
|
||||
"data": {
|
||||
// 字典分类数据
|
||||
},
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字典项更新消息:
|
||||
```json
|
||||
{
|
||||
"type": "dictionary_item_update",
|
||||
"data": {
|
||||
"action": "create|update|delete|batch_delete|batch_update_status",
|
||||
"resource_type": "dictionary_item",
|
||||
"data": {
|
||||
// 字典项数据
|
||||
},
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **前端实现**
|
||||
|
||||
创建了 `resources/admin/src/composables/useWebSocket.js` 来处理 WebSocket 连接和消息监听:
|
||||
|
||||
**主要功能:**
|
||||
- 初始化 WebSocket 连接(检查用户登录状态、验证用户信息完整性)
|
||||
- 消息处理器:`handleDictionaryUpdate` 和 `handleDictionaryItemUpdate`
|
||||
- 缓存刷新:接收到更新通知后,自动刷新字典缓存并显示成功提示
|
||||
|
||||
**App.vue 集成:**
|
||||
|
||||
```javascript
|
||||
onMounted(async () => {
|
||||
// 初始化 WebSocket 连接
|
||||
if (userStore.isLoggedIn()) {
|
||||
initWebSocket()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 关闭 WebSocket 连接
|
||||
closeWebSocket()
|
||||
})
|
||||
```
|
||||
|
||||
**工作流程:**
|
||||
|
||||
```
|
||||
用户操作(增删改字典)
|
||||
↓
|
||||
后端 Controller 调用 Service
|
||||
↓
|
||||
Service 执行数据库操作
|
||||
↓
|
||||
Service 清理后端缓存(Redis)
|
||||
↓
|
||||
Service 发送 WebSocket 广播通知
|
||||
↓
|
||||
WebSocket 推送消息到所有在线客户端
|
||||
↓
|
||||
前端接收 WebSocket 消息
|
||||
↓
|
||||
触发相应的消息处理器
|
||||
↓
|
||||
刷新前端字典缓存
|
||||
↓
|
||||
显示成功提示
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
- WebSocket 仅在用户登录后建立连接
|
||||
- 连接失败会自动重试(最多 5 次)
|
||||
- 页面卸载时会自动关闭连接
|
||||
- WebSocket 通知功能依赖于 Laravel-S (Swoole) 环境
|
||||
- 在普通 PHP 环境下运行时,WebSocket 通知会优雅降级(不发送通知,但不影响功能)
|
||||
|
||||
#### 获取字典列表
|
||||
- **接口**: `GET /admin/dictionaries`
|
||||
- **参数**:
|
||||
@@ -658,8 +939,17 @@ app/Http/Controllers/System/
|
||||
### 数据字典缓存
|
||||
|
||||
- **缓存键**: `dictionary:all` 或 `dictionary:code:{code}`
|
||||
- **过期时间**: 60分钟
|
||||
- **更新时机**: 字典数据增删改时自动清除
|
||||
- **过期时间**: 3600秒(1小时)
|
||||
- **更新时机**:
|
||||
- 字典数据增删改时自动清除后端缓存(Redis)
|
||||
- 通过 WebSocket 通知前端自动刷新缓存
|
||||
|
||||
**字典缓存同步流程:**
|
||||
1. 后端执行字典操作(增删改)
|
||||
2. 清理 Redis 缓存
|
||||
3. 发送 WebSocket 广播通知
|
||||
4. 前端接收通知并自动刷新缓存
|
||||
5. 显示成功提示
|
||||
|
||||
## 服务层说明
|
||||
|
||||
@@ -681,13 +971,24 @@ app/Http/Controllers/System/
|
||||
|
||||
**主要方法**:
|
||||
- `getList()`: 获取日志列表
|
||||
- `getById()`: 根据 ID 获取日志详情
|
||||
- `getStatistics()`: 获取统计数据
|
||||
- `getListQuery()`: 获取日志查询构建器
|
||||
- `clearLogs()`: 清理过期日志
|
||||
- `delete()`: 删除单条日志
|
||||
- `batchDelete()`: 批量删除日志
|
||||
- `record()`: 记录日志(由中间件自动调用)
|
||||
|
||||
**特性**:
|
||||
- 自动记录所有经过中间件的请求
|
||||
- 计算请求执行时间
|
||||
- 过滤敏感参数
|
||||
- 获取客户端真实 IP(支持代理)
|
||||
- 异常处理,记录失败不影响业务
|
||||
|
||||
### DictionaryService
|
||||
|
||||
提供数据字典和字典项的管理功能。
|
||||
提供数据字典和字典项的管理功能,包括 WebSocket 通知机制。
|
||||
|
||||
**主要方法**:
|
||||
- `getList()`: 获取字典列表
|
||||
@@ -696,6 +997,13 @@ app/Http/Controllers/System/
|
||||
- `createItem()`: 创建字典项
|
||||
- `update()`: 更新字典
|
||||
- `updateItem()`: 更新字典项
|
||||
- `notifyDictionaryUpdate()`: 发送字典更新通知
|
||||
- `notifyDictionaryItemUpdate()`: 发送字典项更新通知
|
||||
|
||||
**缓存机制**:
|
||||
- Redis 缓存字典数据(TTL: 3600秒)
|
||||
- WebSocket 实时通知前端更新
|
||||
- 前端 Pinia + 本地存储持久化
|
||||
|
||||
### TaskService
|
||||
|
||||
@@ -744,49 +1052,459 @@ php artisan db:seed --class=SystemSeeder
|
||||
- 常用数据字典
|
||||
- 全国省市区数据
|
||||
|
||||
## 前端集成示例
|
||||
|
||||
### 日志管理页面
|
||||
|
||||
```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. **Swoole环境注意事项**:
|
||||
- 文件上传时注意临时文件清理
|
||||
- 使用Redis缓存避免内存泄漏
|
||||
- 图片压缩使用协程安全的方式
|
||||
### 1. Swoole环境注意事项
|
||||
- 文件上传时注意临时文件清理
|
||||
- 使用Redis缓存避免内存泄漏
|
||||
- 图片压缩使用协程安全的方式
|
||||
- WebSocket 通知依赖于 Laravel-S 环境
|
||||
|
||||
2. **安全注意事项**:
|
||||
- 文件上传必须验证文件类型和大小
|
||||
- 敏感操作必须记录日志
|
||||
- 配置数据不要存储密码等敏感信息
|
||||
### 2. 安全注意事项
|
||||
- 文件上传必须验证文件类型和大小
|
||||
- 敏感操作必须记录日志
|
||||
- 配置数据不要存储密码等敏感信息
|
||||
- 日志敏感信息已自动过滤
|
||||
|
||||
3. **性能优化**:
|
||||
- 城市数据使用Redis缓存
|
||||
- 大量日志数据定期清理
|
||||
- 图片上传时进行压缩处理
|
||||
### 3. 性能优化
|
||||
- 城市数据使用Redis缓存
|
||||
- 大量日志数据定期清理
|
||||
- 图片上传时进行压缩处理
|
||||
- 日志记录在请求处理后执行,不影响响应速度
|
||||
|
||||
4. **文件上传**:
|
||||
- 限制文件上传大小
|
||||
- 验证文件MIME类型
|
||||
- 定期清理临时文件
|
||||
### 4. 文件上传
|
||||
- 限制文件上传大小
|
||||
- 验证文件MIME类型
|
||||
- 定期清理临时文件
|
||||
|
||||
### 5. 日志管理
|
||||
- 定期清理历史日志(建议使用任务调度器)
|
||||
- 确保查询字段有索引
|
||||
- 使用分页查询避免加载过多数据
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 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. 数据库索引
|
||||
|
||||
确保以下字段有索引:
|
||||
- `system_logs`: `user_id`, `username`, `module`, `status`, `created_at`
|
||||
- `system_dictionaries`: `code`, `status`
|
||||
- `system_dictionary_items`: `dictionary_id`, `status`
|
||||
- `system_configs`: `group`, `key`, `status`
|
||||
|
||||
### 3. 分页查询
|
||||
|
||||
列表查询必须使用分页,避免一次加载过多数据。
|
||||
|
||||
### 4. 异步记录
|
||||
|
||||
日志记录操作应放在请求处理后,不影响响应速度。
|
||||
|
||||
### 5. 细粒度缓存更新
|
||||
|
||||
字典缓存当前实现为全量刷新,未来可以优化为增量更新:
|
||||
|
||||
```javascript
|
||||
// 只更新受影响的字典
|
||||
async function handleDictionaryUpdate(data) {
|
||||
const { action, data: dictData } = data
|
||||
|
||||
if (action === 'update' && dictData.code) {
|
||||
// 只更新特定的字典
|
||||
await dictionaryStore.getDictionary(dictData.code, true)
|
||||
} else {
|
||||
// 全量刷新
|
||||
await dictionaryStore.refresh(true)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. **日志告警**: 添加日志异常告警功能
|
||||
2. **配置加密**: 敏感配置数据加密存储
|
||||
3. **多语言**: 支持配置数据的多语言
|
||||
4. **任务监控**: 添加任务执行监控和通知
|
||||
5. **CDN集成**: 文件上传支持CDN分发
|
||||
### 1. 日志告警
|
||||
添加日志异常告警功能,当出现大量错误日志时自动通知管理员。
|
||||
|
||||
### 2. 配置加密
|
||||
敏感配置数据加密存储,提高安全性。
|
||||
|
||||
### 3. 多语言
|
||||
支持配置数据的多语言,便于国际化部署。
|
||||
|
||||
### 4. 任务监控
|
||||
添加任务执行监控和通知,实时掌握任务运行状态。
|
||||
|
||||
### 5. CDN集成
|
||||
文件上传支持CDN分发,提高访问速度。
|
||||
|
||||
### 6. WebSocket 权限控制
|
||||
可以只向有权限的用户发送通知:
|
||||
|
||||
```php
|
||||
// 后端只向有字典管理权限的用户发送
|
||||
$adminUserIds = User::whereHas('roles', function($query) {
|
||||
$query->where('name', 'admin');
|
||||
})->pluck('id')->toArray();
|
||||
|
||||
$this->webSocketService->sendToUsers($adminUserIds, $message);
|
||||
```
|
||||
|
||||
### 7. 消息队列
|
||||
对于高并发场景,可以使用消息队列异步发送 WebSocket 通知:
|
||||
|
||||
```php
|
||||
// 使用 Laravel 队列
|
||||
UpdateDictionaryCacheJob::dispatch($action, $data);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何清除城市数据缓存?
|
||||
### Q1: 如何清除城市数据缓存?
|
||||
A: 调用 `CityService::clearCache()` 方法或运行 `php artisan cache:forget city:tree`。
|
||||
|
||||
### Q: 图片上传后如何压缩?
|
||||
### Q2: 图片上传后如何压缩?
|
||||
A: 上传时设置 `compress=true` 和 `quality` 参数,系统会自动压缩。
|
||||
|
||||
### Q: 如何配置定时任务?
|
||||
### Q3: 如何配置定时任务?
|
||||
A: 在Admin后台创建任务,设置Cron表达式,系统会自动调度执行。
|
||||
|
||||
### Q: 数据字典如何使用?
|
||||
### Q4: 数据字典如何使用?
|
||||
A: 通过Public API获取字典数据,前端根据数据渲染下拉框等组件。
|
||||
|
||||
### Q: 日志数据过多如何处理?
|
||||
### Q5: 日志数据过多如何处理?
|
||||
A: 定期使用 `/admin/logs/clear` 接口清理过期日志,或在后台设置自动清理任务。
|
||||
|
||||
### Q6: 为什么某些请求没有被记录?
|
||||
A: 检查路由是否应用了 `log.request` 中间件,或者在中间件中是否被排除了。
|
||||
|
||||
### Q7: 字典缓存未更新怎么办?
|
||||
A: 检查以下几点:
|
||||
- 确认 Laravel-S 服务是否启动:`php bin/laravels status`
|
||||
- 检查浏览器控制台是否有 WebSocket 错误
|
||||
- 确认用户已登录且有 token
|
||||
- 手动刷新页面验证 API 是否正常
|
||||
|
||||
### Q8: WebSocket 连接失败会影响功能吗?
|
||||
A: 不会。WebSocket 连接失败不影响页面正常使用,只是不会收到自动更新通知。字典数据仍会正常更新到数据库和 Redis 缓存,只是前端不会收到实时通知,需要手动刷新页面。
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 功能测试
|
||||
1. 测试各种请求是否被正确记录
|
||||
2. 测试敏感信息是否被正确过滤
|
||||
3. 测试日志查询和筛选功能
|
||||
4. 测试日志导出功能
|
||||
5. 测试批量删除和清理功能
|
||||
6. 测试字典 WebSocket 通知是否正确
|
||||
|
||||
### 2. 性能测试
|
||||
1. 测试日志记录对响应时间的影响
|
||||
2. 测试大量日志数据的查询性能
|
||||
3. 测试并发写入的性能
|
||||
4. 测试 WebSocket 广播性能
|
||||
|
||||
### 3. 集成测试(字典 WebSocket)
|
||||
1. 启动后端服务(Laravel-S)
|
||||
2. 启动前端开发服务器
|
||||
3. 在浏览器中登录系统
|
||||
4. 打开开发者工具的 Network -> WS 标签查看 WebSocket 消息
|
||||
5. 执行字典增删改操作
|
||||
6. 验证:
|
||||
- WebSocket 消息是否正确接收
|
||||
- 缓存是否自动刷新
|
||||
- 页面数据是否更新
|
||||
- 提示消息是否显示
|
||||
|
||||
### 4. 并发测试
|
||||
1. 打开多个浏览器窗口并登录
|
||||
2. 在一个窗口中进行字典操作
|
||||
3. 验证所有窗口的缓存是否同步更新
|
||||
|
||||
### 5. 边界测试
|
||||
1. 测试异常情况下的日志记录
|
||||
2. 测试超长参数的处理
|
||||
3. 测试特殊字符的处理
|
||||
4. 测试 WebSocket 断连重连机制
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 核心文件
|
||||
|
||||
**控制器:**
|
||||
```
|
||||
app/Http/Controllers/System/Admin/Config.php
|
||||
app/Http/Controllers/System/Admin/Log.php
|
||||
app/Http/Controllers/System/Admin/Dictionary.php
|
||||
app/Http/Controllers/System/Admin/Task.php
|
||||
app/Http/Controllers/System/Admin/City.php
|
||||
app/Http/Controllers/System/Admin/Upload.php
|
||||
app/Http/Controllers/System/WebSocket.php
|
||||
```
|
||||
|
||||
**中间件:**
|
||||
```
|
||||
app/Http/Middleware/LogRequestMiddleware.php
|
||||
```
|
||||
|
||||
**请求验证:**
|
||||
```
|
||||
app/Http/Requests/LogRequest.php
|
||||
```
|
||||
|
||||
**服务层:**
|
||||
```
|
||||
app/Services/System/ConfigService.php
|
||||
app/Services/System/LogService.php
|
||||
app/Services/System/DictionaryService.php
|
||||
app/Services/System/TaskService.php
|
||||
app/Services/System/CityService.php
|
||||
app/Services/System/UploadService.php
|
||||
app/Services/WebSocket/WebSocketService.php
|
||||
```
|
||||
|
||||
**模型:**
|
||||
```
|
||||
app/Models/System/Config.php
|
||||
app/Models/System/Log.php
|
||||
app/Models/System/Dictionary.php
|
||||
app/Models/System/DictionaryItem.php
|
||||
app/Models/System/Task.php
|
||||
app/Models/System/City.php
|
||||
```
|
||||
|
||||
**路由:**
|
||||
```
|
||||
routes/admin.php (后台管理路由)
|
||||
routes/api.php (公共 API 路由)
|
||||
```
|
||||
|
||||
**前端:**
|
||||
```
|
||||
resources/admin/src/composables/useWebSocket.js
|
||||
resources/admin/src/App.vue (集成 WebSocket)
|
||||
```
|
||||
|
||||
**文档:**
|
||||
```
|
||||
docs/README_SYSTEM.md (本文档)
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
System 基础模块提供了完整的系统管理功能,包括:
|
||||
|
||||
### 核心功能
|
||||
|
||||
- ✅ 系统配置管理(多分组、多类型支持)
|
||||
- ✅ 数据字典管理(分类 + 字典项)
|
||||
- ✅ 操作日志管理(自动记录、多维度查询、导出)
|
||||
- ✅ 任务管理(定时任务、手动执行、统计)
|
||||
- ✅ 城市数据管理(三级联动、缓存优化)
|
||||
- ✅ 文件上传管理(单文件、多文件、Base64、压缩)
|
||||
|
||||
### 高级特性
|
||||
|
||||
- ✅ WebSocket 实时通知(字典缓存自动更新)
|
||||
- ✅ Redis 缓存机制(性能优化)
|
||||
- ✅ 自动化日志记录(中间件拦截)
|
||||
- ✅ 敏感信息保护(自动过滤)
|
||||
- ✅ 数据导出功能(Excel 导出)
|
||||
- ✅ 批量操作支持
|
||||
- ✅ 完整的 API 文档
|
||||
|
||||
### 性能优化
|
||||
|
||||
- ✅ Redis 缓存(城市数据、系统配置、数据字典)
|
||||
- ✅ 日志异步记录(不影响响应速度)
|
||||
- ✅ 分页查询(避免加载过多数据)
|
||||
- ✅ 图片压缩(减少存储空间)
|
||||
- ✅ WebSocket 实时更新(减少不必要的请求)
|
||||
|
||||
### 安全特性
|
||||
|
||||
- ✅ 敏感信息过滤
|
||||
- ✅ 文件类型验证
|
||||
- ✅ 请求日志记录
|
||||
- ✅ IP 地址记录
|
||||
- ✅ 异常处理机制
|
||||
|
||||
System 模块现已完全集成到项目中,提供了完整的系统管理功能和优秀的用户体验。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user