Jelajahi Sumber

feat(exam-analysis): validate prerequisites before pdf queue enqueue

Add validateAnalysisReportEnqueue (paper/student/OCR/consistency + non-empty
analysis JSON). Call from generateReport and Filament PDF endpoint; API returns
422 on InvalidArgumentException.

Made-with: Cursor
yemeishu 2 minggu lalu
induk
melakukan
358bce0681

+ 5 - 0
app/Http/Controllers/Api/ExamAnalysisApiController.php

@@ -72,6 +72,11 @@ class ExamAnalysisApiController extends Controller
 
             return response()->json($payload, 200, [], JSON_UNESCAPED_SLASHES);
 
+        } catch (\InvalidArgumentException $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
         } catch (\Exception $e) {
             Log::error('学情报告API失败', [
                 'paper_id' => $paperId,

+ 11 - 1
app/Http/Controllers/ExamAnalysisPdfController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers;
 
 use App\Jobs\GenerateAnalysisPdfJob;
+use App\Services\ExamAnalysisService;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
@@ -12,7 +13,7 @@ class ExamAnalysisPdfController extends Controller
 {
     private const ENQUEUE_LOCK_TTL_SECONDS = 120;
 
-    public function show(Request $request)
+    public function show(Request $request, ExamAnalysisService $examAnalysisService)
     {
         $paperId = $request->query('paperId');
         $studentId = $request->query('studentId');
@@ -50,6 +51,15 @@ class ExamAnalysisPdfController extends Controller
             ]);
         }
 
+        try {
+            $examAnalysisService->validateAnalysisReportEnqueue($paperId, $studentId, $recordId);
+        } catch (\InvalidArgumentException $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage(),
+            ], 422);
+        }
+
         $lockKey = sprintf('analysis_pdf:enqueue:%s:%s', $paperId, $studentId);
         $acquired = Cache::add($lockKey, now()->timestamp, now()->addSeconds(self::ENQUEUE_LOCK_TTL_SECONDS));
         if (! $acquired) {

+ 53 - 0
app/Services/ExamAnalysisService.php

@@ -5,9 +5,11 @@ namespace App\Services;
 use App\DTO\ExamAnalysisDataDto;
 use App\DTO\ReportPayloadDto;
 use App\Jobs\ProcessAnalysisReportTaskJob;
+use App\Models\OCRRecord;
 use App\Models\Paper;
 use App\Models\PaperQuestion;
 use App\Models\Student;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
 /**
@@ -25,12 +27,63 @@ class ExamAnalysisService
         private readonly ExamAnswerAnalysisService $examAnswerAnalysisService
     ) {}
 
+    /**
+     * 入队前校验:试卷/学生/分析数据等,避免无效任务进入 pdf 队列占用 worker。
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function validateAnalysisReportEnqueue(string $paperId, string $studentId, ?string $recordId = null): void
+    {
+        $studentIdNorm = (string) $studentId;
+
+        $paper = Paper::query()->select(['paper_id', 'student_id'])->find($paperId);
+        if (! $paper) {
+            throw new \InvalidArgumentException('试卷不存在');
+        }
+
+        if ($paper->student_id !== null && $paper->student_id !== ''
+            && (string) $paper->student_id !== $studentIdNorm) {
+            throw new \InvalidArgumentException('学生与试卷不匹配');
+        }
+
+        if (! Student::query()->where('student_id', $studentId)->exists()) {
+            throw new \InvalidArgumentException('学生不存在');
+        }
+
+        if ($recordId !== null && $recordId !== '') {
+            $ocr = OCRRecord::query()->find($recordId);
+            if (! $ocr) {
+                throw new \InvalidArgumentException('OCR 记录不存在');
+            }
+            if ((string) $ocr->student_id !== $studentIdNorm) {
+                throw new \InvalidArgumentException('OCR 记录与学生不匹配');
+            }
+        }
+
+        $row = DB::connection('mysql')->table('exam_analysis_results')
+            ->where('paper_id', $paperId)
+            ->where('student_id', $studentId)
+            ->orderByDesc('created_at')
+            ->first();
+
+        if (! $row || $row->analysis_data === null || $row->analysis_data === '') {
+            throw new \InvalidArgumentException('尚未完成答题分析或分析数据为空,请先完成阅卷分析后再生成报告');
+        }
+
+        $decoded = json_decode((string) $row->analysis_data, true);
+        if (! is_array($decoded) || $decoded === []) {
+            throw new \InvalidArgumentException('分析数据无效,无法生成 PDF');
+        }
+    }
+
     /**
      * 生成学情报告
      * 异步模式,立即返回任务ID
      */
     public function generateReport(string $paperId, string $studentId, ?string $recordId = null): string
     {
+        $this->validateAnalysisReportEnqueue($paperId, $studentId, $recordId);
+
         // 创建异步任务
         $taskId = $this->taskManager->createTask(
             TaskManager::TASK_TYPE_ANALYSIS,