Files
Edu/services/teacher-bff/src/teacher/teacher.service.ts

810 lines
23 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.
// TeacherService — GraphQL Resolver 的业务逻辑层v2 P3+ 全量实现)
// 通过 5 个 DownstreamClient 调下游 gRPCB2 裁决:首次实现即 gRPC
// 裁决依据:
// - B1P2 起 GraphQL
// - B2gRPC 下游通信)
// - B8DownstreamClient 抽象)
// - president §2.6(降级模式方案 B下游不可用时返回空 + warning
// - president §2.9(越权防御 DEV_MODE 放行)
// 实现:
// - P2 核心 5 Querydashboard / 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<GraphQLUser> {
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<ViewportItem[]> {
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<DashboardData> {
const [user, viewports] = await Promise.all([
this.getCurrentUser(ctx),
this.iam.getViewports(ctx),
]);
// v2: classes 来自 core-edustats 来自 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 };
}
/** 获取教师班级列表v2core-edu.GetClassesByTeacher */
async getClasses(ctx: CallContext): Promise<ClassInfo[]> {
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 [];
}
}
/** 获取单个班级详情v2core-edu */
async getClass(ctx: CallContext, classId: string): Promise<ClassInfo | null> {
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 考试/作业/成绩 =====
/** 获取班级考试列表v2core-edu.ListExamsByClass */
async getExamsByClass(ctx: CallContext, classId: string): Promise<Exam[]> {
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 [];
}
}
/** 获取班级作业列表v2core-edu.ListHomeworkByClass */
async getHomeworkByClass(
ctx: CallContext,
classId: string,
): Promise<Homework[]> {
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 [];
}
}
/** 获取考试成绩列表v2core-edu.ListGradesByExam */
async getGradesByExam(ctx: CallContext, examId: string): Promise<Grade[]> {
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 [];
}
}
/** 创建考试v2core-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 };
}
/** 布置作业v2core-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 };
}
/** 记录成绩v2core-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 知识路径/学情分析 =====
/** 获取知识路径v2content.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: [] };
}
}
/** 班级学情分析v2data-ana.GetClassPerformance */
async getClassPerformance(
ctx: CallContext,
classId: string,
): Promise<ClassPerformance> {
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: [],
};
}
}
/** 学生薄弱点v2data-ana.GetStudentWeakness */
async getStudentWeakness(
ctx: CallContext,
studentId: string,
): Promise<StudentWeaknessDTO> {
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: [] };
}
}
/** 学习趋势v2data-ana.GetLearningTrend */
async getLearningTrend(
ctx: CallContext,
studentId: string,
dateRange?: { start: string; end: string },
): Promise<LearningTrendDTO> {
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 =====
/** 通知列表v2msg.ListNotifications */
async getNotifications(
ctx: CallContext,
unreadOnly?: boolean,
): Promise<Notification[]> {
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 [];
}
}
/** 标记通知已读v2msg.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 生成题目v2ai.GenerateQuestion */
async generateQuestion(
ctx: CallContext,
input: {
subject: string;
gradeLevel?: string;
difficulty: string;
knowledgePointIds: string[];
},
): Promise<GeneratedQuestion> {
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 流式聊天v2ai.StreamChatSSE 透传) */
async streamChat(
ctx: CallContext,
messages: ChatMessage[],
): Promise<AsyncIterable<StreamChatChunk>> {
return this.ai.streamChat(ctx, {
messages,
model: "",
temperature: 0.7,
userId: ctx.userId,
sessionId: ctx.traceId,
});
}
// ===== P3-P5 扩展 Service 方法 =====
/** 班级学生列表v2core-edu.ListStudentsByClass */
async getClassStudents(
ctx: CallContext,
classId: string,
): Promise<GraphQLStudent[]> {
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 [];
}
}
/** 考试详情v2core-edu.GetExamquestions 暂空) */
async getExamDetail(
ctx: CallContext,
id: string,
): Promise<GraphQLExamDetail | null> {
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;
}
}
/** 作业详情v2core-edu.GetHomeworksubmissions 暂空) */
async getHomeworkDetail(
ctx: CallContext,
id: string,
): Promise<GraphQLHomeworkDetail | null> {
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;
}
}
/** 更新用户信息v2iam.UpdateUserid 缺省时更新当前用户) */
async updateUser(
ctx: CallContext,
input: UpdateUserRequest,
id?: string,
): Promise<GraphQLUser> {
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<GraphQLGeneratedLessonPlan> {
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<GraphQLGeneratedReport> {
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<GraphQLKnowledgeGraph> {
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<GraphQLClassAnalytics> {
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<GraphQLStudentAnalytics> {
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 };