feat(student-bff): extended queries/mutations resolvers + Dockerfile + nextstep 文档
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,792 @@
|
||||
/**
|
||||
* Extended Queries Resolver - student-bff 扩展查询 (P3-P5).
|
||||
*
|
||||
* 仲裁依据:
|
||||
* - student-bff.schema.graphql 扩展 Query (examDetail/homeworkDetail/serverTime 等)
|
||||
* - coord-final-decisions §2 B2 (gRPC 调用下游)
|
||||
* - coord-final-decisions §2 B4 (强制自我越权防御)
|
||||
* - coord-final-decisions §2 B6 (Redis 短缓存)
|
||||
* - president-final-rulings §2.6 (降级模式方案 B)
|
||||
*
|
||||
* 实现: 22 个新 Query, 涵盖 P3 扩展/P4 学习/P5 校园生活.
|
||||
* 响应严格对齐 schema 类型 (Connection/Payload), 降级时返回空数据.
|
||||
*/
|
||||
import type { StudentBffContext } from "../../shared/graphql/yoga.js";
|
||||
import { UnauthorizedError } from "../../shared/errors/application-error.js";
|
||||
|
||||
/**
|
||||
* 空分页响应 (降级模式).
|
||||
*/
|
||||
function emptyConnection<T>(): {
|
||||
edges: T[];
|
||||
pageInfo: {
|
||||
hasNextPage: false;
|
||||
hasPreviousPage: false;
|
||||
startCursor: null;
|
||||
endCursor: null;
|
||||
};
|
||||
totalCount: 0;
|
||||
} {
|
||||
return {
|
||||
edges: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
},
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 降级 Payload (无核心数据).
|
||||
*/
|
||||
function degradedPayload<T extends object>(
|
||||
emptyData: T,
|
||||
reason: string,
|
||||
degradedFields: string[],
|
||||
): T & {
|
||||
degraded: true;
|
||||
degradedReason: string;
|
||||
degradedFields: string[];
|
||||
} {
|
||||
return {
|
||||
...emptyData,
|
||||
degraded: true,
|
||||
degradedReason: reason,
|
||||
degradedFields,
|
||||
};
|
||||
}
|
||||
|
||||
export const extendedQueriesResolvers = {
|
||||
Query: {
|
||||
/**
|
||||
* myAttendance: 我的考勤记录 (core-edu AttendanceService.ListAttendanceByStudent).
|
||||
* B4 强制 userId = studentId (DataScope SELF).
|
||||
*/
|
||||
async myAttendance(
|
||||
_parent: unknown,
|
||||
args: {
|
||||
after?: string;
|
||||
first?: number;
|
||||
before?: string;
|
||||
last?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
},
|
||||
ctx: StudentBffContext,
|
||||
): Promise<{
|
||||
edges: unknown[];
|
||||
pageInfo: {
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
startCursor: string | null;
|
||||
endCursor: string | null;
|
||||
};
|
||||
totalCount: number;
|
||||
}> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"core-edu",
|
||||
"ListAttendanceByStudent",
|
||||
{
|
||||
studentId: ctx.userId,
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
first: args.first ?? 20,
|
||||
after: args.after,
|
||||
},
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as {
|
||||
edges?: Array<{ node?: unknown; cursor?: string }>;
|
||||
pageInfo?: {
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
startCursor?: string | null;
|
||||
endCursor?: string | null;
|
||||
};
|
||||
totalCount?: number;
|
||||
};
|
||||
return {
|
||||
edges: result?.edges ?? [],
|
||||
pageInfo: {
|
||||
hasNextPage: result?.pageInfo?.hasNextPage ?? false,
|
||||
hasPreviousPage: result?.pageInfo?.hasPreviousPage ?? false,
|
||||
startCursor: result?.pageInfo?.startCursor ?? null,
|
||||
endCursor: result?.pageInfo?.endCursor ?? null,
|
||||
},
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
};
|
||||
} catch {
|
||||
return emptyConnection<unknown>();
|
||||
}
|
||||
},
|
||||
|
||||
/** examDetail: 考试详情 (含题目). */
|
||||
async examDetail(
|
||||
_parent: unknown,
|
||||
args: { examId: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"core-edu",
|
||||
"GetExam",
|
||||
{ examId: args.examId, studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as {
|
||||
exam?: unknown;
|
||||
questions?: unknown[];
|
||||
mySubmission?: unknown;
|
||||
};
|
||||
return {
|
||||
exam: result?.exam ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ exam: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["exam"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** homeworkDetail: 作业详情. */
|
||||
async homeworkDetail(
|
||||
_parent: unknown,
|
||||
args: { homeworkId: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"core-edu",
|
||||
"GetHomework",
|
||||
{ homeworkId: args.homeworkId, studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { homework?: unknown };
|
||||
return {
|
||||
homework: result?.homework ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ homework: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["homework"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** serverTime: 服务器时间 (BFF 本地, 无下游). */
|
||||
async serverTime(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
_ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
const now = new Date();
|
||||
return {
|
||||
serverTime: now.toISOString(),
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
||||
timestamp: now.getTime(),
|
||||
};
|
||||
},
|
||||
|
||||
/** mySchedule: 我的课表 (周视图). */
|
||||
async mySchedule(
|
||||
_parent: unknown,
|
||||
args: { weekStart?: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"core-edu",
|
||||
"GetScheduleByStudent",
|
||||
{ studentId: ctx.userId, weekStart: args.weekStart },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { items?: unknown[]; weekStart?: string };
|
||||
return {
|
||||
items: result?.items ?? [],
|
||||
weekStart: result?.weekStart ?? args.weekStart ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ items: [], weekStart: args.weekStart ?? null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["items"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** studentGrowth: 学生成长曲线. */
|
||||
async studentGrowth(
|
||||
_parent: unknown,
|
||||
args: { subjectId?: string; startDate?: string; endDate?: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"data-ana",
|
||||
"GetStudentGrowth",
|
||||
{
|
||||
studentId: ctx.userId,
|
||||
subjectId: args.subjectId,
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
},
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as {
|
||||
points?: unknown[];
|
||||
averageScore?: number;
|
||||
classAverage?: number;
|
||||
growthRate?: number;
|
||||
};
|
||||
return {
|
||||
studentId: ctx.userId,
|
||||
subjectId: args.subjectId ?? null,
|
||||
points: result?.points ?? [],
|
||||
averageScore: result?.averageScore ?? null,
|
||||
classAverage: result?.classAverage ?? null,
|
||||
growthRate: result?.growthRate ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{
|
||||
studentId: ctx.userId,
|
||||
subjectId: args.subjectId ?? null,
|
||||
points: [],
|
||||
averageScore: null,
|
||||
classAverage: null,
|
||||
growthRate: null,
|
||||
},
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["points", "averageScore", "classAverage", "growthRate"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** assignmentAnalysis: 作业分析. */
|
||||
async assignmentAnalysis(
|
||||
_parent: unknown,
|
||||
args: { homeworkId?: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"data-ana",
|
||||
"GetAssignmentAnalysis",
|
||||
{
|
||||
studentId: ctx.userId,
|
||||
homeworkId: args.homeworkId,
|
||||
},
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { analysis?: unknown };
|
||||
return {
|
||||
analysis: result?.analysis ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ analysis: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["analysis"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myProfile: 个人资料. */
|
||||
async myProfile(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"iam",
|
||||
"GetUserProfile",
|
||||
{ userId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { profile?: unknown };
|
||||
return {
|
||||
profile: result?.profile ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ profile: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["profile"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myMasterySummary: 掌握度概览. */
|
||||
async myMasterySummary(
|
||||
_parent: unknown,
|
||||
args: { subjectId?: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"data-ana",
|
||||
"GetMasterySummary",
|
||||
{ studentId: ctx.userId, subjectId: args.subjectId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { summary?: unknown };
|
||||
return {
|
||||
summary: result?.summary ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ summary: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["summary"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myDiagnosticReports: 诊断报告列表. */
|
||||
async myDiagnosticReports(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"data-ana",
|
||||
"ListDiagnosticReports",
|
||||
{ studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { reports?: unknown[]; totalCount?: number };
|
||||
return {
|
||||
reports: result?.reports ?? [],
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ reports: [], totalCount: 0 },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["reports"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myErrorBook: 错题本. */
|
||||
async myErrorBook(
|
||||
_parent: unknown,
|
||||
args: {
|
||||
subjectId?: string;
|
||||
mastered?: boolean;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
},
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"data-ana",
|
||||
"ListErrorBookItems",
|
||||
{
|
||||
studentId: ctx.userId,
|
||||
subjectId: args.subjectId,
|
||||
mastered: args.mastered,
|
||||
page: args.page ?? 1,
|
||||
pageSize: args.pageSize ?? 20,
|
||||
},
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as {
|
||||
items?: unknown[];
|
||||
totalCount?: number;
|
||||
subjectStats?: unknown[];
|
||||
};
|
||||
return {
|
||||
items: result?.items ?? [],
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
subjectStats: result?.subjectStats ?? [],
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ items: [], totalCount: 0, subjectStats: [] },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["items", "subjectStats"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** announcements: 公告列表. */
|
||||
async announcements(
|
||||
_parent: unknown,
|
||||
args: { first?: number; category?: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"msg",
|
||||
"ListAnnouncements",
|
||||
{
|
||||
userId: ctx.userId,
|
||||
first: args.first ?? 20,
|
||||
category: args.category,
|
||||
},
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { edges?: unknown[]; totalCount?: number };
|
||||
return {
|
||||
edges: result?.edges ?? [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
},
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
};
|
||||
} catch {
|
||||
return emptyConnection();
|
||||
}
|
||||
},
|
||||
|
||||
/** announcementDetail: 公告详情. */
|
||||
async announcementDetail(
|
||||
_parent: unknown,
|
||||
args: { announcementId: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"msg",
|
||||
"GetAnnouncement",
|
||||
{
|
||||
announcementId: args.announcementId,
|
||||
userId: ctx.userId,
|
||||
},
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { announcement?: unknown };
|
||||
return {
|
||||
announcement: result?.announcement ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ announcement: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["announcement"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myLeaveRequests: 我的请假记录. */
|
||||
async myLeaveRequests(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"core-edu",
|
||||
"ListLeaveRequestsByStudent",
|
||||
{ studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as {
|
||||
requests?: unknown[];
|
||||
totalCount?: number;
|
||||
pendingCount?: number;
|
||||
};
|
||||
return {
|
||||
requests: result?.requests ?? [],
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
pendingCount: result?.pendingCount ?? 0,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ requests: [], totalCount: 0, pendingCount: 0 },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["requests"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myElectiveSelections: 我的选课. */
|
||||
async myElectiveSelections(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"content",
|
||||
"ListElectiveSelectionsByStudent",
|
||||
{ studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { selections?: unknown[]; totalCount?: number };
|
||||
return {
|
||||
selections: result?.selections ?? [],
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ selections: [], totalCount: 0 },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["selections"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** availableElectiveCourses: 可选课程. */
|
||||
async availableElectiveCourses(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"content",
|
||||
"ListAvailableElectiveCourses",
|
||||
{ studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { courses?: unknown[]; totalCount?: number };
|
||||
return {
|
||||
courses: result?.courses ?? [],
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ courses: [], totalCount: 0 },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["courses"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myLessonPlans: 我的课案. */
|
||||
async myLessonPlans(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"content",
|
||||
"ListLessonPlansByStudent",
|
||||
{ studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { plans?: unknown[]; totalCount?: number };
|
||||
return {
|
||||
plans: result?.plans ?? [],
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ plans: [], totalCount: 0 },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["plans"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** lessonPlanDetail: 课案详情. */
|
||||
async lessonPlanDetail(
|
||||
_parent: unknown,
|
||||
args: { lessonPlanId: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"content",
|
||||
"GetLessonPlan",
|
||||
{ lessonPlanId: args.lessonPlanId, studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { plan?: unknown };
|
||||
return {
|
||||
plan: result?.plan ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ plan: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["plan"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myCoursePlans: 我的课程计划. */
|
||||
async myCoursePlans(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"content",
|
||||
"ListCoursePlansByStudent",
|
||||
{ studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { plans?: unknown[]; totalCount?: number };
|
||||
return {
|
||||
plans: result?.plans ?? [],
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ plans: [], totalCount: 0 },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["plans"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** coursePlanDetail: 课程计划详情. */
|
||||
async coursePlanDetail(
|
||||
_parent: unknown,
|
||||
args: { coursePlanId: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"content",
|
||||
"GetCoursePlan",
|
||||
{ coursePlanId: args.coursePlanId, studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { plan?: unknown };
|
||||
return {
|
||||
plan: result?.plan ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ plan: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["plan"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myReportCard: 成绩报告卡. */
|
||||
async myReportCard(
|
||||
_parent: unknown,
|
||||
args: { examId?: string; term?: string; academicYear?: string },
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"core-edu",
|
||||
"GetReportCard",
|
||||
{
|
||||
studentId: ctx.userId,
|
||||
examId: args.examId,
|
||||
term: args.term,
|
||||
academicYear: args.academicYear,
|
||||
},
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { reportCard?: unknown };
|
||||
return {
|
||||
reportCard: result?.reportCard ?? null,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ reportCard: null },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["reportCard"],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/** myPracticeSessions: 我的练习会话. */
|
||||
async myPracticeSessions(
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: StudentBffContext,
|
||||
): Promise<unknown> {
|
||||
if (!ctx.userId) throw new UnauthorizedError();
|
||||
try {
|
||||
const result = (await ctx.downstream.call(
|
||||
"data-ana",
|
||||
"ListPracticeSessionsByStudent",
|
||||
{ studentId: ctx.userId },
|
||||
{ traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } },
|
||||
)) as { sessions?: unknown[]; totalCount?: number };
|
||||
return {
|
||||
sessions: result?.sessions ?? [],
|
||||
totalCount: result?.totalCount ?? 0,
|
||||
degraded: false,
|
||||
degradedReason: null,
|
||||
degradedFields: [],
|
||||
};
|
||||
} catch (err) {
|
||||
return degradedPayload(
|
||||
{ sessions: [], totalCount: 0 },
|
||||
`DOWNSTREAM_FAILURE: ${(err as Error).message}`,
|
||||
["sessions"],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,8 +4,8 @@
|
||||
* 将所有 Resolver 合并为一个 GraphQL Resolver 映射表,
|
||||
* 供 GraphQL Yoga makeExecutableSchema 使用.
|
||||
*
|
||||
* Resolver 清单 (按 schema 第一版):
|
||||
* Query:
|
||||
* Resolver 清单 (v2 完整版):
|
||||
* Query (35):
|
||||
* - currentUser (auth)
|
||||
* - studentDashboard (dashboard)
|
||||
* - myHomework (homework)
|
||||
@@ -16,10 +16,25 @@
|
||||
* - myWeakness / myTrend (analytics, P4)
|
||||
* - myNotifications / myNotificationUnreadCount (notifications, P5)
|
||||
* - aiChat (ai, P5)
|
||||
* Mutation:
|
||||
* - myAttendance (attendance, P3)
|
||||
* - examDetail / homeworkDetail / serverTime / mySchedule / studentGrowth
|
||||
* - assignmentAnalysis / myProfile / myMasterySummary / myDiagnosticReports
|
||||
* - myErrorBook / announcements / announcementDetail / myLeaveRequests
|
||||
* - myElectiveSelections / availableElectiveCourses / myLessonPlans
|
||||
* - lessonPlanDetail / myCoursePlans / coursePlanDetail / myReportCard
|
||||
* - myPracticeSessions
|
||||
* Mutation (23):
|
||||
* - submitHomework (homework)
|
||||
* - markNotificationAsRead (notifications, P5)
|
||||
* Subscription:
|
||||
* - markAsRead / markAllAsRead / updateNotificationPreference
|
||||
* - submitExam / saveExamDraft / recordExamViolation / recordPasteEvent
|
||||
* - updateProfile / changePassword / requestExtension
|
||||
* - joinClass / leaveClass
|
||||
* - addErrorBookItem / updateErrorBookItem / deleteErrorBookItem
|
||||
* - markAnnouncementRead / createLeaveRequest / cancelLeaveRequest
|
||||
* - selectElectiveCourse / dropElectiveCourse
|
||||
* - startPracticeSession / submitPracticeAnswer
|
||||
* Subscription (1):
|
||||
* - aiStreamChat (ai-stream, P5 SSE)
|
||||
*/
|
||||
import { authResolvers } from "./auth.resolver.js";
|
||||
@@ -33,6 +48,8 @@ import { analyticsResolvers } from "./analytics.resolver.js";
|
||||
import { notificationsResolvers } from "./notifications.resolver.js";
|
||||
import { aiResolvers } from "./ai.resolver.js";
|
||||
import { aiStreamResolvers } from "./ai-stream.resolver.js";
|
||||
import { extendedQueriesResolvers } from "./extended-queries.resolver.js";
|
||||
import { extendedMutationsResolvers } from "./extended-mutations.resolver.js";
|
||||
|
||||
/**
|
||||
* 全部 Resolver 手动合并 (spread 运算符).
|
||||
@@ -52,10 +69,12 @@ export const studentBffResolvers = {
|
||||
...(analyticsResolvers.Query ?? {}),
|
||||
...(notificationsResolvers.Query ?? {}),
|
||||
...(aiResolvers.Query ?? {}),
|
||||
...(extendedQueriesResolvers.Query ?? {}),
|
||||
},
|
||||
Mutation: {
|
||||
...(homeworkResolvers.Mutation ?? {}),
|
||||
...(notificationsResolvers.Mutation ?? {}),
|
||||
...(extendedMutationsResolvers.Mutation ?? {}),
|
||||
},
|
||||
Subscription: {
|
||||
...(aiStreamResolvers.Subscription ?? {}),
|
||||
|
||||
Reference in New Issue
Block a user