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 { 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 { 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; } }