patch3
This commit is contained in:
@@ -158,6 +158,21 @@ function updateQualificationSelection(container) {
|
||||
if (count) count.textContent = `已选 ${selected.length} 人`;
|
||||
}
|
||||
|
||||
function updatePlacementSelection() {
|
||||
const table = document.getElementById('placementReviewTable');
|
||||
if (!table) return;
|
||||
const selectable = [...table.querySelectorAll('[data-placement-select]:not(:disabled)')];
|
||||
const visible = selectable.filter(input => !input.closest('tr').hidden);
|
||||
const selected = selectable.filter(input => input.checked);
|
||||
const selectAll = document.querySelector('[data-placement-select-all][data-target="placementReviewTable"]');
|
||||
if (selectAll) {
|
||||
selectAll.checked = visible.length > 0 && visible.every(input => input.checked);
|
||||
selectAll.indeterminate = visible.some(input => input.checked) && !selectAll.checked;
|
||||
}
|
||||
const count = document.querySelector('[data-placement-selected-count]');
|
||||
if (count) count.textContent = `已选 ${selected.length} 人`;
|
||||
}
|
||||
|
||||
function applyTableFilters(tableId) {
|
||||
const table = document.getElementById(tableId);
|
||||
if (!table) return;
|
||||
@@ -174,6 +189,7 @@ function applyTableFilters(tableId) {
|
||||
if (tableId === 'registrationTable') updateRegistrationSelection();
|
||||
if (tableId === 'candidateTable') updateCandidateSelection();
|
||||
if (tableId === 'paymentTable') updatePaymentSelection();
|
||||
if (tableId === 'placementReviewTable') updatePlacementSelection();
|
||||
if (table.closest('.qualification-ledger')) updateQualificationSelection(table.closest('.qualification-ledger'));
|
||||
}
|
||||
|
||||
@@ -360,6 +376,20 @@ document.addEventListener('click', async event => {
|
||||
toast(data.published ? '批量确认完成并自动公示' : '批量确认完成', `已更新 ${data.count} 名考生`);
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'bulk-placement-review') {
|
||||
const ids = [...document.querySelectorAll('#placementReviewTable [data-placement-select]:checked')].map(input => input.value);
|
||||
const decision = target.dataset.decision;
|
||||
if (!ids.length) return toast('请先选择待审核考生', '可勾选单人,或选择当前筛选结果');
|
||||
let note = '';
|
||||
if (decision === 'withdraw') {
|
||||
note = window.prompt(`将为所选 ${ids.length} 名考生申请退档,请填写统一的特殊理由(至少 8 个字)`, '') ?? '';
|
||||
if (!note) return;
|
||||
if (note.trim().length < 8) return toast('退档理由至少需要 8 个字');
|
||||
} else if (!window.confirm(`确认批量接收所选 ${ids.length} 名投档考生吗?`)) return;
|
||||
const result = await api('/api/admission/placements/bulk', { method: 'POST', body: { ids, decision, note } });
|
||||
toast(decision === 'accept' ? '批量接收完成' : '批量退档申请已提交', `已处理 ${result.count} 名考生`);
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'toggle-admin-account') {
|
||||
const active = target.dataset.active === 'true';
|
||||
if (!active && !window.confirm('确认停用该管理员账户?其现有登录会话会立即失效,历史审批记录将保留。')) return;
|
||||
@@ -373,6 +403,19 @@ document.addEventListener('click', async event => {
|
||||
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 === 'toggle-admission-account') {
|
||||
const active = target.dataset.active === 'true';
|
||||
if (!active && !window.confirm('确认停用该招生学校账户?现有登录会话会立即失效,历史审核记录继续保留。')) return;
|
||||
await api(`/api/admin/admission-school-accounts/${target.dataset.id}`, { method: 'PATCH', body: { active } });
|
||||
toast(active ? '招生学校账户已启用' : '招生学校账户已停用');
|
||||
return renderRoute();
|
||||
}
|
||||
if (action === 'reset-admission-account-password') {
|
||||
if (!window.confirm('确认重置该招生学校账户密码?旧密码和现有登录会话会立即失效。')) return;
|
||||
const data = await api(`/api/admin/admission-school-accounts/${target.dataset.id}/reset-password`, { method: 'POST' });
|
||||
setModal(`<div class="modal-head"><div><span>ADMISSION ACCOUNT 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));
|
||||
@@ -635,6 +678,14 @@ document.addEventListener('input', event => {
|
||||
});
|
||||
|
||||
document.addEventListener('change', event => {
|
||||
if (event.target.matches('[data-placement-select]')) updatePlacementSelection();
|
||||
if (event.target.matches('[data-placement-select-all]')) {
|
||||
const table = document.getElementById(event.target.dataset.target);
|
||||
table?.querySelectorAll('[data-placement-select]:not(:disabled)').forEach(input => {
|
||||
if (!input.closest('tr').hidden) input.checked = event.target.checked;
|
||||
});
|
||||
updatePlacementSelection();
|
||||
}
|
||||
if (event.target.matches('[data-action="result-bulk-exam"]')) {
|
||||
state.resultExamFilter = event.target.value;
|
||||
state.resultSubjectFilter = '';
|
||||
|
||||
@@ -191,11 +191,12 @@ export function createAdminViews(context) {
|
||||
const withdrawals = data.placements.filter(item => item.status === 'withdrawal_pending');
|
||||
const phaseLabels = { draft: '草稿', filling: '志愿填报中', closed: '填报已截止', matching: '投档中', school_review: '学校审核中', supplementary: '补录填报中', completed: '录取完成' };
|
||||
const settings = `<section class="panel admission-settings-panel"><div class="panel-title"><div><h2>考试志愿设置</h2><p>成绩发布后按场次开放,并持续向考生展示录取进度。</p></div></div><form data-form="admission-setting"><label><span>考试 *</span><select name="examId" required>${data.exams.map(exam => `<option value="${h(exam.id)}" ${exam.id === selected?.examId ? 'selected' : ''}>${h(exam.name)}</option>`).join('')}</select></label><div class="field-row"><label><span>填报开始</span><input name="preferenceStart" type="datetime-local" value="${h(selected?.payload?.preferenceStart?.slice(0,16))}"></label><label><span>填报结束</span><input name="preferenceEnd" type="datetime-local" value="${h(selected?.payload?.preferenceEnd?.slice(0,16))}"></label></div><div class="field-row"><label><span>当前阶段</span><select name="status">${Object.entries(phaseLabels).map(([value,label]) => `<option value="${value}" ${value === (selected?.status || 'draft') ? 'selected' : ''}>${label}</option>`).join('')}</select></label><label><span>普通志愿数</span><input name="maxChoices" type="number" min="1" max="20" value="${h(selected?.payload?.maxChoices || 5)}"></label></div><div class="field-row"><label><span>最多提交次数</span><input name="maxSubmissions" type="number" min="1" max="50" value="${h(selected?.payload?.maxSubmissions || 3)}"><small>首次保存也计 1 次;达到上限自动锁定。</small></label><label><span>考生进度说明</span><input name="progress" value="${h(selected?.payload?.progress || '')}" placeholder="例如:招生学校正在审核投档名单"></label></div><label class="agreement"><input type="checkbox" name="enabled" ${selected?.payload?.enabled ? 'checked' : ''}><span>为本场考试启用志愿填报</span></label><label class="agreement"><input type="checkbox" name="autoPublish" ${selected?.payload?.autoPublish === false ? '' : 'checked'}><span>录取结束后自动脱敏公示录取结果与分数线</span></label><button class="solid-button" type="submit">保存志愿设置</button></form>${selected ? `<div class="admission-control-actions"><button class="solid-button" data-action="admission-match" data-exam-id="${h(selected.examId)}">按规则投档</button><button class="ghost-button" data-action="admission-finalize" data-exam-id="${h(selected.examId)}">结束录取并通知</button><button class="ghost-button" data-action="admission-supplementary" data-exam-id="${h(selected.examId)}">开启补录</button></div>` : ''}</section>`;
|
||||
const accounts = `<section class="panel admission-account-panel"><div class="panel-title"><div><h2>招生学校账号</h2><p>只可绑定已在学校管理中标记为“招生校”的学校。</p></div><span>${data.schoolAccounts.length} 个</span></div><form data-form="admission-account"><label><span>招生学校 *</span><select name="schoolId" required>${data.admissionSchools.map(school => `<option value="${h(school.id)}">${h(school.code)} · ${h(school.name)}</option>`).join('')}</select></label><div class="field-row"><label><span>登录账号 *</span><input name="username" required></label><label><span>初始密码 *</span><input name="password" type="password" minlength="8" required></label></div><label><span>显示名称</span><input name="displayName" placeholder="学校招生办公室"></label><button class="solid-button" type="submit">创建招生学校账号</button></form></section>`;
|
||||
const accountRows = data.schoolAccounts.map(account => `<tr data-status="${account.active ? 'active' : 'disabled'}" data-school="${h(account.schoolId)}"><td><div class="person-cell"><span>${h((account.displayName || '招').slice(0, 1))}</span><div><strong>${h(account.displayName)}</strong><small>${h(account.id)}</small></div></div></td><td><strong class="mono">${h(account.username)}</strong></td><td><strong>${h(account.schoolName || '未绑定')}</strong><small class="mono">${h(account.schoolCode || '')}</small></td><td><span class="status status-${account.active ? 'approved' : 'closed'}">${account.active ? '已启用' : '已停用'}</span></td><td>${formatDate(account.createdAt, true)}</td><td><div class="admin-account-actions"><button class="row-action" data-action="reset-admission-account-password" data-id="${h(account.id)}">重置密码</button><button class="row-action ${account.active ? 'danger' : 'primary'}" data-action="toggle-admission-account" data-id="${h(account.id)}" data-active="${account.active ? 'false' : 'true'}">${account.active ? '停用账户' : '重新启用'}</button></div></td></tr>`).join('');
|
||||
const accounts = `<section class="panel admission-account-panel admission-account-management"><div class="panel-title"><div><h2>招生学校账户管理</h2><p>创建并维护各招生学校登录账户;停用会立即结束该账户现有会话。</p></div><span>${data.schoolAccounts.filter(item => item.active).length} 个启用 / 共 ${data.schoolAccounts.length} 个</span></div><form data-form="admission-account"><label><span>招生学校 *</span><select name="schoolId" required>${data.admissionSchools.map(school => `<option value="${h(school.id)}">${h(school.code)} · ${h(school.name)}</option>`).join('')}</select></label><div class="field-row"><label><span>登录账号 *</span><input name="username" required></label><label><span>初始密码 *</span><input name="password" type="password" minlength="8" required></label></div><label><span>显示名称</span><input name="displayName" placeholder="学校招生办公室"></label><button class="solid-button" type="submit">创建招生学校账号</button></form><div class="account-management-divider"><strong>已建账户</strong><span>保留历史记录,不提供物理删除</span></div><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="admissionAccountTable" placeholder="搜索学校、账号或显示名称"></label><div class="table-filter-selects"><select data-table-filter="school" data-target="admissionAccountTable"><option value="">全部招生学校</option>${data.admissionSchools.map(school => `<option value="${h(school.id)}">${h(school.name)}</option>`).join('')}</select></div></div><div class="filter-pills account-status-pills"><button type="button" class="active" data-action="status-filter" data-target="admissionAccountTable" data-status="all">全部</button><button type="button" data-action="status-filter" data-target="admissionAccountTable" data-status="active">已启用</button><button type="button" data-action="status-filter" data-target="admissionAccountTable" data-status="disabled">已停用</button></div><div class="table-scroll"><table id="admissionAccountTable"><thead><tr><th>账户</th><th>登录账号</th><th>绑定招生学校</th><th>状态</th><th>创建时间</th><th>账户操作</th></tr></thead><tbody>${accountRows || '<tr><td colspan="6" class="empty-state">还没有招生学校账户</td></tr>'}</tbody></table></div></section>`;
|
||||
const planForm = `<section class="panel admission-plan-console structured"><div class="panel-title"><div><h2>代招生校上传计划</h2><p>每个类别独立设置人数、特长资格和生源校指标,保存后直接审核通过。</p></div></div><form data-form="admission-plan"><div class="field-row"><label><span>考试 *</span><select name="examId" required>${data.exams.map(exam => `<option value="${h(exam.id)}">${h(exam.name)}</option>`).join('')}</select></label><label><span>招生学校 *</span><select name="schoolId" required>${data.admissionSchools.map(school => `<option value="${h(school.id)}">${h(school.code)} · ${h(school.name)}</option>`).join('')}</select></label></div>${admissionCategoriesEditor(h, data.sourceSchools)}<label><span>计划说明</span><textarea name="note" rows="2" placeholder="可填写政策依据或补充说明"></textarea></label><button class="solid-button" type="submit">代上传并审核通过</button></form></section>`;
|
||||
const plans = `<section class="panel data-panel"><div class="panel-title"><div><h2>招生计划审核</h2><p>核对类别资格、计划总数和指标分配。</p></div><span>${pendingPlans.length} 份待审</span></div><div class="table-scroll"><table><thead><tr><th>考试 / 学校</th><th>计划构成</th><th>指标分配</th><th>状态</th><th>操作</th></tr></thead><tbody>${data.plans.map(plan => `<tr><td><strong>${h(plan.examName)}</strong><small>${h(plan.schoolName)}</small></td><td>${plan.payload.categories.map(category => `<strong>${h(category.name)} ${h(category.quota)} 人</strong><small>${h(specialtyLabel(category.specialtyCategory, category.specialtyType) || '普通 / 政策类')}</small>`).join('')}</td><td>${plan.payload.categories.flatMap(category => (category.indicatorAllocations || []).map(allocation => `${h(data.sourceSchools.find(item => item.id === allocation.sourceSchoolId)?.name || allocation.sourceSchoolId)} ${h(allocation.quota)} 人`)).join('<br>') || '无定向指标'}</td><td>${badge(plan.status)}</td><td>${plan.status === 'pending' ? `<button class="row-action primary" data-action="admission-plan-review" data-id="${h(plan.id)}" data-status="approved">通过</button><button class="row-action" data-action="admission-plan-review" data-id="${h(plan.id)}" data-status="rejected">退回</button>` : h(plan.payload.reviewNote || '')}</td></tr>`).join('') || '<tr><td colspan="5" class="empty-state">暂无招生计划</td></tr>'}</tbody></table></div></section>`;
|
||||
const placements = `<section class="panel data-panel"><div class="panel-title"><h2>投档与退档监督</h2><span>志愿只读,管理员均无修改入口</span></div><div class="table-scroll"><table><thead><tr><th>考生</th><th>成绩 / 志愿</th><th>投档学校</th><th>类别</th><th>状态</th><th>退档审核</th></tr></thead><tbody>${data.placements.map(item => `<tr><td><strong>${h(item.candidate.name)}</strong><small class="mono">${h(item.candidate.registrationNumber)}</small><small>${h(item.candidate.specialtyLabel || '普通生')}</small></td><td>${h(item.payload.totalScore)} 分 · 特征分 ${h(item.payload.featureScore || 0)} · 第 ${h(item.payload.preferenceOrder)} 志愿</td><td>${h(item.schoolName)}</td><td>${h(item.payload.categoryName)}</td><td>${badge(item.status)}</td><td>${item.status === 'withdrawal_pending' ? `<small>${h(item.payload.withdrawalReason)}</small><button class="row-action primary" data-action="withdrawal-review" data-id="${h(item.id)}" data-approved="true">批准</button><button class="row-action" data-action="withdrawal-review" data-id="${h(item.id)}" data-approved="false">驳回</button>` : '—'}</td></tr>`).join('') || '<tr><td colspan="6" class="empty-state">尚未产生投档记录</td></tr>'}</tbody></table></div></section>`;
|
||||
return `<section class="admission-command-banner"><div><span>ADMISSION COMMAND</span><h2>中考招生录取控制台</h2><p>学校代码、资格类别、招生计划和指标名额在一条可审计链路中完成。</p></div><dl><div><dt>待审计划</dt><dd>${pendingPlans.length}</dd></div><div><dt>学校审核中</dt><dd>${data.placements.filter(item => item.status === 'school_review').length}</dd></div><div><dt>退档待审</dt><dd>${withdrawals.length}</dd></div><div><dt>正式录取</dt><dd>${data.placements.filter(item => item.status === 'final').length}</dd></div></dl></section><div class="admission-admin-grid">${settings}${accounts}</div>${planForm}${plans}${placements}`;
|
||||
return `<section class="admission-command-banner"><div><span>ADMISSION COMMAND</span><h2>中考招生录取控制台</h2><p>学校代码、资格类别、招生计划和指标名额在一条可审计链路中完成。</p></div><dl><div><dt>待审计划</dt><dd>${pendingPlans.length}</dd></div><div><dt>学校审核中</dt><dd>${data.placements.filter(item => item.status === 'school_review').length}</dd></div><div><dt>退档待审</dt><dd>${withdrawals.length}</dd></div><div><dt>正式录取</dt><dd>${data.placements.filter(item => item.status === 'final').length}</dd></div></dl></section><div class="admission-admin-grid single">${settings}</div>${accounts}${planForm}${plans}${placements}`;
|
||||
}
|
||||
|
||||
function adminIndicatorQualifications(data) {
|
||||
|
||||
@@ -29,7 +29,11 @@ export function createAdmissionViews(context) {
|
||||
|
||||
function placements(data) {
|
||||
const exportBar = data.completedExams?.length ? `<section class="panel admission-export-bar"><div><span>FINAL ROSTER</span><strong>正式录取考生信息 Excel</strong><small>仅录取工作结束后开放,包含本校全部正式录取考生资料与当次成绩。</small></div><label><span>已完成考试</span><select name="exportExamId">${data.completedExams.map(exam => `<option value="${h(exam.id)}">${h(exam.name)}</option>`).join('')}</select></label><button class="solid-button" data-action="download-admitted-candidates">下载 Excel</button></section>` : '';
|
||||
return `${exportBar}<section class="panel data-panel"><div class="panel-title"><div><h2>本校投档名单</h2><p>显示投档所需的考生信息与当次成绩,不包含其余志愿。</p></div><span>${data.placements.length} 人</span></div><div class="table-scroll"><table><thead><tr><th>考生</th><th>资格</th><th>当次成绩</th><th>投档类别</th><th>状态</th><th>审核</th></tr></thead><tbody>${data.placements.map(item => `<tr><td><strong>${h(item.candidate.name)}</strong><small class="mono">${h(item.candidate.registrationNumber)} · ${h(item.candidate.idNumberMasked)}</small></td><td>${h(item.candidate.specialtyLabel || '普通生')}<small>${h(item.candidate.specialtyCertificate || '')}</small><small>${h(item.candidate.policyEligibility || '')}</small></td><td>${item.results.map(result => `${h(result.subjectName)} ${h(result.score)}`).join('<br>')}<strong>总分 ${h(item.payload.totalScore)} · 特征分 ${h(item.featureScore || 0)}</strong></td><td>${h(item.payload.categoryName)}<small>第 ${h(item.payload.preferenceOrder)} 志愿</small></td><td>${badge(item.status)}</td><td>${item.status === 'school_review' ? `<form class="placement-review-form" data-form="placement-review"><input type="hidden" name="id" value="${h(item.id)}"><select name="decision"><option value="accept">接收</option><option value="withdraw">申请退档</option></select><input name="note" placeholder="退档须填写特殊理由"><button class="row-action primary" type="submit">确认</button></form>` : `<small>${h(item.payload.schoolDecisionNote || '已处理')}</small>`}</td></tr>`).join('') || '<tr><td colspan="6" class="empty-state">暂无投档考生</td></tr>'}</tbody></table></div></section>`;
|
||||
const exams = [...new Map(data.placements.map(item => [item.examId, item.examName])).entries()];
|
||||
const categories = [...new Set(data.placements.map(item => item.payload.categoryName).filter(Boolean))];
|
||||
const pendingCount = data.placements.filter(item => item.status === 'school_review').length;
|
||||
const rows = data.placements.map(item => `<tr data-status="${h(item.status)}" data-exam="${h(item.examId)}" data-category="${h(item.payload.categoryName)}"><td><input type="checkbox" data-placement-select value="${h(item.id)}" ${item.status === 'school_review' ? '' : 'disabled'} aria-label="选择 ${h(item.candidate.name)}"></td><td><strong>${h(item.candidate.name)}</strong><small class="mono">${h(item.candidate.registrationNumber)} · ${h(item.candidate.idNumberMasked)}</small><small>${h(item.examName)}</small></td><td>${h(item.candidate.specialtyLabel || '普通生')}<small>${h(item.candidate.specialtyCertificate || '')}</small><small>${h(item.candidate.policyEligibility || '')}</small></td><td>${item.results.map(result => `${h(result.subjectName)} ${h(result.score)}`).join('<br>')}<strong>投档分 ${h(item.payload.totalScore)} · 特征分 ${h(item.featureScore || 0)}</strong></td><td>${h(item.payload.categoryName)}<small>第 ${h(item.payload.preferenceOrder)} 志愿</small></td><td>${badge(item.status)}</td><td>${item.status === 'school_review' ? `<form class="placement-review-form" data-form="placement-review"><input type="hidden" name="id" value="${h(item.id)}"><select name="decision"><option value="accept">接收</option><option value="withdraw">申请退档</option></select><input name="note" placeholder="退档须填写至少 8 字理由"><button class="row-action primary" type="submit">确认</button></form>` : `<small>${h(item.payload.schoolDecisionNote || '已处理')}</small>`}</td></tr>`).join('');
|
||||
return `${exportBar}<section class="panel data-panel placement-review-ledger"><div class="panel-title"><div><h2>本校投档审核台账</h2><p>可搜索、筛选和多选批量处理;仅待审核记录可被选中。</p></div><span>${pendingCount} 人待审 / 共 ${data.placements.length} 人</span></div><div class="data-toolbar"><label class="search-box">${icons.search}<input data-action="table-search" data-target="placementReviewTable" placeholder="搜索姓名、报名号、考试、类别或资格"></label><div class="table-filter-selects"><select data-table-filter="exam" data-target="placementReviewTable"><option value="">全部考试</option>${exams.map(([id, name]) => `<option value="${h(id)}">${h(name)}</option>`).join('')}</select><select data-table-filter="category" data-target="placementReviewTable"><option value="">全部招生类别</option>${categories.map(category => `<option value="${h(category)}">${h(category)}</option>`).join('')}</select><button type="button" class="row-action" data-action="clear-table-filters" data-target="placementReviewTable">清除筛选</button></div></div><div class="filter-pills placement-status-pills"><button type="button" class="active" data-action="status-filter" data-target="placementReviewTable" data-status="all">全部</button><button type="button" data-action="status-filter" data-target="placementReviewTable" data-status="school_review">待审核</button><button type="button" data-action="status-filter" data-target="placementReviewTable" data-status="admitted">已接收</button><button type="button" data-action="status-filter" data-target="placementReviewTable" data-status="withdrawal_pending">退档待审</button><button type="button" data-action="status-filter" data-target="placementReviewTable" data-status="final">正式录取</button></div><div class="placement-bulk-toolbar"><label><input type="checkbox" data-placement-select-all data-target="placementReviewTable"><span>选择当前筛选结果中的待审核考生</span></label><div><strong data-placement-selected-count>已选 0 人</strong><button type="button" class="ghost-button" data-action="bulk-placement-review" data-decision="withdraw">批量申请退档</button><button type="button" class="solid-button" data-action="bulk-placement-review" data-decision="accept">批量接收</button></div></div><div class="table-scroll"><table id="placementReviewTable"><thead><tr><th class="select-column">选择</th><th>考生 / 考试</th><th>资格</th><th>当次成绩</th><th>投档类别</th><th>状态</th><th>单人审核</th></tr></thead><tbody>${rows || '<tr><td colspan="7" class="empty-state">暂无投档考生</td></tr>'}</tbody></table></div></section>`;
|
||||
}
|
||||
return { renderAdmission };
|
||||
}
|
||||
|
||||
@@ -225,7 +225,10 @@ export function createAdminRoutes(context) {
|
||||
const profile = db.candidateProfiles.find(item => item.userId === preference.userId) || {};
|
||||
return { ...preference, candidate: { registrationNumber: account.candidateNumber, name: profile.name }, choices: (preference.payload?.choices || []).map(choice => ({ ...choice, schoolName: db.schools.find(item => item.id === choice.schoolId)?.name || '' })) };
|
||||
});
|
||||
const schoolAccounts = db.users.filter(item => item.role === 'admission_school').map(safeUser);
|
||||
const schoolAccounts = db.users.filter(item => item.role === 'admission_school').map(item => {
|
||||
const school = db.schools.find(entry => entry.id === item.schoolId);
|
||||
return { ...safeUser(item), active: item.active !== false, createdAt: item.createdAt, schoolName: school?.name || '', schoolCode: school?.code || '' };
|
||||
});
|
||||
return sendJson(response, 200, { ok: true, settings, plans, preferences, placements, schoolAccounts, schools: db.schools.filter(item => item.active), admissionSchools: db.schools.filter(item => item.active && item.isAdmissionSchool), sourceSchools: db.schools.filter(item => item.active && item.isSourceSchool), exams: db.exams.filter(item => !item.archivedAt) });
|
||||
}
|
||||
if (pathname === '/api/admin/admission-school-accounts' && request.method === 'POST') {
|
||||
@@ -240,6 +243,30 @@ export function createAdminRoutes(context) {
|
||||
await database.createAdmissionSchoolAccount(account, logAction(db, user, '创建招生学校账号', `${school.name} · ${username}`));
|
||||
return sendJson(response, 201, { ok: true, account: safeUser(account) });
|
||||
}
|
||||
const admissionAccountMatch = pathname.match(/^\/api\/admin\/admission-school-accounts\/([^/]+)$/);
|
||||
if (admissionAccountMatch && request.method === 'PATCH') {
|
||||
if (user.adminLevel !== 'super') return sendError(response, 403, '只有超级管理员可以维护招生学校账号');
|
||||
const target = db.users.find(item => item.id === admissionAccountMatch[1] && item.role === 'admission_school');
|
||||
if (!target) return sendError(response, 404, '招生学校账号不存在');
|
||||
const body = await readJson(request);
|
||||
target.active = body.active == null ? target.active : Boolean(body.active);
|
||||
target.displayName = cleanText(body.displayName || target.displayName, 80);
|
||||
await database.updateAdmin(target, false, logAction(db, user, target.active ? '启用招生学校账号' : '停用招生学校账号', `${target.displayName} · ${target.username}`));
|
||||
if (!target.active) for (const [token, session] of sessions) if (session.userId === target.id) sessions.delete(token);
|
||||
return sendJson(response, 200, { ok: true, account: { ...safeUser(target), active: target.active } });
|
||||
}
|
||||
const admissionAccountResetMatch = pathname.match(/^\/api\/admin\/admission-school-accounts\/([^/]+)\/reset-password$/);
|
||||
if (admissionAccountResetMatch && request.method === 'POST') {
|
||||
if (user.adminLevel !== 'super') return sendError(response, 403, '只有超级管理员可以重置招生学校账号密码');
|
||||
const target = db.users.find(item => item.id === admissionAccountResetMatch[1] && item.role === 'admission_school');
|
||||
if (!target) return sendError(response, 404, '招生学校账号不存在');
|
||||
const temporaryPassword = `Reset-${randomBytes(7).toString('base64url')}`;
|
||||
target.passwordHash = hashPassword(temporaryPassword);
|
||||
target.active = true;
|
||||
await database.updateAdmin(target, true, logAction(db, user, '重置招生学校账号密码', `${target.displayName} · ${target.username}`));
|
||||
for (const [token, session] of sessions) if (session.userId === target.id) sessions.delete(token);
|
||||
return sendJson(response, 200, { ok: true, username: target.username, temporaryPassword });
|
||||
}
|
||||
const settingMatch = pathname.match(/^\/api\/admin\/admissions\/([^/]+)\/setting$/);
|
||||
if (settingMatch && request.method === 'PUT') {
|
||||
if (user.adminLevel !== 'super') return sendError(response, 403, '只有超级管理员可以设置志愿填报');
|
||||
|
||||
@@ -62,7 +62,7 @@ export function createAdmissionRoutes(context) {
|
||||
return { subjectName: exam?.subjects.find(subject => subject.id === result.subjectId)?.name || result.subjectId, score: result.score };
|
||||
});
|
||||
const qualification = resolveProfileSpecialty(profile);
|
||||
return { ...item, candidate: { registrationNumber: account.candidateNumber, name: profile.name, gender: profile.gender, idNumberMasked: maskId(profile.idNumber), specialtyCategory: qualification.category, specialtyType: qualification.type, specialtyLabel: specialtyLabel(qualification.category, qualification.type), specialtyCertificate: profile.specialtyCertificate || '', policyEligibility: profile.policyEligibility || '' }, featureScore: Number(registration?.featureScore || 0), results };
|
||||
return { ...item, examName: db.exams.find(exam => exam.id === item.examId)?.name || item.examId, candidate: { registrationNumber: account.candidateNumber, name: profile.name, gender: profile.gender, idNumberMasked: maskId(profile.idNumber), specialtyCategory: qualification.category, specialtyType: qualification.type, specialtyLabel: specialtyLabel(qualification.category, qualification.type), specialtyCertificate: profile.specialtyCertificate || '', policyEligibility: profile.policyEligibility || '' }, featureScore: Number(registration?.featureScore || 0), results };
|
||||
});
|
||||
const completedExams = db.exams.filter(exam => admissionRecords(db, 'setting', exam.id).some(setting => setting.status === 'completed') && placements.some(item => item.examId === exam.id && item.status === 'final'));
|
||||
return sendJson(response, 200, { ok: true, school, placements, completedExams });
|
||||
@@ -93,6 +93,26 @@ export function createAdmissionRoutes(context) {
|
||||
const buffer = Buffer.from(await buildWorkbook('admitted_candidates', rows, { subtitle: `${exam.name}|${school.name}` }));
|
||||
return sendWorkbook(response, buffer, `${exam.name}-${school.name}-录取考生信息.xlsx`);
|
||||
}
|
||||
if (request.method === 'POST' && pathname === '/api/admission/placements/bulk') {
|
||||
const body = await readJson(request);
|
||||
const ids = [...new Set((Array.isArray(body.ids) ? body.ids : []).map(id => cleanText(id, 64)).filter(Boolean))];
|
||||
const decision = cleanText(body.decision, 30);
|
||||
const note = cleanText(body.note, 500);
|
||||
if (!ids.length) return sendError(response, 400, '请至少选择一名待审核考生');
|
||||
if (!['accept', 'withdraw'].includes(decision)) return sendError(response, 400, '请选择接收或申请退档');
|
||||
if (decision === 'withdraw' && note.length < 8) return sendError(response, 400, '批量申请退档必须填写至少 8 个字的特殊理由');
|
||||
const placements = admissionRecords(db, 'placement').filter(item => ids.includes(item.id) && item.schoolId === school.id && item.status === 'school_review');
|
||||
if (placements.length !== ids.length) return sendError(response, 409, '所选记录中包含已处理或不属于本校的投档记录,请刷新后重试');
|
||||
const now = nowIso();
|
||||
for (const placement of placements) {
|
||||
placement.status = decision === 'accept' ? 'admitted' : 'withdrawal_pending';
|
||||
placement.payload.schoolDecisionNote = note;
|
||||
if (decision === 'withdraw') placement.payload.withdrawalReason = note;
|
||||
placement.updatedAt = now;
|
||||
}
|
||||
await database.saveAdmissionRecords(placements, logAction(db, user, decision === 'accept' ? '批量接收投档考生' : '批量申请退档', `${school.name} · ${placements.length} 人`));
|
||||
return sendJson(response, 200, { ok: true, count: placements.length, decision });
|
||||
}
|
||||
const placementMatch = pathname.match(/^\/api\/admission\/placements\/([^/]+)$/);
|
||||
if (request.method === 'PATCH' && placementMatch) {
|
||||
const placement = admissionRecords(db, 'placement').find(item => item.id === placementMatch[1] && item.schoolId === school.id);
|
||||
|
||||
@@ -631,11 +631,13 @@ button:disabled { cursor: not-allowed; opacity: .5; }
|
||||
.admission-command-banner { display:flex; justify-content:space-between; gap:28px; margin-bottom:18px; padding:26px 30px; border-radius:14px; color:#fff; background:linear-gradient(118deg,#17375f 0%,#245783 62%,#2b7180 100%); box-shadow:0 16px 34px rgba(23,55,95,.18); }
|
||||
.admission-command-banner > div span { color:#9fcad5; font:700 10px/1.2 Consolas,monospace; letter-spacing:1.8px; }.admission-command-banner h2 { margin:8px 0 6px; font-size:24px; }.admission-command-banner p { max-width:650px; margin:0; color:#dceaf0; line-height:1.7; }.admission-command-banner dl { display:grid; grid-template-columns:repeat(4,minmax(70px,1fr)); gap:10px; margin:0; }.admission-command-banner dl div { padding:12px; border:1px solid rgba(255,255,255,.16); border-radius:9px; background:rgba(255,255,255,.07); }.admission-command-banner dt { color:#b8d6df; }.admission-command-banner dd { margin:4px 0 0; font-size:22px; font-weight:800; }
|
||||
.admission-admin-grid { display:grid; grid-template-columns:1.35fr .85fr; gap:16px; margin-bottom:16px; }.admission-settings-panel form,.admission-account-panel form,.admission-plan-console form { display:grid; gap:12px; }.admission-control-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:18px; padding-top:16px; border-top:1px solid var(--line); }
|
||||
.admission-admin-grid.single { grid-template-columns:1fr; }
|
||||
.admission-candidate-list { display:grid; gap:18px; }.admission-candidate-card { padding:24px; }.admission-candidate-card > header { display:flex; justify-content:space-between; gap:18px; }.admission-candidate-card > header span { color:var(--muted); font:700 10px Consolas,monospace; }.admission-candidate-card h2 { margin:5px 0 0; }
|
||||
.admission-progress-track { position:relative; display:grid; grid-template-columns:repeat(4,1fr); margin:26px 0; }.admission-progress-track::before { content:""; position:absolute; top:15px; left:10%; right:10%; height:2px; background:#dbe4ec; }.admission-progress-track div { position:relative; z-index:1; display:grid; justify-items:center; gap:7px; color:#8190a0; }.admission-progress-track i { display:grid; place-items:center; width:32px; height:32px; border:2px solid #dbe4ec; border-radius:50%; background:#fff; font-style:normal; font-weight:800; }.admission-progress-track .done i,.admission-progress-track .current i { border-color:#287486; color:#fff; background:#287486; }.admission-progress-track .current i { box-shadow:0 0 0 6px rgba(40,116,134,.12); }.admission-progress-track .done,.admission-progress-track .current { color:#214d5a; font-weight:700; }
|
||||
.admission-score-strip { display:flex; align-items:center; gap:12px; padding:14px 16px; border-radius:10px; background:#f2f7f8; }.admission-score-strip strong { margin-right:auto; font-size:18px; }.admission-score-strip em { color:#287486; font-style:normal; font-weight:700; }.admission-progress-copy { color:var(--muted); }.admission-result-banner { display:grid; gap:4px; margin:14px 0; padding:16px; border-left:4px solid #287486; border-radius:8px; background:#eef7f8; }.admission-result-banner strong { font-size:17px; }
|
||||
.preference-form { margin-top:18px; padding-top:18px; border-top:1px solid var(--line); }.preference-form-head { display:flex; justify-content:space-between; gap:12px; margin-bottom:12px; }.preference-form-head small { display:block; margin-top:4px; color:var(--muted); }.preference-choice-list { display:grid; gap:9px; margin-bottom:14px; }.preference-choice-list label { display:grid; grid-template-columns:34px 1fr; align-items:center; gap:9px; }.preference-choice-list b { display:grid; place-items:center; width:30px; height:30px; border-radius:50%; color:#fff; background:#244e72; }.preference-choice-list select,.placement-review-form select,.placement-review-form input { min-height:40px; padding:8px 10px; border:1px solid var(--line); border-radius:8px; background:#fff; }.locked-preferences { display:grid; gap:8px; margin-top:16px; }.locked-preferences span { display:flex; gap:10px; padding:10px 12px; border-radius:8px; background:#f5f7fa; }.locked-preferences b { color:#287486; }
|
||||
.placement-review-form { display:grid; min-width:210px; gap:7px; }.public-admission-board { margin-bottom:18px; overflow:hidden; }.public-admission-board > header { display:flex; justify-content:space-between; padding:20px 22px; color:#fff; background:#214d5a; }.public-admission-board h3 { margin:5px 0 0; font-size:19px; }.public-admission-board table { margin:0; }
|
||||
.placement-review-ledger { margin-bottom:16px; overflow:hidden; }.placement-review-ledger .data-toolbar { flex-wrap:wrap; }.placement-review-ledger table { min-width:1250px; }.placement-review-ledger td > small,.placement-review-ledger td > strong { display:block; margin-top:3px; }.placement-status-pills,.account-status-pills { margin:0 24px 12px; }.placement-bulk-toolbar { display:flex; align-items:center; justify-content:space-between; gap:18px; margin:0 24px 16px; padding:14px 16px; border:1px solid #d8e2e9; border-radius:10px; background:#f6f9fb; }.placement-bulk-toolbar > label,.placement-bulk-toolbar > div { display:flex; align-items:center; gap:10px; }.placement-bulk-toolbar input { width:17px; height:17px; }.placement-bulk-toolbar strong { margin-right:5px; color:#287486; }.placement-bulk-toolbar button { min-height:40px; }
|
||||
@media (max-width:1000px) { .admission-command-banner { flex-direction:column; }.admission-admin-grid { grid-template-columns:1fr; } }
|
||||
@media (max-width:620px) { .admission-command-banner { padding:20px; }.admission-command-banner dl { grid-template-columns:repeat(2,1fr); }.admission-progress-track span { font-size:9px; }.admission-score-strip,.preference-form-head { align-items:flex-start; flex-direction:column; }.admission-score-strip strong { margin-right:0; } }
|
||||
|
||||
@@ -688,6 +690,7 @@ button:disabled { cursor: not-allowed; opacity: .5; }
|
||||
|
||||
/* 招生控制台:用稳定的表单栅格替代浏览器默认控件排版 */
|
||||
.admission-settings-panel,.admission-account-panel,.admission-plan-console { overflow:hidden; }
|
||||
.admission-account-management { margin-bottom:16px; }.admission-account-management > form { grid-template-columns:minmax(240px,1fr) minmax(380px,1.4fr) minmax(220px,.8fr) auto; align-items:end; }.admission-account-management > form > .field-row { grid-template-columns:1fr 1fr; }.admission-account-management > form > .solid-button { min-width:170px; }.account-management-divider { display:flex; align-items:center; justify-content:space-between; padding:15px 24px; border-top:1px solid #e1e7ee; border-bottom:1px solid #e1e7ee; background:#f7f9fc; }.account-management-divider strong { color:#263a54; }.account-management-divider span { color:#8390a3; font-size:12px; }.admission-account-management .data-toolbar { padding-top:16px; padding-bottom:12px; }.admission-account-management table { min-width:980px; }
|
||||
.admission-settings-panel .panel-title,.admission-account-panel .panel-title,.admission-plan-console .panel-title { min-height:88px; height:auto; padding:20px 24px; background:linear-gradient(100deg,#fff,#f6f9fc); }
|
||||
.admission-settings-panel .panel-title h2,.admission-account-panel .panel-title h2,.admission-plan-console .panel-title h2 { font:400 23px/1.2 STKaiti,KaiTi,serif; }
|
||||
.admission-settings-panel .panel-title p,.admission-account-panel .panel-title p,.admission-plan-console .panel-title p { margin:7px 0 0; color:#69778c; font-size:13px; line-height:1.6; }
|
||||
@@ -828,3 +831,9 @@ body.review-subpage-open { overflow:hidden; }
|
||||
@media (max-width:650px) {
|
||||
.result-workbench-selectors,.result-workbench-summary { grid-template-columns:1fr; }.result-workbench-summary > span { padding:6px 0; border-right:0; border-bottom:1px solid #e2e8f0; }.result-workbench-actions { align-items:stretch; flex-direction:column; }.result-workbench-actions > div { margin:0 0 5px; }.result-workbench-actions button { width:100%; }
|
||||
}
|
||||
@media (max-width:1100px) {
|
||||
.admission-account-management > form { grid-template-columns:1fr 1fr; }.admission-account-management > form > .solid-button { width:100%; }
|
||||
}
|
||||
@media (max-width:800px) {
|
||||
.admission-account-management > form { grid-template-columns:1fr; }.placement-bulk-toolbar,.placement-bulk-toolbar > div { align-items:stretch; flex-direction:column; }.placement-bulk-toolbar button { width:100%; }.placement-status-pills,.account-status-pills { margin-right:18px; margin-left:18px; }
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ try {
|
||||
const clientSources = await Promise.all([
|
||||
'app.js',
|
||||
'src/client/admin-views.mjs',
|
||||
'src/client/admission-views.mjs',
|
||||
'src/client/candidate-views.mjs',
|
||||
'src/client/public-views.mjs'
|
||||
].map(file => readFile(resolve(root, file), 'utf8')));
|
||||
@@ -170,6 +171,9 @@ try {
|
||||
assert.match(appSource, /data-action="edit-exam"/, '考试草稿应提供编辑入口');
|
||||
assert.match(appSource, /data-action="archive-exam"/, '超级管理员界面应提供不可逆考试归档入口');
|
||||
assert.match(appSource, /data-action="refresh-results-cache"/, '成绩管理中心应提供 Redis 成绩缓存刷新入口');
|
||||
assert.match(appSource, /data-action="bulk-placement-review"/, '招生学校投档审核应提供批量处理入口');
|
||||
assert.match(appSource, /data-action="toggle-admission-account"/, '超级管理员应有招生学校账户启停入口');
|
||||
assert.match(appSource, /data-action="reset-admission-account-password"/, '超级管理员应有招生学校账户密码重置入口');
|
||||
assert.match(appSource, /归档不可撤销/, '归档前应明确提示成绩将永久锁定');
|
||||
|
||||
const { DatabaseSync } = await import('node:sqlite');
|
||||
@@ -620,6 +624,53 @@ try {
|
||||
assert.equal(qualificationPublication.rows.find(item => item.registrationNumber === candidateNumber).eligible, true, '资格公示应公开考生有无指标分配资格');
|
||||
const invalidSpecialtyPlan = await admissionSchoolClient.request('/api/admission/plans', { method: 'POST', body: { examId: exam.id, categories: [{ code: 'bad', name: '错误特长类别', quota: 1, specialtyCategory: 'arts', specialtyType: 'track_field', indicatorAllocations: [] }] } });
|
||||
assert.equal(invalidSpecialtyPlan.response.status, 400, '招生计划不得把艺术大类与体育小类混用');
|
||||
|
||||
const placementCandidates = qualificationExam.qualificationStatus.rows.slice(0, 3);
|
||||
assert.equal(placementCandidates.length, 3, '批量投档测试至少需要三名候选考生');
|
||||
const placementIds = placementCandidates.map((row, index) => `placement_bulk_test_${index + 1}`);
|
||||
const placementCreatedAt = new Date().toISOString();
|
||||
const placementWriter = new DatabaseSync(testDb);
|
||||
const insertPlacement = placementWriter.prepare(`
|
||||
INSERT INTO admission_records (id, kind, exam_id, user_id, school_id, status, payload_json, created_at, updated_at)
|
||||
VALUES (?, 'placement', ?, ?, ?, 'school_review', ?, ?, ?)
|
||||
`);
|
||||
for (const [index, row] of placementCandidates.entries()) {
|
||||
insertPlacement.run(
|
||||
placementIds[index], exam.id, row.userId, admissionOnlySchool.data.school.id,
|
||||
JSON.stringify({ categoryCode: index === 2 ? 'arts' : 'general', categoryName: index === 2 ? '美术特长生' : '普通生', totalScore: 620 - index, preferenceOrder: index + 1 }),
|
||||
placementCreatedAt, placementCreatedAt
|
||||
);
|
||||
}
|
||||
placementWriter.close();
|
||||
|
||||
const placementReviewLedger = await admissionSchoolClient.request('/api/admission/placements');
|
||||
const seededPlacements = placementReviewLedger.data.placements.filter(item => placementIds.includes(item.id));
|
||||
assert.equal(seededPlacements.length, 3, '招生学校投档台账应返回可供筛选、搜索的完整候选记录');
|
||||
assert.ok(seededPlacements.every(item => item.examName === exam.name && item.candidate.registrationNumber), '投档台账应包含考试名称和考生报名号');
|
||||
const bulkAccept = await admissionSchoolClient.request('/api/admission/placements/bulk', { method: 'POST', body: { ids: placementIds.slice(0, 2), decision: 'accept', note: '批量核验通过' } });
|
||||
assert.equal(bulkAccept.response.status, 200, '招生学校应能批量接收多名投档考生');
|
||||
assert.equal(bulkAccept.data.count, 2, '批量接收应返回实际处理人数');
|
||||
const shortBulkWithdrawal = await admissionSchoolClient.request('/api/admission/placements/bulk', { method: 'POST', body: { ids: [placementIds[2]], decision: 'withdraw', note: '材料不符' } });
|
||||
assert.equal(shortBulkWithdrawal.response.status, 400, '批量退档必须填写充分的特殊理由');
|
||||
const bulkWithdrawal = await admissionSchoolClient.request('/api/admission/placements/bulk', { method: 'POST', body: { ids: [placementIds[2]], decision: 'withdraw', note: '专项资格证明材料复核不通过' } });
|
||||
assert.equal(bulkWithdrawal.response.status, 200, '招生学校应能批量申请退档');
|
||||
const processedPlacementLedger = await admissionSchoolClient.request('/api/admission/placements');
|
||||
assert.ok(processedPlacementLedger.data.placements.filter(item => placementIds.slice(0, 2).includes(item.id)).every(item => item.status === 'admitted'), '批量接收后所选记录应全部进入拟录取状态');
|
||||
assert.equal(processedPlacementLedger.data.placements.find(item => item.id === placementIds[2]).status, 'withdrawal_pending', '批量退档后记录应进入上级审核状态');
|
||||
assert.equal((await admissionSchoolClient.request('/api/admission/placements/bulk', { method: 'POST', body: { ids: placementIds.slice(0, 2), decision: 'accept' } })).response.status, 409, '已处理记录不得被重复批量审核');
|
||||
|
||||
const admissionAccountLedger = await admin.request('/api/admin/admissions');
|
||||
const managedAdmissionAccount = admissionAccountLedger.data.schoolAccounts.find(item => item.id === admissionAccount.data.account.id);
|
||||
assert.equal(managedAdmissionAccount.schoolName, '海州市招生实验学校', '招生学校账户台账应显示绑定学校');
|
||||
assert.equal(managedAdmissionAccount.active, true, '招生学校账户台账应返回真实启停状态');
|
||||
const disabledAdmissionAccount = await admin.request(`/api/admin/admission-school-accounts/${managedAdmissionAccount.id}`, { method: 'PATCH', body: { active: false } });
|
||||
assert.equal(disabledAdmissionAccount.data.account.active, false, '超级管理员应能停用招生学校账户');
|
||||
assert.equal((await admissionSchoolClient.request('/api/admission/context')).response.status, 401, '停用招生学校账户应立即使既有会话失效');
|
||||
assert.equal((await admin.request(`/api/admin/admission-school-accounts/${managedAdmissionAccount.id}`, { method: 'PATCH', body: { active: true } })).data.account.active, true, '超级管理员应能重新启用招生学校账户');
|
||||
const resetAdmissionPassword = await admin.request(`/api/admin/admission-school-accounts/${managedAdmissionAccount.id}/reset-password`, { method: 'POST' });
|
||||
assert.match(resetAdmissionPassword.data.temporaryPassword, /^Reset-/, '重置招生学校账户密码应返回一次性临时密码');
|
||||
assert.equal((await createClient().request('/api/auth/login', { method: 'POST', body: { username: managedAdmissionAccount.username, password: 'Admission123!' } })).response.status, 401, '招生学校账户重置后原密码应失效');
|
||||
assert.equal((await createClient().request('/api/auth/login', { method: 'POST', body: { username: managedAdmissionAccount.username, password: resetAdmissionPassword.data.temporaryPassword } })).response.status, 200, '招生学校账户应能使用临时密码重新登录');
|
||||
const createdExamInspector = new DatabaseSync(testDb, { readOnly: true });
|
||||
const createdExamPartition = createdExamInspector.prepare('SELECT * FROM exam_data_partitions WHERE exam_id = ?').get(exam.id);
|
||||
assert.ok(createdExamPartition, '创建考试时应同步登记该场考试的专属物理表');
|
||||
|
||||
Reference in New Issue
Block a user