feat(courseware,docs): 完善课件传输与岛编辑并统一全仓文档
- 增加课件上传、班级可见范围、设备补收与传输进度 - 整合自定义灵动岛编辑器和相关客户端交互 - 同步三端契约、数据库迁移、部署配置与隔离回归 - 按源码统一文档与官网说明,移除过时任务书和审查记录 - 保持产品版本为 1.4.0
This commit is contained in:
1 parent
536379036a
commit
c7f215a09c
127 files changed
+4334
-8482
No files matched your search
+50
-191
@@ -1,210 +1,69 @@
|
||||
---
|
||||
title: 角色与权限
|
||||
description: 权限矩阵、科目维度的收口规则,以及"前端隐藏 ≠ 权限"这条纪律。
|
||||
description: 功能权限、岗位范围与班级科目边界。
|
||||
---
|
||||
|
||||
# 角色与权限
|
||||
|
||||
## 矩阵在哪
|
||||
授权分为功能权限、数据范围和业务关系三层,任何一层拒绝都不能操作。
|
||||
shared 的 `permissions.ts` 定义键、标签、默认模板与班级关系规则;服务端 `lib/permissions.ts`、
|
||||
`lib/permission-scope.ts`、`lib/access.ts` 分别计算生效键、查询范围和具体业务断言。
|
||||
|
||||
**唯一一份**在 `packages/shared/src/permissions.ts`:
|
||||
## 身份与权限组
|
||||
|
||||
- 后端 `lib/access.ts` 复用它做断言;
|
||||
- 两端前端用它隐藏入口;
|
||||
- 验收脚本按它逐条断言。
|
||||
账号角色为 ADMIN、TEACHER;班级登录形成 CLASS_DEVICE 会话。学生是 Student 名单,不是账号。
|
||||
HEAD/SUBJECT 是班级关系,级长/部长是岗位组,均不新增 User.role。
|
||||
|
||||
所以不存在"前端一套后端一套"。改权限就改这一个文件(改完记得 `pnpm build:shared`)。
|
||||
| 情况 | 有效功能权限 |
|
||||
| ------------ | ----------------------------------------------------- |
|
||||
| 已分配权限组 | 所有分组键的并集,可以比角色默认更少 |
|
||||
| 没有分组 | 数据库对应角色的可编辑默认组 |
|
||||
| ADMIN | 同上,始终额外保留 permission:manage 与 system:manage |
|
||||
| CLASS_DEVICE | 空集;通过明确允许班级会话的接口与业务断言操作 |
|
||||
|
||||
## 角色判定
|
||||
每请求读取生效权限,编辑后无需等待令牌过期。内置组可改不可删,默认组不是写死的全权限快捷方式。
|
||||
路由使用 requirePermission(AND)、requireAnyPermission(OR),员工与班级端共享读取入口按 requireStaffPermission 等规则检查。
|
||||
具体 API 的额外角色限制以模块路由和服务断言为准,不能把“默认只分配给管理员”写成所有接口硬编码 ADMIN。
|
||||
|
||||
角色只有三种,**没有 `STUDENT`** —— 学生不是账号(见[数据模型](data-model.md))。
|
||||
班级内的角色由「账号角色 + 与该班级的关系」共同决定:
|
||||
## 数据范围与科目
|
||||
|
||||
| 角色 | 判定方式 | 可见范围 |
|
||||
| --------------------- | ---------------------------------------- | -------------------------- |
|
||||
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
|
||||
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
|
||||
| 科任老师 `SUBJECT` | 该班存在 `Course.teacherId === 当前用户` | 被任课的班级 |
|
||||
| `CLASS_DEVICE` 班级端 | 班级码 + 班级密码登录(`classSession`) | 本班(代全班读写个人数据) |
|
||||
| `NONE` | 与这个班没有任何关系 | 什么都看不到 |
|
||||
| 岗位/身份 | 范围 |
|
||||
| ----------- | --------------------------------------------- |
|
||||
| 管理员 | 全部班级;仍须功能键 |
|
||||
| 班主任/教师 | 自己担任班主任或任课的班级,按分组 scope 收敛 |
|
||||
| 级长 | 配置年级 |
|
||||
| 部长 | 配置的一组年级,scopeValue 用逗号分隔 |
|
||||
| 班级端 | 登录绑定班级 |
|
||||
|
||||
判定函数是 `resolveClassRole()`。「科任」的判定源是 **`Course`**(班级 + 科目 + 教师),
|
||||
不是那张已经没有的 `ClassTeacher` 协作表。
|
||||
空年级/部配置不放行。设备写操作只合并含对应功能键的分组范围,不能借只读组扩权。
|
||||
科目编辑再由 `assertCanManageSubjectContent` 检查:管理员可管理,教师仅自己任课科目,无科目内容归班主任。
|
||||
班主任不任教某科就不能改该科的作业或成绩;班级端只允许作业写入,不能写成绩。
|
||||
|
||||
## 权限矩阵
|
||||
| 业务 | 主要功能键与额外边界 |
|
||||
| ---------------- | ---------------------------------------------------------------------------- |
|
||||
| 名单读取/管理 | roster:read / roster:manage;管理仍须管理员或本班班主任断言 |
|
||||
| 教师列表/维护 | teacher:read / teacher:manage;导入名单另检查导入权限和角色 |
|
||||
| 日课表写入 | schedule:write + 管理员/本班班主任 |
|
||||
| 全校时间表写入 | timetable:manage;默认给 ADMIN |
|
||||
| 作业/成绩 | homework:write / grade:write 或 grade:import,再按班级科目收口 |
|
||||
| 通知/叫人 | notification:publish / call:create,再检查班级范围 |
|
||||
| 课件 | courseware:read / courseware:write,上传和推送自己的文件还受本人授课班级约束 |
|
||||
| 客户端维护/远控 | client:read、client:manage、remote:control,写操作按对应键范围 |
|
||||
| 版本与安装包写入 | client:update;旧 client:update-push 不再作为写入口要求 |
|
||||
| 数据库/系统/权限 | database:manage / system:manage / permission:manage |
|
||||
|
||||
下表描述角色与班级关系允许的范围;员工还必须拥有对应功能权限,管理员也不例外。
|
||||
名单读取使用 `roster:read` / `teacher:read`,管理使用独立的 `roster:manage` / `teacher:manage`。
|
||||
日志与客户端开发者模式不再单列无效权限键,服务端系统设置由 `system:manage` 控制。
|
||||
完整键目录读取 `PERMISSION_KEYS`,不在文档维护固定键数。
|
||||
|
||||
| 操作 | 管理员 | 班主任 | 科任老师 | 班级端 |
|
||||
| ----------------------------------------------------------- | :----: | :----------: | :------: | :--------: |
|
||||
| 班级创建 / 修改 / 删除 | ✅ | ❌ | ❌ | ❌ |
|
||||
| 分配班主任与各科任课老师 | ✅ | ❌ | ❌ | ❌ |
|
||||
| 学生名单管理(增删 / 导入 / 调班 / 转出) | ✅ | ✅(仅本班) | ❌ | ❌ |
|
||||
| 教师录入(新建 / 导入名单 / 修改密码) | ✅ | ❌ | ❌ | ❌ |
|
||||
| 毕业归档 / 学年升级 / 学期周次 | ✅ | ❌ | ❌ | ❌ |
|
||||
| 数据库管理(备份 / 导入导出 / 一键切换) | ✅ | ❌ | ❌ | ❌ |
|
||||
| 课表管理(增删改 / 时间配置导入) | ✅ | ✅(仅本班) | ❌ | ❌ |
|
||||
| 布置作业(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ✅ |
|
||||
| 叫人 / 发布通知 | ✅ | ✅ | ✅ | ✅ |
|
||||
| 成绩录入 / 修改 / 导入(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ❌ |
|
||||
| 查看本班成绩汇总 | ✅ | ✅ | ✅ | ✅ |
|
||||
| 按学号查询学生成绩明细 | ✅ | ✅ | ✅ | 需班级开关 |
|
||||
| 查看 ClassHelper 班级端在线状态 | ✅ | ✅ | ✅ | ❌ |
|
||||
| 班级密码 / 班级码维护(`PATCH /classes/:id/class-account`) | ✅ | ❌ | ❌ | ❌ |
|
||||
## 前端门控与成绩隐私
|
||||
|
||||
## 科目维度的收口(本次重构的核心规则)
|
||||
Web 菜单属性来自路由,`meta.roles` 粗筛、`meta.permission` 细筛;先 ensurePermissions 再按生效集合判断。
|
||||
服务端仍检查功能、范围与业务关系,直接构造请求不能绕过。
|
||||
岗位范围可以扩大读范围,不能自动扩大科目写权限,也不能替代课件的本人任课关系。
|
||||
班级成绩查询开关仅员工按业务权限修改;关闭后班级端仍看集体聚合,个人明细、原始全班成绩和排名返回拒绝。
|
||||
稀疏分段与补集抑制,服务端不向客户端下发个人原始成绩再聚合。
|
||||
|
||||
作业与成绩不是"凡是能进这个班就能改",而是**再按科目收一道**:
|
||||
## 设备维护
|
||||
|
||||
| 情形 | 谁可以写 |
|
||||
| --------------------------- | ------------------------------------------------------ |
|
||||
| 管理员 | 全部 |
|
||||
| 挂了科目(`courseId` 有值) | **该科的任课老师**(`Course.teacherId`) |
|
||||
| 没挂科目(`courseId` 为空) | 本班**班主任** |
|
||||
| ClassHelper 班级端 | 只有**作业**可以(老师在教室随手录一条);**成绩不给** |
|
||||
|
||||
于是:班主任若不教这一科,就改不了这一科的作业与成绩 —— 这正是需求
|
||||
「班主任不自动拥有所有科目作业成绩编辑权,只有同时是该科科任老师时才有该科权限」。
|
||||
|
||||
实现在 `lib/access.ts` 的 **`assertCanManageSubjectContent(user, classId, courseId, action, { allowClassDevice })`**,
|
||||
作业 / 成绩 / 叫人三处写入统一走它。
|
||||
|
||||
## 权限键与权限组(2026-10 起:权限组驱动,角色只是默认模板)
|
||||
|
||||
从权限组重构开始,**功能级准入不再看角色,而看权限键**:键目录同样是
|
||||
`packages/shared/src/permissions.ts` 的 `PERMISSION_KEYS`(形如 `domain:action`,
|
||||
中文名在 `PERMISSION_LABELS`、分组在 `PERMISSION_CATEGORIES`)。路由上挂的是
|
||||
|
||||
```ts
|
||||
requirePermission('notification:publish'); // 需要全部列出的键
|
||||
requireAnyPermission('grade:write', 'grade:import'); // 有任意一个即可
|
||||
```
|
||||
|
||||
**生效规则(四句话)**:
|
||||
|
||||
| 账号 | 有效权限 |
|
||||
| ------------------------ | ----------------------------------------------------------------------- |
|
||||
| `ADMIN` | 按权限组计算;其中 `permission:manage` / `system:manage` **不可被剥夺** |
|
||||
| `CLASS_DEVICE`(班级端) | 空集,**不受任何分组影响**(它的能力来自班级维度断言) |
|
||||
| 被分配了权限分组 | 分组键的**并集**(可以比角色默认更少 —— 这就是"能收权") |
|
||||
| 没有被分配任何分组 | 使用可编辑的角色默认权限组 |
|
||||
|
||||
默认模板逐条镜像重构前的 `requireRole`,所以"不动分组"= 行为与以前完全一致。
|
||||
实现见 `packages/server/src/lib/permissions.ts` 的 `loadEffectivePermissions()`:它**每个请求实时查库**
|
||||
(不缓存),因此管理员改完分组**立刻生效**,不需要等令牌过期 —— 与下面「鉴权中间件会回查数据库」是同一个理由。
|
||||
|
||||
**管理接口**(`packages/server/src/modules/permissions/`):
|
||||
|
||||
| 方法与路径 | 权限要求 | 说明 |
|
||||
| --------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------- |
|
||||
| `GET /api/permissions/me` | 登录即可 | 当前账号的有效权限与来源(前端据此显示/隐藏入口) |
|
||||
| `GET /api/permissions/catalog` | `permission:manage` | 全部权限键(含中文名、分类、默认角色、不可移除标记) |
|
||||
| `GET/POST /api/permissions/groups`、`PATCH/DELETE /groups/:id` | `permission:manage` | 分组 CRUD;内置分组(`builtin=true`)可改不可删 |
|
||||
| `GET /api/permissions/users`、`PUT /api/permissions/users/:id/groups` | `permission:manage` | 成员列表与「整组替换」式分配;管理员与教师可分配;班级端不参与 |
|
||||
|
||||
**一条不能松的边界**:权限组只管**功能/路由级**准入;**班级维度的科目规则不可被权限组覆盖**。
|
||||
换句话说,给某人授了 `homework:write` 只代表"他能进布置作业这条路由",
|
||||
**具体到哪个班、哪个科目**仍然由 `assertCanManageSubjectContent` 判(下一节)。
|
||||
否则「班主任能布置别人科目的作业」这个老 bug 会从权限组这条新路重新长出来。
|
||||
|
||||
## 实现要点
|
||||
|
||||
**路由级只做权限过滤**:`requirePermission('…')`(需要列出的键全有)或
|
||||
`requireAnyPermission('…')`(任意一个即可)。改造前这里是 `requireRole('ADMIN','TEACHER')`,
|
||||
按键目录里的 `PERMISSION_DEFAULT_ROLES` 逐条映射过来,因此**默认行为没有变化**。
|
||||
403 的文案会直接点名缺哪个键:「当前账号缺少「X」权限,请联系管理员在「权限组」里分配」。
|
||||
|
||||
**班级维度的判断在服务层**:`src/lib/access.ts` 提供
|
||||
|
||||
```
|
||||
assertCanManageClasses / assertCanAssignTeachers / assertCanManageRoster
|
||||
assertCanManageSchedule / assertCanPublishNotification
|
||||
assertCanManageGrades / assertCanManageSubjectContent
|
||||
```
|
||||
|
||||
**列表数据用 `resolveClassScope()` 收敛**(`classId in [...]`),单条接口直接断言;
|
||||
越权一律 403。
|
||||
|
||||
**鉴权中间件会回查数据库**(`middleware/auth.ts`):JWT 校验之后重新读一次账号,
|
||||
所以改了角色 / 班级关系会**立即生效**,不用等令牌过期。班级端会话则回查 `Class` 表。
|
||||
|
||||
**班级端的成绩查询开关**:`Class.studentGradeQueryEnabled`(默认开启)。
|
||||
关闭后班级端调 `GET /grades/student/:studentNo` 一律 403,教师端不受影响 ——
|
||||
这就是需求「是否允许学生通过班级端查看本人明细,由教师或管理员开关控制」。
|
||||
|
||||
## 前端隐藏 ≠ 权限
|
||||
|
||||
前端的隐藏只是"别让人看见入口":
|
||||
|
||||
- 教师登录时侧边栏不显示「班级管理 / 学生管理 / 教师管理 / 毕业归档 / 学期周次 / 数据库管理 / 权限组 / 客户端设备」;
|
||||
- 课表页的增删改按钮只对「管理员或本班班主任」显示;
|
||||
- 成绩页的录入 / 删除按钮对教师显示(科任也能用,**具体到某一条由后端按科目判定**)。
|
||||
|
||||
**门控怎么写的**:路由 `meta.roles` 做粗筛(角色),`meta.permission` 做细筛(权限键)——
|
||||
`packages/web-admin/src/router/index.ts` 的 `beforeEach` 会 `await auth.ensurePermissions()`
|
||||
再问 `auth.can(key)`(管理员同样按生效集合判断,仅保留权限和系统管理入口),不通过就 **fail-closed** 回仪表盘;
|
||||
侧栏菜单项同理带 `permission` 字段(`AdminLayout.vue` 的 `visibleMenuItems`)。
|
||||
`auth.ensurePermissions()` 读的就是 `GET /api/permissions/me`。
|
||||
|
||||
但这些页面**直接输网址也进不去**(路由 `meta.roles` 会把非管理员挡回仪表盘),
|
||||
而且**所有写入请求服务端都会再断言一次**。绕过菜单、绕过前端,一样 403。
|
||||
|
||||
> 新增视图时别忘了两处:路由的 `meta.roles` + 服务层对应 service 里的 `assertCanXxx`。
|
||||
> 只做其中一个都等于没做。
|
||||
|
||||
## 学生没有账号的两个反向约束
|
||||
|
||||
1. **学生不能登录,而且根本不存在可以登录的学生账号**。`User` 表里没有学生行,
|
||||
拿学号当账号登录只会得到 401("工号或密码错误"),连"这个学号存在吗"都探测不出来。
|
||||
2. **学生没有任何密码接口**。`POST /students/:id/reset-password` 已删除(调用返回 404),
|
||||
新建学生也不设密码 —— `Student` 表里压根没有那一列。
|
||||
|
||||
## 默认权限组与客户端维护(2026-10-16)
|
||||
|
||||
管理员和教师各有一个默认权限组;未分配其他组时使用对应默认组,编辑后立即生效。
|
||||
默认组可以改名、增减权限,但不能删除。分配多个组时取并集。
|
||||
管理员始终保留 `permission:manage` 和 `system:manage`,防止失去管理入口。
|
||||
班级端不参与账号权限组,班级与科目范围的检查保持生效。
|
||||
|
||||
「客户端」页统一显示设备心跳、版本、是否接受自动更新、最近指令结果和安装时机。
|
||||
更新先从本服务器下载并校验 SHA256,再按所选时机安装:
|
||||
|
||||
- 立即更新:下载完成后安装并重启客户端,会中断当前客户端界面。
|
||||
- 放学后更新:按服务器当天日期,等待所选最早时间与本班最后一节课结束时间的较晚者;上课中继续等待。
|
||||
- 空闲时更新:不上课且主窗口隐藏满五分钟后安装。
|
||||
|
||||
离线设备上线后补领更新;取消下发后,已下载的安装包不再自动安装。
|
||||
客户端关闭自动更新时,任何下发策略都不会强制安装。
|
||||
远控由管理端确认下发;客户端立即执行白名单动作并回传真实执行结果。
|
||||
|
||||
联动插件及相关接口已下线;升级迁移会删除旧联动设备和推送记录,保留业务课表、通知及离线 JSON 导入。
|
||||
服务端兼容旧客户端的心跳字段读取,旧设备可领取修复版;第一次升级仍遵循旧版空闲安装条件,新策略需修复版客户端。
|
||||
|
||||
## 日课表与岗位范围(2026-10-17)
|
||||
|
||||
| 能力 | 新权限键 | 默认角色 |
|
||||
| -------------------------- | ------------------ | -------- |
|
||||
| 全校时间表与时间点管理 | `timetable:manage` | ADMIN |
|
||||
| 安装包、目标版本与安装策略 | `client:update` | ADMIN |
|
||||
|
||||
`client:update-push` 只保留旧配置兼容标识;下发与安装包写入口统一检查 `client:update`。
|
||||
新增权限同步出现在 shared 的键目录、标签、类别与默认角色表;升级迁移为管理员默认组补齐新键。
|
||||
|
||||
| 内置教师岗位组 | 默认功能 | 数据范围 |
|
||||
| -------------- | ---------------------------------------- | -------------------------------- |
|
||||
| 班主任 | 教学内容 + 查看/管理设备,不含版本下发 | 自己担任班主任的班级 |
|
||||
| 教师 | 作业、成绩、通知、叫人、课表与名单读取 | 班主任与任教班级 |
|
||||
| 级长 | 设备、远控、名单/班级/课表/成绩/看板读取 | 管理员配置的年级 |
|
||||
| 部长 | 同级长 | 管理员配置的一组年级(逗号分隔) |
|
||||
|
||||
四组 `builtin=true`、`defaultRole=null`;账号仍是 TEACHER。实际范围来自分组并集,
|
||||
空年级/部范围不授权。通用查询通过 `teacherScopeWhere/resolveClassScope`,
|
||||
设备写入还只合并含对应功能键的分组范围。管理员的 `permission:manage/system:manage` 不可剥夺。
|
||||
范围从不赋予科目编辑权,仍须 `assertCanManageSubjectContent` 检查 Course 任课关系。
|
||||
|
||||
教师名单页复用学生管理视图与导出,路由 `roster:read`,隐藏编辑/调班/导入操作;服务端过滤范围和转出学生。
|
||||
成绩关闭个人查询时只返回服务端聚合;班级端无个人明细、排名或极值,稀疏样本抑制,教师/管理员不受该个人查询开关影响。
|
||||
|
||||
旧时间表导入接口的写入/删除也要求 `timetable:manage`,避免绕过统一时间表权限;离线整套课表导入仍可由本班班主任使用,创建班级自有时间点。
|
||||
`/api/clients/heartbeat` 和命令回执供已登录设备使用,不依赖班级端的空权限集合;服务端校验 deviceKey 与班级归属。
|
||||
远控从管理端确认下发,客户端执行后回传真实结果。更新文件由服务端缓存,教室机验 SHA256,
|
||||
按立即、放学后、空闲策略安装;关闭自动更新不会强制安装。详见[更新与升级](../management/upgrade.md)。
|
||||
Reference in new issue
Block a user