chore(release): 1.3.3 发布并退役旧联动

This commit is contained in:
LoveChengke committed 2026-10-17 03:06:46 +08:00
1 parent b7cdc7c9ea
commit c0ebf88138
184 files changed
+5450 -11705

No files matched your search

+70 -45
View File
@@ -20,46 +20,50 @@ description: 权限矩阵、科目维度的收口规则,以及"前端隐藏
角色只有三种,**没有 `STUDENT`** —— 学生不是账号(见[数据模型](data-model.md))。
班级内的角色由「账号角色 + 与该班级的关系」共同决定:
| 角色 | 判定方式 | 可见范围 |
| --------------------------- | ------------------------------------------ | ------------------------------------ |
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
| 科任老师 `SUBJECT` | 该班存在 `Course.teacherId === 当前用户` | 被任课的班级 |
| `CLASS_DEVICE` 班级端 | 班级码 + 班级密码登录(`classSession`) | 本班(代全班读写个人数据) |
| `NONE` | 与这个班没有任何关系 | 什么都看不到 |
| 角色 | 判定方式 | 可见范围 |
| --------------------- | ---------------------------------------- | -------------------------- |
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
| 科任老师 `SUBJECT` | 该班存在 `Course.teacherId === 当前用户` | 被任课的班级 |
| `CLASS_DEVICE` 班级端 | 班级码 + 班级密码登录(`classSession`) | 本班(代全班读写个人数据) |
| `NONE` | 与这个班没有任何关系 | 什么都看不到 |
判定函数是 `resolveClassRole()`。「科任」的判定源是 **`Course`**(班级 + 科目 + 教师),
不是那张已经没有的 `ClassTeacher` 协作表。
## 权限矩阵
| 操作 | 管理员 | 班主任 | 科任老师 | 班级端 |
| -------------------------------------------------------- | :----: | :----------: | :------: | :----: |
| 班级创建 / 修改 / 删除 | ✅ | ❌ | ❌ | ❌ |
| 分配班主任与各科任课老师 | ✅ | ❌ | ❌ | ❌ |
| 学生名单管理(增删 / 导入 / 调班 / 转出) | ✅ | ✅(仅本班) | ❌ | ❌ |
| 教师录入(新建 / 导入名单 / 修改密码) | ✅ | ❌ | ❌ | ❌ |
| 毕业归档 / 学年升级 / 学期周次 | ✅ | ❌ | ❌ | ❌ |
| 数据库管理(备份 / 导入导出 / 一键切换) | ✅ | ❌ | ❌ | ❌ |
| 课表管理(增删改 / 时间配置导入) | ✅ | ✅(仅本班) | ❌ | ❌ |
| 布置作业(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ✅ |
| 叫人 / 发布通知 | ✅ | ✅ | ✅ | ✅ |
| 成绩录入 / 修改 / 导入(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ❌ |
| 查看本班成绩汇总 | ✅ | ✅ | ✅ | ✅ |
| 按学号查询学生成绩明细 | ✅ | ✅ | ✅ | 需班级开关 |
| 查看 ClassHelper 班级端在线状态 | ✅ | ✅ | ✅ | ❌ |
| 班级密码 / 班级码维护(`PATCH /classes/:id/class-account`) | ✅ | ❌ | ❌ | ❌ |
下表描述角色与班级关系允许的范围;员工还必须拥有对应功能权限,管理员也不例外。
名单读取使用 `roster:read` / `teacher:read`,管理使用独立的 `roster:manage` / `teacher:manage`。
日志与客户端开发者模式不再单列无效权限键,服务端系统设置由 `system:manage` 控制。
| 操作 | 管理员 | 班主任 | 科任老师 | 班级端 |
| ----------------------------------------------------------- | :----: | :----------: | :------: | :--------: |
| 班级创建 / 修改 / 删除 | ✅ | ❌ | ❌ | ❌ |
| 分配班主任与各科任课老师 | ✅ | ❌ | ❌ | ❌ |
| 学生名单管理(增删 / 导入 / 调班 / 转出) | ✅ | ✅(仅本班) | ❌ | ❌ |
| 教师录入(新建 / 导入名单 / 修改密码) | ✅ | ❌ | ❌ | ❌ |
| 毕业归档 / 学年升级 / 学期周次 | ✅ | ❌ | ❌ | ❌ |
| 数据库管理(备份 / 导入导出 / 一键切换) | ✅ | ❌ | ❌ | ❌ |
| 课表管理(增删改 / 时间配置导入) | ✅ | ✅(仅本班) | ❌ | ❌ |
| 布置作业(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ✅ |
| 叫人 / 发布通知 | ✅ | ✅ | ✅ | ✅ |
| 成绩录入 / 修改 / 导入(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ❌ |
| 查看本班成绩汇总 | ✅ | ✅ | ✅ | ✅ |
| 按学号查询学生成绩明细 | ✅ | ✅ | ✅ | 需班级开关 |
| 查看 ClassHelper 班级端在线状态 | ✅ | ✅ | ✅ | ❌ |
| 班级密码 / 班级码维护(`PATCH /classes/:id/class-account`) | ✅ | ❌ | ❌ | ❌ |
## 科目维度的收口(本次重构的核心规则)
作业与成绩不是"凡是能进这个班就能改",而是**再按科目收一道**:
| 情形 | 谁可以写 |
| --------------------------------------- | ------------------------------------------ |
| 管理员 | 全部 |
| 挂了科目(`courseId` 有值) | **该科的任课老师**(`Course.teacherId`) |
| 没挂科目(`courseId` 为空) | 本班**班主任** |
| ClassHelper 班级端 | 只有**作业**可以(老师在教室随手录一条);**成绩不给** |
| 情形 | 谁可以写 |
| --------------------------- | ------------------------------------------------------ |
| 管理员 | 全部 |
| 挂了科目(`courseId` 有值) | **该科的任课老师**(`Course.teacherId`) |
| 没挂科目(`courseId` 为空) | 本班**班主任** |
| ClassHelper 班级端 | 只有**作业**可以(老师在教室随手录一条);**成绩不给** |
于是:班主任若不教这一科,就改不了这一科的作业与成绩 —— 这正是需求
「班主任不自动拥有所有科目作业成绩编辑权,只有同时是该科科任老师时才有该科权限」。
@@ -70,22 +74,22 @@ description: 权限矩阵、科目维度的收口规则,以及"前端隐藏
## 权限键与权限组(2026-10 起:权限组驱动,角色只是默认模板)
从权限组重构开始,**功能级准入不再看角色,而看权限键**:键目录同样是
`packages/shared/src/permissions.ts` 的 `PERMISSION_KEYS`(39 个,形如 `domain:action`,
`packages/shared/src/permissions.ts` 的 `PERMISSION_KEYS`(37 个,形如 `domain:action`,
中文名在 `PERMISSION_LABELS`、分组在 `PERMISSION_CATEGORIES`)。路由上挂的是
```ts
requirePermission('notification:publish') // 需要全部列出的键
requireAnyPermission('grade:write', 'grade:import') // 有任意一个即可
requirePermission('notification:publish'); // 需要全部列出的键
requireAnyPermission('grade:write', 'grade:import'); // 有任意一个即可
```
**生效规则(四句话)**:
| 账号 | 有效权限 |
| --------------------------------------- | -------------------------------------------------------------- |
| `ADMIN` | 全集;其中 `permission:manage` / `system:manage` **不可被剥夺** |
| `CLASS_DEVICE`(班级端) | 空集,**不受任何分组影响**(它的能力来自班级维度断言) |
| 被分配了权限分组 | 分组键的**并集**(可以比角色默认更少 —— 这就是"能收权") |
| 没有被分配任何分组 | 回落到角色默认模板 `defaultPermissionsForRole(role)` |
| 账号 | 有效权限 |
| ------------------------ | ----------------------------------------------------------------------- |
| `ADMIN` | 按权限组计算;其中 `permission:manage` / `system:manage` **不可被剥夺** |
| `CLASS_DEVICE`(班级端) | 空集,**不受任何分组影响**(它的能力来自班级维度断言) |
| 被分配了权限分组 | 分组键的**并集**(可以比角色默认更少 —— 这就是"能收权") |
| 没有被分配任何分组 | 使用可编辑的角色默认权限组 |
默认模板逐条镜像重构前的 `requireRole`,所以"不动分组"= 行为与以前完全一致。
实现见 `packages/server/src/lib/permissions.ts` 的 `loadEffectivePermissions()`:它**每个请求实时查库**
@@ -93,12 +97,12 @@ requireAnyPermission('grade:write', 'grade:import') // 有任意一个即可
**管理接口**(`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` | 成员列表与「整组替换」式分配;拒绝给 ADMIN / CLASS_DEVICE 分配 |
| 方法与路径 | 权限要求 | 说明 |
| --------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------- |
| `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` 只代表"他能进布置作业这条路由",
@@ -140,7 +144,7 @@ assertCanManageGrades / assertCanManageSubjectContent
**门控怎么写的**:路由 `meta.roles` 做粗筛(角色),`meta.permission` 做细筛(权限键)——
`packages/web-admin/src/router/index.ts` 的 `beforeEach` 会 `await auth.ensurePermissions()`
再问 `auth.can(key)`(管理员短路为 true),不通过就 **fail-closed** 回仪表盘;
再问 `auth.can(key)`(管理员同样按生效集合判断,仅保留权限和系统管理入口),不通过就 **fail-closed** 回仪表盘;
侧栏菜单项同理带 `permission` 字段(`AdminLayout.vue` 的 `visibleMenuItems`)。
`auth.ensurePermissions()` 读的就是 `GET /api/permissions/me`。
@@ -156,3 +160,24 @@ assertCanManageGrades / assertCanManageSubjectContent
拿学号当账号登录只会得到 401("工号或密码错误"),连"这个学号存在吗"都探测不出来。
2. **学生没有任何密码接口**。`POST /students/:id/reset-password` 已删除(调用返回 404),
新建学生也不设密码 —— `Student` 表里压根没有那一列。
## 默认权限组与客户端维护(2026-10-16)
管理员和教师各有一个默认权限组;未分配其他组时使用对应默认组,编辑后立即生效。
默认组可以改名、增减权限,但不能删除。分配多个组时取并集。
管理员始终保留 `permission:manage` 和 `system:manage`,防止失去管理入口。
班级端不参与账号权限组,班级与科目范围的检查保持生效。
「客户端」页统一显示设备心跳、版本、是否接受自动更新、最近指令结果和安装时机。
更新先从本服务器下载并校验 SHA256,再按所选时机安装:
- 立即更新:下载完成后安装并重启客户端,会中断当前客户端界面。
- 放学后更新:按服务器当天日期,等待所选最早时间与本班最后一节课结束时间的较晚者;上课中继续等待。
- 空闲时更新:不上课且主窗口隐藏满五分钟后安装。
离线设备上线后补领更新;取消下发后,已下载的安装包不再自动安装。
客户端关闭自动更新时,任何下发策略都不会强制安装。
远控关机、重启、注销和锁屏仍需管理端确认及本机确认,取消或系统执行失败都会回执。
联动插件及相关接口已下线;升级迁移会删除旧联动设备和推送记录,保留业务课表、通知及离线 JSON 导入。
服务端兼容旧客户端的心跳字段读取,旧设备可领取修复版;第一次升级仍遵循旧版空闲安装条件,新策略需修复版客户端。