feat(core-edu): admin/dashboard/leave-requests 模块 + gRPC + 状态机测试 + nextstep 文档

This commit is contained in:
SpecialX
2026-07-14 15:56:10 +08:00
parent d260df864c
commit 7dd5c44406
23 changed files with 3344 additions and 2 deletions

View File

@@ -0,0 +1,190 @@
// gRPC smoke test for core-edu service (P3.13)
// 运行: node services/core-edu/test/grpc-smoke.mjs
// 前提: edu-core-edu-test 容器已启动gRPC 端口 50053 可达
import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROTO_PATH = path.resolve(__dirname, "../../../packages/shared-proto/proto/core_edu.proto");
const packageDef = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDef);
const TARGET = "localhost:50053";
const clientMap = {};
const services = [
"ExamService",
"HomeworkService",
"GradeService",
"ClassService",
"AttendanceService",
"ScheduleService",
"LeaveRequestService",
"DashboardService",
"AdminService",
];
for (const s of services) {
const Ctor = proto.next_edu_cloud.core_edu.v1[s];
if (!Ctor) {
console.error(`MISSING service: ${s}`);
continue;
}
clientMap[s] = new Ctor(TARGET, grpc.credentials.createInsecure());
}
const results = { pass: [], fail: [] };
function record(name, ok, detail) {
const entry = { rpc: name, ok, detail: detail ? String(detail).slice(0, 300) : undefined };
if (ok) results.pass.push(entry);
else results.fail.push(entry);
const tag = ok ? "PASS" : "FAIL";
console.log(`[${tag}] ${name}${detail ? " :: " + entry.detail : ""}`);
}
function call(client, method, req) {
return new Promise((resolve) => {
if (!client) {
resolve({ error: new Error("client missing") });
return;
}
if (typeof client[method] !== "function") {
resolve({ error: new Error(`method ${method} not found`) });
return;
}
client[method](req, (error, response) => {
resolve({ error, response });
});
});
}
async function main() {
console.log(`\n=== core-edu gRPC smoke test @ ${TARGET} ===\n`);
// ---- ExamService ----
let r;
r = await call(clientMap.ExamService, "ListExamsByClass", { class_id: "cls-nonexistent" });
record("ExamService.ListExamsByClass", !r.error, r.error?.message || `exams=${r.response?.exams?.length ?? 0}`);
r = await call(clientMap.ExamService, "GetExam", { id: "exam-nonexistent" });
// NotFound 是正常的业务错误(说明 RPC 可达且执行了)
record("ExamService.GetExam", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || "ok");
r = await call(clientMap.ExamService, "SaveExamDraft", { exam_id: "1b7f27aa-f45d-495d-b74f-cecbd0230e2e", student_id: "stu-1", answers: [] });
record("ExamService.SaveExamDraft", !r.error, r.error?.message || `draftId=${r.response?.draftId}`);
r = await call(clientMap.ExamService, "RecordExamViolation", { exam_id: "1b7f27aa-f45d-495d-b74f-cecbd0230e2e", student_id: "stu-1", violation_type: "tab_switch", detail: "test", severity: 1 });
record("ExamService.RecordExamViolation", !r.error, r.error?.message || `violationId=${r.response?.violationId}`);
// ---- HomeworkService ----
r = await call(clientMap.HomeworkService, "ListHomeworkByClass", { class_id: "cls-nonexistent" });
record("HomeworkService.ListHomeworkByClass", !r.error, r.error?.message || `homework=${r.response?.homework?.length ?? 0}`);
r = await call(clientMap.HomeworkService, "GetHomework", { id: "hw-nonexistent" });
record("HomeworkService.GetHomework", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || "ok");
// ---- GradeService ----
r = await call(clientMap.GradeService, "ListGradesByStudent", { student_id: "stu-nonexistent" });
record("GradeService.ListGradesByStudent", !r.error, r.error?.message || `grades=${r.response?.grades?.length ?? 0}`);
r = await call(clientMap.GradeService, "GetReportCard", { student_id: "stu-nonexistent" });
record("GradeService.GetReportCard", !r.error, r.error?.message || `overallGrade=${r.response?.overallGrade}`);
// ---- ClassService ----
r = await call(clientMap.ClassService, "GetClass", { id: "cls-nonexistent" });
record("ClassService.GetClass", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || "ok");
r = await call(clientMap.ClassService, "GetClassesByTeacher", { teacher_id: "t-nonexistent" });
record("ClassService.GetClassesByTeacher", !r.error, r.error?.message || `classes=${r.response?.classes?.length ?? 0}`);
r = await call(clientMap.ClassService, "BatchGetClasses", { ids: [] });
record("ClassService.BatchGetClasses", !r.error, r.error?.message || `classes=${r.response?.classes?.length ?? 0}`);
r = await call(clientMap.ClassService, "ListStudentsByClass", { class_id: "cls-nonexistent" });
record("ClassService.ListStudentsByClass", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || `students=${r.response?.students?.length ?? 0}`);
// ---- AttendanceService ----
r = await call(clientMap.AttendanceService, "ListAttendanceByStudent", { student_id: "stu-nonexistent" });
record("AttendanceService.ListAttendanceByStudent", !r.error, r.error?.message || `attendance=${r.response?.attendance?.length ?? 0}`);
r = await call(clientMap.AttendanceService, "ListAttendanceByClass", { class_id: "cls-nonexistent" });
record("AttendanceService.ListAttendanceByClass", !r.error, r.error?.message || `attendance=${r.response?.attendance?.length ?? 0}`);
r = await call(clientMap.AttendanceService, "GetAttendance", { id: "att-nonexistent" });
record("AttendanceService.GetAttendance", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || "ok");
// ---- ScheduleService ----
r = await call(clientMap.ScheduleService, "GetScheduleByStudent", { student_id: "stu-nonexistent" });
record("ScheduleService.GetScheduleByStudent", !r.error, r.error?.message || `slots=${r.response?.slots?.length ?? 0}`);
// ---- LeaveRequestService ----
r = await call(clientMap.LeaveRequestService, "ListLeaveRequestsByStudent", { student_id: "stu-nonexistent" });
record("LeaveRequestService.ListLeaveRequestsByStudent", !r.error, r.error?.message || `requests=${r.response?.leaveRequests?.length ?? 0}`);
r = await call(clientMap.LeaveRequestService, "CreateLeaveRequest", {
student_id: "stu-smoke",
class_id: "cls-smoke",
leave_type: "personal",
start_date: "2026-07-14",
end_date: "2026-07-14",
reason: "smoke test",
submitted_by: "dev-teacher",
school_id: "sch-smoke",
});
record("LeaveRequestService.CreateLeaveRequest", !r.error, r.error?.message || `id=${r.response?.id}`);
// ---- DashboardService ----
r = await call(clientMap.DashboardService, "GetDashboard", { teacher_id: "t-nonexistent" });
record("DashboardService.GetDashboard", !r.error, r.error?.message || `totalClasses=${r.response?.totalClasses}`);
r = await call(clientMap.DashboardService, "GetClassPerformance", { class_id: "cls-nonexistent" });
record("DashboardService.GetClassPerformance", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || `avg=${r.response?.averageScore}`);
// ---- AdminService ----
r = await call(clientMap.AdminService, "ListSchools", {});
record("AdminService.ListSchools", !r.error, r.error?.message || `schools=${r.response?.schools?.length ?? 0}`);
r = await call(clientMap.AdminService, "ListGradeLevels", { school_id: "sch-nonexistent" });
record("AdminService.ListGradeLevels", !r.error, r.error?.message || `gradeLevels=${r.response?.gradeLevels?.length ?? 0}`);
r = await call(clientMap.AdminService, "ListDepartments", { school_id: "sch-nonexistent" });
record("AdminService.ListDepartments", !r.error, r.error?.message || `departments=${r.response?.departments?.length ?? 0}`);
r = await call(clientMap.AdminService, "ListAcademicYears", {});
record("AdminService.ListAcademicYears", !r.error, r.error?.message || `academicYears=${r.response?.academicYears?.length ?? 0}`);
// ---- 汇总 ----
console.log(`\n=== 汇总 ===`);
console.log(`PASS: ${results.pass.length} FAIL: ${results.fail.length} TOTAL: ${results.pass.length + results.fail.length}`);
if (results.fail.length > 0) {
console.log("\n失败项:");
for (const f of results.fail) {
console.log(` - ${f.rpc}: ${f.detail}`);
}
process.exit(1);
} else {
console.log("全部 RPC 可达 ✓");
}
for (const s of services) {
clientMap[s]?.close?.();
}
grpc.getClientChannelMap?.();
process.exit(0);
}
main().catch((e) => {
console.error("smoke test crashed:", e);
process.exit(2);
});