Files
OJ2/apps/api/src/routes/task.ts
T
xuyueandClaude Opus 5.5 29c31640b5 refactor(课堂任务): 「课堂看板」改名「课堂任务」,地址换成 /task;班级 PK 换成 /pk
- 界面上的「课堂看板」「课堂作业」统一叫「课堂任务」,顶栏、老师首页、后台菜单一起改
- 前端 oj/classroom → oj/task、oj/class → oj/pk,两个名字不再只差几个字母却毫不相干;旧地址不留跳转
- 后端 routes/classroom.ts 拆开:课堂任务的接口进 routes/task.ts(/task/board|lesson|today),
  首页的「上次来」「班里在做」进 routes/home.ts;共用的取题口径挪进 services/lesson.ts
- 契约 classroom.ts 同样拆成 task.ts / home.ts,ClassBoard* 改叫 TaskBoard*

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:53:07 +08:00

551 lines
26 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 {
STUDENT_ROLES,
classLessonRequestSchema,
type ClassToday,
type TaskBoard,
type TaskBoardStudent,
type ClassLesson,
lessonNeeds,
type StatisticInfo,
} from '@oj2/contract'
import { and, desc, eq, gte, inArray, isNotNull, isNull, sql } from 'drizzle-orm'
import { Hono } from 'hono'
import { requireTeacher, type AppEnv } from '../auth/middleware'
import { seenToday } from '../auth/presence'
import { presenceOf } from '../collab/state'
import { db, schema } from '../db'
import { challengeSuccessesOf } from '../services/challenge'
import { failure, parseBody, success } from '../http'
import { isAccepted } from '../judge/status'
import { activityRows, classMemberIds, FLOWCHART_PASSED, lessonLanguageFilter, SOLVED_RESULTS, suggestActiveClass } from '../services/class-activity'
import {
announceLessonChange,
CLASS_ACTIVITY_MIN_USERS,
classDayGroups,
classLessonProblems,
lessonDoneCounts,
resolveLessonProblems,
todayLessonSnapshot,
} from '../services/lesson'
import { calendarDay, dayStart, localTime } from '../time'
/**
* 课堂任务(老师):布置这节课的题、看全班做到哪、老师首页的「今天的课」。
* 「这节课是哪几道题」的取法在 services/lesson.ts,学生首页的「班里在做」也用它(routes/home.ts)
*/
export const taskRoutes = new Hono<AppEnv>()
/**
* 一张流程图走对了几组、一共几组:2026-10 起的「走例子」报告(ai_criteria_details version 2,
* 形状见契约 FlowchartCaseReport)里数。老评法、还没评完的、评失败的都是 null
*/
function flowchartCaseCounts() {
const details = schema.flowchartSubmission.aiCriteriaDetails
const isReport = sql`${schema.flowchartSubmission.status} = 2 and ${details}->>'version' = '2' and jsonb_typeof(${details}->'cases') = 'array'`
return {
passed: sql<number | null>`case when ${isReport} then (select count(*) from jsonb_array_elements(${details}->'cases') e where e->>'ok' = 'true')::int end`,
total: sql<number | null>`case when ${isReport} then jsonb_array_length(${details}->'cases') end`,
}
}
/**
* 任务页上的「最近几节课交了几节」:分清「今天没来」和「一直不动手」。2025 秋至少上过
* 5 节课的学生里,12.5% 七成以上的课一道都没交,37% 的学生贡献了一半以上的「没交」
* —— 当堂那份名单把这两种人排在同一行里,老师分不出来。
*/
const RECENT_LESSONS = 5
/** 往回找最近几节课时最多看多远:寒暑假之后开学第一节课,前面几节是上学期的,没意义 */
const RECENT_LESSONS_WINDOW_DAYS = 45
/**
* 课堂任务:这个班、今天、这节课的几道题 × 全班学生。老师在课上用它看谁还没开始、
* 谁卡住了 —— 2025 秋平均每节课有 14 个平时在用的学生一道都没交(占三分之一),
* 而交了 3 次以上还没过的平均不到 1 个,所以「还没开始」才是这张表的重点,排序在前端。
*/
taskRoutes.get('/task/board', requireTeacher, async (c) => {
const day = calendarDay()
const className = c.req.query('className')?.trim() || (await suggestActiveClass())
if (!className) {
return success(c, {
className: null,
day,
source: null,
lessonId: null,
language: null,
mode: null,
assignedAt: null,
assignedBy: null,
lastLanguage: null,
problems: [],
students: [],
recentLessons: 0,
} satisfies TaskBoard)
}
// 推断只看今天(回看 1 天):任务页是给这节课用的,昨天的题不该冒出来。
// lastLanguage 给语言下拉当默认值:这个班上一次布置选的什么(一个班一般一学期只学一种)
const [lesson, [last]] = await Promise.all([
classLessonProblems(className, 1),
db
.select({ language: schema.classLesson.language })
.from(schema.classLesson)
.where(and(eq(schema.classLesson.className, className), isNotNull(schema.classLesson.language)))
.orderBy(desc(schema.classLesson.day), desc(schema.classLesson.createdAt))
.limit(1),
])
const ids = lesson.problems.map((problem) => problem.id)
const start = dayStart()
const roster = await db
.select({
userId: schema.user.id,
username: schema.user.username,
realName: schema.userProfile.realName,
})
.from(schema.user)
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
.where(and(eq(schema.user.className, className), eq(schema.user.isDisabled, false), inArray(schema.user.adminType, [...STUDENT_ROLES])))
const userIds = roster.map((row) => row.userId)
// 最近几节课:这个班同学一起做题的那些天(不含今天)
const windowStart = dayStart(Date.now() - RECENT_LESSONS_WINDOW_DAYS * 86_400_000)
const recentDays = [...new Set((await classDayGroups(className, windowStart, { minUsers: CLASS_ACTIVITY_MIN_USERS })).map((row) => row.day))]
.filter((d) => d < day)
.sort()
.slice(-RECENT_LESSONS)
// 「来了几次」代码和流程图合起来数:那天只画了流程图的也算来过,和 recentDays 同一个口径
const recentActivity = activityRows({ since: windowStart, users: userIds })
const localDay = sql<string>`to_char(${localTime(recentActivity.createTime)}, 'YYYY-MM-DD')`
const [challengeConfigs, challengeDone] = await Promise.all([
ids.length ? db.select({ id: schema.problem.id, challenges: schema.problem.challenges }).from(schema.problem).where(inArray(schema.problem.id, ids)) : [],
challengeSuccessesOf(userIds, ids),
])
// 只认现在还配着的挑战:规则改过的旧 id 不算
const liveChallenges = new Map(challengeConfigs.map((row) => [row.id, new Set(Object.values(row.challenges ?? {}).flatMap((list) => list.map((c) => c.id)))]))
const challengeDoneKeys = new Set(
challengeDone.filter((row) => liveChallenges.get(row.problemId)?.has(row.challengeId)).map((row) => `${row.userId}:${row.problemId}`),
)
// 老师布置的这节做什么;推断出来的课为 null,代码交对或流程图画对都算(和加「做什么」之前一样)
const mode = lesson.source === 'teacher' ? lesson.mode : null
const needs = mode ? lessonNeeds(mode) : null
const [cells, lastSubmits, lastDrawn, attendance, drawn, todays, seen, lastFlows] = await Promise.all([
ids.length && userIds.length
? db
.select({
userId: schema.submission.userId,
problemId: schema.submission.problemId,
attempts: sql<number>`count(*) filter (where ${gte(schema.submission.createTime, start)})::int`,
firstAcceptedAt: sql<string | null>`min(${schema.submission.createTime}) filter (where ${inArray(schema.submission.result, SOLVED_RESULTS)})`,
})
.from(schema.submission)
.where(
and(
isNull(schema.submission.contestId),
inArray(schema.submission.userId, userIds),
inArray(schema.submission.problemId, ids),
lessonLanguageFilter(lesson.language),
),
)
.groupBy(schema.submission.userId, schema.submission.problemId)
: [],
userIds.length
? db
.select({
userId: schema.submission.userId,
lastAt: sql<string>`max(${schema.submission.createTime})`,
// 非比赛的最后一次,给「点名字跳提交列表」判断用(列表只列非比赛的)
lastPublicAt: sql<string | null>`max(${schema.submission.createTime}) filter (where ${isNull(schema.submission.contestId)})`,
})
.from(schema.submission)
.where(and(inArray(schema.submission.userId, userIds), gte(schema.submission.createTime, start)))
.groupBy(schema.submission.userId)
: [],
// 今天只画了流程图的学生,「最后一次提交」也得算上画图,不然他在任务页上是一直没动手。
// 和上面代码那条同一个范围:今天、所有题、不论评没评完
userIds.length
? db
.select({
userId: schema.flowchartSubmission.userId,
lastAt: sql<string>`max(${schema.flowchartSubmission.createTime})`,
})
.from(schema.flowchartSubmission)
.where(and(inArray(schema.flowchartSubmission.userId, userIds), gte(schema.flowchartSubmission.createTime, start)))
.groupBy(schema.flowchartSubmission.userId)
: [],
recentDays.length && userIds.length
? db
.select({
userId: recentActivity.userId,
days: sql<number>`count(distinct ${localDay})::int`,
})
.from(recentActivity)
.where(inArray(localDay, recentDays))
.groupBy(recentActivity.userId)
: [],
/**
* 流程图作业:画到 A / S 也算这道题做完,和学生首页的课堂条同一个口径(/me/class-activity)。
* 有的题这节课整个班都在画流程图,只数代码提交的话,任务页上满屏「没开始」。
* 和代码那条(cells)同一个范围:交过就算尝试,评分中、评失败的也算
*/
ids.length && userIds.length
? db
.select({
userId: schema.flowchartSubmission.userId,
problemId: schema.flowchartSubmission.problemId,
attempts: sql<number>`count(*) filter (where ${gte(schema.flowchartSubmission.createTime, start)})::int`,
firstPassedAt: sql<string | null>`min(${schema.flowchartSubmission.createTime}) filter (where ${FLOWCHART_PASSED})`,
})
.from(schema.flowchartSubmission)
.where(and(inArray(schema.flowchartSubmission.userId, userIds), inArray(schema.flowchartSubmission.problemId, ids)))
.groupBy(schema.flowchartSubmission.userId, schema.flowchartSubmission.problemId)
: [],
/**
* 每人每题今天交的每一次代码,从早到晚。最后一次给没做对的格子点开讲错误、按错法归堆,
* 整串给做对了的格子看轨迹。范围和 cells 一样(非比赛、布置的语言),只多一个「今天」
*/
ids.length && userIds.length
? db
.select({
userId: schema.submission.userId,
problemId: schema.submission.problemId,
id: schema.submission.id,
result: schema.submission.result,
createTime: schema.submission.createTime,
runtimeError: sql<StatisticInfo['runtime_error'] | null>`${schema.submission.statisticInfo}->'runtime_error'`,
sampleCheck: sql<StatisticInfo['sample_check'] | null>`${schema.submission.statisticInfo}->'sample_check'`,
})
.from(schema.submission)
.where(
and(
isNull(schema.submission.contestId),
inArray(schema.submission.userId, userIds),
inArray(schema.submission.problemId, ids),
gte(schema.submission.createTime, start),
lessonLanguageFilter(lesson.language),
),
)
.orderBy(schema.submission.createTime)
: [],
seenToday(userIds),
/**
* 要画流程图的课:每人每题最后一张图(不限今天,和 drawn 的 firstPassedAt 同一个范围),
* 还没画对的格子点开讲它哪里错、按走对几组归堆。走对几组从 2026-10 起的「走例子」报告里数,
* 老评法和还没评完的为 null
*/
needs?.draw && ids.length && userIds.length
? db
.selectDistinctOn([schema.flowchartSubmission.userId, schema.flowchartSubmission.problemId], {
userId: schema.flowchartSubmission.userId,
problemId: schema.flowchartSubmission.problemId,
id: schema.flowchartSubmission.id,
status: schema.flowchartSubmission.status,
grade: schema.flowchartSubmission.aiGrade,
...flowchartCaseCounts(),
})
.from(schema.flowchartSubmission)
.where(and(inArray(schema.flowchartSubmission.userId, userIds), inArray(schema.flowchartSubmission.problemId, ids)))
.orderBy(schema.flowchartSubmission.userId, schema.flowchartSubmission.problemId, desc(schema.flowchartSubmission.createTime))
: [],
])
const lastFlowByKey = new Map(lastFlows.map((row) => [`${row.userId}:${row.problemId}`, row]))
const todayByKey = Map.groupBy(todays, (row) => `${row.userId}:${row.problemId}`)
const cellByKey = new Map(cells.map((row) => [`${row.userId}:${row.problemId}`, row]))
const drawnByKey = new Map(drawn.map((row) => [`${row.userId}:${row.problemId}`, row]))
const lastByUser = new Map(lastSubmits.map((row) => [row.userId, row.lastAt]))
const codeTodayUsers = new Set(lastSubmits.filter((row) => row.lastPublicAt).map((row) => row.userId))
const drawnTodayUsers = new Set(lastDrawn.map((row) => row.userId))
for (const row of lastDrawn) {
const known = lastByUser.get(row.userId)
if (!known || Date.parse(row.lastAt) > Date.parse(known)) lastByUser.set(row.userId, row.lastAt)
}
const earliest = (a: string | null | undefined, b: string | null | undefined) => (!a ? (b ?? null) : !b ? a : Date.parse(a) <= Date.parse(b) ? a : b)
const latestOf = (a: string, b: string) => (Date.parse(a) >= Date.parse(b) ? a : b)
type Step = 'accepted' | 'tried' | 'none'
const attendedByUser = new Map(attendance.map((row) => [row.userId, row.days]))
return success(c, {
className,
day,
source: lesson.source,
lessonId: lesson.lessonId,
language: lesson.language,
mode,
assignedAt: lesson.assignedAt,
assignedBy: lesson.assignedBy,
lastLanguage: last?.language ?? null,
problems: lesson.problems.map((problem) => ({
problemId: problem.id,
problemDisplayId: problem.displayId,
title: problem.title,
isSql: problem.isSql,
hasChallenges: (liveChallenges.get(problem.id)?.size ?? 0) > 0,
})),
students: roster.map(
(student) =>
({
userId: student.userId,
username: student.username,
realName: student.realName ?? null,
cells: ids.map((id) => {
const key = `${student.userId}:${id}`
const cell = cellByKey.get(key)
const flow = drawnByKey.get(key)
const codeAt = cell?.firstAcceptedAt ?? null
const flowAt = flow?.firstPassedAt ?? null
const codeStatus: Step = codeAt ? 'accepted' : cell ? 'tried' : 'none'
const drawStatus: Step = flowAt ? 'accepted' : flow ? 'tried' : 'none'
let status: Step
let acceptedAt: string | null
let attempts: number
if (!needs) {
acceptedAt = earliest(codeAt, flowAt)
status = !cell && !flow ? 'none' : acceptedAt ? 'accepted' : 'tried'
attempts = (cell?.attempts ?? 0) + (flow?.attempts ?? 0)
} else {
// 要哪几步看哪几步:两步都做到的时刻是晚的那一步
const steps = [needs.draw ? { status: drawStatus, at: flowAt } : null, needs.code ? { status: codeStatus, at: codeAt } : null].filter(
(step) => step !== null,
)
acceptedAt = steps.every((step) => step.at) ? steps.map((step) => step.at!).reduce(latestOf) : null
status = acceptedAt ? 'accepted' : steps.some((step) => step.status !== 'none') ? 'tried' : 'none'
attempts = (needs.code ? (cell?.attempts ?? 0) : 0) + (needs.draw ? (flow?.attempts ?? 0) : 0)
}
// 下面几样都是代码那一步的:只画流程图的课没有
const codeWanted = !needs || needs.code
const today = codeWanted ? (todayByKey.get(key) ?? []) : []
const codeDone = needs ? codeAt : acceptedAt
const latest = codeDone ? undefined : today.at(-1)
const firstTry = codeDone && Date.parse(codeDone) >= Date.parse(start) ? today.findIndex((row) => isAccepted(row.result)) : -1
const lastFlow = lastFlowByKey.get(key)
return {
status,
attempts,
codeAttempts: cell?.attempts ?? 0,
flowchartAttempts: flow?.attempts ?? 0,
acceptedAt,
codeStatus,
flowchart: needs?.draw
? {
status: drawStatus,
passedAt: flowAt,
latest:
!flowAt && lastFlow
? { id: lastFlow.id, status: lastFlow.status, grade: lastFlow.grade, passed: lastFlow.passed, total: lastFlow.total }
: null,
}
: null,
latest: latest
? {
submissionId: latest.id,
result: latest.result,
runtimeError: latest.runtimeError ?? undefined,
sampleCheck: latest.sampleCheck ?? undefined,
}
: null,
trail: codeDone
? today.map((row) => ({
id: row.id,
result: row.result,
createTime: row.createTime,
}))
: [],
acceptedTry: firstTry >= 0 ? firstTry + 1 : null,
challenge:
!codeWanted || !codeDone || !liveChallenges.get(id)?.size
? null
: challengeDoneKeys.has(key)
? 'done'
: today.some((row) => Date.parse(row.createTime) > Date.parse(codeDone))
? 'doing'
: null,
}
}),
lastSubmitAt: lastByUser.get(student.userId) ?? null,
codeToday: codeTodayUsers.has(student.userId),
drawnToday: drawnTodayUsers.has(student.userId),
recentAttended: attendedByUser.get(student.userId) ?? 0,
editing: presenceOf(student.userId)?.problemId ?? null,
online: seen.get(student.userId)?.online ?? false,
seenAt: seen.has(student.userId) ? new Date(seen.get(student.userId)!.at).toISOString() : null,
}) satisfies TaskBoardStudent,
),
recentLessons: recentDays.length,
} satisfies TaskBoard)
})
/**
* 老师首页「今天的课」:今天在上课的每个班一行。在上课 = 同班 ≥5 人今天一起交过题(和「班里在做」
* 同一个门槛),或者老师今天在任务页上布置过题(刚布置、还没人交也要列出来)。每个班的题和人数
* 和任务页同一套取法(classLessonProblems),「全做对」看这几道题做没做对过,不限今天
*/
taskRoutes.get('/task/today', requireTeacher, async (c) => {
const start = dayStart()
const today = calendarDay()
const activity = activityRows({ since: start })
const users = sql<number>`count(distinct ${activity.userId})::int`
const lastAt = sql<string>`max(${activity.createTime})`
const [active, lessons] = await Promise.all([
db
.select({ className: schema.user.className, users, lastAt })
.from(activity)
.innerJoin(schema.user, eq(schema.user.id, activity.userId))
.where(and(isNotNull(schema.user.className), eq(schema.user.isDisabled, false), inArray(schema.user.adminType, [...STUDENT_ROLES])))
.groupBy(schema.user.className),
db.select({ className: schema.classLesson.className }).from(schema.classLesson).where(eq(schema.classLesson.day, today)),
])
const activeByClass = new Map(active.filter((row) => row.className && row.users >= CLASS_ACTIVITY_MIN_USERS).map((row) => [row.className!, row]))
const names = [...new Set([...activeByClass.keys(), ...lessons.map((row) => row.className)])]
const classes = await Promise.all(
names.map(async (className) => {
const [lesson, [roster]] = await Promise.all([
classLessonProblems(className, 1),
db
.select({ value: sql<number>`count(*)::int` })
.from(schema.user)
.where(and(eq(schema.user.className, className), eq(schema.user.isDisabled, false), inArray(schema.user.adminType, [...STUDENT_ROLES]))),
])
const ids = lesson.problems.map((problem) => problem.id)
let allDoneCount = 0
if (ids.length && lesson.source === 'teacher' && lesson.lessonId && lesson.mode) {
// 老师布置的:和任务页、后台课堂任务同一个口径(要画的看图、要写的看代码)
const counts = await lessonDoneCounts([
{ id: lesson.lessonId, className, day: calendarDay(), problemIds: ids, language: lesson.language, mode: lesson.mode },
])
allDoneCount = counts.get(lesson.lessonId) ?? 0
} else if (ids.length) {
// 推断出来的:做没做对不限今天(和任务页的格子一样:以前做对的也算做完),代码或流程图都算
const done = activityRows({
since: new Date(0).toISOString(),
users: classMemberIds(className),
problemIds: ids,
})
const rows = await db
.select({
userId: done.userId,
solved: sql<number>`count(distinct ${done.problemId})::int`,
})
.from(done)
.where(sql`${done.solved}`)
.groupBy(done.userId)
allDoneCount = rows.filter((row) => row.solved >= ids.length).length
}
const today = activeByClass.get(className)
return {
className,
source: lesson.source,
assignedAt: lesson.assignedAt,
assignedBy: lesson.assignedBy,
problems: lesson.problems.map((problem) => ({
problemDisplayId: problem.displayId,
title: problem.title,
userCount: problem.userCount,
acceptedCount: problem.acceptedCount,
})),
activeCount: today?.users ?? 0,
rosterCount: roster?.value ?? 0,
allDoneCount,
lastSubmitAt: today?.lastAt ? new Date(today.lastAt).toISOString() : null,
}
}),
)
// 正在交的在前,只布置了还没人交的放最后
classes.sort((a, b) => (b.lastSubmitAt ?? '').localeCompare(a.lastSubmitAt ?? ''))
return success(c, { classes } satisfies ClassToday)
})
/**
* 提交列表「这节课」按钮:这个班这节课是哪几道题,取法同课堂任务(老师布置的优先,
* 否则按今天同班提交推断)。不传班级就猜最近两小时在交题的班
*/
taskRoutes.get('/task/lesson', requireTeacher, async (c) => {
const className = c.req.query('className')?.trim() || (await suggestActiveClass())
if (!className)
return success(c, {
className: null,
source: null,
language: null,
problems: [],
} satisfies ClassLesson)
// 和任务页一样只回看今天:「这节课」不该冒出昨天的题
const lesson = await classLessonProblems(className, 1)
// C / Python 的作业夹了 SQL 题就不筛语言(见契约 classLessonSchema.language)
const mixed = lesson.problems.some((problem) => problem.isSql !== (lesson.language === 'SQL'))
return success(c, {
className,
source: lesson.source,
language: mixed ? null : lesson.language,
problems: lesson.problems.map((problem) => ({
problemDisplayId: problem.displayId,
title: problem.title,
})),
} satisfies ClassLesson)
})
/**
* 老师给这个班布置今天的一节课。按展示题号给、不分大小写,存 problem.id 并保留输入顺序;
* 有对不上的题号就整个拒掉并点名是哪几个,免得存下半张单子老师还不知道。空数组 = 清掉这一节,
* 今天一节都不剩时学生那边退回推断。改哪一节、还是新开一节,见契约 classLessonRequestSchema。
*/
taskRoutes.put('/task/lesson', requireTeacher, async (c) => {
const parsed = await parseBody(c, classLessonRequestSchema, '题号或语言不对')
if (!parsed.success) return parsed.response
const user = c.get('user')!
const { className, problemDisplayIds, lessonId, newLesson, mode } = parsed.data
// 只画流程图不用语言:存成 null,任务页和学生那边就不会写「C语言」
const language = mode === 'flowchart' ? null : parsed.data.language
const day = calendarDay()
const [exists] = await db.select({ id: schema.user.id }).from(schema.user).where(eq(schema.user.className, className)).limit(1)
if (!exists) return failure(c, 400, 'class-not-found', `没有 ${className} 这个班`)
// 改之前学生看到的那一节,改完比一比、告诉全班改了哪些
const before = await todayLessonSnapshot(className)
// 要改的是哪一行:点名的那一节(只认这个班今天的),新开一节就没有,老写法是今天最后布置的那一节
const [target] = newLesson
? []
: await db
.select({ id: schema.classLesson.id })
.from(schema.classLesson)
.where(
and(
eq(schema.classLesson.className, className),
eq(schema.classLesson.day, day),
lessonId === undefined ? undefined : eq(schema.classLesson.id, lessonId),
),
)
.orderBy(desc(schema.classLesson.createdAt), desc(schema.classLesson.id))
.limit(1)
if (!problemDisplayIds.length) {
if (target) await db.delete(schema.classLesson).where(eq(schema.classLesson.id, target.id))
await announceLessonChange(className, before)
return success(c, null)
}
const resolved = await resolveLessonProblems(problemDisplayIds, language, mode)
if ('code' in resolved) return failure(c, 400, resolved.code, resolved.message)
const { problemIds } = resolved
const now = new Date().toISOString()
if (target) {
// 「改」不换布置的人,和 createdAt 一样认第一次
await db.update(schema.classLesson).set({ problemIds, language, mode, updatedAt: now }).where(eq(schema.classLesson.id, target.id))
} else {
// 新开一节、今天还没有,或者点名的那一节已经没了(别的老师清掉了)—— 最后这种也照新开处理,
// 老师手上的题号不至于白填
await db.insert(schema.classLesson).values({
className,
day,
problemIds,
language,
mode,
createdBy: user.id,
createdAt: now,
updatedAt: now,
})
}
await announceLessonChange(className, before)
return success(c, null)
})