# student-bff GraphQL Schema (v1) # # 负责人: 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 # - 分页采用 Relay Cursor Connections 规范 ({ edges, pageInfo, totalCount }) # - 错误响应: GraphQL errors 数组 + extensions.code + extensions.traceId # - 权限点标注: # @permission: _ # - DataScope 标注: # @dataScope: OWN (学生数据隔离 SELF) # - Type 用 PascalCase, 字段用 camelCase, 枚举用 UPPER_SNAKE_CASE # - 必填字段用 !, 可空字段不标 ! (避免破坏性变更) # # 降级模式 (president §2.6 方案 B): # - 下游不可用时 success=true + error=null + data 内 degraded=true # - 降级字段返回 null, 父对象加 degraded/degradedReason/degradedFields scalar DateTime scalar JSON # ============================================================================ # Relay Cursor Connections 规范 # ============================================================================ interface Node { id: ID! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } # ============================================================================ # 错误扩展 (GraphQL errors 数组 + extensions) # ============================================================================ """ GraphQL errors 数组中 extensions 字段规范 (president §2.2.3 + G8 裁决). 由 GlobalErrorFilter 注入, 不在 schema 中显式暴露. """ type GraphQLErrorExtension { code: String! # BFF_STUDENT_* 前缀 (B5 裁决) traceId: String! # 全链路追踪 ID (Gateway 注入 X-Request-Id) i18nKey: String! # i18n key: error.bffStudent. (F4 裁决) severity: String! # error / warning / info } # ============================================================================ # 通用类型 # ============================================================================ """ 降级标记 (president §2.6 方案 B). 当下游服务不可用但需返回部分数据时, 父对象包含此接口字段. """ interface Degradable { degraded: Boolean! degradedReason: String degradedFields: [String!] } type DegradationInfo { degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 用户与权限 (下游: iam) # ============================================================================ type UserProfile { id: ID! email: String! name: String! avatar: String roles: [String!]! permissions: [String!]! } type ViewportItem { key: String! label: String! route: String! icon: String sortOrder: Int! requiredPermission: String } type ViewportConfig { navigation: [ViewportItem!]! dataScope: StudentDataScope! } type StudentDataScope { showHistoryGrades: Boolean! showClassRanking: Boolean! enableAIChat: Boolean! } type CurrentUserPayload implements Degradable { user: UserProfile viewports: ViewportConfig effectivePermissions: [String!]! dataScope: String # SELF (学生固定) degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 班级 (下游: core-edu ClassService) # ============================================================================ type StudentClass { id: ID! name: String! gradeId: String! gradeName: String subjects: [String!]! homeroomTeacher: TeacherBrief } type TeacherBrief { id: ID! name: String! avatar: String } type StudentClassConnection { edges: [StudentClassEdge!]! pageInfo: PageInfo! totalCount: Int! } type StudentClassEdge { node: StudentClass! cursor: String! } type StudentClassPayload implements Degradable { classes: [StudentClass!]! degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 考试 (下游: core-edu ExamService) # ============================================================================ enum ExamStatus { DRAFT PUBLISHED IN_PROGRESS COMPLETED CANCELLED ARCHIVED } type Exam { id: ID! classId: ID! title: String! description: String examDate: DateTime! duration: Int! # 考试时长(分钟) totalScore: Float! status: ExamStatus! createdBy: ID! createdAt: DateTime! updatedAt: DateTime! daysLeft: Int # 距考试天数(负数表示已过) } type ExamConnection { edges: [ExamEdge!]! pageInfo: PageInfo! totalCount: Int! } type ExamEdge { node: Exam! cursor: String! } type ExamListPayload implements Degradable { exams: [Exam!]! degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 作业 (下游: core-edu HomeworkService) # ============================================================================ enum HomeworkStatus { ASSIGNED SUBMITTED GRADED OVERDUE RETURNED } type Homework { id: ID! classId: ID! title: String! description: String dueDate: DateTime! status: HomeworkStatus! createdBy: ID! createdAt: DateTime! updatedAt: DateTime! submission: HomeworkSubmission # 当前学生的提交 } type HomeworkSubmission { id: ID! homeworkId: ID! studentId: ID! status: HomeworkStatus! submittedAt: DateTime gradedAt: DateTime score: Float feedback: String } type HomeworkConnection { edges: [HomeworkEdge!]! pageInfo: PageInfo! totalCount: Int! } type HomeworkEdge { node: Homework! cursor: String! } type HomeworkListPayload implements Degradable { homework: [Homework!]! degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 成绩 (下游: core-edu GradeService) # ============================================================================ type Grade { id: ID! studentId: ID! examId: ID homeworkId: ID examTitle: String homeworkTitle: String subject: String score: Float! totalScore: Float! feedback: String gradedBy: ID! gradedAt: DateTime! createdAt: DateTime! updatedAt: DateTime! } type GradeConnection { edges: [GradeEdge!]! pageInfo: PageInfo! totalCount: Int! } type GradeEdge { node: Grade! cursor: String! } type GradeListPayload implements Degradable { grades: [Grade!]! averageScore: Float totalCount: Int! degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 考勤 (下游: core-edu AttendanceService, P3 预留) # ============================================================================ enum AttendanceStatus { PRESENT ABSENT LATE EARLY_LEAVE EXCUSED } type AttendanceRecord { id: ID! studentId: ID! classId: ID! date: DateTime! status: AttendanceStatus! remark: String recordedBy: ID! createdAt: DateTime! } type AttendanceConnection { edges: [AttendanceEdge!]! pageInfo: PageInfo! totalCount: Int! } type AttendanceEdge { node: AttendanceRecord! cursor: String! } type AttendanceListPayload implements Degradable { records: [AttendanceRecord!]! presentCount: Int! absentCount: Int! lateCount: Int! totalCount: Int! degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 教材与章节 (下游: content, P4) # ============================================================================ type Textbook { id: ID! title: String! subjectId: ID! subjectName: String gradeId: ID! gradeName: String version: String! coverImage: String chapters: [Chapter!]! } type Chapter { id: ID! textbookId: ID! title: String! description: String sortOrder: Int! parentId: ID # 父章节(支持章节树) knowledgePoints: [KnowledgePoint!]! } type KnowledgePoint { id: ID! title: String! description: String mastery: Float # 0-1 掌握度 } type TextbookConnection { edges: [TextbookEdge!]! pageInfo: PageInfo! totalCount: Int! } type TextbookEdge { node: Textbook! cursor: String! } type TextbookListPayload implements Degradable { textbooks: [Textbook!]! degraded: Boolean! degradedReason: String degradedFields: [String!] } type ChapterListPayload implements Degradable { chapters: [Chapter!]! degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 学习路径 (下游: content KnowledgeGraphService, P4) # ============================================================================ type LearningPath { studentId: ID! subjectId: ID! points: [KnowledgePoint!]! recommendedOrder: [String!]! # 知识点 id 顺序 estimatedHours: Float } type LearningPathPayload implements Degradable { path: LearningPath degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 学情分析 (下游: data-ana, P4) # ============================================================================ type StudentDashboard { studentId: ID! averageScore: Float classRank: Int totalStudents: Int pendingHomeworkCount: Int! upcomingExamCount: Int! unreadNotificationCount: Int! lastGrade: Grade weakness: WeakPointSummary # P4 data-ana 启用后填充 trend: LearningTrendSummary # P4 data-ana 启用后填充 } type WeakPointSummary { knowledgePointId: ID! title: String! mastery: Float! subject: String } type StudentWeakness { studentId: ID! subjectId: ID weakPoints: [WeakPoint!]! } type WeakPoint { knowledgePointId: ID! title: String! mastery: Float! subject: String lastAssessedAt: DateTime } type LearningTrendSummary { studentId: ID! subjectId: ID points: [TrendPoint!]! } type LearningTrend { studentId: ID! subjectId: ID points: [TrendPoint!]! } type TrendPoint { date: DateTime! score: Float! subject: String } type StudentDashboardPayload implements Degradable { dashboard: StudentDashboard degraded: Boolean! degradedReason: String degradedFields: [String!] } type WeaknessPayload implements Degradable { weakness: StudentWeakness degraded: Boolean! degradedReason: String degradedFields: [String!] } type TrendPayload implements Degradable { trend: LearningTrend degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # 通知 (下游: msg, P5) # ============================================================================ enum NotificationType { HOMEWORK_ASSIGNED HOMEWORK_GRADED EXAM_PUBLISHED EXAM_UPDATED GRADE_RECORDED SYSTEM ANNOUNCEMENT } enum NotificationChannel { IN_APP EMAIL SMS PUSH } type Notification { id: ID! userId: ID! type: NotificationType! title: String! content: String! channel: NotificationChannel! isRead: Boolean! createdAt: DateTime! readAt: DateTime metadata: JSON } type NotificationConnection { edges: [NotificationEdge!]! pageInfo: PageInfo! totalCount: Int! } type NotificationEdge { node: Notification! cursor: String! } type NotificationListPayload implements Degradable { notifications: [Notification!]! degraded: Boolean! degradedReason: String degradedFields: [String!] } type UnreadCountPayload implements Degradable { count: Int! degraded: Boolean! degradedReason: String degradedFields: [String!] } # ============================================================================ # AI 答疑 (下游: ai, P5) # ============================================================================ type AIChatResponse { content: String! model: String! usage: AIUsage! } type AIUsage { promptTokens: Int! completionTokens: Int! totalTokens: Int! } type AIChatPayload implements Degradable { response: AIChatResponse degraded: Boolean! degradedReason: String degradedFields: [String!] } input AIChatInput { messages: [AIChatMessageInput!]! model: String = "gpt-4o-mini" context: AIChatContextInput } # ============================================================================ # Mutation 结果 # ============================================================================ type SubmitHomeworkResult { success: Boolean! submissionId: ID homeworkId: ID! submittedAt: DateTime status: HomeworkStatus error: MutationError } type MarkNotificationReadResult { success: Boolean! notificationId: ID! readAt: DateTime error: MutationError } type MutationError { code: String! # BFF_STUDENT_* 前缀 message: String! traceId: String i18nKey: String } # ============================================================================ # Query 根类型 # ============================================================================ type Query { """当前学生信息 + 权限 + 视口 (聚合 iam.GetUserInfo + GetEffectivePermissions + GetViewports)""" # @permission: AUTH_READ # @dataScope: OWN currentUser: CurrentUserPayload! """我的班级列表 (core-edu.ClassService.GetClass + ListStudentsByClass)""" # @permission: CLASS_READ # @dataScope: OWN myClasses( after: String first: Int = 20 before: String last: Int ): StudentClassConnection! """我的考试列表 (core-edu.ExamService.ListExamsByClass)""" # @permission: EXAM_READ # @dataScope: OWN myExams( after: String first: Int = 20 before: String last: Int status: ExamStatus ): ExamConnection! """我的作业列表 (core-edu.HomeworkService.ListHomeworkByClass)""" # @permission: HOMEWORK_READ # @dataScope: OWN myHomework( after: String first: Int = 20 before: String last: Int status: HomeworkStatus ): HomeworkConnection! """我的成绩列表 (core-edu.GradeService.ListGradesByStudent, B4 强制 userId 比对)""" # @permission: GRADE_READ # @dataScope: OWN myGrades( after: String first: Int = 20 before: String last: Int subject: String startDate: DateTime endDate: DateTime ): GradeConnection! """我的考勤记录 (core-edu.AttendanceService.ListAttendanceByStudent)""" # @permission: ATTENDANCE_READ # @dataScope: OWN myAttendance( after: String first: Int = 20 before: String last: Int startDate: DateTime endDate: DateTime ): AttendanceConnection! """教材列表 (content.TextbookService.ListTextbooks)""" # @permission: TEXTBOOK_READ # @dataScope: OWN textbooks( after: String first: Int = 20 before: String last: Int subjectId: ID gradeId: ID ): TextbookConnection! """章节列表 (content.ChapterService.ListChapters)""" # @permission: CHAPTER_READ # @dataScope: OWN chapters( after: String first: Int = 20 before: String last: Int textbookId: ID! ): ChapterListPayload! """学习路径推荐 (content.KnowledgeGraphService.GetLearningPath)""" # @permission: LEARNING_PATH_READ # @dataScope: OWN learningPath(subjectId: ID!): LearningPathPayload! """学生仪表盘 (data-ana.AnalyticsService.GetStudentDashboard)""" # @permission: DASHBOARD_VIEW # @dataScope: OWN studentDashboard: StudentDashboardPayload! """我的薄弱点 (data-ana.AnalyticsService.GetStudentWeakness)""" # @permission: WEAKNESS_READ # @dataScope: OWN myWeakness(subjectId: ID): WeaknessPayload! """学习趋势 (data-ana.AnalyticsService.GetLearningTrend)""" # @permission: TREND_READ # @dataScope: OWN myTrend( subjectId: ID startDate: DateTime endDate: DateTime ): TrendPayload! """我的通知列表 (msg.NotificationService.ListNotifications)""" # @permission: NOTIFICATION_READ # @dataScope: OWN myNotifications( after: String first: Int = 20 before: String last: Int onlyUnread: Boolean = false ): NotificationConnection! """通知未读数 (msg.NotificationService.GetUnreadCount)""" # @permission: NOTIFICATION_READ # @dataScope: OWN myNotificationUnreadCount: UnreadCountPayload! """AI 答疑 (同步, ai.Chat). 实际为写操作但 schema 设计为 Query 便于前端 GET 缓存""" # @permission: STUDENT_AI_CHAT # @dataScope: OWN aiChat(input: AIChatInput!): AIChatPayload! } # ============================================================================ # Mutation 根类型 # ============================================================================ type Mutation { """提交作业 (core-edu.HomeworkService.SubmitHomework, B4 强制 userId 比对)""" # @permission: HOMEWORK_SUBMIT # @dataScope: OWN submitHomework( input: SubmitHomeworkInput! ): SubmitHomeworkResult! """标记通知已读 (msg.NotificationService.MarkAsRead)""" # @permission: NOTIFICATION_UPDATE # @dataScope: OWN markNotificationAsRead( input: MarkNotificationReadInput! ): MarkNotificationReadResult! } # ============================================================================ # Input 类型 # ============================================================================ input SubmitHomeworkInput { homeworkId: ID! studentId: ID! # 必须与 x-user-id 一致 (B4 越权防御) answers: [HomeworkAnswerInput!]! } input HomeworkAnswerInput { questionId: ID! content: String! # 最大 5000 字符 attachments: [String!] # URL 列表, 最多 5 个 } input MarkNotificationReadInput { notificationId: ID! userId: ID! # 必须与 x-user-id 一致 (B4 越权防御) } # ============================================================================ # AI 流式答疑 (SSE Subscription, P5) # ============================================================================ type AIStreamChunk { content: String! # 本次分块内容 done: Boolean! # 是否结束 model: String usage: AIUsage # done=true 时填充 } input AIStreamChatInput { messages: [AIChatMessageInput!]! model: String = "gpt-4o-mini" context: AIChatContextInput } input AIChatMessageInput { role: String! # user / assistant content: String! } input AIChatContextInput { subject: String knowledgePointId: ID } # ============================================================================ # Subscription 根类型 (SSE 传输, GraphQL Yoga 原生支持) # ============================================================================ type Subscription { """AI 答疑流式响应 (ai.StreamChat, SSE 传输)""" # @permission: STUDENT_AI_CHAT # @dataScope: OWN aiStreamChat(input: AIStreamChatInput!): AIStreamChunk! }