Files
ClassHelper/docs/dev/api.md
T
LoveChengke 817af02357 feat(permissions,clients,ai,system,server,web,client,shared)!: 权限组 + 客户端设备远控与自动更新 + 大模型助手 + 系统诊断(1.3.0)
功能级准入改为「权限键」,`requireRole` 退成默认模板。

权限组
- 权限目录(39 键:键 / 中文名 / 分类 / 默认角色)收口到 shared,路由挂 requirePermission / requireAnyPermission
- 有效权限每请求实时算,改完分组立刻生效:ADMIN 全集且 permission:manage、system:manage 不可剥夺;
  CLASS_DEVICE 空集(不受分组影响);有分组 = 并集(因此也能用来**收回**权限);无分组 = 角色默认模板
- 新增「权限组」页:组增删改、成员分配、生效权限预览
- 班级维度的科目规则(assertCanManageSubjectContent)故意不可被权限组覆盖

客户端设备(ClassHelper 桌面端)
- 设备身份是 deviceKey(首次启动生成的 UUID,saveConfig 拒绝改写、退出登录也保留)
- 心跳走「DB 记状态 + 客户端定时拉」,不引入新的长连接:30 秒一次,在线窗口 90 秒
- 远控命令 shutdown / restart / logoff / lock:服务端有效期 10 分钟,客户端弹默认选中「取消」的对话框、
  60 秒未确认按取消处理,执行后必须回执(重复回执幂等),同一台机器同一条命令只弹一次
- 新增「客户端设备」页:版本一览、一键推送更新、下发远控

客户端安装包由服务端下载并缓存(教室机常常没有外网,浏览器也受 CSP 限制)
- POST /api/update/client/fetch 由服务端下载到 data/client-releases/<版本>/ 并登记 sha256 + size
- 自动安装三条护栏:没有 sha256 就拒收、必须等到非上课且主窗口隐藏满 5 分钟、冒烟模式永不安装

大模型助手:只解析、不落库
- POST /api/ai/interpret 返回**待确认**的结构化动作(建作业 / 发通知 / 叫人),
  真正落库仍调既有端点 —— 权限断言、上课时段紧急通知保护、ClassIsland 推送全部自动复用
- 模型给的 params 用与手动入口**同一份** zod 再校验(模型输出不可信)
- API Key 走 AES-256-GCM(密钥由 JWT_SECRET 经 scrypt 派生)加密存 AppSetting,只进不出
- provider=mock 是离线确定性解析,自动化验收一律用它;每次解析写一条 AiAudit(不含密钥)

系统与运维
- 开发者模式(服务端内存缓存开关,热路径不查库;开启后 500 才附 error.stack)、运行诊断(绝不回传密钥)
- 开机自启动运行期可改:Windows 改 HKCU\...\Run、Linux systemctl enable/disable,
  **非安装形态一律返回 supported:false**,不往开发机的注册表里写自启项
- 服务端侧栏折叠只用 CSS 收拢(不重建 DOM),关怀模式只放大字号与控件、不动任何几何令牌

客户端
- 崩溃自愈:渲染进程挂掉 / 超 10 秒无响应先静默重建主窗口(10 分钟最多 3 次),
  未捕获异常只在「没在上课、没人看着」时才重启进程;冒烟模式绝不重启(会杀掉验证实例)
- 上线补弹离线期间收到的消息只做一次:起点只在「可达 → 不可达」的边界写,恢复时取最近 5 条按时间升序推

发布内容批量
- POST /notifications/bulk、POST /homeworks/bulk:**单班失败不影响整批**,逐班返回结果
- 作业的科目名跨班**按名匹配**已有课程,找不到就整班记失败,绝不自动建课
  (自动建课会把创建者写成任课老师,等于越权改任教关系)
- 通知与作业同口径按「所属日期」归档,新增 /notifications/days 与 /notifications/creators

BREAKING CHANGE: 能不能做某件事不再看角色名,而是看权限键(GET /api/permissions/me)。
未分组的账号按角色默认模板生效,行为与 1.2.0 一致;分组之后生效集合只由组的并集决定。
服务端首次启动会自动应用 1 个数据库迁移(权限组 3 表 / AppSetting / 客户端设备·命令·安装包 /
AiAudit / Notification.assignDate / User.careMode,历史通知按本地日期回填),升级前请先备份。
2026-10-11 11:49:32 +08:00

139 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: REST API
description: ClassHelper后端全部接口一览:方法、路径、权限与说明。
---
# REST API
## 通用约定
- 所有接口前缀 `/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 | `/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` | 公开 | 健康检查(含版本号与已挂载模块) |
另有三个探针(不带 `/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` |
| **三个接口故意不挂权限键** | `/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` 只解析不落库** | 助手只回结构化参数与中文预览;落库由前端确认后调既有端点 —— 权限与上课时段保护都在那条路径上 |
## 相关
- 实时事件见[实时事件(Socket.IO)](realtime.md);
- 权限矩阵见[角色与权限](permissions.md);
- 数据模型见[数据模型](data-model.md)。