chore(release): 1.3.3 发布并退役旧联动
This commit is contained in:
1 parent
b7cdc7c9ea
commit
c0ebf88138
184 files changed
+5450
-11705
No files matched your search
+100
-109
@@ -10,129 +10,120 @@ description: ClassHelper后端全部接口一览:方法、路径、权限与
|
||||
- 所有接口前缀 `/api`(设备接口也一样);
|
||||
- 统一响应体:`{ "success": true, "data": {}, "message": "" }`,错误为 `success: false` + `code`;
|
||||
- 除登录、健康检查与设备接口外,都需要 `Authorization: Bearer <token>`;
|
||||
- **ClassIsland 设备接口用 `X-ClassIsland-Token`**,不走 JWT(见[ClassIsland 插件](classisland-plugin.md));
|
||||
- 请求体校验走 zod(`middleware/validate.ts`),错误由 `middleware/error.ts` 统一映射。
|
||||
|
||||
## 接口一览
|
||||
|
||||
| 方法 | 路径 | 权限 | 说明 |
|
||||
| --------------------------- | ------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
|
||||
| POST | `/auth/login` | 公开 | 登录(教师 / 管理员的**工号** + 密码);学生不是账号,拿学号登录只会得到 401 |
|
||||
| POST | `/auth/class-login` | 公开 | **班级账号登录**:班级码 + 班级密码 → 班级会话 |
|
||||
| GET | `/auth/me` | 登录 | 当前用户(学生附带班级 / 年级) |
|
||||
| PATCH | `/auth/password` | 登录 | 修改自己的密码(班级会话 → 班级密码) |
|
||||
| POST | `/auth/logout` | 登录 | 退出(无状态,客户端丢弃 token) |
|
||||
| GET | `/permissions/me` | 登录 | 当前账号的生效权限(**无权限键**;班级端会话返回空集) |
|
||||
| GET | `/permissions/catalog` | `permission:manage` | 权限键目录(权限页整棵勾选树的唯一来源) |
|
||||
| GET / POST | `/permissions/groups` | `permission:manage` | 权限组列表 / 新建 |
|
||||
| PATCH / DELETE | `/permissions/groups/:id` | `permission:manage` | 改名 / 改描述 / 整体替换权限键;内置组不可删 |
|
||||
| GET | `/permissions/users` | `permission:manage` | 成员列表(所属权限组 + 生效权限数量) |
|
||||
| PUT | `/permissions/users/:id/groups` | `permission:manage` | 整体替换某账号的权限组(空数组 = 回落角色默认模板) |
|
||||
| GET | `/classes` | 登录 | 班级列表(按权限收敛) |
|
||||
| GET | `/classes/:id` | 班级可见 | 班级详情(学生 / 课程 / 班主任 / 各科任课老师) |
|
||||
| POST | `/classes` | 管理员 | 创建班级(自动生成班级码,可指定班主任) |
|
||||
| PATCH / DELETE | `/classes/:id` | 管理员 / 教师 | 编辑 / 删除班级 |
|
||||
| PATCH | `/classes/:id/class-account` | 管理员 | 设置 / 重置班级账号(班级码 + 班级密码) |
|
||||
| PATCH | `/classes/:id/head-teacher` | 管理员 | 设置 / 更换班主任 |
|
||||
| GET | `/classes/:id/classisland-status` | 班级可见 | 本班 ClassIsland 联动状态(教室客户端设置页用) |
|
||||
| GET | `/classes/:id/notification-channel` | 班级可见 | 读取「通知显示到哪个端」 |
|
||||
| PATCH | `/classes/:id/notification-channel` | 班级账号 / 教师 / 管理员 | 设置「通知显示到哪个端」 |
|
||||
| GET / POST | `/classes/:id/students` | 班级可见 / ADMIN·TEACHER | 学生名单 / 添加学生 |
|
||||
| DELETE | `/classes/:id/students/:userId` | ADMIN·TEACHER | 移出学生 |
|
||||
| PUT | `/classes/:id/subject-teachers` | ADMIN | 设置各科任课老师(班级 + 科目 + 教师) |
|
||||
| GET / POST / PATCH / DELETE | `/courses` | 登录(写:班主任 / 管理员) | 课程管理 |
|
||||
| GET | `/schedules?classId=&week=&dayOfWeek=` | 登录 | 课表列表(`week` 过滤周次范围) |
|
||||
| GET | `/schedules/grid?classId=&week=` | 登录 | 周视图(7 列结构,供客户端直接渲染) |
|
||||
| GET | `/schedules/current?classId=&at=` | 登录 | 当前上课状态(`inClass` / `current` / `next`;`at` 为诊断用时间覆盖) |
|
||||
| POST / PATCH / DELETE | `/schedules[/:id]` | 班主任 / 管理员 | 课表增删改(广播 `schedule:updated`) |
|
||||
| GET | `/homeworks?classId=&courseId=&date=&pendingOnly=&keyword=` | 登录 | 作业列表(支持按所属日期;学生带完成状态,教师带完成人数) |
|
||||
| GET | `/homeworks/days?classId=&from=&to=` | 登录 | 哪些天有作业(日期高亮) |
|
||||
| GET | `/homeworks/:id` | 班级可见 | 作业详情 |
|
||||
| POST | `/homeworks` | 教师 / 管理员 / **班级账号** | 发布作业(附 `assignDate`;班级账号用于教室机器录入,归属班主任) |
|
||||
| PATCH / DELETE | `/homeworks/:id` | 教师 / 管理员 | 修改 / 删除(广播 `homework:new` / `homework:updated`) |
|
||||
| PATCH | `/homeworks/:id/status` | 登录 | 标记完成 / 取消(广播 `homework:status`) |
|
||||
| GET / PATCH | `/homeworks/:id/submissions` | 登录 | 未交名单:读名单 / 回写全班完成状态 |
|
||||
| POST | `/homeworks/bulk` | `homework:write` | 一次给多个班级布置同一份作业(`subjectName` 跨班按名匹配科目;逐班结果) |
|
||||
| GET | `/notifications?classId=&priority=&unreadOnly=&keyword=` | 登录 | 通知列表(带已读状态;另支持 `date` / `from` / `to` / `createdBy`) |
|
||||
| GET | `/notifications/unread-count` | 登录 | 未读数(红点) |
|
||||
| POST | `/notifications` | 教师 / 管理员 | 发布通知(广播 `notification:new`);上课时段发紧急通知需 `confirmDuringClass` |
|
||||
| POST | `/notifications/:id/read`、`/notifications/read-all` | 登录 | 标记已读(班级设备会写全班) |
|
||||
| DELETE | `/notifications/:id` | 教师 / 管理员 | 删除通知 |
|
||||
| GET | `/notifications/days?classId=&from=&to=&createdBy=` | 登录 | 通知按天归档(日期 + 条数,日期取 `assignDate`) |
|
||||
| GET | `/notifications/creators?classId=&from=&to=&date=` | 登录 | 通知按发布人归档 |
|
||||
| POST | `/notifications/bulk` | `notification:publish` | 一次给多个班级发同一条通知(逐班结果,**单班失败不影响整批**) |
|
||||
| POST | `/calls` | 教师 / 管理员 | **叫人**(`urgent` 决定 URGENT / HIGH;广播 `notification:new` 与 `call:new`) |
|
||||
| GET | `/grades/my` | 登录 | 个人成绩(班级会话 → 全班总览) |
|
||||
| GET | `/grades?classId=&courseId=&userId=&examName=` | 教师 / 管理员 | 班级成绩 |
|
||||
| GET | `/grades/stats?classId=&courseId=&examName=` | 教师 / 管理员 | 等级分布 + 各课程平均得分率 |
|
||||
| POST | `/grades`、`/grades/bulk` | 班主任 / 管理员 | 单条 / 批量录入(广播 `grade:updated`) |
|
||||
| PATCH / DELETE | `/grades/:id` | 班主任 / 管理员 | 修改 / 删除成绩 |
|
||||
| GET | `/students?classId=&keyword=` | **管理员** | 学生名单 |
|
||||
| POST / PATCH / DELETE | `/students[/:id]` | **管理员** | 学生名单增删改(无密码概念;重置密码接口已下线 → 404) |
|
||||
| GET | `/teachers?keyword=` | **管理员** | 教师列表 |
|
||||
| POST / PATCH / DELETE | `/teachers[/:id]` | **管理员** | 新建 / 编辑 / 删除教师账号 |
|
||||
| POST | `/teachers/:id/reset-password` | **管理员** | 修改 / 重置教师密码(`newPassword` 可选,留空 = 默认初始密码) |
|
||||
| GET | `/dashboard/summary`、`/dashboard/term` | 登录 | 仪表盘汇总 / 学期周次 |
|
||||
| GET | `/imports/template?kind=&format=` | 管理员 / 班主任 | 导入模板下载(`csv` 走 JSON,`xlsx` 走二进制) |
|
||||
| POST | `/imports/table/preview` | 管理员 / 班主任 | 上传表格(base64)解析预览:列名 + 前 20 行 + 校验问题 + 建议映射 |
|
||||
| POST | `/imports/table/commit` | 管理员 / 班主任 | 按字段映射与写入模式导入(成绩 / 学生名单 / 教师名单) |
|
||||
| POST | `/imports/time-layout/preview` | 管理员 / 本班班主任 | 解析 ClassIsland 时间配置 JSON(只解析不落库) |
|
||||
| GET / POST / DELETE | `/imports/time-layout[/:id]` | 管理员 / 本班班主任 | 时间配置列表 / 导入(`replace` 覆盖、`merge` 合并)/ 删除 |
|
||||
| POST | `/imports/class-plan/preview`、`/imports/class-plan` | 管理员 / 本班班主任 | ClassIsland 课程表解析预览 / 导入(支持单双周) |
|
||||
| GET / POST / PATCH / DELETE | `/integrations/devices[/:id]` | 管理员 / 教师 | ClassIsland 联动设备管理(令牌只存 sha256,令牌前缀用于人眼识别) |
|
||||
| POST | `/integrations/devices/:id/token` | 管理员 / 教师 | 重置设备令牌(旧令牌立即失效,明文只返回一次) |
|
||||
| POST | `/integrations/classisland/notify` | 管理员 / 教师 | 下发提醒到该班 ClassIsland 设备(广播 `classisland:notification`) |
|
||||
| GET | `/database/status` | **管理员** | 数据库状态(连接 / 版本 / 体积 / 23 张表行数 / 备份列表 / 定时配置) |
|
||||
| POST | `/database/test-connection` | **管理员** | 测试任意目标库连通性(provider 仅 sqlite / mysql) |
|
||||
| GET / POST | `/database/backups` | **管理员** | 备份列表 / 立即备份(gzip JSON 快照,存 `<数据目录>/backups/`) |
|
||||
| POST / DELETE | `/database/backups/:name/restore` · `/database/backups/:name` | **管理员** | 从备份恢复(整库覆盖)/ 删除备份 |
|
||||
| GET | `/database/export`、`/database/sqlite-file` | **管理员** | 下载 JSON 快照 / 下载数据库文件(后者仅 SQLite) |
|
||||
| POST | `/database/import` | **管理员** | 导入快照(base64,整库覆盖;可在 SQLite / MySQL 之间互迁) |
|
||||
| GET / PUT | `/database/backup-schedule` | **管理员** | 定时备份配置(enabled / intervalHours / keepCount) |
|
||||
| POST / GET | `/database/switch`、`/database/switch/jobs/:id` | **管理员** | **一键切换数据库**(异步任务)/ 轮询进度 |
|
||||
| POST | `/integrations/classisland/report` | **设备令牌** | 插件上报状态 + 课表 + 节次时间 |
|
||||
| GET | `/integrations/classisland/pending` | **设备令牌** | 插件拉取尚未确认的提醒(离线期间老师发的通知,重连后补齐) |
|
||||
| POST | `/integrations/classisland/ack` | **设备令牌** | 插件确认提醒已弹出(确认后不再补发) |
|
||||
| GET | `/integrations/classisland/class-plan` | **设备令牌** | 插件拉取本班课表(开启镜像时)用于写回 ClassIsland |
|
||||
| GET | `/update/check` | 登录 | 更新检查(服务端代查 GitHub 最新 Release;`?force=1` 绕过缓存仅管理员生效) |
|
||||
| POST | `/clients/heartbeat` | 登录 | 客户端每 30 秒上报(**无权限键**);返回更新目标 + 安装包信息 + 一条待执行命令 |
|
||||
| POST | `/clients/commands/:id/ack` | 登录 | 命令执行回执(**无权限键**,靠 body 的 `deviceKey` 校验归属,幂等) |
|
||||
| POST | `/clients/commands` | `remote:control` | 下发远控命令(关机 / 重启 / 注销 / 锁屏;自动撤销该机更早的未回执命令) |
|
||||
| GET | `/clients/devices?classId=` | `client:manage` | 客户端设备列表(版本 / 在线 / 待执行命令) |
|
||||
| POST | `/clients/updates` | `client:update-push` | 统一下发客户端更新(按设备 / 班级 / 全部) |
|
||||
| DELETE | `/clients/devices/:id` | `client:manage` | 移除设备记录(换机 / 旧机器清理) |
|
||||
| GET | `/update/client/releases` | `client:manage` | 服务端已缓存了哪些客户端安装包 |
|
||||
| POST | `/update/client/fetch` | `client:update-push` | 服务端代教室机器从 GitHub 下载安装包并登记 sha256 + size |
|
||||
| POST | `/update/client/releases` | `client:update-push` | 手工登记已放进磁盘的安装包(内网 / U 盘分发兜底) |
|
||||
| DELETE | `/update/client/releases/:version` | `client:update-push` | 删除缓存的安装包(元数据 + 磁盘文件) |
|
||||
| GET | `/update/client/download/:version` | 登录 | 下载安装包(**故意不挂权限键**,班级端要能下;`res.download`) |
|
||||
| GET / PUT | `/ai/settings` | `ai:manage` | 大模型设置(API Key 加密存储、**只进不出**,只回 `hasApiKey` / `keyHint`) |
|
||||
| 方法 | 路径 | 权限 | 说明 |
|
||||
| --------------------------- | ------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------- |
|
||||
| POST | `/auth/login` | 公开 | 登录(教师 / 管理员的**工号** + 密码);学生不是账号,拿学号登录只会得到 401 |
|
||||
| POST | `/auth/class-login` | 公开 | **班级账号登录**:班级码 + 班级密码 → 班级会话 |
|
||||
| GET | `/auth/me` | 登录 | 当前用户(学生附带班级 / 年级) |
|
||||
| PATCH | `/auth/password` | 登录 | 修改自己的密码(班级会话 → 班级密码) |
|
||||
| POST | `/auth/logout` | 登录 | 退出(无状态,客户端丢弃 token) |
|
||||
| GET | `/permissions/me` | 登录 | 当前账号的生效权限(**无权限键**;班级端会话返回空集) |
|
||||
| GET | `/permissions/catalog` | `permission:manage` | 权限键目录(权限页整棵勾选树的唯一来源) |
|
||||
| GET / POST | `/permissions/groups` | `permission:manage` | 权限组列表 / 新建 |
|
||||
| PATCH / DELETE | `/permissions/groups/:id` | `permission:manage` | 改名 / 改描述 / 整体替换权限键;内置组不可删 |
|
||||
| GET | `/permissions/users` | `permission:manage` | 成员列表(所属权限组 + 生效权限数量) |
|
||||
| PUT | `/permissions/users/:id/groups` | `permission:manage` | 整体替换某账号的权限组(空数组 = 回落角色默认模板) |
|
||||
| GET | `/classes` | 登录 | 班级列表(按权限收敛) |
|
||||
| GET | `/classes/:id` | 班级可见 | 班级详情(学生 / 课程 / 班主任 / 各科任课老师) |
|
||||
| POST | `/classes` | 管理员 | 创建班级(自动生成班级码,可指定班主任) |
|
||||
| PATCH / DELETE | `/classes/:id` | 管理员 / 教师 | 编辑 / 删除班级 |
|
||||
| PATCH | `/classes/:id/class-account` | 管理员 | 设置 / 重置班级账号(班级码 + 班级密码) |
|
||||
| PATCH | `/classes/:id/head-teacher` | 管理员 | 设置 / 更换班主任 |
|
||||
| GET / POST | `/classes/:id/students` | 班级可见 / ADMIN·TEACHER | 学生名单 / 添加学生 |
|
||||
| DELETE | `/classes/:id/students/:userId` | ADMIN·TEACHER | 移出学生 |
|
||||
| PUT | `/classes/:id/subject-teachers` | ADMIN | 设置各科任课老师(班级 + 科目 + 教师) |
|
||||
| GET / POST / PATCH / DELETE | `/courses` | 登录(写:班主任 / 管理员) | 课程管理 |
|
||||
| GET | `/schedules?classId=&week=&dayOfWeek=` | 登录 | 课表列表(`week` 过滤周次范围) |
|
||||
| GET | `/schedules/grid?classId=&week=` | 登录 | 周视图(7 列结构,供客户端直接渲染) |
|
||||
| GET | `/schedules/current?classId=&at=` | 登录 | 当前上课状态(`inClass` / `current` / `next`;`at` 为诊断用时间覆盖) |
|
||||
| POST / PATCH / DELETE | `/schedules[/:id]` | 班主任 / 管理员 | 课表增删改(广播 `schedule:updated`) |
|
||||
| GET | `/homeworks?classId=&courseId=&date=&pendingOnly=&keyword=` | 登录 | 作业列表(支持按所属日期;学生带完成状态,教师带完成人数) |
|
||||
| GET | `/homeworks/days?classId=&from=&to=` | 登录 | 哪些天有作业(日期高亮) |
|
||||
| GET | `/homeworks/:id` | 班级可见 | 作业详情 |
|
||||
| POST | `/homeworks` | 教师 / 管理员 / **班级账号** | 发布作业(附 `assignDate`;班级账号用于教室机器录入,归属班主任) |
|
||||
| PATCH / DELETE | `/homeworks/:id` | 教师 / 管理员 | 修改 / 删除(广播 `homework:new` / `homework:updated`) |
|
||||
| PATCH | `/homeworks/:id/status` | 登录 | 标记完成 / 取消(广播 `homework:status`) |
|
||||
| GET / PATCH | `/homeworks/:id/submissions` | 登录 | 未交名单:读名单 / 回写全班完成状态 |
|
||||
| POST | `/homeworks/bulk` | `homework:write` | 一次给多个班级布置同一份作业(`subjectName` 跨班按名匹配科目;逐班结果) |
|
||||
| GET | `/notifications?classId=&priority=&unreadOnly=&keyword=` | 登录 | 通知列表(带已读状态;另支持 `date` / `from` / `to` / `createdBy`) |
|
||||
| GET | `/notifications/unread-count` | 登录 | 未读数(红点) |
|
||||
| POST | `/notifications` | 教师 / 管理员 | 发布通知(广播 `notification:new`);上课时段发紧急通知需 `confirmDuringClass` |
|
||||
| POST | `/notifications/:id/read`、`/notifications/read-all` | 登录 | 标记已读(班级设备会写全班) |
|
||||
| DELETE | `/notifications/:id` | 教师 / 管理员 | 删除通知 |
|
||||
| GET | `/notifications/days?classId=&from=&to=&createdBy=` | 登录 | 通知按天归档(日期 + 条数,日期取 `assignDate`) |
|
||||
| GET | `/notifications/creators?classId=&from=&to=&date=` | 登录 | 通知按发布人归档 |
|
||||
| POST | `/notifications/bulk` | `notification:publish` | 一次给多个班级发同一条通知(逐班结果,**单班失败不影响整批**) |
|
||||
| POST | `/calls` | 教师 / 管理员 | **叫人**(`urgent` 决定 URGENT / HIGH;广播 `notification:new` 与 `call:new`) |
|
||||
| GET | `/grades/my` | 登录 | 个人成绩(班级会话 → 全班总览) |
|
||||
| GET | `/grades?classId=&courseId=&userId=&examName=` | 教师 / 管理员 | 班级成绩 |
|
||||
| GET | `/grades/stats?classId=&courseId=&examName=` | 教师 / 管理员 | 等级分布 + 各课程平均得分率 |
|
||||
| POST | `/grades`、`/grades/bulk` | 班主任 / 管理员 | 单条 / 批量录入(广播 `grade:updated`) |
|
||||
| PATCH / DELETE | `/grades/:id` | 班主任 / 管理员 | 修改 / 删除成绩 |
|
||||
| GET | `/students?classId=&keyword=` | **管理员** | 学生名单 |
|
||||
| POST / PATCH / DELETE | `/students[/:id]` | **管理员** | 学生名单增删改(无密码概念;重置密码接口已下线 → 404) |
|
||||
| GET | `/teachers?keyword=` | **管理员** | 教师列表 |
|
||||
| POST / PATCH / DELETE | `/teachers[/:id]` | **管理员** | 新建 / 编辑 / 删除教师账号 |
|
||||
| POST | `/teachers/:id/reset-password` | **管理员** | 修改 / 重置教师密码(`newPassword` 可选,留空 = 默认初始密码) |
|
||||
| GET | `/dashboard/summary`、`/dashboard/term` | 登录 | 仪表盘汇总 / 学期周次 |
|
||||
| GET | `/imports/template?kind=&format=` | 管理员 / 班主任 | 导入模板下载(`csv` 走 JSON,`xlsx` 走二进制) |
|
||||
| POST | `/imports/table/preview` | 管理员 / 班主任 | 上传表格(base64)解析预览:列名 + 前 20 行 + 校验问题 + 建议映射 |
|
||||
| POST | `/imports/table/commit` | 管理员 / 班主任 | 按字段映射与写入模式导入(成绩 / 学生名单 / 教师名单) |
|
||||
| POST | `/imports/time-layout/preview` | 管理员 / 本班班主任 | 解析 ClassIsland 时间配置 JSON(只解析不落库) |
|
||||
| GET / POST / DELETE | `/imports/time-layout[/:id]` | 管理员 / 本班班主任 | 时间配置列表 / 导入(`replace` 覆盖、`merge` 合并)/ 删除 |
|
||||
| POST | `/imports/class-plan/preview`、`/imports/class-plan` | 管理员 / 本班班主任 | ClassIsland 课程表解析预览 / 导入(支持单双周) |
|
||||
| GET | `/database/status` | **管理员** | 数据库状态(连接 / 版本 / 体积 / 23 张表行数 / 备份列表 / 定时配置) |
|
||||
| POST | `/database/test-connection` | **管理员** | 测试任意目标库连通性(provider 仅 sqlite / mysql) |
|
||||
| GET / POST | `/database/backups` | **管理员** | 备份列表 / 立即备份(gzip JSON 快照,存 `<数据目录>/backups/`) |
|
||||
| POST / DELETE | `/database/backups/:name/restore` · `/database/backups/:name` | **管理员** | 从备份恢复(整库覆盖)/ 删除备份 |
|
||||
| GET | `/database/export`、`/database/sqlite-file` | **管理员** | 下载 JSON 快照 / 下载数据库文件(后者仅 SQLite) |
|
||||
| POST | `/database/import` | **管理员** | 导入快照(base64,整库覆盖;可在 SQLite / MySQL 之间互迁) |
|
||||
| GET / PUT | `/database/backup-schedule` | **管理员** | 定时备份配置(enabled / intervalHours / keepCount) |
|
||||
| POST / GET | `/database/switch`、`/database/switch/jobs/:id` | **管理员** | **一键切换数据库**(异步任务)/ 轮询进度 |
|
||||
| GET | `/update/check` | 登录 | 更新检查(服务端代查 GitHub 最新 Release;`?force=1` 绕过缓存仅管理员生效) |
|
||||
| POST | `/clients/heartbeat` | 登录 | 客户端每 30 秒上报(**无权限键**);返回更新目标 + 安装包信息 + 一条待执行命令 |
|
||||
| POST | `/clients/commands/:id/ack` | 登录 | 命令执行回执(**无权限键**,靠 body 的 `deviceKey` 校验归属,幂等) |
|
||||
| POST | `/clients/commands` | `remote:control` | 下发远控命令(关机 / 重启 / 注销 / 锁屏;自动撤销该机更早的未回执命令) |
|
||||
| GET | `/clients/devices?classId=` | `client:manage` | 客户端设备列表(版本 / 在线 / 待执行命令) |
|
||||
| POST | `/clients/updates` | `client:update-push` | 统一下发客户端更新(按设备 / 班级 / 全部) |
|
||||
| DELETE | `/clients/devices/:id` | `client:manage` | 移除设备记录(换机 / 旧机器清理) |
|
||||
| GET | `/update/client/releases` | `client:manage` | 服务端已缓存了哪些客户端安装包 |
|
||||
| POST | `/update/client/fetch` | `client:update-push` | 服务端代教室机器从 GitHub 下载安装包并登记 sha256 + size |
|
||||
| POST | `/update/client/releases` | `client:update-push` | 手工登记已放进磁盘的安装包(内网 / U 盘分发兜底) |
|
||||
| DELETE | `/update/client/releases/:version` | `client:update-push` | 删除缓存的安装包(元数据 + 磁盘文件) |
|
||||
| GET | `/update/client/download/:version` | 登录 | 下载安装包(**故意不挂权限键**,班级端要能下;`res.download`) |
|
||||
| GET / PUT | `/ai/settings` | `ai:manage` | 大模型设置(API Key 加密存储、**只进不出**,只回 `hasApiKey` / `keyHint`) |
|
||||
| POST | `/ai/interpret` | `ai:use` | 一句话解析(**只解析不落库**);回 `action` / `params` / `preview` / `confidence` |
|
||||
| GET | `/ai/audit?limit=` | `ai:manage` | 助手使用记录(每次解析写一条 `AiAudit`) |
|
||||
| GET / PUT | `/system/settings` | `system:manage` | 系统设置(`developerMode`:开启后 500 响应带 `error.stack`) |
|
||||
| GET / PUT | `/system/autostart` | `system:manage` | 开机自启状态 / 开关(Windows 注册表 Run 项,Linux systemd 单元) |
|
||||
| GET | `/system/diagnostics` | `system:manage` | 运行诊断(版本 / Node / 运行时长 / 数据目录 / 在线客户端数 / 已加载模块) |
|
||||
| GET | `/health` | 公开 | 健康检查(含版本号与已挂载模块) |
|
||||
| GET | `/ai/audit?limit=` | `ai:manage` | 助手使用记录(每次解析写一条 `AiAudit`) |
|
||||
| GET / PUT | `/system/settings` | `system:manage` | 系统设置(`developerMode`:开启后 500 响应带 `error.stack`) |
|
||||
| GET / PUT | `/system/autostart` | `system:manage` | 开机自启状态 / 开关(Windows 注册表 Run 项,Linux systemd 单元) |
|
||||
| GET | `/system/diagnostics` | `system:manage` | 运行诊断(版本 / Node / 运行时长 / 数据目录 / 在线客户端数 / 已加载模块) |
|
||||
| GET | `/health` | 公开 | 健康检查(含版本号与已挂载模块) |
|
||||
|
||||
另有三个探针(不带 `/api` 前缀):`/healthz`(存活)、`/readyz`(就绪)、以及 `/api/health`(详情)。
|
||||
|
||||
## 几条容易踩的接口约定
|
||||
|
||||
| 约定 | 说明 |
|
||||
| -------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| **`POST /homeworks` 故意不加 `requireRole`** | 权限判在 `homeworks.service.createHomework` 里(staff 或本班班级账号),因为教室机器也要能录作业 |
|
||||
| **班级账号的 `sub` 是班级 id** | 写 `createdBy` 前要换成该班班主任,否则撞 `User` 外键 |
|
||||
| **登录限流挂在两个入口上** | `/auth/login` 与 `/auth/class-login` 都挂 `loginRateLimiter()` |
|
||||
| **角色判定在密码校验之前** | 否则"密码错 / 密码对但账号停用"的差异会把学生账号密码变成可离线验证的预言机 |
|
||||
| **会渲染成链接的字段要校验 scheme** | `Homework.attachmentUrl` 只允许 `http(s)://` 或站内相对路径 |
|
||||
| **表格导入 8MB ↔ 请求体 12MB** | 文件以 base64 放进 JSON(约 4/3 倍),超限映射成 413 `IMPORT_TOO_LARGE` |
|
||||
| 约定 | 说明 |
|
||||
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`POST /homeworks` 故意不加 `requireRole`** | 权限判在 `homeworks.service.createHomework` 里(staff 或本班班级账号),因为教室机器也要能录作业 |
|
||||
| **班级账号的 `sub` 是班级 id** | 写 `createdBy` 前要换成该班班主任,否则撞 `User` 外键 |
|
||||
| **登录限流挂在两个入口上** | `/auth/login` 与 `/auth/class-login` 都挂 `loginRateLimiter()` |
|
||||
| **角色判定在密码校验之前** | 否则"密码错 / 密码对但账号停用"的差异会把学生账号密码变成可离线验证的预言机 |
|
||||
| **会渲染成链接的字段要校验 scheme** | `Homework.attachmentUrl` 只允许 `http(s)://` 或站内相对路径 |
|
||||
| **表格导入 8MB ↔ 请求体 12MB** | 文件以 base64 放进 JSON(约 4/3 倍),超限映射成 413 `IMPORT_TOO_LARGE` |
|
||||
| **三个接口故意不挂权限键** | `/permissions/me`、`/clients/heartbeat`、`/update/client/download/:version` —— 前两者由班级端(`CLASS_DEVICE`,权限集为空)调用,下载接口要能装包;保护只有"已登录 + 限流" |
|
||||
| **查询参数校验失败是 422** | `middleware/validate.ts` 的 zod 解析错误由错误中间件映射成 422 `VALIDATION_ERROR`(不是 400) |
|
||||
| **批量发布逐班判定、不整批回滚** | `/notifications/bulk` 与 `/homeworks/bulk` 的单班失败(403 越权 / `URGENT_DURING_CLASS`)不影响其它班,成功班照常落库并实时推送 |
|
||||
| **`/ai/interpret` 只解析不落库** | 助手只回结构化参数与中文预览;落库由前端确认后调既有端点 —— 权限与上课时段保护都在那条路径上 |
|
||||
| **查询参数校验失败是 422** | `middleware/validate.ts` 的 zod 解析错误由错误中间件映射成 422 `VALIDATION_ERROR`(不是 400) |
|
||||
| **批量发布逐班判定、不整批回滚** | `/notifications/bulk` 与 `/homeworks/bulk` 的单班失败(403 越权 / `URGENT_DURING_CLASS`)不影响其它班,成功班照常落库并实时推送 |
|
||||
| **`/ai/interpret` 只解析不落库** | 助手只回结构化参数与中文预览;落库由前端确认后调既有端点 —— 权限与上课时段保护都在那条路径上 |
|
||||
|
||||
## 相关
|
||||
|
||||
- 实时事件见[实时事件(Socket.IO)](realtime.md);
|
||||
- 权限矩阵见[角色与权限](permissions.md);
|
||||
- 数据模型见[数据模型](data-model.md)。
|
||||
|
||||
客户端心跳可携带 `agentProtocol: 2`,业务数据读取统一响应的 `data`。未带此标记时额外返回旧版需要的顶层字段;放学后更新在到时前不向旧客户端提供安装包。推送更新支持 `mode: immediate | after-school | idle`,`afterSchoolTime` 为服务端本地时区的 `HH:mm`。
|
||||
+14
-17
@@ -18,7 +18,6 @@ description: 目录结构、技术栈、模块化设计与几条贯穿全局的
|
||||
| 校验 | zod | 4.6 |
|
||||
| Web 端 | Vue + Vite + Element Plus + Pinia + Vue Router + Axios | 3.5 / 8.3 / 2.14 / 4.0 / 5.3 / 1.20 |
|
||||
| 客户端 | Electron + Vue + Element Plus + Pinia + Socket.IO Client + IndexedDB | 44.3 / 3.5 / 2.14 / 4.0 / 4.8 |
|
||||
| 插件 | .NET 8 / C# + Avalonia(ClassIsland 插件 API 2.0) | net8.0 |
|
||||
| 打包 | electron-builder(nsis / portable)+ NSIS | 26.15 |
|
||||
| 文档站 | docfx | 2.81 |
|
||||
|
||||
@@ -56,7 +55,7 @@ class-helper/
|
||||
│ ├── get-started/ app/ management/ dev/ # 四个章节
|
||||
│ ├── templates/classhelper/ # docfx 模板覆盖(_master.tmpl + public/main.css + 图标)
|
||||
│ ├── images/ # 展示图:文档站 / README / 官网共用的同一份(1× 逻辑像素)
|
||||
│ ├── screenshots/ # 回归测试留档的截图(island / client / web-mobile / classisland),页面不展示
|
||||
│ ├── screenshots/ # 回归测试留档的截图(island / client / web-mobile),页面不展示
|
||||
│ ├── build.mjs # 构建入口(注入版本号与 404 的 base)
|
||||
│ ├── serve.mjs # 本地预览(:5181)
|
||||
│ └── _site/ # 构建产物(gitignore)
|
||||
@@ -69,7 +68,7 @@ class-helper/
|
||||
│ ├── prisma/schema.prisma # 数据模型(24 个 model,只有一份,切库改 provider)
|
||||
│ ├── prisma/migrations/ # 迁移历史(10 个)
|
||||
│ ├── prisma/seed.ts # 种子数据(会清空业务表后重建演示数据)
|
||||
│ ├── scripts/ # verify-e2e.mjs / verify-classisland.mjs / apply-column-migrations.cjs
|
||||
│ ├── scripts/ # verify-e2e.mjs / apply-column-migrations.cjs
|
||||
│ └── src/
|
||||
│ ├── app.ts # Express 装配(探针 + 限流 + 模块挂载 + 静态托管 + 兜底)
|
||||
│ ├── index.ts # 启动入口(自检 + HTTP + Socket.IO + 优雅退出)
|
||||
@@ -80,7 +79,6 @@ class-helper/
|
||||
│ ├── middleware/ # auth / error / validate / security(helmet + 限流 + 耗时日志)
|
||||
│ ├── realtime/ # socket.ts(房间)+ bus.ts(事件总线)
|
||||
│ └── modules/ # 15 个功能模块 + registry.ts(模块注册表)
|
||||
├── classisland-plugin/ # ClassIsland 联动插件(.NET 8 / C#,独立于 pnpm workspace)
|
||||
│ ├── manifest.yml
|
||||
│ ├── src/Plugin.cs
|
||||
│ ├── src/Services/ # BridgeService / ScheduleMapper / ClassPlanWriter /
|
||||
@@ -105,7 +103,7 @@ class-helper/
|
||||
后端每个功能是一个独立目录,统一契约:
|
||||
|
||||
```ts
|
||||
// packages/server/src/modules/registry.ts(当前 21 个模块)
|
||||
// packages/server/src/modules/registry.ts(当前 20 个模块)
|
||||
export const apiModules: ApiModule[] = [
|
||||
authModule,
|
||||
aiModule, // 大模型助手(只解析不落库,见 docs/dev/api.md)
|
||||
@@ -116,7 +114,6 @@ export const apiModules: ApiModule[] = [
|
||||
notificationsModule,
|
||||
callsModule,
|
||||
importsModule,
|
||||
integrationsModule,
|
||||
gradesModule,
|
||||
studentsModule,
|
||||
teachersModule,
|
||||
@@ -141,18 +138,18 @@ export const apiModules: ApiModule[] = [
|
||||
|
||||
这些都是长期演进中固化下来的,改代码前值得先看(完整版在 `AGENTS.md`):
|
||||
|
||||
| 约定 | 为什么 |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **统一响应与校验** | 用 `lib/http.ts` 的 `sendOk/sendCreated`,用 `middleware/validate.ts` 挂 zod,错误交给 `middleware/error.ts` |
|
||||
| **功能级准入看权限键** | 路由挂 `requirePermission('…')` / `requireAnyPermission(…)`,角色只是默认模板(`ROLE_DEFAULT_PERMISSIONS`)—— 见 `docs/dev/permissions.md` |
|
||||
| 约定 | 为什么 |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **统一响应与校验** | 用 `lib/http.ts` 的 `sendOk/sendCreated`,用 `middleware/validate.ts` 挂 zod,错误交给 `middleware/error.ts` |
|
||||
| **功能级准入看权限键** | 路由挂 `requirePermission('…')` / `requireAnyPermission(…)`,角色只是默认模板(`ROLE_DEFAULT_PERMISSIONS`)—— 见 `docs/dev/permissions.md` |
|
||||
| **权限判在服务层** | 权限组只管功能级;班级维度与科目维度仍用 `lib/access.ts` 的断言(`assertCanManageSubjectContent` 等),**权限组不可覆盖** —— 前端隐藏 ≠ 权限 |
|
||||
| **周一为新一周的第一天** | 周次口径唯一:`resolveCurrentWeek(TERM_START_DATE, now, MAX_TERM_WEEK)` |
|
||||
| **作业按 `assignDate` 归类** | 用 `createdAt`(UTC)会在晚上把作业算到第二天 |
|
||||
| **新增实时事件要改三处** | 服务端 bus/socket、Web 端订阅、客户端订阅(灵动岛也依赖) |
|
||||
| **跨桥接只传纯数据** | 传 Vue 的 `ref.value` 会在进入 preload 之前就抛 `could not be cloned` |
|
||||
| **桌面端产物里不得有凭据** | asar 可直接解包;构建有 `check-bundle-secrets` 门禁 |
|
||||
| **图标只有一个图形源** | 别改 `scripts/icons/` 的派生逻辑,换图标就换 `build/classhelper.{png,ico}` |
|
||||
| **前端不写死颜色** | 深色主题靠语义令牌,写死的颜色在深色下会露馅 |
|
||||
| **周一为新一周的第一天** | 周次口径唯一:`resolveCurrentWeek(TERM_START_DATE, now, MAX_TERM_WEEK)` |
|
||||
| **作业按 `assignDate` 归类** | 用 `createdAt`(UTC)会在晚上把作业算到第二天 |
|
||||
| **新增实时事件要改三处** | 服务端 bus/socket、Web 端订阅、客户端订阅(灵动岛也依赖) |
|
||||
| **跨桥接只传纯数据** | 传 Vue 的 `ref.value` 会在进入 preload 之前就抛 `could not be cloned` |
|
||||
| **桌面端产物里不得有凭据** | asar 可直接解包;构建有 `check-bundle-secrets` 门禁 |
|
||||
| **图标只有一个图形源** | 别改 `scripts/icons/` 的派生逻辑,换图标就换 `build/classhelper.{png,ico}` |
|
||||
| **前端不写死颜色** | 深色主题靠语义令牌,写死的颜色在深色下会露馅 |
|
||||
|
||||
## 本地开发
|
||||
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
---
|
||||
title: ClassIsland 插件
|
||||
description: 联动插件的契约、镜像为什么要带节次时间、上课时段暂存,以及部署与调试。
|
||||
---
|
||||
|
||||
# ClassIsland 插件
|
||||
|
||||
源码在 `packages/classisland-plugin/`,是一个 **.NET 8 / C#** 的 ClassIsland 插件
|
||||
(ClassIsland 插件 API 2.0)。它**独立于 pnpm workspace**,不参与 `pnpm install`。
|
||||
|
||||
## 三条链路
|
||||
|
||||
```
|
||||
ClassIsland(教室机器)
|
||||
│ ① 上报:状态 + 课表 + 节次时间(每次上报顺带取回一条待弹提醒)
|
||||
▼
|
||||
/api/integrations/classisland/report ← X-ClassIsland-Token(设备令牌,非 JWT)
|
||||
│
|
||||
│ ② 下发:老师在 Web 端发提醒 → 落库 → 插件下次上报时取回 → ClassIsland 全屏弹出 → ack
|
||||
▼
|
||||
/api/integrations/classisland/pending、/ack
|
||||
|
||||
│ ③ 镜像(可选):服务端把本班课表下发,插件在 ClassIsland 里新建一份档案课表
|
||||
▼
|
||||
/api/integrations/classisland/class-plan
|
||||
```
|
||||
|
||||
## 为什么走 HTTP 而不是 IPC
|
||||
|
||||
ClassIsland 的跨进程 IPC 数据质量不足以还原课表。插件内部**直接读**
|
||||
`ILessonsService`(课程状态)与 `IProfileService`(档案课表)最准。
|
||||
|
||||
与后端之间用「**上报即拉取**」:每次上报的返回值里顺带带一条待弹出的提醒 ——
|
||||
插件因此**不需要 WebSocket 依赖**,故障面更小,而且**上报间隔本身就是心跳**。
|
||||
|
||||
提醒是**轮询取回**的(默认最多 10 秒延迟,设置页「提醒轮询间隔」可调到 5 秒)。
|
||||
|
||||
## 设备令牌
|
||||
|
||||
- 令牌是**设备级**的,不是用户级;
|
||||
- 插件在本机保存明文,服务端只存 **sha256** 与一个前缀提示(`chci_xxxxxxxx`)用于人眼识别;
|
||||
- 同一班级下按 `deviceKey`(机器码)唯一 —— 插件重装后带同一 `deviceKey` 会**自动重绑**到原记录,
|
||||
不会在设备列表里堆重复项;
|
||||
- 关闭设备的「启用」开关后,该设备的上报与下发一律 403。
|
||||
|
||||
## 提醒的排队规则
|
||||
|
||||
| 提醒类型 | 上课时段 | 下课 / 放学 |
|
||||
| ------------------------------------------------------ | ---------------------------- | ----------- |
|
||||
| 通知类(`kind=notification`:通知发布页 / 联动页下发) | **暂存不弹** | 自动补弹 |
|
||||
| 主动通知:紧急提醒、**叫人**(`kind=call`) | **立刻弹**(老师正在等学生) | 立刻弹 |
|
||||
|
||||
插件侧的实现在 `ClassHelperNotificationProvider`(`_deferred` + `FlushDeferred`,订阅课程事件);
|
||||
客户端侧的实现在 `renderer/island/bridge.ts`。**改这条规则要两端一起改。**
|
||||
|
||||
暂存期间**不回执**,所以服务端会一直保留着待提醒;`ClassIslandPush.expiresAt` 到期后不再补发
|
||||
(避免学生几天后突然弹出旧提醒)。
|
||||
|
||||
## 镜像:为什么必须带 `startTime` / `endTime`
|
||||
|
||||
ClassIsland 的 `ClassPlan.Classes[i]` **强制与时间表里第 i 个 `TimeType == 0`(上课)的时间点对齐**。
|
||||
|
||||
所以镜像下发时,每一条课目都要带上自己的 `startTime` / `endTime`。
|
||||
|
||||
> **少一个时间点,整份课表就会错位** —— 而且表现为"课表看着有内容、每节课都对不上",
|
||||
> 极难排查。这是插件契约里最容易踩的坑。
|
||||
|
||||
另外镜像进 ClassIsland 的课表还要**带课间**:每两节之间补一段(上一节下课 → 下一节上课),
|
||||
**最后一节之后不补**(放学)。实现见 `ClassPlanWriter.BuildTimeLayout`(自己按课目时间算),
|
||||
服务端合成节次时间表用的是同一口径。
|
||||
|
||||
## 上报课表:单向还是双向
|
||||
|
||||
**双向对齐**。插件每次全量上报时,服务端只清理「`source='classisland'` 且本次未再上报」的行:
|
||||
|
||||
- 老师在 ClassIsland 里删掉 / 挪动一节 → 小助手这边跟着同步;
|
||||
- 老师在小助手这边手排的课 → **一律不删**。
|
||||
|
||||
merge 的去重键是「星期 + 开始时间 + 单双周」。导入时**不要覆盖 `weekStart` / `weekEnd`** ——
|
||||
插件上报契约里没有周次范围,硬写"整学期"会把老师设的「第 1~10 周」抹掉。
|
||||
|
||||
## 单双周的映射
|
||||
|
||||
ClassIsland 的 `WeekCountDiv` / `WeekCountDivTotal` ⇄ 本项目的 `ALL / ODD / EVEN`,
|
||||
走 `@classhelper/shared` 的 `weekParityFromDiv` / `weekDivFromParity` ——
|
||||
**服务端与插件共用同一份语义**,两边不会各算各的。
|
||||
|
||||
## 代理:本地地址强制直连
|
||||
|
||||
`ClassHelperClient` 里有一个 `LocalAwareProxy`:对本地 / 内网地址**强制直连**,
|
||||
不走系统代理。
|
||||
|
||||
原因:教室机器上如果开了系统代理,填好令牌后表现为"立刻 HTTP 404"(其实是代理返回的错误页)。
|
||||
`pnpm verify:classisland-plugin` 有 4 项断言盯着这段逻辑,别删。
|
||||
|
||||
## 构建、打包与验收
|
||||
|
||||
```bash
|
||||
pnpm build:classisland-plugin # 编译(.NET 8;缓存与临时目录自动指到仓库内 .cache/)
|
||||
pnpm dist:classisland-plugin # 打包 .cipx 并归集到 releases/classisland-plugin/<版本>/
|
||||
pnpm verify:classisland-plugin # 静态契约校验(不需要 .NET,不需要服务端)
|
||||
```
|
||||
|
||||
`verify:classisland-plugin` 会逐项校验:清单一致性、注册完整性、
|
||||
**C# DTO ↔ 服务端 zod 与路由的字段对齐**,以及已修复坑的护栏。
|
||||
|
||||
### 那个预期内的编译警告
|
||||
|
||||
全量编译会有 **1 个警告 `AVLN3001`**:设置页只有 DI 构造函数、没有公开无参构造。
|
||||
本插件走的是编译期 XAML + ClassIsland 的 DI 实例化,**加个无参构造反而会把 DI 弄坏**。
|
||||
别为了消警告去改它。
|
||||
|
||||
## 部署到教室机器
|
||||
|
||||
把编出来的 DLL 覆盖到这台机器的 ClassIsland 数据目录(默认 `%USERPROFILE%\ClassIsland\data`,
|
||||
便携版则是安装目录下的 `data\`)。ClassIsland **运行时会锁住 DLL,必须先完全退出它**:
|
||||
|
||||
```powershell
|
||||
# 1) 完全退出 ClassIsland(它会锁住 DLL)
|
||||
Get-Process ClassIsland.Desktop | Stop-Process -Force
|
||||
# 2) 覆盖插件 DLL(**不要覆盖 Settings.json**,那里存着服务器地址与设备令牌)
|
||||
Copy-Item packages\classisland-plugin\bin\Release\ClassHelper.ClassIslandPlugin.dll `
|
||||
"$env:USERPROFILE\ClassIsland\data\Plugins\classhelper.classisland.bridge\" -Force
|
||||
# 3) 重新启动
|
||||
Start-Process "$env:USERPROFILE\ClassIsland\ClassIsland.exe"
|
||||
```
|
||||
|
||||
改配置要在 **ClassIsland 退出时**改:运行中改会被内存里的副本随时回写覆盖。
|
||||
|
||||
### 读日志要用 GBK
|
||||
|
||||
.NET 的控制台输出是**系统 ANSI(GBK)**编码。用 UTF-8 读会变成乱码,
|
||||
**中文关键字一条都搜不到**(只能搜到 ASCII 的类名):
|
||||
|
||||
```powershell
|
||||
[System.IO.File]::ReadAllLines($path, [System.Text.Encoding]::GetEncoding(936))
|
||||
```
|
||||
|
||||
日志文件被运行中的进程占用时,先 `Copy-Item` 一份再读。
|
||||
|
||||
## 真机验收链路
|
||||
|
||||
`pnpm verify:classisland`(需要后端在跑)会真的走一遍:设备令牌 → 上报 → 提醒下发与回执 →
|
||||
镜像契约 → 幽灵行清理。
|
||||
|
||||
> 这个脚本开头会先**抽干本班的历史积压提醒**。因为待提醒接口是「本班全部未确认提醒,
|
||||
> 按时间升序取前 20 条」,而其他冒烟会真的往演示班发通知 —— 攒够 20 条之后,
|
||||
> 本脚本刚发的提醒会被挤出窗口,表现为"待提醒里找不到刚发的那条",跟代码对不对毫无关系。
|
||||
> **别把那个 drain 删掉。**
|
||||
+24
-46
@@ -32,52 +32,30 @@ description: 24 张表的职责、跨库通用约定、迁移历史与启动时
|
||||
|
||||
## 24 张表
|
||||
|
||||
| 表 | 职责 | 值得注意的字段 |
|
||||
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
||||
| `User` | **纯账号表**:管理员 / 教师 / ClassHelper 班级端 | `username`(教师即**工号**,班级端即班级码)、`role`、`phone`、`careMode`(关怀模式偏好,目前只由本机界面读写);**学生不在这里** |
|
||||
| `Student` | 学生名单记录(**没有账号**) | `studentNo`(学号,唯一,唯一标识与查询键)、`classId`、`status`、`archivedYearId` |
|
||||
| `StudentClassTransfer` | 调班 / 转出历史 | 班级 id 与班级名**都存快照且不建外键**(班级删了历史仍要能读)、`mode`(single|batch|transfer-out) |
|
||||
| `ArchivedYear` | 毕业归档的届别档案 | `enrollmentYear`(届别,唯一)、`graduationYear`、归档时的统计快照 |
|
||||
| `Class` | 班级 | `name`(**「2026级1班」**,由 `enrollmentYear` + `classIndex` 生成)、`code` + `passwordHash`(班级端账号)、`termWeeks`、`termStartDate`、`notificationChannel`、`studentGradeQueryEnabled`、`archivedYearId` |
|
||||
| `TermWeek` | 学期逐周日期区间 | `classId`(**空串 = 全校默认**)、`weekNumber`、`startDate` / `endDate` |
|
||||
| `Course` | 课程 = **「班级 + 科目 + 教师」任课关系** | `teacherId` 就是该班该科的任课老师;`@@unique([classId, name])` |
|
||||
| `Schedule` | 课表条目 | `weekStart` / `weekEnd` / `weekParity`(单双周)/ `source`(manual|classisland) |
|
||||
| `Homework` | 作业 | `assignDate`(所属日期)、`attachmentUrl`;`dueAt` 是已下线的遗留列 |
|
||||
| `HomeworkStatus` | 学生作业完成状态 | `@@unique([homeworkId, studentId])` |
|
||||
| `Notification` | 通知 | `priority`、`assignDate`(所属本地日期,禁止 UTC 切片);`@@index([classId, assignDate])` 供按天归档 |
|
||||
| `NotificationRead` | 已读记录 | `@@unique([notificationId, studentId])` |
|
||||
| `TimeLayout` | 节次时间配置(ClassIsland 同款 JSON 导入得到) | `items` 是 JSON 字符串:`[{ index, name, startTime, endTime, type, skipped }]` |
|
||||
| `Grade` | 成绩 | `studentId`、`score` / `totalScore`、`levelType`(percent|letter|custom)与 `level` |
|
||||
| `IntegrationDevice` | ClassHelper 班级端 / ClassIsland 联动设备 | `tokenHash`(只存 sha256)、`lastHeartbeatAt`(在线判定)、`lastSeenAt`(最后一次带状态的上报)、三个方向开关 |
|
||||
| `ClassIslandPush` | 下发给 ClassIsland 的提醒 | `kind`(notification|call)、`ackedAt`、`expiresAt` |
|
||||
| `PermissionGroup` | 权限组(角色之上的**默认模板**) | `name`(唯一)、`builtin`(内置组不可删);生效集合见 [角色与权限](permissions.md) |
|
||||
| `PermissionGroupItem` | 权限组里的一个权限键(一行一个键) | `@@unique([groupId, key])`;键取值必须是 shared 的 `PERMISSION_KEYS` 之一 |
|
||||
| `UserPermissionGroup` | 账号 ←→ 权限组 的分配关系(可属多个组) | `@@unique([userId, groupId])`;**没有任何行 = 回落角色默认模板** |
|
||||
| `AppSetting` | 全局键值设置(大模型配置 / 开发者模式) | `key` 即主键、`value` 存字符串(复杂结构存 JSON);密钥类值做 AES-256-GCM 加密后存这里(**只进不出**) |
|
||||
| `ClientDevice` | ClassHelper 客户端设备(教室 / 学生机) | `deviceKey`(唯一,客户端生成)、`classId`、`clientVersion`、`updateTargetVersion`、`lastHeartbeatAt`(**在线判定看它**) |
|
||||
| `ClientCommand` | 下发给客户端的远控指令 | `kind`(shutdown|restart|logoff|lock)、`expiresAt`(10 分钟)、`ackedAt` / `result`、`cancelledAt` |
|
||||
| `ClientRelease` | 服务端缓存的客户端安装包 | `version`(唯一,白名单校验防路径穿越)、`fileName`、`sha256`、`size`;文件在 `data/client-releases/` |
|
||||
| `AiAudit` | 大模型指令审计(意图与结果,不记密钥) | `userId`、`prompt`、`intent`、`status`(pending|confirmed|rejected|failed)、`rawReply` |
|
||||
| 表 | 职责 | 值得注意的字段 |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `User` | **纯账号表**:管理员 / 教师 / ClassHelper 班级端 | `username`(教师即**工号**,班级端即班级码)、`role`、`phone`、`careMode`(关怀模式偏好,目前只由本机界面读写);**学生不在这里** |
|
||||
| `Student` | 学生名单记录(**没有账号**) | `studentNo`(学号,唯一,唯一标识与查询键)、`classId`、`status`、`archivedYearId` |
|
||||
| `StudentClassTransfer` | 调班 / 转出历史 | 班级 id 与班级名**都存快照且不建外键**(班级删了历史仍要能读)、`mode`(single|batch|transfer-out) |
|
||||
| `ArchivedYear` | 毕业归档的届别档案 | `enrollmentYear`(届别,唯一)、`graduationYear`、归档时的统计快照 |
|
||||
| `Class` | 班级 | `name`(**「2026级1班」**,由 `enrollmentYear` + `classIndex` 生成)、`code` + `passwordHash`(班级端账号)、`termWeeks`、`termStartDate`、`studentGradeQueryEnabled`、`archivedYearId` |
|
||||
| `TermWeek` | 学期逐周日期区间 | `classId`(**空串 = 全校默认**)、`weekNumber`、`startDate` / `endDate` |
|
||||
| `Course` | 课程 = **「班级 + 科目 + 教师」任课关系** | `teacherId` 就是该班该科的任课老师;`@@unique([classId, name])` |
|
||||
| `Schedule` | 课表条目 | `weekStart` / `weekEnd` / `weekParity`(单双周)/ `source`(manual) |
|
||||
| `Homework` | 作业 | `assignDate`(所属日期)、`attachmentUrl`;`dueAt` 是已下线的遗留列 |
|
||||
| `HomeworkStatus` | 学生作业完成状态 | `@@unique([homeworkId, studentId])` |
|
||||
| `Notification` | 通知 | `priority`、`assignDate`(所属本地日期,禁止 UTC 切片);`@@index([classId, assignDate])` 供按天归档 |
|
||||
| `NotificationRead` | 已读记录 | `@@unique([notificationId, studentId])` |
|
||||
| `TimeLayout` | 节次时间配置(ClassIsland 同款 JSON 导入得到) | `items` 是 JSON 字符串:`[{ index, name, startTime, endTime, type, skipped }]` |
|
||||
| `Grade` | 成绩 | `studentId`、`score` / `totalScore`、`levelType`(percent|letter|custom)与 `level` |
|
||||
| `PermissionGroup` | 权限组(角色之上的**默认模板**) | `name`(唯一)、`builtin`(内置组不可删);生效集合见 [角色与权限](permissions.md) |
|
||||
| `PermissionGroupItem` | 权限组里的一个权限键(一行一个键) | `@@unique([groupId, key])`;键取值必须是 shared 的 `PERMISSION_KEYS` 之一 |
|
||||
| `UserPermissionGroup` | 账号 ←→ 权限组 的分配关系(可属多个组) | `@@unique([userId, groupId])`;**没有任何行 = 回落角色默认模板** |
|
||||
| `AppSetting` | 全局键值设置(大模型配置 / 开发者模式) | `key` 即主键、`value` 存字符串(复杂结构存 JSON);密钥类值做 AES-256-GCM 加密后存这里(**只进不出**) |
|
||||
| `ClientDevice` | ClassHelper 客户端设备(教室 / 学生机) | `deviceKey`(唯一,客户端生成)、`classId`、`clientVersion`、`updateTargetVersion`、`lastHeartbeatAt`(**客户端在线判定看 `ClientDevice.lastHeartbeatAt`**:每 30 秒心跳,90 秒内有成功上报即在线。 |
|
||||
| 更新目标、安装策略与最早安装时间持久化在设备记录中,离线设备下次上线继续领取。 |
|
||||
|
||||
### 几个容易忽略的设计
|
||||
|
||||
**`ClassIslandPush` 与 `IntegrationDevice`** —— 提醒是**落库**的,不是"发出去就算"。
|
||||
设备离线时通知不丢:插件重连后拉取未确认的那条立即弹出,弹完回执(`ackedAt`)。
|
||||
`expiresAt` 防止学生几天后突然弹出旧提醒。
|
||||
|
||||
**在线判定看 `lastHeartbeatAt`,不看 `lastSeenAt`** —— 同一台设备的上报其实有两种:
|
||||
心跳(每 60 秒一次,只表明"我还在线")与带状态的上报(课表 / 上课状态)。
|
||||
合成一个字段后无法区分"在线但一直没新状态"和"整个失联",所以拆开:
|
||||
`lastHeartbeatAt` 每次上报都刷(60 秒窗口判在线),`lastSeenAt` 只在带运行状态时刷。
|
||||
|
||||
**课表同步方向:服务端 → 教室是主方向。**
|
||||
`IntegrationDevice.mirrorScheduleToClassIsland` 默认 **true**(服务端自动下发),
|
||||
`syncScheduleToServer` 默认 **false**(教室不再自动回传)。要取教室的课表,走
|
||||
`POST /integrations/devices/:id/request-schedule` 这个**人工动作**(前端点击前会弹警告)——
|
||||
自动回传会在老师手排课之后被教室的旧课表悄悄覆盖。
|
||||
|
||||
**`Schedule.source`** —— 决定插件上报时能清理哪些行:只清 `classisland` 来源且本次未再上报的行。
|
||||
**`source` 只在 create 时确定,update 一律保持原值**(否则手排的课会被"收养"后又被清掉)。
|
||||
**`Schedule.source`** —— 联动退役后历史同步课表统一归为 `manual`,保留排课数据。
|
||||
|
||||
**`Homework.dueAt`** —— 截止时间功能已下线,UI 与接口都不再读写它。
|
||||
保留列是为了**不对已安装的库做破坏性迁移**;新库里它恒为 `null`。
|
||||
@@ -148,7 +126,7 @@ add_permission_groups_and_clients
|
||||
生成骨架,再手工补数据搬运语句;
|
||||
3. 改完核对 **`SNAPSHOT_TABLES`**(新增 / 删除表要同步)与 `deleteTeacher` 的护栏清单
|
||||
(它必须覆盖 `Class.teacherId` / `Course.teacherId` / `Homework.createdBy` /
|
||||
`Notification.createdBy` 四项;`ClassIslandPush.createdBy` 是裸列、无外键、不级联,不用加);
|
||||
`Notification.createdBy` 四项);
|
||||
再回到 `packages/server/scripts/apply-column-migrations.cjs` 补上可 `ALTER TABLE` 的简单列。
|
||||
|
||||
模型总数与迁移个数会随版本增长,以 `schema.prisma` 与 `prisma/migrations/` 的实际内容为准;
|
||||
|
||||
+6
-9
@@ -7,13 +7,12 @@ description: ClassHelper的架构、接口、数据模型、权限、动效层
|
||||
|
||||
ClassHelper是一个 **pnpm monorepo**,一份代码产出四个交付物:
|
||||
|
||||
| 交付物 | 位置 | 形态 |
|
||||
| -------------------- | ----------------------------- | --------------------------------------------------------------- |
|
||||
| 后端服务 | `packages/server` | Express 5 + Prisma 7 + Socket.IO,接口前缀 `/api` |
|
||||
| Web 管理端 | `packages/web-admin` | Vue 3 + Vite + Element Plus(教师 / 管理员用,PWA 可安装) |
|
||||
| 桌面客户端 | `packages/desktop-client` | Electron + Vue 3(ClassHelper 班级端 / 教室机器,含灵动岛浮窗) |
|
||||
| 共享契约 | `packages/shared` | 三端共用的类型 / 常量 / 权限 / 工具函数 |
|
||||
| ClassIsland 联动插件 | `packages/classisland-plugin` | .NET 8 / C#(**独立于 pnpm workspace**,不参与 `pnpm install`) |
|
||||
| 交付物 | 位置 | 形态 |
|
||||
| ---------- | ------------------------- | --------------------------------------------------------------- |
|
||||
| 后端服务 | `packages/server` | Express 5 + Prisma 7 + Socket.IO,接口前缀 `/api` |
|
||||
| Web 管理端 | `packages/web-admin` | Vue 3 + Vite + Element Plus(教师 / 管理员用,PWA 可安装) |
|
||||
| 桌面客户端 | `packages/desktop-client` | Electron + Vue 3(ClassHelper 班级端 / 教室机器,含灵动岛浮窗) |
|
||||
| 共享契约 | `packages/shared` | 三端共用的类型 / 常量 / 权限 / 工具函数 |
|
||||
|
||||
核心链路:
|
||||
|
||||
@@ -26,7 +25,6 @@ ClassHelper是一个 **pnpm monorepo**,一份代码产出四个交付物:
|
||||
▼
|
||||
Socket.IO ──► class:{classId} 房间 ──► 学生桌面客户端实时更新 UI(含灵动岛)
|
||||
|
||||
ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/classisland/*(设备令牌鉴权)
|
||||
▲ │
|
||||
└──── 老师下发的提醒 ◄──────┘
|
||||
```
|
||||
@@ -41,7 +39,6 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
|
||||
| [数据模型](data-model.md) | 24 张表、迁移历史、跨库约定 |
|
||||
| [角色与权限](permissions.md) | 权限矩阵与实现位置 |
|
||||
| [动效层与主题](motion.md) | beUI 令牌、CSS `linear()` 弹簧、深色主题 |
|
||||
| [ClassIsland 插件](classisland-plugin.md) | 插件契约、镜像为什么要带 `startTime`、部署与调试 |
|
||||
| [灵动岛设计令牌](winisland-design-tokens.md) | 岛逐项的几何 / 配色 / 排版 token |
|
||||
| [验收与测试](testing.md) | 五套验收脚本怎么跑、哪些用例是脆弱的 |
|
||||
| [版本迭代与发布](release.md) | 改版本号、出安装包、发 Release 的完整流程 |
|
||||
|
||||
+70
-45
@@ -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 导入。
|
||||
服务端兼容旧客户端的心跳字段读取,旧设备可领取修复版;第一次升级仍遵循旧版空闲安装条件,新策略需修复版客户端。
|
||||
@@ -30,8 +30,6 @@ io(url, { auth: { token } }); // 握手阶段用 JWT 鉴权
|
||||
| `schedule:updated` | 服务端 → 客户端 | `{ classId, action, schedule? }` |
|
||||
| `class:updated` | 服务端 → 客户端 | `{ classId, action }` |
|
||||
| `call:new` | 服务端 → 客户端 | 叫人消息(定向到 `user:{studentId}` / 班级设备) |
|
||||
| `classisland:state` | 服务端 → 客户端 | `ClassIslandStateEvent`(教室现在上什么课) |
|
||||
| `classisland:notification` | 服务端 → 客户端 | `ClassIslandNotificationEvent`(提醒已下发) |
|
||||
| `class:join` / `class:leave` / `ping` | 客户端 → 服务端 | 手动订阅班级(服务端二次校验权限) |
|
||||
|
||||
## 房间规则
|
||||
@@ -44,7 +42,7 @@ io(url, { auth: { token } }); // 握手阶段用 JWT 鉴权
|
||||
| `user:{userId}` | 某个用户自己的连接 |
|
||||
| `teacher:{teacherId}` | 某个教师 |
|
||||
| `role:{role}` | 某一角色(`ADMIN` / `TEACHER` …) |
|
||||
| `students` | 全部 ClassHelper 班级端连接 |
|
||||
| `students` | 全部 ClassHelper 班级端连接 |
|
||||
| `teachers` | 全部教师端连接 |
|
||||
|
||||
服务端通过 `realtime/bus.ts` 的 `emitToClass` / `emitToUser` 发事件 ——
|
||||
|
||||
+13
-23
@@ -10,33 +10,31 @@ description: 改版本号、跑验收、打安装包、发 GitHub Release、发
|
||||
> **版本号按「发布」迭代,不按「提交」迭代。**
|
||||
|
||||
日常的 `feat` / `fix` 提交**不动版本号**。只有当你要产出一批安装包、在 GitHub 上开一个 Release 时,
|
||||
才走一次完整的版本迭代。理由很实际:安装包、`.cipx`、Docker 镜像 tag、文档站的版本徽标、
|
||||
更新检查接口全都按版本号对齐;每次提交都动它,等于每天都在发版,用户那边的「检查更新」就废了。
|
||||
|
||||
| 算一次发布 | 不算 |
|
||||
| --------------------------------------- | ---------------------------- |
|
||||
| 攒够一批功能 / 修好一个用户能感知的 bug | 改注释、改文档、重构、加测试 |
|
||||
| 要给别人一份新安装包 | 只在自己机器上跑 |
|
||||
| 插件单独发补丁(只动插件第 4 段版本号) | — |
|
||||
|
||||
## 每次发版必须一起交付的四样东西
|
||||
|
||||
打安装包只是其中一件。**少一样都算「发到一半」**:
|
||||
|
||||
| # | 交付物 | 落在哪 | 怎么做 |
|
||||
| -- | ----------------- | ------------------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| ① | **更新日志** | GitHub Release 的正文 | 手写,草稿放 `releases/ReleaseNotes-<版本>.md`,用 `--notes-file` 发布。别只挂自动生成的列表 |
|
||||
| ② | **Linux 服务端包** | Release 上的 `classhelper-server-linux-x64-<版本>.tar.gz` | tag 推上去由 CI 构建;**推完去 Actions 确认没红**(见第 7 步) |
|
||||
| ③ | **文档** | `docs/reference.md`(功能说明 + 验收对照表)与相关章节 | 新增功能补对应小节;改交付形态一并改第 2 章 / 第 7 章 |
|
||||
| ④ | **官网与 README** | `website/index.html`、`website/download.html`、`README.md` | 新功能补进官网功能块与 README 功能清单;版本号由脚本覆盖 |
|
||||
| # | 交付物 | 落在哪 | 怎么做 |
|
||||
| --- | ------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| ① | **更新日志** | GitHub Release 的正文 | 手写,草稿放 `releases/ReleaseNotes-<版本>.md`,用 `--notes-file` 发布。别只挂自动生成的列表 |
|
||||
| ② | **Linux 服务端包** | Release 上的 `classhelper-server-linux-x64-<版本>.tar.gz` | tag 推上去由 CI 构建;**推完去 Actions 确认没红**(见第 7 步) |
|
||||
| ③ | **文档** | `docs/reference.md`(功能说明 + 验收对照表)与相关章节 | 新增功能补对应小节;改交付形态一并改第 2 章 / 第 7 章 |
|
||||
| ④ | **官网与 README** | `website/index.html`、`website/download.html`、`README.md` | 新功能补进官网功能块与 README 功能清单;版本号由脚本覆盖 |
|
||||
|
||||
更新日志的骨架(照 1.2.0 / 1.3.0 那两份写):
|
||||
**一句话定位 → 分节讲功能 → ⚠️ 升级须知(含会自动应用的迁移)→ 验收表 → 产物表**。
|
||||
验收表里**只写真跑过的数字**,没跑就直说没跑,不要照抄上一版的。
|
||||
|
||||
## 版本号写在哪(14 处 / 12 个文件)
|
||||
## 版本号写在哪(12 处 / 12 个文件)
|
||||
|
||||
同一个版本号分散在 14 个落点上,**手工改必然漏一处**,而漏掉的那处不会报错:
|
||||
同一个版本号分散在 12 个落点上,**手工改必然漏一处**,而漏掉的那处不会报错:
|
||||
|
||||
| 文件 | 内容 |
|
||||
| ---------------------------------------------------------------- | ---------------------------------- |
|
||||
@@ -44,8 +42,6 @@ description: 改版本号、跑验收、打安装包、发 GitHub Release、发
|
||||
| `packages/{shared,server,web-admin,desktop-client}/package.json` | 各包的版本 |
|
||||
| `deploy/package.runtime.json` | 安装包 / 容器里的运行时依赖清单 |
|
||||
| `deploy/docker-compose.yml`、`docker-compose.sqlite.yml` | 镜像 tag 的默认值 |
|
||||
| `packages/classisland-plugin/manifest.yml` | 插件清单(四段:`x.y.z.0`) |
|
||||
| `packages/classisland-plugin/src/Services/BridgeService.cs` | 插件上报的 `PluginVersion` 常量 |
|
||||
| `website/index.html` × 3 | 官网顶栏徽标 / 首屏芯片 / 页脚 |
|
||||
| `website/download.html`(`<meta name="ch-version">`) | 下载页读不到版本清单时的兜底版本号 |
|
||||
|
||||
@@ -65,9 +61,6 @@ pnpm version:check # 再确认一次
|
||||
|
||||
脚本按表逐个改写,**任何一处没匹配到或匹配到多处就直接失败退出**,不做"尽力而为"的部分改写。
|
||||
|
||||
> 插件版本是 **四段** `x.y.z.0`:前三段跟产品版本走,第 4 段留给"只改插件、不动产品版本"的补丁发布。
|
||||
> `--bump` / `--set` 会把第 4 段归零;要单独发插件补丁就手工改那两处(清单 + `BridgeService.cs`)。
|
||||
|
||||
## 一次完整的发布
|
||||
|
||||
下面这一串命令就是全部流程,逐条照抄即可。
|
||||
@@ -94,8 +87,6 @@ pnpm docs:check # 文档站构建(警告即失败)
|
||||
pnpm dev:server # 另开一个终端跑着
|
||||
|
||||
pnpm verify:e2e
|
||||
pnpm verify:classisland
|
||||
pnpm verify:classisland-plugin
|
||||
|
||||
pnpm build:desktop && pnpm verify:desktop
|
||||
pnpm build && pnpm verify:web
|
||||
@@ -107,7 +98,7 @@ pnpm build && pnpm verify:web
|
||||
### 4. 打安装包
|
||||
|
||||
```bash
|
||||
pnpm dist:all # = dist:server + dist:win + dist:classisland-plugin
|
||||
|
||||
```
|
||||
|
||||
产物落在 `releases/<组件>/<版本>/`:`安装包/`(交付物)、`构建中间/`(可重新生成)。
|
||||
@@ -127,7 +118,7 @@ pnpm verify:packaged --exe "releases/client/<版本>/安装包/ClassHelper-<版
|
||||
### 5. 生成跨组件哈希清单
|
||||
|
||||
`releases/SHA256SUMS-<版本>.txt` 是**人读的**(表头 + 体积 + 说明),由发布流程维护,
|
||||
本地只写 Windows 与插件那几行:
|
||||
本地只写 Windows那几行:
|
||||
|
||||
```bash
|
||||
cd releases/client/<版本>/安装包 && sha256sum *.exe > ../../../SHA256SUMS-<版本>.txt # 按实际文件名整理
|
||||
@@ -156,7 +147,6 @@ mkdir -p .cache/release-staging
|
||||
cp "releases/client/$V/安装包/"*-x64-setup.exe ".cache/release-staging/ClassHelper-$V-x64-client-setup.exe"
|
||||
cp "releases/client/$V/安装包/"*-x64-portable.exe ".cache/release-staging/ClassHelper-$V-x64-client-portable.exe"
|
||||
cp "releases/server/$V/安装包/"*-x64-setup.exe ".cache/release-staging/ClassHelper-$V-x64-server-setup.exe"
|
||||
cp "releases/classisland-plugin/$V/安装包/ClassHelper.ClassIslandPlugin.cipx" .cache/release-staging/
|
||||
|
||||
gh release create v$V --title "ClassHelper v$V(<一句话定位>)" --notes-file "releases/ReleaseNotes-$V.md"
|
||||
gh release upload v$V .cache/release-staging/* "releases/SHA256SUMS-$V.txt" --clobber
|
||||
@@ -176,7 +166,7 @@ gh release upload v$V .cache/release-staging/* "releases/SHA256SUMS-$V.txt" --cl
|
||||
构建 Linux 包(ubuntu runner)。它跑完之后会:
|
||||
|
||||
1. 把 `classhelper-server-linux-x64-<版本>.tar.gz` 与 `.sha256` 挂到 Release 上;
|
||||
2. **把 Linux 那一行合并进** `SHA256SUMS-<版本>.txt`(保留 Windows / 插件那几行)。
|
||||
2. **把 Linux 那一行合并进** `SHA256SUMS-<版本>.txt`(保留 Windows那几行)。
|
||||
|
||||
> **顺序很重要**:Linux workflow 是在最后一步才去合并 SHA256SUMS 的,
|
||||
> 所以第 5 步那份清单必须**先上传**。万一手速反了(workflow 先跑完、你后上传),
|
||||
@@ -213,7 +203,7 @@ https://lovechengke.github.io/ClassHelper/docs/ ← 文档站
|
||||
|
||||
文档站顶栏的版本徽标取自根 `package.json`,所以第 1 步改完就跟着变了;
|
||||
官网首屏那排芯片、顶栏徽标与页脚里的版本号,以及下载页那个兜底版本号,
|
||||
也都是**由 `pnpm version:bump` 一起改的**(它们是那 14 个落点里的四个)。
|
||||
也都是**由 `pnpm version:bump` 一起改的**(它们是那 12 个落点里的四个)。
|
||||
|
||||
> 官网的「立即下载」会跳到 `/download.html`,那一页的版本清单是**当场**从 GitHub Releases 读的
|
||||
> (读不到才退回上面那个兜底版本号),所以发完 Release 不用重新部署那一页 —— 但**兜底值要跟着发版一起改**,
|
||||
@@ -221,7 +211,7 @@ https://lovechengke.github.io/ClassHelper/docs/ ← 文档站
|
||||
|
||||
### 9. 检查
|
||||
|
||||
- [ ] Release 页面上四个组件都在,`SHA256SUMS-<版本>.txt` 里 Windows / 插件 / Linux 三种行齐全;
|
||||
- [ ] Release 页面上三个组件都在,`SHA256SUMS-<版本>.txt` 里 Windows / Linux 三种行齐全;
|
||||
- [ ] 文档站能打开,顶栏版本号是新版;
|
||||
- [ ] 装一台机器试一遍(或至少跑 `pnpm verify:packaged`)。
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: 2026-10-17 审查修复记录
|
||||
description: 按审查报告核对缺陷、修复行为与验收方式。
|
||||
---
|
||||
|
||||
# 2026-10-17 审查修复记录
|
||||
|
||||
本次按仓库根的代码审查报告逐项核对,保留此前联动退役与更新策略改动。
|
||||
|
||||
| 报告条目 | 处理结果 |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0.1 | 环境文档补充 PATH 与 Git Bash 启动方式;变更文档统一格式化。 |
|
||||
| 1.1 | 恢复学期周次、毕业归档、权限组菜单,开发期检查路由是否遗漏菜单。 |
|
||||
| 1.2 | 叫人通知写本地所属日期;启动时分批幂等回填旧通知,兼容 SQLite / MySQL。 |
|
||||
| 1.3、2.14、3.10、3.14、4.5 | 每次建窗挂关闭隐藏处理;捕获加载失败;仅主窗口触发恢复,恢复期间等待异步加载;普通 Promise 拒绝不再重启进程,移除 activate 重复加载。 |
|
||||
| 1.4、1.5 | 缓存按服务端和班级隔离,个人资料按服务端和登录名隔离;忽略跨会话的迟到响应,打开失败和版本切换后可重试 IndexedDB。旧无命名空间缓存不再直接作为新会话数据读取。 |
|
||||
| 1.6 | 学期周统一加载后再取灵动岛课表,教学周变化触发重取;失败且无缓存时保留已知课表;主窗口禁用后台节流。 |
|
||||
| 1.7、3.11 | IPC 拒绝渲染端改安装记录;安装只接受更新专用目录中的文件,拒绝目录外及软链接目标;取消、更换目标和更新完成后清理旧包。 |
|
||||
| 2.1 | 空角色原本已保留旧值;现在未识别的非空值也保留原角色,只有明确的教师/管理员值才变更。 |
|
||||
| 2.2、2.9 | 员工的成绩明细、看板、更新检查与强制检查落实权限;班级端仍由本班边界授权。增加学生/教师名单只读权限,旧管理组继承对应读取能力;下线没有独立服务端能力的日志与开发者模式权限键,系统设置仍由系统管理权限控制。 |
|
||||
| 2.3 | 当前班级范围已由 CLASS_DEVICE 角色正确解析,不存在报告描述的必然拒绝,也不会查询全班学生。补齐 Socket 身份中的 classSession,保持元数据完整。 |
|
||||
| 2.4、2.5 | 课表导入读取本班学期长度,创建课表按实际兜底周数做区间交叉校验。 |
|
||||
| 2.6 | 姓名重复时要求正确学号,不能静默绑定到同名学生。 |
|
||||
| 2.7 | 增加共享删除事件与两端订阅,并移除通知中心及灵动岛中的已删通知。 |
|
||||
| 2.8 | 批量录入接口本身只接收单个 classId;真正缺口在跨班批量改等级,现已逐班广播。 |
|
||||
| 2.10 | 调班历史按学生当前班级及显式班级过滤条件校验权限;未分班学生仅管理员可读。 |
|
||||
| 2.11—2.13 | 引导使用主题文字变量,仅可见时响应 Esc;成绩明细优先展示保存的等级。 |
|
||||
| 2.15—2.17 | 助手叫人校验非空消息;周次草稿提交前校验日期;通知日期改变后清除不再可选的发布人。发布人选择本身已有 change 处理,未重复添加。 |
|
||||
| 3.1、3.4、3.9 | 列表丢弃过期响应;考试口径读写固定同一个班级/考试快照并禁用加载中的开关;切班立即清空课程候选。 |
|
||||
| 3.2 | 提交函数增加互斥与 finally 释放,按钮同步显示 loading,快速双击不会重复发请求。 |
|
||||
| 3.3 | 考试下拉从独立考试列表生成,不再由筛选后的成绩反推。 |
|
||||
| 3.5—3.8 | 不回退到已归档届别;使用主题语义色、清理死样式;拒绝协议相对和反斜线附件地址;人数加载失败显示不可用。 |
|
||||
| 3.12、3.13 | 取消确认框正常返回;列表加载与实时新增、删除、已读合并,防止快照抹掉飞行中的变更。 |
|
||||
| 3.15 | 核对并修正登录、班级开关、设备权限、菜单数量、Socket 房间注释。登录限流当前已经一次挂载到两个入口并共享计数器,该条报告不成立。 |
|
||||
| 4.1 | 百分制/字母制批量重算使用分数计算等级;自定义等级没有通用换算规则,禁止整批猜算,保留逐条编辑。 |
|
||||
| 4.2 | 审计明确记录解析结果 interpreted / unrecognized,不再写成永远等待执行的 pending;它不宣称前端业务发布已成功。 |
|
||||
| 4.3、4.4 | 助手上下文与预览参数均校验班级、科目范围;缺失通知优先级按普通通知展示。 |
|
||||
| 4.6 | 外观 IPC 在一帧内合并滑块事件,读取时先刷新最后一帧,避免丢失最终值。 |
|
||||
| 5 | 清理无调用的名单/课程封装和事件标签等;班级页接通既有任课关系导入。旧时间配置弹窗仍直接调用四个 API,按报告的保留要求保留组件及依赖,不盲目删除。 |
|
||||
|
||||
管理员的 Web 权限判断已移除全权限短路,与服务端保持一致;默认组收回的功能入口会隐藏。
|
||||
日常冒烟采用后台快速模式,视觉验收显式执行;具体命令和验证范围见[验收与测试](testing.md)。
|
||||
|
||||
## 本次验证
|
||||
|
||||
- 全仓类型检查、ESLint、构建、变更格式检查通过,文档构建无警告。
|
||||
- 隔离测试库端到端 243/243;审查接口回归 31/31;客户端维护接口 28/28。
|
||||
- 无窗口逻辑回归 18/18;模拟远控及更新代理 15/15;旧库升级回归通过。
|
||||
- 后台 Web 回归 37/37,约 38 秒;客户端联网冒烟 10/10,二次启动 9/9,总计约 15 秒。
|
||||
- 本次使用独立 SQLite 测试库,没有执行真实关机或安装;未运行会占用桌面的完整视觉验收。
|
||||
+50
-46
@@ -1,27 +1,33 @@
|
||||
---
|
||||
title: 验收与测试
|
||||
description: 五套验收脚本怎么跑、覆盖什么、哪些用例是脆弱的,以及怎么判断失败是不是真回归。
|
||||
description: 验收验收脚本怎么跑、覆盖什么、哪些用例是脆弱的,以及怎么判断失败是不是真回归。
|
||||
---
|
||||
|
||||
# 验收与测试
|
||||
|
||||
这个仓库的验收方式是**跑真东西**:真的起后端、真的开 Electron 窗口、真的点击菜单、
|
||||
真的发一条通知。不是单元测试,是端到端回归。
|
||||
验收分为后台快速冒烟、隔离的接口与逻辑回归、显式视觉验收。日常默认不显示窗口、不占用前台;
|
||||
涉及动画、桌面像素和灵动岛几何时,再单独运行视觉验收。
|
||||
|
||||
## 五套脚本
|
||||
## 验收脚本
|
||||
|
||||
| 命令 | 覆盖 | 需要什么 |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------- | --------------------------- |
|
||||
| `pnpm verify:e2e` | 后端 + 实时推送 + RBAC 越权 + 上课时段 + 导入 + 班级账号 + 数据库管理 + 更新检查 | **后端已启动** |
|
||||
| `pnpm verify:web` | Web 管理端真实点击回归(Electron 驱动) | 后端已启动 + Web 产物已构建 |
|
||||
| `pnpm verify:desktop` | 客户端冒烟(Electron,无人工点击) | 已构建的客户端产物 |
|
||||
| `pnpm verify:packaged` | 对**打包后 / 已安装**的客户端 EXE 复跑同一套冒烟(`--exe` 指定路径) | 打包产物 |
|
||||
| `pnpm verify:classisland` | 设备令牌 / 上报 / 提醒下发与回执 / 镜像契约 / 幽灵行清理 | 后端已启动 |
|
||||
| `pnpm verify:classisland-plugin` | 插件静态契约(清单一致性 / 注册完整性 / C# DTO ↔ 服务端 zod 与路由) | 不需要 .NET,也不需要后端 |
|
||||
| 命令 | 覆盖 | 需要什么 |
|
||||
| ---------------------- | -------------------------------------------------------------------------------- | --------------------------- |
|
||||
| `pnpm verify:e2e` | 后端 + 实时推送 + RBAC 越权 + 上课时段 + 导入 + 班级账号 + 数据库管理 + 更新检查 | **后端已启动** |
|
||||
| `pnpm verify:web` | Web 管理端真实点击回归(Electron 驱动) | 后端已启动 + Web 产物已构建 |
|
||||
| `pnpm verify:desktop` | 客户端冒烟(Electron,无人工点击) | 已构建的客户端产物 |
|
||||
| `pnpm verify:packaged` | 对**打包后 / 已安装**的客户端 EXE 复跑同一套冒烟(`--exe` 指定路径) | 打包产物 |
|
||||
|
||||
条目数由脚本自行统计并打印(`=== 结果:N/N 项通过 ===`),**文档里不写死容易过期的总数** ——
|
||||
每次跑完看脚本输出。
|
||||
|
||||
客户端维护专项回归:
|
||||
|
||||
- `pnpm verify:client-agent`:模拟操作系统验证远控、回执重试与安装策略,不执行真实关机或安装器。
|
||||
- `pnpm verify:client-migrations`:创建临时旧库验证升级、联动移除与默认组编辑持久化。
|
||||
- `pnpm verify:clients-api`:面向 `http://127.0.0.1:4199` 的演示测试库(可用 `VERIFY_BASE_URL` 指定),验证权限、远控与更新接口。会创建并清理测试记录,请使用隔离测试服务。
|
||||
- `pnpm verify:review-client`:无窗口验证缓存故障恢复、通知竞态、教学周同步、管理员界面权限与崩溃恢复。
|
||||
- `pnpm verify:review-api`:针对审查缺陷验证接口权限、导入边界和实时删除等,必须连接隔离测试服务。
|
||||
|
||||
另外三套是"门禁"而不是验收:`pnpm typecheck`、`pnpm lint`、`pnpm check:icons`。
|
||||
|
||||
### 一次完整的交付前检查
|
||||
@@ -32,8 +38,6 @@ pnpm typecheck # 全仓库类型检查(含 vue-tsc)
|
||||
|
||||
pnpm dev:server # 另开一个终端
|
||||
pnpm verify:e2e
|
||||
pnpm verify:classisland
|
||||
pnpm verify:classisland-plugin
|
||||
|
||||
pnpm build:desktop && pnpm verify:desktop
|
||||
pnpm build && pnpm verify:web
|
||||
@@ -46,47 +50,49 @@ pnpm build && pnpm verify:web
|
||||
|
||||
下面这些用例**不是回归**,遇到时先看条件。完整清单在 `AGENTS.md` 的「已知脆弱用例」表。
|
||||
|
||||
| 用例 | 触发条件 |
|
||||
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `verify:desktop`「真实通知链路」「灵动岛标为已读」等 3 项 | 要求**当前不在上课时段**。种子课表恰有课正在进行时,客户端会**正确地**把普通通知暂存到下课 —— 这三项就会红(日志里会写 `客户端判定上课中=true`) |
|
||||
| `verify:e2e`「上课状态接口(at=周一 08:10)」 | 种子把周一第 1 节限制在第 1 周,且用例把 `2026-09-07` 当第 1 周。`.env` 的 `TERM_START_DATE` 对齐即可全绿 |
|
||||
| `verify:web`「课表科目为全校统一目录」 | 种子给每个班建满了全部科目,下拉里就没有可自动建课的「统一科目」分组 |
|
||||
| `verify:desktop`「课表今天时间轴」 | 凌晨约 00:00–03:00 时「已结束」探针被夹紧成 0 长度而被过滤 |
|
||||
| `verify:desktop`「作业看板全屏自适应」 | 演示数据某天有 6 条长作业、而冒烟窗口只有约 543px 高时,缩放被下限 0.45 钳住 → 必然溢出 |
|
||||
| 岛交互 / 岛动画 / 岛截图留档 | 窗口被遮挡或跑在远程会话里时会间歇性失败:置顶透明小窗的 rAF 被限流、形变采样抓不到帧。**先原样重跑一次再动手改** |
|
||||
| 用例 | 触发条件 |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `verify:desktop:visual`「真实通知链路」「灵动岛标为已读」等 3 项 | 要求**当前不在上课时段**。种子课表恰有课正在进行时,客户端会**正确地**把普通通知暂存到下课 —— 这三项就会红(日志里会写 `客户端判定上课中=true`) |
|
||||
| `verify:e2e`「上课状态接口(at=周一 08:10)」 | 种子把周一第 1 节限制在第 1 周,且用例把 `2026-09-07` 当第 1 周。`.env` 的 `TERM_START_DATE` 对齐即可全绿 |
|
||||
| `verify:web`「课表科目为全校统一目录」 | 种子给每个班建满了全部科目,下拉里就没有可自动建课的「统一科目」分组 |
|
||||
| `verify:desktop:visual`「课表今天时间轴」 | 凌晨约 00:00–03:00 时「已结束」探针被夹紧成 0 长度而被过滤 |
|
||||
| `verify:desktop:visual`「作业看板全屏自适应」 | 演示数据某天有 6 条长作业、而冒烟窗口只有约 543px 高时,缩放被下限 0.45 钳住 → 必然溢出 |
|
||||
| 岛交互 / 岛动画 / 岛截图留档 | 窗口被遮挡或跑在远程会话里时会间歇性失败:置顶透明小窗的 rAF 被限流、形变采样抓不到帧。**先原样重跑一次再动手改** |
|
||||
|
||||
## 大面积报「登录失败 / 401」时
|
||||
|
||||
先确认演示账号的**当前**口令。种子脚本硬编码的是 `teacher123`,但「修改密码」弹窗留空会落到
|
||||
默认初始密码(`DEFAULT_TEACHER_PASSWORD`,默认 `123456`),任何一次误操作都会把它改掉 ——
|
||||
而**两个演示账号不一定一起漂**,所以不能简单地全局替换。
|
||||
先检查隔离服务是否启动,以及测试管理员的当前口令。不要修改真实班级密码来迁就测试。
|
||||
联网客户端和 Web 回归会自行创建临时教师与班级;下面的环境变量只配置测试管理员:
|
||||
|
||||
**处理方式是用环境变量传实际凭据,不要去改数据。**
|
||||
| 脚本 | 管理员凭据 |
|
||||
| ------------------- | -------------------------------------------------------- |
|
||||
| 客户端联网/打包冒烟 | `ELECTRON_SMOKE_ADMIN` / `ELECTRON_SMOKE_ADMIN_PASSWORD` |
|
||||
| Web 回归 | `UI_SMOKE_ADMIN` / `UI_SMOKE_ADMIN_PASS` |
|
||||
| 后端端到端回归 | 使用隔离种子库,口令由种子脚本与验收脚本约定 |
|
||||
|
||||
| 脚本 | 覆盖凭据的方式 |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `verify:desktop` | `set "ELECTRON_SMOKE_USER=teacher1" && set "ELECTRON_SMOKE_PASSWORD=123456" && pnpm verify:desktop` |
|
||||
| `verify:packaged` | 同上(`ELECTRON_SMOKE_*`) |
|
||||
| `verify:web` | `set "UI_SMOKE_USER=teacher1" && set "UI_SMOKE_PASS=123456" && pnpm verify:web`(另有 `UI_SMOKE_ADMIN*`) |
|
||||
| `verify:e2e` | **不支持环境变量**(口令硬编码在脚本里),需自己用改过口令的临时副本跑;副本必须放在 `packages/server/scripts/` 里,否则 `import 'socket.io-client'` 会失败 |
|
||||
默认离线快速冒烟不需要服务端凭据。
|
||||
|
||||
## 冒烟会真的动你的屏幕
|
||||
## 默认后台运行,视觉验收单独执行
|
||||
|
||||
`verify:web` / `verify:desktop` / `verify:packaged` 会**真的弹窗口**、真的发通知、真的移动鼠标:
|
||||
`pnpm verify:desktop` 默认运行隐藏、不可聚焦的离屏窗口,仅检查启动、桥接白名单、缓存读写、
|
||||
跨服务端/跨班级缓存隔离和关闭隐藏。二次启动也只跑快速检查,不再重复整套动画。
|
||||
|
||||
- 跑的时候别动键鼠;
|
||||
- 验证实例的灵动岛是**洋红主题色**(`#e91e8c`),一眼可以与你自己客户端那个区分开。
|
||||
两条反馈里的"屏幕上出现了第二个灵动岛""点它反应不对"都源于这个叠影;
|
||||
- 冒烟会往演示班发几条「张老师 · _自检_」通知,用完即删;中途被 kill 会留下残条。
|
||||
需要联网时运行 `pnpm verify:desktop:online`,创建临时教师与临时班级,结束后删除。
|
||||
不再自动探测并复用真实班级,也不会重置真实班级密码。Web 回归同样使用隔离的验证教师与班级。
|
||||
|
||||
`pnpm verify:desktop:visual`(或客户端启动器 `--visual`)才运行灵动岛动画、透明度、桌面截图等完整验收,
|
||||
这类用例确实需要使用桌面。`verify:packaged` 也默认快速模式,显式传 `--visual` 才跑视觉验收。
|
||||
视觉验收不适合在用户操作屏幕时自动运行。截图采集命令需要传入 `--visual`(或设置 `ELECTRON_SMOKE_MODE=visual`)。
|
||||
结果记录模式和耗时;启动器设置超时,避免验证进程无限等待。
|
||||
|
||||
## 截图留档
|
||||
|
||||
截图分两套,各管一件事:
|
||||
|
||||
| 目录 | 谁在用 | 谁产出 |
|
||||
| ------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `docs/images/` | **文档站、README、官网用的是同一份图**,页面上展示的都是它 | `capture-shots.mjs`(Web / 手机)+ 客户端冒烟(客户端 / 灵动岛) |
|
||||
| `docs/screenshots/` | 回归留档,页面不展示;`verify:*` 的像素级断言比对的就是它们 | 各冒烟脚本自动写入(island / client / web-mobile / classisland 四组) |
|
||||
| 目录 | 谁在用 | 谁产出 |
|
||||
| ------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `docs/images/` | **文档站、README、官网用的是同一份图**,页面上展示的都是它 | `capture-shots.mjs`(Web / 手机)+ 客户端冒烟(客户端 / 灵动岛) |
|
||||
| `docs/screenshots/` | 回归留档,页面不展示;`verify:*` 的像素级断言比对的就是它们 | 各冒烟脚本自动写入(island / client / web-mobile 三组) |
|
||||
|
||||
**展示图重新采集**(保证文档站与 README、官网用的是同一份图):
|
||||
|
||||
@@ -98,8 +104,7 @@ CAPTURE_OUT=docs/images,website/assets/shots node website/tools/capture-shots.mj
|
||||
# (*-window.png / *-backdrop.png),所以先落到临时目录,再挑要用的拷进去。
|
||||
ISLAND_SHOTS_DIR="$PWD/.cache/shots" ELECTRON_CLIENT_SHOTS_DIR="$PWD/.cache/shots" \
|
||||
ELECTRON_SMOKE_ISLAND_ACCENT='#0a84ff' ELECTRON_SMOKE_ISLAND_STYLE=black \
|
||||
ELECTRON_SMOKE_USER=teacher1 ELECTRON_SMOKE_PASSWORD=123456 \
|
||||
node packages/desktop-client/scripts/smoke.mjs
|
||||
node packages/desktop-client/scripts/smoke.mjs --visual --online
|
||||
cp .cache/shots/{island-*,client-*}.png docs/images/
|
||||
rm -f docs/images/*-window.png docs/images/*-backdrop.png
|
||||
cp docs/images/{island-*,client-*}.png website/assets/shots/ # 官网那份保持同步
|
||||
@@ -112,8 +117,7 @@ pnpm dev:server
|
||||
ISLAND_SHOTS_DIR="$PWD/.cache/island-shots" \
|
||||
ELECTRON_CLIENT_SHOTS_DIR="$PWD/.cache/client-shots" \
|
||||
ELECTRON_SMOKE_ISLAND_ACCENT='#0a84ff' ELECTRON_SMOKE_ISLAND_STYLE=black \
|
||||
ELECTRON_SMOKE_USER=teacher1 ELECTRON_SMOKE_PASSWORD=123456 \
|
||||
node packages/desktop-client/scripts/smoke.mjs
|
||||
node packages/desktop-client/scripts/smoke.mjs --visual --online
|
||||
```
|
||||
|
||||
> 展示图都是**逻辑像素 1×**(灵动岛的 268×44、424×230 就是这个数,不再乘 2),
|
||||
|
||||
+2
-2
@@ -12,11 +12,11 @@
|
||||
href: permissions.md
|
||||
- name: 动效层与主题
|
||||
href: motion.md
|
||||
- name: ClassIsland 插件
|
||||
href: classisland-plugin.md
|
||||
- name: 灵动岛设计令牌
|
||||
href: winisland-design-tokens.md
|
||||
- name: 验收与测试
|
||||
href: testing.md
|
||||
- name: 2026-10-17 审查修复记录
|
||||
href: review-2026-10-17.md
|
||||
- name: 版本迭代与发布
|
||||
href: release.md
|
||||
Reference in new issue
Block a user