本文目录导读:

设计一个既优雅又实用的 Laravel API 回收站功能,需要从 模型、控制器、中间件/全局作用域、以及 API 路由 四个层面来规划。
下面是主流且推荐的设计方案,结合了 Laravel 内置的 SoftDeletes 特性。
核心思路:SoftDeletes + 统一查询作用域
- 模型层:使用
Illuminate\Database\Eloquent\SoftDeletesTrait。 - 数据库:
deleted_at字段 (datetime, 可空)。 - API 层:设计专门的回收站相关路由和控制器方法。
第一步:数据库与模型准备
迁移文件添加 deleted_at 字段
// database/migrations/xxxx_xx_xx_add_softdeletes_to_posts_table.php
Schema::table('posts', function (Blueprint $table) {
$table->softDeletes(); // 添加 deleted_at 字段
});
模型引入 SoftDeletes
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use SoftDeletes; // 关键 Trait
protected $fillable = ['title', 'content'];
// 1. 正常查询会自动排除被软删除的记录
// Post::all() -> 只返回 deleted_at 为 null 的记录
// 2. 定义自定义的已删除作用域
public function scopeOnlyTrashed($query)
{
return $query->whereNotNull('deleted_at');
}
}
第二步:设计 API 路由
建议遵循 RESTful 风格,并新增一组专门操作回收站的端点。
| 功能 | HTTP 方法 | 路由 | 控制器方法 | 说明 |
|---|---|---|---|---|
| 获取回收站列表 | GET | /api/posts/trashed |
getTrashed |
查看所有已软删除的记录 |
| 单条记录还原 | PATCH | /api/posts/{post}/restore |
restore |
恢复指定 ID 的记录 |
| 批量还原 | PATCH | /api/posts/restore |
restoreMultiple |
根据 ID 数组批量恢复 |
| 永久删除 (单条) | DELETE | /api/posts/{post}/force-delete |
forceDelete |
彻底从数据库删除 |
| 批量永久删除 | DELETE | /api/posts/force-delete |
forceDeleteMultiple |
批量永久删除 |
| 清空回收站 | DELETE | /api/posts/trashed/clear |
clearTrashed |
删除回收站中所有记录 |
路由定义示例 (routes/api.php):
Route::prefix('posts')->group(function () {
Route::get('/', [PostController::class, 'index']); // 正常列表
Route::post('/', [PostController::class, 'store']);
Route::get('/{post}', [PostController::class, 'show']);
Route::put('/{post}', [PostController::class, 'update']);
Route::delete('/{post}', [PostController::class, 'destroy']); // 普通删除 -> 软删除
// ===== 回收站专属路由 =====
Route::get('/trashed', [PostController::class, 'getTrashed']); // 回收站列表
Route::patch('/restore', [PostController::class, 'restoreMultiple']); // 批量还原 (需置于具体ID之前)
Route::delete('/force-delete', [PostController::class, 'forceDeleteMultiple']); // 批量强制删除
Route::delete('/trashed/clear', [PostController::class, 'clearTrashed']); // 清空回收站
// 带模型绑定的路由
Route::patch('/{post}/restore', [PostController::class, 'restore']);
Route::delete('/{post}/force-delete', [PostController::class, 'forceDelete']);
});
第三步:控制器具体实现
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class PostController extends Controller
{
/**
* 1. 获取回收站列表 (分页)
*/
public function getTrashed(Request $request)
{
// onlyTrashed() 是 Eloquent 全局作用域提供的,配合 SoftDeletes
$posts = Post::onlyTrashed()
->orderBy('deleted_at', 'desc')
->paginate($request->per_page ?? 15);
return response()->json([
'success' => true,
'data' => $posts->items(),
'meta' => [
'total' => $posts->total(),
'per_page' => $posts->perPage(),
'current_page' => $posts->currentPage(),
'last_page' => $posts->lastPage(),
],
]);
}
/**
* 2. 还原单条记录
*/
public function restore($id)
{
// 注意:withTrashed() 允许查找已被软删除的记录
$post = Post::withTrashed()->findOrFail($id);
// 检查是否已经在正常状态
if (!$post->trashed()) {
return response()->json([
'success' => false,
'message' => '该记录未在回收站中。'
], 400);
}
$post->restore();
return response()->json([
'success' => true,
'message' => '记录已成功还原。',
'data' => $post
]);
}
/**
* 3. 批量还原
*/
public function restoreMultiple(Request $request)
{
$validator = Validator::make($request->all(), [
'ids' => 'required|array',
'ids.*' => 'integer|exists:posts,id',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
// 只还原那些确实在回收站的记录
$restoredCount = Post::onlyTrashed()
->whereIn('id', $request->ids)
->restore(); // restore() 在批量作用域下生效
return response()->json([
'success' => true,
'message' => "成功还原 {$restoredCount} 条记录。",
'count' => $restoredCount
]);
}
/**
* 4. 永久删除单条
*/
public function forceDelete($id)
{
$post = Post::withTrashed()->findOrFail($id);
// 可选:限制只能删除回收站中的记录
if (!$post->trashed()) {
return response()->json([
'success' => false,
'message' => '只能从回收站进行永久删除,请先删除该记录。'
], 400);
}
$post->forceDelete();
return response()->json([
'success' => true,
'message' => '记录已被永久删除。'
]);
}
/**
* 5. 批量永久删除
*/
public function forceDeleteMultiple(Request $request)
{
$validator = Validator::make($request->all(), [
'ids' => 'required|array',
'ids.*' => 'integer|exists:posts,id',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$deletedCount = Post::onlyTrashed()
->whereIn('id', $request->ids)
->forceDelete();
return response()->json([
'success' => true,
'message' => "已永久删除 {$deletedCount} 条记录。",
'count' => $deletedCount
]);
}
/**
* 6. 清空回收站
*/
public function clearTrashed()
{
$deletedCount = Post::onlyTrashed()->forceDelete();
return response()->json([
'success' => true,
'message' => "回收站已清空,共永久删除 {$deletedCount} 条记录。",
'count' => $deletedCount
]);
}
// ===== 标准 CRUD (仅展示软删除部分) =====
/**
* 标准删除 -> 执行软删除
*/
public function destroy($id)
{
$post = Post::findOrFail($id);
// 执行软删除
$post->delete();
return response()->json([
'success' => true,
'message' => '记录已移入回收站。'
]);
}
}
第四步:高级优化与注意事项
使用资源类统一响应格式
避免手动写 response()->json(...),创建 PostResource 和 PostCollection。
php artisan make:resource PostResource php artisan make:resource PostCollection
控制器返回:
use App\Http\Resources\PostResource;
public function getTrashed(Request $request)
{
$posts = Post::onlyTrashed()->paginate();
return PostResource::collection($posts);
}
public function restore($id)
{
$post = Post::withTrashed()->findOrFail($id);
$post->restore();
return new PostResource($post);
}
在 forceDelete 时处理关联外键
如果模型有 hasMany 关联,且外键 onDelete('set null') 或 cascade 配置不当,强制删除主记录可能导致数据不一致,建议在模型 booted 方法中添加监听:
protected static function booted()
{
static::forceDeleting(function ($post) {
// 永久删除前,手动清理关联数据
$post->comments()->delete(); // 或者 forceDelete()
});
}
回收站过期自动清理 (Cron Job)
在 app/Console/Kernel.php 中计划一个任务,定期清理超过 N 天的回收站数据:
// command: php artisan app:clear-old-trashed-posts
$schedule->command('app:clear-old-trashed-posts')->dailyAt('03:00');
命令实现:
public function handle()
{
$days = config('app.recycle_bin_retention_days', 30);
$count = Post::onlyTrashed()
->where('deleted_at', '<', now()->subDays($days))
->forceDelete();
$this->info("已清理 {$count} 条超过 {$days} 天的回收站记录。");
}
权限控制 (Policy/Laravel Permission)
确保不是所有用户都能操作回收站,在控制器中使用 authorize:
public function forceDelete($id)
{
$this->authorize('forceDelete', Post::class); // 需要管理员权限
// ...
}
最佳实践清单
| 类别 | 建议 |
|---|---|
| 模型 | 必须使用 SoftDeletes,自定义 onlyTrashed 作用域。 |
| 路由 | 从 GET /posts/trashed 到 DELETE /posts/trashed/clear 都保持一致。 |
| 控制器 | 分离清晰:index (正常列表) / getTrashed (回收站) / restore / forceDelete。 |
| 安全性 | 批量操作时验证 ids 数组;强制删除前检查是否已在回收站。 |
| 资源类 | 统一响应结构,便于前端处理。 |
| 维护 | 定期清理过期回收站数据,避免表体积膨胀。 |
这样做之后,你的 API 回收站功能既符合 RESTful 设计,又充分利用了 Laravel 的 Eloquent 特性,并且具备良好的扩展性。