准考证优化

This commit is contained in:
2026-07-20 14:18:37 +08:00 Unverified
parent 9566542414
commit c5b18d0b15
13 changed files with 447 additions and 57 deletions
+71 -1
View File
@@ -4,7 +4,7 @@ import { resolve } from 'node:path';
import assert from 'node:assert/strict';
import ExcelJS from 'exceljs';
import { relationalTables } from '../database.mjs';
import { buildWorkbook } from '../excel.mjs';
import { buildCenterMaterialsWorkbook, buildWorkbook } from '../excel.mjs';
const root = resolve(process.cwd());
const port = 4182;
@@ -91,9 +91,15 @@ try {
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
`).all().map(row => row.name);
const schemaVersion = inspector.prepare('SELECT schema_version FROM schema_metadata WHERE id = 1').get().schema_version;
const admitCardColumns = inspector.prepare('PRAGMA table_info(admit_cards)').all().map(row => row.name);
const admitSubjectColumns = inspector.prepare('PRAGMA table_info(admit_card_subjects)').all().map(row => row.name);
inspector.close();
assert.deepEqual(tableNames, [...relationalTables].sort(), '业务数据必须按关系模型分表存储');
assert.ok(!tableNames.includes('app_state'), '不得使用单表 JSON 状态存储');
assert.equal(schemaVersion, 10, '准考证详细位置快照应使用 v10 数据结构');
assert.ok(['center_code', 'center_address'].every(column => admitCardColumns.includes(column)), '准考证应保存考点代码与详细地址快照');
assert.ok(['building', 'floor'].every(column => admitSubjectColumns.includes(column)), '分科考场应保存楼栋楼层快照');
const publicHome = await anonymous.request('/api/public/home');
assert.equal(publicHome.response.status, 200);
@@ -406,8 +412,14 @@ try {
const generateAdmit = await admin.request(`/api/admin/exams/${exam.id}/admission-arrangement`, { method: 'POST', body: arrangementBody });
const generatedCard = generateAdmit.data.cards.find(item => item.registrationId === registrationId);
assert.ok(generatedCard.number, '管理员应能按整场考试批量生成准考证号');
assert.ok(generatedCard.centerCode && generatedCard.centerAddress, '准考证应包含考点代码和完整地址');
assert.equal(new Set(generatedCard.assignments.map(item => item.centerId)).size, 1, '同一考生所有科目必须固定在同一考点');
assert.equal(generatedCard.assignments.length, submitRegistration.data.registration.subjectIds.length, '每个报考科目都必须有独立考场座位');
assert.ok(generatedCard.assignments.every(item => item.examRoomCode && item.roomName && item.roomCode && item.building && item.floor), '逐科安排必须同时保存考试考场序号、物理场地和楼栋楼层');
const codesByPhysicalRoom = Map.groupBy
? Map.groupBy(generateAdmit.data.cards.flatMap(card => card.assignments), item => item.roomId)
: generateAdmit.data.cards.flatMap(card => card.assignments).reduce((groups, item) => groups.set(item.roomId, [...(groups.get(item.roomId) || []), item]), new Map());
assert.ok([...codesByPhysicalRoom.values()].every(items => new Set(items.map(item => item.examRoomCode)).size === 1), '同一场考试内,同一物理考场跨科目必须保持同一个考试考场序号');
assert.equal((await admin.request(`/api/admin/registrations/${registrationId}/admit-card`, { method: 'POST', body: {} })).response.status, 410, '旧单人生成入口应明确停用');
const candidateRegistrations = await candidate.request('/api/candidate/registrations');
@@ -417,8 +429,66 @@ try {
const downloadAdmit = await candidate.request(`/api/candidate/registrations/${registrationId}/admit-card`);
assert.equal(downloadAdmit.response.status, 200, '规定时间内应可下载准考证');
assert.match(downloadAdmit.data, /测试考生新名/);
assert.match(downloadAdmit.data, /考试考场序号/);
assert.match(downloadAdmit.data, /考场通用名称 \/ 场地代码/);
assert.match(downloadAdmit.data, new RegExp(generatedCard.assignments[0].building));
assert.match(downloadAdmit.data, new RegExp(generatedCard.assignments[0].floor));
assert.match(downloadAdmit.response.headers.get('content-disposition') || '', /attachment/);
const schoolAdmitContext = await schoolAdmin.request('/api/admin/admission-arrangements');
assert.equal(schoolAdmitContext.response.status, 200, '校级管理员应有校内准考证入口');
assert.equal(schoolAdmitContext.data.canArrange, false, '校级管理员不能执行整场编排');
assert.ok(schoolAdmitContext.data.registrations.every(item => item.candidate.schoolId === 'school_hz1'), '校级准考证台账必须限制为本校考生');
const classAdmitContext = await classAdmin.request('/api/admin/admission-arrangements');
assert.equal(classAdmitContext.response.status, 200, '班级管理员应有本班准考证入口');
assert.ok(classAdmitContext.data.registrations.every(item => item.candidate.classId === 'class_hz1_302'), '班级准考证台账必须限制为本班考生');
const schoolBatchCards = await schoolAdmin.request(`/api/admin/admission-exports/admit-cards?examId=${exam.id}`);
assert.equal(schoolBatchCards.response.status, 200, '校级管理员应可批量下载校内准考证');
assert.match(schoolBatchCards.response.headers.get('content-disposition') || '', /attachment/);
assert.match(schoolBatchCards.data, /考试考场序号/);
const classBatchCards = await classAdmin.request(`/api/admin/admission-exports/admit-cards?examId=${exam.id}`);
assert.equal(classBatchCards.response.status, 200, '班级管理员应可批量下载本班准考证');
assert.match(classBatchCards.data, /测试考生新名/);
const classInfoExport = await classAdmin.request(`/api/admin/admission-exports/info?examId=${exam.id}`);
assert.equal(classInfoExport.response.status, 200, '班级管理员应可导出本班准考证信息台账');
const classInfoBook = new ExcelJS.Workbook();
await classInfoBook.xlsx.load(classInfoExport.data);
const classInfoSheet = classInfoBook.getWorksheet('准考证信息');
assert.ok(classInfoSheet, '准考证信息导出应有独立工作表');
assert.equal(classInfoSheet.rowCount, generatedCard.assignments.length + 2, '准考证信息应按考生逐科导出');
assert.equal(classInfoSheet.getCell('D3').value, '测试考生新名');
assert.equal(classInfoSheet.getCell('O3').value, generatedCard.assignments[0].examRoomCode, '信息台账应明确导出考试考场序号');
assert.equal(classInfoSheet.getCell('P3').value, generatedCard.assignments[0].roomName, '信息台账应另列考场通用名称');
const schoolMaterials = await schoolAdmin.request(`/api/admin/admission-exports/center-materials?examId=${exam.id}`);
assert.equal(schoolMaterials.response.status, 200, '校级管理员应可导出本校维护考点的现场物料');
const materialsBook = new ExcelJS.Workbook();
await materialsBook.xlsx.load(schoolMaterials.data);
assert.deepEqual(materialsBook.worksheets.map(sheet => sheet.name), ['桌贴', '门贴', '考场签名单'], '考点物料应包含桌贴、门贴和考场签名单三张表');
const deskStickerSheet = materialsBook.getWorksheet('桌贴');
assert.ok(deskStickerSheet.rowCount >= 8, '每张方形桌贴应占用完整标签卡片区域');
assert.equal(deskStickerSheet.pageSetup.orientation, 'portrait', '方形桌贴应使用 A4 纵向打印');
assert.equal(deskStickerSheet.getRow(1).height, 32.5, '桌贴应使用固定行高形成方形标签');
assert.equal(deskStickerSheet.getColumn(1).width, deskStickerSheet.getColumn(3).width, '桌贴卡片各列应等宽');
assert.ok(deskStickerSheet.model.merges.includes('A1:C1'), '第一张桌贴应跨三列形成完整方形卡片');
assert.match(String(deskStickerSheet.getCell('A3').value), /考试考场序号/);
assert.equal(deskStickerSheet.getCell('A4').text, generatedCard.assignments[0].examRoomCode, '桌贴应突出考试考场序号');
assert.match(String(deskStickerSheet.getCell('A5').value), new RegExp(generatedCard.assignments[0].seat), '桌贴应突出座位号');
assert.match(String(materialsBook.getWorksheet('门贴').getCell('A1').value), /考场门贴/);
assert.equal((await classAdmin.request(`/api/admin/admission-exports/center-materials?examId=${exam.id}`)).response.status, 403, '班级管理员不得导出考点级现场物料');
const paginationRows = Array.from({ length: 7 }, (_, index) => ({
examCode: 'EX-QA', examName: '桌贴分页测试', subjectName: '语文', subjectDate: '2026-10-01', subjectTime: '09:00—11:00',
centerCode: 'C01', centerName: '测试考点', examRoomCode: '001', roomName: '第一教室', roomCode: 'R01', building: '教学楼', floor: '1 层',
seat: String(index + 1).padStart(2, '0'), cardNumber: `CARD${index + 1}`, candidateName: `考生${index + 1}`, schoolName: '测试学校', className: '测试班'
}));
const paginationBook = new ExcelJS.Workbook();
await paginationBook.xlsx.load(await buildCenterMaterialsWorkbook(paginationRows, '分页测试'));
assert.equal(paginationBook.getWorksheet('桌贴').pageSetup.printArea, 'A1:G48', '超过六张桌贴时应扩展为两页固定打印区域');
assert.equal(paginationBook.getWorksheet('桌贴').getRow(32).height, 32.5, '第二页桌贴仍应保持相同方形标签高度');
const futureExamResponse = await admin.request('/api/admin/exams', {
method: 'POST',
body: {