/** * M5 形成性评价闭环 - 数据访问层 * * publish 课案时嵌入互动组件(poll/quiz/exit_ticket), * 学生课中作答后结果回写到 lessonPlanFormativeResponses 表, * 教师可查看实时反馈,闭环到课案优化。 */ import "server-only"; import { db } from "@/shared/db"; import { lessonPlanFormativeItems, lessonPlanFormativeResponses, } from "@/shared/db/schema"; import { eq, desc, asc } from "drizzle-orm"; import type { FormativeInteractionType } from "./lib/type-guards"; import { isFormativeInteractionType } from "./lib/type-guards"; /** 互动组件项 */ export interface FormativeItem { id: string; planId: string; blockId: string; interactionType: FormativeInteractionType; payload: unknown; instantFeedback: boolean; orderIndex: number; createdAt: Date; updatedAt: Date; } /** 学生作答记录 */ export interface FormativeResponse { id: string; itemId: string; studentId: string; classId?: string; response: unknown; isCorrect?: boolean; durationSec?: number; createdAt: Date; } /** * 查询课案的所有互动组件 */ export async function getFormativeItemsByPlanId( planId: string, ): Promise { const rows = await db .select() .from(lessonPlanFormativeItems) .where(eq(lessonPlanFormativeItems.planId, planId)) .orderBy(asc(lessonPlanFormativeItems.orderIndex)); return rows.map(mapRowToItem); } /** * 查询单个互动组件 */ export async function getFormativeItemById( id: string, ): Promise { const rows = await db .select() .from(lessonPlanFormativeItems) .where(eq(lessonPlanFormativeItems.id, id)) .limit(1); return rows.length === 0 ? null : mapRowToItem(rows[0]!); } /** * 创建互动组件 */ export async function createFormativeItem( input: { planId: string; blockId: string; interactionType: FormativeInteractionType; payload: unknown; instantFeedback?: boolean; orderIndex?: number; }, ): Promise { const [row] = await db.insert(lessonPlanFormativeItems).values({ planId: input.planId, blockId: input.blockId, interactionType: input.interactionType, payload: input.payload, instantFeedback: input.instantFeedback ?? false, orderIndex: input.orderIndex ?? 0, }); const insertedId = row.insertId; const created = await getFormativeItemById(String(insertedId)); if (!created) throw new Error("FORMATIVE_ITEM_CREATE_FAILED"); return created; } /** * 更新互动组件 */ export async function updateFormativeItem( id: string, patch: { payload?: unknown; instantFeedback?: boolean; orderIndex?: number; }, ): Promise { await db .update(lessonPlanFormativeItems) .set({ ...(patch.payload !== undefined ? { payload: patch.payload } : {}), ...(patch.instantFeedback !== undefined ? { instantFeedback: patch.instantFeedback } : {}), ...(patch.orderIndex !== undefined ? { orderIndex: patch.orderIndex } : {}), }) .where(eq(lessonPlanFormativeItems.id, id)); } /** * 删除互动组件 */ export async function deleteFormativeItem(id: string): Promise { await db .delete(lessonPlanFormativeItems) .where(eq(lessonPlanFormativeItems.id, id)); } /** * 提交学生作答 */ export async function submitFormativeResponse( input: { itemId: string; studentId: string; classId?: string; response: unknown; isCorrect?: boolean; durationSec?: number; }, ): Promise { const [row] = await db.insert(lessonPlanFormativeResponses).values({ itemId: input.itemId, studentId: input.studentId, classId: input.classId, response: input.response, isCorrect: input.isCorrect, durationSec: input.durationSec, }); const insertedId = row.insertId; const items = await getResponsesByItemId(input.itemId); const created = items.find((r) => r.id === String(insertedId)); if (!created) throw new Error("FORMATIVE_RESPONSE_CREATE_FAILED"); return created; } /** * 查询互动组件的所有作答 */ export async function getResponsesByItemId( itemId: string, ): Promise { const rows = await db .select() .from(lessonPlanFormativeResponses) .where(eq(lessonPlanFormativeResponses.itemId, itemId)) .orderBy(desc(lessonPlanFormativeResponses.createdAt)); return rows.map(mapRowToResponse); } /** * 查询某学生作答历史 */ export async function getResponsesByStudentId( studentId: string, planId?: string, ): Promise { // 简化查询:按 studentId 查询,可选按 planId 过滤 if (planId) { const items = await db .select({ id: lessonPlanFormativeItems.id }) .from(lessonPlanFormativeItems) .where(eq(lessonPlanFormativeItems.planId, planId)); if (items.length === 0) return []; const itemIds = items.map((i) => i.id); const rows = await db .select() .from(lessonPlanFormativeResponses) .where(eq(lessonPlanFormativeResponses.studentId, studentId)); return rows.filter((r) => itemIds.includes(r.itemId)).map(mapRowToResponse); } const rows = await db .select() .from(lessonPlanFormativeResponses) .where(eq(lessonPlanFormativeResponses.studentId, studentId)); return rows.map(mapRowToResponse); } /** * 统计互动组件的作答情况(用于教师查看实时反馈) */ export async function getFormativeItemStats( itemId: string, ): Promise<{ total: number; correct: number; incorrect: number; avgDurationSec: number }> { const rows = await db .select() .from(lessonPlanFormativeResponses) .where(eq(lessonPlanFormativeResponses.itemId, itemId)); const total = rows.length; let correct = 0; let incorrect = 0; let totalDuration = 0; let durationCount = 0; for (const r of rows) { if (r.isCorrect === true) correct++; else if (r.isCorrect === false) incorrect++; if (r.durationSec !== null && r.durationSec !== undefined) { totalDuration += r.durationSec; durationCount++; } } return { total, correct, incorrect, avgDurationSec: durationCount > 0 ? Math.round(totalDuration / durationCount) : 0, }; } function mapRowToItem( row: typeof lessonPlanFormativeItems.$inferSelect, ): FormativeItem { const interactionType = isFormativeInteractionType(row.interactionType) ? row.interactionType : "poll"; return { id: row.id, planId: row.planId, blockId: row.blockId, interactionType, payload: row.payload, instantFeedback: row.instantFeedback, orderIndex: row.orderIndex, createdAt: row.createdAt, updatedAt: row.updatedAt, }; } function mapRowToResponse( row: typeof lessonPlanFormativeResponses.$inferSelect, ): FormativeResponse { return { id: row.id, itemId: row.itemId, studentId: row.studentId, classId: row.classId ?? undefined, response: row.response, isCorrect: row.isCorrect ?? undefined, durationSec: row.durationSec ?? undefined, createdAt: row.createdAt, }; }