实现考试归档并永久锁定成绩流程
This commit is contained in:
@@ -753,6 +753,7 @@ export function createAdminRoutes(context) {
|
||||
const body = await readJson(request);
|
||||
const registration = db.registrations.find(item => item.id === registrationMatch[1]);
|
||||
if (!registration) return sendError(response, 404, '报名记录不存在');
|
||||
if (db.exams.find(item => item.id === registration.examId)?.archivedAt) return sendError(response, 409, '该考试已归档,报名流程已冻结');
|
||||
if (!registrationInScope(db, user, registration) && user.adminLevel !== 'super') return sendError(response, 403, '该报名不在你的数据范围内');
|
||||
if (!['approved', 'rejected'].includes(body.status)) return sendError(response, 400, '审核状态无效');
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
@@ -802,6 +803,7 @@ export function createAdminRoutes(context) {
|
||||
const note = cleanText(body.reviewNote, 300);
|
||||
const action = { id: uid('flow_action'), instanceId: instance.id, actorId: user.id, action: body.status === 'approved' ? 'approve' : 'reject', note, fromAssigneeId: instance.assigneeId, toAssigneeId: null, createdAt: nowIso() };
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
if (exam?.archivedAt) return sendError(response, 409, '该考试已归档,成绩及复议流程已永久锁定');
|
||||
const subject = exam?.subjects.find(item => item.id === result.subjectId);
|
||||
const finalStep = instance.currentStep >= workflow.steps.length;
|
||||
let reviewedScore = null;
|
||||
@@ -841,6 +843,8 @@ export function createAdminRoutes(context) {
|
||||
const arrangementMatch = pathname.match(/^\/api\/admin\/exams\/([^/]+)\/admission-arrangement(\/preview)?$/);
|
||||
if (request.method === 'POST' && arrangementMatch) {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const arrangementExam = db.exams.find(item => item.id === arrangementMatch[1]);
|
||||
if (arrangementExam?.archivedAt) return sendError(response, 409, '该考试已归档,不能重新编排准考证');
|
||||
const body = await readJson(request);
|
||||
const generatedAt = nowIso();
|
||||
const result = buildAdmissionArrangement(db, {
|
||||
@@ -897,12 +901,32 @@ export function createAdminRoutes(context) {
|
||||
await database.createExam(exam, log);
|
||||
return sendJson(response, 201, { ok: true, exam });
|
||||
}
|
||||
const examArchiveMatch = pathname.match(/^\/api\/admin\/exams\/([^/]+)\/archive$/);
|
||||
if (request.method === 'POST' && examArchiveMatch) {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const exam = db.exams.find(item => item.id === examArchiveMatch[1]);
|
||||
if (!exam) return sendError(response, 404, '考试不存在');
|
||||
if (exam.archivedAt) return sendError(response, 409, '该考试已经归档,归档操作不可撤销');
|
||||
const resultIds = new Set(db.results.filter(result => {
|
||||
const registration = db.registrations.find(item => item.id === result.registrationId);
|
||||
return registration?.examId === exam.id;
|
||||
}).map(result => result.id));
|
||||
const pendingAppeals = db.workflowInstances.filter(instance => instance.businessType === 'score_appeal' && instance.status === 'pending' && resultIds.has(instance.businessId));
|
||||
if (pendingAppeals.length) return sendError(response, 409, `本场还有 ${pendingAppeals.length} 项成绩复议待处理,请先办结后再归档`);
|
||||
exam.archivedAt = nowIso();
|
||||
exam.archivedBy = user.id;
|
||||
exam.status = 'closed';
|
||||
const log = logAction(db, user, '归档考试并永久锁定成绩', `${exam.name} · ${exam.code}`);
|
||||
await database.archiveExam(exam, log);
|
||||
return sendJson(response, 200, { ok: true, exam: publicExam(exam), message: '考试已归档,全部成绩已永久锁定' });
|
||||
}
|
||||
const examMatch = pathname.match(/^\/api\/admin\/exams\/([^/]+)$/);
|
||||
if (request.method === 'PATCH' && examMatch) {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const exam = db.exams.find(item => item.id === examMatch[1]);
|
||||
if (!exam) return sendError(response, 404, '考试不存在');
|
||||
if (exam.archivedAt) return sendError(response, 409, '该考试已归档,所有配置和成绩均已锁定');
|
||||
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 || body.passPolicy != null || body.passValue != null;
|
||||
@@ -1021,6 +1045,7 @@ export function createAdminRoutes(context) {
|
||||
const registration = db.registrations.find(item => item.id === body.registrationId && item.status === 'approved');
|
||||
if (!registration) return sendError(response, 404, '已通过的报名记录不存在');
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
if (exam?.archivedAt) return sendError(response, 409, '该考试已归档,成绩已永久锁定');
|
||||
if (!registration.subjectIds.includes(body.subjectId) || !exam.subjects.some(item => item.id === body.subjectId)) return sendError(response, 400, '该考生未报名此科目');
|
||||
const score = Number(body.score);
|
||||
const subject = exam.subjects.find(item => item.id === body.subjectId);
|
||||
|
||||
@@ -102,7 +102,7 @@ export function createCandidateRoutes(context) {
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/candidate/exams') {
|
||||
const registrations = db.registrations.filter(item => item.userId === user.id);
|
||||
const exams = db.exams.filter(item => item.status === 'published').map(exam => ({ ...publicExam(exam), registration: registrations.find(reg => reg.examId === exam.id) || null }));
|
||||
const exams = db.exams.filter(item => item.status === 'published' && !item.archivedAt).map(exam => ({ ...publicExam(exam), registration: registrations.find(reg => reg.examId === exam.id) || null }));
|
||||
return sendJson(response, 200, { ok: true, profileStatus: profile.status, exams });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/candidate/registrations') {
|
||||
@@ -111,7 +111,7 @@ export function createCandidateRoutes(context) {
|
||||
if (request.method === 'POST' && pathname === '/api/candidate/registrations') {
|
||||
if (profile.status !== 'approved') return sendError(response, 403, '个人资料审核通过后才能报名考试');
|
||||
const body = await readJson(request);
|
||||
const exam = db.exams.find(item => item.id === body.examId && item.status === 'published');
|
||||
const exam = db.exams.find(item => item.id === body.examId && item.status === 'published' && !item.archivedAt);
|
||||
if (!exam) return sendError(response, 404, '考试不存在或尚未发布');
|
||||
const state = publicExam(exam).registrationState;
|
||||
if (state !== 'open') return sendError(response, 400, state === 'upcoming' ? '报名尚未开始' : '报名已经截止');
|
||||
@@ -134,7 +134,7 @@ export function createCandidateRoutes(context) {
|
||||
const rank = resultRankInfo(db, result);
|
||||
const pass = subjectPassEvaluation(db, result, subject);
|
||||
return {
|
||||
...result, ...rank, grade: rank.grade, examId: exam.id, examName: exam.name, examCode: exam.code, examStart: exam.examStart,
|
||||
...result, ...rank, grade: rank.grade, examId: exam.id, examName: exam.name, examCode: exam.code, examStart: exam.examStart, archivedAt: exam.archivedAt || null,
|
||||
subjectName: subject?.name || result.subjectId, fullScore: subject?.fullScore || 150,
|
||||
passRule: subject?.passRule || 'fixed_score', passValue: subject?.passValue ?? subject?.passScore,
|
||||
passScore: pass.passScore, cutoffRank: pass.cutoffRank, passText: subjectPassText(subject), qualified: pass.qualified,
|
||||
@@ -149,13 +149,14 @@ export function createCandidateRoutes(context) {
|
||||
const result = db.results.find(item => item.id === scoreAppealMatch[1] && item.published);
|
||||
const registration = db.registrations.find(item => item.id === result?.registrationId && item.userId === user.id);
|
||||
if (!result || !registration) return sendError(response, 404, '已发布成绩不存在或不属于当前考生');
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
if (exam?.archivedAt) return sendError(response, 409, '该考试已归档,成绩及复议入口已永久锁定');
|
||||
if (pendingWorkflow(db, 'score_appeal', result.id)) return sendError(response, 409, '该科成绩已有待处理复议,请勿重复提交');
|
||||
const body = await readJson(request);
|
||||
const reason = cleanText(body.reason, 500);
|
||||
if (reason.length < 5) return sendError(response, 400, '请至少填写 5 个字的复议理由');
|
||||
const { instance, action } = createWorkflowSubmission(db, 'score_appeal', result.id, profile, user.id);
|
||||
action.note = reason;
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
const subject = exam?.subjects.find(item => item.id === result.subjectId);
|
||||
const log = logAction(db, user, '提交成绩复议', `${exam?.name || ''} · ${subject?.name || ''}`);
|
||||
await database.createWorkflow(instance, action, log);
|
||||
|
||||
@@ -51,8 +51,8 @@ export function createPublicRoutes(context) {
|
||||
const db = await readDb();
|
||||
if (pathname === '/api/public/home') {
|
||||
const publishedNotices = db.notices.filter(item => item.status === 'published').sort((a, b) => Number(b.pinned) - Number(a.pinned) || new Date(b.publishAt) - new Date(a.publishAt));
|
||||
const exams = db.exams.filter(item => item.status === 'published').map(exam => ({ ...publicExam(exam), registrationCount: db.registrations.filter(reg => reg.examId === exam.id).length }));
|
||||
return sendJson(response, 200, { ok: true, organization: db.organization, schools: db.schools.filter(item => item.active), classes: db.classes.filter(item => item.active), selfRegistrationEnabled: db.settings.selfRegistrationEnabled, notices: publishedNotices, exams, stats: { candidates: db.candidateProfiles.length, exams: db.exams.filter(item => item.status === 'published').length, registrations: db.registrations.length } });
|
||||
const exams = db.exams.filter(item => item.status === 'published' && !item.archivedAt).map(exam => ({ ...publicExam(exam), registrationCount: db.registrations.filter(reg => reg.examId === exam.id).length }));
|
||||
return sendJson(response, 200, { ok: true, organization: db.organization, schools: db.schools.filter(item => item.active), classes: db.classes.filter(item => item.active), selfRegistrationEnabled: db.settings.selfRegistrationEnabled, notices: publishedNotices, exams, stats: { candidates: db.candidateProfiles.length, exams: exams.length, registrations: db.registrations.length } });
|
||||
}
|
||||
const noticeMatch = pathname.match(/^\/api\/public\/notices\/([^/]+)$/);
|
||||
if (noticeMatch) {
|
||||
|
||||
Reference in New Issue
Block a user