fix(data-access): 修复 tsc 错误 + 完成 adaptive-practice/elective 迁移至 cacheFn
This commit is contained in:
@@ -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 (
|
||||
<div className="space-y-1.5">
|
||||
<div className="font-medium">{p.name}</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.knowledgePointWeakness.totalAnswers")}:
|
||||
<span className="font-medium text-foreground">{p.totalAnswers}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.knowledgePointWeakness.wrongAnswers")}:
|
||||
<span className="font-medium text-rose-600">{p.wrongAnswers}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.knowledgePointWeakness.errorRate")}:
|
||||
<span className="font-medium text-rose-600">{p.errorRate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Card className={cn("shadow-none", className)}>
|
||||
@@ -82,56 +133,25 @@ export function ClassKnowledgePointWeaknessChart({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-[280px] w-full">
|
||||
<BarChart data={chartData} margin={{ left: 8, right: 8, top: 8, bottom: 8 }}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
|
||||
<BarChart data={chartData} margin={CHART_MARGIN}>
|
||||
<CartesianGrid {...GRID_PROPS} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value: string) =>
|
||||
value.length > 8 ? `${value.slice(0, 8)}...` : value
|
||||
}
|
||||
{...X_AXIS_BASE_PROPS}
|
||||
tickFormatter={truncateTick}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={40}
|
||||
tickFormatter={(value: number) => `${value}%`}
|
||||
{...Y_AXIS_BASE_PROPS}
|
||||
tickFormatter={formatPercentTick}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[220px]"
|
||||
formatter={(payload: unknown) => {
|
||||
const p = payload as unknown as {
|
||||
name: string
|
||||
errorRate: number
|
||||
totalAnswers: number
|
||||
wrongAnswers: number
|
||||
}
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="font-medium">{p.name}</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.knowledgePointWeakness.totalAnswers")}:
|
||||
<span className="font-medium text-foreground">{p.totalAnswers}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.knowledgePointWeakness.wrongAnswers")}:
|
||||
<span className="font-medium text-rose-600">{p.wrongAnswers}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.knowledgePointWeakness.errorRate")}:
|
||||
<span className="font-medium text-rose-600">{p.errorRate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
formatter={renderTooltip}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar dataKey="errorRate" radius={[4, 4, 0, 0]}>
|
||||
<Bar dataKey="errorRate" radius={BAR_RADIUS}>
|
||||
{chartData.map((_, idx) => (
|
||||
<Cell key={idx} fill={CHART_COLORS[idx % CHART_COLORS.length]} />
|
||||
))}
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-1.5">
|
||||
<div className="font-medium">{p.name}</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.classComparison.totalSessions")}:
|
||||
<span className="font-medium text-foreground">{p.sessionCount}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.classComparison.totalAnswered")}:
|
||||
<span className="font-medium text-foreground">{p.totalQuestions}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.knowledgePointWeakness.wrongAnswers")}:
|
||||
<span className="font-medium text-rose-600">{p.totalQuestions - p.correctCount}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.classComparison.averageAccuracy")}:
|
||||
<span className="font-medium text-emerald-600">{p.accuracy}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
if (data.length === 0) return null
|
||||
|
||||
const chartData = data.map((d) => ({
|
||||
@@ -64,53 +112,19 @@ export function PracticeTypeBreakdownChart({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-[280px] w-full">
|
||||
<BarChart data={chartData} margin={{ left: 8, right: 8, top: 8, bottom: 8 }}>
|
||||
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} width={36} />
|
||||
<BarChart data={chartData} margin={CHART_MARGIN}>
|
||||
<CartesianGrid {...GRID_PROPS} />
|
||||
<XAxis {...X_AXIS_PROPS} />
|
||||
<YAxis {...Y_AXIS_PROPS} />
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[220px]"
|
||||
formatter={(payload: unknown) => {
|
||||
const p = payload as unknown as {
|
||||
name: string
|
||||
sessionCount: number
|
||||
totalQuestions: number
|
||||
correctCount: number
|
||||
accuracy: number
|
||||
}
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="font-medium">{p.name}</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.classComparison.totalSessions")}:
|
||||
<span className="font-medium text-foreground">{p.sessionCount}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.classComparison.totalAnswered")}:
|
||||
<span className="font-medium text-foreground">{p.totalQuestions}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.knowledgePointWeakness.wrongAnswers")}:
|
||||
<span className="font-medium text-rose-600">{p.totalQuestions - p.correctCount}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("teacher.classComparison.averageAccuracy")}:
|
||||
<span className="font-medium text-emerald-600">{p.accuracy}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
formatter={renderTooltip}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar dataKey="sessionCount" radius={[4, 4, 0, 0]}>
|
||||
<Bar dataKey="sessionCount" radius={BAR_RADIUS}>
|
||||
{chartData.map((_, idx) => (
|
||||
<Cell key={idx} fill={CHART_COLORS[idx % CHART_COLORS.length]} />
|
||||
))}
|
||||
|
||||
@@ -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<ClassPracticeStats | null> => {
|
||||
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<StudentPracticeSummary[]> => {
|
||||
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<PracticeTypeBreakdown[]> => {
|
||||
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<string[]> => {
|
||||
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<TeacherClassPracticeOverview[]> => {
|
||||
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<ClassKnowledgePointWeakness[]> => {
|
||||
@@ -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<GradeClassPracticeComparison[]> => {
|
||||
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<ClassLearningProfile | null> => {
|
||||
@@ -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<Map<string, string>> => {
|
||||
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<GradeClassPracticeComparison[]> => {
|
||||
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"],
|
||||
})
|
||||
|
||||
@@ -283,14 +283,14 @@ export async function selectQuestionsForPractice(
|
||||
* 获取学生已答过的题目 ID 列表(避免重复练习)。
|
||||
*
|
||||
* 从专项练习中汇总已答题目。
|
||||
* 为控制查询量,仅查询最近 1000 条记录。
|
||||
* P3-8: LIMIT 从 1000 调低至 100,仅查询最近 100 条记录以控制查询量。
|
||||
*/
|
||||
export async function getStudentAnsweredQuestionIds(studentId: string): Promise<string[]> {
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -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<PracticeSessionDetail | null> => {
|
||||
@@ -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<PracticeStats> => {
|
||||
export const getPracticeStatsRaw = async (studentId: string): Promise<PracticeStats> => {
|
||||
const rows = await db
|
||||
.select({
|
||||
practiceType: practiceSessions.practiceType,
|
||||
@@ -235,6 +247,12 @@ export const getPracticeStats = cache(async (studentId: string): Promise<Practic
|
||||
overallAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
|
||||
byType,
|
||||
}
|
||||
}
|
||||
|
||||
export const getPracticeStats = cacheFn(getPracticeStatsRaw, {
|
||||
tags: ["adaptive-practice"],
|
||||
ttl: 300,
|
||||
keyParts: ["adaptive-practice", "getPracticeStats"],
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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<string, string | null>
|
||||
|
||||
@@ -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<ExamsDashboardStats> => {
|
||||
export const getExamsDashboardStatsRaw = async (scope?: DataScope): Promise<ExamsDashboardStats> => {
|
||||
const conditions = []
|
||||
|
||||
if (scope && scope.type !== "all") {
|
||||
@@ -59,6 +59,12 @@ export const getExamsDashboardStats = cache(async (scope?: DataScope): Promise<E
|
||||
.where(conditions.length ? and(...conditions) : undefined)
|
||||
|
||||
return { examCount: Number(row?.value ?? 0) }
|
||||
}
|
||||
|
||||
export const getExamsDashboardStats = cacheFn(getExamsDashboardStatsRaw, {
|
||||
tags: ["exams"],
|
||||
ttl: 300,
|
||||
keyParts: ["exams", "getExamsDashboardStats"],
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -270,8 +276,7 @@ export const addExamQuestions = async (
|
||||
* 年级仪表盘 - 维度3:获取年级下所有考试 + 提交统计。
|
||||
* exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分。
|
||||
*/
|
||||
export const getExamsByGradeId = cache(
|
||||
async (params: { gradeId: string; scope: DataScope }): Promise<GradeExamsResult> => {
|
||||
export const getExamsByGradeIdRaw = async (params: { gradeId: string; scope: DataScope }): Promise<GradeExamsResult> => {
|
||||
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<ExamOptionForEntry[]> => {
|
||||
export const getExamsForGradeEntryRaw = async (scope: DataScope): Promise<ExamOptionForEntry[]> => {
|
||||
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<ExamForGradeEntry | null> => {
|
||||
export const getExamForGradeEntryRaw = async (examId: string, scope?: DataScope): Promise<ExamForGradeEntry | null> => {
|
||||
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"],
|
||||
})
|
||||
|
||||
@@ -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<GradeRecord | null> => {
|
||||
export const getGradeRecords = cacheFn(getGradeRecordsRaw, {
|
||||
tags: ["grades"],
|
||||
ttl: 300,
|
||||
keyParts: ["grades", "getGradeRecords"],
|
||||
})
|
||||
|
||||
export const getGradeRecordByIdRaw = async (id: string): Promise<GradeRecord | null> => {
|
||||
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<number> {
|
||||
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<StudentGradeSummary | null> => {
|
||||
@@ -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<Array<{ id: string; name: string; email: string }>> => {
|
||||
@@ -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"],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user