onQueue('pdf'); $this->afterCommit(); } public int $tries = 3; public int $timeout = 300; public function handle( ExamPdfExportService $examPdfExportService, TaskManager $taskManager ): void { try { $record = KnowledgeExplanation::query() ->where('knowledge_id', $this->knowledgeId) ->first(); if (! $record) { $taskManager->markTaskFailed($this->taskId, '知识点讲解记录不存在'); return; } $taskManager->updateTaskProgress($this->taskId, 70, '开始渲染知识点讲解PDF...'); $pdfUrl = $examPdfExportService->generateKnowledgeExplanationStandalonePdf($record, $this->knowledgePoints); if (! $pdfUrl) { $record->update(['status' => 'failed']); $taskManager->markTaskFailed($this->taskId, '知识点讲解PDF生成失败'); return; } $record->update([ 'status' => 'completed', 'pdf_url' => $pdfUrl, 'generated_at' => now(), ]); $taskSnapshot = $taskManager->getTaskStatus($this->taskId); $taskData = is_array($taskSnapshot) && isset($taskSnapshot['data']) && is_array($taskSnapshot['data']) ? $taskSnapshot['data'] : []; $examContent = $this->buildKnowledgeExamContent($record, $pdfUrl, $taskData); $this->syncPaperRecord($record, $pdfUrl, $examContent); $difficultyCategoryForStats = (int) ($examContent['paper_info']['difficulty_category'] ?? 2); $taskManager->markTaskCompleted($this->taskId, [ 'paper_id' => $this->knowledgeId, 'knowledge_id' => $this->knowledgeId, 'pdfs' => [ 'all_pdf' => $pdfUrl, ], 'exam_content' => $examContent, 'stats' => [ 'difficulty_category' => $difficultyCategoryForStats, 'total_selected' => count($examContent['questions'] ?? []), 'difficulty_distribution_applied' => true, ], ]); $taskManager->sendCallback($this->taskId); } catch (\Throwable $e) { Log::error('GenerateKnowledgeExplanationPdfJob 失败', [ 'task_id' => $this->taskId, 'knowledge_id' => $this->knowledgeId, 'error' => $e->getMessage(), ]); KnowledgeExplanation::query() ->where('knowledge_id', $this->knowledgeId) ->update(['status' => 'failed']); $taskManager->markTaskFailed($this->taskId, $e->getMessage()); } } public function failed(Throwable $exception): void { KnowledgeExplanation::query() ->where('knowledge_id', $this->knowledgeId) ->update(['status' => 'failed']); app(TaskManager::class)->markTaskFailed($this->taskId, $exception->getMessage()); } private function syncPaperRecord(KnowledgeExplanation $record, string $pdfUrl, array $examContent): void { $paperId = (string) ($record->knowledge_id ?? ''); if ($paperId === '') { return; } $displayCode = (string) preg_replace('/^(paper_|knowledge_)/', '', $paperId); if ($displayCode === '') { $displayCode = $paperId; } $paperInfo = $examContent['paper_info'] ?? []; $totalQuestions = (int) ($paperInfo['total_questions'] ?? 0); $difficultyCategory = $paperInfo['difficulty_category'] ?? null; Paper::query()->updateOrCreate( ['paper_id' => $paperId], [ 'student_id' => (string) ($record->student_id ?? ''), 'teacher_id' => (string) ($record->teacher_id ?? ''), 'params' => [ 'source' => 'knowledge_explanation', 'knowledge_id' => $paperId, 'kp_codes' => is_array($record->kp_codes) ? $record->kp_codes : [], ], 'paper_name' => '知识点讲解_' . $displayCode, 'paper_type' => 22, 'total_questions' => $totalQuestions, 'total_score' => 100, 'status' => 'completed', 'difficulty_category' => $difficultyCategory !== null && $difficultyCategory !== '' ? (string) $difficultyCategory : null, 'exam_pdf_url' => $pdfUrl, 'grading_pdf_url' => null, 'all_pdf_url' => $pdfUrl, 'completed_at' => now(), ] ); } private function buildKnowledgeExamContent(KnowledgeExplanation $record, string $pdfUrl, array $taskData = []): array { $paperId = (string) ($record->knowledge_id ?? ''); $displayCode = (string) preg_replace('/^(paper_|knowledge_)/', '', $paperId); if ($displayCode === '') { $displayCode = $paperId; } $kpCodes = is_array($record->kp_codes) ? array_values($record->kp_codes) : []; $difficultyCategoryRaw = $taskData['difficulty_category'] ?? null; $difficultyCategoryStr = $difficultyCategoryRaw !== null && $difficultyCategoryRaw !== '' ? (string) $difficultyCategoryRaw : '2'; $questions = $this->buildKnowledgeQuestions(100); $knowledgeDistribution = []; foreach ($kpCodes as $kpCode) { $knowledgeDistribution[(string) $kpCode] = 0; } foreach ($questions as $q) { $kp = (string) ($q['knowledge_point'] ?? ''); if ($kp === '') { continue; } $knowledgeDistribution[$kp] = (int) ($knowledgeDistribution[$kp] ?? 0) + 1; } $typeDistribution = ['choice' => 0, 'fill' => 0, 'answer' => 0]; foreach ($questions as $q) { $t = (string) ($q['question_type'] ?? 'answer'); if ($t === 'choice') { $typeDistribution['choice']++; } elseif ($t === 'fill') { $typeDistribution['fill']++; } else { $typeDistribution['answer']++; } } $difficultyDistribution = []; $difficultySum = 0.0; $difficultyCount = 0; foreach ($questions as $q) { $dl = $q['metadata']['difficulty_label'] ?? ''; if ($dl !== '') { $difficultyDistribution[$dl] = ($difficultyDistribution[$dl] ?? 0) + 1; } if (isset($q['difficulty']) && is_numeric($q['difficulty'])) { $difficultySum += (float) $q['difficulty']; $difficultyCount++; } } $averageDifficulty = $difficultyCount > 0 ? $difficultySum / $difficultyCount : null; $totalEstimatedTime = count($questions) * 300; return [ 'paper_info' => [ 'paper_id' => $paperId, 'paper_name' => '知识点讲解_' . $displayCode, 'student_id' => (string) ($record->student_id ?? ''), 'teacher_id' => (string) ($record->teacher_id ?? ''), 'total_questions' => count($questions), 'total_score' => 100, 'difficulty_category' => $difficultyCategoryStr, 'created_at' => optional($record->created_at)->toISOString(), 'updated_at' => optional($record->updated_at)->toISOString(), 'exam_code' => $displayCode, 'grading_code' => $displayCode, 'paper_id_num' => $displayCode, ], 'questions' => $questions, 'knowledge_points' => $kpCodes, 'statistics' => [ 'type_distribution' => $typeDistribution, 'difficulty_distribution' => $difficultyDistribution, 'knowledge_point_distribution' => $knowledgeDistribution, 'average_difficulty' => $averageDifficulty, 'total_estimated_time' => $totalEstimatedTime, ], 'pdfs' => [ 'all_pdf' => $pdfUrl, ], 'source' => 'knowledge_explanation', ]; } private function buildKnowledgeQuestions(int $totalScore = 100): array { $questions = []; $seq = 1; foreach ($this->knowledgePoints as $point) { $kpCode = (string) ($point['kp_code'] ?? ''); $kpName = (string) ($point['kp_name'] ?? $kpCode); $cases = is_array($point['cases'] ?? null) ? $point['cases'] : []; foreach ($cases as $case) { $questionId = (int) ($case['question_id'] ?? 0); $qType = (string) ($case['question_type'] ?? 'answer'); $solutionText = (string) ($case['solution'] ?? ''); $difficultyVal = isset($case['difficulty']) && is_numeric($case['difficulty']) ? (float) $case['difficulty'] : null; $questions[] = [ 'question_number' => $seq++, 'question_id' => $questionId > 0 ? (string) $questionId : '', 'question_bank_id' => $questionId > 0 ? $questionId : null, 'question_type' => $qType, 'knowledge_point' => $kpCode, 'knowledge_point_name' => $kpName, 'difficulty' => $difficultyVal, 'score' => 0, 'estimated_time' => 300, 'stem' => (string) ($case['stem'] ?? ''), 'options' => is_array($case['options'] ?? null) ? $case['options'] : [], 'correct_answer' => (string) ($case['answer'] ?? ''), 'solution' => $solutionText, 'metadata' => [ 'source_type' => (string) ($case['source_type'] ?? ''), 'source_label' => (string) ($case['source_label'] ?? ''), 'is_wrong_case' => (bool) ($case['is_wrong_case'] ?? false), 'child_kp_code' => $case['child_kp_code'] ?? null, 'child_kp_name' => $case['child_kp_name'] ?? null, 'has_solution' => $solutionText !== '', 'is_choice' => $qType === 'choice', 'is_fill' => $qType === 'fill', 'is_answer' => $qType === 'answer', 'difficulty_label' => $this->difficultyLabelForPayload($difficultyVal), 'question_type_label' => $this->questionTypeLabelForPayload($qType), ], ]; } } $count = count($questions); if ($count > 0) { $baseScore = intdiv($totalScore, $count); $remainder = $totalScore - ($baseScore * $count); foreach ($questions as &$q) { $q['score'] = $baseScore; } unset($q); if ($remainder > 0) { // 余数分散到末尾若干题,保证总分精确为 totalScore for ($i = $count - $remainder; $i < $count; $i++) { if ($i >= 0 && isset($questions[$i])) { $questions[$i]['score'] += 1; } } } } return $questions; } private function questionTypeLabelForPayload(?string $type): string { return match ($type) { 'choice' => '选择题', 'fill' => '填空题', 'answer' => '解答题', default => '未知题型', }; } private function difficultyLabelForPayload(?float $difficulty): string { if ($difficulty === null) { return '未知'; } if ($difficulty <= 0.4) { return '基础'; } if ($difficulty <= 0.7) { return '中等'; } return '拔高'; } }