性能
This commit is contained in:
+51
-12
@@ -49,6 +49,41 @@ export function createAdminRoutes(context) {
|
||||
permissionsByLevel
|
||||
} = context;
|
||||
|
||||
const passPolicies = new Set(['fixed_score', 'score_ratio', 'rank_percent', 'subject_scores', 'none']);
|
||||
|
||||
function normalizeSubjects(input, examStart) {
|
||||
const source = Array.isArray(input) ? input : String(input || '').split(/[,,]/);
|
||||
return source.map((item, index) => {
|
||||
const structured = item && typeof item === 'object';
|
||||
const name = cleanText(structured ? item.name : item, 50);
|
||||
if (!name) return null;
|
||||
const fullScore = Number(structured ? item.fullScore : 150);
|
||||
return {
|
||||
id: uid('sub'),
|
||||
name,
|
||||
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),
|
||||
fullScore,
|
||||
passScore: Number(structured ? item.passScore : fullScore * .6),
|
||||
order: index + 1
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function validateExamScoring(subjects, passPolicy, passValue) {
|
||||
if (!subjects.length) return '请至少添加一个考试科目';
|
||||
if (subjects.some(item => !Number.isFinite(item.fullScore) || item.fullScore <= 0 || item.fullScore > 1000)) return '科目满分必须大于 0 且不超过 1000';
|
||||
if (subjects.some(item => !Number.isFinite(item.passScore) || item.passScore < 0 || item.passScore > item.fullScore)) return '单科合格分必须在 0 与该科满分之间';
|
||||
if (subjects.some(item => !Number.isFinite(item.fee) || item.fee < 0 || item.fee > 100000)) return '科目费用必须在有效范围内';
|
||||
if (!passPolicies.has(passPolicy)) return '请选择有效的合格线策略';
|
||||
const totalScore = subjects.reduce((sum, item) => sum + item.fullScore, 0);
|
||||
if (passPolicy === 'fixed_score' && (!Number.isFinite(passValue) || passValue < 0 || passValue > totalScore)) return `固定合格线必须在 0 与总分 ${totalScore} 之间`;
|
||||
if (['score_ratio', 'rank_percent'].includes(passPolicy) && (!Number.isFinite(passValue) || passValue <= 0 || passValue > 100)) return '百分比必须大于 0 且不超过 100';
|
||||
return '';
|
||||
}
|
||||
|
||||
async function handleAdmin(request, response, pathname) {
|
||||
if (!pathname.startsWith('/api/admin/')) return false;
|
||||
const user = await requireUser(request, response, 'admin');
|
||||
@@ -607,10 +642,12 @@ export function createAdminRoutes(context) {
|
||||
const body = await readJson(request);
|
||||
const name = cleanText(body.name, 100);
|
||||
if (!name || !body.registrationStart || !body.registrationEnd || !body.examStart || !body.examEnd) return sendError(response, 400, '请完整填写考试名称和关键日期');
|
||||
const subjectNames = Array.isArray(body.subjects) ? body.subjects : String(body.subjects || '').split(/[,,]/);
|
||||
const subjects = subjectNames.map(name => cleanText(typeof name === 'string' ? name : name.name, 30)).filter(Boolean).map((name, index) => ({ id: uid('sub'), name, date: cleanText(body.examStart, 10), start: '09:00', end: '11:00', fee: 0, order: index + 1 }));
|
||||
if (!subjects.length) return sendError(response, 400, '请至少添加一个考试科目');
|
||||
const exam = { id: uid('exam'), code: cleanText(body.code, 30) || `EX-${new Date().getFullYear()}-${String(db.exams.length + 1).padStart(2, '0')}`, name, description: cleanText(body.description, 500), registrationStart: body.registrationStart, registrationEnd: body.registrationEnd, examStart: body.examStart, examEnd: body.examEnd, admitDownloadStart: body.admitDownloadStart || body.registrationEnd, admitDownloadEnd: body.admitDownloadEnd || body.examStart, location: cleanText(body.location, 100), status: body.status === 'published' ? 'published' : 'draft', subjects, createdAt: nowIso() };
|
||||
const subjects = normalizeSubjects(body.subjects, body.examStart);
|
||||
const passPolicy = passPolicies.has(body.passPolicy) ? body.passPolicy : 'score_ratio';
|
||||
const passValue = ['subject_scores', 'none'].includes(passPolicy) ? 0 : Number(body.passValue ?? 60);
|
||||
const scoringError = validateExamScoring(subjects, passPolicy, passValue);
|
||||
if (scoringError) return sendError(response, 400, scoringError);
|
||||
const exam = { id: uid('exam'), code: cleanText(body.code, 30) || `EX-${new Date().getFullYear()}-${String(db.exams.length + 1).padStart(2, '0')}`, name, description: cleanText(body.description, 500), registrationStart: body.registrationStart, registrationEnd: body.registrationEnd, examStart: body.examStart, examEnd: body.examEnd, admitDownloadStart: body.admitDownloadStart || body.registrationEnd, admitDownloadEnd: body.admitDownloadEnd || body.examStart, location: cleanText(body.location, 100), passPolicy, passValue, status: body.status === 'published' ? 'published' : 'draft', subjects, createdAt: nowIso() };
|
||||
const log = logAction(db, user, '创建考试', `${exam.name} · ${subjects.length} 个科目`);
|
||||
await database.createExam(exam, log);
|
||||
return sendJson(response, 201, { ok: true, exam });
|
||||
@@ -623,19 +660,20 @@ export function createAdminRoutes(context) {
|
||||
if (!exam) return sendError(response, 404, '考试不存在');
|
||||
const originalStatus = exam.status;
|
||||
const detailFields = ['code', 'name', 'description', 'location', 'registrationStart', 'registrationEnd', 'examStart', 'examEnd', 'admitDownloadStart', 'admitDownloadEnd'];
|
||||
const editingDetails = detailFields.some(field => body[field] != null) || body.subjects != null;
|
||||
const editingDetails = detailFields.some(field => body[field] != null) || body.subjects != null || body.passPolicy != null || body.passValue != null;
|
||||
if (editingDetails && originalStatus !== 'draft') return sendError(response, 409, '请先将考试撤回为草稿后再编辑');
|
||||
if (body.status && ['draft', 'published', 'closed'].includes(body.status)) exam.status = body.status;
|
||||
detailFields.forEach(field => { if (body[field] != null) exam[field] = cleanText(body[field], field === 'description' ? 500 : 100); });
|
||||
if (body.passPolicy != null && passPolicies.has(body.passPolicy)) exam.passPolicy = body.passPolicy;
|
||||
if (body.passValue != null) exam.passValue = Number(body.passValue);
|
||||
let replaceSubjects = false;
|
||||
if (body.subjects != null) {
|
||||
if (db.registrations.some(registration => registration.examId === exam.id)) return sendError(response, 409, '已有报名记录,不能修改考试科目');
|
||||
const subjectNames = Array.isArray(body.subjects) ? body.subjects : String(body.subjects || '').split(/[,,]/);
|
||||
const names = subjectNames.map(item => cleanText(typeof item === 'string' ? item : item.name, 30)).filter(Boolean);
|
||||
if (!names.length) return sendError(response, 400, '请至少添加一个考试科目');
|
||||
exam.subjects = names.map((name, index) => ({ id: uid('sub'), name, date: String(exam.examStart).slice(0, 10), start: '09:00', end: '11:00', fee: 0, order: index + 1 }));
|
||||
exam.subjects = normalizeSubjects(body.subjects, exam.examStart);
|
||||
replaceSubjects = true;
|
||||
}
|
||||
const scoringError = validateExamScoring(exam.subjects, exam.passPolicy, Number(exam.passValue));
|
||||
if (scoringError) return sendError(response, 400, scoringError);
|
||||
if (!exam.name || !exam.registrationStart || !exam.registrationEnd || !exam.examStart || !exam.examEnd) return sendError(response, 400, '请完整填写考试名称和关键日期');
|
||||
if (exam.status === 'published' && !exam.subjects.length) return sendError(response, 400, '请先配置考试科目再发布');
|
||||
const log = logAction(db, user, '更新考试', `${exam.name} · 状态 ${exam.status}`);
|
||||
@@ -693,16 +731,17 @@ export function createAdminRoutes(context) {
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
if (!registration.subjectIds.includes(body.subjectId) || !exam.subjects.some(item => item.id === body.subjectId)) return sendError(response, 400, '该考生未报名此科目');
|
||||
const score = Number(body.score);
|
||||
if (!Number.isFinite(score) || score < 0 || score > 150) return sendError(response, 400, '成绩必须在 0—150 之间');
|
||||
const subject = exam.subjects.find(item => item.id === body.subjectId);
|
||||
if (!Number.isFinite(score) || score < 0 || score > subject.fullScore) return sendError(response, 400, `成绩必须在 0—${subject.fullScore} 之间`);
|
||||
let result = db.results.find(item => item.registrationId === registration.id && item.subjectId === body.subjectId);
|
||||
const isNew = !result;
|
||||
if (!result) {
|
||||
result = { id: uid('result'), registrationId: registration.id, subjectId: body.subjectId };
|
||||
db.results.push(result);
|
||||
}
|
||||
Object.assign(result, { score, grade: cleanText(body.grade, 10) || (score >= 135 ? 'A+' : score >= 120 ? 'A' : score >= 105 ? 'B+' : score >= 90 ? 'B' : score >= 60 ? 'C' : 'D'), published: Boolean(body.published), updatedAt: nowIso(), publishedAt: body.published ? nowIso() : null });
|
||||
const ratio = score / subject.fullScore;
|
||||
Object.assign(result, { score, grade: cleanText(body.grade, 10) || (ratio >= .9 ? 'A+' : ratio >= .8 ? 'A' : ratio >= .7 ? 'B+' : ratio >= .6 ? 'B' : ratio >= .4 ? 'C' : 'D'), published: Boolean(body.published), updatedAt: nowIso(), publishedAt: body.published ? nowIso() : null });
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
const subject = exam.subjects.find(item => item.id === body.subjectId);
|
||||
const log = logAction(db, user, body.published ? '发布成绩' : '保存成绩', `${profile?.name} · ${subject?.name} · ${score}`);
|
||||
await database.saveResult(result, isNew, log);
|
||||
return sendJson(response, 200, { ok: true, result });
|
||||
|
||||
@@ -31,6 +31,7 @@ export function createCandidateRoutes(context) {
|
||||
maskId,
|
||||
publicExam,
|
||||
examRegistrationView,
|
||||
examResultSummary,
|
||||
logAction,
|
||||
excelResourceNames,
|
||||
excelRowsForResource,
|
||||
@@ -121,9 +122,10 @@ export function createCandidateRoutes(context) {
|
||||
const registration = registrations.find(reg => reg.id === result.registrationId);
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
const subject = exam.subjects.find(item => item.id === result.subjectId);
|
||||
return { ...result, examName: exam.name, examCode: exam.code, subjectName: subject?.name || result.subjectId };
|
||||
return { ...result, examId: exam.id, examName: exam.name, examCode: exam.code, subjectName: subject?.name || result.subjectId, fullScore: subject?.fullScore || 150, passScore: subject?.passScore || 90 };
|
||||
});
|
||||
return sendJson(response, 200, { ok: true, results });
|
||||
const summaries = registrations.map(registration => examResultSummary(db, registration)).filter(summary => summary?.publishedSubjects);
|
||||
return sendJson(response, 200, { ok: true, results, summaries });
|
||||
}
|
||||
const admitMatch = pathname.match(/^\/api\/candidate\/registrations\/([^/]+)\/admit-card$/);
|
||||
if (request.method === 'GET' && admitMatch) {
|
||||
|
||||
Reference in New Issue
Block a user