Implement batch candidate account requests and approval-based number eal
This commit is contained in:
+49
-15
@@ -56,6 +56,7 @@ const schoolAdmin2 = createClient();
|
||||
const classAdmin = createClient();
|
||||
const candidate = createClient();
|
||||
const selfCandidate = createClient();
|
||||
const batchCandidate = createClient();
|
||||
const anonymous = createClient();
|
||||
|
||||
try {
|
||||
@@ -98,16 +99,21 @@ try {
|
||||
assert.equal((await schoolAdmin2.request('/api/auth/login', { method: 'POST', body: { username: 'school_admin_2', password: 'School123!' } })).data.user.adminLevel, 'school');
|
||||
assert.equal((await classAdmin.request('/api/auth/login', { method: 'POST', body: { username: 'class_admin', password: 'Class123!' } })).data.user.adminLevel, 'class');
|
||||
|
||||
const issuedAccount = await schoolAdmin.request('/api/admin/candidate-accounts', { method: 'POST', body: {
|
||||
name: '测试考生', gender: '男', classId: 'class_hz1_302', initialPassword: 'Initial123!'
|
||||
} });
|
||||
assert.equal(issuedAccount.response.status, 201, '校级管理员应能为本校考生创建固定报名号账户');
|
||||
const candidateNumber = issuedAccount.data.candidate.candidateNumber;
|
||||
assert.match(candidateNumber, /^2026-HZ01-M-\d{4}$/, '账户报名号应按当前规则生成');
|
||||
const loginCandidate = await candidate.request('/api/auth/login', { method: 'POST', body: { username: candidateNumber, password: 'Initial123!' } });
|
||||
assert.equal((await admin.request('/api/admin/candidate-accounts', { method: 'POST', body: {} })).response.status, 404, '超级管理员不得再从旧入口直接生成报名号');
|
||||
const singleAccountBatch = await schoolAdmin.request('/api/admin/candidate-account-batches', { method: 'POST', body: { quotas: [
|
||||
{ classId: 'class_hz1_302', count: 1 }
|
||||
] } });
|
||||
assert.equal(singleAccountBatch.response.status, 202, '即使只申领一个账号,校级管理员也应提交审批');
|
||||
assert.ok(singleAccountBatch.data.batch.items.every(item => !item.candidateNumber), '最终批准前不得生成账号');
|
||||
const approvedSingleBatch = await admin.request(`/api/admin/candidate-account-batches/${singleAccountBatch.data.batch.id}`, { method: 'PATCH', body: { status: 'approved', reviewNote: '单人名额核验通过' } });
|
||||
assert.equal(approvedSingleBatch.response.status, 200);
|
||||
const candidateNumber = approvedSingleBatch.data.batch.items[0].candidateNumber;
|
||||
const initialPassword = approvedSingleBatch.data.batch.items[0].initialPassword;
|
||||
assert.match(candidateNumber, /^2026-HZ01-X-\d{4}$/, '占位账户应按当前规则生成固定报名号');
|
||||
const loginCandidate = await candidate.request('/api/auth/login', { method: 'POST', body: { username: candidateNumber, password: initialPassword } });
|
||||
assert.equal(loginCandidate.data.user.mustChangePassword, true, '学校下发账户首次登录必须修改初始密码');
|
||||
assert.equal((await candidate.request('/api/candidate/dashboard')).response.status, 428, '未修改初始密码前不得进入考生业务');
|
||||
const changedPassword = await candidate.request('/api/auth/change-password', { method: 'POST', body: { currentPassword: 'Initial123!', newPassword: 'Test12345!' } });
|
||||
const changedPassword = await candidate.request('/api/auth/change-password', { method: 'POST', body: { currentPassword: initialPassword, newPassword: 'Test12345!' } });
|
||||
assert.equal(changedPassword.data.user.mustChangePassword, false, '修改密码后应解除首次登录限制');
|
||||
assert.equal((await candidate.request('/api/candidate/dashboard')).response.status, 428, '未补全个人信息前仍不得进入考试业务');
|
||||
const updateProfile = await candidate.request('/api/candidate/profile', {
|
||||
@@ -172,12 +178,39 @@ try {
|
||||
]
|
||||
} });
|
||||
assert.equal(saveNumberRule.response.status, 200, '超级管理员应可自由组合并启用报名号逻辑');
|
||||
assert.ok(numberRules.data.batchCandidates.some(item => item.id === 'reg_demo_2026'), '报名号页面应列出审核通过但缺少号码的记录');
|
||||
const batchNumbers = await admin.request('/api/admin/registration-numbers/batch', { method: 'POST', body: { examId: 'exam_autumn_2026', schoolId: 'school_hz1' } });
|
||||
assert.equal(batchNumbers.response.status, 200, '超级管理员应可按考试和学校批量生成报名号');
|
||||
assert.equal(batchNumbers.data.count, 1, '批量生成只处理筛选范围内缺失号码的记录');
|
||||
assert.match(batchNumbers.data.registrations[0].registrationNumber, /^2026-HZ01-F-\d{4}$/);
|
||||
assert.equal((await admin.request('/api/admin/registration-numbers/batch', { method: 'POST', body: { examId: 'exam_autumn_2026', schoolId: 'school_hz1' } })).response.status, 409, '重复批量执行不得覆盖已有报名号');
|
||||
assert.equal(numberRules.data.batchCandidates, undefined, '超级管理员号码规则页不应再提供直接批量建号数据');
|
||||
assert.equal((await admin.request('/api/admin/registration-numbers/batch', { method: 'POST', body: {} })).response.status, 404, '旧的超级管理员直接批量生成接口应移除');
|
||||
|
||||
const candidatesBeforeBatch = (await schoolAdmin.request('/api/admin/candidates')).data.candidates.length;
|
||||
const createAccountBatch = await schoolAdmin.request('/api/admin/candidate-account-batches', { method: 'POST', body: { quotas: [
|
||||
{ classId: 'class_hz1_301', count: 2 }, { classId: 'class_hz1_302', count: 1 }
|
||||
] } });
|
||||
assert.equal(createAccountBatch.response.status, 202, '校级管理员应可按多个班级数量提交批量建号申请');
|
||||
assert.equal(createAccountBatch.data.batch.totalCount, 3, '批次总数应等于各班级配额之和');
|
||||
assert.deepEqual(createAccountBatch.data.batch.quotas.map(item => item.count).sort(), [1, 2], '批次应保留每个班级的独立数量');
|
||||
assert.ok(createAccountBatch.data.batch.items.every(item => !item.candidateNumber && !item.initialPassword), '审批通过前不得提前生成报名号或初始密码');
|
||||
assert.equal((await schoolAdmin.request('/api/admin/candidates')).data.candidates.length, candidatesBeforeBatch, '审批通过前不得创建任何考生账户');
|
||||
const batchId = createAccountBatch.data.batch.id;
|
||||
assert.equal((await schoolAdmin.request(`/api/admin/candidate-account-batches/${batchId}`, { method: 'PATCH', body: { status: 'approved' } })).response.status, 403, '校级申请人不得绕过超级管理员审批');
|
||||
const batchFlowList = await admin.request('/api/admin/workflow-instances');
|
||||
const batchFlow = batchFlowList.data.instances.find(item => item.businessType === 'candidate_account_batch' && item.businessId === batchId);
|
||||
assert.equal(batchFlow.batchTotalCount, 3, '超级管理员流程中心应展示批次总数');
|
||||
assert.deepEqual(batchFlow.accountBatch.quotas.map(item => item.count).sort(), [1, 2], '流程详情应展示班级配额');
|
||||
const approveAccountBatch = await admin.request(`/api/admin/candidate-account-batches/${batchId}`, { method: 'PATCH', body: { status: 'approved', reviewNote: '学籍名额核验通过' } });
|
||||
assert.equal(approveAccountBatch.response.status, 200, '超级管理员最终批准后应生成整批账号');
|
||||
assert.equal(approveAccountBatch.data.batch.status, 'approved');
|
||||
assert.equal(approveAccountBatch.data.batch.items.length, 3);
|
||||
assert.ok(approveAccountBatch.data.batch.items.every(item => /^2026-HZ01-X-\d{4}$/.test(item.candidateNumber)), '批量占位账户应按规则使用未知性别 X 生成固定号码');
|
||||
assert.equal(new Set(approveAccountBatch.data.batch.items.map(item => item.candidateNumber)).size, 3, '批量生成的报名号必须唯一');
|
||||
assert.ok(approveAccountBatch.data.batch.items.every(item => item.initialPassword.startsWith('Init-')), '结果应向校级管理员返回随机初始密码');
|
||||
assert.equal((await schoolAdmin.request('/api/admin/candidates')).data.candidates.length, candidatesBeforeBatch + 3, '最终批准应原子创建全部考生账户');
|
||||
assert.equal((await admin.request(`/api/admin/candidate-account-batches/${batchId}`, { method: 'PATCH', body: { status: 'approved' } })).response.status, 404, '重复审批不得再次生成账号');
|
||||
const schoolBatchResult = await schoolAdmin.request('/api/admin/candidate-account-batches');
|
||||
const returnedBatch = schoolBatchResult.data.batches.find(item => item.id === batchId);
|
||||
assert.ok(returnedBatch.items.every(item => item.candidateNumber && item.initialPassword), '批准结果应回到校级管理员的批次详情');
|
||||
const firstIssued = returnedBatch.items[0];
|
||||
const batchLogin = await batchCandidate.request('/api/auth/login', { method: 'POST', body: { username: firstIssued.candidateNumber, password: firstIssued.initialPassword } });
|
||||
assert.equal(batchLogin.data.user.mustChangePassword, true, '批量下发账户首次登录也必须强制修改密码');
|
||||
|
||||
const now = Date.now();
|
||||
const hour = 60 * 60 * 1000;
|
||||
@@ -314,6 +347,7 @@ try {
|
||||
const workflowDefinitions = await admin.request('/api/admin/workflows');
|
||||
const profileWorkflow = workflowDefinitions.data.workflows.find(item => item.businessType === 'profile_change');
|
||||
assert.ok(workflowDefinitions.data.workflows.some(item => item.businessType === 'center_change'), '流程设计应包含考点考场变更审批');
|
||||
assert.ok(workflowDefinitions.data.workflows.some(item => item.businessType === 'candidate_account_batch'), '流程设计应包含批量报名号申领审批');
|
||||
const updateWorkflow = await admin.request('/api/admin/workflows/profile_change', { method: 'PUT', body: { name: profileWorkflow.name, steps: profileWorkflow.steps.map(item => ({ name: item.name, adminLevel: item.adminLevel })) } });
|
||||
assert.equal(updateWorkflow.response.status, 200, '超级管理员应可设计考生信息修改审批流程');
|
||||
|
||||
@@ -323,7 +357,7 @@ try {
|
||||
console.log('✓ 固定报名号账户、首次强制改密、完整资料与注册开关');
|
||||
console.log('✓ 多科目考试创建与考生自主选科报名');
|
||||
console.log('✓ 审批流程设计、同级转交与超级管理员监督退回');
|
||||
console.log('✓ 自定义账户号码、旧记录批量同步、准考证与下载窗口限制');
|
||||
console.log('✓ 校级按班级批量申领、终审原子建号与结果返回');
|
||||
console.log('✓ 结构化考点考场档案、变更审批与班级只读边界');
|
||||
console.log('✓ 成绩录入、发布与考生查询');
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user