Procházet zdrojové kódy

feat: 增加调试题库生成pdf的接口

过卫栋 před 5 dny
rodič
revize
48ed0224b8

+ 246 - 0
app/Http/Controllers/Api/QuestionPdfController.php

@@ -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(),
+        ];
+    }
+}

+ 109 - 0
app/Services/ExamPdfExportService.php

@@ -2045,4 +2045,113 @@ class ExamPdfExportService
         $text = str_replace('\\n', '<br>', $text);
         $text = str_replace('\\n', '<br>', $text);
         return preg_replace('/(<br>\s*){3,}/', '<br><br>', $text);
         return preg_replace('/(<br>\s*){3,}/', '<br><br>', $text);
     }
     }
+
+    /**
+     * 根据指定的题目生成PDF(新增方法)
+     *
+     * @param object $paper 虚拟试卷对象
+     * @param array $groupedQuestions 按题型分组的题目数据
+     * @param array $student 学生信息 ['name' => '', 'grade' => '']
+     * @param array $teacher 教师信息 ['name' => '']
+     * @param bool $includeGrading 是否包含判卷版本
+     * @return array 返回 ['pdf_url' => '...', 'grading_pdf_url' => '...']
+     */
+    public function generateByQuestions(
+        object $paper,
+        array $groupedQuestions,
+        array $student = [],
+        array $teacher = [],
+        bool $includeGrading = false
+    ): array {
+        Log::info('generateByQuestions 开始', [
+            'paper_id' => $paper->paper_id,
+            'question_counts' => [
+                'choice' => count($groupedQuestions['choice'] ?? []),
+                'fill' => count($groupedQuestions['fill'] ?? []),
+                'answer' => count($groupedQuestions['answer'] ?? []),
+            ],
+            'include_grading' => $includeGrading,
+        ]);
+
+        try {
+            $result = [];
+
+            // 1. 生成试卷PDF(不含答案)
+            $examHtml = $this->renderCustomExamHtml($paper, $groupedQuestions, $student, $teacher, false);
+            if ($examHtml) {
+                $examPdf = $this->buildPdf($examHtml);
+                if ($examPdf) {
+                    $examPath = "custom_exams/{$paper->paper_id}_exam.pdf";
+                    $examUrl = $this->pdfStorageService->put($examPath, $examPdf);
+                    $result['pdf_url'] = $examUrl;
+                    Log::info('试卷PDF生成成功', ['url' => $examUrl]);
+                }
+            }
+
+            // 2. 如果需要,生成判卷PDF(含答案)
+            if ($includeGrading) {
+                $gradingHtml = $this->renderCustomExamHtml($paper, $groupedQuestions, $student, $teacher, true);
+                if ($gradingHtml) {
+                    $gradingPdf = $this->buildPdf($gradingHtml);
+                    if ($gradingPdf) {
+                        $gradingPath = "custom_exams/{$paper->paper_id}_grading.pdf";
+                        $gradingUrl = $this->pdfStorageService->put($gradingPath, $gradingPdf);
+                        $result['grading_pdf_url'] = $gradingUrl;
+                        Log::info('判卷PDF生成成功', ['url' => $gradingUrl]);
+                    }
+                }
+            }
+
+            return $result;
+
+        } catch (\Throwable $e) {
+            Log::error('generateByQuestions 失败', [
+                'paper_id' => $paper->paper_id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+            ]);
+            throw $e;
+        }
+    }
+
+    /**
+     * 渲染自定义题目的HTML
+     */
+    private function renderCustomExamHtml(
+        object $paper,
+        array $groupedQuestions,
+        array $student,
+        array $teacher,
+        bool $grading
+    ): ?string {
+        try {
+            $viewName = $grading ? 'pdf.exam-grading' : 'pdf.exam-paper';
+
+            $html = view($viewName, [
+                'paper' => $paper,
+                'questions' => $groupedQuestions,
+                'student' => $student,
+                'teacher' => $teacher,
+                'grading' => $grading,
+            ])->render();
+
+            if (empty(trim($html))) {
+                Log::error('renderCustomExamHtml: 视图渲染结果为空', [
+                    'paper_id' => $paper->paper_id,
+                    'view_name' => $viewName,
+                ]);
+                return null;
+            }
+
+            return $this->ensureUtf8Html($html);
+
+        } catch (\Exception $e) {
+            Log::error('renderCustomExamHtml 失败', [
+                'paper_id' => $paper->paper_id,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+            ]);
+            return null;
+        }
+    }
 }
 }

+ 12 - 0
routes/api.php

@@ -1129,6 +1129,7 @@ Route::get('/health', [HealthCheckController::class, 'index'])
 */
 */
 
 
 use App\Http\Controllers\Api\StudentProgressController;
 use App\Http\Controllers\Api\StudentProgressController;
+use App\Http\Controllers\Api\QuestionPdfController;
 
 
 // 获取单个学生学习进度
 // 获取单个学生学习进度
 Route::get('/students/{studentId}/learning-progress', [StudentProgressController::class, 'show'])
 Route::get('/students/{studentId}/learning-progress', [StudentProgressController::class, 'show'])
@@ -1140,6 +1141,17 @@ Route::post('/students/learning-progress/batch', [StudentProgressController::cla
     ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
     ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
     ->name('api.students.learning-progress.batch');
     ->name('api.students.learning-progress.batch');
 
 
+/*
+|--------------------------------------------------------------------------
+| 题目PDF生成 API 路由
+|--------------------------------------------------------------------------
+*/
+
+// 根据指定题目ID生成PDF
+Route::post('/questions/pdf', [QuestionPdfController::class, 'generate'])
+    ->withoutMiddleware([Authenticate::class, 'auth', 'auth:sanctum', 'auth:api'])
+    ->name('api.questions.pdf.generate');
+
 /*
 /*
 |--------------------------------------------------------------------------
 |--------------------------------------------------------------------------
 | 以下为旧代码(已迁移到 Controller,保留注释供参考)
 | 以下为旧代码(已迁移到 Controller,保留注释供参考)