ExamPdfController.php 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Services\QuestionBankService;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Support\Facades\Cache;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Support\Facades\Log;
  8. class ExamPdfController extends Controller
  9. {
  10. /**
  11. * 标准化选项格式为数组值列表
  12. * 支持格式:
  13. * 1. {"A": "0", "B": "5", "C": "-3", "D": "12"} -> ["0", "5", "-3", "12"]
  14. * 2. [["label": "A", "text": "选项A"], ...] -> ["选项A", "选项B", ...]
  15. * 3. ["A", "B", "C", "D"] -> ["A", "B", "C", "D"]
  16. */
  17. private function normalizeOptions($options): array
  18. {
  19. if (empty($options)) {
  20. return [];
  21. }
  22. // 如果是对象格式 {"A": "值1", "B": "值2", ...}
  23. if (is_array($options) && ! isset($options[0])) {
  24. return array_values($options);
  25. }
  26. // 如果是AI生成格式 [{"label": "A", "text": "选项A"}, ...]
  27. if (is_array($options) && isset($options[0]) && is_array($options[0])) {
  28. // 提取text字段,如果不存在则使用整个数组元素
  29. $normalized = [];
  30. foreach ($options as $opt) {
  31. if (isset($opt['text'])) {
  32. $normalized[] = $opt['text'];
  33. } elseif (isset($opt['value'])) {
  34. $normalized[] = $opt['value'];
  35. } else {
  36. // 如果既没有text也没有value,取数组的第一个值
  37. $normalized[] = is_array($opt) ? (string) reset($opt) : (string) $opt;
  38. }
  39. }
  40. return $normalized;
  41. }
  42. // 如果已经是简单数组格式 ["A", "B", "C", "D"]
  43. if (is_array($options)) {
  44. return array_values($options);
  45. }
  46. // 其他情况返回空数组
  47. return [];
  48. }
  49. /**
  50. * 根据题目内容或类型字段判断题型
  51. */
  52. private function determineQuestionType(array $question): string
  53. {
  54. // 优先根据题目内容判断(而不是数据库字段)
  55. $stem = $question['stem'] ?? $question['content'] ?? '';
  56. $tags = $question['tags'] ?? '';
  57. $skills = $question['skills'] ?? [];
  58. // 1. 根据题干内容判断 - 选择题特征:必须包含 A. B. C. D. 选项(至少2个)
  59. if (is_string($stem)) {
  60. // 选择题特征:必须包含 A. B. C. D. 四个选项(至少2个)
  61. $hasOptionA = preg_match('/\bA\s*[\.\、\:]/', $stem) || preg_match('/\(A\)/', $stem) || preg_match('/^A[\.\s]/', $stem);
  62. $hasOptionB = preg_match('/\bB\s*[\.\、\:]/', $stem) || preg_match('/\(B\)/', $stem) || preg_match('/^B[\.\s]/', $stem);
  63. $hasOptionC = preg_match('/\bC\s*[\.\、\:]/', $stem) || preg_match('/\(C\)/', $stem) || preg_match('/^C[\.\s]/', $stem);
  64. $hasOptionD = preg_match('/\bD\s*[\.\、\:]/', $stem) || preg_match('/\(D\)/', $stem) || preg_match('/^D[\.\s]/', $stem);
  65. $hasOptionE = preg_match('/\bE\s*[\.\、\:]/', $stem) || preg_match('/\(E\)/', $stem) || preg_match('/^E[\.\s]/', $stem);
  66. // 至少有2个选项就认为是选择题(降低阈值)
  67. $optionCount = ($hasOptionA ? 1 : 0) + ($hasOptionB ? 1 : 0) + ($hasOptionC ? 1 : 0) + ($hasOptionD ? 1 : 0) + ($hasOptionE ? 1 : 0);
  68. if ($optionCount >= 2) {
  69. return 'choice';
  70. }
  71. // 检查是否有"( )"或"( )"括号,这通常是选择题的标志
  72. if (preg_match('/(\s*)|\(\s*\)/', $stem) && (strpos($stem, 'A.') !== false || strpos($stem, 'B.') !== false || strpos($stem, 'C.') !== false || strpos($stem, 'D.') !== false)) {
  73. return 'choice';
  74. }
  75. }
  76. // 2. 根据技能点判断
  77. if (is_array($skills)) {
  78. $skillsStr = implode(',', $skills);
  79. if (strpos($skillsStr, '选择题') !== false) {
  80. return 'choice';
  81. }
  82. if (strpos($skillsStr, '填空题') !== false) {
  83. return 'fill';
  84. }
  85. if (strpos($skillsStr, '解答题') !== false) {
  86. return 'answer';
  87. }
  88. }
  89. // 3. 根据题目已有类型字段判断(作为后备)
  90. if (! empty($question['question_type'])) {
  91. $type = strtolower(trim($question['question_type']));
  92. if (in_array($type, ['choice', '选择题', 'choice question'])) {
  93. return 'choice';
  94. }
  95. if (in_array($type, ['fill', '填空题', 'fill in the blank'])) {
  96. return 'fill';
  97. }
  98. if (in_array($type, ['answer', '解答题', 'calculation', '简答题'])) {
  99. return 'answer';
  100. }
  101. }
  102. if (! empty($question['type'])) {
  103. $type = strtolower(trim($question['type']));
  104. if (in_array($type, ['choice', '选择题', 'choice question'])) {
  105. return 'choice';
  106. }
  107. if (in_array($type, ['fill', '填空题', 'fill in the blank'])) {
  108. return 'fill';
  109. }
  110. if (in_array($type, ['answer', '解答题', 'calculation', '简答题'])) {
  111. return 'answer';
  112. }
  113. }
  114. // 4. 根据标签判断
  115. if (is_string($tags)) {
  116. if (strpos($tags, '选择') !== false || strpos($tags, '选择题') !== false) {
  117. return 'choice';
  118. }
  119. if (strpos($tags, '填空') !== false || strpos($tags, '填空题') !== false) {
  120. return 'fill';
  121. }
  122. if (strpos($tags, '解答') !== false || strpos($tags, '简答') !== false || strpos($tags, '证明') !== false) {
  123. return 'answer';
  124. }
  125. }
  126. // 5. 填空题特征:连续下划线或明显的填空括号
  127. if (is_string($stem)) {
  128. // 检查填空题特征:连续下划线
  129. if (preg_match('/_{3,}/', $stem) || strpos($stem, '____') !== false) {
  130. return 'fill';
  131. }
  132. // 空括号填空
  133. if (preg_match('/(\s*)/', $stem) || preg_match('/\(\s*\)/', $stem)) {
  134. return 'fill';
  135. }
  136. }
  137. // 6. 根据题干内容关键词判断
  138. if (is_string($stem)) {
  139. // 有证明、解答、计算、求证等关键词的是解答题
  140. if (preg_match('/(证明|求证|解方程|计算:|求解|推导|说明理由)/', $stem)) {
  141. return 'answer';
  142. }
  143. }
  144. // 默认是解答题(更安全的默认值)
  145. return 'answer';
  146. }
  147. private function normalizeQuestionTypeValue(string $type): string
  148. {
  149. $type = trim($type);
  150. $lower = strtolower($type);
  151. if (in_array($lower, ['choice', 'single_choice', 'multiple_choice'], true)) {
  152. return 'choice';
  153. }
  154. if (in_array($lower, ['fill', 'blank', 'fill_in_the_blank'], true)) {
  155. return 'fill';
  156. }
  157. if (in_array($lower, ['answer', 'calculation', 'word_problem', 'proof'], true)) {
  158. return 'answer';
  159. }
  160. if (in_array($type, ['CHOICE', 'SINGLE_CHOICE', 'MULTIPLE_CHOICE'], true)) {
  161. return 'choice';
  162. }
  163. if (in_array($type, ['FILL', 'FILL_IN_THE_BLANK'], true)) {
  164. return 'fill';
  165. }
  166. if (in_array($type, ['CALCULATION', 'WORD_PROBLEM', 'PROOF'], true)) {
  167. return 'answer';
  168. }
  169. if (in_array($type, ['选择题'], true)) {
  170. return 'choice';
  171. }
  172. if (in_array($type, ['填空题'], true)) {
  173. return 'fill';
  174. }
  175. if (in_array($type, ['解答题', '计算题'], true)) {
  176. return 'answer';
  177. }
  178. return $lower ?: 'answer';
  179. }
  180. /**
  181. * 从题目内容中提取选项
  182. */
  183. private function extractOptions(string $content): array
  184. {
  185. $options = [];
  186. // 【修复】先移除SVG内容,避免误匹配SVG注释中的 BD:DC、A:B 等内容
  187. $contentWithoutSvg = preg_replace('/<svg[^>]*>.*?<\/svg>/is', '[SVG_PLACEHOLDER]', $content);
  188. // 1. 尝试匹配多种格式的选项:A. / A、/ A: / A.(中文句点)/ A.(无空格)
  189. // 【修复】选项标记必须在行首或空白后,避免误匹配 SVG 注释中的 BD:DC 等内容
  190. $pattern = '/(?:^|\s)([A-D])[\.、:.:]\s*(.+?)(?=(?:^|\s)[A-D][\.、:.:]|$)/su';
  191. if (preg_match_all($pattern, $contentWithoutSvg, $matches, PREG_SET_ORDER)) {
  192. foreach ($matches as $match) {
  193. $optionText = trim($match[2]);
  194. // 移除末尾的换行和空白
  195. $optionText = preg_replace('/\s+$/', '', $optionText);
  196. // 清理 LaTeX 格式但保留内容
  197. $optionText = preg_replace('/^\$\$\s*/', '', $optionText);
  198. $optionText = preg_replace('/\s*\$\$$/', '', $optionText);
  199. if (! empty($optionText)) {
  200. $options[] = $optionText;
  201. }
  202. }
  203. }
  204. // 2. 如果上面没提取到,尝试按换行分割
  205. if (empty($options)) {
  206. $lines = preg_split('/[\r\n]+/', $contentWithoutSvg);
  207. foreach ($lines as $line) {
  208. $line = trim($line);
  209. // 【修复】行首匹配选项标记
  210. if (preg_match('/^([A-D])[\.、:.:]\s*(.+)$/u', $line, $match)) {
  211. $optionText = trim($match[2]);
  212. if (! empty($optionText)) {
  213. $options[] = $optionText;
  214. }
  215. }
  216. }
  217. }
  218. Log::debug('选项提取结果', [
  219. 'content_preview' => mb_substr($content, 0, 150),
  220. 'options_count' => count($options),
  221. 'options' => $options,
  222. ]);
  223. return $options;
  224. }
  225. /**
  226. * 分离题干内容和选项
  227. */
  228. private function separateStemAndOptions(string $content): array
  229. {
  230. // 【修复】先移除SVG内容,避免误匹配SVG注释中的 BD:DC、A:B 等内容
  231. $contentWithoutSvg = preg_replace('/<svg[^>]*>.*?<\/svg>/is', '[SVG_PLACEHOLDER]', $content);
  232. // 【修复】检测是否有选项时,要求选项标记在行首或空白后
  233. $hasOptions = preg_match('/(?:^|\s)[A-D][\.、:.:]/u', $contentWithoutSvg);
  234. if (! $hasOptions) {
  235. return [$content, []];
  236. }
  237. // 提取选项
  238. $options = $this->extractOptions($content);
  239. // 如果提取到选项,分离题干
  240. if (! empty($options)) {
  241. // 【修复】找到第一个选项的位置,要求选项标记在行首或空白后
  242. if (preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:.:])/su', $contentWithoutSvg, $match)) {
  243. $stem = trim($match[1]);
  244. // 如果题干中有SVG占位符,从原始内容中提取对应部分
  245. if (strpos($stem, '[SVG_PLACEHOLDER]') !== false) {
  246. // 找到原始内容中对应位置的题干
  247. $stemLength = mb_strlen(str_replace('[SVG_PLACEHOLDER]', '', $stem));
  248. // 使用更精确的方法:找到第一个有效选项标记的位置
  249. foreach (['A.', 'A、', 'A:', 'A.', 'A:'] as $marker) {
  250. // 只匹配在空白后的选项标记
  251. if (preg_match('/\s'.preg_quote($marker, '/').'/', $content, $m, PREG_OFFSET_CAPTURE)) {
  252. $pos = $m[0][1];
  253. if ($pos > 0) {
  254. $stem = trim(mb_substr($content, 0, $pos));
  255. break;
  256. }
  257. }
  258. }
  259. }
  260. } else {
  261. // 如果正则失败,尝试按位置分割
  262. $stem = $content;
  263. foreach (['A.', 'A、', 'A:', 'A.', 'A:'] as $marker) {
  264. // 【修复】只匹配在空白后的选项标记
  265. if (preg_match('/\s'.preg_quote($marker, '/').'/', $content, $m, PREG_OFFSET_CAPTURE)) {
  266. $pos = $m[0][1];
  267. if ($pos > 0) {
  268. $stem = trim(mb_substr($content, 0, $pos));
  269. break;
  270. }
  271. }
  272. }
  273. }
  274. // 移除末尾的括号或空白
  275. $stem = preg_replace('/()\s*$/', '', $stem);
  276. $stem = trim($stem);
  277. return [$stem, $options];
  278. }
  279. return [$content, []];
  280. }
  281. /**
  282. * 根据题型获取默认分数
  283. */
  284. private function getQuestionScore(string $type): int
  285. {
  286. switch ($type) {
  287. case 'choice':
  288. return 5; // 选择题5分
  289. case 'fill':
  290. return 5; // 填空题5分
  291. case 'answer':
  292. return 10; // 解答题10分
  293. default:
  294. return 5;
  295. }
  296. }
  297. /**
  298. * 获取学生信息
  299. */
  300. private function getStudentInfo(?string $studentId): array
  301. {
  302. if (! $studentId) {
  303. return [
  304. 'name' => '未知学生',
  305. 'grade' => '未知年级',
  306. 'class' => '未知班级',
  307. ];
  308. }
  309. try {
  310. $student = DB::table('students')
  311. ->where('student_id', $studentId)
  312. ->first();
  313. if ($student) {
  314. return [
  315. 'name' => $student->name ?? $studentId,
  316. 'grade' => $student->grade ?? '未知',
  317. 'class' => $student->class ?? '未知',
  318. ];
  319. }
  320. } catch (\Exception $e) {
  321. Log::warning('获取学生信息失败', [
  322. 'student_id' => $studentId,
  323. 'error' => $e->getMessage(),
  324. ]);
  325. }
  326. return [
  327. 'name' => $studentId,
  328. 'grade' => '未知',
  329. 'class' => '未知',
  330. ];
  331. }
  332. /**
  333. * 为 PDF 预览筛选题目(简化版)
  334. */
  335. private function selectBestQuestionsForPdf(array $questions, int $targetCount, string $difficultyCategory): array
  336. {
  337. if (count($questions) <= $targetCount) {
  338. return $questions;
  339. }
  340. // 1. 按题型分类题目
  341. $categorizedQuestions = [
  342. 'choice' => [],
  343. 'fill' => [],
  344. 'answer' => [],
  345. ];
  346. foreach ($questions as $question) {
  347. $type = $this->determineQuestionType($question);
  348. if (! isset($categorizedQuestions[$type])) {
  349. $type = 'answer';
  350. }
  351. $categorizedQuestions[$type][] = $question;
  352. }
  353. // 2. 默认题型配比
  354. $typeRatio = [
  355. '选择题' => 50, // 50%
  356. '填空题' => 30, // 30%
  357. '解答题' => 20, // 20%
  358. ];
  359. // 3. 根据配比选择题目
  360. $selectedQuestions = [];
  361. foreach ($typeRatio as $type => $ratio) {
  362. $typeKey = $type === '选择题' ? 'choice' : ($type === '填空题' ? 'fill' : 'answer');
  363. $countForType = floor($targetCount * $ratio / 100);
  364. if ($countForType > 0 && ! empty($categorizedQuestions[$typeKey])) {
  365. $availableCount = count($categorizedQuestions[$typeKey]);
  366. $takeCount = min($countForType, $availableCount, $targetCount - count($selectedQuestions));
  367. // 随机选择题目
  368. $keys = array_keys($categorizedQuestions[$typeKey]);
  369. shuffle($keys);
  370. $selectedKeys = array_slice($keys, 0, $takeCount);
  371. foreach ($selectedKeys as $key) {
  372. $selectedQuestions[] = $categorizedQuestions[$typeKey][$key];
  373. }
  374. }
  375. }
  376. // 4. 如果数量不足,随机补充
  377. while (count($selectedQuestions) < $targetCount) {
  378. $randomQuestion = $questions[array_rand($questions)];
  379. if (! in_array($randomQuestion, $selectedQuestions)) {
  380. $selectedQuestions[] = $randomQuestion;
  381. }
  382. }
  383. // 5. 限制数量并打乱
  384. shuffle($selectedQuestions);
  385. return array_slice($selectedQuestions, 0, $targetCount);
  386. }
  387. /**
  388. * 获取教师信息
  389. */
  390. private function getTeacherInfo(?string $teacherId): array
  391. {
  392. if (! $teacherId) {
  393. return [
  394. 'name' => '未知教师',
  395. ];
  396. }
  397. try {
  398. $teacher = DB::table('teachers')
  399. ->where('teacher_id', $teacherId)
  400. ->first();
  401. if ($teacher) {
  402. return [
  403. 'name' => $teacher->name ?? $teacherId,
  404. ];
  405. }
  406. } catch (\Exception $e) {
  407. Log::warning('获取教师信息失败', [
  408. 'teacher_id' => $teacherId,
  409. 'error' => $e->getMessage(),
  410. ]);
  411. }
  412. return [
  413. 'name' => $teacherId,
  414. ];
  415. }
  416. public function show(Request $request, $paper_id)
  417. {
  418. // 获取是否显示答案的参数,默认为true
  419. $includeAnswer = $request->query('answer', 'true') !== 'false';
  420. // 使用 Eloquent 模型获取试卷数据
  421. $paper = \App\Models\Paper::where('paper_id', $paper_id)->first();
  422. if (! $paper) {
  423. // 尝试从缓存中获取生成的试卷数据(用于 demo 试卷)
  424. $cached = Cache::get('generated_exam_'.$paper_id);
  425. if ($cached) {
  426. Log::info('从缓存获取试卷数据', [
  427. 'paper_id' => $paper_id,
  428. 'cached_count' => count($cached['questions'] ?? []),
  429. 'cached_question_types' => array_column($cached['questions'] ?? [], 'question_type'),
  430. ]);
  431. // 构造临时 Paper 对象
  432. $paper = (object) [
  433. 'paper_id' => $paper_id,
  434. 'paper_name' => $cached['paper_name'] ?? 'Demo Paper',
  435. 'student_id' => $cached['student_id'] ?? null,
  436. 'teacher_id' => $cached['teacher_id'] ?? null,
  437. ];
  438. // 对于 demo 试卷,需要检查题目数量并限制为用户要求的数量
  439. $questionsData = $cached['questions'] ?? [];
  440. $totalQuestions = $cached['total_questions'] ?? count($questionsData);
  441. $difficultyCategory = $cached['difficulty_category'] ?? '中等';
  442. // 为 demo 试卷获取完整的题目详情(包括选项)
  443. if (! empty($questionsData)) {
  444. $questionBankService = app(QuestionBankService::class);
  445. $questionIds = array_column($questionsData, 'id');
  446. $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
  447. $responseData = $questionsResponse['data'] ?? [];
  448. if (! empty($responseData)) {
  449. $responseDataMap = [];
  450. foreach ($responseData as $respQ) {
  451. $responseDataMap[$respQ['id']] = $respQ;
  452. }
  453. // 合并题库数据
  454. $questionsData = array_map(function ($q) use ($responseDataMap) {
  455. if (isset($responseDataMap[$q['id']])) {
  456. $apiData = $responseDataMap[$q['id']];
  457. $rawContent = $apiData['stem'] ?? $q['stem'] ?? $q['content'] ?? '';
  458. // 分离题干和选项
  459. [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
  460. $q['stem'] = $stem;
  461. $q['content'] = $stem; // 同时设置content字段
  462. $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
  463. $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
  464. $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
  465. // 优先使用API选项,支持多种数据格式
  466. $apiOptions = $apiData['options'] ?? null;
  467. if (! empty($apiOptions)) {
  468. // 标准化options格式为数组值列表
  469. $q['options'] = $this->normalizeOptions($apiOptions);
  470. Log::debug('使用标准化API options', [
  471. 'question_id' => $q['id'],
  472. 'raw_options' => $apiOptions,
  473. 'normalized_options' => $q['options'],
  474. ]);
  475. } else {
  476. // 备选:从题干中提取的选项
  477. $q['options'] = $extractedOptions;
  478. Log::debug('使用提取的options', [
  479. 'question_id' => $q['id'],
  480. 'extracted_options' => $extractedOptions,
  481. ]);
  482. }
  483. }
  484. return $q;
  485. }, $questionsData);
  486. }
  487. }
  488. if (count($questionsData) > $totalQuestions) {
  489. Log::info('PDF预览时发现题目过多,进行筛选', [
  490. 'paper_id' => $paper_id,
  491. 'cached_count' => count($questionsData),
  492. 'required_count' => $totalQuestions,
  493. ]);
  494. $questionsData = $this->selectBestQuestionsForPdf($questionsData, $totalQuestions, $difficultyCategory);
  495. Log::info('筛选后题目数据', [
  496. 'paper_id' => $paper_id,
  497. 'filtered_count' => count($questionsData),
  498. 'filtered_types' => array_column($questionsData, 'question_type'),
  499. ]);
  500. }
  501. } else {
  502. abort(404, '试卷未找到');
  503. }
  504. } else {
  505. // 获取试卷题目
  506. $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paper_id)
  507. ->orderBy('question_number')
  508. ->get();
  509. Log::info('从数据库获取题目', [
  510. 'paper_id' => $paper_id,
  511. 'question_count' => $paperQuestions->count(),
  512. ]);
  513. // 将 paper_questions 表的数据转换为题库格式
  514. $questionsData = [];
  515. foreach ($paperQuestions as $pq) {
  516. $questionsData[] = [
  517. 'id' => $pq->question_bank_id,
  518. 'question_number' => $pq->question_number, // 【关键】保留题目序号,用于排序
  519. 'kp_code' => $pq->knowledge_point,
  520. 'question_type' => $pq->question_type ?? 'answer', // 包含题目类型
  521. 'stem' => $pq->question_text ?? '题目内容缺失', // 如果有存储题目文本
  522. 'solution' => $pq->solution ?? '', // 保存解题思路!
  523. 'answer' => $pq->correct_answer ?? '', // 保存正确答案
  524. 'difficulty' => $pq->difficulty ?? 0.5,
  525. 'score' => $pq->score ?? 5, // 包含已计算的分值
  526. 'tags' => '',
  527. 'content' => $pq->question_text ?? '',
  528. ];
  529. }
  530. Log::info('paper_questions表原始数据', [
  531. 'paper_id' => $paper_id,
  532. 'sample_questions' => array_slice($questionsData, 0, 3),
  533. 'all_types' => array_column($questionsData, 'question_type'),
  534. ]);
  535. // 如果需要完整题目详情(stem等),可以从题库获取
  536. // 但要严格限制只获取这8道题
  537. if (! empty($questionsData)) {
  538. $questionBankService = app(QuestionBankService::class);
  539. $questionIds = array_column($questionsData, 'id');
  540. $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
  541. $responseData = $questionsResponse['data'] ?? [];
  542. // 确保只返回请求的ID对应的题目,并保留数据库中的 question_type
  543. if (! empty($responseData)) {
  544. // 创建题库返回数据的映射
  545. $responseDataMap = [];
  546. foreach ($responseData as $respQ) {
  547. $responseDataMap[$respQ['id']] = $respQ;
  548. }
  549. // 遍历所有数据库中的题目,合并题库返回的数据
  550. $questionsData = array_map(function ($q) use ($responseDataMap, $paperQuestions) {
  551. // 从题库API获取的详细数据(如果有)
  552. if (isset($responseDataMap[$q['id']])) {
  553. $apiData = $responseDataMap[$q['id']];
  554. $rawContent = $apiData['stem'] ?? $q['stem'] ?? '题目内容缺失';
  555. // 分离题干和选项
  556. [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
  557. // 合并数据,优先使用题库API的 stem、answer、solution、options
  558. $q['stem'] = $stem;
  559. $q['content'] = $stem; // 同时设置content字段
  560. $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
  561. $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
  562. $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
  563. // 优先使用API选项,支持多种数据格式
  564. $apiOptions = $apiData['options'] ?? null;
  565. if (! empty($apiOptions)) {
  566. // 标准化options格式为数组值列表
  567. $q['options'] = $this->normalizeOptions($apiOptions);
  568. Log::debug('使用标准化API options', [
  569. 'question_id' => $q['id'],
  570. 'raw_options' => $apiOptions,
  571. 'normalized_options' => $q['options'],
  572. ]);
  573. } else {
  574. // 备选:从题干中提取的选项
  575. $q['options'] = $extractedOptions;
  576. Log::debug('使用提取的options', [
  577. 'question_id' => $q['id'],
  578. 'extracted_options' => $extractedOptions,
  579. ]);
  580. }
  581. }
  582. // 从数据库 paper_questions 表中获取 question_type(已在前面设置,这里确保有值)
  583. if (! isset($q['question_type']) || empty($q['question_type'])) {
  584. $dbQuestion = $paperQuestions->firstWhere('question_bank_id', $q['id']);
  585. if ($dbQuestion && $dbQuestion->question_type) {
  586. $q['question_type'] = $dbQuestion->question_type;
  587. }
  588. }
  589. return $q;
  590. }, $questionsData);
  591. }
  592. }
  593. }
  594. // 按题型分类(使用标准的中学数学试卷格式)
  595. $questions = ['choice' => [], 'fill' => [], 'answer' => []];
  596. foreach ($questionsData as $q) {
  597. // 题库API返回的是 stem 字段,不是 content
  598. $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失';
  599. // 分离题干和选项
  600. [$content, $extractedOptions] = $this->separateStemAndOptions($rawContent);
  601. // 如果从题库API获取了选项,优先使用
  602. $options = $q['options'] ?? $extractedOptions;
  603. $answer = $q['answer'] ?? '';
  604. $solution = $q['solution'] ?? '';
  605. // 优先使用 question_type 字段,如果没有则根据内容智能判断
  606. $type = isset($q['question_type'])
  607. ? $this->normalizeQuestionTypeValue((string) $q['question_type'])
  608. : $this->determineQuestionType($q);
  609. // 详细调试:记录题目类型判断结果
  610. Log::info('题目类型判断', [
  611. 'question_id' => $q['id'] ?? '',
  612. 'has_question_type' => isset($q['question_type']),
  613. 'question_type_value' => $q['question_type'] ?? null,
  614. 'tags' => $q['tags'] ?? '',
  615. 'stem_length' => mb_strlen($content),
  616. 'stem_preview' => mb_substr($content, 0, 100),
  617. 'has_extracted_options' => ! empty($extractedOptions),
  618. 'extracted_options_count' => count($extractedOptions),
  619. 'has_api_options' => isset($q['options']) && ! empty($q['options']),
  620. 'api_options_count' => isset($q['options']) ? count($q['options']) : 0,
  621. 'final_options_count' => count($options),
  622. 'determined_type' => $type,
  623. ]);
  624. if (! isset($questions[$type])) {
  625. $type = 'answer';
  626. }
  627. // 统一处理数学公式和选项数据
  628. $questionData = [
  629. 'id' => $q['id'] ?? $q['question_bank_id'] ?? null,
  630. 'question_number' => $q['question_number'] ?? null, // 【关键】保留题目序号
  631. 'content' => $content,
  632. 'stem' => $content, // 同时提供stem字段
  633. 'answer' => $answer,
  634. 'solution' => $solution,
  635. 'difficulty' => $q['difficulty'] ?? 0.5,
  636. 'kp_code' => $q['kp_code'] ?? '',
  637. 'tags' => $q['tags'] ?? '',
  638. 'options' => $options, // 使用分离后的选项
  639. 'score' => $q['score'] ?? $this->getQuestionScore($type),
  640. 'question_type' => $type,
  641. ];
  642. // 统一处理数学公式 - 标记已处理,避免模板中重复处理
  643. $questionData = \App\Services\MathFormulaProcessor::processQuestionData($questionData);
  644. $questionData['math_processed'] = true; // 添加标记
  645. $qData = (object) $questionData;
  646. $questions[$type][] = $qData;
  647. }
  648. // 【关键】确保每个题型内的题目按 question_number 排序
  649. foreach (['choice', 'fill', 'answer'] as $type) {
  650. if (! empty($questions[$type])) {
  651. usort($questions[$type], function ($a, $b) {
  652. $aNum = $a->question_number ?? 0;
  653. $bNum = $b->question_number ?? 0;
  654. return $aNum <=> $bNum;
  655. });
  656. }
  657. }
  658. // 调试:记录最终分类结果
  659. Log::info('最终分类结果', [
  660. 'paper_id' => $paper_id,
  661. 'choice_count' => count($questions['choice']),
  662. 'fill_count' => count($questions['fill']),
  663. 'answer_count' => count($questions['answer']),
  664. 'total' => count($questions['choice']) + count($questions['fill']) + count($questions['answer']),
  665. ]);
  666. // 渲染视图
  667. $viewName = $includeAnswer ? 'pdf.exam-grading' : 'pdf.exam-paper';
  668. return view($viewName, [
  669. 'paper' => $paper,
  670. 'questions' => $questions,
  671. 'student' => $this->getStudentInfo($paper->student_id),
  672. 'teacher' => $this->getTeacherInfo($paper->teacher_id),
  673. 'includeAnswer' => $includeAnswer,
  674. ]);
  675. }
  676. /**
  677. * 判卷视图:题目前带方框,题后附"正确答案+解题思路"
  678. */
  679. public function showGrading(Request $request, $paper_id)
  680. {
  681. // 复用现有逻辑获取题目分类
  682. $includeAnswer = true;
  683. // 直接调用 show 的前置逻辑(简化复用)
  684. $request->merge(['answer' => 'true']);
  685. // 复用 show() 内逻辑获取 questions/paper
  686. // 为避免重复代码,简单调用 showData 方法(拆分为私有方法?暂直接重用现有方法流程)
  687. // 这里直接复制 show 的主体以保持兼容
  688. // 使用 Eloquent 模型获取试卷数据
  689. $paper = \App\Models\Paper::where('paper_id', $paper_id)->first();
  690. if (! $paper) {
  691. $cached = Cache::get('generated_exam_'.$paper_id);
  692. if (! $cached) {
  693. abort(404, '试卷未找到');
  694. }
  695. $paper = (object) [
  696. 'paper_id' => $paper_id,
  697. 'paper_name' => $cached['paper_name'] ?? 'Demo Paper',
  698. 'student_id' => $cached['student_id'] ?? null,
  699. 'teacher_id' => $cached['teacher_id'] ?? null,
  700. ];
  701. $questionsData = $cached['questions'] ?? [];
  702. $totalQuestions = $cached['total_questions'] ?? count($questionsData);
  703. $difficultyCategory = $cached['difficulty_category'] ?? '中等';
  704. if (! empty($questionsData)) {
  705. $questionBankService = app(QuestionBankService::class);
  706. $questionIds = array_column($questionsData, 'id');
  707. $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
  708. $responseData = $questionsResponse['data'] ?? [];
  709. if (! empty($responseData)) {
  710. $responseDataMap = [];
  711. foreach ($responseData as $respQ) {
  712. $responseDataMap[$respQ['id']] = $respQ;
  713. }
  714. $questionsData = array_map(function ($q) use ($responseDataMap) {
  715. if (isset($responseDataMap[$q['id']])) {
  716. $apiData = $responseDataMap[$q['id']];
  717. $rawContent = $apiData['stem'] ?? $q['stem'] ?? $q['content'] ?? '';
  718. [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
  719. $q['stem'] = $stem;
  720. $q['content'] = $stem; // 同时设置content字段
  721. $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
  722. $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
  723. $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
  724. // 优先使用API选项,支持多种数据格式
  725. $apiOptions = $apiData['options'] ?? null;
  726. if (! empty($apiOptions)) {
  727. // 标准化options格式为数组值列表
  728. $q['options'] = $this->normalizeOptions($apiOptions);
  729. Log::debug('使用标准化API options', [
  730. 'question_id' => $q['id'],
  731. 'raw_options' => $apiOptions,
  732. 'normalized_options' => $q['options'],
  733. ]);
  734. } else {
  735. // 备选:从题干中提取的选项
  736. $q['options'] = $extractedOptions;
  737. Log::debug('使用提取的options', [
  738. 'question_id' => $q['id'],
  739. 'extracted_options' => $extractedOptions,
  740. ]);
  741. }
  742. }
  743. return $q;
  744. }, $questionsData);
  745. }
  746. }
  747. if (count($questionsData) > $totalQuestions) {
  748. $questionsData = $this->selectBestQuestionsForPdf($questionsData, $totalQuestions, $difficultyCategory);
  749. }
  750. } else {
  751. $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paper_id)
  752. ->orderBy('question_number')
  753. ->get();
  754. $questionsData = [];
  755. foreach ($paperQuestions as $pq) {
  756. $questionsData[] = [
  757. 'id' => $pq->question_bank_id,
  758. 'question_number' => $pq->question_number, // 【关键】保留题目序号
  759. 'kp_code' => $pq->knowledge_point,
  760. 'question_type' => $pq->question_type ?? 'answer',
  761. 'stem' => $pq->question_text ?? '题目内容缺失',
  762. 'solution' => $pq->solution ?? '', // 保存解题思路!
  763. 'answer' => $pq->correct_answer ?? '', // 保存正确答案
  764. 'difficulty' => $pq->difficulty ?? 0.5,
  765. 'score' => $pq->score ?? 5,
  766. 'tags' => '',
  767. 'content' => $pq->question_text ?? '',
  768. ];
  769. }
  770. if (! empty($questionsData)) {
  771. $questionBankService = app(QuestionBankService::class);
  772. $questionIds = array_column($questionsData, 'id');
  773. $questionsResponse = $questionBankService->getQuestionsByIds($questionIds);
  774. $responseData = $questionsResponse['data'] ?? [];
  775. if (! empty($responseData)) {
  776. $responseDataMap = [];
  777. foreach ($responseData as $respQ) {
  778. $responseDataMap[$respQ['id']] = $respQ;
  779. }
  780. $questionsData = array_map(function ($q) use ($responseDataMap, $paperQuestions) {
  781. if (isset($responseDataMap[$q['id']])) {
  782. $apiData = $responseDataMap[$q['id']];
  783. $rawContent = $apiData['stem'] ?? $q['stem'] ?? '题目内容缺失';
  784. [$stem, $extractedOptions] = $this->separateStemAndOptions($rawContent);
  785. $q['stem'] = $stem;
  786. $q['content'] = $stem; // 同时设置content字段
  787. $q['answer'] = $apiData['answer'] ?? $q['answer'] ?? '';
  788. $q['solution'] = $apiData['solution'] ?? $q['solution'] ?? '';
  789. $q['tags'] = $apiData['tags'] ?? $q['tags'] ?? '';
  790. // 优先使用API选项,支持多种数据格式
  791. $apiOptions = $apiData['options'] ?? null;
  792. if (! empty($apiOptions)) {
  793. // 标准化options格式为数组值列表
  794. $q['options'] = $this->normalizeOptions($apiOptions);
  795. Log::debug('使用标准化API options', [
  796. 'question_id' => $q['id'],
  797. 'raw_options' => $apiOptions,
  798. 'normalized_options' => $q['options'],
  799. ]);
  800. } else {
  801. // 备选:从题干中提取的选项
  802. $q['options'] = $extractedOptions;
  803. Log::debug('使用提取的options', [
  804. 'question_id' => $q['id'],
  805. 'extracted_options' => $extractedOptions,
  806. ]);
  807. }
  808. }
  809. if (! isset($q['question_type']) || empty($q['question_type'])) {
  810. $dbQuestion = $paperQuestions->firstWhere('question_bank_id', $q['id']);
  811. if ($dbQuestion && $dbQuestion->question_type) {
  812. $q['question_type'] = $dbQuestion->question_type;
  813. }
  814. }
  815. return $q;
  816. }, $questionsData);
  817. }
  818. }
  819. }
  820. $questions = ['choice' => [], 'fill' => [], 'answer' => []];
  821. foreach ($questionsData as $q) {
  822. $rawContent = $q['stem'] ?? $q['content'] ?? '题目内容缺失';
  823. [$content, $extractedOptions] = $this->separateStemAndOptions($rawContent);
  824. $options = $q['options'] ?? $extractedOptions;
  825. $answer = $q['answer'] ?? '';
  826. $solution = $q['solution'] ?? '';
  827. $type = isset($q['question_type'])
  828. ? $this->normalizeQuestionTypeValue((string) $q['question_type'])
  829. : $this->determineQuestionType($q);
  830. if (! isset($questions[$type])) {
  831. $type = 'answer';
  832. }
  833. // 统一处理数学公式和选项数据
  834. $questionData = [
  835. 'id' => $q['id'] ?? $q['question_bank_id'] ?? null,
  836. 'question_number' => $q['question_number'] ?? null, // 【关键】保留题目序号
  837. 'content' => $content,
  838. 'stem' => $content, // 同时提供stem字段
  839. 'answer' => $answer,
  840. 'solution' => $solution,
  841. 'difficulty' => $q['difficulty'] ?? 0.5,
  842. 'kp_code' => $q['kp_code'] ?? '',
  843. 'tags' => $q['tags'] ?? '',
  844. 'options' => $options,
  845. 'score' => $q['score'] ?? $this->getQuestionScore($type),
  846. 'question_type' => $type,
  847. ];
  848. // 统一处理数学公式 - 标记已处理,避免模板中重复处理
  849. $questionData = \App\Services\MathFormulaProcessor::processQuestionData($questionData);
  850. $questionData['math_processed'] = true; // 添加标记
  851. $qData = (object) $questionData;
  852. $questions[$type][] = $qData;
  853. }
  854. // 【关键】确保每个题型内的题目按 question_number 排序
  855. foreach (['choice', 'fill', 'answer'] as $type) {
  856. if (! empty($questions[$type])) {
  857. usort($questions[$type], function ($a, $b) {
  858. $aNum = $a->question_number ?? 0;
  859. $bNum = $b->question_number ?? 0;
  860. return $aNum <=> $bNum;
  861. });
  862. }
  863. }
  864. return view('pdf.exam-grading', [
  865. 'paper' => $paper,
  866. 'questions' => $questions,
  867. 'student' => $this->getStudentInfo($paper->student_id),
  868. 'teacher' => $this->getTeacherInfo($paper->teacher_id),
  869. 'includeAnswer' => true,
  870. ]);
  871. }
  872. /**
  873. * 知识点讲解视图
  874. */
  875. public function showKnowledgeExplanation(Request $request, $paper_id)
  876. {
  877. // 获取试卷数据
  878. $paper = \App\Models\Paper::where('paper_id', $paper_id)->first();
  879. if (! $paper) {
  880. abort(404, '试卷未找到');
  881. }
  882. // 提取15位paper_id数字部分作为学案编号
  883. $rawPaperId = $paper->paper_id ?? $paper_id;
  884. preg_match('/paper_(\d{15})/', (string) $rawPaperId, $matches);
  885. $examCode = $matches[1] ?? preg_replace('/[^0-9]/', '', (string) $rawPaperId);
  886. // 生成日期
  887. $generateDate = now()->locale('zh_CN')->isoFormat('M月D日');
  888. // 提取并去重知识点代码
  889. $paperQuestions = \App\Models\PaperQuestion::where('paper_id', $paper_id)->get();
  890. $kpCodes = [];
  891. $seen = [];
  892. foreach ($paperQuestions as $pq) {
  893. $kpCode = trim((string) ($pq->knowledge_point ?? ''));
  894. if ($kpCode === '') {
  895. continue;
  896. }
  897. if (isset($seen[$kpCode])) {
  898. continue;
  899. }
  900. $seen[$kpCode] = true;
  901. $kpCodes[] = $kpCode;
  902. }
  903. // 使用 ExamPdfExportService 构建知识点数据
  904. $pdfService = app(\App\Services\ExamPdfExportService::class);
  905. // 获取知识点名称映射
  906. $kpNameMap = [];
  907. try {
  908. $kpNameMap = app(\App\Services\QuestionServiceApi::class)->getKnowledgePointOptions();
  909. } catch (\Throwable $e) {
  910. // 静默失败,使用 code 作为名称
  911. }
  912. // 批量获取知识点讲解
  913. $explanations = $pdfService->buildExplanations($kpCodes);
  914. $knowledgePoints = [];
  915. foreach ($kpCodes as $kpCode) {
  916. $kpData = [
  917. 'kp_code' => $kpCode,
  918. 'kp_name' => $kpNameMap[$kpCode] ?? $kpCode,
  919. 'explanation' => $explanations[$kpCode] ?? '',
  920. ];
  921. $knowledgePoints[] = $kpData;
  922. }
  923. return view('pdf.exam-knowledge-explanation', [
  924. 'paperId' => $paper_id,
  925. 'examCode' => $examCode ?: $paper_id,
  926. 'generateDate' => $generateDate,
  927. 'knowledgePoints' => $knowledgePoints,
  928. ]);
  929. }
  930. /**
  931. * 重新生成 PDF(统一生成卷子和判卷)
  932. *
  933. * @param string $paper_id
  934. * @return \Illuminate\Http\JsonResponse
  935. */
  936. public function regeneratePdf(Request $request, $paper_id)
  937. {
  938. Log::info('RegeneratePdf: 开始重新生成PDF', ['paper_id' => $paper_id]);
  939. // 验证 paper_id 格式
  940. if (empty($paper_id) || ! preg_match('/^paper_\d+$/', $paper_id)) {
  941. return response()->json([
  942. 'success' => false,
  943. 'message' => '无效的试卷ID格式',
  944. 'paper_id' => $paper_id,
  945. ], 400);
  946. }
  947. try {
  948. // 调用 PDF 生成服务
  949. $pdfService = app(\App\Services\ExamPdfExportService::class);
  950. // 是否包含知识点讲解(可选):未传参则使用 config/pdf.php 默认值
  951. $includeKpExplain = null;
  952. if ($request->has('include_kp_explain')) {
  953. $includeKpExplain = filter_var(
  954. $request->input('include_kp_explain'),
  955. FILTER_VALIDATE_BOOLEAN,
  956. FILTER_NULL_ON_FAILURE
  957. );
  958. }
  959. // 生成统一 PDF(卷子 + 判卷)
  960. $pdfUrl = $pdfService->generateUnifiedPdf($paper_id, $includeKpExplain);
  961. if ($pdfUrl) {
  962. Log::info('RegeneratePdf: PDF重新生成成功', [
  963. 'paper_id' => $paper_id,
  964. 'url' => $pdfUrl,
  965. ]);
  966. return response()->json([
  967. 'success' => true,
  968. 'message' => 'PDF重新生成成功',
  969. 'paper_id' => $paper_id,
  970. 'pdf_url' => $pdfUrl,
  971. ]);
  972. }
  973. Log::error('RegeneratePdf: PDF生成失败', ['paper_id' => $paper_id]);
  974. return response()->json([
  975. 'success' => false,
  976. 'message' => 'PDF生成失败',
  977. 'paper_id' => $paper_id,
  978. ], 500);
  979. } catch (\Exception $e) {
  980. Log::error('RegeneratePdf: 异常错误', [
  981. 'paper_id' => $paper_id,
  982. 'error' => $e->getMessage(),
  983. 'trace' => $e->getTraceAsString(),
  984. ]);
  985. return response()->json([
  986. 'success' => false,
  987. 'message' => 'PDF生成异常:'.$e->getMessage(),
  988. 'paper_id' => $paper_id,
  989. ], 500);
  990. }
  991. }
  992. /**
  993. * 重新生成试卷 PDF(不含答案)
  994. *
  995. * @param string $paper_id
  996. * @return \Illuminate\Http\JsonResponse
  997. */
  998. public function regenerateExamPdf(Request $request, $paper_id)
  999. {
  1000. Log::info('RegenerateExamPdf: 开始重新生成试卷PDF', ['paper_id' => $paper_id]);
  1001. if (empty($paper_id) || ! preg_match('/^paper_\d+$/', $paper_id)) {
  1002. return response()->json([
  1003. 'success' => false,
  1004. 'message' => '无效的试卷ID格式',
  1005. 'paper_id' => $paper_id,
  1006. ], 400);
  1007. }
  1008. try {
  1009. $pdfService = app(\App\Services\ExamPdfExportService::class);
  1010. $pdfUrl = $pdfService->generateExamPdf($paper_id);
  1011. if ($pdfUrl) {
  1012. return response()->json([
  1013. 'success' => true,
  1014. 'message' => '试卷PDF重新生成成功',
  1015. 'paper_id' => $paper_id,
  1016. 'pdf_url' => $pdfUrl,
  1017. ]);
  1018. }
  1019. return response()->json([
  1020. 'success' => false,
  1021. 'message' => '试卷PDF生成失败',
  1022. 'paper_id' => $paper_id,
  1023. ], 500);
  1024. } catch (\Exception $e) {
  1025. Log::error('RegenerateExamPdf: 异常错误', [
  1026. 'paper_id' => $paper_id,
  1027. 'error' => $e->getMessage(),
  1028. ]);
  1029. return response()->json([
  1030. 'success' => false,
  1031. 'message' => 'PDF生成异常:'.$e->getMessage(),
  1032. 'paper_id' => $paper_id,
  1033. ], 500);
  1034. }
  1035. }
  1036. /**
  1037. * 重新生成判卷 PDF(含答案)
  1038. *
  1039. * @param string $paper_id
  1040. * @return \Illuminate\Http\JsonResponse
  1041. */
  1042. public function regenerateGradingPdf(Request $request, $paper_id)
  1043. {
  1044. Log::info('RegenerateGradingPdf: 开始重新生成判卷PDF', ['paper_id' => $paper_id]);
  1045. if (empty($paper_id) || ! preg_match('/^paper_\d+$/', $paper_id)) {
  1046. return response()->json([
  1047. 'success' => false,
  1048. 'message' => '无效的试卷ID格式',
  1049. 'paper_id' => $paper_id,
  1050. ], 400);
  1051. }
  1052. try {
  1053. $pdfService = app(\App\Services\ExamPdfExportService::class);
  1054. $pdfUrl = $pdfService->generateGradingPdf($paper_id);
  1055. if ($pdfUrl) {
  1056. return response()->json([
  1057. 'success' => true,
  1058. 'message' => '判卷PDF重新生成成功',
  1059. 'paper_id' => $paper_id,
  1060. 'pdf_url' => $pdfUrl,
  1061. ]);
  1062. }
  1063. return response()->json([
  1064. 'success' => false,
  1065. 'message' => '判卷PDF生成失败',
  1066. 'paper_id' => $paper_id,
  1067. ], 500);
  1068. } catch (\Exception $e) {
  1069. Log::error('RegenerateGradingPdf: 异常错误', [
  1070. 'paper_id' => $paper_id,
  1071. 'error' => $e->getMessage(),
  1072. ]);
  1073. return response()->json([
  1074. 'success' => false,
  1075. 'message' => 'PDF生成异常:'.$e->getMessage(),
  1076. 'paper_id' => $paper_id,
  1077. ], 500);
  1078. }
  1079. }
  1080. }