| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- <?php
- namespace App\Jobs;
- use App\Models\KnowledgeExplanation;
- use App\Models\Paper;
- use App\Services\ExamPdfExportService;
- use App\Services\TaskManager;
- 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;
- use Throwable;
- class GenerateKnowledgeExplanationPdfJob implements ShouldQueue
- {
- use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(
- public string $taskId,
- public string $knowledgeId,
- public array $knowledgePoints
- ) {
- $this->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 '拔高';
- }
- }
|