From e9d3030f2f86687c472c0cf2dc269684e2947051 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:51:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(shared-ts):=20=E5=90=8C=E6=AD=A5=203=20?= =?UTF-8?q?=E4=B8=AA=20BFF=20GraphQL=20schema=20+=20downstream-client/logg?= =?UTF-8?q?er/outbox=20=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contracts/graphql/parent-bff.graphql | 509 +++- .../graphql/student-bff.schema.graphql | 848 +++++- .../graphql/teacher-bff.schema.graphql | 2341 +++++++++++++++-- .../shared-ts/src/bff/downstream-client.ts | 2 +- packages/shared-ts/src/bff/logger.ts | 9 +- .../shared-ts/src/outbox/outbox.module.ts | 9 +- 6 files changed, 3474 insertions(+), 244 deletions(-) diff --git a/packages/shared-ts/contracts/graphql/parent-bff.graphql b/packages/shared-ts/contracts/graphql/parent-bff.graphql index 908d43c..1bd1b2e 100644 --- a/packages/shared-ts/contracts/graphql/parent-bff.graphql +++ b/packages/shared-ts/contracts/graphql/parent-bff.graphql @@ -10,10 +10,13 @@ # - C1:错误码前缀 BFF_PARENT_ # - U4:BFF 豁免 @RequirePermission,仅校验 x-user-id + ChildGuard # - 02 §9 #5:depth ≤ 7 / cost ≤ 1000 +# +# 版本:v2(32 Query + 6 Mutation,对齐 parent-portal 全部 operations) scalar DateTime +scalar JSON -# ============ Types ============ +# ============ Legacy Types ============ type Parent { id: ID! @@ -21,7 +24,9 @@ type Parent { name: String! avatar: String roles: [String!]! + permissions: [String!] dataScope: DataScope! + schoolId: ID } enum DataScope { @@ -184,9 +189,413 @@ type DashboardData { degraded: Boolean! } -# ============ Query ============ +# ============ Extended Types(v2 新增) ============ + +type ChildBrief { + id: ID! + name: String! + grade: String! + classId: ID + className: String + avatar: String +} + +type ChildSummary { + childId: ID! + avgScore: Float + classRank: Int + classSize: Int + attendanceRate: Float + pendingHomeworkCount: Int! + recentGradeTrend: Float + recentScores: [ChildGrade!]! + upcomingEvents: [UpcomingEvent!]! +} + +type UpcomingEvent { + id: ID! + type: String! + title: String! + dueDate: DateTime! +} + +type ChildDetail { + childId: ID! + basicInfo: ChildBasicInfo! + todaySchedule: [JSON!]! + weeklySchedule: [JSON!]! + homeworkSummary: HomeworkSummary! + gradeSummary: GradeSummary! + examResults: ExamResultSummary! +} + +type ChildBasicInfo { + name: String! + avatar: String + grade: String! + className: String! + schoolName: String! + relation: String! +} + +type HomeworkSummary { + pendingCount: Int! + overdueCount: Int! + submittedCount: Int! + gradedCount: Int! +} + +type GradeSummary { + avgScore: Float! + classRank: Int + classSize: Int + trend: Float +} + +type ExamResultSummary { + upcoming: Int! + completed: Int! + avgScore: Float +} + +type ChildGrade { + examId: ID! + examName: String! + examDate: DateTime! + subject: String! + studentScore: Float! + classAverage: Float + classMax: Float + classMin: Float + gradeLevel: String +} + +type ChildHomework { + id: ID! + title: String! + subject: String! + className: String! + assignedDate: DateTime! + dueDate: DateTime! + status: String! + score: Float + maxScore: Float + feedback: String +} + +type ChildExam { + id: ID! + name: String! + subject: String! + status: String! + startsAt: DateTime! + expiresAt: DateTime + durationSeconds: Int + questionCount: Int + totalScore: Float + submittedAt: DateTime +} + +type AttendanceRecord { + id: ID! + date: DateTime! + status: String! + checkInTime: DateTime + checkOutTime: DateTime + note: String +} + +type ExamResult { + examId: ID! + childId: ID! + score: Float + rank: Int + subjectScores: [SubjectScore!]! + feedback: String +} + +type SubjectScore { + subject: String! + score: Float! + fullScore: Float! +} + +type ChildClass { + id: ID! + name: String! + homeroomTeacher: String + studentCount: Int + grade: String! + year: String +} + +type ReportCard { + childId: ID! + academicYearId: ID! + semester: Int! + subjects: [ReportCardSubject!]! + overallComment: String + classRank: Int +} + +type ReportCardSubject { + subject: String! + score: Float! + grade: String! + teacherComment: String +} + +type GrowthArchive { + childId: ID! + subject: String + dataPoints: [GrowthDataPoint!]! +} + +type GrowthDataPoint { + date: DateTime! + category: String! + title: String! + description: String! + evidence: String +} + +type WeaknessItem { + id: ID! + knowledgePoint: String! + masteryLevel: Float! + subject: String! + recommendation: String +} + +type ChildTrend { + childId: ID! + period: TrendPeriod! + dataPoints: [TrendDataPoint!]! +} + +type TrendDataPoint { + date: DateTime! + score: Float! + subject: String +} + +type LearningPathItem { + id: ID! + title: String! + subject: String! + order: Int! + masteryLevel: Float! + resources: [String!]! +} + +type ErrorBookStats { + childId: ID! + totalCount: Int! + newCount: Int! + learningCount: Int! + masteredCount: Int! + dueReviewCount: Int! + masteredRate: Float! +} + +type WrongQuestion { + id: ID! + questionId: ID! + subject: String! + content: String! + wrongAnswer: String! + correctAnswer: String! + addedAt: DateTime! + status: String! +} + +type WeakKp { + id: ID! + knowledgePoint: String! + subject: String! + masteryLevel: Float! + recommendation: String +} + +type MasterySummary { + childId: ID! + overallMastery: Float! + subjectMastery: [SubjectMastery!]! + totalKps: Int! + masteredKps: Int! +} + +type SubjectMastery { + subject: String! + mastery: Float! + totalKps: Int! + masteredKps: Int! +} + +type DiagnosticReport { + id: ID! + childId: ID! + subject: String! + reportDate: DateTime! + summary: String! + recommendations: [String!]! +} + +type PracticeStats { + childId: ID! + totalSessions: Int! + completedSessions: Int! + totalQuestionsAnswered: Int! + overallAccuracy: Float! +} + +type PracticeSession { + id: ID! + childId: ID! + subject: String! + startedAt: DateTime! + completedAt: DateTime + questionCount: Int! + correctCount: Int! + accuracy: Float! +} + +type CoursePlan { + id: ID! + childId: ID! + subject: String! + title: String! + startDate: DateTime! + endDate: DateTime! + progress: Float! +} + +type CoursePlanDetail { + id: ID! + childId: ID! + subject: String! + title: String! + startDate: DateTime! + endDate: DateTime! + progress: Float! + lessons: [CoursePlanLesson!]! +} + +type CoursePlanLesson { + id: ID! + title: String! + date: DateTime! + completed: Boolean! +} + +type LessonPlan { + id: ID! + childId: ID! + subject: String! + title: String! + date: DateTime! + teacherName: String! +} + +type LessonPlanDetail { + id: ID! + childId: ID! + subject: String! + title: String! + date: DateTime! + teacherName: String! + objectives: [String!]! + content: String! + homework: String +} + +type ElectiveCourse { + id: ID! + childId: ID! + name: String! + subject: String! + teacher: String! + schedule: String! + selected: Boolean! +} + +type LeaveRequestItem { + id: ID! + childId: ID! + childName: String + className: String + type: String! + startDate: DateTime! + endDate: DateTime! + reason: String! + status: String! + submittedAt: DateTime! + reviewedAt: DateTime + reviewerName: String + reviewComment: String +} + +type AcademicYear { + id: ID! + name: String! + startDate: DateTime! + endDate: DateTime! + isCurrent: Boolean! +} + +type MyNotification { + id: ID! + childId: ID + eventType: String! + title: String! + body: String! + read: Boolean! + createdAt: DateTime! + actionUrl: String + pinned: Boolean! +} + +type MyNotificationPreferences { + parentId: ID! + preferences: JSON! + defaults: NotificationPreferencesDefaults! + updatedAt: DateTime +} + +type NotificationPreferencesDefaults { + channels: [NotificationChannel!]! + eventTypes: NotificationEventTypes! +} + +type MarkAsReadResult { + id: ID! + read: Boolean! +} + +type MarkAllAsReadResult { + count: Int! +} + +type SwitchChildResult { + childId: ID! + childName: String! + selectedAt: DateTime! +} + +type ExportChildGradesResult { + downloadUrl: String! + expiresAt: DateTime! +} + +type UpdateNotificationPreferencesResult { + parentId: ID! + updatedAt: DateTime! +} + +# ============ Query(32 个) ============ type Query { + # Legacy(11 个) dashboard: DashboardData! viewports: [ViewportItem!]! me: Parent! @@ -207,27 +616,81 @@ type Query { pageSize: Int = 20 ): [Notification!]! notificationPreferences: NotificationPreferences! + + # Extended(21 个) + currentUser: Parent! + myChildren: [ChildBrief!]! + childSummary(childId: ID!): ChildSummary + childDetail(childId: ID!): ChildDetail + childAttendance( + childId: ID! + startDate: DateTime! + endDate: DateTime! + ): [AttendanceRecord!]! + childExamResult(childId: ID!, examId: ID!): ExamResult + childClasses(childId: ID!): [ChildClass!]! + childReportCard( + childId: ID! + academicYearId: ID! + semester: Int! + ): ReportCard + childGrowthArchive(childId: ID!, subject: String): GrowthArchive! + childWeakness(childId: ID!): [WeaknessItem!]! + childTrend(childId: ID!, period: TrendPeriod!): ChildTrend! + childLearningPath(childId: ID!): [LearningPathItem!]! + childErrorBookStats(childId: ID!): ErrorBookStats! + childTopWrongQuestions(childId: ID!, limit: Int): [WrongQuestion!]! + childWeakKps(childId: ID!, limit: Int): [WeakKp!]! + childMasterySummary(childId: ID!): MasterySummary! + childDiagnosticReports(childId: ID!): [DiagnosticReport!]! + childPracticeStats(childId: ID!): PracticeStats! + childPracticeSessions(childId: ID!, limit: Int): [PracticeSession!]! + childCoursePlans(childId: ID!): [CoursePlan!]! + childCoursePlanDetail(childId: ID!, planId: ID!): CoursePlanDetail + childLessonPlans(childId: ID!, subject: String): [LessonPlan!]! + childLessonPlanDetail(childId: ID!, planId: ID!): LessonPlanDetail + childElective(childId: ID!): [ElectiveCourse!]! + childLeaveRequests(childId: ID!): [LeaveRequestItem!]! + academicYears: [AcademicYear!]! + myNotifications(unreadOnly: Boolean, limit: Int): [MyNotification!]! + myNotificationPreferences: MyNotificationPreferences! } +# ============ Mutation(6 个) ============ + +type Mutation { + # Legacy(3 个) + selectChild(childId: ID!): SelectChildResult! + markNotificationRead(notificationId: ID!): Notification! + updateNotificationPreferences( + input: UpdateNotificationPreferencesInput! + ): NotificationPreferences! + + # Extended(6 个新 mutation,覆盖前端全部操作) + markAsRead(notificationId: ID!): MarkAsReadResult! + markAllAsRead: MarkAllAsReadResult! + switchChild(childId: ID!): SwitchChildResult! + updateMyNotificationPreferences( + parentId: ID! + preferences: JSON! + defaults: JSON + ): UpdateNotificationPreferencesResult! + createLeaveRequest(input: LeaveRequestInput!): LeaveRequestItem! + exportChildGrades(childId: ID!, subject: String): ExportChildGradesResult! +} + +# ============ Inputs ============ + input DateRangeInput { start: DateTime! end: DateTime! } -# ============ Mutation ============ - -type Mutation { - selectChild(childId: ID!): SelectChildResult! - markNotificationRead(notificationId: ID!): Notification! - updateNotificationPreferences( - input: UpdateNotificationPreferencesInput! - ): NotificationPreferences! -} - -type SelectChildResult { - childId: ID! - selectedAt: DateTime! - audited: Boolean! +enum TrendPeriod { + WEEK + MONTH + SEMESTER + YEAR } input UpdateNotificationPreferencesInput { @@ -242,3 +705,17 @@ input NotificationEventTypesInput { attendanceAlert: Boolean schoolAnnouncement: Boolean } + +input LeaveRequestInput { + childId: ID! + type: String! + startDate: DateTime! + endDate: DateTime! + reason: String! +} + +type SelectChildResult { + childId: ID! + selectedAt: DateTime! + audited: Boolean! +} diff --git a/packages/shared-ts/contracts/graphql/student-bff.schema.graphql b/packages/shared-ts/contracts/graphql/student-bff.schema.graphql index a59dca7..47ca36f 100644 --- a/packages/shared-ts/contracts/graphql/student-bff.schema.graphql +++ b/packages/shared-ts/contracts/graphql/student-bff.schema.graphql @@ -1,9 +1,8 @@ -# student-bff GraphQL Schema (v1) +# student-bff GraphQL Schema (v2) # # 负责人: ai04 # 仲裁依据: coord-final-decisions §2 B1-B8 + president-final-rulings §2.2 # 存放路径: packages/shared-ts/contracts/graphql/student-bff.schema.graphql (president §2.2.1) -# 起草与仲裁流程: ai04 起草 → coord 在批次 2 启动前仲裁第一版 → ai14 (student-portal) 消费 # # 设计规范 (president §2.2.5): # - Query/Mutation 用 camelCase @@ -17,6 +16,12 @@ # 降级模式 (president §2.6 方案 B): # - 下游不可用时 success=true + error=null + data 内 degraded=true # - 降级字段返回 null, 父对象加 degraded/degradedReason/degradedFields +# +# v2 变更: +# - 补全 22 个扩展 Query (examDetail/homeworkDetail/serverTime/mySchedule/...) +# - 补全 21 个扩展 Mutation (submitExam/saveExamDraft/updateProfile/...) +# - startPracticeSession 从 Query 移到 Mutation (语义为创建会话) +# - 总计 36 Query + 23 Mutation + 1 Subscription = 60 operations scalar DateTime scalar JSON @@ -197,6 +202,16 @@ type ExamListPayload implements Degradable { degradedFields: [String!] } +# 考试详情 (含题目与学生提交) +type ExamDetailPayload implements Degradable { + exam: JSON + questions: [JSON!] + mySubmission: JSON + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + # ============================================================================ # 作业 (下游: core-edu HomeworkService) # ============================================================================ @@ -251,6 +266,14 @@ type HomeworkListPayload implements Degradable { degradedFields: [String!] } +# 作业详情 +type HomeworkDetailPayload implements Degradable { + homework: JSON + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + # ============================================================================ # 成绩 (下游: core-edu GradeService) # ============================================================================ @@ -292,8 +315,16 @@ type GradeListPayload implements Degradable { degradedFields: [String!] } +# 成绩报告卡 +type ReportCardPayload implements Degradable { + reportCard: JSON + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + # ============================================================================ -# 考勤 (下游: core-edu AttendanceService, P3 预留) +# 考勤 (下游: core-edu AttendanceService) # ============================================================================ enum AttendanceStatus { @@ -337,6 +368,155 @@ type AttendanceListPayload implements Degradable { degradedFields: [String!] } +# ============================================================================ +# 课表 / 请假 / 选课 / 课案 / 课程计划 (下游: core-edu + content) +# ============================================================================ + +# 服务器时间 +type ServerTimePayload { + serverTime: String! + timezone: String! + timestamp: Int! +} + +# 课表 +type ScheduleItem { + id: ID! + classId: ID! + className: String + subject: String! + date: DateTime! + startTime: DateTime! + endTime: DateTime! + teacher: TeacherBrief + room: String +} + +type SchedulePayload implements Degradable { + items: [ScheduleItem!]! + weekStart: String + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 请假记录 +type LeaveRequest { + id: ID! + studentId: ID! + type: String! + startDate: DateTime! + endDate: DateTime! + reason: String! + status: String! + attachments: [String!] + createdAt: DateTime! + updatedAt: DateTime! +} + +type LeaveRequestsPayload implements Degradable { + requests: [LeaveRequest!]! + totalCount: Int! + pendingCount: Int! + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 选课记录 +type ElectiveSelection { + id: ID! + studentId: ID! + courseId: ID! + courseName: String! + status: String! + selectedAt: DateTime! +} + +type ElectiveSelectionsPayload implements Degradable { + selections: [ElectiveSelection!]! + totalCount: Int! + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 可选课程 +type ElectiveCourse { + id: ID! + title: String! + subject: String! + description: String + capacity: Int! + enrolledCount: Int! + teacher: TeacherBrief +} + +type AvailableElectiveCoursesPayload implements Degradable { + courses: [ElectiveCourse!]! + totalCount: Int! + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 课案 +type LessonPlan { + id: ID! + title: String! + subject: String! + description: String + objectives: [String!] + content: JSON + createdBy: ID! + createdAt: DateTime! + updatedAt: DateTime! +} + +type LessonPlansPayload implements Degradable { + plans: [LessonPlan!]! + totalCount: Int! + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +type LessonPlanDetailPayload implements Degradable { + plan: LessonPlan + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 课程计划 +type CoursePlan { + id: ID! + title: String! + subject: String! + description: String + startDate: DateTime! + endDate: DateTime! + objectives: [String!] + createdBy: ID! + createdAt: DateTime! + updatedAt: DateTime! +} + +type CoursePlansPayload implements Degradable { + plans: [CoursePlan!]! + totalCount: Int! + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +type CoursePlanDetailPayload implements Degradable { + plan: CoursePlan + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + # ============================================================================ # 教材与章节 (下游: content, P4) # ============================================================================ @@ -491,8 +671,98 @@ type TrendPayload implements Degradable { degradedFields: [String!] } +# 学生成长曲线 +type StudentGrowthPayload implements Degradable { + studentId: ID! + subjectId: ID + points: [TrendPoint!]! + averageScore: Float + classAverage: Float + growthRate: Float + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 作业分析 +type AssignmentAnalysisPayload implements Degradable { + analysis: JSON + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 个人资料 +type MyProfilePayload implements Degradable { + profile: JSON + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 掌握度概览 +type MasterySummaryPayload implements Degradable { + summary: JSON + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 诊断报告 +type DiagnosticReportsPayload implements Degradable { + reports: [JSON!]! + totalCount: Int! + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 错题本 +type ErrorBookItem { + id: ID! + questionId: ID! + subjectId: ID! + knowledgePointId: ID! + myAnswer: String + correctAnswer: String! + note: String + tags: [String!] + mastered: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +type ErrorBookPayload implements Degradable { + items: [ErrorBookItem!]! + totalCount: Int! + subjectStats: [JSON!]! + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + +# 练习会话 +type PracticeSession { + id: ID! + studentId: ID! + subjectId: ID! + status: String! + totalQuestions: Int! + answeredCount: Int! + startedAt: DateTime! + completedAt: DateTime +} + +type PracticeSessionsPayload implements Degradable { + sessions: [PracticeSession!]! + totalCount: Int! + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + # ============================================================================ -# 通知 (下游: msg, P5) +# 通知 / 公告 (下游: msg, P5) # ============================================================================ enum NotificationType { @@ -550,6 +820,38 @@ type UnreadCountPayload implements Degradable { degradedFields: [String!] } +# 公告 +type Announcement { + id: ID! + title: String! + content: String! + authorId: ID! + status: String! + category: String + targetAudience: String + publishedAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} + +type AnnouncementConnection { + edges: [AnnouncementEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type AnnouncementEdge { + node: Announcement! + cursor: String! +} + +type AnnouncementDetailPayload implements Degradable { + announcement: Announcement + degraded: Boolean! + degradedReason: String + degradedFields: [String!] +} + # ============================================================================ # AI 答疑 (下游: ai, P5) # ============================================================================ @@ -583,6 +885,13 @@ input AIChatInput { # Mutation 结果 # ============================================================================ +type MutationError { + code: String! # BFF_STUDENT_* 前缀 + message: String! + traceId: String + i18nKey: String +} + type SubmitHomeworkResult { success: Boolean! submissionId: ID @@ -599,15 +908,121 @@ type MarkNotificationReadResult { error: MutationError } -type MutationError { - code: String! # BFF_STUDENT_* 前缀 - message: String! - traceId: String - i18nKey: String +# 通用 Mutation 结果 +type GenericMutationResult { + success: Boolean! + error: MutationError +} + +# 通知相关 Mutation 结果 +type MarkAllAsReadResult { + success: Boolean! + markedCount: Int! + error: MutationError +} + +# 考试相关 Mutation 结果 +type SubmitExamResult { + success: Boolean! + submissionId: ID + examId: ID! + submittedAt: DateTime + score: Float + status: String + error: MutationError +} + +type SaveExamDraftResult { + success: Boolean! + draftId: ID + examId: ID! + savedAt: DateTime + error: MutationError +} + +type RecordExamViolationResult { + success: Boolean! + violationId: ID + examId: ID! + violationType: String! + recordedAt: DateTime + error: MutationError +} + +# 个人资料相关 Mutation 结果 +type UpdateProfileResult { + success: Boolean! + profile: JSON + error: MutationError +} + +# 作业延期申请 +type RequestExtensionResult { + success: Boolean! + requestId: ID + homeworkId: ID! + status: String! + error: MutationError +} + +# 班级相关 Mutation 结果 +type JoinClassResult { + success: Boolean! + classId: ID! + joinedAt: DateTime + error: MutationError +} + +type LeaveClassResult { + success: Boolean! + classId: ID! + leftAt: DateTime + error: MutationError +} + +# 错题相关 Mutation 结果 +type ErrorBookItemResult { + success: Boolean! + itemId: ID + error: MutationError +} + +# 请假相关 Mutation 结果 +type CreateLeaveRequestResult { + success: Boolean! + requestId: ID + status: String! + error: MutationError +} + +# 选课相关 Mutation 结果 +type SelectElectiveResult { + success: Boolean! + selectionId: ID + courseId: ID! + status: String! + error: MutationError +} + +# 练习相关 Mutation 结果 +type SubmitPracticeAnswerResult { + success: Boolean! + questionId: ID! + isCorrect: Boolean + correctAnswer: String + explanation: String + error: MutationError +} + +type StartPracticeSessionResult { + success: Boolean! + session: JSON + questions: [JSON!]! + error: MutationError } # ============================================================================ -# Query 根类型 +# Query 根类型 (36 Queries) # ============================================================================ type Query { @@ -740,10 +1155,131 @@ type Query { # @permission: STUDENT_AI_CHAT # @dataScope: OWN aiChat(input: AIChatInput!): AIChatPayload! + + # ===== 扩展 Query (v2 新增, P3-P5) ===== + + """考试详情 (含题目与学生提交, core-edu.ExamService.GetExam)""" + # @permission: EXAM_READ + # @dataScope: OWN + examDetail(examId: ID!): ExamDetailPayload! + + """作业详情 (core-edu.HomeworkService.GetHomework)""" + # @permission: HOMEWORK_READ + # @dataScope: OWN + homeworkDetail(homeworkId: ID!): HomeworkDetailPayload! + + """服务器时间 (BFF 本地, 无下游)""" + serverTime: ServerTimePayload! + + """我的课表 (周视图, core-edu.ScheduleService.GetScheduleByStudent)""" + # @permission: SCHEDULE_READ + # @dataScope: OWN + mySchedule(weekStart: DateTime): SchedulePayload! + + """学生成长曲线 (data-ana.GetStudentGrowth)""" + # @permission: ANALYTICS_READ + # @dataScope: OWN + studentGrowth( + subjectId: ID + startDate: DateTime + endDate: DateTime + ): StudentGrowthPayload! + + """作业分析 (data-ana.GetAssignmentAnalysis)""" + # @permission: ANALYTICS_READ + # @dataScope: OWN + assignmentAnalysis(homeworkId: ID): AssignmentAnalysisPayload! + + """个人资料 (iam.GetUserProfile)""" + # @permission: AUTH_READ + # @dataScope: OWN + myProfile: MyProfilePayload! + + """掌握度概览 (data-ana.GetMasterySummary)""" + # @permission: ANALYTICS_READ + # @dataScope: OWN + myMasterySummary(subjectId: ID): MasterySummaryPayload! + + """诊断报告列表 (data-ana.ListDiagnosticReports)""" + # @permission: ANALYTICS_READ + # @dataScope: OWN + myDiagnosticReports: DiagnosticReportsPayload! + + """错题本 (data-ana.ListErrorBookItems)""" + # @permission: ANALYTICS_READ + # @dataScope: OWN + myErrorBook( + subjectId: ID + mastered: Boolean + page: Int = 1 + pageSize: Int = 20 + ): ErrorBookPayload! + + """公告列表 (msg.ListAnnouncements)""" + # @permission: ANNOUNCEMENT_READ + # @dataScope: OWN + announcements( + first: Int = 20 + category: String + ): AnnouncementConnection! + + """公告详情 (msg.GetAnnouncement)""" + # @permission: ANNOUNCEMENT_READ + # @dataScope: OWN + announcementDetail(announcementId: ID!): AnnouncementDetailPayload! + + """我的请假记录 (core-edu.ListLeaveRequestsByStudent)""" + # @permission: LEAVE_REQUEST_READ + # @dataScope: OWN + myLeaveRequests: LeaveRequestsPayload! + + """我的选课记录 (content.ListElectiveSelectionsByStudent)""" + # @permission: ELECTIVE_READ + # @dataScope: OWN + myElectiveSelections: ElectiveSelectionsPayload! + + """可选课程 (content.ListAvailableElectiveCourses)""" + # @permission: ELECTIVE_READ + # @dataScope: OWN + availableElectiveCourses: AvailableElectiveCoursesPayload! + + """我的课案 (content.ListLessonPlansByStudent)""" + # @permission: LESSON_PLAN_READ + # @dataScope: OWN + myLessonPlans: LessonPlansPayload! + + """课案详情 (content.GetLessonPlan)""" + # @permission: LESSON_PLAN_READ + # @dataScope: OWN + lessonPlanDetail(lessonPlanId: ID!): LessonPlanDetailPayload! + + """我的课程计划 (content.ListCoursePlansByStudent)""" + # @permission: COURSE_PLAN_READ + # @dataScope: OWN + myCoursePlans: CoursePlansPayload! + + """课程计划详情 (content.GetCoursePlan)""" + # @permission: COURSE_PLAN_READ + # @dataScope: OWN + coursePlanDetail(coursePlanId: ID!): CoursePlanDetailPayload! + + """成绩报告卡 (core-edu.GradeService.GetReportCard)""" + # @permission: GRADE_READ + # @dataScope: OWN + myReportCard( + examId: ID + term: String + academicYear: String + ): ReportCardPayload! + + """我的练习会话列表 (data-ana.ListPracticeSessionsByStudent)""" + # @permission: ANALYTICS_READ + # @dataScope: OWN + myPracticeSessions: PracticeSessionsPayload! } # ============================================================================ -# Mutation 根类型 +# Mutation 根类型 (23 Mutations) # ============================================================================ type Mutation { @@ -760,6 +1296,120 @@ type Mutation { markNotificationAsRead( input: MarkNotificationReadInput! ): MarkNotificationReadResult! + + # ===== 扩展 Mutation (v2 新增, P3-P5) ===== + + """标记通知已读 (别名, msg.MarkAsRead)""" + # @permission: NOTIFICATION_UPDATE + # @dataScope: OWN + markAsRead(input: MarkAsReadInput!): MarkNotificationReadResult! + + """全部通知标记已读 (msg.MarkAllNotificationsAsRead)""" + # @permission: NOTIFICATION_UPDATE + # @dataScope: OWN + markAllAsRead(input: MarkAllAsReadInput!): MarkAllAsReadResult! + + """更新通知偏好 (msg.UpdateNotificationPreferences)""" + # @permission: NOTIFICATION_UPDATE + # @dataScope: OWN + updateNotificationPreference( + input: UpdateNotificationPreferenceInput! + ): GenericMutationResult! + + """提交考试 (含幂等键, core-edu.ExamService.SubmitExam)""" + # @permission: EXAM_SUBMIT + # @dataScope: OWN + submitExam(input: SubmitExamInput!): SubmitExamResult! + + """保存考试草稿 (core-edu.ExamService.SaveExamDraft)""" + # @permission: EXAM_SUBMIT + # @dataScope: OWN + saveExamDraft(input: SaveExamDraftInput!): SaveExamDraftResult! + + """记录考试违规 (防作弊, core-edu.ExamService.RecordExamViolation)""" + # @permission: EXAM_RECORD_VIOLATION + # @dataScope: OWN + recordExamViolation(input: RecordViolationInput!): RecordExamViolationResult! + + """记录粘贴事件 (防作弊, core-edu.ExamService.RecordPasteEvent)""" + # @permission: EXAM_SUBMIT + # @dataScope: OWN + recordPasteEvent(input: RecordPasteEventInput!): RecordExamViolationResult! + + """更新个人资料 (iam.UpdateUserProfile)""" + # @permission: AUTH_UPDATE + # @dataScope: OWN + updateProfile(input: UpdateProfileInput!): UpdateProfileResult! + + """修改密码 (iam.ChangePassword)""" + # @permission: AUTH_UPDATE + # @dataScope: OWN + changePassword(input: ChangePasswordInput!): GenericMutationResult! + + """作业延期申请 (core-edu.RequestHomeworkExtension)""" + # @permission: HOMEWORK_UPDATE + # @dataScope: OWN + requestExtension(input: RequestExtensionInput!): RequestExtensionResult! + + """加入班级 (core-edu.JoinClass)""" + # @permission: CLASS_JOIN + # @dataScope: OWN + joinClass(input: JoinClassInput!): JoinClassResult! + + """退出班级 (core-edu.LeaveClass)""" + # @permission: CLASS_LEAVE + # @dataScope: OWN + leaveClass(input: LeaveClassInput!): LeaveClassResult! + + """添加错题 (data-ana.AddErrorBookItem)""" + # @permission: ANALYTICS_UPDATE + # @dataScope: OWN + addErrorBookItem(input: AddErrorBookItemInput!): ErrorBookItemResult! + + """更新错题 (data-ana.UpdateErrorBookItem)""" + # @permission: ANALYTICS_UPDATE + # @dataScope: OWN + updateErrorBookItem(input: UpdateErrorBookItemInput!): ErrorBookItemResult! + + """删除错题 (data-ana.DeleteErrorBookItem)""" + # @permission: ANALYTICS_UPDATE + # @dataScope: OWN + deleteErrorBookItem(input: DeleteErrorBookItemInput!): ErrorBookItemResult! + + """标记公告已读 (msg.MarkAnnouncementAsRead)""" + # @permission: ANNOUNCEMENT_READ + # @dataScope: OWN + markAnnouncementRead(input: MarkAnnouncementReadInput!): GenericMutationResult! + + """创建请假申请 (core-edu.CreateLeaveRequest)""" + # @permission: LEAVE_REQUEST_CREATE + # @dataScope: OWN + createLeaveRequest(input: CreateLeaveRequestInput!): CreateLeaveRequestResult! + + """取消请假申请 (core-edu.CancelLeaveRequest)""" + # @permission: LEAVE_REQUEST_UPDATE + # @dataScope: OWN + cancelLeaveRequest(input: CancelLeaveRequestInput!): CreateLeaveRequestResult! + + """选择课程 (content.SelectElectiveCourse)""" + # @permission: ELECTIVE_SELECT + # @dataScope: OWN + selectElectiveCourse(input: SelectElectiveInput!): SelectElectiveResult! + + """退选课程 (content.DropElectiveCourse)""" + # @permission: ELECTIVE_DROP + # @dataScope: OWN + dropElectiveCourse(input: DropElectiveInput!): SelectElectiveResult! + + """启动练习会话 (data-ana.StartPracticeSession, 从 Query 移到 Mutation)""" + # @permission: ANALYTICS_UPDATE + # @dataScope: OWN + startPracticeSession(input: StartPracticeSessionInput!): StartPracticeSessionResult! + + """提交练习答案 (data-ana.SubmitPracticeAnswer)""" + # @permission: ANALYTICS_UPDATE + # @dataScope: OWN + submitPracticeAnswer(input: SubmitPracticeAnswerInput!): SubmitPracticeAnswerResult! } # ============================================================================ @@ -783,6 +1433,182 @@ input MarkNotificationReadInput { userId: ID! # 必须与 x-user-id 一致 (B4 越权防御) } +input MarkAsReadInput { + notificationId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +input MarkAllAsReadInput { + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +input UpdateNotificationPreferenceInput { + channel: NotificationChannel! + enabled: Boolean! + categories: [String!] +} + +input SubmitExamInput { + examId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 + answers: [ExamAnswerInput!]! + idempotencyKey: String! +} + +input ExamAnswerInput { + questionId: ID! + content: String! + attachments: [String!] +} + +input SaveExamDraftInput { + examId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 + answers: [ExamAnswerInput!]! + draftId: ID +} + +input RecordViolationInput { + examId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 + violationType: ViolationType! + severity: ViolationSeverity! + details: String + timestamp: DateTime! +} + +enum ViolationType { + TAB_SWITCH + COPY_PASTE + WINDOW_BLUR + FULLSCREEN_EXIT +} + +enum ViolationSeverity { + LOW + MEDIUM + HIGH +} + +input RecordPasteEventInput { + examId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 + questionId: ID! + pastedContent: String! + timestamp: DateTime! +} + +input UpdateProfileInput { + name: String + avatar: String + phone: String + address: String + bio: String + birthday: String + gender: String +} + +input ChangePasswordInput { + currentPassword: String! + newPassword: String! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +input RequestExtensionInput { + homeworkId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 + reason: String! + requestedDays: Int! +} + +input JoinClassInput { + classCode: String! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +input LeaveClassInput { + classId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 + reason: String +} + +input AddErrorBookItemInput { + questionId: ID! + subjectId: ID! + knowledgePointId: ID! + myAnswer: String + correctAnswer: String! + note: String + tags: [String!] +} + +input UpdateErrorBookItemInput { + itemId: ID! + note: String + tags: [String!] + mastered: Boolean +} + +input DeleteErrorBookItemInput { + itemId: ID! +} + +input MarkAnnouncementReadInput { + announcementId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +input CreateLeaveRequestInput { + type: LeaveRequestType! + startDate: DateTime! + endDate: DateTime! + reason: String! + attachments: [String!] + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +enum LeaveRequestType { + SICK + PERSONAL + FAMILY + OTHER +} + +input CancelLeaveRequestInput { + requestId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +input SelectElectiveInput { + courseId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +input DropElectiveInput { + courseId: ID! + studentId: ID # 可选, 若提供必须与 x-user-id 一致 +} + +input StartPracticeSessionInput { + subjectId: ID! + knowledgePointIds: [ID!] + difficulty: PracticeDifficulty +} + +enum PracticeDifficulty { + EASY + MEDIUM + HARD + ADAPTIVE +} + +input SubmitPracticeAnswerInput { + sessionId: ID! + questionId: ID! + answer: String! + timeSpent: Int +} + # ============================================================================ # AI 流式答疑 (SSE Subscription, P5) # ============================================================================ diff --git a/packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql b/packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql index c4e8bd3..b2a39c1 100644 --- a/packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql +++ b/packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql @@ -1,5 +1,5 @@ # teacher-bff GraphQL Schema (SDL-first, president §2.17) -# 版本:v1(P2 第一版,coord 仲裁) +# 版本:v2(ARB-001 扁平命名重构,admin 字段直接挂载根 Query/Mutation) # 负责人:ai03 # 关联:contracts/teacher-bff_contract.md §1.3、workline §3.1 # @@ -8,18 +8,14 @@ # 2. Query 为主,Mutation 谨慎(BFF 偏读多写少) # 3. N+1 防御:所有 list 字段经 DataLoader(P4 全量接入) # 4. 复杂度限制:depth ≤ 7,query cost ≤ 1000 -# 5. admin 命名空间预留(president §5.1:P2 预留 schema,P6 实现 Resolver) +# 5. admin 扁平命名(ARB-001 裁决:admin 字段直接挂载根 Query/Mutation) # # 错误响应格式:GraphQL errors 数组 + extensions.code = BFF_TEACHER_* + extensions.traceId # 降级模式 B(president §2.6):success=true + error=null + data 内 degraded 字段 -# -# 阶段标注: -# P2 = 当前实现(iam gRPC,未启用字段返 null + extensions.warning = "field_unavailable_in_p2") -# P3+ = 跨阶段扩展例外(president §2.3),Resolver 内逐步补 gRPC 调用 scalar DateTime -# ============ Types ============ +# ============ Teacher 域类型 ============ type User { id: ID! @@ -32,10 +28,10 @@ type User { enum DataScope { SELF + SUBJECT CLASS GRADE SCHOOL - DISTRICT ALL } @@ -52,9 +48,7 @@ type Class { id: ID! name: String! gradeId: String! - # 延迟加载:班级下的考试(P3+ core-edu gRPC) exams: [Exam!]! - # 延迟加载:班级下的作业(P3+ core-edu gRPC) homework: [Homework!]! } @@ -64,7 +58,6 @@ type Exam { classId: ID! status: ExamStatus! publishedAt: DateTime - # 延迟加载:考试下的成绩(P3+ core-edu gRPC) grades: [Grade!]! } @@ -82,6 +75,7 @@ type Homework { title: String! classId: ID! dueDate: DateTime! + status: String submissions: [Submission!]! } @@ -113,7 +107,6 @@ type DashboardStats { todayHomework: Int } -# Dashboard 聚合数据(P2: iam gRPC 用户基础信息,stats/classes 为 null + warning) type DashboardData { user: User classes: [Class!]! @@ -121,133 +114,6 @@ type DashboardData { stats: DashboardStats } -# ============ Query ============ -# P2 核心 5 Query(workline §3.2):dashboard / viewports / me / classes / class - -type Query { - # 仪表盘聚合(P2: iam gRPC;P3+: + core-edu + data-ana) - # @dataScope: OWN - dashboard: DashboardData! - - # 视口配置(L1 导航,iam.GetViewports) - viewports: [ViewportItem!]! - - # 当前用户信息(iam.GetUserInfo + GetEffectivePermissions) - me: User! - - # 教师班级列表(P2: iam 数据;P3+: core-edu.GetClassesByTeacher) - # @dataScope: OWN - classes: [Class!]! - - # 单个班级详情(P3+: core-edu) - # @permission: CLASS_READ - # @dataScope: OWN - class(id: ID!): Class - - # ===== P3+ 跨阶段扩展(core-edu gRPC,按 §2.3 跨阶段扩展例外新增)===== - - # 班级下的考试列表(P3: core-edu.ExamService.ListExamsByClass) - # @permission: EXAM_READ - # @dataScope: OWN - exams(classId: ID!): [Exam!]! - - # 班级下的作业列表(P3: core-edu.HomeworkService.ListHomeworkByClass) - # @permission: HOMEWORK_READ - # @dataScope: OWN - homework(classId: ID!): [Homework!]! - - # 考试下的成绩列表(P3: core-edu.GradeService.ListGradesByExam) - # @permission: GRADE_READ - # @dataScope: OWN - grades(examId: ID!): [Grade!]! - - # ===== P4+ 跨阶段扩展(content + data-ana gRPC)===== - - # 知识图谱学习路径(P4: content.KnowledgeGraphService.GetLearningPath) - knowledgePath(knowledgePointId: ID!): KnowledgePath! - - # 班级成绩分析(P4: data-ana.AnalyticsService.GetClassPerformance) - # @permission: ANALYTICS_TEACHER_DASHBOARD - classPerformance(classId: ID!): ClassPerformance! - - # 学生薄弱点(P4: data-ana.AnalyticsService.GetStudentWeakness) - studentWeakness(studentId: ID!): StudentWeakness! - - # 学习趋势(P4: data-ana.AnalyticsService.GetLearningTrend) - learningTrend(studentId: ID!, dateRange: DateRangeInput): LearningTrend! - - # ===== P5+ 跨阶段扩展(ai + msg gRPC)===== - - # 教师通知列表(P5: msg.NotificationService.ListNotifications) - notifications(unreadOnly: Boolean): [Notification!]! - - # ===== admin 命名空间(president §5.1:P2 预留 schema,P6 实现 Resolver)===== - # admin-portal 复用 teacher-bff GraphQL endpoint,admin 操作低 QPS 无需独立 BFF - admin: AdminQuery! -} - -input DateRangeInput { - start: DateTime! - end: DateTime! -} - -# ============ Mutation ============ - -type Mutation { - # ===== P3+ 跨阶段扩展(core-edu gRPC 透传)===== - - # 创建考试(P3: core-edu.ExamService.CreateExam) - # @permission: EXAM_CREATE - createExam(input: CreateExamInput!): Exam! - - # 布置作业(P3: core-edu.HomeworkService.AssignHomework) - # @permission: HOMEWORK_ASSIGN - assignHomework(input: AssignHomeworkInput!): Homework! - - # 录入成绩(P3: core-edu.GradeService.RecordGrade) - # @permission: GRADE_RECORD - recordGrade(input: RecordGradeInput!): Grade! - - # ===== P5+ 跨阶段扩展(ai + msg gRPC)===== - - # AI 出题(P5: ai.AiService.GenerateQuestion) - generateQuestion(input: GenerateQuestionInput!): GeneratedQuestion! - - # 标记通知已读(P5: msg.NotificationService.MarkAsRead) - markNotificationAsRead(id: ID!): Notification! - - # ===== admin 命名空间 Mutation(president §5.1:P2 预留,P6 实现)===== - admin: AdminMutation! -} - -input CreateExamInput { - classId: ID! - title: String! - subject: String! - scheduledAt: DateTime! -} - -input AssignHomeworkInput { - classId: ID! - title: String! - dueDate: DateTime! -} - -input RecordGradeInput { - examId: ID! - studentId: ID! - score: Float! -} - -input GenerateQuestionInput { - subject: String! - gradeLevel: String! - difficulty: String! - knowledgePointIds: [ID!]! -} - -# ============ P4+ 类型(content + data-ana)============ - type KnowledgePath { knowledgePointId: ID! name: String! @@ -283,8 +149,6 @@ type TrendPoint { score: Float! } -# ============ P5+ 类型(ai + msg)============ - type Notification { id: ID! type: NotificationType! @@ -310,78 +174,69 @@ type GeneratedQuestion { answer: String! } -# ============ admin 命名空间(president §5.1:P2 预留 schema,P6 实现 Resolver)============ -# P2: 类型声明占位,Resolver 返 null(不阻塞 admin-portal schema 内省) -# P6: ai03 实现 admin Resolver 真实数据(用户/角色/学校/组织/审计日志管理) - -type AdminQuery { - # 用户管理 - users(page: Int = 1, pageSize: Int = 20): AdminUserPage - user(id: ID!): AdminUser - - # 角色权限管理 - roles: [AdminRole!]! - role(id: ID!): AdminRole - - # 学校设置 - school: AdminSchool - - # 组织管理 - organizations: [AdminOrganization!]! - - # 审计日志查询 - auditLogs(page: Int = 1, pageSize: Int = 20): AuditLogPage -} - -type AdminMutation { - createUser(input: AdminCreateUserInput!): AdminUser! - updateUser(id: ID!, input: AdminUpdateUserInput!): AdminUser! - deleteUser(id: ID!): Boolean! - - createRole(input: AdminCreateRoleInput!): AdminRole! - updateRole(id: ID!, input: AdminUpdateRoleInput!): AdminRole! - deleteRole(id: ID!): Boolean! - - updateSchool(input: AdminUpdateSchoolInput!): AdminSchool! - - createOrganization(input: AdminCreateOrganizationInput!): AdminOrganization! - updateOrganization(id: ID!, input: AdminUpdateOrganizationInput!): AdminOrganization! - deleteOrganization(id: ID!): Boolean! -} +# ============ Admin 域类型(ARB-001 扁平命名)============ type AdminUser { id: ID! email: String! name: String! - roles: [String!]! + roles: [AdminRoleRef!]! status: String! - createdAt: DateTime! + dataScope: String + organizationId: ID + schoolName: String lastLoginAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} + +type AdminRoleRef { + id: ID! + name: String! + code: String! } type AdminUserPage { - edges: [AdminUser!]! - totalCount: Int! + items: [AdminUser!]! + total: Int! page: Int! pageSize: Int! + hasNext: Boolean! } type AdminRole { id: ID! name: String! + code: String! description: String - permissions: [String!]! + permissions: [AdminPermission!]! userCount: Int! + dataScope: String + isSystem: Boolean! + createdAt: DateTime! + updatedAt: DateTime! } -type AdminSchool { +type AdminPermission { id: ID! + code: String! name: String! - address: String - phone: String - studentCount: Int! - teacherCount: Int! - classCount: Int! + description: String + resource: String! + action: String! + isSystem: Boolean! +} + +type AdminViewport { + id: ID! + key: String! + label: String! + route: String! + icon: String + sortOrder: String! + requiredPermission: String + scope: String! + isVisible: Boolean! } type AdminOrganization { @@ -389,64 +244,2122 @@ type AdminOrganization { name: String! type: String! parentId: ID + childrenCount: Int! + sortOrder: Int! +} + +type AdminClass { + id: ID! + name: String! + gradeName: String! + schoolName: String! + headTeacherName: String + studentCount: Int! createdAt: DateTime! } -type AuditLogPage { - edges: [AuditLog!]! - totalCount: Int! +type AdminClassPage { + items: [AdminClass!]! + total: Int! page: Int! pageSize: Int! + hasNext: Boolean! } -type AuditLog { +type AdminTeacher { id: ID! + email: String! + name: String! + schoolName: String + subjects: [String!]! + classCount: Int! + status: String! + lastLoginAt: DateTime +} + +type AdminTeacherPage { + items: [AdminTeacher!]! + total: Int! + page: Int! + pageSize: Int! + hasNext: Boolean! +} + +type AdminStudent { + id: ID! + email: String! + name: String! + className: String + gradeName: String + schoolName: String + guardianName: String + guardianPhone: String + status: String! +} + +type AdminStudentPage { + items: [AdminStudent!]! + total: Int! + page: Int! + pageSize: Int! + hasNext: Boolean! +} + +type AuditLogEntry { + id: ID! + eventId: String! + occurredAt: DateTime! actorUserId: ID! + actorName: String action: String! resourceType: String! resourceId: ID + ip: String + userAgent: String + beforeState: String + afterState: String + traceId: String +} + +type AuditLogPage { + items: [AuditLogEntry!]! + total: Int! + page: Int! + pageSize: Int! + hasNext: Boolean! +} + +type AdminDashboard { + totalTeachers: Int! + totalStudents: Int! + totalClasses: Int! + totalSchools: Int! + schoolAvgScore: Float! + activeUsersToday: Int! + auditEventsToday: Int! + trend: [AdminDashboardTrendPoint!]! + serviceHealth: [ServiceHealthItem!]! +} + +type AdminDashboardTrendPoint { + date: String! + teachers: Int! + students: Int! + avgScore: Float! +} + +type ServiceHealthItem { + serviceName: String! + status: String! + latencyMs: Int! +} + +type SystemSettings { + schoolName: String! + schoolCode: String! + academicYear: String! + semester: String! + contactEmail: String! + contactPhone: String! + address: String! + timezone: String! + locale: String! +} + +type AuditOverviewStats { + totalEvents: Int! + todayEvents: Int! + uniqueActors: Int! + failedActions: Int! + topActions: [AuditActionCount!]! + topResources: [AuditResourceCount!]! +} + +type AuditActionCount { + action: String! + count: Int! +} + +type AuditResourceCount { + resource: String! + count: Int! +} + +type AuditTrendPoint { + date: String! + count: Int! + action: String! +} + +type DataChangeLog { + id: ID! occurredAt: DateTime! + actorUserId: ID! + actorName: String + tableName: String! + action: String! + recordId: ID + beforeState: String + afterState: String ip: String } -input AdminCreateUserInput { - email: String! - name: String! - roleIds: [ID!]! +type DataChangeLogPage { + items: [DataChangeLog!]! + total: Int! + page: Int! + pageSize: Int! + hasNext: Boolean! } -input AdminUpdateUserInput { +type LoginLog { + id: ID! + occurredAt: DateTime! + userId: ID! + userName: String + action: String! + status: String! + ip: String + userAgent: String + failureReason: String +} + +type LoginLogPage { + items: [LoginLog!]! + total: Int! + page: Int! + pageSize: Int! + hasNext: Boolean! +} + +type AdminSchool { + id: ID! + name: String! + code: String! + address: String + contactPhone: String + contactEmail: String + principalName: String + gradeCount: Int! + classCount: Int! + studentCount: Int! + teacherCount: Int! + createdAt: DateTime! +} + +type AdminGrade { + id: ID! + name: String! + schoolId: ID! + schoolName: String! + directorName: String + classCount: Int! + studentCount: Int! + sortOrder: Int! + createdAt: DateTime! +} + +type AdminGradeInsights { + gradeId: ID! + gradeName: String! + schoolWideSummary: SchoolWideSummary! + homeworkTimeline: [HomeworkTimelineItem!]! + classRankings: [ClassRankingItem!]! +} + +type SchoolWideSummary { + totalStudents: Int! + totalClasses: Int! + avgScore: Float! + passingRate: Float! +} + +type HomeworkTimelineItem { + homeworkId: ID! + title: String! + className: String! + avgScore: Float! + submittedCount: Int! + totalCount: Int! + dueDate: DateTime! +} + +type ClassRankingItem { + classId: ID! + className: String! + avgScore: Float! + rank: Int! + delta: Float! +} + +type AdminDepartment { + id: ID! + name: String! + schoolId: ID! + schoolName: String! + leaderName: String + memberCount: Int! + subject: String + createdAt: DateTime! +} + +type AdminAcademicYear { + id: ID! + name: String! + startDate: DateTime! + endDate: DateTime! + firstSemesterStart: DateTime! + firstSemesterEnd: DateTime! + secondSemesterStart: DateTime! + secondSemesterEnd: DateTime! + isCurrent: Boolean! + createdAt: DateTime! +} + +type AdminAnnouncement { + id: ID! + title: String! + content: String! + status: String! + audience: String! + authorName: String + isPinned: Boolean! + readCount: Int! + totalCount: Int! + publishedAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} + +type AdminAnnouncementPage { + items: [AdminAnnouncement!]! + total: Int! + page: Int! + pageSize: Int! + hasNext: Boolean! +} + +type AdminInvitationCode { + id: ID! + code: String! + status: String! + roleId: ID! + roleName: String + classId: ID + className: String + email: String + batchId: String! + usedBy: ID + usedByName: String + usedAt: DateTime + createdAt: DateTime! + expiresAt: DateTime! +} + +type AdminInvitationCodePage { + items: [AdminInvitationCode!]! + total: Int! + page: Int! + pageSize: Int! + hasNext: Boolean! +} + +type AdminFile { + id: ID! + filename: String! + mimeType: String! + size: Int! + url: String! + entityType: String + entityId: ID + uploadedByName: String + createdAt: DateTime! +} + +type AdminFilePage { + items: [AdminFile!]! + total: Int! + page: Int! + pageSize: Int! + hasNext: Boolean! +} + +type AdminFileStats { + totalFiles: Int! + totalSize: Int! + byMimeType: [FileMimeTypeStat!]! + byEntityType: [FileEntityTypeStat!]! +} + +type FileMimeTypeStat { + mimeType: String! + count: Int! + size: Int! +} + +type FileEntityTypeStat { + entityType: String! + count: Int! +} + +type AiProvider { + id: ID! + name: String! + type: String! + model: String! + apiKey: String! + baseUrl: String + isEnabled: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +type AiUsage { + totalRequests: Int! + totalTokens: Int! + totalCost: Float! + byProvider: [AiProviderUsage!]! + byDay: [AiDayUsage!]! +} + +type AiProviderUsage { + providerId: ID! + providerName: String! + requests: Int! + tokens: Int! + cost: Float! +} + +type AiDayUsage { + date: String! + requests: Int! + tokens: Int! + cost: Float! +} + +type GenerateInvitationCodesResult { + codes: [String!]! + batchId: String! +} + +# ============ Query(41 个:teacher 域 13 + admin 域 28)============ + +type Query { + # ===== Teacher 域(13 Query)===== + + dashboard: DashboardData! + viewports: [ViewportItem!]! + me: User! + classes: [Class!]! + class(id: ID!): Class + exams(classId: ID!): [Exam!]! + homework(classId: ID!): [Homework!]! + grades(examId: ID!): [Grade!]! + knowledgePath(knowledgePointId: ID!): KnowledgePath! + classPerformance(classId: ID!): ClassPerformance! + studentWeakness(studentId: ID!): StudentWeakness! + learningTrend(studentId: ID!, dateRange: DateRangeInput): LearningTrend! + notifications(unreadOnly: Boolean): [Notification!]! + + # ===== P3-P5 扩展 Query(teacher-portal 期望)===== + + classStudents(classId: ID!): [Student!]! + examDetail(id: ID!): ExamDetail + homeworkDetail(id: ID!): HomeworkDetail + classExams(classId: ID!): [Exam!]! + classHomework(classId: ID!): [Homework!]! + examGrades(examId: ID!): [Grade!]! + + # ===== P4 扩展 Query ===== + + knowledgeGraph(classId: ID, subject: String): KnowledgeGraph! + classAnalytics(classId: ID!): ClassAnalytics! + studentAnalytics(studentId: ID!): StudentAnalytics! + + # ===== P5 扩展 Query ===== + + myNotifications(unreadOnly: Boolean): [Notification!]! + + # ===== P7 扩展 Query(全部降级返回空/null)===== + + attendanceList(filter: AttendanceListFilterInput): AttendanceListPage! + attendanceSheet(classId: ID!, date: String!): [AttendanceSheetItem!]! + attendanceStats(classId: ID!, startDate: String!, endDate: String!): AttendanceStats! + attendanceClassComparison(startDate: String!, endDate: String!): [AttendanceClassComparisonItem!]! + attendanceWarnings(classId: ID!, startDate: String!, endDate: String!): [AttendanceWarning!]! + attendanceReport(classId: ID!, reportType: AttendanceReportType!, startDate: String!, endDate: String!): AttendanceReport! + classDetail(id: ID!): ClassDetail + classSchedule(classId: ID): [ClassScheduleItem!]! + leaveRequests(filter: LeaveRequestsFilterInput): LeaveRequestPage! + scheduleChanges(page: Int, pageSize: Int): ScheduleChangePage! + electiveCourses(status: String, subject: String): [ElectiveCourse!]! + electiveCourseDetail(id: ID!): ElectiveCourse + examRichEditor(examId: ID!): ExamRichContent + proctoringStatus(examId: ID!): ProctoringStatus! + + # ===== P7-exams 扩展 Query ===== + + examBuild(examId: ID!, candidatePage: Int, candidatePageSize: Int, filter: QuestionsLibraryFilter): ExamBuild! + examAnalytics(examId: ID!): ExamAnalytics! + questionsLibrary(filter: QuestionsLibraryFilter): QuestionsLibraryPage! + questionBatchExport(filter: QuestionsLibraryFilter): QuestionBatchExportResult! + textbooks(filter: TextbooksFilter): TextbooksPage! + textbookDetail(id: ID!): TextbookDetail + + # ===== P7-grades 扩展 Query ===== + + gradeEntry(examId: ID!, classId: ID!): [GradeEntryItem!]! + gradeStats(classId: ID!, subject: String): GradeStats! + gradeAnalytics(classId: ID!, semester: String, examType: String): GradeAnalytics! + reportCard(studentId: ID!, academicYear: String, semester: String): ReportCard! + homeworkSubmissions(classId: ID, status: String): [HomeworkSubmissionSummary!]! + homeworkAssignmentSubmissions(homeworkId: ID!): HomeworkAssignmentSubmissions! + aiBatchGrading(homeworkId: ID!): [AiBatchGradingItem!]! + + # ===== P7-insights 扩展 Query ===== + + lessonPlans(subject: String): [LessonPlan!]! + lessonPlanDetail(planId: ID!): LessonPlanDetail + lessonPlanLibrary(subject: String, grade: String): [LessonPlanLibraryItem!]! + lessonPlanCalendar(year: Int!, month: Int!): LessonPlanCalendar! + lessonPlanHeatmap: LessonPlanHeatmap! + coursePlans(status: CoursePlanStatus): [CoursePlan!]! + coursePlanDetail(id: ID!): CoursePlanDetail + diagnosticReports(type: DiagnosticReportType, status: DiagnosticReportStatus): [DiagnosticReport!]! + classDiagnostic(classId: ID!): ClassDiagnostic! + studentDiagnostic(studentId: ID!): StudentDiagnostic! + errorBook(subject: String!, classId: String): ErrorBook! + practiceAnalytics(classId: String): PracticeAnalytics! + + # ===== Admin 域(28 Query,ARB-001 扁平命名)===== + + currentUser: User! + adminUsers(filter: AdminUserFilterInput): AdminUserPage! + adminUser(id: ID!): AdminUser + adminRoles: [AdminRole!]! + adminPermissions: [AdminPermission!]! + adminViewports(scope: String): [AdminViewport!]! + adminOrganization(parentId: ID): [AdminOrganization!]! + adminClasses(filter: AdminClassFilterInput): AdminClassPage! + adminTeachers(filter: AdminTeacherFilterInput): AdminTeacherPage! + adminStudents(filter: AdminStudentFilterInput): AdminStudentPage! + auditLogs(filter: AuditLogFilterInput): AuditLogPage! + auditOverviewStats: AuditOverviewStats! + auditTrend(days: Int): [AuditTrendPoint!]! + dataChangeLogs(filter: DataChangeLogFilterInput): DataChangeLogPage! + loginLogs(filter: LoginLogFilterInput): LoginLogPage! + adminDashboard: AdminDashboard! + systemSettings: SystemSettings! + adminSchools: [AdminSchool!]! + adminGrades(schoolId: ID): [AdminGrade!]! + adminGradeInsights(gradeId: ID!): AdminGradeInsights! + adminDepartments(schoolId: ID): [AdminDepartment!]! + adminAcademicYears: [AdminAcademicYear!]! + adminAnnouncements(filter: AnnouncementFilterInput): AdminAnnouncementPage! + adminInvitationCodes(filter: InvitationCodeFilterInput): AdminInvitationCodePage! + adminFiles(filter: FileFilterInput): AdminFilePage! + adminFileStats: AdminFileStats! + aiProviders: [AiProvider!]! + aiUsage: AiUsage! +} + +# ============ Mutation(20 个:teacher 域 5 + admin 域 15)============ + +type Mutation { + # ===== Teacher 域(5 Mutation)===== + + createExam(input: CreateExamInput!): Exam! + assignHomework(input: AssignHomeworkInput!): Homework! + recordGrade(input: RecordGradeInput!): Grade! + generateQuestion(input: GenerateQuestionInput!): GeneratedQuestion! + markNotificationAsRead(id: ID!): Notification! + + # ===== P3-P5 扩展 Mutation(teacher-portal 期望)===== + + updateUser(id: ID, input: UpdateUserInput!): User! + markAsRead(id: ID!): Notification! + generateLessonPlan(input: GenerateLessonPlanInput!): GeneratedLessonPlan! + generateReport(input: GenerateReportInput!): GeneratedReport! + + # ===== P7 扩展 Mutation(全部降级返回默认 success)===== + + saveAttendanceSheet(input: SaveAttendanceSheetInput!): SaveAttendanceSheetResult! + approveLeaveRequest(input: ApproveLeaveRequestInput!): ApproveLeaveRequestResult! + submitLeaveRequest(input: SubmitLeaveRequestInput!): SubmitLeaveRequestResult! + submitScheduleChange(input: SubmitScheduleChangeInput!): SubmitScheduleChangeResult! + createElectiveCourse(input: CreateElectiveCourseInput!): ElectiveCourse! + updateElectiveCourse(input: UpdateElectiveCourseInput!): ElectiveCourse! + publishElectiveCourse(id: ID!): ElectiveCourse! + cancelElectiveCourse(id: ID!): ElectiveCourse! + saveExamRichContent(input: SaveExamRichContentInput!): SaveExamRichContentResult! + proctoringEvent(input: ProctoringEventInput!): ProctoringEventResult! + saveScanGrading(input: SaveScanGradingInput!): SaveScanGradingResult! + + # ===== P7-exams 扩展 Mutation ===== + + saveExamBuild(input: SaveExamBuildInput!): SaveExamBuildResult! + questionCreate(input: QuestionCreateInput!): Question! + questionUpdate(input: QuestionUpdateInput!): Question! + questionDelete(questionId: ID!): QuestionDeleteResult! + questionBatchImport(input: QuestionBatchImportInput!): QuestionBatchImportResult! + textbookCreate(input: TextbookCreateInput!): TextbookItem! + + # ===== P7-grades 扩展 Mutation ===== + + saveGradeEntries(input: SaveGradeEntriesInput!): SaveGradeEntriesResult! + gradeSubmission(input: GradeSubmissionInput!): GradeSubmissionResult! + + # ===== P7-insights 扩展 Mutation ===== + + createLessonPlan(input: CreateLessonPlanInput!): LessonPlan! + updateLessonPlan(input: UpdateLessonPlanInput!): LessonPlan! + aiGenerateLessonPlan(input: AIGenerateLessonPlanInput!): AIGenerateLessonPlanResult! + forkLessonPlan(planId: ID!): LessonPlan! + createCoursePlan(input: CreateCoursePlanInput!): CoursePlan! + updateCoursePlan(input: UpdateCoursePlanInput!): CoursePlan! + + # ===== Admin 域(15 Mutation,ARB-001 扁平命名)===== + + createUser(input: CreateUserInput!): AdminUser! + toggleUserStatus(id: ID!, status: String!): AdminUser! + createRole(input: CreateRoleInput!): AdminRole! + updateRolePermissions(roleId: ID!, permissionCodes: [String!]!): AdminRole! + updateViewport(id: ID!, input: UpdateViewportInput!): AdminViewport! + updateSystemSettings(input: SystemSettingsInput!): SystemSettings! + createAnnouncement(input: CreateAnnouncementInput!): AdminAnnouncement! + publishAnnouncement(id: ID!): AdminAnnouncement! + archiveAnnouncement(id: ID!): AdminAnnouncement! + toggleAnnouncementPin(id: ID!): AdminAnnouncement! + generateInvitationCodes(input: GenerateInvitationCodesInput!): GenerateInvitationCodesResult! + deleteInvitationCodes(ids: [ID!]!): Boolean! + deleteFiles(ids: [ID!]!): Boolean! + updateAiProvider(id: ID!, input: UpdateAiProviderInput!): AiProvider! +} + +# ============ Input 类型 ============ + +input DateRangeInput { + start: DateTime! + end: DateTime! +} + +input CreateExamInput { + classId: ID! + title: String! + subject: String! + scheduledAt: DateTime! +} + +input AssignHomeworkInput { + classId: ID! + title: String! + dueDate: DateTime! +} + +input RecordGradeInput { + examId: ID! + studentId: ID! + score: Float! +} + +input GenerateQuestionInput { + subject: String! + gradeLevel: String! + difficulty: String! + knowledgePointIds: [ID!]! +} + +input AdminUserFilterInput { + keyword: String + status: String + roleId: ID + page: Int = 1 + pageSize: Int = 20 +} + +input AdminClassFilterInput { + keyword: String + schoolId: ID + gradeId: ID + page: Int = 1 + pageSize: Int = 20 +} + +input AdminTeacherFilterInput { + keyword: String + schoolId: ID + subject: String + page: Int = 1 + pageSize: Int = 20 +} + +input AdminStudentFilterInput { + keyword: String + schoolId: ID + gradeId: ID + classId: ID + page: Int = 1 + pageSize: Int = 20 +} + +input AuditLogFilterInput { + actorUserId: ID + action: String + resourceType: String + resourceId: ID + startDate: DateTime + endDate: DateTime + page: Int = 1 + pageSize: Int = 20 +} + +input DataChangeLogFilterInput { + actorUserId: ID + tableName: String + action: String + startDate: DateTime + endDate: DateTime + page: Int = 1 + pageSize: Int = 20 +} + +input LoginLogFilterInput { + userId: ID + status: String + startDate: DateTime + endDate: DateTime + page: Int = 1 + pageSize: Int = 20 +} + +input AnnouncementFilterInput { + status: String + audience: String + keyword: String + page: Int = 1 + pageSize: Int = 20 +} + +input InvitationCodeFilterInput { + status: String + roleId: ID + batchId: String + page: Int = 1 + pageSize: Int = 20 +} + +input FileFilterInput { + entityType: String + entityId: ID + mimeType: String + page: Int = 1 + pageSize: Int = 20 +} + +input CreateUserInput { + email: String! + name: String! + password: String! + roleIds: [ID!]! + schoolId: ID +} + +input UpdateUserInput { name: String roleIds: [ID!] status: String + schoolId: ID } -input AdminCreateRoleInput { +input CreateRoleInput { name: String! + code: String! description: String permissions: [String!]! } -input AdminUpdateRoleInput { - name: String - description: String - permissions: [String!] +input UpdateViewportInput { + label: String + icon: String + sortOrder: String + isVisible: Boolean } -input AdminUpdateSchoolInput { - name: String +input SystemSettingsInput { + schoolName: String + schoolCode: String + academicYear: String + semester: String + contactEmail: String + contactPhone: String address: String - phone: String + timezone: String + locale: String } -input AdminCreateOrganizationInput { - name: String! - type: String! - parentId: ID +input CreateAnnouncementInput { + title: String! + content: String! + audience: String! } -input AdminUpdateOrganizationInput { +input GenerateInvitationCodesInput { + count: Int! + roleId: ID! + classId: ID + email: String + expiresInDays: Int +} + +input UpdateAiProviderInput { name: String - type: String - parentId: ID + apiKey: String + baseUrl: String + isEnabled: Boolean +} + +# ============ P3-P5 扩展类型 ============ + +type Student { + id: ID! + name: String! + email: String! + avatarUrl: String +} + +type ExamQuestion { + id: ID! + examId: ID! + title: String! + type: String! + score: Float! + order: Int! +} + +type ExamDetail { + id: ID! + classId: ID! + title: String! + description: String + examDate: DateTime! + duration: Int! + totalScore: Float! + status: ExamStatus! + questions: [ExamQuestion!]! +} + +type HomeworkSubmission { + id: ID! + homeworkId: ID! + studentId: ID! + studentName: String! + status: SubmissionStatus! + score: Float + feedback: String + submittedAt: DateTime +} + +type HomeworkDetail { + id: ID! + classId: ID! + title: String! + description: String + dueDate: DateTime! + status: SubmissionStatus! + submissions: [HomeworkSubmission!]! +} + +type GeneratedLessonPlan { + id: ID! + content: String! + summary: String! + degraded: Boolean + degradedReason: String +} + +type GeneratedReport { + id: ID! + content: String! + summary: String! + recommendations: [String!]! + degraded: Boolean + degradedReason: String +} + +# ============ P4 扩展类型 ============ + +type KnowledgeGraphNode { + id: ID! + name: String! + subject: String! + description: String + masteryLevel: Int! +} + +type KnowledgeGraphEdge { + from: ID! + to: ID! + type: String! +} + +type KnowledgeGraph { + nodes: [KnowledgeGraphNode!]! + edges: [KnowledgeGraphEdge!]! +} + +type ClassAnalytics { + classId: ID! + className: String! + avgScore: Float! + passRate: Float! + avgTrend: [Float!]! + topStudents: [StudentAnalytics!]! + weakPoints: [String!]! +} + +type StudentAnalytics { + studentId: ID! + studentName: String! + avgScore: Float! + trend: [Float!]! + weakPoints: [String!]! + strongPoints: [String!]! + masteryRate: Float! +} + +# ============ P7-admin 扩展类型 ============ + +type AttendanceRecord { + id: ID! + classId: ID! + className: String! + studentId: ID! + studentName: String! + studentNo: String! + date: String! + status: String! + note: String +} + +type AttendanceListPage { + items: [AttendanceRecord!]! + total: Int! + page: Int! + pageSize: Int! + totalPages: Int! +} + +type AttendanceSheetItem { + studentId: ID! + studentName: String! + studentNo: String! + date: String! + status: String + note: String +} + +type AttendanceTrendPoint { + date: String! + presentRate: Float! + lateRate: Float! + absentRate: Float! +} + +type AttendanceStats { + classId: ID! + className: String! + startDate: String! + endDate: String! + totalRecords: Int! + presentCount: Int! + absentCount: Int! + lateCount: Int! + earlyLeaveCount: Int! + leaveCount: Int! + presentRate: Float! + lateRate: Float! + earlyLeaveRate: Float! + leaveRate: Float! + warningCount: Int! + trend: [AttendanceTrendPoint!]! +} + +type AttendanceClassComparisonItem { + classId: ID! + className: String! + presentRate: Float! + lateRate: Float! + absentRate: Float! +} + +type AttendanceWarning { + studentId: ID! + studentName: String! + studentNo: String! + className: String! + presentRate: Float! + absentCount: Int! + lateCount: Int! +} + +enum AttendanceReportType { + WEEKLY + MONTHLY +} + +type AttendanceReportSummary { + totalRecords: Int! + presentRate: Float! + lateRate: Float! + earlyLeaveRate: Float! + leaveRate: Float! + warningCount: Int! +} + +type AttendanceReportDetail { + studentId: ID! + studentName: String! + studentNo: String! + presentCount: Int! + absentCount: Int! + lateCount: Int! + earlyLeaveCount: Int! + leaveCount: Int! + presentRate: Float! +} + +type AttendanceReport { + classId: ID! + className: String! + reportType: AttendanceReportType! + startDate: String! + endDate: String! + generatedAt: String! + summary: AttendanceReportSummary! + details: [AttendanceReportDetail!]! +} + +type ClassDetail { + id: ID! + name: String! + gradeId: String! + subject: String! + studentCount: Int! + homeroomTeacher: String! + room: String + schoolName: String! +} + +type ClassScheduleItem { + id: ID! + classId: ID! + className: String! + dayOfWeek: Int! + period: Int! + subject: String! + teacherName: String! + room: String! + startTime: String! + endTime: String! +} + +type LeaveRequest { + id: ID! + applicantId: ID! + applicantName: String! + applicantRole: String! + type: String! + startDate: String! + endDate: String! + reason: String! + status: String! + submittedAt: String! + reviewedAt: String + reviewerId: ID + reviewerName: String + reviewComment: String +} + +type LeaveRequestPage { + items: [LeaveRequest!]! + total: Int! + page: Int! + pageSize: Int! +} + +type ScheduleChange { + id: ID! + requesterId: ID! + requesterName: String! + classId: ID! + className: String! + originalDate: String! + originalPeriod: Int! + originalSubject: String! + adjustedDate: String! + adjustedPeriod: Int! + adjustedSubject: String! + substituteTeacherId: ID + substituteTeacherName: String + type: String! + reason: String! + status: String! + submittedAt: String! + reviewedAt: String + reviewComment: String +} + +type ScheduleChangePage { + items: [ScheduleChange!]! + total: Int! + page: Int! + pageSize: Int! +} + +type ElectiveStudent { + studentId: ID! + studentName: String! + studentNo: String! + enrolledAt: String! + status: String! +} + +type ElectiveCourse { + id: ID! + title: String! + subject: String! + grade: String! + capacity: Int! + enrolledCount: Int! + teacherId: ID! + teacherName: String! + semester: String! + status: String! + description: String + students: [ElectiveStudent!] + createdAt: String + updatedAt: String +} + +# ============ P7-advanced 扩展类型 ============ + +type ExamRichContent { + examId: ID! + title: String! + totalScore: Float! + questionCount: Int! + content: String! + updatedAt: String! +} + +type ProctoringSummary { + examId: ID! + expectedCount: Int! + onlineCount: Int! + submittedCount: Int! + flaggedCount: Int! + isProctoring: Boolean! + startedAt: String +} + +type ProctoringStudent { + studentId: ID! + studentName: String! + studentNo: String! + status: String! + lastEventAt: String + ipAddress: String +} + +type ProctoringEventItem { + eventId: ID! + examId: ID! + studentId: ID! + studentName: String! + studentNo: String! + eventType: String! + note: String + createdAt: String! +} + +type ProctoringStatus { + summary: ProctoringSummary! + students: [ProctoringStudent!]! + recentEvents: [ProctoringEventItem!]! +} + +type ProctoringEventResult { + eventId: ID! + createdAt: String! +} + +type SaveScanGradingResult { + submissionId: ID! + totalScore: Float! + savedAt: String! +} + +# ============ P7-exams 扩展类型 ============ + +type ExamBuildNode { + questionId: ID! + score: Float! + sortOrder: Int! + content: String! + type: String! + difficulty: String! +} + +type ExamBuild { + examId: ID! + title: String! + totalScore: Float! + passScore: Float! + duration: Int! + selected: [ExamBuildNode!]! + candidates: QuestionsLibraryPage! +} + +type SaveExamBuildResult { + examId: ID! + totalScore: Float! + questionCount: Int! + savedAt: String! +} + +type ExamAnalyticsSummary { + expectedCount: Int! + attendedCount: Int! + avgScore: Float! + maxScore: Float! + minScore: Float! + passRate: Float! +} + +type ExamScoreBand { + label: String! + min: Float! + max: Float! + count: Int! +} + +type ExamQuestionAccuracy { + questionId: ID! + questionTitle: String! + order: Int! + correctRate: Float! + avgScore: Float! + maxScore: Float! +} + +type ExamKpMastery { + knowledgePoint: String! + mastery: Float! +} + +type ExamClassComparison { + classId: ID! + className: String! + avg: Float! + significant: Boolean! +} + +type ExamHistoryTrend { + examTitle: String! + examDate: String! + avg: Float! +} + +type ExamStudentRank { + rank: Int! + studentId: ID! + studentName: String! + studentNo: String! + totalScore: Float! + level: String! +} + +type ExamAnalytics { + examId: ID! + examTitle: String! + summary: ExamAnalyticsSummary! + distribution: [ExamScoreBand!]! + questionAccuracy: [ExamQuestionAccuracy!]! + knowledgeMastery: [ExamKpMastery!]! + classComparison: [ExamClassComparison!]! + historyTrend: [ExamHistoryTrend!]! + rankings: [ExamStudentRank!]! +} + +type Question { + questionId: ID! + content: String! + type: String! + difficulty: String! + textbookId: ID + textbookName: String + chapterId: ID + chapterName: String + kpId: ID + kpName: String + score: Float! + answer: String! + analysis: String! + createdAt: String! + updatedAt: String! +} + +type QuestionsLibraryPage { + items: [Question!]! + total: Int! + page: Int! + pageSize: Int! +} + +type QuestionBatchExportResult { + items: [Question!]! + exportedAt: String! +} + +type QuestionDeleteResult { + questionId: ID! + deleted: Boolean! +} + +type QuestionBatchImportResult { + importedCount: Int! + failedCount: Int! + errors: [String!]! +} + +type TextbookItem { + id: ID! + title: String! + subject: String! + grade: String! + version: String! + coverUrl: String + chapterCount: Int! + createdAt: String! +} + +type TextbooksPage { + items: [TextbookItem!]! + total: Int! + page: Int! + pageSize: Int! +} + +type TextbookKp { + id: ID! + name: String! + description: String +} + +type TextbookChapter { + id: ID! + textbookId: ID! + title: String! + order: Int! + content: String! + parentId: ID + knowledgePoints: [TextbookKp!]! +} + +type TextbookDetail { + id: ID! + title: String! + subject: String! + grade: String! + version: String! + coverUrl: String + chapters: [TextbookChapter!]! +} + +# ============ P7-grades 扩展类型 ============ + +type GradeEntryItem { + studentId: ID! + studentName: String! + studentNo: String! + score: Float + feedback: String +} + +type GradeRanking { + rank: Int! + studentId: ID! + studentName: String! + studentNo: String! + totalScore: Float! + level: String! +} + +type GradeStats { + classId: ID! + className: String! + subject: String! + avg: Float! + median: Float! + max: Float! + min: Float! + passRate: Float! + stdDev: Float! + totalCount: Int! + rankings: [GradeRanking!]! +} + +type GradeTrendPoint { + month: String! + avg: Float! + max: Float! + min: Float! +} + +type GradeDistributionBand { + label: String! + min: Float! + max: Float! + count: Int! +} + +type SubjectComparisonItem { + subject: String! + avg: Float! +} + +type ClassComparisonItem { + classId: ID! + className: String! + avg: Float! + significant: Boolean! +} + +type KnowledgeMasteryItem { + knowledgePoint: String! + mastery: Float! +} + +type GradeAnalytics { + classId: ID! + className: String! + trend: [GradeTrendPoint!]! + distribution: [GradeDistributionBand!]! + subjectComparison: [SubjectComparisonItem!]! + classComparison: [ClassComparisonItem!]! + knowledgeMastery: [KnowledgeMasteryItem!]! +} + +type ReportCardSubject { + subject: String! + score: Float! + rank: Int! + teacherName: String! +} + +type ReportCard { + studentId: ID! + studentName: String! + studentNo: String! + className: String! + academicYear: String! + semester: String! + subjects: [ReportCardSubject!]! + totalScore: Float! + classRank: Int! + classSize: Int! + gradeRank: Int! + gradeSize: Int! + teacherComment: String! +} + +type HomeworkSubmissionSummary { + homeworkId: ID! + title: String! + classId: ID! + className: String! + status: String! + dueDate: String! + totalCount: Int! + submittedCount: Int! + gradedCount: Int! + avgScore: Float +} + +type SubmissionAnswer { + questionId: ID! + questionTitle: String! + questionType: String! + maxScore: Float! + studentAnswer: String! + correctAnswer: String! + score: Float + feedback: String + aiSuggestion: String +} + +type HomeworkSubmissionDetail { + submissionId: ID! + homeworkId: ID! + homeworkTitle: String! + classId: ID! + className: String! + studentId: ID! + studentName: String! + studentNo: String! + status: String! + submittedAt: String! + totalScore: Float + answers: [SubmissionAnswer!]! + prevSubmissionId: ID + nextSubmissionId: ID +} + +type HomeworkAssignmentSubmissions { + submissionId: ID! + homeworkId: ID! + homeworkTitle: String! + classId: ID! + className: String! + studentId: ID! + studentName: String! + studentNo: String! + status: String! + submittedAt: String! + totalScore: Float + answers: [SubmissionAnswer!]! + prevSubmissionId: ID + nextSubmissionId: ID +} + +type AiBatchGradingItem { + submissionId: ID! + suggestedScore: Float! + suggestion: String! +} + +type SaveGradeEntriesResult { + examId: ID! + classId: ID! + savedCount: Int! +} + +type GradeSubmissionResult { + submissionId: ID! + status: String! + totalScore: Float! + feedback: String +} + +# ============ P7-insights 扩展类型 ============ + +type LessonPlanAIHistoryItem { + id: ID! + prompt: String! + generatedAt: String! + model: String! +} + +type LessonPlan { + id: ID! + title: String! + subject: String! + grade: String! + chapter: String! + status: String! + textbookId: ID + textbookTitle: String + classIds: [String!]! + updatedAt: String! +} + +type LessonPlanDetail { + id: ID! + title: String! + subject: String! + grade: String! + chapter: String! + content: String! + status: String! + textbookId: ID + textbookTitle: String + classIds: [String!]! + aiHistory: [LessonPlanAIHistoryItem!]! + updatedAt: String! + createdAt: String! +} + +type LessonPlanLibraryItem { + id: ID! + title: String! + author: String! + subject: String! + grade: String! + chapter: String! + rating: Float! + forkCount: Int! + updatedAt: String! +} + +type LessonPlanCalendarDay { + date: String! + planCount: Int! + planTitles: [String!]! +} + +type LessonPlanCalendar { + year: Int! + month: Int! + days: [LessonPlanCalendarDay!]! +} + +type LessonPlanHeatmapCell { + textbookKp: String! + planIds: [String!]! + covered: Boolean! +} + +type LessonPlanHeatmap { + rows: [String!]! + cols: [String!]! + cells: [LessonPlanHeatmapCell!]! +} + +enum CoursePlanStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +type CoursePlanChapter { + id: ID! + title: String! + hours: Int! + objectives: String! + keyPoints: String! + activities: String! +} + +type CoursePlan { + id: ID! + title: String! + subject: String! + grade: String! + academicYear: String! + semester: String! + totalHours: Int! + status: CoursePlanStatus! + textbookId: ID + updatedAt: String! +} + +type CoursePlanDetail { + id: ID! + title: String! + subject: String! + grade: String! + academicYear: String! + semester: String! + totalHours: Int! + status: CoursePlanStatus! + textbookId: ID + textbookTitle: String + homeworkCount: Int! + chapters: [CoursePlanChapter!]! + updatedAt: String! +} + +enum DiagnosticReportType { + INDIVIDUAL + CLASS + GRADE +} + +enum DiagnosticReportStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +type DiagnosticReport { + id: ID! + title: String! + type: DiagnosticReportType! + targetType: String! + status: DiagnosticReportStatus! + createdAt: String! +} + +type DiagnosticKpDistribution { + mastered: Int! + partial: Int! + weak: Int! +} + +type DiagnosticKpMastery { + knowledgePoint: String! + avg: Float! + median: Float! + studentCount: Int! + distribution: DiagnosticKpDistribution! +} + +type DiagnosticStudentRank { + rank: Int! + studentId: ID! + studentName: String! + masteryAvg: Float! +} + +type ClassDiagnostic { + classId: ID! + className: String! + studentCount: Int! + summary: [DiagnosticSummaryItem!]! + kpMastery: [DiagnosticKpMastery!]! + rankings: [DiagnosticStudentRank!]! +} + +type DiagnosticSummaryItem { + knowledgePoint: String! + avg: Float! +} + +type DiagnosticRadarPoint { + axis: String! + value: Float! + classAvg: Float! +} + +type StudentDiagnosticReport { + id: ID! + title: String! + createdAt: String! + status: DiagnosticReportStatus! +} + +type StudentDiagnostic { + studentId: ID! + studentName: String! + radar: [DiagnosticRadarPoint!]! + reports: [StudentDiagnosticReport!]! +} + +type ErrorBookSummary { + totalErrors: Int! + highFreqErrors: Int! + weakKpCount: Int! + avgErrorRate: Float! + trendUp: Boolean! +} + +type ErrorBookClassComparison { + classId: ID! + className: String! + errorCount: Int! +} + +type ErrorBookChapterWeakness { + chapter: String! + errorCount: Int! +} + +type ErrorBookKpWeakness { + knowledgePoint: String! + errorRate: Float! +} + +type ErrorBookTopError { + questionId: ID! + content: String! + errorRate: Float! +} + +type ErrorBookStudentGroup { + studentId: ID! + studentName: String! + errorCount: Int! + topErrors: [ErrorBookTopError!]! +} + +type ErrorBookTopQuestion { + questionId: ID! + content: String! + errorRate: Float! + subject: String! +} + +type ErrorBook { + subject: String! + classId: String! + summary: ErrorBookSummary! + classComparison: [ErrorBookClassComparison!]! + chapterWeakness: [ErrorBookChapterWeakness!]! + kpWeakness: [ErrorBookKpWeakness!]! + studentGroups: [ErrorBookStudentGroup!]! + topQuestions: [ErrorBookTopQuestion!]! +} + +type PracticeSummary { + totalSessions: Int! + completionRate: Float! + avgAccuracy: Float! + weakKpCount: Int! + participationRate: Float! +} + +type PracticeClassComparison { + classId: ID! + className: String! + totalSessions: Int! + completionRate: Float! + avgAccuracy: Float! + participationRate: Float! + weakKpCount: Int! +} + +type PracticeTypeBreakdown { + type: String! + count: Int! + ratio: Float! +} + +type PracticeKpWeakness { + knowledgePoint: String! + accuracy: Float! +} + +type PracticeStudentRank { + rank: Int! + studentId: ID! + studentName: String! + studentNo: String! + totalSessions: Int! + accuracy: Float! + weakKp: String! +} + +type PracticeInactiveStudent { + studentId: ID! + studentName: String! + lastActiveDays: Int! +} + +type PracticeAnalytics { + classId: String! + summary: PracticeSummary! + classComparison: [PracticeClassComparison!]! + typeBreakdown: [PracticeTypeBreakdown!]! + kpWeakness: [PracticeKpWeakness!]! + rankings: [PracticeStudentRank!]! + inactiveStudents: [PracticeInactiveStudent!]! +} + +type SaveAttendanceSheetResult { + classId: ID! + date: String! + savedCount: Int! + savedAt: String! +} + +type SaveExamRichContentResult { + examId: ID! + updatedAt: String! +} + +type ApproveLeaveRequestResult { + requestId: ID! + status: String! + reviewedAt: String! + reviewerId: ID! + reviewerName: String! + reviewComment: String +} + +type SubmitLeaveRequestResult { + requestId: ID! + status: String! + submittedAt: String! +} + +type SubmitScheduleChangeResult { + requestId: ID! + status: String! + submittedAt: String! +} + +type AIGenerateLessonPlanResult { + id: ID! + content: String! + summary: String! + model: String! + generatedAt: String! +} + +# ============ P7 扩展 Input 类型 ============ + +input GenerateLessonPlanInput { + classId: String! + subject: String! + topic: String! + objectives: String! +} + +input GenerateReportInput { + classId: String! + reportType: String! + studentId: String +} + +input AttendanceListFilterInput { + classId: ID + startDate: String + endDate: String + statuses: [String!] + page: Int + pageSize: Int +} + +input SaveAttendanceRecordInput { + studentId: ID! + status: String! + note: String +} + +input SaveAttendanceSheetInput { + classId: ID! + date: String! + records: [SaveAttendanceRecordInput!]! +} + +input LeaveRequestsFilterInput { + dataScope: String + status: String + page: Int + pageSize: Int +} + +input ApproveLeaveRequestInput { + requestId: ID! + action: String! + comment: String +} + +input SubmitLeaveRequestInput { + type: String! + startDate: String! + endDate: String! + reason: String! +} + +input SubmitScheduleChangeInput { + classId: ID! + originalDate: String! + originalPeriod: Int! + originalSubject: String! + adjustedDate: String! + adjustedPeriod: Int! + adjustedSubject: String! + substituteTeacherId: ID + type: String! + reason: String! +} + +input CreateElectiveCourseInput { + title: String! + subject: String! + grade: String! + capacity: Int! + semester: String! + description: String +} + +input UpdateElectiveCourseInput { + id: ID! + title: String + subject: String + grade: String + capacity: Int + semester: String + description: String +} + +input SaveExamRichContentInput { + examId: ID! + content: String! +} + +input ProctoringEventInput { + examId: ID! + studentId: ID! + eventType: String! + note: String +} + +input SaveScanGradingItem { + questionId: ID! + score: Float! + feedback: String +} + +input SaveScanGradingInput { + submissionId: ID! + items: [SaveScanGradingItem!]! +} + +input QuestionsLibraryFilter { + q: String + type: String + difficulty: String + kp: String + textbook: String + chapter: String + page: Int + pageSize: Int +} + +input QuestionCreateInput { + content: String! + type: String! + difficulty: String! + textbookId: ID + chapterId: ID + kpId: ID + score: Float! + answer: String! + analysis: String +} + +input QuestionUpdateInput { + questionId: ID! + content: String + type: String + difficulty: String + textbookId: ID + chapterId: ID + kpId: ID + score: Float + answer: String + analysis: String +} + +input QuestionBatchImportItem { + content: String! + type: String! + difficulty: String! + textbookId: ID + chapterId: ID + kpId: ID + score: Float! + answer: String! + analysis: String +} + +input QuestionBatchImportInput { + items: [QuestionBatchImportItem!]! +} + +input TextbooksFilter { + q: String + subject: String + grade: String + page: Int + pageSize: Int +} + +input TextbookCreateInput { + title: String! + subject: String! + grade: String! + version: String! + coverUrl: String +} + +input SaveExamBuildNodeInput { + questionId: ID! + score: Float! + sortOrder: Int! +} + +input SaveExamBuildInput { + examId: ID! + questions: [SaveExamBuildNodeInput!]! +} + +input SaveGradeEntryInput { + studentId: ID! + score: Float! + feedback: String +} + +input SaveGradeEntriesInput { + examId: ID! + classId: ID! + entries: [SaveGradeEntryInput!]! +} + +input GradeSubmissionQuestionScore { + questionId: ID! + score: Float! + feedback: String +} + +input GradeSubmissionInput { + submissionId: ID! + score: Float! + feedback: String + aiAssisted: Boolean + questionScores: [GradeSubmissionQuestionScore!] +} + +input CreateLessonPlanInput { + title: String! + subject: String! + grade: String! + chapter: String! + textbookId: ID + template: String! +} + +input UpdateLessonPlanInput { + planId: ID! + title: String + content: String + status: String + textbookId: ID + chapter: String + classIds: [String!] +} + +input AIGenerateLessonPlanInput { + subject: String! + grade: String! + chapter: String! + prompt: String! +} + +input CreateCoursePlanInput { + title: String! + subject: String! + grade: String! + academicYear: String! + semester: String! + totalHours: Int! + textbookId: ID +} + +input UpdateCoursePlanInput { + id: ID! + title: String + status: CoursePlanStatus + chapters: [CoursePlanChapterInput!] +} + +input CoursePlanChapterInput { + id: ID + title: String! + hours: Int! + objectives: String! + keyPoints: String! + activities: String! } diff --git a/packages/shared-ts/src/bff/downstream-client.ts b/packages/shared-ts/src/bff/downstream-client.ts index c18a265..32dc6b2 100644 --- a/packages/shared-ts/src/bff/downstream-client.ts +++ b/packages/shared-ts/src/bff/downstream-client.ts @@ -348,7 +348,7 @@ export class DownstreamClient { resolveWait = resolve; }); if (result.done) break; - yield result.value; + yield (result as { done: false; value: TResponse }).value; } if (streamError) { diff --git a/packages/shared-ts/src/bff/logger.ts b/packages/shared-ts/src/bff/logger.ts index 2320177..4a22911 100644 --- a/packages/shared-ts/src/bff/logger.ts +++ b/packages/shared-ts/src/bff/logger.ts @@ -9,7 +9,14 @@ * * 该 logger 也作为 DownstreamClient 默认 logger, 避免循环依赖. */ -import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino"; +import * as pinoNs from "pino"; +import { type Logger as PinoLogger, type LoggerOptions } from "pino"; + +// pino 在 NodeNext + ESM 模式下 default import 丢失 callable 签名 (declaration merging 失效) +// 通过 namespace import + 显式类型断言恢复可调用性 +type PinoFn = (options?: LoggerOptions | unknown) => PinoLogger; +const pino = ((pinoNs as unknown as { default: PinoFn }).default ?? + (pinoNs as unknown as PinoFn)) as PinoFn; /** * 默认日志级别 (可通过环境变量 LOG_LEVEL 覆盖). diff --git a/packages/shared-ts/src/outbox/outbox.module.ts b/packages/shared-ts/src/outbox/outbox.module.ts index 36f8b6a..d3ca866 100644 --- a/packages/shared-ts/src/outbox/outbox.module.ts +++ b/packages/shared-ts/src/outbox/outbox.module.ts @@ -1,5 +1,12 @@ import { Module, type DynamicModule } from "@nestjs/common"; -import pino from "pino"; +import * as pinoNs from "pino"; +import type { Logger as PinoLogger, LoggerOptions } from "pino"; + +// pino 在 NodeNext + ESM 模式下 default import 丢失 callable 签名 (declaration merging 失效) +// 通过 namespace import + 显式类型断言恢复可调用性 +type PinoFn = (options?: LoggerOptions | unknown) => PinoLogger; +const pino = ((pinoNs as unknown as { default: PinoFn }).default ?? + (pinoNs as unknown as PinoFn)) as PinoFn; import { OutboxService } from "./outbox.service.js"; import { OutboxPublisher } from "./publisher.js"; import { createOutboxTable } from "./schema.js";