feat(export): 多考试对比导出(横向交叉表)

新增 export_multi 接口与多考试选择弹窗:
- 第一行显示考试名称,前两列固定为学号/姓名,默认按学号排序
- 每场考试成绩可选「最新成绩」或「最高成绩」(latest 跳过 temp_save 草稿,highest 取跨全部尝试最高分)
- 学生取并集(任一考试有提交即纳入),未考的考试单元格留空
- 纯逻辑抽离到 lib/multi_export.php,新增 26 项回归测试(tests/test-multi-export.php)
This commit is contained in:
2026-09-25 08:32:24 +08:00
parent f661862d46
commit 0ba9c7c33c
7 changed files with 400 additions and 1 deletions
+48
View File
@@ -5,6 +5,7 @@
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require __DIR__ . '/lib/multi_export.php';
$action = (string)($_GET['action'] ?? $_POST['action'] ?? '');
@@ -64,6 +65,11 @@ function ny_absent_sheet_rows(array $absent): array
return $out;
}
/**
* 多考试对比导出(成绩交叉矩阵)的纯逻辑见 lib/multi_export.php
* (ny_exam_cell_score / ny_assemble_multi_exam_matrix),便于在 api.php 与单测中复用。
*/
/**
* 带短期缓存的数据构建(含并发锁,避免多人同时刷新时重复打卷王造成惊群)
*
@@ -854,6 +860,48 @@ try {
echo $bin;
exit;
// ---------------- 多考试对比导出(横向交叉表) ----------------
case 'export_multi':
$idsRaw = (string)($_GET['projectIds'] ?? '');
$ids = array_values(array_unique(array_filter(
array_map('trim', explode(',', $idsRaw)),
static fn($v): bool => $v !== ''
)));
if ($ids === []) {
ny_fail('请至少选择一个问卷 / 考试');
}
$nameQid = (string)($_GET['nameQid'] ?? '');
$scoreMode = ((string)($_GET['scoreMode'] ?? '') === 'highest') ? 'highest' : 'latest';
$c = cfg();
// 逐场考试取数(复用与 data 接口相同的缓存),按选择顺序组装交叉矩阵
$exams = [];
foreach ($ids as $pid) {
$built = ny_build_data($c, $pid, $nameQid, !empty($_GET['force']));
$result = $built['result'];
$exams[$pid] = [
'name' => (string)($result['project']['name'] ?? $pid),
'rows' => (array)($result['rows'] ?? []),
];
}
$matrix = ny_assemble_multi_exam_matrix($exams, $scoreMode);
$sheetRows = array_merge([$matrix['head']], $matrix['rows']);
$widths = array_merge([20, 14], array_fill(0, count($matrix['exam_order']), 12));
$sheets = [['name' => '多考试对比', 'rows' => $sheetRows, 'widths' => $widths]];
$safe = static fn(string $s): string => preg_replace('/[\\\\\\/:*?"<>|]/u', '_', $s) ?? '';
$filename = '多考试成绩对比_' . count($ids) . '场_'
. ($scoreMode === 'highest' ? '最高分' : '最新成绩') . '_' . date('Ymd_His') . '.xlsx';
$bin = XlsxWriter::renderMulti($sheets);
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="export.xlsx"; filename*=UTF-8\'\'' . rawurlencode($filename));
header('Content-Length: ' . strlen($bin));
header('Cache-Control: no-store');
echo $bin;
exit;
// ---------------- 随机抽取:导出白名单(xlsx,列:姓名 / 登陆账号) ----------------
case 'draw_export':
$raw = ny_request_body();