Procházet zdrojové kódy

feat: 批量重生成 PDF 改为队列模式,取消数量限制

- 新增 RegeneratePdfJob 队列任务
- regeneratePdfBatch 改为投递到 pdf 队列,接口立即返回
- 移除 limit 参数,按时间区间全量处理

Made-with: Cursor
yemeishu před 1 týdnem
rodič
revize
f5d6d71708

+ 26 - 57
app/Http/Controllers/ExamPdfController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers;
 
+use App\Jobs\RegeneratePdfJob;
 use App\Models\Paper;
 use App\Support\PaperNaming;
 use App\Services\QuestionBankService;
@@ -1323,21 +1324,18 @@ class ExamPdfController extends Controller
     }
 
     /**
-     * 批量重新生成 PDF:按时间区间遍历 completed_at=NULL 的卷子,使用新的分析重新生成统一 PDF 入库
+     * 批量重新生成 PDF:按时间区间遍历 completed_at=NULL 的卷子,投递到队列异步处理
      *
-     * 基于 /api/papers/{paper_id}/regenerate 逻辑,遍历指定时间段创建且未完成的试卷
+     * 基于 /api/papers/{paper_id}/regenerate 逻辑,将符合条件的试卷投递到 pdf 队列
      * 时间格式 Y-m-d,start_date 从 00:00:00,end_date 到 23:59:59。
+     * 接口立即返回,实际生成由 queue worker 处理。
      *
      * @return \Illuminate\Http\JsonResponse
      */
     public function regeneratePdfBatch(Request $request)
     {
-        // 批量 PDF 生成耗时较长,解除 30 秒执行限制(Guzzle 等会触发)
-        set_time_limit(0);
-
         $startDate = $request->input('start_date');
         $endDate = $request->input('end_date');
-        $limit = (int) $request->input('limit', 100);
 
         $datePattern = '/^\d{4}-\d{2}-\d{2}$/';
         if (empty($startDate) || ! preg_match($datePattern, $startDate)) {
@@ -1366,10 +1364,9 @@ class ExamPdfController extends Controller
             ? filter_var($request->input('include_kp_explain'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
             : null;
 
-        Log::info('RegeneratePdfBatch: 开始批量重生成', [
+        Log::info('RegeneratePdfBatch: 投递队列', [
             'start_date' => $startDate,
             'end_date' => $endDate,
-            'limit' => $limit,
         ]);
 
         try {
@@ -1377,7 +1374,6 @@ class ExamPdfController extends Controller
                 ->whereBetween('created_at', [$startTime, $endTime])
                 ->whereNull('completed_at')
                 ->orderBy('paper_id')
-                ->limit($limit)
                 ->get();
 
             if ($papers->isEmpty()) {
@@ -1386,73 +1382,46 @@ class ExamPdfController extends Controller
                     'message' => '未找到符合条件的试卷',
                     'start_date' => $startDate,
                     'end_date' => $endDate,
-                    'total' => 0,
-                    'success_count' => 0,
-                    'fail_count' => 0,
-                    'details' => [],
+                    'queued' => 0,
+                    'skipped' => 0,
+                    'paper_ids' => [],
                 ]);
             }
 
-            $pdfService = app(\App\Services\ExamPdfExportService::class);
-            $successList = [];
-            $failList = [];
-
+            $queued = [];
+            $skipped = 0;
             foreach ($papers as $paper) {
                 if ($paper->questions->isEmpty()) {
-                    $failList[] = [
-                        'paper_id' => $paper->paper_id,
-                        'reason' => '无题目',
-                    ];
-                    continue;
-                }
+                    $skipped++;
 
-                $useKpExplain = $includeKpExplain ?? ($paper->paper_type !== 0);
-
-                try {
-                    $pdfUrl = $pdfService->generateUnifiedPdf($paper->paper_id, $useKpExplain);
-                    if ($pdfUrl) {
-                        $successList[] = [
-                            'paper_id' => $paper->paper_id,
-                            'pdf_url' => $pdfUrl,
-                        ];
-                        Log::info('RegeneratePdfBatch: 成功', ['paper_id' => $paper->paper_id]);
-                    } else {
-                        $failList[] = [
-                            'paper_id' => $paper->paper_id,
-                            'reason' => 'generateUnifiedPdf 返回空',
-                        ];
-                    }
-                } catch (\Exception $e) {
-                    Log::error('RegeneratePdfBatch: 异常', [
-                        'paper_id' => $paper->paper_id,
-                        'error' => $e->getMessage(),
-                    ]);
-                    $failList[] = [
-                        'paper_id' => $paper->paper_id,
-                        'reason' => $e->getMessage(),
-                    ];
+                    continue;
                 }
+                RegeneratePdfJob::dispatch($paper->paper_id, $includeKpExplain);
+                $queued[] = $paper->paper_id;
             }
 
+            Log::info('RegeneratePdfBatch: 已投递', [
+                'start_date' => $startDate,
+                'end_date' => $endDate,
+                'queued' => count($queued),
+                'skipped' => $skipped,
+            ]);
+
             return response()->json([
                 'success' => true,
-                'message' => '批量重生成完成',
+                'message' => '已投递到 pdf 队列,请确保 queue worker 正在运行',
                 'start_date' => $startDate,
                 'end_date' => $endDate,
-                'total' => $papers->count(),
-                'success_count' => count($successList),
-                'fail_count' => count($failList),
-                'details' => [
-                    'succeeded' => $successList,
-                    'failed' => $failList,
-                ],
+                'queued' => count($queued),
+                'skipped' => $skipped,
+                'paper_ids' => $queued,
             ]);
         } catch (\Exception $e) {
             Log::error('RegeneratePdfBatch: 批量异常', ['error' => $e->getMessage()]);
 
             return response()->json([
                 'success' => false,
-                'message' => '批量重生成异常:'.$e->getMessage(),
+                'message' => '批量投递异常:'.$e->getMessage(),
             ], 500);
         }
     }

+ 64 - 0
app/Jobs/RegeneratePdfJob.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\Paper;
+use App\Services\ExamPdfExportService;
+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\Log;
+
+/**
+ * 单个试卷 PDF 重新生成任务(用于批量重生成 API 的队列)
+ */
+class RegeneratePdfJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public string $paperId;
+
+    public ?bool $includeKpExplain;
+
+    public int $tries = 2;
+
+    public int $timeout = 300;
+
+    public function __construct(string $paperId, ?bool $includeKpExplain = null)
+    {
+        $this->paperId = $paperId;
+        $this->includeKpExplain = $includeKpExplain;
+
+        $this->onQueue('pdf');
+    }
+
+    public function handle(ExamPdfExportService $pdfExportService): void
+    {
+        $paper = Paper::with('questions')->find($this->paperId);
+        if (! $paper || $paper->questions->isEmpty()) {
+            Log::warning('RegeneratePdfJob: 跳过无题目试卷', ['paper_id' => $this->paperId]);
+
+            return;
+        }
+
+        $useKpExplain = $this->includeKpExplain ?? ($paper->paper_type !== 0);
+
+        try {
+            Log::info('RegeneratePdfJob: 开始', ['paper_id' => $this->paperId]);
+            $pdfUrl = $pdfExportService->generateUnifiedPdf($this->paperId, $useKpExplain);
+            if ($pdfUrl) {
+                Log::info('RegeneratePdfJob: 成功', ['paper_id' => $this->paperId, 'pdf_url' => $pdfUrl]);
+            } else {
+                Log::warning('RegeneratePdfJob: 返回空', ['paper_id' => $this->paperId]);
+            }
+        } catch (\Exception $e) {
+            Log::error('RegeneratePdfJob: 异常', [
+                'paper_id' => $this->paperId,
+                'error' => $e->getMessage(),
+            ]);
+            throw $e;
+        }
+    }
+}