Merge worktree branch merge-15-modules-to-main-5ug5xJ
This commit is contained in:
@@ -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)
|
||||
> 说明:本契约基于 GraphQL(ARB-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 §5:admin 权限点 `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 配置风格) |
|
||||
| shared(singleton) | 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 返回固定 JWT(admin 角色) |
|
||||
| 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 不走 MF,Shell 独占)。管理员登录后按 admin 角色重定向到 `/admin/dashboard`。开发期 mock 登录由 Shell 的 MSW handler 提供(admin 角色 JWT + permissions=["*"])。
|
||||
|
||||
### 2.4 GraphQL 查询域(经 api-gateway 代理到 teacher-bff admin 命名空间)
|
||||
|
||||
> 依赖 teacher-bff admin 命名空间 schema(ARB-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 就绪(ai13,ARB-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 + UserInfo(admin 角色,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 JWT(admin 角色),存入 httpOnly cookie
|
||||
- 连接后每 30 秒推送 1 条 mock 系统通知(审计告警/异常登录)
|
||||
- **JWT mock**:使用固定 mock JWT(admin 角色,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 /ws(admin-portal 实时通知) | ai02 | ⚠️ 待 coord 仲裁(ISSUE-006) |
|
||||
| `packages/contracts` ADMIN_* 权限点常量 | coord | ⏳ |
|
||||
|
||||
@@ -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 | ⏳ 待补 proto(ISSUE-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 | ⏳ 待补 proto(ISSUE-03) |
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
> **RPC 总数**:P5 目标 6 RPC(ai12 建议,见 ISSUE-03)。备课工作流的"查询状态/确认入库"用 HTTP 端点实现,避免 RPC 膨胀;如 coord 裁定需 gRPC 则扩到 8 RPC(追加 GetLessonPlanStatus / ConfirmLessonPlan)。
|
||||
> **proto 现状**:ai.proto 仅 4 RPC(Chat/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 RPC:StreamChat / 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>` | 学校用量(管理员) | ⏳ 待实现 |
|
||||
|
||||
> **响应信封**:所有响应必须为 ActionState(004 §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 | AIUsageEvent(operation: 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/3002(HTTP 端口),应以 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 Provider(OpenAI/百川/本地) | POST /v1/chat/completions | 调用大模型生成内容 | 开发期使用本地 mock server 返回固定 ChatResponse,不消耗真实 token |
|
||||
| LLM Provider(OpenAI/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 + 字段扩展)—— coord(ISSUE-03)
|
||||
- [ ] events.proto 补 `AIUsageEvent` message —— coord(ISSUE-04)
|
||||
- [ ] ai 用量事件 topic 命名裁决 —— coord(ISSUE-02)
|
||||
- [ ] content gRPC 50054 启用(ai09)—— 知识点维度 + 题库检索 + 入库
|
||||
- [ ] data-ana gRPC 50055 启用(ai11,可选)—— 学生薄弱点(可降级独立运行)
|
||||
- [ ] iam `GetEffectiveDataScope` RPC P4 补全(ai06 + coord,ISSUE-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 返回固定 ChatResponse(content="这是 AI 助手的模拟回复")
|
||||
- AiService.StreamChat 返回固定流(3 个 ChatChunk,最后一个 done=true)
|
||||
- AiService.GenerateQuestion 返回固定 GeneratedQuestion(question/answer/explanation)
|
||||
- AiService.GenerateLessonPlan 返回固定 LessonPlan(3 个 LessonSection)
|
||||
- AiService.StreamGenerateQuestion 返回固定流(2 个 GeneratedQuestion)
|
||||
- AiService.GenerateLessonPlan 返回固定 LessonPlanResponse(workflow_id + 3 个题目)
|
||||
- AiService.StreamGenerateQuestion 返回固定流(2 个题目逐字 chunk)
|
||||
- AiService.OptimizeExpression 返回固定 OptimizedExpression
|
||||
- **Kafka mock**:ai 就绪前不发布真实 AIUsageEvent,data-ana 仪表盘 AI 用量显示"暂无数据"
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
@@ -105,4 +182,22 @@
|
||||
- **LLM Provider mock**:本地启动 mock server,POST /v1/chat/completions 返回固定 JSON(不消耗真实 token,不产生费用)
|
||||
- **content 知识点**:内置固定知识点表(数学 20 个知识点 + 前置依赖关系),不依赖 content gRPC
|
||||
- **data-ana 薄弱点**:内置固定学生薄弱点(2 个 weak_points),不依赖 data-ana gRPC
|
||||
- **事件订阅**:不订阅 content 事件,知识点维度表静态
|
||||
- **iam DataScope**:内置固定 DataScope(SCHOOL 级),不依赖 iam GetEffectiveDataScope
|
||||
- **事件订阅**:P5 不订阅任何事件,无 mock 需要
|
||||
|
||||
---
|
||||
|
||||
## §5 契约待裁决项汇总
|
||||
|
||||
> 以下字段待 coord 裁决后最终定稿,ai12 当前按建议方案先行实现。
|
||||
|
||||
| 待裁决项 | ISSUE | ai12 建议方案 | 影响章节 |
|
||||
| --------------------------------- | ------ | ---------------------------------------------- | -------------- |
|
||||
| ai gRPC 端口(50057 vs 50058) | ISSUE-01 | 50058(port-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 |
|
||||
| 备课工作流 Temporal(P6 决策点) | ISSUE-06 | P5 用 BackgroundTasks + Redis,P6 评估 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 |
|
||||
|
||||
@@ -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
|
||||
> 版本:v2(2026-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 | Path(Gateway 外部路径) | 目标服务 | 端口 | 鉴权 | 公开子路径 | 阶段 |
|
||||
| ------ | --------------------------------------------------- | ------------------------- | ----- | ---- | --------------------------------------- | ---- |
|
||||
| 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-bff(GraphQL) | 3003 | JWT | — | P2 |
|
||||
| ANY | /api/v1/student/v1/*path + /api/v1/student/v1 | student-bff(GraphQL) | 3009 | JWT | — | P3 |
|
||||
| ANY | /api/v1/parent/v1/*path + /api/v1/parent/v1 | parent-bff(GraphQL) | 3010 | JWT | — | P4 |
|
||||
| ANY | /api/v1/classes/v1/*path + /api/v1/classes/v1 | core-edu(classes 合并) | 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 验签,不解析 GraphQL(GraphQL 由 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 公钥集(JWKS),TTL 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 Fetcher(HTTP 拉 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/push(WebSocket 协作评估)| 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**:使用 MSW(Mock Service Worker)或本地 nginx 拦截
|
||||
- /api/auth/login 返回固定 JWT(mock 签发)+ 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` 返回固定 JWT(mock 签发)+ UserInfo
|
||||
- `/api/v1/teacher/v1/*` `/api/v1/student/v1/*` `/api/v1/parent/v1/*` 直接返回各 BFF 的 mock GraphQL 响应
|
||||
- `/healthz` `/readyz` 返回 200
|
||||
- **JWT mock**:前端开发期使用固定 mock JWT(api-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 JWT(DevMode 下生效)
|
||||
- **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 |
|
||||
|
||||
@@ -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 对外契约统一为 gRPC(REST 端点仅自身管理面用,不作为下游消费契约)
|
||||
|
||||
---
|
||||
|
||||
## §0 契约状态总览
|
||||
|
||||
| 维度 | 当前状态 | 目标状态(P4 完成) |
|
||||
| ---------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
| gRPC | ❌ 未实现([content.proto](../../../packages/shared-proto/proto/content.proto) 仅 5 RPC 定义) | ✅ 21 RPC(4 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 总数**:21(TextbookService 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 = +2;TextbookService 补 Update + Delete = +2;QuestionService 原 contract 已含 Publish/Search,design 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 | KnowledgePointEvent(action: created/updated/prerequisite_added/prerequisite_removed) | data-ana / ai / Neo4j Sync Worker / ES Sync Worker |
|
||||
| edu.content.question.events | QuestionEvent(action: created/updated/published/deleted) | data-ana |
|
||||
message Chapter {
|
||||
string id = 1;
|
||||
string textbook_id = 2;
|
||||
string title = 3;
|
||||
int32 order = 4; // DB 列名 order_num,proto 字段名 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 两类 topic,Textbook/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-005):content 是 core-edu 的上游(core-edu 调 content 查知识点),不是下游。content 不消费 core-edu 事件。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
| Topic | Event | 发布方 | mock 策略 |
|
||||
| ---------------- | --------------------------------- | --------------- | ----------------------------------------------------- |
|
||||
| edu.class.events | ClassEvent(action: 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 mock(grpc-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 就绪前不发布真实事件,下游使用本地 stub(data-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 个 Question(source=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 topic(kp + 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 RPC(TextbookService 3 + KnowledgeGraphService 2),无 Chapter/Question | 21 RPC(4 Service 完整) |
|
||||
| events.proto | 4 message(ClassEvent/ExamEvent/HomeworkEvent/GradeEvent) | 8 message(+ TextbookEvent/ChapterEvent/KnowledgePointEvent/QuestionEvent) |
|
||||
|
||||
---
|
||||
|
||||
## §6 变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 |
|
||||
| ---------- | ---- | ---------------------------------------------------------------------------------------------- | ------ |
|
||||
| 2026-07-09 | v1.0 | 初版(18 RPC,2 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 |
|
||||
|
||||
@@ -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 RPC(ExamService 5 + HomeworkService 4 + GradeService 5)
|
||||
- P3 目标态:5 Service / 27 RPC(+ClassService 4 + AttendanceService 4 + P3 新增 5 RPC)
|
||||
- 增量:13 RPC(ClassService 4 + AttendanceService 4 + PublishExam/SubmitExam/GradeExam/GradeHomework/UpdateGrade 5)
|
||||
|
||||
### 1.2 HTTP 端点(REST,端口 3004)
|
||||
|
||||
> 当前为 REST 入口(P2 已就绪),P3 启用 gRPC 后 REST 保留为 BFF 兼容入口
|
||||
> 所有端点走 AuthMiddleware + PermissionGuard + Zod ValidationPipe + GlobalErrorFilter(ActionState 信封)
|
||||
|
||||
| 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) | ✅ P2(P3 补 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 | ExamEvent(action: created/updated/deleted) | msg / data-ana |
|
||||
| edu.homework.events | HomeworkEvent(action: assigned/submitted/graded) | msg / data-ana |
|
||||
| edu.grade.events | GradeEvent(action: recorded/updated) | msg / data-ana |
|
||||
| edu.class.events | ClassEvent(action: 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 | Event(action) | 触发时机 | 消费方 | 状态 |
|
||||
| ----- | --------------- | -------- | ------ | ---- |
|
||||
| `edu.teaching.exam.created` | ExamEvent.created | CreateExam 事务内 | msg / data-ana / push-gateway | ⏳ P3(TOPIC_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 | ⏳ P3(topic 待 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 校验) | P4(content 就绪后) |
|
||||
| core-edu | temporal | Workflow.start(examPublishWorkflow) | 考试发布编排工作流 | P3(Temporal 部署后) |
|
||||
|
||||
> 注:core-edu **不主动调 iam gRPC**,通过 Kafka 事件接收 iam 用户变更(见 §2.2)。
|
||||
> P3 不调 content(题库 question_id 仅作外键引用,不校验存在性)。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
| Topic | Event | 发布方 | mock 策略 |
|
||||
| ------------------- | --------------------------------------------------------- | ---------- | -------------------------------------------------------------------- |
|
||||
| edu.iam.user.events | UserEvent(action: created/updated/deleted/role_changed) | iam (ai06) | iam 就绪前不订阅,使用本地内置用户数据(teacher_id/student_id 固定) |
|
||||
| edu.iam.role.events | RoleEvent(action: 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 去重 | P4(P3 可选) |
|
||||
|
||||
> 注:当前 `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 50054(P4) | ai09 | HealthService.Check = SERVING | ⏳(阻塞 P4.2 知识点关联) |
|
||||
| data-ana gRPC 50055(P4) | ai11 | HealthService.Check = SERVING | ⏳(阻塞 P4.1 mastery 消费) |
|
||||
| msg gRPC 50056(P5) | 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 + /readyz(DB 探针)+ REST CRUD(exams/homework/grades)+ Outbox | teacher-bff(REST 调用) | ✅ |
|
||||
| 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.GetKnowledgePoints,lessons.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) |
|
||||
|
||||
@@ -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)
|
||||
> 修订:v2(2026-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 RPC(GetClassPerformance / 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 | MasteryEvent(action: 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 缓存 5min(key: `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 message(ISSUE-002),P5 前需 coord 补全。
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
无。data-ana 不通过 HTTP 调用上游服务。
|
||||
|
||||
### 2.4 CDC 数据源(补充)
|
||||
### 2.4 CDC 数据源
|
||||
|
||||
| 数据源 | 用途 | mock 策略 |
|
||||
| ----------------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------- |
|
||||
| core-edu MySQL(exams/homework/grades/attendance 表) | Debezium CDC → Kafka 同步读模型 | core-edu 就绪前使用 ClickHouse 内置模拟数据集(30 学生 × 5 考试 × 10 作业) |
|
||||
| content MySQL(knowledge_points 表) | Debezium CDC → 知识点元数据同步 | content 就绪前使用内置固定知识点表(数学 50 个知识点) |
|
||||
| iam MySQL(users 表) | 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 RPC(ai06/coord,ISSUE-001)—— P4 阻塞项
|
||||
- [ ] analytics.proto 扩展至 12 RPC(coord/ai11,ISSUE-003)—— gRPC stub 生成前提
|
||||
- [ ] events.proto 补全 AIUsageEvent message(coord,ISSUE-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 就绪前不发布真实 MasteryEvent,msg 使用本地 stub 预警
|
||||
- **Kafka mock**:data-ana 就绪前不发布真实 MasteryUpdated / WarningTriggered,msg 使用本地 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`
|
||||
|
||||
@@ -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.16):REST 供 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 端点(如有)
|
||||
> 端口 50052,P2 即启用(I1 裁决)。⚠️ 当前 iam.proto 仅 4 RPC,待 coord 补全至 12 RPC(ISSUE-005)。
|
||||
|
||||
无对外 HTTP 端点,仅 gRPC。
|
||||
| Service | RPC | 请求 | 响应 | 端口 | 消费方 |
|
||||
| ---------- | ----------------------- | ------------------------------ | ---------------------------- | ----- | ---------------------------------------- |
|
||||
| IamService | Register | RegisterRequest | AuthResponse | 50052 | api-gateway(REST 透传)/ admin-portal |
|
||||
| IamService | Login | LoginRequest | AuthResponse | 50052 | api-gateway(REST 透传)/ admin-portal |
|
||||
| IamService | RefreshToken | RefreshTokenRequest | TokenPair | 50052 | api-gateway(REST 透传) |
|
||||
| IamService | Logout | LogoutRequest | LogoutResponse | 50052 | api-gateway(REST 透传) |
|
||||
| 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-gateway(JWKS 验签) |
|
||||
| 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 Set(Gateway 拉取验签) | 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` | 无 | readiness(5 依赖检查) | k8s / 监控 |
|
||||
| GET | `/metrics` | 无 | Prometheus 指标 | Prometheus |
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。
|
||||
不适用。iam 是业务服务,不暴露 GraphQL(GraphQL 由 BFF 层提供)。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
@@ -66,16 +88,23 @@
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
无上游依赖。
|
||||
iam 是身份根服务,无业务上游依赖。但依赖以下基础设施契约(coord 提供):
|
||||
|
||||
| 依赖项 | 提供方 | 就绪标志 | 状态 |
|
||||
| ------ | ------ | -------- | ---- |
|
||||
| iam.proto 补全至 12 RPC | coord | proto 含 12 RPC + 全部 message | ❌ 仅 4 RPC(ISSUE-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 返回固定 AuthResponse(user.id="mock-user-001", tokens.access_token="mock-access-token")
|
||||
- **gRPC mock**(供 BFF):使用 grpc-mock 拦截 50052 端口,Register/Login 返回固定 AuthResponse(user.id="mock-user-001", tokens.access_token="mock-access-token")
|
||||
- **REST mock**(供 gateway / admin-portal):MSW 或 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 是身份根服务,无业务上游依赖)。
|
||||
|
||||
@@ -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-008(topic 命名)、ISSUE-009(RPC 数量)、ISSUE-013(events.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 RPC,02-architecture-design.md 设计 17 RPC。下表列出设计文档完整 17 RPC,标注基线 13 RPC(✅基线 / ➕扩展待仲裁)。coord 裁决后裁剪或放宽。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
#### NotificationService(9 RPC:5 基线 + 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)
|
||||
#### NotificationPreferenceService(2 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 | NotificationEvent(action: sent/read/recalled/failed) | push-gateway / data-ana |
|
||||
#### NotificationTemplateService(4 RPC:4 基线 + 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 RPC(9 基线表中 ✅ × 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_EXCEEDED(P5 新增 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-005):004 §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 | ExamEvent(action: created/updated/deleted) | core-edu (ai08) | core-edu 就绪前不订阅,使用本地 stub 事件触发 mock 通知 |
|
||||
| edu.homework.events | HomeworkEvent(action: assigned/submitted/graded) | core-edu (ai08) | 同上 |
|
||||
| edu.grade.events | GradeEvent(action: recorded/updated) | core-edu (ai08) | 同上 |
|
||||
| edu.class.events | ClassEvent(action: transferred) | core-edu (ai08) | 同上 |
|
||||
| edu.data_ana.mastery.events | MasteryEvent(action: 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 topic(edu.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 补齐 proto(D1)。补齐前用通用 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 走 gRPC,HTTP 仅为降级)。
|
||||
|
||||
---
|
||||
|
||||
@@ -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 | D1,ISSUE-013 |
|
||||
| events.proto GradeEvent 补 class_id;全部补 student_ids[] | coord | 🔴 阻塞 fan-out | D2,ISSUE-006 |
|
||||
| msg.proto 补 5 扩展 RPC + Preference/Template Service | coord | 🔴 阻塞 gRPC | D3-D4,ISSUE-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 就绪前不发布真实 NotificationEvent,push-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-event(02-architecture-design.md §5 + 004 §7.2) |
|
||||
| ISSUE-009 | RPC 数量(13 vs 17) | 🟡 | 17(02-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 单向) |
|
||||
|
||||
@@ -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,不暴露 gRPC(C2 仲裁) |
|
||||
| API 风格 | GraphQL Yoga(U3 仲裁,P4 直接 GraphQL,不走 REST 过渡) |
|
||||
| 权限校验 | BFF 豁免 @RequirePermission(U4 仲裁),仅校验 x-user-id + ChildGuard 越权防御 |
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
### 1.1 gRPC 接口
|
||||
|
||||
无对外 gRPC。parent-bff 是 GraphQL 聚合层。
|
||||
无对外 gRPC。parent-bff 是 GraphQL 聚合层,对上游仅暴露 HTTP/GraphQL(C2 仲裁)。
|
||||
|
||||
### 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 就绪前返回固定 UserInfo(parent 角色) |
|
||||
| iam (ai06) | IamService.GetEffectivePermissions | 权限校验 | iam 就绪前返回家长权限集 |
|
||||
| iam (ai06) | IamService.GetViewports | 家长导航菜单 | iam 就绪前返回固定视口列表 |
|
||||
| iam (ai06) | IamService.GetChildrenByParent | 查询关联孩子列表(核心) | iam 就绪前返回固定 2 个 ChildInfo(I3/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 | 获取当前家长信息 | 返回固定 UserInfo(parent 角色) | ✅ 已有 |
|
||||
| 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 #5,P3 补全) |
|
||||
| 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 #3,ai05 提请,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-007),GraphQL `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)—— **核心依赖 GetChildrenByParent(I3/ISSUE-047 裁决)**
|
||||
- [ ] iam gRPC 50052 启用(ai06)—— **核心依赖 GetChildrenByParent(I6 裁决,P0 阻塞)**
|
||||
- [ ] iam 补 GetViewports / GetEffectivePermissions RPC(coord-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 + Redis,02 §9 #7)
|
||||
- [ ] GraphQL schema 可内省(POST /graphql 返回 schema)
|
||||
- [ ] 核心 Query 可执行:currentUser / myChildren / childSummary / childGrades
|
||||
- [ ] 核心 Mutation 可执行:markAsRead
|
||||
- [ ] 数据范围校验生效(家长只能查自己孩子的数据,基于 iam.GetChildrenByParent 返回的 user_id 校验)
|
||||
- [ ] 核心 Query 可执行:dashboard / children / childGrades / childAnalytics
|
||||
- [ ] 核心 Mutation 可执行:selectChild(P4)/ markNotificationRead(P5)
|
||||
- [ ] 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**(P5):fetch mock 返回 success
|
||||
- **Redis**:Testcontainers 真实 Redis 实例(不用 mock)
|
||||
- **Kafka**(P5):kafkajs 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_id(student-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 仲裁 |
|
||||
|
||||
@@ -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-001(BFF GraphQL)、ARB-002(MF 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-006):parent-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 角色 | Remote(Shell = 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 shared(singleton) | react / react-dom / urql / graphql / @tanstack/react-query / zustand / nuqs / @edu/ui-tokens / @edu/ui-components / @edu/hooks(ARB-002) |
|
||||
| dev/prod 端口 | 4002([port-allocation.md](../../../infra/port-allocation.md) §4) |
|
||||
| feature flag | `NEXT_PUBLIC_MF_ENABLED`(ARB-002,P4 默认开) |
|
||||
|
||||
> **注**(ISSUE-007):MF 配置文件统一为 `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 返回固定 JWT(parent 角色) |
|
||||
|
||||
> **注**(ISSUE-004):
|
||||
> - 登录端点统一为 `POST /api/v1/iam/login`(与 [matrix.md](./matrix.md) §5 `/api/v1/iam/*` + 01 §3.1 前缀一致)
|
||||
> - 登录是 parent-portal 唯一走 REST(非 GraphQL)的端点:登录前无 JWT,GraphQL 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-001(BFF 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.GetChildrenByParent(I3/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 shared(singleton):react / react-dom / urql / graphql / @tanstack/react-query / zustand / nuqs / @edu/ui-tokens / @edu/ui-components / @edu/hooks(ARB-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 exposes(AppShell + 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-010):iam `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 + UserInfo(parent 角色)
|
||||
- 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 + UserInfo(parent 角色)
|
||||
- **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-001(REST vs GraphQL) | §2.4 | 按 GraphQL 编写(依 ARB-001),待 coord 确认 |
|
||||
| ISSUE-004(登录端点) | §2.3 | 暂用 `POST /api/v1/iam/login`,待 coord 确认 |
|
||||
| ISSUE-006(HTTP 端点分类) | §1.2 | 已修正为"无对外 HTTP API" |
|
||||
| ISSUE-007(MF 配置文件名) | §1.6 | 已修正为 `next.config.js` |
|
||||
| ISSUE-008(GraphQL 路径前缀) | §2.4 | 已修正为 `/api/v1/parent/graphql` |
|
||||
| ISSUE-009(switchChild Mutation) | §2.4 | 列为待仲裁,标注两种方案 |
|
||||
| ISSUE-010(iam GetChildrenByParent 缺失) | §3.1 | P0 阻塞,用 mock 开发 |
|
||||
|
||||
详见 [objections/parent-portal_issue.md](../objections/parent-portal_issue.md)。
|
||||
|
||||
@@ -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)
|
||||
> 版本:v2(2026-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/online),HTTP 同步响应更直接;广播走 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 RS256(query `?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
|
||||
- 服务端自动回 Pong(gorilla/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 | NotificationEvent(action: 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 批次 1):iam gRPC 50052 + `/.well-known/jwks.json` 可访问
|
||||
- [ ] **msg gRPC + Kafka topic**(ai10 批次 4):msg gRPC 50056 + `edu.notification.requested` topic 有事件发布
|
||||
- [x] **Redis 基础设施**(coord P1):Redis 7.x 可访问 ✅ 已就绪
|
||||
- [x] **Kafka 基础设施**(coord P1):Kafka 可访问 ✅ 已就绪
|
||||
- [ ] **ISSUE-001~007 仲裁**(coord):[coord.md](../coord.md) 追加 ARB-003+ 仲裁章节
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] push-gateway HTTP :8081 启用(/healthz 返回 200)
|
||||
- [ ] /readyz 返回 200(含 Kafka 连通性检查通过)
|
||||
- [ ] 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 返 200(含 Redis/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 NotificationEvent(action=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 |
|
||||
|
||||
@@ -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 起直接 GraphQL(GraphQL 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 不订阅 Kafka,P5 后再订阅 | §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 | 降级模式方案 B(data 内 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 起直接 GraphQL(GraphQL 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.1:3 个 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. **消费方**:ai14(student-portal)基于仲裁版 schema 消费
|
||||
4. **变更流程**:
|
||||
- schema 变更需 PR + ai04(BFF)+ 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 |
|
||||
|
||||
> **分页**:所有列表 Query(myClasses / 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(通知未读数)+ ai(AI 助教入口)数据源
|
||||
- 未启用的下游字段返回 `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 key(F4 规范:`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 200,GraphQL `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 就绪前返回固定 UserInfo(student 角色) |
|
||||
| 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 就绪前返回固定 5 个 Grade |
|
||||
| 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 就绪前返回固定 UserInfo(student 角色) |
|
||||
| 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 就绪前返回固定 2 个 Exam |
|
||||
| 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 降级方案**:权限缓存用短 TTL(5min)兜底,不订阅 Kafka 事件。
|
||||
|
||||
### 2.3 HTTP 调用
|
||||
|
||||
**无**。依据 B2 裁决,student-bff 对下游全部走 gRPC,禁止 HTTP fetch。
|
||||
|
||||
### 2.4 DownstreamClient 抽象层(B8 裁决)
|
||||
|
||||
依据 B8 裁决:
|
||||
|
||||
1. **回写 teacher-bff**:DownstreamClient 作为 BFF 模式 v2 标准抽象,回写到 teacher-bff,3 个 BFF(teacher-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-edu(3 项)。
|
||||
|
||||
---
|
||||
|
||||
## §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 返回 200(P3 探针 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 真实就绪前,为 ai14(student-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 success(P5 启用前) |
|
||||
|
||||
> 所有上游 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 |
|
||||
|
||||
@@ -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)
|
||||
> 版本:v2(ai14 接管审计与补全版,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 Remote,ARB-002 对齐)
|
||||
|
||||
| 角色 | 说明 |
|
||||
| ---------------------- | ----------------------------------------------------------------- |
|
||||
| MF Remote | 学生门户是微前端远程模块,由 teacher-portal AppShell 或独立壳加载 |
|
||||
| 暴露的 remote 模块 | StudentApp(学生端完整应用)、shared 学生端组件 |
|
||||
| module federation 配置 | `apps/student-portal/module-federation.config.ts` |
|
||||
| 角色 | 说明 |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| MF Remote | student-portal 是微前端远程模块(P3 首个 Remote,ARB-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 / RequirePermission(ARB-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-ts(ARB-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 /ws(WebSocket) | 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/*` 保持命名一致性
|
||||
> - 由 ai01(api-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 Query(读,16 个)
|
||||
|
||||
| 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 Mutation(写,8 个)
|
||||
|
||||
| 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 Shell(ai13 P2)| exposes AppShell/GraphQLProvider/useGraphQLClient/useAuth/usePermission + shared singleton | P2 启动 | ⏳ 待 ai13 P2 |
|
||||
| api-gateway HTTP :8080(ai01 P3) | `/api/v1/student/*` 反向代理 student-bff 可用 | P3 启动 | ⏳ 待 ai01 P3 |
|
||||
| student-bff GraphQL(ai04 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 50053(ai08 P3) | ExamService/HomeworkService/GradeService/AttendanceService/ClassService | P3 启动 | ⏳ 待 ai08 P3 |
|
||||
| iam gRPC 50052(ai06 P2) | GetUserInfo + GetEffectivePermissions + GetViewports | P3 启动 | ⏳ 待 ai06 P2 |
|
||||
| content gRPC 50054(ai09 P4) | TextbookService + ChapterService + KnowledgeGraphService | P4 启动 | ⏳ 待 ai09 P4 |
|
||||
| data-ana gRPC 50055(ai11 P4) | AnalyticsService.GetStudentWeakness + GetLearningTrend | P4 启动 | ⏳ 待 ai11 P4 |
|
||||
| push-gateway WebSocket :8081/ws(ai02 P5) | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |
|
||||
| msg gRPC 50056(ai10 P5) | NotificationService.ListNotifications + MarkAsRead | P5 启动 | ⏳ 待 ai10 P5 |
|
||||
| ai 服务 gRPC 50057(ai12 P5,可选) | AiService.Chat(SSE 流式) | 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 + UserInfo(student 角色)
|
||||
- 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 mock(MSW)
|
||||
|
||||
- `POST /api/v1/student/graphql` → 按 operationName 返回对应 mock 响应(见 §2.4)
|
||||
- `POST /api/auth/login` → 返回固定 JWT + UserInfo(student 角色)由 Shell 处理
|
||||
- `POST /api/v1/student/upload` → 返回固定 signed URL(待 ISSUE-014-05 仲裁后实现)
|
||||
- 所有 mock 响应定义在 `apps/student-portal/src/mocks/fixtures/*.json`
|
||||
|
||||
#### 4.2.2 WebSocket mock(mock-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` 存储(前端可独立测试断网恢复逻辑)
|
||||
- 不需要 mock,IDB 在浏览器原生支持
|
||||
|
||||
---
|
||||
|
||||
## §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.3(auth/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**: ai14(student-portal)
|
||||
**Branch**: feat-review-student-portal-docs-9yN6Av
|
||||
**Coordinator**: coord-ai
|
||||
|
||||
@@ -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)
|
||||
> 版本:v2(2026-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 启动前仲裁第一版。
|
||||
> 前端 AI(ai13/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.5,P2 班级列表来自 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.AttendanceService(C4 裁决 P3 补全) | P3+ |
|
||||
| **content** | textbooks / chapters / knowledgePoints / questions | content 4 个 Service | P4+ |
|
||||
| **notifications** | myNotifications / markAsRead | msg.NotificationService | P5+ |
|
||||
| **ai** | aiChat / generateQuestion / generateLessonPlan | ai.AiService(A4 裁决 P5 补全 GenerateLessonPlan) | P5+ |
|
||||
| **admin.\*** | admin.schoolStats / admin.listClasses / admin.listTeachers 等 | admin 命名空间,复用 teacher-bff endpoint(president §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 endpoint,ai03 在 P6 实现 admin Resolver 真实数据
|
||||
- **理由**:admin 操作低 QPS,无需独立 admin-bff 服务;减少 ai16 工作量
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。teacher-bff 不发布事件,仅做 gRPC 聚合。
|
||||
无。teacher-bff 不发布事件,仅做 gRPC 聚合(B7 裁决:P2-P4 不订阅 Kafka,P5 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 就绪前返回固定 UserInfo(teacher 角色) |
|
||||
| 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 | 返回固定 UserInfo(teacher 角色) |
|
||||
| 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 RPC(Register/Login/RefreshToken/GetUserInfo),matrix.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=true(student-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=true(C4 裁决补全) |
|
||||
| AttendanceService.GetClassAttendance | 查询考勤 | ❌ | P3 | 返回固定考勤数据(C4 裁决补全) |
|
||||
|
||||
> core_edu.proto 现状 14 RPC(ExamService 5 + HomeworkService 4 + GradeService 5),matrix.md §2 标称 22 RPC 5 Service,待 coord 按 C4/C5 裁决补 AttendanceService + ClassService(GetClassesByTeacher / 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 RPC(TextbookService 3 + KnowledgeGraphService 2),matrix.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+ | 流式 mock(D4 裁决补全) |
|
||||
|
||||
> analytics.proto 现状 3 RPC,matrix.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 RPC(NotificationService),matrix.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 | 流式 mock(SSE) |
|
||||
| AiService.GenerateQuestion | AI 出题 | ✅ | P5 | 返回固定题目 |
|
||||
| AiService.OptimizeExpression | 表达优化 | ✅ | P5 | 返回固定优化结果 |
|
||||
| AiService.GenerateLessonPlan | AI 备课 | ❌ | P5 | 返回固定教案(A4 裁决补全) |
|
||||
| AiService.StreamGenerateQuestion (stream) | 流式出题 | ❌ | P5 | 流式 mock(A4 裁决补全) |
|
||||
|
||||
> ai.proto 现状 4 RPC,matrix.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 裁决),上游就绪后移除拦截器切换真实调用。
|
||||
|
||||
@@ -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 存入 localStorage,F12;P6 迁移 httpOnly cookie)
|
||||
- [ ] GraphQL 查询可执行(dashboard / viewports / me / classes / class Query,ARB-001)
|
||||
- [ ] WebSocket 通知可接收(push-gateway 推送 → 前端通知中心更新)
|
||||
|
||||
---
|
||||
@@ -118,9 +118,9 @@ teacher-portal 是最前端,无下游消费方。但对开发体验提供:
|
||||
|
||||
- **HTTP/GraphQL mock**:使用 MSW(Mock 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 公钥配对),存入 localStorage(F12;P6 迁移 httpOnly cookie)
|
||||
- **环境切换**:通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制是否启用 MSW,上游就绪后设为 `disabled`
|
||||
|
||||
Reference in New Issue
Block a user