Files
2026-09-29 09:49:04 +08:00

11 KiB
Raw Permalink Blame History

5.14 操作日志

:::tip 概述 操作日志通过 #[Log] 注解 + LogMiddleware 自动记录,无需手动埋点。所有写操作(POST/PUT/DELETE/PATCH)始终记录,GET 请求仅当注解类型为导出/下载/导入时记录。日志写入失败不影响主业务流程。 :::

记录流程

请求进入
    │
    ▼
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 包裹,失败不影响主业务)

记录字段

字段 说明 来源 示例
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

注解使用

基本用法

// 写操作(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 { ... }

带描述占位符

#[Log('用户管理-删除记录', Log::TYPE_DELETE, '删除用户ID:{id}')]
public function delete(int $id): Json { ... }

// 占位符 {id} 会被替换为 $request->param('id') 的值
// 实际记录:"删除用户ID:42"

#[Log('用户管理-新增记录', Log::TYPE_ADD, '新增用户:{username}')]
public function add(): Json { ... }

// 实际记录:"新增用户:admin"

#[Log('用户管理-批量删除', Log::TYPE_DELETE, '批量删除用户,ID列表:{ids}')]
public function batchDelete(): Json { ... }

// 实际记录:"批量删除用户,ID列表:[1,2,3]"

导出操作(GET 也会记录)

#[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 默认不记录)

// TYPE_QUERY 不会被记录(GET 请求默认不记录)
#[Log('用户管理-查询分页', Log::TYPE_QUERY)]
public function page(): Json { ... }

// 如果确实需要记录某个查询,可以使用 TYPE_OTHER
#[Log('用户管理-敏感查询', Log::TYPE_OTHER)]
public function sensitiveQuery(): Json { ... }

与 Permission 注解组合

#[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 批量删除

排除配置

// config/middleware.php
'log_except' => [
    'login',      // 登录日志单独记录在 login_log 表
    'logout',
    'captcha',
    'oauth2',
    'index/getMenus',    // 获取菜单(频繁调用,不记录)
    'index/getUserInfo', // 获取用户信息
],

核心代码

// 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;
}

日志查询与分析

按操作人查询

GET /api/oper/log/page?createUser=admin&pageNo=1&pageSize=20

按操作类型查询

GET /api/oper/log/page?type=3&pageNo=1&pageSize=20
(查询所有删除操作)

按时间范围查询

GET /api/oper/log/page?startTime=2026-09-01&endTime=2026-09-22

按状态查询(查异常操作)

GET /api/oper/log/page?status=1
(查询所有失败的操作)

日志保留策略

策略 说明 适用场景
按时间清理 定期删除 N 天前的日志 大多数场景
按数量保留 只保留最近 N 条 日志量大
归档到冷存储 导出到文件/对象存储 合规要求
-- 示例:清理 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