Add admission function #1
+6
-2
@@ -357,6 +357,10 @@ export function buildSeedOperations(state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stateFromRows(rows) {
|
function stateFromRows(rows) {
|
||||||
|
const parseJson = (value, fallback) => {
|
||||||
|
if (value != null && typeof value === 'object') return value;
|
||||||
|
try { return JSON.parse(value || JSON.stringify(fallback)); } catch { return fallback; }
|
||||||
|
};
|
||||||
const subjectsByExam = new Map();
|
const subjectsByExam = new Map();
|
||||||
for (const row of rows.subjects) {
|
for (const row of rows.subjects) {
|
||||||
const fullScore = Number(row.full_score ?? 150);
|
const fullScore = Number(row.full_score ?? 150);
|
||||||
@@ -518,7 +522,7 @@ function stateFromRows(rows) {
|
|||||||
postalCode: row.postal_code || '',
|
postalCode: row.postal_code || '',
|
||||||
guardianName: row.guardian_name || '',
|
guardianName: row.guardian_name || '',
|
||||||
guardianPhone: row.guardian_phone || '',
|
guardianPhone: row.guardian_phone || '',
|
||||||
specialtyTypes: (() => { try { return JSON.parse(row.specialty_types || '[]'); } catch { return []; } })(),
|
specialtyTypes: parseJson(row.specialty_types, []),
|
||||||
specialtyCertificate: row.specialty_certificate || '',
|
specialtyCertificate: row.specialty_certificate || '',
|
||||||
policyEligibility: row.policy_eligibility || '',
|
policyEligibility: row.policy_eligibility || '',
|
||||||
profileCompleted: Boolean(row.profile_completed),
|
profileCompleted: Boolean(row.profile_completed),
|
||||||
@@ -759,7 +763,7 @@ function stateFromRows(rows) {
|
|||||||
userId: row.user_id || null,
|
userId: row.user_id || null,
|
||||||
schoolId: row.school_id || null,
|
schoolId: row.school_id || null,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
payload: (() => { try { return JSON.parse(row.payload_json || '{}'); } catch { return {}; } })(),
|
payload: parseJson(row.payload_json, {}),
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const mysqlBusinessTables = [
|
|||||||
'schools', 'school_classes', 'candidate_profiles', 'notices', 'exams', 'exam_subjects',
|
'schools', 'school_classes', 'candidate_profiles', 'notices', 'exams', 'exam_subjects',
|
||||||
'registrations', 'registration_subjects', 'exam_arrangement_plans', 'admit_cards', 'admit_card_subjects',
|
'registrations', 'registration_subjects', 'exam_arrangement_plans', 'admit_cards', 'admit_card_subjects',
|
||||||
'results', 'test_centers', 'test_rooms', 'center_change_requests', 'center_change_rooms',
|
'results', 'test_centers', 'test_rooms', 'center_change_requests', 'center_change_rooms',
|
||||||
'candidate_account_batches', 'candidate_account_batch_items', 'workflow_instances', 'workflow_actions', 'audit_logs'
|
'candidate_account_batches', 'candidate_account_batch_items', 'workflow_instances', 'workflow_actions', 'admission_records', 'audit_logs'
|
||||||
];
|
];
|
||||||
|
|
||||||
async function prepareMysql() {
|
async function prepareMysql() {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -175,9 +175,11 @@ export function createAdminRoutes(context) {
|
|||||||
const exam = db.exams.find(item => item.id === settingMatch[1] && !item.archivedAt);
|
const exam = db.exams.find(item => item.id === settingMatch[1] && !item.archivedAt);
|
||||||
if (!exam) return sendError(response, 404, '考试不存在或已经归档');
|
if (!exam) return sendError(response, 404, '考试不存在或已经归档');
|
||||||
const body = await readJson(request);
|
const body = await readJson(request);
|
||||||
const status = admissionPhases.has(body.status) ? body.status : 'draft';
|
|
||||||
const now = nowIso();
|
const now = nowIso();
|
||||||
const setting = admissionSetting(db, exam.id) || { id: uid('admission_setting'), kind: 'setting', examId: exam.id, userId: user.id, schoolId: null, createdAt: now };
|
const setting = admissionSetting(db, exam.id) || { id: uid('admission_setting'), kind: 'setting', examId: exam.id, userId: user.id, schoolId: null, createdAt: now };
|
||||||
|
const requestedStatus = admissionPhases.has(body.status) ? body.status : 'draft';
|
||||||
|
const manualPhases = ['draft', 'filling', 'closed'];
|
||||||
|
const status = setting.status && !manualPhases.includes(setting.status) ? setting.status : manualPhases.includes(requestedStatus) ? requestedStatus : (setting.status || 'draft');
|
||||||
setting.status = status;
|
setting.status = status;
|
||||||
setting.updatedAt = now;
|
setting.updatedAt = now;
|
||||||
setting.payload = { ...setting.payload, enabled: body.enabled === true, preferenceStart: cleanText(body.preferenceStart, 35), preferenceEnd: cleanText(body.preferenceEnd, 35), maxChoices: Math.min(20, Math.max(1, Math.trunc(Number(body.maxChoices || 5)))), round: Math.max(1, Math.trunc(Number(body.round || setting.payload?.round || 1))), autoPublish: body.autoPublish !== false, progress: cleanText(body.progress, 200) || '等待志愿填报开始' };
|
setting.payload = { ...setting.payload, enabled: body.enabled === true, preferenceStart: cleanText(body.preferenceStart, 35), preferenceEnd: cleanText(body.preferenceEnd, 35), maxChoices: Math.min(20, Math.max(1, Math.trunc(Number(body.maxChoices || 5)))), round: Math.max(1, Math.trunc(Number(body.round || setting.payload?.round || 1))), autoPublish: body.autoPublish !== false, progress: cleanText(body.progress, 200) || '等待志愿填报开始' };
|
||||||
@@ -195,6 +197,7 @@ export function createAdminRoutes(context) {
|
|||||||
if (categories.some(item => item.indicatorAllocations.reduce((sum, allocation) => sum + allocation.quota, 0) > item.quota)) return sendError(response, 400, '指标分配合计不能超过类别计划人数');
|
if (categories.some(item => item.indicatorAllocations.reduce((sum, allocation) => sum + allocation.quota, 0) > item.quota)) return sendError(response, 400, '指标分配合计不能超过类别计划人数');
|
||||||
if (categories.some(item => item.indicatorAllocations.some(allocation => !db.schools.some(entry => entry.id === allocation.sourceSchoolId)))) return sendError(response, 400, '指标分配中包含无效的生源学校 ID');
|
if (categories.some(item => item.indicatorAllocations.some(allocation => !db.schools.some(entry => entry.id === allocation.sourceSchoolId)))) return sendError(response, 400, '指标分配中包含无效的生源学校 ID');
|
||||||
const existing = admissionRecords(db, 'plan', exam.id).find(item => item.schoolId === school.id);
|
const existing = admissionRecords(db, 'plan', exam.id).find(item => item.schoolId === school.id);
|
||||||
|
if (admissionRecords(db, 'placement', exam.id).some(item => item.schoolId === school.id && item.status !== 'withdrawn')) return sendError(response, 409, '已经产生投档记录,不能再修改该校本轮招生计划');
|
||||||
const now = nowIso();
|
const now = nowIso();
|
||||||
const plan = existing || { id: uid('plan'), kind: 'plan', examId: exam.id, schoolId: school.id, createdAt: now };
|
const plan = existing || { id: uid('plan'), kind: 'plan', examId: exam.id, schoolId: school.id, createdAt: now };
|
||||||
Object.assign(plan, { userId: user.id, status: 'approved', updatedAt: now, payload: { categories, note: cleanText(body.note, 500), submittedBy: user.displayName, reviewedBy: user.displayName, reviewedAt: now, reviewNote: '超级管理员代上传并审核通过' } });
|
Object.assign(plan, { userId: user.id, status: 'approved', updatedAt: now, payload: { categories, note: cleanText(body.note, 500), submittedBy: user.displayName, reviewedBy: user.displayName, reviewedAt: now, reviewNote: '超级管理员代上传并审核通过' } });
|
||||||
@@ -239,6 +242,7 @@ export function createAdminRoutes(context) {
|
|||||||
}
|
}
|
||||||
const body = await readJson(request);
|
const body = await readJson(request);
|
||||||
const now = nowIso();
|
const now = nowIso();
|
||||||
|
if (admissionRecords(db, 'placement', setting.examId).some(item => ['school_review', 'withdrawal_pending'].includes(item.status))) return sendError(response, 409, '仍有学校审核或退档申请待处理,暂不能开启补录');
|
||||||
setting.status = 'supplementary'; setting.updatedAt = now; setting.payload = { ...setting.payload, round: Number(setting.payload.round || 1) + 1, preferenceStart: cleanText(body.preferenceStart, 35) || now, preferenceEnd: cleanText(body.preferenceEnd, 35), progress: '招生计划未满,补录志愿填报进行中' };
|
setting.status = 'supplementary'; setting.updatedAt = now; setting.payload = { ...setting.payload, round: Number(setting.payload.round || 1) + 1, preferenceStart: cleanText(body.preferenceStart, 35) || now, preferenceEnd: cleanText(body.preferenceEnd, 35), progress: '招生计划未满,补录志愿填报进行中' };
|
||||||
await database.saveAdmissionRecord(setting, logAction(db, user, '开启补录', `${setting.examId} · 第 ${setting.payload.round} 轮`));
|
await database.saveAdmissionRecord(setting, logAction(db, user, '开启补录', `${setting.examId} · 第 ${setting.payload.round} 轮`));
|
||||||
return sendJson(response, 200, { ok: true, setting });
|
return sendJson(response, 200, { ok: true, setting });
|
||||||
|
|||||||
+14
-3
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|||||||
import { pbkdf2Sync, randomBytes } from 'node:crypto';
|
import { pbkdf2Sync, randomBytes } from 'node:crypto';
|
||||||
import { readFile, rm } from 'node:fs/promises';
|
import { readFile, rm } from 'node:fs/promises';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
import { createServer as createNetServer } from 'node:net';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import ExcelJS from 'exceljs';
|
import ExcelJS from 'exceljs';
|
||||||
import { createDatabase, relationalTables } from '../database.mjs';
|
import { createDatabase, relationalTables } from '../database.mjs';
|
||||||
@@ -13,7 +14,14 @@ import { buildCenterMaterialsWorkbook, buildWorkbook } from '../excel.mjs';
|
|||||||
|
|
||||||
const root = resolve(process.cwd());
|
const root = resolve(process.cwd());
|
||||||
assert.equal(totpAtStep('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', 1), '287082', 'TOTP 实现应符合 RFC 6238 SHA-1 测试向量的 6 位结果');
|
assert.equal(totpAtStep('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', 1), '287082', 'TOTP 实现应符合 RFC 6238 SHA-1 测试向量的 6 位结果');
|
||||||
const port = 4182;
|
const port = await new Promise((resolvePort, rejectPort) => {
|
||||||
|
const probe = createNetServer();
|
||||||
|
probe.once('error', rejectPort);
|
||||||
|
probe.listen(0, '127.0.0.1', () => {
|
||||||
|
const selectedPort = probe.address().port;
|
||||||
|
probe.close(error => error ? rejectPort(error) : resolvePort(selectedPort));
|
||||||
|
});
|
||||||
|
});
|
||||||
const baseUrl = `http://127.0.0.1:${port}`;
|
const baseUrl = `http://127.0.0.1:${port}`;
|
||||||
const testDb = resolve(root, 'data', 'test-db.sqlite');
|
const testDb = resolve(root, 'data', 'test-db.sqlite');
|
||||||
const mysqlRuleSchemas = mysqlSchema.filter(statement => /CREATE TABLE IF NOT EXISTS (?:admission_number_rules|number_rules)\b/.test(statement));
|
const mysqlRuleSchemas = mysqlSchema.filter(statement => /CREATE TABLE IF NOT EXISTS (?:admission_number_rules|number_rules)\b/.test(statement));
|
||||||
@@ -961,8 +969,11 @@ try {
|
|||||||
console.log('✓ 成绩录入、发布与考生查询');
|
console.log('✓ 成绩录入、发布与考生查询');
|
||||||
console.log('✓ 成绩复议、班级审批、范围匹配与多人均分');
|
console.log('✓ 成绩复议、班级审批、范围匹配与多人均分');
|
||||||
} finally {
|
} finally {
|
||||||
server.kill('SIGTERM');
|
if (server.exitCode == null && server.signalCode == null) {
|
||||||
await new Promise(resolveWait => server.once('exit', resolveWait));
|
const serverExit = new Promise(resolveWait => server.once('exit', resolveWait));
|
||||||
|
server.kill('SIGTERM');
|
||||||
|
await serverExit;
|
||||||
|
}
|
||||||
await rm(testDb, { force: true });
|
await rm(testDb, { force: true });
|
||||||
await rm(`${testDb}-shm`, { force: true });
|
await rm(`${testDb}-shm`, { force: true });
|
||||||
await rm(`${testDb}-wal`, { force: true });
|
await rm(`${testDb}-wal`, { force: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user