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:
1 parent
6d1bfc522d
commit
11194543a3
14 files changed
+4621
-268
No files matched your search
@@ -0,0 +1,70 @@
|
||||
import { and, asc, eq, inArray, isNull, sql } from "drizzle-orm"
|
||||
|
||||
import { db, schema } from "../db"
|
||||
|
||||
/** 「去做几道题」里优先推荐、要标出来的那批题挂的标签 */
|
||||
export const MUST_DO_TAG = "必会题"
|
||||
|
||||
const PROJECT_RE = /\*\*所属项目[::]\s*([^*\n]+)\*\*/
|
||||
|
||||
/**
|
||||
* 从正文第一段的「**所属项目:项目三 · 智慧农业·蜜橘园**」抽出项目名,抽不到给空串。
|
||||
* 和 0031 迁移回填用的是同一个写法;发布脚本不传 `project` 时靠它。
|
||||
*/
|
||||
export function projectFromContent(content: string) {
|
||||
return (content.match(PROJECT_RE)?.[1] ?? "").trim().slice(0, 64)
|
||||
}
|
||||
|
||||
/**
|
||||
* 放进 select 列表的关联子查询**一律手写表别名**:drizzle 在 select 列表里渲染列时
|
||||
* 会省掉表名,子查询里的 `"id"` 要么报 ambiguous,要么就近绑到子查询自己的表上 ——
|
||||
* 后一种不报错,只是数全错。外层表也写死成 `"problem"` / `"tutorial"`。
|
||||
*/
|
||||
|
||||
/** 这道题带不带「必会题」标签,做成 select 里的一列 */
|
||||
export const mustDoColumn = sql<boolean>`exists (
|
||||
select 1 from "problem_tags" pt join "problem_tag" t on t."id" = pt."problemtag_id"
|
||||
where pt."problem_id" = "problem"."id" and t."name" = ${MUST_DO_TAG}
|
||||
)`
|
||||
|
||||
/** 这道题的全部标签名 */
|
||||
export const problemTagNames = sql<string[]>`coalesce((
|
||||
select array_agg(t."name" order by t."name")
|
||||
from "problem_tags" pt join "problem_tag" t on t."id" = pt."problemtag_id"
|
||||
where pt."problem_id" = "problem"."id"
|
||||
), '{}')`
|
||||
|
||||
/** 一课有几道练一练 / 挂了几道题 */
|
||||
export const exerciseCountColumn = sql<number>`(
|
||||
select count(*) from "exercise" e where e."tutorial_id" = "tutorial"."id"
|
||||
)`.mapWith(Number)
|
||||
export const problemCountColumn = sql<number>`(
|
||||
select count(*) from "tutorial_problem" tp where tp."tutorial_id" = "tutorial"."id"
|
||||
)`.mapWith(Number)
|
||||
|
||||
/**
|
||||
* 几课各挂了哪些题,按老师排的顺序。`publicOnly` 给学生端用:没公开的题、比赛题不出现
|
||||
* (老师挑的时候公开、后来藏起来了的,学生点进去只会 404)。
|
||||
*/
|
||||
export async function tutorialProblems(tutorialIds: number[], publicOnly: boolean) {
|
||||
if (tutorialIds.length === 0) return []
|
||||
return db
|
||||
.select({
|
||||
tutorialId: schema.tutorialProblem.tutorialId,
|
||||
id: schema.problem.id,
|
||||
displayId: schema.problem.displayId,
|
||||
title: schema.problem.title,
|
||||
mustDo: mustDoColumn,
|
||||
})
|
||||
.from(schema.tutorialProblem)
|
||||
.innerJoin(schema.problem, eq(schema.problem.id, schema.tutorialProblem.problemId))
|
||||
.where(
|
||||
and(
|
||||
inArray(schema.tutorialProblem.tutorialId, tutorialIds),
|
||||
publicOnly
|
||||
? and(eq(schema.problem.visible, true), isNull(schema.problem.contestId))
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
.orderBy(asc(schema.tutorialProblem.tutorialId), asc(schema.tutorialProblem.order))
|
||||
}
|
||||
Reference in new issue
Block a user