Merge worktree branch merge-15-modules-to-main-5ug5xJ

This commit is contained in:
SpecialX
2026-07-10 15:28:20 +08:00
parent 60d7173545
commit df62ffc176
51 changed files with 11559 additions and 1908 deletions

View File

@@ -1,49 +1,71 @@
# admin-portal 对接契约
> 负责人ai16
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 关联:[matrix.md](./matrix.md)、[coord.md](./coord.md)、[ai-allocation.md §5 ai16](../../ai-allocation.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 说明:本契约基于 GraphQLARB-001 admin 命名空间)+ 端口 4003 + ai16 归属,作为 01/02 文档修订基准(见 [objections/admin-portal_issue.md](../objections/admin-portal_issue.md) ISSUE-001~006
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 gRPC 接口
无。admin-portal 是前端微前端 Remote。
无。admin-portal 是前端微前端 Remote,不提供 gRPC
### 1.2 HTTP 端点(如有
### 1.2 前端路由MF Remote 暴露给 Shell 动态加载
| Method | Path | 用途 | 认证 |
| ------ | ----------- | ---------------- | --------------------- |
| GET | / | 管理后台首页 | JWT 必需 + admin 角色 |
| GET | /users | 用户管理 | JWT 必需 + admin |
| GET | /roles | 角色权限管理 | JWT 必需 + admin |
| GET | /classes | 班级管理(全局) | JWT 必需 + admin |
| GET | /teachers | 教师管理 | JWT 必需 + admin |
| GET | /students | 学生管理 | JWT 必需 + admin |
| GET | /audit-logs | 审计日志 | JWT 必需 + admin |
| GET | /dashboard | 管理员仪表盘 | JWT 必需 + admin |
| GET | /system | 系统配置 | JWT 必需 + admin |
> admin-portal 是前端,**不对外提供 HTTP 端点**。此处列出的是 admin-portal 在 Shell 路由树 `/admin/*` 下注册的前端页面路由,由 Shell 动态 import admin Remote 加载。
### 1.3 GraphQL schema如 BFF
| 路由 | 页面 | 权限点 | 数据范围 |
| ------------------- | ---------------- | ---------------------- | --------------- |
| `/admin/dashboard` | 管理员仪表盘 | `ADMIN_DASHBOARD_VIEW` | L3-L5 |
| `/admin/users` | 用户管理 | `IAM_USER_READ` | L3-L5 |
| `/admin/roles` | 角色权限管理 | `IAM_ROLE_READ` | L3-L5 |
| `/admin/permissions`| 权限点管理 | `IAM_PERMISSION_READ` | L5 |
| `/admin/viewports` | 视口配置 | `IAM_VIEWPORT_READ` | L5 |
| `/admin/organization` | 组织管理 | `ORG_MANAGE` | L3-L5 |
| `/admin/system` | 学校设置 | `ADMIN_SYSTEM_MANAGE` | L5 |
| `/admin/classes` | 班级管理(全局) | `ADMIN_CLASS_READ` | L3-L5 |
| `/admin/teachers` | 教师管理 | `ADMIN_TEACHER_READ` | L3-L5 |
| `/admin/students` | 学生管理 | `ADMIN_STUDENT_READ` | L3-L5 |
| `/admin/audit-logs` | 审计日志 | `ADMIN_AUDIT_READ` | L5 |
不适用。admin-portal 消费 teacher-bff GraphQL admin namespace自身不提供 schema
> 权限点使用 `ADMIN_` / `IAM_` / `ORG_` 前缀ai-allocation §5admin 权限点 `ADMIN_` 前缀)。`ADMIN_*` 常量由 coord 维护于 `packages/contracts/src/permissions.ts`,前端不硬编码
### 1.4 Kafka 事件发布(如有)
### 1.3 GraphQL schema
不适用。admin-portal **消费** teacher-bff GraphQL admin 命名空间,自身不提供 schema
### 1.4 Kafka 事件发布
无。前端不发布 Kafka 事件。
### 1.5 错误码前缀
无(前端不定义错误码前缀,透传 BFF 错误码)。
无(前端不定义错误码前缀,透传 BFF/网关错误码)。消费的错误码前缀见 §2.5。
### 1.6 微前端架构(补充)
### 1.6 微前端架构
| 角色 | 说明 |
| ---------------------- | ----------------------------------------------- |
| MF Remote | 管理后台是微前端远程模块 |
| 暴露的 remote 模块 | AdminApp管理后台完整应用、shared 管理端组件 |
| module federation 配置 | `apps/admin-portal/module-federation.config.ts` |
| 角色 | 说明 |
| ---- | ---- |
| MF Remote | admin-portal 是微前端远程模块,挂载到 teacher-portal Shell |
| Remote 名称 | `admin_app`Shell `remotes` 中引用为 `admin: 'admin_app@http://localhost:4003/_next/static/chunks/remoteEntry.js'` |
| 暴露模块 | `./AdminApp`(管理后台应用入口,供 Shell 动态 import |
| MF 配置位置 | `apps/admin-portal/next.config.js`NextFederationPlugin对齐 ARB-002 §2.2 配置风格) |
| sharedsingleton | react / react-dom / urql / graphql / @edu/ui-tokens / @edu/ui-components / @edu/hooks / @edu/contracts |
| 复用 Shell 暴露 | `GraphQLProvider` / `AppShell` / `useAuth` / `usePermission` / `useGraphQLClient` / `ErrorBoundary` / `RequirePermission`ARB-002 §2.2 |
> admin-portal **不暴露共享组件给 Shell**(共享组件由 Shell 统一暴露,对齐 ARB-002。admin 特有组件UserManagementTable / RolePermissionMatrix / ViewportConfigEditor仅 admin-portal 内部使用。
### 1.7 i18n 命名空间
| 命名空间 | 用途 |
| -------- | ---- |
| `admin.*` | 管理端 UI 文案(导航/按钮/表单标签等) |
| `iam.error.*` | iam 服务错误码 i18n透传 |
| `bff.error.*` | teacher-bff 错误码 i18n透传 |
| `gateway.error.*` | api-gateway 错误码 i18n透传 |
| `network.error.*` | 前端网络层错误 i18n |
---
@@ -55,28 +77,49 @@
### 2.2 Kafka 事件订阅(异步)
无。前端不直接订阅 Kafka审计日志通过 GraphQL 查询,非直接订阅)。
无。前端不直接订阅 Kafka审计日志经 teacher-bff 聚合后通过 GraphQL `auditLogs` Query 消费链路iam → Kafka `edu.iam.audit.created`**teacher-bff 消费** → GraphQL → admin-portal)。
### 2.3 HTTP 调用(如有)
> 注:[matrix.md §4](../matrix.md) 将 admin-portal 列为 `edu.iam.audit.created` 消费方,表述不精确,已提请 coord 修正为 teacher-bff见 [objections ISSUE-007](../objections/admin-portal_issue.md))。
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------- | ----------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------- |
| api-gateway (ai01) | POST /api/admin/graphql | 管理 GraphQL 查询(经网关代理到 teacher-bff admin namespace | api-gateway/teacher-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| api-gateway (ai01) | POST /api/auth/login | 管理员登录 | api-gateway 就绪前使用 MSW 返回固定 JWTadmin 角色) |
| push-gateway (ai02) | GET /ws | WebSocket 实时通知 | push-gateway 就绪前使用 mock-socket 模拟 WS 推送 |
### 2.3 HTTP 调用
### 2.4 GraphQL 查询域(经 api-gateway 代理到 teacher-bff admin namespace
| 被调用方 | Method.Path | 用途 | mock 策略 |
| -------- | ----------- | ---- | --------- |
| api-gateway (ai01) | POST /api/admin/graphql | 管理 GraphQL 查询(经网关代理到 teacher-bff admin 命名空间) | api-gateway/teacher-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| push-gateway (ai02) | GET /ws | WebSocket 实时通知(审计告警/异常登录/系统异常) | push-gateway 就绪前使用 mock-socket 模拟 WS 推送(**ISSUE-006 待 coord 仲裁** |
| Query/Mutation | 用途 | mock 策略 |
| ------------------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------- |
| currentUser | 当前管理员信息 | MSW 返回固定管理员admin 角色) |
| adminUsers / createUser / updateUser / deleteUser | 用户管理 | MSW 返回固定 50 个用户 + CRUD success |
| adminRoles / updateRolePermissions | 角色权限管理 | MSW 返回固定 5 个角色 + 权限矩阵 |
| adminClasses | 班级管理(全局) | MSW 返回固定 20 个班级 |
| adminTeachers | 教师管理 | MSW 返回固定 50 个教师 |
| adminStudents | 学生管理 | MSW 返回固定 1200 个学生 |
| auditLogs | 审计日志(聚合 iam AuditEvent | MSW 返回固定 100 条审计日志 |
| adminDashboard | 管理员仪表盘 | MSW 返回固定仪表盘total_teachers=50, total_students=1200, school_avg_score=80.0 |
> **登录不自行实现**admin-portal 复用 Shell 统一登录入口 `/login`ARB-002 §2.3:登录页 P2 不走 MFShell 独占)。管理员登录后按 admin 角色重定向到 `/admin/dashboard`。开发期 mock 登录由 Shell 的 MSW handler 提供admin 角色 JWT + permissions=["*"])。
### 2.4 GraphQL 查询域(经 api-gateway 代理到 teacher-bff admin 命名空间)
> 依赖 teacher-bff admin 命名空间 schemaARB-001**ISSUE-005 待 ai03 补齐**)。以下为 admin-portal 消费的 Query/Mutation 清单,作为 ai03 补齐 schema 的输入。
| Query/Mutation | 类型 | 用途 | mock 策略 |
| -------------- | ---- | ---- | --------- |
| `currentUser` | Query | 当前管理员信息 | MSW 返回固定管理员admin 角色) |
| `adminUsers` | Query | 用户列表(含筛选/分页) | MSW 返回固定 50 个用户 |
| `adminUser(id)` | Query | 用户详情 | MSW 返回对应用户 |
| `createUser` / `updateUser` / `deleteUser` / `toggleUserStatus` | Mutation | 用户 CRUD | MSW 返回 CRUD success |
| `adminRoles` | Query | 角色列表(含权限) | MSW 返回固定 5 个角色 + 权限矩阵 |
| `createRole` / `updateRolePermissions` | Mutation | 角色 CRUD + 权限矩阵 | MSW 返回 success |
| `adminPermissions` | Query | 全量权限点(按 resource 分组) | MSW 返回固定权限矩阵 |
| `adminViewports(scope)` | Query | 视口配置列表 | MSW 返回固定 7 个视口 |
| `updateViewport` | Mutation | 视口配置更新 | MSW 返回 success |
| `adminOrganization(parentId)` | Query | 组织树 | MSW 返回固定 school/grade/class 树 |
| `adminClasses` | Query | 班级管理(全局) | MSW 返回固定 20 个班级 |
| `adminTeachers` | Query | 教师管理 | MSW 返回固定 50 个教师 |
| `adminStudents` | Query | 学生管理 | MSW 返回固定 1200 个学生 |
| `auditLogs(filter)` | Query | 审计日志(聚合 iam AuditEvent | MSW 返回固定 100 条审计日志 |
| `adminDashboard` | Query | 管理员仪表盘聚合 | MSW 返回固定仪表盘total_teachers=50, total_students=1200, school_avg_score=80.0 |
### 2.5 消费的错误码前缀(前端 i18n 路由)
| 前缀 | 来源服务 | i18n key 模式 |
| ---- | -------- | ------------- |
| `IAM_` | iam | `iam.error.{{code}}` |
| `BFF_TEACHER_` | teacher-bff | `bff.error.{{code}}` |
| `GW_` | api-gateway | `gateway.error.{{code}}` |
| `NETWORK_` | 前端网络层 | `network.error.{{code}}` |
---
@@ -85,18 +128,21 @@
### 3.1 我依赖的上游就绪标志
- [ ] api-gateway HTTP :8080 启用ai01—— 前端请求入口 + admin 角色校验
- [ ] teacher-bff GraphQL :3003 启用ai03—— admin namespace 可用
- [ ] teacher-portal Shell MF exposes/shared 就绪ai13ARB-002—— Remote 挂载前提
- [ ] teacher-bff GraphQL :3003 启用 + **admin 命名空间 schema 就绪**ai03—— **ISSUE-005 待 ai03 补齐**
- [ ] iam gRPC 50052 启用ai06—— 用户/角色/审计日志数据来源
- [ ] edu.iam.audit.created topic 有事件发布ai06—— 审计日志来源
- [ ] data-ana gRPC 50055 启用ai11)—— adminDashboard 数据来源
- [ ] push-gateway WebSocket :8081/ws 启用ai02—— 实时通知
- [ ] `edu.iam.audit.created` topic 有事件发布ai06—— 审计日志来源(经 teacher-bff 消费)
- [ ] push-gateway WebSocket :8081/ws 启用ai02)—— 实时通知(**ISSUE-006 待 coord 仲裁**
- [ ] `packages/contracts` admin 权限点 `ADMIN_*` 常量就绪coord
> adminDashboard 的数据聚合由 teacher-bff 完成teacher-bff 内部聚合 iam + core-edu + data-ana。admin-portal **不直连 data-ana / core-edu gRPC**,统一经 teacher-bff GraphQL。原 contract 误列 data-ana gRPC 50055 为直接依赖,已修正。
### 3.2 我的就绪标志(供下游消费)
- [ ] admin-portal dev server :4003 启用
- [ ] MF Remote 可被 AppShell 加载(暴露 AdminApp 模块)
- [ ] MF Remote 可被 AppShell 加载(暴露 `./AdminApp` 模块)
- [ ] 独立壳渲染(首页 + 导航 + 路由守卫 + admin 角色校验)
- [ ] 登录流程可用(POST /api/auth/login 获取 JWT前端校验 admin 角色
- [ ] 登录流程可用(复用 Shell `/login`admin 角色校验后重定向 `/admin/dashboard`
- [ ] GraphQL 查询可执行currentUser / adminDashboard / auditLogs 返回数据)
- [ ] 用户/角色 CRUD 可执行createUser / updateRolePermissions
- [ ] WebSocket 通知可接收
@@ -117,13 +163,25 @@ admin-portal 是前端,无下游消费方。但对开发体验提供:
在真实上游就绪前admin-portal 使用以下 mock
- **HTTP/GraphQL mock**:使用 MSW 拦截所有请求
- POST /api/auth/login → 返回固定 JWT + UserInfoadmin 角色permissions=["*"]
- POST /api/admin/graphql → 根据 operationName 返回对应 mock 响应(与 teacher-bff admin namespace mock 数据一致)
- POST /api/admin/graphql → 按 operationName 返回对应 mock 响应(与 teacher-bff admin namespace mock 数据一致
- auditLogs mock 返回固定 100 条审计日志(含 action: create/update/delete/login/logout/permission_change
- adminDashboard mock 返回固定全校统计仪表盘
- 所有 mock 响应定义在 `apps/admin-portal/src/mocks/fixtures/*.json`
- **WebSocket mock**:使用 mock-socket 库
- 连接后每 30 秒推送 1 条 mock 系统通知
- **JWT mock**:使用固定 mock JWTadmin 角色),存入 httpOnly cookie
- 连接后每 30 秒推送 1 条 mock 系统通知(审计告警/异常登录)
- **JWT mock**:使用固定 mock JWTadmin 角色permissions=["*"]),由 Shell MSW handler 写入 httpOnly cookie复用 Shell 登录 mock
- **权限矩阵 mock**:内置固定 5 个角色 + 完整权限矩阵teacher/student/parent/admin/super_admin
- **环境切换**:通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制,上游就绪后设为 `disabled`
---
## §5 跨模块契约确认清单(需对应 AI 确认)
| 契约 | 提供方 | 当前状态 |
| ---- | ------ | -------- |
| teacher-bff admin 命名空间 schema§2.4 全部 Query/Mutation | ai03 | ⚠️ 待补齐ISSUE-005 |
| Shell 暴露 GraphQLProvider / useGraphQLClient / AppShell / useAuth / usePermission | ai13 | ⏳ P2 交付ARB-002 |
| iam 用户/角色/权限/视口 CRUD gRPC + AuditEvent Kafka | ai06 | ⏳ P2.1 |
| api-gateway /api/admin/graphql 代理路由 + admin 角色校验 | ai01 | ⏳ |
| push-gateway GET /wsadmin-portal 实时通知) | ai02 | ⚠️ 待 coord 仲裁ISSUE-006 |
| `packages/contracts` ADMIN_* 权限点常量 | coord | ⏳ |

View File

@@ -1,42 +1,99 @@
# ai 对接契约
> 负责人ai12
> 关联:[matrix.md](./matrix.md)、[ai.proto](../../../packages/shared-proto/proto/ai.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 关联:[matrix.md](../matrix.md)、[port-allocation.md](../../../../infra/port-allocation.md)、[ai.proto](../../../../packages/shared-proto/proto/ai.proto)、[events.proto](../../../../packages/shared-proto/proto/events.proto)、[02-architecture-design.md](../../../../services/ai/docs/02-architecture-design.md)、[objections/ai_issue.md](../objections/ai_issue.md)
> 端口权威源:[port-allocation.md](../../../../infra/port-allocation.md) §3/§5 —— ai = HTTP 3008 / gRPC 50058
> **本契约已对齐 02-architecture-design.md 设计文档**。原 coord 模板的 5 处矛盾已修正(见 [objections/ai_issue.md](../objections/ai_issue.md) ISSUE-05端口 50057→50058、补 HTTP 端点、topic 三义待裁决、错误码对齐 §6.2、消费事件改 P6+ 评估。标注 ⏳ 的字段待 coord 裁决 ISSUE-02/03/04 后最终定稿。
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 gRPC 接口
| Service | RPC | 请求 | 响应 | 端口 |
| --------- | ---------------------- | ----------------------------- | ------------------------ | ----- |
| AiService | Chat | ChatRequest | ChatResponse | 50057 |
| AiService | StreamChat | ChatRequest | stream ChatChunk | 50057 |
| AiService | GenerateQuestion | GenerateQuestionRequest | GeneratedQuestion | 50057 |
| AiService | OptimizeExpression | OptimizeExpressionRequest | OptimizedExpression | 50057 |
| AiService | GenerateLessonPlan | GenerateLessonPlanRequest | LessonPlan | 50057 |
| AiService | StreamGenerateQuestion | StreamGenerateQuestionRequest | stream GeneratedQuestion | 50057 |
| Service | RPC | 请求 | 响应 | 端口 | 状态 |
| --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------- | ----- | ---- |
| AiService | Chat | `ChatRequest{messages, model, temperature, user_id?, session_id?, data_scope?}` | `ChatResponse{content, model, usage}` | 50058 | ⏳ 待实现 |
| AiService | StreamChat | `ChatRequest` | `stream ChatChunk` | 50058 | ⏳ 待实现 |
| AiService | GenerateQuestion | `GenerateQuestionRequest{prompt, subject, difficulty, grade?, knowledge_point_ids?, question_type?, count?}` | `GeneratedQuestion` | 50058 | ⏳ 待实现 |
| AiService | StreamGenerateQuestion | `GenerateQuestionRequest` | `stream GeneratedQuestionChunk` | 50058 | ⏳ 待补 protoISSUE-03 |
| AiService | OptimizeExpression | `OptimizeExpressionRequest{text, context}` | `OptimizedExpression` | 50058 | ⏳ 待实现 |
| AiService | GenerateLessonPlan | `GenerateLessonPlanRequest{class_id, subject_id, topic, user_id, data_scope}` | `LessonPlanResponse{workflow_id, status, questions?}` | 50058 | ⏳ 待补 protoISSUE-03 |
### 1.2 HTTP 端点(如有)
> **RPC 总数**P5 目标 6 RPCai12 建议,见 ISSUE-03。备课工作流的"查询状态/确认入库"用 HTTP 端点实现,避免 RPC 膨胀;如 coord 裁定需 gRPC 则扩到 8 RPC追加 GetLessonPlanStatus / ConfirmLessonPlan
> **proto 现状**ai.proto 仅 4 RPCChat/StreamChat/GenerateQuestion/OptimizeExpression缺 GenerateLessonPlan / StreamGenerateQuestion且字段未扩展。待 coord 升级 ai.proto 到 v1 完整版ISSUE-03
> **proto package 偏离**:现状 `next_edu_cloud.ai.v1`,不符合 project_rules §5 `edu.<domain>.v1`,见 ISSUE-08。
无对外 HTTP 端点,仅 gRPC含 2 个 Server Streaming RPCStreamChat / StreamGenerateQuestion
### 1.2 HTTP 端点
> HTTP 保留作 api-gateway 直连降级 + SSE 流式。api-gateway 代理 `/api/v1/ai/*` → ai `/ai/v1/*`(见 [main.py:70](../../../../services/ai/src/ai/main.py) 注释 + matrix.md §5
| Method | Path | 权限 | 响应 | 说明 | 状态 |
| ------ | ------------------------------------------------- | ------------------------ | -------------------------------------- | --------------------------------- | ---- |
| GET | `/healthz` | — | `{status, service}` | liveness | ✅ 已实现 |
| GET | `/readyz` | — | `{status, llm_configured, providers, downstream_grpc, redis, kafka}` | readiness多维度检查 | ⚠️ 待扩展 |
| GET | `/metrics` | — | Prometheus | 指标 | ✅ 已实现 |
| POST | `/ai/v1/chat` | `AI_CHAT` | `ActionState<ChatData>` | LLM 聊天 | ⚠️ 当前 `/ai/chat`,待加 /v1 + ActionState |
| POST | `/ai/v1/chat/stream` | `AI_CHAT` | SSE stream | 流式聊天 | ⚠️ 同上 |
| POST | `/ai/v1/generate/question` | `AI_QUESTION_GENERATE` | `ActionState<GeneratedQuestionData>` | 生成题目 | ⚠️ 同上 |
| POST | `/ai/v1/generate/question/stream` | `AI_QUESTION_GENERATE` | SSE stream题目逐字生成 | 题目逐字流式 | ⏳ 待实现 |
| POST | `/ai/v1/optimize/expression` | `AI_EXPRESSION_OPTIMIZE` | `ActionState<OptimizedExpressionData>` | 优化表达 | ⚠️ 同上 |
| POST | `/ai/v1/lesson/preparation` | `AI_LESSON_PREPARE` | `ActionState<LessonPreparationData>` | 备课工作流启动 | ⏳ 待实现 |
| GET | `/ai/v1/lesson/preparation/{workflow_id}` | `AI_LESSON_PREPARE` | `ActionState<WorkflowState>` | 查询工作流状态 | ⏳ 待实现 |
| POST | `/ai/v1/lesson/preparation/{workflow_id}/confirm` | `AI_LESSON_PREPARE` | `ActionState<PersistResult>` | 教师确认入库 | ⏳ 待实现 |
| GET | `/ai/v1/prompts` | `AI_PROMPT_READ` | `ActionState<Page<TemplateSummary>>` | 模板列表 | ⏳ 待实现 |
| POST | `/ai/v1/prompts` | `AI_PROMPT_CREATE` | `ActionState<PromptTemplate>` | 创建模板 | ⏳ 待实现 |
| GET | `/ai/v1/prompts/{id}` | `AI_PROMPT_READ` | `ActionState<PromptTemplate>` | 获取模板 | ⏳ 待实现 |
| PUT | `/ai/v1/prompts/{id}` | `AI_PROMPT_UPDATE` | `ActionState<PromptTemplate>` | 更新模板(版本化) | ⏳ 待实现 |
| GET | `/ai/v1/usage/me` | `AI_USAGE_READ` | `ActionState<UsageSummary>` | 当前用户用量 | ⏳ 待实现 |
| GET | `/ai/v1/usage/school/{school_id}` | `AI_USAGE_READ_ALL` | `ActionState<UsageSummary>` | 学校用量(管理员) | ⏳ 待实现 |
> **响应信封**:所有响应必须为 ActionState004 §11.5 强制,见 ISSUE-09。当前 main.py 返回 `{success, data, degraded}` 顶层 degraded 字段违反约束P5 必须整改。
> **路径演进**:当前实现是 `/ai/*`(无 /v1目标态 `/ai/v1/*`(加版本前缀,便于未来破坏性变更)。
### 1.3 GraphQL schema如 BFF
不适用。
不适用。ai 是业务服务,不暴露 GraphQL由 teacher-bff 聚合 ai gRPC 能力为 GraphQL。
### 1.4 Kafka 事件发布(如有)
### 1.4 Kafka 事件发布
| Topic | Event | 消费方 |
| ------------------- | ---------------------------------------------------------------------------------------- | -------- |
| edu.ai.usage.events | AIUsageEventoperation: chat/generate_question/optimize_expression/lesson_preparation | data-ana |
| Topic | Event | 触发时机 | 消费方 | Payload |
| ----------------- | ---------------------- | ----------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `edu.ai.usage`(建议,待 ISSUE-02 裁决) | `AIUsageEvent`(待 ISSUE-04 补 proto | 每次 LLM 调用完成 | data-ana | `{event_id, aggregate_id, event_type, occurred_at, user_id, school_id, request_id, provider, model, operation, prompt_tokens, completion_tokens, total_tokens, latency_ms, success, degraded, metadata}` |
> AIUsageEvent 豁免 Outbox 模式(派生数据事件,见 004 §12.2 + §15.3 #6)。
> **topic 命名三义**01/02 文档写 `edu.insight.ai.usage`、matrix.md 写 `edu.ai.usage`、原 contract 写 `edu.ai.usage.events`。ai12 建议采用 `edu.ai.usage`(最简短),待 coord 裁决ISSUE-02)。
> **Outbox 豁免**AIUsageEvent 为派生数据事件004 §12.2 + §15.3 #6 仲裁豁免 Outbox允许直接 producer`aiokafka` + acks=all + idempotent + transactional_id
> **events.proto 现状**:无 `AIUsageEvent` message待 coord 补全ISSUE-04建议 schema 见 [02-architecture-design.md §3.3](../../../../services/ai/docs/02-architecture-design.md)。
> **长期可发布事件**P6+ 评估,待 coord 仲裁):`AIContentGenerated`topic `edu.ai.generated`,生成内容审计)、`AIFeedbackRecorded`topic `edu.ai.feedback`RLHF 数据)、`AIWorkflowEvent`topic `edu.ai.workflow`,工作流监控)。
### 1.5 错误码前缀
`AI_`如 AI_PROVIDER_UNAVAILABLE、AI_TOKEN_LIMIT_EXCEEDED、AI_CONTENT_FILTERED
`AI_*`对齐 [02-architecture-design.md §6.2](../../../../services/ai/docs/02-architecture-design.md) 完整清单matrix.md §6 已确认 ai 前缀为 `AI_`
| 错误码 | 触发条件 | HTTP | gRPC status |
| -------------------------------- | ----------------------------------- | ---- | ------------------- |
| `AI_UNAUTHORIZED` | 缺失 x-user-id 或 token 无效 | 401 | UNAUTHENTICATED |
| `AI_FORBIDDEN` | 角色无对应权限 | 403 | PERMISSION_DENIED |
| `AI_RATE_LIMITED` | 触发限流user/IP/school | 429 | RESOURCE_EXHAUSTED |
| `AI_QUOTA_EXCEEDED` | 学校/教师月度 token 配额耗尽 | 429 | RESOURCE_EXHAUSTED |
| `AI_LLM_UNAVAILABLE` | LLM Provider 不可达(降级骨架) | 200 | OK + degraded flag |
| `AI_LLM_TIMEOUT` | LLM 调用超时30s | 504 | DEADLINE_EXCEEDED |
| `AI_LLM_ALL_PROVIDERS_FAILED` | 所有 Provider 故障切换链均失败 | 503 | UNAVAILABLE |
| `AI_INVALID_MODEL` | model 名不支持 | 400 | INVALID_ARGUMENT |
| `AI_INVALID_DIFFICULTY` | difficulty 不在 easy/medium/hard | 400 | INVALID_ARGUMENT |
| `AI_INVALID_QUESTION_TYPE` | question_type 不在枚举内 | 400 | INVALID_ARGUMENT |
| `AI_DOWNSTREAM_UNAVAILABLE` | content / data-ana gRPC 不可达 | 502 | UNAVAILABLE |
| `AI_PROMPT_RENDER_FAILED` | Prompt 模板渲染失败 | 500 | INTERNAL |
| `AI_PROMPT_TEMPLATE_NOT_FOUND` | Prompt 模板不存在 | 404 | NOT_FOUND |
| `AI_WORKFLOW_NOT_FOUND` | 备课工作流 ID 不存在 | 404 | NOT_FOUND |
| `AI_WORKFLOW_EXPIRED` | 工作流已过期24h 未审核) | 410 | FAILED_PRECONDITION |
| `AI_WORKFLOW_STATE_INVALID` | 工作流状态不允许此操作 | 409 | FAILED_PRECONDITION |
| `AI_EVALUATION_FAILED` | 生成质量评估未通过且重试耗尽 | 503 | UNAVAILABLE |
| `AI_PII_DETECTED` | 输入包含未脱敏 PII | 400 | INVALID_ARGUMENT |
| `AI_PROMPT_INJECTION_DETECTED` | 输入疑似 Prompt 注入攻击 | 400 | INVALID_ARGUMENT |
| `AI_CONTENT_MODERATION_REJECTED` | 输出内容审核不通过(敏感词) | 503 | UNAVAILABLE |
| `AI_INTERNAL_ERROR` | 未捕获异常 | 500 | INTERNAL |
---
@@ -44,24 +101,37 @@
### 2.1 gRPC 调用(同步)
| 被调用方 | Service.RPC | 用途 | mock 策略 |
| --------------- | -------------------------------------- | ---------------------------- | ----------------------------------------------------------- |
| content (ai09) | KnowledgeGraphService.GetPrerequisites | 生成题目时获取知识点前置依赖 | content 就绪前使用本地知识点 stub固定 3 个前置知识点) |
| content (ai09) | QuestionService.SearchQuestions | 备课时检索同类题目参考 | content 就绪前返回空列表 |
| data-ana (ai11) | AnalyticsService.GetStudentWeakness | 个性化出题时获取学生薄弱点 | data-ana 就绪前使用本地薄弱点 stub固定 2 个 weak_points |
| 被调用方 | Service.RPC | 用途 | mock 策略 |
| --------------- | -------------------------------------- | ---------------------------- | ------------------------------------------------------------- |
| content (ai09) | `KnowledgeGraphService.GetPrerequisites` | 出题上下文:查询知识点前置依赖 | content 就绪前使用本地知识点 stub固定 3 个前置知识点) |
| content (ai09) | `KnowledgeGraphService.GetLearningPath` | 个性化出题:查询学生学习路径 | content 就绪前返回空路径 |
| content (ai09) | `TextbookService.ListTextbooks` | 备课工作流:查询教材章节关联知识点 | content 就绪前返回空列表 |
| content (ai09) | `QuestionService.CreateQuestions`(待 coord 补 proto | 备课工作流:生成的题目入库 | content 就绪前跳过入库,工作流标记 PersistFailed |
| data-ana (ai11) | `AnalyticsService.GetStudentWeakness` | 靶向出题:查询学生薄弱知识点 | data-ana 就绪前使用本地薄弱点 stub固定 2 个 weak_points |
| data-ana (ai11) | `AnalyticsService.GetLearningTrend` | 难度调节:查询学习趋势 | data-ana 就绪前返回默认趋势 |
| data-ana (ai11) | `AnalyticsService.GetClassPerformance` | 备课工作流:班级整体学情 | data-ana 就绪前降级跳过学情查询(`degraded:true` |
| iam (ai06) | `IamService.GetEffectiveDataScope`(待 P4 补全ISSUE-07 | 多租户配额:查询用户 DataScope | iam RPC 未就绪时降级为"仅按 user_id 配额,不按 school_id" |
> **端口**content gRPC 50054 / data-ana 50055 / iam 50052[port-allocation.md](../../../../infra/port-allocation.md) §5。注意 02-architecture-design.md §1.1/§1.2 mermaid 图误标为 3005/3006/3002HTTP 端口),应以 50054/50055/50052 为准。
### 2.2 Kafka 事件订阅(异步)
| Topic | Event | 发布方 | mock 策略 |
| ---------------------------------- | ------------------- | -------------- | -------------------------------------- |
| edu.content.knowledge_point.events | KnowledgePointEvent | content (ai09) | content 就绪前不订阅,使用内置知识点表 |
| edu.content.question.events | QuestionEvent | content (ai09) | content 就绪前忽略 |
> ai 是**无状态服务P5 不消费任何 Kafka 事件**01/02 §5.1 明确)。原 contract 列出的 content 事件订阅已删除(与设计文档矛盾,见 ISSUE-05.5)。
### 2.3 HTTP 调用(如有)
**P6+ 评估可消费事件**(待 coord 仲裁,非 P5 范围):
| Topic | Event | 发布方 | 用途 | 评估阶段 |
| ---------------------------------- | ------------------- | -------------- | -------------------------------- | -------- |
| `edu.content.question.events` | QuestionEvent | content (ai09) | 题目查重:避免 AI 生成与已有重复 | P6+ |
| `edu.iam.user.events` | UserEvent | iam (ai06) | 角色变更:主动失效 DataScope 缓存 | P6+ |
### 2.3 HTTP 调用(外部)
| 被调用方 | Method.Path | 用途 | mock 策略 |
| -------------------------------- | ------------------------- | ------------------ | ------------------------------------------------------------------ |
| LLM ProviderOpenAI/百川/本地 | POST /v1/chat/completions | 调用大模型生成内容 | 开发期使用本地 mock server 返回固定 ChatResponse不消耗真实 token |
| LLM ProviderOpenAI/Anthropic/百川/Ollama | POST /v1/chat/completions | 调用大模型生成内容 | 开发期使用本地 mock server 返回固定 ChatResponse不消耗真实 token |
> 多 Provider 通过 `ProviderFailoverChain` 故障切换OpenAI → Anthropic → 百川 → 本地 Ollama熔断器连续 3 次失败触发 60s 熔断。
---
@@ -69,18 +139,24 @@
### 3.1 我依赖的上游就绪标志
- [ ] content gRPC 50054 启用ai09—— 知识点维度 + 题库检索
- [ ] edu.content.knowledge_point.events topic 有事件发布ai09
- [ ] data-ana gRPC 50055 启用ai11—— 学生薄弱点可选ai 可先独立运行
- [ ] ai.proto 升级 v1 完整版6 RPC + 字段扩展)—— coordISSUE-03
- [ ] events.proto 补 `AIUsageEvent` message —— coordISSUE-04
- [ ] ai 用量事件 topic 命名裁决 —— coordISSUE-02
- [ ] content gRPC 50054 启用ai09—— 知识点维度 + 题库检索 + 入库
- [ ] data-ana gRPC 50055 启用ai11可选—— 学生薄弱点(可降级独立运行)
- [ ] iam `GetEffectiveDataScope` RPC P4 补全ai06 + coordISSUE-07可降级
- [ ] LLM Provider API key 配置(人类决策者)
### 3.2 我的就绪标志(供下游消费)
- [ ] ai gRPC 50057 启用HealthService.Check 返回 SERVING
- [ ] ai gRPC 50058 启用HealthService.Check 返回 SERVING
- [ ] AiService.Chat / StreamChat 可调用(含流式响应)
- [ ] AiService.GenerateQuestion / StreamGenerateQuestion 可调用
- [ ] AiService.GenerateLessonPlan 可调用P5 补全)
- [ ] AiService.OptimizeExpression 可调用
- [ ] edu.ai.usage.events topic 可发布(供 data-ana 统计 AI 用量)
- [ ] ai 用量事件 topic 可发布(供 data-ana 统计 AI 用量)
> **端口**50058[port-allocation.md](../../../../infra/port-allocation.md) §3/§5/§7 权威源2026-07-09 coord 仲裁"50058 让给 ai")。注意 [matrix.md](../matrix.md) §2/§8 仍写 50057待 coord 同步ISSUE-01
---
@@ -90,12 +166,13 @@
在 ai 真实服务就绪前为下游teacher-bff提供以下 mock
- **gRPC mock**:使用 grpc-mock 拦截 50057 端口
- **gRPC mock**:使用 grpc-mock 拦截 **50058** 端口
- AiService.Chat 返回固定 ChatResponsecontent="这是 AI 助手的模拟回复"
- AiService.StreamChat 返回固定流3 个 ChatChunk最后一个 done=true
- AiService.GenerateQuestion 返回固定 GeneratedQuestionquestion/answer/explanation
- AiService.GenerateLessonPlan 返回固定 LessonPlan3 个 LessonSection
- AiService.StreamGenerateQuestion 返回固定流2 个 GeneratedQuestion
- AiService.GenerateLessonPlan 返回固定 LessonPlanResponseworkflow_id + 3 个题目
- AiService.StreamGenerateQuestion 返回固定流2 个题目逐字 chunk
- AiService.OptimizeExpression 返回固定 OptimizedExpression
- **Kafka mock**ai 就绪前不发布真实 AIUsageEventdata-ana 仪表盘 AI 用量显示"暂无数据"
### 4.2 我消费的 mock
@@ -105,4 +182,22 @@
- **LLM Provider mock**:本地启动 mock serverPOST /v1/chat/completions 返回固定 JSON不消耗真实 token不产生费用
- **content 知识点**:内置固定知识点表(数学 20 个知识点 + 前置依赖关系),不依赖 content gRPC
- **data-ana 薄弱点**内置固定学生薄弱点2 个 weak_points不依赖 data-ana gRPC
- **事件订阅**:不订阅 content 事件,知识点维度表静态
- **iam DataScope**:内置固定 DataScopeSCHOOL 级),不依赖 iam GetEffectiveDataScope
- **事件订阅**P5 不订阅任何事件,无 mock 需要
---
## §5 契约待裁决项汇总
> 以下字段待 coord 裁决后最终定稿ai12 当前按建议方案先行实现。
| 待裁决项 | ISSUE | ai12 建议方案 | 影响章节 |
| --------------------------------- | ------ | ---------------------------------------------- | -------------- |
| ai gRPC 端口50057 vs 50058 | ISSUE-01 | 50058port-allocation.md 已定,待 matrix 同步) | §1.1 / §3.2 |
| ai 用量事件 topic 命名 | ISSUE-02 | `edu.ai.usage` | §1.4 |
| ai.proto P5 目标 RPC 数6 vs 8 | ISSUE-03 | 6 RPC查询/确认用 HTTP | §1.1 |
| events.proto 补 AIUsageEvent | ISSUE-04 | 按 02-architecture-design.md §3.3 schema | §1.4 |
| 备课工作流 TemporalP6 决策点) | ISSUE-06 | P5 用 BackgroundTasks + RedisP6 评估 Temporal | §2.1(无影响) |
| iam GetEffectiveDataScope P4 补全 | ISSUE-07 | 确认 P4 已补全;未补全则降级 | §2.1 |
| proto package 命名(全局) | ISSUE-08 | 待 coord 裁定是否迁移 `edu.<domain>.v1` | §1.1 |
| 响应信封 ActionState 整改 | ISSUE-09 | P5 整改degraded 作为 error.details 子字段 | §1.2 |

