新),tempMarks 标记对应提交是否为草稿。 */ function stuRow(string $sid, string $name, array $scoresAsc, array $tempMarks = [], ?float $best = null): array { $list = []; $t = 1000000; // 不用数字分隔符,保证 PHP 7.3+ 都能跑回归测试 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); /* ---------------- 「每次考试」块表(两行表头 / 合并单元格 / 未考) ---------------- */ /** * 行内容比较:数据行是「参差不齐」的(某场次数少就没有尾巴上的格子), * 短的一侧按空串补齐到等长再逐格比,避免因为"少几个空格子"误判。 */ function rowEq(array $a, array $b): bool { $n = max(count($a), count($b)); for ($i = 0; $i < $n; $i++) { $x = isset($a[$i]) ? (string)$a[$i] : ''; $y = isset($b[$i]) ? (string)$b[$i] : ''; if ($x !== $y) { return false; } } return true; } /** merges 转成 "A2:A4" 形式,便于与期望值比较(不依赖生成顺序) */function mergeRefs(array $merges): array { $col = static function (int $n): string { $s = ''; while ($n > 0) { $m = ($n - 1) % 26; $s = chr(65 + $m) . $s; $n = (int)(($n - $m) / 26); } return $s; }; $out = []; foreach ($merges as $m) { $out[] = $col($m[1]) . $m[0] . ':' . $col($m[3]) . $m[2]; } sort($out); return $out; } /** 构造带班级 / 组别的学生行 */ function stuRowCG(string $sid, string $name, string $cls, string $grp, array $scoresAsc, array $tempMarks = [], ?float $best = null): array { $r = stuRow($sid, $name, $scoresAsc, $tempMarks, $best); $r['cls'] = $cls; $r['grp'] = $grp; return $r; } // 版式场景完全对齐需求截图:张三 4 次数学 / 2 次英语;李四只有英语(数学「未考」) $shotMath = ['name' => '数学', 'rows' => [ stuRow('356001', '张三', [112.0, 100.0, 136.0, 127.0], [], 136.0), ]]; $shotEng = ['name' => '英语', 'rows' => [ stuRow('356001', '张三', [112.0, 100.0], [], 112.0), stuRow('356002', '李四', [130.0, 129.0, 133.0], [], 133.0), ]]; echo "== 每次考试块表:两行表头 / 共享第几次 / 未考(截图一版式) ==\n"; $bs = ny_assemble_block_sheet(['m' => $shotMath, 'e' => $shotEng]); $rows = $bs['rows']; check('表头第 1 行:学号 / 姓名 / 第几次 / 考试名(每场 1 列)', $rows[0] === ['学号', '姓名', '第几次', '数学', '英语'], json_encode($rows[0], JSON_UNESCAPED_UNICODE)); check('表头第 2 行:只有「成绩」小标题', $rows[1] === ['', '', '', '成绩', '成绩'], json_encode($rows[1], JSON_UNESCAPED_UNICODE)); check('header_rows = 2', $bs['header_rows'] === 2); /* 每场考试固定占 1 列(历史上每场占 2 列,第 2 列是遗留空位, 表现为"每场右边多出一根空白列");「第几次」逐行写各自次序号,不纵向合并 (合并会把 2、3 吞掉,只剩首行的 1)。 */ check('张三块首行:第几次=1,两场成绩(每场 1 列)', rowEq($rows[2], ['356001', '张三', '1', '112', '112']), json_encode($rows[2], JSON_UNESCAPED_UNICODE)); check('张三第 2 行:第几次=2,数学 100 / 英语 100', rowEq($rows[3], ['', '', '2', '100', '100']), json_encode($rows[3], JSON_UNESCAPED_UNICODE)); /* 严格校验:3 个前导列 + 每场 1 列,所有行宽必须完全一致。 rowEq 会把短的一侧按空串补齐,宽松比较测不出「多一根空白列」这种回归。 */ check('每场考试只占 1 列:所有行宽一致 = 3 + 考试数', count($rows) === 9 && !array_filter(array_map('count', $rows), static fn($n) => $n !== 5), '列数序列 = ' . json_encode(array_map('count', $rows))); check('张三第 3 行:第几次=3,数学 136 / 英语空', $rows[4] === ['', '', '3', '136', ''], json_encode($rows[4], JSON_UNESCAPED_UNICODE)); check('张三第 4 行:第几次=4,数学 127 / 英语空', $rows[5] === ['', '', '4', '127', ''], json_encode($rows[5], JSON_UNESCAPED_UNICODE)); check('李四块首行:数学未考 + 英语 130', rowEq($rows[6], ['356002', '李四', '1', '未考', '130']), json_encode($rows[6], JSON_UNESCAPED_UNICODE)); check('李四第 2 / 3 行:第几次 2、3(未考列在合并区内留空)', $rows[7] === ['', '', '2', '', '129'] && $rows[8] === ['', '', '3', '', '133'], json_encode([$rows[7], $rows[8]], JSON_UNESCAPED_UNICODE)); check('「第几次」逐行递增,不是被合并压成的一串 1', array_map(static fn($r) => (string)$r[2], array_slice($rows, 2)) === ['1', '2', '3', '4', '1', '2', '3'], json_encode(array_map(static fn($r) => $r[2], array_slice($rows, 2)), JSON_UNESCAPED_UNICODE)); check('总行数 = 9(2 行表头 + 张三 4 + 李四 3)', count($rows) === 9, (string)count($rows)); check('total_rows / students 统计正确', $bs['total_rows'] === 7 && $bs['students'] === 2, $bs['total_rows'] . '/' . $bs['students']); check('未启用合格行时 pass_row=false', $bs['pass_row'] === false, var_export($bs['pass_row'], true)); $refs = mergeRefs($bs['merges']); check('合并区间:表头纵向 + 学号姓名纵向 + 未考单列纵向', $refs === ['A1:A2', 'A3:A6', 'A7:A9', 'B1:B2', 'B3:B6', 'B7:B9', 'C1:C2', 'D7:D9'], json_encode($refs)); check('「第几次」列没有任何纵向合并(表头 C1:C2 除外)', !array_filter($bs['merges'], static fn($m) => (int)$m[0] > 1 && (int)$m[1] === 3 && (int)$m[3] === 3), json_encode($bs['merges'])); check('表头第 1 行没有任何横向合并(考试名各占 1 列)', !array_filter($bs['merges'], static fn($m) => (int)$m[0] === 1 && (int)$m[2] === 1 && $m[1] !== $m[3]), json_encode($bs['merges'])); /* 列宽够用就好:考试列要装得下表头考试名(中文按 2 个半角宽算)。 */ check('列宽 = 学号 10 / 姓名 9 / 第几次 10 / 每场考试 1 列(按考试名估宽)', $bs['widths'] === [10, 9, 10, 8, 8], json_encode($bs['widths'])); echo "== 每次考试块表:最高分标红 / 合格次数标绿 ==\n"; // 合格线 110: // 张三数学 [112,100,136,127] → 112/136/127 合格 = 3 // 张三英语 [112,100] → 112 合格 = 1 // 李四英语 [130,129,133] → 130/129/133 合格 = 3 $bp = ny_assemble_block_sheet(['m' => $shotMath, 'e' => $shotEng], ['pass_line' => 110]); $pp = $bp['rows']; check('启用合格行后 header_rows 仍为 2', $bp['header_rows'] === 2); check('pass_row=true 且 pass_line 回传', $bp['pass_row'] === true && $bp['pass_line'] === 110.0, var_export($bp['pass_line'], true)); // 张三块:4 行成绩 + 1 行合格次数(第 7 行) check('张三合格次数行:数学 3 次 / 英语 1 次', $pp[6] === ['', '', '合格次数', '3', '1'], json_encode($pp[6], JSON_UNESCAPED_UNICODE)); // 李四块:3 行成绩 + 1 行合格次数(第 11 行) check('李四合格次数行:数学未考留空 / 英语 3 次', $pp[10] === ['', '', '合格次数', '', '3'], json_encode($pp[10], JSON_UNESCAPED_UNICODE)); check('总行数 = 11(2 + (4+1) + (3+1))', count($pp) === 11, (string)count($pp)); check('total_rows 含合格次数行 = 9', $bp['total_rows'] === 9, (string)$bp['total_rows']); $pm = $bp['cell_styles']; // 张三数学最高分 136 在第 5 行第 4 列 → 红字 check('张三数学最高分 136 标红', ($pm['5,4'] ?? '') === 'red', json_encode($pm)); // 张三英语最高分 112 在第 3 行第 5 列 → 红字 check('张三英语最高分 112 标红', ($pm['3,5'] ?? '') === 'red', json_encode($pm)); // 李四英语最高分 133 在第 10 行第 5 列 → 红字 check('李四英语最高分 133 标红', ($pm['10,5'] ?? '') === 'red', json_encode($pm)); // 张三合格次数(第 7 行):数学 3 达标绿底、英语 1 达标绿底 check('张三数学合格次数达标绿底', ($pm['7,4'] ?? '') === 'green', json_encode($pm)); check('张三英语合格次数达标绿底', ($pm['7,5'] ?? '') === 'green', json_encode($pm)); // 李四:数学未考 → 不标绿;英语 3 次达标绿底 check('李四数学未考不标绿', !isset($pm['11,4']), json_encode($pm)); check('李四英语合格次数达标绿底', ($pm['11,5'] ?? '') === 'green', json_encode($pm)); // 未达标(阈值 4 次)→ 仍显示实际次数(张三数学 3 / 英语 1),但不标绿 $bh = ny_assemble_block_sheet(['m' => $shotMath, 'e' => $shotEng], ['pass_line' => 110, 'pass_times' => 4]); $hp = $bh['rows']; check('阈值 4 次时张三仍显示实际合格次数(不隐藏)', $hp[6] === ['', '', '合格次数', '3', '1'], json_encode($hp[6], JSON_UNESCAPED_UNICODE)); check('阈值 4 次时不达标不标绿', !isset($bh['cell_styles']['7,4']) && !isset($bh['cell_styles']['7,5']), json_encode($bh['cell_styles'])); echo "== 每次考试块表:草稿 / 多班级组别列 / 空考试 ==\n"; // 赵六:数学第 2 次是草稿 → 默认跳过(不占编号) $draftMath = ['name' => '数学', 'rows' => [ stuRow('D1', '赵六', [60.0, 99.0], [false, true], 99.0), ]]; $ds = ny_assemble_block_sheet(['m' => $draftMath]); check('默认跳过草稿:只剩 1 次', rowEq($ds['rows'][2], ['D1', '赵六', '1', '60']), json_encode($ds['rows'][2], JSON_UNESCAPED_UNICODE)); check('跳过草稿后无多余行', count($ds['rows']) === 3, (string)count($ds['rows'])); $di = ny_assemble_block_sheet(['m' => $draftMath], ['skip_temp' => false]); check('保留草稿:2 次且草稿分参与', rowEq($di['rows'][3], ['', '', '2', '99']), json_encode($di['rows'][3], JSON_UNESCAPED_UNICODE)); // 只有草稿的学生:跳过草稿后整场无有效成绩 → 不出现在表里 $onlyDraft = ['name' => '数学', 'rows' => [ stuRow('X1', '只交草稿', [88.0], [true], 88.0), stuRow('X2', '正常', [70.0], [], 70.0), ]]; $od = ny_assemble_block_sheet(['m' => $onlyDraft]); check('全草稿学生被剔除(只剩正常那位)', $od['students'] === 1, (string)$od['students']); check('全草稿学生未写「未考」占位', rowEq($od['rows'][2], ['X2', '正常', '1', '70']), json_encode($od['rows'][2], JSON_UNESCAPED_UNICODE)); // 多班级 / 多组别:按 班级 → 组别 → 学号 排序,连续相同班级 / 组别合并 $multi = ['name' => '数学', 'rows' => [ stuRowCG('S3', '王五', '1班', 'B', [60.0]), stuRowCG('S4', '赵六', '2班', 'B', [50.0]), stuRowCG('S1', '张三', '1班', 'A', [90.0, 80.0]), stuRowCG('S2', '李四', '1班', 'A', [70.0]), ]]; $mc = ny_assemble_block_sheet(['m' => $multi], ['with_cls' => true, 'with_grp' => true]); check('多班级表头 = 班级/组别/学号/姓名/第几次/考试名', $mc['rows'][0] === ['班级', '组别', '学号', '姓名', '第几次', '数学'], json_encode($mc['rows'][0], JSON_UNESCAPED_UNICODE)); check('按 班级 → 组别 → 学号 排序', [$mc['rows'][2][2], $mc['rows'][4][2], $mc['rows'][5][2], $mc['rows'][6][2]] === ['S1', 'S2', 'S3', 'S4'], json_encode([$mc['rows'][2][2], $mc['rows'][4][2], $mc['rows'][5][2], $mc['rows'][6][2]])); check('班级 / 组别只写在学生块首行', rowEq($mc['rows'][2], ['1班', 'A', 'S1', '张三', '1', '90']), json_encode($mc['rows'][2], JSON_UNESCAPED_UNICODE)); $mcRefs = mergeRefs($mc['merges']); check('相同班级 / 相同组别的单元格合并(1班 S1~S3 归一格,A 组 S1~S2 归一格)', in_array('A3:A6', $mcRefs, true) && in_array('B3:B5', $mcRefs, true), json_encode($mcRefs)); check('班级列位移后学号 / 姓名合并列正确(C / D 列)', in_array('C3:C4', $mcRefs, true) && in_array('D3:D4', $mcRefs, true), json_encode($mcRefs)); // 空考试:只出两行表头 $bEmpty = ny_assemble_block_sheet(['none' => ['name' => '无人考试', 'rows' => []]]); check('空考试只有两行表头', count($bEmpty['rows']) === 2 && $bEmpty['students'] === 0); check('空考试仅剩表头合并(考试名不再横向合并)', mergeRefs($bEmpty['merges']) === ['A1:A2', 'B1:B2', 'C1:C2'], json_encode(mergeRefs($bEmpty['merges']))); // 考试列顺序跟随传入顺序 $bOrder = ny_assemble_block_sheet(['e' => $shotEng, 'm' => $shotMath]); check('考试列顺序跟随传入顺序(英语在前)', $bOrder['rows'][0][3] === '英语' && $bOrder['rows'][0][4] === '数学', json_encode($bOrder['rows'][0], JSON_UNESCAPED_UNICODE)); echo "== 单场「每次考试」宽表(截图二版式) ==\n"; $w = ny_assemble_attempt_wide_sheet($shotEng, ['pass_line' => 110]); check('表头 = 学号/姓名/第1次…第N次/合格次数', $w['rows'][0] === ['学号', '姓名', '第1次', '第2次', '第3次', '合格次数'], json_encode($w['rows'][0], JSON_UNESCAPED_UNICODE)); check('每人一行,共 2 人', count($w['rows']) === 3 && $w['students'] === 2, (string)count($w['rows'])); // 张三英语 [112,100]:只有 2 次,第 3 列留空;≥110 → 112 合格 = 1 check('张三行:112 / 100 / 空,合格 1 次', $w['rows'][1] === ['356001', '张三', '112', '100', '', '1'], json_encode($w['rows'][1], JSON_UNESCAPED_UNICODE)); // 李四英语 [130,129,133]:全 ≥110 = 3 次合格 check('李四行:130 / 129 / 133,合格 3 次', $w['rows'][2] === ['356002', '李四', '130', '129', '133', '3'], json_encode($w['rows'][2], JSON_UNESCAPED_UNICODE)); check('attempt_cols=3', $w['attempt_cols'] === 3, (string)$w['attempt_cols']); check('最高分 112(张三第 3 列)标红', ($w['cell_styles']['2,3'] ?? '') === 'red', json_encode($w['cell_styles'])); check('最高分 133(李四第 5 列)标红', ($w['cell_styles']['3,5'] ?? '') === 'red', json_encode($w['cell_styles'])); check('张三合格次数 1 次达标绿底', ($w['cell_styles']['2,6'] ?? '') === 'green', json_encode($w['cell_styles'])); check('李四合格次数 3 次达标绿底', ($w['cell_styles']['3,6'] ?? '') === 'green', json_encode($w['cell_styles'])); // 阈值 4 次 → 都不到阈值,仍显示实际次数(1 / 3),但不标绿 $w4 = ny_assemble_attempt_wide_sheet($shotEng, ['pass_line' => 110, 'pass_times' => 4]); check('阈值 4 次时仍显示实际合格次数(不隐藏)', $w4['rows'][1][5] === '1' && $w4['rows'][2][5] === '3', json_encode([$w4['rows'][1][5], $w4['rows'][2][5]], JSON_UNESCAPED_UNICODE)); check('阈值 4 次时不标绿', !isset($w4['cell_styles']['2,6']) && !isset($w4['cell_styles']['3,6']), json_encode($w4['cell_styles'])); // 关闭标红 / 关掉合格行 $wOff = ny_assemble_attempt_wide_sheet($shotEng, ['pass_line' => 110, 'mark_best' => false, 'pass_row' => false]); check('mark_best=false 不标红', $wOff['cell_styles'] === [], json_encode($wOff['cell_styles'])); check('pass_row=false 表头无合格次数', !in_array('合格次数', $wOff['rows'][0], true), json_encode($wOff['rows'][0], JSON_UNESCAPED_UNICODE)); /** 检查合并区间两两不重叠(区间重叠会让 Excel 打开时报"需要修复") */ function overlapsOf(array $merges): array { $bad = []; $n = count($merges); for ($i = 0; $i < $n; $i++) { for ($j = $i + 1; $j < $n; $j++) { $a = $merges[$i]; $b = $merges[$j]; if ($a[0] <= $b[2] && $b[0] <= $a[2] && $a[1] <= $b[3] && $b[1] <= $a[3]) { $bad[] = json_encode($a) . '×' . json_encode($b); } } } return $bad; } echo "== 班级 / 组别列的显示规则 ==\n"; $f1 = ny_multi_col_flags(['1班'], [], ['1班'], ['A', 'B']); check('只选 1 个班级且未选组别 → 班级 / 组别列都不出', $f1['with_cls'] === false && $f1['with_grp'] === false, json_encode($f1)); $f2 = ny_multi_col_flags(['1班', '2班'], ['A', 'B'], ['1班', '2班'], ['A', 'B']); check('选了多班级 + 多组别 → 两列都出', $f2['with_cls'] === true && $f2['with_grp'] === true, json_encode($f2)); $f3 = ny_multi_col_flags(['1班'], ['A', 'B'], ['1班'], ['A', 'B']); check('只选 1 个班级但选了多组别 → 只出组别列', $f3['with_cls'] === false && $f3['with_grp'] === true, json_encode($f3)); $f4 = ny_multi_col_flags([], [], ['1班'], ['A']); check('未筛选且数据只有 1 个班 / 1 个组 → 两列都不出', $f4['with_cls'] === false && $f4['with_grp'] === false, json_encode($f4)); $f5 = ny_multi_col_flags([], [], ['1班', '2班', '3班'], ['A', 'B']); check('未筛选且数据跨 3 个班 → 按数据补出班级 / 组别列', $f5['with_cls'] === true && $f5['with_grp'] === true, json_encode($f5)); $f6 = ny_multi_col_flags(['1班', '2班'], ['A'], ['1班', '2班'], ['A']); check('多班级 + 只选 1 个组别 → 只出班级列', $f6['with_cls'] === true && $f6['with_grp'] === false, json_encode($f6)); echo "== 合并区间不重叠(防 Excel 报修复) ==\n"; check('截图版式:无重叠', overlapsOf($bs['merges']) === [], json_encode(overlapsOf($bs['merges']))); check('多班级多组别:无重叠', overlapsOf($mc['merges']) === [], json_encode(overlapsOf($mc['merges']))); check('单场导出(带班级组别列):无重叠', overlapsOf(ny_assemble_block_sheet(['m' => $multi], ['with_cls' => true])['merges']) === []); check('草稿模式:无重叠', overlapsOf(ny_assemble_block_sheet(['m' => $draftMath], ['skip_temp' => false])['merges']) === []); echo "\n通过 {$pass} 项,失败 {$fail} 项\n"; exit($fail === 0 ? 0 : 1);