Files
ClassHelper/docs/dev/architecture.md
T
LoveChengke 0ea6bd887f feat(website,docs,build): 官网加一个下载页(挑版本装机),文档站的照片换成与 README 同一份
下载页(`website/download.html` + `assets/download.js`)

- 官网首页的「立即下载」、顶栏与页脚的「取安装包」全部指向新页 `/download.html`。
  那一页是「先挑一个版本,再挑要装的那一份」:一个版本分段控件 + 四张卡
  (班级端 / 服务端 Windows / 服务端 Linux / ClassIsland 插件),每张卡列出这一版里
  真实存在的文件、体积与直链;
- 版本与文件清单**不在站点里存副本**,每次打开都由 `assets/download.js` 当场读 GitHub
  Releases —— 文件名、体积、链接照抄发布页,不靠命名约定去猜;
- 读不到时(离线,或匿名接口 60 次/小时按出口 IP 被限流)退回页面上那份
  `<meta name="ch-version">`,按发布命名约定拼直链,并在页面上**明说这是拼出来的**;
- 老版本缺哪个包就显示「这一版没有发布…」(`v0.1.0-fix` 只有插件包,实测正确);
  Linux 那一行安装命令跟着选中版本钉上 `--version`(tag 不是 `x.y.z` 时不带);
- 动态建出来的卡片与文件行派发 `ch:content`,由 `main.js` 补挂按压反馈
  —— 这些元素是拉到清单之后才出现的,启动时还看不到它们。

文档站的照片(`docs/images/`)

- 页面里的图一律从 `docs/screenshots/**` 换成 `docs/images/**`:后者就是 README 与官网
  吃的那一份(字节相同),前者是 9-26 的冒烟留档,比 README 落后一个版本;
- 展示图与回归留档这两套图的分工写进 `docs/dev/testing.md`「截图留档」,含重新采集命令
  (冒烟会把「岛外留白对照图」落在同一个目录,所以要先落临时目录再挑);
- 顺带修 README 手机截图那张标错的图注(`mobile-dashboard.jpg` 是仪表盘,不是抽屉菜单);
- `docs/reference.md` 验收表里那张 ClassIsland「真机实测截图」不再嵌图:它是连着桌面、
  任务栏和另一个窗口整屏截下来的,当文档配图不能用。断言与用例名都保留,另外注明了抓图位置。

版本落点

- `scripts/version.mjs` 增加 `website/download.html` 的兜底版本号,落点 13 → 14 处
  (`AGENTS.md` §4.4 与 `docs/dev/release.md` 同步);
- README「开始使用」表里那三个文件名改成**发布页上真实的名字**
  (`-x64-client-setup` / `-x64-client-portable` / `-x64-server-setup`)—— 原来写的是构建
  产物的名字;「上传前把产物改成 ASCII 名」这一步也一并写进发布流程,
  它是官网下载页离线兜底与 README 表格的共同依据。
2026-10-06 18:37:42 +08:00

