feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层

包含 clients/graphql/middleware、health probes、shared-ts contracts 等
This commit is contained in:
SpecialX
2026-07-10 19:10:07 +08:00
parent b82593aac2
commit 99155a5ea1
37 changed files with 2862 additions and 324 deletions

View File

@@ -1,158 +1,122 @@
import { Injectable } from "@nestjs/common";
import { env } from "../config/env.js";
// TeacherService — GraphQL Resolver 的业务逻辑层B1 裁决P2 起 GraphQL
// 通过 IamClient 调下游 iam gRPCB2 裁决:首次实现即 gRPC
// P2: 仅 iam 数据P3+ 扩展 core-edu / content / data-ana / 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 type { CallContext } from "../clients/types.js";
import type {
UserInfo,
ViewportItem,
EffectivePermissions,
} from "../clients/iam/iam.types.js";
import { logger } from "../shared/observability/logger.js";
import { BadGatewayError } from "../shared/errors/application-error.js";
export interface ViewportItem {
key: string;
label: string;
route: string;
icon: string | null;
sortOrder: string;
requiredPermission: string | null;
/** GraphQL User 类型(聚合 UserInfo + EffectivePermissions */
export interface GraphQLUser {
id: string;
email: string;
name: string;
roles: string[];
permissions: string[];
dataScope: string;
}
interface DownstreamEnvelope<T> {
success: boolean;
data?: T;
/** GraphQL DashboardData 类型 */
export interface DashboardData {
user: GraphQLUser | null;
classes: unknown[];
viewports: ViewportItem[];
stats: {
totalExams: number;
pendingGrading: number;
todayHomework: number;
} | null;
}
interface DashboardData {
user: unknown;
classes: unknown;
/** GraphQL Class 类型P2 mockP3+ core-edu 真实数据) */
export interface ClassInfo {
id: string;
name: string;
gradeId: string;
}
/** 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" },
];
@Injectable()
export class TeacherService {
// 聚合 IAM + classes 服务的数据
async getDashboard(userId: string): Promise<DashboardData> {
const [iamRes, classesRes] = await Promise.allSettled([
fetch(`${env.IamServiceUrl}/iam/me`, {
headers: { "x-user-id": userId },
}),
fetch(`${env.ClassesServiceUrl}/classes`, {
headers: { "x-user-id": userId },
}),
constructor(@Inject(IAM_CLIENT) private readonly iam: IamClient) {}
/** 获取当前用户信息(聚合 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 聚合数据president §2.8P2 仅调 iam gRPC
* P2: user + viewports 有数据classes 返回 mockstats 返回 null + warning
* P3+: classes 来自 core-edustats 来自 data-ana.GetTeacherDashboard
*/
async getDashboard(ctx: CallContext): Promise<DashboardData> {
const [user, viewports] = await Promise.all([
this.getCurrentUser(ctx),
this.iam.getViewports(ctx),
]);
let user: unknown = null;
let classes: unknown = null;
if (iamRes.status === "fulfilled") {
if (iamRes.value.ok) {
user = await iamRes.value.json();
} else {
logger.warn(
{ status: iamRes.value.status, url: iamRes.value.url },
"Downstream IAM service call failed",
);
}
} else {
logger.warn(
{ err: iamRes.reason, service: "iam" },
"Downstream IAM service call rejected",
);
}
if (classesRes.status === "fulfilled") {
if (classesRes.value.ok) {
classes = await classesRes.value.json();
} else {
logger.warn(
{ status: classesRes.value.status, url: classesRes.value.url },
"Downstream classes service call failed",
);
}
} else {
logger.warn(
{ err: classesRes.reason, service: "classes" },
"Downstream classes service call rejected",
);
}
return { user, classes };
}
// 聚合 IAM 视口配置L1 导航)
async getViewports(userId: string): Promise<ViewportItem[]> {
const res = await fetch(`${env.IamServiceUrl}/iam/viewports`, {
headers: { "x-user-id": userId },
});
if (!res.ok) {
logger.warn(
{ status: res.status, url: res.url },
"Downstream IAM service call failed",
);
throw new BadGatewayError(`Downstream service returned ${res.status}`, {
service: "iam",
endpoint: "viewports",
status: res.status,
});
}
const json = (await res.json()) as DownstreamEnvelope<ViewportItem[]>;
return json.data ?? [];
}
// 聚合班级下的考试列表core-edu
async listExamsByClass(userId: string, classId: string): Promise<unknown> {
const res = await fetch(
`${env.CoreEduServiceUrl}/exams/class/${encodeURIComponent(classId)}`,
{ headers: { "x-user-id": userId } },
// 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)",
);
if (!res.ok) {
logger.warn(
{ status: res.status, url: res.url },
"Downstream core-edu service call failed",
);
throw new BadGatewayError(`Downstream service returned ${res.status}`, {
service: "core-edu",
endpoint: "exams-by-class",
status: res.status,
});
}
const json = (await res.json()) as DownstreamEnvelope<unknown>;
return json.data ?? [];
return {
user,
viewports,
classes: MOCK_CLASSES,
stats: null,
};
}
// 聚合班级下的作业列表core-edu
async listHomeworkByClass(userId: string, classId: string): Promise<unknown> {
const res = await fetch(
`${env.CoreEduServiceUrl}/homework/class/${encodeURIComponent(classId)}`,
{ headers: { "x-user-id": userId } },
/** 获取教师班级列表P2: mockP3+: core-edu.GetClassesByTeacher */
async getClasses(ctx: CallContext): Promise<ClassInfo[]> {
logger.warn(
{ userId: ctx.userId, phase: "P2" },
"Classes P2: using mock data (core-edu not ready, field_unavailable_in_p2)",
);
if (!res.ok) {
logger.warn(
{ status: res.status, url: res.url },
"Downstream core-edu service call failed",
);
throw new BadGatewayError(`Downstream service returned ${res.status}`, {
service: "core-edu",
endpoint: "homework-by-class",
status: res.status,
});
}
const json = (await res.json()) as DownstreamEnvelope<unknown>;
return json.data ?? [];
return MOCK_CLASSES;
}
// 聚合考试下的成绩列表(core-edu
async listGradesByExam(userId: string, examId: string): Promise<unknown> {
const res = await fetch(
`${env.CoreEduServiceUrl}/grades/exam/${encodeURIComponent(examId)}`,
{ headers: { "x-user-id": userId } },
/** 获取单个班级详情P2: mockP3+: core-edu */
async getClass(ctx: CallContext, classId: string): Promise<ClassInfo | null> {
logger.warn(
{ userId: ctx.userId, classId, phase: "P2" },
"Class P2: using mock data (core-edu not ready, field_unavailable_in_p2)",
);
if (!res.ok) {
logger.warn(
{ status: res.status, url: res.url },
"Downstream core-edu service call failed",
);
throw new BadGatewayError(`Downstream service returned ${res.status}`, {
service: "core-edu",
endpoint: "grades-by-exam",
status: res.status,
});
}
const json = (await res.json()) as DownstreamEnvelope<unknown>;
return json.data ?? [];
return MOCK_CLASSES.find((c) => c.id === classId) ?? null;
}
}
/** 导出 EffectivePermissions 类型供外部使用 */
export type { EffectivePermissions, UserInfo };