report_teacher_weekly_stats.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <?php
  2. /**
  3. * 近 7 天老师组卷 + 学情分析套数(exam_analysis_results 按 paper_id 去重,一套卷计 1)
  4. * 用法:
  5. * php scripts/report_teacher_weekly_stats.php
  6. * php scripts/report_teacher_weekly_stats.php > storage/app/reports/teacher-weekly-stats-$(date +%Y-%m-%d)_$(date +%H%M%S).md
  7. */
  8. require __DIR__ . '/../vendor/autoload.php';
  9. $app = require_once __DIR__ . '/../bootstrap/app.php';
  10. $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
  11. $endCurrent = now();
  12. $startCurrent = now()->subDays(7);
  13. $startPrev = now()->subDays(14);
  14. $endPrev = $startCurrent;
  15. $db = \Illuminate\Support\Facades\DB::class;
  16. $sumPapers = static function ($from, $toExclusive) use ($db) {
  17. $q = $db::table('papers')
  18. ->whereNotNull('teacher_id')
  19. ->where('teacher_id', '!=', '')
  20. ->where('created_at', '>=', $from);
  21. if ($toExclusive !== null) {
  22. $q->where('created_at', '<', $toExclusive);
  23. }
  24. return (int) $q->count();
  25. };
  26. /** 学情分析:以卷子为单位,同一 paper_id 在区间内多条记录只计 1 */
  27. $countDistinctAnalysisPapers = static function ($from, $toExclusive) use ($db) {
  28. $q = $db::table('exam_analysis_results as ear')
  29. ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
  30. ->whereNotNull('p.teacher_id')
  31. ->where('p.teacher_id', '!=', '')
  32. ->where('ear.created_at', '>=', $from);
  33. if ($toExclusive !== null) {
  34. $q->where('ear.created_at', '<', $toExclusive);
  35. }
  36. return (int) $q->distinct()->count('ear.paper_id');
  37. };
  38. $countActiveTeachers = static function ($from, $toExclusive) use ($db) {
  39. $q = $db::table('papers')
  40. ->whereNotNull('teacher_id')
  41. ->where('teacher_id', '!=', '')
  42. ->where('created_at', '>=', $from);
  43. if ($toExclusive !== null) {
  44. $q->where('created_at', '<', $toExclusive);
  45. }
  46. return (int) $q->distinct()->count('teacher_id');
  47. };
  48. $totalPapersCur = $sumPapers($startCurrent, null);
  49. $totalPapersPrev = $sumPapers($startPrev, $endPrev);
  50. $totalAnalysisCur = $countDistinctAnalysisPapers($startCurrent, null);
  51. $totalAnalysisPrev = $countDistinctAnalysisPapers($startPrev, $endPrev);
  52. $teachersCur = $countActiveTeachers($startCurrent, null);
  53. $teachersPrev = $countActiveTeachers($startPrev, $endPrev);
  54. $wowLine = static function (int $cur, int $prev): string {
  55. $delta = $cur - $prev;
  56. if ($prev === 0) {
  57. if ($cur === 0) {
  58. return '0';
  59. }
  60. return sprintf('+%d(上周期0)', $delta);
  61. }
  62. $pct = round(($delta / $prev) * 100, 2);
  63. $sign = $delta >= 0 ? '+' : '';
  64. $dir = match (true) {
  65. $delta > 0 => '↑',
  66. $delta < 0 => '↓',
  67. default => '→',
  68. };
  69. return sprintf('%s%d(%s%.2f%%)%s', $sign, $delta, $sign, $pct, $dir);
  70. };
  71. /** 总量表环比列:仅增长标绿 */
  72. $wowLineHtml = static function (int $cur, int $prev) use ($wowLine): string {
  73. $plain = $wowLine($cur, $prev);
  74. if ($cur > $prev) {
  75. return '<span style="color:#16a34a;font-weight:600;">'.$plain.'</span>';
  76. }
  77. return $plain;
  78. };
  79. /** 明细环比列:仅增长标绿 */
  80. $compareCellHtml = static function (int $cur, int $prev): string {
  81. $d = $cur - $prev;
  82. if ($d === 0) {
  83. return '0';
  84. }
  85. if ($prev === 0) {
  86. if ($cur === 0) {
  87. return '0';
  88. }
  89. return '<span style="color:#16a34a;font-weight:600;">+'.$d.'(上0)</span>';
  90. }
  91. $pct = round(($d / $prev) * 100, 1);
  92. $sign = $d > 0 ? '+' : '';
  93. $text = sprintf('%s%d(%s%.1f%%)', $sign, $d, $sign, $pct);
  94. if ($d > 0) {
  95. return '<span style="color:#16a34a;font-weight:600;">'.$text.'</span>';
  96. }
  97. return $text;
  98. };
  99. $byTeacher = \Illuminate\Support\Facades\DB::table('papers')
  100. ->whereNotNull('teacher_id')
  101. ->where('teacher_id', '!=', '')
  102. ->where('created_at', '>=', $startCurrent)
  103. ->selectRaw('teacher_id, COUNT(*) as paper_count')
  104. ->groupBy('teacher_id')
  105. ->get();
  106. // 近 7 天产生学情分析的试卷套数:按 ear.paper_id 去重后归到 papers.teacher_id
  107. $analysisByTeacher = \Illuminate\Support\Facades\DB::table('exam_analysis_results as ear')
  108. ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
  109. ->whereNotNull('p.teacher_id')
  110. ->where('p.teacher_id', '!=', '')
  111. ->where('ear.created_at', '>=', $startCurrent)
  112. ->selectRaw('p.teacher_id, COUNT(DISTINCT ear.paper_id) AS paper_set_count')
  113. ->groupBy('p.teacher_id')
  114. ->get();
  115. $analysisMap = [];
  116. foreach ($analysisByTeacher as $r) {
  117. $analysisMap[(string) $r->teacher_id] = (int) $r->paper_set_count;
  118. }
  119. $paperMap = [];
  120. foreach ($byTeacher as $r) {
  121. $paperMap[(string) $r->teacher_id] = (int) $r->paper_count;
  122. }
  123. $byTeacherPrev = $db::table('papers')
  124. ->whereNotNull('teacher_id')
  125. ->where('teacher_id', '!=', '')
  126. ->where('created_at', '>=', $startPrev)
  127. ->where('created_at', '<', $endPrev)
  128. ->selectRaw('teacher_id, COUNT(*) as paper_count')
  129. ->groupBy('teacher_id')
  130. ->get();
  131. $analysisByTeacherPrev = $db::table('exam_analysis_results as ear')
  132. ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
  133. ->whereNotNull('p.teacher_id')
  134. ->where('p.teacher_id', '!=', '')
  135. ->where('ear.created_at', '>=', $startPrev)
  136. ->where('ear.created_at', '<', $endPrev)
  137. ->selectRaw('p.teacher_id, COUNT(DISTINCT ear.paper_id) AS paper_set_count')
  138. ->groupBy('p.teacher_id')
  139. ->get();
  140. $paperMapPrev = [];
  141. foreach ($byTeacherPrev as $r) {
  142. $paperMapPrev[(string) $r->teacher_id] = (int) $r->paper_count;
  143. }
  144. $analysisMapPrev = [];
  145. foreach ($analysisByTeacherPrev as $r) {
  146. $analysisMapPrev[(string) $r->teacher_id] = (int) $r->paper_set_count;
  147. }
  148. $names = \Illuminate\Support\Facades\DB::table('teachers')->pluck('name', 'teacher_id');
  149. $nameStrMap = [];
  150. foreach ($names as $tid => $nm) {
  151. $nameStrMap[(string) $tid] = $nm;
  152. }
  153. $rows = [];
  154. foreach ($paperMap as $tid => $paperCount) {
  155. $rows[] = [
  156. 'teacher_id' => $tid,
  157. 'name' => (string) ($nameStrMap[$tid] ?? $tid),
  158. 'papers' => $paperCount,
  159. 'analysis_sets' => (int) ($analysisMap[$tid] ?? 0),
  160. 'papers_prev' => (int) ($paperMapPrev[$tid] ?? 0),
  161. 'analysis_sets_prev' => (int) ($analysisMapPrev[$tid] ?? 0),
  162. ];
  163. }
  164. usort($rows, static fn ($a, $b) => $b['papers'] <=> $a['papers']);
  165. $windowCur = sprintf(
  166. '%s ~ %s',
  167. $startCurrent->format('Y-m-d H:i:s'),
  168. $endCurrent->format('Y-m-d H:i:s')
  169. );
  170. $windowPrev = sprintf(
  171. '%s ~ %s',
  172. $startPrev->format('Y-m-d H:i:s'),
  173. $endPrev->format('Y-m-d H:i:s')
  174. );
  175. $generatedAt = $endCurrent->format('Y-m-d H:i:s');
  176. $tz = (string) config('app.timezone', 'UTC');
  177. echo "## 老师组卷与学情分析(近7天)\n\n";
  178. echo "> 生成 {$generatedAt} · {$tz} · 本 {$windowCur} · 上 {$windowPrev}\n\n";
  179. echo "### 总量\n\n";
  180. echo "| 指标 | 本周期 | 上周期 | 环比 |\n";
  181. echo "| --- | ---: | ---: | --- |\n";
  182. echo sprintf("| 组卷总套数 | %d | %d | %s |\n", $totalPapersCur, $totalPapersPrev, $wowLineHtml($totalPapersCur, $totalPapersPrev));
  183. echo sprintf("| 学情分析套数(卷去重) | %d | %d | %s |\n", $totalAnalysisCur, $totalAnalysisPrev, $wowLineHtml($totalAnalysisCur, $totalAnalysisPrev));
  184. echo sprintf("| 有组卷老师数 | %d | %d | %s |\n", $teachersCur, $teachersPrev, $wowLineHtml($teachersCur, $teachersPrev));
  185. echo "\n### 按老师\n\n";
  186. echo "| 排名 | 老师 | teacher_id | 组卷·本 | 组卷·上 | 组卷·环比 | 学情·本 | 学情·上 | 学情·环比 |\n";
  187. echo "| ---: | --- | --- | ---: | ---: | --- | ---: | ---: | --- |\n";
  188. $i = 1;
  189. foreach ($rows as $r) {
  190. $nm = str_replace(['|', "\n", '<', '>'], ['/', ' ', '', ''], $r['name']);
  191. $pc = $r['papers'];
  192. $pp = $r['papers_prev'];
  193. $ac = $r['analysis_sets'];
  194. $ap = $r['analysis_sets_prev'];
  195. echo sprintf(
  196. "| %d | %s | %s | %d | %d | %s | %d | %d | %s |\n",
  197. $i++,
  198. $nm,
  199. $r['teacher_id'],
  200. $pc,
  201. $pp,
  202. $compareCellHtml($pc, $pp),
  203. $ac,
  204. $ap,
  205. $compareCellHtml($ac, $ap)
  206. );
  207. }
  208. echo sprintf("\n本周期有组卷 **%d** 人。\n", count($rows));