准考证编排
This commit is contained in:
+72
-21
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user