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

Vue+Laravel+Inertia更新Job资源时出现404错误求助

Job资源更新时出现404错误(Laravel + Inertia + Vue)

我采用Vue作为前端、Laravel作为后端,通过Inertia实现前后端数据传递。尝试更新Job资源时触发404错误,但执行php artisan route:list显示update路由存在:

PUT|PATCH       jobs/{job} ................................... jobs.update › JobController@update

相关代码

web.php

<?php

use App\Http\Controllers\JobController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Models\User;
use App\Models\Job;
use Illuminate\Support\Facades\Auth;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return redirect('/jobs');
});

Route::get('/admin', function () {
    return Inertia::render('Admin');
})->middleware(['admin', 'verified'])->name('admin');

Route::get('/dashboard', function (\Illuminate\Http\Request $request) {

    $limit = $request->get('limit', 25);
    $offset = $request->get('offset', 0);

    $query = Job::query()
            ->where('id', $request->user()->id)
            ->with('tags', 'clicks')
            ->latest();

    $jobs = $query->skip($offset)->take($limit)->get();
    
    return Inertia::render('Dashboard', [
        'jobs' => $jobs
    ]);
})->middleware(['auth', 'verified'])->name('dashboard');


Route::resource('jobs', JobController::class);

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

require __DIR__.'/auth.php';

Route::get('/{job}/apply', [JobController::class, 'apply'])
    ->name('jobs.apply');

Route::get('tags', [JobController::class, 'tags'])
    ->name('jobs.tags');

JobController.php

<?php

namespace App\Http\Controllers;

use App\Models\Job;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;
use Inertia\Inertia;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Carbon\Carbon;

class JobController extends Controller
{
    /**
     * Display a job listing of the resource.
     */
    public function index(Request $request)
    {
        $query = Job::query()
            ->where('is_active', true)
            ->whereDate('created_at', '>=', Carbon::now()->subDays(30))
            ->with('tags')
            ->latest();

        if ($request->has('tag')) {
            $tag = $request->get('tag');
            $query->whereHas('tags', function (Builder $builder) use ($tag) {
                $builder->where('slug', $tag);
            });
        }

        //Search
        if ($request->has('s')) {
            $search = strtolower($request->get('s'));
            $query->where(function($query) use ($search) {
                $query->where('title', 'like', '%' . $search . '%')
                      ->orWhere('company', 'like', '%' . $search . '%')
                      ->orWhere('location', 'like', '%' . $search . '%');
            });
        }

        // Pagination
        $limit = $request->get('limit', 25);
        $offset = $request->get('offset', 0);
        $jobs = $query->skip($offset)->take($limit)->get();

        $tags = Tag::whereHas('jobs')->groupBy('id')->orderBy('name')->get();

        return Inertia::render('Jobs/Index', [
            'jobs' => $jobs,
            'tags'  => $tags,
        ]);
    }


    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return Inertia::render('Jobs/Create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        $validationArray = [
            'title' => 'required',
            'company' => 'required',
            'logo' => 'file|max:2048',
            'location' => 'required',
            'apply_link' => 'required|url',
            'payment_method_id' => 'required'
        ];

        if (!Auth::check()) {
            $validationArray = array_merge($validationArray, [
                'email' => 'required|email|unique:users',
                'password' => 'required|confirmed|min:5',
                'name' => 'required'
            ]);
        }

        $request->validate($validationArray);

        $user = Auth::user();

        if (!$user) {
            $user = User::create([
                'name' => $request->name,
                'email' => $request->email,
                'password' => Hash::make($request->password)
            ]);

            $user->createAsStripeCustomer();

            Auth::login($user);
        }

        try {
            $amount = 29900;
            if ($request->filled('is_highlighted')) {
                $amount += 1900;
            }

            $user->charge($amount, $request->payment_method_id);

            $job = $user->jobs()
                ->create([
                    'title' => $request->title,
                    'slug' => Str::slug($request->title) . '-' . rand(1111, 9999),
                    'company' => $request->company,
                    'logo' => basename($request->file('logo')->store('public')),
                    'location' => $request->location,
                    'apply_link' => $request->apply_link,
                    'is_highlighted' => $request->filled('is_highlighted'),
                    'is_active' => true
                ]);

            foreach(explode(',', $request->tags) as $requestTag) {
                $tag = Tag::firstOrCreate([
                    'slug' => Str::slug(trim($requestTag))
                ], [
                    'name' => ucwords(trim($requestTag))
                ]);

                $tag->jobs()->attach($job->id);
            }

            return to_route('dashboard');
        } catch(\Exception $e) {
            return redirect()->back()
                ->withErrors(['error' => $e->getMessage()]);
        }
    }

    /**
     * Display the specified resource.
     */
    public function show(Job $job, Request $request)
    {
        return Inertia::render('Jobs/Job', [
            'job' => $job,
            'tags' => $job->tags,
        ]);
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(Job $job)
    {
        return Inertia::render('Jobs/Edit', [
            'job' => $job,
            'tags' => $job->tags,
        ]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, Job $job)
    {
        $data = $request->validate([
            'title' => 'required',
            'company' => 'required',
            'location' => 'required',
            'apply_link' => 'required|url',
        ]);

        if ($request->hasFile('logo')) {
            $data['logo'] = basename($request->file('logo')->store('public'));
        }

        try {
            $job->update($data);
            return redirect()->route('dashboard');
        } catch(\Exception $e) {
            return redirect()->back()
                ->withErrors(['error' => $e->getMessage()]);
        }
    }


    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Job $job)
    {
        //
    }

    public function apply(Job $job, Request $request)
    {
        $job->clicks()
            ->create([
                'user_agent' => $request->userAgent(),
                'ip' => $request->ip()
            ]);

        return redirect()->to($job->apply_link);

    }

    public function tags()
    {
        $tags = Tag::all();

        return Inertia::render('Jobs/Tags', [
            'tags' => $tags,
        ]);
    }
}

