Bladeren bron

optimize analysis PDF memory footprint and diagnostics

Reduce database and memory pressure in analysis PDF generation by using module-scoped knowledge point loading, cache-backed metadata retrieval with fallback, batched parent-node queries, and minimal start marker logging for faster failure localization.

Made-with: Cursor
yemeishu 2 weken geleden
bovenliggende
commit
a9d1edabe1
1 gewijzigde bestanden met toevoegingen van 333 en 84 verwijderingen
  1. 333 84
      app/Services/ExamPdfExportService.php

+ 333 - 84
app/Services/ExamPdfExportService.php

@@ -12,6 +12,7 @@ use App\Services\Analytics\QuestionDifficultyCalibrationAnalyzer;
 use App\Support\GradingStyleQuestionStem;
 use App\Support\GradingStyleQuestionStem;
 use App\Support\PaperNaming;
 use App\Support\PaperNaming;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\File;
 use Illuminate\Support\Facades\File;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Log;
@@ -33,13 +34,15 @@ class ExamPdfExportService
     private ?KatexRenderer $katexRenderer = null;
     private ?KatexRenderer $katexRenderer = null;
     private const PDF_IMAGE_WIDTH_WIDE_PX = 250;
     private const PDF_IMAGE_WIDTH_WIDE_PX = 250;
     private const PDF_IMAGE_WIDTH_VERY_WIDE_PX = 330;
     private const PDF_IMAGE_WIDTH_VERY_WIDE_PX = 330;
+    private const KNOWLEDGE_POINTS_CACHE_TTL_SECONDS = 86400 * 3;
+    private const KNOWLEDGE_POINTS_VERSION_TTL_SECONDS = 600;
 
 
     /**
     /**
      * @var array<string, array{w:int,h:int}|null>
      * @var array<string, array{w:int,h:int}|null>
      */
      */
     private array $pdfImageDimensionCache = [];
     private array $pdfImageDimensionCache = [];
     private ?bool $hasPdfImageMetricsTable = null;
     private ?bool $hasPdfImageMetricsTable = null;
-    private ?array $knowledgePointMetaCache = null;
+    private array $knowledgePointMetaCache = [];
     private ?string $lastDebugHtmlPath = null;
     private ?string $lastDebugHtmlPath = null;
 
 
     public function __construct(
     public function __construct(
@@ -347,6 +350,10 @@ class ExamPdfExportService
                 'student_id' => $studentId,
                 'student_id' => $studentId,
                 'record_id' => $recordId,
                 'record_id' => $recordId,
             ]);
             ]);
+            Log::warning('ExamPdfExportService: ANALYSIS_PDF_START', [
+                'paper_id' => $paperId,
+                'student_id' => $studentId,
+            ]);
 
 
             // 构建分析数据
             // 构建分析数据
             $analysisData = $this->buildAnalysisData($paperId, $studentId);
             $analysisData = $this->buildAnalysisData($paperId, $studentId);
@@ -360,12 +367,13 @@ class ExamPdfExportService
                 return null;
                 return null;
             }
             }
 
 
-            Log::info('ExamPdfExportService: buildAnalysisData返回数据', [
+            Log::info('ExamPdfExportService: buildAnalysisData返回摘要', [
                 'paper_id' => $paperId,
                 'paper_id' => $paperId,
                 'student_id' => $studentId,
                 'student_id' => $studentId,
-                'analysisData_keys' => array_keys($analysisData),
-                'mastery_count' => count($analysisData['mastery']['items'] ?? []),
+                'analysisData_keys_count' => count(array_keys($analysisData)),
+                'analysis_question_count' => count($analysisData['analysis_data']['question_analysis'] ?? []),
                 'questions_count' => count($analysisData['questions'] ?? []),
                 'questions_count' => count($analysisData['questions'] ?? []),
+                'mastery_count' => count($analysisData['mastery']['items'] ?? []),
             ]);
             ]);
 
 
             // 创建DTO
             // 创建DTO
@@ -373,17 +381,13 @@ class ExamPdfExportService
             $payloadDto = ReportPayloadDto::fromExamAnalysisDataDto($dto);
             $payloadDto = ReportPayloadDto::fromExamAnalysisDataDto($dto);
             $mark('build_payload_dto_ms');
             $mark('build_payload_dto_ms');
 
 
