Files
Edu/packages/shared-ts/contracts/graphql/student-bff.schema.graphql
SpecialX f585080e70 feat(student-bff): 完整实现 student-bff 聚合层
包含 src 全部实现、Dockerfile、shared-ts/bff 包等
2026-07-10 19:10:51 +08:00

806 lines
18 KiB
GraphQL

# 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: <RESOURCE>_<ACTION>
# - 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.<code_snake> (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 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!]
}
# ============================================================================
# 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!
}
# ============================================================================
# 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!
}