- ny_assemble_block_sheet() 中每场考试由 2 列改为 1 列,删除历史遗留的 「是否最佳」空白列,消除每场右侧多余空白窄列 - 删除「第几次」列的纵向合并,改为逐行显示 1/2/3…,不再被吞成 1 - 合格次数行同步改为每场 1 列;未考合并加 $end > $start 跨行守卫, 规避单行学生产生的非法 1x1 合并(Excel 修复提示) - 列宽按考试名估算(CJK 记 2 半角宽),clamp 8~18 - 同步更新 test-multi-export.php(95 项含 4 条防回归断言)、 make-samples.php(李四数学改为真正整场未考)、导出/接口/测试文档 及样例预览文件;全量回归 404 项 0 失败
441 lines
23 KiB
PHP
441 lines
23 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 = 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);
|
||
|