签入版本
This commit is contained in:
@@ -1,323 +1,61 @@
|
||||
# 5.14 操作日志
|
||||
|
||||
:::tip 概述
|
||||
操作日志通过 `#[Log]` 注解 + LogMiddleware 自动记录,无需手动埋点。所有写操作(POST/PUT/DELETE/PATCH)始终记录,GET 请求仅当注解类型为导出/下载/导入时记录。日志写入失败不影响主业务流程。
|
||||
操作日志由 **RouteSpec 声明驱动、中间件链尾采集、异步落库**——业务代码零参与。记什么、记不记、什么类型,全部来自路由声明与配置开关。
|
||||
:::
|
||||
|
||||
## 记录流程
|
||||
## 声明驱动
|
||||
|
||||
```go
|
||||
// internal/router/router.go(RouteSpec)
|
||||
"Add": {Method: "POST", Path: "/api/example/add", Log: "案例演示-新增记录", LogType: "add"},
|
||||
"Page": {Method: "GET", Path: "/api/example/page", Log: "案例演示-查询分页记录", LogType: "query"},
|
||||
```
|
||||
|
||||
| 字段 | 语义 |
|
||||
|---|---|
|
||||
| Log | 日志标题("模块-动作"格式);空 = 不记录 |
|
||||
| LogType | 动作字符串(add/update/delete/query/import/export/reset/generate/execute/pause…),落库时映射为数值枚举 |
|
||||
|
||||
**数值枚举**(`internal/consts/log_type.go`,前端按值映射显示,数值勿改动):1 新增 / 2 修改 / 3 删除 / 4 查询 / 5 导入 / 6 导出 / 7 审批 / 8 驳回 / 9 提交 / 10 撤回 / 11 关闭 / 12 开启 / 13 启用 / 14 禁用 / 15 上传 / 16 下载 / 17 发送 / 18 同步 / 19 代码生成 / 20 清空 / 21 还原 / 22 登录 / 23 登出 / 24 重置 / 25 分配 / 26 授权 / 27 执行 / 28 暂停 / 29 恢复 / 30 发布 / 31 下架 / … / 40 解绑,99 其他,0 为自动推断占位(路由 LogType 为空时落 0)。
|
||||
|
||||
改一条声明即改变该接口的日志行为,authz/公开判断不受影响(同源)。
|
||||
|
||||
## 采集链路
|
||||
|
||||
```text
|
||||
请求进入
|
||||
│
|
||||
▼
|
||||
LogMiddleware::handle()
|
||||
│
|
||||
├─ 记录开始时间 microtime(true)
|
||||
│
|
||||
├─ 执行后续中间件和控制器($next($request))
|
||||
│
|
||||
├─ 获取响应
|
||||
│
|
||||
├─ 检查排除路由(config middleware.log_except)
|
||||
│ └─ 命中 → 直接返回,不记录
|
||||
│
|
||||
├─ 判断是否需要记录
|
||||
│ ├─ POST/PUT/DELETE/PATCH → 始终记录 ✓
|
||||
│ ├─ GET + #[Log] TYPE_EXPORT → 记录 ✓
|
||||
│ ├─ GET + #[Log] TYPE_DOWNLOAD → 记录 ✓
|
||||
│ ├─ GET + #[Log] TYPE_IMPORT → 记录 ✓
|
||||
│ └─ 其它 GET → 不记录 ✗
|
||||
│
|
||||
├─ 读取 #[Log] 注解(AttributeService::getLog)
|
||||
│ ├─ 有注解 → 使用注解的 title、type、description
|
||||
│ └─ 无注解 → 使用路径作为 title,按 HTTP 方法猜测 type
|
||||
│
|
||||
├─ 组装日志数据
|
||||
│ ├─ 标题、类型(来自注解)
|
||||
│ ├─ URL、请求参数(截取前 2000 字符)
|
||||
│ ├─ 响应内容(截取前 2000 字符)
|
||||
│ ├─ IP、归属地、操作系统、浏览器(RequestInfoService)
|
||||
│ ├─ 执行耗时 = (当前时间 - 开始时间) × 1000 毫秒
|
||||
│ └─ 状态(检测响应中的 code:0=正常 1=异常)、错误信息
|
||||
│
|
||||
└─ 写入 OperationLog 表(try-catch 包裹,失败不影响主业务)
|
||||
请求进入 → OperationLog 中间件(链尾)登记开始
|
||||
↓
|
||||
正常返回 / 拒绝 / panic —— 中间件读到最终响应摘要
|
||||
↓
|
||||
组装条目(操作人、IP、UA、归属地、路径、入参摘要、结果码、耗时)
|
||||
↓
|
||||
异步写 gin_operation_log(不阻塞响应)
|
||||
```
|
||||
|
||||
## 记录字段
|
||||
## 开关
|
||||
|
||||
| 字段 | 说明 | 来源 | 示例 |
|
||||
|------|------|------|------|
|
||||
| `title` | 操作标题 | `#[Log]` 注解 title 参数 | 用户管理-新增记录 |
|
||||
| `type` | 操作类型 | `#[Log]` 注解 type 参数 | 1=新增 |
|
||||
| `source` | 来源 | 固定 0 | 0=后台 |
|
||||
| `method` | HTTP 方法 | 请求方法 | POST |
|
||||
| `request_method` | 控制器方法名 | 请求 action | add |
|
||||
| `url` | 请求 URL | 请求 url | /api/user/add |
|
||||
| `param` | 请求参数 | JSON 截取前 2000 字符 | {"username":"admin"} |
|
||||
| `result` | 响应内容 | JSON 截取前 2000 字符 | {"code":0,"msg":"添加成功"} |
|
||||
| `ip` | 客户端 IP | RequestInfoService | 192.168.1.100 |
|
||||
| `location` | IP 归属地 | zoujingli/ip2region | 江苏省南京市 |
|
||||
| `os` | 操作系统 | RequestInfoService | Windows 10 |
|
||||
| `browser` | 浏览器 | RequestInfoService | Chrome 120 |
|
||||
| `consume_time` | 执行耗时 | 毫秒 | 125 |
|
||||
| `status` | 状态 | 响应 code 判断 | 0=正常 1=异常 |
|
||||
| `error` | 错误信息 | 响应中的 msg(异常时) | 用户名已存在 |
|
||||
| `create_user` | 操作人 | 当前登录用户名 | admin |
|
||||
| `create_time` | 操作时间 | 自动填充 | 2026-09-22 10:30:00 |
|
||||
|
||||
## 注解使用
|
||||
|
||||
### 基本用法
|
||||
|
||||
```php
|
||||
// 写操作(POST/PUT/DELETE/PATCH)始终记录
|
||||
#[Log('用户管理-新增', Log::TYPE_ADD)]
|
||||
public function add(): Json { ... }
|
||||
|
||||
#[Log('用户管理-修改', Log::TYPE_UPDATE)]
|
||||
public function update(): Json { ... }
|
||||
|
||||
#[Log('用户管理-删除', Log::TYPE_DELETE)]
|
||||
public function delete(int $id): Json { ... }
|
||||
```yaml
|
||||
operationLog:
|
||||
enable: true # 总开关
|
||||
logQuery: true # 是否记录查询类(LogType=query)
|
||||
```
|
||||
|
||||
### 带描述占位符
|
||||
审计要求高的环境两个都开;日志量敏感时关 logQuery(写操作仍记录)。
|
||||
|
||||
```php
|
||||
#[Log('用户管理-删除记录', Log::TYPE_DELETE, '删除用户ID:{id}')]
|
||||
public function delete(int $id): Json { ... }
|
||||
## 不记录的请求
|
||||
|
||||
// 占位符 {id} 会被替换为 $request->param('id') 的值
|
||||
// 实际记录:"删除用户ID:42"
|
||||
被中间件链提前拒绝的请求不产生操作日志:限流拒绝、演示模式拒绝、未认证 401、租户拒绝、权限 403。这类拒绝在 metrics 中归类(denied/error),且运行日志有痕——避免日志表被攻击尝试刷爆。
|
||||
|
||||
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
|
||||
public function add(): Json { ... }
|
||||
## 归属地与 UA
|
||||
|
||||
// 实际记录:"新增用户:admin"
|
||||
条目含 IP 归属地(ip2region 离线库)与 User-Agent 解析——登录/操作审计可直接看出来源设备与地域。
|
||||
|
||||
#[Log('用户管理-批量删除', Log::TYPE_DELETE, '批量删除用户,ID列表:{ids}')]
|
||||
public function batchDelete(): Json { ... }
|
||||
## 消费
|
||||
|
||||
// 实际记录:"批量删除用户,ID列表:[1,2,3]"
|
||||
```
|
||||
- 管理界面:日志管理页按操作人/类型/时间检索(见 [6.20 日志管理](/zh/modules/logger.html))
|
||||
- 保留期:`job.logRetentionDays`(默认 90 天),LogClean 任务超期物理清理
|
||||
|
||||
### 导出操作(GET 也会记录)
|
||||
## 下一步
|
||||
|
||||
```php
|
||||
#[Log('用户管理-导出数据', Log::TYPE_EXPORT)]
|
||||
public function export(): Json { ... }
|
||||
|
||||
#[Log('用户管理-导入数据', Log::TYPE_IMPORT)]
|
||||
public function import(): Json { ... }
|
||||
```
|
||||
|
||||
:::warning GET 请求的记录策略
|
||||
GET 请求仅当注解类型为 `TYPE_EXPORT`、`TYPE_DOWNLOAD` 或 `TYPE_IMPORT` 时才会记录日志。普通查询(`TYPE_QUERY`)不会被记录,以避免大量查询淹没日志。
|
||||
:::
|
||||
|
||||
### 查询类型(GET 默认不记录)
|
||||
|
||||
```php
|
||||
// TYPE_QUERY 不会被记录(GET 请求默认不记录)
|
||||
#[Log('用户管理-查询分页', Log::TYPE_QUERY)]
|
||||
public function page(): Json { ... }
|
||||
|
||||
// 如果确实需要记录某个查询,可以使用 TYPE_OTHER
|
||||
#[Log('用户管理-敏感查询', Log::TYPE_OTHER)]
|
||||
public function sensitiveQuery(): Json { ... }
|
||||
```
|
||||
|
||||
### 与 Permission 注解组合
|
||||
|
||||
```php
|
||||
#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
|
||||
#[Permission('sys:user:add', '添加用户')]
|
||||
public function add(): Json { ... }
|
||||
|
||||
// 执行顺序:
|
||||
// 1. AuthMiddleware 校验权限(#[Permission])
|
||||
// 2. 权限通过后进入 Controller
|
||||
// 3. LogMiddleware 记录日志(#[Log])
|
||||
```
|
||||
|
||||
## 操作类型常量
|
||||
|
||||
| 常量 | 值 | 说明 | GET 是否记录 |
|
||||
|------|-----|------|-------------|
|
||||
| `Log::TYPE_ADD` | 1 | 新增 | — |
|
||||
| `Log::TYPE_UPDATE` | 2 | 修改 | — |
|
||||
| `Log::TYPE_DELETE` | 3 | 删除 | — |
|
||||
| `Log::TYPE_QUERY` | 4 | 查询 | ✗ |
|
||||
| `Log::TYPE_IMPORT` | 5 | 导入 | ✓ |
|
||||
| `Log::TYPE_EXPORT` | 6 | 导出 | ✓ |
|
||||
| `Log::TYPE_DOWNLOAD` | 7 | 下载 | ✓ |
|
||||
| `Log::TYPE_APPROVE` | 8 | 审批 | — |
|
||||
| `Log::TYPE_REJECT` | 9 | 驳回 | — |
|
||||
| `Log::TYPE_SUBMIT` | 10 | 提交 | — |
|
||||
| `Log::TYPE_WITHDRAW` | 11 | 撤回 | — |
|
||||
| `Log::TYPE_LOGIN` | 21 | 登录 | — |
|
||||
| `Log::TYPE_LOGOUT` | 22 | 登出 | — |
|
||||
| `Log::TYPE_RESET` | 24 | 重置 | — |
|
||||
| `Log::TYPE_OTHER` | 99 | 其他 | — |
|
||||
|
||||
完整常量列表见 `app/attribute/Log.php`(共 40 种)。
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/oper/log/page` | sys:oper:log:list | 分页列表 |
|
||||
| GET | `/api/oper/log/detail/:id` | sys:oper:log:detail | 详情 |
|
||||
| DELETE | `/api/oper/log/delete/:id` | sys:oper:log:delete | 删除 |
|
||||
| DELETE | `/api/oper/log/batchDelete` | sys:oper:log:delete | 批量删除 |
|
||||
|
||||
## 排除配置
|
||||
|
||||
```php
|
||||
// config/middleware.php
|
||||
'log_except' => [
|
||||
'login', // 登录日志单独记录在 login_log 表
|
||||
'logout',
|
||||
'captcha',
|
||||
'oauth2',
|
||||
'index/getMenus', // 获取菜单(频繁调用,不记录)
|
||||
'index/getUserInfo', // 获取用户信息
|
||||
],
|
||||
```
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/middleware/LogMiddleware.php
|
||||
|
||||
public function handle(Request $request, \Closure $next): Response
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$response = $next($request);
|
||||
|
||||
$path = $request->pathinfo();
|
||||
$exceptList = config('middleware.log_except', $this->except);
|
||||
|
||||
// 检查排除路由
|
||||
foreach ($exceptList as $except) {
|
||||
if (strpos($path, $except) !== false) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
$method = strtoupper($request->method());
|
||||
|
||||
// 获取注解信息
|
||||
$controllerClass = 'app\\controller\\' . $request->controller();
|
||||
$logAttr = AttributeService::getLog($controllerClass, $request->action());
|
||||
|
||||
// 判断是否需要记录
|
||||
$needLog = false;
|
||||
if (in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'])) {
|
||||
$needLog = true; // 写操作始终记录
|
||||
} elseif ($method === 'GET' && $logAttr) {
|
||||
$logType = $logAttr->type;
|
||||
if (in_array($logType, [Log::TYPE_EXPORT, Log::TYPE_DOWNLOAD, Log::TYPE_IMPORT])) {
|
||||
$needLog = true; // GET 仅记录导出/下载/导入
|
||||
}
|
||||
}
|
||||
|
||||
if (!$needLog) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
try {
|
||||
$consumeTime = round((microtime(true) - $startTime) * 1000);
|
||||
$userInfo = $request->userInfo ?? null;
|
||||
$requestInfo = RequestInfoService::create($request);
|
||||
|
||||
OperationLog::create([
|
||||
'title' => $logAttr?->title ?: $path,
|
||||
'type' => $logAttr?->type ?: $this->guessType($method),
|
||||
'method' => $method,
|
||||
'url' => $request->url(),
|
||||
'param' => mb_substr(json_encode($request->param()), 0, 2000),
|
||||
'result' => mb_substr($response->getContent(), 0, 2000),
|
||||
'ip' => $requestInfo->getIp(),
|
||||
'location' => $requestInfo->getIpLocation(),
|
||||
'os' => $requestInfo->getOs(),
|
||||
'browser' => $requestInfo->getBrowser(),
|
||||
'consume_time' => $consumeTime,
|
||||
'status' => $this->detectStatus($response->getContent()),
|
||||
'error' => $this->detectError($response->getContent()),
|
||||
'create_user' => $userInfo->username ?? '',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 日志记录失败不影响业务
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
```
|
||||
|
||||
## 日志查询与分析
|
||||
|
||||
### 按操作人查询
|
||||
|
||||
```text
|
||||
GET /api/oper/log/page?createUser=admin&pageNo=1&pageSize=20
|
||||
```
|
||||
|
||||
### 按操作类型查询
|
||||
|
||||
```text
|
||||
GET /api/oper/log/page?type=3&pageNo=1&pageSize=20
|
||||
(查询所有删除操作)
|
||||
```
|
||||
|
||||
### 按时间范围查询
|
||||
|
||||
```text
|
||||
GET /api/oper/log/page?startTime=2026-09-01&endTime=2026-09-22
|
||||
```
|
||||
|
||||
### 按状态查询(查异常操作)
|
||||
|
||||
```text
|
||||
GET /api/oper/log/page?status=1
|
||||
(查询所有失败的操作)
|
||||
```
|
||||
|
||||
## 日志保留策略
|
||||
|
||||
| 策略 | 说明 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| 按时间清理 | 定期删除 N 天前的日志 | 大多数场景 |
|
||||
| 按数量保留 | 只保留最近 N 条 | 日志量大 |
|
||||
| 归档到冷存储 | 导出到文件/对象存储 | 合规要求 |
|
||||
|
||||
```sql
|
||||
-- 示例:清理 90 天前的操作日志
|
||||
DELETE FROM think_operation_log WHERE create_time < DATE_SUB(NOW(), INTERVAL 90 DAY);
|
||||
|
||||
-- 示例:按月归档
|
||||
SELECT * FROM think_operation_log
|
||||
WHERE create_time >= '2026-09-01' AND create_time < '2026-10-01'
|
||||
INTO OUTFILE '/backup/operation_log_202609.csv';
|
||||
```
|
||||
|
||||
## 与登录日志的区别
|
||||
|
||||
| 维度 | 操作日志 | 登录日志 |
|
||||
|------|---------|---------|
|
||||
| 表 | `think_operation_log` | `think_login_log` |
|
||||
| 记录方 | LogMiddleware 自动记录 | LoginLogic 手动记录 |
|
||||
| 记录内容 | 所有写操作 + 导出/导入 | 仅登录/登出 |
|
||||
| 触发方式 | `#[Log]` 注解 | 代码中调用 `recordLoginLog()` |
|
||||
| 用途 | 业务操作审计 | 登录安全审计 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
| 实践 | 说明 |
|
||||
|------|------|
|
||||
| 标题语义化 | 使用"模块-操作"格式,如"用户管理-新增记录" |
|
||||
| 描述加关键参数 | 用 `{param}` 占位符记录业务主键,便于追溯 |
|
||||
| 敏感操作必须标注 | 删除、重置密码、权限变更等操作必须加 `#[Log]` |
|
||||
| 查询不记录 | 普通查询不加 `#[Log]` 或使用 `TYPE_QUERY`(不记录) |
|
||||
| 定期清理 | 避免日志表无限增长影响性能 |
|
||||
| 排除高频接口 | 将频繁调用的接口加入 `log_except` |
|
||||
→ [5.15 登录日志](/zh/core/login-log.html)
|
||||
|
||||
Reference in New Issue
Block a user