成绩明细历史记录:按提交年月日分类,每个分类标记最高分
新增 DataService::dayKeyOf(),日期键一律用服务端本地时区计算(避免 UTC 导致东八区零点前后错一天);aggregateByStudent 为 attempts_list 每项附加 day/day_best/day_count,expandAttempts 透传。 弹窗按天分组渲染分组行(日期 + 共N次 + 最高X分),组内新到旧;当日最高标签仅在当天有多次提交时显示,避免单次日每行都挂标签。 修复:submit_ts=0(时间未知)的记录原本被误标为第1次(升序排序 idx=1 但展示在末尾),现显示 -;未知时间统一归入「未知时间」组。 aggregate 单测 17 -> 33(跨天归属/日内次数/每日最高/未知时间组/时区边界 00:30 与 23:30);全量 148 项全绿。
This commit is contained in:
+76
-20
@@ -463,8 +463,11 @@ class DataService
|
||||
if ($list === []) {
|
||||
// 没有明细时退回代表行本身
|
||||
$one = $r;
|
||||
$one['idx'] = 1;
|
||||
$one['is_best'] = true;
|
||||
$one['idx'] = 1;
|
||||
$one['is_best'] = true;
|
||||
$one['day'] = self::dayKeyOf((int)($r['submit_ts'] ?? 0), (string)($r['submit_at'] ?? ''));
|
||||
$one['day_best'] = true;
|
||||
$one['day_count'] = 1;
|
||||
$out[] = $one;
|
||||
continue;
|
||||
}
|
||||
@@ -485,6 +488,9 @@ class DataService
|
||||
'best_score' => $r['best_score'] ?? null,
|
||||
'idx' => (int)($a['idx'] ?? 1),
|
||||
'is_best' => !empty($a['is_best']),
|
||||
'day' => (string)($a['day'] ?? ''),
|
||||
'day_best' => !empty($a['day_best']),
|
||||
'day_count' => (int)($a['day_count'] ?? 1),
|
||||
'last_submit_ts' => (int)($r['last_submit_ts'] ?? 0),
|
||||
'last_submit_at' => (string)($r['last_submit_at'] ?? '-'),
|
||||
];
|
||||
@@ -552,6 +558,28 @@ class DataService
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某次提交的「年月日」分组键。
|
||||
*
|
||||
* 关键:一律用服务端本地时区 date('Y-m-d', $ts) 计算,**不能用 UTC**(gmdate / JS toISOString),
|
||||
* 否则零点前后提交的答卷会被算到前一天/后一天(东八区会整体错一天)。
|
||||
* 无有效时间戳(createAt 缺失导致 ny_to_timestamp 返回 0)时,退回从显示串取前 10 位;
|
||||
* 仍取不到则返回空串,上层归为「未知时间」分组并排在最后。
|
||||
*
|
||||
* @param int $ts 提交时间戳(秒),0 表示未知
|
||||
* @param string $submitAt 展示用时间串(形如 2026-09-08 09:41:20)
|
||||
*/
|
||||
public static function dayKeyOf(int $ts, string $submitAt = ''): string
|
||||
{
|
||||
if ($ts > 0) {
|
||||
return date('Y-m-d', $ts);
|
||||
}
|
||||
if ($submitAt !== '' && $submitAt !== '-' && preg_match('/^(\d{4}-\d{2}-\d{2})/', $submitAt, $m) === 1) {
|
||||
return $m[1];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 按学号聚合一次提交的答卷记录,解决"同人多次刷卷"在看板中被当成多个人的问题。
|
||||
*
|
||||
@@ -589,27 +617,55 @@ class DataService
|
||||
}
|
||||
// 最近一次提交(升序排序后的最后一份)
|
||||
$lastSubmitTs = (int)($sorted[count($sorted) - 1]['submit_ts'] ?? 0);
|
||||
|
||||
// 按「提交时间年月日」分组统计:每天的最高分与提交次数。
|
||||
// 历史弹窗据此按天分类,并在每个分类内标记当日最高分。
|
||||
$dayMax = []; // day => 当天最高分
|
||||
$dayCount = []; // day => 当天提交次数(含未判分)
|
||||
foreach ($sorted as $a) {
|
||||
$day = self::dayKeyOf((int)($a['submit_ts'] ?? 0), (string)($a['submit_at'] ?? ''));
|
||||
$dayCount[$day] = ($dayCount[$day] ?? 0) + 1;
|
||||
if ($a['score'] !== null && (!array_key_exists($day, $dayMax) || $a['score'] > $dayMax[$day])) {
|
||||
$dayMax[$day] = $a['score'];
|
||||
}
|
||||
}
|
||||
|
||||
// 标记是否已给"最佳"打过标(同分时只标最早达到该分数的那次)
|
||||
$bestMarked = false;
|
||||
// 每天是否已标记过"当日最高"(同分时同样只标最早达到的那次,与全局 is_best 一致)
|
||||
$dayMarked = [];
|
||||
$idx = 0;
|
||||
$attemptsList = array_map(static function (array $a) use (&$idx, $maxScore, &$bestMarked): array {
|
||||
$idx++;
|
||||
$isBest = (!$bestMarked && $a['score'] !== null && $maxScore !== null && (float)$a['score'] === (float)$maxScore);
|
||||
if ($isBest) {
|
||||
$bestMarked = true;
|
||||
}
|
||||
return [
|
||||
'idx' => $idx,
|
||||
'is_best' => $isBest,
|
||||
'submit_at' => $a['submit_at'],
|
||||
'submit_ts' => $a['submit_ts'],
|
||||
'score' => $a['score'],
|
||||
'duration' => $a['duration'],
|
||||
'duration_txt' => $a['duration_txt'],
|
||||
'answer_id' => $a['answer_id'],
|
||||
'temp_save' => $a['temp_save'],
|
||||
];
|
||||
}, $sorted);
|
||||
$attemptsList = array_map(
|
||||
static function (array $a) use (&$idx, $maxScore, &$bestMarked, &$dayMarked, $dayMax, $dayCount): array {
|
||||
$idx++;
|
||||
$isBest = (!$bestMarked && $a['score'] !== null && $maxScore !== null && (float)$a['score'] === (float)$maxScore);
|
||||
if ($isBest) {
|
||||
$bestMarked = true;
|
||||
}
|
||||
$day = self::dayKeyOf((int)($a['submit_ts'] ?? 0), (string)($a['submit_at'] ?? ''));
|
||||
$dayBest = ($a['score'] !== null && array_key_exists($day, $dayMax)
|
||||
&& (float)$a['score'] === (float)$dayMax[$day] && empty($dayMarked[$day]));
|
||||
if ($dayBest) {
|
||||
$dayMarked[$day] = true;
|
||||
}
|
||||
return [
|
||||
'idx' => $idx,
|
||||
'is_best' => $isBest,
|
||||
'submit_at' => $a['submit_at'],
|
||||
'submit_ts' => $a['submit_ts'],
|
||||
'score' => $a['score'],
|
||||
'duration' => $a['duration'],
|
||||
'duration_txt' => $a['duration_txt'],
|
||||
'answer_id' => $a['answer_id'],
|
||||
'temp_save' => $a['temp_save'],
|
||||
// 按天分类用:day 为空串表示时间未知
|
||||
'day' => $day,
|
||||
'day_best' => $dayBest,
|
||||
'day_count' => (int)($dayCount[$day] ?? 1),
|
||||
];
|
||||
},
|
||||
$sorted
|
||||
);
|
||||
|
||||
// 代表成绩选择(仅影响主明细表那一行的成绩,不影响考试次数 / 历史明细)
|
||||
if ($dedupe) {
|
||||
|
||||
Reference in New Issue
Block a user