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

Laravel+Livewire多图上传绑定独立caption时数据表重复数据排查

问题描述

当前开发项目图片上传功能,需求为通过项目创建表单上传多张图片,且每张图片支持配置独立的描述说明文字(caption)。目前除project_images表因嵌套foreach循环逻辑错误出现重复记录外,其余功能均正常运行,以下为项目核心代码结构,重点排查storeProject方法的逻辑问题。

  • 技术栈:Laravel + Livewire
相关代码

数据库迁移文件

projects表迁移

public function up()
{
    Schema::create('projects', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id');
        $table->foreign('user_id')->references('id')->on('users');
        $table->string('title');
        $table->integer('budget');
        $table->string('proposed_by');
        $table->string('executed_by');
        $table->string('status')->nullable();
        $table->integer('progress_indicator')->nullable();
        $table->date('starting_date');
        $table->date('completion_date')->nullable();
        $table->string('cover_image')->nullable();
        $table->mediumText('body')->nullable();
        $table->timestamps();
    });
}

project_images表迁移

public function up()
{
    Schema::create('project_images', function (Blueprint $table) {
        $table->id();
        $table->string('images');
        $table->string('caption')->nullable();
        $table->unsignedBigInteger('project_id');
        $table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade');
        $table->timestamps();
    });
}

前端表单代码

<x-slot name="content">
        <x-errors />
        <div class="space-y-8 divide-y divide-gray-200  mt-10">
            <form method="#" enctype="multipart/form-data">
                    @if ($projectImages)
                        Images Preview:
                        <div class="flex flex-row m-8 ">
                            @foreach ($projectImages as $key => $projectImage)
                                <div class="flex flex-col gap-3">
                                    <img width="100%" src="{{ $projectImage->temporaryUrl() }}">
                                    <x-input placeholder="Caption"
                                        wire:model.defer="captions.{{ $key }}" />
                                </div>
                            @endforeach
                        </div>
                    @endif
                <label class="inline-block mb-2 text-gray-500">Upload Projects Images</label>
                <div class="flex items-center justify-center w-full">
                    <label class="flex flex-col w-full h-32 border-2 border-dashed hover:bg-gray-500 hover:border-gray-300">
                        <div class="flex flex-col items-center justify-center pt-7">
                            <p class="pt-1 text-sm tracking-wider text-gray-400 group-hover:text-gray-600">
                                Select a photo</p>
                        </div>
                        <input wire:model="projectImages" type="file" multiple class="opacity-0" />
                    </label>
                </div>
                <x-textarea wire:model.lazy="body" label="Body" placeholder="write your article" />
            </form>
        </div>
    </x-slot>
    <x-slot name="footer">
        @if ($projectId)
            <div class="flex items-center gap-x-3 justify-end">
                <x-button wire:click="CancelConfirmation" label="Cancel" flat />
                <x-button type="submit" wire:click="updateProject" label="Update" wire:loading.attr="disabled" primary />
            </div>
        @else
            <div class="flex items-center gap-x-3 justify-end">
                <x-button wire:click="CancelConfirmation" label="Cancel" flat />
                <x-button type="submit" wire:click="storeProject" label="Create" wire:loading.attr="disabled" primary />
            </div>
        @endif
    </x-slot>
</x-jet-dialog-modal>

Livewire组件代码

public function storeProject()
{
    $this->validate([
        'title' => 'required',
        'status' => 'required',
        'budget' => 'required',
        'proposedBy' => 'required',
        'executedBy' => 'required',
        'startingDate' => 'required',
        'completionDate' => 'required',
        'progressIndicator' => 'required',
        'body'  => 'required',
        'image' => 'image|max:1024|nullable'
    ]);

    $image_name = $this->image->getClientOriginalName();
    $this->image->storeAs('public/photos', $image_name);
    $project = new Project();
    $project->user_id = auth()->user()->id;
    $project->title = $this->title;
    $project->status = $this->status;
    $project->budget = $this->budget;
    $project->proposed_by = $this->proposedBy;
    $project->executed_by = $this->executedBy;
    $project->starting_date = Carbon::create($this->startingDate);
    $project->completion_date = Carbon::create($this->completionDate);
    $project->progress_indicator = $this->progressIndicator;
    $project->body = $this->body;
    $project->cover_image = $image_name;
    $project->save();

    foreach ($this->projectImages as $projectImage) {
        $image_name = $projectImage->getClientOriginalName();
        $projectImage->storeAs('public/photos', $image_name);
        // 问题出在这层嵌套循环
        foreach ($this->captions as $caption) {
            $projectImages = new ProjectImages();
            $projectImages->project_id = $project->id;
            $projectImages->images = $image_name;
            $projectImages->caption = $caption;
            $projectImages->save();
        }
    }
    
    $this->notification()->success(
        $title = 'Success',
        $description = 'Project Created Successfully'
    );
    $this->reset();
}

项目表单运行效果

故障原因

重复记录的核心问题是循环逻辑错误:
外层遍历单张上传图片时,内层嵌套遍历了全部caption输入值,会给每张图片绑定所有提交的caption,最终插入上传图片数 * caption总数条重复数据。
从前端绑定逻辑可以看到,caption输入框通过循环$key和图片数组下标一一对应,二者是索引匹配的关系,完全不需要两层循环。

修复方案

删除内层遍历caption的循环,直接通过外层循环的键名取对应下标的caption即可,修改后的图片入库逻辑如下:

// 项目主体保存完成后,遍历上传的图片
foreach ($this->projectImages as $key => $projectImage) {
    $image_name = $projectImage->getClientOriginalName();
    $projectImage->storeAs('public/photos', $image_name);
    
    // 直接取同索引的caption,无需内层循环
    $projectImageRecord = new ProjectImages();
    $projectImageRecord->project_id = $project->id;
    $projectImageRecord->images = $image_name;
    $projectImageRecord->caption = $this->captions[$key] ?? null;
    $projectImageRecord->save();
}

优化建议:

  • 保存图片时生成唯一文件名,避免不同用户上传同名文件导致资源覆盖
  • 验证规则补充projectImages数组的图片格式、大小校验,同时校验captions数组长度和图片数量一致,避免下标不存在的报错

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 20:12:16