招生管理完善
This commit is contained in:
@@ -48,7 +48,7 @@ export function createPublicViews(context) {
|
|||||||
const ordinary = (state.publicData.notices || []).filter(item => !String(item.id).startsWith('system-')).map(item => ({ ...item, documentId: item.id, documentType: 'notice', subtype: item.category || '通知公告', publishedAt: item.publishAt }));
|
const ordinary = (state.publicData.notices || []).filter(item => !String(item.id).startsWith('system-')).map(item => ({ ...item, documentId: item.id, documentType: 'notice', subtype: item.category || '通知公告', publishedAt: item.publishAt }));
|
||||||
const plans = (data.plans || []).map(item => ({ ...item, documentId: `plan-${item.id}`, documentType: 'plan', category: '招生公示', subtype: '招生计划', title: `${item.examName} · ${item.schoolName}招生计划公示`, summary: `共 ${item.rows.reduce((sum, row) => sum + Number(row.quota || 0), 0)} 个招生名额,计划审核通过后由系统自动公示。` }));
|
const plans = (data.plans || []).map(item => ({ ...item, documentId: `plan-${item.id}`, documentType: 'plan', category: '招生公示', subtype: '招生计划', title: `${item.examName} · ${item.schoolName}招生计划公示`, summary: `共 ${item.rows.reduce((sum, row) => sum + Number(row.quota || 0), 0)} 个招生名额,计划审核通过后由系统自动公示。` }));
|
||||||
const qualifications = (data.qualifications || []).map(item => ({ ...item, documentId: `qualification-${item.id}`, documentType: 'qualification', category: '录取公示', subtype: '指标资格', title: `${item.examName} · ${item.schoolName}指标分配资格公示`, summary: `本次公开 ${item.rows.length} 名考生的指标分配资格及特长类型。` }));
|
const qualifications = (data.qualifications || []).map(item => ({ ...item, documentId: `qualification-${item.id}`, documentType: 'qualification', category: '录取公示', subtype: '指标资格', title: `${item.examName} · ${item.schoolName}指标分配资格公示`, summary: `本次公开 ${item.rows.length} 名考生的指标分配资格及特长类型。` }));
|
||||||
const admissions = (data.admissions || []).map(item => ({ ...item, documentId: `admission-${item.id}`, documentType: 'admission', category: '录取公示', subtype: '录取名单', title: `${item.examName}最终录取名单`, summary: `共 ${item.rows.length} 名考生正式录取,公开报名号、姓名、总成绩和录取学校。` }));
|
const admissions = (data.admissions || []).map(item => ({ ...item, documentId: `admission-${item.id}`, documentType: 'admission', category: '录取公示', subtype: item.round ? `第 ${item.round} 轮录取名单` : '最终录取名单', title: item.title || `${item.examName}最终录取名单`, summary: `共 ${item.rows.length} 名考生正式录取,公开报名号、姓名、总成绩和录取学校。` }));
|
||||||
const cutoffs = (data.cutoffs || []).map(item => ({ ...item, documentId: `cutoff-${item.id}`, documentType: 'cutoff', category: '录取公示', subtype: '录取分数线', title: `${item.examName}录取分数线`, summary: `按招生学校和招生类别公布 ${item.rows.length} 条最低录取分数线。` }));
|
const cutoffs = (data.cutoffs || []).map(item => ({ ...item, documentId: `cutoff-${item.id}`, documentType: 'cutoff', category: '录取公示', subtype: '录取分数线', title: `${item.examName}录取分数线`, summary: `按招生学校和招生类别公布 ${item.rows.length} 条最低录取分数线。` }));
|
||||||
const reports = (data.reports || []).map(item => ({ ...item, documentId: `reporting-${item.id}`, documentType: 'reporting', category: '录取公示', subtype: item.supplementDecision === 'supplement' ? '报到与补录' : '报到情况', title: item.title, summary: item.summary }));
|
const reports = (data.reports || []).map(item => ({ ...item, documentId: `reporting-${item.id}`, documentType: 'reporting', category: '录取公示', subtype: item.supplementDecision === 'supplement' ? '报到与补录' : '报到情况', title: item.title, summary: item.summary }));
|
||||||
return [...ordinary, ...plans, ...qualifications, ...admissions, ...cutoffs, ...reports].sort((left, right) => new Date(right.publishedAt) - new Date(left.publishedAt));
|
return [...ordinary, ...plans, ...qualifications, ...admissions, ...cutoffs, ...reports].sort((left, right) => new Date(right.publishedAt) - new Date(left.publishedAt));
|
||||||
@@ -83,7 +83,7 @@ export function createPublicViews(context) {
|
|||||||
const page = publicPaged(document.rows, key);
|
const page = publicPaged(document.rows, key);
|
||||||
const schools = [...new Set(document.rows.map(row => row.admittedSchool).filter(Boolean))];
|
const schools = [...new Set(document.rows.map(row => row.admittedSchool).filter(Boolean))];
|
||||||
const categories = [...new Set(document.rows.map(row => row.categoryName).filter(Boolean))];
|
const categories = [...new Set(document.rows.map(row => row.categoryName).filter(Boolean))];
|
||||||
return `<p class="document-lead">报名号、姓名、考生总成绩与录取学校公开透明;证件号和联系方式不在本页展示。</p><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="${h(key)}" placeholder="跨页搜索报名号、姓名、学校或类别"></label><div class="table-filter-selects"><select data-table-filter="school" data-target="${h(key)}"><option value="">全部录取学校</option>${schools.map(school => `<option value="${h(school)}">${h(school)}</option>`).join('')}</select><select data-table-filter="category" data-target="${h(key)}"><option value="">全部录取类别</option>${categories.map(category => `<option value="${h(category)}">${h(category)}</option>`).join('')}</select><button class="row-action" data-action="clear-table-filters" data-target="${h(key)}">清除筛选</button></div></div><div class="table-scroll"><table id="${h(key)}"><thead><tr><th>报名号</th><th>姓名</th><th>总成绩</th><th>录取学校</th><th>录取类别</th></tr></thead><tbody>${page.items.map(row => `<tr><td class="mono">${h(row.registrationNumber)}</td><td><strong>${h(row.name)}</strong></td><td>${h(row.totalScore)}</td><td>${h(row.admittedSchool)}</td><td>${h(row.categoryName)}</td></tr>`).join('') || '<tr><td colspan="5" class="empty-state">没有符合条件的录取记录</td></tr>'}</tbody></table></div>${publicPagination(page)}`;
|
return `<p class="document-lead">${document.round ? `本公示为第 ${h(document.round)} 轮录取通知书签发时生成的名单快照。` : '本公示为全部录取与报到流程结束后的最终名单。'}报名号、姓名、考生总成绩与录取学校公开透明;证件号和联系方式不在本页展示。</p><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="${h(key)}" placeholder="跨页搜索报名号、姓名、学校或类别"></label><div class="table-filter-selects"><select data-table-filter="school" data-target="${h(key)}"><option value="">全部录取学校</option>${schools.map(school => `<option value="${h(school)}">${h(school)}</option>`).join('')}</select><select data-table-filter="category" data-target="${h(key)}"><option value="">全部录取类别</option>${categories.map(category => `<option value="${h(category)}">${h(category)}</option>`).join('')}</select><button class="row-action" data-action="clear-table-filters" data-target="${h(key)}">清除筛选</button></div></div><div class="table-scroll"><table id="${h(key)}"><thead><tr><th>报名号</th><th>姓名</th><th>总成绩</th><th>录取学校</th><th>录取类别</th></tr></thead><tbody>${page.items.map(row => `<tr><td class="mono">${h(row.registrationNumber)}</td><td><strong>${h(row.name)}</strong></td><td>${h(row.totalScore)}</td><td>${h(row.admittedSchool)}</td><td>${h(row.categoryName)}</td></tr>`).join('') || '<tr><td colspan="5" class="empty-state">没有符合条件的录取记录</td></tr>'}</tbody></table></div>${publicPagination(page)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDocumentBody(document) {
|
function renderDocumentBody(document) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { admissionMixingScopes, buildAdmissionArrangement } from '../services/admission-arrangement.mjs';
|
import { admissionMixingScopes, buildAdmissionArrangement } from '../services/admission-arrangement.mjs';
|
||||||
import { noticeForClient, noticePlainText, sanitizeNoticeContent } from '../security/notice-content.mjs';
|
import { noticeForClient, noticePlainText, sanitizeNoticeContent } from '../security/notice-content.mjs';
|
||||||
import { admissionCutoffRows, admissionPhases, admissionPlanProgress, admissionRecords, admissionReportingRecord, admissionSetting, assignAdmissionNoticeNumbers, approvedPlans, buildVolunteerPlacements, candidateTotalScore, publicAdmissionRows, remainingPlanQuota, sourceSchoolQualificationStatus } from '../services/volunteer-admission.mjs';
|
import { admissionCutoffRows, admissionPhases, admissionPlanProgress, admissionRecords, admissionReportingRecord, admissionRoundPublications, admissionSetting, assignAdmissionNoticeNumbers, approvedPlans, buildVolunteerPlacements, candidateTotalScore, publicAdmissionRows, remainingPlanQuota, sourceSchoolQualificationStatus } from '../services/volunteer-admission.mjs';
|
||||||
import { isValidSpecialty, resolveProfileSpecialty, specialtyLabel } from '../data/specialty-types.mjs';
|
import { isValidSpecialty, resolveProfileSpecialty, specialtyLabel } from '../data/specialty-types.mjs';
|
||||||
import { systemNotificationItems } from '../services/system-notifications.mjs';
|
import { systemNotificationItems } from '../services/system-notifications.mjs';
|
||||||
|
|
||||||
@@ -151,7 +151,16 @@ export function createAdminRoutes(context) {
|
|||||||
item.payload?.publishedAt || item.updatedAt,
|
item.payload?.publishedAt || item.updatedAt,
|
||||||
'资格确认完成后由系统生成,内容随资格确认结果更新。'
|
'资格确认完成后由系统生成,内容随资格确认结果更新。'
|
||||||
));
|
));
|
||||||
const admissions = admissionRecords(db, 'setting').filter(item => item.status === 'completed' && item.payload?.autoPublish !== false).map(item => view(
|
const roundAdmissions = admissionRoundPublications(db).filter(item => admissionSetting(db, item.examId)?.payload?.autoPublish !== false).map(item => view(
|
||||||
|
{ ...item, id: item.sourceRecordId || item.id },
|
||||||
|
'admission',
|
||||||
|
'录取名单',
|
||||||
|
`${examName(item.examId)}第 ${item.round} 轮录取名单公示`,
|
||||||
|
item.publishedAt,
|
||||||
|
`第 ${item.round} 轮录取通知书签发后由系统自动生成,共 ${item.rows.length} 人。`
|
||||||
|
));
|
||||||
|
const virtualSourceIds = new Set(admissionRoundPublications(db).filter(item => item.virtual).map(item => item.sourceRecordId));
|
||||||
|
const admissions = admissionRecords(db, 'setting').filter(item => item.status === 'completed' && item.payload?.autoPublish !== false && !virtualSourceIds.has(item.id)).map(item => view(
|
||||||
item,
|
item,
|
||||||
'admission',
|
'admission',
|
||||||
'录取名单',
|
'录取名单',
|
||||||
@@ -171,7 +180,7 @@ export function createAdminRoutes(context) {
|
|||||||
id: item.id, sourceType: 'reporting', category: item.category, title: item.title, summary: item.summary,
|
id: item.id, sourceType: 'reporting', category: item.category, title: item.title, summary: item.summary,
|
||||||
author: item.author, publishedAt: item.publishAt, visible: item.visible, status: item.status
|
author: item.author, publishedAt: item.publishAt, visible: item.visible, status: item.status
|
||||||
}));
|
}));
|
||||||
return [...plans, ...qualifications, ...admissions, ...cutoffs, ...reports]
|
return [...plans, ...qualifications, ...roundAdmissions, ...admissions, ...cutoffs, ...reports]
|
||||||
.sort((left, right) => new Date(right.publishedAt) - new Date(left.publishedAt));
|
.sort((left, right) => new Date(right.publishedAt) - new Date(left.publishedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,9 +426,14 @@ export function createAdminRoutes(context) {
|
|||||||
const record = existing || { id: uid('admission_reporting'), kind: 'notification', examId: setting.examId, userId: null, schoolId: plan.schoolId, createdAt: now };
|
const record = existing || { id: uid('admission_reporting'), kind: 'notification', examId: setting.examId, userId: null, schoolId: plan.schoolId, createdAt: now };
|
||||||
return { ...record, status: 'draft', updatedAt: now, payload: { type: 'admission_reporting', round, rows, openedAt: now, openedBy: user.displayName } };
|
return { ...record, status: 'draft', updatedAt: now, payload: { type: 'admission_reporting', round, rows, openedAt: now, openedBy: user.displayName } };
|
||||||
});
|
});
|
||||||
setting.status = 'reporting'; setting.updatedAt = now; setting.payload.progress = `第 ${round} 轮录取结束,${admitted.length} 名考生已签发通知书,招生学校正在登记报到`;
|
const publicationDb = { ...db, admissionRecords: db.admissionRecords.map(item => admitted.find(entry => entry.id === item.id) || item) };
|
||||||
await database.saveAdmissionRecords([setting, ...admitted, ...notifications, ...reportingRecords], logAction(db, user, '签发录取通知书并开启报到', `${setting.examId} · ${admitted.length} 人`));
|
const existingPublication = admissionRecords(db, 'notification', setting.examId).find(item => item.userId == null && item.payload?.type === 'admission_round_publication' && Number(item.payload?.round || 1) === round);
|
||||||
return sendJson(response, 200, { ok: true, admittedCount: admitted.length, reportingSchoolCount: reportingRecords.length });
|
const admissionPublication = existingPublication || { id: uid('admission_round_publication'), kind: 'notification', examId: setting.examId, userId: null, schoolId: null, createdAt: now };
|
||||||
|
Object.assign(admissionPublication, { status: 'published', updatedAt: now, payload: { ...admissionPublication.payload, type: 'admission_round_publication', round, publishedAt: now, publishedBy: user.displayName, rows: publicAdmissionRows(publicationDb, setting.examId, { round }) } });
|
||||||
|
setting.status = 'reporting'; setting.updatedAt = now; setting.payload = { ...setting.payload, roundPublishedAt: now, progress: `第 ${round} 轮录取结束,${admitted.length} 名考生已签发通知书,录取名单已公示,招生学校正在登记报到` };
|
||||||
|
await database.saveAdmissionRecords([setting, ...admitted, ...notifications, ...reportingRecords, admissionPublication], logAction(db, user, '签发录取通知书并公示本轮录取名单', `${setting.examId} · 第 ${round} 轮 · ${admitted.length} 人`));
|
||||||
|
await cache.invalidate('public');
|
||||||
|
return sendJson(response, 200, { ok: true, admittedCount: admitted.length, reportingSchoolCount: reportingRecords.length, publicationId: admissionPublication.id });
|
||||||
}
|
}
|
||||||
if (action === 'supplementary') return sendError(response, 409, '补录必须由招生学校提交报到情况和补录决定,再经超级管理员审批开启');
|
if (action === 'supplementary') return sendError(response, 409, '补录必须由招生学校提交报到情况和补录决定,再经超级管理员审批开启');
|
||||||
const body = await readJson(request);
|
const body = await readJson(request);
|
||||||
@@ -1482,7 +1496,9 @@ export function createAdminRoutes(context) {
|
|||||||
if (!requirePermission(user, response, '*')) return true;
|
if (!requirePermission(user, response, '*')) return true;
|
||||||
const kindByType = { plan: 'plan', qualification: 'qualification_publication', admission: 'setting', cutoff: 'cutoff_publication', reporting: 'notification' };
|
const kindByType = { plan: 'plan', qualification: 'qualification_publication', admission: 'setting', cutoff: 'cutoff_publication', reporting: 'notification' };
|
||||||
const sourceType = publicationMatch[1];
|
const sourceType = publicationMatch[1];
|
||||||
const record = admissionRecords(db, kindByType[sourceType]).find(item => item.id === publicationMatch[2] && (sourceType !== 'reporting' || item.payload?.type === 'admission_reporting'));
|
const record = sourceType === 'admission'
|
||||||
|
? (db.admissionRecords || []).find(item => item.id === publicationMatch[2] && (item.kind === 'setting' || (item.kind === 'notification' && item.payload?.type === 'admission_round_publication')))
|
||||||
|
: admissionRecords(db, kindByType[sourceType]).find(item => item.id === publicationMatch[2] && (sourceType !== 'reporting' || item.payload?.type === 'admission_reporting'));
|
||||||
if (!record) return sendError(response, 404, '系统公示不存在');
|
if (!record) return sendError(response, 404, '系统公示不存在');
|
||||||
const body = await readJson(request);
|
const body = await readJson(request);
|
||||||
if (typeof body.visible !== 'boolean') return sendError(response, 400, '请明确设置是否显示');
|
if (typeof body.visible !== 'boolean') return sendError(response, 400, '请明确设置是否显示');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { noticeForClient } from '../security/notice-content.mjs';
|
import { noticeForClient } from '../security/notice-content.mjs';
|
||||||
import { admissionRecords, admissionSetting, publicAdmissionRows, sourceSchoolQualificationStatus } from '../services/volunteer-admission.mjs';
|
import { admissionRecords, admissionRoundPublications, admissionSetting, publicAdmissionRows, sourceSchoolQualificationStatus } from '../services/volunteer-admission.mjs';
|
||||||
import { specialtyLabel } from '../data/specialty-types.mjs';
|
import { specialtyLabel } from '../data/specialty-types.mjs';
|
||||||
import { systemNotificationItems } from '../services/system-notifications.mjs';
|
import { systemNotificationItems } from '../services/system-notifications.mjs';
|
||||||
|
|
||||||
@@ -121,7 +121,8 @@ export function createPublicRoutes(context) {
|
|||||||
id: item.id, examId: item.examId, examName: db.exams.find(exam => exam.id === item.examId)?.name || '', schoolName: db.schools.find(school => school.id === item.schoolId)?.name || '', publishedAt: item.payload?.publishedAt || item.updatedAt,
|
id: item.id, examId: item.examId, examName: db.exams.find(exam => exam.id === item.examId)?.name || '', schoolName: db.schools.find(school => school.id === item.schoolId)?.name || '', publishedAt: item.payload?.publishedAt || item.updatedAt,
|
||||||
rows: (item.payload?.rows || []).map(row => ({ registrationNumber: row.registrationNumber, name: row.name, eligible: row.eligible === true, specialtyLabel: row.specialtyLabel || '普通生' }))
|
rows: (item.payload?.rows || []).map(row => ({ registrationNumber: row.registrationNumber, name: row.name, eligible: row.eligible === true, specialtyLabel: row.specialtyLabel || '普通生' }))
|
||||||
})).sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
|
})).sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
|
||||||
const admissions = admissionRecords(db, 'setting').filter(item => item.status === 'completed' && item.payload?.autoPublish !== false && item.payload?.publicVisible !== false).map(setting => ({ id: setting.id, examId: setting.examId, examName: db.exams.find(item => item.id === setting.examId)?.name || '', publishedAt: setting.payload?.completedAt || setting.updatedAt, rows: publicAdmissionRows(db, setting.examId) })).sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
|
const roundAdmissions = admissionRoundPublications(db).filter(item => admissionSetting(db, item.examId)?.payload?.autoPublish !== false && item.payload?.publicVisible !== false).map(item => ({ id: item.id, examId: item.examId, examName: db.exams.find(exam => exam.id === item.examId)?.name || '', round: item.round, title: `${db.exams.find(exam => exam.id === item.examId)?.name || ''}第 ${item.round} 轮录取名单公示`, publishedAt: item.publishedAt, rows: item.rows }));
|
||||||
|
const admissions = [...roundAdmissions, ...admissionRecords(db, 'setting').filter(item => item.status === 'completed' && item.payload?.autoPublish !== false && item.payload?.publicVisible !== false).map(setting => ({ id: setting.id, examId: setting.examId, examName: db.exams.find(item => item.id === setting.examId)?.name || '', title: `${db.exams.find(item => item.id === setting.examId)?.name || ''}最终录取名单`, publishedAt: setting.payload?.completedAt || setting.updatedAt, rows: publicAdmissionRows(db, setting.examId) }))].sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
|
||||||
const cutoffs = admissionRecords(db, 'cutoff_publication').filter(item => item.status === 'published' && item.payload?.publicVisible !== false && admissionSetting(db, item.examId)?.payload?.autoPublish !== false).map(item => ({ id: item.id, examId: item.examId, examName: db.exams.find(exam => exam.id === item.examId)?.name || '', publishedAt: item.payload?.publishedAt || item.updatedAt, rows: item.payload?.rows || [] })).sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
|
const cutoffs = admissionRecords(db, 'cutoff_publication').filter(item => item.status === 'published' && item.payload?.publicVisible !== false && admissionSetting(db, item.examId)?.payload?.autoPublish !== false).map(item => ({ id: item.id, examId: item.examId, examName: db.exams.find(exam => exam.id === item.examId)?.name || '', publishedAt: item.payload?.publishedAt || item.updatedAt, rows: item.payload?.rows || [] })).sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
|
||||||
const reports = systemNotificationItems(db).filter(item => item.sourceType === 'reporting' && item.visible).map(item => ({ id: item.id, examId: item.examId, schoolId: item.schoolId, examName: db.exams.find(exam => exam.id === item.examId)?.name || '', schoolName: db.schools.find(school => school.id === item.schoolId)?.name || '', title: item.title, summary: item.summary, publishedAt: item.publishAt, statistics: admissionRecords(db, 'notification').find(record => record.id === item.id)?.payload?.statistics || {}, supplementDecision: admissionRecords(db, 'notification').find(record => record.id === item.id)?.payload?.supplementDecision || '', decisionNote: admissionRecords(db, 'notification').find(record => record.id === item.id)?.payload?.decisionNote || '' }));
|
const reports = systemNotificationItems(db).filter(item => item.sourceType === 'reporting' && item.visible).map(item => ({ id: item.id, examId: item.examId, schoolId: item.schoolId, examName: db.exams.find(exam => exam.id === item.examId)?.name || '', schoolName: db.schools.find(school => school.id === item.schoolId)?.name || '', title: item.title, summary: item.summary, publishedAt: item.publishAt, statistics: admissionRecords(db, 'notification').find(record => record.id === item.id)?.payload?.statistics || {}, supplementDecision: admissionRecords(db, 'notification').find(record => record.id === item.id)?.payload?.supplementDecision || '', decisionNote: admissionRecords(db, 'notification').find(record => record.id === item.id)?.payload?.decisionNote || '' }));
|
||||||
return { ok: true, plans, qualifications, admissions, cutoffs, reports };
|
return { ok: true, plans, qualifications, admissions, cutoffs, reports };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { admissionRecords, admissionSetting } from './volunteer-admission.mjs';
|
import { admissionRecords, admissionRoundPublications, admissionSetting } from './volunteer-admission.mjs';
|
||||||
|
|
||||||
const h = value => String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
const h = value => String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
||||||
|
|
||||||
@@ -32,6 +32,11 @@ export function systemNotificationItems(db) {
|
|||||||
'生源学校资格确认完成,系统已生成指标分配资格公示。', record.payload?.publishedAt || record.updatedAt,
|
'生源学校资格确认完成,系统已生成指标分配资格公示。', record.payload?.publishedAt || record.updatedAt,
|
||||||
`<p>${h(schoolName(record.schoolId))}指标分配资格确认已经完成,共 ${Number(record.payload?.rows?.length || 0)} 条记录。</p>`
|
`<p>${h(schoolName(record.schoolId))}指标分配资格确认已经完成,共 ${Number(record.payload?.rows?.length || 0)} 条记录。</p>`
|
||||||
));
|
));
|
||||||
|
const roundAdmissions = admissionRoundPublications(db).filter(record => admissionSetting(db, record.examId)?.payload?.autoPublish !== false).map(record => item(
|
||||||
|
record, 'admission', '录取名单', `${examName(record.examId)}第 ${record.round} 轮录取名单公示`,
|
||||||
|
`第 ${record.round} 轮录取通知书已签发,共 ${record.rows.length} 名考生进入本轮录取公示。`, record.publishedAt,
|
||||||
|
`<p>${h(examName(record.examId))}第 ${record.round} 轮录取工作已经完成,共 ${record.rows.length} 名考生正式录取。</p>`
|
||||||
|
));
|
||||||
const admissions = admissionRecords(db, 'setting').filter(record => record.status === 'completed' && record.payload?.autoPublish !== false).map(record => item(
|
const admissions = admissionRecords(db, 'setting').filter(record => record.status === 'completed' && record.payload?.autoPublish !== false).map(record => item(
|
||||||
record, 'admission', '录取名单', `${examName(record.examId)}最终录取名单`,
|
record, 'admission', '录取名单', `${examName(record.examId)}最终录取名单`,
|
||||||
'录取与报到决策已经办结,最终录取结果已自动公开。', record.payload?.completedAt || record.updatedAt,
|
'录取与报到决策已经办结,最终录取结果已自动公开。', record.payload?.completedAt || record.updatedAt,
|
||||||
@@ -53,7 +58,6 @@ export function systemNotificationItems(db) {
|
|||||||
return item(record, 'reporting', '考生报到', title, summary, record.payload?.approvedAt || record.updatedAt,
|
return item(record, 'reporting', '考生报到', title, summary, record.payload?.approvedAt || record.updatedAt,
|
||||||
`<p>${h(summary)}</p><p>${h(decision)}</p><ul><li>正式录取:${Number(stats.finalCount || 0)} 人</li><li>已报到:${Number(stats.reportedCount || 0)} 人</li><li>未报到:${Number(stats.notReportedCount || 0)} 人</li><li>计划缺额:${Number(stats.reportingGap || 0)} 人</li></ul><p>${h(record.payload?.approvalNote || '')}</p>`);
|
`<p>${h(summary)}</p><p>${h(decision)}</p><ul><li>正式录取:${Number(stats.finalCount || 0)} 人</li><li>已报到:${Number(stats.reportedCount || 0)} 人</li><li>未报到:${Number(stats.notReportedCount || 0)} 人</li><li>计划缺额:${Number(stats.reportingGap || 0)} 人</li></ul><p>${h(record.payload?.approvalNote || '')}</p>`);
|
||||||
});
|
});
|
||||||
return [...plans, ...qualifications, ...admissions, ...cutoffs, ...reports]
|
return [...plans, ...qualifications, ...roundAdmissions, ...admissions, ...cutoffs, ...reports]
|
||||||
.sort((left, right) => new Date(right.publishAt) - new Date(left.publishAt));
|
.sort((left, right) => new Date(right.publishAt) - new Date(left.publishAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,8 +148,11 @@ export function assignAdmissionNoticeNumbers(db, placements) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function publicAdmissionRows(db, examId) {
|
export function publicAdmissionRows(db, examId, options = {}) {
|
||||||
return admissionRecords(db, 'placement', examId).filter(item => item.status === 'final').map(item => {
|
const round = Math.max(0, Number(options.round || 0));
|
||||||
|
return admissionRecords(db, 'placement', examId).filter(item => round
|
||||||
|
? Number(item.payload?.finalizedRound || 1) === round && ['final', 'forfeited'].includes(item.status)
|
||||||
|
: item.status === 'final').map(item => {
|
||||||
const user = db.users.find(entry => entry.id === item.userId) || {};
|
const user = db.users.find(entry => entry.id === item.userId) || {};
|
||||||
const profile = db.candidateProfiles.find(entry => entry.userId === item.userId) || {};
|
const profile = db.candidateProfiles.find(entry => entry.userId === item.userId) || {};
|
||||||
const school = db.schools.find(entry => entry.id === item.schoolId) || {};
|
const school = db.schools.find(entry => entry.id === item.schoolId) || {};
|
||||||
@@ -165,6 +168,37 @@ export function publicAdmissionRows(db, examId) {
|
|||||||
}).sort((a, b) => b.totalScore - a.totalScore || a.registrationNumber.localeCompare(b.registrationNumber));
|
}).sort((a, b) => b.totalScore - a.totalScore || a.registrationNumber.localeCompare(b.registrationNumber));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function admissionRoundPublications(db) {
|
||||||
|
const stored = admissionRecords(db, 'notification')
|
||||||
|
.filter(item => item.userId == null && item.status === 'published' && item.payload?.type === 'admission_round_publication')
|
||||||
|
.map(item => ({ ...item, round: Number(item.payload?.round || 1), publishedAt: item.payload?.publishedAt || item.updatedAt, rows: item.payload?.rows || [], virtual: false }));
|
||||||
|
const keys = new Set(stored.map(item => `${item.examId}:${item.round}`));
|
||||||
|
const fallback = admissionRecords(db, 'setting').filter(item => ['reporting', 'supplementary', 'completed'].includes(item.status)).flatMap(setting => {
|
||||||
|
const rounds = admissionRecords(db, 'placement', setting.examId)
|
||||||
|
.filter(item => ['final', 'forfeited'].includes(item.status))
|
||||||
|
.map(item => Number(item.payload?.finalizedRound || 1));
|
||||||
|
const round = rounds.length ? Math.max(...rounds) : 0;
|
||||||
|
if (!round || keys.has(`${setting.examId}:${round}`)) return [];
|
||||||
|
return [{
|
||||||
|
id: `${setting.id}-round-${round}`,
|
||||||
|
sourceRecordId: setting.id,
|
||||||
|
kind: 'notification',
|
||||||
|
examId: setting.examId,
|
||||||
|
schoolId: null,
|
||||||
|
userId: null,
|
||||||
|
status: 'published',
|
||||||
|
createdAt: setting.updatedAt,
|
||||||
|
updatedAt: setting.updatedAt,
|
||||||
|
round,
|
||||||
|
publishedAt: setting.payload?.roundPublishedAt || setting.updatedAt,
|
||||||
|
rows: publicAdmissionRows(db, setting.examId, { round }),
|
||||||
|
virtual: true,
|
||||||
|
payload: { type: 'admission_round_publication', round, publicVisible: setting.payload?.publicVisible, publishedAt: setting.payload?.roundPublishedAt || setting.updatedAt }
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
return [...stored, ...fallback].sort((left, right) => new Date(right.publishedAt) - new Date(left.publishedAt));
|
||||||
|
}
|
||||||
|
|
||||||
export function admissionCutoffRows(db, examId) {
|
export function admissionCutoffRows(db, examId) {
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
for (const placement of admissionRecords(db, 'placement', examId).filter(item => item.status === 'final')) {
|
for (const placement of admissionRecords(db, 'placement', examId).filter(item => item.status === 'final')) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { admissionCutoffRows, admissionPlanProgress, assignAdmissionNoticeNumbers, buildVolunteerPlacements, candidateAdmissionScore, candidateTotalScore, publicAdmissionRows, remainingPlanQuota, sourceSchoolQualificationStatus } from '../src/services/volunteer-admission.mjs';
|
import { admissionCutoffRows, admissionPlanProgress, admissionRoundPublications, assignAdmissionNoticeNumbers, buildVolunteerPlacements, candidateAdmissionScore, candidateTotalScore, publicAdmissionRows, remainingPlanQuota, sourceSchoolQualificationStatus } from '../src/services/volunteer-admission.mjs';
|
||||||
import { candidateEligibleForCategory, specialtyLabel } from '../src/data/specialty-types.mjs';
|
import { candidateEligibleForCategory, specialtyLabel } from '../src/data/specialty-types.mjs';
|
||||||
import { systemNotificationItems } from '../src/services/system-notifications.mjs';
|
import { systemNotificationItems } from '../src/services/system-notifications.mjs';
|
||||||
|
|
||||||
@@ -97,6 +97,11 @@ const numbered = assignAdmissionNoticeNumbers(documentDb, targetPlacements);
|
|||||||
assert.deepEqual(numbered.map(item => item.payload.noticeNumber), ['AD02-EX-2026-ZK-000001', 'AD02-EX-2026-ZK-000002'], '通知书编号应按学校与考试独立生成连续流水号');
|
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');
|
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 = documentDb.admissionRecords.map(item => numbered.find(numberedItem => numberedItem.id === item.id) || item);
|
||||||
|
const legacyRoundDb = structuredClone(documentDb);
|
||||||
|
legacyRoundDb.admissionRecords.push({ id: 'setting-exam', kind: 'setting', examId: 'exam', status: 'reporting', updatedAt: now, payload: { enabled: true, autoPublish: true, round: 1, roundPublishedAt: now } });
|
||||||
|
const legacyRoundPublication = admissionRoundPublications(legacyRoundDb).find(item => item.examId === 'exam' && item.round === 1);
|
||||||
|
assert.ok(legacyRoundPublication?.virtual, '历史报到中数据缺少轮次公示记录时应自动兼容回显');
|
||||||
|
assert.equal(legacyRoundPublication.rows.length, publicAdmissionRows(legacyRoundDb, 'exam', { round: 1 }).length, '历史轮次公示应恢复该轮全部正式录取名单');
|
||||||
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' }] } });
|
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);
|
const progress = admissionPlanProgress(documentDb, reportingPlan);
|
||||||
assert.equal(progress.totalQuota, 2, '计划完成率分母应来自学校审核通过的计划人数');
|
assert.equal(progress.totalQuota, 2, '计划完成率分母应来自学校审核通过的计划人数');
|
||||||
|
|||||||
@@ -682,6 +682,13 @@ try {
|
|||||||
const finalizedAdmission = await admin.request(`/api/admin/admissions/${exam.id}/finalize`, { method: 'POST' });
|
const finalizedAdmission = await admin.request(`/api/admin/admissions/${exam.id}/finalize`, { method: 'POST' });
|
||||||
assert.equal(finalizedAdmission.response.status, 200, '超级管理员应能签发带编号的录取通知书并开启报到');
|
assert.equal(finalizedAdmission.response.status, 200, '超级管理员应能签发带编号的录取通知书并开启报到');
|
||||||
assert.equal(finalizedAdmission.data.admittedCount, 2, '正式签发人数应与学校接收人数一致');
|
assert.equal(finalizedAdmission.data.admittedCount, 2, '正式签发人数应与学校接收人数一致');
|
||||||
|
const roundAdmissionAnnouncements = await anonymous.request('/api/public/announcements');
|
||||||
|
const roundAdmissionPublication = roundAdmissionAnnouncements.data.admissions.find(item => item.examId === exam.id && item.round === 1);
|
||||||
|
assert.ok(roundAdmissionPublication, '每轮录取通知书签发后应立即自动生成本轮录取名单公示');
|
||||||
|
assert.equal(roundAdmissionPublication.rows.length, 2, '本轮录取公示应固定保存本轮全部正式录取考生');
|
||||||
|
assert.ok(roundAdmissionPublication.title.includes('第 1 轮录取名单公示'), '本轮公示标题应明确标注录取轮次');
|
||||||
|
const homeAfterRoundAdmission = await anonymous.request('/api/public/home');
|
||||||
|
assert.ok(homeAfterRoundAdmission.data.notices.some(item => item.title.includes('第 1 轮录取名单公示')), '本轮录取公示应同步进入首页通知公告');
|
||||||
const reportingLedger = await admissionSchoolClient.request('/api/admission/reporting');
|
const reportingLedger = await admissionSchoolClient.request('/api/admission/reporting');
|
||||||
const reportingBatch = reportingLedger.data.batches.find(item => item.exam.id === exam.id);
|
const reportingBatch = reportingLedger.data.batches.find(item => item.exam.id === exam.id);
|
||||||
assert.equal(reportingBatch.rows.length, 2, '招生学校报到台账应包含本轮全部正式录取考生');
|
assert.equal(reportingBatch.rows.length, 2, '招生学校报到台账应包含本轮全部正式录取考生');
|
||||||
|
|||||||
Reference in New Issue
Block a user