feat(teacher-bff): admin 命名空间 + 5 个 gRPC client + health probes + merge-resolvers + nextstep 文档
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// CoreEduClient 接口 + DI token(B8 裁决:DownstreamClient 抽象)
|
||||
// 接口定义所有 P3+ 需要的 core-edu RPC,实现分 gRPC + mock 两种
|
||||
// 选择策略:TEACHER_BFF_DEV_MODE=true → mock,false → gRPC(未就绪 RPC 降级 mock + warning)
|
||||
import type { CallContext } from "../types.js";
|
||||
import type {
|
||||
Exam,
|
||||
Homework,
|
||||
Grade,
|
||||
ClassInfo,
|
||||
StudentInfo,
|
||||
CreateExamRequest,
|
||||
CreateExamResponse,
|
||||
UpdateExamRequest,
|
||||
UpdateExamResponse,
|
||||
DeleteExamResponse,
|
||||
AssignHomeworkRequest,
|
||||
AssignHomeworkResponse,
|
||||
SubmitHomeworkResponse,
|
||||
RecordGradeRequest,
|
||||
RecordGradeResponse,
|
||||
ListExamsResponse,
|
||||
ListHomeworkResponse,
|
||||
ListGradesResponse,
|
||||
} from "./core-edu.types.js";
|
||||
|
||||
/** CoreEduClient DI token(NestJS 注入用) */
|
||||
export const CORE_EDU_CLIENT = Symbol("CORE_EDU_CLIENT");
|
||||
|
||||
/** CoreEduClient 接口(所有 BFF 统一依赖此接口,不依赖具体实现) */
|
||||
export interface CoreEduClient {
|
||||
// ===== ExamService =====
|
||||
createExam(
|
||||
ctx: CallContext,
|
||||
req: CreateExamRequest,
|
||||
): Promise<CreateExamResponse>;
|
||||
getExam(ctx: CallContext, id: string): Promise<Exam>;
|
||||
listExamsByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<ListExamsResponse>;
|
||||
updateExam(
|
||||
ctx: CallContext,
|
||||
req: UpdateExamRequest,
|
||||
): Promise<UpdateExamResponse>;
|
||||
deleteExam(ctx: CallContext, id: string): Promise<DeleteExamResponse>;
|
||||
|
||||
// ===== HomeworkService =====
|
||||
assignHomework(
|
||||
ctx: CallContext,
|
||||
req: AssignHomeworkRequest,
|
||||
): Promise<AssignHomeworkResponse>;
|
||||
getHomework(ctx: CallContext, id: string): Promise<Homework>;
|
||||
listHomeworkByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<ListHomeworkResponse>;
|
||||
submitHomework(ctx: CallContext, id: string): Promise<SubmitHomeworkResponse>;
|
||||
|
||||
// ===== GradeService =====
|
||||
recordGrade(
|
||||
ctx: CallContext,
|
||||
req: RecordGradeRequest,
|
||||
): Promise<RecordGradeResponse>;
|
||||
getGrade(ctx: CallContext, id: string): Promise<Grade>;
|
||||
listGradesByStudent(
|
||||
ctx: CallContext,
|
||||
studentId: string,
|
||||
): Promise<ListGradesResponse>;
|
||||
listGradesByExam(
|
||||
ctx: CallContext,
|
||||
examId: string,
|
||||
): Promise<ListGradesResponse>;
|
||||
listGradesByHomework(
|
||||
ctx: CallContext,
|
||||
homeworkId: string,
|
||||
): Promise<ListGradesResponse>;
|
||||
|
||||
// ===== ClassService(待 coord 补全,未就绪) =====
|
||||
getClassesByTeacher(
|
||||
ctx: CallContext,
|
||||
teacherId: string,
|
||||
): Promise<ClassInfo[]>;
|
||||
listStudentsByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<StudentInfo[]>;
|
||||
batchGetClasses(ctx: CallContext, classIds: string[]): Promise<ClassInfo[]>;
|
||||
|
||||
// ===== 健康检查 =====
|
||||
checkHealth(): Promise<{
|
||||
serving: boolean;
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
// CoreEduClient gRPC 实现(B2 裁决:首次实现即 gRPC)
|
||||
// core_edu.proto 现状 14 RPC(ExamService 5 + HomeworkService 4 + GradeService 5)
|
||||
// 未就绪 RPC(ClassService.GetClassesByTeacher 等)降级 mock + warning
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type * as grpc from "@grpc/grpc-js";
|
||||
import { BaseDownstreamClient } from "../base.client.js";
|
||||
import {
|
||||
createGrpcMetadata,
|
||||
getGrpcClient,
|
||||
checkGrpcHealth,
|
||||
} from "../grpc/grpc.factory.js";
|
||||
import type { CallContext } from "../types.js";
|
||||
import type { CoreEduClient } from "./core-edu-client.interface.js";
|
||||
import type {
|
||||
Exam,
|
||||
Homework,
|
||||
Grade,
|
||||
ClassInfo,
|
||||
StudentInfo,
|
||||
CreateExamRequest,
|
||||
CreateExamResponse,
|
||||
UpdateExamRequest,
|
||||
UpdateExamResponse,
|
||||
DeleteExamResponse,
|
||||
AssignHomeworkRequest,
|
||||
AssignHomeworkResponse,
|
||||
SubmitHomeworkResponse,
|
||||
RecordGradeRequest,
|
||||
RecordGradeResponse,
|
||||
ListExamsResponse,
|
||||
ListHomeworkResponse,
|
||||
ListGradesResponse,
|
||||
} from "./core-edu.types.js";
|
||||
import { CoreEduMockClient } from "./core-edu-mock.client.js";
|
||||
|
||||
/** proto message 字段是 snake_case,gRPC 返回需转 camelCase */
|
||||
function mapExam(raw: Record<string, unknown>): Exam {
|
||||
return {
|
||||
id: String(raw.id ?? ""),
|
||||
classId: String(raw.class_id ?? ""),
|
||||
title: String(raw.title ?? ""),
|
||||
description: String(raw.description ?? ""),
|
||||
examDate: String(raw.exam_date ?? ""),
|
||||
duration: String(raw.duration ?? ""),
|
||||
totalScore: String(raw.total_score ?? ""),
|
||||
status: String(raw.status ?? ""),
|
||||
createdBy: String(raw.created_by ?? ""),
|
||||
createdAt: String(raw.created_at ?? ""),
|
||||
updatedAt: String(raw.updated_at ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
function mapHomework(raw: Record<string, unknown>): Homework {
|
||||
return {
|
||||
id: String(raw.id ?? ""),
|
||||
classId: String(raw.class_id ?? ""),
|
||||
title: String(raw.title ?? ""),
|
||||
description: String(raw.description ?? ""),
|
||||
dueDate: String(raw.due_date ?? ""),
|
||||
status: String(raw.status ?? ""),
|
||||
createdBy: String(raw.created_by ?? ""),
|
||||
createdAt: String(raw.created_at ?? ""),
|
||||
updatedAt: String(raw.updated_at ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
function mapGrade(raw: Record<string, unknown>): Grade {
|
||||
return {
|
||||
id: String(raw.id ?? ""),
|
||||
studentId: String(raw.student_id ?? ""),
|
||||
examId: String(raw.exam_id ?? ""),
|
||||
homeworkId: String(raw.homework_id ?? ""),
|
||||
score: String(raw.score ?? ""),
|
||||
feedback: String(raw.feedback ?? ""),
|
||||
gradedBy: String(raw.graded_by ?? ""),
|
||||
createdAt: String(raw.created_at ?? ""),
|
||||
updatedAt: String(raw.updated_at ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CoreEduGrpcClient
|
||||
extends BaseDownstreamClient
|
||||
implements CoreEduClient
|
||||
{
|
||||
readonly serviceName = "core-edu" as const;
|
||||
private readonly mock: CoreEduMockClient;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.initLogger();
|
||||
this.mock = new CoreEduMockClient();
|
||||
}
|
||||
|
||||
// ===== ExamService =====
|
||||
|
||||
async createExam(
|
||||
ctx: CallContext,
|
||||
req: CreateExamRequest,
|
||||
): Promise<CreateExamResponse> {
|
||||
return this.callGrpc("CreateExam", async () => {
|
||||
const client = getGrpcClient("core-edu", "ExamService") as unknown as {
|
||||
createExam(
|
||||
req: Record<string, unknown>,
|
||||
meta: grpc.Metadata,
|
||||
cb: (err: grpc.ServiceError | null, res: { id: string }) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<CreateExamResponse>((resolve, reject) => {
|
||||
client.createExam(
|
||||
{
|
||||
class_id: req.classId,
|
||||
title: req.title,
|
||||
description: req.description,
|
||||
exam_date: req.examDate,
|
||||
duration: req.duration,
|
||||
total_score: req.totalScore,
|
||||
created_by: req.createdBy,
|
||||
},
|
||||
meta,
|
||||
(err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: res.id });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getExam(ctx: CallContext, id: string): Promise<Exam> {
|
||||
return this.callGrpc("GetExam", async () => {
|
||||
const client = getGrpcClient("core-edu", "ExamService") as unknown as {
|
||||
getExam(
|
||||
req: { id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: Record<string, unknown>,
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<Exam>((resolve, reject) => {
|
||||
client.getExam({ id }, meta, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve(mapExam(res));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async listExamsByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<ListExamsResponse> {
|
||||
return this.callGrpc("ListExamsByClass", async () => {
|
||||
const client = getGrpcClient("core-edu", "ExamService") as unknown as {
|
||||
listExamsByClass(
|
||||
req: { class_id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: { exams?: unknown[] },
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<ListExamsResponse>((resolve, reject) => {
|
||||
client.listExamsByClass({ class_id: classId }, meta, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
const exams = (res.exams ?? []) as Record<string, unknown>[];
|
||||
resolve({ exams: exams.map(mapExam) });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async updateExam(
|
||||
ctx: CallContext,
|
||||
req: UpdateExamRequest,
|
||||
): Promise<UpdateExamResponse> {
|
||||
return this.callGrpc("UpdateExam", async () => {
|
||||
const client = getGrpcClient("core-edu", "ExamService") as unknown as {
|
||||
updateExam(
|
||||
req: Record<string, unknown>,
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: { success: boolean },
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<UpdateExamResponse>((resolve, reject) => {
|
||||
client.updateExam(
|
||||
{
|
||||
id: req.id,
|
||||
title: req.title ?? "",
|
||||
description: req.description ?? "",
|
||||
exam_date: req.examDate ?? "",
|
||||
duration: req.duration ?? "",
|
||||
total_score: req.totalScore ?? "",
|
||||
status: req.status ?? "",
|
||||
},
|
||||
meta,
|
||||
(err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ success: res.success });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async deleteExam(ctx: CallContext, id: string): Promise<DeleteExamResponse> {
|
||||
return this.callGrpc("DeleteExam", async () => {
|
||||
const client = getGrpcClient("core-edu", "ExamService") as unknown as {
|
||||
deleteExam(
|
||||
req: { id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: { success: boolean },
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<DeleteExamResponse>((resolve, reject) => {
|
||||
client.deleteExam({ id }, meta, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ success: res.success });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== HomeworkService =====
|
||||
|
||||
async assignHomework(
|
||||
ctx: CallContext,
|
||||
req: AssignHomeworkRequest,
|
||||
): Promise<AssignHomeworkResponse> {
|
||||
return this.callGrpc("AssignHomework", async () => {
|
||||
const client = getGrpcClient(
|
||||
"core-edu",
|
||||
"HomeworkService",
|
||||
) as unknown as {
|
||||
assignHomework(
|
||||
req: Record<string, unknown>,
|
||||
meta: grpc.Metadata,
|
||||
cb: (err: grpc.ServiceError | null, res: { id: string }) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<AssignHomeworkResponse>((resolve, reject) => {
|
||||
client.assignHomework(
|
||||
{
|
||||
class_id: req.classId,
|
||||
title: req.title,
|
||||
description: req.description,
|
||||
due_date: req.dueDate,
|
||||
created_by: req.createdBy,
|
||||
},
|
||||
meta,
|
||||
(err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: res.id });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getHomework(ctx: CallContext, id: string): Promise<Homework> {
|
||||
return this.callGrpc("GetHomework", async () => {
|
||||
const client = getGrpcClient(
|
||||
"core-edu",
|
||||
"HomeworkService",
|
||||
) as unknown as {
|
||||
getHomework(
|
||||
req: { id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: Record<string, unknown>,
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<Homework>((resolve, reject) => {
|
||||
client.getHomework({ id }, meta, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve(mapHomework(res));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async listHomeworkByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<ListHomeworkResponse> {
|
||||
return this.callGrpc("ListHomeworkByClass", async () => {
|
||||
const client = getGrpcClient(
|
||||
"core-edu",
|
||||
"HomeworkService",
|
||||
) as unknown as {
|
||||
listHomeworkByClass(
|
||||
req: { class_id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: { homework?: unknown[] },
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<ListHomeworkResponse>((resolve, reject) => {
|
||||
client.listHomeworkByClass({ class_id: classId }, meta, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
const homework = (res.homework ?? []) as Record<string, unknown>[];
|
||||
resolve({ homework: homework.map(mapHomework) });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async submitHomework(
|
||||
ctx: CallContext,
|
||||
id: string,
|
||||
): Promise<SubmitHomeworkResponse> {
|
||||
return this.callGrpc("SubmitHomework", async () => {
|
||||
const client = getGrpcClient(
|
||||
"core-edu",
|
||||
"HomeworkService",
|
||||
) as unknown as {
|
||||
submitHomework(
|
||||
req: { id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: { success: boolean },
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<SubmitHomeworkResponse>((resolve, reject) => {
|
||||
client.submitHomework({ id }, meta, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ success: res.success });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== GradeService =====
|
||||
|
||||
async recordGrade(
|
||||
ctx: CallContext,
|
||||
req: RecordGradeRequest,
|
||||
): Promise<RecordGradeResponse> {
|
||||
return this.callGrpc("RecordGrade", async () => {
|
||||
const client = getGrpcClient("core-edu", "GradeService") as unknown as {
|
||||
recordGrade(
|
||||
req: Record<string, unknown>,
|
||||
meta: grpc.Metadata,
|
||||
cb: (err: grpc.ServiceError | null, res: { id: string }) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<RecordGradeResponse>((resolve, reject) => {
|
||||
client.recordGrade(
|
||||
{
|
||||
student_id: req.studentId,
|
||||
exam_id: req.examId ?? "",
|
||||
homework_id: req.homeworkId ?? "",
|
||||
score: req.score,
|
||||
feedback: req.feedback ?? "",
|
||||
graded_by: req.gradedBy,
|
||||
},
|
||||
meta,
|
||||
(err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: res.id });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getGrade(ctx: CallContext, id: string): Promise<Grade> {
|
||||
return this.callGrpc("GetGrade", async () => {
|
||||
const client = getGrpcClient("core-edu", "GradeService") as unknown as {
|
||||
getGrade(
|
||||
req: { id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: Record<string, unknown>,
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<Grade>((resolve, reject) => {
|
||||
client.getGrade({ id }, meta, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve(mapGrade(res));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async listGradesByStudent(
|
||||
ctx: CallContext,
|
||||
studentId: string,
|
||||
): Promise<ListGradesResponse> {
|
||||
return this.callGrpc("ListGradesByStudent", async () => {
|
||||
const client = getGrpcClient("core-edu", "GradeService") as unknown as {
|
||||
listGradesByStudent(
|
||||
req: { student_id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: { grades?: unknown[] },
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<ListGradesResponse>((resolve, reject) => {
|
||||
client.listGradesByStudent(
|
||||
{ student_id: studentId },
|
||||
meta,
|
||||
(err, res) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
const grades = (res.grades ?? []) as Record<string, unknown>[];
|
||||
resolve({ grades: grades.map(mapGrade) });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async listGradesByExam(
|
||||
ctx: CallContext,
|
||||
examId: string,
|
||||
): Promise<ListGradesResponse> {
|
||||
return this.callGrpc("ListGradesByExam", async () => {
|
||||
const client = getGrpcClient("core-edu", "GradeService") as unknown as {
|
||||
listGradesByExam(
|
||||
req: { exam_id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: { grades?: unknown[] },
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<ListGradesResponse>((resolve, reject) => {
|
||||
client.listGradesByExam({ exam_id: examId }, meta, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
const grades = (res.grades ?? []) as Record<string, unknown>[];
|
||||
resolve({ grades: grades.map(mapGrade) });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async listGradesByHomework(
|
||||
ctx: CallContext,
|
||||
homeworkId: string,
|
||||
): Promise<ListGradesResponse> {
|
||||
return this.callGrpc("ListGradesByHomework", async () => {
|
||||
const client = getGrpcClient("core-edu", "GradeService") as unknown as {
|
||||
listGradesByHomework(
|
||||
req: { homework_id: string },
|
||||
meta: grpc.Metadata,
|
||||
cb: (
|
||||
err: grpc.ServiceError | null,
|
||||
res: { grades?: unknown[] },
|
||||
) => void,
|
||||
): void;
|
||||
};
|
||||
const meta = createGrpcMetadata(ctx.userId, ctx.traceId);
|
||||
return new Promise<ListGradesResponse>((resolve, reject) => {
|
||||
client.listGradesByHomework(
|
||||
{ homework_id: homeworkId },
|
||||
meta,
|
||||
(err, res) => {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
const grades = (res.grades ?? []) as Record<string, unknown>[];
|
||||
resolve({ grades: grades.map(mapGrade) });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== ClassService(proto 未定义,降级 mock + warning) =====
|
||||
|
||||
async getClassesByTeacher(
|
||||
ctx: CallContext,
|
||||
teacherId: string,
|
||||
): Promise<ClassInfo[]> {
|
||||
this.log.warn(
|
||||
{
|
||||
rpc: "GetClassesByTeacher",
|
||||
reason: "ClassService not in core_edu.proto yet",
|
||||
},
|
||||
"Downstream RPC not ready, falling back to mock",
|
||||
);
|
||||
return this.mock.getClassesByTeacher(ctx, teacherId);
|
||||
}
|
||||
|
||||
async listStudentsByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<StudentInfo[]> {
|
||||
this.log.warn(
|
||||
{
|
||||
rpc: "ListStudentsByClass",
|
||||
reason: "ClassService not in core_edu.proto yet",
|
||||
},
|
||||
"Downstream RPC not ready, falling back to mock",
|
||||
);
|
||||
return this.mock.listStudentsByClass(ctx, classId);
|
||||
}
|
||||
|
||||
async batchGetClasses(
|
||||
ctx: CallContext,
|
||||
classIds: string[],
|
||||
): Promise<ClassInfo[]> {
|
||||
this.log.warn(
|
||||
{
|
||||
rpc: "BatchGetClasses",
|
||||
reason: "ClassService not in core_edu.proto yet",
|
||||
},
|
||||
"Downstream RPC not ready, falling back to mock",
|
||||
);
|
||||
return this.mock.batchGetClasses(ctx, classIds);
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<{
|
||||
serving: boolean;
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}> {
|
||||
return checkGrpcHealth("core-edu");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// CoreEduClient Mock 实现(B8 裁决:上游就绪前的降级策略)
|
||||
// DEV_MODE=true 时全部走 mock;DEV_MODE=false 时仅未就绪 RPC 走 mock
|
||||
// mock 数据对齐 contract §4.2 mock 策略
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseDownstreamClient } from "../base.client.js";
|
||||
import type { CallContext } from "../types.js";
|
||||
import type { CoreEduClient } from "./core-edu-client.interface.js";
|
||||
import type {
|
||||
Exam,
|
||||
Homework,
|
||||
Grade,
|
||||
ClassInfo,
|
||||
StudentInfo,
|
||||
CreateExamRequest,
|
||||
CreateExamResponse,
|
||||
UpdateExamRequest,
|
||||
UpdateExamResponse,
|
||||
DeleteExamResponse,
|
||||
AssignHomeworkRequest,
|
||||
AssignHomeworkResponse,
|
||||
SubmitHomeworkResponse,
|
||||
RecordGradeRequest,
|
||||
RecordGradeResponse,
|
||||
ListExamsResponse,
|
||||
ListHomeworkResponse,
|
||||
ListGradesResponse,
|
||||
} from "./core-edu.types.js";
|
||||
|
||||
/** mock 考试数据(contract §4.2:返回固定 5 场考试) */
|
||||
const MOCK_EXAMS: Exam[] = [
|
||||
{
|
||||
id: "exam-001",
|
||||
classId: "class-001",
|
||||
title: "语文月考",
|
||||
description: "三年级1班语文月考",
|
||||
examDate: "2026-07-15",
|
||||
duration: "90",
|
||||
totalScore: "100",
|
||||
status: "PUBLISHED",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-01T00:00:00Z",
|
||||
updatedAt: "2026-07-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "exam-002",
|
||||
classId: "class-001",
|
||||
title: "数学单元测验",
|
||||
description: "三年级1班数学单元测验",
|
||||
examDate: "2026-07-18",
|
||||
duration: "60",
|
||||
totalScore: "100",
|
||||
status: "DRAFT",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-02T00:00:00Z",
|
||||
updatedAt: "2026-07-02T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "exam-003",
|
||||
classId: "class-002",
|
||||
title: "英语期末考试",
|
||||
description: "三年级2班英语期末考试",
|
||||
examDate: "2026-07-20",
|
||||
duration: "120",
|
||||
totalScore: "100",
|
||||
status: "PUBLISHED",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-03T00:00:00Z",
|
||||
updatedAt: "2026-07-03T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "exam-004",
|
||||
classId: "class-002",
|
||||
title: "科学测验",
|
||||
description: "三年级2班科学测验",
|
||||
examDate: "2026-07-22",
|
||||
duration: "45",
|
||||
totalScore: "50",
|
||||
status: "DRAFT",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-04T00:00:00Z",
|
||||
updatedAt: "2026-07-04T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "exam-005",
|
||||
classId: "class-003",
|
||||
title: "综合测试",
|
||||
description: "三年级3班综合测试",
|
||||
examDate: "2026-07-25",
|
||||
duration: "90",
|
||||
totalScore: "100",
|
||||
status: "PUBLISHED",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-05T00:00:00Z",
|
||||
updatedAt: "2026-07-05T00:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
/** mock 作业数据(contract §4.2:返回固定 5 份作业) */
|
||||
const MOCK_HOMEWORK: Homework[] = [
|
||||
{
|
||||
id: "hw-001",
|
||||
classId: "class-001",
|
||||
title: "语文作业1",
|
||||
description: "阅读理解练习",
|
||||
dueDate: "2026-07-16",
|
||||
status: "PUBLISHED",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-01T00:00:00Z",
|
||||
updatedAt: "2026-07-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "hw-002",
|
||||
classId: "class-001",
|
||||
title: "数学作业1",
|
||||
description: "加减法练习",
|
||||
dueDate: "2026-07-17",
|
||||
status: "PUBLISHED",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-02T00:00:00Z",
|
||||
updatedAt: "2026-07-02T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "hw-003",
|
||||
classId: "class-002",
|
||||
title: "英语作业1",
|
||||
description: "单词默写",
|
||||
dueDate: "2026-07-18",
|
||||
status: "PUBLISHED",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-03T00:00:00Z",
|
||||
updatedAt: "2026-07-03T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "hw-004",
|
||||
classId: "class-002",
|
||||
title: "科学作业1",
|
||||
description: "观察记录",
|
||||
dueDate: "2026-07-19",
|
||||
status: "PUBLISHED",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-04T00:00:00Z",
|
||||
updatedAt: "2026-07-04T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "hw-005",
|
||||
classId: "class-003",
|
||||
title: "综合作业1",
|
||||
description: "期末复习",
|
||||
dueDate: "2026-07-20",
|
||||
status: "PUBLISHED",
|
||||
createdBy: "teacher-001",
|
||||
createdAt: "2026-07-05T00:00:00Z",
|
||||
updatedAt: "2026-07-05T00:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
/** mock 成绩数据(contract §4.2:返回固定 10 条成绩) */
|
||||
const MOCK_GRADES: Grade[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `grade-${String(i + 1).padStart(3, "0")}`,
|
||||
studentId: `student-${String(i + 1).padStart(3, "0")}`,
|
||||
examId: i < 5 ? "exam-001" : "exam-003",
|
||||
homeworkId: "",
|
||||
score: String(80 + (i % 20)),
|
||||
feedback: i % 2 === 0 ? "表现良好" : "需要努力",
|
||||
gradedBy: "teacher-001",
|
||||
createdAt: "2026-07-10T00:00:00Z",
|
||||
updatedAt: "2026-07-10T00:00:00Z",
|
||||
}));
|
||||
|
||||
/** mock 班级数据(contract §4.2:返回固定 3 个 ClassInfo) */
|
||||
const MOCK_CLASSES: ClassInfo[] = [
|
||||
{ id: "class-001", name: "三年级1班", gradeId: "grade-3", studentCount: 30 },
|
||||
{ id: "class-002", name: "三年级2班", gradeId: "grade-3", studentCount: 28 },
|
||||
{ id: "class-003", name: "三年级3班", gradeId: "grade-3", studentCount: 32 },
|
||||
];
|
||||
|
||||
/** mock 学生数据(contract §4.2:返回固定 30 个 StudentInfo) */
|
||||
const MOCK_STUDENTS: StudentInfo[] = Array.from({ length: 30 }, (_, i) => ({
|
||||
id: `student-${String(i + 1).padStart(3, "0")}`,
|
||||
name: `学生${String(i + 1).padStart(2, "0")}`,
|
||||
classId: "class-001",
|
||||
}));
|
||||
|
||||
@Injectable()
|
||||
export class CoreEduMockClient
|
||||
extends BaseDownstreamClient
|
||||
implements CoreEduClient
|
||||
{
|
||||
readonly serviceName = "core-edu" as const;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.initLogger();
|
||||
}
|
||||
|
||||
async createExam(
|
||||
_ctx: CallContext,
|
||||
req: CreateExamRequest,
|
||||
): Promise<CreateExamResponse> {
|
||||
this.log.debug({ req }, "Mock createExam");
|
||||
return { id: `exam-${Date.now()}` };
|
||||
}
|
||||
|
||||
async getExam(ctx: CallContext, id: string): Promise<Exam> {
|
||||
this.log.debug({ userId: ctx.userId, id }, "Mock getExam");
|
||||
return MOCK_EXAMS[0]!;
|
||||
}
|
||||
|
||||
async listExamsByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<ListExamsResponse> {
|
||||
this.log.debug({ userId: ctx.userId, classId }, "Mock listExamsByClass");
|
||||
return { exams: MOCK_EXAMS.filter((e) => e.classId === classId) };
|
||||
}
|
||||
|
||||
async updateExam(
|
||||
_ctx: CallContext,
|
||||
_req: UpdateExamRequest,
|
||||
): Promise<UpdateExamResponse> {
|
||||
this.log.debug({ req: _req }, "Mock updateExam");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async deleteExam(ctx: CallContext, id: string): Promise<DeleteExamResponse> {
|
||||
this.log.debug({ userId: ctx.userId, id }, "Mock deleteExam");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async assignHomework(
|
||||
_ctx: CallContext,
|
||||
req: AssignHomeworkRequest,
|
||||
): Promise<AssignHomeworkResponse> {
|
||||
this.log.debug({ req }, "Mock assignHomework");
|
||||
return { id: `hw-${Date.now()}` };
|
||||
}
|
||||
|
||||
async getHomework(ctx: CallContext, id: string): Promise<Homework> {
|
||||
this.log.debug({ userId: ctx.userId, id }, "Mock getHomework");
|
||||
return MOCK_HOMEWORK[0]!;
|
||||
}
|
||||
|
||||
async listHomeworkByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<ListHomeworkResponse> {
|
||||
this.log.debug({ userId: ctx.userId, classId }, "Mock listHomeworkByClass");
|
||||
return { homework: MOCK_HOMEWORK.filter((h) => h.classId === classId) };
|
||||
}
|
||||
|
||||
async submitHomework(
|
||||
ctx: CallContext,
|
||||
id: string,
|
||||
): Promise<SubmitHomeworkResponse> {
|
||||
this.log.debug({ userId: ctx.userId, id }, "Mock submitHomework");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async recordGrade(
|
||||
_ctx: CallContext,
|
||||
req: RecordGradeRequest,
|
||||
): Promise<RecordGradeResponse> {
|
||||
this.log.debug({ req }, "Mock recordGrade");
|
||||
return { id: `grade-${Date.now()}` };
|
||||
}
|
||||
|
||||
async getGrade(ctx: CallContext, id: string): Promise<Grade> {
|
||||
this.log.debug({ userId: ctx.userId, id }, "Mock getGrade");
|
||||
return MOCK_GRADES[0]!;
|
||||
}
|
||||
|
||||
async listGradesByStudent(
|
||||
ctx: CallContext,
|
||||
studentId: string,
|
||||
): Promise<ListGradesResponse> {
|
||||
this.log.debug(
|
||||
{ userId: ctx.userId, studentId },
|
||||
"Mock listGradesByStudent",
|
||||
);
|
||||
return { grades: MOCK_GRADES.filter((g) => g.studentId === studentId) };
|
||||
}
|
||||
|
||||
async listGradesByExam(
|
||||
ctx: CallContext,
|
||||
examId: string,
|
||||
): Promise<ListGradesResponse> {
|
||||
this.log.debug({ userId: ctx.userId, examId }, "Mock listGradesByExam");
|
||||
return { grades: MOCK_GRADES.filter((g) => g.examId === examId) };
|
||||
}
|
||||
|
||||
async listGradesByHomework(
|
||||
ctx: CallContext,
|
||||
homeworkId: string,
|
||||
): Promise<ListGradesResponse> {
|
||||
this.log.debug(
|
||||
{ userId: ctx.userId, homeworkId },
|
||||
"Mock listGradesByHomework",
|
||||
);
|
||||
return { grades: MOCK_GRADES.filter((g) => g.homeworkId === homeworkId) };
|
||||
}
|
||||
|
||||
async getClassesByTeacher(
|
||||
ctx: CallContext,
|
||||
teacherId: string,
|
||||
): Promise<ClassInfo[]> {
|
||||
this.log.debug(
|
||||
{ userId: ctx.userId, teacherId },
|
||||
"Mock getClassesByTeacher",
|
||||
);
|
||||
return MOCK_CLASSES;
|
||||
}
|
||||
|
||||
async listStudentsByClass(
|
||||
ctx: CallContext,
|
||||
classId: string,
|
||||
): Promise<StudentInfo[]> {
|
||||
this.log.debug({ userId: ctx.userId, classId }, "Mock listStudentsByClass");
|
||||
return MOCK_STUDENTS.filter((s) => s.classId === classId);
|
||||
}
|
||||
|
||||
async batchGetClasses(
|
||||
ctx: CallContext,
|
||||
classIds: string[],
|
||||
): Promise<ClassInfo[]> {
|
||||
this.log.debug({ userId: ctx.userId, classIds }, "Mock batchGetClasses");
|
||||
return MOCK_CLASSES.filter((c) => classIds.includes(c.id));
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<{
|
||||
serving: boolean;
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}> {
|
||||
return { serving: true, latencyMs: 0 };
|
||||
}
|
||||
}
|
||||
35
services/teacher-bff/src/clients/core-edu/core-edu.module.ts
Normal file
35
services/teacher-bff/src/clients/core-edu/core-edu.module.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// core-edu 模块(B8 裁决:按 DEV_MODE 或 target 配置选择 mock 或 gRPC 实现)
|
||||
import { Module } from "@nestjs/common";
|
||||
import { env } from "../../config/env.js";
|
||||
import { logger } from "../../shared/observability/logger.js";
|
||||
import { CORE_EDU_CLIENT } from "./core-edu-client.interface.js";
|
||||
import { CoreEduGrpcClient } from "./core-edu-grpc.client.js";
|
||||
import { CoreEduMockClient } from "./core-edu-mock.client.js";
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: CORE_EDU_CLIENT,
|
||||
useFactory: () => {
|
||||
// DEV_MODE=true 或 CORE_EDU_GRPC_TARGET 未配置 → 使用 mock
|
||||
if (env.TEACHER_BFF_DEV_MODE || !env.CORE_EDU_GRPC_TARGET) {
|
||||
logger.warn(
|
||||
{
|
||||
devMode: env.TEACHER_BFF_DEV_MODE,
|
||||
target: env.CORE_EDU_GRPC_TARGET,
|
||||
},
|
||||
"CoreEduClient using mock (DEV_MODE or target not configured)",
|
||||
);
|
||||
return new CoreEduMockClient();
|
||||
}
|
||||
logger.info(
|
||||
{ devMode: false, target: env.CORE_EDU_GRPC_TARGET },
|
||||
"CoreEduClient using gRPC",
|
||||
);
|
||||
return new CoreEduGrpcClient();
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [CORE_EDU_CLIENT],
|
||||
})
|
||||
export class CoreEduModule {}
|
||||
136
services/teacher-bff/src/clients/core-edu/core-edu.types.ts
Normal file
136
services/teacher-bff/src/clients/core-edu/core-edu.types.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// core-edu 下游服务类型定义(对齐 core_edu.proto)
|
||||
// core_edu.proto 现状:14 RPC(ExamService 5 + HomeworkService 4 + GradeService 5)
|
||||
// 待 coord 补全:ClassService(GetClassesByTeacher / ListStudentsByClass / BatchGetClasses)+ AttendanceService
|
||||
|
||||
/** 考试(对齐 core_edu.proto Exam message) */
|
||||
export interface Exam {
|
||||
id: string;
|
||||
classId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
examDate: string;
|
||||
duration: string;
|
||||
totalScore: string;
|
||||
status: string;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** 作业(对齐 core_edu.proto Homework message) */
|
||||
export interface Homework {
|
||||
id: string;
|
||||
classId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
dueDate: string;
|
||||
status: string;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** 成绩(对齐 core_edu.proto Grade message) */
|
||||
export interface Grade {
|
||||
id: string;
|
||||
studentId: string;
|
||||
examId: string;
|
||||
homeworkId: string;
|
||||
score: string;
|
||||
feedback: string;
|
||||
gradedBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** 班级信息(待 coord 补 ClassService.GetClassesByTeacher RPC) */
|
||||
export interface ClassInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
gradeId: string;
|
||||
studentCount: number;
|
||||
}
|
||||
|
||||
/** 学生信息(待 coord 补 ClassService.ListStudentsByClass RPC) */
|
||||
export interface StudentInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
classId: string;
|
||||
}
|
||||
|
||||
// ===== Request 类型 =====
|
||||
|
||||
export interface CreateExamRequest {
|
||||
classId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
examDate: string;
|
||||
duration: string;
|
||||
totalScore: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
export interface UpdateExamRequest {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
examDate?: string;
|
||||
duration?: string;
|
||||
totalScore?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface AssignHomeworkRequest {
|
||||
classId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
dueDate: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
export interface RecordGradeRequest {
|
||||
studentId: string;
|
||||
examId?: string;
|
||||
homeworkId?: string;
|
||||
score: string;
|
||||
feedback?: string;
|
||||
gradedBy: string;
|
||||
}
|
||||
|
||||
// ===== Response 类型 =====
|
||||
|
||||
export interface CreateExamResponse {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface UpdateExamResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteExamResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface AssignHomeworkResponse {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface SubmitHomeworkResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface RecordGradeResponse {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ListExamsResponse {
|
||||
exams: Exam[];
|
||||
}
|
||||
|
||||
export interface ListHomeworkResponse {
|
||||
homework: Homework[];
|
||||
}
|
||||
|
||||
export interface ListGradesResponse {
|
||||
grades: Grade[];
|
||||
}
|
||||
Reference in New Issue
Block a user