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
> AI:ai07(TS/React · 教学场景域前端 shell)
> AI:ai13(TS/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-09(2026-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-gateway(REST,经 Next.js `rewrites` 代理 `/api/v1/*`)
- **下游(同步)**:api-gateway(经 Next.js `rewrites` 代理 `/api/v1/*`),再反向代理到 teacher-bff `POST /graphql`
- **下游(推送,P5)**:push-gateway(WebSocket/SSE)
- **BFF 对接**:teacher-bff(GraphQL Yoga + DataLoader,P2-P3 用 REST 过渡)
- **通信方式**:HTTP/REST(前端→Gateway)+ WebSocket(前端→push-gateway,P5)
- **BFF 对接**:teacher-bff(GraphQL Yoga + DataLoader,P2 起 all-in GraphQL,F9 裁决,无 REST 过渡)
- **通信方式**:GraphQL over HTTP(前端→Gateway→teacher-bff)+ WebSocket(前端→push-gateway,P5)+ 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 用 GraphQL(Yoga + DataLoader),前端用 urql client,**无 REST 过渡阶段**(详见 [03-long-term-architecture.md §1.4](./03-long-term-architecture.md))
- MF 架构下,teacher-portal 作为 Shell 宿主提供 AppShell + GraphQLProvider 单例 + 共享组件库 + 权限 Hook(ARB-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-edu(classes 模块) | 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/*` | ai(SSE 流式) | 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 Operation(P2 Must Have 加粗) |
| ---- | ---------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| P2 | GraphQL Query(teacher-bff,ARB-001 第一版) | teacher-bff → iam/classes | **`dashboard`**、**`viewports`**、**`me`**、**`classes`**、**`class(id)`**(5 个 Query,ARB-001 §1.2) |
| P2 | HTTP(登录,MF 依赖认证前提,非 GraphQL) | api-gateway → iam | `POST /api/auth/login`(F12:JWT 存 localStorage,P6 迁移 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 Query(P3 core-edu 就绪后)、classPerformance/studentWeakness/learningTrend(P4 data-ana)、notifications(P5 msg)、所有 Mutation(P3+)、Subscription(P6+ 评估)——见 ARB-001 §1.2。
### 3.2 统一响应契约(ActionState 信封 + GraphQL errors)
BFF GraphQL 始终返回 `ActionState` 信封(004 §11.5),GraphQL `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)
| 协议 | 场景 |
| ------------------------- | -------------------------------------------- |
| WebSocket(push-gateway) | 学生提交作业通知、考试成绩录入提醒、全校广播 |
| SSE(ai 服务) | AI 辅助出题流式响应 |
| SSE(ai 服务,经 teacher-bff 代理或直连) | AI 辅助出题流式响应 |
### 3.4 proto 不直接消费
前端不调用 gRPC,BFF 把 gRPC 聚合为 REST/GraphQL 暴露给前端。前端仅消费 `packages/contracts/src/permissions.ts` 中的权限点常量(TS 文件,非 proto 生成)。
前端不调用 gRPC,BFF 把 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 = Shell(ARB-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 cacheExchange(GraphQL 数据)+ 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-a11y(error 级) | WCAG 2.2 AA |
| 字体 | Inter(sans)/ Fraunces(serif)/ JetBrains Mono(mono) | 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
> AI:ai07(TS/React · 教学场景域前端 shell)
> AI:ai13(TS/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-09(2026-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)
> 状态:✅ 已对齐 F9(GraphQL P2 起)+ ARB-001(schema 第一版)+ ARB-002(MF Shell 暴露清单)
---
## 1. 模块内部分层图(4 端统一 MF 架构)
## 1. 模块内部分层图(4 端统一 MF + GraphQL 架构)
```mermaid
graph TB
@@ -16,26 +16,27 @@ graph TB
URL[URL 路由]
end
subgraph Shell["teacher-portal(Shell 宿主)"]
AppShell[AppShell<br/>左栏导航 + 主内容区]
subgraph Shell["teacher-portal(Shell 宿主 :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-portal(Remote)"]
subgraph RemoteStudent["student-portal(Remote,P3+)"]
StudentPages[学习场景页面<br/>dashboard/homework/submit/diagnostic/exam-taking]
end
subgraph RemoteParent["parent-portal(Remote)"]
subgraph RemoteParent["parent-portal(Remote,P4+)"]
ParentPages[家长场景页面<br/>dashboard/children-switch/grades/notifications]
end
subgraph RemoteAdmin["admin-portal(Remote)"]
subgraph RemoteAdmin["admin-portal(Remote,P6+)"]
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 /graphql(ARB-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-portal,ARB-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(左侧导航 + 主内容区 + 用户信息 + 登出 + 路由守卫)
- GraphQLProvider(urql client 单例,总裁 §2.17 方案 A,Remote 复用)
- 共享依赖暴露(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.js,ARB-002 §2.2 裁决)
```javascript
// teacher-portal/next.config.js(Shell)
// teacher-portal/next.config.js(Shell,ARB-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`,
});
// P2:remotes 为空(NEXT_PUBLIC_MF_ENABLED=false,ARB-002 §2.3),P3+ 逐步接入
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 GraphQL(F9 裁决)。
### 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: localStorage(P6 迁移 cookie)
viewports: ViewportItem[]; // L1 导航视口(GraphQL viewports Query)
permissions: string[]; // 权限点(GraphQL me Query 内嵌或 effective-permissions)
expiresAt: number; // access token 过期时间戳
}
```
存储:Zustand sessionSlice(L3)+ localStorage 持久化(刷新恢复)+ TanStack Query 缓存 `['session']`(L2)。
存储:Zustand sessionSlice(L3)+ localStorage 持久化(刷新恢复,F12)+ urql cache(L2,GraphQL document cache)。
### 2.2 视口模型(Viewport)
```typescript
// 对齐 ARB-001 §1.2 schema:ViewportItem
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; // 图标 key(schema 可空)
sortOrder: String; // 排序(schema 为 String)
requiredPermission: String | null; // 'CLASSES_READ' 等
}
```
来源:`GET /api/v1/{scope}/viewports`(BFF 聚合 iam 视口配置)。AppShell 按 `scope` 过滤渲染对应 portal 的导航。
来源:GraphQL `viewports` Query(ARB-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 | ALL(ARB-001 enum)
hasPermission: (perm: string) => boolean;
hasAnyPermission: (perms: string[]) => boolean;
hasAllPermissions: (perms: string[]) => boolean;
}
```
来源:`GET /api/v1/iam/effective-permissions` → `{ permissions, viewports, dataScope }`。Redis 缓存 5min(iam 侧),前端 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 | 失效策略 |
| ----------------------- | ---------------------- | --------------------------- | -------------------------------------- |
| Session(token + user) | localStorage + Zustand | access 15min / refresh 7day | 401 自动 refresh,refresh 失败跳登录 |
| 权限列表 | TanStack Query cache | 5min | 角色变更事件 invalidate |
| 视口列表 | TanStack Query cache | 5min | 同上 |
| 班级/年级列表 | TanStack Query cache | 5min | staleTime 5min,mutation 后 invalidate |
| 教学资源详情 | TanStack Query cache | 30s | staleTime 30s |
| 学情宽表 | TanStack Query cache | 30s | staleTime 30s(实时性由 BFF 决定) |
| URL 状态(分页/筛选) | nuqs | — | 永久(可分享) |
| 表单临时态 | react-hook-form | — | 卸载即销毁 |
| 数据类型 | 存储 | staleTime | 失效策略 |
| ----------------------- | ----------------------------- | ---------------------------------- | --------------------------------------------- |
| Session(token + user) | localStorage + Zustand(F12) | access 15min / refresh 7day | 401 自动 refresh,refresh 失败跳登录 |
| 权限列表 + 视口 | urql cacheExchange | 0(always 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 cache(03 §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 GraphQL(ARB-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.ts(Shell 暴露,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 注入给所有 Remote(ARB-002 方案 A)
- token 刷新/401 处理由 useAuth 统一拦截(不污染 urql exchange 链,见 [03 §2.7](./03-long-term-architecture.md))
- MF shared singleton(react/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 Query(ARB-001 §1.2,5 个 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+ Mutation(ARB-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 errors,ARB-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 Events(AI 流式)。
### 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 Subscription(P6+ 评估)
data: {"delta": "题目"}\n\n
data: {"delta": "A. option1"}\n\n
data: {"done": true}\n\n
```
前端用 `AsyncIterable<T>` 消费,Tiptap 逐字插入。
前端用 `AsyncIterable<T>` 消费(非 GraphQL,TanStack 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-ts,ai07 负责调用)。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 + 结构化;生产环境 → Sentry(P6)
// 必含字段: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` | 全局 Modal(ModalRoot + 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` | 全局 Modal(ModalRoot + 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. 共享 Hooks(packages/hooks/,待建立,ai07 维护)
## 8. 共享 Hooks(packages/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()` | 全局 toast(Zustand 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 场景(文件上传/SSE)ApiClient 实例 | — |
| `useA11yId()` | 唯一 ARIA ID 生成 | — |
| `useAriaLive()` | aria-live 区域管理 | — |
| `useToast()` | 全局 toast(Zustand 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 | HTTP(GraphQL 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 | GraphQL(ARB-001 schema) | `dashboard`/`viewports`/`me`/`classes`/`class` Query(P2)+ 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/useAuth(ARB-002 暴露) | P2+ |
| 依赖 | coord 维护 | — | `packages/contracts` | Permissions 常量 + 类型 | P2+ |
> **proto 不直接消费**:前端不调用 gRPC,BFF 把 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 提供上下文 | 优先 CSR,SSR 仅用于首屏 dashboard;MF 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 client(urql/apollo)?建议:P2-P3 用 REST,P4 起若 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/hooks;coord 维护 shared-ts/contracts | 总裁 §2.18(ISSUE-039) |
| GraphQL vs REST | **P2 起 all-in GraphQL(urql),无 REST 过渡** | F9 + ARB-001(ISSUE-036/037) |
| i18n key 命名 | `error.{{service}}.{{code_snake_case}}`,与错误码前缀对齐 | F11 |
| MF 暴露粒度 | 暴露 AppShell 整体 + GraphQLProvider + hooks + UI 组件(ARB-002 清单) | 总裁 §2.17 + ARB-002(ISSUE-038) |
| GraphQL client 单例 | Shell 暴露 GraphQLProvider,Remote 复用(方案 A) | 总裁 §2.17(ISSUE-038) |
| P2 Remote 数量 | 0(NEXT_PUBLIC_MF_ENABLED=false),P3 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(建议) | usePermission、useAuth、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 Query(ARB-001) | teacher-bff(ai03)| ⏳ 待 coord 仲裁 |
| `POST /api/auth/login` | api-gateway(ai01)→ iam | ⏳ 待 ai01 |
| classExams/classHomework/studentGrades Query + Mutation | teacher-bff(ai03 P3)| ⏳ 待 ai03 P3 |
| knowledgeGraph/studentAnalytics Query | teacher-bff(ai03 P4)| ⏳ 待 ai03 P4 |
| myNotifications/markAsRead Query + Mutation | teacher-bff(ai03 P5)| ⏳ 待 ai03 P5 |
| `ws://push-gateway:8081/ws` 推送 | push-gateway(ai02 P5)| ⏳ 待 ai02 P5 |
| `GET /api/v1/ai/generate-questions`(SSE) | ai(ai12 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 审计对齐)
### P2(MF 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. AppShell(Layout + 侧边栏 + 路由守卫 + ErrorBoundary + 设计令牌三层)
5. 登录页(`POST /api/auth/login` → JWT 存 localStorage,F12)
6. 权限上下文(usePermission + useViewports,权限点 `<RESOURCE>_<ACTION>` F7)
7. Dashboard 框架(`dashboard` GraphQL Query)+ 班级列表(`classes`/`class` Query)+ 学生列表 + 个人设置
8. 补 ErrorBoundary + /api/health route
9. 配置 next.config.js Module Federation(Shell 角色)
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 会话同步(BroadcastChannel,03 §2.5)
### P5(推送 + AI 接入)
1. teacher-portal 接入 WebSocket(push-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)
> 说明:本契约基于 GraphQL(ARB-001 admin 命名空间)+ 端口 4003 + ai16 归属,作为 01/02 文档修订基准(见 [objections/admin-portal_issue.md](../objections/admin-portal_issue.md) ISSUE-001~006)。
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 gRPC 接口
无。admin-portal 是前端微前端 Remote。
无。admin-portal 是前端微前端 Remote,不提供 gRPC。
### 1.2 HTTP 端点(如有)
### 1.2 前端路由(MF Remote 暴露给 Shell 动态加载)
| Method | Path | 用途 | 认证 |
| ------ | ----------- | ---------------- | --------------------- |
| GET | / | 管理后台首页 | JWT 必需 + admin 角色 |
| GET | /users | 用户管理 | JWT 必需 + admin |
| GET | /roles | 角色权限管理 | JWT 必需 + admin |
| GET | /classes | 班级管理(全局) | JWT 必需 + admin |
| GET | /teachers | 教师管理 | JWT 必需 + admin |
| GET | /students | 学生管理 | JWT 必需 + admin |
| GET | /audit-logs | 审计日志 | JWT 必需 + admin |
| GET | /dashboard | 管理员仪表盘 | JWT 必需 + admin |
| GET | /system | 系统配置 | JWT 必需 + admin |
> admin-portal 是前端,**不对外提供 HTTP 端点**。此处列出的是 admin-portal 在 Shell 路由树 `/admin/*` 下注册的前端页面路由,由 Shell 动态 import admin Remote 加载。
### 1.3 GraphQL schema(如 BFF)
| 路由 | 页面 | 权限点 | 数据范围 |
| ------------------- | ---------------- | ---------------------- | --------------- |
| `/admin/dashboard` | 管理员仪表盘 | `ADMIN_DASHBOARD_VIEW` | L3-L5 |
| `/admin/users` | 用户管理 | `IAM_USER_READ` | L3-L5 |
| `/admin/roles` | 角色权限管理 | `IAM_ROLE_READ` | L3-L5 |
| `/admin/permissions`| 权限点管理 | `IAM_PERMISSION_READ` | L5 |
| `/admin/viewports` | 视口配置 | `IAM_VIEWPORT_READ` | L5 |
| `/admin/organization` | 组织管理 | `ORG_MANAGE` | L3-L5 |
| `/admin/system` | 学校设置 | `ADMIN_SYSTEM_MANAGE` | L5 |
| `/admin/classes` | 班级管理(全局) | `ADMIN_CLASS_READ` | L3-L5 |
| `/admin/teachers` | 教师管理 | `ADMIN_TEACHER_READ` | L3-L5 |
| `/admin/students` | 学生管理 | `ADMIN_STUDENT_READ` | L3-L5 |
| `/admin/audit-logs` | 审计日志 | `ADMIN_AUDIT_READ` | L5 |
不适用。admin-portal 消费 teacher-bff GraphQL admin namespace,自身不提供 schema。
> 权限点使用 `ADMIN_` / `IAM_` / `ORG_` 前缀(ai-allocation §5:admin 权限点 `ADMIN_` 前缀)。`ADMIN_*` 常量由 coord 维护于 `packages/contracts/src/permissions.ts`,前端不硬编码。
### 1.4 Kafka 事件发布(如有)
### 1.3 GraphQL schema
无。
不适用。admin-portal **消费** teacher-bff GraphQL admin 命名空间,自身不提供 schema。
### 1.4 Kafka 事件发布
无。前端不发布 Kafka 事件。
### 1.5 错误码前缀
无(前端不定义错误码前缀,透传 BFF 错误码)。
无(前端不定义错误码前缀,透传 BFF/网关错误码)。消费的错误码前缀见 §2.5。
### 1.6 微前端架构(补充)
### 1.6 微前端架构
| 角色 | 说明 |
| ---------------------- | ----------------------------------------------- |
| MF Remote | 管理后台是微前端远程模块 |
| 暴露的 remote 模块 | AdminApp(管理后台完整应用)、shared 管理端组件 |
| module federation 配置 | `apps/admin-portal/module-federation.config.ts` |
| 角色 | 说明 |
| ---- | ---- |
| MF Remote | admin-portal 是微前端远程模块,挂载到 teacher-portal Shell |
| Remote 名称 | `admin_app`(Shell `remotes` 中引用为 `admin: 'admin_app@http://localhost:4003/_next/static/chunks/remoteEntry.js'`) |
| 暴露模块 | `./AdminApp`(管理后台应用入口,供 Shell 动态 import) |
| MF 配置位置 | `apps/admin-portal/next.config.js`(NextFederationPlugin,对齐 ARB-002 §2.2 配置风格) |
| shared(singleton) | react / react-dom / urql / graphql / @edu/ui-tokens / @edu/ui-components / @edu/hooks / @edu/contracts |
| 复用 Shell 暴露 | `GraphQLProvider` / `AppShell` / `useAuth` / `usePermission` / `useGraphQLClient` / `ErrorBoundary` / `RequirePermission` 等(ARB-002 §2.2) |
> admin-portal **不暴露共享组件给 Shell**(共享组件由 Shell 统一暴露,对齐 ARB-002)。admin 特有组件(UserManagementTable / RolePermissionMatrix / ViewportConfigEditor)仅 admin-portal 内部使用。
### 1.7 i18n 命名空间
| 命名空间 | 用途 |
| -------- | ---- |
| `admin.*` | 管理端 UI 文案(导航/按钮/表单标签等) |
| `iam.error.*` | iam 服务错误码 i18n(透传) |
| `bff.error.*` | teacher-bff 错误码 i18n(透传) |
| `gateway.error.*` | api-gateway 错误码 i18n(透传) |
| `network.error.*` | 前端网络层错误 i18n |
---
@@ -55,28 +77,49 @@
### 2.2 Kafka 事件订阅(异步)
无。前端不直接订阅 Kafka(审计日志通过 GraphQL 查询,非直接订阅)。
无。前端不直接订阅 Kafka。审计日志经 teacher-bff 聚合后通过 GraphQL `auditLogs` Query 消费(链路:iam → Kafka `edu.iam.audit.created` → **teacher-bff 消费** → GraphQL → admin-portal)。
### 2.3 HTTP 调用(如有)
> 注:[matrix.md §4](../matrix.md) 将 admin-portal 列为 `edu.iam.audit.created` 消费方,表述不精确,已提请 coord 修正为 teacher-bff(见 [objections ISSUE-007](../objections/admin-portal_issue.md))。
| 被调用方 | Method.Path | 用途 | mock 策略 |
| ------------------- | ----------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------- |
| api-gateway (ai01) | POST /api/admin/graphql | 管理 GraphQL 查询(经网关代理到 teacher-bff admin namespace) | api-gateway/teacher-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| api-gateway (ai01) | POST /api/auth/login | 管理员登录 | api-gateway 就绪前使用 MSW 返回固定 JWT(admin 角色) |
| push-gateway (ai02) | GET /ws | WebSocket 实时通知 | push-gateway 就绪前使用 mock-socket 模拟 WS 推送 |
### 2.3 HTTP 调用
### 2.4 GraphQL 查询域(经 api-gateway 代理到 teacher-bff admin namespace)
| 被调用方 | Method.Path | 用途 | mock 策略 |
| -------- | ----------- | ---- | --------- |
| api-gateway (ai01) | POST /api/admin/graphql | 管理 GraphQL 查询(经网关代理到 teacher-bff admin 命名空间) | api-gateway/teacher-bff 就绪前使用 MSW 拦截返回 mock GraphQL 响应 |
| push-gateway (ai02) | GET /ws | WebSocket 实时通知(审计告警/异常登录/系统异常) | push-gateway 就绪前使用 mock-socket 模拟 WS 推送(**ISSUE-006 待 coord 仲裁**) |
| Query/Mutation | 用途 | mock 策略 |
| ------------------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------- |
| currentUser | 当前管理员信息 | MSW 返回固定管理员(admin 角色) |
| adminUsers / createUser / updateUser / deleteUser | 用户管理 | MSW 返回固定 50 个用户 + CRUD success |
| adminRoles / updateRolePermissions | 角色权限管理 | MSW 返回固定 5 个角色 + 权限矩阵 |
| adminClasses | 班级管理(全局) | MSW 返回固定 20 个班级 |
| adminTeachers | 教师管理 | MSW 返回固定 50 个教师 |
| adminStudents | 学生管理 | MSW 返回固定 1200 个学生 |
| auditLogs | 审计日志(聚合 iam AuditEvent) | MSW 返回固定 100 条审计日志 |
| adminDashboard | 管理员仪表盘 | MSW 返回固定仪表盘(total_teachers=50, total_students=1200, school_avg_score=80.0) |
> **登录不自行实现**:admin-portal 复用 Shell 统一登录入口 `/login`(ARB-002 §2.3:登录页 P2 不走 MF,Shell 独占)。管理员登录后按 admin 角色重定向到 `/admin/dashboard`。开发期 mock 登录由 Shell 的 MSW handler 提供(admin 角色 JWT + permissions=["*"])。
### 2.4 GraphQL 查询域(经 api-gateway 代理到 teacher-bff admin 命名空间)
> 依赖 teacher-bff admin 命名空间 schema(ARB-001,**ISSUE-005 待 ai03 补齐**)。以下为 admin-portal 消费的 Query/Mutation 清单,作为 ai03 补齐 schema 的输入。
| Query/Mutation | 类型 | 用途 | mock 策略 |
| -------------- | ---- | ---- | --------- |
| `currentUser` | Query | 当前管理员信息 | MSW 返回固定管理员(admin 角色) |
| `adminUsers` | Query | 用户列表(含筛选/分页) | MSW 返回固定 50 个用户 |
| `adminUser(id)` | Query | 用户详情 | MSW 返回对应用户 |
| `createUser` / `updateUser` / `deleteUser` / `toggleUserStatus` | Mutation | 用户 CRUD | MSW 返回 CRUD success |
| `adminRoles` | Query | 角色列表(含权限) | MSW 返回固定 5 个角色 + 权限矩阵 |
| `createRole` / `updateRolePermissions` | Mutation | 角色 CRUD + 权限矩阵 | MSW 返回 success |
| `adminPermissions` | Query | 全量权限点(按 resource 分组) | MSW 返回固定权限矩阵 |
| `adminViewports(scope)` | Query | 视口配置列表 | MSW 返回固定 7 个视口 |
| `updateViewport` | Mutation | 视口配置更新 | MSW 返回 success |
| `adminOrganization(parentId)` | Query | 组织树 | MSW 返回固定 school/grade/class 树 |
| `adminClasses` | Query | 班级管理(全局) | MSW 返回固定 20 个班级 |
| `adminTeachers` | Query | 教师管理 | MSW 返回固定 50 个教师 |
| `adminStudents` | Query | 学生管理 | MSW 返回固定 1200 个学生 |
| `auditLogs(filter)` | Query | 审计日志(聚合 iam AuditEvent) | MSW 返回固定 100 条审计日志 |
| `adminDashboard` | Query | 管理员仪表盘聚合 | MSW 返回固定仪表盘(total_teachers=50, total_students=1200, school_avg_score=80.0) |
### 2.5 消费的错误码前缀(前端 i18n 路由)
| 前缀 | 来源服务 | i18n key 模式 |
| ---- | -------- | ------------- |
| `IAM_` | iam | `iam.error.{{code}}` |
| `BFF_TEACHER_` | teacher-bff | `bff.error.{{code}}` |
| `GW_` | api-gateway | `gateway.error.{{code}}` |
| `NETWORK_` | 前端网络层 | `network.error.{{code}}` |
---
@@ -85,18 +128,21 @@
### 3.1 我依赖的上游就绪标志
- [ ] api-gateway HTTP :8080 启用(ai01)—— 前端请求入口 + admin 角色校验
- [ ] teacher-bff GraphQL :3003 启用(ai03)—— admin namespace 可用
- [ ] teacher-portal Shell MF exposes/shared 就绪(ai13,ARB-002)—— Remote 挂载前提
- [ ] teacher-bff GraphQL :3003 启用 + **admin 命名空间 schema 就绪**(ai03)—— **ISSUE-005 待 ai03 补齐**
- [ ] iam gRPC 50052 启用(ai06)—— 用户/角色/审计日志数据来源
- [ ] edu.iam.audit.created topic 有事件发布(ai06)—— 审计日志来源
- [ ] data-ana gRPC 50055 启用(ai11)—— adminDashboard 数据来源
- [ ] push-gateway WebSocket :8081/ws 启用(ai02)—— 实时通知
- [ ] `edu.iam.audit.created` topic 有事件发布(ai06)—— 审计日志来源(经 teacher-bff 消费)
- [ ] push-gateway WebSocket :8081/ws 启用(ai02)—— 实时通知(**ISSUE-006 待 coord 仲裁**)
- [ ] `packages/contracts` admin 权限点 `ADMIN_*` 常量就绪(coord)
> 注:adminDashboard 的数据聚合由 teacher-bff 完成(teacher-bff 内部聚合 iam + core-edu + data-ana)。admin-portal **不直连 data-ana / core-edu gRPC**,统一经 teacher-bff GraphQL。原 contract 误列 data-ana gRPC 50055 为直接依赖,已修正。
### 3.2 我的就绪标志(供下游消费)
- [ ] admin-portal dev server :4003 启用
- [ ] MF Remote 可被 AppShell 加载(暴露 AdminApp 模块)
- [ ] MF Remote 可被 AppShell 加载(暴露 `./AdminApp` 模块)
- [ ] 独立壳渲染(首页 + 导航 + 路由守卫 + admin 角色校验)
- [ ] 登录流程可用(POST /api/auth/login 获取 JWT,前端校验 admin 角色)
- [ ] 登录流程可用(复用 Shell `/login`,admin 角色校验后重定向 `/admin/dashboard`)
- [ ] GraphQL 查询可执行(currentUser / adminDashboard / auditLogs 返回数据)
- [ ] 用户/角色 CRUD 可执行(createUser / updateRolePermissions)
- [ ] WebSocket 通知可接收
@@ -117,13 +163,25 @@ admin-portal 是前端,无下游消费方。但对开发体验提供:
在真实上游就绪前,admin-portal 使用以下 mock:
- **HTTP/GraphQL mock**:使用 MSW 拦截所有请求
- POST /api/auth/login → 返回固定 JWT + UserInfo(admin 角色,permissions=["*"])
- POST /api/admin/graphql → 根据 operationName 返回对应 mock 响应(与 teacher-bff admin namespace mock 数据一致)
- POST /api/admin/graphql → 按 operationName 返回对应 mock 响应(与 teacher-bff admin namespace mock 数据一致)
- auditLogs mock 返回固定 100 条审计日志(含 action: create/update/delete/login/logout/permission_change)
- adminDashboard mock 返回固定全校统计仪表盘
- 所有 mock 响应定义在 `apps/admin-portal/src/mocks/fixtures/*.json`
- **WebSocket mock**:使用 mock-socket 库
- 连接后每 30 秒推送 1 条 mock 系统通知
- **JWT mock**:使用固定 mock JWT(admin 角色),存入 httpOnly cookie
- 连接后每 30 秒推送 1 条 mock 系统通知(审计告警/异常登录)
- **JWT mock**:使用固定 mock JWT(admin 角色,permissions=["*"]),由 Shell MSW handler 写入 httpOnly cookie(复用 Shell 登录 mock)
- **权限矩阵 mock**:内置固定 5 个角色 + 完整权限矩阵(teacher/student/parent/admin/super_admin)
- **环境切换**:通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制,上游就绪后设为 `disabled`
---
## §5 跨模块契约确认清单(需对应 AI 确认)
| 契约 | 提供方 | 当前状态 |
| ---- | ------ | -------- |
| teacher-bff admin 命名空间 schema(§2.4 全部 Query/Mutation) | ai03 | ⚠️ 待补齐(ISSUE-005) |
| Shell 暴露 GraphQLProvider / useGraphQLClient / AppShell / useAuth / usePermission | ai13 | ⏳ P2 交付(ARB-002) |
| iam 用户/角色/权限/视口 CRUD gRPC + AuditEvent Kafka | ai06 | ⏳ P2.1 |
| api-gateway /api/admin/graphql 代理路由 + admin 角色校验 | ai01 | ⏳ |
| push-gateway GET /ws(admin-portal 实时通知) | ai02 | ⚠️ 待 coord 仲裁(ISSUE-006) |
| `packages/contracts` ADMIN_* 权限点常量 | coord | ⏳ |

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

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

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

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

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

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

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

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

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

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

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

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

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

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 存入 localStorage,F12;P6 迁移 httpOnly cookie)
- [ ] GraphQL 查询可执行(dashboard / viewports / me / classes / class Query,ARB-001)
- [ ] WebSocket 通知可接收(push-gateway 推送 → 前端通知中心更新)
---
@@ -118,9 +118,9 @@ teacher-portal 是最前端,无下游消费方。但对开发体验提供:
- **HTTP/GraphQL mock**:使用 MSW(Mock Service Worker)拦截所有请求
- POST /api/auth/login → 返回固定 JWT + UserInfo
- POST /api/teacher/graphql → 根据 operationName 返回对应 mock 响应(与 teacher-bff mock 数据一致)
- POST /api/v1/teacher/graphql → 根据 operationName 返回对应 mock 响应(与 teacher-bff mock 数据一致,对齐 §2.3)
- 所有 mock 响应定义在 `apps/teacher-portal/src/mocks/fixtures/*.json`
- **WebSocket mock**:使用 mock-socket 库
- 连接 ws://localhost:8081/ws 后每 30 秒推送 1 条 mock 通知
- **JWT mock**:使用固定 mock JWT(与 api-gateway mock 公钥配对),存入 httpOnly cookie
- **JWT mock**:使用固定 mock JWT(与 api-gateway mock 公钥配对),存入 localStorage(F12;P6 迁移 httpOnly cookie)
- **环境切换**:通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制是否启用 MSW,上游就绪后设为 `disabled`

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-001(teacher-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 的归属与时间点——是否由 ai03(teacher-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-002(MF 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 | 同类 portal(parent-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-ai16:01/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) 头部均标注"AI:ai07(TS/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 标识与分支命名更正为 ai16;ai07 在 admin-portal 的产出视为历史草稿,由 ai16 接管修订。
- **状态**:待 coord 仲裁
- **提请方**:aiXX
- **日期**:YYYY-MM-DD
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**:[详细描述问题]
- **建议方案**:[AI 的建议]
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
-->
### ISSUE-002-ai16:01/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-ai16:01/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/002(2026-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-ai16:01/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-ai16:teacher-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-ai16:01/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-ai16:matrix.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-001(coord.md §1) | teacher-bff GraphQL schema 第一版 | ❌ 否 | 与 ai 无关,不适用。 | — |
| ARB-002(coord.md §2) | MF Shell 暴露清单 | ❌ 否 | 与 ai 无关,不适用。 | — |
| port-allocation.md §7 | 50058 让给 ai(push-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.md(50058 已定)与 004 §2.3(Temporal 部分仲裁)。ai 文档对"004 §7.2/§1.2"的两处引用失实,需修正引用源。
---
## 问题列表
### ISSUE-01-ai12:matrix.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 50058(2026-07-09 coord 仲裁"push-gateway 豁免 gRPC,释放 50057;50058 让给 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-ai12:ai 用量事件 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` message,004 无 topic 记录。三义导致 data-ana 消费方无法对齐。
- **建议方案**:ai12 建议采用 `edu.ai.usage`(与 matrix.md 一致,最简短,符合 `edu.<domain>.<action>` 简洁约定)。请 coord 裁定并在 004 §7.2 补登 + events.proto 补 `AIUsageEvent` message(schema 见 02-architecture-design.md §3.3)。ai12 据裁决同步 01/02 文档与 contract。
- **状态**:待 coord 仲裁
### ISSUE-03-ai12:ai.proto 待补全(备课工作流 RPC + 字段扩展)
- **提请方**:ai12
- **日期**:2026-07-10
- **类型**:契约不明确 / 前置依赖缺失
- **描述**:ai.proto 现状仅 4 RPC(Chat / 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 RPC;02-architecture-design.md §4.2 单方面扩到 8 RPC(追加 GetLessonPlanStatus / ConfirmLessonPlan)。需 coord 裁定 P5 目标 RPC 清单。
- **建议方案**:ai12 建议 P5 目标 6 RPC(Chat / 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-ai12:events.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 文档已提请(A3),matrix.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-ai12:contracts/ai_contract.md 与设计文档多处矛盾(ai12 自查自纠清单)
- **提请方**:ai12
- **日期**:2026-07-10
- **类型**:契约不明确 / 文档不同步
- **描述**:现有 contracts/ai_contract.md(coord 模板,ai12 接管前未细化)与 01/02 设计文档存在 5 处矛盾:
1. §1.1 gRPC 端口 50057(应为 50058,见 ISSUE-01)
2. §1.2 "无对外 HTTP 端点,仅 gRPC"——错误。api-gateway 代理 `/api/v1/ai/*` → ai HTTP(main.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:备课工作流是否引入 Temporal(P6 决策点)
- **提请方**:ai12
- **日期**:2026-07-10
- **类型**:工作量超批 / 前置依赖缺失
- **描述**:004 §2.3 列出 Temporal 用于"AI 编排",属部分仲裁。ai12 在 02-architecture-design.md §8.3 建议:P5 用 FastAPI BackgroundTasks + Redis(24h TTL)实现 4 步备课工作流;P6 评估迁移 Temporal(支持长运行跨天审核 + 复杂状态机)。需 coord 在 P6 决策点确认是否引入,避免 P5 实现被推翻重做。
- **建议方案**:P5 采用 BackgroundTasks + Redis(02-architecture-design.md §2.4 状态机已设计);P6 由 coord 评估 Temporal 引入时机。请 coord 在 roadmap 标注 P6 决策点。
- **状态**:待 coord 仲裁(P6 决策点)
### ISSUE-07-ai12:iam GetEffectiveDataScope gRPC RPC 待 P4 补全
- **提请方**:ai12
- **日期**:2026-07-10
- **类型**:前置依赖缺失
- **描述**:ai 多租户用量配额校验需调用 `IamService.GetEffectiveDataScope` 查询用户 DataScope。01/02 文档已提请(A4),coord 已仲裁 P4 补全(§15.3 #5),但 iam.proto 现状未见此 RPC。ai P5 实施时依赖,若 P4 未补全将阻塞配额校验功能。
- **建议方案**:请 coord 确认 iam (ai06) 在 P4 已补全 `GetEffectiveDataScope` RPC;ai12 在 P5 实施时调用,Redis 缓存 5min。若 P4 未补全,ai 降级为"仅按 user_id 配额,不按 school_id"。
- **状态**:待 coord 确认 P4 补全情况
### ISSUE-08-ai12:proto 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 文档已提请(A5),contracts 未记录。P5 实施必须整改。
- **建议方案**:P5 实施时所有 HTTP 端点 + gRPC RPC 返回值改为 ActionState;degraded 作为 `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 必须引入 Outbox(004 §12.2 强制条款) | §3.1.5 content_outbox_events 表 + §5.4 Outbox Publisher | ✅ 已落实 |
| C4 | P4 必须实现 gRPC controller | §4.2 gRPC API(4 个 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-ai09:REST 端点设计文档与现有实现不一致
- **提请方**: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-ai09:content 发布事件 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-ai09:Textbook/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-ai09:gRPC 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 | 3(Create/List/Get) | 4(含 Update,无 Delete) | — |
| KnowledgeGraphService | 4 | 4 | — |
| QuestionService | 6(无 Publish/Search) | 7(含 Publish/Search) | — |
| **合计** | **18** | **18** | **18** |
- design doc 缺 QuestionService.PublishQuestion / SearchQuestions(contract 有)
- contract 缺 TextbookService.Update/Delete(design doc 有)
- design doc ChapterService 缺 Update(contract 有);contract ChapterService 缺 Delete(design doc 也缺)
- **建议方案**:以 contract.md 为契约唯一源(已对齐 matrix.md 18 RPC 总数),反向修正 design doc:
1. TextbookService 补 Update/Delete(与 contract 对齐)
2. ChapterService 补 Update + Delete(design 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-ai09:core-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.3:AI 禁止 `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-ai09:questions 表 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-ai09:knowledge-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-ai09:Neo4j 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-ai08:core_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 RPC:GetClass / GetClassesByTeacher / BatchGetClasses / ListStudentsByClass)
- `AttendanceService`(4 RPC:RecordAttendance / 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 RPC(5+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-ai08:events.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 message(ClassEvent/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.md(core-edu) | coord.md §1.2 GraphQL schema(teacher-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` |
| SubmissionStatus(exam) | `in_progress / submitted / graded` | `NOT_SUBMITTED / SUBMITTED / GRADED` | core-edu 用 `in_progress`,coord 用 `NOT_SUBMITTED` |
| SubmissionStatus(homework) | `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-ai08:class.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`(ClassEvent,action: 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-ai08:core-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 8(5+3 新增)+ HomeworkService 5(4+1 新增)+ GradeService 6(5+1 新增)+ AttendanceService 4 = **27 RPC**。
- **影响**:
- matrix.md §2 声明 22 RPC 但含 ClassService,02 文档声明 22 RPC 但不含 ClassService,下游 AI 无法判断应实现多少 RPC
- 就绪信号"core-edu gRPC 50053 + 22 RPC"含义模糊
- **建议方案**:
coord 统一 RPC 统计口径:
- matrix.md §2 改为 "27 RPC(P3 全量: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-ai08:core-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-ai08:core-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` RPC,P4 补全,data-ana gRPC 调用 | iam | ⚠️ **未落实**:iam.proto 当前仅 4 RPC(Register/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 message(Class/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 服务信封改为 ActionState(degraded 放 details 子字段) | ai11 | ✅ **已对齐**:02 §4.3 ActionState 实现已修正,degraded 移至顶层 details |
| 7 | §6 #4 | 同 #6,ai06 修正 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.5,01/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 直接相关的 proto(iam/analytics/events)。§8.2 声明与实际文件严重不符,可能原因:(a) 声明为计划态但未执行;(b) 执行后未提交到本 worktree 分支;(c) 在其他分支已执行但未合并。无论哪种原因,data-ana 的 P4 实现依赖这些 proto 补全,当前实际状态构成 P4 阻塞。
---
## §1 问题列表
### ISSUE-001-ai11:iam.proto 缺 GetEffectiveDataScope RPC(P4 阻塞)
- **提请方**:ai11
- **日期**:2026-07-10
- **类型**:前置依赖缺失
- **描述**:coord-cross-review.md §2.2 #3 已仲裁"iam P4 补全 `GetEffectiveDataScope` RPC,data-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-ai11:events.proto 缺 AIUsageEvent message(P5 阻塞,P4 预备)
- **提请方**:ai11
- **日期**:2026-07-10
- **类型**:前置依赖缺失
- **描述**:coord-cross-review.md §3.2 已仲裁"补登 `edu.insight.ai.usage` topic + events.proto 补 `AIUsageEvent` message",但 events.proto 当前仅 4 message(ClassEvent/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-ai11:analytics.proto 仅 3 RPC,coord-cross-review §8.2 声称已扩展至 12 RPC 但实际未落实(P4 阻塞)
- **提请方**:ai11
- **日期**:2026-07-10
- **类型**:前置依赖缺失 + 声明与实际不符
- **描述**:coord-cross-review.md §8.2 声称"analytics.proto 扩展 ✅ 12 RPC(含 Stream)",但实际文件仅 3 RPC(GetClassPerformance/GetStudentWeakness/GetLearningTrend)。ai-allocation.md §5 与 matrix.md §2 均要求 data-ana 提供 12 RPC,02-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-ai11:coord-cross-review §6 整改清单 coord 责任项未落实,导致 004 章节引用断裂
- **提请方**:ai11
- **日期**:2026-07-10
- **类型**:契约不明确
- **描述**:coord-cross-review.md §6 整改清单中标注"coord"责任的 4 项整改未在 004 正文中落实:
- #7:004 §7.2 补登 6 个 topic + CDC 命名规范 → 004 正文无对应段落
- #8:004 §1.2 新增 HTTP/gRPC 端口两列 → 004 §1.2 服务清单无端口列
- #9:004 §4.1 补充 gRPC 启用阶段矩阵 → 004 正文无 §4.2 子节
- #10:004 §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-ai11:data-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-ai11:coord-cross-review §8.2 批次 0 产出声明与 proto 实际文件状态严重不符
- **提请方**:ai11
- **日期**:2026-07-10
- **类型**:其他(声明与实际不符)
- **描述**:coord-cross-review.md §8.2 声称批次 0 已完成 iam.proto(12 RPC)/ analytics.proto(12 RPC)/ events.proto(9 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-I8(iam 专项)核查
| 裁决 | 内容摘要 | 核查结论 | 证据 |
| ---- | -------- | -------- | ---- |
| I1 | P2 即启用 gRPC server 50052,REST + gRPC 并存 | ⚠️ **02 文档未回写**:[02-architecture-design.md](../../../services/iam/docs/02-architecture-design.md) §1/§7.1/§8.1 决策点 5 仍写"P2 仅 REST,P3 随 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` map(admin/teacher);02 文档 §8.1 决策点 9 描述为"待改造" | [permission.guard.ts](../../../services/iam/src/middleware/permission.guard.ts) |
| I4 | 首次实现即注册 AuthMiddleware,Controller 用 @Req() 注入 | ❌ **源码未注册**:[app.module.ts](../../../services/iam/src/app.module.ts) 仅注册 PermissionGuard 为 APP_GUARD,未在 configure() 消费 AuthMiddleware;02 文档 §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.1(8 RPC 核心)+ P2.2(扩展) | ⚠️ workline.md 仅粗略列出 P2.1,P2.2-P6 未细化(见 worklines/iam_workline.md) |
| §5.5 | 审计日志归 iam(AuditEvent + 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/AuditEvent(coord-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-ai06:01/02 文档未回写 I1-I8 裁决,中间过渡方案残留
- **提请方**:ai06
- **日期**:2026-07-10
- **类型**:契约不明确 / 其他
- **描述**:coord-final-decisions §0.1 强制覆盖声明要求各 AI 在 3 个工作日内回写 02 文档,删除所有"中间过渡方案"。president-final-rulings §3.4 也明确要求 ai06 回写 iam 02(I1-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 决策点 5:P2 仅 REST → P3 gRPC(违反 I1)
- 02 §8.1 决策点 4:iam 自建 Outbox(违反 I2)
- 02 §8.1 决策点 9:PermissionGuard 待改造(违反 I3)
- 02 §1.1/§1.2/§8.1 决策点 10:AuthMiddleware P2 不注册(违反 I4)
- 02 §3.1.2:表名 iam_parent_student_relations(违反 I6)
- 02 §4.1:API 路径无 /v1 前缀(违反 I7)
- 01 §1:teacher-bff HTTP 调用 iam(违反 B2,应 gRPC)
- **建议方案**:ai06 立即回写 01/02 文档,删除全部中间过渡方案描述,对齐 I1-I8 + §2.15/§2.16/§5.5 最终方案。
- **状态**:待 coord 仲裁(确认回写范围与验收标准)
### ISSUE-002-ai06:events.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-ai06:Topic 命名三方不一致(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,非 identity),coord 同步修正 004 §7.2 的 `edu.identity.*` → `edu.iam.*`。同时明确 Topic 粒度:matrix.md 用聚合 topic(`edu.iam.user.events` 含多 action),004 §7.2 用具体动作 topic(`edu.iam.user.created`),需统一为一种风格。
- **状态**:待 coord 仲裁
### ISSUE-004-ai06:DataScope 枚举三方不一致
- **提请方**:ai06
- **日期**:2026-07-10
- **类型**:契约不明确
- **描述**:DataScope 6 级枚举存在三方不一致:
- [004 §5.3 表](../../../docs/architecture/004_architecture_impact_map.md) 行 463-470:SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL
- [004 §5.3](../../../docs/architecture/004_architecture_impact_map.md) 行 505:all/grade_managed/class_taught/children/owned + 自定义(语义命名,与表不同)
- president §3.2 P2.2:ALL/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-ai06:iam.proto 仅 4 RPC,未补全至 12 RPC
- **提请方**:ai06
- **日期**:2026-07-10
- **类型**:前置依赖缺失
- **描述**:coord-final-decisions §5.1 要求 iam.proto 在"P2 启动前"补全 8 个新 RPC(GetViewports / GetEffectivePermissions / GetEffectiveAccess / Logout / GetPublicKey / BatchGetUsers / GetEffectiveDataScope / GetChildrenByParent),president §6 批次 0 任务 0.3 也明确"coord 补全 iam.proto 8 RPC"。但实际 [iam.proto](../../../packages/shared-proto/proto/iam.proto) 仍仅 4 RPC(Register/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-ai06:01/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) 署名"AI:ai02(TS / 身份认证)"、"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-gateway(coord-final-decisions §3.7)。文档署名错误会导致多 AI 协作时身份混淆。
- **建议方案**:回写时将 01/02 文档署名从 ai02 改为 ai06。
- **状态**:待 coord 仲裁
### ISSUE-007-ai06:contract.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-ai10:M8 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-ai10:M12 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-ai10:M2 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 必须实现 Outbox(P5 强制)
- **状态**:已裁决(核查通过)
### ISSUE-004-ai10:core-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-ai10:Push 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-ai10:events.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-ai10:proto 包名规则冲突(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-ai10:Kafka topic 命名三套约定并存
- **提请方**:ai10
- **日期**:2026-07-10
- **类型**:契约不明确
- **描述**:msg 相关的 Kafka topic 命名存在三套约定:
- **约定 A(004 §7.2,per-event topic)**:`edu.identity.user.created` / `edu.teaching.exam.published` / `edu.notification.sent`
- **约定 B(matrix.md §4 + events.proto 注释,aggregate topic)**:`edu.iam.user.events` / `edu.exam.events` / `edu.notification.requested`
- **约定 C(msg_contract.md §1.4,aggregate topic + action 字段)**:`edu.msg.notification.events`(action: sent/read/recalled/failed)
- 02-architecture-design.md §5.1/§5.2 采用约定 A;msg_contract.md §1.4 采用约定 C;matrix.md 采用约定 B。known-issues §全局 L182 已标记此冲突。
- **建议方案**:coord 统一为一套约定。ai10 倾向约定 A(per-event topic),理由:
- 004 §7.2 已采用,是架构设计意图唯一源
- per-event topic 便于消费者按需订阅,避免反序列化无关事件
- 与 NotificationSent / NotificationRead 等事件命名(PascalCase)对齐
- **状态**:待 coord 仲裁
### ISSUE-009-ai10:RPC 数量超预算(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 RPC(SendNotification / BatchSendNotification / ListNotifications / GetUnreadCount / MarkAsRead / BatchMarkAsRead / MarkAllAsRead / SearchNotifications / RecallNotification)
- NotificationPreferenceService 2 RPC(GetPreferences / UpdatePreferences)
- NotificationTemplateService 6 RPC(CreateTemplate / 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-ai10:markAsRead 权限点与设计不一致
- **提请方**: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 为准(READ),P5 实现时修正 controller 权限点
- **状态**:待 coord 确认
### ISSUE-011-ai10:DB→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 未覆盖:
- **三层幂等防线**(L344):L1 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-ai10:events.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-ai10:msg_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-ai10:msg_contract.md Kafka 发布事件与设计文档不一致
- **提请方**:ai10
- **日期**:2026-07-10
- **类型**:契约不明确
- **描述**:
- msg_contract.md §1.4:`edu.msg.notification.events` topic,单一 NotificationEvent 含 action 字段
- 02-architecture-design.md §5.2:4 个 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 豁免 gRPC,HTTP /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-ai05:01-understanding.md 未同步 ai05 接手与多项仲裁
- **提请方**:ai05
- **日期**:2026-07-10
- **类型**:文档同步缺失
- **描述**:01-understanding.md 头部仍标 "AI 标识:ai04"、"状态:待 coord 审核",未反映 ai05 已正式接手(ai-allocation.md §3.2)。同时以下仲裁已裁决但 01 未同步:
- U3(GraphQL P2 引入):01 §4.1 仍建议"P4 先对齐 teacher-bff REST 现状",与 U3 仲裁冲突
- U4(BFF 豁免 @RequirePermission):01 §6 表格"权限装饰器"行标"⚠️ 不对齐",未引用 U4 仲裁
- C1(错误码前缀 BFF_PARENT_):01 §3.3 仍用 `PARENT_BFF_` 旧前缀
- **建议方案**:01-understanding.md 头部更新为 ai05 复审版,同步 U3/U4/C1 仲裁结论;或由 coord 确认 01 作为"阶段 1 历史快照"保留原样,以 02 为准
- **状态**:待 coord 仲裁
### ISSUE-002-ai05:02 文档内部 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-ai05:contract.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-ai05:004 §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-ai05:parent-portal 01 文档与 parent-bff GraphQL 决策跨模块冲突
- **提请方**:ai05
- **日期**:2026-07-10
- **类型**:跨模块契约冲突
- **描述**:U3 仲裁决定 parent-bff P4 直接用 GraphQL(02 §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-bff(ai04 设计)",ai04 已过时(现 ai05)
此冲突若不解决,parent-portal(ai15)会按 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-ai05:proto 包名引用不一致(缺失 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-ai05:GraphQL 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-ai05:core_edu.proto 缺 ClassService,02 §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-ai05:ai-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 责任方为 "ai06(iam 现归属)" 但 §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-ai05:02 缺少 ADR / NFR / 容量规划 / 威胁建模(业界规范差距)
- **提请方**:ai05
- **日期**:2026-07-10
- **类型**:架构文档规范缺失
- **描述**:对照业界通用架构文档规范(如 C4 model + ADR + NFR),02-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-001(teacher-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.3(GraphQL 端点 :3010)和 [matrix.md](../matrix.md) §3(parent-bff GraphQL)直接冲突。提请 ISSUE-001。
### 0.2 ARB-002(MF Shell 暴露清单)核查
| 核查项 | ARB-002 结论 | parent-portal 落地情况 | 状态 |
| ------ | ------------ | ---------------------- | ---- |
| Shell 暴露 GraphQLProvider | ✅ 已裁决 | 01 §4 技术栈未列 urql/GraphQL client;02 §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-ai15:01/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-001(coord.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 用 REST,contract.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.1(X-Fields 字段裁剪改为 GraphQL query 字段选择)、02 §4.1(`useParentApi` 改为 GraphQL hooks)、§4.2(TanStack Query 约定配合 GraphQL operations)、§11.3 未决设计决策 #2(移除,已裁决)
3. 若 coord 另有裁决(如 parent-portal 特殊走 REST),以 coord 裁决为准
- **状态**:待 coord 仲裁
### ISSUE-002-ai15:MF 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-002(coord.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 client(urql),与 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-ai15:switch-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)的端点,因登录前无 JWT,GraphQL endpoint 需鉴权。请 coord 确认登录是否走 REST `/api/v1/iam/login`,其余走 GraphQL
- **状态**:待 coord 仲裁
### ISSUE-005-ai15:02 §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-001(coord.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-001,parent-portal 用 GraphQL 消费 parent-bff"
- **状态**:待 coord 仲裁
### ISSUE-006-ai15:contract.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-ai15:contract.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-ai15:contract.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-ai15:parent-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-001),parent-bff 需提供 `mutation switchChild(childId: ID!): SwitchChildPayload!`
- 但 parent-bff contract 未列此 Mutation,且 iam.GetChildrenByParent 已返回子女列表,切换子女是否需后端记录(还是纯前端 localStorage)需明确
- **建议方案**:
1. 请 coord 协调 ai05(parent-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-ai15:iam 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)—— 核心依赖 GetChildrenByParent(I3/ISSUE-047 裁决)"
- 此为 parent-portal 多子女场景的 P0 阻塞项,ai06(iam)需在 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-ai02:SSE 端点是否提供(跨文档三方冲突)
- **提请方**: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 明确建议"不支持 SSE,WebSocket 已够用,避免协议膨胀"
- 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 兜底)、ai10(msg 不需调 /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 隔离,共享密钥足够
- **影响方**:ai10(msg 调用方需用相同头名)、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-30KB,10w 连接约 2-3GB,单节点可承载
3. 50k 目标过于保守,与横向扩展方案不匹配
- **影响方**:coord(更新 modules/README.md §7)、ai02(02 §11 已正确)
- **状态**:待 coord 仲裁
### ISSUE-004-ai02:contract.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 定位,避免引入持久化层增加运维复杂度。
- **影响方**:ai10(msg 需确认审计字段是否覆盖 push-gateway 推送结果)、coord(澄清 ai-allocation §5 表述)
- **状态**:待 coord 仲裁
### ISSUE-006-ai02:ISSUE-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-ai02:02 文档缺 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/网络带宽估算缺失
- **建议方案**:在批次 4(P5)补全 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 02:B1 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-ai04(02 文档回写)。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-ai04:01-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-ai04:02-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 + B5;ActionState 信封规范应改为 [coord-final-decisions.md](../../coord-final-decisions.md) G8 + F9
- **建议方案**:02 文档回写时修正引用源;coord 确认是否需要在 004 补充 §11.4/§11.5 章节,或统一指向 coord-final-decisions
- **状态**:待 coord 仲裁
### ISSUE-STU-003-ai04:02 文档 §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 风格 | A(P3 REST) | **B1:P2 起直接 GraphQL** | coord-final-decisions §2 B1 |
| 2 | BFF 是否做权限校验 | A(不校验) | **B3:BFF 豁免 @RequirePermission** | coord-final-decisions §2 B3 |
| 3 | 自我越权防御 | B(做) | **B4:全部 BFF 强制自我越权防御** | coord-final-decisions §2 B4 |
| 4 | /readyz 检查逻辑 | A(P3 直接 ok) | **G2 + §2.4:按阶段扩展探针,必需依赖失败 503,可选依赖软失败** | president §2.4 |
| 5 | Kafka 事件订阅时机 | A(P3 不订阅) | **B7:P2-P4 不订阅 Kafka,P5 后订阅** | coord-final-decisions §2 B7 |
| 6 | 缓存策略 | B(Redis 5-30s) | **B6:Redis 5-30s 短缓存** | coord-final-decisions §2 B6 |
| 8 | 错误码前缀 | BFF_STUDENT_ | **B5:BFF_STUDENT_** | coord-final-decisions §2 B5 |
| 9 | DownstreamClient 回写 | B(回写) | **B8:回写 teacher-bff,3 个 BFF 统一** | coord-final-decisions §2 B8 |
仅剩 #7(端口 3009)、#10(CQRS 不引入)、#11(SSE 实现)、#12(熔断器引入)属合理的设计决策,但 #12 熔断器 president §2.4 已暗示按阶段评估。
- **建议方案**:02 文档回写时删除已裁决项的"待仲裁"标注,改为"已裁决(见 coord-final-decisions §2 BX)"
- **状态**:待 coord 确认(与 ISSUE-028-ai04 合并处理)
### ISSUE-STU-004-ai04:student-bff GraphQL schema 第一版尚未起草
- **提请方**:ai04
- **日期**:2026-07-10
- **类型**:前置依赖缺失
- **描述**:president §2.2 裁决"ai04 起草 student-bff schema(批次 1 等待期),coord 在批次 2 启动前仲裁第一版"。当前批次 1 已启动(2026-07-10),ai04 处于批次 1 等待期,但 schema 草案尚未产出。02-architecture-design.md 仍是 REST 设计,未定义 GraphQL Query/Mutation/Type。
- **影响**:阻塞 ai14(student-portal)P3 启动(依赖 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-ai04:ai04 02-architecture-design.md 与 coord B1+B2 裁决冲突需回写
- **提请方**:ai04(student-bff + parent-bff)
- **日期**:2026-07-09
- **类型**:裁决冲突(文档回写义务)
- **描述**:02-architecture-design.md 与 coord-final-decisions B1(P2 起直接 GraphQL)+ B2(首次实现即 gRPC)存在 2 项重大冲突(§4 API 设计决策为 REST;§9.2 演进路线 REST→GraphQL)
- **裁决**:president §3.4 — ai04 须在批次 2 启动前回写 student-bff 02:B1 GraphQL + B2 gRPC + B8 DownstreamClient
- **状态**:⚠️ 未执行(详见 §0 核查)
### ISSUE-029-ai04:ai04 P3 启动前置依赖确认(4 项强阻塞)
- **提请方**:ai04(student-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-ai04:ai04⟷ai14 student-bff GraphQL schema 第一版仲裁时机
- **提请方**:ai04(student-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-ai04:issues.md 编号冲突
- **提请方**:ai04(student-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-001(teacher-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 全量 Query(currentUser/myClasses/myExams/myHomework/myGrades/myAttendance/studentDashboard) | ✅ 已落实 |
| P2 Mutation 范围 | ⚠️ 部分参考。student-portal P3 起步即需要 submitHomework mutation(作业提交是 P3 核心场景) | ai14 P3 即消费 submitHomework mutation;ai04 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、复杂度限制)适用于所有 BFF,student-portal 已在 02-architecture-design.md v2 中全面落实。
---
### 1.2 ARB-002(MF Shell 暴露清单)对 student-portal 的影响核查
| 裁决点 | 对 student-portal 的适用性 | ai14 落实方案 | 状态 |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| Shell 身份 | ✅ 适用。teacher-portal 是 MF Shell,student-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 暴露 GraphQLProvider,student-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` 控制是否走 MF;P3 默认 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-ai14:GraphQL 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`
- 由 ai01(api-gateway)确认路由:`/api/v1/student/*` → `student-bff:3009/*`(即 `/api/v1/student/graphql` → `student-bff:3009/graphql`)
- 由 ai04(student-bff)确认 GraphQL endpoint 路径为 `POST /graphql`(与 teacher-bff 一致)
- **状态**:待 coord 仲裁
---
### ISSUE-014-02-ai14:student-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 类型
- 由 ai04(student-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?
- 这些决策影响 ai04(student-bff)是否需要提供 `recordExamViolation` mutation,以及 ai08(core-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` 事件拦截,但学生作答主观题时可能需要粘贴(如从草稿本粘贴长文本)
- 策略不明确:全部禁止粘贴?仅主观题允许?仅客观题禁止?
- 影响学生体验和防作弊效果平衡
- **建议方案**:
- **客观题**:禁止粘贴(防作弊优先)
- **主观题(简答/论述)**:允许粘贴,但记录粘贴事件 + 粘贴内容长度,教师端批改时可见
- **作文题**:允许粘贴(学生体验优先),不记录
- 由 ai04(student-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 multipart(graphql-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 鉴权
- 由 ai01(api-gateway)确认是否提供 `/api/v1/student/upload` 路由,由 ai04(student-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`?由 ai08(core-edu)发布?
2. **题目重排**(教师重排题目顺序):事件名 `ExamQuestionReordered`?是否需要前端实时重排?
3. **考试强制提交**(教师强制收卷):事件名 `ExamForceSubmitted`?前端收到后立即提交?
- 这些事件影响 student-portal 考试作答页的实时响应逻辑
- **建议方案**:
- **考试延长**:ai08(core-edu)发布 `ExamExtended` 事件到 `edu.exam.events` topic,msg(ai10)消费后通过 push-gateway 推送,student-portal 收到后更新倒计时
- **题目重排**:P3 不实现(题目顺序固定),P4 评估是否需要实时重排
- **考试强制提交**:ai08 发布 `ExamForceSubmitted` 事件,student-portal 收到后立即触发提交流程
- 由 ai08(core-edu)确认事件命名,由 ai10(msg)确认推送路径
- **状态**:待 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)
- 由 ai04(student-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-ai03:admin 命名空间 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.1(ISSUE-044)裁决 "ai03 P2 预留 admin schema 命名空间(如 `admin.*` Query/Mutation)",且 §7.3 ai03 工作清单明确 "批次 1(P2):Must Have 13 项 + DownstreamClient 抽象 + admin schema 命名空间预留"。两处对 P2 admin 命名空间的要求不一致——总裁裁决要求 P2 预留(schema 中声明占位),coord 仲裁说 P2 不包含。
- **建议方案**:以总裁裁决为准(裁决优先级 president > coord),修正 ARB-001 §1.3 为 "P2 预留 admin 命名空间占位(schema 中声明 `admin` Query/Mutation 类型骨架,无实际 Resolver),P6 admin-portal 阶段实现具体 Resolver"。ai03 在 P2 schema 第一版中预留 `admin` 命名空间类型声明。
- **状态**:待 coord 仲裁
### ISSUE-002-ai03:P2 dashboard classes 数据来源与 ARB-001 §1.4 调用链冲突
- **提请方**:ai03
- **日期**:2026-07-10
- **类型**:前置依赖缺失
- **描述**:coord.md ARB-001 §1.4 ai03 执行项第 4 条写 "dashboard Resolver 并行调用 iam(3 RPC)+ classes(1 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 无 gRPC,teacher-bff 不能用 REST 调 classes(B2 禁止 REST 过渡)。
3. **president §3.5 功能范围**:president-final-rulings.md §3.5 裁决 P2 实现 "班级列表(iam 数据)",即 P2 班级列表数据来自 iam,不是 classes 服务。
- **建议方案**:采纳 president §3.5,P2 dashboard 的 classes 数据从 iam gRPC 获取(iam `GetEffectiveAccess` 或 `GetViewports` 返回的关联班级),不调 classes 服务。ARB-001 §1.4 第 4 条修正为 "dashboard Resolver 并行调用 iam gRPC(GetUserInfo + GetViewports + GetEffectiveAccess),classes 列表从 iam 返回数据推导,P3 core-edu gRPC 就绪后切换为 `GetClassesByTeacher` RPC"。这样 P2 不依赖 classes gRPC,与 B2 + president §3.5 一致。
- **状态**:待 coord 仲裁
### ISSUE-003-ai03:GraphQL 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 架构,**未同步**以下已裁决事项:
- F9(ISSUE-036):P2 起 all-in GraphQL,无 REST 过渡 — 01 §1/§3.1 仍写"P2-P3 用 REST 过渡";02 全文基于 REST(ApiClient/TanStack Query/契约清单)
- ARB-001(ISSUE-037):teacher-bff GraphQL schema 第一版 5 Query — 01 §3.1 列 REST 端点;02 §4/§10 全 REST
- ARB-002(ISSUE-038/039):MF Shell 暴露清单(GraphQLProvider + hooks + UI 组件 + urql/graphql singleton)— 01 未提;02 §1.2 exposes 仅 AppShell+shared-deps、P2 配 3 remotes、shared 无 urql
- 总裁 §2.17(ISSUE-038):GraphQL client 单例方案 A — 02 §11.3.2 仍列"GraphQL vs REST"未决(已裁决)
- 对照已回写的 `03-long-term-architecture.md §1.4`(GraphQL 最终方案),01/02 严重滞后
- **建议方案**:
1. 01-understanding.md:ai07→ai13;§1 删除 REST 过渡;§3.1 REST 端点→GraphQL queries(ARB-001);§4 技术栈补 urql;补 ARB-002 暴露清单
2. 02-architecture-design.md:ai07→ai13;全文 REST→GraphQL 重写(MF 配置对齐 ARB-002;API 层改 urql client;契约清单改 GraphQL;§11.3 未决决策改已决策)
- **状态**:✅ 已回写闭合(2026-07-10,审查批次)
- **coord 裁决**:无需新裁决(复用 ISSUE-036~040 已有裁决),本次为文档同步执行
- **回写执行**:
- `01-understanding.md`:已修正 ai07→ai13、§1 REST→GraphQL、§3.1 REST 端点→GraphQL queries(ARB-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**: ai13(teacher-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,最后统一集成测试)
> 说明:本排期基于 GraphQL(ARB-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-001),admin 权限点使用 `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 排期。
> 关键路径(crit):16.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-002,ai13 P2 交付)
- **交付物**:
- `apps/admin-portal/next.config.js`(NextFederationPlugin,Remote 角色,`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 JWT(admin 角色,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` schema(ISSUE-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` 页(树形 + DataTable,school/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` Query(ISSUE-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` 页(recharts:total_teachers / total_students / school_avg_score / 趋势图)
- **验收标准**:adminDashboard Query 返回聚合数据;60s 轮询监控指标 + 5min 轮询统计
### P6 阶段(硬化)
#### 16.13 A11y WCAG 2.2 AA 审计 + 修复
- **交付物**:eslint-plugin-jsx-a11y(error 级)+ 手动审计修复
- **验收标准**:0 个 error 级违规
#### 16.14 Web Vitals + OTel browser SDK 接入
- **交付物**:`next/web-vitals` → `POST /api/admin/web-vitals`;OTel browser SDK(复用 Shell TracerProvider,scope='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 就绪(ai13,ARB-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 providers,Shell 就绪后切换 | ⏳ |
| MF SSR 对齐复杂 | Remote SSR 上下文依赖 Shell | 优先 CSR,SSR 仅首屏 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,最后统一集成测试)
> 阶段归属:批次 4(P5),见 [workline.md §1](../workline.md) 甘特图 `b4c: ai12 ai服务 gRPC 50058, after b3a, 13d`
---
## §1 总览
ai 是智能服务,提供 AiService(6 个 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 全阶段甘特图(P5,13 天)
> 对齐 [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 排期。
**关键路径**(红色 crit):LLMProvider 抽象 → gRPC server → PromptTemplateService → GenerateQuestion → gRPC client → 备课工作流编排
---
## §3 详细任务
### 全阶段任务
### M14 基础架构(第1-4天)
#### P5-12.1:LLMProvider 抽象 + 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.2:ProviderFailoverChain + 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.3:gRPC server + ActionState 整改(P0 阻塞)
- **负责人**:ai12
- **依赖**:P5-12.1;**前置**:ISSUE-03(coord 升级 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.4:Redis 多维度限流 + 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.5:PromptTemplateService + 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.6:GenerateQuestion + StreamGenerateQuestion 逐字流式
- **负责人**:ai12
- **依赖**:P5-12.5;**前置**:ISSUE-03(ai.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-judge,5 维度加权评分)
- `services/ai/src/ai/evaluation/quality_gate.py`(阈值 0.7,不达标重试 < 3 次)
- **验收标准**:评估通过率 > 80%;不达标自动重试;重试耗尽返回 `AI_EVALUATION_FAILED`
#### P5-12.8:UsageRecorder + KafkaProducer + QuotaEnforcer
- **负责人**:ai12
- **依赖**:P5-12.6;**前置**:ISSUE-02(topic 裁决)+ ISSUE-04(events.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.10:gRPC client(content / data-ana / iam)+ interceptor
- **负责人**:ai12
- **依赖**:P5-12.3;**前置**:content gRPC 50054 就绪(ai09 P4)、ISSUE-07(iam 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`(GetEffectiveDataScope,Redis 缓存 5min)
- `grpc.aio.ClientInterceptor`(trace 注入 + 重试 + 熔断)
- **验收标准**:下游 gRPC 不可达时降级(跳过学情查询 + `degraded:true`);DataScope 缓存命中
#### P5-12.11:LessonPreparationWorkflow 4 步编排 + 状态机
- **负责人**:ai12
- **依赖**:P5-12.10;**前置**:ISSUE-03(ai.proto 补 `GenerateLessonPlan` RPC)
- **交付物**:
- `services/ai/src/ai/services/lesson_preparation_workflow.py`(4 步:分析学情 → 推荐知识点 → 生成题目 → 教师审核)
- 状态机实现(02-architecture-design.md §2.4:Pending→Analyzing→Recommended→Generating→PendingReview→Persisted)
- gRPC RPC:`GenerateLessonPlan`
- HTTP 端点:`POST /ai/v1/lesson/preparation` + `GET /ai/v1/lesson/preparation/{id}`
- **验收标准**:端到端跑通 4 步;评估未通过自动重试 < 3 次;工作流状态可查询
#### P5-12.12:WorkflowStateStore + 教师审核 + 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-python,ai.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 key(OpenAI / 百川 / 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. ai12:ai 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 启动前补全 proto(ISSUE-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 评估迁移 Temporal(ISSUE-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 RS256(HTTP 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 排期。
**关键路径**(红色 crit):P2.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 GetPublicKey(ai06)
- **验收标准**:路由双入口 + 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.1:shared-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/logger(zap 或 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.2:JWT 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 5min(shared-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.16(HTTP 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.4:slog + 业务 metrics + tracer 资源属性
- **负责人**:ai01
- **依赖**:P2.1 shared-go 接入完成
- **交付物**:
- 按 ISSUE-004 仲裁结果统一 logger(zap 或 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`(Counter,method/endpoint/status)
- `api_gateway_http_request_duration_seconds`(Histogram,method/endpoint)
- `api_gateway_circuit_breaker_state`(Gauge,service/state)
- `api_gateway_rate_limited_total`(Counter,ip)
- `api_gateway_proxy_upstream_duration_seconds`(Histogram,upstream)
- `api_gateway_jwks_refresh_total`(Counter,result)
- `api_gateway_auth_failures_total`(Counter,reason)
- [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.6:DevMode 生产防护
- **负责人**: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-003(API 版本化路由规则);iam (ai06) / teacher-bff (ai03) P2 就绪
- **交付物**:
- 按 ISSUE-003 仲裁结果更新路由(方案 A/B/C 之一)
- 若方案 A:iam 路由改为 `/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.1:student-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.2:core-edu 路由
- **负责人**:ai01
- **依赖**:core-edu (ai08) P3 就绪;classes 服务已合并入 core-edu(C1 裁决)
- **交付物**:
- `main.go` classes 路由目标改为 core-edu(`cfg.ClassesServiceURL` → `cfg.CoreEduServiceURL`)
- 或保留 classes 路由别名,proxy 到 core-edu
- exams / homework / grades 路由已在 P1 实现,无需修改
- **验收标准**:`/api/v1/classes/*` 代理到 core-edu:3004
### P3.3:API 版本化 /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.1:parent-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.2:content 路由
- **负责人**:ai01
- **依赖**:content (ai09) P4 就绪
- **交付物**:
- `main.go` 已有 content 路由(P1 实现:textbooks / chapters / knowledge-points / questions)
- 验证路由目标 `cfg.ContentServiceURL`(:3005)正确
- 按 P3.3 版本化规则加 `/v1` 前缀
- **验收标准**:content 路由可用 + 版本化
### P4.3:data-ana 路由
- **负责人**:ai01
- **依赖**:data-ana (ai11) P4 就绪
- **交付物**:
- `main.go` 已有 data-ana 路由(P1 实现:analytics)
- 增加 `dashboard` 路由别名:`/api/v1/dashboard` + `/*path` → data-ana
- 按版本化规则加 `/v1` 前缀
- **验收标准**:data-ana 路由可用 + dashboard 别名可用
---
### P5.1:msg 路由
- **负责人**:ai01
- **依赖**:msg (ai10) P5 就绪
- **交付物**:
- `main.go` 已有 msg 路由(P1 实现:notifications)
- 增加 `messages` 路由别名:`/api/v1/messages` + `/*path` → msg
- 按版本化规则加 `/v1` 前缀
- **验收标准**:msg 路由可用 + messages 别名可用
### P5.2:ai 路由
- **负责人**:ai01
- **依赖**:ai (ai12) P5 就绪
- **交付物**:
- `main.go` 已有 ai 路由(P1 实现)
- 按版本化规则加 `/v1` 前缀
- **验收标准**:ai 路由可用
### P5.3:push-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.2:per-服务实例熔断评估
- **负责人**: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 RPC(ai06)
- **我的就绪信号**: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 启用(不影响 Gateway,Gateway 走 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 Service(Textbook/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+ 长远演进:教材版本管理 / 跨租户内容共享 / 个性化学习路径推荐
**关键路径位置**:批次 3(P4),依赖批次 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 producer(idempotent + transactionalId)
- **负责人**:ai09
- **依赖**:P4.2
- **交付物**:
- 新建 `src/shared/kafka/producer.ts`(kafkajs 客户端,idempotent=true,transactionalId=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 节点最终一致出现(延迟 < 2s);Neo4j 故障时事件不丢失,恢复后补齐
#### 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`
- **验收标准**:业务事务内不再直接写 Neo4j;Neo4j 写入全部走异步 Sync Worker(解决 ISSUE-010 + 01-understanding C9)
#### P4.6 content.proto 补 ChapterService / QuestionService
- **负责人**:ai09
- **依赖**:无(自身 proto 现状)
- **交付物**:
- [content.proto](../../../packages/shared-proto/proto/content.proto) 补 ChapterService(CreateChapter/ListChapters/GetChapter/UpdateChapter/DeleteChapter)
- 补 QuestionService(CreateQuestion/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` 无破坏性变更(新增字段 OK);contract.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=degraded;DB 不可用 → 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 过滤
- **验收标准**:检索延迟 < 200ms(P5 退出标准);返回分页结构
#### 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 对齐
- 批次 3(P4):ai09 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 全部代码,最后统一集成测试)
> 批次归属:批次 2(P3 核心教学),关键路径
---
## §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 + /readyz(DB 探针) |
| 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.2(TOPIC_MAP)→ P3.3(状态机)→ P3.4(gRPC)→ P3.5/P3.6/P3.7/P3.8/P3.9/P3.10/P3.11(并行)→ P3.12(测试)
**预计工期**:
- P3:13 天(含 2 天等待仲裁)
- P4:5 天
- P5:3 天
- P6:4 天
- 合计:25 天(P3 后)
---
## §3 详细任务
### 全阶段任务
### P3.0 等待 coord 仲裁 ISSUE-001 ~ ISSUE-006
- **负责人**:ai08(等待 coord)
- **依赖**:无
- **交付物**:coord 出具仲裁结论(见 [coord.md](../coord.md) 后续章节)
- **验收标准**:6 项 ISSUE 全部裁决,状态更新为"已裁决"
- **阻塞说明**:
- ISSUE-001(proto 实际状态):阻塞 P3.4 gRPC 实现
- ISSUE-002(events.proto 未同步):阻塞 P3.2 TOPIC_MAP 重命名
- ISSUE-003(状态命名不一致):阻塞 P3.3 状态机实现
- ISSUE-004(class.transferred topic):阻塞 P3.11 classes 合并
- ISSUE-005(RPC 数量口径):阻塞就绪信号声明
- ISSUE-006(7 项设计决策):阻塞 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 ValidationPipe(exams/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 RPC(P1)
- **负责人**: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 RPC:CreateExam / GetExam / ListExamsByClass / UpdateExam / DeleteExam / PublishExam / SubmitExam / GradeExam)
3. `src/homework/homework.grpc-controller.ts`(HomeworkService 5 RPC:AssignHomework / GetHomework / ListHomeworkByClass / SubmitHomework / GradeHomework)
4. `src/grades/grades.grpc-controller.ts`(GradeService 6 RPC:RecordGrade / GetGrade / ListGradesByStudent / ListGradesByExam / ListGradesByHomework / UpdateGrade)
5. `src/attendance/attendance.grpc-controller.ts`(AttendanceService 4 RPC:RecordAttendance / GetAttendance / ListAttendanceByStudent / ListAttendanceByClass)
6. `src/classes/classes.grpc-controller.ts`(ClassService 4 RPC:GetClass / 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 + PermissionGuard(gRPC 上下文适配)
### P3.5 排课考勤数据模型 + AttendanceService(P2)
- **负责人**: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 决策 #3(scope 优先级)
- **交付物**:
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-edu(P3)
- **负责人**: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 50055(P4) | ai11 | HealthService.Check = SERVING | ⏳ | 阻塞 P4.1 mastery 消费 |
| content gRPC 50054(P4) | ai09 | HealthService.Check = SERVING | ⏳ | 阻塞 P4.2 知识点关联 |
| msg gRPC 50056(P5) | ai10 | HealthService.Check = SERVING | ⏳ | 阻塞 P5.1 事件联调 |
### 4.2 我的就绪标志(供下游消费)
| 阶段 | 就绪信号 | 消费方 | 状态 |
| ---- | -------- | ------ | ---- |
| P2(已就绪) | HTTP 3004 可访问 + /healthz + /readyz(DB 探针) | teacher-bff(REST 调用) | ✅ |
| P3(核心) | gRPC 50053 + 27 RPC + HealthService SERVING | teacher-bff / student-bff / parent-bff / ai | ⏳ |
| P3 子信号 1 | ClassService 4 RPC 可调用 | teacher-bff(班级列表) | ⏳ |
| P3 子信号 2 | ExamService 8 RPC 可调用 | 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),批次 1(P2)2026-07-10 启动
---
## §1 总览
data-ana 是数据分析服务,提供 AnalyticsService(12 个 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 排期。
> **关键路径**(crit):ActionState 信封重构 → gRPC 50055 启用 → analytics.proto 扩展 → iam GetEffectiveDataScope 集成
> **总工期**:约 51 天(2026-07-10 ~ 2026-08-30),其中 P4 主战场 11 天为关键交付期
---
## §3 详细任务
### 全阶段任务
### 3.1 P2 预备期(2026-07-10 ~ 2026-07-18,8d)
#### 任务 2.1:ClickHouse DDL 5 宽表建表
- **负责人**:ai11
- **交付物**:⚠️ 由 ai11 自行补充
- **依赖**:见 [contracts/data-ana_contract.md](../contracts/data-ana_contract.md)
- **验收标准**:⚠️ 由 ai11 自行补充
- **依赖**:无(ClickHouse 实例就绪,由 infra 提供)
- **交付物**:`infra/clickhouse/ddl/data_ana.sql`(5 宽表 DDL:student_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_log)
- **验收标准**:5 表在 ClickHouse 中创建成功,ReplacingMergeTree 引擎 + ORDER BY + PARTITION BY 符合 02 §3 DDL 设计
#### 任务 2.2:CDC 消费骨架
- **负责人**:ai11
- **依赖**:Kafka 就绪
- **交付物**:`src/data_ana/cdc_consumer.py` 重构(aiokafka AIOKafkaConsumer + EventHandler 路由框架)
- **验收标准**:能消费 mock CDC 事件并打印路由日志,consumer group = `data-ana-cdc`
#### 任务 2.3:mock 数据集
- **负责人**:ai11
- **依赖**:任务 2.1
- **交付物**:`scripts/seed_clickhouse.py`(批量导入 30 学生 × 5 考试 × 10 作业 × 30 天出勤模拟数据)
- **验收标准**:ClickHouse 5 表有数据,可查询返回非空结果
#### 任务 2.4:ActionState 信封重构(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.details),ruff 零错误
#### 任务 2.5:config.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-26,8d)
#### 任务 3.1:gRPC server 骨架(3 RPC,不正式启用)
- **负责人**:ai11
- **依赖**:analytics.proto 当前 3 RPC(无需 coord 补全)
- **交付物**:`src/data_ana/grpc_server.py`(grpc.aio Server 骨架 + 3 RPC 实现,绑定 :50055 但不启动对外)
- **验收标准**:本地可启动 gRPC server,3 RPC 可调用返回 mock 数据
#### 任务 3.2:core-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.3:ExamCache 内存 LRU
- **负责人**:ai11
- **依赖**:任务 3.2
- **交付物**:`src/data_ana/exam_cache.py`(内存 LRU dict,max 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.5:ClickHouseRepository 封装
- **负责人**:ai11
- **依赖**:任务 2.1
- **交付物**:`src/data_ana/clickhouse_client.py` 重构(查询封装 + FINAL/argMax 去重 + DataScope WHERE 注入接口)
- **验收标准**:查询 student_dashboard_view 返回去重后最新版本数据
---
### 3.3 P4 主战场期(2026-07-26 ~ 2026-08-06,11d,批次 3)
#### 任务 4.1:gRPC 50055 正式启用
- **负责人**:ai11
- **依赖**:任务 3.1
- **交付物**:main.py lifespan 启动 gRPC server :50055,HealthService.Check 返回 SERVING
- **验收标准**:gRPC server 对外可访问,HealthService.Check = SERVING
#### 任务 4.2:analytics.proto 扩展 12 RPC
- **负责人**:ai11(本分支内补全 proto,提请 coord 合并)
- **依赖**:ISSUE-003 解决(coord 确认或 ai11 自行补全)
- **交付物**:`packages/shared-proto/proto/analytics.proto` 扩展至 12 RPC(3 现有 + 9 新增 message 定义)
- **验收标准**:`buf lint` 零错误,`buf generate` 生成 Python stub 成功,12 RPC 全部可调用
#### 任务 4.3:4 端 Dashboard RPC 实现
- **负责人**:ai11
- **依赖**:任务 4.2
- **交付物**:GetTeacherDashboard / GetStudentDashboard / GetParentDashboard / GetAdminDashboard 4 RPC 实现
- **验收标准**:4 RPC 返回 ActionState[DashboardData],DataScope 过滤生效,降级时返回骨架数据 + degraded: true
#### 任务 4.4:WarningService + 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.5:GetMasteryDistribution + GetStudentMastery
- **负责人**:ai11
- **依赖**:任务 3.4 + 任务 4.2
- **交付物**:2 RPC 实现(班级掌握度分布 + 学生知识点掌握度明细)
- **验收标准**:返回 mastered/progressing/weak 三档分布数据
#### 任务 4.6:iam.GetEffectiveDataScope 集成(降级兜底)
- **负责人**:ai11
- **依赖**:ISSUE-001 解决(iam.proto 补全 GetEffectiveDataScope)。若 P4 时 iam 未就绪,使用降级兜底
- **交付物**:`src/data_ana/iam_client.py`(gRPC 调 iam.GetEffectiveDataScope + Redis 缓存 5min + 降级兜底)
- **验收标准**:iam 可用时调 gRPC 获取 DataScope;iam 不可用时按 role 映射默认 DataScope + degraded: true
#### 任务 4.7:DataScope 6 级 WHERE 注入
- **负责人**:ai11
- **依赖**:任务 4.6 + 任务 3.5
- **交付物**:ClickHouseRepository 查询方法注入 DataScope WHERE 子句(SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL)
- **验收标准**:教师只能查自己班级数据,学生只能查自己数据,管理员可查全校数据
#### 任务 4.8:attendance + content CDC 消费
- **负责人**:ai11
- **依赖**:任务 3.2(CDC 框架)
- **交付物**:EventHandler 扩展 attendance_logs 表 CDC + content_knowledge_points 表 CDC
- **验收标准**:考勤事件落 attendance_logs 表,知识点事件更新 mastery_snapshot 元数据
#### 任务 4.9:MasteryEvent + 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.10:HTTP 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-19,13d,批次 4 并行)
#### 任务 5.1:SubscribeMasteryUpdate Server Streaming RPC
- **负责人**:ai11
- **依赖**:任务 4.2 + 任务 4.9
- **交付物**:SubscribeMasteryUpdate RPC 实现(server-streaming,客户端订阅 student_id/class_id,掌握度更新时推送)
- **验收标准**:客户端订阅后,掌握度计算完成时收到 MasteryUpdateEvent 流
#### 任务 5.2:AIUsageEvent 消费 → 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` + 手动 commit(at-least-once)
- **验收标准**:ClickHouse 写入成功后才 commit offset,重启后无重复消费(依赖 ReplacingMergeTree 去重)
#### 任务 5.4:Admin Dashboard AI 用量区块
- **负责人**:ai11
- **依赖**:任务 5.2
- **交付物**:GetAdminDashboard RPC 补全 AI 用量统计区块(按 provider/model/时间窗聚合)
- **验收标准**:Admin Dashboard 返回 AI 用量数据,无数据时显示"暂无数据"
---
### 3.5 P6 硬化期(2026-08-19 ~ 2026-08-30,11d,批次 5 并行)
#### 任务 6.1:CDC 多实例水平扩展
- **负责人**:ai11
- **依赖**:任务 5.3(手动 commit)
- **交付物**:CdcConsumer 支持多实例分摊 partition(consumer group 不变)
- **验收标准**:2+ 实例消费同一 topic 无重复无遗漏
#### 任务 6.2:ExamCache 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.5:readyz 深度硬化
- **负责人**:ai11
- **依赖**:任务 4.10
- **交付物**:/readyz 检查项完善(ClickHouse 查询超时 1s + Redis ping + iam gRPC 超时 2s + CDC consumer lag < 1000)
- **验收标准**:任一依赖不健康时 /readyz 返回 503,K8s 摘流量
---
## §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/coord,ISSUE-001)—— DataScope 解析
- [ ] **analytics.proto 扩展至 12 RPC**(coord/ai11,ISSUE-003)—— gRPC stub 生成前提
- [ ] **events.proto 补全 AIUsageEvent message**(coord,ISSUE-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-bff(ai03) | gRPC 50055 GetTeacherDashboard / GetClassPerformance 等 | P4 |
| student-bff(ai04) | gRPC 50055 GetStudentDashboard / GetStudentWeakness 等 | P4 |
| parent-bff(ai05) | gRPC 50055 GetParentDashboard | P4 |
| ai 服务(ai12) | gRPC 50055 反向调用查学情(GetStudentMastery / GetLearningTrend) | P5 |
| core-edu(ai08) | Kafka `edu.insight.mastery.updated`(推荐个性化练习) | P4 |
| msg(ai10) | Kafka `edu.insight.warning.triggered`(推送通知) | P4 |
---
## §5 风险与缓解
| 风险 | 影响 | 缓解措施 |
| -------------------------------------------- | ---- | ------------------------------------------------------------------------------------------ |
| iam.proto 未补全 GetEffectiveDataScope | P4 | 降级兜底:按 role 映射默认 DataScope + degraded: true(ISSUE-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 ExamCache;P4 阶段监控 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):
- I1:P2 即启用 gRPC server 50052(REST + gRPC 双入口并存,非"P2 仅 REST → P3 gRPC")
- I2:直接用 shared-ts Outbox 工具包(非 iam 自建)
- I3:首次实现即 DB 驱动 + Redis 缓存 PermissionGuard(废弃硬编码 ROLE_PERMISSIONS map)
- I4:首次实现即注册 AuthMiddleware(Controller 通过 @Req() 注入用户上下文)
- I5:P2 本地文件 RS256 密钥(IAM_PRIVATE_KEY_PATH / IAM_PUBLIC_KEY_PATH),P6 迁 Vault
- I6:P2 即补全 iam_student_guardians 表 + GetChildrenByParent RPC + GET /iam/children
- I7:/iam/v1/* 前缀(Controller 加 v1 前缀,Gateway 透传)
- I8:统一 GET /iam/permissions/effective
**工作量分级**(president §3.2):iam 实际工作量 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.1:gRPC + 12 RPC + /readyz + DataScope
### P2.1:gRPC + 8 RPC + RS256 + AuthMiddleware + Outbox(阻塞批次 2)
- **负责人**:ai06
- **批次**:批次 1(P2)
- **预估**:5-7 天
- **前置依赖**:
- coord 补全 iam.proto 至 12 RPC(ISSUE-005,阻塞项)
- coord 补全 events.proto UserEvent/RoleEvent(ISSUE-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')`)+ 端点路径统一 I8(I7)
6. iam_student_guardians 表 + GET /iam/v1/children REST + GetChildrenByParent gRPC(I6)
7. shared-ts Outbox 接入,发布 UserEvent/RoleEvent(I2)
8. DB 驱动 PermissionGuard 基础(废弃 permission.guard.ts 硬编码 ROLE_PERMISSIONS map,改调 IamService.getEffectivePermissions)(I3)
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.user(userId/roles/dataScope)
- /readyz 返回 5 项依赖状态
- **就绪信号**:gRPC 50052 启用 + GetPublicKey RPC 可用 + HealthService.Check 返回 SERVING
### P2.2:三层角色 + DataScope + 视口 + 审计 + 缓存(P3 期间持续补,不阻塞)
- **负责人**:ai06
- **批次**:批次 2-3 期间(P3 进行中持续补)
- **预估**:12-15 天
- **前置依赖**:P2.1 完成 + ISSUE-004(DataScope 枚举裁决)
- **交付物**:
1. 三层角色模型(system / organization / temporary,iam_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 topic,president §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-P6:RBAC CRUD + 2FA + Vault 迁移 + 硬化
- **负责人**:ai06
- **批次**:批次 4-5(P5-P6 期间)
- **预估**:8-10 天
- **交付物**:
1. RBAC CRUD 完整化(角色/权限/视口增删改,系统角色禁止删除)
2. 2FA 实现(TOTP,pending-features §P2 提及)
3. JWT 密钥迁移 Vault(P6,president 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 RPC(ISSUE-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 是消息服务,提供 NotificationService、PreferenceService、TemplateService,并基于 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 多渠道
- P5:gRPC 50056 启用 + PushGatewayClient gRPC + 12 类事件 consumer + ES mapping
- P6:测试覆盖 ≥ 80% + /readyz 硬化 + 黄金模板对齐 + README 修正
**批次归属**:批次 4(P5),依赖批次 3 content(ai09)就绪后启动,预估 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-T2:schema 迁移(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` 表 schema(code + type + title_template + content_template + default_channels + variables + locale + status),UNIQUE INDEX `(code, locale)`
- **验收标准**:schema 定义 + 迁移脚本,符合 02-architecture-design.md §3.1.3
#### T4:新建 msg_outbox_events 表 + Outbox Publisher worker
- **负责人**:ai10
- **依赖**:T1-T2
- **交付物**:
- `msg_outbox_events` 表 schema(event_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-003(Outbox 强制)
#### 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 + IdempotencyGuard(SETNX)
- **负责人**: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 主体实现
#### T7:ChannelDispatcher 多渠道抽象
- **负责人**: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-010(markAsRead 权限改 READ)
#### T12:NotificationPreference CRUD
- **负责人**:ai10
- **依赖**:T1-T2
- **交付物**:`preferences/` 目录(controller + service + repository + schema + dto)
- **验收标准**:GET / PUT preferences 端点可用
#### T13:NotificationTemplate CRUD + render
- **负责人**:ai10
- **依赖**:T3
- **交付物**:`templates/` 目录(controller + service + repository + schema + dto),含 `{{variable}}` 占位符替换渲染
- **验收标准**:CreateTemplate / GetTemplate / ListTemplates / RenderTemplate 可用
#### T15:createBatch 改批量 INSERT
- **负责人**:ai10
- **依赖**:T10
- **交付物**:createBatch 改为 `db.insert(notifications).values([...])` 批量 INSERT
- **验收标准**:1 万条广播通知 < 5s(性能验收)
### P5 gRPC + 事件 + ES
#### T8:PushGatewayClient gRPC
- **负责人**:ai10
- **依赖**:D5(push-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(调用方向澄清)
#### T9:12 类 Kafka 事件 consumer
- **负责人**:ai10
- **依赖**:T5、T6、D1-D2(events.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-013(events.proto 缺 4 类 message,阻塞)
#### T14:ES 索引 mapping + ensureIndex + 同步
- **负责人**:ai10
- **依赖**:无
- **交付物**:
- `config/elasticsearch.ts` 补 `notifications` 索引 mapping(ik_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-D4(msg.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 返回 SERVING,13 RPC 可调用
- **关联 ISSUE**:ISSUE-009(RPC 数量待仲裁 13 vs 17)
### P6 硬化与对齐
#### T16:NotificationsModule 补 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 → down;Redis/ES/Kafka down → degraded)
#### T19:统一关闭到 LifecycleService
- **负责人**:ai10
- **依赖**:无
- **交付物**:移除 main.ts 重复 closeDb/closeEs,统一到 LifecycleService,8 步关闭序列
- **验收标准**:符合 02-architecture-design.md §6.7 优雅关闭顺序
#### T20-T21:DB 改 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 message(coord 维护)— 🔴 阻塞 T9
- [ ] **D2**:events.proto GradeEvent 补 class_id;全部事件补 student_ids[](coord 维护)— 🔴 阻塞 T9 fan-out
- [ ] **D3**:msg.proto 补 BatchSendNotification / GetUnreadCount / BatchMarkAsRead / MarkAllAsRead / RecallNotification RPC(coord 维护)— 🔴 阻塞 gRPC(依赖 ISSUE-009 仲裁)
- [ ] **D4**:msg.proto 补 NotificationPreferenceService + NotificationTemplateService(coord 维护)— 🔴 阻塞 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.GetChildrenByParent(I6 裁决)+ 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 监控 + 灰度发布 | — |
**关键路径**:批次 0(coord 补 proto)→ 批次 1(iam gRPC + GetChildrenByParent)→ 批次 2(core-edu gRPC)→ **批次 3(parent-bff P4 MVP)** → 批次 4(msg 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-007,P5 阶段阻塞)
---
## §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.2:GraphQL 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 ≤ 7,cost ≤ 1000,02 §9 #5)
- `packages/shared-ts/contracts/graphql/parent-bff.graphql`(SDL-first 集中管理,对齐 coord ARB-001 模式)
- **验收标准**:POST /graphql 可内省 schema;深度超 7 的查询被拒;cost 超 1000 被拒
### P4.3:DownstreamClient 抽象 + gRPC mock
- **负责人**:ai05
- **依赖**:P4.1 + ai03 DownstreamClient 抽象模式(teacher-bff 参考)
- **交付物**:
- `src/clients/grpc/grpc.factory.ts`(gRPC client 创建 + interceptor:trace/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.4:ChildGuard + 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.5:Dashboard / 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.6:Redis 缓存层 + 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-instrumentations(http/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.1:msg 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.2:Notification 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.3:push-gateway HTTP /internal/push 接入
- **负责人**:ai05
- **依赖**:push-gateway /internal/push 就绪(ai02)或 mock
- **交付物**:
- `src/clients/http/push-http.client.ts`(HTTP POST /internal/push,U2 仲裁)
- env.ts 启用 PushGatewayUrl
- **验收标准**:mock 模式下 pushViaHttp 返回 success
### P5.4:Kafka 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 去重
- DLQ:edu.parent-bff.dlq
- **验收标准**:
- 收到 edu.teaching.grade.recorded 后 bff:parent:grades:{childId} 缓存失效
- 家长关闭"成绩推送"偏好时,该家长不收到推送
- 重复 event_id 不重复处理
### P5.5:通知偏好过滤逻辑
- **负责人**:ai05
- **依赖**:P5.4
- **交付物**:通知偏好过滤逻辑集成到 notification-push.handler(02 §5.4)
- 拉取家长 NotificationPreferences(Redis 缓存 300s)
- 按 eventTypeMap 映射事件类型 → 偏好开关
- 取 prefs.channels 与 event.channels 交集
- **验收标准**:偏好开关为 false 时不推送;channels 无交集时不推送
### P6.1:opossum 熔断器 per-service
- **负责人**:ai05
- **依赖**:P5 完成
- **交付物**:
- `src/clients/grpc/circuit-breaker.ts`(opossum,per-downstream-service 独立 circuit:iam/core-edu/data-ana/msg)
- 熔断开启时抛 BFF_PARENT_SERVICE_UNAVAILABLE(503)
- metrics: parent_bff_circuit_state Gauge
- **验收标准**:下游连续失败 5 次熔断开启;30s 后半开探测
### P6.2:HPA + 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.3:SLO 告警规则 + 灰度发布
- **负责人**: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 | P4(P0 阻塞) | ⏳ |
| iam_student_guardians 表 | 表已建 + Repository 查询方法可用 | ai06 | P4(P0 阻塞) | ⏳ |
| 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-portal(ai15) |
| 核心 Query 可执行 | dashboard / myChildren / childGrades / childAnalytics | parent-portal |
| 核心 Mutation 可执行 | selectChild / markNotificationRead(P5) | 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 拦截 + 固定 UserInfo(parent 角色)+ 固定 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 | P4(P0) | 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 角色**:Remote(Shell = teacher-portal :4000)
- **端口**:4002(dev/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 周)
> **关键路径**(红色 crit):MF 骨架 → GraphQL client → ChildSwitcher → 质量保障 → WebSocket → 通知中心
---
## §3 详细任务
### 全阶段任务
### P4 阶段任务
#### P4-1:MF Remote 骨架 + next.config.js + 健康检查
- **负责人**:ai15
- **交付物**:⚠️ 由 ai15 自行补充
- **依赖**:见 [contracts/parent-portal_contract.md](../contracts/parent-portal_contract.md)
- **验收标准**:⚠️ 由 ai15 自行补充
- **依赖**:teacher-portal Shell MF 配置就绪(ARB-002,ai13 P2 交付);ISSUE-002 仲裁(MF shared 清单)
- **交付物**:
- `apps/parent-portal/next.config.js`(NextFederationPlugin,Remote 角色,`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.json,strict)
- `apps/parent-portal/package.json`(依赖对齐 Shell:react 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.js(MF 拓扑验证)
4. feature flag `NEXT_PUBLIC_MF_ENABLED` 可控制 MF 开关
#### P4-2:GraphQL client 接入 + MSW mock 层
- **负责人**:ai15
- **依赖**:P4-1;ISSUE-001 仲裁(确认 GraphQL);teacher-portal Shell 暴露 GraphQLProvider(ARB-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-3:ChildSwitcher + useChildSwitcher + Zustand slice
- **负责人**:ai15
- **依赖**:P4-2;ISSUE-009 仲裁(switchChild 是 GraphQL Mutation 还是纯前端状态)
- **交付物**:
- `apps/parent-portal/src/stores/childSwitcherSlice.ts`(Zustand slice:children / 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 子女显示 EmptyChildState;1 子女不显示 TabBar;2-3 子女显示 Tab
4. 切换子女竞态:快速连续切换,旧请求 abort,新数据正确显示
#### P4-4:Dashboard 页面 + 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-9:Vitest 单测 + 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-10:Dockerfile 多阶段构建
- **负责人**:ai15
- **依赖**:P4-1
- **交付物**:`apps/parent-portal/Dockerfile`(builder + runtime,node:22-alpine)
- **验收标准**:
1. `docker build` 成功
2. HEALTHCHECK 指向 `/api/health`
3. 镜像体积 < 300MB
### P5 阶段任务
#### P5-1:WebSocket 接入 + 事件处理
- **负责人**:ai15
- **依赖**:push-gateway :8081/ws 就绪(ai02);parent-portal P4 完成
- **交付物**:
- `apps/parent-portal/src/hooks/useWebSocket.ts`(连接 push-gateway ws,JWT 鉴权,自动重连)
- 事件处理:`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-1:Web Vitals + OTel browser SDK
- **交付物**:`next/web-vitals` 上报 + OTel browser SDK(复用 Shell 暴露的 TracerProvider)
- **验收标准**:LCP/CLS/TTFB 指标上报到 Gateway
#### P6-2:A11y 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-5:PWA(Service 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 exposes(AppShell + 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/Logger(coord 维护) | coord | ⏳ | 基础工具 |
| contracts | Permissions 常量(coord 维护) | coord | ⏳ | 权限校验 |
| ui-tokens / ui-components / hooks | 三层令牌 + shadcn + usePermission/useAuth(ai07/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 返回固定 JWT(parent 角色) | 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 预排期;仲裁若改 REST,P4-2 重写(预计 1d) |
| iam GetChildrenByParent 缺失(ISSUE-010) | 多子女场景无法落地 | mock 固定 2 子女开发;coord 跟踪 ai06 P3 补全 |
| MF SSR 对齐复杂 | Remote SSR 需 Shell 上下文 | 优先 CSR,仅 Dashboard 首屏 SSR;P4-1 PoC 验证 |
| parent-bff 契约未最终确认 | GraphQL schema 可能变动 | P4 启动前与 ai05 对齐 schema;MSW 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 强制)
- A11y:jsx-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 仲裁
- **批次 4(P5,13 天)**:完整实现 /internal/push + WebSocket 生命周期 + Redis Pub/Sub + Kafka 消费 + /readyz + /metrics + OTel + 多阶段 Dockerfile + slog + shared-go 接入 + 集成测试
- **批次 5(P6 硬化,5 天)**:Reconnect 协议 + Redis Stream 持久化 + 测试覆盖率 ≥ 80% + ADR/非功能性需求/失败模式章节补全
**关键路径依赖**:
- 批次 0.14:shared-go 包骨架(coord)—— tracer/logger/jwks/env 4 模块
- 批次 1:iam P2.1(JWT RS256 + JWKS 端点)—— WebSocket 鉴权前置
- 批次 4:msg 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)
> **总工期**:批次 0(2 天)+ 批次 4(13 天)+ 批次 5(5 天)= **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 批次 4(P5):完整实现(13 天)
#### 任务 4.1:Origin 校验 + CheckOrigin 白名单(P0,1 天)
- **负责人**: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-Token(P0,1 天)
- **负责人**: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 控制帧 + SetReadDeadline(P0,1 天)
- **负责人**: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=5(P0,1 天)
- **负责人**: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.5:Send 通道满时指标 + 日志(P0,1 天)
- **负责人**: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.6:Dockerfile 重构(P0,1 天)
- **负责人**: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 替换标准 log(P0,1 天)
- **负责人**: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-go(P0,1 天)
- **负责人**:ai02
- **依赖**:任务 4.7 + 批次 0.14(shared-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.9:Redis Pub/Sub 跨实例广播(P1,2 天)
- **负责人**: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.10:Redis SET 在线状态 + 启动重建(P1,1 天)
- **负责人**: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 软失败(P1,1 天)
- **负责人**: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 连接(P1,1 天)
- **负责人**: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.13:Kafka 消费 edu.notification.requested(P2,2 天)
- **负责人**: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.14:JWT RS256 升级 + JWKS fetcher(P2,1 天)
- **负责人**:ai02
- **依赖**:任务 4.8(shared-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:设计决策记录章节回写(P2,1 天)
- **负责人**:ai02
- **依赖**:任务 4.13
- **交付物**:02-architecture-design.md 新增 §5.4(ISSUE-056)
- 标题:"设计决策记录:gRPC vs HTTP 协议选型(coord 已采纳 P1)"
- 正文标注"coord 已采纳,见 coord-final-decisions P1/P5/P6"
- 记录决策背景、方案对比、采纳理由
- **验收标准**:章节存在且标注正确
#### 任务 4.16:与 msg 联调 /internal/push 双通道(P2,1 天)
- **负责人**: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:集成测试 + 端到端验证(P2,1 天)
- **负责人**:ai02
- **依赖**:任务 4.16
- **交付物**:集成测试用例 + 端到端验证报告
- 单实例 1000 连接压测
- 双实例跨实例推送验证
- Redis 故障降级验证
- Kafka 消费积压验证
- **验收标准**:
- [workline.md](../workline.md) §8 push-gateway 行更新为 ✅
- [matrix.md](../matrix.md) §8 push-gateway 就绪信号 ✅
### 3.3 批次 5(P6):硬化(5 天)
#### 任务 5.1:Reconnect 协议(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.2:Redis Stream 替代 Pub/Sub(2 天)
- **负责人**: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.4:ADR + 非功能性需求 + 失败模式章节补全(2 天,可与 5.1/5.2 并行)
- **负责人**:ai02
- **依赖**:批次 4 完成 + ISSUE-007 仲裁
- **交付物**:02-architecture-design.md 新增章节
- §14 ADR(Architecture Decision Records)
- ADR-001:gorilla/websocket 选型(vs nhooyr/websocket)
- ADR-002:Redis Pub/Sub vs Stream(P5 Pub/Sub → P6 Stream 演进)
- ADR-003:心跳间隔 30s/60s 选型依据
- ADR-004:10w 容量依据(goroutine-per-connection 内存估算)
- ADR-005:HTTP + 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 基础设施 | coord(P1) | Redis 7.x 可访问 | ✅ P1 已就绪 | 任务 4.9/4.10 |
| Kafka 基础设施 | coord(P1) | 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 mock:msg 未就绪前,push-gateway 内置定时器每 30s 生成 mock 事件推所有在线客户端
- JWT 验签 mock:iam 未就绪前使用本地固定 RS256 公钥(或 DevMode dev-token)
- Kafka 订阅 mock:msg 未就绪前不启动 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)
- **端口**:3009(HTTP 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 起直接 GraphQL(Yoga + 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-bff,3 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 排期。
> **关键路径**(crit):schema 起草 → 文档回写 → 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 核心 Query:studentDashboard / myHomework / myGrades / myExams / myClasses / currentUser
- P3 核心 Mutation:submitHomework
- 提交 coord 仲裁(president §2.2:coord 在批次 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 client(iam + 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 interceptor:traceId 透传 + 错误归一化
- **验收标准**:可调用 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`:myHomework(core-edu)
- `src/student/resolvers/grades.resolver.ts`:myGrades(core-edu,B4 强制 userId 比对)
- `src/student/resolvers/exams.resolver.ts`:myExams(core-edu)
- `src/student/resolvers/classes.resolver.ts`:myClasses(core-edu)
- `src/student/resolvers/auth.resolver.ts`:currentUser(聚合 iam.GetUserInfo + GetEffectivePermissions + GetViewports)
- 并行编排:Promise.allSettled + 部分降级(president §2.6 方案 B:data 内 degraded 字段)
- **验收标准**:5 个核心 Query 可执行,返回 ActionState 信封
- **状态**:⏳ 待办
#### P3.4 Mutation(submitHomework)+ AuthorizationGuard(B4)
- **负责人**: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-30s,B6)
- 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`:/healthz(liveness)+ /readyz(readiness)
- P3 /readyz 探针:iam gRPC 50052 + core-edu gRPC 50053(2 项,president §2.4)
- 必需依赖失败返回 503;可选依赖软失败返回 200 + degraded
- **验收标准**:/readyz 返回 2 项探针状态
- **状态**:⏳ 待办
#### P3.8 横切关注点
- **负责人**:ai04
- **依赖**:P3.1
- **交付物**:
- `src/shared/observability/logger.ts`(pino,service: 'student-bff')
- `src/shared/observability/metrics.ts`(prom-client,11 个 student_bff_* 指标)
- `src/shared/observability/tracer.ts`(OTel,serviceName: '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 client:TextbookService + ChapterService + QuestionService + KnowledgeGraphService
- data-ana gRPC client:AnalyticsService.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 client:NotificationService.ListNotifications + MarkAsRead
- ai gRPC client:AiService.Chat + StreamChat(SSE 流式透传)
- Query/Mutation 扩展:myNotifications / markAsRead Mutation / aiChat / aiStreamChat
- **状态**:⏳ 待办
#### P5.4-P5.6 Kafka EventSubscriber + push-gateway + 探针扩展
- **交付物**:
- `src/student/events/event-subscriber.ts`:Kafka 消费者组(B7 P5 才订阅)
- 订阅 topic:edu.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) | coord(president §2.5) | proto message + RPC 签名定义 | 批次 2 P3 | ⏳ |
| buf.gen.yaml gRPC 插件 | coord(批次 0.9) | grpc/node 插件配置 | 批次 2 P3 | ⏳ |
| ai03 DownstreamClient 抽象 | ai03(B8 裁决) | teacher-bff P2 产出可复用抽象 | 批次 2 P3 | ⏳ |
| iam gRPC 50052 + 12 RPC | ai06(I1 裁决) | HealthService.Check = SERVING | 批次 2 P3 | ⏳ |
| core-edu gRPC 50053 + 22 RPC | ai08(C2 裁决) | 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 第一版 | coord(president §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 | ai14(student-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.6(ai14 P3 功能范围)+ §7.14(ai14 工作内容最终清单)+ ARB-002 §2.3(P3 首个 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)):
- 批次 2(P3):ai14 与 ai07 + ai08 + ai04 + ai03扩展 并行启动(`b2d, after b1d, 8d`)
- 批次 3(P4):与 ai09 + ai11 + ai05 + ai15 并行
- 批次 4(P5):与 ai10 + ai02 + ai12 + ai03扩展 并行
- 批次 5(P6):与 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 详细任务
### 全阶段任务
### P2:MF Remote 骨架预埋
- **负责人**:ai14
- **交付物**:⚠️ 由 ai14 自行补充
- **依赖**:见 [contracts/student-portal_contract.md](../contracts/student-portal_contract.md)
- **验收标准**:⚠️ 由 ai14 自行补充
- **裁决依据**:ARB-002 §2.3(P3 首个 Remote,但 P2 可预埋骨架)+ 总裁裁决 §3.6(ai14 P3 起步)+ §2.17(MF 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.tsx,MF 关闭时可独立渲染)
- 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/hooks,ai13 批次 0.15 已完成)
- teacher-portal MF Shell 配置(ai13 P2,exposes 就绪)
- **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.6(ai14 P3 功能范围)+ ARB-001 §1.3(ActionState 信封 + 降级模式方案 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` query(upcomingHomework + 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` hook,5 分钟重新同步,倒计时基于服务器时间)
- 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 schema(ai04 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 gRPC(ai08 P3,提供 ExamService/HomeworkService/GradeService/AttendanceService/ClassService)
- iam gRPC(ai06 P2,GetUserInfo + GetEffectivePermissions + GetViewports)
- data-ana gRPC(ai11 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 gRPC(ai09 P4,TextbookService + ChapterService + KnowledgeGraphService)
- data-ana gRPC(ai11 P4,AnalyticsService.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 WebSocket(ai02 P5,`/ws` 端点)
- msg gRPC(ai10 P5,NotificationService)
- ai 服务 gRPC(ai12 P5,AiService.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 AA:eslint-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.1(P75)
- 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 Shell(ai13 P2) | exposes AppShell/GraphQLProvider/useGraphQLClient/useAuth/usePermission + shared singleton | P2 启动 | ⏳ 待 ai13 P2 |
| api-gateway HTTP :8080(ai01 P3) | `/api/v1/student/*` 反向代理 student-bff 可用 | P3 启动 | ⏳ 待 ai01 P3 |
| student-bff GraphQL(ai04 P3) | `POST /graphql` :3009 + 核心 Query/Mutation 可执行 | P3 启动 | ⏳ 待 ai04 P3 |
| student-bff GraphQL schema(ai04 + ISSUE-014-02 仲裁) | `packages/shared-ts/contracts/graphql/student-bff.graphql` 创建 | P3 启动 | ⏳ 待仲裁 |
| core-edu gRPC 50053(ai08 P3) | ExamService/HomeworkService/GradeService/AttendanceService/ClassService 全部 RPC | P3 启动 | ⏳ 待 ai08 P3 |
| iam gRPC 50052(ai06 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 50054(ai09 P4) | TextbookService + ChapterService + KnowledgeGraphService | P4 启动 | ⏳ 待 ai09 P4 |
| data-ana gRPC 50055(ai11 P4) | AnalyticsService.GetStudentWeakness + GetLearningTrend | P4 启动 | ⏳ 待 ai11 P4 |
| push-gateway WebSocket :8081/ws(ai02 P5) | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |
| msg gRPC 50056(ai10 P5) | NotificationService.ListNotifications + MarkAsRead | P5 启动 | ⏳ 待 ai10 P5 |
| ai 服务 gRPC 50057(ai12 P5,可选) | AiService.Chat(SSE 流式) | P5 启动 | ⏳ 待 ai12 P5 |
| 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-portal(ai13 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/mutation,schema 就绪后切换;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**: ai14(student-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 consumer(push-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 排期。
> **总工期**:28d(P2 8d + P3 5d + P4 4d + P5 8d + P6 3d),与 workline.md §1 总时间线对齐(批次1 8d + 批次2 5d + 批次4 8d + P4/P6 合并 7d)。
---
## §3 详细任务
### P2:GraphQL schema + 5 Query + DataLoader + ActionState
### P2:GraphQL 基础 + DownstreamClient + 越权防御(批次 1,8d)
> 裁决依据:president §3.1 Must Have 13 项(teacher-bff 无 DB,G10/G11 不适用,实际 11 项 + DownstreamClient + admin 预留)
#### 3.1 GraphQL schema 第一版 + admin 预留(2d,P0 阻塞 ai13)
- **负责人**:ai03
- **依赖**:coord 仲裁 ARB-001(已裁决)
- **交付物**:
- `packages/shared-ts/contracts/graphql/teacher-bff.graphql` — P2 schema
- Yoga endpoint `POST /graphql`
- 5 Query:dashboard / viewports / me / classes / class
- DataLoader + ActionState 信封 + 降级模式 B
- **依赖**:iam gRPC(ai06)+ coord 仲裁 ARB-001
- **验收标准**:5 Query 可用 + ActionState 信封 + depth ≤ 7
- **完整 P3-P6 任务**:⚠️ 由 ai03 自行补充
- `packages/shared-ts/contracts/graphql/teacher-bff.schema.graphql` — P2 schema(5 Query: dashboard/viewports/me/classes/class + admin 命名空间占位)
- admin 命名空间预留:schema 中声明 `admin` Query/Mutation 类型骨架(无实际 Resolver),P6 实现
- **验收标准**: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 Resolver(3d,P0 阻塞 ai13)
- **负责人**:ai03
- **依赖**:3.1 schema + iam gRPC 50052(ai06 P2.1)
- **交付物**:
- `POST /graphql` Yoga GraphQL endpoint
- 5 Query Resolver:dashboard / viewports / me / classes / class
- Dashboard Resolver P2 实现方式(ISSUE-032):P2 仅调 iam gRPC,未启用字段返回 null + `extensions.warning = "field_unavailable_in_p2"`
- **验收标准**:5 Query 可执行 + ActionState 信封 + 降级模式 B(success=true + error=null + data 内 degraded 字段)
- **裁决引用**:B1 + ARB-001 §1.4 + president §2.6/§2.8
#### 3.3 DownstreamClient 抽象 + iam gRPC(2d,P0 阻塞 ai04/ai05)
- **负责人**:ai03
- **依赖**:iam gRPC 50052(ai06 P2.1)
- **交付物**:
- `src/clients/` DownstreamClient 抽象层(B8:BFF 模式 v2 标准抽象,3 个 BFF 统一使用,回写 teacher-bff)
- IamClient gRPC 实现:`@grpc/grpc-js` + `@bufbuild/protobuf`,调 iam:50052
- gRPC interceptor:注入 trace context(traceparent)+ x-user-id metadata + metrics
- **验收标准**:IamClient gRPC 调 iam GetUserInfo/GetViewports/GetEffectiveAccess 成功
- **裁决引用**:B2(首次实现即 gRPC)+ B8(DownstreamClient 抽象)
#### 3.4 AuthorizationGuard 越权防御(1d,P0)
- **负责人**: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 信封 + 降级模式 B(1d)
- **负责人**: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 50052(teacher-bff 无 DB,2 项)
- **验收标准**:/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 liveness(G3) | ✅ 已具备 | health.controller.ts |
| GlobalErrorFilter(G8) | ✅ 已具备 | global-error.filter.ts |
| ESM import .js 后缀(G12) | ✅ 已具备 | 源码已用 .js |
| import type(G13) | ✅ 已具备 | 源码已用 import type |
| OTel tracer(G6) | ⚠️ Should Have | P2 可降级为 logger-only,P5 补全 |
| /metrics 业务指标(G5) | ⚠️ Should Have | P2 仅暴露 process metrics |
| DataLoader(B1) | ⚠️ Should Have | P2 可先用普通 resolver,P4 全量接入 |
| Redis 5-30s 短缓存(B6) | ⚠️ Should Have | P3 接入聚合缓存 |
| Zod 全量验证(G7) | ⚠️ Nice to Have | P2 先校验核心 Query,P6 全量 |
| 优雅关闭 SIGTERM(G9) | ⚠️ Nice to Have | P2 已有基础,P6 补全关闭顺序 |
**P2 退出标准**:POST /graphql 可用 + 5 Query Resolver + DownstreamClient + AuthorizationGuard + ActionState 信封 + /readyz 2 项探针 + 错误码 BFF_TEACHER_* + admin 命名空间预留。
---
### P3:core-edu gRPC 扩展(批次 2,5d)
> 裁决依据:§2.3 跨阶段扩展例外(新增下游 gRPC 调用 + AuthorizationGuard 内部实现替换 + /readyz 探针扩展)
#### 3.8 CoreEduClient gRPC(2d,P0)
- **负责人**:ai03
- **依赖**:core-edu gRPC 50053(ai08 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.9(P3 接入 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 项探针。
---
### P4:content + data-ana gRPC 扩展(4d)
> 裁决依据:§2.3 跨阶段扩展例外
#### 3.12 ContentClient + DataAnaClient gRPC(2d)
- **依赖**:content gRPC 50054(ai09 P4)+ data-ana gRPC 50055(ai11 P4)
- **交付物**:
- ContentClient gRPC:KnowledgeGraphService(GetPrerequisites / GetLearningPath)
- DataAnaClient gRPC:AnalyticsService(GetClassPerformance / 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-ana(5 项:Redis + iam + core-edu + content + data-ana)
**P4 退出标准**:content + data-ana gRPC + 5 Query + DataLoader 全量 + /readyz 5 项探针。
---
### P5:ai + msg gRPC 扩展 + SSE + Kafka(批次 4,8d)
> 裁决依据:B7(P5 push-gateway 落地后再订阅 Kafka)
#### 3.15 AiClient + MsgClient gRPC(2d)
- **依赖**:ai gRPC 50057(ai12 P5)+ msg gRPC 50056(ai10 P5)
- **交付物**:
- AiClient gRPC:AiService(Chat / StreamChat streaming / GenerateQuestion / OptimizeExpression)
- MsgClient gRPC:NotificationService(ListNotifications / SearchNotifications / MarkAsRead)
- GraphQL Query 扩展:notifications + Mutation:generateQuestion / markNotificationAsRead
#### 3.16 SSE 流式透传(2d)
- **交付物**:`GET /ai/chat/stream` SSE 端点(ai.StreamChat gRPC stream → BFF → 前端 EventSource)
- **验收标准**:SSE 三层透传端到端通 + 背压处理 + 超时取消
- **裁决引用**:02 文档 §10
#### 3.17 Kafka consumer(2d,push-gateway 落地后)
- **交付物**:
- Kafka consumer 订阅 `edu.identity.user.role_changed` / `edu.identity.role.updated`,精确失效 Redis 权限缓存
- 幂等性:基于 event_id 去重(Redis SETNX)
- **验收标准**:权限变更秒级缓存失效 + 幂等消费
- **裁决引用**:B7(P5 push-gateway 落地后再订阅)
#### 3.18 /readyz + ai + msg 探针 + Should Have 补全(2d)
- **交付物**:
- /readyz 探针扩展 ai + msg(7 项: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 补全。
---
### P6:admin 命名空间 + 硬化(3d)
> 裁决依据:president §5.1(admin-portal 复用 teacher-bff)+ P6 硬化
#### 3.19 admin 命名空间实现(1d)
- **依赖**:admin-portal(ai16 P6)
- **交付物**:
- admin schema 命名空间 Resolver 实现(P2 预留的占位类型填充实际 Resolver)
- admin Query/Mutation:用户管理 / 角色权限管理 / 学校设置 / 组织管理 / 审计日志查询
- **验收标准**:admin namespace 可内省 + admin-portal 可消费
- **裁决引用**:president §5.1(admin-portal 复用 teacher-bff GraphQL endpoint)
#### 3.20 熔断 / 重试 / 超时(1d)
- **交付物**:
- Circuit Breaker(opossum,per-downstream-service)
- Retry(gRPC interceptor,仅幂等 RPC,指数退避)
- Timeout(per-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 50052(ai06)+ core-edu gRPC 50053(ai08,P3+)
- **我的就绪信号**:POST /graphql 可用 + dashboard Query 返回正确数据
### 4.1 我依赖的上游就绪信号
| 上游 | 就绪信号 | 阶段 | 状态 |
| ---- | -------- | ---- | ---- |
| iam(ai06) | gRPC 50052 + 8 RPC(GetUserInfo/GetViewports/GetEffectivePermissions/GetEffectiveAccess/Logout/GetPublicKey/BatchGetUsers/GetChildrenByParent) | P2 | ⏳ |
| core-edu(ai08) | gRPC 50053 + ExamService/HomeworkService/GradeService | P3 | ⏳ |
| content(ai09) | gRPC 50054 + KnowledgeGraphService | P4 | ⏳ |
| data-ana(ai11) | gRPC 50055 + AnalyticsService(含 GetTeacherDashboard,ISSUE-027 补全) | P4 | ⏳ |
| ai(ai12) | gRPC 50057 + AiService(含 StreamChat) | P5 | ⏳ |
| msg(ai10) | gRPC 50056 + NotificationService | P5 | ⏳ |
| push-gateway(ai02) | /internal/push 落地(B7 Kafka 订阅前提) | P5 | ⏳ |
| admin-portal(ai16) | admin schema 需求确认 | P6 | ⏳ |
### 4.2 我的就绪信号(供下游消费)
| 阶段 | 就绪信号 | 消费方 |
| ---- | -------- | ------ |
| P2 | POST /graphql 可用 + 5 Query + admin 预留 | teacher-portal(ai13) |
| P2 | DownstreamClient 抽象(B8 回写) | student-bff(ai04)/ parent-bff(ai05) |
| P3 | exams/homework/grades Query + Mutation | teacher-portal(ai13) |
| P4 | 学情分析 Query + DataLoader | teacher-portal(ai13) |
| P5 | SSE 流式 + notifications Query | teacher-portal(ai13) |
| P6 | admin namespace 可用 | admin-portal(ai16) |
---
## §5 跨阶段扩展例外验收清单
> 裁决依据:president §2.3(ISSUE-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 query,iam 数据)
- 个人设置页(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 schema(ai03 + coord 仲裁 ISSUE-037) | packages/shared-ts/contracts/graphql/ 第一版 | P2 启动 | ⏳ 待 coord 仲裁 |
| api-gateway HTTP :8080(ai01) | /api/teacher/graphql + /api/auth/login 路由可用 | P2 启动 | ⏳ 待 ai01 |
| api-gateway HTTP :8080(ai01) | /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/ws(ai02 P5) | WS 连接可建立 + 推送可接收 | P5 启动 | ⏳ 待 ai02 P5 |

View File

@@ -7,6 +7,7 @@
> 日期:2026-07-09(v1 by ai06)/ 2026-07-10(v2 审核修订 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 重新分配给 ai11(ai 单独分配给 ai12),ai11 接手后对 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`)
- Redis(DataScope 缓存 5min + 预警去重位图 + CDC 幂等去重 SETNX,004 §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 统一响应信封 ActionState(Python 服务必须改 ActionState,degraded 作为 `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 服务必须改 ActionState,degraded 作为 `details.degraded` 子字段)。阶段 2 设计已对齐(见 02-architecture-design.md §4.3)。
**gRPC 契约**(analytics.proto,P4 启用 server,端口 50055):
@@ -113,11 +114,13 @@
| 事件 | Topic(004 §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` 直接发布(已豁免 Outbox,004 §15.3 #6 仲裁结论),下游 core-edu / msg 消费。失败重试 3 次仍失败落 `mastery_publish_failed` 本地表。
> **当前未实现发布**:data-ana 当前只消费不发布。掌握度计算完成后通过 `aiokafka.AIOKafkaProducer` 直接发布(已豁免 Outbox,coord-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)
- **缓存**:Redis(DataScope 缓存 5min 事件驱动失效 / CDC event_id 幂等去重 SETNX TTL 7d / 预警去重位图);学情宽表走 ClickHouse 实时,CDC 同步延迟 < 5s
## 4. 我的技术栈
@@ -145,7 +148,7 @@
- **交付物**(pending-features P4):DataAna 学情诊断宽表 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 写成绩到 MySQL(Debezium 监听 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 #5:iam 新增 `GetEffectiveDataScope` gRPC RPC(P4 补全);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 #5:P4 补全 | ✅ 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.1:P4 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 #3:iam 新增 `GetEffectiveDataScope` gRPC RPC(P4 补全);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 #3:P4 补全;**当前 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` 派生数据事件(豁免 Outbox,004 §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 级在查询层注入 WHERE;iam `GetEffectiveDataScope` gRPC(004 §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 级在查询层注入 WHERE;iam `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. **长远架构演进**:为 P5(ai 用量消费 / gRPC stream)、P6(CDC 水平扩展 / 容量规划 / 数据治理 / 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 RPC(3 现有 + 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=True,degraded 标记放 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) |
| 被调用 | ai(P5+) | 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-edu(CDC) | Kafka | `edu-cdc.next_edu_cloud.core_edu_grades/exams/homework_submissions/attendance` | 学情 + 考勤数据投递 |
| 消费 | core-edu(CDC) | Kafka | `edu-cdc.next_edu_cloud.classes` | 班级维度同步 |
| 消费 | iam(CDC) | Kafka | `edu-cdc.next_edu_cloud.iam_users` | 用户 dataScope 同步 |
| 消费 | content(CDC) | Kafka | `edu-cdc.next_edu_cloud.content_knowledge_points` | 知识点元数据同步(v2 新增) |
| 消费 | ai(P5+) | 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) | 掌握度更新通知 |
| 消费 | ai(P5+) | 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 API(004 §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 未实现此 RPC,P4 阻塞项**。若 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` topic(004 §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)是否合规 | ✅ 已仲裁:派生数据事件豁免 Outbox(004 §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. 非功能性需求(NFR,v2.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 RPC,v2 提案扩展 7 RPC |
| 路由 | ✅ | HTTP 13 端点 + gRPC 10 RPC |
| 契约(proto) | ✅ | analytics.proto 已定义 3 RPC,v2 提案扩展至 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 + /readyz(v2 补 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 端点计数 14;Dockerfile HEALTHCHECK 改用 urllib 避免 curl 依赖;新增 §19 NFR 章节
**v2 审核结论**: 16 项遗漏已全部补强(3 项 P0 + 5 项 P1 + 8 项 P2),文档进入实现阶段无阻塞(P4 阻塞项:iam.proto GetEffectiveDataScope + events.proto AIUsageEvent 待 coord 落实,可用 fallback/mock 兜底)