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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 04:28:10