|
@@ -129,12 +129,13 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
|
|
|
} catch (\Throwable $e) {
|
|
} catch (\Throwable $e) {
|
|
|
$failed++;
|
|
$failed++;
|
|
|
|
|
|
|
|
- Log::warning('Markdown batch item failed', [
|
|
|
|
|
|
|
+ Log::error('Markdown batch item failed', [
|
|
|
'import_id' => $this->markdownImportId,
|
|
'import_id' => $this->markdownImportId,
|
|
|
'candidate_id' => $record->id,
|
|
'candidate_id' => $record->id,
|
|
|
'sequence' => $record->sequence,
|
|
'sequence' => $record->sequence,
|
|
|
'index' => $record->index,
|
|
'index' => $record->index,
|
|
|
'error' => $e->getMessage(),
|
|
'error' => $e->getMessage(),
|
|
|
|
|
+ 'trace' => $e->getTraceAsString(),
|
|
|
]);
|
|
]);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -152,6 +153,33 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
|
|
|
$this->finalizeIfDone();
|
|
$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
|
|
private function finalizeIfDone(): void
|
|
|
{
|
|
{
|
|
|
$import = MarkdownImport::find($this->markdownImportId);
|
|
$import = MarkdownImport::find($this->markdownImportId);
|
|
@@ -159,40 +187,20 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
|
|
|
return;
|
|
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) {
|
|
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) {
|
|
if ($updated) {
|
|
|
Log::info('Markdown import finalized', [
|
|
Log::info('Markdown import finalized', [
|
|
@@ -204,9 +212,12 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private function refreshProgress(): void
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算进度:返回 [总数量, 已处理数量]
|
|
|
|
|
+ */
|
|
|
|
|
+ private function calculateProgress(): array
|
|
|
{
|
|
{
|
|
|
- // 总候选题数(排除被过滤的)
|
|
|
|
|
|
|
+ // 总候选题数(排除被过滤的和已废弃的)
|
|
|
$total = PreQuestionCandidate::query()
|
|
$total = PreQuestionCandidate::query()
|
|
|
->where('import_id', $this->markdownImportId)
|
|
->where('import_id', $this->markdownImportId)
|
|
|
->where('status', '!=', PreQuestionCandidate::STATUS_SUPERSEDED)
|
|
->where('status', '!=', PreQuestionCandidate::STATUS_SUPERSEDED)
|
|
@@ -216,7 +227,8 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
|
|
|
})
|
|
})
|
|
|
->count();
|
|
->count();
|
|
|
|
|
|
|
|
- // 真正完成AI解析的判断:有stem字段或有有效的ai_confidence
|
|
|
|
|
|
|
+ // 真正完成AI解析的判断:有 stem 字段且不为空,或有有效的 ai_confidence
|
|
|
|
|
+ // 或已经被过滤 (filtered_out=true)
|
|
|
$parsed = PreQuestionCandidate::query()
|
|
$parsed = PreQuestionCandidate::query()
|
|
|
->where('import_id', $this->markdownImportId)
|
|
->where('import_id', $this->markdownImportId)
|
|
|
->where('status', '!=', PreQuestionCandidate::STATUS_SUPERSEDED)
|
|
->where('status', '!=', PreQuestionCandidate::STATUS_SUPERSEDED)
|
|
@@ -225,15 +237,25 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
|
|
|
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.filtered_out')) != 'true'");
|
|
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.filtered_out')) != 'true'");
|
|
|
})
|
|
})
|
|
|
->where(function ($query) {
|
|
->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();
|
|
->count();
|
|
|
|
|
|
|
|
|
|
+ return [$total, $parsed];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function refreshProgress(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ [$total, $parsed] = $this->calculateProgress();
|
|
|
|
|
+
|
|
|
// 计算有stem但AI置信度为0的数量(可能是非题目被错误解析)
|
|
// 计算有stem但AI置信度为0的数量(可能是非题目被错误解析)
|
|
|
$stemOnlyCount = PreQuestionCandidate::query()
|
|
$stemOnlyCount = PreQuestionCandidate::query()
|
|
|
->where('import_id', $this->markdownImportId)
|
|
->where('import_id', $this->markdownImportId)
|
|
@@ -257,9 +279,9 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
|
|
|
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.filtered_out')) = 'true'")
|
|
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.filtered_out')) = 'true'")
|
|
|
->count();
|
|
->count();
|
|
|
|
|
|
|
|
- DB::table('markdown_imports')
|
|
|
|
|
- ->where('id', $this->markdownImportId)
|
|
|
|
|
- ->update([
|
|
|
|
|
|
|
+ $import = MarkdownImport::find($this->markdownImportId);
|
|
|
|
|
+ if ($import) {
|
|
|
|
|
+ $import->update([
|
|
|
'progress_total' => $total,
|
|
'progress_total' => $total,
|
|
|
'progress_current' => min($parsed, $total),
|
|
'progress_current' => min($parsed, $total),
|
|
|
'progress_updated_at' => now(),
|
|
'progress_updated_at' => now(),
|
|
@@ -268,6 +290,7 @@ class ProcessMarkdownCandidateBatch implements ShouldQueue
|
|
|
($stemOnlyCount > 0 ? " (含{$stemOnlyCount}个待筛选)" : '') .
|
|
($stemOnlyCount > 0 ? " (含{$stemOnlyCount}个待筛选)" : '') .
|
|
|
($filteredCount > 0 ? " (已过滤{$filteredCount}个非题目)" : ''),
|
|
($filteredCount > 0 ? " (已过滤{$filteredCount}个非题目)" : ''),
|
|
|
]);
|
|
]);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private function markParsed(PreQuestionCandidate $record): void
|
|
private function markParsed(PreQuestionCandidate $record): void
|