缴费
This commit is contained in:
+36
-4
@@ -16,6 +16,8 @@ assert.equal(mysqlRuleSchemas.length, 2, 'MySQL 应包含两张号码规则表')
|
||||
for (const statement of mysqlRuleSchemas) {
|
||||
assert.match(statement, /`separator` VARCHAR\(10\)/, 'MySQL 保留字 separator 必须作为标识符转义');
|
||||
}
|
||||
const mysqlRegistrationSchema = mysqlSchema.find(statement => /CREATE TABLE IF NOT EXISTS registrations\b/.test(statement));
|
||||
assert.match(mysqlRegistrationSchema || '', /paid_at VARCHAR\(35\).*paid_by VARCHAR\(64\)/s, 'MySQL 报名表应保存缴费确认时间和班级负责人');
|
||||
await rm(testDb, { force: true });
|
||||
await rm(`${testDb}-shm`, { force: true });
|
||||
await rm(`${testDb}-wal`, { force: true });
|
||||
@@ -106,13 +108,15 @@ try {
|
||||
const examSubjectColumns = inspector.prepare('PRAGMA table_info(exam_subjects)').all().map(row => row.name);
|
||||
const examColumns = inspector.prepare('PRAGMA table_info(exams)').all().map(row => row.name);
|
||||
const userColumns = inspector.prepare('PRAGMA table_info(users)').all().map(row => row.name);
|
||||
const registrationColumns = inspector.prepare('PRAGMA table_info(registrations)').all().map(row => row.name);
|
||||
inspector.close();
|
||||
assert.deepEqual(tableNames, [...relationalTables].sort(), '业务数据必须按关系模型分表存储');
|
||||
assert.ok(!tableNames.includes('app_state'), '不得使用单表 JSON 状态存储');
|
||||
assert.equal(schemaVersion, 14, '考试归档应使用 v14 数据结构');
|
||||
assert.equal(schemaVersion, 15, '线下缴费确认应使用 v15 数据结构');
|
||||
assert.ok(['archived_at', 'archived_by'].every(column => examColumns.includes(column)), '考试应保存不可逆归档时间和超级管理员');
|
||||
assert.equal(resultLockTriggers.length, 3, '数据库应从插入、更新、删除三个方向永久锁定归档成绩');
|
||||
assert.ok(['archived_at', 'archived_by'].every(column => userColumns.includes(column)), '账户应保存独立归档状态和校方操作人');
|
||||
assert.ok(['payment_status', 'paid_at', 'paid_by'].every(column => registrationColumns.includes(column)), '报名应保存缴费状态、确认时间和班级负责人');
|
||||
assert.ok(['center_code', 'center_address'].every(column => admitCardColumns.includes(column)), '准考证应保存考点代码与详细地址快照');
|
||||
assert.ok(['building', 'floor'].every(column => admitSubjectColumns.includes(column)), '分科考场应保存楼栋楼层快照');
|
||||
assert.ok(['pass_rule', 'pass_value'].every(column => examSubjectColumns.includes(column)), '科目应保存独立及格线计算方式和规则数值');
|
||||
@@ -334,9 +338,9 @@ try {
|
||||
examStart: new Date(now + 48 * hour).toISOString(), examEnd: new Date(now + 60 * hour).toISOString(),
|
||||
admitDownloadStart: new Date(now - hour).toISOString(), admitDownloadEnd: new Date(now + 47 * hour).toISOString(),
|
||||
location: '测试考点', status: 'published', subjects: [
|
||||
{ name: '语文', date: new Date(now + 48 * hour).toISOString().slice(0, 10), start: '09:00', end: '11:00', fee: 0, fullScore: 150, passRule: 'fixed_score', passValue: 90 },
|
||||
{ name: '数学', date: new Date(now + 48 * hour).toISOString().slice(0, 10), start: '13:00', end: '15:00', fee: 0, fullScore: 150, passScore: 90 },
|
||||
{ name: '外语', date: new Date(now + 48 * hour).toISOString().slice(0, 10), start: '15:30', end: '17:00', fee: 0, fullScore: 150, passRule: 'rank_percent', passValue: 60 }
|
||||
{ name: '语文', date: new Date(now + 48 * hour).toISOString().slice(0, 10), start: '09:00', end: '11:00', fee: 20, fullScore: 150, passRule: 'fixed_score', passValue: 90 },
|
||||
{ name: '数学', date: new Date(now + 48 * hour).toISOString().slice(0, 10), start: '13:00', end: '15:00', fee: 20, fullScore: 150, passScore: 90 },
|
||||
{ name: '外语', date: new Date(now + 48 * hour).toISOString().slice(0, 10), start: '15:30', end: '17:00', fee: 25, fullScore: 150, passRule: 'rank_percent', passValue: 60 }
|
||||
]
|
||||
}
|
||||
});
|
||||
@@ -431,6 +435,32 @@ try {
|
||||
const approveRegistration = await admin.request(`/api/admin/registrations/${registrationId}`, { method: 'PATCH', body: { status: 'approved', reviewNote: '科目与资格终审通过' } });
|
||||
assert.equal(approveRegistration.data.registration.status, 'approved');
|
||||
assert.equal(approveRegistration.data.registration.registrationNumber, candidateNumber, '考试报名必须复用考生账户的固定报名号');
|
||||
assert.equal(approveRegistration.data.registration.paymentStatus, 'unpaid', '报名终审不得代替线下缴费确认');
|
||||
|
||||
const superPaymentList = await admin.request('/api/admin/payments');
|
||||
const schoolPaymentList = await schoolAdmin.request('/api/admin/payments');
|
||||
const classPaymentList = await classAdmin.request('/api/admin/payments');
|
||||
assert.ok(superPaymentList.data.registrations.some(item => item.id === registrationId), '超级管理员应能查看全部范围缴费名单');
|
||||
assert.ok(schoolPaymentList.data.registrations.some(item => item.id === registrationId), '校级管理员应能查看本校缴费名单');
|
||||
assert.ok(classPaymentList.data.registrations.some(item => item.id === registrationId), '班级负责人应能查看本班缴费名单');
|
||||
assert.equal(classPaymentList.data.canConfirmPayment, true, '班级负责人应取得缴费确认能力');
|
||||
assert.equal(superPaymentList.data.canConfirmPayment, false, '超级管理员只能查看和导出,不得代班确认');
|
||||
assert.equal((await admin.request(`/api/admin/payments/${registrationId}`, { method: 'PATCH' })).response.status, 403, '超级管理员不得确认考生缴费');
|
||||
assert.equal((await schoolAdmin.request(`/api/admin/payments/${registrationId}`, { method: 'PATCH' })).response.status, 403, '校级管理员不得确认考生缴费');
|
||||
const confirmedPayment = await classAdmin.request(`/api/admin/payments/${registrationId}`, { method: 'PATCH' });
|
||||
assert.equal(confirmedPayment.response.status, 200, '班级负责人应能在线下收款后确认缴费');
|
||||
assert.equal(confirmedPayment.data.payment.status, 'paid');
|
||||
assert.ok(confirmedPayment.data.payment.paidAt && confirmedPayment.data.payment.paidByName, '缴费确认应记录时间和办理人');
|
||||
assert.equal((await classAdmin2.request(`/api/admin/payments/${registrationId}`, { method: 'PATCH' })).response.status, 409, '重复确认缴费应被拒绝');
|
||||
const paidCandidateRegistration = (await candidate.request('/api/candidate/registrations')).data.registrations.find(item => item.id === registrationId);
|
||||
assert.equal(paidCandidateRegistration.paymentStatus, 'paid', '班级确认后考生端应同步显示已缴费');
|
||||
assert.ok(paidCandidateRegistration.paidAt && paidCandidateRegistration.paidByName, '考生应能看到缴费确认记录');
|
||||
assert.equal(paidCandidateRegistration.amountDue, 45, '应缴金额应按所选科目费用汇总');
|
||||
for (const [client, label] of [[admin, '超级管理员'], [schoolAdmin, '校级管理员'], [classAdmin, '班级负责人']]) {
|
||||
const exportPaymentList = await client.request('/api/admin/excel/payments');
|
||||
assert.equal(exportPaymentList.response.status, 200, `${label}应能导出管理范围内的缴费名单`);
|
||||
assert.ok(Buffer.isBuffer(exportPaymentList.data) && exportPaymentList.data.length > 1000, `${label}缴费名单应为有效 Excel 文件`);
|
||||
}
|
||||
|
||||
const arrangementContext = await admin.request('/api/admin/admission-arrangements');
|
||||
assert.equal(arrangementContext.data.rules.length, 4, '系统应预置四种准考证号规则');
|
||||
@@ -661,6 +691,7 @@ try {
|
||||
assert.ok(workflowDefinitions.data.workflows.some(item => item.businessType === 'candidate_account_batch'), '流程设计应包含批量报名号申领审批');
|
||||
assert.ok(workflowDefinitions.data.workflows.some(item => item.businessType === 'score_appeal'), '流程设计应包含考生成绩复议');
|
||||
assert.match(appSource, /renderAdmin, workflowStepEditor/, '添加流程步骤事件必须取得视图模块导出的编辑器,避免未定义错误');
|
||||
assert.match(appSource, /createAdminViews, numberSegmentMeta/, '保存报名号规则必须导入字段元数据,避免提交时出现未定义错误');
|
||||
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, '超级管理员应可设计考生信息修改审批流程');
|
||||
|
||||
@@ -674,6 +705,7 @@ try {
|
||||
console.log('✓ 校级按班级批量申领、终审原子建号与结果返回');
|
||||
console.log('✓ 结构化考点考场档案、变更审批与班级只读边界');
|
||||
console.log('✓ 本校班级/班级管理员管理与多资源 Excel 导入导出');
|
||||
console.log('✓ 班级负责人线下缴费确认、状态同步与三级名单导出');
|
||||
console.log('✓ 成绩录入、发布与考生查询');
|
||||
console.log('✓ 成绩复议、班级审批、范围匹配与多人均分');
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user