import "server-only" import { cache } from "react" import { and, asc, eq, inArray, sql, count } from "drizzle-orm" import { db } from "@/shared/db" import { chapters, knowledgePoints, knowledgePointPrerequisites, questionsToKnowledgePoints, knowledgePointMastery, } from "@/shared/db/schema" import type { KpWithRelations, MasteryInfo } from "./types" /** * 获取教材下全书知识点(含前置依赖 + 关联题目数 + 章节标题)。 * * 一次查询聚合,避免 N+1。 */ export const getKnowledgePointsWithRelations = cache(async ( textbookId: string, ): Promise => { // 1. 查询全书知识点 + 章节标题 const kpRows = await db .select({ id: knowledgePoints.id, name: knowledgePoints.name, description: knowledgePoints.description, parentId: knowledgePoints.parentId, chapterId: knowledgePoints.chapterId, level: knowledgePoints.level, order: knowledgePoints.order, chapterTitle: chapters.title, }) .from(knowledgePoints) .innerJoin(chapters, eq(chapters.id, knowledgePoints.chapterId)) .where(eq(chapters.textbookId, textbookId)) .orderBy(asc(chapters.order), asc(knowledgePoints.order), asc(knowledgePoints.name)) if (kpRows.length === 0) return [] const kpIds = kpRows.map((r) => r.id) // 2. 查询关联题目数(批量聚合) const questionCountRows = await db .select({ knowledgePointId: questionsToKnowledgePoints.knowledgePointId, count: count(), }) .from(questionsToKnowledgePoints) .where(inArray(questionsToKnowledgePoints.knowledgePointId, kpIds)) .groupBy(questionsToKnowledgePoints.knowledgePointId) const questionCountMap = new Map() for (const r of questionCountRows) { questionCountMap.set(r.knowledgePointId, Number(r.count)) } // 3. 查询前置依赖(批量,仅查询属于当前教材知识点的依赖) // 双向过滤:knowledgePointId 和 prerequisiteKpId 都必须在当前教材的知识点集合内 const prereqRows = await db .select({ knowledgePointId: knowledgePointPrerequisites.knowledgePointId, prerequisiteKpId: knowledgePointPrerequisites.prerequisiteKpId, }) .from(knowledgePointPrerequisites) .where(and( inArray(knowledgePointPrerequisites.knowledgePointId, kpIds), inArray(knowledgePointPrerequisites.prerequisiteKpId, kpIds), )) const prereqMap = new Map() for (const r of prereqRows) { const arr = prereqMap.get(r.knowledgePointId) ?? [] arr.push(r.prerequisiteKpId) prereqMap.set(r.knowledgePointId, arr) } // 4. 组装结果 return kpRows.map((r) => ({ id: r.id, name: r.name, description: r.description, parentId: r.parentId, chapterId: r.chapterId, level: r.level ?? 0, order: r.order ?? 0, chapterTitle: r.chapterTitle, questionCount: questionCountMap.get(r.id) ?? 0, prerequisiteIds: prereqMap.get(r.id) ?? [], })) }) /** * 获取学生在某教材下所有知识点的掌握度。 */ export const getStudentKpMastery = cache(async ( studentId: string, textbookId: string, ): Promise> => { const rows = await db .select({ knowledgePointId: knowledgePointMastery.knowledgePointId, masteryLevel: knowledgePointMastery.masteryLevel, totalQuestions: knowledgePointMastery.totalQuestions, correctQuestions: knowledgePointMastery.correctQuestions, lastAssessedAt: knowledgePointMastery.lastAssessedAt, }) .from(knowledgePointMastery) .innerJoin(knowledgePoints, eq(knowledgePoints.id, knowledgePointMastery.knowledgePointId)) .innerJoin(chapters, eq(chapters.id, knowledgePoints.chapterId)) .where(and( eq(knowledgePointMastery.studentId, studentId), eq(chapters.textbookId, textbookId), )) const map = new Map() for (const r of rows) { map.set(r.knowledgePointId, { masteryLevel: Number(r.masteryLevel), totalQuestions: r.totalQuestions, correctQuestions: r.correctQuestions, lastAssessedAt: r.lastAssessedAt, }) } return map }) /** * 获取班级(教师所带班级的所有学生)在某教材下知识点的平均掌握度。 * * @param studentIds 班级学生 ID 列表 * @param textbookId 教材 ID */ export const getClassKpMastery = cache(async ( studentIds: string[], textbookId: string, ): Promise> => { if (studentIds.length === 0) return new Map() const rows = await db .select({ knowledgePointId: knowledgePointMastery.knowledgePointId, avgMastery: sql`AVG(${knowledgePointMastery.masteryLevel})`, totalQuestions: sql`SUM(${knowledgePointMastery.totalQuestions})`, correctQuestions: sql`SUM(${knowledgePointMastery.correctQuestions})`, lastAssessedAt: sql`MAX(${knowledgePointMastery.lastAssessedAt})`, }) .from(knowledgePointMastery) .innerJoin(knowledgePoints, eq(knowledgePoints.id, knowledgePointMastery.knowledgePointId)) .innerJoin(chapters, eq(chapters.id, knowledgePoints.chapterId)) .where(and( inArray(knowledgePointMastery.studentId, studentIds), eq(chapters.textbookId, textbookId), )) .groupBy(knowledgePointMastery.knowledgePointId) const map = new Map() for (const r of rows) { map.set(r.knowledgePointId, { masteryLevel: Number(r.avgMastery), totalQuestions: Number(r.totalQuestions), correctQuestions: Number(r.correctQuestions), lastAssessedAt: r.lastAssessedAt, }) } return map }) /** * 获取某个知识点的前置依赖列表(含知识点详情)。 */ export const getPrerequisitesForKp = cache(async ( kpId: string, ): Promise<{ id: string; name: string; description: string | null }[]> => { const rows = await db .select({ id: knowledgePoints.id, name: knowledgePoints.name, description: knowledgePoints.description, }) .from(knowledgePointPrerequisites) .innerJoin(knowledgePoints, eq(knowledgePoints.id, knowledgePointPrerequisites.prerequisiteKpId)) .where(eq(knowledgePointPrerequisites.knowledgePointId, kpId)) return rows }) /** * 获取某个知识点的后置知识点列表(即哪些知识点以此 KP 为前置)。 */ export const getSuccessorsForKp = cache(async ( kpId: string, ): Promise<{ id: string; name: string; description: string | null }[]> => { const rows = await db .select({ id: knowledgePoints.id, name: knowledgePoints.name, description: knowledgePoints.description, }) .from(knowledgePointPrerequisites) .innerJoin(knowledgePoints, eq(knowledgePoints.id, knowledgePointPrerequisites.knowledgePointId)) .where(eq(knowledgePointPrerequisites.prerequisiteKpId, kpId)) return rows })