GenerateKnowledgeExplanationPdfJob.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. <?php
  2. namespace App\Jobs;
  3. use App\Models\KnowledgeExplanation;
  4. use App\Models\Paper;
  5. use App\Services\ExamPdfExportService;
  6. use App\Services\TaskManager;
  7. use Illuminate\Bus\Queueable;
  8. use Illuminate\Contracts\Queue\ShouldQueue;
  9. use Illuminate\Foundation\Bus\Dispatchable;
  10. use Illuminate\Queue\InteractsWithQueue;
  11. use Illuminate\Queue\SerializesModels;
  12. use Illuminate\Support\Facades\Log;
  13. use Throwable;
  14. class GenerateKnowledgeExplanationPdfJob implements ShouldQueue
  15. {
  16. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  17. public function __construct(
  18. public string $taskId,
  19. public string $knowledgeId,
  20. public array $knowledgePoints
  21. ) {
  22. $this->onQueue('pdf');
  23. $this->afterCommit();
  24. }
  25. public int $tries = 3;
  26. public int $timeout = 300;
  27. public function handle(
  28. ExamPdfExportService $examPdfExportService,
  29. TaskManager $taskManager
  30. ): void {
  31. try {
  32. $record = KnowledgeExplanation::query()
  33. ->where('knowledge_id', $this->knowledgeId)
  34. ->first();
  35. if (! $record) {
  36. $taskManager->markTaskFailed($this->taskId, '知识点讲解记录不存在');
  37. return;
  38. }
  39. $taskManager->updateTaskProgress($this->taskId, 70, '开始渲染知识点讲解PDF...');
  40. $pdfUrl = $examPdfExportService->generateKnowledgeExplanationStandalonePdf($record, $this->knowledgePoints);
  41. if (! $pdfUrl) {
  42. $record->update(['status' => 'failed']);
  43. $taskManager->markTaskFailed($this->taskId, '知识点讲解PDF生成失败');
  44. return;
  45. }
  46. $record->update([
  47. 'status' => 'completed',
  48. 'pdf_url' => $pdfUrl,
  49. 'generated_at' => now(),
  50. ]);
  51. $taskSnapshot = $taskManager->getTaskStatus($this->taskId);
  52. $taskData = is_array($taskSnapshot) && isset($taskSnapshot['data']) && is_array($taskSnapshot['data'])
  53. ? $taskSnapshot['data']
  54. : [];
  55. $examContent = $this->buildKnowledgeExamContent($record, $pdfUrl, $taskData);
  56. $this->syncPaperRecord($record, $pdfUrl, $examContent);
  57. $difficultyCategoryForStats = (int) ($examContent['paper_info']['difficulty_category'] ?? 2);
  58. $taskManager->markTaskCompleted($this->taskId, [
  59. 'paper_id' => $this->knowledgeId,
  60. 'knowledge_id' => $this->knowledgeId,
  61. 'pdfs' => [
  62. 'all_pdf' => $pdfUrl,
  63. ],
  64. 'exam_content' => $examContent,
  65. 'stats' => [
  66. 'difficulty_category' => $difficultyCategoryForStats,
  67. 'total_selected' => count($examContent['questions'] ?? []),
  68. 'difficulty_distribution_applied' => true,
  69. ],
  70. ]);
  71. $taskManager->sendCallback($this->taskId);
  72. } catch (\Throwable $e) {
  73. Log::error('GenerateKnowledgeExplanationPdfJob 失败', [
  74. 'task_id' => $this->taskId,
  75. 'knowledge_id' => $this->knowledgeId,
  76. 'error' => $e->getMessage(),
  77. ]);
  78. KnowledgeExplanation::query()
  79. ->where('knowledge_id', $this->knowledgeId)
  80. ->update(['status' => 'failed']);
  81. $taskManager->markTaskFailed($this->taskId, $e->getMessage());
  82. }
  83. }
  84. public function failed(Throwable $exception): void
  85. {
  86. KnowledgeExplanation::query()
  87. ->where('knowledge_id', $this->knowledgeId)
  88. ->update(['status' => 'failed']);
  89. app(TaskManager::class)->markTaskFailed($this->taskId, $exception->getMessage());
  90. }
  91. private function syncPaperRecord(KnowledgeExplanation $record, string $pdfUrl, array $examContent): void
  92. {
  93. $paperId = (string) ($record->knowledge_id ?? '');
  94. if ($paperId === '') {
  95. return;
  96. }
  97. $displayCode = (string) preg_replace('/^(paper_|knowledge_)/', '', $paperId);
  98. if ($displayCode === '') {
  99. $displayCode = $paperId;
  100. }
  101. $paperInfo = $examContent['paper_info'] ?? [];
  102. $totalQuestions = (int) ($paperInfo['total_questions'] ?? 0);
  103. $difficultyCategory = $paperInfo['difficulty_category'] ?? null;
  104. Paper::query()->updateOrCreate(
  105. ['paper_id' => $paperId],
  106. [
  107. 'student_id' => (string) ($record->student_id ?? ''),
  108. 'teacher_id' => (string) ($record->teacher_id ?? ''),
  109. 'params' => [
  110. 'source' => 'knowledge_explanation',
  111. 'knowledge_id' => $paperId,
  112. 'kp_codes' => is_array($record->kp_codes) ? $record->kp_codes : [],
  113. ],
  114. 'paper_name' => '知识点讲解_' . $displayCode,
  115. 'paper_type' => 22,
  116. 'total_questions' => $totalQuestions,
  117. 'total_score' => 100,
  118. 'status' => 'completed',
  119. 'difficulty_category' => $difficultyCategory !== null && $difficultyCategory !== ''
  120. ? (string) $difficultyCategory
  121. : null,
  122. 'exam_pdf_url' => $pdfUrl,
  123. 'grading_pdf_url' => null,
  124. 'all_pdf_url' => $pdfUrl,
  125. 'completed_at' => now(),
  126. ]
  127. );
  128. }
  129. private function buildKnowledgeExamContent(KnowledgeExplanation $record, string $pdfUrl, array $taskData = []): array
  130. {
  131. $paperId = (string) ($record->knowledge_id ?? '');
  132. $displayCode = (string) preg_replace('/^(paper_|knowledge_)/', '', $paperId);
  133. if ($displayCode === '') {
  134. $displayCode = $paperId;
  135. }
  136. $kpCodes = is_array($record->kp_codes) ? array_values($record->kp_codes) : [];
  137. $difficultyCategoryRaw = $taskData['difficulty_category'] ?? null;
  138. $difficultyCategoryStr = $difficultyCategoryRaw !== null && $difficultyCategoryRaw !== ''
  139. ? (string) $difficultyCategoryRaw
  140. : '2';
  141. $questions = $this->buildKnowledgeQuestions(100);
  142. $knowledgeDistribution = [];
  143. foreach ($kpCodes as $kpCode) {
  144. $knowledgeDistribution[(string) $kpCode] = 0;
  145. }
  146. foreach ($questions as $q) {
  147. $kp = (string) ($q['knowledge_point'] ?? '');
  148. if ($kp === '') {
  149. continue;
  150. }
  151. $knowledgeDistribution[$kp] = (int) ($knowledgeDistribution[$kp] ?? 0) + 1;
  152. }
  153. $typeDistribution = ['choice' => 0, 'fill' => 0, 'answer' => 0];
  154. foreach ($questions as $q) {
  155. $t = (string) ($q['question_type'] ?? 'answer');
  156. if ($t === 'choice') {
  157. $typeDistribution['choice']++;
  158. } elseif ($t === 'fill') {
  159. $typeDistribution['fill']++;
  160. } else {
  161. $typeDistribution['answer']++;
  162. }
  163. }
  164. $difficultyDistribution = [];
  165. $difficultySum = 0.0;
  166. $difficultyCount = 0;
  167. foreach ($questions as $q) {
  168. $dl = $q['metadata']['difficulty_label'] ?? '';
  169. if ($dl !== '') {
  170. $difficultyDistribution[$dl] = ($difficultyDistribution[$dl] ?? 0) + 1;
  171. }
  172. if (isset($q['difficulty']) && is_numeric($q['difficulty'])) {
  173. $difficultySum += (float) $q['difficulty'];
  174. $difficultyCount++;
  175. }
  176. }
  177. $averageDifficulty = $difficultyCount > 0 ? $difficultySum / $difficultyCount : null;
  178. $totalEstimatedTime = count($questions) * 300;
  179. return [
  180. 'paper_info' => [
  181. 'paper_id' => $paperId,
  182. 'paper_name' => '知识点讲解_' . $displayCode,
  183. 'student_id' => (string) ($record->student_id ?? ''),
  184. 'teacher_id' => (string) ($record->teacher_id ?? ''),
  185. 'total_questions' => count($questions),
  186. 'total_score' => 100,
  187. 'difficulty_category' => $difficultyCategoryStr,
  188. 'created_at' => optional($record->created_at)->toISOString(),
  189. 'updated_at' => optional($record->updated_at)->toISOString(),
  190. 'exam_code' => $displayCode,
  191. 'grading_code' => $displayCode,
  192. 'paper_id_num' => $displayCode,
  193. ],
  194. 'questions' => $questions,
  195. 'knowledge_points' => $kpCodes,
  196. 'statistics' => [
  197. 'type_distribution' => $typeDistribution,
  198. 'difficulty_distribution' => $difficultyDistribution,
  199. 'knowledge_point_distribution' => $knowledgeDistribution,
  200. 'average_difficulty' => $averageDifficulty,
  201. 'total_estimated_time' => $totalEstimatedTime,
  202. ],
  203. 'pdfs' => [
  204. 'all_pdf' => $pdfUrl,
  205. ],
  206. 'source' => 'knowledge_explanation',
  207. ];
  208. }
  209. private function buildKnowledgeQuestions(int $totalScore = 100): array
  210. {
  211. $questions = [];
  212. $seq = 1;
  213. foreach ($this->knowledgePoints as $point) {
  214. $kpCode = (string) ($point['kp_code'] ?? '');
  215. $kpName = (string) ($point['kp_name'] ?? $kpCode);
  216. $cases = is_array($point['cases'] ?? null) ? $point['cases'] : [];
  217. foreach ($cases as $case) {
  218. $questionId = (int) ($case['question_id'] ?? 0);
  219. $qType = (string) ($case['question_type'] ?? 'answer');
  220. $solutionText = (string) ($case['solution'] ?? '');
  221. $difficultyVal = isset($case['difficulty']) && is_numeric($case['difficulty']) ? (float) $case['difficulty'] : null;
  222. $questions[] = [
  223. 'question_number' => $seq++,
  224. 'question_id' => $questionId > 0 ? (string) $questionId : '',
  225. 'question_bank_id' => $questionId > 0 ? $questionId : null,
  226. 'question_type' => $qType,
  227. 'knowledge_point' => $kpCode,
  228. 'knowledge_point_name' => $kpName,
  229. 'difficulty' => $difficultyVal,
  230. 'score' => 0,
  231. 'estimated_time' => 300,
  232. 'stem' => (string) ($case['stem'] ?? ''),
  233. 'options' => is_array($case['options'] ?? null) ? $case['options'] : [],
  234. 'correct_answer' => (string) ($case['answer'] ?? ''),
  235. 'solution' => $solutionText,
  236. 'metadata' => [
  237. 'source_type' => (string) ($case['source_type'] ?? ''),
  238. 'source_label' => (string) ($case['source_label'] ?? ''),
  239. 'is_wrong_case' => (bool) ($case['is_wrong_case'] ?? false),
  240. 'child_kp_code' => $case['child_kp_code'] ?? null,
  241. 'child_kp_name' => $case['child_kp_name'] ?? null,
  242. 'has_solution' => $solutionText !== '',
  243. 'is_choice' => $qType === 'choice',
  244. 'is_fill' => $qType === 'fill',
  245. 'is_answer' => $qType === 'answer',
  246. 'difficulty_label' => $this->difficultyLabelForPayload($difficultyVal),
  247. 'question_type_label' => $this->questionTypeLabelForPayload($qType),
  248. ],
  249. ];
  250. }
  251. }
  252. $count = count($questions);
  253. if ($count > 0) {
  254. $baseScore = intdiv($totalScore, $count);
  255. $remainder = $totalScore - ($baseScore * $count);
  256. foreach ($questions as &$q) {
  257. $q['score'] = $baseScore;
  258. }
  259. unset($q);
  260. if ($remainder > 0) {
  261. // 余数分散到末尾若干题,保证总分精确为 totalScore
  262. for ($i = $count - $remainder; $i < $count; $i++) {
  263. if ($i >= 0 && isset($questions[$i])) {
  264. $questions[$i]['score'] += 1;
  265. }
  266. }
  267. }
  268. }
  269. return $questions;
  270. }
  271. private function questionTypeLabelForPayload(?string $type): string
  272. {
  273. return match ($type) {
  274. 'choice' => '选择题',
  275. 'fill' => '填空题',
  276. 'answer' => '解答题',
  277. default => '未知题型',
  278. };
  279. }
  280. private function difficultyLabelForPayload(?float $difficulty): string
  281. {
  282. if ($difficulty === null) {
  283. return '未知';
  284. }
  285. if ($difficulty <= 0.4) {
  286. return '基础';
  287. }
  288. if ($difficulty <= 0.7) {
  289. return '中等';
  290. }
  291. return '拔高';
  292. }
  293. }