QuestionPdfController.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\Http\Controllers\Controller;
  4. use App\Services\ExamPdfExportService;
  5. use Illuminate\Http\Request;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Support\Facades\Log;
  8. use Illuminate\Support\Facades\Validator;
  9. class QuestionPdfController extends Controller
  10. {
  11. protected ExamPdfExportService $pdfService;
  12. public function __construct(ExamPdfExportService $pdfService)
  13. {
  14. $this->pdfService = $pdfService;
  15. }
  16. /**
  17. * Generate PDF for specified questions
  18. *
  19. * POST /api/questions/pdf
  20. *
  21. * Request body:
  22. * {
  23. * "question_ids": [19903, 17766, ...],
  24. * "student_id": "123456",
  25. * "student_name": "张三", // optional
  26. * "student_grade": "初二", // optional
  27. * "teacher_name": "李老师", // optional
  28. * "paper_name": "专项练习", // optional
  29. * "include_grading": false, // optional, whether to generate grading PDF
  30. * "source": "main" // optional: default=questions_tem;main=questions(正式题库)
  31. * }
  32. */
  33. public function generate(Request $request)
  34. {
  35. $validator = Validator::make($request->all(), [
  36. 'question_ids' => 'required|array|min:1|max:100',
  37. 'question_ids.*' => 'required|integer',
  38. 'student_id' => 'nullable|string',
  39. 'student_name' => 'nullable|string|max:50',
  40. 'student_grade' => 'nullable|string|max:20',
  41. 'teacher_name' => 'nullable|string|max:50',
  42. 'paper_name' => 'nullable|string|max:100',
  43. 'include_grading' => 'nullable|boolean',
  44. 'source' => 'nullable|string|in:default,ai,main', // 题库来源:default=questions_tem,ai=questions_ai,main=questions
  45. ]);
  46. if ($validator->fails()) {
  47. return response()->json([
  48. 'success' => false,
  49. 'message' => '参数验证失败',
  50. 'errors' => $validator->errors(),
  51. ], 422);
  52. }
  53. $questionIds = $request->input('question_ids');
  54. $studentId = (string) $request->input('student_id', 'question_check');
  55. $studentName = $request->input('student_name', '________');
  56. $studentGrade = $request->input('student_grade', '________');
  57. $teacherName = $request->input('teacher_name', '________');
  58. $paperName = $request->input('paper_name', '专项练习');
  59. $source = $request->input('source', 'default'); // 题库来源:default=questions_tem, ai=questions_ai, main=questions
  60. Log::info('生成指定题目PDF', [
  61. 'question_ids' => $questionIds,
  62. 'student_id' => $studentId,
  63. 'count' => count($questionIds),
  64. 'source' => $source,
  65. ]);
  66. try {
  67. // 1. Fetch questions from database
  68. $tableName = match ($source) {
  69. 'ai' => 'questions_ai',
  70. 'main' => 'questions',
  71. default => 'questions_tem',
  72. };
  73. $questions = DB::connection('remote_mysql')
  74. ->table($tableName)
  75. ->whereIn('id', $questionIds)
  76. ->get();
  77. if ($questions->isEmpty()) {
  78. return response()->json([
  79. 'success' => false,
  80. 'message' => '未找到指定的题目',
  81. ], 404);
  82. }
  83. // 2. Group questions by type
  84. $groupedQuestions = $this->groupQuestionsByType($questions, $questionIds);
  85. // 3. Build virtual paper structure
  86. $paper = $this->buildVirtualPaper($paperName, $studentId, $groupedQuestions);
  87. // 4. 生成题目质检PDF(固定判题卡体系样式,不走正常组卷路径)
  88. $result = $this->pdfService->generateQuestionCheckPdf(
  89. $paper,
  90. $groupedQuestions,
  91. [
  92. 'name' => $studentName,
  93. 'grade' => $studentGrade,
  94. ],
  95. ['name' => $teacherName]
  96. );
  97. Log::info('题目质检PDF生成成功', [
  98. 'student_id' => $studentId,
  99. 'question_count' => count($questionIds),
  100. 'pdf_url' => $result['pdf_url'] ?? null,
  101. ]);
  102. return response()->json([
  103. 'success' => true,
  104. 'message' => '题目质检PDF生成成功',
  105. 'data' => $result,
  106. ]);
  107. } catch (\Exception $e) {
  108. Log::error('生成指定题目PDF失败', [
  109. 'error' => $e->getMessage(),
  110. 'trace' => $e->getTraceAsString(),
  111. ]);
  112. return response()->json([
  113. 'success' => false,
  114. 'message' => '生成PDF失败: ' . $e->getMessage(),
  115. ], 500);
  116. }
  117. }
  118. /**
  119. * Group questions by type and maintain order
  120. */
  121. private function groupQuestionsByType($questions, array $originalOrder): array
  122. {
  123. // Create a map for quick lookup and preserve original order
  124. $questionMap = [];
  125. foreach ($questions as $q) {
  126. $questionMap[$q->id] = $q;
  127. }
  128. $grouped = [
  129. 'choice' => [],
  130. 'fill' => [],
  131. 'answer' => [],
  132. ];
  133. $questionNumber = 1;
  134. // Process in original order
  135. foreach ($originalOrder as $id) {
  136. if (!isset($questionMap[$id])) {
  137. continue;
  138. }
  139. $q = $questionMap[$id];
  140. $type = $this->normalizeQuestionType($q->question_type);
  141. // Build question object for template
  142. $questionObj = (object) [
  143. 'id' => $q->id,
  144. 'question_number' => $questionNumber++,
  145. 'content' => $q->stem,
  146. 'options' => is_string($q->options) ? json_decode($q->options, true) : ($q->options ?? []),
  147. 'answer' => $q->answer,
  148. 'solution' => $q->solution,
  149. 'score' => $this->getDefaultScore($type),
  150. 'difficulty' => $q->difficulty,
  151. 'kp_code' => $q->kp_code,
  152. ];
  153. $grouped[$type][] = $questionObj;
  154. }
  155. return $grouped;
  156. }
  157. /**
  158. * Normalize question type to standard format
  159. */
  160. private function normalizeQuestionType(?string $type): string
  161. {
  162. if (!$type) {
  163. return 'answer';
  164. }
  165. $type = strtolower(trim($type));
  166. $typeMap = [
  167. 'choice' => 'choice',
  168. '选择题' => 'choice',
  169. 'single_choice' => 'choice',
  170. 'multiple_choice' => 'choice',
  171. 'fill' => 'fill',
  172. '填空题' => 'fill',
  173. 'blank' => 'fill',
  174. 'answer' => 'answer',
  175. '解答题' => 'answer',
  176. 'subjective' => 'answer',
  177. 'calculation' => 'answer',
  178. 'proof' => 'answer',
  179. ];
  180. return $typeMap[$type] ?? 'answer';
  181. }
  182. /**
  183. * Get default score by question type
  184. */
  185. private function getDefaultScore(string $type): int
  186. {
  187. return match ($type) {
  188. 'choice' => 5,
  189. 'fill' => 5,
  190. 'answer' => 10,
  191. default => 5,
  192. };
  193. }
  194. /**
  195. * Build virtual paper structure
  196. */
  197. private function buildVirtualPaper(string $paperName, string $studentId, array $groupedQuestions): object
  198. {
  199. $totalScore = 0;
  200. $totalQuestions = 0;
  201. foreach ($groupedQuestions as $type => $questions) {
  202. foreach ($questions as $q) {
  203. $totalScore += $q->score;
  204. $totalQuestions++;
  205. }
  206. }
  207. // Generate unique paper ID
  208. $paperId = $studentId . '_' . time() . '_' . uniqid();
  209. return (object) [
  210. 'paper_id' => $paperId,
  211. 'paper_name' => $paperName,
  212. 'total_score' => $totalScore,
  213. 'total_questions' => $totalQuestions,
  214. 'created_at' => now()->toDateTimeString(),
  215. ];
  216. }
  217. }