feat(挑战): 做对之后可以加一条写法上的限制再做一遍,判题时记下谁做到了

借小丑牌的赌注档:同一道题加一条限制(「整首歌只用 1 次 print」)再做一遍,
给课上做得快的学生找事做(做完一节课的题,中位要空等 13 分钟)。
判定沿用 AST 规则,挑战配在 problem.challenges,做到的记在 challenge_success;
做对的提交顺手判一遍,挑战没做到也照样算做对。没过的规则带上是第几行。
0039 灌进第一批 56 道常用题的 73 条挑战,api 起来后拿历史上做对的代码补一遍谁已经做到了。
出题页能配挑战、从做对的代码里找候选;课堂看板的格子带上挑战状态。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 08:52:50 +08:00
1 parent cc1361e596
commit 0faf78ad8f
17 files changed
+9057 -37

No files matched your search

+31
View File
@@ -20,6 +20,7 @@ import { seenToday } from "../auth/presence"
import { getLoginWindow } from "../auth/session"
import { presenceOf } from "../collab/state"
import { db, schema } from "../db"
import { challengeSuccessesOf } from "../services/challenge"
import { failure, parseBody, success } from "../http"
import { isAccepted } from "../judge/status"
import {
@@ -465,6 +466,27 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
// 「来了几次」代码和流程图合起来数:那天只画了流程图的也算来过,和 recentDays 同一个口径
const recentActivity = activityRows({ since: windowStart, users: userIds })
const localDay = sql<string>`to_char(${localTime(recentActivity.createTime)}, 'YYYY-MM-DD')`
const [challengeConfigs, challengeDone] = await Promise.all([
ids.length
? db
.select({ id: schema.problem.id, challenges: schema.problem.challenges })
.from(schema.problem)
.where(inArray(schema.problem.id, ids))
: [],
challengeSuccessesOf(userIds, ids),
])
// 只认现在还配着的挑战:规则改过的旧 id 不算
const liveChallenges = new Map(
challengeConfigs.map((row) => [
row.id,
new Set(Object.values(row.challenges ?? {}).flatMap((list) => list.map((c) => c.id))),
]),
)
const challengeDoneKeys = new Set(
challengeDone
.filter((row) => liveChallenges.get(row.problemId)?.has(row.challengeId))
.map((row) => `${row.userId}:${row.problemId}`),
)
const [cells, lastSubmits, lastDrawn, attendance, drawn, todays, seen] = await Promise.all([
ids.length && userIds.length
? db
@@ -619,6 +641,7 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
problemDisplayId: problem.displayId,
title: problem.title,
isSql: problem.isSql,
hasChallenges: (liveChallenges.get(problem.id)?.size ?? 0) > 0,
})),
students: roster.map(
(student) =>
@@ -659,6 +682,14 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
}))
: [],
acceptedTry: firstTry >= 0 ? firstTry + 1 : null,
challenge:
!acceptedAt || !liveChallenges.get(id)?.size
? null
: challengeDoneKeys.has(key)
? "done"
: today.some((row) => Date.parse(row.createTime) > Date.parse(acceptedAt))
? "doing"
: null,
}
}),
lastSubmitAt: lastByUser.get(student.userId) ?? null,