This commit is contained in:
2026-07-20 18:04:10 +08:00 Unverified
parent a30477bb58
commit 67863e7ebf
16 changed files with 211 additions and 57 deletions
+51 -4
View File
@@ -127,13 +127,14 @@ export function createAdminRoutes(context) {
});
}
const excelMatch = pathname.match(/^\/api\/admin\/excel\/(classes|class_admins|account_quotas|account_results|candidates|centers|results)$/);
const excelMatch = pathname.match(/^\/api\/admin\/excel\/(classes|class_admins|account_quotas|account_results|candidates|payments|centers|results)$/);
if (excelMatch && request.method === 'GET') {
const resource = excelMatch[1];
if (!hasExcelResource(resource)) return sendError(response, 404, 'Excel 数据类型不存在');
if (['classes', 'class_admins', 'account_quotas', 'account_results'].includes(resource) && !['school', 'super'].includes(user.adminLevel)) return sendError(response, 403, '当前账号不能导出该数据');
if (resource === 'centers' && !hasPermission(user, 'centers.read')) return sendError(response, 403, '当前账号不能导出考点考场');
if (resource === 'candidates' && !hasPermission(user, 'candidates.read')) return sendError(response, 403, '当前账号不能导出考生资料');
if (resource === 'payments' && !hasPermission(user, 'payments.read')) return sendError(response, 403, '当前账号不能导出缴费名单');
if (resource === 'results' && !hasPermission(user, 'results.read')) return sendError(response, 403, '当前账号不能导出成绩');
const requestUrl = new URL(request.url, `http://${request.headers.host || '127.0.0.1'}`);
const template = requestUrl.searchParams.get('template') === '1';
@@ -144,7 +145,7 @@ export function createAdminRoutes(context) {
}
if (excelMatch && request.method === 'POST') {
const resource = excelMatch[1];
if (resource === 'account_results') return sendError(response, 400, '账号结果清单只支持导出');
if (['account_results', 'payments'].includes(resource)) return sendError(response, 400, '清单只支持导出');
const rows = await parseWorkbook(resource, await readBodyBuffer(request));
if (resource === 'results') {
if (user.adminLevel !== 'super') return sendError(response, 403, '只有超级管理员可以预览导入成绩');
@@ -568,12 +569,13 @@ export function createAdminRoutes(context) {
});
const pendingCandidates = profiles.filter(item => item.status === 'pending').length;
const pendingRegistrations = registrations.filter(item => item.status === 'pending').length;
const pendingPayments = registrations.filter(item => item.status === 'approved' && item.paymentStatus === 'unpaid').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 },
metrics: { candidates: profiles.length, pendingCandidates, registrations: registrations.length, pendingRegistrations, pendingPayments, 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)
});
}
@@ -679,6 +681,51 @@ export function createAdminRoutes(context) {
});
return sendJson(response, 200, { ok: true, registrations });
}
if (request.method === 'GET' && pathname === '/api/admin/payments') {
if (!requirePermission(user, response, 'payments.read')) return true;
const registrations = db.registrations
.filter(registration => registration.status === 'approved' && registrationInScope(db, user, registration))
.map(registration => {
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
const view = examRegistrationView(db, registration);
return {
...view,
candidate: profile ? { ...profile, idNumber: maskId(profile.idNumber) } : null,
schoolName: db.schools.find(item => item.id === profile?.schoolId)?.name || profile?.school || '',
className: db.classes.find(item => item.id === profile?.classId)?.name || profile?.grade || '',
amountDue: Number(view.subjects.reduce((sum, subject) => sum + Number(subject.fee || 0), 0).toFixed(2)),
paidByName: db.users.find(item => item.id === registration.paidBy)?.displayName || ''
};
});
return sendJson(response, 200, {
ok: true,
scopeLabel: adminScopeLabel(db, user),
canConfirmPayment: user.adminLevel === 'class',
registrations
});
}
const paymentMatch = pathname.match(/^\/api\/admin\/payments\/([^/]+)$/);
if (request.method === 'PATCH' && paymentMatch) {
if (user.adminLevel !== 'class') return sendError(response, 403, '缴费确认只能由考生所属班级负责人办理');
const registration = db.registrations.find(item => item.id === paymentMatch[1]);
if (!registration || !registrationInScope(db, user, registration)) return sendError(response, 404, '缴费记录不存在或不在本班范围内');
if (registration.status !== 'approved') return sendError(response, 409, '报名审核通过后才能确认缴费');
if (db.exams.find(item => item.id === registration.examId)?.archivedAt) return sendError(response, 409, '该考试已归档,缴费记录已冻结');
if (registration.paymentStatus === 'paid') return sendError(response, 409, '该考生已经确认缴费,请勿重复操作');
registration.paymentStatus = 'paid';
registration.paidAt = nowIso();
registration.paidBy = user.id;
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
const exam = db.exams.find(item => item.id === registration.examId);
await database.updateRegistrationPayment(
registration,
logAction(db, user, '确认考生缴费', `${profile?.name || registration.registrationNumber} · ${exam?.name || registration.examId}`)
);
return sendJson(response, 200, {
ok: true,
payment: { registrationId: registration.id, status: registration.paymentStatus, paidAt: registration.paidAt, paidBy: registration.paidBy, paidByName: user.displayName }
});
}
if (request.method === 'GET' && pathname === '/api/admin/admission-arrangements') {
if (!requirePermission(user, response, 'registrations.read')) return true;
const scopedRegistrations = db.registrations.filter(item => item.status === 'approved' && registrationInScope(db, user, item));
@@ -777,7 +824,7 @@ export function createAdminRoutes(context) {
const account = db.users.find(item => item.id === registration.userId);
if (!account?.candidateNumber) return sendError(response, 409, '考生账户尚未分配报名号,请先在报名号管理中完成分配');
instance.status = 'approved'; instance.completedAt = nowIso(); instance.assigneeId = null;
registration.status = 'approved'; registration.paymentStatus = 'paid'; registration.reviewNote = note; registration.reviewedAt = nowIso();
registration.status = 'approved'; registration.reviewNote = note; registration.reviewedAt = nowIso();
registration.registrationNumber = account.candidateNumber;
registration.numberRuleId = db.numberRules.find(item => item.active)?.id || registration.numberRuleId;
}
+1 -1
View File
@@ -118,7 +118,7 @@ export function createCandidateRoutes(context) {
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 registration = { id: uid('reg'), userId: user.id, examId: exam.id, subjectIds, status: 'pending', paymentStatus: 'unpaid', paidAt: null, paidBy: null, 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: '考试报名已提交' });