Files
OJ2/apps/web/CLAUDE.md
yuetsh b9a80d62bc docs(CLAUDE.md): 合并顶部叠着的更正块,订正前端验证方式,补上契约收紧的边界
## 顶部

两层引用块套着一段「2026-09-10 更正」,说的是同一件事的两个版本(7 张表已删 →
其实还剩一张)。合成一段现状:旧栈不可逆下线、唯一退路是备份恢复、漏网那张
django_migrations 由 0014 补删。考古过程留在迁移文件的注释里,这里不重复。27 行 → 14 行。

## 常用检查

`cd apps/web && bun run build` 后面那句「vite 不做类型检查,构建即验证」是错的:
vite 确实不做类型检查,但**构建也不是验证**。补上 `bun run type-check`,并写明两条
会静默通过的假路子 —— `vue-tsc --noEmit -p tsconfig.json` 检查 0 个文件(那个
tsconfig 是 files: [] + references 的壳,0.2 秒跑完就是信号)、`vite build` 不看类型。
后端也改成 `bun run --filter '@oj2/api' typecheck`(脚本本来就有)。

## 新增「契约收紧要挑地方」

前一个 commit 的教训值得留在这儿:契约 schema 后端也在读路径上 parse,收紧字段
等于给历史数据加闸,对不上要么 500(exerciseSchema)、要么静默塌成 {}(info,
9163/124192 条)。JSONB 原文的形状真相在写入侧,闸就设在那里;要收紧先拿生产备份
跑全量,重点看空值不是键集合。AST 规则的 astRulesError() 本来就是同一个道理。

## apps/web/CLAUDE.md

Commands 段还是 ojnext 时代的 npm start / npm fmt,全部换成 bun 并补上 type-check
的坑。Module Pattern 写的 `views/` 这一层实际不存在(页面组件直接放模块根下),
api.ts 也不按模块分(学生端 oj/api.ts、后台 admin/api.ts、跨端 shared/api.ts)。

工作区根目录的 CLAUDE.md / AGENTS.md 同样折叠了那段更正、订正了类型检查命令
(那两份不在 git 里)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
2026-09-10 04:57:34 -06:00

7.7 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

OJ2 的前端OJ2/apps/web),代码从上一代 ojnext/ 原样搬来、只替换了 API 层 ojnext../OnlineJudge 都已下线且完全冻结,一行都不改。Vue 3 + TypeScript ViteRolldown 内核、Naive UI、Pinia、Vue Router。

要兼容机房的老 Chrome< 94vite.config.ts 的 legacy 配置与 mermaid-legacy 等 fallback 依赖不能动,理由写在该文件的注释里。

Commands

前端一般不单独起,OJ2/ 根目录 bun run dev 会把 api + worker + web 一起拉起来。 只跑前端或要验证时:

bun run dev            # 只起前端 dev server5173后端得另外起
bun run type-check     # 类型检查。改完 .vue / .ts 必须跑这个
bun run build          # 生产构建
bun run fmt            # Prettier

⚠️ 验证只认 bun run type-check vue-tsc --noEmit -p tsconfig.json静默 通过——那个 tsconfig 是 files: [] + references 的壳,真正的配置在 tsconfig.app.json0.2 秒跑完就是没在检查的信号);vite build 也不做类型检查。

不写测试沿用项目约定验证靠实跑。lint 只有 Prettier。

Architecture

Directory Structure

src/
├── shared/        # Cross-cutting concerns: layout, stores, composables, API
├── oj/            # User-facing features (problems, submissions, contests, etc.)
├── admin/         # Admin panel features
├── utils/         # Constants, types, HTTP client, helpers
├── routes.ts      # Route definitions (two top-level: ojs, admins)
├── main.ts        # App entry point
└── App.vue        # Root component with Naive UI theme setup

Module Pattern

