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
@@ -11,7 +11,7 @@ import {
|
||||
type SampleUser,
|
||||
type WeeklyChampion,
|
||||
} from "@oj2/contract"
|
||||
import { and, countDistinct, desc, eq, gte, inArray, isNotNull, ne } from "drizzle-orm"
|
||||
import { and, eq, inArray, isNotNull } from "drizzle-orm"
|
||||
import { Hono } from "hono"
|
||||
|
||||
import { optionalAuth, requireAuth, requireTeacher, type AppEnv } from "../auth/middleware"
|
||||
@@ -23,10 +23,10 @@ import {
|
||||
classBattle,
|
||||
classDetail,
|
||||
classPk,
|
||||
defaultClassFor,
|
||||
loadEntrants,
|
||||
PK_MAX_CLASSES,
|
||||
pkPartner,
|
||||
rankedStudents,
|
||||
standings,
|
||||
WEEK_MS,
|
||||
type Standing,
|
||||
@@ -73,48 +73,6 @@ function periodStart(period: RankPeriod) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 老师打开排名页默认看哪个班:自己最近布置过作业的班 → 最近一周交题人数最多的班。
|
||||
* 不记老师上次选的:统计面板吃过那个亏(上一节课的班悄悄留在框里)。
|
||||
*/
|
||||
async function defaultClassFor(teacherId: number) {
|
||||
const [lesson] = await db
|
||||
.select({ className: schema.classLesson.className })
|
||||
.from(schema.classLesson)
|
||||
.where(eq(schema.classLesson.createdBy, teacherId))
|
||||
.orderBy(desc(schema.classLesson.day), desc(schema.classLesson.updatedAt))
|
||||
.limit(1)
|
||||
if (lesson) return lesson.className
|
||||
|
||||
const users = countDistinct(schema.submission.userId)
|
||||
const [busiest] = await db
|
||||
.select({ className: schema.user.className, users })
|
||||
.from(schema.submission)
|
||||
.innerJoin(schema.user, eq(schema.user.id, schema.submission.userId))
|
||||
.where(
|
||||
and(
|
||||
rankedStudents,
|
||||
isNotNull(schema.user.className),
|
||||
ne(schema.user.className, ""),
|
||||
gte(schema.submission.createTime, new Date(Date.now() - WEEK_MS).toISOString()),
|
||||
),
|
||||
)
|
||||
.groupBy(schema.user.className)
|
||||
.orderBy(desc(users))
|
||||
.limit(1)
|
||||
if (busiest?.className) return busiest.className
|
||||
|
||||
// 放假回来第一次打开:一周里没人交过题,就看最后一个交题的学生是哪个班
|
||||
const [latest] = await db
|
||||
.select({ className: schema.user.className })
|
||||
.from(schema.submission)
|
||||
.innerJoin(schema.user, eq(schema.user.id, schema.submission.userId))
|
||||
.where(and(rankedStudents, isNotNull(schema.user.className), ne(schema.user.className, "")))
|
||||
.orderBy(desc(schema.submission.createTime))
|
||||
.limit(1)
|
||||
return latest?.className ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 这次请求看的是哪个班 / 哪个年级。学生只能看自己的班和年级;老师用 `className` 选,
|
||||
* 不给就猜(`defaultClassFor`)。返回 undefined 表示该报「没有班级」。
|
||||
|
||||
Reference in new issue
Block a user