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

Laravel Backpack导入CSV时请求超时问题排查与解决咨询

解决Laravel Backpack 5 CSV导入超时与实时进度显示问题

问题根源

你遇到的浏览器超时,是因为Web服务器(Apache/Nginx)的连接超时限制(默认通常是2分钟)先切断了前端请求,而PHP进程之所以还能继续后台处理,是因为PHP的max_execution_time配置生效了,但Web服务器已经断开了与浏览器的连接,导致前端显示超时错误。

解决方案:队列异步处理 + 实时进度查询

要实现任务按需运行且显示实时进度,核心是把耗时的导入逻辑从同步请求中剥离,用Laravel队列异步执行,同时通过缓存记录进度,前端轮询获取进度数据。

1. 创建异步队列任务

首先生成队列任务类,将CSV导入、Google Places验证逻辑移到任务中:

php artisan make:job ImportContactsJob

编辑app/Jobs/ImportContactsJob.php:

<?php

namespace App\Jobs;

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\Cache;
use Illuminate\Support\Facades\Storage;
use League\Csv\Reader;
use GuzzleHttp\Client;

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

    protected $filePath;
    protected $jobId;

    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
        $this->jobId = $this->uuid();
    }

    public function handle()
    {
        // 读取CSV文件
        $csv = Reader::createFromPath(Storage::path($this->filePath), 'r');
        $csv->setHeaderOffset(0);
        $records = $csv->getRecords();
        $total = iterator_count($records);
        $processed = 0;

        // 初始化进度缓存
        Cache::put("import_progress_{$this->jobId}", [
            'total' => $total,
            'processed' => $processed,
            'status' => 'processing'
        ], now()->addHours(2));

        // 重新读取CSV(迭代器被count消耗后需重置)
        $csv = Reader::createFromPath(Storage::path($this->filePath), 'r');
        $csv->setHeaderOffset(0);

        foreach ($csv->getRecords() as $record) {
            // Google Places API地址验证
            $client = new Client();
            try {
                $response = $client->get('https://maps.googleapis.com/maps/api/place/findplacefromtext/json', [
                    'query' => [
                        'input' => $record['address'],
                        'inputtype' => 'textquery',
                        'fields' => 'formatted_address,address_components',
                        'key' => env('GOOGLE_PLACES_API_KEY')
                    ]
                ]);
                $placeData = json_decode($response->getBody(), true);
                
                // 处理地址组件并写入数据库
                // 替换为你的实际入库逻辑
                // Contact::create([...]);
            } catch (\Exception $e) {
                // 处理API请求失败逻辑
                \Log::error('地址验证失败:'.$e->getMessage());
            }

            // 更新进度
            $processed++;
            Cache::put("import_progress_{$this->jobId}", [
                'total' => $total,
                'processed' => $processed,
                'status' => 'processing'
            ], now()->addHours(2));

            // 避免API频率限制,添加0.5秒延迟
            usleep(500000);
        }

        // 标记任务完成
        Cache::put("import_progress_{$this->jobId}", [
            'total' => $total,
            'processed' => $processed,
            'status' => 'completed'
        ], now()->addHours(2));

        // 删除临时CSV文件
        Storage::delete($this->filePath);
    }

    public function failed(\Throwable $exception)
    {
        // 标记任务失败并记录错误
        Cache::put("import_progress_{$this->jobId}", [
            'status' => 'failed',
            'error' => $exception->getMessage()
        ], now()->addHours(2));
        Storage::delete($this->filePath);
    }
}

2. 修改控制器导入逻辑

在Backpack控制器中,将同步导入改为分发队列任务:

use App\Jobs\ImportContactsJob;
use Illuminate\Support\Facades\Storage;

public function import(Request $request)
{
    $request->validate([
        'csv_file' => 'required|mimes:csv,txt',
    ]);

    // 保存上传的CSV到临时目录
    $file = $request->file('csv_file');
    $tempPath = 'temp/'.uniqid().'.'.$file->getClientOriginalExtension();
    $file->storeAs('temp', $tempPath);

    // 分发队列任务
    $job = new ImportContactsJob($tempPath);
    dispatch($job);

    return response()->json([
        'job_id' => $job->jobId,
        'message' => '导入任务已启动,正在后台处理'
    ]);
}

3. 实现实时进度查询

添加API路由用于查询进度:

// routes/api.php
Route::get('/import-progress/{jobId}', [YourContactController::class, 'getImportProgress']);

控制器中添加进度查询方法:

use Illuminate\Support\Facades\Cache;

public function getImportProgress(string $jobId)
{
    $progress = Cache::get("import_progress_{$jobId}");
    
    if (!$progress) {
        return response()->json(['status' => 'not_found'], 404);
    }
    
    return response()->json($progress);
}

4. 前端进度条实现

在Backpack的导入视图中添加前端逻辑,通过AJAX轮询进度:

<form id="importForm" enctype="multipart/form-data">
    <input type="file" name="csv_file" accept=".csv" required>
    <button type="submit" class="btn btn-primary">开始导入</button>
</form>

<div id="progressContainer" style="display: none; margin-top: 20px;">
    <div>当前进度:<span id="progressPercent">0%</span></div>
    <div class="progress" style="height: 20px; margin: 10px 0;">
        <div id="progressBar" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
    </div>
    <div id="statusText" class="text-muted"></div>
</div>

<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script>
    $('#importForm').submit(function(e) {
        e.preventDefault();
        let formData = new FormData(this);
        
        $.ajax({
            url: '{{ route("your-import-route-name") }}',
            method: 'POST',
            data: formData,
            processData: false,
            contentType: false,
            headers: {
                'X-CSRF-TOKEN': '{{ csrf_token() }}'
            },
            success: function(res) {
                $('#progressContainer').show();
                startProgressPoll(res.job_id);
            }
        });
    });

    function startProgressPoll(jobId) {
        let interval = setInterval(() => {
            $.ajax({
                url: `/api/import-progress/${jobId}`,
                method: 'GET',
                success: function(progress) {
                    let percent = Math.round((progress.processed / progress.total) * 100);
                    $('#progressPercent').text(percent + '%');
                    $('#progressBar').css('width', percent + '%');
                    $('#statusText').text('状态:' + progress.status);

                    // 任务完成或失败时停止轮询
                    if (progress.status === 'completed' || progress.status === 'failed') {
                        clearInterval(interval);
                        if (progress.status === 'failed') {
                            $('#statusText').addClass('text-danger').text('导入失败:' + progress.error);
                        }
                    }
                },
                error: function() {
                    clearInterval(interval);
                    $('#statusText').addClass('text-danger').text('无法获取进度信息');
                }
            });
        }, 1000); // 每秒查询一次进度
    }
</script>

5. 队列配置与启动

  • 确保队列驱动配置正确(推荐用Redis或Database,不要用sync),在.env中设置:
    QUEUE_CONNECTION=redis
    
  • 启动队列Worker,设置足够长的超时时间:
    php artisan queue:work --timeout=3600 # 超时1小时,根据需求调整
    
  • 若使用cPanel,可通过Supervisor配置守护进程,确保Worker持续运行。

关键说明

  • 之前的set_time_limit、max_execution_time等设置只影响PHP进程时长,无法绕过Web服务器的连接超时限制,队列异步处理从根源上避免了这个问题。
  • Google Places API有请求频率限制,需根据配额调整延迟时间,或申请提升API配额。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 09:13:10