Laravel带PDF附件的任务队列本地正常线上报错求助
问题:生产环境下邮件发送PDF时提示无法打开路径,但文件已生成
业务场景:支持用户导出PDF到本地,也支持通过邮件发送PDF;由于PDF生成耗时较长,使用队列异步处理邮件发送逻辑。本地环境运行正常,但生产环境抛出以下错误:
Symfony\Component\Mime\Exception\InvalidArgumentException: Unable to open path "https://my-website/public/reports/products-report-1693859875.pdf". in /home/my_website/public_html/vendor/symfony/mime/Part/TextPart.php:154
实际PDF文件已生成,推测是邮件发送时PDF文件尚未完成写入,或路径处理存在问题。尝试过Job Batching和单独拆分PDF生成任务,均未解决。
相关代码
控制器代码
class ReportsController extends Controller { public function sendByEmail(Request $request) { $report = (new ReportService) ->setStartAt($request->start_at) ->setEndAt($request->end_at) ->setProducts($request->product) ->sendByEmail(); return response()->json([], 200); } }
ReportService代码
class ReportService { ..... /** * 分发发送报告邮件的任务 */ public function sendByEmail(): void { dispatch(new \App\Jobs\SendReportByEmail($this)); } /** * 下载/导出文件 */ public function download(): string|Exception { $url = (new ReportProductService) ->setReportService($this) ->download(); return $url; } }
SendReportByEmail任务代码
class SendReportByEmail implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; /** * 创建任务实例 */ public function __construct(public ReportService $reportService) { } /** * 执行任务 */ public function handle(): void { // 在任务中生成PDF文件 $url = $this->reportService->download(); \Mail::queue((new \App\Mails\Report($this->reportService, $url))); } }
解决方案
1. 确保PDF文件完全写入后再执行邮件逻辑
生产环境磁盘IO速度可能慢于本地,download()返回URL时,文件可能仍在写入过程中。可以在ReportProductService::download()中确保文件写入完成:
- 使用
file_put_contents后调用fflush()强制写入磁盘 - 或者检查文件大小稳定后再返回路径
另外,在任务中添加文件存在性验证和等待逻辑:
public function handle(): void { // 将URL转换为本地绝对路径 $localPath = str_replace('https://my-website/public/', public_path('/'), $this->reportService->download()); // 验证文件存在 if (!file_exists($localPath)) { $this->fail(new \Exception("PDF文件不存在: {$localPath}")); return; } // 等待文件写入完成(最多等待5秒) $maxWait = 5; $currentSize = filesize($localPath); for ($i = 0; $i < $maxWait; $i++) { sleep(1); $newSize = filesize($localPath); if ($newSize === $currentSize) { break; } $currentSize = $newSize; } // 发送邮件时使用本地路径添加附件 \Mail::queue((new \App\Mails\Report($this->reportService))->attach($localPath)); }
2. 使用本地文件路径而非HTTP URL
Symfony Mime组件处理远程URL时可能存在延迟或权限问题,直接传递本地绝对路径给邮件类更可靠:
- 修改
ReportService::download()返回本地路径(如public_path('reports/xxx.pdf')) - 邮件类中使用
attach()或attachFromPath()添加本地文件作为附件
3. 用任务链拆分PDF生成与邮件发送
将逻辑拆分为两个独立任务,确保PDF生成完成后再触发邮件发送:
// ReportService中修改sendByEmail方法 public function sendByEmail(): void { dispatch(new GeneratePdfJob($this->startAt, $this->endAt, $this->products)) ->chain([ new SendReportByEmailJob($this->startAt, $this->endAt, $this->products), ]); } // GeneratePdfJob 负责生成PDF class GeneratePdfJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public string $startAt, public string $endAt, public array $products ) { } public function handle(): string { $localPath = (new ReportService) ->setStartAt($this->startAt) ->setEndAt($this->endAt) ->setProducts($this->products) ->download(); if (!file_exists($localPath)) { $this->fail(new \Exception("PDF生成失败")); } return $localPath; } } // SendReportByEmailJob 负责发送邮件 class SendReportByEmailJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public string $startAt, public string $endAt, public array $products, public string $pdfPath ) { } public function handle(): void { $reportService = (new ReportService) ->setStartAt($this->startAt) ->setEndAt($this->endAt) ->setProducts($this->products); \Mail::queue((new \App\Mails\Report($reportService))->attach($this->pdfPath)); } }
4. 检查队列进程的文件权限
确保队列进程用户(如www-data、nginx)拥有public/reports目录的读写权限:
# 设置目录权限 chmod -R 755 public/reports # 设置目录所有者为队列进程用户 chown -R www-data:www-data public/reports
5. 避免序列化整个ReportService对象
序列化Service实例可能导致状态丢失或依赖问题,改为传递必要参数到任务中,减少序列化风险(如上述任务链示例中的做法)。
内容的提问来源于stack exchange,提问作者Linesofcode
相关产品推荐
相关产品推荐

