taskManager->createTask( TaskManager::TASK_TYPE_ANALYSIS, compact('paperId', 'studentId', 'recordId') ); Log::info('ExamAnalysisService: 开始生成学情报告', [ 'task_id' => $taskId, 'paper_id' => $paperId, 'student_id' => $studentId, 'record_id' => $recordId, ]); // 触发后台处理(实际项目中应使用队列) // dispatch(new AnalysisReportJob($taskId)); // 目前使用同步调用模拟异步 $this->processReportGeneration($taskId, $paperId, $studentId, $recordId); return $taskId; } /** * 获取分析数据(同步模式) * 用于页面直接展示,不生成PDF */ public function getAnalysisData(string $paperId, string $studentId, ?string $recordId = null): ExamAnalysisDataDto { try { Log::info('ExamAnalysisService: 获取分析数据', compact('paperId', 'studentId')); // 获取试卷数据 $paperData = $this->getPaperData($paperId); if (!$paperData) { throw new \Exception('未找到试卷数据'); } // 获取学生数据 $studentData = $this->getStudentData($studentId); // 获取题目数据 $questionsData = $this->getQuestionsData($paperId, $paperData); // 获取分析数据 $analysisData = $this->getLearningAnalysisData($paperId, $studentId, $paperData); // 获取掌握度数据 $masteryData = $this->getMasteryData($studentId); // 获取学习建议 $recommendations = $this->getLearningRecommendations($studentId); $dto = new ExamAnalysisDataDto( paper: $paperData, student: $studentData, questions: $questionsData, mastery: $masteryData, insights: $analysisData, recommendations: $recommendations, analysisId: $paperData['analysis_id'] ?? null ); Log::info('ExamAnalysisService: 分析数据获取完成', [ 'paper_id' => $paperId, 'student_id' => $studentId, 'question_count' => count($questionsData), 'has_analysis' => !empty($analysisData), ]); return $dto; } catch (\Exception $e) { Log::error('ExamAnalysisService: 获取分析数据失败', [ 'paper_id' => $paperId, 'student_id' => $studentId, 'error' => $e->getMessage(), ]); throw $e; } } /** * 处理报告生成(后台任务) */ private function processReportGeneration(string $taskId, string $paperId, string $studentId, ?string $recordId): void { try { $this->taskManager->updateTaskProgress($taskId, 10, '正在获取分析数据...'); // 获取分析数据 $analysisData = $this->getAnalysisData($paperId, $studentId, $recordId); $this->taskManager->updateTaskProgress($taskId, 50, '正在生成PDF报告...'); // 生成PDF $pdfUrl = $this->pdfExportService->generateAnalysisReportPdf($paperId, $studentId, $recordId); if (!$pdfUrl) { throw new \Exception('PDF生成失败'); } $this->taskManager->updateTaskProgress($taskId, 90, '正在保存报告...'); // 保存PDF URL到数据库 $this->savePdfUrl($paperId, $studentId, $recordId, $pdfUrl); // 标记任务完成 $this->taskManager->markTaskCompleted($taskId, [ 'pdf_url' => $pdfUrl, ]); Log::info('ExamAnalysisService: 学情报告生成完成', [ 'task_id' => $taskId, 'paper_id' => $paperId, 'student_id' => $studentId, 'pdf_url' => $pdfUrl, ]); // 发送回调通知 $this->taskManager->sendCallback($taskId); } catch (\Exception $e) { Log::error('ExamAnalysisService: 报告生成失败', [ 'task_id' => $taskId, 'paper_id' => $paperId, 'student_id' => $studentId, 'error' => $e->getMessage(), ]); $this->taskManager->markTaskFailed($taskId, $e->getMessage()); } } /** * 获取试卷数据 */ private function getPaperData(string $paperId): ?array { $paper = Paper::with(['questions' => function ($query) { $query->orderBy('question_number')->orderBy('id'); }])->find($paperId); if (!$paper) { return null; } return [ 'id' => $paper->paper_id, 'paper_id' => $paper->paper_id, 'name' => $paper->paper_name, 'total_questions' => $paper->question_count, 'total_score' => $paper->total_score, 'analysis_id' => $paper->analysis_id, 'created_at' => $paper->created_at->toISOString(), ]; } /** * 获取学生数据 */ private function getStudentData(string $studentId): array { $student = Student::find($studentId); return [ 'id' => $student?->student_id ?? $studentId, 'student_id' => $student?->student_id ?? $studentId, 'name' => $student?->name ?? $studentId, 'grade' => $student?->grade ?? '未知年级', 'class' => $student?->class_name ?? '未知班级', ]; } /** * 获取题目数据 */ private function getQuestionsData(string $paperId, array $paperData): array { $paper = Paper::with('questions')->find($paperId); if (!$paper || $paper->questions->isEmpty()) { return []; } $kpNameMap = $this->getKnowledgePointNameMap(); $questionDetails = $this->getQuestionDetails($paper); $questions = []; $sortedQuestions = $paper->questions->sortBy(function (PaperQuestion $q, int $idx) { $number = $q->question_number ?? $idx + 1; return is_numeric($number) ? (float) $number : ($q->id ?? $idx); }); foreach ($sortedQuestions as $idx => $question) { $kpCode = $question->knowledge_point ?? ''; $kpName = $kpNameMap[$kpCode] ?? $kpCode ?: '未标注'; $detail = $questionDetails[(string) ($question->question_id ?? '')] ?? []; $solution = $detail['solution'] ?? $detail['解析'] ?? $detail['analysis'] ?? null; $typeRaw = $question->question_type ?? ($detail['question_type'] ?? $detail['type'] ?? ''); $normalizedType = $this->normalizeQuestionType($typeRaw); $number = $question->question_number ?? ($idx + 1); $questions[] = [ 'question_number' => $number, 'question_text' => is_array($question->question_text) ? json_encode($question->question_text, JSON_UNESCAPED_UNICODE) : ($question->question_text ?? ''), 'question_type' => $normalizedType, 'knowledge_point' => $kpCode, 'knowledge_point_name' => $kpName, 'score' => $question->score, 'solution' => $solution, ]; } return $questions; } /** * 获取学习分析数据 */ private function getLearningAnalysisData(string $paperId, string $studentId, array $paperData): array { $analysisData = []; if (!empty($paperData['analysis_id'])) { $analysis = $this->learningAnalyticsService->getAnalysisResult($paperData['analysis_id']); if (!empty($analysis['data'])) { $analysisData = $analysis['data']; } } return $analysisData; } /** * 获取掌握度数据 */ private function getMasteryData(string $studentId): array { $masteryData = []; $masteryResponse = $this->learningAnalyticsService->getStudentMastery($studentId); if (!empty($masteryResponse['data'])) { $masteryData = $this->buildMasterySummary($masteryResponse['data']); } return $masteryData; } /** * 获取学习建议 */ private function getLearningRecommendations(string $studentId): array { $recommendations = []; $recommendationResponse = $this->learningAnalyticsService->getLearningRecommendations($studentId); if (!empty($recommendationResponse['data'])) { $recommendations = $recommendationResponse['data']; } return $recommendations; } /** * 获取知识点名称映射 */ private function getKnowledgePointNameMap(): array { try { $options = $this->questionServiceApi->getKnowledgePointOptions(); return $options ?: []; } catch (\Exception $e) { Log::warning('ExamAnalysisService: 获取知识点名称失败', [ 'error' => $e->getMessage(), ]); return []; } } /** * 获取题目详情 */ private function getQuestionDetails(Paper $paper): array { $details = []; $questionIds = $paper->questions->pluck('question_id')->filter()->unique()->values(); foreach ($questionIds as $qid) { try { $detail = $this->questionBankService->getQuestion((string) $qid); if (!empty($detail)) { $details[(string) $qid] = $detail; } } catch (\Throwable $e) { Log::warning('ExamAnalysisService: 获取题目详情失败', [ 'question_id' => $qid, 'error' => $e->getMessage(), ]); } } return $details; } /** * 构建掌握度摘要 */ private function buildMasterySummary(array $masteryData): array { $items = []; $total = 0; $count = 0; foreach ($masteryData as $row) { $code = $row['kp_code'] ?? null; $name = $row['kp_name'] ?? ($code ?? '未知知识点'); $level = (float) ($row['mastery_level'] ?? 0); $delta = $row['mastery_change'] ?? null; $items[] = [ 'kp_code' => $code, 'kp_name' => $name, 'mastery_level' => $level, 'mastery_change' => $delta, ]; $total += $level; $count++; } $average = $count > 0 ? round($total / $count, 2) : null; // 按掌握度从低到高排序 usort($items, fn($a, $b) => ($a['mastery_level'] <=> $b['mastery_level'])); return [ 'items' => $items, 'average' => $average, 'weak_list' => array_slice($items, 0, 5), ]; } /** * 标准化题型 */ private function normalizeQuestionType(string $type): string { $t = strtolower(trim($type)); return match (true) { str_contains($t, 'choice') || str_contains($t, '选择') => 'choice', str_contains($t, 'fill') || str_contains($t, 'blank') || str_contains($t, '填空') => 'fill', default => 'answer', }; } /** * 保存PDF URL到数据库 */ private function savePdfUrl(string $paperId, string $studentId, ?string $recordId, string $pdfUrl): void { try { if ($recordId) { // OCR记录 $ocrRecord = \App\Models\OCRRecord::find($recordId); if ($ocrRecord) { $ocrRecord->update(['analysis_pdf_url' => $pdfUrl]); } } else { // 学生记录 - 使用新的 student_reports 表 \App\Models\StudentReport::updateOrCreate( [ 'student_id' => $studentId, 'report_type' => 'exam_analysis', 'exam_id' => $paperId, ], [ 'pdf_url' => $pdfUrl, 'generation_status' => 'completed', 'generated_at' => now(), 'updated_at' => now(), ] ); Log::info('ExamAnalysisService: PDF URL已保存到student_reports表', [ 'paper_id' => $paperId, 'student_id' => $studentId, 'pdf_url' => $pdfUrl, ]); } } catch (\Throwable $e) { Log::error('ExamAnalysisService: 保存PDF URL失败', [ 'paper_id' => $paperId, 'student_id' => $studentId, 'record_id' => $recordId, 'error' => $e->getMessage(), ]); } } }