feat(lesson-preparation): add AI evaluation, analytics, attachments, calendar, comments, review, substitutes, formative, and version diff
- Add actions-ai-evaluation, actions-analytics, actions-attachments, actions-calendar, actions-comments, actions-formative, actions-questions, actions-review, actions-substitutes - Add corresponding data-access layers for each new action module - Add calendar-view, curriculum-map-view, version-diff-viewer components - Add editor-slice, selection-slice, version-slice hooks for state management - Add document-diff and scope-check lib utilities - Add default-question-service and external-questions-bridge services
This commit is contained in:
264
src/modules/lesson-preparation/data-access-formative.ts
Normal file
264
src/modules/lesson-preparation/data-access-formative.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* 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<FormativeItem[]> {
|
||||
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<FormativeItem | null> {
|
||||
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<FormativeItem> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<FormativeResponse> {
|
||||
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<FormativeResponse[]> {
|
||||
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<FormativeResponse[]> {
|
||||
// 简化查询:按 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user