57 KiB
班级小助手(Class Helper)
班级信息管理系统,采用 pnpm monorepo,包含后端服务、Web 管理端(教师/管理员)与 桌面客户端(学生)三端。核心链路:
教师在 Web 端发布内容
│ REST API(JWT 鉴权)
▼
后端服务(Express + Prisma)
│ 写入数据库 + 按班级房间广播
▼
Socket.IO ──► class:{classId} 房间 ──► 学生桌面客户端实时更新 UI
当前交付范围
| 阶段 | 内容 | 状态 |
|---|---|---|
| 1 | pnpm monorepo 脚手架、TypeScript / ESLint / Prettier / 环境变量 | ✅ 已完成 |
| 2 | 后端:Prisma schema + 迁移 + 种子数据 + JWT 认证 + RBAC + 模块化 REST API + Socket.IO | ✅ 已完成 |
| 3 | Web 管理端:登录、主布局、仪表盘、班级/学生/课表/作业/通知/成绩页面、Axios 封装、实时提示 | ✅ 已完成 |
| 4 | EXE 客户端:Electron 主进程/preload/渲染进程、登录、课表/作业/通知/成绩/设置、实时推送、IndexedDB 离线缓存 | ✅ 已完成(冒烟验证 19/19) |
| 5 | 三端联调脚本、打包命令、完整 README | ✅ 已完成(三套安装包 + 两套 UI 回归测试) |
| 6 | 测试账号与种子数据说明 | ✅ 已完成(见下文) |
| 7 | 生产化:服务端安装程序(内置 Node)、Web 端 PWA 可安装、Docker + Nginx 部署、生产加固与运维文档 | ✅ 已完成(见 docs/production.md) |
| 8 | 客户端灵动岛(通知浮窗,上课隐藏 / 下课弹出 / 紧急立即展开)与上课时段紧急通知二次确认 | ✅ 已完成(见"上课时段策略") |
与原始提示词的两处偏差(已与你确认)
- 桌面客户端采用 Electron + Vue 3(而非 Avalonia/FluentAvalonia):正文 90% 的要求 (Socket.IO Client、Pinia、IndexedDB 离线缓存、electron-builder)都基于 Electron, 且本机没有 .NET SDK。
- 数据库 先用 SQLite(libSQL 内嵌)跑通 MVP,并预留 MySQL 切换:本机没有可用 MySQL 实例;
docs/mysql.md给出云数据库切换的完整步骤,deploy/提供 MySQL 版 Compose。
三套交付产物
| 产物 | 文件 | 用途 |
|---|---|---|
| 服务端 + Web 管理端安装程序 | release-server/班级小助手服务端-0.1.0-x64-setup.exe(34 MB) |
装到教师电脑/校服务器即完整系统,内置 Node 运行时,双击安装、开机自启、自动建库建号 |
| 学生客户端安装程序 | packages/desktop-client/release/班级小助手-0.1.0-x64-setup.exe(107 MB) |
学生机安装(NSIS 安装包) |
| 学生客户端单文件版 | packages/desktop-client/release/班级小助手-0.1.0-x64-portable.exe(107 MB) |
免安装直接运行(U 盘分发) |
| Web 管理端(PWA) | 由服务端在 / 直接托管 |
浏览器打开即用,可在 Edge/Chrome 中「安装为应用」 |
生产部署(Windows 安装包 / Docker + MySQL / 手动部署)请看 docs/production.md。
目录结构
class-helper/
├── package.json # 根脚本(dev / build / db:* / verify:* / dist:* / icons)
├── pnpm-workspace.yaml # workspace 定义 + allowBuilds(pnpm 11 依赖构建白名单)
├── tsconfig.base.json # 共享 TS 基础配置
├── eslint.config.mjs # ESLint 扁平配置(TS + Vue)
├── .dockerignore # 容器构建上下文排除项
├── build/icon.{ico,png} # 应用图标(由 pnpm icons 生成)
├── scripts/
│ ├── use-database.mjs # SQLite ⇄ MySQL provider 切换助手
│ ├── generate-icons.mjs # 用 Electron 渲染 SVG 生成 PNG/ICO 图标
│ ├── dist-server.mjs # 服务端 + Web 管理端 打包(免安装目录 + NSIS 安装程序)
│ ├── lib/electron-env.mjs # 启动 Electron 的公共处理(清理 RUN_AS_NODE + 定位可执行文件)
│ ├── nsis/server-installer.nsi# 安装程序脚本模板
│ └── ui-smoke/ # Web 管理端 UI 真实点击回归测试(Electron 驱动 + 上课时段探针)
├── deploy/ # 生产部署:Dockerfile / docker-compose.yml / nginx.conf
├── docs/
│ ├── production.md # 生产部署指南(三种形态 + 运维 + 安全清单)
│ └── mysql.md # MySQL 切换指南
└── packages/
├── shared/ # 三端共享:类型契约、常量、工具函数
│ └── src/{types,constants,utils,index}.ts
├── server/ # 后端服务
│ ├── prisma/schema.prisma # 数据模型
│ ├── prisma/migrations/ # 迁移历史(已生成并应用)
│ ├── prisma/seed.ts # 种子数据
│ ├── prisma.config.ts # Prisma 7 配置(迁移/种子/连接串)
│ ├── scripts/verify-e2e.mjs # 后端端到端验收脚本(62 项)
│ └── src/
│ ├── app.ts # Express 装配(静态托管 + 探针 + 限流 + 模块挂载)
│ ├── index.ts # 启动入口(自检/初始化 + HTTP + Socket.IO + 优雅退出)
│ ├── config/env.ts # 环境变量校验(zod)+ 生产配置自检
│ ├── lib/ # db / db-bootstrap / web-static / http / jwt / access(RBAC) / mappers
│ ├── middleware/ # auth / error / validate / security(helmet+限流+耗时日志)
│ ├── realtime/ # socket.ts + bus.ts(事件总线)
│ └── modules/ # 功能模块 + registry.ts
├── web-admin/ # Web 管理端(Vue 3 + Vite + Element Plus + PWA)
│ ├── public/ # manifest / sw.js / 图标(由 pnpm icons 生成)
│ └── src/{api,stores,router,layouts,views,styles}
└── desktop-client/ # EXE 客户端(Electron + Vue 3)
├── electron-builder.yml # 打包配置(nsis 安装包 + portable 单文件)
├── build/icon.ico # 应用图标(electron-builder 自动使用)
├── vite.config.mts # 渲染进程构建(base: './',hash 路由)
├── scripts/{build-main,dev,smoke,dist-win}.mjs
└── src/
├── main/ # 主进程:窗口/单实例/配置持久化(IPC)/冒烟验证/灵动岛
├── preload/ # contextBridge 安全桥(不暴露 ipcRenderer 本体)
├── island/ # 灵动岛渲染进程(独立透明置顶窗口)
├── types/desktop.d.ts # 主进程 <-> 渲染进程契约
└── renderer/ # 渲染进程:api / stores / cache / router / layouts / views
技术栈
| 层 | 选型 | 版本 |
|---|---|---|
| 包管理 | pnpm workspace | 11.8.0 |
| 语言 | TypeScript | 5.9.3(typescript-eslint 要求 < 6.1,因此未用 7.x) |
| 后端 | Node.js + Express | 24.18 / 5.2 |
| ORM | Prisma + driver adapter | 7.10(+ @prisma/adapter-libsql) |
| 实时 | Socket.IO | 4.8 |
| 认证 | jsonwebtoken + bcryptjs | 9.0 / 3.0 |
| 校验 | zod | 4.6 |
| Web 端 | Vue 3 + Vite + Element Plus + Pinia + Vue Router + Axios + ECharts | 3.5 / 8.3 / 2.14 / 4.0 / 5.3 / 1.20 / 6.1 |
| EXE 客户端 | Electron + Vue 3 + Element Plus + Pinia + Socket.IO Client + IndexedDB | 44.3 / 3.5 / 2.14 / 4.0 / 4.8 |
| 打包 | electron-builder(nsis / portable) | 26.15 |
运行环境要求:Node.js ≥ 20.19(Prisma 7 硬性要求,推荐 22/24)。
快速开始
# 1. 安装依赖(Node >= 20.19,pnpm >= 10)
pnpm install
# 2. 准备环境变量(后端)
cp packages/server/.env.example packages/server/.env
# → 至少修改 JWT_SECRET;本地开发可直接使用仓库内已有的 .env
# 3. 生成 Prisma Client 并建库、写入种子数据
pnpm db:generate
pnpm db:migrate
pnpm db:seed
# 4. 启动(后端 4000 + Web 端 5173 一起启动)
pnpm dev
启动后访问:
| 服务 | 地址 |
|---|---|
| Web 管理端(开发) | http://127.0.0.1:5173 |
| 后端服务首页(浏览器可读,列出已挂载模块) | http://127.0.0.1:4000 |
| 后端 API | http://127.0.0.1:4000/api |
| 健康检查 | http://127.0.0.1:4000/api/health |
后端只提供 REST API 与 WebSocket,没有业务网页界面。直接访问
http://127.0.0.1:4000/会返回服务首页(带Accept: application/json时返回结构化信息); 其它未匹配路径统一返回{ "success": false, "code": "NOT_FOUND" }。 Web 管理端地址是 5173 端口,请勿访问 80 端口(会连接被拒绝)。
也可以分开启动:pnpm dev:server(仅后端)、pnpm dev:web(仅 Web 端)。
启动 EXE 客户端(学生端)
# 开发模式:自动构建主进程 + 启动 Vite 渲染进程(5174) + 打开 Electron 窗口(支持热更新)
pnpm dev:desktop
# 已构建产物的冒烟验证(不弹窗,跑完自动退出)
pnpm build:desktop
pnpm verify:desktop
客户端首次启动会带上默认服务器地址 http://127.0.0.1:4000,可用学生账号 student01 / student123 登录。
若要验证"断网可查看缓存",登录并浏览过程序后关闭后端(Ctrl+C 停掉 pnpm dev:server),
客户端会在顶部提示「当前处于离线状态」,页面继续显示本地缓存数据;重新启动后端后会自动同步。
端到端验收脚本
# 另开一个终端先启动后端:pnpm dev:server
pnpm verify:e2e # 后端 + REST + Socket.IO + RBAC + 上课时段拦截:62 项
pnpm verify:web # Web 管理端真实点击(含上课时段紧急通知全屏警告):8 项
pnpm verify:desktop # Electron 客户端(含灵动岛与联网集成):19 项
后端脚本验证实时推送时延、作业完成、成绩下发、权限隔离、上课时段紧急通知拦截等 62 项, 实测通知 37ms、作业 26ms、成绩 25ms 到达(要求 < 5 秒),紧急通知 409 拦截与二次确认后发布均通过; 客户端脚本验证 preload 桥接、渲染进程、IndexedDB 读写、断网回退、灵动岛四种状态切换与联网集成 (登录 + 四类数据 + Socket.IO),实测 19/19 通过。
默认账号与种子数据
pnpm db:seed 会清空业务表并写入一套完整演示数据(1 管理员 / 2 教师 / 3 班级 / 15 学生 /
15 课程 / 90 条课表 / 15 份作业 / 12 条通知 / 90 条成绩)。
| 角色 | 用户名 | 密码 | 说明 |
|---|---|---|---|
| 管理员 | admin |
admin123 |
可访问全部班级 |
| 教师 | teacher1 |
teacher123 |
张老师:高一(1)班、高二(3)班班主任 |
| 教师 | teacher2 |
teacher123 |
李老师:高一(2)班班主任,高二(3)班协作教师 |
| 学生 | student01 |
student123 |
高一(1)班 |
| 学生 | student02 … student05 |
student123 |
高一(1)班 |
| 学生 | student06 … student10 |
student123 |
高一(2)班 |
| 学生 | student11 … student15 |
student123 |
高二(3)班 |
- 学生初始密码可用
DEFAULT_STUDENT_PASSWORD配置(默认123456), 教师也可在「学生管理」里一键重置为默认密码。 - 教学周由
TERM_START_DATE(第 1 教学周的周一)换算,当前周次用于课表默认视图。
常用命令
| 命令 | 说明 |
|---|---|
pnpm dev |
并行启动 shared(tsc watch) + 后端 + Web 端 |
pnpm dev:server / pnpm dev:web |
只启动后端 / 只启动 Web 端 |
pnpm dev:desktop |
启动 EXE 客户端开发模式(Vite 5174 + Electron,热更新) |
pnpm build |
构建 shared + 后端 + Web 端 + EXE 客户端 |
pnpm build:desktop |
仅构建 EXE 客户端(esbuild 主进程/preload + Vite 渲染进程) |
pnpm icons |
生成应用图标(PNG/ICO,用 Electron 渲染 SVG) |
pnpm dist:server |
打包服务端 + Web 管理端(免安装目录 + NSIS 安装程序,内置 Node) |
pnpm dist:dir |
打包客户端免安装目录 release/win-unpacked(含可执行文件,最快) |
pnpm dist:win |
打包客户端 nsis 安装包 + portable 单文件 EXE |
pnpm dist:all |
服务端安装程序 + 客户端安装程序一起打 |
pnpm verify:e2e |
后端端到端验收(62 项,需服务端已启动) |
pnpm verify:web |
Web 管理端 UI 真实点击测试(Electron 驱动,8 项) |
pnpm verify:desktop |
EXE 客户端冒烟验证(19 项,含灵动岛像素级截图校验、侧边栏点击与离线回退) |
pnpm typecheck |
全仓库类型检查(含 vue-tsc) |
pnpm lint / pnpm lint:fix |
ESLint 检查 / 自动修复 |
pnpm format / pnpm format:check |
Prettier 格式化 / 检查 |
pnpm db:generate |
生成 Prisma Client(输出到 packages/server/src/generated/prisma) |
pnpm db:migrate |
创建并应用迁移 |
pnpm db:seed |
写入种子数据 |
pnpm db:reset |
重置数据库并重新执行 seed |
pnpm db:studio |
打开 Prisma Studio |
pnpm db:switch:mysql / db:switch:sqlite |
切换数据库 provider |
EXE 客户端(学生端)
进程结构与安全设置
| 进程 | 文件 | 职责 |
|---|---|---|
| 主进程 | src/main/* → dist/main/index.js |
窗口/单实例锁、配置持久化(safeStorage 加密 token)、IPC、冒烟验证 |
| 预加载 | src/preload/index.ts → dist/preload/index.js |
通过 contextBridge 暴露最小 API(getConfig/saveConfig/getAppInfo/openExternal),不暴露 ipcRenderer 本体 |
| 渲染进程 | src/renderer/** → dist/renderer/* |
Vue 3 界面、Axios 请求、Socket.IO 实时、IndexedDB 离线缓存 |
安全基线:contextIsolation: true、nodeIntegration: false、sandbox: true、webSecurity: true,
外部链接一律交给系统浏览器打开,禁止应用内导航到非本地地址。
功能页面
| 页面 | 功能 |
|---|---|
| 登录 | 服务器地址 + 用户名 + 密码,附带「测试连接」;已登录但服务器不可达时可「离线进入」 |
| 课表 | 按周展示 7 列课表,支持上一周/下一周/下拉选择周次,高亮"今天",显示周次范围与地点 |
| 作业 | 列表(全部/未完成/已完成过滤)、详情抽屉、标记完成/取消完成、附件链接、逾期高亮 |
| 通知 | 未读红点(侧边栏徽标)、优先级标签、点击自动标为已读、全部已读、未读过滤、详情抽屉 |
| 成绩 | 表格(分数/得分率进度条/等级)+ ECharts 柱状图(各次考试平均得分率)+ 等级分布 + 最近更新 |
| 设置 | 服务器地址(保存并测试)、账号信息、离线缓存条目统计与清空、客户端版本信息、退出登录 |
通知到达时还会由灵动岛在桌面中上方浮出提醒(见上一节),上课时段自动隐藏、紧急通知立即展开。
灵动岛(桌面通知浮窗)
客户端有一个独立于主窗口的灵动岛:一个无边框、透明、置顶、不占任务栏的窄条窗口,
常驻桌面中上方(屏幕工作区顶部居中,y = workArea.y + 8)。它是"消息到达"的第一现场:
| 场景 | 灵动岛行为 |
|---|---|
| 收到通知(非上课时段) | 从隐藏态淡入为胶囊:新消息 · 共 N 条 + 发送人 + 点击查看,15 秒后自动收起 |
| 点击胶囊 | 展开为详情卡:标题、内容、时间、打开应用 / 标为已读 / 知道了,20 秒后收起 |
| 上课时间段收到普通通知 | 自动隐藏(不打扰课堂),进入待发队列;下课后自动在桌面中上方弹出详情(14 秒)→ 收起为胶囊(25 秒) |
| 上课时间段收到紧急通知 | 无论是否上课立刻展开显示详情(带红色呼吸光晕与"紧急"角标,无需点击),45 秒后收起 |
| 多条约谈 | 队列按时间排序,紧急通知优先插播;卡片底部提示"还有 N 条通知" |
| 退出/断开 | 主窗口退出时灵动岛一并关闭;上课状态来自 GET /api/schedules/current(10 秒心跳 + 下课时长定时器) |
- 动画:窗口尺寸用
easeOutCubic260ms 逐帧缓动(胶囊 ↔ 详情 形变),透明度单独淡入淡出; 卡片内部再用cubic-bezier(0.34, 1.56, 0.64, 1)(弹簧回弹)做入场与点击反馈, 紧急形态的红色光晕由独立的.glow层以urgent-breath无限呼吸,未读点用dot-pulse。 两套窗口动画使用独立令牌,互不打断(早前"淡入取消形变导致窗口卡在胶囊尺寸"的问题已修复)。 - 事件流:
notification:new→renderer/stores/realtime.ts→pushNotificationToIsland()→ 主进程IslandController(决定隐藏/胶囊/详情)→ 灵动岛渲染进程src/island/IslandApp.vue。 - 相关文件:
src/main/island.ts(控制器 + IPC)、src/preload/island.ts、src/island/IslandApp.vue、src/renderer/island/bridge.ts(上课状态轮询与通知转发)。
各形态实拍(由 pnpm verify:desktop 自动截取并归档到 docs/screenshots/island/,并做像素级校验):
| 形态 | 截图 | 尺寸(逻辑 / 截图) | 说明 |
|---|---|---|---|
| 下课后自动弹出详情 | ![]() |
404×316 / 808×632 | 上课期间暂存的通知,下课后自动弹出 |
| 上课期间紧急通知 | ![]() |
424×344 / 848×688 | 红色光晕 + 紧急角标,无需点击 |
| 非上课时段"新消息"胶囊 | ![]() |
268×44 / 536×88 | 点击后展开详情 |
| 点击胶囊后的详情 | ![]() |
404×316 / 808×632 | 标题/内容/时间 + 打开应用/标为已读/知道了 |
截图路径可用
ISLAND_SHOTS_DIR覆盖(默认.cache/island-shots/); 归档目录里的图就是打包后 EXE 跑自动化验证时产出的同一份文件。
离线缓存与自动同步
┌────────────── 在线 ──────────────┐
请求 → fetchWithCache(store, key, loader, fallback)
└── 失败 → 读取 IndexedDB 缓存 → 返回旧数据(UI 标记"离线缓存")
实时推送(notification:new / grade:updated ...) → 更新内存状态 + 写回 IndexedDB
服务器不可达 → 可达(health 轮询 20s 或任意请求成功)→ 触发 onServerRecovered → 各页自动重新拉取
- 缓存按业务域分为 6 个 object store:
profile/classes/schedules/homeworks/notifications/grades。 - 缓存写入前统一做 JSON 纯化(Vue 响应式对象是 Proxy,直接写入 IndexedDB 会抛
could not be cloned)。 - 登录令牌由主进程用 Electron
safeStorage(Windows 下为 DPAPI)加密后落盘,因此重启客户端可离线进入。 - 令牌缺失/失效时保持缓存资料可见,恢复网络后自动刷新个人资料。
打包 Windows EXE
pnpm dist:dir # 免安装目录:packages/desktop-client/release/win-unpacked/班级小助手.exe
pnpm dist:win # nsis 安装包 + portable 单文件(需联网下载 NSIS 工具,可用镜像)
打包脚本 packages/desktop-client/scripts/dist-win.mjs 做了三件事,规避常见网络问题:
- 复用本地已解压的 Electron:检测到
node_modules/electron/dist/electron.exe时通过--config.electronDist直接使用,不再重复下载 150MB+ 发行包; - 默认走国内镜像(可用环境变量覆盖):
ELECTRON_MIRROR=https://cdn.npmmirror.com/binaries/electron/、ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/; - 构建缓存固定到仓库内
.cache/,不污染全局目录。
产物说明:release/win-unpacked/班级小助手.exe(约 235MB,Electron 运行时)+ resources/app.asar
(约 2.7MB,仅含 dist/ 与 package.json,运行时不需要 node_modules)。
默认使用 Electron 内置图标(未提供 build/icon.ico)。
客户端冒烟验证(无需人工点击)
pnpm verify:desktop 以 ELECTRON_SMOKE_TEST=1 启动应用(不弹窗),主进程依次校验后自动退出:
| 校验项 | 实测结果 |
|---|---|
| 渲染进程挂载 + 窗口标题 | ✅ children=1 / title=班级小助手 |
| 登录页渲染(三个输入框) | ✅ 服务器地址 / 用户名 / 密码 |
| preload contextBridge 注入 | ✅ 9 个方法(含灵动岛 3 个) |
| IPC 往返(getAppInfo/saveConfig) | ✅ Electron 44.3.0 |
| 配置文件写入用户目录 | ✅ %APPDATA%\@classhelper\desktop-client\config.json |
| IndexedDB 缓存读写 | ✅ 读写/统计/删除 + 6 个 store |
| 断网时回退本地缓存 | ✅ fromCache=true(请求不可达端口) |
| 灵动岛窗口创建(置顶/透明/不占任务栏) | ✅ ready=true |
| 上课期间普通通知自动隐藏(暂存) | ✅ mode=hidden queued=1 |
| 下课后自动弹出暂存通知详情 | ✅ mode=expanded reason=after-class |
| 上课期间紧急通知立即展开(无需点击) | ✅ mode=expanded reason=urgent |
| 胶囊 → 点击 → 展开详情 | ✅ 新消息 · 共 2 条 → expanded(含真实 DOM 断言与截图) |
| 灵动岛截图留档 + 像素级校验 | ✅ 4 张(808×632 / 848×688 / 536×88 / 808×632);宽高比与状态机配置偏差 0.000;胶囊态偏红 205 px、紧急态偏红 20473 px(红色光晕);形态尺寸递增 pill < expanded < urgent |
联网集成(ELECTRON_SMOKE_ONLINE=1��‰ |
✅ 登录王小明 / 1 班级 / 6 作业 / 9 通知 / 8 成绩 / 第1周30节课 / Socket.IO 已连接 |
该验证对开发产物与打包后的 EXE 都适用(打包后用 ELECTRON_SMOKE_RESULT=<file> 写出 JSON 结果,
已实测 packaged: true、16/16 通过、退出码 0)。灵动岛各状态的窗口截图会写到
ISLAND_SHOTS_DIR(默认 .cache/island-shots/,仓库内留档目录 docs/screenshots/island/),
并对尺寸、宽高比、绘制内容(不透明像素/颜色种类)与紧急态红色像素做断言。
上课时段策略(紧急通知二次确认)
"上课时段"由课表实时判定:GET /api/schedules/current?classId= 返回
{ inClass, current, next, week, serverTime }(at 参数可覆盖判定时刻,便于联调与测试)。
服务端强制拦截(不是只靠前端提醒):
POST /api/notifications { classId, title, content, priority: "URGENT" } → 409 URGENT_DURING_CLASS
POST /api/notifications { …, priority: "URGENT", confirmDuringClass: true } → 201
POST /api/notifications { …, priority: "NORMAL" }(上课时段) → 201(不受限)
409 响应体携带 details: { classId, current, next, week, serverTime },前端据此渲染"正在上的课"。
这样即使有人绕过界面直接调接口,也一定会被拦下来。
Web 管理端:教师在通知发布弹窗选择"紧急"并提交时,先查询班级上课状态,命中则弹出全屏二次确认
(packages/web-admin/src/components/UrgentClassWarning.vue):
- 全屏遮罩 + 强警示配色,文案"现在为上课时间段……" + 正在上的课时段 + 待发布标题;
- 确认按钮带 3 秒倒计时(圆环进度 + 每秒钟数提示),倒计时结束前不可点击,防止误触;
- 取消则不发;确认后带
confirmDuringClass: true重新提交并成功发布; - 兜底:并发场景(提交瞬间刚好打铃)仍会收到 409,此时同样弹出该警告。
EXE 客户端:上课时段非紧急通知自动隐藏、下课后自动弹出;紧急通知无论是否上课都立即展开(见"灵动岛"一节)。
回归测试:verify:e2e 覆盖 409 拦截 / 二次确认后 201 / 普通通知不受限(用 at 固定时刻,结果可复现);
verify:web 用真实点击构造"正在上课"场景并断言全屏警告文案、按钮禁用与倒计时时长(实测 ~2.96s)。
REST API 一览
统一响应体:{ "success": true, "data": {}, "message": "" }(错误为 success:false + code)。
所有接口前缀 /api,除登录外均需 Authorization: Bearer <token>。
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| POST | /auth/login |
公开 | 登录,返回 token + 用户信息 |
| GET | /auth/me |
登录 | 当前用户(学生附带班级/年级) |
| PATCH | /auth/password |
登录 | 修改自己的密码 |
| POST | /auth/logout |
登录 | 退出(无状态,客户端丢弃 token) |
| GET | /classes |
登录 | 班级列表(按权限收敛) |
| GET | /classes/:id |
班级可见 | 班级详情(学生/课程/协作教师) |
| POST / PATCH / DELETE | /classes /classes/:id |
教师/管理员 | 班级增删改 |
| GET / POST | /classes/:id/students |
班级可见 / 可写 | 学生名单 / 添加学生(已存在账号直接转入) |
| DELETE | /classes/:id/students/:userId |
教师/管理员 | 移出学生 |
| POST / DELETE | /classes/:id/teachers[/:teacherId] |
教师/管理员 | 分配 / 取消协作教师 |
| GET / POST / PATCH / DELETE | /courses |
登录 / 教师 | 课程管理 |
| GET | /schedules?classId=&week=&dayOfWeek= |
登录 | 课表列表(week 过滤周次范围) |
| GET | /schedules/grid?classId=&week= |
登录 | 周视图(7 列结构,供客户端直接渲染) |
| GET | /schedules/current?classId=&at= |
登录 | 当前上课状态(inClass / current / next;at 为诊断用时间覆盖) |
| POST / PATCH / DELETE | /schedules |
教师/管理员 | 课表增删改(广播 schedule:updated) |
| GET | /homeworks?classId=&courseId=&pendingOnly=&keyword= |
登录 | 作业列表(学生带完成状态,教师带完成人数) |
| GET | /homeworks/:id |
班级可见 | 作业详情 |
| POST / PATCH / DELETE | /homeworks |
教师/管理员 | 发布/修改/删除(广播 homework:new / homework:updated) |
| PATCH | /homeworks/:id/status |
登录 | 标记完成/取消(广播 homework:status) |
| GET | /notifications?classId=&priority=&unreadOnly=&keyword= |
登录 | 通知列表(带已读状态) |
| GET | /notifications/unread-count |
登录 | 未读数(红点) |
| POST | /notifications |
教师/管理员 | 发布通知(广播 notification:new);上课时段发布紧急通知需 confirmDuringClass |
| POST | /notifications/:id/read、/notifications/read-all |
登录 | 标记已读 |
| DELETE | /notifications/:id |
教师/管理员 | 删除通知 |
| GET | /grades/my |
登录 | 个人成绩 |
| GET | /grades?classId=&courseId=&userId=&examName= |
教师/管理员 | 班级成绩 |
| GET | /grades/stats?classId=&courseId=&examName= |
教师/管理员 | 等级分布 + 各课程平均得分率 |
| POST | /grades、/grades/bulk |
教师/管理员 | 单条 / 批量录入(广播 grade:updated) |
| PATCH / DELETE | /grades/:id |
教师/管理员 | 修改 / 删除成绩 |
| GET | /students?classId=&keyword= |
教师/管理员 | 学生名单 |
| POST / PATCH / DELETE | /students |
教师/管理员 | 学生账号增删改 |
| POST | /students/:id/reset-password |
教师/管理员 | 重置密码 |
| GET | /teachers?keyword= |
教师/管理员 | 教师列表(分配协作教师用) |
| POST | /teachers |
管理员 | 新建教师账号 |
| GET | /dashboard/summary / /dashboard/term |
登录 | 仪表盘汇总 / 学期周次 |
| GET | /health |
公开 | 健康检查(含已挂载模块列表) |
WebSocket 事件
连接方式:io(url, { auth: { token } }),握手阶段用 JWT 鉴权。
| 事件 | 方向 | 载荷 |
|---|---|---|
connected |
服务端 → 客户端 | { userId, role, rooms } |
notification:new |
服务端 → 客户端 | NotificationDto |
homework:new |
服务端 → 客户端 | HomeworkDto |
homework:updated |
服务端 → 客户端 | HomeworkDto & { deleted?: boolean } |
homework:status |
服务端 → 客户端 | HomeworkStatusDto & { classId } |
grade:updated |
服务端 → 客户端 | GradeDto |
schedule:updated |
服务端 → 客户端 | { classId, action, schedule? } |
class:updated |
服务端 → 客户端 | { classId, action } |
class:join / class:leave / ping |
客户端 → 服务端 | 手动订阅班级(服务端二次校验权限) |
房间规则(@classhelper/shared 的 SOCKET_ROOMS):
class:{classId}、user:{userId}、role:{role}、students、teachers。
客户端断线由 Socket.IO 自动重连(Web 端 store 与后续 EXE 端共用同一套事件名)。
权限模型(RBAC)
| 角色 | 可见范围 | 写权限 |
|---|---|---|
ADMIN |
全部班级 | 全部 |
TEACHER |
自己创建(Class.teacherId)或被分配(ClassTeacher)的班级 |
仅上述班级 |
STUDENT |
仅自己所在班级(User.classId + Enrollment) |
无(仅能标记自己的作业状态 / 已读通知) |
实现要点:
src/middleware/auth.ts:JWT 校验后会回查数据库,班级/角色变更立即生效。src/lib/access.ts:集中实现assertClassAccess/assertClassWritable/resolveClassScope, 列表接口通过 scope 收敛数据(classId in [...]),单条接口直接断言。- 越权请求统一返回
403,未登录返回401(验收脚本已覆盖 10 项越权场景)。
模块化设计(可随时增删模块)
后端每个功能是一个独立目录,统一契约 ApiModule = { name, basePath, router, enabled? }:
// packages/server/src/modules/registry.ts
export const apiModules: ApiModule[] = [
authModule,
classesModule,
coursesModule,
schedulesModule,
homeworksModule,
notificationsModule,
gradesModule,
studentsModule,
teachersModule, // ← 新增模块只需在这里加一行
dashboardModule,
].filter((module) => module.enabled !== false);
- 新增模块:新建
modules/<name>/(*.schemas.ts校验、*.service.ts业务、*.module.ts路由), 在 registry 里加一行即可,app.ts无需改动。 - 移除/下线模块:删掉对应行,或把
enabled置为false,其接口立即从路由表消失。 - 实时推送解耦:业务模块只调用
realtime/bus.ts的emitToClass/emitToUser, 不直接依赖 Socket.IO 实例,因此「移除实时通道」不影响业务代码。 - 共享契约:所有 DTO 与类型集中在
@classhelper/shared,三端共用,避免契约漂移。
数据库
- 默认:SQLite(Prisma 7 的 libSQL driver adapter,数据文件
packages/server/prisma/dev.db)。 - 模型设计为跨库通用(无 enum、无
@db.*原生类型),切换到 MySQL 只需改 provider + 连接串。 - 完整步骤见
docs/mysql.md:
pnpm db:switch:mysql
pnpm --filter @classhelper/server add @prisma/adapter-mariadb
# 修改 packages/server/.env 的 DATABASE_PROVIDER / DATABASE_URL
pnpm db:migrate && pnpm db:seed
数据库表:User / Class / ClassTeacher / Enrollment / Course / Schedule /
Homework / HomeworkStatus / Notification / NotificationRead / Grade(共 11 张)。
验收标准对照
| 验收项 | 结果 | 证据 |
|---|---|---|
| 教师 Web 端发布通知,学生端 5 秒内收到 | ✅ | verify:e2e:notification:new 37–44ms |
| 教师发布作业,学生能查看并标记完成 | ✅ | homework:new 26–40ms,PATCH /homeworks/:id/status 200 且列表回显 completed=true |
| 教师录入成绩,学生能查看个人成绩 | ✅ | grade:updated 21–25ms,/grades/my 返回记录;批量录入与统计接口通过 |
| 学生能查看课表,支持按周切换 | ✅ | /schedules/grid?week=1 返回 30 节,week 过滤 weekStart ≤ week ≤ weekEnd |
| 断网后客户端可查看缓存数据 | ✅ | 客户端冒烟:断网时回退到本地缓存 → fromCache=true items=1;离线横幅 + 缓存统计页可用 |
| 权限隔离:学生不能访问其他班级数据 | ✅ | 10 项越权断言全部 403/401(学生跨班/跨班作业/跨班课表、教师跨班发布、未登录访问…) |
| 上课时段发布紧急通知必须二次确认 | ✅ | 服务端 409 URGENT_DURING_CLASS(confirmDuringClass 后 201);Web 端全屏警告 + 3 秒倒计时 |
| 客户端灵动岛:上课隐藏 / 下课弹出 / 紧急立即展开 | ✅ | verify:desktop 四种状态断言 + 截图(.cache/island-shots/) |
| 能成功打包 Windows EXE | ✅ | 班级小助手-0.1.0-x64-setup.exe / -portable.exe / win-unpacked/*.exe(见下表) |
| 提供完整 README(启动、构建、打包、默认账号) | ✅ | 本文档含快速开始、命令表、API、WebSocket、RBAC、模块化、MySQL 切换、打包与常见问题 |
打包产物验证(对最终 EXE 实测,非仅开发产物):
| 产物 | 大小 | 冒烟结果 |
|---|---|---|
| 服务端安装程序(内置 Node + Web 端) | 34MB | ✅ 静默安装(升级保留 .env 与数据库)→ 自动建库建号 → 服务就绪 → 对安装实例跑 62/62 e2e + 8/8 UI |
release/win-unpacked/班级小助手.exe |
234.7MB | ✅ 16/16,packaged: true,退出码 0 |
release/…-x64-portable.exe(单文件) |
~107MB | ✅(含联网集成:登录王小明 / Socket.IO 已连接 / 四类数据) |
release/…-x64-setup.exe(客户端) |
~107MB | ✅ 构建成功,已嵌入自定义图标;包含灵动岛 |
当前实测:
| 验证 | 结果 |
|---|---|
pnpm verify:e2e(开发环境与安装后的生产实例各跑一次) |
62/62 ✅ |
pnpm verify:web(Web 管理端真实点击 + 上课时段紧急通知 + PWA) |
8/8 ✅ |
pnpm verify:desktop(客户端冒烟 + 灵动岛四态 + 像素级截图校验 + 离线回退) |
19/19 ✅ |
pnpm typecheck / pnpm lint / pnpm format:check |
全部通过 ✅ |
| 安装程序完整生命周期(静默安装 → 启停脚本 → 卸载) | 通过 ✅ |
| Docker / Nginx 部署样例 | 文件已提供,本机无 Docker 未实测 ⚠️ |
常见问题(本机环境已知坑)
- 在 DSH 桌面端(Electron 宿主)里执行 pnpm 时,依赖的
.bin不会被注入 PATH 现象:pnpm run <script>报'prisma' 不是内部或外部命令。 原因:本机 pnpm 由 Electron 进程承载,bin 目录位于node_modules/.pnpm/node_modules/.bin,未注入子进程 PATH。 规避:直接用 Node 调用 CLI 入口,例如node packages/server/node_modules/prisma/build/index.js generate、node packages/web-admin/node_modules/vite/bin/vite.js build。 在普通终端(非 Electron 宿主)中pnpm <script>一切正常。
1.5 脚本里启动 Electron 时必须清掉 ELECTRON_RUN_AS_NODE,并且不要 import 'electron' 取路径
现象(两种,都只在本机这种"宿主终端本身是 Electron"的环境里出现):
① node scripts/smoke.mjs 正常,但 pnpm verify:desktop 报
Cannot find module 'electron'(Electron 退化成纯 Node 运行);
② pnpm dist:win 报 Unknown argument: .../electron-builder/out/cli/cli.js。
原因:本机 pnpm 运行在 Electron 内置 Node 上,会给子进程带上 ELECTRON_RUN_AS_NODE=1;
同时 process.versions.electron 存在时,require('electron') 返回的是 API 对象而非可执行文件路径,
spawn(process.execPath, [cli.js, …]) 会被 Electron 当成"启动应用"而多出一个位置参数。
规避(已内置到代码里):
scripts/lib/electron-env.mjs:resolveElectronEnv()清理变量、resolveElectronExecutable()从node_modules/electron/path.txt定位 electron.exe;scripts/lib/node-runtime.mjs:resolveNodeRuntime()明确找一个真正的 node.exe, 供 electron-builder CLI 与"内置 Node 运行时"打包使用。
-
不要用 PowerShell 管道调用
start.cmd现象:& "$install\start.cmd" | Select-Object -First 20会一直不返回。 原因:脚本内启动的 node 进程在该上下文里继承了管道句柄。 规避:双击运行,或用Start-Process cmd.exe "/c start.cmd"(脚本自身 4 秒内退出)。 -
原生模块会按 Electron ABI 构建 同一原因会让
node-gyp/prebuild-install以runtime=electron为目标, 装出的.node在 Node 进程里无法加载。本项目因此改用 libSQL 适配器 (@libsql/win32-x64-msvc为 npm 预编译包,无需本地编译),彻底规避该问题。 -
本机 TLS 中间人证书导致依赖二进制下载失败 现象:
prebuild-install warn install unable to verify the first certificate。 规避:安装时设置NODE_OPTIONS=--use-system-ca(Node ≥ 22.15 支持), 并确保npm_config_cache指向可写目录(如仓库内.cache/npm-cache)。 -
受限沙箱下 Vite 构建报
spawn EPERMVite 在 Windows 上通过execFile解析真实路径,需要放开子进程管道限制后再构建。 -
Prisma 7 与旧版本差异
prisma的 npmlatest标签当前指向8.0.0-rc,本项目显式锁定 7.10.0; 生成器为prisma-client(输出到src/generated/prisma,已 gitignore), 连接串在prisma.config.ts中配置,且必须通过 driver adapter 接入数据库。 -
Electron 二进制下载慢或失败
electron的 postinstall 会从 GitHub 拉取约 151MB 发行包,本机实测 10 分钟仍未完成。 规避:安装前设置镜像与缓存目录(本项目已把 electron 缓存指向.cache/electron):ELECTRON_MIRROR=https://cdn.npmmirror.com/binaries/electron/。 打包阶段scripts/dist-win.mjs已内置该镜像,并优先复用本地已解压的 Electron。 -
electron-builder 的构建工具(NSIS / winCodeSign)下载慢 通过
ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/解决(已内置到dist-win.mjs),实测 nsis-3.0.4.1 与 nsis-resources 正常下载。scripts/dist-server.mjs会直接复用 electron-builder 缓存里的makensis。 -
应用图标
pnpm icons会用 Electron 渲染 SVG 生成packages/desktop-client/build/icon.ico(多尺寸)、 PWA 所需的 PNG 与favicon.svg。electron-builder 与 NSIS 安装程序都会自动使用该图标。 -
服务端安装包内的
.cmd脚本为什么是英文提示? cmd.exe 按 OEM 代码页(GBK)解析脚本文件,UTF-8 中文会产生乱码甚至语法错误, 因此脚本提示统一用 ASCII,中文说明放在安装向导与README.txt中。
交付清单(阶段 7 · 生产化)
| 路径 | 说明 |
|---|---|
scripts/dist-server.mjs |
服务端 + Web 端打包(内置 Node、依赖、迁移、启停脚本、安装程序) |
scripts/nsis/server-installer.nsi |
NSIS 安装程序模板(安装/快捷方式/开机自启/卸载保留数据) |
scripts/generate-icons.mjs + scripts/icons/ |
图标生成(Electron 渲染 SVG → PNG/ICO) |
scripts/ui-smoke/ |
Web 管理端 UI 真实点击回归测试 |
packages/server/src/middleware/security.ts |
helmet + 限流(通用/登录)+ 请求耗时日志 |
packages/server/src/lib/web-static.ts |
Web 管理端静态托管 + SPA 回退 + 缓存策略 |
packages/server/src/lib/db-bootstrap.ts |
首启动自动迁移 + 自动创建管理员 |
packages/web-admin/public/{manifest,sw.js} |
PWA:可安装为应用 + 离线外壳 |
deploy/{Dockerfile,docker-compose.yml,nginx.conf} |
云部署与 HTTPS 反代样例 |
docs/production.md |
生产部署指南(三种形态 + 运维 + 安全清单 + 故障排查) |
阶段 8(灵动岛 + 上课时段策略)新增文件:
| 路径 | 说明 |
|---|---|
packages/desktop-client/src/main/island.ts |
灵动岛控制器(窗口/状态机/动画/超时/IPC) |
packages/desktop-client/src/preload/island.ts |
灵动岛 preload 桥(islandGetState/islandPush/islandSetClassState) |
packages/desktop-client/src/island/IslandApp.vue |
灵动岛界面(胶囊 / 详情 / 紧急三种形态 + 动画) |
packages/desktop-client/src/renderer/island/bridge.ts |
上课状态轮询(10s)与通知转发 |
packages/server/src/modules/schedules/schedules.service.ts |
computeClassStatus / getClassStatus(上课时段判定) |
packages/web-admin/src/components/UrgentClassWarning.vue |
上课时段发布紧急通知的全屏二次确认(3 秒倒计时) |
scripts/ui-smoke/live-probe.mjs |
UI 回归测试的"真实上课时段"探针(自建课表 + 用后清理) |
scripts/lib/{electron-env,node-runtime}.mjs |
Electron/Node 运行时定位与宿主环境兼容处理 |
docs/screenshots/island/*.png |
灵动岛四种形态的自动化留档截图(由冒烟验证生成,含像素级断言) |
后续可选增强
- 代码签名证书(消除 SmartScreen 提示)。
- 客户端"提交类操作离线队列"(当前离线为只读,联网后自动同步读取的数据)。
- Socket.IO 多实例广播(引入
@socket.io/redis-adapter)与 CI 流水线。 - 灵动岛位置/尺寸可配置(多显示器时跟随当前活动屏幕,而非固定主屏)。



