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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 09:33:22