162 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 架构总览
description: 目录结构、技术栈、模块化设计与几条贯穿全局的工程约定。
---
# 架构总览
## 技术栈
| 层 | 选型 | 版本(`package.json` 实际声明) |
| ------ | -------------------------------------------------------------------- | ---------------------------------------------------------- |
| 包管理 | pnpm workspace | 11.8.0 |
| 语言 | TypeScript | 5.9.3(`typescript-eslint` 要求 < 6.1,因此未用 7.x) |
| 后端 | Node.js + Express | ≥ 20.19 / 5.2 |
| ORM | Prisma + driver adapter | 7.10(+ `@prisma/adapter-libsql`,切 MySQL 用 `-mariadb`) |
| 实时 | Socket.IO | 4.8 |
| 认证 | jsonwebtoken + bcryptjs | 9.0 / 3.0 |
| 校验 | zod | 4.6 |
| Web 端 | Vue + Vite + Element Plus + Pinia + Vue Router + Axios | 3.5 / 8.3 / 2.14 / 4.0 / 5.3 / 1.20 |
| 客户端 | Electron + Vue + Element Plus + Pinia + Socket.IO Client + IndexedDB | 44.3 / 3.5 / 2.14 / 4.0 / 4.8 |
| 插件 | .NET 8 / C# + Avalonia(ClassIsland 插件 API 2.0) | net8.0 |
| 打包 | electron-builder(nsis / portable)+ NSIS | 26.15 |
| 文档站 | docfx | 2.81 |
## 目录结构
```
class-helper/
├── package.json # 根脚本(dev / build / db:* / verify:* / dist:* / docs:* / icons)
├── pnpm-workspace.yaml # workspace 定义 + allowBuilds(pnpm 11 依赖构建白名单)
├── tsconfig.base.json # 共享 TS 基础配置(ESM + NodeNext + strict)
├── eslint.config.mjs # ESLint 扁平配置(TS + Vue)
├── .prettierrc.json # 单引号 / 110 列 / LF / 尾逗号
├── build/classhelper.{png,ico} # **图标源**(设计导出、手工放入);pnpm icons 从它派生其余全部图标
├── scripts/
│ ├── use-database.mjs # SQLite ⇄ MySQL provider 切换助手
│ ├── generate-icons.mjs # 启动 Electron 跑 icons/render.cjs(= pnpm icons)
│ ├── icons/render.cjs # 把 classhelper.png 重采样成各端图标(CommonJS)
│ ├── version.mjs # 版本号单一来源的读写与一致性校验(= pnpm version:*)
│ ├── dist-server.mjs # 服务端 + Web 管理端打包(免安装目录 + NSIS 安装程序)
│ ├── verify-packaged.mjs # 对打包后 / 已安装的客户端 EXE 复跑冒烟
│ ├── lib/ # electron-env / node-runtime / smoke-class
│ ├── nsis/server-installer.nsi# 安装程序脚本模板(版本号由 dist-server 注入)
│ └── ui-smoke/ # Web 管理端 UI 真实点击回归(Electron 驱动)
├── deploy/ # 生产部署:Dockerfile / docker-compose{,.sqlite}.yml / nginx.conf
│ ├── install.sh # Linux 一键安装器(TUI;native/docker 双形态;自带 --check 体检)
│ ├── classhelper # 运维命令本体(软链 /usr/local/bin/classhelper)
│ ├── verify-linux.sh # Linux 安装/改密/备份/升级/回滚验收(自统计 N/N 项)
│ ├── tools/admin-cli.mjs # 离线账号工具(改密、列账号、SQLite 一致性快照)
│ ├── classhelper.service # systemd 单元模板
│ └── package.runtime.json # 运行时依赖清单(安装包 / 容器 / Linux 部署共用)
├── docs/ # **文档站**(docfx 项目,发布到 GitHub Pages)
│ ├── docfx.json # docfx 配置
│ ├── toc.yml # 顶栏导航;各子目录各有一份 toc.yml 作为侧栏
│ ├── index.md # 首页
│ ├── get-started/ app/ management/ dev/ # 四个章节
│ ├── templates/classhelper/ # docfx 模板覆盖(_master.tmpl + public/main.css + 图标)
│ ├── images/ # 展示图:文档站 / README / 官网共用的同一份(1× 逻辑像素)
│ ├── screenshots/ # 回归测试留档的截图(island / client / web-mobile / classisland),页面不展示
│ ├── build.mjs # 构建入口(注入版本号与 404 的 base)
│ ├── serve.mjs # 本地预览(:5181)
│ └── _site/ # 构建产物(gitignore)
├── website/ # 产品官网(纯静态,无构建;node website/serve.mjs 本地预览)
│ └── assets/{styles.css,motion.js,main.js,icon.png,shots/}
└── packages/
├── shared/ # 三端共享:类型契约、常量、权限矩阵、动效令牌、工具函数
│ └── src/{types,constants,permissions,motion,island-layout,utils,index}.ts
├── server/ # 后端服务
│ ├── prisma/schema.prisma # 数据模型(14 个 model,只有一份,切库改 provider)
│ ├── prisma/migrations/ # 迁移历史(10 个)
│ ├── prisma/seed.ts # 种子数据(会清空业务表后重建演示数据)
│ ├── scripts/ # verify-e2e.mjs / verify-classisland.mjs / apply-column-migrations.cjs
│ └── src/
│ ├── app.ts # Express 装配(探针 + 限流 + 模块挂载 + 静态托管 + 兜底)
│ ├── index.ts # 启动入口(自检 + HTTP + Socket.IO + 优雅退出)
│ ├── config/env.ts # 环境变量校验(zod)+ 生产配置自检
│ ├── lib/ # access(RBAC) / snapshot(全库 JSON 快照) / db / db-bootstrap /
│ │ # session / web-static / http / jwt / logger / mappers / version …
│ ├── tools/apply-snapshot.ts # 跨库迁移的子进程入口
│ ├── middleware/ # auth / error / validate / security(helmet + 限流 + 耗时日志)
│ ├── realtime/ # socket.ts(房间)+ bus.ts(事件总线)
│ └── modules/ # 15 个功能模块 + registry.ts(模块注册表)
├── classisland-plugin/ # ClassIsland 联动插件(.NET 8 / C#,独立于 pnpm workspace)
│ ├── manifest.yml
│ ├── src/Plugin.cs
│ ├── src/Services/ # BridgeService / ScheduleMapper / ClassPlanWriter /
│ │ # ClassHelperNotificationProvider
│ ├── src/Views/ # BridgeSettingsPage.axaml(Avalonia 设置页)
│ └── src/Interop/ClassHelperClient.cs # HTTP 客户端 + 逐字段对齐服务端 zod 的 DTO
├── web-admin/ # Web 管理端(Vue 3 + Vite + Element Plus + PWA)
│ └── src/{api,stores,router,layouts,views,components,composables,styles}
└── desktop-client/ # 桌面客户端(Electron + Vue 3)
├── electron-builder.yml
├── scripts/{build-main,dev,smoke,dist-win,check-bundle-secrets}.mjs
└── src/
├── main/ # 主进程:窗口 / 单实例 / 配置持久化 / 托盘 / 灵动岛 / 冒烟
├── preload/ # contextBridge 安全桥(index.ts + island.ts)
├── island/ # 灵动岛渲染进程(独立透明置顶窗口)
├── types/desktop.d.ts # 主进程 ↔ 渲染进程契约
└── renderer/ # 渲染进程:api / stores / cache / router / views / island
```
## 模块化设计
后端每个功能是一个独立目录,统一契约:
```ts
// packages/server/src/modules/registry.ts(当前 15 个模块)
export const apiModules: ApiModule[] = [
authModule,
classesModule,
coursesModule,
schedulesModule,
homeworksModule,
notificationsModule,
callsModule,
importsModule,
integrationsModule,
gradesModule,
studentsModule,
teachersModule,
dashboardModule,
databaseModule, // 数据库管理(仅管理员)
updateModule, // 更新检查(服务端唯一出网请求)
].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`(改完要 `pnpm build:shared`),三端共用。
## 几条贯穿全局的硬约定
这些都是踩过坑之后固化下来的,改代码前值得先看(完整版在 `AGENTS.md`):
| 约定 | 为什么 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
| **统一响应与校验** | 用 `lib/http.ts` 的 `sendOk/sendCreated`,用 `middleware/validate.ts` 挂 zod,错误交给 `middleware/error.ts` |
| **权限判在服务层** | 路由只做角色过滤,班级维度的判断用 `lib/access.ts` 断言 —— 前端隐藏 ≠ 权限 |
| **周一为新一周的第一天** | 周次口径唯一:`resolveCurrentWeek(TERM_START_DATE, now, MAX_TERM_WEEK)` |
| **作业按 `assignDate` 归类** | 用 `createdAt`(UTC)会在晚上把作业算到第二天 |
| **新增实时事件要改三处** | 服务端 bus/socket、Web 端订阅、客户端订阅(灵动岛也依赖) |
| **跨桥接只传纯数据** | 传 Vue 的 `ref.value` 会在进入 preload 之前就抛 `could not be cloned` |
| **桌面端产物里不得有凭据** | asar 可直接解包;构建有 `check-bundle-secrets` 门禁 |
| **图标只有一个图形源** | 别改 `scripts/icons/` 的派生逻辑,换图标就换 `build/classhelper.{png,ico}` |
| **前端不写死颜色** | 深色主题靠语义令牌,写死的颜色在深色下会露馅 |
## 本地开发
```bash
pnpm install
cp packages/server/.env.example packages/server/.env # 至少改 JWT_SECRET
pnpm db:generate && pnpm --filter @classhelper/server db:deploy
pnpm build:shared # 必须先构建 shared,否则后续全部 ERR_MODULE_NOT_FOUND
pnpm db:seed # 演示数据(会清空业务表)
pnpm dev # shared watch + 后端 4000 + Web 端 5173
```
常用命令与端口见[安装服务端](../get-started/server.md);验收脚本见[验收与测试](testing.md)。