Refactor exam information workflow
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
export function createCandidateRoutes(context) {
|
||||
const {
|
||||
database,
|
||||
readDb,
|
||||
sendJson,
|
||||
sendError,
|
||||
readJson,
|
||||
readBodyBuffer,
|
||||
sendWorkbook,
|
||||
currentUser,
|
||||
safeUser,
|
||||
requireUser,
|
||||
hasPermission,
|
||||
requirePermission,
|
||||
profileInScope,
|
||||
registrationInScope,
|
||||
adminScopeLabel,
|
||||
adminsForStep,
|
||||
activeWorkflow,
|
||||
createWorkflowSubmission,
|
||||
workflowView,
|
||||
pendingWorkflow,
|
||||
candidateSequence,
|
||||
generateCandidateNumber,
|
||||
cleanText,
|
||||
centerScopeProfile,
|
||||
workflowScopeProfile,
|
||||
candidateAccountBatchView,
|
||||
centerChangeView,
|
||||
parseCenterChange,
|
||||
maskId,
|
||||
publicExam,
|
||||
examRegistrationView,
|
||||
logAction,
|
||||
excelResourceNames,
|
||||
excelRowsForResource,
|
||||
importExcelResource,
|
||||
admitCardHtml,
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
uid,
|
||||
nowIso,
|
||||
sessions,
|
||||
buildWorkbook,
|
||||
hasExcelResource,
|
||||
parseWorkbook,
|
||||
adminLevelNames
|
||||
} = context;
|
||||
|
||||
async function handleCandidate(request, response, pathname) {
|
||||
if (!pathname.startsWith('/api/candidate/')) return false;
|
||||
const user = await requireUser(request, response, 'candidate');
|
||||
if (!user) return true;
|
||||
const db = await readDb();
|
||||
const profile = db.candidateProfiles.find(item => item.userId === user.id);
|
||||
if (user.mustChangePassword) return sendError(response, 428, '首次登录必须先修改初始密码');
|
||||
const profileRoute = pathname === '/api/candidate/profile';
|
||||
if (!profile.profileCompleted && !profileRoute) return sendError(response, 428, '请先补全个人信息并提交审核');
|
||||
|
||||
if (request.method === 'GET' && pathname === '/api/candidate/dashboard') {
|
||||
const registrations = db.registrations.filter(item => item.userId === user.id).map(item => examRegistrationView(db, item));
|
||||
const results = db.results.filter(result => result.published && registrations.some(reg => reg.id === result.registrationId));
|
||||
const notices = db.notices.filter(item => item.status === 'published').sort((a, b) => new Date(b.publishAt) - new Date(a.publishAt)).slice(0, 5);
|
||||
const profileInstance = pendingWorkflow(db, 'profile_change', profile.id)
|
||||
|| db.workflowInstances.filter(item => item.businessType === 'profile_change' && item.businessId === profile.id)[0];
|
||||
return sendJson(response, 200, { ok: true, profile, profileWorkflow: workflowView(db, profileInstance), registrations, results, notices });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/candidate/profile') {
|
||||
const instance = pendingWorkflow(db, 'profile_change', profile.id)
|
||||
|| db.workflowInstances.filter(item => item.businessType === 'profile_change' && item.businessId === profile.id)[0];
|
||||
return sendJson(response, 200, { ok: true, profile, workflow: workflowView(db, instance), schools: db.schools.filter(item => item.active), classes: db.classes.filter(item => item.active) });
|
||||
}
|
||||
if (request.method === 'PUT' && pathname === '/api/candidate/profile') {
|
||||
const body = await readJson(request);
|
||||
const fields = ['name', 'gender', 'idNumber', 'phone', 'email', 'address', 'emergencyContact', 'emergencyPhone', 'nativePlace', 'birthDate', 'ethnicity', 'postalCode', 'guardianName', 'guardianPhone'];
|
||||
for (const field of fields) profile[field] = cleanText(body[field], field === 'address' ? 160 : 80);
|
||||
const school = db.schools.find(item => item.id === cleanText(body.schoolId, 64) && item.active);
|
||||
const schoolClass = db.classes.find(item => item.id === cleanText(body.classId, 64) && item.schoolId === school?.id && item.active);
|
||||
if (!school || !schoolClass) return sendError(response, 400, '请选择有效的学校和班级');
|
||||
profile.schoolId = school.id;
|
||||
profile.classId = schoolClass.id;
|
||||
profile.school = school.name;
|
||||
profile.grade = schoolClass.name;
|
||||
if (!profile.name || !['男', '女'].includes(profile.gender) || !profile.idNumber || profile.idNumber.startsWith('PENDING-') || !profile.nativePlace || !profile.address || !profile.phone || !profile.email || !profile.school || !profile.classId) return sendError(response, 400, '请完整填写姓名、性别、证件号码、籍贯、家庭住址、手机号、邮箱、学校和班级');
|
||||
if (db.candidateProfiles.some(item => item.id !== profile.id && item.idNumber === profile.idNumber)) return sendError(response, 409, '证件号码已被其他考生使用');
|
||||
profile.status = 'pending';
|
||||
profile.profileCompleted = true;
|
||||
profile.reviewNote = '';
|
||||
profile.updatedAt = nowIso();
|
||||
const existingWorkflow = pendingWorkflow(db, 'profile_change', profile.id);
|
||||
const submission = existingWorkflow ? null : createWorkflowSubmission(db, 'profile_change', profile.id, profile, user.id);
|
||||
await database.updateCandidateProfile(profile, profile.name, submission?.instance, submission?.action);
|
||||
return sendJson(response, 200, { ok: true, profile, message: '资料已提交,等待管理员复核' });
|
||||
}
|
||||
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 }));
|
||||
return sendJson(response, 200, { ok: true, profileStatus: profile.status, exams });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/candidate/registrations') {
|
||||
return sendJson(response, 200, { ok: true, registrations: db.registrations.filter(item => item.userId === user.id).map(item => examRegistrationView(db, item)) });
|
||||
}
|
||||
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');
|
||||
if (!exam) return sendError(response, 404, '考试不存在或尚未发布');
|
||||
const state = publicExam(exam).registrationState;
|
||||
if (state !== 'open') return sendError(response, 400, state === 'upcoming' ? '报名尚未开始' : '报名已经截止');
|
||||
if (db.registrations.some(item => item.userId === user.id && item.examId === exam.id)) return sendError(response, 409, '你已经报名该考试');
|
||||
const subjectIds = [...new Set(Array.isArray(body.subjectIds) ? body.subjectIds : [])];
|
||||
if (!subjectIds.length || subjectIds.some(id => !exam.subjects.some(subject => subject.id === id))) return sendError(response, 400, '请选择有效的报考科目');
|
||||
const registration = { id: uid('reg'), userId: user.id, examId: exam.id, subjectIds, status: 'pending', paymentStatus: 'unpaid', createdAt: nowIso(), registrationNumber: user.candidateNumber, numberRuleId: db.numberRules.find(item => item.active)?.id || null, admitCard: null };
|
||||
const { instance, action } = createWorkflowSubmission(db, 'registration_review', registration.id, profile, user.id);
|
||||
await database.createRegistration(registration, instance, action);
|
||||
return sendJson(response, 201, { ok: true, registration: examRegistrationView(db, registration), message: '考试报名已提交' });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/candidate/results') {
|
||||
const registrations = db.registrations.filter(item => item.userId === user.id);
|
||||
const results = db.results.filter(item => item.published && registrations.some(reg => reg.id === item.registrationId)).map(result => {
|
||||
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 sendJson(response, 200, { ok: true, results });
|
||||
}
|
||||
const admitMatch = pathname.match(/^\/api\/candidate\/registrations\/([^/]+)\/admit-card$/);
|
||||
if (request.method === 'GET' && admitMatch) {
|
||||
const registration = db.registrations.find(item => item.id === admitMatch[1] && item.userId === user.id);
|
||||
if (!registration || !registration.admitCard) return sendError(response, 404, '准考证尚未生成');
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
const now = Date.now();
|
||||
if (now < new Date(exam.admitDownloadStart).getTime()) return sendError(response, 403, '准考证下载尚未开放');
|
||||
if (now > new Date(exam.admitDownloadEnd).getTime()) return sendError(response, 403, '准考证下载时间已结束');
|
||||
const html = admitCardHtml(db, user, profile, registration);
|
||||
const filename = encodeURIComponent(`${exam.name}-${profile.name}-准考证.html`);
|
||||
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Disposition': `attachment; filename*=UTF-8''${filename}`, 'Cache-Control': 'no-store' });
|
||||
response.end(html);
|
||||
return true;
|
||||
}
|
||||
return sendError(response, 404, '考生功能接口不存在');
|
||||
}
|
||||
|
||||
return handleCandidate;
|
||||
}
|
||||
Reference in New Issue
Block a user