feat(shared,server,web,client,plugin)!: 角色与权限重构 + 学期周次 + 课表方向反转(1.2.0)

学生、任课关系、周次口径三处长期靠约定维持的事情,改为由数据模型保证。

学生不是账号
- 拆出独立 Student 表,没有 passwordHash / role / 任何登录字段
- 「学生不能登录」由表结构保证,不再靠「登录前判 role 再 403」的代码分支
- 三张个人数据表外键 userId → studentId;删掉 Enrollment 与 ClassTeacher
- 学生一律称「学号」、教师一律称「工号」,「学生账号」这个概念消失

任课关系唯一来源 = Course(班级 + 科目 + 教师)
- 班主任 = Class.teacherId,科任 = Course.teacherId,同一人两处指向同一账号、不重复建号
- 作业与成绩按科目再收一道:挂科目只由该科任课老师改,没挂科目归班主任 / 管理员
- 新增「任课教师分配」与「班级教师批量导入」(带「使用工号匹配」开关)

成绩与名单
- 成绩等级:百分制 / 等级制 A·B·C·D / 自定义,支持单个与批量改等级
- 按学号查成绩明细(教师端 + 班级端 + 设备令牌),受按班开关控制
- 调班 / 转出(单个与批量)+ 可查历史;学号不变、历史作业成绩留原归属
- 毕业归档页(按年度记录毕业班级 / 毕业生 / 转出学生 / 该届通知与作业)
- 学年升级(未毕业不归档、数据沿用);班级称呼改为「2026级1班」

ClassHelper 班级端在线状态
- Web 端显示 班级 / 设备名 / 设备号 / 在线离线 / 最后在线 / 最后心跳
- lastHeartbeatAt 驱动 60 秒在线窗口,与只表示「带状态上报」的 lastSeenAt 分开

学期周次
- 新增 TermWeek(classId 空串 = 全校默认),管理员可用日期范围选择器逐周配置
- 一键按开学日期生成整学期;联网拉取调休建议(只给建议、不改数据,无外网失败属正常)
- 改周次后随课表下发给教室的 ClassHelper 班级端

课表方向反转
- 主方向改为「服务端 → 教室」自动下发;教室不再自动回传
- 取教室课表改为手动动作(点击前弹警告),插件按待办标记在下一次心跳补报

改名
- 班级小助手 → ClassHelper:界面、窗口标题、托盘、灵动岛、EXE、安装包、
  安装目录、systemd 描述、插件清单与设置页、官网、文档站、README