Edit.vue

<template>
    <app-layout>
        <div class="container mx-auto px-4 mt-10">
            <!-- Form Section -->
            <div class="bg-gray-100 p-6 rounded-lg shadow-md">
                <div class="mb-4 pl-4 mt-4">
                    <h2 class="text-2xl font-medium text-gray-900 title-font">
                        Edit Job Listing
                    </h2>
                </div>

                <form id="edit_form" class="bg-gray-100 p-4" enctype="multipart/form-data">
                    <!-- Form Fields -->
                    <div class="mb-4 mx-2">
                        <label for="title" class="block font-medium text-sm text-gray-700">Job Title</label>
                        <input
                            id="title"
                            class="block mt-1 w-full"
                            type="text"
                            name="title"
                            v-model="form.title"
                            required />
                    </div>

                    <!-- Company Name -->
                    <div class="mb-4 mx-2">
                        <label for="company" class="block font-medium text-sm text-gray-700">Company Name</label>
                        <input
                            id="company"
                            class="block mt-1 w-full"
                            type="text"
                            name="company"
                            v-model="form.company"
                            required />
                    </div>

                    <!-- Company Logo -->
                    <div class="mb-4 mx-2">
                        <label for="logo" class="block font-medium text-sm text-gray-700">Company Logo</label>
                        <input
                            type="file"
                            id="logo"
                            class="mt-1 w-full"
                            ref="logoInput"
                            accept="image/*"
                            @change="onFileChange"
                        />
                        <img v-if="form.logo" :src="'/storage/' + form.logo" alt="Company Logo" class="mt-2 h-20 w-20 object-cover" />
                    </div>

                    <!-- Apply Link -->
                    <div class="mb-4 mx-2">
                        <label for="apply_link" class="block font-medium text-sm text-gray-700">Link To Apply</label>
                        <input
                            id="apply_link"
                            class="block mt-1 w-full"
                            type="text"
                            name="apply_link"
                            v-model="form.apply_link"
                            required />
                    </div>

                    <!-- Job Tags -->
                    <div class="mb-4 mx-2">
                        <label for="tags" class="block font-medium text-sm text-gray-700">Tags (separate by comma)</label>
                        <input
                            type="text"
                            id="tags"
                            class="block mt-1 w-full"
                            v-model="tagsInput"
                            placeholder="Enter tags separated by commas"
                        />
                    </div>

                    <!-- Job Location -->
                    <div class="mb-4 mx-2">
                        <label for="location" class="block font-medium text-sm text-gray-700">Location</label>
                        <input
                            id="location"
                            class="block mt-1 w-full"
                            type="text"
                            name="location"
                            v-model="form.location"
                            required />
                    </div>


                    <!-- Submit Button -->
                    <div class="flex justify-end mt-4">
                        <button type="submit" @click="submitForm($event)" class="bg-indigo-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
                            Update Listing
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </app-layout>
</template>

<script setup>
import AppLayout from '../../Components/AppLayout.vue';
import { usePage, useForm } from '@inertiajs/vue3'
import { ref, toRefs, watchEffect } from 'vue';

const page = usePage();

const form = useForm({
    title: page.props.job.title,
    company: page.props.job.company,
    logo: page.props.job.logo,
    apply_link: page.props.job.apply_link,
    tags: page.props.job.tags.map(tag => tag.name),
    location: page.props.job.location,
});


// methods
const onFileChange = (e) => {
    let files = e.target.files || e.dataTransfer.files;
    if (!files.length)
        return;
    form.logo = files[0];
};

const submitForm = (event) => {
    event.preventDefault();
    form.put(route('jobs.update', page.props.job.id), { onError: () => console.log(form.errors) });
};

const tagsInput = ref(form.tags.join(', '));

watchEffect(() => {
    form.tags = tagsInput.value.split(',').map(tag => tag.trim());
});
</script>

原因分析与修复方案

1. 路由优先级冲突(核心问题)

你在Route::resource('jobs', JobController::class)之后定义了Route::get('/{job}/apply', ...),Laravel路由匹配是按顺序执行的,当请求jobs/1时,框架会把jobs解析为{job}参数,优先匹配到/{job}/apply路由,导致update路由无法被命中,返回404。

修复:调整路由顺序,将资源路由放在自定义路由之后:

// 先定义自定义路由
Route::get('/{job}/apply', [JobController::class, 'apply'])->name('jobs.apply');
Route::get('tags', [JobController::class, 'tags'])->name('jobs.tags');

// 再定义资源路由
Route::resource('jobs', JobController::class);

2. 隐式模型绑定验证

检查Job模型的主键是否为id(默认配置无需修改),同时确认前端传递的page.props.job.id是数据库中存在的有效记录ID,避免因模型绑定失败返回404。

3. 额外优化建议

  • 在Edit.vue中添加加载状态,防止重复提交:
<button type="submit" @click="submitForm($event)" :disabled="form.processing" class="bg-indigo-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
    {{ form.processing ? '更新中...' : 'Update Listing' }}
</button>
  • 在JobController的update方法中添加权限验证,确保只有Job所有者能修改:
public function update(Request $request, Job $job)
{
    abort_if($job->user_id !== auth()->id(), 403);
    
    // 原有验证与更新逻辑
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 07:12:00