feat(teacher-bff): admin 命名空间 + 5 个 gRPC client + health probes + merge-resolvers + nextstep 文档

This commit is contained in:
SpecialX
2026-07-14 16:00:46 +08:00
parent 7b790f1276
commit 895a060491
50 changed files with 9739 additions and 334 deletions

View File

@@ -1,15 +1,54 @@
// TeacherService — GraphQL Resolver 的业务逻辑层(B1 裁决P2 起 GraphQL
// 通过 IamClient 调下游 iam gRPCB2 裁决:首次实现即 gRPC
// P2: 仅 iam 数据P3+ 扩展 core-edu / content / data-ana / msg / ai
// 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 */
@@ -34,23 +73,105 @@ export interface DashboardData {
} | null;
}
/** GraphQL Class 类型(P2 mockP3+ core-edu 真实数据 */
/** GraphQL Class 类型(v2来自 core-edu */
export interface ClassInfo {
id: string;
name: string;
gradeId: string;
studentCount?: number;
}
/** P2 mock 班级数据president §3.5P2 班级列表来自 iam 数据P3+ core-edu */
const MOCK_CLASSES: ClassInfo[] = [
{ id: "class-001", name: "三年级1班", gradeId: "grade-3" },
{ id: "class-002", name: "三年级2班", gradeId: "grade-3" },
{ id: "class-003", name: "三年级3班", gradeId: "grade-3" },
];
/** 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) {}
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> {
@@ -74,9 +195,10 @@ export class TeacherService {
}
/**
* 获取 Dashboard 聚合数据(president §2.8P2 仅调 iam gRPC
* P2: user + viewports 有数据classes 返回 mockstats 返回 null + warning
* P3+: classes 来自 core-edustats 来自 data-ana.GetTeacherDashboard
* 获取 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([
@@ -84,37 +206,602 @@ export class TeacherService {
this.iam.getViewports(ctx),
]);
// P2: classes 返回 mock 数据P3+ 替换为 core-edu.GetClassesByTeacher
// P2: stats 返回 nullP4+ 替换为 data-ana.GetTeacherDashboard
logger.warn(
{ userId: ctx.userId, phase: "P2" },
"Dashboard P2: classes using mock, stats unavailable (field_unavailable_in_p2)",
);
// 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 {
user,
viewports,
classes: MOCK_CLASSES,
stats: null,
id: `lesson-plan-${Date.now()}`,
content: "",
summary: "",
degraded: true,
degradedReason: "AI GenerateLessonPlan RPC not yet available",
};
}
/** 获取教师班级列表P2: mockP3+: core-edu.GetClassesByTeacher */
async getClasses(ctx: CallContext): Promise<ClassInfo[]> {
/** 生成报告ai.GenerateReport 暂未实现,返回降级响应 */
async generateReport(
ctx: CallContext,
input: { classId: string; reportType: string; studentId?: string },
): Promise<GraphQLGeneratedReport> {
logger.warn(
{ userId: ctx.userId, phase: "P2" },
"Classes P2: using mock data (core-edu not ready, field_unavailable_in_p2)",
{ userId: ctx.userId, input },
"generateReport: ai.GenerateReport not implemented, returning degraded response",
);
return MOCK_CLASSES;
return {
id: `report-${Date.now()}`,
content: "",
summary: "",
recommendations: [],
degraded: true,
degradedReason: "AI GenerateReport RPC not yet available",
};
}
/** 获取单个班级详情P2: mockP3+: core-edu */
async getClass(ctx: CallContext, classId: string): Promise<ClassInfo | null> {
// ===== P4 扩展 Service 方法 =====
/** 知识图谱content.GetKnowledgeGraph 暂未实现,返回降级空图) */
async getKnowledgeGraph(
ctx: CallContext,
_classId?: string,
_subject?: string,
): Promise<GraphQLKnowledgeGraph> {
logger.warn(
{ userId: ctx.userId, classId, phase: "P2" },
"Class P2: using mock data (core-edu not ready, field_unavailable_in_p2)",
{ userId: ctx.userId },
"getKnowledgeGraph: content.GetKnowledgeGraph not implemented, returning empty graph",
);
return MOCK_CLASSES.find((c) => c.id === classId) ?? null;
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,
};
}
}
}