请求为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
相关产品推荐
相关产品推荐

