|
@@ -0,0 +1,246 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Http\Controllers\Api;
|
|
|
|
|
+
|
|
|
|
|
+use App\Http\Controllers\Controller;
|
|
|
|
|
+use App\Services\ExamPdfExportService;
|
|
|
|
|
+use Illuminate\Http\Request;
|
|
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
|
|
+use Illuminate\Support\Facades\Validator;
|
|
|
|
|
+
|
|
|
|
|
+class QuestionPdfController extends Controller
|
|
|
|
|
+{
|
|
|
|
|
+ protected ExamPdfExportService $pdfService;
|
|
|
|
|
+
|
|
|
|
|
+ public function __construct(ExamPdfExportService $pdfService)
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->pdfService = $pdfService;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Generate PDF for specified questions
|
|
|
|
|
+ *
|
|
|
|
|
+ * POST /api/questions/pdf
|
|
|
|
|
+ *
|
|
|
|
|
+ * Request body:
|
|
|
|
|
+ * {
|
|
|
|
|
+ * "question_ids": [19903, 17766, ...],
|
|
|
|
|
+ * "student_id": "123456",
|
|
|
|
|
+ * "student_name": "张三", // optional
|
|
|
|
|
+ * "student_grade": "初二", // optional
|
|
|
|
|
+ * "teacher_name": "李老师", // optional
|
|
|
|
|
+ * "paper_name": "专项练习", // optional
|
|
|
|
|
+ * "include_grading": false // optional, whether to generate grading PDF
|
|
|
|
|
+ * }
|
|
|
|
|
+ */
|
|
|
|
|
+ public function generate(Request $request)
|
|
|
|
|
+ {
|
|
|
|
|
+ $validator = Validator::make($request->all(), [
|
|
|
|
|
+ 'question_ids' => 'required|array|min:1|max:100',
|
|
|
|
|
+ 'question_ids.*' => 'required|integer',
|
|
|
|
|
+ 'student_id' => 'required|string',
|
|
|
|
|
+ 'student_name' => 'nullable|string|max:50',
|
|
|
|
|
+ 'student_grade' => 'nullable|string|max:20',
|
|
|
|
|
+ 'teacher_name' => 'nullable|string|max:50',
|
|
|
|
|
+ 'paper_name' => 'nullable|string|max:100',
|
|
|
|
|
+ 'include_grading' => 'nullable|boolean',
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ if ($validator->fails()) {
|
|
|
|
|
+ return response()->json([
|
|
|
|
|
+ 'success' => false,
|
|
|
|
|
+ 'message' => '参数验证失败',
|
|
|
|
|
+ 'errors' => $validator->errors(),
|
|
|
|
|
+ ], 422);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $questionIds = $request->input('question_ids');
|
|
|
|
|
+ $studentId = $request->input('student_id');
|
|
|
|
|
+ $studentName = $request->input('student_name', '');
|
|
|
|
|
+ $studentGrade = $request->input('student_grade', '');
|
|
|
|
|
+ $teacherName = $request->input('teacher_name', '');
|
|
|
|
|
+ $paperName = $request->input('paper_name', '专项练习');
|
|
|
|
|
+ $includeGrading = $request->input('include_grading', false);
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('生成指定题目PDF', [
|
|
|
|
|
+ 'question_ids' => $questionIds,
|
|
|
|
|
+ 'student_id' => $studentId,
|
|
|
|
|
+ 'count' => count($questionIds),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1. Fetch questions from database
|
|
|
|
|
+ $questions = DB::connection('remote_mysql')
|
|
|
|
|
+ ->table('questions')
|
|
|
|
|
+ ->whereIn('id', $questionIds)
|
|
|
|
|
+ ->get();
|
|
|
|
|
+
|
|
|
|
|
+ if ($questions->isEmpty()) {
|
|
|
|
|
+ return response()->json([
|
|
|
|
|
+ 'success' => false,
|
|
|
|
|
+ 'message' => '未找到指定的题目',
|
|
|
|
|
+ ], 404);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. Group questions by type
|
|
|
|
|
+ $groupedQuestions = $this->groupQuestionsByType($questions, $questionIds);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. Build virtual paper structure
|
|
|
|
|
+ $paper = $this->buildVirtualPaper($paperName, $studentId, $groupedQuestions);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. Generate PDF
|
|
|
|
|
+ $result = $this->pdfService->generateByQuestions(
|
|
|
|
|
+ $paper,
|
|
|
|
|
+ $groupedQuestions,
|
|
|
|
|
+ [
|
|
|
|
|
+ 'name' => $studentName,
|
|
|
|
|
+ 'grade' => $studentGrade,
|
|
|
|
|
+ ],
|
|
|
|
|
+ [
|
|
|
|
|
+ 'name' => $teacherName,
|
|
|
|
|
+ ],
|
|
|
|
|
+ $includeGrading
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ Log::info('指定题目PDF生成成功', [
|
|
|
|
|
+ 'student_id' => $studentId,
|
|
|
|
|
+ 'question_count' => count($questionIds),
|
|
|
|
|
+ 'pdf_url' => $result['pdf_url'] ?? null,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return response()->json([
|
|
|
|
|
+ 'success' => true,
|
|
|
|
|
+ 'message' => 'PDF生成成功',
|
|
|
|
|
+ 'data' => $result,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (\Exception $e) {
|
|
|
|
|
+ Log::error('生成指定题目PDF失败', [
|
|
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
|
|
+ 'trace' => $e->getTraceAsString(),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return response()->json([
|
|
|
|
|
+ 'success' => false,
|
|
|
|
|
+ 'message' => '生成PDF失败: ' . $e->getMessage(),
|
|
|
|
|
+ ], 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Group questions by type and maintain order
|
|
|
|
|
+ */
|
|
|
|
|
+ private function groupQuestionsByType($questions, array $originalOrder): array
|
|
|
|
|
+ {
|
|
|
|
|
+ // Create a map for quick lookup and preserve original order
|
|
|
|
|
+ $questionMap = [];
|
|
|
|
|
+ foreach ($questions as $q) {
|
|
|
|
|
+ $questionMap[$q->id] = $q;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $grouped = [
|
|
|
|
|
+ 'choice' => [],
|
|
|
|
|
+ 'fill' => [],
|
|
|
|
|
+ 'answer' => [],
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ $questionNumber = 1;
|
|
|
|
|
+
|
|
|
|
|
+ // Process in original order
|
|
|
|
|
+ foreach ($originalOrder as $id) {
|
|
|
|
|
+ if (!isset($questionMap[$id])) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $q = $questionMap[$id];
|
|
|
|
|
+ $type = $this->normalizeQuestionType($q->question_type);
|
|
|
|
|
+
|
|
|
|
|
+ // Build question object for template
|
|
|
|
|
+ $questionObj = (object) [
|
|
|
|
|
+ 'id' => $q->id,
|
|
|
|
|
+ 'question_number' => $questionNumber++,
|
|
|
|
|
+ 'content' => $q->stem,
|
|
|
|
|
+ 'options' => is_string($q->options) ? json_decode($q->options, true) : ($q->options ?? []),
|
|
|
|
|
+ 'answer' => $q->answer,
|
|
|
|
|
+ 'solution' => $q->solution,
|
|
|
|
|
+ 'score' => $this->getDefaultScore($type),
|
|
|
|
|
+ 'difficulty' => $q->difficulty,
|
|
|
|
|
+ 'kp_code' => $q->kp_code,
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ $grouped[$type][] = $questionObj;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $grouped;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Normalize question type to standard format
|
|
|
|
|
+ */
|
|
|
|
|
+ private function normalizeQuestionType(?string $type): string
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!$type) {
|
|
|
|
|
+ return 'answer';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $type = strtolower(trim($type));
|
|
|
|
|
+
|
|
|
|
|
+ $typeMap = [
|
|
|
|
|
+ 'choice' => 'choice',
|
|
|
|
|
+ '选择题' => 'choice',
|
|
|
|
|
+ 'single_choice' => 'choice',
|
|
|
|
|
+ 'multiple_choice' => 'choice',
|
|
|
|
|
+ 'fill' => 'fill',
|
|
|
|
|
+ '填空题' => 'fill',
|
|
|
|
|
+ 'blank' => 'fill',
|
|
|
|
|
+ 'answer' => 'answer',
|
|
|
|
|
+ '解答题' => 'answer',
|
|
|
|
|
+ 'subjective' => 'answer',
|
|
|
|
|
+ 'calculation' => 'answer',
|
|
|
|
|
+ 'proof' => 'answer',
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ return $typeMap[$type] ?? 'answer';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Get default score by question type
|
|
|
|
|
+ */
|
|
|
|
|
+ private function getDefaultScore(string $type): int
|
|
|
|
|
+ {
|
|
|
|
|
+ return match ($type) {
|
|
|
|
|
+ 'choice' => 5,
|
|
|
|
|
+ 'fill' => 5,
|
|
|
|
|
+ 'answer' => 10,
|
|
|
|
|
+ default => 5,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Build virtual paper structure
|
|
|
|
|
+ */
|
|
|
|
|
+ private function buildVirtualPaper(string $paperName, string $studentId, array $groupedQuestions): object
|
|
|
|
|
+ {
|
|
|
|
|
+ $totalScore = 0;
|
|
|
|
|
+ $totalQuestions = 0;
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($groupedQuestions as $type => $questions) {
|
|
|
|
|
+ foreach ($questions as $q) {
|
|
|
|
|
+ $totalScore += $q->score;
|
|
|
|
|
+ $totalQuestions++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Generate unique paper ID
|
|
|
|
|
+ $paperId = 'custom_' . $studentId . '_' . time() . '_' . uniqid();
|
|
|
|
|
+
|
|
|
|
|
+ return (object) [
|
|
|
|
|
+ 'paper_id' => $paperId,
|
|
|
|
|
+ 'paper_name' => $paperName,
|
|
|
|
|
+ 'total_score' => $totalScore,
|
|
|
|
|
+ 'total_questions' => $totalQuestions,
|
|
|
|
|
+ 'created_at' => now()->toDateTimeString(),
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+}
|