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

后台重构为一级业务域,并将招生录取拆为 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
+22 -1
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import { admissionCutoffRows, buildVolunteerPlacements, candidateAdmissionScore, candidateTotalScore, publicAdmissionRows, remainingPlanQuota, sourceSchoolQualificationStatus } from '../src/services/volunteer-admission.mjs';
import { admissionCutoffRows, admissionPlanProgress, assignAdmissionNoticeNumbers, buildVolunteerPlacements, candidateAdmissionScore, candidateTotalScore, publicAdmissionRows, remainingPlanQuota, sourceSchoolQualificationStatus } from '../src/services/volunteer-admission.mjs';
import { candidateEligibleForCategory, specialtyLabel } from '../src/data/specialty-types.mjs';
import { systemNotificationItems } from '../src/services/system-notifications.mjs';
const now = '2026-07-21T08:00:00.000Z';
let sequence = 0;
@@ -88,4 +89,24 @@ const qualificationStatus = sourceSchoolQualificationStatus(db, 'exam', 'source-
assert.equal(qualificationStatus.complete, true, '生源校全部考生确认后应达到自动公示条件');
assert.equal(qualificationStatus.rows.find(item => item.userId === 'u-sport').specialtyLabel, '体育·田径', '资格公示应包含对应特长类型');
const documentDb = structuredClone(db);
documentDb.exams = [{ id: 'exam', code: 'EX-2026-ZK', name: '中考' }];
documentDb.schools.find(item => item.id === 'target-b').code = 'AD02';
const targetPlacements = documentDb.admissionRecords.filter(item => item.kind === 'placement' && item.schoolId === 'target-b');
const numbered = assignAdmissionNoticeNumbers(documentDb, targetPlacements);
assert.deepEqual(numbered.map(item => item.payload.noticeNumber), ['AD02-EX-2026-ZK-000001', 'AD02-EX-2026-ZK-000002'], '通知书编号应按学校与考试独立生成连续流水号');
const reportingPlan = documentDb.admissionRecords.find(item => item.id === 'plan-b');
documentDb.admissionRecords = documentDb.admissionRecords.map(item => numbered.find(numberedItem => numberedItem.id === item.id) || item);
documentDb.admissionRecords.push({ id: 'reporting-b', kind: 'notification', examId: 'exam', schoolId: 'target-b', userId: null, status: 'draft', payload: { type: 'admission_reporting', round: 1, rows: [{ placementId: numbered[0].id, status: 'reported' }, { placementId: numbered[1].id, status: 'not_reported' }] } });
const progress = admissionPlanProgress(documentDb, reportingPlan);
assert.equal(progress.totalQuota, 2, '计划完成率分母应来自学校审核通过的计划人数');
assert.equal(progress.reportedCount, 1, '实际报到人数应来自学校报到暂存台账');
assert.equal(progress.reportingRate, 50, '实际报到完成率应实时按计划人数计算');
documentDb.admissionRecords.at(-1).status = 'approved';
documentDb.admissionRecords.at(-1).payload.supplementDecision = 'no_supplement';
documentDb.admissionRecords.at(-1).payload.decisionNote = '学校研究决定不进行补录。';
documentDb.admissionRecords.at(-1).payload.statistics = progress;
const reportingNotice = systemNotificationItems(documentDb).find(item => item.sourceType === 'reporting');
assert.ok(reportingNotice.title.includes('报到情况公示') && !reportingNotice.title.includes('补录'), '计划完成或决定不补录时公告标题不应出现“补录”');
console.log('志愿投档、指标名额与脱敏公示测试通过');
+3 -1
View File
@@ -21,13 +21,15 @@ assert.equal(safeCodeEqual(scoreCode, `${scoreCode}0`), false);
const placement = {
id: 'placement_1', userId: 'candidate_1', schoolId: 'school_1',
payload: { categoryCode: 'general' }, updatedAt: '2026-07-21T08:00:00.000Z'
payload: { categoryCode: 'general', noticeNumber: 'AD01-EX-2026-000001' }, updatedAt: '2026-07-21T08:00:00.000Z'
};
const noticeCode = admissionNoticeCode(secret, placement, exam);
const movedSchoolCode = admissionNoticeCode(secret, { ...placement, schoolId: 'school_2' }, exam);
const changedNumberCode = admissionNoticeCode(secret, { ...placement, payload: { ...placement.payload, noticeNumber: 'AD01-EX-2026-000002' } }, exam);
assert.match(noticeCode, /^AN-[A-F0-9]{24}$/);
assert.notEqual(noticeCode, movedSchoolCode, '录取学校变化必须使旧通知书防伪码失效');
assert.notEqual(noticeCode, changedNumberCode, '录取通知书编号变化必须使旧防伪码失效');
assert.throws(
() => resolveDocumentVerificationSecret({ NODE_ENV: 'production', DOCUMENT_VERIFICATION_SECRET: 'too-short' }),
+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, '考生总分满分应按实际报考科目汇总');