Files
ClassHelper/docs/dev/architecture.md
T
LoveChengke 19c2aa07b2 feat(docs,scripts,build): 用 docfx 搭文档站并发布到 GitHub Pages,版本号收口成脚本迭代
文档站(`docs/` 就是 docfx 项目,发布在 https://lovechengke.github.io/ClassHelper/):

- 四章 + 常见问题 + 完整参考,共 44 页:快速上手(8 页)/ 应用帮助(10 页)/
  管理运维(6 页)/ 开发文档(11 页)。信息架构照 ClassIsland 文档站来:
  顶栏分章、左侧章节导航、右侧本页目录、上一页 / 下一页、站内搜索、深色主题跟随系统;
- 自定义模板覆盖(`templates/classhelper/`):把 docfx modern 模板那套英文 UI 文案
  (In this article / Improve this doc / Next…)换成中文,补 GitHub 入口与版本徽标,
  并加一层只调排版与配色的 `public/main.css`(不动 DOM 结构);
- 站点顶栏的版本徽标取自根 `package.json`(`docs/build.mjs` 用 `--metadata` 注入),
  不在文档里写第二份 —— 否则一定漂;
- 404 页单独生成:GitHub Pages 的项目页挂在 `/<仓库名>/` 子路径下,而浏览器是按**被请求的路径**
  解析页面里的相对链接的,所以 `<base>` 必须钉到站点根,由 `build.mjs`(本地 `./`)与
  workflow(CI 传 `/<仓库名>/`)分别注入;
- 构建前清空 `_site`:docfx 只覆盖不清理,删掉或改名的页面会以旧文件留在目录里,
  而 Pages 上传的是整个目录。

版本迭代(**每一次 releases,不是每一次提交**):

- 新增 `scripts/version.mjs`。这个仓库里同一个版本号散在 **10 个文件**里(5 份 `package.json`、
  `deploy/package.runtime.json`、两个 compose 的镜像 tag、插件 `manifest.yml`、以及插件 C# 里的
  `PluginVersion` 常量),手工改必然漏一处,而漏掉的那处**不会报错**,只在某个不常走的分支上
  表现为"版本号对不上"。现在收口成两条命令:`pnpm version:check`(逐处比对,不一致 exit 1)
  与 `pnpm version:bump patch|minor|major`(一次改写全部 10 处,任何一处匹配不到或匹配到多处
  就直接失败退出,不做尽力而为的部分改写);
- `AGENTS.md` 新增 §4.4「版本迭代(每一次 releases,不是每一次提交)」:写清版本号落在哪 10 处、
  完整发布命令序列(体检 → 验收 → `dist:all` → 哈希清单 → 推 master → `gh release create` →
  Linux 包由 CI 合并哈希行),以及"已发布的版本号不要复用"的理由。

其它:

- `.github/workflows/docs.yml`:推 master(`docs/**` / `package.json` / workflow 自身有变)即构建并
  部署到 Pages;构建带 `--check`,把 docfx 的警告当错误 —— 最常见的警告是"某个 `.md` 没被任何
  `toc.yml` 引用",那种页面有内容但没有侧边栏,属于必须修掉的疏漏;
- docs 目录重组后的引用同步:`production` / `mysql` / `linux-deploy` / `winisland-design-tokens`
  移到新位置后,AGENTS.md、`deploy/`、`scripts/`、客户端源码注释里的路径引用一并更新;
- 文档站只发布 `docs/` 这一棵树,页面里指向树外的相对链接(`../AGENTS.md` 等)改成 GitHub 绝对 URL,
  否则上线全是 404;
- 图标派生清单补上文档站模板里的 `logo.png` 与 `favicon.png`(`pnpm icons` 一并生成,
  仍然只有一个图形源 `build/classhelper.{png,ico}`);
- `docs/_site` 进 `.gitignore` / `.prettierignore` 与 ESLint ignores;
- 根脚本补 `version:check` / `version:bump` / `docs:build` / `docs:serve` / `docs:check`。
2026-10-06 16:27:44 +08:00

12 KiB
Raw Blame History

title, description
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 + 图标)
│   ├── 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

模块化设计

后端每个功能是一个独立目录,统一契约:

// 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}
前端不写死颜色 深色主题靠语义令牌,写死的颜色在深色下会露馅

本地开发

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

常用命令与端口见安装服务端;验收脚本见验收与测试。