Add indicator qualification and admissions announcements

This commit is contained in:
2026-07-21 14:37:06 +08:00 Unverified
parent 5e0bee60e2
commit 6be1ff227a
17 changed files with 332 additions and 70 deletions
+48 -5
View File
@@ -1,6 +1,6 @@
import { admissionMixingScopes, buildAdmissionArrangement } from '../services/admission-arrangement.mjs';
import { noticeForClient, noticePlainText, sanitizeNoticeContent } from '../security/notice-content.mjs';
import { admissionPhases, admissionRecords, admissionSetting, buildVolunteerPlacements, candidateTotalScore, publicAdmissionRows, remainingPlanQuota } from '../services/volunteer-admission.mjs';
import { admissionCutoffRows, admissionPhases, admissionRecords, admissionSetting, buildVolunteerPlacements, candidateTotalScore, publicAdmissionRows, remainingPlanQuota, sourceSchoolQualificationStatus } from '../services/volunteer-admission.mjs';
import { isValidSpecialty, resolveProfileSpecialty, specialtyLabel } from '../data/specialty-types.mjs';
export function createAdminRoutes(context) {
@@ -141,6 +141,44 @@ export function createAdminRoutes(context) {
});
}
if (pathname === '/api/admin/indicator-qualifications' && request.method === 'GET') {
if (user.adminLevel !== 'school' || !user.schoolId) return sendError(response, 403, '只有生源校学校管理员可以确认指标分配资格');
const school = db.schools.find(item => item.id === user.schoolId && item.active && item.isSourceSchool);
if (!school) return sendError(response, 403, '当前学校未设置为生源学校');
const exams = admissionRecords(db, 'setting').filter(item => item.payload?.enabled).map(setting => {
const exam = db.exams.find(item => item.id === setting.examId);
return { ...setting, exam: exam ? publicExam(exam) : null, qualificationStatus: sourceSchoolQualificationStatus(db, setting.examId, school.id) };
}).filter(item => item.exam);
return sendJson(response, 200, { ok: true, school, exams });
}
const qualificationMatch = pathname.match(/^\/api\/admin\/indicator-qualifications\/([^/]+)\/([^/]+)$/);
if (qualificationMatch && request.method === 'PUT') {
if (user.adminLevel !== 'school' || !user.schoolId) return sendError(response, 403, '只有生源校学校管理员可以确认指标分配资格');
const setting = admissionSetting(db, qualificationMatch[1]);
const profile = db.candidateProfiles.find(item => item.userId === qualificationMatch[2] && item.schoolId === user.schoolId && item.profileCompleted);
const account = db.users.find(item => item.id === profile?.userId && item.role === 'candidate' && item.active);
if (!setting?.payload?.enabled) return sendError(response, 404, '该考试未启用志愿填报');
if (!profile || !account) return sendError(response, 404, '本校有效考生不存在');
const body = await readJson(request);
if (typeof body.eligible !== 'boolean') return sendError(response, 400, '请选择有或无指标分配资格');
const now = nowIso();
const existing = admissionRecords(db, 'indicator_qualification', setting.examId).find(item => item.userId === profile.userId);
const qualification = existing || { id: uid('indicator_qualification'), kind: 'indicator_qualification', examId: setting.examId, userId: profile.userId, schoolId: user.schoolId, createdAt: now };
Object.assign(qualification, { status: 'confirmed', updatedAt: now, payload: { eligible: body.eligible, confirmedBy: user.displayName, confirmedAt: now } });
const nextRecords = [...db.admissionRecords.filter(item => item.id !== qualification.id), qualification];
const nextDb = { ...db, admissionRecords: nextRecords };
const status = sourceSchoolQualificationStatus(nextDb, setting.examId, user.schoolId);
const records = [qualification];
if (status.complete) {
const published = admissionRecords(nextDb, 'qualification_publication', setting.examId).find(item => item.schoolId === user.schoolId);
const publication = published || { id: uid('qualification_publication'), kind: 'qualification_publication', examId: setting.examId, userId: user.id, schoolId: user.schoolId, createdAt: now };
Object.assign(publication, { status: 'published', updatedAt: now, payload: { publishedAt: now, rows: status.rows } });
records.push(publication);
}
await database.saveAdmissionRecords(records, logAction(db, user, '确认指标分配资格', `${account.candidateNumber} · ${body.eligible ? '有资格' : '无资格'}${status.complete ? ' · 全校已自动公示' : ''}`));
return sendJson(response, 200, { ok: true, qualification, qualificationStatus: status, published: status.complete, message: status.complete ? '资格已确认;本校全部考生确认完成,公示已自动发布' : '资格已确认' });
}
if (pathname === '/api/admin/admissions' && request.method === 'GET') {
if (user.adminLevel !== 'super') return sendError(response, 403, '只有超级管理员可以查看志愿与录取数据');
const settings = admissionRecords(db, 'setting').map(setting => ({ ...setting, exam: db.exams.find(item => item.id === setting.examId), publicRows: setting.status === 'completed' ? publicAdmissionRows(db, setting.examId) : [] }));
@@ -184,7 +222,7 @@ export function createAdminRoutes(context) {
const status = setting.status && !manualPhases.includes(setting.status) ? setting.status : manualPhases.includes(requestedStatus) ? requestedStatus : (setting.status || 'draft');
setting.status = status;
setting.updatedAt = now;
setting.payload = { ...setting.payload, enabled: body.enabled === true, preferenceStart: cleanText(body.preferenceStart, 35), preferenceEnd: cleanText(body.preferenceEnd, 35), maxChoices: Math.min(20, Math.max(1, Math.trunc(Number(body.maxChoices || 5)))), round: Math.max(1, Math.trunc(Number(body.round || setting.payload?.round || 1))), autoPublish: body.autoPublish !== false, progress: cleanText(body.progress, 200) || '等待志愿填报开始' };
setting.payload = { ...setting.payload, enabled: body.enabled === true, preferenceStart: cleanText(body.preferenceStart, 35), preferenceEnd: cleanText(body.preferenceEnd, 35), maxChoices: Math.min(20, Math.max(1, Math.trunc(Number(body.maxChoices || 5)))), maxSubmissions: Math.min(50, Math.max(1, Math.trunc(Number(body.maxSubmissions || 3)))), round: Math.max(1, Math.trunc(Number(body.round || setting.payload?.round || 1))), autoPublish: body.autoPublish !== false, progress: cleanText(body.progress, 200) || '等待志愿填报开始' };
if (setting.payload.preferenceStart && setting.payload.preferenceEnd && new Date(setting.payload.preferenceStart) >= new Date(setting.payload.preferenceEnd)) return sendError(response, 400, '志愿填报结束时间必须晚于开始时间');
await database.saveAdmissionRecord(setting, logAction(db, user, '设置志愿填报', `${exam.name} · ${status}`));
return sendJson(response, 200, { ok: true, setting });
@@ -241,9 +279,14 @@ export function createAdminRoutes(context) {
const now = nowIso();
const admitted = placements.filter(item => item.status === 'admitted').map(item => ({ ...item, status: 'final', updatedAt: now }));
const notifications = admitted.map(item => ({ id: uid('notification'), kind: 'notification', examId: setting.examId, userId: item.userId, schoolId: item.schoolId, status: 'unread', createdAt: now, updatedAt: now, payload: { title: '录取结果通知', message: `你已被${db.schools.find(school => school.id === item.schoolId)?.name || '招生学校'}录取`, placementId: item.id } }));
setting.status = 'completed'; setting.updatedAt = now; setting.payload.progress = '本次录取工作已结束,录取结果已经通知并自动公'; setting.payload.completedAt = now;
await database.saveAdmissionRecords([setting, ...admitted, ...notifications], logAction(db, user, '结束录取并发布结果', `${setting.examId} · ${admitted.length}`));
return sendJson(response, 200, { ok: true, admittedCount: admitted.length, publicRows: publicAdmissionRows({ ...db, admissionRecords: [...db.admissionRecords.filter(item => !admitted.some(entry => entry.id === item.id)), ...admitted] }, setting.examId) });
setting.status = 'completed'; setting.updatedAt = now; setting.payload.progress = '本次录取工作已结束,录取结果与分数线已经自动公'; setting.payload.completedAt = now;
const completedDb = { ...db, admissionRecords: [...db.admissionRecords.filter(item => !admitted.some(entry => entry.id === item.id)), ...admitted] };
const cutoffRows = admissionCutoffRows(completedDb, setting.examId);
const existingCutoff = admissionRecords(db, 'cutoff_publication', setting.examId)[0];
const cutoffPublication = existingCutoff || { id: uid('cutoff_publication'), kind: 'cutoff_publication', examId: setting.examId, userId: user.id, schoolId: null, createdAt: now };
Object.assign(cutoffPublication, { status: 'published', updatedAt: now, payload: { publishedAt: now, rows: cutoffRows } });
await database.saveAdmissionRecords([setting, ...admitted, ...notifications, cutoffPublication], logAction(db, user, '结束录取并发布结果与分数线', `${setting.examId} · ${admitted.length}`));
return sendJson(response, 200, { ok: true, admittedCount: admitted.length, publicRows: publicAdmissionRows(completedDb, setting.examId), cutoffRows });
}
const body = await readJson(request);
const now = nowIso();