Files
OJ2/apps/api/src/services/tutorial.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

71 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}