Эх сурвалжийг харах

feat(report): 新增老师增降汇总与高体量异常清单

在老师明细前增加增降对比汇总,并将异常输出改为文字清单,仅保留学案本/上超过100的老师,便于快速研判波动。

Made-with: Cursor
yemeishu 1 сар өмнө
parent
commit
8eb3ede186

+ 138 - 0
scripts/report_teacher_weekly_stats.php

@@ -516,6 +516,144 @@ echo '<div class="weekly-chart">';
 echo $chartSvg;
 echo $chartSvg;
 echo "</div>\n\n";
 echo "</div>\n\n";
 
 
+// 老师列表汇总:先给出增/降总体倾向,再给出异常清单
+$calcPct = static function (int $delta, int $prev): ?float {
+    if ($prev <= 0) {
+        return null;
+    }
+    return round(($delta / $prev) * 100, 1);
+};
+
+$fmtDeltaWithPct = static function (int $delta, int $prev): string {
+    $sign = $delta > 0 ? '+' : '';
+    $pct = $prev > 0 ? sprintf('(%s%.1f%%)', $sign, ($delta / $prev) * 100) : '(上0)';
+    return $sign.$delta.$pct;
+};
+
+$paperUp = 0; $paperDown = 0; $paperFlat = 0;
+$analysisUp = 0; $analysisDown = 0; $analysisFlat = 0;
+$studentUp = 0; $studentDown = 0; $studentFlat = 0;
+$sumPaperDelta = 0; $sumAnalysisDelta = 0; $sumStudentDelta = 0;
+$anomalies = [];
+
+foreach ($rows as $r) {
+    $tidKey = (string) $r['teacher_id'];
+    $name = (string) $r['name'];
+    $papersCur = (int) $r['papers'];
+    $papersPrev = (int) $r['papers_prev'];
+    $analysisCur = (int) $r['analysis_sets'];
+    $analysisPrev = (int) $r['analysis_sets_prev'];
+    $studentCur = (int) ($studentUnionCurMap[$tidKey] ?? 0);
+    $studentPrev = (int) ($studentUnionPrevMap[$tidKey] ?? 0);
+
+    $paperDelta = $papersCur - $papersPrev;
+    $analysisDelta = $analysisCur - $analysisPrev;
+    $studentDelta = $studentCur - $studentPrev;
+    $sumPaperDelta += $paperDelta;
+    $sumAnalysisDelta += $analysisDelta;
+    $sumStudentDelta += $studentDelta;
+
+    if ($paperDelta > 0) { $paperUp++; } elseif ($paperDelta < 0) { $paperDown++; } else { $paperFlat++; }
+    if ($analysisDelta > 0) { $analysisUp++; } elseif ($analysisDelta < 0) { $analysisDown++; } else { $analysisFlat++; }
+    if ($studentDelta > 0) { $studentUp++; } elseif ($studentDelta < 0) { $studentDown++; } else { $studentFlat++; }
+
+    $paperPct = $calcPct($paperDelta, $papersPrev);
+    $analysisPct = $calcPct($analysisDelta, $analysisPrev);
+    $studentPct = $calcPct($studentDelta, $studentPrev);
+
+    // 异常口径(用于快速巡检):
+    // 1) 学案/学情明显下滑;2) 学案有量但学情为 0;3) 学情明显高于学案;
+    // 4) 学案与学情方向背离;5) 覆盖学生数异常波动。
+    if (
+        ($papersPrev >= 20 && ($paperDelta <= -20 || ($paperPct !== null && $paperPct <= -50.0))) ||
+        ($analysisPrev >= 15 && ($analysisDelta <= -15 || ($analysisPct !== null && $analysisPct <= -50.0))) ||
+        ($papersCur >= 10 && $analysisCur === 0) ||
+        ($analysisCur >= 10 && $analysisCur > $papersCur + 5) ||
+        (($paperDelta > 0 && $analysisDelta < 0 && abs($analysisDelta) >= 5) ||
+         ($paperDelta < 0 && $analysisDelta > 0 && abs($paperDelta) >= 5)) ||
+        (abs($studentDelta) >= 10 || ($studentPct !== null && abs($studentPct) >= 100.0 && $studentPrev >= 5))
+    ) {
+        $severity = abs($paperDelta) + abs($analysisDelta) + abs($studentDelta);
+        if ($papersCur >= 10 && $analysisCur === 0) { $severity += 20; }
+        if ($analysisCur >= 10 && $analysisCur > $papersCur + 5) { $severity += 12; }
+
+        $reason = [];
+        if ($papersPrev >= 20 && ($paperDelta <= -20 || ($paperPct !== null && $paperPct <= -50.0))) { $reason[] = '学案下滑'; }
+        if ($analysisPrev >= 15 && ($analysisDelta <= -15 || ($analysisPct !== null && $analysisPct <= -50.0))) { $reason[] = '学情下滑'; }
+        if ($papersCur >= 10 && $analysisCur === 0) { $reason[] = '学案有量但学情为0'; }
+        if ($analysisCur >= 10 && $analysisCur > $papersCur + 5) { $reason[] = '学情高于学案'; }
+        if (($paperDelta > 0 && $analysisDelta < 0) || ($paperDelta < 0 && $analysisDelta > 0)) { $reason[] = '学案学情背离'; }
+        if (abs($studentDelta) >= 10 || ($studentPct !== null && abs($studentPct) >= 100.0 && $studentPrev >= 5)) { $reason[] = '学生覆盖波动'; }
+
+        $anomalies[] = [
+            'severity' => $severity,
+            'teacher_id' => $tidKey,
+            'name' => $name,
+            'papers_cur' => $papersCur,
+            'papers_prev' => $papersPrev,
+            'analysis_cur' => $analysisCur,
+            'analysis_prev' => $analysisPrev,
+            'student_cur' => $studentCur,
+            'student_prev' => $studentPrev,
+            'paper_delta_text' => $fmtDeltaWithPct($paperDelta, $papersPrev),
+            'analysis_delta_text' => $fmtDeltaWithPct($analysisDelta, $analysisPrev),
+            'student_delta_text' => $fmtDeltaWithPct($studentDelta, $studentPrev),
+            'reason' => implode(' / ', array_values(array_unique($reason))),
+        ];
+    }
+}
+
+usort($anomalies, static fn ($a, $b) => $b['severity'] <=> $a['severity']);
+
+$paperTrend = $paperUp > $paperDown ? '学案整体偏增长' : ($paperUp < $paperDown ? '学案整体偏下降' : '学案整体增降持平');
+$analysisTrend = $analysisUp > $analysisDown ? '学情整体偏增长' : ($analysisUp < $analysisDown ? '学情整体偏下降' : '学情整体增降持平');
+$studentTrend = $studentUp > $studentDown ? '学生覆盖整体偏增长' : ($studentUp < $studentDown ? '学生覆盖整体偏下降' : '学生覆盖整体增降持平');
+
+echo "### 老师列表汇总(增降对比)\n\n";
+echo "| 维度 | 增长老师数 | 下降老师数 | 持平老师数 | 判断 |\n";
+echo "| :--- | ---: | ---: | ---: | :--- |\n";
+echo sprintf("| 学案(组卷) | %d | %d | %d | %s |\n", $paperUp, $paperDown, $paperFlat, $paperTrend);
+echo sprintf("| 学情(分析) | %d | %d | %d | %s |\n", $analysisUp, $analysisDown, $analysisFlat, $analysisTrend);
+echo sprintf("| 学生覆盖(去重) | %d | %d | %d | %s |\n\n", $studentUp, $studentDown, $studentFlat, $studentTrend);
+
+echo "### 异常数据(学案本/上超过100,文字清单)\n\n";
+$highVolumeAnomalies = array_values(array_filter(
+    $anomalies,
+    static fn (array $a): bool => ((int) ($a['papers_cur'] ?? 0) > 100) || ((int) ($a['papers_prev'] ?? 0) > 100)
+));
+if ($highVolumeAnomalies === []) {
+    echo "本周期未命中“学案本/上超过100”的异常老师。\n\n";
+} else {
+    $display = array_slice($highVolumeAnomalies, 0, 30);
+    $idx = 1;
+    foreach ($display as $a) {
+        $nameEsc = htmlspecialchars((string) $a['name'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+        $tidEsc = htmlspecialchars((string) $a['teacher_id'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+        $reasonEsc = htmlspecialchars((string) $a['reason'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+        $line = sprintf(
+            '%d) %s(%s):学案 %d/%d(%s);学情 %d/%d(%s);学生 %d/%d(%s);异常:%s',
+            $idx++,
+            $nameEsc,
+            $tidEsc,
+            (int) $a['papers_cur'],
+            (int) $a['papers_prev'],
+            (string) $a['paper_delta_text'],
+            (int) $a['analysis_cur'],
+            (int) $a['analysis_prev'],
+            (string) $a['analysis_delta_text'],
+            (int) $a['student_cur'],
+            (int) $a['student_prev'],
+            (string) $a['student_delta_text'],
+            $reasonEsc
+        );
+        echo '- '.$line."\n";
+    }
+    if (count($highVolumeAnomalies) > 30) {
+        echo sprintf("\n> 仅展示前 30 条,共 %d 条。\n", count($highVolumeAnomalies));
+    }
+    echo "\n";
+}
+
 echo sprintf("### 按老师 本周期有组卷 **%d** 人。\n\n", count($rows));
 echo sprintf("### 按老师 本周期有组卷 **%d** 人。\n\n", count($rows));
 
 
 echo '<table class="weekly-teacher-table">';
 echo '<table class="weekly-teacher-table">';