|
@@ -818,14 +818,16 @@ class ExamPdfExportService
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $explain = '暂无足够数据评估难度匹配。';
|
|
|
|
|
- if ($matchStatus === '匹配') {
|
|
|
|
|
- $explain = '本次题目整体难度与学案目标难度基本一致,结果可直接反映当前掌握水平。';
|
|
|
|
|
- } elseif ($matchStatus === '偏难') {
|
|
|
|
|
- $explain = '本次题目整体偏难,错误率偏高有客观因素,建议先补齐同模块中档题再冲高档。';
|
|
|
|
|
- } elseif ($matchStatus === '偏易') {
|
|
|
|
|
- $explain = '本次题目整体偏易,若得分高不代表上限已到,建议补充更高一档难度验证稳定性。';
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $scoreRate = $this->resolveCurrentScoreRateForDifficultyInsight($templateData);
|
|
|
|
|
+ $scoreBand = $this->resolveScoreRateBandForDifficultyInsight($scoreRate);
|
|
|
|
|
+ $seed = sprintf(
|
|
|
|
|
+ '%s|%s|%s|%s',
|
|
|
|
|
+ (string) ($paper['id'] ?? ''),
|
|
|
|
|
+ (string) (($templateData['student'] ?? [])['id'] ?? ''),
|
|
|
|
|
+ $matchStatus,
|
|
|
|
|
+ $scoreBand
|
|
|
|
|
+ );
|
|
|
|
|
+ $explain = $this->buildDifficultyExplainByContext($matchStatus, $scoreBand, $seed);
|
|
|
|
|
|
|
|
return [
|
|
return [
|
|
|
'target_category_raw' => $rawCategory,
|
|
'target_category_raw' => $rawCategory,
|
|
@@ -838,10 +840,90 @@ class ExamPdfExportService
|
|
|
'deviation' => $deviation !== null ? round($deviation, 4) : null,
|
|
'deviation' => $deviation !== null ? round($deviation, 4) : null,
|
|
|
'status' => $matchStatus,
|
|
'status' => $matchStatus,
|
|
|
'question_count' => count($difficulties),
|
|
'question_count' => count($difficulties),
|
|
|
|
|
+ 'score_rate' => $scoreRate !== null ? round($scoreRate, 4) : null,
|
|
|
|
|
+ 'score_band' => $scoreBand,
|
|
|
'explain' => $explain,
|
|
'explain' => $explain,
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private function resolveCurrentScoreRateForDifficultyInsight(array $templateData): ?float
|
|
|
|
|
+ {
|
|
|
|
|
+ $rawOverall = (array) (($templateData['analysis_data'] ?? [])['overall_summary'] ?? []);
|
|
|
|
|
+ if (isset($rawOverall['score_rate']) && is_numeric($rawOverall['score_rate'])) {
|
|
|
|
|
+ $v = (float) $rawOverall['score_rate'];
|
|
|
|
|
+ if ($v >= 0.0 && $v <= 1.0) {
|
|
|
|
|
+ return $v;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($v > 1.0 && $v <= 100.0) {
|
|
|
|
|
+ return $v / 100.0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $questions = $templateData['questions'] ?? [];
|
|
|
|
|
+ $total = 0.0;
|
|
|
|
|
+ $obtained = 0.0;
|
|
|
|
|
+ foreach ($questions as $q) {
|
|
|
|
|
+ $max = $q['score'] ?? $q['max_score'] ?? null;
|
|
|
|
|
+ $got = $q['score_obtained'] ?? null;
|
|
|
|
|
+ if (! is_numeric($max) || ! is_numeric($got)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $maxVal = (float) $max;
|
|
|
|
|
+ if ($maxVal <= 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $gotVal = max(0.0, min((float) $got, $maxVal));
|
|
|
|
|
+ $total += $maxVal;
|
|
|
|
|
+ $obtained += $gotVal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $total > 0 ? ($obtained / $total) : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function resolveScoreRateBandForDifficultyInsight(?float $scoreRate): string
|
|
|
|
|
+ {
|
|
|
|
|
+ if ($scoreRate === null) {
|
|
|
|
|
+ return 'unknown';
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($scoreRate >= 0.8) {
|
|
|
|
|
+ return 'high';
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($scoreRate >= 0.6) {
|
|
|
|
|
+ return 'mid';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return 'low';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function buildDifficultyExplainByContext(string $matchStatus, string $scoreBand, string $seed): string
|
|
|
|
|
+ {
|
|
|
|
|
+ $messageMap = config('exam.analysis_report_v3.difficulty_explain_messages', []);
|
|
|
|
|
+ if (! is_array($messageMap) || empty($messageMap)) {
|
|
|
|
|
+ $messageMap = [
|
|
|
|
|
+ '暂无' => [
|
|
|
|
|
+ 'unknown' => [
|
|
|
|
|
+ '暂无足够数据评估难度匹配。',
|
|
|
|
|
+ ],
|
|
|
|
|
+ ],
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $statusMap = $messageMap[$matchStatus] ?? $messageMap['暂无'];
|
|
|
|
|
+ $candidates = $statusMap[$scoreBand] ?? $statusMap['unknown'] ?? $messageMap['暂无']['unknown'];
|
|
|
|
|
+
|
|
|
|
|
+ return $this->pickStableVariantMessage($candidates, $seed);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function pickStableVariantMessage(array $messages, string $seed): string
|
|
|
|
|
+ {
|
|
|
|
|
+ if (empty($messages)) {
|
|
|
|
|
+ return '暂无足够数据评估难度匹配。';
|
|
|
|
|
+ }
|
|
|
|
|
+ $idx = abs(crc32($seed)) % count($messages);
|
|
|
|
|
+
|
|
|
|
|
+ return (string) $messages[$idx];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private function buildV3ComparisonInsight(array $templateData, ?float $currentScoreRate, ?float $currentMastery): array
|
|
private function buildV3ComparisonInsight(array $templateData, ?float $currentScoreRate, ?float $currentMastery): array
|
|
|
{
|
|
{
|
|
|
$paper = $templateData['paper'] ?? [];
|
|
$paper = $templateData['paper'] ?? [];
|
|
@@ -1218,68 +1300,16 @@ class ExamPdfExportService
|
|
|
default => 'E',
|
|
default => 'E',
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- $messagesByBucket = [
|
|
|
|
|
- 'A' => [
|
|
|
|
|
- '这次开局很稳,说明你的基础和状态都在线。',
|
|
|
|
|
- '第一次就拿到高分,后续保持节奏会更强。',
|
|
|
|
|
- '你的学习方法是有效的,继续按这个路径推进。',
|
|
|
|
|
- '这是一个很好的起点,接下来可以适度挑战难题。',
|
|
|
|
|
- '成绩很亮眼,说明你已经具备较强的掌握能力。',
|
|
|
|
|
- '你的投入有明显回报,继续保持就会持续领先。',
|
|
|
|
|
- '开局高分值得肯定,下一步重点是稳定输出。',
|
|
|
|
|
- '这次表现优秀,后续可以往“又快又准”再升级。',
|
|
|
|
|
- '你已经在高水平区间,继续打磨细节会更出色。',
|
|
|
|
|
- '这是非常有竞争力的起步,继续冲就对了。',
|
|
|
|
|
- ],
|
|
|
|
|
- 'B' => [
|
|
|
|
|
- '这个分数是很不错的起点,方向完全正确。',
|
|
|
|
|
- '你已经进入良好区间,再补几处薄弱点就能上台阶。',
|
|
|
|
|
- '开局表现可圈可点,继续练会更稳定。',
|
|
|
|
|
- '说明你有扎实基础,后续提升空间也很清晰。',
|
|
|
|
|
- '这次成绩不错,下一步就是把失分点逐个清掉。',
|
|
|
|
|
- '起步良好,继续保持专注,进步会很快。',
|
|
|
|
|
- '你已经具备不错的能力,差的是一点点细节打磨。',
|
|
|
|
|
- '这个起点很健康,后续很有机会冲到更高档。',
|
|
|
|
|
- '成绩说明你在正轨上,继续按计划推进就行。',
|
|
|
|
|
- '这次发挥稳定,接下来把短板补齐会很明显。',
|
|
|
|
|
- ],
|
|
|
|
|
- 'C' => [
|
|
|
|
|
- '这是正常且可提升的起点,先稳住基础最关键。',
|
|
|
|
|
- '你已经有一定掌握度,接下来重点是补薄弱模块。',
|
|
|
|
|
- '这个分数段提升通常很快,方向对了就会涨。',
|
|
|
|
|
- '开局在中位区间,不焦虑,持续练习就会突破。',
|
|
|
|
|
- '先把常错题型吃透,你的分数会明显上来。',
|
|
|
|
|
- '这次结果能帮我们精准定位问题,价值很大。',
|
|
|
|
|
- '起点清晰、空间也清晰,后续提升可期待。',
|
|
|
|
|
- '你的基础在,下一步要把稳定性做出来。',
|
|
|
|
|
- '这个阶段最怕放弃,最值得坚持。',
|
|
|
|
|
- '继续按节奏推进,很快就能看到上升曲线。',
|
|
|
|
|
- ],
|
|
|
|
|
- 'D' => [
|
|
|
|
|
- '第一次这个分数不代表上限,只代表当前起点。',
|
|
|
|
|
- '现在最重要的是先建立信心,再逐步提分。',
|
|
|
|
|
- '这次结果很有价值,能帮你更精准地补基础。',
|
|
|
|
|
- '先把核心概念补牢,分数会先稳再升。',
|
|
|
|
|
- '这个阶段提升潜力很大,方法对了进步会很快。',
|
|
|
|
|
- '不用和别人比,先和昨天的自己比就很好。',
|
|
|
|
|
- '先做对“会做的题”,再攻“有难度的题”。',
|
|
|
|
|
- '你现在需要的是节奏和耐心,不是否定自己。',
|
|
|
|
|
- '起步偏低很常见,持续练习就会逐渐反转。',
|
|
|
|
|
- '只要不放弃,这个分段通常最容易拉开增幅。',
|
|
|
|
|
- ],
|
|
|
|
|
- 'E' => [
|
|
|
|
|
- '第一次分数偏低很正常,先把学习路径走顺。',
|
|
|
|
|
- '这不是结论,只是起点,我们从基础一点点重建。',
|
|
|
|
|
- '先把会做题做稳,信心会先回来。',
|
|
|
|
|
- '现在最关键的是“稳基础、慢提速”。',
|
|
|
|
|
- '低分并不定义能力,持续训练才会定义结果。',
|
|
|
|
|
- '先把核心知识补齐,后续提升会很明显。',
|
|
|
|
|
- '今天看到的是起点,不是终点。',
|
|
|
|
|
- '你需要的是清晰步骤,不是压力。',
|
|
|
|
|
- '每次进步一点点,累计起来会很惊人。',
|
|
|
|
|
- '从现在开始,踏实走每一步,结果一定会变。',
|
|
|
|
|
- ],
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ $messagesByBucket = config('exam.analysis_report_v3.first_exam_messages_by_bucket', []);
|
|
|
|
|
+ if (! is_array($messagesByBucket) || empty($messagesByBucket)) {
|
|
|
|
|
+ $messagesByBucket = [
|
|
|
|
|
+ 'A' => ['这次开局很稳,说明你的基础和状态都在线。'],
|
|
|
|
|
+ 'B' => ['这个分数是很不错的起点,方向完全正确。'],
|
|
|
|
|
+ 'C' => ['这是正常且可提升的起点,先稳住基础最关键。'],
|
|
|
|
|
+ 'D' => ['第一次这个分数不代表上限,只代表当前起点。'],
|
|
|
|
|
+ 'E' => ['第一次分数偏低很正常,先把学习路径走顺。'],
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
$messages = $messagesByBucket[$bucket] ?? $messagesByBucket['C'];
|
|
$messages = $messagesByBucket[$bucket] ?? $messagesByBucket['C'];
|
|
|
$idx = random_int(0, count($messages) - 1);
|
|
$idx = random_int(0, count($messages) - 1);
|