2026-10-06 16:27:44 +08:00
|
|
|
|
---
|
|
|
|
|
|
title: 角色与权限
|
2026-10-06 19:41:18 +08:00
|
|
|
|
description: 权限矩阵、科目维度的收口规则,以及"前端隐藏 ≠ 权限"这条纪律。
|
2026-10-06 16:27:44 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 角色与权限
|
|
|
|
|
|
|
|
|
|
|
|
## 矩阵在哪
|
|
|
|
|
|
|
|
|
|
|
|
**唯一一份**在 `packages/shared/src/permissions.ts`:
|
|
|
|
|
|
|
|
|
|
|
|
- 后端 `lib/access.ts` 复用它做断言;
|
|
|
|
|
|
- 两端前端用它隐藏入口;
|
|
|
|
|
|
- 验收脚本按它逐条断言。
|
|
|
|
|
|
|
|
|
|
|
|
所以不存在"前端一套后端一套"。改权限就改这一个文件(改完记得 `pnpm build:shared`)。
|
|
|
|
|
|
|
|
|
|
|
|
## 角色判定
|
|
|
|
|
|
|
2026-10-06 19:41:18 +08:00
|
|
|
|
角色只有三种,**没有 `STUDENT`** —— 学生不是账号(见[数据模型](data-model.md))。
|
2026-10-06 16:27:44 +08:00
|
|
|
|
班级内的角色由「账号角色 + 与该班级的关系」共同决定:
|
|
|
|
|
|
|
2026-10-06 19:41:18 +08:00
|
|
|
|
| 角色 | 判定方式 | 可见范围 |
|
|
|
|
|
|
| --------------------------- | ------------------------------------------ | ------------------------------------ |
|
|
|
|
|
|
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
|
|
|
|
|
|
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
|
|
|
|
|
|
| 科任老师 `SUBJECT` | 该班存在 `Course.teacherId === 当前用户` | 被任课的班级 |
|
|
|
|
|
|
| `CLASS_DEVICE` 班级端 | 班级码 + 班级密码登录(`classSession`) | 本班(代全班读写个人数据) |
|
|
|
|
|
|
| `NONE` | 与这个班没有任何关系 | 什么都看不到 |
|
2026-10-06 16:27:44 +08:00
|
|
|
|
|
2026-10-06 19:41:18 +08:00
|
|
|
|
判定函数是 `resolveClassRole()`。「科任」的判定源是 **`Course`**(班级 + 科目 + 教师),
|
|
|
|
|
|
不是那张已经没有的 `ClassTeacher` 协作表。
|
2026-10-06 16:27:44 +08:00
|
|
|
|
|
|
|
|
|
|
## 权限矩阵
|
|
|
|
|
|
|
2026-10-06 19:41:18 +08:00
|
|
|
|
| 操作 | 管理员 | 班主任 | 科任老师 | 班级端 |
|
|
|
|
|
|
| -------------------------------------------------------- | :----: | :----------: | :------: | :----: |
|
|
|
|
|
|
| 班级创建 / 修改 / 删除 | ✅ | ❌ | ❌ | ❌ |
|
|
|
|
|
|
| 分配班主任与各科任课老师 | ✅ | ❌ | ❌ | ❌ |
|
|
|
|
|
|
| 学生名单管理(增删 / 导入 / 调班 / 转出) | ✅ | ✅(仅本班) | ❌ | ❌ |
|
|
|
|
|
|
| 教师录入(新建 / 导入名单 / 修改密码) | ✅ | ❌ | ❌ | ❌ |
|
|
|
|
|
|
| 毕业归档 / 学年升级 / 学期周次 | ✅ | ❌ | ❌ | ❌ |
|
|
|
|
|
|
| 数据库管理(备份 / 导入导出 / 一键切换) | ✅ | ❌ | ❌ | ❌ |
|
|
|
|
|
|
| 课表管理(增删改 / 时间配置导入) | ✅ | ✅(仅本班) | ❌ | ❌ |
|
|
|
|
|
|
| 布置作业(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ✅ |
|
|
|
|
|
|
| 叫人 / 发布通知 | ✅ | ✅ | ✅ | ✅ |
|
|
|
|
|
|
| 成绩录入 / 修改 / 导入(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ❌ |
|
|
|
|
|
|
| 查看本班成绩汇总 | ✅ | ✅ | ✅ | ✅ |
|
|
|
|
|
|
| 按学号查询学生成绩明细 | ✅ | ✅ | ✅ | 需班级开关 |
|
|
|
|
|
|
| 查看 ClassHelper 班级端在线状态 | ✅ | ✅ | ✅ | ❌ |
|
|
|
|
|
|
| 班级密码 / 班级码维护(`PATCH /classes/:id/class-account`) | ✅ | ❌ | ❌ | ❌ |
|
|
|
|
|
|
|
|
|
|
|
|
## 科目维度的收口(本次重构的核心规则)
|
|
|
|
|
|
|
|
|
|
|
|
作业与成绩不是"凡是能进这个班就能改",而是**再按科目收一道**:
|
|
|
|
|
|
|
|
|
|
|
|
| 情形 | 谁可以写 |
|
|
|
|
|
|
| --------------------------------------- | ------------------------------------------ |
|
|
|
|
|
|
| 管理员 | 全部 |
|
|
|
|
|
|
| 挂了科目(`courseId` 有值) | **该科的任课老师**(`Course.teacherId`) |
|
|
|
|
|
|
| 没挂科目(`courseId` 为空) | 本班**班主任** |
|
|
|
|
|
|
| ClassHelper 班级端 | 只有**作业**可以(老师在教室随手录一条);**成绩不给** |
|
|
|
|
|
|
|
|
|
|
|
|
于是:班主任若不教这一科,就改不了这一科的作业与成绩 —— 这正是需求
|
|
|
|
|
|
「班主任不自动拥有所有科目作业成绩编辑权,只有同时是该科科任老师时才有该科权限」。
|
|
|
|
|
|
|
|
|
|
|
|
实现在 `lib/access.ts` 的 **`assertCanManageSubjectContent(user, classId, courseId, action, { allowClassDevice })`**,
|
|
|
|
|
|
作业 / 成绩 / 叫人三处写入统一走它。
|
2026-10-06 16:27:44 +08:00
|
|
|
|
|
|
|
|
|
|
## 实现要点
|
|
|
|
|
|
|
|
|
|
|
|
**路由级只做角色过滤**:`requireRole('ADMIN', 'TEACHER')`。
|
|
|
|
|
|
|
|
|
|
|
|
**班级维度的判断在服务层**:`src/lib/access.ts` 提供
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-10-06 19:41:18 +08:00
|
|
|
|
assertCanManageClasses / assertCanAssignTeachers / assertCanManageRoster
|
|
|
|
|
|
assertCanManageSchedule / assertCanPublishNotification
|
|
|
|
|
|
assertCanManageGrades / assertCanManageSubjectContent
|
2026-10-06 16:27:44 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**列表数据用 `resolveClassScope()` 收敛**(`classId in [...]`),单条接口直接断言;
|
|
|
|
|
|
越权一律 403。
|
|
|
|
|
|
|
2026-10-06 19:41:18 +08:00
|
|
|
|
**鉴权中间件会回查数据库**(`middleware/auth.ts`):JWT 校验之后重新读一次账号,
|
|
|
|
|
|
所以改了角色 / 班级关系会**立即生效**,不用等令牌过期。班级端会话则回查 `Class` 表。
|
|
|
|
|
|
|
|
|
|
|
|
**班级端的成绩查询开关**:`Class.studentGradeQueryEnabled`(默认开启)。
|
|
|
|
|
|
关闭后班级端调 `GET /grades/student/:studentNo` 一律 403,教师端不受影响 ——
|
|
|
|
|
|
这就是需求「是否允许学生通过班级端查看本人明细,由教师或管理员开关控制」。
|
2026-10-06 16:27:44 +08:00
|
|
|
|
|
|
|
|
|
|
## 前端隐藏 ≠ 权限
|
|
|
|
|
|
|
|
|
|
|
|
前端的隐藏只是"别让人看见入口":
|
|
|
|
|
|
|
2026-10-06 19:41:18 +08:00
|
|
|
|
- 教师登录时侧边栏不显示「班级管理 / 学生管理 / 教师管理 / 毕业归档 / 学期周次 / 数据库管理」;
|
2026-10-06 16:27:44 +08:00
|
|
|
|
- 课表页的增删改按钮只对「管理员或本班班主任」显示;
|
2026-10-06 19:41:18 +08:00
|
|
|
|
- 成绩页的录入 / 删除按钮对教师显示(科任也能用,**具体到某一条由后端按科目判定**)。
|
2026-10-06 16:27:44 +08:00
|
|
|
|
|
|
|
|
|
|
但这些页面**直接输网址也进不去**(路由 `meta.roles` 会把非管理员挡回仪表盘),
|
|
|
|
|
|
而且**所有写入请求服务端都会再断言一次**。绕过菜单、绕过前端,一样 403。
|
|
|
|
|
|
|
|
|
|
|
|
> 新增视图时别忘了两处:路由的 `meta.roles` + 服务层对应 service 里的 `assertCanXxx`。
|
|
|
|
|
|
> 只做其中一个都等于没做。
|
|
|
|
|
|
|
2026-10-06 19:41:18 +08:00
|
|
|
|
## 学生没有账号的两个反向约束
|
2026-10-06 16:27:44 +08:00
|
|
|
|
|
2026-10-06 19:41:18 +08:00
|
|
|
|
1. **学生不能登录,而且根本不存在可以登录的学生账号**。`User` 表里没有学生行,
|
|
|
|
|
|
拿学号当账号登录只会得到 401("工号或密码错误"),连"这个学号存在吗"都探测不出来。
|
2026-10-06 16:27:44 +08:00
|
|
|
|
2. **学生没有任何密码接口**。`POST /students/:id/reset-password` 已删除(调用返回 404),
|
2026-10-06 19:41:18 +08:00
|
|
|
|
新建学生也不设密码 —— `Student` 表里压根没有那一列。
|