This commit is contained in:
2026-07-20 10:45:35 +08:00 Unverified
parent 7094e9b916
commit 352f471ed0
17 changed files with 342 additions and 60 deletions
+58 -3
View File
@@ -247,10 +247,62 @@ function publicExam(exam) {
const end = new Date(exam.registrationEnd).getTime();
return {
...exam,
totalScore: exam.subjects.reduce((sum, subject) => sum + Number(subject.fullScore || 0), 0),
registrationState: now < start ? 'upcoming' : now > end ? 'closed' : 'open'
};
}
function examResultSummary(db, registration) {
const exam = db.exams.find(item => item.id === registration.examId);
if (!exam) return null;
const subjects = exam.subjects.filter(subject => registration.subjectIds.includes(subject.id));
const published = db.results.filter(result => result.registrationId === registration.id && result.published);
const resultsBySubject = new Map(published.map(result => [result.subjectId, result]));
const complete = subjects.length > 0 && subjects.every(subject => resultsBySubject.has(subject.id));
const total = subjects.reduce((sum, subject) => sum + Number(resultsBySubject.get(subject.id)?.score || 0), 0);
const fullScore = subjects.reduce((sum, subject) => sum + Number(subject.fullScore || 0), 0);
const scoreRatio = fullScore ? total / fullScore * 100 : 0;
const policy = exam.passPolicy || 'score_ratio';
const value = Number(exam.passValue ?? 60);
let qualified = null;
let rank = null;
let cohortSize = null;
if (complete && policy === 'fixed_score') qualified = total >= value;
if (complete && policy === 'score_ratio') qualified = scoreRatio >= value;
if (complete && policy === 'subject_scores') qualified = subjects.every(subject => Number(resultsBySubject.get(subject.id).score) >= Number(subject.passScore));
if (complete && policy === 'none') qualified = null;
if (complete && policy === 'rank_percent') {
const subjectKey = [...registration.subjectIds].sort().join('|');
const totals = db.registrations
.filter(item => item.examId === exam.id && item.status === 'approved' && [...item.subjectIds].sort().join('|') === subjectKey)
.map(item => {
const itemResults = db.results.filter(result => result.registrationId === item.id && result.published);
if (!item.subjectIds.every(id => itemResults.some(result => result.subjectId === id))) return null;
return itemResults.filter(result => item.subjectIds.includes(result.subjectId)).reduce((sum, result) => sum + Number(result.score), 0);
})
.filter(item => item != null);
cohortSize = totals.length;
rank = 1 + totals.filter(item => item > total).length;
qualified = rank <= Math.max(1, Math.ceil(cohortSize * value / 100));
}
return {
examId: exam.id,
complete,
publishedSubjects: published.length,
subjectCount: subjects.length,
total,
fullScore,
scoreRatio: Number(scoreRatio.toFixed(2)),
passPolicy: policy,
passValue: value,
qualified,
rank,
cohortSize
};
}
function examRegistrationView(db, registration) {
const exam = db.exams.find(item => item.id === registration.examId);
const subjects = (exam?.subjects || []).filter(subject => registration.subjectIds.includes(subject.id));
@@ -313,7 +365,8 @@ function excelRowsForResource(db, user, resource, searchParams) {
return db.results.filter(result => scopedRegistrations.some(item => item.id === result.registrationId)).map(result => {
const registration = db.registrations.find(item => item.id === result.registrationId);
const exam = db.exams.find(item => item.id === registration?.examId);
return { candidateNumber: db.users.find(item => item.id === registration?.userId)?.candidateNumber || '', examCode: exam?.code || '', subjectName: exam?.subjects.find(item => item.id === result.subjectId)?.name || '', score: result.score, grade: result.grade, published: result.published ? '发布' : '不发布' };
const subject = exam?.subjects.find(item => item.id === result.subjectId);
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 ? '发布' : '不发布' };
});
}
return [];
@@ -427,11 +480,12 @@ async function importExcelResource(db, user, resource, rows) {
const registration = db.registrations.find(item => item.userId === account?.id && item.examId === exam?.id && item.status === 'approved');
const subject = exam?.subjects.find(item => item.name === cleanText(row.subjectName, 50));
const score = Number(row.score);
if (!registration || !subject || !registration.subjectIds.includes(subject.id) || !Number.isFinite(score) || score < 0 || score > 150) throw excelImportError(row, '报名号、考试、科目或成绩无效');
if (!registration || !subject || !registration.subjectIds.includes(subject.id) || !Number.isFinite(score) || score < 0 || score > subject.fullScore) throw excelImportError(row, `报名号、考试、科目无效,或成绩不在 0—${subject?.fullScore || 0} 之间`);
let result = db.results.find(item => item.registrationId === registration.id && item.subjectId === subject.id);
const isNew = !result;
if (!result) result = { id: uid('result'), registrationId: registration.id, subjectId: subject.id };
Object.assign(result, { score, grade: cleanText(row.grade, 10) || (score >= 120 ? 'A' : score >= 90 ? 'B' : score >= 60 ? 'C' : 'D'), published: row.published === '发布', updatedAt: nowIso(), publishedAt: row.published === '发布' ? nowIso() : null });
const ratio = score / subject.fullScore;
Object.assign(result, { score, grade: cleanText(row.grade, 10) || (ratio >= .9 ? 'A+' : ratio >= .8 ? 'A' : ratio >= .7 ? 'B+' : ratio >= .6 ? 'B' : ratio >= .4 ? 'C' : 'D'), published: row.published === '发布', updatedAt: nowIso(), publishedAt: row.published === '发布' ? nowIso() : null });
await database.saveResult(result, isNew, logAction(db, user, 'Excel 导入成绩', `${row.candidateNumber} · ${exam.name} · ${subject.name}`));
if (isNew) db.results.push(result);
}
@@ -484,6 +538,7 @@ const routeContext = {
maskId,
publicExam,
examRegistrationView,
examResultSummary,
logAction,
excelResourceNames,
excelRowsForResource,