## 顶部
两层引用块套着一段「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
169 lines
7.7 KiB
Markdown
169 lines
7.7 KiB
Markdown
# 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,
|
||
Vite(Rolldown 内核)、Naive UI、Pinia、Vue Router。
|
||
|
||
**要兼容机房的老 Chrome(< 94)**:`vite.config.ts` 的 legacy 配置与
|
||
`mermaid-legacy` 等 fallback 依赖不能动,理由写在该文件的注释里。
|
||
|
||
## Commands
|
||
|
||
前端一般不单独起,`OJ2/` 根目录 `bun run dev` 会把 api + worker + web 一起拉起来。
|
||
只跑前端或要验证时:
|
||
|
||
```bash
|
||
bun run dev # 只起前端 dev server(5173),后端得另外起
|
||
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.json`(0.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.vue`、`problem/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()` 的那三条。留着它们的理由是**别抛错**,不是校验:
|
||
|
||
```ts
|
||
// 原来是 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 后端也在 `parse`**(`submissionDetailSchema` /
|
||
`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`.
|
||
|
||
## Related Repository
|
||
|
||
后端就在同一个仓库的 `../api`(Bun + Hono + Drizzle,编译成单二进制),
|
||
契约在 `../../packages/contract`。**不要再去看 `OnlineJudge/`** —— 那是已下线的
|
||
Django 后端,只作参照、完全冻结。详见 `../CLAUDE.md`。
|