feat(teacher-bff): admin 命名空间 + 5 个 gRPC client + health probes + merge-resolvers + nextstep 文档
This commit is contained in:
@@ -1,15 +1,54 @@
|
||||
// TeacherService — GraphQL Resolver 的业务逻辑层(B1 裁决:P2 起 GraphQL)
|
||||
// 通过 IamClient 调下游 iam gRPC(B2 裁决:首次实现即 gRPC)
|
||||
// P2: 仅 iam 数据;P3+ 扩展 core-edu / content / data-ana / msg / ai
|
||||
// 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) */
|
||||
@@ -34,23 +73,105 @@ export interface DashboardData {
|
||||
} | null;
|
||||
}
|
||||
|
||||
/** GraphQL Class 类型(P2 mock,P3+ core-edu 真实数据) */
|
||||
/** GraphQL Class 类型(v2:来自 core-edu) */
|
||||
export interface ClassInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
gradeId: string;
|
||||
studentCount?: number;
|
||||
}
|
||||
|
||||
/** P2 mock 班级数据(president §3.5:P2 班级列表来自 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.8:P2 仅调 iam gRPC)
|
||||
* P2: user + viewports 有数据,classes 返回 mock,stats 返回 null + warning
|
||||
* P3+: classes 来自 core-edu,stats 来自 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 返回 null(P4+ 替换为 data-ana.GetTeacherDashboard)
|
||||
logger.warn(
|
||||
{ userId: ctx.userId, phase: "P2" },
|
||||
"Dashboard P2: classes using mock, stats unavailable (field_unavailable_in_p2)",
|
||||
);
|
||||
// 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<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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取单个班级详情(v2:core-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 考试/作业/成绩 =====
|
||||
|
||||
/** 获取班级考试列表(v2:core-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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取班级作业列表(v2:core-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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取考试成绩列表(v2:core-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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建考试(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<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: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 学生薄弱点(v2:data-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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/** 学习趋势(v2:data-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 =====
|
||||
|
||||
/** 通知列表(v2:msg.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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 标记通知已读(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<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 流式聊天(v2:ai.StreamChat,SSE 透传) */
|
||||
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 方法 =====
|
||||
|
||||
/** 班级学生列表(v2:core-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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 考试详情(v2:core-edu.GetExam,questions 暂空) */
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** 作业详情(v2:core-edu.GetHomework,submissions 暂空) */
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新用户信息(v2:iam.UpdateUser;id 缺省时更新当前用户) */
|
||||
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: mock;P3+: 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: mock;P3+: 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user