面向单校部署的开源作业收取系统(Web + PWA + Server,无原生 App)。教师建班/建作业,学生持唯一二维码,教师/课代表手机扫码登记纸质作业,WebSocket 实时统计已交/未交。
所有命令在仓库根目录运行。
pnpm install # 安装依赖(workspace)
pnpm dev # 本地开发:前端 5173 + 后端 3000
pnpm build # 全量构建
pnpm typecheck # 全量类型检查
pnpm lint # lint(当前为占位 echo)
pnpm --filter @handyin/server prisma:generate # 生成 Prisma Client
pnpm --filter @handyin/server prisma:push # 同步数据库结构(dev 用 db push,无 migrations)
pnpm --filter @handyin/server test # 跑测试(见下方说明)- 框架:vitest + fastify.inject(无需起端口),测试文件在
apps/server/test/。 - vitest 按
projects拆分:单元测试test/unit/(纯函数,无 DB,本地可跑pnpm --filter @handyin/server test:unit);集成测试test/根目录(依赖 PostgreSQL)。 - 本地无 Docker,集成测试依赖 PostgreSQL,只能在服务器容器内跑;本地只能跑单元测试
test:unit、typecheck、build。 - 服务器跑测试(跑全部,含集成):
ssh aliyun 'cd ~/HandyIn && sudo docker compose -f docker-compose.test.yml up --build --abort-on-container-exit test'- 包管理:pnpm workspace(monorepo)。根
package.json定义了@prisma/client、prisma为根 devDependencies。 apps/web:React 19 + Vite 6 + TypeScript + Tailwind CSS 4 + PWA(vite-plugin-pwa)+ react-router-dom 7 + ZXing(扫码)+ qrcode.reactapps/server:Fastify 5 + Prisma 6 + TypeScript(ESM)+ @node-rs/argon2 + Zod + @fastify/*(cookie/cors/multipart/rate-limit/static/websocket)packages/types:共享 TS 类型与常量(DEPARTMENT_CODES、ROLES、DTO)packages/validation:Zod schema 与学号解析(parseStudentNumber/buildStudentNumber)prisma/schema.prisma:数据模型(见 PROJECT_STATUS.md)
后端 apps/server/src 结构:index.ts(组装 App,导出 buildApp(config))、config.ts(loadConfig)、prisma.ts(全局单例)、seed.ts(初始教师)、errors.ts、plugins/(auth、static)、routes/(auth/users/classes/students/assignments/submissions/reps/ws)、lib/(session/password/qrcode/import/stats/realtime/permissions)。
-
子路径部署:整站走
/handyin前缀,Caddy 只按路径转发、不剥离前缀。四处必须一致:Vitebase(vite.config.ts)、react-routerbasename(App.tsx)、API prefix(index.ts的apiPrefix)、API_BASE(web/src/lib/api.ts)。改HANDYIN_BASE_PATH(.env)全站生效。 -
Fastify 5 的 hook 必须是 async 或调用 done 回调。同步 preHandler 在正常路径(不 throw)返回 undefined 会导致请求永久挂起(不报错、无响应)。本项目
requireAuth/requireTeacher/requireCollector/requireAssignmentCollector均已改为async。新增权限 hook 时必须用async。 -
Prisma binaryTargets 必须显式声明:
prisma/schema.prisma里binaryTargets = ["native", "debian-openssl-3.0.x"]。原因:node:22-slim构建阶段未装 libssl,Prisma 的native检测会 fallback 到debian-openssl-1.1.x,与运行阶段(装了 openssl/libssl3)不匹配,导致 query engine 找不到。 -
国内 npm 镜像:Dockerfile 内
ARG NPM_REGISTRY=https://registry.npmmirror.com+ENV npm_config_registry,docker-compose.yml通过${NPM_REGISTRY}传入。不要在 Dockerfile 里写死 registry.npmjs.org。 -
自动建表:
Dockerfile运行阶段 CMD 为sh -c "node_modules/.bin/prisma db push --schema prisma/schema.prisma --skip-generate && node apps/server/dist/index.js"。项目无 migrations 目录,统一用prisma db push,启动时幂等自检。 -
Session 存内存 Map(
lib/session.ts),不进数据库;服务重启即失效。若需多实例/持久化需改造。 -
密码用 Argon2id(
@node-rs/argon2),参数见lib/password.ts;不存明文。 -
学号规则:
YYYYDDCCNN(入学年 4 位 + 学部 2 位 + 班级 2 位 + 班内学号 2 位),学部码01=高中部 02=初中部 03=小学部。学生studentNumber全局 UNIQUE。学生二维码存随机qrToken(handyin://student/<hex>),不直接用学号。 -
权限分层:身份(登录)→ 角色(ADMIN/TEACHER/REPRESENTATIVE)→ 班级归属(
assertClassMember)→ 作业归属(requireAssignmentCollector)。requireTeacher放宽为「TEACHER 或 ADMIN」;requireAdmin仅管理员(用户管理、班级建/改/删/批量)。教师需先加入班级(TeacherClass)才能操作该班学生/作业;课代表按作业授权(AssignmentRep),可过期。 -
错误处理:
errors.ts的AppError,FastifysetErrorHandler统一转 JSON。登录失败统一文案,不泄露用户是否存在。
- Docker Compose(
docker-compose.yml):postgres(16-alpine,volumepgdata)+server(多阶段构建镜像)+caddy(2-alpine,映射 80/443)。 - 配置:
.env(从.env.example复制,必填POSTGRES_PASSWORD、SESSION_SECRET、INIT_ADMIN_PASSWORD)。 - 启动:
docker compose up -d --build;查看:docker compose logs -f server。 - 服务器信息、访问方式见 PROJECT_STATUS.md。
- 仓库:
git@github.com:Lan-Fa/HandyIn.git,分支main。 - 本环境
git push被权限规则 deny,需用户手动git push origin main;push 前先完成 commit。 - 服务器通过 SSH clone(
~/HandyIn),日常新增改动以「本地 commit → 用户 push → 服务器git pull+docker compose up -d --build」同步。不要直接在服务器改 git 跟踪文件(会造成 pull 冲突)。 - commit 风格:Conventional Commits,如
fix(server): ...、feat(deploy): ...、test(server): ...。