chore(题单): 删掉客户端自报进度的 PUT /problem-set-progress
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
这个端点是「AC 之后前端回调一下,把进度写进题单」那套设计的残留,早就被服务端记账 取代了 —— SubmitCode.vue 里留着当时的说明:客户端那条路只认路由参数里的那一个题单 (从普通题库入口做出同一道题不计进度),网络一抖、页面提前关掉进度就静默丢失; 现在判完之后由 judge/run.ts → services/problemset.ts 的 recordSolvedProblem 记账, 而且记进所有已加入且包含这道题的题单。 上一个提交删掉前端最后一个调用方 updateProblemSetProgress 之后,它就彻底没人打了。 留着的代价不只是死代码:那是一条**学生可以自己写进度**的写入口。 删之前逐个核过: - 前端零引用(唯一的 wrapper 已在上个提交删掉); - recomputeProgress 还有 POST 那条在用,保留;computeProgress / eligibleForBadge / updateAchievementsForProblemSet / publishAchievementNotification 在别处都有调用方; - 它往 problemset_submission 写的那一笔,services/problemset.ts:260 做的是一模一样的 去重后插入,不会因此少写。 顺带清掉因此变成孤儿的四个 import 和契约里的 updateProblemSetProgressRequestSchema 与 UpdateProblemSetProgressRequest。 ## 验证 起服务实跑: - PUT /api/problem-set-progress 现在 404; - 保留的 POST(加入题单)仍然 201,progress 行照常由 recomputeProgress 建出来; - 服务端那条替代路径端到端跑通:新建题单 → 加入题目 1004 → 学生加入 → 交一发 AC, 判完后 problemset_progress 自动变成 completed=1/total=1/100%/得分 10, problemset_submission 也落了一行 —— 全程没有任何客户端回调。 tsc、check:routes、vue-tsc、单二进制编译均通过;测试题单已清理。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
joinProblemSetRequestSchema,
|
joinProblemSetRequestSchema,
|
||||||
updateProblemSetProgressRequestSchema,
|
|
||||||
type ProblemSet,
|
type ProblemSet,
|
||||||
type ProblemSetBadge,
|
type ProblemSetBadge,
|
||||||
type ProblemSetList,
|
type ProblemSetList,
|
||||||
@@ -27,11 +26,8 @@ import { Hono } from "hono"
|
|||||||
|
|
||||||
import { optionalAuth, requireAuth, requireTeacher, type AppEnv } from "../auth/middleware"
|
import { optionalAuth, requireAuth, requireTeacher, type AppEnv } from "../auth/middleware"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { publishAchievementNotification } from "../events"
|
|
||||||
import { failure, success } from "../http"
|
import { failure, success } from "../http"
|
||||||
import { JudgeStatus } from "../judge/status"
|
import { computeProgress } from "../services/problemset"
|
||||||
import { updateAchievementsForProblemSet } from "../services/achievements"
|
|
||||||
import { computeProgress, eligibleForBadge } from "../services/problemset"
|
|
||||||
import { asFilterValue, objectValue, queryInteger, sampleUser } from "./helpers"
|
import { asFilterValue, objectValue, queryInteger, sampleUser } from "./helpers"
|
||||||
|
|
||||||
export const problemsetRoutes = new Hono<AppEnv>()
|
export const problemsetRoutes = new Hono<AppEnv>()
|
||||||
@@ -246,93 +242,6 @@ problemsetRoutes.post("/problem-set-progress", requireAuth, async (c) => {
|
|||||||
return success(c, null, 201)
|
return success(c, null, 201)
|
||||||
})
|
})
|
||||||
|
|
||||||
problemsetRoutes.put("/problem-set-progress", requireAuth, async (c) => {
|
|
||||||
const parsed = updateProblemSetProgressRequestSchema.safeParse(await c.req.json().catch(() => null))
|
|
||||||
if (!parsed.success) return failure(c, 400, "invalid-request", "Invalid progress payload")
|
|
||||||
const user = c.get("user")!
|
|
||||||
const result = await db.transaction(async (tx) => {
|
|
||||||
const [problemSet] = await tx.select().from(schema.problemset).where(and(
|
|
||||||
eq(schema.problemset.id, parsed.data.problemSetId), eq(schema.problemset.visible, true), ne(schema.problemset.status, "draft"),
|
|
||||||
)).limit(1)
|
|
||||||
if (!problemSet) return { error: "problem-set-not-found" as const }
|
|
||||||
const [progress] = await tx.select().from(schema.problemsetProgress).where(and(
|
|
||||||
eq(schema.problemsetProgress.problemsetId, problemSet.id), eq(schema.problemsetProgress.userId, user.id),
|
|
||||||
)).for("update").limit(1)
|
|
||||||
if (!progress) return { error: "not-joined" as const }
|
|
||||||
const [submission] = await tx.select().from(schema.submission).where(and(
|
|
||||||
eq(schema.submission.id, parsed.data.submissionId), eq(schema.submission.userId, user.id), eq(schema.submission.problemId, parsed.data.problemId),
|
|
||||||
)).limit(1)
|
|
||||||
if (!submission) return { error: "submission-not-found" as const }
|
|
||||||
if (![JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED].includes(submission.result as 0 | 10)) return { error: "submission-not-accepted" as const }
|
|
||||||
const [link] = await tx.select().from(schema.problemsetProblem).where(and(
|
|
||||||
eq(schema.problemsetProblem.problemsetId, problemSet.id), eq(schema.problemsetProblem.problemId, parsed.data.problemId),
|
|
||||||
)).limit(1)
|
|
||||||
if (!link) return { error: "problem-not-in-set" as const }
|
|
||||||
const detail = objectValue(progress.progressDetail)
|
|
||||||
detail[String(parsed.data.problemId)] = { score: link.score, submit_time: new Date().toISOString() }
|
|
||||||
const updated = await recomputeProgress(tx, progress, detail)
|
|
||||||
const [existingSubmission] = await tx.select({ id: schema.problemsetSubmission.id })
|
|
||||||
.from(schema.problemsetSubmission).where(and(
|
|
||||||
eq(schema.problemsetSubmission.problemsetId, problemSet.id),
|
|
||||||
eq(schema.problemsetSubmission.userId, user.id),
|
|
||||||
eq(schema.problemsetSubmission.problemId, parsed.data.problemId),
|
|
||||||
)).limit(1)
|
|
||||||
if (!existingSubmission) {
|
|
||||||
await tx.insert(schema.problemsetSubmission).values({
|
|
||||||
problemsetId: problemSet.id,
|
|
||||||
userId: user.id,
|
|
||||||
submissionId: submission.id,
|
|
||||||
problemId: parsed.data.problemId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const badges = await tx.select().from(schema.problemsetBadge).where(eq(schema.problemsetBadge.problemsetId, problemSet.id))
|
|
||||||
// 判定走 services/problemset.ts 那一份 —— 这里原来是第三份手抄的达标逻辑,
|
|
||||||
// 后台重算和补发脚本各有各的,改一处规则就会漏掉另外两处
|
|
||||||
const hits = badges.filter((badge) => eligibleForBadge(badge, updated))
|
|
||||||
if (hits.length === 0) return { earned: [] as (typeof schema.problemsetBadge.$inferSelect)[] }
|
|
||||||
// 达标的奖章一次插完,冲突忽略后 returning 回来的就是这次真拿到的
|
|
||||||
const inserted = await tx.insert(schema.userBadge).values(hits.map((badge) => ({
|
|
||||||
userId: user.id,
|
|
||||||
badgeId: badge.id,
|
|
||||||
earnedTime: new Date().toISOString(),
|
|
||||||
}))).onConflictDoNothing({ target: [schema.userBadge.badgeId, schema.userBadge.userId] })
|
|
||||||
.returning({ badgeId: schema.userBadge.badgeId })
|
|
||||||
const insertedIds = new Set(inserted.map((row) => row.badgeId))
|
|
||||||
return { earned: hits.filter((badge) => insertedIds.has(badge.id)) }
|
|
||||||
})
|
|
||||||
if ("error" in result && result.error) {
|
|
||||||
const error = result.error
|
|
||||||
const messages = {
|
|
||||||
"problem-set-not-found": "题单不存在",
|
|
||||||
"not-joined": "未加入该题单",
|
|
||||||
"submission-not-found": "提交记录不存在",
|
|
||||||
"submission-not-accepted": "只有通过的提交才能更新进度",
|
|
||||||
"problem-not-in-set": "题目不在题单中",
|
|
||||||
}
|
|
||||||
return failure(c, error.endsWith("not-found") ? 404 : 400, error, messages[error])
|
|
||||||
}
|
|
||||||
const unlocked = await updateAchievementsForProblemSet(user.id)
|
|
||||||
await Promise.all([
|
|
||||||
publishAchievementNotification(user.id, result.earned.map((badge) => ({
|
|
||||||
id: badge.id,
|
|
||||||
name: badge.name,
|
|
||||||
description: badge.description,
|
|
||||||
icon: badge.icon,
|
|
||||||
rarity: "bronze",
|
|
||||||
kind: "badge",
|
|
||||||
}))),
|
|
||||||
publishAchievementNotification(user.id, unlocked.map((achievement) => ({
|
|
||||||
id: achievement.id,
|
|
||||||
name: achievement.name,
|
|
||||||
description: achievement.description,
|
|
||||||
icon: achievement.icon,
|
|
||||||
rarity: achievement.rarity,
|
|
||||||
kind: "achievement",
|
|
||||||
}))),
|
|
||||||
])
|
|
||||||
return success(c, { earnedBadges: result.earned.map((badge) => badgeData(badge)) })
|
|
||||||
})
|
|
||||||
|
|
||||||
problemsetRoutes.get("/users/:username/badges", optionalAuth, async (c) => {
|
problemsetRoutes.get("/users/:username/badges", optionalAuth, async (c) => {
|
||||||
const requested = c.req.param("username")
|
const requested = c.req.param("username")
|
||||||
const username = requested === "me" ? c.get("user")?.username : requested
|
const username = requested === "me" ? c.get("user")?.username : requested
|
||||||
|
|||||||
@@ -71,11 +71,6 @@ export const joinProblemSetRequestSchema = z.object({
|
|||||||
problemSetId: z.number().int().positive(),
|
problemSetId: z.number().int().positive(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateProblemSetProgressRequestSchema = z.object({
|
|
||||||
problemSetId: z.number().int().positive(),
|
|
||||||
problemId: z.number().int().positive(),
|
|
||||||
submissionId: z.string().min(1),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const completedProblemSchema = z.object({
|
export const completedProblemSchema = z.object({
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
@@ -125,4 +120,3 @@ export type CompletedProblem = z.infer<typeof completedProblemSchema>
|
|||||||
|
|
||||||
export type ProblemSetUserProgressSummary = z.infer<typeof problemSetUserProgressSummarySchema>
|
export type ProblemSetUserProgressSummary = z.infer<typeof problemSetUserProgressSummarySchema>
|
||||||
export type JoinProblemSetRequest = z.infer<typeof joinProblemSetRequestSchema>
|
export type JoinProblemSetRequest = z.infer<typeof joinProblemSetRequestSchema>
|
||||||
export type UpdateProblemSetProgressRequest = z.infer<typeof updateProblemSetProgressRequestSchema>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user