From a6bf845e06849723d80e47d0be69daa000264aa9 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 27 Sep 2026 22:47:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AF=BE=E5=A0=82=E7=9C=8B=E6=9D=BF):=20?= =?UTF-8?q?=E6=A0=87=E5=87=BA=E6=9C=80=E8=BF=91=E5=87=A0=E8=8A=82=E8=AF=BE?= =?UTF-8?q?=E7=BB=8F=E5=B8=B8=E6=B2=A1=E4=BA=A4=E7=9A=84=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=EF=BC=8C=E6=B1=82=E5=8A=A9=E7=9A=84=E5=AD=A6=E7=94=9F=E7=BD=AE?= =?UTF-8?q?=E9=A1=B6=EF=BC=8C=E7=82=B9=E4=B8=80=E4=B8=8B=E6=89=93=E5=BC=80?= =?UTF-8?q?=E6=B1=82=E5=8A=A9=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2025 秋至少上过 5 节课的学生里,12.5% 七成以上的课一道都没交,37% 的学生贡献了 一半以上的「没交」;当堂那份「今天还没交过」把偶尔没来的和一直不动手的排在同一行。 - 看板接口给每个学生加 recentAttended:最近几节课(最多 5 节、不含今天、45 天内; 一节课 = 同班同日 ≥5 人做同一题)里交过几节,另给 recentLessons - 「今天还没交过」拆成「经常没交」(≥3 节可比且最多交过 1 节)和其他;表格加一列 「最近 N 节」;只给老师看,投影模式不出现 - 求助读老师端常驻 WebSocket 的 collabStore.requests(不改后端):求助的学生置顶, 状态显示「举手了 / 老师在帮」,点一下打开求助列表;求助提示不论布没布置题都显示 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/routes/classroom.ts | 46 +++++++++++++++- apps/web/src/oj/classroom/board.vue | 82 +++++++++++++++++++++++++++-- packages/contract/src/classroom.ts | 7 +++ 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/apps/api/src/routes/classroom.ts b/apps/api/src/routes/classroom.ts index f5aa0f5..8b38592 100644 --- a/apps/api/src/routes/classroom.ts +++ b/apps/api/src/routes/classroom.ts @@ -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`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`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) }) diff --git a/apps/web/src/oj/classroom/board.vue b/apps/web/src/oj/classroom/board.vue index cf21a5a..37e1e00 100644 --- a/apps/web/src/oj/classroom/board.vue +++ b/apps/web/src/oj/classroom/board.vue @@ -1,5 +1,6 @@