feat(parent-bff): graphql schema 扩展 + extended-resolvers + grpc factory + nextstep 文档
This commit is contained in:
@@ -11,6 +11,7 @@ import type {
|
||||
} from "../clients/dtos.js";
|
||||
import type {
|
||||
ChildAnalyticsType,
|
||||
ChildBriefType,
|
||||
ChildType,
|
||||
ClassInfoType,
|
||||
ExamType,
|
||||
@@ -41,7 +42,23 @@ export function mapParent(dto: UserInfoDto): ParentType {
|
||||
name: dto.name,
|
||||
avatar: null,
|
||||
roles: dto.roles,
|
||||
permissions: dto.permissions,
|
||||
dataScope: "CHILDREN",
|
||||
schoolId: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ChildDto → ChildBriefType(扁平结构,用于 myChildren / childSummary 等扩展查询)。
|
||||
*/
|
||||
export function mapChildBrief(dto: ChildDto): ChildBriefType {
|
||||
return {
|
||||
id: dto.id,
|
||||
name: dto.name,
|
||||
grade: dto.grade,
|
||||
classId: dto.classId,
|
||||
className: dto.className,
|
||||
avatar: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
credentials,
|
||||
loadPackageDefinition,
|
||||
@@ -8,12 +7,14 @@ import {
|
||||
import protoLoader from "@grpc/proto-loader";
|
||||
import { logger } from "../../shared/observability/logger.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const PROTO_ROOT = resolve(
|
||||
__dirname,
|
||||
"../../../../packages/shared-proto/proto",
|
||||
);
|
||||
/**
|
||||
* 使用 process.cwd() 解析 proto 路径,兼容开发模式(cwd=services/parent-bff)
|
||||
* 和生产模式(cwd=/app/services/parent-bff)。
|
||||
*
|
||||
* 开发模式:services/parent-bff → ../../packages/shared-proto/proto
|
||||
* 生产模式:/app/services/parent-bff → ../../packages/shared-proto/proto → /app/packages/shared-proto/proto
|
||||
*/
|
||||
const PROTO_ROOT = resolve(process.cwd(), "../../packages/shared-proto/proto");
|
||||
|
||||
const LOADER_OPTIONS: protoLoader.Options = {
|
||||
keepCase: false,
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { YogaInstance } from "../graphql/yoga.js";
|
||||
*
|
||||
* graphql-yoga v5:Yoga 实例本身是 callable handler,直接调用 yoga(req, res)。
|
||||
*/
|
||||
@Controller("graphql")
|
||||
@Controller("v1/graphql")
|
||||
export class GraphqlController {
|
||||
private readonly yoga: YogaInstance;
|
||||
|
||||
|
||||
@@ -63,6 +63,6 @@ import { buildResolvers, type ResolverDeps } from "./resolvers/index.js";
|
||||
})
|
||||
export class GraphqlModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer): void {
|
||||
consumer.apply(contextMiddleware).forRoutes("graphql");
|
||||
consumer.apply(contextMiddleware).forRoutes("v1/graphql");
|
||||
}
|
||||
}
|
||||
|
||||
1109
services/parent-bff/src/graphql/resolvers/extended-resolvers.ts
Normal file
1109
services/parent-bff/src/graphql/resolvers/extended-resolvers.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,42 @@ import {
|
||||
buildNotificationPreferencesQueryResolver,
|
||||
buildUpdateNotificationPreferencesMutationResolver,
|
||||
} from "./notification-preference.resolver.js";
|
||||
import {
|
||||
buildAcademicYearsQueryResolver,
|
||||
buildChildAttendanceQueryResolver,
|
||||
buildChildClassesQueryResolver,
|
||||
buildChildCoursePlanDetailQueryResolver,
|
||||
buildChildCoursePlansQueryResolver,
|
||||
buildChildDetailQueryResolver,
|
||||
buildChildDiagnosticReportsQueryResolver,
|
||||
buildChildElectiveQueryResolver,
|
||||
buildChildErrorBookStatsQueryResolver,
|
||||
buildChildExamResultQueryResolver,
|
||||
buildChildGrowthArchiveQueryResolver,
|
||||
buildChildLeaveRequestsQueryResolver,
|
||||
buildChildLearningPathQueryResolver,
|
||||
buildChildLessonPlanDetailQueryResolver,
|
||||
buildChildLessonPlansQueryResolver,
|
||||
buildChildMasterySummaryQueryResolver,
|
||||
buildChildPracticeSessionsQueryResolver,
|
||||
buildChildPracticeStatsQueryResolver,
|
||||
buildChildReportCardQueryResolver,
|
||||
buildChildSummaryQueryResolver,
|
||||
buildChildTopWrongQuestionsQueryResolver,
|
||||
buildChildTrendQueryResolver,
|
||||
buildChildWeakKpsQueryResolver,
|
||||
buildChildWeaknessQueryResolver,
|
||||
buildCreateLeaveRequestMutationResolver,
|
||||
buildCurrentUserQueryResolver,
|
||||
buildExportChildGradesMutationResolver,
|
||||
buildMarkAllAsReadMutationResolver,
|
||||
buildMarkAsReadMutationResolver,
|
||||
buildMyChildrenQueryResolver,
|
||||
buildMyNotificationPreferencesQueryResolver,
|
||||
buildMyNotificationsQueryResolver,
|
||||
buildSwitchChildMutationResolver,
|
||||
buildUpdateMyNotificationPreferencesMutationResolver,
|
||||
} from "./extended-resolvers.js";
|
||||
|
||||
/**
|
||||
* Resolver 依赖注入容器。
|
||||
@@ -96,6 +132,47 @@ const DateTimeScalar = new GraphQLScalarType({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* JSON 标量:任意 JSON 值(用于 NotificationPreferences.preferences 等动态结构)。
|
||||
*/
|
||||
const JSONScalar = new GraphQLScalarType({
|
||||
name: "JSON",
|
||||
description: "Arbitrary JSON value",
|
||||
|
||||
serialize(value: unknown): unknown {
|
||||
return value;
|
||||
},
|
||||
|
||||
parseValue(value: unknown): unknown {
|
||||
return value;
|
||||
},
|
||||
|
||||
parseLiteral(ast): unknown {
|
||||
switch (ast.kind) {
|
||||
case Kind.STRING:
|
||||
case Kind.BOOLEAN:
|
||||
return ast.value;
|
||||
case Kind.INT:
|
||||
return Number(ast.value);
|
||||
case Kind.FLOAT:
|
||||
return Number(ast.value);
|
||||
case Kind.LIST:
|
||||
return ast.values.map((v) => JSONScalar.parseLiteral(v));
|
||||
case Kind.OBJECT: {
|
||||
const obj: Record<string, unknown> = {};
|
||||
for (const field of ast.fields) {
|
||||
obj[field.name.value] = JSONScalar.parseLiteral(field.value);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
case Kind.NULL:
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 构建 GraphQL resolvers(对齐 02-architecture-design.md §4.2 SDL)。
|
||||
*
|
||||
@@ -111,8 +188,10 @@ const DateTimeScalar = new GraphQLScalarType({
|
||||
export function buildResolvers(deps: ResolverDeps): IResolvers {
|
||||
return {
|
||||
DateTime: DateTimeScalar,
|
||||
JSON: JSONScalar,
|
||||
|
||||
Query: {
|
||||
// Legacy(11 个)
|
||||
dashboard: buildDashboardResolver(deps),
|
||||
viewports: buildViewportsQueryResolver(deps),
|
||||
me: buildMeQueryResolver(deps),
|
||||
@@ -122,19 +201,56 @@ export function buildResolvers(deps: ResolverDeps): IResolvers {
|
||||
childHomework: buildChildHomeworkQueryResolver(deps),
|
||||
childExams: buildChildExamsQueryResolver(deps),
|
||||
childAnalytics: buildChildAnalyticsQueryResolver(deps),
|
||||
|
||||
// P5 通知 resolvers
|
||||
notifications: buildNotificationsQueryResolver(deps),
|
||||
notificationPreferences: buildNotificationPreferencesQueryResolver(deps),
|
||||
|
||||
// Extended(21 个)
|
||||
currentUser: buildCurrentUserQueryResolver(deps),
|
||||
myChildren: buildMyChildrenQueryResolver(deps),
|
||||
childSummary: buildChildSummaryQueryResolver(deps),
|
||||
childDetail: buildChildDetailQueryResolver(deps),
|
||||
childAttendance: buildChildAttendanceQueryResolver(deps),
|
||||
childExamResult: buildChildExamResultQueryResolver(deps),
|
||||
childClasses: buildChildClassesQueryResolver(deps),
|
||||
childReportCard: buildChildReportCardQueryResolver(deps),
|
||||
childGrowthArchive: buildChildGrowthArchiveQueryResolver(deps),
|
||||
childWeakness: buildChildWeaknessQueryResolver(deps),
|
||||
childTrend: buildChildTrendQueryResolver(deps),
|
||||
childLearningPath: buildChildLearningPathQueryResolver(deps),
|
||||
childErrorBookStats: buildChildErrorBookStatsQueryResolver(deps),
|
||||
childTopWrongQuestions: buildChildTopWrongQuestionsQueryResolver(deps),
|
||||
childWeakKps: buildChildWeakKpsQueryResolver(deps),
|
||||
childMasterySummary: buildChildMasterySummaryQueryResolver(deps),
|
||||
childDiagnosticReports: buildChildDiagnosticReportsQueryResolver(deps),
|
||||
childPracticeStats: buildChildPracticeStatsQueryResolver(deps),
|
||||
childPracticeSessions: buildChildPracticeSessionsQueryResolver(deps),
|
||||
childCoursePlans: buildChildCoursePlansQueryResolver(deps),
|
||||
childCoursePlanDetail: buildChildCoursePlanDetailQueryResolver(deps),
|
||||
childLessonPlans: buildChildLessonPlansQueryResolver(deps),
|
||||
childLessonPlanDetail: buildChildLessonPlanDetailQueryResolver(deps),
|
||||
childElective: buildChildElectiveQueryResolver(deps),
|
||||
childLeaveRequests: buildChildLeaveRequestsQueryResolver(deps),
|
||||
academicYears: buildAcademicYearsQueryResolver(deps),
|
||||
myNotifications: buildMyNotificationsQueryResolver(deps),
|
||||
myNotificationPreferences:
|
||||
buildMyNotificationPreferencesQueryResolver(deps),
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
// Legacy(3 个)
|
||||
selectChild: buildSelectChildMutationResolver(deps),
|
||||
|
||||
// P5 通知 mutations
|
||||
markNotificationRead: buildMarkNotificationReadMutationResolver(deps),
|
||||
updateNotificationPreferences:
|
||||
buildUpdateNotificationPreferencesMutationResolver(deps),
|
||||
|
||||
// Extended(6 个)
|
||||
markAsRead: buildMarkAsReadMutationResolver(deps),
|
||||
markAllAsRead: buildMarkAllAsReadMutationResolver(deps),
|
||||
switchChild: buildSwitchChildMutationResolver(deps),
|
||||
updateMyNotificationPreferences:
|
||||
buildUpdateMyNotificationPreferencesMutationResolver(deps),
|
||||
createLeaveRequest: buildCreateLeaveRequestMutationResolver(deps),
|
||||
exportChildGrades: buildExportChildGradesMutationResolver(deps),
|
||||
},
|
||||
|
||||
Child: {
|
||||
|
||||
@@ -26,13 +26,33 @@ const CHANNEL_MAP: Record<string, NotificationChannel> = {
|
||||
|
||||
/**
|
||||
* DTO → GraphQL Type 映射。
|
||||
*
|
||||
* 旧版 schema 使用 channels/eventTypes 顶层字段,
|
||||
* 新版 schema 使用 preferences/defaults 嵌套结构。
|
||||
* 此处同时填充两组字段以兼容 legacy NotificationPreferences 和新版 MyNotificationPreferences。
|
||||
*/
|
||||
function mapPreferences(
|
||||
dto: NotificationPreferencesDto,
|
||||
parentId: string,
|
||||
): NotificationPreferencesType {
|
||||
const channels = dto.channels.map((c) => CHANNEL_MAP[c] ?? "APP");
|
||||
const eventTypes = { ...dto.eventTypes };
|
||||
return {
|
||||
channels: dto.channels.map((c) => CHANNEL_MAP[c] ?? "APP"),
|
||||
eventTypes: { ...dto.eventTypes },
|
||||
parentId,
|
||||
preferences: { channels, eventTypes },
|
||||
defaults: {
|
||||
channels: ["APP" as NotificationChannel],
|
||||
eventTypes: {
|
||||
gradeReleased: true,
|
||||
homeworkGraded: true,
|
||||
examPublished: true,
|
||||
attendanceAlert: true,
|
||||
schoolAnnouncement: true,
|
||||
},
|
||||
},
|
||||
updatedAt: null,
|
||||
channels,
|
||||
eventTypes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +83,29 @@ export function buildNotificationPreferencesQueryResolver(deps: {
|
||||
if (!dto) {
|
||||
// 下游失败且无缓存,返回默认偏好
|
||||
return {
|
||||
channels: ["APP"],
|
||||
parentId,
|
||||
preferences: {
|
||||
channels: ["APP" as NotificationChannel],
|
||||
eventTypes: {
|
||||
gradeReleased: true,
|
||||
homeworkGraded: true,
|
||||
examPublished: true,
|
||||
attendanceAlert: true,
|
||||
schoolAnnouncement: true,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
channels: ["APP" as NotificationChannel],
|
||||
eventTypes: {
|
||||
gradeReleased: true,
|
||||
homeworkGraded: true,
|
||||
examPublished: true,
|
||||
attendanceAlert: true,
|
||||
schoolAnnouncement: true,
|
||||
},
|
||||
},
|
||||
updatedAt: null,
|
||||
channels: ["APP" as NotificationChannel],
|
||||
eventTypes: {
|
||||
gradeReleased: true,
|
||||
homeworkGraded: true,
|
||||
@@ -73,7 +115,7 @@ export function buildNotificationPreferencesQueryResolver(deps: {
|
||||
},
|
||||
};
|
||||
}
|
||||
return mapPreferences(dto);
|
||||
return mapPreferences(dto, parentId);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,6 +181,6 @@ export function buildUpdateNotificationPreferencesMutationResolver(deps: {
|
||||
"Notification preferences updated",
|
||||
);
|
||||
|
||||
return mapPreferences(updated);
|
||||
return mapPreferences(updated, parentId);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MsgClient } from "../../clients/msg.client.js";
|
||||
import type { NotificationDto } from "../../clients/dtos.js";
|
||||
import type { NotificationItem, NotificationType } from "../types.js";
|
||||
import type { NotificationItem } from "../types.js";
|
||||
import type { GraphqlContext } from "../context.js";
|
||||
import { CacheKeys } from "../../shared/cache/cache-key.builder.js";
|
||||
import {
|
||||
@@ -10,29 +10,26 @@ import {
|
||||
import { env } from "../../config/env.js";
|
||||
import { logger } from "../../shared/observability/logger.js";
|
||||
|
||||
const NOTIFICATION_TYPE_MAP: Record<string, NotificationType> = {
|
||||
SYSTEM: "SYSTEM",
|
||||
EXAM: "EXAM",
|
||||
HOMEWORK: "HOMEWORK",
|
||||
GRADE: "GRADE",
|
||||
ATTENDANCE: "ATTENDANCE",
|
||||
ANNOUNCEMENT: "ANNOUNCEMENT",
|
||||
};
|
||||
|
||||
/**
|
||||
* DTO → GraphQL Type 映射。
|
||||
*
|
||||
* createdAt: proto int64(epoch ms)→ ISO 8601 字符串(DateTime scalar)
|
||||
* 旧版 schema 使用 type/content,新版 schema 使用 eventType/body,
|
||||
* 此处同时填充两组字段以兼容 legacy Notification type 和新版 MyNotification。
|
||||
*/
|
||||
function mapNotification(dto: NotificationDto): NotificationItem {
|
||||
return {
|
||||
id: dto.id,
|
||||
type: NOTIFICATION_TYPE_MAP[dto.type] ?? "SYSTEM",
|
||||
type: dto.type || "SYSTEM",
|
||||
eventType: dto.type || "SYSTEM",
|
||||
title: dto.title,
|
||||
content: dto.content,
|
||||
body: dto.content,
|
||||
read: dto.isRead,
|
||||
childId: dto.childId ?? null,
|
||||
createdAt: new Date(dto.createdAt).toISOString(),
|
||||
actionUrl: null,
|
||||
pinned: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,11 +110,15 @@ export function buildMarkNotificationReadMutationResolver(deps: {
|
||||
return {
|
||||
id: args.notificationId,
|
||||
type: "SYSTEM",
|
||||
eventType: "SYSTEM",
|
||||
title: "",
|
||||
content: "",
|
||||
body: "",
|
||||
read: true,
|
||||
childId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
actionUrl: null,
|
||||
pinned: false,
|
||||
};
|
||||
}
|
||||
return mapNotification(updated);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { resolve } from "node:path";
|
||||
import { makeExecutableSchema } from "@graphql-tools/schema";
|
||||
import type { IResolvers } from "@graphql-tools/utils";
|
||||
|
||||
@@ -9,11 +8,13 @@ import type { IResolvers } from "@graphql-tools/utils";
|
||||
*
|
||||
* 文件位置:packages/shared-ts/contracts/graphql/parent-bff.graphql
|
||||
* 该文件是 parent-bff GraphQL 端点的契约唯一源,前后端共享。
|
||||
*
|
||||
* 使用 process.cwd() 解析路径,兼容开发模式(cwd=services/parent-bff)
|
||||
* 和生产模式(cwd=/app/services/parent-bff)。
|
||||
*/
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SDL_PATH = resolve(
|
||||
__dirname,
|
||||
"../../../../packages/shared-ts/contracts/graphql/parent-bff.graphql",
|
||||
process.cwd(),
|
||||
"../../packages/shared-ts/contracts/graphql/parent-bff.graphql",
|
||||
);
|
||||
|
||||
const typeDefs = readFileSync(SDL_PATH, "utf-8");
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
export type DataScope =
|
||||
"SELF" | "CHILDREN" | "CLASS" | "GRADE" | "SCHOOL" | "DISTRICT" | "ALL";
|
||||
|
||||
// ============ Parent / Viewport ============
|
||||
|
||||
export interface ParentType {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar?: string | null;
|
||||
roles: string[];
|
||||
permissions?: string[];
|
||||
dataScope: DataScope;
|
||||
schoolId?: string | null;
|
||||
}
|
||||
|
||||
export interface ViewportItemType {
|
||||
@@ -26,6 +30,8 @@ export interface ViewportItemType {
|
||||
requiredPermission?: string | null;
|
||||
}
|
||||
|
||||
// ============ Legacy Child / Grade / Homework / Exam ============
|
||||
|
||||
export interface ClassInfoType {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -98,6 +104,7 @@ export interface ChildAnalyticsType {
|
||||
classAverage?: number | null;
|
||||
}
|
||||
|
||||
/** 旧版 Child 类型(含嵌套 class 对象,用于 legacy Child field resolver) */
|
||||
export interface ChildType {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -110,17 +117,373 @@ export interface ChildType {
|
||||
analytics?: ChildAnalyticsType;
|
||||
}
|
||||
|
||||
export type NotificationType =
|
||||
"SYSTEM" | "EXAM" | "HOMEWORK" | "GRADE" | "ATTENDANCE" | "ANNOUNCEMENT";
|
||||
// ============ Extended: Child Brief / Summary / Detail ============
|
||||
|
||||
export interface ChildBriefType {
|
||||
id: string;
|
||||
name: string;
|
||||
grade: string;
|
||||
classId: string | null;
|
||||
className: string | null;
|
||||
avatar?: string | null;
|
||||
}
|
||||
|
||||
export interface ChildSummaryType {
|
||||
childId: string;
|
||||
avgScore: number | null;
|
||||
classRank: number | null;
|
||||
classSize: number | null;
|
||||
attendanceRate: number | null;
|
||||
pendingHomeworkCount: number;
|
||||
recentGradeTrend: number | null;
|
||||
recentScores: ChildGradeType[];
|
||||
upcomingEvents: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
dueDate: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ChildDetailType {
|
||||
childId: string;
|
||||
basicInfo: {
|
||||
name: string;
|
||||
avatar: string | null;
|
||||
grade: string;
|
||||
className: string;
|
||||
schoolName: string;
|
||||
relation: string;
|
||||
};
|
||||
todaySchedule: unknown[];
|
||||
weeklySchedule: unknown[];
|
||||
homeworkSummary: {
|
||||
pendingCount: number;
|
||||
overdueCount: number;
|
||||
submittedCount: number;
|
||||
gradedCount: number;
|
||||
};
|
||||
gradeSummary: {
|
||||
avgScore: number;
|
||||
classRank: number | null;
|
||||
classSize: number | null;
|
||||
trend: number | null;
|
||||
};
|
||||
examResults: {
|
||||
upcoming: number;
|
||||
completed: number;
|
||||
avgScore: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
// ============ Extended: Child Grade / Homework / Exam ============
|
||||
|
||||
export interface ChildGradeType {
|
||||
examId: string;
|
||||
examName: string;
|
||||
examDate: string;
|
||||
subject: string;
|
||||
studentScore: number;
|
||||
classAverage: number | null;
|
||||
classMax: number | null;
|
||||
classMin: number | null;
|
||||
gradeLevel: string | null;
|
||||
}
|
||||
|
||||
export interface ChildHomeworkType {
|
||||
id: string;
|
||||
title: string;
|
||||
subject: string;
|
||||
className: string;
|
||||
assignedDate: string;
|
||||
dueDate: string;
|
||||
status: string;
|
||||
score: number | null;
|
||||
maxScore: number | null;
|
||||
feedback: string | null;
|
||||
}
|
||||
|
||||
export interface ChildExamType {
|
||||
id: string;
|
||||
name: string;
|
||||
subject: string;
|
||||
status: string;
|
||||
startsAt: string;
|
||||
expiresAt: string | null;
|
||||
durationSeconds: number | null;
|
||||
questionCount: number | null;
|
||||
totalScore: number | null;
|
||||
submittedAt: string | null;
|
||||
}
|
||||
|
||||
export interface AttendanceRecordType {
|
||||
id: string;
|
||||
date: string;
|
||||
status: string;
|
||||
checkInTime: string | null;
|
||||
checkOutTime: string | null;
|
||||
note: string | null;
|
||||
}
|
||||
|
||||
export interface ExamResultType {
|
||||
examId: string;
|
||||
childId: string;
|
||||
score: number | null;
|
||||
rank: number | null;
|
||||
subjectScores: Array<{
|
||||
subject: string;
|
||||
score: number;
|
||||
fullScore: number;
|
||||
}>;
|
||||
feedback: string | null;
|
||||
}
|
||||
|
||||
export interface ChildClassType {
|
||||
id: string;
|
||||
name: string;
|
||||
homeroomTeacher: string | null;
|
||||
studentCount: number | null;
|
||||
grade: string;
|
||||
year: string | null;
|
||||
}
|
||||
|
||||
export interface ReportCardType {
|
||||
childId: string;
|
||||
academicYearId: string;
|
||||
semester: number;
|
||||
subjects: Array<{
|
||||
subject: string;
|
||||
score: number;
|
||||
grade: string;
|
||||
teacherComment: string | null;
|
||||
}>;
|
||||
overallComment: string | null;
|
||||
classRank: number | null;
|
||||
}
|
||||
|
||||
export interface GrowthArchiveType {
|
||||
childId: string;
|
||||
subject: string | null;
|
||||
dataPoints: Array<{
|
||||
date: string;
|
||||
category: string;
|
||||
title: string;
|
||||
description: string;
|
||||
evidence: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
// ============ Extended: Analytics ============
|
||||
|
||||
export interface WeaknessItemType {
|
||||
id: string;
|
||||
knowledgePoint: string;
|
||||
masteryLevel: number;
|
||||
subject: string;
|
||||
recommendation: string | null;
|
||||
}
|
||||
|
||||
export type TrendPeriod = "WEEK" | "MONTH" | "SEMESTER" | "YEAR";
|
||||
|
||||
export interface ChildTrendType {
|
||||
childId: string;
|
||||
period: TrendPeriod;
|
||||
dataPoints: Array<{
|
||||
date: string;
|
||||
score: number;
|
||||
subject: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface LearningPathItemType {
|
||||
id: string;
|
||||
title: string;
|
||||
subject: string;
|
||||
order: number;
|
||||
masteryLevel: number;
|
||||
resources: string[];
|
||||
}
|
||||
|
||||
export interface ErrorBookStatsType {
|
||||
childId: string;
|
||||
totalCount: number;
|
||||
newCount: number;
|
||||
learningCount: number;
|
||||
masteredCount: number;
|
||||
dueReviewCount: number;
|
||||
masteredRate: number;
|
||||
}
|
||||
|
||||
export interface WrongQuestionType {
|
||||
id: string;
|
||||
questionId: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
wrongAnswer: string;
|
||||
correctAnswer: string;
|
||||
addedAt: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface WeakKpType {
|
||||
id: string;
|
||||
knowledgePoint: string;
|
||||
subject: string;
|
||||
masteryLevel: number;
|
||||
recommendation: string | null;
|
||||
}
|
||||
|
||||
export interface MasterySummaryType {
|
||||
childId: string;
|
||||
overallMastery: number;
|
||||
subjectMastery: Array<{
|
||||
subject: string;
|
||||
mastery: number;
|
||||
totalKps: number;
|
||||
masteredKps: number;
|
||||
}>;
|
||||
totalKps: number;
|
||||
masteredKps: number;
|
||||
}
|
||||
|
||||
export interface DiagnosticReportType {
|
||||
id: string;
|
||||
childId: string;
|
||||
subject: string;
|
||||
reportDate: string;
|
||||
summary: string;
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
export interface PracticeStatsType {
|
||||
childId: string;
|
||||
totalSessions: number;
|
||||
completedSessions: number;
|
||||
totalQuestionsAnswered: number;
|
||||
overallAccuracy: number;
|
||||
}
|
||||
|
||||
export interface PracticeSessionType {
|
||||
id: string;
|
||||
childId: string;
|
||||
subject: string;
|
||||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
questionCount: number;
|
||||
correctCount: number;
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
// ============ Extended: Course / Lesson / Elective ============
|
||||
|
||||
export interface CoursePlanType {
|
||||
id: string;
|
||||
childId: string;
|
||||
subject: string;
|
||||
title: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export interface CoursePlanDetailType {
|
||||
id: string;
|
||||
childId: string;
|
||||
subject: string;
|
||||
title: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
progress: number;
|
||||
lessons: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
completed: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface LessonPlanType {
|
||||
id: string;
|
||||
childId: string;
|
||||
subject: string;
|
||||
title: string;
|
||||
date: string;
|
||||
teacherName: string;
|
||||
}
|
||||
|
||||
export interface LessonPlanDetailType {
|
||||
id: string;
|
||||
childId: string;
|
||||
subject: string;
|
||||
title: string;
|
||||
date: string;
|
||||
teacherName: string;
|
||||
objectives: string[];
|
||||
content: string;
|
||||
homework: string | null;
|
||||
}
|
||||
|
||||
export interface ElectiveCourseType {
|
||||
id: string;
|
||||
childId: string;
|
||||
name: string;
|
||||
subject: string;
|
||||
teacher: string;
|
||||
schedule: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
// ============ Extended: Leave Request ============
|
||||
|
||||
export interface LeaveRequestInput {
|
||||
childId: string;
|
||||
type: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface LeaveRequestItem {
|
||||
id: string;
|
||||
childId: string;
|
||||
childName: string | null;
|
||||
className: string | null;
|
||||
type: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
submittedAt: string;
|
||||
reviewedAt: string | null;
|
||||
reviewerName: string | null;
|
||||
reviewComment: string | null;
|
||||
}
|
||||
|
||||
// ============ Extended: Academic Year ============
|
||||
|
||||
export interface AcademicYearType {
|
||||
id: string;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
// ============ Extended: Notification ============
|
||||
|
||||
export interface NotificationItem {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
childId: string | null;
|
||||
eventType: string;
|
||||
title: string;
|
||||
content: string;
|
||||
body: string;
|
||||
read: boolean;
|
||||
childId?: string | null;
|
||||
createdAt: string;
|
||||
actionUrl: string | null;
|
||||
pinned: boolean;
|
||||
/** 旧版字段(legacy schema 用) */
|
||||
type?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export type NotificationChannel = "APP" | "SMS" | "EMAIL" | "WECHAT";
|
||||
@@ -134,10 +497,50 @@ export interface NotificationEventTypes {
|
||||
}
|
||||
|
||||
export interface NotificationPreferencesType {
|
||||
channels: NotificationChannel[];
|
||||
eventTypes: NotificationEventTypes;
|
||||
parentId: string;
|
||||
preferences: {
|
||||
channels?: NotificationChannel[];
|
||||
eventTypes?: Partial<NotificationEventTypes>;
|
||||
};
|
||||
defaults: {
|
||||
channels: NotificationChannel[];
|
||||
eventTypes: NotificationEventTypes;
|
||||
};
|
||||
updatedAt: string | null;
|
||||
/** 旧版字段(legacy schema 用) */
|
||||
channels?: NotificationChannel[];
|
||||
eventTypes?: NotificationEventTypes;
|
||||
}
|
||||
|
||||
export interface UpdateNotificationPreferencesResultType {
|
||||
parentId: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface MarkAsReadResultType {
|
||||
id: string;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export interface MarkAllAsReadResultType {
|
||||
count: number;
|
||||
}
|
||||
|
||||
// ============ Extended: Mutation Results ============
|
||||
|
||||
export interface SwitchChildResultType {
|
||||
childId: string;
|
||||
childName: string;
|
||||
selectedAt: string;
|
||||
}
|
||||
|
||||
export interface ExportChildGradesResultType {
|
||||
downloadUrl: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
// ============ Legacy Types(保留旧版兼容) ============
|
||||
|
||||
export interface DashboardDataType {
|
||||
parent: ParentType | null;
|
||||
children: ChildType[];
|
||||
|
||||
@@ -74,7 +74,7 @@ export function createYogaInstance(
|
||||
|
||||
return createYoga({
|
||||
schema,
|
||||
graphqlEndpoint: "/graphql",
|
||||
graphqlEndpoint: "/v1/graphql",
|
||||
graphiql:
|
||||
env.NODE_ENV === "development" && env.GRAPHQL_INTROSPECTION_ENABLED,
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import pino from "pino";
|
||||
import { pino } from "pino";
|
||||
import { env } from "../../config/env.js";
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user