问题根源在于首次失败未被监听,导致批处理监控链路未激活;需确保队列驱动正确、job 引入 batchable trait、handle 中检查 batch 上下文、failed 方法中显式调用 markasfailed()。

当你在 Laravel 8 中调用 Bus::batch() 启动一批任务,却始终看不到 job_batches 表中写入记录、前端无法查进度、then() 或 catch() 回调也不触发,问题往往出在第一批任务失败时未被监听——因为 Laravel 批处理的整个生命周期依赖首次失败检测来激活监控链路,漏掉这一步,后续所有状态更新都会静默失效。
为什么首次失败必须被监听
批处理不是靠轮询或定时器驱动状态更新,而是靠每个 Job 在执行过程中主动上报状态。当第一个 Job 抛出异常且未被捕获,Laravel 才会触发 job_batches 表的 failed_jobs 字段更新,并将该 batch 标记为「已进入失败路径」。此后所有同批次 Job 的 $this->batch()->incrementTotalJobs() 和 decrementPendingJobs() 才会被框架认可并落库。
若首个失败 Job 被 try/catch 吞掉、或 failed() 方法里没调用 $this->batch()->markAsFailed(),则 job_batches.failed_job_ids 字段保持空字符串,整个批次卡在 pending_jobs = total_jobs 状态,progress() 永远返回 0。
确保首次失败被监听的实操步骤
第一步:确认队列驱动为 database 或 redis,且 QUEUE_CONNECTION 环境变量已正确设置——【QUEUE_CONNECTION=sync 会导致 Bus::batch() 完全跳过数据库写入,不生成 batch_id】。
第二步:检查每个 Job 类是否引入了 Illuminate\Bus\Batchable trait。缺这个 trait,Job 在构建 Batch 时会被忽略,整个批次对象创建失败但不报错。
第三步:在 Job 的 handle() 方法开头插入状态检查:
if (! $batch = $this->batch()) { return; }
这行代码必须存在——它防止该 Job 被手动 dispatch 或重试时脱离批次上下文,否则 $batch->markAsFailed() 会抛出 Call to a member function markAsFailed() on null 异常,导致失败被吞掉。
第四步:在 Job 的 failed() 方法中,显式调用 $this->batch()->markAsFailed(); 并传入当前 Job ID:
public function failed($exception) { if ($batch = $this->batch()) { $batch->markAsFailed($this->job->getJobId()); } Log::error('Batch job failed', ['job' => get_class($this), 'exception' => $exception->getMessage()]); }
验证监听是否生效的两个关键检查点
方法一:直接查表。执行一次失败任务后,立刻运行 SELECT * FROM job_batches WHERE id = 'your_batch_id';,确认 failed_job_ids 字段已变成类似 "[\"123\",\"456\"]" 的 JSON 字符串——注意是字符串不是数组,PHP 中必须用 json_decode($batch->failed_job_ids, true) 解析。
方法二:观察日志。启动 php artisan queue:work --verbose,当首个 Job 失败时,控制台应输出 [Batch] Marking batch {id} as failed。如果没有这行,说明 markAsFailed() 未被执行,回溯检查 failed() 方法是否被跳过或逻辑有误。
方法三:测试回调。在 Bus::batch()->catch() 回调里加一行 Log::info('Batch caught failure');,只有首次失败被监听后,这行日志才会出现。











