Files
OJ2/apps/api/src/routes/trial-run.ts
T
xuyueandClaude Opus 5.5 11194543a3 feat(自学): 教程有了「所属项目」和课末的相关题,正文里的代码能直接运行
- 0031 迁移:tutorial 加 project 列(从正文「所属项目:」回填),新表 tutorial_problem
  挂每课的相关题(两头 CASCADE)
- 学完一课 = 练一练全对(契约 isLessonDone),不再按停留满 3 分钟算
- /tutorials/:id 带相关题和做对没有,/tutorials/:id/exercises 带每道做对没有
- /trial-runs 收 tutorialId:教程里的代码块走同一台判题机跑一组
- 后台:教程列表带练一练 / 相关题数 / 打开过的人,拖动排序、项目改名、挑题候选(必会题在前)
- 自学情况换成学生 × 课的方块格(/learn-analytics/board),默认班和排名页同一套猜法

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:40:34 +08:00

129 lines
5.1 KiB
TypeScript

import { TRIAL_MAX_CASES, trialRunRequestSchema, type TrialRunResponse } from "@oj2/contract"
import { and, eq, isNull } from "drizzle-orm"
import { Hono } from "hono"
import { requireAuth, type AppEnv } from "../auth/middleware"
import { db, schema } from "../db"
import { failure, parseBody, success } from "../http"
import { templateForLanguage } from "../judge/run"
import { parseProblemTemplate } from "../judge/template"
import { runTrial, TrialBusyError, TrialUserLimitError } from "../judge/trial"
import { canAccessContest, findAccessibleContest } from "../services/contest"
import { isAdminRole } from "./helpers"
export const trialRunRoutes = new Hono<AppEnv>()
/** 后台生成测试点时题目可能还没存,没有题目的限制可用,按这两个跑 */
const ADMIN_TIME_LIMIT = 3000
const ADMIN_MEMORY_LIMIT = 256
/** 自学教程里的代码块:都是几行的例子,给和普通题一样的限制,死循环早点停 */
const TUTORIAL_TIME_LIMIT = 1000
const TUTORIAL_MEMORY_LIMIT = 256
/**
* 试运行,见 judge/trial.ts。不落库、不算提交、不走令牌桶 ——
* 令牌桶是提交的,试运行扣它的话学生多点几次「运行例子」就交不上了;
* 这里的闸是 trial.ts 里的排队和每人两批的上限。
*/
trialRunRoutes.post("/trial-runs", requireAuth, async (c) => {
const parsed = await parseBody(c, trialRunRequestSchema, "试运行的参数不对")
if (!parsed.success) return parsed.response
const body = parsed.data
const user = c.get("user")!
const admin = isAdminRole(user)
if (!admin && body.cases.length > TRIAL_MAX_CASES) {
return failure(c, 400, "too-many-cases", `一次最多试运行 ${TRIAL_MAX_CASES} 组`)
}
let timeLimit = ADMIN_TIME_LIMIT
let memoryLimit = ADMIN_MEMORY_LIMIT
let source = body.code
let prependLines = 0
if (body.problemId === undefined && body.tutorialId !== undefined) {
// 教程正文里的代码块:只有一组输入(代码块下面那个输入框),教程得是公开的
if (body.cases.length !== 1) {
return failure(c, 400, "too-many-cases", "教程里的代码一次只能跑一组")
}
const [tutorial] = await db
.select({ isPublic: schema.tutorial.isPublic })
.from(schema.tutorial)
.where(eq(schema.tutorial.id, body.tutorialId))
.limit(1)
if (!tutorial || (!tutorial.isPublic && !admin)) {
return failure(c, 404, "tutorial-not-found", "教程不存在")
}
timeLimit = TUTORIAL_TIME_LIMIT
memoryLimit = TUTORIAL_MEMORY_LIMIT
} else if (body.problemId === undefined) {
if (!admin) return failure(c, 403, "permission-denied", "请从题目页试运行")
} else {
let contestId: number | null = null
if (body.contestId) {
const contest = await findAccessibleContest(user, body.contestId)
if (!contest) return failure(c, 404, "contest-not-found", "比赛不存在")
const access = await canAccessContest(c, contest, "problems")
if (!access.ok) return failure(c, 403, access.code, access.message)
contestId = contest.id
}
const [problem] = await db
.select({
languages: schema.problem.languages,
timeLimit: schema.problem.timeLimit,
memoryLimit: schema.problem.memoryLimit,
template: schema.problem.template,
visible: schema.problem.visible,
})
.from(schema.problem)
.where(
and(
eq(schema.problem.id, body.problemId),
contestId === null
? isNull(schema.problem.contestId)
: eq(schema.problem.contestId, contestId),
),
)
.limit(1)
// 管理员在后台预览没公开的题,也要能试运行
if (!problem || (!problem.visible && !admin)) {
return failure(c, 404, "problem-not-found", "题目不存在")
}
if (!problem.languages.includes(body.language)) {
return failure(c, 400, "language-not-allowed", `这道题不能用 ${body.language}`)
}
timeLimit = problem.timeLimit
memoryLimit = problem.memoryLimit
// 和提交一样套上题目的代码模板(judgeSubmission),不然有模板的题试运行和提交跑的不是同一份代码
const rawTemplate = templateForLanguage(problem.template, body.language)
const template = rawTemplate ? parseProblemTemplate(rawTemplate) : null
if (template) {
source = `${template.prepend}\n${body.code}\n${template.append}`
prependLines = template.prepend.split("\n").length
}
}
try {
const result = await runTrial({
userId: user.id,
language: body.language,
source,
code: body.code,
prependLines,
timeLimit,
memoryLimit,
cases: body.cases,
fullOutput: admin,
})
return success(c, result satisfies TrialRunResponse)
} catch (error) {
if (error instanceof TrialUserLimitError) {
return failure(c, 429, "trial-in-progress", "上一次还没跑完,稍等一下")
}
if (error instanceof TrialBusyError) {
return failure(c, 429, "trial-busy", "现在试运行的人太多了,过一会儿再点")
}
console.error("Trial run failed", error)
return failure(c, 502, "judge-unavailable", "判题机暂时连不上")
}
})