新),分数有高有低。 */ function makeRows(string $sid, array $scoresByTime): array { $rows = []; $t = 1000; // 递增时间戳 foreach ($scoresByTime as $sc) { $t += 600; $rows[] = [ 'sid' => $sid, 'name' => '测试生', 'cls' => '1班', 'grp' => 'A', 'score' => $sc, 'submit_ts' => $t, 'submit_at' => date('Y-m-d H:i:s', $t), 'duration' => 120, 'duration_txt'=> '2分0秒', 'answer_id' => 'ans-' . $t, 'matched' => true, 'temp_save' => false, ]; } return $rows; } echo "== dedupe=true(默认:代表成绩取最新一份)==\n"; // 11 次提交:分数各不相同,最新一份是 84.0,最高是 98.0 $scores = [84.0, 98.0, 96.0, 96.0, 96.0, 94.0, 94.0, 96.0, 90.0, 88.0, 98.0]; $rows = makeRows('S001', $scores); $agg = DataService::aggregateByStudent($rows, true); check('聚合后只有 1 行(一人一行)', count($agg['rows']) === 1); $r = $agg['rows'][0]; check('attempts = 11(真实次数,而非被吞成 1)', ($r['attempts'] ?? 0) === 11, 'got ' . var_export($r['attempts'] ?? null, true)); check('attempts_list 含 11 条(历史可展开全部)', count($r['attempts_list'] ?? []) === 11, 'got ' . count($r['attempts_list'] ?? [])); check('best_score = 98.0(跨全部尝试的真实最高分)', (float)($r['best_score'] ?? -1) === 98.0, 'got ' . var_export($r['best_score'] ?? null, true)); // 代表成绩 = 最新一份(数组中最后一个时间 = 98.0,因为 scores 数组最后一个是 98.0;注意最新提交对应最后构造的 98.0) check('代表成绩取最新提交 = 98.0', (float)($r['score'] ?? -1) === 98.0, 'got ' . var_export($r['score'] ?? null, true)); check('attempts_list 项数 idx 连续 1..11', (int)($r['attempts_list'][10]['idx'] ?? 0) === 11); check('attempts_list 至少一条 is_best(最高分标记)', count(array_filter($r['attempts_list'], fn($a) => !empty($a['is_best']))) >= 1); echo "\n== dedupe=false(代表成绩取最高分)==\n"; $agg2 = DataService::aggregateByStudent($rows, false); $r2 = $agg2['rows'][0]; check('attempts = 11(与 dedupe 无关,始终真实)', ($r2['attempts'] ?? 0) === 11, 'got ' . var_export($r2['attempts'] ?? null, true)); check('attempts_list 含 11 条', count($r2['attempts_list'] ?? []) === 11, 'got ' . count($r2['attempts_list'] ?? [])); check('代表成绩取最高分 = 98.0', (float)($r2['score'] ?? -1) === 98.0, 'got ' . var_export($r2['score'] ?? null, true)); check('best_score = 98.0', (float)($r2['best_score'] ?? -1) === 98.0, 'got ' . var_export($r2['best_score'] ?? null, true)); echo "\n== 重考低分也应被统计(历史含低分,且因提交时间更新能被发现)==\n"; // 先考 90,后重考 70(更低):考试次数必须=2,最新提交=70 $rows3 = makeRows('S002', [90.0, 70.0]); $agg3 = DataService::aggregateByStudent($rows3, true); $r3 = $agg3['rows'][0]; check('attempts = 2', ($r3['attempts'] ?? 0) === 2, 'got ' . var_export($r3['attempts'] ?? null, true)); check('attempts_list = 2 条(含低分重考)', count($r3['attempts_list'] ?? []) === 2); check('代表成绩取最新 = 70.0', (float)($r3['score'] ?? -1) === 70.0, 'got ' . var_export($r3['score'] ?? null, true)); check('last_submit_ts 指向最新提交(重考可被高亮发现)', ($r3['last_submit_ts'] ?? 0) === $rows3[1]['submit_ts']); echo "\n== 仅 1 次提交不受影响 ==\n"; $rows4 = makeRows('S003', [85.0]); $agg4 = DataService::aggregateByStudent($rows4, true); $r4 = $agg4['rows'][0]; check('单次提交 attempts = 1', ($r4['attempts'] ?? 0) === 1); check('单次提交 attempts_list = 1', count($r4['attempts_list'] ?? []) === 1); echo "\n== 按提交时间年月日分组:每个分类内标记当日最高 ==\n"; /** 按 [时间戳, 分数] 造行;ts=0 表示时间未知 */ function makeRowsAt(string $sid, array $items): array { $rows = []; foreach ($items as $it) { [$ts, $sc] = $it; $rows[] = [ 'sid' => $sid, 'name' => '测试生', 'cls' => '1班', 'grp' => 'A', 'score' => $sc, 'submit_ts' => $ts, 'submit_at' => $ts > 0 ? date('Y-m-d H:i:s', $ts) : '-', 'duration' => 120, 'duration_txt' => '2分0秒', 'answer_id' => 'ans-' . $ts, 'matched' => true, 'temp_save' => false, ]; } return $rows; } // 09-08 三次(70/95/88),09-09 两次(60/90) $rows5 = makeRowsAt('S005', [ [strtotime('2026-09-08 09:00:00'), 70.0], [strtotime('2026-09-08 11:00:00'), 95.0], [strtotime('2026-09-08 15:00:00'), 88.0], [strtotime('2026-09-09 10:00:00'), 60.0], [strtotime('2026-09-09 14:00:00'), 90.0], ]); $agg5 = DataService::aggregateByStudent($rows5, true); $r5 = $agg5['rows'][0]; $al5 = $r5['attempts_list']; check('5 次提交 attempts_list = 5', count($al5) === 5); check('前 3 条归属 2026-09-08', ($al5[0]['day'] ?? '') === '2026-09-08' && ($al5[2]['day'] ?? '') === '2026-09-08', 'got ' . ($al5[0]['day'] ?? '?')); check('后 2 条归属 2026-09-09', ($al5[3]['day'] ?? '') === '2026-09-09' && ($al5[4]['day'] ?? '') === '2026-09-09', 'got ' . ($al5[3]['day'] ?? '?')); check('09-08 当天次数 = 3', ($al5[0]['day_count'] ?? 0) === 3, 'got ' . ($al5[0]['day_count'] ?? '?')); check('09-09 当天次数 = 2', ($al5[3]['day_count'] ?? 0) === 2, 'got ' . ($al5[3]['day_count'] ?? '?')); $dayBestIdx = array_values(array_filter($al5, fn($a) => !empty($a['day_best']))); check('每个分类各标记 1 个当日最高(共 2 个)', count($dayBestIdx) === 2, 'got ' . count($dayBestIdx)); check('09-08 当日最高 = 95(第 2 次)', (int)($dayBestIdx[0]['idx'] ?? 0) === 2 && (float)($dayBestIdx[0]['score'] ?? -1) === 95.0, 'got idx=' . ($dayBestIdx[0]['idx'] ?? '?') . ' score=' . ($dayBestIdx[0]['score'] ?? '?')); check('09-09 当日最高 = 90(第 5 次)', (int)($dayBestIdx[1]['idx'] ?? 0) === 5 && (float)($dayBestIdx[1]['score'] ?? -1) === 90.0, 'got idx=' . ($dayBestIdx[1]['idx'] ?? '?') . ' score=' . ($dayBestIdx[1]['score'] ?? '?')); check('全局最佳仍是 95(第 2 次)', (int)($al5[1]['is_best'] ?? 0) === 1 || !empty($al5[1]['is_best'])); echo "\n== 时间未知(submit_ts=0)归入「未知时间」分组 ==\n"; $rows6 = makeRowsAt('S006', [[0, 50.0], [0, 80.0]]); $agg6 = DataService::aggregateByStudent($rows6, true); $al6 = $agg6['rows'][0]['attempts_list']; check('时间未知时 day = 空串', ($al6[0]['day'] ?? 'x') === '' && ($al6[1]['day'] ?? 'x') === '', 'got ' . var_export($al6[0]['day'] ?? null, true)); check('未知时间组次数 = 2', ($al6[0]['day_count'] ?? 0) === 2, 'got ' . ($al6[0]['day_count'] ?? '?')); check('未知时间组内仍标记最高(80)', !empty($al6[1]['day_best']) && (float)($al6[1]['score'] ?? -1) === 80.0); echo "\n== 日期键时区边界(不能用 UTC,否则零点前后错一天)==\n"; check('00:30 仍算当天(东八区不会退到前一天)', DataService::dayKeyOf((int)strtotime('2026-09-08 00:30:00')) === '2026-09-08', 'got ' . DataService::dayKeyOf((int)strtotime('2026-09-08 00:30:00'))); check('23:30 仍算当天', DataService::dayKeyOf((int)strtotime('2026-09-08 23:30:00')) === '2026-09-08', 'got ' . DataService::dayKeyOf((int)strtotime('2026-09-08 23:30:00'))); check('无时间戳时从展示串取日期', DataService::dayKeyOf(0, '2026-09-08 09:41:20') === '2026-09-08'); check('无时间戳且展示串为 - 时返回空串', DataService::dayKeyOf(0, '-') === ''); echo "\n========================================\n"; echo "通过 {$pass} · 失败 {$fail}\n"; exit($fail === 0 ? 0 : 1);