全站大数据台账补齐跨页搜索、筛选和分页,包括投档监督、报到审批、成绩复议、公开录取名单等。

后台重构为一级业务域,并将招生录取拆为 5 个二级菜单。
修复成绩等级排名遮挡科目。
成绩单、录取通知书均支持带防伪码和二维码的 PDF 下载。
录取通知书移除“录取专用章”,支持学校自定义模板。
通知书编号采用“学校代码 + 考试代码 + 学校独立流水号”。
完成招生学校报到台账:Y/N/P 状态、暂存、Excel 导入导出、二维码扫描确认。
完成报到提交、计划完成率、补录决定、超级管理员审批和自动公开闭环。
自动公告现已进入公开首页、考生通知及招生学校工作台。
验证结果:
完整 npm test 全部通过。
浏览器端三个角色工作台验收通过。
Excel 模板已实际导出并渲染验证。
两类 PDF 已实际生成、转图检查,二维码、编号和无印章版式正常。
This commit is contained in:
2026-07-22 09:50:52 +08:00 Unverified
parent d029931c31
commit 3e1bced990
22 changed files with 813 additions and 94 deletions
+50 -1
View File
@@ -12,6 +12,7 @@ import { mysqlSchema } from '../src/database/schema.mjs';
import { CURRENT_SCHEMA_VERSION } from '../src/database/version.mjs';
import { totpAtStep } from '../src/security/totp.mjs';
import { buildCenterMaterialsWorkbook, buildWorkbook } from '../excel.mjs';
import { admissionNoticeCode } from '../src/security/document-verification.mjs';
const root = resolve(process.cwd());
assert.equal(totpAtStep('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', 1), '287082', 'TOTP 实现应符合 RFC 6238 SHA-1 测试向量的 6 位结果');
@@ -598,7 +599,8 @@ try {
assert.equal(createExam.data.exam.subjects[2].passRule, 'rank_percent', '每科应可独立按排名比例计算及格线');
assert.equal(createExam.data.exam.subjects[2].passScore, null, '排名比例不应伪造固定分数线');
const exam = createExam.data.exam;
assert.equal((await admin.request(`/api/admin/admissions/${exam.id}/setting`, { method: 'PUT', body: { enabled: true, status: 'draft', maxChoices: 5, maxSubmissions: 1 } })).response.status, 200, '超级管理员应能按考试启用志愿功能并设置填报次数');
const admissionSettingResult = await admin.request(`/api/admin/admissions/${exam.id}/setting`, { method: 'PUT', body: { enabled: true, status: 'draft', maxChoices: 5, maxSubmissions: 1 } });
assert.equal(admissionSettingResult.response.status, 200, `超级管理员应能按考试启用志愿功能并设置填报次数:${JSON.stringify(admissionSettingResult.data)}\n${serverError}`);
const structuredPlan = await admissionSchoolClient.request('/api/admission/plans', { method: 'POST', body: { examId: exam.id, note: '结构化计划测试', categories: [
{ code: 'general', name: '普通生', quota: 20, specialtyCategory: '', specialtyType: '', indicatorAllocations: [{ sourceSchoolId: 'school_hz1', quota: 5 }] },
{ code: 'arts', name: '美术特长生', quota: 4, specialtyCategory: 'arts', specialtyType: 'fine_arts', indicatorAllocations: [] }
@@ -657,6 +659,7 @@ try {
placementCreatedAt, placementCreatedAt
);
}
placementWriter.prepare("UPDATE admission_records SET status = 'school_review' WHERE kind = 'setting' AND exam_id = ?").run(exam.id);
placementWriter.close();
const placementReviewLedger = await admissionSchoolClient.request('/api/admission/placements');
@@ -675,6 +678,51 @@ try {
assert.equal(processedPlacementLedger.data.placements.find(item => item.id === placementIds[2]).status, 'withdrawal_pending', '批量退档后记录应进入上级审核状态');
assert.equal((await admissionSchoolClient.request('/api/admission/placements/bulk', { method: 'POST', body: { ids: placementIds.slice(0, 2), decision: 'accept' } })).response.status, 409, '已处理记录不得被重复批量审核');
assert.equal((await admin.request(`/api/admin/admission-withdrawals/${placementIds[2]}`, { method: 'PATCH', body: { approved: true, reviewNote: '同意退档' } })).response.status, 200, '超级管理员应先办结退档再签发录取通知书');
const finalizedAdmission = await admin.request(`/api/admin/admissions/${exam.id}/finalize`, { method: 'POST' });
assert.equal(finalizedAdmission.response.status, 200, '超级管理员应能签发带编号的录取通知书并开启报到');
assert.equal(finalizedAdmission.data.admittedCount, 2, '正式签发人数应与学校接收人数一致');
const reportingLedger = await admissionSchoolClient.request('/api/admission/reporting');
const reportingBatch = reportingLedger.data.batches.find(item => item.exam.id === exam.id);
assert.equal(reportingBatch.rows.length, 2, '招生学校报到台账应包含本轮全部正式录取考生');
assert.deepEqual(reportingBatch.rows.map(item => item.noticeNumber), [`${admissionOnlySchool.data.school.code}-${exam.code}-000001`, `${admissionOnlySchool.data.school.code}-${exam.code}-000002`], '通知书编号应使用学校代码、考试代码和学校独立流水号');
const reportingExport = await admissionSchoolClient.request(`/api/admission/reporting/export?examId=${exam.id}`);
assert.equal(reportingExport.response.status, 200, '招生学校应能导出报到状态 Excel');
const reportingWorkbook = new ExcelJS.Workbook(); await reportingWorkbook.xlsx.load(reportingExport.data);
const reportingSheet = reportingWorkbook.getWorksheet('考生报到');
assert.ok(reportingSheet.getRow(2).values.includes('报到状态码*Y/N/P'), '报到 Excel 应明确提供 Y/N/P 状态码列');
assert.equal(reportingSheet.getCell('G3').value, 'P', '新报到批次导出时应默认为待确认状态码 P');
const finalizedInspector = new DatabaseSync(testDb, { readOnly: true });
const finalizedPlacementRow = finalizedInspector.prepare('SELECT * FROM admission_records WHERE id = ?').get(placementIds[0]);
finalizedInspector.close();
const finalizedPlacement = { id: finalizedPlacementRow.id, kind: finalizedPlacementRow.kind, examId: finalizedPlacementRow.exam_id, userId: finalizedPlacementRow.user_id, schoolId: finalizedPlacementRow.school_id, status: finalizedPlacementRow.status, payload: JSON.parse(finalizedPlacementRow.payload_json), createdAt: finalizedPlacementRow.created_at, updatedAt: finalizedPlacementRow.updated_at };
const noticeCode = admissionNoticeCode('development-document-verification-secret', finalizedPlacement, exam);
const scannedReporting = await admissionSchoolClient.request('/api/admission/reporting/scan', { method: 'POST', body: { code: `http://127.0.0.1/#verify/${noticeCode}` } });
assert.equal(scannedReporting.response.status, 200, '招生学校应能扫描通知书防伪二维码并暂存报到');
assert.equal(scannedReporting.data.row.status, 'reported', '扫码核验通过后应暂存为已报到而不是直接提交');
const secondRow = reportingBatch.rows[1];
const importReportingFile = Buffer.from(await buildWorkbook('admission_reporting', [{ noticeNumber: secondRow.noticeNumber, candidateNumber: secondRow.candidateNumber, name: secondRow.name, examCode: exam.code, schoolCode: admissionOnlySchool.data.school.code, categoryName: secondRow.categoryName, reportingStatusCode: 'N', reportingNote: '逾期未报到' }]));
const importedReporting = await admissionSchoolClient.request(`/api/admission/reporting/import?examId=${exam.id}`, { method: 'POST', headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, body: importReportingFile });
assert.equal(importedReporting.response.status, 200, '招生学校应能导入修改后的报到 Excel 并暂存');
assert.equal(importedReporting.data.count, 1, 'Excel 导入应返回实际暂存人数');
assert.equal((await admissionSchoolClient.request('/api/admission/reporting/submit', { method: 'POST', body: { examId: exam.id } })).response.status, 200, '全部状态确认后招生学校应能提交报到情况');
assert.equal((await admissionSchoolClient.request('/api/admission/reporting/decision', { method: 'POST', body: { examId: exam.id, supplement: true, decisionNote: '一名考生未报到,申请补录缺额' } })).response.status, 200, '招生学校应能根据实时完成率提交补录决定');
const reportingApprovalLedger = await admin.request('/api/admin/admissions');
const pendingReportingApproval = reportingApprovalLedger.data.reportingRequests.find(item => item.examId === exam.id && item.status === 'pending_approval');
assert.ok(pendingReportingApproval, '超级管理员应看到招生学校报到与补录审批待办');
const supplementEnd = new Date(Date.now() + 48 * hour).toISOString();
const approvedReporting = await admin.request(`/api/admin/admission-reporting/${pendingReportingApproval.id}`, { method: 'PATCH', body: { approved: true, approvalNote: '同意按缺额补录', preferenceEnd: supplementEnd } });
assert.equal(approvedReporting.response.status, 200, '超级管理员应能批准补录并自动公开报到情况');
assert.equal(approvedReporting.data.phase, 'supplementary', '全部学校审批完成且存在补录申请时应自动开启下一轮补录');
const reportingAnnouncements = await anonymous.request('/api/public/announcements');
const reportingPublication = reportingAnnouncements.data.reports.find(item => item.id === pendingReportingApproval.id);
assert.ok(reportingPublication?.title.includes('补录说明'), '批准补录后报到公示标题应自动包含补录说明');
assert.equal(reportingPublication.statistics.reportedCount, 1, '公开报到情况应包含简单统计数据');
const homeAfterReporting = await anonymous.request('/api/public/home');
assert.ok(homeAfterReporting.data.notices.some(item => item.id === `system-reporting-${pendingReportingApproval.id}`), '系统自动生成的报到公示应进入所有用户共用的通知列表');
const admissionAccountLedger = await admin.request('/api/admin/admissions');
const managedAdmissionAccount = admissionAccountLedger.data.schoolAccounts.find(item => item.id === admissionAccount.data.account.id);
assert.equal(managedAdmissionAccount.schoolName, '海州市招生实验学校', '招生学校账户台账应显示绑定学校');
@@ -1026,6 +1074,7 @@ try {
const results = await candidate.request('/api/candidate/results');
assert.ok(results.data.results.some(item => item.score === 126 && item.subjectName === '语文'), '已发布成绩应在考生端可查询');
const resultSummary = results.data.summaries.find(item => item.examId === exam.id);
assert.match(resultSummary.verificationQr, /^data:image\/png;base64,/, '成绩单防伪信息应包含可直接扫描的二维码');
assert.equal(resultSummary.featureScore, 87.5, '考生端整场成绩应单独显示特征分');
assert.equal(resultSummary.total, 216, '考生端应汇总已报科目的总分');
assert.equal(resultSummary.fullScore, 300, '考生总分满分应按实际报考科目汇总');