yemeishu 5 дней назад
Родитель
Сommit
909dd98dbe

+ 17 - 5
app/Http/Controllers/Api/IntelligentExamController.php

@@ -277,14 +277,26 @@ class IntelligentExamController extends Controller
 
     /**
      * 触发PDF生成
-     * 实际项目中应使用队列dispatch(new GenerateExamPdfJob($taskId, $paperId));
+     * 使用队列进行异步处理
      */
     private function triggerPdfGeneration(string $taskId, string $paperId): void
     {
-        // 实际项目中应该:
-        // dispatch(new GenerateExamPdfJob($taskId, $paperId));
-        // 目前使用同步调用模拟异步
-        $this->processPdfGeneration($taskId, $paperId);
+        // 异步处理PDF生成 - 将任务放入队列
+        try {
+            dispatch(new \App\Jobs\GenerateExamPdfJob($taskId, $paperId));
+            Log::info('PDF生成任务已加入队列', [
+                'task_id' => $taskId,
+                'paper_id' => $paperId
+            ]);
+        } catch (\Exception $e) {
+            Log::error('PDF生成任务队列失败,回退到同步处理', [
+                'task_id' => $taskId,
+                'paper_id' => $paperId,
+                'error' => $e->getMessage()
+            ]);
+            // 队列失败时回退到同步处理
+            $this->processPdfGeneration($taskId, $paperId);
+        }
     }
 
     /**

+ 92 - 0
app/Jobs/GenerateExamPdfJob.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\Paper;
+use App\Services\ExamPdfExportService;
+use App\Services\QuestionBankService;
+use App\Services\PaperPayloadService;
+use App\Services\TaskManager;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+
+class GenerateExamPdfJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public string $taskId;
+    public string $paperId;
+    public int $maxAttempts = 3;
+
+    public function __construct(string $taskId, string $paperId)
+    {
+        $this->taskId = $taskId;
+        $this->paperId = $paperId;
+    }
+
+    public function handle(
+        ExamPdfExportService $pdfExportService,
+        QuestionBankService $questionBankService,
+        PaperPayloadService $paperPayloadService,
+        TaskManager $taskManager
+    ): void {
+        try {
+            Log::info('开始处理PDF生成队列任务', [
+                'task_id' => $this->taskId,
+                'paper_id' => $this->paperId
+            ]);
+
+            $taskManager->updateTaskProgress($this->taskId, 10, '开始生成试卷PDF...');
+
+            // 生成试卷PDF
+            $pdfUrl = $pdfExportService->generateExamPdf($this->paperId)
+                ?? $questionBankService->exportExamToPdf($this->paperId)
+                ?? route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $this->paperId, 'answer' => 'false']);
+
+            $taskManager->updateTaskProgress($this->taskId, 50, '试卷PDF生成完成,开始生成判卷PDF...');
+
+            // 生成判卷PDF
+            $gradingPdfUrl = $pdfExportService->generateGradingPdf($this->paperId)
+                ?? route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $this->paperId, 'answer' => 'true']);
+
+            // 构建完整的试卷内容
+            $paperModel = Paper::with('questions')->find($this->paperId);
+            $examContent = $paperModel
+                ? $paperPayloadService->buildExamContent($paperModel)
+                : [];
+
+            // 标记任务完成
+            $taskManager->markTaskCompleted($this->taskId, [
+                'exam_content' => $examContent,
+                'pdfs' => [
+                    'exam_paper_pdf' => $pdfUrl,
+                    'grading_pdf' => $gradingPdfUrl,
+                ],
+            ]);
+
+            Log::info('PDF生成队列任务完成', [
+                'task_id' => $this->taskId,
+                'paper_id' => $this->paperId,
+                'pdf_url' => $pdfUrl,
+                'grading_pdf_url' => $gradingPdfUrl,
+            ]);
+
+            // 发送回调通知
+            $taskManager->sendCallback($this->taskId);
+
+        } catch (\Exception $e) {
+            Log::error('PDF生成队列任务失败', [
+                'task_id' => $this->taskId,
+                'paper_id' => $this->paperId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+            ]);
+
+            $taskManager->markTaskFailed($this->taskId, $e->getMessage());
+        }
+    }
+}

+ 154 - 20
app/Services/LearningAnalyticsService.php

@@ -1271,12 +1271,37 @@ class LearningAnalyticsService
             file_put_contents($logFile, $logMsg, FILE_APPEND);
 
             // 2. 调用题库API获取符合条件的所有题目
-            $allQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, $difficultyRatio, 200);
+            try {
+                Log::info('开始调用 getQuestionsFromBank', [
+                    'kp_codes_count' => count($kpCodes),
+                    'skills_count' => count($skills)
+                ]);
 
-            $logMsg = "getQuestionsFromBank 返回\n";
-            $logMsg .= "questions_count: " . count($allQuestions) . "\n";
-            $logMsg .= "耗时: " . round((microtime(true) - $startTime) * 1000, 2) . "ms\n\n";
-            file_put_contents($logFile, $logMsg, FILE_APPEND);
+                $allQuestions = $this->getQuestionsFromBank($kpCodes, $skills, $studentId, $questionTypeRatio, $difficultyRatio, 200);
+
+                Log::info('getQuestionsFromBank 调用完成', [
+                    'questions_count' => count($allQuestions),
+                    'is_array' => is_array($allQuestions),
+                    'first_question_id' => !empty($allQuestions) ? ($allQuestions[0]['id'] ?? 'N/A') : 'N/A'
+                ]);
+
+                $logMsg = "getQuestionsFromBank 返回\n";
+                $logMsg .= "questions_count: " . count($allQuestions) . "\n";
+                $logMsg .= "耗时: " . round((microtime(true) - $startTime) * 1000, 2) . "ms\n\n";
+                file_put_contents($logFile, $logMsg, FILE_APPEND);
+
+                Log::info('getQuestionsFromBank 返回', [
+                    'questions_count' => count($allQuestions),
+                    'time_ms' => round((microtime(true) - $startTime) * 1000, 2)
+                ]);
+            } catch (\Exception $e) {
+                Log::error('getQuestionsFromBank 调用失败', [
+                    'error' => $e->getMessage(),
+                    'trace' => $e->getTraceAsString()
+                ]);
+
+                throw $e;
+            }
 
             if (empty($allQuestions)) {
                 // 如果指定了知识点但题库为空,给出明确提示
@@ -1304,6 +1329,12 @@ class LearningAnalyticsService
             }
 
             // 3. 根据掌握度对题目进行筛选和排序
+            Log::info('开始调用 selectQuestionsByMastery', [
+                'input_count' => count($allQuestions),
+                'target_count' => $totalQuestions
+            ]);
+
+            $startTime = microtime(true);
             $selectedQuestions = $this->selectQuestionsByMastery(
                 $allQuestions,
                 $studentId,
@@ -1313,11 +1344,13 @@ class LearningAnalyticsService
                 $difficultyLevels,
                 $weaknessFilter
             );
+            $selectTime = (microtime(true) - $startTime) * 1000;
 
             Log::info('题目筛选结果', [
                 'input_count' => count($allQuestions),
                 'selected_count' => count($selectedQuestions),
-                'target_count' => $totalQuestions
+                'target_count' => $totalQuestions,
+                'select_time_ms' => round($selectTime, 2)
             ]);
 
             if (empty($selectedQuestions)) {
@@ -1397,24 +1430,57 @@ class LearningAnalyticsService
             $questions = $this->questionBankService->selectQuestionsForExam($totalNeeded, $filters);
 
             if (!empty($questions)) {
-                // 过滤掉没有解题思路的题目
-                $questionsWithSolution = array_filter($questions, function($q) {
-                    $solution = $q['solution'] ?? '';
-                    // 处理 solution 可能是数组的情况
-                    if (is_array($solution)) {
-                        $solution = json_encode($solution, JSON_UNESCAPED_UNICODE);
+                Log::info('从题库获取到题目,开始过滤解题思路', [
+                    'total_from_bank' => count($questions),
+                    'filters' => $filters
+                ]);
+
+                $filterStartTime = microtime(true);
+                $questionsWithSolution = [];
+                $noSolutionCount = 0;
+
+                foreach ($questions as $index => $q) {
+                    try {
+                        $solution = $q['solution'] ?? '';
+                        // 处理 solution 可能是数组的情况
+                        if (is_array($solution)) {
+                            $solution = json_encode($solution, JSON_UNESCAPED_UNICODE);
+                        }
+                        if (!empty(trim($solution))) {
+                            $questionsWithSolution[] = $q;
+                        } else {
+                            $noSolutionCount++;
+                        }
+                    } catch (\Exception $e) {
+                        Log::error('过滤解题思路时出错', [
+                            'question_index' => $index,
+                            'error' => $e->getMessage()
+                        ]);
                     }
-                    return !empty(trim($solution));
-                });
+                }
+
+                $filterTime = (microtime(true) - $filterStartTime) * 1000;
+                $hasSolutionCount = count($questionsWithSolution);
 
                 Log::info('从题库智能获取题目', [
                     'total_from_bank' => count($questions),
-                    'has_solution' => count($questionsWithSolution),
-                    'filtered_out' => count($questions) - count($questionsWithSolution),
+                    'has_solution' => $hasSolutionCount,
+                    'no_solution' => $noSolutionCount,
+                    'filter_time_ms' => round($filterTime, 2),
                     'filters' => $filters
                 ]);
 
-                return array_values($questionsWithSolution);
+                if ($hasSolutionCount > 0) {
+                    Log::info('返回有解题思路的题目', [
+                        'count' => $hasSolutionCount
+                    ]);
+                    return array_values($questionsWithSolution);
+                } else {
+                    Log::warning('所有题目都没有解题思路,返回空数组', [
+                        'total_questions' => count($questions)
+                    ]);
+                    return [];
+                }
             }
 
             Log::warning('智能选题返回空结果', [
@@ -1443,6 +1509,12 @@ class LearningAnalyticsService
         array $difficultyLevels,
         array $weaknessFilter
     ): array {
+        Log::info('selectQuestionsByMastery 开始', [
+            'question_count' => count($questions),
+            'student_id' => $studentId,
+            'total_questions' => $totalQuestions
+        ]);
+
         // 如果未选择难度,则不过滤(随机生成所有难度)
         if (empty($difficultyLevels)) {
             Log::info('用户未选择难度,将随机生成所有难度的题目');
@@ -1457,7 +1529,16 @@ class LearningAnalyticsService
             }));
         }
 
+        Log::info('难度筛选完成', [
+            'after_filter_count' => count($questions)
+        ]);
+
         // 1. 按知识点分组
+        Log::info('开始按知识点分组', [
+            'question_count' => count($questions)
+        ]);
+
+        $groupStartTime = microtime(true);
         $questionsByKp = [];
         foreach ($questions as $question) {
             $kpCode = $question['kp_code'] ?? '';
@@ -1466,13 +1547,53 @@ class LearningAnalyticsService
             }
             $questionsByKp[$kpCode][] = $question;
         }
+        $groupTime = (microtime(true) - $groupStartTime) * 1000;
+
+        Log::info('按知识点分组完成', [
+            'kp_count' => count($questionsByKp),
+            'group_time_ms' => round($groupTime, 2)
+        ]);
 
         // 2. 为每个知识点计算权重
         $kpWeights = [];
-        foreach (array_keys($questionsByKp) as $kpCode) {
+        $kpCodes = array_keys($questionsByKp);
+        Log::info('开始计算知识点权重', [
+            'kp_count' => count($kpCodes),
+            'student_id' => $studentId
+        ]);
+
+        $startTime = microtime(true);
+        $allMastery = [];
+
+        if ($studentId) {
+            // 批量获取所有知识点的掌握度(一次查询)
+            $masteryStart = microtime(true);
+            try {
+                $masteryRecords = DB::table('student_mastery')
+                    ->where('student_id', $studentId)
+                    ->whereIn('kp', $kpCodes)
+                    ->pluck('mastery', 'kp')
+                    ->all();
+
+                Log::debug('批量获取掌握度', [
+                    'student_id' => $studentId,
+                    'kp_count' => count($kpCodes),
+                    'found_count' => count($masteryRecords),
+                    'time_ms' => round((microtime(true) - $masteryStart) * 1000, 2)
+                ]);
+
+                $allMastery = $masteryRecords;
+            } catch (\Exception $e) {
+                Log::warning('批量获取掌握度失败,将使用默认值', [
+                    'student_id' => $studentId,
+                    'error' => $e->getMessage()
+                ]);
+            }
+        }
+
+        foreach ($kpCodes as $kpCode) {
             if ($studentId) {
-                // 获取学生对该知识点的掌握度
-                $mastery = $this->getStudentKpMastery($studentId, $kpCode);
+                $mastery = $allMastery[$kpCode] ?? 0.5; // 默认0.5(中等掌握度)
 
                 // 薄弱点权重更高
                 if (in_array($kpCode, $weaknessFilter)) {
@@ -1481,11 +1602,24 @@ class LearningAnalyticsService
                     // 掌握度越低,权重越高
                     $kpWeights[$kpCode] = 1.0 + (1.0 - $mastery) * 1.5;
                 }
+
+                Log::debug('计算知识点权重', [
+                    'kp_code' => $kpCode,
+                    'mastery' => $mastery,
+                    'weight' => $kpWeights[$kpCode]
+                ]);
             } else {
                 $kpWeights[$kpCode] = 1.0; // 未指定学生时平均分配
             }
         }
 
+        $totalWeightTime = (microtime(true) - $startTime) * 1000;
+        Log::info('知识点权重计算完成', [
+            'total_kp_count' => count($kpCodes),
+            'total_weight_time_ms' => round($totalWeightTime, 2),
+            'avg_time_per_kp_ms' => count($kpCodes) > 0 ? round($totalWeightTime / count($kpCodes), 2) : 0
+        ]);
+
         // 3. 按权重分配题目数量
         $totalWeight = array_sum($kpWeights);
         $selectedQuestions = [];