Implement batch candidate account requests and approval-based number eal
This commit is contained in:
+132
-77
@@ -45,7 +45,7 @@ function seedDatabase() {
|
||||
const examId = 'exam_autumn_2026';
|
||||
const registrationId = 'reg_demo_2026';
|
||||
return {
|
||||
meta: { version: 4, createdAt: nowIso() },
|
||||
meta: { version: 5, createdAt: nowIso() },
|
||||
settings: { selfRegistrationEnabled: false },
|
||||
organization: {
|
||||
name: '海州市教育考试中心',
|
||||
@@ -112,7 +112,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', registrationNumber: '', numberRuleId: null,
|
||||
status: 'approved', paymentStatus: 'paid', createdAt: '2026-07-08T05:18:00.000Z', reviewedAt: '2026-07-18T08:32:00.000Z', registrationNumber: '2026-HZ01-F-0001', numberRuleId: 'rule_default',
|
||||
admitCard: { number: '260816-031-08', testCenter: '海州市第三中学', room: '031 考场', seat: '08', generatedAt: '2026-07-19T02:00:00.000Z' }
|
||||
}
|
||||
],
|
||||
@@ -133,6 +133,8 @@ function seedDatabase() {
|
||||
],
|
||||
centerChangeRequests: [],
|
||||
centerChangeRooms: [],
|
||||
candidateAccountBatches: [],
|
||||
candidateAccountBatchItems: [],
|
||||
numberRules: [
|
||||
{ id: 'rule_default', name: '年度学校性别流水号', separator: '-', active: true, createdBy: adminId, updatedAt: nowIso(), segments: [
|
||||
{ id: 'segment_year', position: 1, type: 'year', value: '', width: 4 },
|
||||
@@ -152,6 +154,9 @@ function seedDatabase() {
|
||||
] },
|
||||
{ id: 'workflow_center', businessType: 'center_change', name: '考点考场变更审批', active: true, updatedBy: adminId, updatedAt: nowIso(), steps: [
|
||||
{ id: 'workflow_center_step_1', position: 1, name: '考试中心考务终审', adminLevel: 'super' }
|
||||
] },
|
||||
{ id: 'workflow_account_batch', businessType: 'candidate_account_batch', name: '批量报名号申领审批', active: true, updatedBy: adminId, updatedAt: nowIso(), steps: [
|
||||
{ id: 'workflow_account_batch_step_1', position: 1, name: '考试中心账号终审', adminLevel: 'super' }
|
||||
] }
|
||||
],
|
||||
workflowInstances: [],
|
||||
@@ -365,8 +370,37 @@ function workflowScopeProfile(db, instance) {
|
||||
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;
|
||||
if (instance.businessType === 'center_change') {
|
||||
const change = db.centerChangeRequests.find(item => item.id === instance.businessId);
|
||||
return change ? centerScopeProfile(db, change.schoolId) : null;
|
||||
}
|
||||
if (instance.businessType === 'candidate_account_batch') {
|
||||
const batch = db.candidateAccountBatches.find(item => item.id === instance.businessId);
|
||||
return batch ? centerScopeProfile(db, batch.schoolId) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function candidateAccountBatchView(db, batch) {
|
||||
const items = db.candidateAccountBatchItems.filter(item => item.batchId === batch.id).sort((a, b) => a.position - b.position);
|
||||
const quotaMap = new Map();
|
||||
for (const item of items) quotaMap.set(item.classId, (quotaMap.get(item.classId) || 0) + 1);
|
||||
const instance = db.workflowInstances.find(item => item.businessType === 'candidate_account_batch' && item.businessId === batch.id);
|
||||
return {
|
||||
...batch,
|
||||
schoolName: db.schools.find(item => item.id === batch.schoolId)?.name || '',
|
||||
requesterName: db.users.find(item => item.id === batch.requestedBy)?.displayName || '原提交人',
|
||||
totalCount: items.length,
|
||||
quotas: [...quotaMap.entries()].map(([classId, count]) => {
|
||||
const schoolClass = db.classes.find(item => item.id === classId);
|
||||
return { classId, className: schoolClass?.name || '未知班级', grade: schoolClass?.grade || '', count };
|
||||
}),
|
||||
items: items.map(item => {
|
||||
const schoolClass = db.classes.find(entry => entry.id === item.classId);
|
||||
return { ...item, className: schoolClass?.name || '未知班级', grade: schoolClass?.grade || '' };
|
||||
}),
|
||||
workflow: workflowView(db, instance)
|
||||
};
|
||||
}
|
||||
|
||||
function centerChangeView(db, change) {
|
||||
@@ -691,24 +725,91 @@ async function handleAdmin(request, response, pathname) {
|
||||
await database.updateRegistrationSetting(enabled, log);
|
||||
return sendJson(response, 200, { ok: true, enabled });
|
||||
}
|
||||
if (pathname === '/api/admin/candidate-accounts' && request.method === 'POST') {
|
||||
if (pathname === '/api/admin/candidate-account-batches' && request.method === 'GET') {
|
||||
if (!requirePermission(user, response, 'candidates.write')) return true;
|
||||
if (!['school', 'super'].includes(user.adminLevel)) return sendError(response, 403, '只有校级管理员可以申领批量报名号');
|
||||
const batches = db.candidateAccountBatches
|
||||
.filter(item => user.adminLevel === 'super' || item.schoolId === user.schoolId)
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
.map(item => candidateAccountBatchView(db, item));
|
||||
const classes = db.classes.filter(item => item.active && (user.adminLevel === 'super' || item.schoolId === user.schoolId));
|
||||
return sendJson(response, 200, { ok: true, batches, classes, schools: db.schools.filter(item => item.active) });
|
||||
}
|
||||
if (pathname === '/api/admin/candidate-account-batches' && request.method === 'POST') {
|
||||
if (user.adminLevel !== 'school' || !requirePermission(user, response, 'candidates.write')) return user.adminLevel === 'school' ? true : sendError(response, 403, '批量报名号由校级管理员发起申领');
|
||||
const body = await readJson(request);
|
||||
const requestedQuotas = Array.isArray(body.quotas) ? body.quotas : [];
|
||||
const quotas = requestedQuotas.map(item => ({ classId: cleanText(item.classId, 64), count: Number(item.count) })).filter(item => item.count > 0);
|
||||
if (!quotas.length) return sendError(response, 400, '请至少为一个班级填写申领数量');
|
||||
if (new Set(quotas.map(item => item.classId)).size !== quotas.length) return sendError(response, 400, '同一班级只能填写一次申领数量');
|
||||
if (quotas.some(item => !Number.isInteger(item.count) || item.count < 1 || item.count > 200)) return sendError(response, 400, '每个班级一次可申领 1—200 个报名号');
|
||||
if (quotas.some(item => !db.classes.some(schoolClass => schoolClass.id === item.classId && schoolClass.schoolId === user.schoolId && schoolClass.active))) return sendError(response, 400, '只能为本校有效班级申领报名号');
|
||||
const totalCount = quotas.reduce((sum, item) => sum + item.count, 0);
|
||||
if (totalCount > 500) return sendError(response, 400, '单个批次最多申领 500 个报名号');
|
||||
const batch = { id: uid('account_batch'), schoolId: user.schoolId, requestedBy: user.id, status: 'pending', reviewNote: '', createdAt: nowIso(), reviewedAt: null };
|
||||
const items = [];
|
||||
let position = 1;
|
||||
for (const quota of quotas) for (let index = 0; index < quota.count; index += 1) {
|
||||
items.push({ id: uid('account_batch_item'), batchId: batch.id, classId: quota.classId, position, candidateNumber: '', initialPassword: '', userId: null, createdAt: null });
|
||||
position += 1;
|
||||
}
|
||||
const { instance, action } = createWorkflowSubmission(db, 'candidate_account_batch', batch.id, centerScopeProfile(db, user.schoolId), user.id);
|
||||
const quotaSummary = quotas.map(item => `${db.classes.find(entry => entry.id === item.classId)?.name} ${item.count} 人`).join(';');
|
||||
const log = logAction(db, user, '提交批量报名号申领', `${totalCount} 个账户 · ${quotaSummary}`);
|
||||
await database.createCandidateAccountBatch(batch, items, instance, action, log);
|
||||
const fresh = await readDb();
|
||||
return sendJson(response, 202, { ok: true, batch: candidateAccountBatchView(fresh, fresh.candidateAccountBatches.find(item => item.id === batch.id)) });
|
||||
}
|
||||
const accountBatchMatch = pathname.match(/^\/api\/admin\/candidate-account-batches\/([^/]+)$/);
|
||||
if (accountBatchMatch && request.method === 'PATCH') {
|
||||
if (!requirePermission(user, response, 'candidates.write')) return true;
|
||||
const body = await readJson(request);
|
||||
const name = cleanText(body.name, 50);
|
||||
const gender = cleanText(body.gender, 10);
|
||||
const initialPassword = String(body.initialPassword || '');
|
||||
const schoolId = user.adminLevel === 'super' ? cleanText(body.schoolId, 64) : user.schoolId;
|
||||
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 (!name || !['男', '女'].includes(gender) || initialPassword.length < 8 || !school || !schoolClass) return sendError(response, 400, '请填写姓名、性别、有效学校班级和至少 8 位初始密码');
|
||||
const generated = generateCandidateNumber(db, { schoolId, classId, gender });
|
||||
const userId = uid('usr');
|
||||
const candidateUser = { id: userId, username: generated.number, candidateNumber: generated.number, passwordHash: hashPassword(initialPassword), role: 'candidate', displayName: name, schoolId, classId, active: true, mustChangePassword: true, createdAt: nowIso() };
|
||||
const profile = { id: uid('profile'), userId, name, gender, idNumber: `PENDING-${userId}`, phone: '', email: '', school: school.name, grade: schoolClass.name, schoolId, classId, address: '', emergencyContact: '', emergencyPhone: '', nativePlace: '', birthDate: '', ethnicity: '', postalCode: '', guardianName: '', guardianPhone: '', profileCompleted: false, status: 'pending', reviewNote: '', updatedAt: nowIso() };
|
||||
const log = logAction(db, user, '创建考生账户', `${name} · ${generated.number} · ${school.name} ${schoolClass.name}`);
|
||||
await database.createCandidate(candidateUser, profile, null, null, log);
|
||||
return sendJson(response, 201, { ok: true, candidate: { ...profile, candidateNumber: generated.number, mustChangePassword: true } });
|
||||
if (!['approved', 'rejected'].includes(body.status)) return sendError(response, 400, '审批状态无效');
|
||||
const batch = db.candidateAccountBatches.find(item => item.id === accountBatchMatch[1] && item.status === 'pending');
|
||||
if (!batch) return sendError(response, 404, '待审批的批量报名号申请不存在');
|
||||
const instance = pendingWorkflow(db, 'candidate_account_batch', batch.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' ? '审批批量报名号申领' : '退回批量报名号申领', `${db.schools.find(item => item.id === batch.schoolId)?.name} · ${note || '无备注'}`);
|
||||
if (body.status === 'rejected') {
|
||||
instance.status = 'rejected'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
batch.status = 'rejected'; batch.reviewNote = note; batch.reviewedAt = nowIso();
|
||||
await database.processWorkflow(instance, action, batch, 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, batch.schoolId))[0];
|
||||
if (!nextAssignee) return sendError(response, 409, `没有可承接“${nextStep.name}”的管理员`);
|
||||
instance.currentStep += 1; instance.assigneeId = nextAssignee.id; action.toAssigneeId = nextAssignee.id;
|
||||
batch.reviewNote = note;
|
||||
await database.processWorkflow(instance, action, batch, log);
|
||||
} else {
|
||||
const batchItems = db.candidateAccountBatchItems.filter(item => item.batchId === batch.id).sort((a, b) => a.position - b.position);
|
||||
if (!batchItems.length || batchItems.some(item => item.userId || item.candidateNumber)) return sendError(response, 409, '批次明细异常或已经生成过账号');
|
||||
const generationDb = { ...db, users: [...db.users] };
|
||||
const users = [];
|
||||
const profiles = [];
|
||||
for (const [index, item] of batchItems.entries()) {
|
||||
const schoolClass = db.classes.find(entry => entry.id === item.classId && entry.schoolId === batch.schoolId);
|
||||
if (!schoolClass) return sendError(response, 409, '批次包含无效班级,无法生成账号');
|
||||
const generated = generateCandidateNumber(generationDb, { schoolId: batch.schoolId, classId: item.classId, gender: '' });
|
||||
const userId = uid('usr');
|
||||
const initialPassword = `Init-${randomBytes(6).toString('base64url')}`;
|
||||
const displayName = `待补录考生 ${String(index + 1).padStart(3, '0')}`;
|
||||
const candidateUser = { id: userId, username: generated.number, candidateNumber: generated.number, passwordHash: hashPassword(initialPassword), role: 'candidate', displayName, schoolId: batch.schoolId, classId: item.classId, active: true, mustChangePassword: true, createdAt: nowIso() };
|
||||
const profile = { id: uid('profile'), userId, name: displayName, gender: '', idNumber: `PENDING-${userId}`, phone: '', email: '', school: db.schools.find(entry => entry.id === batch.schoolId)?.name || '', grade: schoolClass.name, schoolId: batch.schoolId, classId: item.classId, address: '', emergencyContact: '', emergencyPhone: '', nativePlace: '', birthDate: '', ethnicity: '', postalCode: '', guardianName: '', guardianPhone: '', profileCompleted: false, status: 'pending', reviewNote: '', updatedAt: nowIso() };
|
||||
item.candidateNumber = generated.number; item.initialPassword = initialPassword; item.userId = userId; item.createdAt = nowIso();
|
||||
users.push(candidateUser); profiles.push(profile); generationDb.users.push(candidateUser);
|
||||
}
|
||||
instance.status = 'approved'; instance.completedAt = nowIso(); instance.assigneeId = null;
|
||||
batch.status = 'approved'; batch.reviewNote = note; batch.reviewedAt = nowIso();
|
||||
await database.completeCandidateAccountBatch(batch, batchItems, users, profiles, instance, action, log);
|
||||
}
|
||||
const fresh = await readDb();
|
||||
return sendJson(response, 200, { ok: true, batch: candidateAccountBatchView(fresh, fresh.candidateAccountBatches.find(item => item.id === batch.id)) });
|
||||
}
|
||||
|
||||
if (pathname === '/api/admin/centers' && request.method === 'GET') {
|
||||
@@ -804,11 +905,7 @@ async function handleAdmin(request, response, pathname) {
|
||||
const previewProfile = db.candidateProfiles[0] || { gender: '女', schoolId: db.schools[0]?.id };
|
||||
let preview = '';
|
||||
if (rule) preview = generateCandidateNumber(db, 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 });
|
||||
return sendJson(response, 200, { ok: true, rules: db.numberRules, activeRule: rule, preview });
|
||||
}
|
||||
if (pathname === '/api/admin/number-rules' && request.method === 'POST') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
@@ -827,44 +924,11 @@ async function handleAdmin(request, response, pathname) {
|
||||
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 activeRule = db.numberRules.find(item => item.active) || null;
|
||||
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 changedUsers = new Map();
|
||||
const generated = eligible.map(registration => {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === registration.userId);
|
||||
const account = db.users.find(item => item.id === registration.userId);
|
||||
if (!account.candidateNumber) {
|
||||
const result = generateCandidateNumber(db, profile);
|
||||
account.candidateNumber = result.number;
|
||||
changedUsers.set(account.id, account);
|
||||
}
|
||||
registration.registrationNumber = account.candidateNumber;
|
||||
registration.numberRuleId = activeRule?.id || null;
|
||||
return registration;
|
||||
});
|
||||
const log = logAction(db, user, '批量同步账户报名号', `${generated.length} 条考试报名 · ${changedUsers.size} 个新账户号码`);
|
||||
await database.assignCandidateNumbers([...changedUsers.values()], 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)$/);
|
||||
const workflowDefinitionMatch = pathname.match(/^\/api\/admin\/workflows\/(profile_change|registration_review|center_change|candidate_account_batch)$/);
|
||||
if (workflowDefinitionMatch && request.method === 'PUT') {
|
||||
if (!requirePermission(user, response, '*')) return true;
|
||||
const body = await readJson(request);
|
||||
@@ -872,6 +936,7 @@ async function handleAdmin(request, response, pathname) {
|
||||
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, '流程至少需要一个校级或超级管理员审批步骤');
|
||||
if (workflowDefinitionMatch[1] === 'candidate_account_batch' && steps.at(-1)?.adminLevel !== 'super') return sendError(response, 400, '批量报名号申领的最终步骤必须由超级管理员审批');
|
||||
workflow.name = cleanText(body.name, 80) || workflow.name;
|
||||
workflow.updatedBy = user.id;
|
||||
workflow.updatedAt = nowIso();
|
||||
@@ -891,10 +956,13 @@ async function handleAdmin(request, response, pathname) {
|
||||
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;
|
||||
const accountBatch = instance.businessType === 'candidate_account_batch' ? db.candidateAccountBatches.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
|
||||
centerName: centerChange?.name || '', requestType: centerChange?.requestType || '', centerChange: centerChange ? centerChangeView(db, centerChange) : null,
|
||||
accountBatch: accountBatch ? candidateAccountBatchView(db, accountBatch) : null,
|
||||
batchTotalCount: accountBatch ? db.candidateAccountBatchItems.filter(item => item.batchId === accountBatch.id).length : 0
|
||||
};
|
||||
});
|
||||
const availableAdmins = db.users.filter(item => item.role === 'admin' && item.active).map(safeUser);
|
||||
@@ -925,6 +993,7 @@ async function handleAdmin(request, response, pathname) {
|
||||
const body = await readJson(request);
|
||||
const instance = db.workflowInstances.find(item => item.id === superviseMatch[1]);
|
||||
if (!instance) return sendError(response, 404, '流程不存在');
|
||||
if (instance.businessType === 'candidate_account_batch' && db.candidateAccountBatchItems.some(item => item.batchId === instance.businessId && item.userId)) return sendError(response, 409, '已生成账号的批次不可重新打开,避免重复建号');
|
||||
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);
|
||||
@@ -941,7 +1010,9 @@ async function handleAdmin(request, response, pathname) {
|
||||
? profile
|
||||
: instance.businessType === 'registration_review'
|
||||
? db.registrations.find(item => item.id === instance.businessId)
|
||||
: db.centerChangeRequests.find(item => item.id === instance.businessId);
|
||||
: instance.businessType === 'center_change'
|
||||
? db.centerChangeRequests.find(item => item.id === instance.businessId)
|
||||
: db.candidateAccountBatches.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);
|
||||
@@ -1055,22 +1126,6 @@ async function handleAdmin(request, response, pathname) {
|
||||
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 account = db.users.find(item => item.id === registration.userId);
|
||||
if (!account?.candidateNumber) return sendError(response, 409, '考生账户尚未分配报名号');
|
||||
registration.registrationNumber = account.candidateNumber;
|
||||
registration.numberRuleId = db.numberRules.find(item => item.active)?.id || null;
|
||||
const log = logAction(db, user, '同步账户报名号', `${profile?.name || registration.userId} · ${account.candidateNumber}`);
|
||||
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]);
|
||||
|
||||
Reference in New Issue
Block a user