Each feature module (under oj/ or admin/) typically has:

  • 页面组件直接放模块根下(problem/list.vueproblem/detail.vue没有 views/ 这一层
  • components/ — feature-specific components
  • composables/ / utils/ — 模块自己的组合式函数与纯函数(按需,不是每个模块都有)

API 调用不按模块分:学生端全在 oj/api.ts、后台全在 admin/api.ts、 跨端的(登录、资料、标签、验证码)在 shared/api.ts

Shared logic lives in shared/:

  • store/ — Pinia stores: user (auth/roles), config (site-wide settings), authModal (login/signup form state), screenMode (problem split-screen layout), loginSummary (AI activity summary), collab (help-request queue + collab room)
  • composables/pagination (URL-synced), websocket (reconnect + heartbeat), collabDoc (Yjs binding for the collab channel), configUpdate (WS-pushed config sync), useMermaid (lazy Mermaid render), breakpoints, maxkb
  • layout/default.vue and admin.vue layout wrappers
  • api.ts — shared API calls (auth, profile, tags, captcha)

Auto-Imports

Configured via unplugin-auto-import and unplugin-vue-components. You do not need to manually import:

  • Vue APIs (ref, computed, watch, etc.)
  • Vue Router (useRouter, useRoute)
  • Pinia (defineStore, storeToRefs)
  • VueUse composables
  • Naive UI composables (useDialog, useMessage, useNotification, useLoadingBar)
  • Naive UI components (all N* components)
  • Naive UI types (DataTableColumn, FormRules, FormItemRule, SelectOption, UploadCustomRequestOptions, UploadFileInfo, MenuOption, DropdownOption)

Generated type declaration files: src/auto-imports.d.ts, src/components.d.ts.

Path Aliases

utils  →  ./src/utils
oj     →  ./src/oj
admin  →  ./src/admin
shared →  ./src/shared

HTTP Client

utils/api.ts — Axios instance with interceptors (baseURL: "/api", withCredentials). It unwraps both the axios envelope and the backend's { data } envelope, so callers get the payload directly. All API calls proxy through the dev server (see vite.config.ts).

Contract guard (utils/contract.ts)

@oj2/contract 的 zod schema 是前后端唯一的形状来源utils/types.ts 只做 z.infer 派生与少量前端专有的收窄(都写了理由)。

运行时闸门只挂三处:题目详情、提交详情、shared/api.ts 的用户资料 —— 原本就写了 .parse() 的那三条。留着它们的理由是别抛错,不是校验:

// 原来是 problemDetailSchema.parse(v) as Problem —— `as` 让校验白做,
// 而 parse 抛错会让整个题目页白屏
return contract("GET /problems/:id", problemDetailSchema, value)

失败时记一条控制台日志再放行原始数据,页面照常渲染。

不要把它铺到更多端点上。 试过一次41 个),收益是 41 次 safeParse 加一条 没人读的 console.error前后端同仓、共享同一份 schema「后端改字段前端不知道」 tsc 已经抓了。

什么该收紧,什么不该

JSONB 原文(submission.info / statistic_info / exercise.data)不在读出侧 校验。 它们的形状真相在写入侧 —— 判题机、services/exercise.ts。在读出侧再收 一遍的结果实测过两次:

  • info 按采样键集收紧后124192 条提交里 9163 条RE、TLE、MLE 全中)对不上, 被 union 的空对象分支静默剥成 {},管理员的测试点表格无声消失;
  • exercise.data 按题型收紧后,后端读路径(routes/content.ts 硬 parse变成 一道闸,一行脏数据能让整条练习列表 500。

所以:同一个 schema 后端也在 parsesubmissionDetailSchema / exerciseSchema / contestRankItemSchema 都是),收紧任何字段之前,拿根目录 那份生产备份把全量数据跑一遍,尤其要看空值而不只是键集合。

Key Utilities

  • utils/constants.ts — Judge status codes, language IDs, difficulty levels, contest types
  • utils/types.ts — 契约类型的派生与前端专有收窄(不是手写的一份平行类型)
  • utils/contract.ts — 运行时契约闸门,见上
  • utils/judge.ts — Judge-related utilities
  • utils/renders.ts — Table column render helpers for Naive UI DataTable

Environment Variables

Variables prefixed with PUBLIC_ are injected at build time. Env files: .env, .env.staging, .env.test.

Variable Purpose
PUBLIC_OJ_URL Backend REST API base URL
PUBLIC_WS_URL WebSocket server URL
PUBLIC_ENV Environment name (dev/staging/production)
PUBLIC_CODE_URL Code execution service
PUBLIC_JUDGE0_URL Judge0 API
PUBLIC_MAXKB_URL Knowledge base service
PUBLIC_ICONIFY_URL Iconify icon CDN

Routing

Routes are defined in src/routes.ts with two root routes: ojs (user-facing) and admins (admin panel). Route meta fields used:

  • requiresAuth — redirect to login if not authenticated
  • requiresSuperAdmin — super admin only
  • requiresProblemPermission — problem management access

Real-time Features

  • WebSocket via composable in shared/composables/ for submission status updates
  • Yjs over the /ws/collab channel for classroom help requests and collaborative code editing (students raise a hand, teachers join their editor). The server is a dumb relay — it authenticates, assigns rooms, and forwards frames without parsing them. See docs/specs/2026-08-28-collab-help-request-design.md.

后端就在同一个仓库的 ../apiBun + Hono + Drizzle编译成单二进制 契约在 ../../packages/contract不要再去看 OnlineJudge/ —— 那是已下线的 Django 后端,只作参照、完全冻结。详见 ../CLAUDE.md