@php
// 提取15位paper_id数字部分作为学案编号
$rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
$reportCode = $matches[1] ?? preg_replace('/[^0-9]/', '', $rawPaperId);
$averageMastery = isset($mastery['average']) ? number_format($mastery['average'] * 100, 1) . '%' : '无数据';
// 【修复】从insights中获取AI分析结果(而不是从analysis_data)
$questionAnalysis = $insights ?? [];
// 生成时间(格式:2026年01月30日 15:04:05)
$generateDateTime = now()->format('Y年m月d日 H:i:s');
@endphp
本次命中子知识点掌握度
@php
// 严格口径:顶部列表来自“本卷题目关联知识点”
$hitCodes = array_values(array_unique(array_filter($exam_hit_kp_codes ?? [])));
$masteryByCode = [];
$hitMasteryFromParents = [];
$questionKpNameMap = [];
foreach (($questions ?? []) as $q) {
$code = trim((string) ($q['knowledge_point'] ?? ''));
$name = trim((string) ($q['knowledge_point_name'] ?? ''));
if ($code !== '' && $name !== '') {
$questionKpNameMap[$code] = $name;
}
}
// 优先使用父节点树里的命中子节点掌握度(与左侧树口径一致)
foreach (($parent_mastery_levels ?? []) as $parent) {
$childrenAll = $parent['children_all'] ?? [];
if (!is_array($childrenAll)) {
continue;
}
foreach ($childrenAll as $child) {
$cCode = trim((string) ($child['kp_code'] ?? ''));
if ($cCode === '' || empty($child['is_hit'])) {
continue;
}
$hitMasteryFromParents[$cCode] = floatval($child['mastery_level'] ?? 0);
}
}
foreach (($mastery['items'] ?? []) as $item) {
$code = (string) ($item['kp_code'] ?? '');
if ($code !== '') {
$masteryByCode[$code] = $item;
}
}
$filteredMasteryItems = [];
foreach ($hitCodes as $kpCode) {
if (in_array($kpCode, ['K-GENERAL', 'GENERAL', 'DEFAULT'])) {
continue;
}
$base = $masteryByCode[$kpCode] ?? null;
$levelFromParentTree = $hitMasteryFromParents[$kpCode] ?? null;
$resolvedLevel = $levelFromParentTree !== null
? floatval($levelFromParentTree)
: floatval($base['mastery_level'] ?? 0);
$resolvedChange = $base['mastery_change'] ?? null;
// 当顶部使用了父节点树中的实时值且与旧值不一致时,避免显示旧变化值误导
if ($levelFromParentTree !== null && $base !== null) {
$oldLevel = floatval($base['mastery_level'] ?? 0);
if (abs($resolvedLevel - $oldLevel) > 0.0001) {
$resolvedChange = null;
}
}
$filteredMasteryItems[] = [
'kp_code' => $kpCode,
'kp_name' => $questionKpNameMap[$kpCode] ?? ($base['kp_name'] ?? $kpCode),
'mastery_level' => $resolvedLevel,
'mastery_change' => $resolvedChange,
'is_hit' => true,
];
}
@endphp
@if(!empty($filteredMasteryItems))
@foreach($filteredMasteryItems as $item)
@php
$pct = min(100, max(0, ($item['mastery_level'] ?? 0) * 100));
$barColor = $pct >= 80 ? '#10b981' : ($pct >= 60 ? '#f59e0b' : '#ef4444');
$delta = $item['mastery_change'] ?? null;
$isHitItem = !empty($item['is_hit']);
// 只有当有变化值时才显示变化信息
$changeText = '';
if ($delta !== null && $delta !== '' && abs($delta) > 0.001) {
$changeText = ($delta > 0 ? '↑ ' : '↓ ') . number_format(abs($delta) * 100, 1) . '%';
}
@endphp
{{ $item['kp_name'] ?? $item['kp_code'] ?? '未知知识点' }}
{{ number_format($pct, 1) }}%
{{ $changeText ? '子节点变化 ' . $changeText : '' }}
@endforeach
@else
暂无有效掌握度数据(已过滤通用知识点)
@endif
📊 知识点层级掌握度分析
@php
$parentMasteryLevels = $parent_mastery_levels ?? [];
$hasParentMastery = !empty($parentMasteryLevels);
$childMasteryMap = [];
foreach (($filteredMasteryItems ?? []) as $it) {
$childMasteryMap[$it['kp_code']] = [
'level' => floatval($it['mastery_level'] ?? 0),
'delta' => isset($it['mastery_change']) ? floatval($it['mastery_change']) : null,
];
}
@endphp
@if($hasParentMastery)
父子知识点关系
@foreach($parentMasteryLevels as $parentData)
@php
$childrenAll = $parentData['children_all'] ?? [];
$children = $parentData['children'] ?? []; // 命中子节点
$childCount = count($childrenAll);
$hitCount = count($children);
$hitAvg = $parentData['children_hit_avg_mastery'] ?? null;
$parentPct = number_format(floatval($parentData['mastery_percentage'] ?? 0), 1);
$parentLevel = floatval($parentData['mastery_level'] ?? 0);
$parentClass = $parentLevel >= 0.8 ? 'high' : ($parentLevel >= 0.6 ? 'mid' : 'low');
$delta = $parentData['mastery_change'] ?? null;
$hitNames = array_values(array_map(fn($c) => $c['kp_name'] ?? ($c['kp_code'] ?? ''), $children));
$allChildrenPerfect = !empty($childrenAll) && count(array_filter($childrenAll, function ($c) {
return floatval($c['mastery_level'] ?? 0) >= 0.999;
})) === count($childrenAll);
@endphp
{{ $parentData['kp_name'] ?? $parentData['kp_code'] }}
@if(!empty($childrenAll))
@foreach($childrenAll as $child)
@php
$isHit = !empty($child['is_hit']);
$m = floatval($child['mastery_level'] ?? 0);
$badgeClass = $m >= 0.8 ? 'high' : ($m >= 0.6 ? 'mid' : ($m > 0 ? 'low' : 'miss'));
$badgeText = number_format($m * 100, 1) . '%';
@endphp
└─ {{ $child['kp_name'] }}
{{ $badgeText }}
@endforeach
@else
无命中子知识点
@endif
{{ $parentData['kp_name'] ?? $parentData['kp_code'] }}
{{ $parentPct }}%
父节点变化:
@if($delta !== null)
{{ $delta >= 0 ? '↑ ' : '↓ ' }}{{ number_format(abs($delta) * 100, 1) }}%
@else
-
@endif
本次重点子节点:
@if($hitCount > 0)
@foreach($hitNames as $hitName)
@if(!empty(trim($hitName)))
{{ $hitName }}
@endif
@endforeach
@else
无
@endif
子节点总数 {{ $childCount }} 个,本次命中 {{ $hitCount }} 个,命中均值 {{ $hitAvg !== null ? number_format(floatval($hitAvg) * 100, 1) . '%' : '-' }}(命中表示本次覆盖,不等于已掌握)
@endforeach
@php
$allParentPerfect = !empty($parentMasteryLevels) && count(array_filter($parentMasteryLevels, function ($p) {
return floatval($p['mastery_level'] ?? 0) >= 0.999;
})) === count($parentMasteryLevels);
$allHitChildrenPerfect = !empty($filteredMasteryItems) && count(array_filter($filteredMasteryItems, function ($it) {
return floatval($it['mastery_level'] ?? 0) >= 0.999;
})) === count($filteredMasteryItems);
$isAllPerfect = $allParentPerfect && $allHitChildrenPerfect;
// 1) 优先:本次命中子知识点中的低掌握度(<60%)
$hitWeakChildren = [];
$hitWeakKeys = [];
foreach (($filteredMasteryItems ?? []) as $hitItem) {
$level = floatval($hitItem['mastery_level'] ?? 0);
$name = trim((string) ($hitItem['kp_name'] ?? $hitItem['kp_code'] ?? ''));
if ($name === '' || $level >= 0.6) {
continue;
}
$key = (string) ($hitItem['kp_code'] ?? $name);
$hitWeakChildren[$key] = [
'name' => $name,
'level' => $level,
];
$hitWeakKeys[$key] = true;
}
$hitWeakChildren = array_values($hitWeakChildren);
usort($hitWeakChildren, function ($a, $b) {
return $a['level'] <=> $b['level'];
});
// 2) 兜底:若命中子知识点都 >=60%,再从其他低掌握度子知识点补
$otherWeakChildren = [];
foreach (($parentMasteryLevels ?? []) as $pData) {
foreach (($pData['children_all'] ?? []) as $child) {
$level = floatval($child['mastery_level'] ?? 0);
$name = trim((string) ($child['kp_name'] ?? $child['kp_code'] ?? ''));
if ($name === '' || $level >= 0.6) {
continue;
}
$key = ($child['kp_code'] ?? $name);
if (isset($hitWeakKeys[$key])) {
continue;
}
$otherWeakChildren[$key] = [
'name' => $name,
'level' => $level,
];
}
}
$otherWeakChildren = array_values($otherWeakChildren);
usort($otherWeakChildren, function ($a, $b) {
return $a['level'] <=> $b['level'];
});
// 3) 最终展示:总数不超过5
$globalWeakChildren = [];
if (!empty($hitWeakChildren)) {
$globalWeakChildren = array_slice($hitWeakChildren, 0, 5);
} else {
$globalWeakChildren = array_slice($otherWeakChildren, 0, 5);
}
@endphp
学习建议:
@if($isAllPerfect)
本次学案表现非常出色,相关知识点掌握稳定且完整。建议继续进入新的知识点专题学习,优先选择同层级未覆盖内容或更高难度综合题,保持进阶节奏。
@else
建议重点关注掌握度较低的知识点,通过专项练习提升整体学习水平。优先练习掌握度低于60%的知识点。
@endif
@if(!empty($globalWeakChildren))
@foreach($globalWeakChildren as $weakChild)
{{ $weakChild['name'] }}({{ number_format($weakChild['level'] * 100, 1) }}%)
@endforeach
@endif
@else
暂无父节点掌握度数据
当前分析主要基于具体知识点掌握度。完整的层级掌握度分析需要在系统中建立完整的知识点层级关系。
@endif
@php
$insightMap = [];
foreach (($question_insights ?? []) as $insight) {
$no = $insight['question_number'] ?? $insight['question_id'] ?? null;
if ($no !== null) {
$insightMap[$no] = $insight;
}
}
$analysisWrongMap = [];
foreach (($analysis_data['question_analysis'] ?? []) as $qa) {
$qid = $qa['question_bank_id'] ?? $qa['question_id'] ?? null;
if ($qid === null || $qid === '') {
continue;
}
$rawCorrect = $qa['is_correct'] ?? null;
$isWrongFromAnalysis = false;
if (is_array($rawCorrect)) {
$isWrongFromAnalysis = in_array(0, $rawCorrect, true);
} elseif ($rawCorrect !== null) {
$isWrongFromAnalysis = !boolval($rawCorrect);
}
if ($isWrongFromAnalysis) {
$analysisWrongMap[(string)$qid] = true;
}
}
$wrongQuestions = [];
foreach (($questions ?? []) as $qItem) {
$studentAnswerProbe = $qItem['student_answer'] ?? null;
$correctAnswerProbe = $qItem['answer'] ?? ($qItem['correct_answer'] ?? null);
$isCorrectProbe = $qItem['is_correct'] ?? null;
if ($isCorrectProbe === null && !empty($studentAnswerProbe) && !empty($correctAnswerProbe)) {
$isCorrectProbe = (trim((string)$studentAnswerProbe) === trim((string)$correctAnswerProbe)) ? 1 : 0;
}
$normalizedCorrect = $isCorrectProbe;
if ($isCorrectProbe !== null) {
$normalizedCorrect = is_bool($isCorrectProbe) ? ($isCorrectProbe ? 1 : 0) : intval($isCorrectProbe);
}
$qidProbe = (string)($qItem['question_bank_id'] ?? $qItem['question_id'] ?? '');
$isWrongByAnalysis = ($qidProbe !== '' && isset($analysisWrongMap[$qidProbe]));
if ($normalizedCorrect === 0 || $isWrongByAnalysis) {
$wrongQuestions[] = $qItem;
}
}
// 错题知识点聚合统计(同知识点错几题/共几题)
$kpStats = [];
foreach (($questions ?? []) as $qItem) {
$kpName = trim((string)($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
if ($kpName === '') {
$kpName = '未标注知识点';
}
if (!isset($kpStats[$kpName])) {
$kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
}
$kpStats[$kpName]['total']++;
}
foreach ($wrongQuestions as $qItem) {
$kpName = trim((string)($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
if ($kpName === '') {
$kpName = '未标注知识点';
}
if (!isset($kpStats[$kpName])) {
$kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
}
$kpStats[$kpName]['wrong']++;
}
$kpWrongStats = [];
foreach ($kpStats as $kpName => $stat) {
if (($stat['wrong'] ?? 0) <= 0) {
continue;
}
$total = max(1, intval($stat['total'] ?? 0));
$wrong = intval($stat['wrong'] ?? 0);
$kpWrongStats[] = [
'kp_name' => $kpName,
'wrong' => $wrong,
'total' => $total,
'rate' => $wrong / $total,
];
}
usort($kpWrongStats, function ($a, $b) {
if ($a['rate'] === $b['rate']) {
return $b['wrong'] <=> $a['wrong'];
}
return $b['rate'] <=> $a['rate'];
});
@endphp
@if(!empty($wrongQuestions))
错题列表
@if(!empty($kpWrongStats))
知识点错误率
@foreach($kpWrongStats as $item)
{{ $item['kp_name'] }}:{{ $item['wrong'] }}/{{ $item['total'] }}({{ number_format($item['rate'] * 100, 1) }}%)
@endforeach
@endif
@foreach($wrongQuestions as $q)
@php
// 【修复】从题目数据中获取学生答案、正确答案和判分结果
$studentAnswer = $q['student_answer'] ?? $q['student_answer'] ?? null;
$correctAnswer = $q['answer'] ?? $q['correct_answer'] ?? null;
$isCorrect = $q['is_correct'] ?? null; // 1=正确,0=错误,null=未判
// 如果未判分但有学生答案和正确答案,自动比较判断
if ($isCorrect === null && !empty($studentAnswer) && !empty($correctAnswer)) {
$isCorrect = (trim($studentAnswer) === trim($correctAnswer)) ? 1 : 0;
}
// 判分状态显示逻辑
$statusText = '';
$statusColor = '';
if ($isCorrect === 1) {
$statusText = '正确';
$statusColor = '#10b981';
} elseif ($isCorrect === 0) {
$statusText = '错误';
$statusColor = '#ef4444';
}
$showStatus = $statusText !== '';
$insight = $insightMap[$q['question_number']] ?? ($insightMap[$q['display_number'] ?? null] ?? []);
// 【修复】得分显示:答错显示实际得分,答对显示满分
$fullScore = $insight['full_score'] ?? ($q['score'] ?? null);
if ($isCorrect === 1) {
// 答对了,显示满分
$score = $fullScore;
} elseif ($isCorrect === 0) {
// 答错了,显示实际得分(可能为0分或其他分数)
$score = $q['score_obtained'] ?? 0;
} else {
// 未判分或未答题,不显示得分
$score = null;
}
$analysisRaw = $insight['analysis']
?? $insight['thinking_process']
?? $insight['feedback']
?? $insight['suggestions']
?? $insight['reason']
?? ($insight['correct_solution'] ?? null);
// 若有下一步建议,追加
if (empty($analysisRaw) && !empty($insight['next_steps'])) {
$analysisRaw = '后续建议:' . (is_array($insight['next_steps']) ? implode(';', $insight['next_steps']) : $insight['next_steps']);
}
$analysis = is_array($analysisRaw) ? json_encode($analysisRaw, JSON_UNESCAPED_UNICODE) : $analysisRaw;
if ($analysis === null || $analysis === '') {
$analysis = '暂无解题思路,待补充';
}
if (is_string($analysis)) {
$analysis = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $analysis);
}
$formatSolutionLikeGrading = function ($text) {
if (!is_string($text) || trim($text) === '') {
return $text;
}
$normalized = preg_replace('/\s*;\s*步骤\s*(\d+)/u', ";\n步骤$1", $text);
$normalized = preg_replace('/\s*。\s*步骤\s*(\d+)/u', "。\n步骤$1", $normalized);
$normalized = preg_replace('/(? '选择题', 'fill' => '填空题', 'answer' => '解答题'];
$typeLabel = $typeMap[$q['question_type'] ?? ''] ?? ($q['question_type'] ?? '题型未标注');
$questionText = is_string($q['question_text']) ? $q['question_text'] : json_encode($q['question_text'], JSON_UNESCAPED_UNICODE);
$solution = $q['solution'] ?? null;
if (is_string($solution)) {
$solution = preg_replace('/^【?\s*解题思路\s*】?\s*[::]?\s*/u', '', $solution);
}
$solution = $formatSolutionLikeGrading($solution);
$analysis = $formatSolutionLikeGrading($analysis);
// 对齐判卷渲染口径:直接走 MathFormulaProcessor,保留图片与公式标签
$renderLikeGrading = function ($text) {
if (is_array($text)) {
$text = json_encode($text, JSON_UNESCAPED_UNICODE);
}
$text = is_string($text) ? trim($text) : '';
if ($text === '') {
return '';
}
return \App\Services\MathFormulaProcessor::processFormulas($text);
};
// 选择题:选项来自服务端 questions.options / 题干解析;正确答案字母用于打 ✅
$displayCorrectAnswer = is_array($correctAnswer)
? json_encode($correctAnswer, JSON_UNESCAPED_UNICODE)
: (string) $correctAnswer;
$questionTypeRaw = strtolower(trim((string) ($q['question_type'] ?? '')));
$isChoiceQuestion = in_array($questionTypeRaw, ['choice', 'multiple_choice', 'single_choice', '选择题', 'select'], true);
$normalizedOptions = [];
$correctAnswerLetters = [];
if ($isChoiceQuestion) {
$rawOptions = $q['options'] ?? [];
if (is_string($rawOptions)) {
$decodedOptions = json_decode($rawOptions, true);
$rawOptions = is_array($decodedOptions) ? $decodedOptions : [];
}
if (is_array($rawOptions)) {
foreach ($rawOptions as $optKey => $optValue) {
$letter = null;
if (is_string($optKey) && preg_match('/([A-H])/i', $optKey, $m)) {
$letter = strtoupper($m[1]);
} elseif (is_array($optValue)) {
$candidate = $optValue['label'] ?? $optValue['key'] ?? $optValue['option'] ?? null;
if (is_string($candidate) && preg_match('/([A-H])/i', $candidate, $m)) {
$letter = strtoupper($m[1]);
}
}
if ($letter === null) {
continue;
}
$content = is_array($optValue)
? ($optValue['content'] ?? $optValue['text'] ?? $optValue['value'] ?? '')
: $optValue;
if (!is_string($content)) {
$content = json_encode($content, JSON_UNESCAPED_UNICODE);
}
$content = trim((string) $content);
if ($content !== '') {
$normalizedOptions[$letter] = $content;
}
}
}
if (trim((string) $correctAnswer) !== '') {
preg_match_all('/[A-H]/i', strtoupper((string) $correctAnswer), $answerMatches);
$correctAnswerLetters = array_values(array_unique($answerMatches[0] ?? []));
}
if (!empty($normalizedOptions) && !empty($correctAnswerLetters)) {
$mappedAnswers = [];
foreach ($correctAnswerLetters as $letter) {
if (isset($normalizedOptions[$letter])) {
$mappedAnswers[] = $letter . '. ' . $normalizedOptions[$letter];
}
}
if (!empty($mappedAnswers)) {
$displayCorrectAnswer = implode(';', $mappedAnswers);
}
}
}
$choiceOptionLetters = !empty($normalizedOptions) ? array_keys($normalizedOptions) : [];
sort($choiceOptionLetters);
$choiceLayoutClass = 'options-grid-1';
$layoutDecider = app(\App\Support\OptionLayoutDecider::class);
if (! empty($normalizedOptions) && ! empty($choiceOptionLetters)) {
$optValuesForLayout = [];
foreach ($choiceOptionLetters as $L) {
$optValuesForLayout[] = $normalizedOptions[$L];
}
$layoutMeta = $layoutDecider->decide($optValuesForLayout, 'grading');
$choiceLayoutClass = $layoutMeta['class'] ?? 'options-grid-1';
}
@endphp
题号 {{ $q['display_number'] ?? $q['question_number'] }} · {{ $typeLabel }}
@php
$kpName = $q['knowledge_point_name'] ?? $q['knowledge_point'] ?? null;
if (!empty($kpName) && $kpName !== '-' && $kpName !== '未标注') {
echo '' . e($kpName) . '';
}
@endphp
@if($showStatus)
{{ $statusText }}
@endif
@if($score !== null && $fullScore !== null)
得分 {{ $score }} / {{ $fullScore }}
@endif
{{-- 题干:数据已在 ExamPdfExportService 中经 processQuestionData(含
→
、公式);样式对齐判卷 question-stem --}}
{!! $questionText !!}
{{-- 选择题:与判卷页相同网格布局(OptionLayoutDecider),正确项旁 ✅ --}}
@if(!empty($isChoiceQuestion) && !empty($normalizedOptions))
@foreach($choiceOptionLetters as $optLetter)
@php
$isCorrectOpt = in_array($optLetter, $correctAnswerLetters ?? [], true);
$rawOpt = (string) ($normalizedOptions[$optLetter] ?? '');
$normalizedOpt = str_replace('\\dfrac', '\\frac', $rawOpt);
$normalizedOpt = str_replace('\\displaystyle', '', $normalizedOpt);
$normalizedOpt = $layoutDecider->normalizeCompactMathForDisplay($normalizedOpt);
$rawOptPlain = html_entity_decode(strip_tags($rawOpt), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$rawOptPlain = preg_replace('/\s+/u', '', $rawOptPlain ?? '');
$isShortOption = mb_strlen((string) $rawOptPlain, 'UTF-8') <= 8;
$valClass = $isShortOption ? 'option-short' : 'option-long';
$renderedOpt = $renderLikeGrading($normalizedOpt);
@endphp
{{ $optLetter }}.
{!! $renderedOpt !!}
@if($isCorrectOpt)
✅
@endif
@endforeach
@endif
{{-- 非选择题,或选择题未能得到选项列表时:显示「正确答案」文字框 --}}
@if(!empty($correctAnswer) && (!$isChoiceQuestion || empty($normalizedOptions)))
正确答案
{!! $renderLikeGrading($displayCorrectAnswer) !!}
@endif
{{-- 解题思路:判卷页 answer-meta 口径(单行标题+正文,避免大块色条占行) --}}
@if(!empty($solution))
@elseif(!empty($analysis) && $analysis !== '暂无解题思路记录')
@endif
{{-- 解题步骤(如果有) --}}
@if(!empty($steps))
解题步骤
@foreach($steps as $s)
- {!! nl2br(e(is_array($s) ? json_encode($s, JSON_UNESCAPED_UNICODE) : $s)) !!}
@endforeach
@endif
@endforeach
@endif
{{-- 闭合page div --}}