-            // 打印传给模板的数据
-            $templateData = $payloadDto->toArray();
-            Log::info('ExamPdfExportService: 传给模板的数据', [
-                'paper' => $templateData['paper'] ?? null,
-                'student' => $templateData['student'] ?? null,
-                'mastery' => $templateData['mastery'] ?? null,
-                'parent_mastery_levels' => $templateData['parent_mastery_levels'] ?? null,
-                'questions_count' => count($templateData['questions'] ?? []),
-                'insights_count' => count($templateData['question_insights'] ?? []),
-                'recommendations_count' => count($templateData['recommendations'] ?? []),
-            ]);
+            $templateData = $this->reduceTemplateDataForV3($payloadDto->toArray());
+            unset($analysisData, $dto, $payloadDto);
+            if (function_exists('gc_collect_cycles')) {
+                gc_collect_cycles();
+            }
+
+            Log::info('ExamPdfExportService: 模板数据摘要', $this->summarizeTemplateDataForLog($templateData));
 
 
             // V3 学情报告不再渲染逐题错题卡,保留题目元数据用于统计即可。
             // V3 学情报告不再渲染逐题错题卡,保留题目元数据用于统计即可。
             $mark('prepare_report_data_ms');
             $mark('prepare_report_data_ms');
@@ -462,13 +466,13 @@ class ExamPdfExportService
         $questionRows = $rawAnalysis['question_analysis'] ?? [];
         $questionRows = $rawAnalysis['question_analysis'] ?? [];
         $overallSummary = $rawAnalysis['overall_summary'] ?? [];
         $overallSummary = $rawAnalysis['overall_summary'] ?? [];
 
 
-        $kpMeta = $this->getKnowledgePointMetaMap();
         $grade = (string) ($templateData['student']['grade'] ?? '');
         $grade = (string) ($templateData['student']['grade'] ?? '');
         $stage = $this->resolveRadarStage($grade, $templateData['full_parent_mastery_levels'] ?? []);
         $stage = $this->resolveRadarStage($grade, $templateData['full_parent_mastery_levels'] ?? []);
         $profile = $this->getRadarProfileByStage($stage);
         $profile = $this->getRadarProfileByStage($stage);
         $rootCode = (string) ($profile['root_code'] ?? 'M00');
         $rootCode = (string) ($profile['root_code'] ?? 'M00');
         $moduleCodes = $profile['module_codes'] ?? [];
         $moduleCodes = $profile['module_codes'] ?? [];
         $moduleNames = $profile['module_names'] ?? [];
         $moduleNames = $profile['module_names'] ?? [];
+        $kpMeta = $this->getKnowledgePointMetaMap($rootCode, $moduleCodes);
 
 
         $moduleAgg = [];
         $moduleAgg = [];
         foreach ($moduleCodes as $moduleCode) {
         foreach ($moduleCodes as $moduleCode) {
@@ -668,7 +672,14 @@ class ExamPdfExportService
                 }
                 }
             }
             }
         }
         }
-        $stageCodes = $this->collectStageKnowledgePoints($rootCode);
+        $moduleSet = array_fill_keys(array_map(static fn ($code) => (string) $code, $moduleCodes), true);
+        $stageCodes = [];
+        foreach (array_keys($kpMeta) as $kpCode) {
+            if ($kpCode === $rootCode || isset($moduleSet[$kpCode])) {
+                continue;
+            }
+            $stageCodes[] = $kpCode;
+        }
         $childrenByParent = [];
         $childrenByParent = [];
         foreach ($kpMeta as $code => $meta) {
         foreach ($kpMeta as $code => $meta) {
             $p = trim((string) ($meta['parent_kp_code'] ?? ''));
             $p = trim((string) ($meta['parent_kp_code'] ?? ''));
@@ -1472,41 +1483,52 @@ class ExamPdfExportService
             return [];
             return [];
         }
         }
 
 