View File

@@ -1,40 +1,90 @@
# api-gateway 对接契约
> 负责人ai01
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[objections/api-gateway_issue.md](../objections/api-gateway_issue.md)、[worklines/api-gateway_workline.md](../worklines/api-gateway_workline.md)、[02-architecture-design.md](../../../services/api-gateway/docs/02-architecture-design.md)
> 依据:[coord-final-decisions.md](../../coord-final-decisions.md) §3.8 W1-W8、[president-final-rulings.md](../../president-final-rulings.md) §2.15/§2.16/§2.19
> 版本v22026-07-10 修正:路径前缀 /api→/api/v1、JWKS 拉取方式 gRPC→HTTP、删除 GetEffectiveAccess、错误码加 GW_ 前缀)
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 gRPC 接口
无。api-gateway 是 HTTP 入口,不对外提供 gRPC。
无。api-gateway 是 HTTP 入口,不对外提供 gRPC(依据 [president-final-rulings.md](../../president-final-rulings.md) §2.16 裁决gateway 保持 HTTP 透传,不改为 gRPC 客户端)
### 1.2 HTTP 端点(如有)
### 1.2 HTTP 端点
| Method | Path | 用途 | 认证 |
| ------ | -------------- | ------------------------------------------- | ---------------------- |
| ANY | /api/auth/* | 代理到 iam 认证相关(登录/注册/刷新 token | 公开(登录注册免认证) |
| ANY | /api/teacher/* | 代理到 teacher-bff GraphQL:3003 | JWT 必需 |
| ANY | /api/student/* | 代理到 student-bff GraphQL:3009 | JWT 必需 |
| ANY | /api/parent/* | 代理到 parent-bff GraphQL:3010 | JWT 必需 |
| ANY | /api/admin/* | 代理到 teacher-bff GraphQL admin namespace | JWT 必需 + admin 角色 |
| GET | /healthz | 网关健康检查liveness | 公开 |
| GET | /readyz | 网关就绪检查readiness含 iam 连通性) | 公开 |
| GET | /metrics | Prometheus 指标端点 | 公开(内网) |
#### 1.2.1 健康检查与指标端点(公开,无鉴权)
### 1.3 GraphQL schema如 BFF
| Method | Path | 用途 | 认证 | 阶段 |
| ------ | ---------- | ------------------------------------------- | ---- | ---- |
| GET | /healthz | 网关存活探针liveness | 公开 | P1 |
| GET | /readyz | 网关就绪探针readiness并行 ping 下游 /healthz| 公开 | P2 |
| GET | /metrics | Prometheus 指标端点7 个业务指标 + Go runtime| 公开(内网)| P2 |
不适用。api-gateway 仅做 HTTP 反向代理 + JWT 验签,不解析 GraphQL。
#### 1.2.2 反向代理路由(JWT 必需,除白名单外)
### 1.4 Kafka 事件发布(如有)
> **路径前缀**:所有业务路由统一 `/api/v1/*` 前缀(与 [main.go](../../../services/api-gateway/main.go) L59 `r.Group("/api/v1")` 一致)。
>
> **API 版本化**:按 [president-final-rulings.md](../../president-final-rulings.md) §2.15 裁决,各服务 Controller 内加 `/v1` 前缀。Gateway 透传时不改路径,前端调用 `/api/v1/<service>/v1/*`ISSUE-003 待 coord 仲裁最终方案,本表暂列方案 A
无。api-gateway 不发布事件。
| Method | PathGateway 外部路径) | 目标服务 | 端口 | 鉴权 | 公开子路径 | 阶段 |
| ------ | --------------------------------------------------- | ------------------------- | ----- | ---- | --------------------------------------- | ---- |
| ANY | /api/v1/iam/v1/*path + /api/v1/iam/v1 | iam | 3002 | JWT | `/iam/v1/register` `/login` `/refresh` | P2 |
| ANY | /api/v1/teacher/v1/*path + /api/v1/teacher/v1 | teacher-bffGraphQL | 3003 | JWT | — | P2 |
| ANY | /api/v1/student/v1/*path + /api/v1/student/v1 | student-bffGraphQL | 3009 | JWT | — | P3 |
| ANY | /api/v1/parent/v1/*path + /api/v1/parent/v1 | parent-bffGraphQL | 3010 | JWT | — | P4 |
| ANY | /api/v1/classes/v1/*path + /api/v1/classes/v1 | core-educlasses 合并) | 3004 | JWT | — | P3 |
| ANY | /api/v1/exams/v1/*path + /api/v1/homework/v1/*path + /api/v1/grades/v1/*path | core-edu | 3004 | JWT | — | P3 |
| ANY | /api/v1/textbooks/v1/*path + /api/v1/chapters/v1/*path + /api/v1/knowledge-points/v1/*path + /api/v1/questions/v1/*path | content | 3005 | JWT | — | P4 |
| ANY | /api/v1/analytics/v1/*path + /api/v1/dashboard/v1/*path | data-ana | 3006 | JWT | — | P4 |
| ANY | /api/v1/notifications/v1/*path + /api/v1/messages/v1/*path | msg | 3007 | JWT | — | P5 |
| ANY | /api/v1/ai/v1/*path + /api/v1/ai/v1 | ai | 3008 | JWT | — | P5 |
| ANY | /api/v1/admin/v1/*path + /api/v1/admin/v1 | teacher-bff admin namespace | 3003 | JWT + admin 角色 | — | P6 |
> **注 1**:每个前缀同时注册无尾斜杠与通配符两条路由(`/iam/v1` + `/iam/v1/*path`),因为 `r.RedirectTrailingSlash = false`[main.go](../../../services/api-gateway/main.go) L34
>
> **注 2**admin-portal P6 阶段复用 teacher-bff + admin schema 命名空间(依据 [coord.md](../coord.md) §1.3 ARB-001 裁决)。
>
> **注 3**:当前 P1 代码暂未加 `/v1` 前缀(如 `/api/v1/iam/*path`),待 ISSUE-003 仲裁后 P2.7 任务统一迁移。
#### 1.2.3 透传请求头Gateway 注入,下游读取)
| 头名 | 来源 | 用途 | 下游消费方 |
| -------------- | ----------------------------- | ----------------------------- | ---------- |
| `x-user-id` | JWT `sub` claim | 用户身份传递 | 所有下游 |
| `x-user-roles` | JWT `roles` claim逗号分隔| 角色传递 | 所有下游 |
| `x-data-scope` | JWT `data_scope` claim | 数据范围传递P2 新增) | 所有下游 |
| `X-Request-Id` | 客户端透传或 Gateway 生成 UUID | 请求 ID全链路追踪 | 所有下游 |
| `traceparent` | OTel SDK 自动处理 | W3C Trace Context链路追踪| 所有下游 |
| `tracestate` | OTel SDK 自动处理 | W3C Trace Context 扩展 | 所有下游 |
### 1.3 GraphQL schema
不适用。api-gateway 仅做 HTTP 反向代理 + JWT 验签,不解析 GraphQLGraphQL 由 BFF 层处理)。
### 1.4 Kafka 事件发布
无。api-gateway 不发布 Kafka 事件(纯同步 HTTP 反向代理)。
### 1.5 错误码前缀
`GW_`如 GW_UNAUTHORIZED、GW_RATE_LIMITED、GW_CIRCUIT_OPEN、GW_BACKEND_UNAVAILABLE
`GW_`依据 [coord-final-decisions.md](../../coord-final-decisions.md) W1 / G14 裁决
### 1.6 错误码清单
| 错误码 | HTTP | 触发条件 | 响应体ActionState 信封) |
| ------------------------ | ---- | --------------------- | ----------------------------------------------------------------------- |
| `GW_UNAUTHORIZED` | 401 | 缺失 Authorization 头 | `{success:false,error:{code:"GW_UNAUTHORIZED",message:"..."}}` |
| `GW_INVALID_TOKEN` | 401 | JWT 签名/格式错误 | 同上 |
| `GW_INVALID_CLAIMS` | 401 | JWT claims 解析失败 | 同上 |
| `GW_RATE_LIMITED` | 429 | 超出令牌桶限流 | `{success:false,error:{code:"GW_RATE_LIMITED",message:"...",retry_after:60}}` |
| `GW_CIRCUIT_OPEN` | 503 | 下游熔断打开 | `{success:false,error:{code:"GW_CIRCUIT_OPEN",message:"...",retry_after:30}}` |
| `GW_REQUEST_TOO_LARGE` | 413 | 请求体超 10MB | `{success:false,error:{code:"GW_REQUEST_TOO_LARGE",message:"..."}}` |
| `GW_INTERNAL_ERROR` | 500 | panic 兜底 | `{success:false,error:{code:"GW_INTERNAL_ERROR",message:"...",request_id:"..."}}` |
> 依据 W1 / W2 裁决:错误码统一 `GW_` 前缀,响应体统一 ActionState 信封 `{success,error:{code,message}}`。
---
@@ -42,22 +92,48 @@
### 2.1 gRPC 调用(同步)
| 被调用方 | Service.RPC | 用途 | mock 策略 |
| ---------- | ----------------------------- | ---------------------------------------- | ----------------------------------------------------------------- |
| iam (ai06) | IamService.GetPublicKey | 启动时拉取 RS256 公钥,用于 JWT 验签 | iam 就绪前使用本地固定 mock 公钥(与 mock 私钥配对签发 mock JWT |
| iam (ai06) | IamService.GetEffectiveAccess | 权限校验(可选,部分路由需要细粒度权限) | iam 就绪前放行所有请求(仅校验 JWT 签名) |
**无**。依据 [president-final-rulings.md](../../president-final-rulings.md) §2.16 裁决"gateway 保持 HTTP 透传,不改为 gRPC 客户端"api-gateway 不消费任何 gRPC 接口。
### 2.2 Kafka 事件订阅(异步)
> **勘误**v1 版本曾错误声明消费 `IamService.GetPublicKey` 和 `IamService.GetEffectiveAccess`v2 已删除):
> - JWT 公钥拉取走 HTTP JWKS 端点,非 gRPC见 §2.3
> - Gateway 不做权限点校验,不消费 `GetEffectiveAccess`(权限由下游服务 Controller `@RequirePermission` 自校验,见 [01-understanding.md](../../../services/api-gateway/docs/01-understanding.md) §2 + B3 裁决)
> - [matrix.md](./matrix.md) §2 gRPC 接口提供方矩阵中"iam 消费方"应移除 api-gateway已提请 ISSUE-001
### 2.2 Kafka 事件订阅
无。api-gateway 不订阅 Kafka 事件。
### 2.3 HTTP 调用(如有
### 2.3 HTTP 调用(同步
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------ | ------------- | --------------------------- | -------------------------------------------------- |
| teacher-bff (ai03) | POST /graphql | 反向代理教师端 GraphQL 请求 | teacher-bff 就绪前返回 502前端使用本地 mock 数据 |
| student-bff (ai04) | POST /graphql | 反向代理学生端 GraphQL 请求 | student-bff 就绪前返回 502 |
| parent-bff (ai05) | POST /graphql | 反向代理家长端 GraphQL 请求 | parent-bff 就绪前返回 502 |
| 被调用方 | Method.Path | 用途 | mock 策略 | 阶段 |
| ------------------ | ----------------------------------- | ------------------------------------------ | --------------------------------------------------------------- | ---- |
| iam (ai06) | `GET /.well-known/jwks.json` | 拉 RS256 公钥集JWKSTTL 5min 缓存 | iam 就绪前使用本地固定 mock RS256 公钥(与 mock 私钥配对签发 mock JWT| P2 |
| iam (ai06) | `GET /healthz` | /readyz 下游健康检查 | iam 就绪前 /readyz 软失败(返回 200 + degraded | P2 |
| teacher-bff (ai03) | `POST /graphql`(反向代理) | 代理教师端 GraphQL 请求 | teacher-bff 就绪前返回 503 + Retry-After前端降级到本地 mock | P2 |
| student-bff (ai04) | `POST /graphql`(反向代理) | 代理学生端 GraphQL 请求 | student-bff 就绪前返回 503 | P3 |
| parent-bff (ai05) | `POST /graphql`(反向代理) | 代理家长端 GraphQL 请求 | parent-bff 就绪前返回 503 | P4 |
| core-edu (ai08) | `GET /healthz` + 业务 REST反向代理| /readyz 检查 + 业务请求代理 | core-edu 就绪前 /readyz 软失败 | P3 |
| content (ai09) | `GET /healthz` + 业务 REST | /readyz 检查 + 业务请求代理 | content 就绪前 /readyz 软失败 | P4 |
| data-ana (ai11) | `GET /healthz` + 业务 REST | /readyz 检查 + 业务请求代理 | data-ana 就绪前 /readyz 软失败 | P4 |
| msg (ai10) | `GET /healthz` + 业务 REST | /readyz 检查 + 业务请求代理 | msg 就绪前 /readyz 软失败 | P5 |
| ai (ai12) | `GET /healthz` + 业务 REST | /readyz 检查 + 业务请求代理 | ai 就绪前 /readyz 软失败 | P5 |
> **JWKS 缓存策略**[packages/shared-go/jwks/jwks.go](../../../packages/shared-go/jwks/jwks.go)
> - TTL 5min到期后台异步刷新不阻塞请求
> - kid 未命中时强制同步刷新一次
> - 刷新失败保留旧公钥集继续服务fail-open 1 次后 fail-close
> - 启动时同步拉取一次,失败则 panic 拒绝启动
### 2.4 shared-go 包依赖
依据 [president-final-rulings.md](../../president-final-rulings.md) §2.19 裁决ai01 直接 import `packages/shared-go`
| 模块 | 用途 | 接入阶段 |
| ----------------- | ------------------------------------------ | -------- |
| `shared-go/jwks` | JWKS FetcherHTTP 拉 RS256 公钥 + 缓存) | P2.2 |
| `shared-go/logger`| 结构化日志zap 或 slog待 ISSUE-004 仲裁)| P2.1 |
| `shared-go/tracer`| OTel tracer 初始化(评估接入,若接口兼容) | P2.1 |
| `shared-go/env` | 环境变量加载(评估接入,若接口兼容) | P2.1 |
---
@@ -65,38 +141,70 @@
### 3.1 我依赖的上游就绪标志
- [ ] iam gRPC 50052 启用ai06—— GetPublicKey 拉取验签公钥
- [ ] teacher-bff GraphQL :3003 启用ai03
- [ ] student-bff GraphQL :3009 启用ai04
- [ ] parent-bff GraphQL :3010 启用ai05
| 上游 | 就绪标志 | 阶段 | 状态 |
| ---- | -------- | ---- | ---- |
| coord | shared-go 包骨架tracer/logger/jwks/env 4 模块)| 批次 0 | ✅ 已完成 |
| coord | ISSUE-001 仲裁JWKS HTTP 确认)| P2 启动前 | ⏳ 待仲裁 |
| coord | ISSUE-002 仲裁shared-go 接入)| P2 启动前 | ⏳ 待仲裁 |
| coord | ISSUE-003 仲裁API 版本化路由方案)| P2.7 前 | ⏳ 待仲裁 |
| coord | ISSUE-004 仲裁zap vs slog| P2.1 前 | ⏳ 待仲裁 |
| iam (ai06) | `GET /.well-known/jwks.json` HTTP 端点 + RS256 JWT 签发 | P2 | ⏳ |
| iam (ai06) | `GET /healthz` 端点 | P2 | ⏳ |
| teacher-bff (ai03) | `POST /graphql` :3003 启用 + `GET /healthz` | P2 | ⏳ |
| student-bff (ai04) | `POST /graphql` :3009 启用 + `GET /healthz` | P3 | ⏳ |
| core-edu (ai08) | `GET /healthz` + classes 合并 | P3 | ⏳ |
| parent-bff (ai05) | `POST /graphql` :3010 启用 + `GET /healthz` | P4 | ⏳ |
| content (ai09) | `GET /healthz` + REST 端点 | P4 | ⏳ |
| data-ana (ai11) | `GET /healthz` + REST 端点 | P4 | ⏳ |
| msg (ai10) | `GET /healthz` + REST 端点 | P5 | ⏳ |
| ai (ai12) | `GET /healthz` + REST 端点 | P5 | ⏳ |
| push-gateway (ai02) | :8081 启用 + /internal/pushWebSocket 协作评估)| P5 | ⏳ |
### 3.2 我的就绪标志(供下游消费)
- [ ] api-gateway HTTP :8080 启用(/healthz 返回 200
- [ ] /readyz 返回 200含 iam 连通性检查通过)
- [ ] JWT 验签链路打通(使用 iam 公钥校验 access_token
- [ ] /api/auth/* 代理到 iam 认证链路可用
- [ ] /api/teacher/* /api/student/* /api/parent/* 反向代理到各 BFF 可用
- [ ] 限流IP 级令牌桶)+ 熔断(各后端独立熔断器)生效
| 阶段 | 就绪标志 | 状态 |
| ---- | -------- | ---- |
| P2 | api-gateway HTTP :8080 启用(/healthz 返回 200| ⏳ |
| P2 | /readyz 返回 200含 iam + teacher-bff 连通性检查通过)| ⏳ |
| P2 | JWT RS256 验签链路打通(使用 iam JWKS 公钥校验 access_token| ⏳ |
| P2 | /api/v1/iam/v1/* 代理到 iam 认证链路可用 | ⏳ |
| P2 | /api/v1/teacher/v1/* 反向代理到 teacher-bff GraphQL 可用 | ⏳ |
| P2 | 限流IP 级令牌桶)+ 熔断(共享 downstream+ CORS 白名单生效 | ⏳ |
| P2 | 7 个业务指标暴露在 /metrics | ⏳ |
| P2 | 错误响应统一 ActionState 信封 + GW_ 前缀 | ⏳ |
| P3 | /api/v1/student/v1/* + /api/v1/exams/v1/* 等路由可用 | ⏳ |
| P4 | /api/v1/parent/v1/* + /api/v1/textbooks/v1/* + /api/v1/analytics/v1/* 路由可用 | ⏳ |
| P5 | /api/v1/notifications/v1/* + /api/v1/ai/v1/* 路由可用 | ⏳ |
| P6 | 限流迁 Redis + 测试覆盖率 ≥ 80% | ⏳ |
---
## §4 Mock 策略
### 4.1 我提供的 mock
### 4.1 我提供的 mock(供下游各前端 portal 消费)
在 api-gateway 真实就绪前,为下游(各前端 portal提供以下 mock
在 api-gateway 真实就绪前,为下游(teacher-portal / student-portal / parent-portal / admin-portal提供以下 mock
- **HTTP mock**:使用 MSWMock Service Worker或本地 nginx 拦截
- /api/auth/login 返回固定 JWTmock 签发)+ UserInfo
- /api/teacher/* /api/student/* /api/parent/* 直接返回各 BFF 的 mock GraphQL 响应
- /healthz /readyz 返回 200
- **HTTP mock**(由各前端 portal 自行用 MSW 拦截api-gateway 不提供 mock 服务):
- `/api/v1/iam/v1/login` 返回固定 JWTmock 签发)+ UserInfo
- `/api/v1/teacher/v1/*` `/api/v1/student/v1/*` `/api/v1/parent/v1/*` 直接返回各 BFF 的 mock GraphQL 响应
- `/healthz` `/readyz` 返回 200
- **JWT mock**:前端开发期使用固定 mock JWTapi-gateway 就绪前不走真实验签)
### 4.2 我消费的 mock
### 4.2 我消费的 mock(在真实上游就绪前)
在真实上游就绪前api-gateway 使用以下 mock
- **iam 公钥**:使用本地固定 mock RS256 公钥(与 mock 私钥配对),验签 mock JWT
- **iam 权限校验**GetEffectiveAccess 返回 allowed=true放行所有请求
- **各 BFF 代理**BFF 就绪前返回 503 + Retry-After前端降级到本地 mock 数据
- **iam JWKS 公钥**:使用本地固定 mock RS256 公钥(与 mock 私钥配对),验签 mock JWTDevMode 下生效)
- **iam /readyz 检查**iam 就绪前软失败,返回 200 + `degraded: true`
- **各 BFF 代理**BFF 就绪前返回 503 + `Retry-After`,前端降级到本地 mock 数据
- **下游 /healthz 检查**:未就绪服务软失败(返回 200 + degraded已就绪服务硬失败返回 503
---
## §5 变更记录
| 版本 | 日期 | 变更内容 | 变更者 |
| ---- | ---------- | ------------------------------------------------------------------------ | ------ |
| v1 | 2026-07-09 | 初始创建(含错误:/api/* 路径、gRPC GetPublicKey、GetEffectiveAccess | ai01 |
| v2 | 2026-07-10 | 修正:路径 /api→/api/v1、JWKS 拉取 gRPC→HTTP、删除 GetEffectiveAccess、错误码加 GW_ 前缀、对齐 ActionState 信封、补充 shared-go 依赖、补充 admin-portal P6 路由 | ai01 |

View File

@@ -1,53 +1,223 @@
# content 对接契约
> 负责人ai09
> 关联:[matrix.md](./matrix.md)、[content.proto](../../../packages/shared-proto/proto/content.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 关联:[matrix.md](../matrix.md)、[content.proto](../../../packages/shared-proto/proto/content.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[../../services/content/docs/02-architecture-design.md](../../../services/content/docs/02-architecture-design.md)、[../objections/content_issue.md](../objections/content_issue.md)、[../worklines/content_workline.md](../worklines/content_workline.md)
> 当前分支:`feat-review-content-module-docs-WAIyMA`
> 契约策略:[coord-final-decisions.md §2 B2](../../coord-final-decisions.md) "首次实现即 gRPC 调用下游"——content 对外契约统一为 gRPCREST 端点仅自身管理面用,不作为下游消费契约)
---
## §0 契约状态总览
| 维度 | 当前状态 | 目标状态P4 完成) |
| ---------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| gRPC | ❌ 未实现([content.proto](../../../packages/shared-proto/proto/content.proto) 仅 5 RPC 定义) | ✅ 21 RPC4 Service |
| HTTP REST | ✅ 已实现 22 端点4 Controller | 🔁 保留作为管理面(不作为下游契约,下游统一 gRPC |
| Kafka 发布 | ❌ 未实现 | ✅ 4 聚合 topic待 ISSUE-002 仲裁确认策略) |
| Kafka 消费 | ❌ 未实现 | 🟢 可选content 是上游,不主动消费 core-edu 事件,见 ISSUE-005 |
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有
### 1.1 gRPC 接口(目标态 · P4 完成
| Service | RPC | 请求 | 响应 | 端口 |
| --------------------- | ------------------ | ------------------------- | -------------------------- | ----- |
| TextbookService | CreateTextbook | CreateTextbookRequest | Textbook | 50054 |
| TextbookService | GetTextbook | GetTextbookRequest | Textbook | 50054 |
| TextbookService | ListTextbooks | ListTextbooksRequest | ListTextbooksResponse | 50054 |
| ChapterService | GetChapter | GetChapterRequest | Chapter | 50054 |
| ChapterService | ListChapters | ListChaptersRequest | ListChaptersResponse | 50054 |
| ChapterService | CreateChapter | CreateChapterRequest | Chapter | 50054 |
| ChapterService | UpdateChapter | UpdateChapterRequest | Chapter | 50054 |
| KnowledgeGraphService | GetPrerequisites | GetPrerequisitesRequest | KnowledgePointsResponse | 50054 |
| KnowledgeGraphService | GetLearningPath | GetLearningPathRequest | LearningPath | 50054 |
| KnowledgeGraphService | AddPrerequisite | AddPrerequisiteRequest | AddPrerequisiteResponse | 50054 |
| KnowledgeGraphService | RemovePrerequisite | RemovePrerequisiteRequest | RemovePrerequisiteResponse | 50054 |
| QuestionService | CreateQuestion | CreateQuestionRequest | Question | 50054 |
| QuestionService | GetQuestion | GetQuestionRequest | Question | 50054 |
| QuestionService | ListQuestions | ListQuestionsRequest | ListQuestionsResponse | 50054 |
| QuestionService | UpdateQuestion | UpdateQuestionRequest | Question | 50054 |
| QuestionService | DeleteQuestion | DeleteQuestionRequest | DeleteQuestionResponse | 50054 |
| QuestionService | PublishQuestion | PublishQuestionRequest | PublishQuestionResponse | 50054 |
| QuestionService | SearchQuestions | SearchQuestionsRequest | SearchQuestionsResponse | 50054 |
> 依据 [coord-final-decisions.md §3.3](../../coord-final-decisions.md) N1/N3/N5 + [02-architecture-design.md §4.2](../../../services/content/docs/02-architecture-design.md)
> 待 ISSUE-004 仲裁后定稿,当前按建议方案 21 RPC 列出
### 1.2 HTTP 端点(如有)
| Service | RPC | 请求 | 响应 | 端口 | 阶段 |
| --------------------- | -------------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | ----- | ---- |
| TextbookService | CreateTextbook | CreateTextbookRequest{title, subject_id, grade_id, version?} | Textbook | 50054 | P4 |
| TextbookService | GetTextbook | GetTextbookRequest{id} | Textbook | 50054 | P4 |
| TextbookService | ListTextbooks | ListTextbooksRequest{subject_id?, grade_id?, page_token, page_size} | ListTextbooksResponse{textbooks[], next_page_token} | 50054 | P4 |
| TextbookService | UpdateTextbook | UpdateTextbookRequest{id, title?, status?, metadata?} | Textbook | 50054 | P4 |
| TextbookService | DeleteTextbook | DeleteTextbookRequest{id} | Empty | 50054 | P4 |
| ChapterService | CreateChapter | CreateChapterRequest{textbook_id, title, order, parent_id?} | Chapter | 50054 | P4 |
| ChapterService | GetChapter | GetChapterRequest{id} | Chapter | 50054 | P4 |
| ChapterService | ListChapters | ListChaptersRequest{textbook_id, parent_id?} | ListChaptersResponse{chapters[]} | 50054 | P4 |
| ChapterService | UpdateChapter | UpdateChapterRequest{id, title?, order?, status?} | Chapter | 50054 | P4 |
| ChapterService | DeleteChapter | DeleteChapterRequest{id} | Empty | 50054 | P4 |
| KnowledgeGraphService | GetPrerequisites | GetPrerequisitesRequest{knowledge_point_id, depth?} | KnowledgePointsResponse{points[]} | 50054 | P4 |
| KnowledgeGraphService | GetLearningPath | GetLearningPathRequest{student_id, subject_id} | LearningPath{points[], recommended_order[]} | 50054 | P4 |
| KnowledgeGraphService | AddPrerequisite | AddPrerequisiteRequest{kp_id, prerequisite_id} | Empty | 50054 | P4 |
| KnowledgeGraphService | RemovePrerequisite | RemovePrerequisiteRequest{kp_id, prerequisite_id} | Empty | 50054 | P4 |
| QuestionService | CreateQuestion | CreateQuestionRequest{knowledge_point_id, type, content, options?, answer, explanation?, difficulty?, source?, created_by?} | Question | 50054 | P4 |
| QuestionService | BatchCreateQuestions | BatchCreateQuestionsRequest{questions[]} | BatchCreateQuestionsResponse{ids[], failed[]} | 50054 | P4 |
| QuestionService | GetQuestion | GetQuestionRequest{id} | Question | 50054 | P4 |
| QuestionService | ListQuestions | ListQuestionsRequest{knowledge_point_id?, type?, difficulty?, status?, page_token, page_size} | ListQuestionsResponse{questions[], next_page_token} | 50054 | P4 |
| QuestionService | UpdateQuestion | UpdateQuestionRequest{id, content?, answer?, status?} | Question | 50054 | P4 |
| QuestionService | DeleteQuestion | DeleteQuestionRequest{id} | Empty | 50054 | P4 |
| QuestionService | PublishQuestion | PublishQuestionRequest{id} | Empty | 50054 | P4 |
| QuestionService | SearchQuestions | SearchQuestionsRequest{q?, type?, difficulty?, knowledge_point_id?, page_token, page_size} | SearchQuestionsResponse{questions[], total, next_page_token} | 50054 | P5 |
无对外 HTTP 端点,仅 gRPC。
**RPC 总数**21TextbookService 5 + ChapterService 5 + KnowledgeGraphService 4 + QuestionService 7
### 1.3 GraphQL schema如 BFF
> ⚠️ **matrix.md §2 同步项**:当前 matrix.md §2 登记 content 为 18 RPC待 ISSUE-004 仲裁后需更新为 21 RPC差额ChapterService 补 Update + Delete = +2TextbookService 补 Update + Delete = +2QuestionService 原 contract 已含 Publish/Searchdesign doc 缺,对齐后 +0原合计 18 + 4 - 1 = 21
不适用。
#### proto message 字段说明(关键字段)
### 1.4 Kafka 事件发布(如有)
```protobuf
message Textbook {
string id = 1;
string title = 2;
string subject_id = 3;
string grade_id = 4;
string version = 5;
string status = 6; // draft/pending_review/published/archived
string tenant_id = 7; // 多租户预留
google.protobuf.Struct metadata = 8; // 扩展字段
int64 created_at = 9;
int64 updated_at = 10;
}
| Topic | Event | 消费方 |
| ---------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------- |
| edu.content.knowledge_point.events | KnowledgePointEventaction: created/updated/prerequisite_added/prerequisite_removed | data-ana / ai / Neo4j Sync Worker / ES Sync Worker |
| edu.content.question.events | QuestionEventaction: created/updated/published/deleted | data-ana |
message Chapter {
string id = 1;
string textbook_id = 2;
string title = 3;
int32 order = 4; // DB 列名 order_numproto 字段名 order
string parent_id = 5; // 树形结构
string status = 6;
int64 created_at = 7;
int64 updated_at = 8;
}
message KnowledgePoint {
string id = 1;
string chapter_id = 2;
string title = 3;
string description = 4;
int32 difficulty = 5; // 1-5
google.protobuf.Struct metadata = 6;
int64 created_at = 7;
int64 updated_at = 8;
}
message Question {
string id = 1;
string knowledge_point_id = 2;
string type = 3; // single_choice/multiple_choice/short_answer/essay
string content = 4; // 题干HTML/markdown
google.protobuf.Struct options = 5; // 选项(选择题)
string answer = 6;
string explanation = 7;
int32 difficulty = 8; // 1-5
string status = 9; // draft/pending_review/published/rejected/archived
string source = 10; // manual/ai_generated/imported
string created_by = 11;
google.protobuf.Struct metadata = 12;
int64 created_at = 13;
int64 updated_at = 14;
}
```
### 1.2 HTTP 端点(管理面 · 非下游契约)
> ⚠️ 以下 REST 端点仅供 content 自身管理面/直接 Gateway 访问用,**下游服务teacher-bff/student-bff/ai统一走 gRPC**,不消费以下 REST 端点。
当前已实现 22 端点4 Controller详见 [02-architecture-design.md §4.1](../../../services/content/docs/02-architecture-design.md)。P4 重构后将统一加 `/v1/` 版本前缀(见 ISSUE-008
### 1.3 GraphQL schema
不适用。content 是 gRPC 服务,不暴露 GraphQL。
### 1.4 Kafka 事件发布(目标态 · P4 完成)
> 待 ISSUE-002 仲裁确认 topic 命名策略,当前按**聚合 topic + action 字段**策略列出(与 matrix.md §4 / events.proto ClassEvent 模式一致)
| Topic | Event message | action 字段值 | 消费方 | 阶段 |
| ------------------------------------ | -------------------- | ---------------------------------------------------------- | -------------------------------------------------- | ---- |
| edu.content.textbook.events | TextbookEvent | created / updated / published / archived | data-ana / msg通知教师教材发布 | P4 |
| edu.content.chapter.events | ChapterEvent | created / updated / deleted | data-ana | P4 |
| edu.content.knowledge_point.events | KnowledgePointEvent | created / updated / prerequisite_added / prerequisite_removed | data-ana / ai / Neo4j Sync Worker / ES Sync Worker | P4 |
| edu.content.question.events | QuestionEvent | created / updated / published / deleted | data-ana / ai / ES Sync Worker | P4 |
> ⚠️ **ISSUE-003 待仲裁**:当前 matrix.md §4 仅登记 kp + question 两类 topicTextbook/Chapter 事件未登记。本契约按 design doc §5.1 补全 4 类,待 coord 仲裁后同步 matrix.md。
#### 事件 payload schema待补 events.proto
需在 [events.proto](../../../packages/shared-proto/proto/events.proto) 追加 4 个 message
```protobuf
message TextbookEvent {
string event_id = 1;
string aggregate_id = 2;
string event_type = 3; // edu.content.textbook.created 等
int64 occurred_at = 4;
string textbook_id = 5;
string title = 6;
string subject_id = 7;
string grade_id = 8;
string version = 9;
string action = 10; // created/updated/published/archived
map<string, string> metadata = 11;
}
message ChapterEvent {
string event_id = 1;
string aggregate_id = 2;
string event_type = 3;
int64 occurred_at = 4;
string chapter_id = 5;
string textbook_id = 6;
string title = 7;
int32 order = 8;
string action = 9; // created/updated/deleted
map<string, string> metadata = 10;
}
message KnowledgePointEvent {
string event_id = 1;
string aggregate_id = 2;
string event_type = 3;
int64 occurred_at = 4;
string kp_id = 5;
string chapter_id = 6;
string title = 7;
int32 difficulty = 8;
string action = 9; // created/updated/prerequisite_added/prerequisite_removed
string prerequisite_id = 10; // 仅 prerequisite_* 有值
map<string, string> metadata = 11;
}
message QuestionEvent {
string event_id = 1;
string aggregate_id = 2;
string event_type = 3;
int64 occurred_at = 4;
string question_id = 5;
string kp_id = 6;
string type = 7; // single_choice 等
int32 difficulty = 8;
string status = 9;
string source = 10; // manual/ai_generated/imported
string created_by = 11;
string action = 12; // created/updated/published/deleted
map<string, string> metadata = 13;
}
```
### 1.5 错误码前缀
`CONTENT_`如 CONTENT_TEXTBOOK_NOT_FOUND、CONTENT_QUESTION_DUPLICATE
`CONTENT_`详见 [02-architecture-design.md §6.2](../../../services/content/docs/02-architecture-design.md)
| 错误码 | HTTP | gRPC status | 触发条件 |
| ------------------------- | ---- | ------------------ | ---------------------------------- |
| CONTENT_VALIDATION_ERROR | 400 | INVALID_ARGUMENT | Zod 校验失败 / 题型非法 / 难度越界 |
| CONTENT_NOT_FOUND | 404 | NOT_FOUND | 资源不存在 |
| CONTENT_PERMISSION_DENIED | 403 | PERMISSION_DENIED | 权限不足 |
| CONTENT_CONFLICT | 409 | ALREADY_EXISTS | 唯一约束冲突 / 状态机非法转换 |
| CONTENT_BUSINESS_ERROR | 422 | FAILED_PRECONDITION | 业务规则违反(如循环依赖检测) |
| CONTENT_DATABASE_ERROR | 500 | INTERNAL | Drizzle 操作异常 |
| CONTENT_INTERNAL_ERROR | 500 | INTERNAL | 未知异常 |
| CONTENT_NEO4J_UNAVAILABLE | 503 | UNAVAILABLE | Neo4j 不可用且无降级路径 |
### 1.6 健康检查端点
| 端点 | 用途 | 鉴权 | 响应 |
| ----------- | ------------------------------------------------------- | ---- | ------------------------------------------------------------------------------- |
| GET /healthz | 存活探针liveness仅返回进程状态 | 无 | `{ "status": "ok", "service": "content", "timestamp": "..." }` |
| GET /readyz | 就绪探针readiness检查 DB/Neo4j/Kafka 三依赖 | 无 | `{ "status": "ok|degraded|down", "checks": { database, neo4j, kafka_producer, kafka_consumer } }` |
| GET /metrics | Prometheus 指标 | 无 | Prometheus exposition format |
---
@@ -55,18 +225,30 @@
### 2.1 gRPC 调用(同步)
无直接 gRPC 调用上游。content 通过 Kafka 事件接收 core-edu 班级/学生变更用于数据一致性
**无**。content 是内容资源上游提供方,不主动调用其他业务服务 gRPC
> content 与 core-edu 的关系澄清(见 ISSUE-005content 是 core-edu 的上游core-edu 调 content 查知识点不是下游。content 不消费 core-edu 事件。
### 2.2 Kafka 事件订阅(异步)
| Topic | Event | 发布方 | mock 策略 |
| ---------------- | --------------------------------- | --------------- | ----------------------------------------------------- |
| edu.class.events | ClassEventaction: transferred | core-edu (ai08) | core-edu 就绪前不订阅content 内部不依赖班级实时数据 |
| edu.exam.events | ExamEvent | core-edu (ai08) | core-edu 就绪前忽略,题目关联知识点不依赖考试事件 |
**P4 不订阅任何事件**(按 ISSUE-005 建议删除原 `edu.teaching.content.invalidated` 条目)。
### 2.3 HTTP 调用(如有)
若 P6+ 有教材/章节联动需求,由 core-edu 主动调用 content gRPC UpdateQuestion/UpdateTextbook 状态变更,而非事件驱动。
无。
### 2.3 HTTP 调用
**无**
### 2.4 基础设施依赖
| 依赖 | 用途 | 配置项env.ts | 必需性 |
| --------------- | ----------------------------- | ----------------------------- | ---------------------- |
| MySQL 8 | 写模型主库4 张业务表 + outbox | DATABASE_URL | 🔴 必需 |
| Neo4j 5 | 知识图谱PREREQUISITE_OF | NEO4J_URL / NEO4J_PASSWORD | 🔴 必需(图谱查询核心) |
| Kafka | Outbox 事件发布 | KAFKA_BROKERS待补 env.ts | 🔴 必需Outbox 强制) |
| Redis | 缓存(教材树/章节树P5+ | REDIS_URL已预留 | 🟢 P5+ 可选 |
| Elasticsearch 8 | 题库全文检索P5+ | ES_URL已预留 | 🟢 P5+ 必需 |
| OTLP Collector | 链路追踪 | OTEL_EXPORTER_OTLP_ENDPOINT | 🟢 可选(未配置时降级) |
---
@@ -74,37 +256,113 @@
### 3.1 我依赖的上游就绪标志
- [ ] core-edu gRPC 50053 启用ai08—— 用于知识点与班级关联可选content 可先独立运行)
- [ ] edu.class.events / edu.exam.events topic 有事件发布ai08
| 上游 | 就绪标志 | 必需性 | mock 策略 |
| -------------- | ----------------------------------------- | ---------------------- | ----------------------------------------------- |
| infra | MySQL 8 可用 | 🔴 必需 | 本地 docker-compose |
| infra | Neo4j 5 可用 | 🔴 必需 | 本地 docker-compose未配置时图谱查询返回 503 |
| infra | Kafka 集群可用 | 🔴 必需 | 本地 docker-compose |
| shared-proto | content.proto / events.proto 补全 | 🔴 必需P4.6 自身完成) | ai09 自行修改 proto |
| core-edu (ai08) | gRPC 50053 启用 | 🟢 可选content 独立) | 不依赖 core-edu 实时数据 |
| ai (ai12) | gRPC 50058 启用 | 🟢 P5 联调时必需 | grpc-mock 拦截 |
### 3.2 我的就绪标志(供下游消费)
#### P4 就绪(核心交付)
- [ ] content gRPC 50054 启用HealthService.Check 返回 SERVING
- [ ] TextbookService 3 RPC 可调用
- [ ] ChapterService 4 RPC 可调用
- [ ] TextbookService 5 RPC 可调用Create/Get/List/Update/Delete
- [ ] ChapterService 5 RPC 可调用Create/Get/List/Update/Delete
- [ ] KnowledgeGraphService 4 RPC 可调用GetPrerequisites/GetLearningPath/AddPrerequisite/RemovePrerequisite
- [ ] QuestionService 7 RPC 可调用(含 SearchQuestions 全文检索
- [ ] edu.content.knowledge_point.events / edu.content.question.events topic 可发布
- [ ] QuestionService 7 RPC 可调用(Create/BatchCreate/Get/List/Update/Delete/Publish/Search
- [ ] edu.content.textbook.events / chapter.events / knowledge_point.events / question.events 4 topic 可发布
- [ ] /readyz 返回 DB/Neo4j/Kafka 三依赖状态
- [ ] Outbox Publisher worker 运行中content_outbox_events 表 PENDING 事件 < 100
- [ ] Neo4j Sync Worker 运行中(知识点节点最终一致延迟 < 2s
#### P5 就绪(检索 + AI 集成)
- [ ] GET /questions/search 检索 API 可用(延迟 < 200ms
- [ ] QuestionService.SearchQuestions gRPC 可调用
- [ ] QuestionService.BatchCreateQuestions 与 ai12 联调通过
- [ ] ES Sync Worker 运行中(题目索引最终一致延迟 < 2s
- [ ] 测试覆盖率 ≥ 80%
#### P6+ 就绪(演进)
- [ ] Question 审核工作流状态机完整
- [ ] 知识图谱可视化 API 可用
- [ ] 教材版本管理启用
---
## §4 Mock 策略
### 4.1 我提供的 mock
### 4.1 我提供的 mock(供下游消费)
在 content 真实服务就绪前为下游teacher-bff / student-bff / ai / data-ana提供以下 mock
- **gRPC mock**:使用 grpc-mock 拦截 50054 端口
- TextbookService.ListTextbooks 返回固定 5 个 Textbook语数英理化
- ChapterService.ListChapters 返回固定章节树(每教材 10 章)
- KnowledgeGraphService.GetLearningPath 返回固定 8 个 KnowledgePoint 推荐顺序
- KnowledgeGraphService.GetPrerequisites 返回固定 3前置知识点
- QuestionService.SearchQuestions 返回固定 20 个 Question含 options
- **Kafka mock**content 就绪前不发布真实事件,下游使用本地 stub
#### gRPC mockgrpc-mock 拦截 50054 端口
| Service | RPC | Mock 返回 |
| --------------------- | --------------------- | ---------------------------------------------------------- |
| TextbookService | ListTextbooks | 固定 5 Textbook语数英理化subject_id=math/chn/eng/phy/chem |
| TextbookService | GetTextbook | 返回第一个 Textbook |
| ChapterService | ListChapters | 固定章节树(每教材 10 章,含 parent_id 树形结构) |
| KnowledgeGraphService | GetLearningPath | 固定 8 个 KnowledgePoint 推荐顺序(按 difficulty 升序) |
| KnowledgeGraphService | GetPrerequisites | 固定 3 个前置知识点 |
| QuestionService | ListQuestions | 固定 20 个 Question含 4 种题型各 5 个) |
| QuestionService | SearchQuestions | 固定 20 个 Question含 options |
| QuestionService | BatchCreateQuestions | 返回成功 + 生成 20 个 cuid2 ID |
| QuestionService | CreateQuestion | 返回成功 + 生成 1 个 cuid2 ID |
#### Kafka mock
content 就绪前不发布真实事件,下游使用本地 stubdata-ana / ai 各自维护测试数据集)。
### 4.2 我消费的 mock
在真实 core-edu 就绪前content 使用以下 mock
content P4 不消费任何上游事件,无需 mock 上游。
- 班级/学生数据:不依赖 core-edu 实时数据,知识点关联使用固定 subject_id/grade
- 事件订阅:不订阅 edu.class.events / edu.exam.events内部数据自洽
P5 联调阶段消费 ai (ai12) 的 gRPC使用 grpc-mock 拦截 50058 端口:
- AiService.GenerateQuestion 返回固定 1 个 Questionsource=ai_generated
- AiService.Chat 返回固定文本响应
---
## §5 契约一致性核查2026-07-10
> 本节记录 contract.md 与 design doc / matrix.md / proto 的对齐情况
### 5.1 与 02-architecture-design.md 对齐
| 维度 | design doc | contract.md本文件 | 一致性 | 备注 |
| ------------- | -------------------------------- | ------------------------------ | ------ | ------------------------------------------ |
| gRPC RPC 数 | §4.2 列 18 RPC | §1.1 列 21 RPC | ⚠️ | 待 ISSUE-004 仲裁;建议以 21 RPC 为准 |
| 事件 topic | §5.1 列 12 独立 topic | §1.4 列 4 聚合 topic | ⚠️ | 待 ISSUE-002 仲裁;建议以 4 聚合 topic 为准 |
| 错误码 | §6.2 列 8 个 | §1.5 列 8 个 | ✅ | 一致 |
| 健康检查 | §6.6 /readyz 多依赖 | §1.6 三依赖 | ✅ | 一致 |
### 5.2 与 matrix.md 对齐
| 维度 | matrix.md | contract.md本文件 | 一致性 | 备注 |
| ------------- | -------------------------------- | ------------------------------ | ------ | ------------------------------------------ |
| gRPC RPC 总数 | §2 列 18 RPC | §1.1 列 21 RPC | ⚠️ | 待 ISSUE-004 仲裁后同步 matrix.md |
| Kafka topic | §4 列 2 topickp + question | §1.4 列 4 topic | ⚠️ | 待 ISSUE-003 仲裁后同步 matrix.md |
| 错误码前缀 | §6 列 CONTENT_ | §1.5 列 CONTENT_ | ✅ | 一致 |
| 端口 | §2 列 50054 | §1.1 列 50054 | ✅ | 一致 |
### 5.3 与 proto 文件对齐
| 文件 | 当前状态 | 目标状态P4 完成) |
| ------------- | ---------------------------------------------------------------- | ------------------------------------------------ |
| content.proto | 5 RPCTextbookService 3 + KnowledgeGraphService 2无 Chapter/Question | 21 RPC4 Service 完整) |
| events.proto | 4 messageClassEvent/ExamEvent/HomeworkEvent/GradeEvent | 8 message+ TextbookEvent/ChapterEvent/KnowledgePointEvent/QuestionEvent |
---
## §6 变更记录
| 日期 | 版本 | 变更内容 | 变更人 |
| ---------- | ---- | ---------------------------------------------------------------------------------------------- | ------ |
| 2026-07-09 | v1.0 | 初版18 RPC2 topic | coord |
| 2026-07-10 | v1.1 | ai09 复审:补全至 21 RPC+ TextbookService Update/Delete + ChapterService Update/Delete + QuestionService Publish/Search补全至 4 topic+ Textbook/Chapter 事件);补 events.proto 4 message 草案;补 §0 状态总览 / §5 一致性核查 / §6 变更记录;明确 REST 端点为管理面(非下游契约) | ai09 |

View File

@@ -1,59 +1,151 @@
# core-edu 对接契约
> 负责人ai08
> 关联:[matrix.md](./matrix.md)、[core_edu.proto](../../../packages/shared-proto/proto/core_edu.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 关联:[matrix.md](./matrix.md)、[core_edu.proto](../../../packages/shared-proto/proto/core_edu.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[services/core-edu/docs/02-architecture-design.md](../../../services/core-edu/docs/02-architecture-design.md)、[objections/core-edu_issue.md](../objections/core-edu_issue.md)
> 状态说明:本契约按 P3 目标态描述。P2 已就绪部分标注 ✅P3 待实施部分标注 ⏳(依赖 [objections/core-edu_issue.md](../objections/core-edu_issue.md) ISSUE-001 ~ ISSUE-006 仲裁结果)
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有
### 1.1 gRPC 接口(P3 启用,端口 50053
| Service | RPC | 请求 | 响应 | 端口 |
| ----------------- | ----------------------- | ------------------------------ | --------------------------- | ----- |
| ClassService | GetClass | GetClassRequest | ClassInfo | 50053 |
| ClassService | GetClassesByTeacher | GetClassesByTeacherRequest | GetClassesByTeacherResponse | 50053 |
| ClassService | BatchGetClasses | BatchGetClassesRequest | BatchGetClassesResponse | 50053 |
| ClassService | ListStudentsByClass | ListStudentsByClassRequest | ListStudentsByClassResponse | 50053 |
| ExamService | CreateExam | CreateExamRequest | CreateExamResponse | 50053 |
| ExamService | GetExam | GetExamRequest | Exam | 50053 |
| ExamService | ListExamsByClass | ListExamsByClassRequest | ListExamsResponse | 50053 |
| ExamService | UpdateExam | UpdateExamRequest | UpdateExamResponse | 50053 |
| ExamService | DeleteExam | DeleteExamRequest | DeleteExamResponse | 50053 |
| HomeworkService | AssignHomework | AssignHomeworkRequest | AssignHomeworkResponse | 50053 |
| HomeworkService | GetHomework | GetHomeworkRequest | Homework | 50053 |
| HomeworkService | ListHomeworkByClass | ListHomeworkByClassRequest | ListHomeworkResponse | 50053 |
| HomeworkService | SubmitHomework | SubmitHomeworkRequest | SubmitHomeworkResponse | 50053 |
| GradeService | RecordGrade | RecordGradeRequest | RecordGradeResponse | 50053 |
| GradeService | GetGrade | GetGradeRequest | Grade | 50053 |
| GradeService | ListGradesByStudent | ListGradesByStudentRequest | ListGradesResponse | 50053 |
| GradeService | ListGradesByExam | ListGradesByExamRequest | ListGradesResponse | 50053 |
| GradeService | ListGradesByHomework | ListGradesByHomeworkRequest | ListGradesResponse | 50053 |
| AttendanceService | RecordAttendance | RecordAttendanceRequest | RecordAttendanceResponse | 50053 |
| AttendanceService | GetAttendance | GetAttendanceRequest | Attendance | 50053 |
| AttendanceService | ListAttendanceByStudent | ListAttendanceByStudentRequest | ListAttendanceResponse | 50053 |
| AttendanceService | ListAttendanceByClass | ListAttendanceByClassRequest | ListAttendanceResponse | 50053 |
> 包名:`next_edu_cloud.core_edu.v1`[coord-cross-review §2.1](../../coord-cross-review.md) 仲裁)
> 总计 5 Service / 27 RPC含 P3 新增 5 RPC
> 注:当前 `core_edu.proto` 实际仅 3 Service / 14 RPC缺 ClassService + AttendanceService + P3 新增 RPC见 [ISSUE-001](../objections/core-edu_issue.md#issue-001-ai08core_eduproto-实际状态与-coord-仲裁声称不一致p0)
### 1.2 HTTP 端点(如有)
| Service | RPC | 请求 | 响应 | 状态 |
| ------- | --- | ---- | ---- | ---- |
| ClassService | GetClass | GetClassRequest | ClassInfo | ⏳ P3 |
| ClassService | GetClassesByTeacher | GetClassesByTeacherRequest | GetClassesByTeacherResponse | ⏳ P3 |
| ClassService | BatchGetClasses | BatchGetClassesRequest | BatchGetClassesResponse | ⏳ P3 |
| ClassService | ListStudentsByClass | ListStudentsByClassRequest | ListStudentsByClassResponse | ⏳ P3 |
| ExamService | CreateExam | CreateExamRequest | CreateExamResponse | ⏳ proto 已定义 |
| ExamService | GetExam | GetExamRequest | Exam | ⏳ proto 已定义 |
| ExamService | ListExamsByClass | ListExamsByClassRequest | ListExamsResponse | ⏳ proto 已定义 |
| ExamService | UpdateExam | UpdateExamRequest | UpdateExamResponse | ⏳ proto 已定义 |
| ExamService | DeleteExam | DeleteExamRequest | DeleteExamResponse | ⏳ proto 已定义 |
| ExamService | **PublishExam** | PublishExamRequest | PublishExamResponse | ⏳ P3 新增proto 待补) |
| ExamService | **SubmitExam** | SubmitExamRequest含 answers | SubmitExamResponse | ⏳ P3 新增proto 待补) |
| ExamService | **GradeExam** | GradeExamRequest | GradeExamResponse | ⏳ P3 新增proto 待补) |
| HomeworkService | AssignHomework | AssignHomeworkRequest | AssignHomeworkResponse | ⏳ proto 已定义 |
| HomeworkService | GetHomework | GetHomeworkRequest | Homework | ⏳ proto 已定义 |
| HomeworkService | ListHomeworkByClass | ListHomeworkByClassRequest | ListHomeworkResponse | ⏳ proto 已定义 |
| HomeworkService | SubmitHomework | SubmitHomeworkRequest**P3 增强含 answers** | SubmitHomeworkResponse | ⏳ proto 已定义(缺 answers 字段) |
| HomeworkService | **GradeHomework** | GradeHomeworkRequest | GradeHomeworkResponse | ⏳ P3 新增proto 待补) |
| GradeService | RecordGrade | RecordGradeRequest | RecordGradeResponse | ⏳ proto 已定义 |
| GradeService | GetGrade | GetGradeRequest | Grade | ⏳ proto 已定义 |
| GradeService | ListGradesByStudent | ListGradesByStudentRequest | ListGradesResponse | ⏳ proto 已定义 |
| GradeService | ListGradesByExam | ListGradesByExamRequest | ListGradesResponse | ⏳ proto 已定义 |
| GradeService | ListGradesByHomework | ListGradesByHomeworkRequest | ListGradesResponse | ⏳ proto 已定义 |
| GradeService | **UpdateGrade** | UpdateGradeRequest | UpdateGradeResponse | ⏳ P3 新增proto 待补) |
| AttendanceService | **RecordAttendance** | RecordAttendanceRequest | RecordAttendanceResponse | ⏳ P3 新增proto 待补) |
| AttendanceService | **GetAttendance** | GetAttendanceRequest | Attendance | ⏳ P3 新增proto 待补) |
| AttendanceService | **ListAttendanceByStudent** | ListAttendanceByStudentRequest | ListAttendanceResponse | ⏳ P3 新增proto 待补) |
| AttendanceService | **ListAttendanceByClass** | ListAttendanceByClassRequest | ListAttendanceResponse | ⏳ P3 新增proto 待补) |
| HealthService | Check | grpc.health.v1.HealthCheckRequest | grpc.health.v1.HealthCheckResponse | ⏳ P3 |
无对外 HTTP 端点,仅 gRPC。
**统计**
- P2 基线proto 已定义3 Service / 14 RPCExamService 5 + HomeworkService 4 + GradeService 5
- P3 目标态5 Service / 27 RPC+ClassService 4 + AttendanceService 4 + P3 新增 5 RPC
- 增量13 RPCClassService 4 + AttendanceService 4 + PublishExam/SubmitExam/GradeExam/GradeHomework/UpdateGrade 5
### 1.2 HTTP 端点REST端口 3004
> 当前为 REST 入口P2 已就绪P3 启用 gRPC 后 REST 保留为 BFF 兼容入口
> 所有端点走 AuthMiddleware + PermissionGuard + Zod ValidationPipe + GlobalErrorFilterActionState 信封)
| Method | Path | 权限 | 状态 |
| ------ | ---- | ---- | ---- |
| POST | /exams | CORE_EDU_EXAM_CREATE | ✅ P2 |
| GET | /exams/:id | CORE_EDU_EXAM_READ | ✅ P2 |
| GET | /exams/class/:classId | CORE_EDU_EXAM_READ | ✅ P2 |
| PUT | /exams/:id | CORE_EDU_EXAM_UPDATE | ✅ P2 |
| DELETE | /exams/:id | CORE_EDU_EXAM_DELETE | ✅ P2 |
| POST | /exams/:id/publish | CORE_EDU_EXAM_PUBLISH | ⏳ P3 新增 |
| POST | /exams/:id/start | CORE_EDU_EXAM_SUBMIT | ⏳ P3 新增 |
| POST | /exams/:id/submit | CORE_EDU_EXAM_SUBMIT | ⏳ P3 新增 |
| POST | /exams/:id/grade | CORE_EDU_EXAM_GRADE | ⏳ P3 新增 |
| POST | /exams/:id/archive | CORE_EDU_EXAM_UPDATE | ⏳ P3 新增 |
| POST | /homework | CORE_EDU_HOMEWORK_CREATE | ✅ P2 |
| GET | /homework/:id | CORE_EDU_HOMEWORK_READ | ✅ P2 |
| GET | /homework/class/:classId | CORE_EDU_HOMEWORK_READ | ✅ P2 |
| POST | /homework/:id/submit | CORE_EDU_HOMEWORK_SUBMIT | ⏳ P3 增强(含 answers |
| POST | /homework/:id/grade | CORE_EDU_HOMEWORK_GRADE | ⏳ P3 新增 |
| POST | /grades | CORE_EDU_GRADE_CREATE | ✅ P2 |
| GET | /grades/:id | CORE_EDU_GRADE_READ | ✅ P2 |
| GET | /grades/student/:studentId | CORE_EDU_GRADE_READ | ✅ P2 |
| GET | /grades/exam/:examId | CORE_EDU_GRADE_READ | ✅ P2 |
| GET | /grades/homework/:homeworkId | CORE_EDU_GRADE_READ | ✅ P2 |
| PUT | /grades/:id | CORE_EDU_GRADE_UPDATE | ⏳ P3 新增 |
| POST | /attendance | CORE_EDU_ATTENDANCE_CREATE | ⏳ P3 新增 |
| GET | /attendance/schedule/:scheduleId | CORE_EDU_ATTENDANCE_READ | ⏳ P3 新增 |
| GET | /attendance/student/:studentId | CORE_EDU_ATTENDANCE_READ | ⏳ P3 新增 |
| POST | /courses | CORE_EDU_COURSE_CREATE | ⏳ P3 新增 |
| POST | /schedules | CORE_EDU_SCHEDULE_CREATE | ⏳ P3 新增 |
| GET | /schedules/teacher/:teacherId | CORE_EDU_SCHEDULE_READ | ⏳ P3 新增 |
| GET | /schedules/class/:classId | CORE_EDU_SCHEDULE_READ | ⏳ P3 新增 |
| GET | /healthz | 无liveness | ✅ P2 |
| GET | /readyz | 无readiness | ✅ P2P3 补 Redis/Kafka 探针) |
| GET | /metrics | 无Prometheus | ✅ P2 |
### 1.3 GraphQL schema如 BFF
不适用。
不适用。core-edu 是业务服务,不提供 GraphQL。GraphQL 由 teacher-bff / student-bff / parent-bff 提供。
### 1.4 Kafka 事件发布(如有)
### 1.4 Kafka 事件发布
| Topic | Event | 消费方 |
| ------------------- | -------------------------------------------------- | -------------- |
| edu.exam.events | ExamEventaction: created/updated/deleted | msg / data-ana |
| edu.homework.events | HomeworkEventaction: assigned/submitted/graded | msg / data-ana |
| edu.grade.events | GradeEventaction: recorded/updated | msg / data-ana |
| edu.class.events | ClassEventaction: transferred | msg / data-ana |
> Topic 命名遵循 [coord-cross-review §3.1](../../coord-cross-review.md#31-core-edu-教学事件-topic-命名双轨p0-最严重) 仲裁:`edu.teaching.<aggregate>.<action>`
> 所有事件 payload 必须含:`schema_version`(默认 `"v1"`+ `event_id`UUID+ `occurred_at`(业务时间戳)+ `metadata: { traceId, userId }`
> 注:当前 `events.proto` 仍用旧 topic 命名 + 缺 schema_version 字段 + 缺 AttendanceEvent message见 [ISSUE-002](../objections/core-edu_issue.md#issue-002-ai08eventsproto-未同步-coord-topic-命名仲裁p0)
### 1.5 错误码前缀
| Topic | Eventaction | 触发时机 | 消费方 | 状态 |
| ----- | --------------- | -------- | ------ | ---- |
| `edu.teaching.exam.created` | ExamEvent.created | CreateExam 事务内 | msg / data-ana / push-gateway | ⏳ P3TOPIC_MAP 改名) |
| `edu.teaching.exam.updated` | ExamEvent.updated | UpdateExam | msg | ⏳ P3 |
| `edu.teaching.exam.deleted` | ExamEvent.deleted | DeleteExam软删除 | data-ana | ⏳ P3 |
| `edu.teaching.exam.published` | ExamEvent.published | PublishExam状态机转换 | msg / data-ana | ⏳ P3 新增 |
| `edu.teaching.exam.submitted` | ExamEvent.submitted | 学生提交答卷 | data-ana / msg | ⏳ P3 新增 |
| `edu.teaching.homework.assigned` | HomeworkEvent.assigned | AssignHomework | msg / data-ana | ⏳ P3 |
| `edu.teaching.homework.submitted` | HomeworkEvent.submitted | 学生提交作业 | data-ana / msg | ⏳ P3 |
| `edu.teaching.homework.graded` | HomeworkEvent.graded | 教师批改完成 | msg / data-ana | ⏳ P3 新增 |
| `edu.teaching.grade.recorded` | GradeEvent.recorded | RecordGrade | data-ana / msg / push-gateway / parent-bff | ⏳ P3 |
| `edu.teaching.grade.updated` | GradeEvent.updated | UpdateGrade | data-ana | ⏳ P3 新增 |
| `edu.teaching.attendance.recorded` | AttendanceEvent.recorded | RecordAttendance | data-ana / msg | ⏳ P3 新增proto 待补 AttendanceEvent |
| `edu.teaching.class.transferred` | ClassEvent.transferred | classes 合并后 | data-ana / msg | ⏳ P3topic 待 ISSUE-004 仲裁) |
`CORE_EDU_`(如 CORE_EDU_CLASS_NOT_FOUND、CORE_EDU_EXAM_CONFLICT
**注意**
- `class.transferred` 的 topic 命名存在跨文档不一致([ISSUE-004](../objections/core-edu_issue.md#issue-004-ai08classtransferred-事件-topic-三处不一致p1)ai08 倾向 `edu.teaching.class.transferred`,待 coord 仲裁。
- 当前 `events.proto` 文件头注释仍用旧命名(`edu.exam.events` 等),需 coord 同步更新。
### 1.5 CDC 数据流(被动同步,无主动接口)
> core-edu 不主动配合 CDC由 data-ana 通过 Debezium 监听 MySQL binlog 自动同步
| MySQL 表 | CDC topic | 消费方 | 用途 |
| -------- | --------- | ------ | ---- |
| core_edu_exams | `edu-cdc.next_edu_cloud.core_edu_exams` | data-ana | ClickHouse 宽表 |
| core_edu_homework | `edu-cdc.next_edu_cloud.core_edu_homework` | data-ana | ClickHouse 宽表 |
| core_edu_grades | `edu-cdc.next_edu_cloud.core_edu_grades` | data-ana | ClickHouse 宽表 |
| core_edu_attendance | `edu-cdc.next_edu_cloud.core_edu_attendance` | data-ana | ClickHouse 宽表 |
### 1.6 错误码前缀
`CORE_EDU_*`[coord-cross-review §5.5](../../coord-cross-review.md#55-p1-问题core-edu-子模块前缀) 仲裁:子域统一 `CORE_EDU_*`,不再细分 `EXAMS_`/`HOMEWORK_`/`GRADES_`
完整错误码清单见 [02-architecture-design.md §6.2](../../../services/core-edu/docs/02-architecture-design.md#62-错误码清单)。
### 1.7 响应信封
所有 HTTP/gRPC 响应遵循 ActionState 信封([004 §11.5](../../004_architecture_impact_map.md)
```typescript
{
success: boolean,
data?: T,
error?: { code: string, message: string, details?: unknown, traceId?: string }
}
```
---
@@ -61,18 +153,30 @@
### 2.1 gRPC 调用(同步)
无直接 gRPC 调用上游。core-edu 通过 Kafka 事件接收 iam 用户变更,不主动调 iam。
| 调用方 | 目标服务 | RPC | 用途 | 阶段 |
| ------ | -------- | --- | ---- | ---- |
| core-edu | content | ContentService.GetKnowledgePoints | 排课关联知识点lessons.knowledge_point_ids 校验) | P4content 就绪后) |
| core-edu | temporal | Workflow.startexamPublishWorkflow | 考试发布编排工作流 | P3Temporal 部署后) |
> core-edu **不主动调 iam gRPC**,通过 Kafka 事件接收 iam 用户变更(见 §2.2)。
> P3 不调 content题库 question_id 仅作外键引用,不校验存在性)。
### 2.2 Kafka 事件订阅(异步)
| Topic | Event | 发布方 | mock 策略 |
| ------------------- | --------------------------------------------------------- | ---------- | -------------------------------------------------------------------- |
| edu.iam.user.events | UserEventaction: created/updated/deleted/role_changed | iam (ai06) | iam 就绪前不订阅使用本地内置用户数据teacher_id/student_id 固定) |
| edu.iam.role.events | RoleEventaction: created/updated | iam (ai06) | iam 就绪前忽略,权限校验在 core-edu 内部 mock |
> Topic 命名遵循 [004 §7.2](../../004_architecture_impact_map.md#72-事件-topic-分类) 事件分类
| Topic | Event | 发布方 | 消费动作 | 幂等策略 | 阶段 |
| ----- | ----- | ------ | -------- | -------- | ---- |
| `edu.identity.user.created` | UserEvent.created | iam (ai06) | 初始化教师默认班级关联(写 core_edu_teacher_associations | 唯一索引 (teacher_id, class_id, subject_id) | P3 |
| `edu.identity.user.updated` | UserEvent.updated | iam (ai06) | 更新教师关联(角色变更时) | 基于用户事件序列号去重 | P3 |
| `edu.identity.user.deleted` | UserEvent.deleted | iam (ai06) | 软删除教师关联(保留历史成绩归属) | 基于用户 id 去重 | P3 |
| `edu.insight.mastery.updated` | MasteryEvent.updated | data-ana (ai11) | 接收学生掌握度,用于推荐个性化练习 | 基于 mastery_score_id 去重 | P4P3 可选) |
> 注:当前 `events.proto` 无 UserEvent message 定义IAM 事件可能在另一个 proto 文件ai08 在 P3 实施时确认 IAM 事件 proto 定义位置。
### 2.3 HTTP 调用(如有)
无。
无。core-edu 不主动发起 HTTP 调用。
---
@@ -80,39 +184,91 @@
### 3.1 我依赖的上游就绪标志
- [ ] iam gRPC 50052 启用ai06—— 用于用户身份一致性校验可选core-edu 可先独立运行)
- [ ] edu.iam.user.events topic 有事件发布ai06—— 用于同步用户缓存
| 依赖项 | 提供方 | 就绪信号 | 状态 |
| ------ | ------ | -------- | ---- |
| coord 仲裁 ISSUE-001 ~ ISSUE-006 | coord | coord.md 仲裁章节 | ⏳ 待仲裁 |
| core_edu.proto 补全 | coord 或 ai08 | 5 Service / 27 RPC 定义 | ⏳(见 [ISSUE-001](../objections/core-edu_issue.md) |
| events.proto 同步 | coord | 含 AttendanceEvent + schema_version + `edu.teaching.*` 注释 | ⏳(见 [ISSUE-002](../objections/core-edu_issue.md) |
| buf.gen.yaml gRPC 插件 | coord | `buf generate` 产出 TS gRPC 代码 | ⏳ |
| iam gRPC 50052 | ai06 | HealthService.Check = SERVING | ⏳(阻塞 P3.9 消费 IAM 事件core-edu 可先独立运行) |
| Redis 部署 | infra | redis:6379 可连接 | ⏳(阻塞 P3.7 分布式锁 + /readyz 探针) |
| Temporal server 部署 | infra | temporal:7233 可连接 | ⏳(阻塞 P3.10 工作流试点,可降级为纯事件驱动) |
| content gRPC 50054P4 | ai09 | HealthService.Check = SERVING | ⏳(阻塞 P4.2 知识点关联) |
| data-ana gRPC 50055P4 | ai11 | HealthService.Check = SERVING | ⏳(阻塞 P4.1 mastery 消费) |
| msg gRPC 50056P5 | ai10 | HealthService.Check = SERVING | ⏳(阻塞 P5.1 事件联调) |
### 3.2 我的就绪标志(供下游消费)
- [ ] core-edu gRPC 50053 启用HealthService.Check 返回 SERVING
- [ ] ClassService 4 RPC 可调用GetClass/GetClassesByTeacher/BatchGetClasses/ListStudentsByClass
- [ ] ExamService 5 RPC 可调用
- [ ] HomeworkService 4 RPC 可调用
- [ ] GradeService 5 RPC 可调用
- [ ] AttendanceService 4 RPC 可调用
- [ ] edu.exam.events / edu.homework.events / edu.grade.events / edu.class.events topic 可发布
| 阶段 | 就绪信号 | 消费方 | 状态 |
| ---- | -------- | ------ | ---- |
| P2已就绪 | HTTP 3004 可访问 + /healthz + /readyzDB 探针)+ REST CRUDexams/homework/grades+ Outbox | teacher-bffREST 调用) | ✅ |
| P3核心 | gRPC 50053 + 27 RPC + HealthService SERVING | teacher-bff / student-bff / parent-bff / ai | ⏳ |
| P3 子信号 1 | ClassService 4 RPC 可调用 | teacher-bff班级列表 | ⏳ |
| P3 子信号 2 | ExamService 8 RPC 可调用(含 PublishExam/SubmitExam/GradeExam | teacher-bff / student-bff / ai | ⏳ |
| P3 子信号 3 | HomeworkService 5 RPC 可调用(含 GradeHomework | teacher-bff / student-bff | ⏳ |
| P3 子信号 4 | GradeService 6 RPC 可调用(含 UpdateGrade | teacher-bff / student-bff / parent-bff | ⏳ |
| P3 子信号 5 | AttendanceService 4 RPC 可调用 | parent-bff | ⏳ |
| P3 子信号 6 | `edu.teaching.*` topic 可发布(含 attendance.recorded | msg / data-ana / push-gateway | ⏳ |
---
## §4 Mock 策略
### 4.1 我提供的 mock
### 4.1 我提供的 mock(供下游消费)
在 core-edu 真实服务就绪前为下游teacher-bff / student-bff / parent-bff / content / msg / data-ana提供以下 mock
- **gRPC mock**使用 grpc-mock 拦截 50053 端口
- ClassService.GetClassesByTeacher 返回固定 3 个 ClassInfo
- ClassService.ListStudentsByClass 返回固定 30 个 StudentInfo
- ExamService.ListExamsByClass 返回固定 2 个 Exam
- HomeworkService.ListHomeworkByClass 返回固定 3 个 Homework
- GradeService.ListGradesByStudent 返回固定 5 个 Grade
- AttendanceService.ListAttendanceByStudent 返回固定 10 条 Attendance
- **Kafka mock**core-edu 就绪前不发布真实事件,下游 data-ana/msg 使用本地 stub 事件
**gRPC mock**使用 grpc-mock 拦截 50053 端口
### 4.2 我消费的 mock
| Service | RPC | mock 返回 |
| ------- | --- | --------- |
| ClassService | GetClassesByTeacher | 固定 3 个 ClassInfo |
| ClassService | ListStudentsByClass | 固定 30 个 StudentInfo |
| ClassService | BatchGetClasses | 按 id 列表返回对应 ClassInfo |
| ExamService | ListExamsByClass | 固定 2 个 Exam |
| ExamService | GetExam | 固定 1 个 Exam含题目列表 |
| ExamService | CreateExam | 返回固定 examId |
| HomeworkService | ListHomeworkByClass | 固定 3 个 Homework |
| HomeworkService | SubmitHomework | 返回固定 submissionId |
| GradeService | ListGradesByStudent | 固定 5 个 Grade |
| GradeService | ListGradesByExam | 固定 30 个 Grade按班级学生数 |
| AttendanceService | ListAttendanceByStudent | 固定 10 条 Attendance |
| AttendanceService | RecordAttendance | 返回固定 attendanceId |
| HealthService | Check | 返回 SERVING |
**Kafka mock**core-edu 就绪前不发布真实事件):
- 下游 data-ana / msg 使用本地 stub 事件(固定 JSON payload含 schema_version/event_id/occurred_at/metadata
- stub 事件 JSON 文件位置:`services/core-edu/test/stubs/events/`ai08 P3.12 测试阶段产出)
### 4.2 我消费的 mock在真实上游就绪前
在真实 iam 就绪前core-edu 使用以下 mock
- 用户数据:内置固定 teacher_id / student_id不订阅 edu.iam.user.events
- 权限校验core-edu 内部不校验权限(由 Gateway/BFF 层负责),仅记录 created_by 字段
| 依赖 | mock 方式 | 切换真实时机 |
| ---- | --------- | ------------ |
| 用户数据 | 内置固定 teacher_id / student_id不订阅 `edu.identity.user.*` | iam gRPC 50052 就绪 + IAM 事件 topic 有事件发布 |
| 权限校验 | core-edu 内部不校验权限(由 Gateway/BFF 层负责),仅记录 created_by 字段 | iam 就绪后仍由 Gateway/BFF 负责core-edu 仅做 DataScope 下推 |
| content 知识点 | 不调用 ContentService.GetKnowledgePointslessons.knowledge_point_ids 仅存储不校验 | content gRPC 50054 就绪P4 |
| Temporal 工作流 | 考试发布降级为同步事件驱动(无工作流) | Temporal server 部署就绪 |
| Redis 分布式锁 | 降级为 DB SELECT FOR UPDATE性能下降但功能可用 | Redis 部署就绪 |
---
## §5 跨模块契约对齐状态ai08 核查)
> 核查日期2026-07-10
> 详细核查记录见 [objections/core-edu_issue.md §0](../objections/core-edu_issue.md#0-已有仲裁核查记录ai08-接管后核查)
| 待确认项 | coord 仲裁结论 | 核查状态 |
| -------- | -------------- | -------- |
| iam `user.created` 等事件 topic | `edu.identity.user.created` / `.updated` / `.deleted`004 §7.2 | ✅ 已仲裁core-edu P3 实现消费端 |
| core-edu 端口 3004 + gRPC 50053 | 不冲突,已纳入 coord 全局端口矩阵 | ✅ 已仲裁 |
| Kafka topic 命名 | 统一为 `edu.teaching.<aggregate>.<action>`coord §3.1 | ✅ 已仲裁core-edu P3 修 TOPIC_MAP |
| proto 包名 | `next_edu_cloud.core_edu.v1`coord §2.1 | ✅ 已仲裁 |
| 错误码前缀 | `CORE_EDU_*` 统一coord §5.5 | ✅ 已仲裁 |
| core_edu.proto 补 AttendanceService | coord 整改 #14 | ❌ 仲裁声称已补全实际未补ISSUE-001 |
| events.proto 同步 `edu.teaching.*` 命名 | coord §3.1 | ❌ 未同步ISSUE-002 |
| 考试/作业状态命名 | 未仲裁 | ❌ 跨模块不一致ISSUE-003 |
| class.transferred topic | 未仲裁 | ❌ 三处不一致ISSUE-004 |
| RPC 数量统计口径 | 未仲裁 | ❌ 三处不一致ISSUE-005 |
| 7 项设计决策 | 未仲裁 | ❌ 待提请ISSUE-006 |

View File

@@ -1,13 +1,15 @@
# data-ana 对接契约
> 负责人ai11
> 关联:[matrix.md](./matrix.md)、[analytics.proto](../../../packages/shared-proto/proto/analytics.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 关联:[matrix.md](../matrix.md)、[coord-cross-review.md](../../coord-cross-review.md)、[analytics.proto](../../../packages/shared-proto/proto/analytics.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)
> 对齐文档:[01-understanding.md](../../../services/data-ana/docs/01-understanding.md)、[02-architecture-design.md](../../../services/data-ana/docs/02-architecture-design.md)、[objections/data-ana_issue.md](../objections/data-ana_issue.md)、[worklines/data-ana_workline.md](../worklines/data-ana_workline.md)
> 修订v22026-07-10 by ai11—— 修正 topic 命名 / gRPC 调用声明 / HTTP 端点声明 / 引用断裂,对齐 01/02 v2.1
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 gRPC 接口
| Service | RPC | 请求 | 响应 | 端口 |
| ---------------- | ---------------------- | ----------------------------- | ------------------------- | ----- |
@@ -18,31 +20,71 @@
| AnalyticsService | GetStudentDashboard | GetStudentDashboardRequest | StudentDashboard | 50055 |
| AnalyticsService | GetParentDashboard | GetParentDashboardRequest | ParentDashboard | 50055 |
| AnalyticsService | GetAdminDashboard | GetAdminDashboardRequest | AdminDashboard | 50055 |
| AnalyticsService | GetWarningList | GetWarningListRequest | WarningListResponse | 50055 |
| AnalyticsService | GetWarnings | GetWarningsRequest | WarningList | 50055 |
| AnalyticsService | TriggerWarning | TriggerWarningRequest | TriggerWarningResponse | 50055 |
| AnalyticsService | GetMasteryDistribution | GetMasteryDistributionRequest | MasteryDistribution | 50055 |
| AnalyticsService | GetStudentMastery | GetStudentMasteryRequest | StudentMastery | 50055 |
| AnalyticsService | SubscribeMasteryUpdate | SubscribeMasteryUpdateRequest | stream MasteryUpdateEvent | 50055 |
### 1.2 HTTP 端点(如有)
> **proto 状态**analytics.proto 当前仅 3 RPCGetClassPerformance / GetStudentWeakness / GetLearningTrend扩展至 12 RPC 待 coord 补全或 ai11 在 P4 阶段自行补全(见 [ISSUE-003](../objections/data-ana_issue.md))。完整 message 定义见 [02-architecture-design.md §4.2](../../../services/data-ana/docs/02-architecture-design.md)。
> **gRPC 启用阶段**P4 启用coord-cross-review.md §2.3 裁决P2-P3 仅 HTTP。
> **响应信封**:所有 RPC 返回 ActionState[T]coord-cross-review.md §5.3 P0 整改)。
无对外 HTTP 端点,仅 gRPC含 1 个 Server Streaming RPC
### 1.2 HTTP 端点
data-ana 保留 HTTP :3006 端点作 Gateway 直连降级gRPC 不可用时 BFF 可走 HTTP。共 14 端点3 基础 + 11 业务):
| method | path | 权限 | 响应 |
| ------ | -------------------------------------------- | ----------------------------- | ----------------------------------- |
| GET | `/healthz` | — | `{status, service}` |
| GET | `/readyz` | — | `{status, ready, degraded, clickhouse, cdc_consumer, redis, iam_grpc}` |
| GET | `/metrics` | — | Prometheus 格式 |
| GET | `/analytics/class/{class_id}/performance` | `ANALYTICS_CLASS_READ` | `ActionState<ClassPerformanceData>` |
| GET | `/analytics/student/{student_id}/weakness` | `ANALYTICS_STUDENT_READ` | `ActionState<StudentWeaknessData>` |
| GET | `/analytics/student/{student_id}/errorbook` | `ANALYTICS_STUDENT_READ` | `ActionState<StudentErrorBookData>` |
| GET | `/analytics/student/{student_id}/trend` | `ANALYTICS_STUDENT_READ` | `ActionState<LearningTrendData>` |
| GET | `/analytics/student/{student_id}/attendance` | `ANALYTICS_STUDENT_READ` | `ActionState<AttendanceData>` |
| GET | `/analytics/dashboard/teacher/{user_id}` | `ANALYTICS_TEACHER_DASHBOARD` | `ActionState<TeacherDashboardData>` |
| GET | `/analytics/dashboard/student/{user_id}` | `ANALYTICS_STUDENT_DASHBOARD` | `ActionState<StudentDashboardData>` |
| GET | `/analytics/dashboard/parent/{user_id}` | `ANALYTICS_PARENT_DASHBOARD` | `ActionState<ParentDashboardData>` |
| GET | `/analytics/dashboard/admin/{user_id}` | `ANALYTICS_ADMIN_DASHBOARD` | `ActionState<AdminDashboardData>` |
| GET | `/analytics/warnings` | `ANALYTICS_WARNING_READ` | `ActionState<WarningListData>` |
| GET | `/analytics/mastery/distribution` | `ANALYTICS_CLASS_READ` | `ActionState<MasteryDistributionData>` |
> 完整端点设计见 [02-architecture-design.md §4.1](../../../services/data-ana/docs/02-architecture-design.md)。
### 1.3 GraphQL schema如 BFF
不适用。
不适用。data-ana 是业务服务,不提供 GraphQL。BFF 层teacher-bff / student-bff / parent-bff聚合 data-ana gRPC 后对外暴露 GraphQL。
### 1.4 Kafka 事件发布(如有)
### 1.4 Kafka 事件发布
| Topic | Event | 消费方 |
| --------------------------- | --------------------------------------------------------- | -------------- |
| edu.data_ana.mastery.events | MasteryEventaction: mastery.updated/warning.triggered | core-edu / msg |
| Topic | Event | 消费方 | Outbox | 说明 |
| -------------------------------- | --------------- | ------------------------------- | ------ | ---- |
| `edu.insight.mastery.updated` | MasteryUpdated | core-edu推荐个性化练习/ msg | ❌ 豁免 | 掌握度计算完成触发 |
| `edu.insight.warning.triggered` | WarningTriggered | msg推送通知/ core-edu标记关注 | ❌ 豁免 | 预警阈值触发 |
> MasteryEvent 豁免 Outbox 模式(派生数据事件,见 004 §12.2 + §15.3 #6
> **Outbox 豁免**派生数据事件(非业务事务写),豁免 Outbox 约束([coord-cross-review.md §3.3](../../coord-cross-review.md) 已仲裁)。直接用 aiokafka AIOKafkaProducer 发布,`idempotent=true` + `transactional_id="data-ana-producer"`
>
> **topic 命名说明**:按 004 §7.2 命名规范 `edu.<domain>.<aggregate>.<action>`data-ana 属于 D6 智能洞察领域domain=insight故 `edu.insight.mastery.updated` 符合规范。matrix.md §4 中 `edu.analytics.mastery` 命名不符合规范(缺 action 层级domain 用了服务名而非领域名),已提请 coord 统一(见 [ISSUE-005](../objections/data-ana_issue.md))。
### 1.5 错误码前缀
`DATA_ANA_`(如 DATA_ANA_DASHBOARD_UNAVAILABLE、DATA_ANA_MASTERY_NOT_COMPUTED
`DATA_ANA_`(如 DATA_ANA_DASHBOARD_UNAVAILABLE、DATA_ANA_MASTERY_NOT_COMPUTED、DATA_ANA_CLICKHOUSE_UNAVAILABLE
> 来源:[matrix.md §6](../matrix.md) 错误码前缀矩阵。
### 1.6 ClickHouse 宽表(供 coord 统一管理 DDL
| 宽表名 | 用途 | 引擎 |
| ------------------------- | -------------------- | ----------------------------- |
| student_dashboard_view | 学生学情宽表 | ReplacingMergeTree(last_updated) |
| student_errors | 学生错题本 | ReplacingMergeTree(last_error_time) |
| mastery_snapshot | 知识点掌握度历史快照 | MergeTree |
| attendance_logs | 学生考勤记录 | ReplacingMergeTree(occurred_at) |
| ai_usage_log | AI 用量计费记录 | ReplacingMergeTree(occurred_at) |
> DDL 由 coord 统一管理在 `infra/clickhouse/ddl/`(待 coord 建立data-ana 提供内容。完整 DDL 见 [02-architecture-design.md §3](../../../services/data-ana/docs/02-architecture-design.md)。
---
@@ -50,29 +92,43 @@
### 2.1 gRPC 调用(同步)
无主动 gRPC 调用上游。data-ana 通过 CDC + Kafka 事件接收数据,计算后发布 MasteryEvent。
| 调用方 | 被调用方 | RPC | 用途 | 端口 | 阶段 | 状态 |
| -------- | -------- | ------------------------- | ---------------------------- | ----- | ---- | ---- |
| data-ana | iam | GetEffectiveDataScope | DataScope 6 级过滤解析 | 50052 | P4 | ⚠️ iam.proto 当前未实现ISSUE-001 |
> **降级兜底**iam GetEffectiveDataScope 未就绪时data-ana 按 role 映射默认 DataScope教师=CLASS学生=SELF管理员=SCHOOL标注 `details.degraded: true`。结果 Redis 缓存 5minkey: `data_ana:datascope:{user_id}`)。
>
> **裁决依据**coord-cross-review.md §2.2 #3 已仲裁 iam P4 补全此 RPC。
### 2.2 Kafka 事件订阅(异步)
| Topic | Event | 发布方 | mock 策略 |
| ---------------------------------- | ------------------- | --------------- | ------------------------------------------------- |
| edu.exam.events | ExamEvent | core-edu (ai08) | core-edu 就绪前使用 CDC 模拟数据 + 本地 stub 事件 |
| edu.homework.events | HomeworkEvent | core-edu (ai08) | 同上 |
| edu.grade.events | GradeEvent | core-edu (ai08) | 同上 |
| edu.class.events | ClassEvent | core-edu (ai08) | 同上 |
| edu.content.knowledge_point.events | KnowledgePointEvent | content (ai09) | content 就绪前使用内置知识点维度表 |
| edu.content.question.events | QuestionEvent | content (ai09) | content 就绪前忽略 |
| edu.ai.usage.events | AIUsageEvent | ai (ai12) | ai 就绪前忽略AI 用量统计为空 |
| Topic | Event | 发布方 | mock 策略 |
| -------------------------------------- | ------------------- | --------------- | ------------------------------------------------- |
| `edu.teaching.exam.published` | ExamEvent | core-edu (ai08) | core-edu 就绪前使用 CDC 模拟数据 + 本地 stub 事件 |
| `edu.teaching.homework.assigned` | HomeworkEvent | core-edu (ai08) | 同上 |
| `edu.teaching.grade.recorded` | GradeEvent | core-edu (ai08) | 同上 |
| `edu.teaching.class.transferred` | ClassEvent | core-edu (ai08) | 同上 |
| `edu.content.kp.events` | KnowledgePointEvent | content (ai09) | content 就绪前使用内置知识点维度表 |
| `edu.content.question.events` | QuestionEvent | content (ai09) | content 就绪前忽略 |
| `edu.insight.ai.usage` | AIUsageEvent | ai (ai12) | ai 就绪前忽略AI 用量统计为空P5 |
> **topic 命名对齐**core-edu 教学事件 topic 按 coord-cross-review.md §3.1 裁决统一为 `edu.teaching.<aggregate>.<action>` 风格。core-edu 代码实际发布 `edu.exam.events` 等,待 core-edu 整改后切换。
>
> **AIUsageEvent 缺失**events.proto 当前缺 AIUsageEvent messageISSUE-002P5 前需 coord 补全。
### 2.3 HTTP 调用(如有)
无。
无。data-ana 不通过 HTTP 调用上游服务。
### 2.4 CDC 数据源(补充)
### 2.4 CDC 数据源
| 数据源 | 用途 | mock 策略 |
| ----------------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------- |
| core-edu MySQLexams/homework/grades/attendance 表) | Debezium CDC → Kafka 同步读模型 | core-edu 就绪前使用 ClickHouse 内置模拟数据集30 学生 × 5 考试 × 10 作业) |
| content MySQLknowledge_points 表) | Debezium CDC → 知识点元数据同步 | content 就绪前使用内置固定知识点表(数学 50 个知识点) |
| iam MySQLusers 表) | Debezium CDC → 用户 dataScope 同步 | iam 就绪前使用硬编码 DataScope 降级 |
> **CDC topic 命名**`edu-cdc.next_edu_cloud.<table>`coord-cross-review.md §3.2 裁决补登 CDC topic 命名规范段,待 coord 在 004 §7.2 落实)。
---
@@ -81,17 +137,23 @@
### 3.1 我依赖的上游就绪标志
- [ ] core-edu gRPC 50053 启用ai08—— 业务事件 + CDC 数据源
- [ ] edu.exam.events / edu.homework.events / edu.grade.events / edu.class.events topic 有事件发布ai08
- [ ] content gRPC 50054 启用ai09—— 知识点维度
- [ ] edu.content.knowledge_point.events topic 有事件发布ai09
- [ ] ai gRPC 50057 启用ai12—— AI 用量统计(可选,仪表盘补全
- [ ] core-edu MySQL Debezium CDC 配置ai08 + SRE—— CDC 通道前提
- [ ] `edu.teaching.exam.published` / `edu.teaching.homework.assigned` / `edu.teaching.grade.recorded` / `edu.teaching.class.transferred` topic 有事件发布ai08
- [ ] content gRPC 50054 启用ai09—— 知识点维度 CDC
- [ ] `edu.content.kp.events` topic 有事件发布ai09
- [ ] iam.proto 补全 GetEffectiveDataScope RPCai06/coordISSUE-001—— P4 阻塞项
- [ ] analytics.proto 扩展至 12 RPCcoord/ai11ISSUE-003—— gRPC stub 生成前提
- [ ] events.proto 补全 AIUsageEvent messagecoordISSUE-002—— P5 前补全
- [ ] ai gRPC 50057 启用ai12—— AI 用量统计P5可选
### 3.2 我的就绪标志(供下游消费)
- [ ] data-ana gRPC 50055 启用HealthService.Check 返回 SERVING
- [ ] AnalyticsService 12 RPC 可调用(含 4 端 Dashboard + Server Streaming SubscribeMasteryUpdate
- [ ] GetTeacherDashboard / GetStudentDashboard / GetParentDashboard / GetAdminDashboard 返回结构化数据
- [ ] edu.data_ana.mastery.events topic 可发布mastery.updated / warning.triggered
- [ ] **P4 就绪**data-ana gRPC 50055 启用HealthService.Check 返回 SERVING
- [ ] **P4 就绪**AnalyticsService 12 RPC 可调用(含 4 端 Dashboard + Warning + Mastery + Server Streaming SubscribeMasteryUpdate
- [ ] **P4 就绪**GetTeacherDashboard / GetStudentDashboard / GetParentDashboard / GetAdminDashboard 返回结构化数据
- [ ] **P4 就绪**`edu.insight.mastery.updated` topic 可发布mastery.updated / warning.triggered
- [ ] **P5 就绪**SubscribeMasteryUpdate server-streaming RPC 可订阅
- [ ] **P6 就绪**CDC 多实例水平扩展 + ExamCache Redis 化完成
---
@@ -106,16 +168,17 @@
- GetStudentDashboard 返回固定仪表盘avg_score=85.0, class_rank=5, weak_points 3 个)
- GetParentDashboard 返回固定仪表盘child_avg_score=85.0, child_class_rank=5
- GetAdminDashboard 返回固定仪表盘total_teachers=50, total_students=1200, school_avg_score=80.0
- GetWarningList 返回固定 5 条预警severity: warning/critical
- GetWarnings 返回固定 5 条预警severity: warning/critical
- GetMasteryDistribution 返回固定分布mastered=20, progressing=7, weak=3
- SubscribeMasteryUpdate 返回固定流(每 5 秒推 1 个 MasteryUpdateEvent
- **Kafka mock**data-ana 就绪前不发布真实 MasteryEventmsg 使用本地 stub 预警
- **Kafka mock**data-ana 就绪前不发布真实 MasteryUpdated / WarningTriggeredmsg 使用本地 stub 预警
### 4.2 我消费的 mock
在真实上游就绪前data-ana 使用以下 mock
- 业务数据ClickHouse 内置模拟数据集30 学生 × 5 考试 × 10 作业 × 30 天出勤),不依赖 core-edu CDC
- 知识点维度:内置固定知识点表(数学 50 个知识点),不依赖 content 事件
- AI 用量AIUsageEvent 为空,仪表盘 AI 用量区块显示"暂无数据"
- CDC 通道core-edu 就绪前 Debezium 不启动,使用 ClickHouse 批量导入模拟数据
- **业务数据**ClickHouse 内置模拟数据集30 学生 × 5 考试 × 10 作业 × 30 天出勤),不依赖 core-edu CDC
- **知识点维度**:内置固定知识点表(数学 50 个知识点),不依赖 content 事件
- **AI 用量**AIUsageEvent 为空,仪表盘 AI 用量区块显示"暂无数据"
- **CDC 通道**core-edu 就绪前 Debezium 不启动,使用 ClickHouse 批量导入模拟数据
- **DataScope**iam GetEffectiveDataScope 未就绪前,按 role 映射默认 DataScope 降级(教师=CLASS学生=SELF管理员=SCHOOL标注 `details.degraded: true`

View File

@@ -2,35 +2,57 @@
> 负责人ai06
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 裁决依据:[coord-final-decisions](../../coord-final-decisions.md) I1-I8、[president-final-rulings](../../president-final-rulings.md) §2.15/§2.16/§5.5
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
> **双入口策略**president §2.16REST 供 gateway 透传 + admin-portal 直连gRPC 供 BFF 聚合调用。同一 Application Service 同时被 REST Controller + gRPC Controller 调用业务逻辑不重复。gateway 保持 HTTP 透传(不改为 gRPC 客户端)。
| Service | RPC | 请求 | 响应 | 端口 |
| ---------- | ----------------------- | ------------------------------ | ---------------------------- | ----- |
| IamService | Register | RegisterRequest | AuthResponse | 50052 |
| IamService | Login | LoginRequest | AuthResponse | 50052 |
| IamService | RefreshToken | RefreshTokenRequest | TokenPair | 50052 |
| IamService | Logout | LogoutRequest | LogoutResponse | 50052 |
| IamService | GetUserInfo | GetUserInfoRequest | UserInfo | 50052 |
| IamService | BatchGetUsers | BatchGetUsersRequest | BatchGetUsersResponse | 50052 |
| IamService | GetEffectivePermissions | GetEffectivePermissionsRequest | EffectivePermissionsResponse | 50052 |
| IamService | GetEffectiveAccess | GetEffectiveAccessRequest | EffectiveAccessResponse | 50052 |
| IamService | GetEffectiveDataScope | GetEffectiveDataScopeRequest | DataScopeResponse | 50052 |
| IamService | GetViewports | GetViewportsRequest | ViewportsResponse | 50052 |
| IamService | GetPublicKey | GetPublicKeyRequest | PublicKeyResponse | 50052 |
| IamService | GetChildrenByParent | GetChildrenByParentRequest | ChildrenResponse | 50052 |
### 1.1 gRPC 接口(供 BFF 聚合调用)
### 1.2 HTTP 端点(如有)
> 端口 50052P2 即启用I1 裁决)。⚠️ 当前 iam.proto 仅 4 RPC待 coord 补全至 12 RPCISSUE-005
无对外 HTTP 端点,仅 gRPC。
| Service | RPC | 请求 | 响应 | 端口 | 消费方 |
| ---------- | ----------------------- | ------------------------------ | ---------------------------- | ----- | ---------------------------------------- |
| IamService | Register | RegisterRequest | AuthResponse | 50052 | api-gatewayREST 透传)/ admin-portal |
| IamService | Login | LoginRequest | AuthResponse | 50052 | api-gatewayREST 透传)/ admin-portal |
| IamService | RefreshToken | RefreshTokenRequest | TokenPair | 50052 | api-gatewayREST 透传) |
| IamService | Logout | LogoutRequest | LogoutResponse | 50052 | api-gatewayREST 透传) |
| IamService | GetUserInfo | GetUserInfoRequest | UserInfo | 50052 | teacher-bff / student-bff / parent-bff |
| IamService | BatchGetUsers | BatchGetUsersRequest | BatchGetUsersResponse | 50052 | teacher-bff / admin-portal |
| IamService | GetEffectivePermissions | GetEffectivePermissionsRequest | EffectivePermissionsResponse | 50052 | teacher-bff / student-bff / parent-bff |
| IamService | GetEffectiveAccess | GetEffectiveAccessRequest | EffectiveAccessResponse | 50052 | teacher-bff / student-bff / parent-bff |
| IamService | GetEffectiveDataScope | GetEffectiveDataScopeRequest | DataScopeResponse | 50052 | teacher-bff / data-ana |
| IamService | GetViewports | GetViewportsRequest | ViewportsResponse | 50052 | teacher-bff / student-bff / parent-bff |
| IamService | GetPublicKey | GetPublicKeyRequest | PublicKeyResponse | 50052 | api-gatewayJWKS 验签) |
| IamService | GetChildrenByParent | GetChildrenByParentRequest | ChildrenResponse | 50052 | parent-bff |
### 1.2 HTTP 端点(供 gateway 透传 + admin-portal 直连)
> REST 与 gRPC 双入口并存president §2.16。REST 路径统一加 `/v1` 前缀I7 裁决。gateway 路由 `/iam/v1/*` → iam 服务 `/v1/iam/*`(透传不改路径)。
| Method | Path | 权限 | 说明 | 消费方 |
| ------ | --------------------------------- | ----------------- | -------------------------------------- | -------------------- |
| POST | `/iam/v1/register` | 公开 | 注册 + 自动分配 teacher 角色 | api-gateway / admin |
| POST | `/iam/v1/login` | 公开 | 登录,返回 accessToken + refreshToken | api-gateway |
| POST | `/iam/v1/refresh` | 公开 | 刷新令牌(轮换 + 旧 token 黑名单) | api-gateway |
| POST | `/iam/v1/logout` | `IAM_USER_READ` | 登出refresh token 加黑名单) | api-gateway |
| GET | `/iam/v1/me` | `IAM_USER_READ` | 当前用户信息 | api-gateway / admin |
| GET | `/iam/v1/viewports` | `IAM_USER_READ` | 当前用户视口L1 导航) | api-gateway |
| GET | `/iam/v1/permissions/effective` | `IAM_USER_READ` | 当前用户有效权限I8 统一路径) | api-gateway |
| GET | `/iam/v1/.well-known/jwks.json` | 公开 | RS256 公钥 JWK SetGateway 拉取验签) | api-gateway |
| GET | `/iam/v1/children` | `IAM_USER_READ` | 当前家长的孩子列表I6 裁决) | api-gateway / parent |
| GET | `/iam/v1/roles` | `IAM_ROLE_MANAGE` | 角色列表(管理端) | admin-portal |
| GET | `/iam/v1/permissions` | `IAM_ROLE_MANAGE` | 权限点列表(管理端) | admin-portal |
| GET | `/healthz` | 无 | liveness | k8s / 监控 |
| GET | `/readyz` | 无 | readiness5 依赖检查) | k8s / 监控 |
| GET | `/metrics` | 无 | Prometheus 指标 | Prometheus |
### 1.3 GraphQL schema如 BFF
不适用。
不适用。iam 是业务服务,不暴露 GraphQLGraphQL 由 BFF 层提供)。
### 1.4 Kafka 事件发布(如有)
@@ -66,16 +88,23 @@
### 3.1 我依赖的上游就绪标志
无上游依赖。
iam 是身份根服务无业务上游依赖。但依赖以下基础设施契约coord 提供):
| 依赖项 | 提供方 | 就绪标志 | 状态 |
| ------ | ------ | -------- | ---- |
| iam.proto 补全至 12 RPC | coord | proto 含 12 RPC + 全部 message | ❌ 仅 4 RPCISSUE-005 |
| events.proto 补全 UserEvent/RoleEvent/AuditEvent | coord | proto 含 3 个 message | ❌ 缺失ISSUE-002 |
| shared-ts Outbox 工具包 | coord | outbox.service.ts 可导入 | ✅ 已就绪 |
### 3.2 我的就绪标志(供下游消费)
- [ ] iam gRPC 50052 启用HealthService.Check 返回 SERVING
- [ ] IamService.Register/Login/RefreshToken/Logout 可调用(返回 AuthResponse/TokenPair
- [ ] IamService 12 RPC 全部可调用(Register/Login/RefreshToken/Logout/GetUserInfo/BatchGetUsers/GetEffectivePermissions/GetEffectiveAccess/GetEffectiveDataScope/GetViewports/GetPublicKey/GetChildrenByParent
- [ ] IamService.GetPublicKey 可用(返回 RS256 PEM 公钥,供 api-gateway 验签)
- [ ] IamService.GetChildrenByParent 可用(供 parent-bff 查孩子列表)
- [ ] edu.iam.user.events / edu.iam.role.events / edu.iam.audit.created topic 可发布
- [ ] JWT RS256 签发链路打通access_token + refresh_token
- [ ] JWT RS256 签发链路打通access_token 15min + refresh_token 7day 轮换
- [ ] /iam/v1/* REST 端点可用(供 gateway 透传 + admin-portal 直连,双入口策略)
---
@@ -83,13 +112,15 @@
### 4.1 我提供的 mock
在 iam 真实服务就绪前为下游api-gateway / 各 BFF提供以下 mock
在 iam 真实服务就绪前为下游api-gateway / 各 BFF / admin-portal)提供以下 mock(双入口)
- **gRPC mock**:使用 grpc-mock 拦截 50052 端口Register/Login 返回固定 AuthResponseuser.id="mock-user-001", tokens.access_token="mock-access-token"
- **gRPC mock**(供 BFF:使用 grpc-mock 拦截 50052 端口Register/Login 返回固定 AuthResponseuser.id="mock-user-001", tokens.access_token="mock-access-token"
- **REST mock**(供 gateway / admin-portalMSW 或 express mock 拦截 /iam/v1/* 路径,返回与 gRPC mock 一致的结构
- **GetPublicKey mock**:返回固定 RS256 公钥 PEM与 mock 私钥配对),供 api-gateway 验签 mock JWT
- **GetChildrenByParent mock**:返回固定 ChildInfo 列表2 个孩子)
- **JWKS mock**GET /iam/v1/.well-known/jwks.json 返回固定 JWK Set
- **Kafka mock**iam 服务就绪前不发布真实事件,下游订阅方使用本地 stub
### 4.2 我消费的 mock
不适用(上游依赖)。
不适用(iam 是身份根服务,无业务上游依赖)。

View File

@@ -1,47 +1,95 @@
# msg 对接契约
> 负责人ai10
> 关联:[matrix.md](./matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 关联:[matrix.md](../matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[02-architecture-design.md](../../../services/msg/docs/02-architecture-design.md)
> 仲裁依赖ISSUE-008topic 命名、ISSUE-009RPC 数量、ISSUE-013events.proto 补齐)
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 gRPC 接口
| Service | RPC | 请求 | 响应 | 端口 |
| ----------------------------- | ---------------------- | ----------------------------- | --------------------------- | ----- |
| NotificationService | SendNotification | SendNotificationRequest | Notification | 50056 |
| NotificationService | ListNotifications | ListNotificationsRequest | ListNotificationsResponse | 50056 |
| NotificationService | MarkAsRead | MarkAsReadRequest | MarkAsReadResponse | 50056 |
| NotificationService | SearchNotifications | SearchNotificationsRequest | SearchNotificationsResponse | 50056 |
| NotificationService | RecallNotification | RecallNotificationRequest | RecallNotificationResponse | 50056 |
| NotificationPreferenceService | GetPreference | GetPreferenceRequest | NotificationPreference | 50056 |
| NotificationPreferenceService | UpdatePreference | UpdatePreferenceRequest | NotificationPreference | 50056 |
| NotificationPreferenceService | GetPreferenceByChannel | GetPreferenceByChannelRequest | ChannelPreference | 50056 |
| NotificationPreferenceService | ListPreferences | ListPreferencesRequest | ListPreferencesResponse | 50056 |
| NotificationTemplateService | CreateTemplate | CreateTemplateRequest | NotificationTemplate | 50056 |
| NotificationTemplateService | GetTemplate | GetTemplateRequest | NotificationTemplate | 50056 |
| NotificationTemplateService | ListTemplates | ListTemplatesRequest | ListTemplatesResponse | 50056 |
| NotificationTemplateService | RenderTemplate | RenderTemplateRequest | RenderedTemplate | 50056 |
> 端口50056 | proto 包名:`next_edu_cloud.msg.v1` | proto 文件:[msg.proto](../../../packages/shared-proto/proto/msg.proto)
>
> **⚠️ ISSUE-009 待仲裁**ai-allocation.md 规定 13 RPC02-architecture-design.md 设计 17 RPC。下表列出设计文档完整 17 RPC标注基线 13 RPC✅基线 / 扩展待仲裁。coord 裁决后裁剪或放宽。
### 1.2 HTTP 端点(如有
#### NotificationService9 RPC5 基线 + 4 扩展
无对外 HTTP 端点,仅 gRPC。
| RPC | 请求 | 响应 | 基线/扩展 | 说明 |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | --------- | ----------------- |
| SendNotification | `SendNotificationRequest{user_id, type, title, content, channel, metadata?, related_entity_type?, related_entity_id?}` | `Notification` | ✅基线 | 单条发送 |
| BatchSendNotification | `BatchSendNotificationRequest{items[], group_id?}` | `BatchSendNotificationResponse{ids[], failed[]}` | ➕扩展 | 批量发送 |
| ListNotifications | `ListNotificationsRequest{user_id, only_unread?, type?, page?, page_size?}` | `ListNotificationsResponse{notifications[], total}` | ✅基线 | 列表(补分页) |
| GetUnreadCount | `GetUnreadCountRequest{user_id}` | `GetUnreadCountResponse{count}` | ➕扩展 | 未读数Redis |
| MarkAsRead | `MarkAsReadRequest{id, user_id}` | `Empty` | ✅基线 | 标记已读 |
| BatchMarkAsRead | `BatchMarkAsReadRequest{ids[], user_id}` | `Empty` | ➕扩展 | 批量已读 |
| MarkAllAsRead | `MarkAllAsReadRequest{user_id, before?}` | `Empty` | ➕扩展 | 全部已读 |
| SearchNotifications | `SearchNotificationsRequest{user_id, query, type?, page?, page_size?}` | `SearchNotificationsResponse{notifications[], total}` | ✅基线 | ES 全文检索 |
| RecallNotification | `RecallNotificationRequest{group_id, reason?}` | `RecallNotificationResponse{recalled_count}` | ✅基线 | 撤回广播 |
### 1.3 GraphQL schema如 BFF
#### NotificationPreferenceService2 RPC 基线
不适用。
| RPC | 请求 | 响应 | 基线/扩展 | 说明 |
| ------------------ | -------------------------------------------------- | --------------------------------------- | --------- | -------- |
| GetPreferences | `GetPreferencesRequest{user_id}` | `GetPreferencesResponse{preferences[]}` | ✅基线 | 偏好查询 |
| UpdatePreferences | `UpdatePreferencesRequest{user_id, preferences[]}` | `Empty` | ✅基线 | 偏好更新 |
### 1.4 Kafka 事件发布(如有
> **注**02-architecture-design.md 设计 2 RPC原 contract 版本的 GetPreferenceByChannel / ListPreferences 暂移除(待 ISSUE-009 仲裁是否保留
| Topic | Event | 消费方 |
| --------------------------- | ------------------------------------------------------ | ----------------------- |
| edu.msg.notification.events | NotificationEventaction: sent/read/recalled/failed | push-gateway / data-ana |
#### NotificationTemplateService4 RPC4 基线 + 2 扩展)
| RPC | 请求 | 响应 | 基线/扩展 | 说明 |
| -------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------- | --------- | ---------- |
| CreateTemplate | `CreateTemplateRequest{code, type, title_template, content_template, default_channels, variables}` | `NotificationTemplate` | ✅基线 | 创建模板 |
| GetTemplate | `GetTemplateRequest{id}` | `NotificationTemplate` | ✅基线 | 查询模板 |
| ListTemplates | `ListTemplatesRequest{type?, status?}` | `ListTemplatesResponse{templates[]}` | ✅基线 | 模板列表 |
| UpdateTemplate | `UpdateTemplateRequest{id, ...}` | `NotificationTemplate` | ➕扩展 | 更新模板 |
| DeleteTemplate | `DeleteTemplateRequest{id}` | `Empty` | ➕扩展 | 删除模板 |
| RenderTemplate | `RenderTemplateRequest{code, variables, locale?}` | `RenderedNotification{title, content}` | ✅基线 | 渲染模板 |
> **汇总**:基线 13 RPC9 基线表中 ✅ × 5 + 偏好 ✅ × 2 + 模板 ✅ × 4 = 11... 实际基线 = NotificationService 5 + Preference 2 + Template 4 = 11。**注**:若严格按 ai-allocation 13 RPC基线应为 13此处设计文档 11 基线 + 6 扩展 = 17与 13 预算差 4。待 ISSUE-009 仲裁后最终确认。
### 1.2 HTTP 端点
> msg 对外以 gRPC 为主BFF 通过 gRPC 调用)。以下 REST 端点为过渡期/管理端使用,最终将逐步迁移至 gRPC。
| Method | Path | 权限 | 说明 |
| ------ | ---------------------------------------- | ----------------------- | -------------------- |
| POST | /notifications/send | MSG_NOTIFICATION_SEND | 单条发送 |
| POST | /notifications/batch | MSG_NOTIFICATION_SEND | 批量发送 |
| GET | /notifications/user/:userId | MSG_NOTIFICATION_READ | 用户通知列表 |
| GET | /notifications/user/:userId/unread-count | MSG_NOTIFICATION_READ | 未读数Redis |
| PUT | /notifications/:id/read | MSG_NOTIFICATION_READ | 标记已读 |
| GET | /notifications/search | MSG_NOTIFICATION_READ | ES 全文检索 |
| GET | /preferences/user/:userId | MSG_NOTIFICATION_READ | 用户偏好 |
| PUT | /preferences/user/:userId | MSG_NOTIFICATION_MANAGE | 更新偏好 |
| GET | /templates | MSG_NOTIFICATION_MANAGE | 模板列表 |
| POST | /templates | MSG_NOTIFICATION_MANAGE | 创建模板 |
### 1.3 GraphQL schema
不适用msg 是业务服务,非 BFF
### 1.4 Kafka 事件发布
> **⚠️ ISSUE-008 待仲裁**topic 命名存在三套约定per-event / aggregate / aggregate+action。本表采用 02-architecture-design.md §5.2 约定per-event topic与 004 §7.2 一致。coord 裁决后统一。
| Topic | Event Type | 触发时机 | 消费方 | Outbox |
| ---------------------------- | --------------------------- | -------------- | ------------------------------ | ------ |
| `edu.notification.sent` | NotificationSent | 通知发送成功 | push-gateway / data-ana | ✅ |
| `edu.notification.read` | NotificationRead | 通知被标记已读 | data-ana | ✅ |
| `edu.notification.recalled` | NotificationRecalled | 通知被撤回 | push-gateway删除已推送消息 | ✅ |
| `edu.notification.failed` | NotificationFailed | 通知投递失败 | data-ana监控告警 | ✅ |
> **Producer 幂等**`idempotent=true` + `transactionalId=msg-producer`
> **DLQ**:消费失败超 3 次投递 `edu.notification.dlq`(见 02-architecture-design.md §5待补充
### 1.5 错误码前缀
`MSG_`如 MSG_TEMPLATE_NOT_FOUND、MSG_CHANNEL_DISABLED、MSG_RATE_LIMITED
`MSG_`完整清单见 02-architecture-design.md §6.2
- MSG_VALIDATION_ERROR / MSG_NOT_FOUND / MSG_PERMISSION_DENIED / MSG_CONFLICT / MSG_BUSINESS_ERROR / MSG_DATABASE_ERROR / MSG_INTERNAL_ERROR当前已实现 7 个)
- MSG_ES_UNAVAILABLE / MSG_REDIS_UNAVAILABLE / MSG_PUSH_GATEWAY_UNAVAILABLE / MSG_TEMPLATE_NOT_FOUND / MSG_TEMPLATE_RENDER_ERROR / MSG_RATE_LIMIT_EXCEEDEDP5 新增 6 个)
---
@@ -49,22 +97,39 @@
### 2.1 gRPC 调用(同步)
无主动 gRPC 调用上游。msg 通过 Kafka 事件被动接收业务事件后触发通知。
| 调用方 | 对方服务 | 协议 | RPC | 用途 | 状态 |
| ------------ | ------------ | ---- | -------------------- | ---------------------- | ------------------ |
| msg → push | push-gateway | gRPC | PushService.Push | 实时推送通知到在线用户 | P5 待实现D5 |
> 当前降级fetch POST `/internal/push`(见 [notifications.service.ts](../../../services/msg/src/notifications/notifications.service.ts) L179
> **调用方向澄清**ISSUE-005004 §4.1 表述"push-gateway → Msg"有歧义,实际方向是 msg → push-gateway
### 2.2 Kafka 事件订阅(异步)
| Topic | Event | 发布方 | mock 策略 |
| --------------------------- | --------------------------------------------------------- | --------------- | ------------------------------------------------------- |
| edu.iam.user.events | UserEvent | iam (ai06) | iam 就绪前使用本地用户偏好默认值 |
| edu.exam.events | ExamEventaction: created/updated/deleted | core-edu (ai08) | core-edu 就绪前不订阅,使用本地 stub 事件触发 mock 通知 |
| edu.homework.events | HomeworkEventaction: assigned/submitted/graded | core-edu (ai08) | 同上 |
| edu.grade.events | GradeEventaction: recorded/updated | core-edu (ai08) | 同上 |
| edu.class.events | ClassEventaction: transferred | core-edu (ai08) | 同上 |
| edu.data_ana.mastery.events | MasteryEventaction: mastery.updated/warning.triggered | data-ana (ai11) | data-ana 就绪前不订阅,预警通知使用本地 stub |
> **⚠️ ISSUE-008 待仲裁**topic 命名采用 02-architecture-design.md §5.1 约定per-event topic与 004 §7.2 一致)。原 contract 版本的 aggregate topicedu.iam.user.events / edu.exam.events待 coord 裁决后统一。
### 2.3 HTTP 调用(如有)
| Topic | 来源服务 | 处理逻辑 | 触发通知 | 幂等键 | 阻塞性 |
| ----------------------------------- | ------------- | ---------------------------- | -------------------- | ---------- | ------ |
| `edu.identity.user.created` | iam (ai06) | 发送欢迎通知 | welcome 通知 | `event_id` | 🟡 |
| `edu.identity.user.updated` | iam (ai06) | 用户信息变更,清理缓存 | — | `event_id` | 🟡 |
| `edu.identity.user.deleted` | iam (ai06) | 用户删除,归档通知 | — | `event_id` | 🟡 |
| `edu.identity.user.role_changed` | iam (ai06) | 角色变更通知 | system 通知 | `event_id` | 🟡 |
| `edu.identity.role.created` | iam (ai06) | 角色新增(管理通知) | system 通知 | `event_id` | 🟡 |
| `edu.identity.role.updated` | iam (ai06) | 角色权限变更通知 | system 通知 | `event_id` | 🟡 |
| `edu.teaching.exam.published` | core-edu (ai08) | 推送考试通知给班级学生 | exam 通知fan-out | `event_id` | 🟡 |
| `edu.teaching.assignment.submitted` | core-edu (ai08) | 通知教师有学生提交作业 | homework 通知 | `event_id` | 🟡 |
| `edu.teaching.assignment.graded` | core-edu (ai08) | 通知学生作业已批改 | grade 通知 | `event_id` | 🟡 |
| `edu.teaching.grade.recorded` | core-edu (ai08) | 通知学生成绩已录入 | grade 通知 | `event_id` | 🟡 |
| `edu.teaching.attendance.recorded` | core-edu (ai08) | 出勤异常通知家长 | attendance 通知 | `event_id` | 🟡 |
| `edu.insight.mastery.updated` | data-ana (ai11) | 学情掌握度下降,触发预警 | mastery_alert 通知 | `event_id` | 🟡 |
> **⚠️ ISSUE-013 阻塞**events.proto 当前仅有 ClassEvent / ExamEvent / HomeworkEvent / GradeEvent缺 UserEvent / RoleEvent / MasteryEvent / NotificationEvent 4 类 message。msg 消费需 coord 补齐 protoD1。补齐前用通用 JSON payload 解析
>
> **幂等去重**三层防线L1 Redis SETNX `msg:processed:{event_id}` TTL 7d / L2 msg_idempotency 表 / L3 notifications.event_id UNIQUE INDEX
### 2.3 HTTP 调用
无主动 HTTP 调用上游push-gateway 走 gRPCHTTP 仅为降级)。
---
@@ -72,39 +137,60 @@
### 3.1 我依赖的上游就绪标志
- [ ] iam gRPC 50052 启用ai06—— 用于用户通知偏好查询(可选)
- [ ] core-edu gRPC 50053 启用ai08—— 业务事件来源
- [ ] edu.exam.events / edu.homework.events / edu.grade.events / edu.class.events topic 有事件发布ai08
- [ ] data-ana gRPC 50055 启用ai11—— 预警事件来源
- [ ] edu.data_ana.mastery.events topic 有事件发布ai11
| 标志 | 提供方 | 阻塞性 | 说明 |
| ---- | ------ | ------ | ---- |
| events.proto 补 UserEvent/RoleEvent/MasteryEvent/NotificationEvent | coord | 🔴 阻塞 T9 | D1ISSUE-013 |
| events.proto GradeEvent 补 class_id全部补 student_ids[] | coord | 🔴 阻塞 fan-out | D2ISSUE-006 |
| msg.proto 补 5 扩展 RPC + Preference/Template Service | coord | 🔴 阻塞 gRPC | D3-D4ISSUE-009 |
| push-gateway gRPC PushService.Push | ai02 | 🔴 阻塞 T8 | D5 |
| iam 发布 6 类 user/role 事件 | ai06 | 🟡 集成验证 | D6开发期用 mock |
| core-edu 发布 5 类教学事件 | ai08 | 🟡 集成验证 | D7 |
| data-ana 发布 mastery 事件 | ai11 | 🟡 集成验证 | D8 |
| Redis 集群可用 | infra | 🟡 降级 DB | D9 |
### 3.2 我的就绪标志(供下游消费)
- [ ] msg gRPC 50056 启用HealthService.Check 返回 SERVING
- [ ] NotificationService 5 RPC 可调用
- [ ] NotificationPreferenceService 4 RPC 可调用
- [ ] NotificationTemplateService 4 RPC 可调用(含 RenderTemplate 模板渲染
- [ ] edu.msg.notification.events topic 可发布(供 push-gateway 推送
- [ ] NotificationService RPC 可调用(基线 5 + 扩展 4待 ISSUE-009 仲裁)
- [ ] NotificationPreferenceService 2 RPC 可调用
- [ ] NotificationTemplateService RPC 可调用(基线 4 + 扩展 2含 RenderTemplate
- [ ] `edu.notification.sent/read/recalled/failed` topic 可发布(待 ISSUE-008 仲裁命名
- [ ] /readyz 返回 6 项依赖状态DB/ES/Redis/Kafka producer/Kafka consumer/PushGateway
- [ ] 测试覆盖率 ≥ 80%
---
## §4 Mock 策略
### 4.1 我提供的 mock
### 4.1 我提供的 mock(供下游)
在 msg 真实服务就绪前为下游teacher-bff / student-bff / parent-bff / push-gateway提供以下 mock
在 msg 真实服务就绪前为下游teacher-bff / student-bff / parent-bff / push-gateway提供 mock
- **gRPC mock**使用 grpc-mock 拦截 50056 端口
- NotificationService.ListNotifications 返回固定 10 条未读通知
- NotificationService.MarkAsRead 返回 success=true
- NotificationPreferenceService.GetPreference 返回默认偏好in_app+email 开启sms+push 关闭)
- NotificationTemplateService.RenderTemplate 返回固定 title+content
- **Kafka mock**msg 就绪前不发布真实 NotificationEventpush-gateway 使用本地 stub 推送
- **gRPC mock**grpc-mock 拦截 50056 端口
- ListNotifications 返回固定 10 条未读通知
- MarkAsRead 返回 success=true
- GetPreferences 返回默认偏好in_app + email 开启sms + push 关闭)
- RenderTemplate 返回固定 title + content
- **Kafka mock**msg 就绪前不发布真实通知事件push-gateway 使用本地 stub 推送
### 4.2 我消费的 mock
### 4.2 我消费的 mock(开发期间)
在真实上游就绪前msg 使用以下 mock
- 业务事件core-edu/data-ana 就绪前msg 内置定时器发布本地 stub 事件ExamEvent/HomeworkEvent触发 mock 通知流程
- 用户偏好iam 就绪前使用默认偏好(所有用户 in_app 开启)
- 模板渲染:内置 5 个常用模板exam.created / homework.assigned / grade.recorded / warning.triggered / system.notice
- **业务事件**core-edu / data-ana 就绪前msg 内置定时器发布本地 stub 事件ExamEvent / HomeworkEvent触发 mock 通知流程
- **用户偏好**iam 就绪前使用默认偏好(所有用户 in_app 开启)
- **模板渲染**:内置 5 个常用模板exam.published / homework.graded / grade.recorded / mastery.warning / system.notice
- **Push Gateway**ai02 就绪前用 fetch POST /internal/push 降级(当前实现保留)
---
## §5 待仲裁项汇总
| ISSUE | 主题 | 阻塞性 | 当前采用 |
| ----- | ---- | ------ | -------- |
| ISSUE-008 | Kafka topic 命名per-event vs aggregate | 🟡 | per-event02-architecture-design.md §5 + 004 §7.2 |
| ISSUE-009 | RPC 数量13 vs 17 | 🟡 | 1702-architecture-design.md §4.2),标注基线/扩展 |
| ISSUE-013 | events.proto 缺 4 类 message | 🔴 | 用 JSON payload 降级,待 coord 补齐 |
| ISSUE-006 | events.proto P9 字段描述 | 🟡 | 待 coord 修正 |
| ISSUE-010 | markAsRead 权限点 | 🟡 | 以 02-architecture-design.md §6.1 为准READ |
| ISSUE-011 | DB↔ES 降级方向 | 🟡 | 待 coord 仲裁(双向降级 vs 单向) |

View File

@@ -1,48 +1,98 @@
# parent-bff 对接契约
> 负责人ai05
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[core_edu.proto](../../../packages/shared-proto/proto/core_edu.proto)、[analytics.proto](../../../packages/shared-proto/proto/analytics.proto)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)
> 关联:[matrix.md](../matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[core_edu.proto](../../../packages/shared-proto/proto/core_edu.proto)、[analytics.proto](../../../packages/shared-proto/proto/analytics.proto)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[02-architecture-design.md](../../../services/parent-bff/docs/02-architecture-design.md)
> 修订2026-07-10 ai05 复审修正仲裁引用、proto 包名、契约缺口标注)
---
## §0 契约基线说明
| 维度 | 内容 |
| --- | --- |
| proto 包名前缀 | 所有 proto 包名统一为 `next_edu_cloud.<domain>.v1`(如 `next_edu_cloud.iam.v1`),引用 proto message 时须带完整包名 |
| GraphQL schema 文件 | `packages/shared-ts/contracts/graphql/parent-bff.graphql`SDL-first 集中管理,对齐 coord ARB-001 模式) |
| 错误码前缀 | `BFF_PARENT_`C1 仲裁BFF 在前) |
| 端口 | HTTP 3010不暴露 gRPCC2 仲裁) |
| API 风格 | GraphQL YogaU3 仲裁P4 直接 GraphQL不走 REST 过渡) |
| 权限校验 | BFF 豁免 @RequirePermissionU4 仲裁),仅校验 x-user-id + ChildGuard 越权防御 |
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 gRPC 接口
无对外 gRPC。parent-bff 是 GraphQL 聚合层。
无对外 gRPC。parent-bff 是 GraphQL 聚合层,对上游仅暴露 HTTP/GraphQLC2 仲裁)
### 1.2 HTTP 端点(如有)
### 1.2 HTTP 端点
| Method | Path | 用途 | 认证 |
| ------ | -------- | ----------------------------------------- | ---------------------- |
| POST | /graphql | 家长 BFF GraphQL 端点 | JWT 必需 + parent 角色 |
| GET | /graphql | GraphQL Playground开发环境 | 开发环境公开 |
| GET | /healthz | 健康检查liveness | 公开 |
| GET | /readyz | 就绪检查readiness含下游 gRPC 连通性 | 公开 |
| Method | Path | 用途 | 认证 | 阶段 |
| --- | --- | --- | --- | --- |
| POST | /graphql | 家长 BFF GraphQL 端点U3 仲裁) | JWT 必需 + parent 角色 | P4 |
| GET | /graphql | GraphQL Playground开发环境) | 开发环境公开 | P4 |
| GET | /healthz | 健康检查liveness,直接返回 ok | 公开 | P4 |
| GET | /readyz | 就绪检查readiness含下游 gRPC 连通性02 §9 #7 | 公开 | P4 |
| GET | /metrics | Prometheus 指标parent_bff_* | 公开 | P4 |
### 1.3 GraphQL schema如 BFF
> 网关路径:`/api/v1/parent/*` → api-gateway 剥离 `/api/v1` 后代理到 parent-bff:3010
> **不实现 REST 业务端点**02 §4.1,仅保留 /healthz /readyz /metrics 基础端点)
GraphQL schema 文件路径:`apps/parent-bff/src/schema/*.graphql`(端口 :3010
### 1.3 GraphQL schema
核心 Query / Mutation 域:
**schema 文件**`packages/shared-ts/contracts/graphql/parent-bff.graphql`SDL-first
**完整 schema 定义**:见 [02-architecture-design.md §4.2](../../../services/parent-bff/docs/02-architecture-design.md) GraphQL Schema 完整定义
- **auth**currentUser聚合 iam.GetUserInfo + GetEffectivePermissions + GetViewports
- **children**myChildren聚合 iam.GetChildrenByParent核心依赖 I3 裁决)
- **childSummary**childSummary聚合 data-ana.AnalyticsService.GetParentDashboard
- **childGrades**childGrades聚合 core-edu.GradeService.ListGradesByStudent
- **childAttendance**childAttendance聚合 core-edu.AttendanceService.ListAttendanceByStudent
- **childHomework**childHomework聚合 core-edu.HomeworkService.ListHomeworkByClass
- **childWeakness**childWeakness聚合 data-ana.AnalyticsService.GetStudentWeakness
- **childTrend**childTrend聚合 data-ana.AnalyticsService.GetLearningTrend
- **notifications**myNotifications / markAsRead聚合 msg.NotificationService
核心 Query / Mutation 域(按阶段分级):
### 1.4 Kafka 事件发布(如有)
| 类型 | 字段 | 聚合下游 | ChildGuard | 阶段 |
| --- | --- | --- | --- | --- |
| Query | dashboard | iam + core-edu | 否(聚合所有孩子) | P4 |
| Query | viewports | iam | 否 | P4 |
| Query | me | iam | 否 | P4 |
| Query | children | iam | 否 | P4 |
| Query | child | iam + core-edu | 是 | P4 |
| Query | childGrades | core-edu | 是 | P4 |
| Query | childHomework | core-edu | 是 | P4 |
| Query | childExams | core-edu | 是 | P4 |
| Query | childAnalytics | data-ana | 是 | P4 |
| Query | notifications | msg | 否 | P5 |
| Query | notificationPreferences | msg | 否 | P5 |
| Mutation | selectChild | BFF 内部审计) | 是 | P4 |
| Mutation | markNotificationRead | msg | 否 | P5 |
| Mutation | updateNotificationPreferences | msg | 否 | P5 |
无。parent-bff 不发布事件,仅做 gRPC 聚合。
**统一响应信封**GraphQL 规范data/errors错误扩展字段携带 `extensions.code = "BFF_PARENT_*"`02 §4.5
### 1.4 Kafka 事件发布
无。parent-bff 不发布领域事件BFF 聚合层无业务状态变更02 §5.6)。
### 1.5 错误码前缀
`BFF_PARENT_`如 BFF_PARENT_UPSTREAM_UNAVAILABLE、BFF_PARENT_AGGREGATION_FAILED、BFF_PARENT_NO_CHILDREN、BFF_PARENT_FORBIDDEN
`BFF_PARENT_`C1 仲裁BFF 在前;非 PARENT_BFF_
错误码清单(完整见 02 §6.2
| 错误码 | HTTP | 触发条件 |
| --- | --- | --- |
| BFF_PARENT_VALIDATION_ERROR | 400 | Zod / GraphQL input 校验失败 |
| BFF_PARENT_UNAUTHORIZED | 401 | 缺失 x-user-id 头 |
| BFF_PARENT_CHILD_NOT_BOUND | 403 | ChildGuard 拦截childId 不在家长绑定列表 |
| BFF_PARENT_NOT_FOUND | 404 | 资源不存在 |
| BFF_PARENT_BAD_GATEWAY | 502 | 下游服务返回非 ok 或 gRPC rejected |
| BFF_PARENT_GATEWAY_TIMEOUT | 504 | 下游调用超时 |
| BFF_PARENT_SERVICE_UNAVAILABLE | 503 | 熔断器开启P6 |
| BFF_PARENT_INTERNAL_ERROR | 500 | 未捕获异常 |
**下游错误透传**C3 仲裁core-edu 统一 CORE_EDU_*
| 下游服务 | 错误码前缀 | 示例 |
| --- | --- | --- |
| iam | IAM_ | IAM_USER_NOT_FOUND |
| core-edu | CORE_EDU_ | CORE_EDU_GRADE_NOT_FOUND |
| data-ana | DATA_ANA_ | DATA_ANA_ANALYTICS_NOT_READY |
| msg | MSG_ | MSG_NOTIFICATION_NOT_FOUND |
---
@@ -50,28 +100,59 @@ GraphQL schema 文件路径:`apps/parent-bff/src/schema/*.graphql`(端口 :3
### 2.1 gRPC 调用(同步)
| 被调用方 | Service.RPC | 用途 | mock 策略 |
| --------------- | ----------------------------------------- | ------------------------ | ------------------------------------------------------ |
| iam (ai06) | IamService.GetUserInfo | 获取当前家长信息 | iam 就绪前返回固定 UserInfoparent 角色) |
| iam (ai06) | IamService.GetEffectivePermissions | 权限校验 | iam 就绪前返回家长权限集 |
| iam (ai06) | IamService.GetViewports | 家长导航菜单 | iam 就绪前返回固定视口列表 |
| iam (ai06) | IamService.GetChildrenByParent | 查询关联孩子列表(核心) | iam 就绪前返回固定 2 个 ChildInfoI3/ISSUE-047 裁决) |
| core-edu (ai08) | GradeService.ListGradesByStudent | 孩子成绩 | core-edu 就绪前返回固定 5 个 Grade |
| core-edu (ai08) | AttendanceService.ListAttendanceByStudent | 孩子考勤 | core-edu 就绪前返回固定 10 条 Attendance |
| core-edu (ai08) | HomeworkService.ListHomeworkByClass | 孩子作业 | core-edu 就绪前返回固定 3 个 Homework |
| data-ana (ai11) | AnalyticsService.GetParentDashboard | 家长仪表盘 | data-ana 就绪前返回固定仪表盘child_avg_score=85.0 |
| data-ana (ai11) | AnalyticsService.GetStudentWeakness | 孩子薄弱点 | data-ana 就绪前返回固定 3 个 weak_points |
| data-ana (ai11) | AnalyticsService.GetLearningTrend | 孩子学习趋势 | data-ana 就绪前返回固定趋势数据 |
| msg (ai10) | NotificationService.ListNotifications | 家长通知 | msg 就绪前返回固定 10 条通知 |
| msg (ai10) | NotificationService.MarkAsRead | 标记已读 | msg 就绪前返回 success=true |
> proto 包名均为 `next_edu_cloud.<domain>.v1`
> **状态标注**:✅ 已有 = proto 已定义;❌ 待补 = proto 缺失;⚠️ 待仲裁 = ai05 提请 coord 仲裁中
### 2.2 Kafka 事件订阅(异步)
| 被调用方 | Service.RPC | proto message 包名 | 用途 | mock 策略 | 状态 |
| --- | --- | --- | --- | --- | --- |
| iam (ai06) | IamService.GetUserInfo | next_edu_cloud.iam.v1.UserInfo | 获取当前家长信息 | 返回固定 UserInfoparent 角色) | ✅ 已有 |
| iam (ai06) | IamService.GetViewports | next_edu_cloud.iam.v1.Viewport[] | 家长导航菜单 | 返回固定视口列表 | ❌ 待 ai06 补coord-cross-review #1 |
| iam (ai06) | IamService.GetEffectivePermissions | next_edu_cloud.iam.v1.EffectivePermissions | 权限校验 | 返回家长权限集 | ❌ 待 ai06 补 |
| iam (ai06) | **IamService.GetChildrenByParent** | next_edu_cloud.iam.v1.Child[] | 查询关联孩子列表(**P0 核心依赖** | 返回固定 2 个 ChildInfo | ❌ 待 ai06 补(**I6 裁决**P0 阻塞) |
| core-edu (ai08) | GradeService.ListGradesByStudent | next_edu_cloud.core_edu.v1.Grade[] | 孩子成绩 | 返回固定 5 个 Grade | ✅ 已有 |
| core-edu (ai08) | HomeworkService.ListHomeworkByClass | next_edu_cloud.core_edu.v1.Homework[] | 孩子作业 | 返回固定 3 个 Homework | ✅ 已有 |
| core-edu (ai08) | ExamService.ListExamsByClass | next_edu_cloud.core_edu.v1.Exam[] | 孩子考试 | 返回固定 3 个 Exam | ✅ 已有 |
| core-edu (ai08) | **ClassService.GetClass** | next_edu_cloud.core_edu.v1.Class | 孩子班级信息 | 返回固定 ClassInfo | ❌ 待补(**ISSUE-008**proto 缺 ClassService待 coord 仲裁归属) |
| core-edu (ai08) | AttendanceService.ListAttendanceByStudent | next_edu_cloud.core_edu.v1.Attendance[] | 孩子考勤P5+ | 返回固定 10 条 | ❌ 待 ai08 补coord-cross-review #5P3 补全) |
| data-ana (ai11) | AnalyticsService.GetStudentWeakness | next_edu_cloud.analytics.v1.StudentWeakness | 孩子薄弱点 | 返回固定 3 个 weak_points | ✅ 已有 |
| data-ana (ai11) | AnalyticsService.GetLearningTrend | next_edu_cloud.analytics.v1.LearningTrend | 孩子学习趋势 | 返回固定趋势数据 | ✅ 已有 |
| data-ana (ai11) | AnalyticsService.GetClassPerformance | next_edu_cloud.analytics.v1.ClassPerformance | 班级学情对比 | 返回固定班级数据 | ✅ 已有 |
| data-ana (ai11) | **AnalyticsService.GetParentDashboard** | — | 家长仪表盘聚合(多子女防 N+1 | 返回固定仪表盘 | ⚠️ 待仲裁02 §14 #3ai05 提请proto 未定义) |
| msg (ai10) | NotificationService.ListNotifications | next_edu_cloud.msg.v1.Notification[] | 家长通知 | 返回固定 10 条通知 | ✅ 已有 |
| msg (ai10) | NotificationService.MarkAsRead | next_edu_cloud.msg.v1.Empty | 标记已读 | 返回 success | ✅ 已有 |
| msg (ai10) | **NotificationPreferenceService.*** | — | 通知偏好配置 | 返回固定偏好 | ❌ 待 ai10 补proto + 实现均缺失P5 |
无。parent-bff 不订阅 Kafka 事件,仅做同步 gRPC 聚合。
**类型映射注意**ISSUE-008
### 2.3 HTTP 调用(如有)
- `core_edu.v1.Grade.score``string` 类型GraphQL `Grade.score``Float!`BFF response-mapper 需做 `Number.parseFloat(score)` 转换,转换失败抛 BFF_PARENT_BAD_GATEWAY
- `msg.v1.Notification.is_read` 映射为 GraphQL `Notification.read`(字段名重命名)
- `msg.v1.Notification``child_id` 字段ISSUE-007GraphQL `Notification.childId` 暂从 notification.type+content 解析或置 null待 ai10 补 proto 字段
无。
### 2.2 Kafka 事件订阅异步P5 可选)
> P4 阶段不订阅事件02 §5.1。P5 阶段可选订阅以下 topic 用于实时推送 + 缓存失效02 §5.2)。
| Topic | 事件 | 发布方 | 消费动作 | 幂等性 | 阶段 |
| --- | --- | --- | --- | --- | --- |
| edu.notification.sent | 通知发送 | msg | 推送给家长push-gateway HTTP | event_id SETNX | P5 |
| edu.notification.read | 通知已读 | msg | 失效 bff:parent:notifications:* | event_id SETNX | P5 |
| edu.notification.recalled | 通知撤回 | msg | 失效通知缓存 + 推送撤回 | event_id SETNX | P5 |
| edu.notification.failed | 通知失败 | msg | 记录日志 + 告警 | event_id SETNX | P5 |
| edu.teaching.grade.recorded | 成绩录入 | core-edu | 失效 bff:parent:grades:{childId} + 推送 | event_id SETNX | P5 |
| edu.teaching.homework.graded | 作业批改 | core-edu | 失效 bff:parent:homework:{childId} + 推送 | event_id SETNX | P5 |
| edu.teaching.exam.published | 考试发布 | core-edu | 失效 bff:parent:exams:{childId} + 推送 | event_id SETNX | P5 |
**消费者组**`parent-bff-event-subscriber`02 §5.5
**DLQ**`edu.parent-bff.dlq`
**提交策略**manual commit
### 2.3 HTTP 调用(非 gRPC
| 被调用方 | Method | Path | 用途 | 认证 | 阶段 |
| --- | --- | --- | --- | --- | --- |
| push-gateway (ai02) | POST | /internal/push | 推送给在线家长(**U2 仲裁**push-gateway 豁免 gRPC | X-Internal-Key | P5 |
> push-gateway HTTP 调用在 P5 阶段启用P4 不涉及。
---
@@ -79,43 +160,73 @@ GraphQL schema 文件路径:`apps/parent-bff/src/schema/*.graphql`(端口 :3
### 3.1 我依赖的上游就绪标志
- [ ] iam gRPC 50052 启用ai06—— **核心依赖 GetChildrenByParentI3/ISSUE-047 裁决**
- [ ] iam gRPC 50052 启用ai06—— **核心依赖 GetChildrenByParentI6 裁决P0 阻塞**
- [ ] iam 补 GetViewports / GetEffectivePermissions RPCcoord-cross-review #1
- [ ] iam_student_guardians 表已建立I6 裁决)
- [ ] core-edu gRPC 50053 启用ai08
- [ ] core-edu ClassService.GetClass proto 补全ISSUE-008 待仲裁)
- [ ] data-ana gRPC 50055 启用ai11
- [ ] msg gRPC 50056 启用ai10
- [ ] msg gRPC 50056 启用ai10—— P5
- [ ] msg.proto Notification 补 child_id 字段ISSUE-007 待仲裁)—— P5
- [ ] msg NotificationPreferenceService 补全 —— P5
- [ ] push-gateway /internal/push 启用ai02—— P5
- [ ] api-gateway /parent 路由注册ai01
- [ ] Kafka topic 已创建C5 仲裁)—— P5
- [ ] Redis 已部署且网络可达
- [ ] buf.gen.yaml 补 gRPC TS 插件coord
### 3.2 我的就绪标志(供下游消费)
- [ ] parent-bff GraphQL :3010 启用(/healthz 返回 200
- [ ] /readyz 返回 200含 4 个下游 gRPC 连通性检查)
- [ ] /readyz 返回 200含 4 个下游 gRPC 连通性检查iam + core-edu + data-ana + Redis02 §9 #7
- [ ] GraphQL schema 可内省POST /graphql 返回 schema
- [ ] 核心 Query 可执行:currentUser / myChildren / childSummary / childGrades
- [ ] 核心 Mutation 可执行:markAsRead
- [ ] 数据范围校验生效(家长只能查自己孩子的数据,基于 iam.GetChildrenByParent 返回的 user_id 校验)
- [ ] 核心 Query 可执行:dashboard / children / childGrades / childAnalytics
- [ ] 核心 Mutation 可执行:selectChildP4/ markNotificationReadP5
- [ ] DataScope=CHILDREN 校验生效(家长只能查自己孩子的数据,ChildGuard 基于 iam.GetChildrenByParent 返回的列表校验)
- [ ] /metrics 暴露 parent_bff_* 指标
---
## §4 Mock 策略
### 4.1 我提供的 mock
### 4.1 我提供的 mock(供下游 parent-portal ai15
在 parent-bff 真实就绪前,为下游(parent-portal提供以下 mock
在 parent-bff 真实就绪前,为 parent-portal 提供以下 mock
- **GraphQL mock**:使用 Apollo Server mockProviders 或 MSW 拦截 POST /graphql
- currentUser 返回固定家长id="parent-001", name="王家长", roles=["parent"]
- myChildren 返回固定 2 个孩子id="student-001" 李同学 + id="student-002" 李妹妹)
- childSummary 返回固定仪表盘child_avg_score=85.0, child_class_rank=5
- childGrades 返回固定 5 个成绩
- childAttendance 返回固定 10 条考勤
- myNotifications 返回固定 10 条通知
- **GraphQL mock**:使用 MSW 拦截 POST /graphql 或 Apollo Server mockProviders
- `dashboard` 返回固定家长id="parent-001", name="王家长", roles=["parent"]+ 2 个孩子 + unreadNotifications=3
- `children` 返回固定 2 个孩子id="student-001" 李同学 + id="student-002" 李妹妹)
- `childGrades` 返回固定 5 个成绩(含 score 字段Float 类型
- `childAnalytics` 返回固定学情child_avg_score=85.0, class_rank=5
- `childHomework` 返回固定 3 个作业
- `childExams` 返回固定 3 个考试
- `notifications`P5返回固定 10 条通知(含 childId 字段)
### 4.2 我消费的 mock
### 4.2 我消费的 mock(上游未就绪时)
在真实上游就绪前parent-bff 使用以下 mock详见 §2.1 mock 策略列):
- **iam mock**:固定 UserInfo + 家长权限 + 固定视口 + 固定 2 个 ChildInfo家长-学生关联核心数据)
- **core-edu mock**:固定孩子成绩/考勤/作业
- **data-ana mock**:固定家长仪表盘/孩子薄弱点/趋势
- **msg mock**:固定通知列表 + MarkAsRead success
- **core-edu mock**:固定孩子成绩/作业/考试/班级信息
- **data-ana mock**:固定孩子薄弱点/趋势/班级对比
- **msg mock**P5:固定通知列表(含 childId+ MarkAsRead success + 固定偏好配置
- **push-gateway mock**P5fetch mock 返回 success
- **Redis**Testcontainers 真实 Redis 实例(不用 mock
- **Kafka**P5kafkajs mock + jest.mock
> 关键iam.GetChildrenByParent 的 mock 必须返回与 core-edu mock 数据一致的 student_id,否则数据范围校验会失败。parent-bff 启动时校验 myChildren 返回的 user_id 与下游查询的 student_id 一致性。
> **关键约束**iam.GetChildrenByParent 的 mock 必须返回与 core-edu mock 数据一致的 student_idstudent-001 + student-002否则 ChildGuard 越权校验会失败。parent-bff 启动时校验 myChildren 返回的 user_id 与下游查询的 student_id 一致性。
---
## §5 跨模块契约冲突跟踪
> 以下为 ai05 复审发现的跨模块契约问题,详见 [objections/parent-bff_issue.md](../objections/parent-bff_issue.md)
| ISSUE | 问题 | 影响 | 状态 |
| --- | --- | --- | --- |
| ISSUE-003 | contract.md 仲裁引用编号错误I3 → I6 | 引用勘误,已修正本文档 | 待 coord 确认 |
| ISSUE-004 | 004 §4 + matrix.md §1 未同步 C6 仲裁(缺 DataAna + Msg | 新 AI 误判依赖 | 待 coord 仲裁 |
| ISSUE-005 | parent-portal 01 文档仍按 REST 消费 parent-bff | 跨模块契约冲突 | 待 coord 仲裁 |
| ISSUE-006 | proto 包名引用缺 next_edu_cloud 前缀 | gRPC 代码生成错误,已修正本文档 | 待 coord 确认 |
| ISSUE-007 | msg.proto Notification 缺 child_id 字段 | 按孩子过滤通知失效 | 待 coord 仲裁 |
| ISSUE-008 | core_edu.proto 缺 ClassService + Grade.score 类型不一致 | 班级信息查询 + 类型转换 | 待 coord 仲裁 |

View File

@@ -1,7 +1,9 @@
# parent-portal 对接契约
> 负责人ai15
> 关联:[matrix.md](./matrix.md)
> 关联:[matrix.md](./matrix.md)、[parent-bff_contract.md](./parent-bff_contract.md)、[iam_contract.md](./iam_contract.md)、[push-gateway_contract.md](./push-gateway_contract.md)
> 依据ARB-001BFF GraphQL、ARB-002MF Shell 暴露清单)、[port-allocation.md](../../../infra/port-allocation.md) §4
> 待仲裁ISSUE-001 ~ ISSUE-010见 [objections/parent-portal_issue.md](../objections/parent-portal_issue.md)),仲裁前本契约按 ARB-001 GraphQL 方向编写
---
@@ -9,20 +11,20 @@
### 1.1 gRPC 接口(如有)
无。parent-portal 是前端微前端 Remote。
无。parent-portal 是前端微前端 Remote,不提供 gRPC
### 1.2 HTTP 端点(如有)
| Method | Path | 用途 | 认证 |
| ------ | --------------------- | ------------ | ------------------------------------- |
| GET | / | 家长门户首页 | JWT 必需(前端路由守卫) |
| GET | /children | 我的孩子列表 | JWT 必需 |
| GET | /child/:id/summary | 孩子概况 | JWT 必需 + 数据范围校验(仅自己孩子) |
| GET | /child/:id/grades | 孩子成绩 | JWT 必需 + 数据范围校验 |
| GET | /child/:id/attendance | 孩子考勤 | JWT 必需 + 数据范围校验 |
| GET | /child/:id/homework | 孩子作业 | JWT 必需 + 数据范围校验 |
| GET | /child/:id/weakness | 孩子薄弱点 | JWT 必需 + 数据范围校验 |
| GET | /notifications | 通知中心 | JWT 必需 |
无对外 HTTP API 端点。parent-portal 是 Next.js 前端应用MF Remote不对外暴露 REST API。
> **说明**ISSUE-006parent-portal 的页面路由(`/parent/dashboard`、`/parent/grades` 等)是前端 SSR/CSR 路由,不是 HTTP API 端点。页面路由清单见 [01-understanding.md §8 L2 路由表](../../../apps/parent-portal/docs/01-understanding.md#8-l2-路由表)。
>
> parent-portal 仅提供两个内部健康检查端点(非业务 API
| Method | Path | 用途 | 认证 |
| ------ | ------------ | ----------------------- | ---- |
| GET | /api/health | Dockerfile HEALTHCHECK | 无 |
| GET | /api/ready | K8s readinessProbe | 无 |
### 1.3 GraphQL schema如 BFF
@@ -30,19 +32,26 @@
### 1.4 Kafka 事件发布(如有)
无。
无。前端不发布 Kafka 事件。
### 1.5 错误码前缀
无(前端不定义错误码前缀,透传 BFF 错误码)
parent-portal 不产生错误码前缀(前端不定义错误码)。消费侧错误码前缀见 §2.5
### 1.6 微前端架构(补充)
### 1.6 微前端架构
| 角色 | 说明 |
| ---------------------- | ------------------------------------------------ |
| MF Remote | 家长门户是微前端远程模块 |
| 暴露的 remote 模块 | ParentApp家长端完整应用、shared 家长端组件 |
| module federation 配置 | `apps/parent-portal/module-federation.config.ts` |
| 角色 | 说明 |
| ---- | ---- |
| MF 角色 | RemoteShell = teacher-portal :4000 |
| Remote name | `parent_app` |
| remoteEntry 路径 | `static/chunks/remoteEntry.js` |
| 暴露模块 | `./pages`(家长场景页面)、`./ChildSwitcher`(多子女切换组件) |
| MF 配置文件 | `apps/parent-portal/next.config.js`NextFederationPlugin见 [02-architecture-design §1.2](../../../apps/parent-portal/docs/02-architecture-design.md#12-mf-配置parent-portalnextconfigjs-remote-角色) |
| MF sharedsingleton | react / react-dom / urql / graphql / @tanstack/react-query / zustand / nuqs / @edu/ui-tokens / @edu/ui-components / @edu/hooksARB-002 |
| dev/prod 端口 | 4002[port-allocation.md](../../../infra/port-allocation.md) §4 |
| feature flag | `NEXT_PUBLIC_MF_ENABLED`ARB-002P4 默认开) |
> **注**ISSUE-007MF 配置文件统一为 `next.config.js`,不使用 `module-federation.config.ts`(与 02-architecture-design + teacher-portal Shell 一致)。
---
@@ -56,27 +65,85 @@
无。前端不直接订阅 Kafka。
### 2.3 HTTP 调用(如有
### 2.3 HTTP 调用(非 GraphQL
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------- | ------------------------ | -------------------------------------------- | ---------------------------------------------------------------- |
| api-gateway (ai01) | POST /api/parent/graphql | 家长 GraphQL 查询(经网关代理到 parent-bff | api-gateway/parent-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| api-gateway (ai01) | POST /api/auth/login | 家长登录 | api-gateway 就绪前使用 MSW 返回固定 JWT |
| push-gateway (ai02) | GET /ws | WebSocket 实时通知 | push-gateway 就绪前使用 mock-socket 模拟 WS 推送 |
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------ | --------------------- | -------- | ------------------------------------------- |
| api-gateway (ai01) | POST /api/v1/iam/login | 家长登录 | api-gateway 就绪前 MSW 返回固定 JWTparent 角色) |
> **注**ISSUE-004
> - 登录端点统一为 `POST /api/v1/iam/login`(与 [matrix.md](./matrix.md) §5 `/api/v1/iam/*` + 01 §3.1 前缀一致)
> - 登录是 parent-portal 唯一走 REST非 GraphQL的端点登录前无 JWTGraphQL endpoint 需鉴权
> - 待 coord 确认登录是否走 REST其余走 GraphQL
### 2.4 GraphQL 查询域(经 api-gateway 代理到 parent-bff
| Query/Mutation | 用途 | mock 策略 |
| ---------------------------- | -------------------- | ----------------------------- |
| currentUser | 当前家长信息 | MSW 返回固定家长 |
| myChildren | 我的孩子列表(核心) | MSW 返回固定 2 个孩子 |
| childSummary | 孩子概况 | MSW 返回固定仪表盘 |
| childGrades | 孩子成绩 | MSW 返回固定 5 个成绩 |
| childAttendance | 孩子考勤 | MSW 返回固定 10 条考勤 |
| childHomework | 孩子作业 | MSW 返回固定 3 个作业 |
| childWeakness | 孩子薄弱点 | MSW 返回固定 3 个 weak_points |
| childTrend | 孩子学习趋势 | MSW 返回固定趋势数据 |
| myNotifications / markAsRead | 通知中心 | MSW 返回固定通知 |
> **依据**ARB-001BFF GraphQL+ [parent-bff_contract.md](./parent-bff_contract.md) §1.3
>
> **端点**`POST /api/v1/parent/graphql`api-gateway 代理 `/api/v1/parent/*` → parent-bff :3010 `/graphql`
> **注**ISSUE-001 / ISSUE-008
> - 01/02 文档描述为 REST 消费,与 ARB-001 冲突,待 coord 仲裁
> - 仲裁前本表按 GraphQL 方向编写(与 parent-bff contract + matrix.md 一致)
> - 路径前缀统一为 `/api/v1/parent/graphql`(与 matrix.md §5 一致,旧版缺 `v1`
| Query/Mutation | 类型 | 用途 | 对应 parent-bff 聚合 | mock 策略 |
| ------------------------------- | -------- | -------------------- | ------------------------------------------- | ---------------------------------- |
| currentUser | Query | 当前家长信息 | iam.GetUserInfo + GetEffectivePermissions + GetViewports | MSW 返回固定家长parent-001 王家长) |
| myChildren | Query | 我的子女列表(核心) | iam.GetChildrenByParentI3/ISSUE-047 裁决) | MSW 返回固定 2 个子女student-001 + student-002 |
| childSummary(childId) | Query | 子女仪表盘概览 | data-ana.GetParentDashboard | MSW 返回固定仪表盘 |
| childGrades(childId) | Query | 子女成绩 | core-edu.ListGradesByStudent | MSW 返回固定 5 个成绩 |
| childAttendance(childId) | Query | 子女考勤 | core-edu.ListAttendanceByStudent | MSW 返回固定 10 条考勤 |
| childHomework(childId) | Query | 子女作业 | core-edu.ListHomeworkByClass | MSW 返回固定 3 个作业 |
| childWeakness(childId) | Query | 子女薄弱点 | data-ana.GetStudentWeakness | MSW 返回固定 3 个 weak_points |
| childTrend(childId) | Query | 子女学习趋势 | data-ana.GetLearningTrend | MSW 返回固定趋势数据 |
| myNotifications | Query | 通知列表P5 | msg.ListNotifications | MSW 返回固定 10 条通知 |
| markAsRead(notificationId) | Mutation | 标记已读P5 | msg.MarkAsRead | MSW 返回 success=true |
| updateNotificationPreferences | Mutation | 更新通知偏好 | msg待 ai05 确认) | MSW 返回 success=true |
| switchChild(childId) | Mutation | 切换当前子女 | 待 ISSUE-009 仲裁确认 | 见 ISSUE-009 |
> **switchChild 说明**ISSUE-009
> - parent-bff_contract.md §1.3 未列 switchChild Mutation
> - 待 coord 仲裁switchChild 是 GraphQL Mutation后端记录当前子女还是纯前端状态localStorage + Zustand
> - 若纯前端:本表移除 switchChild切换逻辑在 `useChildSwitcher` 内直接写 Zustand + localStorage
### 2.5 消费的错误码前缀(前端 i18n 路由)
parent-portal 不产生错误码,仅消费。前端 API 请求层根据 `error.code` 前缀路由到对应 i18n key
| 前缀 | 来源服务 | i18n key 模式 |
| ------------- | ----------- | ------------------------- |
| `IAM_` | iam | `error.iam.{{code}}` |
| `CORE_EDU_` | core-edu | `error.core_edu.{{code}}` |
| `BFF_PARENT_` | parent-bff | `error.bff_parent.{{code}}` |
| `GW_` | api-gateway | `error.gw.{{code}}` |
| `NETWORK_` | 前端网络层 | `error.network.{{code}}` |
> **注**:与 [matrix.md](./matrix.md) §6 错误码前缀矩阵对齐。`BFF_PARENT_` 前缀由 parent-bff 定义(见 [parent-bff_contract.md](./parent-bff_contract.md) §1.5)。
### 2.6 WebSocket 推送P5
| 被调用方 | 协议 | 路径 | 用途 | mock 策略 |
| -------------------- | ----------- | ---- | ---------- | -------------------------------------- |
| push-gateway (ai02) | WebSocket | /ws | 实时推送 | mock-socket 模拟 WS 推送(每 30s 1 条) |
| push-gateway (ai02) | SSE降级 | /sse | SSE 降级 | — |
> WebSocket 连接由 Shell 建立统一连接管理parent-portal 通过 Zustand ui-store 订阅事件流。
### 2.7 消费的 MF Shell 暴露ARB-002
| 暴露模块 | 来源 | 用途 |
| -------- | ---- | ---- |
| AppShell | teacher-portal Shell | 左栏导航 + 主内容区布局 |
| GraphQLProvider | teacher-portal Shell | urql client 单例ARB-002 |
| useAuth | packages/hooks | 会话状态 |
| usePermission | packages/hooks | 权限查询 |
| useGraphQLClient | packages/hooks | urql client 获取 |
| ErrorBoundary | packages/ui-components | React 渲染异常兜底 |
| Loading / Empty | packages/ui-components | 骨架屏 / 空态 |
| RequirePermission | packages/ui-components | L3 组件级视口控制 |
> MF sharedsingletonreact / react-dom / urql / graphql / @tanstack/react-query / zustand / nuqs / @edu/ui-tokens / @edu/ui-components / @edu/hooksARB-002 裁决,见 [coord.md](../coord.md) §2
---
@@ -84,19 +151,37 @@
### 3.1 我依赖的上游就绪标志
- [ ] api-gateway HTTP :8080 启用ai01—— 前端请求入口
- [ ] parent-bff GraphQL :3010 启用ai05—— 数据来源
- [ ] push-gateway WebSocket :8081/ws 启用ai02—— 实时通知
| 上游 | 就绪信号 | 提供方 | 状态 |
| ---- | -------- | ------ | ---- |
| api-gateway | HTTP :8080 启用 + JWT 验签 + `/api/v1/parent/*` 代理 | ai01 | ⏳ |
| parent-bff GraphQL | `POST /graphql` :3010 + currentUser/myChildren/childSummary/childGrades Query | ai05 | ⏳ P4 |
| iam GetChildrenByParent | gRPC 50052 + `iam_student_guardians`I3/ISSUE-047 裁决) | ai06 | ⏳ P3 补全 |
| teacher-portal Shell | MF exposesAppShell + GraphQLProvider + hooks + UI 组件)+ shared singleton | ai13 | ⏳ P2 |
| push-gateway | WebSocket :8081/ws | ai02 | ⏳ P5 |
| msg | gRPC 50056 + NotificationService | ai10 | ⏳ P5 |
| shared-ts / contracts | ApiClient / Logger / Permissions 常量 | coord | ⏳ |
| ui-tokens / ui-components / hooks | 三层令牌 + shadcn + usePermission/useAuth | ai07/ai13 | ⏳ P2 收尾 |
> **P0 阻塞**ISSUE-010iam `GetChildrenByParent` 缺失,多子女场景无法落地。补全前用 mock固定 2 个子女 student-001 + student-002开发。
### 3.2 我的就绪标志(供下游消费)
- [ ] parent-portal dev server :4002 启用
- [ ] MF Remote 可被 AppShell 加载(暴露 ParentApp 模块)
- [ ] 独立壳渲染(首页 + 导航 + 路由守卫)
- [ ] 登录流程可用POST /api/auth/login 获取 JWT 存入 cookie
- [ ] GraphQL 查询可执行currentUser / myChildren / childSummary 返回数据)
- [ ] 数据范围校验生效(前端路由守卫校验 child:id 是否在 myChildren 返回列表中)
- [ ] WebSocket 通知可接收
> 与 [matrix.md](./matrix.md) §8 就绪信号跟踪表对齐
| 信号 | 说明 | 阶段 |
| ---- | ---- | ---- |
| parent-portal dev server :4002 启用 | MF Remote 可被 Shell 加载 | P4-1 |
| MF Remote remoteEntry.js 可加载 | Shell 端 `remotes.parent = parent_app@http://localhost:4002/...` 可解析 | P4-1 |
| 独立壳渲染 | 首页 + 导航 + 路由守卫 | P4-1 |
| 登录流程可用 | `POST /api/v1/iam/login` 获取 JWT 存入 httpOnly cookie | P4-2 |
| GraphQL 查询可执行 | currentUser / myChildren / childSummary 返回数据mock 或真实) | P4-2 |
| 多子女切换可用 | ChildSwitcher + invalidate 流程通过 | P4-3 |
| 数据范围校验生效 | 前端路由守卫校验 childId 是否在 myChildren 返回列表中 | P4-3 |
| Dashboard 可访问 | 家长登录 → 看到 Dashboard含子女卡片 | P4-4 |
| 健康检查通过 | `GET /api/health` + `GET /api/ready` 200 | P4-1 |
| 测试覆盖率达标 | 单元 ≥ 85% + 集成 ≥ 75% | P4-9 |
| Docker 镜像可构建 | `docker build` 成功 | P4-10 |
| WebSocket 通知可接收 | push-gateway WS 事件正确处理 | P5-1 |
---
@@ -111,14 +196,38 @@ parent-portal 是前端,无下游消费方。但对开发体验提供:
### 4.2 我消费的 mock
在真实上游就绪前parent-portal 使用以下 mock
在真实上游就绪前parent-portal 使用以下 mock(由 `NEXT_PUBLIC_API_MOCKING=enabled` 控制)
- **HTTP/GraphQL mock**使用 MSW 拦截所有请求
- POST /api/auth/login → 返回固定 JWT + UserInfoparent 角色
- POST /api/parent/graphql → 根据 operationName 返回对应 mock 响应(与 parent-bff mock 数据一致
- myChildren mock 必须返回固定 2 个id="student-001" + "student-002",与其他 child* 查询的 student_id 一致
- **GraphQL mock**MSW 拦截 `POST /api/v1/parent/graphql`
- 按 operationName 返回对应 mock 响应(与 parent-bff mock 数据一致
- currentUser → 固定家长id="parent-001", name="王家长", roles=["parent"]
- myChildren 固定 2 个子id="student-001" 李同学 + id="student-002" 李妹妹
- childSummary → 固定仪表盘child_avg_score=85.0, child_class_rank=5
- childGrades → 固定 5 个成绩
- childAttendance → 固定 10 条考勤
- childHomework → 固定 3 个作业
- myNotifications → 固定 10 条通知
- 所有 mock 响应定义在 `apps/parent-portal/src/mocks/fixtures/*.json`
- **WebSocket mock**:使用 mock-socket 库
- 连接后每 30 秒推送 1 条 mock 通知
- **JWT mock**使用固定 mock JWT存入 httpOnly cookie
- **环境切换**通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制,上游就绪后设为 `disabled`
- **HTTP mock**MSW 拦截 `POST /api/v1/iam/login` → 返回固定 JWT + UserInfoparent 角色)
- **WebSocket mock**mock-socket 库,连接后每 30 秒推送 1 条 mock 通知
- **JWT mock**:固定 mock JWT存入 httpOnly cookie
- **环境切换**`NEXT_PUBLIC_API_MOCKING=enabled`(开发)/ `disabled`(上游就绪后)
- **数据一致性**myChildren mock 必须返回固定 2 个孩子student-001 + student-002与所有 child* 查询的 student_id 一致(否则前端数据范围校验失败)
---
## §5 待协调事项(指向 objections
以下事项已提请 coord 仲裁,仲裁结果可能影响本契约:
| ISSUE | 影响章节 | 当前处理 |
| ----- | -------- | -------- |
| ISSUE-001REST vs GraphQL | §2.4 | 按 GraphQL 编写(依 ARB-001待 coord 确认 |
| ISSUE-004登录端点 | §2.3 | 暂用 `POST /api/v1/iam/login`,待 coord 确认 |
| ISSUE-006HTTP 端点分类) | §1.2 | 已修正为"无对外 HTTP API" |
| ISSUE-007MF 配置文件名) | §1.6 | 已修正为 `next.config.js` |
| ISSUE-008GraphQL 路径前缀) | §2.4 | 已修正为 `/api/v1/parent/graphql` |
| ISSUE-009switchChild Mutation | §2.4 | 列为待仲裁,标注两种方案 |
| ISSUE-010iam GetChildrenByParent 缺失) | §3.1 | P0 阻塞,用 mock 开发 |
详见 [objections/parent-portal_issue.md](../objections/parent-portal_issue.md)。

View File

@@ -1,7 +1,9 @@
# push-gateway 对接契约
> 负责人ai02
> 关联:[matrix.md](./matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 关联:[matrix.md](./matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[02-architecture-design.md](../../../services/push-gateway/docs/02-architecture-design.md)
> 版本v22026-07-10对齐总裁裁决 ISSUE-053/055/056/058 + 02 文档 + 现码)
> 变更摘要:① topic 改 `edu.notification.requested`ISSUE-053② /internal/send → /internal/push对齐代码 + 总裁 §4.2);③ 鉴权 mTLS → X-Internal-Token对齐总裁 §7.2);④ 移除 /sse待 ISSUE-001 仲裁);⑤ 新增 /internal/online 端点
---
@@ -9,31 +11,83 @@
### 1.1 gRPC 接口(如有)
无对外 gRPC。push-gateway 是 WebSocket/SSE 推送入口。
**无对外 gRPC**。push-gateway 是 WebSocket 推送入口,仅通过 HTTP /internal/* 接收 msg 服务调用
### 1.2 HTTP 端点(如有)
> 协议选型决策coord 已采纳 P1见 [02 文档 §5.4](../../../services/push-gateway/docs/02-architecture-design.md)HTTP /internal/* + Kafka 双通道,不走 gRPC。理由推送结果需同步返回delivered/onlineHTTP 同步响应更直接;广播走 Kafka 解耦。
| Method | Path | 用途 | 认证 |
| ------ | ------------------- | -------------------------------------- | -------------------------------- |
| GET | /ws | WebSocket 升级端点(实时推送通知) | JWT 必需query param 传 token |
| GET | /sse | SSE 推送端点(备选实时通道) | JWT 必需 |
| POST | /internal/broadcast | 内部广播接口msg 服务触发) | 内网 mTLS |
| POST | /internal/send | 内部单推接口msg 服务触发) | 内网 mTLS |
| GET | /healthz | 健康检查liveness | 公开 |
| GET | /readyz | 就绪检查readiness含 Kafka 连通性) | 公开 |
| GET | /metrics | Prometheus 指标端点 | 公开(内网) |
### 1.2 HTTP 端点
| Method | Path | 用途 | 认证 | 请求体 | 响应 |
| ------ | --------------------------- | -------------------------------------- | --------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| GET | /ws | WebSocket 升级端点(实时推送通知) | JWT RS256query `?token=``Authorization` | — | 升级为 WebSocket 长连接 |
| POST | /internal/push | 内部单推接口msg 服务触发) | `X-Internal-Token` 头 | `{user_id, event, data, ttl?}` | `{success, delivered, online}` |
| POST | /internal/broadcast | 内部广播接口msg 服务触发) | `X-Internal-Token` 头 | `{event, data, filter?}` | `{success, reached}` |
| GET | /internal/online/\<userID\> | 查询用户在线状态 | `X-Internal-Token` 头 | — | `{online: bool, instances: []}` |
| GET | /healthz | 健康检查liveness | 公开 | — | `{status:"ok", service, version, connections}` |
| GET | /readyz | 就绪检查readiness | 公开 | — | `{status, degraded?}`Redis/Kafka 软失败时 degraded:true |
| GET | /metrics | Prometheus 指标端点 | 公开(内网) | — | Prometheus 文本格式 |
**待 ISSUE-001 仲裁项**
- ~~`GET /sse`~~02 文档 §10 建议不支持contract v1 列出但与 02 冲突,待 coord 仲裁移除)
**待 ISSUE-002 仲裁项**
- 内部 API 鉴权头命名:本契约采用 `X-Internal-Token`(对齐总裁裁决 §7.2);现码为 `X-Internal-Key`,待仲裁确认后统一
**`X-Internal-Token` 校验机制**
- 启动时从 `INTERNAL_API_TOKEN` 环境变量加载
- 与 msg 服务共享同一密钥K8s Secret 注入)
- DevMode`DEV_MODE=true`)下跳过校验,便于本地联调
- 缺失/不匹配 → 401 + `PUSH_UNAUTHORIZED`
**响应语义**/internal/push
- `delivered: true`:本实例或跨实例成功投递到至少一个连接
- `online: false`用户离线msg 应走离线推送SMS/邮件)
- `delivered: false, online: true`:投递失败(连接满/异常msg 应重试或落库
### 1.3 GraphQL schema如 BFF
不适用。
不适用。push-gateway 非 BFF。
### 1.4 Kafka 事件发布(如有)
。push-gateway 不发布事件,仅消费事件触发推送。
**无**。push-gateway 不发布任何 Kafka 事件,仅消费事件触发推送。推送结果通过 HTTP /internal/push 同步响应返 msg 服务。
### 1.5 错误码前缀
`PUSH_`如 PUSH_CONNECTION_FAILED、PUSH_CHANNEL_CLOSED、PUSH_AUTH_INVALID
`PUSH_`见 [matrix.md](./matrix.md) §6 错误码前缀矩阵
**错误码清单**(对齐 [02 文档 §6.2](../../../services/push-gateway/docs/02-architecture-design.md)
| 错误码 | HTTP | 触发条件 |
| --------------------------- | ---- | ---------------------- |
| `PUSH_UNAUTHORIZED` | 401 | 缺失/无效 token |
| `PUSH_INVALID_REQUEST` | 400 | JSON 解析失败 |
| `PUSH_INVALID_PAYLOAD` | 400 | event/data 字段缺失 |
| `PUSH_TOO_MANY_CONNECTIONS` | 429 | 单用户连接数超限(>5 |
| `PUSH_INTERNAL_ERROR` | 500 | panic / Redis 不可达 |
### 1.6 WebSocket 应用层消息协议
**服务端 → 客户端**
```json
{
"type": "message",
"event": "notification.created",
"data": { ... },
"seq": 12345,
"timestamp": "2026-07-09T..."
}
```
**客户端 → 服务端**
- WebSocket Ping 控制帧心跳30s 间隔,非文本消息)
- 重连时:`GET /ws?token=&session_id=<id>&last_seq=<n>`P6 实现)
**心跳规则**RFC 6455 控制帧):
- 客户端每 30s 发送 Ping
- 服务端自动回 Ponggorilla/websocket 默认)
- 服务端 `SetReadDeadline(60s)`60s 无消息则关闭连接
---
@@ -41,24 +95,49 @@
### 2.1 gRPC 调用(同步)
无主动 gRPC 调用上游。
**无主动 gRPC 调用上游**
> JWT 公钥通过 HTTP `GET iam/.well-known/jwks.json` 拉取(非 gRPC由 `shared-go/auth/jwks` 实现5 分钟缓存刷新。
### 2.2 Kafka 事件订阅(异步)
| Topic | Event | 发布方 | mock 策略 |
| --------------------------- | --------------------------------- | ---------- | --------------------------------------------------------------------------- |
| edu.msg.notification.events | NotificationEventaction: sent | msg (ai10) | msg 就绪前不订阅,使用本地定时器每 30 秒推送 1 条 mock 通知到所有连接客户端 |
| Topic | Event | 发布方 | mock 策略 |
| ------------------------------ | ------------------------ | ---------- | --------------------------------------------------------------------------- |
| `edu.notification.requested` | NotificationRequested | msg (ai10) | msg 就绪前不订阅,使用本地定时器每 30 秒推送 1 条 mock 通知到所有在线客户端 |
> **topic 命名对齐 ISSUE-053 裁决**[president-final-rulings.md](../president-final-rulings.md) §1.5):禁止抽象名 `edu.*.events`,统一 `edu.<domain>.<aggregate>.<action>` 格式。
> 原 contract v1 写的 `edu.msg.notification.events` 已废弃。
**消费语义**
- Consumer Group`push-gateway`
- 至少一次at-least-once消费失败重试 3 次后入死信队列
- 幂等性:基于 `event_id` Redis SETNX 去重TTL 24h
### 2.3 HTTP 调用(如有)
无。
| 调用方 | Method | Path | 用途 | 时机 |
| -------------- | ------ | --------------------------------- | --------------------------------- | ---------- |
| push-gateway | GET | `iam/.well-known/jwks.json` | 拉取 RS256 公钥校验 WebSocket JWT | iam 就绪后 |
### 2.4 内部接口msg 调用 push-gateway
### 2.4 Redis 协议
| 用途 | 数据结构 | Key 模式 | TTL |
| -------------------- | --------------- | ------------------------------------ | --------------- |
| 在线用户所在实例集合 | SET | `edu:push:online:<userID>` | 60s心跳续期 |
| 单连接元数据 | HASH | `edu:push:session:<userID>:<connID>` | 60s |
| 跨实例定向推送 channel | Pub/Sub channel | `edu:push:channel:user:<userID>` | — |
| 跨实例广播 channel | Pub/Sub channel | `edu:push:channel:broadcast` | — |
| 幂等去重 | SETNX | `edu:push:idempotent:<event_id>` | 24h |
> **Hub 启动重建机制**(对齐 ISSUE-058实例启动时遍历内存连接 SADD + EXPIRE 60s先清空 Redis 中本 instanceID 旧成员避免幽灵成员;实例崩溃 SET 自然过期60s
### 2.5 内部接口msg 调用 push-gateway
| 被调用方 | Method.Path | 用途 | 说明 |
| ------------ | ------------------------ | ------------------ | ---------------------------------------- |
| push-gateway | POST /internal/push | msg 服务单用户推送 | msg 渲染模板后定向推送给目标用户 |
| push-gateway | POST /internal/broadcast | msg 服务批量推送 | msg 收到业务事件后渲染模板,调此接口广播 |
| push-gateway | POST /internal/send | msg 服务单用户推送 | msg 渲染后定向推送给目标用户 |
| push-gateway | GET /internal/online/<userID> | msg 查在线状态 | msg 决定走在线推送还是离线推送SMS/邮件) |
---
@@ -66,18 +145,22 @@
### 3.1 我依赖的上游就绪标志
- [ ] msg gRPC 50056 启用ai10—— 通知事件来源
- [ ] edu.msg.notification.events topic 有事件发布ai10
- [ ] iam gRPC 50052 启用ai06—— WebSocket 连接时 JWT 验签可选push-gateway 可独立验签)
- [ ] **shared-go 包骨架**coord 批次 0.14`packages/shared-go` 含 tracer/logger/jwks/env 4 模块
- [ ] **iam JWT RS256 + JWKS 端点**ai06 批次 1iam gRPC 50052 + `/.well-known/jwks.json` 可访问
- [ ] **msg gRPC + Kafka topic**ai10 批次 4msg gRPC 50056 + `edu.notification.requested` topic 有事件发布
- [x] **Redis 基础设施**coord P1Redis 7.x 可访问 ✅ 已就绪
- [x] **Kafka 基础设施**coord P1Kafka 可访问 ✅ 已就绪
- [ ] **ISSUE-001~007 仲裁**coord[coord.md](../coord.md) 追加 ARB-003+ 仲裁章节
### 3.2 我的就绪标志(供下游消费)
- [ ] push-gateway HTTP :8081 启用(/healthz 返 200
- [ ] /readyz 返 200Kafka 连通性检查通过
- [ ] WebSocket /ws 端点可升级连接JWT 鉴权后建立长连接)
- [ ] SSE /sse 端点可建立 EventStream
- [ ] /internal/broadcast + /internal/send 接收 msg 推送并下发到在线客户端
- [ ] Kafka consumer edu.msg.notification.events 订阅成功
- [ ] push-gateway HTTP :8081 启用(`GET /healthz` 返 200
- [ ] /readyz 返 200Redis/Kafka 软失败检查,`degraded` 字段
- [ ] WebSocket /ws 端点可升级连接JWT RS256 鉴权后建立长连接)
- [ ] /internal/push + /internal/broadcast 接收 msg 推送并下发到在线客户端
- [ ] /internal/online/<userID> 查在线状态可调用
- [ ] Kafka consumer `edu.notification.requested` 订阅成功Consumer Group `push-gateway` lag=0
- [ ] /metrics 暴露 8+ 自定义指标(`push_gateway_*` 系列)
---
@@ -88,14 +171,41 @@
在 push-gateway 真实就绪前,为下游(各前端 portal提供以下 mock
- **WebSocket mock**:前端开发期使用 mock-socket 库模拟 WS 连接
- 连接成功后每 30 秒推送 1 条 mock 通知type="system", title="测试通知"
- **SSE mock**前端使用 EventSource polyfill本地定时推送 mock 事件
- **HTTP mock**/internal/* 接口返回 200 success
- 连接成功后每 30 秒推送 1 条 mock 通知(`type="system"`, `event="notification.created"`, `data={title:"测试通知"}`
- **HTTP mock**/internal/* 接口返回 `{success:true, delivered:true, online:true}`
### 4.2 我消费的 mock
在真实上游就绪前push-gateway 使用以下 mock
- **NotificationEvent mock**msg 就绪前push-gateway 内置定时器每 30 秒生成 1 条 mock NotificationEventaction=sent,推送到所有在线客户端
- **JWT 验签**iam 就绪前使用本地固定 mock RS256 公钥验签 WebSocket 连接的 token
- **Kafka 订阅**msg 就绪前不启动 Kafka consumer使用本地定时器替代
- **NotificationEvent mock**msg 就绪前push-gateway 内置定时器每 30 秒生成 1 条 mock NotificationRequested 事件,推送到所有在线客户端
- **JWT 验签 mock**iam 就绪前使用本地固定 mock RS256 公钥验签 WebSocket 连接的 token(或 DevMode `dev-token` 跳过)
- **Kafka 订阅 mock**msg 就绪前不启动 Kafka consumer使用本地定时器替代
- **JWKS fetcher mock**iam 就绪前 shared-go/jwks 返回硬编码公钥
---
## §5 与 02 文档、现码、总裁裁决的对齐说明
| 维度 | 现 code | 02 文档 | 总裁裁决 | 本 contract 采用 | 对齐任务 |
| ---- | ------- | ------- | -------- | ---------------- | -------- |
| 内部端点路径 | `/internal/push` | `/internal/push` | §4.2 as-is 采纳 02 §4.2 | `/internal/push` | ✅ 已对齐v1 写 `/internal/send` 已修正) |
| 鉴权头名 | `X-Internal-Key` | `X-Internal-Token` | §7.2 "X-Internal-Token 重命名" | `X-Internal-Token` | 待 ISSUE-002 仲裁后改代码 |
| 鉴权环境变量 | `INTERNAL_API_KEY` | `INTERNAL_API_TOKEN` | §7.2 | `INTERNAL_API_TOKEN` | 待 ISSUE-002 仲裁后改代码 |
| 鉴权机制 | 共享密钥 | 共享密钥 | — | 共享密钥v1 写 mTLS 已废弃) | ✅ 已对齐 |
| Kafka topic | —(未实现) | `edu.notification.events` | §1.5 ISSUE-053 → `edu.notification.requested` | `edu.notification.requested` | ✅ 已对齐 ISSUE-053 |
| /readyz Redis 失败策略 | —(仅返连接数) | 返 503 硬失败 | §4.3 ISSUE-058 "仅告警不阻塞" | 软失败 200 + `degraded:true` | 待 ISSUE-006 仲裁最终策略 |
| /readyz Kafka 失败策略 | — | 未描述 | §3.3 ISSUE-055 软失败 | 软失败 200 + `degraded:true` | ✅ 已对齐 ISSUE-055 |
| /sse 端点 | 无 | §10 建议不支持 | — | 不提供(待 ISSUE-001 仲裁) | 待 ISSUE-001 仲裁 |
| 容量目标 | — | 10w+ | — | 10w+ | 待 ISSUE-003 仲裁更新 modules/README |
| 错误码前缀 | 无前缀 | `PUSH_*` | — | `PUSH_*` | 待 P5 实现时统一 |
| 心跳协议 | 文本 ping/pong | RFC 6455 控制帧 | — | RFC 6455 控制帧 | 待 P5 任务 4.3 重构 |
---
## §6 变更历史
| 版本 | 日期 | 变更内容 | 变更依据 |
| ---- | ---------- | ------------------------------------------------------------------------ | ------------------------------------- |
| v1 | 2026-07-09 | 初始版本coord 生成) | — |
| v2 | 2026-07-10 | topic 改 `edu.notification.requested`/internal/send → /internal/push鉴权 mTLS → X-Internal-Token移除 /sse待仲裁新增 /internal/online新增 §5 对齐表 | ISSUE-053/055/056/058 + 总裁 §4.2/§7.2 |

View File

@@ -1,50 +1,310 @@
# student-bff 对接契约
> 负责人ai04
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[core_edu.proto](../../../packages/shared-proto/proto/core_edu.proto)、[content.proto](../../../packages/shared-proto/proto/content.proto)、[analytics.proto](../../../packages/shared-proto/proto/analytics.proto)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)
> 阶段P3批次 2 启动)
> 版本v2对齐 coord-final-decisions B1-B8 + president-final-rulings §2.2/2.3/2.4/2.6/2.7/2.8/2.9
> 日期2026-07-10
> 关联文档:
>
> - [matrix.md](../matrix.md)
> - [coord-final-decisions.md](../../coord-final-decisions.md) §2 BFF 专项裁决 B1-B8
> - [president-final-rulings.md](../../president-final-rulings.md) §2.2 GraphQL schema 仲裁机制
> - [iam.proto](../../../../packages/shared-proto/proto/iam.proto)
> - [core_edu.proto](../../../../packages/shared-proto/proto/core_edu.proto)
> - [content.proto](../../../../packages/shared-proto/proto/content.proto)
> - [analytics.proto](../../../../packages/shared-proto/proto/analytics.proto)
> - [msg.proto](../../../../packages/shared-proto/proto/msg.proto)
---
## §0 裁决对齐声明
本文件已对齐以下裁决(无遗漏):
| 裁决编号 | 内容 | 本文件章节 |
| -------- | ------------------------------------------ | -------------- |
| B1 | P2 起直接 GraphQLGraphQL Yoga + DataLoader | §1.2 / §1.3 |
| B2 | 首次实现即 gRPC 调用下游 | §2.1 / §2.4 |
| B3 | BFF 豁免 @RequirePermission | §1.6 / §1.7 |
| B4 | 全部 BFF 强制自我越权防御 | §1.7 |
| B5 | 错误码前缀 `BFF_STUDENT_` | §1.6 |
| B6 | Redis 5-30s 短缓存 | §7 |
| B7 | P2-P4 不订阅 KafkaP5 后再订阅 | §1.5 / §2.2 |
| B8 | DownstreamClient 抽象回写 teacher-bff | §2.4 |
| G14 | 错误码前缀服务名大写 | §1.6 |
| F4 | i18n key `error.bffStudent.<code_snake>` | §1.6 |
| F7 | 权限点命名 `<RESOURCE>_<ACTION>[_<SCOPE>]` | §1.3.4 |
| F9 | 前端用 urql/apollo 消费 GraphQL | §1.2 |
| §2.2 | GraphQL schema 仲裁机制 | §1.3.2 / §1.3.3 |
| §2.3 | BFF 跨阶段扩展例外 | §3 |
| §2.4 | /readyz 探针按阶段扩展 | §4 |
| §2.6 | 降级模式方案 Bdata 内 degraded 字段) | §1.4.2 |
| §2.7 | BFF 错误码语义区分3 类) | §1.6 / §1.7.3 |
| §2.8 | Dashboard Query Resolver P2 实现方式 | §1.3.5 |
| §2.9 | 越权防御 P2 实现方式DEV_MODE 放行) | §1.7.4 |
| §5.1 | admin-portal 复用 teacher-bff不占 student-bff 命名空间 | §1.3.6 |
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 服务基础信息
无对外 gRPC。student-bff 是 GraphQL 聚合层。
| 项目 | 值 |
| ------------- | --------------------------------------------- |
| 服务名 | student-bff |
| 服务类型 | BFF 聚合层(无 DB无 Outbox |
| HTTP 端口 | 3009 |
| gRPC 端口 | 不暴露BFF 仅对下游走 gRPC不对外提供 gRPC |
| 路由前缀 | `/student/*`api-gateway 透传) |
| 部署目录 | `services/student-bff/` |
| 角色要求 | `student`JWT 必需 + student 角色) |
### 1.2 HTTP 端点(如有)
### 1.2 HTTP 端点
| Method | Path | 用途 | 认证 |
| ------ | -------- | ----------------------------------------- | ----------------------- |
| POST | /graphql | 学生 BFF GraphQL 端点 | JWT 必需 + student 角色 |
| GET | /graphql | GraphQL Playground开发环境 | 开发环境公开 |
| GET | /healthz | 健康检查liveness | 公开 |
| GET | /readyz | 就绪检查readiness含下游 gRPC 连通性) | 公开 |
| Method | Path | 用途 | 认证 | 实现 |
| ------ | ---------- | ------------------------------------------ | ---------------------------- | ------- |
| POST | /graphql | 学生 BFF GraphQL 端点B1 裁决GraphQL Yoga | JWT 必需 + student 角色 | P3 |
| GET | /graphql | GraphQL Playground开发环境 | 开发环境公开 | P3 |
| GET | /healthz | 健康检查liveness | 公开 | P3 |
| GET | /readyz | 就绪检查readiness按阶段扩展探针) | 公开 | P3 |
| GET | /metrics | Prometheus 指标端点 | 公开(生产限内网) | P3 |
### 1.3 GraphQL schema如 BFF
> **B1 裁决**P2 起直接 GraphQLGraphQL Yoga + DataLoader禁止 REST → GraphQL 渐进式过渡。
> **B2 裁决**:对下游通信首次实现即 gRPC禁止 HTTP fetch → gRPC 渐进式过渡。
> **F9 裁决**:前端 student-portal 用 urql/apollo 消费 GraphQL。
GraphQL schema 文件路径:`apps/student-bff/src/schema/*.graphql`(端口 :3009
### 1.3 GraphQL schema(核心契约
核心 Query / Mutation 域:
#### 1.3.1 schema 存放路径
- **auth**currentUser聚合 iam.GetUserInfo + GetEffectivePermissions + GetViewports
- **myClasses**:我的班级(聚合 core-edu.ClassService.GetClass + ListStudentsByClass
- **myExams**:我的考试列表(聚合 core-edu.ExamService.ListExamsByClass
- **myHomework**:我的作业(聚合 core-edu.HomeworkService.ListHomeworkByClass + SubmitHomework
- **myGrades**:我的成绩(聚合 core-edu.GradeService.ListGradesByStudent
- **myAttendance**:我的考勤(聚合 core-edu.AttendanceService.ListAttendanceByStudent
- **content**textbooks / chapters / learningPath聚合 content.KnowledgeGraphService.GetLearningPath
- **dashboard**studentDashboard聚合 data-ana.AnalyticsService.GetStudentDashboard
- **weakness**myWeakness聚合 data-ana.AnalyticsService.GetStudentWeakness
- **trend**myTrend聚合 data-ana.AnalyticsService.GetLearningTrend
- **notifications**myNotifications / markAsRead聚合 msg.NotificationService
**强制路径**`packages/shared-ts/contracts/graphql/student-bff.schema.graphql`
### 1.4 Kafka 事件发布(如有)
> 依据 president-final-rulings §2.2.13 个 BFF schema 统一存放于 `packages/shared-ts/contracts/graphql/`,由各 BFF 负责 AI 起草、coord 仲裁。student-bff schema 不放在 `services/student-bff/src/` 下,避免前端 AI 难以发现。
无。student-bff 不发布事件,仅做 gRPC 聚合。
#### 1.3.2 起草与仲裁流程
### 1.5 错误码前缀
依据 president-final-rulings §2.2.6
`BFF_STUDENT_`(如 BFF_STUDENT_UPSTREAM_UNAVAILABLE、BFF_STUDENT_AGGREGATION_FAILED、BFF_STUDENT_FORBIDDEN
1. **起草方**ai04 在批次 1 等待期起草 student-bff GraphQL schema 草案
2. **仲裁方**coord 在**批次 2 启动前**仲裁第一版 schema
3. **消费方**ai14student-portal基于仲裁版 schema 消费
4. **变更流程**
- schema 变更需 PR + ai04BFF+ ai14前端双方 review
- 重大变更(删除字段 / 修改类型)需 coord 仲裁
- 新增字段允许,无需 coord 仲裁,但需通知 ai14
5. **版本管理**SDL-first`schema.graphql` 文件),配合 `graphql-codegen` 生成 TS 类型
#### 1.3.3 schema 设计规范
依据 president-final-rulings §2.2.5
| 规范项 | 规则 |
| -------------- | ----------------------------------------------------------------- |
| 命名风格 | Query/Mutation 用 camelCase`studentDashboard` / `myClasses` |
| 分页规范 | **Relay Cursor Connections**`{ edges, pageInfo, totalCount }` |
| 错误响应 | GraphQL errors 数组 + `extensions.code` + `extensions.traceId` |
| 权限点标注 | 注释形式 `# @permission: DASHBOARD_VIEW`F7 规范) |
| DataScope 标注 | 注释形式 `# @dataScope: OWN`(学生数据隔离 SELF |
| 字段命名 | Type 用 PascalCase字段用 camelCase枚举用 UPPER_SNAKE_CASE |
| 非空与可选 | 必填字段用 `!`,可空字段不标 `!`(避免破坏性变更) |
#### 1.3.4 核心 Query / Mutation 域
> 完整 SDL 定义见 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`ai04 起草coord 仲裁)。
**Query 域**
| Query | 用途 | 聚合下游 RPC | 权限点标注 | DataScope |
| ---------------------- | ------------------------ | ----------------------------------------------------------- | --------------------------- | --------- |
| `currentUser` | 当前学生信息 + 权限 + 视口 | iam.GetUserInfo + GetEffectivePermissions + GetViewports | `# @permission: AUTH_READ` | OWN |
| `myClasses` | 我的班级列表 | core-edu.ClassService.GetClass + ListStudentsByClass | `# @permission: CLASS_READ` | OWN |
| `myExams` | 我的考试列表 | core-edu.ExamService.ListExamsByClass | `# @permission: EXAM_READ` | OWN |
| `myHomework` | 我的作业列表 | core-edu.HomeworkService.ListHomeworkByClass | `# @permission: HOMEWORK_READ` | OWN |
| `myGrades` | 我的成绩列表 | core-edu.GradeService.ListGradesByStudent | `# @permission: GRADE_READ` | OWN |
| `myAttendance` | 我的考勤记录 | core-edu.AttendanceService.ListAttendanceByStudent | `# @permission: ATTENDANCE_READ` | OWN |
| `textbooks` | 教材列表 | content.TextbookService.ListTextbooks | `# @permission: TEXTBOOK_READ` | OWN |
| `chapters` | 章节列表 | content.ChapterService.ListChapters | `# @permission: CHAPTER_READ` | OWN |
| `learningPath` | 学习路径推荐 | content.KnowledgeGraphService.GetLearningPath | `# @permission: LEARNING_PATH_READ` | OWN |
| `studentDashboard` | 学生仪表盘 | data-ana.AnalyticsService.GetStudentDashboard | `# @permission: DASHBOARD_VIEW` | OWN |
| `myWeakness` | 我的薄弱点 | data-ana.AnalyticsService.GetStudentWeakness | `# @permission: WEAKNESS_READ` | OWN |
| `myTrend` | 学习趋势 | data-ana.AnalyticsService.GetLearningTrend | `# @permission: TREND_READ` | OWN |
| `myNotifications` | 我的通知列表 | msg.NotificationService.ListNotifications | `# @permission: NOTIFICATION_READ` | OWN |
| `myNotificationUnreadCount` | 通知未读数 | msg.NotificationService.GetUnreadCount | `# @permission: NOTIFICATION_READ` | OWN |
**Mutation 域**
| Mutation | 用途 | 聚合下游 RPC | 权限点标注 | DataScope |
| ---------------------- | ---------------- | ----------------------------------------- | --------------------------- | --------- |
| `submitHomework` | 提交作业 | core-edu.HomeworkService.SubmitHomework | `# @permission: HOMEWORK_SUBMIT` | OWN |
| `markNotificationAsRead` | 标记通知已读 | msg.NotificationService.MarkAsRead | `# @permission: NOTIFICATION_UPDATE` | OWN |
> **分页**:所有列表 QuerymyClasses / myExams / myHomework / myGrades / myAttendance / textbooks / chapters / myNotifications使用 Relay Cursor Connections 规范,返回 `{ edges, pageInfo, totalCount }`。
> **DataScope=OWN**:学生数据隔离为 SELF所有 Query 透传 `x-user-id` 给下游,下游 Repository 按 DataScope=SELF 过滤。
#### 1.3.5 Dashboard Query Resolver 实现方式§2.8 裁决)
依据 president-final-rulings §2.8
1. **GraphQL schema 设计完整**(含全部 Query/Mutation 字段定义P3 即定型,后续不重构
2. **Resolver 实现**
- **P3 阶段**`studentDashboard` Query Resolver 内部调 iam + core-edu gRPC返回学生基础信息 + 班级列表 + 考试列表 + 作业列表 + 成绩列表
- **P4 扩展**:增加 content教材/章节)+ data-ana仪表盘/薄弱点/趋势)数据源,将 null 字段替换为真实数据
- **P5 扩展**:增加 msg通知未读数+ aiAI 助教入口)数据源
- 未启用的下游字段返回 `null` + `extensions.warning = "field_unavailable_in_p3"`
3. **前端配合**ai14 student-portal 对 null 字段做 UI 降级展示(如"数据加载中"或隐藏模块)
4. **此方案不违反"不分阶段"**schema 即最终方案Resolver 内部数据源扩展属"跨阶段扩展例外"(见 §3
#### 1.3.6 admin-portal 命名空间
依据 president-final-rulings §5.1
- **admin-portal 复用 teacher-bff GraphQL endpoint**,不新建 admin-bff 服务
- **student-bff 不预留 admin schema 命名空间**admin 操作走 teacher-bff 的 `admin.*` 命名空间)
- ai04 无需为 admin-portal 做任何 schema 预留
### 1.4 GraphQL 错误响应格式
#### 1.4.1 GraphQL errors 数组 + ActionState 扩展
依据 president-final-rulings §2.2.3 + G8 裁决:
GraphQL 错误响应遵循标准 errors 数组格式,扩展 ActionState 字段:
```json
{
"errors": [
{
"message": "学生身份验证失败",
"extensions": {
"code": "BFF_STUDENT_UNAUTHORIZED",
"traceId": "abc-123-def-456",
"i18nKey": "error.bffStudent.unauthorized",
"severity": "error"
}
}
],
"data": null
}
```
| 字段 | 类型 | 说明 |
| ------------------- | ------ | ----------------------------------------------- |
| `extensions.code` | string | 错误码BFF_STUDENT_* 前缀,见 §1.6 |
| `extensions.traceId`| string | 全链路追踪 ID由 Gateway 注入 X-Request-Id |
| `extensions.i18nKey`| string | i18n keyF4 规范:`error.bffStudent.<code_snake>` |
| `extensions.severity` | string | `error` / `warning` / `info` |
#### 1.4.2 降级模式(方案 B§2.6 裁决)
依据 president-final-rulings §2.6:当下游服务不可用但需返回部分数据时,采用**方案 B**success=true + error=null + data 内 degraded 字段):
```json
{
"data": {
"studentDashboard": {
"user": { "id": "stu-001", "name": "李同学" },
"classes": [{ "id": "cls-001", "name": "高三1班" }],
"weakness": null,
"degraded": true,
"degradedReason": "data_ana_unavailable",
"degradedFields": ["weakness"]
}
}
}
```
**规则**
1. 降级时 HTTP 200GraphQL `data` 非 null
2. 降级字段返回 `null`,并在父对象内加 `degraded: true` + `degradedReason: string` + `degradedFields: string[]`
3. 前端检查 `data.degraded` 判断降级,对 `degradedFields` 内字段做 UI 降级展示
4. 降级场景示例:
- data-ana gRPC 不可用 → `studentDashboard.weakness` / `myTrend` / `myWeakness` 降级
- content gRPC 不可用 → `textbooks` / `chapters` / `learningPath` 降级
- msg gRPC 不可用 → `myNotifications` / `myNotificationUnreadCount` 降级
### 1.5 Kafka 事件发布
**无**。student-bff 是纯聚合层,不发布 Kafka 事件,不写 Outbox。
### 1.6 错误码前缀与列表
依据 B5 + G14 + F4 + §2.7 裁决:
**错误码前缀**`BFF_STUDENT_`(统一 BFF_ 前缀,服务名大写)
**i18n key 规范**`error.bffStudent.<code_snake>`F4 裁决)
**错误码清单**
| 错误码 | HTTP | 场景 | i18n key |
| ------------------------------------- | ---- | ---------------------------------------------- | --------------------------------------------- |
| `BFF_STUDENT_UNAUTHORIZED` | 401 | x-user-id 缺失或无效 | `error.bffStudent.unauthorized` |
| `BFF_STUDENT_FORBIDDEN_RESOURCE` | 403 | 学生越权访问他人数据(场景 A | `error.bffStudent.forbidden_resource` |
| `BFF_STUDENT_IDENTITY_MISMATCH` | 403 | JWT userId 与请求 body userId 不一致(场景 B | `error.bffStudent.identity_mismatch` |
| `BFF_STUDENT_BAD_GATEWAY` | 502 | 下游 gRPC 调用失败(非业务错误) | `error.bffStudent.bad_gateway` |
| `BFF_STUDENT_UPSTREAM_UNAVAILABLE` | 503 | 下游服务不可用(降级模式触发) | `error.bffStudent.upstream_unavailable` |
| `BFF_STUDENT_AGGREGATION_FAILED` | 500 | 聚合逻辑异常(未知错误) | `error.bffStudent.aggregation_failed` |
| `BFF_STUDENT_VALIDATION_ERROR` | 400 | 输入参数校验失败Zod 校验) | `error.bffStudent.validation_error` |
| `BFF_STUDENT_GRAPHQL_PARSE_ERROR` | 400 | GraphQL 语法解析错误 | `error.bffStudent.graphql_parse_error` |
| `BFF_STUDENT_GRAPHQL_VALIDATION_ERROR`| 400 | GraphQL 字段类型校验错误 | `error.bffStudent.graphql_validation_error` |
| `BFF_STUDENT_RATE_LIMITED` | 429 | 限流触发Gateway 层处理BFF 兜底) | `error.bffStudent.rate_limited` |
> **B3 裁决澄清**BFF 豁免 `@RequirePermission` 指不做"功能权限决策"(如"能否查看仪表盘"),但必须做"数据权限防御"(如"只能看自己的数据"),见 §1.7。
### 1.7 BFF 越权防御契约B4 裁决)
#### 1.7.1 越权防御场景
依据 B4 + §2.7 裁决student-bff 强制自我越权防御:
| 场景 | 描述 | 防御方式 |
| ---- | ---------------------------------------------- | --------------------------------------------------- |
| A | 学生查询他人数据(如 query 传入非自己 userId | AuthorizationGuard 比对 `x-user-id` 与查询参数 |
| B | JWT userId 与请求 body userId 不一致 | Mutation 入参校验,拒绝不一致请求 |
#### 1.7.2 AuthorizationGuard 接口
依据 §2.9 裁决:
```typescript
// services/student-bff/src/middleware/authorization.guard.ts
export interface AuthorizationGuard {
/**
* 校验学生是否有权访问指定资源
* @param currentUserId 从 x-user-id header 获取
* @param resourceUserId 查询参数中的 userId
* @returns true 允许访问false 拒绝
*/
canAccessSelfData(currentUserId: string, resourceUserId: string): Promise<boolean>;
}
```
#### 1.7.3 3 类错误码语义§2.7 裁决)
| 错误码 | HTTP | 场景 | 触发条件 |
| -------------------------------- | ---- | ---------------------------------------------------- | ----------------------------------------- |
| `BFF_STUDENT_UNAUTHORIZED` | 401 | x-user-id 缺失或无效 | header 无 x-user-id 或为空 |
| `BFF_STUDENT_FORBIDDEN_RESOURCE` | 403 | 学生越权访问他人数据(场景 A | canAccessSelfData 返回 false |
| `BFF_STUDENT_IDENTITY_MISMATCH` | 403 | JWT userId 与请求 body userId 不一致(场景 B | Mutation submitHomework 等 body userId 不匹配 |
#### 1.7.4 P3 实现方式§2.9 裁决)
依据 §2.9 裁决:
1. **P3 抽象 AuthorizationGuard 接口**`canAccessSelfData`,见 §1.7.2
2. **P3 内部实现为"DEV_MODE 放行 + 生产拒绝"**(保守策略):
- `DEV_MODE=true`:放行所有请求,仅记录 warn 日志
- `DEV_MODE=false`:严格校验,越权返回 `BFF_STUDENT_FORBIDDEN_RESOURCE`
3. **Redis 缓存**P3 后期接入):
- key: `authz:student:{userId}`
- value: 用户基础信息userId / roles / classIds
- TTL: 5min
- AuthorizationGuard 优先查缓存,缓存未命中调 iam gRPC
4. **此方案不违反"不分阶段"**Guard 接口即最终方案P3→P3 后期仅替换内部实现(属"跨阶段扩展例外",见 §3
**P3 阶段生产环境**Guard 全部拒绝时student-bff P3 端到端验证仅限 DEV_MODE。生产环境 P3 不接入流量(仅 dev 测试P3 后期接入真实校验后正式上线。
---
@@ -52,79 +312,288 @@ GraphQL schema 文件路径:`apps/student-bff/src/schema/*.graphql`(端口 :
### 2.1 gRPC 调用(同步)
| 被调用方 | Service.RPC | 用途 | mock 策略 |
| --------------- | ----------------------------------------- | ---------------- | ------------------------------------------- |
| iam (ai06) | IamService.GetUserInfo | 获取当前学生信息 | iam 就绪前返回固定 UserInfostudent 角色) |
| iam (ai06) | IamService.GetEffectivePermissions | 权限校验 | iam 就绪前返回学生权限集 |
| iam (ai06) | IamService.GetViewports | 学生导航菜单 | iam 就绪前返回固定视口列表 |
| core-edu (ai08) | ClassService.GetClass | 我的班级详情 | core-edu 就绪前返回固定 ClassInfo |
| core-edu (ai08) | ClassService.ListStudentsByClass | 班级同学名单 | core-edu 就绪前返回固定 30 个 StudentInfo |
| core-edu (ai08) | ExamService.ListExamsByClass | 我的考试 | core-edu 就绪前返回固定 2 个 Exam |
| core-edu (ai08) | HomeworkService.ListHomeworkByClass | 我的作业 | core-edu 就绪前返回固定 3 个 Homework |
| core-edu (ai08) | HomeworkService.SubmitHomework | 提交作业 | core-edu 就绪前返回 success=true |
| core-edu (ai08) | GradeService.ListGradesByStudent | 我的成绩 | core-edu 就绪前返回固定 5Grade |
| core-edu (ai08) | AttendanceService.ListAttendanceByStudent | 我的考勤 | core-edu 就绪前返回固定 10 条 Attendance |
| content (ai09) | TextbookService.ListTextbooks | 教材列表 | content 就绪前返回固定 5 个教材 |
| content (ai09) | ChapterService.ListChapters | 章节列表 | content 就绪前返回固定章节树 |
| content (ai09) | KnowledgeGraphService.GetLearningPath | 学习路径 | content 就绪前返回固定 8 个知识点推荐顺序 |
| data-ana (ai11) | AnalyticsService.GetStudentDashboard | 学生仪表盘 | data-ana 就绪前返回固定仪表盘 |
| data-ana (ai11) | AnalyticsService.GetStudentWeakness | 我的薄弱点 | data-ana 就绪前返回固定 3 个 weak_points |
| data-ana (ai11) | AnalyticsService.GetLearningTrend | 学习趋势 | data-ana 就绪前返回固定趋势数据 |
| msg (ai10) | NotificationService.ListNotifications | 学生通知 | msg 就绪前返回固定 10 条通知 |
| msg (ai10) | NotificationService.MarkAsRead | 标记已读 | msg 就绪前返回 success=true |
依据 B2 裁决student-bff 对下游全部走 gRPC禁止 HTTP fetch
| 被调用方 | Service.RPC | 用途 | 启用阶段 | mock 策略 |
| --------------- | ----------------------------------------- | ---------------- | -------- | ------------------------------------------- |
| iam (ai06) | IamService.GetUserInfo | 获取当前学生信息 | P3 | iam 就绪前返回固定 UserInfostudent 角色) |
| iam (ai06) | IamService.GetEffectivePermissions | 权限校验 | P3 | iam 就绪前返回学生权限集 |
| iam (ai06) | IamService.GetViewports | 学生导航菜单 | P3 | iam 就绪前返回固定视口列表 |
| iam (ai06) | IamService.GetEffectiveDataScope | DataScope 透传 | P3 | iam 就绪前返回 `SELF` |
| core-edu (ai08) | ClassService.GetClass | 我的班级详情 | P3 | core-edu 就绪前返回固定 ClassInfo |
| core-edu (ai08) | ClassService.ListStudentsByClass | 班级同学名单 | P3 | core-edu 就绪前返回固定 30 个 StudentInfo |
| core-edu (ai08) | ExamService.ListExamsByClass | 我的考试 | P3 | core-edu 就绪前返回固定 2Exam |
| core-edu (ai08) | HomeworkService.ListHomeworkByClass | 我的作业 | P3 | core-edu 就绪前返回固定 3 个 Homework |
| core-edu (ai08) | HomeworkService.SubmitHomework | 提交作业 | P3 | core-edu 就绪前返回 success=true |
| core-edu (ai08) | GradeService.ListGradesByStudent | 我的成绩 | P3 | core-edu 就绪前返回固定 5 个 Grade |
| core-edu (ai08) | AttendanceService.ListAttendanceByStudent | 我的考勤 | P3 | core-edu 就绪前返回固定 10 条 Attendance |
| content (ai09) | TextbookService.ListTextbooks | 教材列表 | P4 | content 就绪前返回固定 5 个教材 |
| content (ai09) | ChapterService.ListChapters | 章节列表 | P4 | content 就绪前返回固定章节树 |
| content (ai09) | KnowledgeGraphService.GetLearningPath | 学习路径 | P4 | content 就绪前返回固定 8 个知识点推荐顺序 |
| data-ana (ai11) | AnalyticsService.GetStudentDashboard | 学生仪表盘 | P4 | data-ana 就绪前返回固定仪表盘 |
| data-ana (ai11) | AnalyticsService.GetStudentWeakness | 我的薄弱点 | P4 | data-ana 就绪前返回固定 3 个 weak_points |
| data-ana (ai11) | AnalyticsService.GetLearningTrend | 学习趋势 | P4 | data-ana 就绪前返回固定趋势数据 |
| msg (ai10) | NotificationService.ListNotifications | 学生通知 | P5 | msg 就绪前返回固定 10 条通知 |
| msg (ai10) | NotificationService.GetUnreadCount | 通知未读数 | P5 | msg 就绪前返回固定 count=3 |
| msg (ai10) | NotificationService.MarkAsRead | 标记已读 | P5 | msg 就绪前返回 success=true |
> **命名对齐**coord §5.2
> - `AnalyticsService.GetStudentDashboard`(无 Stats 后缀,统一命名)
> - `KnowledgeGraphService.GetLearningPath`(禁用 ContentService 命名)
### 2.2 Kafka 事件订阅(异步)
无。student-bff 不订阅 Kafka 事件,仅做同步 gRPC 聚合。
依据 B7 裁决:
### 2.3 HTTP 调用(如有
- **P3-P4 阶段****不订阅 Kafka**(仅同步 gRPC 聚合
- **P5 阶段**push-gateway 落地后,评估是否订阅 Kafka`edu.identity.user.role_changed` 用于权限缓存失效)
无。
**P5 可选订阅的 topic**(待 P5 评估):
| Topic | 用途 | 触发动作 |
| ------------------------------- | -------------------------- | ------------------------------------- |
| `edu.identity.user.role_changed` | 学生角色变更,失效权限缓存 | iam 发布student-bff 清除 Redis 缓存 |
> **P3-P4 降级方案**:权限缓存用短 TTL5min兜底不订阅 Kafka 事件。
### 2.3 HTTP 调用
**无**。依据 B2 裁决student-bff 对下游全部走 gRPC禁止 HTTP fetch。
### 2.4 DownstreamClient 抽象层B8 裁决)
依据 B8 裁决:
1. **回写 teacher-bff**DownstreamClient 作为 BFF 模式 v2 标准抽象,回写到 teacher-bff3 个 BFFteacher-bff / student-bff / parent-bff统一使用
2. **抽象位置**`packages/shared-ts/src/bff/downstream-client.ts`coord 维护)
3. **核心能力**
```typescript
export class DownstreamClient {
/**
* gRPC 调用封装
* @param service 下游服务名(如 'iam' / 'core-edu'
* @param method RPC 方法名(如 'GetUserInfo'
* @param request 请求 message
* @param options 超时 / 重试 / traceId 透传
*/
call<TRequest, TResponse>(
service: string,
method: string,
request: TRequest,
options?: CallOptions,
): Promise<TResponse>;
}
interface CallOptions {
timeoutMs?: number; // 默认 5000ms
retryCount?: number; // 默认 2
retryBackoffMs?: number; // 默认 100ms指数退避
traceId?: string; // 从 x-request-id header 获取
metadata?: Record<string, string>; // gRPC metadata含 x-user-id / x-user-roles / x-dataScope
}
```
4. **student-bff 使用方式**
```typescript
// services/student-bff/src/auth/auth.service.ts
import { DownstreamClient } from '@edu/shared-ts/bff/downstream-client';
@Injectable()
export class AuthService {
constructor(private readonly downstream: DownstreamClient) {}
async getCurrentUser(userId: string): Promise<UserInfo> {
return this.downstream.call('iam', 'GetUserInfo', { userId }, {
metadata: { 'x-user-id': userId },
});
}
}
```
5. **回写义务**ai04 在 P3 实现时,将 DownstreamClient 抽象回写到 teacher-bff替换 teacher-bff 现有的散落 fetch 调用),保证 3 个 BFF 统一。
---
## §3 就绪信号
## §3 跨阶段扩展例外规则§2.3 裁决)
### 3.1 我依赖的上游就绪标志
依据 president-final-rulings §2.3
- [ ] iam gRPC 50052 启用ai06
- [ ] core-edu gRPC 50053 启用ai08
- [ ] content gRPC 50054 启用ai09
- [ ] data-ana gRPC 50055 启用ai11
- [ ] msg gRPC 50056 启用ai10
### 3.1 允许扩展(无需 coord 仲裁
### 3.2 我的就绪标志(供下游消费
1. 新增下游 gRPC 调用(新增 RPC 方法到 DownstreamClient
2. 新增下游配置gRPC endpoint 配置)
3. 新增 /readyz 探针(按阶段启用,见 §4
4. Dashboard Query Resolver 内部数据源扩展null 字段 → 真实数据)
5. AuthorizationGuard 内部实现替换DEV_MODE 放行 → 真实 gRPC 校验 + Redis 缓存)
- [ ] student-bff GraphQL :3009 启用(/healthz 返回 200
- [ ] /readyz 返回 200含 5 个下游 gRPC 连通性检查)
- [ ] GraphQL schema 可内省POST /graphql 返回 schema
- [ ] 核心 Query 可执行currentUser / myClasses / studentDashboard / myGrades
- [ ] 核心 Mutation 可执行submitHomework / markAsRead
### 3.2 禁止变更(需 coord 仲裁
1. 修改已有 RPC 调用的签名或返回类型
2. 删除已实现的 RPC 调用(除非下游服务下线)
3. 修改 GraphQL schema 已有字段的类型(新增字段允许,无需仲裁)
4. 修改 /readyz 已有探针的检查项(只能新增,不能修改)
### 3.3 验收标准
扩展时必须:
1. 更新 student-bff 02-architecture-design.md 下游调用矩阵§7
2. 更新 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`
3. 运行 `pnpm run arch:scan` 更新 arch.db
4. coord 在批次验收时检查上述 3 项
---
## §4 Mock 策略
## §4 /readyz 探针按阶段扩展规则§2.4 裁决)
### 4.1 我提供的 mock
依据 president-final-rulings §2.4 + G2 裁决:
在 student-bff 真实就绪前为下游student-portal提供以下 mock
### 4.1 探针列表(按阶段)
- **GraphQL mock**:使用 Apollo Server mockProviders 或 MSW 拦截 POST /graphql
- currentUser 返回固定学生id="student-001", name="李同学", roles=["student"]
- myClasses 返回固定 1 个班级
- studentDashboard 返回固定仪表盘avg_score=85.0, class_rank=5
- myGrades 返回固定 5 个成绩
- myHomework 返回固定 3 个作业1 个待提交)
- myNotifications 返回固定 10 条通知
student-bff 无 DB探针仅检查 Redis + 下游 gRPC 可达性:
### 4.2 我消费的 mock
| 阶段 | 探针列表 | 数量 |
| ---- | ----------------------------------------------------- | ---- |
| P3 | Redis PING + iam gRPC 50052 + core-edu gRPC 50053 | 3 |
| P4 | + content gRPC 50054 + data-ana gRPC 50055 | 5 |
| P5 | + ai gRPC 50058 + msg gRPC 50056 | 7 |
**实现方式**`DownstreamHealthCheck` 注册表模式,每个下游注册独立探针,按阶段启用(通过 ENV 过滤):
```typescript
const checks: HealthCheck[] = [
checkRedis(),
checkGrpc('iam', 50052),
checkGrpc('core-edu', 50053),
];
if (env.CONTENT_GRPC_ENABLED) checks.push(checkGrpc('content', 50054));
if (env.DATA_ANA_GRPC_ENABLED) checks.push(checkGrpc('data-ana', 50055));
if (env.AI_GRPC_ENABLED) checks.push(checkGrpc('ai', 50058));
if (env.MSG_GRPC_ENABLED) checks.push(checkGrpc('msg', 50056));
```
### 4.2 软失败规则
依据 §2.4.3
- **必需依赖**Redis + 已启用 gRPC 下游):失败返回 503触发 Pod 重启
- **可选依赖**(未启用 gRPC 下游 / Kafka 订阅):失败仅告警,返回 200 + body `degraded: true`
**student-bff 软失败场景**
| 依赖 | 类型 | 失败行为 |
| ------------------- | ------ | ------------------------------------------- |
| Redis | 必需 | 503缓存失效会影响性能但 BFF 仍可降级运行) |
| iam gRPC 50052 | 必需 | 503无 iam 无法做身份校验) |
| core-edu gRPC 50053 | 必需 | 503核心数据源 |
| content gRPC 50054 | 可选P4 启用前) | 200 + degraded=true |
| data-ana gRPC 50055 | 可选P4 启用前) | 200 + degraded=true |
| ai gRPC 50058 | 可选P5 启用前) | 200 + degraded=true |
| msg gRPC 50056 | 可选P5 启用前) | 200 + degraded=true |
> **P3 阶段**content / data-ana / ai / msg 均为可选P3 /readyz 仅检查 Redis + iam + core-edu3 项)。
---
## §5 就绪信号
### 5.1 我依赖的上游就绪标志
| 上游依赖 | 就绪标志 | 责任方 | 完成期限 |
| ---------------- | ------------------------------------------- | ------ | ------------- |
| iam gRPC 50052 | iam /readyz 返回 200 + GetUserInfo RPC 可调 | ai06 | 批次 1 完成 |
| core-edu gRPC 50053 | core-edu /readyz 返回 200 + ListExamsByClass RPC 可调 | ai08 | 批次 2 完成 |
| content gRPC 50054 | content /readyz 返回 200 + ListTextbooks RPC 可调 | ai09 | 批次 3 完成 |
| data-ana gRPC 50055 | data-ana /readyz 返回 200 + GetStudentDashboard RPC 可调 | ai11 | 批次 3 完成 |
| msg gRPC 50056 | msg /readyz 返回 200 + ListNotifications RPC 可调 | ai10 | 批次 4 完成 |
| GraphQL schema 仲裁 | coord 仲裁 student-bff schema 第一版 | coord | 批次 2 启动前 |
| DownstreamClient 抽象 | packages/shared-ts/src/bff/downstream-client.ts 就绪 | coord | 批次 1 启动前 |
| shared-go 包 | packages/shared-go/ 骨架就绪 | coord | 批次 0.14 |
### 5.2 我的就绪标志(供下游消费)
| 就绪标志 | 完成标准 | 完成阶段 |
| ----------------------------------------- | ----------------------------------------------------------- | -------- |
| student-bff GraphQL :3009 启用 | /healthz 返回 200 | P3 |
| /readyz 返回 200P3 探针 3 项) | Redis + iam gRPC + core-edu gRPC 全部通过 | P3 |
| GraphQL schema 可内省 | POST /graphql `{ query: "{ __schema { types { name } } }" }` 返回 schema | P3 |
| 核心 Query 可执行 | currentUser / myClasses / myExams / myHomework / myGrades 返回真实数据 | P3 |
| 核心 Mutation 可执行 | submitHomework 可执行 | P3 |
| AuthorizationGuard 接口就绪 | canAccessSelfData 接口已实现DEV_MODE 放行) | P3 |
| DownstreamClient 回写 teacher-bff 完成 | teacher-bff 已切换为 DownstreamClient 抽象 | P3 |
| /readyz 扩展 P4 探针5 项) | + content gRPC + data-ana gRPC | P4 |
| /readyz 扩展 P5 探针7 项) | + ai gRPC + msg gRPC | P5 |
---
## §6 Mock 策略
### 6.1 我提供的 mock供 student-portal 消费)
在 student-bff 真实就绪前,为 ai14student-portal提供以下 mock
**GraphQL mock 实现方式**GraphQL Yoga 内置 mock 模式(`graphql-yoga``mocking` 配置)或 MSW 拦截 POST /graphql
| Query/Mutation | mock 返回 |
| --------------------------- | ------------------------------------------------------------ |
| `currentUser` | 固定学生id="student-001", name="李同学", roles=["student"] |
| `myClasses` | 固定 1 个班级id="cls-001", name="高三1班" |
| `myExams` | 固定 2 个考试 |
| `myHomework` | 固定 3 个作业1 个待提交) |
| `myGrades` | 固定 5 个成绩avg_score=85.0 |
| `myAttendance` | 固定 10 条考勤 |
| `studentDashboard` | 固定仪表盘avg_score=85.0, class_rank=5 |
| `myNotifications` | 固定 10 条通知3 条未读) |
| `myNotificationUnreadCount` | 固定 count=3 |
| `submitHomework` | 返回 success=true |
| `markNotificationAsRead` | 返回 success=true |
> **P4 字段降级**P3 阶段 `studentDashboard.weakness` / `myTrend` / `textbooks` / `chapters` / `learningPath` 返回 null + `extensions.warning = "field_unavailable_in_p3"`
### 6.2 我消费的 mock上游未就绪时
在真实上游就绪前student-bff 使用以下 mock详见 §2.1 mock 策略列):
- **iam mock**:固定 UserInfo + 学生权限 + 固定视口
- **core-edu mock**:固定班级/同学/考试/作业/成绩/考勤
- **content mock**:固定教材/章节/学习路径
- **data-ana mock**:固定仪表盘/薄弱点/趋势
- **msg mock**:固定通知列表 + MarkAsRead success
| 上游 | mock 实现 |
| -------- | ------------------------------------------------------------ |
| iam | 固定 UserInfo + 学生权限集 + 固定视口 + DataScope=SELF |
| core-edu | 固定班级/同学/考试/作业/成绩/考勤 |
| content | 固定教材/章节/学习路径P4 启用前) |
| data-ana | 固定仪表盘/薄弱点/趋势P4 启用前) |
| msg | 固定通知列表 + GetUnreadCount + MarkAsRead successP5 启用前) |
> 所有上游 mock 通过 gRPC client 拦截器实现,上游就绪后移除拦截器切换真实调用
> **mock 实现方式**:通过 DownstreamClient 的 gRPC client 拦截器实现,上游就绪后移除拦截器切换真实调用。具体:在 `DownstreamClient.call()` 内部检查 `env.MOCK_UPSTREAM=true`,若为 true 则返回 mock 数据,否则走真实 gRPC
---
## §7 缓存策略B6 裁决)
依据 B6 裁决 + 004 §6.2 BFF 混合读策略:
### 7.1 Redis 短缓存
| 缓存对象 | key 模式 | TTL | 失效策略 |
| ----------------------- | ----------------------------------- | ----- | ----------------------- |
| currentUser 聚合结果 | `bff:student:user:{userId}` | 30s | TTL 过期 |
| myClasses 聚合结果 | `bff:student:classes:{userId}` | 30s | TTL 过期 |
| studentDashboard 聚合结果 | `bff:student:dashboard:{userId}` | 5s | TTL 过期(实时性要求高)|
| 权限列表 | `authz:student:{userId}` | 5min | P5 订阅 Kafka 事件失效 |
| 视口配置 | `bff:student:viewports:{userId}` | 5min | TTL 过期 |
### 7.2 缓存规则
1. **仅缓存 Query**Mutation 不缓存
2. **缓存粒度**:按 Query + userId 维度缓存(学生数据隔离 SELF
3. **降级策略**Redis 不可用时,直接走 gRPC 调用(不缓存),返回数据 + `degraded: true`(标记缓存降级)
4. **缓存击穿防护**:使用 `ioredis``GETSET` 或单飞模式(同一 key 并发请求只发一个 gRPC 调用)
---
## §8 变更记录
| 日期 | 版本 | 变更 | 负责人 |
| ---------- | ---- | -------------------------------------------------------------------- | ------ |
| 2026-07-09 | v1 | 初始创建 | ai04 |
| 2026-07-10 | v2 | 对齐 coord B1-B8 + president §2.2-2.9 裁决schema 路径/错误响应/越权防御/DownstreamClient/跨阶段扩展/readyz 探针/降级模式/缓存策略) | ai04 |

