From c2a81209d70ad246bdd6cd48c69adce89c7274a4 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 25 Aug 2026 13:44:24 -0600 Subject: [PATCH] =?UTF-8?q?refactor(=E5=89=8D=E7=AB=AF):=20=E6=8B=86?= =?UTF-8?q?=E6=8E=89=20camelCase=E2=86=92snake=5Fcase=20=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E5=B1=82=EF=BC=8C=E5=A5=91=E7=BA=A6=E6=88=90=E4=B8=BA=E5=94=AF?= =?UTF-8?q?=E4=B8=80=E7=9C=9F=E7=9B=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit utils/legacy.ts 是迁移期的临时层:新后端一律 camelCase,而组件读的还是 旧 Django 的 snake_case,于是在 api 层做一次递归键名重写。它自己的注释就 写了「迁移完成后这一层应当整体拆掉」。现在拆了。 代价不只是那 96 处包装:每个响应都要递归遍历整个对象重写一遍键名,而且 utils/types.ts 和 packages/contract 是两份真相 —— 手抄的那份还抄歪了好几处。 做法是按域推进,每域都用 vue-tsc 相对基线做差,确认零新增错误后再往下走。 前端的类型现在一律以契约为准,只在必要处窄化(比如 languages/template 的键 窄化成 LANGUAGE),删掉的重复定义包括 WebsiteConfig、LoginSummary、 AchievementSummary、ProblemSet、Contest、User、Profile、AdminTag、 StuckProblem 等等,其中 ClassComparison 有两个组件各手抄了一份。 ## 顺带修掉的真 bug - 管理端公告列表的「可见」开关每次都 400:列表响应被契约 omit 掉了 content, 而更新接口要求 content 必填,toggleVisible 把列表行原样回传。而且是乐观 翻转、不 await 不 catch,管理员看到开关动了、实际没存也没有提示。 改成先 GET 整条再 PUT,加失败提示。 - 删有提交的题时只显示笼统的「删除失败」:前端还在 match 旧 Django 的英文 文案,而后端返回的是 problem-has-submissions + 中文。连同另外 8 处同类 匹配一起改成判错误码 —— 文案是后端随时能改的,match 文案改一个字就静默失效。 - SubmissionStatus.time_limit_exceeded 写成 `1 | 2`,TS 按位或算成 3,和 memory_limit_exceeded 撞了同一个值。后端 judge/status.ts 里这是分开的 两个码,按后端拆成 cpu_/real_ 两项。当前没有代码读这两个成员,但 CLAUDE.md 明确要求判题状态码三处同步。 - 流程图历史翻到没有提交的那一页会直接抛:契约里 submission 是 nullable, 被 any 掩盖成看起来非空。补了 null 分支。 ## 契约里被逼出来的三处不诚实 - grade 写成 z.string(),但 averageGrade() 在没有可用数据时返回空串, 前端三张图表拿它查 Record 会查出 undefined。按实际收紧成 z.enum([...,""]),四个查表点都补了「无评级」分支。 - difficulty 写成 z.string()。核对过生产库 dump:956 道题只有 Low/Mid/High 三个值(761/149/46)。收紧成枚举。 - topReaction 写成 z.string(),既对不上前端渲染的 {type,count},也对不上 旧后端 get_top_reactions 下发的形状。改成正确形状并注明当前恒传 null。 ## 明确保留 snake_case 的 54 处 判题沙箱原始输出(cpu_time/exit_code/output_md5/compile_output)、 statistic_info 内容(err_info/time_cost/ast_results)、submission_info JSONB(is_ac/ac_time/error_number,回滚时旧后端还要读)、SQL 判题引擎的 total_rows/order_sensitive/changed_tables、WebSocket 的 submission_id、 以及数据库选项键 enable_maxkb。每一处都在类型定义旁写了为什么不能改。 language 没有跟着收紧契约 —— 它是配置项、随时可能加语言,收紧会让新语言 在后端 parse 时直接抛。改在 api 边界一处窄化。 ## 另外 - utils/http.ts 整个模块已是死代码(四处引用全是 import type),删除。 - profile 的 blog/github/school/major/language 五个字段全链路空转,没有 任何组件读,从契约到类型一并摘除(数据库列不动)。 - admin/account.ts 往 user_profile 塞的 totalScore 是 OI 模式遗留,表里 没这一列。Drizzle 按表定义拼列名会把它静默丢弃,所以没出过错,是死代码。 验证:vue-tsc 143 → 54 条且无新增,apps/api tsc、check:routes、web build 全通过;各域响应形状逐条打接口核对过。 Co-Authored-By: Claude Opus 5 --- apps/api/src/routes/account.ts | 5 - apps/api/src/routes/admin/account.ts | 1 - apps/api/src/routes/auth.ts | 40 +- apps/api/src/services/profile.ts | 5 - .../components/AchievementModal.vue | 5 +- apps/web/src/admin/achievement/list.vue | 4 +- apps/web/src/admin/ai/list.vue | 37 +- apps/web/src/admin/announcement/list.vue | 49 +- apps/web/src/admin/api.ts | 564 ++++++------- apps/web/src/admin/contest/detail.vue | 32 +- apps/web/src/admin/contest/helper.vue | 48 +- apps/web/src/admin/contest/list.vue | 4 +- apps/web/src/admin/problem/Stuck.vue | 20 +- apps/web/src/admin/problem/TopACTrend.vue | 28 +- .../src/admin/problem/components/Actions.vue | 6 +- .../admin/problem/components/AddButton.vue | 4 +- .../problem/components/BatchTagModal.vue | 4 +- .../problem/components/SQLTestcaseEditor.vue | 6 +- apps/web/src/admin/problem/detail.vue | 146 ++-- apps/web/src/admin/problem/list.vue | 10 +- apps/web/src/admin/problem/tags.vue | 6 +- .../problemset/components/AddBadgeModal.vue | 2 +- .../problemset/components/BadgeManagement.vue | 6 +- .../problemset/components/EditBadgeModal.vue | 6 +- .../components/EditProblemModal.vue | 2 +- .../components/ProblemManagement.vue | 2 +- .../problemset/components/ProblemSetInfo.vue | 6 +- .../components/ProgressManagement.vue | 6 +- apps/web/src/admin/problemset/edit.vue | 10 +- apps/web/src/admin/problemset/list.vue | 10 +- apps/web/src/admin/setting/config.vue | 61 +- apps/web/src/admin/setting/home.vue | 8 +- apps/web/src/admin/transforms.ts | 21 +- .../tutorial/components/ExerciseManager.vue | 2 +- apps/web/src/admin/tutorial/detail.vue | 8 +- apps/web/src/admin/tutorial/list.vue | 30 +- .../web/src/admin/user/components/Actions.vue | 6 +- apps/web/src/admin/user/components/Name.vue | 14 +- apps/web/src/admin/user/list.vue | 51 +- apps/web/src/oj/achievement/api.ts | 20 +- .../components/AchievementCard.vue | 12 +- apps/web/src/oj/achievement/index.vue | 19 +- .../oj/ai/components/DifficultyGradeChart.vue | 1 + .../src/oj/ai/components/DurationChart.vue | 4 +- .../src/oj/ai/components/EfficiencyChart.vue | 4 +- apps/web/src/oj/ai/components/Grade.vue | 4 +- apps/web/src/oj/ai/components/Overview.vue | 5 +- .../src/oj/ai/components/ProgressChart.vue | 3 +- .../ai/components/RankDistributionChart.vue | 6 +- apps/web/src/oj/ai/components/SolvedTable.vue | 26 +- apps/web/src/oj/ai/components/TagTitle.vue | 12 +- .../oj/ai/components/TimeActivityHeatmap.vue | 2 +- apps/web/src/oj/announcement/list.vue | 6 +- apps/web/src/oj/api.ts | 493 ++++------- apps/web/src/oj/class/pk.vue | 187 ++--- .../oj/contest/components/AcAndSubmission.vue | 4 +- .../src/oj/contest/components/ContestInfo.vue | 8 +- .../src/oj/contest/components/LineChart.vue | 4 +- apps/web/src/oj/contest/list.vue | 14 +- apps/web/src/oj/contest/pages/rank.vue | 10 +- apps/web/src/oj/problem/components/Form.vue | 4 +- .../oj/problem/components/ProblemContent.vue | 24 +- .../problem/components/ProblemFlowchart.vue | 4 +- .../src/oj/problem/components/ProblemInfo.vue | 16 +- .../problem/components/ProblemListTitle.vue | 6 +- .../oj/problem/components/ProblemReaction.vue | 2 +- .../problem/components/ProblemSubmission.vue | 20 +- .../problem/components/ProblemYearlyChart.vue | 4 +- .../problem/components/SubmissionResult.vue | 12 +- .../src/oj/problem/components/SubmitCode.vue | 10 +- .../oj/problem/components/SubmitFlowchart.vue | 42 +- apps/web/src/oj/problem/detail.vue | 10 +- .../components/ProblemSetHeader.vue | 5 +- .../components/ProblemSetProblemsList.vue | 4 +- .../components/UserProgressView.vue | 26 +- apps/web/src/oj/problemset/detail.vue | 8 +- apps/web/src/oj/problemset/list.vue | 35 +- apps/web/src/oj/rank/components/Chart.vue | 4 +- apps/web/src/oj/rank/list.vue | 121 +-- apps/web/src/oj/store/ai.ts | 11 +- apps/web/src/oj/store/contest.ts | 14 +- apps/web/src/oj/store/problem.ts | 2 +- .../components/FlowchartScoreDetail.vue | 46 +- .../src/oj/submission/components/Grade.vue | 1 + .../submission/components/SubmissionLink.vue | 4 +- apps/web/src/oj/submission/detail.vue | 10 +- apps/web/src/oj/submission/list.vue | 25 +- apps/web/src/oj/transforms.ts | 36 +- apps/web/src/oj/user/index.vue | 10 +- apps/web/src/oj/user/message.vue | 2 +- apps/web/src/oj/user/setting.vue | 4 +- apps/web/src/shared/api.ts | 33 +- .../src/shared/components/AuthorSelect.vue | 10 +- .../src/shared/components/ContestTitle.vue | 6 +- .../web/src/shared/components/ContestType.vue | 6 +- apps/web/src/shared/components/Header.vue | 4 +- apps/web/src/shared/components/Login.vue | 11 +- .../shared/components/LoginSummaryModal.vue | 12 +- apps/web/src/shared/components/Signup.vue | 4 +- apps/web/src/shared/components/UserBadge.vue | 10 +- apps/web/src/shared/composables/maxkb.ts | 6 +- .../src/shared/extensions/autocompletion.ts | 2 +- apps/web/src/shared/store/achievement.ts | 12 +- apps/web/src/shared/store/config.ts | 18 +- apps/web/src/shared/store/loginSummary.ts | 23 +- apps/web/src/shared/store/user.ts | 24 +- apps/web/src/utils/api2.ts | 25 +- apps/web/src/utils/constants.ts | 10 +- apps/web/src/utils/functions.ts | 2 +- apps/web/src/utils/http.ts | 79 -- apps/web/src/utils/legacy.ts | 27 - apps/web/src/utils/permissions.ts | 6 +- apps/web/src/utils/types.ts | 762 +++++++----------- packages/contract/src/account.ts | 22 +- packages/contract/src/achievement.ts | 30 +- packages/contract/src/admin.ts | 41 +- packages/contract/src/ai.ts | 21 +- packages/contract/src/auth.ts | 5 - packages/contract/src/classroom.ts | 5 + packages/contract/src/content.ts | 4 + packages/contract/src/contest.ts | 5 + packages/contract/src/flowchart.ts | 7 + packages/contract/src/problem.ts | 17 +- packages/contract/src/problemset.ts | 9 + packages/contract/src/submission.ts | 5 + 125 files changed, 1651 insertions(+), 2253 deletions(-) delete mode 100644 apps/web/src/utils/http.ts delete mode 100644 apps/web/src/utils/legacy.ts diff --git a/apps/api/src/routes/account.ts b/apps/api/src/routes/account.ts index df0360d..fb98c85 100644 --- a/apps/api/src/routes/account.ts +++ b/apps/api/src/routes/account.ts @@ -84,15 +84,10 @@ accountRoutes.post("/users", async (c) => { userId: created.id, acmProblemsStatus: {}, avatar: `${config.avatarUriPrefix}/default.png`, - blog: null, mood: null, acceptedNumber: 0, submissionNumber: 0, - github: null, - school: null, - major: null, realName: null, - language: null, }) }) return success(c, { ok: true }, 201) diff --git a/apps/api/src/routes/admin/account.ts b/apps/api/src/routes/admin/account.ts index 0228932..db31310 100644 --- a/apps/api/src/routes/admin/account.ts +++ b/apps/api/src/routes/admin/account.ts @@ -227,7 +227,6 @@ adminAccountRoutes.post("/users", requireSuperAdmin, async (c) => { acmProblemsStatus: {}, submissionNumber: 0, acceptedNumber: 0, - totalScore: 0, }))) return users.length }) diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 2b3477c..18aa090 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -1,9 +1,5 @@ -import { - loginRequestSchema, - sessionUserSchema, - userProfileSchema, -} from "@oj2/contract" -import { and, eq, sql } from "drizzle-orm" +import { loginRequestSchema } from "@oj2/contract" +import { eq, sql } from "drizzle-orm" import { Hono } from "hono" import { optionalAuth, type AppEnv } from "../auth/middleware" @@ -17,21 +13,31 @@ import { getUserProfileById } from "../services/profile" export const authRoutes = new Hono() authRoutes.post("/auth/login", async (c) => { - const parsed = loginRequestSchema.safeParse(await c.req.json().catch(() => null)) + const parsed = loginRequestSchema.safeParse( + await c.req.json().catch(() => null), + ) if (!parsed.success) { - return failure(c, 400, "invalid-request", "Username and password are required") + return failure( + c, + 400, + "invalid-request", + "Username and password are required", + ) } const [user] = await db .select() .from(schema.user) - .where( - sql`lower(${schema.user.username}) = lower(${parsed.data.username})`, - ) + .where(sql`lower(${schema.user.username}) = lower(${parsed.data.username})`) .limit(1) if (!user) { - return failure(c, 401, "invalid-credentials", "Invalid username or password") + return failure( + c, + 401, + "invalid-credentials", + "Invalid username or password", + ) } if (user.isDisabled) { return failure(c, 403, "account-disabled", "Your account has been disabled") @@ -39,7 +45,12 @@ authRoutes.post("/auth/login", async (c) => { const password = await verifyPassword(parsed.data.password, user.password) if (!password.valid) { - return failure(c, 401, "invalid-credentials", "Invalid username or password") + return failure( + c, + 401, + "invalid-credentials", + "Invalid username or password", + ) } const now = new Date().toISOString() @@ -67,6 +78,7 @@ authRoutes.get("/me", optionalAuth, async (c) => { if (!authUser) return success(c, null) const data = await getUserProfileById(authUser.id, true) - if (!data) return failure(c, 404, "profile-not-found", "User profile does not exist") + if (!data) + return failure(c, 404, "profile-not-found", "User profile does not exist") return success(c, data) }) diff --git a/apps/api/src/services/profile.ts b/apps/api/src/services/profile.ts index 718a5d7..7b1f6e5 100644 --- a/apps/api/src/services/profile.ts +++ b/apps/api/src/services/profile.ts @@ -29,12 +29,7 @@ export async function getUserProfileById(userId: number, showRealName: boolean) realName: showRealName ? row.profile.realName : null, acmProblemsStatus: row.profile.acmProblemsStatus, avatar: row.profile.avatar, - blog: row.profile.blog, mood: row.profile.mood, - github: row.profile.github, - school: row.profile.school, - major: row.profile.major, - language: row.profile.language, acceptedNumber: row.profile.acceptedNumber, submissionNumber: row.profile.submissionNumber, }) diff --git a/apps/web/src/admin/achievement/components/AchievementModal.vue b/apps/web/src/admin/achievement/components/AchievementModal.vue index 676ba19..d6481fb 100644 --- a/apps/web/src/admin/achievement/components/AchievementModal.vue +++ b/apps/web/src/admin/achievement/components/AchievementModal.vue @@ -1,4 +1,5 @@ - 仅 {{ achievement.unlock_rate }}% 的人获得 + 仅 {{ achievement.unlockRate }}% 的人获得 diff --git a/apps/web/src/oj/achievement/index.vue b/apps/web/src/oj/achievement/index.vue index 4b54628..f11d5e1 100644 --- a/apps/web/src/oj/achievement/index.vue +++ b/apps/web/src/oj/achievement/index.vue @@ -7,25 +7,10 @@ import type { Achievement, AchievementRarity, AchievementSummary, + UserBadge, } from "utils/types" import AchievementCard from "./components/AchievementCard.vue" -interface UserBadge { - id: number - earned_time: string - badge: { - id: number - name: string - description: string - icon: string - } - // 奖章来自哪个题单,接口在 UserBadgeSerializer 里带出来 - problemset: { - id: number - title: string - } | null -} - const route = useRoute() const name = computed(() => (route.query.name as string) || undefined) @@ -72,7 +57,7 @@ async function load() { // http 客户端返回 ApiResponse,真实载荷在 .data 里 achievements.value = list.data.achievements summary.value = sum.data - badges.value = (badgeRes.data ?? []) as UserBadge[] + badges.value = badgeRes.data ?? [] } finally { loading.value = false } diff --git a/apps/web/src/oj/ai/components/DifficultyGradeChart.vue b/apps/web/src/oj/ai/components/DifficultyGradeChart.vue index 6add8be..b21aa4b 100644 --- a/apps/web/src/oj/ai/components/DifficultyGradeChart.vue +++ b/apps/web/src/oj/ai/components/DifficultyGradeChart.vue @@ -83,6 +83,7 @@ const data = computed(() => { // 根据等级返回对应的颜色 function getGradeColor(grade: Grade): string { const colors: { [key in Grade]: string } = { + "": "#C9CDD4", // 无评级:后端在没有可用数据时下发空串 S: "#FF6384", A: "#FFCE56", B: "#36A2EB", diff --git a/apps/web/src/oj/ai/components/DurationChart.vue b/apps/web/src/oj/ai/components/DurationChart.vue index 502d7ca..7331939 100644 --- a/apps/web/src/oj/ai/components/DurationChart.vue +++ b/apps/web/src/oj/ai/components/DurationChart.vue @@ -73,14 +73,14 @@ const data = computed>(() => { { type: "bar", label: "完成题目数", - data: aiStore.durationData.map((duration) => duration.problem_count), + data: aiStore.durationData.map((duration) => duration.problemCount), yAxisID: "y", order: 2, }, { type: "bar", label: "总提交次数", - data: aiStore.durationData.map((duration) => duration.submission_count), + data: aiStore.durationData.map((duration) => duration.submissionCount), yAxisID: "y", order: 2, }, diff --git a/apps/web/src/oj/ai/components/EfficiencyChart.vue b/apps/web/src/oj/ai/components/EfficiencyChart.vue index 8b74bf4..1659a98 100644 --- a/apps/web/src/oj/ai/components/EfficiencyChart.vue +++ b/apps/web/src/oj/ai/components/EfficiencyChart.vue @@ -59,8 +59,8 @@ const show = computed(() => { // 计算提交效率数据 const efficiencyData = computed(() => { return aiStore.durationData.map((duration) => { - const problemCount = duration.problem_count || 0 - const submissionCount = duration.submission_count || 0 + const problemCount = duration.problemCount || 0 + const submissionCount = duration.submissionCount || 0 // 计算效率:提交次数/完成题目数 // 值越接近1,说明一次AC率越高 diff --git a/apps/web/src/oj/ai/components/Grade.vue b/apps/web/src/oj/ai/components/Grade.vue index b5096d1..19ee246 100644 --- a/apps/web/src/oj/ai/components/Grade.vue +++ b/apps/web/src/oj/ai/components/Grade.vue @@ -28,8 +28,10 @@