准考证优化

This commit is contained in:
2026-07-20 14:18:37 +08:00 Unverified
parent 9566542414
commit c5b18d0b15
13 changed files with 447 additions and 57 deletions
+49 -9
View File
@@ -37,8 +37,11 @@ export function createAdminRoutes(context) {
logAction,
excelResourceNames,
excelRowsForResource,
admissionRowsForRegistrations,
centerMaterialRows,
importExcelResource,
admitCardHtml,
admitCardsHtml,
hashPassword,
verifyPassword,
randomBytes,
@@ -46,6 +49,7 @@ export function createAdminRoutes(context) {
nowIso,
sessions,
buildWorkbook,
buildCenterMaterialsWorkbook,
hasExcelResource,
parseWorkbook,
adminLevelNames,
@@ -593,8 +597,9 @@ export function createAdminRoutes(context) {
return sendJson(response, 200, { ok: true, registrations });
}
if (request.method === 'GET' && pathname === '/api/admin/admission-arrangements') {
if (!requirePermission(user, response, '*')) return true;
const registrations = db.registrations.filter(item => item.status === 'approved').map(registration => {
if (!requirePermission(user, response, 'registrations.read')) return true;
const scopedRegistrations = db.registrations.filter(item => item.status === 'approved' && registrationInScope(db, user, item));
const registrations = scopedRegistrations.map(registration => {
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
return { ...examRegistrationView(db, registration), candidate: profile ? { ...profile, idNumber: maskId(profile.idNumber) } : null };
});
@@ -604,26 +609,61 @@ export function createAdminRoutes(context) {
ruleName: db.admissionNumberRules.find(item => item.id === plan.numberRuleId)?.name || '',
mixingScopeName: admissionMixingScopes.find(item => item.code === plan.mixingScope)?.name || plan.mixingScope
}));
const exams = db.exams.map(exam => ({
const visibleExams = user.adminLevel === 'super' ? db.exams : db.exams.filter(exam => scopedRegistrations.some(item => item.examId === exam.id));
const exams = visibleExams.map(exam => ({
...publicExam(exam),
approvedCount: db.registrations.filter(item => item.examId === exam.id && item.status === 'approved').length,
arrangedCount: db.registrations.filter(item => item.examId === exam.id && item.admitCard).length,
approvedCount: scopedRegistrations.filter(item => item.examId === exam.id).length,
arrangedCount: scopedRegistrations.filter(item => item.examId === exam.id && item.admitCard).length,
plan: plans.find(item => item.examId === exam.id) || null
}));
const centerScope = user.adminLevel === 'super'
? db.testCenters
: user.adminLevel === 'school' ? db.testCenters.filter(item => item.schoolId === user.schoolId) : [];
return sendJson(response, 200, {
ok: true,
canArrange: user.adminLevel === 'super',
canExportCenterMaterials: user.adminLevel === 'school',
scopeLabel: adminScopeLabel(db, user),
exams,
registrations,
plans,
rules: db.admissionNumberRules.filter(item => item.active),
mixingScopes: admissionMixingScopes,
centers: db.testCenters.filter(item => item.status === 'active').map(center => ({
plans: user.adminLevel === 'super' ? plans : [],
rules: user.adminLevel === 'super' ? db.admissionNumberRules.filter(item => item.active) : [],
mixingScopes: user.adminLevel === 'super' ? admissionMixingScopes : [],
centers: centerScope.filter(item => item.status === 'active').map(center => ({
...center,
roomCount: db.testRooms.filter(room => room.centerId === center.id && room.status === 'active').length,
capacity: db.testRooms.filter(room => room.centerId === center.id && room.status === 'active' && room.roomType !== 'spare').reduce((sum, room) => sum + room.capacity, 0)
}))
});
}
const admissionExportMatch = pathname.match(/^\/api\/admin\/admission-exports\/(admit-cards|info|center-materials)$/);
if (request.method === 'GET' && admissionExportMatch) {
if (!requirePermission(user, response, 'registrations.read')) return true;
const requestUrl = new URL(request.url, `http://${request.headers.host || '127.0.0.1'}`);
const examId = cleanText(requestUrl.searchParams.get('examId'), 64);
const exam = db.exams.find(item => item.id === examId);
if (!exam) return sendError(response, 404, '请选择有效考试');
const type = admissionExportMatch[1];
if (type === 'center-materials') {
if (user.adminLevel !== 'school') return sendError(response, 403, '考点桌贴、门贴和签名单只能由维护该考点的校级管理员导出');
const rows = centerMaterialRows(db, user.schoolId, exam.id);
if (!rows.length) return sendError(response, 404, '本校维护考点暂无该考试的已编排考生');
const buffer = Buffer.from(await buildCenterMaterialsWorkbook(rows, `${exam.name}${adminScopeLabel(db, user)}`));
return sendWorkbook(response, buffer, `${exam.name}-${adminScopeLabel(db, user)}-考点桌贴门贴签名单.xlsx`);
}
const scoped = db.registrations.filter(item => item.examId === exam.id && item.admitCard && registrationInScope(db, user, item));
if (!scoped.length) return sendError(response, 404, '当前范围暂无已生成的准考证');
if (type === 'admit-cards') {
const html = admitCardsHtml(db, scoped, `${exam.name}-${adminScopeLabel(db, user)}-准考证`);
const filename = encodeURIComponent(`${exam.name}-${adminScopeLabel(db, user)}-准考证批量打印.html`);
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Disposition': `attachment; filename*=UTF-8''${filename}`, 'Cache-Control': 'no-store' });
response.end(html);
return true;
}
const rows = admissionRowsForRegistrations(db, scoped);
const buffer = Buffer.from(await buildWorkbook('admit_cards', rows, { subtitle: `${exam.name}${adminScopeLabel(db, user)}` }));
return sendWorkbook(response, buffer, `${exam.name}-${adminScopeLabel(db, user)}-准考证信息.xlsx`);
}
const registrationMatch = pathname.match(/^\/api\/admin\/registrations\/([^/]+)$/);
if (request.method === 'PATCH' && registrationMatch) {
if (!requirePermission(user, response, 'registrations.review')) return true;