后台重构为一级业务域,并将招生录取拆为 5 个二级菜单。 修复成绩等级排名遮挡科目。 成绩单、录取通知书均支持带防伪码和二维码的 PDF 下载。 录取通知书移除“录取专用章”,支持学校自定义模板。 通知书编号采用“学校代码 + 考试代码 + 学校独立流水号”。 完成招生学校报到台账:Y/N/P 状态、暂存、Excel 导入导出、二维码扫描确认。 完成报到提交、计划完成率、补录决定、超级管理员审批和自动公开闭环。 自动公告现已进入公开首页、考生通知及招生学校工作台。 验证结果: 完整 npm test 全部通过。 浏览器端三个角色工作台验收通过。 Excel 模板已实际导出并渲染验证。 两类 PDF 已实际生成、转图检查,二维码、编号和无印章版式正常。
113 lines
12 KiB
JavaScript
113 lines
12 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
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;
|
|
const db = {
|
|
users: [
|
|
{ id: 'u-high', role: 'candidate', active: true, candidateNumber: '20260001', displayName: '高分考生' },
|
|
{ id: 'u-low', role: 'candidate', active: true, candidateNumber: '20260002', displayName: '次高考生' },
|
|
{ id: 'u-sport', role: 'candidate', active: true, candidateNumber: '20260003', displayName: '特长考生' }
|
|
],
|
|
candidateProfiles: [
|
|
{ userId: 'u-high', name: '高分考生', schoolId: 'source-a', profileCompleted: true, idNumber: '320101200901011234', phone: '13812345678', specialtyTypes: [] },
|
|
{ userId: 'u-low', name: '次高考生', schoolId: 'source-b', profileCompleted: true, idNumber: '320101200902021234', phone: '13912345678', specialtyTypes: [] },
|
|
{ userId: 'u-sport', name: '特长考生', schoolId: 'source-b', profileCompleted: true, idNumber: '320101200903031234', phone: '13712345678', specialtyCategory: 'sports', specialtyType: 'track_field', specialtyTypes: ['track_field'] }
|
|
],
|
|
schools: [
|
|
{ id: 'source-a', name: '生源学校 A' }, { id: 'source-b', name: '生源学校 B' },
|
|
{ id: 'target-a', name: '第一中学' }, { id: 'target-b', name: '第二中学' }
|
|
],
|
|
registrations: [
|
|
{ id: 'r-high', examId: 'exam', userId: 'u-high', status: 'approved', subjectIds: ['cn', 'math'] },
|
|
{ id: 'r-low', examId: 'exam', userId: 'u-low', status: 'approved', subjectIds: ['cn', 'math'] },
|
|
{ id: 'r-sport', examId: 'exam', userId: 'u-sport', status: 'approved', subjectIds: ['cn', 'math'], featureScore: 88.5 }
|
|
],
|
|
results: [
|
|
{ registrationId: 'r-high', subjectId: 'cn', score: 120, published: true }, { registrationId: 'r-high', subjectId: 'math', score: 130, published: true },
|
|
{ registrationId: 'r-low', subjectId: 'cn', score: 118, published: true }, { registrationId: 'r-low', subjectId: 'math', score: 126, published: true },
|
|
{ registrationId: 'r-sport', subjectId: 'cn', score: 105, published: true }, { registrationId: 'r-sport', subjectId: 'math', score: 110, published: true }
|
|
],
|
|
admissionRecords: [
|
|
{ id: 'plan-a', kind: 'plan', examId: 'exam', schoolId: 'target-a', status: 'approved', payload: { categories: [{ code: 'general', name: '普通生', quota: 1, specialtyType: '', indicatorAllocations: [] }] } },
|
|
{ id: 'plan-b', kind: 'plan', examId: 'exam', schoolId: 'target-b', status: 'approved', payload: { categories: [
|
|
{ code: 'general', name: '普通生', quota: 1, specialtyType: '', indicatorAllocations: [] },
|
|
{ code: 'sport', name: '田径特长生', quota: 1, specialtyCategory: 'sports', specialtyType: 'track_field', indicatorAllocations: [{ sourceSchoolId: 'source-b', quota: 1 }] }
|
|
] } },
|
|
{ id: 'qual-low', kind: 'indicator_qualification', examId: 'exam', userId: 'u-low', schoolId: 'source-b', status: 'confirmed', payload: { eligible: false } },
|
|
{ id: 'qual-sport', kind: 'indicator_qualification', examId: 'exam', userId: 'u-sport', schoolId: 'source-b', status: 'confirmed', payload: { eligible: true } },
|
|
{ id: 'pref-high', kind: 'preference', examId: 'exam', userId: 'u-high', status: 'submitted', payload: { round: 1, choices: [{ schoolId: 'target-b', categoryCode: 'general', preferenceType: 'general' }, { schoolId: 'target-a', categoryCode: 'general', preferenceType: 'general' }] } },
|
|
{ id: 'pref-low', kind: 'preference', examId: 'exam', userId: 'u-low', status: 'submitted', payload: { round: 1, choices: [{ schoolId: 'target-b', categoryCode: 'general', preferenceType: 'general' }, { schoolId: 'target-a', categoryCode: 'general', preferenceType: 'general' }] } },
|
|
{ id: 'pref-sport', kind: 'preference', examId: 'exam', userId: 'u-sport', status: 'submitted', payload: { round: 1, choices: [{ schoolId: 'target-b', categoryCode: 'sport', preferenceType: 'indicator' }] } }
|
|
]
|
|
};
|
|
|
|
const setting = { examId: 'exam', payload: { round: 1 } };
|
|
const placements = buildVolunteerPlacements(db, setting, { uid: prefix => `${prefix}-${++sequence}`, nowIso: () => now });
|
|
assert.equal(candidateTotalScore(db, 'exam', 'u-high'), 250, '投档总分应取当次全部已发布科目之和');
|
|
assert.equal(candidateAdmissionScore(db, 'exam', 'u-sport', { code: 'general' }), 215, '普通招生类别不得加入特征分');
|
|
assert.equal(candidateAdmissionScore(db, 'exam', 'u-sport', { specialtyCategory: 'sports', specialtyType: 'track_field' }), 303.5, '特长生招生类别应使用文化课总分加特征分');
|
|
assert.equal(placements.length, 3, '三个符合条件且计划充足的考生都应投档');
|
|
assert.equal(placements.find(item => item.userId === 'u-high').schoolId, 'target-b', '最高分考生应优先满足第一志愿');
|
|
assert.equal(placements.find(item => item.userId === 'u-low').schoolId, 'target-a', '第一志愿已满时应继续遵循下一志愿');
|
|
assert.equal(placements.find(item => item.userId === 'u-sport').payload.quotaBucket, 'indicator:source-b', '特长生指标应使用对应生源学校指标名额');
|
|
assert.equal(placements.find(item => item.userId === 'u-sport').payload.featureScore, 88.5, '特征分应随投档材料发送');
|
|
assert.equal(placements.find(item => item.userId === 'u-sport').payload.culturalScore, 215, '特长生投档材料应保留文化课原始总分');
|
|
assert.equal(placements.find(item => item.userId === 'u-sport').payload.totalScore, 303.5, '特长生类别投档总分应加入特征分');
|
|
assert.equal(specialtyLabel('sports', 'track_field'), '体育·田径', '特长资格应显示大类和小类');
|
|
assert.equal(candidateEligibleForCategory(db.candidateProfiles[2], { specialtyCategory: 'arts', specialtyType: 'fine_arts' }), false, '体育资格考生不得填报艺术类计划');
|
|
|
|
const specialtyPriorityDb = structuredClone(db);
|
|
specialtyPriorityDb.users.push({ id: 'u-sport-rival', role: 'candidate', active: true, candidateNumber: '20260004', displayName: '特长竞争考生' });
|
|
specialtyPriorityDb.candidateProfiles.push({ userId: 'u-sport-rival', name: '特长竞争考生', schoolId: 'source-b', profileCompleted: true, specialtyCategory: 'sports', specialtyType: 'track_field', specialtyTypes: ['track_field'] });
|
|
specialtyPriorityDb.registrations.push({ id: 'r-sport-rival', examId: 'exam', userId: 'u-sport-rival', status: 'approved', subjectIds: ['cn', 'math'], featureScore: 0 });
|
|
specialtyPriorityDb.results.push({ registrationId: 'r-sport-rival', subjectId: 'cn', score: 125, published: true }, { registrationId: 'r-sport-rival', subjectId: 'math', score: 125, published: true });
|
|
specialtyPriorityDb.admissionRecords.push(
|
|
{ id: 'qual-sport-rival', kind: 'indicator_qualification', examId: 'exam', userId: 'u-sport-rival', schoolId: 'source-b', status: 'confirmed', payload: { eligible: true } },
|
|
{ id: 'pref-sport-rival', kind: 'preference', examId: 'exam', userId: 'u-sport-rival', status: 'submitted', payload: { round: 1, choices: [{ schoolId: 'target-b', categoryCode: 'sport', preferenceType: 'indicator' }] } }
|
|
);
|
|
const specialtyPriorityPlacements = buildVolunteerPlacements(specialtyPriorityDb, setting, { uid: prefix => `${prefix}-priority-${++sequence}`, nowIso: () => now });
|
|
assert.equal(specialtyPriorityPlacements.find(item => item.payload.categoryCode === 'sport').userId, 'u-sport', '特长类别应按文化课加特征分排序,而不是只按文化课排序');
|
|
|
|
db.admissionRecords.push(...placements.map(item => ({ ...item, status: 'final' })));
|
|
const remaining = remainingPlanQuota(db, db.admissionRecords.find(item => item.id === 'plan-b'));
|
|
assert.equal(remaining.find(item => item.code === 'general').remaining, 0, '普通生计划占用应准确统计');
|
|
assert.equal(remaining.find(item => item.code === 'sport').remaining, 0, '特长生计划占用应准确统计');
|
|
const publicRows = publicAdmissionRows(db, 'exam');
|
|
const highPublicRow = publicRows.find(item => item.registrationNumber === '20260001');
|
|
assert.equal(highPublicRow.registrationNumber, '20260001', '公示必须公开报名号');
|
|
assert.equal(highPublicRow.name, '高分考生', '公示必须公开姓名');
|
|
assert.equal(highPublicRow.totalScore, 250, '普通类别公示总成绩不得加入特征分');
|
|
assert.equal(highPublicRow.admittedSchool, '第二中学', '公示必须公开录取学校');
|
|
assert.ok(highPublicRow.idNumberMasked.includes('*') && !highPublicRow.idNumberMasked.includes('20090101'), '重要身份信息必须脱敏');
|
|
assert.equal(publicRows.find(item => item.registrationNumber === '20260003').totalScore, 303.5, '特长生类别公示总成绩应包含特征分');
|
|
const cutoffs = admissionCutoffRows(db, 'exam');
|
|
assert.equal(cutoffs.find(item => item.schoolId === 'target-b' && item.categoryCode === 'general').cutoffScore, 250, '录取分数线应取学校招生类别最终录取最低总分');
|
|
const qualificationStatus = sourceSchoolQualificationStatus(db, 'exam', 'source-b');
|
|
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('志愿投档、指标名额与脱敏公示测试通过');
|