feat(rbac): 成绩录入/导入放开到本班班主任(需求 6 与需求 7 口径统一)
依据: 需求 6 明确要求'成绩与表格导入……老师端与学生端班级账号均可用且不越权',
需求 7 的权限清单并未把成绩收归管理员专属(只限制班级增删改、人员分配与科任权限)。
变更:
- shared/permissions: canManageGrades(classRole) = ADMIN | HEAD;canImport 按 kind 分发
(成绩=ADMIN|HEAD、名单=仅管理员、课表时间=ADMIN|HEAD);PERMISSION_MATRIX 同步
- lib/access: assertCanManageGrades(user, classId) 异步按班级角色判定,科任/跨班给出针对性提示
- grades 服务与路由、imports 的模板/预览/提交路由放宽为 ADMIN|TEACHER,
服务层按 kind + 班级二次校验(名单仍仅管理员,科任仍 403)
- Web 管理端: 成绩录入菜单对教师可见;写入/导入按钮仅对'管理员或所选班级班主任'显示
验证:
- verify:e2e 133/133(新增: 班主任录入本班成绩 201 + 跨班 403、班主任模板/预览/导入本班成绩可用、
科任导入 403、班主任名单模板与导入 403、append 重复导入跳过)
- verify:web 19/19(新增: 班主任可见成绩导入入口并打开弹窗,越权入口仍隐藏、菜单 5 项)
- verify:desktop 47/47(未受影响)
文档: README 权限矩阵与'需求 6/7 口径统一'对照表、各项计数更新
This commit is contained in:
1 parent
21485ea294
commit
97a89de944
11 files changed
+359
-89
No files matched your search
@@ -39,10 +39,11 @@ const MENU_ITEMS = [
|
||||
{ label: '课表管理', path: '/schedules' },
|
||||
{ label: '作业发布', path: '/homeworks' },
|
||||
{ label: '通知发布', path: '/notifications' },
|
||||
{ label: '成绩录入', path: '/grades' },
|
||||
];
|
||||
|
||||
/** 教师端必须隐藏的入口(前端隐藏 + 后端 403,双重保障) */
|
||||
const HIDDEN_MENU_LABELS = ['班级管理', '学生管理', '成绩录入'];
|
||||
const HIDDEN_MENU_LABELS = ['班级管理', '学生管理'];
|
||||
|
||||
const results = [];
|
||||
function record(name, ok, detail = '') {
|
||||
@@ -355,7 +356,7 @@ async function main() {
|
||||
return { labels, leaked };
|
||||
})()`);
|
||||
record(
|
||||
'教师端隐藏无权入口(班级管理/学生管理/成绩录入)',
|
||||
'教师端隐藏无权入口(班级管理/学生管理)',
|
||||
(hiddenCheck?.leaked ?? ['?']).length === 0,
|
||||
`可见菜单=[${(hiddenCheck?.labels ?? []).join(',')}] 越权入口=[${(hiddenCheck?.leaked ?? []).join(',')}]`,
|
||||
);
|
||||
@@ -413,6 +414,56 @@ async function main() {
|
||||
})()`);
|
||||
await sleep(300);
|
||||
|
||||
// 7.65 需求 6:班主任(teacher1)在「成绩录入」页看到导入入口,且弹窗真实可用
|
||||
const gradesImportUi = await win.webContents.executeJavaScript(`(async () => {
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const menu = Array.from(document.querySelectorAll('.el-menu-item')).find((node) =>
|
||||
(node.textContent ?? '').trim().startsWith('成绩录入'),
|
||||
);
|
||||
if (!menu) return { ok: false, reason: '班主任看不到「成绩录入」入口(需求 6 未生效)' };
|
||||
menu.click();
|
||||
const deadline = Date.now() + 6000;
|
||||
while (Date.now() < deadline && location.pathname !== '/grades') await sleep(80);
|
||||
await sleep(900);
|
||||
|
||||
const importButton = Array.from(document.querySelectorAll('button')).find((node) =>
|
||||
(node.textContent ?? '').trim() === '导入表格',
|
||||
);
|
||||
if (!importButton) return { ok: false, reason: '成绩页没有「导入表格」按钮(班主任应可导入本班成绩)' };
|
||||
importButton.click();
|
||||
await sleep(700);
|
||||
|
||||
const dialog = Array.from(document.querySelectorAll('.el-dialog')).find((node) =>
|
||||
(node.querySelector('.el-dialog__title')?.textContent ?? '').includes('导入成绩表格'),
|
||||
);
|
||||
if (!dialog) return { ok: false, reason: '导入弹窗未打开' };
|
||||
|
||||
const buttons = Array.from(dialog.querySelectorAll('button')).map((node) => (node.textContent ?? '').trim());
|
||||
return {
|
||||
ok: true,
|
||||
hasExcelTemplate: buttons.some((text) => text.includes('下载模板(Excel)')),
|
||||
hasCsvTemplate: buttons.some((text) => text.includes('下载模板(CSV)')),
|
||||
hasFileInput: Boolean(dialog.querySelector('input[type=file]')),
|
||||
path: location.pathname,
|
||||
};
|
||||
})()`);
|
||||
record(
|
||||
'班主任可用成绩导入入口(模板下载 + 选择文件)',
|
||||
Boolean(gradesImportUi?.ok) &&
|
||||
gradesImportUi?.hasExcelTemplate === true &&
|
||||
gradesImportUi?.hasCsvTemplate === true &&
|
||||
gradesImportUi?.hasFileInput === true,
|
||||
`path=${gradesImportUi?.path ?? '-'} Excel模板=${gradesImportUi?.hasExcelTemplate} CSV模板=${gradesImportUi?.hasCsvTemplate} 文件选择=${gradesImportUi?.hasFileInput}` +
|
||||
`${gradesImportUi?.reason ? ` 原因=${gradesImportUi.reason}` : ''}`,
|
||||
);
|
||||
await win.webContents.executeJavaScript(`(() => {
|
||||
const buttons = Array.from(document.querySelectorAll('.el-dialog__footer button'));
|
||||
const close = buttons.find((node) => (node.textContent ?? '').trim() === '关闭');
|
||||
if (close) close.click();
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(300);
|
||||
|
||||
// 7.7 ClassIsland 时间配置导入弹窗(班主任可见:粘贴 JSON → 解析预览 → 确认导入)
|
||||
const layoutDialog = await win.webContents.executeJavaScript(`(async () => {
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
Reference in new issue
Block a user