refactor(契约): 补上 AI / 站内信 / 自学留痕等接口的闸门,并订正前端 CLAUDE.md 的过期内容
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
闸门再补 13 个端点:AI 系列(detail / solved / duration / heatmap / login-summary / pinned)、站内信列表、教程列表与学习进度、题目逐年 AC、 流程图列表。学生端读接口至此基本覆盖(写操作与纯前端伪状态不接)。 文档订正(原文写着"后端是 ../OnlineJudge 的 Django 5"、"utils/http.ts"、 "utils/permissions.ts",这些都早已不存在): - 项目概述改成 OJ2 前端,并点明要兼容机房老 Chrome; - HTTP 客户端改成实际存在的 utils/api.ts; - 新增「Contract guard」一节,把**失败策略**(记日志 + 放行原始数据、不抛错) 和排查入口(window.__OJ2_CONTRACT_DRIFT__)写清楚,并提醒同一个 schema 后端也在 parse、收紧前要用生产数据核验; - Related Repository 指向同仓的 ../api 与 packages/contract。 验证:vue-tsc 与 vite build 通过;13 个新端点对真实接口全部通过 (11 通过 / 0 失败 / 0 跳过)。
This commit is contained in:
@@ -4,7 +4,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
**ojnext** is the frontend for an Online Judge platform. Built with Vue 3 + TypeScript using Vite (Rolldown-based bundler), Naive UI component library, Pinia for state management, and Vue Router.
|
||||
**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
|
||||
|
||||
@@ -70,15 +75,37 @@ shared → ./src/shared
|
||||
|
||||
### HTTP Client
|
||||
|
||||
`utils/http.ts` — Axios instance with interceptors. All API calls proxy through the dev server:
|
||||
- `/api` and `/public` → `PUBLIC_OJ_URL` (backend)
|
||||
- `/ws` → `PUBLIC_WS_URL` (WebSocket backend)
|
||||
`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` 派生与少量前端专有的收窄(都写了理由)。读接口应当走守卫:
|
||||
|
||||
```ts
|
||||
const endpoint = `problems/${encodeURIComponent(id)}`
|
||||
return contract("GET /problems/:id", problemDetailSchema, await api.get<unknown>(endpoint))
|
||||
```
|
||||
|
||||
**失败策略是「记日志 + 放行原始数据」,不抛错。** 形状对不上时:控制台打一条带
|
||||
端点和字段路径的记录、去重后记进 `window.__OJ2_CONTRACT_DRIFT__`、然后**返回原始
|
||||
数据让页面继续渲染**。面向学生的生产站点,少一个字段的代价远小于白屏。
|
||||
|
||||
排查线上分歧就是打开控制台敲 `window.__OJ2_CONTRACT_DRIFT__`;某条路径长期为空之后,
|
||||
那条路径可以升级成硬失败(直接 `schema.parse`),在那之前不要改。
|
||||
|
||||
改动 schema 时要记住**同一个 schema 后端也在 `parse`**(如
|
||||
`submissionDetailSchema.parse` 在路由里),所以收紧一个字段前先用生产数据核一遍,
|
||||
否则一条不符合的历史记录会让整个列表 500。
|
||||
|
||||
### Key Utilities
|
||||
|
||||
- `utils/constants.ts` — Judge status codes, language IDs, difficulty levels, contest types
|
||||
- `utils/types.ts` — TypeScript interfaces for all domain models
|
||||
- `utils/permissions.ts` — Permission check helpers
|
||||
- `utils/types.ts` — 契约类型的派生与前端专有收窄(不是手写的一份平行类型)
|
||||
- `utils/contract.ts` — 运行时契约闸门,见上
|
||||
- `utils/judge.ts` — Judge-related utilities
|
||||
- `utils/renders.ts` — Table column render helpers for Naive UI DataTable
|
||||
|
||||
@@ -113,4 +140,6 @@ Routes are defined in `src/routes.ts` with two root routes: `ojs` (user-facing)
|
||||
|
||||
## Related Repository
|
||||
|
||||
The backend is at `../OnlineJudge` — a Django 5 + DRF project. See its CLAUDE.md for backend details.
|
||||
后端就在同一个仓库的 `../api`(Bun + Hono + Drizzle,编译成单二进制),
|
||||
契约在 `../../packages/contract`。**不要再去看 `OnlineJudge/`** —— 那是已下线的
|
||||
Django 后端,只作参照、完全冻结。详见 `../CLAUDE.md`。
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import {
|
||||
type AiAnalysisRecord,
|
||||
type FormatCodeResponse,
|
||||
type TutorialSummary,
|
||||
type ClassComparisonResponse,
|
||||
type ClassRankItem,
|
||||
type ClassUserRank,
|
||||
type CreateSubmissionResponse,
|
||||
type ProblemAuthor,
|
||||
type YearlyAc,
|
||||
type CreateFlowchartResponse,
|
||||
type FlowchartSubmission,
|
||||
type AiDetail,
|
||||
type DurationData,
|
||||
type HeatmapItem,
|
||||
type LoginSummary,
|
||||
type SolvedList,
|
||||
problemDetailSchema,
|
||||
problemListSchema,
|
||||
problemListItemSchema,
|
||||
@@ -45,20 +37,28 @@ import {
|
||||
flowchartStatisticsSchema,
|
||||
problemSetProgressListSchema,
|
||||
problemSetProblemSchema,
|
||||
tutorialSummarySchema,
|
||||
tutorialProgressSchema,
|
||||
messageListSchema,
|
||||
yearlyAcSchema,
|
||||
aiDetailSchema,
|
||||
solvedListSchema,
|
||||
durationDataSchema,
|
||||
heatmapItemSchema,
|
||||
loginSummarySchema,
|
||||
aiAnalysisRecordSchema,
|
||||
} from "@oj2/contract"
|
||||
import api from "utils/api"
|
||||
import { contract } from "utils/contract"
|
||||
import { filterResult } from "oj/transforms"
|
||||
import type {
|
||||
Profile,
|
||||
Message,
|
||||
Exercise,
|
||||
Problem,
|
||||
ReactionKey,
|
||||
ReactionState,
|
||||
SubmissionListPayload,
|
||||
SubmitCodePayload,
|
||||
TutorialProgress,
|
||||
} from "utils/types"
|
||||
|
||||
/**
|
||||
@@ -384,11 +384,13 @@ export async function getAnnouncement(id: number) {
|
||||
)
|
||||
}
|
||||
|
||||
export function getMessageList(offset = 0, limit = 10) {
|
||||
// language 的收窄同 getSubmissions,见那里的说明
|
||||
return api.get<{ results: Message[]; total: number }>("messages", {
|
||||
params: { limit, offset },
|
||||
})
|
||||
export async function getMessageList(offset = 0, limit = 10) {
|
||||
const endpoint = "messages"
|
||||
return contract(
|
||||
"GET /messages",
|
||||
messageListSchema,
|
||||
await api.get<unknown>(endpoint, { params: { limit, offset } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function getReaction(problemID: number) {
|
||||
@@ -417,48 +419,85 @@ export async function getTutorial(id: number) {
|
||||
)
|
||||
}
|
||||
|
||||
export function getTutorials(type: "python" | "c") {
|
||||
return api.get<TutorialSummary[]>("tutorials", { params: { type } })
|
||||
export async function getTutorials(type: "python" | "c") {
|
||||
const endpoint = "tutorials"
|
||||
return contract(
|
||||
"GET /tutorials",
|
||||
tutorialSummarySchema.array(),
|
||||
await api.get<unknown>(endpoint, { params: { type } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAIDetailData(start: string, end: string, username?: string) {
|
||||
return api.get<AiDetail>("ai/detail", { params: { start, end, username } })
|
||||
export async function getAIDetailData(
|
||||
start: string,
|
||||
end: string,
|
||||
username?: string,
|
||||
) {
|
||||
const endpoint = "ai/detail"
|
||||
return contract(
|
||||
"GET /ai/detail",
|
||||
aiDetailSchema,
|
||||
await api.get<unknown>(endpoint, { params: { start, end, username } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAISolved(
|
||||
export async function getAISolved(
|
||||
start: string,
|
||||
end: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
username?: string,
|
||||
) {
|
||||
return api.get<SolvedList>("ai/solved", {
|
||||
params: { start, end, offset, limit, username },
|
||||
})
|
||||
const endpoint = "ai/solved"
|
||||
return contract(
|
||||
"GET /ai/solved",
|
||||
solvedListSchema,
|
||||
await api.get<unknown>(endpoint, {
|
||||
params: { start, end, offset, limit, username },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAIDurationData(
|
||||
export async function getAIDurationData(
|
||||
end: string,
|
||||
duration: string,
|
||||
username?: string,
|
||||
) {
|
||||
return api.get<DurationData[]>("ai/duration", {
|
||||
params: { end, duration, username },
|
||||
})
|
||||
const endpoint = "ai/duration"
|
||||
return contract(
|
||||
"GET /ai/duration",
|
||||
durationDataSchema.array(),
|
||||
await api.get<unknown>(endpoint, { params: { end, duration, username } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAIHeatmapData(username?: string) {
|
||||
return api.get<HeatmapItem[]>("ai/heatmap", {
|
||||
params: username ? { username } : {},
|
||||
})
|
||||
export async function getAIHeatmapData(username?: string) {
|
||||
const endpoint = "ai/heatmap"
|
||||
return contract(
|
||||
"GET /ai/heatmap",
|
||||
heatmapItemSchema.array(),
|
||||
await api.get<unknown>(endpoint, {
|
||||
params: username ? { username } : {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAILoginSummary() {
|
||||
return api.get<LoginSummary>("ai/login-summary")
|
||||
export async function getAILoginSummary() {
|
||||
const endpoint = "ai/login-summary"
|
||||
return contract(
|
||||
"GET /ai/login-summary",
|
||||
loginSummarySchema,
|
||||
await api.get<unknown>(endpoint),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAIPinnedReport() {
|
||||
return api.get<AiAnalysisRecord | null>("ai/pinned")
|
||||
export async function getAIPinnedReport() {
|
||||
const endpoint = "ai/pinned"
|
||||
return contract(
|
||||
"GET /ai/pinned",
|
||||
aiAnalysisRecordSchema.nullable(),
|
||||
await api.get<unknown>(endpoint),
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 相似题目推荐 ====================
|
||||
@@ -475,9 +514,12 @@ export async function getSimilarProblems(problemId: string) {
|
||||
|
||||
export type { YearlyAc as YearlyACData } from "@oj2/contract"
|
||||
|
||||
export function getProblemYearlyAC(problemId: string) {
|
||||
return api.get<YearlyAc[]>(
|
||||
`problems/${encodeURIComponent(problemId)}/yearly-ac`,
|
||||
export async function getProblemYearlyAC(problemId: string) {
|
||||
const endpoint = `problems/${encodeURIComponent(problemId)}/yearly-ac`
|
||||
return contract(
|
||||
"GET /problems/:id/yearly-ac",
|
||||
yearlyAcSchema.array(),
|
||||
await api.get<unknown>(endpoint),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -666,8 +708,13 @@ export function reportExerciseAttempt(
|
||||
}).catch(() => undefined)
|
||||
}
|
||||
|
||||
export function getLearnProgress(type: "python" | "c") {
|
||||
return api.get<TutorialProgress[]>("learn/progress", { params: { type } })
|
||||
export async function getLearnProgress(type: "python" | "c") {
|
||||
const endpoint = "learn/progress"
|
||||
return contract(
|
||||
"GET /learn/progress",
|
||||
tutorialProgressSchema.array(),
|
||||
await api.get<unknown>(endpoint, { params: { type } }),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user