Files
Edu/services/core-edu/src/grpc/grpc.server.ts

974 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import path from "node:path";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import type { INestApplicationContext } from "@nestjs/common";
import { logger } from "../shared/observability/logger.js";
import { env } from "../config/env.js";
import { ExamsService } from "../exams/exams.service.js";
import { HomeworkService } from "../homework/homework.service.js";
import { GradesService } from "../grades/grades.service.js";
import { ClassesService } from "../classes/classes.service.js";
import { AttendanceService } from "../attendance/attendance.service.js";
import { SchedulingService } from "../scheduling/scheduling.service.js";
import { LeaveRequestsService } from "../leave-requests/leave-requests.service.js";
import { DashboardService } from "../dashboard/dashboard.service.js";
import { AdminService } from "../admin/admin.service.js";
import {
ApplicationError,
NotFoundError,
ValidationError,
ConflictError,
} from "../shared/errors/application-error.js";
import type { Exam } from "../exams/exams.schema.js";
import type { Homework } from "../homework/homework.schema.js";
import type { Grade } from "../grades/grades.schema.js";
import type { Class } from "../classes/classes.schema.js";
import type { Attendance } from "../attendance/attendance.schema.js";
// proto 包名(与 core_edu.proto 中 package 声明一致)
const PROTO_PACKAGE = "next_edu_cloud.core_edu.v1";
// protoLoader 加载选项keepCase 保留 snake_case 字段名
const PROTO_OPTIONS: protoLoader.Options = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
};
let grpcServer: grpc.Server | null = null;
// ----------------------------------------------------------------------------
// proto 文件路径解析
// ----------------------------------------------------------------------------
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* 解析 core_edu.proto 文件路径。
* 按优先级尝试多个候选路径,兼容本地开发与 Docker 运行时:
* 1. 相对于源文件位置devsrc/grpcproddist/grpc目录层级一致
* 2. 相对于 cwd 的 packages/shared-proto/protocwd = repo 根)
* 3. Docker 中 COPY 到 /app/proto 的副本
*/
function resolveProtoPath(): string {
const candidates = [
path.resolve(
__dirname,
"../../../../packages/shared-proto/proto/core_edu.proto",
),
path.resolve(process.cwd(), "packages/shared-proto/proto/core_edu.proto"),
path.resolve(process.cwd(), "proto/core_edu.proto"),
"/app/proto/core_edu.proto",
];
for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate;
}
}
// 全部缺失时回退到首选路径,让 proto-loader 抛出明确的文件不存在错误
const fallback = candidates[0];
return fallback ?? candidates[1] ?? "";
}
/**
* 沿 next_edu_cloud.core_edu.v1 导航 proto 包定义,返回目标包 GrpcObject。
* proto-loader 的 GrpcObject 索引签名包含 ProtobufTypeDefinition
* 包层级在运行时一定是 GrpcObject经 unknown 取出以避开联合类型。
*/
function getCoreEduPackage(protoDescriptor: grpc.GrpcObject): grpc.GrpcObject {
const root = protoDescriptor as unknown as Record<string, unknown>;
const segment = root["next_edu_cloud"];
const coreEdu = (segment as Record<string, unknown> | undefined)?.[
"core_edu"
];
const v1 = (coreEdu as Record<string, unknown> | undefined)?.["v1"];
if (!v1 || typeof v1 !== "object") {
throw new Error(`gRPC package "${PROTO_PACKAGE}" not found in proto`);
}
return v1 as unknown as grpc.GrpcObject;
}
/**
* 从 proto 包中取出服务的 ServiceDefinition。
* proto-loader 生成的服务构造器上有静态 .service 属性(方法定义表),
* 该属性不在 grpc.Client 基类类型声明中,需要经 unknown 断言取出。
*/
function getServiceDefinition(
pkg: grpc.GrpcObject,
serviceName: string,
): grpc.ServiceDefinition<grpc.UntypedServiceImplementation> {
const service = pkg[serviceName];
if (service === undefined || typeof service !== "function") {
throw new Error(`gRPC service "${serviceName}" not found in proto package`);
}
const ctor = service as unknown as {
service: grpc.ServiceDefinition<grpc.UntypedServiceImplementation>;
};
if (!ctor.service) {
throw new Error(
`Service definition missing on "${serviceName}" constructor`,
);
}
return ctor.service;
}
// ----------------------------------------------------------------------------
// 错误处理:将 ApplicationError 映射为 gRPC status code
// ----------------------------------------------------------------------------
/**
* gRPC 服务端错误,实现 grpc.ServiceError 接口以便 callback 直接使用。
*/
class GrpcServiceError extends Error implements grpc.ServiceError {
readonly code: grpc.status;
readonly details: string;
readonly metadata: grpc.Metadata;
constructor(code: grpc.status, message: string) {
super(message);
this.name = "GrpcServiceError";
this.code = code;
this.details = message;
this.metadata = new grpc.Metadata();
}
}
function toGrpcError(err: unknown): grpc.ServiceError {
if (err instanceof NotFoundError) {
return new GrpcServiceError(grpc.status.NOT_FOUND, err.message);
}
if (err instanceof ValidationError) {
return new GrpcServiceError(grpc.status.INVALID_ARGUMENT, err.message);
}
if (err instanceof ConflictError) {
return new GrpcServiceError(grpc.status.FAILED_PRECONDITION, err.message);
}
if (err instanceof ApplicationError) {
// 按业务错误状态码补充映射
if (err.statusCode === 401) {
return new GrpcServiceError(grpc.status.UNAUTHENTICATED, err.message);
}
if (err.statusCode === 403) {
return new GrpcServiceError(grpc.status.PERMISSION_DENIED, err.message);
}
return new GrpcServiceError(grpc.status.INTERNAL, err.message);
}
const message = err instanceof Error ? err.message : "Internal server error";
return new GrpcServiceError(grpc.status.INTERNAL, message);
}
// ----------------------------------------------------------------------------
// handler 包装:把 async (req) => Promise<resp> 转为 grpc handleUnaryCall
// ----------------------------------------------------------------------------
type ProtoRequest = Record<string, unknown>;
type ProtoResponse = Record<string, unknown>;
function wrapHandler(
handler: (req: ProtoRequest) => Promise<ProtoResponse>,
): grpc.handleUnaryCall<unknown, unknown> {
return (call, callback) => {
// call.request 类型为 unknownaddService 的实现签名),此处从 unknown 转换为记录
const request = call.request as ProtoRequest;
Promise.resolve()
.then(() => handler(request))
.then((result) => callback(null, result))
.catch((err: unknown) => callback(toGrpcError(err)));
};
}
// ----------------------------------------------------------------------------
// 字段转换工具Date -> ISO stringnull -> ""
// ----------------------------------------------------------------------------
function toIso(value: Date | string | null | undefined): string {
if (value === null || value === undefined) {
return "";
}
if (typeof value === "string") {
return value;
}
return value.toISOString();
}
function str(value: string | null | undefined): string {
return value ?? "";
}
// ----------------------------------------------------------------------------
// 实体 -> proto message 映射camelCase -> snake_case + ISO 日期)
// ----------------------------------------------------------------------------
function toExamProto(exam: Exam): ProtoResponse {
return {
id: exam.id,
class_id: exam.classId,
subject_id: exam.subjectId,
title: exam.title,
description: str(exam.description),
exam_date: toIso(exam.examDate),
duration: exam.duration,
total_score: exam.totalScore,
status: exam.status,
status_changed_at: toIso(exam.statusChangedAt),
status_changed_by: str(exam.statusChangedBy),
school_id: exam.schoolId,
created_by: exam.createdBy,
archived_at: toIso(exam.archivedAt),
created_at: toIso(exam.createdAt),
updated_at: toIso(exam.updatedAt),
};
}
function toHomeworkProto(hw: Homework): ProtoResponse {
return {
id: hw.id,
class_id: hw.classId,
subject_id: hw.subjectId,
title: hw.title,
description: str(hw.description),
due_date: toIso(hw.dueDate),
grace_period: hw.gracePeriod,
status: hw.status,
school_id: hw.schoolId,
created_by: hw.createdBy,
created_at: toIso(hw.createdAt),
updated_at: toIso(hw.updatedAt),
};
}
function toGradeProto(grade: Grade): ProtoResponse {
return {
id: grade.id,
student_id: grade.studentId,
exam_id: str(grade.examId),
homework_id: str(grade.homeworkId),
score: grade.score,
total_score: grade.totalScore,
feedback: str(grade.feedback),
graded_by: grade.gradedBy,
school_id: grade.schoolId,
idempotency_key: str(grade.idempotencyKey),
created_at: toIso(grade.createdAt),
updated_at: toIso(grade.updatedAt),
};
}
function toClassProto(cls: Class): ProtoResponse {
return {
id: cls.id,
name: cls.name,
grade_id: cls.gradeId,
head_teacher_id: str(cls.headTeacherId),
description: str(cls.description),
created_at: toIso(cls.createdAt),
updated_at: toIso(cls.updatedAt),
};
}
function toAttendanceProto(att: Attendance): ProtoResponse {
return {
id: att.id,
schedule_id: att.scheduleId,
student_id: att.studentId,
status: att.status,
remark: str(att.remark),
recorded_by: att.recordedBy,
school_id: att.schoolId,
created_at: toIso(att.createdAt),
updated_at: toIso(att.updatedAt),
};
}
// ----------------------------------------------------------------------------
// proto 请求字段读取工具(从 snake_case 请求中安全取值)
// ----------------------------------------------------------------------------
function reqStr(req: ProtoRequest, key: string): string {
const v = req[key];
return typeof v === "string" ? v : "";
}
function reqNum(req: ProtoRequest, key: string): number {
const v = req[key];
return typeof v === "number" ? v : 0;
}
function reqStrArr(req: ProtoRequest, key: string): string[] {
const v = req[key];
if (!Array.isArray(v)) {
return [];
}
// Array.isArray 将 unknown 收窄为 any[],先经 unknown[] 再用类型守卫过滤
const arr = v as unknown[];
return arr.filter((x): x is string => typeof x === "string");
}
function reqObjArr(req: ProtoRequest, key: string): ProtoRequest[] {
const v = req[key];
if (!Array.isArray(v)) {
return [];
}
const arr = v as unknown[];
return arr.map((item) =>
item !== null && typeof item === "object" ? (item as ProtoRequest) : {},
);
}
// ----------------------------------------------------------------------------
// ExamService handlers8 RPC
// ----------------------------------------------------------------------------
function buildExamHandlers(
service: ExamsService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
CreateExam: wrapHandler(async (req) => {
const result = await service.createExam({
classId: reqStr(req, "class_id"),
subjectId: reqStr(req, "subject_id"),
title: reqStr(req, "title"),
description: reqStr(req, "description") || undefined,
examDate: reqStr(req, "exam_date"),
duration: reqNum(req, "duration"),
totalScore: reqStr(req, "total_score"),
schoolId: reqStr(req, "school_id"),
createdBy: reqStr(req, "created_by"),
});
return { id: result.id };
}),
GetExam: wrapHandler(async (req) => {
const exam = await service.getExam(reqStr(req, "id"));
return toExamProto(exam);
}),
ListExamsByClass: wrapHandler(async (req) => {
const exams = await service.listExamsByClass(reqStr(req, "class_id"));
return { exams: exams.map(toExamProto) };
}),
UpdateExam: wrapHandler(async (req) => {
await service.updateExam(reqStr(req, "id"), {
title: reqStr(req, "title") || undefined,
description: reqStr(req, "description") || undefined,
examDate: reqStr(req, "exam_date")
? new Date(reqStr(req, "exam_date"))
: undefined,
duration: reqNum(req, "duration") || undefined,
totalScore: reqStr(req, "total_score") || undefined,
});
return { success: true };
}),
DeleteExam: wrapHandler(async (req) => {
await service.deleteExam(reqStr(req, "id"));
return { success: true };
}),
PublishExam: wrapHandler(async (req) => {
await service.publishExam(reqStr(req, "id"), reqStr(req, "published_by"));
return { success: true };
}),
SubmitExam: wrapHandler(async (req) => {
const answers = reqObjArr(req, "answers").map((a) => ({
questionId: reqStr(a, "question_id"),
answer: reqStr(a, "answer"),
}));
const result = await service.submitExam(
reqStr(req, "exam_id"),
reqStr(req, "student_id"),
answers,
);
return { submission_id: result.submissionId };
}),
GradeExam: wrapHandler(async (req) => {
const scores = reqObjArr(req, "scores").map((s) => ({
questionId: reqStr(s, "question_id"),
score: reqStr(s, "score"),
teacherComment: reqStr(s, "teacher_comment") || undefined,
}));
const result = await service.gradeExam(
reqStr(req, "exam_id"),
reqStr(req, "submission_id"),
scores,
reqStr(req, "graded_by"),
);
return { success: true, total_score: result.totalScore };
}),
// P3.13 新增
SaveExamDraft: wrapHandler(async (req) => {
const answers = reqObjArr(req, "answers").map((a) => ({
questionId: reqStr(a, "question_id"),
answer: reqStr(a, "answer"),
}));
const result = await service.saveExamDraft(
reqStr(req, "exam_id"),
reqStr(req, "student_id"),
answers,
);
return { draft_id: result.draftId };
}),
RecordExamViolation: wrapHandler(async (req) => {
const result = await service.recordExamViolation(
reqStr(req, "exam_id"),
reqStr(req, "student_id"),
reqStr(req, "violation_type"),
reqStr(req, "detail"),
reqNum(req, "severity"),
);
return { violation_id: result.violationId };
}),
};
}
// ----------------------------------------------------------------------------
// HomeworkService handlers5 RPC
// ----------------------------------------------------------------------------
function buildHomeworkHandlers(
service: HomeworkService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
AssignHomework: wrapHandler(async (req) => {
const result = await service.assignHomework({
classId: reqStr(req, "class_id"),
subjectId: reqStr(req, "subject_id"),
title: reqStr(req, "title"),
description: reqStr(req, "description") || undefined,
dueDate: reqStr(req, "due_date"),
gracePeriod: reqNum(req, "grace_period"),
schoolId: reqStr(req, "school_id"),
createdBy: reqStr(req, "created_by"),
});
return { id: result.id };
}),
GetHomework: wrapHandler(async (req) => {
const hw = await service.getHomework(reqStr(req, "id"));
return toHomeworkProto(hw);
}),
ListHomeworkByClass: wrapHandler(async (req) => {
const list = await service.listByClass(reqStr(req, "class_id"));
return { homework: list.map(toHomeworkProto) };
}),
SubmitHomework: wrapHandler(async (req) => {
const answers = reqObjArr(req, "answers").map((a) => ({
questionId: reqStr(a, "question_id"),
answer: reqStr(a, "answer"),
}));
const result = await service.submitHomework(
reqStr(req, "homework_id"),
reqStr(req, "student_id"),
answers,
);
return { submission_id: result.submissionId };
}),
GradeHomework: wrapHandler(async (req) => {
const scores = reqObjArr(req, "scores").map((s) => ({
questionId: reqStr(s, "question_id"),
score: reqStr(s, "score"),
teacherComment: reqStr(s, "teacher_comment") || undefined,
}));
const result = await service.gradeHomework(
reqStr(req, "homework_id"),
reqStr(req, "submission_id"),
scores,
reqStr(req, "feedback") || undefined,
reqStr(req, "graded_by"),
);
return { success: true, total_score: result.totalScore };
}),
};
}
// ----------------------------------------------------------------------------
// GradeService handlers6 RPC
// ----------------------------------------------------------------------------
function buildGradeHandlers(
service: GradesService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
RecordGrade: wrapHandler(async (req) => {
const result = await service.recordGrade({
studentId: reqStr(req, "student_id"),
examId: reqStr(req, "exam_id") || undefined,
homeworkId: reqStr(req, "homework_id") || undefined,
score: reqStr(req, "score"),
totalScore: reqStr(req, "total_score"),
feedback: reqStr(req, "feedback") || undefined,
gradedBy: reqStr(req, "graded_by"),
schoolId: reqStr(req, "school_id"),
idempotencyKey: reqStr(req, "idempotency_key") || undefined,
});
return { id: result.id };
}),
GetGrade: wrapHandler(async (req) => {
const grade = await service.getGrade(reqStr(req, "id"));
return toGradeProto(grade);
}),
ListGradesByStudent: wrapHandler(async (req) => {
const grades = await service.listByStudent(reqStr(req, "student_id"));
return { grades: grades.map(toGradeProto) };
}),
ListGradesByExam: wrapHandler(async (req) => {
const grades = await service.listByExam(reqStr(req, "exam_id"));
return { grades: grades.map(toGradeProto) };
}),
ListGradesByHomework: wrapHandler(async (req) => {
const grades = await service.listByHomework(reqStr(req, "homework_id"));
return { grades: grades.map(toGradeProto) };
}),
UpdateGrade: wrapHandler(async (req) => {
await service.updateGrade(
reqStr(req, "id"),
{
score: reqStr(req, "score") || undefined,
feedback: reqStr(req, "feedback") || undefined,
},
reqStr(req, "updated_by"),
);
return { success: true };
}),
// P3.13 新增
GetReportCard: wrapHandler(async (req) => {
const reportCard = await service.getReportCard(
reqStr(req, "student_id"),
reqStr(req, "term_id") || undefined,
);
return {
student_id: reportCard.studentId,
term_id: reportCard.termId,
entries: reportCard.entries.map((e) => ({
subject_id: e.subjectId,
subject_name: e.subjectName,
exam_score: e.examScore,
exam_total: e.examTotal,
homework_score: e.homeworkScore,
homework_total: e.homeworkTotal,
final_score: e.finalScore,
grade_level: e.gradeLevel,
teacher_comment: e.teacherComment,
})),
overall_grade: reportCard.overallGrade,
class_rank: reportCard.classRank,
created_at: reportCard.createdAt,
};
}),
};
}
// ----------------------------------------------------------------------------
// ClassService handlers4 RPC
// ----------------------------------------------------------------------------
function buildClassHandlers(
service: ClassesService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
GetClass: wrapHandler(async (req) => {
const cls = await service.getClass(reqStr(req, "id"));
return toClassProto(cls);
}),
GetClassesByTeacher: wrapHandler(async (req) => {
const list = await service.getClassesByTeacher(reqStr(req, "teacher_id"));
return { classes: list.map(toClassProto) };
}),
BatchGetClasses: wrapHandler(async (req) => {
const list = await service.batchGetClasses(reqStrArr(req, "ids"));
return { classes: list.map(toClassProto) };
}),
ListStudentsByClass: wrapHandler(async (req) => {
const students = await service.listStudentsByClass(
reqStr(req, "class_id"),
);
return {
students: students.map((s) => ({
id: s.id,
name: s.name,
class_id: s.classId,
})),
};
}),
};
}
// ----------------------------------------------------------------------------
// AttendanceService handlers4 RPC
// ----------------------------------------------------------------------------
function buildAttendanceHandlers(
service: AttendanceService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
RecordAttendance: wrapHandler(async (req) => {
const result = await service.recordAttendance({
scheduleId: reqStr(req, "schedule_id"),
studentId: reqStr(req, "student_id"),
status: reqStr(req, "status"),
remark: reqStr(req, "remark") || undefined,
recordedBy: reqStr(req, "recorded_by"),
schoolId: reqStr(req, "school_id"),
});
return { id: result.id };
}),
GetAttendance: wrapHandler(async (req) => {
const att = await service.getAttendance(reqStr(req, "id"));
return toAttendanceProto(att);
}),
ListAttendanceByStudent: wrapHandler(async (req) => {
const list = await service.listByStudent(reqStr(req, "student_id"));
return { attendance: list.map(toAttendanceProto) };
}),
ListAttendanceByClass: wrapHandler(async (req) => {
const list = await service.listByClass(reqStr(req, "class_id"));
return { attendance: list.map(toAttendanceProto) };
}),
};
}
// ----------------------------------------------------------------------------
// ScheduleService handlersP3.13 新增1 RPC
// ----------------------------------------------------------------------------
function buildScheduleHandlers(
service: SchedulingService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
GetScheduleByStudent: wrapHandler(async (req) => {
const slots = await service.getScheduleByStudent(
reqStr(req, "student_id"),
reqStr(req, "week_start") || undefined,
);
return {
slots: slots.map((s) => ({
id: s.id,
course_id: s.courseId,
course_name: s.courseName,
teacher_id: s.teacherId,
class_id: s.classId,
room_id: s.roomId,
start_time: s.startTime,
end_time: s.endTime,
subject_id: s.subjectId,
})),
};
}),
};
}
// ----------------------------------------------------------------------------
// LeaveRequestService handlersP3.13 新增3 RPC
// ----------------------------------------------------------------------------
function buildLeaveRequestHandlers(
service: LeaveRequestsService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
ListLeaveRequestsByStudent: wrapHandler(async (req) => {
const list = await service.listByStudent(
reqStr(req, "student_id"),
reqStr(req, "status") || undefined,
);
return {
leave_requests: list.map((lr) => ({
id: lr.id,
student_id: lr.studentId,
class_id: lr.classId,
leave_type: lr.leaveType,
start_date:
lr.startDate instanceof Date
? lr.startDate.toISOString().slice(0, 10)
: lr.startDate,
end_date:
lr.endDate instanceof Date
? lr.endDate.toISOString().slice(0, 10)
: lr.endDate,
reason: lr.reason,
status: lr.status,
submitted_by: lr.submittedBy,
reviewed_by: str(lr.reviewedBy),
review_comment: str(lr.reviewComment),
school_id: lr.schoolId,
created_at: toIso(lr.createdAt),
updated_at: toIso(lr.updatedAt),
})),
};
}),
CreateLeaveRequest: wrapHandler(async (req) => {
const result = await service.create({
studentId: reqStr(req, "student_id"),
classId: reqStr(req, "class_id"),
leaveType: reqStr(req, "leave_type"),
startDate: reqStr(req, "start_date"),
endDate: reqStr(req, "end_date"),
reason: reqStr(req, "reason"),
submittedBy: reqStr(req, "submitted_by"),
schoolId: reqStr(req, "school_id"),
});
return { id: result.id };
}),
CancelLeaveRequest: wrapHandler(async (req) => {
await service.cancel(reqStr(req, "id"), reqStr(req, "cancelled_by"));
return { success: true };
}),
};
}
// ----------------------------------------------------------------------------
// DashboardService handlersP3.13 新增2 RPC
// ----------------------------------------------------------------------------
function buildDashboardHandlers(
service: DashboardService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
GetDashboard: wrapHandler(async (req) => {
const data = await service.getDashboard(reqStr(req, "teacher_id"));
return {
teacher_id: data.teacherId,
total_classes: data.totalClasses,
total_students: data.totalStudents,
pending_homework: data.pendingHomework,
upcoming_exams: data.upcomingExams,
ungraded_submissions: data.ungradedSubmissions,
classes: data.classes.map((c) => ({
class_id: c.classId,
class_name: c.className,
student_count: c.studentCount,
})),
upcoming_exam_list: data.upcomingExamList.map((e) => ({
exam_id: e.examId,
title: e.title,
exam_date: e.examDate,
class_id: e.classId,
class_name: e.className,
})),
generated_at: data.generatedAt,
};
}),
GetClassPerformance: wrapHandler(async (req) => {
const perf = await service.getClassPerformance(
reqStr(req, "class_id"),
reqStr(req, "subject_id") || undefined,
);
return {
class_id: perf.classId,
class_name: perf.className,
student_count: perf.studentCount,
average_score: perf.averageScore,
highest_score: perf.highestScore,
lowest_score: perf.lowestScore,
median_score: perf.medianScore,
subjects: perf.subjects.map((s) => ({
subject_id: s.subjectId,
subject_name: s.subjectName,
average_score: s.averageScore,
student_count: s.studentCount,
})),
generated_at: perf.generatedAt,
};
}),
};
}
// ----------------------------------------------------------------------------
// AdminService handlersP3.13 新增4 RPC - aggregation stubs
// ----------------------------------------------------------------------------
function buildAdminHandlers(
service: AdminService,
): Record<string, grpc.handleUnaryCall<unknown, unknown>> {
return {
ListSchools: wrapHandler(async () => {
const schools = await service.listSchools();
return {
schools: schools.map((s) => ({
id: s.id,
name: s.name,
address: s.address,
principal_id: s.principalId,
created_at: s.createdAt,
})),
};
}),
ListGradeLevels: wrapHandler(async (req) => {
const levels = await service.listGradeLevels(reqStr(req, "school_id"));
return {
grade_levels: levels.map((l) => ({
id: l.id,
name: l.name,
school_id: l.schoolId,
order: l.order,
})),
};
}),
ListDepartments: wrapHandler(async (req) => {
const depts = await service.listDepartments(reqStr(req, "school_id"));
return {
departments: depts.map((d) => ({
id: d.id,
name: d.name,
school_id: d.schoolId,
head_id: d.headId,
created_at: d.createdAt,
})),
};
}),
ListAcademicYears: wrapHandler(async (req) => {
const years = await service.listAcademicYears(
reqStr(req, "school_id") || undefined,
);
return {
academic_years: years.map((y) => ({
id: y.id,
name: y.name,
school_id: y.schoolId,
start_date: y.startDate,
end_date: y.endDate,
is_current: y.isCurrent,
})),
};
}),
};
}
// ----------------------------------------------------------------------------
// 启动 / 停止
// ----------------------------------------------------------------------------
/**
* 启动 gRPC server监听 env.GRPC_PORT默认 50053
* 通过 NestJS application context 获取各 Service 实例并注册 handler。
*/
export async function startGrpcServer(
app: INestApplicationContext,
): Promise<void> {
const protoPath = resolveProtoPath();
const packageDefinition = protoLoader.loadSync(protoPath, PROTO_OPTIONS);
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const pkg = getCoreEduPackage(protoDescriptor);
// 从 NestJS 容器获取 service 实例
const examsService = app.get(ExamsService);
const homeworkService = app.get(HomeworkService);
const gradesService = app.get(GradesService);
const classesService = app.get(ClassesService);
const attendanceService = app.get(AttendanceService);
const schedulingService = app.get(SchedulingService);
const leaveRequestsService = app.get(LeaveRequestsService);
const dashboardService = app.get(DashboardService);
const adminService = app.get(AdminService);
const server = new grpc.Server();
server.addService(
getServiceDefinition(pkg, "ExamService"),
buildExamHandlers(examsService),
);
server.addService(
getServiceDefinition(pkg, "HomeworkService"),
buildHomeworkHandlers(homeworkService),
);
server.addService(
getServiceDefinition(pkg, "GradeService"),
buildGradeHandlers(gradesService),
);
server.addService(
getServiceDefinition(pkg, "ClassService"),
buildClassHandlers(classesService),
);
server.addService(
getServiceDefinition(pkg, "AttendanceService"),
buildAttendanceHandlers(attendanceService),
);
// P3.13 新增 4 个服务
server.addService(
getServiceDefinition(pkg, "ScheduleService"),
buildScheduleHandlers(schedulingService),
);
server.addService(
getServiceDefinition(pkg, "LeaveRequestService"),
buildLeaveRequestHandlers(leaveRequestsService),
);
server.addService(
getServiceDefinition(pkg, "DashboardService"),
buildDashboardHandlers(dashboardService),
);
server.addService(
getServiceDefinition(pkg, "AdminService"),
buildAdminHandlers(adminService),
);
const address = `0.0.0.0:${env.GRPC_PORT}`;
await new Promise<void>((resolve, reject) => {
server.bindAsync(
address,
grpc.ServerCredentials.createInsecure(),
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
},
);
});
grpcServer = server;
logger.info(
{ port: env.GRPC_PORT, protoPath, service: "core-edu" },
"gRPC server is listening",
);
}
/**
* 优雅停止 gRPC server。
*/
export async function stopGrpcServer(): Promise<void> {
const server = grpcServer;
if (!server) {
return;
}
await new Promise<void>((resolve, reject) => {
server.tryShutdown((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
grpcServer = null;
logger.info({ service: "core-edu" }, "gRPC server stopped");
}