View File

@@ -1,7 +1,9 @@
# student-portal 对接契约
> 负责人ai14
> 关联:[matrix.md](./matrix.md)
> 关联:[matrix.md](./matrix.md)、[coord.md §1 ARB-001](../coord.md)、[coord.md §2 ARB-002](../coord.md)、[student-bff_contract.md](./student-bff_contract.md)、[teacher-portal_contract.md](./teacher-portal_contract.md)
> 版本v2ai14 接管审计与补全版2026-07-10
> 修订摘要GraphQL endpoint 路径修正为 `/api/v1/student/graphql`(对齐 matrix.md §5+ 补全 24 个 GraphQL query/mutation对齐 02 §4.2+ 补充 ARB-002 MF Shell 暴露清单 + 补充就绪信号明细
---
@@ -18,32 +20,54 @@
| GET | / | 学生门户首页 | JWT 必需(前端路由守卫) |
| GET | /my-classes | 我的班级 | JWT 必需 |
| GET | /my-exams | 我的考试 | JWT 必需 |
| GET | /my-exams/[id]/take | 考试作答页 | JWT 必需 |
| GET | /my-exams/[id]/result | 考试结果页 | JWT 必需 |
| GET | /my-homework | 我的作业 | JWT 必需 |
| GET | /my-homework/[id]/submit | 作业提交页 | JWT 必需 |
| GET | /my-grades | 我的成绩 | JWT 必需 |
| GET | /my-attendance | 我的考勤 | JWT 必需 |
| GET | /learning-path | 学习路径 | JWT 必需 |
| GET | /dashboard | 学生仪表盘 | JWT 必需 |
| GET | /dashboard/weakness | 学情诊断 | JWT 必需 |
| GET | /dashboard/trend | 学习趋势 | JWT 必需 |
| GET | /textbooks | 教材列表 | JWT 必需 |
| GET | /textbooks/[id]/chapters | 章节列表 | JWT 必需 |
| GET | /notifications | 通知中心 | JWT 必需 |
| GET | /ai-tutor | AI 辅助答疑P5 可选) | JWT 必需 |
> **路由前缀**:无 `/student/` 前缀student-portal 作为 MF Remote由 Shell 路由 `/student/*` 加载,内部路由无前缀)。
### 1.3 GraphQL schema如 BFF
不适用。student-portal 消费 student-bff GraphQL自身不提供 schema。
> **消费的 schema 文件**`packages/shared-ts/contracts/graphql/student-bff.graphql`(待 ISSUE-014-02 仲裁后由 ai04 创建,对齐 ARB-001 §1.3 集中管理原则)
### 1.4 Kafka 事件发布(如有)
无。
### 1.5 错误码前缀
无(前端不定义错误码前缀,透传 BFF 错误码)。
无(前端不定义错误码前缀,透传 BFF 错误码 `BFF_STUDENT_*`,见 [matrix.md §6](../matrix.md))。
### 1.6 微前端架构(补充
### 1.6 微前端架构(MF RemoteARB-002 对齐
| 角色 | 说明 |
| ---------------------- | ----------------------------------------------------------------- |
| MF Remote | 学生门户是微前端远程模块,由 teacher-portal AppShell 或独立壳加载 |
| 暴露的 remote 模块 | StudentApp学生端完整应用、shared 学生端组件 |
| module federation 配置 | `apps/student-portal/module-federation.config.ts` |
| 角色 | 说明 |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| MF Remote | student-portal 是微前端远程模块P3 首个 RemoteARB-002 §2.3,由 teacher-portal AppShell 加载 |
| 暴露的 remote 模块 | `./StudentApp`(学生端完整应用) |
| module federation 配置 | `apps/student-portal/next.config.js`NextFederationPlugin |
| Shell 暴露清单(复用) | AppShell / GraphQLProvider / useAuth / usePermission / useGraphQLClient / ErrorBoundary / Loading / Empty / RequirePermissionARB-002 §2.2 |
| shared singleton | react / react-dom / urql / graphql / @tanstack/react-query / zustand / nuqs / @edu/ui-tokens / @edu/ui-components / @edu/hooks / @edu/contracts / @edu/shared-tsARB-002 §2.2 |
| feature flag | `NEXT_PUBLIC_MF_ENABLED`P2=false 独立壳P3=true 接入 Shell |
| 登录页 | 不实现,未登录跳转 `http://localhost:4000/login?redirect=student`ARB-002 §2.3 登录由 Shell 独占) |
### 1.7 WebSocket 消费push-gateway
| 端点 | 用途 | 认证 | 事件类型 |
| --------------------- | ----------------------- | ---- | ------------------------------------------------------------------------ |
| `ws://push-gateway:8081/ws` | 实时通知推送 | JWT | 作业通知 / 考试通知 / 成绩通知 / 系统通知 / 考试延长(待 ISSUE-014-06 仲裁) / 考试强制提交(待 ISSUE-014-06 仲裁) |
---
@@ -59,27 +83,59 @@
### 2.3 HTTP 调用(如有)
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------- | ------------------------- | --------------------------------------------- | ----------------------------------------------------------------- |
| api-gateway (ai01) | POST /api/student/graphql | 学生 GraphQL 查询(经网关代理到 student-bff | api-gateway/student-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| api-gateway (ai01) | POST /api/auth/login | 学生登录 | api-gateway 就绪前使用 MSW 返回固定 JWT |
| push-gateway (ai02) | GET /ws | WebSocket 实时通知 | push-gateway 就绪前使用 mock-socket 模拟 WS 推送 |
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------- | ------------------------------ | --------------------------------------------- | ----------------------------------------------------------------- |
| api-gateway (ai01) | POST /api/v1/student/graphql | 学生 GraphQL 查询(经网关代理到 student-bff :3009/graphql | api-gateway/student-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| api-gateway (ai01) | POST /api/auth/login | 学生登录Shell 独占student-portal 不直接调用,仅跳转) | api-gateway 就绪前由 Shell 处理 |
| api-gateway (ai01) | POST /api/v1/student/upload | 作业附件上传(待 ISSUE-014-05 仲裁) | 待仲裁后实现 |
| push-gateway (ai02) | GET /wsWebSocket | WebSocket 实时通知 | push-gateway 就绪前使用 mock-socket 模拟 WS 推送 |
> **路径说明**ISSUE-014-01
> - `POST /api/v1/student/graphql` 经 api-gateway 反向代理到 student-bff `POST /graphql`:3009
> - 路径前缀 `/api/v1/student/*` 与 matrix.md §5、teacher-portal `/api/v1/teacher/*` 保持命名一致性
> - 由 ai01api-gateway确认路由配置`/api/v1/student/*` → `student-bff:3009/*`
### 2.4 GraphQL 查询域(经 api-gateway 代理到 student-bff
| Query/Mutation | 用途 | mock 策略 |
| ----------------------------------- | --------------- | -------------------------------------------------- |
| currentUser | 当前学生信息 | MSW 返回固定学生 |
| myClasses | 我的班级 | MSW 返回固定 1 个班级 |
| myExams | 我的考试 | MSW 返回固定 2 个考试 |
| myHomework / submitHomework | 我的作业 + 提交 | MSW 返回固定作业 + submitHomework success |
| myGrades | 我的成绩 | MSW 返回固定 5 个成绩 |
| myAttendance | 我的考勤 | MSW 返回固定 10 条考勤 |
| textbooks / chapters / learningPath | 学习内容 | MSW 返回固定内容 + 学习路径 |
| studentDashboard | 学生仪表盘 | MSW 返回固定仪表盘avg_score=85.0, class_rank=5 |
| myWeakness | 我的薄弱点 | MSW 返回固定 3 个 weak_points |
| myTrend | 学习趋势 | MSW 返回固定趋势数据 |
| myNotifications / markAsRead | 通知中心 | MSW 返回固定通知 |
> 对齐 [02-architecture-design.md v2 §4.2](../../../apps/student-portal/docs/02-architecture-design.md) GraphQL 操作清单(共 24 个)
#### 2.4.1 Query16 个)
| Query | 用途 | mock 策略 |
| ------------------------------ | --------------------- | -------------------------------------------------- |
| currentUser | 当前学生信息 | MSW 返回固定学生id=student-001, roles=[student]|
| myClasses | 我的班级 | MSW 返回固定 1 个班级 |
| myExams | 我的考试列表 | MSW 返回固定 2 个考试 |
| examDetail(id: ID!) | 考试详情(含题目) | MSW 返回固定考试 + 5 道题 |
| myHomework | 我的作业列表 | MSW 返回固定 3 个作业1 个待提交) |
| homeworkDetail(id: ID!) | 作业详情 | MSW 返回固定作业 + 题目 |
| myGrades | 我的成绩 | MSW 返回固定 5 个成绩 |
| myAttendance | 我的考勤 | MSW 返回固定 10 条考勤 |
| textbooks | 教材列表 | MSW 返回固定 5 个教材 |
| chapters(textbookId: ID!) | 章节列表 | MSW 返回固定章节树 |
| learningPath | 学习路径 | MSW 返回固定 8 个知识点推荐顺序 |
| studentDashboard | 学生仪表盘 | MSW 返回固定仪表盘avg_score=85.0, class_rank=5 |
| myWeakness | 我的薄弱点 | MSW 返回固定 3 个 weak_points |
| myTrend | 学习趋势 | MSW 返回固定趋势数据 |
| myNotifications(first: Int, after: String) | 通知列表 | MSW 返回固定 10 条通知 |
| serverTime | 服务器时间(考试倒计时对齐) | MSW 返回当前时间 + 100ms 延迟 |
#### 2.4.2 Mutation8 个)
| Mutation | 用途 | mock 策略 |
| --------------------------------------- | ------------------- | -------------------------------------- |
| submitHomework(input: SubmitHomeworkInput!) | 提交作业 | MSW 返回 success=true |
| submitExam(input: SubmitExamInput!) | 提交考试作答 | MSW 返回 success=true + submittedAt |
| saveExamDraft(input: SaveExamDraftInput!) | 保存考试草稿 | MSW 返回 success=true |
| markAsRead(notificationId: ID!) | 标记通知已读 | MSW 返回 success=true |
| markAllAsRead | 全部标记已读 | MSW 返回 success=true |
| recordExamViolation(input: RecordExamViolationInput!) | 记录防作弊违规(待 ISSUE-014-03 仲裁) | MSW 返回 success=true |
| recordPasteEvent(input: RecordPasteEventInput!) | 记录粘贴事件(待 ISSUE-014-04 仲裁) | MSW 返回 success=true |
| updateNotificationPreference(input: UpdateNotificationPreferenceInput!) | 更新通知偏好P5 | MSW 返回 success=true |
> **DataScope L0 强制执行**ISSUE-014-07
> - 所有学生端 Query 不传 `studentId` 参数,由 student-bff 在 Resolver 层从 JWT `x-user-id` 提取并强制过滤
> - 前端无法绕过 L0 边界(前端篡改 JWT 无效gRPC 层会重新校验)
---
@@ -87,18 +143,31 @@
### 3.1 我依赖的上游就绪标志
- [ ] api-gateway HTTP :8080 启用ai01—— 前端请求入口
- [ ] student-bff GraphQL :3009 启用ai04—— 数据来源
- [ ] push-gateway WebSocket :8081/ws 启用ai02—— 实时通知
| 上游 | 就绪标志 | 阻塞阶段 | 状态 |
| --------------------------------- | ------------------------------------------------------------------------- | -------- | ------------ |
| packages 骨架ai13 批次 0.15 | ui-tokens / ui-components / hooks 可 import | P2 启动 | ✅ 已就绪 |
| teacher-portal MF Shellai13 P2| exposes AppShell/GraphQLProvider/useGraphQLClient/useAuth/usePermission + shared singleton | P2 启动 | ⏳ 待 ai13 P2 |
| api-gateway HTTP :8080ai01 P3 | `/api/v1/student/*` 反向代理 student-bff 可用 | P3 启动 | ⏳ 待 ai01 P3 |
| student-bff GraphQLai04 P3 | `POST /graphql` :3009 + 核心 Query/Mutation 可执行 | P3 启动 | ⏳ 待 ai04 P3 |
| student-bff GraphQL schema | `packages/shared-ts/contracts/graphql/student-bff.graphql` 创建(待 ISSUE-014-02 仲裁) | P3 启动 | ⏳ 待仲裁 |
| core-edu gRPC 50053ai08 P3 | ExamService/HomeworkService/GradeService/AttendanceService/ClassService | P3 启动 | ⏳ 待 ai08 P3 |
| iam gRPC 50052ai06 P2 | GetUserInfo + GetEffectivePermissions + GetViewports | P3 启动 | ⏳ 待 ai06 P2 |
| content gRPC 50054ai09 P4 | TextbookService + ChapterService + KnowledgeGraphService | P4 启动 | ⏳ 待 ai09 P4 |
| data-ana gRPC 50055ai11 P4 | AnalyticsService.GetStudentWeakness + GetLearningTrend | P4 启动 | ⏳ 待 ai11 P4 |
| push-gateway WebSocket :8081/wsai02 P5 | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |
| msg gRPC 50056ai10 P5 | NotificationService.ListNotifications + MarkAsRead | P5 启动 | ⏳ 待 ai10 P5 |
| ai 服务 gRPC 50057ai12 P5可选 | AiService.ChatSSE 流式) | P5 启动 | ⏳ 待 ai12 P5 |
### 3.2 我的就绪标志(供下游消费)
- [ ] student-portal dev server :4001 启用
- [ ] MF Remote 可被 AppShell 加载(暴露 StudentApp 模块)
- [ ] 独立壳渲染(首页 + 导航 + 路由守卫)
- [ ] 登录流程可用(POST /api/auth/login 获取 JWT 存入 cookie
- [ ] GraphQL 查询可执行currentUser / myClasses / studentDashboard 返回数据)
- [ ] WebSocket 通知可接收
- [ ] student-portal dev server :4001 启用`pnpm dev` 可访问)
- [ ] MF Remote 可被 AppShell 加载(暴露 `./StudentApp` 模块teacher-portal Shell 可加载
- [ ] 独立壳渲染(`NEXT_PUBLIC_MF_ENABLED=false`首页 + 导航 + 路由守卫独立可用
- [ ] 登录流程可用(未登录跳转 Shell `/login`,登录后回跳 student
- [ ] GraphQL 查询可执行currentUser / studentDashboard / myClasses 返回数据)
- [ ] 考试作答链路通(进入作答 → 自动保存 → 提交 → 跳转结果页)
- [ ] WebSocket 通知可接收(通知中心实时更新)
- [ ] lint + typecheck 零错误
---
@@ -108,18 +177,78 @@
student-portal 是前端,无下游消费方。但对开发体验提供:
- **Storybook**:各组件独立 story
- **Storybook**:各组件独立 story`apps/student-portal/.storybook/`
- **MSW handlers**`apps/student-portal/src/mocks/handlers.ts`,拦截所有 GraphQL/HTTP 请求
- **Mock fixtures**`apps/student-portal/src/mocks/fixtures/*.json`,与 student-bff mock 数据一致
### 4.2 我消费的 mock
在真实上游就绪前student-portal 使用以下 mock
- **HTTP/GraphQL mock**:使用 MSW 拦截所有请求
- POST /api/auth/login → 返回固定 JWT + UserInfostudent 角色)
- POST /api/student/graphql → 根据 operationName 返回对应 mock 响应(与 student-bff mock 数据一致
- 所有 mock 响应定义在 `apps/student-portal/src/mocks/fixtures/*.json`
- **WebSocket mock**:使用 mock-socket 库
- 连接后每 30 秒推送 1 条 mock 通知
- **JWT mock**:使用固定 mock JWT存入 httpOnly cookie
- **环境切换**:通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制,上游就绪后设为 `disabled`
#### 4.2.1 HTTP / GraphQL mockMSW
- `POST /api/v1/student/graphql` operationName 返回对应 mock 响应(见 §2.4
- `POST /api/auth/login` → 返回固定 JWT + UserInfostudent 角色)由 Shell 处理
- `POST /api/v1/student/upload` → 返回固定 signed URL待 ISSUE-014-05 仲裁后实现)
- 所有 mock 响应定义在 `apps/student-portal/src/mocks/fixtures/*.json`
#### 4.2.2 WebSocket mockmock-socket
- 连接 `ws://localhost:8081/ws` 后每 30 秒推送 1 条 mock 通知
- 通知类型轮询:作业通知 / 考试通知 / 成绩通知 / 系统通知
- 支持模拟考试延长事件(待 ISSUE-014-06 仲裁后实现)
#### 4.2.3 JWT mock
- 使用固定 mock JWT`eyJhbGciOiJSUzI1NiIs...`payload 含 `sub=student-001, roles=[student], dataScope=SELF`
- 存入 httpOnly cookie由 Shell 登录流程设置)
#### 4.2.4 环境切换
- 通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制
- 上游就绪后设为 `disabled`,切换到真实请求
- MF 切换:`NEXT_PUBLIC_MF_ENABLED=false`P2 独立壳)→ `true`P3 接入 Shell
#### 4.2.5 IDB 草稿恢复(真实 idb-keyval
- 考试作答草稿使用真实 `idb-keyval` 存储(前端可独立测试断网恢复逻辑)
- 不需要 mockIDB 在浏览器原生支持
---
## §5 与 student-bff 契约对齐核查
> 参考 [student-bff_contract.md](./student-bff_contract.md)ai04 维护)
| 对齐项 | student-portal 期望 | student-bff 提供 | 状态 |
| ----------------------- | ---------------------------------------------------- | ----------------------------------------------------------------- | ---- |
| GraphQL endpoint | `POST /api/v1/student/graphql`(经 api-gateway 代理)| `POST /graphql` :3009 | ✅ 对齐api-gateway 代理) |
| GraphQL schema 文件 | `packages/shared-ts/contracts/graphql/student-bff.graphql` | `apps/student-bff/src/schema/*.graphql`(待 ISSUE-014-02 仲裁) | ⏳ 待仲裁 |
| Query 域16 个) | 见 §2.4.1 | 见 student-bff §1.3auth/myClasses/myExams/myHomework/myGrades/myAttendance/content/dashboard/weakness/trend/notifications | ⏳ 待 ai04 确认 serverTime/examDetail/homeworkDetail |
| Mutation 域8 个) | 见 §2.4.2 | student-bff §1.3 仅列 submitHomework + markAsRead | ⏳ 待 ai04 补全 submitExam/saveExamDraft/recordExamViolation/recordPasteEvent/updateNotificationPreference |
| 错误码前缀 | 透传 `BFF_STUDENT_*` | `BFF_STUDENT_`student-bff §1.5 | ✅ 对齐 |
| ActionState 信封 | success/errors/data + extensions.degraded | 待 ai04 实现ARB-001 §1.3 原则) | ⏳ 待 ai04 |
| DataLoader 防 N+1 | 依赖 student-bff 实现 | 待 ai04 实现ARB-001 §1.3 原则) | ⏳ 待 ai04 |
| DataScope L0 强制执行 | student-bff Resolver 层从 JWT 提取 studentId | 待 ISSUE-014-07 仲裁 | ⏳ 待仲裁 |
---
## §6 异议引用
> 详见 [objections/student-portal_issue.md](../objections/student-portal_issue.md)
| 编号 | 标题 | 影响 |
| ------------ | -------------------------------------------------------- | --------------------------------------------- |
| ISSUE-014-01 | GraphQL endpoint 路径不一致 | 影响 §2.3 路径配置 |
| ISSUE-014-02 | student-bff GraphQL schema 存放位置不一致 | 影响 §1.3 schema 文件路径 |
| ISSUE-014-03 | 考试作答页全屏策略与防作弊检测边界 | 影响 §2.4.2 recordExamViolation mutation 实现 |
| ISSUE-014-04 | 主观题粘贴策略(防作弊 vs 学生体验) | 影响 §2.4.2 recordPasteEvent mutation 实现 |
| ISSUE-014-05 | 作业附件上传协议GraphQL mutation vs REST multipart | 影响 §2.3 `/api/v1/student/upload` 端点 |
| ISSUE-014-06 | 考试延长/题目重排等实时事件命名未确认 | 影响 §1.7 WebSocket 事件类型 |
| ISSUE-014-07 | 学生端 DataScope L0 边界的强制执行层 | 影响 §2.4 GraphQL 查询域参数设计 |
---
**AI Agent**: ai14student-portal
**Branch**: feat-review-student-portal-docs-9yN6Av
**Coordinator**: coord-ai

