From 9653f1541a5ec99acd409415fd5c5bca5a1052ad Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 25 Aug 2026 19:02:25 -0600 Subject: [PATCH] =?UTF-8?q?refactor(=E5=A5=91=E7=BA=A6):=20SQL=20=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E6=95=B0=E6=8D=AE=E8=BF=9B=E5=A5=91=E7=BA=A6=EF=BC=8C?= =?UTF-8?q?topReaction=20=E6=94=B6=E6=88=90=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit d3b05b8 把 SQLDisplay* 列进「保留不动的窄化」,理由是契约那边就是 Record。这次把那个碗底扫了 —— 因为「契约说不知道形状、 前端手抄一份来渲染」正是那个 commit 修的五处分歧的同一个模子。 SQL 题两个 JSONB 列现在有精确 schema:sqlConfigSchema、sqlDisplaySchema、 sqlDisplayTableSchema、sqlDisplayColumnSchema。键名保持 snake_case,是 problem.sql_config / sql_display 的原文,回滚时旧后端要读同一份。前端 utils/types.ts 里手抄的 SQLConfig / SQLDisplay / SQLDisplayTable / SQLDisplayColumn 共 30 行删掉改成 re-export,Problem 和 AdminProblem 的 Omit 列表各短两项。手抄那份把 SQLDisplayColumn.type 写成了可选,后端一直 是必有的空串。 后端跟着收紧:commonChecks / generateSqlDisplay 的 Record 换成 SqlConfig,`sqlConfig.mode === "modify" ? "modify" : "query"` 这句防御 删了 —— 现在类型上就只有那两个值。请求侧 createProblemRequestSchema.sqlConfig 也不再是 record:mode 是枚举、order_sensitive 缺省补 false,正好是旧后端 SQLConfigSerializer 的口径(新后端之前反而比旧的松,什么都收)。 顺带修 adminProblemListItemSchema.topReaction 的注释:写着「当前后端恒传 null,get_top_reactions 没跟着迁过来」,但 admin/problem.ts:257 早就在调了, 只有比赛题列表恒 null。这条注释会骗人去补一个已经存在的实现。类型同时从 z.string() 收成 reactionKeySchema —— getTopReactions 本来就把库里认不出的 类型滤掉了,只是类型上没体现,前端因此得在 transforms.ts 写 `as AdminProblemFiltered["topReaction"]`,现在那个强转和 `?? null` 一起没了。 服务层里的 `as ReactionKey` 换成 isReactionKey 类型守卫。 **收紧 JSONB 的 schema 会把「存量数据形状不对」从静默降级变成 500,所以实打了:** - 从生产库备份捞出 9 道 SQL 题的 sql_config / sql_display 原文,逐条过新 schema,9/9 通过;键集与旧后端 judge/sql_runner.py:build_display 的产出 逐字一致(columns/name/rows/total_rows/truncated,expected 两形态)。 - 把其中 query 形态、modify 形态各一条种进本地库,起 API 打 oj 详情和后台 详情共四个端点,全 200,expected 两种分支都正确解析。 - topReaction:插两条并列票,按 reactionKeySchema 顺序正确取到 confusing; 再插一条库里已下掉的类型,被守卫滤掉返回 null。 另:`bunx vue-tsc --noEmit` 不带 -p 是**无效的**,根 tsconfig.json 是 "files": [],塞个类型错误进去照样 exit 0。要跑 `bun run type-check` (-p tsconfig.app.json),这次的结论出自它。tsc(apps/api) 0 error、 check:routes 168 条无遮蔽、vite build 通过。 Co-Authored-By: Claude Opus 5 --- apps/api/src/routes/admin/problem.ts | 8 +-- apps/api/src/services/reaction.ts | 13 ++-- apps/web/src/admin/api.ts | 4 +- .../problem/components/SQLTestcaseEditor.vue | 8 +-- apps/web/src/admin/transforms.ts | 4 +- .../oj/problem/components/SQLDataTable.vue | 4 +- apps/web/src/utils/types.ts | 59 +++++-------------- packages/contract/src/admin.ts | 20 ++++--- packages/contract/src/problem.ts | 55 ++++++++++++++++- 9 files changed, 103 insertions(+), 72 deletions(-) diff --git a/apps/api/src/routes/admin/problem.ts b/apps/api/src/routes/admin/problem.ts index e595d54..25c3d59 100644 --- a/apps/api/src/routes/admin/problem.ts +++ b/apps/api/src/routes/admin/problem.ts @@ -11,6 +11,7 @@ import { sqlPreviewRequestSchema, sqlTestCaseScriptSchema, uploadTestCaseResponseSchema, + type SqlConfig, } from "@oj2/contract" import { and, count, desc, eq, ilike, inArray, isNull, ne, or, sql } from "drizzle-orm" import { Hono } from "hono" @@ -138,7 +139,7 @@ function commonChecks(data: { inputDescription: string outputDescription: string samples: unknown[] - sqlConfig: Record | null + sqlConfig: SqlConfig | null answers: Record[] }): { error: string } | { sql: boolean } { if (data.languages.includes("SQL")) { @@ -164,7 +165,7 @@ function commonChecks(data: { async function generateSqlDisplay( testCaseId: string, answers: Record[], - sqlConfig: Record, + sqlConfig: SqlConfig, ): Promise<{ error: string } | { display: unknown }> { const info = await readInfo(testCaseId) if (!info) return { error: "测试点信息读取失败,请重新上传测试点" } @@ -183,8 +184,7 @@ async function generateSqlDisplay( (item) => item.language === "SQL" && typeof item.code === "string" && item.code.trim(), )?.code if (typeof refSql !== "string") return { error: "题目缺少 SQL 标准答案" } - const mode = sqlConfig.mode === "modify" ? "modify" as const : "query" as const - const outcome = await buildSqlDisplay(initSql, refSql, mode) + const outcome = await buildSqlDisplay(initSql, refSql, sqlConfig.mode) if (!outcome.ok) return { error: `SQL 展示数据生成失败: ${outcome.message}` } return { display: outcome.value } } diff --git a/apps/api/src/services/reaction.ts b/apps/api/src/services/reaction.ts index 7b640eb..7e9d7d7 100644 --- a/apps/api/src/services/reaction.ts +++ b/apps/api/src/services/reaction.ts @@ -1,4 +1,4 @@ -import { reactionKeySchema } from "@oj2/contract" +import { reactionKeySchema, type ReactionKey } from "@oj2/contract" import { count, inArray } from "drizzle-orm" import { db, schema } from "../db" @@ -12,8 +12,13 @@ const TYPE_ORDER = new Map( reactionKeySchema.options.map((key, index) => [key, index]), ) +/** 库里的 type 是裸字符串;在 TYPE_ORDER 里就等价于「契约认得的类型」。 */ +function isReactionKey(value: string): value is ReactionKey { + return TYPE_ORDER.has(value) +} + export interface TopReaction { - type: string + type: ReactionKey count: number } @@ -38,10 +43,10 @@ export async function getTopReactions(problemIds: number[]) { .groupBy(schema.reaction.problemId, schema.reaction.type) for (const row of rows) { - const order = TYPE_ORDER.get(row.type) // 库里可能残留前端已经下掉的旧类型。跳过而不是当成并列最优 —— // 否则一个已经不展示的类型会把真正的最高票挤掉。 - if (order === undefined) continue + if (!isReactionKey(row.type)) continue + const order = TYPE_ORDER.get(row.type)! const current = top.get(row.problemId) if ( !current || diff --git a/apps/web/src/admin/api.ts b/apps/web/src/admin/api.ts index 49ab240..307e902 100644 --- a/apps/web/src/admin/api.ts +++ b/apps/web/src/admin/api.ts @@ -29,7 +29,7 @@ import type { Contest, Exercise, ExerciseType, - SQLDisplay, + SqlDisplay, TestcaseUploadedReturns, Tutorial, User, @@ -237,7 +237,7 @@ export function previewSQLTestcase(data: { refSql: string mode: "query" | "modify" }) { - return api2.post("admin/sql-test-cases/preview", data) + return api2.post("admin/sql-test-cases/preview", data) } // 回显已上传的 SQL 测试点脚本内容(按 1.sql, 2.sql... 排序) diff --git a/apps/web/src/admin/problem/components/SQLTestcaseEditor.vue b/apps/web/src/admin/problem/components/SQLTestcaseEditor.vue index 7263f43..c13d61c 100644 --- a/apps/web/src/admin/problem/components/SQLTestcaseEditor.vue +++ b/apps/web/src/admin/problem/components/SQLTestcaseEditor.vue @@ -1,5 +1,5 @@