Implement new application functionality and supporting components
This commit is contained in:
+671
-42
@@ -39,25 +39,40 @@ function verifyPassword(password, stored) {
|
||||
|
||||
function seedDatabase() {
|
||||
const adminId = 'usr_admin';
|
||||
const schoolAdminId = 'usr_school_admin';
|
||||
const schoolAdmin2Id = 'usr_school_admin_2';
|
||||
const candidateId = 'usr_demo';
|
||||
const examId = 'exam_autumn_2026';
|
||||
const registrationId = 'reg_demo_2026';
|
||||
return {
|
||||
meta: { version: 1, createdAt: nowIso() },
|
||||
meta: { version: 3, createdAt: nowIso() },
|
||||
organization: {
|
||||
name: '海州市教育考试中心',
|
||||
code: 'HZ-EDU-032',
|
||||
phone: '0518-8602 3158',
|
||||
address: '海州市清河区文教路 18 号'
|
||||
},
|
||||
schools: [
|
||||
{ id: 'school_hz1', name: '海州市第一中学', code: 'HZ01', address: '海州市清河区学府路 8 号', active: true },
|
||||
{ id: 'school_hz3', name: '海州市第三中学', code: 'HZ03', address: '海州市滨河区育才路 16 号', active: true }
|
||||
],
|
||||
classes: [
|
||||
{ id: 'class_hz1_301', schoolId: 'school_hz1', name: '高三(1)班', grade: '高三', active: true },
|
||||
{ id: 'class_hz1_302', schoolId: 'school_hz1', name: '高三(2)班', grade: '高三', active: true },
|
||||
{ id: 'class_hz3_301', schoolId: 'school_hz3', name: '高三(1)班', grade: '高三', active: true }
|
||||
],
|
||||
users: [
|
||||
{ id: adminId, username: 'admin', passwordHash: hashPassword('Admin123!'), role: 'admin', displayName: '林老师', createdAt: nowIso() },
|
||||
{ id: candidateId, username: '13800138000', passwordHash: hashPassword('Candidate123!'), role: 'candidate', displayName: '周雨桐', createdAt: nowIso() }
|
||||
{ id: adminId, username: 'admin', passwordHash: hashPassword('Admin123!'), role: 'admin', adminLevel: 'super', displayName: '林老师', active: true, createdAt: nowIso() },
|
||||
{ id: 'usr_supervisor', username: 'supervisor', passwordHash: hashPassword('Admin123!'), role: 'admin', adminLevel: 'super', displayName: '赵督导', active: true, createdAt: nowIso() },
|
||||
{ id: schoolAdminId, username: 'school_admin', passwordHash: hashPassword('School123!'), role: 'admin', adminLevel: 'school', schoolId: 'school_hz1', displayName: '王校管', active: true, createdAt: nowIso() },
|
||||
{ id: schoolAdmin2Id, username: 'school_admin_2', passwordHash: hashPassword('School123!'), role: 'admin', adminLevel: 'school', schoolId: 'school_hz1', displayName: '陈校管', active: true, createdAt: nowIso() },
|
||||
{ id: 'usr_class_admin', username: 'class_admin', passwordHash: hashPassword('Class123!'), role: 'admin', adminLevel: 'class', schoolId: 'school_hz1', classId: 'class_hz1_302', displayName: '孙班管', active: true, createdAt: nowIso() },
|
||||
{ id: candidateId, username: '13800138000', passwordHash: hashPassword('Candidate123!'), role: 'candidate', displayName: '周雨桐', active: true, createdAt: nowIso() }
|
||||
],
|
||||
candidateProfiles: [
|
||||
{
|
||||
id: 'profile_demo', userId: candidateId, name: '周雨桐', gender: '女', idNumber: '320101200808164821',
|
||||
phone: '13800138000', email: 'zhou@example.com', school: '海州市第一中学', grade: '高三(2)班',
|
||||
phone: '13800138000', email: 'zhou@example.com', school: '海州市第一中学', grade: '高三(2)班', schoolId: 'school_hz1', classId: 'class_hz1_302',
|
||||
address: '海州市清河区', emergencyContact: '周建国', emergencyPhone: '13900139000',
|
||||
status: 'approved', reviewNote: '身份信息与学籍信息核验一致', reviewedAt: '2026-07-18T08:30:00.000Z', updatedAt: '2026-07-17T09:20:00.000Z'
|
||||
}
|
||||
@@ -95,7 +110,7 @@ function seedDatabase() {
|
||||
registrations: [
|
||||
{
|
||||
id: registrationId, userId: candidateId, examId, subjectIds: ['sub_chinese', 'sub_math', 'sub_physics', 'sub_english', 'sub_chemistry'],
|
||||
status: 'approved', paymentStatus: 'paid', createdAt: '2026-07-08T05:18:00.000Z', reviewedAt: '2026-07-18T08:32:00.000Z',
|
||||
status: 'approved', paymentStatus: 'paid', createdAt: '2026-07-08T05:18:00.000Z', reviewedAt: '2026-07-18T08:32:00.000Z', registrationNumber: '', numberRuleId: null,
|
||||
admitCard: { number: '260816-031-08', testCenter: '海州市第三中学', room: '031 考场', seat: '08', generatedAt: '2026-07-19T02:00:00.000Z' }
|
||||
}
|
||||
],
|
||||
@@ -103,6 +118,42 @@ function seedDatabase() {
|
||||
{ id: 'result_demo_1', registrationId, subjectId: 'sub_chinese', score: 118, grade: 'B+', published: true, publishedAt: '2026-07-19T03:00:00.000Z' },
|
||||
{ id: 'result_demo_2', registrationId, subjectId: 'sub_math', score: 132, grade: 'A', published: true, publishedAt: '2026-07-19T03:00:00.000Z' }
|
||||
],
|
||||
testCenters: [
|
||||
{ id: 'center_hz1', schoolId: 'school_hz1', code: 'HZ01-C01', name: '海州市第一中学考点', address: '海州市清河区学府路 8 号', contact: '0518-8602 1101', managerName: '王立新', managerPhone: '13800001101', emergencyPhone: '0518-8602 1190', gateOpenTime: '07:00', transport: '地铁 2 号线学府路站 2 号口,步行约 600 米', status: 'active', notes: '南门为考生唯一入口,无障碍通道位于东侧。', rooms: '教学楼 A:001、002;实验楼:机考 01', updatedAt: nowIso() },
|
||||
{ id: 'center_hz3', schoolId: 'school_hz3', code: 'HZ03-C01', name: '海州市第三中学考点', address: '海州市滨河区育才路 16 号', contact: '0518-8602 3301', managerName: '李文峰', managerPhone: '13800003301', emergencyPhone: '0518-8602 3390', gateOpenTime: '07:10', transport: '公交 18、32 路育才路站,考点不提供社会车辆停车位', status: 'active', notes: '西门设置临时物品存放区。', rooms: '笃学楼:001、002', updatedAt: nowIso() }
|
||||
],
|
||||
testRooms: [
|
||||
{ id: 'room_hz1_001', centerId: 'center_hz1', code: '001', name: '第 001 考场', building: '教学楼 A', floor: '1 层', capacity: 30, seatStart: 1, seatEnd: 30, roomType: 'standard', status: 'active', notes: '' },
|
||||
{ id: 'room_hz1_002', centerId: 'center_hz1', code: '002', name: '第 002 考场', building: '教学楼 A', floor: '1 层', capacity: 30, seatStart: 31, seatEnd: 60, roomType: 'standard', status: 'active', notes: '' },
|
||||
{ id: 'room_hz1_pc01', centerId: 'center_hz1', code: 'PC01', name: '机考 01 考场', building: '实验楼', floor: '3 层', capacity: 40, seatStart: 1, seatEnd: 40, roomType: 'computer', status: 'active', notes: '配备备用终端 4 台' },
|
||||
{ id: 'room_hz3_001', centerId: 'center_hz3', code: '001', name: '第 001 考场', building: '笃学楼', floor: '1 层', capacity: 30, seatStart: 1, seatEnd: 30, roomType: 'standard', status: 'active', notes: '' },
|
||||
{ id: 'room_hz3_002', centerId: 'center_hz3', code: '002', name: '第 002 考场', building: '笃学楼', floor: '1 层', capacity: 30, seatStart: 31, seatEnd: 60, roomType: 'accessible', status: 'active', notes: '靠近无障碍通道' }
|
||||
],
|
||||
centerChangeRequests: [],
|
||||
centerChangeRooms: [],
|
||||
numberRules: [
|
||||
{ id: 'rule_default', name: '年度学校性别流水号', separator: '-', active: true, createdBy: adminId, updatedAt: nowIso(), segments: [
|
||||
{ id: 'segment_year', position: 1, type: 'year', value: '', width: 4 },
|
||||
{ id: 'segment_school', position: 2, type: 'school_code', value: '', width: 0 },
|
||||
{ id: 'segment_gender', position: 3, type: 'gender', value: '', width: 0 },
|
||||
{ id: 'segment_sequence', position: 4, type: 'sequence', value: '', width: 4 }
|
||||
] }
|
||||
],
|
||||
workflows: [
|
||||
{ id: 'workflow_profile', businessType: 'profile_change', name: '考生信息修改审批', active: true, updatedBy: adminId, updatedAt: nowIso(), steps: [
|
||||
{ id: 'workflow_profile_step_1', position: 1, name: '学校学籍复核', adminLevel: 'school' },
|
||||
{ id: 'workflow_profile_step_2', position: 2, name: '考试中心终审', adminLevel: 'super' }
|
||||
] },
|
||||
{ id: 'workflow_registration', businessType: 'registration_review', name: '考试报名审核', active: true, updatedBy: adminId, updatedAt: nowIso(), steps: [
|
||||
{ id: 'workflow_registration_step_1', position: 1, name: '学校报名初审', adminLevel: 'school' },
|
||||
{ id: 'workflow_registration_step_2', position: 2, name: '考试中心终审', adminLevel: 'super' }
|
||||
] },
|
||||
{ id: 'workflow_center', businessType: 'center_change', name: '考点考场变更审批', active: true, updatedBy: adminId, updatedAt: nowIso(), steps: [
|
||||
{ id: 'workflow_center_step_1', position: 1, name: '考试中心考务终审', adminLevel: 'super' }
|
||||
] }
|
||||
],
|
||||
workflowInstances: [],
|
||||
workflowActions: [],
|
||||
auditLogs: [
|
||||
{ id: 'log_1', actorId: adminId, action: '发布通知', detail: '发布《市第三中学考点交通提示》', createdAt: '2026-07-18T06:00:00.000Z' }
|
||||
]
|
||||
@@ -152,11 +203,20 @@ async function currentUser(request) {
|
||||
return null;
|
||||
}
|
||||
const db = await readDb();
|
||||
return db.users.find(user => user.id === session.userId) || null;
|
||||
const user = db.users.find(item => item.id === session.userId) || null;
|
||||
return user?.active === false ? null : user;
|
||||
}
|
||||
|
||||
function safeUser(user) {
|
||||
return { id: user.id, username: user.username, role: user.role, displayName: user.displayName };
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
adminLevel: user.adminLevel || null,
|
||||
schoolId: user.schoolId || null,
|
||||
classId: user.classId || null,
|
||||
displayName: user.displayName
|
||||
};
|
||||
}
|
||||
|
||||
async function requireUser(request, response, role) {
|
||||
@@ -172,10 +232,193 @@ async function requireUser(request, response, role) {
|
||||
return user;
|
||||
}
|
||||
|
||||
const adminLevelNames = { super: '超级管理员', school: '校级管理员', class: '班级管理员' };
|
||||
|
||||
const permissionsByLevel = {
|
||||
super: ['*'],
|
||||
school: ['dashboard.read', 'candidates.read', 'candidates.review', 'registrations.read', 'registrations.review', 'results.read', 'centers.read', 'centers.write', 'workflows.inbox'],
|
||||
class: ['dashboard.read', 'candidates.read', 'registrations.read', 'results.read']
|
||||
};
|
||||
|
||||
function hasPermission(user, permission) {
|
||||
if (user?.role !== 'admin') return false;
|
||||
const permissions = permissionsByLevel[user.adminLevel || 'super'] || [];
|
||||
return permissions.includes('*') || permissions.includes(permission);
|
||||
}
|
||||
|
||||
function requirePermission(user, response, permission) {
|
||||
if (hasPermission(user, permission)) return true;
|
||||
sendError(response, 403, '当前管理员层级无权执行此操作');
|
||||
return false;
|
||||
}
|
||||
|
||||
function profileInScope(user, profile) {
|
||||
if (user.adminLevel === 'super') return true;
|
||||
if (user.adminLevel === 'school') return Boolean(user.schoolId && profile.schoolId === user.schoolId);
|
||||
return Boolean(user.classId && profile.classId === user.classId);
|
||||
}
|
||||
|
||||
function registrationInScope(db, user, registration) {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
return Boolean(profile && profileInScope(user, profile));
|
||||
}
|
||||
|
||||
function adminScopeLabel(db, user) {
|
||||
if (user.adminLevel === 'super') return '全部学校与班级';
|
||||
const school = db.schools.find(item => item.id === user.schoolId)?.name || '未绑定学校';
|
||||
if (user.adminLevel === 'school') return school;
|
||||
const schoolClass = db.classes.find(item => item.id === user.classId)?.name || '未绑定班级';
|
||||
return `${school} · ${schoolClass}`;
|
||||
}
|
||||
|
||||
function adminsForStep(db, adminLevel, profile) {
|
||||
return db.users.filter(item => {
|
||||
if (item.role !== 'admin' || !item.active || item.adminLevel !== adminLevel) return false;
|
||||
if (adminLevel === 'super') return true;
|
||||
if (adminLevel === 'school') return Boolean(profile?.schoolId && item.schoolId === profile.schoolId);
|
||||
return Boolean(profile?.classId && item.classId === profile.classId);
|
||||
});
|
||||
}
|
||||
|
||||
function activeWorkflow(db, businessType) {
|
||||
return db.workflows.find(item => item.businessType === businessType && item.active);
|
||||
}
|
||||
|
||||
function createWorkflowSubmission(db, businessType, businessId, profile, actorId = null) {
|
||||
const workflow = activeWorkflow(db, businessType);
|
||||
if (!workflow?.steps.length) throw Object.assign(new Error('该业务尚未配置审批流程'), { status: 409 });
|
||||
const firstStep = workflow.steps[0];
|
||||
const assignee = adminsForStep(db, firstStep.adminLevel, profile)[0];
|
||||
if (!assignee) throw Object.assign(new Error(`没有可承接“${firstStep.name}”的${adminLevelNames[firstStep.adminLevel]}`), { status: 409 });
|
||||
const instance = {
|
||||
id: uid('flow'), workflowId: workflow.id, businessType, businessId, status: 'pending', currentStep: 1,
|
||||
assigneeId: assignee.id, createdAt: nowIso(), completedAt: null
|
||||
};
|
||||
const action = {
|
||||
id: uid('flow_action'), instanceId: instance.id, actorId, action: 'submit', note: '提交审批',
|
||||
fromAssigneeId: null, toAssigneeId: assignee.id, createdAt: nowIso()
|
||||
};
|
||||
return { workflow, instance, action };
|
||||
}
|
||||
|
||||
function workflowView(db, instance) {
|
||||
if (!instance) return null;
|
||||
const workflow = db.workflows.find(item => item.id === instance.workflowId);
|
||||
const assignee = db.users.find(item => item.id === instance.assigneeId);
|
||||
const actions = db.workflowActions.filter(item => item.instanceId === instance.id).map(item => ({
|
||||
...item,
|
||||
actorName: db.users.find(user => user.id === item.actorId)?.displayName || '系统',
|
||||
fromAssigneeName: db.users.find(user => user.id === item.fromAssigneeId)?.displayName || '',
|
||||
toAssigneeName: db.users.find(user => user.id === item.toAssigneeId)?.displayName || ''
|
||||
}));
|
||||
return {
|
||||
...instance,
|
||||
workflowName: workflow?.name || '未命名流程',
|
||||
steps: workflow?.steps || [],
|
||||
currentStepDetail: workflow?.steps.find(step => step.position === instance.currentStep) || null,
|
||||
assignee: assignee ? safeUser(assignee) : null,
|
||||
actions
|
||||
};
|
||||
}
|
||||
|
||||
function pendingWorkflow(db, businessType, businessId) {
|
||||
return db.workflowInstances.find(item => item.businessType === businessType && item.businessId === businessId && item.status === 'pending');
|
||||
}
|
||||
|
||||
function registrationSequence(db, rule, schoolId, year) {
|
||||
const prefixParts = rule.segments.filter(item => item.type !== 'sequence').map(segment => segment.type === 'year' ? year : segment.type === 'school_code' ? db.schools.find(school => school.id === schoolId)?.code || '' : '').filter(Boolean);
|
||||
const prefix = prefixParts.join(rule.separator);
|
||||
return db.registrations.filter(item => item.registrationNumber && (!prefix || item.registrationNumber.startsWith(prefix))).length + 1;
|
||||
}
|
||||
|
||||
function generateRegistrationNumber(db, registration, profile) {
|
||||
const rule = db.numberRules.find(item => item.active);
|
||||
if (!rule?.segments.length) throw Object.assign(new Error('尚未配置可用的报名号生成规则'), { status: 409 });
|
||||
const school = db.schools.find(item => item.id === profile.schoolId);
|
||||
const exam = db.exams.find(item => item.id === registration.examId);
|
||||
const year = String(new Date(exam?.examStart || Date.now()).getFullYear());
|
||||
const sequence = registrationSequence(db, rule, profile.schoolId, year);
|
||||
const parts = rule.segments.map(segment => {
|
||||
if (segment.type === 'year') return year.slice(-Math.max(2, segment.width || 4));
|
||||
if (segment.type === 'school_code') return school?.code || 'NOSCHOOL';
|
||||
if (segment.type === 'gender') return profile.gender === '男' ? 'M' : profile.gender === '女' ? 'F' : 'X';
|
||||
if (segment.type === 'sequence') return String(sequence).padStart(Math.max(1, segment.width || 4), '0');
|
||||
return cleanText(segment.value, 20).toUpperCase();
|
||||
});
|
||||
return { number: parts.join(rule.separator), ruleId: rule.id };
|
||||
}
|
||||
|
||||
function cleanText(value, max = 200) {
|
||||
return String(value ?? '').trim().slice(0, max);
|
||||
}
|
||||
|
||||
function centerScopeProfile(db, schoolId) {
|
||||
const school = db.schools.find(item => item.id === schoolId);
|
||||
return { schoolId, classId: null, school: school?.name || '', grade: '' };
|
||||
}
|
||||
|
||||
function workflowScopeProfile(db, instance) {
|
||||
if (instance.businessType === 'profile_change') return db.candidateProfiles.find(item => item.id === instance.businessId) || null;
|
||||
if (instance.businessType === 'registration_review') {
|
||||
const registration = db.registrations.find(item => item.id === instance.businessId);
|
||||
return db.candidateProfiles.find(item => item.userId === registration?.userId) || null;
|
||||
}
|
||||
const change = db.centerChangeRequests.find(item => item.id === instance.businessId);
|
||||
return change ? centerScopeProfile(db, change.schoolId) : null;
|
||||
}
|
||||
|
||||
function centerChangeView(db, change) {
|
||||
const instance = db.workflowInstances.find(item => item.businessType === 'center_change' && item.businessId === change.id);
|
||||
return {
|
||||
...change,
|
||||
schoolName: db.schools.find(item => item.id === change.schoolId)?.name || '',
|
||||
rooms: db.centerChangeRooms.filter(item => item.requestId === change.id),
|
||||
workflow: workflowView(db, instance)
|
||||
};
|
||||
}
|
||||
|
||||
function parseCenterChange(db, body, schoolId, center = null) {
|
||||
const code = cleanText(body.code, 30).toUpperCase();
|
||||
const name = cleanText(body.name, 100);
|
||||
const address = cleanText(body.address, 200);
|
||||
const rooms = Array.isArray(body.rooms) ? body.rooms : [];
|
||||
if (!code || !name || !address) throw Object.assign(new Error('请填写考点代码、名称和详细地址'), { status: 400 });
|
||||
if (!rooms.length) throw Object.assign(new Error('请至少配置一个结构化考场'), { status: 400 });
|
||||
const duplicateCenter = db.testCenters.some(item => item.code.toUpperCase() === code && item.id !== center?.id)
|
||||
|| db.centerChangeRequests.some(item => item.status === 'pending' && item.code.toUpperCase() === code && item.centerId !== center?.id);
|
||||
if (duplicateCenter) throw Object.assign(new Error('考点代码已被正式档案或待审批申请占用'), { status: 409 });
|
||||
const roomCodes = new Set();
|
||||
const normalizedRooms = rooms.map((room, index) => {
|
||||
const roomCode = cleanText(room.code, 30).toUpperCase();
|
||||
const roomName = cleanText(room.name, 80);
|
||||
const building = cleanText(room.building, 80);
|
||||
const capacity = Number(room.capacity);
|
||||
const seatStart = Number(room.seatStart);
|
||||
const seatEnd = Number(room.seatEnd);
|
||||
if (!roomCode || !roomName || !building || !Number.isInteger(capacity) || capacity < 1 || !Number.isInteger(seatStart) || !Number.isInteger(seatEnd) || seatStart < 1 || seatEnd < seatStart) {
|
||||
throw Object.assign(new Error(`第 ${index + 1} 个考场的代码、名称、楼栋、容量或座位号范围无效`), { status: 400 });
|
||||
}
|
||||
if (seatEnd - seatStart + 1 > capacity) throw Object.assign(new Error(`第 ${index + 1} 个考场的座位号数量不能超过考场容量`), { status: 400 });
|
||||
if (roomCodes.has(roomCode)) throw Object.assign(new Error(`考场代码 ${roomCode} 重复`), { status: 400 });
|
||||
roomCodes.add(roomCode);
|
||||
return {
|
||||
id: uid('change_room'), roomId: cleanText(room.id, 64) || null, code: roomCode, name: roomName,
|
||||
building, floor: cleanText(room.floor, 30), capacity, seatStart, seatEnd,
|
||||
roomType: ['standard', 'computer', 'accessible', 'spare'].includes(room.roomType) ? room.roomType : 'standard',
|
||||
status: room.status === 'inactive' ? 'inactive' : 'active', notes: cleanText(room.notes, 300)
|
||||
};
|
||||
});
|
||||
return {
|
||||
center: {
|
||||
schoolId, code, name, address, contact: cleanText(body.contact, 80), managerName: cleanText(body.managerName, 50),
|
||||
managerPhone: cleanText(body.managerPhone, 30), emergencyPhone: cleanText(body.emergencyPhone, 30),
|
||||
gateOpenTime: cleanText(body.gateOpenTime, 20), transport: cleanText(body.transport, 500),
|
||||
centerStatus: body.status === 'inactive' ? 'inactive' : 'active', notes: cleanText(body.notes, 1000)
|
||||
},
|
||||
rooms: normalizedRooms
|
||||
};
|
||||
}
|
||||
|
||||
function maskId(value) {
|
||||
const text = String(value || '');
|
||||
return text.length > 8 ? `${text.slice(0, 4)}********${text.slice(-4)}` : text;
|
||||
@@ -194,7 +437,9 @@ function publicExam(exam) {
|
||||
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));
|
||||
return { ...registration, exam, subjects };
|
||||
const instance = db.workflowInstances.find(item => item.businessType === 'registration_review' && item.businessId === registration.id && item.status === 'pending')
|
||||
|| db.workflowInstances.filter(item => item.businessType === 'registration_review' && item.businessId === registration.id)[0];
|
||||
return { ...registration, exam, subjects, workflow: workflowView(db, instance) };
|
||||
}
|
||||
|
||||
function logAction(db, user, action, detail) {
|
||||
@@ -220,7 +465,7 @@ async function handlePublic(pathname, response) {
|
||||
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, notices: publishedNotices, exams, stats: { candidates: db.candidateProfiles.length, exams: db.exams.filter(item => item.status === 'published').length, registrations: db.registrations.length } });
|
||||
return sendJson(response, 200, { ok: true, organization: db.organization, schools: db.schools.filter(item => item.active), classes: db.classes.filter(item => item.active), notices: publishedNotices, exams, stats: { candidates: db.candidateProfiles.length, exams: db.exams.filter(item => item.status === 'published').length, registrations: db.registrations.length } });
|
||||
}
|
||||
const noticeMatch = pathname.match(/^\/api\/public\/notices\/([^/]+)$/);
|
||||
if (noticeMatch) {
|
||||
@@ -236,7 +481,7 @@ async function handleAuth(request, response, pathname) {
|
||||
if (!user) return sendJson(response, 200, { ok: true, user: null });
|
||||
const db = await readDb();
|
||||
const profile = user.role === 'candidate' ? db.candidateProfiles.find(item => item.userId === user.id) : null;
|
||||
return sendJson(response, 200, { ok: true, user: safeUser(user), profile });
|
||||
return sendJson(response, 200, { ok: true, user: safeUser(user), profile, ...(user.role === 'admin' ? { permissions: permissionsByLevel[user.adminLevel || 'super'], scopeLabel: adminScopeLabel(db, user) } : {}) });
|
||||
}
|
||||
if (request.method === 'POST' && pathname === '/api/auth/register') {
|
||||
const body = await readJson(request);
|
||||
@@ -248,18 +493,24 @@ async function handleAuth(request, response, pathname) {
|
||||
if (!username || !name || !idNumber || !phone) return sendError(response, 400, '请完整填写账号和身份信息');
|
||||
if (password.length < 8) return sendError(response, 400, '密码至少需要 8 位');
|
||||
const db = await readDb();
|
||||
const schoolId = cleanText(body.schoolId, 64);
|
||||
const classId = cleanText(body.classId, 64);
|
||||
const school = db.schools.find(item => item.id === schoolId && item.active);
|
||||
const schoolClass = db.classes.find(item => item.id === classId && item.schoolId === schoolId && item.active);
|
||||
if (!school || !schoolClass) return sendError(response, 400, '请选择有效的学校和班级');
|
||||
if (db.users.some(user => user.username.toLowerCase() === username.toLowerCase())) return sendError(response, 409, '该账号已注册');
|
||||
if (db.candidateProfiles.some(profile => profile.idNumber === idNumber)) return sendError(response, 409, '该证件号码已注册');
|
||||
const user = { id: uid('usr'), username, passwordHash: hashPassword(password), role: 'candidate', displayName: name, createdAt: nowIso() };
|
||||
const profile = { id: uid('profile'), userId: user.id, name, idNumber, phone, gender: cleanText(body.gender, 10), email: cleanText(body.email, 80), school: cleanText(body.school, 80), grade: cleanText(body.grade, 50), address: '', emergencyContact: '', emergencyPhone: '', status: 'pending', reviewNote: '', updatedAt: nowIso() };
|
||||
await database.createCandidate(user, profile);
|
||||
const profile = { id: uid('profile'), userId: user.id, name, idNumber, phone, gender: cleanText(body.gender, 10), email: cleanText(body.email, 80), school: school.name, grade: schoolClass.name, schoolId, classId, address: '', emergencyContact: '', emergencyPhone: '', status: 'pending', reviewNote: '', updatedAt: nowIso() };
|
||||
const { instance, action } = createWorkflowSubmission(db, 'profile_change', profile.id, profile, user.id);
|
||||
await database.createCandidate(user, profile, instance, action);
|
||||
return sendJson(response, 201, { ok: true, message: '注册成功,请等待管理员审核资料' });
|
||||
}
|
||||
if (request.method === 'POST' && pathname === '/api/auth/login') {
|
||||
const body = await readJson(request);
|
||||
const db = await readDb();
|
||||
const user = db.users.find(item => item.username.toLowerCase() === cleanText(body.username, 50).toLowerCase());
|
||||
if (!user || !verifyPassword(String(body.password || ''), user.passwordHash)) return sendError(response, 401, '账号或密码不正确');
|
||||
if (!user || user.active === false || !verifyPassword(String(body.password || ''), user.passwordHash)) return sendError(response, 401, '账号或密码不正确');
|
||||
const token = randomBytes(32).toString('hex');
|
||||
sessions.set(token, { userId: user.id, expiresAt: Date.now() + 8 * 60 * 60 * 1000 });
|
||||
return sendJson(response, 200, { ok: true, user: safeUser(user) }, { 'Set-Cookie': `hz_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=28800` });
|
||||
@@ -283,19 +534,34 @@ async function handleCandidate(request, response, pathname) {
|
||||
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);
|
||||
return sendJson(response, 200, { ok: true, profile, registrations, results, notices });
|
||||
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 === 'GET' && pathname === '/api/candidate/profile') return sendJson(response, 200, { ok: true, profile });
|
||||
if (request.method === 'PUT' && pathname === '/api/candidate/profile') {
|
||||
const body = await readJson(request);
|
||||
const fields = ['name', 'gender', 'idNumber', 'phone', 'email', 'school', 'grade', 'address', 'emergencyContact', 'emergencyPhone'];
|
||||
const fields = ['name', 'gender', 'idNumber', 'phone', 'email', 'address', 'emergencyContact', 'emergencyPhone'];
|
||||
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 || !profile.idNumber || !profile.phone || !profile.school) 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.reviewNote = '';
|
||||
profile.updatedAt = nowIso();
|
||||
await database.updateCandidateProfile(profile, profile.name);
|
||||
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') {
|
||||
@@ -316,8 +582,9 @@ async function handleCandidate(request, response, pathname) {
|
||||
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(), admitCard: null };
|
||||
await database.createRegistration(registration);
|
||||
const registration = { id: uid('reg'), userId: user.id, examId: exam.id, subjectIds, status: 'pending', paymentStatus: 'unpaid', createdAt: nowIso(), registrationNumber: '', numberRuleId: 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') {
|
||||
@@ -353,31 +620,344 @@ async function handleAdmin(request, response, pathname) {
|
||||
if (!user) return true;
|
||||
const db = await readDb();
|
||||
|
||||
if (request.method === 'GET' && pathname === '/api/admin/context') {
|
||||
return sendJson(response, 200, {
|
||||
ok: true,
|
||||
admin: safeUser(user),
|
||||
adminLevelName: adminLevelNames[user.adminLevel || 'super'],
|
||||
permissions: permissionsByLevel[user.adminLevel || 'super'],
|
||||
scopeLabel: adminScopeLabel(db, user),
|
||||
schools: db.schools,
|
||||
classes: db.classes
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname === '/api/admin/admins' && request.method === 'GET') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const admins = db.users.filter(item => item.role === 'admin').map(item => ({
|
||||
...safeUser(item),
|
||||
active: item.active,
|
||||
levelName: adminLevelNames[item.adminLevel],
|
||||
schoolName: db.schools.find(school => school.id === item.schoolId)?.name || '',
|
||||
className: db.classes.find(schoolClass => schoolClass.id === item.classId)?.name || ''
|
||||
}));
|
||||
return sendJson(response, 200, { ok: true, admins, schools: db.schools, classes: db.classes });
|
||||
}
|
||||
if (pathname === '/api/admin/admins' && request.method === 'POST') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const username = cleanText(body.username, 50);
|
||||
const password = String(body.password || '');
|
||||
const displayName = cleanText(body.displayName, 50);
|
||||
const adminLevel = cleanText(body.adminLevel, 20);
|
||||
if (!username || !displayName || password.length < 8 || !['super', 'school', 'class'].includes(adminLevel)) return sendError(response, 400, '请完整填写管理员账号、姓名、层级和至少 8 位密码');
|
||||
if (db.users.some(item => item.username.toLowerCase() === username.toLowerCase())) return sendError(response, 409, '该登录账号已存在');
|
||||
const schoolId = adminLevel === 'super' ? null : cleanText(body.schoolId, 64);
|
||||
const classId = adminLevel === 'class' ? cleanText(body.classId, 64) : null;
|
||||
if (adminLevel !== 'super' && !db.schools.some(item => item.id === schoolId)) return sendError(response, 400, '校级和班级管理员必须绑定学校');
|
||||
if (adminLevel === 'class' && !db.classes.some(item => item.id === classId && item.schoolId === schoolId)) return sendError(response, 400, '请选择该学校下的有效班级');
|
||||
const created = { id: uid('usr'), username, passwordHash: hashPassword(password), role: 'admin', adminLevel, schoolId, classId, displayName, active: true, createdAt: nowIso() };
|
||||
const log = logAction(db, user, '创建管理员', `${displayName} · ${adminLevelNames[adminLevel]}`);
|
||||
await database.createAdmin(created, log);
|
||||
return sendJson(response, 201, { ok: true, admin: safeUser(created) });
|
||||
}
|
||||
|
||||
if (pathname === '/api/admin/centers' && request.method === 'GET') {
|
||||
if (!requirePermission(user, response, 'centers.read')) return true;
|
||||
const centers = db.testCenters.filter(item => user.adminLevel === 'super' || item.schoolId === user.schoolId).map(item => ({
|
||||
...item,
|
||||
schoolName: db.schools.find(school => school.id === item.schoolId)?.name || '',
|
||||
rooms: db.testRooms.filter(room => room.centerId === item.id),
|
||||
totalCapacity: db.testRooms.filter(room => room.centerId === item.id && room.status === 'active').reduce((sum, room) => sum + Number(room.capacity || 0), 0),
|
||||
pendingChange: db.centerChangeRequests.some(change => change.centerId === item.id && change.status === 'pending')
|
||||
}));
|
||||
const changeRequests = db.centerChangeRequests
|
||||
.filter(item => user.adminLevel === 'super' || item.schoolId === user.schoolId)
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
.map(item => centerChangeView(db, item));
|
||||
return sendJson(response, 200, { ok: true, centers, changeRequests, schools: user.adminLevel === 'super' ? db.schools : db.schools.filter(item => item.id === user.schoolId) });
|
||||
}
|
||||
if (pathname === '/api/admin/centers' && request.method === 'POST') {
|
||||
if (!requirePermission(user, response, 'centers.write')) return true;
|
||||
const body = await readJson(request);
|
||||
const schoolId = user.adminLevel === 'super' ? cleanText(body.schoolId, 64) : user.schoolId;
|
||||
if (!db.schools.some(item => item.id === schoolId)) return sendError(response, 400, '考点必须归属有效学校');
|
||||
const parsed = parseCenterChange(db, body, schoolId);
|
||||
const change = { id: uid('center_change'), centerId: null, schoolId, requestType: 'create', ...parsed.center, status: 'pending', reviewNote: '', requestedBy: user.id, createdAt: nowIso(), reviewedAt: null };
|
||||
const { instance, action } = createWorkflowSubmission(db, 'center_change', change.id, centerScopeProfile(db, schoolId), user.id);
|
||||
const log = logAction(db, user, '提交新增考点审批', `${change.name} · ${parsed.rooms.length} 个考场`);
|
||||
await database.createCenterChangeRequest(change, parsed.rooms, instance, action, log);
|
||||
return sendJson(response, 202, { ok: true, changeRequest: { ...change, rooms: parsed.rooms, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) } });
|
||||
}
|
||||
const centerMatch = pathname.match(/^\/api\/admin\/centers\/([^/]+)$/);
|
||||
if (centerMatch && request.method === 'PATCH') {
|
||||
if (!requirePermission(user, response, 'centers.write')) return true;
|
||||
const body = await readJson(request);
|
||||
const center = db.testCenters.find(item => item.id === centerMatch[1]);
|
||||
if (!center) return sendError(response, 404, '考点不存在');
|
||||
if (user.adminLevel !== 'super' && center.schoolId !== user.schoolId) return sendError(response, 403, '只能维护本校考点');
|
||||
if (db.centerChangeRequests.some(item => item.centerId === center.id && item.status === 'pending')) return sendError(response, 409, '该考点已有待审批变更,请处理完成后再提交');
|
||||
const parsed = parseCenterChange(db, body, center.schoolId, center);
|
||||
const change = { id: uid('center_change'), centerId: center.id, schoolId: center.schoolId, requestType: 'update', ...parsed.center, status: 'pending', reviewNote: '', requestedBy: user.id, createdAt: nowIso(), reviewedAt: null };
|
||||
const { instance, action } = createWorkflowSubmission(db, 'center_change', change.id, centerScopeProfile(db, center.schoolId), user.id);
|
||||
const log = logAction(db, user, '提交考点变更审批', `${change.name} · ${parsed.rooms.length} 个考场`);
|
||||
await database.createCenterChangeRequest(change, parsed.rooms, instance, action, log);
|
||||
return sendJson(response, 202, { ok: true, changeRequest: { ...change, rooms: parsed.rooms, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) } });
|
||||
}
|
||||
const centerChangeMatch = pathname.match(/^\/api\/admin\/center-change-requests\/([^/]+)$/);
|
||||
if (centerChangeMatch && request.method === 'PATCH') {
|
||||
if (!requirePermission(user, response, 'centers.write')) return true;
|
||||
const body = await readJson(request);
|
||||
if (!['approved', 'rejected'].includes(body.status)) return sendError(response, 400, '审批状态无效');
|
||||
const change = db.centerChangeRequests.find(item => item.id === centerChangeMatch[1] && item.status === 'pending');
|
||||
if (!change) return sendError(response, 404, '待审批的考点变更不存在');
|
||||
if (user.adminLevel !== 'super' && change.schoolId !== user.schoolId) return sendError(response, 403, '该变更不在你的学校范围内');
|
||||
const instance = pendingWorkflow(db, 'center_change', change.id);
|
||||
const workflow = instance && db.workflows.find(item => item.id === instance.workflowId);
|
||||
const step = workflow?.steps.find(item => item.position === instance.currentStep);
|
||||
if (!instance || !workflow || !step) return sendError(response, 409, '考点变更审批流程状态异常');
|
||||
if (user.adminLevel !== 'super' && (instance.assigneeId !== user.id || step.adminLevel !== user.adminLevel)) return sendError(response, 403, '该流程当前未分配给你,可由当前处理人转交');
|
||||
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 log = logAction(db, user, body.status === 'approved' ? '审批考点变更' : '退回考点变更', `${change.name} · ${note || '无备注'}`);
|
||||
if (body.status === 'rejected') {
|
||||
instance.status = 'rejected'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
change.status = 'rejected'; change.reviewNote = note; change.reviewedAt = nowIso();
|
||||
await database.applyCenterChange(change, instance, action, null, [], log);
|
||||
} else if (instance.currentStep < workflow.steps.length) {
|
||||
const nextStep = workflow.steps.find(item => item.position === instance.currentStep + 1);
|
||||
const nextAssignee = adminsForStep(db, nextStep.adminLevel, centerScopeProfile(db, change.schoolId))[0];
|
||||
if (!nextAssignee) return sendError(response, 409, `没有可承接“${nextStep.name}”的管理员`);
|
||||
instance.currentStep += 1; instance.assigneeId = nextAssignee.id; action.toAssigneeId = nextAssignee.id;
|
||||
change.reviewNote = note;
|
||||
await database.processWorkflow(instance, action, change, log);
|
||||
} else {
|
||||
instance.status = 'approved'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
change.status = 'approved'; change.reviewNote = note; change.reviewedAt = nowIso();
|
||||
const centerId = change.centerId || uid('center');
|
||||
const proposedRooms = db.centerChangeRooms.filter(item => item.requestId === change.id);
|
||||
const rooms = proposedRooms.map(room => ({ ...room, id: room.roomId || uid('room'), centerId }));
|
||||
const center = {
|
||||
id: centerId, schoolId: change.schoolId, code: change.code, name: change.name, address: change.address,
|
||||
contact: change.contact, managerName: change.managerName, managerPhone: change.managerPhone,
|
||||
emergencyPhone: change.emergencyPhone, gateOpenTime: change.gateOpenTime, transport: change.transport,
|
||||
status: change.centerStatus, notes: change.notes,
|
||||
rooms: rooms.map(room => `${room.building} ${room.name}`).join(';'), updatedAt: nowIso()
|
||||
};
|
||||
await database.applyCenterChange(change, instance, action, center, rooms, log);
|
||||
}
|
||||
return sendJson(response, 200, { ok: true, changeRequest: centerChangeView({ ...db, workflowActions: [...db.workflowActions, action] }, change) });
|
||||
}
|
||||
|
||||
if (pathname === '/api/admin/number-rules' && request.method === 'GET') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const rule = db.numberRules.find(item => item.active) || null;
|
||||
const previewProfile = db.candidateProfiles[0] || { gender: '女', schoolId: db.schools[0]?.id };
|
||||
let preview = '';
|
||||
if (rule) {
|
||||
const sampleRegistration = { examId: db.exams[0]?.id };
|
||||
preview = generateRegistrationNumber(db, sampleRegistration, previewProfile).number;
|
||||
}
|
||||
const batchCandidates = db.registrations.filter(item => item.status === 'approved' && !item.registrationNumber).map(registration => {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
return { id: registration.id, examId: registration.examId, examName: db.exams.find(item => item.id === registration.examId)?.name || '', schoolId: profile?.schoolId || '', schoolName: profile?.school || '', candidateName: profile?.name || '', createdAt: registration.createdAt };
|
||||
});
|
||||
return sendJson(response, 200, { ok: true, rules: db.numberRules, activeRule: rule, preview, batchCandidates, exams: db.exams, schools: db.schools });
|
||||
}
|
||||
if (pathname === '/api/admin/number-rules' && request.method === 'POST') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const allowedTypes = ['year', 'school_code', 'gender', 'sequence', 'literal'];
|
||||
const requested = Array.isArray(body.segments) ? body.segments : [];
|
||||
if (!requested.length || requested.some(item => !allowedTypes.includes(item.type)) || !requested.some(item => item.type === 'sequence')) return sendError(response, 400, '报名号规则至少包含一个流水号段');
|
||||
const existing = db.numberRules.find(item => item.id === body.id);
|
||||
const rule = {
|
||||
id: existing?.id || uid('rule'), name: cleanText(body.name, 80) || '自定义报名号规则', separator: cleanText(body.separator, 3),
|
||||
active: true, createdBy: user.id, updatedAt: nowIso(), segments: requested.map((item, index) => ({
|
||||
id: uid('segment'), position: index + 1, type: item.type, value: cleanText(item.value, 20), width: Math.min(12, Math.max(0, Number(item.width || 0)))
|
||||
}))
|
||||
};
|
||||
const log = logAction(db, user, '更新报名号规则', `${rule.name} · ${rule.segments.map(item => item.type).join(' + ')}`);
|
||||
await database.saveNumberRule(rule, !existing, log);
|
||||
return sendJson(response, 200, { ok: true, rule });
|
||||
}
|
||||
if (pathname === '/api/admin/registration-numbers/batch' && request.method === 'POST') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const examId = cleanText(body.examId, 64);
|
||||
const schoolId = cleanText(body.schoolId, 64);
|
||||
const selectedIds = Array.isArray(body.registrationIds) ? new Set(body.registrationIds.map(item => cleanText(item, 64))) : null;
|
||||
const eligible = db.registrations.filter(registration => {
|
||||
if (registration.status !== 'approved' || registration.registrationNumber) return false;
|
||||
if (examId && registration.examId !== examId) return false;
|
||||
if (selectedIds && !selectedIds.has(registration.id)) return false;
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
return Boolean(profile && (!schoolId || profile.schoolId === schoolId));
|
||||
}).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt) || a.id.localeCompare(b.id)).slice(0, 5000);
|
||||
if (!eligible.length) return sendError(response, 409, '当前筛选条件下没有审核通过且尚未生成报名号的记录');
|
||||
const generated = eligible.map(registration => {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
const result = generateRegistrationNumber(db, registration, profile);
|
||||
registration.registrationNumber = result.number;
|
||||
registration.numberRuleId = result.ruleId;
|
||||
return registration;
|
||||
});
|
||||
const log = logAction(db, user, '批量生成报名号', `${generated.length} 条 · ${examId || '全部考试'} · ${schoolId || '全部学校'}`);
|
||||
await database.assignRegistrationNumbers(generated, log);
|
||||
return sendJson(response, 200, { ok: true, count: generated.length, registrations: generated.map(item => ({ id: item.id, registrationNumber: item.registrationNumber })) });
|
||||
}
|
||||
|
||||
if (pathname === '/api/admin/workflows' && request.method === 'GET') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
return sendJson(response, 200, { ok: true, workflows: db.workflows });
|
||||
}
|
||||
const workflowDefinitionMatch = pathname.match(/^\/api\/admin\/workflows\/(profile_change|registration_review|center_change)$/);
|
||||
if (workflowDefinitionMatch && request.method === 'PUT') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const workflow = activeWorkflow(db, workflowDefinitionMatch[1]);
|
||||
if (!workflow) return sendError(response, 404, '审批流程不存在');
|
||||
const steps = Array.isArray(body.steps) ? body.steps : [];
|
||||
if (!steps.length || steps.some(item => !['school', 'super'].includes(item.adminLevel))) return sendError(response, 400, '流程至少需要一个校级或超级管理员审批步骤');
|
||||
workflow.name = cleanText(body.name, 80) || workflow.name;
|
||||
workflow.updatedBy = user.id;
|
||||
workflow.updatedAt = nowIso();
|
||||
workflow.steps = steps.map((item, index) => ({ id: uid('workflow_step'), position: index + 1, name: cleanText(item.name, 80) || `第 ${index + 1} 步`, adminLevel: item.adminLevel }));
|
||||
const log = logAction(db, user, '修改审批流程', `${workflow.name} · ${workflow.steps.length} 个步骤`);
|
||||
await database.saveWorkflow(workflow, log);
|
||||
return sendJson(response, 200, { ok: true, workflow });
|
||||
}
|
||||
|
||||
if (pathname === '/api/admin/workflow-instances' && request.method === 'GET') {
|
||||
if (user.adminLevel === 'class') return sendError(response, 403, '班级管理员只读查看考生、成绩和报名状态');
|
||||
const instances = db.workflowInstances.filter(instance => {
|
||||
if (user.adminLevel === 'super') return true;
|
||||
const profile = workflowScopeProfile(db, instance);
|
||||
return Boolean(profile && profileInScope(user, profile));
|
||||
}).map(instance => {
|
||||
const profile = workflowScopeProfile(db, instance);
|
||||
const registration = instance.businessType === 'registration_review' ? db.registrations.find(item => item.id === instance.businessId) : null;
|
||||
const centerChange = instance.businessType === 'center_change' ? db.centerChangeRequests.find(item => item.id === instance.businessId) : null;
|
||||
return {
|
||||
...workflowView(db, instance), candidateName: profile?.name || '', schoolName: profile?.school || '', className: profile?.grade || '',
|
||||
examName: registration ? db.exams.find(item => item.id === registration.examId)?.name || '' : '',
|
||||
centerName: centerChange?.name || '', requestType: centerChange?.requestType || '', centerChange: centerChange ? centerChangeView(db, centerChange) : null
|
||||
};
|
||||
});
|
||||
const availableAdmins = db.users.filter(item => item.role === 'admin' && item.active).map(safeUser);
|
||||
return sendJson(response, 200, { ok: true, instances, availableAdmins, canSupervise: user.adminLevel === 'super' });
|
||||
}
|
||||
const transferMatch = pathname.match(/^\/api\/admin\/workflow-instances\/([^/]+)\/transfer$/);
|
||||
if (transferMatch && request.method === 'PATCH') {
|
||||
const body = await readJson(request);
|
||||
const instance = db.workflowInstances.find(item => item.id === transferMatch[1] && item.status === 'pending');
|
||||
if (!instance) return sendError(response, 404, '待处理流程不存在');
|
||||
const workflow = db.workflows.find(item => item.id === instance.workflowId);
|
||||
const step = workflow?.steps.find(item => item.position === instance.currentStep);
|
||||
if (user.adminLevel !== 'super' && instance.assigneeId !== user.id) return sendError(response, 403, '只有当前处理人可以转交该流程');
|
||||
const target = db.users.find(item => item.id === body.assigneeId && item.role === 'admin' && item.active && item.adminLevel === step?.adminLevel);
|
||||
if (!target) return sendError(response, 400, '只能转交给当前步骤同级管理员');
|
||||
const profile = workflowScopeProfile(db, instance);
|
||||
if (step.adminLevel === 'school' && target.schoolId !== profile?.schoolId) return sendError(response, 400, '校级流程只能转交给本校同级管理员');
|
||||
const previous = instance.assigneeId;
|
||||
instance.assigneeId = target.id;
|
||||
const action = { id: uid('flow_action'), instanceId: instance.id, actorId: user.id, action: 'transfer', note: cleanText(body.note, 300), fromAssigneeId: previous, toAssigneeId: target.id, createdAt: nowIso() };
|
||||
const log = logAction(db, user, '转交审批流程', `${workflow.name} → ${target.displayName}`);
|
||||
await database.transferWorkflow(instance, action, log);
|
||||
return sendJson(response, 200, { ok: true, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) });
|
||||
}
|
||||
const superviseMatch = pathname.match(/^\/api\/admin\/workflow-instances\/([^/]+)\/supervise$/);
|
||||
if (superviseMatch && request.method === 'PATCH') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const instance = db.workflowInstances.find(item => item.id === superviseMatch[1]);
|
||||
if (!instance) return sendError(response, 404, '流程不存在');
|
||||
const workflow = db.workflows.find(item => item.id === instance.workflowId);
|
||||
const requestedStep = Math.min(workflow.steps.length, Math.max(1, Number(body.currentStep || instance.currentStep)));
|
||||
const step = workflow.steps.find(item => item.position === requestedStep);
|
||||
const profile = workflowScopeProfile(db, instance);
|
||||
const eligible = adminsForStep(db, step.adminLevel, profile);
|
||||
const assignee = eligible.find(item => item.id === body.assigneeId) || eligible[0];
|
||||
if (!assignee) return sendError(response, 409, '目标步骤没有可用管理员');
|
||||
const previous = instance.assigneeId;
|
||||
const previousStep = instance.currentStep;
|
||||
instance.status = 'pending'; instance.completedAt = null; instance.currentStep = requestedStep; instance.assigneeId = assignee.id;
|
||||
const note = cleanText(body.note, 300) || `超级管理员将流程调整到第 ${requestedStep} 步`;
|
||||
const action = { id: uid('flow_action'), instanceId: instance.id, actorId: user.id, action: requestedStep < previousStep ? 'return' : 'supervise', note, fromAssigneeId: previous, toAssigneeId: assignee.id, createdAt: nowIso() };
|
||||
const business = instance.businessType === 'profile_change'
|
||||
? profile
|
||||
: instance.businessType === 'registration_review'
|
||||
? db.registrations.find(item => item.id === instance.businessId)
|
||||
: db.centerChangeRequests.find(item => item.id === instance.businessId);
|
||||
business.status = 'pending'; business.reviewNote = note; business.reviewedAt = null; business.reviewerId = null;
|
||||
const log = logAction(db, user, '监督调整审批流程', `${workflow.name} · 第 ${requestedStep} 步 · ${assignee.displayName}`);
|
||||
await database.processWorkflow(instance, action, business, log);
|
||||
return sendJson(response, 200, { ok: true, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) });
|
||||
}
|
||||
|
||||
if (request.method === 'GET' && pathname === '/api/admin/dashboard') {
|
||||
const pendingCandidates = db.candidateProfiles.filter(item => item.status === 'pending').length;
|
||||
const pendingRegistrations = db.registrations.filter(item => item.status === 'pending').length;
|
||||
return sendJson(response, 200, { ok: true, metrics: { candidates: db.candidateProfiles.length, pendingCandidates, registrations: db.registrations.length, pendingRegistrations, publishedExams: db.exams.filter(item => item.status === 'published').length, notices: db.notices.filter(item => item.status === 'published').length }, logs: db.auditLogs.slice(0, 8) });
|
||||
const profiles = db.candidateProfiles.filter(item => profileInScope(user, item));
|
||||
const registrations = db.registrations.filter(item => registrationInScope(db, user, item));
|
||||
const visibleFlows = db.workflowInstances.filter(instance => {
|
||||
if (user.adminLevel === 'super') return true;
|
||||
if (user.adminLevel === 'class') return false;
|
||||
const business = workflowScopeProfile(db, instance);
|
||||
return business && profileInScope(user, business) && (instance.assigneeId === user.id || instance.status !== 'pending');
|
||||
});
|
||||
const pendingCandidates = profiles.filter(item => item.status === 'pending').length;
|
||||
const pendingRegistrations = registrations.filter(item => item.status === 'pending').length;
|
||||
return sendJson(response, 200, {
|
||||
ok: true,
|
||||
admin: safeUser(user),
|
||||
scopeLabel: adminScopeLabel(db, user),
|
||||
permissions: permissionsByLevel[user.adminLevel || 'super'],
|
||||
metrics: { candidates: profiles.length, pendingCandidates, registrations: registrations.length, pendingRegistrations, pendingFlows: visibleFlows.filter(item => item.status === 'pending').length, publishedExams: db.exams.filter(item => item.status === 'published').length, notices: db.notices.filter(item => item.status === 'published').length },
|
||||
logs: user.adminLevel === 'super' ? db.auditLogs.slice(0, 8) : db.auditLogs.filter(log => log.actorId === user.id).slice(0, 8)
|
||||
});
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/admin/candidates') {
|
||||
const candidates = db.candidateProfiles.map(profile => ({ ...profile, idNumberMasked: maskId(profile.idNumber), username: db.users.find(item => item.id === profile.userId)?.username }));
|
||||
if (!requirePermission(user, response, 'candidates.read')) return true;
|
||||
const candidates = db.candidateProfiles.filter(profile => profileInScope(user, profile)).map(profile => {
|
||||
const instance = pendingWorkflow(db, 'profile_change', profile.id) || db.workflowInstances.filter(item => item.businessType === 'profile_change' && item.businessId === profile.id)[0];
|
||||
return { ...profile, idNumberMasked: maskId(profile.idNumber), username: db.users.find(item => item.id === profile.userId)?.username, workflow: workflowView(db, instance) };
|
||||
});
|
||||
return sendJson(response, 200, { ok: true, candidates });
|
||||
}
|
||||
const candidateMatch = pathname.match(/^\/api\/admin\/candidates\/([^/]+)$/);
|
||||
if (request.method === 'PATCH' && candidateMatch) {
|
||||
if (!requirePermission(user, response, 'candidates.review')) return true;
|
||||
const body = await readJson(request);
|
||||
const profile = db.candidateProfiles.find(item => item.id === candidateMatch[1]);
|
||||
if (!profile) return sendError(response, 404, '考生资料不存在');
|
||||
if (!profileInScope(user, profile) && user.adminLevel !== 'super') return sendError(response, 403, '该考生不在你的数据范围内');
|
||||
if (!['approved', 'rejected'].includes(body.status)) return sendError(response, 400, '审核状态无效');
|
||||
profile.status = body.status;
|
||||
profile.reviewNote = cleanText(body.reviewNote, 300);
|
||||
profile.reviewedAt = nowIso();
|
||||
profile.reviewerId = user.id;
|
||||
const log = logAction(db, user, body.status === 'approved' ? '通过考生资料' : '退回考生资料', `${profile.name}:${profile.reviewNote || '无备注'}`);
|
||||
await database.reviewCandidate(profile, log);
|
||||
return sendJson(response, 200, { ok: true, profile });
|
||||
const instance = pendingWorkflow(db, 'profile_change', profile.id);
|
||||
if (!instance) return sendError(response, 409, '当前没有待处理的考生信息流程');
|
||||
const workflow = db.workflows.find(item => item.id === instance.workflowId);
|
||||
const step = workflow?.steps.find(item => item.position === instance.currentStep);
|
||||
if (user.adminLevel !== 'super' && (instance.assigneeId !== user.id || step?.adminLevel !== user.adminLevel)) return sendError(response, 403, '该流程当前未分配给你,可由当前处理人转交');
|
||||
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() };
|
||||
if (body.status === 'rejected') {
|
||||
instance.status = 'rejected'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
profile.status = 'rejected'; profile.reviewNote = note; profile.reviewedAt = nowIso(); profile.reviewerId = user.id;
|
||||
} else if (instance.currentStep < workflow.steps.length) {
|
||||
const nextStep = workflow.steps.find(item => item.position === instance.currentStep + 1);
|
||||
const nextAssignee = adminsForStep(db, nextStep.adminLevel, profile)[0];
|
||||
if (!nextAssignee) return sendError(response, 409, `没有可承接“${nextStep.name}”的管理员`);
|
||||
instance.currentStep += 1; instance.assigneeId = nextAssignee.id; action.toAssigneeId = nextAssignee.id;
|
||||
profile.status = 'pending'; profile.reviewNote = note;
|
||||
} else {
|
||||
instance.status = 'approved'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
profile.status = 'approved'; profile.reviewNote = note; profile.reviewedAt = nowIso(); profile.reviewerId = user.id;
|
||||
}
|
||||
const log = logAction(db, user, body.status === 'approved' ? '处理考生信息流程' : '退回考生信息', `${profile.name}:${note || '无备注'}`);
|
||||
await database.processWorkflow(instance, action, profile, log);
|
||||
return sendJson(response, 200, { ok: true, profile, workflow: workflowView({ ...db, workflowActions: [...db.workflowActions, action] }, instance) });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/admin/registrations') {
|
||||
const registrations = db.registrations.map(registration => {
|
||||
if (!requirePermission(user, response, 'registrations.read')) return true;
|
||||
const registrations = db.registrations.filter(registration => registrationInScope(db, user, registration)).map(registration => {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
return { ...examRegistrationView(db, registration), candidate: profile ? { ...profile, idNumber: maskId(profile.idNumber) } : null };
|
||||
});
|
||||
@@ -385,21 +965,57 @@ async function handleAdmin(request, response, pathname) {
|
||||
}
|
||||
const registrationMatch = pathname.match(/^\/api\/admin\/registrations\/([^/]+)$/);
|
||||
if (request.method === 'PATCH' && registrationMatch) {
|
||||
if (!requirePermission(user, response, 'registrations.review')) return true;
|
||||
const body = await readJson(request);
|
||||
const registration = db.registrations.find(item => item.id === registrationMatch[1]);
|
||||
if (!registration) return sendError(response, 404, '报名记录不存在');
|
||||
if (!registrationInScope(db, user, registration) && user.adminLevel !== 'super') return sendError(response, 403, '该报名不在你的数据范围内');
|
||||
if (!['approved', 'rejected'].includes(body.status)) return sendError(response, 400, '审核状态无效');
|
||||
registration.status = body.status;
|
||||
registration.reviewNote = cleanText(body.reviewNote, 300);
|
||||
registration.reviewedAt = nowIso();
|
||||
if (body.status === 'approved') registration.paymentStatus = 'paid';
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
const log = logAction(db, user, body.status === 'approved' ? '通过考试报名' : '退回考试报名', `${profile?.name || registration.userId} · ${db.exams.find(item => item.id === registration.examId)?.name}`);
|
||||
await database.reviewRegistration(registration, log);
|
||||
return sendJson(response, 200, { ok: true, registration });
|
||||
const instance = pendingWorkflow(db, 'registration_review', registration.id);
|
||||
if (!instance) return sendError(response, 409, '当前没有待处理的报名审核流程');
|
||||
const workflow = db.workflows.find(item => item.id === instance.workflowId);
|
||||
const step = workflow?.steps.find(item => item.position === instance.currentStep);
|
||||
if (user.adminLevel !== 'super' && (instance.assigneeId !== user.id || step?.adminLevel !== user.adminLevel)) return sendError(response, 403, '该流程当前未分配给你,可由当前处理人转交');
|
||||
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() };
|
||||
if (body.status === 'rejected') {
|
||||
instance.status = 'rejected'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
registration.status = 'rejected'; registration.reviewNote = note; registration.reviewedAt = nowIso();
|
||||
} else if (instance.currentStep < workflow.steps.length) {
|
||||
const nextStep = workflow.steps.find(item => item.position === instance.currentStep + 1);
|
||||
const nextAssignee = adminsForStep(db, nextStep.adminLevel, profile)[0];
|
||||
if (!nextAssignee) return sendError(response, 409, `没有可承接“${nextStep.name}”的管理员`);
|
||||
instance.currentStep += 1; instance.assigneeId = nextAssignee.id; action.toAssigneeId = nextAssignee.id;
|
||||
registration.status = 'pending'; registration.reviewNote = note;
|
||||
} else {
|
||||
const generated = registration.registrationNumber ? null : generateRegistrationNumber(db, registration, profile);
|
||||
instance.status = 'approved'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
registration.status = 'approved'; registration.paymentStatus = 'paid'; registration.reviewNote = note; registration.reviewedAt = nowIso();
|
||||
if (generated) { registration.registrationNumber = generated.number; registration.numberRuleId = generated.ruleId; }
|
||||
}
|
||||
const log = logAction(db, user, body.status === 'approved' ? '处理报名审核流程' : '退回考试报名', `${profile?.name || registration.userId} · ${db.exams.find(item => item.id === registration.examId)?.name}`);
|
||||
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 numberMatch = pathname.match(/^\/api\/admin\/registrations\/([^/]+)\/registration-number$/);
|
||||
if (request.method === 'POST' && numberMatch) {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const registration = db.registrations.find(item => item.id === numberMatch[1]);
|
||||
if (!registration) return sendError(response, 404, '报名记录不存在');
|
||||
if (!registration.registrationNumber) {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
const generated = generateRegistrationNumber(db, registration, profile);
|
||||
registration.registrationNumber = generated.number;
|
||||
registration.numberRuleId = generated.ruleId;
|
||||
const log = logAction(db, user, '生成报名号', `${profile?.name || registration.userId} · ${generated.number}`);
|
||||
await database.assignRegistrationNumber(registration, log);
|
||||
}
|
||||
return sendJson(response, 200, { ok: true, registrationNumber: registration.registrationNumber });
|
||||
}
|
||||
if (request.method === 'POST' && admitMatch) {
|
||||
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, '报名审核通过后才能生成准考证');
|
||||
@@ -419,8 +1035,12 @@ async function handleAdmin(request, response, pathname) {
|
||||
}
|
||||
return sendJson(response, 200, { ok: true, admitCard: registration.admitCard });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/admin/exams') return sendJson(response, 200, { ok: true, exams: db.exams.map(exam => ({ ...publicExam(exam), registrationCount: db.registrations.filter(reg => reg.examId === exam.id).length })) });
|
||||
if (request.method === 'GET' && pathname === '/api/admin/exams') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
return sendJson(response, 200, { ok: true, exams: db.exams.map(exam => ({ ...publicExam(exam), registrationCount: db.registrations.filter(reg => reg.examId === exam.id).length })) });
|
||||
}
|
||||
if (request.method === 'POST' && pathname === '/api/admin/exams') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
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, '请完整填写考试名称和关键日期');
|
||||
@@ -434,6 +1054,7 @@ async function handleAdmin(request, response, pathname) {
|
||||
}
|
||||
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, '考试不存在');
|
||||
@@ -458,8 +1079,12 @@ async function handleAdmin(request, response, pathname) {
|
||||
await database.updateExam(exam, log, replaceSubjects);
|
||||
return sendJson(response, 200, { ok: true, exam });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/admin/notices') return sendJson(response, 200, { ok: true, notices: db.notices.sort((a, b) => new Date(b.publishAt || b.createdAt) - new Date(a.publishAt || a.createdAt)) });
|
||||
if (request.method === 'GET' && pathname === '/api/admin/notices') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
return sendJson(response, 200, { ok: true, notices: db.notices.sort((a, b) => new Date(b.publishAt || b.createdAt) - new Date(a.publishAt || a.createdAt)) });
|
||||
}
|
||||
if (request.method === 'POST' && pathname === '/api/admin/notices') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const title = cleanText(body.title, 120);
|
||||
const content = cleanText(body.content, 5000);
|
||||
@@ -471,6 +1096,7 @@ async function handleAdmin(request, response, pathname) {
|
||||
}
|
||||
const noticeMatch = pathname.match(/^\/api\/admin\/notices\/([^/]+)$/);
|
||||
if (request.method === 'PATCH' && noticeMatch) {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const notice = db.notices.find(item => item.id === noticeMatch[1]);
|
||||
if (!notice) return sendError(response, 404, '通知不存在');
|
||||
@@ -485,16 +1111,19 @@ async function handleAdmin(request, response, pathname) {
|
||||
return sendJson(response, 200, { ok: true, notice });
|
||||
}
|
||||
if (request.method === 'GET' && pathname === '/api/admin/results') {
|
||||
const results = db.results.map(result => {
|
||||
if (!requirePermission(user, response, 'results.read')) return true;
|
||||
const scopedRegistrations = db.registrations.filter(item => registrationInScope(db, user, item));
|
||||
const results = db.results.filter(result => scopedRegistrations.some(item => item.id === result.registrationId)).map(result => {
|
||||
const registration = db.registrations.find(item => item.id === result.registrationId);
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration?.userId);
|
||||
const exam = db.exams.find(item => item.id === registration?.examId);
|
||||
const subject = exam?.subjects.find(item => item.id === result.subjectId);
|
||||
return { ...result, candidateName: profile?.name, examName: exam?.name, subjectName: subject?.name };
|
||||
});
|
||||
return sendJson(response, 200, { ok: true, results, registrations: db.registrations.filter(item => item.status === 'approved').map(item => examRegistrationView(db, item)) });
|
||||
return sendJson(response, 200, { ok: true, results, registrations: user.adminLevel === 'super' ? scopedRegistrations.filter(item => item.status === 'approved').map(item => examRegistrationView(db, item)) : [] });
|
||||
}
|
||||
if (request.method === 'POST' && pathname === '/api/admin/results') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
const registration = db.registrations.find(item => item.id === body.registrationId && item.status === 'approved');
|
||||
if (!registration) return sendError(response, 404, '已通过的报名记录不存在');
|
||||
|
||||
Reference in New Issue
Block a user