必须用 app_init + action_begin + action_end 三阶段组合捕获接口调用前后异常操作,单靠 app_end 会漏掉路由失败、中间件异常等404/401/500请求。

ThinkPHP里怎么记录用户行为序列到接口调用上下文
直接说结论:不能靠 $this->request 或 session() 单点存取撑起行为序列,得主动构造带时间戳、操作类型、来源路径的结构化日志,并绑定请求生命周期。
常见错误是把行为当普通日志打在文件里,结果查不到上下文关联——比如用户 A 在 3 秒内连续调用 /api/v1/order/create、/api/v1/payment/pay、/api/v1/order/status,但每条日志孤立,没法还原链路。
- 用中间件统一拦截接口请求,在
handle()开始时生成唯一$traceId,注入到think\Request的扩展属性(如$request->setAttr('trace_id', $traceId)) - 每个业务方法里手动追加行为:调用
Log::record()时带上['trace_id' => $request->attr('trace_id'), 'action' => 'create_order', 'ts' => time()] - 别用
session()存行为数组——并发请求会覆盖,且 CLI 或跨域场景 session 不可靠 - 如果用 Redis 做临时行为缓存,key 必须含
trace_id和用户 ID,过期时间设为 60 秒,避免堆积
用哪个钩子捕获 ThinkPHP 接口调用前后的异常操作模式
答案很明确:必须用 app_init + action_begin + action_end 三阶段组合,单靠 app_end 拦不住未进控制器就崩掉的请求(比如路由匹配失败、中间件抛异常)。
典型踩坑是只监听 action_end,结果漏掉大量 404、401、500 类请求,而这些恰恰是暴力探测或越权尝试的高发行为。
立即学习“PHP免费学习笔记(深入)”;
-
app_init阶段做全局 trace 初始化和 IP/UA 采集 -
action_begin记录接口名、参数摘要(注意脱敏password、token等字段)、请求头中的X-Forwarded-For -
action_end补充响应码、耗时、是否抛出异常;若$response->getCode() >= 400,立即触发异常模式检测逻辑 - 异常模式判断别写死阈值,比如“1 秒内 5 次失败”,而是用滑动窗口(Redis
ZSET+zremrangebyscore)动态算
ThinkPHP 日志格式怎么改才能支撑行为序列分析
默认日志格式不带 trace_id、不结构化、不分 level 细粒度,根本没法做聚合查询。必须重写 think\log\driver\File 的 write() 方法,或更稳妥地用自定义日志通道。
直接改原生驱动容易被升级覆盖,推荐新建 app\common\log\BehaviorLog 类,继承 think\log\driver\File,覆盖 write(),确保每条输出是 JSON 行格式(JSON Lines)。
- 每行必须含:
trace_id、user_id(登录态取$this->auth->id,未登录填guest)、action、method、path、status_code、cost_ms、ts - 禁止在日志里拼接敏感字段,
input()结果要先用Arr::except($data, ['password', 'old_password'])过滤 - 别用
Log::info()打行为日志——它走的是默认通道,格式不可控;统一走Log::channel('behavior')->info() - 日志文件按天切分,但文件名里加哈希后缀(如
behavior_20240520_abc123.log),避免多进程写入冲突
为什么用 Db::table()->insertAll() 写行为日志会拖慢接口
因为每次 insertAll 都是一次独立事务 + 网络往返,10 条行为 = 10 次 MySQL 请求,QPS 上不去还容易锁表。真实生产环境必须异步落库或批量缓冲。
不是不能用数据库存行为,而是不能“每条都实时插”。尤其高频接口(如心跳、轮询),同步写库等于自废武功。
- 用 Redis
LPUSH behavior_queue先收拢,再由单独的命令行任务(php think behavior:sync)定时LRANGE + INSERT IGNORE批量刷库 - 如果非得实时写,至少用
Db::transaction()包住一批行为(比如 20 条/次),而不是单条 commit - MySQL 表引擎必须是
InnoDB,字段加联合索引(user_id, trace_id, ts),否则查某用户最近 10 条行为会全表扫 - 别在日志表里建外键关联用户表——写放大严重,且行为日志本身就应该允许用户已注销但仍可查历史
最麻烦的其实是时间精度和时区。所有 ts 字段必须用 microtime(true) 记录秒级浮点数,入库前转成 datetime(3),并确认 MySQL 的 time_zone 和 PHP 的 date_default_timezone_set() 一致,否则序列时间线错乱,分析就全偏了。


















