fix(data-access): 修复 tsc 错误 + 完成 adaptive-practice/elective 迁移至 cacheFn
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback } from "react"
|
||||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
|
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { BarChart3 } from "lucide-react"
|
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"
|
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 {
|
interface ClassKnowledgePointWeaknessChartProps {
|
||||||
data: ClassKnowledgePointWeakness[]
|
data: ClassKnowledgePointWeakness[]
|
||||||
className?: string
|
className?: string
|
||||||
@@ -41,6 +62,36 @@ export function ClassKnowledgePointWeaknessChart({
|
|||||||
}: ClassKnowledgePointWeaknessChartProps) {
|
}: ClassKnowledgePointWeaknessChartProps) {
|
||||||
const t = useTranslations("practice")
|
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) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Card className={cn("shadow-none", className)}>
|
<Card className={cn("shadow-none", className)}>
|
||||||
@@ -82,56 +133,25 @@ export function ClassKnowledgePointWeaknessChart({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ChartContainer config={chartConfig} className="h-[280px] w-full">
|
<ChartContainer config={chartConfig} className="h-[280px] w-full">
|
||||||
<BarChart data={chartData} margin={{ left: 8, right: 8, top: 8, bottom: 8 }}>
|
<BarChart data={chartData} margin={CHART_MARGIN}>
|
||||||
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
|
<CartesianGrid {...GRID_PROPS} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="name"
|
{...X_AXIS_BASE_PROPS}
|
||||||
tickLine={false}
|
tickFormatter={truncateTick}
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
tickFormatter={(value: string) =>
|
|
||||||
value.length > 8 ? `${value.slice(0, 8)}...` : value
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tickLine={false}
|
{...Y_AXIS_BASE_PROPS}
|
||||||
axisLine={false}
|
tickFormatter={formatPercentTick}
|
||||||
width={40}
|
|
||||||
tickFormatter={(value: number) => `${value}%`}
|
|
||||||
/>
|
/>
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
className="w-[220px]"
|
className="w-[220px]"
|
||||||
formatter={(payload: unknown) => {
|
formatter={renderTooltip}
|
||||||
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>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="errorRate" radius={[4, 4, 0, 0]}>
|
<Bar dataKey="errorRate" radius={BAR_RADIUS}>
|
||||||
{chartData.map((_, idx) => (
|
{chartData.map((_, idx) => (
|
||||||
<Cell key={idx} fill={CHART_COLORS[idx % CHART_COLORS.length]} />
|
<Cell key={idx} fill={CHART_COLORS[idx % CHART_COLORS.length]} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback } from "react"
|
||||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
|
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
|
||||||
import { useTranslations } from "next-intl"
|
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"
|
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 {
|
interface PracticeTypeBreakdownChartProps {
|
||||||
data: PracticeTypeBreakdown[]
|
data: PracticeTypeBreakdown[]
|
||||||
className?: string
|
className?: string
|
||||||
@@ -39,46 +52,10 @@ export function PracticeTypeBreakdownChart({
|
|||||||
}: PracticeTypeBreakdownChartProps) {
|
}: PracticeTypeBreakdownChartProps) {
|
||||||
const t = useTranslations("practice")
|
const t = useTranslations("practice")
|
||||||
|
|
||||||
if (data.length === 0) return null
|
const renderTooltip = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
const chartData = data.map((d) => ({
|
// 从 unknown 转换:recharts tooltip payload 类型未知,需类型断言
|
||||||
name: t(`types.${d.practiceType}` as const),
|
const p = payload as {
|
||||||
sessionCount: d.sessionCount,
|
|
||||||
totalQuestions: d.totalQuestions,
|
|
||||||
correctCount: d.correctCount,
|
|
||||||
accuracy: Number((d.accuracy * 100).toFixed(0)),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const chartConfig: ChartConfig = {
|
|
||||||
sessionCount: {
|
|
||||||
label: t("teacher.classComparison.totalSessions"),
|
|
||||||
color: "var(--color-chart-1)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={cn("overflow-hidden", className)}>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">{t("teacher.typeBreakdown.title")}</CardTitle>
|
|
||||||
<CardDescription>{t("teacher.typeBreakdown.description")}</CardDescription>
|
|
||||||
</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} />
|
|
||||||
<ChartTooltip
|
|
||||||
content={
|
|
||||||
<ChartTooltipContent
|
|
||||||
className="w-[220px]"
|
|
||||||
formatter={(payload: unknown) => {
|
|
||||||
const p = payload as unknown as {
|
|
||||||
name: string
|
name: string
|
||||||
sessionCount: number
|
sessionCount: number
|
||||||
totalQuestions: number
|
totalQuestions: number
|
||||||
@@ -106,11 +83,48 @@ export function PracticeTypeBreakdownChart({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
},
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (data.length === 0) return null
|
||||||
|
|
||||||
|
const chartData = data.map((d) => ({
|
||||||
|
name: t(`types.${d.practiceType}` as const),
|
||||||
|
sessionCount: d.sessionCount,
|
||||||
|
totalQuestions: d.totalQuestions,
|
||||||
|
correctCount: d.correctCount,
|
||||||
|
accuracy: Number((d.accuracy * 100).toFixed(0)),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const chartConfig: ChartConfig = {
|
||||||
|
sessionCount: {
|
||||||
|
label: t("teacher.classComparison.totalSessions"),
|
||||||
|
color: "var(--color-chart-1)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={cn("overflow-hidden", className)}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">{t("teacher.typeBreakdown.title")}</CardTitle>
|
||||||
|
<CardDescription>{t("teacher.typeBreakdown.description")}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer config={chartConfig} className="h-[280px] w-full">
|
||||||
|
<BarChart data={chartData} margin={CHART_MARGIN}>
|
||||||
|
<CartesianGrid {...GRID_PROPS} />
|
||||||
|
<XAxis {...X_AXIS_PROPS} />
|
||||||
|
<YAxis {...Y_AXIS_PROPS} />
|
||||||
|
<ChartTooltip
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
className="w-[220px]"
|
||||||
|
formatter={renderTooltip}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="sessionCount" radius={[4, 4, 0, 0]}>
|
<Bar dataKey="sessionCount" radius={BAR_RADIUS}>
|
||||||
{chartData.map((_, idx) => (
|
{chartData.map((_, idx) => (
|
||||||
<Cell key={idx} fill={CHART_COLORS[idx % CHART_COLORS.length]} />
|
<Cell key={idx} fill={CHART_COLORS[idx % CHART_COLORS.length]} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { and, count, desc, eq, inArray, sql } from "drizzle-orm"
|
import { and, count, desc, eq, inArray, sql } from "drizzle-orm"
|
||||||
|
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
@@ -64,7 +64,7 @@ export interface PracticeTypeBreakdown {
|
|||||||
*
|
*
|
||||||
* @param classId 班级 ID
|
* @param classId 班级 ID
|
||||||
*/
|
*/
|
||||||
export const getClassPracticeStats = cache(async (
|
export const getClassPracticeStatsRaw = async (
|
||||||
classId: string,
|
classId: string,
|
||||||
): Promise<ClassPracticeStats | null> => {
|
): Promise<ClassPracticeStats | null> => {
|
||||||
const studentIds = await getActiveStudentIdsByClassId(classId)
|
const studentIds = await getActiveStudentIdsByClassId(classId)
|
||||||
@@ -99,6 +99,12 @@ export const getClassPracticeStats = cache(async (
|
|||||||
averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
|
averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
|
||||||
activeStudents,
|
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
|
* @param classId 班级 ID
|
||||||
*/
|
*/
|
||||||
export const getClassStudentPracticeSummaries = cache(async (
|
export const getClassStudentPracticeSummariesRaw = async (
|
||||||
classId: string,
|
classId: string,
|
||||||
): Promise<StudentPracticeSummary[]> => {
|
): Promise<StudentPracticeSummary[]> => {
|
||||||
const studentIds = await getActiveStudentIdsByClassId(classId)
|
const studentIds = await getActiveStudentIdsByClassId(classId)
|
||||||
@@ -138,6 +144,12 @@ export const getClassStudentPracticeSummaries = cache(async (
|
|||||||
: 0,
|
: 0,
|
||||||
lastPracticeAt: r.lastPracticeAt,
|
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
|
* @param gradeId 年级 ID
|
||||||
*/
|
*/
|
||||||
export const getGradePracticeStats = cache(async (
|
export const getGradePracticeStatsRaw = async (
|
||||||
gradeId: string,
|
gradeId: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
totalSessions: number
|
totalSessions: number
|
||||||
@@ -186,6 +198,12 @@ export const getGradePracticeStats = cache(async (
|
|||||||
averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
|
averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
|
||||||
activeStudents: Number(row.activeStudents),
|
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 列表
|
* @param studentIds 学生 ID 列表
|
||||||
*/
|
*/
|
||||||
export const getPracticeTypeBreakdown = cache(async (
|
export const getPracticeTypeBreakdownRaw = async (
|
||||||
studentIds: string[],
|
studentIds: string[],
|
||||||
): Promise<PracticeTypeBreakdown[]> => {
|
): Promise<PracticeTypeBreakdown[]> => {
|
||||||
if (studentIds.length === 0) return []
|
if (studentIds.length === 0) return []
|
||||||
@@ -218,6 +236,12 @@ export const getPracticeTypeBreakdown = cache(async (
|
|||||||
? Number(r.correctCount) / Number(r.totalQuestions)
|
? Number(r.correctCount) / Number(r.totalQuestions)
|
||||||
: 0,
|
: 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
|
* @param classId 班级 ID
|
||||||
*/
|
*/
|
||||||
export const getStudentsWithoutPractice = cache(async (
|
export const getStudentsWithoutPracticeRaw = async (
|
||||||
classId: string,
|
classId: string,
|
||||||
): Promise<string[]> => {
|
): Promise<string[]> => {
|
||||||
const studentIds = await getActiveStudentIdsByClassId(classId)
|
const studentIds = await getActiveStudentIdsByClassId(classId)
|
||||||
@@ -245,6 +269,12 @@ export const getStudentsWithoutPractice = cache(async (
|
|||||||
|
|
||||||
// 返回没有练习记录的学生
|
// 返回没有练习记录的学生
|
||||||
return studentIds.filter((id) => !activeSet.has(id))
|
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 列表
|
* @param classIds 教师/年级主任可访问的班级 ID 列表
|
||||||
*/
|
*/
|
||||||
export const getTeacherClassPracticeOverviews = cache(async (
|
export const getTeacherClassPracticeOverviewsRaw = async (
|
||||||
classIds: string[],
|
classIds: string[],
|
||||||
): Promise<TeacherClassPracticeOverview[]> => {
|
): Promise<TeacherClassPracticeOverview[]> => {
|
||||||
if (classIds.length === 0) return []
|
if (classIds.length === 0) return []
|
||||||
@@ -351,6 +381,12 @@ export const getTeacherClassPracticeOverviews = cache(async (
|
|||||||
participationRate: s.totalStudents > 0 ? activeStudents / s.totalStudents : 0,
|
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 classId 班级 ID
|
||||||
* @param limit 返回条数(默认 10)
|
* @param limit 返回条数(默认 10)
|
||||||
*/
|
*/
|
||||||
export const getClassKnowledgePointWeakness = cache(async (
|
export const getClassKnowledgePointWeaknessRaw = async (
|
||||||
classId: string,
|
classId: string,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
): Promise<ClassKnowledgePointWeakness[]> => {
|
): Promise<ClassKnowledgePointWeakness[]> => {
|
||||||
@@ -418,6 +454,12 @@ export const getClassKnowledgePointWeakness = cache(async (
|
|||||||
errorRate: totalAnswers > 0 ? wrongAnswers / totalAnswers : 0,
|
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
|
* @param gradeId 年级 ID
|
||||||
*/
|
*/
|
||||||
export const getGradeClassPracticeComparison = cache(async (
|
export const getGradeClassPracticeComparisonRaw = async (
|
||||||
gradeId: string,
|
gradeId: string,
|
||||||
): Promise<GradeClassPracticeComparison[]> => {
|
): Promise<GradeClassPracticeComparison[]> => {
|
||||||
const classList = await getClassesByGradeId(gradeId)
|
const classList = await getClassesByGradeId(gradeId)
|
||||||
@@ -521,6 +563,12 @@ export const getGradeClassPracticeComparison = cache(async (
|
|||||||
|
|
||||||
// 按参与率降序排列
|
// 按参与率降序排列
|
||||||
return results.sort((a, b) => b.participationRate - a.participationRate)
|
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 classId 班级 ID
|
||||||
* @param weakKpLimit 知识点薄弱度返回条数(默认 5)
|
* @param weakKpLimit 知识点薄弱度返回条数(默认 5)
|
||||||
*/
|
*/
|
||||||
export const getClassLearningProfile = cache(async (
|
export const getClassLearningProfileRaw = async (
|
||||||
classId: string,
|
classId: string,
|
||||||
weakKpLimit = 5,
|
weakKpLimit = 5,
|
||||||
): Promise<ClassLearningProfile | null> => {
|
): Promise<ClassLearningProfile | null> => {
|
||||||
@@ -584,6 +632,12 @@ export const getClassLearningProfile = cache(async (
|
|||||||
inactiveStudentIds: inactiveIds,
|
inactiveStudentIds: inactiveIds,
|
||||||
weakKnowledgePoints: weakKps,
|
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 列表
|
* @param studentIds 学生 ID 列表
|
||||||
*/
|
*/
|
||||||
export const getStudentNameMap = cache(async (
|
export const getStudentNameMapRaw = async (
|
||||||
studentIds: string[],
|
studentIds: string[],
|
||||||
): Promise<Map<string, string>> => {
|
): Promise<Map<string, string>> => {
|
||||||
if (studentIds.length === 0) return new Map()
|
if (studentIds.length === 0) return new Map()
|
||||||
@@ -604,6 +658,12 @@ export const getStudentNameMap = cache(async (
|
|||||||
result.set(id, info.name ?? "Unknown")
|
result.set(id, info.name ?? "Unknown")
|
||||||
}
|
}
|
||||||
return result
|
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
|
* @param gradeId 年级 ID
|
||||||
*/
|
*/
|
||||||
export const getGradeClassOverviews = cache(async (
|
export const getGradeClassOverviewsRaw = async (
|
||||||
gradeId: string,
|
gradeId: string,
|
||||||
): Promise<GradeClassPracticeComparison[]> => {
|
): Promise<GradeClassPracticeComparison[]> => {
|
||||||
return getGradeClassPracticeComparison(gradeId)
|
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
|
* @param gradeId 年级 ID
|
||||||
*/
|
*/
|
||||||
export const getGradePracticeOverview = cache(async (
|
export const getGradePracticeOverviewRaw = async (
|
||||||
gradeId: string,
|
gradeId: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
totalClasses: number
|
totalClasses: number
|
||||||
@@ -659,4 +725,10 @@ export const getGradePracticeOverview = cache(async (
|
|||||||
totalCorrect,
|
totalCorrect,
|
||||||
averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
|
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 列表(避免重复练习)。
|
* 获取学生已答过的题目 ID 列表(避免重复练习)。
|
||||||
*
|
*
|
||||||
* 从专项练习中汇总已答题目。
|
* 从专项练习中汇总已答题目。
|
||||||
* 为控制查询量,仅查询最近 1000 条记录。
|
* P3-8: LIMIT 从 1000 调低至 100,仅查询最近 100 条记录以控制查询量。
|
||||||
*/
|
*/
|
||||||
export async function getStudentAnsweredQuestionIds(studentId: string): Promise<string[]> {
|
export async function getStudentAnsweredQuestionIds(studentId: string): Promise<string[]> {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ questionId: practiceAnswers.questionId })
|
.select({ questionId: practiceAnswers.questionId })
|
||||||
.from(practiceAnswers)
|
.from(practiceAnswers)
|
||||||
.where(eq(practiceAnswers.studentId, studentId))
|
.where(eq(practiceAnswers.studentId, studentId))
|
||||||
.limit(1000)
|
.limit(100)
|
||||||
|
|
||||||
return Array.from(new Set(rows.map((r) => r.questionId)))
|
return Array.from(new Set(rows.map((r) => r.questionId)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { and, count, desc, eq, inArray } from "drizzle-orm"
|
import { and, count, desc, eq, inArray } from "drizzle-orm"
|
||||||
import { createId } from "@paralleldrive/cuid2"
|
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,
|
studentId: string,
|
||||||
options?: {
|
options?: {
|
||||||
status?: PracticeStatus
|
status?: PracticeStatus
|
||||||
@@ -128,13 +128,19 @@ export const getPracticeSessions = cache(async (
|
|||||||
data: rows.map(mapSessionRow),
|
data: rows.map(mapSessionRow),
|
||||||
total,
|
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,
|
sessionId: string,
|
||||||
studentId: string,
|
studentId: string,
|
||||||
): Promise<PracticeSessionDetail | null> => {
|
): Promise<PracticeSessionDetail | null> => {
|
||||||
@@ -180,13 +186,19 @@ export const getPracticeSessionById = cache(async (
|
|||||||
sourceMeta: asPracticeSourceMeta(session.sourceMeta),
|
sourceMeta: asPracticeSourceMeta(session.sourceMeta),
|
||||||
answers: mappedAnswers,
|
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
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
practiceType: practiceSessions.practiceType,
|
practiceType: practiceSessions.practiceType,
|
||||||
@@ -235,6 +247,12 @@ export const getPracticeStats = cache(async (studentId: string): Promise<Practic
|
|||||||
overallAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
|
overallAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
|
||||||
byType,
|
byType,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPracticeStats = cacheFn(getPracticeStatsRaw, {
|
||||||
|
tags: ["adaptive-practice"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["adaptive-practice", "getPracticeStats"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
|
||||||
import { createId } from "@paralleldrive/cuid2"
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { and, desc, eq, inArray, sql, type SQL } from "drizzle-orm"
|
import { and, desc, eq, inArray, sql, type SQL } from "drizzle-orm"
|
||||||
|
|
||||||
@@ -101,11 +100,24 @@ export const buildCourseSelect = () =>
|
|||||||
.from(electiveCourses)
|
.from(electiveCourses)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缓存科目/年级选项(单次请求内复用,避免高频访问时重复查询)。
|
* 缓存科目/年级选项(带 TTL 与缓存标签,跨请求复用)。
|
||||||
* 使用 React `cache()` 在同一渲染周期内去重。
|
|
||||||
*/
|
*/
|
||||||
const getCachedSubjectOptions = cache(async () => getCourseDisplayResolver().getSubjectOptions())
|
const getCachedSubjectOptions = cacheFn(
|
||||||
const getCachedGradeOptions = cache(async () => getCourseDisplayResolver().getGradeOptions())
|
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<{
|
export const resolveCourseDisplayNames = async (rows: CourseCoreRow[]): Promise<{
|
||||||
teacherNames: Map<string, string | null>
|
teacherNames: Map<string, string | null>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import "server-only"
|
|||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import { exams, examQuestions, examSubmissions, submissionAnswers } from "@/shared/db/schema"
|
import { exams, examQuestions, examSubmissions, submissionAnswers } from "@/shared/db/schema"
|
||||||
import { count, eq, desc, and, inArray, asc, type SQL } from "drizzle-orm"
|
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 { getClassGradeIdsByClassIds } from "@/modules/classes/data-access"
|
||||||
import { getSubjectOptions } from "@/modules/school/data-access"
|
import { getSubjectOptions } from "@/modules/school/data-access"
|
||||||
import { getQuestionTypeMapByIds } from "@/modules/questions/data-access"
|
import { getQuestionTypeMapByIds } from "@/modules/questions/data-access"
|
||||||
@@ -22,7 +22,7 @@ export type ExamsDashboardStats = {
|
|||||||
examCount: number
|
examCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getExamsDashboardStats = cache(async (scope?: DataScope): Promise<ExamsDashboardStats> => {
|
export const getExamsDashboardStatsRaw = async (scope?: DataScope): Promise<ExamsDashboardStats> => {
|
||||||
const conditions = []
|
const conditions = []
|
||||||
|
|
||||||
if (scope && scope.type !== "all") {
|
if (scope && scope.type !== "all") {
|
||||||
@@ -59,6 +59,12 @@ export const getExamsDashboardStats = cache(async (scope?: DataScope): Promise<E
|
|||||||
.where(conditions.length ? and(...conditions) : undefined)
|
.where(conditions.length ? and(...conditions) : undefined)
|
||||||
|
|
||||||
return { examCount: Number(row?.value ?? 0) }
|
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:获取年级下所有考试 + 提交统计。
|
* 年级仪表盘 - 维度3:获取年级下所有考试 + 提交统计。
|
||||||
* exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分。
|
* exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分。
|
||||||
*/
|
*/
|
||||||
export const getExamsByGradeId = cache(
|
export const getExamsByGradeIdRaw = async (params: { gradeId: string; scope: DataScope }): Promise<GradeExamsResult> => {
|
||||||
async (params: { gradeId: string; scope: DataScope }): Promise<GradeExamsResult> => {
|
|
||||||
const conditions: SQL[] = [eq(exams.gradeId, params.gradeId)]
|
const conditions: SQL[] = [eq(exams.gradeId, params.gradeId)]
|
||||||
|
|
||||||
// scope 过滤
|
// scope 过滤
|
||||||
@@ -387,13 +392,17 @@ export const getExamsByGradeId = cache(
|
|||||||
|
|
||||||
return { gradeId: params.gradeId, exams: items, totals }
|
return { gradeId: params.gradeId, exams: items, totals }
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
export const getExamsByGradeId = cacheFn(getExamsByGradeIdRaw, {
|
||||||
|
tags: ["exams"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["exams", "getExamsByGradeId"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取可用于成绩录入的试卷列表(按 scope 过滤,只返回有题目的试卷)。
|
* 获取可用于成绩录入的试卷列表(按 scope 过滤,只返回有题目的试卷)。
|
||||||
*/
|
*/
|
||||||
export const getExamsForGradeEntry = cache(
|
export const getExamsForGradeEntryRaw = async (scope: DataScope): Promise<ExamOptionForEntry[]> => {
|
||||||
async (scope: DataScope): Promise<ExamOptionForEntry[]> => {
|
|
||||||
const conditions: SQL[] = []
|
const conditions: SQL[] = []
|
||||||
|
|
||||||
if (scope.type === "owned") {
|
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(
|
export const getExamForGradeEntryRaw = async (examId: string, scope?: DataScope): Promise<ExamForGradeEntry | null> => {
|
||||||
async (examId: string, scope?: DataScope): Promise<ExamForGradeEntry | null> => {
|
|
||||||
const exam = await db.query.exams.findFirst({
|
const exam = await db.query.exams.findFirst({
|
||||||
where: eq(exams.id, examId),
|
where: eq(exams.id, examId),
|
||||||
with: { subject: true, gradeEntity: true },
|
with: { subject: true, gradeEntity: true },
|
||||||
@@ -510,5 +523,10 @@ export const getExamForGradeEntry = cache(
|
|||||||
type: questionTypeMap.get(q.questionId) ?? "text",
|
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 "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { createId } from "@paralleldrive/cuid2"
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { and, count, desc, eq, inArray, sql } from "drizzle-orm"
|
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(),
|
updatedAt: r.updatedAt.toISOString(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getGradeRecords = cache(
|
export const getGradeRecordsRaw = async (
|
||||||
async (
|
|
||||||
params: GradeQueryParams & {
|
params: GradeQueryParams & {
|
||||||
scope: DataScope
|
scope: DataScope
|
||||||
currentUserId?: string
|
currentUserId?: string
|
||||||
@@ -146,11 +145,22 @@ export const getGradeRecords = cache(
|
|||||||
|
|
||||||
return { records, total }
|
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)
|
const [row] = await db.select().from(gradeRecords).where(eq(gradeRecords.id, id)).limit(1)
|
||||||
return row ? serializeRecord(row) : null
|
return row ? serializeRecord(row) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGradeRecordById = cacheFn(getGradeRecordByIdRaw, {
|
||||||
|
tags: ["grades"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["grades", "getGradeRecordById"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function createGradeRecord(
|
export async function createGradeRecord(
|
||||||
@@ -289,8 +299,7 @@ export async function bulkDeleteGradeRecords(ids: string[]): Promise<number> {
|
|||||||
return existingIds.length
|
return existingIds.length
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getClassGradeStats = cache(
|
export const getClassGradeStatsRaw = async (
|
||||||
async (
|
|
||||||
classId: string,
|
classId: string,
|
||||||
subjectId?: string,
|
subjectId?: string,
|
||||||
examId?: string,
|
examId?: string,
|
||||||
@@ -317,7 +326,12 @@ export const getClassGradeStats = cache(
|
|||||||
|
|
||||||
return computeGradeStats(rows)
|
return computeGradeStats(rows)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
export const getClassGradeStats = cacheFn(getClassGradeStatsRaw, {
|
||||||
|
tags: ["grades"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["grades", "getClassGradeStats"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P2-4: 计算单个学生在班级中的排名。
|
* P2-4: 计算单个学生在班级中的排名。
|
||||||
@@ -373,8 +387,7 @@ export async function getStudentRankInClass(
|
|||||||
return distinctHigherAvgs.size + 1
|
return distinctHigherAvgs.size + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStudentGradeSummary = cache(
|
export const getStudentGradeSummaryRaw = async (
|
||||||
async (
|
|
||||||
studentId: string,
|
studentId: string,
|
||||||
scope?: DataScope
|
scope?: DataScope
|
||||||
): Promise<StudentGradeSummary | null> => {
|
): Promise<StudentGradeSummary | null> => {
|
||||||
@@ -468,10 +481,14 @@ export const getStudentGradeSummary = cache(
|
|||||||
rank,
|
rank,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export const getClassRanking = cache(
|
export const getStudentGradeSummary = cacheFn(getStudentGradeSummaryRaw, {
|
||||||
async (
|
tags: ["grades"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["grades", "getStudentGradeSummary"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getClassRankingRaw = async (
|
||||||
classId: string,
|
classId: string,
|
||||||
subjectId?: string,
|
subjectId?: string,
|
||||||
examId?: string,
|
examId?: string,
|
||||||
@@ -529,10 +546,14 @@ export const getClassRanking = cache(
|
|||||||
|
|
||||||
return ranked
|
return ranked
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export const getClassStudentsForEntry = cache(
|
export const getClassRanking = cacheFn(getClassRankingRaw, {
|
||||||
async (
|
tags: ["grades"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["grades", "getClassRanking"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getClassStudentsForEntryRaw = async (
|
||||||
classId: string,
|
classId: string,
|
||||||
scope?: DataScope
|
scope?: DataScope
|
||||||
): Promise<Array<{ id: string; name: string; email: string }>> => {
|
): Promise<Array<{ id: string; name: string; email: string }>> => {
|
||||||
@@ -559,10 +580,14 @@ export const getClassStudentsForEntry = cache(
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export const getClassGradeStatsWithMeta = cache(
|
export const getClassStudentsForEntry = cacheFn(getClassStudentsForEntryRaw, {
|
||||||
async (
|
tags: ["grades"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["grades", "getClassStudentsForEntry"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getClassGradeStatsWithMetaRaw = async (
|
||||||
classId: string,
|
classId: string,
|
||||||
subjectId?: string,
|
subjectId?: string,
|
||||||
examId?: string,
|
examId?: string,
|
||||||
@@ -601,4 +626,9 @@ export const getClassGradeStatsWithMeta = cache(
|
|||||||
studentCount: activeStudentIds.length,
|
studentCount: activeStudentIds.length,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
export const getClassGradeStatsWithMeta = cacheFn(getClassGradeStatsWithMetaRaw, {
|
||||||
|
tags: ["grades"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["grades", "getClassGradeStatsWithMeta"],
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user