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

@@ -0,0 +1,95 @@
// CoreEduClient 接口 + DI tokenB8 裁决DownstreamClient 抽象)
// 接口定义所有 P3+ 需要的 core-edu RPC实现分 gRPC + mock 两种
// 选择策略TEACHER_BFF_DEV_MODE=true → mockfalse → 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 tokenNestJS 注入用) */
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;
}>;
}

View File

@@ -0,0 +1,561 @@
// CoreEduClient gRPC 实现B2 裁决:首次实现即 gRPC
// core_edu.proto 现状 14 RPCExamService 5 + HomeworkService 4 + GradeService 5
// 未就绪 RPCClassService.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_casegRPC 返回需转 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) });
}
},
);
});
});
}
// ===== ClassServiceproto 未定义,降级 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");
}
}

View File

@@ -0,0 +1,336 @@
// CoreEduClient Mock 实现B8 裁决:上游就绪前的降级策略)
// DEV_MODE=true 时全部走 mockDEV_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 };
}
}

View 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 {}

View File

@@ -0,0 +1,136 @@
// core-edu 下游服务类型定义(对齐 core_edu.proto
// core_edu.proto 现状14 RPCExamService 5 + HomeworkService 4 + GradeService 5
// 待 coord 补全ClassServiceGetClassesByTeacher / 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[];
}