Files
ny-exam-platform/tests/test-multi-export.php
T
nxxy335 0ba9c7c33c feat(export): 多考试对比导出(横向交叉表)
新增 export_multi 接口与多考试选择弹窗:
- 第一行显示考试名称,前两列固定为学号/姓名,默认按学号排序
- 每场考试成绩可选「最新成绩」或「最高成绩」(latest 跳过 temp_save 草稿,highest 取跨全部尝试最高分)
- 学生取并集(任一考试有提交即纳入),未考的考试单元格留空
- 纯逻辑抽离到 lib/multi_export.php,新增 26 项回归测试(tests/test-multi-export.php)
2026-09-25 08:32:24 +08:00

137 lines
6.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 多考试对比导出(成绩交叉矩阵)回归测试
*
* 覆盖 ny_exam_cell_score / ny_assemble_multi_exam_matrix:
* 1) 矩阵结构:第一行 = 考试名称,前两列 = 学号 / 姓名
* 2) 默认按学号升序
* 3) 最新成绩 = attempts_list 中最近一次「非暂存」提交;最高成绩 = 跨全部尝试的最高分
* 4) 草稿(temp_save)不计入"最新成绩",但计入"最高成绩"
* 5) 重考分数更低时,最新≠最高
* 6) 未考的考试列为空;学生并集(任一考试有提交即纳入)
* 7) 同一学号跨考试姓名以首个非空为准
*/
require __DIR__ . '/../lib/multi_export.php';
$fail = 0;
$pass = 0;
function check(string $name, bool $cond, string $detail = ''): void
{
global $fail, $pass;
if ($cond) {
$pass++;
echo " ✅ {$name}\n";
} else {
$fail++;
echo " ❌ {$name} {$detail}\n";
}
}
/**
* 构造一名学生的聚合行(与 DataService::aggregateByStudent 产出的结构保持一致)。
* 分数按时间升序给出(旧 -> 新),tempMarks 标记对应提交是否为草稿。
*/
function stuRow(string $sid, string $name, array $scoresAsc, array $tempMarks = [], ?float $best = null): array
{
$list = [];
$t = 1_000_000;
foreach ($scoresAsc as $i => $sc) {
$t += 600;
$list[] = [
'idx' => $i + 1,
'submit_ts' => $t,
'score' => $sc,
'temp_save' => !empty($tempMarks[$i]),
'duration' => 120,
];
}
return [
'sid' => $sid,
'name' => $name,
'cls' => '1班',
'grp' => 'A',
'score' => end($scoresAsc),
'best_score' => $best ?? (float)max($scoresAsc),
'attempts' => count($scoresAsc),
'attempts_list'=> $list,
];
}
/* ---------------- 场景 ---------------- */
$examMath = [
'name' => '数学期中',
'rows' => [
// 张三:第一次 90,重考降到 80(最新≠最高);草稿 95 只算最高
stuRow('2021001', '张三', [90.0, 80.0], [false, false], 90.0),
stuRow('2021002', '李四', [70.0]),
// 草稿分:time1 真实 60,time2 草稿 99 → 最新应取 60,最高取 99
stuRow('2021004', '赵六', [60.0, 99.0], [false, true], 99.0),
],
];
$examEnglish = [
'name' => '英语期中',
'rows' => [
stuRow('2021001', '张三', [85.0]),
stuRow('2021003', '王五', [60.0]),
],
];
echo "== 最新成绩矩阵 ==\n";
$m = ny_assemble_multi_exam_matrix(['math' => $examMath, 'eng' => $examEnglish], 'latest');
check('表头第一列是学号', ($m['head'][0] ?? '') === '学号', json_encode($m['head']));
check('表头第二列是姓名', ($m['head'][1] ?? '') === '姓名');
check('表头第三列起是考试名称', ($m['head'][2] ?? '') === '数学期中' && ($m['head'][3] ?? '') === '英语期中', json_encode($m['head']));
check('考试列顺序保持选择顺序', $m['exam_order'] === ['math', 'eng']);
// 学号排序:2021001,2021002,2021003,2021004
// 注意:PHP 会把数字字符串键自动转成 int('2021001' -> 2021001),排序结果本身正确,
// 这里用 strval 归一后比较排序顺序(显示层 XlsxWriter 会再转回字符串,无影响)。
$sids = array_map('strval', array_column($m['rows'], 0));
check('按学号升序', $sids === ['2021001', '2021002', '2021003', '2021004'], json_encode($sids));
// 张三:数学最新=80(重考降分,跳过草稿),英语最新=85
$zhang = $m['rows'][0];
check('张三行:姓名正确', $zhang[1] === '张三');
check('张三-数学最新=80(重考降分)', (string)$zhang[2] === '80', (string)$zhang[2]);
check('张三-英语最新=85', (string)$zhang[3] === '85', (string)$zhang[3]);
// 李四只在数学:英语为空
$li = $m['rows'][1];
check('李四-数学=70', (string)$li[2] === '70', (string)$li[2]);
check('李四-英语为空', $li[3] === '', var_export($li[3], true));
// 王五只在英语:数学为空
$wang = $m['rows'][2];
check('王五-数学为空', $wang[2] === '', var_export($wang[2], true));
check('王五-英语=60', (string)$wang[3] === '60', (string)$wang[3]);
// 赵六:数学最新跳过草稿=60
$zhao = $m['rows'][3];
check('赵六-数学最新跳过草稿=60', (string)$zhao[2] === '60', (string)$zhao[2]);
echo "== 最高成绩矩阵 ==\n";
$mh = ny_assemble_multi_exam_matrix(['math' => $examMath, 'eng' => $examEnglish], 'highest');
$zhangH = $mh['rows'][0];
check('张三-数学最高=90(含重考)', (string)$zhangH[2] === '90', (string)$zhangH[2]);
check('张三-英语最高=85', (string)$zhangH[3] === '85', (string)$zhangH[3]);
$zhaoH = $mh['rows'][3];
check('赵六-数学最高含草稿=99', (string)$zhaoH[2] === '99', (string)$zhaoH[2]);
echo "== 跨考试姓名 / 空考试 ==\n";
// 仅一场考试时仍可正常产出;姓名取首个非空
$single = ny_assemble_multi_exam_matrix(['math' => $examMath], 'latest');
check('单场考试表头正确', $single['head'] === ['学号', '姓名', '数学期中']);
check('单场考试行数=3', count($single['rows']) === 3, (string)count($single['rows']));
echo "== ny_exam_cell_score 单测 ==\n";
$r0 = stuRow('X', 'Y', [10.0, 20.0]); // latest=20, highest=20
check('latest=20', ny_exam_cell_score($r0, 'latest') === 20.0);
check('highest=20', ny_exam_cell_score($r0, 'highest') === 20.0);
$r1 = stuRow('X', 'Y', [20.0, 10.0]); // 重考降分
check('latest 取最近=10', ny_exam_cell_score($r1, 'latest') === 10.0);
check('highest 取最高=20', ny_exam_cell_score($r1, 'highest') === 20.0);
$r2 = stuRow('X', 'Y', [50.0, 88.0], [false, true]); // 草稿
check('latest 跳过草稿=50', ny_exam_cell_score($r2, 'latest') === 50.0);
check('highest 含草稿=88', ny_exam_cell_score($r2, 'highest') === 88.0);
$r3 = ['sid' => 'X', 'name' => 'Y', 'score' => 77.0, 'best_score' => 77.0]; // 无明细
check('无明细 latest 退回代表行=77', ny_exam_cell_score($r3, 'latest') === 77.0);
check('无明细 highest=77', ny_exam_cell_score($r3, 'highest') === 77.0);
echo "\n通过 {$pass} 项,失败 {$fail} 项\n";
exit($fail === 0 ? 0 : 1);