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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
# 模块理解确认书 — teacher-portal
> AIai07TS/React · 教学场景域前端 shell
> AIai13TS/React · 教学场景域前端 shell
> 阶段:阶段 1 交付物
> 日期2026-07-09
> 关联:[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.1a/1.1b/§5.4、[AI 分配方案](../../../docs/architecture/ai-allocation.md) §5 ai13/ai07、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)、[known-issues §2.12](../../../docs/troubleshooting/known-issues.md)
> 日期2026-07-092026-07-10 审查回写:对齐 F9 GraphQL P2 起 + ARB-001/002 仲裁)
> 关联:[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.1a/1.1b/§5.4、[AI 分配方案](../../../docs/architecture/ai-allocation.md) §5 ai13、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)、[known-issues §2.12](../../../docs/troubleshooting/known-issues.md)、[coord 仲裁 ARB-001/002](../../../docs/architecture/issues/coord.md)
---
@@ -12,16 +12,17 @@
- **层级**L2 微前端层004 §3.1 六层架构中的前端层)
- **MF 角色****Shell 宿主**(主应用),其余 3 端student/parent/admin-portal作为 Remote 子应用挂载
- **上游(谁调用我)**:浏览器(教师 / 教导主任 / 教研组长)
- **下游(同步)**api-gatewayREST经 Next.js `rewrites` 代理 `/api/v1/*`
- **下游(同步)**api-gateway经 Next.js `rewrites` 代理 `/api/v1/*`,再反向代理到 teacher-bff `POST /graphql`
- **下游推送P5**push-gatewayWebSocket/SSE
- **BFF 对接**teacher-bffGraphQL Yoga + DataLoaderP2-P3 用 REST 过渡)
- **通信方式**HTTP/REST前端→Gateway+ WebSocket前端→push-gatewayP5
- **BFF 对接**teacher-bffGraphQL Yoga + DataLoaderP2 起 all-in GraphQLF9 裁决,无 REST 过渡)
- **通信方式**GraphQL over HTTP前端→Gateway→teacher-bff+ WebSocket前端→push-gatewayP5+ SSE前端→ai 服务P5
- **不直连**:前端不直连任何业务服务或 BFF 后端实例,全部经 api-gateway 代理
**说明**
- 通过 `next.config.js``rewrites``/api/v1/*` 代理到 `api-gateway`
- MF 架构下teacher-portal 作为 Shell 宿主提供 AppShell + 共享组件库 + 权限 Hook + API 请求层
- 通过 `next.config.js``rewrites``/api/v1/*` 代理到 `api-gateway`api-gateway 再反向代理到 teacher-bff GraphQL endpoint
- F9 裁决P2 起 BFF 用 GraphQLYoga + DataLoader前端用 urql client**无 REST 过渡阶段**(详见 [03-long-term-architecture.md §1.4](./03-long-term-architecture.md)
- MF 架构下teacher-portal 作为 Shell 宿主提供 AppShell + GraphQLProvider 单例 + 共享组件库 + 权限 HookARB-002 暴露清单)
- 场景域 BFF 复用策略004 §5.4):教导主任/教研组长复用 teacher-portal + 额外管理视口,不单独建 portal
## 2. 我的限界上下文
@@ -49,19 +50,21 @@
### 3.1 消费的后端 API经 api-gateway 代理)
| 路径前缀 | 下游 BFF/服务 | 关键端点 |
| ------------------------------------------------------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/api/v1/iam/*` | iam | `POST /iam/login``POST /iam/register``POST /iam/refresh``GET /iam/me``GET /iam/rbac/...``GET /iam/effective-permissions` |
| `/api/v1/teacher/*` | teacher-bff | `GET /teacher/viewports``GET /teacher/dashboard``GET /teacher/classes/:id/exams``GET /teacher/classes/:id/homework``GET /teacher/exams/:id/grades` |
| `/api/v1/classes/*` | core-educlasses 模块) | CRUD黄金模板 |
| `/api/v1/exams/*` `/api/v1/homework/*` `/api/v1/grades/*` | core-edu | P3 教学核心 |
| `/api/v1/textbooks/*` `/api/v1/knowledge-points/*` `/api/v1/questions/*` | content | P4 内容 |
| `/api/v1/ai/*` | aiSSE 流式) | P5 AI 辅助出题 |
| `/api/v1/notifications/*` | msg | 通知中心P5 |
F9 裁决P2 起 all-in GraphQL前端不再消费 REST 端点登录除外。teacher-bff GraphQL endpoint `POST /graphql`(经 api-gateway `/api/v1/teacher/*` 代理schema 详见 [coord ARB-001](../../../docs/architecture/issues/coord.md#§1) + [teacher-portal_contract.md §2.4](../../../docs/architecture/issues/contracts/teacher-portal_contract.md)。
### 3.2 统一响应契约
| 阶段 | 消费方式 | 下游 BFF/服务 | 关键 GraphQL OperationP2 Must Have 加粗) |
| ---- | ---------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| P2 | GraphQL Queryteacher-bffARB-001 第一版) | teacher-bff → iam/classes | **`dashboard`**、**`viewports`**、**`me`**、**`classes`**、**`class(id)`**5 个 QueryARB-001 §1.2 |
| P2 | HTTP登录MF 依赖认证前提,非 GraphQL | api-gateway → iam | `POST /api/auth/login`F12JWT 存 localStorageP6 迁移 httpOnly cookie |
| P3 | GraphQL Query + Mutation | teacher-bff → core-edu | `classExams` / `createExam` / `classHomework` / `assignHomework` / `studentGrades` / `recordGrade` |
| P4 | GraphQL Query | teacher-bff → content/data-ana | `knowledgeGraph` / `studentAnalytics` |
| P5 | GraphQL Query + Mutation + SSE/WS | teacher-bff → msg/ai + push-gateway | `myNotifications` / `markAsRead` / `generateQuestion`SSE 流式)/ `generateLessonPlan` |
所有后端响应遵循 `ActionState` 结构(迁移指南 §7.5
> P2 不包含exams/homework/grades QueryP3 core-edu 就绪后、classPerformance/studentWeakness/learningTrendP4 data-ana、notificationsP5 msg、所有 MutationP3+、SubscriptionP6+ 评估)——见 ARB-001 §1.2。
### 3.2 统一响应契约ActionState 信封 + GraphQL errors
BFF GraphQL 始终返回 `ActionState` 信封004 §11.5GraphQL `errors[]` 数组扩展 ActionState 字段(`extensions.code` = `BFF_TEACHER_*`G14 裁决):
```typescript
type ActionState<T> =
@@ -72,18 +75,18 @@ type ActionState<T> =
};
```
错误码前缀按服务名大写(如 `IAM_``CORE_EDU_``CONTENT_``MSG_``AI_``BFF_TEACHER_``GW_`)。前端 API 请求层根据 `error.code` 前缀路由到对应 i18n key。
错误码前缀按服务名大写(如 `IAM_``CORE_EDU_``CONTENT_``MSG_``AI_``BFF_TEACHER_``GW_`,见 [matrix.md §6](../../../docs/architecture/issues/matrix.md))。前端 urql client 解析 `errors[].extensions.code` 前缀路由到对应 i18n key(详见 [03 §1.4.3](./03-long-term-architecture.md)
### 3.3 推送契约P5
| 协议 | 场景 |
| ------------------------- | -------------------------------------------- |
| WebSocketpush-gateway | 学生提交作业通知、考试成绩录入提醒、全校广播 |
| SSEai 服务 | AI 辅助出题流式响应 |
| SSEai 服务,经 teacher-bff 代理或直连) | AI 辅助出题流式响应 |
### 3.4 proto 不直接消费
前端不调用 gRPCBFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
前端不调用 gRPCBFF 把 gRPC 聚合为 GraphQL 暴露给前端F9 裁决)。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
## 4. 我的技术栈
@@ -91,11 +94,12 @@ type ActionState<T> =
| --------------------------- | -------------------------------------------------------- | ---------------------------------------------- |
| 框架 | Next.js 14+App Router | server components 默认client components 按需 |
| 语言 | TypeScript 5.5+strict | 沿用 tsconfig.base.json |
| 微前端 | Module Federation 2.0@module-federation/nextjs-mf | teacher-portal = Shell |
| 微前端 | Module Federation 2.0@module-federation/nextjs-mf | teacher-portal = ShellARB-002 暴露清单) |
| **GraphQL client** | **urql**F9 裁决 P2 起shell 单例ARB-002 shared singleton | cacheExchange + fetchExchange详见 [03 §1.4](./03-long-term-architecture.md) |
| 样式 | Tailwind CSS 3.4+ | 配合设计令牌三层模型 |
| UI 组件库 | shadcn/ui迁移指南 §7.2 | 平移至 `packages/ui-components/`MF 共享 |
| 状态管理 L1 URL | nuqs | 可分享、可刷新状态 |
| 状态管理 L2 Server | TanStack Query v5 | 服务端数据缓存、重试、乐观更新 |
| 状态管理 L2 Server | urql cacheExchangeGraphQL 数据)+ TanStack Query非 GraphQL 场景:文件上传/SSE | F9 起不再双缓存层,详见 [03 §1.4.4](./03-long-term-architecture.md) |
| 状态管理 L3 Client Business | Zustand slice | 客户端业务状态 |
| 状态管理 L4 Global UI | Zustand ui-store + ModalRoot | 全局 UI 状态 |
| 状态管理 L5 Form | react-hook-form + zodResolver | 表单状态 |
@@ -103,7 +107,7 @@ type ActionState<T> =
| 图表 | recharts | 学情、Dashboard |
| i18n | next-intl | BFF/服务返回 i18n key + 参数,前端翻译 |
| A11y | eslint-plugin-jsx-a11yerror 级) | WCAG 2.2 AA |
| 字体 | Intersans/ Frauncesserif/ JetBrains Monomono | next/font/google 加载CSS 变量暴露 |
| 字体 | sans/serif/mono通过 `var(--font-family-*)` 引用,禁止字面量 | next/font/google 加载CSS 变量暴露§3.10 强制) |
## 5. 我的阶段归属
@@ -201,5 +205,5 @@ apps/teacher-portal/
---
**AI Agent**: ai07 (teacher-portal shell)
**Branch**: docs/teacher-portal-stage1-stage2-design-ai07
**AI Agent**: ai13 (teacher-portal shell)
**Branch**: feat-review-teacher-portal-docs-vErc0L

View File

@@ -1,14 +1,14 @@
# 模块架构设计文档 — teacher-portal
> AIai07TS/React · 教学场景域前端 shell
> AIai13TS/React · 教学场景域前端 shell
> 阶段:阶段 2 交付物
> 日期2026-07-09
> 关联:[阶段 1 理解确认书](./01-understanding.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §5.4、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)
> 状态:待 coord 交叉审查
> 日期2026-07-092026-07-10 审查回写:对齐 F9 GraphQL P2 起 + ARB-001/002 仲裁REST→GraphQL 全量重写)
> 关联:[阶段 1 理解确认书](./01-understanding.md)、[长远架构补全](./03-long-term-architecture.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §5.4、[coord 仲裁 ARB-001/002](../../../docs/architecture/issues/coord.md)、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)
> 状态:✅ 已对齐 F9GraphQL P2 起)+ ARB-001schema 第一版)+ ARB-002MF Shell 暴露清单)
---
## 1. 模块内部分层图4 端统一 MF 架构)
## 1. 模块内部分层图4 端统一 MF + GraphQL 架构)
```mermaid
graph TB
@@ -16,26 +16,27 @@ graph TB
URL[URL 路由]
end
subgraph Shell["teacher-portalShell 宿主)"]
AppShell[AppShell<br/>左栏导航 + 主内容区]
subgraph Shell["teacher-portalShell 宿主 :4000"]
RootLayout[RootLayout<br/>字体/令牌/i18n Provider]
GraphQLProvider[GraphQLProvider<br/>urql client 单例ARB-002 §2.17 方案 A]
AppShell[AppShell<br/>左栏导航 + 主内容区 + 路由守卫]
Router[Next.js App Router]
SharedDeps["共享依赖暴露<br/>react/react-dom/@tanstack/react-query/zustand/nuqs"]
SharedDeps["共享依赖暴露<br/>react/react-dom/urql/graphql/@edu/*ARB-002 shared singleton"]
end
subgraph RemoteTeacher["teacher-portal Remote 模块"]
TeacherPages[教学场景页面<br/>dashboard/classes/exams/homework/grades/ai-assist]
end
subgraph RemoteStudent["student-portalRemote"]
subgraph RemoteStudent["student-portalRemoteP3+"]
StudentPages[学习场景页面<br/>dashboard/homework/submit/diagnostic/exam-taking]
end
subgraph RemoteParent["parent-portalRemote"]
subgraph RemoteParent["parent-portalRemoteP4+"]
ParentPages[家长场景页面<br/>dashboard/children-switch/grades/notifications]
end
subgraph RemoteAdmin["admin-portalRemote"]
subgraph RemoteAdmin["admin-portalRemoteP6+"]
AdminPages[管理场景页面<br/>users/roles/permissions/viewports/monitoring]
end
@@ -43,22 +44,27 @@ graph TB
UITokens[ui-tokens<br/>三层设计令牌]
UIComponents[ui-components<br/>shadcn + A11y + ErrorBoundary]
Contracts[contracts<br/>Permissions 常量 + 类型]
Hooks[hooks<br/>usePermission/useAuth/useA11y]
LibTS[shared-ts<br/>通用工具]
Hooks[hooks<br/>usePermission/useAuth/useGraphQLClient]
LibTS[shared-ts<br/>GraphQL schema + 通用工具]
end
subgraph Gateway["api-gateway"]
subgraph Gateway["api-gateway :8080"]
GW[Gin 路由/鉴权/限流]
end
subgraph TeacherBFF["teacher-bff :3003"]
BFF[GraphQL Yoga + DataLoader<br/>POST /graphqlARB-001 schema]
end
Browser --> URL
URL --> RootLayout
RootLayout --> AppShell
RootLayout --> GraphQLProvider
GraphQLProvider --> AppShell
AppShell --> Router
Router -->|动态加载| RemoteTeacher
Router -->|动态加载| RemoteStudent
Router -->|动态加载| RemoteParent
Router -->|动态加载| RemoteAdmin
Router -->|动态加载 P3+| RemoteStudent
Router -->|动态加载 P4+| RemoteParent
Router -->|动态加载 P6+| RemoteAdmin
RemoteTeacher --> SharedDeps
RemoteStudent --> SharedDeps
@@ -74,41 +80,43 @@ graph TB
RemoteParent --> UITokens
RemoteAdmin --> UITokens
AppShell -->|fetch /api/v1/iam/effective-permissions| Hooks
Hooks -->|透传 token| GW
RemoteTeacher -->|fetch /api/v1/teacher/*| GW
RemoteStudent -->|fetch /api/v1/student/*| GW
RemoteParent -->|fetch /api/v1/parent/*| GW
RemoteAdmin -->|fetch /api/v1/iam/* + /api/v1/admin/*| GW
GraphQLProvider -->|POST /graphql over HTTP| GW
GW -->|反向代理 /api/v1/teacher/*| BFF
BFF -->|gRPC 聚合 iam+classes+core-edu+content+data-ana+msg+ai| Services
```
### 1.1 MF 拓扑选型
| 方案 | 选否 | 理由 |
| ------------------------------------ | ---- | --------------------------------------------------------------------------------------- |
| 4 端独立部署 + 独立域名 + 各自 Shell | ❌ | 4 套 Shell 重复,登录态/权限/组件库要重复实现 |
| 单 Shell + 4 Remote**采用** | ✅ | teacher-portal 作为 Shell 宿主,提供 AppShell + 共享依赖;其余 3 端作为 Remote 动态加载 |
| 4 端独立部署 + 独立域名 + 各自 Shell | ❌ | 4 套 Shell 重复,登录态/权限/组件库/GraphQL client 要重复实现 |
| 单 Shell + 4 Remote**采用** | ✅ | teacher-portal 作为 Shell 宿主,提供 AppShell + GraphQLProvider 单例 + 共享依赖;其余 3 端作为 Remote 动态加载 |
| 单一 Next.js 应用 + 4 路由组 | ❌ | 违反"微前端独立部署"目标ADR-012 |
**Shell 职责**teacher-portal
**Shell 职责**teacher-portalARB-002 §2.2
- RootLayout字体、设计令牌、i18n Provider、TanStack QueryClientProvider、Zustand StoreProvider
- AppShell左侧导航 + 主内容区 + 用户信息 + 登出)
- 共享依赖暴露react、react-dom、@tanstack/react-query、zustand、nuqs、ui-components、ui-tokens、contracts、hooks
- RootLayout字体、设计令牌、i18n Provider、GraphQLProvider、Zustand StoreProvider
- AppShell左侧导航 + 主内容区 + 用户信息 + 登出 + 路由守卫
- GraphQLProviderurql client 单例,总裁 §2.17 方案 ARemote 复用
- 共享依赖暴露react、react-dom、urql、graphql、@edu/ui-tokens、@edu/ui-components、@edu/hooks
- 路由表4 端路由前缀:`/teacher/*``/student/*``/parent/*``/admin/*`
- 登录页(统一登录入口,按角色重定向到对应 portal
- 登录页(统一登录入口 `POST /api/auth/login`,非 MF登录是认证前提
### 1.2 MF 配置teacher-portal/next.config.js
### 1.2 MF 配置teacher-portal/next.config.jsARB-002 §2.2 裁决
```javascript
// teacher-portal/next.config.jsShell
// teacher-portal/next.config.jsShellARB-002 第一版暴露清单
const NextFederationPlugin = require("@module-federation/nextjs-mf");
const remotes = (isServer) => ({
student: `student_app@http://localhost:3001/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
parent: `parent_app@http://localhost:3002/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
admin: `admin_app@http://localhost:3003/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
});
// P2remotes 为空NEXT_PUBLIC_MF_ENABLED=falseARB-002 §2.3P3+ 逐步接入
const remotes = (isServer) => {
if (process.env.NEXT_PUBLIC_MF_ENABLED !== "true") return {};
return {
student: `student_app@http://localhost:4001/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
parent: `parent_app@http://localhost:4002/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
admin: `admin_app@http://localhost:4003/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
};
};
module.exports = {
reactStrictMode: true,
@@ -117,17 +125,28 @@ module.exports = {
new NextFederationPlugin({
name: "teacher_app",
filename: "static/chunks/remoteEntry.js",
remotes: remotes(isServer),
remotes: remotes(isServer), // P2 为 {}ARB-002 §2.3
exposes: {
"./AppShell": "./src/components/AppShell",
"./shared-deps": "./src/shared/deps",
// ARB-002 §2.2 暴露清单
"./AppShell": "./src/components/AppShell.tsx",
"./GraphQLProvider": "./src/app/providers.tsx",
"./useAuth": "./packages/hooks/src/use-auth.ts",
"./usePermission": "./packages/hooks/src/use-permission.ts",
"./useGraphQLClient": "./packages/hooks/src/use-graphql-client.ts",
"./ErrorBoundary": "./packages/ui-components/src/error-boundary.tsx",
"./Loading": "./packages/ui-components/src/loading.tsx",
"./Empty": "./packages/ui-components/src/empty.tsx",
"./RequirePermission": "./packages/ui-components/src/require-permission.tsx",
},
shared: {
// ARB-002 §2.2 singleton 配置
react: { singleton: true, requiredVersion: "^18.3.0" },
"react-dom": { singleton: true, requiredVersion: "^18.3.0" },
"@tanstack/react-query": { singleton: true },
zustand: { singleton: true },
nuqs: { singleton: true },
urql: { singleton: true, requiredVersion: "^2.2.0" },
graphql: { singleton: true, requiredVersion: "^16.8.0" },
"@edu/ui-tokens": { singleton: true },
"@edu/ui-components": { singleton: true },
"@edu/hooks": { singleton: true },
},
extraOptions: { exposePages: false },
}),
@@ -145,154 +164,181 @@ module.exports = {
};
```
> Remote 端配置对称:`name: 'student_app'``exposes: { './pages': './src/pages' }``remotes: { teacher: 'teacher_app@...' }`
> **P2 Remote 数量 = 0**ARB-002 §2.3`NEXT_PUBLIC_MF_ENABLED=false` 默认关。P3 student-portal 作为首个 Remote 接入(总裁 §1.3 step 4。feature flag 控制回退单体路由
## 2. 领域模型(前端视角)
前端不持有业务聚合根,仅持有"视图模型"ViewModel和"会话状态"。
前端不持有业务聚合根,仅持有"视图模型"ViewModel和"会话状态"。数据来源统一为 teacher-bff GraphQLF9 裁决)。
### 2.1 会话状态Session
```typescript
interface Session {
user: UserInfo; // { id, email, name, roles, permissions, dataScope }
tokens: { accessToken: string; refreshToken: string };
viewports: ViewportItem[]; // L1 导航视口
user: UserInfo; // { id, email, name, roles, dataScope }GraphQL me Query
tokens: { accessToken: string; refreshToken: string }; // F12: localStorageP6 迁移 cookie
viewports: ViewportItem[]; // L1 导航视口GraphQL viewports Query
permissions: string[]; // 权限点GraphQL me Query 内嵌或 effective-permissions
expiresAt: number; // access token 过期时间戳
}
```
存储Zustand sessionSliceL3+ localStorage 持久化(刷新恢复+ TanStack Query 缓存 `['session']`L2)。
存储Zustand sessionSliceL3+ localStorage 持久化(刷新恢复F12+ urql cacheL2GraphQL document cache)。
### 2.2 视口模型Viewport
```typescript
// 对齐 ARB-001 §1.2 schemaViewportItem
interface ViewportItem {
key: string; // 'dashboard' | 'classes' | ...
label: string; // i18n key 或显式文案
route: string; // '/teacher/dashboard'
icon: string | null; // 图标 key按需
sortOrder: number; // 排序
requiredPermission: string | null; // 'CLASSES_READ' 等
scope: "teacher" | "student" | "parent" | "admin"; // 标记归属哪个 portal
route: String; // '/teacher/dashboard'schema 为 String
icon: String | null; // 图标 keyschema 可空
sortOrder: String; // 排序schema 为 String
requiredPermission: String | null; // 'CLASSES_READ' 等
}
```
来源:`GET /api/v1/{scope}/viewports`BFF 聚合 iam 视口配置。AppShell 按 `scope` 过滤渲染对应 portal 的导航。
来源:GraphQL `viewports` QueryARB-001 §1.2。AppShell 按 `requiredPermission` + `usePermission().hasPermission()` 过滤渲染导航。
### 2.3 权限模型Permission
```typescript
interface PermissionState {
permissions: string[]; // ['CLASSES_READ', 'EXAMS_CREATE', ...]
dataScope: DataScope; // L0-L5
permissions: string[]; // ['CLASSES_READ', 'EXAMS_CREATE', ...]GraphQL me Query 返回)
dataScope: DataScope; // SELF | CLASS | GRADE | SCHOOL | DISTRICT | ALLARB-001 enum
hasPermission: (perm: string) => boolean;
hasAnyPermission: (perms: string[]) => boolean;
hasAllPermissions: (perms: string[]) => boolean;
}
```
来源:`GET /api/v1/iam/effective-permissions``{ permissions, viewports, dataScope }`。Redis 缓存 5miniam 侧),前端 TanStack Query 缓存 5min角色变更主动 invalidate
来源:GraphQL `me` Query 返回 `{ user, roles, dataScope }`ARB-001 §1.2)。角色变更由 iam 发 Kafka → msg → push-gateway WebSocket 推送 → 前端 invalidate urql cache见 [03 §2.5](./03-long-term-architecture.md)
## 3. 数据模型(前端)
## 3. 数据模型(前端缓存层
前端无数据库,仅有缓存层
前端无数据库,仅有 urql 缓存层03 §1.4.4 缓存策略)
| 数据类型 | 存储 | TTL | 失效策略 |
| ----------------------- | ---------------------- | --------------------------- | -------------------------------------- |
| Sessiontoken + user | localStorage + Zustand | access 15min / refresh 7day | 401 自动 refreshrefresh 失败跳登录 |
| 权限列表 | TanStack Query cache | 5min | 角色变更事件 invalidate |
| 视口列表 | TanStack Query cache | 5min | 同上 |
| 班级/年级列表 | TanStack Query cache | 5min | staleTime 5minmutation 后 invalidate |
| 教学资源详情 | TanStack Query cache | 30s | staleTime 30s |
| 学情宽表 | TanStack Query cache | 30s | staleTime 30s(实时性由 BFF 决定) |
| URL 状态(分页/筛选) | nuqs | — | 永久(可分享) |
| 表单临时态 | react-hook-form | — | 卸载即销毁 |
| 数据类型 | 存储 | staleTime | 失效策略 |
| ----------------------- | ----------------------------- | ---------------------------------- | --------------------------------------------- |
| Sessiontoken + user | localStorage + ZustandF12 | access 15min / refresh 7day | 401 自动 refreshrefresh 失败跳登录 |
| 权限列表 + 视口 | urql cacheExchange | 0always fetch | 角色变更 WebSocket 推送后强制 invalidate |
| 班级/年级列表 | urql cacheExchange | 30s | mutation 后 invalidate |
| 考试/作业/成绩 | urql cacheExchange | 10s | mutation 后 invalidate + 乐观更新03 §10.1 |
| 教学资源详情 | urql cacheExchange | 30s | staleTime 30s |
| 学情宽表 | urql cacheExchange | 60s | ISR revalidate 60s + CSR 交互 |
| URL 状态(分页/筛选) | nuqs | — | 永久(可分享) |
| 表单临时态 | react-hook-form | — | 卸载即销毁 |
## 4. API 设计(前端 → 后端)
> urql 默认 document cache 适合简单场景复杂关联班级↔考试↔成绩P3 起评估切 `@urql/exchange-graphcache` normalized cache03 §1.4.4)。不引入 TanStack Query 做 GraphQL 缓存避免双缓存层TanStack Query 仅用于非 GraphQL 场景(文件上传/SSE
前端不设计后端 API仅声明消费的端点。详见 [01-understanding.md §3.1](./01-understanding.md)。
## 4. API 设计(前端 → teacher-bff GraphQL
### 4.1 统一 API 请求层lib/api.ts
F9 裁决P2 起 all-in GraphQL。前端不再设计 REST 请求层,统一通过 urql client 消费 teacher-bff GraphQLARB-001 schema 第一版)。
### 4.1 urql client 单例packages/hooks + Shell GraphQLProvider
```typescript
// packages/shared-ts/src/api-client.ts共享
interface ApiClientOptions {
baseUrl?: string; // 默认 ''(走 Next.js rewrites
getToken?: () => string | null;
onUnauthorized?: () => void; // 401 → refresh → 重试 / 跳登录
onError?: (error: ApiError) => void; // 全局 toast
}
// packages/hooks/src/use-graphql-client.tsShell 暴露Remote 复用ARB-002 §2.17 方案 A
import { createClient, Client } from "urql";
import { cacheExchange, fetchExchange } from "urql";
class ApiClient {
async get<T>(path: string, query?: Record<string, string>): Promise<T>;
async post<T>(path: string, body: unknown): Promise<T>;
async put<T>(path: string, body: unknown): Promise<T>;
async delete<T>(path: string): Promise<T>;
async sse<T>(path: string, body: unknown): AsyncIterable<T>; // AI 流式
}
const client: Client = createClient({
url: process.env.NEXT_PUBLIC_TEACHER_BFF_GRAPHQL_URL!, // teacher-bff POST /graphql经 api-gateway 代理)
fetchOptions: () => {
const token = readAuthToken(); // useAuth 注入F12: localStorage
const traceId = readTraceId();
return {
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
"X-Trace-Id": traceId,
},
};
},
exchanges: [cacheExchange, fetchExchange],
});
// 错误结构
interface ApiError {
code: string; // 'IAM_INVALID_CREDENTIALS'
message: string; // 已 i18n 翻译或后端原文
details?: unknown;
httpStatus: number;
}
// Shell: src/app/providers.tsx 包裹 <Provider value={client}>,所有 Remote 复用同一 client
```
**职责**
- 自动注入 `Authorization: Bearer ${token}`
- 401 自动 refresh token 一次,失败调 `onUnauthorized`
- 解析 `ActionState`success=false 抛 `ApiError`
- `error.code` 前缀路由 i18n key
- 全局错误 toast除 401
- 请求/响应 trace_id 透传(从响应头 `X-Request-Id` 提取)
- Shell 初始化 urql client 单例,通过 React Context 注入给所有 RemoteARB-002 方案 A
- token 刷新/401 处理由 useAuth 统一拦截(不污染 urql exchange 链,见 [03 §2.7](./03-long-term-architecture.md)
- MF shared singletonreact/urql/graphql避免 Remote 多实例 + 缓存不一致
- 请求/响应 trace_id 透传(`X-Trace-Id` header
### 4.2 TanStack Query 约定
### 4.2 GraphQL Query/Mutation 约定(对齐 ARB-001 schema
```typescript
// Query Key 命名:[scope, resource, ...args]
queryKey: ["teacher", "classes", { gradeId }];
queryKey: ["teacher", "exams", classId];
queryKey: ["session", "effective-permissions"];
queryKey: ["session", "viewports", "teacher"];
// P2 QueryARB-001 §1.25 个 Must Have
import { gql } from "urql";
// Mutation 约定
const mutation = useMutation({
mutationFn: (input) => api.post("/api/v1/classes", input),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["teacher", "classes"] }),
onError: (e: ApiError) => toast.error(e.message),
});
const DashboardQuery = gql`
query Dashboard {
dashboard {
user { id email name roles dataScope }
classes { id name gradeId studentCount }
viewports { key label route icon sortOrder requiredPermission }
stats { totalExams pendingGrading todayHomework }
}
}
`;
const ViewportsQuery = gql`query { viewports { key label route icon sortOrder requiredPermission } }`;
const MeQuery = gql`query { me { id email name roles dataScope } }`;
const ClassesQuery = gql`query { classes { id name gradeId studentCount } }`;
// P3+ MutationARB-001 §1.2 P2 不包含)
const CreateExamMutation = gql`
mutation CreateExam($input: CreateExamInput!) {
createExam(input: $input) { ... on ExamCreated { id } }
}
`;
```
**urql mutation + 乐观更新**
```typescript
const [, createExam] = useMutation(CreateExamMutation);
// 乐观更新onMutate 回滚 + invalidateQueries见 03 §10.1
```
### 4.3 错误处理ActionState 信封 + GraphQL errorsARB-001 §1.3
| 场景 | 处理 |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| GraphQL 错误errors[] | 解析 `extensions.code`BFF_TEACHER_* 前缀G14映射到 i18n key `error.teacher_bff.<code_snake>`F11 |
| 网络错误 | 重试 1 次 → 提示网络异常 → 记录 trace_id |
| 401 | useAuth 静默 refresh 一次 → 重试;失败跳登录(见 03 §2.7 |
| 部分失败data + errors 共存) | 渲染 data 可用部分 + errors 区域提示不整体失败ARB-001 §1.3 降级模式方案 B |
- BFF 始终返回 `ActionState` 信封004 §11.5),前端统一走 `normalizeActionState()` 工具packages/hooks
## 5. 事件设计
前端不发布 Kafka 事件,仅消费 WebSocket 推送P5和 Server-Sent EventsAI 流式)。
### 5.1 WebSocket 推送P5
| 事件 | 触发 | teacher-portal 前端动作 |
| ----------------------- | ------------ | ------------------------------- |
| `NotificationRequested` | msg 服务投递 | toast 提示 + 通知中心未读数 +1 |
| `HomeworkSubmitted` | 学生提交作业 | toast + 作业批改列表 invalidate |
| 事件 | 触发 | teacher-portal 前端动作 |
| ----------------------- | ------------ | ----------------------------------------------- |
| `NotificationRequested` | msg 服务投递 | toast 提示 + 通知中心未读数 +1 |
| `HomeworkSubmitted` | 学生提交作业 | toast + urql cache invalidate 作业批改列表 |
| 角色变更iam | iam Kafka | urql cache invalidate 权限/视口(强制刷新 me Query |
### 5.2 SSE 流式P5 AI 辅助出题)
```
GET /api/v1/ai/generate-questions (SSE)
GET /api/v1/ai/generate-questions (SSE) 或 GraphQL SubscriptionP6+ 评估)
data: {"delta": "题目"}\n\n
data: {"delta": "A. option1"}\n\n
data: {"done": true}\n\n
```
前端用 `AsyncIterable<T>` 消费Tiptap 逐字插入。
前端用 `AsyncIterable<T>` 消费(非 GraphQLTanStack Query 场景)Tiptap 逐字插入。
## 6. 横切关注点对齐清单
### 6.1 权限(前端等价)
### 6.1 权限(前端等价F7 命名 `<RESOURCE>_<ACTION>`
| 路由 | requiredPermission |
| ----------------------------- | ------------------------ |
@@ -308,9 +354,9 @@ data: {"done": true}\n\n
| `/teacher/lesson-prep` | `LESSON_PREP_VIEW` |
| `/teacher/knowledge-graph` | `CONTENT_READ` |
> 完整权限点常量集中在 `packages/contracts/src/permissions.ts`待建立,coord 负责 shared-tsai07 负责调用。L3 组件级视口用 `<RequirePermission perm="EXAMS_CREATE"><Button>新建考试</Button></RequirePermission>`。
> 完整权限点常量集中在 `packages/contracts/src/permissions.ts`coord 维护ai13 消费。L3 组件级视口用 `<RequirePermission perm="EXAMS_CREATE"><Button>新建考试</Button></RequirePermission>`。
### 6.2 错误码清单(前端 i18n 路由)
### 6.2 错误码清单(前端 i18n 路由,对齐 [matrix.md §6](../../../docs/architecture/issues/matrix.md)
| 前缀 | 来源服务 | i18n key 模式 |
| -------------- | ----------- | ------------------------ |
@@ -320,7 +366,7 @@ data: {"done": true}\n\n
| `CONTENT_` | content | `content.error.{{code}}` |
| `MSG_` | msg | `msg.error.{{code}}` |
| `AI_` | ai | `ai.error.{{code}}` |
| `BFF_TEACHER_` | teacher-bff | `bff.error.{{code}}` |
| `BFF_TEACHER_` | teacher-bff | `error.teacher_bff.{{code_snake}}` |
| `GW_` | api-gateway | `gateway.error.{{code}}` |
| `NETWORK_` | 前端网络层 | `network.error.{{code}}` |
@@ -333,7 +379,6 @@ interface Logger {
warn(msg: string, meta?: Record<string, unknown>): void;
error(msg: string, meta?: Record<string, unknown>): void;
}
// 实现:开发环境 console + 结构化;生产环境 → SentryP6
// 必含字段trace_id从响应头提取、user_id、scope、path
```
@@ -344,7 +389,7 @@ interface Logger {
| ----------------------------- | ---- | --------------------------------------------------- |
| `teacher_portal_lcp_seconds` | LCP | `next/web-vitals``POST /api/v1/admin/web-vitals` |
| `teacher_portal_cls` | CLS | 同上 |
| `teacher_portal_fid_seconds` | FID | 同上 |
| `teacher_portal_inp_seconds` | INP | 同上 |
| `teacher_portal_ttfb_seconds` | TTFB | 同上 |
P6 接入P2-P5 暂缓。
@@ -369,37 +414,38 @@ import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
Next.js 无长连接(除 SSE/WS无需特殊处理。SSE/WS 在 P5 由 push-gateway 管理,前端断线自动重连。
## 7. 共享组件库packages/ui-components/待建立ai07 维护
## 7. 共享组件库packages/ui-components/ai13 维护ARB-002 暴露
| 组件 | 用途 | 来源 |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------ |
| `AppShell` | 左侧栏 + 主内容区布局 | teacher-portal 现有 → 抽取共享 |
| `RequirePermission` | L3 组件级视口控制(无权限不渲染 children | 新建 |
| `ErrorBoundary` | React 渲染异常兜底fallback UI | 新建 |
| `Loading` | 骨架屏Skeleton | 新建 |
| `Empty` | 空态(插画 + 文案 + CTA | 新建 |
| `Modal` / `Dialog` | 全局 ModalModalRoot + Zustand ui-store | shadcn/ui |
| `Toast` | 全局 toast错误/成功/警告) | shadcn/ui sonner |
| `Button` / `Input` / `Select` / `Textarea` | 基础表单 | shadcn/ui |
| `DataTable` | 表格(排序/分页/筛选) | shadcn/ui + TanStack Table |
| `Chart` | 图表封装recharts | 新建 |
| `A11y` 工具集 | useA11yId / mergeA11yProps / describeInput / focus-trap / skip-link / visually-hidden / aria-status | 迁移指南 §7.7 |
| `Form` | react-hook-form + zodResolver 封装 | 新建 |
| `RichTextEditor` | Tiptap 封装(备课/出题/反馈) | 新建 |
| 组件 | 用途 | MF 暴露ARB-002 |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------ |
| `AppShell` | 左侧栏 + 主内容区布局 | `./AppShell` |
| `RequirePermission` | L3 组件级视口控制(无权限不渲染 children | `./RequirePermission` |
| `ErrorBoundary` | React 渲染异常兜底fallback UI | `./ErrorBoundary` |
| `Loading` | 骨架屏Skeleton | `./Loading` |
| `Empty` | 空态(插画 + 文案 + CTA | `./Empty` |
| `Modal` / `Dialog` | 全局 ModalModalRoot + Zustand ui-store | shadcn/ui |
| `Toast` | 全局 toast错误/成功/警告) | shadcn/ui sonner |
| `Button` / `Input` / `Select` / `Textarea` | 基础表单 | shadcn/ui |
| `DataTable` | 表格(排序/分页/筛选) | shadcn/ui + TanStack Table |
| `Chart` | 图表封装recharts | 新建 |
| `A11y` 工具集 | useA11yId / mergeA11yProps / describeInput / focus-trap / skip-link / visually-hidden / aria-status | 迁移指南 §7.7 |
| `Form` | react-hook-form + zodResolver 封装 | 新建 |
| `RichTextEditor` | Tiptap 封装(备课/出题/反馈) | 新建 |
## 8. 共享 Hookspackages/hooks/待建立ai07 维护
## 8. 共享 Hookspackages/hooks/ai13 维护ARB-002 暴露
| Hook | 职责 |
| --------------------- | --------------------------------------------------- |
| `useAuth()` | 会话状态user/token/refresh/login/logout |
| `usePermission()` | 权限查询hasPermission/hasAny/hasAll + dataScope |
| `useViewports(scope)` | 视口列表(按 scope 过滤) |
| `useApi()` | ApiClient 实例(注入 token + 401 处理) |
| `useA11yId()` | 唯一 ARIA ID 生成 |
| `useAriaLive()` | aria-live 区域管理 |
| `useToast()` | 全局 toastZustand ui-store |
| Hook | 职责 | MF 暴露ARB-002 |
| --------------------- | --------------------------------------------------- | ------------------ |
| `useGraphQLClient()` | urql client 单例Shell 注入Remote 复用) | ✅ `./useGraphQLClient` |
| `useAuth()` | 会话状态user/token/refresh/login/logout | ✅ `./useAuth` |
| `usePermission()` | 权限查询hasPermission/hasAny/hasAll + dataScope | ✅ `./usePermission` |
| `useViewports(scope)` | 视口列表GraphQL viewports Query按 scope 过滤) | — |
| `useApi()` | 非 GraphQL 场景(文件上传/SSEApiClient 实例 | — |
| `useA11yId()` | 唯一 ARIA ID 生成 | — |
| `useAriaLive()` | aria-live 区域管理 | — |
| `useToast()` | 全局 toastZustand ui-store | — |
## 9. 设计令牌三层packages/ui-tokens/待建立ai07 维护)
## 9. 设计令牌三层packages/ui-tokens/ai13 维护)
```
packages/ui-tokens/
@@ -429,52 +475,54 @@ packages/ui-tokens/
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 |
| ------ | ------------ | ------------------ | --------------------------------------------------- | ---------------------------- | ---- |
| 调用 | api-gateway | HTTP/REST | `/api/v1/*` 代理 | 全部业务请求 | P1+ |
| 调用 | push-gateway | WebSocket | `ws://push-gateway/ws` | 实时推送 | P5 |
| 调用 | ai | SSE | `GET /api/v1/ai/generate-questions` | AI 流式出题 | P5 |
| 调用 | api-gateway | HTTPGraphQL over HTTP | `/api/v1/teacher/*` 代理到 teacher-bff `POST /graphql` | 全部 GraphQL 业务请求 | P2+ |
| 调用 | api-gateway | HTTP | `POST /api/auth/login` | 教师登录(非 MF认证前提 | P2 |
| 调用 | push-gateway | WebSocket | `ws://push-gateway:8081/ws` | 实时推送 | P5 |
| 调用 | ai | SSE | `GET /api/v1/ai/generate-questions` | AI 流式出题(非 GraphQL | P5 |
| 被调用 | — | — | — | 前端不暴露接口给其他服务 | — |
| 消费 | teacher-bff | HTTP经 Gateway | `GET /teacher/viewports` 等 | 教师场景聚合 | P2+ |
| 消费 | iam | HTTP经 Gateway | `/iam/*` | 登录/权限/视口/用户管理 | P2+ |
| 消费 | core-edu | HTTP经 Gateway | `/classes/*` `/exams/*` `/homework/*` `/grades/*` | 教学核心 | P2+ |
| 消费 | content | HTTP经 Gateway | `/textbooks/*` `/knowledge-points/*` `/questions/*` | 内容资源 | P4+ |
| 消费 | data-ana | HTTP经 Gateway | `/analytics/*` | 学情分析 | P4+ |
| 消费 | msg | HTTP经 Gateway | `/notifications/*` | 通知中心 | P5+ |
| 消费 | teacher-bff | GraphQLARB-001 schema | `dashboard`/`viewports`/`me`/`classes`/`class` QueryP2+ P3+ Mutation | 教师场景聚合 | P2+ |
| 消费 | push-gateway | WebSocket | `NotificationRequested`/`HomeworkSubmitted`/角色变更 | 实时推送 | P5+ |
| 依赖 | coord 维护 | — | `packages/shared-proto` | TS 类型(仅 contracts 部分) | P1+ |
| 依赖 | coord 维护 | — | `packages/shared-ts`(待建) | ApiClient/Logger/通用工具 | P2+ |
| 依赖 | ai07 维护 | — | `packages/ui-tokens`待建) | 三层设计令牌 | P2+ |
| 依赖 | ai07 维护 | — | `packages/ui-components`待建) | shadcn + 共享组件 | P2+ |
| 依赖 | ai07 维护 | — | `packages/hooks`待建) | usePermission/useAuth | P2+ |
| 依赖 | coord 维护 | — | `packages/contracts`(待建) | Permissions 常量 + 类型 | P2+ |
| 依赖 | coord 维护 | — | `packages/shared-ts` + `contracts/graphql/teacher-bff.graphql` | ApiClient/Logger/GraphQL schema/通用工具 | P2+ |
| 依赖 | ai13 维护 | — | `packages/ui-tokens`已建,批次 0.15 | 三层设计令牌 | P2+ |
| 依赖 | ai13 维护 | — | `packages/ui-components`已建,批次 0.15 | shadcn + 共享组件ARB-002 暴露) | P2+ |
| 依赖 | ai13 维护 | — | `packages/hooks`已建,批次 0.15 | useGraphQLClient/usePermission/useAuthARB-002 暴露) | P2+ |
| 依赖 | coord 维护 | — | `packages/contracts` | Permissions 常量 + 类型 | P2+ |
> **proto 不直接消费**:前端不调用 gRPCBFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
> **前端不直接调 gRPC**teacher-bff 把 gRPC 聚合为 GraphQL 暴露给前端F9 裁决)。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量TS 文件,非 proto 生成)。
## 11. 风险与假设
### 11.1 假设
1. **假设 coord 建立 `packages/shared-ts`、`packages/contracts`**:包含 ApiClient、Logger、Permissions 常量、通用类型。若 coord 未建立ai07 自行在 `apps/teacher-portal/src/shared/` 内实现,后续提取到 packages
2. **假设 iam 提供 `GET /iam/effective-permissions`**:返回 `{ permissions, viewports, dataScope }`。当前已实现known-issues §2.3 iam
3. **假设 teacher-bff 提供 `GET /teacher/viewports`**:返回 L1 导航视口。当前已实现
4. **假设 core-edu classes 模块维持 `ActionState` 响应结构**:前端 API 请求层依赖此契约
5. **假设 Next.js 14+ Module Federation 2.0 稳定**`@module-federation/nextjs-mf` 在 Next.js App Router 下可用。若不稳定,降级为 4 端独立部署 + 各自 Shell重复实现 AppShell
1. **假设 coord 建立 `packages/shared-ts`、`packages/contracts`**:包含 GraphQL schema、Logger、Permissions 常量、通用类型。✅ packages 骨架ui-tokens/ui-components/hooks已于批次 0.15 建立ISSUE-039
2. **假设 teacher-bff 提供 `POST /graphql`ARB-001 schema 第一版)**:返回 dashboard/viewports/me/classes/class 5 个 Query。⏳ 待 ai03 + coord 仲裁
3. **假设 api-gateway 代理 `/api/v1/teacher/*` → teacher-bff:3003**:⏳ 待 ai01
4. **假设 Next.js 14+ Module Federation 2.0 稳定**`@module-federation/nextjs-mf` 在 Next.js App Router 下可用。若不稳定,降级为 4 端独立部署 + 各自 Shell重复实现 AppShell
5. **假设 iam 提供 refresh cookie 端点**P6 localStorage→httpOnly cookie 迁移前置。⏳ 待 ai06 P6
### 11.2 技术风险
| 风险 | 影响 | 缓解 |
| ------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| MF SSR 对齐复杂 | Remote 在 SSR 时需 Shell 提供上下文 | 优先 CSRSSR 仅用于首屏 dashboardMF 2.0 支持 SSR |
| 共享依赖版本漂移 | Remote 与 Shell 的 react/react-dom 版本不一致导致运行时错误 | MF `shared.singleton: true` + CI 检查版本对齐 |
| Token 刷新竞态 | 多请求同时 401 触发多次 refresh | ApiClient 全局单例 + refresh promise 复用 |
| 权限缓存陈旧 | 角色变更后前端 5min 内仍用旧权限 | iam 角色变更发 Kafka 事件 → msg 推送 WebSocket → 前端 invalidate |
| 设计令牌迁移破坏现有样式 | teacher-portal 现有硬编码令牌迁移到三层模型后样式漂移 | 灰度迁移:先建 ui-tokens 包teacher-portal 引入但不删除旧 globals.css验证后切换 |
| TanStack Query 缓存膨胀 | 长时间使用后缓存项过多 | `gcTime` 5min + `staleTime` 按数据类型分级 |
| 共享依赖版本漂移 | Remote 与 Shell 的 react/react-dom/urql/graphql 版本不一致导致运行时错误 | MF `shared.singleton: true`ARB-002+ CI 检查版本对齐 |
| Token 刷新竞态 | 多请求同时 401 触发多次 refresh | useAuth 全局单例 + refresh promise 复用03 §2.7 |
| 权限缓存陈旧 | 角色变更后前端仍用旧权限 | iam 角色变更发 Kafka → msg → push-gateway WebSocket → urql cache invalidate |
| GraphQL schema 演进阻塞 | ai03 schema 变更未同步 ai13 导致前端 query 失效 | SDL-first + schema 注册表 `packages/shared-ts/contracts/graphql/`ARB-001 |
| 设计令牌迁移破坏现有样式 | teacher-portal 现有硬编码令牌迁移到三层模型后样式漂移 | 灰度迁移:先建 ui-tokens 包teacher-portal 引入但不删除旧 globals.css验证后切换 |
| urql cache 膨胀 | 长时间使用后缓存项过多 | staleTime 分级 + P3 评估 graphcache normalized cache |
### 11.3 未决设计决策(需 coord 仲裁)
### 11.3 已决策设计点(原未决决策,现已对齐仲裁)
1. **packages 归属**`ui-tokens` / `ui-components` / `hooks` 是 ai07 维护还是 coord 维护建议ai07 维护前端专属coord 仅维护 `shared-ts` / `contracts`(跨语言/跨服务)。
2. **GraphQL vs REST**004 §11.3 提到 BFF GraphQL Yoga + DataLoader但当前 teacher-bff 实现为 REST。前端 API 请求层是否需要 GraphQL clienturql/apollo建议P2-P3 用 RESTP4 起若 BFF 切 GraphQL 再引入 urql。
3. **i18n key 命名**`iam.error.IAM_INVALID_CREDENTIALS` 还是 `error.iam.invalid_credentials`?建议:`error.{{service}}.{{code_snake_case}}`,与错误码前缀对齐。
4. **MF 暴露粒度**Shell 暴露整个 AppShell 还是暴露更细粒度的组件Sidebar、Header、Content建议暴露 AppShell 整体 + 各 Remote 自行决定内部布局。
| 决策点 | 结论 | 裁决依据 |
| ------------------- | ----------------------------------------------------------- | ------------------------------- |
| packages 归属 | ai13 维护 ui-tokens/ui-components/hookscoord 维护 shared-ts/contracts | 总裁 §2.18ISSUE-039 |
| GraphQL vs REST | **P2 起 all-in GraphQLurql无 REST 过渡** | F9 + ARB-001ISSUE-036/037 |
| i18n key 命名 | `error.{{service}}.{{code_snake_case}}`,与错误码前缀对齐 | F11 |
| MF 暴露粒度 | 暴露 AppShell 整体 + GraphQLProvider + hooks + UI 组件ARB-002 清单) | 总裁 §2.17 + ARB-002ISSUE-038 |
| GraphQL client 单例 | Shell 暴露 GraphQLProviderRemote 复用(方案 A | 总裁 §2.17ISSUE-038 |
| P2 Remote 数量 | 0NEXT_PUBLIC_MF_ENABLED=falseP3 student 首个接入 | ARB-002 §2.3 |
## 12. coord 交叉审查所需信息
@@ -482,68 +530,74 @@ packages/ui-tokens/
| 端 | dev 端口 | 生产端口 | 备注 |
| -------------- | -------- | -------- | ---------- |
| teacher-portal | 3000 | 3000 | Shell 宿主 |
| teacher-portal | 4000 | 4000 | Shell 宿主 |
> 与 [full-stack-runbook](../../../docs/standards/full-stack-runbook.md) 端口矩阵对齐。
> 与 [matrix.md](../../../docs/architecture/issues/matrix.md) §1 端口矩阵对齐teacher-portal :4000
### 12.2 依赖的共享包(需 coord 建立)
### 12.2 依赖的共享包
| 包 | 路径 | 维护方 | 内容 |
| --------------- | ------------------------- | ------------ | ------------------------------------------------- |
| `shared-ts` | `packages/shared-ts/` | coord | ApiClient、Logger、通用工具 |
| `shared-ts` | `packages/shared-ts/` | coord | ApiClient、Logger、GraphQL schema、通用工具 |
| `contracts` | `packages/contracts/` | coord | Permissions 常量、ActionState 类型、UserInfo 类型 |
| `ui-tokens` | `packages/ui-tokens/` | ai07建议 | 三层设计令牌 |
| `ui-components` | `packages/ui-components/` | ai07建议 | shadcn + ErrorBoundary + RequirePermission |
| `hooks` | `packages/hooks/` | ai07建议 | usePermissionuseAuth、useViewports |
| `ui-tokens` | `packages/ui-tokens/` | ai13 | 三层设计令牌(✅ 已建,批次 0.15 |
| `ui-components` | `packages/ui-components/` | ai13 | shadcn + ErrorBoundary + RequirePermission(✅ 已建ARB-002 暴露) |
| `hooks` | `packages/hooks/` | ai13 | useGraphQLClient/usePermission/useAuth(✅ 已建ARB-002 暴露) |
### 12.3 依赖的后端契约(需对应 AI 确认)
| 契约 | 提供方 | 当前状态 |
| ------------------------------------------------------------------ | ------------------ | ------------------- |
| `POST /iam/login``GET /iam/effective-permissions``GET /iam/me` | iam | ✅ 已实现 |
| `GET /teacher/viewports``GET /teacher/dashboard` | teacher-bff | ✅ 已实现 |
| `/classes/*` CRUD | core-edu | ✅ 已实现 |
| `/exams/*` `/homework/*` `/grades/*` | core-edu | ✅ 已实现P3 |
| `/textbooks/*` `/knowledge-points/*` `/questions/*` | content | ✅ 已实现P4 |
| `/analytics/*` | data-ana | ✅ 已实现P4 CDC |
| `/notifications/*` + WebSocket 推送 | msg + push-gateway | 📐 待 P5 |
| `GET /ai/generate-questions`SSE | ai | 📐 待 P5 |
| `POST /graphql` + dashboard/viewports/me/classes/class QueryARB-001 | teacher-bffai03| ⏳ 待 coord 仲裁 |
| `POST /api/auth/login` | api-gatewayai01→ iam | ⏳ 待 ai01 |
| classExams/classHomework/studentGrades Query + Mutation | teacher-bffai03 P3| ⏳ 待 ai03 P3 |
| knowledgeGraph/studentAnalytics Query | teacher-bffai03 P4| ⏳ 待 ai03 P4 |
| myNotifications/markAsRead Query + Mutation | teacher-bffai03 P5| ⏳ 待 ai03 P5 |
| `ws://push-gateway:8081/ws` 推送 | push-gatewayai02 P5| ⏳ 待 ai02 P5 |
| `GET /api/v1/ai/generate-questions`SSE | aiai12 P5 | ⏳ 待 ai12 P5 |
### 12.4 错误码前缀(前端 i18n 路由依赖)
### 12.4 错误码前缀(前端 i18n 路由依赖,对齐 [matrix.md §6](../../../docs/architecture/issues/matrix.md)
前端不产生错误码,仅消费。需各服务确认错误码前缀不重叠:
| 前缀 | 服务 | 状态 |
| ------------------------------ | ----------- | --------- |
| `IAM_` | iam | ✅ 已用 |
| `CORE_EDU_` | core-edu | ✅ 已用 |
| `CLASSES_` | core-edu | ✅ 已用 |
| `EXAMS_`/`HOMEWORK_`/`GRADES_` | core-edu | ⚠️ 待确认 |
| `CONTENT_` | content | ⚠️ 待确认 |
| `MSG_` | msg | ⚠️ 待确认 |
| `AI_` | ai | ⚠️ 待确认 |
| `BFF_TEACHER_` | teacher-bff | ⚠️ 待确认 |
| `BFF_TEACHER_` | teacher-bff | ✅ 已裁决G14 |
| `GW_` | api-gateway | ✅ 已用 |
| `NETWORK_` | 前端 | ai07 自有 |
| `NETWORK_` | 前端 | ai13 自有 |
### 12.5 不产生 Kafka 事件
前端不发布/消费 Kafka 事件。WebSocket 推送由 push-gateway 消费 Kafka 转发。
## 13. 实施路线ai07 自用)
## 13. 实施路线ai13 自用,详见 [workline.md](../../../docs/architecture/issues/worklines/teacher-portal_workline.md)
### P2 收尾teacher-portal 审计对齐
### P2MF Shell + GraphQL client + 基础页面ARB-001/002
1. `packages/ui-tokens/`(三层设计令牌)+ `packages/ui-components/`ErrorBoundary/RequirePermission/Loading/Empty+ `packages/hooks/`usePermission/useAuth
2. teacher-portal 引入 TanStack Query + Zustand + nuqs + react-hook-form
3. 抽取 `lib/api.ts` 统一 API 请求层
4. AppShell 改用 `usePermission()`,删除 `user.roles.join(", ")` 硬编码
5. globals.css / tailwind.config.js 迁移到 ui-tokens 三层令牌
6. 引入 next-intl + i18n key 路由
7. 引入 ESLint flat config 自定义规则no-hardcoded-fonts / design-tokens
1. packages 骨架(ui-tokens/ui-components/hooks批次 0.15 已完成
2. NextFederationPlugin 配置ARB-002 exposes + shared**无 remotes**
3. GraphQLProvider + urql client 单例Shell 暴露§2.17 方案 A
4. AppShellLayout + 侧边栏 + 路由守卫 + ErrorBoundary + 设计令牌三层)
5. 登录页(`POST /api/auth/login` → JWT 存 localStorageF12
6. 权限上下文usePermission + useViewports权限点 `<RESOURCE>_<ACTION>` F7
7. Dashboard 框架(`dashboard` GraphQL Query+ 班级列表(`classes`/`class` Query+ 学生列表 + 个人设置
8. 补 ErrorBoundary + /api/health route
9. 配置 next.config.js Module FederationShell 角色
9. 引入 ESLint flat config 自定义规则no-hardcoded-fonts / design-tokens
10. 补 Vitest 单测 + Playwright E2E覆盖率 ≥ 80%
### P3考试/作业/成绩 + 乐观更新 + 多 Tab 同步)
1. 考试/作业/成绩管理页面GraphQL Query + Mutation
2. 乐观更新urql mutation onMutate 回滚 + invalidate
3. 多 Tab 会话同步BroadcastChannel03 §2.5
### P5推送 + AI 接入)
1. teacher-portal 接入 WebSocketpush-gateway
@@ -553,10 +607,11 @@ packages/ui-tokens/
1. Web Vitals + OTel browser SDK 接入
2. A11y WCAG 2.2 AA 审计
3. 性能优化MF shared 单例验证、bundle 分析)
3. localStorage → httpOnly Cookie 迁移
4. 性能优化MF shared 单例验证、bundle 分析)
---
**AI Agent**: ai07 (teacher-portal shell)
**Branch**: docs/teacher-portal-stage1-stage2-design-ai07
**AI Agent**: ai13 (teacher-portal shell)
**Branch**: feat-review-teacher-portal-docs-vErc0L
**Coordinator**: coord-ai

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,109 @@
# admin-portal 问题记录
> 负责人ai16
> 关联:[coord.md](../coord.md)、[contracts/admin-portal_contract.md](../contracts/admin-portal_contract.md)
> 关联:[coord.md](../coord.md)、[contracts/admin-portal_contract.md](../contracts/admin-portal_contract.md)、[matrix.md](../matrix.md)、[ai-allocation.md §5 ai16](../../ai-allocation.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
---
## §0 已有仲裁核查ai16 复核)
> 任务要求:对 coord 已有仲裁进行核查。以下为 ai16 对照 admin-portal 实际职责逐条复核结论。
### 0.1 ARB-001teacher-bff GraphQL schema 第一版)— 核查结论:✅ 通过,但存在依赖缺口
- **核查点**ARB-001 §1.3 裁决"admin 命名空间 P2 不包含P6 admin-portal 阶段新增 admin 命名空间"。
- **核查结论**裁决方向正确admin-portal 复用 teacher-bff + admin schema 命名空间,与 ai-allocation §5 ai16 设计重点一致)。
- **发现的缺口**teacher-bff 当前 [02-architecture-design.md](../../../services/teacher-bff/docs/02-architecture-design.md) **未定义 admin 命名空间的 GraphQL schema**(全文仅 1 处 audit 提及,无 adminUsers/adminRoles/auditLogs/adminDashboard 等 Query。admin-portal P6 的全部业务查询依赖该 schema属前置依赖缺失。
- **建议**:请 coord 仲裁 admin 命名空间 schema 的归属与时间点——是否由 ai03teacher-bff在 P6 启动前补齐 `packages/shared-ts/contracts/graphql/teacher-bff.graphql` 的 admin 命名空间部分(参照本模块 [contract §2.4](../contracts/admin-portal_contract.md) 的 Query 清单)。
- **状态**:待 coord 仲裁(见新异议 ISSUE-005
### 0.2 ARB-002MF Shell 暴露清单)— 核查结论:✅ 通过,但 01/02 文档未跟进
- **核查点**ARB-002 §2.2 Shell 暴露清单含 `GraphQLProvider` / `useGraphQLClient` / `useAuth` / `usePermission` / `AppShell` / `ErrorBoundary` / `Loading` / `Empty` / `RequirePermission`**不含** `useApi` / `ApiClient`
- **核查结论**裁决正确。admin-portal 应通过 `useGraphQLClient()` 消费 teacher-bff GraphQL而非 REST ApiClient。
- **发现的问题**:本模块 [01-understanding.md](../../../apps/admin-portal/docs/01-understanding.md) 与 [02-architecture-design.md](../../../apps/admin-portal/docs/02-architecture-design.md) 仍基于 REST `useApi()` / `ApiClient` 编写,未跟进 ARB-002。属本模块文档与仲裁不同步见新异议 ISSUE-003
- **状态**:本模块文档待修订(见 ISSUE-003
### 0.3 未仲裁但影响 admin-portal 的关键项
ARB-001/ARB-002 均未明确仲裁以下三项,而它们直接影响 admin-portal 实现,建议 coord 补充裁决:
| 待裁决项 | 当前依据 | 影响 |
| -------- | -------- | ---- |
| admin-portal 端口 | matrix.md / ai-allocation.md = 4003 | 01/02 文档误用 3003与 teacher-bff 冲突) |
| admin-portal 是否消费 push-gateway WebSocket | 同类 portalparent-portal契约 = 消费 | 01/02 文档误声明"不消费推送" |
| 审计日志消费机制iam Kafka → teacher-bff → GraphQL auditLogs | matrix.md §4 列 admin-portal 为 edu.iam.audit.created 消费方 | 前端不直连 Kafka需经 teacher-bff 聚合matrix.md 表述不精确 |
---
## 问题列表
<!--
追加条目格式:
### ISSUE-001-ai1601/02 模块文档归属错误ai07 → ai16
### ISSUE-[编号]-[AI标识][标题]
- **提请方**ai16
- **日期**2026-07-10
- **类型**:编号冲突 / 文档归属
- **描述**[01-understanding.md](../../../apps/admin-portal/docs/01-understanding.md) 与 [02-architecture-design.md](../../../apps/admin-portal/docs/02-architecture-design.md) 头部均标注"AIai07TS/React · 管理场景域前端 remote",分支名 `docs/admin-portal-stage1-stage2-design-ai07`。但 [ai-allocation.md §5](../../ai-allocation.md) 第 54/97/118/159/278 行明确 admin-portal 归属 **ai16**ai07 实际负责 classes → core-edu 交接(见 [workline.md §4.7](../workline.md))。
- **建议方案**:将 01/02 文档头部 AI 标识与分支命名更正为 ai16ai07 在 admin-portal 的产出视为历史草稿,由 ai16 接管修订。
- **状态**:待 coord 仲裁
- **提请方**aiXX
- **日期**YYYY-MM-DD
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**[详细描述问题]
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
### ISSUE-002-ai1601/02 文档端口错误3003 → 4003且 3003 与 teacher-bff 冲突
(暂无问题)
- **提请方**ai16
- **日期**2026-07-10
- **类型**:契约不明确 / 编号冲突
- **描述**01 §1 与 02 §12.1 声明 admin-portal 端口 3003并称"与 [full-stack-runbook](../../../docs/standards/full-stack-runbook.md) 端口矩阵对齐"。但 full-stack-runbook §2.1 中 **3003 = teacher-bff**admin-portal 未列入该 runbook。coord 维护的 [matrix.md §1](../matrix.md) 与 ai-allocation.md 统一采用 4000 段teacher-portal :4000 / student-portal :4001 / parent-portal :4002 / admin-portal :4003。
- **建议方案**:确认 admin-portal 端口为 **4003**;同步更新 full-stack-runbook §2.1 补齐 4 个 portal 的 4000 段端口(消除 runbook 与 matrix.md 的端口双轨制)。
- **状态**:待 coord 仲裁
### ISSUE-003-ai1601/02 文档通信协议与 ARB-001/ARB-002 不一致REST → GraphQL
- **提请方**ai16
- **日期**2026-07-10
- **类型**:契约不明确 / 前置依赖缺失
- **描述**01 §3.1 / 02 §1、§4 全文基于 REST`/api/v1/iam/*` + `/api/v1/admin/*` + `useApi()` + `ApiClient`)。但 ARB-001 已裁决 admin-portal 复用 teacher-bff GraphQL **admin 命名空间**ARB-002 已裁决 Shell 暴露 `GraphQLProvider` + `useGraphQLClient`(不含 `useApi`。02 §11.3 仍将"GraphQL vs REST"列为未决与仲裁结论冲突。根因01/02 文档参照的 teacher-portal 02 文档(同样基于 REST、将 GraphQL 列为未决)早于 ARB-001/0022026-07-09未跟进仲裁。
- **建议方案**admin-portal 通信协议统一为 GraphQL`POST /api/admin/graphql` → teacher-bff admin 命名空间);删除 `useApi`/`ApiClient` 依赖,改用 `useGraphQLClient()`02 §11.3 移除已裁决项。本模块 [contract.md](../contracts/admin-portal_contract.md) 已按 GraphQL 编写,作为修订基准。
- **状态**:待 coord 仲裁
### ISSUE-004-ai1601/02 文档遗漏审计日志与学校设置ai-allocation §5 明确职责)
- **提请方**ai16
- **日期**2026-07-10
- **类型**:工作量超批 / 契约不明确
- **描述**[ai-allocation.md §5 ai16](../../ai-allocation.md) 第 282 行明确 admin-portal 设计重点含"用户管理 + 角色权限管理 + 学校设置 + 组织管理 + **审计日志消费**"。但 01 §2.1/§L1 导航/§L2 路由表均**无审计日志、无学校设置**(仅有 dashboard/users/roles/permissions/viewports/organization/monitoring 7 个视口)。本模块 [contract.md §1.2/§2.4](../contracts/admin-portal_contract.md) 已含 audit-logs / system 路由与 auditLogs Query与 01/02 不一致。
- **建议方案**admin-portal 视口补齐为 9 个dashboard / users / roles / permissions / viewports / organization / classes / teachers / students / audit-logs / system按 ai-allocation §5 + contract §1.2 对齐);审计日志经 teacher-bff GraphQL `auditLogs` Query 消费(聚合 iam `AuditEvent`)。
- **状态**:待 coord 仲裁
### ISSUE-005-ai16teacher-bff 缺 admin 命名空间 GraphQL schema前置依赖缺失
- **提请方**ai16
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**ARB-001 裁决 P6 新增 admin 命名空间,但 teacher-bff [02-architecture-design.md](../../../services/teacher-bff/docs/02-architecture-design.md) 未定义该 schema无 adminUsers / adminRoles / adminClasses / adminTeachers / adminStudents / auditLogs / adminDashboard 等 Query/Mutation。admin-portal P6 全部业务查询依赖此 schema且需 SDL-first 存放于 `packages/shared-ts/contracts/graphql/teacher-bff.graphql`ARB-001 §1.3)。
- **建议方案**:请 coord 仲裁——由 ai03 在 P6 启动前补齐 teacher-bff admin 命名空间 schema参照本模块 contract §2.4 Query 清单),作为 admin-portal P6 的前置就绪信号;并更新 [matrix.md §3](../matrix.md) teacher-bff 行的 schema 文件状态。
- **状态**:待 coord 仲裁
### ISSUE-006-ai1601/02 文档推送策略与同类 portal 契约不一致(轮询 → WebSocket
- **提请方**ai16
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**01 §3.3 / 02 §5 声明 admin-portal"不消费 WebSocket/SSE采用轮询"。但同类 portal 契约([parent-portal_contract.md §2.3](./parent-portal_contract.md))消费 push-gateway `GET /ws`,本模块 [contract.md §2.3](../contracts/admin-portal_contract.md) 亦声明消费 WebSocket 实时通知。matrix.md §5 列 push-gateway WS 消费方含全部 portal。管理端审计告警/异常登录等场景对实时性有合理需求。
- **建议方案**admin-portal 接入 push-gateway WebSocket与同类 portal 一致用于审计告警、异常登录、系统异常等实时通知保留轮询仅用于监控指标60s与统计5min这类天然适合轮询的低频数据。
- **状态**:待 coord 仲裁
### ISSUE-007-ai16matrix.md §4 将 admin-portal 列为 Kafka 直消费方,与前端层级矛盾
- **提请方**ai16
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**[matrix.md §4](../matrix.md) 第 111 行将 admin-portal 列为 `edu.iam.audit.created` 的消费方。但前端不直连 Kafka[contract.md §2.2](../contracts/admin-portal_contract.md) 已明确审计日志经 GraphQL 查询。实际链路应为iam → Kafka → **teacher-bff** 消费 → GraphQL `auditLogs` Query → admin-portal。
- **建议方案**matrix.md §4 该行消费方更正为 **teacher-bff**admin-portal 经 teacher-bff 间接消费),避免误导架构分层。
- **状态**:待 coord 仲裁
---
## §1 历史问题
(暂无已裁决问题)

View File

@@ -1,13 +1,130 @@
# ai 问题记录
> 负责人ai12
> 关联:[coord.md](../coord.md)、[contracts/ai_contract.md](../contracts/ai_contract.md)
> 关联:[coord.md](../coord.md)、[contracts/ai_contract.md](../contracts/ai_contract.md)、[matrix.md](../matrix.md)、[port-allocation.md](../../../../infra/port-allocation.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
---
## §0 已有仲裁核查ai12 复核2026-07-10
> 任务要求对已有的仲裁进行核查。coord.md 当前仅含 ARB-001 / ARB-002另在 [port-allocation.md](../../../../infra/port-allocation.md) §7、coord-final-decisions.md、president-final-rulings.md 中存在涉及 ai 的历史仲裁。逐项核查如下。
| 仲裁编号 / 来源 | 主题 | 涉及 ai | 核查结论 | 状态 |
| -------------------------- | ----------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| ARB-001coord.md §1 | teacher-bff GraphQL schema 第一版 | ❌ 否 | 与 ai 无关,不适用。 | — |
| ARB-002coord.md §2 | MF Shell 暴露清单 | ❌ 否 | 与 ai 无关,不适用。 | — |
| port-allocation.md §7 | 50058 让给 aipush-gateway 豁免 gRPC | ✅ 是 | **仲裁有效**port-allocation.md §3/§5 已登记 ai = HTTP 3008 / gRPC 50058。ai 的 01/02 文档已按 50058 设计,一致。**但 [matrix.md](../matrix.md) §2 gRPC 接口提供方矩阵、§8 就绪信号跟踪表仍写 50057未同步** → 提请 coord 同步(见 ISSUE-01 | ⚠️ 仲裁有效但未同步 |
| coord-final-decisions §1-§4 | iam P2 契约 / BFF 设计 / Gateway / content | ❌ 否 | 历史仲裁,与 ai 无直接约束。ai 仅消费 content/data-ana gRPC需待其就绪。 | — |
| president-final-rulings §2.3 | Temporal 用于 AI 编排 | ✅ 是 | **部分仲裁**004 §2.3 列出 Temporal。ai12 已在 02-architecture-design.md §8.3 建议简单 4 步工作流用 BackgroundTasks + Redis长运行评估 Temporal。需 coord 在 P6 决策点确认(见 ISSUE-06。 | ⚠️ 部分仲裁 |
| 01/02 文档引用"004 §7.2 已确认 topic edu.insight.ai.usage" | ai 用量事件 topic | ✅ 是 | **核查不通过**grep 004 全文未发现 `ai.usage` / `insight.ai` / `AIUsage` 任何字样004 §7.2 并未确认 ai 的 topic。该引用无法证实topic 命名实际处于三义未决状态(见 ISSUE-02。 | ❌ 引用失实 |
| 01/02 文档引用"004 §1.2 端口矩阵 HTTP+10050 规则" | ai gRPC 端口推导规则 | ✅ 是 | **核查不通过**004 全文无端口矩阵;`HTTP+10050` 公式不成立3008+10050=13058≠50058。端口唯一源是 port-allocation.md顺序分配ai=50058。ai 文档应改引 port-allocation.md。 | ❌ 引用失实 |
**核查总结**coord.md 现有 2 项仲裁ARB-001/002均与 ai 无关;涉及 ai 的真实仲裁在 port-allocation.md50058 已定)与 004 §2.3Temporal 部分仲裁。ai 文档对"004 §7.2/§1.2"的两处引用失实,需修正引用源。
---
## 问题列表
### ISSUE-01-ai12matrix.md 端口 50057 与 port-allocation.md 50058 不同步
- **提请方**ai12
- **日期**2026-07-10
- **类型**:编号冲突 / 文档不同步
- **描述**[matrix.md](../matrix.md) §2 gRPC 接口提供方矩阵写 "ai (ai12) | 50057"§8 就绪信号跟踪表写 "ai gRPC 50057"。但 [port-allocation.md](../../../../infra/port-allocation.md) §3/§5/§7 明确 ai = HTTP 3008 / gRPC 500582026-07-09 coord 仲裁"push-gateway 豁免 gRPC释放 5005750058 让给 ai")。两份 coord 文件矛盾matrix.md 过时。ai 01/02 文档与 workline.md §1/§4.12 已正确采用 50058。
- **建议方案**coord 将 matrix.md §2 ai 行 gRPC 端口 50057 → 50058§8 就绪信号"ai gRPC 50057"→ 50058。ai12 同步将 contracts/ai_contract.md 全部 50057 改 50058。
- **状态**:待 coord 仲裁(实为同步操作,仲裁已存在于 port-allocation.md §7
### ISSUE-02-ai12ai 用量事件 Kafka topic 命名三义未决
- **提请方**ai12
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**ai 用量计费事件 topic 在三份文档中命名不一致,且无权威源:
- 01-understanding.md §3.2 / 02-architecture-design.md §5.2`edu.insight.ai.usage`(自称"004 §7.2 已确认",但 004 中不存在)
- matrix.md §4 Kafka 事件发布方矩阵:`edu.ai.usage`,事件 `AIUsageEvent`
- contracts/ai_contract.md §1.4`edu.ai.usage.events`
events.proto 现状无 `AIUsageEvent` message004 无 topic 记录。三义导致 data-ana 消费方无法对齐。
- **建议方案**ai12 建议采用 `edu.ai.usage`(与 matrix.md 一致,最简短,符合 `edu.<domain>.<action>` 简洁约定)。请 coord 裁定并在 004 §7.2 补登 + events.proto 补 `AIUsageEvent` messageschema 见 02-architecture-design.md §3.3。ai12 据裁决同步 01/02 文档与 contract。
- **状态**:待 coord 仲裁
### ISSUE-03-ai12ai.proto 待补全(备课工作流 RPC + 字段扩展)
- **提请方**ai12
- **日期**2026-07-10
- **类型**:契约不明确 / 前置依赖缺失
- **描述**ai.proto 现状仅 4 RPCChat / StreamChat / GenerateQuestion / OptimizeExpression`GenerateQuestionRequest` 仅 prompt/subject/difficulty 三字段。P5 交付需要:
- 新增 RPC`GenerateLessonPlan``StreamGenerateQuestion`ai-allocation §5 "题目逐字生成"
- `GenerateQuestionRequest` 扩展字段grade / knowledge_point_ids / question_type / count
- `ChatRequest` 扩展可选字段user_id / session_id / data_scope
关于 RPC 总数存在分歧matrix.md §2 与 contract 写 6 RPC02-architecture-design.md §4.2 单方面扩到 8 RPC追加 GetLessonPlanStatus / ConfirmLessonPlan。需 coord 裁定 P5 目标 RPC 清单。
- **建议方案**ai12 建议 P5 目标 6 RPCChat / StreamChat / GenerateQuestion / StreamGenerateQuestion / OptimizeExpression / GenerateLessonPlan。备课工作流的"查询状态/确认入库"用 HTTP 端点(`GET /ai/v1/lesson/preparation/{id}` + `POST .../confirm`)实现,避免 RPC 膨胀;如 coord 认为查询/确认也需 gRPC则定为 8 RPC。请 coord 在 P5 启动前升级 ai.proto 到 v1 完整版。
- **状态**:待 coord 仲裁
### ISSUE-04-ai12events.proto 缺 AIUsageEvent message
- **提请方**ai12
- **日期**2026-07-10
- **类型**:契约不明确 / 前置依赖缺失
- **描述**004 §12.2 + §15.3 #6 仲裁 ai 用量事件豁免 Outbox派生数据但 events.proto 现状仅有 ClassEvent / ExamEvent / HomeworkEvent / GradeEvent**无 AIUsageEvent**。data-ana 消费方无 schema 可循。01/02 文档已提请A3matrix.md §4 已列事件名,但 proto 未落地。
- **建议方案**coord 在 events.proto 新增 `AIUsageEvent` message建议 schema 见 02-architecture-design.md §3.3(含 event_id / user_id / school_id / provider / model / operation / prompt_tokens / completion_tokens / total_tokens / latency_ms / success / degraded / metadata。与 ISSUE-02 一并裁决。
- **状态**:待 coord 仲裁
### ISSUE-05-ai12contracts/ai_contract.md 与设计文档多处矛盾ai12 自查自纠清单)
- **提请方**ai12
- **日期**2026-07-10
- **类型**:契约不明确 / 文档不同步
- **描述**:现有 contracts/ai_contract.mdcoord 模板ai12 接管前未细化)与 01/02 设计文档存在 5 处矛盾:
1. §1.1 gRPC 端口 50057应为 50058见 ISSUE-01
2. §1.2 "无对外 HTTP 端点,仅 gRPC"——错误。api-gateway 代理 `/api/v1/ai/*` → ai HTTPmain.py 已实现 /ai/* 端点 + SSE 流式HTTP 保留作 Gateway 直连降级
3. §1.4 topic `edu.ai.usage.events`(三义,见 ISSUE-02
4. §1.5 错误码示例 `AI_PROVIDER_UNAVAILABLE` / `AI_TOKEN_LIMIT_EXCEEDED` / `AI_CONTENT_FILTERED` 与 02-architecture-design.md §6.2 清单(`AI_LLM_UNAVAILABLE` / `AI_QUOTA_EXCEEDED` / `AI_CONTENT_MODERATION_REJECTED`)命名不一致
5. §2.2 列出 ai 消费 2 个 content Kafka 事件,与 01/02 §5.1 "ai 不消费任何事件(无状态)"矛盾
- **建议方案**ai12 在本次工作中直接重写 contracts/ai_contract.md 对齐设计文档(端口 50058 / 补 HTTP 端点 / topic 待 ISSUE-02 裁决后填 / 错误码对齐 02 §6.2 / 删除消费事件或标注 P6+ 评估)。仅 RPC 数与 topic 命名待 coord 裁决后最终定稿。
- **状态**ai12 自纠中(依赖 ISSUE-01/02/03 裁决的字段待补)
### ISSUE-06-ai12备课工作流是否引入 TemporalP6 决策点)
- **提请方**ai12
- **日期**2026-07-10
- **类型**:工作量超批 / 前置依赖缺失
- **描述**004 §2.3 列出 Temporal 用于"AI 编排"属部分仲裁。ai12 在 02-architecture-design.md §8.3 建议P5 用 FastAPI BackgroundTasks + Redis24h TTL实现 4 步备课工作流P6 评估迁移 Temporal支持长运行跨天审核 + 复杂状态机)。需 coord 在 P6 决策点确认是否引入,避免 P5 实现被推翻重做。
- **建议方案**P5 采用 BackgroundTasks + Redis02-architecture-design.md §2.4 状态机已设计P6 由 coord 评估 Temporal 引入时机。请 coord 在 roadmap 标注 P6 决策点。
- **状态**:待 coord 仲裁P6 决策点)
### ISSUE-07-ai12iam GetEffectiveDataScope gRPC RPC 待 P4 补全
- **提请方**ai12
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**ai 多租户用量配额校验需调用 `IamService.GetEffectiveDataScope` 查询用户 DataScope。01/02 文档已提请A4coord 已仲裁 P4 补全§15.3 #5),但 iam.proto 现状未见此 RPC。ai P5 实施时依赖,若 P4 未补全将阻塞配额校验功能。
- **建议方案**:请 coord 确认 iam (ai06) 在 P4 已补全 `GetEffectiveDataScope` RPCai12 在 P5 实施时调用Redis 缓存 5min。若 P4 未补全ai 降级为"仅按 user_id 配额,不按 school_id"。
- **状态**:待 coord 确认 P4 补全情况
### ISSUE-08-ai12proto package 命名不符合 project_rules §5
- **提请方**ai12
- **日期**2026-07-10
- **类型**:契约不明确 / 架构约束
- **描述**project_rules §5 规定 proto 包名规范 `edu.<domain>.v1`(如 `edu.iam.v1``edu.core_edu.v1`)。但 ai.proto 现状 package 为 `next_edu_cloud.ai.v1`events.proto 为 `next_edu_cloud.events.v1`均不符合规范。01/02 文档未指出此偏离。此为全局 proto 命名问题(涉及全部 proto 文件),非 ai 独有,但 ai12 在审查中发现需提请。
- **建议方案**:请 coord 裁定是否统一迁移 proto package 至 `edu.<domain>.v1`(全局变更,需 buf breaking 评估或保留现状作为历史包袱。ai12 在 ai.proto 补全 RPC 时遵循最终裁定。
- **状态**:待 coord 仲裁(全局 proto 命名)
### ISSUE-09-ai12响应信封偏离 ActionState 强制整改P0
- **提请方**ai12
- **日期**2026-07-10
- **类型**:架构约束
- **描述**004 §11.5 强制响应信封为 ActionState`{success, data, error:{code,message,details,traceId}}`。ai 当前 main.py 违反:返回 `{success:true, data:..., degraded:false}` 顶层 degraded 字段(见 main.py:89/201-210/233-244。01/02 文档已提请A5contracts 未记录。P5 实施必须整改。
- **建议方案**P5 实施时所有 HTTP 端点 + gRPC RPC 返回值改为 ActionStatedegraded 作为 `error.details.degraded` 子字段。请 coord 在 known-issues §2.9 ai 分区记录此约束01 文档已提请,需 coord 落地 known-issues
- **状态**:待 coord 在 known-issues 记录约束(整改由 ai12 P5 执行)
---
<!--
追加条目格式:
@@ -20,5 +137,3 @@
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
(暂无问题)

View File

@@ -1,24 +1,3 @@
# api-gateway 问题记录
> 负责人ai01
> 关联:[coord.md](../coord.md)、[contracts/api-gateway_contract.md](../contracts/api-gateway_contract.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
---
## 问题列表
<!--
追加条目格式:
### ISSUE-[编号]-[AI标识][标题]
- **提请方**aiXX
- **日期**YYYY-MM-DD
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**[详细描述问题]
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
(暂无问题)
>

View File

@@ -1,24 +1,187 @@
# content 问题记录
> 负责人ai09
> 关联:[coord.md](../coord.md)、[contracts/content_contract.md](../contracts/content_contract.md)
> 关联:[coord.md](../coord.md)、[contracts/content_contract.md](../contracts/content_contract.md)、[../../services/content/docs/01-understanding.md](../../../services/content/docs/01-understanding.md)、[../../services/content/docs/02-architecture-design.md](../../../services/content/docs/02-architecture-design.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
---
## 问题列表
## §0 已有仲裁核查2026-07-10 复核)
<!--
追加条目格式:
> 复核依据:[coord-final-decisions.md §3.3](../../coord-final-decisions.md) N1-N5、[01-understanding.md §A](../../../services/content/docs/01-understanding.md) ai09 复核记录
### ISSUE-[编号]-[AI标识][标题]
### 0.1 coord-final-decisions.md N1-N5 核查
- **提请方**aiXX
- **日期**YYYY-MM-DD
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**[详细描述问题]
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
| 编号 | 仲裁结论 | 02-architecture-design.md 落实位置 | 核查结果 |
| ---- | ----------------------------------------------------------------------- | ------------------------------------------------ | -------- |
| N1 | P4 首次实现即启用 gRPC server 50054 | §1.2 入口 HTTP 3005 / gRPC 50054§4.2 gRPC API | ✅ 已落实 |
| N2 | 首次实现即检查 DB/Neo4j/Kafka | §6.6 /readyz 多依赖检查 | ✅ 已落实 |
| N3 | P4 即补全 QuestionService proto不等到 P5 | §4.2.4 QuestionService 6 RPC | ✅ 已落实 |
| N4 | 首次实现即对齐 ActionState | §4.3 错误响应结构 success/error 信封 | ✅ 已落实 |
| N5 | P4 首次实现即补全 ChapterService | §4.2.2 ChapterService 3 RPC | ✅ 已落实 |
(暂无问题)
### 0.2 01-understanding.md §A 已裁决项核查
| 原编号 | 仲裁结论 | 02-architecture-design.md 落实位置 | 核查结果 |
| ------ | --------------------------------------------------------------------- | ----------------------------------------- | -------- |
| C2 | P4 必须引入 Outbox004 §12.2 强制条款) | §3.1.5 content_outbox_events 表 + §5.4 Outbox Publisher | ✅ 已落实 |
| C4 | P4 必须实现 gRPC controller | §4.2 gRPC API4 个 Service | ✅ 已落实 |
| C10 | proto 包名保持 `next_edu_cloud.content.v1` | —(保持现状) | ✅ 已落实 |
### 0.3 核查结论
N1-N5 与 C2/C4/C10 共 8 项已有仲裁**全部在 02-architecture-design.md 中正确落实**,无遗漏、无偏离。
---
## §1 新提请异议2026-07-10 ai09 复审)
### ISSUE-001-ai09REST 端点设计文档与现有实现不一致
- **提请方**ai09
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**02-architecture-design.md §4.1 列出的 REST API 与现有源码实现存在三处偏差:
1. **knowledge-points 列表**:设计文档为 `GET /knowledge-points?chapterId=`query 参数),源码 [knowledge-points.controller.ts](../../../services/content/src/knowledge-points/knowledge-points.controller.ts) 实现为 `GET /knowledge-points/chapter/:chapterId`path 参数)
2. **knowledge-points 删除前置**:设计文档列 `DELETE /knowledge-points/:id/prerequisites/:prereqId`,源码未实现该端点
3. **chapters 列表**:设计文档为 `GET /chapters?textbookId=`,源码实现为 `GET /chapters/textbook/:textbookId`
- **建议方案**以设计文档为目标态query 参数 + 补 DELETE prerequisite 端点),在 P4 重构时统一对齐。但需 coord 确认是否允许 API 路径变更(影响 teacher-bff 消费方)。
- **状态**:待 coord 仲裁
### ISSUE-002-ai09content 发布事件 topic 命名策略与契约文档不一致
- **提请方**ai09
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**:事件 topic 命名存在两种策略冲突:
- **02-architecture-design.md §5.1 + §5.3 TOPIC_MAP**:每个事件类型独立 topic`edu.content.textbook.created` / `edu.content.question.published` 等共 12 个 topic
- **contracts/content_contract.md §1.4 + matrix.md §4**:按聚合根聚合 topic`edu.content.knowledge_point.events` / `edu.content.question.events` 共 2 个 topic事件类型用 `action` 字段区分)
- **events.proto**:未定义 KnowledgePointEvent / QuestionEvent message仅 ClassEvent/ExamEvent/HomeworkEvent/GradeEvent
- **建议方案**:采用**聚合 topic + action 字段**策略与契约文档、matrix.md、events.proto ClassEvent 模式一致),原因:
1. 与 core-edu 既有模式edu.exam.events / edu.homework.events 等)一致
2. 减少 topic 数量12 → 4降低 Kafka 集群元数据压力
3. 消费方按 action 字段过滤,订阅灵活性更高
4. 需补 `KnowledgePointEvent` / `QuestionEvent` / `TextbookEvent` / `ChapterEvent` proto message
- **状态**:待 coord 仲裁
### ISSUE-003-ai09Textbook/Chapter 事件在契约文档遗漏
- **提请方**ai09
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**02-architecture-design.md §5.1 列出 4 类 textbook 事件 + 1 类 chapter 事件,但 contracts/content_contract.md §1.4 仅列出 knowledge_point 与 question 两类事件Textbook/Chapter 事件未登记。matrix.md §4 也仅列 kp + question。导致下游data-ana无法感知教材/章节变更。
- **建议方案**:在 contract.md 与 matrix.md 补登记 `edu.content.textbook.events`action: created/updated/published/archived`edu.content.chapter.events`action: created/updated/deleted。若 coord 认为教材/章节无需对外发事件,则在 design doc §5.1 删除相关事件。
- **状态**:待 coord 仲裁
### ISSUE-004-ai09gRPC RPC 数量三方文档不一致
- **提请方**ai09
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**content gRPC RPC 数量在三处文档不一致:
| Service | 02-architecture-design.md §4.2 | contracts/content_contract.md §1.1 | matrix.md §2 |
| --------------------- | ------------------------------ | ---------------------------------- | ------------ |
| TextbookService | 5含 Update/Delete 新增) | 3无 Update/Delete | 18总数 |
| ChapterService | 3Create/List/Get | 4含 Update无 Delete | — |
| KnowledgeGraphService | 4 | 4 | — |
| QuestionService | 6无 Publish/Search | 7含 Publish/Search | — |
| **合计** | **18** | **18** | **18** |
- design doc 缺 QuestionService.PublishQuestion / SearchQuestionscontract 有)
- contract 缺 TextbookService.Update/Deletedesign doc 有)
- design doc ChapterService 缺 Updatecontract 有contract ChapterService 缺 Deletedesign doc 也缺)
- **建议方案**:以 contract.md 为契约唯一源(已对齐 matrix.md 18 RPC 总数),反向修正 design doc
1. TextbookService 补 Update/Delete与 contract 对齐)
2. ChapterService 补 Update + Deletedesign doc + contract 都缺 Delete需补
3. QuestionService 补 PublishQuestion + SearchQuestions与 contract 对齐)
4. 最终 RPC 总数TextbookService 5 + ChapterService 5 + KnowledgeGraphService 4 + QuestionService 7 = **21 RPC**(需同步更新 matrix.md §2 的 18 → 21
- **状态**:待 coord 仲裁
### ISSUE-005-ai09core-edu → content 失效事件 topic 无定义
- **提请方**ai09
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**02-architecture-design.md §5.2 列出 content 消费 `edu.teaching.content.invalidated`(待 ai03 确认 topic
1. events.proto 无 ContentInvalidatedEvent message
2. matrix.md §4 未登记该 topic
3. core-edu 设计文档ai08未明确发布该事件
4. 01-understanding.md §5 也标注"具体 topic 待 core-edu ai03 设计确认"——此处 ai03 疑为笔误core-edu 实际由 ai08 负责
- **建议方案**content 不主动消费 core-edu 失效事件content 是上游内容提供方core-edu 是消费方),删除 §5.2 中该条目;若确有联动需求,由 core-edu 主动调用 content gRPC UpdateQuestion 状态变更,而非事件驱动。
- **状态**:待 coord 仲裁
### ISSUE-006-ai09文档结尾"直接 push main"与项目规则冲突
- **提请方**ai09
- **日期**2026-07-10
- **类型**:其他
- **描述**01-understanding.md 末尾与 02-architecture-design.md 末尾均标注 `Branch: 单仓库并行模式(直接 push main`,但 [project_rules §8 Git 工作流](../../../../.trae/rules/project_rules.md) 明确规定:
- §8分支开发AI 不得自行切换/创建/合并分支
- §14.3AI 禁止 `git merge``git push origin main`
- 当前 worktree 分支为 `feat-review-content-module-docs-WAIyMA`
- **建议方案**:删除两份文档末尾"单仓库并行模式(直接 push main"字样,改为 `Branch: feat-review-content-module-docs-WAIyMA分支开发提交后通知人类合并`
- **状态**:待 coord 仲裁
### ISSUE-007-ai09questions 表 created_by 字段迁移风险
- **提请方**ai09
- **日期**2026-07-10
- **类型**:其他
- **描述**02-architecture-design.md §3.1.4 questions 表新增 `created_by varchar(32) NOT NULL`,但现有 [questions.schema.ts](../../../services/content/src/questions/questions.schema.ts) 无该字段,且现有数据无 created_by 值。schema 迁移时 NOT NULL 约束会导致历史数据迁移失败。
- **建议方案**:迁移期间先用 `created_by varchar(32) NULL`,数据回填后再加 NOT NULL 约束;或为新数据强制要求 created_by应用层校验历史数据用 `'system'` 默认值回填。
- **状态**:待 coord 仲裁
### ISSUE-008-ai09设计文档缺缓存策略与 API 版本化策略
- **提请方**ai09
- **日期**2026-07-10
- **类型**:其他
- **描述**02-architecture-design.md 未涉及两个长远架构必备项:
1. **缓存策略**[env.ts](../../../services/content/src/config/env.ts) 已预留 `REDIS_URL`,但 design doc 未设计缓存层(教材树/知识点树是典型读多写少场景,应缓存)
2. **API 版本化**REST 端点无 `/v1/` 前缀matrix.md §5 显示 api-gateway 路由为 `/api/v1/teacher/*`,但 content 自身端点 `/textbooks` 无版本号),未来破坏性变更无版本隔离机制
- **建议方案**
1. P4 在 design doc §6 补"缓存策略"小节:教材树/章节树 Redis 缓存 + 失效策略Outbox 事件触发缓存失效)
2. P4 在 design doc §4 补"API 版本化"说明REST 端点统一加 `/v1/` 前缀gRPC 用 proto package version
- **状态**:待 coord 仲裁
### ISSUE-009-ai09knowledge-points schema 实际缺 difficulty/metadata 字段
- **提请方**ai09
- **日期**2026-07-10
- **类型**:其他
- **描述**02-architecture-design.md §3.1.3 knowledge_points 表列出 `difficulty tinyint NOT NULL DEFAULT 3``metadata json NULL`,但实际 [textbooks.schema.ts](../../../services/content/src/textbooks/textbooks.schema.ts) 中 `knowledgePoints` 表仅含 id/chapterId/title/description 四个字段,无 difficulty 与 metadata。01-understanding.md C7 提到"时间戳缺失"但未提到 difficulty/metadata 缺失。
- **建议方案**P4 schema 迁移时一并补齐 difficulty + metadata + created_at + updated_at与 design doc §3.1.3 对齐)。
- **状态**:待 coord 仲裁
### ISSUE-010-ai09Neo4j Sync Worker 与 ES Sync Worker 在 P4 阶段不必要
- **提请方**ai09
- **日期**2026-07-10
- **类型**:工作量超批
- **描述**02-architecture-design.md §1.1 分层图将 Neo4j Sync Worker 与 ES Sync Worker 并列展示,但:
1. ES 在 P5 才引入ES Sync Worker 在 P4 不必要
2. 当前 [knowledge-points.service.ts](../../../services/content/src/knowledge-points/knowledge-points.service.ts) 的 `safeCreateNode` 是同步双写(业务事务内写 Neo4j与 design doc §0.1 第 3 条"禁止业务事务内同步双写"原则冲突
3. P4 应改为 Outbox 事件驱动异步同步 Neo4j但 design doc §1.1 图中 Neo4j Sync Worker 的输入源同时画了"Kafka Consumer"与"CONSUMER",链路不清晰
- **建议方案**
1. §1.1 图中明确标注 ES Sync Worker 为 P5 组件(虚线或灰显)
2. §1.1 图中 Neo4j Sync Worker 的输入仅来自 content 自身 Outbox 事件(不消费 core-edu 事件)
3. P4 任务 T6 明确"重构 knowledge-points.service.ts移除 safeCreateNode 同步写,改为发 Outbox 事件"
- **状态**:待 coord 仲裁
---
## §2 待 coord 仲裁项汇总
| # | 标题 | 阻塞性 | 状态 |
| ---- | -------------------------------------------- | ----------------------- | ------------ |
| 001 | REST 端点设计与实现不一致 | 🟡 P4 重构时对齐 | 待 coord 仲裁 |
| 002 | 事件 topic 命名策略冲突(独立 vs 聚合) | 🔴 阻塞 Outbox 实现 | 待 coord 仲裁 |
| 003 | Textbook/Chapter 事件在契约文档遗漏 | 🟡 契约完整性 | 待 coord 仲裁 |
| 004 | gRPC RPC 数量三方文档不一致 | 🔴 阻塞 proto 修改 | 待 coord 仲裁 |
| 005 | core-edu → content 失效事件 topic 无定义 | 🟢 建议删除 | 待 coord 仲裁 |
| 006 | "直接 push main"与项目规则冲突 | 🟡 文档修正 | 待 coord 仲裁 |
| 007 | questions.created_by 迁移风险 | 🟡 schema 迁移 | 待 coord 仲裁 |
| 008 | 缓存策略与 API 版本化策略缺失 | 🟢 长远架构 | 待 coord 仲裁 |
| 009 | knowledge-points schema 实际缺字段 | 🟡 P4 schema 迁移 | 待 coord 仲裁 |
| 010 | Sync Worker 链路与 P4 阶段不必要 | 🟡 设计澄清 | 待 coord 仲裁 |

View File

@@ -1,11 +1,34 @@
# core-edu 问题记录
> 负责人ai08
> 关联:[coord.md](../coord.md)、[contracts/core-edu_contract.md](../contracts/core-edu_contract.md)
> 关联:[coord.md](../coord.md)、[contracts/core-edu_contract.md](../contracts/core-edu_contract.md)、[coord-cross-review.md](../../coord-cross-review.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
---
## §0 已有仲裁核查记录ai08 接管后核查)
> 核查日期2026-07-10
> 核查范围01-understanding.md / 02-architecture-design.md 引用的 coord 仲裁结论
> 核查方法:对照 [coord-cross-review.md](../../coord-cross-review.md) 原文 + 实际 proto 文件 + 实际源码
### 0.1 已核查通过的仲裁
| 仲裁编号 | 位置 | 核查结论 |
| -------- | ---- | -------- |
| coord-cross-review §3.1 | topic 命名统一为 `edu.teaching.<aggregate>.<action>` | ✅ 仲裁真实存在结论准确core-edu 01/02 文档引用正确 |
| coord-cross-review §5.5 | 错误码前缀 `CORE_EDU_*` 统一(不再细分 `EXAMS_`/`HOMEWORK_`/`GRADES_` | ✅ 仲裁真实存在,结论准确 |
| coord-cross-review §5.7 | ActionState 信封结构统一 | ✅ 仲裁真实存在core-edu GlobalErrorFilter 已对齐 |
| coord-cross-review §2.1 | proto 包名 `next_edu_cloud.core_edu.v1` | ✅ 仲裁真实存在core_edu.proto L3 符合 |
| coord-cross-review §6 整改 #14 | core_edu.proto 补 AttendanceService | ⚠️ 仲裁真实存在,但状态与实际不符(见 ISSUE-001 |
| coord-cross-review §6 整改 #16 | buf.gen.yaml 补 gRPC 插件 | ⚠️ 待 ai08 核实 buf.gen.yaml 实际状态 |
### 0.2 核查发现的仲裁状态不一致(升级为 ISSUE
详见下方 ISSUE-001 ~ ISSUE-003。
---
## 问题列表
<!--
@@ -21,4 +44,208 @@
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
(暂无问题)
### ISSUE-001-ai08core_edu.proto 实际状态与 coord 仲裁声称不一致P0
- **提请方**ai08
- **日期**2026-07-10
- **类型**:契约不明确(仲裁状态与代码实际不符)
- **描述**
[coord-cross-review.md](../../coord-cross-review.md) L312 声称:
> `core_edu.proto 补全 | packages/shared-proto/proto/core_edu.proto | ✅ 5 service+ClassService +AttendanceService`
但 ai08 核查实际文件 `packages/shared-proto/proto/core_edu.proto`**实际只有 3 个 service**
```
service ExamService { ... } // 5 RPC
service HomeworkService { ... } // 4 RPC
service GradeService { ... } // 5 RPC
```
**缺失**
- `ClassService`4 RPCGetClass / GetClassesByTeacher / BatchGetClasses / ListStudentsByClass
- `AttendanceService`4 RPCRecordAttendance / GetAttendance / ListAttendanceByStudent / ListAttendanceByClass
同时 Exam/Homework/Grade message **缺 P3 新增字段**
- Exam 缺 `subject_id`、`school_id`、`status_changed_at`、`status_changed_by`、`archived_at`
- Homework 缺 `subject_id`、`grace_period`、`school_id`
- Grade 缺 `total_score`、`school_id`、`idempotency_key`
- SubmitHomeworkRequest 缺 `answers` 字段02 文档 §4.2 要求含完整 answers
缺失 P3 新增 RPC`PublishExam` / `SubmitExam` / `GradeExam` / `GradeHomework` / `UpdateGrade`。
- **影响**
1. 01-understanding.md L48 仅声称缺 AttendanceService遗漏了 ClassService 也缺失
2. matrix.md §2 声称 core-edu 22 RPC但实际 proto 仅 14 RPC5+4+5
3. 下游 teacher-bff / student-bff / parent-bff 按 22 RPC 设计 mock实际无 proto 定义可生成代码
- **建议方案**
coord 确认 `core_edu.proto` 补全工作的实际负责人coord 自行补全,还是交由 ai08 在 P3 补全)。
- 若 coord 已补全但未提交:请 coord 提交最新 proto
- 若待 ai08 P3 补全coord-cross-review.md L312 的 "✅ 已补全" 状态需更正为 "⏳ 待 ai08 P3 补全"01-understanding.md L48 需补"ClassService 也缺失"
- **状态**:待 coord 仲裁
---
### ISSUE-002-ai08events.proto 未同步 coord topic 命名仲裁P0
- **提请方**ai08
- **日期**2026-07-10
- **类型**:契约不明确(仲裁未落实到 proto
- **描述**
[coord-cross-review.md](../../coord-cross-review.md) §3.1 仲裁"统一为 `edu.teaching.<aggregate>.<action>` 风格",但 `packages/shared-proto/proto/events.proto` 实际状态:
1. **文件头注释L9-13仍是旧 topic 命名**
```
// edu.exam.events <- exam.created / exam.updated / exam.deleted
// edu.homework.events <- homework.assigned / homework.submitted / homework.graded
// edu.grade.events <- grade.recorded / grade.updated
// edu.class.events <- class.transferred
```
未同步 `edu.teaching.exam.created` 等仲裁后命名。
2. **缺 `AttendanceEvent` message**events.proto 仅定义 ClassEvent / ExamEvent / HomeworkEvent / GradeEvent无 AttendanceEvent。02 文档 §5.1 要求发布 `edu.teaching.attendance.recorded` 事件,但 proto 无对应 message。
3. **缺 `schema_version` 字段**:所有 Event messageClassEvent/ExamEvent/HomeworkEvent/GradeEvent均无 `schema_version` 字段。coord §3.1 仲裁 + known-issues §1.3 + 01-understanding.md L67 + 02-architecture-design.md §5.1 均要求事件 payload 含 `schema_version`,但 proto 未定义。
4. **matrix.md §4 与 coord §3.1 仲裁不一致**matrix.md §4 Kafka 事件发布方矩阵仍列 `edu.exam.events` / `edu.homework.events` / `edu.grade.events` / `edu.class.events`,未同步 coord §3.1 仲裁后的 `edu.teaching.*` 命名。
- **影响**
- core-edu 修改 TOPIC_MAP 后,发布到 `edu.teaching.*` topic但 events.proto 注释和 matrix.md 仍记录旧 topic下游 AI 文档被误导
- 缺 AttendanceEvent message 导致 AttendanceService 无事件契约可发布
- 缺 schema_version 字段导致消费端无法按版本处理
- **建议方案**
1. coord 同步更新 events.proto
- 文件头注释改为 `edu.teaching.*` 命名
- 新增 `AttendanceEvent` message
- 所有 Event message 新增 `string schema_version = N;` 字段
2. coord 同步更新 matrix.md §4 为 `edu.teaching.*` 命名
3. ai08 在 core-edu `outbox.publisher.ts` TOPIC_MAP 按 `edu.teaching.*` 命名实现
- **状态**:待 coord 仲裁
---
### ISSUE-003-ai08考试/作业状态命名跨模块不一致P1
- **提请方**ai08
- **日期**2026-07-10
- **类型**:契约不明确(跨模块命名冲突)
- **描述**
02-architecture-design.md 定义的状态命名与 [coord.md](../coord.md) §1.2 GraphQL schema 枚举不一致:
| 维度 | 02-architecture-design.mdcore-edu | coord.md §1.2 GraphQL schemateacher-bff | 不一致点 |
| ---- | ------------------------------------- | ------------------------------------------- | -------- |
| ExamStatus | `draft / published / in_progress / grading / graded / archived / cancelled` | `DRAFT / PUBLISHED / IN_PROGRESS / GRADING / SCORED / ARCHIVED` | core-edu 用 `graded`coord GraphQL 用 `SCORED`core-edu 多 `cancelled` |
| SubmissionStatusexam | `in_progress / submitted / graded` | `NOT_SUBMITTED / SUBMITTED / GRADED` | core-edu 用 `in_progress`coord 用 `NOT_SUBMITTED` |
| SubmissionStatushomework | `draft / submitted / graded` | `NOT_SUBMITTED / SUBMITTED / GRADED` | core-edu exam 用 `in_progress`homework 用 `draft`,自身也不一致 |
02 文档 §3.1.1 exam_submissions.status 注释 `in_progress / submitted / graded`§3.1.2 homework_submissions.status 注释 `draft / submitted / graded`**core-edu 内部 exam 与 homework 的初始状态命名也不一致**`in_progress` vs `draft`)。
- **影响**
- teacher-bff GraphQL schema 枚举值与 core-edu DB status 字段值无法直接映射BFF 需要转换层
- 前端展示需处理两套命名
- coord 仲裁 teacher-bff schema 时未与 core-edu 状态机命名对齐
- **建议方案**
coord 仲裁统一状态命名(建议二选一):
- **方案 A推荐**core-edu DB status 统一为小写动词形式 `draft / published / in_progress / grading / graded / archived / cancelled`GraphQL 枚举映射为大写 `DRAFT / PUBLISHED / IN_PROGRESS / GRADING / GRADED / ARCHIVED / CANCELLED`(去掉 `SCORED`,统一用 `GRADED`。SubmissionStatus 统一为 `NOT_SUBMITTED / SUBMITTED / GRADED`core-edu exam/homework submissions 初始状态统一为 `not_submitted`(不再用 `in_progress` 或 `draft`)。
- **方案 B**:保留 coord GraphQL 现状(`SCORED`core-edu 改 `graded` 为 `scored`。
ai08 倾向方案 A`graded` 是教育领域通用术语,`scored` 歧义大)。
- **状态**:待 coord 仲裁
---
### ISSUE-004-ai08class.transferred 事件 topic 三处不一致P1
- **提请方**ai08
- **日期**2026-07-10
- **类型**契约不明确topic 命名跨文档不一致)
- **描述**
`class.transferred` 事件的 topic 命名在三处文档不一致:
| 文档 | topic 命名 |
| ---- | ---------- |
| 01-understanding.md L64 / 02-architecture-design.md §5.1 / §7 | `edu.org.class.created`(合并后归 org 域) |
| matrix.md §4 | `edu.class.events`ClassEventaction: transferred |
| events.proto L13 注释 | `edu.class.events` |
core-edu 文档声称"classes 合并后归 org 域"用 `edu.org.class.created`,但 coord 维护的 matrix.md 和 events.proto 仍用 `edu.class.events`。
- **影响**
- core-edu 按 `edu.org.class.created` 实现 TOPIC_MAP 后matrix.md 记录的 `edu.class.events` topic 下游消费者订阅不上
- 命名归类不一致org 域 vs class 域)
- **建议方案**
coord 仲裁统一:
- 若 classes 合并到 core-edu 后仍归"教学组织域",则 topic 应为 `edu.teaching.class.transferred`(遵循 §3.1 仲裁的 `edu.teaching.<aggregate>.<action>` 风格)
- 若归"组织域",则为 `edu.org.class.transferred`(注意 action 应为 `transferred` 而非 `created`
ai08 倾向 `edu.teaching.class.transferred`(与 §3.1 仲裁风格一致classes 合并到 core-edu 后属教学域)。
- **状态**:待 coord 仲裁
---
### ISSUE-005-ai08core-edu gRPC RPC 数量统计口径不一致P1
- **提请方**ai08
- **日期**2026-07-10
- **类型**:契约不明确(统计口径冲突)
- **描述**
core-edu gRPC RPC 数量在三处文档统计不一致:
| 文档 | RPC 数 | 包含 ClassService | 包含 P3 新增 RPC |
| ---- | ------ | ----------------- | ---------------- |
| matrix.md §2 | 22 | ✅ 是4 RPC | ❌ 否P2 基线) |
| 02-architecture-design.md §4.2 | 22 | ❌ 否 | ✅ 是PublishExam/SubmitExam/GradeExam/GradeHomework/UpdateGrade |
| core-edu_contract.md §1.1 | 22 | ✅ 是4 RPC | ❌ 否P2 基线) |
P3 全量应为ClassService 4 + ExamService 85+3 新增)+ HomeworkService 54+1 新增)+ GradeService 65+1 新增)+ AttendanceService 4 = **27 RPC**。
- **影响**
- matrix.md §2 声明 22 RPC 但含 ClassService02 文档声明 22 RPC 但不含 ClassService下游 AI 无法判断应实现多少 RPC
- 就绪信号"core-edu gRPC 50053 + 22 RPC"含义模糊
- **建议方案**
coord 统一 RPC 统计口径:
- matrix.md §2 改为 "27 RPCP3 全量ClassService 4 + ExamService 8 + HomeworkService 5 + GradeService 6 + AttendanceService 4"
- 就绪信号改为 "core-edu gRPC 50053 + 27 RPC + HealthService SERVING"
- core-edu_contract.md §1.1 补全 P3 新增 RPC
- **状态**:待 coord 仲裁
---
### ISSUE-006-ai08core-edu 02 文档 §13.3 七项未决决策待仲裁P2
- **提请方**ai08
- **日期**2026-07-10
- **类型**:契约不明确(设计决策未仲裁)
- **描述**
02-architecture-design.md §13.3 列出 7 项未决设计决策ai08 尚未正式提请 coord 仲裁,现集中提请:
| # | 决策点 | ai08 倾向 |
| - | ------ | --------- |
| 1 | classes 服务合并到 core-edu 的时机 | (a) P3 初期合并 |
| 2 | 排课 room_id 是否 P3 实现 | (b) 仅预留字段 |
| 3 | 成绩计算公式 scope 优先级 | (a) class > subject > school |
| 4 | 作业 grace_period 默认值 | (b) 300 秒5 分钟宽限) |
| 5 | P3 是否启用 events.proto schema 强制校验 | (b) 仅文档约束 |
| 6 | exam.submitted 事件是否包含完整 answers | (b) 仅含 submission_id |
| 7 | archived 考试数据是否物理迁移 | (b) P3 仅软删除 |
注:决策 #4 与 02 文档 §3.1.2 homework 表 `gracePeriod: int("grace_period").notNull().default(0)` 默认 0 不一致,仲裁后需统一 schema 默认值。
- **影响**
- 决策未定core-edu P3 实现无法启动
- 决策 #4 schema 默认值与倾向不一致,仲裁前可能实现错
- **建议方案**
coord 逐项仲裁ai08 按倾向方案实现。决策 #4 若采 (b) 300 秒02 文档 §3.1.2 schema 默认值改为 `.default(300)`。
- **状态**:待 coord 仲裁
---
### ISSUE-007-ai08core-edu 01 文档审计表"待核对"项待 coord 确认P2
- **提请方**ai08
- **日期**2026-07-10
- **类型**:其他(审计未完成项)
- **描述**
01-understanding.md 审计表与"黄金模板对齐清单"中 3 项标记"待核对"
1. Dockerfile 多阶段构建L111 / L128 审计表"待核对"
2. ActionState 信封 traceId 验证L116
3. homework/grades controller 权限装饰器覆盖核对L104
ai08 在 P3 实施前需 coord 确认这些项是否作为 P3 验收硬性标准。
- **建议方案**
coord 确认上述 3 项是否纳入 P3 验收标准若纳入ai08 在 P3 实施时补齐。
- **状态**:待 coord 仲裁

View File

@@ -1,24 +1,129 @@
# data-ana 问题记录
> 负责人ai11
> 关联:[coord.md](../coord.md)、[contracts/data-ana_contract.md](../contracts/data-ana_contract.md)
> 关联:[coord.md](../coord.md)、[coord-cross-review.md](../../coord-cross-review.md)、[contracts/data-ana_contract.md](../contracts/data-ana_contract.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
---
## 问题列表
## §0 已有仲裁核查记录2026-07-10
<!--
追加条目格式:
> ai11 在编写 objections 前对 coord.md 与 coord-cross-review.md 中涉及 data-ana 的仲裁逐项核查落实状态。
### ISSUE-[编号]-[AI标识][标题]
### 0.1 coord.md 仲裁核查
- **提请方**aiXX
- **日期**YYYY-MM-DD
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**[详细描述问题]
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
| 编号 | 主题 | 涉及 data-ana | 核查结论 |
| ------- | ---------------------------- | ------------- | ------------------------------ |
| ARB-001 | teacher-bff GraphQL schema | ❌ 否 | 不涉及,无需 action |
| ARB-002 | MF Shell 暴露清单 | ❌ 否 | 不涉及,无需 action |
(暂无问题)
**结论**coord.md 无 data-ana 直接仲裁。
### 0.2 coord-cross-review.md 仲裁核查8 项涉及 data-ana
| # | 审查章节 | 裁决内容 | 责任方 | 核查结论 |
| -- | ---------------- | ------------------------------------------------------------------ | ------ | ---------------------------------------------------------------------------------------------------- |
| 1 | §2.2 #3 | iam 新增 `GetEffectiveDataScope` RPCP4 补全data-ana gRPC 调用 | iam | ⚠️ **未落实**iam.proto 当前仅 4 RPCRegister/Login/RefreshToken/GetUserInfo无 GetEffectiveDataScope |
| 2 | §2.3 P4 行 | content + data-ana P4 启用 gRPC server | ai11 | ⏳ 未到 P4 阶段,待执行 |
| 3 | §3.2 | 补登 `edu.insight.ai.usage` topic + events.proto 补 AIUsageEvent | coord | ⚠️ **未落实**events.proto 当前仅 4 messageClass/Exam/Homework/GradeEvent缺 AIUsageEvent |
| 4 | §3.3 | Python 服务 Outbox 豁免MasteryUpdated / WarningTriggered | coord | ✅ **已对齐**01/02 文档已声明豁免,引用 coord-cross-review.md §3.3 |
| 5 | §4.3 | data-ana HTTP=3006 / gRPC=50055 | coord | ✅ **已对齐**01/02 文档端口声明一致 |
| 6 | §5.3 | Python 服务信封改为 ActionStatedegraded 放 details 子字段) | ai11 | ✅ **已对齐**02 §4.3 ActionState 实现已修正degraded 移至顶层 details |
| 7 | §6 #4 | 同 #6ai06 修正 data-ana/ai 02 文档 + 代码 | ai11 | ✅ **已对齐(文档)**02 已修正;代码待 P4 实现阶段重构 |
| 8 | §6 #7/#8/#9/#10 | coord 在 004 §7.2 补登 topic + §1.2 端口列 + §4.1 gRPC 矩阵 + §12.2 豁免 | coord | ⚠️ **未落实**004 正文无 §4.2/§7.2 补登段/§11.4/§11.501/02 引用断裂已临时改为引 coord-cross-review.md |
### 0.3 coord-cross-review §8.2 批次 0 产出声明核查
coord-cross-review.md §8.2 声称批次 0 已完成 proto 补全ai11 逐文件核查实际状态:
| 声明产出项 | 声明状态 | 实际文件状态ai11 核查) | 核查结论 |
| -------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------- | ------------ |
| iam.proto 12 RPC | ✅ 12 RPC | **4 RPC**Register/Login/RefreshToken/GetUserInfo | ⚠️ 严重不符 |
| analytics.proto 扩展 | ✅ 12 RPC含 Stream | **3 RPC**GetClassPerformance/GetStudentWeakness/GetLearningTrend | ⚠️ 严重不符 |
| events.proto 补全 | ✅ 9 message+AuditEvent | **4 message**ClassEvent/ExamEvent/HomeworkEvent/GradeEvent | ⚠️ 严重不符 |
| core_edu.proto 补全 | ✅ 5 service | 未由 ai11 核查(非本模块边界) | — |
| buf.gen.yaml 插件 | ✅ go + python | 未由 ai11 核查coord 维护) | — |
> **核查说明**ai11 仅核查与 data-ana 直接相关的 protoiam/analytics/events。§8.2 声明与实际文件严重不符,可能原因:(a) 声明为计划态但未执行;(b) 执行后未提交到本 worktree 分支;(c) 在其他分支已执行但未合并。无论哪种原因data-ana 的 P4 实现依赖这些 proto 补全,当前实际状态构成 P4 阻塞。
---
## §1 问题列表
### ISSUE-001-ai11iam.proto 缺 GetEffectiveDataScope RPCP4 阻塞)
- **提请方**ai11
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**coord-cross-review.md §2.2 #3 已仲裁"iam P4 补全 `GetEffectiveDataScope` RPCdata-ana gRPC 调用",但 iam.proto 当前仅 4 RPC无此 RPC。data-ana 的 DataScope 6 级过滤SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL依赖此 RPC 解析用户可见数据范围,是 P4 实现的硬阻塞项。
- **建议方案**coord 确认 iam.proto 补全进度。若 iam 侧尚未实现data-ana P4 阶段将使用硬编码 DataScope 降级(按 role 映射默认 scope并标注 `details.degraded: true`,待 iam 就绪后切换。
- **状态**:待 coord 仲裁(核查已有仲裁 §0.2 #1 未落实)
---
### ISSUE-002-ai11events.proto 缺 AIUsageEvent messageP5 阻塞P4 预备)
- **提请方**ai11
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**coord-cross-review.md §3.2 已仲裁"补登 `edu.insight.ai.usage` topic + events.proto 补 `AIUsageEvent` message",但 events.proto 当前仅 4 messageClassEvent/ExamEvent/HomeworkEvent/GradeEvent缺 AIUsageEvent。data-ana 需消费此事件落 `ai_usage_log` 宽表,供管理员仪表盘展示 AI 用量统计。
- **建议方案**coord 在 events.proto 补 `AIUsageEvent` message字段建议`{event_id, request_id, user_id, provider, model, prompt_tokens, completion_tokens, total_tokens, latency_ms, success, cost_cents, occurred_at}`。P5 前补全即可P4 仪表盘 AI 用量区块显示"暂无数据"。
- **状态**:待 coord 仲裁(核查已有仲裁 §0.2 #3 未落实)
---
### ISSUE-003-ai11analytics.proto 仅 3 RPCcoord-cross-review §8.2 声称已扩展至 12 RPC 但实际未落实P4 阻塞)
- **提请方**ai11
- **日期**2026-07-10
- **类型**:前置依赖缺失 + 声明与实际不符
- **描述**coord-cross-review.md §8.2 声称"analytics.proto 扩展 ✅ 12 RPC含 Stream",但实际文件仅 3 RPCGetClassPerformance/GetStudentWeakness/GetLearningTrend。ai-allocation.md §5 与 matrix.md §2 均要求 data-ana 提供 12 RPC02-architecture-design.md §4.2 已设计完整 12 RPC 清单(含 4 端 Dashboard + Warning + Mastery + Server Streaming但 proto 未补全导致无法生成 stub。
- **建议方案**coord 确认 analytics.proto 扩展进度。ai11 可提供 12 RPC 的完整 message 定义提案(见 [02-architecture-design.md §4.2](../../../services/data-ana/docs/02-architecture-design.md)coord 审议后合并到 analytics.proto。若 coord 未补全ai11 在 P4 阶段自行补全 proto本分支内提请 coord 合并。
- **状态**:待 coord 仲裁
---
### ISSUE-004-ai11coord-cross-review §6 整改清单 coord 责任项未落实,导致 004 章节引用断裂
- **提请方**ai11
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**coord-cross-review.md §6 整改清单中标注"coord"责任的 4 项整改未在 004 正文中落实:
- #7004 §7.2 补登 6 个 topic + CDC 命名规范 → 004 正文无对应段落
- #8004 §1.2 新增 HTTP/gRPC 端口两列 → 004 §1.2 服务清单无端口列
- #9004 §4.1 补充 gRPC 启用阶段矩阵 → 004 正文无 §4.2 子节
- #10004 §12.2 补充派生数据事件 Outbox 豁免条款 → 004 §12.2 未补充
这导致 01-understanding.md 和 02-architecture-design.md 中引用 004 §4.2/§11.4/§11.5/§15.3 等章节均断裂004 正文仅到 §14§15 在 004-p6-addendum.md 但内容不同。ai11 已在 v2.1 修订中临时改为引用 coord-cross-review.md 对应裁决章节,但这是过渡方案。
- **建议方案**coord 按整改清单 #7/#8/#9/#10 补全 004 对应章节,使 004 成为可信的架构设计意图唯一源。各 AI 文档随后将引用从 coord-cross-review.md 回切到 004 对应章节。
- **状态**:待 coord 仲裁
---
### ISSUE-005-ai11data-ana 发布的 MasteryEvent topic 命名三处不一致
- **提请方**ai11
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**data-ana 发布的掌握度/预警事件 topic 命名在三个文档中不一致:
| 文档 | topic 命名 |
| -------------------------------------- | ------------------------------------- |
| 01-understanding.md / 02-architecture-design.md | `edu.insight.mastery.updated` + `edu.insight.warning.triggered` |
| matrix.md §4 | `edu.analytics.mastery` |
| contracts/data-ana_contract.md修正前 | `edu.data_ana.mastery.events` |
按 004 §7.2 命名规范 `edu.<domain>.<aggregate>.<action>`data-ana 属于 D6 智能洞察领域domain=insight`edu.insight.mastery.updated` 符合规范。matrix.md 的 `edu.analytics.mastery` 不符合命名规范(缺 action 层级,且 domain 用了服务名而非领域名)。
- **建议方案**coord 裁决统一为 `edu.insight.mastery.updated` + `edu.insight.warning.triggered`coord 修正 matrix.md §4。ai11 已在 contract.md 中采用此命名。
- **状态**:待 coord 仲裁
---
### ISSUE-006-ai11coord-cross-review §8.2 批次 0 产出声明与 proto 实际文件状态严重不符
- **提请方**ai11
- **日期**2026-07-10
- **类型**:其他(声明与实际不符)
- **描述**coord-cross-review.md §8.2 声称批次 0 已完成 iam.proto12 RPC/ analytics.proto12 RPC/ events.proto9 message补全但 ai11 逐文件核查发现实际均未补全(详见 §0.3 核查表)。这影响所有依赖这些 proto 的下游 AI 的排期评估——若 AI 信任 §8.2 声明,会在排期中忽略 proto 补全的等待时间,导致排期失真。
- **建议方案**coord 核实 §8.2 声明真实性。若实际已补全但未合并到各 worktree 分支,请协调合并;若实际未补全,请更新 §8.2 状态为"计划中"或"待执行",并明确补全时间点,以便下游 AI 据此排期。
- **状态**:待 coord 仲裁

View File

@@ -6,6 +6,124 @@
---
## §1 已有仲裁核查
> 对已生效的仲裁coord-final-decisions I1-I8、president-final-rulings 相关条款、coord.md ARB-001/ARB-002逐条核查落地情况。
### 1.1 coord-final-decisions I1-I8iam 专项)核查
| 裁决 | 内容摘要 | 核查结论 | 证据 |
| ---- | -------- | -------- | ---- |
| I1 | P2 即启用 gRPC server 50052REST + gRPC 并存 | ⚠️ **02 文档未回写**[02-architecture-design.md](../../../services/iam/docs/02-architecture-design.md) §1/§7.1/§8.1 决策点 5 仍写"P2 仅 RESTP3 随 core-edu 引入 gRPC"iam.proto 仅 4 RPC 未补全 | [iam.proto](../../../packages/shared-proto/proto/iam.proto) 仅 4 RPC[02 文档 §8.1](../../../services/iam/docs/02-architecture-design.md) 决策点 5 |
| I2 | 直接建 shared-ts Outbox 工具包iam 首次实现即用 | ✅ **工具包已建立**`packages/shared-ts/src/outbox/` 存在 outbox.service.ts + outbox.module.ts 02 文档 §8.1 决策点 4 仍写"iam 自建轻量 Outbox",未回写 | [shared-ts/outbox](../../../packages/shared-ts/src/outbox/) |
| I3 | 首次实现即 DB 驱动 + Redis 缓存,废弃本地 map | ❌ **源码未改造**[permission.guard.ts](../../../services/iam/src/middleware/permission.guard.ts) 第 30-39 行仍用硬编码 `ROLE_PERMISSIONS` mapadmin/teacher02 文档 §8.1 决策点 9 描述为"待改造" | [permission.guard.ts](../../../services/iam/src/middleware/permission.guard.ts) |
| I4 | 首次实现即注册 AuthMiddlewareController 用 @Req() 注入 | ❌ **源码未注册**[app.module.ts](../../../services/iam/src/app.module.ts) 仅注册 PermissionGuard 为 APP_GUARD未在 configure() 消费 AuthMiddleware02 文档 §1.1/§1.2/§8.1 决策点 10 仍写"P2 仍不注册" | [app.module.ts](../../../services/iam/src/app.module.ts) |
| I5 | P2 本地文件 IAM_PRIVATE_KEY_PATH / IAM_PUBLIC_KEY_PATH | ⚠️ **源码未实现**[iam.service.ts](../../../services/iam/src/iam/iam.service.ts) 第 170-186 行仍用 `env.JWT_SECRET`HS256 单密钥02 文档 §8.1 决策点 1 已对齐本地文件→P6 Vault但未落地 | [iam.service.ts](../../../services/iam/src/iam/iam.service.ts) |
| I6 | P2 即补全 iam_student_guardians 表 + GetChildrenByParent RPC + GET /iam/children | ❌ **02 文档表名错误**[02 文档 §3.1.2](../../../services/iam/docs/02-architecture-design.md) 用 `iam_parent_student_relations`,裁决表名为 `iam_student_guardians`;源码未实现 | 02 文档 §3.1.2 |
| I7 | 采用 /iam/v1/* 前缀Gateway 透传 | ❌ **02 文档未回写**[02 文档 §4.1](../../../services/iam/docs/02-architecture-design.md) REST API 清单全部用 `/iam/*``/v1` 前缀;源码 Controller 用 `@Controller("iam")` 无版本前缀 | [iam.controller.ts](../../../services/iam/src/iam/iam.controller.ts)、[rbac.controller.ts](../../../services/iam/src/iam/rbac.controller.ts) |
| I8 | 统一 GET /iam/permissions/effective | ✅ **源码已对齐**[rbac.controller.ts](../../../services/iam/src/iam/rbac.controller.ts) 第 33 行用 `@Get("permissions/effective")` | [rbac.controller.ts](../../../services/iam/src/iam/rbac.controller.ts) |
### 1.2 president-final-rulings 相关条款核查
| 条款 | 内容摘要 | 核查结论 |
| ----- | -------- | -------- |
| §2.15 | /iam/v1/* 版本化规则Controller 加 v1 前缀) | ❌ 同 I7未落地 |
| §2.16 | gRPC 与 REST 双入口策略gateway HTTP 透传 + BFF gRPC 调用) | ⚠️ 02 文档未体现双入口设计,仅描述 REST 单入口contract.md §1.2 写"无对外 HTTP 端点,仅 gRPC"与此冲突 |
| §3.2 | iam P2 拆分 P2.18 RPC 核心)+ P2.2(扩展) | ⚠️ workline.md 仅粗略列出 P2.1P2.2-P6 未细化(见 worklines/iam_workline.md |
| §5.5 | 审计日志归 iamAuditEvent + edu.iam.audit.created topic + user_audit_log 表) | ❌ 02 文档未包含审计日志设计events.proto 未定义 AuditEvent message |
| §5.1 | events.proto 补全 UserEvent/RoleEvent | ❌ events.proto 实际仅含 ClassEvent/ExamEvent/HomeworkEvent/GradeEvent缺 UserEvent/RoleEvent/AuditEventcoord-final-decisions §5.1 标注"✅ 已补全"与实际不符) |
### 1.3 coord.md ARB-001 / ARB-002 核查
| 仲裁 | 内容摘要 | 核查结论 |
| ------ | -------- | -------- |
| ARB-001 | teacher-bff GraphQL schema 第一版5 Query | ✅ schema 中 `me: User!``viewports` 依赖 iam与 iam GetUserInfo/GetViewports RPC 对齐,无冲突 |
| ARB-002 | MF Shell 暴露清单 | ✅ 不涉及 iam 直接交付物,无冲突 |
---
## §2 新提请异议
### ISSUE-001-ai0601/02 文档未回写 I1-I8 裁决,中间过渡方案残留
- **提请方**ai06
- **日期**2026-07-10
- **类型**:契约不明确 / 其他
- **描述**coord-final-decisions §0.1 强制覆盖声明要求各 AI 在 3 个工作日内回写 02 文档,删除所有"中间过渡方案"。president-final-rulings §3.4 也明确要求 ai06 回写 iam 02I1-I8§3.4)。但当前 [01-understanding.md](../../../services/iam/docs/01-understanding.md) 和 [02-architecture-design.md](../../../services/iam/docs/02-architecture-design.md) 仍残留大量过渡方案:
- 02 §1/§7.1/§8.1 决策点 5P2 仅 REST → P3 gRPC违反 I1
- 02 §8.1 决策点 4iam 自建 Outbox违反 I2
- 02 §8.1 决策点 9PermissionGuard 待改造(违反 I3
- 02 §1.1/§1.2/§8.1 决策点 10AuthMiddleware P2 不注册(违反 I4
- 02 §3.1.2:表名 iam_parent_student_relations违反 I6
- 02 §4.1API 路径无 /v1 前缀(违反 I7
- 01 §1teacher-bff HTTP 调用 iam违反 B2应 gRPC
- **建议方案**ai06 立即回写 01/02 文档,删除全部中间过渡方案描述,对齐 I1-I8 + §2.15/§2.16/§5.5 最终方案。
- **状态**:待 coord 仲裁(确认回写范围与验收标准)
### ISSUE-002-ai06events.proto 缺少 UserEvent/RoleEvent/AuditEvent message
- **提请方**ai06
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**coord-final-decisions §5.1 标注"events.proto ✅ 已补全UserEvent/RoleEvent/NotificationEvent/MasteryEvent/AIUsageEvent/KnowledgePointEvent/QuestionEvent"president §5.5 也要求"events.proto 补 AuditEvent 在批次 0.10 完成"。但实际 [events.proto](../../../packages/shared-proto/proto/events.proto) 仅定义 ClassEvent/ExamEvent/HomeworkEvent/GradeEvent 4 个 message**完全缺少** UserEvent/RoleEvent/AuditEvent 等 iam 依赖的事件契约。这直接阻塞 iam Outbox 事件发布iam 无法写入未定义 schema 的事件)。
- **建议方案**coord 立即补全 events.proto至少新增 UserEvent、RoleEvent、AuditEvent 三个 message按 02 文档 §5.2 和 president §5.5 的字段定义)。
- **状态**:待 coord 仲裁
### ISSUE-003-ai06Topic 命名三方不一致edu.identity.* vs edu.iam.*
- **提请方**ai06
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**iam 事件 Topic 命名存在三方不一致:
- [004 §7.2](../../../docs/architecture/004_architecture_impact_map.md) 行 620-621`edu.identity.user.created` / `edu.identity.user.updated`(用 `identity` 域名)
- [matrix.md §4](../matrix.md) / [iam_contract.md §1.4](../contracts/iam_contract.md)`edu.iam.user.events` / `edu.iam.role.events` / `edu.iam.audit.created`(用 `iam` 域名)
- president §5.5`edu.iam.audit.created`(用 `iam` 域名)
- coord-final-decisions G16 规则:`edu.<domain>.<aggregate>.<action>`
- **建议方案**:统一用 `edu.iam.*`(服务名为 iam非 identitycoord 同步修正 004 §7.2 的 `edu.identity.*``edu.iam.*`。同时明确 Topic 粒度matrix.md 用聚合 topic`edu.iam.user.events` 含多 action004 §7.2 用具体动作 topic`edu.iam.user.created`),需统一为一种风格。
- **状态**:待 coord 仲裁
### ISSUE-004-ai06DataScope 枚举三方不一致
- **提请方**ai06
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**DataScope 6 级枚举存在三方不一致:
- [004 §5.3 表](../../../docs/architecture/004_architecture_impact_map.md) 行 463-470SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL
- [004 §5.3](../../../docs/architecture/004_architecture_impact_map.md) 行 505all/grade_managed/class_taught/children/owned + 自定义(语义命名,与表不同)
- president §3.2 P2.2ALL/SCHOOL/GRADE/CLASS/SUBJECT/SELF**SUBJECT 替代 DISTRICT**
- 源码 [iam.schema.ts](../../../services/iam/src/iam/iam.schema.ts) 第 16-25 行self/class/grade/school/district/all与 004 表一致,与 president 不一致)
- **建议方案**coord 统一裁定最终枚举值。若采用 president 的 SUBJECT学科级数据范围K12 场景更实用),需同步修改源码 schema + 004 §5.3 表 + 02 文档;若保留 DISTRICT需修正 president §3.2。同时修正 004 行 505 的语义命名使其与枚举表一致。
- **状态**:待 coord 仲裁
### ISSUE-005-ai06iam.proto 仅 4 RPC未补全至 12 RPC
- **提请方**ai06
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**coord-final-decisions §5.1 要求 iam.proto 在"P2 启动前"补全 8 个新 RPCGetViewports / GetEffectivePermissions / GetEffectiveAccess / Logout / GetPublicKey / BatchGetUsers / GetEffectiveDataScope / GetChildrenByParentpresident §6 批次 0 任务 0.3 也明确"coord 补全 iam.proto 8 RPC"。但实际 [iam.proto](../../../packages/shared-proto/proto/iam.proto) 仍仅 4 RPCRegister/Login/RefreshToken/GetUserInfo未补全。这阻塞 iam P2.1 的 8 RPC 实现proto 是契约先行前提)。
- **建议方案**coord 立即补全 iam.proto 至 12 RPC含对应 message 定义),对齐 [iam_contract.md §1.1](../contracts/iam_contract.md) 的 12 RPC 清单。
- **状态**:待 coord 仲裁
### ISSUE-006-ai0601/02 文档 AI 身份署名错误
- **提请方**ai06
- **日期**2026-07-10
- **类型**:其他
- **描述**[01-understanding.md](../../../services/iam/docs/01-understanding.md) 和 [02-architecture-design.md](../../../services/iam/docs/02-architecture-design.md) 署名"AIai02TS / 身份认证)"、"AI Agent: ai02 (iam-module)"。但 coord-final-decisions §3.1、president §3.2、[matrix.md](../matrix.md)、[workline.md](../workline.md)、[iam_workline.md](../worklines/iam_workline.md)、[iam_contract.md](../contracts/iam_contract.md) 全部指明 iam 由 **ai06** 负责。ai02 实际负责 push-gatewaycoord-final-decisions §3.7)。文档署名错误会导致多 AI 协作时身份混淆。
- **建议方案**:回写时将 01/02 文档署名从 ai02 改为 ai06。
- **状态**:待 coord 仲裁
### ISSUE-007-ai06contract.md §1.2 与双入口策略冲突
- **提请方**ai06
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**[iam_contract.md §1.2](../contracts/iam_contract.md) 写"无对外 HTTP 端点,仅 gRPC"。但 president §2.16 裁决双入口策略REST 供 gateway 透传 + gRPC 供 BFF 聚合调用,"gateway 保持 HTTP 透传"。若 iam 不暴露 HTTP 端点gateway 无法透传gateway 不改为 gRPC 客户端)。
- **建议方案**:修正 contract.md §1.2,补充 REST 端点清单(/iam/v1/* 系列),标明"REST 供 gateway 透传 + admin-portal 直连gRPC 供 BFF 聚合调用"。
- **状态**:待 coord 仲裁
---
## 问题列表
<!--
@@ -21,4 +139,4 @@
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
(暂无问题)
见上方 §2 新提请异议ISSUE-001 ~ ISSUE-007

View File

@@ -6,19 +6,189 @@
---
## 问题列表
## §0 已有仲裁核查ai10 复核)
<!--
追加条目格式:
> 本节核查 01-understanding.md / 02-architecture-design.md 中引用的已有仲裁,对照源码/proto 验证准确性。
### ISSUE-[编号]-[AI标识][标题]
### ISSUE-001-ai10M8 ZodError 已修复 — 核查通过
- **提请方**aiXX
- **日期**YYYY-MM-DD
- **类型**契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**[详细描述问题]
- **建议方案**[AI 的建议]
- **状态**待 coord 仲裁 / 已裁决(见 coord.md §X
-->
- **提请方**ai10
- **日期**2026-07-10
- **类型**仲裁核查
- **描述**01-understanding.md M8 称"GlobalErrorFilter 已识别 ZodError 返回 400"。核查 [global-error.filter.ts](../../../services/msg/src/shared/errors/global-error.filter.ts) L32-42`else if (exception instanceof ZodError) { statusCode = 400; ... }`,确实已处理,返回 `MSG_VALIDATION_ERROR` + 400。
- **核查结论**:✅ 仲裁准确M8 标记"已修复"无误
- **状态**已裁决(核查通过,无需处理
(暂无问题)
### ISSUE-002-ai10M12 proto 包名规范 — 核查通过
- **提请方**ai10
- **日期**2026-07-10
- **类型**:仲裁核查
- **描述**01-understanding.md M12 称"实际 `next_edu_cloud.msg.v1` 符合规范"。核查 [msg.proto](../../../packages/shared-proto/proto/msg.proto) L3`package next_edu_cloud.msg.v1;`,与 01-understanding.md 描述一致。
- **核查结论**:✅ 仲裁准确,但存在规则冲突(见 ISSUE-007
- **状态**:已裁决(核查通过)
### ISSUE-003-ai10M2 msg 必须有 Outbox — 核查通过
- **提请方**ai10
- **日期**2026-07-10
- **类型**:仲裁核查
- **描述**01-understanding.md M2 称"004 §7.2 明确 msg 生产 `edu.notification.events`msg 必须有 Outbox"。核查 004 §7.3 L638 `NotificationRequested` 事件Msg 投递通知到多渠道)+ known-issues §msg L346"Outbox 强制"。
- **核查结论**:✅ 仲裁准确msg 必须实现 OutboxP5 强制)
- **状态**:已裁决(核查通过)
### ISSUE-004-ai10core-edu 事件 topic 命名统一 — 核查部分通过
- **提请方**ai10
- **日期**2026-07-10
- **类型**:仲裁核查
- **描述**02-architecture-design.md §7.2 P11 称"coord 已仲裁采用 `edu.teaching.*` 新约定"。核查:
- 004 §7.2 L623-625 使用 `edu.teaching.assignment.submitted` / `edu.teaching.exam.published` / `edu.teaching.grade.recorded`(新约定)✅
- [events.proto](../../../packages/shared-proto/proto/events.proto) L9-13 注释仍用 `edu.exam.events` / `edu.homework.events`(旧约定)❌ 未同步
- [matrix.md](../matrix.md) §4 L112-115 使用 `edu.exam.events` / `edu.homework.events`(旧约定)❌ 未同步
- **核查结论**:⚠️ 仲裁已作出但未全量同步events.proto 注释与 matrix.md 仍用旧约定,需 coord 统一更新
- **状态**:待 coord 同步(见 ISSUE-008
### ISSUE-005-ai10Push Gateway 调用方向歧义 — 核查通过
- **提请方**ai10
- **日期**2026-07-10
- **类型**:仲裁核查
- **描述**02-architecture-design.md §7.2 P10 称"004 §4.1 写 PushGW→Msg实际是 Msg→PushGW"。核查 004 §4.1 L413`push-gateway → Msg | gRPC | 推送通道建立`,方向确实反了。实际流程是 msg 调 push-gateway 的 gRPC PushService.Push见 [notifications.service.ts](../../../services/msg/src/notifications/notifications.service.ts) L179 fetch POST /internal/push 降级实现)。
- **核查结论**:✅ 歧义确认,建议 coord 修正 004 §4.1 表述为"Msg → push-gateway (gRPC)"
- **状态**:待 coord 修正 004
---
## §1 新发现问题ai10 提请)
### ISSUE-006-ai10events.proto P9 字段描述不准确
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**02-architecture-design.md §7.2 P9 称"events.proto ExamEvent / HomeworkEvent / GradeEvent 需补 `class_id` / `student_ids[]` 字段"。核查 events.proto
- `ExamEvent` L32 **已有** `class_id` 字段 ✅
- `HomeworkEvent` L45 **已有** `class_id` 字段 ✅
- `GradeEvent` L50-59 **无** `class_id`(仅有 `student_id`)❌
- 三者均**无** `student_ids[]`(复数,用于 fan-out 广播)❌
- **建议方案**:修正 P9 表述为"`GradeEvent` 需补 `class_id`;全部事件需补 `student_ids[]` 字段msg fan-out 广播通知需要)"
- **状态**:待 coord 仲裁
### ISSUE-007-ai10proto 包名规则冲突project_rules vs 实际)
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**[project_rules §5](../../../.trae/rules/project_rules.md) 规定"包名规范:`edu.<domain>.v1`(如 `edu.iam.v1``edu.core_edu.v1`",但实际所有 proto 文件使用 `next_edu_cloud.<domain>.v1`(如 msg.proto L3 `next_edu_cloud.msg.v1`、events.proto L3 `next_edu_cloud.events.v1`。01-understanding.md 称"coord 已裁决采用 `next_edu_cloud.*`",但 project_rules §5 未同步更新,仍写 `edu.<domain>.v1`
- **建议方案**coord 统一裁决,二选一:
- 方案 A更新 project_rules §5 为 `next_edu_cloud.<domain>.v1`(与实际 proto 一致)
- 方案 B重命名所有 proto package 为 `edu.<domain>.v1`(与规则一致,但改动大)
- **状态**:待 coord 仲裁
### ISSUE-008-ai10Kafka topic 命名三套约定并存
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**msg 相关的 Kafka topic 命名存在三套约定:
- **约定 A004 §7.2per-event topic**`edu.identity.user.created` / `edu.teaching.exam.published` / `edu.notification.sent`
- **约定 Bmatrix.md §4 + events.proto 注释aggregate topic**`edu.iam.user.events` / `edu.exam.events` / `edu.notification.requested`
- **约定 Cmsg_contract.md §1.4aggregate topic + action 字段)**`edu.msg.notification.events`action: sent/read/recalled/failed
- 02-architecture-design.md §5.1/§5.2 采用约定 Amsg_contract.md §1.4 采用约定 Cmatrix.md 采用约定 B。known-issues §全局 L182 已标记此冲突。
- **建议方案**coord 统一为一套约定。ai10 倾向约定 Aper-event topic理由
- 004 §7.2 已采用,是架构设计意图唯一源
- per-event topic 便于消费者按需订阅,避免反序列化无关事件
- 与 NotificationSent / NotificationRead 等事件命名PascalCase对齐
- **状态**:待 coord 仲裁
### ISSUE-009-ai10RPC 数量超预算17 vs 13
- **提请方**ai10
- **日期**2026-07-10
- **类型**:工作量超批
- **描述**[ai-allocation.md §3.2](../../ai-allocation.md) L112 与 [matrix.md](../matrix.md) §2 L90 均规定 msg 为"3 Service 13 RPC"。但 02-architecture-design.md §4.2 列出 17 RPC
- NotificationService 9 RPCSendNotification / BatchSendNotification / ListNotifications / GetUnreadCount / MarkAsRead / BatchMarkAsRead / MarkAllAsRead / SearchNotifications / RecallNotification
- NotificationPreferenceService 2 RPCGetPreferences / UpdatePreferences
- NotificationTemplateService 6 RPCCreateTemplate / GetTemplate / ListTemplates / UpdateTemplate / DeleteTemplate / RenderTemplate
- 而 msg_contract.md §1.1 列出 13 RPC分布不同5+4+4两文档互相不一致
- **建议方案**coord 裁决 RPC 范围,二选一:
- 方案 A维持 13 RPC 预算02-architecture-design.md 裁剪至 13移除 BatchSendNotification / GetUnreadCount / BatchMarkAsRead / MarkAllAsRead / UpdateTemplate / DeleteTemplate降级为 REST only 或合并)
- 方案 B放宽至 17 RPC同步更新 ai-allocation.md + matrix.md + msg_contract.md
- **状态**:待 coord 仲裁
### ISSUE-010-ai10markAsRead 权限点与设计不一致
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**[notifications.controller.ts](../../../services/msg/src/notifications/notifications.controller.ts) L74 markAsRead 使用 `MSG_NOTIFICATION_MANAGE` 权限,但 02-architecture-design.md §6.1 L730 规定 markAsRead 应使用 `MSG_NOTIFICATION_READ`。MANAGE 权限通常给管理员,学生标记自己通知已读不应需要 MANAGE 权限。
- **建议方案**:以 02-architecture-design.md §6.1 为准READP5 实现时修正 controller 权限点
- **状态**:待 coord 确认
### ISSUE-011-ai10DB→ES 降级方向与 ai-allocation §5 相反
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- [ai-allocation.md §5](../../ai-allocation.md) ai10 设计重点要求"ES 降级查询策略(**DB 不可用时走 ES 索引**"——即 DB 故障时 ES 作为读模型兜底
- 02-architecture-design.md §3.2.2 / §3.4 描述"**ES 不可用时降级到 MySQL LIKE 查询**"——即 ES 故障时 DB 兜底
- 01-understanding.md M16 称"无 DB→ES 降级读路径"
- 三处描述方向相反,需统一
- **建议方案**ai10 倾向双向降级(两种故障场景都覆盖):
- ES 故障 → DB LIKE 查询(设计文档已覆盖)
- DB 故障 → ES 只读模式ai-allocation 要求,设计文档需补充)
- 但 DB 故障时写操作无法降级(必须等 DB 恢复),仅读操作可走 ES
- **状态**:待 coord 仲裁
### ISSUE-012-ai10设计文档缺 DLQ 与三层幂等防线
- **提请方**ai10
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**[known-issues §msg](../../../docs/troubleshooting/known-issues.md) L344 / L356 已记录两项 ai10 设计点,但 02-architecture-design.md 未覆盖:
- **三层幂等防线**L344L1 Redis SETNX / L2 msg_idempotency 表 / L3 notifications.source_event_id 唯一索引。设计文档 §5.5 仅描述两层Redis + DB UNIQUE缺中间层 msg_idempotency 表
- **死信队列**L356消费失败超 3 次投递 `edu.notification.dlq`。设计文档 §5 完全未提及 DLQ 设计
- **建议方案**02-architecture-design.md 补充:
- §3.1 补 `msg_idempotency` 表 schema中间层
- §5.5 改为三层幂等防线
- §5 补 DLQ 设计(重试 3 次后投递 `edu.notification.dlq` + 告警)
- **状态**:待 coord 确认非阻塞ai10 自行补充设计文档即可)
### ISSUE-013-ai10events.proto 缺 4 类 message 阻塞 msg 消费
- **提请方**ai10
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**[events.proto](../../../packages/shared-proto/proto/events.proto) 仅有 ClassEvent / ExamEvent / HomeworkEvent / GradeEvent 4 个 message。msg 消费还需要:
- `UserEvent`iam 发布 user.created/updated/deleted/role_changed— 阻塞欢迎通知/角色变更通知
- `RoleEvent`iam 发布 role.created/updated— 阻塞角色变更通知
- `MasteryEvent`data-ana 发布 mastery.updated— 阻塞学情预警通知
- `NotificationEvent`msg 发布 notification.sent/read/recalled/failed— 阻塞 push-gateway 消费 msg 事件
- **建议方案**coord 维护 shared-proto在 P5 启动前补齐这 4 个 message。msg 在 proto 补齐前用通用 JSON payload 解析A6 假设)
- **状态**:待 coord 仲裁(🔴 阻塞 P5 消费链路)
### ISSUE-014-ai10msg_contract.md 与 02-architecture-design.md RPC 清单不一致
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**:两文档 RPC 清单存在差异:
- msg_contract.md 独有02 缺GetPreferenceByChannel、ListPreferences
- 02-architecture-design.md 独有contract 缺BatchSendNotification、GetUnreadCount、BatchMarkAsRead、MarkAllAsRead、UpdateTemplate、DeleteTemplate
- 即使忽略 ISSUE-009 的数量问题,两文档的 RPC 组合也不同
- **建议方案**:待 ISSUE-009 仲裁后,统一两文档 RPC 清单
- **状态**:待 coord 仲裁(依赖 ISSUE-009
### ISSUE-015-ai10msg_contract.md Kafka 发布事件与设计文档不一致
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- msg_contract.md §1.4`edu.msg.notification.events` topic单一 NotificationEvent 含 action 字段
- 02-architecture-design.md §5.24 个 per-event topic`edu.notification.sent` / `edu.notification.read` / `edu.notification.recalled` / `edu.notification.failed`
- matrix.md §4 L118`edu.notification.requested`(第三种命名)
- **建议方案**:待 ISSUE-008 仲裁 topic 命名约定后统一
- **状态**:待 coord 仲裁(依赖 ISSUE-008

View File

@@ -6,8 +6,165 @@
---
## §0 已有仲裁核查结论ai05 复审2026-07-10
> 对 parent-bff 相关的已仲裁决策(用户 U1-U4 + coord C1-C6 + coord-final-decisions I6逐项核查执行情况。
### 0.1 核查通过项(已正确执行)
| 仲裁 | 主题 | 核查结论 |
| --- | --- | --- |
| U2 | push-gateway 豁免 gRPCHTTP /internal/push | ✅ 02 §5.3/§7.1 已执行 HTTP 调用 push-gateway |
| C2 | 端口 3010不暴露 gRPC | ✅ 02 §7.2 + matrix.md §3 已执行 |
| C3 | core-edu 错误码 CORE_EDU_* | ✅ 02 §6.2 已执行 |
| C5 | Kafka topic edu.notification.sent/read/recalled/failed | ✅ 02 §5.2 已执行004 §7.2 同步属 coord 待办 #7,不阻塞 parent-bff |
### 0.2 核查发现的问题(仲裁已裁决但文档未同步/执行有偏差)
以下问题均为"仲裁结论正确,但相关文档未同步"或"文档内部不一致",提请 coord 确认处理方式。
---
## 问题列表
### ISSUE-001-ai0501-understanding.md 未同步 ai05 接手与多项仲裁
- **提请方**ai05
- **日期**2026-07-10
- **类型**:文档同步缺失
- **描述**01-understanding.md 头部仍标 "AI 标识ai04"、"状态:待 coord 审核",未反映 ai05 已正式接手ai-allocation.md §3.2)。同时以下仲裁已裁决但 01 未同步:
- U3GraphQL P2 引入01 §4.1 仍建议"P4 先对齐 teacher-bff REST 现状",与 U3 仲裁冲突
- U4BFF 豁免 @RequirePermission01 §6 表格"权限装饰器"行标"⚠️ 不对齐",未引用 U4 仲裁
- C1错误码前缀 BFF_PARENT_01 §3.3 仍用 `PARENT_BFF_` 旧前缀
- **建议方案**01-understanding.md 头部更新为 ai05 复审版,同步 U3/U4/C1 仲裁结论;或由 coord 确认 01 作为"阶段 1 历史快照"保留原样,以 02 为准
- **状态**:待 coord 仲裁
### ISSUE-002-ai0502 文档内部 ChildGuard 缓存 TTL 不一致
- **提请方**ai05
- **日期**2026-07-10
- **类型**:文档内部不一致
- **描述**02-architecture-design.md 内部 ChildGuard 绑定列表缓存 TTL 三处不一致:
- §3.1.1 Redis 缓存 Schema 表:写 "60s"coord 推断原值)
- §9 #2 ai05 review 结论:调整为 "30s + 主动失效"
- §13 #7 黄金模板对齐表:写 "30s"
§3.1.1 表格未同步 §9 的调整,导致同一文档内 60s 与 30s 并存。
- **建议方案**§3.1.1 表格 ChildGuard 行 TTL 改为 "30s",与 §9 #2 + §13 #7 一致
- **状态**:待 coord 仲裁ai05 建议直接修正,属于文档勘误)
### ISSUE-003-ai05contract.md 仲裁引用编号错误I3 → I6
- **提请方**ai05
- **日期**2026-07-10
- **类型**:契约引用错误
- **描述**contracts/parent-bff_contract.md §2.1 表格 + §3.1 引用 "I3/ISSUE-047 裁决" 作为 GetChildrenByParent 的仲裁依据。但核查 coord-final-decisions.md 发现:
- I3 是 "PermissionGuard 本地 map → DB 驱动" 裁决,与家长-学生关联无关
- I6 才是 "家长-学生关联P2 即补全 iam_student_guardians 表 + GetChildrenByParent RPC" 裁决
- 全仓库未检索到 "ISSUE-047" 编号grep 无结果),疑为虚构编号
- **建议方案**contract.md 将 "I3/ISSUE-047 裁决" 修正为 "I6 裁决coord-final-decisions.md §1"
- **状态**:待 coord 仲裁ai05 建议直接修正,属于引用勘误)
### ISSUE-004-ai05004 §4 服务依赖图与 matrix.md §1 未同步 C6 仲裁
- **提请方**ai05
- **日期**2026-07-10
- **类型**:架构图未同步
- **描述**C6 仲裁将 parent-bff 依赖扩展为 iam + core-edu + data-ana + msg
- 004_architecture_impact_map.md §4 服务依赖图line 376-377仍只画 `PBFF --> IAM` + `PBFF --> CoreEdu`,未加 DataAna + Msg
- matrix.md §1 服务依赖矩阵line 64-65同样只画 `PBFF --> IAM` + `PBFF --> CORE`
- 02 §0.1 C6 行已标注 "004 §4 待 coord 同步更新",但至今未同步
此差异导致新接手的 AI 看 004/matrix 会误以为 parent-bff 不依赖 data-ana/msg与 02 设计冲突。
- **建议方案**coord 在 004 §4 服务依赖图补 `PBFF --> DataAna` + `PBFF --> Msg`matrix.md §1 同步;更新 004 时按 project_rules §1 "改码必同步图" 执行
- **状态**:待 coord 仲裁
### ISSUE-005-ai05parent-portal 01 文档与 parent-bff GraphQL 决策跨模块冲突
- **提请方**ai05
- **日期**2026-07-10
- **类型**:跨模块契约冲突
- **描述**U3 仲裁决定 parent-bff P4 直接用 GraphQL02 §4 已执行),但 parent-portal 01-understanding.md §3.1 仍按 REST 设计消费 parent-bff
- parent-portal §3.1 列 `GET /parent/viewports``GET /parent/dashboard``GET /parent/children` 等 REST 端点
- parent-bff 02 §4.1 明确 "不实现 REST 业务端点(仅保留 /healthz /readyz /metrics"
- parent-portal §3.1 还引用 `GET /iam/effective-permissions`C4 仲裁已改为 `/iam/permissions/effective`
- parent-portal §3.1 标 "BFF 对接parent-bffai04 设计)"ai04 已过时(现 ai05
此冲突若不解决parent-portalai15会按 REST 实现 frontend client与 parent-bff GraphQL 端点不兼容。
- **建议方案**coord 协调 ai15 将 parent-portal 01/02 文档的 parent-bff 消费契约从 REST 改为 GraphQL`POST /api/v1/parent/graphql`),同步 C4 iam 路径仲裁;此属跨模块契约,按 project_rules §14.4 跨模块变更顺序处理
- **状态**:待 coord 仲裁
### ISSUE-006-ai05proto 包名引用不一致(缺失 next_edu_cloud 前缀)
- **提请方**ai05
- **日期**2026-07-10
- **类型**:契约引用错误
- **描述**02-architecture-design.md §3.3 DTO 映射表引用 proto message 为 `iam.v1.UserInfo``core_edu.v1.Grade[]``analytics.v1.StudentWeakness``msg.v1.Notification[]`。但实际 proto 文件包名均带 `next_edu_cloud.` 前缀:
- iam.proto: `package next_edu_cloud.iam.v1;`
- core_edu.proto: `package next_edu_cloud.core_edu.v1;`
- analytics.proto: `package next_edu_cloud.analytics.v1;`
- msg.proto: `package next_edu_cloud.msg.v1;`
引用不一致会导致 gRPC client 代码生成时 package 路径错误。
- **建议方案**02 §3.3 DTO 映射表 proto message 列全部补 `next_edu_cloud.` 前缀;或确认是否统一去掉前缀(需 buf.yaml 配置一致)
- **状态**:待 coord 仲裁
### ISSUE-007-ai05GraphQL Notification.childId 字段在 msg.proto 缺失
- **提请方**ai05
- **日期**2026-07-10
- **类型**:契约缺口
- **描述**02 §4.2 GraphQL schema 定义 `Notification` type 含 `childId: ID` 字段(家长场景需知道通知关联哪个孩子)。但 msg.proto 的 Notification message 无 childId 字段:
```proto
message Notification {
string id = 1;
string user_id = 2;
string type = 3;
string title = 4;
string content = 5;
string channel = 6;
bool is_read = 7;
int64 created_at = 8;
}
```
parent-bff 无法从 msg 服务获取通知关联的孩子 ID影响"按孩子过滤通知"场景。
- **建议方案**coord 协调 ai10 在 msg.proto Notification message 补 `string child_id = 9;` 字段(可选,非家长通知为空);或 parent-bff 从 notification.content 解析(脆弱,不推荐)
- **状态**:待 coord 仲裁
### ISSUE-008-ai05core_edu.proto 缺 ClassService02 §7.1 列为已有
- **提请方**ai05
- **日期**2026-07-10
- **类型**:契约缺口
- **描述**02 §7.1 交互矩阵列 `core-edu ClassService.GetClass`(查孩子班级信息)状态为 "✅ 已有"。但核查 core_edu.proto 实际只有 ExamService / HomeworkService / GradeService 三个 service无 ClassService。matrix.md §2 却声称 core-edu 有 "ClassService + ExamService + HomeworkService + GradeService + AttendanceService" 共 22 RPC。proto 与 matrix.md 不一致,且 02 错误标注为"已有"。
core_edu.proto 的 Grade.score 是 string 类型02 GraphQL Grade.score 是 Float!string→Float 转换规则未在 §3.3 说明。
- **建议方案**
1. coord 确认 ClassService 归属core-edu 还是 classes 服务),补 proto
2. 02 §7.1 ClassService.GetClass 状态从 "✅ 已有" 改为 "❌ 待补"
3. 02 §3.3 补 Grade.score string→Float 转换规则说明
- **状态**:待 coord 仲裁
### ISSUE-009-ai05ai-allocation iam 责任方与 01/02 文档不一致
- **提请方**ai05
- **日期**2026-07-10
- **类型**:责任方引用过时
- **描述**01-understanding.md §7.1/§7.3 多处提"推动 ai02 在 iam 补接口"02 §8.1 P0 阻塞项 + §14.1 P0-1 也标 iam 责任方为 "ai06iam 现归属)" 但 §7.3 #1 仍标 ai02。实际 ai-allocation.md §3.2 确认 iam 归属 ai06。01 文档未同步。
- **建议方案**01 §7.1/§7.3 将 "ai02" 改为 "ai06"02 §7.3 #1 同步
- **状态**:待 coord 仲裁ai05 建议直接修正,属于引用勘误)
### ISSUE-010-ai0502 缺少 ADR / NFR / 容量规划 / 威胁建模(业界规范差距)
- **提请方**ai05
- **日期**2026-07-10
- **类型**:架构文档规范缺失
- **描述**:对照业界通用架构文档规范(如 C4 model + ADR + NFR02-architecture-design.md 存在以下规范差距:
1. **ADR 缺失**:虽有"已仲裁决策"表,但未按 ADR 格式Context/Decision/Consequences记录关键决策如 GraphQL vs REST、ChildGuard 位置、多子女切换方案)。建议补 ADR 索引章节。
2. **NFR 未量化**§10.4 P6 提"SLO 监控"但文档前部未明确非功能性需求P95 延迟、可用性、吞吐量目标)。业界规范要求架构文档开头列 NFR。
3. **容量规划缺失**:未估算家长端 QPS、并发数、数据量家长数 × 孩子数 × 成绩数),无法指导 HPA 副本数和 Redis 容量规划。
4. **安全威胁建模缺失**§6.2 列错误码但未做威胁建模STRIDE。家长场景涉及未成年人数据COPPA/FERPA/PIPL应补威胁模型。
5. **数据流图DFD缺失**§1.2 只有 Dashboard 时序图,缺少 DFD 展示数据跨信任边界流动。
- **建议方案**coord 确认是否在 02 补全上述章节,或作为 P6 硬化阶段补全;当前不阻塞 P4 实施
- **状态**:待 coord 仲裁
---
<!--
追加条目格式:
@@ -20,5 +177,3 @@
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
(暂无问题)

View File

@@ -6,6 +6,33 @@
---
## §0 已有仲裁核查ai15 复核 ARB-001 / ARB-002 落地情况)
> ai15 接管 parent-portal 后,核查 coord 已发布的两项仲裁ARB-001 teacher-bff GraphQL schema、ARB-002 MF Shell 暴露清单)在 parent-portal 文档中的落地情况。
### 0.1 ARB-001teacher-bff GraphQL schema 第一版)核查
| 核查项 | ARB-001 结论 | parent-portal 落地情况 | 状态 |
| ------ | ------------ | ---------------------- | ---- |
| BFF 用 GraphQL非 REST | ✅ 已裁决 GraphQL | 01-understanding §3.1 + 02-architecture-design §4.1 全部描述为 REST 消费 | ❌ 未落地 |
| ActionState 信封 | ✅ 已裁决 | 01 §3.2 已对齐 | ✅ |
| 错误码前缀路由 | ✅ 已裁决 | 01 §3.2 + 02 §6.2 已对齐 | ✅ |
**结论**ARB-001 的核心裁决BFF = GraphQL在 parent-portal 的 01/02 文档中**未落地**01 §3.1 与 02 §4.1 仍按 REST 编写,与 [parent-bff_contract.md](../contracts/parent-bff_contract.md) §1.3GraphQL 端点 :3010和 [matrix.md](../matrix.md) §3parent-bff GraphQL直接冲突。提请 ISSUE-001。
### 0.2 ARB-002MF Shell 暴露清单)核查
| 核查项 | ARB-002 结论 | parent-portal 落地情况 | 状态 |
| ------ | ------------ | ---------------------- | ---- |
| Shell 暴露 GraphQLProvider | ✅ 已裁决 | 01 §4 技术栈未列 urql/GraphQL client02 §4.1 用 `useApi()`REST ApiClient而非 `useGraphQLClient()` | ❌ 未落地 |
| MF shared 含 urql/graphql/@edu/* | ✅ 已裁决 | 02 §1.2 `shared` 仅列 react/react-dom/@tanstack/react-query/zustand/nuqs缺 urql/graphql/@edu/ui-tokens/@edu/ui-components/@edu/hooks | ❌ 未落地 |
| Shell 暴露 AppShell | ✅ 已裁决 | 01 §9.1 + 02 §7.1 已对齐 | ✅ |
| feature flag NEXT_PUBLIC_MF_ENABLED | ✅ 已裁决 | 01/02 均未提及 | ❌ 未落地 |
**结论**ARB-002 关于 GraphQL client 与 MF shared 的裁决在 parent-portal 文档中**部分未落地**。提请 ISSUE-002。
---
## 问题列表
<!--
@@ -21,4 +48,164 @@
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
(暂无问题)
### ISSUE-001-ai1501/02 文档 REST 消费 parent-bff 与 ARB-001 GraphQL 裁决冲突
- **提请方**ai15
- **日期**2026-07-10
- **类型**:契约不明确(文档与已裁决架构冲突)
- **描述**
- 01-understanding.md §3.1 列出 parent-portal 经 REST 消费 parent-bff`GET /parent/viewports``GET /parent/children``POST /parent/children/:childId/select``GET /parent/notifications``PUT /parent/notification-preferences`
- 02-architecture-design.md §4.1 `useParentApi` 实现全部基于 `api.get()`/`api.post()` REST 调用
- 但 ARB-001coord.md §1已裁决 BFF 用 GraphQL[parent-bff_contract.md](../contracts/parent-bff_contract.md) §1.3 明确 parent-bff 提供 `POST /graphql`:3010[matrix.md](../matrix.md) §3 确认 parent-bff = GraphQL
- parent-portal 自己的 [contract.md](../contracts/parent-portal_contract.md) §2.3-2.4 也写明消费 GraphQL`POST /api/parent/graphql`Query 域currentUser/myChildren/childSummary/childGrades 等)
- **文档内部自相矛盾**01/02 用 RESTcontract.md 用 GraphQL
- **建议方案**
1. coord 确认 parent-portal 消费 parent-bff **统一用 GraphQL**(与 ARB-001、parent-bff contract、matrix.md 一致)
2. ai15 据此修订 01 §3.1(改为 GraphQL Query/Mutation 域、§3.1.1X-Fields 字段裁剪改为 GraphQL query 字段选择、02 §4.1`useParentApi` 改为 GraphQL hooks、§4.2TanStack Query 约定配合 GraphQL operations、§11.3 未决设计决策 #2(移除,已裁决)
3. 若 coord 另有裁决(如 parent-portal 特殊走 REST以 coord 裁决为准
- **状态**:待 coord 仲裁
### ISSUE-002-ai15MF shared 配置缺 urql/graphql/@edu/* 与 ARB-002 冲突
- **提请方**ai15
- **日期**2026-07-10
- **类型**:契约不明确(文档与已裁决 MF 配置冲突)
- **描述**
- 02-architecture-design.md §1.2 MF `shared` 配置仅列:`react``react-dom``@tanstack/react-query``zustand``nuqs`
- ARB-002coord.md §2裁决的 `shared` 应包含:`react``react-dom``urql``graphql``@edu/ui-tokens``@edu/ui-components``@edu/hooks`
- 缺失 `urql`/`graphql` 会导致 Remote 与 Shell 各加载一份 GraphQL client 实例,破坏单例,引发缓存不一致与重复请求
- 缺失 `@edu/*` 会导致设计令牌/UI 组件/Hooks 各加载一份
- 同时 01 §4 技术栈表未列 GraphQL clienturql与 ARB-002 Shell 暴露 GraphQLProvider 矛盾
- **建议方案**
1. coord 确认 parent-portal MF `shared` 必须包含 ARB-002 全部 7 项react/react-dom/urql/graphql/@edu/ui-tokens/@edu/ui-components/@edu/hooks
2. ai15 修订 02 §1.2 `shared` 配置 + 01 §4 技术栈表(新增 urql + GraphQL client 行)
3. 02 §4.1 `useParentApi` 改为从 Shell 暴露的 `useGraphQLClient()` 获取 urql client不再用 REST ApiClient
- **状态**:待 coord 仲裁
### ISSUE-003-ai15switch-child 端点在 01/02 文档间不一致
- **提请方**ai15
- **日期**2026-07-10
- **类型**:契约不明确(文档内部不一致)
- **描述**
- 01-understanding.md §3.1 列 `POST /parent/children/:childId/select`
- 02-architecture-design.md §2.2 + §4.1 用 `POST /api/v1/parent/switch-child`body 携带 childId
- 两处路径与语义均不一致URL param vs body param
- parent-bff contract.md 未列 switch-child其 §1.3 仅列 Query/Mutation 域,未细到 switch-child
- **建议方案**
1. 若走 GraphQL依 ISSUE-001 裁决switch-child 应为 `mutation switchChild(childId: ID!): SwitchChildPayload!`,不存在 REST 路径
2. 若走 REST统一为 `POST /api/v1/parent/switch-child`body 携带 childId与 02 一致),修订 01 §3.1
3. 请 coord 一并明确 parent-bff GraphQL schema 是否包含 `switchChild` Mutation当前 parent-bff contract.md 未列)
- **状态**:待 coord 仲裁
### ISSUE-004-ai15登录端点在 01 / contract.md / matrix.md 间三方不一致
- **提请方**ai15
- **日期**2026-07-10
- **类型**:契约不明确(跨文档不一致)
- **描述**
- 01-understanding.md §3.1 列 `POST /iam/login`
- parent-portal_contract.md §2.3 列 `POST /api/auth/login`
- matrix.md §5 规范 iam 经 api-gateway 代理路径为 `/api/v1/iam/*`
- 三处不一致,且 contract.md 的 `/api/auth/login` 路径在 matrix.md 中不存在
- **建议方案**
1. 统一为 `POST /api/v1/iam/login`(与 matrix.md §5 + 01 §3.1 的 `/api/v1/iam/*` 前缀一致)
2. ai15 修订 contract.md §2.3 路径
3. 注意:登录是 parent-portal 唯一可能走 REST非 GraphQL的端点因登录前无 JWTGraphQL endpoint 需鉴权。请 coord 确认登录是否走 REST `/api/v1/iam/login`,其余走 GraphQL
- **状态**:待 coord 仲裁
### ISSUE-005-ai1502 §11.3 未决设计决策 #2 "GraphQL vs REST" 已由 ARB-001 裁决,应移除
- **提请方**ai15
- **日期**2026-07-10
- **类型**:其他(文档过时)
- **描述**
- 02-architecture-design.md §11.3 第 2 项将 "GraphQL vs REST" 列为未决设计决策,建议 "P4 用 REST后续若 BFF 切 GraphQL 再引入 urql"
- 但 ARB-001coord.md §1已于 2026-07-09 裁决 BFF 用 GraphQL且 parent-bff contract.md 确认 GraphQL
- 此项已过时,会误导后续开发
- **建议方案**
1. coord 确认 ARB-001 适用于 parent-portal即 parent-portal P4 起必须用 GraphQL 消费 parent-bff
2. ai15 移除 02 §11.3 第 2 项,改为 "已裁决:见 ARB-001parent-portal 用 GraphQL 消费 parent-bff"
- **状态**:待 coord 仲裁
### ISSUE-006-ai15contract.md §1.2 将前端页面路由误标为 HTTP 端点
- **提请方**ai15
- **日期**2026-07-10
- **类型**:其他(文档分类错误)
- **描述**
- parent-portal_contract.md §1.2 "HTTP 端点" 列出:`GET /``GET /children``GET /child/:id/summary``GET /child/:id/grades`
- 但 parent-portal 是 Next.js 前端应用,这些是**前端页面路由**SSR/CSR 路由),不是对外 HTTP API 端点
- 将页面路由放在 "HTTP 端点" 表中会误导下游消费方以为这些是 REST API
- 且这些路由与 01 §8 L2 路由表(`/parent/dashboard``/parent/children`路径还不一致contract 用 `/children`01 用 `/parent/children`
- **建议方案**
1. coord 确认 parent-portal 作为前端 Remote不对外提供 HTTP API 端点§1.2 应为"无"
2. ai15 将 contract.md §1.2 改为 "无parent-portal 是前端 Remote不对外提供 HTTP API",页面路由信息保留在 01 §8 L2 路由表中,不进 contract.md
3. 若需保留 MF 暴露信息,归入 §1.6 微前端架构(已有)
- **状态**:待 coord 仲裁
### ISSUE-007-ai15contract.md §1.6 module-federation.config.ts 与 02 next.config.js 不一致
- **提请方**ai15
- **日期**2026-07-10
- **类型**:其他(文档内部不一致)
- **描述**
- parent-portal_contract.md §1.6 列 MF 配置文件为 `apps/parent-portal/module-federation.config.ts`
- 02-architecture-design.md §1.2 MF 配置写在 `next.config.js` 中(用 `NextFederationPlugin`
- 两处文件名与位置不一致
- **建议方案**
1. 统一为 `apps/parent-portal/next.config.js`(与 02 + teacher-portal Shell 一致Next.js 项目 MF 配置应在 next.config.js
2. ai15 修订 contract.md §1.6
- **状态**:待 coord 仲裁
### ISSUE-008-ai15contract.md §2.3 GraphQL 路径前缀与 matrix.md 不一致
- **提请方**ai15
- **日期**2026-07-10
- **类型**:契约不明确(跨文档不一致)
- **描述**
- parent-portal_contract.md §2.3 列 `POST /api/parent/graphql`
- matrix.md §5 规范 api-gateway 代理 parent-bff 路径为 `/api/v1/parent/*`
-`v1` 版本号
- **建议方案**
1. 统一为 `POST /api/v1/parent/graphql`(与 matrix.md §5 一致)
2. ai15 修订 contract.md §2.3 + §2.4
- **状态**:待 coord 仲裁
### ISSUE-009-ai15parent-bff GraphQL schema 是否包含 switchChild Mutation 未明确
- **提请方**ai15
- **日期**2026-07-10
- **类型**:前置依赖缺失(上游契约不全)
- **描述**
- parent-bff_contract.md §1.3 列出的 Query/Mutation 域未包含 "switchChild"(切换当前选中子女)
- 01-understanding.md §2.2 + 02 §2.2 描述 parent-portal 需调用 `POST /parent/switch-child` 切换子女
- 若走 GraphQL依 ISSUE-001parent-bff 需提供 `mutation switchChild(childId: ID!): SwitchChildPayload!`
- 但 parent-bff contract 未列此 Mutation且 iam.GetChildrenByParent 已返回子女列表,切换子女是否需后端记录(还是纯前端 localStorage需明确
- **建议方案**
1. 请 coord 协调 ai05parent-bff确认switchChild 是 GraphQL Mutation 还是纯前端状态localStorage + Zustand
2. 若纯前端01/02 移除 `POST /parent/switch-child` 调用,改为 `useChildSwitcher` 直接写 Zustand + localStorage
3. 若需后端记录:请 ai05 在 parent-bff contract.md §1.3 补充 `switchChild` Mutation
- **状态**:待 coord 仲裁
### ISSUE-010-ai15iam GetChildrenByParent 接口缺失P0 阻塞,跨模块)
- **提请方**ai15
- **日期**2026-07-10
- **类型**:前置依赖缺失(跨模块,承自 parent-bff §7.1
- **描述**
- 01-understanding.md §3.1 注明iam 缺失 "家长-学生关联查询" 接口(`GetChildrenByParent` proto + `GET /iam/children` REST + `iam_student_guardians` 表三缺失)
- parent-bff_contract.md §2.1 也标注 "核心依赖 I3/ISSUE-047 裁决"
- parent-bff_contract.md §3.1 标注 "iam gRPC 50052 启用ai06—— 核心依赖 GetChildrenByParentI3/ISSUE-047 裁决)"
- 此为 parent-portal 多子女场景的 P0 阻塞项ai06iam需在 P3 收尾前补全
- ai15 在此提请,请 coord 跟踪 ai06 进度并确认补全时间点
- **建议方案**
1. coord 确认 ai06 补全 `GetChildrenByParent` 的时间点(应在 P4 启动前)
2. 在补全前parent-portal 用 mock固定 2 个子女开发mock 数据与 parent-bff mock 一致student-001 + student-002
- **状态**:待 coord 仲裁
---
## §1 已裁决问题
(暂无已裁决问题)

View File

@@ -1,24 +1,167 @@
# push-gateway 问题记录
> 负责人ai02
> 关联:[coord.md](../coord.md)、[contracts/push-gateway_contract.md](../contracts/push-gateway_contract.md)
> 关联:[coord.md](../coord.md)、[contracts/push-gateway_contract.md](../contracts/push-gateway_contract.md)、[push-gateway 02 架构设计](../../../services/push-gateway/docs/02-architecture-design.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
---
## 问题列表
## §0 已有仲裁核查ai02 复审 02 文档对总裁裁决的回写情况)
<!--
追加条目格式:
> 本节为 ai02 在批次 0 等待期对 president-final-rulings 已裁决事项的回写核查。
> 裁决来源:[president-final-rulings.md](../../president-final-rulings.md) §1.5 / §3.3 / §4.2 / §4.3 / §4.4 / §7.2 / §3.4
> 核查日期2026-07-10
> 核查结论5 项裁决中 **0 项已完全回写**、**1 项部分回写**、**4 项未回写**
### ISSUE-[编号]-[AI标识][标题]
### 核查矩阵
- **提请方**aiXX
- **日期**YYYY-MM-DD
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**[详细描述问题]
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
| 裁决编号 | 主题 | 裁决要求(摘要) | 02 文档现状 | 核查结论 | 状态 |
| -------- | ---- | ---------------- | ----------- | -------- | ---- |
| ISSUE-053 | Kafka topic 命名 | 02 §5.1 topic 改为 `edu.notification.requested`,禁止抽象名 `edu.*.events` | §5.1 仍写 `edu.notification.events` / `NotificationRequested` | ❌ 未回写 | 待 ai02 修复 |
| ISSUE-055 | /readyz 软失败 | push-gateway /readyz 对 Kafka 软失败(失败仅告警 + `degraded: true` + 返 200不返 503 | §6.7 仅 Redis PING 硬失败返 503无 Kafka 软失败逻辑 | ❌ 未回写 | 待 ai02 修复 |
| ISSUE-056 | 设计决策记录章节 | 02 §5.4 改名为"设计决策记录gRPC vs HTTP 协议选型coord 已采纳 P1",正文标注"coord 已采纳" | 02 无"设计决策记录"章节 | ❌ 未回写 | 待 ai02 修复 |
| ISSUE-058 | Redis SET 启动重建 | 02 §3.1 补充"Hub 启动时遍历内存连接 SADD + EXPIRE 60s + 清空旧 instanceID 成员"/readyz Redis 失败仅告警不阻塞metrics 暴露 `push_gateway_redis_set_rebuild_total`;文档化 60s 不一致窗口 | §3.1/§8.4 仅描述运行期 SADD/SREM无启动重建§6.7 Redis 硬失败返 503与"仅告警不阻塞"冲突);无重建指标;无 60s 窗口说明 | ❌ 未回写 | 待 ai02 修复 |
| ARB /internal/push 契约§4.2 | 第一版 /internal/push 契约 | coord "as-is" 采纳 ai02 02 §4.2 作为第一版契约,仅在 ai10 异议时调整 | 02 §4.2 已定义 `{user_id, event, data, ttl?}``{success, delivered, online}` | ✅ 已落地 | 无需动作 |
(暂无问题)
### 核查结论
- **ISSUE-053/055/056/058 共 4 项须 ai02 在批次 4 启动前回写到 02 文档**president-final-rulings §3.4 明确"批次 4 启动前"完成回写)
- **ARB /internal/push 契约已落地**,无需动作;但 ai10 若提出异议(如 batch 接口/异步回调coord 会公布差异点
- ISSUE-058 中"/readyz Redis 失败仅告警不阻塞"与 ISSUE-055"软失败规则"形成耦合Redis 作为 push-gateway 必需依赖本应硬失败,但 ISSUE-058 裁决要求"仅告警不阻塞"以避免雪崩 —— **此耦合需 coord 明确优先级**(见下方 ISSUE-006-ai02
---
## §1 问题列表(提请 coord 仲裁)
### ISSUE-001-ai02SSE 端点是否提供(跨文档三方冲突)
- **提请方**ai02
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**SSE 端点(`/sse`)在三个文档中存在冲突:
- [push-gateway_contract.md](../contracts/push-gateway_contract.md) §1.2 列出 `GET /sse` 端点,认证 JWT
- [matrix.md](../matrix.md) §5 HTTP 接口矩阵列出 `push-gateway (ai02) | SSE | /sse`
- [02-architecture-design.md](../../../services/push-gateway/docs/02-architecture-design.md) §10 明确建议"不支持 SSEWebSocket 已够用,避免协议膨胀"
- 01-understanding.md 完全未提及 SSE
- 实际代码无 `/sse` 实现,`gin-contrib/sse` 仅为 gin 间接依赖
- **建议方案**:采纳 02 文档建议 —— **push-gateway 不提供 SSE仅 WebSocket**。理由:
1. 单一协议降低维护成本与测试矩阵
2. SSE 单向下行 + 文本协议,不适合未来 reconnect/ack 双向协议
3. 各 portal 已规划 WebSocket 接入([coord-cross-review.md](../../coord-cross-review.md)
- **影响方**ai13/ai14/ai15前端需统一走 WebSocket移除 SSE 兜底、ai10msg 不需调 /sse、coord更新 matrix.md §5 与 contract.md
- **状态**:待 coord 仲裁
### ISSUE-002-ai02内部 API 鉴权命名三方不一致
- **提请方**ai02
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**:内部 API 鉴权头与环境变量在四处不一致:
- **代码**[handler.go#L30](../../../services/push-gateway/internal/ws/handler.go#L30) + [config.go#L41](../../../services/push-gateway/internal/config/config.go#L41)`X-Internal-Key` 头 + `INTERNAL_API_KEY` 环境变量
- **02 文档** §4.2/§6.1`X-Internal-Token` 头 + `INTERNAL_API_TOKEN` 环境变量
- **ai-allocation.md** §5`X-Internal-Key`
- **president-final-rulings.md** §7.2"X-Internal-Token 重命名"(暗示应改为 Token
- **contract.md** §1.2`内网 mTLS`(第四种方案!)
- **建议方案**:统一为 `X-Internal-Token` + `INTERNAL_API_TOKEN`(对齐总裁裁决 §7.2)。理由:
1. 总裁裁决已明确倾向 Token 命名
2. "Token"语义比"Key"更准确(共享密钥而非公私钥对)
3. mTLS 在 P5 阶段引入成本过高,且 K8s 内网已有 NetworkPolicy 隔离,共享密钥足够
- **影响方**ai10msg 调用方需用相同头名、coord更新 contract.md 与 matrix.md §5 移除 mTLS
- **状态**:待 coord 仲裁
### ISSUE-003-ai02单节点容量目标 50k vs 10w+ 冲突
- **提请方**ai02
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**:单节点最大连接数目标在两份文档冲突:
- [modules/push-gateway/README.md](../../../docs/modules/push-gateway/README.md) §7"单节点最大连接数 50k超出时拒绝新连接"
- [02-architecture-design.md](../../../services/push-gateway/docs/02-architecture-design.md) §11"单实例最大连接数 10w+"
- [01-understanding.md](../../../services/push-gateway/docs/01-understanding.md) §5 引用 pending-features"单节点支撑 10w+ 连接"
- **建议方案**:统一为 **10w+**(对齐 02 文档与 pending-features。理由
1. 10w+ 是 P5 设计目标pending-features 权威)
2. Go goroutine-per-connection + 64KB send chan 单连接约 20-30KB10w 连接约 2-3GB单节点可承载
3. 50k 目标过于保守,与横向扩展方案不匹配
- **影响方**coord更新 modules/README.md §7、ai0202 §11 已正确)
- **状态**:待 coord 仲裁
### ISSUE-004-ai02contract.md 内部端点路径 /internal/send vs /internal/push 冲突
- **提请方**ai02
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**:内部单推端点路径在 contract.md 与代码/02 文档冲突:
- [contract.md](../contracts/push-gateway_contract.md) §1.2 + §2.4`POST /internal/send`
- **代码**[main.go#L58](../../../services/push-gateway/main.go#L58)+ **02 文档** §4.2`POST /internal/push`
- 总裁裁决 §4.2 已"as-is 采纳 ai02 02 §4.2",即应使用 `/internal/push`
- **建议方案**contract.md 统一改为 `POST /internal/push`(对齐总裁裁决与代码)。此为 ai02 自主回写范畴,不需 coord 仲裁动作,仅在此登记以便 coord 复核。
- **状态**ai02 自行修复(见 contracts 回写)
### ISSUE-005-ai02审计表6 字段)设计缺失
- **提请方**ai02
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**[ai-allocation.md](../../ai-allocation.md) §5 将"审计表6 字段)"列为 ai02 设计重点,但:
- 01-understanding.md §2 明确"不持有业务状态""无 DB"
- 02-architecture-design.md §3 明确"无数据库。所有状态在内存 + Redis"
- 两份文档均无审计表设计
- **疑问**:审计表是否要求 push-gateway 引入 MySQL/PostgreSQL这与"无 DB"定位冲突。可能的解读:
1. push-gateway 引入轻量审计表(如 SQLite/Redis Stream 持久化推送记录)
2. 审计表由 msg 服务维护msg 已落库push-gateway 仅通过 Kafka 事件回流
3. ai-allocation 表述过度,审计需求由 msg 满足
- **建议方案**:方案 2审计由 msg 维护push-gateway 仅同步返结果)。理由:保持 push-gateway 无 DB 定位,避免引入持久化层增加运维复杂度。
- **影响方**ai10msg 需确认审计字段是否覆盖 push-gateway 推送结果、coord澄清 ai-allocation §5 表述)
- **状态**:待 coord 仲裁
### ISSUE-006-ai02ISSUE-058 与 ISSUE-055 对 Redis /readyz 失败策略耦合冲突
- **提请方**ai02
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**:两份裁决对 push-gateway /readyz Redis 检查失败的策略存在表述冲突:
- **ISSUE-055**[president §3.3](../../president-final-rulings.md)):将 Redis 列为"必需依赖",失败返 503 触发 Pod 重启
- **ISSUE-058**[president §4.3](../../president-final-rulings.md)"/readyz Redis 检查失败时仅告警不阻塞,与 ISSUE-055 协调,避免雪崩"
- **冲突点**Redis 是 push-gateway 跨实例广播的必需依赖(必需 → 503但实例重启不能恢复 Redis 故障,且重启会丢失本地连接表加剧雪崩(应仅告警)
- **建议方案**:明确为 **Redis 软失败**(仅告警 + `degraded: true` + 返 200从 ISSUE-055 必需依赖列表中移除 push-gateway → Redis。理由
1. push-gateway 重启不解决 Redis 故障
2. Redis 故障时单实例仍能服务本地连接(仅跨实例广播失效)
3. 雪崩风险高于短暂不一致
- **影响方**coord澄清两裁决优先级
- **状态**:待 coord 仲裁
### ISSUE-007-ai0202 文档缺 ADR / 非功能性需求 / 失败模式章节(不符业界架构文档规范)
- **提请方**ai02
- **日期**2026-07-10
- **类型**:工作量超批
- **描述**02-architecture-design.md 不符合业界架构文档规范arc42 / C4 模型):
1. **无 ADR 章节**[modules/push-gateway/README.md](../../../docs/modules/push-gateway/README.md) §8 提到"待 P5 交付时补充 ADR 记录",但 02 文档未落地。关键决策gorilla/websocket 选型、Redis Pub/Sub vs Stream、心跳间隔 30s/60s 选型、10w 容量依据)无 ADR
2. **无非功能性需求章节**:无可用性 SLO如 99.9%)、安全合规、容量 SLA
3. **无失败模式/混沌工程章节**实例崩溃、Redis 故障、网络分区、Kafka 消费积压场景下的降级策略缺失
4. **§11 容量表无依据**10w 连接的内存/CPU/网络带宽估算缺失
- **建议方案**:在批次 4P5补全 02 文档 §14-§17 四个章节ADR / 非功能性需求 / 失败模式 / 容量估算。预估工作量1-1.5 天。
- **影响方**ai02自主补全
- **状态**:待 coord 确认是否纳入 P5 Must Have
---
## §2 已自主修复的文档偏差ai02 直接修复,不需 coord 仲裁)
> 以下为 01-understanding.md 与现码不符的偏差ai02 在批次 0 自主修复
| # | 位置 | 偏差 | 修复方向 |
| - | ---- | ---- | -------- |
| 1 | 01 §3.2 | `/readyz` 标"无(待实现)",实际已实现(仅未检查 Redis | 改为"已实现,仅返连接数,待补 Redis PING" |
| 2 | 01 §3.2 | `/metrics` 标"无(待实现)",实际已挂载 promhttp | 改为"已实现,待补自定义指标" |
| 3 | 01 §3.2 | `/internal/push` `/internal/broadcast` 标"待补鉴权",实际已实现 X-Internal-Key | 改为"已实现 X-Internal-Key 校验DevMode 跳过)" |
| 4 | 01 §6 + §7 | Dockerfile 标"❌ 单阶段",实际为多阶段(缺非 root/healthcheck/ldflags | 改为"⚠️ 多阶段但缺非 root + healthcheck + ldflags" |
| 5 | 01 §7.1 #6 | 引用 `main.go L44-46` 行号过期,鉴权状态错误 | 更新行号并改为"已实现" |
| 6 | 01 全文 | 未提及 SSE 端点contract.md/matrix.md 列出但 02 建议不支持) | 待 ISSUE-001 仲裁后补充结论 |
| 7 | 01 全文 | 未提及审计表ai-allocation §5 设计重点) | 待 ISSUE-005 仲裁后补充 |
---
## §3 历史问题
(暂无)

View File

@@ -1,24 +1,221 @@
# student-bff 问题记录
> 负责人ai04
> 关联:[coord.md](../coord.md)、[contracts/student-bff_contract.md](../contracts/student-bff_contract.md)
> 关联:[coord.md](../coord.md)、[contracts/student-bff_contract.md](../contracts/student-bff_contract.md)、[matrix.md](../matrix.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
> 仲裁依据:[coord-final-decisions.md](../../coord-final-decisions.md) §2 BFF 专项裁决B1-B8、[president-final-rulings.md](../../president-final-rulings.md)
---
## 问题列表
## §0 已有仲裁核查总结2026-07-10 审查)
<!--
追加条目格式:
> 本次审查对历史 issues.md 中 ai04 提请的 4 项问题ISSUE-028/029/030/031-ai04逐一核查总裁裁决落地情况。
### ISSUE-[编号]-[AI标识][标题]
| 编号 | 主题 | 总裁裁决章节 | 裁决要点 | 核查结果 |
| ---- | ---- | ------------ | -------- | -------- |
| ISSUE-028-ai04 | 02 文档与 B1+B2 裁决冲突需回写 | president §3.4 | ai04 须在批次 2 启动前回写 student-bff 02B1 GraphQL + B2 gRPC + B8 DownstreamClient | ⚠️ **未执行**02-architecture-design.md 仍为 REST 设计§4 21 个 REST 端点 / §9.2 REST→GraphQL 演进 / §9.3 HTTP→gRPC 演进),违反 B1、B2 |
| ISSUE-029-ai04 | P3 启动前置依赖确认4 项强阻塞) | president §4.1 / §6.1 | 批次 0 完成信号机制 + 批次 2 启动条件:批次 1 P2.1 完成 + ai03 DownstreamClient 抽象就绪 | ✅ **已裁决**:批次时间线 §6.1 明确批次 2 启动条件;前置依赖检查清单机制已建立 |
| ISSUE-030-ai04 | student-bff GraphQL schema 第一版仲裁时机 | president §2.2 | ai04 起草 schema批次 1 等待期coord 在批次 2 启动前仲裁第一版;存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql` | ✅ **已裁决**schema 仲裁机制已建立§2.2);但 schema 第一版尚未起草,需 ai04 在批次 1 等待期产出 |
| ISSUE-031-ai04 | issues.md 编号冲突 | president §0.4 | 保留原始内容不删除,用 `ISSUE-XXX-<提请AI>` 格式唯一定位,不重新编号 | ✅ **已裁决**:编号规则已生效,本文件即按新流程在 objections/ 下维护 |
- **提请方**aiXX
- **日期**YYYY-MM-DD
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**[详细描述问题]
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
### 0.1 核查结论
(暂无问题)
- **唯一未落地项**ISSUE-028-ai0402 文档回写。02-architecture-design.md 当前内容与 B1/B2/B8 裁决严重冲突,须在批次 2 启动前完成回写。
- **schema 第一版未起草**ISSUE-030-ai04 虽已建立仲裁机制,但 ai04 尚未产出 student-bff GraphQL schema 草案,需在批次 1 等待期完成。
- **01-understanding.md 同步问题**:阶段 1 文档同样存在 REST 假设与错误码前缀错误(详见 §2需与 02 文档一并修正。
---
## §1 待 coord 仲裁的新问题(本次审查发现)
### ISSUE-STU-001-ai0401-understanding.md 与 B1/B2/B5 裁决冲突
- **提请方**ai04
- **日期**2026-07-10
- **类型**:裁决冲突(文档未回写)
- **描述**:阶段 1 文档 `services/student-bff/docs/01-understanding.md` 存在 3 项与已裁决规则的冲突:
1. **B1 冲突API 风格)**§3.2 暴露 14 个 REST 端点(`/student/dashboard`§4 / §4.1 明确"先对齐 teacher-bff 现状REST + fetch",建议"P3 阶段先 REST后续统一升级 GraphQL"。与 B1"P2 起直接 GraphQL"冲突,属禁止的"中间过渡方案"。
2. **B2 冲突(下游通信)**§3.1 表述"BFF→Service 走 HTTP fetch当前阶段"§4 技术栈"HTTP fetch当前阶段对齐 teacher-bff 模式)"。与 B2"首次实现即 gRPC 调用下游"冲突。
3. **B5 冲突(错误码前缀)**§3.3 / §6 表格用 `STUDENT_BFF_` 前缀。与 B5"统一 BFF_ 前缀BFF_TEACHER_ / BFF_STUDENT_ / BFF_PARENT_"冲突。
- **建议方案**:与 ISSUE-028-ai04 合并处理01 文档随 02 文档一并回写:
- 删除 REST 端点清单,改为 GraphQL Query/Mutation 清单
- 删除"HTTP fetch 对齐 teacher-bff 现状"表述,改为"gRPC 调用下游(@grpc/grpc-js + @bufbuild/protobuf"
- 错误码前缀统一为 `BFF_STUDENT_`
- §7.2"待 coord 仲裁"项中B1/B2/B3/B5/B6/B7 已裁决,删除重复提请
- **状态**:待 coord 确认回写范围(是否 01 文档也纳入回写义务)
### ISSUE-STU-002-ai0402-architecture-design.md 引用不存在的 004 章节
- **提请方**ai04
- **日期**2026-07-10
- **类型**:契约不明确(文档引用错误)
- **描述**02-architecture-design.md 多处引用"004 §11.4 错误码前缀矩阵"和"004 §11.5 统一响应信封 ActionState",但实际 004_architecture_impact_map.md §11 仅包含:
- §11.1 Protobuf 契约体系
- §11.2 契约规则
- §11.3 BFF 聚合模式
- **不存在 §11.4 和 §11.5**
- **影响**:错误码前缀 BFF_STUDENT_ 的权威来源应改为 [coord-final-decisions.md](../../coord-final-decisions.md) G14 + B5ActionState 信封规范应改为 [coord-final-decisions.md](../../coord-final-decisions.md) G8 + F9
- **建议方案**02 文档回写时修正引用源coord 确认是否需要在 004 补充 §11.4/§11.5 章节,或统一指向 coord-final-decisions
- **状态**:待 coord 仲裁
### ISSUE-STU-003-ai0402 文档 §8.3 列 12 项未决决策,其中 8 项已裁决
- **提请方**ai04
- **日期**2026-07-10
- **类型**:裁决冲突(文档未回写)
- **描述**02-architecture-design.md §8.3"未决设计决策(待 coord 仲裁)"列出 12 项,但其中 8 项已被 coord-final-decisions §2 裁决:
| # | 决策点 | 02 文档建议 | 已裁决结论 | 裁决章节 |
| --- | ------ | ----------- | ---------- | -------- |
| 1 | BFF API 风格 | AP3 REST | **B1P2 起直接 GraphQL** | coord-final-decisions §2 B1 |
| 2 | BFF 是否做权限校验 | A不校验 | **B3BFF 豁免 @RequirePermission** | coord-final-decisions §2 B3 |
| 3 | 自我越权防御 | B | **B4全部 BFF 强制自我越权防御** | coord-final-decisions §2 B4 |
| 4 | /readyz 检查逻辑 | AP3 直接 ok | **G2 + §2.4:按阶段扩展探针,必需依赖失败 503可选依赖软失败** | president §2.4 |
| 5 | Kafka 事件订阅时机 | AP3 不订阅) | **B7P2-P4 不订阅 KafkaP5 后订阅** | coord-final-decisions §2 B7 |
| 6 | 缓存策略 | BRedis 5-30s | **B6Redis 5-30s 短缓存** | coord-final-decisions §2 B6 |
| 8 | 错误码前缀 | BFF_STUDENT_ | **B5BFF_STUDENT_** | coord-final-decisions §2 B5 |
| 9 | DownstreamClient 回写 | B回写 | **B8回写 teacher-bff3 个 BFF 统一** | coord-final-decisions §2 B8 |
仅剩 #7(端口 3009#10CQRS 不引入)、#11SSE 实现)、#12(熔断器引入)属合理的设计决策,但 #12 熔断器 president §2.4 已暗示按阶段评估。
- **建议方案**02 文档回写时删除已裁决项的"待仲裁"标注,改为"已裁决(见 coord-final-decisions §2 BX"
- **状态**:待 coord 确认(与 ISSUE-028-ai04 合并处理)
### ISSUE-STU-004-ai04student-bff GraphQL schema 第一版尚未起草
- **提请方**ai04
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**president §2.2 裁决"ai04 起草 student-bff schema批次 1 等待期coord 在批次 2 启动前仲裁第一版"。当前批次 1 已启动2026-07-10ai04 处于批次 1 等待期,但 schema 草案尚未产出。02-architecture-design.md 仍是 REST 设计,未定义 GraphQL Query/Mutation/Type。
- **影响**:阻塞 ai14student-portalP3 启动(依赖 schema 契约);阻塞 coord 批次 2 启动前仲裁
- **建议方案**ai04 在批次 1 等待期优先产出 student-bff GraphQL schema 草案,存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`,提交 coord 仲裁
- **状态**ai04 自行执行(非 coord 仲裁项),记录待办
---
## §2 01-understanding.md 审查详情
### 2.1 准确性问题
| 位置 | 问题 | 严重度 |
| ---- | ---- | ------ |
| §1 通信方式(入/出) | 表述"HTTP REST当前阶段/ HTTP fetch当前阶段",实际 B1/B2 裁决为 GraphQL + gRPC | 高 |
| §3.1 表格 | 列"当前 REST 端点(实际可用)"列,暗示走 REST但 BFF 是新服务不存在"当前 REST 现状" | 高 |
| §3.2 端点表 | 14 个 REST 端点,违反 B1 GraphQL | 高 |
| §3.3 错误码前缀 | `STUDENT_BFF_` 违反 B5 `BFF_STUDENT_` | 中 |
| §4 技术栈 API 风格 | "HTTP REST当前阶段",违反 B1 | 高 |
| §6 权限装饰器 | "⚠️ 不对齐"结论正确B3 豁免),但理由应补充"已裁决 B3" | 低 |
| §7.2 决策点 1-5 | 列为"待 coord 仲裁",但 B1/B2/B3/B5/B6/B7 均已裁决 | 高 |
### 2.2 遗漏项
| 遗漏内容 | 应补充位置 | 依据 |
| -------- | ---------- | ---- |
| GraphQL schema 设计意图Query/Mutation/Type | §3.2 | B1 + president §2.2 |
| gRPC 下游调用设计(@grpc/grpc-js + @bufbuild/protobuf | §3.1 / §4 | B2 |
| DataLoader 防 N+1 策略 | §4 | 004 §11.3 + B1 |
| B4 自我越权防御userId 强制比对) | §2.3 / §3 | B4 |
| B8 DownstreamClient 抽象(复用 teacher-bff | §4 / §6 | B8 |
| GraphQL schema 存放路径 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql` | §3.2 | president §2.2 |
| GraphQL errors 数组 + extensions.code + extensions.traceId 错误格式 | §3.3 | president §2.2 #5 |
| Relay Cursor Connections 分页规范 | §3.2 | president §2.2 #5 |
---
## §3 02-architecture-design.md 审查详情
### 3.1 架构合理性评估
| 维度 | 评估 | 说明 |
| ---- | ---- | ---- |
| 分层设计§1 | ✅ 合理 | Controller → Service → Aggregator → Cache → DownstreamClient 五层清晰DownstreamClient/Aggregator/Transformer 抽象优于 teacher-bff 现状,符合 B8 |
| 领域模型§2 | ✅ 合理 | "场景聚合视图"概念恰当BFF 无领域模型DataScope=SELF 强制实现§2.3)符合 B4 |
| 缓存设计§3 | ✅ 合理 | Redis Key 规范、TTL 分档、失效策略完整,符合 B6 |
| API 设计§4 | ❌ 严重冲突 | 21 个 REST 端点违反 B1 GraphQL须改为 GraphQL Query/Mutation |
| 事件设计§5 | ⚠️ 部分冲突 | 订阅清单合理,但 B7 裁决 P2-P4 不订阅 Kafka§5 应明确"P5 才落地"且标注 B7 |
| 横切关注点§6 | ✅ 合理 | logger/metrics/tracer/health/优雅关闭对齐黄金模板;错误码 BFF_STUDENT_ 正确§0.2 已修正) |
| 契约矩阵§7 | ⚠️ 需更新 | 下游通信"HTTP→gRPC P3+"表述违反 B2应改为"gRPC 首次实现即用" |
| 风险与假设§8 | ⚠️ 需更新 | §8.3 12 项未决决策中 8 项已裁决(见 ISSUE-STU-003 |
| 演进路线§9 | ❌ 严重冲突 | §9.2 REST→GraphQL、§9.3 HTTP→gRPC 违反"不分阶段"原则president §0.3 |
| 扩展点§10 | ✅ 优秀 | 多端适配/国际化/多角色复用/离线模式/AI 增强/学习路径预留设计前瞻 |
| 性能容量§11 | ✅ 合理 | SLO 分级、容量规划、限流策略完整 |
| 安全合规§12 | ✅ 合理 | 身份认证/授权隔离/输入安全/数据合规/审计完整 |
| 可观测性§13 | ✅ 合理 | 日志规范/span/告警/Grafana 面板完整 |
### 3.2 长远性评估
| 长远性维度 | 评估 | 说明 |
| ---------- | ---- | ---- |
| 多角色复用§9.5 | ✅ | 学习委员/课代表/走读生差异化通过视口扩展,无需改代码 |
| 多端适配§10.1 | ✅ | Transformer 层按 x-client-type 裁剪H5/小程序可扩展 |
| GraphQL 演进§9.2 | ❌ | 规划为"P6+ 可选",但 B1 已裁决 GraphQL 是起点非终点,须移除"可选" |
| 通信协议演进§9.3 | ❌ | 规划"P3 HTTP → P4 gRPC 混合 → P6 Service Mesh",违反 B2"首次实现即 gRPC" |
| 推送通道演进§9.4 | ✅ | P3 无推送 → P5 SSE → P5+ WebSocket → P6+ 移动端推送,渐进合理 |
| AI 答疑增强§10.5 | ✅ | 预留 context 参数,支持多步编排 |
| 国际化§10.2 | ✅ | 预留 I18nContext 接入点 |
### 3.3 业界架构文档规范符合度
| 规范项 | 符合度 | 说明 |
| ------ | ------ | ---- |
| 文档导航/导读 | ✅ | §0.3 文档结构清晰16 章覆盖完整 |
| 设计原则 | ✅ | §0.1 列 P1-P9 九项原则 |
| 架构图C4 模型) | ✅ | §1 物理分层图 + 调用链时序图,符合 C4 Level 2/3 |
| ADR 决策记录 | ⚠️ | §8.3 列决策但未用 ADR 格式,且已裁决项未更新 |
| 非功能性需求 | ✅ | §11 性能 SLO + §12 安全 + §13 可观测性 |
| 演进路线 | ⚠️ | 有 §9 但违反"不分阶段"原则 |
| 实施清单 | ✅ | §14 P3-P6 分阶段清单完整 |
| 风险登记 | ✅ | §8.1 技术风险 + §8.2 外部依赖假设 |
### 3.4 关键遗漏
| 遗漏内容 | 应补充位置 | 依据 |
| -------- | ---------- | ---- |
| GraphQL Schema 完整定义Query/Mutation/Type/Enum | 新增 §4.2 或独立 §5 | B1 + president §2.2 |
| DataLoader 批量策略(哪些 Query 需要 DataLoader | §1.2 或 §6 | 004 §11.3 + B1 |
| gRPC client 设计channel 复用、interceptor、metadata 透传) | §1.2 或 §7 | B2 |
| GraphQL errors 数组扩展 ActionState 字段规范 | §6.2 错误码清单 | president §2.2 #3 |
| Relay Cursor Connections 分页规范 | §4 API 设计 | president §2.2 #5 |
| GraphQL schema 存放路径与 codegen 配置 | §14 实施清单 | president §2.2 #4 |
| AuthorizationGuard 接口设计B4 越权防御 P3 实现方式) | §2.3 或 §6 | president §2.9(参照 teacher-bff ISSUE-033-ai03 |
---
## §4 历史问题归档
> 以下问题原记录在 `docs/issues.md`(旧流程),现按新流程归档至此。总裁裁决详见 [president-final-rulings.md](../../president-final-rulings.md) §10 问题索引表。
### ISSUE-028-ai04ai04 02-architecture-design.md 与 coord B1+B2 裁决冲突需回写
- **提请方**ai04student-bff + parent-bff
- **日期**2026-07-09
- **类型**:裁决冲突(文档回写义务)
- **描述**02-architecture-design.md 与 coord-final-decisions B1P2 起直接 GraphQL+ B2首次实现即 gRPC存在 2 项重大冲突§4 API 设计决策为 REST§9.2 演进路线 REST→GraphQL
- **裁决**president §3.4 — ai04 须在批次 2 启动前回写 student-bff 02B1 GraphQL + B2 gRPC + B8 DownstreamClient
- **状态**:⚠️ 未执行(详见 §0 核查)
### ISSUE-029-ai04ai04 P3 启动前置依赖确认4 项强阻塞)
- **提请方**ai04student-bff + parent-bff
- **日期**2026-07-09
- **类型**:前置依赖未就绪
- **描述**P3 启动依赖 4 项强阻塞前置core_edu.proto 补全 / buf.gen.yaml gRPC 插件 / ai03 DownstreamClient 抽象 / ai08 core-edu gRPC server
- **裁决**president §4.1 / §6.1 — 批次 0 完成信号机制 + 批次 2 启动条件明确
- **状态**:✅ 已裁决
### ISSUE-030-ai04ai04⟷ai14 student-bff GraphQL schema 第一版仲裁时机
- **提请方**ai04student-bff + parent-bff
- **日期**2026-07-09
- **类型**:契约不明确
- **描述**student-bff GraphQL schema 第一版仲裁时机未明确
- **裁决**president §2.2 — ai04 起草 schema批次 1 等待期coord 在批次 2 启动前仲裁第一版
- **状态**:✅ 已裁决schema 第一版待 ai04 起草,见 ISSUE-STU-004
### ISSUE-031-ai04issues.md 编号冲突
- **提请方**ai04student-bff + parent-bff
- **日期**2026-07-09
- **类型**:工作归属不明(文档规范)
- **描述**issues.md ISSUE-024/025 编号冲突ai11 与 ai09 重复)
- **裁决**president §0.4 — 保留原始内容,用 `ISSUE-XXX-<提请AI>` 格式定位,不重新编号
- **状态**:✅ 已裁决

View File

@@ -1,12 +1,208 @@
# student-portal 问题记录
> 负责人ai14
> 关联:[coord.md](../coord.md)、[contracts/student-portal_contract.md](../contracts/student-portal_contract.md)
> 关联:[coord.md](../coord.md)、[contracts/student-portal_contract.md](../contracts/student-portal_contract.md)、[matrix.md](../matrix.md)
> 规则AI 遇到问题时在此追加条目coord 仲裁后更新状态
---
## 问题列表
## §1 已有仲裁核查ARB-001 / ARB-002 对 student-portal 的影响)
### 1.1 ARB-001teacher-bff GraphQL schema 第一版)对 student-portal 的影响核查
| 裁决点 | 对 student-portal 的适用性 | ai14 落实方案 | 状态 |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------ |
| Schema 存放位置 | ✅ 适用原则一致。student-bff GraphQL schema 应存放于 `packages/shared-ts/contracts/graphql/student-bff.graphql`(集中管理,与 teacher-bff 同源) | ai14 在 contract.md §1.3 已标注预期路径;实际由 ai04 创建ai14 仅消费 | ⏳ 待 ai04 |
| P2 Query 范围 | ⚠️ 部分参考。ARB-001 是 teacher-bff 的 P2 范围student-portal 起步于 P3不在 P2因此 student-bff 直接以 P3 全量 Query 起步 | ai14 在 P3 直接消费 student-bff 全量 QuerycurrentUser/myClasses/myExams/myHomework/myGrades/myAttendance/studentDashboard | ✅ 已落实 |
| P2 Mutation 范围 | ⚠️ 部分参考。student-portal P3 起步即需要 submitHomework mutation作业提交是 P3 核心场景) | ai14 P3 即消费 submitHomework mutationai04 P3 必须提供 | ⏳ 待 ai04 |
| DataLoader 防 N+1 | ✅ 适用。student-bff 聚合多 gRPC 时(如 studentDashboard 聚合 iam+core-edu+data-ana必须使用 DataLoader | ai14 不直接实现,但依赖 student-bff 返回结构稳定(无 N+1 慢查询) | ⏳ 待 ai04 |
| 复杂度限制depth ≤ 7 | ✅ 适用。student-portal 发起的 GraphQL query 深度必须 ≤ 7 | ai14 在 [02-architecture-design.md §4.2](../../../apps/student-portal/docs/02-architecture-design.md) 已设计扁平 query | ✅ 已落实 |
| ActionState 信封 | ✅ 适用。student-bff 必须返回 ActionState 信封success/errors/data | ai14 在 GraphQL 请求层02 §3.2)已处理信封解包 + 降级字段识别 | ✅ 已落实 |
| 降级模式(方案 B | ✅ 适用。部分聚合失败时 success=true + data 内 `extensions.degraded: true` | ai14 在 [02 §18.2 降级策略矩阵](../../../apps/student-portal/docs/02-architecture-design.md) 已设计 12 个降级场景 | ✅ 已落实 |
| admin 命名空间 | ❌ 不适用。student-portal 不涉及 admin 命名空间 | - | - |
**核查结论**ARB-001 是 teacher-bff 的 P2 仲裁但其设计原则Schema 集中管理、ActionState 信封、降级模式方案 B、复杂度限制适用于所有 BFFstudent-portal 已在 02-architecture-design.md v2 中全面落实。
---
### 1.2 ARB-002MF Shell 暴露清单)对 student-portal 的影响核查
| 裁决点 | 对 student-portal 的适用性 | ai14 落实方案 | 状态 |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| Shell 身份 | ✅ 适用。teacher-portal 是 MF Shellstudent-portal 是 P3 首个 Remote | ai14 在 [02 §3.2 MF 配置](../../../apps/student-portal/docs/02-architecture-design.md) 已声明 `remotes: { teacher: 'teacher@http://localhost:4000/_next/static/chunks/remoteEntry.js' }` | ✅ 已落实 |
| P3 首个 Remote | ✅ 适用。ARB-002 §2.3 明确 P3 首个 Remote 是 student-portal | ai14 P3 任务启动即接入 MF Remote | ✅ 已落实 |
| GraphQL client 归属 | ✅ 适用。Shell 暴露 GraphQLProviderstudent-portal 复用,**不重复创建 client** | ai14 在 [02 §3.2 GraphQL 请求层](../../../apps/student-portal/docs/02-architecture-design.md) 已使用 `useGraphQLClient()``@edu/hooks` 获取 | ✅ 已落实 |
| MF shared singleton 配置 | ✅ 适用。student-portal 必须将 react/react-dom/urql/graphql/@edu/* 声明为 singleton | ai14 在 [02 §3.2 next.config.js](../../../apps/student-portal/docs/02-architecture-design.md) 已声明全部 singleton | ✅ 已落实 |
| AppShell 复用 | ✅ 适用。student-portal 不重复实现 AppShell复用 Shell 暴露的 AppShell | ai14 在 02 §3.2 已设计 `<AppShell>` 包裹 + 学生端导航覆写 | ✅ 已落实 |
| useAuth / usePermission 复用 | ✅ 适用。student-portal 复用 Shell 暴露的 useAuth/usePermission | ai14 在 [01-understanding.md §6](../../../apps/student-portal/docs/01-understanding.md) 已声明权限校验走 usePermission | ✅ 已落实 |
| ErrorBoundary / Loading / Empty 复用 | ✅ 适用。student-portal 复用 Shell 暴露的共享 UI 组件 | ai14 在 02 §6 组件设计已使用 `@edu/ui-components` | ✅ 已落实 |
| feature flag | ✅ 适用。`NEXT_PUBLIC_MF_ENABLED` 控制是否走 MFP3 默认 true | ai14 在 02 §3.2 已设计独立壳回退MF 关闭时独立渲染) | ✅ 已落实 |
| 登录页 | ✅ 适用。P2 登录页由 Shell 独占student-portal 不实现登录页,未登录跳转 Shell `/login` | ai14 在 02 §3.2 已设计未认证 → 跳转 `window.location.href = 'http://localhost:4000/login?redirect=student'` | ✅ 已落实 |
**核查结论**ARB-002 是 student-portal 接入 MF 的直接依据ai14 已在 02-architecture-design.md v2 中全面落实。无异议。
---
### 1.3 ARB-001 / ARB-002 核查总结
| 仲裁 | 对 student-portal 的影响 | ai14 落实情况 | 异议 |
| ------ | ------------------------ | ------------- | ---- |
| ARB-001 | 设计原则适用GraphQL + ActionState + 降级模式) | ✅ 已落实 | 无 |
| ARB-002 | 直接适用P3 首个 Remote + Shell 暴露清单) | ✅ 已落实 | 无 |
> **ai14 声明**ARB-001 / ARB-002 已在 02-architecture-design.md v2 中全面落实,无需新增仲裁。
---
## §2 新提请异议(待 coord 仲裁)
### ISSUE-014-01-ai14GraphQL endpoint 路径不一致(`/api/student/graphql` vs `/api/v1/student/graphql`
- **提请方**ai14
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- `student-portal_contract.md` §2.3 当前写 `POST /api/student/graphql`(无 `/v1/` 前缀)
- `matrix.md` §5 HTTP 接口矩阵明确写 `api-gateway` 反向代理 `student-portal` 的路径是 `/api/v1/student/*`
- `01-understanding.md` v2 §3.1 和 `02-architecture-design.md` v2 §4.1 已统一为 `POST /api/v1/student/graphql`
- 三处不一致,需要 coord 仲裁统一为 `/api/v1/student/graphql`(与 matrix.md §5 对齐,与 teacher-portal `/api/v1/teacher/graphql` 保持命名一致性)
- **建议方案**
- 统一为 `POST /api/v1/student/graphql`
- 由 ai01api-gateway确认路由`/api/v1/student/*``student-bff:3009/*`(即 `/api/v1/student/graphql``student-bff:3009/graphql`
- 由 ai04student-bff确认 GraphQL endpoint 路径为 `POST /graphql`(与 teacher-bff 一致)
- **状态**:待 coord 仲裁
---
### ISSUE-014-02-ai14student-bff GraphQL schema 文件存放位置不一致(集中管理 vs 应用内管理)
- **提请方**ai14
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- ARB-001 §1.3 关键裁决明确teacher-bff GraphQL schema 存放于 `packages/shared-ts/contracts/graphql/teacher-bff.graphql`(集中管理,总裁裁决 §2.17 SDL-first + 集中管理)
- `student-bff_contract.md` §1.3 写:`apps/student-bff/src/schema/*.graphql`(应用内管理,与 ARB-001 原则不一致)
- `matrix.md` §3 GraphQL 接口提供方矩阵写:`packages/shared-ts/contracts/graphql/student-bff.graphql`(与 ARB-001 一致)
- 两处不一致,需要 coord 仲裁统一
- **建议方案**
- 统一为 `packages/shared-ts/contracts/graphql/student-bff.graphql`(与 ARB-001 原则对齐,集中管理便于前端 codegen
- ai14 在 student-portal 端使用 `graphql-codegen` 从该 schema 生成 TypeScript 类型
- 由 ai04student-bff创建该 schema 文件并维护
- **状态**:待 coord 仲裁
---
### ISSUE-014-03-ai14考试作答页全屏策略与防作弊检测边界
- **提请方**ai14
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- student-portal 02-architecture-design.md §14 设计了防作弊检测visibilitychange/copy/paste/fullscreen/contextmenu但未明确以下边界
1. **全屏 API 强制策略**是否强制全屏Fullscreen API退出全屏是否触发警告/记录?
2. **离开页面策略**visibilitychange hidden 触发时,是仅记录还是自动提交?
3. **多标签检测**BroadcastChannel 检测到多标签时,是警告还是阻止作答?
4. **防作弊事件上报**:前端采集的防作弊事件如何上报?走 student-bff GraphQL mutation 还是 push-gateway WebSocket
- 这些决策影响 ai04student-bff是否需要提供 `recordExamViolation` mutation以及 ai08core-edu是否需要存储违规记录
- **建议方案**
- **全屏策略**P3 推荐但不强制(提示"建议全屏作答"P4 评估是否升级为强制(基于教师反馈)
- **离开页面策略**visibilitychange hidden 触发时仅记录(不自动提交),累计 3 次警告后教师端可见
- **多标签检测**:警告 + 记录,不阻止作答(避免误伤合法场景如查词典)
- **防作弊事件上报**:走 student-bff GraphQL mutation `recordExamViolation(examId, type, payload)`,由 ai04 在 P3 提供
- **状态**:待 coord 仲裁
---
### ISSUE-014-04-ai14主观题粘贴策略防作弊 vs 学生体验)
- **提请方**ai14
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- 02-architecture-design.md §14 防作弊检测包含 `paste` 事件拦截,但学生作答主观题时可能需要粘贴(如从草稿本粘贴长文本)
- 策略不明确:全部禁止粘贴?仅主观题允许?仅客观题禁止?
- 影响学生体验和防作弊效果平衡
- **建议方案**
- **客观题**:禁止粘贴(防作弊优先)
- **主观题(简答/论述)**:允许粘贴,但记录粘贴事件 + 粘贴内容长度,教师端批改时可见
- **作文题**:允许粘贴(学生体验优先),不记录
- 由 ai04student-bff在 P3 提供 `recordPasteEvent` mutation或复用 ISSUE-014-03 的 `recordExamViolation`type=`PASTE`
- **状态**:待 coord 仲裁
---
### ISSUE-014-05-ai14作业附件上传协议GraphQL mutation vs REST multipart
- **提请方**ai14
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- 学生提交作业时可能需要上传附件(图片/PDF/文档GraphQL mutation 不适合处理大文件上传multipart/form-data
- 当前 contract.md 未明确附件上传协议
- 选项:
- A. 走 api-gateway REST 端点(`POST /api/v1/student/upload` → 对象存储),返回 URL再走 GraphQL mutation 提交 URL
- B. 走 student-bff GraphQL multipartgraphql-upload需要 ai04 支持)
- C. 走独立上传服务(如 push-gateway 扩展或新建 upload-service
- **建议方案**
- **推荐 A**:走 api-gateway REST `POST /api/v1/student/upload` → 对象存储MinIO/OSS返回 signed URL再走 GraphQL `submitHomework(attachmentUrls: [String!])` mutation 提交
- 理由GraphQL 不适合大文件传输REST + 对象存储是业界通用方案api-gateway 已有 JWT 鉴权
- 由 ai01api-gateway确认是否提供 `/api/v1/student/upload` 路由,由 ai04student-bff确认 `submitHomework` mutation 是否接受 `attachmentUrls` 字段
- **状态**:待 coord 仲裁
---
### ISSUE-014-06-ai14考试延长/题目重排等实时事件命名未确认
- **提请方**ai14
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- student-portal 02-architecture-design.md §16 设计了 WebSocket 实时通知,但以下事件命名未在 matrix.md §4 Kafka 事件矩阵中确认:
1. **考试延长**(教师延长考试时间):事件名 `ExamExtended`?还是 `ExamUpdated`?由 ai08core-edu发布
2. **题目重排**(教师重排题目顺序):事件名 `ExamQuestionReordered`?是否需要前端实时重排?
3. **考试强制提交**(教师强制收卷):事件名 `ExamForceSubmitted`?前端收到后立即提交?
- 这些事件影响 student-portal 考试作答页的实时响应逻辑
- **建议方案**
- **考试延长**ai08core-edu发布 `ExamExtended` 事件到 `edu.exam.events` topicmsgai10消费后通过 push-gateway 推送student-portal 收到后更新倒计时
- **题目重排**P3 不实现题目顺序固定P4 评估是否需要实时重排
- **考试强制提交**ai08 发布 `ExamForceSubmitted` 事件student-portal 收到后立即触发提交流程
- 由 ai08core-edu确认事件命名由 ai10msg确认推送路径
- **状态**:待 coord 仲裁
---
### ISSUE-014-07-ai14学生端 DataScope L0 边界(仅能查看自己数据)的强制执行层
- **提请方**ai14
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- 01-understanding.md §8 和 02-architecture-design.md §6 声明学生 DataScope L0仅能查看自己数据
- 但强制执行层不明确:
- A. student-bff 在 Resolver 层基于 JWT 的 `x-user-id` 强制过滤(推荐,前端无法绕过)
- B. student-portal 在 GraphQL query 中显式传 `studentId`(不安全,前端可篡改)
- 当前 02-architecture-design.md §4.2 的 GraphQL query 设计中,部分 query 显式传 `studentId`(如 `myClasses(studentId: ID!)`),这与 L0 强制执行矛盾
- **建议方案**
- **统一为方案 A**student-bff 在 Resolver 层从 JWT `x-user-id` 提取 studentId强制过滤前端 query 不传 `studentId` 参数
- ai14 修改 02-architecture-design.md §4.2 的 GraphQL query 定义,移除 `studentId` 参数(如 `myClasses` 改为无参 query
- 由 ai04student-bff确认所有学生端 Query 均从 JWT 提取 studentId不接受前端传入
- **状态**:待 coord 仲裁
---
## §3 异议状态汇总
| 编号 | 类型 | 标题 | 状态 |
| ------------- | ------------ | -------------------------------------------------------- | ------------ |
| ISSUE-014-01 | 契约不明确 | GraphQL endpoint 路径不一致 | 待 coord 仲裁 |
| ISSUE-014-02 | 契约不明确 | student-bff GraphQL schema 存放位置不一致 | 待 coord 仲裁 |
| ISSUE-014-03 | 契约不明确 | 考试作答页全屏策略与防作弊检测边界 | 待 coord 仲裁 |
| ISSUE-014-04 | 契约不明确 | 主观题粘贴策略(防作弊 vs 学生体验) | 待 coord 仲裁 |
| ISSUE-014-05 | 契约不明确 | 作业附件上传协议GraphQL mutation vs REST multipart | 待 coord 仲裁 |
| ISSUE-014-06 | 契约不明确 | 考试延长/题目重排等实时事件命名未确认 | 待 coord 仲裁 |
| ISSUE-014-07 | 契约不明确 | 学生端 DataScope L0 边界的强制执行层 | 待 coord 仲裁 |
---
<!--
追加条目格式:
@@ -20,5 +216,3 @@
- **建议方案**[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
(暂无问题)

View File

@@ -21,4 +21,38 @@
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X
-->
(暂无问题)
### ISSUE-001-ai03admin 命名空间 P2 预留与 ARB-001 "P2 不包含" 冲突
- **提请方**ai03
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**coord.md ARB-001 §1.3 关键裁决表写 "admin 命名空间 | **P2 不包含**P6 admin-portal 阶段新增 `admin` 命名空间";但 president-final-rulings.md §5.1ISSUE-044裁决 "ai03 P2 预留 admin schema 命名空间(如 `admin.*` Query/Mutation",且 §7.3 ai03 工作清单明确 "批次 1P2Must Have 13 项 + DownstreamClient 抽象 + admin schema 命名空间预留"。两处对 P2 admin 命名空间的要求不一致——总裁裁决要求 P2 预留schema 中声明占位coord 仲裁说 P2 不包含。
- **建议方案**:以总裁裁决为准(裁决优先级 president > coord修正 ARB-001 §1.3 为 "P2 预留 admin 命名空间占位schema 中声明 `admin` Query/Mutation 类型骨架,无实际 ResolverP6 admin-portal 阶段实现具体 Resolver"。ai03 在 P2 schema 第一版中预留 `admin` 命名空间类型声明。
- **状态**:待 coord 仲裁
### ISSUE-002-ai03P2 dashboard classes 数据来源与 ARB-001 §1.4 调用链冲突
- **提请方**ai03
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**coord.md ARB-001 §1.4 ai03 执行项第 4 条写 "dashboard Resolver 并行调用 iam3 RPC+ classes1 RPC用 DataLoader 防御 N+1",暗示 P2 dashboard 需 gRPC 调 classes 服务。但存在三重冲突:
1. **classes P2 无 gRPC server**classes.proto 注释 "P1: REST 实现P3 起转 gRPC"matrix.md §2 gRPC 接口提供方矩阵中 core-edu含 classes 合并gRPC 50053 状态为 P3 就绪。classes 服务 P2 阶段未启用 gRPC。
2. **B2 裁决约束**coord-final-decisions.md B2 裁决 "首次实现即 gRPC 调用下游",若 classes P2 无 gRPCteacher-bff 不能用 REST 调 classesB2 禁止 REST 过渡)。
3. **president §3.5 功能范围**president-final-rulings.md §3.5 裁决 P2 实现 "班级列表iam 数据)",即 P2 班级列表数据来自 iam不是 classes 服务。
- **建议方案**:采纳 president §3.5P2 dashboard 的 classes 数据从 iam gRPC 获取iam `GetEffectiveAccess``GetViewports` 返回的关联班级),不调 classes 服务。ARB-001 §1.4 第 4 条修正为 "dashboard Resolver 并行调用 iam gRPCGetUserInfo + GetViewports + GetEffectiveAccessclasses 列表从 iam 返回数据推导P3 core-edu gRPC 就绪后切换为 `GetClassesByTeacher` RPC"。这样 P2 不依赖 classes gRPC与 B2 + president §3.5 一致。
- **状态**:待 coord 仲裁
### ISSUE-003-ai03GraphQL schema 文件命名不一致ARB-001 vs president §2.17
- **提请方**ai03
- **日期**2026-07-10
- **类型**:编号冲突
- **描述**GraphQL schema 文件存放目录两处一致(`packages/shared-ts/contracts/graphql/`),但文件名不一致:
- coord.md ARB-001 §1.3 + §1.4`teacher-bff.graphql`
- president-final-rulings.md §2.17 第 1 条:`teacher-bff.schema.graphql`
ai03 创建文件时无法确定用哪个文件名。
- **建议方案**:以 president §2.17 为准(裁决优先级 president > coord统一为 `teacher-bff.schema.graphql`。coord 修正 ARB-001 §1.3/§1.4 文件名。
- **状态**:待 coord 仲裁
---

View File

@@ -155,6 +155,31 @@
---
### ISSUE-042-ai13仲裁核查 - 01/02 文档未同步已裁决的 GraphQL 架构
- **提请方**ai13
- **日期**2026-07-10
- **类型**:文档同步遗漏(仲裁核查)
- **阶段**P2
- **描述**
- 按"对已有仲裁进行核查"要求,审查 ISSUE-036~041均已裁决在文档中的落地情况
- 核查发现:`apps/teacher-portal/docs/01-understanding.md``02-architecture-design.md` 仍为 ai07 标识 + REST 架构,**未同步**以下已裁决事项:
- F9ISSUE-036P2 起 all-in GraphQL无 REST 过渡 — 01 §1/§3.1 仍写"P2-P3 用 REST 过渡"02 全文基于 RESTApiClient/TanStack Query/契约清单)
- ARB-001ISSUE-037teacher-bff GraphQL schema 第一版 5 Query — 01 §3.1 列 REST 端点02 §4/§10 全 REST
- ARB-002ISSUE-038/039MF Shell 暴露清单GraphQLProvider + hooks + UI 组件 + urql/graphql singleton— 01 未提02 §1.2 exposes 仅 AppShell+shared-deps、P2 配 3 remotes、shared 无 urql
- 总裁 §2.17ISSUE-038GraphQL client 单例方案 A — 02 §11.3.2 仍列"GraphQL vs REST"未决(已裁决)
- 对照已回写的 `03-long-term-architecture.md §1.4`GraphQL 最终方案01/02 严重滞后
- **建议方案**
1. 01-understanding.mdai07→ai13§1 删除 REST 过渡§3.1 REST 端点→GraphQL queriesARB-001§4 技术栈补 urql补 ARB-002 暴露清单
2. 02-architecture-design.mdai07→ai13全文 REST→GraphQL 重写MF 配置对齐 ARB-002API 层改 urql client契约清单改 GraphQL§11.3 未决决策改已决策)
- **状态**:✅ 已回写闭合2026-07-10审查批次
- **coord 裁决**:无需新裁决(复用 ISSUE-036~040 已有裁决),本次为文档同步执行
- **回写执行**
- `01-understanding.md`:已修正 ai07→ai13、§1 REST→GraphQL、§3.1 REST 端点→GraphQL queriesARB-001、§4 补 urql/GraphQL client 技术栈、补 ARB-002 暴露清单、端口对齐、字体令牌描述
- `02-architecture-design.md`:全量重写为 GraphQL 架构§1 MF 图加 GraphQLProvider 层§1.2 MF 配置对齐 ARB-002 exposes/shared/remotes=0§2 领域模型数据源改 GraphQL Query§3 缓存层改 urql cacheExchange§4 API 设计改 urql client 单例 + ARB-001 Query/Mutation§10 契约清单改 GraphQL§11.3 未决决策改已决策表;端口 3000→4000
---
**AI Agent**: ai13teacher-portal
**Branch**: feat/teacher-portal-issues-migrate-ai13
**Coordinator**: coord-ai

View File

@@ -1,45 +1,246 @@
# admin-portal 工作排期
> 负责人ai16
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/admin-portal_contract.md](../contracts/admin-portal_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/admin-portal_contract.md](../contracts/admin-portal_contract.md)、[ai-allocation.md §5 ai16](../../ai-allocation.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
> 说明:本排期基于 GraphQLARB-001 admin 命名空间)+ 端口 4003 + ai16 归属,已对齐 [matrix.md](../matrix.md) 与 ai-allocation.md。01/02 文档中 REST/3003/ai07 表述待 coord 仲裁后修订(见 [objections/admin-portal_issue.md](../objections/admin-portal_issue.md))。
---
## §1 总览
admin-portal 是管理端微前端,通过 MF Remote 接入主应用,覆盖用户管理角色权限、审计日志等场景。全阶段目标P2 MF Remote 骨架 → P3 用户管理+角色权限+审计日志 → P4-P6 持续优化
admin-portal 是管理端微前端MF Remote),挂载到 teacher-portal Shell,覆盖**用户管理 / 角色权限管理 / 学校设置 / 组织管理 / 审计日志消费**等管理场景ai-allocation §5 ai16。复用 teacher-bff GraphQL endpoint 的 **admin 命名空间**ARB-001admin 权限点使用 `ADMIN_` 前缀ai-allocation §5
全阶段目标:
- **P2**MF Remote 骨架next.config.js + MF 配置 + 独立壳渲染 + MSW mock
- **P3**:用户管理 + 角色权限管理admin 命名空间 Query/Mutation
- **P4**:组织管理 + 学校设置 + 班级/教师/学生全局管理
- **P5**审计日志消费auditLogs Query 聚合 iam AuditEvent+ WebSocket 实时通知
- **P6**硬化A11y WCAG 2.2 AA / Web Vitals / OTel / 测试覆盖率 ≥ 80% / Dockerfile 多阶段)
> 跨阶段:开发期间全部经 MSW mock见 [contract §4](../contracts/admin-portal_contract.md)),上游就绪后逐项切换真实。
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
title ai16 admin-portal 全阶段排期
title ai16 admin-portal 全阶段排期P2-P6
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a16a, 2026-07-10, Xd
section P2 骨架
16.1 MF Remote 骨架(next.config+独立壳) :crit, a16a, 2026-07-10, 2d
16.2 MSW handlers+fixtures+mock JWT :a16b, after a16a, 2d
16.3 接入Shell共享(GraphQLProvider/AppShell/useAuth) :crit, a16c, after a16a, 2d
section P3 用户/角色权限
16.4 用户管理页(users CRUD+UserManagementTable) :crit, a16d, after a16c, 3d
16.5 角色权限矩阵(RolePermissionMatrix+updateRolePermissions) :a16e, after a16d, 3d
16.6 权限点管理+视口配置(ADMIN_前缀) :a16f, after a16e, 2d
section P4 组织/学校/全局实体
16.7 组织树管理(organization) :a16g, after a16f, 2d
16.8 学校设置(system) :a16h, after a16g, 2d
16.9 班级/教师/学生全局管理(adminClasses/Teachers/Students) :a16i, after a16h, 3d
section P5 审计日志/实时通知
16.10 审计日志页(auditLogs Query+筛选/导出) :crit, a16j, after a16i, 3d
16.11 WebSocket实时通知(审计告警/异常登录) :a16k, after a16j, 2d
16.12 管理仪表盘(adminDashboard聚合) :a16l, after a16k, 2d
section P6 硬化
16.13 A11y WCAG 2.2 AA审计+修复 :crit, a16m, after a16l, 3d
16.14 Web Vitals+OTel browser SDK接入 :a16n, after a16m, 2d
16.15 Vitest单测+Playwright E2E(≥80%) :crit, a16o, after a16n, 3d
16.16 Dockerfile多阶段+/api/health+/api/ready :a16p, after a16o, 2d
```
> **注意**:以上为 coord 初始规划ai16 接管后必须自行细化为完整 P2-P6 排期
> 关键路径crit16.1 → 16.3 → 16.4 → 16.10 → 16.13 → 16.15。总工期约 34 个工作日
---
## §3 详细任务
### 全阶段任务
### P2 阶段
#### 16.1 MF Remote 骨架next.config + 独立壳)
- **负责人**ai16
- **交付物**:⚠️ 由 ai16 自行补充
- **依赖**:见 [contracts/admin-portal_contract.md](../contracts/admin-portal_contract.md)
- **验收标准**:⚠️ 由 ai16 自行补充
- **依赖**teacher-portal Shell MF exposes/shared 配置就绪ARB-002ai13 P2 交付)
- **交付物**
- `apps/admin-portal/next.config.js`NextFederationPluginRemote 角色,`name: 'admin_app'``filename: 'static/chunks/remoteEntry.js'``exposes: { './AdminApp': './src/app/admin-app.tsx' }`shared 全部 singleton
- `apps/admin-portal/src/app/admin-app.tsx`(独立壳入口,供 Shell 动态加载 + 独立 dev 渲染)
- `apps/admin-portal/src/app/standalone.tsx`(独立 dev 壳:自实现 AppShell 占位 + mock providers`pnpm --filter admin-portal dev` 在 :4003 独立预览)
- `tsconfig.json`(沿用 tsconfig.base.json`tailwind.config.js`(引入 `@edu/ui-tokens`)、`package.json`
- **验收标准**
- `pnpm --filter admin-portal dev` 在 :4003 启动,独立壳渲染首页 + 导航占位
- MF `remoteEntry.js` 可被 Shell `dynamic import` 加载feature flag `NEXT_PUBLIC_MF_ENABLED` 控制)
- shared 单例配置通过react/react-dom/urql/graphql/@edu/* 全 singleton
#### 16.2 MSW handlers + fixtures + mock JWT
- **负责人**ai16
- **依赖**mock 先行)
- **交付物**
- `apps/admin-portal/src/mocks/handlers.ts`(拦截 `POST /api/admin/graphql` + `POST /api/auth/login` + `GET /ws`
- `apps/admin-portal/src/mocks/fixtures/*.json`50 用户 / 5 角色 / 20 班级 / 50 教师 / 1200 学生 / 100 审计日志 / 仪表盘统计)
- mock JWTadmin 角色permissions=["*"]httpOnly cookie
- **验收标准**
- `NEXT_PUBLIC_API_MOCKING=enabled` 时所有请求被 MSW 拦截返回 mock
- GraphQL mock 按 operationName 返回对应 fixture与 teacher-bff admin namespace mock 数据一致)
#### 16.3 接入 Shell 共享GraphQLProvider/AppShell/useAuth
- **负责人**ai16
- **依赖**ARB-002 Shell 暴露清单就绪ai13
- **交付物**
- `AdminApp` 通过 MF `import from 'teacher/GraphQLProvider'``'teacher/AppShell'``'teacher/useAuth'``'teacher/usePermission'``'teacher/useGraphQLClient'`
- `<AppShell scope="admin">` 渲染管理端视口导航
- GraphQL client 经 `useGraphQLClient()` 获取(**不使用 useApi/ApiClient**,对齐 ARB-002
- **验收标准**
- urql client 单例跨 Remote 共享(与 Shell 同一实例)
- `currentUser` Query 返回 mock 管理员信息
- admin 角色校验:非 admin 角色重定向到登录页
### P3 阶段
#### 16.4 用户管理页users CRUD + UserManagementTable
- **负责人**ai16
- **依赖**teacher-bff admin 命名空间 `adminUsers`/`createUser`/`updateUser`/`deleteUser` schemaISSUE-005 待 coord 仲裁 ai03 补齐)
- **交付物**
- `/admin/users` 列表页UserManagementTable邮箱/姓名/角色/状态/数据范围/最后登录 + 筛选/分页/批量操作)
- `/admin/users/new` + `/admin/users/:id` 表单页react-hook-form + zodResolver
- admin 业务 Hooks`useUsers` / `useUser` / `useCreateUser` / `useUpdateUser` / `useToggleUserStatus`urql query/mutation
- **验收标准**
- 列表筛选/分页正常mutation 后 invalidate 刷新
- DataScope L3-L5 越权由后端强制,前端 `AdminDataScopeFilter` 仅作 UI 提示
- 权限:`IAM_USER_READ` / `IAM_USER_CREATE` / `IAM_USER_UPDATE`
#### 16.5 角色权限矩阵RolePermissionMatrix + updateRolePermissions
- **负责人**ai16
- **依赖**teacher-bff `adminRoles` / `updateRolePermissions` schema
- **交付物**
- `/admin/roles`RolePermissionMatrix行=角色,列=权限按 resource 分组checkbox 网格)
- 系统预置角色只读isSystem=true 不可编辑/删除)
- Hooks`useRoles` / `useRole` / `useCreateRole` / `useUpdateRolePermissions`
- **验收标准**
- 勾选触发 `updateRolePermissions` Mutation乐观更新 + 失败回滚
- 删除前校验 userCount > 0 时禁用删除并提示
#### 16.6 权限点管理 + 视口配置ADMIN_ 前缀)
- **负责人**ai16
- **依赖**`packages/contracts/src/permissions.ts`coord 维护admin 权限点 `ADMIN_*` 前缀)
- **交付物**
- `/admin/permissions` 只读列表DataTable按 resource 分组)
- `/admin/viewports` 视口配置编辑器ViewportConfigEditor@dnd-kit 拖拽排序 + 权限绑定 + scope/isVisible
- Hooks`usePermissions`30min 缓存)/ `useViewportsConfig` / `useUpdateViewport`
- **验收标准**
- 权限点常量来自 `@edu/contracts`(不硬编码)
- 视口配置保存后 AppShell 导航即时刷新invalidate viewports queryKey
### P4 阶段
#### 16.7 组织树管理organization
- **交付物**`/admin/organization` 页(树形 + DataTableschool/grade/class 层级 CRUD
- **验收标准**:树形展开/折叠 + 拖拽调整层级(后端校验)+ DataScope 过滤
#### 16.8 学校设置system
- **交付物**`/admin/system` 页(学校基础信息 / 学年学期 / 系统参数表单)
- **验收标准**:表单 zod 校验 + 保存后 toast 反馈;仅 L5 系统管理员可编辑
#### 16.9 班级/教师/学生全局管理
- **交付物**`/admin/classes` / `/admin/teachers` / `/admin/students` 三个全局管理页adminClasses/adminTeachers/adminStudents Query
- **验收标准**:跨班级/跨年级全局视角(区别于 teacher-portal 的教师自身视角DataScope 控制可见范围
### P5 阶段
#### 16.10 审计日志页auditLogs Query + 筛选/导出)
- **负责人**ai16
- **依赖**teacher-bff 消费 `edu.iam.audit.created` Kafka 并暴露 `auditLogs` QueryISSUE-007 待 coord 修正 matrix.md §4 消费方为 teacher-bff
- **交付物**
- `/admin/audit-logs`DataTable时间/操作人/action/resource/ip + 筛选action/user/dateRange + 导出 CSV
- Hooks`useAuditLogs`(含游标分页)
- **验收标准**
- 审计日志经 GraphQL 查询(**非直接订阅 Kafka**,对齐 contract §2.2
- 100 条 mock 审计日志覆盖 create/update/delete/login/logout/permission_change
#### 16.11 WebSocket 实时通知(审计告警/异常登录)
- **交付物**
- 接入 push-gateway `GET /ws`(与 parent-portal 同类契约一致,对齐 ISSUE-006
- 审计告警 / 异常登录 / 系统异常 toast + 通知中心入口
- **验收标准**
- mock-socket 每 30s 推送 1 条 mock 系统通知
- WebSocket 断线自动重连
#### 16.12 管理仪表盘adminDashboard 聚合)
- **交付物**`/admin/dashboard`rechartstotal_teachers / total_students / school_avg_score / 趋势图)
- **验收标准**adminDashboard Query 返回聚合数据60s 轮询监控指标 + 5min 轮询统计
### P6 阶段(硬化)
#### 16.13 A11y WCAG 2.2 AA 审计 + 修复
- **交付物**eslint-plugin-jsx-a11yerror 级)+ 手动审计修复
- **验收标准**0 个 error 级违规
#### 16.14 Web Vitals + OTel browser SDK 接入
- **交付物**`next/web-vitals``POST /api/admin/web-vitals`OTel browser SDK复用 Shell TracerProviderscope='admin'
- **验收标准**LCP/CLS/FID/TTFB 上报 + trace 上报 collector
#### 16.15 Vitest 单测 + Playwright E2E覆盖率 ≥ 80%
- **交付物**`apps/admin-portal/src/**/*.test.tsx` + `e2e/*.spec.ts`
- **验收标准**:覆盖率 ≥ 80%E2E 覆盖登录 → dashboard → 用户 CRUD → 审计日志主链路
#### 16.16 Dockerfile 多阶段 + /api/health + /api/ready
- **交付物**`apps/admin-portal/Dockerfile`builder + runtime非 root+ `src/app/api/health/route.ts` + `src/app/api/ready/route.ts`
- **验收标准**`/api/health` 返回 200`/api/ready` 检查 Shell URL 可达Dockerfile HEALTHCHECK 配置
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai16 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai16 自行补充
### 4.1 我依赖(上游就绪标志
- [ ] teacher-portal Shell MF exposes/shared 就绪ai13ARB-002—— Remote 挂载前提
- [ ] teacher-bff GraphQL :3003 启用ai03—— admin 命名空间可用(**ISSUE-005 待 ai03 补齐 schema**
- [ ] api-gateway HTTP :8080 启用 + JWT 验签 + admin 角色校验ai01
- [ ] iam gRPC 50052 启用ai06—— 用户/角色/审计日志数据来源
- [ ] `edu.iam.audit.created` topic 有事件发布ai06—— 审计日志来源(经 teacher-bff 消费)
- [ ] push-gateway WebSocket :8081/ws 启用ai02—— 实时通知(**ISSUE-006 待 coord 仲裁**
- [ ] `packages/contracts` admin 权限点 `ADMIN_*` 常量就绪coord
### 4.2 我的就绪信号(供下游消费)
- [ ] admin-portal dev server :4003 启用
- [ ] MF Remote 可被 AppShell 加载(暴露 `./AdminApp` 模块)
- [ ] 独立壳渲染(首页 + 导航 + 路由守卫 + admin 角色校验)
- [ ] 登录流程可用(复用 Shell `/login`admin 角色校验后重定向 `/admin/dashboard`)—— **不自行实现登录页**(对齐 ARB-002 §2.3
- [ ] GraphQL 查询可执行currentUser / adminDashboard / auditLogs 返回数据)
- [ ] 用户/角色 CRUD 可执行createUser / updateRolePermissions
- [ ] WebSocket 通知可接收
---
## §5 风险跟踪
| 风险 | 影响 | 缓解 | 状态 |
| ---- | ---- | ---- | ---- |
| teacher-bff admin namespace schema 缺失ISSUE-005 | P3+ 全部业务页阻塞 | 提请 coord 仲裁 ai03 在 P3 启动前补齐P2 用 MSW mock 推进 | ⏳ 待仲裁 |
| 01/02 文档 REST/3003/ai07 与仲裁不一致 | 误导实现 | 已提 7 项异议ISSUE-001~007以 contract.md 为修订基准 | ⏳ 待仲裁 |
| Shell 延迟暴露 GraphQLProvider | P2 骨架阻塞 | P2 用独立壳 + mock providersShell 就绪后切换 | ⏳ |
| MF SSR 对齐复杂 | Remote SSR 上下文依赖 Shell | 优先 CSRSSR 仅首屏 dashboard | ⏳ |

View File

@@ -1,45 +1,264 @@
# ai 工作排期
> 负责人ai12
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/ai_contract.md](../contracts/ai_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/ai_contract.md](../contracts/ai_contract.md)、[objections/ai_issue.md](../objections/ai_issue.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
> 阶段归属:批次 4P5见 [workline.md §1](../workline.md) 甘特图 `b4c: ai12 ai服务 gRPC 50058, after b3a, 13d`
---
## §1 总览
ai 是智能服务,提供 AiService6 个 RPC结合 Elasticsearch 检索与 LLM 网关实现智能问答与推荐。全阶段目标P2 ES 接入+LLM 网关 → P3 AiService 6 RPC → P4-P6 持续优化
ai 是 D6 智能洞察领域的"生成子域"服务Python/FastAPI无状态统一封装 LLM 调用(多 Provider 适配 + 故障切换 + 限流 + 成本控制),提供聊天 / 出题 / 表达优化 / 备课工作流四类 AI 能力。通过 gRPC 查询 content 知识点与 data-ana 学情,通过 Kafka 外发用量计费事件供 data-ana 落 ClickHouse
**端口**HTTP 3008 + gRPC 50058[port-allocation.md](../../../../infra/port-allocation.md) §3/§5 权威源)
**P5 全阶段目标**(退出标准,对应 [pending-features P5](../../../architecture/roadmap/pending-features.md) + ai-allocation §5
1. LLM Provider 适配器模式OpenAI/百川/Anthropic/本地 Ollama统一接口 + 故障切换)
2. SSE / gRPC 流式响应(题目逐字生成 + 前端打字机效果)
3. 出题 Prompt 模板管理YAML + Jinja2模板 CRUD + 参数注入:年级/学科/难度/知识点)
4. 备课工作流 4 步编排(分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库)
5. 用量计费 / 频率限制(按用户 / 按 IP / 按 token / 按学校配额)
6. 生成质量门禁RuleValidator + LLMJudge评估通过率 > 80%
7. 安全层PII 脱敏 + Prompt 注入防御 + 输出内容审核)
---
## §2 全阶段甘特图P2-P6各 AI 自行细化
## §2 全阶段甘特图P513 天
> 对齐 [workline.md §1](../workline.md) 批次 4`ai12 ai服务 gRPC 50058 :b4c, after b3a, 13d`b3a = content P4 就绪后启动)
```mermaid
gantt
title ai12 ai 全阶段排期
title ai12 ai 服务 P5 排期13 天)
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a12a, 2026-07-10, Xd
section M14 基础架构第1-4天
12.1 LLMProvider抽象+4适配器 :crit, a12a, 2026-07-10, 2d
12.2 ProviderFailoverChain+CircuitBreaker :a12b, after a12a, 1d
12.3 gRPC server(Chat+StreamChat)+ActionState整改 :crit, a12c, after a12a, 2d
12.4 Redis多维度限流+Dockerfile多阶段 :a12d, after a12b, 1d
section M15 出题核心第5-9天
12.5 PromptTemplateService+Jinja2渲染 :crit, a12e, after a12c, 2d
12.6 GenerateQuestion+StreamGenerateQuestion逐字流式 :crit, a12f, after a12e, 2d
12.7 RuleValidator+LLMJudge+QualityGate评估三道防线 :a12g, after a12f, 1d
12.8 UsageRecorder+KafkaProducer+QuotaEnforcer :a12h, after a12g, 1d
12.9 PIIRedactor+InputSanitizer+OutputModerator安全层 :a12i, after a12g, 1d
section M16 备课工作流第10-13天
12.10 gRPC client(content/data-ana/iam)+interceptor :crit, a12j, after a12f, 1d
12.11 LessonPreparationWorkflow 4步编排+状态机 :crit, a12k, after a12j, 2d
12.12 WorkflowStateStore(Redis)+教师审核+content入库 :a12l, after a12k, 1d
12.13 集成测试+契约测试+文档同步+arch:scan :a12m, after a12l, 1d
```
> **注意**:以上为 coord 初始规划ai12 接管后必须自行细化为完整 P2-P6 排期。
**关键路径**(红色 critLLMProvider 抽象 → gRPC server → PromptTemplateService → GenerateQuestion → gRPC client → 备课工作流编排
---
## §3 详细任务
### 全阶段任务
### M14 基础架构第1-4天
#### P5-12.1LLMProvider 抽象 + 4 适配器
- **负责人**ai12
- **交付物**:⚠️ 由 ai12 自行补充
- **依赖**:见 [contracts/ai_contract.md](../contracts/ai_contract.md)
- **验收标准**:⚠️ 由 ai12 自行补充
- **依赖**P5 起点)
- **交付物**
- `services/ai/src/ai/providers/base.py``LLMProvider` 抽象接口chat / stream_chat / embed
- `services/ai/src/ai/providers/openai_provider.py`
- `services/ai/src/ai/providers/anthropic_provider.py`
- `services/ai/src/ai/providers/baichuan_provider.py`
- `services/ai/src/ai/providers/local_ollama_provider.py`
- 重构 `llm_client.py` 为基于抽象接口的调用
- **验收标准**
- 4 Provider 切换可用(通过 `llm_model_routing` 配置路由)
- httpx 异步调用,不依赖 openai SDK
- 单元测试覆盖 ≥ 80%(用 MockLLMProvider
- `ruff check src/` 零错误
#### P5-12.2ProviderFailoverChain + CircuitBreaker
- **负责人**ai12
- **依赖**P5-12.1
- **交付物**
- `services/ai/src/ai/providers/failover.py`(按优先级尝试 Provider失败自动切换
- `services/ai/src/ai/providers/circuit_breaker.py`(连续 3 次失败触发熔断 60s
- **验收标准**:单 Provider 故障自动切下一个熔断器状态正确closed/open/half_open
#### P5-12.3gRPC server + ActionState 整改P0 阻塞)
- **负责人**ai12
- **依赖**P5-12.1**前置**ISSUE-03coord 升级 ai.proto 到 v1 完整版)
- **交付物**
- `services/ai/src/ai/grpc_server.py``grpc.aio` server端口 50058
- 实现 `Chat` + `StreamChat` 两个 RPC含流式
- `grpc.aio.ServerInterceptor` 透传 W3C traceparent
- **ActionState 整改**ISSUE-09所有 HTTP 端点 + gRPC RPC 返回值改为 `{success, data, error:{code,message,details,traceId}}`,删除顶层 `degraded` 字段
- **验收标准**
- teacher-bff 可调通 ai gRPC 50058 `Chat` / `StreamChat`(含流式)
- HealthService.Check 返回 SERVING
- 响应信封 004 §11.5 合规
- `pnpm run arch:scan` 更新 arch.db
#### P5-12.4Redis 多维度限流 + Dockerfile 多阶段
- **负责人**ai12
- **依赖**P5-12.2
- **交付物**
- `services/ai/src/ai/middleware/rate_limit.py`Redis 令牌桶user/IP/school 三维度)
- `services/ai/Dockerfile`(多阶段构建,目标镜像 < 200MB
- **验收标准**限流命中准确user 10/min、IP 30/min、school 100/min镜像 < 200MB
---
### M15 出题核心第5-9天
#### P5-12.5PromptTemplateService + Jinja2 渲染
- **负责人**ai12
- **依赖**P5-12.3
- **交付物**
- `services/ai/src/ai/prompts/*.yaml`5+ 模板generate_question / optimize_expression / chat / lesson_plan 等)
- `services/ai/src/ai/services/prompt_template_service.py`(模板注册 + Jinja2 渲染 + CRUD
- HTTP 端点:`GET/POST/PUT /ai/v1/prompts`
- **验收标准**5+ 模板可渲染;变量缺失返回 `AI_PROMPT_RENDER_FAILED`;模板缓存 1h TTL
#### P5-12.6GenerateQuestion + StreamGenerateQuestion 逐字流式
- **负责人**ai12
- **依赖**P5-12.5**前置**ISSUE-03ai.proto 补 `StreamGenerateQuestion` + `GenerateQuestionRequest` 字段扩展)
- **交付物**
- `services/ai/src/ai/services/question_generation_service.py`
- gRPC RPC`GenerateQuestion` + `StreamGenerateQuestion`(题目逐字流式生成)
- HTTP 端点:`POST /ai/v1/generate/question` + `POST /ai/v1/generate/question/stream`
- **验收标准**题目逐字流式返回Pydantic 请求模型完整grade/knowledge_point_ids/question_type/count
#### P5-12.7评估三道防线RuleValidator + LLMJudge + QualityGate
- **负责人**ai12
- **依赖**P5-12.6
- **交付物**
- `services/ai/src/ai/evaluation/rule_validator.py`(题型匹配/答案非空/解析合理/知识点覆盖)
- `services/ai/src/ai/evaluation/llm_judge.py`LLM-as-judge5 维度加权评分)
- `services/ai/src/ai/evaluation/quality_gate.py`(阈值 0.7,不达标重试 < 3 次)
- **验收标准**:评估通过率 > 80%;不达标自动重试;重试耗尽返回 `AI_EVALUATION_FAILED`
#### P5-12.8UsageRecorder + KafkaProducer + QuotaEnforcer
- **负责人**ai12
- **依赖**P5-12.6**前置**ISSUE-02topic 裁决)+ ISSUE-04events.proto 补 AIUsageEvent
- **交付物**
- `services/ai/src/ai/usage/usage_recorder.py`token 消耗统计)
- `services/ai/src/ai/usage/kafka_producer.py``aiokafka` + acks=all + idempotent + transactional_id
- `services/ai/src/ai/usage/quota_enforcer.py`(学校/教师月度配额Redis 计数)
- HTTP 端点:`GET /ai/v1/usage/me` + `GET /ai/v1/usage/school/{id}`
- **验收标准**:用量事件落 data-ana ClickHouse配额超限返回 `AI_QUOTA_EXCEEDED`event_id SETNX 去重
#### P5-12.9安全层PIIRedactor + InputSanitizer + OutputModerator
- **负责人**ai12
- **依赖**P5-12.6
- **交付物**
- `services/ai/src/ai/security/pii_redactor.py`(学生姓名/手机号/身份证/邮箱脱敏)
- `services/ai/src/ai/security/input_sanitizer.py`Prompt 注入防御)
- `services/ai/src/ai/security/output_moderator.py`(敏感词过滤 + 安全校验)
- **验收标准**安全测试通过PII 检出返回 `AI_PII_DETECTED`;注入检出返回 `AI_PROMPT_INJECTION_DETECTED`
---
### M16 备课工作流第10-13天
#### P5-12.10gRPC clientcontent / data-ana / iam+ interceptor
- **负责人**ai12
- **依赖**P5-12.3**前置**content gRPC 50054 就绪ai09 P4、ISSUE-07iam GetEffectiveDataScope P4 补全)
- **交付物**
- `services/ai/src/ai/clients/content_client.py`KnowledgeGraphService.GetPrerequisites / GetLearningPath
- `services/ai/src/ai/clients/data_ana_client.py`AnalyticsService.GetStudentWeakness / GetLearningTrend / GetClassPerformance
- `services/ai/src/ai/clients/iam_client.py`GetEffectiveDataScopeRedis 缓存 5min
- `grpc.aio.ClientInterceptor`trace 注入 + 重试 + 熔断)
- **验收标准**:下游 gRPC 不可达时降级(跳过学情查询 + `degraded:true`DataScope 缓存命中
#### P5-12.11LessonPreparationWorkflow 4 步编排 + 状态机
- **负责人**ai12
- **依赖**P5-12.10**前置**ISSUE-03ai.proto 补 `GenerateLessonPlan` RPC
- **交付物**
- `services/ai/src/ai/services/lesson_preparation_workflow.py`4 步:分析学情 → 推荐知识点 → 生成题目 → 教师审核)
- 状态机实现02-architecture-design.md §2.4Pending→Analyzing→Recommended→Generating→PendingReview→Persisted
- gRPC RPC`GenerateLessonPlan`
- HTTP 端点:`POST /ai/v1/lesson/preparation` + `GET /ai/v1/lesson/preparation/{id}`
- **验收标准**:端到端跑通 4 步;评估未通过自动重试 < 3 次;工作流状态可查询
#### P5-12.12WorkflowStateStore + 教师审核 + content 入库
- **负责人**ai12
- **依赖**P5-12.11**前置**content `QuestionService.CreateQuestions`(待 coord 补 proto见 02 doc §7
- **交付物**
- `services/ai/src/ai/workflow/workflow_state_store.py`Redis 持久化key `ai:workflow:{id}`TTL 24h
- HTTP 端点:`POST /ai/v1/lesson/preparation/{id}/confirm`(教师确认/修改/拒绝)
- 调 content.CreateQuestions 入库
- **验收标准**24h 内工作流可恢复;教师可审核/修改/拒绝入库成功24h 未审核过期
#### P5-12.13:集成测试 + 契约测试 + 文档同步
- **负责人**ai12
- **依赖**P5-12.12
- **交付物**
- `services/ai/tests/`pytest + pytest-asyncio + testcontainers覆盖率 ≥ 80%
- 契约测试pact-pythonai.proto 与 teacher-bff 一致性)
- 更新 `services/ai/README.md` + `docs/troubleshooting/known-issues.md` ai 分区
- `pnpm run arch:scan` 确认 arch.db 已更新
- **验收标准**`ruff check src/` + `pytest` 零错误;覆盖率 ≥ 80%README 含完整架构图
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai12 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai12 自行补充
### 4.1 我依赖的上游就绪标志
| 依赖项 | 提供方 | 就绪标志 | 状态 | 阻塞任务 |
| --------------------------------------------------- | ------------- | ----------------------------------------- | ---- | ------------- |
| ai.proto 升级 v1 完整版6 RPC + 字段扩展) | coord (shared-proto) | proto 文件含 GenerateLessonPlan / StreamGenerateQuestion | ⏳ ISSUE-03 | P5-12.3/12.6/12.11 |
| events.proto 补 AIUsageEvent message | coord (shared-proto) | proto 含 AIUsageEvent | ⏳ ISSUE-04 | P5-12.8 |
| ai 用量事件 topic 命名裁决 | coord | 004 §7.2 补登 | ⏳ ISSUE-02 | P5-12.8 |
| content gRPC 50054 启用 | ai09 (content) | HealthService.Check = SERVING | ⏳ P4 | P5-12.10 |
| data-ana gRPC 50055 启用(可选) | ai11 (data-ana) | HealthService.Check = SERVING | ⏳ P4 | P5-12.10(可降级) |
| iam `GetEffectiveDataScope` RPC P4 补全 | ai06 (iam) + coord | iam.proto 含此 RPC | ⏳ ISSUE-07 | P5-12.10(可降级) |
| LLM Provider API keyOpenAI / 百川 / Ollama | 人类决策者 | 环境变量配置 | — | P5-12.1 |
### 4.2 我的就绪信号(供下游消费)
| 就绪标志 | 消费方 | 状态 |
| ------------------------------------------------- | ---------------------- | ---- |
| ai gRPC 50058 启用HealthService.Check = SERVING | teacher-bff (ai03) | ⏳ |
| AiService.Chat / StreamChat 可调用(含流式) | teacher-bff | ⏳ |
| AiService.GenerateQuestion / StreamGenerateQuestion 可调用 | teacher-bff | ⏳ |
| AiService.GenerateLessonPlan 可调用P5 补全) | teacher-bff | ⏳ |
| AiService.OptimizeExpression 可调用 | teacher-bff | ⏳ |
| ai 用量事件 topic 可发布(供 data-ana 统计) | data-ana (ai11) | ⏳ |
### 4.3 完成信号(批次 4 P5 完成)
ai P5 完成的 5 个标志:
1. ai12ai gRPC 50058 启用 + 6 RPC 全部实现 + HealthService SERVING
2. LLM Provider 4 适配器 + FailoverChain + CircuitBreaker 可用
3. 备课工作流 4 步端到端跑通(含教师审核 + content 入库)
4. 用量事件可发布到 Kafka + data-ana ClickHouse 可落库
5. 端到端teacher-portal 教师 AI 出题 → 流式返回 → 审核入库
---
## §5 风险与缓解
| 风险 | 缓解措施 |
| ----------------------------- | -------------------------------------------------------------------------- |
| coord 未在 P5 启动前补全 protoISSUE-02/03/04 | ai12 先按 02-architecture-design.md §3.3/§4.2 建议 schema 实现proto 落地后对齐 |
| content / iam gRPC 未就绪 | 降级:跳过学情查询 + `degraded:true`;配额降级为仅 user_id 维度 |
| LLM API key 未配置 | 降级骨架响应已具备main.py 当前行为) |
| 工作流状态丢失Redis 故障) | Redis 哨兵P6 硬化)+ 事件日志P6 评估迁移 TemporalISSUE-06 |

View File

@@ -1,57 +1,393 @@
# api-gateway 工作排期
> 负责人ai01
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/api-gateway_contract.md](../contracts/api-gateway_contract.md)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/api-gateway_contract.md](../contracts/api-gateway_contract.md)、[objections/api-gateway_issue.md](../objections/api-gateway_issue.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 依据:[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、[02-architecture-design.md](../../../services/api-gateway/docs/02-architecture-design.md) §9
---
## §1 总览
api-gateway 是 Edu 系统统一入口负责路由、JWT 验签、限流、熔断、CORS。全阶段目标P2 路由+JWT → P3 限流加固 → P4-P6 持续优化
api-gateway 是 Edu 系统统一入口L3 网关层),负责路由转发、JWT RS256 验签、限流、熔断、CORS、可观测性。无业务状态,纯 HTTP 反向代理
**全阶段目标**
- **P2**:路由表 + JWT RS256HTTP JWKS + shared-go 接入 + 错误码 GW_ 前缀 + ActionState 信封 + slog + /readyz 真实检查 + 业务 metrics + tracer 资源属性 + DevMode 防护(遵循 W1-W8 / G1-G17 裁决)
- **P3**路由扩展student-bff :3009+ core-edu 路由
- **P4**路由扩展parent-bff :3010+ content / data-ana 路由
- **P5**路由扩展msg / ai 路由)+ 接入 push-gateway 协作WebSocket 升级透传评估)
- **P6**:限流迁 Redis + per-服务实例熔断评估 + 测试覆盖率 ≥ 80% + 安全加固
**当前状态2026-07-10**P1 已交付classes 域 CRUD 端到端跑通P2 升级未启动。已有仲裁核查发现 6 项未遵循裁决(见 [objections/api-gateway_issue.md](../objections/api-gateway_issue.md) §0.4)。
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
title ai01 api-gateway 全阶段排期
title ai01 api-gateway 全阶段排期P2-P6
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2 基础
路由表+双入口+shared-go :a1a, 2026-07-10, 3d
JWT校验+JWKS fetcher :a1b, after a1a, 3d
限流+熔断+CORS :a1c, after a1b, 2d
section P2 基础升级批次1
P2.0 修复P0遗留 :crit, p2a, 2026-07-10, 1d
P2.1 shared-go接入 :crit, p2b, after p2a, 2d
P2.2 JWT RS256+JWKS :crit, p2c, after p2b, 3d
P2.3 错误码GW_+ActionState :crit, p2d, after p2c, 1d
P2.4 slog+metrics+tracer :crit, p2e, after p2d, 2d
P2.5 /readyz真实检查 :crit, p2f, after p2e, 1d
P2.6 DevMode防护 :p2g, after p2f, 1d
P2.7 路由表扩展iam/teacher :p2h, after p2g, 1d
section P3-P6 持续优化
路由扩展(student/parent/admin) :a1d, after a1c, 2d
指标+链路加固 :a1e, after a1d, 2d
section P3 路由扩展批次2
P3.1 student-bff路由 :p3a, after p2h, 1d
P3.2 core-edu路由 :p3b, after p3a, 1d
P3.3 API版本化/v1迁移 :p3c, after p3b, 2d
section P4 路由扩展批次3
P4.1 parent-bff路由 :p4a, after p3c, 1d
P4.2 content路由 :p4b, after p4a, 1d
P4.3 data-ana路由 :p4c, after p4b, 1d
section P5 路由扩展批次4
P5.1 msg路由 :p5a, after p4c, 1d
P5.2 ai路由 :p5b, after p5a, 1d
P5.3 push-gateway协作评估 :p5c, after p5b, 2d
section P6 硬化批次5
P6.1 限流迁Redis :p6a, after p5c, 3d
P6.2 per-服务熔断评估 :p6b, after p6a, 2d
P6.3 测试覆盖率80% :p6c, after p6b, 3d
P6.4 安全加固 :p6d, after p6c, 2d
```
> **注意**:以上为 coord 初始规划ai01 接管后必须自行细化为完整 P2-P6 排期。
**关键路径**(红色 critP2.0 → P2.1 → P2.2 → P2.3 → P2.4 → P2.5 → P2.6 → P2.7
**总时间线**P2 约 11 天 + P3-P5 约 9 天 + P6 约 10 天 = 约 30 天
---
## §3 详细任务
### P2:路由表 + JWT + 限流
### P2.0:修复 P0 遗留问题
- **负责人**ai01
- **依赖**:无
- **交付物**
- `services/api-gateway/internal/routing/router.go` — 路由表
- `/api/v1/teacher/*` → teacher-bff:3003 代理
- `/api/v1/iam/*` → iam:3002 代理
- JWT RS256 验签shared-go/jwks
- 限流 + 熔断 + CORS
- **依赖**shared-go 骨架(批次 0 已完成)+ iam GetPublicKeyai06
- **验收标准**路由双入口 + JWT 验签 + 限流 + CORS 白名单
- **完整 P3-P6 任务**:⚠️ 由 ai01 自行补充
- `services/api-gateway/go.mod` L3 改为 `go 1.22`(修复 ISSUE-005
- `go.work` L1 改为 `go 1.22`
- 删除 [01-understanding.md](../../../services/api-gateway/docs/01-understanding.md) §7.1 issue #3 死代码引用ISSUE-008
- 修正 [01-understanding.md](../../../services/api-gateway/docs/01-understanding.md) §6 审计表 metrics 行ISSUE-007
- 修正 [README.md](../../../services/api-gateway/README.md) L38 删除不存在的 `/health` 兼容端点描述
- 修正 [proxy.go](../../../services/api-gateway/internal/proxy/proxy.go) L24 删除冗余 `TrimPrefix("/api")`
- **验收标准**`go build ./...` + `go vet ./...` 通过;文档与代码一致
- **对应 ISSUE**ISSUE-005 / ISSUE-007 / ISSUE-008
### P2.1shared-go 包接入
- **负责人**ai01
- **依赖**[packages/shared-go](../../../packages/shared-go/) 已建立(批次 0 已完成coord 仲裁 ISSUE-002 / ISSUE-004
- **交付物**
- `go.work` 增加 `./packages/shared-go`
- `services/api-gateway/go.mod` 增加 `github.com/edu-cloud/shared-go` 依赖
- 按 ISSUE-004 仲裁结果接入 shared-go/loggerzap 或 slog取决于 coord 裁决)
- [tracer.go](../../../services/api-gateway/internal/observability/tracer.go) 评估接入 shared-go/tracer若接口兼容
- [config.go](../../../services/api-gateway/internal/config/config.go) 评估接入 shared-go/env
- **验收标准**`go build ./...` 通过import shared-go 成功logger 输出结构化 JSON
- **对应 ISSUE**ISSUE-002 / ISSUE-004
### P2.2JWT RS256 升级 + JWKS 缓存
- **负责人**ai01
- **依赖**iam (ai06) 暴露 `GET /.well-known/jwks.json` HTTP 端点coord 仲裁 ISSUE-001确认 HTTP JWKS
- **交付物**
- [auth.go](../../../services/api-gateway/internal/middleware/auth.go) 改用 shared-go/jwks.Fetcher`jwks.NewFetcher(cfg.JWKSURL)`
- HS256 逻辑废弃,`cfg.JWTSecret` 仅 DevMode 下用作 mock 密钥
- JWKS 缓存策略TTL 5minshared-go/jwks 默认kid 未命中时强制刷新,刷新失败保留旧公钥
- 启动时同步拉取一次 JWKS失败则 panic 拒绝启动
- claims 增加 `data_scope` 字段提取,注入 `x-data-scope`
- `internal/config/config.go` 增加 `JWKSURL` 字段(环境变量 `IAM_JWKS_URL`
- **验收标准**JWT RS256 验签通过JWKS 缓存命中率达 99%+kid 未命中自动刷新
- **对应裁决**W1错误码加 GW_ 前缀、§2.16HTTP JWKS非 gRPC
### P2.3:错误码 GW_ 前缀 + ActionState 信封
- **负责人**ai01
- **依赖**:无
- **交付物**
- [auth.go](../../../services/api-gateway/internal/middleware/auth.go) 错误码改为 `GW_UNAUTHORIZED` / `GW_INVALID_TOKEN` / `GW_INVALID_CLAIMS`
- [ratelimit.go](../../../services/api-gateway/internal/middleware/ratelimit.go) 响应体改为 `{success:false,error:{code:"GW_RATE_LIMITED",message:"...",retry_after:60}}`
- [circuit-breaker.go](../../../services/api-gateway/internal/middleware/circuit-breaker.go) 响应体改为 `{success:false,error:{code:"GW_CIRCUIT_OPEN",message:"...",retry_after:30}}`
- [recovery.go](../../../services/api-gateway/internal/middleware/recovery.go) 响应体改为 `{success:false,error:{code:"GW_INTERNAL_ERROR",message:"...",request_id:"..."}}`
- `RequestBodyLimit` 超限响应改为 `{success:false,error:{code:"GW_REQUEST_TOO_LARGE",message:"..."}}`
- **验收标准**:所有错误响应符合 ActionState 信封;错误码统一 `GW_` 前缀
- **对应裁决**W1 / W2 / G14
- **对应 ISSUE**ISSUE-009
### P2.4slog + 业务 metrics + tracer 资源属性
- **负责人**ai01
- **依赖**P2.1 shared-go 接入完成
- **交付物**
- 按 ISSUE-004 仲裁结果统一 loggerzap 或 slog
- 所有 `log.Printf` / `log.Println` / `log.Fatal` 改为结构化日志(带 `request_id` / `trace_id` / `user_id` / `method` / `path` / `status` / `latency_ms` 字段)
- 新增 `internal/observability/metrics.go`,注册 7 个业务指标:
- `api_gateway_http_requests_total`Countermethod/endpoint/status
- `api_gateway_http_request_duration_seconds`Histogrammethod/endpoint
- `api_gateway_circuit_breaker_state`Gaugeservice/state
- `api_gateway_rate_limited_total`Counterip
- `api_gateway_proxy_upstream_duration_seconds`Histogramupstream
- `api_gateway_jwks_refresh_total`Counterresult
- `api_gateway_auth_failures_total`Counterreason
- [tracer.go](../../../services/api-gateway/internal/observability/tracer.go) 资源属性补全:`service.name` + `service.version`(编译时注入)+ `deployment.environment`ENV 变量)+ `host.name`
- Metrics 中间件:在 Auth 之后、CircuitBreaker 之前注册(统计通过鉴权的请求)
- **验收标准**`/metrics` 端点返回 7 个业务指标;日志为 JSON 结构化tracer 资源属性完整
- **对应裁决**W3 / W5 / W6 / G4 / G5 / G6
### P2.5/readyz 真实健康检查
- **负责人**ai01
- **依赖**:所有下游服务实现 `/healthz`P2 阶段 iam / teacher-bff 已就绪)
- **交付物**
- [health.go](../../../services/api-gateway/internal/health/health.go) `Readyz` 重构为并行 ping 下游 `/healthz`
- 下游清单从 `cfg.ServicesURL` 动态读取iam / classes / teacher-bff / core-edu / content / msg / ai / data-ana
- 超时 2s任一不可达返回 503 + `{"status":"error","unhealthy":["iam","core-edu"]}`
- 全部可达返回 200 + `{"status":"ok"}`
- 可选依赖软失败规则:未启用 gRPC 的下游P3-P5 阶段未就绪的服务)失败仅告警,返回 200 + `degraded: true`(依据 president-final-rulings.md §3.3
- **验收标准**/readyz 真实检查下游;某服务下线时返回 503
- **对应裁决**W4 / G2
### P2.6DevMode 生产防护
- **负责人**ai01
- **依赖**:无
- **交付物**
- [config.go](../../../services/api-gateway/internal/config/config.go) `Load()` 增加 `ENV` 环境变量读取
-`DevMode=true && ENV=production``panic` 拒绝启动
- 启动日志打印 `ENV` / `DevMode` 状态
- **验收标准**`DEV_MODE=true ENV=production` 启动失败;`DEV_MODE=true ENV=development` 启动成功
- **对应裁决**W7
### P2.7路由表扩展iam / teacher
- **负责人**ai01
- **依赖**coord 仲裁 ISSUE-003API 版本化路由规则iam (ai06) / teacher-bff (ai03) P2 就绪
- **交付物**
- 按 ISSUE-003 仲裁结果更新路由(方案 A/B/C 之一)
- 若方案 Aiam 路由改为 `/api/v1/iam/v1/*path`proxy 透传 `/iam/v1/*path`
- teacher-bff 路由保持 `/api/v1/teacher/*path`
- [02-architecture-design.md](../../../services/api-gateway/docs/02-architecture-design.md) §4.1 路由表同步更新
- **验收标准**:路由表与代码一致;前端调用 `/api/v1/iam/v1/auth/login` 透传到 iam 服务
- **对应裁决**§2.15
---
### P3.1student-bff 路由
- **负责人**ai01
- **依赖**student-bff (ai04) P3 就绪
- **交付物**
- `main.go` 增加 student-bff 路由:`/api/v1/student` + `/api/v1/student/*path``cfg.StudentBffURL`:3009
- `config.go` 增加 `StudentBffURL` 字段(环境变量 `STUDENT_BFF_URL`
- **验收标准**`/api/v1/student/*` 代理到 student-bff:3009
### P3.2core-edu 路由
- **负责人**ai01
- **依赖**core-edu (ai08) P3 就绪classes 服务已合并入 core-eduC1 裁决)
- **交付物**
- `main.go` classes 路由目标改为 core-edu`cfg.ClassesServiceURL``cfg.CoreEduServiceURL`
- 或保留 classes 路由别名proxy 到 core-edu
- exams / homework / grades 路由已在 P1 实现,无需修改
- **验收标准**`/api/v1/classes/*` 代理到 core-edu:3004
### P3.3API 版本化 /v1 迁移
- **负责人**ai01
- **依赖**P2.7 路由规则仲裁结果core-edu (ai08) P3 就绪
- **交付物**
- 按 ISSUE-003 仲裁结果,全服务路由统一加 `/v1` 前缀
- core-edu 路由:`/api/v1/exams/v1/*path` 等(若方案 A
- 更新 [02-architecture-design.md](../../../services/api-gateway/docs/02-architecture-design.md) §4.1 路由表
- **验收标准**:所有业务路由含 `/v1` 版本前缀
---
### P4.1parent-bff 路由
- **负责人**ai01
- **依赖**parent-bff (ai05) P4 就绪
- **交付物**
- `main.go` 增加 parent-bff 路由:`/api/v1/parent` + `/api/v1/parent/*path``cfg.ParentBffURL`:3010
- `config.go` 增加 `ParentBffURL` 字段
- **验收标准**`/api/v1/parent/*` 代理到 parent-bff:3010
### P4.2content 路由
- **负责人**ai01
- **依赖**content (ai09) P4 就绪
- **交付物**
- `main.go` 已有 content 路由P1 实现textbooks / chapters / knowledge-points / questions
- 验证路由目标 `cfg.ContentServiceURL`:3005正确
- 按 P3.3 版本化规则加 `/v1` 前缀
- **验收标准**content 路由可用 + 版本化
### P4.3data-ana 路由
- **负责人**ai01
- **依赖**data-ana (ai11) P4 就绪
- **交付物**
- `main.go` 已有 data-ana 路由P1 实现analytics
- 增加 `dashboard` 路由别名:`/api/v1/dashboard` + `/*path` → data-ana
- 按版本化规则加 `/v1` 前缀
- **验收标准**data-ana 路由可用 + dashboard 别名可用
---
### P5.1msg 路由
- **负责人**ai01
- **依赖**msg (ai10) P5 就绪
- **交付物**
- `main.go` 已有 msg 路由P1 实现notifications
- 增加 `messages` 路由别名:`/api/v1/messages` + `/*path` → msg
- 按版本化规则加 `/v1` 前缀
- **验收标准**msg 路由可用 + messages 别名可用
### P5.2ai 路由
- **负责人**ai01
- **依赖**ai (ai12) P5 就绪
- **交付物**
- `main.go` 已有 ai 路由P1 实现)
- 按版本化规则加 `/v1` 前缀
- **验收标准**ai 路由可用
### P5.3push-gateway 协作评估
- **负责人**ai01
- **依赖**push-gateway (ai02) P5 就绪
- **交付物**
- 评估 WebSocket 升级请求是否需要 Gateway 透传到 push-gateway
- 若需要:增加 `/ws` + `/sse` 路由透传到 push-gateway:8081
- 若不需要:文档说明 WebSocket 直连 push-gateway不经过 Gateway
- 更新 [02-architecture-design.md](../../../services/api-gateway/docs/02-architecture-design.md) §7 交互点清单
- **验收标准**WebSocket 推送链路可用(透传或直连)
---
### P6.1:限流迁 Redis
- **负责人**ai01
- **依赖**Redis 基础设施就绪
- **交付物**
- [ratelimit.go](../../../services/api-gateway/internal/middleware/ratelimit.go) 改用 Redis 令牌桶(`github.com/go-redis/redis_rate/v10`
- 支持多副本一致限流
- 增加用户级限流(基于 `x-user-id` 头)
- 登录接口额外加用户级限流(防爆破)
- 保留 DevMode 下内存令牌桶回退
- **验收标准**:多副本部署时限流一致;用户级限流生效
### P6.2per-服务实例熔断评估
- **负责人**ai01
- **依赖**P6.1 完成
- **交付物**
- 评估是否将共享 `downstream` 熔断器拆分为 per-服务实例iam / core-edu / teacher-bff 等)
- 若拆分:每个下游服务独立熔断状态,互不影响
- 若不拆分:保持 W8 裁决现状,文档说明理由
- 按 W8 裁决,此项 P6 单独评估,不强制拆分
- **验收标准**:评估报告 + 决策记录
### P6.3:测试覆盖率 80%
- **负责人**ai01
- **依赖**P2-P5 全部完成
- **交付物**
- 补全 [auth_test.go](../../../services/api-gateway/internal/middleware/auth_test.go)JWKS 验签 / claims 解析 / DevMode 旁路 / 公开路径白名单
- 补全 [cors_test.go](../../../services/api-gateway/internal/middleware/cors_test.go):白名单匹配 / 预检请求 / Vary 头
- 补全 [security_test.go](../../../services/api-gateway/internal/middleware/security_test.go):安全头设置 / Server 头移除
- 补全 [recovery_test.go](../../../services/api-gateway/internal/middleware/recovery_test.go)panic 捕获 / request_id 生成 / ActionState 信封
- 补全 [requestid_test.go](../../../services/api-gateway/internal/middleware/requestid_test.go):透传 / 生成 / 响应头
- 补全 [proxy_test.go](../../../services/api-gateway/internal/proxy/proxy_test.go):路径前缀去除 / Host 改写
- 补全 [health_test.go](../../../services/api-gateway/internal/health/health_test.go)/readyz 下游检查 / 软失败规则
- **验收标准**`go test ./... -cover` 覆盖率 ≥ 80%
### P6.4:安全加固
- **负责人**ai01
- **依赖**P6.1-P6.3 完成
- **交付物**
- 评估 IP 黑名单 / WAF 规则是否在 Gateway 层实现(建议在 Istio 层做,本服务不介入)
- 限流策略表 per-路由细化02 §4.2
- 熔断阈值 per-服务配置02 §4.3
- CORS 白名单生产环境强制配置(禁止 `*`
- 审计日志:记录所有 401 / 403 / 429 / 503 响应
- **验收标准**:安全扫描通过;审计日志可追溯
---
## §4 依赖与就绪信号
- **我依赖**iam GetPublicKey RPCai06
- **我的就绪信号**api-gateway :8080 可访问 + JWT 验签可用
### 4.1 我依赖的上游就绪标志
| 上游 | 就绪标志 | 阶段 | 状态 |
| ---- | -------- | ---- | ---- |
| coord | shared-go 包骨架tracer/logger/jwks/env| 批次 0 | ✅ 已完成 |
| coord | ISSUE-001 仲裁JWKS vs gRPC| 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 签发 | P2 | ⏳ |
| iam (ai06) | gRPC 50052 启用(不影响 GatewayGateway 走 HTTP| P2 | ⏳ |
| teacher-bff (ai03) | `POST /graphql` :3003 启用 | P2 | ⏳ |
| student-bff (ai04) | `POST /graphql` :3009 启用 | P3 | ⏳ |
| core-edu (ai08) | gRPC 50053 启用 + classes 合并 | P3 | ⏳ |
| parent-bff (ai05) | `POST /graphql` :3010 启用 | P4 | ⏳ |
| content (ai09) | gRPC 50054 启用 | P4 | ⏳ |
| data-ana (ai11) | gRPC 50055 启用 | P4 | ⏳ |
| msg (ai10) | gRPC 50056 启用 | P5 | ⏳ |
| ai (ai12) | gRPC 50058 启用 | P5 | ⏳ |
| push-gateway (ai02) | :8081 启用 + /internal/push | P5 | ⏳ |
### 4.2 我的就绪标志(供下游消费)
| 阶段 | 就绪标志 | 状态 |
| ---- | -------- | ---- |
| P2 | api-gateway :8080 可访问 + JWT RS256 验签可用 + 7 个业务指标 + /readyz 真实检查 | ⏳ |
| P2 | /api/v1/iam/* + /api/v1/teacher/* 路由可用 | ⏳ |
| P3 | /api/v1/student/* + /api/v1/exams/* 等路由可用 | ⏳ |
| P4 | /api/v1/parent/* + /api/v1/textbooks/* + /api/v1/analytics/* 路由可用 | ⏳ |
| P5 | /api/v1/notifications/* + /api/v1/ai/* 路由可用 | ⏳ |
| P6 | 限流迁 Redis + 测试覆盖率 ≥ 80% | ⏳ |
---
## §5 风险与应对
| 风险 | 概率 | 影响 | 应对 |
| ---- | ---- | ---- | ---- |
| coord 仲裁延期ISSUE-001/002/003/004| 中 | P2 阻塞 | ai01 先按 HTTP JWKS + shared-go + 方案 A 推进,仲裁后调整 |
| iam JWKS 端点延期 | 中 | P2.2 阻塞 | DevMode 下用本地 mock RS256 公钥(与 mock 私钥配对) |
| shared-go 接口不兼容 | 低 | P2.1 阻塞 | ai01 自行适配,或反馈 coord 修改 shared-go |
| 下游服务未实现 /healthz | 中 | /readyz 误报 | 软失败规则:未就绪服务失败仅告警,返回 200 + degraded |
| JWKS 缓存过期时 iam 不可达 | 低 | 全量 401 | fail-open 1 次后 fail-close监控 `jwks_refresh_total` 指标 |
| DevMode 旁路误开到生产 | 低 | 鉴权绕过 | P2.6 生产防护W7 裁决) |
---
## §6 与其他模块的协作
| 模块 | 协作内容 | 时机 |
| ---- | -------- | ---- |
| iam (ai06) | JWKS HTTP 端点 + JWT RS256 签发 | P2 |
| teacher-bff (ai03) | 反向代理 :3003 GraphQL | P2 |
| student-bff (ai04) | 反向代理 :3009 GraphQL | P3 |
| parent-bff (ai05) | 反向代理 :3010 GraphQL | P4 |
| core-edu (ai08) | 反向代理 :3004 + classes 合并 | P3 |
| content (ai09) | 反向代理 :3005 | P4 |
| data-ana (ai11) | 反向代理 :3006 | P4 |
| msg (ai10) | 反向代理 :3007 | P5 |
| ai (ai12) | 反向代理 :3008 | P5 |
| push-gateway (ai02) | WebSocket 升级透传评估 | P5 |
| coord | shared-go 包维护 + ISSUE 仲裁 | 持续 |

View File

@@ -1,45 +1,359 @@
# content 工作排期
> 负责人ai09
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/content_contract.md](../contracts/content_contract.md)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/content_contract.md](../contracts/content_contract.md)、[objections/content_issue.md](../objections/content_issue.md)、[../../services/content/docs/02-architecture-design.md](../../../services/content/docs/02-architecture-design.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 当前分支:`feat-review-content-module-docs-WAIyMA`
---
## §1 总览
content 是内容服务,提供 TextbookService、ChapterService、KnowledgeGraphService、QuestionService结合 Neo4j 知识图谱与 Elasticsearch 全文检索。全阶段目标P2 服务骨架+Neo4j+ES → P3 四大 Service 实现 → P4-P6 持续优化
content 是内容资源中台服务P4 阶段),承载 D4 内容资源限界上下文,提供 Textbook / Chapter / KnowledgePoint / Question 四个聚合的 CRUD 与知识图谱查询
**关键交付**
- gRPC 50054 + 4 ServiceTextbook/Chapter/KnowledgeGraph/Question按 [coord-final-decisions.md §3.3](../../coord-final-decisions.md) N1/N3/N5 仲裁P4 首次实现即启用 gRPC + 补全 QuestionService/ChapterService proto
- MySQL 写模型 + Neo4j 知识图谱 + Outbox 事件驱动异步同步(禁止业务事务内同步双写 Neo4j
- Kafka 发布 `edu.content.knowledge_point.events` / `edu.content.question.events`(聚合 topic 策略,待 ISSUE-002 仲裁)
- P5 引入 Elasticsearch 全文检索 + AI 出题入库QuestionService.BatchCreateQuestions
- P6+ 长远演进:教材版本管理 / 跨租户内容共享 / 个性化学习路径推荐
**关键路径位置**:批次 3P4依赖批次 2 core-edu 完成实际可并行content 不强依赖 core-edu
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P4-P6
```mermaid
gantt
title ai09 content 全阶段排期
title ai09 content 全阶段排期P4-P6
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a9a, 2026-07-10, Xd
section P4 基础设施
P4.1 schema 迁移补字段 :crit, c4a, 2026-07-19, 2d
P4.2 Outbox 表+Publisher worker :crit, c4b, after c4a, 3d
P4.3 Kafka producer(idempotent+txn) :crit, c4c, after c4b, 2d
P4.4 Neo4j Sync Worker(异步) :crit, c4d, after c4c, 2d
P4.5 重构 kp.service 移除同步双写 :crit, c4e, after c4d, 1d
section P4 gRPC 契约
P4.6 content.proto 补 ChapterService/QuestionService :crit, c4f, 2026-07-19, 1d
P4.7 gRPC controller 实现(4 Service) :crit, c4g, after c4f, 4d
P4.8 buf generate + 类型校验 :c4h, after c4g, 1d
section P4 横切与质量
P4.9 /readyz 多依赖(DB/Neo4j/Kafka) :c4i, after c4e, 1d
P4.10 ZodError GlobalErrorFilter 分支 :c4j, after c4i, 1d
P4.11 DB 改 getDb()+ID 改 cuid2 :c4k, after c4j, 1d
P4.12 Repository 抽象补齐 :c4l, after c4k, 2d
P4.13 单元测试(Service/Repository)≥60% :c4m, after c4l, 3d
P4.14 修正 README 与实现对齐 :c4n, after c4m, 1d
section P5 ES+AI 集成
P5.1 引入 @elastic/elasticsearch :crit, c5a, after c4m, 1d
P5.2 ES mapping+ensureIndex :crit, c5b, after c5a, 1d
P5.3 ES Sync Worker(消费事件同步索引) :crit, c5c, after c5b, 2d
P5.4 GET /questions/search 检索 API :crit, c5d, after c5c, 2d
P5.5 QuestionService gRPC 完善Publish/Search :c5e, after c5d, 1d
P5.6 AI 出题 BatchCreateQuestions 联调 :c5f, after c5e, 2d
P5.7 检索性能优化(<200ms) :c5g, after c5f, 2d
P5.8 测试覆盖率≥80% :c5h, after c5g, 2d
section P6+ 演进
P6.1 Question 审核工作流状态机 :c6a, after c5h, 3d
P6.2 知识图谱可视化 API :c6b, after c6a, 3d
P6.3 教材版本管理 :c6c, after c6b, 2d
P6.4 /readyz 硬化+监控告警完善 :c6d, after c6c, 2d
```
> **注意**:以上为 coord 初始规划ai09 接管后必须自行细化为完整 P2-P6 排期。
**预估总工期**P4 约 21 天 + P5 约 13 天 + P6+ 约 10 天 = **44 天**(与 workline.md §1 批次 3+4 时间窗口一致)
---
## §3 详细任务
### 阶段任务
### 3.1 P4 阶段任务
#### P4.1 schema 迁移补字段
- **负责人**ai09
- **交付物**:⚠️ 由 ai09 自行补充
- **依赖**:见 [contracts/content_contract.md](../contracts/content_contract.md)
- **验收标准**:⚠️ 由 ai09 自行补充
- **依赖**:无(自身 schema 现状)
- **交付物**
- [textbooks.schema.ts](../../../services/content/src/textbooks/textbooks.schema.ts) textbooks 表补 `status` / `tenant_id` / `metadata` 字段
- chapters 表补 `created_at` / `updated_at` / `status`(解决 [01-understanding.md](../../../services/content/docs/01-understanding.md) C7
- knowledge_points 表补 `difficulty` / `metadata` / `created_at` / `updated_at`(解决 ISSUE-009
- questions 表补 `status` / `source` / `created_by`NULL 起步,解决 ISSUE-007/ `metadata`
- **验收标准**`pnpm typecheck` 通过Drizzle 类型重新生成;迁移脚本可幂等执行
#### P4.2 Outbox 表 + Publisher worker
- **负责人**ai09
- **依赖**P4.1
- **交付物**
- 新建 `src/shared/outbox/outbox.schema.ts`content_outbox_events 表,见 design doc §3.1.5
- 新建 `src/shared/outbox/outbox.publisher.ts`(轮询 PENDING 事件投递 Kafka指数退避重试
- 新建 `src/shared/outbox/outbox.module.ts`
- **验收标准**:业务事务内写 questions + outbox 同事务提交Publisher worker 独立轮询retry_count 累加正确
#### P4.3 Kafka produceridempotent + transactionalId
- **负责人**ai09
- **依赖**P4.2
- **交付物**
- 新建 `src/shared/kafka/producer.ts`kafkajs 客户端idempotent=truetransactionalId=content-producer
- 新建 `src/shared/kafka/kafka.module.ts`
- package.json 添加 kafkajs 依赖
- **验收标准**producer 启动成功transactionalId 唯一;幂等投递无重复
#### P4.4 Neo4j Sync Worker异步同步
- **负责人**ai09
- **依赖**P4.3
- **交付物**
- 新建 `src/shared/sync/neo4j-sync.worker.ts`(消费 content 自身 Outbox 事件,异步创建/更新 Neo4j 节点与关系)
- 消费 `KnowledgePointCreated` / `KnowledgePointPrerequisiteAdded` 等事件
- **验收标准**MySQL 写知识点后Neo4j 节点最终一致出现(延迟 < 2sNeo4j 故障时事件不丢失,恢复后补齐
#### P4.5 重构 knowledge-points.service.ts 移除同步双写
- **负责人**ai09
- **依赖**P4.4
- **交付物**
- [knowledge-points.service.ts](../../../services/content/src/knowledge-points/knowledge-points.service.ts) 删除 `safeCreateNode` 同步写 Neo4j 逻辑
- 改为发 Outbox 事件 `KnowledgePointCreated`
- `addPrerequisite` 改为发 Outbox 事件 `KnowledgePointPrerequisiteAdded`
- **验收标准**:业务事务内不再直接写 Neo4jNeo4j 写入全部走异步 Sync Worker解决 ISSUE-010 + 01-understanding C9
#### P4.6 content.proto 补 ChapterService / QuestionService
- **负责人**ai09
- **依赖**:无(自身 proto 现状)
- **交付物**
- [content.proto](../../../packages/shared-proto/proto/content.proto) 补 ChapterServiceCreateChapter/ListChapters/GetChapter/UpdateChapter/DeleteChapter
- 补 QuestionServiceCreateQuestion/BatchCreateQuestions/GetQuestion/ListQuestions/UpdateQuestion/DeleteQuestion/PublishQuestion/SearchQuestions
- 补 TextbookService.UpdateTextbook / DeleteTextbook
- 补全 message 定义Chapter / Question / QuestionRequest 等)
- 同步补 events.proto 的 KnowledgePointEvent / QuestionEvent / TextbookEvent / ChapterEvent待 ISSUE-002 仲裁后定)
- **验收标准**`buf lint` 通过;`buf breaking` 无破坏性变更(新增字段 OKcontract.md 与 design doc §4.2 RPC 数对齐
#### P4.7 gRPC controller 实现4 Service
- **负责人**ai09
- **依赖**P4.6
- **交付物**
- 新建 `src/textbooks/textbooks.grpc.controller.ts`
- 新建 `src/chapters/chapters.grpc.controller.ts`
- 新建 `src/knowledge-points/knowledge-points.grpc.controller.ts`
- 新建 `src/questions/questions.grpc.controller.ts`
- main.ts 启用 gRPC server 50054
- **验收标准**`grpcurl` 调用 4 Service 全部 RPC 返回正确HealthService.Check 返回 SERVING解决 N1
#### P4.8 buf generate + 类型校验
- **负责人**ai09
- **依赖**P4.7
- **交付物**`pnpm buf:generate` 生成 TS 类型content 服务引用生成类型
- **验收标准**`pnpm typecheck` 通过
#### P4.9 /readyz 多依赖检查
- **负责人**ai09
- **依赖**P4.5
- **交付物**[health.controller.ts](../../../services/content/src/shared/health/health.controller.ts) 改造 /readyz检查 DB / Neo4j / Kafka producer / Kafka consumer lag
- **验收标准**:返回 design doc §6.6 格式Neo4j 不可用 → status=degradedDB 不可用 → status=down解决 N2 + 01-understanding C-section readyz 问题)
#### P4.10 ZodError GlobalErrorFilter 分支
- **负责人**ai09
- **依赖**:无
- **交付物**[global-error.filter.ts](../../../services/content/src/shared/errors/global-error.filter.ts) 增加 ZodError 识别分支,返回 400 + 字段级错误详情
- **验收标准**Zod 校验失败返回 design doc §4.3 错误结构
#### P4.11 DB 改 getDb() + ID 改 cuid2
- **负责人**ai09
- **依赖**:无
- **交付物**
- [database.ts](../../../services/content/src/config/database.ts) 改为 `getDb()` 函数式懒加载(对齐 classes 黄金模板)
- service 层 `randomUUID()` 改为 `cuid2()`package.json 添加 @paralleldrive/cuid2
- **验收标准**:所有 service 使用 getDb();所有 ID 生成用 cuid2
#### P4.12 Repository 抽象补齐
- **负责人**ai09
- **依赖**P4.11
- **交付物**:补齐 textbooks/questions 的 Repository 抽象(与 chapters/knowledge-points 一致)
- **验收标准**Service 层不直接调用 Drizzle API全部走 Repository
#### P4.13 单元测试 ≥ 60%
- **负责人**ai09
- **依赖**P4.12
- **交付物**
- 新建 `*.spec.ts` 覆盖 Service 层 + Repository 层
- 重点覆盖 QuestionsService 题型校验 / KnowledgePointsService 前置依赖 / Outbox Publisher 重试逻辑
- **验收标准**`pnpm test` 通过;覆盖率 ≥ 60%
#### P4.14 修正 README 与实现对齐
- **负责人**ai09
- **依赖**P4.13
- **交付物**[README.md](../../../services/content/README.md) 修正 `TextbooksService.createKnowledgeGraph` 错误描述(实际在 KnowledgePointsService补齐 4 个领域模块说明
- **验收标准**README 与源码完全一致(解决 01-understanding C3
### 3.2 P5 阶段任务
#### P5.1 引入 @elastic/elasticsearch
- **负责人**ai09
- **依赖**P4 全部完成
- **交付物**package.json 添加 @elastic/elasticsearch;新建 `src/config/elasticsearch.ts`
- **验收标准**esClient 单例ES_URL 未配置时 esClient=null 降级
#### P5.2 ES mapping + ensureIndex
- **负责人**ai09
- **依赖**P5.1
- **交付物**:按 design doc §3.3.1 实现 questions 索引 mapping启动时 `ensureIndex` 幂等
- **验收标准**索引创建成功ik_max_word / ik_smart 分词器配置正确
#### P5.3 ES Sync Worker
- **负责人**ai09
- **依赖**P5.2
- **交付物**:新建 `src/shared/sync/es-sync.worker.ts`,消费 `QuestionCreated` / `QuestionUpdated` / `QuestionPublished` / `QuestionDeleted` 事件增量更新索引
- **验收标准**MySQL 写题目后ES 索引最终一致(延迟 < 2s
#### P5.4 GET /questions/search 检索 API
- **负责人**ai09
- **依赖**P5.3
- **交付物**[questions.controller.ts](../../../services/content/src/questions/questions.controller.ts) 增加 `@Get("search")` 端点ES 查询支持 q / type / difficulty / knowledgePointId 过滤
- **验收标准**:检索延迟 < 200msP5 退出标准);返回分页结构
#### P5.5 QuestionService gRPC 完善 Publish/Search
- **负责人**ai09
- **依赖**P5.4
- **交付物**gRPC controller 补 PublishQuestion / SearchQuestions RPC 实现
- **验收标准**:与 contract.md §1.1 完全对齐(解决 ISSUE-004 部分)
#### P5.6 AI 出题 BatchCreateQuestions 联调
- **负责人**ai09
- **依赖**P5.5 + ai12 ai 服务就绪
- **交付物**:与 ai12 联调 BatchCreateQuestions RPC服务账号权限校验
- **验收标准**AI 服务调用成功入库batch_size ≤ 100 限制生效
#### P5.7 检索性能优化
- **负责人**ai09
- **依赖**P5.6
- **交付物**ES 查询 DSL 优化缓存热点查询结果Redis
- **验收标准**P95 延迟 < 200ms
#### P5.8 测试覆盖率 ≥ 80%
- **负责人**ai09
- **依赖**P5.7
- **交付物**补集成测试gRPC + ES + Neo4j 端到端)
- **验收标准**:覆盖率 ≥ 80%
### 3.3 P6+ 演进任务
#### P6.1 Question 审核工作流状态机
- **负责人**ai09
- **依赖**P5 完成
- **交付物**Question.status 状态机完整实现draft → pending_review → published/rejected → archived审核日志表
- **验收标准**:状态转换校验正确;非法转换返回 409
#### P6.2 知识图谱可视化 API
- **负责人**ai09
- **依赖**P6.1
- **交付物**`GET /knowledge-graph/visualization` 返回 nodes/edges 结构(解决 01-understanding L3
- **验收标准**:返回 D3.js / vis.js 可消费的图结构
#### P6.3 教材版本管理
- **负责人**ai09
- **依赖**P6.2
- **交付物**Textbook.version 字段启用;版本切换不破坏题库引用
- **验收标准**:新旧版本教材并存;题库引用按版本隔离
#### P6.4 /readyz 硬化 + 监控告警完善
- **负责人**ai09
- **依赖**P6.3
- **交付物**/readyz 探针列表完善Prometheus 告警规则补齐consumer lag / outbox pending / ES latency
- **验收标准**:告警阈值合理;故障演练通过
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai09 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai09 自行补充
### 4.1 我依赖的上游就绪标志
| 上游 | 就绪标志 | 必需性 | mock 策略 |
| -------------- | ----------------------------------------------------- | ---------------------- | ------------------------------------------ |
| infra | MySQL 8 / Neo4j 5 / Kafka 集群可用 | 🔴 必需 | 本地 docker-compose |
| infra | Redis 可用P5+ 缓存用env.ts 已预留 REDIS_URL | 🟢 P5+ 可选 | 未配置时跳过缓存 |
| infra | Elasticsearch 8 可用P5+ | 🟢 P5+ 必需 | 未配置时检索降级到 MySQL LIKE |
| shared-proto | content.proto / events.proto 补全 | 🔴 必需P4.6 自身完成) | 自行修改 |
| core-edu (ai08) | gRPC 50053 启用 | 🟢 可选content 独立) | 不依赖 core-edu 实时数据 |
| ai (ai12) | gRPC 50058 启用 | 🟢 P5 联调时必需 | grpc-mock 拦截 |
### 4.2 我的就绪标志(供下游消费)
- [ ] **P4 就绪**
- [ ] content gRPC 50054 启用HealthService.Check 返回 SERVING
- [ ] 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 可调用Create/BatchCreate/Get/List/Update/Delete/Publish/Search
- [ ] edu.content.knowledge_point.events / edu.content.question.events topic 可发布
- [ ] /readyz 返回 DB/Neo4j/Kafka 三依赖状态
- [ ] **P5 就绪**
- [ ] GET /questions/search 检索 API 可用(延迟 < 200ms
- [ ] QuestionService.SearchQuestions gRPC 可调用
- [ ] BatchCreateQuestions 与 ai12 联调通过
- [ ] **P6+ 就绪**
- [ ] 审核工作流状态机完整
- [ ] 知识图谱可视化 API 可用
### 4.3 我提供的 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
- QuestionService.BatchCreateQuestions 返回成功 + 生成 20 个 ID
- **Kafka mock**content 就绪前不发布真实事件,下游使用本地 stub
---
## §5 风险与缓解
| 风险 | 阶段 | 缓解措施 |
| ------------------------------------------ | ---- | ------------------------------------------------------------ |
| ISSUE-002 topic 策略未仲裁导致 Outbox 阻塞 | P4 | 优先推动 coord 仲裁;开发期间用 stub topic仲裁后切换 |
| ISSUE-004 RPC 数量未仲裁导致 proto 阻塞 | P4 | 优先推动 coord 仲裁;按建议方案 21 RPC 实现,仲裁后调整 |
| Neo4j 与 MySQL 双向一致性 | P4 | Outbox 事件驱动 + 幂等去重 + consumer lag 监控 |
| ES 索引重建期间检索不可用 | P5 | alias 切换模式(双索引蓝绿) |
| AI 批量出题 CreateQuestions 高并发 | P5 | batch_size ≤ 100 + 异步队列 + 限流 |
| schema 迁移期间历史数据 created_by 缺失 | P4 | 按 ISSUE-007 方案NULL 起步或 'system' 默认值回填 |
---
## §6 与 workline.md §1 对齐
- 批次 3P4ai09 content 11 天workline.md §1 排期 `after b2b, 11d`
- 本排期 P4 实际 21 天(含测试 + README 修正),与 workline.md 11 天存在差距
- **差距原因**workline.md §1 为 coord 初始规划(标注"ai09 接管后必须自行细化"),本文件为 ai09 细化后的实际排期
- **同步动作**:提请 coord 在 workline.md §1 更新 content 工期为 21 天(或协调压缩测试任务)

View File

@@ -1,45 +1,370 @@
# core-edu 工作排期
> 负责人ai08
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/core-edu_contract.md](../contracts/core-edu_contract.md)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/core-edu_contract.md](../contracts/core-edu_contract.md)、[services/core-edu/docs/02-architecture-design.md](../../../services/core-edu/docs/02-architecture-design.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 批次归属:批次 2P3 核心教学),关键路径
---
## §1 总览
core-edu 是教学核心服务,提供 ClassService、ExamService、HomeworkService、GradeService、AttendanceService并基于 Outbox 模式发布领域事件。全阶段目标P2 服务骨架+Outbox → P3 五大 Service 实现 → P4-P6 持续优化
core-edu 是教学核心服务,承载 D2 教学组织classes+ D3 教学核心exams / homework / grades / attendance / schedule两个限界上下文
**全阶段目标**
| 阶段 | 目标 | 就绪信号 |
| ---- | ---- | -------- |
| P2已部分完成 | 服务骨架 + Outbox 模式 + REST CRUD + 三支柱可观测 | HTTP 3004 可访问 + /healthz + /readyzDB 探针) |
| P3核心 | gRPC 50053 启用 + 5 Service 全量 RPC + 状态机 + Outbox 事件全量 + 排课考勤 + 成绩计算配置化 + Temporal 试点 | gRPC 50053 + 27 RPC + HealthService SERVING |
| P4 | 持续优化 + 消费 data-ana mastery 事件 + content gRPC 调用(知识点关联) | — |
| P5 | 配合 msg 服务事件消费联调 + AI 辅助批改预留 | — |
| P6 | /readyz 硬化 + Outbox relay 迁独立 Go 服务评估 + 多租户行级隔离 | — |
**当前状态2026-07-10**
- ✅ P2 骨架已就绪exams/homework/grades 三域 REST CRUD + Outbox + 三支柱)
- ⏳ P3 待启动(依赖 ISSUE-001 ~ ISSUE-006 仲裁结果)
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
title ai08 core-edu 全阶段排期
title ai08 core-edu 全阶段排期P2-P6
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a8a, 2026-07-10, Xd
section P2 骨架(已就绪)
P2 服务骨架+Outbox+REST CRUD :done, p2, 2026-07-01, 7d
section P3 核心教学批次2 关键路径)
P3.0 等待 coord 仲裁 ISSUE-001~006 :crit, p3a, 2026-07-10, 2d
P3.1 黄金模板对齐Drizzle getDb+Zod+kafka logger+/readyz探针 :crit, p3b, after p3a, 2d
P3.2 TOPIC_MAP 重命名 edu.teaching.* + payload schema_version/event_id :crit, p3c, after p3b, 1d
P3.3 考试状态机+作业状态机+成绩幂等 :crit, p3d, after p3c, 3d
P3.4 gRPC server 50053 启用+5 Service 27 RPC :crit, p3e, after p3d, 3d
P3.5 排课考勤数据模型+AttendanceService :p3f, after p3e, 2d
P3.6 成绩计算配置化grade_formulas+GradeCalculator :p3g, after p3e, 2d
P3.7 作业提交 Redis 分布式锁 :p3h, after p3e, 1d
P3.8 DataScope 下推Repository WHERE 注入) :p3i, after p3e, 1d
P3.9 消费 IAM 事件user.created/updated/deleted :p3j, after p3e, 1d
P3.10 Temporal 工作流试点(考试发布编排) :p3k, after p3e, 2d
P3.11 classes 服务合并到 core-edu :p3l, after p3e, 2d
P3.12 测试覆盖率≥80%+Dockerfile多阶段核对 :p3m, after p3l, 2d
section P4 持续优化
P4.1 消费 data-ana mastery.updated 事件 :p4a, after p3m, 2d
P4.2 content gRPC 调用(知识点关联) :p4b, after p4a, 2d
P4.3 读模型双轨读策略验证MySQL+ClickHouse :p4c, after p4b, 1d
section P5 联调
P5.1 msg 事件消费联调(通知触发) :p5a, after p4c, 2d
P5.2 AI 辅助批改接口预留GetExam/ListGradesByExam :p5b, after p5a, 1d
section P6 硬化
P6.1 /readyz 硬化Kafka+Redis+Temporal 探针) :p6a, after p5b, 1d
P6.2 Outbox relay 迁独立 Go 服务评估 :p6b, after p6a, 2d
P6.3 多租户行级隔离验证 :p6c, after p6b, 1d
```
> **注意**:以上为 coord 初始规划ai08 接管后必须自行细化为完整 P2-P6 排期。
**关键路径**P3.0(仲裁)→ P3.1(模板对齐)→ P3.2TOPIC_MAP→ P3.3(状态机)→ P3.4gRPC→ P3.5/P3.6/P3.7/P3.8/P3.9/P3.10/P3.11(并行)→ P3.12(测试)
**预计工期**
- P313 天(含 2 天等待仲裁)
- P45 天
- P53 天
- P64 天
- 合计25 天P3 后)
---
## §3 详细任务
### 全阶段任务
### P3.0 等待 coord 仲裁 ISSUE-001 ~ ISSUE-006
- **负责人**ai08等待 coord
- **依赖**:无
- **交付物**coord 出具仲裁结论(见 [coord.md](../coord.md) 后续章节)
- **验收标准**6 项 ISSUE 全部裁决,状态更新为"已裁决"
- **阻塞说明**
- ISSUE-001proto 实际状态):阻塞 P3.4 gRPC 实现
- ISSUE-002events.proto 未同步):阻塞 P3.2 TOPIC_MAP 重命名
- ISSUE-003状态命名不一致阻塞 P3.3 状态机实现
- ISSUE-004class.transferred topic阻塞 P3.11 classes 合并
- ISSUE-005RPC 数量口径):阻塞就绪信号声明
- ISSUE-0067 项设计决策):阻塞 P3 全部实施
### P3.1 黄金模板对齐P0
- **负责人**ai08
- **交付物**:⚠️ 由 ai08 自行补充
- **依赖**:见 [contracts/core-edu_contract.md](../contracts/core-edu_contract.md)
- **验收标准**:⚠️ 由 ai08 自行补充
- **依赖**:无(可与其他 P3 任务并行)
- **交付物**
1. `src/config/database.ts` 改为 `getDb()` 函数式(对齐 classes 黄金模板)
2. `src/config/kafka.ts` L20/L22 `console.log`/`console.warn``logger.info`/`logger.warn`
3. 全部 Controller 接入 Zod ValidationPipeexams/homework/grades schema 已存在,需接入到 Controller
4. `src/shared/health/health.controller.ts` /readyz 补 Redis ping + Kafka producer 连接探针
- **验收标准**
- `pnpm run lint` + `pnpm run typecheck` 零错误
- /readyz 返回 3 项依赖状态MySQL + Redis + Kafka
- kafka.ts 无 console.* 调用
### P3.2 TOPIC_MAP 重命名 + payload 补字段P0
- **负责人**ai08
- **依赖**ISSUE-002 仲裁events.proto 同步)
- **交付物**
1. `src/shared/outbox/outbox.publisher.ts` TOPIC_MAP 改为 `edu.teaching.<aggregate>.<action>` 风格:
- `edu.exam.events``edu.teaching.exam.created` / `.updated` / `.deleted` / `.published` / `.submitted`
- `edu.homework.events``edu.teaching.homework.assigned` / `.submitted` / `.graded`
- `edu.grade.events``edu.teaching.grade.recorded` / `.updated`
- `edu.class.events` → 按 ISSUE-004 仲裁结果
- 新增 `edu.teaching.attendance.recorded`
2. outbox payload 补 `schema_version`(默认 `"v1"`+ `event_id`UUID+ `occurred_at`(业务时间戳)+ `metadata: { traceId, userId }`
3. `src/shared/outbox/outbox.schema.ts``event_id``occurred_at``next_retry_at` 字段 + `uniq_event_id` 唯一索引
- **验收标准**
- TOPIC_MAP 命名符合 coord §3.1 仲裁
- outbox 表新增字段已迁移
- payload JSON 含 schema_version + event_id + occurred_at + metadata
### P3.3 考试/作业状态机 + 成绩幂等P1
- **负责人**ai08
- **依赖**ISSUE-003 仲裁(状态命名统一)
- **交付物**
1. `src/exams/domain/exam-state-machine.ts`纯函数canTransition / transition
2. `src/homework/domain/homework-state-machine.ts`(纯函数)
3. `src/exams/exams.service.ts` 接入状态机PublishExam / StartExam / SubmitExam / GradeExam / ArchiveExam
4. `src/homework/homework.service.ts` 接入状态机SubmitHomework / GradeHomework
5. `src/grades/grades.schema.ts``idempotency_key` 字段 + `uniq_student_exam` / `uniq_student_hw` / `uniq_idempotency` 唯一索引
6. `src/grades/grades.service.ts` 实现幂等录入(先 SELECT 检查,再 INSERT
- **验收标准**
- 状态机非法转换抛 `CORE_EDU_EXAM_INVALID_STATUS_TRANSITION`409
- 同一学生同一考试/作业重复录入返回已存在成绩(幂等)
- 状态命名按 ISSUE-003 仲裁结果统一
### P3.4 gRPC server 启用 + 5 Service 27 RPCP1
- **负责人**ai08
- **依赖**ISSUE-001 仲裁proto 补全、ISSUE-005 仲裁RPC 统计口径)
- **交付物**
1. `src/main.ts` 启用 gRPC server端口 50053`@grpc/grpc-js` + `@bufbuild/protobuf`
2. `src/exams/exams.grpc-controller.ts`ExamService 8 RPCCreateExam / GetExam / ListExamsByClass / UpdateExam / DeleteExam / PublishExam / SubmitExam / GradeExam
3. `src/homework/homework.grpc-controller.ts`HomeworkService 5 RPCAssignHomework / GetHomework / ListHomeworkByClass / SubmitHomework / GradeHomework
4. `src/grades/grades.grpc-controller.ts`GradeService 6 RPCRecordGrade / GetGrade / ListGradesByStudent / ListGradesByExam / ListGradesByHomework / UpdateGrade
5. `src/attendance/attendance.grpc-controller.ts`AttendanceService 4 RPCRecordAttendance / GetAttendance / ListAttendanceByStudent / ListAttendanceByClass
6. `src/classes/classes.grpc-controller.ts`ClassService 4 RPCGetClass / GetClassesByTeacher / BatchGetClasses / ListStudentsByClass
7. `src/shared/health/health.grpc-controller.ts`HealthService.Check 返回 SERVING
- **验收标准**
- gRPC 50053 可连接HealthService.Check 返回 SERVING
- 27 RPC 全部可调用(含 P3 新增 5 RPC
- gRPC 调用走 AuthMiddleware + PermissionGuardgRPC 上下文适配)
### P3.5 排课考勤数据模型 + AttendanceServiceP2
- **负责人**ai08
- **依赖**P3.4 gRPC 启用
- **交付物**
1. `src/schedule/schedule.schema.ts`core_edu_courses / core_edu_lessons / core_edu_schedules 三表)
2. `src/attendance/attendance.schema.ts`core_edu_attendance 表)
3. `src/schedule/schedule.service.ts`(含 ScheduleConflictChecker教师/班级时间冲突检测)
4. `src/attendance/attendance.service.ts`(含唯一索引幂等)
5. `src/schedule/schedule.controller.ts` + `src/attendance/attendance.controller.ts`REST + gRPC 双入口)
- **验收标准**
- 排课冲突检测正确(教师时间重叠抛 CORE_EDU_SCHEDULE_CONFLICT 409
- 考勤录入幂等(同一学生同一课时仅 1 条)
### P3.6 成绩计算配置化P2
- **负责人**ai08
- **依赖**P3.4 gRPC 启用、ISSUE-006 决策 #3scope 优先级)
- **交付物**
1. `src/grades/grade-formulas.schema.ts`core_edu_grade_formulas 表scope / scope_id / formula_type / weights / custom_expression / effective_from / effective_to
2. `src/grades/domain/grade-calculator.ts`(纯函数:按 scope 优先级查询公式 → 加权/平均/自定义求值)
3. `src/grades/grades.service.ts` 接入 GradeCalculator
4. custom 公式安全求值(白名单正则 + Function 沙箱)
- **验收标准**
- 三种公式类型weighted / average / custom均正确计算
- scope 优先级按 ISSUE-006 决策 #3 仲裁结果
- custom 公式禁止函数调用 / 对象访问 / eval白名单校验
### P3.7 作业提交 Redis 分布式锁P2
- **负责人**ai08
- **依赖**P3.4 gRPC 启用、Redis 部署
- **交付物**
1. `src/shared/redis/redis.client.ts`Redis 单例)
2. `src/homework/homework.service.ts` submitHomework 接入 Redis 分布式锁
- 锁 key`lock:hw:submit:{homeworkId}:{studentId}`
- 锁过期30s
- 重试5 次 × 100ms 间隔
- 超时429 CORE_EDU_HOMEWORK_SUBMIT_LOCK_TIMEOUT
3. 同步对 exam submit 也接入锁(锁 key`lock:exam:submit:{examId}:{studentId}`
- **验收标准**
- 50 并发提交同一作业,仅 1 条提交入库
- 锁超时返回 429
- DB 唯一索引兜底(锁失效时仍幂等)
### P3.8 DataScope 下推P1
- **负责人**ai08
- **依赖**:无
- **交付物**
1. `src/shared/datascope/datascope-injector.ts`(根据 x-user-data-scope 头注入 WHERE 条件)
2. `src/exams/exams.repository.ts` 接入 DataScopeInjector按 class_id / school_id 过滤)
3. `src/grades/grades.repository.ts` 接入 DataScopeInjector按 student_id / class_id / school_id 过滤)
4. `src/middleware/permission.guard.ts` 增强:解析 dataScope 注入到 request
- **验收标准**
- 教师仅能查自己班级的考试/成绩
- 学生仅能查自己的成绩
- 校管理员可查全校
### P3.9 消费 IAM 事件P1
- **负责人**ai08
- **依赖**IAM gRPC 50052 就绪ai06
- **交付物**
1. `src/shared/kafka/kafka.consumer.ts`Kafka consumer 单例)
2. `src/iam-events/iam-user.consumer.ts`(订阅 edu.identity.user.created / .updated / .deleted
3. `src/iam-events/teacher-associations.schema.ts`core_edu_teacher_associations 表 + uniq_teacher_class_subject 唯一索引)
4. 消费幂等(基于 user_id + class_id + subject_id 唯一索引)
- **验收标准**
- IAM 发布 user.created 事件后core-edu 写入 teacher_associations
- 重复消费同一事件不重复写入(幂等)
- user.deleted 事件软删除教师关联(保留历史成绩归属)
### P3.10 Temporal 工作流试点P3
- **负责人**ai08
- **依赖**Temporal server 部署infra、P3.4 gRPC 启用
- **交付物**
1. `src/workflows/exam-publish.workflow.ts`(考试发布编排工作流)
2. `src/workflows/exam-publish.activities.ts`5 个 Activity创建提交骨架 / 通知 msg / 等待作答窗口 / 自动提交未答 / 通知教师)
3. `src/exams/exams.service.ts` publishExam 调用 `workflowClient.start(examPublishWorkflow, ...)`
4. `src/shared/temporal/temporal.client.ts`Temporal client 单例)
- **验收标准**
- 考试发布后 Temporal UI 可见工作流实例
- 工作流完成 5 个 Activity
- 失败可重试
### P3.11 classes 服务合并到 core-eduP3
- **负责人**ai08
- **依赖**ISSUE-006 决策 #1合并时机仲裁、P3.4 gRPC 启用
- **交付物**
1. `services/classes/src/` 代码迁入 `services/core-edu/src/classes/`
2. 删除独立 `services/classes/` 目录
3. `src/classes/classes.module.ts` 接入 core-edu AppModule
4. `src/classes/classes.grpc-controller.ts`ClassService 4 RPC
5. classes 错误码 `CLASSES_*` 保留coord §5.5 仲裁,黄金模板历史遗留)
- **验收标准**
- classes 服务代码完全迁入 core-edu
- ClassService 4 RPC 可调用
- 原 classes 服务端口 3001 不再存在
- `pnpm run arch:scan` 确认 arch.db 已更新
### P3.12 测试覆盖率 + Dockerfile 核对P3
- **负责人**ai08
- **依赖**P3.1 ~ P3.11 全部完成
- **交付物**
1. 单元测试:状态机 / GradeCalculator / ScheduleConflictChecker / DataScopeInjector纯函数优先
2. 集成测试ExamsService / HomeworkService / GradesService / AttendanceService / ScheduleService
3. Outbox relay 测试mock Kafka
4. `Dockerfile` 多阶段构建核对builder → runner最终镜像无 devDependencies
- **验收标准**
- `pnpm run test` 通过
- 覆盖率 ≥ 80%
- Dockerfile 多阶段构建,最终镜像 ≤ 200MB
### P4.1 消费 data-ana mastery.updated 事件
- **负责人**ai08
- **依赖**data-ana gRPC 50055 就绪ai11
- **交付物**`src/data-ana-events/mastery.consumer.ts`(订阅 edu.insight.mastery.updated基于 mastery_score_id 幂等)
- **验收标准**:重复消费不重复写入
### P4.2 content gRPC 调用(知识点关联)
- **负责人**ai08
- **依赖**content gRPC 50054 就绪ai09
- **交付物**`src/content/content.client.ts`(调用 ContentService.GetKnowledgePoints排课关联知识点
- **验收标准**lessons.knowledge_point_ids 引用的知识点在 content 服务可查
### P4.3 读模型双轨读策略验证
- **负责人**ai08
- **依赖**data-ana CDC 链路就绪
- **交付物**:验证刚提交成绩查 MySQL强一致聚合统计查 ClickHouse最终一致 < 5s
- **验收标准**:双轨读策略符合 02 文档 §3.3
### P5.1 msg 事件消费联调
- **负责人**ai08
- **依赖**msg gRPC 50056 就绪ai10
- **交付物**:验证 msg 消费 core-edu 事件触发通知edu.teaching.exam.created / homework.assigned / grade.recorded
- **验收标准**:端到端事件链路通
### P5.2 AI 辅助批改接口预留
- **负责人**ai08
- **依赖**:无
- **交付物**:确认 ExamService.GetExam / GradeService.ListGradesByExam 接口对 ai 服务可用
- **验收标准**ai 服务可调用上述 RPC
### P6.1 /readyz 硬化
- **负责人**ai08
- **依赖**:无
- **交付物**/readyz 补 Temporal client 连接探针
- **验收标准**/readyz 返回 4 项依赖状态MySQL + Redis + Kafka + Temporal
### P6.2 Outbox relay 迁独立 Go 服务评估
- **负责人**ai08评估不实施
- **依赖**:无
- **交付物**:评估报告(是否迁出进程内 relay
- **验收标准**:产出评估结论
### P6.3 多租户行级隔离验证
- **负责人**ai08
- **依赖**:无
- **交付物**:验证 school_id 行级隔离(不同学校数据互不可见)
- **验收标准**:跨学校查询返回空
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai08 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai08 自行补充
### 4.1 我依赖的上游就绪标志
| 依赖项 | 提供方 | 就绪信号 | 状态 | 影响 |
| ------ | ------ | -------- | ---- | ---- |
| coord 仲裁 ISSUE-001 ~ ISSUE-006 | coord | coord.md 仲裁章节 | ⏳ 待仲裁 | 阻塞 P3 全部 |
| iam gRPC 50052 | ai06 | HealthService.Check = SERVING | ⏳ | 阻塞 P3.9 消费 IAM 事件 |
| events.proto 同步 | coord | events.proto 含 AttendanceEvent + schema_version | ⏳ | 阻塞 P3.2 TOPIC_MAP |
| core_edu.proto 补全 | coord 或 ai08 | 5 service 27 RPC 定义 | ⏳ | 阻塞 P3.4 gRPC 实现 |
| Redis 部署 | infra | redis:6379 可连接 | ⏳ | 阻塞 P3.7 分布式锁 + P3.1 /readyz |
| Temporal server 部署 | infra | temporal:7233 可连接 | ⏳ | 阻塞 P3.10 工作流试点 |
| buf.gen.yaml gRPC 插件 | coord | buf generate 产出 TS gRPC 代码 | ⏳ | 阻塞 P3.4 gRPC 实现 |
| data-ana gRPC 50055P4 | ai11 | HealthService.Check = SERVING | ⏳ | 阻塞 P4.1 mastery 消费 |
| content gRPC 50054P4 | ai09 | HealthService.Check = SERVING | ⏳ | 阻塞 P4.2 知识点关联 |
| msg gRPC 50056P5 | ai10 | HealthService.Check = SERVING | ⏳ | 阻塞 P5.1 事件联调 |
### 4.2 我的就绪标志(供下游消费)
| 阶段 | 就绪信号 | 消费方 | 状态 |
| ---- | -------- | ------ | ---- |
| P2已就绪 | HTTP 3004 可访问 + /healthz + /readyzDB 探针) | teacher-bffREST 调用) | ✅ |
| P3核心 | gRPC 50053 + 27 RPC + HealthService SERVING | teacher-bff / student-bff / parent-bff / ai | ⏳ |
| P3 子信号 1 | ClassService 4 RPC 可调用 | teacher-bff班级列表 | ⏳ |
| P3 子信号 2 | ExamService 8 RPC 可调用 | teacher-bff / student-bff / ai | ⏳ |
| P3 子信号 3 | HomeworkService 5 RPC 可调用 | teacher-bff / student-bff | ⏳ |
| P3 子信号 4 | GradeService 6 RPC 可调用 | 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.3 Mock 策略(全并行开发期间)
详见 [contracts/core-edu_contract.md §4](../contracts/core-edu_contract.md)。

View File

@@ -1,18 +1,34 @@
# data-ana 工作排期
> 负责人ai11
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/data-ana_contract.md](../contracts/data-ana_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/data-ana_contract.md](../contracts/data-ana_contract.md)、[objections/data-ana_issue.md](../objections/data-ana_issue.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
> 基线日期:批次 0 已完成2026-07-09批次 1P22026-07-10 启动
---
## §1 总览
data-ana 是数据分析服务,提供 AnalyticsService12 个 RPC基于 ClickHouse 列存查询与 CDC 消费实现实时分析。全阶段目标P2 ClickHouse 接入+CDC 消费 → P3 AnalyticsService 12 RPC → P4-P6 持续优化
data-ana 是 D6 智能洞察领域的纯读模型服务Python/FastAPI基于 ClickHouse ReplacingMergeTree 宽表 + Debezium CDC 消费实现实时学情分析
**核心交付物**
- gRPC server :50055 + AnalyticsService 12 RPC含 1 个 Server Streaming
- HTTP :3006 14 端点3 基础 + 11 业务,保留作 Gateway 直连降级)
- ClickHouse 5 宽表student_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_log
- CDC 消费者core-edu MySQL binlog → Kafka → ClickHouse 宽表投影)
- 掌握度计算(加权滑动平均 + 遗忘曲线)+ 预警评估
- 派生数据事件发布edu.insight.mastery.updated / edu.insight.warning.triggered豁免 Outbox
**阶段里程碑**
- P2 预备ClickHouse 接入 + CDC 骨架 + ActionState 信封重构 + mock 数据集
- P3 预备CDC 通道接入 + 掌握度算法 v1 + Repository 封装
- P4 主战场gRPC 50055 启用 + 12 RPC + 4 端 Dashboard + Warning + DataScope + 事件发布
- P5 扩展SubscribeMasteryUpdate stream + AI 用量消费 + 手动 commit
- P6 硬化CDC 水平扩展 + 容量规划 + 数据治理 + 监控告警
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
@@ -20,26 +36,317 @@ gantt
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a11a, 2026-07-10, Xd
section P2 预备与批次1并行
2.1 ClickHouse DDL 5宽表建表 :a11a, 2026-07-10, 2d
2.2 CDC消费骨架(aiokafka) :a11b, after a11a, 2d
2.3 mock数据集(30学生×5考试×10作业) :a11c, after a11a, 1d
2.4 ActionState信封重构(P0整改) :crit, a11d, 2026-07-10, 2d
2.5 config.py修正(env/Redis/gRPC) :a11e, after a11d, 1d
section P3 预备与批次2并行
3.1 gRPC server骨架(3 RPC,不启用) :a11f, after a11b, 2d
3.2 core-edu CDC通道接入(grades/exams/homework) :a11g, after a11b, 3d
3.3 ExamCache内存LRU :a11h, after a11g, 1d
3.4 掌握度算法v1(weighted_moving_avg) :a11i, after a11g, 2d
3.5 ClickHouseRepository(FINAL/argMax) :a11j, after a11i, 2d
section P4 主战场批次3, 11d
4.1 gRPC 50055正式启用 :crit, a11k, after a11j, 1d
4.2 analytics.proto扩展12 RPC :crit, a11l, after a11k, 2d
4.3 4端Dashboard RPC实现 :a11m, after a11l, 3d
4.4 WarningService+TriggerWarning :a11n, after a11l, 2d
4.5 GetMasteryDistribution+GetStudentMastery :a11o, after a11m, 1d
4.6 iam.GetEffectiveDataScope集成(降级兜底) :crit, a11p, after a11k, 2d
4.7 DataScope 6级WHERE注入 :a11q, after a11p, 1d
4.8 attendance+content CDC消费 :a11r, after a11g, 2d
4.9 MasteryEvent+WarningTriggered发布 :a11s, after a11n, 1d
4.10 HTTP 14端点+readyz硬化 :a11t, after a11m, 2d
section P5 扩展批次4并行
5.1 SubscribeMasteryUpdate stream RPC :a11u, after a11t, 3d
5.2 AIUsageEvent消费→ai_usage_log :a11v, after a11u, 2d
5.3 手动commit替换auto_commit :a11w, after a11v, 1d
5.4 Admin Dashboard AI用量区块 :a11x, after a11v, 2d
section P6 硬化批次5并行
6.1 CDC多实例水平扩展 :a11y, after a11x, 3d
6.2 ExamCache Redis化 :a11z, after a11y, 2d
6.3 容量规划+TTL归档策略 :a11aa, after a11z, 2d
6.4 监控告警(consumer lag HPA) :a11ab, after a11aa, 2d
6.5 readyz深度硬化 :a11ac, after a11ab, 1d
```
> **注意**:以上为 coord 初始规划ai11 接管后必须自行细化为完整 P2-P6 排期。
> **关键路径**critActionState 信封重构 → gRPC 50055 启用 → analytics.proto 扩展 → iam GetEffectiveDataScope 集成
> **总工期**:约 51 天2026-07-10 ~ 2026-08-30其中 P4 主战场 11 天为关键交付期
---
## §3 详细任务
### 全阶段任务
### 3.1 P2 预备期2026-07-10 ~ 2026-07-188d
#### 任务 2.1ClickHouse DDL 5 宽表建表
- **负责人**ai11
- **交付物**:⚠️ 由 ai11 自行补充
- **依赖**:见 [contracts/data-ana_contract.md](../contracts/data-ana_contract.md)
- **验收标准**⚠️ 由 ai11 自行补充
- **依赖**ClickHouse 实例就绪,由 infra 提供)
- **交付物**`infra/clickhouse/ddl/data_ana.sql`5 宽表 DDLstudent_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_log
- **验收标准**5 表在 ClickHouse 中创建成功ReplacingMergeTree 引擎 + ORDER BY + PARTITION BY 符合 02 §3 DDL 设计
#### 任务 2.2CDC 消费骨架
- **负责人**ai11
- **依赖**Kafka 就绪
- **交付物**`src/data_ana/cdc_consumer.py` 重构aiokafka AIOKafkaConsumer + EventHandler 路由框架)
- **验收标准**:能消费 mock CDC 事件并打印路由日志consumer group = `data-ana-cdc`
#### 任务 2.3mock 数据集
- **负责人**ai11
- **依赖**:任务 2.1
- **交付物**`scripts/seed_clickhouse.py`(批量导入 30 学生 × 5 考试 × 10 作业 × 30 天出勤模拟数据)
- **验收标准**ClickHouse 5 表有数据,可查询返回非空结果
#### 任务 2.4ActionState 信封重构P0 整改coord-cross-review §5.3
- **负责人**ai11
- **依赖**:无
- **交付物**`src/data_ana/shared/action_state.py`ActionState[T] 泛型 + ActionStateError + ok()/fail() 类方法)
- **验收标准**main.py 所有端点返回 `ActionState[T]`degraded 标记在顶层 `details.degraded`(非 error.detailsruff 零错误
#### 任务 2.5config.py 修正
- **负责人**ai11
- **依赖**:无
- **交付物**`src/data_ana/config.py` 修正env_prefix 补 Redis / gRPC / ClickHouse 配置项pydantic-settings 校验)
- **验收标准**:配置项覆盖 02 §13 配置清单,环境变量缺失时 pydantic-settings 报错
---
### 3.2 P3 预备期2026-07-18 ~ 2026-07-268d
#### 任务 3.1gRPC server 骨架3 RPC不正式启用
- **负责人**ai11
- **依赖**analytics.proto 当前 3 RPC无需 coord 补全)
- **交付物**`src/data_ana/grpc_server.py`grpc.aio Server 骨架 + 3 RPC 实现,绑定 :50055 但不启动对外)
- **验收标准**:本地可启动 gRPC server3 RPC 可调用返回 mock 数据
#### 任务 3.2core-edu CDC 通道接入
- **负责人**ai11
- **依赖**core-edu MySQL 就绪 + Debezium CDC 配置core-edu 就绪前用 mock binlog 事件)
- **交付物**`src/data_ana/cdc_consumer.py` 完善EventHandler 处理 grades/exams/homework/classes 表 CDC 事件)
- **验收标准**:消费 CDC 事件 → 解析 Debezium JSON → 查 ExamCache 填 class_id → upsert ClickHouse 宽表
#### 任务 3.3ExamCache 内存 LRU
- **负责人**ai11
- **依赖**:任务 3.2
- **交付物**`src/data_ana/exam_cache.py`(内存 LRU dictmax 10000 条exam_id → {class_id, subject_id}
- **验收标准**CDC exams 事件触发 ExamCache 更新grades 事件查 ExamCache 获取 class_id
#### 任务 3.4:掌握度算法 v1
- **负责人**ai11
- **依赖**:任务 3.2
- **交付物**`src/data_ana/mastery_service.py`(加权滑动平均算法,权重 w_i = 0.6^i归一化
- **验收标准**:输入学生近期 N 次成绩 → 输出 mastery_level (0.0-1.0) → 写 mastery_snapshot 表
#### 任务 3.5ClickHouseRepository 封装
- **负责人**ai11
- **依赖**:任务 2.1
- **交付物**`src/data_ana/clickhouse_client.py` 重构(查询封装 + FINAL/argMax 去重 + DataScope WHERE 注入接口)
- **验收标准**:查询 student_dashboard_view 返回去重后最新版本数据
---
### 3.3 P4 主战场期2026-07-26 ~ 2026-08-0611d批次 3
#### 任务 4.1gRPC 50055 正式启用
- **负责人**ai11
- **依赖**:任务 3.1
- **交付物**main.py lifespan 启动 gRPC server :50055HealthService.Check 返回 SERVING
- **验收标准**gRPC server 对外可访问HealthService.Check = SERVING
#### 任务 4.2analytics.proto 扩展 12 RPC
- **负责人**ai11本分支内补全 proto提请 coord 合并)
- **依赖**ISSUE-003 解决coord 确认或 ai11 自行补全)
- **交付物**`packages/shared-proto/proto/analytics.proto` 扩展至 12 RPC3 现有 + 9 新增 message 定义)
- **验收标准**`buf lint` 零错误,`buf generate` 生成 Python stub 成功12 RPC 全部可调用
#### 任务 4.34 端 Dashboard RPC 实现
- **负责人**ai11
- **依赖**:任务 4.2
- **交付物**GetTeacherDashboard / GetStudentDashboard / GetParentDashboard / GetAdminDashboard 4 RPC 实现
- **验收标准**4 RPC 返回 ActionState[DashboardData]DataScope 过滤生效,降级时返回骨架数据 + degraded: true
#### 任务 4.4WarningService + TriggerWarning
- **负责人**ai11
- **依赖**:任务 3.4(掌握度算法)
- **交付物**`src/data_ana/warning_service.py`(预警阈值评估 + TriggerWarning RPC + GetWarnings RPC
- **验收标准**:掌握度 < 0.4 触发 LOW_MASTERY 预警,成绩环比下降 20% 触发 SCORE_DROP缺勤 ≥ 3 次/周触发 ABSENT_FREQUENT
#### 任务 4.5GetMasteryDistribution + GetStudentMastery
- **负责人**ai11
- **依赖**:任务 3.4 + 任务 4.2
- **交付物**2 RPC 实现(班级掌握度分布 + 学生知识点掌握度明细)
- **验收标准**:返回 mastered/progressing/weak 三档分布数据
#### 任务 4.6iam.GetEffectiveDataScope 集成(降级兜底)
- **负责人**ai11
- **依赖**ISSUE-001 解决iam.proto 补全 GetEffectiveDataScope。若 P4 时 iam 未就绪,使用降级兜底
- **交付物**`src/data_ana/iam_client.py`gRPC 调 iam.GetEffectiveDataScope + Redis 缓存 5min + 降级兜底)
- **验收标准**iam 可用时调 gRPC 获取 DataScopeiam 不可用时按 role 映射默认 DataScope + degraded: true
#### 任务 4.7DataScope 6 级 WHERE 注入
- **负责人**ai11
- **依赖**:任务 4.6 + 任务 3.5
- **交付物**ClickHouseRepository 查询方法注入 DataScope WHERE 子句SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL
- **验收标准**:教师只能查自己班级数据,学生只能查自己数据,管理员可查全校数据
#### 任务 4.8attendance + content CDC 消费
- **负责人**ai11
- **依赖**:任务 3.2CDC 框架)
- **交付物**EventHandler 扩展 attendance_logs 表 CDC + content_knowledge_points 表 CDC
- **验收标准**:考勤事件落 attendance_logs 表,知识点事件更新 mastery_snapshot 元数据
#### 任务 4.9MasteryEvent + WarningTriggered 事件发布
- **负责人**ai11
- **依赖**:任务 3.4 + 任务 4.4
- **交付物**`src/data_ana/kafka_producer.py`aiokafka AIOKafkaProducer + idempotent + transactional_id
- **验收标准**:掌握度计算完成发布 `edu.insight.mastery.updated`,预警触发发布 `edu.insight.warning.triggered`,豁免 Outbox
#### 任务 4.10HTTP 14 端点 + readyz 硬化
- **负责人**ai11
- **依赖**:任务 4.3 + 任务 4.4 + 任务 4.5
- **交付物**main.py 14 个 HTTP 端点全部实现3 基础 + 11 业务)+ /readyz 检查 4 依赖clickhouse/cdc_consumer/redis/iam_grpc
- **验收标准**14 端点返回 ActionState[T]/readyz 依赖检查正确反映服务状态
---
### 3.4 P5 扩展期2026-08-06 ~ 2026-08-1913d批次 4 并行)
#### 任务 5.1SubscribeMasteryUpdate Server Streaming RPC
- **负责人**ai11
- **依赖**:任务 4.2 + 任务 4.9
- **交付物**SubscribeMasteryUpdate RPC 实现server-streaming客户端订阅 student_id/class_id掌握度更新时推送
- **验收标准**:客户端订阅后,掌握度计算完成时收到 MasteryUpdateEvent 流
#### 任务 5.2AIUsageEvent 消费 → ai_usage_log
- **负责人**ai11
- **依赖**ISSUE-002 解决events.proto 补 AIUsageEvent+ ai 服务发布 `edu.insight.ai.usage` topic
- **交付物**EventHandler 扩展 AIUsageEvent 消费 → 落 ai_usage_log 表
- **验收标准**ai 服务发布用量事件后ai_usage_log 表有数据Admin Dashboard AI 用量区块可展示
#### 任务 5.3:手动 commit 替换 auto_commit
- **负责人**ai11
- **依赖**:任务 3.2
- **交付物**cdc_consumer.py 改为 `enable_auto_commit=False` + 手动 commitat-least-once
- **验收标准**ClickHouse 写入成功后才 commit offset重启后无重复消费依赖 ReplacingMergeTree 去重)
#### 任务 5.4Admin Dashboard AI 用量区块
- **负责人**ai11
- **依赖**:任务 5.2
- **交付物**GetAdminDashboard RPC 补全 AI 用量统计区块(按 provider/model/时间窗聚合)
- **验收标准**Admin Dashboard 返回 AI 用量数据,无数据时显示"暂无数据"
---
### 3.5 P6 硬化期2026-08-19 ~ 2026-08-3011d批次 5 并行)
#### 任务 6.1CDC 多实例水平扩展
- **负责人**ai11
- **依赖**:任务 5.3(手动 commit
- **交付物**CdcConsumer 支持多实例分摊 partitionconsumer group 不变)
- **验收标准**2+ 实例消费同一 topic 无重复无遗漏
#### 任务 6.2ExamCache Redis 化
- **负责人**ai11
- **依赖**:任务 6.1
- **交付物**exam_cache.py 改为 Redis 实现key: `data_ana:exam:{exam_id}` TTL 30 天)
- **验收标准**:多实例共享 ExamCache重启后缓存不丢失
#### 任务 6.3:容量规划 + TTL 归档策略
- **负责人**ai11
- **依赖**:无
- **交付物**ClickHouse TTL 策略student_dashboard_view 保留 2 年ai_usage_log 保留 1 年)+ 冷热数据分离方案
- **验收标准**TTL 配置生效,过期数据自动清理
#### 任务 6.4:监控告警完善
- **负责人**ai11
- **依赖**:任务 6.1
- **交付物**Prometheus 指标补全consumer lag histogram + 慢查询 counter + ClickHouse 连接池 gauge+ Grafana dashboard
- **验收标准**consumer lag 超阈值触发 HPA慢查询超阈值告警
#### 任务 6.5readyz 深度硬化
- **负责人**ai11
- **依赖**:任务 4.10
- **交付物**/readyz 检查项完善ClickHouse 查询超时 1s + Redis ping + iam gRPC 超时 2s + CDC consumer lag < 1000
- **验收标准**:任一依赖不健康时 /readyz 返回 503K8s 摘流量
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai11 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai11 自行补充
### 4.1 我依赖的上游就绪标志
- [ ] **core-edu gRPC 50053 启用**ai08批次 2—— CDC 数据源grades/exams/homework/attendance 表 binlog
- [ ] **core-edu MySQL Debezium CDC 配置**ai08 + SRE—— CDC 通道前提
- [ ] **content gRPC 50054 启用**ai09批次 3—— 知识点维度 CDC
- [ ] **iam.proto 补全 GetEffectiveDataScope RPC**ai06/coordISSUE-001—— DataScope 解析
- [ ] **analytics.proto 扩展至 12 RPC**coord/ai11ISSUE-003—— gRPC stub 生成前提
- [ ] **events.proto 补全 AIUsageEvent message**coordISSUE-002—— AI 用量消费P5
- [ ] **ai 服务发布 `edu.insight.ai.usage` topic**ai12批次 4—— AI 用量统计P5
> **降级兜底**core-edu / content / iam 未就绪时,使用 ClickHouse 内置 mock 数据集 + 硬编码 DataScope 降级,标注 `details.degraded: true`
### 4.2 我的就绪信号(供下游消费)
- [ ] **P4 就绪**data-ana gRPC 50055 启用HealthService.Check = SERVING+ AnalyticsService 12 RPC 可调用 + 4 端 Dashboard 返回结构化数据
- [ ] **P4 就绪**`edu.insight.mastery.updated` topic 可发布mastery.updated / warning.triggered
- [ ] **P5 就绪**SubscribeMasteryUpdate server-streaming RPC 可订阅
- [ ] **P6 就绪**CDC 多实例水平扩展 + ExamCache Redis 化完成
### 4.3 下游消费方
| 下游 | 消费接口 | 就绪依赖阶段 |
| -------------------------- | ---------------------------------------------------------------- | ------------ |
| teacher-bffai03 | gRPC 50055 GetTeacherDashboard / GetClassPerformance 等 | P4 |
| student-bffai04 | gRPC 50055 GetStudentDashboard / GetStudentWeakness 等 | P4 |
| parent-bffai05 | gRPC 50055 GetParentDashboard | P4 |
| ai 服务ai12 | gRPC 50055 反向调用查学情GetStudentMastery / GetLearningTrend | P5 |
| core-eduai08 | Kafka `edu.insight.mastery.updated`(推荐个性化练习) | P4 |
| msgai10 | Kafka `edu.insight.warning.triggered`(推送通知) | P4 |
---
## §5 风险与缓解
| 风险 | 影响 | 缓解措施 |
| -------------------------------------------- | ---- | ------------------------------------------------------------------------------------------ |
| iam.proto 未补全 GetEffectiveDataScope | P4 | 降级兜底:按 role 映射默认 DataScope + degraded: trueISSUE-001 |
| analytics.proto 未扩展 12 RPC | P4 | ai11 本分支自行补全 proto提请 coord 合并ISSUE-003 |
| core-edu CDC 通道未就绪 | P3-P4 | mock 数据集降级 + 本地 stub CDC 事件 |
| ClickHouse ReplacingMergeTree 去重延迟 | P4 | 查询加 FINAL / argMax 强制去重02 §3.6 已设计) |
| 单实例 CDC 消费者单点故障 | P4 | P6 演进为多实例 + Redis ExamCacheP4 阶段监控 consumer lag 告警 |
| 掌握度算法精度不足 | P4 | v1 用加权滑动平均P5+ 评估引入遗忘曲线 max 叠加02 §9 已设计 MasteryMethod 枚举预留) |

View File

@@ -3,54 +3,154 @@
> 负责人ai06
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/iam_contract.md](../contracts/iam_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 裁决依据:[coord-final-decisions](../../coord-final-decisions.md) I1-I8、[president-final-rulings](../../president-final-rulings.md) §3.2/§2.15/§2.16/§5.5
---
## §1 总览
iam 是身份认证服务全阶段目标gRPC 50052 + 12 RPC + /readyz 深度 + iam_student_guardians + DataScope + 审计日志 + Outbox。
iam 是身份认证服务全阶段目标gRPC 50052 + 12 RPC + REST 双入口 + /readyz 深度 + iam_student_guardians + DataScope 6 级 + 审计日志 + Outbox + RBAC CRUD 完整化
**核心裁决约束**coord-final-decisions I1-I8
- I1P2 即启用 gRPC server 50052REST + gRPC 双入口并存,非"P2 仅 REST → P3 gRPC"
- I2直接用 shared-ts Outbox 工具包(非 iam 自建)
- I3首次实现即 DB 驱动 + Redis 缓存 PermissionGuard废弃硬编码 ROLE_PERMISSIONS map
- I4首次实现即注册 AuthMiddlewareController 通过 @Req() 注入用户上下文)
- I5P2 本地文件 RS256 密钥IAM_PRIVATE_KEY_PATH / IAM_PUBLIC_KEY_PATHP6 迁 Vault
- I6P2 即补全 iam_student_guardians 表 + GetChildrenByParent RPC + GET /iam/children
- I7/iam/v1/* 前缀Controller 加 v1 前缀Gateway 透传)
- I8统一 GET /iam/permissions/effective
**工作量分级**president §3.2iam 实际工作量 26-27 天,拆分 P2.1(核心,阻塞批次 2+ P2.2扩展P3 期间持续补,不阻塞)。
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
title ai06 iam 全阶段排期
title ai06 iam 全阶段排期P2.1/P2.2 拆分版)
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2.1 核心
gRPC server 50052启用 :crit, a6a, 2026-07-10, 2d
12 RPC实现 :crit, a6b, after a6a, 4d
/readyz深度(5依赖) :a6c, after a6b, 1d
iam_student_guardians+DataScope :a6d, after a6b, 1d
section P2.1 核心阻塞批次2
1.1 proto 契约确认(依赖coord补全iam.proto) :crit, a1, 2026-07-10, 1d
1.2 gRPC server 50052 + AuthMiddleware注册 :crit, a2, after a1, 2d
1.3 8 RPC实现(GetViewports/GetEffectivePermissions/GetEffectiveAccess/Logout/GetPublicKey/BatchGetUsers/GetEffectiveDataScope/GetChildrenByParent) :crit, a3, after a2, 4d
1.4 JWT RS256本地文件+refresh轮换 :crit, a4, after a2, 2d
1.5 /iam/v1/*前缀迁移+端点统一(I7/I8) :crit, a5, after a3, 1d
1.6 iam_student_guardians表+GetChildrenByParent(I6) :crit, a6, after a3, 1d
1.7 shared-ts Outbox接入+事件发布(I2) :crit, a7, after a3, 2d
1.8 DB驱动PermissionGuard基础(I3) :crit, a8, after a3, 2d
1.9 /readyz深度(5依赖:DB/Redis/Kafka/gRPC/JWKS) :a9, after a8, 1d
1.10 02文档回写(I1-I8对齐) :crit, a10, 2026-07-10, 1d
section P2.2-P6 扩展
审计日志+Outbox :a6e, after a6d, 3d
持续补全 :a6f, after a6e, 5d
section P2.2 扩展P3期间持续补
2.1 三层角色模型(system/organization/temporary) :b1, after a8, 3d
2.2 DataScope 6级实现(待ISSUE-004裁决) :b2, after a8, 2d
2.3 视口4层+getEffectivePermissions完整 :b3, after b1, 3d
2.4 审计日志(user_audit_log表+AuditEvent发布) :b4, after b1, 3d
2.5 Redis缓存完整实现(I3完整) :b5, after b1, 2d
2.6 密码策略(强度/过期/重用限制) :b6, after b4, 2d
2.7 单元测试+集成测试(覆盖率≥80%) :b7, after b6, 3d
section P3-P6 持续优化
3.1 RBAC CRUD完整化(角色/权限/视口增删改) :c1, after b3, 3d
3.2 2FA实现(TOTP) :c2, after b6, 3d
3.3 JWT密钥迁移Vault(P6) :c3, after c2, 2d
3.4 /readyz硬化+性能优化 :c4, after c1, 2d
```
> **注意**:以上为 coord 初始规划ai06 接管后必须自行细化为完整 P2-P6 排期。
---
## §3 详细任务
### P2.1gRPC + 12 RPC + /readyz + DataScope
### P2.1gRPC + 8 RPC + RS256 + AuthMiddleware + Outbox阻塞批次 2
- **负责人**ai06
- **批次**:批次 1P2
- **预估**5-7 天
- **前置依赖**
- coord 补全 iam.proto 至 12 RPCISSUE-005阻塞项
- coord 补全 events.proto UserEvent/RoleEventISSUE-002阻塞 Outbox 事件发布)
- shared-ts Outbox 工具包已就绪(✅ 已存在)
- **交付物**
- gRPC server 50052 + 12 RPC见 iam.proto
- /readyz 5 项依赖检查
- iam_student_guardians 表 + DataScope=CHILDREN
- **依赖**iam.proto批次 0 已完成
- **验收标准**12 RPC 全部可用 + /readyz 返回 5 项状态
- **完整 P2.2-P6 任务**:⚠️ 由 ai06 自行补充
1. gRPC server 50052 启用NestJS gRPC transport
2. 8 RPC 实现GetViewports / GetEffectivePermissions / GetEffectiveAccess / Logout / GetPublicKey / BatchGetUsers / GetEffectiveDataScope / GetChildrenByParent
3. AuthMiddleware 注册app.module.ts configure 消费Controller 改用 @Req() 注入用户上下文I4
4. JWT RS256 本地文件加载IAM_PRIVATE_KEY_PATH / IAM_PUBLIC_KEY_PATH+ refresh token 轮换 + 旧 token 黑名单I5
5. /iam/v1/* 前缀迁移Controller `@Controller('v1/iam')`+ 端点路径统一 I8I7
6. iam_student_guardians 表 + GET /iam/v1/children REST + GetChildrenByParent gRPCI6
7. shared-ts Outbox 接入,发布 UserEvent/RoleEventI2
8. DB 驱动 PermissionGuard 基础(废弃 permission.guard.ts 硬编码 ROLE_PERMISSIONS map改调 IamService.getEffectivePermissionsI3
9. /readyz 深度检查 5 项依赖DB SELECT 1 / Redis PING / Kafka 连接 / gRPC 自身可达 / JWKS 可读)
10. 01/02 文档回写(删除全部中间过渡方案,对齐 I1-I8 + §2.15/§2.16/§5.5
- **验收标准**
- gRPC 50052 HealthService.Check 返回 SERVING
- 8 RPC 全部可调用并返回正确响应
- GetPublicKey 返回 RS256 PEM 公钥(供 api-gateway 验签)
- GetChildrenByParent 返回学生列表(供 parent-bff P4 消费)
- /iam/v1/* 前缀生效,旧路径不保留
- AuthMiddleware 注入 req.useruserId/roles/dataScope
- /readyz 返回 5 项依赖状态
- **就绪信号**gRPC 50052 启用 + GetPublicKey RPC 可用 + HealthService.Check 返回 SERVING
### P2.2:三层角色 + DataScope + 视口 + 审计 + 缓存P3 期间持续补,不阻塞)
- **负责人**ai06
- **批次**:批次 2-3 期间P3 进行中持续补)
- **预估**12-15 天
- **前置依赖**P2.1 完成 + ISSUE-004DataScope 枚举裁决)
- **交付物**
1. 三层角色模型system / organization / temporaryiam_roles 表扩展 role_type + level 字段)
2. DataScope 6 级实现ALL / SCHOOL / GRADE / CLASS / SUBJECT|DISTRICT / SELF待 ISSUE-004 裁决)
3. 视口 4 层admin / teacher / student / parent+ getEffectivePermissions 完整聚合
4. 审计日志user_audit_log 表 + AuditEvent 发布到 edu.iam.audit.created topicpresident §5.5
5. Redis 缓存完整实现getEffectivePermissions TTL 5min + 角色变更主动 DEL + getUserViewports 缓存)
6. 密码策略(强度校验 / 过期提醒 / 重用限制)
7. 单元测试 + 集成测试(覆盖率 ≥ 80%,对齐 classes 黄金模板)
- **验收标准**
- 三层角色可创建/分配/查询
- DataScope 在 Repository 层动态注入 WHERE 条件
- 审计事件可发布到 Kafka
- Redis 缓存命中率可观测iam_permission_cache_hits_total 指标)
- 测试覆盖率 ≥ 80%
### P3-P6RBAC CRUD + 2FA + Vault 迁移 + 硬化
- **负责人**ai06
- **批次**:批次 4-5P5-P6 期间)
- **预估**8-10 天
- **交付物**
1. RBAC CRUD 完整化(角色/权限/视口增删改,系统角色禁止删除)
2. 2FA 实现TOTPpending-features §P2 提及)
3. JWT 密钥迁移 VaultP6president X8
4. /readyz 硬化 + 性能优化(连接池调优、缓存策略优化)
- **验收标准**
- RBAC CRUD 全部端点可用 + 权限装饰器覆盖
- 2FA 可启用/验证/禁用
- Vault 密钥轮换不中断服务
---
## §4 依赖与就绪信号
- **我依赖**iam 是基础服务)
- **我的就绪信号**gRPC 50052 启用 + GetPublicKey RPC 可用 + HealthService.Check 返回 SERVING
### 4.1 我依赖的上游就绪标志
| 依赖项 | 提供方 | 就绪标志 | 状态 |
| ------ | ------ | -------- | ---- |
| iam.proto 补全至 12 RPC | coord | proto 文件含 12 RPC + 全部 message | ❌ 仅 4 RPCISSUE-005 |
| events.proto 补全 UserEvent/RoleEvent/AuditEvent | coord | proto 文件含 3 个 message | ❌ 缺失ISSUE-002 |
| shared-ts Outbox 工具包 | coord | outbox.service.ts + outbox.module.ts 可导入 | ✅ 已就绪 |
| shared-ts Redis 工具包 | coord | redis client 单例可导入 | ⏳ 待确认 |
### 4.2 我的就绪信号(供下游消费)
- [ ] iam gRPC 50052 启用HealthService.Check 返回 SERVING
- [ ] 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 15min + refresh_token 7day 轮换)
- [ ] /iam/v1/* REST 端点可用(供 gateway 透传 + admin-portal 直连)

View File

@@ -1,45 +1,316 @@
# msg 工作排期
> 负责人ai10
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/msg_contract.md](../contracts/msg_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/msg_contract.md](../contracts/msg_contract.md)、[02-architecture-design.md §10](../../../services/msg/docs/02-architecture-design.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
---
## §1 总览
msg 是消息服务,提供 NotificationServicePreferenceServiceTemplateService基于 Outbox 模式发布消息事件。全阶段目标P2 服务骨架+Outbox → P3 三大 Service 实现 → P4-P6 持续优化
msg 是消息通知中台P5,提供 NotificationService + NotificationPreferenceService + NotificationTemplateService 三服务,基于 Outbox 模式发布通知事件,消费 iam/core-edu/data-ana 共 12 类事件触发多渠道通知
**全阶段目标**
- P2-P3服务骨架补全schema 迁移 + Outbox + Kafka 基础设施 + mock 消费)
- P4三大 Service 主体实现Notification + Preference + Template+ ChannelDispatcher 多渠道
- P5gRPC 50056 启用 + PushGatewayClient gRPC + 12 类事件 consumer + ES mapping
- P6测试覆盖 ≥ 80% + /readyz 硬化 + 黄金模板对齐 + README 修正
**批次归属**:批次 4P5依赖批次 3 contentai09就绪后启动预估 13 天。
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
title ai10 msg 全阶段排期
title ai10 msg 全阶段排期13 天)
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a10a, 2026-07-10, Xd
```
section P2-P3 骨架补全
T1-T2 schema迁移(notifications+preferences字段) :crit, a1, 2026-07-10, 1d
T3 新建msg_notification_templates表 :a2, after a1, 1d
T4 新建msg_outbox_events+Publisher worker :crit, a3, after a1, 2d
T5 新建shared/kafka(producer+consumer骨架) :crit, a4, after a3, 1d
T6 引入ioredis+IdempotencyGuard(SETNX) :a5, after a3, 1d
> **注意**:以上为 coord 初始规划ai10 接管后必须自行细化为完整 P2-P6 排期。
section P4 主体实现
T7 ChannelDispatcher多渠道抽象 :crit, a6, after a4, 2d
T10 重构notifications.service(移除同步fetch) :a7, after a6, 1d
T11 batchMarkAsRead/markAllAsRead/recall/getUnreadCount :a8, after a7, 1d
T12 NotificationPreference CRUD :a9, after a7, 1d
T13 NotificationTemplate CRUD+render :a10, after a7, 1d
T15 createBatch改批量INSERT :a11, after a7, 1d
section P5 gRPC+事件+ES
T8 PushGatewayClient gRPC(替代fetch降级) :crit, a12, after a8, 1d
T9 12类Kafka事件consumer(iam/core-edu/data-ana) :crit, a13, after a12, 2d
T14 ES索引mapping+ensureIndex+同步 :a14, after a12, 1d
gRPC 50056启用+3 Service 13 RPC :crit, a15, after a13, 1d
section P6 硬化与对齐
T16 NotificationsModule补exports :a16, after a15, 1d
T17 /readyz多依赖(DB/ES/Redis/Kafka/PushGW) :a17, after a15, 1d
T19 统一关闭到LifecycleService :a18, after a15, 1d
T20-T21 DB改getDb()+ID改cuid2 :a19, after a15, 1d
T22 单元测试覆盖≥80% :crit, a20, after a19, 2d
T23 修正README与实现对齐 :a21, after a20, 1d
```
---
## §3 详细任务
### 全阶段任务
### P2-P3 骨架补
#### T1-T2schema 迁移notifications + preferences 字段扩展)
- **负责人**ai10
- **交付物**:⚠️ 由 ai10 自行补充
- **依赖**:见 [contracts/msg_contract.md](../contracts/msg_contract.md)
- **验收标准**:⚠️ 由 ai10 自行补充
- **依赖**msg 独占 DB
- **交付物**
- `msg_notifications` 表新增 status / metadata / related_entity_type / related_entity_id / group_id / sender_id / template_id / event_id / updated_at 字段 + 6 个索引
- `msg_notification_preferences` 表补齐 created_at / updated_at + 新增 frequency_limit / quiet_hours_start / quiet_hours_end / quiet_hours_timezone 字段
- **验收标准**Drizzle schema 定义更新,迁移脚本可执行,索引符合 02-architecture-design.md §3.1.1 / §3.1.2
#### T3新建 msg_notification_templates 表
- **负责人**ai10
- **依赖**T1-T2
- **交付物**`msg_notification_templates` 表 schemacode + type + title_template + content_template + default_channels + variables + locale + statusUNIQUE INDEX `(code, locale)`
- **验收标准**schema 定义 + 迁移脚本,符合 02-architecture-design.md §3.1.3
#### T4新建 msg_outbox_events 表 + Outbox Publisher worker
- **负责人**ai10
- **依赖**T1-T2
- **交付物**
- `msg_outbox_events` 表 schemaevent_id PK + aggregate_type + aggregate_id + event_type + topic + payload + status + retry_count + created_at + published_at + next_retry_at
- `shared/outbox/outbox.publisher.ts`(独立 worker每 1s 轮询 PENDING 事件投递 Kafka
- `shared/outbox/outbox.schema.ts`Drizzle schema
- **验收标准**Outbox Publisher 可轮询 + 投递 + 更新 status=SENT符合 02-architecture-design.md §3.1.4 / §5.4
- **关联 ISSUE**ISSUE-003Outbox 强制)
#### T5新建 shared/kafka/producer + consumer 骨架)
- **负责人**ai10
- **依赖**T4
- **交付物**
- `shared/kafka/kafka.producer.ts`idempotent=true + transactionalId=msg-producer
- `shared/kafka/kafka.consumer.ts`consumer group = msg-service
- `shared/kafka/topic-map.ts`PRODUCER_TOPIC_MAP + CONSUMER_TOPICS
- **验收标准**producer 可投递消息consumer 可订阅 topic符合 02-architecture-design.md §5.3
#### T6引入 ioredis + IdempotencyGuardSETNX
- **负责人**ai10
- **依赖**:无
- **交付物**
- `shared/redis/redis.client.ts`ioredis 客户端,连接 REDIS_URL
- `shared/redis/idempotency.guard.ts`SETNX `msg:processed:{event_id}` TTL 7 天)
- `shared/redis/read-bitmap.ts`(已读位图 BITCOUNT / GETBIT
- env.ts 补 REDIS_URL 必填校验
- **验收标准**IdempotencyGuard SETNX 原子去重Redis 不可用时降级到 DB 唯一索引
- **关联 ISSUE**ISSUE-012三层幂等防线补 msg_idempotency 表中间层)
### P4 主体实现
#### T7ChannelDispatcher 多渠道抽象
- **负责人**ai10
- **依赖**T5、T6
- **交付物**
- `channels/notification-channel.interface.ts`NotificationChannel 接口)
- `channels/in-app.channel.ts`(站内信,写 MySQL
- `channels/email.channel.ts`邮件SMTP异步队列
- `channels/sms.channel.ts`短信HTTP API
- `channels/wechat.channel.ts`微信HTTP API
- `channels/push.channel.ts`(推送,调 PushGatewayClient
- `channels/channel-dispatcher.ts`Promise.allSettled 并行投递 + in_app 总是发送)
- **验收标准**:新增渠道只需实现接口 + 注册,符合 02-architecture-design.md §12
#### T10重构 notifications.service.ts
- **负责人**ai10
- **依赖**T4、T5、T7
- **交付物**:重构 notifications.service.ts移除同步 fetch push-gateway改为 ChannelDispatcher + Outbox 事务
- **验收标准**send 方法走 BEGIN TX → INSERT notifications + INSERT outbox → COMMIT → ChannelDispatcher.dispatch
#### T11新增端点batchMarkAsRead / markAllAsRead / recall / getUnreadCount
- **负责人**ai10
- **依赖**T10
- **交付物**controller + service 新增 4 个端点
- **验收标准**:符合 02-architecture-design.md §4.1 REST API 表
- **关联 ISSUE**ISSUE-010markAsRead 权限改 READ
#### T12NotificationPreference CRUD
- **负责人**ai10
- **依赖**T1-T2
- **交付物**`preferences/` 目录controller + service + repository + schema + dto
- **验收标准**GET / PUT preferences 端点可用
#### T13NotificationTemplate CRUD + render
- **负责人**ai10
- **依赖**T3
- **交付物**`templates/` 目录controller + service + repository + schema + dto`{{variable}}` 占位符替换渲染
- **验收标准**CreateTemplate / GetTemplate / ListTemplates / RenderTemplate 可用
#### T15createBatch 改批量 INSERT
- **负责人**ai10
- **依赖**T10
- **交付物**createBatch 改为 `db.insert(notifications).values([...])` 批量 INSERT
- **验收标准**1 万条广播通知 < 5s性能验收
### P5 gRPC + 事件 + ES
#### T8PushGatewayClient gRPC
- **负责人**ai10
- **依赖**D5push-gateway 提供 gRPC PushService.Push
- **交付物**`shared/push/push-gateway.client.ts`gRPC 调用,替代 fetch POST /internal/push 降级)
- **验收标准**gRPC 调用 push-gateway PushService.Push降级模式保留push-gateway 不可用时走 in_app
- **关联 ISSUE**ISSUE-005调用方向澄清
#### T912 类 Kafka 事件 consumer
- **负责人**ai10
- **依赖**T5、T6、D1-D2events.proto 补齐 message + 字段)
- **交付物**
- `shared/kafka/consumers/iam.consumer.ts`6 类 user/role 事件)
- `shared/kafka/consumers/core-edu.consumer.ts`5 类 exam/homework/grade/attendance 事件)
- `shared/kafka/consumers/data-ana.consumer.ts`1 类 mastery 事件)
- 每个 consumer 走 IdempotencyGuard → NotificationService.createNotificationFromEvent
- **验收标准**12 类事件均可消费 + 幂等去重 + fan-out 通知
- **关联 ISSUE**ISSUE-013events.proto 缺 4 类 message阻塞
#### T14ES 索引 mapping + ensureIndex + 同步
- **负责人**ai10
- **依赖**:无
- **交付物**
- `config/elasticsearch.ts``notifications` 索引 mappingik_max_word 分词)
- ensureIndex 幂等创建
- 数据同步Outbox 事件触发 ES 索引更新(替代当前同步 safeIndex
- **验收标准**ES 检索可用mapping 符合 02-architecture-design.md §3.2.1
- **关联 ISSUE**ISSUE-011降级方向待仲裁
#### gRPC 50056 启用 + 3 Service 13 RPC
- **负责人**ai10
- **依赖**D3-D4msg.proto 补 RPC + Service
- **交付物**
- `notifications.grpc.controller.ts`NotificationService gRPC
- `preferences.grpc.controller.ts`NotificationPreferenceService gRPC
- `templates.grpc.controller.ts`NotificationTemplateService gRPC
- app.module.ts 注册 gRPC server :50056
- **验收标准**HealthService.Check 返回 SERVING13 RPC 可调用
- **关联 ISSUE**ISSUE-009RPC 数量待仲裁 13 vs 17
### P6 硬化与对齐
#### T16NotificationsModule 补 exports
- **负责人**ai10
- **依赖**:无
- **交付物**notifications.module.ts 补 `exports: [NotificationsService]`
- **验收标准**BFF 可注入 NotificationsService
#### T17/readyz 多依赖检查
- **负责人**ai10
- **依赖**T4、T5、T6、T8
- **交付物**health.controller.ts /readyz 检查 DB / ES / Redis / Kafka producer / Kafka consumer / PushGateway 6 项依赖
- **验收标准**:符合 02-architecture-design.md §6.6 判定规则DB down → downRedis/ES/Kafka down → degraded
#### T19统一关闭到 LifecycleService
- **负责人**ai10
- **依赖**:无
- **交付物**:移除 main.ts 重复 closeDb/closeEs统一到 LifecycleService8 步关闭序列
- **验收标准**:符合 02-architecture-design.md §6.7 优雅关闭顺序
#### T20-T21DB 改 getDb() + ID 改 cuid2
- **负责人**ai10
- **依赖**:无
- **交付物**database.ts 改 getDb() 函数式service 层 randomUUID → cuid2
- **验收标准**:与 classes 黄金模板对齐
#### T22单元测试覆盖 ≥ 80%
- **负责人**ai10
- **依赖**:全部 P4-P5 任务
- **交付物**`*.spec.ts`Service / Repository / ChannelDispatcher / IdempotencyGuard / OutboxPublisher
- **验收标准**:覆盖率 ≥ 80%
#### T23修正 README
- **负责人**ai10
- **依赖**:全部任务
- **交付物**README.md API 表与实现对齐PUT /:id/read、补 batch/user/:userId/page 端点、补 env 变量表)
- **验收标准**:无文档脱节
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai10 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai10 自行补充
### 4.1 我依赖的上游就绪标志
- [ ] **D1**events.proto 补 UserEvent / RoleEvent / NotificationEvent / MasteryEvent messagecoord 维护)— 🔴 阻塞 T9
- [ ] **D2**events.proto GradeEvent 补 class_id全部事件补 student_ids[]coord 维护)— 🔴 阻塞 T9 fan-out
- [ ] **D3**msg.proto 补 BatchSendNotification / GetUnreadCount / BatchMarkAsRead / MarkAllAsRead / RecallNotification RPCcoord 维护)— 🔴 阻塞 gRPC依赖 ISSUE-009 仲裁)
- [ ] **D4**msg.proto 补 NotificationPreferenceService + NotificationTemplateServicecoord 维护)— 🔴 阻塞 gRPC
- [ ] **D5**push-gateway 提供 gRPC PushService.Push 方法ai02— 🔴 阻塞 T8
- [ ] **D6**iam 发布 6 类 user/role 事件ai06— 🟡 不阻塞开发(用 mock阻塞集成验证
- [ ] **D7**core-edu 发布 5 类教学事件ai08— 🟡 同上
- [ ] **D8**data-ana 发布 mastery 事件ai11— 🟡 同上
- [ ] **D9**Redis 集群可用infra 部署)— 🟡 降级到 DB 唯一索引
### 4.2 我的就绪标志(供下游消费)
- [ ] msg gRPC 50056 启用HealthService.Check 返回 SERVING
- [ ] NotificationService RPC 可调用(数量待 ISSUE-009 仲裁)
- [ ] NotificationPreferenceService RPC 可调用
- [ ] NotificationTemplateService RPC 可调用(含 RenderTemplate
- [ ] `edu.notification.*` topic 可发布(供 push-gateway / data-ana 消费,命名待 ISSUE-008 仲裁)
- [ ] /readyz 返回 6 项依赖状态
- [ ] 测试覆盖率 ≥ 80%
---
## §5 Mock 策略
### 5.1 我提供的 mock供下游
在 msg 真实服务就绪前为下游teacher-bff / student-bff / parent-bff / push-gateway提供 mock
- **gRPC mock**grpc-mock 拦截 50056 端口
- ListNotifications 返回固定 10 条未读通知
- MarkAsRead 返回 success=true
- GetPreference 返回默认偏好in_app + email 开启sms + push 关闭)
- RenderTemplate 返回固定 title + content
- **Kafka mock**msg 就绪前不发布真实通知事件push-gateway 使用本地 stub 推送
### 5.2 我消费的 mock开发期间
在真实上游就绪前msg 使用以下 mock
- **业务事件**core-edu / data-ana 就绪前msg 内置定时器发布本地 stub 事件ExamEvent / HomeworkEvent触发 mock 通知流程验证 consumer 链路
- **用户偏好**iam 就绪前使用默认偏好(所有用户 in_app 开启)
- **模板渲染**:内置 5 个常用模板exam.published / homework.graded / grade.recorded / mastery.warning / system.notice
- **Push Gateway**ai02 就绪前用 fetch POST /internal/push 降级(当前实现保留)
---
## §6 风险与缓解
| 风险 | 影响 | 缓解 |
| ---- | ---- | ---- |
| events.proto 补齐延迟D1-D2 | T9 consumer 无法验证 | 开发期用 stub 事件proto 补齐后切换 |
| RPC 数量仲裁延迟ISSUE-009 | gRPC controller 实现范围不确定 | 先实现 13 RPC 基线,仲裁后增减 |
| topic 命名仲裁延迟ISSUE-008 | Kafka producer/consumer topic 不确定 | 开发期用 02-architecture-design.md §5.3 的 TOPIC_MAP仲裁后统一 |
| push-gateway gRPC 延迟D5 | T8 无法验证 | 保留 fetch POST 降级gRPC 就绪后切换 |

View File

@@ -1,45 +1,333 @@
# parent-bff 工作排期
> 负责人ai05
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/parent-bff_contract.md](../contracts/parent-bff_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/parent-bff_contract.md](../contracts/parent-bff_contract.md)、[02-architecture-design.md](../../../services/parent-bff/docs/02-architecture-design.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
---
## §1 总览
parent-bff 为家长端提供 GraphQL 聚合 API覆盖 Dashboard、多子女切换、成绩趋势等场景。全阶段目标P2 GraphQL schema 骨架 → P3 Dashboard+多子女+成绩趋势 → P4-P6 持续优化
parent-bff 为家长端提供 GraphQL 聚合 API(端口 3010,覆盖 Dashboard、多子女切换、成绩趋势、学情诊断、通知偏好等场景。
**全阶段目标**
| 阶段 | 交付核心 | 依赖 |
| --- | --- | --- |
| P4 MVP | GraphQL Yoga + DataLoader + 多子女切换 + ChildGuard + 并行 gRPC 聚合 + Redis 缓存 + /readyz 下游探针 | iam.GetChildrenByParentI6 裁决)+ core-edu gRPC + data-ana gRPC |
| P5 通知接入 | msg gRPC + push-gateway HTTP + Kafka consumer 缓存失效 + 通知偏好过滤 | msg gRPC 50056 + push-gateway /internal/push |
| P6 硬化 | 熔断器 opossum + HPA + SLO 监控 + 灰度发布 | — |
**关键路径**:批次 0coord 补 proto→ 批次 1iam gRPC + GetChildrenByParent→ 批次 2core-edu gRPC**批次 3parent-bff P4 MVP** → 批次 4msg P5→ parent-bff P5 接入
**P0 阻塞项**(详见 [objections/parent-bff_issue.md](../objections/parent-bff_issue.md) ISSUE-008/007
- iam.GetChildrenByParent RPC + iam_student_guardians 表I6 裁决ai06 负责)
- core-edu ClassService.GetClass proto 缺失ISSUE-008待 coord 仲裁归属)
- msg.proto Notification 缺 child_id 字段ISSUE-007P5 阶段阻塞)
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
title ai05 parent-bff 全阶段排期
title ai05 parent-bff 全阶段排期(全并行 + mock
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a5a, 2026-07-10, Xd
section P4 MVP 核心
P4.1 骨架搭建(env+main+health) :crit, a5a, 2026-07-31, 2d
P4.2 GraphQL Yoga+schema第一版 :crit, a5b, after a5a, 3d
P4.3 DownstreamClient抽象+gRPC mock :crit, a5c, after a5a, 2d
P4.4 ChildGuard+DataLoader实现 :crit, a5d, after a5b, 3d
P4.5 Dashboard/children/grades Resolver :a5e, after a5d, 2d
P4.6 Redis缓存层+ Orchestrator降级 :a5f, after a5e, 2d
P4.7 /readyz下游探针+可观测三支柱 :a5g, after a5f, 1d
P4.8 单元+集成测试(≥80%覆盖) :a5h, after a5g, 2d
section P5 通知接入
P5.1 msg gRPC client接入 :b5a, after a5h, 2d
P5.2 Notification Resolver+偏好配置 :b5b, after b5a, 2d
P5.3 push-gateway HTTP /internal/push :b5c, after b5a, 1d
P5.4 Kafka consumer订阅+缓存失效 :b5d, after b5c, 3d
P5.5 通知偏好过滤逻辑 :b5e, after b5d, 1d
section P6 硬化
P6.1 opossum熔断器per-service :c5a, after b5e, 2d
P6.2 HPA+podAntiAffinity :c5b, after c5a, 1d
P6.3 SLO告警规则+灰度发布 :c5c, after c5b, 2d
```
> **注意**:以上为 coord 初始规划ai05 接管后必须自行细化为完整 P2-P6 排期
> **说明**:以上日期为 coord 总排期推算(批次 3 P4 在批次 2 P3 完成后启动。全并行模式下P4/P5/P6 代码一口气完成,上游未就绪时用 mock最后统一集成测试
---
## §3 详细任务
### 全阶段任务
### P4.1骨架搭建env + main + health
- **负责人**ai05
- **交付物**:⚠️ 由 ai05 自行补充
- **依赖**:见 [contracts/parent-bff_contract.md](../contracts/parent-bff_contract.md)
- **验收标准**:⚠️ 由 ai05 自行补充
- **依赖**:无(克隆 teacher-bff 骨架)
- **交付物**
- `services/parent-bff/package.json`name=@edu/parent-bff
- `src/config/env.ts`Zod 校验,见 02 §12.1 完整配置项)
- `src/main.ts`(启动 + /metrics + SIGTERM 优雅关闭)
- `src/app.module.ts`
- `src/shared/health/health.controller.ts`/healthz 直接 ok
- `src/shared/observability/{logger,metrics,tracer}.ts`service=parent-bff
- `src/shared/errors/{application-error,global-error.filter}.ts`BFF_PARENT_ 前缀)
- `Dockerfile`多阶段EXPOSE 3010
- `tsconfig.json`NodeNext + ESM .js 后缀)
- **验收标准**`pnpm run typecheck` + `pnpm run lint` 零错误;`docker build` 通过;本地启动 /healthz 返回 200
### P4.2GraphQL Yoga + schema 第一版
- **负责人**ai05
- **依赖**P4.1
- **交付物**
- `src/entry/graphql.controller.ts`POST /graphql + GET /graphql playground 仅 dev
- `src/entry/context.middleware.ts`(解析 x-user-id/x-user-roles/x-request-id 注入 GraphQL context
- `src/graphql/schema.ts`typeDefs + resolvers见 02 §4.2 GraphQL schema
- `src/graphql/types/`parent/child/grade/homework/exam/analytics/notification.type.ts
- GraphQL 复杂度限制depth ≤ 7cost ≤ 100002 §9 #5
- `packages/shared-ts/contracts/graphql/parent-bff.graphql`SDL-first 集中管理,对齐 coord ARB-001 模式)
- **验收标准**POST /graphql 可内省 schema深度超 7 的查询被拒cost 超 1000 被拒
### P4.3DownstreamClient 抽象 + gRPC mock
- **负责人**ai05
- **依赖**P4.1 + ai03 DownstreamClient 抽象模式teacher-bff 参考)
- **交付物**
- `src/clients/grpc/grpc.factory.ts`gRPC client 创建 + interceptortrace/metrics/retry
- `src/clients/iam.client.ts`IamClient interface + gRPC impl + mock impl
- `src/clients/core-edu.client.ts`CoreEduClient interface + gRPC impl + mock impl
- `src/clients/data-ana.client.ts`DataAnaClient interface + gRPC impl + mock impl
- mock 数据:固定 2 个孩子student-001 李同学 + student-002 李妹妹)+ 固定成绩/作业/考试/学情
- **验收标准**DEV_MODE=true 时走 mock impl返回固定数据mock 数据 student_id 与 core-edu mock 一致(见 contract.md §4.2
### P4.4ChildGuard + DataLoader 实现
- **负责人**ai05
- **依赖**P4.2 + P4.3
- **交付物**
- `src/aggregation/child-guard.ts`DataScope=CHILDREN 越权校验,见 02 §2.3
- 30s TTL Redis 缓存绑定列表ISSUE-002 修正§3.1.1 同步为 30s
- singleflight 模式防缓存击穿02 §14 #5
- 越权时抛 BFF_PARENT_CHILD_NOT_BOUND(403)
- `src/dataloader/dataloader.module.ts`per-request 实例注册器)
- `src/dataloader/{children,grade,homework,exam}.dataloader.ts`(批量去重 N+1 防御)
- **验收标准**
- childId ∉ 绑定列表时抛 403
- 30s 内第二次查询不调 iam.GetChildrenByParent
- 并发 100 请求只调 iam 1 次singleflight
- DataLoader 同 parentId 多次调用合并为 1 次 gRPC
### P4.5Dashboard / children / grades Resolver
- **负责人**ai05
- **依赖**P4.4
- **交付物**
- `src/graphql/resolvers/dashboard.resolver.ts`(聚合 iam.GetUserInfo + iam.GetChildrenByParent + core-edu.ListGradesByStudent 并行)
- `src/graphql/resolvers/child.resolver.ts`childQuery + ChildGuard 校验 + 延迟加载 grades/homework/exams/analytics
- `src/graphql/resolvers/select-child.resolver.ts`mutation仅审计日志不持久化
- `src/graphql/resolvers/grade.resolver.ts`childGrades query含分页
- **验收标准**
- dashboard Query 返回 parent + children + unreadNotifications
- child(childId) 对未绑定 childId 返回 403
- selectChild mutation 记录审计日志traceId + parentId + childId + timestamp
### P4.6Redis 缓存层 + Orchestrator 降级
- **负责人**ai05
- **依赖**P4.5
- **交付物**
- `src/shared/cache/redis.client.ts`ioredis 连接)
- `src/shared/cache/cache-key.builder.ts`bff:parent:* 前缀,见 02 §3.1.1
- `src/aggregation/orchestrator.ts`Promise.allSettled 并行 + 降级标记 partial
- `src/aggregation/response-mapper.ts`proto → GraphQL type含 Grade.score string→Float 转换ISSUE-008
- `src/aggregation/fallback-strategy.ts`(下游失败时返回缓存陈旧数据或 null 字段)
- **验收标准**
- dashboard 聚合结果缓存 15s第二次命中不调下游
- data-ana 失败时返回 dashboard.degraded=true其他字段正常
- Redis 不可用时降级为内存 LRU
### P4.7/readyz 下游探针 + 可观测三支柱
- **负责人**ai05
- **依赖**P4.6
- **交付物**
- `src/shared/health/health.controller.ts` 补 /readyz 下游探针02 §9 #7 ai05 调整)
- 探针iam gRPC + core-edu gRPC + data-ana gRPC + Redis超时 1s/服务
- 任一失败返回 503 + degraded=true
- metrics 指标全量落地02 §6.4 表格 11 项指标)
- tracer auto-instrumentationshttp/nestjs/express/ioredis/grpc-js
- logger 字段对齐parentId/childId/operation/traceId
- **验收标准**
- /readyz 返回 4 项依赖状态
- /metrics 暴露 parent_bff_* 指标
- Jaeger 可看到 dashboard 请求完整 span 链
### P4.8:单元 + 集成测试≥80% 覆盖)
- **负责人**ai05
- **依赖**P4.7
- **交付物**
- `test/unit/child-guard.test.ts`(越权拦截 + 缓存命中 + singleflight
- `test/unit/orchestrator.test.ts`(并行编排 + 部分失败降级)
- `test/unit/dataloader.test.ts`(批量去重)
- `test/unit/graphql-complexity.test.ts`depth/cost 限制)
- `test/integration/dashboard.test.ts`3 子女 × 3 下游并行Redis Testcontainers
- `test/integration/readyz.test.ts`iam 故障时 503
- `vitest.config.ts`(覆盖率阈值 80%
- **验收标准**:覆盖率 ≥ 80%10 项关键用例02 §11.2)全部通过
### P5.1msg gRPC client 接入
- **负责人**ai05
- **依赖**msg gRPC 50056 就绪ai10或 mock
- **交付物**
- `src/clients/msg.client.ts`MsgClient interface + gRPC impl + mock impl
- env.ts 启用 MsgServiceUrl / MsgGrpcTarget
- **验收标准**mock 模式下 NotificationService.ListNotifications / MarkAsRead 可调
### P5.2Notification Resolver + 偏好配置
- **负责人**ai05
- **依赖**P5.1 + msg.proto 补 child_id 字段ISSUE-007 仲裁结果)
- **交付物**
- `src/graphql/resolvers/notification.resolver.ts`notifications query + markNotificationRead mutation
- `src/graphql/resolvers/notification-preference.resolver.ts`notificationPreferences query + updateNotificationPreferences mutation
- `src/parent/dto/parent-inputs.dto.ts`UpdateNotificationPreferencesSchema Zod 校验)
- **验收标准**notifications Query 返回家长通知列表(含 childId偏好更新后缓存失效
### P5.3push-gateway HTTP /internal/push 接入
- **负责人**ai05
- **依赖**push-gateway /internal/push 就绪ai02或 mock
- **交付物**
- `src/clients/http/push-http.client.ts`HTTP POST /internal/pushU2 仲裁)
- env.ts 启用 PushGatewayUrl
- **验收标准**mock 模式下 pushViaHttp 返回 success
### P5.4Kafka consumer 订阅 + 缓存失效
- **负责人**ai05
- **依赖**Kafka topic 已创建C5 仲裁edu.notification.sent/read/recalled/failed + edu.teaching.grade.recorded/homework.graded/exam.published
- **交付物**
- `src/shared/kafka/kafka.consumer.ts`consumer group: parent-bff-event-subscriber
- `src/shared/kafka/handlers/notification-push.handler.ts`(偏好过滤 + push-gateway 推送)
- `src/shared/kafka/handlers/cache-invalidation.handler.ts`(成绩/作业/考试事件失效对应缓存)
- 幂等性Redis SETNX event_id 去重
- DLQedu.parent-bff.dlq
- **验收标准**
- 收到 edu.teaching.grade.recorded 后 bff:parent:grades:{childId} 缓存失效
- 家长关闭"成绩推送"偏好时,该家长不收到推送
- 重复 event_id 不重复处理
### P5.5:通知偏好过滤逻辑
- **负责人**ai05
- **依赖**P5.4
- **交付物**:通知偏好过滤逻辑集成到 notification-push.handler02 §5.4
- 拉取家长 NotificationPreferencesRedis 缓存 300s
- 按 eventTypeMap 映射事件类型 → 偏好开关
- 取 prefs.channels 与 event.channels 交集
- **验收标准**:偏好开关为 false 时不推送channels 无交集时不推送
### P6.1opossum 熔断器 per-service
- **负责人**ai05
- **依赖**P5 完成
- **交付物**
- `src/clients/grpc/circuit-breaker.ts`opossumper-downstream-service 独立 circuitiam/core-edu/data-ana/msg
- 熔断开启时抛 BFF_PARENT_SERVICE_UNAVAILABLE(503)
- metrics: parent_bff_circuit_state Gauge
- **验收标准**:下游连续失败 5 次熔断开启30s 后半开探测
### P6.2HPA + podAntiAffinity
- **负责人**ai05
- **依赖**P6.1
- **交付物**`infra/k8s/helm/parent-bff/` Chart对齐 004 §1.2 端口)
- HPA 2-10 副本CPU 70% / 内存 80% 触发)
- podAntiAffinity 跨节点分布
- values-dev.yaml / values-staging.yaml / values-prod.yaml
- **验收标准**helm template 通过HPA 可根据负载扩缩
### P6.3SLO 告警规则 + 灰度发布
- **负责人**ai05
- **依赖**P6.2
- **交付物**
- `infra/prometheus/rules.yml` 追加 parent-bff 告警规则P95 > 200ms / 错误率 > 0.1% / 可用性 < 99.9%
- `infra/grafana/dashboards/parent-bff.json` 面板
- 灰度发布:按 parentId hash 路由流量百分比K8s Service + Istio/Envoy weight
- **验收标准**Prometheus 告警规则 lint 通过Grafana 面板可展示 parent-bff 指标
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai05 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai05 自行补充
### 4.1 我依赖的上游就绪标志
| 上游 | 就绪信号 | 责任方 | 阻塞阶段 | 状态 |
| --- | --- | --- | --- | --- |
| iam gRPC 50052 + GetChildrenByParent RPC | HealthService.Check = SERVING + GetChildrenByParent 可调 | ai06 | P4P0 阻塞) | ⏳ |
| iam_student_guardians 表 | 表已建 + Repository 查询方法可用 | ai06 | P4P0 阻塞) | ⏳ |
| core-edu gRPC 50053 | HealthService.Check = SERVING + Exam/Homework/Grade Service 可调 | ai08 | P4 | ⏳ |
| core-edu ClassService.GetClass | proto 补全 + RPC 实现ISSUE-008 待仲裁) | ai08 或 classes | P4 | ⏳ |
| data-ana gRPC 50055 | HealthService.Check = SERVING + AnalyticsService 可调 | ai11 | P4 | ⏳ |
| msg gRPC 50056 | HealthService.Check = SERVING + NotificationService 可调 | ai10 | P5 | ⏳ |
| msg.proto Notification.child_id | proto 字段补全ISSUE-007 待仲裁) | ai10 | P5 | ⏳ |
| push-gateway /internal/push | HTTP 端点可用 | ai02 | P5 | ⏳ |
| api-gateway /parent 路由 | `/api/v1/parent/*` → parent-bff:3010 代理生效 | ai01 | P4 | ⏳ |
| Kafka topic 已创建 | edu.notification.sent/read/recalled/failed + edu.teaching.* | coord/infra | P5 | ⏳ |
| Redis 已部署 | redis://edu-redis:6379 可达 | coord/infra | P4 | ⏳ |
| buf.gen.yaml gRPC 插件 | TS gRPC client 代码生成可用 | coord | P4 | ⏳ |
| ai03 DownstreamClient 抽象 | teacher-bff clients/ 抽象层可参考 | ai03 | P4 | ⏳ |
### 4.2 我的就绪信号(供下游消费)
| 信号 | 检查方式 | 消费方 |
| --- | --- | --- |
| parent-bff GraphQL :3010 启用 | GET /healthz 返回 200 | api-gateway / K8s |
| /readyz 返回 200含 4 下游 gRPC 连通性) | GET /readyz 返回 200 | K8s readinessProbe |
| GraphQL schema 可内省 | POST /graphql 返回 schema | parent-portalai15 |
| 核心 Query 可执行 | dashboard / myChildren / childGrades / childAnalytics | parent-portal |
| 核心 Mutation 可执行 | selectChild / markNotificationReadP5 | parent-portal |
| DataScope=CHILDREN 校验生效 | 家长查询未绑定 childId 返回 403 | 集成测试 |
| metrics 暴露 | GET /metrics 返回 parent_bff_* 指标 | Prometheus |
### 4.3 全并行 Mock 策略
> 开发期间上游未就绪时parent-bff 使用 mock 完成全部 P4-P6 代码,最后统一集成测试。
| 下游 | Mock 方式 | 切换真实时机 |
| --- | --- | --- |
| iam gRPC | grpc-mock 拦截 + 固定 UserInfoparent 角色)+ 固定 2 个 ChildInfo | iam 就绪信号 ✅ |
| core-edu gRPC | grpc-mock 拦截 + 固定成绩/作业/考试 | core-edu 就绪信号 ✅ |
| data-ana gRPC | grpc-mock 拦截 + 固定学情/趋势 | data-ana 就绪信号 ✅ |
| msg gRPC | grpc-mock 拦截 + 固定 10 条通知(含 childId | msg 就绪信号 ✅ |
| push-gateway HTTP | fetch mock + 返回 success | push-gateway 就绪信号 ✅ |
| Redis | Testcontainers 真实 Redis 实例 | — |
| Kafka | kafkajs mock + jest.mock | Kafka topic 创建 ✅ |
**关键**iam.GetChildrenByParent 的 mock 必须返回与 core-edu mock 数据一致的 student_id否则 ChildGuard 越权校验会失败。
---
## §5 跨模块协作需求(需 coord 协调)
| # | 需求 | 涉及 AI | 阻塞阶段 | 协调内容 |
| --- | --- | --- | --- | --- |
| 1 | iam 补 GetChildrenByParent RPC + iam_student_guardians 表 | ai06 | P4P0 | I6 裁决已定ai06 P2.1 即补 |
| 2 | core-edu ClassService 归属仲裁 + proto 补全 | ai08 / classes | P4 | ISSUE-008 待 coord 仲裁 |
| 3 | msg.proto Notification 补 child_id 字段 | ai10 | P5 | ISSUE-007 待 coord 仲裁 |
| 4 | api-gateway 新增 /parent 路由 | ai01 | P4 | main.go + config.go 新增 ParentBffURL |
| 5 | 004 §4 依赖图同步 C6 仲裁(补 DataAna + Msg | coord | P4 | ISSUE-004 |
| 6 | parent-portal 文档同步 GraphQL 决策 | ai15 | P4 | ISSUE-005 跨模块契约冲突 |
| 7 | buf.gen.yaml 补 gRPC TS 插件 | coord | P4 | 02 §7.3 #7 |
| 8 | docker-compose.deploy.yml 新增 parent-bff 服务 | coord | P4 | 端口 3010 + edu-net |
| 9 | full-stack-runbook 端口矩阵追加 3010 | coord | P4 | 02 §7.3 #5 |
| 10 | shared-ts/contracts/graphql/parent-bff.graphql 建库 | ai05 | P4 | SDL-first 集中管理 |

View File

@@ -2,44 +2,331 @@
> 负责人ai15
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/parent-portal_contract.md](../contracts/parent-portal_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
---
## §1 总览
parent-portal 是家长端微前端,通过 MF Remote 接入主应用,覆盖 Dashboard、多子女切换等场景。全阶段目标P2 MF Remote 骨架 → P3 Dashboard+多子女切换 → P4-P6 持续优化
parent-portal 是家长端微前端MF Remote),挂载到 teacher-portal Shell覆盖家长仪表盘、多子女切换、子女学情查看、通知偏好等场景
- **MF 角色**RemoteShell = teacher-portal :4000
- **端口**4002dev/prod 一致,[port-allocation.md](../../../../infra/port-allocation.md) §4
- **阶段归属**P4 启动(依赖 P4 的 parent-bff + data-ana 就绪)
- **DataScope**CHILDREN仅查看自己绑定子女的数据
- **全阶段目标**P4 MF Remote 骨架 + 核心页面 → P5 推送接入 + 通知中心 → P6 硬化A11y/性能/安全/PWA/多语言)
> **前置阻塞**(见 [objections/parent-portal_issue.md](../objections/parent-portal_issue.md) ISSUE-010
> - iam `GetChildrenByParent` 接口缺失P0 阻塞ai06 补全),补全前用 mock固定 2 个子女 student-001 + student-002
> - ISSUE-001 待 coord 仲裁REST vs GraphQL仲裁前按 GraphQL 预排期mock 用 MSW 拦截 GraphQL
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P4-P6
```mermaid
gantt
title ai15 parent-portal 全阶段排期
title ai15 parent-portal 全阶段排期P4-P6
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a15a, 2026-07-10, Xd
section P4 骨架与核心页面11d
4.1 MF Remote 骨架+next.config.js+健康检查 :crit, a15a, 2026-07-29, 2d
4.2 GraphQL client接入+MSW mock层 :crit, a15b, after a15a, 1d
4.3 ChildSwitcher+useChildSwitcher+Zustand slice :crit, a15c, after a15b, 2d
4.4 Dashboard页面+ParentDashboard组件 :a15d, after a15c, 2d
4.5 子女成绩页面+ChildGradeChart :a15e, after a15c, 1d
4.6 子女作业页面 :a15f, after a15e, 1d
4.7 通知偏好页面+PreferenceForm+Zod :a15g, after a15d, 1d
4.8 跨标签同步(BroadcastChannel) :a15h, after a15c, 1d
section P4 质量保障(并行)
4.9 Vitest单测+MSW集成测试 :a15i, after a15g, 2d
4.10 Dockerfile多阶段构建 :a15j, after a15a, 1d
section P5 推送与通知中心5d
5.1 WebSocket接入+事件处理 :crit, a15k, after a15i, 2d
5.2 通知中心页面+NotificationFeed :a15l, after a15k, 2d
5.3 推送降级(HTTP轮询) :a15m, after a15l, 1d
section P6 硬化8d
6.1 Web Vitals+OTel browser SDK :a15n, after a15m, 2d
6.2 A11y WCAG 2.2 AA审计+修复 :a15o, after a15n, 2d
6.3 性能优化+bundle分析 :a15p, after a15o, 1d
6.4 多语言扩展(en-US) :a15q, after a15p, 1d
6.5 PWA(Service Worker+manifest) :a15r, after a15q, 1d
6.6 安全硬化(CSP+敏感数据脱敏) :a15s, after a15r, 1d
```
> **注意**:以上为 coord 初始规划ai15 接管后必须自行细化为完整 P2-P6 排期。
> **总工期**P4 11d + P5 5d + P6 8d = 24d约 5 周)
> **关键路径**(红色 critMF 骨架 → GraphQL client → ChildSwitcher → 质量保障 → WebSocket → 通知中心
---
## §3 详细任务
### 阶段任务
### P4 阶段任务
#### P4-1MF Remote 骨架 + next.config.js + 健康检查
- **负责人**ai15
- **交付物**:⚠️ 由 ai15 自行补充
- **依赖**:见 [contracts/parent-portal_contract.md](../contracts/parent-portal_contract.md)
- **验收标准**:⚠️ 由 ai15 自行补充
- **依赖**teacher-portal Shell MF 配置就绪ARB-002ai13 P2 交付ISSUE-002 仲裁MF shared 清单)
- **交付物**
- `apps/parent-portal/next.config.js`NextFederationPluginRemote 角色,`name: parent_app``exposes: ./pages + ./ChildSwitcher``shared` 含 ARB-002 全部 7 项)
- `apps/parent-portal/src/app/layout.tsx`RootLayout复用 Shell 暴露的字体/令牌/i18n Provider
- `apps/parent-portal/src/app/api/health/route.ts``GET /api/health``{ status: 'ok', ts }`
- `apps/parent-portal/src/app/api/ready/route.ts``GET /api/ready` → 检查 API_GATEWAY_URL 可达)
- `apps/parent-portal/tsconfig.json`(继承 tsconfig.base.jsonstrict
- `apps/parent-portal/package.json`(依赖对齐 Shellreact 18.3 + next 14 + urql + @tanstack/react-query v5 + zustand + nuqs
- **验收标准**
1. `pnpm --filter parent-portal dev` 启动 :4002
2. `GET /api/health` 返回 200
3. teacher-portal Shell 能加载 parent-portal remoteEntry.jsMF 拓扑验证)
4. feature flag `NEXT_PUBLIC_MF_ENABLED` 可控制 MF 开关
#### P4-2GraphQL client 接入 + MSW mock 层
- **负责人**ai15
- **依赖**P4-1ISSUE-001 仲裁(确认 GraphQLteacher-portal Shell 暴露 GraphQLProviderARB-002
- **交付物**
- `apps/parent-portal/src/lib/graphql-client.ts`(从 Shell 暴露的 `useGraphQLClient()` 获取 urql client 单例)
- `apps/parent-portal/src/graphql/queries/`currentUser / myChildren / childSummary / childGrades / childHomework / childTrend / childWeakness 查询文档)
- `apps/parent-portal/src/graphql/mutations/`markAsRead / updateNotificationPreferences mutation 文档)
- `apps/parent-portal/src/mocks/handlers.ts`MSW 拦截 `POST /api/v1/parent/graphql`,按 operationName 返回 mock
- `apps/parent-portal/src/mocks/fixtures/*.json`(固定 2 个子女 student-001 李同学 + student-002 李妹妹,与 parent-bff mock 一致)
- `apps/parent-portal/src/mocks/browser.ts`MSW worker 初始化,`NEXT_PUBLIC_API_MOCKING=enabled` 控制)
- **验收标准**
1. MSW enabled 时,所有 GraphQL 查询返回 mock 数据
2. myChildren mock 返回 2 个子女id 与 childGrades/childHomework mock 的 student_id 一致
3. urql client 单例跨组件共享MF shared singleton 验证)
#### P4-3ChildSwitcher + useChildSwitcher + Zustand slice
- **负责人**ai15
- **依赖**P4-2ISSUE-009 仲裁switchChild 是 GraphQL Mutation 还是纯前端状态)
- **交付物**
- `apps/parent-portal/src/stores/childSwitcherSlice.ts`Zustand slicechildren / currentChildId / isLoading / error / switchChild / refreshChildren
- `apps/parent-portal/src/hooks/useChildSwitcher.ts`(封装 myChildren GraphQL query + 切换逻辑 + invalidate 子女维度查询)
- `apps/parent-portal/src/components/ChildSwitcher.tsx`variant: tab | dropdown状态机idle/switching/switched/error
- `apps/parent-portal/src/components/MultiChildTabBar.tsx`≤3 子女用 Tab>3 用下拉,移动端友好)
- localStorage 持久化 `parent:currentChildId`(刷新恢复)
- **验收标准**
1. 切换子女后,`['parent','grades',currentChildId]` 等子女维度查询 invalidate 重拉
2. 刷新页面后 currentChildId 从 localStorage 恢复
3. 0 子女显示 EmptyChildState1 子女不显示 TabBar2-3 子女显示 Tab
4. 切换子女竞态:快速连续切换,旧请求 abort新数据正确显示
#### P4-4Dashboard 页面 + ParentDashboard 组件
- **负责人**ai15
- **依赖**P4-3
- **交付物**
- `apps/parent-portal/src/app/(app)/parent/dashboard/page.tsx`
- `apps/parent-portal/src/components/ParentDashboard.tsx`(组合 useChildren + useChildSummary插槽summary-cards / todo-reminders / recent-grades / attendance / custom
- `apps/parent-portal/src/components/ChildSummaryCard.tsx`(单子女概览:头像/姓名/年级/今日作业数/成绩趋势缩略图)
- `apps/parent-portal/src/components/AttendanceCalendar.tsx`(出勤日历热力图)
- **验收标准**
1. 多子女并列卡片展示
2. 权限校验:`PARENT_DASHBOARD_VIEW`,用 `<RequirePermission>`
3. SSR 首屏 + CSR 交互(依 02 §14.4 渲染策略)
#### P4-5子女成绩页面 + ChildGradeChart
- **负责人**ai15
- **依赖**P4-3
- **交付物**
- `apps/parent-portal/src/app/(app)/parent/grades/page.tsx`
- `apps/parent-portal/src/components/ChildGradeChart.tsx`recharts 折线 + 班级均分对比 + 多子女对比模式)
- **验收标准**
1. 权限校验:`GRADES_READ_CHILD`
2. 切换子女后图表刷新
3. 多子女对比模式可选
#### P4-6子女作业页面
- **负责人**ai15
- **依赖**P4-3
- **交付物**`apps/parent-portal/src/app/(app)/parent/homework/page.tsx`
- **验收标准**
1. 权限校验:`HOMEWORK_READ_CHILD`
2. 复用 Shell 暴露的 DataTable 展示作业列表
#### P4-7通知偏好页面 + PreferenceForm + Zod
- **负责人**ai15
- **依赖**P4-4
- **交付物**
- `apps/parent-portal/src/app/(app)/parent/preferences/page.tsx`
- `apps/parent-portal/src/components/PreferenceForm.tsx`(矩阵式 UI子女×事件×渠道react-hook-form + zodResolver
- `apps/parent-portal/src/schemas/notificationPreferences.ts`Zod schema见 02 §14.4
- **验收标准**
1. 权限校验:`PARENT_PREFERENCES_UPDATE`
2. 不可用渠道 Toggle disabled + tooltip
3. 保存成功后 invalidate `['parent','preferences']`
4. 表单 dirty 状态追踪 + 离开页提示
#### P4-8跨标签同步BroadcastChannel
- **负责人**ai15
- **依赖**P4-3
- **交付物**
- `apps/parent-portal/src/lib/crossTabSync.ts`(见 02 §16.2 完整实现)
- 集成到 RootLayout`useCrossTabSync()`
- **验收标准**
1. Tab A 切换子女 → Tab B 同步更新
2. LWW 冲突解决ts 大的胜出)
3. Safari 降级为 storage 事件
#### P4-9Vitest 单测 + MSW 集成测试
- **负责人**ai15
- **依赖**P4-4 ~ P4-7
- **交付物**
- `apps/parent-portal/vitest.config.ts`
- 单测:组件渲染/交互、Hook 逻辑、Zod schema 校验、纯函数 utils覆盖率 ≥ 85%
- 集成测试useChildSwitcher + Zustand slice + invalidate 流程MSW mock覆盖率 ≥ 75%
- **验收标准**
1. `pnpm --filter parent-portal test` 全绿
2. 覆盖率达标(单元 ≥ 85%,集成 ≥ 75%
#### P4-10Dockerfile 多阶段构建
- **负责人**ai15
- **依赖**P4-1
- **交付物**`apps/parent-portal/Dockerfile`builder + runtimenode:22-alpine
- **验收标准**
1. `docker build` 成功
2. HEALTHCHECK 指向 `/api/health`
3. 镜像体积 < 300MB
### P5 阶段任务
#### P5-1WebSocket 接入 + 事件处理
- **负责人**ai15
- **依赖**push-gateway :8081/ws 就绪ai02parent-portal P4 完成
- **交付物**
- `apps/parent-portal/src/hooks/useWebSocket.ts`(连接 push-gateway wsJWT 鉴权,自动重连)
- 事件处理:`NotificationRequested` → toast + 未读数+1`GradeRecorded` → toast + 成绩 invalidate`SchoolAnnouncement` → toast + dashboard invalidate
- **验收标准**
1. WS 连接建立后收事件正常
2. 断线自动重连(指数退避)
#### P5-2通知中心页面 + NotificationFeed
- **负责人**ai15
- **依赖**P5-1
- **交付物**
- `apps/parent-portal/src/app/(app)/parent/notifications/page.tsx`
- `apps/parent-portal/src/components/NotificationFeed.tsx`(按子女×类型×已读筛选,批量已读,跳转,置顶)
- **验收标准**
1. 权限校验:`NOTIFICATION_READ_OWN`
2. WebSocket 推送 invalidate 通知列表
#### P5-3推送降级HTTP 轮询)
- **负责人**ai15
- **依赖**P5-1
- **交付物**WS 重试 5 次失败后降级为 HTTP 轮询60s 拉取通知列表)
- **验收标准**:降级后通知延迟 ≤ 60s用户感知降级提示
### P6 阶段任务
#### P6-1Web Vitals + OTel browser SDK
- **交付物**`next/web-vitals` 上报 + OTel browser SDK复用 Shell 暴露的 TracerProvider
- **验收标准**LCP/CLS/TTFB 指标上报到 Gateway
#### P6-2A11y WCAG 2.2 AA 审计 + 修复
- **交付物**axe-core 自动扫描 + 手动键盘导航测试0 严重违规
- **验收标准**:所有页面 0 严重 A11y 违规
#### P6-3性能优化 + bundle 分析
- **交付物**bundle 分析报告 + 代码分割优化(首屏 JS ≤ 80KB gzipped
- **验收标准**Lighthouse 移动端 4G ≥ 90 分
#### P6-4多语言扩展en-US
- **交付物**`apps/parent-portal/src/i18n/messages/en-US/*.json`(镜像 zh-CN 结构)
- **验收标准**en-US 完成度 100%
#### P6-5PWAService Worker + manifest
- **交付物**`public/manifest.json` + Service Worker 缓存策略(见 02 §18.4
- **验收标准**:可安装到主屏,离线可查看缓存的子女数据
#### P6-6安全硬化CSP + 敏感数据脱敏)
- **交付物**CSP 头配置(复用 Shell+ 截图脱敏 + 页面离开遮罩
- **验收标准**CSP 无违规报告;敏感数据不泄漏
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai15 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai15 自行补充
### 4.1 我依赖的上游就绪标志
| 上游 | 就绪信号 | 提供方 | 状态 | 阻塞影响 |
| ---- | -------- | ------ | ---- | -------- |
| teacher-portal Shell | MF exposesAppShell + GraphQLProvider + hooks + UI 组件)+ shared singleton | ai13 | ⏳ P2 | P4 无法启动 |
| parent-bff GraphQL | `POST /graphql` :3010 + currentUser/myChildren/childSummary/childGrades Query | ai05 | ⏳ P4 | 核心数据源 |
| iam GetChildrenByParent | gRPC 50052 + `iam_student_guardians` 表 | ai06 | ⏳ P3 补全 | P0 多子女阻塞(见 ISSUE-010 |
| core-edu | gRPC 50053 + GradeService/HomeworkService/AttendanceService | ai08 | ⏳ P3 | 成绩/作业数据 |
| data-ana | gRPC 50055 + AnalyticsService | ai11 | ⏳ P4 | 学情分析数据 |
| msg | gRPC 50056 + NotificationService | ai10 | ⏳ P5 | 通知中心 |
| push-gateway | :8081/ws WebSocket | ai02 | ⏳ P5 | 实时推送 |
| shared-ts | ApiClient/Loggercoord 维护) | coord | ⏳ | 基础工具 |
| contracts | Permissions 常量coord 维护) | coord | ⏳ | 权限校验 |
| ui-tokens / ui-components / hooks | 三层令牌 + shadcn + usePermission/useAuthai07/ai13 维护) | ai07/ai13 | ⏳ P2 收尾 | UI 基础 |
### 4.2 我的就绪信号(供下游消费)
| 信号 | 说明 | 阶段 |
| ---- | ---- | ---- |
| parent-portal :4002 dev server 启用 | MF Remote 可被 Shell 加载 | P4-1 完成 |
| MF Remote remoteEntry.js 可加载 | Shell 端 `remotes.parent` 可解析 | P4-1 完成 |
| 核心 GraphQL 查询可执行 | currentUser / myChildren / childSummary 返回数据mock 或真实) | P4-2 完成 |
| 多子女切换可用 | ChildSwitcher + invalidate 流程通过 | P4-3 完成 |
| Dashboard 可访问 | 家长登录 → 看到 Dashboard含子女卡片 | P4-4 完成 |
| 健康检查通过 | `GET /api/health` + `GET /api/ready` 200 | P4-1 完成 |
| 测试覆盖率达标 | 单元 ≥ 85% + 集成 ≥ 75% | P4-9 完成 |
| Docker 镜像可构建 | `docker build` 成功 | P4-10 完成 |
### 4.3 全并行 Mock 策略
| 消费接口 | Mock 方式 | 切换真实时机 |
| -------- | --------- | ------------ |
| parent-bff GraphQL | MSW 拦截 `POST /api/v1/parent/graphql`,按 operationName 返回 fixtures | parent-bff GraphQL :3010 就绪 ✅ |
| iam login | MSW 返回固定 JWTparent 角色) | api-gateway + iam 就绪 ✅ |
| push-gateway WebSocket | mock-socket 模拟 WS 推送(每 30s 1 条通知) | push-gateway :8081 就绪 ✅ |
| 子女数据一致性 | myChildren mock 返回 student-001 + student-002与所有 child* 查询 student_id 一致 | iam GetChildrenByParent 就绪 |
> Mock 由 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制,上游就绪后设为 `disabled`。
---
## §5 风险与缓解
| 风险 | 影响 | 缓解 |
| ---- | ---- | ---- |
| ISSUE-001/002 未仲裁REST vs GraphQL | P4-2 GraphQL client 接入方向不确定 | 先按 GraphQL 预排期;仲裁若改 RESTP4-2 重写(预计 1d |
| iam GetChildrenByParent 缺失ISSUE-010 | 多子女场景无法落地 | mock 固定 2 子女开发coord 跟踪 ai06 P3 补全 |
| MF SSR 对齐复杂 | Remote SSR 需 Shell 上下文 | 优先 CSR仅 Dashboard 首屏 SSRP4-1 PoC 验证 |
| parent-bff 契约未最终确认 | GraphQL schema 可能变动 | P4 启动前与 ai05 对齐 schemaMSW mock 解耦 |
| TanStack Query 缓存膨胀 | 多子女历史查询堆积 | gcTime 5min + 切换子女清理非当前子女缓存 |
---
## §6 质量门禁
每个任务完成前必须通过:
- `pnpm --filter parent-portal lint` 零错误
- `pnpm --filter parent-portal typecheck` 零错误
- `pnpm --filter parent-portal test` 全绿P4-9 起强制)
- 设计令牌三层规则(无 `#hex` / 无硬编码字体 / 无任意值ESLint 强制)
- A11yjsx-a11y error 级零违规
> 提交前校验见 [project_rules §8](../../../../.trae/rules/project_rules.md)commit 遵循 Conventional Commits`feat(parent-portal): ...`

View File

@@ -1,45 +1,406 @@
# push-gateway 工作排期
> 负责人ai02
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/push-gateway_contract.md](../contracts/push-gateway_contract.md)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/push-gateway_contract.md](../contracts/push-gateway_contract.md)、[objections/push-gateway_issue.md](../objections/push-gateway_issue.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 依据:[ai-allocation.md](../../ai-allocation.md) §7.2、[02-architecture-design.md](../../../services/push-gateway/docs/02-architecture-design.md) §12 实施优先级、[president-final-rulings.md](../../president-final-rulings.md) §3.4 回写义务
---
## §1 总览
push-gateway 负责长连接接入HTTP /internal/* + WebSocket/SSE与多设备会话管理配合审计表实现消息推送可观测。全阶段目标P2 HTTP+WS 接入 → P3 多设备会话 → P4-P6 审计与加固
push-gateway 是 Go 实现的实时推送基础设施服务L3 网关层),管理 WebSocket 长连接,接收 msg 服务的推送请求投递到在线客户端。无业务状态,仅持有连接池 + Redis 跨实例广播
**全阶段目标**
- **批次 0等待期**:复审 02 文档 + 回写 ISSUE-053/055/056/058 裁决 + 提请 ISSUE-001~007 仲裁
- **批次 4P513 天)**:完整实现 /internal/push + WebSocket 生命周期 + Redis Pub/Sub + Kafka 消费 + /readyz + /metrics + OTel + 多阶段 Dockerfile + slog + shared-go 接入 + 集成测试
- **批次 5P6 硬化5 天)**Reconnect 协议 + Redis Stream 持久化 + 测试覆盖率 ≥ 80% + ADR/非功能性需求/失败模式章节补全
**关键路径依赖**
- 批次 0.14shared-go 包骨架coord—— tracer/logger/jwks/env 4 模块
- 批次 1iam P2.1JWT RS256 + JWKS 端点)—— WebSocket 鉴权前置
- 批次 4msg gRPC 50056 + edu.notification.requested topic —— 推送事件来源
---
## §2 全阶段甘特图(P2-P6各 AI 自行细化
## §2 全阶段甘特图(批次 0 + 批次 4 + 批次 5
```mermaid
gantt
title ai02 push-gateway 全阶段排期
title ai02 push-gateway 全阶段排期(批次 0 + 批次 4 P5 + 批次 5 P6
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a2a, 2026-07-10, Xd
section 批次0 等待期 2天
0.1 复审01/02文档+核查已有仲裁 :crit, a0a, 2026-07-10, 1d
0.2 回写ISSUE-053/055/056/058到02文档 :crit, a0b, after a0a, 1d
0.3 提请ISSUE-001~007待coord仲裁 :a0c, after a0a, 1d
section 批次4 P5-P0 安全加固 3天
4.1 Origin校验+CheckOrigin白名单 :crit, a4a, after b3a, 1d
4.2 /internal/*鉴权对齐X-Internal-Token :crit, a4b, after a4a, 1d
4.3 心跳改WebSocket控制帧+SetReadDeadline :crit, a4c, after a4a, 1d
4.4 单用户连接数限制MaxConn=5 :crit, a4d, after a4c, 1d
4.5 Send通道满时指标+日志 :a4e, after a4d, 1d
section 批次4 P5-P0 基础设施 2天
4.6 Dockerfile重构多阶段+非root+healthcheck+ldflags :crit, a4f, after b3a, 1d
4.7 引入log/slog替换标准log :a4g, after a4f, 1d
4.8 接入shared-go tracer/logger/jwks/env :a4h, after a4g, 1d
section 批次4 P5-P1 横向扩展 3天
4.9 Redis Pub/Sub跨实例广播 :crit, a4i, after a4h, 2d
4.10 Redis SET在线状态+启动重建(ISSUE-058) :crit, a4j, after a4i, 1d
4.11 /metrics自定义指标+/readyz软失败(ISSUE-055) :a4k, after a4j, 1d
4.12 优雅关闭所有WebSocket连接 :a4l, after a4k, 1d
section 批次4 P5-P2 高级功能 3天
4.13 Kafka消费edu.notification.requested(ISSUE-053) :a4m, after a4j, 2d
4.14 JWT RS256升级+JWKS fetcher :a4n, after a4m, 1d
4.15 设计决策记录章节回写(ISSUE-056) :a4o, after a4n, 1d
section 批次4 P5-P2 集成 2天
4.16 与msg联调/internal/push双通道 :crit, a4p, after a4o, 1d
4.17 集成测试+端到端验证 :crit, a4q, after a4p, 1d
section 批次5 P6 硬化 5天
5.1 Reconnect协议session_id+last_seq :a5a, after a4q, 2d
5.2 Redis Stream替代Pub/Sub持久化 :a5b, after a5a, 2d
5.3 测试覆盖率≥80% :crit, a5c, after a4q, 3d
5.4 ADR+非功能性需求+失败模式章节(ISSUE-007) :a5d, after a4q, 2d
```
> **注意**:以上为 coord 初始规划ai02 接管后必须自行细化为完整 P2-P6 排期。
> **依赖锚点**`b3a` = 批次 3 完成信号content + data-ana 就绪,见 [workline.md](../workline.md) §1 批次 3
> **总工期**:批次 02 天)+ 批次 413 天)+ 批次 55 天)= **20 天**
---
## §3 详细任务
### 全阶段任务
### 3.1 批次 0等待期2 天)
#### 任务 0.1:复审 01/02 文档 + 核查已有仲裁
- **负责人**ai02
- **交付物**:⚠️ 由 ai02 自行补充
- **依赖**:见 [contracts/push-gateway_contract.md](../contracts/push-gateway_contract.md)
- **验收标准**:⚠️ 由 ai02 自行补充
- **依赖**:无
- **交付物**
- [objections/push-gateway_issue.md](../objections/push-gateway_issue.md) §0 核查矩阵
- 01/02 文档审查结论(已汇报给用户)
- **验收标准**
- 5 项已有仲裁ISSUE-053/055/056/058 + ARB /internal/push核查完成
- 01 文档 7 项偏差登记
- 02 文档 4 项未回写 + 3 项规范缺失登记
#### 任务 0.2:回写 ISSUE-053/055/056/058 到 02 文档
- **负责人**ai02
- **依赖**:任务 0.1
- **交付物**02-architecture-design.md 修订
- §5.1 topic 改为 `edu.notification.requested`ISSUE-053
- §6.7 增 Kafka 软失败逻辑 + Redis 软失败ISSUE-055/058待 ISSUE-006 仲裁最终策略)
- 新增 §5.4"设计决策记录gRPC vs HTTP 协议选型coord 已采纳 P1"ISSUE-056
- §3.1/§8.4 补 Hub 启动 Redis SET 重建 + 60s 不一致窗口文档化 + `push_gateway_redis_set_rebuild_total` 指标ISSUE-058
- **验收标准**4 项裁决全部回写,[objections/push-gateway_issue.md](../objections/push-gateway_issue.md) §0 核查矩阵状态更新为 ✅
#### 任务 0.3:提请 ISSUE-001~007 待 coord 仲裁
- **负责人**ai02
- **依赖**:任务 0.1
- **交付物**[objections/push-gateway_issue.md](../objections/push-gateway_issue.md) §1 七项 issue
- **验收标准**coord 在 [coord.md](../coord.md) 追加 ARB-003+ 仲裁章节
### 3.2 批次 4P5完整实现13 天)
#### 任务 4.1Origin 校验 + CheckOrigin 白名单P01 天)
- **负责人**ai02
- **依赖**:批次 3 完成信号 `b3a`
- **交付物**[internal/ws/handler.go](../../../services/push-gateway/internal/ws/handler.go) 修订
- `upgrader.CheckOrigin``return true` 改为读 `WS_ALLOWED_ORIGINS` 环境变量白名单
- 无 Origin 头拒绝
- **验收标准**
- 非白名单 Origin 返 403
- 白名单来源teacher/student/parent portal 域名)通过
#### 任务 4.2/internal/* 鉴权对齐 X-Internal-TokenP01 天)
- **负责人**ai02
- **依赖**:任务 4.1 + ISSUE-002 仲裁结果
- **交付物**
- [internal/ws/handler.go](../../../services/push-gateway/internal/ws/handler.go) `internalAPIKeyHeader` 改为 `X-Internal-Token`(待仲裁确认)
- [internal/config/config.go](../../../services/push-gateway/internal/config/config.go) `InternalAPIKey``InternalAPIToken`,环境变量 `INTERNAL_API_TOKEN`
- 错误码对齐 `PUSH_UNAUTHORIZED`
- **验收标准**:无 token/错 token 返 401 + `PUSH_UNAUTHORIZED`DevMode 跳过
#### 任务 4.3:心跳改用 WebSocket 控制帧 + SetReadDeadlineP01 天)
- **负责人**ai02
- **依赖**:任务 4.1
- **交付物**[internal/ws/handler.go](../../../services/push-gateway/internal/ws/handler.go) 重构
- 移除文本消息 `ping/pong` 逻辑([handler.go#L82-L84](../../../services/push-gateway/internal/ws/handler.go#L82-L84)
- 改用 `conn.SetReadDeadline(60s)` + `conn.SetPongHandler`
- 客户端 Ping 控制帧 → gorilla 自动回 Pong
- 60s 无任何消息则关闭连接
- **验收标准**
- 僵尸连接 60s 后自动清理
- 心跳走 RFC 6455 控制帧,不再走文本消息
#### 任务 4.4:单用户连接数限制 MaxConn=5P01 天)
- **负责人**ai02
- **依赖**:任务 4.3
- **交付物**[internal/hub/hub.go](../../../services/push-gateway/internal/hub/hub.go) 修订
- Hub 增加 `counters map[string]int`02 文档 §2
- `Register` 检查 `counters[userID] >= 5``ErrTooManyConnections`
- 超限返 close 帧code=1008 policy violation
- **验收标准**:第 6 个连接被拒,错误码 `PUSH_TOO_MANY_CONNECTIONS` 429
#### 任务 4.5Send 通道满时指标 + 日志P01 天)
- **负责人**ai02
- **依赖**:任务 4.4
- **交付物**[internal/hub/hub.go](../../../services/push-gateway/internal/hub/hub.go) `Send` 方法修订
- 通道满时 `slog.Warn` + `messages_dropped_total` Counter
- **验收标准**`/metrics` 暴露 `push_gateway_messages_dropped_total{reason="channel_full"}`
#### 任务 4.6Dockerfile 重构P01 天)
- **负责人**ai02
- **依赖**:批次 3 完成信号 `b3a`
- **交付物**[Dockerfile](../../../services/push-gateway/Dockerfile) 重构
- 多阶段(已有,保留)
- 非 root 用户(`adduser -D appuser` + `USER appuser`
- healthcheck`wget --spider http://localhost:8081/healthz`
- ldflags 优化(`-ldflags="-s -w -X main.Version=$(git rev-parse --short HEAD)"`
- go.mod 与 Dockerfile 版本对齐1.25.0 vs golang:1.22-alpine
- **验收标准**`docker build` 通过,容器以非 root 运行healthcheck 工作
#### 任务 4.7:引入 log/slog 替换标准 logP01 天)
- **负责人**ai02
- **依赖**:任务 4.6
- **交付物**:新增 `internal/observability/logger.go`
- `slog.NewJSONHandler` + `slog.SetDefault`
- 日志字段:`timestamp` `level` `service=push-gateway` `request_id` `trace_id` `user_id` `conn_id` `event`
- main.go / handler.go / hub.go 替换所有 `log.Printf``slog`
- **验收标准**:日志输出 JSON 格式,包含 trace_id 字段
#### 任务 4.8:接入 shared-goP01 天)
- **负责人**ai02
- **依赖**:任务 4.7 + 批次 0.14shared-go 骨架)
- **交付物**
- go.mod 引入 `github.com/edu-cloud/shared-go`
- 替换本地 [observability/tracer.go](../../../services/push-gateway/internal/observability/tracer.go) 为 `shared-go/observability/tracer`
- 引入 `shared-go/observability/logger`(替换任务 4.7 本地实现)
- 引入 `shared-go/config/env`(替换 [config.go](../../../services/push-gateway/internal/config/config.go) `getEnv`
- 引入 `shared-go/auth/jwks`(任务 4.14 使用)
- **验收标准**:本地 tracer.go/logger.go 删除,统一从 shared-go import
#### 任务 4.9Redis Pub/Sub 跨实例广播P12 天)
- **负责人**ai02
- **依赖**:任务 4.8
- **交付物**:新增 `internal/redis/pubsub.go` + Hub 改造
- 订阅 `edu.push.channel.user.*` + `edu.push.channel.broadcast`
- 本实例无目标用户时 PUBLISH 到对应 channel
- 持有该用户的实例订阅后投递到本地连接
- 引入 `github.com/redis/go-redis/v9` 依赖
- **验收标准**
- 双实例部署msg 调实例 A `/internal/push user=B`,实例 B 持有 B → 收到推送
- 广播 PUBLISH 一次,所有实例投递本地连接
#### 任务 4.10Redis SET 在线状态 + 启动重建P11 天)
- **负责人**ai02
- **依赖**:任务 4.9
- **交付物**Hub 改造(对齐 ISSUE-058
- 连接建立:`SADD edu:push:online:<userID> <instanceID>` + `EXPIRE 60s`
- 心跳续期:`EXPIRE 60s`
- 连接断开:`SREM` + 空 SET 则 `DEL`
- **Hub 启动重建**:遍历内存连接 SADD + EXPIRE先清空 Redis 中本 instanceID 旧成员(避免幽灵)
- 实例崩溃 SET 自然过期60s
- **验收标准**
- 实例重启后 60s 内 Redis SET 重建完成
- `push_gateway_redis_set_rebuild_total` 指标暴露
#### 任务 4.11/metrics 自定义指标 + /readyz 软失败P11 天)
- **负责人**ai02
- **依赖**:任务 4.10
- **交付物**:新增 `internal/observability/metrics.go` + /readyz 重构
- 指标清单02 文档 §6.4`active_connections` `messages_pushed_total` `messages_dropped_total` `heartbeat_total` `disconnect_total` `redis_pubsub_latency_seconds` `kafka_consumed_total` `redis_set_rebuild_total`
- /readyz 检查 Redis PING + Kafka consumer lag
- Redis/Kafka 软失败ISSUE-055/058待 ISSUE-006 仲裁):返 200 + `degraded: true`
- **验收标准**
- `/metrics` 暴露 8+ 自定义指标
- Redis 故障时 /readyz 返 200 + `degraded: true`(不返 503
#### 任务 4.12:优雅关闭所有 WebSocket 连接P11 天)
- **负责人**ai02
- **依赖**:任务 4.11
- **交付物**[main.go](../../../services/push-gateway/main.go) + Hub 改造
- Hub 新增 `CloseAll()` 方法,向所有连接发 close 帧code=1001 going away
- SIGTERM → 标记 Hub closing拒新连接→ CloseAll → 等 10s → srv.Shutdown → 关 Kafka consumer → 关 Redis subscriber → tracerShutdown
- **验收标准**SIGTERM 后所有连接收到 close 帧,无连接泄漏
#### 任务 4.13Kafka 消费 edu.notification.requestedP22 天)
- **负责人**ai02
- **依赖**:任务 4.10 + msg 就绪信号
- **交付物**:新增 `internal/kafka/consumer.go`
- Consumer Group `push-gateway`
- 订阅 `edu.notification.requested`ISSUE-053 裁决的 topic 名)
- 至少一次 + 重试 3 次入 DLQ
- 幂等:`event_id` Redis SETNX TTL 24h
- 消费 → 调 Hub.SendToUser / Broadcast
- **验收标准**
- msg 发布 `NotificationRequested` → push-gateway 消费 → 推送到在线客户端
- 重复 event_id 不重投
#### 任务 4.14JWT RS256 升级 + JWKS fetcherP21 天)
- **负责人**ai02
- **依赖**:任务 4.8shared-go/jwks+ iam 就绪信号
- **交付物**[internal/ws/handler.go](../../../services/push-gateway/internal/ws/handler.go) `authenticate` 重构
- 移除 HS256 共享密钥校验
- 改用 RS256通过 `shared-go/auth/jwks` 拉取 iam `/.well-known/jwks.json` 公钥
- 缓存公钥 + 定期刷新5 分钟)
- **验收标准**
- iam 签发的 RS256 JWT 通过校验
- 公钥轮换后 5 分钟内生效
#### 任务 4.15设计决策记录章节回写P21 天)
- **负责人**ai02
- **依赖**:任务 4.13
- **交付物**02-architecture-design.md 新增 §5.4ISSUE-056
- 标题:"设计决策记录gRPC vs HTTP 协议选型coord 已采纳 P1"
- 正文标注"coord 已采纳,见 coord-final-decisions P1/P5/P6"
- 记录决策背景、方案对比、采纳理由
- **验收标准**:章节存在且标注正确
#### 任务 4.16:与 msg 联调 /internal/push 双通道P21 天)
- **负责人**ai02
- **依赖**:任务 4.13 + 任务 4.14 + msg 就绪
- **交付物**:联调测试报告
- 定向推送msg → HTTP /internal/push → push-gateway → WebSocket
- 广播msg → Kafka NotificationRequested → push-gateway → 全在线客户端
- 离线场景:`delivered: false, online: false` → msg 走 SMS/邮件
- **验收标准**[matrix.md](../matrix.md) §9.5 推送链路检查清单全通过
#### 任务 4.17:集成测试 + 端到端验证P21 天)
- **负责人**ai02
- **依赖**:任务 4.16
- **交付物**:集成测试用例 + 端到端验证报告
- 单实例 1000 连接压测
- 双实例跨实例推送验证
- Redis 故障降级验证
- Kafka 消费积压验证
- **验收标准**
- [workline.md](../workline.md) §8 push-gateway 行更新为 ✅
- [matrix.md](../matrix.md) §8 push-gateway 就绪信号 ✅
### 3.3 批次 5P6硬化5 天)
#### 任务 5.1Reconnect 协议2 天)
- **负责人**ai02
- **依赖**:批次 4 完成
- **交付物**02 文档 §7.3 落地
- 首次连接返 `{type:"hello", session_id, seq:0}`
- 每条推送带递增 `seq`
- 重连 `/ws?token=&session_id=&last_seq=` → 从 msg 拉取 `last_seq+1` 到当前补推
- **验收标准**:客户端断线 30s 内重连,未送达消息补推成功
#### 任务 5.2Redis Stream 替代 Pub/Sub2 天)
- **负责人**ai02
- **依赖**:任务 5.1
- **交付物**`internal/redis/stream.go`
- Pub/Sub → Redis Stream持久化
- Consumer Group `push-gateway`
- ACK 机制:投递成功后 XACK
- 崩溃恢复:未 ACK 消息重新投递
- **验收标准**实例崩溃时未投递消息不丢失msg 落库兜底 + Stream 持久化双保险)
#### 任务 5.3:测试覆盖率 ≥ 80%3 天,可与 5.1/5.2 并行)
- **负责人**ai02
- **依赖**:批次 4 完成
- **交付物**
- `hub_test.go`:注册/注销/推送/广播/连接数限制
- `ws_test.go`:鉴权/心跳/Origin 校验
- `config_test.go`:环境变量加载
- `redis_test.go`Pub/Sub + SET 重建
- `kafka_test.go`:消费 + 幂等
- **验收标准**`go test -cover ./...` ≥ 80%
#### 任务 5.4ADR + 非功能性需求 + 失败模式章节补全2 天,可与 5.1/5.2 并行)
- **负责人**ai02
- **依赖**:批次 4 完成 + ISSUE-007 仲裁
- **交付物**02-architecture-design.md 新增章节
- §14 ADRArchitecture Decision Records
- ADR-001gorilla/websocket 选型vs nhooyr/websocket
- ADR-002Redis Pub/Sub vs StreamP5 Pub/Sub → P6 Stream 演进)
- ADR-003心跳间隔 30s/60s 选型依据
- ADR-00410w 容量依据goroutine-per-connection 内存估算)
- ADR-005HTTP + Kafka 双通道vs 单通道)
- §15 非功能性需求(可用性 SLO 99.9% / 安全合规 / 容量 SLA
- §16 失败模式(实例崩溃 / Redis 故障 / 网络分区 / Kafka 积压降级)
- §17 容量估算10w 连接内存/CPU/带宽)
- **验收标准**4 章节齐全,符合 arc42 规范
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai02 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai02 自行补充
### 4.1 我依赖的上游就绪标志
| 依赖项 | 提供方 | 就绪信号 | 当前状态 | 影响任务 |
| ------ | ------ | -------- | -------- | -------- |
| shared-go 包骨架 | coord批次 0.14 | `packages/shared-go` 含 tracer/logger/jwks/env 4 模块 | ⏳ | 任务 4.8 |
| iam JWT RS256 + JWKS 端点 | ai06批次 1 | iam gRPC 50052 + `/.well-known/jwks.json` 可访问 | ⏳ | 任务 4.14 |
| msg gRPC + Kafka topic | ai10批次 4 | msg gRPC 50056 + `edu.notification.requested` topic 有事件 | ⏳ | 任务 4.13/4.16 |
| Redis 基础设施 | coordP1 | Redis 7.x 可访问 | ✅ P1 已就绪 | 任务 4.9/4.10 |
| Kafka 基础设施 | coordP1 | Kafka 可访问 | ✅ P1 已就绪 | 任务 4.13 |
| ISSUE-001~007 仲裁 | coord | [coord.md](../coord.md) 追加 ARB-003+ | ⏳ | 任务 4.2/4.11/4.15/5.4 |
### 4.2 我的就绪信号(供下游消费)
| 就绪标志 | 验证方式 | 供消费方 |
| -------- | -------- | -------- |
| push-gateway HTTP :8081 启用 | `GET /healthz` 返 200 | k8s 探针 / 监控 |
| /readyz 返 200含 Redis/Kafka 软失败检查) | `GET /readyz` 返 200 + `degraded` 字段 | k8s 探针 |
| WebSocket /ws 端点可升级JWT RS256 鉴权) | 客户端 `ws://host:8081/ws?token=JWT` 建立连接 | teacher-portal / student-portal / parent-portal |
| /internal/push + /internal/broadcast 接收 msg 推送 | msg 调用返 `{success:true, delivered:true}` | msg (ai10) |
| /internal/online/<userID> 查在线状态 | 返 `{online:bool, instances:[]}` | msg (ai10) |
| Kafka consumer `edu.notification.requested` 订阅成功 | Consumer Group `push-gateway` lag=0 | msg (ai10) |
| /metrics 暴露 8+ 自定义指标 | `GET /metrics``push_gateway_*` 指标 | Prometheus |
### 4.3 Mock 策略(开发期间)
**我提供的 mock**push-gateway 未就绪前,供前端 portal
- WebSocket mock前端用 mock-socket 库模拟 WS 连接,每 30s 推 1 条 mock 通知
- HTTP mock/internal/* 返 200 success
**我消费的 mock**(上游未就绪前):
- NotificationEvent mockmsg 未就绪前push-gateway 内置定时器每 30s 生成 mock 事件推所有在线客户端
- JWT 验签 mockiam 未就绪前使用本地固定 RS256 公钥(或 DevMode dev-token
- Kafka 订阅 mockmsg 未就绪前不启动 Kafka consumer用本地定时器替代
---
## §5 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
| ---- | ---- | ---- | -------- |
| ISSUE-001~007 仲裁延迟 | 中 | 阻塞批次 4 启动 | ai02 先按建议方案推进,仲裁结果出来后调整 |
| msg 就绪延迟 | 中 | 阻塞任务 4.13/4.16 | 用 mock NotificationEvent 先完成 Kafka 消费逻辑 |
| 10w 连接压测不达标 | 低 | 容量目标降级 | P6 阶段压测,若不达标则 horizontal scaling 兜底 |
| Redis Pub/Sub 消息丢失 | 中 | 跨实例推送丢失 | msg 落库兜底 + P6 升级 Redis Stream |
| shared-go 接口变更 | 低 | 任务 4.8 返工 | 紧跟 coord 0.14 任务,接口冻结后立即对接 |

View File

@@ -1,45 +1,362 @@
# student-bff 工作排期
> 负责人ai04
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/student-bff_contract.md](../contracts/student-bff_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/student-bff_contract.md](../contracts/student-bff_contract.md)、[objections/student-bff_issue.md](../objections/student-bff_issue.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
> 裁决依据:[coord-final-decisions.md](../../coord-final-decisions.md) §2 B1-B8、[president-final-rulings.md](../../president-final-rulings.md) §2.2/§3.4/§6.1
---
## §1 总览
student-bff 为学生端提供 GraphQL 聚合 API,覆盖 Dashboard、考试作答、作业提交等场景。全阶段目标P2 GraphQL schema 骨架 → P3 Dashboard+考试+作业聚合 → P4-P6 持续优化
student-bff 为学生端提供 **GraphQL 聚合 API**B1 裁决P2 起直接 GraphQL + DataLoader下游通过 **gRPC** 调用业务服务B2 裁决:首次实现即 gRPC复用 teacher-bff 产出的 **DownstreamClient 抽象**B8 裁决)
- **阶段归属**P3 核心教学阶段(批次 2
- **端口**3009HTTP GraphQL endpoint
- **路由前缀**`/student`api-gateway 代理 `/api/v1/student/*` → student-bff:3009
- **schema 存放**`packages/shared-ts/contracts/graphql/student-bff.schema.graphql`president §2.2
- **核心场景**:学生 Dashboard 聚合 + 考试作答 + 作业提交 + 成绩查看
### 1.1 关键裁决对齐
| 裁决 | 结论 | 对齐方式 |
| ---- | ---- | -------- |
| B1 API 风格 | P2 起直接 GraphQLYoga + DataLoader | P3 首次实现即 GraphQL禁止 REST |
| B2 下游通信 | 首次实现即 gRPC | @grpc/grpc-js + @bufbuild/protobuf,禁止 HTTP fetch |
| B3 权限装饰器 | BFF 豁免 @RequirePermission | 仅校验 x-user-id 存在,权限交下游 |
| B4 越权防御 | 全部 BFF 强制 | AuthorizationGuard 强制 userId 比对 |
| B5 错误码前缀 | BFF_STUDENT_ | 统一 BFF_ 前缀 |
| B6 缓存策略 | Redis 5-30s 短缓存 | CacheInterceptor + Redis |
| B7 Kafka 订阅 | P2-P4 不订阅P5 后订阅 | P3/P4 纯同步聚合P5 引入 EventSubscriber |
| B8 DownstreamClient | 回写 teacher-bff3 BFF 统一 | 复用 ai03 P2 产出的抽象 |
---
## §2 全阶段甘特图(P2-P6各 AI 自行细化
## §2 全阶段甘特图(批次 1 等待期 + 批次 2-5
```mermaid
gantt
title ai04 student-bff 全阶段排期
title ai04 student-bff 全阶段排期(对齐总裁 §6.1 批次时间线)
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a4a, 2026-07-10, Xd
section 批次1等待期
W0.1 GraphQL schema 第一版起草 :crit, w1, 2026-07-10, 3d
W0.2 01/02 文档回写(B1/B2/B5/B8) :crit, w2, after w1, 3d
W0.3 schema 提交 coord 仲裁 :milestone, w3, after w2, 0d
section 批次2 P3 核心
P3.1 NestJS+GraphQL Yoga 骨架 :crit, p1, after w3, 2d
P3.2 DownstreamClient+gRPC client(iam+core-edu) :crit, p2, after p1, 3d
P3.3 核心 Query Resolver(dashboard/homework/grades/exams) :crit, p3, after p2, 3d
P3.4 Mutation(submitHomework)+AuthorizationGuard(B4) :crit, p4, after p3, 2d
P3.5 DataLoader+N+1防御 :p5, after p4, 1d
P3.6 Redis缓存(B6)+ActionState信封 :p6, after p4, 1d
P3.7 /healthz+/readyz探针(iam+core-edu) :p7, after p6, 1d
P3.8 横切关注点(logger/metrics/tracer/error filter) :p8, after p6, 2d
P3.9 单元测试(覆盖率≥80%) :p9, after p8, 2d
section 批次3 P4 扩展
P4.1 content gRPC client(textbooks/chapters/questions) :p10, after p9, 3d
P4.2 data-ana gRPC client(weakness/trend) :p11, after p10, 2d
P4.3 Query 扩展(myTextbooks/myWeakness/myTrend) :p12, after p11, 2d
P4.4 /readyz 扩展探针(content+data-ana) :p13, after p12, 1d
P4.5 Dashboard Resolver 字段扩展 :p14, after p12, 1d
section 批次4 P5 扩展
P5.1 msg gRPC client(notifications) :p15, after p14, 2d
P5.2 ai gRPC client(chat/streamChat SSE) :p16, after p15, 3d
P5.3 Query/Mutation 扩展(myNotifications/markAsRead/aiChat) :p17, after p16, 2d
P5.4 Kafka EventSubscriber(B7 P5订阅) :p18, after p17, 2d
P5.5 push-gateway 推送通道 :p19, after p18, 2d
P5.6 /readyz 扩展探针(msg+ai) :p20, after p19, 1d
section 批次5 P6 硬化
P6.1 熔断器(opossum)完善 :p21, after p20, 2d
P6.2 HPA+全链路可观测 :p22, after p21, 2d
P6.3 灾备演练+99.9%可用性 :p23, after p22, 3d
```
> **注意**:以上为 coord 初始规划ai04 接管后必须自行细化为完整 P2-P6 排期。
> **关键路径**critschema 起草 → 文档回写 → P3 骨架 → gRPC client → Query Resolver → Mutation+Guard
> **总时间线**:批次 1 等待期 6 天 + 批次 2 P3 约 17 天 + 批次 3 P4 约 9 天 + 批次 4 P5 约 12 天 + 批次 5 P6 约 7 天
---
## §3 详细任务
### 全阶段任务
### 3.1 批次 1 等待期2026-07-10 起,约 6 天)
#### W0.1 GraphQL schema 第一版起草
- **负责人**ai04
- **交付物**:⚠️ 由 ai04 自行补充
- **依赖**:见 [contracts/student-bff_contract.md](../contracts/student-bff_contract.md)
- **验收标准**:⚠️ 由 ai04 自行补充
- **依赖**president §2.2 裁决 ai04 在批次 1 等待期起草)
- **交付物**
- `packages/shared-ts/contracts/graphql/student-bff.schema.graphql` 第一版
- 包含 Query/Mutation 清单 + 类型定义 + 权限点标注(`# @permission:`+ DataScope 标注(`# @dataScope: SELF`
- 分页采用 Relay Cursor Connections 规范president §2.2 #5
- 错误格式GraphQL errors 数组 + `extensions.code` + `extensions.traceId`president §2.2 #3
- **验收标准**
- P3 核心 QuerystudentDashboard / myHomework / myGrades / myExams / myClasses / currentUser
- P3 核心 MutationsubmitHomework
- 提交 coord 仲裁president §2.2coord 在批次 2 启动前仲裁第一版)
- **状态**:⏳ 待办(见 objections ISSUE-STU-004
#### W0.2 01/02 文档回写
- **负责人**ai04
- **依赖**:无
- **交付物**
- `services/student-bff/docs/01-understanding.md` 回写ISSUE-STU-001
- §3.2 REST 端点 → GraphQL Query/Mutation 清单
- §3.1/§4 HTTP fetch → gRPC 下游调用
- §3.3/§6 错误码 `STUDENT_BFF_``BFF_STUDENT_`
- §7.2 删除已裁决的"待仲裁"项
- `services/student-bff/docs/02-architecture-design.md` 回写ISSUE-028-ai04
- §4 21 个 REST 端点 → GraphQL Schema 设计
- §9.2 删除 REST→GraphQL 演进,改为 GraphQL 即起点
- §9.3 删除 HTTP→gRPC 演进,改为 gRPC 首次实现即用
- §8.3 删除 8 项已裁决的"待仲裁"标注ISSUE-STU-003
- 修正 §11.4/§11.5 错误引用ISSUE-STU-002
- 补充 GraphQL Schema / DataLoader / gRPC client / AuthorizationGuard 设计
- **验收标准**:与 coord-final-decisions §2 B1-B8 + president §2.2 完全一致
- **状态**:⏳ 待办
### 3.2 批次 2 P3 核心教学(约 17 天)
#### P3.1 NestJS + GraphQL Yoga 骨架
- **负责人**ai04
- **依赖**:批次 1 完成iam gRPC 50052 + ai03 DownstreamClient 抽象就绪president §6.1
- **交付物**
- `services/student-bff/` 服务骨架(克隆 teacher-bff 结构B8 复用 shared/
- `src/app.module.ts` + `src/main.ts`(端口 3009
- GraphQL Yoga endpoint`POST /graphql`+ Playground开发环境
- `package.json`@edu/student-bff+ `tsconfig.json`NodeNext ESM+ `nest-cli.json`
- `Dockerfile`多阶段构建EXPOSE 3009
- **验收标准**`POST /graphql` 返回 200 + schema 内省可用
- **状态**:⏳ 待办
#### P3.2 DownstreamClient + gRPC clientiam + core-edu
- **负责人**ai04
- **依赖**P3.1 + ai03 teacher-bff P2 产出的 DownstreamClient 抽象B8
- **交付物**
- `src/shared/downstream/downstream-client.ts`(复用 teacher-bff 抽象B8
- gRPC client 配置iam:50052 + core-edu:50053
- `src/config/env.ts`IamGrpcUrl + CoreEduGrpcUrl + 超时/重试参数
- gRPC interceptortraceId 透传 + 错误归一化
- **验收标准**:可调用 iam.GetUserInfo + core-edu.HomeworkService.ListHomeworkByClass
- **状态**:⏳ 待办
#### P3.3 核心 Query Resolver
- **负责人**ai04
- **依赖**P3.2 + coord 仲裁的 schema 第一版
- **交付物**
- `src/student/resolvers/dashboard.resolver.ts`studentDashboard聚合 iam + core-edu
- `src/student/resolvers/homework.resolver.ts`myHomeworkcore-edu
- `src/student/resolvers/grades.resolver.ts`myGradescore-eduB4 强制 userId 比对)
- `src/student/resolvers/exams.resolver.ts`myExamscore-edu
- `src/student/resolvers/classes.resolver.ts`myClassescore-edu
- `src/student/resolvers/auth.resolver.ts`currentUser聚合 iam.GetUserInfo + GetEffectivePermissions + GetViewports
- 并行编排Promise.allSettled + 部分降级president §2.6 方案 Bdata 内 degraded 字段)
- **验收标准**5 个核心 Query 可执行,返回 ActionState 信封
- **状态**:⏳ 待办
#### P3.4 MutationsubmitHomework+ AuthorizationGuardB4
- **负责人**ai04
- **依赖**P3.3
- **交付物**
- `src/student/resolvers/homework.mutation.resolver.ts`submitHomework Mutation
- `src/student/guards/authorization.guard.ts`B4 自我越权防御
- 接口:`canAccessOwnData(userId, requestedStudentId): Promise<boolean>`
- P3 实现:强制 `studentId === userId`(学生只能操作自己数据)
- 参照 teacher-bff ISSUE-033-ai03 的 AuthorizationGuard 模式president §2.9
- Zod 输入校验SubmitHomeworkInput schema
- **验收标准**submitHomework 可提交越权请求studentId ≠ userId返回 BFF_STUDENT_FORBIDDEN
- **状态**:⏳ 待办
#### P3.5 DataLoader + N+1 防御
- **负责人**ai04
- **依赖**P3.3
- **交付物**
- `src/student/dataloaders/homework.loader.ts`:批量加载作业
- `src/student/dataloaders/grades.loader.ts`:批量加载成绩
- Dashboard 内多学生场景用 DataLoader 批量去重004 §11.3
- **验收标准**N+1 查询场景下下游 gRPC 调用数 ≤ 2
- **状态**:⏳ 待办
#### P3.6 Redis 缓存B6+ ActionState 信封
- **负责人**ai04
- **依赖**P3.3
- **交付物**
- `src/shared/cache/cache.module.ts`Redis CacheInterceptor
- 缓存 Key 规范:`student:dashboard:{userId}`TTL 5-30sB6
- ActionState 信封:`{success, data, meta?}` / `{success: false, error: {code, message, details?, traceId?}}`
- 降级模式:`data.degraded = true` + `data.degradedReason`president §2.6 方案 B
- **验收标准**:缓存命中时 P50 < 100ms降级响应符合方案 B
- **状态**:⏳ 待办
#### P3.7 /healthz + /readyz 探针
- **负责人**ai04
- **依赖**P3.2
- **交付物**
- `src/shared/health/health.controller.ts`/healthzliveness+ /readyzreadiness
- P3 /readyz 探针iam gRPC 50052 + core-edu gRPC 500532 项president §2.4
- 必需依赖失败返回 503可选依赖软失败返回 200 + degraded
- **验收标准**/readyz 返回 2 项探针状态
- **状态**:⏳ 待办
#### P3.8 横切关注点
- **负责人**ai04
- **依赖**P3.1
- **交付物**
- `src/shared/observability/logger.ts`pinoservice: 'student-bff'
- `src/shared/observability/metrics.ts`prom-client11 个 student_bff_* 指标)
- `src/shared/observability/tracer.ts`OTelserviceName: 'student-bff'
- `src/shared/errors/global-error.filter.ts`@Catch()BFF_STUDENT_* 错误码)
- `src/shared/errors/application-error.ts`(错误类层次)
- 优雅关闭SIGTERM → app.close() → shutdownTracer()
- **验收标准**/metrics 可访问GlobalErrorFilter 捕获所有异常
- **状态**:⏳ 待办
#### P3.9 单元测试
- **负责人**ai04
- **依赖**P3.3-P3.8
- **交付物**
- `test/unit/resolvers/*.test.ts`Resolver 聚合逻辑mock gRPC 下游)
- `test/unit/guards/*.test.ts`AuthorizationGuard 越权防御
- `test/unit/dataloaders/*.test.ts`DataLoader 批量逻辑
- `vitest.config.ts`(对齐 classes 测试框架)
- **验收标准**:覆盖率 ≥ 80%
- **状态**:⏳ 待办
### 3.3 批次 3 P4 内容分析扩展(约 9 天)
#### P4.1-P4.2 content + data-ana gRPC client
- **负责人**ai04
- **依赖**:批次 3 启动content gRPC 50054 + data-ana gRPC 50055 就绪)
- **交付物**
- content gRPC clientTextbookService + ChapterService + QuestionService + KnowledgeGraphService
- data-ana gRPC clientAnalyticsService.GetStudentWeakness + GetLearningTrend
- **验收标准**:可调用 content + data-ana gRPC RPC
- **状态**:⏳ 待办(属"跨阶段扩展例外"president §2.3 允许新增下游 gRPC 调用)
#### P4.3-P4.5 Query 扩展 + 探针扩展 + Dashboard 字段扩展
- **交付物**
- Query 扩展myTextbooks / myChapters / myQuestions / myLearningPath / myWeakness / myTrend
- /readyz 扩展探针:+ content 50054 + data-ana 50055共 4 项)
- Dashboard Resolver 字段扩展null 字段 → 真实 data-ana 数据president §2.3 #4 允许)
- **状态**:⏳ 待办
### 3.4 批次 4 P5 沟通 AI 扩展(约 12 天)
#### P5.1-P5.3 msg + ai gRPC client + Query/Mutation 扩展
- **交付物**
- msg gRPC clientNotificationService.ListNotifications + MarkAsRead
- ai gRPC clientAiService.Chat + StreamChatSSE 流式透传)
- Query/Mutation 扩展myNotifications / markAsRead Mutation / aiChat / aiStreamChat
- **状态**:⏳ 待办
#### P5.4-P5.6 Kafka EventSubscriber + push-gateway + 探针扩展
- **交付物**
- `src/student/events/event-subscriber.ts`Kafka 消费者组B7 P5 才订阅)
- 订阅 topicedu.homework.events / edu.exam.events / edu.grade.events / edu.identity.user.role_changed
- 幂等性Redis SETNX event_id 去重
- push-gateway 推送通道POST /push/user/:userId
- /readyz 扩展探针:+ msg 50056 + ai 50058共 6 项)
- **状态**:⏳ 待办
### 3.5 批次 5 P6 硬化(约 7 天)
#### P6.1-P6.3 熔断器 + HPA + 灾备
- **交付物**
- 熔断器opossum完善每个下游 gRPC client 独立熔断器
- HPA 自动扩缩容配置
- 全链路 trace + Grafana 仪表盘
- 灾备演练 + 99.9% 可用性压测
- **状态**:⏳ 待办
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai04 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai04 自行补充
### 4.1 我依赖的上游就绪标志
| 依赖项 | 提供 AI | 就绪信号 | 阻塞阶段 | 状态 |
| ------ | ------- | -------- | -------- | ---- |
| core_edu.proto 补全AttendanceService / GetClassesByTeacher | coordpresident §2.5 | proto message + RPC 签名定义 | 批次 2 P3 | ⏳ |
| buf.gen.yaml gRPC 插件 | coord批次 0.9 | grpc/node 插件配置 | 批次 2 P3 | ⏳ |
| ai03 DownstreamClient 抽象 | ai03B8 裁决) | teacher-bff P2 产出可复用抽象 | 批次 2 P3 | ⏳ |
| iam gRPC 50052 + 12 RPC | ai06I1 裁决) | HealthService.Check = SERVING | 批次 2 P3 | ⏳ |
| core-edu gRPC 50053 + 22 RPC | ai08C2 裁决) | HealthService.Check = SERVING | 批次 2 P3 | ⏳ |
| content gRPC 50054 + 18 RPC | ai09 | HealthService.Check = SERVING | 批次 3 P4 | ⏳ |
| data-ana gRPC 50055 + 12 RPC | ai11 | HealthService.Check = SERVING | 批次 3 P4 | ⏳ |
| msg gRPC 50056 + 13 RPC | ai10 | HealthService.Check = SERVING | 批次 4 P5 | ⏳ |
| ai gRPC 50058 + 6 RPC | ai12 | HealthService.Check = SERVING | 批次 4 P5 | ⏳ |
| coord 仲裁 student-bff schema 第一版 | coordpresident §2.2 | schema 第一版裁定 | 批次 2 P3 | ⏳ |
| api-gateway `/student` 路由 | ai01 | /api/v1/student/* 可代理 | 批次 2 P3 | ⏳ |
### 4.2 我的就绪标志(供下游消费)
| 就绪标志 | 验证方式 | 消费方 |
| -------- | -------- | ------ |
| student-bff GraphQL :3009 启用 | GET /healthz 返回 200 | k8s / 监控 |
| /readyz 返回 200含下游 gRPC 连通性) | GET /readyz 返回 200 + checks | k8s / 监控 |
| GraphQL schema 可内省 | POST /graphql 返回 schema | ai14student-portal |
| 核心 Query 可执行 | studentDashboard / myHomework / myGrades / myClasses / currentUser | ai14 |
| 核心 Mutation 可执行 | submitHomework | ai14 |
| /metrics 可访问 | GET /metrics 返回 prometheus 格式 | Prometheus |
---
## §5 Mock 策略(全并行开发期间)
### 5.1 我提供的 mock供 ai14 student-portal
在 student-bff 真实就绪前,为 ai14 提供 GraphQL mock
- **方式**MSW 拦截 POST /graphql + 固定 response
- **mock 数据**
- currentUser 返回固定学生id="student-001", name="李同学", roles=["student"]
- studentDashboard 返回固定仪表盘pendingHomework=3, upcomingExams=2, unreadNotifications=5
- myHomework 返回固定 3 个作业1 个待提交)
- myGrades 返回固定 5 个成绩
- myExams 返回固定 2 个考试
- myClasses 返回固定 1 个班级
### 5.2 我消费的 mock上游未就绪前
| 上游 | mock 方式 | 切换真实时机 |
| ---- | --------- | ------------ |
| iam gRPC | grpc-mock 拦截 + 固定 UserInfo/Permissions/Viewports | iam 就绪信号 ✅ |
| core-edu gRPC | grpc-mock 拦截 + 固定 Homework/Exam/Grade/Class | core-edu 就绪信号 ✅ |
| content gRPC | grpc-mock 拦截 + 固定 Textbook/Chapter/Question | content 就绪信号 ✅ |
| data-ana gRPC | grpc-mock 拦截 + 固定 Weakness/Trend | data-ana 就绪信号 ✅ |
| msg gRPC | grpc-mock 拦截 + 固定 Notification | msg 就绪信号 ✅ |
| ai gRPC | grpc-mock 拦截 + 固定 Chat response | ai 就绪信号 ✅ |
> 所有上游 mock 通过 gRPC client 拦截器实现,上游就绪后移除拦截器切换真实调用(对齐 matrix.md §7 全并行 Mock 策略)。
---
## §6 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
| ---- | ---- | ---- | -------- |
| schema 仲裁延迟阻塞 P3 启动 | 中 | 高 | ai04 批次 1 等待期优先产出 schema 草案W0.1 |
| ai03 DownstreamClient 抽象未就绪 | 中 | 高 | ISSUE-007 已识别coord 验收批次 1 时检查 |
| core_edu.proto 补全延迟 | 低 | 高 | president §2.5 已明确 coord 负责 proto 定义 |
| GraphQL + gRPC 首次实现复杂度高 | 中 | 中 | 复用 teacher-bff P2 模式B8 DownstreamClient + Yoga endpoint |
| 下游 gRPC mock 与真实行为不一致 | 中 | 低 | 集成测试阶段统一验证matrix.md §9 |

View File

@@ -1,45 +1,271 @@
# student-portal 工作排期
> 负责人ai14
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/student-portal_contract.md](../contracts/student-portal_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 关联:[workline.md](../workline.md)、[coord.md §1 ARB-001](../coord.md)、[coord.md §2 ARB-002](../coord.md)、[contracts/student-portal_contract.md](../contracts/student-portal_contract.md)、[matrix.md](../matrix.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
> 依据president-final-rulings.md §3.6ai14 P3 功能范围)+ §7.14ai14 工作内容最终清单)+ ARB-002 §2.3P3 首个 Remote+ 02-architecture-design.md v2 阶段能力累积矩阵
---
## §1 总览
student-portal 是学生端微前端,通过 MF Remote 接入主应用覆盖考试作答、作业提交等场景。全阶段目标P2 MF Remote 骨架 → P3 考试作答+作业提交 → P4-P6 持续优化
student-portal 是学生端微前端MF Remote),通过 Module Federation 接入 teacher-portal Shell,覆盖考试作答、作业提交、学情查看等场景。全阶段目标P2 MF Remote 骨架预埋 → P3 考试作答 + 作业提交 + 基础页面 → P4 知识图谱 + 学情诊断 → P5 实时通知 + AI 辅助 → P6 可观测性硬化 + A11y + 性能
**全并行模式**ai14 一口气完成 P2-P6 全部代码,开发期间用 MSW mock 上游student-bff GraphQL / api-gateway HTTP / push-gateway WebSocket上游就绪后在 [matrix.md](../matrix.md) §8 更新就绪信号,最后统一集成测试。
**批次归属**(见 [workline.md §1](../workline.md)
- 批次 2P3ai14 与 ai07 + ai08 + ai04 + ai03扩展 并行启动(`b2d, after b1d, 8d`
- 批次 3P4与 ai09 + ai11 + ai05 + ai15 并行
- 批次 4P5与 ai10 + ai02 + ai12 + ai03扩展 并行
- 批次 5P6与 ai16 + 持续优化 并行
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
title ai14 student-portal 全阶段排期
title ai14 student-portal 全阶段排期(全并行)
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a14a, 2026-07-10, Xd
```
section P2 预埋
项目骨架+设计令牌三层+独立壳路由 :crit, a14p0, 2026-07-10, 2d
MF Remote配置(NextFederationPlugin remotes) :crit, a14a, after a14p0, 2d
MSW基础设施+GraphQL请求层骨架 :a14b, after a14a, 2d
> **注意**:以上为 coord 初始规划ai14 接管后必须自行细化为完整 P2-P6 排期。
section P3 学生核心
AppShell复用+学生端导航+路由守卫 :crit, a14c, after a14b, 2d
Dashboard页(studentDashboard query) :a14d, after a14c, 2d
我的班级页+我的考试列表+我的作业列表 :a14e, after a14d, 3d
考试作答页(状态机+服务器时间同步) :crit, a14f, after a14e, 4d
IDB断网恢复队列+自动保存+DraftRecovery :crit, a14g, after a14f, 3d
防作弊采集+BroadcastChannel多标签检测 :a14h, after a14g, 2d
作业提交页(submitHomework mutation+乐观更新) :a14i, after a14h, 2d
我的成绩页+我的考勤页 :a14j, after a14i, 2d
section P4 知识与学情
学习路径页(learningPath query+知识点卡片) :a14k, after a14j, 3d
学情诊断页(myWeakness+myTrend query+图表) :a14l, after a14k, 3d
教材章节浏览页(textbooks+chapters) :a14m, after a14l, 2d
section P5 推送与AI
WebSocket通知中心(myNotifications+markAsRead) :a14n, after a14m, 2d
跨Tab通知同步(BroadcastChannel) :a14o, after a14n, 1d
AI辅助答疑(SSE流式,可选) :a14p, after a14o, 3d
section P6 硬化
Sentry+RUM+OTel browser :a14q, after a14p, 2d
A11y审计(WCAG 2.2 AA) :a14r, after a14q, 2d
性能优化+bundle门禁(Remote <80KB) :a14s, after a14r, 2d
P3未尽事项补全+降级策略验证 :a14t, after a14s, 2d
```
---
## §3 详细任务
### 全阶段任务
### P2MF Remote 骨架预埋
- **负责人**ai14
- **交付物**:⚠️ 由 ai14 自行补充
- **依赖**:见 [contracts/student-portal_contract.md](../contracts/student-portal_contract.md)
- **验收标准**:⚠️ 由 ai14 自行补充
- **裁决依据**ARB-002 §2.3P3 首个 Remote但 P2 可预埋骨架)+ 总裁裁决 §3.6ai14 P3 起步)+ §2.17MF GraphQL client 单例方案 A
- **交付物**
- 项目骨架(`apps/student-portal/` 目录结构:`src/app/``src/components/``src/lib/``src/hooks/``src/mocks/`
- 设计令牌三层primitive.css / semantic.css / tailwind-theme.ts与 teacher-portal Shell 一致)
- 独立壳路由next.config.js + app/layout.tsx + app/page.tsxMF 关闭时可独立渲染)
- NextFederationPlugin 配置(`remotes: { teacher: 'teacher@http://localhost:4000/_next/static/chunks/remoteEntry.js' }``exposes: { './StudentApp': './src/app/student-app.tsx' }`
- MF shared singleton 配置react/react-dom/urql/graphql/@tanstack/react-query/zustand/nuqs/@edu/* 全部 singleton
- MSW 基础设施(`src/mocks/handlers.ts` + `src/mocks/fixtures/*.json` + `NEXT_PUBLIC_API_MOCKING=enabled`
- GraphQL 请求层骨架(`src/lib/graphql.ts` 复用 Shell `useGraphQLClient()`,不重复创建 client
- **依赖**
- packages 骨架ui-tokens/ui-components/hooksai13 批次 0.15 已完成)
- teacher-portal MF Shell 配置ai13 P2exposes 就绪)
- **Mock 策略**MSW 拦截 `POST /api/v1/student/graphql` + `POST /api/auth/login`
- **验收标准**
- `pnpm dev` 启动 :4001 可访问
- MF 配置不破坏独立壳渲染(`NEXT_PUBLIC_MF_ENABLED=false` 时独立渲染首页)
- MSW 拦截 GraphQL 请求返回 mock 数据
- lint + typecheck 零错误
### P3考试作答 + 作业提交 + 基础页面(核心)
- **负责人**ai14
- **裁决依据**:总裁裁决 §3.6ai14 P3 功能范围)+ ARB-001 §1.3ActionState 信封 + 降级模式方案 B+ ARB-002 §2.2(复用 Shell 暴露清单)+ 02-architecture-design.md v2 §14考试作答架构设计
- **交付物**
- **AppShell 复用 + 学生端导航**:从 Shell 导入 `AppShell`覆写学生端视口myClasses/myExams/myHomework/myGrades/myAttendance/learningPath/dashboard/notifications
- **路由守卫**:未登录跳转 `http://localhost:4000/login?redirect=student`,登录后回跳
- **Dashboard 页**`/dashboard`):消费 `studentDashboard` queryupcomingHomework + upcomingExams + recentGrades + attendanceRate + learningStreakDays
- **我的班级页**`/my-classes`):消费 `myClasses` query
- **我的考试列表页**`/my-exams`):消费 `myExams` query按状态分组未开始/进行中/已提交/已批改)
- **我的作业列表页**`/my-homework`):消费 `myHomework` query按状态分组
- **考试作答页**`/my-exams/[id]/take`
- 状态机NotStarted → InProgress → AutoSaving → Submitting → Submitted见 02 §14 状态机图)
- 服务器时间同步(`useServerTimeSync` hook5 分钟重新同步,倒计时基于服务器时间)
- IDB 断网恢复队列(`idb-keyval` 存草稿 + 队列,网络恢复后重试)
- 自动保存(每 30s + blur 事件触发,乐观更新本地状态)
- DraftRecovery 草稿恢复(进入作答页时检查 IDB 草稿,提示恢复)
- 防作弊采集visibilitychange/copy/paste/fullscreen/contextmenu 事件监听 + 记录)
- BroadcastChannel 多标签检测(`edu-exam-session` channel检测到多标签警告
- 提交防重复idempotency key + 提交按钮 disabled + 提交中状态)
- **作业提交页**`/my-homework/[id]/submit`
- 消费 `submitHomework` mutation
- 乐观更新useMutation onMutate 回滚 + invalidateQueries
- 附件上传(待 ISSUE-014-05 仲裁后实现,暂走 mock
- **我的成绩页**`/my-grades`):消费 `myGrades` query成绩列表 + 趋势图
- **我的考勤页**`/my-attendance`):消费 `myAttendance` query考勤日历
- **依赖**
- student-bff GraphQL schemaai04 P3`packages/shared-ts/contracts/graphql/student-bff.graphql`,待 ISSUE-014-02 仲裁)
- api-gateway 路由ai01 P3`/api/v1/student/*` 反向代理 student-bff待 ISSUE-014-01 仲裁)
- core-edu gRPCai08 P3提供 ExamService/HomeworkService/GradeService/AttendanceService/ClassService
- iam gRPCai06 P2GetUserInfo + GetEffectivePermissions + GetViewports
- data-ana gRPCai11 P4但 studentDashboard 聚合需要P3 用 mock
- **Mock 策略**
- MSW 拦截 `POST /api/v1/student/graphql`,按 operationName 返回 mock 响应
- mock-socket 模拟 WebSocket 推送(考试延长/强制提交事件)
- IDB 草稿恢复用真实 idb-keyval前端可独立测试
- **验收标准**
- Dashboard 页渲染mock 数据upcomingHomework + upcomingExams + recentGrades 三栏
- 考试作答页状态机完整:进入 → 作答 → 自动保存 → 提交 → 跳转结果页
- 断网恢复:手动 offline → 作答 → 恢复网络 → 草稿自动提交
- 防作弊采集visibilitychange hidden 触发记录mock 上报)
- 多标签检测:开第二个 Tab 作答,第一个 Tab 收到警告
- 作业提交乐观更新:提交后立即 UI 反馈,失败回滚
- lint + typecheck 零错误
### P4知识图谱 + 学情诊断
- **负责人**ai14
- **交付物**
- **学习路径页**`/learning-path`):消费 `learningPath` query知识点卡片列表 + 掌握度进度条
- **学情诊断页**`/dashboard/weakness`):消费 `myWeakness` query薄弱点雷达图recharts
- **学习趋势页**`/dashboard/trend`):消费 `myTrend` query趋势折线图recharts
- **教材章节浏览页**`/textbooks``/textbooks/[id]/chapters`):消费 `textbooks` + `chapters` query
- **依赖**
- student-bff 扩展 content + data-ana gRPC 调用ai04 P4
- content gRPCai09 P4TextbookService + ChapterService + KnowledgeGraphService
- data-ana gRPCai11 P4AnalyticsService.GetStudentWeakness + GetLearningTrend
- **Mock 策略**MSW 返回固定知识点 + 薄弱点 + 趋势数据
- **验收标准**
- 学习路径页渲染知识点卡片 + 掌握度mock
- 学情诊断页雷达图 + 趋势折线图渲染mock
- 教材章节树形导航可用
### P5实时通知 + AI 辅助
- **负责人**ai14
- **交付物**
- **WebSocket 通知中心**`/notifications`
- 消费 `myNotifications` query + `markAsRead` mutation
- WebSocket 连接 `ws://push-gateway:8081/ws`,实时接收通知
- 通知分类(作业/考试/成绩/系统),未读计数
- **跨 Tab 通知同步**BroadcastChannel `edu-notification` channel新通知在所有 Tab 同步)
- **AI 辅助答疑**(可选,`/ai-tutor`
- SSE 流式接收 AI 回答
- 消费 ai 服务(待 ai12 P5 就绪)
- **依赖**
- push-gateway WebSocketai02 P5`/ws` 端点)
- msg gRPCai10 P5NotificationService
- ai 服务 gRPCai12 P5AiService.Chat可选
- **Mock 策略**mock-socket 模拟 WS 推送(每 30 秒 1 条通知)+ MSW 返回固定 AI 响应SSE 用 ReadableStream mock
- **验收标准**
- 通知中心实时接收 WS 推送mock
- 多 Tab 同步Tab A 收到通知Tab B 未读计数同步更新
- AI 辅助答疑流式输出mock
### P6可观测性硬化 + A11y + 性能
- **负责人**ai14
- **交付物**
- **Sentry 错误追踪**`NEXT_PUBLIC_SENTRY_DSN` + beforeSend PII 过滤,学生隐私合规)
- **Web Vitals RUM**LCP/INP/CLS/TTFB → `/api/v1/admin/web-vitals`
- **OTel browser SDK**(自动埋点 fetch/XHR/document load → OTLP collector
- **A11y 审计**WCAG 2.2 AAeslint-plugin-jsx-a11y + @axe-core/playwright + 对比度审计)
- **性能优化**bundle analyzer + size-limit CI 门禁Remote < 80KB / CSS < 50KB
- **P3 未尽事项补全**(根据 ISSUE-014-03/04/05/06/07 仲裁结果补全防作弊策略、附件上传、实时事件响应、DataScope 强制执行)
- **降级策略验证**02 §18.2 降级策略矩阵的 12 个场景端到端验证)
- **依赖**
- push-gateway WebSocket 真实就绪ai02 P5
- Sentry DSN + OTel collector基础设施
- 全部上游就绪(统一集成测试)
- **验收标准**
- 99.9% 可用 + WCAG 2.2 AA + LCP < 2.5s / INP < 200ms / CLS < 0.1P75
- Remote bundle < 80KB / CSS < 50KB
- 12 个降级场景全部验证通过
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai14 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai14 自行补充
### §4.1 我依赖的上游就绪标志
| 上游 | 就绪标志 | 阻塞阶段 | 状态 |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------- | ----------- |
| packages 骨架ai13 批次 0.15 | ui-tokens/ui-components/hooks 可 import | P2 启动 | ✅ 已就绪 |
| teacher-portal MF Shellai13 P2 | exposes AppShell/GraphQLProvider/useGraphQLClient/useAuth/usePermission + shared singleton | P2 启动 | ⏳ 待 ai13 P2 |
| api-gateway HTTP :8080ai01 P3 | `/api/v1/student/*` 反向代理 student-bff 可用 | P3 启动 | ⏳ 待 ai01 P3 |
| student-bff GraphQLai04 P3 | `POST /graphql` :3009 + 核心 Query/Mutation 可执行 | P3 启动 | ⏳ 待 ai04 P3 |
| student-bff GraphQL schemaai04 + ISSUE-014-02 仲裁) | `packages/shared-ts/contracts/graphql/student-bff.graphql` 创建 | P3 启动 | ⏳ 待仲裁 |
| core-edu gRPC 50053ai08 P3 | ExamService/HomeworkService/GradeService/AttendanceService/ClassService 全部 RPC | P3 启动 | ⏳ 待 ai08 P3 |
| iam gRPC 50052ai06 P2 | GetUserInfo + GetEffectivePermissions + GetViewports | P3 启动 | ⏳ 待 ai06 P2 |
| student-bff content/data-ana 扩展ai04 P4 | learningPath/myWeakness/myTrend/textbooks/chapters query 可用 | P4 启动 | ⏳ 待 ai04 P4 |
| content gRPC 50054ai09 P4 | TextbookService + ChapterService + KnowledgeGraphService | P4 启动 | ⏳ 待 ai09 P4 |
| data-ana gRPC 50055ai11 P4 | AnalyticsService.GetStudentWeakness + GetLearningTrend | P4 启动 | ⏳ 待 ai11 P4 |
| push-gateway WebSocket :8081/wsai02 P5 | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |
| msg gRPC 50056ai10 P5 | NotificationService.ListNotifications + MarkAsRead | P5 启动 | ⏳ 待 ai10 P5 |
| ai 服务 gRPC 50057ai12 P5可选 | AiService.ChatSSE 流式) | P5 启动 | ⏳ 待 ai12 P5 |
| Sentry DSN + OTel collector基础设施 | Sentry 项目创建 + OTel collector 可接收 OTLP | P6 启动 | ⏳ 待基础设施 |
### §4.2 我的就绪信号(供下游消费)
| 信号 | 就绪标志 | 消费方 |
| ------------------------------- | --------------------------------------------------------- | ------ |
| student-portal dev server :4001 | `pnpm dev` 启动 + 首页可访问 | 无(最前端,但 teacher-portal Shell 需加载 Remote |
| MF Remote 可加载 | teacher-portal Shell 可加载 `student-portal/StudentApp` | teacher-portalai13 P3 集成测试) |
| 登录流程可用 | 未登录跳转 Shell `/login`,登录后回跳 student | 无 |
| GraphQL 查询可执行 | currentUser/studentDashboard/myClasses 返回数据 | 无 |
| 考试作答链路通 | 进入作答 → 自动保存 → 提交 → 跳转结果页 | 无 |
| WebSocket 通知可接收 | 通知中心实时更新mock | 无 |
---
## §5 全并行开发说明
按 [matrix.md](../matrix.md) §全并行模式:
1. ai14 一口气完成 P2-P6 全部代码,开发期间用 MSW mock 上游
2. 上游就绪后在 matrix.md §8 更新就绪信号(`student-portal | ai14 | :4001 可访问 + MF Remote | ⏳ → ✅`
3. 所有模块就绪后统一集成测试matrix.md §9 检查清单)
4. Mock 切换:`NEXT_PUBLIC_API_MOCKING=enabled``disabled`
5. MF 切换:`NEXT_PUBLIC_MF_ENABLED=false`P2 独立壳)→ `true`P3 接入 Shell
---
## §6 阶段能力累积矩阵(与 02-architecture-design.md v2 §20 对齐)
| 阶段 | 能力 | 关键页面/功能 |
| ---- | --------------------------------------------------- | -------------------------------------------------------------------------- |
| P2 | 项目骨架 + MF Remote 配置 + MSW 基础设施 | 独立壳首页(占位) |
| P3 | + 考试作答 + 作业提交 + 基础页面 | Dashboard / myClasses / myExams / myHomework / myGrades / myAttendance |
| P4 | + 知识图谱 + 学情诊断 + 教材章节 | learningPath / myWeakness / myTrend / textbooks / chapters |
| P5 | + 实时通知 + 跨 Tab 同步 + AI 辅助(可选) | notifications / ai-tutor |
| P6 | + 可观测性 + A11y + 性能 + 降级验证 + 尽事项补全 | Sentry / RUM / OTel / WCAG 2.2 AA / bundle 门禁 |
---
## §7 关键风险与缓解
| 风险 | 影响 | 缓解措施 |
| ---------------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------- |
| student-bff GraphQL schema 未就绪ai04 P3 延迟) | 高 | MSW mock 全量 query/mutationschema 就绪后切换ai14 自行维护 mock schema 用于 codegen |
| 考试作答断网恢复逻辑复杂IDB 队列 + 服务器时间对齐)| 高 | 02 §14 已设计完整状态机 + 时间同步算法P3 优先实现核心链路P6 验证降级场景 |
| MF Remote 加载失败Shell 未就绪或版本不兼容) | 中 | 02 §3.2 已设计独立壳回退(`NEXT_PUBLIC_MF_ENABLED=false` 时独立渲染) |
| 防作弊策略未仲裁ISSUE-014-03/04 | 中 | P3 先实现采集 + 本地记录P6 根据仲裁结果补全上报逻辑 |
| 附件上传协议未仲裁ISSUE-014-05 | 中 | P3 先实现文本作业提交,附件上传 P6 根据仲裁结果补全 |
| 实时事件命名未确认ISSUE-014-06 | 低 | P3 不依赖实时事件考试作答页基于本地倒计时P5 根据仲裁结果接入 WebSocket 实时事件 |
---
**AI Agent**: ai14student-portal
**Branch**: feat-review-student-portal-docs-9yN6Av
**Coordinator**: coord-ai

View File

@@ -1,18 +1,27 @@
# teacher-bff 工作排期
> 负责人ai03
> 关联:[workline.md](../workline.md)、[coord.md §1 ARB-001](../coord.md)、[contracts/teacher-bff_contract.md](../contracts/teacher-bff_contract.md)
> 关联:[workline.md](../workline.md)、[coord.md §1 ARB-001](../coord.md)、[contracts/teacher-bff_contract.md](../contracts/teacher-bff_contract.md)、[president-final-rulings.md §3.1/§7.3](../../president-final-rulings.md)、[coord-final-decisions.md §2 B1-B8](../../coord-final-decisions.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 裁决依据B1 P2 即 GraphQL / B2 首次实现即 gRPC / B3 豁免 @RequirePermission / B4 越权防御 / B5 BFF_TEACHER_ 前缀 / B6 Redis 短缓存 / B7 P2-P4 不订阅 Kafka / B8 DownstreamClient 抽象
---
## §1 总览
teacher-bff 是教学场景域聚合层,全阶段目标:P2 GraphQL schema 第一版 → P3 扩展 exams/homework/grades → P4 学情分析 → P5 通知+SSE → P6 admin 命名空间。
teacher-bff 是教学场景域聚合层BFF,全阶段目标:
| 阶段 | 核心交付 | 裁决依据 |
| ---- | -------- | -------- |
| P2 | GraphQL schema 第一版5 Query + admin 预留)+ DownstreamClient 抽象 + iam gRPC + AuthorizationGuard + ActionState 信封 | B1/B2/B3/B4/B5/B8 + ARB-001 + §3.1 |
| P3 | core-edu gRPC 扩展exams/homework/grades Query + Mutation+ AuthorizationGuard 接入 core-edu + Redis 聚合缓存 | §2.3 跨阶段扩展例外 |
| P4 | content + data-ana gRPC 扩展(学情分析 Query+ DataLoader 全量接入 | §2.3 + §2.8 |
| P5 | ai + msg gRPC 扩展SSE 流式 + notifications Query+ Kafka consumerpush-gateway 落地后) | B7 |
| P6 | admin 命名空间实现 + 硬化(熔断/重试/超时/HPA/mTLS | §5.1 + P6 硬化 |
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
@@ -20,39 +29,307 @@ gantt
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2 GraphQL 基础
schema第一版(ARB-001) :crit, a3a, 2026-07-10, 2d
section P2 GraphQL 基础8d
schema第一版+admin预留(ARB-001) :crit, a3a, 2026-07-10, 2d
Yoga endpoint+5 Query Resolver :crit, a3b, after a3a, 3d
DataLoader+N+1防御 :a3c, after a3b, 1d
ActionState信封+降级模式B :a3d, after a3b, 1d
DownstreamClient+iam gRPC :crit, a3c, after a3a, 2d
AuthorizationGuard越权防御(B4) :crit, a3d, after a3b, 1d
ActionState信封+降级模式B :a3e, after a3b, 1d
/readyz探针注册表+iam探针 :a3f, after a3d, 1d
错误码迁移BFF_TEACHER_+回写02 :a3g, after a3e, 1d
section P3-P6 扩展
exams/homework/grades Query :a3e, after a3d, 3d
学情分析+通知+SSE :a3f, after a3e, 4d
admin命名空间 :a3g, after a3f, 2d
section P3 core-edu 扩展5d
CoreEduClient gRPC(exams/homework/grades) :crit, a3h, after a3g, 2d
Mutation(createExam/assignHomework/recordGrade) :a3i, after a3h, 1d
AuthorizationGuard接入core-edu+Redis缓存 :a3j, after a3h, 1d
/readyz+core-edu探针 :a3k, after a3j, 1d
section P4 content+data-ana 扩展4d
ContentClient+DataAnaClient gRPC :a3l, after a3k, 2d
学情分析Query+DataLoader全量 :a3m, after a3l, 1d
/readyz+content+data-ana探针 :a3n, after a3m, 1d
section P5 ai+msg 扩展8d
AiClient+MsgClient gRPC :a3o, after a3n, 2d
SSE流式透传+notifications Query :a3p, after a3o, 2d
Kafka consumer(push-gateway落地后) :a3q, after a3p, 2d
/readyz+ai+msg探针 :a3r, after a3q, 1d
Should Have补全(OTel/metrics/DataLoader/Redis) :a3s, after a3r, 1d
section P6 admin+硬化3d
admin命名空间实现 :a3t, after a3s, 1d
熔断/重试/超时 :a3u, after a3t, 1d
Nice to Have补全(Zod全量/优雅关闭) :a3v, after a3u, 1d
```
> **注意**:以上为 coord 初始规划ai03 接管后必须自行细化为完整 P2-P6 排期
> **总工期**28dP2 8d + P3 5d + P4 4d + P5 8d + P6 3d与 workline.md §1 总时间线对齐批次1 8d + 批次2 5d + 批次4 8d + P4/P6 合并 7d
---
## §3 详细任务
### P2GraphQL schema + 5 Query + DataLoader + ActionState
### P2GraphQL 基础 + DownstreamClient + 越权防御(批次 18d
> 裁决依据president §3.1 Must Have 13 项teacher-bff 无 DBG10/G11 不适用,实际 11 项 + DownstreamClient + admin 预留)
#### 3.1 GraphQL schema 第一版 + admin 预留2dP0 阻塞 ai13
- **负责人**ai03
- **依赖**coord 仲裁 ARB-001已裁决
- **交付物**
- `packages/shared-ts/contracts/graphql/teacher-bff.graphql` — P2 schema
- Yoga endpoint `POST /graphql`
- 5 Querydashboard / viewports / me / classes / class
- DataLoader + ActionState 信封 + 降级模式 B
- **依赖**iam gRPCai06+ coord 仲裁 ARB-001
- **验收标准**5 Query 可用 + ActionState 信封 + depth ≤ 7
- **完整 P3-P6 任务**:⚠️ 由 ai03 自行补充
- `packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql` — P2 schema5 Query: dashboard/viewports/me/classes/class + admin 命名空间占位)
- admin 命名空间预留schema 中声明 `admin` Query/Mutation 类型骨架(无实际 ResolverP6 实现
- **验收标准**5 Query SDL 定义完整 + admin 占位类型声明 + depth ≤ 7 + cost ≤ 1000
- **裁决引用**ARB-001 §1.2/§1.3 + president §5.1/§2.17
#### 3.2 Yoga endpoint + 5 Query Resolver3dP0 阻塞 ai13
- **负责人**ai03
- **依赖**3.1 schema + iam gRPC 50052ai06 P2.1
- **交付物**
- `POST /graphql` Yoga GraphQL endpoint
- 5 Query Resolverdashboard / viewports / me / classes / class
- Dashboard Resolver P2 实现方式ISSUE-032P2 仅调 iam gRPC未启用字段返回 null + `extensions.warning = "field_unavailable_in_p2"`
- **验收标准**5 Query 可执行 + ActionState 信封 + 降级模式 Bsuccess=true + error=null + data 内 degraded 字段)
- **裁决引用**B1 + ARB-001 §1.4 + president §2.6/§2.8
#### 3.3 DownstreamClient 抽象 + iam gRPC2dP0 阻塞 ai04/ai05
- **负责人**ai03
- **依赖**iam gRPC 50052ai06 P2.1
- **交付物**
- `src/clients/` DownstreamClient 抽象层B8BFF 模式 v2 标准抽象3 个 BFF 统一使用,回写 teacher-bff
- IamClient gRPC 实现:`@grpc/grpc-js` + `@bufbuild/protobuf`,调 iam:50052
- gRPC interceptor注入 trace contexttraceparent+ x-user-id metadata + metrics
- **验收标准**IamClient gRPC 调 iam GetUserInfo/GetViewports/GetEffectiveAccess 成功
- **裁决引用**B2首次实现即 gRPC+ B8DownstreamClient 抽象)
#### 3.4 AuthorizationGuard 越权防御1dP0
- **负责人**ai03
- **依赖**3.2 Resolver
- **交付物**
- `src/middleware/authorization.guard.ts` — AuthorizationGuard 接口(`canAccessClass(userId, classId): Promise<boolean>`
- P2 内部实现DEV_MODE 放行 + 生产拒绝(保守策略)
- 错误码:`BFF_TEACHER_FORBIDDEN_RESOURCE`teacherId 与资源无归属)+ `BFF_TEACHER_IDENTITY_MISMATCH`JWT teacherId 与 body 不一致)
- **验收标准**Guard 接口定义 + DEV_MODE 放行 + 生产拒绝 + 2 个越权错误码
- **裁决引用**B4 + president §2.7(错误码语义)+ §2.9(越权防御 P2 实现)
#### 3.5 ActionState 信封 + 降级模式 B1d
- **负责人**ai03
- **依赖**3.2 Resolver
- **交付物**GlobalErrorFilter + ActionState 信封success/errors/data+ 降级模式 B
- **验收标准**GraphQL errors 数组扩展 ActionState 字段,`extensions.code = BFF_TEACHER_*`
- **裁决引用**G8 + president §2.6
#### 3.6 /readyz 探针注册表 + iam 探针1d
- **负责人**ai03
- **依赖**3.3 IamClient
- **交付物**
- `src/shared/health/readiness.probe.ts` — DownstreamHealthCheck 注册表模式
- P2 探针Redis + iam gRPC 50052teacher-bff 无 DB2 项)
- **验收标准**/readyz 返回 2 项检查结果 + 必需依赖失败返回 503
- **裁决引用**G2 + president §2.4(探针按阶段扩展)
#### 3.7 错误码迁移 + 回写 02 文档1d
- **负责人**ai03
- **依赖**3.2-3.6
- **交付物**
- `application-error.ts` 全量迁移 `TEACHER_BFF_*``BFF_TEACHER_*`
- 回写 02 文档4 处 `GetTeacherDashboardStats``GetTeacherDashboard`ISSUE-035+ B1/B2/B4/B8 裁决对齐
- **验收标准**:源码零 `TEACHER_BFF_*` + 02 文档与裁决一致
- **裁决引用**B5 + G14 + president §3.4 回写义务
#### P2 横切项(贯穿 3.1-3.7
| 项 | 状态 | 说明 |
| -- | ---- | ---- |
| pino 结构化日志G4 | ✅ 已具备 | logger.ts |
| /healthz livenessG3 | ✅ 已具备 | health.controller.ts |
| GlobalErrorFilterG8 | ✅ 已具备 | global-error.filter.ts |
| ESM import .js 后缀G12 | ✅ 已具备 | 源码已用 .js |
| import typeG13 | ✅ 已具备 | 源码已用 import type |
| OTel tracerG6 | ⚠️ Should Have | P2 可降级为 logger-onlyP5 补全 |
| /metrics 业务指标G5 | ⚠️ Should Have | P2 仅暴露 process metrics |
| DataLoaderB1 | ⚠️ Should Have | P2 可先用普通 resolverP4 全量接入 |
| Redis 5-30s 短缓存B6 | ⚠️ Should Have | P3 接入聚合缓存 |
| Zod 全量验证G7 | ⚠️ Nice to Have | P2 先校验核心 QueryP6 全量 |
| 优雅关闭 SIGTERMG9 | ⚠️ Nice to Have | P2 已有基础P6 补全关闭顺序 |
**P2 退出标准**POST /graphql 可用 + 5 Query Resolver + DownstreamClient + AuthorizationGuard + ActionState 信封 + /readyz 2 项探针 + 错误码 BFF_TEACHER_* + admin 命名空间预留。
---
### P3core-edu gRPC 扩展(批次 25d
> 裁决依据§2.3 跨阶段扩展例外(新增下游 gRPC 调用 + AuthorizationGuard 内部实现替换 + /readyz 探针扩展)
#### 3.8 CoreEduClient gRPC2dP0
- **负责人**ai03
- **依赖**core-edu gRPC 50053ai08 P3
- **交付物**
- CoreEduClient gRPC 实现ExamService / HomeworkService / GradeService
- GraphQL Query 扩展exams(classId) / homework(classId) / grades(examId)
- Dashboard Resolver 扩展null 字段替换为 core-edu 真实数据
- **验收标准**3 个 Query 返回 core-edu 数据 + Dashboard null 字段消除
- **裁决引用**§2.3 跨阶段扩展例外 + §2.8 Dashboard Resolver 扩展
#### 3.9 Mutation 透传1d
- **交付物**createExam / assignHomework / recordGrade Mutation透传 core-edu gRPC
- **验收标准**3 个 Mutation 可执行 + 返回 ActionState 信封
#### 3.10 AuthorizationGuard 接入 core-edu + Redis 缓存1d
- **交付物**
- AuthorizationGuard 内部实现替换DEV_MODE 放行 → 真实 gRPC 校验
- Redis 缓存:`GetClassesByTeacher` 结果缓存key: `authz:teacher:{teacherId}:classes`TTL 5min
- **验收标准**:生产环境越权防御生效 + Redis 缓存命中
- **裁决引用**president §2.9P3 接入 core-edu 后替换 Guard 实现)
#### 3.11 /readyz + core-edu 探针1d
- **交付物**/readyz 探针注册表扩展 core-edu gRPC 50053 探针3 项Redis + iam + core-edu
- **验收标准**/readyz 返回 3 项检查结果
**P3 退出标准**core-edu gRPC 3 Query + 3 Mutation + AuthorizationGuard 生产生效 + Redis 缓存 + /readyz 3 项探针。
---
### P4content + data-ana gRPC 扩展4d
> 裁决依据§2.3 跨阶段扩展例外
#### 3.12 ContentClient + DataAnaClient gRPC2d
- **依赖**content gRPC 50054ai09 P4+ data-ana gRPC 50055ai11 P4
- **交付物**
- ContentClient gRPCKnowledgeGraphServiceGetPrerequisites / GetLearningPath
- DataAnaClient gRPCAnalyticsServiceGetClassPerformance / GetStudentWeakness / GetLearningTrend / GetTeacherDashboard
- GraphQL Query 扩展knowledgePath / classPerformance / studentWeakness / learningTrend / teacherDashboard
- **验收标准**5 个 Query 返回真实数据
#### 3.13 DataLoader 全量接入1d
- **交付物**DataLoader 覆盖全部 N+1 风险点UserLoader / ClassLoader / ExamLoader / HomeworkLoader / GradeLoader
- **验收标准**DataLoader per-request 实例 + 批量化窗口 16ms
- **裁决引用**B1 Should Have → P4 全量接入
#### 3.14 /readyz + content + data-ana 探针1d
- **交付物**/readyz 探针扩展 content + data-ana5 项Redis + iam + core-edu + content + data-ana
**P4 退出标准**content + data-ana gRPC + 5 Query + DataLoader 全量 + /readyz 5 项探针。
---
### P5ai + msg gRPC 扩展 + SSE + Kafka批次 48d
> 裁决依据B7P5 push-gateway 落地后再订阅 Kafka
#### 3.15 AiClient + MsgClient gRPC2d
- **依赖**ai gRPC 50057ai12 P5+ msg gRPC 50056ai10 P5
- **交付物**
- AiClient gRPCAiServiceChat / StreamChat streaming / GenerateQuestion / OptimizeExpression
- MsgClient gRPCNotificationServiceListNotifications / SearchNotifications / MarkAsRead
- GraphQL Query 扩展notifications + MutationgenerateQuestion / markNotificationAsRead
#### 3.16 SSE 流式透传2d
- **交付物**`GET /ai/chat/stream` SSE 端点ai.StreamChat gRPC stream → BFF → 前端 EventSource
- **验收标准**SSE 三层透传端到端通 + 背压处理 + 超时取消
- **裁决引用**02 文档 §10
#### 3.17 Kafka consumer2dpush-gateway 落地后)
- **交付物**
- Kafka consumer 订阅 `edu.identity.user.role_changed` / `edu.identity.role.updated`,精确失效 Redis 权限缓存
- 幂等性:基于 event_id 去重Redis SETNX
- **验收标准**:权限变更秒级缓存失效 + 幂等消费
- **裁决引用**B7P5 push-gateway 落地后再订阅)
#### 3.18 /readyz + ai + msg 探针 + Should Have 补全2d
- **交付物**
- /readyz 探针扩展 ai + msg7 项Redis + iam + core-edu + content + data-ana + ai + msg
- Should Have 补全OTel tracer 全链路 + /metrics 业务指标 + Redis 聚合缓存 5-30s
- **验收标准**/readyz 7 项 + OTel 全链路 trace + 缓存命中率 ≥ 60%
**P5 退出标准**ai + msg gRPC + SSE 流式 + notifications Query + Kafka consumer + /readyz 7 项探针 + Should Have 补全。
---
### P6admin 命名空间 + 硬化3d
> 裁决依据president §5.1admin-portal 复用 teacher-bff+ P6 硬化
#### 3.19 admin 命名空间实现1d
- **依赖**admin-portalai16 P6
- **交付物**
- admin schema 命名空间 Resolver 实现P2 预留的占位类型填充实际 Resolver
- admin Query/Mutation用户管理 / 角色权限管理 / 学校设置 / 组织管理 / 审计日志查询
- **验收标准**admin namespace 可内省 + admin-portal 可消费
- **裁决引用**president §5.1admin-portal 复用 teacher-bff GraphQL endpoint
#### 3.20 熔断 / 重试 / 超时1d
- **交付物**
- Circuit Breakeropossumper-downstream-service
- RetrygRPC interceptor仅幂等 RPC指数退避
- Timeoutper-RPC 3s聚合总超时 5s
- **验收标准**:熔断/重试/超时生效 + 降级策略覆盖
#### 3.21 Nice to Have 补全1d
- **交付物**Zod 全量验证 + 优雅关闭顺序HTTP→Redis→gRPC→Kafka→Tracer+ 测试覆盖率 ≥ 80%
- **验收标准**Zod 全 Controller 覆盖 + SIGTERM 顺序关闭 + Vitest 覆盖率 ≥ 80%
**P6 退出标准**admin namespace 实现 + 熔断/重试/超时 + Zod 全量 + 测试 ≥ 80% + SLO 99.9%。
---
## §4 依赖与就绪信号
- **我依赖**iam gRPC 50052ai06+ core-edu gRPC 50053ai08P3+
- **我的就绪信号**POST /graphql 可用 + dashboard Query 返回正确数据
### 4.1 我依赖的上游就绪信号
| 上游 | 就绪信号 | 阶段 | 状态 |
| ---- | -------- | ---- | ---- |
| iamai06 | gRPC 50052 + 8 RPCGetUserInfo/GetViewports/GetEffectivePermissions/GetEffectiveAccess/Logout/GetPublicKey/BatchGetUsers/GetChildrenByParent | P2 | ⏳ |
| core-eduai08 | gRPC 50053 + ExamService/HomeworkService/GradeService | P3 | ⏳ |
| contentai09 | gRPC 50054 + KnowledgeGraphService | P4 | ⏳ |
| data-anaai11 | gRPC 50055 + AnalyticsService含 GetTeacherDashboardISSUE-027 补全) | P4 | ⏳ |
| aiai12 | gRPC 50057 + AiService含 StreamChat | P5 | ⏳ |
| msgai10 | gRPC 50056 + NotificationService | P5 | ⏳ |
| push-gatewayai02 | /internal/push 落地B7 Kafka 订阅前提) | P5 | ⏳ |
| admin-portalai16 | admin schema 需求确认 | P6 | ⏳ |
### 4.2 我的就绪信号(供下游消费)
| 阶段 | 就绪信号 | 消费方 |
| ---- | -------- | ------ |
| P2 | POST /graphql 可用 + 5 Query + admin 预留 | teacher-portalai13 |
| P2 | DownstreamClient 抽象B8 回写) | student-bffai04/ parent-bffai05 |
| P3 | exams/homework/grades Query + Mutation | teacher-portalai13 |
| P4 | 学情分析 Query + DataLoader | teacher-portalai13 |
| P5 | SSE 流式 + notifications Query | teacher-portalai13 |
| P6 | admin namespace 可用 | admin-portalai16 |
---
## §5 跨阶段扩展例外验收清单
> 裁决依据president §2.3ISSUE-020。每次跨阶段扩展时对照检查。
- [ ] 扩展时更新 02-architecture-design.md 下游调用矩阵
- [ ] 扩展时更新 packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql
- [ ] 扩展时运行 `pnpm run arch:scan` 更新 arch.db
- [ ] 未修改已有 RPC 调用签名或返回类型
- [ ] 未删除已实现的 RPC 调用
- [ ] 未修改 GraphQL schema 已有字段类型(仅新增字段)
- [ ] 未修改 /readyz 已有探针检查项(仅新增)

View File

@@ -74,7 +74,7 @@ gantt
- 学生列表页classStudents GraphQL queryiam 数据)
- 个人设置页currentUser + updateUser GraphQL query/mutation
- **依赖**teacher-bff GraphQL schema 第一版ai03 + coord 仲裁 ISSUE-037+ api-gateway 路由ai01
- **Mock 策略**MSW 拦截 POST /api/teacher/graphql + POST /api/auth/login见 contract.md §4.2
- **Mock 策略**MSW 拦截 POST /api/v1/teacher/graphql + POST /api/auth/login见 contract.md §4.2,对齐 matrix.md §5 路径前缀
- **验收标准**MF 配置不破坏单体 + GraphQL 拉取数据 + 登录→Dashboard→班级列表→学生列表链路通
### P3考试/作业/成绩 + 乐观更新 + 多 Tab 同步
@@ -135,7 +135,7 @@ gantt
| --------------------------------------------------------- | ------------------------------------------------- | -------- | ---------------------- |
| packages 骨架ai13 自建) | ui-tokens/ui-components/hooks 可 import | P2 启动 | ✅ 已就绪(批次 0.15 |
| teacher-bff GraphQL schemaai03 + coord 仲裁 ISSUE-037 | packages/shared-ts/contracts/graphql/ 第一版 | P2 启动 | ⏳ 待 coord 仲裁 |
| api-gateway HTTP :8080ai01 | /api/teacher/graphql + /api/auth/login 路由可用 | P2 启动 | ⏳ 待 ai01 |
| api-gateway HTTP :8080ai01 | /api/v1/teacher/graphql + /api/auth/login 路由可用 | P2 启动 | ⏳ 待 ai01 |
| teacher-bff core-edu 扩展ai03 P3 | classExams/classHomework/studentGrades query 可用 | P3 启动 | ⏳ 待 ai03 P3 |
| teacher-bff content/data-ana 扩展ai03 P4 | knowledgeGraph/studentAnalytics query 可用 | P4 启动 | ⏳ 待 ai03 P4 |
| push-gateway WebSocket :8081/wsai02 P5 | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |

View File

@@ -7,6 +7,7 @@
> 日期2026-07-09v1 by ai06/ 2026-07-10v2 审核修订 by ai11
> 关联文档:[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[known-issues.md](../../../docs/troubleshooting/known-issues.md)
> 审核修订说明ai-allocation.md §3.2 将 data-ana 重新分配给 ai11ai 单独分配给 ai12ai11 接手后对 ai06 v1 进行审核,本版为 v2 修订。
> v2.1 审核修订ai11核查发现多处 004 章节引用断裂§4.2/§11.4/§11.5/§15.3 在 004 正文中不存在coord-cross-review §6 整改清单要求 coord 补充但尚未落实),已改为引用 coord-cross-review.md 对应裁决;修正 topic 命名统一为 `edu.insight.mastery.updated`;补充 events.proto AIUsageEvent 缺失说明。
---
@@ -41,11 +42,11 @@
- ClickHouse独占读模型宽表 `student_dashboard_view` / `student_errors` / `mastery_snapshot` / `ai_usage_log`
- RedisDataScope 缓存 5min + 预警去重位图 + CDC 幂等去重 SETNX004 §6.3 缓存策略矩阵)
- Kafka消费 Debezium CDC 事件 + 发布 `edu.insight.mastery.updated` 派生数据事件)
- iam gRPC`GetEffectiveDataScope` 解析数据范围,已仲裁 P4 补全,见 004 §15.3 #5
- iam gRPC`GetEffectiveDataScope` 解析数据范围,已仲裁 P4 补全,见 [coord-cross-review.md §2](../../docs/architecture/coord-cross-review.md) #3
- **通信方式**
- 入口HTTP`/analytics/*`,保留作 Gateway 直连降级)+ **gRPC** `AnalyticsService`P4 启用主入口BFF 调用)
- 出口Kafka 消费CDC 主通道 + 领域事件订阅备通道Kafka 发布(`edu.insight.mastery.updated`**未实现**,属派生数据豁免 Outbox见 004 §12.2
- **端口**HTTP=3006见 [data-ana config.py:7](../src/data_ana/config.py) + [api-gateway config.go:55](../../api-gateway/internal/config/config.go)**gRPC=50055**004 §1.2 服务清单 + §4.2 gRPC 启用阶段矩阵P4 启用)
- 出口Kafka 消费CDC 主通道 + 领域事件订阅备通道Kafka 发布(`edu.insight.mastery.updated`**未实现**,属派生数据豁免 Outbox [coord-cross-review.md §3.3](../../docs/architecture/coord-cross-review.md) + 004 §12.2
- **端口**HTTP=3006见 [data-ana config.py:7](../src/data_ana/config.py) + [api-gateway config.go:55](../../api-gateway/internal/config/config.go)**gRPC=50055**[coord-cross-review.md §4.3](../../docs/architecture/coord-cross-review.md) 全局端口矩阵 + [matrix.md §2](../../docs/architecture/issues/matrix.md)P4 启用)
## 2. 我的限界上下文
@@ -79,7 +80,7 @@
| events.proto | `HomeworkEvent` | 备通道(未消费) | 未来双消费:作业提交/批改事件 → 更新学情 |
| events.proto | `ClassEvent` | 备通道(未消费) | 未来双消费:班级变更事件 → 同步班级维度 |
| analytics.proto | `GetClassPerformanceRequest` 等 | 自身暴露契约(待实现 gRPC server | P4 启用 gRPC=50055 后暴露 `AnalyticsService` |
| iam.proto | `GetEffectiveDataScopeRequest`**待 coord 在 iam.proto 新增**004 §15.3 #5 已裁决 P4 补全) | 调用契约 | gRPC 调 iam 解析 DataScope结果 Redis 缓存 5min |
| iam.proto | `GetEffectiveDataScopeRequest`**待 coord 在 iam.proto 新增**[coord-cross-review.md §2](../../docs/architecture/coord-cross-review.md) #3 已裁决 P4 补全;当前 iam.proto 仅有 4 RPC无此 RPC**P4 阻塞项** | 调用契约 | gRPC 调 iam 解析 DataScope结果 Redis 缓存 5min |
### 暴露的 API / 事件
@@ -94,7 +95,7 @@
| GET | `/analytics/student/{student_id}/weakness` | 学生薄弱知识点mastery < 0.6 |
| GET | `/analytics/student/{student_id}/errorbook` | 学生错题本 |
> **响应信封约束**:以上 HTTP 端点当前实现为 `{success, data, degraded}` 结构,违反 004 §11.5 统一响应信封 ActionStatePython 服务必须改 ActionStatedegraded 作为 `details.degraded` 子字段)。阶段 2 设计已对齐(见 02-architecture-design.md §4.3)。
> **响应信封约束**:以上 HTTP 端点当前实现为 `{success, data, degraded}` 结构,违反统一响应信封 ActionState[coord-cross-review.md §5.3](../../docs/architecture/coord-cross-review.md) 已裁决 Python 服务必须改 ActionStatedegraded 作为 `details.degraded` 子字段)。阶段 2 设计已对齐(见 02-architecture-design.md §4.3)。
**gRPC 契约**analytics.protoP4 启用 server端口 50055
@@ -113,11 +114,13 @@
| 事件 | Topic004 §7.2 | 触发时机 | 消费者004 §7.3 | Outbox 合规性 |
| ---------------- | ----------------------------- | -------------- | --------------------------------------- | ----------------------------------------------------------- |
| `MasteryUpdated` | `edu.insight.mastery.updated` | 掌握度计算完成 | core-edu推荐个性化练习、msg预警 | **豁免 Outbox**(派生数据,见 004 §12.2 + §15.3 #6 已仲裁 |
| `MasteryUpdated` | `edu.insight.mastery.updated` | 掌握度计算完成 | core-edu推荐个性化练习、msg预警 | **豁免 Outbox**(派生数据,见 [coord-cross-review.md §3.3](../../docs/architecture/coord-cross-review.md) + 004 §12.2 |
> **当前未实现发布**data-ana 当前只消费不发布。掌握度计算完成后通过 `aiokafka.AIOKafkaProducer` 直接发布(已豁免 Outbox004 §15.3 #6 仲裁结论),下游 core-edu / msg 消费。失败重试 3 次仍失败落 `mastery_publish_failed` 本地表。
> **当前未实现发布**data-ana 当前只消费不发布。掌握度计算完成后通过 `aiokafka.AIOKafkaProducer` 直接发布(已豁免 Outboxcoord-cross-review.md §3.3 仲裁结论),下游 core-edu / msg 消费。失败重试 3 次仍失败落 `mastery_publish_failed` 本地表。
- **错误码前缀**`DATA_ANA_*`004 §11.4 错误码前缀矩阵已登记,清单见阶段 2 §6.2
> **Topic 命名一致性**:本模块统一使用 `edu.insight.mastery.updated`(与 004 §7.2 + matrix.md §4 对齐。contract.md 早期版本写 `edu.data_ana.mastery.events`,已在 v2.1 修正。
- **错误码前缀**`DATA_ANA_*`[matrix.md §6](../../docs/architecture/issues/matrix.md) 错误码前缀矩阵已登记,清单见阶段 2 §6.2
- **缓存**RedisDataScope 缓存 5min 事件驱动失效 / CDC event_id 幂等去重 SETNX TTL 7d / 预警去重位图);学情宽表走 ClickHouse 实时CDC 同步延迟 < 5s
## 4. 我的技术栈
@@ -145,7 +148,7 @@
- **交付物**pending-features P4DataAna 学情诊断宽表 5s 内返回 + CDC 链路延迟 < 5s + **双轨读策略落地(实时查主库 + 聚合查宽表)** + CDC 模式回写黄金模板 README + 打 tag `v0.4.0-p4`
- **依赖上游**
- P1 地基api-gateway 路由 + arch.db 扫描器 Python 支持
- P2 身份iam `GetEffectiveDataScope` gRPC已仲裁 P4 补全,见 004 §15.3 #5
- P2 身份iam `GetEffectiveDataScope` gRPC已仲裁 P4 补全,见 [coord-cross-review.md §2](../../docs/architecture/coord-cross-review.md) #3**当前 iam.proto 未实现P4 阻塞项**
- P3 核心教学core-edu 写成绩到 MySQLDebezium 监听 binlog+ Outbox 领域事件(备通道)
- P4 同期content 服务(提供知识点 ID 供掌握度计算content → data-ana 事件流见 004 §4 服务依赖图)
- **下游依赖我**
@@ -160,8 +163,8 @@
> Python 服务无 NestJS 装饰器体系,权限校验等通过等价方式实现。
- [ ] 权限装饰器等价物:**当前 HTTP 端点全部裸露,无权限校验**。Gateway 层做 JWT 校验,但 data-ana 本身未校验 `x-user-id` / DataScope。**阶段 2 需设计 FastAPI Depends 权限依赖 + DataScope 过滤注入**
- [ ] 错误码前缀统一:**当前无错误码体系**,降级时返回 `degraded: true` 标记但无业务错误码。**阶段 2 需定义 `DATA_ANA_*` 错误码清单**004 §11.4 已登记前缀)
- [ ] **响应信封对齐 ActionState**004 §11.5 已仲裁 P0 整改):当前 `{success, data, degraded}` 偏离结构,须改为 `{success: true, data: T}` / `{success: false, error: {code, message, details?, traceId?}}`degraded 作为 `details.degraded` 子字段
- [ ] 错误码前缀统一:**当前无错误码体系**,降级时返回 `degraded: true` 标记但无业务错误码。**阶段 2 需定义 `DATA_ANA_*` 错误码清单**[matrix.md §6](../../docs/architecture/issues/matrix.md) 已登记前缀)
- [ ] **响应信封对齐 ActionState**[coord-cross-review.md §5.3](../../docs/architecture/coord-cross-review.md) 已裁决 P0 整改):当前 `{success, data, degraded}` 偏离结构,须改为 `{success: true, data: T}` / `{success: false, error: {code, message, details?, traceId?}}`degraded 作为 `details.degraded` 子字段
- [x] logger / metrics / tracer 三支柱(已具备,见 main.py + clickhouse_client.py生产改 JSONRenderer
- [x] `/healthz` + `/readyz` 健康检查已具备readyz 含 ClickHouse ping + CDC 状态;**待补 Redis + iam gRPC 连通性检查**
- [ ] 优雅关闭 SIGTERM当前 lifespan 仅关闭 CDC task + ClickHouse client**未注册 SIGTERM 信号处理器**显式 drain。阶段 2 设计顺序HTTP stop → gRPC graceful stop 30s → CDC commit offset → Kafka producer flush → ClickHouse close → iam channel close
@@ -210,17 +213,17 @@
## coord 交叉审查结论对齐v2 修订,原 v1 标题"待 coord 交叉审查的跨模块契约对齐项"
> v1 提请的 5 项跨模块契约对齐项coord 已在 [coord-cross-review.md](../../../docs/architecture/coord-cross-review.md) 完成仲裁,裁决结论中涉及架构设计意图的部分已沉淀到 004 对应章节004 §15.3 共性问题 #1-#7。本节 v2 改为"对齐状态"。
> v1 提请的 5 项跨模块契约对齐项coord 已在 [coord-cross-review.md](../../../docs/architecture/coord-cross-review.md) 完成仲裁。本节 v2.1 改为"对齐状态",并标注落实情况
| # | 议题v1 提请) | coord 裁决结论(004 §15.3 | data-ana 文档对齐状态v2 |
| # | 议题v1 提请) | coord 裁决结论(coord-cross-review.md | data-ana 文档对齐状态v2.1 |
| --- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| 1 | **data-ana 是否发布 `edu.insight.mastery.updated` 事件** | §15.3 #6:派生数据事件豁免 Outbox允许直接 Kafka producer | ✅ v2 已对齐§3 标注"豁免 Outbox",阶段 2 §5.2 设计直接 producer 链路 |
| 2 | **data-ana / ai 是否需要实现 gRPC server** | §4.2 gRPC 启用阶段矩阵P4 content + data-ana 启用 | ✅ v2 已对齐§1 标注"gRPC=50055 P4 启用",阶段 2 §1 分层图含 grpc.aio Server |
| 3 | **CDC 直连 vs Outbox 领域事件双通道** | §15.3 #6 + ADR-008维持 CDC 为主通道events.proto 作为业务语义补充,待 P4 后期评估是否双消费 | ✅ v2 已对齐§3 双通道说明 + 标注"领域事件为备通道" |
| 4 | **data-ana DataScope 过滤实现位置** | §15.3 #5iam 新增 `GetEffectiveDataScope` gRPC RPCP4 补全data-ana 在 ClickHouse 查询 SQL 拼接时注入 WHERE | ✅ v2 已对齐§3 列出 iam.proto 调用契约;阶段 2 §6.1 设计 `inject_data_scope` Depends |
| 5 | **data-ana ClickHouse DDL 管理位置** | §15.3 #7:采纳 `infra/clickhouse/ddl/`coord 建立data-ana 提供 DDL 内容 | ✅ v2 已对齐:阶段 2 §3 标注"DDL 文件由 coord 统一管理在 `infra/clickhouse/ddl/`" |
| 6 | **新增 `edu.insight.ai.usage` topic**v1 阶段 2 §8.3 #3 提请) | §15.3 #4:补登,见 004 §7.2 | ✅ v2 已对齐:阶段 2 §5.1 列出消费此 topic |
| 7 | **iam GetEffectiveDataScope proto 新增**v1 阶段 2 §8.3 #2 提请) | §15.3 #5P4 补全 | ✅ v2 已对齐§3 列出 iam.proto 调用契约 |
| 1 | **data-ana 是否发布 `edu.insight.mastery.updated` 事件** | §3.3:派生数据事件豁免 Outbox允许直接 Kafka producer | ✅ v2 已对齐§3 标注"豁免 Outbox",阶段 2 §5.2 设计直接 producer 链路 |
| 2 | **data-ana / ai 是否需要实现 gRPC server** | §2.1P4 content + data-ana 启用 gRPC | ✅ v2 已对齐§1 标注"gRPC=50055 P4 启用",阶段 2 §1 分层图含 grpc.aio Server |
| 3 | **CDC 直连 vs Outbox 领域事件双通道** | §3.3 + ADR-008维持 CDC 为主通道events.proto 作为业务语义补充,待 P4 后期评估是否双消费 | ✅ v2 已对齐§3 双通道说明 + 标注"领域事件为备通道" |
| 4 | **data-ana DataScope 过滤实现位置** | §2 #3iam 新增 `GetEffectiveDataScope` gRPC RPCP4 补全data-ana 在 ClickHouse 查询 SQL 拼接时注入 WHERE | ✅ v2 已对齐§3 列出 iam.proto 调用契约;阶段 2 §6.1 设计 `inject_data_scope` Depends |
| 5 | **data-ana ClickHouse DDL 管理位置** | §4:采纳 `infra/clickhouse/ddl/`coord 建立data-ana 提供 DDL 内容 | ✅ v2 已对齐:阶段 2 §3 标注"DDL 文件由 coord 统一管理在 `infra/clickhouse/ddl/`" |
| 6 | **新增 `edu.insight.ai.usage` topic**v1 阶段 2 §8.3 #3 提请) | §3.2:补登,见 004 §7.2**events.proto AIUsageEvent message 待 coord 补充** | ✅ v2 已对齐:阶段 2 §5.1 列出消费此 topic;⚠️ events.proto 缺 AIUsageEvent 定义 |
| 7 | **iam GetEffectiveDataScope proto 新增**v1 阶段 2 §8.3 #2 提请) | §2 #3P4 补全**当前 iam.proto 仅 4 RPC未实现P4 阻塞项** | ✅ v2 已对齐§3 列出 iam.proto 调用契约;⚠️ iam.proto 未实现 |
> v1 §8.3 "未决设计决策"3 项全部已被 coord 仲裁v2 不再列为"未决"。文档后续修订如发现新冲突,按 ai-allocation.md §9.4 proto 变更流程提请 coord。

View File

@@ -16,11 +16,11 @@
1. **契约先行**proto 已定义analytics.proto / events.proto / iam.proto实现前不修改 proto如需修改走 coord 流程ai-allocation.md §9.4
2. **CQRS 读写分离**data-ana 是纯读模型服务(无 MySQL 写ClickHouse 宽表由 CDC 投影构建
3. **事件驱动**data-ana 消费 CDC主通道+ 领域事件(备通道,待 P4 后期评估);发布 `edu.insight.mastery.updated` 派生数据事件(豁免 Outbox004 §12.2 + §15.3 #6 已仲裁)
4. **gRPC 优先**004 §4.1 + §4.2 明确 BFF → 业务服务走 gRPC**P4 启用 data-ana gRPC server 端口 50055**HTTP 保留作 Gateway 直连降级)
5. **DataScope 过滤**004 §5.3 DataScope 6 级在查询层注入 WHEREiam `GetEffectiveDataScope` gRPC004 §15.3 #5 已仲裁 P4 补全)
3. **事件驱动**data-ana 消费 CDC主通道+ 领域事件(备通道,待 P4 后期评估);发布 `edu.insight.mastery.updated` 派生数据事件(豁免 Outbox[coord-cross-review.md §3.3](../../docs/architecture/coord-cross-review.md) + 004 §12.2 已仲裁)
4. **gRPC 优先**004 §4.1 + [coord-cross-review.md §2.1](../../docs/architecture/coord-cross-review.md) 明确 BFF → 业务服务走 gRPC**P4 启用 data-ana gRPC server 端口 50055**HTTP 保留作 Gateway 直连降级)
5. **DataScope 过滤**004 §5.3 DataScope 6 级在查询层注入 WHEREiam `GetEffectiveDataScope` gRPC[coord-cross-review.md §2](../../docs/architecture/coord-cross-review.md) #3 已仲裁 P4 补全)
6. **三支柱可观测**structlog + prometheus-client + OpenTelemetry已具备需补业务指标 + gRPC server interceptor
7. **统一响应信封 ActionState**004 §11.5 已仲裁 P0 整改):成功 `{success: true, data: T}` / 失败 `{success: false, error: {code, message, details?, traceId?}}` / 降级 `degraded` 作为 `details.degraded` 子字段
7. **统一响应信封 ActionState**[coord-cross-review.md §5.3](../../docs/architecture/coord-cross-review.md) 已裁决 P0 整改):成功 `{success: true, data: T}` / 失败 `{success: false, error: {code, message, details?, traceId?}}` / 降级 `degraded` 作为顶层 `details.degraded` 子字段(非 `error.details`
8. **降级模式**外部依赖ClickHouse / Kafka / iam gRPC / Redis不可用时返回骨架数据 + `details.degraded: true`
9. **Python 规范**pydantic-settings 配置 / Pydantic 模型校验 / async 优先 / 类型注解强制 / ruff 零错误
10. **长远架构演进**:为 P5ai 用量消费 / gRPC stream、P6CDC 水平扩展 / 容量规划 / 数据治理 / Service Mesh做好铺垫见 §14 / §19
@@ -295,13 +295,15 @@ ORDER BY (student_id, class_id, attendance_date);
| `GetAdminDashboard` | `GetAdminDashboardRequest{user_id, scope, scope_id?}` | `AdminDashboard` | `ANALYTICS_ADMIN_DASHBOARD` |
| `GetWarnings` | `GetWarningsRequest{class_id?, severity?, since?}` | `WarningList` | `ANALYTICS_WARNING_READ` |
| `GetMasteryDistribution` | `GetMasteryDistributionRequest{class_id, subject_id, knowledge_point_id?}` | `MasteryDistribution` | `ANALYTICS_CLASS_READ` |
| `GetStudentMastery` | `GetStudentMasteryRequest{student_id, subject_id?}` | `StudentMastery` | `ANALYTICS_STUDENT_READ` |
| `TriggerWarning` | `TriggerWarningRequest{target_id, warning_type, severity}` | `TriggerWarningResponse` | `ANALYTICS_WARNING_READ` |
| `SubscribeMasteryUpdate` | `SubscribeMasteryUpdateRequest{student_id?, class_id?}` | `stream MasteryUpdateEvent` | `ANALYTICS_STUDENT_READ` |
> **proto 扩展提案**(待 coord 审议):上述新增 RPC 的 message 定义需在 `packages/shared-proto/proto/analytics.proto` 补充。`SubscribeMasteryUpdate` 为 server-streaming RPC为 P5+ AI 个性化推荐预留实时推送通道。
> **proto 扩展提案**(待 coord 审议):上述新增 RPC 的 message 定义需在 `packages/shared-proto/proto/analytics.proto` 补充,共 12 RPC3 现有 + 9 扩展)。`SubscribeMasteryUpdate` 为 server-streaming RPC为 P5+ AI 个性化推荐预留实时推送通道。
**权限校验**gRPC server interceptor 从 metadata 提取 `x-user-id` / `x-user-roles` / `x-data-scope`,调用 `AuthDepends` 等价逻辑。
### 4.3 ActionState 统一响应信封(004 §11.5 P0 整改)
### 4.3 ActionState 统一响应信封(coord-cross-review.md §5.3 P0 整改)
所有 HTTP/gRPC 响应必须遵循 ActionState 信封:
@@ -318,23 +320,22 @@ class ActionStateError(BaseModel):
trace_id: str | None = None
class ActionState(BaseModel, Generic[T]):
"""统一响应信封(004 §11.5.
"""统一响应信封(coord-cross-review.md §5.3 裁决.
成功:{success: true, data: T}
失败:{success: false, error: {code, message, details?, trace_id?}}
降级success=true 但 error.details.degraded=true保留功能但数据可能不完整
降级:{success: true, data: T, details: {degraded: true}}(保留功能但数据可能不完整)
"""
success: bool
data: T | None = None
error: ActionStateError | None = None
details: dict[str, Any] | None = None # 降级标记放此字段,不放 error
@classmethod
def ok(cls, data: T, *, degraded: bool = False) -> "ActionState[T]":
# 降级不是错误success=Truedegraded 标记放 details 子字段(非 error.details
details = {"degraded": True} if degraded else None
# 简化degraded 作为 data 的元信息附加,复杂场景用 details
return cls(success=True, data=data, error=None if not degraded else
ActionStateError(code="DATA_ANA_DEGRADED", message="degraded mode",
details=details))
return cls(success=True, data=data, error=None, details=details)
@classmethod
def fail(cls, code: str, message: str, *, trace_id: str | None = None,
@@ -361,7 +362,7 @@ class StudentScore(BaseModel):
# 端点返回类型ActionState[ClassPerformanceData]
```
> **P0 整改要点**:当前 main.py 返回 `{success, data, degraded}` 三字段平铺,违反 004 §11.5。实现阶段需重构为 `ActionState[T]` 泛型,`degraded` 移到 `error.details.degraded`。
> **P0 整改要点**:当前 main.py 返回 `{success, data, degraded}` 三字段平铺,违反 coord-cross-review.md §5.3 裁决。实现阶段需重构为 `ActionState[T]` 泛型,`degraded` 移到顶层 `details.degraded`(非 `error.details`,降级不是错误)
## 5. 事件设计
@@ -547,13 +548,13 @@ async def readyz() -> dict:
| 被调用 | api-gateway | HTTP | `/analytics/*` | Gateway 代理(降级通道) |
| 被调用 | teacher-bff / student-bff / parent-bff | gRPC | `AnalyticsService.*` | BFF 聚合查询4 端 Dashboard |
| 被调用 | aiP5+ | gRPC | `AnalyticsService.GetStudentWeakness / GetLearningTrend / SubscribeMasteryUpdate` | AI 个性化出题上下文 + 实时掌握度推送 |
| 调用 | iam | gRPC | `IamService.GetEffectiveDataScope`004 §15.3 #5 已仲裁 P4 补全 | DataScope 解析 |
| 调用 | iam | gRPC | `IamService.GetEffectiveDataScope`[coord-cross-review.md §2](../../docs/architecture/coord-cross-review.md) #3 已仲裁 P4 补全**当前 iam.proto 未实现** | DataScope 解析 |
| 消费 | core-eduCDC | Kafka | `edu-cdc.next_edu_cloud.core_edu_grades/exams/homework_submissions/attendance` | 学情 + 考勤数据投递 |
| 消费 | core-eduCDC | Kafka | `edu-cdc.next_edu_cloud.classes` | 班级维度同步 |
| 消费 | iamCDC | Kafka | `edu-cdc.next_edu_cloud.iam_users` | 用户 dataScope 同步 |
| 消费 | contentCDC | Kafka | `edu-cdc.next_edu_cloud.content_knowledge_points` | 知识点元数据同步v2 新增) |
| 消费 | aiP5+ | Kafka | `edu.insight.ai.usage`004 §15.3 #6 已仲裁004 §7.2 已登记) | AI 用量落库 |
| 发布 | core-edu / msg | Kafka | `edu.insight.mastery.updated`004 §12.2 + §15.3 #6 已仲裁:派生数据豁免 Outbox | 掌握度更新通知 |
| 消费 | aiP5+ | Kafka | `edu.insight.ai.usage`[coord-cross-review.md §3.2](../../docs/architecture/coord-cross-review.md) 已仲裁004 §7.2 已登记) | AI 用量落库 |
| 发布 | core-edu / msg | Kafka | `edu.insight.mastery.updated`[coord-cross-review.md §3.3](../../docs/architecture/coord-cross-review.md) + 004 §12.2 已仲裁:派生数据豁免 Outbox | 掌握度更新通知 |
| 发布 | msg / core-edu | Kafka | `edu.insight.warning.triggered`v2 新增,需 coord 在 004 §7.2 登记) | 预警触发通知 |
> **004 §4.1 服务间通信矩阵对齐**content → data-ana 已声明"教学内容变更通知",本次落实为 CDC 订阅 `content_knowledge_points` 表。
@@ -562,10 +563,10 @@ async def readyz() -> dict:
### 8.1 假设
- **假设 1**iam 在 P4 阶段提供 `GetEffectiveDataScope(userId) → DataScope` gRPC API004 §15.3 #5 已仲裁)。若 iam 未及时提供fallback 为:从 `x-user-roles` 头推导admin=ALL, teacher=CLASS_TAUGHT, student=SELF但无法支持细粒度年级/学校范围
- **假设 1**iam 在 P4 阶段提供 `GetEffectiveDataScope(userId) → DataScope` gRPC API[coord-cross-review.md §2](../../docs/architecture/coord-cross-review.md) #3 已仲裁)。**当前 iam.proto 仅 4 RPC 未实现此 RPCP4 阻塞项**。若 iam 未及时提供fallback 为:从 `x-user-roles` 头推导admin=ALL, teacher=CLASS_TAUGHT, student=SELF但无法支持细粒度年级/学校范围
- **假设 2**core-edu 的 `core_edu_homework_submissions` / `core_edu_attendance` 表存在 binlog。若不存在需 core-edu 补表或走 Outbox 事件(领域事件备通道)
- **假设 3**ClickHouse `ReplacingMergeTree` 在查询时需 `FINAL` 关键字确保去重生效。**v2 修复要求**:所有查询加 `FINAL` 或使用 `argMax` 聚合(当前实现未加,是 P0 整改项)
- **假设 4**coord 已在 004 §7.2 登记新增 `edu.insight.ai.usage` topic004 §15.3 #6 已仲裁)
- **假设 4**coord 已在 004 §7.2 登记新增 `edu.insight.ai.usage` topic[coord-cross-review.md §3.2](../../docs/architecture/coord-cross-review.md) 已仲裁);**events.proto AIUsageEvent message 待 coord 补充**
### 8.2 技术风险
@@ -582,22 +583,22 @@ async def readyz() -> dict:
| 大数据量 Dashboard 聚合超时 | 管理员 Dashboard 全校聚合慢 | 物化视图预聚合 + 异步刷新 + 缓存 5min |
| 知识点元数据与成绩关联失败 | mastery_snapshot 缺知识点标题 | content CDC 同步 + 缺失时显示 `knowledge_point_id` |
### 8.3 coord 交叉审查结论对齐v2原"未决设计决策"已全部仲裁)
### 8.3 coord 交叉审查结论对齐v2.1:原"未决设计决策"已全部仲裁)
| # | 议题 | coord 仲裁结论 | 涉及文档 |
| --- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | ----------------------- |
| 1 | data-ana 发布 `edu.insight.mastery.updated` 用直接 producer非 Outbox是否合规 | ✅ 已仲裁:派生数据事件豁免 Outbox004 §12.2 + §15.3 #6 | 004 §12.2 |
| 2 | 新增 `edu.insight.ai.usage` topic + `AIUsageEvent` proto message | ✅ 已仲裁coord 在 004 §7.2 登记 + events.proto 补 message | 004 §7.2 + events.proto |
| 3 | iam 新增 `GetEffectiveDataScope` gRPC RPC | ✅ 已仲裁P4 补全(004 §15.3 #5 | iam.proto |
| 4 | data-ana 实现 gRPC server 决策 | ✅ 已仲裁P4 启用(004 §4.2 gRPC 启用阶段矩阵) | 004 §4.2 |
| 1 | data-ana 发布 `edu.insight.mastery.updated` 用直接 producer非 Outbox是否合规 | ✅ 已仲裁:派生数据事件豁免 Outbox[coord-cross-review §3.3](../../docs/architecture/coord-cross-review.md) + 004 §12.2 | 004 §12.2 |
| 2 | 新增 `edu.insight.ai.usage` topic + `AIUsageEvent` proto message | ✅ 已仲裁coord 在 004 §7.2 登记 + events.proto 补 message**⚠️ events.proto 当前缺 AIUsageEvent待 coord 落实** | 004 §7.2 + events.proto |
| 3 | iam 新增 `GetEffectiveDataScope` gRPC RPC | ✅ 已仲裁P4 补全([coord-cross-review §2](../../docs/architecture/coord-cross-review.md) #3**⚠️ iam.proto 当前仅 4 RPC未实现** | iam.proto |
| 4 | data-ana 实现 gRPC server 决策 | ✅ 已仲裁P4 启用([coord-cross-review §2.1](../../docs/architecture/coord-cross-review.md) gRPC 启用阶段) | 004 §4.1 |
| 5 | ClickHouse DDL 管理位置(建议 `infra/clickhouse/ddl/` | ⏳ 待 coord 建立 `infra/clickhouse/ddl/` 目录 + data-ana 提供内容 | infra/ |
| 6 | 端口冲突检查data-ana HTTP=3006 / gRPC=50055 | ✅ 无冲突(004 §1.2 端口分配) | 004 §1.2 |
| 7 | 错误码前缀检查:`DATA_ANA_*` | ✅ 无冲突(004 §11.4 错误码前缀矩阵) | 004 §11.4 |
| 8 | 黄金模板对齐Python 服务无 NestJS 装饰器,权限校验用 FastAPI Depends 等价物是否认可 | ✅ 已仲裁:认可(004 §15.3 | — |
| 6 | 端口冲突检查data-ana HTTP=3006 / gRPC=50055 | ✅ 无冲突([coord-cross-review §4.3](../../docs/architecture/coord-cross-review.md) 全局端口矩阵) | port-allocation |
| 7 | 错误码前缀检查:`DATA_ANA_*` | ✅ 无冲突([matrix.md §6](../../docs/architecture/issues/matrix.md) 错误码前缀矩阵) | matrix.md §6 |
| 8 | 黄金模板对齐Python 服务无 NestJS 装饰器,权限校验用 FastAPI Depends 等价物是否认可 | ✅ 已仲裁:认可([coord-cross-review §5.3](../../docs/architecture/coord-cross-review.md) | — |
| 9 | `edu.insight.warning.triggered` topic 新增 | ⏳ 待 coord 在 004 §7.2 登记v2 新提案) | 004 §7.2 |
| 10 | analytics.proto 扩展4 端 Dashboard / Warning / MasteryDistribution / Stream RPC | ⏳ 待 coord 审议v2 新提案§4.2 | analytics.proto |
> 所有"未决设计决策"已消除,进入实现阶段无阻塞。剩余 3 项 ⏳ 为 v2 新提案,待 coord 审议但不阻塞 P4 主体实现(可先用现有 RPC + HTTP 端点兜底)。
> 所有"未决设计决策"已消除。**P4 阻塞项**#2 events.proto 缺 AIUsageEvent、#3 iam.proto 缺 GetEffectiveDataScope均待 coord 落实。剩余 3 项 ⏳ 为 v2 新提案,待 coord 审议但不阻塞 P4 主体实现(可先用现有 RPC + HTTP 端点 + mock 兜底)。
---
@@ -783,7 +784,7 @@ ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
EXPOSE 3006 50055
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3006/healthz || exit 1
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:3006/healthz')" || exit 1
CMD ["python", "-m", "uvicorn", "src.data_ana.main:app", \
"--host", "0.0.0.0", "--port", "3006"]
```
@@ -945,12 +946,26 @@ CDC → ClickHouse 原始表 → MaterializedView → 宽表(自动聚合)
- **AI 驱动的学情诊断**data-ana 提供数据ai 服务提供模型,组合成"智能诊断报告"
- **多租户**:当前 school_id 隐含在 class_id 中,远期可显式多租户隔离
## 19. 服务审计表 v2黄金模板对齐
## 19. 非功能性需求NFRv2.1 新增
| 维度 | 指标 | 目标值 | 验证方式 |
| ---------- | ----------------------------- | --------------- | ------------------------------- |
| 性能 | ClickHouse 宽表查询 P99 延迟 | < 5s | P4 退出标准 + Prometheus 监控 |
| 性能 | CDC 链路延迟 | < 5s | Debezium ts_ms → CH 写入时间差 |
| 性能 | gRPC RPC P99 延迟 | < 500ms | OTel trace |
| 可用性 | P4 单实例 | 99% | 降级模式保证 |
| 可用性 | P6 多实例 + ClickHouse 集群 | 99.9% | HPA + 副本 |
| 安全 | DataScope 越权防护 | 0 越权 | 权限测试 + 渗透测试 |
| 安全 | PII 日志脱敏 | 100% student_id hash | structlog processor 单测 |
| 可扩展性 | CDC 消费者水平扩展 | N 实例无重复 | ReplacingMergeTree + Redis SETNX |
| 数据保留 | ClickHouse TTL | 见 §11.1 | TTL 策略自动执行 |
## 20. 服务审计表 v2黄金模板对齐
| 维度 | 状态 | 说明 |
| ------------- | ---- | ----------------------------------------------------------------------- |
| 契约proto | ✅ | analytics.proto 已定义 3 RPCv2 提案扩展 7 RPC |
| 路由 | ✅ | HTTP 13 端点 + gRPC 10 RPC |
| 契约proto | ✅ | analytics.proto 已定义 3 RPCv2 提案扩展至 12 RPC含 GetStudentMastery / TriggerWarning |
| 路由 | ✅ | HTTP 14 端点3 基础 + 11 业务)+ gRPC 12 RPC |
| 数据访问 | ✅ | ClickHouse 5 宽表 + Redis 缓存 + iam gRPC |
| 鉴权 | ✅ | FastAPI Depends 等价物 + 7 权限点 |
| 错误处理 | ✅ | 13 错误码 `DATA_ANA_*` + ActionState 信封 |
@@ -960,6 +975,7 @@ CDC → ClickHouse 原始表 → MaterializedView → 宽表(自动聚合)
| 健康检查 | ✅ | /healthz + /readyzv2 补 redis/iam_grpc |
| 配置管理 | ✅ | pydantic-settings 完整配置项§15 |
| 降级模式 | ✅ | 4 降级场景CH/Kafka/Redis/iam |
| NFR | ✅ | v2.1 新增 §19性能/可用性/安全/可扩展性目标已定义 |
| 响应信封 | ⚠️ | v2 设计对齐 ActionState实现阶段需重构P0 整改) |
| Redis | ⚠️ | v2 设计就绪,实现阶段需新增 redis-py 依赖 + RedisClient |
| gRPC server | ⚠️ | v2 设计就绪,实现阶段需新增 grpc.aio + betterproto + server interceptor |
@@ -975,5 +991,6 @@ CDC → ClickHouse 原始表 → MaterializedView → 宽表(自动聚合)
**AI Agent**: ai11 (data-ana)
**Branch**: 单仓库并行模式(直接提交 main
**Coordinator**: coord-ai
**v2 修订依据**: ai-allocation.md §3.2 重新分配 + 004 §1.2/§4.2/§7.2/§11.5/§12.2/§15.3 + ai-allocation §5 设计重点 + pending-features P4 退出标准
**v2 审核结论**: 16 项遗漏已全部补强3 项 P0 + 5 项 P1 + 8 项 P2文档进入实现阶段无阻塞
**v2 修订依据**: ai-allocation.md §3.2 重新分配 + coord-cross-review.md §2/§3.3/§5.3 + 004 §4.1/§7.2/§12.2 + ai-allocation §5 设计重点 + pending-features P4 退出标准
**v2.1 审核修订**: 修正 004 章节引用断裂§4.2/§11.4/§11.5/§15.3 不存在→改引 coord-cross-review修正 ActionState 实现矛盾degraded 从 error.details 移至顶层 details补 RPC 至 12 个GetStudentMastery/TriggerWarning修正 HTTP 端点计数 14Dockerfile HEALTHCHECK 改用 urllib 避免 curl 依赖;新增 §19 NFR 章节
**v2 审核结论**: 16 项遗漏已全部补强3 项 P0 + 5 项 P1 + 8 项 P2文档进入实现阶段无阻塞P4 阻塞项iam.proto GetEffectiveDataScope + events.proto AIUsageEvent 待 coord 落实,可用 fallback/mock 兜底)