Bladeren bron

学案报告

yemeishu 4 dagen geleden
bovenliggende
commit
548eeb5fb6
2 gewijzigde bestanden met toevoegingen van 118 en 45 verwijderingen
  1. 68 45
      app/Jobs/ProcessMarkdownCandidateBatch.php
  2. 50 0
      app/Models/MarkdownImport.php

+ 68 - 45
app/Jobs/ProcessMarkdownCandidateBatch.php

@@ -129,12 +129,13 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
             } catch (\Throwable $e) {
                 $failed++;
 
-                Log::warning('Markdown batch item failed', [
+                Log::error('Markdown batch item failed', [
                     'import_id' => $this->markdownImportId,
                     'candidate_id' => $record->id,
                     'sequence' => $record->sequence,
                     'index' => $record->index,
                     'error' => $e->getMessage(),
+                    'trace' => $e->getTraceAsString(),
                 ]);
             }
         }
@@ -152,6 +153,33 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
         $this->finalizeIfDone();
     }
 
+    /**
+     * 任务失败时的处理
+     */
+    public function failed(\Throwable $exception): void
+    {
+        Log::error('Markdown batch job failed permanently', [
+            'import_id' => $this->markdownImportId,
+            'sequence_start' => $this->sequenceStart,
+            'sequence_end' => $this->sequenceEnd,
+            'attempts' => $this->attempts(),
+            'error' => $exception->getMessage(),
+            'trace' => $exception->getTraceAsString(),
+        ]);
+
+        // 标记导入记录为失败状态,避免卡住
+        $import = MarkdownImport::find($this->markdownImportId);
+        if ($import) {
+            $import->update([
+                'status' => MarkdownImport::STATUS_FAILED,
+                'progress_stage' => MarkdownImport::STAGE_FAILED,
+                'progress_message' => 'AI 解析任务失败',
+                'error_message' => '队列任务执行失败,已超过最大重试次数',
+                'processing_finished_at' => now(),
+            ]);
+        }
+    }
+
     private function finalizeIfDone(): void
     {
         $import = MarkdownImport::find($this->markdownImportId);
@@ -159,40 +187,20 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
             return;
         }
 
-        // 重新计算真实的解析进度
-        $total = PreQuestionCandidate::query()
-            ->where('import_id', $this->markdownImportId)
-            ->where('status', '!=', PreQuestionCandidate::STATUS_SUPERSEDED)
-            ->count();
-
-        // 真正完成AI解析的记录数
-        $parsed = PreQuestionCandidate::query()
-            ->where('import_id', $this->markdownImportId)
-            ->where('status', '!=', PreQuestionCandidate::STATUS_SUPERSEDED)
-            ->where(function ($query) {
-                $query->whereNotNull('stem')
-                    ->where('stem', '!=', '')
-                    ->orWhere(function ($q) {
-                        $q->whereNotNull('ai_confidence')
-                          ->where('ai_confidence', '>', 0);
-                    });
-            })
-            ->count();
+        // 统一使用与 refreshProgress 相同的查询逻辑
+        [$total, $parsed] = $this->calculateProgress();
 
