订单日志应异步写入独立结构化数据库表,通过队列处理避免阻塞主事务;监听模型updated事件捕获真实状态变更;data字段需包含operator_id、ip、previous、current、source等上下文;务必建立order_id、event+created_at等关键索引以保障查询性能。

订单日志该写到哪里:数据库 vs 文件 vs 队列
直接往数据库表里 insert 日志,看似简单,但高并发下单时容易拖慢主事务。Laravel 默认的 Log 门面写文件(如 storage/logs/laravel.log)又难检索、无结构、不带订单上下文。
更合理的做法是:用独立数据表存结构化日志,并通过队列异步写入。这样既保证主流程不卡顿,又能按 order_id、status、created_at 快速查。
- 建表迁移示例:
php artisan make:migration create_order_logs_table
,表中至少包含order_id(索引)、event(如"paid"、"shipped")、data(json类型,存变动字段、操作人、IP 等) - 不要在控制器或模型的
save()方法里同步DB::insert()—— 这会把日志错误拖垮订单创建 - 用
dispatch(new LogOrderEvent($order, 'created', $context))推进队列,Job 内再持久化
怎么记录关键状态变更:监听模型事件比手动写更可靠
订单状态不是只在“支付成功”时变,还可能被客服改、被超时任务关、被退款回调触发。硬编码日志调用极易漏场景。
Laravel 模型事件(updating、updated、saved)配合守卫逻辑,能自动捕获所有变更点:
立即学习“PHP免费学习笔记(深入)”;
- 在
App\Models\Order中定义protected $dispatchesEvents = ['updated' => OrderUpdated::class] -
OrderUpdated事件类里判断$event->getOriginal('status') !== $event->status,只对真实变更写日志 - 避免重复日志:不要监听
saving或creating—— 它们发生在事务提交前,若后续回滚,日志就失真
日志内容要带什么字段:别只记“做了什么”,要记“谁在什么时候为什么做”
光写 "order #123 status changed to shipped" 没用。排查问题时需要还原现场。
建议 data 字段固定存这些键:
-
operator_id:当前操作人 ID(从Auth::id()或请求头取;后台操作必须有,接口回调可设为null或"system") -
ip:request()->ip(),非回调场景必填 -
previous和current:状态/金额/物流单号等关键字段变更前后值,方便 diff -
source:标识来源,如"wechat_payment_callback"、"admin_panel"、"cron_timeout_job"
示例写入结构:
["operator_id" => 42, "ip" => "192.168.1.100", "previous" => ["status" => "pending"], "current" => ["status" => "paid"], "source" => "alipay_callback"]
查日志时总卡死:索引和查询方式得提前设计好
线上跑三个月后,order_logs 表几十万行,where order_id = ? 还快,但 where event = 'refunded' and created_at > ? 就慢——因为缺联合索引。
必须加的索引:
-
order_id单列索引(查单个订单全生命周期) -
event+created_at联合索引(查某类事件近期分布) - 如果常按操作人查,加
operator_id索引(但注意 NULL 值不进 B-Tree 索引,回调日志里设"system"比留空好)
别用 LIKE '%shipped%' 搜 data 字段——JSON 字段应走 -> 操作符,例如:
whereJsonContains('data->source', 'admin_panel'),并给 data 字段加生成列+索引(MySQL 5.7+ 支持)
实际最难的不是写日志,是定义清楚哪些变更算“值得记”。比如地址微调、备注修改要不要进日志?这得和产品、客服对齐,而不是由开发拍脑袋决定。











