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
相关产品推荐
相关产品推荐

