招生管理完善
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 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 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 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));
|
||||
@@ -83,7 +83,7 @@ export function createPublicViews(context) {
|
||||
const page = publicPaged(document.rows, key);
|
||||
const schools = [...new Set(document.rows.map(row => row.admittedSchool).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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { admissionMixingScopes, buildAdmissionArrangement } from '../services/admission-arrangement.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 { systemNotificationItems } from '../services/system-notifications.mjs';
|
||||
|
||||
@@ -151,7 +151,16 @@ export function createAdminRoutes(context) {
|
||||
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,
|
||||
'admission',
|
||||
'录取名单',
|
||||
@@ -171,7 +180,7 @@ export function createAdminRoutes(context) {
|
||||
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
|
||||
}));
|
||||
return [...plans, ...qualifications, ...admissions, ...cutoffs, ...reports]
|
||||
return [...plans, ...qualifications, ...roundAdmissions, ...admissions, ...cutoffs, ...reports]
|
||||
.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 };
|
||||
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} 名考生已签发通知书,招生学校正在登记报到`;
|
||||
await database.saveAdmissionRecords([setting, ...admitted, ...notifications, ...reportingRecords], logAction(db, user, '签发录取通知书并开启报到', `${setting.examId} · ${admitted.length} 人`));
|
||||
return sendJson(response, 200, { ok: true, admittedCount: admitted.length, reportingSchoolCount: reportingRecords.length });
|
||||
const publicationDb = { ...db, admissionRecords: db.admissionRecords.map(item => admitted.find(entry => entry.id === item.id) || item) };
|
||||
const existingPublication = admissionRecords(db, 'notification', setting.examId).find(item => item.userId == null && item.payload?.type === 'admission_round_publication' && Number(item.payload?.round || 1) === round);
|
||||
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, '补录必须由招生学校提交报到情况和补录决定,再经超级管理员审批开启');
|
||||
const body = await readJson(request);
|
||||
@@ -1482,7 +1496,9 @@ export function createAdminRoutes(context) {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const kindByType = { plan: 'plan', qualification: 'qualification_publication', admission: 'setting', cutoff: 'cutoff_publication', reporting: 'notification' };
|
||||
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, '系统公示不存在');
|
||||
const body = await readJson(request);
|
||||
if (typeof body.visible !== 'boolean') return sendError(response, 400, '请明确设置是否显示');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { 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,
|
||||
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));
|
||||
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 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 };
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -32,6 +32,11 @@ export function systemNotificationItems(db) {
|
||||
'生源学校资格确认完成,系统已生成指标分配资格公示。', record.payload?.publishedAt || record.updatedAt,
|
||||
`<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(
|
||||
record, 'admission', '录取名单', `${examName(record.examId)}最终录取名单`,
|
||||
'录取与报到决策已经办结,最终录取结果已自动公开。', record.payload?.completedAt || record.updatedAt,
|
||||
@@ -53,7 +58,6 @@ export function systemNotificationItems(db) {
|
||||
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>`);
|
||||
});
|
||||
return [...plans, ...qualifications, ...admissions, ...cutoffs, ...reports]
|
||||
return [...plans, ...qualifications, ...roundAdmissions, ...admissions, ...cutoffs, ...reports]
|
||||
.sort((left, right) => new Date(right.publishAt) - new Date(left.publishAt));
|
||||
}
|
||||
|
||||
|
||||
@@ -148,8 +148,11 @@ export function assignAdmissionNoticeNumbers(db, placements) {
|
||||
return output;
|
||||
}
|
||||
|
||||
export function publicAdmissionRows(db, examId) {
|
||||
return admissionRecords(db, 'placement', examId).filter(item => item.status === 'final').map(item => {
|
||||
export function publicAdmissionRows(db, examId, options = {}) {
|
||||
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 profile = db.candidateProfiles.find(entry => entry.userId === item.userId) || {};
|
||||
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));
|
||||
}
|
||||
|
||||
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) {
|
||||
const groups = new Map();
|
||||
for (const placement of admissionRecords(db, 'placement', examId).filter(item => item.status === 'final')) {
|
||||
|
||||
Reference in New Issue
Block a user