You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel多PDF生成进程异常终止问题求助

这个批量PDF生成进程无法终止的问题确实挺头疼的——用户关了浏览器、中途出异常,服务器还在闷头生成文件,既浪费资源又可能产生垃圾文件。我给你几个实用的解决方案,从快速改造到生产级方案都有:

解决方案1:队列任务+取消令牌(推荐)

把同步执行的PDF生成逻辑改成异步队列任务,同时给每个任务绑定一个「取消令牌」,这样不管是用户主动关闭页面还是中途异常,都能通过令牌终止任务,还能避免长时间占用前端请求连接。

步骤1:创建带取消令牌的队列任务

新建一个队列任务类,在每次生成PDF前检查令牌是否有效:

<?php

namespace App\Jobs;

use Barryvdh\DomPDF\Facade as PDF;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Redis;

class GeneratePdfBatch implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $data;
    protected $cancelToken;
    protected $pdfDir;

    public function __construct($data, $cancelToken)
    {
        $this->data = $data;
        $this->cancelToken = $cancelToken;
        $this->pdfDir = public_path('storage/pdf');
    }

    public function handle()
    {
        // 确保PDF存储目录存在
        if (!file_exists($this->pdfDir)) {
            mkdir($this->pdfDir, 0755, true);
        }

        $landscape = 'portrait'; // 根据你的业务逻辑赋值

        foreach ($this->data as $datum) {
            // 每次循环前检查是否已取消
            if ($this->isTaskCanceled()) {
                $this->cleanupGeneratedFiles();
                return;
            }

            // 生成并保存PDF
            $pdf = PDF::loadView('pdf.index', compact('datum'))->setPaper('a4', $landscape);
            $filePath = $this->pdfDir . '/' . $datum->name . '.pdf';
            $pdf->save($filePath);
        }

        // 所有PDF生成完成后,执行压缩和清理
        $this->createZipArchive();
        $this->cleanupGeneratedFiles();
    }

    /**
     * 检查任务是否已被取消
     * 用Redis存储令牌状态,高效且支持过期清理
     */
    protected function isTaskCanceled(): bool
    {
        return !Redis::exists("pdf_batch:{$this->cancelToken}");
    }

    /**
     * 清理已生成的PDF文件
     */
    protected function cleanupGeneratedFiles(): void
    {
        Storage::disk('public')->deleteDirectory('pdf');
    }

    /**
     * 生成压缩包
     */
    protected function createZipArchive(): void
    {
        $zipFile = public_path('storage/zipped.zip');
        $zip = new \ZipArchive();

        if ($zip->open($zipFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE)) {
            $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->pdfDir));
            foreach ($files as $file) {
                if (!$file->isDir()) {
                    $relativePath = 'pdf/' . substr($file->getRealPath(), strlen($this->pdfDir) + 1);
                    $zip->addFile($file->getRealPath(), $relativePath);
                }
            }
            $zip->close();
        }
    }
}

步骤2:前端与后端交互逻辑

  • 前端生成唯一取消令牌,发起任务请求,并监听页面关闭事件发送取消信号:
// 生成唯一取消令牌(可以用uuid库或自定义生成)
const cancelToken = crypto.randomUUID();

// 启动批量PDF生成任务
axios.post('/api/start-pdf-batch', {
    cancelToken: cancelToken,
    data: yourBatchData // 你的批量数据
}).then(res => {
    // 可以轮询任务状态,准备下载压缩包
});

// 用户关闭页面时,发送取消请求
window.addEventListener('beforeunload', () => {
    axios.post('/api/cancel-pdf-batch', { cancelToken: cancelToken });
});
  • 后端路由处理任务启动和取消:
use App\Jobs\GeneratePdfBatch;
use Illuminate\Support\Facades\Redis;

// 启动批量PDF任务
Route::post('/api/start-pdf-batch', function (Request $request) {
    $cancelToken = $request->cancelToken;
    // 存储令牌到Redis,设置1小时过期(避免遗留无效令牌)
    Redis::setex("pdf_batch:{$cancelToken}", 3600, 'active');
    // 分发队列任务
    GeneratePdfBatch::dispatch($request->data, $cancelToken);
    return response()->json(['message' => '任务已启动', 'cancelToken' => $cancelToken]);
});

// 取消批量PDF任务
Route::post('/api/cancel-pdf-batch', function (Request $request) {
    Redis::del("pdf_batch:{$request->cancelToken}");
    return response()->json(['message' => '任务已取消']);
});
解决方案2:同步脚本的连接中断检测(快速改造)

如果暂时不想用队列,可以修改现有同步代码,让PHP检测用户连接是否中断,一旦中断就终止进程并清理文件:

use Barryvdh\DomPDF\Facade as PDF;
use Illuminate\Support\Facades\Storage;

// 告诉PHP:用户断开连接时不要继续执行脚本
ignore_user_abort(false);

$landscape = (格式为landscape时赋值'landscape',否则为'portrait');
$pdfDir = public_path('storage/pdf');

if (!file_exists($pdfDir)) {
    mkdir($pdfDir, 0755, true);
}

foreach ($data as $datum) {
    // 检查用户是否已断开连接
    if (connection_aborted()) {
        // 清理已生成的文件
        Storage::disk('public')->deleteDirectory('pdf');
        exit;
    }

    $pdf = PDF::loadView('pdf.index', compact('datum'))->setPaper('a4', $landscape);
    $file = $pdfDir.'/'.$datum->name.'.pdf';
    $pdf->save($file);

    // 刷新输出缓冲区,确保PHP能及时检测到连接中断
    ob_flush();
    flush();
}

// 压缩文件与删除文件夹代码...

⚠️ 注意:这个方法在反向代理(如Nginx)环境下可能不太可靠,因为代理可能会保持连接一段时间,导致connection_aborted()无法及时触发。

解决方案3:超时兜底控制

不管用队列还是同步脚本,都可以设置超时时间,防止进程因异常无限运行:

  • 队列任务中设置超时:
class GeneratePdfBatch implements ShouldQueue
{
    public $timeout = 300; // 5分钟超时,自动终止任务
    // ...其他代码
}
  • 同步脚本中设置超时:
// 设置脚本最大执行时间为5分钟(300秒)
set_time_limit(300);

内容的提问来源于stack exchange,提问作者nazhannasir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 07:59:49