-        // 如果所有候选题都已解析完成,更新状态
+        // 只有当所有候选题都已处理(解析或过滤)完成时才更新状态
         if ($total > 0 && $parsed >= $total) {
-            $updated = DB::table('markdown_imports')
-                ->where('id', $this->markdownImportId)
-                ->where('status', 'processing')
-                ->update([
-                    'status' => 'parsed',
-                    'progress_stage' => MarkdownImport::STAGE_PARSED,
-                    'progress_message' => "解析完成,等待人工校对 ({$parsed}/{$total})",
-                    'progress_total' => $total,
-                    'progress_current' => $parsed,
-                    'progress_updated_at' => now(),
-                    'processing_finished_at' => now(),
-                ]);
+            $updated = $import->update([
+                'status' => MarkdownImport::STATUS_PARSED,
+                'progress_stage' => MarkdownImport::STAGE_PARSED,
+                'progress_message' => "解析完成,等待人工校对 ({$parsed}/{$total})",
+                'progress_total' => $total,
+                'progress_current' => $parsed,
+                'progress_updated_at' => now(),
+                'processing_finished_at' => now(),
+            ]);
 
             if ($updated) {
                 Log::info('Markdown import finalized', [
@@ -204,9 +212,12 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
         }
     }
 
-    private function refreshProgress(): void
+    /**
+     * 计算进度:返回 [总数量, 已处理数量]
+     */
+    private function calculateProgress(): array
     {
-        // 总候选题数(排除被过滤的)
+        // 总候选题数(排除被过滤的和已废弃的
         $total = PreQuestionCandidate::query()
             ->where('import_id', $this->markdownImportId)
             ->where('status', '!=', PreQuestionCandidate::STATUS_SUPERSEDED)
@@ -216,7 +227,8 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
             })
             ->count();
 
-        // 真正完成AI解析的判断:有stem字段或有有效的ai_confidence
+        // 真正完成AI解析的判断:有 stem 字段且不为空,或有有效的 ai_confidence
+        // 或已经被过滤 (filtered_out=true)
         $parsed = PreQuestionCandidate::query()
             ->where('import_id', $this->markdownImportId)
             ->where('status', '!=', PreQuestionCandidate::STATUS_SUPERSEDED)
@@ -225,15 +237,25 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
                     ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.filtered_out')) != 'true'");
             })
             ->where(function ($query) {
-                $query->whereNotNull('stem')
-                    ->where('stem', '!=', '')
-                    ->orWhere(function ($q) {
-                        $q->whereNotNull('ai_confidence')
-                          ->where('ai_confidence', '>', 0);
-                    });
+                $query->where(function ($q) {
+                    // 有题目内容
+                    $q->whereNotNull('stem')
+                      ->where('stem', '!=', '');
+                })->orWhere(function ($q) {
+                    // 或有有效的AI置信度
+                    $q->whereNotNull('ai_confidence')
+                      ->where('ai_confidence', '>', 0);
+                });
             })
             ->count();
 
+        return [$total, $parsed];
+    }
+
+    private function refreshProgress(): void
+    {
+        [$total, $parsed] = $this->calculateProgress();
+
         // 计算有stem但AI置信度为0的数量(可能是非题目被错误解析)
         $stemOnlyCount = PreQuestionCandidate::query()
             ->where('import_id', $this->markdownImportId)
@@ -257,9 +279,9 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
             ->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.filtered_out')) = 'true'")
             ->count();
 
-        DB::table('markdown_imports')
-            ->where('id', $this->markdownImportId)
-            ->update([
+        $import = MarkdownImport::find($this->markdownImportId);
+        if ($import) {
+            $import->update([
                 'progress_total' => $total,
                 'progress_current' => min($parsed, $total),
                 'progress_updated_at' => now(),
@@ -268,6 +290,7 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
                     ($stemOnlyCount > 0 ? " (含{$stemOnlyCount}个待筛选)" : '') .
                     ($filteredCount > 0 ? " (已过滤{$filteredCount}个非题目)" : ''),
             ]);
+        }
     }
 
     private function markParsed(PreQuestionCandidate $record): void

+ 50 - 0
app/Models/MarkdownImport.php

@@ -219,6 +219,56 @@ class MarkdownImport extends Model
         };
     }
 
+    /**
+     * 检查并修复卡住的状态(超过30分钟无进度则重置)
+     */
+    public function checkAndFixStuckStatus(): void
+    {
+        if ($this->status !== self::STATUS_PROCESSING) {
+            return;
+        }
+
+        // 超过30分钟无进度则重置
+        if ($this->progress_updated_at?->lt(now()->subMinutes(30))) {
+            \Log::warning('Auto-fixing stuck markdown import', [
+                'import_id' => $this->id,
+                'last_update' => $this->progress_updated_at,
+            ]);
+
+            $this->update([
+                'status' => self::STATUS_PENDING,
+                'progress_stage' => self::STAGE_QUEUED,
+                'progress_message' => '自动重置为待处理(超时)',
+                'error_message' => '队列任务可能已丢失,请重新提交',
+                'processing_finished_at' => now(),
+            ]);
+        }
+    }
+
+    /**
+     * 静态方法:批量检查并修复卡住的记录
+     */
+    public static function checkAndFixAllStuckRecords(): int
+    {
+        $stuckRecords = self::where('status', self::STATUS_PROCESSING)
+            ->where('progress_updated_at', '<', now()->subMinutes(30))
+            ->get();
+
+        $fixed = 0;
+        foreach ($stuckRecords as $record) {
+            $record->checkAndFixStuckStatus();
+            $fixed++;
+        }
+
+        if ($fixed > 0) {
+            \Log::info('Auto-fixed stuck markdown imports', [
+                'count' => $fixed,
+            ]);
+        }
+
+        return $fixed;
+    }
+
     public function parseFilename(): array
     {
         if (empty($this->file_name)) {