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

请求为Laravel Nova实现类WP Heartbeat的资源编辑锁定提示功能

实现Laravel Nova资源编辑时的独占锁定功能(类似WP Heartbeat机制)

1. 创建资源锁定数据表

首先生成迁移文件并定义锁定表结构:

php artisan make:migration create_resource_locks_table

修改迁移文件的up方法:

Schema::create('resource_locks', function (Blueprint $table) {
    $table->id();
    $table->string('resource_type'); // 存储资源类名,如App\Models\Post
    $table->unsignedBigInteger('resource_id');
    $table->foreignId('user_id')->constrained();
    $table->timestamp('locked_at')->useCurrent();
    $table->timestamp('expires_at');
    $table->unique(['resource_type', 'resource_id']); // 确保同一资源仅能被锁定一次
    $table->index(['user_id', 'resource_type']);
});

执行迁移:

php artisan migrate

2. 建立ResourceLock模型

创建app/Models/ResourceLock.php,封装锁定逻辑:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ResourceLock extends Model
{
    protected $fillable = ['resource_type', 'resource_id', 'user_id', 'expires_at'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    // 锁定资源,默认锁定5分钟
    public static function lock(string $resourceType, int $resourceId, int $userId): void
    {
        $expiresAt = now()->addMinutes(5);
        self::updateOrCreate(
            ['resource_type' => $resourceType, 'resource_id' => $resourceId],
            ['user_id' => $userId, 'expires_at' => $expiresAt]
        );
    }

    // 解锁资源
    public static function unlock(string $resourceType, int $resourceId): void
    {
        self::where('resource_type', $resourceType)->where('resource_id', $resourceId)->delete();
    }

    // 检查资源是否被有效锁定(自动清理过期锁定)
    public static function isLocked(string $resourceType, int $resourceId): bool
    {
        self::where('expires_at', '<=', now())->delete();
        return self::where('resource_type', $resourceType)
            ->where('resource_id', $resourceId)
            ->exists();
    }

    // 获取锁定资源的用户
    public static function getLockOwner(string $resourceType, int $resourceId): ?User
    {
        $lock = self::where('resource_type', $resourceType)
            ->where('resource_id', $resourceId)
            ->first();
        return $lock?->user;
    }

    // 刷新锁定过期时间
    public static function refreshLock(string $resourceType, int $resourceId): void
    {
        self::where('resource_type', $resourceType)
            ->where('resource_id', $resourceId)
            ->update(['expires_at' => now()->addMinutes(5)]);
    }
}

3. 自定义Nova资源控制器逻辑

创建自定义控制器继承Nova默认资源控制器,重写编辑和更新逻辑:

namespace App\Http\Controllers\Nova;

use Laravel\Nova\Http\Controllers\ResourceController as BaseController;
use Laravel\Nova\Http\Requests\NovaRequest;
use App\Models\ResourceLock;

class CustomResourceController extends BaseController
{
    public function edit(NovaRequest $request)
    {
        $resource = $request->findResourceOrFail();
        $model = $resource->model();
        $resourceType = get_class($model);
        $resourceId = $model->getKey();

        // 检查是否被其他用户锁定
        if (ResourceLock::isLocked($resourceType, $resourceId)) {
            $owner = ResourceLock::getLockOwner($resourceType, $resourceId);
            if ($owner->id !== auth()->id()) {
                return redirect()->route('nova.index', [$resource::uriKey()])
                    ->withErrors([
                        'message' => "该资源正在被 {$owner->name} 编辑,请稍后再试。"
                    ]);
            }
        }

        // 当前用户锁定资源
        ResourceLock::lock($resourceType, $resourceId, auth()->id());
        return parent::edit($request);
    }

    // 更新完成后解锁资源
    public function update(NovaRequest $request)
    {
        $resource = $request->findResourceOrFail();
        $model = $resource->model();
        $response = parent::update($request);

        ResourceLock::unlock(get_class($model), $model->getKey());
        return $response;
    }
}

在Nova资源类中指定自定义控制器,比如app/Nova/Post.php:

namespace App\Nova;

use App\Http\Controllers\Nova\CustomResourceController;

class Post extends Resource
{
    // ...其他配置
    public static $controller = CustomResourceController::class;
    // ...
}

4. 添加前端心跳维持锁定

在Nova资源类的boot方法中注入心跳脚本,定时刷新锁定时间,并在页面关闭时解锁:

public static function boot()
{
    parent::boot();

    static::serving(function () {
        Nova::script('resource-lock-heartbeat', function () {
            $resourceType = get_class(static::$model);
            $resourceId = request()->route('resourceId');

            return <<<JS
                // 每4分钟刷新一次锁定(比锁定时长少1分钟)
                setInterval(() => {
                    fetch('/nova-api/resource-locks/refresh', {
                        method: 'POST',
                        headers: {
                            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
                            'Accept': 'application/json'
                        },
                        body: JSON.stringify({
                            resource_type: '$resourceType',
                            resource_id: $resourceId
                        })
                    });
                }, 240000);

                // 页面关闭/刷新时解锁
                window.addEventListener('beforeunload', () => {
                    fetch('/nova-api/resource-locks/unlock', {
                        method: 'POST',
                        headers: {
                            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
                            'Accept': 'application/json'
                        },
                        body: JSON.stringify({
                            resource_type: '$resourceType',
                            resource_id: $resourceId
                        })
                    });
                });
            JS;
        });
    });
}

创建API路由和控制器处理心跳请求:
在routes/nova.php添加:

use App\Http\Controllers\Nova\ResourceLockController;

Route::post('/resource-locks/refresh', [ResourceLockController::class, 'refresh']);
Route::post('/resource-locks/unlock', [ResourceLockController::class, 'unlock']);

创建app/Http/Controllers/Nova/ResourceLockController.php:

namespace App\Http\Controllers\Nova;

use Illuminate\Http\Request;
use App\Models\ResourceLock;

class ResourceLockController extends Controller
{
    public function refresh(Request $request)
    {
        $request->validate([
            'resource_type' => 'required|string',
            'resource_id' => 'required|integer'
        ]);

        $lock = ResourceLock::where('resource_type', $request->resource_type)
            ->where('resource_id', $request->resource_id)
            ->where('user_id', auth()->id())
            ->first();

        if ($lock) {
            ResourceLock::refreshLock($request->resource_type, $request->resource_id);
            return response()->json(['success' => true]);
        }
        return response()->json(['success' => false], 403);
    }

    public function unlock(Request $request)
    {
        $request->validate([
            'resource_type' => 'required|string',
            'resource_id' => 'required|integer'
        ]);

        $lock = ResourceLock::where('resource_type', $request->resource_type)
            ->where('resource_id', $request->resource_id)
            ->where('user_id', auth()->id())
            ->first();

        if ($lock) {
            ResourceLock::unlock($request->resource_type, $request->resource_id);
            return response()->json(['success' => true]);
        }
        return response()->json(['success' => false], 403);
    }
}

5. 在资源列表添加锁定状态提示

修改Nova资源类的fields方法,添加自定义字段显示锁定状态:

use Laravel\Nova\Fields\Field;
use App\Models\ResourceLock;

// ...在fields方法中添加
Field::make('锁定状态', function () {
    if (ResourceLock::isLocked(get_class($this), $this->id)) {
        $owner = ResourceLock::getLockOwner(get_class($this), $this->id);
        return "<span class='flex items-center text-red-500'><svg class='w-4 h-4 mr-1' fill='currentColor' viewBox='0 0 20 20'><path d='M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2H7V7a3 3 0 015.905-.75 1 1 0 001.937-.5A5.002 5.002 0 0010 2z'></path></svg> 被 {$owner->name} 锁定</span>";
    }
    return "<span class='text-green-500'><svg class='w-4 h-4 mr-1' fill='currentColor' viewBox='0 0 20 20'><path fill-rule='evenodd' d='M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z' clip-rule='evenodd'></path></svg> 可编辑</span>";
})->asHtml()->onlyOnIndex(),

6. 定时清理过期锁定(可选)

创建Artisan命令定期清理过期锁定:

php artisan make:command ClearExpiredResourceLocks

修改命令的handle方法:

public function handle()
{
    \App\Models\ResourceLock::where('expires_at', '<=', now())->delete();
    $this->info('已清理所有过期的资源锁定');
}

在app/Console/Kernel.php添加定时任务:

protected function schedule(Schedule $schedule)
{
    $schedule->command('clear:expired-resource-locks')->everyMinute();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 21:05:24