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. 相对于源文件位置(dev:src/grpc;prod:dist/grpc,目录层级一致) * 2. 相对于 cwd 的 packages/shared-proto/proto(cwd = 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; const segment = root["next_edu_cloud"]; const coreEdu = (segment as Record | undefined)?.[ "core_edu" ]; const v1 = (coreEdu as Record | 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 { 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; }; 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 转为 grpc handleUnaryCall // ---------------------------------------------------------------------------- type ProtoRequest = Record; type ProtoResponse = Record; function wrapHandler( handler: (req: ProtoRequest) => Promise, ): grpc.handleUnaryCall { return (call, callback) => { // call.request 类型为 unknown(addService 的实现签名),此处从 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 string,null -> "" // ---------------------------------------------------------------------------- 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 handlers(8 RPC) // ---------------------------------------------------------------------------- function buildExamHandlers( service: ExamsService, ): Record> { 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 handlers(5 RPC) // ---------------------------------------------------------------------------- function buildHomeworkHandlers( service: HomeworkService, ): Record> { 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 handlers(6 RPC) // ---------------------------------------------------------------------------- function buildGradeHandlers( service: GradesService, ): Record> { 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 handlers(4 RPC) // ---------------------------------------------------------------------------- function buildClassHandlers( service: ClassesService, ): Record> { 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 handlers(4 RPC) // ---------------------------------------------------------------------------- function buildAttendanceHandlers( service: AttendanceService, ): Record> { 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 handlers(P3.13 新增,1 RPC) // ---------------------------------------------------------------------------- function buildScheduleHandlers( service: SchedulingService, ): Record> { 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 handlers(P3.13 新增,3 RPC) // ---------------------------------------------------------------------------- function buildLeaveRequestHandlers( service: LeaveRequestsService, ): Record> { 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 handlers(P3.13 新增,2 RPC) // ---------------------------------------------------------------------------- function buildDashboardHandlers( service: DashboardService, ): Record> { 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 handlers(P3.13 新增,4 RPC - aggregation stubs) // ---------------------------------------------------------------------------- function buildAdminHandlers( service: AdminService, ): Record> { 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 { 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((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 { const server = grpcServer; if (!server) { return; } await new Promise((resolve, reject) => { server.tryShutdown((err) => { if (err) { reject(err); } else { resolve(); } }); }); grpcServer = null; logger.info({ service: "core-edu" }, "gRPC server stopped"); }