feat(题目): 「运行例子」「自己输入」改用和提交同一台判题机——例子上跑出什么样,提交在这几个例子上就是什么样
Deploy / deploy (push) Canceled after 0s

原来试运行走另一台 Judge0:C 是 GCC 9、编译参数也不一样,Python 的语法错误回来的是运行错误,
没有题目的时间内存限制和代码模板,例子上的结果常和提交对不上。现在走本站判题机
(POST /trial-runs),同一套编译命令、这道题的限制和模板,对不对也由判题机比。

- 不落库、不算提交、不进判题队列:api 进程里自己排队,全站同时 TRIAL_CONCURRENCY 批
  (服务器 2、机房 4),每人最多两批,排满或等超过 30 秒就请学生过一会儿再点
- 死循环不停打印时判题机回包会很大,超过 16MB 就掐断,告诉学生输出太多、停不下来
- Python 语法错误用和提交一样的说明:第几行、中文标点标红、一键换成英文
- 运行时错误和提交用同一个解析,说清楚第几行、为什么(下标越界、int() 转换失败、段错误……);
  比赛里照旧只给按结果码的那一句
- 语法没过、服务没跑成时,标题不再说「N 个例子没对上」
- 后台「生成测试点」一并迁过来,编译警告不会再写进 .out;Judge0 的地址和调用全部删掉

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-29 08:09:44 +08:00
1 parent 8834536135
commit eb32676de4
27 files changed
+691 -169

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
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
/**
* 试运行,见 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) {
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", "判题机暂时连不上")
}
})