BREAKING CHANGE: 产品名与 EXE 名变更(班级小助手.exe → ClassHelper.exe),
安装目录随之改变,不支持覆盖安装,需先卸载旧版。
服务端首次启动会自动应用 6 个数据库迁移(学生行按 id 原样搬迁,历史数据不丢),
升级前请先备份。
This commit is contained in:
LoveChengke committed 2026-10-06 19:41:18 +08:00
1 parent 17f55864bf
commit 9d5ac7adf9
172 files changed
+8761 -1896

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@
<base href="{{BASE}}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>页面不存在 | 班级小助手文档</title>
<title>页面不存在 | ClassHelper文档</title>
<script>
// 与 docfx 主题选择同一套 key(theme 存 light / dark / auto),避免两边主题不一致
const theme = localStorage.getItem('theme') || 'auto';
+1 -1
View File
@@ -19,7 +19,7 @@ description: 老师点名让学生过来:普通与紧急两档、快捷短语
## 两档
| 级别 | 落库级别 | 学生端行为 |
| 级别 | 落库级别 | ClassHelper 班级端行为 |
| ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| **普通叫人** | `HIGH` | 课间先显示「叫人」胶囊,点击展开;**上课时段只进队列、不打断课堂**,下课后自动弹出详情(90 秒后收起) |
| **紧急叫人** | `URGENT` | **无视上课时段立即展开**,上课时被收起后仍保留胶囊、可再次点开 |
+6 -6
View File
@@ -1,11 +1,11 @@
---
title: 班级、学生与教师
description: 班级账号、学生名单、班主任与协作教师,以及"谁能看什么、谁能改什么"。
description: 班级账号、学生名单、班主任与各科任课老师,以及"谁能看什么、谁能改什么"。
---
# 班级、学生与教师
## 班级账号:学生端的身份
## 班级账号:ClassHelper 班级端的身份
学生**没有个人账号**。一台教室机器用一个**班级账号**登录:
@@ -36,7 +36,7 @@ description: 班级账号、学生名单、班主任与协作教师,以及"谁
| 密码 | **没有密码概念**。学生的 `passwordHash` 是空串占位,不能登录,也没有任何重置密码接口 |
| 所属班级 | 一个学生属于一个主班级 |
> 用学生用户名去登录会被拒。就算拿教师 / 管理员账号去试,服务端也会在**密码校验之前**先判角色 ——
> 学生**不是账号**(`Student` 表里没有密码、没有角色),拿学号去登录只会得到 401 ——
> 否则"密码错(401)"和"密码对但学生账号已停用(403)"的差异会把学生账号密码变成可离线验证的预言机。
## 教师
@@ -46,7 +46,7 @@ description: 班级账号、学生名单、班主任与协作教师,以及"谁
| 新建 / 编辑 / 删除 | 「教师管理」 | **仅管理员** |
| 导入名单(xlsx/csv) | 「教师管理 → 导入名单」 | **仅管理员** |
| 重置密码 | 「教师管理」(可指定新密码,留空用默认初始密码) | **仅管理员** |
| 加进某个班 | 班级详情 → 协作教师 | 管理员 / 教师 |
| 加进某个班 | 班级详情 → 任课老师(班级 + 科目 + 教师) | 管理员 |
| 换班主任 | 班级详情顶部「更换班主任」 | **仅管理员** |
> **教师录入只有管理员能做**:`/api/teachers` 整组接口都是 `requireRole('ADMIN')`,教师调用返回 403。
@@ -58,7 +58,7 @@ description: 班级账号、学生名单、班主任与协作教师,以及"谁
1. 他还是某个班的**班主任**;
2. 名下有**课程**;
3. 有**班级分配**(协作教师);
3. 有**任课关系**(`Course.teacherId === 他`);
4. **发布过作业或通知**。
(后两项曾经漏检,会静默删掉该教师发过的作业与通知。)
@@ -71,7 +71,7 @@ description: 班级账号、学生名单、班主任与协作教师,以及"谁
| ------------------ | ------------------------------ | -------------------------- |
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
| 科任老师 `SUBJECT` | `ClassTeacher` 里有当前用户 | 被分配的班级 |
| 科任老师 `SUBJECT` | 该班 `Course.teacherId` 有当前用户 | 被任课的班级 |
| 学生 `STUDENT` | `User.classId` / 班级账号 | 自己的班级(只读业务数据) |
### 权限矩阵(摘要)
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: 成绩
description: 成绩录入与导入、等级与得分率、学生端看到的全班总览。
description: 成绩录入与导入、等级与得分率、ClassHelper 班级端看到的全班总览。
---
# 成绩
@@ -31,7 +31,7 @@ Web 管理端「成绩录入」页:
## 学生看到的是什么
学生端(教室机器)用的是**班级账号**,所以它看到的不是"我的成绩",而是**全班成绩总览**:
ClassHelper 班级端(教室机器)用的是**班级账号**,所以它看到的不是"我的成绩",而是**全班成绩总览**:
- 表格:分数 / **得分率** / **等级**;
- 柱状图;
+4 -4
View File
@@ -15,7 +15,7 @@ description: 成绩、学生名单、教师名单的表格导入,以及从 Cla
1. **下载模板** —— `GET /imports/template?kind=grades&format=xlsx|csv`。
CSV 带 BOM,Excel 直接双击打开不乱码。
2. **上传预览** —— 服务端返回列名、前 20 行、总行数与**建议的字段映射**,以及校验问题。
列名按同义词自动映射(「学号→学生用户名」「得分→分数」「满分→总分」……),也可以手动改。
列名按同义词自动映射(「用户名→学号」「得分→分数」「满分→总分」「课程→科目」……),也可以手动改。
**缺必填列不会静默通过**,会直接给出「缺少必填列「…」」。
3. **确认导入** —— 按映射与写入模式提交:
@@ -34,7 +34,7 @@ description: 成绩、学生名单、教师名单的表格导入,以及从 Cla
## 1. 成绩表格导入
- 入口:成绩录入 → 「导入表格」
- 模板列:`学生用户名 / 学生姓名 / 考试名称 / 分数 / 总分 / 课程`
- 模板列:`学生学号 / 学生姓名 / 考试名称 / 分数 / 总分 / 科目 / 等级`
- 支持 `.xlsx / .xls / .csv / .tsv`(自动识别分隔符)
- **重复判定**:同班级 + 同学生 + 同考试 + 同课程
- 学生按「用户名」优先、「姓名」兜底匹配
@@ -42,14 +42,14 @@ description: 成绩、学生名单、教师名单的表格导入,以及从 Cla
## 2. 学生名单导入
- 入口:学生管理 → 「导入名单」(仅管理员)
- 模板列:`用户名 / 姓名` —— 学生没有账号属性,**没有密码列**
- 模板列:`学号 / 姓名 / 班级 / 性别 / 家长手机号` —— 学生**不是账号**,没有密码列
- 用户名必须匹配 `^[A-Za-z0-9_.-]{3,32}$`
- 用户名已经被某个教师 / 管理员账号占用时,**这一行进错误列表**,不会覆盖他人账号
## 3. 教师名单导入
- 入口:教师管理 → 「导入名单」(仅管理员)
- 模板列:`用户名 / 姓名 / 初始密码 / 角色`
- 模板列:`工号 / 姓名 / 手机号 / 初始密码 / 角色`
- 角色取 `ADMIN` / `TEACHER`,留空按 `TEACHER`
- 与新建教师同一套校验,导入完可以直接登录
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: 应用帮助
description: 班级小助手每一项功能怎么用:课表、作业、通知、叫人、成绩、班级、灵动岛、外观与导入。
description: ClassHelper每一项功能怎么用:课表、作业、通知、叫人、成绩、班级、灵动岛、外观与导入。
---
# 应用帮助
@@ -15,7 +15,7 @@ description: 班级小助手每一项功能怎么用:课表、作业、通知
| [通知与提醒](notifications.md) | 三档优先级、上课时段策略、二次确认、已读与红点 |
| [叫人](calls.md) | 普通叫人与紧急叫人的差别、快捷短语、谁能叫谁 |
| [成绩](grades.md) | 录入方式、导入、等级分布、学生看到的是什么 |
| [班级、学生与教师](classes.md) | 班级账号、名单、班主任与协作教师、谁看得见什么 |
| [班级、学生与教师](classes.md) | 班级账号、名单、班主任与各科任课老师、谁看得见什么 |
| [灵动岛](island.md) | 胶囊与卡片、多条通知的竖排列表、停靠与外观、触摸屏 |
| [外观、主题与动效](appearance.md) | 深浅色、设计令牌、动效语言、降低动态效果 |
| [表格与课表导入](imports.md) | 成绩 / 名单 / 教师表格导入与 ClassIsland 档案导入 |
+2 -2
View File
@@ -10,7 +10,7 @@ description: 通知的三档优先级、上课时段策略、紧急通知的二
## 三档优先级
| 优先级 | 学生端表现 |
| 优先级 | ClassHelper 班级端表现 |
| ------ | -------------------------------------------------------------------------------------------- |
| 普通 | 课间弹「新消息」胶囊,点击展开详情;**上课时段完全不显示**(窗口隐藏)并进队列,下课自动补弹 |
| 重要 | 同上,卡片上有优先级标签 |
@@ -22,7 +22,7 @@ description: 通知的三档优先级、上课时段策略、紧急通知的二
「上课时段」由课表实时判定(见[课表](schedules.md))。判定之后:
| 学生端行为 | 上课时段 | 课间 / 放学 |
| ClassHelper 班级端行为 | 上课时段 | 课间 / 放学 |
| ---------- | ---------------------------------------- | ----------- |
| 普通通知 | 收着不弹(窗口隐藏),进待发队列 | 直接弹 |
| 紧急通知 | **立刻弹** | 立刻弹 |
+5 -5
View File
@@ -1,6 +1,6 @@
---
title: REST API
description: 班级小助手后端全部接口一览:方法、路径、权限与说明。
description: ClassHelper后端全部接口一览:方法、路径、权限与说明。
---
# REST API
@@ -17,13 +17,13 @@ description: 班级小助手后端全部接口一览:方法、路径、权限
| 方法 | 路径 | 权限 | 说明 |
| --------------------------- | ------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
| POST | `/auth/login` | 公开 | 登录(教师 / 管理员);`role=STUDENT` 返回 403(学生只有名单,改用班级登录) |
| POST | `/auth/login` | 公开 | 登录(教师 / 管理员的**工号** + 密码);学生不是账号,拿学号登录只会得到 401 |
| POST | `/auth/class-login` | 公开 | **班级账号登录**:班级码 + 班级密码 → 班级会话 |
| GET | `/auth/me` | 登录 | 当前用户(学生附带班级 / 年级) |
| PATCH | `/auth/password` | 登录 | 修改自己的密码(班级会话 → 班级密码) |
| POST | `/auth/logout` | 登录 | 退出(无状态,客户端丢弃 token) |
| GET | `/classes` | 登录 | 班级列表(按权限收敛) |
| GET | `/classes/:id` | 班级可见 | 班级详情(学生 / 课程 / 协作教师) |
| GET | `/classes/:id` | 班级可见 | 班级详情(学生 / 课程 / 班主任 / 各科任课老师) |
| POST | `/classes` | 管理员 | 创建班级(自动生成班级码,可指定班主任) |
| PATCH / DELETE | `/classes/:id` | 管理员 / 教师 | 编辑 / 删除班级 |
| PATCH | `/classes/:id/class-account` | 管理员 | 设置 / 重置班级账号(班级码 + 班级密码) |
@@ -33,7 +33,7 @@ description: 班级小助手后端全部接口一览:方法、路径、权限
| PATCH | `/classes/:id/notification-channel` | 班级账号 / 教师 / 管理员 | 设置「通知显示到哪个端」 |
| GET / POST | `/classes/:id/students` | 班级可见 / ADMIN·TEACHER | 学生名单 / 添加学生 |
| DELETE | `/classes/:id/students/:userId` | ADMIN·TEACHER | 移出学生 |
| POST / DELETE | `/classes/:id/teachers[/:teacherId]` | 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 列结构,供客户端直接渲染) |
@@ -72,7 +72,7 @@ description: 班级小助手后端全部接口一览:方法、路径、权限
| GET / POST / PATCH / DELETE | `/integrations/devices[/:id]` | 管理员 / 教师 | ClassIsland 联动设备管理(令牌只存 sha256,令牌前缀用于人眼识别) |
| POST | `/integrations/devices/:id/token` | 管理员 / 教师 | 重置设备令牌(旧令牌立即失效,明文只返回一次) |
| POST | `/integrations/classisland/notify` | 管理员 / 教师 | 下发提醒到该班 ClassIsland 设备(广播 `classisland:notification`) |
| GET | `/database/status` | **管理员** | 数据库状态(连接 / 版本 / 体积 / 14 张表行数 / 备份列表 / 定时配置) |
| GET | `/database/status` | **管理员** | 数据库状态(连接 / 版本 / 体积 / 16 张表行数 / 备份列表 / 定时配置) |
| POST | `/database/test-connection` | **管理员** | 测试任意目标库连通性(provider 仅 sqlite / mysql) |
| GET / POST | `/database/backups` | **管理员** | 备份列表 / 立即备份(gzip JSON 快照,存 `<数据目录>/backups/`) |
| POST / DELETE | `/database/backups/:name/restore` · `/database/backups/:name` | **管理员** | 从备份恢复(整库覆盖)/ 删除备份 |
+73 -30
View File
@@ -1,6 +1,6 @@
---
title: 数据模型
description: 14 张表的职责、跨库通用约定、迁移历史与启动时自动迁移。
description: 16 张表的职责、跨库通用约定、迁移历史与启动时自动迁移。
---
# 数据模型
@@ -15,45 +15,83 @@ description: 14 张表的职责、跨库通用约定、迁移历史与启动时
- 因此切库只需改 `provider` 与连接串(`pnpm db:switch:mysql` 就地改写它,没有第二份 schema);
- 主键统一用 cuid 字符串,便于多端离线生成与合并。
## 14 张表
## 两条核心不变量
| 表 | 职责 | 值得注意的字段 |
| ------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------- |
| `User` | 教师 / 管理员 / 学生统一存储 | `role`、`classId`;学生 `passwordHash` 是空串占位,不能登录 |
| `Class` | 班级 | `code` + `passwordHash`(班级账号)、`termWeeks`、`notificationChannel` |
| `ClassTeacher` | 教师-班级 分配关系(协作教师) | `@@unique([classId, teacherId])` |
| `Enrollment` | 学生-班级 归属记录 | 与 `User.classId` 同步,为将来多班预留 |
| `Course` | 课程(班级维度) | `@@unique([classId, name])` |
| `Schedule` | 课表条目 | `weekStart` / `weekEnd` / `weekParity`(单双周)/ `source`(manual|classisland) |
| `Homework` | 作业 | `assignDate`(所属日期)、`attachmentUrl`;`dueAt` 是已下线的遗留列 |
| `HomeworkStatus` | 学生作业完成状态 | `@@unique([homeworkId, userId])` |
| `Notification` | 通知 | `priority` |
| `NotificationRead` | 已读记录 | `@@unique([notificationId, userId])` |
| `TimeLayout` | 节次时间配置(ClassIsland 同款 JSON 导入得到) | `items` 是 JSON 字符串:`[{ index, name, startTime, endTime, type, skipped }]` |
| `Grade` | 成绩 | `score` / `totalScore` / `examName` |
| `IntegrationDevice` | ClassIsland 联动设备 | `tokenHash`(只存 sha256)、`tokenHint`、两个开关、最后一次上报的快照字段 |
| `ClassIslandPush` | 下发给 ClassIsland 的提醒 | `kind`(notification|call)、`ackedAt`、`expiresAt` |
这两条是 2026-10-06 那次重构立下的,后面所有表都围着它们转:
### 三个容易忽略的设计
**① 学生不是账号。** `Student` 是独立的一张表,只有学号 / 姓名 / 班级 / 性别 / 家长手机号 /
状态,**没有 `passwordHash`、没有 `role`、没有登录字段** —— 「学生不能登录」由表结构保证,
不再靠"登录前先判 role === 'STUDENT' 并 403"这类代码分支维持。
`User` 因此收缩为**纯账号表**(管理员 / 教师 / ClassHelper 班级端)。
**② 任课关系的唯一来源是 `Course`。** `Course(classId, name, teacherId)` 就是
「班级 + 科目 + 教师」这条关系本身,作业与成绩都挂在它上面;班主任另由 `Class.teacherId` 表示
(每班 1 人)。历史上那张**没有科目**的 `ClassTeacher` 协作表已删除 —— 两套关系并存必然漂移。
同一位教师可以在同一个班同时是班主任和某科科任老师:两处指向同一个 `User`,不重复建号,
权限在 `@classhelper/shared` 的 `resolveClassRole()` 里合并。
## 16 张表
| 表 | 职责 | 值得注意的字段 |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `User` | **纯账号表**:管理员 / 教师 / ClassHelper 班级端 | `username`(教师即**工号**,班级端即班级码)、`role`、`phone`;**学生不在这里** |
| `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` |
| `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` |
### 几个容易忽略的设计
**`ClassIslandPush` 与 `IntegrationDevice`** —— 提醒是**落库**的,不是"发出去就算"。
设备离线时通知不丢:插件重连后拉取未确认的那条立即弹出,弹完回执(`ackedAt`)。
`expiresAt` 防止学生几天后突然弹出旧提醒。
**`Schedule.source`** —— 决定插件全量上报时能清理哪些行:只清 `classisland` 来源且本次未再上报的行。
**在线判定看 `lastHeartbeatAt`,不看 `lastSeenAt`** —— 同一台设备的上报其实有两种:
心跳(每 60 秒一次,只表明"我还在线")与带状态的上报(课表 / 上课状态)。
合成一个字段后无法区分"在线但一直没新状态"和"整个失联",所以拆开:
`lastHeartbeatAt` 每次上报都刷(60 秒窗口判在线),`lastSeenAt` 只在带运行状态时刷。
**课表同步方向:服务端 → 教室是主方向。**
`IntegrationDevice.mirrorScheduleToClassIsland` 默认 **true**(服务端自动下发),
`syncScheduleToServer` 默认 **false**(教室不再自动回传)。要取教室的课表,走
`POST /integrations/devices/:id/request-schedule` 这个**人工动作**(前端点击前会弹警告)——
自动回传会在老师手排课之后被教室的旧课表悄悄覆盖。
**`Schedule.source`** —— 决定插件上报时能清理哪些行:只清 `classisland` 来源且本次未再上报的行。
**`source` 只在 create 时确定,update 一律保持原值**(否则手排的课会被"收养"后又被清掉)。
**`Homework.dueAt`** —— 截止时间功能已下线,UI 与接口都不再读写它。
保留列是为了**不对已安装的库做破坏性迁移**;新库里它恒为 `null`。
## 迁移历史(10 个)
**`TermWeek.classId` 用空串而不是 null 表示"全校默认"** —— SQLite 与 MySQL 的唯一约束都把
NULL 视作互不相同,`@@unique([classId, weekNumber])` 用 null 就形同虚设。
## 迁移历史(16 个)
```
init → add_time_layout → add_class_account → add_schedule_week_parity →
add_class_term_weeks → add_classisland_integration → add_class_notification_channel →
add_homework_assign_date → add_push_kind → add_schedule_source
add_homework_assign_date → add_push_kind → add_schedule_source →
add_student_table → add_student_grade_query → add_grade_level →
add_classhelper_heartbeat → add_archives → add_term_weeks
```
`add_student_table` 是一次真正的表重构(不是加列):新建 `Student` / `StudentClassTransfer`、
把 `User` 里 `role='STUDENT'` 的行**按 id 原样搬过去**、把 `Grade` / `HomeworkStatus` /
`NotificationRead` 的外键从 `userId` 换到 `studentId`、删掉 `Enrollment` 与 `ClassTeacher`。
因为学生行的 id 保持不变,三张个人数据表的 `userId` 可以直接当 `studentId` 用,历史一条不丢。
### 启动时自动迁移
`src/lib/db-bootstrap.ts` 用一张账本表 `_ch_migrations` 记录已执行的迁移目录名:
@@ -69,19 +107,24 @@ add_homework_assign_date → add_push_kind → add_schedule_source
## 备份格式就是这份模型的拓扑序导出
「数据库管理」里的备份 / 导出导入 / 跨库迁移共用一种 JSON 快照:14 张表按拓扑序导出,
恢复时**临时关闭外键检查** —— 因为 `User.classId → Class` 与 `Class.teacherId → User`
**互相引用**,任何排序都会被其中一侧卡死。见[数据库管理](../management/database.md)。
「数据库管理」里的备份 / 导出导入 / 跨库迁移共用一种 JSON 快照:16 张表按拓扑序导出
(见 `lib/snapshot.ts` 的 `SNAPSHOT_TABLES`),恢复时**临时关闭外键检查** ——
因为 `Class.teacherId → User` 与 `Student.classId → Class` 是一条链、
`Class/Student.archivedYearId → ArchivedYear` 又是一条,任何排序都会被其中一侧卡死。
见[数据库管理](../management/database.md)。
## 开发时的模型改动
1. 改 `schema.prisma`;
2. 在 `prisma/migrations/` 下加一个迁移目录(本机用
`pnpm --filter @classhelper/server db:deploy` 应用,**别用 `pnpm db:migrate`** ——
它在 Windows 上不可靠);
3. 改 schema 时先看**哪些外键指向 `User` 且是 Cascade**,再回来核对
`deleteTeacher` 的护栏清单(它必须覆盖 `Class.teacherId` / `Course.teacherId` /
`ClassTeacher.teacherId` / `Homework.createdBy` / `Notification.createdBy` 五项)。
它在 Windows 上不可靠)。要改已有表结构而不只是加列时,可以先用
`pnpm exec prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --script`
生成骨架,再手工补数据搬运语句;
3. 改完核对 **`SNAPSHOT_TABLES`**(新增 / 删除表要同步)与 `deleteTeacher` 的护栏清单
(它必须覆盖 `Class.teacherId` / `Course.teacherId` / `Homework.createdBy` /
`Notification.createdBy` 四项;`ClassIslandPush.createdBy` 是裸列、无外键、不级联,不用加);
再回到 `packages/server/scripts/apply-column-migrations.cjs` 补上可 `ALTER TABLE` 的简单列。
模型总数与迁移个数会随版本增长,以 `schema.prisma` 与 `prisma/migrations/` 的实际内容为准;
本文里的数字标注的是当前版本(1.1.2)的状态。
本文里的数字标注的是当前版本(1.2.0)的状态。
+4 -4
View File
@@ -1,17 +1,17 @@
---
title: 开发文档
description: 班级小助手的架构、接口、数据模型、权限、动效层,以及验收与发版流程。
description: ClassHelper的架构、接口、数据模型、权限、动效层,以及验收与发版流程。
---
# 开发文档
班级小助手是一个 **pnpm monorepo**,一份代码产出四个交付物:
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(学生端 / 教室机器,含灵动岛浮窗) |
| 桌面客户端 | `packages/desktop-client` | Electron + Vue 3(ClassHelper 班级端 / 教室机器,含灵动岛浮窗) |
| 共享契约 | `packages/shared` | 三端共用的类型 / 常量 / 权限 / 工具函数 |
| ClassIsland 联动插件 | `packages/classisland-plugin` | .NET 8 / C#(**独立于 pnpm workspace**,不参与 `pnpm install`) |
@@ -38,7 +38,7 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
| [架构总览](architecture.md) | 目录结构、技术栈、模块化设计、几条贯穿全局的约定 |
| [REST API](api.md) | 全部接口一览(方法 / 路径 / 权限) |
| [实时事件(Socket.IO)](realtime.md) | 事件清单、房间规则、新增事件要改哪三处 |
| [数据模型](data-model.md) | 14 张表、迁移历史、跨库约定 |
| [数据模型](data-model.md) | 16 张表、迁移历史、跨库约定 |
| [角色与权限](permissions.md) | 权限矩阵与实现位置 |
| [动效层与主题](motion.md) | beUI 令牌、CSS `linear()` 弹簧、深色主题 |
| [ClassIsland 插件](classisland-plugin.md) | 插件契约、镜像为什么要带 `startTime`、部署与调试 |
+59 -33
View File
@@ -1,6 +1,6 @@
---
title: 角色与权限
description: 权限矩阵、实现位置,以及"前端隐藏 ≠ 权限"这条纪律。
description: 权限矩阵、科目维度的收口规则,以及"前端隐藏 ≠ 权限"这条纪律。
---
# 角色与权限
@@ -17,33 +17,55 @@ description: 权限矩阵、实现位置,以及"前端隐藏 ≠ 权限"这条
## 角色判定
角色只有三种,**没有 `STUDENT`** —— 学生不是账号(见[数据模型](data-model.md))。
班级内的角色由「账号角色 + 与该班级的关系」共同决定:
| 角色 | 判定方式 | 可见范围 |
| ------------------ | ------------------------------ | -------------------------- |
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
| 科任老师 `SUBJECT` | `ClassTeacher` 中存在当前用户 | 被分配的班级 |
| 学生 `STUDENT` | `User.classId` / 班级账号 | 自己的班级(只读业务数据) |
| `NONE` | 与这个班没有任何关系 | 什么都看不到 |
| 角色 | 判定方式 | 可见范围 |
| --------------------------- | ------------------------------------------ | ------------------------------------ |
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
| 科任老师 `SUBJECT` | 该班存在 `Course.teacherId === 当前用户` | 被任课的班级 |
| `CLASS_DEVICE` 班级端 | 班级码 + 班级密码登录(`classSession`) | 本班(代全班读写个人数据) |
| `NONE` | 与这个班没有任何关系 | 什么都看不到 |
判定函数是 `resolveClassRole()`。
判定函数是 `resolveClassRole()`。「科任」的判定源是 **`Course`**(班级 + 科目 + 教师),
不是那张已经没有的 `ClassTeacher` 协作表。
## 权限矩阵
| 操作 | 管理员 | 班主任 | 科任老师 |
| -------------------------------------------------- | :----: | :----------: | :------: |
| 班级创建 / 修改 / 删除 | ✅ | ❌ | ❌ |
| 分配班主任与科任老师 | ✅ | ❌ | ❌ |
| 学生名单管理(增删 / 导入) | ✅ | ❌ | ❌ |
| 教师录入(新建 / 导入名单 / 修改密码) | ✅ | ❌ | ❌ |
| 数据库管理(备份 / 导入导出 / 一键切换) | ✅ | ❌ | ❌ |
| 课表管理(增删改 / 时间配置导入) | ✅ | ✅(仅本班) | ❌ |
| 布置作业 | ✅ | ✅ | ✅ |
| 叫人 | ✅ | ✅ | ✅ |
| 发布通知 | ✅ | ✅ | ✅ |
| 成绩录入 / 修改 / 导入(仅本班) | ✅ | ✅(仅本班) | ❌ |
| 班级密码维护(`PATCH /classes/:id/class-account`) | ✅ | ❌ | ❌ |
| 操作 | 管理员 | 班主任 | 科任老师 | 班级端 |
| -------------------------------------------------------- | :----: | :----------: | :------: | :----: |
| 班级创建 / 修改 / 删除 | ✅ | ❌ | ❌ | ❌ |
| 分配班主任与各科任课老师 | ✅ | ❌ | ❌ | ❌ |
| 学生名单管理(增删 / 导入 / 调班 / 转出) | ✅ | ✅(仅本班) | ❌ | ❌ |
| 教师录入(新建 / 导入名单 / 修改密码) | ✅ | ❌ | ❌ | ❌ |
| 毕业归档 / 学年升级 / 学期周次 | ✅ | ❌ | ❌ | ❌ |
| 数据库管理(备份 / 导入导出 / 一键切换) | ✅ | ❌ | ❌ | ❌ |
| 课表管理(增删改 / 时间配置导入) | ✅ | ✅(仅本班) | ❌ | ❌ |
| 布置作业(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ✅ |
| 叫人 / 发布通知 | ✅ | ✅ | ✅ | ✅ |
| 成绩录入 / 修改 / 导入(**自己任教的科目**) | ✅ | ✅(本班) | ✅ | ❌ |
| 查看本班成绩汇总 | ✅ | ✅ | ✅ | ✅ |
| 按学号查询学生成绩明细 | ✅ | ✅ | ✅ | 需班级开关 |
| 查看 ClassHelper 班级端在线状态 | ✅ | ✅ | ✅ | ❌ |
| 班级密码 / 班级码维护(`PATCH /classes/:id/class-account`) | ✅ | ❌ | ❌ | ❌ |
## 科目维度的收口(本次重构的核心规则)
作业与成绩不是"凡是能进这个班就能改",而是**再按科目收一道**:
| 情形 | 谁可以写 |
| --------------------------------------- | ------------------------------------------ |
| 管理员 | 全部 |
| 挂了科目(`courseId` 有值) | **该科的任课老师**(`Course.teacherId`) |
| 没挂科目(`courseId` 为空) | 本班**班主任** |
| ClassHelper 班级端 | 只有**作业**可以(老师在教室随手录一条);**成绩不给** |
于是:班主任若不教这一科,就改不了这一科的作业与成绩 —— 这正是需求
「班主任不自动拥有所有科目作业成绩编辑权,只有同时是该科科任老师时才有该科权限」。
实现在 `lib/access.ts` 的 **`assertCanManageSubjectContent(user, classId, courseId, action, { allowClassDevice })`**,
作业 / 成绩 / 叫人三处写入统一走它。
## 实现要点
@@ -52,23 +74,28 @@ description: 权限矩阵、实现位置,以及"前端隐藏 ≠ 权限"这条
**班级维度的判断在服务层**:`src/lib/access.ts` 提供
```
assertCanManageClasses / assertCanAssignTeachers / assertCanManageRoster
assertCanManageSchedule / assertCanPublishContent / assertCanManageGrades
assertCanManageClasses / assertCanAssignTeachers / assertCanManageRoster
assertCanManageSchedule / assertCanPublishNotification
assertCanManageGrades / assertCanManageSubjectContent
```
**列表数据用 `resolveClassScope()` 收敛**(`classId in [...]`),单条接口直接断言;
越权一律 403。
**鉴权中间件会回查数据库**(`middleware/auth.ts`):JWT 校验之后重新读一次用户与班级关系,
所以改了班级 / 角色会**立即生效**,不用等令牌过期。班级会话则回查 `Class` 表。
**鉴权中间件会回查数据库**(`middleware/auth.ts`):JWT 校验之后重新读一次账号,
所以改了角色 / 班级关系会**立即生效**,不用等令牌过期。班级端会话则回查 `Class` 表。
**班级端的成绩查询开关**:`Class.studentGradeQueryEnabled`(默认开启)。
关闭后班级端调 `GET /grades/student/:studentNo` 一律 403,教师端不受影响 ——
这就是需求「是否允许学生通过班级端查看本人明细,由教师或管理员开关控制」。
## 前端隐藏 ≠ 权限
前端的隐藏只是"别让人看见入口":
- 教师登录时侧边栏不显示「班级管理 / 学生管理 / 教师管理 / 数据库管理」;
- 教师登录时侧边栏不显示「班级管理 / 学生管理 / 教师管理 / 毕业归档 / 学期周次 / 数据库管理」;
- 课表页的增删改按钮只对「管理员或本班班主任」显示;
- 成绩页的录入 / 删除按钮同理。
- 成绩页的录入 / 删除按钮对教师显示(科任也能用,**具体到某一条由后端按科目判定**)。
但这些页面**直接输网址也进不去**(路由 `meta.roles` 会把非管理员挡回仪表盘),
而且**所有写入请求服务端都会再断言一次**。绕过菜单、绕过前端,一样 403。
@@ -76,10 +103,9 @@ assertCanManageSchedule / assertCanPublishContent / assertCanManageGrades
> 新增视图时别忘了两处:路由的 `meta.roles` + 服务层对应 service 里的 `assertCanXxx`。
> 只做其中一个都等于没做。
## 学生账号的两个反向约束
## 学生没有账号的两个反向约束
1. **学生不能登录**。`/auth/login` 遇到 `role=STUDENT` 直接 403;
且**角色判定在密码校验之前** —— 否则"密码错(401) / 密码对但学生账号已停用(403)"的差异,
会把学生账号密码变成可离线验证的预言机。
1. **学生不能登录,而且根本不存在可以登录的学生账号**。`User` 表里没有学生行,
拿学号当账号登录只会得到 401("工号或密码错误"),连"这个学号存在吗"都探测不出来。
2. **学生没有任何密码接口**。`POST /students/:id/reset-password` 已删除(调用返回 404),
新建学生也不设密码(`passwordHash` 是空串占位)。
新建学生也不设密码 —— `Student` 表里压根没有那一列。
+1 -1
View File
@@ -44,7 +44,7 @@ io(url, { auth: { token } }); // 握手阶段用 JWT 鉴权
| `user:{userId}` | 某个用户自己的连接 |
| `teacher:{teacherId}` | 某个教师 |
| `role:{role}` | 某一角色(`ADMIN` / `TEACHER` …) |
| `students` | 全部学生端连接 |
| `students` | 全部 ClassHelper 班级端连接 |
| `teachers` | 全部教师端连接 |
服务端通过 `realtime/bus.ts` 的 `emitToClass` / `emitToUser` 发事件 ——
+1 -1
View File
@@ -575,7 +575,7 @@ VISUALIZER_TITLE_OFFSET = 4.0 // 视觉条中心 y = 曲名基线 - 4
---
## 附:班级小助手实际采纳了哪些(本次重写对照表)
## 附:ClassHelper实际采纳了哪些(本次重写对照表)
| WinIsland 做法 | 出处 | 我们的落地 | 验证 |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
+3 -3
View File
@@ -16,8 +16,8 @@
"output": "_site",
"template": ["default", "modern", "templates/classhelper"],
"globalMetadata": {
"_appName": "班级小助手",
"_appTitle": "班级小助手文档",
"_appName": "ClassHelper",
"_appTitle": "ClassHelper文档",
"_appLogoPath": "logo.png",
"_appLogoUrl": "",
"_appFaviconPath": "favicon.png",
@@ -27,7 +27,7 @@
"repo": "https://github.com/LoveChengke/classhelper",
"branch": "master"
},
"_appFooter": "<div class=\"ch-footer\"><span>班级小助手(Class Helper)· 文档</span><span class=\"ch-footer-sep\">·</span><a href=\"https://github.com/LoveChengke/classhelper\">GitHub 仓库</a><span class=\"ch-footer-sep\">·</span><a href=\"https://github.com/LoveChengke/classhelper/releases\">下载与更新日志</a><span class=\"ch-footer-sep\">·</span><span>构建于 docfx</span></div>"
"_appFooter": "<div class=\"ch-footer\"><span>ClassHelper(Class Helper)· 文档</span><span class=\"ch-footer-sep\">·</span><a href=\"https://github.com/LoveChengke/classhelper\">GitHub 仓库</a><span class=\"ch-footer-sep\">·</span><a href=\"https://github.com/LoveChengke/classhelper/releases\">下载与更新日志</a><span class=\"ch-footer-sep\">·</span><span>构建于 docfx</span></div>"
},
"sitemap": {
"baseUrl": "https://lovechengke.github.io/ClassHelper/docs/",
+2 -2
View File
@@ -32,7 +32,7 @@ icacls <产物目录> /setintegritylevel Medium /T /C
### 安装包装不上,说应用正在运行
同上。托盘 → 退出班级小助手。
同上。托盘 → 退出 ClassHelper。
## 连接与登录
@@ -104,7 +104,7 @@ icacls <产物目录> /setintegritylevel Medium /T /C
### 学生登录不了
**学生没有个人账号。** 学生端统一用**班级码 + 班级密码**登录(见[装学生端](get-started/client.md))。
**学生没有个人账号。** ClassHelper 班级端统一用**班级码 + 班级密码**登录(见[装 ClassHelper 班级端](get-started/client.md))。
用学生用户名去登录一定会失败,这是设计。
### 客户端里找不到"修改密码"
+8 -8
View File
@@ -1,20 +1,20 @@
---
title: 接入 ClassIsland
description: 把教室里的 ClassIsland 接上班级小助手:下课表上报、大屏提醒下发、课表镜像三个方向。
description: 把教室里的 ClassIsland 接上 ClassHelper:下课表上报、大屏提醒下发、课表镜像三个方向。
---
# 接入 ClassIsland
课堂里真正"知道现在第几节、在上什么课"的是 [ClassIsland](https://classisland.tech)。
它跟班级小助手管的事情不重叠,接起来之后两边各拿各的好处:
它跟 ClassHelper管的事情不重叠,接起来之后两边各拿各的好处:
| 方向 | 做什么 | 老师在哪看 |
| ------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------- |
| ClassIsland → 班级小助手 | 上报**课表 + 节次时间 + 上课状态**(现在上什么 / 下一节 / 本节起止),写进本班课表 | Web 端「ClassIsland 联动」的实时状态卡片 |
| 班级小助手 → ClassIsland | 老师发一条**提醒** → 落库 → 教室插件取回 → ClassIsland 上**全屏弹出**(可语音朗读) | Web 端「ClassIsland 联动 → 下发提醒」 |
| ClassIsland → ClassHelper | 上报**课表 + 节次时间 + 上课状态**(现在上什么 / 下一节 / 本节起止),写进本班课表 | Web 端「ClassIsland 联动」的实时状态卡片 |
| ClassHelper → ClassIsland | 老师发一条**提醒** → 落库 → 教室插件取回 → ClassIsland 上**全屏弹出**(可语音朗读) | Web 端「ClassIsland 联动 → 下发提醒」 |
| (可选)班级 → ClassIsland 镜像 | 把小助手这边排好的课表**镜像**回 ClassIsland,新建一份档案,不动老师原有的课表 | 同一页的「开镜像」开关 |
> 没接 ClassIsland 也能用班级小助手,只是教室里少一个大屏播报的出口。**这一步是可选的。**
> 没接 ClassIsland 也能用 ClassHelper,只是教室里少一个大屏播报的出口。**这一步是可选的。**
## 前置:ClassIsland 装好了
@@ -46,7 +46,7 @@ description: 把教室里的 ClassIsland 接上班级小助手:下课表上报
### 填令牌
ClassIsland 的**设置 → 班级小助手联动**里填:
ClassIsland 的**设置 → ClassHelper 联动**里填:
| 设置项 | 说明 |
| ---------------------------- | --------------------------------------------------------------- |
@@ -85,7 +85,7 @@ ClassIsland 的**设置 → 班级小助手联动**里填:
| 要同步的内容 | 需要打开 |
| -------------------------------- | ------------------------------------------------------------------------------- |
| 服务端 → ClassIsland(镜像课表) | ① 插件设置「把班级课表镜像回 ClassIsland」+ ② Web 端该设备的「镜像课表」 |
| ClassIsland → 服务端(上报课表) | 插件设置「上报课表到班级小助手」 |
| ClassIsland → 服务端(上报课表) | 插件设置「上报课表到 ClassHelper」 |
| 提醒下发 | ① 教室客户端的「通知显示位置」不是"只在 ClassHelper" + ② 插件「接收小助手提醒」 |
## 上课时段:两边同一套规则
@@ -110,7 +110,7 @@ ClassIsland 侧的暂存由插件实现:它订阅课程服务,下课后把
## 镜像:把小助手的课表写回 ClassIsland
开启后,插件会新建一份名为 `班级小助手-<班级名>` 的档案课表,**不动老师原有的课表**。
开启后,插件会新建一份名为 `ClassHelper-<班级名>` 的档案课表,**不动老师原有的课表**。
镜像时会连节次时间一起带过去,并且**每两节之间补一段课间、最后一节之后不补**(放学)。
这一步不做的话,ClassIsland 的课表会整体错位 —— 表现为"看着有内容、每节课都对不上"。
+5 -5
View File
@@ -1,9 +1,9 @@
---
title: 装学生端
description: 在教室机器上安装班级小助手桌面客户端,用班级码登录,并第一次跑通实时通知。
title: 装 ClassHelper 班级端
description: 在教室机器上安装 ClassHelper桌面客户端,用班级码登录,并第一次跑通实时通知。
---
# 装学生端
# 装 ClassHelper 班级端
桌面客户端装在**教室的那台机器**上(教室大屏、讲台电脑)。它是学生看课表作业的地方,
也是「消息到达的第一现场」—— 灵动岛就长在这一端。
@@ -14,8 +14,8 @@ description: 在教室机器上安装班级小助手桌面客户端,用班级
| 产物 | 用途 |
| ------------------------------------ | ------------------------------------------ |
| `班级小助手-<版本>-x64-setup.exe` | 安装包(约 98 MB),装到本机并建快捷方式 |
| `班级小助手-<版本>-x64-portable.exe` | 单文件便携版,双击即用,适合不改动机房机器 |
| `ClassHelper-<版本>-x64-setup.exe` | 安装包(约 98 MB),装到本机并建快捷方式 |
| `ClassHelper-<版本>-x64-portable.exe` | 单文件便携版,双击即用,适合不改动机房机器 |
要自己打:
+6 -6
View File
@@ -1,6 +1,6 @@
---
title: 发布第一条内容
description: 发通知、布置作业、叫人、录成绩——把班级小助手真正用起来的第一轮操作。
description: 发通知、布置作业、叫人、录成绩——把 ClassHelper真正用起来的第一轮操作。
---
# 发布第一条内容
@@ -11,7 +11,7 @@ description: 发通知、布置作业、叫人、录成绩——把班级小助
**通知发布** → 选班级 → 填标题和内容 → 选优先级 → 发布。
| 优先级 | 学生端表现 |
| 优先级 | ClassHelper 班级端表现 |
| ------ | -------------------------------------------------------------------------- |
| 普通 | 课间弹胶囊,点击展开;**上课时段只进队列**,下课自动补弹 |
| 重要 | 同上,卡片上带优先级标签 |
@@ -32,14 +32,14 @@ description: 发通知、布置作业、叫人、录成绩——把班级小助
附件支持站内相对路径或 `http(s)://` 链接(其他 scheme 服务端会拒,避免被当成脚本执行)。
学生端那边默认是**看板模式**:按科目分卡片、每条作业完整显示要求、整个看板缩放到刚好铺满屏幕;
ClassHelper 班级端那边默认是**看板模式**:按科目分卡片、每条作业完整显示要求、整个看板缩放到刚好铺满屏幕;
老师也可以在教室机器上直接**录入作业**、勾选**未交名单**(教室机器用的是班级账号)。
## 叫一位同学
**通知发布 → 叫人**(或管理员在**学生管理 → 叫人**)→ 选学生 → 点一个快捷短语或自己写一句 → 发送。
| 级别 | 学生端 |
| 级别 | ClassHelper 班级端 |
| -------- | ------------------------------------------------------------------ |
| 普通叫人 | 课间显示「叫人」胶囊,点击展开;**上课时段只进队列**,下课自动弹出 |
| 紧急叫人 | **无视上课时段立即展开**,上课时被收起后仍保留胶囊、可再次点开 |
@@ -53,11 +53,11 @@ description: 发通知、布置作业、叫人、录成绩——把班级小助
**成绩录入** → 选班级与考试名称 → 单条录入 / 批量录入 / 导入表格。
- **科目**下拉同样是全校统一目录,选一个该班还没有的科目会自动建课。
- 导入表格支持 `.xlsx / .xls / .csv / .tsv`,列名按同义词自动映射(「学号→学生用户名」「得分→分数」),
- 导入表格支持 `.xlsx / .xls / .csv / .tsv`,列名按同义词自动映射(「用户名→学号」「得分→分数」),
也可以手动改。**文件上限 8MB**,超了会返回明确的 413 而不是一句英文的内部错误。
- 导入结果会给逐行的错误与警告,`errors[].row` 是 **Excel 视角的行号**(含表头),改完可以再导。
学生端看到的是**全班总览**:表格(分数 / 得分率 / 等级)+ 柱状图 + 等级分布 + 最近更新。
ClassHelper 班级端看到的是**全班总览**:表格(分数 / 得分率 / 等级)+ 柱状图 + 等级分布 + 最近更新。
## 在教室大屏上播一条提醒(需要 ClassIsland)
+3 -3
View File
@@ -1,11 +1,11 @@
---
title: 快速上手
description: 从零把班级小助手跑起来:装服务端、认管理端、建班级排课、装学生端、接 ClassIsland、发第一条内容。
description: 从零把 ClassHelper跑起来:装服务端、认管理端、建班级排课、装 ClassHelper 班级端、接 ClassIsland、发第一条内容。
---
# 快速上手
班级小助手不是一个单机软件,它由四部分组成,**必须先有服务端,其他三端才有内容可看**:
ClassHelper不是一个单机软件,它由四部分组成,**必须先有服务端,其他三端才有内容可看**:
| 组成部分 | 装在哪 | 谁用 | 必须先装? |
| ------------------------ | --------------------------------------- | -------------------------- | ---------------- |
@@ -20,7 +20,7 @@ description: 从零把班级小助手跑起来:装服务端、认管理端、
2. [安装服务端](server.md) —— Windows 安装包 / Linux 一键脚本 / Docker / 源码跑
3. [认识 Web 管理端](web-admin.md) —— 登录进去,认一认界面
4. [建班级、排课表](setup-class.md) —— 这是后面所有功能的前提
5. [装学生端](client.md) —— 教室机器用班级码登录
5. [装 ClassHelper 班级端](client.md) —— 教室机器用班级码登录
6. [接入 ClassIsland](classisland.md) —— 让教室大屏也会弹提醒(可选)
7. [发布第一条内容](first-content.md) —— 通知 / 作业 / 叫人 / 成绩
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: 系统要求
description: 班级小助手服务端、Web 管理端、桌面客户端的运行环境与网络要求。
description: ClassHelper 服务端、Web 管理端、桌面客户端的运行环境与网络要求。
---
# 系统要求
@@ -38,7 +38,7 @@ ldd --version | head -1
| 屏幕 | 桌面 1280×720 以上;手机(≤768px)有一套独立的抽屉式布局,同样可用 |
| 安装 PWA | 手机 / 桌面 Chrome 里「添加到主屏幕」可当 App 用(需要 HTTPS 或 localhost) |
## 桌面客户端(学生端 / 教室机器)
## 桌面客户端(ClassHelper 班级端 / 教室机器)
| 项目 | 要求 |
| -------- | ---------------------------------------------------------------------------- |
+4 -4
View File
@@ -1,6 +1,6 @@
---
title: 安装服务端
description: 用 Windows 安装包、Linux 一键脚本、Docker 或源码把班级小助手服务端跑起来。
description: 用 Windows 安装包、Linux 一键脚本、Docker 或源码把 ClassHelper 服务端跑起来。
---
# 安装服务端
@@ -21,7 +21,7 @@ description: 用 Windows 安装包、Linux 一键脚本、Docker 或源码把班
### 生成安装包
安装包不入库(体积大),从 [Releases](https://github.com/LoveChengke/classhelper/releases) 直接下载
`班级小助手服务端-<版本>-x64-setup.exe` 即可。要自己打:
`ClassHelper 服务端-<版本>-x64-setup.exe` 即可。要自己打:
```bash
pnpm dist:server # 首次会执行一次 npm install(约 2 分钟)
@@ -32,7 +32,7 @@ pnpm dist:server -- --reuse-deps # 迭代打包:复用已装依赖,跳过
| 路径 | 说明 |
| --------------------------------------------------------------------- | ------------------------------------------- |
| `releases/server/<版本>/安装包/班级小助手服务端-<版本>-x64-setup.exe` | 安装程序(约 62 MB,NSIS) |
| `releases/server/<版本>/安装包/ClassHelper 服务端-<版本>-x64-setup.exe` | 安装程序(约 62 MB,NSIS) |
| `releases/server/<版本>/免安装/` | 免安装目录,拷到任意 Windows x64 机器即可跑 |
包内自带 Node 运行时、后端产物、Web 管理端产物、生产依赖、迁移 SQL、随机 `JWT_SECRET` 的 `.env`
@@ -43,7 +43,7 @@ pnpm dist:server -- --reuse-deps # 迭代打包:复用已装依赖,跳过
双击安装程序即可,**不需要管理员权限**(当前用户级安装)。
- 安装目录:`%LOCALAPPDATA%\Programs\ClassHelperServer`
- 静默安装:`"班级小助手服务端-<版本>-x64-setup.exe" /S`
- 静默安装:`"ClassHelper 服务端-<版本>-x64-setup.exe" /S`
- 开始菜单:启动服务 / 停止服务 / 重启服务 / 打开管理端 / 使用说明 / 卸载
- 自动加入当前用户开机自启
+5 -5
View File
@@ -20,7 +20,7 @@ Web 管理端 → **班级管理** → 「新建班级」:
| 班级码 | 保存时自动生成(字母数字 4–16 位,存大写),可以改成好记的,例如 `G203` |
| 班级密码 | 新建班级时的初始密码就是它 —— 学生在教室机器上要用这个登录 |
> **班级码 + 班级密码 = 学生端的登录凭据。** 学生端没有个人账号,一台教室机器用班级账号登录后,
> **班级码 + 班级密码 = ClassHelper 班级端的登录凭据。** ClassHelper 班级端没有个人账号,一台教室机器用班级账号登录后,
> 那台机器上的「已读 / 完成」是**代全班**操作的。
>
> 学期周数只有管理员能改:它决定"现在是第几教学周",乱改会让课表的周次过滤和上课时段判定错位。
@@ -30,7 +30,7 @@ Web 管理端 → **班级管理** → 「新建班级」:
**学生管理** → 「新建」或「导入名单」:
- **单个新建**:填姓名即可,登录名会自动生成。
- **导入名单**:下载模板(列只有 `用户名 / 姓名`,学生没有账号属性,**没有密码列**),填好上传。
- **导入名单**:下载模板(列是 `学号 / 姓名 / 班级 / 性别 / 家长手机号`,学生**不是账号**,没有密码列),填好上传。
用户名要匹配 `^[A-Za-z0-9_.-]{3,32}$`;重复用户名在 `append` 模式下跳过、`upsert` 模式下更新姓名。
用户名已经属于某个教师 / 管理员账号时,这一行会进错误列表,不会覆盖别人的账号。
@@ -39,7 +39,7 @@ Web 管理端 → **班级管理** → 「新建班级」:
## 3. 加科任老师
教师的**录入**只有管理员能做(**教师管理**页,支持单个录入与导入名单)。班级层面的**分配**在班级详情里:
把老师加进「协作教师」,他就能看到这个班、给这个班发作业 / 通知 / 叫人。
在「班级详情 → 任课老师」里给某位老师配一门课,他就能看到这个班,并**只能改自己任教科目**的作业与成绩。
角色与可见范围见[班级、学生与教师](../app/classes.md)与[角色与权限](../dev/permissions.md)。
@@ -62,10 +62,10 @@ Web 管理端 → **班级管理** → 「新建班级」:
## 5. 确认「上课时段」判定是对的
班级小助手有一整套「上课时段策略」:普通通知在上课时段只进队列、下课自动补弹,紧急通知与叫人则立刻弹。
ClassHelper有一整套「上课时段策略」:普通通知在上课时段只进队列、下课自动补弹,紧急通知与叫人则立刻弹。
这套判定完全基于课表,所以**课表排错,提醒就会在该弹的时候不弹**。
排完课之后建议立刻验一下:在**通知发布**页选这个班,随便发一条普通通知,
看客户端是不是在课间弹出来、上课时是不是安静地收着。规则详见[通知与提醒](../app/notifications.md)。
下一步:[装学生端](client.md)。
下一步:[装 ClassHelper 班级端](client.md)。
+1 -1
View File
@@ -8,7 +8,7 @@
href: web-admin.md
- name: 建班级、排课表
href: setup-class.md
- name: 装学生端
- name: 装 ClassHelper 班级端
href: client.md
- name: 接入 ClassIsland
href: classisland.md
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: 认识 Web 管理端
description: 登录班级小助手 Web 管理端,认一认 12 个页面、新手引导与手机端布局。
description: 登录 ClassHelper Web 管理端,认一认 12 个页面、新手引导与手机端布局。
---
# 认识 Web 管理端
+2 -2
View File
@@ -12,7 +12,7 @@ Web 管理端有一个「数据库管理」页(`/database`,**仅管理员可
| 能力 | 说明 |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 连接状态检测 | 当前类型(SQLite / MySQL)、连通性与延迟、引擎版本、数据体积、14 张表的行数、连接串(MySQL 密码脱敏) |
| 连接状态检测 | 当前类型(SQLite / MySQL)、连通性与延迟、引擎版本、数据体积、16 张表的行数、连接串(MySQL 密码脱敏) |
| 连接测试 | 对任意目标库试连(切换前的第一步) |
| **一键切换** | SQLite ⇄ MySQL:自动「备份当前库 → 改写 schema provider → 生成客户端 / 建表 → 子进程迁移全部数据 → 改写 `.env`」,异步任务带步骤进度,**失败自动回滚** |
| 备份 / 恢复 | gzip JSON 快照存在 `<数据目录>/backups/`;手动备份 / 从备份恢复(整库覆盖)/ 删除 |
@@ -23,7 +23,7 @@ Web 管理端有一个「数据库管理」页(`/database`,**仅管理员可
**备份、导入导出、跨库迁移共用同一种「JSON 快照」**:
- 14 张表按**拓扑序**导出;
- 16 张表按**拓扑序**导出;
- 恢复时临时**关闭外键检查** —— 因为 `User.classId → Class` 与 `Class.teacherId → User`
是**互相引用**的,任何排序都会被其中一侧卡死;
- 分批 `createMany` 写入。
+3 -3
View File
@@ -285,7 +285,7 @@ classhelper logs --unit classhelper-backup # 看定时备份的日志
sudo classhelper password # 改 admin
sudo classhelper password teacher1 # 改指定教师
sudo classhelper password --list # 列出可登录账号与班级账号(含是否已设密码)
sudo classhelper password --class G101 # 改某个班的班级密码(学生端登录用)
sudo classhelper password --class G101 # 改某个班的班级密码(ClassHelper 班级端登录用)
```
安全约定(照要求逐条落实):
@@ -432,7 +432,7 @@ sudo cp deploy/nginx.conf /etc/nginx/conf.d/classhelper.conf
| 升级卡住/失败 | 失败会自动回滚;看 `classhelper logs -n 100`,确认磁盘余量 ≥1GB |
| `classhelper` 命令找不到 | `ls -l /usr/local/bin/classhelper`;被删了就重装或 `ln -sfn <安装目录>/bin/classhelper /usr/local/bin/classhelper` |
| 忘记管理员密码 | `sudo classhelper password admin`(直连数据库,不需要登录) |
| 学生端连不上 | 客户端「服务器地址」填 `http://IP:端口` 或域名;确认没有多余路径与斜杠 |
| ClassHelper 班级端连不上 | 客户端「服务器地址」填 `http://IP:端口` 或域名;确认没有多余路径与斜杠 |
| CentOS 7 装完立刻退出 | glibc < 2.28,见下面的"已知限制" |
---
@@ -527,7 +527,7 @@ sudo bash /tmp/verify-linux.sh --full --package /tmp/classhelper-server-linux-x6
```bash
# ① 浏览器打开 http://<IP>:4000/ ,用 admin 登录,改一次密码再登录
# ② 学生端客户端填 http://<IP>:4000 用班级码 G101/123456 登录,收到一条老师发的通知
# ② ClassHelper 班级端客户端填 http://<IP>:4000 用班级码 G101/123456 登录,收到一条老师发的通知
# ③ 重跑安装器(幂等性):sudo bash /tmp/install.sh --yes --package <同一个包>
# 期望:配置与数据保留、JWT_SECRET 不变(已登录用户不被踢下线)
# ④ 卸载:sudo classhelper uninstall --yes
+5 -5
View File
@@ -4,7 +4,7 @@
| 形态 | 适用场景 | 产物 | 本机验证情况 |
| --------------------- | -------------------------------------------- | -------------------------------------------------------- | -------------------------------------- |
| **A. Windows 安装包** | 学校机房、教师办公电脑(单机即完整系统) | `releases/server/<版本>/安装包/班级小助手服务端-<版本>-x64-setup.exe` | ✅ 已完整实测(静默安装/启停/卸载/UI) |
| **A. Windows 安装包** | 学校机房、教师办公电脑(单机即完整系统) | `releases/server/<版本>/安装包/ClassHelper 服务端-<版本>-x64-setup.exe` | ✅ 已完整实测(静默安装/启停/卸载/UI) |
| **B. Docker + MySQL** | 云服务器、多终端共享一套数据(推荐长期方案) | `deploy/Dockerfile` + `deploy/docker-compose.yml` | ⚠️ 文件已提供,本机无 Docker 未实测 |
| **C. Linux 原生部署** | 已有 Linux 服务器(systemd,SQLite 或 MySQL) | [`deploy/install.sh`](https://github.com/LoveChengke/classhelper/blob/master/deploy/install.sh) + `classhelper` 运维命令 | ⚠️ 本机无 Linux,实机验收见 [linux-deploy.md](linux-deploy.md) §10 |
| **D. 手动部署** | 已有 Node 环境的服务器、需要自定义 | `pnpm dist:server` 的免安装目录 | ✅ 免安装目录已实测(163 项端到端) |
@@ -25,7 +25,7 @@ pnpm dist:server -- --reuse-deps # 迭代打包:复用已安装依赖,跳
| 路径 | 说明 |
| ----------------------------------------------------------------------- | ----------------------------------------------- |
| `releases/server/<版本>/免安装/` | 免安装目录,可直接拷到任意 Windows x64 机器运行 |
| `releases/server/<版本>/安装包/班级小助手服务端-<版本>-x64-setup.exe` | 安装程序(约 62 MB,NSIS) |
| `releases/server/<版本>/安装包/ClassHelper 服务端-<版本>-x64-setup.exe` | 安装程序(约 62 MB,NSIS) |
打包内容:内置 **Node 运行时**(目标机无需安装 Node.js)、后端产物、Web 管理端产物、
生产依赖(真实目录,非软链)、迁移 SQL、`.env`(随机 JWT 密钥)、启停脚本。
@@ -35,9 +35,9 @@ pnpm dist:server -- --reuse-deps # 迭代打包:复用已安装依赖,跳
双击安装程序即可,**无需管理员权限**(当前用户级安装):
- 安装目录:`%LOCALAPPDATA%\Programs\ClassHelperServer`
- 静默安装:`"班级小助手服务端-1.1.2-x64-setup.exe" /S`
- 静默安装:`"ClassHelper 服务端-1.2.0-x64-setup.exe" /S`
- 开始菜单:启动服务 / 停止服务 / 重启服务 / 打开管理端 / 使用说明 / 卸载
- 桌面:`班级小助手服务端.lnk`(打开管理端)
- 桌面:`ClassHelper 服务端.lnk`(打开管理端)
- 自动加入当前用户开机自启(`HKCU\...\CurrentVersion\Run`)
- 卸载信息注册到「应用和功能」
@@ -314,7 +314,7 @@ start.cmd :: Windows
| `PID_FILE` | — | PID 文件路径(安装包启停脚本依赖),相对 `server` 根 |
| `WEB_DIST_DIR` | 自动探测 | Web 管理端产物目录(`../web-admin/dist` 或 `./web`) |
| `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` |
| `DEFAULT_CLASS_PASSWORD` | `123456` | 新建班级时班级账号(学生端登录)的默认密码 |
| `DEFAULT_CLASS_PASSWORD` | `123456` | 新建班级时班级账号(ClassHelper 班级端登录)的默认密码 |
| `DEFAULT_TEACHER_PASSWORD` | `123456` | 新增/重置教师账号的默认密码(管理员录入教师用) |
| `TERM_START_DATE` | — | 第 1 教学周的周一,用于"当前周次"与课表默认视图 |
| `INITIAL_ADMIN_USERNAME` / `INITIAL_ADMIN_PASSWORD` | `admin` / `admin123` | 空库首启动创建的管理员 |
+1 -1
View File
@@ -107,7 +107,7 @@ docker compose up -d
日志里会明确写出「全新安装 / 升级安装」、应用了几个迁移、跳过了几个已存在对象。
### 客户端(学生端)
### 客户端(ClassHelper 班级端)
从 Releases 取新的 `setup.exe` 覆盖安装即可,配置在 `%APPDATA%`,不受影响。
+127 -47
View File
@@ -1,7 +1,7 @@
# ClassHelper · 完整参考
班级信息管理系统。一个 pnpm monorepo,一份代码产出四个交付物:**后端服务**、**Web 管理端**(教师 / 管理员)、
**桌面客户端**(ClassHelper 班级端 / 教室机器)与 **ClassIsland 联动插件**(装在教室的 ClassIsland 上)。**当前版本 1.1.2**。
**桌面客户端**(ClassHelper 班级端 / 教室机器)与 **ClassIsland 联动插件**(装在教室的 ClassIsland 上)。**当前版本 1.2.0**。
核心链路:
@@ -19,7 +19,7 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
└──── 老师下发的提醒 ◄────────┘
```
> 本文是ClassHelper的**完整参考**:功能行为、接口契约、验收证据与已知问题都在这里,是功能口径的权威来源。
> 本文是 ClassHelper的**完整参考**:功能行为、接口契约、验收证据与已知问题都在这里,是功能口径的权威来源。
> 面向「第一次打开仓库」的展示型介绍在 [README](https://github.com/LoveChengke/classhelper/blob/master/README.md);按读者分组的引导式文档在文档站(本目录)。
> 工程约定(改代码前必读)与本机踩坑在 [AGENTS.md](https://github.com/LoveChengke/classhelper/blob/master/AGENTS.md);生产部署在
> [production.md](management/production.md);切换数据库在 [mysql.md](management/mysql.md)。
@@ -78,7 +78,7 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
| 产物 | 产物文件位置(打包后) | 用途 |
| ------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **服务端 + Web 管理端安装程序** | `releases/server/<版本>/安装包/ClassHelper服务端-<版本>-x64-setup.exe` | 装到教师电脑 / 校服务器即完整系统,**内置 Node 运行时**,双击安装、开机自启、自动建库建号 |
| **服务端 + Web 管理端安装程序** | `releases/server/<版本>/安装包/ClassHelper 服务端-<版本>-x64-setup.exe` | 装到教师电脑 / 校服务器即完整系统,**内置 Node 运行时**,双击安装、开机自启、自动建库建号 |
| **服务端 Linux 安装包** | `releases/server/<版本>/linux-x64/classhelper-server-linux-x64-<版本>.tar.gz` | 给 `deploy/install.sh` 用(另见同名 `.sha256`);由 Actions 在 Linux 上构建后挂 Release |
| **学生客户端安装程序** | `releases/client/<版本>/安装包/ClassHelper-<版本>-x64-setup.exe` | 学生机安装(NSIS) |
| **学生客户端单文件版** | `releases/client/<版本>/安装包/ClassHelper-<版本>-x64-portable.exe` | 免安装直接运行(U 盘分发) |
@@ -94,7 +94,7 @@ ClassIsland(教室机器)──► 联动插件 ──► /api/integrations/
| 场景 | 形态 | 入口 |
| ----------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| 学校机房 / 教师电脑(Windows 单机) | Windows 服务端安装程序(内置 Node,双击即用) | `releases/server/<版本>/安装包/ClassHelper服务端-<版本>-x64-setup.exe` |
| 学校机房 / 教师电脑(Windows 单机) | Windows 服务端安装程序(内置 Node,双击即用) | `releases/server/<版本>/安装包/ClassHelper 服务端-<版本>-x64-setup.exe` |
| 云服务器 / 多终端共享(推荐长期) | Docker Compose + MySQL(另有 SQLite 单容器版) | `deploy/Dockerfile`、`deploy/docker-compose{,.sqlite}.yml` |
| 已有 Linux 服务器 | **一键安装器 + `classhelper` 运维命令**(systemd / docker) | `sudo bash deploy/install.sh`(先 `--check` 体检);运维见 `classhelper help` |
| 自定义 / 已有 Node 环境 | 手动部署免安装目录 | `pnpm dist:server` → `releases/server/<版本>/免安装/` |
@@ -175,23 +175,28 @@ pnpm verify:desktop
### 默认账号与种子数据
`pnpm db:seed` 会清空业务表并写入一套完整演示数据(计数由种子脚本自己打印,**2026-09-30 实测**:
用户 18(1 管理员 + 2 教师 + 15 名学生名单)/ 班级 3 / 课程 54(每班 18 科)/ 课表 93 / 作业 15 / 通知 12 / 成绩 90):
`pnpm db:seed` 会清空业务表并写入一套完整演示数据(计数由种子脚本自己打印,**2026-10-06 实测**:
账号 3 / 在读班级 3 / 学生 15 / 课程 54(每班 18 科)/ 课表 93 / 作业 16 / 通知 13 / 成绩 90 /
调班历史 2 / 已归档届别 1):
| 角色 | 用户名 | 密码 | 说明 |
| ------ | ------------- | ------------ | ------------------------------------------ |
| 管理员 | `admin` | `admin123` | 可访问全部班级 |
| 教师 | `teacher1` | `teacher123` | 张老师:高一(1)班、高二(3)班班主任 |
| 教师 | `teacher2` | `teacher123` | 李老师:高一(2)班班主任,高二(3)班协作教师 |
| 班级 | 班级码 `G101` | `123456` | 高一(1)班(**ClassHelper 班级端唯一登录方式**) |
| 班级 | 班级码 `G102` | `123456` | 高一(2)班 |
| 班级 | 班级码 `G203` | `123456` | 高二(3)班(不是 `G103`) |
| 角色 | 凭据 | 密码 | 说明 |
| ----------------------- | ------------- | ------------ | ----------------------------------------------------------- |
| 管理员 | `admin` | `admin123` | 可访问全部班级、可执行毕业归档 / 学年升级 / 学期周次 |
| 教师 | `teacher1` | `teacher123` | 张老师:`2026级1班` 班主任,兼该班数学 |
| 教师 | `teacher2` | `teacher123` | 李老师:`2026级2班` 班主任,兼 `2026级1班` 语文 |
| ClassHelper 班级端 | 班级码 `G101` | `123456` | `2026级1班`(**教室机器唯一的登录方式**) |
| ClassHelper 班级端 | 班级码 `G102` | `123456` | `2026级2班` |
| ClassHelper 班级端 | 班级码 `G203` | `123456` | `2025级3班`(不是 `G103`) |
- 密码由**种子脚本硬编码**(`admin123` / `teacher123` / 班级 `123456`);`.env` 的
`DEFAULT_CLASS_PASSWORD` 只决定**新建**班级时的初始班级密码。
- **学生没有个人账号**(2026-10-01 起彻底清理):学生只是名单记录(成绩 / 未交名单 / 已读人数 / 叫人都按名单),
没有用户名密码、不能登录;ClassHelper 班级端统一用班级码 + 班级密码。
- 教学周由 `TERM_START_DATE`(第 1 教学周的周一)换算,当前周次用于课表默认视图。
- **学生不是账号**(2026-10-06 起由表结构保证):`Student` 表里没有 `passwordHash` / `role`,
学号(`202601`…`202615`)只是名单记录与查询键 —— 拿学号去登录只会得到 401。
- 任课关系刻意让**语文**由另一位老师任教,所以班主任在语文上没有编辑权,
正好覆盖「班主任不自动拥有所有科目作业成绩编辑权」这条规则。
- 演示数据里还有一个**已归档**的 `2023级1班`(3 名毕业生),用于验证「毕业归档」页。
- 教学周由 `TERM_START_DATE`(第 1 教学周的周一)换算;管理员可在「学期周次」页逐周覆盖它,
见[学期周次](#412-学期周次可逐周覆盖仅管理员)。
### 常用命令
@@ -283,7 +288,7 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
| 能力 | 说明 |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **教师管理**(独立页面) | 单个录入(姓名 / 用户名 / 初始密码 / 角色)+导入名单(xlsx/csv)+编辑(改名 / 改角色)+修改密码+删除 |
| **教师管理**(独立页面) | 单个录入(姓名 / **工号** / 手机号 / 初始密码 / 角色)+导入名单(xlsx/csv)+编辑(改名 / 改工号 / 改角色)+修改密码+删除 |
| 录入教师仅管理员 | 页面只对 `ADMIN` 显示,路由 `meta.roles=['ADMIN']`;服务端 `/api/teachers` 全量接口 `requireRole('ADMIN')`(教师调用 403) |
| 删除教师有护栏 | 四种情况一律 409:还是**班主任**、名下有课程 / 班级分配、**发布过作业或通知**(后两项也是级联删除,删账号会连带删掉他在各班发过的内容) |
| 创建班级仅管理员 | `POST /api/classes` = `requireRole('ADMIN')`,教师(含班主任)403 |
@@ -440,7 +445,7 @@ Element Plus 也通过对齐它的 `--el-border-radius-*` 一并改造:
教师 / 管理员本人密码在 Web 管理端顶栏下拉修改 —— 防止教室机器上误改班级密码后其他机器被锁在门外。
**系统托盘**:关闭主窗口不退出程序,而是隐藏到托盘后台继续接收通知;托盘图标(左键单击 / 双击)恢复主窗口,
右键菜单提供「显示主窗口 / 隐藏到托盘 / 退出ClassHelper」。退出时统一清理:销毁托盘 → 销毁灵动岛
右键菜单提供「显示主窗口 / 隐藏到托盘 / 退出 ClassHelper」。退出时统一清理:销毁托盘 → 销毁灵动岛
(含帧循环与定时器)→ 销毁全部窗口,并用 `app.exit(0)` 兜底,任务管理器中不留残留进程。
#### 离线缓存与自动同步
@@ -608,27 +613,30 @@ POST /api/notifications { …, priority: "NORMAL" }(上课时段)
界面:Web 管理端 →「成绩录入」→「导入表格」(`components/TableImportDialog.vue`)。
1. **下载模板**:`GET /imports/template?kind=grades&format=xlsx|csv`
模板列:`学生用户名 / 学生姓名 / 考试名称 / 分数 / 总分 / 课程`(CSV 带 BOM,Excel 直接双击不乱码)。
模板列:`学生学号 / 学生姓名 / 考试名称 / 分数 / 总分 / 科目 / 等级`(CSV 带 BOM,Excel 直接双击不乱码)。
2. **上传预览**:`POST /imports/table/preview` → 列名、前 20 行、`totalRows`、**建议字段映射**与校验问题。
- 支持 `.xlsx / .xls / .csv / .tsv`(SheetJS,自动识别分隔符),**表格文件上限 8MB**;
文件以 base64 放进 JSON,请求体上限设为 12MB 才能让 8MB 文件真正传得进来;超限返回 413 `IMPORT_TOO_LARGE`。
- 列名按同义词自动映射(如「学号→学生用户名」「得分→分数」「满分→总分」),也可手动改;
- 列名按同义词自动映射(如「用户名→学号」「得分→分数」「满分→总分」「课程→科目」),也可手动改;
缺必填列不会静默通过,直接给出「缺少必填列「…」」提示。
3. **确认导入**:`POST /imports/table/commit`,参数 `mapping`(规范字段 → 列名)+ `mode`:
- `upsert`(默认)已存在则更新;`append` 已存在则跳过;
- 成绩重复判定:**同班级 + 同学生 + 同考试 + 同课程**;学生按「用户名」优先、「姓名」兜底匹配;
- 成绩重复判定:**同班级 + 同学生 + 同考试 + 同科目**;学生按「学号」优先、「姓名」兜底匹配;
- 返回 `{ total, inserted, updated, skipped, failed, errors[], warnings[] }`,
`errors[].row` 是 **Excel 视角的行号**(含表头),便于老师改完再导。
#### 2)学生名单导入(管理员)
界面:Web 管理端 →「学生管理」→「导入名单」(同一组件,`kind=students`)。模板列 `用户名 / 姓名`
(学生没有账号属性,**没有密码列**);用户名必须匹配 `^[A-Za-z0-9_.-]{3,32}$`;重复按同一用户名
(`append` 跳过 / `upsert` 更新姓名与班级);用户名被教师 / 管理员占用时该行进错误行,不会覆盖他人账号。
界面:Web 管理端 →「学生管理」→「导入名单」(同一组件,`kind=students`)。模板列
`学号 / 姓名 / 班级 / 性别 / 家长手机号`(学生不是账号,**没有密码列**);学号必须匹配
`^[A-Za-z0-9._-]{1,32}$`(入库前去掉全部空白并统一大写,所以唯一约束天然大小写无关);
重复按同一学号(`append` 跳过 / `upsert` 更新姓名、班级与联系方式);
表格里带「班级」列时按班级名解析(重名会报错让老师改用班级码),否则用弹窗里选的班级。
**只写 `Student` 名单,不创建任何账号。**
#### 3)教师名单导入(管理员)
界面:Web 管理端 →「教师管理」→「导入名单」(`kind=teachers`)。模板列 `用户名 / 姓名 / 初始密码 / 角色`;
界面:Web 管理端 →「教师管理」→「导入名单」(`kind=teachers`)。模板列 `工号 / 姓名 / 手机号 / 初始密码 / 角色`;
角色取 `ADMIN` / `TEACHER`(留空按 `TEACHER`);与新建教师同一套校验,导入后可直接登录。
#### 4)ClassIsland 时间配置导入(管理员 / 本班班主任)
@@ -685,13 +693,13 @@ POST /api/notifications { …, priority: "NORMAL" }(上课时段)
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- |
| ClassIsland → ClassHelper | 上报**课表 + 节次时间 + 上课状态**(现在上什么 / 下一节 / 本节起止时间),写入本班课表 | Web 端「ClassIsland 联动」实时状态卡片 |
| ClassHelper → ClassIsland | 老师在 Web 端发一条**提醒** → 落库 → 教室机器的插件取回 → ClassIsland 上**全屏弹出**(可语音朗读),播完回执、不会重复弹 | Web 端「ClassIsland 联动 → 下发提醒」 |
| (可选)班级 → ClassIsland | 把ClassHelper上排好的课表**镜像**回 ClassIsland:新建一份 `ClassHelper-<班级名>` 档案课表,不动老师原有的课表 | 同上页面的「开镜像」开关 |
| (可选)班级 → ClassIsland | 把 ClassHelper上排好的课表**镜像**回 ClassIsland:新建一份 `ClassHelper-<班级名>` 档案课表,不动老师原有的课表 | 同上页面的「开镜像」开关 |
#### 接入流程(三步)
1. **Web 端签发设备令牌**:管理员 / 本班班主任在「ClassIsland 联动」页点「接入新设备」,选择班级并生成令牌
(形如 `chci_xxxxxxxx…`,**明文只显示这一次**,服务端只存 sha256);
2. **教室机器填令牌**:在 ClassIsland 的「设置 → ClassHelper联动」里填入后端地址 + 令牌,点「立即上报」;
2. **教室机器填令牌**:在 ClassIsland 的「设置 → ClassHelper 联动」里填入后端地址 + 令牌,点「立即上报」;
3. **验证**:页面顶部出现设备卡片与「在线」标记,说明链路通了 —— 之后课表与上课状态自动同步,
老师发的提醒也会在这台机器上弹出来。
@@ -721,8 +729,8 @@ POST /api/notifications { …, priority: "NORMAL" }(上课时段)
| 要同步的内容 | 需要打开的开关 |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 服务端 → ClassIsland(镜像课表) | ① 插件设置「把班级课表镜像回 ClassIsland」+ ② Web 端该设备的「镜像课表」 |
| ClassIsland → 服务端(上报课表) | 插件设置「上报课表到ClassHelper」(关着时日志会明说「仅状态(开关已关闭)」);同步是**双向对齐**的:老师在 ClassIsland 里删掉 / 挪动的节次会一并清理,Web 端手排的课不会被删 |
| 提醒下发 | ① 教室客户端「通知显示位置」不是「只在 ClassHelper」+ ② 插件「接收ClassHelper提醒」 |
| ClassIsland → 服务端(上报课表) | 插件设置「上报课表到 ClassHelper」(关着时日志会明说「仅状态(开关已关闭)」);同步是**双向对齐**的:老师在 ClassIsland 里删掉 / 挪动的节次会一并清理,Web 端手排的课不会被删 |
| 提醒下发 | ① 教室客户端「通知显示位置」不是「只在 ClassHelper」+ ② 插件「接收 ClassHelper提醒」 |
#### 提醒弹在哪个端:由教室客户端自己选
@@ -757,14 +765,14 @@ Web 管理端「数据库管理」页(`/database`,仅 ADMIN 可见,后端
| 能力 | 说明 |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 连接状态检测 | 当前类型(SQLite/MySQL)、连通性与延迟、引擎版本、数据体积、14 张表行数、连接串(MySQL 密码脱敏) |
| 连接状态检测 | 当前类型(SQLite/MySQL)、连通性与延迟、引擎版本、数据体积、16 张表行数、连接串(MySQL 密码脱敏) |
| 连接测试 | 任意目标库的连通性试连(切换前的第一步) |
| **一键切换** | SQLite ⇄ MySQL:自动「备份当前库 → 改写 schema provider → prisma generate / db push 建表 → 子进程迁移全部数据 → 改写 .env」,异步任务带步骤进度,失败自动回滚 |
| 备份 / 恢复 | gzip JSON 快照存 `<数据目录>/backups/`;手动备份 / 从备份恢复(整库覆盖)/ 删除 |
| 定时备份 | enabled + 间隔小时数 + 保留份数,存 `data/database-settings.json`;服务端每 10 分钟检查一次到点任务(进程不运行不补跑) |
| 快捷导入导出 | 下载快照 JSON(跨库通用)/ 下载数据库文件(仅 SQLite)/ 导入快照(整库覆盖,base64 上传) |
关键设计:**备份、导入导出、跨库迁移共用同一种「JSON 快照」**(`lib/snapshot.ts`,14 张表按拓扑序导出,
关键设计:**备份、导入导出、跨库迁移共用同一种「JSON 快照」**(`lib/snapshot.ts`,16 张表按拓扑序导出,
恢复时临时关外键检查 —— `User.classId` 与 `Class.teacherId` 互相引用),因此任意备份都能恢复回任意一种受支持的数据库。
MySQL 的连接测试 / 建表 / 写入依赖随包内置的 `@prisma/adapter-mariadb`(已在依赖里,不再是「切换时手动安装」)。
@@ -818,6 +826,72 @@ MySQL 的连接测试 / 建表 / 写入依赖随包内置的 `@prisma/adapter-ma
## 5. 技术参考
### 4.11 毕业归档(仅管理员)
按**年度**把毕业的班级与学生归档,并留档当年转出的学生、以及毕业班级发过的作业与通知。
入口:Web 管理端 →「毕业归档」。
**归档 = 打标记 + 只读,不删任何数据**:
| 动作 | 结果 |
| ------------ | ---------------------------------------------------------------------------------------- |
| 归档一个届别 | 该届班级从常规列表隐去(`Class.archivedYearId`)、在读学生标记为「已毕业」(`Student.archivedYearId`) |
| 作业与通知 | **原样留在库里**,在「届别详情 → 看作业与通知」里只读浏览 |
| 撤销归档 | 班级与学生回到在读状态;同样不删任何数据(转出的学生保持转出 —— 那是事实,不是归档造成的) |
| 班级端登录 | 归档班级的教室机器**不能再登录**(403 `CLASS_ARCHIVED`),继续登进去只会看到一个只读的壳子 |
**「未毕业而升级的班级不归档」**:学年升级在「班级管理 → 班级设置 → 学年升级」里做,
**只改 `Class.grade`**(高一 → 高二),班级记录、学生名单、学号、作业与成绩全部沿用。
**班级称呼是「XXXX级X班」(入学年份 + 班号)**,不是「高一(1)班」—— 后者每年都要改一遍,
前者一路跟着这届学生到毕业。称呼由服务端按 `enrollmentYear` + `classIndex` 生成,表单里只填这两个数。
**调班 / 转出**(Web 管理端 →「学生管理」):
- **调班**(单个 / 批量同一接口):只改学生**当前班级**,**学号不变**、不创建账号;
原班级 / 新班级 / 操作人 / 时间记入 `StudentClassTransfer`,可查;
历史作业与成绩保留原归属(它们挂在各自的 `classId` 上,不随学生走)。
- **转出**:学籍离开本校(不是调班)。班级归属保留作历史,状态置为 `transferred`,
并写一条 `mode='transfer-out'` 的历史,归档页据此统计"当年转出的学生"。
### 4.12 学期周次(可逐周覆盖,仅管理员)
入口:Web 管理端 →「学期周次」。
课表、作业、成绩都按**教学周**组织,而「开学日期 + 每周七天」的线性推算遇到法定节假日调休、
周末补课、错峰开学就会**整体错位**(错一周全错)。所以支持逐周指定实际日期:
| 操作 | 说明 |
| ---------------- | ------------------------------------------------------------------------ |
| 选配置范围 | 留空 = **全校默认**;也可以挑某个班单独配(该班优先用自己的) |
| 学期开始日期 | 第 1 教学周的周一;填完点「按开学日期生成」得到一份标准学期 |
| 逐周日期区间 | 用日期范围选择器改任意一周(调休那一周拉长 / 开学前那几天并进第 1 周…) |
| 拉取放假安排 | 联网获取当年的法定节假日,**只给建议、不会自动改数据** |
| 保存 | 校验区间不重叠(重叠会让"现在第几周"出现歧义),然后**下发给所有 ClassHelper 班级端** |
- 存储:`TermWeek`(`classId = ''` 表示全校默认)+ `Class.termStartDate`;
- 判定:`lib/term.ts` 的 `loadTermContext()` 做「班级覆盖 → 全校默认 → 纯线性」三级回落,
`resolveCurrentWeek(now, context)` 优先按区间判定;
- 机房/服务器**没有外网时,"拉取放假安排"返回 `ok:false` 是正常结果**(不是错误),
失败结果同样进缓存,避免断网点一次要白等一个超时。
### 4.13 课表同步方向(服务端 → 教室)
2026-10-06 起,课表以**服务端为准**:
| 方向 | 开关 | 默认 | 谁触发 |
| --------------------- | --------------------------------- | ------ | ------------------------------------------ |
| 服务端 → 教室(下发) | `mirrorScheduleToClassIsland` | **开** | 老师改完课表自动镜像回教室 |
| 教室 → 服务端(上传) | `syncScheduleToServer` | **关** | 老师在 Web 端点「**从教室机器获取课表**」 |
为什么要反过来:自动回传会在老师手排课之后被教室的旧课表**悄悄覆盖**。
所以「取教室课表」改成一次显式的人工动作 —— 点击前会弹**警告对话框**说明会覆盖 / 合并本班课表,
确认后服务端置一个待办(`IntegrationDevice.requestScheduleReport`),
插件在下一次心跳(约 1 分钟)看到就把当前课表推上来并清除标记。
> 插件侧对应 `ReportSettingsDto.scheduleRequested` 与 `ReportAsync(..., forceSchedule: true)`;
> 补发发生在**镜像之前**(顺序反了会把刚拿到的课表盖掉)。
### 5.1 目录结构
```
@@ -894,7 +968,7 @@ class-helper/
│ ├── src/Services/ScheduleMapper.cs # 课表与单双周映射
│ ├── src/Services/ClassPlanWriter.cs # 镜像回 ClassIsland(含课间时间点)
│ ├── src/Services/ClassHelperNotificationProvider.cs # 把老师发的提醒显示到 ClassIsland 上
│ ├── src/Views/BridgeSettingsPage.axaml(.cs) # 「ClassHelper联动」设置页(Avalonia)
│ ├── src/Views/BridgeSettingsPage.axaml(.cs) # 「ClassHelper 联动」设置页(Avalonia)
│ ├── src/Interop/ClassHelperClient.cs # 后端 HTTP 客户端 + 对齐服务端 zod 的 DTO
│ └── scripts/{build,verify}.mjs # 构建打包(.cipx)与静态契约校验
├── web-admin/ # Web 管理端(Vue 3 + Vite + Element Plus + PWA)
@@ -939,7 +1013,7 @@ class-helper/
| 方法 | 路径 | 权限 | 说明 |
| --------------------------- | ------------------------------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------- |
| POST | `/auth/login` | 公开 | 登录(教师 / 管理员);`role=STUDENT` 返回 403(学生只有名单,改用班级登录) |
| POST | `/auth/login` | 公开 | 登录(教师 / 管理员的**工号** + 密码);学生不是账号,拿学号登录只会得到 401 |
| POST | `/auth/class-login` | 公开 | **班级账号登录**:班级码 + 班级密码 → `classSession` 会话 |
| GET | `/auth/me` | 登录 | 当前用户(学生附带班级 / 年级) |
| PATCH | `/auth/password` | 登录 | 修改自己的密码(班级会话 → 班级密码) |
@@ -994,7 +1068,7 @@ class-helper/
| GET / POST / PATCH / DELETE | `/integrations/devices[/:id]` | 管理员 / 教师 | ClassIsland 联动设备管理(令牌只存 sha256,令牌前缀用于人眼识别) |
| POST | `/integrations/devices/:id/token` | 管理员 / 教师 | 重置设备令牌(旧令牌立即失效,明文只返回一次) |
| POST | `/integrations/classisland/notify` | 管理员 / 教师 | 下发提醒到该班 ClassIsland 设备(广播 `classisland:notification`) |
| GET | `/database/status` | **管理员** | 数据库状态(连接 / 版本 / 体积 / 14 张表行数 / 备份列表 / 定时配置) |
| GET | `/database/status` | **管理员** | 数据库状态(连接 / 版本 / 体积 / 16 张表行数 / 备份列表 / 定时配置) |
| POST | `/database/test-connection` | **管理员** | 测试任意目标库连通性(provider 仅 sqlite / mysql) |
| GET / POST | `/database/backups` | **管理员** | 备份列表 / 立即备份(gzip JSON 快照,存 `<数据目录>/backups/`) |
| POST / DELETE | `/database/backups/:name/restore` · `/database/backups/:name` | **管理员** | 从备份恢复(整库覆盖)/ 删除备份 |
@@ -1038,12 +1112,12 @@ class-helper/
班级内的角色由「账号角色 + 与该班级的关系」共同决定,前后端共用同一份矩阵
(`packages/shared/src/permissions.ts`,后端 `lib/access.ts` 复用,前端用它隐藏入口):
| 角色 | 判定方式 | 可见范围 |
| ------------------ | ------------------------------ | -------------------------- |
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
| 科任老师 `SUBJECT` | `ClassTeacher` 中存在当前用户 | 被分配的班级 |
| 学生 `STUDENT` | `User.classId` / 班级账号 | 自己的班级(只读业务数据) |
| 角色 | 判定方式 | 可见范围 |
| --------------------- | ---------------------------------------- | -------------------------- |
| `ADMIN` 管理员 | `User.role = ADMIN` | 全部班级 |
| 班主任 `HEAD` | `Class.teacherId === 当前用户` | 本班 |
| 科任老师 `SUBJECT` | 该班存在 `Course.teacherId === 当前用户` | 被任课的班级 |
| `CLASS_DEVICE` 班级端 | 班级码 + 班级密码登录 | 本班(代全班读写个人数据) |
**权限矩阵**(`PERMISSION_MATRIX`,验收脚本按此逐条断言):
@@ -1118,15 +1192,21 @@ pnpm db:generate && pnpm --filter @classhelper/server db:deploy && pnpm db:seed
> MySQL 驱动适配器 `@prisma/adapter-mariadb` **已随包内置**(数据库管理模块的连接测试 / 迁移需要),无需手动安装。
数据模型共 **14 张表**:`User` / `Class` / `ClassTeacher` / `Enrollment` / `Course` / `Schedule` / `Homework` /
`HomeworkStatus` / `Notification` / `NotificationRead` / `TimeLayout` / `Grade` / `IntegrationDevice` / `ClassIslandPush`。
其中 `Class.code` / `Class.passwordHash` 是班级账号字段;`Class.notificationChannel` 决定提醒弹在哪端;
数据模型共 **16 张表**:`User`(纯账号表)/ `Student`(名单,不是账号)/ `StudentClassTransfer`(调班与转出历史)/
`ArchivedYear`(毕业归档届别)/ `Class` / `TermWeek`(学期周次区间)/ `Course`(= 班级 + 科目 + 教师的任课关系)/
`Schedule` / `Homework` / `HomeworkStatus` / `Notification` / `NotificationRead` / `TimeLayout` / `Grade` /
`IntegrationDevice` / `ClassIslandPush`。
其中 `Class.name` 是「2026级1班」这种**届别称呼**;`Class.code` / `Class.passwordHash` 是班级端账号字段;
`Class.notificationChannel` 决定提醒弹在哪端;`Class.studentGradeQueryEnabled` 是班级端查成绩的开关;
`Student.studentNo` 是学生的唯一标识与查询键;`Grade.levelType` / `level` 是等级口径与等级;
`Homework.assignDate` 是作业的「所属日期」;`Schedule.weekParity` 是单双周;`Schedule.source` 是课表来源;
`ClassIslandPush.kind` 区分通知类与叫人。
迁移历史(**10 个**,按时间顺序):`init` → `add_time_layout` → `add_class_account` → `add_schedule_week_parity` →
迁移历史(**16 个**,按时间顺序):`init` → `add_time_layout` → `add_class_account` → `add_schedule_week_parity` →
`add_class_term_weeks` → `add_classisland_integration` → `add_class_notification_channel` →
`add_homework_assign_date` → `add_push_kind` → `add_schedule_source`。
`add_homework_assign_date` → `add_push_kind` → `add_schedule_source` →
`add_student_table` → `add_student_grade_query` → `add_grade_level` →
`add_classhelper_heartbeat` → `add_archives` → `add_term_weeks`。
**启动时迁移(安装版自动升级)**:`src/lib/db-bootstrap.ts` 用一张账本表 `_ch_migrations` 记录已执行的迁移目录名 ——
全新安装依次执行全部迁移并创建初始管理员;覆盖安装只补跑账本里没有的迁移(已存在的表 / 索引自动跳过),
@@ -1234,7 +1314,7 @@ pnpm db:generate && pnpm --filter @classhelper/server db:deploy && pnpm db:seed
| **支持在客户端录入作业(含自定义快捷短语)** | ✅ | `verify:e2e`:班级账号录入 201(归属班主任)、跨班 403;客户端录入弹窗 + 「设置 → 通用」管理短语 |
| **提醒弹在哪个端由客户端自行选择** | ✅ | 客户端「设置 → 提醒」三选一(`verify:desktop`:设置页真实点击 → 写回本地配置);服务端按班级设置决定是否推送(`verify:classisland`:`client` 时通知与联动页下发都被跳过) |
| **ClassHelper能读取 ClassIsland 的课表** | ✅ | `verify:classisland`:插件上报课表 → 本班课表新增 / 更新(重复上报幂等)、节次时间写入、设备状态快照可在 Web 端展示;`verify:web`:教师在「ClassIsland 联动」页真实签发设备令牌 |
| **用户可选择通知是否在 ClassIsland 上显示** | ✅ | 插件设置页开关「接收ClassHelper提醒」(本机总闸)+ Web 端「下发提醒」(标题 / 内容 / 时长 / 优先级 / 语音朗读 / 是否同步通知中心);`verify:classisland`:提醒落库 → 待提醒 → 回执 → 确认后不补发 |
| **用户可选择通知是否在 ClassIsland 上显示** | ✅ | 插件设置页开关「接收 ClassHelper提醒」(本机总闸)+ Web 端「下发提醒」(标题 / 内容 / 时长 / 优先级 / 语音朗读 / 是否同步通知中心);`verify:classisland`:提醒落库 → 待提醒 → 回执 → 确认后不补发 |
| 能成功打包 Windows EXE | ✅ | `-x64-setup.exe` / `-x64-portable.exe` / `win-unpacked/*.exe`(见 [7. 打包与交付](#7-打包与交付)) |
| 提供完整 README(启动、构建、打包、默认账号) | ✅ | 本文档含快速开始、命令表、API、WebSocket、RBAC、模块化、数据库、打包与常见问题 |
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* 班级小助手文档站 —— docfx modern 模板的样式覆盖。
* ClassHelper文档站 —— docfx modern 模板的样式覆盖。
*
* docfx 按 `template` 数组的顺序合并模板,这个文件排在最后,因此覆盖掉 modern 模板自带的
* `public/main.css`(它本身是空的,被留作官方覆盖点)。docfx.min.css 在本文件之前加载,