Add admission function #1

Merged
biss merged 5 commits from Add-Admission-Function into master 2026-07-21 15:11:56 +08:00
29 changed files with 1699 additions and 83 deletions

No files matched your search

+18
View File
@@ -256,6 +256,24 @@ npm run seed-test-data:mysql -- --force
强制模式会删除该 MySQL 数据库内现有应用数据并在同一事务中写入样例数据,但不会删除数据库或数据表。导入完成后再重新启动应用,避免导入期间出现并发写入或保留旧登录会话。不要对生产业务库执行此命令。本地需要明确使用 SQLite 时可运行 `npm run seed-test-data:sqlite`。 强制模式会删除该 MySQL 数据库内现有应用数据并在同一事务中写入样例数据,但不会删除数据库或数据表。导入完成后再重新启动应用,避免导入期间出现并发写入或保留旧登录会话。不要对生产业务库执行此命令。本地需要明确使用 SQLite 时可运行 `npm run seed-test-data:sqlite`。
## 中考志愿填报与招生录取
系统可按考试单独启用志愿填报,未启用的考试不会出现志愿入口。完整流程如下:
1. 超级管理员设置填报时间、普通志愿数、最多提交次数和当前阶段;考生只有在当次成绩全部发布后才能填报,达到提交上限后自动锁定。
2. 招生学校账号以结构化表单上传本校普通生、特长生与生源校指标分配计划,超级管理员审核后生效;超级管理员也可代上传并直接审核。
3. 生源校学校管理员按考试逐人确认指标分配资格;本校资料已完善的在册考生全部确认后,系统自动公开有无资格及对应特长类型。超级管理员和班级管理员均不能代确认。
4. 每名考生有一个专用指标分配志愿栏,只有确认有资格且招生校对本校分配了对应指标时可选;其余均为普通志愿。志愿只能由考生本人保存或修改,班级、校级管理员无权查看,超级管理员只读可见。
5. 超级管理员结束填报并执行投档。系统按总成绩降序逐个检索志愿,严格区分指标计划池与普通计划池,并遵循“分数优先、遵循志愿”。
6. 投档材料只发送到对应招生学校,包含必要考生资料与当次成绩,不包含考生其余志愿。学校可接收或填写特殊理由申请退档,退档由超级管理员统一审核。
7. 未完成计划可开启下一轮补录;已正式录取的考生不会被覆盖。录取结束后系统发送个人通知,并在独立“招生公示”页面自动发布脱敏录取名单及按学校、类别统计的录取分数线。
公开公示固定包含报名号、姓名、考生总成绩和录取学校;证件号、手机号等重要身份信息只提供脱敏值。考生档案中的特长资格按“体育 / 艺术”大类与对应小类登记,志愿页面先按学校代码选择招生校,再仅显示符合本人资格的该校类别。
学校统一在“学校管理”中维护,并可分别标记为生源校、招生校或同时具备两类职责。每场考试报名都包含独立于科目的 `feature_score`(特征分),默认 0,由超级管理员登记;招生学校可在录取结束后下载本校全部正式录取考生信息 Excel。
数据结构版本为 v20,`admission_records` 关系表新增指标资格、资格公示和分数线公告记录,并支持 SQLite / MySQL 自动迁移。新角色值为 `admission_school`。
## 手动测试数据账号 ## 手动测试数据账号
测试数据脚本会提供以下账号;其中初始超级管理员也可能由正常首次建库创建,并可通过环境变量改名、改密,其余校级、班级和考生账号不会在正常启动时创建: 测试数据脚本会提供以下账号;其中初始超级管理员也可能由正常首次建库创建,并可通过环境变量改名、改密,其余校级、班级和考生账号不会在正常启动时创建:
+192 -6
View File
@@ -1,10 +1,13 @@
import { api } from './src/client/api.mjs'; import { api } from './src/client/api.mjs';
import { createAdminViews, numberSegmentMeta } from './src/client/admin-views.mjs'; import { createAdminViews, numberSegmentMeta } from './src/client/admin-views.mjs';
import { createCandidateViews } from './src/client/candidate-views.mjs'; import { createCandidateViews } from './src/client/candidate-views.mjs';
import { createAdmissionViews } from './src/client/admission-views.mjs';
import { admissionCategoryEditor, indicatorAllocationEditor } from './src/client/admission-plan-editor.mjs';
import { createPublicViews } from './src/client/public-views.mjs'; import { createPublicViews } from './src/client/public-views.mjs';
import { state } from './src/client/state.mjs'; import { state } from './src/client/state.mjs';
import { badge, dateRange, formatDate, h, icons, money, passPolicyText, statusLabels } from './src/client/ui.mjs'; import { badge, dateRange, formatDate, h, icons, money, passPolicyText, statusLabels } from './src/client/ui.mjs';
import { formatRegionAddress, mountRegionSelects, updateRegionSelects } from './src/client/region-select.mjs'; import { formatRegionAddress, mountRegionSelects, updateRegionSelects } from './src/client/region-select.mjs';
import { specialtyCatalog } from './src/data/specialty-types.mjs';
const app = document.querySelector('#app'); const app = document.querySelector('#app');
const modalRoot = document.querySelector('#modalRoot'); const modalRoot = document.querySelector('#modalRoot');
@@ -56,9 +59,10 @@ function renderError(error) {
} }
const baseViewContext = { state, app, h, formatDate, dateRange, badge, money, passPolicyText, statusLabels, icons, api, renderError, emptyState }; const baseViewContext = { state, app, h, formatDate, dateRange, badge, money, passPolicyText, statusLabels, icons, api, renderError, emptyState };
const { brand, renderHome, renderAuth } = createPublicViews(baseViewContext); const { brand, renderHome, renderNoticeCenter, renderAuth } = createPublicViews(baseViewContext);
const { adminNavForUser, portalShell, loadingPanel, renderCandidate, accountSecurity } = createCandidateViews({ ...baseViewContext, brand }); const { adminNavForUser, portalShell, loadingPanel, renderCandidate, accountSecurity } = createCandidateViews({ ...baseViewContext, brand });
const { renderAdmin, workflowStepEditor } = createAdminViews({ ...baseViewContext, brand, portalShell, loadingPanel, adminNavForUser, accountSecurity }); const { renderAdmin, workflowStepEditor } = createAdminViews({ ...baseViewContext, brand, portalShell, loadingPanel, adminNavForUser, accountSecurity });
const { renderAdmission } = createAdmissionViews({ ...baseViewContext, brand });
function navigate(route) { function navigate(route) {
location.hash = route; location.hash = route;
@@ -71,9 +75,12 @@ async function renderRoute() {
const route = location.hash.slice(1) || 'home'; const route = location.hash.slice(1) || 'home';
const [section, page = 'dashboard'] = route.split('/'); const [section, page = 'dashboard'] = route.split('/');
if (section === 'home') renderHome(); if (section === 'home') renderHome();
else if (section === 'notices' || section === 'announcements') { state.publicAnnouncements = await api('/api/public/announcements'); renderNoticeCenter(state.publicAnnouncements); }
else if (section === 'notice') { state.publicAnnouncements = await api('/api/public/announcements'); renderNoticeCenter(state.publicAnnouncements, page); }
else if (section === 'login' || section === 'register') renderAuth(section); else if (section === 'login' || section === 'register') renderAuth(section);
else if (section === 'candidate') await renderCandidate(page); else if (section === 'candidate') await renderCandidate(page);
else if (section === 'admin') await renderAdmin(page); else if (section === 'admin') await renderAdmin(page);
else if (section === 'admission_school') await renderAdmission(page);
else navigate('home'); else navigate('home');
} }
@@ -97,6 +104,19 @@ function updateRegistrationSelection() {
document.querySelectorAll('[data-action="bulk-registration-review"]').forEach(button => { button.disabled = selected.length === 0; }); document.querySelectorAll('[data-action="bulk-registration-review"]').forEach(button => { button.disabled = selected.length === 0; });
} }
function updateQualificationSelection(container) {
if (!container) return;
const rows = [...container.querySelectorAll('[data-qualification-select]')];
const selected = rows.filter(input => input.checked);
const selectAll = container.querySelector('[data-action="qualification-select-all"]');
if (selectAll) {
selectAll.checked = rows.length > 0 && selected.length === rows.length;
selectAll.indeterminate = selected.length > 0 && selected.length < rows.length;
}
const count = container.querySelector('[data-qualification-selected-count]');
if (count) count.textContent = `已选 ${selected.length} 人`;
}
function applyTableFilters(tableId) { function applyTableFilters(tableId) {
const table = document.getElementById(tableId); const table = document.getElementById(tableId);
if (!table) return; if (!table) return;
@@ -176,11 +196,116 @@ document.addEventListener('click', async event => {
await refreshPublic(); navigate('home'); toast('已安全退出', '期待下次见面'); return; await refreshPublic(); navigate('home'); toast('已安全退出', '期待下次见面'); return;
} }
if (action === 'open-notice') { if (action === 'open-notice') {
const notice = state.publicData.notices.find(item => item.id === target.dataset.id) || (await api(`/api/public/notices/${target.dataset.id}`)).notice; return navigate(`notice/${target.dataset.id}`);
const contentHtml = notice.contentHtml || `<p>${h(notice.content).replace(/\r?\n/g, '</p><p>')}</p>`; }
setModal(`<div class="modal-head"><div><span>${h(notice.category)}</span><h2>${h(notice.title)}</h2><p>${formatDate(notice.publishAt,true)} · ${h(notice.author)}</p></div><button data-action="close-modal">×</button></div><article class="notice-content">${contentHtml}</article><div class="modal-foot"><button class="ghost-button" data-action="close-modal">关闭</button></div>`); return; if (action === 'notice-category') {
state.noticeCategory = target.dataset.category;
state.noticePage = 1;
renderNoticeCenter(state.publicAnnouncements);
return;
}
if (action === 'notice-page') {
if (target.disabled) return;
state.noticePage = Number(target.dataset.page || 1);
renderNoticeCenter(state.publicAnnouncements);
window.scrollTo({ top: 0, behavior: 'smooth' });
return;
} }
if (action === 'download-admit') { window.location.href = `/api/candidate/registrations/${target.dataset.id}/admit-card`; return; } if (action === 'download-admit') { window.location.href = `/api/candidate/registrations/${target.dataset.id}/admit-card`; return; }
if (action === 'download-admitted-candidates') {
const examId = target.closest('.admission-export-bar')?.querySelector('[name="exportExamId"]')?.value;
if (!examId) return toast('请选择考试', '仅录取工作结束的考试可以下载');
window.location.href = `/api/admission/placements/export?examId=${encodeURIComponent(examId)}`;
return;
}
if (action === 'admission-plan-review') {
const reviewNote = window.prompt(target.dataset.status === 'approved' ? '填写审核意见(可留空)' : '请填写退回原因', '') ?? null;
if (reviewNote == null) return;
await api(`/api/admin/admission-plans/${target.dataset.id}`, { method: 'PATCH', body: { status: target.dataset.status, reviewNote } });
toast(target.dataset.status === 'approved' ? '招生计划已通过' : '招生计划已退回'); return renderRoute();
}
if (action === 'admission-match') {
if (!window.confirm('确认按“分数优先、遵循志愿”执行本轮投档?填报顺序将锁定。')) return;
const data = await api(`/api/admin/admissions/${target.dataset.examId}/match`, { method: 'POST' });
toast('投档完成', `${data.placementCount} 名考生已发送招生学校`); return renderRoute();
}
if (action === 'admission-finalize') {
if (!window.confirm('确认结束本次录取?系统会向考生发送通知,并按设置自动公示。')) return;
const data = await api(`/api/admin/admissions/${target.dataset.examId}/finalize`, { method: 'POST' });
await refreshPublic(); toast('录取工作已结束', `${data.admittedCount} 人正式录取`); return renderRoute();
}
if (action === 'admission-supplementary') {
const preferenceEnd = window.prompt('请输入补录志愿截止时间(例如 2026-07-25T18:00)', '');
if (!preferenceEnd) return;
await api(`/api/admin/admissions/${target.dataset.examId}/supplementary`, { method: 'POST', body: { preferenceEnd } });
toast('补录已开启', '未录取考生可以填报新一轮志愿'); return renderRoute();
}
if (action === 'withdrawal-review') {
const approved = target.dataset.approved === 'true';
const reviewNote = window.prompt(approved ? '填写批准退档意见' : '填写驳回退档意见', '');
if (reviewNote == null) return;
await api(`/api/admin/admission-withdrawals/${target.dataset.id}`, { method: 'PATCH', body: { approved, reviewNote } });
toast(approved ? '退档已批准' : '退档申请已驳回'); return renderRoute();
}
if (action === 'save-indicator-qualification') {
const row = target.closest('tr');
const value = row?.querySelector('[data-indicator-eligible]')?.value;
if (!value) return toast('请选择资格结论', '必须明确选择有资格或无资格');
const data = await api(`/api/admin/indicator-qualifications/${row.dataset.examId}/${row.dataset.userId}`, { method: 'PUT', body: { eligible: value === 'true' } });
toast(data.published ? '资格已确认并自动公示' : '资格已确认', data.published ? '本校全部考生已确认完成' : '继续核对其他考生');
return renderRoute();
}
if (action === 'qualification-select-all') {
const container = target.closest('[data-qualification-bulk]')?.closest('.qualification-ledger');
container?.querySelectorAll('[data-qualification-select]').forEach(input => { input.checked = target.checked; });
updateQualificationSelection(container);
return;
}
if (action === 'bulk-indicator-qualification') {
const toolbar = target.closest('[data-qualification-bulk]');
const ledger = toolbar?.closest('.qualification-ledger');
const userIds = [...ledger.querySelectorAll('[data-qualification-select]:checked')].map(input => input.closest('tr').dataset.userId);
const value = toolbar.querySelector('[data-bulk-eligible]').value;
if (!userIds.length) return toast('请先选择考生', '可使用左侧复选框或全选');
if (!value) return toast('请选择批量资格结论');
if (!window.confirm(`确认将所选 ${userIds.length} 名考生批量设为“${value === 'true' ? '有' : '无'}指标分配资格”?`)) return;
const data = await api(`/api/admin/indicator-qualifications/${toolbar.dataset.examId}/bulk`, { method: 'PUT', body: { userIds, eligible: value === 'true' } });
toast(data.published ? '批量确认完成并自动公示' : '批量确认完成', `已更新 ${data.count} 名考生`);
return renderRoute();
}
if (action === 'toggle-admin-account') {
const active = target.dataset.active === 'true';
if (!active && !window.confirm('确认停用该管理员账户?其现有登录会话会立即失效,历史审批记录将保留。')) return;
await api(`/api/admin/admins/${target.dataset.id}`, { method: 'PATCH', body: { active } });
toast(active ? '管理员账户已启用' : '管理员账户已停用', active ? '该账号可以重新登录' : '现有会话已结束,历史记录仍保留');
return renderRoute();
}
if (action === 'reset-admin-password') {
if (!window.confirm('确认重置该管理员密码?其现有登录会话会立即失效。')) return;
const data = await api(`/api/admin/admins/${target.dataset.id}/reset-password`, { method: 'POST' });
setModal(`<div class="modal-head"><div><span>ADMIN PASSWORD RESET</span><h2>管理员临时密码已生成</h2><p>请通过线下安全渠道交给管理员,本页面关闭后不再展示。</p></div><button data-action="close-modal">×</button></div><div class="issued-number"><span>登录账号</span><strong>${h(data.username)}</strong><span>临时密码</span><strong>${h(data.temporaryPassword)}</strong><p>旧密码和现有登录会话均已失效。</p></div><div class="modal-foot"><button class="solid-button" data-action="close-modal">我已保存</button></div>`);
return;
}
if (action === 'add-admission-category') {
const sources = state.pageData?.sourceSchools || [];
target.closest('form')?.querySelector('[data-admission-categories]')?.insertAdjacentHTML('beforeend', admissionCategoryEditor(h, sources));
return;
}
if (action === 'remove-admission-category') {
const list = target.closest('[data-admission-categories]');
if (list?.children.length <= 1) return toast('至少保留一个招生类别');
target.closest('.admission-category-editor')?.remove();
return;
}
if (action === 'add-indicator-allocation') {
const sources = state.pageData?.sourceSchools || [];
target.closest('.indicator-allocation-editor')?.querySelector('[data-indicator-allocations]')?.insertAdjacentHTML('beforeend', indicatorAllocationEditor(h, sources));
return;
}
if (action === 'remove-indicator-allocation') {
target.closest('.indicator-allocation-row')?.remove();
return;
}
if (['batch-admit-download', 'admit-info-export', 'center-materials-export'].includes(action)) { if (['batch-admit-download', 'admit-info-export', 'center-materials-export'].includes(action)) {
event.preventDefault(); event.preventDefault();
const examId = target.closest('.admission-export-panel')?.querySelector('[data-admission-export-exam]')?.value; const examId = target.closest('.admission-export-panel')?.querySelector('[data-admission-export-exam]')?.value;
@@ -382,6 +507,7 @@ document.addEventListener('input', event => {
}); });
document.addEventListener('change', event => { document.addEventListener('change', event => {
if (event.target.matches('[data-qualification-select]')) updateQualificationSelection(event.target.closest('.qualification-ledger'));
if (event.target.matches('[data-table-filter]')) applyTableFilters(event.target.dataset.target); if (event.target.matches('[data-table-filter]')) applyTableFilters(event.target.dataset.target);
if (event.target.matches('[data-registration-select]')) updateRegistrationSelection(); if (event.target.matches('[data-registration-select]')) updateRegistrationSelection();
if (event.target.matches('[data-registration-select-all]')) { if (event.target.matches('[data-registration-select-all]')) {
@@ -426,6 +552,36 @@ document.addEventListener('change', event => {
classSelect.innerHTML = `<option value="">请选择班级</option>${classes.filter(item => item.schoolId === event.target.value).map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}`; classSelect.innerHTML = `<option value="">请选择班级</option>${classes.filter(item => item.schoolId === event.target.value).map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}`;
} }
} }
if (event.target.matches('[data-action="specialty-category"]')) {
const typeSelect = event.target.closest('.admission-category-editor')?.querySelector('[data-specialty-type]') || event.target.closest('form')?.querySelector('[data-specialty-type]');
const category = specialtyCatalog.find(item => item.code === event.target.value);
if (typeSelect) {
typeSelect.disabled = !category;
typeSelect.innerHTML = `<option value="">${category ? '请选择小类' : '请先选择大类'}</option>${(category?.types || []).map(item => `<option value="${h(item.code)}">${h(item.name)}</option>`).join('')}`;
}
}
if (event.target.matches('[data-action="plan-category-kind"]')) {
const editor = event.target.closest('.admission-category-editor');
const specialtyFields = editor?.querySelector('[data-plan-specialty]');
const enabled = event.target.value === 'specialty';
specialtyFields?.classList.toggle('hidden', !enabled);
specialtyFields?.querySelectorAll('select').forEach(select => { select.disabled = !enabled || (select.hasAttribute('data-specialty-type') && !specialtyFields.querySelector('[name="categorySpecialtyCategory"]')?.value); });
}
if (event.target.matches('[name="categoryName"]')) {
const title = event.target.closest('.admission-category-editor')?.querySelector('header strong');
if (title) title.textContent = event.target.value.trim() || '新类别';
}
if (event.target.matches('[data-action="preference-school"]')) {
const row = event.target.closest('.preference-choice-row');
const categorySelect = row?.querySelector('[name="choiceCategory"]');
const admission = state.pageData?.admissions?.find(item => item.examId === event.target.dataset.examId);
const plan = admission?.plans?.find(item => item.schoolId === event.target.value);
const preferenceType = row?.dataset.preferenceType || 'general';
if (categorySelect) {
categorySelect.disabled = !plan;
categorySelect.innerHTML = `<option value="">${plan ? '请选择招生类别' : '请先按代码选择学校'}</option>${(plan?.categories || []).filter(item => item.preferenceTypes?.includes(preferenceType) && Number(preferenceType === 'indicator' ? item.indicatorRemaining : item.generalRemaining) > 0).map(item => `<option value="${h(item.code)}">${h(item.name)} · 对应余 ${h(preferenceType === 'indicator' ? item.indicatorRemaining : item.generalRemaining)}</option>`).join('')}`;
}
}
if (event.target.matches('[data-region-level]')) updateRegionSelects(event.target); if (event.target.matches('[data-region-level]')) updateRegionSelects(event.target);
if (event.target.matches('[data-action="admin-level"]')) { if (event.target.matches('[data-action="admin-level"]')) {
const form = event.target.closest('form'); const form = event.target.closest('form');
@@ -527,6 +683,10 @@ document.addEventListener('submit', async event => {
} else if (kind === 'candidate-profile') { } else if (kind === 'candidate-profile') {
const data = await api('/api/candidate/profile', { method: 'PUT', body: formObject(form) }); const data = await api('/api/candidate/profile', { method: 'PUT', body: formObject(form) });
state.profile = data.profile; await refreshSession(); toast('资料已提交', '管理员审核后会更新状态'); navigate('candidate/dashboard'); state.profile = data.profile; await refreshSession(); toast('资料已提交', '管理员审核后会更新状态'); navigate('candidate/dashboard');
} else if (kind === 'volunteer-preference') {
const choices = [...form.querySelectorAll('.preference-choice-row')].map(row => ({ schoolId: row.querySelector('[name="choiceSchool"]').value, categoryCode: row.querySelector('[name="choiceCategory"]').value, preferenceType: row.dataset.preferenceType || 'general' })).filter(item => item.schoolId && item.categoryCode);
const data = await api(`/api/candidate/admissions/${form.examId.value}/preferences`, { method: 'PUT', body: { choices } });
toast(data.locked ? '志愿已保存并锁定' : '志愿已保存', data.locked ? '已达到本轮提交次数上限' : `还可提交 ${data.remainingSubmissions} 次`); renderRoute();
} else if (kind === 'exam-registration') { } else if (kind === 'exam-registration') {
const body = { examId: form.examId.value, subjectIds: [...form.querySelectorAll('input[name="subjectIds"]:checked')].map(input => input.value) }; const body = { examId: form.examId.value, subjectIds: [...form.querySelectorAll('input[name="subjectIds"]:checked')].map(input => input.value) };
if (!body.subjectIds.length) throw new Error('请至少选择一个报考科目'); if (!body.subjectIds.length) throw new Error('请至少选择一个报考科目');
@@ -548,8 +708,30 @@ document.addEventListener('submit', async event => {
const body = formObject(form); const body = formObject(form);
await api('/api/admin/admins', { method: 'POST', body }); await api('/api/admin/admins', { method: 'POST', body });
closeModal(); toast('管理员已创建', '权限范围已按层级绑定'); renderRoute(); closeModal(); toast('管理员已创建', '权限范围已按层级绑定'); renderRoute();
} else if (kind === 'admission-setting') {
const body = formObject(form); body.enabled = form.enabled.checked; body.autoPublish = form.autoPublish.checked; body.maxChoices = Number(body.maxChoices || 5); body.maxSubmissions = Number(body.maxSubmissions || 3);
await api(`/api/admin/admissions/${body.examId}/setting`, { method: 'PUT', body });
toast('志愿设置已保存', '考生端阶段与进度已同步'); renderRoute();
} else if (kind === 'admission-account') {
await api('/api/admin/admission-school-accounts', { method: 'POST', body: formObject(form) });
form.reset(); toast('招生学校账号已创建'); renderRoute();
} else if (kind === 'admission-plan' || kind === 'school-admission-plan') {
const body = formObject(form);
body.categories = [...form.querySelectorAll('.admission-category-editor')].map((editor, index) => {
const specialty = editor.querySelector('[name="categoryKind"]').value === 'specialty';
const indicatorAllocations = [...editor.querySelectorAll('.indicator-allocation-row')].map(row => ({ sourceSchoolId: row.querySelector('[name="indicatorSchool"]').value, quota: Number(row.querySelector('[name="indicatorQuota"]').value || 0) })).filter(item => item.sourceSchoolId && item.quota > 0);
return { code: `category_${index + 1}`, name: editor.querySelector('[name="categoryName"]').value.trim(), quota: Number(editor.querySelector('[name="categoryQuota"]').value || 0), isSpecialty: specialty, specialtyCategory: specialty ? editor.querySelector('[name="categorySpecialtyCategory"]').value : '', specialtyType: specialty ? editor.querySelector('[name="categorySpecialtyType"]').value : '', indicatorAllocations };
}).filter(item => item.name && item.quota > 0);
if (!body.categories.length) throw new Error('请至少添加一个有效招生类别');
if (body.categories.some(item => item.isSpecialty && (!item.specialtyCategory || !item.specialtyType))) throw new Error('特长生类别必须同时选择对应的大类和小类');
await api(kind === 'admission-plan' ? '/api/admin/admission-plans' : '/api/admission/plans', { method: 'POST', body });
toast(kind === 'admission-plan' ? '招生计划已代上传并通过' : '招生计划已提交审核'); renderRoute();
} else if (kind === 'placement-review') {
const body = formObject(form);
await api(`/api/admission/placements/${body.id}`, { method: 'PATCH', body });
toast(body.decision === 'accept' ? '已接收投档考生' : '退档申请已提交超级管理员'); renderRoute();
} else if (kind === 'school-form') { } else if (kind === 'school-form') {
const body = formObject(form); body.active = form.active.checked; const body = formObject(form); body.active = form.active.checked; body.isSourceSchool = form.isSourceSchool.checked; body.isAdmissionSchool = form.isAdmissionSchool.checked;
await api(body.id ? `/api/admin/schools/${body.id}` : '/api/admin/schools', { method: body.id ? 'PATCH' : 'POST', body }); await api(body.id ? `/api/admin/schools/${body.id}` : '/api/admin/schools', { method: body.id ? 'PATCH' : 'POST', body });
closeModal(); await refreshPublic(); toast(body.id ? '学校档案已更新' : '学校已创建', `${body.name} · ${body.code.toUpperCase()}`); renderRoute(); closeModal(); await refreshPublic(); toast(body.id ? '学校档案已更新' : '学校已创建', `${body.name} · ${body.code.toUpperCase()}`); renderRoute();
} else if (kind === 'school-class') { } else if (kind === 'school-class') {
@@ -654,6 +836,10 @@ document.addEventListener('submit', async event => {
const body = formObject(form); body.published = form.published.checked; const body = formObject(form); body.published = form.published.checked;
await api('/api/admin/results', { method: 'POST', body }); await api('/api/admin/results', { method: 'POST', body });
toast(body.published ? '成绩已发布' : '成绩已保存', '考生端可见状态已更新'); renderRoute(); toast(body.published ? '成绩已发布' : '成绩已保存', '考生端可见状态已更新'); renderRoute();
} else if (kind === 'feature-score-entry') {
const body = formObject(form); body.featureScore = Number(body.featureScore || 0);
await api(`/api/admin/registrations/${body.registrationId}/feature-score`, { method: 'PATCH', body });
toast('特征分已登记', '该分数独立于考试科目,默认值为 0'); renderRoute();
} else if (kind === 'result-import-commit') { } else if (kind === 'result-import-commit') {
const rows = state.resultImportPreview?.rows || []; const rows = state.resultImportPreview?.rows || [];
if (!rows.length) throw new Error('没有可提交的成绩预览'); if (!rows.length) throw new Error('没有可提交的成绩预览');
@@ -671,7 +857,7 @@ function openAdminForm() {
} }
function openSchoolForm(school = null) { function openSchoolForm(school = null) {
setModal(`<div class="modal-head"><div><span>SCHOOL DIRECTORY</span><h2>${school ? '编辑学校档案' : '创建学校'}</h2><p>学校代码用于管理员范围、报名号规则和数据识别,建议使用稳定且唯一的简称。</p></div><button data-action="close-modal">×</button></div><form class="modal-form" data-form="school-form">${school ? `<input type="hidden" name="id" value="${h(school.id)}">` : ''}<div class="field-row"><label><span>学校名称 *</span><input name="name" required maxlength="100" value="${h(school?.name || '')}" placeholder="例如 海州市第四中学"></label><label><span>学校代码 *</span><input name="code" required maxlength="40" pattern="[A-Za-z0-9_-]+" value="${h(school?.code || '')}" placeholder="例如 HZ04"></label></div><label><span>学校地址</span><input name="address" maxlength="200" value="${h(school?.address || '')}" placeholder="省、市、区县及详细地址"></label><label class="agreement"><input type="checkbox" name="active" ${school?.active === false ? '' : 'checked'}><span>启用该学校,可在公开入口选择并继续绑定管理员、班级和考点</span></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">${school ? '保存学校' : '创建学校'}</button></div></form>`); setModal(`<div class="modal-head"><div><span>SCHOOL DIRECTORY</span><h2>${school ? '编辑学校档案' : '创建学校'}</h2><p>学校代码统一标识学校;同一学校可以同时承担生源校和招生校职责。</p></div><button data-action="close-modal">×</button></div><form class="modal-form" data-form="school-form">${school ? `<input type="hidden" name="id" value="${h(school.id)}">` : ''}<div class="field-row"><label><span>学校名称 *</span><input name="name" required maxlength="100" value="${h(school?.name || '')}" placeholder="例如 海州市第四中学"></label><label><span>学校代码 *</span><input name="code" required maxlength="40" pattern="[A-Za-z0-9_-]+" value="${h(school?.code || '')}" placeholder="例如 HZ04"></label></div><label><span>学校地址</span><input name="address" maxlength="200" value="${h(school?.address || '')}" placeholder="省、市、区县及详细地址"></label><section class="school-role-selector"><strong>学校类型(至少选择一项)</strong><div><label><input type="checkbox" name="isSourceSchool" ${school?.isSourceSchool === false ? '' : 'checked'}><span><b>生源校</b><small>可建立班级、管理考生,并接收指标名额</small></span></label><label><input type="checkbox" name="isAdmissionSchool" ${school?.isAdmissionSchool === false ? '' : 'checked'}><span><b>招生校</b><small>可配置招生账号、上传计划并审核投档</small></span></label></div></section><label class="agreement"><input type="checkbox" name="active" ${school?.active === false ? '' : 'checked'}><span>启用该学校;公开报名入口只显示已启用的生源校</span></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">${school ? '保存学校' : '创建学校'}</button></div></form>`);
} }
function openSchoolClassForm(schoolClass = null) { function openSchoolClassForm(schoolClass = null) {
+105 -17
View File
@@ -35,6 +35,7 @@ export const relationalTables = [
'workflow_steps', 'workflow_steps',
'workflow_instances', 'workflow_instances',
'workflow_actions', 'workflow_actions',
'admission_records',
'audit_logs' 'audit_logs'
]; ];
@@ -44,7 +45,7 @@ function validateState(state, source = '数据库') {
'testCenters', 'testRooms', 'centerChangeRequests', 'centerChangeRooms', 'testCenters', 'testRooms', 'centerChangeRequests', 'centerChangeRooms',
'admissionNumberRules', 'arrangementPlans', 'admissionNumberRules', 'arrangementPlans',
'numberRules', 'candidateAccountBatches', 'candidateAccountBatchItems', 'numberRules', 'candidateAccountBatches', 'candidateAccountBatchItems',
'workflows', 'workflowInstances', 'workflowActions', 'auditLogs' 'workflows', 'workflowInstances', 'workflowActions', 'admissionRecords', 'auditLogs'
]; ];
if (!state || typeof state !== 'object' || collections.some(name => !Array.isArray(state[name]))) { if (!state || typeof state !== 'object' || collections.some(name => !Array.isArray(state[name]))) {
throw new Error(`${source}中的应用数据格式无效`); throw new Error(`${source}中的应用数据格式无效`);
@@ -59,7 +60,7 @@ export function buildSeedOperations(state) {
const nullable = value => value == null || value === '' ? null : value; const nullable = value => value == null || value === '' ? null : value;
add( add(
'UPDATE schema_metadata SET schema_version = 17, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1', 'UPDATE schema_metadata SET schema_version = 20, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
Number(state.meta?.version || 1), state.settings?.selfRegistrationEnabled ? 1 : 0, Number(state.meta?.version || 1), state.settings?.selfRegistrationEnabled ? 1 : 0,
state.meta?.createdAt || new Date().toISOString() state.meta?.createdAt || new Date().toISOString()
); );
@@ -70,8 +71,9 @@ export function buildSeedOperations(state) {
for (const school of state.schools) { for (const school of state.schools) {
add( add(
'INSERT INTO schools (id, name, code, address, active) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO schools (id, name, code, address, is_source_school, is_admission_school, active) VALUES (?, ?, ?, ?, ?, ?, ?)',
school.id, school.name, school.code, nullable(school.address), school.active === false ? 0 : 1 school.id, school.name, school.code, nullable(school.address), school.isSourceSchool === false ? 0 : 1,
school.isAdmissionSchool === false ? 0 : 1, school.active === false ? 0 : 1
); );
} }
@@ -100,8 +102,9 @@ export function buildSeedOperations(state) {
id, user_id, name, gender, id_number, phone, email, school, grade, school_id, class_id, id, user_id, name, gender, id_number, phone, email, school, grade, school_id, class_id,
province_code, province_name, city_code, city_name, district_code, district_name, address, province_code, province_name, city_code, city_name, district_code, district_name, address,
emergency_contact, emergency_phone, native_place, birth_date, ethnicity, postal_code, guardian_name, emergency_contact, emergency_phone, native_place, birth_date, ethnicity, postal_code, guardian_name,
guardian_phone, profile_completed, status, review_note, reviewed_at, reviewer_id, updated_at guardian_phone, specialty_category, specialty_type, specialty_types, specialty_certificate, policy_eligibility,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, profile_completed, status, review_note, reviewed_at, reviewer_id, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
profile.id, profile.userId, profile.name, nullable(profile.gender), profile.idNumber, profile.phone, profile.id, profile.userId, profile.name, nullable(profile.gender), profile.idNumber, profile.phone,
nullable(profile.email), nullable(profile.school), nullable(profile.grade), nullable(profile.schoolId), nullable(profile.email), nullable(profile.school), nullable(profile.grade), nullable(profile.schoolId),
nullable(profile.classId), nullable(profile.provinceCode), nullable(profile.provinceName), nullable(profile.classId), nullable(profile.provinceCode), nullable(profile.provinceName),
@@ -109,6 +112,8 @@ export function buildSeedOperations(state) {
nullable(profile.address), nullable(profile.address),
nullable(profile.emergencyContact), nullable(profile.emergencyPhone), nullable(profile.nativePlace), nullable(profile.birthDate), nullable(profile.emergencyContact), nullable(profile.emergencyPhone), nullable(profile.nativePlace), nullable(profile.birthDate),
nullable(profile.ethnicity), nullable(profile.postalCode), nullable(profile.guardianName), nullable(profile.guardianPhone), nullable(profile.ethnicity), nullable(profile.postalCode), nullable(profile.guardianName), nullable(profile.guardianPhone),
nullable(profile.specialtyCategory), nullable(profile.specialtyType), JSON.stringify(profile.specialtyTypes || []),
nullable(profile.specialtyCertificate), nullable(profile.policyEligibility),
profile.profileCompleted ? 1 : 0, profile.status, nullable(profile.reviewNote), profile.profileCompleted ? 1 : 0, profile.status, nullable(profile.reviewNote),
nullable(profile.reviewedAt), nullable(profile.reviewerId), profile.updatedAt nullable(profile.reviewedAt), nullable(profile.reviewerId), profile.updatedAt
); );
@@ -150,12 +155,12 @@ export function buildSeedOperations(state) {
add( add(
`INSERT INTO registrations ( `INSERT INTO registrations (
id, user_id, exam_id, status, payment_status, paid_at, paid_by, created_at, reviewed_at, review_note, id, user_id, exam_id, status, payment_status, paid_at, paid_by, created_at, reviewed_at, review_note,
registration_number, number_rule_id registration_number, number_rule_id, feature_score
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
registration.id, registration.userId, registration.examId, registration.status, registration.id, registration.userId, registration.examId, registration.status,
registration.paymentStatus, nullable(registration.paidAt), nullable(registration.paidBy), registration.createdAt, registration.paymentStatus, nullable(registration.paidAt), nullable(registration.paidBy), registration.createdAt,
nullable(registration.reviewedAt), nullable(registration.reviewNote), nullable(registration.reviewedAt), nullable(registration.reviewNote),
nullable(registration.registrationNumber), nullable(registration.numberRuleId) nullable(registration.registrationNumber), nullable(registration.numberRuleId), Number(registration.featureScore || 0)
); );
for (const subjectId of registration.subjectIds) { for (const subjectId of registration.subjectIds) {
add('INSERT INTO registration_subjects (registration_id, subject_id) VALUES (?, ?)', registration.id, subjectId); add('INSERT INTO registration_subjects (registration_id, subject_id) VALUES (?, ?)', registration.id, subjectId);
@@ -335,6 +340,16 @@ export function buildSeedOperations(state) {
); );
} }
for (const record of state.admissionRecords) {
add(
`INSERT INTO admission_records (
id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
record.id, record.kind, record.examId, nullable(record.userId), nullable(record.schoolId), record.status,
JSON.stringify(record.payload || {}), record.createdAt, record.updatedAt
);
}
for (const log of state.auditLogs) { for (const log of state.auditLogs) {
add( add(
'INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO audit_logs (id, actor_id, action, detail, created_at) VALUES (?, ?, ?, ?, ?)',
@@ -346,6 +361,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);
@@ -451,6 +470,8 @@ function stateFromRows(rows) {
name: row.name, name: row.name,
code: row.code, code: row.code,
address: row.address || '', address: row.address || '',
isSourceSchool: row.is_source_school == null ? true : Boolean(row.is_source_school),
isAdmissionSchool: row.is_admission_school == null ? true : Boolean(row.is_admission_school),
active: Boolean(row.active) active: Boolean(row.active)
})), })),
classes: rows.classes.map(row => ({ classes: rows.classes.map(row => ({
@@ -507,6 +528,11 @@ 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 || '',
specialtyCategory: row.specialty_category || '',
specialtyType: row.specialty_type || '',
specialtyTypes: parseJson(row.specialty_types, []),
specialtyCertificate: row.specialty_certificate || '',
policyEligibility: row.policy_eligibility || '',
profileCompleted: Boolean(row.profile_completed), profileCompleted: Boolean(row.profile_completed),
status: row.status, status: row.status,
reviewNote: row.review_note || '', reviewNote: row.review_note || '',
@@ -560,6 +586,7 @@ function stateFromRows(rows) {
reviewNote: row.review_note || '', reviewNote: row.review_note || '',
registrationNumber: row.registration_number || '', registrationNumber: row.registration_number || '',
numberRuleId: row.number_rule_id || null, numberRuleId: row.number_rule_id || null,
featureScore: Number(row.feature_score || 0),
admitCard: admitCards.get(row.id) || null admitCard: admitCards.get(row.id) || null
})), })),
results: rows.results.map(row => ({ results: rows.results.map(row => ({
@@ -738,6 +765,17 @@ function stateFromRows(rows) {
toAssigneeId: row.to_assignee_id, toAssigneeId: row.to_assignee_id,
createdAt: row.created_at createdAt: row.created_at
})), })),
admissionRecords: rows.admissionRecords.map(row => ({
id: row.id,
kind: row.kind,
examId: row.exam_id,
userId: row.user_id || null,
schoolId: row.school_id || null,
status: row.status,
payload: parseJson(row.payload_json, {}),
createdAt: row.created_at,
updatedAt: row.updated_at
})),
auditLogs: rows.auditLogs.map(row => ({ auditLogs: rows.auditLogs.map(row => ({
id: row.id, id: row.id,
actorId: row.actor_id, actorId: row.actor_id,
@@ -779,6 +817,7 @@ function readSqliteRows(connection) {
workflowSteps: connection.prepare('SELECT * FROM workflow_steps ORDER BY workflow_id, position, id').all(), workflowSteps: connection.prepare('SELECT * FROM workflow_steps ORDER BY workflow_id, position, id').all(),
workflowInstances: connection.prepare('SELECT * FROM workflow_instances ORDER BY created_at DESC, id').all(), workflowInstances: connection.prepare('SELECT * FROM workflow_instances ORDER BY created_at DESC, id').all(),
workflowActions: connection.prepare('SELECT * FROM workflow_actions ORDER BY created_at, id').all(), workflowActions: connection.prepare('SELECT * FROM workflow_actions ORDER BY created_at, id').all(),
admissionRecords: connection.prepare('SELECT * FROM admission_records ORDER BY created_at, id').all(),
auditLogs: connection.prepare('SELECT * FROM audit_logs ORDER BY created_at DESC, id DESC').all() auditLogs: connection.prepare('SELECT * FROM audit_logs ORDER BY created_at DESC, id DESC').all()
}; };
} }
@@ -815,6 +854,7 @@ async function readMysqlRows(connection) {
workflowSteps: await query('SELECT * FROM workflow_steps ORDER BY workflow_id, position, id'), workflowSteps: await query('SELECT * FROM workflow_steps ORDER BY workflow_id, position, id'),
workflowInstances: await query('SELECT * FROM workflow_instances ORDER BY created_at DESC, id'), workflowInstances: await query('SELECT * FROM workflow_instances ORDER BY created_at DESC, id'),
workflowActions: await query('SELECT * FROM workflow_actions ORDER BY created_at, id'), workflowActions: await query('SELECT * FROM workflow_actions ORDER BY created_at, id'),
admissionRecords: await query('SELECT * FROM admission_records ORDER BY created_at, id'),
auditLogs: await query('SELECT * FROM audit_logs ORDER BY created_at DESC, id DESC') auditLogs: await query('SELECT * FROM audit_logs ORDER BY created_at DESC, id DESC')
}; };
} }
@@ -925,6 +965,7 @@ function createRepository({ client, location, read, transaction, close }) {
province_code = ?, province_name = ?, city_code = ?, city_name = ?, district_code = ?, district_name = ?, address = ?, province_code = ?, province_name = ?, city_code = ?, city_name = ?, district_code = ?, district_name = ?, address = ?,
school_id = ?, class_id = ?, emergency_contact = ?, emergency_phone = ?, status = ?, review_note = ?, school_id = ?, class_id = ?, emergency_contact = ?, emergency_phone = ?, status = ?, review_note = ?,
native_place = ?, birth_date = ?, ethnicity = ?, postal_code = ?, guardian_name = ?, guardian_phone = ?, native_place = ?, birth_date = ?, ethnicity = ?, postal_code = ?, guardian_name = ?, guardian_phone = ?,
specialty_category = ?, specialty_type = ?, specialty_types = ?, specialty_certificate = ?, policy_eligibility = ?,
profile_completed = ?, reviewed_at = ?, reviewer_id = ?, updated_at = ? profile_completed = ?, reviewed_at = ?, reviewer_id = ?, updated_at = ?
WHERE id = ?`, WHERE id = ?`,
profile.name, optional(profile.gender), profile.idNumber, profile.phone, optional(profile.email), profile.name, optional(profile.gender), profile.idNumber, profile.phone, optional(profile.email),
@@ -933,7 +974,9 @@ function createRepository({ client, location, read, transaction, close }) {
optional(profile.address), optional(profile.schoolId), optional(profile.address), optional(profile.schoolId),
optional(profile.classId), optional(profile.emergencyContact), optional(profile.emergencyPhone), profile.status, optional(profile.classId), optional(profile.emergencyContact), optional(profile.emergencyPhone), profile.status,
optional(profile.reviewNote), optional(profile.nativePlace), optional(profile.birthDate), optional(profile.ethnicity), optional(profile.reviewNote), optional(profile.nativePlace), optional(profile.birthDate), optional(profile.ethnicity),
optional(profile.postalCode), optional(profile.guardianName), optional(profile.guardianPhone), profile.profileCompleted ? 1 : 0, optional(profile.reviewedAt), optional(profile.postalCode), optional(profile.guardianName), optional(profile.guardianPhone), optional(profile.specialtyCategory),
optional(profile.specialtyType), JSON.stringify(profile.specialtyTypes || []),
optional(profile.specialtyCertificate), optional(profile.policyEligibility), profile.profileCompleted ? 1 : 0, optional(profile.reviewedAt),
optional(profile.reviewerId), profile.updatedAt, profile.id optional(profile.reviewerId), profile.updatedAt, profile.id
), ),
operation('UPDATE users SET display_name = ? WHERE id = ?', displayName, profile.userId) operation('UPDATE users SET display_name = ? WHERE id = ?', displayName, profile.userId)
@@ -978,12 +1021,12 @@ function createRepository({ client, location, read, transaction, close }) {
const operations = [operation( const operations = [operation(
`INSERT INTO registrations ( `INSERT INTO registrations (
id, user_id, exam_id, status, payment_status, paid_at, paid_by, created_at, reviewed_at, review_note, id, user_id, exam_id, status, payment_status, paid_at, paid_by, created_at, reviewed_at, review_note,
registration_number, number_rule_id registration_number, number_rule_id, feature_score
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
registration.id, registration.userId, registration.examId, registration.status, registration.id, registration.userId, registration.examId, registration.status,
registration.paymentStatus, optional(registration.paidAt), optional(registration.paidBy), registration.createdAt, registration.paymentStatus, optional(registration.paidAt), optional(registration.paidBy), registration.createdAt,
optional(registration.reviewedAt), optional(registration.reviewNote), optional(registration.reviewedAt), optional(registration.reviewNote),
optional(registration.registrationNumber), optional(registration.numberRuleId) optional(registration.registrationNumber), optional(registration.numberRuleId), Number(registration.featureScore || 0)
)]; )];
for (const subjectId of registration.subjectIds) { for (const subjectId of registration.subjectIds) {
operations.push(operation( operations.push(operation(
@@ -1186,15 +1229,60 @@ function createRepository({ client, location, read, transaction, close }) {
auditOperation(log) auditOperation(log)
]); ]);
}, },
async createAdmissionSchoolAccount(user, log) {
await transaction([
operation(
`INSERT INTO users (
id, username, password_hash, role, admin_level, school_id, class_id, active, display_name, created_at
) VALUES (?, ?, ?, 'admission_school', NULL, ?, NULL, ?, ?, ?)`,
user.id, user.username, user.passwordHash, user.schoolId, user.active === false ? 0 : 1, user.displayName, user.createdAt
),
auditOperation(log)
]);
},
async updateFeatureScore(registration, log) {
await transaction([
operation('UPDATE registrations SET feature_score = ? WHERE id = ?', Number(registration.featureScore || 0), registration.id),
auditOperation(log)
]);
},
async saveAdmissionRecord(record, log = null) {
const operations = [operation('DELETE FROM admission_records WHERE id = ?', record.id), operation(
`INSERT INTO admission_records (
id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
record.id, record.kind, record.examId, optional(record.userId), optional(record.schoolId), record.status,
JSON.stringify(record.payload || {}), record.createdAt, record.updatedAt
)];
if (log) operations.push(auditOperation(log));
await transaction(operations);
},
async saveAdmissionRecords(records, log = null) {
const operations = [];
for (const record of records) {
operations.push(operation('DELETE FROM admission_records WHERE id = ?', record.id));
operations.push(operation(
`INSERT INTO admission_records (
id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
record.id, record.kind, record.examId, optional(record.userId), optional(record.schoolId), record.status,
JSON.stringify(record.payload || {}), record.createdAt, record.updatedAt
));
}
if (log) operations.push(auditOperation(log));
await transaction(operations);
},
async saveSchool(school, isNew, log) { async saveSchool(school, isNew, log) {
const change = isNew const change = isNew
? operation( ? operation(
'INSERT INTO schools (id, name, code, address, active) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO schools (id, name, code, address, is_source_school, is_admission_school, active) VALUES (?, ?, ?, ?, ?, ?, ?)',
school.id, school.name, school.code, optional(school.address), school.active ? 1 : 0 school.id, school.name, school.code, optional(school.address), school.isSourceSchool ? 1 : 0,
school.isAdmissionSchool ? 1 : 0, school.active ? 1 : 0
) )
: operation( : operation(
'UPDATE schools SET name = ?, code = ?, address = ?, active = ? WHERE id = ?', 'UPDATE schools SET name = ?, code = ?, address = ?, is_source_school = ?, is_admission_school = ?, active = ? WHERE id = ?',
school.name, school.code, optional(school.address), school.active ? 1 : 0, school.id school.name, school.code, optional(school.address), school.isSourceSchool ? 1 : 0,
school.isAdmissionSchool ? 1 : 0, school.active ? 1 : 0, school.id
); );
await transaction([change, auditOperation(log)]); await transaction([change, auditOperation(log)]);
}, },
Loaded 3 of 29 files, more files were not shown because too many files have changed in this diff. Show more