// TeacherService — GraphQL Resolver 的业务逻辑层(v2 P3+ 全量实现) // 通过 5 个 DownstreamClient 调下游 gRPC(B2 裁决:首次实现即 gRPC) // 裁决依据: // - B1(P2 起 GraphQL) // - B2(gRPC 下游通信) // - B8(DownstreamClient 抽象) // - president §2.6(降级模式方案 B:下游不可用时返回空 + warning) // - president §2.9(越权防御 DEV_MODE 放行) // 实现: // - P2 核心 5 Query(dashboard / viewports / me / classes / class) // - P3 考试/作业/成绩查询 + 创建(core-edu) // - P4 知识路径/班级学情/学生薄弱/学习趋势(content + data-ana) // - P5 通知/AI 生成题目(msg + ai) import { Injectable, Inject } from "@nestjs/common"; import { IAM_CLIENT } from "../clients/iam/iam-client.interface.js"; import type { IamClient } from "../clients/iam/iam-client.interface.js"; import { CORE_EDU_CLIENT } from "../clients/core-edu/core-edu-client.interface.js"; import type { CoreEduClient } from "../clients/core-edu/core-edu-client.interface.js"; import { CONTENT_CLIENT } from "../clients/content/content-client.interface.js"; import type { ContentClient } from "../clients/content/content-client.interface.js"; import { DATA_ANA_CLIENT } from "../clients/data-ana/data-ana-client.interface.js"; import type { DataAnaClient } from "../clients/data-ana/data-ana-client.interface.js"; import { MSG_CLIENT } from "../clients/msg/msg-client.interface.js"; import type { MsgClient } from "../clients/msg/msg-client.interface.js"; import { AI_CLIENT } from "../clients/ai/ai-client.interface.js"; import type { AiClient } from "../clients/ai/ai-client.interface.js"; import type { CallContext } from "../clients/types.js"; import type { UserInfo, ViewportItem, EffectivePermissions, UpdateUserRequest, } from "../clients/iam/iam.types.js"; import type { Exam, Homework, Grade, StudentInfo, } from "../clients/core-edu/core-edu.types.js"; import type { KnowledgePoint } from "../clients/content/content.types.js"; import type { ClassPerformance, StudentWeakness as StudentWeaknessDTO, LearningTrend as LearningTrendDTO, } from "../clients/data-ana/data-ana.types.js"; import type { Notification } from "../clients/msg/msg.types.js"; import type { GeneratedQuestion, ChatMessage, StreamChatChunk, } from "../clients/ai/ai.types.js"; import { logger } from "../shared/observability/logger.js"; /** GraphQL User 类型(聚合 UserInfo + EffectivePermissions) */ export interface GraphQLUser { id: string; email: string; name: string; roles: string[]; permissions: string[]; dataScope: string; } /** GraphQL DashboardData 类型 */ export interface DashboardData { user: GraphQLUser | null; classes: unknown[]; viewports: ViewportItem[]; stats: { totalExams: number; pendingGrading: number; todayHomework: number; } | null; } /** GraphQL Class 类型(v2:来自 core-edu) */ export interface ClassInfo { id: string; name: string; gradeId: string; studentCount?: number; } /** GraphQL Student 类型(classStudents 查询返回) */ export interface GraphQLStudent { id: string; name: string; email: string; avatarUrl: string | null; } /** GraphQL ExamDetail 类型(examDetail 查询返回) */ export interface GraphQLExamDetail { id: string; classId: string; title: string; description: string | null; examDate: string; duration: number; totalScore: number; status: string; questions: unknown[]; } /** GraphQL HomeworkDetail 类型(homeworkDetail 查询返回) */ export interface GraphQLHomeworkDetail { id: string; classId: string; title: string; description: string | null; dueDate: string; status: string; submissions: unknown[]; } /** GraphQL GeneratedLessonPlan 类型 */ export interface GraphQLGeneratedLessonPlan { id: string; content: string; summary: string; degraded: boolean; degradedReason: string; } /** GraphQL GeneratedReport 类型 */ export interface GraphQLGeneratedReport { id: string; content: string; summary: string; recommendations: string[]; degraded: boolean; degradedReason: string; } /** GraphQL KnowledgeGraph 类型 */ export interface GraphQLKnowledgeGraph { nodes: unknown[]; edges: unknown[]; } /** GraphQL ClassAnalytics 类型 */ export interface GraphQLClassAnalytics { classId: string; className: string; avgScore: number; passRate: number; avgTrend: number[]; topStudents: unknown[]; weakPoints: string[]; } /** GraphQL StudentAnalytics 类型 */ export interface GraphQLStudentAnalytics { studentId: string; studentName: string; avgScore: number; trend: number[]; weakPoints: string[]; strongPoints: string[]; masteryRate: number; } @Injectable() export class TeacherService { constructor( @Inject(IAM_CLIENT) private readonly iam: IamClient, @Inject(CORE_EDU_CLIENT) private readonly coreEdu: CoreEduClient, @Inject(CONTENT_CLIENT) private readonly content: ContentClient, @Inject(DATA_ANA_CLIENT) private readonly dataAna: DataAnaClient, @Inject(MSG_CLIENT) private readonly msg: MsgClient, @Inject(AI_CLIENT) private readonly ai: AiClient, ) {} // ===== P2 核心 5 Query ===== /** 获取当前用户信息(聚合 iam.GetUserInfo + GetEffectivePermissions) */ async getCurrentUser(ctx: CallContext): Promise { const [userInfo, perms] = await Promise.all([ this.iam.getUserInfo(ctx), this.iam.getEffectivePermissions(ctx), ]); return { id: userInfo.id, email: userInfo.email, name: userInfo.name, roles: userInfo.roles, permissions: perms.permissions, dataScope: perms.dataScope, }; } /** 获取视口配置(iam.GetViewports) */ async getViewports(ctx: CallContext): Promise { return this.iam.getViewports(ctx); } /** * 获取 Dashboard 聚合数据(v2:聚合 iam + core-edu + data-ana) * - user + viewports: iam * - classes: core-edu.GetClassesByTeacher(降级返回空数组) * - stats: data-ana.GetTeacherDashboard(降级返回 null) */ async getDashboard(ctx: CallContext): Promise { const [user, viewports] = await Promise.all([ this.getCurrentUser(ctx), this.iam.getViewports(ctx), ]); // v2: classes 来自 core-edu,stats 来自 data-ana let classes: unknown[] = []; let stats: { totalExams: number; pendingGrading: number; todayHomework: number; } | null = null; try { const classList = await this.coreEdu.getClassesByTeacher(ctx, ctx.userId); classes = classList; } catch (err) { logger.warn( { userId: ctx.userId, err: (err as Error).message }, "Dashboard: core-edu.GetClassesByTeacher failed, returning empty classes", ); } try { stats = await this.dataAna.getTeacherDashboard(ctx, { userId: ctx.userId, }); } catch (err) { logger.warn( { userId: ctx.userId, err: (err as Error).message }, "Dashboard: data-ana.GetTeacherDashboard failed, returning null stats", ); } return { user, viewports, classes, stats }; } /** 获取教师班级列表(v2:core-edu.GetClassesByTeacher) */ async getClasses(ctx: CallContext): Promise { try { const list = await this.coreEdu.getClassesByTeacher(ctx, ctx.userId); return list.map((c) => ({ id: c.id, name: c.name, gradeId: c.gradeId, studentCount: c.studentCount, })); } catch (err) { logger.warn( { userId: ctx.userId, err: (err as Error).message }, "getClasses: core-edu unavailable, returning empty list", ); return []; } } /** 获取单个班级详情(v2:core-edu) */ async getClass(ctx: CallContext, classId: string): Promise { try { const list = await this.coreEdu.getClassesByTeacher(ctx, ctx.userId); const found = list.find((c) => c.id === classId); return found ? { id: found.id, name: found.name, gradeId: found.gradeId, studentCount: found.studentCount, } : null; } catch (err) { logger.warn( { userId: ctx.userId, classId, err: (err as Error).message }, "getClass: core-edu unavailable, returning null", ); return null; } } // ===== P3 考试/作业/成绩 ===== /** 获取班级考试列表(v2:core-edu.ListExamsByClass) */ async getExamsByClass(ctx: CallContext, classId: string): Promise { try { const res = await this.coreEdu.listExamsByClass(ctx, classId); return res.exams; } catch (err) { logger.warn( { userId: ctx.userId, classId, err: (err as Error).message }, "getExamsByClass: core-edu unavailable, returning empty list", ); return []; } } /** 获取班级作业列表(v2:core-edu.ListHomeworkByClass) */ async getHomeworkByClass( ctx: CallContext, classId: string, ): Promise { try { const res = await this.coreEdu.listHomeworkByClass(ctx, classId); return res.homework; } catch (err) { logger.warn( { userId: ctx.userId, classId, err: (err as Error).message }, "getHomeworkByClass: core-edu unavailable, returning empty list", ); return []; } } /** 获取考试成绩列表(v2:core-edu.ListGradesByExam) */ async getGradesByExam(ctx: CallContext, examId: string): Promise { try { const res = await this.coreEdu.listGradesByExam(ctx, examId); return res.grades; } catch (err) { logger.warn( { userId: ctx.userId, examId, err: (err as Error).message }, "getGradesByExam: core-edu unavailable, returning empty list", ); return []; } } /** 创建考试(v2:core-edu.CreateExam) */ async createExam( ctx: CallContext, input: { classId: string; title: string; subject?: string; scheduledAt?: string; }, ): Promise<{ id: string }> { const res = await this.coreEdu.createExam(ctx, { classId: input.classId, title: input.title, description: "", examDate: input.scheduledAt ?? new Date().toISOString(), duration: "60", totalScore: "100", createdBy: ctx.userId, }); return { id: res.id }; } /** 布置作业(v2:core-edu.AssignHomework) */ async assignHomework( ctx: CallContext, input: { classId: string; title: string; dueDate: string }, ): Promise<{ id: string }> { const res = await this.coreEdu.assignHomework(ctx, { classId: input.classId, title: input.title, description: "", dueDate: input.dueDate, createdBy: ctx.userId, }); return { id: res.id }; } /** 记录成绩(v2:core-edu.RecordGrade) */ async recordGrade( ctx: CallContext, input: { examId: string; studentId: string; score: number }, ): Promise<{ id: string }> { const res = await this.coreEdu.recordGrade(ctx, { studentId: input.studentId, examId: input.examId, score: String(input.score), feedback: "", gradedBy: ctx.userId, }); return { id: res.id }; } // ===== P4 知识路径/学情分析 ===== /** 获取知识路径(v2:content.GetLearningPath) */ async getKnowledgePath( ctx: CallContext, knowledgePointId: string, ): Promise<{ knowledgePointId: string; name: string; prerequisites: KnowledgePoint[]; }> { try { // content.GetPrerequisites 返回前置知识点 const prereqRes = await this.content.getPrerequisites(ctx, { knowledgePointId, }); return { knowledgePointId, name: knowledgePointId, prerequisites: prereqRes.points, }; } catch (err) { logger.warn( { userId: ctx.userId, knowledgePointId, err: (err as Error).message }, "getKnowledgePath: content unavailable, returning empty prerequisites", ); return { knowledgePointId, name: knowledgePointId, prerequisites: [] }; } } /** 班级学情分析(v2:data-ana.GetClassPerformance) */ async getClassPerformance( ctx: CallContext, classId: string, ): Promise { try { const now = new Date(); const startDate = new Date( now.getTime() - 30 * 24 * 60 * 60 * 1000, ).toISOString(); const endDate = now.toISOString(); return await this.dataAna.getClassPerformance(ctx, { classId, subjectId: "", startDate, endDate, }); } catch (err) { logger.warn( { userId: ctx.userId, classId, err: (err as Error).message }, "getClassPerformance: data-ana unavailable, returning empty stats", ); return { classId, averageScore: 0, passRate: 0, totalStudents: 0, scores: [], }; } } /** 学生薄弱点(v2:data-ana.GetStudentWeakness) */ async getStudentWeakness( ctx: CallContext, studentId: string, ): Promise { try { return await this.dataAna.getStudentWeakness(ctx, { studentId, subjectId: "", }); } catch (err) { logger.warn( { userId: ctx.userId, studentId, err: (err as Error).message }, "getStudentWeakness: data-ana unavailable, returning empty weak points", ); return { studentId, weakPoints: [] }; } } /** 学习趋势(v2:data-ana.GetLearningTrend) */ async getLearningTrend( ctx: CallContext, studentId: string, dateRange?: { start: string; end: string }, ): Promise { try { const now = new Date(); const endDate = dateRange?.end ?? now.toISOString(); const startDate = dateRange?.start ?? new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(); return await this.dataAna.getLearningTrend(ctx, { studentId, startDate, endDate, subjectId: "", }); } catch (err) { logger.warn( { userId: ctx.userId, studentId, err: (err as Error).message }, "getLearningTrend: data-ana unavailable, returning empty trend", ); return { studentId, points: [] }; } } // ===== P5 通知 + AI ===== /** 通知列表(v2:msg.ListNotifications) */ async getNotifications( ctx: CallContext, unreadOnly?: boolean, ): Promise { try { const res = await this.msg.listNotifications(ctx, { userId: ctx.userId, onlyUnread: unreadOnly ?? false, type: "", page: 1, pageSize: 50, }); return res.notifications; } catch (err) { logger.warn( { userId: ctx.userId, err: (err as Error).message }, "getNotifications: msg unavailable, returning empty list", ); return []; } } /** 标记通知已读(v2:msg.MarkAsRead) */ async markNotificationAsRead( ctx: CallContext, id: string, ): Promise<{ success: boolean }> { try { const res = await this.msg.markAsRead(ctx, { id, userId: ctx.userId }); return { success: res.success }; } catch (err) { logger.warn( { userId: ctx.userId, id, err: (err as Error).message }, "markNotificationAsRead: msg unavailable", ); return { success: false }; } } /** AI 生成题目(v2:ai.GenerateQuestion) */ async generateQuestion( ctx: CallContext, input: { subject: string; gradeLevel?: string; difficulty: string; knowledgePointIds: string[]; }, ): Promise { try { return await this.ai.generateQuestion(ctx, { prompt: "", subject: input.subject, difficulty: input.difficulty, grade: input.gradeLevel, knowledgePointIds: input.knowledgePointIds, }); } catch (err) { logger.warn( { userId: ctx.userId, err: (err as Error).message }, "generateQuestion: ai unavailable, returning degraded response", ); return { question: "", answer: "", explanation: "", questionType: "", difficulty: input.difficulty, knowledgePointIds: input.knowledgePointIds, degraded: true, degradedReason: `AI service unavailable: ${(err as Error).message}`, }; } } /** AI 流式聊天(v2:ai.StreamChat,SSE 透传) */ async streamChat( ctx: CallContext, messages: ChatMessage[], ): Promise> { return this.ai.streamChat(ctx, { messages, model: "", temperature: 0.7, userId: ctx.userId, sessionId: ctx.traceId, }); } // ===== P3-P5 扩展 Service 方法 ===== /** 班级学生列表(v2:core-edu.ListStudentsByClass) */ async getClassStudents( ctx: CallContext, classId: string, ): Promise { try { const students = await this.coreEdu.listStudentsByClass(ctx, classId); return students.map((s: StudentInfo) => ({ id: s.id, name: s.name, email: "", avatarUrl: null, })); } catch (err) { logger.warn( { userId: ctx.userId, classId, err: (err as Error).message }, "getClassStudents: core-edu unavailable, returning empty list", ); return []; } } /** 考试详情(v2:core-edu.GetExam,questions 暂空) */ async getExamDetail( ctx: CallContext, id: string, ): Promise { try { const exam = await this.coreEdu.getExam(ctx, id); return { id: exam.id, classId: exam.classId, title: exam.title, description: exam.description || null, examDate: exam.examDate, duration: Number(exam.duration) || 0, totalScore: Number(exam.totalScore) || 0, status: exam.status, questions: [], }; } catch (err) { logger.warn( { userId: ctx.userId, id, err: (err as Error).message }, "getExamDetail: core-edu unavailable, returning null", ); return null; } } /** 作业详情(v2:core-edu.GetHomework,submissions 暂空) */ async getHomeworkDetail( ctx: CallContext, id: string, ): Promise { try { const hw = await this.coreEdu.getHomework(ctx, id); return { id: hw.id, classId: hw.classId, title: hw.title, description: hw.description || null, dueDate: hw.dueDate, status: hw.status, submissions: [], }; } catch (err) { logger.warn( { userId: ctx.userId, id, err: (err as Error).message }, "getHomeworkDetail: core-edu unavailable, returning null", ); return null; } } /** 更新用户信息(v2:iam.UpdateUser;id 缺省时更新当前用户) */ async updateUser( ctx: CallContext, input: UpdateUserRequest, id?: string, ): Promise { const targetId = id ?? ctx.userId; try { await this.iam.updateUser(ctx, targetId, input); } catch (err) { logger.warn( { userId: ctx.userId, targetId, err: (err as Error).message }, "updateUser: iam unavailable, returning current user snapshot", ); } // 返回最新用户信息(即使更新失败也返回当前快照,避免阻塞前端) return this.getCurrentUser(ctx); } /** 生成教案(ai.GenerateLessonPlan 暂未实现,返回降级响应) */ async generateLessonPlan( ctx: CallContext, input: { classId: string; subject: string; topic: string; objectives: string; }, ): Promise { logger.warn( { userId: ctx.userId, input }, "generateLessonPlan: ai.GenerateLessonPlan not implemented, returning degraded response", ); return { id: `lesson-plan-${Date.now()}`, content: "", summary: "", degraded: true, degradedReason: "AI GenerateLessonPlan RPC not yet available", }; } /** 生成报告(ai.GenerateReport 暂未实现,返回降级响应) */ async generateReport( ctx: CallContext, input: { classId: string; reportType: string; studentId?: string }, ): Promise { logger.warn( { userId: ctx.userId, input }, "generateReport: ai.GenerateReport not implemented, returning degraded response", ); return { id: `report-${Date.now()}`, content: "", summary: "", recommendations: [], degraded: true, degradedReason: "AI GenerateReport RPC not yet available", }; } // ===== P4 扩展 Service 方法 ===== /** 知识图谱(content.GetKnowledgeGraph 暂未实现,返回降级空图) */ async getKnowledgeGraph( ctx: CallContext, _classId?: string, _subject?: string, ): Promise { logger.warn( { userId: ctx.userId }, "getKnowledgeGraph: content.GetKnowledgeGraph not implemented, returning empty graph", ); return { nodes: [], edges: [] }; } /** 班级学情分析(聚合 data-ana.GetClassPerformance + class name) */ async getClassAnalytics( ctx: CallContext, classId: string, ): Promise { try { const perf = await this.getClassPerformance(ctx, classId); return { classId, className: "", avgScore: perf.averageScore, passRate: perf.passRate, avgTrend: [], topStudents: [], weakPoints: [], }; } catch (err) { logger.warn( { userId: ctx.userId, classId, err: (err as Error).message }, "getClassAnalytics: data-ana unavailable, returning empty analytics", ); return { classId, className: "", avgScore: 0, passRate: 0, avgTrend: [], topStudents: [], weakPoints: [], }; } } /** 学生学情分析(聚合 data-ana.GetStudentWeakness + GetLearningTrend) */ async getStudentAnalytics( ctx: CallContext, studentId: string, ): Promise { try { const [weakness, trend] = await Promise.all([ this.getStudentWeakness(ctx, studentId), this.getLearningTrend(ctx, studentId), ]); const trendScores = trend.points.map((p) => p.score); const avgScore = trendScores.length > 0 ? trendScores.reduce((a, b) => a + b, 0) / trendScores.length : 0; return { studentId, studentName: "", avgScore, trend: trendScores, weakPoints: weakness.weakPoints.map((w) => w.title), strongPoints: [], masteryRate: 0, }; } catch (err) { logger.warn( { userId: ctx.userId, studentId, err: (err as Error).message }, "getStudentAnalytics: data-ana unavailable, returning empty analytics", ); return { studentId, studentName: "", avgScore: 0, trend: [], weakPoints: [], strongPoints: [], masteryRate: 0, }; } } } /** 导出 EffectivePermissions 类型供外部使用 */ export type { EffectivePermissions, UserInfo };