新增 export_multi 接口与多考试选择弹窗: - 第一行显示考试名称,前两列固定为学号/姓名,默认按学号排序 - 每场考试成绩可选「最新成绩」或「最高成绩」(latest 跳过 temp_save 草稿,highest 取跨全部尝试最高分) - 学生取并集(任一考试有提交即纳入),未考的考试单元格留空 - 纯逻辑抽离到 lib/multi_export.php,新增 26 项回归测试(tests/test-multi-export.php)
137 lines
6.1 KiB
PHP
137 lines
6.1 KiB
PHP
<?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);
|