@php
$v3 = $v3 ?? [];
$summary = $v3['summary'] ?? [];
$radar = $v3['radar'] ?? [];
$modules = $v3['modules'] ?? [];
$paths = $v3['paths'] ?? ['keep' => [], 'boost' => [], 'key' => []];
$overallPlan = $v3['overall_plan'] ?? [];
$rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
$reportCode = $matches[1] ?? preg_replace('/[^0-9]/', '', (string) $rawPaperId);
$generateDateTime = now()->format('Y年m月d日 H:i:s');
$scoreObtained = $summary['score_obtained'] ?? null;
$scoreTotal = $summary['score_total'] ?? null;
$scoreRate = $summary['score_rate'] ?? null;
$averageMastery = $summary['average_mastery'] ?? null;
$overallLabel = $summary['overall_label'] ?? '待评估';
$difficultySummary = $summary['difficulty'] ?? [];
$comparisonSummary = $summary['comparison'] ?? [];
$overallLabelDetail = $summary['overall_label_detail'] ?? [];
$historySummary = $comparisonSummary['history'] ?? [];
$peerSummary = $comparisonSummary['peers'] ?? [];
$overallScore = isset($overallLabelDetail['composite_score']) ? (float) $overallLabelDetail['composite_score'] : null;
$overallGrade = (string) ($overallLabelDetail['grade'] ?? 'D');
$currentPart = (float) ($overallLabelDetail['current_score'] ?? 0);
$historyPart = (float) ($overallLabelDetail['history_score'] ?? 0);
$peerPart = (float) ($overallLabelDetail['peer_score'] ?? 0);
$adjustPart = (float) ($overallLabelDetail['difficulty_adjust'] ?? 0);
$compositeFormulaResult = (0.50 * $currentPart) + (0.25 * $historyPart) + (0.25 * $peerPart) + $adjustPart;
$overallBadge = function (string $grade): array {
return match ($grade) {
'S' => ['bg' => '#f5f3ff', 'border' => '#6d28d9', 'text' => '#6d28d9', 'class' => 'badge-s'],
'A' => ['bg' => '#ecfdf3', 'border' => '#22c55e', 'text' => '#166534', 'class' => 'badge-excellent'],
'B' => ['bg' => '#eff6ff', 'border' => '#3b82f6', 'text' => '#1d4ed8', 'class' => 'badge-good'],
'C' => ['bg' => '#fff7ed', 'border' => '#f59e0b', 'text' => '#b45309', 'class' => 'badge-average'],
default => ['bg' => '#fef2f2', 'border' => '#ef4444', 'text' => '#b91c1c', 'class' => 'badge-weak'],
};
};
$overallVisual = $overallBadge((string) $overallGrade);
$trendVisual = function (string $trend): array {
return match ($trend) {
'显著提升' => ['icon' => '▲', 'color' => '#16a34a'],
'小幅提升' => ['icon' => '↗', 'color' => '#0ea5e9'],
'基本持平' => ['icon' => '•', 'color' => '#64748b'],
'小幅回落' => ['icon' => '↘', 'color' => '#f59e0b'],
'明显回落' => ['icon' => '▼', 'color' => '#ef4444'],
default => ['icon' => '•', 'color' => '#64748b'],
};
};
$statusColor = function (string $status): string {
return match ($status) {
'良好' => '#16a34a',
'一般' => '#f97316',
'薄弱' => '#e11d48',
default => '#64748b',
};
};
$n = max(1, count($radar));
$cx = 210;
$cy = 155;
$r = 108;
$outer = [];
$inner = [];
for ($i = 0; $i < $n; $i++) {
$angle = -M_PI / 2 + (2 * M_PI * $i / $n);
$ox = $cx + $r * cos($angle);
$oy = $cy + $r * sin($angle);
$outer[] = [$ox, $oy];
$value = isset($radar[$i]['value']) ? (float) $radar[$i]['value'] : 0.0;
$ratio = max(0.0, min(1.0, $value / 5));
$ix = $cx + $r * $ratio * cos($angle);
$iy = $cy + $r * $ratio * sin($angle);
$inner[] = [$ix, $iy];
}
$outerPoints = implode(' ', array_map(fn ($p) => round($p[0], 2).','.round($p[1], 2), $outer));
$innerPoints = implode(' ', array_map(fn ($p) => round($p[0], 2).','.round($p[1], 2), $inner));
$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) {
$isCorrectProbe = $qItem['is_correct'] ?? null;
$studentAnswerProbe = $qItem['student_answer'] ?? null;
$correctAnswerProbe = $qItem['answer'] ?? ($qItem['correct_answer'] ?? 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'] ?? '未标注知识点'));
$kpName = $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'] ?? '未标注知识点'));
$kpName = $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($scoreObtained !== null && $scoreTotal !== null && $scoreTotal > 0)
{{ rtrim(rtrim(number_format((float) $scoreObtained, 1), '0'), '.') }}/{{ rtrim(rtrim(number_format((float) $scoreTotal, 1), '0'), '.') }}
@else
暂无得分数据
@endif
- 得分率:{{ $scoreRate !== null ? number_format((float) $scoreRate * 100, 1) . '%' : '暂无得分率' }}
- 平均掌握度:{{ $averageMastery !== null ? number_format((float) $averageMastery * 100, 1) . '%' : '暂无掌握度' }}
-
难度匹配:
@if(!empty($difficultySummary['target_label']) && isset($difficultySummary['actual_average_difficulty']))
目标 {{ $difficultySummary['target_label'] }}
@if(!empty($difficultySummary['target_range']))
({{ number_format((float)($difficultySummary['target_range']['min'] ?? 0), 2) }}~{{ number_format((float)($difficultySummary['target_range']['max'] ?? 0), 2) }})
@endif
,实际 {{ number_format((float)($difficultySummary['actual_average_difficulty'] ?? 0), 3) }}
({{ $difficultySummary['status'] ?? '暂无' }})
@else
暂无难度匹配数据
@endif
@if(!empty($difficultySummary['explain']))
- 难度说明:{{ $difficultySummary['explain'] }}
@endif
-
与历史自己对比:
@if(!empty($historySummary['is_first_exam']))
{{ $historySummary['message'] ?? '这是你的第一次分析报告,先积累样本再看趋势。' }}
@elseif(!empty($historySummary['low_baseline_guard']))
{{ $historySummary['message'] ?? '历史基线偏低,建议看连续趋势。' }}
@elseif(!empty($historySummary['has_data']))
@php
$trendText = (string)($historySummary['trend'] ?? '—');
$tVisual = $trendVisual($trendText);
@endphp
近几次均值对比:
{{ number_format((float)($historySummary['baseline_score_rate'] ?? 0) * 100, 1) }}%,
本次{{ ($historySummary['delta_score_rate'] ?? 0) >= 0 ? '提升' : '回落' }}
{{ number_format(abs((float)($historySummary['delta_score_rate'] ?? 0)) * 100, 1) }}%
({{ $tVisual['icon'] ?? '•' }} {{ $trendText }})
@else
{{ $historySummary['message'] ?? '历史样本不足' }}
@endif
@if(!empty($peerSummary['show_line']))
-
与同群体对比:
{{ $peerSummary['message'] ?? '' }}
({{ $peerSummary['band_icon'] ?? '•' }} {{ $peerSummary['band'] ?? '—' }})
@endif
-
整体水平:
@if($overallScore !== null)
{{ number_format($overallScore, 1) }} 分({{ $overallGrade }})
@else
待计算
@endif
规则:综合分 = 当前50% + 历史25% + 同群体25% + 难度校正,即:(({{ number_format($scoreRate !== null ? (float)$scoreRate * 100 : 0, 1) }}×70% + {{ number_format($averageMastery !== null ? (float)$averageMastery * 100 : 0, 1) }}×30%)×50%) + {{ number_format($historyPart, 1) }}×25% + {{ number_format($peerPart, 1) }}×25% + {{ number_format($adjustPart, 1) }} = {{ number_format($overallScore ?? $compositeFormulaResult, 1) }}
二、知识点掌握雷达图
良好(4.0-5.0)
一般(2.0-3.9)
薄弱(0-1.9)
未涉及
子知识点
子知识点变化
外圈越远表示层级越深
雷达图解读
@foreach($radar as $item)
@php $color = $statusColor((string) ($item['status'] ?? '暂无')); @endphp
{{ $item['name'] }}:{{ $item['status'] }}
({{ !empty($item['has_mastery']) ? number_format((float) ($item['value'] ?? 0), 2) . '/5' : '—' }})
@if(!empty($item['children']))
,子知识点 {{ count($item['children']) }} 个
@endif
@endforeach
三、模块能力分析表
| 模块 |
掌握分值 |
掌握状态 |
样本数 |
得分率 |
学生当前能力 |
@foreach($modules as $m)
@php
$status = (string) ($m['status'] ?? '暂无');
$color = $statusColor($status);
$rate = $m['exam_score_rate'] ?? null;
@endphp
| {{ $m['module_name'] ?? '-' }} |
{{ $m['mastery_score_5'] !== null ? number_format((float) $m['mastery_score_5'], 2) . '/5' : '-' }} |
{{ $status }} |
{{ $m['kp_count'] ?? 0 }} |
{{ $rate !== null ? number_format((float) $rate * 100, 1) . '%' : '-' }} |
{{ $m['ability_text'] ?? '-' }} |
@endforeach
四、分模块提分路径
保分模块(保持优势)
@foreach(($paths['keep'] ?? []) as $item)
- {{ $item['name'] }}:掌握度 {{ number_format((float) ($item['mastery_level'] ?? 0) * 100, 1) }}%
@endforeach
@if(empty($paths['keep']))
暂无数据
@endif
涨分模块(重点突破)
@foreach(($paths['boost'] ?? []) as $item)
- {{ $item['name'] }}:掌握度 {{ number_format((float) ($item['mastery_level'] ?? 0) * 100, 1) }}%
@endforeach
@if(empty($paths['boost']))
暂无数据
@endif
提分关键(优先补短)
@foreach(($paths['key'] ?? []) as $item)
- {{ $item['name'] }}:掌握度 {{ number_format((float) ($item['mastery_level'] ?? 0) * 100, 1) }}%
@endforeach
@if(empty($paths['key']))
暂无数据
@endif
五、整体提升方案
@foreach($overallPlan as $line)
- {{ $line }}
@endforeach
@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'] ?? null;
$correctAnswer = $q['answer'] ?? $q['correct_answer'] ?? null;
$isCorrect = $q['is_correct'] ?? null;
if ($isCorrect === null && !empty($studentAnswer) && !empty($correctAnswer)) {
$isCorrect = (trim($studentAnswer) === trim($correctAnswer)) ? 1 : 0;
}
$statusText = '';
$statusColorValue = '';
if ($isCorrect === 1) {
$statusText = '正确';
$statusColorValue = '#10b981';
} elseif ($isCorrect === 0) {
$statusText = '错误';
$statusColorValue = '#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) {
$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);
$renderLikeGrading = function ($text) {
if (is_array($text)) {
$text = json_encode($text, JSON_UNESCAPED_UNICODE);
}
$text = is_string($text) ? trim($text) : '';
if ($text === '') {
return '';
}
// 兼容题库里常见的转义写法:\$x\$、\$\frac{...}\$
$text = preg_replace('/\\\\\\$/u', '$', $text);
return \App\Services\MathFormulaProcessor::processFormulas($text);
};
$questionTextRendered = $renderLikeGrading($questionText);
$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
{!! $questionTextRendered !!}
@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
@if(!empty($solution))
@elseif(!empty($analysis) && $analysis !== '暂无解题思路记录')
@endif
@if(!empty($steps))
解题步骤
@foreach($steps as $s)
@php
$stepText = is_array($s) ? json_encode($s, JSON_UNESCAPED_UNICODE) : (string) $s;
@endphp
- {!! nl2br($renderLikeGrading($stepText)) !!}
@endforeach
@endif
@endforeach
@endif