From 0174b2abc214e021b7a67679c4d350340e31113b Mon Sep 17 00:00:00 2001 From: nxxy335 Date: Thu, 10 Sep 2026 10:41:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=80=83=E8=AF=95=E6=AC=A1=E6=95=B0?= =?UTF-8?q?=E4=B8=8E=E5=8E=86=E5=8F=B2=E5=A7=8B=E7=BB=88=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=8F=90=E4=BA=A4=EF=BC=88=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=A2=AB=20dedupe=20=E5=90=9E=E6=88=90=201=20=E6=9D=A1?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - aggregateByStudent 修复:attempts(真实次数) 与 attempts_list(每次提交明细) 无论 dedupe 取值都使用全部提交;dedupe 仅决定主明细表代表成绩取最新 还是最高分。此前 dedupe=true(默认)把 attempts 写成 1、attempts_list 只留 最新一份,导致考试次数列和点击历史都只显示 1 条,多次刷题数据丢失。 - best_score 改为跨全部尝试的真实最高分,修复历史弹窗标题最高分错误。 - 新增 tests/test-aggregate.php 覆盖:11次/2次/1次提交、dedupe 两种取值、 重考低分、导出展开全部记录,共 17 项断言全部通过。 数据印证:第二周测试(1).xlsx 共 146 份提交 / 35 人,31 人多次提交 (最多 11 次),修复后考试次数与历史均完整呈现。 --- lib/DataService.php | 27 +++++++----- tests/test-aggregate.php | 94 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 tests/test-aggregate.php diff --git a/lib/DataService.php b/lib/DataService.php index bb72a60..aeb1213 100644 --- a/lib/DataService.php +++ b/lib/DataService.php @@ -416,8 +416,13 @@ class DataService /** * 按学号聚合一次提交的答卷记录,解决"同人多次刷卷"在看板中被当成多个人的问题。 - * - dedupe=true:每学号仅保留提交时间最新的一份(原行为) - * - dedupe=false:每学号一行,代表成绩取最高分,附 attempts(次数) / attempts_list(每次提交明细) + * + * 关键语义(修复"考试次数=1 / 历史只有一条"的 bug): + * - attempts(真实考试次数) 与 attempts_list(每次提交明细) **始终等于全部提交**,与 dedupe 无关; + * 这样"考试次数列"与"点击查看历史"永远展示完整数据。 + * - dedupe 仅决定主明细表"代表成绩"那一行取哪次考试的成绩: + * dedupe=true → 取提交时间最新的一份(默认,看板更干净) + * dedupe=false → 取最高分(同分时取最早达到该分数的那次) * * @param array $rows 已匹配学号的原始 row(每条 = 一次提交) * @return array{rows:array, attempts_total:int} @@ -468,14 +473,12 @@ class DataService ]; }, $sorted); + // 代表成绩选择(仅影响主明细表那一行的成绩,不影响考试次数 / 历史明细) if ($dedupe) { - // 仅保留提交时间最新的一份(明细列表也只留这一份,与"只保留最新"语义一致) + // 代表成绩 = 提交时间最新的一份(默认,看板更干净) $row = $sorted[count($sorted) - 1]; - $row['attempts'] = 1; - $row['best_score'] = $row['score']; - $row['attempts_list'] = [$attemptsList[count($attemptsList) - 1]]; } else { - // 代表成绩取最高分(null 成绩不参与比较,全部为 null 时回退为最近一份) + // 代表成绩 = 最高分(null 成绩不参与比较,全部为 null 时回退为最近一份) $best = null; $bestScore = null; foreach ($sorted as $a) { @@ -488,10 +491,14 @@ class DataService $best = $sorted[count($sorted) - 1]; } $row = $best; - $row['attempts'] = count($list); - $row['best_score'] = $bestScore; - $row['attempts_list'] = $attemptsList; } + + // 关键修复:考试次数与历史明细始终使用真实全部提交,与去重无关 + // (之前 dedupe=true 时把 attempts 写成 1、attempts_list 只留最新一份,导致"考试次数"和 + // 历史弹窗都只显示 1 条,多次刷题的数据被吞掉) + $row['attempts'] = count($list); + $row['best_score'] = $maxScore; // 真实最高分(跨全部尝试),用于历史弹窗标题展示 + $row['attempts_list'] = $attemptsList; // 最近一次提交时间(与"代表成绩"可能是不同的一次考试): // 用于"按提交时间排序"和"新提交高亮",保证重考(哪怕分数更低)也能被发现 $row['last_submit_ts'] = $lastSubmitTs; diff --git a/tests/test-aggregate.php b/tests/test-aggregate.php new file mode 100644 index 0000000..5317dc0 --- /dev/null +++ b/tests/test-aggregate.php @@ -0,0 +1,94 @@ + 新),分数有高有低。 + */ +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"; +echo "通过 {$pass} · 失败 {$fail}\n"; +exit($fail === 0 ? 0 : 1);