修正权限

This commit is contained in:
biss committed 2026-07-20 21:27:48 +08:00
1 parent c31602593a
commit 76a1b66f53
12 files changed
+584 -12

No files matched your search

+8
View File
@@ -121,7 +121,9 @@ GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER ON exam_information.* TO 'ex
启动应用时设置连接信息,应用会自动创建以下关系表和系统基础配置,但不会自动写入测试业务数据:
- `schools`、`school_classes`、`users`、`candidate_profiles`
- `school_student_partitions`(学校学生专属表登记)
- `exams`、`exam_subjects`
- `exam_data_partitions`(考试专属表登记)
- `registrations`、`registration_subjects`
- `admission_number_rules`、`exam_arrangement_plans`、`admit_cards`、`admit_card_subjects`
- `results`、`notices`、`audit_logs`
@@ -133,6 +135,12 @@ GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER ON exam_information.* TO 'ex
所有关联均有外键约束,账号、证件号、考试代码、报名关系、准考证号和单科成绩均有对应唯一约束。
系统采用“总表索引 + 独立物理分表”存储:创建每场考试时,会立即创建该场考试专用的
`exam_<分区键>_candidates`、`exam_<分区键>_admissions`、`exam_<分区键>_results`、
`exam_<分区键>_centers` 四张表;创建每所学校时,会创建 `school_<分区键>_students`
学生专属表。分区键由业务 ID 的 SHA-256 摘要生成,不直接拼接用户输入。报名、缴费、准考证编排、
成绩和学生资料发生变化后,专属表会自动同步;总表继续承担跨考试、跨学校查询和外键完整性约束。
```powershell
$env:NODE_ENV = 'production'
$env:DATABASE_CLIENT = 'mysql'
+15
View File
@@ -137,6 +137,13 @@ document.addEventListener('click', async event => {
if (action === 'new-notice') { await openNoticeForm(); return; }
if (action === 'new-exam') return openExamForm();
if (action === 'new-admin') return openAdminForm();
if (action === 'new-school') return openSchoolForm();
if (action === 'edit-school') return openSchoolForm(state.pageData.schools.find(item => item.id === target.dataset.id));
if (action === 'toggle-school') {
const active = target.dataset.active === 'true';
await api(`/api/admin/schools/${target.dataset.id}`, { method: 'PATCH', body: { active } });
toast(active ? '学校已启用' : '学校已停用', active ? '考生公开入口已恢复显示' : '公开入口已隐藏,班级、管理员和历史数据均已保留'); return renderRoute();
}
if (action === 'new-school-class') return openSchoolClassForm();
if (action === 'edit-school-class') return openSchoolClassForm(state.pageData.classes.find(item => item.id === target.dataset.id));
if (action === 'toggle-school-class') {
@@ -428,6 +435,10 @@ document.addEventListener('submit', async event => {
const body = formObject(form);
await api('/api/admin/admins', { method: 'POST', body });
closeModal(); toast('管理员已创建', '权限范围已按层级绑定'); renderRoute();
} else if (kind === 'school-form') {
const body = formObject(form); body.active = form.active.checked;
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();
} else if (kind === 'school-class') {
const body = formObject(form); body.active = form.active.checked;
await api(body.id ? `/api/admin/classes/${body.id}` : '/api/admin/classes', { method: body.id ? 'PATCH' : 'POST', body });
@@ -546,6 +557,10 @@ function openAdminForm() {
setModal(`<div class="modal-head"><div><span>ADMIN SCOPE</span><h2>添加分级管理员</h2><p>同一级可以创建多个账号;校级和班级管理员必须绑定数据范围。</p></div><button data-action="close-modal">×</button></div><form class="modal-form" data-form="admin-form"><div class="field-row"><label><span>姓名 *</span><input name="displayName" required></label><label><span>管理员层级 *</span><select name="adminLevel" data-action="admin-level"><option value="school">校级管理员</option><option value="class">班级管理员</option><option value="super">超级管理员</option></select></label></div><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 data-admin-school><span>绑定学校</span><select name="schoolId" data-action="school-select"><option value="">请选择学校</option>${schools.map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><label class="hidden" data-admin-class><span>绑定班级</span><select name="classId"><option value="">请先选择学校</option>${classes.map(item => `<option value="${h(item.id)}">${h(item.name)}</option>`).join('')}</select></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="close-modal">取消</button><button type="submit" class="solid-button">创建管理员</button></div></form>`);
}
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>`);
}
function openSchoolClassForm(schoolClass = null) {
setModal(`<div class="modal-head"><div><span>SCHOOL CLASS</span><h2>${schoolClass ? '编辑本校班级' : '新增本校班级'}</h2><p>班级是考生资料、报名和成绩权限的最小范围。</p></div><button data-action="close-modal">×</button></div><form class="modal-form" data-form="school-class">${schoolClass ? `<input type="hidden" name="id" value="${h(schoolClass.id)}">` : ''}<div class="field-row"><label><span>年级 *</span><input name="grade" required value="${h(schoolClass?.grade || '')}" placeholder="例如 高三"></label><label><span>班级名称 *</span><input name="name" required value="${h(schoolClass?.name || '')}" placeholder="例如 高三(3)班"></label></div><label class="agreement"><input type="checkbox" name="active" ${schoolClass?.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">${schoolClass ? '保存班级' : '创建班级'}</button></div></form>`);
}
+15 -1
View File
@@ -9,10 +9,12 @@ export const relationalTables = [
'organization',
'schools',
'school_classes',
'school_student_partitions',
'users',
'candidate_profiles',
'notices',
'exams',
'exam_data_partitions',
'exam_subjects',
'registrations',
'registration_subjects',
@@ -57,7 +59,7 @@ export function buildSeedOperations(state) {
const nullable = value => value == null || value === '' ? null : value;
add(
'UPDATE schema_metadata SET schema_version = 15, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
'UPDATE schema_metadata SET schema_version = 16, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
Number(state.meta?.version || 1), state.settings?.selfRegistrationEnabled ? 1 : 0,
state.meta?.createdAt || new Date().toISOString()
);
@@ -1169,6 +1171,18 @@ function createRepository({ client, location, read, transaction, close }) {
auditOperation(log)
]);
},
async saveSchool(school, isNew, log) {
const change = isNew
? operation(
'INSERT INTO schools (id, name, code, address, active) VALUES (?, ?, ?, ?, ?)',
school.id, school.name, school.code, optional(school.address), school.active ? 1 : 0
)
: operation(
'UPDATE schools SET name = ?, code = ?, address = ?, active = ? WHERE id = ?',
school.name, school.code, optional(school.address), school.active ? 1 : 0, school.id
);
await transaction([change, auditOperation(log)]);
},
async saveSchoolClass(schoolClass, isNew, log) {
const change = isNew
? operation(
Loaded 3 of 12 files, more files were not shown because too many files have changed in this diff. Show more