新),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);