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

如何在 Laravel Job 中本地调整并优化多图后再保存

可行方案

可以完全不依赖第三方在线服务,在图片存入正式可访问存储前通过 Laravel Job 完成尺寸调整与优化。禁止直接向 Job 传递 Request 对象或 UploadedFile 实例,需先在当前HTTP请求周期内将上传文件暂存到服务端可控临时目录,再把可正常序列化的文件元数据传入 Job。

核心原因

  • Request 对象绑定当前HTTP请求的完整上下文与资源句柄,无法完整序列化还原,不符合队列任务的传参规则。
  • PHP 默认会在HTTP请求结束后自动清理上传生成的系统临时文件,异步 Job 拉取任务执行时,原临时文件通常已被删除,直接传文件实例会触发文件不存在错误。
  • 先把文件转存到自定义临时目录,既可以保证 Job 执行时文件可读取,传入的路径、文件名等普通数组数据也支持正常序列化。

代码实现

1. 控制器层:校验上传并暂存临时文件

在控制器中完成文件校验,将所有上传文件存入本地非公开临时目录,收集纯数组格式的文件信息后再派发任务。

namespace App\Http\Controllers;

use App\Jobs\ProcessUploadedImage;
use Illuminate\Http\Request;

class ImageUploadController extends Controller
{
    public function upload(Request $request)
    {
        $request->validate([
            'images' => 'required|array',
            'images.*' => 'required|image|max:10240', // 单图最大10MB,按需调整
        ]);

        $uploadFiles = [];
        // 每个上传批次生成唯一临时目录,避免文件重名冲突
        $tempBatchDir = 'temp/image_uploads/' . uniqid('batch_');

        foreach ($request->file('images') as $file) {
            $extension = $file->getClientOriginalExtension();
            $tempFileName = uniqid('img_') . '.' . $extension;
            
            // 将文件存入local磁盘(对应storage/app目录,无法直接通过web访问)
            $tempPath = $file->storeAs($tempBatchDir, $tempFileName, 'local');

            $uploadFiles[] = [
                'temp_path' => $tempPath,
                'original_name' => $file->getClientOriginalName(),
                'mime_type' => $file->getMimeType(),
            ];
        }

        // 仅传入普通数组,不传递请求对象
        ProcessUploadedImage::dispatch($uploadFiles);

        return response()->json(['message' => '上传任务已提交']);
    }
}

2. Job层:处理图片并转存正式目录

图片调整通过本地PHP扩展(GD/Imagick)完成,不需要调用外部服务。示例使用 intervention/image 包简化图片操作,如需完全不用第三方依赖,也可直接通过GD/Imagick原生函数实现相同逻辑。

可选依赖安装命令:

composer require intervention/image

任务类代码:

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\Storage;
use Intervention\Image\Facades\Image;
use Throwable;

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

    private array $uploadFiles;
    // 按需调整配置
    private string $targetDisk = 'public';
    private string $targetDirectory = 'uploads/images';
    private int $maxWidth = 1920;
    private int $optimizeQuality = 80;

    public function __construct(array $uploadFiles)
    {
        $this->uploadFiles = $uploadFiles;
    }

    public function handle(): void
    {
        $tempDisk = Storage::disk('local');

        foreach ($this->uploadFiles as $file) {
            try {
                if (!$tempDisk->exists($file['temp_path'])) {
                    continue;
                }

                // 初始化图片实例
                $image = Image::make($tempDisk->path($file['temp_path']));

                // 等比缩放,仅缩小超大图,不强制放大小图
                if ($image->width() > $this->maxWidth) {
                    $image->resize($this->maxWidth, null, function ($constraint) {
                        $constraint->aspectRatio();
                        $constraint->upsize();
                    });
                }

                // 生成正式存储文件名
                $extension = pathinfo($file['original_name'], PATHINFO_EXTENSION);
                $targetFileName = uniqid('img_') . '.' . $extension;
                $targetPath = $this->targetDirectory . '/' . $targetFileName;

                // 编码压缩后存入目标磁盘
                $encodedImage = (string) $image->encode(null, $this->optimizeQuality);
                Storage::disk($this->targetDisk)->put($targetPath, $encodedImage);

                // 如需持久化图片信息到数据库,在此处编写对应逻辑
            } finally {
                // 单文件处理完成后立即删除临时文件
                if ($tempDisk->exists($file['temp_path'])) {
                    $tempDisk->delete($file['temp_path']);
                }
            }
        }

        // 清理当前批次的空临时目录
        $batchDir = dirname($this->uploadFiles[0]['temp_path']);
        if (empty($tempDisk->allFiles($batchDir))) {
            $tempDisk->deleteDirectory($batchDir);
        }
    }

    /**
     * 任务执行失败时的兜底清理逻辑
     */
    public function failed(Throwable $exception): void
    {
        $tempDisk = Storage::disk('local');

        foreach ($this->uploadFiles as $file) {
            if ($tempDisk->exists($file['temp_path'])) {
                $tempDisk->delete($file['temp_path']);
            }
        }

        if (!empty($this->uploadFiles)) {
            $batchDir = dirname($this->uploadFiles[0]['temp_path']);
            if ($tempDisk->directoryExists($batchDir)) {
                $tempDisk->deleteDirectory($batchDir);
            }
        }
    }
}

注意事项

  • 临时文件默认存在 storage/app 下的非公开目录,处理完成前无法通过外网访问,避免原图泄露。
  • 如果上传图片数量多、单图处理耗时长,可以遍历文件时为每个文件单独派发Job,利用队列并发提升处理效率,传参逻辑一致。
  • 即使当前队列使用同步驱动,也建议按该方案实现,避免后续切换异步队列时出现兼容性错误。
  • 目标磁盘支持本地公开目录、对象存储等所有Laravel文件系统兼容的存储驱动。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.22 06:57:02