后台操作日志和SQL审计必须分开:前者需记录人、IP、动作语义,由中间件在路由后控制器前拦截实现;后者需记录bind、type、真实耗时,依赖Db::listen()或MySQL原生慢日志。

后台操作日志和 SQL 审计必须分开做,不能指望一个 Db::listen() 或一个中间件全搞定——前者要带人、IP、动作语义,后者要带 bind、type、真实耗时,混在一起必然丢字段、漏场景、查不出来。
为什么不能只用 Db::listen() 记后台操作
Db::listen() 只能拿到 $sql、$bind、$time 和 $type,它压根不知道这次 SQL 是谁发的、在哪个控制器里、参数里有没有删用户 ID。常见错误现象包括:
- 日志里全是
"DELETE FROM user WHERE id = ?",但没写是管理员 A 在 /admin/user/delete 页面点的 - 事务里执行了 5 条 UPDATE,Db::listen() 全部触发,但中间件只记一次请求,两边日志对不上
- 登录接口也触发了 Db::listen()(比如查用户密码),被当成“后台操作”塞进审计表,污染数据
所以 Db::listen() 只适合做 SQL 层面的“数据库变更审计”,不是“后台行为审计”。
如何用中间件准确记录后台操作(含敏感操作识别)
真正能串起人、动作、参数、结果的,只有中间件——且必须在路由解析后、控制器执行前拦截。实操要点:
立即学习“PHP免费学习笔记(深入)”;
- 在
app/middleware/AdminLogMiddleware.php的handle()方法里,用Auth::id()拿操作人,失败时 fallback 到$request->header('X-User-ID')(需校验签名) - 用
$request->rule()->getRuleName()匹配敏感规则白名单,例如in_array($rule, ['admin/user/delete', 'admin/role/save']) - 调
$request->param()后立刻过滤敏感键:array_filter($params, function($k) { return !in_array($k, ['password', 'token', 'old_password']); }, ARRAY_FILTER_USE_KEY) - 对 DELETE/UPDATE 类操作,在
after()钩子里捕获异常,并用$response->getData()['code'] ?? 500判断业务是否成功(别信$response->getCode()) - 日志写入必须走
Log::channel('admin'),且config/log.php中该 channel 的type应设为database或think,不能用默认single
怎么让 SQL 日志带参数和耗时(非慢查询专用)
ThinkPHP 6 默认不输出带 bind 的 SQL,Db::getLastSql() 返回的是占位符版本,直接拼字符串会丢类型转换细节。正确做法:
- 开启配置:
'db' => ['debug' => true, 'log_sql' => true]和'log' => ['level' => ['sql']],这会让框架自动把每条 SQL + bind 写进日志文件 - 若需结构化入库(如进 Elasticsearch),改用
Db::listen(),回调函数签名必须是function($sql, $bind, $time, $type)(TP6.0+) -
$time是毫秒级浮点数,单位是秒,别误当毫秒处理;$type是小写字符串,值为select/insert/update/delete,可据此过滤掉健康检查类查询 - 生产环境禁用手动拼接最终 SQL:
str_replace(['?', '?'], array_map('json_encode', $bind), $sql)这种操作性能差、易出错,仅限本地调试
慢 SQL 必须靠 MySQL 原生日志,PHP 层只能打点
所有 PHP 层的 microtime(true) 或 Db::listen() 耗时,都不等于真实数据库耗时——它不含连接池等待、锁竞争、主从延迟。真正可靠的慢查询监控只有一个入口:
- MySQL 必须开启
slow_query_log = ON,并设long_query_time = 1.0(支持小数) - 确认
/var/log/mysql/mysql-slow.log目录属主是mysql用户且可写,否则日志静默失败 - ThinkPHP 的
slow_query_time配置只是个开关,它不接管 slow log,只决定是否在框架日志里额外打一条 warning - 如果服务器不允许改 MySQL 配置,唯一退路是
Db::listen()+ 自定义阈值告警,但得接受“告警滞后、漏报率高”的事实
最常被忽略的一点:SQL 审计日志和慢查询日志的存储路径、轮转策略、权限控制必须分开配置,混用同一个日志通道会导致审计字段被截断、慢查询堆满磁盘、或因权限问题整批丢失。



















