Laravel 9.2分类统计端点重构方案咨询
重构Laravel分类统计端点:异步处理与性能优化
我接手了一个基于Laravel 9.2开发的PHP项目,需要重构其中一个分类统计端点。该端点接收包含ID数组的POST请求,遍历ID调用外部API,聚合各category_id的统计值后返回结果。
不可变更需求
- 接收请求体中的ID数组
- 异步调用外部API
- 返回各
category_id的合并统计结果 - 端点需满足:安全、可靠、可扩展、可用、易维护、高性能
- 支持传入数百个ID,每个外部API响应包含大量分类数据
现有问题
当前代码采用同步调用外部API的方式,当传入大量ID时存在严重性能瓶颈。我考虑过使用异步任务队列,但无法直接返回聚合结果,计划返回任务ID让消费者轮询查询;也考虑过借助数据库存储请求与计算数据,但不确定最佳实现方案。
现有代码:
<?php namespace App\Http\Controllers; use App\Http\Requests\ReportRequest; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; class CategoryReportController extends Controller { /** * @param Request $request */ public function categoryReportTotals(ReportRequest $request) { $responses = []; foreach ($request->parent_category_ids as $parentCategoryId) { $responses[] = Http::withBasicAuth('username', 'password')->get('https://api.endpoint.com/category/' . $parentCategoryId . '?version=5&associated=true'); } $grouped = []; $categoryATotal = 0; $categoryBTotal = 0; $categoryCTotal = 0; foreach ($responses as $response) { $json = $response->json(); $categories = $json['data']['CategoryList']; foreach ($categories as $category) { $grouped[$category['category_id']] = $category; if ($category['category_id'] == 1) { $categoryCTotal = $categoryATotal + ($category['category_count_start'] + $category['count']); } else if ($category['category_id'] == 2) { $categoryCTotal = $categoryBTotal + ($category['category_count_start'] + $category['distinct']); } else if ($category['category_id'] == 3) { $categoryCTotal = $categoryCTotal + ($category['category_count_start'] + $category['count']); } } } $results = [ [ 'category_id' => 1, 'total' => $categoryATotal, ], [ 'category_id' => 2, 'total' => $categoryBTotal, ], [ 'category_id' => 3, 'total' => $categoryCTotal, ] ]; return response()->json([ 'data' => $results ]); } }
重构方案:异步队列+轮询模式
结合Laravel队列系统与数据库存储,实现异步处理+轮询查询结果的模式,完全满足需求:
1. 创建任务模型与迁移
首先创建ReportTask模型,用于存储任务状态、请求参数和最终结果:
php artisan make:migration create_report_tasks_table
迁移文件内容:
public function up() { Schema::create('report_tasks', function (Blueprint $table) { $table->id(); $table->json('parent_category_ids'); // 存储请求的ID数组 $table->enum('status', ['pending', 'processing', 'completed', 'failed'])->default('pending'); $table->json('result')->nullable(); // 存储最终聚合结果 $table->text('error')->nullable(); // 存储错误信息 $table->timestamps(); }); }
生成模型:
php artisan make:model ReportTask
2. 创建异步任务类
生成处理API调用与聚合的任务:
php artisan make:job ProcessCategoryReport
任务类实现:
<?php namespace App\Jobs; use App\Models\ReportTask; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; class ProcessCategoryReport implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $task; public $tries = 3; // 设置任务重试次数 public function __construct(ReportTask $task) { $this->task = $task; } public function handle() { try { $this->task->update(['status' => 'processing']); $parentIds = $this->task->parent_category_ids; $results = [1 => 0, 2 => 0, 3 => 0]; // 并发调用外部API,Laravel 9原生支持 $responses = Http::pool(function ($pool) use ($parentIds) { foreach ($parentIds as $id) { $pool->withBasicAuth('username', 'password') ->get("https://api.endpoint.com/category/{$id}?version=5&associated=true"); } }); // 聚合统计结果 foreach ($responses as $response) { if (!$response->successful()) { throw new \Exception("API调用失败,状态码:{$response->status()}"); } $categories = $response->json()['data']['CategoryList'] ?? []; foreach ($categories as $category) { $catId = $category['category_id']; if (!isset($results[$catId])) continue; switch ($catId) { case 1: $results[$catId] += $category['category_count_start'] + $category['count']; break; case 2: $results[$catId] += $category['category_count_start'] + $category['distinct']; break; case 3: $results[$catId] += $category['category_count_start'] + $category['count']; break; } } } // 格式化结果并更新任务状态 $formattedResult = array_map(function ($total, $id) { return ['category_id' => $id, 'total' => $total]; }, $results, array_keys($results)); $this->task->update([ 'status' => 'completed', 'result' => $formattedResult ]); } catch (\Exception $e) { $this->task->update([ 'status' => 'failed', 'error' => $e->getMessage() ]); } } }
3. 重构控制器端点
修改原控制器,改为创建任务并返回任务ID,同时新增轮询查询端点:
<?php namespace App\Http\Controllers; use App\Http\Requests\ReportRequest; use App\Models\ReportTask; use App\Jobs\ProcessCategoryReport; class CategoryReportController extends Controller { public function categoryReportTotals(ReportRequest $request) { // 创建任务记录 $task = ReportTask::create([ 'parent_category_ids' => $request->parent_category_ids ]); // 分发异步任务 ProcessCategoryReport::dispatch($task); // 返回任务ID,供客户端轮询 return response()->json([ 'task_id' => $task->id, 'status' => 'pending', 'message' => '任务已提交,正在处理中' ]); } // 轮询查询结果的端点 public function getReportResult($taskId) { $task = ReportTask::findOrFail($taskId); $response = [ 'task_id' => $task->id, 'status' => $task->status ]; if ($task->status === 'completed') { $response['data'] = $task->result; } elseif ($task->status === 'failed') { $response['error'] = $task->error; } return response()->json($response); } }
4. 路由配置
在routes/api.php中添加路由:
// 提交统计任务 Route::post('/category-report', [CategoryReportController::class, 'categoryReportTotals']); // 查询任务结果 Route::get('/category-report/{taskId}', [CategoryReportController::class, 'getReportResult']);
5. 优化与增强措施
- 并发请求优化:使用
Http::pool实现并发调用,相比循环同步调用性能提升数倍 - 安全加固:
- 确保
ReportRequest验证parent_category_ids为非空整数数组 - 可添加API令牌认证(如Passport)保护端点
- 对任务查询添加权限控制,防止用户访问他人任务
- 确保
- 可扩展性:
- 将聚合逻辑抽离到独立服务类(如
CategoryReportService),便于后续修改统计规则 - 新增分类统计规则只需修改服务类或任务中的判断逻辑
- 将聚合逻辑抽离到独立服务类(如
- 性能优化:
- 对外部API响应添加Redis缓存,避免重复调用相同ID的接口
- 队列改用Redis驱动,相比数据库队列性能更高
- 可靠性保障:
- 任务类设置重试次数,处理API临时故障
- 任务状态跟踪,便于排查失败原因
内容的提问来源于stack exchange,提问作者Thomas
相关产品推荐
相关产品推荐

