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:
1 parent
17f55864bf
commit
9d5ac7adf9
172 files changed
+8761
-1896
No files matched your search
+1
-1
@@ -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
@@ -19,7 +19,7 @@ description: 老师点名让学生过来:普通与紧急两档、快捷短语
|
||||
|
||||
## 两档
|
||||
|
||||
| 级别 | 落库级别 | 学生端行为 |
|
||||
| 级别 | 落库级别 | ClassHelper 班级端行为 |
|
||||
| ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| **普通叫人** | `HIGH` | 课间先显示「叫人」胶囊,点击展开;**上课时段只进队列、不打断课堂**,下课后自动弹出详情(90 秒后收起) |
|
||||
| **紧急叫人** | `URGENT` | **无视上课时段立即展开**,上课时被收起后仍保留胶囊、可再次点开 |
|
||||
|
||||
+6
-6
@@ -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
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 成绩
|
||||
description: 成绩录入与导入、等级与得分率、学生端看到的全班总览。
|
||||
description: 成绩录入与导入、等级与得分率、ClassHelper 班级端看到的全班总览。
|
||||
---
|
||||
|
||||
# 成绩
|
||||
@@ -31,7 +31,7 @@ Web 管理端「成绩录入」页:
|
||||
|
||||
## 学生看到的是什么
|
||||
|
||||
学生端(教室机器)用的是**班级账号**,所以它看到的不是"我的成绩",而是**全班成绩总览**:
|
||||
ClassHelper 班级端(教室机器)用的是**班级账号**,所以它看到的不是"我的成绩",而是**全班成绩总览**:
|
||||
|
||||
- 表格:分数 / **得分率** / **等级**;
|
||||
- 柱状图;
|
||||
|
||||
+4
-4
@@ -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
@@ -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 档案导入 |
|
||||
|
||||
@@ -10,7 +10,7 @@ description: 通知的三档优先级、上课时段策略、紧急通知的二
|
||||
|
||||
## 三档优先级
|
||||
|
||||
| 优先级 | 学生端表现 |
|
||||
| 优先级 | ClassHelper 班级端表现 |
|
||||
| ------ | -------------------------------------------------------------------------------------------- |
|
||||
| 普通 | 课间弹「新消息」胶囊,点击展开详情;**上课时段完全不显示**(窗口隐藏)并进队列,下课自动补弹 |
|
||||
| 重要 | 同上,卡片上有优先级标签 |
|
||||
@@ -22,7 +22,7 @@ description: 通知的三档优先级、上课时段策略、紧急通知的二
|
||||
|
||||
「上课时段」由课表实时判定(见[课表](schedules.md))。判定之后:
|
||||
|
||||
| 学生端行为 | 上课时段 | 课间 / 放学 |
|
||||
| ClassHelper 班级端行为 | 上课时段 | 课间 / 放学 |
|
||||
| ---------- | ---------------------------------------- | ----------- |
|
||||
| 普通通知 | 收着不弹(窗口隐藏),进待发队列 | 直接弹 |
|
||||
| 紧急通知 | **立刻弹** | 立刻弹 |
|
||||
|
||||
+5
-5
@@ -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
@@ -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
@@ -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
@@ -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` 表里压根没有那一列。
|
||||
@@ -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` 发事件 ——
|
||||
|
||||
@@ -575,7 +575,7 @@ VISUALIZER_TITLE_OFFSET = 4.0 // 视觉条中心 y = 曲名基线 - 4
|
||||
|
||||
---
|
||||
|
||||
## 附:班级小助手实际采纳了哪些(本次重写对照表)
|
||||
## 附:ClassHelper实际采纳了哪些(本次重写对照表)
|
||||
|
||||
| WinIsland 做法 | 出处 | 我们的落地 | 验证 |
|
||||
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
|
||||
+3
-3
@@ -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
@@ -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))。
|
||||
用学生用户名去登录一定会失败,这是设计。
|
||||
|
||||
### 客户端里找不到"修改密码"
|
||||
|
||||
@@ -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 的课表会整体错位 —— 表现为"看着有内容、每节课都对不上"。
|
||||
|
||||
@@ -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` | 单文件便携版,双击即用,适合不改动机房机器 |
|
||||
|
||||
要自己打:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) —— 通知 / 作业 / 叫人 / 成绩
|
||||
|
||||
|
||||
@@ -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 班级端 / 教室机器)
|
||||
|
||||
| 项目 | 要求 |
|
||||
| -------- | ---------------------------------------------------------------------------- |
|
||||
|
||||
@@ -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`
|
||||
- 开始菜单:启动服务 / 停止服务 / 重启服务 / 打开管理端 / 使用说明 / 卸载
|
||||
- 自动加入当前用户开机自启
|
||||
|
||||
|
||||
@@ -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)。
|
||||
@@ -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,6 +1,6 @@
|
||||
---
|
||||
title: 认识 Web 管理端
|
||||
description: 登录班级小助手 Web 管理端,认一认 12 个页面、新手引导与手机端布局。
|
||||
description: 登录 ClassHelper Web 管理端,认一认 12 个页面、新手引导与手机端布局。
|
||||
---
|
||||
|
||||
# 认识 Web 管理端
|
||||
|
||||
@@ -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` 写入。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` | 空库首启动创建的管理员 |
|
||||
|
||||
@@ -107,7 +107,7 @@ docker compose up -d
|
||||
|
||||
日志里会明确写出「全新安装 / 升级安装」、应用了几个迁移、跳过了几个已存在对象。
|
||||
|
||||
### 客户端(学生端)
|
||||
### 客户端(ClassHelper 班级端)
|
||||
|
||||
从 Releases 取新的 `setup.exe` 覆盖安装即可,配置在 `%APPDATA%`,不受影响。
|
||||
|
||||
|
||||
+127
-47
@@ -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
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 班级小助手文档站 —— docfx modern 模板的样式覆盖。
|
||||
* ClassHelper文档站 —— docfx modern 模板的样式覆盖。
|
||||
*
|
||||
* docfx 按 `template` 数组的顺序合并模板,这个文件排在最后,因此覆盖掉 modern 模板自带的
|
||||
* `public/main.css`(它本身是空的,被留作官方覆盖点)。docfx.min.css 在本文件之前加载,
|
||||
|
||||
Reference in new issue
Block a user