准考证编排

This commit is contained in:
2026-07-20 13:06:00 +08:00 Unverified
parent 4c7fafc03d
commit 9566542414
14 changed files with 846 additions and 86 deletions
+72 -21
View File
@@ -1,3 +1,5 @@
import { admissionMixingScopes, buildAdmissionArrangement } from '../services/admission-arrangement.mjs';
export function createAdminRoutes(context) {
const {
database,
@@ -65,7 +67,7 @@ export function createAdminRoutes(context) {
date: cleanText(structured ? item.date : '', 10) || String(examStart).slice(0, 10),
start: cleanText(structured ? item.start : '', 5) || '09:00',
end: cleanText(structured ? item.end : '', 5) || '11:00',
fee: Number(structured ? item.fee : 0),
fee: Number(structured ? item.fee ?? 0 : 0),
fullScore,
passScore: Number(structured ? item.passScore : fullScore * .6),
order: index + 1
@@ -590,6 +592,38 @@ 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 => {
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
return { ...examRegistrationView(db, registration), candidate: profile ? { ...profile, idNumber: maskId(profile.idNumber) } : null };
});
const plans = db.arrangementPlans.map(plan => ({
...plan,
examName: db.exams.find(item => item.id === plan.examId)?.name || '',
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 => ({
...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,
plan: plans.find(item => item.examId === exam.id) || null
}));
return sendJson(response, 200, {
ok: true,
exams,
registrations,
plans,
rules: db.admissionNumberRules.filter(item => item.active),
mixingScopes: admissionMixingScopes,
centers: db.testCenters.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 registrationMatch = pathname.match(/^\/api\/admin\/registrations\/([^/]+)$/);
if (request.method === 'PATCH' && registrationMatch) {
if (!requirePermission(user, response, 'registrations.review')) return true;
@@ -627,7 +661,6 @@ export function createAdminRoutes(context) {
await database.processWorkflow(instance, action, registration, log);
return sendJson(response, 200, { ok: true, registration, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) });
}
const admitMatch = pathname.match(/^\/api\/admin\/registrations\/([^/]+)\/admit-card$/);
const scoreAppealMatch = pathname.match(/^\/api\/admin\/score-appeals\/([^/]+)$/);
if (request.method === 'PATCH' && scoreAppealMatch) {
if (!requirePermission(user, response, 'workflows.inbox')) return true;
@@ -661,26 +694,44 @@ export function createAdminRoutes(context) {
await database.processWorkflow(instance, action, null, log);
return sendJson(response, 200, { ok: true, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) });
}
if (request.method === 'POST' && admitMatch) {
const arrangementMatch = pathname.match(/^\/api\/admin\/exams\/([^/]+)\/admission-arrangement(\/preview)?$/);
if (request.method === 'POST' && arrangementMatch) {
if (!requirePermission(user, response, '*')) return true;
const registration = db.registrations.find(item => item.id === admitMatch[1]);
if (!registration) return sendError(response, 404, '报名记录不存在');
if (registration.status !== 'approved') return sendError(response, 400, '报名审核通过后才能生成准考证');
if (!registration.admitCard) {
const exam = db.exams.find(item => item.id === registration.examId);
const sequence = String(db.registrations.filter(item => item.examId === exam.id && item.admitCard).length + 1).padStart(4, '0');
registration.admitCard = {
number: `${exam.code.replace(/[^a-z0-9]/gi, '').toUpperCase().slice(-12) || String(new Date().getFullYear())}-${sequence}`,
testCenter: cleanText((await readJson(request)).testCenter || '海州市第一中学', 80),
room: `0${Math.ceil(Number(sequence) / 30) || 1} 考场`,
seat: String(((Number(sequence) - 1) % 30) + 1).padStart(2, '0'),
generatedAt: nowIso()
};
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
const log = logAction(db, user, '生成准考证', `${profile?.name || registration.userId} · ${registration.admitCard.number}`);
await database.createAdmitCard(registration.id, registration.admitCard, log);
}
return sendJson(response, 200, { ok: true, admitCard: registration.admitCard });
const body = await readJson(request);
const generatedAt = nowIso();
const result = buildAdmissionArrangement(db, {
examId: arrangementMatch[1],
mixingScope: cleanText(body.mixingScope, 20),
numberRuleId: cleanText(body.numberRuleId, 64),
seed: cleanText(body.seed, 80),
generatedAt
});
if (arrangementMatch[2]) return sendJson(response, 200, {
ok: true,
preview: true,
summary: result.summary,
warnings: result.warnings,
samples: result.cards.slice(0, 5)
});
const plan = {
id: uid('arrangement'),
examId: result.exam.id,
numberRuleId: result.rule.id,
mixingScope: result.mixingScope,
randomSeed: result.seed,
...result.summary,
warnings: result.warnings,
generatedBy: user.id,
generatedAt
};
const log = logAction(db, user, db.arrangementPlans.some(item => item.examId === result.exam.id) ? '重新编排准考证' : '批量编排准考证',
`${result.exam.name} · ${plan.candidateCount} 人 · ${plan.centerCount} 个考点 · ${result.rule.name}`);
await database.saveAdmissionArrangement(plan, result.cards, log);
return sendJson(response, 200, { ok: true, plan, summary: result.summary, warnings: result.warnings, cards: result.cards });
}
const legacyAdmitMatch = pathname.match(/^\/api\/admin\/registrations\/([^/]+)\/admit-card$/);
if (request.method === 'POST' && legacyAdmitMatch) {
return sendError(response, 410, '单人生成已停用,请在“准考证编排”中按整场考试预检并批量生成');
}
if (request.method === 'GET' && pathname === '/api/admin/exams') {
if (!requirePermission(user, response, '*')) return true;