From 83b6b7a81a4921c430cc5c994fb3e23e72698fd9 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 28 Sep 2026 19:35:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E9=A2=98=E7=9B=AE):=20=E4=BF=AE=E6=8E=89?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=AE=A1=E6=9F=A5=E6=9F=A5=E5=87=BA=E7=9A=84?= =?UTF-8?q?=E5=87=A0=E5=A4=84=E9=97=AE=E9=A2=98=E2=80=94=E2=80=94=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E5=9B=BE=E6=8B=BF=E5=88=B0=E7=9A=84=E5=BE=BD=E7=AB=A0?= =?UTF-8?q?=E4=B8=8D=E5=BC=B9=E3=80=81=E7=82=B9=E8=AF=84=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E8=AF=BB=E5=A4=B1=E8=B4=A5=E6=97=B6=E5=85=B3=E4=B8=8D=E6=8E=89?= =?UTF-8?q?=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 流程图画到 A / S 拿到的题单徽章、成就原来不弹:流程图那条 WebSocket 只认评分完成 / 失败, 成就消息被丢掉了,要等下次换路由拉 pending 才出来 - 强制点评弹窗读不出点评、或者一直交不上时,「下一题 / 继续」按钮也给出来:原来弹窗关不掉, 只能按浏览器后退;拦下换题时就记上「继续」,按钮文字不会写错 - 「放回编辑器」走正常的换语言路子(画流程图时切回代码编辑器、读那门语言的草稿); 这道题已经不收的语言(0020 摘掉的 Java / JS / Go)置灰,只能复制 - 举着手时刚交对、1.5 秒内点下一题:原来「确定要换题吗」要问两遍(点评完再问一次), 现在同意过的那个地址不再问 - 课堂看板上评分中、评失败的流程图不再算「尝试过」,和学生的课堂条一个口径; 新加的流程图查询和其他查询并行,不再多等一轮数据库 - 课堂条的数据只认最后发出的那次请求:进页面那次后到的话会把刚做完的那道盖回「没做」 - 「A / S 算做完」收进契约,前后端只留一份;协作中老师的判断统一用 useTeacherCollab; 几处放错位置、说法过时的注释 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/flowchart/grade.ts | 10 -- apps/api/src/flowchart/run.ts | 4 +- apps/api/src/routes/classroom.ts | 97 ++++++++++--------- apps/api/src/routes/submission-statistics.ts | 26 ++--- apps/api/src/services/problemset.ts | 2 +- .../oj/problem/components/EditorToolbar.vue | 11 +-- .../oj/problem/components/FlowchartResult.vue | 2 +- .../oj/problem/components/ProblemEditor.vue | 3 +- .../oj/problem/components/ProblemReaction.vue | 9 +- .../problem/components/ProblemSubmission.vue | 25 ++++- .../problem/components/SubmissionEffects.vue | 10 +- apps/web/src/oj/problem/detail.vue | 22 ++++- apps/web/src/oj/store/flowchart.ts | 18 +++- apps/web/src/oj/store/lesson.ts | 10 +- apps/web/src/shared/components/Header.vue | 2 - packages/contract/src/flowchart.ts | 10 ++ 16 files changed, 162 insertions(+), 99 deletions(-) diff --git a/apps/api/src/flowchart/grade.ts b/apps/api/src/flowchart/grade.ts index 4310bf1..40b3c65 100644 --- a/apps/api/src/flowchart/grade.ts +++ b/apps/api/src/flowchart/grade.ts @@ -9,13 +9,3 @@ export function gradeForScore(score: number) { if (score >= 70) return "B" return "C" } - -/** - * 画到这两档算这道题做完:题单进度、课堂条、「你画的流程图」都按它 - * (设计文档 2026-09-28-problem-page-redesign 第 3 节决定 4、6) - */ -export const FLOWCHART_PASS_GRADES = ["S", "A"] - -export function isFlowchartPass(grade: string | null | undefined) { - return grade === "S" || grade === "A" -} diff --git a/apps/api/src/flowchart/run.ts b/apps/api/src/flowchart/run.ts index cbe24c3..964978a 100644 --- a/apps/api/src/flowchart/run.ts +++ b/apps/api/src/flowchart/run.ts @@ -1,4 +1,4 @@ -import type { FlowchartUpdate } from "@oj2/contract" +import { isFlowchartPass, type FlowchartUpdate } from "@oj2/contract" import { eq } from "drizzle-orm" import { config } from "../config" @@ -6,7 +6,7 @@ import { db, schema } from "../db" import { publishFlowchartUpdate } from "../events" import { completeChat } from "../services/ai" import { recordSolvedAndNotify } from "../services/problemset" -import { gradeForScore, isFlowchartPass } from "./grade" +import { gradeForScore } from "./grade" import type { FlowchartJobData } from "./job" function evaluationPrompt(problem: typeof schema.problem.$inferSelect) { diff --git a/apps/api/src/routes/classroom.ts b/apps/api/src/routes/classroom.ts index 9cfefd5..b9be6e4 100644 --- a/apps/api/src/routes/classroom.ts +++ b/apps/api/src/routes/classroom.ts @@ -10,6 +10,7 @@ import { type ClassComparisonResponse, type ClassRankItem, type ClassUserRank, + FLOWCHART_PASS_GRADES, } from "@oj2/contract" import { and, desc, eq, gte, inArray, isNotNull, isNull, like, lte, sql } from "drizzle-orm" import { Hono } from "hono" @@ -17,7 +18,6 @@ import { Hono } from "hono" import { requireAuth, requireTeacher, type AppEnv } from "../auth/middleware" import { db, schema } from "../db" import { failure, parseBody, success } from "../http" -import { FLOWCHART_PASS_GRADES } from "../flowchart/grade" import { JudgeStatus } from "../judge/status" import { calendarDay, dayStart, localTime } from "../time" import { queryInteger, rounded } from "./helpers" @@ -309,43 +309,45 @@ classroomRoutes.get("/me/class-activity", requireAuth, async (c) => { const lesson = await classLessonProblems(user.className, CLASS_ACTIVITY_LOOKBACK_DAYS) const ids = lesson.problems.map((problem) => problem.id) - const mine = ids.length - ? await db - .select({ - problemId: schema.submission.problemId, - accepted: sql`bool_or(${inArray(schema.submission.result, SOLVED_RESULTS)})`, - }) - .from(schema.submission) - .where( - and( - eq(schema.submission.userId, user.id), - inArray(schema.submission.problemId, ids), - isNull(schema.submission.contestId), - ), - ) - .groupBy(schema.submission.problemId) - : [] + // 代码提交和流程图提交两张表,互不依赖,一起查 + const [mine, drawn] = await Promise.all([ + ids.length + ? db + .select({ + problemId: schema.submission.problemId, + accepted: sql`bool_or(${inArray(schema.submission.result, SOLVED_RESULTS)})`, + }) + .from(schema.submission) + .where( + and( + eq(schema.submission.userId, user.id), + inArray(schema.submission.problemId, ids), + isNull(schema.submission.contestId), + ), + ) + .groupBy(schema.submission.problemId) + : [], + // 流程图作业:画到 A / S 也算做完(设计文档 2026-09-28-problem-page-redesign 第 3 节决定 6)。 + // 有的题流程图交了几百次、代码个位数,只看代码提交的话,这些学生在课堂条上永远是「没做」 + ids.length + ? db + .select({ + problemId: schema.flowchartSubmission.problemId, + passed: sql`bool_or(${inArray(schema.flowchartSubmission.aiGrade, [...FLOWCHART_PASS_GRADES])})`, + }) + .from(schema.flowchartSubmission) + .where( + and( + eq(schema.flowchartSubmission.userId, user.id), + inArray(schema.flowchartSubmission.problemId, ids), + // 只数评完了的:评分中、评失败的那次不算「做过」 + eq(schema.flowchartSubmission.status, 2), + ), + ) + .groupBy(schema.flowchartSubmission.problemId) + : [], + ]) const mineById = new Map(mine.map((row) => [row.problemId, row.accepted])) - - // 流程图作业:画到 A / S 也算做完(设计文档 2026-09-28-problem-page-redesign 第 3 节决定 6)。 - // 有的题流程图交了几百次、代码个位数,只看代码提交的话,这些学生在课堂条上永远是「没做」 - const drawn = ids.length - ? await db - .select({ - problemId: schema.flowchartSubmission.problemId, - passed: sql`bool_or(${inArray(schema.flowchartSubmission.aiGrade, FLOWCHART_PASS_GRADES)})`, - }) - .from(schema.flowchartSubmission) - .where( - and( - eq(schema.flowchartSubmission.userId, user.id), - inArray(schema.flowchartSubmission.problemId, ids), - // 只数评完了的:评分中、评失败的那次不算「做过」 - eq(schema.flowchartSubmission.status, 2), - ), - ) - .groupBy(schema.flowchartSubmission.problemId) - : [] for (const row of drawn) { mineById.set(row.problemId, (mineById.get(row.problemId) ?? false) || row.passed) } @@ -462,7 +464,7 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => { .slice(-RECENT_LESSONS) const localDay = sql`to_char(${localTime(schema.submission.createTime)}, 'YYYY-MM-DD')` - const [cells, lastSubmits, attendance] = await Promise.all([ + const [cells, lastSubmits, attendance, drawn] = await Promise.all([ ids.length && userIds.length ? db .select({ @@ -515,21 +517,19 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => { ) .groupBy(schema.submission.userId) : [], - ]) - /** - * 流程图作业:画到 A / S 也算这道题做完,和学生首页的课堂条同一个口径(/me/class-activity)。 - * 有的题这节课整个班都在画流程图,只数代码提交的话,看板上满屏「没开始」 - */ - const drawn = + /** + * 流程图作业:画到 A / S 也算这道题做完,和学生首页的课堂条同一个口径(/me/class-activity)。 + * 有的题这节课整个班都在画流程图,只数代码提交的话,看板上满屏「没开始」 + */ ids.length && userIds.length - ? await db + ? db .select({ userId: schema.flowchartSubmission.userId, problemId: schema.flowchartSubmission.problemId, attempts: sql`count(*) filter (where ${gte(schema.flowchartSubmission.createTime, start)})::int`, firstPassedAt: sql< string | null - >`min(${schema.flowchartSubmission.createTime}) filter (where ${and(eq(schema.flowchartSubmission.status, 2), inArray(schema.flowchartSubmission.aiGrade, FLOWCHART_PASS_GRADES))})`, + >`min(${schema.flowchartSubmission.createTime}) filter (where ${and(eq(schema.flowchartSubmission.status, 2), inArray(schema.flowchartSubmission.aiGrade, [...FLOWCHART_PASS_GRADES]))})`, lastAt: sql< string | null >`max(${schema.flowchartSubmission.createTime}) filter (where ${gte(schema.flowchartSubmission.createTime, start)})`, @@ -539,10 +539,13 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => { and( inArray(schema.flowchartSubmission.userId, userIds), inArray(schema.flowchartSubmission.problemId, ids), + // 只数评完了的:评分中、评失败的那次不算「做过」,和 /me/class-activity 一致 + eq(schema.flowchartSubmission.status, 2), ), ) .groupBy(schema.flowchartSubmission.userId, schema.flowchartSubmission.problemId) - : [] + : [], + ]) const cellByKey = new Map(cells.map((row) => [`${row.userId}:${row.problemId}`, row])) const drawnByKey = new Map(drawn.map((row) => [`${row.userId}:${row.problemId}`, row])) const lastByUser = new Map(lastSubmits.map((row) => [row.userId, row.lastAt])) diff --git a/apps/api/src/routes/submission-statistics.ts b/apps/api/src/routes/submission-statistics.ts index 52ae43d..6b8d420 100644 --- a/apps/api/src/routes/submission-statistics.ts +++ b/apps/api/src/routes/submission-statistics.ts @@ -296,19 +296,6 @@ async function astOnlyByUser(where: SQL | undefined, userIds: number[]) { * 快照实测 count 65ms → 0.6ms。`ks2` 这种匹配上千个账号的宽前缀退回扫表,30~50ms, * 和原来持平。 */ -/** - * 用户名**精确**匹配。usernameFilter 是「包含」,学号互相包含是常态(ks24a1 包含在 ks24a10 里), - * 协作中老师看「这个学生」的提交时不能把别人的也捞进来。查无此人留恒假条件 - */ -export async function exactUsernameFilter(username: string) { - const [row] = await db - .select({ id: schema.user.id }) - .from(schema.user) - .where(eq(schema.user.username, username)) - .limit(1) - return row ? eq(schema.submission.userId, row.id) : sql`false` -} - export async function usernameFilter(username: string) { const like = `%${username}%` const users = await db @@ -327,6 +314,19 @@ export async function usernameFilter(username: string) { : frozen } +/** + * 用户名**精确**匹配。usernameFilter 是「包含」,学号互相包含是常态(ks24a1 包含在 ks24a10 里), + * 协作中老师看「这个学生」的提交时不能把别人的也捞进来。查无此人留恒假条件 + */ +export async function exactUsernameFilter(username: string) { + const [row] = await db + .select({ id: schema.user.id }) + .from(schema.user) + .where(eq(schema.user.username, username)) + .limit(1) + return row ? eq(schema.submission.userId, row.id) : sql`false` +} + /** * 两条提交列表的题号筛选:先把题号解析成 problem.id,再按 `submission.problem_id` 筛。 * 原来是 join problem 之后比 `lower(problem._id)`,条件落在 problem 表上,规划器只能 diff --git a/apps/api/src/services/problemset.ts b/apps/api/src/services/problemset.ts index 8a4e99f..42abda2 100644 --- a/apps/api/src/services/problemset.ts +++ b/apps/api/src/services/problemset.ts @@ -243,7 +243,7 @@ export async function recordSolvedProblem( /** * 做出来的那次代码提交。流程图画到 A/S 算完成时传 null:problemset_submission.submission_id * 外键指向代码提交表,流程图提交的 id 进不去。不影响「解锁加入题单之前的提交」—— - * 那道闸看的是 progress_detail(routes/submission.ts 的 problemSetJoinTimes) + * 那道闸看的是 progress_detail(本文件下面的 problemSetJoinTimes) */ submissionId: string | null, solvedAt: string, diff --git a/apps/web/src/oj/problem/components/EditorToolbar.vue b/apps/web/src/oj/problem/components/EditorToolbar.vue index c45c21c..6179c03 100644 --- a/apps/web/src/oj/problem/components/EditorToolbar.vue +++ b/apps/web/src/oj/problem/components/EditorToolbar.vue @@ -14,6 +14,7 @@ import { NFlex } from "naive-ui" import SubmitCode from "./SubmitCode.vue" import { useProblemPageContext } from "../composables/problemPageContext" import { statisticsOpen, useEditorMenu } from "../composables/editorMenu" +import { useTeacherCollab } from "../composables/teacherCollab" const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue")) @@ -73,14 +74,10 @@ const showHelpButton = computed( ) /** - * 教师端:协作就开在这道题上。接单后直接跳到题目页协作(原来是 CollabModal 弹框), - * 所以状态和「结束协作」得摆在题目页的工具栏上 —— 也只有这一个按钮能结束, - * 不像弹框那样按一下 Esc 就把协作关掉了。 + * 教师正在这道题上协作:编辑器里是学生的代码。协作状态和「结束协作」在工具栏上面那条 + * 协作条里(CollabBar);工具栏这边禁掉语言选择、不给「写代码 / 画流程图」切换 */ -const collabHere = computed( - () => collabStore.room !== null && collabStore.room.problemId === problem.value?._id, -) -const showCollabBar = computed(() => collabHere.value && userStore.isTeacherOrAbove) +const showCollabBar = useTeacherCollab() /** * 状态全塞进按钮本身。原来旁边还挂一个 n-tag 说明排队情况,一行工具栏 diff --git a/apps/web/src/oj/problem/components/FlowchartResult.vue b/apps/web/src/oj/problem/components/FlowchartResult.vue index 3f9b30f..53d632d 100644 --- a/apps/web/src/oj/problem/components/FlowchartResult.vue +++ b/apps/web/src/oj/problem/components/FlowchartResult.vue @@ -56,7 +56,7 @@ const suggestions = computed(() => ) function gradeType(grade: string) { - if (grade === "S" || grade === "A") return "success" + if (isFlowchartPass(grade)) return "success" if (grade === "B") return "warning" return "error" } diff --git a/apps/web/src/oj/problem/components/ProblemEditor.vue b/apps/web/src/oj/problem/components/ProblemEditor.vue index 6c1622f..82e8606 100644 --- a/apps/web/src/oj/problem/components/ProblemEditor.vue +++ b/apps/web/src/oj/problem/components/ProblemEditor.vue @@ -12,6 +12,7 @@ import { beginEditTrace, editTraceExtensions } from "oj/problem/utils/editTrace" import { errorMarkExtensions } from "oj/problem/utils/errorMark" import EditorToolbar from "./EditorToolbar.vue" import { useDraftKey } from "../composables/draftKey" +import { useTeacherCollab } from "../composables/teacherCollab" import CollabBar from "./CollabBar.vue" import { useFlowchartStore } from "oj/store/flowchart" @@ -39,7 +40,7 @@ const collabHere = computed( ) /** 协作中的教师:编辑器里是学生的代码,不是他自己的 */ -const teacherCollab = computed(() => collabHere.value && collabStore.isTeacher) +const teacherCollab = useTeacherCollab() /** 教师接单前自己选的语言,协作结束后连同草稿一起还原 */ let teacherLanguageBefore: LANGUAGE | null = null diff --git a/apps/web/src/oj/problem/components/ProblemReaction.vue b/apps/web/src/oj/problem/components/ProblemReaction.vue index c447f81..55db22c 100644 --- a/apps/web/src/oj/problem/components/ProblemReaction.vue +++ b/apps/web/src/oj/problem/components/ProblemReaction.vue @@ -347,7 +347,14 @@ function sectorAriaLabel(index: number) {

没记上,再点一次

-
+ +
diff --git a/apps/web/src/oj/problem/components/ProblemSubmission.vue b/apps/web/src/oj/problem/components/ProblemSubmission.vue index 4694f4c..39b3337 100644 --- a/apps/web/src/oj/problem/components/ProblemSubmission.vue +++ b/apps/web/src/oj/problem/components/ProblemSubmission.vue @@ -1,6 +1,7 @@