fix(题目): 修掉题目页重构审查查出的问题——提交途中换题会把结果算到新题上、运行例子把对的菱形判错、点评完「下一题」不跳等
题目页重构之后按「功能对照 / 状态 / 结果页 / 布局与后端」四路审查了一遍,查出的问题一起修: 提交与换题 - 提交、画流程图途中换题:A 题格式化好的代码会盖掉 B 的草稿,A 判对了把 B 标成已解决、 弹 B 的点评、题单里跳回题单。现在每个请求回来先核对题目,轮询和推送回来的也核对提交编号 - 画着流程图换到不能画的题,语言退到题目的第一门、还被记成偏好;现在回到学生习惯的那门 - 强制点评评完点「继续 / 下一题」,学生之前在看「题目」页签的话会停在原题 - 离开题目页之后判题、流程图的 WebSocket 一直连着,现在闲 15 分钟断开 运行例子 - 输出开头的空格被削掉,打印菱形这类题对的代码也显示「没对上」 - C 的编译警告拼进了「你的输出」(后台生成测试点也会写进 .out) - Python 的语法错误(中文标点)被说成「下标有没有越界、除数是不是 0」 - 比赛里仍然把空格画成「·」、把不同的地方标红 入口与协作 - 比赛里不给「课堂统计」、不绑协作:房间和统计都按题号找,比赛题号和公开题撞号 - 「看他交过什么」按整名匹配,不再混进学号相近的同学 - 协作中点上下文条、相似题、下一题开新标签,协作不断 - 题单入口给管理员的「⋯」里留「本题提交」 结果与题面 - 「去自己输入拿 51 这些数试试」取到了题面 HTML 里的颜色值;「题目原话」露出 " - 流程图 A 级的学生标题下还写着「还没交过」 - 「我的提交」上方补回「答案错误 × 3」这样的计数 - 流程图历次评分能点开看那一版的图;读分数失败不再说「还没交过流程图」;手机上能看流程图点评 - 「结果」页签有新结果没看时加粗上色,看过就恢复 - 空画布拖进第一个节点画布会跳一下;画布底下空着一条 - 比赛没开始写成「已结束」、倒计时超过一个月算错;分隔条拖到头工具栏被挤坏;题单里的题目页有备案页脚 - 流程图历次分数只读需要的几列(一个学生一道题 23 次提交:96 kB → 2 kB) - 比赛密码、比赛没开始等几条学生看得到的英文报错换成中文 设计文档照实际做法更新了第 5.5、5.6、6、8、9、10、11 节。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee66cef367
commit
e7b95d211d
34 files changed
+579
-108
No files matched your search
@@ -114,7 +114,7 @@ contestRoutes.get("/contests/:id", optionalAuth, async (c) => {
|
||||
c.get("user"),
|
||||
queryInteger(c.req.param("id"), 0, { min: 1 }),
|
||||
)
|
||||
if (!contest) return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||
if (!contest) return failure(c, 404, "contest-not-found", "比赛不存在")
|
||||
const byId = await creators([contest.createdById])
|
||||
return success(
|
||||
c,
|
||||
@@ -133,9 +133,8 @@ contestRoutes.post("/contests/:id/access", requireAuth, async (c) => {
|
||||
c.get("user"),
|
||||
queryInteger(c.req.param("id"), 0, { min: 1 }),
|
||||
)
|
||||
if (!contest || !contest.password)
|
||||
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||
const parsed = await parseBody(c, contestPasswordRequestSchema, "Password is required")
|
||||
if (!contest || !contest.password) return failure(c, 404, "contest-not-found", "比赛不存在")
|
||||
const parsed = await parseBody(c, contestPasswordRequestSchema, "请输入比赛密码")
|
||||
if (!parsed.success) return parsed.response
|
||||
// 比赛密码往往就是几位数字,不限的话一个脚本几分钟就能扫完。
|
||||
// 按「人 × 比赛」计失败次数:猜错的是自己,锁的也只是自己进这一场。
|
||||
@@ -151,7 +150,7 @@ contestRoutes.post("/contests/:id/access", requireAuth, async (c) => {
|
||||
}
|
||||
if (!checkContestPassword(parsed.data.password, contest.password)) {
|
||||
await countAttempt(attemptKey, CONTEST_PASSWORD_RULE)
|
||||
return failure(c, 403, "wrong-password", "Wrong password or password expired")
|
||||
return failure(c, 403, "wrong-password", "比赛密码不对,或者已经过期了,请重新输入")
|
||||
}
|
||||
await setContestPassword(c, contest.id, parsed.data.password)
|
||||
return success(c, true)
|
||||
@@ -162,8 +161,7 @@ contestRoutes.get("/contests/:id/access", requireAuth, async (c) => {
|
||||
c.get("user"),
|
||||
queryInteger(c.req.param("id"), 0, { min: 1 }),
|
||||
)
|
||||
if (!contest || !contest.password)
|
||||
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||
if (!contest || !contest.password) return failure(c, 404, "contest-not-found", "比赛不存在")
|
||||
const access = await canAccessContest(c, contest, "details")
|
||||
return success(c, { access: access.ok } satisfies ContestAccess)
|
||||
})
|
||||
@@ -276,7 +274,7 @@ contestRoutes.get(
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
if (!row) return failure(c, 404, "problem-not-found", "Problem does not exist")
|
||||
if (!row) return failure(c, 404, "problem-not-found", "题目不存在")
|
||||
const tags = await contestProblemTags([row.problem.id])
|
||||
const allowed = contestDetailsAllowed(c.get("user"), contest)
|
||||
const statuses = await contestProblemStatuses(c.get("user")?.id)
|
||||
|
||||
@@ -618,16 +618,23 @@ flowchartRoutes.post("/flowcharts/:id/retry", requireAuth, async (c) => {
|
||||
/**
|
||||
* 自己在这道题上评完的每一次,按时间从早到晚,已经滤掉加入题单之前的(题单闸门)。
|
||||
* current / history / scores 三个接口都从这一份出。
|
||||
*
|
||||
* 只取分数和闸门要的几列:整行带着画布 JSON 和 mermaid 源码(一条约 5KB),画了几十次
|
||||
* 的学生每切一次题就要搬几百 KB,而结果页签只要分数。要整行的 history 自己按 id 再取。
|
||||
* userId / problemId / createTime 三列是 hiddenByProblemSet 要的,别删。
|
||||
*/
|
||||
async function myEvaluatedFlowcharts(user: AuthUser, problemId: number) {
|
||||
const [rows, joinTimes] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
flowchart: schema.flowchartSubmission,
|
||||
username: schema.user.username,
|
||||
id: schema.flowchartSubmission.id,
|
||||
userId: schema.flowchartSubmission.userId,
|
||||
problemId: schema.flowchartSubmission.problemId,
|
||||
createTime: schema.flowchartSubmission.createTime,
|
||||
aiScore: schema.flowchartSubmission.aiScore,
|
||||
aiGrade: schema.flowchartSubmission.aiGrade,
|
||||
})
|
||||
.from(schema.flowchartSubmission)
|
||||
.innerJoin(schema.user, eq(schema.flowchartSubmission.userId, schema.user.id))
|
||||
.where(
|
||||
and(
|
||||
eq(schema.flowchartSubmission.userId, user.id),
|
||||
@@ -638,7 +645,7 @@ async function myEvaluatedFlowcharts(user: AuthUser, problemId: number) {
|
||||
.orderBy(asc(schema.flowchartSubmission.createTime)),
|
||||
joinTimesFor(user, [{ userId: user.id, problemId }]),
|
||||
])
|
||||
const visible = rows.filter((row) => !hiddenByProblemSet(user, row.flowchart, joinTimes))
|
||||
const visible = rows.filter((row) => !hiddenByProblemSet(user, row, joinTimes))
|
||||
return { visible, hidden: rows.length - visible.length }
|
||||
}
|
||||
|
||||
@@ -650,7 +657,7 @@ async function myEvaluatedFlowcharts(user: AuthUser, problemId: number) {
|
||||
flowchartRoutes.get("/problems/:id/flowchart/current", requireAuth, async (c) => {
|
||||
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
const { visible } = await myEvaluatedFlowcharts(c.get("user")!, problemId)
|
||||
const latest = visible.at(-1)?.flowchart
|
||||
const latest = visible.at(-1)
|
||||
return success(c, {
|
||||
count: visible.length,
|
||||
score: latest?.aiScore ?? 0,
|
||||
@@ -661,11 +668,20 @@ flowchartRoutes.get("/problems/:id/flowchart/current", requireAuth, async (c) =>
|
||||
flowchartRoutes.get("/problems/:id/flowchart/history", requireAuth, async (c) => {
|
||||
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
const page = queryInteger(c.req.query("page"), 0, { min: 0 })
|
||||
const { visible: rows } = await myEvaluatedFlowcharts(c.get("user")!, problemId)
|
||||
const user = c.get("user")!
|
||||
const { visible: rows } = await myEvaluatedFlowcharts(user, problemId)
|
||||
const selected = page === 0 ? rows.at(-1) : rows[page - 1]
|
||||
if (page > rows.length) return failure(c, 400, "page-out-of-range", "Page out of range")
|
||||
// 列表只带了分数,选中的那一条再按 id 取整行。都是本人的提交,用户名就是自己的
|
||||
const [full] = selected
|
||||
? await db
|
||||
.select()
|
||||
.from(schema.flowchartSubmission)
|
||||
.where(eq(schema.flowchartSubmission.id, selected.id))
|
||||
.limit(1)
|
||||
: []
|
||||
return success(c, {
|
||||
submission: selected ? flowchartData(selected.flowchart, selected.username) : null,
|
||||
submission: full ? flowchartData(full, user.username) : null,
|
||||
count: rows.length,
|
||||
} satisfies FlowchartDetail)
|
||||
})
|
||||
@@ -678,7 +694,7 @@ flowchartRoutes.get("/problems/:id/flowchart/scores", requireAuth, async (c) =>
|
||||
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
const { visible, hidden } = await myEvaluatedFlowcharts(c.get("user")!, problemId)
|
||||
return success(c, {
|
||||
scores: visible.map(({ flowchart }) => ({
|
||||
scores: visible.map((flowchart) => ({
|
||||
id: flowchart.id,
|
||||
score: flowchart.aiScore ?? 0,
|
||||
grade: flowchart.aiGrade ?? "",
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function canAccessContest<E extends AppEnv>(
|
||||
return {
|
||||
ok: false as const,
|
||||
code: "wrong-password",
|
||||
message: "Wrong password or password expired",
|
||||
message: "比赛密码不对,或者已经过期了,请重新输入",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export async function canAccessContest<E extends AppEnv>(
|
||||
return {
|
||||
ok: false as const,
|
||||
code: "contest-not-started",
|
||||
message: "Contest has not started yet.",
|
||||
message: "比赛还没有开始",
|
||||
}
|
||||
}
|
||||
return { ok: true as const }
|
||||
@@ -119,7 +119,7 @@ export function requireContestAccess(
|
||||
const id = Number(c.req.param(paramName))
|
||||
const contest =
|
||||
Number.isInteger(id) && id > 0 ? await findAccessibleContest(c.get("user"), id) : null
|
||||
if (!contest) return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||
if (!contest) return failure(c, 404, "contest-not-found", "比赛不存在")
|
||||
const access = await canAccessContest(c, contest, checkType)
|
||||
if (!access.ok) {
|
||||
return failure(c, access.code === "login-required" ? 401 : 403, access.code, access.message)
|
||||
|
||||
Reference in new issue
Block a user