diff --git a/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx b/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx index 1f36cb1..e692482 100644 --- a/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx +++ b/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx @@ -1,5 +1,6 @@ "use client" +import { useCallback } from "react" import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts" import { useTranslations } from "next-intl" import { BarChart3 } from "lucide-react" @@ -16,6 +17,26 @@ import { cn } from "@/shared/lib/utils" import type { ClassKnowledgePointWeakness } from "@/modules/adaptive-practice/data-access-analytics" +/** recharts 内联对象常量:避免每次渲染创建新对象触发 re-render */ +const CHART_MARGIN = { left: 8, right: 8, top: 8, bottom: 8 } +const GRID_PROPS = { vertical: false, strokeDasharray: "4 4", strokeOpacity: 0.4 } +const X_AXIS_BASE_PROPS = { + dataKey: "name", + tickLine: false, + axisLine: false, + tickMargin: 8, +} +const Y_AXIS_BASE_PROPS = { tickLine: false, axisLine: false, width: 40 } +const BAR_RADIUS: [number, number, number, number] = [4, 4, 0, 0] + +function truncateTick(value: string): string { + return value.length > 8 ? `${value.slice(0, 8)}...` : value +} + +function formatPercentTick(value: number): string { + return `${value}%` +} + interface ClassKnowledgePointWeaknessChartProps { data: ClassKnowledgePointWeakness[] className?: string @@ -41,6 +62,36 @@ export function ClassKnowledgePointWeaknessChart({ }: ClassKnowledgePointWeaknessChartProps) { const t = useTranslations("practice") + const renderTooltip = useCallback( + (payload: unknown) => { + // 从 unknown 转换:recharts tooltip payload 类型未知,需类型断言 + const p = payload as { + name: string + errorRate: number + totalAnswers: number + wrongAnswers: number + } + return ( +
+
{p.name}
+
+ {t("teacher.knowledgePointWeakness.totalAnswers")}: + {p.totalAnswers} +
+
+ {t("teacher.knowledgePointWeakness.wrongAnswers")}: + {p.wrongAnswers} +
+
+ {t("teacher.knowledgePointWeakness.errorRate")}: + {p.errorRate}% +
+
+ ) + }, + [t] + ) + if (data.length === 0) { return ( @@ -82,56 +133,25 @@ export function ClassKnowledgePointWeaknessChart({ - - + + - value.length > 8 ? `${value.slice(0, 8)}...` : value - } + {...X_AXIS_BASE_PROPS} + tickFormatter={truncateTick} /> `${value}%`} + {...Y_AXIS_BASE_PROPS} + tickFormatter={formatPercentTick} /> { - const p = payload as unknown as { - name: string - errorRate: number - totalAnswers: number - wrongAnswers: number - } - return ( -
-
{p.name}
-
- {t("teacher.knowledgePointWeakness.totalAnswers")}: - {p.totalAnswers} -
-
- {t("teacher.knowledgePointWeakness.wrongAnswers")}: - {p.wrongAnswers} -
-
- {t("teacher.knowledgePointWeakness.errorRate")}: - {p.errorRate}% -
-
- ) - }} + formatter={renderTooltip} /> } /> - + {chartData.map((_, idx) => ( ))} diff --git a/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx b/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx index 9f091a7..d50f90f 100644 --- a/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx +++ b/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx @@ -1,5 +1,6 @@ "use client" +import { useCallback } from "react" import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts" import { useTranslations } from "next-intl" @@ -14,6 +15,18 @@ import { cn } from "@/shared/lib/utils" import type { PracticeTypeBreakdown } from "@/modules/adaptive-practice/data-access-analytics" +/** recharts 内联对象常量:避免每次渲染创建新对象触发 re-render */ +const CHART_MARGIN = { left: 8, right: 8, top: 8, bottom: 8 } +const GRID_PROPS = { vertical: false, strokeDasharray: "4 4", strokeOpacity: 0.4 } +const X_AXIS_PROPS = { + dataKey: "name", + tickLine: false, + axisLine: false, + tickMargin: 8, +} +const Y_AXIS_PROPS = { tickLine: false, axisLine: false, width: 36 } +const BAR_RADIUS: [number, number, number, number] = [4, 4, 0, 0] + interface PracticeTypeBreakdownChartProps { data: PracticeTypeBreakdown[] className?: string @@ -39,6 +52,41 @@ export function PracticeTypeBreakdownChart({ }: PracticeTypeBreakdownChartProps) { const t = useTranslations("practice") + const renderTooltip = useCallback( + (payload: unknown) => { + // 从 unknown 转换:recharts tooltip payload 类型未知,需类型断言 + const p = payload as { + name: string + sessionCount: number + totalQuestions: number + correctCount: number + accuracy: number + } + return ( +
+
{p.name}
+
+ {t("teacher.classComparison.totalSessions")}: + {p.sessionCount} +
+
+ {t("teacher.classComparison.totalAnswered")}: + {p.totalQuestions} +
+
+ {t("teacher.knowledgePointWeakness.wrongAnswers")}: + {p.totalQuestions - p.correctCount} +
+
+ {t("teacher.classComparison.averageAccuracy")}: + {p.accuracy}% +
+
+ ) + }, + [t] + ) + if (data.length === 0) return null const chartData = data.map((d) => ({ @@ -64,53 +112,19 @@ export function PracticeTypeBreakdownChart({ - - - - + + + + { - const p = payload as unknown as { - name: string - sessionCount: number - totalQuestions: number - correctCount: number - accuracy: number - } - return ( -
-
{p.name}
-
- {t("teacher.classComparison.totalSessions")}: - {p.sessionCount} -
-
- {t("teacher.classComparison.totalAnswered")}: - {p.totalQuestions} -
-
- {t("teacher.knowledgePointWeakness.wrongAnswers")}: - {p.totalQuestions - p.correctCount} -
-
- {t("teacher.classComparison.averageAccuracy")}: - {p.accuracy}% -
-
- ) - }} + formatter={renderTooltip} /> } /> - + {chartData.map((_, idx) => ( ))} diff --git a/src/modules/adaptive-practice/data-access-analytics.ts b/src/modules/adaptive-practice/data-access-analytics.ts index 05e6ad0..837040e 100644 --- a/src/modules/adaptive-practice/data-access-analytics.ts +++ b/src/modules/adaptive-practice/data-access-analytics.ts @@ -1,6 +1,6 @@ import "server-only" -import { cache } from "react" +import { cacheFn } from "@/shared/lib/cache" import { and, count, desc, eq, inArray, sql } from "drizzle-orm" import { db } from "@/shared/db" @@ -64,7 +64,7 @@ export interface PracticeTypeBreakdown { * * @param classId 班级 ID */ -export const getClassPracticeStats = cache(async ( +export const getClassPracticeStatsRaw = async ( classId: string, ): Promise => { const studentIds = await getActiveStudentIdsByClassId(classId) @@ -99,6 +99,12 @@ export const getClassPracticeStats = cache(async ( averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0, activeStudents, } +} + +export const getClassPracticeStats = cacheFn(getClassPracticeStatsRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getClassPracticeStats"], }) /** @@ -106,7 +112,7 @@ export const getClassPracticeStats = cache(async ( * * @param classId 班级 ID */ -export const getClassStudentPracticeSummaries = cache(async ( +export const getClassStudentPracticeSummariesRaw = async ( classId: string, ): Promise => { const studentIds = await getActiveStudentIdsByClassId(classId) @@ -138,6 +144,12 @@ export const getClassStudentPracticeSummaries = cache(async ( : 0, lastPracticeAt: r.lastPracticeAt, })) +} + +export const getClassStudentPracticeSummaries = cacheFn(getClassStudentPracticeSummariesRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getClassStudentPracticeSummaries"], }) /** @@ -148,7 +160,7 @@ export const getClassStudentPracticeSummaries = cache(async ( * * @param gradeId 年级 ID */ -export const getGradePracticeStats = cache(async ( +export const getGradePracticeStatsRaw = async ( gradeId: string, ): Promise<{ totalSessions: number @@ -186,6 +198,12 @@ export const getGradePracticeStats = cache(async ( averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0, activeStudents: Number(row.activeStudents), } +} + +export const getGradePracticeStats = cacheFn(getGradePracticeStatsRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getGradePracticeStats"], }) /** @@ -193,7 +211,7 @@ export const getGradePracticeStats = cache(async ( * * @param studentIds 学生 ID 列表 */ -export const getPracticeTypeBreakdown = cache(async ( +export const getPracticeTypeBreakdownRaw = async ( studentIds: string[], ): Promise => { if (studentIds.length === 0) return [] @@ -218,6 +236,12 @@ export const getPracticeTypeBreakdown = cache(async ( ? Number(r.correctCount) / Number(r.totalQuestions) : 0, })) +} + +export const getPracticeTypeBreakdown = cacheFn(getPracticeTypeBreakdownRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getPracticeTypeBreakdown"], }) /** @@ -228,7 +252,7 @@ export const getPracticeTypeBreakdown = cache(async ( * * @param classId 班级 ID */ -export const getStudentsWithoutPractice = cache(async ( +export const getStudentsWithoutPracticeRaw = async ( classId: string, ): Promise => { const studentIds = await getActiveStudentIdsByClassId(classId) @@ -245,6 +269,12 @@ export const getStudentsWithoutPractice = cache(async ( // 返回没有练习记录的学生 return studentIds.filter((id) => !activeSet.has(id)) +} + +export const getStudentsWithoutPractice = cacheFn(getStudentsWithoutPracticeRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getStudentsWithoutPractice"], }) // --------------------------------------------------------------------------- @@ -278,7 +308,7 @@ export interface TeacherClassPracticeOverview { * * @param classIds 教师/年级主任可访问的班级 ID 列表 */ -export const getTeacherClassPracticeOverviews = cache(async ( +export const getTeacherClassPracticeOverviewsRaw = async ( classIds: string[], ): Promise => { if (classIds.length === 0) return [] @@ -351,6 +381,12 @@ export const getTeacherClassPracticeOverviews = cache(async ( participationRate: s.totalStudents > 0 ? activeStudents / s.totalStudents : 0, } }) +} + +export const getTeacherClassPracticeOverviews = cacheFn(getTeacherClassPracticeOverviewsRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getTeacherClassPracticeOverviews"], }) // --------------------------------------------------------------------------- @@ -378,7 +414,7 @@ export interface ClassKnowledgePointWeakness { * @param classId 班级 ID * @param limit 返回条数(默认 10) */ -export const getClassKnowledgePointWeakness = cache(async ( +export const getClassKnowledgePointWeaknessRaw = async ( classId: string, limit = 10, ): Promise => { @@ -418,6 +454,12 @@ export const getClassKnowledgePointWeakness = cache(async ( errorRate: totalAnswers > 0 ? wrongAnswers / totalAnswers : 0, } }) +} + +export const getClassKnowledgePointWeakness = cacheFn(getClassKnowledgePointWeaknessRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getClassKnowledgePointWeakness"], }) // --------------------------------------------------------------------------- @@ -447,7 +489,7 @@ export interface GradeClassPracticeComparison { * * @param gradeId 年级 ID */ -export const getGradeClassPracticeComparison = cache(async ( +export const getGradeClassPracticeComparisonRaw = async ( gradeId: string, ): Promise => { const classList = await getClassesByGradeId(gradeId) @@ -521,6 +563,12 @@ export const getGradeClassPracticeComparison = cache(async ( // 按参与率降序排列 return results.sort((a, b) => b.participationRate - a.participationRate) +} + +export const getGradeClassPracticeComparison = cacheFn(getGradeClassPracticeComparisonRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getGradeClassPracticeComparison"], }) // --------------------------------------------------------------------------- @@ -554,7 +602,7 @@ export interface ClassLearningProfile { * @param classId 班级 ID * @param weakKpLimit 知识点薄弱度返回条数(默认 5) */ -export const getClassLearningProfile = cache(async ( +export const getClassLearningProfileRaw = async ( classId: string, weakKpLimit = 5, ): Promise => { @@ -584,6 +632,12 @@ export const getClassLearningProfile = cache(async ( inactiveStudentIds: inactiveIds, weakKnowledgePoints: weakKps, } +} + +export const getClassLearningProfile = cacheFn(getClassLearningProfileRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getClassLearningProfile"], }) /** @@ -594,7 +648,7 @@ export const getClassLearningProfile = cache(async ( * * @param studentIds 学生 ID 列表 */ -export const getStudentNameMap = cache(async ( +export const getStudentNameMapRaw = async ( studentIds: string[], ): Promise> => { if (studentIds.length === 0) return new Map() @@ -604,6 +658,12 @@ export const getStudentNameMap = cache(async ( result.set(id, info.name ?? "Unknown") } return result +} + +export const getStudentNameMap = cacheFn(getStudentNameMapRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getStudentNameMap"], }) /** @@ -613,10 +673,16 @@ export const getStudentNameMap = cache(async ( * * @param gradeId 年级 ID */ -export const getGradeClassOverviews = cache(async ( +export const getGradeClassOverviewsRaw = async ( gradeId: string, ): Promise => { return getGradeClassPracticeComparison(gradeId) +} + +export const getGradeClassOverviews = cacheFn(getGradeClassOverviewsRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getGradeClassOverviews"], }) /** @@ -624,7 +690,7 @@ export const getGradeClassOverviews = cache(async ( * * @param gradeId 年级 ID */ -export const getGradePracticeOverview = cache(async ( +export const getGradePracticeOverviewRaw = async ( gradeId: string, ): Promise<{ totalClasses: number @@ -659,4 +725,10 @@ export const getGradePracticeOverview = cache(async ( totalCorrect, averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0, } +} + +export const getGradePracticeOverview = cacheFn(getGradePracticeOverviewRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getGradePracticeOverview"], }) diff --git a/src/modules/adaptive-practice/data-access-strategy.ts b/src/modules/adaptive-practice/data-access-strategy.ts index ae1e8c1..dee04e2 100644 --- a/src/modules/adaptive-practice/data-access-strategy.ts +++ b/src/modules/adaptive-practice/data-access-strategy.ts @@ -283,14 +283,14 @@ export async function selectQuestionsForPractice( * 获取学生已答过的题目 ID 列表(避免重复练习)。 * * 从专项练习中汇总已答题目。 - * 为控制查询量,仅查询最近 1000 条记录。 + * P3-8: LIMIT 从 1000 调低至 100,仅查询最近 100 条记录以控制查询量。 */ export async function getStudentAnsweredQuestionIds(studentId: string): Promise { const rows = await db .select({ questionId: practiceAnswers.questionId }) .from(practiceAnswers) .where(eq(practiceAnswers.studentId, studentId)) - .limit(1000) + .limit(100) return Array.from(new Set(rows.map((r) => r.questionId))) } diff --git a/src/modules/adaptive-practice/data-access.ts b/src/modules/adaptive-practice/data-access.ts index af0d4bc..13f9449 100644 --- a/src/modules/adaptive-practice/data-access.ts +++ b/src/modules/adaptive-practice/data-access.ts @@ -1,6 +1,6 @@ import "server-only" -import { cache } from "react" +import { cacheFn } from "@/shared/lib/cache" import { and, count, desc, eq, inArray } from "drizzle-orm" import { createId } from "@paralleldrive/cuid2" @@ -84,7 +84,7 @@ function mapAnswerRow(row: typeof practiceAnswers.$inferSelect & { // 查询:练习会话列表 // --------------------------------------------------------------------------- -export const getPracticeSessions = cache(async ( +export const getPracticeSessionsRaw = async ( studentId: string, options?: { status?: PracticeStatus @@ -128,13 +128,19 @@ export const getPracticeSessions = cache(async ( data: rows.map(mapSessionRow), total, } +} + +export const getPracticeSessions = cacheFn(getPracticeSessionsRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getPracticeSessions"], }) // --------------------------------------------------------------------------- // 查询:练习会话详情(含答题记录) // --------------------------------------------------------------------------- -export const getPracticeSessionById = cache(async ( +export const getPracticeSessionByIdRaw = async ( sessionId: string, studentId: string, ): Promise => { @@ -180,13 +186,19 @@ export const getPracticeSessionById = cache(async ( sourceMeta: asPracticeSourceMeta(session.sourceMeta), answers: mappedAnswers, } +} + +export const getPracticeSessionById = cacheFn(getPracticeSessionByIdRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getPracticeSessionById"], }) // --------------------------------------------------------------------------- // 查询:练习统计 // --------------------------------------------------------------------------- -export const getPracticeStats = cache(async (studentId: string): Promise => { +export const getPracticeStatsRaw = async (studentId: string): Promise => { const rows = await db .select({ practiceType: practiceSessions.practiceType, @@ -235,6 +247,12 @@ export const getPracticeStats = cache(async (studentId: string): Promise 0 ? totalCorrect / totalQuestionsAnswered : 0, byType, } +} + +export const getPracticeStats = cacheFn(getPracticeStatsRaw, { + tags: ["adaptive-practice"], + ttl: 300, + keyParts: ["adaptive-practice", "getPracticeStats"], }) // --------------------------------------------------------------------------- diff --git a/src/modules/elective/data-access.ts b/src/modules/elective/data-access.ts index ce48164..eb9e775 100644 --- a/src/modules/elective/data-access.ts +++ b/src/modules/elective/data-access.ts @@ -1,6 +1,5 @@ import "server-only" -import { cache } from "react" import { createId } from "@paralleldrive/cuid2" import { and, desc, eq, inArray, sql, type SQL } from "drizzle-orm" @@ -101,11 +100,24 @@ export const buildCourseSelect = () => .from(electiveCourses) /** - * 缓存科目/年级选项(单次请求内复用,避免高频访问时重复查询)。 - * 使用 React `cache()` 在同一渲染周期内去重。 + * 缓存科目/年级选项(带 TTL 与缓存标签,跨请求复用)。 */ -const getCachedSubjectOptions = cache(async () => getCourseDisplayResolver().getSubjectOptions()) -const getCachedGradeOptions = cache(async () => getCourseDisplayResolver().getGradeOptions()) +const getCachedSubjectOptions = cacheFn( + async () => getCourseDisplayResolver().getSubjectOptions(), + { + tags: ["elective"], + ttl: 300, + keyParts: ["elective", "getCachedSubjectOptions"], + }, +) +const getCachedGradeOptions = cacheFn( + async () => getCourseDisplayResolver().getGradeOptions(), + { + tags: ["elective"], + ttl: 300, + keyParts: ["elective", "getCachedGradeOptions"], + }, +) export const resolveCourseDisplayNames = async (rows: CourseCoreRow[]): Promise<{ teacherNames: Map diff --git a/src/modules/exams/data-access-cross-module.ts b/src/modules/exams/data-access-cross-module.ts index 017f74a..7521bd6 100644 --- a/src/modules/exams/data-access-cross-module.ts +++ b/src/modules/exams/data-access-cross-module.ts @@ -3,7 +3,7 @@ import "server-only" import { db } from "@/shared/db" import { exams, examQuestions, examSubmissions, submissionAnswers } from "@/shared/db/schema" import { count, eq, desc, and, inArray, asc, type SQL } from "drizzle-orm" -import { cache } from "react" +import { cacheFn } from "@/shared/lib/cache" import { getClassGradeIdsByClassIds } from "@/modules/classes/data-access" import { getSubjectOptions } from "@/modules/school/data-access" import { getQuestionTypeMapByIds } from "@/modules/questions/data-access" @@ -22,7 +22,7 @@ export type ExamsDashboardStats = { examCount: number } -export const getExamsDashboardStats = cache(async (scope?: DataScope): Promise => { +export const getExamsDashboardStatsRaw = async (scope?: DataScope): Promise => { const conditions = [] if (scope && scope.type !== "all") { @@ -59,6 +59,12 @@ export const getExamsDashboardStats = cache(async (scope?: DataScope): Promise => { +export const getExamsByGradeIdRaw = async (params: { gradeId: string; scope: DataScope }): Promise => { const conditions: SQL[] = [eq(exams.gradeId, params.gradeId)] // scope 过滤 @@ -387,13 +392,17 @@ export const getExamsByGradeId = cache( return { gradeId: params.gradeId, exams: items, totals } } -) + +export const getExamsByGradeId = cacheFn(getExamsByGradeIdRaw, { + tags: ["exams"], + ttl: 300, + keyParts: ["exams", "getExamsByGradeId"], +}) /** * 获取可用于成绩录入的试卷列表(按 scope 过滤,只返回有题目的试卷)。 */ -export const getExamsForGradeEntry = cache( - async (scope: DataScope): Promise => { +export const getExamsForGradeEntryRaw = async (scope: DataScope): Promise => { const conditions: SQL[] = [] if (scope.type === "owned") { @@ -449,13 +458,17 @@ export const getExamsForGradeEntry = cache( } }) } -) + +export const getExamsForGradeEntry = cacheFn(getExamsForGradeEntryRaw, { + tags: ["exams"], + ttl: 300, + keyParts: ["exams", "getExamsForGradeEntry"], +}) /** * 获取单个试卷详情(含题目列表),用于成绩录入表格表头。 */ -export const getExamForGradeEntry = cache( - async (examId: string, scope?: DataScope): Promise => { +export const getExamForGradeEntryRaw = async (examId: string, scope?: DataScope): Promise => { const exam = await db.query.exams.findFirst({ where: eq(exams.id, examId), with: { subject: true, gradeEntity: true }, @@ -508,7 +521,12 @@ export const getExamForGradeEntry = cache( order: q.order ?? 0, score: q.score ?? 0, type: questionTypeMap.get(q.questionId) ?? "text", - })), - } + })), } -) +} + +export const getExamForGradeEntry = cacheFn(getExamForGradeEntryRaw, { + tags: ["exams"], + ttl: 300, + keyParts: ["exams", "getExamForGradeEntry"], +}) diff --git a/src/modules/grades/data-access.ts b/src/modules/grades/data-access.ts index b3975db..055e058 100644 --- a/src/modules/grades/data-access.ts +++ b/src/modules/grades/data-access.ts @@ -1,6 +1,6 @@ import "server-only" -import { cache } from "react" +import { cacheFn } from "@/shared/lib/cache" import { createId } from "@paralleldrive/cuid2" import { and, count, desc, eq, inArray, sql } from "drizzle-orm" @@ -69,8 +69,7 @@ const serializeRecord = (r: typeof gradeRecords.$inferSelect): GradeRecord => ({ updatedAt: r.updatedAt.toISOString(), }) -export const getGradeRecords = cache( - async ( +export const getGradeRecordsRaw = async ( params: GradeQueryParams & { scope: DataScope currentUserId?: string @@ -146,11 +145,22 @@ export const getGradeRecords = cache( return { records, total } } -) -export const getGradeRecordById = cache(async (id: string): Promise => { +export const getGradeRecords = cacheFn(getGradeRecordsRaw, { + tags: ["grades"], + ttl: 300, + keyParts: ["grades", "getGradeRecords"], +}) + +export const getGradeRecordByIdRaw = async (id: string): Promise => { const [row] = await db.select().from(gradeRecords).where(eq(gradeRecords.id, id)).limit(1) return row ? serializeRecord(row) : null +} + +export const getGradeRecordById = cacheFn(getGradeRecordByIdRaw, { + tags: ["grades"], + ttl: 300, + keyParts: ["grades", "getGradeRecordById"], }) export async function createGradeRecord( @@ -289,8 +299,7 @@ export async function bulkDeleteGradeRecords(ids: string[]): Promise { return existingIds.length } -export const getClassGradeStats = cache( - async ( +export const getClassGradeStatsRaw = async ( classId: string, subjectId?: string, examId?: string, @@ -317,7 +326,12 @@ export const getClassGradeStats = cache( return computeGradeStats(rows) } -) + +export const getClassGradeStats = cacheFn(getClassGradeStatsRaw, { + tags: ["grades"], + ttl: 300, + keyParts: ["grades", "getClassGradeStats"], +}) /** * P2-4: 计算单个学生在班级中的排名。 @@ -373,8 +387,7 @@ export async function getStudentRankInClass( return distinctHigherAvgs.size + 1 } -export const getStudentGradeSummary = cache( - async ( +export const getStudentGradeSummaryRaw = async ( studentId: string, scope?: DataScope ): Promise => { @@ -468,10 +481,14 @@ export const getStudentGradeSummary = cache( rank, } } -) -export const getClassRanking = cache( - async ( +export const getStudentGradeSummary = cacheFn(getStudentGradeSummaryRaw, { + tags: ["grades"], + ttl: 300, + keyParts: ["grades", "getStudentGradeSummary"], +}) + +export const getClassRankingRaw = async ( classId: string, subjectId?: string, examId?: string, @@ -529,10 +546,14 @@ export const getClassRanking = cache( return ranked } -) -export const getClassStudentsForEntry = cache( - async ( +export const getClassRanking = cacheFn(getClassRankingRaw, { + tags: ["grades"], + ttl: 300, + keyParts: ["grades", "getClassRanking"], +}) + +export const getClassStudentsForEntryRaw = async ( classId: string, scope?: DataScope ): Promise> => { @@ -559,10 +580,14 @@ export const getClassStudentsForEntry = cache( }) .sort((a, b) => a.name.localeCompare(b.name)) } -) -export const getClassGradeStatsWithMeta = cache( - async ( +export const getClassStudentsForEntry = cacheFn(getClassStudentsForEntryRaw, { + tags: ["grades"], + ttl: 300, + keyParts: ["grades", "getClassStudentsForEntry"], +}) + +export const getClassGradeStatsWithMetaRaw = async ( classId: string, subjectId?: string, examId?: string, @@ -601,4 +626,9 @@ export const getClassGradeStatsWithMeta = cache( studentCount: activeStudentIds.length, } } -) + +export const getClassGradeStatsWithMeta = cacheFn(getClassGradeStatsWithMetaRaw, { + tags: ["grades"], + ttl: 300, + keyParts: ["grades", "getClassGradeStatsWithMeta"], +})