+        $cacheVersion = $this->getKnowledgePointsCacheVersion();
+        $cacheKey = sprintf('exam_pdf:kp_descendants:v1:%s:%s', $rootCode, $cacheVersion);
+
         try {
         try {
-            $rows = DB::connection('mysql')
-                ->table('knowledge_points')
-                ->select('kp_code', 'parent_kp_code')
-                ->get();
+            return $this->rememberWithFallback($cacheKey, self::KNOWLEDGE_POINTS_CACHE_TTL_SECONDS, function () use ($rootCode) {
+                $result = [];
+                $currentParents = [$rootCode];
+                $visitedParents = [];
+                $seenCodes = [];
+                while (! empty($currentParents)) {
+                    $batchParents = [];
+                    foreach ($currentParents as $parentCode) {
+                        $parentCode = trim((string) $parentCode);
+                        if ($parentCode === '' || isset($visitedParents[$parentCode])) {
+                            continue;
+                        }
+                        $visitedParents[$parentCode] = true;
+                        $batchParents[] = $parentCode;
+                    }
 
 
-            $children = [];
-            foreach ($rows as $row) {
-                $code = trim((string) ($row->kp_code ?? ''));
-                $parent = trim((string) ($row->parent_kp_code ?? ''));
-                if ($code === '' || $parent === '') {
-                    continue;
-                }
-                $children[$parent][] = $code;
-            }
+                    if ($batchParents === []) {
+                        break;
+                    }
 
 
-            $result = [];
-            $queue = [$rootCode];
-            $visited = [];
-            while (! empty($queue)) {
-                $parent = array_shift($queue);
-                if (isset($visited[$parent])) {
-                    continue;
-                }
-                $visited[$parent] = true;
-                foreach (($children[$parent] ?? []) as $childCode) {
-                    if ($childCode === $rootCode) {
-                        continue;
+                    $rows = DB::connection('mysql')
+                        ->table('knowledge_points')
+                        ->whereIn('parent_kp_code', $batchParents)
+                        ->select('kp_code')
+                        ->get();
+
+                    $nextParents = [];
+                    foreach ($rows as $row) {
+                        $childCode = trim((string) ($row->kp_code ?? ''));
+                        if ($childCode === '' || $childCode === $rootCode || isset($seenCodes[$childCode])) {
+                            continue;
+                        }
+                        $seenCodes[$childCode] = true;
+                        $result[] = $childCode;
+                        $nextParents[] = $childCode;
                     }
                     }
-                    $result[] = $childCode;
-                    $queue[] = $childCode;
+
+                    $currentParents = $nextParents;
                 }
                 }
-            }
 
 
-            return array_values(array_unique($result));
+                return $result;
+            });
         } catch (\Throwable $e) {
         } catch (\Throwable $e) {
             Log::warning('ExamPdfExportService: collectStageKnowledgePoints failed', [
             Log::warning('ExamPdfExportService: collectStageKnowledgePoints failed', [
                 'root_code' => $rootCode,
                 'root_code' => $rootCode,
@@ -1626,32 +1648,239 @@ class ExamPdfExportService
         ];
         ];
     }
     }
 
 
-    private function getKnowledgePointMetaMap(): array
+    private function getKnowledgePointMetaMap(string $rootCode, array $moduleCodes): array
     {
     {
-        if ($this->knowledgePointMetaCache !== null) {
-            return $this->knowledgePointMetaCache;
+        $moduleCodes = array_values(array_unique(array_filter(array_map(static fn ($code) => trim((string) $code), $moduleCodes))));
+        $cacheVersion = $this->getKnowledgePointsCacheVersion();
+        $inMemoryKey = $rootCode.'|'.implode(',', $moduleCodes).'|'.$cacheVersion;
+        if (isset($this->knowledgePointMetaCache[$inMemoryKey])) {
+            return $this->knowledgePointMetaCache[$inMemoryKey];
         }
         }
 
 
-        $rows = DB::connection('mysql')
-            ->table('knowledge_points')
-            ->select(['kp_code', 'name', 'parent_kp_code'])
-            ->get();
+        $cacheKey = sprintf('exam_pdf:kp_meta_map:v2:%s:%s:%s', $rootCode, md5(implode(',', $moduleCodes)), $cacheVersion);
 
 
-        $map = [];
-        foreach ($rows as $row) {
-            $code = trim((string) ($row->kp_code ?? ''));
-            if ($code === '') {
+        $map = $this->rememberWithFallback($cacheKey, self::KNOWLEDGE_POINTS_CACHE_TTL_SECONDS, function () use ($rootCode, $moduleCodes) {
+            $seedCodes = array_values(array_unique(array_filter(array_merge([$rootCode], $moduleCodes))));
+            if ($seedCodes === []) {
+                return [];
+            }
+
+            $rows = DB::connection('mysql')
+                ->table('knowledge_points')
+                ->whereIn('kp_code', $seedCodes)
+                ->select(['kp_code', 'name', 'parent_kp_code'])
+                ->get();
+
+            $result = [];
+            foreach ($rows as $row) {
+                $code = trim((string) ($row->kp_code ?? ''));
+                if ($code === '') {
+                    continue;
+                }
+                $result[$code] = [
+                    'name' => (string) ($row->name ?? $code),
+                    'parent_kp_code' => trim((string) ($row->parent_kp_code ?? '')),
+                ];
+            }
+
+            // 从模块出发按层拉取子树,避免从学段根节点全量展开
+            $currentParents = $moduleCodes;
+            $visitedParents = [];
+            $guard = 0;
+            while (! empty($currentParents) && $guard < 40) {
+                $batchParents = [];
+                foreach ($currentParents as $parentCode) {
+                    $parentCode = trim((string) $parentCode);
+                    if ($parentCode === '' || isset($visitedParents[$parentCode])) {
+                        continue;
+                    }
+                    $visitedParents[$parentCode] = true;
+                    $batchParents[] = $parentCode;
+                }
+                if ($batchParents === []) {
+                    break;
+                }
+
+                $childRows = DB::connection('mysql')
+                    ->table('knowledge_points')
+                    ->whereIn('parent_kp_code', $batchParents)
+                    ->select(['kp_code', 'name', 'parent_kp_code'])
+                    ->get();
+
+                $nextParents = [];
+                foreach ($childRows as $row) {
+                    $code = trim((string) ($row->kp_code ?? ''));
+                    if ($code === '' || isset($result[$code])) {
+                        continue;
+                    }
+                    $result[$code] = [
+                        'name' => (string) ($row->name ?? $code),
+                        'parent_kp_code' => trim((string) ($row->parent_kp_code ?? '')),
+                    ];
+                    $nextParents[] = $code;
+                }
+                $currentParents = $nextParents;
+                $guard++;
+            }
+
+            // 补齐祖先链,确保路径与模块归属计算不丢失
+            $pendingParents = [];
+            foreach ($result as $meta) {
+                $parent = trim((string) ($meta['parent_kp_code'] ?? ''));
+                if ($parent !== '' && ! isset($result[$parent])) {
+                    $pendingParents[$parent] = true;
+                }
+            }
+
+            $ancestorGuard = 0;
+            while (! empty($pendingParents) && $ancestorGuard < 24) {
+                $parentBatch = array_keys($pendingParents);
+                $pendingParents = [];
+                foreach (array_chunk($parentBatch, 500) as $chunk) {
+                    $ancestorRows = DB::connection('mysql')
+                        ->table('knowledge_points')
+                        ->whereIn('kp_code', $chunk)
+                        ->select(['kp_code', 'name', 'parent_kp_code'])
+                        ->get();
+
+                    foreach ($ancestorRows as $row) {
+                        $code = trim((string) ($row->kp_code ?? ''));
+                        if ($code === '') {
+                            continue;
+                        }
+                        if (! isset($result[$code])) {
+                            $result[$code] = [
+                                'name' => (string) ($row->name ?? $code),
+                                'parent_kp_code' => trim((string) ($row->parent_kp_code ?? '')),
+                            ];
+                        }
+                        $parent = trim((string) ($result[$code]['parent_kp_code'] ?? ''));
+                        if ($parent !== '' && ! isset($result[$parent])) {
+                            $pendingParents[$parent] = true;
+                        }
+                    }
+                }
+                $ancestorGuard++;
+            }
+
+            return $result;
+        });
+
+        $this->knowledgePointMetaCache[$inMemoryKey] = $map;
+
+        return $map;
+    }
+
+    private function getKnowledgePointsCacheVersion(): string
+    {
+        return $this->rememberWithFallback('exam_pdf:kp_cache_version:v1', self::KNOWLEDGE_POINTS_VERSION_TTL_SECONDS, function () {
+            $row = DB::connection('mysql')
+                ->table('knowledge_points')
+                ->selectRaw('COALESCE(UNIX_TIMESTAMP(MAX(updated_at)), 0) as max_updated_ts, COUNT(*) as total_count')
+                ->first();
+
+            $maxUpdatedTs = (string) (($row->max_updated_ts ?? 0));
+            $totalCount = (string) (($row->total_count ?? 0));
+
+            return $maxUpdatedTs.'_'.$totalCount;
+        });
+    }
+
+    private function rememberWithFallback(string $key, int $ttlSeconds, callable $resolver): mixed
+    {
+        $resolved = false;
+        $resolvedValue = null;
+        try {
+            return Cache::remember($key, now()->addSeconds($ttlSeconds), function () use ($resolver, &$resolved, &$resolvedValue) {
+                $resolvedValue = $resolver();
+                $resolved = true;
+
+                return $resolvedValue;
+            });
+        } catch (\Throwable $e) {
+            Log::warning('ExamPdfExportService: 缓存不可用,回退直查', [
+                'cache_key' => $key,
+                'error' => $e->getMessage(),
+            ]);
+
+            if ($resolved) {
+                return $resolvedValue;
+            }
+
+            return $resolver();
+        }
+    }
+
+    /**
+     * V3 报告仅保留模板和统计所需字段,避免大对象在内存中长期驻留。
+     */
+    private function reduceTemplateDataForV3(array $templateData): array
+    {
+        unset($templateData['question_insights'], $templateData['recommendations']);
+
+        $templateData['questions'] = $this->compactQuestionsForV3($templateData['questions'] ?? []);
+
+        if (isset($templateData['analysis_data']['question_analysis']) && is_array($templateData['analysis_data']['question_analysis'])) {
+            $templateData['analysis_data']['question_analysis'] = array_map(static function ($item) {
+                if (! is_array($item)) {
+                    return [];
+                }
+
+                return [
+                    'question_id' => $item['question_id'] ?? null,
+                    'question_bank_id' => $item['question_bank_id'] ?? null,
+                    'is_correct' => $item['is_correct'] ?? null,
+                ];
+            }, $templateData['analysis_data']['question_analysis']);
+        }
+
+        return $templateData;
+    }
+
+    /**
+     * 仅输出摘要日志,避免将大数组序列化进日志造成额外内存开销。
+     *
+     * @return array<string, mixed>
+     */
+    private function summarizeTemplateDataForLog(array $templateData): array
+    {
+        return [
+            'paper_id' => $templateData['paper']['id'] ?? null,
+            'student_id' => $templateData['student']['id'] ?? null,
+            'questions_count' => count($templateData['questions'] ?? []),
+            'analysis_question_count' => count($templateData['analysis_data']['question_analysis'] ?? []),
+            'mastery_count' => count($templateData['mastery']['items'] ?? []),
+            'exam_hit_kp_count' => count($templateData['exam_hit_kp_codes'] ?? []),
+            'parent_mastery_count' => count($templateData['parent_mastery_levels'] ?? []),
+            'full_parent_mastery_count' => count($templateData['full_parent_mastery_levels'] ?? []),
+        ];
+    }
+
+    /**
+     * 仅保留 PDF V3 模板使用到的题目字段,减少内存占用与序列化开销。
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    private function compactQuestionsForV3(array $questions): array
+    {
+        $compact = [];
+        foreach ($questions as $question) {
+            if (! is_array($question)) {
                 continue;
                 continue;
             }
             }
-            $map[$code] = [
-                'name' => (string) ($row->name ?? $code),
-                'parent_kp_code' => trim((string) ($row->parent_kp_code ?? '')),
+            $compact[] = [
+                'question_id' => $question['question_id'] ?? null,
+                'question_bank_id' => $question['question_bank_id'] ?? null,
+                'is_correct' => $question['is_correct'] ?? null,
+                'student_answer' => $question['student_answer'] ?? null,
+                'answer' => $question['answer'] ?? null,
+                'correct_answer' => $question['correct_answer'] ?? null,
+                'knowledge_point' => $question['knowledge_point'] ?? null,
+                'knowledge_point_name' => $question['knowledge_point_name'] ?? null,
             ];
             ];
         }
         }
 
 
-        $this->knowledgePointMetaCache = $map;
-
-        return $map;
+        return $compact;
     }
     }
 
 
     private function mapKpToStageModule(string $kpCode, array $kpMeta, string $rootCode): ?string
     private function mapKpToStageModule(string $kpCode, array $kpMeta, string $rootCode): ?string
@@ -2268,20 +2497,48 @@ class ExamPdfExportService
 
 
                 // 计算与本次考试相关的父节点掌握度(基于所有兄弟节点)
                 // 计算与本次考试相关的父节点掌握度(基于所有兄弟节点)
                 $parentMasteryLevels = [];
                 $parentMasteryLevels = [];
+                $parentCodes = array_values(array_unique(array_filter(array_map(static fn ($code) => trim((string) $code), array_keys($allParentMasteryLevels)))));
+
+                if ($parentCodes !== []) {
+                    $childRows = DB::connection('mysql')
+                        ->table('knowledge_points')
+                        ->whereIn('parent_kp_code', $parentCodes)
+                        ->select(['parent_kp_code', 'kp_code'])
+                        ->get();
+
+                    $childrenByParent = [];
+                    foreach ($childRows as $row) {
+                        $parentCode = trim((string) ($row->parent_kp_code ?? ''));
+                        $childCode = trim((string) ($row->kp_code ?? ''));
+                        if ($parentCode === '' || $childCode === '') {
+                            continue;
+                        }
+                        $childrenByParent[$parentCode][] = $childCode;
+                    }
 
 
-                // 【修复】使用数据库查询正确匹配父子关系,而不是字符串前缀
-                foreach ($allParentMasteryLevels as $parentKpCode => $parentMastery) {
-                    // 查询这个父节点的所有子节点
-                    $childNodes = DB::connection('mysql')
+                    $parentNameMap = DB::connection('mysql')
                         ->table('knowledge_points')
                         ->table('knowledge_points')
-                        ->where('parent_kp_code', $parentKpCode)
-                        ->pluck('kp_code')
+                        ->whereIn('kp_code', $parentCodes)
+                        ->pluck('name', 'kp_code')
                         ->toArray();
                         ->toArray();
 
 
-                    // 检查是否有子节点在本次考试中出现
-                    $relevantChildren = array_intersect($examKpCodes, $childNodes);
+                    foreach ($allParentMasteryLevels as $parentKpCode => $parentMastery) {
+                        $parentKpCode = trim((string) $parentKpCode);
+                        if ($parentKpCode === '') {
+                            continue;
+                        }
+
+                        $childNodes = $childrenByParent[$parentKpCode] ?? [];
+                        if ($childNodes === []) {
+                            continue;
+                        }
+
+                        // 检查是否有子节点在本次考试中出现
+                        $relevantChildren = array_intersect($examKpCodes, $childNodes);
+                        if (empty($relevantChildren)) {
+                            continue;
+                        }
 
 
-                    if (! empty($relevantChildren)) {
                         // 口径统一:父节点掌握度 = 全部直接子节点(含未命中,缺失按0)均值
                         // 口径统一:父节点掌握度 = 全部直接子节点(含未命中,缺失按0)均值
                         $childCurrentLevels = [];
                         $childCurrentLevels = [];
                         $childPreviousLevels = [];
                         $childPreviousLevels = [];
@@ -2303,15 +2560,9 @@ class ExamPdfExportService
                             : $finalParentMastery;
                             : $finalParentMastery;
                         $finalParentChange = $finalParentMastery - $previousParentMastery;
                         $finalParentChange = $finalParentMastery - $previousParentMastery;
 
 
-                        // 获取父节点中文名称
-                        $parentKpInfo = DB::connection('mysql')
-                            ->table('knowledge_points')
-                            ->where('kp_code', $parentKpCode)
-                            ->first();
-
                         $parentMasteryLevels[$parentKpCode] = [
                         $parentMasteryLevels[$parentKpCode] = [
                             'kp_code' => $parentKpCode,
                             'kp_code' => $parentKpCode,
-                            'kp_name' => $parentKpInfo->name ?? $parentKpCode,
+                            'kp_name' => $parentNameMap[$parentKpCode] ?? $parentKpCode,
                             'mastery_level' => $finalParentMastery,
                             'mastery_level' => $finalParentMastery,
                             'mastery_percentage' => round($finalParentMastery * 100, 1),
                             'mastery_percentage' => round($finalParentMastery * 100, 1),
                             'mastery_change' => $finalParentChange,
                             'mastery_change' => $finalParentChange,
@@ -2334,7 +2585,7 @@ class ExamPdfExportService
 
 
             Log::info('ExamPdfExportService: 使用analysisData中的掌握度数据', [
             Log::info('ExamPdfExportService: 使用analysisData中的掌握度数据', [
                 'count' => count($masteryData),
                 'count' => count($masteryData),
-                'masteryData_sample' => ! empty($masteryData) ? array_slice($masteryData, 0, 2) : [],
+                'has_change_values' => collect($masteryData)->contains(fn ($item) => is_array($item) && array_key_exists('mastery_change', $item)),
             ]);
             ]);
         } else {
         } else {
             // 如果没有knowledge_point_analysis,使用MasteryCalculator获取多层级掌握度概览
             // 如果没有knowledge_point_analysis,使用MasteryCalculator获取多层级掌握度概览
@@ -2447,7 +2698,6 @@ class ExamPdfExportService
         $kpNameMap = $this->buildKnowledgePointNameMap();
         $kpNameMap = $this->buildKnowledgePointNameMap();
         Log::info('ExamPdfExportService: 获取知识点名称映射', [
         Log::info('ExamPdfExportService: 获取知识点名称映射', [
             'kpNameMap_count' => count($kpNameMap),
             'kpNameMap_count' => count($kpNameMap),
-            'kpNameMap_keys_sample' => ! empty($kpNameMap) ? array_slice(array_keys($kpNameMap), 0, 5) : [],
         ]);
         ]);
 
 
         // 【修复】直接从MySQL数据库获取题目详情(不通过API)
         // 【修复】直接从MySQL数据库获取题目详情(不通过API)
@@ -2464,9 +2714,9 @@ class ExamPdfExportService
         Log::info('ExamPdfExportService: buildMasterySummary返回结果', [
         Log::info('ExamPdfExportService: buildMasterySummary返回结果', [
             'masteryData_count' => count($masteryData),
             'masteryData_count' => count($masteryData),
             'kpNameMap_count' => count($kpNameMap),
             'kpNameMap_count' => count($kpNameMap),
-            'masterySummary_keys' => array_keys($masterySummary),
             'masterySummary_items_count' => count($masterySummary['items'] ?? []),
             'masterySummary_items_count' => count($masterySummary['items'] ?? []),
-            'masterySummary_items_sample' => ! empty($masterySummary['items']) ? array_slice($masterySummary['items'], 0, 2) : [],
+            'masterySummary_avg' => $masterySummary['average'] ?? null,
+            'masterySummary_weak_count' => count($masterySummary['weak_list'] ?? []),
         ]);
         ]);
 
 
         // 构建当前学生掌握度映射,供父子影响分析展示使用
         // 构建当前学生掌握度映射,供父子影响分析展示使用
@@ -2500,7 +2750,6 @@ class ExamPdfExportService
         Log::info('ExamPdfExportService: 处理后的父节点掌握度', [
         Log::info('ExamPdfExportService: 处理后的父节点掌握度', [
             'raw_count' => count($parentMasteryLevels),
             'raw_count' => count($parentMasteryLevels),
             'processed_count' => count($processedParentMastery),
             'processed_count' => count($processedParentMastery),
-            'processed_sample' => ! empty($processedParentMastery) ? array_slice($processedParentMastery, 0, 3) : [],
         ]);
         ]);
 
 
         return [
         return [