Files
Edu/services/core-edu/src/dashboard/dashboard.service.ts

246 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { eq, inArray, gte, and } from "drizzle-orm";
import { Injectable } from "@nestjs/common";
import { db } from "../config/database.js";
import { classes } from "../classes/classes.schema.js";
import { exams, examSubmissions } from "../exams/exams.schema.js";
import { homework, homeworkSubmissions } from "../homework/homework.schema.js";
import { grades } from "../grades/grades.schema.js";
import {
NotFoundError,
ValidationError,
} from "../shared/errors/application-error.js";
export interface DashboardData {
teacherId: string;
totalClasses: number;
totalStudents: number;
pendingHomework: number;
upcomingExams: number;
ungradedSubmissions: number;
classes: Array<{
classId: string;
className: string;
studentCount: number;
}>;
upcomingExamList: Array<{
examId: string;
title: string;
examDate: string;
classId: string;
className: string;
}>;
generatedAt: string;
}
export interface ClassPerformance {
classId: string;
className: string;
studentCount: number;
averageScore: string;
highestScore: string;
lowestScore: string;
medianScore: string;
subjects: Array<{
subjectId: string;
subjectName: string;
averageScore: string;
studentCount: number;
}>;
generatedAt: string;
}
@Injectable()
export class DashboardService {
async getDashboard(teacherId: string): Promise<DashboardData> {
if (!teacherId) {
throw new ValidationError("teacherId is required");
}
// 1. 查询该教师负责的班级
const teacherClasses = await db
.select()
.from(classes)
.where(eq(classes.headTeacherId, teacherId));
const classIds = teacherClasses.map((c) => c.id);
const classMap = new Map(teacherClasses.map((c) => [c.id, c.name]));
if (classIds.length === 0) {
return {
teacherId,
totalClasses: 0,
totalStudents: 0,
pendingHomework: 0,
upcomingExams: 0,
ungradedSubmissions: 0,
classes: [],
upcomingExamList: [],
generatedAt: new Date().toISOString(),
};
}
// 2. 构造班级卡片(学生数需 IAM 集成P3.13 stub 用 0 占位)
const classCards = teacherClasses.map((c) => ({
classId: c.id,
className: c.name,
studentCount: 0,
}));
// 3. 统计待处理作业status='assigned'
const pendingHomeworkRows = await db
.select({ id: homework.id })
.from(homework)
.where(
and(
inArray(homework.classId, classIds),
eq(homework.status, "assigned"),
),
);
// 4. 统计即将到来的考试status='published', exam_date >= now
const now = new Date();
const upcomingExamRows = await db
.select()
.from(exams)
.where(
and(
inArray(exams.classId, classIds),
eq(exams.status, "published"),
gte(exams.examDate, now),
),
);
const upcomingExamList = upcomingExamRows.map((e) => ({
examId: e.id,
title: e.title,
examDate:
e.examDate instanceof Date ? e.examDate.toISOString() : e.examDate,
classId: e.classId,
className: classMap.get(e.classId) ?? "",
}));
// 5. 统计未批阅提交exam_submissions + homework_submissions status='submitted'
const examIds = upcomingExamRows.map((e) => e.id);
let ungradedExamCount = 0;
if (examIds.length > 0) {
const ungradedExamRows = await db
.select({ id: examSubmissions.id })
.from(examSubmissions)
.where(
and(
inArray(examSubmissions.examId, examIds),
eq(examSubmissions.status, "submitted"),
),
);
ungradedExamCount = ungradedExamRows.length;
}
const homeworkIds = pendingHomeworkRows.map((h) => h.id);
let ungradedHwCount = 0;
if (homeworkIds.length > 0) {
const ungradedHwRows = await db
.select({ id: homeworkSubmissions.id })
.from(homeworkSubmissions)
.where(
and(
inArray(homeworkSubmissions.homeworkId, homeworkIds),
eq(homeworkSubmissions.status, "submitted"),
),
);
ungradedHwCount = ungradedHwRows.length;
}
return {
teacherId,
totalClasses: teacherClasses.length,
totalStudents: 0, // P3.13 stub: 需 IAM 集成
pendingHomework: pendingHomeworkRows.length,
upcomingExams: upcomingExamRows.length,
ungradedSubmissions: ungradedExamCount + ungradedHwCount,
classes: classCards,
upcomingExamList,
generatedAt: new Date().toISOString(),
};
}
async getClassPerformance(
classId: string,
_subjectId?: string,
): Promise<ClassPerformance> {
if (!classId) {
throw new ValidationError("classId is required");
}
// 1. 查询班级信息
const classRows = await db
.select()
.from(classes)
.where(eq(classes.id, classId))
.limit(1);
const cls = classRows[0];
if (!cls) {
throw new NotFoundError(`Class ${classId} not found`);
}
// 2. 查询该班学生的成绩(通过 exam_id 关联 exam.class_id
const classExamIds = await db
.select({ id: exams.id })
.from(exams)
.where(eq(exams.classId, classId));
const examIdList = classExamIds.map((e) => e.id);
let allGrades: Array<{ score: string; totalScore: string }> = [];
if (examIdList.length > 0) {
const gradeRows = await db
.select({
score: grades.score,
totalScore: grades.totalScore,
})
.from(grades)
.where(inArray(grades.examId, examIdList));
allGrades = gradeRows;
}
// 3. 计算统计值
const percentages = allGrades.map((g) => {
const total = Number(g.totalScore);
return total > 0 ? (Number(g.score) / total) * 100 : 0;
});
// 学生数需 IAM 集成P3.13 stub 用成绩记录去重学生数估算
const average =
percentages.length > 0
? percentages.reduce((a, b) => a + b, 0) / percentages.length
: 0;
const highest = percentages.length > 0 ? Math.max(...percentages) : 0;
const lowest = percentages.length > 0 ? Math.min(...percentages) : 0;
const median = this.calcMedian(percentages);
return {
classId,
className: cls.name,
studentCount: 0, // P3.13 stub: 需 IAM 集成
averageScore: average.toFixed(2),
highestScore: highest.toFixed(2),
lowestScore: lowest.toFixed(2),
medianScore: median.toFixed(2),
subjects: [], // P3.13 stub: 需 content 服务集成获取学科名
generatedAt: new Date().toISOString(),
};
}
private calcMedian(values: number[]): number {
if (values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
const a = sorted[mid - 1] ?? 0;
const b = sorted[mid] ?? 0;
return (a + b) / 2;
}
return sorted[mid] ?? 0;
}
}