feat(课堂看板): 标出最近几节课经常没交的学生,求助的学生置顶,点一下打开求助列表
Deploy / deploy (push) Canceled after 0s

2025 秋至少上过 5 节课的学生里,12.5% 七成以上的课一道都没交,37% 的学生贡献了
一半以上的「没交」;当堂那份「今天还没交过」把偶尔没来的和一直不动手的排在同一行。

- 看板接口给每个学生加 recentAttended:最近几节课(最多 5 节、不含今天、45 天内;
  一节课 = 同班同日 ≥5 人做同一题)里交过几节,另给 recentLessons
- 「今天还没交过」拆成「经常没交」(≥3 节可比且最多交过 1 节)和其他;表格加一列
  「最近 N 节」;只给老师看,投影模式不出现
- 求助读老师端常驻 WebSocket 的 collabStore.requests(不改后端):求助的学生置顶,
  状态显示「举手了 / 老师在帮」,点一下打开求助列表;求助提示不论布没布置题都显示

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 22:47:24 +08:00
1 parent 80106eaa04
commit a6bf845e06
3 files changed
+129 -6

No files matched your search

+45 -1
View File
@@ -343,6 +343,15 @@ classroomRoutes.get("/me/class-activity", requireAuth, async (c) => {
} satisfies ClassActivity)
})
/**
* 看板上的「最近几节课交了几节」:分清「今天没来」和「一直不动手」。2025 秋至少上过
* 5 节课的学生里,12.5% 七成以上的课一道都没交,37% 的学生贡献了一半以上的「没交」
* —— 当堂那份名单把这两种人排在同一行里,老师分不出来。
*/
const RECENT_LESSONS = 5
/** 往回找最近几节课时最多看多远:寒暑假之后开学第一节课,前面几节是上学期的,没意义 */
const RECENT_LESSONS_WINDOW_DAYS = 45
/** 看板没指定班级时,猜「最近这么久里提交人数最多的班」—— 老师多半正在上这个班的课 */
const ACTIVE_CLASS_WINDOW_MS = 2 * 60 * 60 * 1000
@@ -389,6 +398,7 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
source: null,
problems: [],
students: [],
recentLessons: 0,
} satisfies ClassBoard)
}
@@ -414,7 +424,21 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
)
const userIds = roster.map((row) => row.userId)
const [cells, lastSubmits] = await Promise.all([
// 最近几节课:这个班同学一起做题的那些天(不含今天)
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)
const localDay = sql<string>`to_char(${localTime(schema.submission.createTime)}, 'YYYY-MM-DD')`
const [cells, lastSubmits, attendance] = await Promise.all([
ids.length && userIds.length
? db
.select({
@@ -450,9 +474,27 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
)
.groupBy(schema.submission.userId)
: [],
recentDays.length && userIds.length
? db
.select({
userId: schema.submission.userId,
days: sql<number>`count(distinct ${localDay})::int`,
})
.from(schema.submission)
.where(
and(
isNull(schema.submission.contestId),
inArray(schema.submission.userId, userIds),
gte(schema.submission.createTime, windowStart),
inArray(localDay, recentDays),
),
)
.groupBy(schema.submission.userId)
: [],
])
const cellByKey = new Map(cells.map((row) => [`${row.userId}:${row.problemId}`, row]))
const lastByUser = new Map(lastSubmits.map((row) => [row.userId, row.lastAt]))
const attendedByUser = new Map(attendance.map((row) => [row.userId, row.days]))
return success(c, {
className,
@@ -478,8 +520,10 @@ classroomRoutes.get("/classroom/board", requireTeacher, async (c) => {
}
}),
lastSubmitAt: lastByUser.get(student.userId) ?? null,
recentAttended: attendedByUser.get(student.userId) ?? 0,
}) satisfies ClassBoardStudent,
),
recentLessons: recentDays.length,
} satisfies ClassBoard)
})