fix(课堂任务): 先画一节、再用这几道题写代码时,老师这边也认没画图的还没做完
- 任务页格子、老师首页「全做对」和每道题做对几人,按今天几节里同一道题合起来的要求算 (和学生那边同一份口径,挪进 services/lesson.ts 的 todayNeeds):写代码那节只交对了代码、 没画图的,格子是「缺图」,不再打 ✓。后台和数据统计还是一节一行、各数各的 - 学生课堂条上「几人做对」也按合起来的要求数,不再把自己没做完的算进去 - 先画再写只交对了代码、图还没画的格子,不再挂挑战的圈 - 题目页标题下:流程图没画到 A 也写「流程图 C 级」(橙色),不再写「还没交过」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c3f1413545
commit
91057c1096
6 files changed
+144
-100
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { type ClassActivity, type ClassActivityProblem, type LastVisit, type LessonLanguage, type LessonMode, lessonNeeds } from '@oj2/contract'
|
||||
import { and, asc, desc, eq, gte, inArray, isNull, lt, sql } from 'drizzle-orm'
|
||||
import { type ClassActivity, type ClassActivityProblem, type LastVisit } from '@oj2/contract'
|
||||
import { and, asc, eq, gte, inArray, isNull, lt, sql } from 'drizzle-orm'
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { requireAuth, type AppEnv } from '../auth/middleware'
|
||||
@@ -9,7 +9,6 @@ import { success } from '../http'
|
||||
import { FLOWCHART_PASSED, lessonLanguageFilter, SOLVED_RESULTS } from '../services/class-activity'
|
||||
import { accepted } from '../services/learning-stats'
|
||||
import { CLASS_ACTIVITY_LOOKBACK_DAYS, classLessonProblems } from '../services/lesson'
|
||||
import { calendarDay } from '../time'
|
||||
|
||||
/**
|
||||
* 首页上和「这节课」有关的两张卡:学生的「上次来」和「班里在做」。
|
||||
@@ -145,7 +144,7 @@ homeRoutes.get('/me/class-activity', requireAuth, async (c) => {
|
||||
|
||||
const lesson = await classLessonProblems(user.className, CLASS_ACTIVITY_LOOKBACK_DAYS)
|
||||
const ids = lesson.problems.map((problem) => problem.id)
|
||||
const steps = lesson.source === 'teacher' && lesson.mode ? await todaySteps(user.className, ids, lesson) : null
|
||||
const steps = lesson.steps
|
||||
const codeLanguage = steps ? steps.language : lesson.language
|
||||
// 代码提交和流程图提交两张表,互不依赖,一起查
|
||||
const [mine, drawn] = await Promise.all([
|
||||
@@ -193,7 +192,7 @@ homeRoutes.get('/me/class-activity', requireAuth, async (c) => {
|
||||
problems: lesson.problems.map((problem) => {
|
||||
const code = stepOf(codeById.get(problem.id))
|
||||
const draw = stepOf(drawById.get(problem.id))
|
||||
const needs = steps?.needs.get(problem.id)
|
||||
const needs = steps?.needsOf(problem.id)
|
||||
let myStatus: Step
|
||||
if (needs) {
|
||||
// 老师布置的:要哪几步就看哪几步,全做到才算做完
|
||||
@@ -215,39 +214,3 @@ homeRoutes.get('/me/class-activity', requireAuth, async (c) => {
|
||||
}),
|
||||
} satisfies ClassActivity)
|
||||
})
|
||||
|
||||
/**
|
||||
* 学生那边一道题要画还是要写:今天这个班布置过的每一节里,凡是有这道题的,要求合起来算。
|
||||
* 老师先布置一节画流程图、再布置一节同样的题写代码时,还没画完的人照样看得到要画 ——
|
||||
* 学生只看得到今天最后布置的那一节,不合起来的话,画流程图那节一布置写代码就没了。
|
||||
*
|
||||
* 写代码用的语言:今天最后布置的、要写代码的那一节选的(只画流程图的课没有语言)
|
||||
*/
|
||||
async function todaySteps(
|
||||
className: string,
|
||||
problemIds: number[],
|
||||
latest: { lessonId: number | null; language: LessonLanguage | null; mode: LessonMode | null },
|
||||
) {
|
||||
const rows = await db
|
||||
.select({ id: schema.classLesson.id, problemIds: schema.classLesson.problemIds, language: schema.classLesson.language, mode: schema.classLesson.mode })
|
||||
.from(schema.classLesson)
|
||||
.where(and(eq(schema.classLesson.className, className), eq(schema.classLesson.day, calendarDay())))
|
||||
.orderBy(desc(schema.classLesson.createdAt), desc(schema.classLesson.id))
|
||||
const needs = new Map<number, { draw: boolean; code: boolean }>()
|
||||
let language: LessonLanguage | null = latest.mode && lessonNeeds(latest.mode).code ? latest.language : null
|
||||
let languageFound = !!latest.mode && lessonNeeds(latest.mode).code
|
||||
for (const row of rows) {
|
||||
const shared = row.id === latest.lessonId ? problemIds : row.problemIds.filter((id) => problemIds.includes(id))
|
||||
if (!shared.length) continue
|
||||
const rowNeeds = lessonNeeds(row.mode)
|
||||
for (const id of shared) {
|
||||
const known = needs.get(id) ?? { draw: false, code: false }
|
||||
needs.set(id, { draw: known.draw || rowNeeds.draw, code: known.code || rowNeeds.code })
|
||||
}
|
||||
if (!languageFound && rowNeeds.code) {
|
||||
language = row.language ?? null
|
||||
languageFound = true
|
||||
}
|
||||
}
|
||||
return { needs, language }
|
||||
}
|
||||
+27
-12
@@ -5,7 +5,6 @@ import {
|
||||
type TaskBoard,
|
||||
type TaskBoardStudent,
|
||||
type ClassLesson,
|
||||
lessonNeeds,
|
||||
type StatisticInfo,
|
||||
} from '@oj2/contract'
|
||||
import { and, desc, eq, gte, inArray, isNotNull, isNull, sql } from 'drizzle-orm'
|
||||
@@ -127,9 +126,14 @@ taskRoutes.get('/task/board', requireTeacher, async (c) => {
|
||||
const challengeDoneKeys = new Set(
|
||||
challengeDone.filter((row) => liveChallenges.get(row.problemId)?.has(row.challengeId)).map((row) => `${row.userId}:${row.problemId}`),
|
||||
)
|
||||
// 老师布置的这节做什么;推断出来的课为 null,代码交对或流程图画对都算(和加「做什么」之前一样)
|
||||
// 老师布置的这节做什么;推断出来的课为 null,代码交对或流程图画对都算(和加「做什么」之前一样)。
|
||||
// 格子按每道题合起来的要求算(todayNeeds):先画流程图一节、再写代码一节,没画图的照样标「缺图」,
|
||||
// 和学生那边的「做完」对得上。代码按的语言也是合起来那份:最后一节只画流程图时,用前面写代码那节的
|
||||
const mode = lesson.source === 'teacher' ? lesson.mode : null
|
||||
const needs = mode ? lessonNeeds(mode) : null
|
||||
const steps = lesson.steps
|
||||
const needsOf = (problemId: number) => steps?.needsOf(problemId) ?? null
|
||||
const anyDraw = ids.some((id) => needsOf(id)?.draw)
|
||||
const codeLanguage = steps ? steps.language : lesson.language
|
||||
const [cells, lastSubmits, lastDrawn, attendance, drawn, todays, seen, lastFlows] = await Promise.all([
|
||||
ids.length && userIds.length
|
||||
? db
|
||||
@@ -145,7 +149,7 @@ taskRoutes.get('/task/board', requireTeacher, async (c) => {
|
||||
isNull(schema.submission.contestId),
|
||||
inArray(schema.submission.userId, userIds),
|
||||
inArray(schema.submission.problemId, ids),
|
||||
lessonLanguageFilter(lesson.language),
|
||||
lessonLanguageFilter(codeLanguage),
|
||||
),
|
||||
)
|
||||
.groupBy(schema.submission.userId, schema.submission.problemId)
|
||||
@@ -223,7 +227,7 @@ taskRoutes.get('/task/board', requireTeacher, async (c) => {
|
||||
inArray(schema.submission.userId, userIds),
|
||||
inArray(schema.submission.problemId, ids),
|
||||
gte(schema.submission.createTime, start),
|
||||
lessonLanguageFilter(lesson.language),
|
||||
lessonLanguageFilter(codeLanguage),
|
||||
),
|
||||
)
|
||||
.orderBy(schema.submission.createTime)
|
||||
@@ -234,7 +238,7 @@ taskRoutes.get('/task/board', requireTeacher, async (c) => {
|
||||
* 还没画对的格子点开讲它哪里错、按走对几组归堆。走对几组从 2026-10 起的「走例子」报告里数,
|
||||
* 老评法和还没评完的为 null
|
||||
*/
|
||||
needs?.draw && ids.length && userIds.length
|
||||
anyDraw && ids.length && userIds.length
|
||||
? db
|
||||
.selectDistinctOn([schema.flowchartSubmission.userId, schema.flowchartSubmission.problemId], {
|
||||
userId: schema.flowchartSubmission.userId,
|
||||
@@ -281,6 +285,7 @@ taskRoutes.get('/task/board', requireTeacher, async (c) => {
|
||||
title: problem.title,
|
||||
isSql: problem.isSql,
|
||||
hasChallenges: (liveChallenges.get(problem.id)?.size ?? 0) > 0,
|
||||
mode: steps?.modeOf(problem.id) ?? null,
|
||||
})),
|
||||
students: roster.map(
|
||||
(student) =>
|
||||
@@ -289,6 +294,7 @@ taskRoutes.get('/task/board', requireTeacher, async (c) => {
|
||||
username: student.username,
|
||||
realName: student.realName ?? null,
|
||||
cells: ids.map((id) => {
|
||||
const needs = needsOf(id)
|
||||
const key = `${student.userId}:${id}`
|
||||
const cell = cellByKey.get(key)
|
||||
const flow = drawnByKey.get(key)
|
||||
@@ -305,11 +311,11 @@ taskRoutes.get('/task/board', requireTeacher, async (c) => {
|
||||
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(
|
||||
const wanted = [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'
|
||||
acceptedAt = wanted.every((step) => step.at) ? wanted.map((step) => step.at!).reduce(latestOf) : null
|
||||
status = acceptedAt ? 'accepted' : wanted.some((step) => step.status !== 'none') ? 'tried' : 'none'
|
||||
attempts = (needs.code ? (cell?.attempts ?? 0) : 0) + (needs.draw ? (flow?.attempts ?? 0) : 0)
|
||||
}
|
||||
// 下面几样都是代码那一步的:只画流程图的课没有
|
||||
@@ -352,8 +358,9 @@ taskRoutes.get('/task/board', requireTeacher, async (c) => {
|
||||
}))
|
||||
: [],
|
||||
acceptedTry: firstTry >= 0 ? firstTry + 1 : null,
|
||||
// 整道题做完了才谈挑战:先画再写只交对了代码、图还没画的,格子是「缺图」,不挂挑战的圈
|
||||
challenge:
|
||||
!codeWanted || !codeDone || !liveChallenges.get(id)?.size
|
||||
!codeWanted || !codeDone || !acceptedAt || !liveChallenges.get(id)?.size
|
||||
? null
|
||||
: challengeDoneKeys.has(key)
|
||||
? 'done'
|
||||
@@ -410,9 +417,17 @@ taskRoutes.get('/task/today', requireTeacher, async (c) => {
|
||||
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 },
|
||||
{
|
||||
id: lesson.lessonId,
|
||||
className,
|
||||
day: calendarDay(),
|
||||
problemIds: ids,
|
||||
language: lesson.steps?.language ?? lesson.language,
|
||||
mode: lesson.mode,
|
||||
modeOf: lesson.steps?.modeOf,
|
||||
},
|
||||
])
|
||||
allDoneCount = counts.get(lesson.lessonId) ?? 0
|
||||
} else if (ids.length) {
|
||||
|
||||
@@ -59,7 +59,16 @@ export function lessonDayEnd(day: string) {
|
||||
* 人按花名册(STUDENT_ROLES、没禁用),和「全班几人」对得上
|
||||
*/
|
||||
export async function lessonDoneCounts(
|
||||
lessons: { id: number; className: string; day: string; problemIds: number[]; language: LessonLanguage | null; mode: LessonMode }[],
|
||||
lessons: {
|
||||
id: number
|
||||
className: string
|
||||
day: string
|
||||
problemIds: number[]
|
||||
language: LessonLanguage | null
|
||||
mode: LessonMode
|
||||
/** 每道题各自做什么(老师首页按今天几节合起来的要求算,见 todayNeeds);不给就都按 mode */
|
||||
modeOf?: (problemId: number) => LessonMode
|
||||
}[],
|
||||
) {
|
||||
const rows = lessons.flatMap((lesson) =>
|
||||
lesson.problemIds.map((problemId) => ({
|
||||
@@ -67,7 +76,7 @@ export async function lessonDoneCounts(
|
||||
class_name: lesson.className,
|
||||
problem_id: problemId,
|
||||
language: lesson.language,
|
||||
mode: lesson.mode,
|
||||
mode: lesson.modeOf?.(problemId) ?? lesson.mode,
|
||||
until: lessonDayEnd(lesson.day),
|
||||
})),
|
||||
)
|
||||
@@ -281,6 +290,45 @@ async function lessonPlan(className: string, day: string) {
|
||||
return row?.problemIds.length ? row : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 一道题今天要画还是要写:这个班今天布置过的每一节里,凡是有这道题的,要求合起来算。
|
||||
* 老师先布置一节画流程图、再「用这几道题写代码」开一节时,还没画完的照样要画 ——
|
||||
* 只看今天最后布置的那一节的话,一布置写代码,画图那一步就没了。学生的课堂条、任务页的格子、
|
||||
* 老师首页的「全做对」都按它算,三处说的「做完」才一样(后台和数据统计还是一节一行、各数各的)。
|
||||
*
|
||||
* `language`:代码按什么语言数 —— 今天最后布置的、要写代码的那一节选的(只画流程图的课没有语言)
|
||||
*/
|
||||
export async function todayNeeds(className: string, problemIds: number[], latest: { id: number; language: LessonLanguage | null; mode: LessonMode }) {
|
||||
const rows = await db
|
||||
.select({ id: schema.classLesson.id, problemIds: schema.classLesson.problemIds, language: schema.classLesson.language, mode: schema.classLesson.mode })
|
||||
.from(schema.classLesson)
|
||||
.where(and(eq(schema.classLesson.className, className), eq(schema.classLesson.day, calendarDay())))
|
||||
.orderBy(desc(schema.classLesson.createdAt), desc(schema.classLesson.id))
|
||||
const needs = new Map<number, { draw: boolean; code: boolean }>()
|
||||
let language: LessonLanguage | null = lessonNeeds(latest.mode).code ? latest.language : null
|
||||
let languageFound = lessonNeeds(latest.mode).code
|
||||
for (const row of rows) {
|
||||
const shared = row.id === latest.id ? problemIds : row.problemIds.filter((id) => problemIds.includes(id))
|
||||
if (!shared.length) continue
|
||||
const rowNeeds = lessonNeeds(row.mode)
|
||||
for (const id of shared) {
|
||||
const known = needs.get(id) ?? { draw: false, code: false }
|
||||
needs.set(id, { draw: known.draw || rowNeeds.draw, code: known.code || rowNeeds.code })
|
||||
}
|
||||
if (!languageFound && rowNeeds.code) {
|
||||
language = row.language ?? null
|
||||
languageFound = true
|
||||
}
|
||||
}
|
||||
const latestNeeds = lessonNeeds(latest.mode)
|
||||
const needsOf = (id: number) => needs.get(id) ?? latestNeeds
|
||||
const modeOf = (id: number): LessonMode => {
|
||||
const { draw, code } = needsOf(id)
|
||||
return draw && code ? 'both' : draw ? 'flowchart' : 'code'
|
||||
}
|
||||
return { needsOf, modeOf, language }
|
||||
}
|
||||
|
||||
/**
|
||||
* 班里「这节课」的题。老师在课堂任务布置过就用老师的(source = teacher),
|
||||
* 否则从同班提交记录推断(source = inferred):同班同一天 ≥ 5 人做过的题,只取最近的
|
||||
@@ -294,17 +342,23 @@ export async function classLessonProblems(className: string, lookbackDays: numbe
|
||||
const plan = await lessonPlan(className, today)
|
||||
if (plan) {
|
||||
const planned = plan.problemIds
|
||||
const [problems, groups] = await Promise.all([
|
||||
const steps = await todayNeeds(className, planned, plan)
|
||||
// 每道题按它合起来的要求数「做过 / 做对」:要求一样的几道一起数
|
||||
const byMode = Map.groupBy(planned, (id) => steps.modeOf(id))
|
||||
const [problems, ...groupLists] = await Promise.all([
|
||||
visibleProblems(planned),
|
||||
classDayGroups(className, dayStart(), { problemIds: planned, language: plan.language, mode: plan.mode }),
|
||||
...[...byMode].map(([mode, ids]) =>
|
||||
classDayGroups(className, dayStart(), { problemIds: ids, language: lessonNeeds(mode).code ? steps.language : null, mode }),
|
||||
),
|
||||
])
|
||||
const groupById = new Map(groups.map((row) => [row.problemId, row]))
|
||||
const groupById = new Map(groupLists.flat().map((row) => [row.problemId, row]))
|
||||
return {
|
||||
source: 'teacher' as const,
|
||||
lessonId: plan.id,
|
||||
day: today,
|
||||
language: plan.language ?? null,
|
||||
mode: plan.mode,
|
||||
steps,
|
||||
assignedAt: plan.createdAt,
|
||||
assignedBy: plan.createdBy,
|
||||
problems: planned.flatMap((id) => {
|
||||
@@ -337,6 +391,7 @@ export async function classLessonProblems(className: string, lookbackDays: numbe
|
||||
// 推断出来的课不知道老师要的是哪种语言、要画还是要写,不限
|
||||
language: null,
|
||||
mode: null,
|
||||
steps: null,
|
||||
assignedAt: null,
|
||||
assignedBy: null,
|
||||
problems: picked.flatMap((row) => {
|
||||
|
||||
Reference in new issue
Block a user