准考证优化
This commit is contained in:
+72
-5
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises';
|
||||
import { extname, join, normalize, resolve } from 'node:path';
|
||||
import { randomBytes, pbkdf2Sync, timingSafeEqual } from 'node:crypto';
|
||||
import { createDatabase } from './database.mjs';
|
||||
import { buildWorkbook, hasExcelResource, parseWorkbook } from './excel.mjs';
|
||||
import { buildCenterMaterialsWorkbook, buildWorkbook, hasExcelResource, parseWorkbook } from './excel.mjs';
|
||||
import { createAdminRoutes } from './src/routes/admin.routes.mjs';
|
||||
import { createCandidateRoutes } from './src/routes/candidate.routes.mjs';
|
||||
import { createAuthRoutes } from './src/routes/auth.routes.mjs';
|
||||
@@ -352,9 +352,44 @@ function logAction(db, user, action, detail) {
|
||||
|
||||
const excelResourceNames = {
|
||||
classes: '班级台账', class_admins: '班级管理员', account_quotas: '报名号班级配额',
|
||||
account_results: '报名号下发结果', candidates: '考生资料', centers: '考点考场档案', results: '成绩台账'
|
||||
account_results: '报名号下发结果', candidates: '考生资料', centers: '考点考场档案', results: '成绩台账',
|
||||
admit_cards: '准考证信息台账'
|
||||
};
|
||||
|
||||
function admissionRowsForRegistrations(db, registrations) {
|
||||
return registrations.flatMap(registration => {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId) || {};
|
||||
const account = db.users.find(item => item.id === registration.userId) || {};
|
||||
const exam = db.exams.find(item => item.id === registration.examId) || { subjects: [] };
|
||||
const school = db.schools.find(item => item.id === profile.schoolId);
|
||||
const schoolClass = db.classes.find(item => item.id === profile.classId);
|
||||
const card = registration.admitCard;
|
||||
if (!card) return [];
|
||||
const center = db.testCenters.find(item => item.id === card.centerId) || {};
|
||||
return (card.assignments || []).map(assignment => {
|
||||
const subject = exam.subjects.find(item => item.id === assignment.subjectId) || {};
|
||||
const room = db.testRooms.find(item => item.id === assignment.roomId) || {};
|
||||
return {
|
||||
schoolName: school?.name || profile.school || '', className: schoolClass?.name || profile.grade || '',
|
||||
candidateNumber: account.candidateNumber || registration.registrationNumber || '', candidateName: profile.name || account.displayName || '', idNumber: profile.idNumber || '',
|
||||
examCode: exam.code || '', examName: exam.name || '', cardNumber: card.number || '',
|
||||
centerCode: card.centerCode || center.code || '', centerName: card.testCenter || center.name || '',
|
||||
centerAddress: card.centerAddress || [center.provinceName, center.cityName, center.districtName, center.address].filter(Boolean).join(' '),
|
||||
subjectName: subject.name || '', subjectDate: subject.date || '', subjectTime: [subject.start, subject.end].filter(Boolean).join('—'),
|
||||
examRoomCode: assignment.examRoomCode || '', roomName: assignment.roomName || assignment.room || room.name || '',
|
||||
roomCode: assignment.roomCode || room.code || '', building: assignment.building || room.building || '',
|
||||
floor: assignment.floor || room.floor || '', seat: assignment.seat || ''
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function centerMaterialRows(db, schoolId, examId) {
|
||||
const centerIds = new Set(db.testCenters.filter(item => item.schoolId === schoolId).map(item => item.id));
|
||||
const registrations = db.registrations.filter(item => item.admitCard && item.examId === examId && centerIds.has(item.admitCard.centerId));
|
||||
return admissionRowsForRegistrations(db, registrations);
|
||||
}
|
||||
|
||||
function excelRowsForResource(db, user, resource, searchParams) {
|
||||
const schools = user.adminLevel === 'super' ? db.schools : db.schools.filter(item => item.id === user.schoolId);
|
||||
if (resource === 'classes') return db.classes.filter(item => schools.some(school => school.id === item.schoolId)).map(item => ({
|
||||
@@ -405,6 +440,11 @@ function excelRowsForResource(db, user, resource, searchParams) {
|
||||
return { candidateNumber: db.users.find(item => item.id === registration?.userId)?.candidateNumber || '', examCode: exam?.code || '', subjectName: subject?.name || '', fullScore: subject?.fullScore || '', passScore: subject?.passScore || '', score: result.score, grade: result.grade, published: result.published ? '发布' : '不发布' };
|
||||
});
|
||||
}
|
||||
if (resource === 'admit_cards') {
|
||||
const examId = cleanText(searchParams.get('examId'), 64);
|
||||
const scoped = db.registrations.filter(item => item.admitCard && registrationInScope(db, user, item) && (!examId || item.examId === examId));
|
||||
return admissionRowsForRegistrations(db, scoped);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -539,15 +579,38 @@ function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char]));
|
||||
}
|
||||
|
||||
function admitCardHtml(db, user, profile, registration) {
|
||||
function admitCardSection(db, user, profile, registration) {
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
const subjects = exam.subjects.filter(subject => registration.subjectIds.includes(subject.id));
|
||||
const assignments = new Map((registration.admitCard.assignments || []).map(item => [item.subjectId, item]));
|
||||
const center = db.testCenters.find(item => item.id === registration.admitCard.centerId) || {};
|
||||
const centerCode = registration.admitCard.centerCode || center.code || '';
|
||||
const centerAddress = registration.admitCard.centerAddress || [center.provinceName, center.cityName, center.districtName, center.address].filter(Boolean).join(' ');
|
||||
const rows = subjects.map(subject => {
|
||||
const assignment = assignments.get(subject.id) || {};
|
||||
return `<tr><td>${escapeHtml(subject.name)}</td><td>${escapeHtml(subject.date)}</td><td>${escapeHtml(subject.start)}—${escapeHtml(subject.end)}</td><td>${escapeHtml(assignment.roomName || assignment.room || '待定')}</td><td>${escapeHtml(assignment.seat || '—')}</td></tr>`;
|
||||
const room = db.testRooms.find(item => item.id === assignment.roomId) || {};
|
||||
return `<tr><td>${escapeHtml(subject.name)}</td><td>${escapeHtml(subject.date)}</td><td>${escapeHtml(subject.start)}—${escapeHtml(subject.end)}</td><td class="room-no">${escapeHtml(assignment.examRoomCode || '待定')}</td><td><strong>${escapeHtml(assignment.roomName || assignment.room || room.name || '待定')}</strong><small>场地代码:${escapeHtml(assignment.roomCode || room.code || '—')}</small></td><td>${escapeHtml(assignment.building || room.building || '—')}<small>${escapeHtml(assignment.floor || room.floor || '楼层待定')}</small></td><td>${escapeHtml(assignment.seat || '—')}</td></tr>`;
|
||||
}).join('');
|
||||
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>${escapeHtml(exam.name)}准考证</title><style>body{font-family:"Microsoft YaHei",sans-serif;color:#182552;padding:32px;background:#f2f4f8}.card{max-width:800px;margin:auto;background:#fff;border:2px solid #182552;padding:32px;box-shadow:0 12px 35px #18255218}.head{display:flex;justify-content:space-between;border-bottom:3px double #182552;padding-bottom:18px}.brand{font-size:28px;font-weight:800}.code{font-family:monospace}.meta{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:24px 0}.meta div{border-bottom:1px solid #ddd;padding:8px}.meta span{color:#777;font-size:12px;margin-right:12px}table{width:100%;border-collapse:collapse}th,td{border:1px solid #ccd2df;padding:12px;text-align:left}th{background:#f3f5f9}.notice{margin-top:20px;padding:15px;background:#fff7e6;color:#704b12;font-size:13px}.footer{display:flex;justify-content:space-between;margin-top:22px;color:#777;font-size:12px}@media print{body{background:#fff;padding:0}.card{box-shadow:none}}</style></head><body><main class="card"><header class="head"><div><div class="brand">衡准 · 准考证</div><p>${escapeHtml(exam.name)}</p></div><div class="code">${escapeHtml(registration.admitCard.number)}</div></header><section class="meta"><div><span>姓名</span><strong>${escapeHtml(profile.name || user.displayName)}</strong></div><div><span>证件号码</span><strong>${escapeHtml(maskId(profile.idNumber))}</strong></div><div><span>固定考点</span><strong>${escapeHtml(registration.admitCard.testCenter)}</strong></div></section><table><thead><tr><th>科目</th><th>日期</th><th>时间</th><th>考场</th><th>座位</th></tr></thead><tbody>${rows}</tbody></table><div class="notice">所有科目均安排在同一考点,但不同科目可能对应不同考场和座位。请逐科核对,并携带本人有效身份证件及本准考证至少提前 40 分钟到达考点。</div><footer class="footer"><span>${escapeHtml(db.organization.name)}</span><span>生成时间:${new Date(registration.admitCard.generatedAt).toLocaleString('zh-CN')}</span></footer></main></body></html>`;
|
||||
return `<main class="card"><header class="head"><div><div class="brand">衡准 · 准考证</div><p>${escapeHtml(exam.name)}</p></div><div><small>准考证号</small><div class="code">${escapeHtml(registration.admitCard.number)}</div></div></header><section class="meta"><div><span>姓名</span><strong>${escapeHtml(profile.name || user.displayName)}</strong></div><div><span>证件号码</span><strong>${escapeHtml(maskId(profile.idNumber))}</strong></div><div><span>报名号</span><strong>${escapeHtml(user.candidateNumber || registration.registrationNumber || '—')}</strong></div></section><section class="center"><div><span>固定考点</span><strong>${escapeHtml(registration.admitCard.testCenter)}</strong><small>考点代码:${escapeHtml(centerCode || '—')}</small></div><div><span>考点详细地址</span><strong>${escapeHtml(centerAddress || '地址待公布')}</strong></div></section><table><thead><tr><th>科目</th><th>日期</th><th>时间</th><th>考试考场序号</th><th>考场通用名称 / 场地代码</th><th>楼栋 / 楼层</th><th>座位号</th></tr></thead><tbody>${rows}</tbody></table><div class="explain"><strong>编号说明</strong>“考试考场序号”是本次考试编排编号;“考场通用名称 / 场地代码”是考点内的物理场地档案,两者不是同一概念。</div><div class="notice">所有科目均安排在同一考点,但不同科目可能对应不同考试考场序号、物理场地和座位。请逐科核对,并携带本人有效身份证件及本准考证至少提前 40 分钟到达考点。</div><footer class="footer"><span>${escapeHtml(db.organization.name)}</span><span>生成时间:${new Date(registration.admitCard.generatedAt).toLocaleString('zh-CN')}</span></footer></main>`;
|
||||
}
|
||||
|
||||
function admitCardsDocument(db, items, title) {
|
||||
const cards = items.map(item => admitCardSection(db, item.user, item.profile, item.registration)).join('');
|
||||
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>@page{size:A4 landscape;margin:10mm}*{box-sizing:border-box}body{font-family:"Microsoft YaHei",sans-serif;color:#182552;margin:0;background:#f2f4f8}.card{max-width:1120px;min-height:720px;margin:24px auto;background:#fff;border:2px solid #182552;padding:26px;box-shadow:0 12px 35px #18255218;break-after:page}.card:last-child{break-after:auto}.head{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:3px double #182552;padding-bottom:14px}.head>div:last-child{text-align:right}.head small{color:#64748b}.brand{font-size:27px;font-weight:800}.head p{margin:6px 0}.code{font:700 22px Consolas,monospace;margin-top:5px}.meta{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:16px 0}.meta div,.center>div{border:1px solid #d9e0e7;padding:9px 12px}.meta span,.center span{display:block;color:#64748b;font-size:11px;margin-bottom:4px}.center{display:grid;grid-template-columns:1fr 1.5fr;gap:10px;margin-bottom:16px}.center small,td small{display:block;color:#64748b;margin-top:4px}table{width:100%;border-collapse:collapse;font-size:12px}th,td{border:1px solid #b9c5d2;padding:9px;text-align:left;vertical-align:middle}th{background:#eaf1f5}.room-no{font:700 15px Consolas,monospace;text-align:center}.explain,.notice{margin-top:12px;padding:10px 12px;font-size:12px}.explain{background:#eaf4f6;color:#245365}.explain strong{margin-right:8px}.notice{background:#fff7e6;color:#704b12}.footer{display:flex;justify-content:space-between;margin-top:14px;color:#64748b;font-size:11px}@media print{body{background:#fff}.card{margin:0;box-shadow:none}}</style></head><body>${cards}</body></html>`;
|
||||
}
|
||||
|
||||
function admitCardHtml(db, user, profile, registration) {
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
return admitCardsDocument(db, [{ user, profile, registration }], `${exam.name}-${profile.name}-准考证`);
|
||||
}
|
||||
|
||||
function admitCardsHtml(db, registrations, title) {
|
||||
const items = registrations.map(registration => ({
|
||||
registration,
|
||||
user: db.users.find(item => item.id === registration.userId) || {},
|
||||
profile: db.candidateProfiles.find(item => item.userId === registration.userId) || {}
|
||||
}));
|
||||
return admitCardsDocument(db, items, title);
|
||||
}
|
||||
|
||||
const routeContext = {
|
||||
@@ -588,8 +651,11 @@ const routeContext = {
|
||||
logAction,
|
||||
excelResourceNames,
|
||||
excelRowsForResource,
|
||||
admissionRowsForRegistrations,
|
||||
centerMaterialRows,
|
||||
importExcelResource,
|
||||
admitCardHtml,
|
||||
admitCardsHtml,
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
randomBytes,
|
||||
@@ -597,6 +663,7 @@ const routeContext = {
|
||||
nowIso,
|
||||
sessions,
|
||||
buildWorkbook,
|
||||
buildCenterMaterialsWorkbook,
|
||||
hasExcelResource,
|
||||
parseWorkbook,
|
||||
adminLevelNames,
|
||||
|
||||
Reference in New Issue
Block a user