feat: initialize parent-bff service with full core features

add complete parent-bff implementation including:
- GraphQL endpoint with depth/cost validation
- ChildGuard越权校验 with redis cache and singleflight
- parallel orchestration with partial failure fallback
- three-level cache fallback strategy (Redis + LRU + downstream)
- Kafka consumer for cache invalidation and notification push
- opossum circuit breaker for downstream services
- Prometheus metrics and SLO alerts
- Helm chart for k8s deployment with multi-environment configs
- Grafana dashboard for observability
- complete unit and integration tests
This commit is contained in:
SpecialX
2026-07-10 18:49:06 +08:00
parent a7d8f92227
commit 2229309a1e
88 changed files with 11221 additions and 0 deletions

View File

@@ -0,0 +1,402 @@
import { describe, it, expect } from "vitest";
import {
mapParent,
mapChild,
mapClassInfo,
mapGrade,
mapHomework,
mapExam,
mapViewport,
mapWeakness,
mapTrend,
mapAnalytics,
} from "../../src/aggregation/response-mapper.js";
import type {
UserInfoDto,
ChildDto,
GradeDto,
HomeworkDto,
ExamDto,
ViewportDto,
StudentWeaknessDto,
LearningTrendDto,
ClassPerformanceDto,
} from "../../src/clients/dtos.js";
describe("Response Mapper", () => {
describe("mapParent", () => {
it("正确映射 UserInfoDto → ParentType", () => {
const dto: UserInfoDto = {
id: "parent-001",
email: "parent@example.com",
name: "王家长",
roles: ["parent"],
permissions: [],
};
const result = mapParent(dto);
expect(result.id).toBe("parent-001");
expect(result.email).toBe("parent@example.com");
expect(result.name).toBe("王家长");
expect(result.avatar).toBeNull();
expect(result.roles).toEqual(["parent"]);
expect(result.dataScope).toBe("CHILDREN");
});
});
describe("mapChild", () => {
it("正确映射 ChildDto → ChildType", () => {
const dto: ChildDto = {
id: "student-001",
name: "李同学",
grade: "三年级",
classId: "class-001",
className: "三年级1班",
gradeId: "grade-003",
};
const result = mapChild(dto);
expect(result.id).toBe("student-001");
expect(result.name).toBe("李同学");
expect(result.grade).toBe("三年级");
expect(result.class.id).toBe("class-001");
expect(result.class.name).toBe("三年级1班");
expect(result.class.gradeId).toBe("grade-003");
});
});
describe("mapClassInfo", () => {
it("正确映射 ClassInfo", () => {
const result = mapClassInfo({
id: "class-001",
name: "三年级1班",
gradeId: "grade-003",
});
expect(result.id).toBe("class-001");
expect(result.name).toBe("三年级1班");
expect(result.gradeId).toBe("grade-003");
});
});
describe("mapGrade", () => {
it("正确映射 GradeDto → GradeTypescore string → number", () => {
const dto: GradeDto = {
id: "grade-001",
studentId: "student-001",
examId: "exam-001",
homeworkId: "",
score: "85.5",
feedback: "数学表现良好",
gradedBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
};
const result = mapGrade(dto);
expect(result.id).toBe("grade-001");
expect(result.examId).toBe("exam-001");
expect(result.score).toBe(85.5);
expect(result.subject).toBe("数学");
expect(result.examTitle).toBe("数学表现良好");
expect(result.rank).toBeNull();
expect(result.gradedAt).toBe("2026-01-02T00:00:00.000Z");
});
it("feedback 为空时 examTitle 为默认值", () => {
const dto: GradeDto = {
id: "grade-001",
studentId: "student-001",
examId: "exam-001",
homeworkId: "",
score: "90",
feedback: "",
gradedBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
};
const result = mapGrade(dto);
expect(result.examTitle).toBe("未命名考试");
expect(result.subject).toBe("未知科目");
});
it("score 非数字时返回 0", () => {
const dto: GradeDto = {
id: "grade-001",
studentId: "student-001",
examId: "exam-001",
homeworkId: "",
score: "invalid",
feedback: "语文表现良好",
gradedBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
};
const result = mapGrade(dto);
expect(result.score).toBe(0);
expect(result.subject).toBe("语文");
});
it("feedback 无科目前缀时 subject 为综合", () => {
const dto: GradeDto = {
id: "grade-001",
studentId: "student-001",
examId: "exam-001",
homeworkId: "",
score: "80",
feedback: "表现不错",
gradedBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
};
const result = mapGrade(dto);
expect(result.subject).toBe("综合");
});
});
describe("mapHomework", () => {
it("SUBMITTED 状态返回 submittedAt", () => {
const dto: HomeworkDto = {
id: "hw-001",
classId: "class-001",
title: "数学练习",
description: "",
dueDate: "2026-01-05T00:00:00.000Z",
status: "SUBMITTED",
createdBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
};
const result = mapHomework(dto);
expect(result.status).toBe("SUBMITTED");
expect(result.submittedAt).toBe("2026-01-02T00:00:00.000Z");
});
it("GRADED 状态返回 submittedAt", () => {
const dto: HomeworkDto = {
id: "hw-001",
classId: "class-001",
title: "数学练习",
description: "",
dueDate: "2026-01-05T00:00:00.000Z",
status: "GRADED",
createdBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-03T00:00:00.000Z",
};
const result = mapHomework(dto);
expect(result.submittedAt).toBe("2026-01-03T00:00:00.000Z");
});
it("NOT_SUBMITTED 状态 submittedAt 为 null", () => {
const dto: HomeworkDto = {
id: "hw-001",
classId: "class-001",
title: "数学练习",
description: "",
dueDate: "2026-01-05T00:00:00.000Z",
status: "NOT_SUBMITTED",
createdBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
};
const result = mapHomework(dto);
expect(result.submittedAt).toBeNull();
});
});
describe("mapExam", () => {
it("PUBLISHED 状态返回 publishedAt", () => {
const dto: ExamDto = {
id: "exam-001",
classId: "class-001",
title: "数学期中考试",
description: "",
examDate: "2026-02-01T00:00:00.000Z",
duration: "90",
totalScore: "100",
status: "PUBLISHED",
createdBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-15T00:00:00.000Z",
};
const result = mapExam(dto);
expect(result.status).toBe("PUBLISHED");
expect(result.publishedAt).toBe("2026-01-15T00:00:00.000Z");
});
it("DRAFT 状态 publishedAt 为 null", () => {
const dto: ExamDto = {
id: "exam-001",
classId: "class-001",
title: "数学期中考试",
description: "",
examDate: "2026-02-01T00:00:00.000Z",
duration: "90",
totalScore: "100",
status: "DRAFT",
createdBy: "teacher-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-15T00:00:00.000Z",
};
const result = mapExam(dto);
expect(result.publishedAt).toBeNull();
});
});
describe("mapViewport", () => {
it("正确映射 ViewportDto", () => {
const dto: ViewportDto = {
key: "dashboard",
label: "首页",
route: "/parent/dashboard",
icon: "home",
sortOrder: "1",
requiredPermission: "parent:dashboard:view",
};
const result = mapViewport(dto);
expect(result.key).toBe("dashboard");
expect(result.label).toBe("首页");
expect(result.icon).toBe("home");
expect(result.requiredPermission).toBe("parent:dashboard:view");
});
it("icon 和 requiredPermission 可选", () => {
const dto: ViewportDto = {
key: "settings",
label: "设置",
route: "/parent/settings",
sortOrder: "5",
};
const result = mapViewport(dto);
expect(result.icon).toBeNull();
expect(result.requiredPermission).toBeNull();
});
});
describe("mapWeakness", () => {
it("正确映射薄弱点列表", () => {
const dto: StudentWeaknessDto = {
studentId: "student-001",
weakPoints: [
{ knowledgePointId: "kp-1", title: "分数加减法", mastery: 0.45 },
{ knowledgePointId: "kp-2", title: "阅读理解", mastery: 0.62 },
],
};
const result = mapWeakness(dto);
expect(result).toHaveLength(2);
expect(result[0]!.knowledgePointId).toBe("kp-1");
expect(result[0]!.name).toBe("分数加减法");
expect(result[0]!.masteryRate).toBe(0.45);
expect(result[0]!.subject).toBe("综合");
});
it("空薄弱点列表", () => {
const dto: StudentWeaknessDto = {
studentId: "student-001",
weakPoints: [],
};
const result = mapWeakness(dto);
expect(result).toHaveLength(0);
});
});
describe("mapTrend", () => {
it("正确映射趋势点int64 ms → ISO string", () => {
const dto: LearningTrendDto = {
studentId: "student-001",
points: [
{ date: 1735689600000, score: 80 },
{ date: 1735776000000, score: 85 },
],
};
const result = mapTrend(dto);
expect(result).toHaveLength(2);
expect(result[0]!.score).toBe(80);
expect(typeof result[0]!.date).toBe("string");
expect(new Date(result[0]!.date).getTime()).toBe(1735689600000);
expect(result[0]!.subject).toBeNull();
});
});
describe("mapAnalytics", () => {
it("正确计算 classRank降序排名", () => {
const weakness: StudentWeaknessDto = {
studentId: "student-001",
weakPoints: [],
};
const trend: LearningTrendDto = {
studentId: "student-001",
points: [],
};
const classPerf: ClassPerformanceDto = {
classId: "class-001",
averageScore: 82.5,
passRate: 0.9,
scores: [
{ studentId: "student-001", score: 85, grade: "A" },
{ studentId: "student-002", score: 92, grade: "A" },
{ studentId: "student-003", score: 65, grade: "C" },
],
};
const result = mapAnalytics("student-001", weakness, trend, classPerf);
expect(result.childId).toBe("student-001");
expect(result.classAverage).toBe(82.5);
const sorted = [...classPerf.scores].sort((a, b) => b.score - a.score);
const idx = sorted.findIndex((s) => s.studentId === "student-001");
expect(result.classRank).toBe(idx + 1);
});
it("classPerf 为 null 时 classRank 和 classAverage 为 null", () => {
const weakness: StudentWeaknessDto = {
studentId: "student-001",
weakPoints: [],
};
const trend: LearningTrendDto = {
studentId: "student-001",
points: [],
};
const result = mapAnalytics("student-001", weakness, trend, null);
expect(result.classRank).toBeNull();
expect(result.classAverage).toBeNull();
});
it("childId 不在 scores 中时 classRank 为 null", () => {
const weakness: StudentWeaknessDto = {
studentId: "student-001",
weakPoints: [],
};
const trend: LearningTrendDto = {
studentId: "student-001",
points: [],
};
const classPerf: ClassPerformanceDto = {
classId: "class-001",
averageScore: 80,
passRate: 0.9,
scores: [
{ studentId: "student-002", score: 90, grade: "A" },
{ studentId: "student-003", score: 70, grade: "B" },
],
};
const result = mapAnalytics("student-001", weakness, trend, classPerf);
expect(result.classRank).toBeNull();
expect(result.classAverage).toBe(80);
});
it("scores 为空数组时 classRank 为 null", () => {
const weakness: StudentWeaknessDto = {
studentId: "student-001",
weakPoints: [],
};
const trend: LearningTrendDto = {
studentId: "student-001",
points: [],
};
const classPerf: ClassPerformanceDto = {
classId: "class-001",
averageScore: 0,
passRate: 0,
scores: [],
};
const result = mapAnalytics("student-001", weakness, trend, classPerf);
expect(result.classRank).toBeNull();
});
});
});