View File

@@ -1,7 +1,8 @@
# teacher-bff 对接契约
> 负责人ai03
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[core_edu.proto](../../../packages/shared-proto/proto/core_edu.proto)、[content.proto](../../../packages/shared-proto/proto/content.proto)、[analytics.proto](../../../packages/shared-proto/proto/analytics.proto)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[ai.proto](../../../packages/shared-proto/proto/ai.proto)
> 版本v22026-07-10对齐 president §2.7/§2.17/§5.1 + coord B5/B8 + port-allocation §5
> 关联:[matrix.md](../matrix.md)、[iam.proto](../../../../packages/shared-proto/proto/iam.proto)、[core_edu.proto](../../../../packages/shared-proto/proto/core_edu.proto)、[content.proto](../../../../packages/shared-proto/proto/content.proto)、[analytics.proto](../../../../packages/shared-proto/proto/analytics.proto)、[msg.proto](../../../../packages/shared-proto/proto/msg.proto)、[ai.proto](../../../../packages/shared-proto/proto/ai.proto)、[coord-final-decisions.md §2 B1-B8](../../coord-final-decisions.md)、[president-final-rulings.md §2.7/§2.17/§5.1](../../president-final-rulings.md)
---
@@ -9,42 +10,77 @@
### 1.1 gRPC 接口(如有)
无对外 gRPC。teacher-bff 是 GraphQL 聚合层。
无对外 gRPC。teacher-bff 是 GraphQL 聚合层B1 裁决P2 起直接 GraphQL
### 1.2 HTTP 端点(如有)
| Method | Path | 用途 | 认证 |
| ------ | -------- | ----------------------------------------- | ----------------------- |
| POST | /graphql | 教师 BFF GraphQL 端点 | JWT 必需 + teacher 角色 |
| GET | /graphql | GraphQL Playground开发环境 | 开发环境公开 |
| GET | /healthz | 健康检查liveness | 公开 |
| GET | /readyz | 就绪检查readiness含下游 gRPC 连通性 | 公开 |
| Method | Path | 用途 | 认证 | 裁决依据 |
| ------ | -------- | ----------------------------------------- | ----------------------- | -------- |
| POST | /graphql | 教师 BFF GraphQL 端点 | JWT 必需 + teacher 角色 | B1 |
| GET | /graphql | GraphQL Playground开发环境,生产关闭 | 开发环境公开 | ARB-001 |
| GET | /healthz | 健康检查liveness | 公开 | G3 |
| GET | /readyz | 就绪检查readiness按阶段扩展下游探针 | 公开 | G2 + §2.4 |
### 1.3 GraphQL schema如 BFF
GraphQL schema 文件路径:`apps/teacher-bff/src/schema/*.graphql`(端口 :3003
**SDL-first**president §2.17 裁决schema 文件路径:
```
packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql
```
> 文件命名遵循 president §2.17 统一规范 `<bff-name>.schema.graphql`,由 ai03 起草、coord 在批次 1 启动前仲裁第一版。
> 前端 AIai13/ai16通过 graphql-codegen 生成 TS 类型消费。
核心 Query / Mutation 域:
- **auth**currentUser聚合 iam.GetUserInfo + GetEffectivePermissions + GetViewports
- **classes**myClasses聚合 core-edu.ClassService.GetClassesByTeacher
- **students**classStudents聚合 core-edu.ClassService.ListStudentsByClass + iam.BatchGetUsers 补用户名)
- **exams**classExams / createExam / updateExam聚合 core-edu.ExamService
- **homework**classHomework / assignHomework聚合 core-edu.HomeworkService
- **grades**studentGrades / recordGrade聚合 core-edu.GradeService
- **attendance**classAttendance / recordAttendance聚合 core-edu.AttendanceService
- **content**textbooks / chapters / knowledgePoints / questions聚合 content 4 个 Service
- **dashboard**teacherDashboard聚合 data-ana.AnalyticsService.GetTeacherDashboard
- **notifications**myNotifications / markAsRead聚合 msg.NotificationService
- **ai**aiChat / generateQuestion / generateLessonPlan聚合 ai.AiService
| 域 | Query / Mutation | 聚合下游 | 阶段 |
| -------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------ |
| **auth** | currentUser | iam.GetUserInfo + GetEffectivePermissions + GetViewports | P2 |
| **dashboard** | teacherDashboard | P2: iam gRPC用户基础信息下游字段返 null + extensions.warning<br>P3+: data-ana.GetTeacherDashboard | P2 null → P4 真实 |
| **classes** | myClasses | P2: iam gRPC按 president §3.5P2 班级列表来自 iam 数据)<br>P3+: core-edu.ClassService.GetClassesByTeacher | P2 iam → P3 core-edu |
| **students** | classStudents | core-edu.ClassService.ListStudentsByClass + iam.BatchGetUsers | P3+ |
| **exams** | classExams / createExam / updateExam / deleteExam | core-edu.ExamService | P3+ |
| **homework** | classHomework / assignHomework | core-edu.HomeworkService | P3+ |
| **grades** | studentGrades / recordGrade | core-edu.GradeService | P3+ |
| **attendance** | classAttendance / recordAttendance | core-edu.AttendanceServiceC4 裁决 P3 补全) | P3+ |
| **content** | textbooks / chapters / knowledgePoints / questions | content 4 个 Service | P4+ |
| **notifications** | myNotifications / markAsRead | msg.NotificationService | P5+ |
| **ai** | aiChat / generateQuestion / generateLessonPlan | ai.AiServiceA4 裁决 P5 补全 GenerateLessonPlan | P5+ |
| **admin.\*** | admin.schoolStats / admin.listClasses / admin.listTeachers 等 | admin 命名空间,复用 teacher-bff endpointpresident §5.1 | P2 预留 schema / P6 实现 |
**admin 命名空间预留**president §5.1 + §7.3 强制):
- **P2 预留**schema 文件中预留 `admin.*` Query/Mutation 命名空间占位(含类型定义但 Resolver 返 null
- **P6 实现**ai16 admin-portal 复用 teacher-bff GraphQL endpointai03 在 P6 实现 admin Resolver 真实数据
- **理由**admin 操作低 QPS无需独立 admin-bff 服务;减少 ai16 工作量
### 1.4 Kafka 事件发布(如有)
无。teacher-bff 不发布事件,仅做 gRPC 聚合。
无。teacher-bff 不发布事件,仅做 gRPC 聚合B7 裁决P2-P4 不订阅 KafkaP5 push-gateway 落地后再订阅)
### 1.5 错误码前缀
`BFF_TEACHER_`如 BFF_TEACHER_UPSTREAM_UNAVAILABLE、BFF_TEACHER_AGGREGATION_FAILED、BFF_TEACHER_FORBIDDEN
`BFF_TEACHER_`B5 + G14 裁决,统一 BFF_ 前缀)。
**BFF 越权防御 3 类错误码**president §2.7 裁决):
| 错误码 | HTTP | 场景 | i18n key |
| -------------------------------- | ---- | ---------------------------------------------------- | ------------------------------------- |
| `BFF_TEACHER_UNAUTHORIZED` | 401 | x-user-id 缺失或无效 | `error.bffTeacher.unauthorized` |
| `BFF_TEACHER_FORBIDDEN_RESOURCE` | 403 | teacherId 与资源无归属关系(场景 A | `error.bffTeacher.forbidden_resource` |
| `BFF_TEACHER_IDENTITY_MISMATCH` | 403 | JWT teacherId 与请求 body teacherId 不一致(场景 B | `error.bffTeacher.identity_mismatch` |
**其他 BFF_TEACHER_ 错误码**(聚合层):
| 错误码 | HTTP | 场景 |
| ------------------------------------- | ---- | -------------------------------------- |
| `BFF_TEACHER_UPSTREAM_UNAVAILABLE` | 502 | 下游 gRPC 不可达 |
| `BFF_TEACHER_AGGREGATION_FAILED` | 500 | 聚合多下游时部分失败且无降级数据 |
| `BFF_TEACHER_VALIDATION_FAILED` | 400 | 输入参数校验失败 |
| `BFF_TEACHER_INTERNAL_ERROR` | 500 | 兜底内部错误 |
**B3 裁决澄清**BFF 豁免 `@RequirePermission` 指不做"功能权限决策",但必须做"数据权限防御"B4 越权防御,见 §3.3)。
---
@@ -52,38 +88,115 @@ GraphQL schema 文件路径:`apps/teacher-bff/src/schema/*.graphql`(端口 :
### 2.1 gRPC 调用(同步)
| 被调用方 | Service.RPC | 用途 | mock 策略 |
| --------------- | ------------------------------------- | ---------------- | ----------------------------------------------- |
| iam (ai06) | IamService.GetUserInfo | 获取当前教师信息 | iam 就绪前返回固定 UserInfoteacher 角色) |
| iam (ai06) | IamService.BatchGetUsers | 批量补全学生姓名 | iam 就绪前返回固定用户名("学生001"~"学生030" |
| iam (ai06) | IamService.GetEffectivePermissions | 权限校验 | iam 就绪前返回全权限(放行) |
| iam (ai06) | IamService.GetViewports | 教师导航菜单 | iam 就绪前返回固定视口列表 |
| core-edu (ai08) | ClassService.GetClassesByTeacher | 教师班级列表 | core-edu 就绪前返回固定 3 个 ClassInfo |
| core-edu (ai08) | ClassService.ListStudentsByClass | 班级学生名单 | core-edu 就绪前返回固定 30 个 StudentInfo |
| core-edu (ai08) | ExamService.* | 考试管理 | core-edu 就绪前返回固定考试数据 |
| core-edu (ai08) | HomeworkService.* | 作业管理 | core-edu 就绪前返回固定作业数据 |
| core-edu (ai08) | GradeService.* | 成绩管理 | core-edu 就绪前返回固定成绩数据 |
| core-edu (ai08) | AttendanceService.* | 考勤管理 | core-edu 就绪前返回固定考勤数据 |
| content (ai09) | TextbookService.ListTextbooks | 教材列表 | content 就绪前返回固定 5 个教材 |
| content (ai09) | ChapterService.ListChapters | 章节列表 | content 就绪前返回固定章节树 |
| content (ai09) | KnowledgeGraphService.* | 知识图谱 | content 就绪前返回固定知识点 |
| content (ai09) | QuestionService.SearchQuestions | 题库检索 | content 就绪前返回固定 20 题 |
| data-ana (ai11) | AnalyticsService.GetTeacherDashboard | 教师仪表盘 | data-ana 就绪前返回固定仪表盘数据 |
| data-ana (ai11) | AnalyticsService.GetClassPerformance | 班级成绩分析 | data-ana 就绪前返回固定分析数据 |
| data-ana (ai11) | AnalyticsService.GetWarningList | 预警列表 | data-ana 就绪前返回固定 5 条预警 |
| msg (ai10) | NotificationService.ListNotifications | 教师通知列表 | msg 就绪前返回固定 10 条通知 |
| msg (ai10) | NotificationService.MarkAsRead | 标记已读 | msg 就绪前返回 success=true |
| ai (ai12) | AiService.Chat | AI 对话 | ai 就绪前返回固定回复 |
| ai (ai12) | AiService.GenerateQuestion | AI 出题 | ai 就绪前返回固定题目 |
| ai (ai12) | AiService.GenerateLessonPlan | AI 备课 | ai 就绪前返回固定教案 |
**B2 裁决**:首次实现即 gRPC 调用下游,禁止 REST fetch 过渡。
**B8 裁决**:通过 `DownstreamClient` 抽象统一封装BFF 模式 v2 标准抽象3 个 BFF 共用)。
> **RPC 现状标注说明**
> - ✅ = proto 中已定义(按 packages/shared-proto/proto/ 现状核对)
> - ❌ = proto 中未定义,待 coord 补全(已在裁决中明确补全时机)
#### 2.1.1 iam (ai06) — gRPC 50052
| Service.RPC | 用途 | 现状 | 阶段 | mock 策略 |
| --------------------------------- | ---------------- | ---- | ---- | ------------------------------------------- |
| IamService.GetUserInfo | 获取当前教师信息 | ✅ | P2 | 返回固定 UserInfoteacher 角色) |
| IamService.GetViewports | 教师导航菜单 | ❌ | P2 | 返回固定视口列表(待 coord 补 proto |
| IamService.GetEffectivePermissions | 权限校验 | ❌ | P2 | 返回全权限(放行) |
| IamService.BatchGetUsers | 批量补全学生姓名 | ❌ | P3+ | 返回固定用户名("学生001"~"学生030" |
| IamService.GetEffectiveDataScope | 数据范围校验 | ❌ | P3+ | 返回 OWN 范围 |
| IamService.GetChildrenByParent | parent-bff 用)| ❌ | P4 | teacher-bff 不调用 |
> iam.proto 现状仅 4 RPCRegister/Login/RefreshToken/GetUserInfomatrix.md §2 标称 12 RPC待 ai06 + coord 按 I1-I8 裁决补全。
#### 2.1.2 core-edu (ai08) — gRPC 50053
| Service.RPC | 用途 | 现状 | 阶段 | mock 策略 |
| ------------------------------------ | -------------- | ---- | ---- | ---------------------------------- |
| ExamService.CreateExam | 创建考试 | ✅ | P3 | 返回固定 examId |
| ExamService.GetExam | 获取考试详情 | ✅ | P3 | 返回固定考试数据 |
| ExamService.ListExamsByClass | 按班级列考试 | ✅ | P3 | 返回固定 5 场考试 |
| ExamService.UpdateExam | 更新考试 | ✅ | P3 | 返回 success=true |
| ExamService.DeleteExam | 删除考试 | ✅ | P3 | 返回 success=true |
| HomeworkService.AssignHomework | 布置作业 | ✅ | P3 | 返回固定 homeworkId |
| HomeworkService.GetHomework | 获取作业详情 | ✅ | P3 | 返回固定作业数据 |
| HomeworkService.ListHomeworkByClass | 按班级列作业 | ✅ | P3 | 返回固定 5 份作业 |
| HomeworkService.SubmitHomework | 提交作业 | ✅ | P3 | 返回 success=truestudent-bff 用)|
| GradeService.RecordGrade | 录入成绩 | ✅ | P3 | 返回 success=true |
| GradeService.GetGrade | 获取成绩 | ✅ | P3 | 返回固定成绩数据 |
| GradeService.ListGradesByStudent | 按学生列成绩 | ✅ | P3 | 返回固定 10 条成绩 |
| GradeService.ListGradesByExam | 按考试列成绩 | ✅ | P3 | 返回固定 30 条成绩 |
| GradeService.ListGradesByHomework | 按作业列成绩 | ✅ | P3 | 返回固定 30 条成绩 |
| ClassService.GetClassesByTeacher | 教师班级列表 | ❌ | P3 | 返回固定 3 个 ClassInfo |
| ClassService.ListStudentsByClass | 班级学生名单 | ❌ | P3 | 返回固定 30 个 StudentInfo |
| ClassService.BatchGetClasses | 批量获取班级 | ❌ | P3 | 返回固定班级数据 |
| AttendanceService.RecordAttendance | 记录考勤 | ❌ | P3 | 返回 success=trueC4 裁决补全) |
| AttendanceService.GetClassAttendance | 查询考勤 | ❌ | P3 | 返回固定考勤数据C4 裁决补全) |
> core_edu.proto 现状 14 RPCExamService 5 + HomeworkService 4 + GradeService 5matrix.md §2 标称 22 RPC 5 Service待 coord 按 C4/C5 裁决补 AttendanceService + ClassServiceGetClassesByTeacher / ListStudentsByClass / BatchGetClasses
#### 2.1.3 content (ai09) — gRPC 50054
| Service.RPC | 用途 | 现状 | 阶段 | mock 策略 |
| ------------------------------------ | ------------ | ---- | ---- | ------------------------------- |
| TextbookService.ListTextbooks | 教材列表 | ✅ | P4 | 返回固定 5 个教材 |
| TextbookService.GetTextbook | 获取教材详情 | ✅ | P4 | 返回固定教材数据 |
| TextbookService.CreateTextbook | 创建教材 | ✅ | P4 | 返回固定 textbookId |
| KnowledgeGraphService.GetPrerequisites | 知识点前置 | ✅ | P4 | 返回固定知识点依赖 |
| KnowledgeGraphService.GetLearningPath | 学习路径 | ✅ | P4 | 返回固定学习路径 |
| ChapterService.ListChapters | 章节列表 | ❌ | P4 | 返回固定章节树N5 裁决补全) |
| ChapterService.GetChapter | 章节详情 | ❌ | P4 | 返回固定章节N5 裁决补全) |
| QuestionService.SearchQuestions | 题库检索 | ❌ | P4 | 返回固定 20 题N3 裁决补全) |
> content.proto 现状 5 RPCTextbookService 3 + KnowledgeGraphService 2matrix.md §2 标称 18 RPC 4 Service待 coord 按 N3/N5 裁决补 ChapterService + QuestionService。
#### 2.1.4 data-ana (ai11) — gRPC 50055
| Service.RPC | 用途 | 现状 | 阶段 | mock 策略 |
| ------------------------------------ | -------------- | ---- | ---- | ---------------------------------- |
| AnalyticsService.GetClassPerformance | 班级成绩分析 | ✅ | P4 | 返回固定分析数据 |
| AnalyticsService.GetStudentWeakness | 学生薄弱点 | ✅ | P4 | 返回固定 3 个薄弱知识点 |
| AnalyticsService.GetLearningTrend | 学习趋势 | ✅ | P4 | 返回固定 12 个月趋势 |
| AnalyticsService.GetTeacherDashboard | 教师仪表盘 | ❌ | P4 | 返回固定仪表盘数据D4 裁决补全) |
| AnalyticsService.GetWarningList | 预警列表 | ❌ | P4 | 返回固定 5 条预警D4 裁决补全) |
| AnalyticsService.GetMasteryDistribution | 知识掌握分布 | ❌ | P4 | 返回固定分布数据D4 裁决补全) |
| AnalyticsService.SubscribeMasteryUpdate (stream) | 掌握度订阅 | ❌ | P5+ | 流式 mockD4 裁决补全) |
> analytics.proto 现状 3 RPCmatrix.md §2 标称 12 RPC待 coord 按 D4 裁决补全 4 端 Dashboard + Warning + MasteryDistribution + SubscribeMasteryUpdate Stream RPC。
#### 2.1.5 msg (ai10) — gRPC 50056
| Service.RPC | 用途 | 现状 | 阶段 | mock 策略 |
| ---------------------------------------- | ------------ | ---- | ---- | ---------------------------------- |
| NotificationService.SendNotification | 发送通知 | ✅ | P5 | 返回固定 notificationId |
| NotificationService.ListNotifications | 教师通知列表 | ✅ | P5 | 返回固定 10 条通知 |
| NotificationService.MarkAsRead | 标记已读 | ✅ | P5 | 返回 success=true |
| NotificationService.SearchNotifications | 搜索通知 | ✅ | P5 | 返回固定搜索结果 |
| NotificationPreferenceService.* | 通知偏好 | ❌ | P5 | 返回默认偏好(待 coord 补 proto |
| NotificationTemplateService.* | 通知模板 | ❌ | P5 | 返回固定模板(待 coord 补 proto |
> msg.proto 现状 4 RPCNotificationServicematrix.md §2 标称 13 RPC 3 Service待 coord 补 NotificationPreferenceService + NotificationTemplateService。
#### 2.1.6 ai (ai12) — gRPC 50058
| Service.RPC | 用途 | 现状 | 阶段 | mock 策略 |
| --------------------------------- | -------- | ---- | ---- | ------------------------------- |
| AiService.Chat | AI 对话 | ✅ | P5 | 返回固定回复 |
| AiService.StreamChat (stream) | 流式对话 | ✅ | P5 | 流式 mockSSE |
| AiService.GenerateQuestion | AI 出题 | ✅ | P5 | 返回固定题目 |
| AiService.OptimizeExpression | 表达优化 | ✅ | P5 | 返回固定优化结果 |
| AiService.GenerateLessonPlan | AI 备课 | ❌ | P5 | 返回固定教案A4 裁决补全) |
| AiService.StreamGenerateQuestion (stream) | 流式出题 | ❌ | P5 | 流式 mockA4 裁决补全) |
> ai.proto 现状 4 RPCmatrix.md §2 标称 6 RPC待 coord 按 A4 裁决补 GenerateLessonPlan + StreamGenerateQuestion。
> **端口说明**ai 服务端口为 **50058**port-allocation.md §5 最终值push-gateway 50057 豁免释放后 50058 让给 ai
### 2.2 Kafka 事件订阅(异步)
无。teacher-bff 不订阅 Kafka 事件,仅同步 gRPC 聚合。
无。teacher-bff 不订阅 Kafka 事件B7 裁决P2-P4 不订阅,仅同步聚合P5 push-gateway 落地后再订阅,届时评估订阅 edu.notification.* 用于实时通知推送)
### 2.3 HTTP 调用(如有)
无。
无。B2 裁决:首次实现即 gRPC禁止 REST fetch。
---
@@ -91,21 +204,53 @@ GraphQL schema 文件路径:`apps/teacher-bff/src/schema/*.graphql`(端口 :
### 3.1 我依赖的上游就绪标志
- [ ] iam gRPC 50052 启用ai06
- [ ] core-edu gRPC 50053 启用ai08
- [ ] content gRPC 50054 启用ai09
- [ ] data-ana gRPC 50055 启用ai11
- [ ] msg gRPC 50056 启用ai10
- [ ] ai gRPC 50057 启用ai12
| 上游 | AI | 就绪信号 | 阶段 |
| ---------- | ---- | ------------------------------------------------ | ---- |
| iam | ai06 | gRPC 50052 + 12 RPC + HealthService SERVING | P2 |
| core-edu | ai08 | gRPC 50053 + 22 RPC + HealthService SERVING | P3 |
| content | ai09 | gRPC 50054 + 18 RPC + HealthService SERVING | P4 |
| data-ana | ai11 | gRPC 50055 + 12 RPC + HealthService SERVING | P4 |
| msg | ai10 | gRPC 50056 + 13 RPC + HealthService SERVING | P5 |
| ai | ai12 | gRPC 50058 + 6 RPC + HealthService SERVING | P5 |
| coord | coord | shared-ts DownstreamClient 抽象包就绪B8 裁决)| P2 启动前 |
| coord | coord | teacher-bff.schema.graphql 第一版仲裁完成(含 admin namespace| P2 启动前 |
### 3.2 我的就绪标志(供下游消费)
- [ ] teacher-bff GraphQL :3003 启用(/healthz 返回 200
- [ ] /readyz 返回 200含 6 个下游 gRPC 连通性检查)
- [ ] GraphQL schema 可内省POST /graphql 返回 schema
- [ ] 核心 Query 可执行currentUser / myClasses / teacherDashboard
- [ ] 核心 Mutation 可执行createExam / assignHomework / recordGrade
- [ ] admin namespace 可用(供 admin-portal 消费)
| 信号 | 阶段 | 消费方 |
| --------------------------------------------- | ---- | ------------------- |
| teacher-bff GraphQL :3003 启用(/healthz 200| P2 | teacher-portal |
| /readyz 返回 200按阶段扩展探针见 §3.3 | P2+ | api-gateway |
| GraphQL schema 可内省POST /graphql | P2 | teacher-portal / admin-portal |
| 核心 Query 可执行currentUser / myClasses / teacherDashboard | P2 | teacher-portal |
| 核心 Mutation 可执行createExam / assignHomework / recordGrade | P3 | teacher-portal |
| admin namespace 预留schema 含 admin.* 类型P6 实现 Resolver| P2 预留 / P6 实现 | admin-portal |
| DownstreamClient 抽象落地3 BFF 共用B8 | P2 | student-bff / parent-bff |
### 3.3 /readyz 探针按阶段扩展president §2.4 + G2
**DownstreamHealthCheck 注册表模式**,每个下游注册独立探针,按阶段启用:
| 阶段 | 探针列表 | 项数 |
| ---- | --------------------------------------------------------------------- | ---- |
| P2 | Redis PING + iam gRPC 50052 | 2 |
| P3 | + core-edu gRPC 50053 | 3 |
| P4 | + content gRPC 50054 + data-ana gRPC 50055 | 5 |
| P5 | + ai gRPC 50058 + msg gRPC 50056 | 7 |
> 总裁裁决 §2.4:探针列表扩展属"跨阶段扩展例外"president §2.3),允许新增探针但禁止修改已有探针检查项。
> **软失败规则**必需依赖Redis / 已启用 gRPC 下游)失败返 503可选依赖Kafka 消费 / 未启用 gRPC 下游)失败仅告警返 200 + `degraded: true`。
### 3.4 越权防御B4 + president §2.9
**AuthorizationGuard** 实现 teacherId 与资源归属校验:
| 阶段 | 实现方式 | 裁决依据 |
| ---- | --------------------------------------------------------------------- | -------- |
| P2 | DEV_MODE 放行(环境变量 TEACHER_BFF_DEV_MODE=true+ 日志告警 | §2.9 |
| P3+ | 接入 core-edu gRPC 真实校验GetClassesByTeacher 比对 teacherId | §2.9 |
> B3 裁决澄清BFF 豁免 `@RequirePermission` 不做"功能权限决策",但必须做"数据权限防御"B4。错误码见 §1.5。
---
@@ -124,7 +269,7 @@ GraphQL schema 文件路径:`apps/teacher-bff/src/schema/*.graphql`(端口 :
### 4.2 我消费的 mock
在真实上游就绪前teacher-bff 使用以下 mock详见 §2.1 mock 策略列):
在真实上游就绪前teacher-bff 使用以下 mock详见 §2.1 各表的 mock 策略列):
- **iam mock**:固定 UserInfo + 全权限 + 固定视口
- **core-edu mock**:固定班级/学生/考试/作业/成绩/考勤数据
@@ -133,4 +278,4 @@ GraphQL schema 文件路径:`apps/teacher-bff/src/schema/*.graphql`(端口 :
- **msg mock**:固定通知列表 + MarkAsRead success
- **ai mock**:固定 AI 回复/题目/教案
> 所有上游 mock 通过 gRPC client 拦截器实现,上游就绪后移除拦截器切换真实调用。
> 所有上游 mock 通过 gRPC client 拦截器实现DownstreamClient 抽象内置 mock 开关B8 裁决),上游就绪后移除拦截器切换真实调用。

