|
@@ -1,12 +1,14 @@
|
|
|
<?php
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 近 7 天老师组卷 + 学情分析套数(exam_analysis_results 按 paper_id 去重,一套卷计 1)。
|
|
|
|
|
- * 按老师:学案/分析/学生数为「本 / 上」并列(仅本侧数字在本大于上时绿色);保留学案·环比、学情·环比。
|
|
|
|
|
|
|
+ * 按「自然日」统计:近 N 个自然日(今日为 0 点—生成时刻)vs 紧邻的前 N 个完整自然日;exam_analysis_results 按 paper_id 去重计套。
|
|
|
|
|
+ * 按老师:学案/分析/学生数为「本 / 上」并列;保留学案·环比、学情·环比。
|
|
|
* 用法:
|
|
* 用法:
|
|
|
* php scripts/report_teacher_weekly_stats.php
|
|
* php scripts/report_teacher_weekly_stats.php
|
|
|
|
|
+ * TEACHER_WEEKLY_REPORT_DAYS=14 php scripts/report_teacher_weekly_stats.php
|
|
|
* php scripts/report_teacher_weekly_stats.php > storage/app/reports/teacher-weekly-stats-$(date +%Y-%m-%d)_$(date +%H%M%S).md
|
|
* php scripts/report_teacher_weekly_stats.php > storage/app/reports/teacher-weekly-stats-$(date +%Y-%m-%d)_$(date +%H%M%S).md
|
|
|
- * (shell 里 %H%M%S 会展开为当前时分秒;PDF 见 scripts/report_teacher_weekly_stats_pdf.php)
|
|
|
|
|
|
|
+ *
|
|
|
|
|
+ * 环境变量 / artisan:TEACHER_WEEKLY_REPORT_DAYS(默认 7)= 本/上周期各包含几个自然日。时区:config app.timezone。
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
require __DIR__ . '/../vendor/autoload.php';
|
|
require __DIR__ . '/../vendor/autoload.php';
|
|
@@ -14,10 +16,20 @@ require __DIR__ . '/../vendor/autoload.php';
|
|
|
$app = require_once __DIR__ . '/../bootstrap/app.php';
|
|
$app = require_once __DIR__ . '/../bootstrap/app.php';
|
|
|
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
|
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
|
|
|
|
|
|
|
-$endCurrent = now();
|
|
|
|
|
-$startCurrent = now()->subDays(7);
|
|
|
|
|
-$startPrev = now()->subDays(14);
|
|
|
|
|
-$endPrev = $startCurrent;
|
|
|
|
|
|
|
+$tz = (string) config('app.timezone', 'UTC');
|
|
|
|
|
+
|
|
|
|
|
+$reportPeriodDays = 7;
|
|
|
|
|
+$envDays = getenv('TEACHER_WEEKLY_REPORT_DAYS');
|
|
|
|
|
+if ($envDays !== false && $envDays !== '') {
|
|
|
|
|
+ $reportPeriodDays = max(1, min(366, (int) $envDays));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 本周期:自「今日 0 点」往回数第 N 天 0 点起,至当前时刻。上周期:紧邻的前 N 个完整自然日。 */
|
|
|
|
|
+$endCurrent = now()->timezone($tz);
|
|
|
|
|
+$todayStart = $endCurrent->copy()->startOfDay();
|
|
|
|
|
+$startCurrent = $todayStart->copy()->subDays($reportPeriodDays - 1);
|
|
|
|
|
+$startPrev = $todayStart->copy()->subDays(2 * $reportPeriodDays - 1);
|
|
|
|
|
+$endPrev = $todayStart->copy()->subDays($reportPeriodDays);
|
|
|
|
|
|
|
|
$db = \Illuminate\Support\Facades\DB::class;
|
|
$db = \Illuminate\Support\Facades\DB::class;
|
|
|
|
|
|
|
@@ -138,16 +150,18 @@ $byTeacher = \Illuminate\Support\Facades\DB::table('papers')
|
|
|
->whereNotNull('teacher_id')
|
|
->whereNotNull('teacher_id')
|
|
|
->where('teacher_id', '!=', '')
|
|
->where('teacher_id', '!=', '')
|
|
|
->where('created_at', '>=', $startCurrent)
|
|
->where('created_at', '>=', $startCurrent)
|
|
|
|
|
+ ->where('created_at', '<', $endCurrent)
|
|
|
->selectRaw('teacher_id, COUNT(*) as paper_count')
|
|
->selectRaw('teacher_id, COUNT(*) as paper_count')
|
|
|
->groupBy('teacher_id')
|
|
->groupBy('teacher_id')
|
|
|
->get();
|
|
->get();
|
|
|
|
|
|
|
|
-// 近 7 天产生学情分析的试卷套数:按 ear.paper_id 去重后归到 papers.teacher_id
|
|
|
|
|
|
|
+// 本时间窗内学情:按 ear.paper_id 去重后归到 papers.teacher_id
|
|
|
$analysisByTeacher = \Illuminate\Support\Facades\DB::table('exam_analysis_results as ear')
|
|
$analysisByTeacher = \Illuminate\Support\Facades\DB::table('exam_analysis_results as ear')
|
|
|
->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
|
|
->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
|
|
|
->whereNotNull('p.teacher_id')
|
|
->whereNotNull('p.teacher_id')
|
|
|
->where('p.teacher_id', '!=', '')
|
|
->where('p.teacher_id', '!=', '')
|
|
|
->where('ear.created_at', '>=', $startCurrent)
|
|
->where('ear.created_at', '>=', $startCurrent)
|
|
|
|
|
+ ->where('ear.created_at', '<', $endCurrent)
|
|
|
->selectRaw('p.teacher_id, COUNT(DISTINCT ear.paper_id) AS paper_set_count')
|
|
->selectRaw('p.teacher_id, COUNT(DISTINCT ear.paper_id) AS paper_set_count')
|
|
|
->groupBy('p.teacher_id')
|
|
->groupBy('p.teacher_id')
|
|
|
->get();
|
|
->get();
|
|
@@ -254,76 +268,77 @@ $windowPrev = sprintf(
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
$generatedAt = $endCurrent->format('Y-m-d H:i:s');
|
|
$generatedAt = $endCurrent->format('Y-m-d H:i:s');
|
|
|
-$tz = (string) config('app.timezone', 'UTC');
|
|
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 将 [from, to) 均分为 7 段,返回每段组卷数、学情套数(与上方「总量」同一窗口可对齐)。
|
|
|
|
|
- *
|
|
|
|
|
- * 学案:每段 COUNT(papers),七段之和 = 窗口内组卷总数。
|
|
|
|
|
- * 学情:每卷在窗口内取 MIN(ear.created_at) 所在段计 1 套,七段之和 = 窗口内卷去重套数(与总量表一致)。
|
|
|
|
|
|
|
+ * 按自然日切段(app.timezone):每天一格;本周期末段为「今日 0 点—当前时刻」。
|
|
|
|
|
+ * 学案:当日创建的组卷数,各日之和 = 窗口总量。
|
|
|
|
|
+ * 学情:该自然日内有 analysis 记录的卷去重(同日多条只计 1);跨日同一卷可在多日重复出现,故各日之和可不等于上方「窗口内卷仅计一次」的总量。
|
|
|
*
|
|
*
|
|
|
* @return array{labels: list<string>, papers: list<int>, analysis: list<int>}
|
|
* @return array{labels: list<string>, papers: list<int>, analysis: list<int>}
|
|
|
*/
|
|
*/
|
|
|
-$dailySlices = static function (\Carbon\Carbon $from, \Carbon\Carbon $toExclusive) use ($db): array {
|
|
|
|
|
- $totalSec = max(1, $toExclusive->getTimestamp() - $from->getTimestamp());
|
|
|
|
|
- $sliceSec = $totalSec / 7;
|
|
|
|
|
|
|
+$dailyNaturalSlices = static function (string $which) use ($db, $reportPeriodDays, $todayStart, $endCurrent): array {
|
|
|
|
|
+ $n = $reportPeriodDays;
|
|
|
$labels = [];
|
|
$labels = [];
|
|
|
$papers = [];
|
|
$papers = [];
|
|
|
- $analysis = array_fill(0, 7, 0);
|
|
|
|
|
- for ($i = 0; $i < 7; $i++) {
|
|
|
|
|
- $sliceFrom = $from->copy()->addSeconds((int) floor($sliceSec * $i));
|
|
|
|
|
- $sliceTo = $i < 6
|
|
|
|
|
- ? $from->copy()->addSeconds((int) floor($sliceSec * ($i + 1)))
|
|
|
|
|
- : $toExclusive;
|
|
|
|
|
- // 横轴日期:取每段结束瞬间的前一秒,避免「最后一段落在 4/19 却仍标成 4/18」(原先用段起点做标签)
|
|
|
|
|
- $labels[] = $sliceTo->copy()->subSecond()->format('n/j');
|
|
|
|
|
- $papers[] = (int) $db::table('papers')
|
|
|
|
|
- ->whereNotNull('teacher_id')
|
|
|
|
|
- ->where('teacher_id', '!=', '')
|
|
|
|
|
- ->where('created_at', '>=', $sliceFrom)
|
|
|
|
|
- ->where('created_at', '<', $sliceTo)
|
|
|
|
|
- ->count();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $analysis = [];
|
|
|
|
|
+
|
|
|
|
|
+ $countAnalysisSetsInRange = static function ($from, $toExclusive) use ($db): int {
|
|
|
|
|
+ return (int) $db::table('exam_analysis_results as ear')
|
|
|
|
|
+ ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
|
|
|
|
|
+ ->whereNotNull('p.teacher_id')
|
|
|
|
|
+ ->where('p.teacher_id', '!=', '')
|
|
|
|
|
+ ->where('ear.created_at', '>=', $from)
|
|
|
|
|
+ ->where('ear.created_at', '<', $toExclusive)
|
|
|
|
|
+ ->distinct()
|
|
|
|
|
+ ->count('ear.paper_id');
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- $firstAnalysisRows = $db::table('exam_analysis_results as ear')
|
|
|
|
|
- ->join('papers as p', 'p.paper_id', '=', 'ear.paper_id')
|
|
|
|
|
- ->whereNotNull('p.teacher_id')
|
|
|
|
|
- ->where('p.teacher_id', '!=', '')
|
|
|
|
|
- ->where('ear.created_at', '>=', $from)
|
|
|
|
|
- ->where('ear.created_at', '<', $toExclusive)
|
|
|
|
|
- ->selectRaw('ear.paper_id, MIN(ear.created_at) AS first_at')
|
|
|
|
|
- ->groupBy('ear.paper_id')
|
|
|
|
|
- ->get();
|
|
|
|
|
-
|
|
|
|
|
- $fromTs = $from->getTimestamp();
|
|
|
|
|
- foreach ($firstAnalysisRows as $row) {
|
|
|
|
|
- $t = \Carbon\Carbon::parse($row->first_at)->getTimestamp();
|
|
|
|
|
- $idx = (int) floor(($t - $fromTs) / $sliceSec);
|
|
|
|
|
- if ($idx < 0) {
|
|
|
|
|
- $idx = 0;
|
|
|
|
|
|
|
+ if ($which === 'current') {
|
|
|
|
|
+ for ($i = 0; $i < $n; $i++) {
|
|
|
|
|
+ $dayStart = $todayStart->copy()->subDays($n - 1 - $i);
|
|
|
|
|
+ $dayEnd = ($i < $n - 1) ? $dayStart->copy()->addDay() : $endCurrent;
|
|
|
|
|
+ $labels[] = $dayStart->format('j');
|
|
|
|
|
+ $papers[] = (int) $db::table('papers')
|
|
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
|
|
+ ->where('created_at', '>=', $dayStart)
|
|
|
|
|
+ ->where('created_at', '<', $dayEnd)
|
|
|
|
|
+ ->count();
|
|
|
|
|
+ $analysis[] = $countAnalysisSetsInRange($dayStart, $dayEnd);
|
|
|
}
|
|
}
|
|
|
- if ($idx > 6) {
|
|
|
|
|
- $idx = 6;
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ for ($i = 0; $i < $n; $i++) {
|
|
|
|
|
+ $dayStart = $todayStart->copy()->subDays(2 * $n - 1 - $i);
|
|
|
|
|
+ $dayEnd = $dayStart->copy()->addDay();
|
|
|
|
|
+ $labels[] = $dayStart->format('j');
|
|
|
|
|
+ $papers[] = (int) $db::table('papers')
|
|
|
|
|
+ ->whereNotNull('teacher_id')
|
|
|
|
|
+ ->where('teacher_id', '!=', '')
|
|
|
|
|
+ ->where('created_at', '>=', $dayStart)
|
|
|
|
|
+ ->where('created_at', '<', $dayEnd)
|
|
|
|
|
+ ->count();
|
|
|
|
|
+ $analysis[] = $countAnalysisSetsInRange($dayStart, $dayEnd);
|
|
|
}
|
|
}
|
|
|
- $analysis[$idx]++;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return ['labels' => $labels, 'papers' => $papers, 'analysis' => $analysis];
|
|
return ['labels' => $labels, 'papers' => $papers, 'analysis' => $analysis];
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-$curDaily = $dailySlices($startCurrent, $endCurrent);
|
|
|
|
|
-$prevDaily = $dailySlices($startPrev, $endPrev);
|
|
|
|
|
|
|
+$curDaily = $dailyNaturalSlices('current');
|
|
|
|
|
+$prevDaily = $dailyNaturalSlices('prev');
|
|
|
|
|
|
|
|
-/** 左侧:学案(组卷)套数;右侧:学情分析套数(卷去重)。实线本周期,虚线上周期。 */
|
|
|
|
|
|
|
+/** 左侧:学案(组卷)套数;右侧:学情分析套数(卷去重)。横轴:本周期日 / 上周期同序号日。 */
|
|
|
$buildDualChartsHtml = static function (array $curDaily, array $prevDaily): string {
|
|
$buildDualChartsHtml = static function (array $curDaily, array $prevDaily): string {
|
|
|
- $labels = $curDaily['labels'];
|
|
|
|
|
|
|
+ $axisCur = $curDaily['labels'];
|
|
|
|
|
+ $axisPrev = $prevDaily['labels'];
|
|
|
$pc = $curDaily['papers'];
|
|
$pc = $curDaily['papers'];
|
|
|
$ac = $curDaily['analysis'];
|
|
$ac = $curDaily['analysis'];
|
|
|
$pp = $prevDaily['papers'];
|
|
$pp = $prevDaily['papers'];
|
|
|
$ap = $prevDaily['analysis'];
|
|
$ap = $prevDaily['analysis'];
|
|
|
|
|
|
|
|
$halfSvg = static function (
|
|
$halfSvg = static function (
|
|
|
- array $labels,
|
|
|
|
|
|
|
+ array $labelsCur,
|
|
|
|
|
+ array $labelsPrev,
|
|
|
array $cur,
|
|
array $cur,
|
|
|
array $prev,
|
|
array $prev,
|
|
|
string $strokeCur,
|
|
string $strokeCur,
|
|
@@ -333,14 +348,14 @@ $buildDualChartsHtml = static function (array $curDaily, array $prevDaily): stri
|
|
|
): string {
|
|
): string {
|
|
|
$maxY = max(1, ...$cur, ...$prev);
|
|
$maxY = max(1, ...$cur, ...$prev);
|
|
|
$W = 248;
|
|
$W = 248;
|
|
|
- $H = 200;
|
|
|
|
|
|
|
+ $H = 206;
|
|
|
$padL = 38;
|
|
$padL = 38;
|
|
|
$padR = 8;
|
|
$padR = 8;
|
|
|
$padT = 14;
|
|
$padT = 14;
|
|
|
- $padB = 34;
|
|
|
|
|
|
|
+ $padB = 38;
|
|
|
$gw = $W - $padL - $padR;
|
|
$gw = $W - $padL - $padR;
|
|
|
$gh = $H - $padT - $padB;
|
|
$gh = $H - $padT - $padB;
|
|
|
- $n = 7;
|
|
|
|
|
|
|
+ $n = max(1, count($labelsCur));
|
|
|
$xAt = static function (int $i) use ($padL, $gw, $n): float {
|
|
$xAt = static function (int $i) use ($padL, $gw, $n): float {
|
|
|
return $padL + ($n <= 1 ? $gw / 2 : $gw * $i / ($n - 1));
|
|
return $padL + ($n <= 1 ? $gw / 2 : $gw * $i / ($n - 1));
|
|
|
};
|
|
};
|
|
@@ -371,12 +386,14 @@ $buildDualChartsHtml = static function (array $curDaily, array $prevDaily): stri
|
|
|
$tickTxt = '';
|
|
$tickTxt = '';
|
|
|
for ($i = 0; $i < $n; $i++) {
|
|
for ($i = 0; $i < $n; $i++) {
|
|
|
$x = $xAt($i);
|
|
$x = $xAt($i);
|
|
|
- $lab = htmlspecialchars((string) ($labels[$i] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
|
|
+ $lc = htmlspecialchars((string) ($labelsCur[$i] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
|
|
+ $lp = htmlspecialchars((string) ($labelsPrev[$i] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
|
$tickTxt .= sprintf(
|
|
$tickTxt .= sprintf(
|
|
|
- '<text x="%.1f" y="%d" text-anchor="middle" font-size="8.5" fill="#374151" font-family="sun-exta,sans-serif">%s</text>',
|
|
|
|
|
|
|
+ '<text x="%.1f" y="%d" text-anchor="middle" font-size="7" fill="#374151" font-family="sun-exta,sans-serif">%s/%s</text>',
|
|
|
$x,
|
|
$x,
|
|
|
- $H - 8,
|
|
|
|
|
- $lab
|
|
|
|
|
|
|
+ $H - 9,
|
|
|
|
|
+ $lc,
|
|
|
|
|
+ $lp
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -443,8 +460,8 @@ $buildDualChartsHtml = static function (array $curDaily, array $prevDaily): stri
|
|
|
return $svg;
|
|
return $svg;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- $left = $halfSvg($labels, $pc, $pp, '#2563eb', '#93c5fd', '学案·本周期', '学案·上周期');
|
|
|
|
|
- $right = $halfSvg($labels, $ac, $ap, '#ea580c', '#fdba74', '学情·本周期', '学情·上周期');
|
|
|
|
|
|
|
+ $left = $halfSvg($axisCur, $axisPrev, $pc, $pp, '#2563eb', '#93c5fd', '学案·本周期', '学案·上周期');
|
|
|
|
|
+ $right = $halfSvg($axisCur, $axisPrev, $ac, $ap, '#ea580c', '#fdba74', '学情·本周期', '学情·上周期');
|
|
|
|
|
|
|
|
return '<table class="weekly-chart-pair" style="width:100%;border-collapse:collapse;margin:0;"><tr>'
|
|
return '<table class="weekly-chart-pair" style="width:100%;border-collapse:collapse;margin:0;"><tr>'
|
|
|
.'<td style="width:50%;vertical-align:top;padding:2px 5px 2px 0;">'.$left.'</td>'
|
|
.'<td style="width:50%;vertical-align:top;padding:2px 5px 2px 0;">'.$left.'</td>'
|
|
@@ -454,8 +471,9 @@ $buildDualChartsHtml = static function (array $curDaily, array $prevDaily): stri
|
|
|
|
|
|
|
|
$chartSvg = $buildDualChartsHtml($curDaily, $prevDaily);
|
|
$chartSvg = $buildDualChartsHtml($curDaily, $prevDaily);
|
|
|
|
|
|
|
|
-echo "## 老师组卷与学情分析(近7天)\n\n";
|
|
|
|
|
-echo "> 生成 {$generatedAt} · {$tz} · 本 {$windowCur} · 上 {$windowPrev}\n\n";
|
|
|
|
|
|
|
+echo sprintf("## 老师组卷与学情分析(近%d个自然日)\n\n", $reportPeriodDays);
|
|
|
|
|
+echo "> 生成 {$generatedAt} · {$tz} · 本 {$windowCur} · 上 {$windowPrev}\n";
|
|
|
|
|
+echo sprintf("> 口径:自然日边界(config timezone);今日段为当日 0:00—生成时刻;上周期为紧邻的前 **%d** 个完整自然日。\n\n", $reportPeriodDays);
|
|
|
|
|
|
|
|
echo "### 总量\n\n";
|
|
echo "### 总量\n\n";
|
|
|
echo "| 指标 | 本周期 | 上周期 | 环比 |\n";
|
|
echo "| 指标 | 本周期 | 上周期 | 环比 |\n";
|
|
@@ -464,7 +482,7 @@ echo sprintf("| 组卷总套数 | %d | %d | %s |\n", $totalPapersCur, $totalPape
|
|
|
echo sprintf("| 学情分析套数(卷去重) | %d | %d | %s |\n", $totalAnalysisCur, $totalAnalysisPrev, $wowLineHtml($totalAnalysisCur, $totalAnalysisPrev, $colorAnalysis));
|
|
echo sprintf("| 学情分析套数(卷去重) | %d | %d | %s |\n", $totalAnalysisCur, $totalAnalysisPrev, $wowLineHtml($totalAnalysisCur, $totalAnalysisPrev, $colorAnalysis));
|
|
|
echo sprintf("| 有组卷老师数 | %d | %d | %s |\n", $teachersCur, $teachersPrev, $wowLineHtml($teachersCur, $teachersPrev, $colorStudents));
|
|
echo sprintf("| 有组卷老师数 | %d | %d | %s |\n", $teachersCur, $teachersPrev, $wowLineHtml($teachersCur, $teachersPrev, $colorStudents));
|
|
|
|
|
|
|
|
-echo "\n### 近7段每日对比(时间轴对齐;左:学案 · 右:学情;七段合计与本表总量一致)\n\n";
|
|
|
|
|
|
|
+echo "### 逐日对比(左:学案 · 右:学情)\n\n";
|
|
|
echo '<div class="weekly-chart">';
|
|
echo '<div class="weekly-chart">';
|
|
|
echo $chartSvg;
|
|
echo $chartSvg;
|
|
|
echo "</div>\n\n";
|
|
echo "</div>\n\n";
|