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>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-12 09:40:34 +08:00
1 parent 6d1bfc522d
commit 11194543a3
14 files changed
+4621 -268

No files matched your search

+19 -1
View File
@@ -16,6 +16,9 @@ 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。不落库、不算提交、不走令牌桶 ——
@@ -37,7 +40,22 @@ trialRunRoutes.post("/trial-runs", requireAuth, async (c) => {
let memoryLimit = ADMIN_MEMORY_LIMIT
let source = body.code
let prependLines = 0
if (body.problemId === undefined) {
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