View File

@@ -60,11 +60,11 @@
### 2.3 HTTP 调用(如有)
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------- | ------------------------- | --------------------------------------------- | ----------------------------------------------------------------- |
| api-gateway (ai01) | POST /api/teacher/graphql | 教师 GraphQL 查询(经网关代理到 teacher-bff | api-gateway/teacher-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| api-gateway (ai01) | POST /api/auth/login | 教师登录 | api-gateway 就绪前使用 MSW 返回固定 JWT |
| push-gateway (ai02) | GET /ws | WebSocket 实时通知 | push-gateway 就绪前使用 mock-socket 模拟 WS 推送 |
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------- | ---------------------------- | --------------------------------------------- | ----------------------------------------------------------------- |
| api-gateway (ai01) | POST /api/v1/teacher/graphql | 教师 GraphQL 查询(经网关代理到 teacher-bff,对齐 matrix.md §5 | api-gateway/teacher-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| api-gateway (ai01) | POST /api/auth/login | 教师登录(非 GraphQL认证前提F12: JWT 存 localStorage | api-gateway 就绪前使用 MSW 返回固定 JWT |
| push-gateway (ai02) | GET /ws | WebSocket 实时通知 | push-gateway 就绪前使用 mock-socket 模拟 WS 推送 |
### 2.4 GraphQL 查询域(经 api-gateway 代理到 teacher-bff
@@ -97,8 +97,8 @@
- [ ] teacher-portal dev server :4000 启用
- [ ] MF Shell 可加载(首页渲染 AppShell + 导航)
- [ ] 子应用路由可访问(/classes /exams /homework 等子页面渲染)
- [ ] 登录流程可用POST /api/auth/login 获取 JWT 存入 cookie
- [ ] GraphQL 查询可执行(currentUser / myClasses 返回数据
- [ ] 登录流程可用POST /api/auth/login 获取 JWT 存入 localStorageF12P6 迁移 httpOnly cookie
- [ ] GraphQL 查询可执行(dashboard / viewports / me / classes / class QueryARB-001
- [ ] WebSocket 通知可接收push-gateway 推送 → 前端通知中心更新)
---
@@ -118,9 +118,9 @@ teacher-portal 是最前端,无下游消费方。但对开发体验提供:
- **HTTP/GraphQL mock**:使用 MSWMock Service Worker拦截所有请求
- POST /api/auth/login → 返回固定 JWT + UserInfo
- POST /api/teacher/graphql → 根据 operationName 返回对应 mock 响应(与 teacher-bff mock 数据一致)
- POST /api/v1/teacher/graphql → 根据 operationName 返回对应 mock 响应(与 teacher-bff mock 数据一致,对齐 §2.3
- 所有 mock 响应定义在 `apps/teacher-portal/src/mocks/fixtures/*.json`
- **WebSocket mock**:使用 mock-socket 库
- 连接 ws://localhost:8081/ws 后每 30 秒推送 1 条 mock 通知
- **JWT mock**:使用固定 mock JWT与 api-gateway mock 公钥配对),存入 httpOnly cookie
- **JWT mock**:使用固定 mock JWT与 api-gateway mock 公钥配对),存入 localStorageF12P6 迁移 httpOnly cookie
- **环境切换**:通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制是否启用 MSW上游就绪后设为 `disabled`