docs: ai 协作文档体系重构与多 ai 仲裁结果落地
1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md) 2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration) 3.各服务 01/02 文档补全 4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens) 5.Proto 契约补全 6.004 架构影响地图更新 7.端口分配表 8.设计规格文档
This commit is contained in:
199
apps/teacher-portal/docs/00-ai13-audit-report.md
Normal file
199
apps/teacher-portal/docs/00-ai13-audit-report.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# ai13 审核 report — teacher-portal 现有文档遗漏分析
|
||||
|
||||
> AI:ai13(TS/React · teacher-portal 正式负责人,依据 ai-allocation.md §3.2)
|
||||
> 阶段:阶段 1/2 文档审核 + 长远架构补全
|
||||
> 日期:2026-07-09
|
||||
> 审核对象:
|
||||
>
|
||||
> - [01-understanding.md](./01-understanding.md)(ai07 于 2026-07-09 产出)
|
||||
> - [02-architecture-design.md](./02-architecture-design.md)(ai07 于 2026-07-09 产出)
|
||||
>
|
||||
> 关联裁决:[coord 交叉审查报告](../../../docs/architecture/coord-cross-review.md) §4.1/§5.2/§5.5/§6(整改项 #2/#5/#17/#18)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.2/§4.2/§7.2/§11.4
|
||||
|
||||
---
|
||||
|
||||
## 1. 归属与版本背景
|
||||
|
||||
| 维度 | ai07 版本(被审核) | ai13 版本(本次修正) |
|
||||
| -------- | --------------------------------------------- | ----------------------------------------- |
|
||||
| AI 标识 | ai07 | ai13(ai-allocation.md §3.2 正式负责人) |
|
||||
| 文档日期 | 2026-07-09 | 2026-07-09(同日审核+补全) |
|
||||
| 文档分支 | docs/teacher-portal-stage1-stage2-design-ai07 | docs/teacher-portal-ai13-audit-supplement |
|
||||
| 触发原因 | ai07 临时承接 4 端合并设计 | ai13 接管 teacher-portal 正式所有权 |
|
||||
|
||||
> **背景说明**:ai-allocation.md §3.2 矩阵中 ai07 正式职责是 classes(黄金模板),ai13 才是 teacher-portal 的负责人。前期因并行加速,ai07 临时承担 4 端合并设计并产出 `apps/teacher-portal/README.md`(945 行)与 docs/01-02 两份文档。coord 已在 [coord-cross-review.md](../../../docs/architecture/coord-cross-review.md) §6 整改清单 #2/#17/#18 中列出多项需修正项,但整改责任原标注为 ai07。本次 ai13 正式接管并一次性完成审核 + 修正 + 长远架构补全。
|
||||
|
||||
---
|
||||
|
||||
## 2. 需修正项(来自 coord 裁决)
|
||||
|
||||
### 2.1 P0 端口冲突修正(coord §4.1,整改 #2)
|
||||
|
||||
| 项 | ai07 原文档 | 正确值 | 依据 |
|
||||
| -------------- | -------------------------- | ------------------------------------------------------------- | ---------------------------------- |
|
||||
| teacher-portal | 3000 | **4000** | 004 §1.2 + project_memory 端口约束 |
|
||||
| student-portal | 3001 | **4001** | 004 §1.2 |
|
||||
| parent-portal | 3002 | **4002** | 004 §1.2 |
|
||||
| admin-portal | 3003 | **4003** | 004 §1.2 |
|
||||
| MF remoteEntry | `localhost:3001/3002/3003` | `localhost:4001/4002/4003` | 与端口矩阵一致 |
|
||||
| 后端冲突说明 | "3001-3003 已被避让" | 删除(错误描述,3001-3003 仍被 classes/iam/teacher-bff 占用) | coord §4.1 表 |
|
||||
|
||||
### 2.2 P1 端点契约对齐(coord §6 整改 #17,4 处端点不一致)
|
||||
|
||||
| ai07 原文档 | 正确值(以后端服务为准) | 依据 |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------- | -------------- |
|
||||
| `GET /api/v1/ai/generate-questions`(SSE) | `POST /api/v1/ai/chat/stream`(SSE)+ `POST /api/v1/ai/generate/question` | coord §3 表 #1 |
|
||||
| `GET /iam/effective-permissions` | `GET /iam/permissions/effective` | coord §3 表 #2 |
|
||||
| `GET /iam/rbac/...` | `GET /iam/viewports`、`GET /iam/roles`、`GET /iam/permissions/*` | coord §3 表 #3 |
|
||||
| `POST /parent/switch-child`(家长端,不影响 teacher-portal) | `POST /parent/children/:childId/select` | coord §3 表 #4 |
|
||||
|
||||
### 2.3 P1 错误码前缀期望修正(coord §5.5,整改 #18)
|
||||
|
||||
| ai07 原文档 | 正确值 | 依据 |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------- | ---------- |
|
||||
| 期望 `EXAMS_` / `HOMEWORK_` / `GRADES_` 子前缀 | 删除,统一为 `CORE_EDU_*`(classes 模块保留 `CLASSES_*` 黄金模板遗留) | coord §5.5 |
|
||||
| BFF 错误码前缀 `BFF_TEACHER_` | 保留(coord §5.2 已裁决 3 BFF 统一为 `BFF_XXX_` 风格) | coord §5.2 |
|
||||
| 期望 `GW_` 前缀 | 保留(coord §5.4 已裁决 api-gateway 加 `GW_` 前缀) | coord §5.4 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 长远架构遗漏分析(ai13 新增)
|
||||
|
||||
ai07 原文档已覆盖阶段 1/2 模板要求(位置/限界/契约/技术栈/分层图/领域模型/数据模型/API/事件/横切/交互/风险),但**未充分覆盖以下"长远、全面、面向未来"的架构维度**。这些维度是通用前端架构标准(Next.js + Module Federation + 企业级 SaaS)必备项,本次审核需补全。
|
||||
|
||||
### 3.1 演进路线缺失
|
||||
|
||||
| 维度 | ai07 原文档 | 缺失内容 |
|
||||
| ------------------- | --------------------- | ------------------------------------------------- |
|
||||
| 当前 → MF 迁移路径 | 仅描述目标态 | 当前单端部署如何演进到 Shell+Remote 的分步路径 |
|
||||
| 阶段间能力累积 | 仅 P2/P5/P6 三段 | P2/P3/P4/P5/P6 每阶段 teacher-portal 必备能力清单 |
|
||||
| 后端契约演进对齐 | 无 | 后端 P3 启用 gRPC 后前端是否需要切换、何时切换 |
|
||||
| REST → GraphQL 切换 | 提一句"待 coord 仲裁" | 切换的触发条件、迁移策略、灰度方案 |
|
||||
|
||||
### 3.2 安全架构缺失
|
||||
|
||||
| 维度 | ai07 原文档 | 缺失内容 |
|
||||
| ------------------- | ----------------- | --------------------------------------------------------------- |
|
||||
| Token 存储安全 | 仅说 localStorage | localStorage 的 XSS 风险 + httpOnly Cookie 替代方案 + CSRF 防护 |
|
||||
| CSP(内容安全策略) | 无 | Next.js `csp()` / `nonce()` 配置,限制 script/style/img 来源 |
|
||||
| XSS 防护 | 仅提 DOMPurify | Tiptap 富文本的 XSS 防护、`dangerouslySetInnerHTML` 白名单 |
|
||||
| CSRF 防护 | 无 | 若改用 Cookie 鉴权需 CSRF Token / SameSite |
|
||||
| 点击劫持防护 | 无 | X-Frame-Options: DENY + CSP frame-ancestors |
|
||||
| 子资源完整性 SRI | 无 | MF remoteEntry.js 的 SRI 校验、CDN 静态资源 integrity |
|
||||
| 敏感数据脱敏 | 无 | 日志/错误上报中 token/PII 脱敏 |
|
||||
|
||||
### 3.3 性能架构缺失
|
||||
|
||||
| 维度 | ai07 原文档 | 缺失内容 |
|
||||
| ------------------- | -------------- | ------------------------------------------------------------------ |
|
||||
| 性能预算(Budget) | 无 | JS bundle ≤ 200KB / CSS ≤ 50KB / LCP ≤ 2.5s / CLS ≤ 0.1 等具体目标 |
|
||||
| 路由级代码分割 | 无 | Next.js App Router 默认按路由分割 + 动态 import 重型组件 |
|
||||
| SSR/CSR 决策矩阵 | 仅提"优先 CSR" | 每类页面(dashboard/exam-taking/lesson-prep)的 SSR/CSR 选择标准 |
|
||||
| 字体加载策略 | 仅说 next/font | FOUT vs FOIT 选择、preload、font-display: swap |
|
||||
| 图片优化 | 无 | next/image + AVIF/WebP + 响应式 sizes + lazy load |
|
||||
| MF 共享依赖版本漂移 | 仅说 singleton | 共享依赖版本协商机制、peer deps 警告 |
|
||||
| Prefetch / Preload | 无 | 路由 prefetch 策略、关键资源 preload |
|
||||
| 长任务优化 | 无 | React 18 concurrent features、useTransition、useDeferredValue |
|
||||
|
||||
### 3.4 可观测性深度缺失
|
||||
|
||||
| 维度 | ai07 原文档 | 缺失内容 |
|
||||
| -------------- | -------------------- | ---------------------------------------------------------------- |
|
||||
| 错误上报 | 提"P6 接入 Sentry" | Sentry DSN 配置、release tracking、source maps、采样率、PII 过滤 |
|
||||
| 性能监控 | 仅列 4 项 Web Vitals | RUM(Real User Monitoring)方案、长任务监听、卡顿追踪 |
|
||||
| 用户行为分析 | 无 | 哪些事件需要埋点、埋点规范、隐私合规 |
|
||||
| Session Replay | 无 | Sentry Replay / LogRocket 选型、隐私区域遮罩 |
|
||||
| 前端日志聚合 | 仅 console | 浏览器日志如何汇聚到 Loki(pino-browser → Gateway → Loki) |
|
||||
| trace_id 透传 | 仅提"从响应头提取" | 浏览器入口生成 trace_id、写入后续请求 header、跨 MF 透传 |
|
||||
|
||||
### 3.5 a11y 实施细节缺失
|
||||
|
||||
| 维度 | ai07 原文档 | 缺失内容 |
|
||||
| ---------------- | --------------- | ------------------------------------------ |
|
||||
| WCAG 2.2 AA 清单 | 仅列工具集 | 逐条对照 WCAG 2.2 AA 13 项准则 + 实施方式 |
|
||||
| 键盘导航 | 仅提 focus-trap | 跳过链接、Tab 顺序、可见焦点、快捷键 |
|
||||
| 屏幕阅读器测试 | 无 | NVDA/JAWS/VoiceOver 测试矩阵 + ARIA 模式 |
|
||||
| 颜色对比度 | 无 | 自动化检测(axe-core)+ 设计令牌对比度审计 |
|
||||
| 移动端 a11y | 无 | 触摸目标 ≥ 44px、缩放支持、手势替代 |
|
||||
|
||||
### 3.6 i18n 实施细节缺失
|
||||
|
||||
| 维度 | ai07 原文档 | 缺失内容 |
|
||||
| -------------- | --------------- | ------------------------------------------------------ |
|
||||
| next-intl 配置 | 仅提"next-intl" | locale 路由策略(前缀 vs 域名)、SSR i18n、locale 检测 |
|
||||
| fallback 策略 | 无 | 缺失翻译时回退到默认语言、缺失 key 在 dev 环境警告 |
|
||||
| 复数/性别 | 无 | ICU MessageFormat 复数规则、性别变化 |
|
||||
| 日期/数字格式 | 无 | Intl.DateTimeFormat / Intl.NumberFormat 按 locale |
|
||||
| RTL 支持 | 无 | 是否需要支持阿拉伯语/希伯来语(影响布局) |
|
||||
| 翻译协作流程 | 无 | 翻译文件位置、协作工具(Crowdin/Transifex)、CI 校验 |
|
||||
|
||||
### 3.7 部署拓扑缺失
|
||||
|
||||
| 维度 | ai07 原文档 | 缺失内容 |
|
||||
| ------------- | ---------------- | ---------------------------------------------------- |
|
||||
| 部署目标 | 仅说 Docker | Docker Compose(P1-P5)→ K8s(P6)演进路径 |
|
||||
| CDN/Edge 缓存 | 无 | Next.js 静态资源 CDN、edge middleware、ISR 缓存 |
|
||||
| 多环境配置 | 无 | dev/staging/prod 环境变量管理、Next.js runtimeConfig |
|
||||
| 健康检查端点 | 仅说 /api/health | /api/health vs /api/ready 区别、K8s probe 配置 |
|
||||
| 灰度发布 | 无 | MF remoteEntry 版本协商、灰度发布策略 |
|
||||
| 回滚策略 | 仅说 git revert | 前端回滚特殊性:CDN 缓存失效、用户会话保持 |
|
||||
|
||||
### 3.8 测试策略细节缺失
|
||||
|
||||
| 维度 | ai07 原文档 | 缺失内容 |
|
||||
| -------------- | --------------- | ------------------------------------------------------- |
|
||||
| 单元测试 | 仅说 Vitest | 测试什么(hooks/utils/pure components)、mocking 策略 |
|
||||
| 集成测试 | 无 | 组件树集成、API mock(MSW)、TanStack Query 集成测试 |
|
||||
| E2E 测试 | 仅说 Playwright | 关键路径(登录/dashboard/班级 CRUD)、CI 并发、稳定策略 |
|
||||
| 视觉回归测试 | 无 | Storybook + Chromatic / Percy 选型 |
|
||||
| 覆盖率目标分层 | 仅说 ≥80% | hooks ≥90% / utils ≥90% / components ≥80% / pages ≥60% |
|
||||
| 性能测试 | 无 | Lighthouse CI / WebPageTest 集成 |
|
||||
|
||||
### 3.9 文件/目录结构目标态缺失
|
||||
|
||||
ai07 原文档 §6/§7 列出共享组件库/Hooks/令牌的"待建包",但**未给出 teacher-portal 自身的目录结构目标态**(当前 vs 目标对比)。
|
||||
|
||||
### 3.10 移动端策略缺失
|
||||
|
||||
ai07 原文档未提及:
|
||||
|
||||
- 是否需要响应式适配(教师在家备课用手机查看)?
|
||||
- 是否需要 PWA / Service Worker(离线批改作业)?
|
||||
- 是否需要后续原生 App(React Native / Electron 桌面端)?
|
||||
- 移动端手势、触摸目标规范
|
||||
|
||||
### 3.11 MF 演进策略缺失
|
||||
|
||||
ai07 原文档描述了 MF 2.0 目标态,但**未回答关键演进问题**:
|
||||
|
||||
- 当前 teacher-portal 是单端部署(无 MF),何时引入 MF?
|
||||
- 引入 MF 后如何保持现有路由不被破坏?
|
||||
- MF 失败时的降级方案(remoteEntry.js 加载失败怎么办)?
|
||||
- MF 版本协商(Shell 与 Remote 的合约版本)?
|
||||
|
||||
### 3.12 数据流与乐观更新缺失
|
||||
|
||||
ai07 原文档 §4.2 提了 TanStack Query mutation,但**未覆盖**:
|
||||
|
||||
- 乐观更新策略(提交作业、批改成绩时如何乐观更新 UI)
|
||||
- 冲突解决(多教师同时批改同一作业)
|
||||
- 失败回滚机制
|
||||
- 缓存失效链路(mutate A 后哪些 B/C/D 需要失效)
|
||||
|
||||
---
|
||||
|
||||
## 4. 本次审核产出
|
||||
|
||||
ai13 本次审核产出 **3 份文档**:
|
||||
|
||||
1. **本报告**:`apps/teacher-portal/docs/00-ai13-audit-report.md`(审核报告,永久保留作为审核凭证)
|
||||
2. **更新** `apps/teacher-portal/docs/01-understanding.md`:修正归属/端口/端点/前缀,补充长远维度
|
||||
3. **更新** `apps/teacher-portal/docs/02-architecture-design.md`:修正归属/端口/端点/前缀,补充横切与演进章节
|
||||
4. **新建** `apps/teacher-portal/docs/03-long-term-architecture.md`:长远架构补全(演进/安全/性能/可观测/a11y/i18n/部署/测试/移动端/MF 演进)
|
||||
|
||||
---
|
||||
|
||||
**AI Agent**: ai13 (teacher-portal)
|
||||
**Branch**: docs/teacher-portal-ai13-audit-supplement
|
||||
**Coordinator**: coord-ai
|
||||
**Audited**: ai07 stage1+2 outputs (2026-07-09)
|
||||
770
apps/teacher-portal/docs/03-long-term-architecture.md
Normal file
770
apps/teacher-portal/docs/03-long-term-architecture.md
Normal file
@@ -0,0 +1,770 @@
|
||||
# teacher-portal 长远架构补全
|
||||
|
||||
> AI:ai07(4 端合并设计,承接 ai13 审核 §3 长远遗漏补全)
|
||||
> 阶段:阶段 2 补充交付物(长远架构)
|
||||
> 日期:2026-07-09
|
||||
> 关联:
|
||||
>
|
||||
> - [02-architecture-design.md](./02-architecture-design.md)(ai13 修正版,阶段 2 主文档)
|
||||
> - [00-ai13-audit-report.md](./00-ai13-audit-report.md) §3 长远遗漏分析
|
||||
> - [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) §1.1a/§3.1/§5.4
|
||||
> - [project_rules](../../../.trae/rules/project_rules.md) §3.10/§4
|
||||
>
|
||||
> 本文档承接 ai13 在 [00-ai13-audit-report.md](./00-ai13-audit-report.md) §3 列出的 12 项长远遗漏,逐一补全。原则:**架构不堵死未来演进路径**,当前不实现的也要预留接入点。
|
||||
|
||||
---
|
||||
|
||||
## 1. 演进与迁移策略(补 ai13 §3.1 / §3.11)
|
||||
|
||||
### 1.1 阶段能力累积矩阵
|
||||
|
||||
| 阶段 | teacher-portal 必备能力 | 依赖上游 | 退出标准 |
|
||||
| ---- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | ------------------------------------------------------------- |
|
||||
| P2 | 登录 + AppShell + 视口导航 + classes CRUD + GraphQL client(urql,F9 裁决 P2 起) + 设计令牌三层 + ErrorBoundary + /api/health&ready | iam、teacher-bff、classes | 教师登录看到按角色的侧边栏 + 班级 CRUD |
|
||||
| P3 | 考试/作业/成绩页面 + MF Shell 配置就绪(暂无 Remote)+ 乐观更新 + 多 Tab 会话同步 | core-edu(exams/homework/grades) | 教师创建考试→学生(student-portal Remote)作答→教师批改全链路 |
|
||||
| P4 | 知识图谱页 + 学情分析页 + parent-portal Remote 接入 | content、data-ana、parent-bff | 教师查看知识图谱 + 学情宽表 5s 返回 |
|
||||
| P5 | WebSocket 推送 + AI 辅助出题 SSE + 通知中心 | msg、push-gateway、ai | 全校广播实时到达 + AI 流式出题 |
|
||||
| P6 | Sentry + RUM + OTel browser + A11y 审计 + 性能优化 + localStorage→httpOnly Cookie 迁移 | 全链路可观测 | 99.9% 可用 + WCAG 2.2 AA |
|
||||
|
||||
### 1.2 后端 gRPC 启用对前端的影响(补 ai13 §3.1)
|
||||
|
||||
004 §4.2 裁决各服务分阶段启用 gRPC(P3 iam+core-edu、P4 content+data-ana、P5 ai)。**前端不直接消费 gRPC**,影响只在 BFF 层。F9 裁决 BFF P2 起即用 GraphQL(非"P4 起 REST→GraphQL 迁移"),前端 P2 起即引入 urql client。
|
||||
|
||||
| 后端切换 | BFF 动作 | 前端动作 |
|
||||
| ---------------------------- | --------------------------------------------- | -------------------------------------------- |
|
||||
| 服务 HTTP→gRPC | BFF 改为 gRPC client 调用服务 | 无感(BFF 仍对前端暴露 GraphQL) |
|
||||
| BFF GraphQL(P2 起,已裁决) | teacher-bff P2 起即 GraphQL Yoga + DataLoader | 前端 P2 起即引入 urql client,统一走 GraphQL |
|
||||
|
||||
**前端契约**:始终消费 `ActionState` 信封(004 §11.5),与传输协议无关。BFF GraphQL schema 由 ai03 维护,coord 在批次 1 启动前仲裁第一版(ISSUE-037)。
|
||||
|
||||
### 1.3 单体 → MF 迁移路径(绞杀者模式,补 ai13 §3.11)
|
||||
|
||||
```
|
||||
当前态 过渡态 目标态
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||
│ teacher-portal │ │ teacher-portal │ │ teacher-portal │
|
||||
│ 单体 Next.js │ ──> │ Shell + MF 配置 │ ──> │ Shell │
|
||||
│ 5 个页面 │ │ (暂无 Remote) │ │ + student Remote │
|
||||
│ │ │ + student-portal │ │ + parent Remote │
|
||||
│ │ │ 作为首个 Remote │ │ + admin Remote │
|
||||
└─────────────┘ └──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
| 步骤 | 动作 | 回滚 | 验证 |
|
||||
| ---- | ------------------------------------------------------------------------------ | -------------------------------------------------------- | ------------------------- |
|
||||
| 1 | 升级 Next.js 14→15 + react 18→19 | git revert | classes CRUD 端到端 |
|
||||
| 2 | 抽取 ui-tokens/ui-components/hooks 到 packages | 引用回退到本地 | 视觉无变化 |
|
||||
| 3 | teacher-portal 加 NextFederationPlugin(Shell,exposes AppShell,暂无 Remote) | 删除 plugin 配置 | 现有页面不受影响 |
|
||||
| 4 | student-portal(P3)接入为首个 Remote | feature flag `NEXT_PUBLIC_MF_ENABLED=false` 回退单体路由 | 学生作答页在 Shell 内渲染 |
|
||||
| 5 | parent/admin 逐步接入 | 同上 | 各端独立可访问 |
|
||||
|
||||
**关键**:每步用 feature flag 控制,失败可一键回退到单体路由;保留旧路由文件直到 MF 稳定(2 个版本周期)。
|
||||
|
||||
### 1.4 GraphQL 客户端架构(P2 起最终方案,补 ai13 §3.1)
|
||||
|
||||
> 本节原为"REST → GraphQL 切换策略"(过渡方案描述)。按总裁裁决 F9(P2 起 BFF 用 GraphQL)+ §4.4(删除过渡方案本身,保留设计决策记录)+ ISSUE-036-ai13 回写义务,已替换为最终方案描述。
|
||||
|
||||
**裁决依据**:coord-final-decisions F9 + 总裁裁决 §2.17(MF AppShell GraphQL client 单例,采纳方案 A:Shell 暴露 GraphQLProvider)。
|
||||
|
||||
#### 1.4.1 client 选型与单例
|
||||
|
||||
| 维度 | 方案 |
|
||||
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| client 库 | urql(轻量、可插拔 exchange、TS 友好;备选 apollo,但 urql bundle 更小,符合 §3.1 性能预算) |
|
||||
| 单例归属 | teacher-portal Shell 暴露 `GraphQLProvider`(@edu/mf-contracts 登记),student/parent/admin Remote 复用 Shell 单例(总裁 §2.17 方案 A) |
|
||||
| MF shared 配置 | `react` / `urql` / `graphql` 设为 MF singleton,避免 Remote 各自初始化导致多实例 + 缓存不一致 |
|
||||
| 缓存交换 | urql `cacheExchange`(文档缓存 +normalized 可选);MF 下 Shell 单例 cache 跨 Remote 共享 |
|
||||
|
||||
#### 1.4.2 client 初始化(P2 起)
|
||||
|
||||
```typescript
|
||||
// teacher-portal Shell:packages/hooks 提供 useGraphQLClient,Shell 注入 GraphQLProvider
|
||||
import { createClient, Client } from "urql";
|
||||
import { cacheExchange, fetchExchange } from "urql";
|
||||
|
||||
const client: Client = createClient({
|
||||
url: process.env.NEXT_PUBLIC_TEACHER_BFF_GRAPHQL_URL!, // teacher-bff /graphql endpoint
|
||||
fetchOptions: () => {
|
||||
const token = readAuthToken(); // useAuth 注入
|
||||
const traceId = readTraceId();
|
||||
return {
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
"X-Trace-Id": traceId,
|
||||
},
|
||||
};
|
||||
},
|
||||
exchanges: [cacheExchange, fetchExchange],
|
||||
});
|
||||
```
|
||||
|
||||
- Shell 在 `app/providers.tsx` 包裹 `<Provider value={client}>`,所有 Remote 通过 Shell 暴露的 `useGraphQLClient()` 取同一 client
|
||||
- token 刷新/401 处理由 useAuth 统一拦截(不污染 urql exchange 链),见 §2.7
|
||||
|
||||
#### 1.4.3 错误处理与 ActionState 契约
|
||||
|
||||
| 场景 | 处理 |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| GraphQL 错误(errors[]) | 解析 `extensions.code`(BFF_TEACHER_* 前缀,G14 裁决),映射到 i18n key `error.teacher_bff.<code_snake>`(F11) |
|
||||
| 网络错误 | 重试 1 次 → 提示网络异常 → 记录 trace_id |
|
||||
| 401 | useAuth 静默 refresh 一次 → 重试;失败跳登录(见 §2.7) |
|
||||
| 部分失败(data + errors 共存) | 渲染 data 可用部分 + errors 区域提示,不整体失败 |
|
||||
|
||||
- BFF 始终返回 `ActionState` 信封(004 §11.5),前端统一走 `normalizeActionState()` 工具(packages/hooks)
|
||||
|
||||
#### 1.4.4 缓存策略
|
||||
|
||||
| 数据类型 | staleTime | 失效策略 |
|
||||
| -------------- | ----------------- | --------------------------------------------- |
|
||||
| 班级列表/详情 | 30s | mutation 后 invalidate(见 §10.2) |
|
||||
| 考试/作业/成绩 | 10s | mutation 后 invalidate + 乐观更新(见 §10.1) |
|
||||
| 用户权限/视口 | 0(always fetch) | 角色变更推送后强制 invalidate |
|
||||
| Dashboard 概览 | 60s | ISR + 客户端 stale-while-revalidate |
|
||||
|
||||
- urql 默认 document cache 适合简单场景;复杂关联(如班级↔考试↔成绩)P3 起评估切 `@urql/exchange-graphcache` normalized cache
|
||||
- 不引入 TanStack Query 做 GraphQL 缓存(urql 自带缓存足够,避免双缓存层);TanStack Query 仅用于非 GraphQL 场景(文件上传/SSE,见 useApi)
|
||||
|
||||
#### 1.4.5 与 ISSUE-037/038/039 的关联
|
||||
|
||||
| ISSUE | 关联点 | 状态 |
|
||||
| -------------- | ----------------------------------------------------------------------- | ------------- |
|
||||
| ISSUE-037-ai13 | teacher-bff GraphQL schema 第一版由 ai03 维护,coord 批次 1 启动前仲裁 | 待 coord 仲裁 |
|
||||
| ISSUE-038-ai13 | MF AppShell 暴露 GraphQL client 单例(总裁 §2.17 方案 A 已采纳) | 已裁决 |
|
||||
| ISSUE-039-ai13 | packages(含 hooks 中的 GraphQL client 工具)建立时机:批次 0.15 已建立 | 已执行 |
|
||||
|
||||
#### 1.4.6 设计决策记录(原过渡方案追溯)
|
||||
|
||||
> 本节为决策追溯,非过渡方案描述。
|
||||
|
||||
- **决策**:P2 起 BFF 即 GraphQL,前端即 urql,不经过 REST 中间态
|
||||
- **裁决来源**:coord F9 + 总裁 §0.3"不分阶段"原则 + §2.17 MF GraphQL client 单例
|
||||
- **原过渡方案**(已废弃):曾考虑 P2-P4 REST + P5+ 评估迁移 GraphQL + feature flag 灰度。该方案违反"不分阶段"原则,已删除
|
||||
- **影响范围**:02-architecture-design.md(已回写)+ 03-long-term-architecture.md §1.4(本次回写)
|
||||
|
||||
---
|
||||
|
||||
## 2. 安全架构(补 ai13 §3.2)
|
||||
|
||||
### 2.1 Token 存储演进
|
||||
|
||||
| 阶段 | access token | refresh token | 风险等级 |
|
||||
| ------- | -------------------------------------- | ------------------------------------------ | -------------- |
|
||||
| P2 过渡 | localStorage(Zustand 持久化) | localStorage | 高(XSS 可读) |
|
||||
| P6 终态 | 内存(Zustand sessionSlice,刷新丢失) | httpOnly + Secure + SameSite=Strict cookie | 极低 |
|
||||
|
||||
**切换条件**:iam 支持 refresh cookie 端点 + CSRF 防护就绪后迁移。
|
||||
|
||||
### 2.2 CSP(内容安全策略)
|
||||
|
||||
MF 下 CSP 必须显式允许 remote origin。Next.js 通过 `next.config.js` headers 或 middleware.ts 注入:
|
||||
|
||||
```
|
||||
Content-Security-Policy:
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline' http://localhost:4001 http://localhost:4002 http://localhost:4003;
|
||||
connect-src 'self' http://localhost:8080 ws://localhost:8081;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: blob:;
|
||||
font-src 'self' data:;
|
||||
frame-ancestors 'none';
|
||||
base-uri 'self';
|
||||
object-src 'none';
|
||||
```
|
||||
|
||||
- 生产环境 remote origin 改对应域名,逐步移除 `'unsafe-inline'`(用 nonce/hash 替代)
|
||||
- 配套头:`X-Content-Type-Options: nosniff`、`X-Frame-Options: DENY`(防点击劫持)、`Referrer-Policy: strict-origin-when-cross-origin`、`Permissions-Policy: geolocation=(), microphone=(), camera=()`
|
||||
|
||||
### 2.3 MF Remote 完整性校验
|
||||
|
||||
Remote `remoteEntry.js` 由 Shell 动态加载,存在"被篡改 remote 注入恶意代码到 Shell 上下文"风险(MF 共享 singleton 使 remote 可访问 Shell 全局状态)。
|
||||
|
||||
| 缓解措施 | 说明 |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| Subresource Integrity(SRI) | remoteEntry.js 计算 SHA-384,Shell 加载时校验(MF 2.0 支持 `integrity` 字段) |
|
||||
| Remote origin 白名单 | CSP `script-src` 限定已知 remote 域名 |
|
||||
| 最小暴露面 | Shell `exposes` 仅暴露必要组件,不暴露整个 store/ApiClient 内部 |
|
||||
| Remote 隔离 | Remote 不直接持有 Shell 的 token,统一通过 `useApi()` Hook 取 ApiClient(token 注入在 Hook 层) |
|
||||
| CDN 静态资源 SRI | next/image、next/font 产物带 integrity 属性 |
|
||||
|
||||
### 2.4 CSRF 防护(切换 cookie 后)
|
||||
|
||||
- SameSite=Strict cookie 阻断跨站携带
|
||||
- 对 state-changing mutation(POST/PUT/DELETE)额外校验 `X-CSRF-Token` header(双提交 cookie 模式)
|
||||
- Next.js middleware.ts 校验 origin header 与允许域名匹配
|
||||
|
||||
### 2.5 多 Tab 会话同步(关键遗漏补齐)
|
||||
|
||||
教师多 Tab 操作时,登出/角色变更须跨 Tab 同步,否则出现"Tab A 登出但 Tab B 仍可用"的越权窗口。
|
||||
|
||||
| 事件 | 同步机制 | 动作 |
|
||||
| ---------- | ------------------------------------------------------- | ----------------------------------------- |
|
||||
| 登出 | `BroadcastChannel('edu-session')` postMessage('logout') | 所有 Tab 清 session + 跳登录 |
|
||||
| Token 刷新 | BroadcastChannel('token-refreshed') | 其他 Tab 更新内存 token |
|
||||
| 角色变更 | WebSocket 推送(P5)+ BroadcastChannel | 所有 Tab invalidate 权限缓存 + 重渲染视口 |
|
||||
| 会话过期 | BroadcastChannel('session-expired') | 跳登录 + 提示 |
|
||||
|
||||
降级:不支持 BroadcastChannel 的浏览器走 `localStorage` `storage` 事件。
|
||||
|
||||
### 2.6 XSS / 注入防护
|
||||
|
||||
- `dangerouslySetInnerHTML` 全局禁用(project_rules §4),AI 流式内容先用 DOMPurify 清洗再渲染
|
||||
- Tiptap 内容序列化为 JSON 存储,渲染走 ProseMirror schema(白名单节点),禁止 raw HTML
|
||||
- 用户输入展示统一走 React 转义
|
||||
|
||||
### 2.7 鉴权边界与权限失效
|
||||
|
||||
| 场景 | 前端动作 |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| access token 过期 | 401 → 静默 refresh 一次 → 重试;失败跳登录 |
|
||||
| refresh token 过期 | 跳登录 + 提示"会话已过期" |
|
||||
| 角色被撤销 | WebSocket 推送 `RoleRevoked` → invalidate 权限 → 重新拉取 → 视口收敛;权限不足以访问当前路由 → 重定向到 dashboard |
|
||||
| 强制下线 | iam 调 push-gateway 推 `ForceLogout` → 前端清 session |
|
||||
|
||||
### 2.8 敏感数据脱敏(补 ai13 §3.2)
|
||||
|
||||
- 日志/错误上报前过滤:token、password、refreshToken、PII(身份证/手机号)字段置 `[REDACTED]`
|
||||
- Sentry 配置 `beforeSend` 钩子剥离 PII;Session Replay 对敏感输入框 `mask` 所有文本
|
||||
- 前端不持久化密码原文,登录后立即从内存清除
|
||||
|
||||
---
|
||||
|
||||
## 3. 性能架构(补 ai13 §3.3)
|
||||
|
||||
### 3.1 性能预算
|
||||
|
||||
| 指标 | 目标 | 度量 | CI 门禁 |
|
||||
| ----------------------- | ----------------- | ----------------------------- | --------------- |
|
||||
| LCP | < 2.5s(P75) | Web Vitals RUM | lighthouse CI |
|
||||
| INP | < 200ms(P75) | Web Vitals RUM(CWV 2024) | lighthouse CI |
|
||||
| CLS | < 0.1 | Web Vitals RUM | lighthouse CI |
|
||||
| TTFB | < 800ms | Web Vitals RUM | — |
|
||||
| 长任务(>50ms) | < 5 次/页 | PerformanceObserver Long Task | — |
|
||||
| Shell JS bundle(gzip) | < 150KB | @next/bundle-analyzer | size-limit fail |
|
||||
| 每个 Remote JS(gzip) | < 80KB | bundle analyzer | size-limit fail |
|
||||
| CSS(gzip) | < 50KB | bundle analyzer | — |
|
||||
| shared deps 重复 | 0(MF singleton) | MF manifest 校验 | CI 检查 |
|
||||
| 首屏请求数 | < 20 | lighthouse CI | — |
|
||||
|
||||
- CI 集成 `@next/bundle-analyzer` + size-limit,超预算 fail build
|
||||
- 每版本 lighthouse CI 报告归档,回归监控
|
||||
|
||||
### 3.2 渲染模式决策矩阵
|
||||
|
||||
| 页面类型 | 模式 | 理由 |
|
||||
| -------------------------- | ------------------------------- | ---------------------------- |
|
||||
| 登录页 | SSR | SEO + 首屏快 |
|
||||
| Dashboard 概览 | SSR + Streaming | 首屏骨架 + Suspense 流式填充 |
|
||||
| 列表/表格(classes/exams) | CSR + TanStack Query | 强交互、筛选、分页 |
|
||||
| 详情页(成绩/学情) | ISR(revalidate 60s)+ CSR 交互 | 数据准实时,CDN 缓存降负载 |
|
||||
| AI 出题(SSE) | CSR | 长连接流式 |
|
||||
| 知识图谱可视化 | CSR | 交互重 |
|
||||
| 管理端配置 | CSR | 纯交互 |
|
||||
| 考试作答(student) | CSR | 防刷新丢答 + 离线缓存 |
|
||||
|
||||
### 3.3 Next.js 缓存层级(App Router)
|
||||
|
||||
| 缓存层 | 用途 | 失效策略 |
|
||||
| ------------------------- | -------------------- | --------------------- |
|
||||
| Request Memoization | 单次请求内去重 fetch | 请求结束即失效 |
|
||||
| Data Cache(fetch cache) | 跨请求 fetch 结果 | revalidate / tag 失效 |
|
||||
| Full Route Cache | 静态路由产物 | 按路由 revalidate |
|
||||
| Router Cache(客户端) | RSC payload + 预取 | staleTime 30s |
|
||||
|
||||
**约定**:BFF 数据 `fetch(..., { next: { revalidate: 30, tags: ['classes'] } })`;mutation 后 `revalidateTag('classes')`。
|
||||
|
||||
### 3.4 Server / Client Component 边界
|
||||
|
||||
- **Server Component**:数据获取、静态展示、设计令牌注入、i18n Provider
|
||||
- **Client Component**:交互(onClick/state)、TanStack Query、Zustand、Tiptap、图表
|
||||
- 边界标记:`'use client'` 仅在交互根节点声明,子树尽量保持 Server Component
|
||||
|
||||
### 3.5 字体加载策略(补 ai13 §3.3)
|
||||
|
||||
| 策略 | 实现 |
|
||||
| -------- | ------------------------------------------------------------------------ |
|
||||
| 加载方式 | `next/font/google`(self-host,无第三方请求) |
|
||||
| 显示策略 | `font-display: swap`(FOUT,优先文本可见性,避免 FOIT 白屏) |
|
||||
| preload | 首屏字体 `preload`,次屏字体 lazy |
|
||||
| 子集化 | Inter/Fraunces 按中文字符子集(next/font 自动 latin,中文需手动 subset) |
|
||||
| 变量暴露 | `var(--font-family-sans/serif/mono)`,禁止字面量(project_rules §3.10) |
|
||||
|
||||
### 3.6 图片优化(补 ai13 §3.3)
|
||||
|
||||
- 统一 `next/image`:自动 AVIF/WebP 转换 + 响应式 `sizes` + lazy load + blur placeholder
|
||||
- 知识图谱节点图、用户头像走 `next/image`;远程图配 `remotePatterns` 白名单
|
||||
- 图表截图导出走 `canvas.toBlob` + CDN,不走 base64 内联
|
||||
|
||||
### 3.7 路由级代码分割与长任务优化(补 ai13 §3.3)
|
||||
|
||||
- App Router 默认按路由分割;重型组件(Tiptap/recharts/知识图谱)用 `@next/dynamic` 懒加载
|
||||
- React 18 concurrent:长列表用 `useDeferredValue`;路由切换用 `useTransition` 标记非紧急更新
|
||||
- MF Remote 按 scope 动态加载,非当前 scope 不下载 remoteEntry.js
|
||||
|
||||
### 3.8 Prefetch / Preload 策略
|
||||
|
||||
| 资源 | 策略 |
|
||||
| ------------------ | ------------------------------------------- |
|
||||
| 当前路由数据 | TanStack Query 预取(`prefetchQuery`) |
|
||||
| 可能访问的 Remote | `requestIdleCallback` 预拉取 remoteEntry.js |
|
||||
| 关键字体/Logo | `<link rel="preload">` |
|
||||
| 下一页 RSC payload | Next.js Link `prefetch={true}`(默认) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 可观测性深度(补 ai13 §3.4)
|
||||
|
||||
### 4.1 三支柱前端落地
|
||||
|
||||
| 支柱 | 工具 | 实现 | 阶段 |
|
||||
| ---- | ------------------------------------------ | ------------------------------------------- | ---- |
|
||||
| 日志 | pino-browser → Gateway → Loki | 结构化 JSON,含 trace_id/user_id/scope/path | P6 |
|
||||
| 指标 | Web Vitals → `/api/v1/admin/web-vitals` | LCP/INP/CLS/TTFB + Long Task | P6 |
|
||||
| 链路 | OTel browser SDK → OTLP collector → Jaeger | 自动埋点 fetch/XHR/document load | P6 |
|
||||
|
||||
### 4.2 trace_id 全链路透传(补 ai13 §3.4)
|
||||
|
||||
```
|
||||
浏览器入口生成 trace_id(crypto.randomUUID)
|
||||
→ 写入 fetch header 'X-Request-Id'
|
||||
→ Gateway 透传
|
||||
→ 服务间 gRPC metadata
|
||||
→ 日志/metrics/trace 统一 trace_id
|
||||
→ 响应头返回 'X-Request-Id'
|
||||
→ 前端日志记录(便于用户报障定位)
|
||||
```
|
||||
|
||||
- MF 透传:Shell 生成 trace_id 后通过共享 `useApi()` 注入所有 Remote 的请求
|
||||
- Session 模型补 `traceId` 字段(ai13 已在 02 补全)
|
||||
|
||||
### 4.3 错误追踪与 Session Replay(补 ai13 §3.4)
|
||||
|
||||
| 维度 | 方案 |
|
||||
| ---------------- | ------------------------------------------------------------------------- |
|
||||
| 平台 | Sentry(P6) |
|
||||
| DSN | 环境变量 `NEXT_PUBLIC_SENTRY_DSN` |
|
||||
| release tracking | 每次构建注入 commit SHA |
|
||||
| source maps | CI 上传到 Sentry(生产不暴露) |
|
||||
| 采样率 | errors 100%,traces 10%,replay 1% |
|
||||
| PII 过滤 | `beforeSend` 剥离 token/password/身份证;Session Replay 对敏感输入 `mask` |
|
||||
| Session Replay | Sentry Replay,敏感区域 `data-sentry-mask` |
|
||||
|
||||
### 4.4 用户行为埋点规范(补 ai13 §3.4)
|
||||
|
||||
| 事件类别 | 示例 | 上报 |
|
||||
| -------- | -------------------------------------------- | -------------------------- |
|
||||
| 页面访问 | `page_view` {path, scope, role} | Gateway 聚合 |
|
||||
| 关键操作 | `exam_create`/`homework_grade`/`ai_generate` | Gateway 聚合 |
|
||||
| 错误 | `action_failed` {code, path} | Sentry + Gateway |
|
||||
| 性能 | `web_vitals` {lcp, inp, cls} | `/api/v1/admin/web-vitals` |
|
||||
|
||||
- 埋点统一走 `packages/hooks/src/useAnalytics()`,禁止散落 `gtag`/`mixpanel` 直调
|
||||
- 隐私合规:不采集身份证/成绩具体值,仅采集聚合指标
|
||||
|
||||
### 4.5 前端日志聚合(补 ai13 §3.4)
|
||||
|
||||
```
|
||||
pino-browser (batch 10 条/5s flush)
|
||||
→ POST /api/v1/admin/logs
|
||||
→ Gateway → Loki
|
||||
→ Grafana 查询(按 trace_id/user_id/path 过滤)
|
||||
```
|
||||
|
||||
- 级别:debug(dev only)/info/warn/error
|
||||
- 采样:info 10%,warn 100%,error 100%
|
||||
|
||||
---
|
||||
|
||||
## 5. a11y 实施细节(补 ai13 §3.5)
|
||||
|
||||
### 5.1 WCAG 2.2 AA 对照清单
|
||||
|
||||
| 准则 | 实施方式 |
|
||||
| -------------- | ----------------------------------------------------- |
|
||||
| 1.1 非文本内容 | 所有 `<img>` 有 `alt`;装饰图 `alt=""` |
|
||||
| 1.3 适应性 | 语义化 HTML(`<nav>/<main>/<aside>`);ARIA role 补充 |
|
||||
| 1.4 可辨别性 | 对比度 ≥ 4.5:1(设计令牌审计);禁用纯色信息编码 |
|
||||
| 2.1 键盘可达 | 全键盘导航;可见焦点(`focus-visible`);跳过链接 |
|
||||
| 2.4 导航 | 标题层级正确;`<title>` 动态;面包屑 |
|
||||
| 2.5 输入模态 | 触摸目标 ≥ 44px;取消指针;防误触 |
|
||||
| 3.1 可读 | `lang` 属性;缩写标注 |
|
||||
| 3.2 可预测 | 一致导航;无意外跳转 |
|
||||
| 3.3 输入辅助 | 错误标识 + `aria-describedby` + 修正建议 |
|
||||
| 4.1 兼容 | ARIA 正确使用;不依赖仅颜色 |
|
||||
|
||||
### 5.2 键盘导航
|
||||
|
||||
- 跳过链接(skip to main content)首位
|
||||
- Tab 顺序与视觉顺序一致(DOM 顺序即 tab 顺序)
|
||||
- Modal 打开时焦点陷阱(`focus-trap`),关闭后焦点返回触发元素
|
||||
- 快捷键:`/` 聚焦搜索、`g` 后接字母跳转(teacher/student/parent/admin)
|
||||
|
||||
### 5.3 屏幕阅读器测试矩阵
|
||||
|
||||
| 读屏器 | 平台 | 阶段 |
|
||||
| --------- | --------- | ------- |
|
||||
| NVDA | Windows | P6 审计 |
|
||||
| JAWS | Windows | P6 抽测 |
|
||||
| VoiceOver | macOS/iOS | P6 审计 |
|
||||
| TalkBack | Android | P6 抽测 |
|
||||
|
||||
### 5.4 自动化检测
|
||||
|
||||
- `eslint-plugin-jsx-a11y`(error 级,CI 强制)
|
||||
- `@axe-core/playwright` E2E 集成,每版本扫描关键页面
|
||||
- 设计令牌对比度审计脚本(primitive 色板两两组合计算对比度)
|
||||
|
||||
---
|
||||
|
||||
## 6. i18n 实施细节(补 ai13 §3.6)
|
||||
|
||||
| 维度 | 方案 |
|
||||
| ----------- | --------------------------------------------------------------------------------------------- |
|
||||
| 库 | next-intl(App Router 原生支持) |
|
||||
| 路由 | `/[locale]/teacher/...`(前缀式,默认 `zh`) |
|
||||
| 检测 | middleware.ts:cookie `NEXT_LOCALE` → `Accept-Language` → 默认 `zh` |
|
||||
| 加载 | 按路由分片动态加载 message(`getMessages` + `useTranslations`) |
|
||||
| fallback | `zh` 为 fallback locale,缺 key 回退;dev 环境缺 key console.warn |
|
||||
| 复数/性别 | ICU MessageFormat(next-intl 内置):`{count, plural, =0 {无} one {# 项} other {# 项}}` |
|
||||
| 数字/日期 | `Intl.NumberFormat` / `Intl.DateTimeFormat`(按 locale,不用 dayjs 本地化) |
|
||||
| 错误码 i18n | `error.{{service}}.{{code_snake_case}}`(见 02 §6.2) |
|
||||
| RTL 预留 | 布局用 logical properties(`margin-inline`/`padding-inline`),未来阿拉伯语只需切换 direction |
|
||||
| 文档术语 | 业务术语统一 glossary(`packages/contracts/src/i18n-glossary.ts`)防翻译漂移 |
|
||||
| 翻译协作 | `i18n/locales/*.json` 纳入 git;CI 校验 key 完整性(zh/en key 集合一致);后续可接 Crowdin |
|
||||
|
||||
> BFF/服务返回 i18n key + 参数(不返回已翻译文案),前端统一翻译,避免多端文案不一致。
|
||||
|
||||
---
|
||||
|
||||
## 7. 部署拓扑(补 ai13 §3.7)
|
||||
|
||||
### 7.1 部署演进
|
||||
|
||||
| 阶段 | 目标 | 方式 |
|
||||
| ----- | ------------------- | ---------------------------------------------------------- |
|
||||
| P1-P5 | 单机 Docker Compose | `docker compose up -d --build`(每服务 Dockerfile 多阶段) |
|
||||
| P6+ | K8s | 每服务 Deployment + HPA + Service + Ingress |
|
||||
|
||||
### 7.2 MF 部署与 CDN
|
||||
|
||||
| 端 | 部署单元 | 策略 | 备注 |
|
||||
| ------------------------------ | -------- | ----------- | ---------------------------------------- |
|
||||
| teacher-portal(Shell) | 独立 | 滚动 | Shell 变更频率最低 |
|
||||
| student/parent/admin(Remote) | 各自独立 | 蓝绿/金丝雀 | remoteEntry.js 不可变文件名 + CDN 长缓存 |
|
||||
|
||||
- **部署顺序约束**:破坏性 shared dep 升级时,Remote 先升、Shell 后升(Shell singleton 向下兼容旧 Remote)
|
||||
- **CDN**:remoteEntry.js 设 immutable 长缓存,版本化文件名 `remoteEntry.{version}.js` 解决缓存失效
|
||||
- **预拉取**:Shell 在 idle 时预拉取用户角色对应 Remote 的 remoteEntry.js(`requestIdleCallback`)
|
||||
- **Edge middleware**:locale 检测、A/B 测试、灰度路由放 edge(Vercel/Cloudflare Workers)
|
||||
|
||||
### 7.3 多环境配置
|
||||
|
||||
| 环境 | 配置来源 | 说明 |
|
||||
| ------- | ---------------------- | -------- |
|
||||
| dev | `.env.local` | 本地开发 |
|
||||
| staging | K8s ConfigMap + Secret | 预发布 |
|
||||
| prod | K8s Secret | 生产 |
|
||||
|
||||
- 前端变量必须 `NEXT_PUBLIC_` 前缀(project_rules §4);服务端变量不加
|
||||
- 环境校验用 `@t3-oss/env-nextjs` + Zod,启动时 fail-fast
|
||||
|
||||
### 7.4 健康检查端点
|
||||
|
||||
| 端点 | 用途 | 实现 |
|
||||
| ----------------- | --------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `GET /api/health` | Dockerfile HEALTHCHECK / liveness | 返回 `{ status: 'ok', ts }` |
|
||||
| `GET /api/ready` | K8s readinessProbe | 检查 `API_GATEWAY_URL` 可达 + MF remoteEntry 可达(Shell)+ 内存 < 阈值 |
|
||||
|
||||
### 7.5 灰度发布与回滚
|
||||
|
||||
| 场景 | 策略 |
|
||||
| ----------- | ---------------------------------------------------------------------- |
|
||||
| Remote 灰度 | remoteEntry.js 多版本并存,manifest 按用户分桶切换 |
|
||||
| 回滚 | remoteEntry.js 保留 N 版,CDN 切上一版本;数据库无变更,无数据回滚风险 |
|
||||
| 会话保持 | 回滚不强制登出,旧版本 Session 兼容(向前兼容设计) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试策略细节(补 ai13 §3.8)
|
||||
|
||||
### 8.1 测试金字塔
|
||||
|
||||
| 层级 | 工具 | 覆盖目标 | 范围 |
|
||||
| -------- | -------------------------------- | -------------------------------------------------- | ----------------------------------- |
|
||||
| 单元 | Vitest + @testing-library/react | hooks ≥90% / utils ≥90% / 纯组件 ≥80% / pages ≥60% | Hook、工具函数、纯组件 |
|
||||
| 集成 | Vitest + MSW | 关键链路 | ApiClient + TanStack Query + 权限流 |
|
||||
| 契约 | Vitest + `@edu/mf-contracts` zod | 100% Shell 暴露 | Shell↔Remote 契约 |
|
||||
| 视觉回归 | Playwright + Percy/Chromatic | 关键页面 | 设计令牌、布局 |
|
||||
| E2E | Playwright | 4 端关键流 | 登录→操作→结果 |
|
||||
| 性能 | Lighthouse CI | 每版本 | LCP/INP/CLS 回归 |
|
||||
|
||||
### 8.2 MF 集成测试
|
||||
|
||||
- Shell 单独测试:mock Remote 为 stub 组件
|
||||
- Remote 单独测试:mock Shell 暴露的 Hook
|
||||
- 契约测试:`@edu/mf-contracts` 携带 zod schema,CI 校验 Shell 暴露与 Remote 消费一致
|
||||
- 跨端 E2E:Playwright 启动 Shell + 真实 Remote,覆盖"教师发作业→学生提交→家长查看"全链路
|
||||
|
||||
### 8.3 E2E 关键路径
|
||||
|
||||
| 路径 | 步骤 |
|
||||
| ---------- | ------------------------------------------------- |
|
||||
| 登录流 | 登录→侧边栏按角色渲染→dashboard |
|
||||
| 班级 CRUD | 新建→列表→详情→编辑→删除 |
|
||||
| 考试全链路 | 教师建考→发布→学生作答→教师批改→成绩发布→家长查看 |
|
||||
| AI 出题 | 选知识点→AI 流式生成→教师审核→入库 |
|
||||
| 权限降级 | 角色变更→视口收敛→路由重定向 |
|
||||
|
||||
### 8.4 CI 稳定策略
|
||||
|
||||
- E2E 并行分片(Playwright shard)
|
||||
- 重试 2 次(flaky 容忍)
|
||||
- 截图归档失败用例
|
||||
- 独立测试 DB,每次重置
|
||||
|
||||
---
|
||||
|
||||
## 9. 移动端策略(补 ai13 §3.10)
|
||||
|
||||
### 9.1 响应式适配
|
||||
|
||||
| 端 | 适配 | 说明 |
|
||||
| -------------- | ------------------ | --------------------------------- |
|
||||
| teacher-portal | 响应式(桌面优先) | 教师主要用桌面,手机用于查看/通知 |
|
||||
| student-portal | 移动优先 | 学生作答多用手机 |
|
||||
| parent-portal | 移动优先 | 家长查看看手机 |
|
||||
|
||||
- Tailwind 断点:`sm 640 / md 768 / lg 1024 / xl 1280`
|
||||
- AppShell 移动端折叠侧边栏为抽屉(汉堡菜单)
|
||||
- 触摸目标 ≥ 44px(WCAG 2.5.5)
|
||||
|
||||
### 9.2 PWA / 离线(预留,补 ai13 §3.10)
|
||||
|
||||
| 能力 | 预留点 | 触发时机 |
|
||||
| -------------- | -------------------------------------------------- | ---------------- |
|
||||
| Service Worker | `sw.ts` 预留,next-pwa 接入 | 教师弱网批改需求 |
|
||||
| 离线缓存 | TanStack Query `onlineManager` + mutation 离线队列 | 弱网 |
|
||||
| 可安装 | manifest.json + installable | 学校要求桌面图标 |
|
||||
| 后台同步 | Background Sync API | 离线提交作业 |
|
||||
|
||||
> 当前不实现,但 ApiClient 的 mutation 设计为可排队(失败入队列,上线重放)。
|
||||
|
||||
### 9.3 原生 App(远期,补 ai13 §3.10)
|
||||
|
||||
- hooks/contracts 纯 TS 不耦合 DOM,可被 React Native 复用
|
||||
- 若未来出 RN App,复用 usePermission/useAuth/ApiClient,UI 层重写
|
||||
- 不为此提前抽象,保持当前 Next.js 实现简洁(YAGNI)
|
||||
|
||||
---
|
||||
|
||||
## 10. 数据流与乐观更新(补 ai13 §3.12)
|
||||
|
||||
### 10.1 乐观更新模式
|
||||
|
||||
```typescript
|
||||
const gradeMutation = useMutation({
|
||||
mutationFn: (input) => api.post("/api/v1/grades", input),
|
||||
onMutate: async (input) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["teacher", "grades", input.classId],
|
||||
});
|
||||
const previous = queryClient.getQueryData([
|
||||
"teacher",
|
||||
"grades",
|
||||
input.classId,
|
||||
]);
|
||||
queryClient.setQueryData(["teacher", "grades", input.classId], (old) =>
|
||||
old?.map((g) => (g.id === input.id ? { ...g, score: input.score } : g)),
|
||||
);
|
||||
return { previous };
|
||||
},
|
||||
onError: (err, input, context) => {
|
||||
queryClient.setQueryData(
|
||||
["teacher", "grades", input.classId],
|
||||
context.previous,
|
||||
); // 回滚
|
||||
toast.error("保存失败,已回滚");
|
||||
},
|
||||
onSettled: (data, err, input) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["teacher", "grades", input.classId],
|
||||
}); // 与服务端同步
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 10.2 缓存失效链路
|
||||
|
||||
mutate 一个资源后,需联动失效所有依赖它的缓存:
|
||||
|
||||
| mutation | 失效 queryKey |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| 创建班级 | `['teacher','classes']`、`['teacher','dashboard']` |
|
||||
| 发布考试 | `['teacher','exams']`、`['teacher','classes',{id},'exams']` |
|
||||
| 批改作业 | `['teacher','grades']`、`['teacher','homework',{id}]`、`['student','grades']`(跨端,走 WebSocket 推送 invalidate) |
|
||||
| 角色变更 | `['session','effective-permissions']`、`['session','viewports']`、所有业务 query |
|
||||
|
||||
### 10.3 冲突解决
|
||||
|
||||
| 场景 | 策略 |
|
||||
| ---------------------- | ------------------------------------------------------------------------ |
|
||||
| 多教师同时批改同一作业 | 后端乐观锁(version 字段);前端 409 时重新拉取 + 提示"数据已被他人修改" |
|
||||
| 学生重复提交 | 后端幂等(event_id 去重);前端禁用提交按钮 + loading |
|
||||
| 缓存与服务端不一致 | staleTime 到期自动重取;关键操作后强制 invalidate |
|
||||
|
||||
---
|
||||
|
||||
## 11. MF 运维架构(补 ai13 §3.11)
|
||||
|
||||
### 11.1 Remote 版本与兼容性
|
||||
|
||||
| 维度 | 策略 |
|
||||
| -------- | --------------------------------------------------------------------------------------- |
|
||||
| 版本号 | Shell 与 Remote 各自 Semver;remoteEntry.js 路径带版本 `remoteEntry.{version}.js` |
|
||||
| 契约版本 | Shell↔Remote 交互契约(暴露组件 props、共享 store shape)独立版本化 `@edu/mf-contracts` |
|
||||
| 兼容范围 | Shell 声明 `peerDeps` 兼容的 Remote 主版本范围;不兼容版本走 fallback UI |
|
||||
| 升级顺序 | 先升 shared deps(react 等)→ 再升 Remote → 最后升 Shell;每步灰度 |
|
||||
| 回滚 | remoteEntry.js 保留 N 个历史版本,CI 一键切回上一版本 URL |
|
||||
|
||||
### 11.2 Remote 故障与降级
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Shell 加载 Remote] --> B{remoteEntry.js 可达?}
|
||||
B -- 否 --> C[重试 3 次指数退避]
|
||||
C --> D{仍失败?}
|
||||
D -- 是 --> E[渲染 Fallback UI]
|
||||
E --> F[提示模块暂不可用 + 重试按钮 + 通知运维]
|
||||
B -- 是 --> G[执行 Remote 模块]
|
||||
G --> H{运行时抛错?}
|
||||
H -- 是 --> I[Remote 级 ErrorBoundary 捕获]
|
||||
I --> E
|
||||
H -- 否 --> J[正常渲染]
|
||||
```
|
||||
|
||||
- 每个 Remote 用独立 `<ErrorBoundary fallback={...}>` 包裹,单 Remote 崩溃不带垮整个 Shell
|
||||
- 动态 import 用 `@next/dynamic` + `ssr: false` + `loading` 占位
|
||||
- Remote 不可用不阻断其他 Remote 与 Shell 自身路由
|
||||
|
||||
### 11.3 共享依赖演化
|
||||
|
||||
| 场景 | 处理 |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| React 18 → 19 大版本升级 | 全 4 端统一窗口期升级;过渡期 MF `shared.singleton` 容忍 minor 差异,major 不一致报错 |
|
||||
| 新增共享依赖(如 zustand) | 先在 `@edu/mf-contracts` 登记 → Shell exposes shared → Remote 声明 shared |
|
||||
| 移除共享依赖 | 先 Remote 移除引用 → 确认无 Remote 引用 → Shell 移除 shared |
|
||||
| 版本漂移检测 | CI 校验 4 端 `package.json` 的 shared deps 版本一致(pnpm dedupe + 自定义检查) |
|
||||
|
||||
### 11.4 跨端类型安全
|
||||
|
||||
- Shell 通过 `@module-federation/typescript` 生成 `@mf-types/*`,Remote 消费 Shell 暴露组件的类型
|
||||
- 运行时边界:跨端调用一律走 Hook(`useApi()`/`usePermission()`),不直接访问对方内部 store
|
||||
- 契约测试:`@edu/mf-contracts` 携带类型 + 运行时 zod schema,Remote 接入 Shell 时校验
|
||||
|
||||
### 11.5 路由策略
|
||||
|
||||
| 关注点 | 方案 |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| 路由前缀 | `/teacher/*` `/student/*` `/parent/*` `/admin/*`(004 §5.4 场景域划分) |
|
||||
| 深链直入 | URL `/student/exams/123` → Shell middleware 识别前缀 → 动态加载 student remote → remote 内部路由匹配 |
|
||||
| 404 | Shell 全局 `not-found.tsx` + 各 Remote 内部 `not-found.tsx` |
|
||||
| 路由守卫 | `middleware.ts` 统一鉴权:无 token → `/login`;权限不足当前前缀 → 重定向到对应 scope dashboard |
|
||||
| 路由切换 loading | next.js `loading.tsx`(Suspense)骨架屏 + 滚动恢复 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 远期演进铺垫
|
||||
|
||||
### 12.1 远期能力预留(不立即实现,但架构不堵死)
|
||||
|
||||
| 能力 | 预留点 | 触发时机 |
|
||||
| ------------------- | ------------------------------------------------------------------ | ---------------- |
|
||||
| PWA / 离线 | Service Worker 预留 `sw.ts`;TanStack Query onlineManager 离线队列 | 教师弱网批改需求 |
|
||||
| 多租户 / SaaS | 前端按 `tenant_id` 注入;i18n glossary 支持租户覆写 | 多校部署 |
|
||||
| Feature Flags | `packages/contracts/src/flags.ts` 统一枚举;middleware 注入 | 灰度发布 |
|
||||
| 移动端复用 | hooks/contracts 纯 TS 可被 React Native 复用;不耦合 DOM | RN App |
|
||||
| 插件化 | Shell 预留 Remote 注册表(动态 manifest),第三方 Remote 可挂载 | 开放生态 |
|
||||
| Web Components 抽取 | ui-components 用 lit 封装跨框架版本(远期) | 跨技术栈 |
|
||||
| A/B 测试 | edge middleware 分桶 + feature flag | 增长需求 |
|
||||
|
||||
### 12.2 ADR(架构决策记录)登记
|
||||
|
||||
本设计产生的关键决策应沉淀为 ADR 供未来回溯:
|
||||
|
||||
| ADR | 决策 | 状态 |
|
||||
| ------- | --------------------------------------------------------- | ---------------------- |
|
||||
| ADR-013 | Token 存储:localStorage 过渡 → 内存+httpOnly cookie 终态 | 拟采纳 |
|
||||
| ADR-014 | MF 拓扑:单 Shell + 3 Remote | 已采纳(沿用 ADR-012) |
|
||||
| ADR-015 | 状态管理 5 层模型 | 拟采纳 |
|
||||
| ADR-016 | i18n 路由前缀式 + next-intl | 拟采纳 |
|
||||
| ADR-017 | 性能预算 + bundle CI 门禁 | 拟采纳 |
|
||||
| ADR-018 | MF Remote SRI + 最小暴露面 | 拟采纳 |
|
||||
| ADR-019 | 多 Tab 会话同步 BroadcastChannel | 拟采纳 |
|
||||
|
||||
> ADR 正式登记由 coord 在 `docs/architecture/adr/` 维护,ai07 提请。
|
||||
|
||||
### 12.3 待 coord 协调问题
|
||||
|
||||
| # | 问题 | 影响 | 建议裁决 |
|
||||
| --- | ------------------------------------------------------------------------------ | --------------------- | ---------------------------------------------------------------- |
|
||||
| 1 | `full-stack-runbook.md` §2.1 端口矩阵未更新到 4 端(仍记 teacher-portal=3000) | 端口权威源不一致 | coord 更新 runbook 为 4000-4003 |
|
||||
| 2 | packages 归属(ui-tokens/ui-components/hooks) | 维护边界 | ai07 维护前端专属包,coord 维护 shared-ts/contracts |
|
||||
| 3 | ~~GraphQL vs REST(BFF 何时切 GraphQL)~~ **已裁决(F9 + 总裁 §0.3)** | 前端是否引入 urql | **P2 起 BFF 即 GraphQL,前端即 urql,无 REST 中间态**(见 §1.4) |
|
||||
| 4 | admin-bff 是否存在(004 §4 未列出,但 admin-portal 消费 `/admin/*`) | admin-portal 契约缺失 | coord 确认 admin-bff 归属或由 iam 承载管理 API |
|
||||
| 5 | shared deps 版本对齐 CI 校验机制 | MF singleton 安全 | coord 在 CI 增加 4 端 package.json diff 检查 |
|
||||
| 6 | `@edu/mf-contracts` 包归属 | Shell↔Remote 契约载体 | 建议 coord 维护(跨 AI 共享) |
|
||||
| 7 | Next.js 14→15 升级窗口 | App Router 破坏性变更 | P2 收尾统一升级,4 端同步 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 覆盖度对照(ai13 审核 §3 遗漏逐项闭合)
|
||||
|
||||
| ai13 §3 遗漏项 | 本文档章节 | 状态 |
|
||||
| -------------------------- | ---------------------- | ------- |
|
||||
| §3.1 演进路线缺失 | §1 演进与迁移策略 | ✅ 闭合 |
|
||||
| §3.2 安全架构缺失 | §2 安全架构 | ✅ 闭合 |
|
||||
| §3.3 性能架构缺失 | §3 性能架构 | ✅ 闭合 |
|
||||
| §3.4 可观测性深度缺失 | §4 可观测性深度 | ✅ 闭合 |
|
||||
| §3.5 a11y 实施细节缺失 | §5 a11y 实施细节 | ✅ 闭合 |
|
||||
| §3.6 i18n 实施细节缺失 | §6 i18n 实施细节 | ✅ 闭合 |
|
||||
| §3.7 部署拓扑缺失 | §7 部署拓扑 | ✅ 闭合 |
|
||||
| §3.8 测试策略细节缺失 | §8 测试策略细节 | ✅ 闭合 |
|
||||
| §3.9 文件/目录结构缺失 | 02 §14(ai13 已补) | ✅ 闭合 |
|
||||
| §3.10 移动端策略缺失 | §9 移动端策略 | ✅ 闭合 |
|
||||
| §3.11 MF 演进策略缺失 | §1.3 + §11 MF 运维架构 | ✅ 闭合 |
|
||||
| §3.12 数据流与乐观更新缺失 | §10 数据流与乐观更新 | ✅ 闭合 |
|
||||
|
||||
---
|
||||
|
||||
**AI Agent**: ai07(4 端合并设计 / 长远架构补全)
|
||||
**Branch**: docs/teacher-portal-long-term-architecture-ai07
|
||||
**Coordinator**: coord-ai
|
||||
**Closes**: ai13 audit §3 all 12 items
|
||||
|
||||
---
|
||||
|
||||
## 14. 回写记录
|
||||
|
||||
### 14.1 ai13 回写(2026-07-09,批次 0.15 后,总裁裁决 §3.4 ISSUE-036-ai13)
|
||||
|
||||
**回写依据**:总裁裁决 §3.4 + §4.4 第 5 条(删除过渡方案本身,保留设计决策记录)+ coord F9(P2 起 BFF 用 GraphQL)。
|
||||
|
||||
**回写内容**:
|
||||
|
||||
| 位置 | 原内容 | 新内容 |
|
||||
| ---------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| §1.1 P2 行 | "统一 API 层" | "GraphQL client(urql,F9 裁决 P2 起)" |
|
||||
| §1.2 表格 | "BFF 切 GraphQL(P4 起) + feature flag 灰度" | "BFF GraphQL(P2 起,已裁决) + 前端 P2 起即 urql" |
|
||||
| §1.4 整节 | "REST → GraphQL 切换策略"(过渡方案:P2-P4 REST + P5+ 迁移 + feature flag 灰度) | "GraphQL 客户端架构(P2 起最终方案)"(client 选型/单例/初始化/错误处理/缓存策略/ISSUE 关联/设计决策记录) |
|
||||
| §12.3 #3 | 待裁决"P2-P3 REST,P4 起 BFF 切 GraphQL" | 已裁决"P2 起 BFF 即 GraphQL,前端即 urql,无 REST 中间态" |
|
||||
|
||||
**未删除内容**(设计决策记录,按 §4.4 保留):
|
||||
|
||||
- §1.4.6 设计决策记录:明确记录原过渡方案已废弃 + 裁决来源 + 影响范围,供未来回溯
|
||||
- §2.1 Token 存储演进(P2 localStorage → P6 httpOnly cookie):属独立决策(F12 裁决 P2 用 localStorage),非"REST→GraphQL 过渡方案",不在本次回写范围
|
||||
|
||||
**关联 ISSUE**:ISSUE-036-ai13(已通过本次回写闭合)
|
||||
|
||||
**AI Agent**: ai13(teacher-portal,回写执行)
|
||||
**Branch**: feat/teacher-portal-docs-rewrite-issue036-ai13
|
||||
**Coordinator**: coord-ai
|
||||
**依据**: president-final-rulings.md §3.4 + §4.4
|
||||
356
docs/architecture/ai-work-orchestration.md
Normal file
356
docs/architecture/ai-work-orchestration.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# AI 工作顺序协同规划
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-09
|
||||
> 决策者:coord(经用户确认)
|
||||
> 适用范围:Edu 项目 16 个 AI(ai01-ai16)+ coord
|
||||
> 关联文档:[coord-final-decisions.md](./coord-final-decisions.md)、[ai-allocation.md](./ai-allocation.md)、[004 §16](./004_architecture_impact_map.md)、[issues.md](../issues.md)
|
||||
|
||||
---
|
||||
|
||||
## 0. 背景
|
||||
|
||||
coord-final-decisions.md 强制"首次实现即最终方案,不分阶段",但实际执行中存在**工作先后排序问题**:
|
||||
|
||||
- teacher-bff 裁决"P2 起 GraphQL + gRPC 调下游",但 core-edu P3 才启用 gRPC → P2 启动时 teacher-bff 调谁?
|
||||
- shared-ts Outbox 工具包谁建?什么时候建?iam P2 即用
|
||||
- parent-bff P4 依赖 iam.GetChildrenByParent,iam 何时补全?
|
||||
|
||||
本文档规划 **AI 工作顺序协同**,**尽量保证并行**,有依赖的才串行,并识别"跨度过大"问题记录到 [issues.md](../issues.md)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 协同原则
|
||||
|
||||
| # | 原则 | 说明 |
|
||||
| --- | ------------------ | -------------------------------------------------------------------------------------------- |
|
||||
| 1 | **批次并行** | 同批次 AI 可同时启动,互不阻塞 |
|
||||
| 2 | **批次间串行** | 下批次依赖上批次产出,必须等待 |
|
||||
| 3 | **coord 前置** | coord 负责的 proto 契约 / 工具包 / 端口表必须在 AI 启动前就位 |
|
||||
| 4 | **双向依赖协调** | 同批次内有双向依赖的 AI(如 BFF ⟷ Portal schema 契约),coord 仲裁第一版契约后并行 |
|
||||
| 5 | **跨阶段扩展例外** | BFF 跨阶段补 gRPC 调用是**允许的例外**(因下游服务按阶段启用本身合理),不视为"中间过渡方案" |
|
||||
| 6 | **疑问记录** | AI 觉得有疑问(前置依赖、跨度过大)可在 [issues.md](../issues.md) 记录,coord 统一解决 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 批次总览
|
||||
|
||||
| 批次 | 阶段 | 可并行 AI 数 | 关键产出 | 跨度过大风险 |
|
||||
| ---- | ------------ | --------------------------------------------- | -------------------------------------------------------- | ------------------------------------------ |
|
||||
| 0 | 全 AI 启动前 | coord 独立 | proto 契约 + shared-ts Outbox + 端口表统一 | shared-ts Outbox 工具包遗漏(已修正) |
|
||||
| 1 | P2 | 4(ai01/ai06 + ai03⟷ai13) | iam gRPC + teacher-bff GraphQL + teacher-portal MF shell | 高(teacher-bff 调下游断裂,见 issues #4) |
|
||||
| 2 | P3 | 4(ai08 + ai04⟷ai14 + ai07 交接 + ai03 扩展) | core-edu gRPC + student-bff GraphQL | 中(student-bff 调 content/data-ana 断裂) |
|
||||
| 3 | P4 | 5(ai09/ai11 + ai05⟷ai15 + ai03/ai04 扩展) | content gRPC + data-ana gRPC + parent-bff GraphQL | 中(teacher-bff 跨阶段改造) |
|
||||
| 4 | P5 | 4(ai10⟷ai02 + ai12 + ai03 扩展) | msg gRPC + ai gRPC + push-gateway HTTP | 低(核心依赖已前置解决) |
|
||||
| 5 | P6 | 1+(ai16 + 持续优化) | admin-portal MVP + P6 硬化 | 低 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 批次 0:coord + ai13 协同完成(全 AI 启动前)
|
||||
|
||||
> **总裁裁决 §6.2 最终版**:批次 0 共 15 项任务,coord 负责 0.1-0.14,ai13 负责 0.15。
|
||||
> 完成期限:3-5 天。批次 0 全部完成后才启动批次 1(见总裁裁决 §4.1 批次 0 完成信号机制)。
|
||||
|
||||
### 3.1 批次 0 详细任务清单(15 项)
|
||||
|
||||
| # | 责任方 | 任务 | 完成信号 | 阻塞批次 |
|
||||
| ---- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------ |
|
||||
| 0.1 | coord | `pnpm run arch:scan` 更新 arch.db | arch.db 已更新 | — |
|
||||
| 0.2 | coord | 修订 coord-cross-review.md(补 coord 交叉审查结论) | 文档已更新 | — |
|
||||
| 0.3 | coord | 补全 `iam.proto` 8 RPC(GetViewports / GetEffectivePermissions / GetEffectiveAccess / Logout / GetPublicKey / BatchGetUsers / GetEffectiveDataScope / GetChildrenByParent) | proto 文件已提交 | 批次 1(ai06) |
|
||||
| 0.4 | coord | 建立 `shared-ts` Outbox 工具包(含设计草案 + 实现,`@edu/shared-ts/outbox`) | packages/shared-ts/src/outbox/ 已建立 | 批次 1(ai06) |
|
||||
| 0.5 | coord | 补全 `content.proto`(ChapterService / QuestionService.* / TextbookService.ListTextbooks) | proto 文件已提交 | 批次 3(ai09) |
|
||||
| 0.6 | coord | 补全 `msg.proto` 基础(5 RPC + NotificationPreferenceService + NotificationTemplateService + 3 message) | proto 文件已提交 | 批次 4(ai10) |
|
||||
| 0.7 | coord | 补全 `analytics.proto` 基础 | proto 文件已提交 | 批次 3(ai11) |
|
||||
| 0.8 | coord | 补全 `core_edu.proto`(AttendanceService / GetClassesByTeacher / BatchGetClasses) | proto 文件已提交 | 批次 2(ai08) |
|
||||
| 0.9 | coord | `buf.gen.yaml` 补 gRPC 插件 | buf generate 可运行 | 批次 1+ |
|
||||
| 0.10 | coord | 补全 `events.proto`(含 AuditEvent message,总裁裁决 §5.5) | proto 文件已提交 | 批次 5(ai16 admin-portal 审计日志) |
|
||||
| 0.11 | coord | `analytics.proto` 扩展(4 端 Dashboard + Warning + MasteryDistribution + SubscribeMasteryUpdate Stream RPC) | proto 文件已提交 | 批次 3(ai11) |
|
||||
| 0.12 | coord | 补全 `ai.proto`(GenerateLessonPlan / StreamGenerateQuestion) | proto 文件已提交 | 批次 4(ai12) |
|
||||
| 0.13 | coord | 补全 `msg.proto`(5 RPC + NotificationPreferenceService + NotificationTemplateService + 3 message) | proto 文件已提交 | 批次 4(ai10) |
|
||||
| 0.14 | coord | 建立 `packages/shared-go` 包骨架(tracer/logger/jwks/env 4 模块的接口定义 + 默认实现,总裁裁决 §2.13) | packages/shared-go/ 已建立 | 批次 1(ai01 Gateway 接入) |
|
||||
| 0.15 | ai13 | 建立 `packages/ui-tokens` + `packages/ui-components` + `packages/hooks` 骨架(总裁裁决 §2.12) | 3 个 packages 已建立 | 批次 1(ai13 teacher-portal) |
|
||||
|
||||
### 3.2 批次 0 完成信号机制(总裁裁决 §4.1)
|
||||
|
||||
1. **coord 维护"批次 0 任务进度表"**:在 ai-work-orchestration.md §3.1 表格"完成信号"列勾选
|
||||
2. **批次 0 全部 15 项完成后,coord 发布批次 1 启动公告**(在本文件 §9 时间线末尾追加"批次 0 已完成,批次 1 启动")
|
||||
3. **建立"前置依赖就绪检查清单"**:coord 在批次 1/2/3/4/5 启动前逐项确认
|
||||
4. **批次 1 启动条件**:0.1-0.15 全部完成 + teacher-bff GraphQL schema 第一版仲裁完成 + MF shell 暴露清单仲裁完成
|
||||
|
||||
---
|
||||
|
||||
## 4. 批次 1:P2 并行批
|
||||
|
||||
### 4.1 前置条件
|
||||
|
||||
- 批次 0 中 0.1-0.7 全部就位
|
||||
|
||||
### 4.2 可并行 AI(4 个 + 1 双向依赖对)
|
||||
|
||||
| AI | 服务 | 核心工作 | 依赖 | 产出 |
|
||||
| ------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------ |
|
||||
| **ai01** | api-gateway | 补 GW_ 前缀 + /parent 路由 + W1-W8 整改 | 独立 | /parent 路由块 |
|
||||
| **ai06** | iam | gRPC server 50052 + 8 RPC + iam_student_guardians 表 + DB 驱动 PermissionGuard + AuthMiddleware + shared-ts Outbox 接入 | 批次 0.3 + 0.4 | iam gRPC 50052 可用(含 GetChildrenByParent) |
|
||||
| **ai03** ⟷ **ai13** | teacher-bff ⟷ teacher-portal | GraphQL schema 契约双向依赖,coord 仲裁第一版 schema 后并行 | ai03 依赖 ai06 gRPC;ai13 依赖 ai03 schema | teacher-bff GraphQL endpoint + DownstreamClient 抽象 + teacher-portal MF shell |
|
||||
|
||||
### 4.3 协调点
|
||||
|
||||
- **ai03 ⟷ ai13 GraphQL schema**:coord 在批次 1 启动前仲裁 schema 第一版(含 Query/Mutation 清单),ai03 实现 server,ai13 实现 client,同步开发
|
||||
- **ai03 DownstreamClient 抽象**:ai03 P2 必须产出可复用的 DownstreamClient 抽象(B8 裁决),供 ai04/ai05 复用,coord 验收时检查
|
||||
|
||||
### 4.4 跨度过大问题(见 [issues.md](../issues.md) #4-#8)
|
||||
|
||||
- teacher-bff P2 只能 gRPC 调 iam,调 core-edu/content/data-ana/ai/msg 全部断裂 → 见 issues #4
|
||||
- teacher-bff 跨阶段持续改造与"不分阶段"原则矛盾 → 见 issues #5(coord 已裁定为例外)
|
||||
|
||||
---
|
||||
|
||||
## 5. 批次 2:P3 并行批
|
||||
|
||||
### 5.1 前置条件
|
||||
|
||||
- 批次 1 完成(iam gRPC 50052 + GetChildrenByParent 已就绪)
|
||||
- 批次 0 中 0.8-0.9 全部就位(core_edu.proto + buf.gen.yaml gRPC 插件)
|
||||
|
||||
### 5.2 可并行 AI(4 个 + 1 双向依赖对 + 1 交接)
|
||||
|
||||
| AI | 服务 | 核心工作 | 依赖 | 产出 |
|
||||
| ------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
||||
| **ai08** | core-edu | 合并 classes + gRPC server 50053 + AttendanceService + GetClassesByTeacher + BatchGetClasses + C1-C11 整改 | ai07 classes 黄金模板代码 | core-edu gRPC 50053 可用;classes 服务下线 |
|
||||
| **ai07** | classes | 在 ai08 启动前完成 classes 代码交接 | — | classes 代码交接完成 |
|
||||
| **ai04** ⟷ **ai14** | student-bff ⟷ student-portal | GraphQL schema 契约双向依赖 | ai04 依赖 ai06 gRPC + ai08 gRPC + ai03 DownstreamClient 抽象;ai14 依赖 ai04 schema + ai13 MF 共享组件 | student-bff GraphQL endpoint + student-portal MF remote |
|
||||
| **ai03** | teacher-bff | P3 扩展,补 gRPC 调 core-edu 50053 | ai08 core-edu gRPC | teacher-bff gRPC 调 core-edu 链路打通 |
|
||||
|
||||
### 5.3 协调点
|
||||
|
||||
- **ai07 → ai08 classes 交接**:ai07 在 ai08 启动前完成 classes 代码交接(C1 裁决"P3 开始前直接合并")
|
||||
- **ai04 ⟷ ai14 GraphQL schema**:coord 仲裁 student-bff schema 第一版
|
||||
- **ai03 跨阶段扩展**:ai03 P3 补 gRPC 调 core-edu(跨阶段扩展例外,见 issues #5)
|
||||
|
||||
### 5.4 跨度过大问题
|
||||
|
||||
- student-bff P3 仍无法 gRPC 调 content / data-ana(P4 才启用)→ 见 issues #6
|
||||
|
||||
---
|
||||
|
||||
## 6. 批次 3:P4 并行批
|
||||
|
||||
### 6.1 前置条件
|
||||
|
||||
- 批次 0 中 0.10-0.11 全部就位(content.proto + analytics.proto 补全)
|
||||
- 批次 2 完成(core-edu gRPC 50053 可用)
|
||||
- iam.GetChildrenByParent 已就绪(批次 1 已完成)
|
||||
|
||||
### 6.2 可并行 AI(5 个 + 1 双向依赖对 + 2 跨阶段扩展)
|
||||
|
||||
| AI | 服务 | 核心工作 | 依赖 | 产出 |
|
||||
| ------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
||||
| **ai09** | content | gRPC server 50054 + ChapterService + QuestionService proto 补全(N3 裁决 P4 即补,不等到 P5)+ N1-N5 整改 | 批次 0.10 | content gRPC 50054 可用(含 QuestionService) |
|
||||
| **ai11** | data-ana | gRPC server 50055 + 4 端 Dashboard + Warning + MasteryDistribution + SubscribeMasteryUpdate Stream + D1-D5 整改 | 批次 0.11 | data-ana gRPC 50055 可用 |
|
||||
| **ai05** ⟷ **ai15** | parent-bff ⟷ parent-portal | GraphQL schema 契约双向依赖 | ai05 依赖 iam.GetChildrenByParent(已就绪)+ core-edu gRPC + ai03 DownstreamClient 抽象 | parent-bff GraphQL endpoint + parent-portal MF remote |
|
||||
| **ai03** | teacher-bff | P4 扩展,补 gRPC 调 content + data-ana | ai09 + ai11 | teacher-bff gRPC 调 content + data-ana 链路打通 |
|
||||
| **ai04** | student-bff | P4 扩展,补 gRPC 调 content + data-ana | ai09 + ai11 | student-bff gRPC 调 content + data-ana 链路打通 |
|
||||
|
||||
### 6.3 协调点
|
||||
|
||||
- **ai05 ⟷ ai15 GraphQL schema**:coord 仲裁 parent-bff schema 第一版
|
||||
- **ai03 / ai04 跨阶段扩展**:补 gRPC 调 content + data-ana(跨阶段扩展例外)
|
||||
- **coord 验收 ai09 P4**:强制检查 QuestionService RPC 已落地(阻塞 ai12 P5 出题)
|
||||
|
||||
### 6.4 跨度过大问题
|
||||
|
||||
- teacher-bff P4 仍需扩展 gRPC 调 ai(P5)+ msg(P5)→ 见 issues #6
|
||||
|
||||
---
|
||||
|
||||
## 7. 批次 4:P5 并行批
|
||||
|
||||
### 7.1 前置条件
|
||||
|
||||
- 批次 0 中 0.12-0.13 全部就位(ai.proto + msg.proto 补全)
|
||||
- 批次 3 完成(content.QuestionService RPC 已补全 + content gRPC 50054 可用)
|
||||
|
||||
### 7.2 可并行 AI(4 个 + 1 双向依赖对 + 1 跨阶段扩展)
|
||||
|
||||
| AI | 服务 | 核心工作 | 依赖 | 产出 |
|
||||
| ------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| **ai10** ⟷ **ai02** | msg ⟷ push-gateway | HTTP /internal/push 契约双向依赖 | ai10 依赖批次 0.13;ai02 独立 | msg gRPC 50056 + push-gateway HTTP /internal/push |
|
||||
| **ai12** | ai | gRPC server 50058 + GenerateLessonPlan + StreamGenerateQuestion + Prompt DB 存储 + FastAPI BackgroundTasks + A1-A8 整改 + 调 content.QuestionService + 调 core-edu.ExamService.GetExam + 调 data-ana | 批次 0.12 + content.QuestionService(已就绪)+ core-edu gRPC(已就绪)+ data-ana gRPC(已就绪) | ai gRPC 50058 可用 |
|
||||
| **ai03** | teacher-bff | P5 扩展,补 gRPC 调 ai + msg | ai10 + ai12 | teacher-bff gRPC 调 ai + msg 链路打通(全部下游 gRPC 调用完成) |
|
||||
|
||||
### 7.3 协调点
|
||||
|
||||
- **ai10 ⟷ ai02 HTTP /internal/push 契约**:coord 仲裁第一版接口契约(path/body/status code),ai02 优先 1-2 天启动 /internal/push 端点,ai10 后启动调用方
|
||||
- **ai12 跨服务调用**:调 content.QuestionService(已就绪)+ core-edu.ExamService.GetExam(已就绪)+ data-ana(已就绪),无阻塞 ✓
|
||||
|
||||
### 7.4 无跨度过大问题
|
||||
|
||||
- 核心依赖已通过前置裁决解决(content.QuestionService P4 补全、core-edu P3 启用、data-ana P4 启用)
|
||||
|
||||
---
|
||||
|
||||
## 8. 批次 5:P6 并行批
|
||||
|
||||
### 8.1 前置条件
|
||||
|
||||
- 批次 4 完成
|
||||
|
||||
### 8.2 可并行 AI(1 个 + 持续优化)
|
||||
|
||||
| AI | 服务 | 核心工作 | 依赖 | 产出 |
|
||||
| -------- | ------------ | ------------------------------------------------------------ | ----------------------------------------- | ------------------ |
|
||||
| **ai16** | admin-portal | MF remote + 管理端路由骨架 + P6+ 评估 X1-X10 事项 | events.proto(P6 实施时补 admin 8 topic) | admin-portal MVP |
|
||||
| 其他 AI | 持续优化 | /readyz 补下游探针、Python ErrorFilter 对齐等 §16.7.6 持续项 | — | 各服务 P6 硬化完成 |
|
||||
|
||||
### 8.3 跨度过大问题
|
||||
|
||||
- admin-portal 8 个新 Kafka topic 在 P6 实施时才补 events.proto message → 见 issues #14
|
||||
|
||||
---
|
||||
|
||||
## 9. 批次时间线(总裁裁决 §6.1 最终版)
|
||||
|
||||
> **总裁裁决最终版**:总时间线 49-64 天(约 10-13 周)。
|
||||
> 实际进度以各 AI 交付为准,但批次启动必须满足前置条件(见各批次 §1-§7 前置条件章节)。
|
||||
|
||||
| 批次 | 阶段 | 预计工作日 | 参与 AI | 关键里程碑 | 启动条件 |
|
||||
| ---------- | ---------- | ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
|
||||
| **批次 0** | coord 前置 | 3-5 天 | coord(0.1-0.14)+ ai13(0.15) | proto 契约 + shared-ts Outbox + shared-go + packages 骨架 | 本裁决发布后立即 |
|
||||
| **批次 1** | P2 | 7-10 天 | ai01 + ai06 + ai03⟷ai13 | iam P2.1 + teacher-bff Must Have + teacher-portal P2 + Gateway 路由 | 批次 0 全部完成 |
|
||||
| **批次 2** | P3 | 7-10 天 | ai08 + ai07 交接 + ai04⟷ai14 + ai03 扩展 | core-edu gRPC 50053 + student-bff + student-portal P3(7 页)+ teacher-bff P3 扩展 | 批次 1 P2.1 完成 + ai03 DownstreamClient 抽象就绪 |
|
||||
| **批次 3** | P4 | 10-12 天 | ai09 + ai11 + ai05⟷ai15 + ai03/ai04 扩展 | content gRPC 50054 + data-ana gRPC 50055 + parent-bff + parent-portal P4 + teacher/student-bff P4 扩展 | 批次 2 完成 |
|
||||
| **批次 4** | P5 | 12-15 天 | ai10⟷ai02 + ai12 + ai03 扩展 | msg gRPC 50056 + push-gateway + ai 服务 + teacher-bff P5 扩展 + 通知功能启用 | 批次 3 完成 |
|
||||
| **批次 5** | P6 | 10-12 天 | ai16 + 持续优化 | admin-portal + 审计日志 + iam P2.2 补全 + 持续优化 | 批次 4 完成 |
|
||||
|
||||
**总时间线**:49-64 天(约 10-13 周)
|
||||
|
||||
### 9.1 批次启动前 coord 仲裁清单
|
||||
|
||||
| 批次 | 仲裁内容 | 完成期限 |
|
||||
| ------ | --------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| 批次 1 | teacher-bff GraphQL schema 第一版(含 admin 命名空间)+ MF shell 暴露清单(含 GraphQL client 单例) | 批次 1 启动前 |
|
||||
| 批次 2 | student-bff GraphQL schema 第一版 | 批次 2 启动前 |
|
||||
| 批次 3 | parent-bff GraphQL schema 第一版 | 批次 3 启动前 |
|
||||
| 批次 4 | /internal/push 契约(采纳 ai02 02 §4.2 作为第一版) | 批次 4 启动前 |
|
||||
| 批次 5 | admin-portal schema 命名空间(复用 teacher-bff GraphQL endpoint) | 批次 5 启动前 |
|
||||
|
||||
### 9.2 批次完成信号
|
||||
|
||||
> **新流程**(2026-07-09 起):批次排期详见 [issues/workline.md](./issues/workline.md),仲裁异议详见 [issues/coord.md](./issues/coord.md)。各 AI 在 `issues/[模块名]_workline.md` 维护自己模块的甘特图。
|
||||
|
||||
- **批次 0 已完成**:✅ 2026-07-09 coord 完成全部 14 项任务(0.1-0.14)+ ai13 完成 0.15 并回写。**批次 1 启动公告**:批次 0 全部就绪,批次 1(P2)正式启动。
|
||||
- 0.1 arch:scan ✅(TS 14 模块 / Go 2 模块 / Proto 297 契约)
|
||||
- 0.2 coord-cross-review.md 修订 ✅(§8 总裁裁决落实记录)
|
||||
- 0.3 iam.proto 8 RPC ✅(12 RPC 总计)
|
||||
- 0.4 shared-ts Outbox 工具包 ✅(6 文件,typecheck/lint 零错误)
|
||||
- 0.5 content.proto ✅(4 service / 22 RPC)
|
||||
- 0.6/0.13 msg.proto ✅(3 service / 13 RPC)
|
||||
- 0.7/0.11 analytics.proto ✅(12 RPC 含 Stream)
|
||||
- 0.8 core_edu.proto ✅(5 service,+ClassService +AttendanceService)
|
||||
- 0.9 buf.gen.yaml gRPC 插件 ✅(go + python)
|
||||
- 0.10 events.proto AuditEvent ✅(9 message)
|
||||
- 0.12 ai.proto ✅(6 RPC,+GenerateLessonPlan +StreamGenerateQuestion)
|
||||
- 0.14 shared-go 包骨架 ✅(tracer/logger/jwks/env 4 模块)
|
||||
- 0.15 packages 骨架 ✅ **ai13 已完成并回写**(known-issues.md L677 + 03-long-term-architecture.md §14.1):
|
||||
- ui-tokens:coord 建立(colors/typography/spacing/shadows)
|
||||
- ui-components:ai13 建立(cn/ErrorBoundary/Loading/Empty/RequirePermission 5 组件)
|
||||
- hooks:ai13 建立(useAuth/usePermission/useViewports/useApi/useA11yId/useAriaLive/useToast/useTraceId 8 hooks + types)
|
||||
- ISSUE-036-ai13 已闭合(删除 REST→GraphQL 过渡方案,替换为 GraphQL 客户端架构最终方案)
|
||||
- **批次 1 已完成**:⏳
|
||||
- **批次 2 已完成**:⏳
|
||||
- **批次 3 已完成**:⏳
|
||||
- **批次 4 已完成**:⏳
|
||||
- **批次 5 已完成**:⏳
|
||||
|
||||
---
|
||||
|
||||
## 10. coord 验收清单
|
||||
|
||||
每个批次完成时,coord 必须验收以下项:
|
||||
|
||||
### 10.1 批次 1 验收(P2)
|
||||
|
||||
- [ ] iam gRPC 50052 可用,8 个 RPC 全部实现
|
||||
- [ ] iam.GetChildrenByParent 已落地(阻塞 parent-bff P4)
|
||||
- [ ] iam_student_guardians 表已建立
|
||||
- [ ] iam 已接入 shared-ts Outbox 工具包
|
||||
- [ ] teacher-bff GraphQL endpoint 可用
|
||||
- [ ] teacher-bff DownstreamClient 抽象已产出(供 ai04/ai05 复用)
|
||||
- [ ] teacher-portal MF shell + 共享组件已产出
|
||||
- [ ] api-gateway /parent 路由块已配置
|
||||
- [ ] api-gateway 错误码已补 GW_ 前缀
|
||||
|
||||
### 10.2 批次 2 验收(P3)
|
||||
|
||||
- [ ] core-edu gRPC 50053 可用
|
||||
- [ ] AttendanceService + GetClassesByTeacher + BatchGetClasses 已实现
|
||||
- [ ] classes 服务已下线,代码合并到 core-edu
|
||||
- [ ] student-bff GraphQL endpoint 可用
|
||||
- [ ] student-portal MF remote 接入完成
|
||||
- [ ] teacher-bff gRPC 调 core-edu 链路打通
|
||||
|
||||
### 10.3 批次 3 验收(P4)
|
||||
|
||||
- [ ] content gRPC 50054 可用
|
||||
- [ ] content.QuestionService RPC 已落地(阻塞 ai P5 出题)
|
||||
- [ ] data-ana gRPC 50055 可用
|
||||
- [ ] parent-bff GraphQL endpoint 可用
|
||||
- [ ] parent-portal MF remote 接入完成
|
||||
- [ ] teacher-bff / student-bff gRPC 调 content + data-ana 链路打通
|
||||
|
||||
### 10.4 批次 4 验收(P5)
|
||||
|
||||
- [ ] msg gRPC 50056 可用
|
||||
- [ ] ai gRPC 50058 可用
|
||||
- [ ] push-gateway HTTP /internal/push 可用
|
||||
- [ ] teacher-bff gRPC 调 ai + msg 链路打通(全部下游 gRPC 调用完成)
|
||||
- [ ] ai 调 content.QuestionService + core-edu.ExamService.GetExam + data-ana 链路打通
|
||||
|
||||
### 10.5 批次 5 验收(P6)
|
||||
|
||||
- [ ] admin-portal MVP 可用
|
||||
- [ ] admin 8 个新 Kafka topic + events.proto message 已补
|
||||
- [ ] 各服务 P6 硬化完成(/readyz 下游探针、Python ErrorFilter 对齐等)
|
||||
|
||||
---
|
||||
|
||||
## 11. AI 疑问记录机制
|
||||
|
||||
各 AI 在执行过程中若发现以下问题,**必须**记录到 [docs/issues.md](../issues.md):
|
||||
|
||||
1. **前置依赖未就绪**:本 AI 工作依赖的上游产出未到位
|
||||
2. **跨度过大**:裁决要求的工作跨度超出当前阶段合理范围
|
||||
3. **裁决冲突**:发现 coord-final-decisions.md 内部或与其他文档冲突
|
||||
4. **契约不明确**:与协作 AI 的接口契约未协调清楚
|
||||
5. **工作归属不明**:某项工作应该由谁负责不明确
|
||||
|
||||
### 11.1 记录格式(见 issues.md 模板)
|
||||
|
||||
```markdown
|
||||
### ISSUE-XXX: <问题标题>
|
||||
|
||||
- **提请 AI**:aiXX
|
||||
- **类别**:前置依赖未就绪 / 跨度过大 / 裁决冲突 / 契约不明确 / 工作归属不明
|
||||
- **阶段**:P2/P3/P4/P5/P6
|
||||
- **问题描述**:<详细描述>
|
||||
- **影响**:<对工作的影响>
|
||||
- **建议方案**:<AI 建议的解决方案>
|
||||
- **状态**:待 coord 仲裁 / coord 已裁决 / 已解决
|
||||
- **coord 裁决**:<coord 的裁决结果(coord 填写)>
|
||||
```
|
||||
|
||||
### 11.2 coord 处理流程
|
||||
|
||||
1. AI 在 issues.md 追加问题记录(状态:待 coord 仲裁)
|
||||
2. coord 定期(每批次结束)统一仲裁,填写 coord 裁决字段
|
||||
3. 状态改为"coord 已裁决"
|
||||
4. AI 按 coord 裁决执行,完成后状态改为"已解决"
|
||||
|
||||
---
|
||||
|
||||
## 12. 变更记录
|
||||
|
||||
| 日期 | 变更 | 决策者 |
|
||||
| ---------- | -------------------------------------------------------- | ----------------- |
|
||||
| 2026-07-09 | 初始创建,规划批次 0-5 + 14 个已识别问题(见 issues.md) | coord(用户确认) |
|
||||
325
docs/architecture/coord-cross-review.md
Normal file
325
docs/architecture/coord-cross-review.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# coord 交叉审查报告(架构设计外包阶段 2)
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-09
|
||||
> 审查执行:coord(协调 AI)
|
||||
> 关联文档:
|
||||
>
|
||||
> - [004 架构影响地图](./004_architecture_impact_map.md) — 架构设计意图唯一源
|
||||
> - [AI 分配方案](./ai-allocation.md) §8 交叉审查规则
|
||||
> - [项目规则](../../.trae/rules/project_rules.md)
|
||||
>
|
||||
> **本文档定位**:架构设计外包阶段 2 的过程性审查产物,记录 coord 对 18 份模块架构设计文档的跨模块一致性审查与裁决。裁决中涉及"架构设计意图"的结论已同步沉淀到 [004 架构影响地图](./004_architecture_impact_map.md) 对应章节,本文件保留完整审查过程与裁决依据作为追溯凭证。
|
||||
|
||||
---
|
||||
|
||||
## 1. 审查范围
|
||||
|
||||
| 维度 | 内容 |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 审查文档 | 18 份 AI 架构设计文档(`services/*/docs/01-understanding.md` + `02-architecture-design.md` + `apps/teacher-portal/README.md`) |
|
||||
| 审查维度 | ① 跨模块接口签名一致性 ② Kafka topic 不重复 ③ 端口不冲突 ④ 错误码前缀不重叠 |
|
||||
| 审查方式 | 4 个 search subagent 并行提取 4 类信息,coord 整合裁决 |
|
||||
| 审查依据 | [004 架构影响地图](./004_architecture_impact_map.md)、[project_rules](../../.trae/rules/project_rules.md)、[ai-allocation §8](./ai-allocation.md) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口签名一致性审查
|
||||
|
||||
### 2.1 Proto 包名规范冲突(P0 全局)
|
||||
|
||||
| 实际使用 | 规则要求 | 涉及 proto | 裁决 |
|
||||
| ---------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `next_edu_cloud.<domain>.v1` | `edu.<domain>.v1`(project_rules §5) | iam.proto / content.proto / msg.proto / core_edu.proto / analytics.proto / ai.proto / events.proto / push.proto(全部 8 份) | **裁决:保持现状 `next_edu_cloud.*`**。理由:全量迁移成本高且无功能收益,仅 project_rules §5 文字与实际不符。**整改**:修订 project_rules §5 包名规范为 `next_edu_cloud.<domain>.v1`,同步 004 §11.2 契约规则表。 |
|
||||
|
||||
### 2.2 Proto 契约缺口(P0 阻塞 parent-bff / P1 高优)
|
||||
|
||||
| # | 缺口 proto | 缺失内容 | 阻塞方 | 裁决 |
|
||||
| --- | -------------- | ------------------------------------------------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| 1 | iam.proto | `GetViewports` / `GetEffectivePermissions` / `Logout` / `GetPublicKey`(JWKS) | teacher-bff / student-bff / parent-bff | **P2 补全**:iam 在 proto 补 4 个 RPC,REST 已实现 |
|
||||
| 2 | iam.proto | `GetChildrenByParent` / `GetParentsByStudent`(家长-学生关联) | **parent-bff P0 阻塞** | **P4 前必须补全**:iam 新增 `ParentStudentService` 或扩展 `IamService`,parent-bff 核心场景依赖 |
|
||||
| 3 | iam.proto | `GetEffectiveDataScope` | data-ana(DataScope 解析) | **P4 补全**:iam 新增 RPC,data-ana gRPC 调用 |
|
||||
| 4 | content.proto | `ChapterService` / `QuestionService` 域 | student-bff / content 自身 | **P4 补全**:content 新增 2 个 service |
|
||||
| 5 | core_edu.proto | `AttendanceService`(出勤) | parent-bff / core-edu | **P3 补全**:core-edu 新增 service |
|
||||
| 6 | events.proto | `NotificationEvent` / `AIUsageEvent` / `UserEvent` / `RoleEvent` | msg / ai / iam | **P2-P5 分阶段补全**:events.proto 按事件类型补 message |
|
||||
| 7 | buf.gen.yaml | 缺 gRPC 插件 | 全部 | **P3 补全**:coord 在 buf.gen.yaml 加 `grpc-node` / `grpc-python` / `grpc-go` 插件 |
|
||||
|
||||
### 2.3 gRPC 启用时机(P1 高优)
|
||||
|
||||
**现状**:所有 6 个业务服务 proto 已定义 gRPC service,但全部仅实现 HTTP REST,gRPC server 未落地。004 §4.1 明确"BFF → 业务服务走 gRPC",但 teacher-bff 实际用 REST fetch。
|
||||
|
||||
**裁决**:
|
||||
|
||||
| 阶段 | gRPC 启用范围 | 理由 |
|
||||
| ---- | --------------------------------- | ----------------------------------------- |
|
||||
| P2 | 不启用 | teacher-bff REST 已跑通,无阻塞 |
|
||||
| P3 | iam + core-edu 启用 gRPC server | student-bff / parent-bff 将依赖,需先就绪 |
|
||||
| P4 | content + data-ana 启用 | ai07 student-portal / parent-portal 依赖 |
|
||||
| P5 | ai 启用(含 StreamChat 流式 RPC) | teacher-portal AI 辅助出题依赖 |
|
||||
|
||||
**整改**:004 §4.1 补充"gRPC 启用阶段矩阵"小节。
|
||||
|
||||
### 2.4 前端与后端端点不一致(P1 高优)
|
||||
|
||||
| # | 前端文档声明 | 后端实际端点 | 冲突类型 | 裁决 |
|
||||
| --- | ------------------------------------------ | ----------------------------------------------------- | ----------------- | --------------------------------------------------- |
|
||||
| 1 | `GET /api/v1/ai/generate-questions`(SSE) | `POST /ai/chat/stream` + `POST /ai/generate/question` | 路径 + 方法不一致 | **以 ai 服务为准**,ai07 修正 teacher-portal README |
|
||||
| 2 | `GET /iam/effective-permissions` | `GET /iam/permissions/effective` | 路径不一致 | **以 iam 为准**,ai07 修正 |
|
||||
| 3 | `GET /iam/rbac/...` | `GET /iam/viewports` / `/iam/roles` 等 | 路径不一致 | **以 iam 为准**,ai07 修正 |
|
||||
| 4 | `POST /parent/switch-child` | `POST /parent/children/:childId/select` | 路径不一致 | **以 parent-bff 为准**,ai07 修正 |
|
||||
|
||||
### 2.5 BFF API 风格:REST vs GraphQL(P1 高优)
|
||||
|
||||
**现状**:004 §11.3 提到 BFF GraphQL Yoga + DataLoader,但 teacher-bff 实际是 REST fetch。
|
||||
|
||||
**裁决**:**P2-P3 用 REST,P4 起若 BFF 切 GraphQL 再引入 urql**。004 §11.3 标注为"目标态",当前阶段以 REST 为准。
|
||||
|
||||
---
|
||||
|
||||
## 3. Kafka Topic 一致性审查
|
||||
|
||||
### 3.1 core-edu 教学事件 topic 命名双轨(P0 最严重)
|
||||
|
||||
| 004 §7.2 规范 | core-edu 代码实际 | 消费方文档 | 冲突 |
|
||||
| ----------------------------- | ----------------- | ------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `edu.teaching.exam.published` | `edu.exam.events` | msg 按规范消费,student-bff/parent-bff 按代码消费 | **契约破裂**:msg 订阅不上 core-edu 实际发布的 topic |
|
||||
|
||||
**裁决**:**统一为 `edu.teaching.<aggregate>.<action>` 风格**(遵循 004 §7.2)。core-edu 修改 `TOPIC_MAP`,student-bff / parent-bff 修改订阅 topic 名。
|
||||
|
||||
**整改清单**:
|
||||
|
||||
- core-edu `shared/outbox/outbox.publisher.ts`:`edu.exam.events` → `edu.teaching.exam.published` 等 4 个 topic
|
||||
- student-bff / parent-bff 文档:同步更新订阅 topic 名
|
||||
- msg 文档:已按规范,无需修改
|
||||
|
||||
### 3.2 004 §7.2 需补登的 topic(P1)
|
||||
|
||||
| Topic | 发布方 | 消费方 | 裁决 |
|
||||
| -------------------------------------------- | -------- | ----------------------------- | ------------------------------------------------ |
|
||||
| `edu.identity.user.deleted` | iam | core-edu / msg / push-gateway | ✅ 补登 |
|
||||
| `edu.identity.user.role_changed` | iam | 自身缓存 / msg | ✅ 补登 |
|
||||
| `edu.identity.role.created` | iam | msg | ✅ 补登 |
|
||||
| `edu.identity.role.updated` | iam | 自身缓存 / msg | ✅ 补登 |
|
||||
| `edu.notification.events` | msg | push-gateway | ✅ 补登(msg 必须明确发布此 topic) |
|
||||
| `edu.insight.ai.usage` | ai | data-ana | ✅ 补登 + events.proto 补 `AIUsageEvent` message |
|
||||
| `edu-cdc.next_edu_cloud.*`(5 个 CDC topic) | Debezium | data-ana | ✅ 补登 CDC topic 命名规范段 |
|
||||
|
||||
### 3.3 Python 服务 Outbox 豁免(P1)
|
||||
|
||||
**现状**:004 §12.2 禁止直接 Kafka producer,但 data-ana / ai 是 Python 服务无 MySQL 写事务,Outbox 不适用。
|
||||
|
||||
**裁决**:**派生数据事件(非业务事务写)允许直接 producer**。data-ana 的 `MasteryUpdated`、ai 的 `AIUsageRecorded` 属于派生数据,豁免 Outbox 约束。
|
||||
|
||||
**整改**:004 §12.2 补充"派生数据事件豁免条款"。
|
||||
|
||||
### 3.4 content Outbox 缺失(P1)
|
||||
|
||||
**现状**:004 §7.2 列 content 发 `edu.content.question.published`,但 content 骨架无 Outbox / Kafka 实现。
|
||||
|
||||
**裁决**:**ai05 在 P4 阶段 2 设计中补全 content Outbox**。
|
||||
|
||||
### 3.5 NotificationRequested 事件发布方未明确(P1)
|
||||
|
||||
**现状**:push-gateway 消费 `edu.notification.events`,但 msg 文档未明确发布此 topic。
|
||||
|
||||
**裁决**:**msg 服务为 `edu.notification.events` 的唯一发布方**,使用 Outbox 模式(msg 有 MySQL 写事务)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 端口冲突审查
|
||||
|
||||
### 4.1 严重前后端端口冲突(P0)
|
||||
|
||||
| 端口 | 前端占用 | 后端占用 | 冲突性质 |
|
||||
| ---- | -------------- | ------------------------------------- | --------------- |
|
||||
| 3001 | student-portal | classes(legacy,P3 合并入 core-edu) | dev server 冲突 |
|
||||
| 3002 | parent-portal | iam | dev server 冲突 |
|
||||
| 3003 | admin-portal | teacher-bff | dev server 冲突 |
|
||||
|
||||
**裁决**:**前端 4 端改用 4000-4003 端口区间**。理由:后端 3001-3010 已成事实基线(api-gateway 路由表 + 各服务自声明),前端迁移成本更低。
|
||||
|
||||
**整改清单**:
|
||||
|
||||
- ai07 修正 `apps/teacher-portal/README.md` §9.1 端口矩阵:3000→4000(teacher-portal)、3001→4001(student-portal)、3002→4002(parent-portal)、3003→4003(admin-portal)
|
||||
- ai07 修正 §9.1 备注"3001-3003 已被避让"的错误描述
|
||||
- ai07 修正 MF 配置示例中的 remoteEntry.js URL 端口
|
||||
|
||||
### 4.2 端口缺口(P1)
|
||||
|
||||
| 服务 | 缺失项 | 裁决 |
|
||||
| ----------------------------- | ----------------------------------------- | ------------------------------------------------ |
|
||||
| push-gateway | docs/01 + docs/02 未声明端口(实际 8081) | ai01 补充端口声明 |
|
||||
| core-edu | gRPC 端口未正式声明 | ai03 声明为 50053 |
|
||||
| content / msg / data-ana / ai | gRPC 端口未声明 | 各 AI 在 gRPC 启用阶段补充 |
|
||||
| 004 §1.2 | 完全未声明端口 | **coord 在 §1.2 新增 HTTP 端口 + gRPC 端口两列** |
|
||||
|
||||
### 4.3 全局端口矩阵(coord 裁决基线,已同步至 004 §1.2)
|
||||
|
||||
| 端口 | 服务 | 用途 | 阶段 |
|
||||
| ----- | -------------- | ------------------------------------ | ---- |
|
||||
| 4000 | teacher-portal | HTTP(Shell) | P2 |
|
||||
| 4001 | student-portal | HTTP(Remote) | P3 |
|
||||
| 4002 | parent-portal | HTTP(Remote) | P4 |
|
||||
| 4003 | admin-portal | HTTP(Remote) | P6 |
|
||||
| 8080 | api-gateway | HTTP + /healthz + /readyz + /metrics | P1 |
|
||||
| 8081 | push-gateway | HTTP + WebSocket | P5 |
|
||||
| 3002 | iam | HTTP/REST | P2 |
|
||||
| 50052 | iam | gRPC | P3 |
|
||||
| 3003 | teacher-bff | HTTP | P2 |
|
||||
| 3009 | student-bff | HTTP | P3 |
|
||||
| 3010 | parent-bff | HTTP | P4 |
|
||||
| 3004 | core-edu | HTTP | P3 |
|
||||
| 50053 | core-edu | gRPC | P3 |
|
||||
| 3005 | content | HTTP | P4 |
|
||||
| 50054 | content | gRPC(待声明) | P4 |
|
||||
| 3006 | data-ana | HTTP | P4 |
|
||||
| 50055 | data-ana | gRPC(待声明) | P4 |
|
||||
| 3007 | msg | HTTP | P5 |
|
||||
| 50056 | msg | gRPC(待声明) | P5 |
|
||||
| 3008 | ai | HTTP | P5 |
|
||||
| 50057 | ai | gRPC(待声明) | P5 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 错误码前缀审查
|
||||
|
||||
### 5.1 前缀不重叠审查结果
|
||||
|
||||
**当前已声明业务前缀互不重叠**(无两个服务声明相同前缀):
|
||||
|
||||
`IAM_` / `CORE_EDU_` / `CLASSES_` / `CONTENT_` / `MSG_` / `AI_` / `DATA_ANA_` / `STUDENT_BFF_` / `PARENT_BFF_` / `PUSH_`
|
||||
|
||||
### 5.2 P0 问题:teacher-bff 无自有前缀
|
||||
|
||||
**现状**:teacher-bff 用 `BAD_GATEWAY`(无前缀),与 student-bff(`STUDENT_BFF_`)/ parent-bff(`PARENT_BFF_`)不一致,且与前端期望 `BFF_TEACHER_` 冲突。
|
||||
|
||||
**裁决**:**3 个 BFF 统一为 `BFF_XXX_` 风格**(BFF 在前,符合前端期望):
|
||||
|
||||
- teacher-bff:`BFF_TEACHER_*`
|
||||
- student-bff:`BFF_STUDENT_*`(从 `STUDENT_BFF_` 迁移)
|
||||
- parent-bff:`BFF_PARENT_*`(从 `PARENT_BFF_` 迁移)
|
||||
|
||||
**整改**:ai03 / ai04 / ai07 同步更新文档与代码。
|
||||
|
||||
### 5.3 P0 问题:Python 服务信封偏离 ActionState
|
||||
|
||||
**现状**:data-ana / ai 用 `{success, data, degraded}` 而非 `{success, error:{code, message}}`,错误时无 `error.code`,前端无法用统一前缀路由 i18n。
|
||||
|
||||
**裁决**:**Python 服务统一为 ActionState 信封**:
|
||||
|
||||
- 成功:`{success: true, data: T}`
|
||||
- 失败:`{success: false, error: {code, message, details?}}`
|
||||
- 降级:`degraded` 作为 `details` 子字段(`details: {degraded: true}`)而非顶层字段
|
||||
|
||||
**整改**:ai06 修正 data-ana / ai 的 02-architecture-design.md §6.2 错误响应信封定义。
|
||||
|
||||
### 5.4 P0 问题:前端 `GW_` 期望与 api-gateway 无前缀冲突
|
||||
|
||||
**现状**:前端 README §9.4 声明 `GW_` → api-gateway("ai01 已用"),但 api-gateway 文档明确"无业务错误码前缀"。
|
||||
|
||||
**裁决**:**api-gateway 加 `GW_` 前缀**。理由:前端 i18n 路由需要统一前缀匹配,`UNAUTHORIZED` 等纯语义码改为 `GW_UNAUTHORIZED` 等。
|
||||
|
||||
**整改**:ai01 修正 api-gateway 01/02 文档错误码清单,加 `GW_` 前缀。
|
||||
|
||||
### 5.5 P1 问题:core-edu 子模块前缀
|
||||
|
||||
**现状**:core-edu 主体用 `CORE_EDU_*`,classes 子模块用 `CLASSES_*`(黄金模板历史遗留)。前端期望 `EXAMS_` / `HOMEWORK_` / `GRADES_` 分前缀。
|
||||
|
||||
**裁决**:**core-edu 统一用 `CORE_EDU_*`**(含 exams/homework/grades 子域),classes 模块保留 `CLASSES_*`(黄金模板历史遗留,P3 合并入 core-edu 后仍保留)。前端删除 `EXAMS_` / `HOMEWORK_` / `GRADES_` 期望。
|
||||
|
||||
**整改**:ai07 修正 teacher-portal README §9.4 前缀期望表。
|
||||
|
||||
### 5.6 P1 问题:push-gateway 01/02 前缀不一致
|
||||
|
||||
**现状**:01 用 `UNAUTHORIZED` / `PUSH_FAILED`(混用),02 统一 `PUSH_*`。
|
||||
|
||||
**裁决**:**以 02 的 `PUSH_*` 为准**,ai01 修正 01 文档。
|
||||
|
||||
### 5.7 P2 问题:错误信封结构不一致
|
||||
|
||||
| 服务 | 信封结构 | 裁决 |
|
||||
| ------------- | ----------------------------------------------------------- | ---------------------------------------- |
|
||||
| iam | `{success:false, error:{code, message, details?, traceId}}` | ✅ 基准 |
|
||||
| api-gateway | `{success:false, error:{code, message}}` | 补 `traceId` / `details` |
|
||||
| push-gateway | 无信封定义 | 明确为 ActionState 对齐 |
|
||||
| data-ana / ai | `{success, data, degraded}` | 改为 ActionState(见 §5.3) |
|
||||
| msg | ZodError 走默认 500 | 补 `ZodError` 分支返回 400 + ActionState |
|
||||
|
||||
---
|
||||
|
||||
## 6. 裁决整改清单汇总
|
||||
|
||||
| # | 优先级 | 整改项 | 责任方 | 涉及文件 |
|
||||
| --- | ------ | -------------------------------------------------------------------------- | --------- | ------------------------------------------------------------ |
|
||||
| 1 | P0 | core-edu topic 命名统一为 `edu.teaching.*` | ai03 | core-edu `outbox.publisher.ts` + student-bff/parent-bff 文档 |
|
||||
| 2 | P0 | 前端 4 端端口改用 4000-4003 | ai07 | teacher-portal README §9.1 + MF 配置 |
|
||||
| 3 | P0 | teacher-bff 补 `BFF_TEACHER_` 前缀 + student/parent-bff 迁移为 `BFF_XXX_` | ai03/ai04 | 3 BFF 文档 + 代码 |
|
||||
| 4 | P0 | Python 服务信封改为 ActionState | ai06 | data-ana/ai 02 文档 + 代码 |
|
||||
| 5 | P0 | api-gateway 加 `GW_` 前缀 | ai01 | api-gateway 01/02 文档 + 代码 |
|
||||
| 6 | P0 | iam 补 `GetChildrenByParent` / `GetParentsByStudent` proto RPC | ai02 | iam.proto + iam 02 文档 |
|
||||
| 7 | P1 | 004 §7.2 补登 6 个 topic + CDC 命名规范 | coord | 004 §7.2 |
|
||||
| 8 | P1 | 004 §1.2 新增 HTTP/gRPC 端口两列 | coord | 004 §1.2 |
|
||||
| 9 | P1 | 004 §4.1 补充 gRPC 启用阶段矩阵 | coord | 004 §4.1 |
|
||||
| 10 | P1 | 004 §12.2 补充派生数据事件 Outbox 豁免条款 | coord | 004 §12.2 |
|
||||
| 11 | P1 | project_rules §5 包名规范改为 `next_edu_cloud.*` | coord | project_rules §5 |
|
||||
| 12 | P1 | iam.proto 补 4 个 RPC(Viewport/EffectivePermissions/Logout/GetPublicKey) | ai02 | iam.proto + iam 02 文档 |
|
||||
| 13 | P1 | content.proto 补 Chapter/Question 域 | ai05 | content.proto + content 02 文档 |
|
||||
| 14 | P1 | core_edu.proto 补 Attendance 域 | ai03 | core_edu.proto + core-edu 02 文档 |
|
||||
| 15 | P1 | events.proto 补 NotificationEvent/AIUsageEvent/UserEvent/RoleEvent | coord | events.proto |
|
||||
| 16 | P1 | buf.gen.yaml 补 gRPC 插件 | coord | buf.gen.yaml |
|
||||
| 17 | P1 | ai07 修正前端 4 处端点不一致 | ai07 | teacher-portal README §1.3.1 + §3.5 |
|
||||
| 18 | P1 | content 补 Outbox 实现 | ai05 | content 代码 + 02 文档 |
|
||||
| 19 | P1 | msg 明确发布 `edu.notification.events` topic | ai05 | msg 02 文档 + 代码 |
|
||||
| 20 | P2 | api-gateway 信封补 traceId/details | ai01 | api-gateway 02 文档 |
|
||||
| 21 | P2 | push-gateway 定义 ActionState 信封 | ai01 | push-gateway 02 文档 |
|
||||
| 22 | P2 | msg ZodError 走 GlobalErrorFilter 返回 400 | ai05 | msg 代码 |
|
||||
| 23 | P2 | core-edu 完整错误码清单文档化 | ai03 | core-edu 02 文档 |
|
||||
| 24 | P2 | 建立 `infra/port-allocation.md` 全局端口表 | coord | infra/port-allocation.md |
|
||||
|
||||
---
|
||||
|
||||
## 7. 裁决生效说明
|
||||
|
||||
- 本章节裁决自发布之日起生效,各 AI 在下一阶段实施时必须遵循
|
||||
- **裁决的设计意图已沉淀到 [004 架构影响地图](./004_architecture_impact_map.md) 对应章节**(§1.2 端口矩阵、§4.2 gRPC 启用阶段矩阵、§7.2 topic 补登、§11.2 包名规范、§11.4 错误码前缀矩阵、§12.2 Outbox 豁免条款)
|
||||
- 整改清单中标注"coord"的责任项由协调 AI 执行,标注具体 AI 的由对应开发 AI 执行
|
||||
- 整改完成后各 AI 需在 `docs/troubleshooting/known-issues.md` 工作经验日志区追加记录
|
||||
- 若整改涉及源码修改,必须运行 `pnpm run arch:scan` 更新 arch.db
|
||||
|
||||
---
|
||||
|
||||
## 8. 总裁裁决落实记录(2026-07-09)
|
||||
|
||||
> 本节记录总裁最终裁决([president-final-rulings.md](./president-final-rulings.md))对本审查报告裁决的落实情况。
|
||||
|
||||
### 8.1 已落实项
|
||||
|
||||
| 审查章节 | 裁决内容 | 落实状态 |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
||||
| §2.3 gRPC 启用时机 | iam gRPC server 50052 从 P3 前移到 P2(I1 裁决) | ✅ 已同步至 004 §4.2 + port-allocation.md §5 |
|
||||
| §3.2 topic 补登 | 新增 `edu.iam.audit.created` + notification 4 个具体动作 topic + warning.triggered 复用 mastery topic | ✅ 已同步至 004 §7.2 |
|
||||
| §3.5 NotificationRequested 发布方 | 归 msg 发布,topic `edu.notification.requested` | ✅ 已同步至 004 §7.2 |
|
||||
| §6 整改清单 | content QuestionService 从 P5 前移到 P4(N3 裁决) | ✅ 已同步至 004 §16.7.4/§16.7.5 |
|
||||
|
||||
### 8.2 批次 0 产出(本日完成)
|
||||
|
||||
| 产出项 | 文件 | 状态 |
|
||||
| ----------------------- | ------------------------------------------------------------------ | -------------------------------------------------------- |
|
||||
| iam.proto 8 RPC 补全 | `packages/shared-proto/proto/iam.proto` | ✅ 12 RPC |
|
||||
| content.proto 补全 | `packages/shared-proto/proto/content.proto` | ✅ 4 service / 22 RPC |
|
||||
| msg.proto 补全 | `packages/shared-proto/proto/msg.proto` | ✅ 3 service / 13 RPC |
|
||||
| analytics.proto 扩展 | `packages/shared-proto/proto/analytics.proto` | ✅ 12 RPC(含 Stream) |
|
||||
| core_edu.proto 补全 | `packages/shared-proto/proto/core_edu.proto` | ✅ 5 service(+ClassService +AttendanceService) |
|
||||
| ai.proto 补全 | `packages/shared-proto/proto/ai.proto` | ✅ 6 RPC(+GenerateLessonPlan +StreamGenerateQuestion) |
|
||||
| events.proto AuditEvent | `packages/shared-proto/proto/events.proto` | ✅ 9 message(+AuditEvent) |
|
||||
| buf.gen.yaml gRPC 插件 | `packages/shared-proto/buf.gen.yaml` | ✅ go + python gRPC 插件 |
|
||||
| shared-ts Outbox 工具包 | `packages/shared-ts/src/outbox/` | ✅ 6 文件(module/service/publisher/types/schema/index) |
|
||||
| shared-go 包骨架 | `packages/shared-go/` | ✅ 4 模块(tracer/logger/jwks/env) |
|
||||
| packages UI 骨架 | `packages/ui-tokens` + `packages/ui-components` + `packages/hooks` | ✅ 3 包 / 20 文件 |
|
||||
| arch.db 更新 | `arch.db` | ✅ TS 14 模块 / Go 2 模块 / Proto 297 契约 |
|
||||
|
||||
### 8.3 未完事项(移交批次 1+)
|
||||
|
||||
- shared-go `go mod tidy` 需在 Go 环境就绪后执行(补 otel/jwt 依赖)
|
||||
- `buf generate` 需在 buf CLI 就绪后执行(生成 3 语言 gRPC stub)
|
||||
- packages `pnpm install` 已执行(shared-ts subagent 已验证 typecheck/lint 零错误)
|
||||
287
docs/architecture/coord-final-decisions.md
Normal file
287
docs/architecture/coord-final-decisions.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Coord 最终方案强制裁决
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-09
|
||||
> 决策者:coord(经用户确认)
|
||||
> 适用范围:Edu 项目全部 16 份 AI 设计文档(ai01-ai16)
|
||||
> 关联文档:[004 架构影响地图 §16](./004_architecture_impact_map.md)、[ai-allocation.md](./ai-allocation.md)、[project_rules.md](../../.trae/rules/project_rules.md)
|
||||
|
||||
---
|
||||
|
||||
## 0. 裁决原则
|
||||
|
||||
用户反馈:**"前面简略实现后面要重写,完全没必要,直接使用最好的方式工作"**。
|
||||
|
||||
各 AI 设计文档中存在大量"P2 简化 → P3 重构 → P4 再重构"的渐进式过渡方案,导致前期代码后期必废弃。coord 裁决如下:
|
||||
|
||||
### 0.1 强制覆盖声明
|
||||
|
||||
- **本文档 > 各 AI 设计文档**:凡本文档列出的裁决项,各 AI 设计文档中与之冲突的"中间过渡方案"一律作废,**直接采用最终方案**
|
||||
- **不分阶段**:最终方案在服务首次落地时即采用,不得以"阶段未到"为由先实现简化版
|
||||
- **回写义务**:各 AI 收到本文档后,必须在 3 个工作日内回写自己的 02-architecture-design.md,删除所有"中间过渡方案"描述,对齐最终方案
|
||||
|
||||
### 0.2 裁决依据
|
||||
|
||||
- 用户明确指示"直接使用最好的方式"
|
||||
- project_rules.md §3.1 已规定企业级规范(如 Dockerfile 多阶段、Zod 验证、GlobalErrorFilter),无理由分阶段
|
||||
- 黄金模板 services/classes/ 已实现全部最终方案,其他服务对齐即可
|
||||
|
||||
---
|
||||
|
||||
## 1. 通用裁决(适用全部服务)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 | 影响服务 |
|
||||
| --- | --------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| G1 | **Dockerfile** | "P3 重构"、"P5 重构"、"待落地" | **首次实现即多阶段构建**(builder + runtime) | 全部 |
|
||||
| G2 | **/readyz 探针** | "P2 仅返回 ok"、"P3+ 补下游探针" | **首次实现即检查全部下游依赖**(DB SELECT 1 / Redis PING / Kafka 连接 / gRPC 下游可达) | 全部 |
|
||||
| G3 | **/healthz** | "P2 改造" | **首次实现即 liveness 探针** | 全部 |
|
||||
| G4 | **Logger** | "待引入 slog"、"console.log → P3 修复" | **首次实现即结构化日志**(TS pino / Go slog / Python structlog),禁止 console.log | 全部 |
|
||||
| G5 | **Metrics** | "自定义业务指标待实现" | **首次实现即 /metrics 端点 + 基础业务指标** | 全部 |
|
||||
| G6 | **Tracer** | "资源属性待补"、"span 待补" | **首次实现即 OTel SDK + OTLP exporter + 完整资源属性 + 全链路 span** | 全部 |
|
||||
| G7 | **Zod 验证** | "P2 补全"、"P3 实施中"、"部分 → P4 全 parse" | **Controller 层首次实现即全量 schema.parse(body/query/params)** | 全部 TS |
|
||||
| G8 | **GlobalErrorFilter** | "待落地"、"P4+ 对齐 ActionState" | **首次实现即注册 GlobalErrorFilter + 响应信封严格对齐 ActionState 结构** | 全部 |
|
||||
| G9 | **优雅关闭** | "P2 补全" | **首次实现即 SIGTERM → app.close() → shutdownTracer(),按序关闭 HTTP→DB→Redis→Kafka** | 全部 |
|
||||
| G10 | **DB 连接模式** | "const db vs getDb()" | **统一 getDb() 函数**(对齐 classes 黄金模板) | 全部 TS 持 DB 服务 |
|
||||
| G11 | **ID 策略** | "cuid2 vs randomUUID" | **统一 cuid2**(对齐 classes 黄金模板) | 全部 TS 持 DB 服务 |
|
||||
| G12 | **ESM import** | 缺 .js 后缀 | **所有相对 import 带 .js 后缀**(NestJS ESM 模式) | 全部 TS |
|
||||
| G13 | **import type** | 混用 | **仅用于类型的导入必须 import type** | 全部 TS |
|
||||
| G14 | **错误码前缀** | api-gateway 无前缀 | **每个服务错误码必须用服务名大写前缀**(GW_/IAM_/CORE_EDU_/CONTENT_/MSG_/DATA_ANA_/AI_/PUSH_/BFF_TEACHER_/BFF_STUDENT_/BFF_PARENT_/CLASSES_) | 全部 |
|
||||
| G15 | **错误码子前缀** | 保留 EXAMS_/HOMEWORK_/GRADES_ | **统一用聚合域前缀 CORE_EDU_***,禁止子前缀 | 前端 + core-edu |
|
||||
| G16 | **Kafka topic 命名** | edu.notification.events 抽象名 | **edu.\<domain>.\<aggregate>.\<action>** 具体动作(如 edu.notification.sent) | 全部 |
|
||||
| G17 | **proto 包名** | 不一致 | **next_edu_cloud.\<domain>.v1** | 全部 |
|
||||
|
||||
---
|
||||
|
||||
## 2. BFF 层专项裁决(teacher-bff / student-bff / parent-bff)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | ------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| B1 | **API 风格** | "P2 REST → P4 GraphQL"、"P3 REST → P4+ GraphQL" | **P2 起直接 GraphQL**(GraphQL Yoga + DataLoader) |
|
||||
| B2 | **对下游通信** | "P2 HTTP fetch → P3+ gRPC"、"P3 HTTP → P3+ gRPC" | **首次实现即 gRPC 调用下游**(iam/core-edu/content/data-ana/msg/ai 全部 gRPC) |
|
||||
| B3 | **权限装饰器** | "BFF 加 @RequirePermission"、"待仲裁" | **BFF 豁免 @RequirePermission**(权限由 Gateway JWT + 下游服务负责),BFF 仅校验 x-user-id 存在 |
|
||||
| B4 | **越权防御** | "仅 teacher-bff 透传"、"student-bff 待仲裁" | **全部 BFF 强制自我越权防御**:student-bff 比对 userId、parent-bff 校验 childId 绑定列表、teacher-bff 校验 teacherId 与班级关系 |
|
||||
| B5 | **错误码前缀** | "STUDENT_BFF_ vs BFF_STUDENT_" | **BFF_TEACHER\_ / BFF_STUDENT\_ / BFF_PARENT_**(统一 BFF_ 前缀) |
|
||||
| B6 | **缓存策略** | "不缓存 vs Redis 短缓存" | **Redis 5-30s 短缓存**(对齐 004 §6.2 BFF 混合读策略) |
|
||||
| B7 | **Kafka 订阅** | "P3 评估"、"P5 评估" | **P2-P4 不订阅 Kafka**(仅同步聚合),P5 push-gateway 落地后再订阅 |
|
||||
| B8 | **DownstreamClient 抽象** | "是否回写 teacher-bff" | **回写 teacher-bff**,作为 BFF 模式 v2 标准抽象,3 个 BFF 统一使用 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 业务服务专项裁决
|
||||
|
||||
### 3.1 iam(ai06)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | ------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| I1 | **gRPC server** | "P2 仅 REST → P3 启用 gRPC" | **P2 即启用 gRPC server 50052**,同时暴露 REST + gRPC |
|
||||
| I2 | **Outbox** | "iam 自建 → core-edu 落地后回写 shared-ts" | **直接建 shared-ts Outbox 工具包**,iam 首次实现即用 |
|
||||
| I3 | **PermissionGuard** | "本地 ROLE_PERMISSIONS map → DB 驱动" | **首次实现即 DB 驱动 + Redis 缓存**,废弃本地 map |
|
||||
| I4 | **AuthMiddleware** | "P2 不注册 → Controller 直读 header" | **首次实现即注册 AuthMiddleware**,Controller 通过 @Req() 获取用户上下文 |
|
||||
| I5 | **JWT 密钥** | "P2 本地文件 → P6 Vault" | **P2 本地文件**(IAM_PRIVATE_KEY_PATH / IAM_PUBLIC_KEY_PATH),P6 迁 Vault 单列后续 |
|
||||
| I6 | **家长-学生关联** | "接口缺失 → P3 补" | **P2 即补全**:iam_student_guardians 表 + GetChildrenByParent RPC + GET /iam/children REST(P0 阻塞 parent-bff) |
|
||||
| I7 | **API 版本化** | "待 coord 审查" | _*采用 /iam/v1/* 前缀_*,Gateway 同步调整 |
|
||||
| I8 | **iam 端点路径** | /iam/permissions/effective vs /iam/effective-permissions | **统一 GET /iam/permissions/effective**(RESTful 资源路径风格) |
|
||||
|
||||
### 3.2 core-edu(ai08)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | ----------------------------------------- | -------------------------------------- | -------------------------------------------------------------------- |
|
||||
| C1 | **classes 合并** | "P3 初期 vs P3 末期" | **P3 开始前直接合并**:classes 代码迁移到 core-edu,classes 服务下线 |
|
||||
| C2 | **gRPC server** | "P3 启用" | **P3 首次实现即启用 gRPC server 50053** |
|
||||
| C3 | **Kafka console.log** | "P3 修复" | **首次实现即用 logger.info**,禁止 console.log |
|
||||
| C4 | **AttendanceService** | "待 coord 补 proto" | **P3 首次实现即补全 proto + 实现** |
|
||||
| C5 | **GetClassesByTeacher / BatchGetClasses** | "待新增" | **P3 首次实现即补全** |
|
||||
| C6 | **排课 room_id** | "P3 实现 vs 预留 vs P6+" | **P3 预留字段**(不实现逻辑,但 schema 含字段) |
|
||||
| C7 | **成绩计算公式** | "scope 优先级待定" | **class > subject > school**(越细粒度优先) |
|
||||
| C8 | **作业 grace_period** | "0 秒 vs 300 秒" | **默认 300 秒**(5 分钟宽限) |
|
||||
| C9 | **schema 强制校验** | "P3 JSON Schema vs 文档约束" | **P3 仅文档约束**,P6+ 引入 schema registry |
|
||||
| C10 | **exam.submitted 事件** | "包含完整 answers vs 仅 submission_id" | **仅含 submission_id**(避免事件过大) |
|
||||
| C11 | **archived 考试** | "物理迁移 vs 软删除 vs P6+" | **仅软删除**(is_archived 字段) |
|
||||
|
||||
### 3.3 content(ai09)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | ------------------- | -------------------- | ------------------------------------------------ |
|
||||
| N1 | **gRPC server** | "gRPC 待实现" | **P4 首次实现即启用 gRPC server 50054** |
|
||||
| N2 | **/readyz** | "仅 DB → P4 多依赖" | **首次实现即检查 DB/Neo4j/Kafka** |
|
||||
| N3 | **QuestionService** | "P5 阻塞待补 proto" | **P4 即补全 QuestionService proto**(不等到 P5) |
|
||||
| N4 | **ErrorFilter** | "错误响应结构待对齐" | **首次实现即对齐 ActionState** |
|
||||
| N5 | **ChapterService** | "缺 GetChapter" | **P4 首次实现即补全** |
|
||||
|
||||
### 3.4 msg(ai10)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | --------------------- | --------------------------- | ----------------------------------------------------- |
|
||||
| M1 | **gRPC server** | "gRPC 待实现" | **P5 首次实现即启用 gRPC server 50056** |
|
||||
| M2 | **/readyz** | "仅 DB → P5 多依赖" | **首次实现即检查 DB/ES/Redis/Kafka/PushGateway** |
|
||||
| M3 | **ZodError** | "未处理 → P5 处理" | **首次实现即处理 ZodError 分支** |
|
||||
| M4 | **调用 push-gateway** | "gRPC PushService.Push" | **HTTP POST /internal/push**(coord 仲裁豁免 gRPC) |
|
||||
| M5 | **事件 topic** | "edu.teaching.assignment.*" | **edu.teaching.homework.***(与 core-edu 发布方一致) |
|
||||
|
||||
### 3.5 data-ana(ai11)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | --------------------------- | --------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| D1 | **ErrorFilter** | "v2 对齐 ActionState" | **首次实现即对齐 ActionState** |
|
||||
| D2 | **Dockerfile** | "v2 设计多阶段" | **首次实现即多阶段** |
|
||||
| D3 | **ClickHouse DDL** | "待 coord 建目录" | **直接建 infra/clickhouse/ddl/** |
|
||||
| D4 | **analytics.proto 扩展** | "待 coord 审议" | **P4 即补全**:4 端 Dashboard + Warning + MasteryDistribution + SubscribeMasteryUpdate Stream RPC |
|
||||
| D5 | **warning.triggered topic** | "待 coord 登记" | **已登记**,004 §7.2 + events.proto MasteryEvent |
|
||||
|
||||
### 3.6 ai(ai12)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | ---------------------- | ------------------------- | ------------------------------------------------------------------------------ |
|
||||
| A1 | **ErrorFilter** | "待对齐" | **首次实现即对齐 ActionState** |
|
||||
| A2 | **Dockerfile** | "实现阶段需新增" | **首次实现即多阶段** |
|
||||
| A3 | **gRPC 端口** | "待 coord 补登" | **已补登 50058**(见 infra/port-allocation.md) |
|
||||
| A4 | **ai.proto 补全** | "待 coord" | **P5 首次实现即补全** GenerateLessonPlan / StreamGenerateQuestion |
|
||||
| A5 | **AIUsageEvent proto** | "待 coord" | **已补全** events.proto |
|
||||
| A6 | **Prompt 模板存储** | "P5 YAML / P6+ DB" | **P5 直接用 DB 存储**(避免后期迁移) |
|
||||
| A7 | **备课工作流** | "P5 Redis / P6+ Temporal" | **P5 用 FastAPI BackgroundTasks + Redis**(Temporal 引入成本高,P6+ 单独评估) |
|
||||
| A8 | **多模态** | "P5 文本 / P6+ 多模态" | **P5 仅文本**,proto v2 预留字段 |
|
||||
|
||||
### 3.7 push-gateway(ai02)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | ------------------ | ---------------------------- | ----------------------------------------------------------------- |
|
||||
| P1 | **gRPC** | "备选 gRPC PushService" | **彻底删除 gRPC 设计**,纯 HTTP + WebSocket(coord 仲裁豁免) |
|
||||
| P2 | **X-Internal-Key** | "→ X-Internal-Token P5 立即" | **首次实现即 X-Internal-Token**(环境变量 INTERNAL_API_TOKEN) |
|
||||
| P3 | **Dockerfile** | "P5 重构" | **首次实现即多阶段** |
|
||||
| P4 | **slog** | "待引入" | **首次实现即用 slog** |
|
||||
| P5 | **SSE 降级** | "P7+ 考虑" | **本期仅 HTTP 长轮询预留**,SSE 不实现 |
|
||||
| P6 | **断线重连** | "本期仅协议预留" | **本期仅协议预留**(session_id + last_seq 字段),P6 实现补推逻辑 |
|
||||
|
||||
### 3.8 api-gateway(ai01)
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | -------------------- | ------------------------------------------- | ----------------------------------------------------- |
|
||||
| W1 | **错误码前缀** | "无前缀" | **GW_***(GW_UNAUTHORIZED 等) |
|
||||
| W2 | **错误信封** | "recovery/ratelimit/circuit-breaker 待整改" | **首次实现即对齐 ActionState** |
|
||||
| W3 | **slog** | "待引入" | **首次实现即用 slog** |
|
||||
| W4 | **/readyz** | "待重构" | **首次实现即真实健康检查**(轮询下游 /healthz) |
|
||||
| W5 | **业务 metrics** | "待实现" | **首次实现即基础业务指标**(请求量/延迟/错误率/熔断) |
|
||||
| W6 | **tracer 资源属性** | "待补" | **首次实现即完整**(version/env/host) |
|
||||
| W7 | **DevMode 防护** | "P1 待办" | **首次实现即生产防护**(DEV_MODE 仅非生产环境生效) |
|
||||
| W8 | **per-服务实例熔断** | "P6 待办" | **保持共享 downstream 熔断**,per-实例 P6 单独评估 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端 4 portal 专项裁决
|
||||
|
||||
| # | 过渡项 | 禁止的中间方案 | 强制最终方案 |
|
||||
| --- | ------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| F1 | **admin-portal 端口** | 3003 / 3000 / 4003 三处矛盾 | **统一 4003** |
|
||||
| F2 | **admin-portal MF 配置** | teacher_app@localhost:3000 | **teacher_app@localhost:4000** |
|
||||
| F3 | **Web Vitals** | FID vs INP 混用 | **统一 INP**(CWV 2024 标准) |
|
||||
| F4 | **i18n key 命名** | bff.error / bffTeacher.error / bffStudent.error 混用 | **统一 error.\<service>.\<code_snake>**(如 error.iam.invalid_credentials、error.bffTeacher.validation_error) |
|
||||
| F5 | **错误码子前缀** | parent-portal 保留 GRADES_/HOMEWORK_ | **统一删除子前缀**,用 CORE_EDU_* |
|
||||
| F6 | **iam 权限端点** | /iam/permissions/effective vs /iam/effective-permissions | **统一 /iam/permissions/effective** |
|
||||
| F7 | **权限点命名风格** | TEACHER_DASHBOARD_VIEW / STUDENT_DASHBOARD_READ / GRADES_READ_CHILD 混用 | **统一 \<RESOURCE>\_\<ACTION>**(如 DASHBOARD_VIEW、EXAM_READ、GRADE_READ),数据范围用后缀 _OWN/_CHILD(如 GRADE_READ_CHILD) |
|
||||
| F8 | **packages 归属** | "ai07/ai13 维护 vs coord 维护" | **ai13 维护 ui-tokens/ui-components/hooks,coord 仅维护 shared-ts/contracts** |
|
||||
| F9 | **GraphQL vs REST** | "P2-P6 REST,未来切 GraphQL" | **P2 起 BFF 用 GraphQL,前端用 urql/apollo** |
|
||||
| F10 | **MF 暴露粒度** | "AppShell 整体 vs 细粒度" | **暴露 AppShell 整体**,各 Remote 自行决定内部布局 |
|
||||
| F11 | **teacher-portal 归属** | "004 §15.1 写 ai07,ai-allocation §3.2 写 ai13" | **ai13 负责 teacher-portal**(ai07 负责 classes 黄金模板) |
|
||||
| F12 | **localStorage token** | "P6 评估迁移 httpOnly Cookie" | **P2 用 localStorage**(httpOnly Cookie + CSRF Token P6 单独评估) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 跨服务契约裁决(一次性补全,不分阶段)
|
||||
|
||||
### 5.1 proto 契约补全(coord 负责,P2 启动前全部就位)
|
||||
|
||||
| proto 文件 | 待补内容 | 阻塞阶段 | 完成期限 |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||
| events.proto | ✅ 已补全(UserEvent/RoleEvent/NotificationEvent/MasteryEvent/AIUsageEvent/KnowledgePointEvent/QuestionEvent) | — | 已完成 |
|
||||
| iam.proto | GetViewports / GetEffectivePermissions / GetEffectiveAccess / Logout / GetPublicKey / BatchGetUsers / GetEffectiveDataScope / GetChildrenByParent | P2 | P2 启动前 |
|
||||
| core_edu.proto | AttendanceService.* / GetClassesByTeacher / BatchGetClasses | P3 | P3 启动前 |
|
||||
| content.proto | ChapterService.GetChapter / QuestionService.* / TextbookService.ListTextbooks 补全 | P4 | P4 启动前 |
|
||||
| analytics.proto | GetTeacherDashboard / GetStudentDashboard / GetParentDashboard / GetAdminDashboard / GetWarnings / GetMasteryDistribution / SubscribeMasteryUpdate (stream) | P4 | P4 启动前 |
|
||||
| ai.proto | GenerateLessonPlan / StreamGenerateQuestion / 字段扩展 | P5 | P5 启动前 |
|
||||
| msg.proto | BatchSendNotification / GetUnreadCount / BatchMarkAsRead / MarkAllAsRead / RecallNotification + NotificationPreferenceService + NotificationTemplateService + 3 个 message | P5 | P5 启动前 |
|
||||
|
||||
### 5.2 接口命名统一
|
||||
|
||||
| 调用方 | 被调方 | 统一命名 | 说明 |
|
||||
| ----------------- | ------------ | ------------------------------------------------------------ | ---------------------------- |
|
||||
| core-edu | content | **KnowledgeGraphService.GetPrerequisites / GetLearningPath** | 禁用 ContentService 命名 |
|
||||
| teacher-bff | data-ana | **GetTeacherDashboard**(无 Stats 后缀) | 统一命名 |
|
||||
| msg / student-bff | push-gateway | **HTTP POST /internal/push**(body 含 user_id) | 豁免 gRPC |
|
||||
| ai | core-edu | **ExamService.GetExam** | 方向:ai 调 core-edu,非反向 |
|
||||
|
||||
### 5.3 gRPC server 启用时机
|
||||
|
||||
| 服务 | gRPC 端口 | 启用时机 | 说明 |
|
||||
| ------------ | --------- | --------------- | ---------------------------------- |
|
||||
| iam | 50052 | **P2** | 首次实现即启用(覆盖"P2 仅 REST") |
|
||||
| core-edu | 50053 | **P3** | 首次实现即启用 |
|
||||
| content | 50054 | **P4** | 首次实现即启用 |
|
||||
| data-ana | 50055 | **P4** | 首次实现即启用 |
|
||||
| msg | 50056 | **P5** | 首次实现即启用 |
|
||||
| ai | 50058 | **P5** | 首次实现即启用 |
|
||||
| push-gateway | — | **不启用** | 豁免 gRPC |
|
||||
| BFF(3 个) | — | **不暴露 gRPC** | 仅对下游走 gRPC |
|
||||
|
||||
---
|
||||
|
||||
## 6. 待 P6+ 单独评估事项(不阻塞当前阶段)
|
||||
|
||||
以下事项不属于"中间过渡方案",属于"未来扩展",保持原状:
|
||||
|
||||
| # | 事项 | 评估时机 |
|
||||
| --- | ------------------------------------------------------------------- | --------- |
|
||||
| X1 | ai 发布 edu.insight.ai.generated / feedback / workflow 3 个新 topic | P6+ |
|
||||
| X2 | admin-portal 8 个新 Kafka topic + events.proto message | P6 实施时 |
|
||||
| X3 | admin-portal 审计日志/安全事件归属(iam vs audit-service) | P6 |
|
||||
| X4 | ai 备课工作流迁移 Temporal | P6+ |
|
||||
| X5 | ai Prompt 模板 DB 存储迁移(已在 A6 裁决 P5 直接用 DB) | — |
|
||||
| X6 | admin-portal Grafana/Jaeger iframe 嵌入 | P6 |
|
||||
| X7 | teacher-portal localStorage → httpOnly Cookie 迁移 | P6 |
|
||||
| X8 | iam JWT 密钥迁移 Vault | P6 |
|
||||
| X9 | api-gateway per-服务实例熔断 | P6 |
|
||||
| X10 | schema registry 引入(events.proto 强制校验) | P6+ |
|
||||
|
||||
---
|
||||
|
||||
## 7. 各 AI 回写义务
|
||||
|
||||
各 AI 收到本文档后,必须执行以下回写:
|
||||
|
||||
### 7.1 回写内容
|
||||
|
||||
1. 删除自己 02-architecture-design.md 中所有"中间过渡方案"描述
|
||||
2. 对齐本文档的最终方案
|
||||
3. 更新 §8 风险与假设(移除已裁决项)
|
||||
|
||||
### 7.2 回写期限
|
||||
|
||||
| AI | 服务 | 回写期限 | 阻塞阶段 |
|
||||
| ---- | -------------- | ---------------------- | -------- |
|
||||
| ai01 | api-gateway | P2 启动前 | — |
|
||||
| ai02 | push-gateway | P5 启动前 | — |
|
||||
| ai03 | teacher-bff | **P2 启动前** | P2 |
|
||||
| ai04 | student-bff | P3 启动前 | — |
|
||||
| ai05 | parent-bff | P4 启动前 | — |
|
||||
| ai06 | iam | **P2 启动前** | P2 |
|
||||
| ai07 | classes | 已是黄金模板,无需回写 | — |
|
||||
| ai08 | core-edu | P3 启动前 | — |
|
||||
| ai09 | content | P4 启动前 | — |
|
||||
| ai10 | msg | P5 启动前 | — |
|
||||
| ai11 | data-ana | P4 启动前 | — |
|
||||
| ai12 | ai | P5 启动前 | — |
|
||||
| ai13 | teacher-portal | **P2 启动前** | P2 |
|
||||
| ai14 | student-portal | P3 启动前 | — |
|
||||
| ai15 | parent-portal | P4 启动前 | — |
|
||||
| ai16 | admin-portal | P6 启动前 | — |
|
||||
|
||||
### 7.3 coord 验收
|
||||
|
||||
各 AI 回写完成后,coord 将重新运行 §8 交叉审查,确认无"中间过渡方案"残留。
|
||||
|
||||
---
|
||||
|
||||
## 8. 变更记录
|
||||
|
||||
| 日期 | 变更 | 决策者 |
|
||||
| ---------- | --------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
||||
| 2026-07-09 | 初始创建,覆盖 G1-G17 / B1-B8 / I1-I8 / C1-C11 / N1-N5 / M1-M5 / D1-D5 / A1-A8 / P1-P6 / W1-W8 / F1-F12 共 80+ 项裁决 | coord(用户确认) |
|
||||
129
docs/architecture/issues/contracts/admin-portal_contract.md
Normal file
129
docs/architecture/issues/contracts/admin-portal_contract.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# admin-portal 对接契约
|
||||
|
||||
> 负责人:ai16
|
||||
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无。admin-portal 是前端微前端 Remote。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
| 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 |
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。admin-portal 消费 teacher-bff GraphQL admin namespace,自身不提供 schema。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
无(前端不定义错误码前缀,透传 BFF 错误码)。
|
||||
|
||||
### 1.6 微前端架构(补充)
|
||||
|
||||
| 角色 | 说明 |
|
||||
| ---------------------- | ----------------------------------------------- |
|
||||
| MF Remote | 管理后台是微前端远程模块 |
|
||||
| 暴露的 remote 模块 | AdminApp(管理后台完整应用)、shared 管理端组件 |
|
||||
| module federation 配置 | `apps/admin-portal/module-federation.config.ts` |
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无。前端不直接调 gRPC。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。前端不直接订阅 Kafka(审计日志通过 GraphQL 查询,非直接订阅)。
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
| 被调用方 | 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.4 GraphQL 查询域(经 api-gateway 代理到 teacher-bff admin namespace)
|
||||
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] api-gateway HTTP :8080 启用(ai01)—— 前端请求入口 + admin 角色校验
|
||||
- [ ] teacher-bff GraphQL :3003 启用(ai03)—— admin namespace 可用
|
||||
- [ ] iam gRPC 50052 启用(ai06)—— 用户/角色/审计日志数据来源
|
||||
- [ ] edu.iam.audit.created topic 有事件发布(ai06)—— 审计日志来源
|
||||
- [ ] data-ana gRPC 50055 启用(ai11)—— adminDashboard 数据来源
|
||||
- [ ] push-gateway WebSocket :8081/ws 启用(ai02)—— 实时通知
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] admin-portal dev server :4003 启用
|
||||
- [ ] MF Remote 可被 AppShell 加载(暴露 AdminApp 模块)
|
||||
- [ ] 独立壳渲染(首页 + 导航 + 路由守卫 + admin 角色校验)
|
||||
- [ ] 登录流程可用(POST /api/auth/login 获取 JWT,前端校验 admin 角色)
|
||||
- [ ] GraphQL 查询可执行(currentUser / adminDashboard / auditLogs 返回数据)
|
||||
- [ ] 用户/角色 CRUD 可执行(createUser / updateRolePermissions)
|
||||
- [ ] WebSocket 通知可接收
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
admin-portal 是前端,无下游消费方。但对开发体验提供:
|
||||
|
||||
- **Storybook**:各组件独立 story(含权限矩阵编辑器、审计日志表格等复杂组件)
|
||||
- **MSW handlers**:`apps/admin-portal/src/mocks/handlers.ts`,拦截所有 GraphQL/HTTP 请求
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实上游就绪前,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 数据一致)
|
||||
- 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
|
||||
- **权限矩阵 mock**:内置固定 5 个角色 + 完整权限矩阵(teacher/student/parent/admin/super_admin)
|
||||
- **环境切换**:通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制,上游就绪后设为 `disabled`
|
||||
108
docs/architecture/issues/contracts/ai_contract.md
Normal file
108
docs/architecture/issues/contracts/ai_contract.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# ai 对接契约
|
||||
|
||||
> 负责人:ai12
|
||||
> 关联:[matrix.md](./matrix.md)、[ai.proto](../../../packages/shared-proto/proto/ai.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 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 |
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
无对外 HTTP 端点,仅 gRPC(含 2 个 Server Streaming RPC:StreamChat / StreamGenerateQuestion)。
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
| Topic | Event | 消费方 |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------- | -------- |
|
||||
| edu.ai.usage.events | AIUsageEvent(operation: chat/generate_question/optimize_expression/lesson_preparation) | data-ana |
|
||||
|
||||
> 注:AIUsageEvent 豁免 Outbox 模式(派生数据事件,见 004 §12.2 + §15.3 #6)。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`AI_`(如 AI_PROVIDER_UNAVAILABLE、AI_TOKEN_LIMIT_EXCEEDED、AI_CONTENT_FILTERED)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 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) |
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
| Topic | Event | 发布方 | mock 策略 |
|
||||
| ---------------------------------- | ------------------- | -------------- | -------------------------------------- |
|
||||
| edu.content.knowledge_point.events | KnowledgePointEvent | content (ai09) | content 就绪前不订阅,使用内置知识点表 |
|
||||
| edu.content.question.events | QuestionEvent | content (ai09) | content 就绪前忽略 |
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
| 被调用方 | Method.Path | 用途 | mock 策略 |
|
||||
| -------------------------------- | ------------------------- | ------------------ | ------------------------------------------------------------------ |
|
||||
| LLM Provider(OpenAI/百川/本地) | POST /v1/chat/completions | 调用大模型生成内容 | 开发期使用本地 mock server 返回固定 ChatResponse,不消耗真实 token |
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] content gRPC 50054 启用(ai09)—— 知识点维度 + 题库检索
|
||||
- [ ] edu.content.knowledge_point.events topic 有事件发布(ai09)
|
||||
- [ ] data-ana gRPC 50055 启用(ai11)—— 学生薄弱点(可选,ai 可先独立运行)
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] ai gRPC 50057 启用(HealthService.Check 返回 SERVING)
|
||||
- [ ] AiService.Chat / StreamChat 可调用(含流式响应)
|
||||
- [ ] AiService.GenerateQuestion / StreamGenerateQuestion 可调用
|
||||
- [ ] AiService.GenerateLessonPlan 可调用(P5 补全)
|
||||
- [ ] AiService.OptimizeExpression 可调用
|
||||
- [ ] edu.ai.usage.events topic 可发布(供 data-ana 统计 AI 用量)
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
在 ai 真实服务就绪前,为下游(teacher-bff)提供以下 mock:
|
||||
|
||||
- **gRPC mock**:使用 grpc-mock 拦截 50057 端口
|
||||
- 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)
|
||||
- **Kafka mock**:ai 就绪前不发布真实 AIUsageEvent,data-ana 仪表盘 AI 用量显示"暂无数据"
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实上游就绪前,ai 使用以下 mock:
|
||||
|
||||
- **LLM Provider mock**:本地启动 mock server,POST /v1/chat/completions 返回固定 JSON(不消耗真实 token,不产生费用)
|
||||
- **content 知识点**:内置固定知识点表(数学 20 个知识点 + 前置依赖关系),不依赖 content gRPC
|
||||
- **data-ana 薄弱点**:内置固定学生薄弱点(2 个 weak_points),不依赖 data-ana gRPC
|
||||
- **事件订阅**:不订阅 content 事件,知识点维度表静态
|
||||
102
docs/architecture/issues/contracts/api-gateway_contract.md
Normal file
102
docs/architecture/issues/contracts/api-gateway_contract.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# api-gateway 对接契约
|
||||
|
||||
> 负责人:ai01
|
||||
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无。api-gateway 是 HTTP 入口,不对外提供 gRPC。
|
||||
|
||||
### 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.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。api-gateway 仅做 HTTP 反向代理 + JWT 验签,不解析 GraphQL。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。api-gateway 不发布事件。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`GW_`(如 GW_UNAUTHORIZED、GW_RATE_LIMITED、GW_CIRCUIT_OPEN、GW_BACKEND_UNAVAILABLE)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
| 被调用方 | Service.RPC | 用途 | mock 策略 |
|
||||
| ---------- | ----------------------------- | ---------------------------------------- | ----------------------------------------------------------------- |
|
||||
| iam (ai06) | IamService.GetPublicKey | 启动时拉取 RS256 公钥,用于 JWT 验签 | iam 就绪前使用本地固定 mock 公钥(与 mock 私钥配对签发 mock JWT) |
|
||||
| iam (ai06) | IamService.GetEffectiveAccess | 权限校验(可选,部分路由需要细粒度权限) | iam 就绪前放行所有请求(仅校验 JWT 签名) |
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。api-gateway 不订阅 Kafka 事件。
|
||||
|
||||
### 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 |
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] iam gRPC 50052 启用(ai06)—— GetPublicKey 拉取验签公钥
|
||||
- [ ] teacher-bff GraphQL :3003 启用(ai03)
|
||||
- [ ] student-bff GraphQL :3009 启用(ai04)
|
||||
- [ ] parent-bff GraphQL :3010 启用(ai05)
|
||||
|
||||
### 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 级令牌桶)+ 熔断(各后端独立熔断器)生效
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
在 api-gateway 真实就绪前,为下游(各前端 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
|
||||
- **JWT mock**:前端开发期使用固定 mock JWT(api-gateway 就绪前不走真实验签)
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实上游就绪前,api-gateway 使用以下 mock:
|
||||
|
||||
- **iam 公钥**:使用本地固定 mock RS256 公钥(与 mock 私钥配对),验签 mock JWT
|
||||
- **iam 权限校验**:GetEffectiveAccess 返回 allowed=true,放行所有请求
|
||||
- **各 BFF 代理**:BFF 就绪前返回 503 + Retry-After,前端降级到本地 mock 数据
|
||||
123
docs/architecture/issues/contracts/classes_contract.md
Normal file
123
docs/architecture/issues/contracts/classes_contract.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# classes 对接契约
|
||||
|
||||
> 负责人:ai07
|
||||
> 关联:[matrix.md](../matrix.md)、[classes.proto](../../../packages/shared-proto/proto/classes.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[02-architecture-design.md](../../../services/classes/docs/02-architecture-design.md)
|
||||
> 状态:黄金模板(P1 已实现),P3 合并入 core-edu
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
| Service | RPC | 请求 | 响应 | 端口 |
|
||||
| ------------ | ----------- | ------------------ | ------------------- | ------------------------------- |
|
||||
| ClassService | CreateClass | CreateClassRequest | Class | 50053(P3 启用,core-edu 承载) |
|
||||
| ClassService | GetClass | GetClassRequest | Class | 50053 |
|
||||
| ClassService | ListClasses | ListClassesRequest | ListClassesResponse | 50053 |
|
||||
| ClassService | UpdateClass | UpdateClassRequest | Class | 50053 |
|
||||
| ClassService | DeleteClass | DeleteClassRequest | Empty | 50053 |
|
||||
|
||||
> **注意**:classes 当前仅 REST(端口 3001),gRPC server 50053 在 P3 合并入 core-edu 后启用。proto 契约已就绪(classes.proto),由 core-edu 实现承载。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
| Method | Path | 权限 | 说明 |
|
||||
| ------ | -------------- | ---------------- | ----------------------------- |
|
||||
| POST | `/classes` | `CLASSES_CREATE` | 创建班级 |
|
||||
| GET | `/classes` | `CLASSES_READ` | 列表(可选 `?gradeId=` 过滤) |
|
||||
| GET | `/classes/:id` | `CLASSES_READ` | 单条查询 |
|
||||
| PUT | `/classes/:id` | `CLASSES_UPDATE` | 更新 |
|
||||
| DELETE | `/classes/:id` | `CLASSES_DELETE` | 删除(先校验存在) |
|
||||
| GET | `/healthz` | 无 | liveness |
|
||||
| GET | `/readyz` | 无 | readiness(校验 DB) |
|
||||
| GET | `/metrics` | 无 | Prometheus 指标 |
|
||||
|
||||
> **响应信封**:ActionState(`{success:true, data:T}` / `{success:false, error:{code,message,details?,traceId?}}`)
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。classes 是业务服务,非 BFF。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
| Topic | Event | 消费方 | 阶段 |
|
||||
| --------------------------- | --------------------------------- | ------------------------------- | ------------------- |
|
||||
| `edu.org.class.created` | ClassEvent(action: created) | data-ana(建宽表行) | P3(core-edu 承载) |
|
||||
| `edu.org.class.updated` | ClassEvent(action: updated) | data-ana、msg(班主任变更通知) | P3 |
|
||||
| `edu.org.class.deleted` | ClassEvent(action: deleted) | data-ana、core-edu(关联检查) | P3 |
|
||||
| `edu.org.class.transferred` | ClassEvent(action: transferred) | msg(通知新/旧班主任) | P3 |
|
||||
|
||||
> **事件 message**:`events.proto` 的 `ClassEvent`(event_id / aggregate_id / event_type / occurred_at / class_id / name / action / metadata)
|
||||
> **发布方式**:Outbox 模式(P3 补齐 `shared/outbox/`),保证事务与事件最终一致
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`CLASSES_`(004 §11.4 确认保留,P3 合并入 core-edu 后保留历史遗留前缀)
|
||||
|
||||
| 错误码 | HTTP | 触发条件 |
|
||||
| --------------------------- | ---- | ----------------------------- |
|
||||
| `CLASSES_VALIDATION_ERROR` | 400 | Zod 校验失败 / 空 update body |
|
||||
| `CLASSES_NOT_FOUND` | 404 | 资源不存在 |
|
||||
| `CLASSES_PERMISSION_DENIED` | 403 | PermissionGuard 校验失败 |
|
||||
| `CLASSES_CONFLICT` | 409 | 并发冲突(预留) |
|
||||
| `CLASSES_BUSINESS_ERROR` | 422 | 业务规则违反(预留) |
|
||||
| `CLASSES_DATABASE_ERROR` | 500 | DB 操作失败 |
|
||||
| `CLASSES_INTERNAL_ERROR` | 500 | 未预期异常 |
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
当前无。P3 合并入 core-edu 后,可能调用 iam 的 `BatchGetUsers`(班主任信息批量查询)。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
| Topic | Event | 消费动作 | 阶段 |
|
||||
| --------------------------- | ---------------------------- | -------------------------------------------- | ---- |
|
||||
| `edu.identity.user.deleted` | UserEvent(action: deleted) | 若 deleted user 是班主任,置空 headTeacherId | P3 |
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。classes 是基础数据源,不反向调用其他服务。
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] MySQL classes_db 可用(已就绪,P1)
|
||||
- [ ] api-gateway `/classes/*` 路由已注册(已就绪,P1)
|
||||
- [ ] P3:iam `BatchGetUsers` gRPC 可用(班主任信息查询)
|
||||
- [ ] P3:Kafka `edu.identity.user.deleted` topic 可消费
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [x] classes REST API 5 端点可用(P1 已实现)
|
||||
- [x] `/healthz` + `/readyz` 可用(P1 已实现)
|
||||
- [x] `/metrics` 可用(P1 已实现)
|
||||
- [ ] P3:gRPC 50053 启用(由 core-edu 承载,`ClassService` 5 RPC 可调用)
|
||||
- [ ] P3:`edu.org.class.created/updated/deleted/transferred` topic 可发布
|
||||
- [ ] P3:Outbox 模式落地(`shared/outbox/` 目录补齐)
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
classes 是 P1 黄金模板,REST API 已实现,**下游无需 mock,可直接调用真实服务**。
|
||||
|
||||
但为 P3 gRPC 迁移期间兼容,提供以下 mock 供下游在 gRPC 未启用时使用:
|
||||
|
||||
- **REST mock**(已可用):直接调用 `http://classes:3001/classes/*`,返回真实数据
|
||||
- **gRPC mock**(P3 过渡期):grpc-mock 拦截 50053,ClassService 5 RPC 返回固定 Class 数据
|
||||
- **Kafka mock**(P3 过渡期):classes 事件未发布前,下游订阅方使用本地 stub(固定 ClassEvent JSON)
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
- P3 期间 iam `BatchGetUsers` 未就绪时,使用 grpc-mock 返回固定用户信息(班主任姓名)
|
||||
- P3 期间 `edu.identity.user.deleted` topic 未就绪时,使用本地 Kafka mock consumer stub
|
||||
110
docs/architecture/issues/contracts/content_contract.md
Normal file
110
docs/architecture/issues/contracts/content_contract.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# content 对接契约
|
||||
|
||||
> 负责人:ai09
|
||||
> 关联:[matrix.md](./matrix.md)、[content.proto](../../../packages/shared-proto/proto/content.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
| 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 |
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
无对外 HTTP 端点,仅 gRPC。
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
| 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 |
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`CONTENT_`(如 CONTENT_TEXTBOOK_NOT_FOUND、CONTENT_QUESTION_DUPLICATE)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无直接 gRPC 调用上游。content 通过 Kafka 事件接收 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 就绪前忽略,题目关联知识点不依赖考试事件 |
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] core-edu gRPC 50053 启用(ai08)—— 用于知识点与班级关联(可选,content 可先独立运行)
|
||||
- [ ] edu.class.events / edu.exam.events topic 有事件发布(ai08)
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] content gRPC 50054 启用(HealthService.Check 返回 SERVING)
|
||||
- [ ] TextbookService 3 RPC 可调用
|
||||
- [ ] ChapterService 4 RPC 可调用
|
||||
- [ ] KnowledgeGraphService 4 RPC 可调用(GetPrerequisites/GetLearningPath/AddPrerequisite/RemovePrerequisite)
|
||||
- [ ] QuestionService 7 RPC 可调用(含 SearchQuestions 全文检索)
|
||||
- [ ] edu.content.knowledge_point.events / edu.content.question.events topic 可发布
|
||||
|
||||
---
|
||||
|
||||
## §4 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
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实 core-edu 就绪前,content 使用以下 mock:
|
||||
|
||||
- 班级/学生数据:不依赖 core-edu 实时数据,知识点关联使用固定 subject_id/grade
|
||||
- 事件订阅:不订阅 edu.class.events / edu.exam.events,内部数据自洽
|
||||
118
docs/architecture/issues/contracts/core-edu_contract.md
Normal file
118
docs/architecture/issues/contracts/core-edu_contract.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
| 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 |
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
无对外 HTTP 端点,仅 gRPC。
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。
|
||||
|
||||
### 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 |
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`CORE_EDU_`(如 CORE_EDU_CLASS_NOT_FOUND、CORE_EDU_EXAM_CONFLICT)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无直接 gRPC 调用上游。core-edu 通过 Kafka 事件接收 iam 用户变更,不主动调 iam。
|
||||
|
||||
### 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 |
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] iam gRPC 50052 启用(ai06)—— 用于用户身份一致性校验(可选,core-edu 可先独立运行)
|
||||
- [ ] edu.iam.user.events topic 有事件发布(ai06)—— 用于同步用户缓存
|
||||
|
||||
### 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 可发布
|
||||
|
||||
---
|
||||
|
||||
## §4 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 事件
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实 iam 就绪前,core-edu 使用以下 mock:
|
||||
|
||||
- 用户数据:内置固定 teacher_id / student_id(不订阅 edu.iam.user.events)
|
||||
- 权限校验:core-edu 内部不校验权限(由 Gateway/BFF 层负责),仅记录 created_by 字段
|
||||
121
docs/architecture/issues/contracts/data-ana_contract.md
Normal file
121
docs/architecture/issues/contracts/data-ana_contract.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# data-ana 对接契约
|
||||
|
||||
> 负责人:ai11
|
||||
> 关联:[matrix.md](./matrix.md)、[analytics.proto](../../../packages/shared-proto/proto/analytics.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
| Service | RPC | 请求 | 响应 | 端口 |
|
||||
| ---------------- | ---------------------- | ----------------------------- | ------------------------- | ----- |
|
||||
| AnalyticsService | GetClassPerformance | GetClassPerformanceRequest | ClassPerformance | 50055 |
|
||||
| AnalyticsService | GetStudentWeakness | GetStudentWeaknessRequest | StudentWeakness | 50055 |
|
||||
| AnalyticsService | GetLearningTrend | GetLearningTrendRequest | LearningTrend | 50055 |
|
||||
| AnalyticsService | GetTeacherDashboard | GetTeacherDashboardRequest | TeacherDashboard | 50055 |
|
||||
| AnalyticsService | GetStudentDashboard | GetStudentDashboardRequest | StudentDashboard | 50055 |
|
||||
| AnalyticsService | GetParentDashboard | GetParentDashboardRequest | ParentDashboard | 50055 |
|
||||
| AnalyticsService | GetAdminDashboard | GetAdminDashboardRequest | AdminDashboard | 50055 |
|
||||
| AnalyticsService | GetWarningList | GetWarningListRequest | WarningListResponse | 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 端点(如有)
|
||||
|
||||
无对外 HTTP 端点,仅 gRPC(含 1 个 Server Streaming RPC)。
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
| Topic | Event | 消费方 |
|
||||
| --------------------------- | --------------------------------------------------------- | -------------- |
|
||||
| edu.data_ana.mastery.events | MasteryEvent(action: mastery.updated/warning.triggered) | core-edu / msg |
|
||||
|
||||
> 注:MasteryEvent 豁免 Outbox 模式(派生数据事件,见 004 §12.2 + §15.3 #6)。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`DATA_ANA_`(如 DATA_ANA_DASHBOARD_UNAVAILABLE、DATA_ANA_MASTERY_NOT_COMPUTED)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无主动 gRPC 调用上游。data-ana 通过 CDC + Kafka 事件接收数据,计算后发布 MasteryEvent。
|
||||
|
||||
### 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 用量统计为空 |
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
### 2.4 CDC 数据源(补充)
|
||||
|
||||
| 数据源 | 用途 | mock 策略 |
|
||||
| ----------------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------- |
|
||||
| core-edu MySQL(exams/homework/grades/attendance 表) | Debezium CDC → Kafka 同步读模型 | core-edu 就绪前使用 ClickHouse 内置模拟数据集(30 学生 × 5 考试 × 10 作业) |
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 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 用量统计(可选,仪表盘补全)
|
||||
|
||||
### 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)
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
在 data-ana 真实服务就绪前,为下游(teacher-bff / student-bff / parent-bff / admin-portal / msg)提供以下 mock:
|
||||
|
||||
- **gRPC mock**:使用 grpc-mock 拦截 50055 端口
|
||||
- GetTeacherDashboard 返回固定仪表盘(total_classes=3, class_avg_score=82.5, top_students 5 个, pending_homework_count=8)
|
||||
- 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)
|
||||
- GetMasteryDistribution 返回固定分布(mastered=20, progressing=7, weak=3)
|
||||
- SubscribeMasteryUpdate 返回固定流(每 5 秒推 1 个 MasteryUpdateEvent)
|
||||
- **Kafka mock**:data-ana 就绪前不发布真实 MasteryEvent,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 批量导入模拟数据
|
||||
95
docs/architecture/issues/contracts/iam_contract.md
Normal file
95
docs/architecture/issues/contracts/iam_contract.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# iam 对接契约
|
||||
|
||||
> 负责人:ai06
|
||||
> 关联:[matrix.md](./matrix.md)、[iam.proto](../../../packages/shared-proto/proto/iam.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 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.2 HTTP 端点(如有)
|
||||
|
||||
无对外 HTTP 端点,仅 gRPC。
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
| Topic | Event | 消费方 |
|
||||
| --------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| edu.iam.user.events | UserEvent(action: created/updated/deleted/role_changed) | core-edu / msg / push-gateway / teacher-bff / student-bff |
|
||||
| edu.iam.role.events | RoleEvent(action: created/updated) | core-edu / teacher-bff |
|
||||
| edu.iam.audit.created | AuditEvent(action: create/update/delete/login/logout/permission_change) | admin-portal |
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`IAM_`(如 IAM_UNAUTHORIZED、IAM_USER_NOT_FOUND、IAM_PERMISSION_DENIED)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无。iam 是身份根服务,不依赖其他业务服务。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
无上游依赖。
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] iam gRPC 50052 启用(HealthService.Check 返回 SERVING)
|
||||
- [ ] IamService.Register/Login/RefreshToken/Logout 可调用(返回 AuthResponse/TokenPair)
|
||||
- [ ] 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)
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
在 iam 真实服务就绪前,为下游(api-gateway / 各 BFF)提供以下 mock:
|
||||
|
||||
- **gRPC mock**:使用 grpc-mock 拦截 50052 端口,Register/Login 返回固定 AuthResponse(user.id="mock-user-001", tokens.access_token="mock-access-token")
|
||||
- **GetPublicKey mock**:返回固定 RS256 公钥 PEM(与 mock 私钥配对),供 api-gateway 验签 mock JWT
|
||||
- **GetChildrenByParent mock**:返回固定 ChildInfo 列表(2 个孩子)
|
||||
- **Kafka mock**:iam 服务就绪前不发布真实事件,下游订阅方使用本地 stub
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
不适用(无上游依赖)。
|
||||
110
docs/architecture/issues/contracts/msg_contract.md
Normal file
110
docs/architecture/issues/contracts/msg_contract.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# msg 对接契约
|
||||
|
||||
> 负责人:ai10
|
||||
> 关联:[matrix.md](./matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 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 |
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
无对外 HTTP 端点,仅 gRPC。
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
| Topic | Event | 消费方 |
|
||||
| --------------------------- | ------------------------------------------------------ | ----------------------- |
|
||||
| edu.msg.notification.events | NotificationEvent(action: sent/read/recalled/failed) | push-gateway / data-ana |
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`MSG_`(如 MSG_TEMPLATE_NOT_FOUND、MSG_CHANNEL_DISABLED、MSG_RATE_LIMITED)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无主动 gRPC 调用上游。msg 通过 Kafka 事件被动接收业务事件后触发通知。
|
||||
|
||||
### 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 |
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 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)
|
||||
|
||||
### 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 推送)
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 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 推送
|
||||
|
||||
### 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)
|
||||
121
docs/architecture/issues/contracts/parent-bff_contract.md
Normal file
121
docs/architecture/issues/contracts/parent-bff_contract.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无对外 gRPC。parent-bff 是 GraphQL 聚合层。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
| Method | Path | 用途 | 认证 |
|
||||
| ------ | -------- | ----------------------------------------- | ---------------------- |
|
||||
| POST | /graphql | 家长 BFF GraphQL 端点 | JWT 必需 + parent 角色 |
|
||||
| GET | /graphql | GraphQL Playground(开发环境) | 开发环境公开 |
|
||||
| GET | /healthz | 健康检查(liveness) | 公开 |
|
||||
| GET | /readyz | 就绪检查(readiness,含下游 gRPC 连通性) | 公开 |
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
GraphQL schema 文件路径:`apps/parent-bff/src/schema/*.graphql`(端口 :3010)
|
||||
|
||||
核心 Query / Mutation 域:
|
||||
|
||||
- **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)
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。parent-bff 不发布事件,仅做 gRPC 聚合。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`BFF_PARENT_`(如 BFF_PARENT_UPSTREAM_UNAVAILABLE、BFF_PARENT_AGGREGATION_FAILED、BFF_PARENT_NO_CHILDREN、BFF_PARENT_FORBIDDEN)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 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 |
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。parent-bff 不订阅 Kafka 事件,仅做同步 gRPC 聚合。
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] iam gRPC 50052 启用(ai06)—— **核心依赖 GetChildrenByParent(I3/ISSUE-047 裁决)**
|
||||
- [ ] core-edu gRPC 50053 启用(ai08)
|
||||
- [ ] data-ana gRPC 50055 启用(ai11)
|
||||
- [ ] msg gRPC 50056 启用(ai10)
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] parent-bff GraphQL :3010 启用(/healthz 返回 200)
|
||||
- [ ] /readyz 返回 200(含 4 个下游 gRPC 连通性检查)
|
||||
- [ ] GraphQL schema 可内省(POST /graphql 返回 schema)
|
||||
- [ ] 核心 Query 可执行:currentUser / myChildren / childSummary / childGrades
|
||||
- [ ] 核心 Mutation 可执行:markAsRead
|
||||
- [ ] 数据范围校验生效(家长只能查自己孩子的数据,基于 iam.GetChildrenByParent 返回的 user_id 校验)
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 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 条通知
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实上游就绪前,parent-bff 使用以下 mock(详见 §2.1 mock 策略列):
|
||||
|
||||
- **iam mock**:固定 UserInfo + 家长权限 + 固定视口 + 固定 2 个 ChildInfo(家长-学生关联核心数据)
|
||||
- **core-edu mock**:固定孩子成绩/考勤/作业
|
||||
- **data-ana mock**:固定家长仪表盘/孩子薄弱点/趋势
|
||||
- **msg mock**:固定通知列表 + MarkAsRead success
|
||||
|
||||
> 关键:iam.GetChildrenByParent 的 mock 必须返回与 core-edu mock 数据一致的 student_id,否则数据范围校验会失败。parent-bff 启动时校验 myChildren 返回的 user_id 与下游查询的 student_id 一致性。
|
||||
124
docs/architecture/issues/contracts/parent-portal_contract.md
Normal file
124
docs/architecture/issues/contracts/parent-portal_contract.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# parent-portal 对接契约
|
||||
|
||||
> 负责人:ai15
|
||||
> 关联:[matrix.md](./matrix.md)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无。parent-portal 是前端微前端 Remote。
|
||||
|
||||
### 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 必需 |
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。parent-portal 消费 parent-bff GraphQL,自身不提供 schema。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
无(前端不定义错误码前缀,透传 BFF 错误码)。
|
||||
|
||||
### 1.6 微前端架构(补充)
|
||||
|
||||
| 角色 | 说明 |
|
||||
| ---------------------- | ------------------------------------------------ |
|
||||
| MF Remote | 家长门户是微前端远程模块 |
|
||||
| 暴露的 remote 模块 | ParentApp(家长端完整应用)、shared 家长端组件 |
|
||||
| module federation 配置 | `apps/parent-portal/module-federation.config.ts` |
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无。前端不直接调 gRPC。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。前端不直接订阅 Kafka。
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
| 被调用方 | 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 推送 |
|
||||
|
||||
### 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 返回固定通知 |
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] api-gateway HTTP :8080 启用(ai01)—— 前端请求入口
|
||||
- [ ] parent-bff GraphQL :3010 启用(ai05)—— 数据来源
|
||||
- [ ] push-gateway WebSocket :8081/ws 启用(ai02)—— 实时通知
|
||||
|
||||
### 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 通知可接收
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
parent-portal 是前端,无下游消费方。但对开发体验提供:
|
||||
|
||||
- **Storybook**:各组件独立 story
|
||||
- **MSW handlers**:`apps/parent-portal/src/mocks/handlers.ts`,拦截所有 GraphQL/HTTP 请求
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实上游就绪前,parent-portal 使用以下 mock:
|
||||
|
||||
- **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 一致
|
||||
- 所有 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`
|
||||
101
docs/architecture/issues/contracts/push-gateway_contract.md
Normal file
101
docs/architecture/issues/contracts/push-gateway_contract.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# push-gateway 对接契约
|
||||
|
||||
> 负责人:ai02
|
||||
> 关联:[matrix.md](./matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无对外 gRPC。push-gateway 是 WebSocket/SSE 推送入口。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
| 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.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。push-gateway 不发布事件,仅消费事件触发推送。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`PUSH_`(如 PUSH_CONNECTION_FAILED、PUSH_CHANNEL_CLOSED、PUSH_AUTH_INVALID)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无主动 gRPC 调用上游。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
| Topic | Event | 发布方 | mock 策略 |
|
||||
| --------------------------- | --------------------------------- | ---------- | --------------------------------------------------------------------------- |
|
||||
| edu.msg.notification.events | NotificationEvent(action: sent) | msg (ai10) | msg 就绪前不订阅,使用本地定时器每 30 秒推送 1 条 mock 通知到所有连接客户端 |
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
### 2.4 内部接口(msg 调用 push-gateway)
|
||||
|
||||
| 被调用方 | Method.Path | 用途 | 说明 |
|
||||
| ------------ | ------------------------ | ------------------ | ---------------------------------------- |
|
||||
| push-gateway | POST /internal/broadcast | msg 服务批量推送 | msg 收到业务事件后渲染模板,调此接口广播 |
|
||||
| push-gateway | POST /internal/send | msg 服务单用户推送 | msg 渲染后定向推送给目标用户 |
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] msg gRPC 50056 启用(ai10)—— 通知事件来源
|
||||
- [ ] edu.msg.notification.events topic 有事件发布(ai10)
|
||||
- [ ] iam gRPC 50052 启用(ai06)—— WebSocket 连接时 JWT 验签(可选,push-gateway 可独立验签)
|
||||
|
||||
### 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 订阅成功
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
在 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
|
||||
|
||||
### 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,使用本地定时器替代
|
||||
130
docs/architecture/issues/contracts/student-bff_contract.md
Normal file
130
docs/architecture/issues/contracts/student-bff_contract.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无对外 gRPC。student-bff 是 GraphQL 聚合层。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
| Method | Path | 用途 | 认证 |
|
||||
| ------ | -------- | ----------------------------------------- | ----------------------- |
|
||||
| POST | /graphql | 学生 BFF GraphQL 端点 | JWT 必需 + student 角色 |
|
||||
| GET | /graphql | GraphQL Playground(开发环境) | 开发环境公开 |
|
||||
| GET | /healthz | 健康检查(liveness) | 公开 |
|
||||
| GET | /readyz | 就绪检查(readiness,含下游 gRPC 连通性) | 公开 |
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
GraphQL schema 文件路径:`apps/student-bff/src/schema/*.graphql`(端口 :3009)
|
||||
|
||||
核心 Query / Mutation 域:
|
||||
|
||||
- **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)
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。student-bff 不发布事件,仅做 gRPC 聚合。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`BFF_STUDENT_`(如 BFF_STUDENT_UPSTREAM_UNAVAILABLE、BFF_STUDENT_AGGREGATION_FAILED、BFF_STUDENT_FORBIDDEN)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 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 |
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。student-bff 不订阅 Kafka 事件,仅做同步 gRPC 聚合。
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] iam gRPC 50052 启用(ai06)
|
||||
- [ ] core-edu gRPC 50053 启用(ai08)
|
||||
- [ ] content gRPC 50054 启用(ai09)
|
||||
- [ ] data-ana gRPC 50055 启用(ai11)
|
||||
- [ ] msg gRPC 50056 启用(ai10)
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] student-bff GraphQL :3009 启用(/healthz 返回 200)
|
||||
- [ ] /readyz 返回 200(含 5 个下游 gRPC 连通性检查)
|
||||
- [ ] GraphQL schema 可内省(POST /graphql 返回 schema)
|
||||
- [ ] 核心 Query 可执行:currentUser / myClasses / studentDashboard / myGrades
|
||||
- [ ] 核心 Mutation 可执行:submitHomework / markAsRead
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
在 student-bff 真实就绪前,为下游(student-portal)提供以下 mock:
|
||||
|
||||
- **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 条通知
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实上游就绪前,student-bff 使用以下 mock(详见 §2.1 mock 策略列):
|
||||
|
||||
- **iam mock**:固定 UserInfo + 学生权限 + 固定视口
|
||||
- **core-edu mock**:固定班级/同学/考试/作业/成绩/考勤
|
||||
- **content mock**:固定教材/章节/学习路径
|
||||
- **data-ana mock**:固定仪表盘/薄弱点/趋势
|
||||
- **msg mock**:固定通知列表 + MarkAsRead success
|
||||
|
||||
> 所有上游 mock 通过 gRPC client 拦截器实现,上游就绪后移除拦截器切换真实调用。
|
||||
125
docs/architecture/issues/contracts/student-portal_contract.md
Normal file
125
docs/architecture/issues/contracts/student-portal_contract.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# student-portal 对接契约
|
||||
|
||||
> 负责人:ai14
|
||||
> 关联:[matrix.md](./matrix.md)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无。student-portal 是前端微前端 Remote。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
| Method | Path | 用途 | 认证 |
|
||||
| ------ | -------------- | ------------ | ------------------------ |
|
||||
| GET | / | 学生门户首页 | JWT 必需(前端路由守卫) |
|
||||
| GET | /my-classes | 我的班级 | JWT 必需 |
|
||||
| GET | /my-exams | 我的考试 | JWT 必需 |
|
||||
| GET | /my-homework | 我的作业 | JWT 必需 |
|
||||
| GET | /my-grades | 我的成绩 | JWT 必需 |
|
||||
| GET | /my-attendance | 我的考勤 | JWT 必需 |
|
||||
| GET | /learning-path | 学习路径 | JWT 必需 |
|
||||
| GET | /dashboard | 学生仪表盘 | JWT 必需 |
|
||||
| GET | /notifications | 通知中心 | JWT 必需 |
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。student-portal 消费 student-bff GraphQL,自身不提供 schema。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
无(前端不定义错误码前缀,透传 BFF 错误码)。
|
||||
|
||||
### 1.6 微前端架构(补充)
|
||||
|
||||
| 角色 | 说明 |
|
||||
| ---------------------- | ----------------------------------------------------------------- |
|
||||
| MF Remote | 学生门户是微前端远程模块,由 teacher-portal AppShell 或独立壳加载 |
|
||||
| 暴露的 remote 模块 | StudentApp(学生端完整应用)、shared 学生端组件 |
|
||||
| module federation 配置 | `apps/student-portal/module-federation.config.ts` |
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无。前端不直接调 gRPC。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。前端不直接订阅 Kafka。
|
||||
|
||||
### 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 推送 |
|
||||
|
||||
### 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 返回固定通知 |
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] api-gateway HTTP :8080 启用(ai01)—— 前端请求入口
|
||||
- [ ] student-bff GraphQL :3009 启用(ai04)—— 数据来源
|
||||
- [ ] push-gateway WebSocket :8081/ws 启用(ai02)—— 实时通知
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] student-portal dev server :4001 启用
|
||||
- [ ] MF Remote 可被 AppShell 加载(暴露 StudentApp 模块)
|
||||
- [ ] 独立壳渲染(首页 + 导航 + 路由守卫)
|
||||
- [ ] 登录流程可用(POST /api/auth/login 获取 JWT 存入 cookie)
|
||||
- [ ] GraphQL 查询可执行(currentUser / myClasses / studentDashboard 返回数据)
|
||||
- [ ] WebSocket 通知可接收
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
student-portal 是前端,无下游消费方。但对开发体验提供:
|
||||
|
||||
- **Storybook**:各组件独立 story
|
||||
- **MSW handlers**:`apps/student-portal/src/mocks/handlers.ts`,拦截所有 GraphQL/HTTP 请求
|
||||
|
||||
### 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`
|
||||
136
docs/architecture/issues/contracts/teacher-bff_contract.md
Normal file
136
docs/architecture/issues/contracts/teacher-bff_contract.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无对外 gRPC。teacher-bff 是 GraphQL 聚合层。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
| Method | Path | 用途 | 认证 |
|
||||
| ------ | -------- | ----------------------------------------- | ----------------------- |
|
||||
| POST | /graphql | 教师 BFF GraphQL 端点 | JWT 必需 + teacher 角色 |
|
||||
| GET | /graphql | GraphQL Playground(开发环境) | 开发环境公开 |
|
||||
| GET | /healthz | 健康检查(liveness) | 公开 |
|
||||
| GET | /readyz | 就绪检查(readiness,含下游 gRPC 连通性) | 公开 |
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
GraphQL schema 文件路径:`apps/teacher-bff/src/schema/*.graphql`(端口 :3003)
|
||||
|
||||
核心 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)
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。teacher-bff 不发布事件,仅做 gRPC 聚合。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
`BFF_TEACHER_`(如 BFF_TEACHER_UPSTREAM_UNAVAILABLE、BFF_TEACHER_AGGREGATION_FAILED、BFF_TEACHER_FORBIDDEN)
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 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 就绪前返回固定教案 |
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。teacher-bff 不订阅 Kafka 事件,仅做同步 gRPC 聚合。
|
||||
|
||||
### 2.3 HTTP 调用(如有)
|
||||
|
||||
无。
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 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)
|
||||
|
||||
### 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 消费)
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
在 teacher-bff 真实就绪前,为下游(teacher-portal / admin-portal)提供以下 mock:
|
||||
|
||||
- **GraphQL mock**:使用 Apollo Server mockProviders 或 MSW 拦截 POST /graphql
|
||||
- currentUser 返回固定教师(id="teacher-001", name="张老师", roles=["teacher"])
|
||||
- myClasses 返回固定 3 个班级
|
||||
- teacherDashboard 返回固定仪表盘数据
|
||||
- myNotifications 返回固定 10 条通知
|
||||
- **admin namespace mock**:admin-portal 查询返回固定管理员视角数据(全校统计)
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实上游就绪前,teacher-bff 使用以下 mock(详见 §2.1 mock 策略列):
|
||||
|
||||
- **iam mock**:固定 UserInfo + 全权限 + 固定视口
|
||||
- **core-edu mock**:固定班级/学生/考试/作业/成绩/考勤数据
|
||||
- **content mock**:固定教材/章节/知识点/题目
|
||||
- **data-ana mock**:固定仪表盘/分析/预警
|
||||
- **msg mock**:固定通知列表 + MarkAsRead success
|
||||
- **ai mock**:固定 AI 回复/题目/教案
|
||||
|
||||
> 所有上游 mock 通过 gRPC client 拦截器实现,上游就绪后移除拦截器切换真实调用。
|
||||
126
docs/architecture/issues/contracts/teacher-portal_contract.md
Normal file
126
docs/architecture/issues/contracts/teacher-portal_contract.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# teacher-portal 对接契约
|
||||
|
||||
> 负责人:ai13
|
||||
> 关联:[matrix.md](./matrix.md)
|
||||
|
||||
---
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
### 1.1 gRPC 接口(如有)
|
||||
|
||||
无。teacher-portal 是前端微前端 Shell。
|
||||
|
||||
### 1.2 HTTP 端点(如有)
|
||||
|
||||
| Method | Path | 用途 | 认证 |
|
||||
| ------ | -------------- | ----------------------------- | ------------------------ |
|
||||
| GET | / | 教师门户首页(MF Shell 容器) | JWT 必需(前端路由守卫) |
|
||||
| GET | /classes/* | 班级管理子应用 | JWT 必需 |
|
||||
| GET | /exams/* | 考试管理子应用 | JWT 必需 |
|
||||
| GET | /homework/* | 作业管理子应用 | JWT 必需 |
|
||||
| GET | /grades/* | 成绩管理子应用 | JWT 必需 |
|
||||
| GET | /attendance/* | 考勤管理子应用 | JWT 必需 |
|
||||
| GET | /content/* | 内容管理子应用 | JWT 必需 |
|
||||
| GET | /dashboard | 教师仪表盘 | JWT 必需 |
|
||||
| GET | /ai/* | AI 助手子应用 | JWT 必需 |
|
||||
| GET | /notifications | 通知中心 | JWT 必需 |
|
||||
|
||||
### 1.3 GraphQL schema(如 BFF)
|
||||
|
||||
不适用。teacher-portal 消费 teacher-bff GraphQL,自身不提供 schema。
|
||||
|
||||
### 1.4 Kafka 事件发布(如有)
|
||||
|
||||
无。
|
||||
|
||||
### 1.5 错误码前缀
|
||||
|
||||
无(前端不定义错误码前缀,透传 BFF 错误码)。
|
||||
|
||||
### 1.6 微前端架构(补充)
|
||||
|
||||
| 角色 | 说明 |
|
||||
| ---------------------- | --------------------------------------------------- |
|
||||
| MF Shell(AppShell) | 教师门户是微前端宿主,加载其他子应用 |
|
||||
| 暴露的 remote 模块 | AppShell(导航/布局/路由守卫)、shared 设计系统组件 |
|
||||
| module federation 配置 | `apps/teacher-portal/module-federation.config.ts` |
|
||||
|
||||
---
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
### 2.1 gRPC 调用(同步)
|
||||
|
||||
无。前端不直接调 gRPC。
|
||||
|
||||
### 2.2 Kafka 事件订阅(异步)
|
||||
|
||||
无。前端不直接订阅 Kafka。
|
||||
|
||||
### 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 推送 |
|
||||
|
||||
### 2.4 GraphQL 查询域(经 api-gateway 代理到 teacher-bff)
|
||||
|
||||
| Query/Mutation | 用途 | mock 策略 |
|
||||
| -------------------------------------------------- | ------------ | ---------------------- |
|
||||
| currentUser | 当前教师信息 | MSW 返回固定教师 |
|
||||
| myClasses | 我的班级 | MSW 返回固定 3 个班级 |
|
||||
| classStudents | 班级学生名单 | MSW 返回固定 30 个学生 |
|
||||
| classExams / createExam | 考试管理 | MSW 返回固定考试数据 |
|
||||
| classHomework / assignHomework | 作业管理 | MSW 返回固定作业数据 |
|
||||
| studentGrades / recordGrade | 成绩管理 | MSW 返回固定成绩数据 |
|
||||
| classAttendance / recordAttendance | 考勤管理 | MSW 返回固定考勤数据 |
|
||||
| textbooks / chapters / knowledgePoints / questions | 内容管理 | MSW 返回固定内容数据 |
|
||||
| teacherDashboard | 教师仪表盘 | MSW 返回固定仪表盘 |
|
||||
| myNotifications / markAsRead | 通知中心 | MSW 返回固定通知 |
|
||||
| aiChat / generateQuestion / generateLessonPlan | AI 助手 | MSW 返回固定 AI 响应 |
|
||||
|
||||
---
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
### 3.1 我依赖的上游就绪标志
|
||||
|
||||
- [ ] api-gateway HTTP :8080 启用(ai01)—— 前端请求入口
|
||||
- [ ] teacher-bff GraphQL :3003 启用(ai03)—— 数据来源
|
||||
- [ ] push-gateway WebSocket :8081/ws 启用(ai02)—— 实时通知
|
||||
|
||||
### 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
- [ ] teacher-portal dev server :4000 启用
|
||||
- [ ] MF Shell 可加载(首页渲染 AppShell + 导航)
|
||||
- [ ] 子应用路由可访问(/classes /exams /homework 等子页面渲染)
|
||||
- [ ] 登录流程可用(POST /api/auth/login 获取 JWT 存入 cookie)
|
||||
- [ ] GraphQL 查询可执行(currentUser / myClasses 返回数据)
|
||||
- [ ] WebSocket 通知可接收(push-gateway 推送 → 前端通知中心更新)
|
||||
|
||||
---
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
### 4.1 我提供的 mock
|
||||
|
||||
teacher-portal 是最前端,无下游消费方。但对开发体验提供:
|
||||
|
||||
- **Storybook**:各组件独立 story,供设计审查
|
||||
- **MSW handlers**:`apps/teacher-portal/src/mocks/handlers.ts`,拦截所有 GraphQL/HTTP 请求返回 mock 数据
|
||||
|
||||
### 4.2 我消费的 mock
|
||||
|
||||
在真实上游就绪前,teacher-portal 使用以下 mock:
|
||||
|
||||
- **HTTP/GraphQL mock**:使用 MSW(Mock Service Worker)拦截所有请求
|
||||
- POST /api/auth/login → 返回固定 JWT + UserInfo
|
||||
- POST /api/teacher/graphql → 根据 operationName 返回对应 mock 响应(与 teacher-bff mock 数据一致)
|
||||
- 所有 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
|
||||
- **环境切换**:通过 `NEXT_PUBLIC_API_MOCKING=enabled` 环境变量控制是否启用 MSW,上游就绪后设为 `disabled`
|
||||
292
docs/architecture/issues/coord.md
Normal file
292
docs/architecture/issues/coord.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# coord 仲裁结果汇总
|
||||
|
||||
> 维护者:coord(协调 AI)
|
||||
> 维护规则:各 AI 在 `objections/[模块名]_issue.md` 提请异议 → coord 审阅 → 在本文件写入仲裁结果
|
||||
> 关联文档:[president-final-rulings.md](../president-final-rulings.md)、[ai-work-orchestration.md](../ai-work-orchestration.md)、[workline.md](./workline.md)、[matrix.md](./matrix.md)
|
||||
|
||||
---
|
||||
|
||||
## 仲裁记录索引
|
||||
|
||||
| 编号 | 日期 | 提请方 | 主题 | 状态 | 仲裁章节 |
|
||||
| ------- | ---------- | ---------------------- | --------------------------------- | --------- | -------- |
|
||||
| ARB-001 | 2026-07-09 | coord(批次 1 启动前) | teacher-bff GraphQL schema 第一版 | ✅ 已裁决 | §1 |
|
||||
| ARB-002 | 2026-07-09 | coord(批次 1 启动前) | MF Shell 暴露清单 | ✅ 已裁决 | §2 |
|
||||
|
||||
---
|
||||
|
||||
## §1 ARB-001:teacher-bff GraphQL schema 第一版
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
批次 1(P2)启动前,需仲裁 teacher-bff GraphQL schema 第一版,作为 ai03(teacher-bff)和 ai13(teacher-portal)的共同契约。
|
||||
|
||||
数据源:ai03 的 [02-architecture-design.md §5](../../services/teacher-bff/docs/02-architecture-design.md#L503) 提供了目标态 schema(P4),但 P2 只需 Must Have 子集。
|
||||
|
||||
### 1.2 仲裁结论
|
||||
|
||||
**P2 第一版 schema 范围**(Must Have,对应 ai03 P2 工作量分级 Must Have 13 项):
|
||||
|
||||
```graphql
|
||||
# ============ P2 第一版 schema ============
|
||||
# 存放位置:packages/shared-ts/contracts/graphql/teacher-bff.graphql
|
||||
# 仲裁原则:仅包含 P2 可实现的 Query(iam + classes 已就绪),Mutation 延后到 P3
|
||||
|
||||
scalar DateTime
|
||||
|
||||
enum DataScope {
|
||||
SELF
|
||||
CLASS
|
||||
GRADE
|
||||
SCHOOL
|
||||
DISTRICT
|
||||
ALL
|
||||
}
|
||||
|
||||
enum ExamStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
IN_PROGRESS
|
||||
GRADING
|
||||
SCORED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum SubmissionStatus {
|
||||
NOT_SUBMITTED
|
||||
SUBMITTED
|
||||
GRADED
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
name: String!
|
||||
roles: [String!]!
|
||||
dataScope: DataScope!
|
||||
}
|
||||
|
||||
type ViewportItem {
|
||||
key: String!
|
||||
label: String!
|
||||
route: String!
|
||||
icon: String
|
||||
sortOrder: String!
|
||||
requiredPermission: String
|
||||
}
|
||||
|
||||
type Class {
|
||||
id: ID!
|
||||
name: String!
|
||||
gradeId: String!
|
||||
studentCount: Int!
|
||||
}
|
||||
|
||||
type DashboardStats {
|
||||
totalExams: Int!
|
||||
pendingGrading: Int!
|
||||
todayHomework: Int!
|
||||
}
|
||||
|
||||
type DashboardData {
|
||||
user: User!
|
||||
classes: [Class!]!
|
||||
viewports: [ViewportItem!]!
|
||||
stats: DashboardStats!
|
||||
}
|
||||
|
||||
# ============ Query(P2 仅读,无 Mutation) ============
|
||||
|
||||
type Query {
|
||||
# 仪表盘聚合(并行:iam.me + iam.viewports + iam.permissions + classes.byTeacher)
|
||||
dashboard: DashboardData!
|
||||
|
||||
# 视口配置(导航)
|
||||
viewports: [ViewportItem!]!
|
||||
|
||||
# 当前用户信息
|
||||
me: User!
|
||||
|
||||
# 班级列表(按教师 dataScope 过滤)
|
||||
classes: [Class!]!
|
||||
class(id: ID!): Class
|
||||
}
|
||||
|
||||
# ============ P2 不包含(P3+ 扩展) ============
|
||||
# - exams / homework / grades Query → P3(core-edu 就绪后)
|
||||
# - classPerformance / studentWeakness / learningTrend → P4(data-ana 就绪后)
|
||||
# - notifications → P5(msg 就绪后)
|
||||
# - 所有 Mutation → P3+
|
||||
# - Subscription → P6+ 评估
|
||||
```
|
||||
|
||||
### 1.3 关键裁决
|
||||
|
||||
| 裁决点 | 结论 | 依据 |
|
||||
| ---------------- | ---------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Schema 存放位置 | `packages/shared-ts/contracts/graphql/teacher-bff.graphql` | 总裁裁决 §2.17 SDL-first + 集中管理 |
|
||||
| P2 Query 范围 | dashboard / viewports / me / classes / class(5 个) | 仅依赖 iam + classes(P2 就绪) |
|
||||
| P2 Mutation 范围 | **无**(P2 纯读) | Mutation 依赖 core-edu(P3),P2 无下游可写 |
|
||||
| DataLoader | dashboard 内 classes 列表用 DataLoader | N+1 防御(ai03 §5.1 第 4 条) |
|
||||
| 复杂度限制 | depth ≤ 7,cost ≤ 1000 | ai03 §5.1 第 5 条 |
|
||||
| admin 命名空间 | **P2 不包含**,P6 admin-portal 阶段新增 `admin` 命名空间 | 总裁裁决 §5.1 admin 复用 teacher-bff + admin schema 命名空间 |
|
||||
| 错误响应 | ActionState 信封(success/errors/data) | coord-final-decisions F9 |
|
||||
| 降级模式 | success=true + error=null + data 内 degraded 字段 | 总裁裁决 §3.4 方案 B |
|
||||
|
||||
### 1.4 ai03 执行项
|
||||
|
||||
1. 在 `packages/shared-ts/contracts/graphql/teacher-bff.graphql` 创建 P2 第一版 schema 文件
|
||||
2. teacher-bff 实现 Yoga GraphQL endpoint(`POST /graphql`)
|
||||
3. 实现 5 个 Query Resolver(dashboard / viewports / me / classes / class)
|
||||
4. dashboard Resolver 并行调用 iam(3 RPC)+ classes(1 RPC),用 DataLoader 防御 N+1
|
||||
5. 实现 ActionState 信封 + 降级模式(方案 B)
|
||||
|
||||
### 1.5 ai13 执行项
|
||||
|
||||
1. teacher-portal Shell 暴露 `GraphQLProvider`(urql client 单例)
|
||||
2. 使用 `useGraphQLClient()` 从 packages/hooks 获取 client
|
||||
3. P2 页面(dashboard / classes)通过 GraphQL query 拉取数据
|
||||
4. **不调用 REST 端点**(除登录外,P2 起 all-in GraphQL)
|
||||
|
||||
---
|
||||
|
||||
## §2 ARB-002:MF Shell 暴露清单
|
||||
|
||||
### 2.1 背景
|
||||
|
||||
批次 1(P2)启动前,需仲裁 teacher-portal 作为 MF Shell 暴露哪些模块给 Remote(student/parent/admin),作为 ai13 的实现依据。
|
||||
|
||||
数据源:ai13 的 [03-long-term-architecture.md §1.4](../../apps/teacher-portal/docs/03-long-term-architecture.md) 提供了 GraphQL client 单例设计。
|
||||
|
||||
### 2.2 仲裁结论
|
||||
|
||||
**P2 第一版 MF Shell 暴露清单**:
|
||||
|
||||
```typescript
|
||||
// teacher-portal/next.config.js — NextFederationPlugin exposes
|
||||
exposes: {
|
||||
// 1. AppShell(布局 + 导航 + 认证守卫)
|
||||
'./AppShell': './src/components/AppShell.tsx',
|
||||
// 2. GraphQLProvider(urql client 单例,总裁 §2.17 方案 A)
|
||||
'./GraphQLProvider': './src/app/providers.tsx',
|
||||
// 3. 共享 hooks
|
||||
'./useAuth': './packages/hooks/src/use-auth.ts',
|
||||
'./usePermission': './packages/hooks/src/use-permission.ts',
|
||||
'./useGraphQLClient': './packages/hooks/src/use-graphql-client.ts',
|
||||
// 4. 共享 UI 组件
|
||||
'./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',
|
||||
}
|
||||
```
|
||||
|
||||
**MF shared(singleton)配置**:
|
||||
|
||||
```typescript
|
||||
shared: {
|
||||
react: { singleton: true, requiredVersion: '^18.3.0' },
|
||||
'react-dom': { singleton: true, requiredVersion: '^18.3.0' },
|
||||
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 },
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 关键裁决
|
||||
|
||||
| 裁决点 | 结论 | 依据 |
|
||||
| ------------------- | ---------------------------------------------- | --------------------------- |
|
||||
| Shell 身份 | teacher-portal 是 MF Shell(非 Remote) | 总裁裁决 §2.17 + ai13 §1.3 |
|
||||
| P2 Remote 数量 | **0**(P2 暂无 Remote,仅配置 exposes/shared) | ai13 §1.3 绞杀者模式 step 3 |
|
||||
| P3 首个 Remote | student-portal | ai13 §1.3 step 4 |
|
||||
| GraphQL client 归属 | Shell 暴露 GraphQLProvider,Remote 复用 | 总裁 §2.17 方案 A |
|
||||
| MF shared singleton | react / react-dom / urql / graphql / @edu/* | 避免多实例 + 缓存不一致 |
|
||||
| AppShell 暴露 | 是(Remote 复用布局 + 导航 + 认证守卫) | ai13 §1.3 |
|
||||
| feature flag | `NEXT_PUBLIC_MF_ENABLED=false`(P2 默认关) | ai13 §1.3 回退策略 |
|
||||
| 登录页 | P2 不走 MF(Shell 独占 `/login`) | 登录是认证前提,MF 依赖认证 |
|
||||
|
||||
### 2.4 ai13 执行项
|
||||
|
||||
1. teacher-portal `next.config.js` 添加 NextFederationPlugin(exposes + shared,**无 remotes**)
|
||||
2. 创建 `src/app/providers.tsx`(GraphQLProvider 包裹)
|
||||
3. AppShell 改造:从 REST `/api/v1/teacher/viewports` 切换为 GraphQL query `viewports`
|
||||
4. P2 页面(dashboard / classes)全部用 GraphQL
|
||||
5. feature flag `NEXT_PUBLIC_MF_ENABLED=false`(P2 默认单体路由)
|
||||
|
||||
### 2.5 ai03 执行项(联动)
|
||||
|
||||
1. teacher-bff 暴露 `POST /graphql` endpoint
|
||||
2. 提供GraphQL playground(仅开发环境)
|
||||
|
||||
---
|
||||
|
||||
## §3 仲裁流程说明
|
||||
|
||||
### 3.1 提请异议
|
||||
|
||||
各 AI 在自己模块的 `objections/[模块名]_issue.md` 中追加异议条目,格式:
|
||||
|
||||
```markdown
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
```
|
||||
|
||||
### 3.2 coord 仲裁
|
||||
|
||||
coord 审阅各 `objections/[模块名]_issue.md` 后,在本文件(coord.md)追加仲裁章节:
|
||||
|
||||
```markdown
|
||||
## §N ARB-XXX:[主题]
|
||||
|
||||
### N.1 背景
|
||||
|
||||
### N.2 仲裁结论
|
||||
|
||||
### N.3 关键裁决(表格)
|
||||
|
||||
### N.4 相关 AI 执行项
|
||||
```
|
||||
|
||||
### 3.3 状态同步
|
||||
|
||||
- 仲裁完成后,coord 在对应 `objections/[模块名]_issue.md` 中更新状态为"已裁决(见 coord.md §N)"
|
||||
- coord 在 [workline.md](./workline.md) §8 就绪信号跟踪表中更新进度
|
||||
|
||||
### 3.4 目录结构
|
||||
|
||||
```
|
||||
issues/
|
||||
├── coord.md # 本文件(仲裁汇总)
|
||||
├── workline.md # 总工作安排
|
||||
├── matrix.md # 上下游对接总矩阵
|
||||
├── objections/ # 各模块问题/异议
|
||||
│ ├── [模块名]_issue.md
|
||||
│ └── cross_issue.md # 跨模块问题
|
||||
├── worklines/ # 各模块工作排期
|
||||
│ └── [模块名]_workline.md
|
||||
└── contracts/ # 各模块对接契约
|
||||
└── [模块名]_contract.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §4 历史仲裁(已迁移)
|
||||
|
||||
以下仲裁已在此前完成,原始记录见 [coord-final-decisions.md](../coord-final-decisions.md) 和 [president-final-rulings.md](../president-final-rulings.md):
|
||||
|
||||
- I1-I8:iam P2 契约(coord-final-decisions.md §1)
|
||||
- F1-F9:BFF 设计裁决(coord-final-decisions.md §2)
|
||||
- G1-G3:Gateway 裁决(coord-final-decisions.md §3)
|
||||
- N1-N3:content 裁决(coord-final-decisions.md §4)
|
||||
- 总裁裁决 70+ 项(president-final-rulings.md §1-§5)
|
||||
|
||||
本文件仅记录**新流程启动后**(2026-07-09 起)的仲裁。
|
||||
229
docs/architecture/issues/matrix.md
Normal file
229
docs/architecture/issues/matrix.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# 上下游对接总矩阵
|
||||
|
||||
> 维护者:coord(协调 AI)
|
||||
> 关联:[coord.md](./coord.md)、[workline.md](./workline.md)、各模块 [contracts/](./contracts/)
|
||||
> 模式:全并行开发(各 AI 一口气完成 P2-P6,开发期间用 mock,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 服务依赖矩阵(谁调用谁)
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Frontend["微前端层"]
|
||||
TP[teacher-portal<br/>ai13 :4000]
|
||||
SP[student-portal<br/>ai14 :4001]
|
||||
PP[parent-portal<br/>ai15 :4002]
|
||||
AP[admin-portal<br/>ai16 :4003]
|
||||
end
|
||||
|
||||
subgraph Gateway["网关层"]
|
||||
AGW[api-gateway<br/>ai01 :8080]
|
||||
PGW[push-gateway<br/>ai02 :8081]
|
||||
end
|
||||
|
||||
subgraph BFF["BFF 层"]
|
||||
TBFF[teacher-bff<br/>ai03 :3003 GraphQL]
|
||||
SBFF[student-bff<br/>ai04 :3009 GraphQL]
|
||||
PBFF[parent-bff<br/>ai05 :3010 GraphQL]
|
||||
end
|
||||
|
||||
subgraph Services["业务服务层"]
|
||||
IAM[iam<br/>ai06 :50052]
|
||||
CORE[core-edu<br/>ai08 :50053]
|
||||
CONTENT[content<br/>ai09 :50054]
|
||||
MSG[msg<br/>ai10 :50056]
|
||||
DATA[data-ana<br/>ai11 :50055]
|
||||
AISVC[ai<br/>ai12 :50057]
|
||||
end
|
||||
|
||||
TP --> AGW
|
||||
SP --> AGW
|
||||
PP --> AGW
|
||||
AP --> AGW
|
||||
TP -.推送.-> PGW
|
||||
SP -.推送.-> PGW
|
||||
PP -.推送.-> PGW
|
||||
|
||||
AGW --> TBFF
|
||||
AGW --> SBFF
|
||||
AGW --> PBFF
|
||||
|
||||
TBFF --> IAM
|
||||
TBFF --> CORE
|
||||
TBFF --> CONTENT
|
||||
TBFF --> DATA
|
||||
TBFF --> MSG
|
||||
TBFF --> AISVC
|
||||
|
||||
SBFF --> IAM
|
||||
SBFF --> CORE
|
||||
SBFF --> CONTENT
|
||||
SBFF --> DATA
|
||||
|
||||
PBFF --> IAM
|
||||
PBFF --> CORE
|
||||
|
||||
PGW --> MSG
|
||||
|
||||
AISVC --> CONTENT
|
||||
AISVC --> DATA
|
||||
|
||||
CORE -.Kafka.-> CONTENT
|
||||
CORE -.Kafka.-> DATA
|
||||
CORE -.Kafka.-> MSG
|
||||
CONTENT -.Kafka.-> DATA
|
||||
IAM -.Kafka.-> CORE
|
||||
IAM -.Kafka.-> MSG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §2 gRPC 接口提供方矩阵
|
||||
|
||||
| 提供方 | gRPC 端口 | Service | RPC 数 | 消费方 | 就绪信号 | 状态 |
|
||||
| --------------- | --------- | --------------------------------------------------------------------------------- | ------ | ---------------------------------------------------- | ----------------------------- | ---- |
|
||||
| iam (ai06) | 50052 | IamService | 12 | teacher-bff / student-bff / parent-bff / api-gateway | HealthService.Check = SERVING | ⏳ |
|
||||
| core-edu (ai08) | 50053 | ClassService + ExamService + HomeworkService + GradeService + AttendanceService | 22 | teacher-bff / student-bff / parent-bff | HealthService.Check = SERVING | ⏳ |
|
||||
| content (ai09) | 50054 | TextbookService + ChapterService + KnowledgeGraphService + QuestionService | 18 | teacher-bff / student-bff / ai | HealthService.Check = SERVING | ⏳ |
|
||||
| data-ana (ai11) | 50055 | AnalyticsService | 12 | teacher-bff / student-bff / parent-bff / ai | HealthService.Check = SERVING | ⏳ |
|
||||
| msg (ai10) | 50056 | NotificationService + NotificationPreferenceService + NotificationTemplateService | 13 | teacher-bff / push-gateway | HealthService.Check = SERVING | ⏳ |
|
||||
| ai (ai12) | 50057 | AiService | 6 | teacher-bff | HealthService.Check = SERVING | ⏳ |
|
||||
|
||||
---
|
||||
|
||||
## §3 GraphQL 接口提供方矩阵
|
||||
|
||||
| 提供方 | HTTP 端口 | Endpoint | 消费方 | schema 文件 | 状态 |
|
||||
| ------------------ | --------- | ------------- | ----------------------------- | -------------------------------------------------------- | ---- |
|
||||
| teacher-bff (ai03) | 3003 | POST /graphql | teacher-portal / admin-portal | packages/shared-ts/contracts/graphql/teacher-bff.graphql | ⏳ |
|
||||
| student-bff (ai04) | 3009 | POST /graphql | student-portal | packages/shared-ts/contracts/graphql/student-bff.graphql | ⏳ |
|
||||
| parent-bff (ai05) | 3010 | POST /graphql | parent-portal | packages/shared-ts/contracts/graphql/parent-bff.graphql | ⏳ |
|
||||
|
||||
---
|
||||
|
||||
## §4 Kafka 事件发布方矩阵
|
||||
|
||||
| 发布方 | Topic | Event | 消费方 | Outbox | 状态 |
|
||||
| --------------- | --------------------------- | ----------------------------------------------------- | --------------------------------------------------------- | ------- | ---- |
|
||||
| iam (ai06) | edu.iam.user.events | UserEvent(created/updated/deleted/role_changed) | core-edu / msg / push-gateway / teacher-bff / student-bff | ✅ | ⏳ |
|
||||
| iam (ai06) | edu.iam.role.events | RoleEvent(created/updated) | core-edu / msg | ✅ | ⏳ |
|
||||
| iam (ai06) | edu.iam.audit.created | AuditEvent(create/update/delete/login/logout) | admin-portal | ✅ | ⏳ |
|
||||
| core-edu (ai08) | edu.exam.events | ExamEvent(created/updated/deleted) | msg / data-ana / push-gateway | ✅ | ⏳ |
|
||||
| core-edu (ai08) | edu.homework.events | HomeworkEvent(assigned/submitted/graded) | msg / data-ana / push-gateway | ✅ | ⏳ |
|
||||
| core-edu (ai08) | edu.grade.events | GradeEvent(recorded/updated) | msg / push-gateway / parent-bff | ✅ | ⏳ |
|
||||
| core-edu (ai08) | edu.class.events | ClassEvent(transferred) | msg / data-ana | ✅ | ⏳ |
|
||||
| content (ai09) | edu.content.kp.events | KnowledgePointEvent(created/updated/prerequisite_*) | data-ana / ai / Neo4j Sync / ES Sync | ✅ | ⏳ |
|
||||
| content (ai09) | edu.content.question.events | QuestionEvent(created/updated/published/deleted) | data-ana / ai / ES Sync | ✅ | ⏳ |
|
||||
| msg (ai10) | edu.notification.requested | NotificationEvent(sent) | push-gateway / data-ana | ✅ | ⏳ |
|
||||
| data-ana (ai11) | edu.analytics.mastery | MasteryEvent(mastery.updated / warning.triggered) | core-edu / msg | ❌ 豁免 | ⏳ |
|
||||
| ai (ai12) | edu.ai.usage | AIUsageEvent(chat/generate/lesson) | data-ana | ❌ 豁免 | ⏳ |
|
||||
|
||||
---
|
||||
|
||||
## §5 HTTP 接口矩阵(非 gRPC / 非 GraphQL)
|
||||
|
||||
| 提供方 | Method | Path | 用途 | 消费方 | 认证 | 状态 |
|
||||
| ------------------- | ------ | ----------------- | -------------------- | ----------------------------------------------- | -------------- | ---- |
|
||||
| api-gateway (ai01) | * | /api/v1/teacher/* | 反向代理 teacher-bff | teacher-portal | JWT | ⏳ |
|
||||
| api-gateway (ai01) | * | /api/v1/student/* | 反向代理 student-bff | student-portal | JWT | ⏳ |
|
||||
| api-gateway (ai01) | * | /api/v1/parent/* | 反向代理 parent-bff | parent-portal | JWT | ⏳ |
|
||||
| api-gateway (ai01) | * | /api/v1/iam/* | 反向代理 iam | (内部) | JWT | ⏳ |
|
||||
| push-gateway (ai02) | WS | /ws | WebSocket 推送通道 | teacher-portal / student-portal / parent-portal | JWT | ⏳ |
|
||||
| push-gateway (ai02) | SSE | /sse | SSE 推送通道 | teacher-portal | JWT | ⏳ |
|
||||
| push-gateway (ai02) | POST | /internal/push | 内部推送入口 | msg | X-Internal-Key | ⏳ |
|
||||
| iam (ai06) | GET | /healthz | 存活检查 | k8s / 监控 | 无 | ⏳ |
|
||||
| iam (ai06) | GET | /readyz | 就绪检查(5 依赖) | k8s / 监控 | 无 | ⏳ |
|
||||
| 所有服务 | GET | /metrics | Prometheus 指标 | Prometheus | 无 | ⏳ |
|
||||
|
||||
---
|
||||
|
||||
## §6 错误码前缀矩阵
|
||||
|
||||
| 模块 | 前缀 | 示例 |
|
||||
| ------------ | --------- | -------------------------------------------------- |
|
||||
| api-gateway | GW_ | GW_UNAUTHORIZED / GW_RATE_LIMITED |
|
||||
| push-gateway | PUSH_ | PUSH_DEVICE_NOT_FOUND / PUSH_CHANNEL_FAILED |
|
||||
| teacher-bff | BFF_XXX_ | BFF_TEACHER_UNAUTHORIZED / BFF_TEACHER_BAD_GATEWAY |
|
||||
| student-bff | BFF_XXX_ | BFF_STUDENT_UNAUTHORIZED |
|
||||
| parent-bff | BFF_XXX_ | BFF_PARENT_CHILD_NOT_BOUND |
|
||||
| iam | IAM_ | IAM_USER_NOT_FOUND / IAM_INVALID_CREDENTIALS |
|
||||
| core-edu | CORE_EDU_ | CORE_EDU_EXAM_NOT_FOUND |
|
||||
| content | CONTENT_ | CONTENT_QUESTION_NOT_FOUND |
|
||||
| msg | MSG_ | MSG_NOTIFICATION_NOT_FOUND |
|
||||
| data-ana | DATA_ANA_ | DATA_ANA_DASHBOARD_UNAVAILABLE |
|
||||
| ai | AI_ | AI_GENERATION_FAILED / AI_QUOTA_EXCEEDED |
|
||||
|
||||
---
|
||||
|
||||
## §7 全并行开发 Mock 策略汇总
|
||||
|
||||
| 消费方 | 消费的接口 | Mock 方式 | 切换真实时机 |
|
||||
| -------------- | ----------------------------------- | --------------------------------------- | ----------------------------------- |
|
||||
| api-gateway | iam.GetPublicKey | 硬编码 RS256 公钥(DEV_MODE) | iam gRPC 50052 就绪 |
|
||||
| teacher-bff | iam gRPC 12 RPC | grpc-mock 拦截 + 固定 JSON | iam 就绪信号 ✅ |
|
||||
| teacher-bff | core-edu gRPC 22 RPC | grpc-mock 拦截 + 固定 JSON | core-edu 就绪信号 ✅ |
|
||||
| student-bff | iam + core-edu + content + data-ana | grpc-mock 拦截 | 各服务就绪信号 ✅ |
|
||||
| parent-bff | iam + core-edu | grpc-mock 拦截 | 各服务就绪信号 ✅ |
|
||||
| teacher-portal | teacher-bff GraphQL | MSW 拦截 + 固定 response | teacher-bff GraphQL 就绪 ✅ |
|
||||
| student-portal | student-bff GraphQL | MSW 拦截 | student-bff GraphQL 就绪 ✅ |
|
||||
| parent-portal | parent-bff GraphQL | MSW 拦截 | parent-bff GraphQL 就绪 ✅ |
|
||||
| admin-portal | teacher-bff GraphQL (admin) | MSW 拦截 | teacher-bff admin namespace 就绪 ✅ |
|
||||
| push-gateway | msg Kafka 事件 | 本地 Kafka mock producer | msg 就绪信号 ✅ |
|
||||
| ai | content + data-ana gRPC | grpc-mock 拦截 | 各服务就绪信号 ✅ |
|
||||
| data-ana | core-edu/content/ai Kafka + CDC | 本地 Kafka mock + ClickHouse 模拟数据集 | 各服务就绪信号 ✅ |
|
||||
|
||||
---
|
||||
|
||||
## §8 就绪信号跟踪表
|
||||
|
||||
> 各 AI 完成模块后,在此表更新状态(⏳ 进行中 → ✅ 就绪)
|
||||
|
||||
| 模块 | AI | 就绪信号 | 状态 | 完成时间 |
|
||||
| -------------- | ---- | ------------------------------------------- | ---- | -------- |
|
||||
| iam | ai06 | gRPC 50052 + 12 RPC + HealthService SERVING | ⏳ | - |
|
||||
| api-gateway | ai01 | :8080 可访问 + JWT 验签 | ⏳ | - |
|
||||
| teacher-bff | ai03 | POST /graphql + 5 Query | ⏳ | - |
|
||||
| teacher-portal | ai13 | :4000 可访问 + 登录→Dashboard | ⏳ | - |
|
||||
| core-edu | ai08 | gRPC 50053 + 22 RPC + HealthService SERVING | ⏳ | - |
|
||||
| content | ai09 | gRPC 50054 + 18 RPC + HealthService SERVING | ⏳ | - |
|
||||
| msg | ai10 | gRPC 50056 + 13 RPC + HealthService SERVING | ⏳ | - |
|
||||
| data-ana | ai11 | gRPC 50055 + 12 RPC + HealthService SERVING | ⏳ | - |
|
||||
| ai | ai12 | gRPC 50057 + 6 RPC + HealthService SERVING | ⏳ | - |
|
||||
| student-bff | ai04 | POST /graphql + Dashboard Query | ⏳ | - |
|
||||
| parent-bff | ai05 | POST /graphql + Dashboard Query | ⏳ | - |
|
||||
| push-gateway | ai02 | :8081 可访问 + /internal/push | ⏳ | - |
|
||||
| student-portal | ai14 | :4001 可访问 + MF Remote | ⏳ | - |
|
||||
| parent-portal | ai15 | :4002 可访问 + MF Remote | ⏳ | - |
|
||||
| admin-portal | ai16 | :4003 可访问 + MF Remote + admin | ⏳ | - |
|
||||
|
||||
---
|
||||
|
||||
## §9 统一集成测试检查清单
|
||||
|
||||
> 所有模块就绪后,按此清单进行统一集成测试
|
||||
|
||||
### 9.1 认证链路
|
||||
|
||||
- [ ] teacher-portal 登录 → api-gateway → iam → JWT 签发
|
||||
- [ ] JWT 验签 → x-user-id 注入 → teacher-bff 读取
|
||||
|
||||
### 9.2 GraphQL 链路
|
||||
|
||||
- [ ] teacher-portal → teacher-bff GraphQL dashboard Query
|
||||
- [ ] teacher-bff → iam gRPC + core-edu gRPC 并行聚合
|
||||
- [ ] ActionState 信封正确返回
|
||||
|
||||
### 9.3 事件链路
|
||||
|
||||
- [ ] core-edu 发布 GradeEvent → Kafka → msg 消费 → push-gateway 推送
|
||||
- [ ] data-ana 消费 CDC 事件 → ClickHouse 写入
|
||||
|
||||
### 9.4 MF 链路
|
||||
|
||||
- [ ] teacher-portal Shell 加载 student-portal Remote
|
||||
- [ ] GraphQL client 单例跨 Remote 共享
|
||||
|
||||
### 9.5 推送链路
|
||||
|
||||
- [ ] msg → push-gateway /internal/push → WebSocket 推送到前端
|
||||
24
docs/architecture/issues/objections/admin-portal_issue.md
Normal file
24
docs/architecture/issues/objections/admin-portal_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# admin-portal 问题记录
|
||||
|
||||
> 负责人:ai16
|
||||
> 关联:[coord.md](../coord.md)、[contracts/admin-portal_contract.md](../contracts/admin-portal_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/ai_issue.md
Normal file
24
docs/architecture/issues/objections/ai_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# ai 问题记录
|
||||
|
||||
> 负责人:ai12
|
||||
> 关联:[coord.md](../coord.md)、[contracts/ai_contract.md](../contracts/ai_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/api-gateway_issue.md
Normal file
24
docs/architecture/issues/objections/api-gateway_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
58
docs/architecture/issues/objections/classes_issue.md
Normal file
58
docs/architecture/issues/objections/classes_issue.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# classes 问题记录
|
||||
|
||||
> 负责人:ai07
|
||||
> 关联:[coord.md](../coord.md)、[contracts/classes_contract.md](../contracts/classes_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
### ISSUE-001-ai07:P3 合并后 classes 目录保留方式
|
||||
|
||||
- **提请方**:ai07
|
||||
- **日期**:2026-07-10
|
||||
- **类型**:契约不明确
|
||||
- **描述**:classes 是黄金模板,P3 合并入 core-edu 后,`services/classes/` 目录是否保留?若保留,是只读对照基准还是改为 shared-ts 模板?若删除,黄金模板对照基准丢失,其他 TS 服务无法对齐。
|
||||
- **建议方案**:保留 `services/classes/` 作为只读对照基准(不删除),core-edu 复制其结构。黄金模板 checklist(02-architecture-design.md §10)由 ai07 持续维护。
|
||||
- **状态**:待 coord 仲裁
|
||||
|
||||
### ISSUE-002-ai07:cuid2 迁移时机
|
||||
|
||||
- **提请方**:ai07
|
||||
- **日期**:2026-07-10
|
||||
- **类型**:前置依赖缺失
|
||||
- **描述**:classes 当前用 uuid v4 生成 ID,project_rules 要求 cuid2。迁移时机有两种选择:(A) P1 黄金模板立即迁移;(B) P3 合并入 core-edu 时统一迁移。若选 A,黄金模板先行避免 ai08 继承 uuid 遗留;若选 B,减少一次迁移成本。
|
||||
- **建议方案**:选 A(P1 立即迁移),黄金模板应先行,避免其他服务继承 uuid 遗留。迁移涉及 classes 表主键、proto message 字段类型。
|
||||
- **状态**:待 coord 仲裁
|
||||
|
||||
### ISSUE-003-ai07:proto ClassService 合并后归属
|
||||
|
||||
- **提请方**:ai07
|
||||
- **日期**:2026-07-10
|
||||
- **类型**:契约不明确
|
||||
- **描述**:P3 合并入 core-edu 后,`classes.proto` 的 `ClassService` 是否迁移到 `core_edu.proto`?若迁移,破坏 proto 契约稳定性(下游需更新 import);若保留,core_edu.proto 不会膨胀但需在 core-edu 实现中 import classes.proto。
|
||||
- **建议方案**:保留 `classes.proto` 由 core-edu 实现(选项 B),保持契约稳定性,避免下游 import 路径变更。
|
||||
- **状态**:待 coord 仲裁
|
||||
|
||||
### ISSUE-004-ai07:node:20-alpine → node:22-alpine 升级时机
|
||||
|
||||
- **提请方**:ai07
|
||||
- **日期**:2026-07-10
|
||||
- **类型**:其他
|
||||
- **描述**:classes Dockerfile 用 `node:20-alpine`,project_rules §15.8 镜像预拉清单是 `node:22-alpine`。是 P1 立即升级还是等统一升级?
|
||||
- **建议方案**:P1 立即升级,影响所有 TS 服务 Dockerfile 基准,黄金模板应先行。
|
||||
- **状态**:待 coord 仲裁
|
||||
24
docs/architecture/issues/objections/content_issue.md
Normal file
24
docs/architecture/issues/objections/content_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# content 问题记录
|
||||
|
||||
> 负责人:ai09
|
||||
> 关联:[coord.md](../coord.md)、[contracts/content_contract.md](../contracts/content_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/core-edu_issue.md
Normal file
24
docs/architecture/issues/objections/core-edu_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# core-edu 问题记录
|
||||
|
||||
> 负责人:ai08
|
||||
> 关联:[coord.md](../coord.md)、[contracts/core-edu_contract.md](../contracts/core-edu_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
13
docs/architecture/issues/objections/cross_issue.md
Normal file
13
docs/architecture/issues/objections/cross_issue.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 跨模块问题记录
|
||||
|
||||
> 维护者:所有 AI(跨模块问题在此追加)
|
||||
> 关联:[coord.md](../coord.md)
|
||||
> 规则:涉及多个模块的问题在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!-- 追加条目格式同各模块 issue.md -->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/data-ana_issue.md
Normal file
24
docs/architecture/issues/objections/data-ana_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# data-ana 问题记录
|
||||
|
||||
> 负责人:ai11
|
||||
> 关联:[coord.md](../coord.md)、[contracts/data-ana_contract.md](../contracts/data-ana_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/iam_issue.md
Normal file
24
docs/architecture/issues/objections/iam_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# iam 问题记录
|
||||
|
||||
> 负责人:ai06
|
||||
> 关联:[coord.md](../coord.md)、[contracts/iam_contract.md](../contracts/iam_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/msg_issue.md
Normal file
24
docs/architecture/issues/objections/msg_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# msg 问题记录
|
||||
|
||||
> 负责人:ai10
|
||||
> 关联:[coord.md](../coord.md)、[contracts/msg_contract.md](../contracts/msg_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/parent-bff_issue.md
Normal file
24
docs/architecture/issues/objections/parent-bff_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# parent-bff 问题记录
|
||||
|
||||
> 负责人:ai05
|
||||
> 关联:[coord.md](../coord.md)、[contracts/parent-bff_contract.md](../contracts/parent-bff_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/parent-portal_issue.md
Normal file
24
docs/architecture/issues/objections/parent-portal_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# parent-portal 问题记录
|
||||
|
||||
> 负责人:ai15
|
||||
> 关联:[coord.md](../coord.md)、[contracts/parent-portal_contract.md](../contracts/parent-portal_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/push-gateway_issue.md
Normal file
24
docs/architecture/issues/objections/push-gateway_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# push-gateway 问题记录
|
||||
|
||||
> 负责人:ai02
|
||||
> 关联:[coord.md](../coord.md)、[contracts/push-gateway_contract.md](../contracts/push-gateway_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/student-bff_issue.md
Normal file
24
docs/architecture/issues/objections/student-bff_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# student-bff 问题记录
|
||||
|
||||
> 负责人:ai04
|
||||
> 关联:[coord.md](../coord.md)、[contracts/student-bff_contract.md](../contracts/student-bff_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/student-portal_issue.md
Normal file
24
docs/architecture/issues/objections/student-portal_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# student-portal 问题记录
|
||||
|
||||
> 负责人:ai14
|
||||
> 关联:[coord.md](../coord.md)、[contracts/student-portal_contract.md](../contracts/student-portal_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
24
docs/architecture/issues/objections/teacher-bff_issue.md
Normal file
24
docs/architecture/issues/objections/teacher-bff_issue.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# teacher-bff 问题记录
|
||||
|
||||
> 负责人:ai03
|
||||
> 关联:[coord.md](../coord.md)、[contracts/teacher-bff_contract.md](../contracts/teacher-bff_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
<!--
|
||||
追加条目格式:
|
||||
|
||||
### ISSUE-[编号]-[AI标识]:[标题]
|
||||
|
||||
- **提请方**:aiXX
|
||||
- **日期**:YYYY-MM-DD
|
||||
- **类型**:契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
|
||||
- **描述**:[详细描述问题]
|
||||
- **建议方案**:[AI 的建议]
|
||||
- **状态**:待 coord 仲裁 / 已裁决(见 coord.md §X)
|
||||
-->
|
||||
|
||||
(暂无问题)
|
||||
161
docs/architecture/issues/objections/teacher-portal_issue.md
Normal file
161
docs/architecture/issues/objections/teacher-portal_issue.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# teacher-portal 问题记录
|
||||
|
||||
> 负责人:ai13
|
||||
> 关联:[coord.md](../coord.md)、[contracts/teacher-portal_contract.md](../contracts/teacher-portal_contract.md)
|
||||
> 规则:AI 遇到问题时在此追加条目,coord 仲裁后更新状态
|
||||
> 迁移说明:本文件 ISSUE-036~041 迁移自旧 `docs/issues.md` §2.7(已按总裁裁决 §0.4 编号冲突整理规则保留原始编号 + 追加 `<提请AI>` 后缀)
|
||||
|
||||
---
|
||||
|
||||
## 问题列表
|
||||
|
||||
### ISSUE-036-ai13:03-long-term-architecture.md "REST→GraphQL 演进策略"与 F9 裁决冲突
|
||||
|
||||
- **提请方**:ai13
|
||||
- **日期**:2026-07-09
|
||||
- **类型**:裁决冲突 + 文档同步遗漏
|
||||
- **阶段**:P2
|
||||
- **描述**:
|
||||
- coord F9 裁决:"P2 起 BFF 用 GraphQL,前端用 urql/apollo",禁止"P2-P6 REST,未来切 GraphQL"
|
||||
- ai13 `apps/teacher-portal/docs/03-long-term-architecture.md` §1.4(提请时误记为 §6,实际 §6 是 i18n)仍保留"REST→GraphQL 切换策略"过渡方案(P2-P4 REST + P5+ 迁移 + feature flag 灰度)
|
||||
- 与 F9 裁决直接冲突,违反"不分阶段"原则
|
||||
- **建议方案**:
|
||||
1. 删除 §1.4 "REST → GraphQL 切换策略"整节
|
||||
2. 替换为"GraphQL 客户端架构(P2 起最终方案)"
|
||||
3. 同步更新 §1.1 P2 行 + §1.2 表格 + §12.3 #3 待裁决问题
|
||||
- **状态**:✅ 已回写闭合(2026-07-09,批次 0.15 后)
|
||||
- **coord 裁决**:总裁裁决 §3.4 + §4.4 第 5 条(删除过渡方案本身,保留设计决策记录)
|
||||
- **回写执行**:详见 [03-long-term-architecture.md §14.1](../../../apps/teacher-portal/docs/03-long-term-architecture.md)
|
||||
|
||||
---
|
||||
|
||||
### ISSUE-037-ai13:ai13 与 ai03 的 GraphQL schema 契约协调机制未明确
|
||||
|
||||
- **提请方**:ai13
|
||||
- **日期**:2026-07-09
|
||||
- **类型**:契约不明确
|
||||
- **阶段**:P2
|
||||
- **描述**:
|
||||
- F9 裁决:P2 起 BFF 用 GraphQL(teacher-bff ai03 实现 GraphQL Yoga server),前端用 urql(teacher-portal ai13 实现 GraphQL client)
|
||||
- B1 裁决:GraphQL Yoga + DataLoader
|
||||
- ai13 与 ai03 的 GraphQL schema 契约如何协调未明确:
|
||||
- schema 定义权归属(ai03 定义 server schema,ai13 消费?还是 coord 仲裁第一版?)
|
||||
- schema 版本管理(GraphQL schema 演进如何同步前后端?)
|
||||
- 查询/变更/订阅的命名规范(camelCase vs snake_case?分页规范?)
|
||||
- 错误响应格式(GraphQL errors 数组 vs ActionState 信封?)
|
||||
- 与 ISSUE-019(ai08 core-edu 调用侧)/ ISSUE-030(ai04 student-bff 前端消费侧)同类但不同 BFF
|
||||
- **建议方案**:
|
||||
1. coord 在批次 1 启动前仲裁 teacher-bff GraphQL schema 第一版
|
||||
2. 建立"schema-first"工作流:ai03 定义 → coord 仲裁 → ai13 消费 → 变更需 PR + 双方 review
|
||||
3. 明确 GraphQL 规范:camelCase 命名、Relay Cursor Connections 分页、errors 数组扩展 ActionState 字段(extensions.code = BFF_TEACHER_*)
|
||||
4. 建立 schema 注册表(packages/shared-ts/contracts/graphql/)
|
||||
- **状态**:✅ 已裁决
|
||||
- **coord 裁决**:总裁裁决 §2.2(GraphQL schema 仲裁机制)—— SDL-first,存放 `packages/shared-ts/contracts/graphql/`,各 BFF AI 起草 + coord 仲裁第一版
|
||||
- **执行**:ai13 待 ai03 起草 teacher-bff schema 后消费,coord 批次 1 启动前仲裁
|
||||
|
||||
---
|
||||
|
||||
### ISSUE-038-ai13:MF 暴露 AppShell 整体是否包含 GraphQL client 单例未明确
|
||||
|
||||
- **提请方**:ai13
|
||||
- **日期**:2026-07-09
|
||||
- **类型**:契约不明确
|
||||
- **阶段**:P2
|
||||
- **描述**:
|
||||
- F10 裁决:"暴露 AppShell 整体,各 Remote 自行决定内部布局"
|
||||
- 问题:AppShell 整体暴露是否包含 GraphQL client 单例(urql Client)?
|
||||
- 方案 A(包含):Shell 初始化 GraphQL client,通过 React Context 注入给所有 Remote,共享同一 client 实例 + 缓存
|
||||
- 方案 B(不包含):各 Remote 自行初始化,缓存隔离
|
||||
- 方案 A 风险:MF 跨 React 实例时 Context 共享有坑(shared singleton 要求)
|
||||
- 方案 B 风险:缓存隔离导致跨 Remote 数据不一致
|
||||
- **建议方案**:建议方案 A(Shell 暴露 GraphQL client 单例)
|
||||
- **状态**:✅ 已裁决
|
||||
- **coord 裁决**:总裁裁决 §2.17(采纳方案 A:Shell 暴露 GraphQLProvider,student/parent/admin Remote 复用 Shell 单例)
|
||||
- **执行**:ai13 P2 实现 Shell GraphQLProvider,ai14/ai15/ai16 消费;MF shared 配置 react/urql/graphql 设为 singleton
|
||||
|
||||
---
|
||||
|
||||
### ISSUE-039-ai13:F8 ai13 维护 packages 的建立时机与依赖未明确
|
||||
|
||||
- **提请方**:ai13
|
||||
- **日期**:2026-07-09
|
||||
- **类型**:前置依赖未就绪 + 工作归属不明
|
||||
- **阶段**:P2
|
||||
- **描述**:
|
||||
- F8 裁决:"ai13 维护 ui-tokens/ui-components/hooks,coord 仅维护 shared-ts/contracts"
|
||||
- 问题:
|
||||
- 这 3 个 packages 何时建立?P2 启动前?P2 启动时?
|
||||
- ai13 P2 首次实现是否依赖这 3 个 packages 已就位?
|
||||
- packages 的依赖关系:ui-components 依赖 ui-tokens,hooks 依赖 ui-components?还是平级?
|
||||
- 4 个 portal 是否都消费?版本管理策略?
|
||||
- ai-work-orchestration.md §3.1 批次 0 未列出这 3 个 packages 的建立任务
|
||||
- **建议方案**:
|
||||
1. ai13 在 P2 启动前(批次 0 末)建立 3 个 packages 骨架
|
||||
2. 依赖关系:ui-tokens(无依赖)→ ui-components(依赖 ui-tokens)→ hooks(依赖 ui-components)
|
||||
3. 4 个 portal 统一消费,版本通过 pnpm workspace 协议("workspace:*")
|
||||
4. ai13 完成后通知 ai14/ai15/ai16 消费
|
||||
- **状态**:✅ 已裁决 + 已执行
|
||||
- **coord 裁决**:总裁裁决 §2.18(ai13 在 P2 启动前 = 批次 0.15 建立 3 个 packages 骨架,4 个 portal 统一消费)
|
||||
- **执行**:ai13 已于批次 0.15 完成建立:
|
||||
- `packages/ui-tokens/`(colors/typography/spacing/shadows + index.ts)
|
||||
- `packages/ui-components/`(cn/error-boundary/loading/empty/require-permission + index.ts)
|
||||
- `packages/hooks/`(use-auth/use-permission/use-a11y-id/use-toast/use-trace-id/use-aria-live/use-api/use-viewports + types + index.ts)
|
||||
- 依赖方向:ui-tokens ← ui-components ← hooks(hooks 不依赖 iam,权限数据 props 注入)
|
||||
- pnpm-workspace.yaml 已注册 packages/*
|
||||
|
||||
---
|
||||
|
||||
### ISSUE-040-ai13:ai13 teacher-portal P2 功能范围与下游服务启用阶段的关系未明确
|
||||
|
||||
- **提请方**:ai13
|
||||
- **日期**:2026-07-09
|
||||
- **类型**:跨度过大
|
||||
- **阶段**:P2
|
||||
- **描述**:
|
||||
- ai-work-orchestration.md §4.3 明确 ai13 P2 范围:client 改造 + MF shell + 共享组件抽取
|
||||
- 但 teacher-portal P2 阶段需对接 teacher-bff(ai03 P2 启动)的哪些 GraphQL 查询未明确:
|
||||
- P2 仅实现登录 + Dashboard 框架?
|
||||
- 还是包含班级列表 + 学生列表等基础教学功能?
|
||||
- teacher-bff P2 只调 iam(gRPC),core-edu P3 / content P4 / data-ana P4 才启用
|
||||
- 若 teacher-portal P2 实现班级/学生/作业等页面,但 teacher-bff P2 无法 gRPC 调 core-edu(P3 才启用),数据来源断裂
|
||||
- ISSUE-004/005 已裁决"BFF 跨阶段扩展下游"是允许的例外,但前端是否也跟随?
|
||||
- **建议方案**:
|
||||
1. 明确 ai13 P2 功能范围:登录 + 权限上下文 + Dashboard 框架 + 个人设置 + 班级列表(iam 数据)+ 学生列表(iam 数据)
|
||||
2. P3 起跟随 teacher-bff 扩展 core-edu 相关页面
|
||||
3. 前端路由骨架 P2 一次性建立,但页面内容随 BFF 能力分阶段填充
|
||||
- **状态**:✅ 已裁决
|
||||
- **coord 裁决**:总裁裁决 §3.5(ai13 P2 功能范围:登录 + 权限上下文 + Dashboard 框架 + 个人设置 + 班级列表 + 学生列表;P3-P5 跟随 teacher-bff 扩展)
|
||||
- **执行**:ai13 P2 按此范围实现,见 workline.md §3 P2 详细任务
|
||||
|
||||
---
|
||||
|
||||
### ISSUE-041-ai13:仲裁评估总结 - 关键裁决对 ai13 的可行性与影响评估
|
||||
|
||||
- **提请方**:ai13
|
||||
- **日期**:2026-07-09
|
||||
- **类型**:跨度过大(评估总结)
|
||||
- **阶段**:P2-P6
|
||||
- **描述**:ai13 完整阅读 coord-final-decisions.md(80+ 项裁决)+ ai-work-orchestration.md(批次 0-5 规划)+ issues.md(35 个已识别问题),对 ai13 的关键裁决影响评估:
|
||||
|
||||
| 裁决编号 | 裁决内容 | 对 ai13 影响 | 可行性 | 风险 |
|
||||
| -------- | -------------------------------- | --------------------------------- | -------------------------------------- | -------------------------------------- |
|
||||
| F9 | P2 起 GraphQL(urql) | 高:从 REST 改造为 GraphQL client | 中:schema 契约协调成本高(ISSUE-037) | schema 未及时仲裁阻塞 P2 |
|
||||
| F12 | P2 localStorage token | 低:与原设计一致 | 高 | 无 |
|
||||
| F8 | ai13 维护 3 个 packages | 中:新增 packages 建立任务 | 高:ai13 已设计完毕 | 时机不明确(ISSUE-039) |
|
||||
| F10 | MF 暴露 AppShell 整体 | 中:Shell 设计需包含 Provider | 高 | GraphQL client 单例不明确(ISSUE-038) |
|
||||
| F7 | 权限点命名 `<RESOURCE>_<ACTION>` | 低:命名规范调整 | 高 | 无 |
|
||||
|
||||
- **影响**:本评估总结供 coord 参考,不直接阻塞工作;具体阻塞问题已分别提请(ISSUE-036~040,均已裁决)
|
||||
- **建议方案**:
|
||||
1. coord 优先处理 ISSUE-037(schema 仲裁机制)+ ISSUE-039(packages 时机),二者是 ai13 P2 启动的关键前置 —— ✅ 均已裁决
|
||||
2. ai13 在等待 coord 仲裁期间,先完成 03 文档回写(删除 REST→GraphQL)+ packages 骨架建立 —— ✅ 均已完成
|
||||
3. ai13 与 ai03 建立 schema 协调通道(PR review + 双方签字)
|
||||
- **状态**:✅ 仅供参考,无具体裁决需求(所有子问题 ISSUE-036~040 均已裁决)
|
||||
- **coord 裁决**:总裁裁决 §10 问题索引表确认 ISSUE-036~041-ai13 映射到 §2.2/2.17/2.18/3.4/3.5
|
||||
|
||||
---
|
||||
|
||||
**AI Agent**: ai13(teacher-portal)
|
||||
**Branch**: feat/teacher-portal-issues-migrate-ai13
|
||||
**Coordinator**: coord-ai
|
||||
**迁移说明**:本文件 ISSUE-036~041 迁移自旧 `docs/issues.md` §2.7,按总裁裁决 §0.4 保留原始编号 + 追加 `<提请AI>` 后缀,状态更新为已裁决
|
||||
268
docs/architecture/issues/workline.md
Normal file
268
docs/architecture/issues/workline.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# 总工作安排(coord 汇总)
|
||||
|
||||
> 维护者:coord(协调 AI)
|
||||
> 维护规则:各 AI 在 `worklines/[模块名]_workline.md` 写入甘特图工作排期 → coord 汇总到本文件
|
||||
> 模式:**全并行**(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock,最后统一集成测试)
|
||||
> 关联文档:[ai-work-orchestration.md](../ai-work-orchestration.md)、[coord.md](./coord.md)、[matrix.md](./matrix.md)、[president-final-rulings.md](../president-final-rulings.md)
|
||||
|
||||
---
|
||||
|
||||
## §1 16 AI 总时间线(甘特图)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Edu 项目 16 AI 功能时间线(总裁裁决 §6.1 最终版)
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section 批次0 coord前置 3-5天
|
||||
coord 0.1-0.14 proto+工具包 :crit, b0, 2026-07-09, 4d
|
||||
ai13 0.15 packages骨架 :b0a, 2026-07-09, 3d
|
||||
|
||||
section 批次1 P2 7-10天
|
||||
ai01 Gateway路由+shared-go :b1a, after b0, 8d
|
||||
ai06 iam P2.1 gRPC+8RPC :crit, b1b, after b0, 8d
|
||||
ai03 teacher-bff GraphQL Must :b1c, after b0, 8d
|
||||
ai13 teacher-portal P2 MF Shell :b1d, after b0, 8d
|
||||
|
||||
section 批次2 P3 7-10天
|
||||
ai07 classes代码交接 :b2a, after b1b, 2d
|
||||
ai08 core-edu gRPC 50053 :crit, b2b, after b1b, 8d
|
||||
ai04 student-bff GraphQL :b2c, after b1c, 8d
|
||||
ai14 student-portal P3 :b2d, after b1d, 8d
|
||||
ai03 teacher-bff P3扩展 :b2e, after b1c, 5d
|
||||
|
||||
section 批次3 P4 10-12天
|
||||
ai09 content gRPC 50054+Question :crit, b3a, after b2b, 11d
|
||||
ai11 data-ana gRPC 50055 :b3b, after b2b, 11d
|
||||
ai05 parent-bff GraphQL :b3c, after b2c, 11d
|
||||
ai15 parent-portal P4 :b3d, after b2d, 11d
|
||||
|
||||
section 批次4 P5 12-15天
|
||||
ai10 msg gRPC 50056 :crit, b4a, after b3a, 13d
|
||||
ai02 push-gateway HTTP :b4b, after b3a, 13d
|
||||
ai12 ai服务 gRPC 50058 :b4c, after b3a, 13d
|
||||
ai03 teacher-bff P5扩展 :b4d, after b3a, 8d
|
||||
|
||||
section 批次5 P6 10-12天
|
||||
ai16 admin-portal MVP :crit, b5a, after b4a, 11d
|
||||
iam P2.2补全 :b5b, after b4a, 11d
|
||||
持续优化 /readyz硬化 :b5c, after b4a, 11d
|
||||
```
|
||||
|
||||
**关键路径**(红色 crit):批次0 → iam P2.1 → core-edu → content → msg → admin-portal
|
||||
**总时间线**:49-64 天(约 10-13 周)
|
||||
|
||||
---
|
||||
|
||||
## §2 批次概览
|
||||
|
||||
| 批次 | 阶段 | 时间 | 参与AI | 完成信号 |
|
||||
| ---- | ---------- | -------- | ------------------------------------ | ------------------ |
|
||||
| 0 | coord 前置 | 3-5 天 | coord + ai13 | ✅ 2026-07-09 完成 |
|
||||
| 1 | P2 | 7-10 天 | ai01 + ai06 + ai03 ⟷ ai13 | ⏳ 进行中 |
|
||||
| 2 | P3 | 7-10 天 | ai07 + ai08 + ai04 ⟷ ai14 + ai03扩展 | ⏳ |
|
||||
| 3 | P4 | 10-12 天 | ai09 + ai11 + ai05 ⟷ ai15 | ⏳ |
|
||||
| 4 | P5 | 12-15 天 | ai10 ⟷ ai02 + ai12 + ai03扩展 | ⏳ |
|
||||
| 5 | P6 | 10-12 天 | ai16 + 持续优化 | ⏳ |
|
||||
|
||||
---
|
||||
|
||||
## §3 批次 1(P2)详细排期
|
||||
|
||||
### 3.1 总甘特图
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title 批次 1(P2)详细排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section ai01 api-gateway
|
||||
1.1 路由表+双入口+shared-go接入 :a1a, 2026-07-10, 3d
|
||||
1.2 JWT校验+JWKS fetcher :a1b, after a1a, 3d
|
||||
1.3 限流+熔断+CORS :a1c, after a1b, 2d
|
||||
|
||||
section ai06 iam P2.1
|
||||
6.1 gRPC server 50052启用 :crit, a6a, 2026-07-10, 2d
|
||||
6.2 8 RPC实现(Register-GetChildrenByParent) :crit, a6b, after a6a, 4d
|
||||
6.3 /readyz深度(5依赖检查) :a6c, after a6b, 1d
|
||||
6.4 iam_student_guardians表+DataScope :a6d, after a6b, 1d
|
||||
|
||||
section ai03 teacher-bff
|
||||
3.1 GraphQL schema第一版(ARB-001) :crit, a3a, 2026-07-10, 2d
|
||||
3.2 Yoga endpoint+5 Query Resolver :crit, a3b, after a3a, 3d
|
||||
3.3 DataLoader+N+1防御 :a3c, after a3b, 1d
|
||||
3.4 ActionState信封+降级模式B :a3d, after a3b, 1d
|
||||
3.5 /readyz探针列表 :a3e, after a3d, 1d
|
||||
|
||||
section ai13 teacher-portal
|
||||
13.1 MF Shell配置(exposes+shared) :crit, a13a, 2026-07-10, 2d
|
||||
13.2 GraphQLProvider+urql client单例 :crit, a13b, after a13a, 2d
|
||||
13.3 AppShell改造(REST→GraphQL) :a13c, after a13b, 2d
|
||||
13.4 登录页+Dashboard页+班级列表页 :a13d, after a13c, 2d
|
||||
```
|
||||
|
||||
### 3.2 依赖关系
|
||||
|
||||
```
|
||||
ai06.6.1 gRPC 启用 ──┬──> ai03.3.2 GraphQL Resolver(调用 iam gRPC)
|
||||
└──> ai01.1.2 JWT 校验(调用 iam GetPublicKey)
|
||||
|
||||
ai03.3.1 schema 第一版 ──> ai13.13.2 GraphQLProvider(消费 schema)
|
||||
ai03.3.2 Resolver ────────> ai13.13.3 AppShell 改造(viewports query)
|
||||
|
||||
ai01.1.1 路由表 ──> ai03.3.2 endpoint 对外可访问
|
||||
└──> ai13.13.4 页面可访问
|
||||
```
|
||||
|
||||
### 3.3 完成信号
|
||||
|
||||
批次 1 完成的 5 个标志:
|
||||
|
||||
1. ai06:iam gRPC 50052 启用 + 12 RPC 全部实现 + /readyz 返回 5 项依赖状态
|
||||
2. ai01:api-gateway 路由 `/api/v1/teacher/*` → teacher-bff:3003 + JWKS 验签 + 限流
|
||||
3. ai03:teacher-bff `POST /graphql` 可用 + 5 Query Resolver + DataLoader + ActionState
|
||||
4. ai13:teacher-portal MF Shell 配置就绪 + 登录/Dashboard/班级列表页可用
|
||||
5. 端到端:教师登录 → 看到 Dashboard(含班级列表 + 视口导航)
|
||||
|
||||
---
|
||||
|
||||
## §4 各 AI 工作清单(汇总)
|
||||
|
||||
> 详细排期见各模块 `_workline.md` 文件
|
||||
|
||||
### 4.1 ai01(api-gateway)
|
||||
|
||||
- P2:路由表 + JWT RS256 验签(JWKS) + 限流 + 熔断 + CORS + shared-go 接入
|
||||
- 详见:[api-gateway_workline.md](./api-gateway_workline.md)
|
||||
|
||||
### 4.2 ai02(push-gateway)
|
||||
|
||||
- P5:HTTP /internal/* + Kafka 双通道 + 多设备会话隔离 + 审计表
|
||||
- 详见:[push-gateway_workline.md](./push-gateway_workline.md)
|
||||
|
||||
### 4.3 ai03(teacher-bff)
|
||||
|
||||
- P2:GraphQL schema 第一版 + 5 Query Resolver + DataLoader + ActionState
|
||||
- P3 扩展:exams/homework/grades Query + Mutation
|
||||
- P5 扩展:notifications Query + SSE
|
||||
- 详见:[teacher-bff_workline.md](./teacher-bff_workline.md)
|
||||
|
||||
### 4.4 ai04(student-bff)
|
||||
|
||||
- P3:GraphQL schema + Dashboard + 考试作答 + 作业提交
|
||||
- 详见:[student-bff_workline.md](./student-bff_workline.md)
|
||||
|
||||
### 4.5 ai05(parent-bff)
|
||||
|
||||
- P4:GraphQL schema + Dashboard + 多子女切换 + 成绩趋势
|
||||
- 详见:[parent-bff_workline.md](./parent-bff_workline.md)
|
||||
|
||||
### 4.6 ai06(iam)
|
||||
|
||||
- P2.1:gRPC 50052 + 12 RPC + /readyz 深度 + iam_student_guardians + DataScope
|
||||
- P2.2:P3-P6 期间持续补充
|
||||
- 详见:[iam_workline.md](./iam_workline.md)
|
||||
|
||||
### 4.7 ai07(classes → core-edu 交接)
|
||||
|
||||
- P3:classes 代码交接 + core-edu 服务骨架
|
||||
- 详见:[core-edu_workline.md](./core-edu_workline.md)
|
||||
|
||||
### 4.8 ai08(core-edu)
|
||||
|
||||
- P3:gRPC 50053 + exams/homework/grades/attendance/class 全部 RPC + Outbox
|
||||
- 详见:[core-edu_workline.md](./core-edu_workline.md)
|
||||
|
||||
### 4.9 ai09(content)
|
||||
|
||||
- P4:gRPC 50054 + textbook/chapter/knowledge-graph/question 4 service + Neo4j + ES
|
||||
- 详见:[content_workline.md](./content_workline.md)
|
||||
|
||||
### 4.10 ai10(msg)
|
||||
|
||||
- P5:gRPC 50056 + notification/preference/template 3 service + Outbox
|
||||
- 详见:[msg_workline.md](./msg_workline.md)
|
||||
|
||||
### 4.11 ai11(data-ana)
|
||||
|
||||
- P4:gRPC 50055 + 4 Dashboard + Warning + Mastery + ClickHouse + CDC 消费
|
||||
- 详见:[data-ana_workline.md](./data-ana_workline.md)
|
||||
|
||||
### 4.12 ai12(ai 服务)
|
||||
|
||||
- P5:gRPC 50058 + Chat/GenerateQuestion/OptimizeExpression/LessonPlan + ES 检索
|
||||
- 详见:[ai_workline.md](./ai_workline.md)
|
||||
|
||||
### 4.13 ai13(teacher-portal)
|
||||
|
||||
- P2:MF Shell + AppShell + GraphQL client + 登录/Dashboard/班级列表
|
||||
- P3:考试/作业/成绩页面 + MF Remote 就绪
|
||||
- 详见:[teacher-portal_workline.md](./teacher-portal_workline.md)
|
||||
|
||||
### 4.14 ai14(student-portal)
|
||||
|
||||
- P3:MF Remote + 考试作答 + 作业提交
|
||||
- 详见:[student-portal_workline.md](./student-portal_workline.md)
|
||||
|
||||
### 4.15 ai15(parent-portal)
|
||||
|
||||
- P4:MF Remote + Dashboard + 多子女切换
|
||||
- 详见:[parent-portal_workline.md](./parent-portal_workline.md)
|
||||
|
||||
### 4.16 ai16(admin-portal)
|
||||
|
||||
- P6:MVP + 用户管理 + 角色权限 + 审计日志
|
||||
- 详见:[admin-portal_workline.md](./admin-portal_workline.md)
|
||||
|
||||
---
|
||||
|
||||
## §5 工作流说明
|
||||
|
||||
### 5.1 各 AI 写入 `_workline.md`
|
||||
|
||||
各 AI 在自己模块的 `[模块名]_workline.md` 中写入甘特图排期,格式:
|
||||
|
||||
````markdown
|
||||
# [模块名] 工作排期
|
||||
|
||||
## §1 总览
|
||||
|
||||
[模块职责简述]
|
||||
|
||||
## §2 甘特图
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title [模块名] 工作排期
|
||||
dateFormat YYYY-MM-DD
|
||||
...
|
||||
```
|
||||
````
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### [阶段] [任务编号]:[任务名]
|
||||
|
||||
- 负责人:aiXX
|
||||
- 依赖:[前置任务]
|
||||
- 交付物:[具体文件/功能]
|
||||
- 验收标准:[验收点]
|
||||
|
||||
```
|
||||
|
||||
### 5.2 coord 汇总
|
||||
|
||||
coord 审阅各 `_workline.md` 后,在本文件(workline.md)汇总:
|
||||
- §1 总时间线(16 AI 甘特图)
|
||||
- §2 批次概览
|
||||
- §3 当前批次详细排期
|
||||
- §4 各 AI 工作清单(链接到各 `_workline.md`)
|
||||
|
||||
### 5.3 更新时机
|
||||
|
||||
- 批次启动前:coord 写入 §3 当前批次详细排期
|
||||
- 批次进行中:各 AI 更新自己的 `_workline.md`(标记完成进度)
|
||||
- 批次完成后:coord 更新 §2 完成信号 + §1 总进度
|
||||
```
|
||||
45
docs/architecture/issues/worklines/admin-portal_workline.md
Normal file
45
docs/architecture/issues/worklines/admin-portal_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# admin-portal 工作排期
|
||||
|
||||
> 负责人:ai16
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/admin-portal_contract.md](../contracts/admin-portal_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
admin-portal 是管理端微前端,通过 MF Remote 接入主应用,覆盖用户管理、角色权限、审计日志等场景。全阶段目标:P2 MF Remote 骨架 → P3 用户管理+角色权限+审计日志 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai16 admin-portal 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a16a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai16 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai16
|
||||
- **交付物**:⚠️ 由 ai16 自行补充
|
||||
- **依赖**:见 [contracts/admin-portal_contract.md](../contracts/admin-portal_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai16 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai16 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai16 自行补充
|
||||
45
docs/architecture/issues/worklines/ai_workline.md
Normal file
45
docs/architecture/issues/worklines/ai_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# ai 工作排期
|
||||
|
||||
> 负责人:ai12
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/ai_contract.md](../contracts/ai_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
ai 是智能服务,提供 AiService(6 个 RPC),结合 Elasticsearch 检索与 LLM 网关实现智能问答与推荐。全阶段目标:P2 ES 接入+LLM 网关 → P3 AiService 6 RPC → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai12 ai 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a12a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai12 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai12
|
||||
- **交付物**:⚠️ 由 ai12 自行补充
|
||||
- **依赖**:见 [contracts/ai_contract.md](../contracts/ai_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai12 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai12 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai12 自行补充
|
||||
57
docs/architecture/issues/worklines/api-gateway_workline.md
Normal file
57
docs/architecture/issues/worklines/api-gateway_workline.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# api-gateway 工作排期
|
||||
|
||||
> 负责人:ai01
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/api-gateway_contract.md](../contracts/api-gateway_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
api-gateway 是 Edu 系统统一入口,负责路由、JWT 验签、限流、熔断、CORS。全阶段目标:P2 路由+JWT → P3 限流加固 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai01 api-gateway 全阶段排期
|
||||
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 P3-P6 持续优化
|
||||
路由扩展(student/parent/admin) :a1d, after a1c, 2d
|
||||
指标+链路加固 :a1e, after a1d, 2d
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai01 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### P2:路由表 + JWT + 限流
|
||||
|
||||
- **负责人**: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 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:iam GetPublicKey RPC(ai06)
|
||||
- **我的就绪信号**:api-gateway :8080 可访问 + JWT 验签可用
|
||||
107
docs/architecture/issues/worklines/classes_workline.md
Normal file
107
docs/architecture/issues/worklines/classes_workline.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# classes 工作排期
|
||||
|
||||
> 负责人:ai07
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/classes_contract.md](../contracts/classes_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
> 特殊说明:classes 是黄金模板(P1 已实现),P3 合并入 core-edu(ai08 接管)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
classes 是 Edu 微服务的黄金模板,P1 阶段已实现班级 CRUD + 全横切关注点。P3 阶段合并入 core-edu 后,classes 模块代码由 ai08 接管维护,但黄金模板对照 checklist 由 ai07 持续维护。
|
||||
|
||||
**全阶段目标**:
|
||||
|
||||
- P1(已完成):班级 CRUD + 黄金模板 + 6 项整改
|
||||
- P3:合并入 core-edu + Outbox + gRPC + DataScope + Redis 缓存 + cuid2 迁移
|
||||
- P6:黄金模板 checklist 持续维护 + 硬化
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P1-P6)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai07 classes 黄金模板全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P1 黄金模板整改
|
||||
阶段1+2架构设计文档 :crit, a7a, 2026-07-10, 1d
|
||||
typeorm移除+tracer修复+@Query替换 :crit, a7b, 2026-07-10, 1d
|
||||
测试覆盖率60→80%+Dockerfile升级 :a7c, 2026-07-11, 1d
|
||||
schema索引补齐 :a7d, 2026-07-11, 1d
|
||||
|
||||
section P3 合并入core-edu
|
||||
shared/outbox目录补齐 :a7e, 2026-07-14, 2d
|
||||
cuid2迁移+DataScope注入 :a7f, 2026-07-15, 2d
|
||||
Redis缓存+软删除+分页 :a7g, 2026-07-17, 3d
|
||||
gRPC 50053启用+核心edu合并 :a7h, 2026-07-20, 2d
|
||||
|
||||
section P6 硬化
|
||||
黄金模板checklist持续维护 :a7i, 2026-07-25, 5d
|
||||
可观测性增强+告警规则 :a7j, 2026-07-30, 3d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### P1:黄金模板整改(立即)
|
||||
|
||||
| # | 任务 | 交付物 | 验收标准 | 状态 |
|
||||
| --- | ----------------------------------- | ------------------------------------------------------------------------- | --------------------------------- | --------- |
|
||||
| 1 | 阶段 1+2 架构设计文档 | `services/classes/docs/01-understanding.md` + `02-architecture-design.md` | coord 交叉审查通过 | ✅ 已完成 |
|
||||
| 2 | 移除 typeorm 冗余依赖 | `package.json` 删除 typeorm 行 | `pnpm install` 零错误 | 待执行 |
|
||||
| 3 | tracer.ts console.log → logger.info | `src/shared/observability/tracer.ts` L20 | grep `console.log` 返回 0 | 待执行 |
|
||||
| 4 | controller list @Req → @Query | `src/classes/classes.controller.ts` L49-53 | list 方法用 `@Query('gradeId')` | 待执行 |
|
||||
| 5 | 测试覆盖率阈值 60% → 80% | `vitest.config.ts` + 补测试用例 | `pnpm test:coverage` 达 80% | 待执行 |
|
||||
| 6 | Dockerfile node:20 → node:22 | `Dockerfile` | 基础镜像 `node:22-alpine` | 待执行 |
|
||||
| 7 | schema 补齐索引 | `classes.schema.ts` + 迁移脚本 | grade_id / head_teacher_id 有索引 | 待执行 |
|
||||
|
||||
### P3:合并入 core-edu(交接给 ai08)
|
||||
|
||||
| # | 任务 | 交付物 | 验收标准 | 状态 |
|
||||
| --- | ------------------------- | ---------------------------------------------------------- | ------------------------------- | ------ |
|
||||
| 1 | `shared/outbox/` 目录补齐 | outbox.repository / outbox.publisher / outbox.relay-worker | 事件可发布到 Kafka | 待执行 |
|
||||
| 2 | cuid2 迁移 | id 字段 uuid v4 → cuid2 + 数据迁移脚本 | 双写过渡期完成 | 待执行 |
|
||||
| 3 | DataScope 6 级 WHERE 注入 | Repository list 方法接收 dataScope 参数 | 6 级过滤生效 | 待执行 |
|
||||
| 4 | Redis 班级列表缓存 | cachedList 方法 + TTL 5 分钟 + 事件失效 | 缓存命中率 > 80% | 待执行 |
|
||||
| 5 | 软删除 + 审计字段 | deleted_at / created_by / updated_by | DELETE 改软删除 | 待执行 |
|
||||
| 6 | 分页 + 批量查询 | 游标分页 + POST /classes/batch | proto page_size/page_token 落地 | 待执行 |
|
||||
| 7 | gRPC server 50053 启用 | ClassService 5 RPC 实现 | gRPC health check SERVING | 待执行 |
|
||||
| 8 | PermissionGuard 改调 iam | 动态权限查询 getEffectivePermissions | 角色变更无需改代码重启 | 待执行 |
|
||||
|
||||
### P6:硬化
|
||||
|
||||
| # | 任务 | 交付物 | 验收标准 | 状态 |
|
||||
| --- | --------------------------- | ---------------------------------- | ---------------- | ------ |
|
||||
| 1 | 黄金模板 checklist 持续维护 | 02-architecture-design.md §10 更新 | 覆盖全部新模式 | 待执行 |
|
||||
| 2 | 可观测性增强 | 日志注入 traceId + Outbox 积压告警 | Grafana 面板可用 | 待执行 |
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:
|
||||
- MySQL classes_db(P1 已就绪)
|
||||
- api-gateway 路由注册(P1 已就绪)
|
||||
- P3:iam `BatchGetUsers` + `getEffectivePermissions`
|
||||
- P3:Kafka 基础设施(ai08 启用)
|
||||
- **我的就绪信号**:
|
||||
- P1:REST API 5 端点 + /healthz + /readyz + /metrics 可用(已就绪)
|
||||
- P3:gRPC 50053 + Outbox 事件发布 + Redis 缓存可用
|
||||
- **交接信号**:P3 合并时,ai07 向 ai08 交接 classes 模块,交接清单见 02-architecture-design.md §11
|
||||
|
||||
---
|
||||
|
||||
## §5 交接计划(ai07 → ai08)
|
||||
|
||||
| 交接项 | 内容 | 时间 |
|
||||
| ------------------ | ------------------------------------------------------- | --------- |
|
||||
| 源码交接 | `services/classes/src/` 全部源码 | P3 启动时 |
|
||||
| 文档交接 | `services/classes/docs/` 2 份设计文档 | P3 启动时 |
|
||||
| proto 交接 | `classes.proto` 由 core-edu 实现承载 | P3 启动时 |
|
||||
| 黄金模板保留 | `services/classes/` 保留作只读对照基准(待 coord 仲裁) | P3 合并后 |
|
||||
| checklist 持续维护 | ai07 持续维护 02-architecture-design.md §10 | 跨阶段 |
|
||||
45
docs/architecture/issues/worklines/content_workline.md
Normal file
45
docs/architecture/issues/worklines/content_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# content 工作排期
|
||||
|
||||
> 负责人:ai09
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/content_contract.md](../contracts/content_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
content 是内容服务,提供 TextbookService、ChapterService、KnowledgeGraphService、QuestionService,结合 Neo4j 知识图谱与 Elasticsearch 全文检索。全阶段目标:P2 服务骨架+Neo4j+ES → P3 四大 Service 实现 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai09 content 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a9a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai09 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai09
|
||||
- **交付物**:⚠️ 由 ai09 自行补充
|
||||
- **依赖**:见 [contracts/content_contract.md](../contracts/content_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai09 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai09 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai09 自行补充
|
||||
45
docs/architecture/issues/worklines/core-edu_workline.md
Normal file
45
docs/architecture/issues/worklines/core-edu_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# core-edu 工作排期
|
||||
|
||||
> 负责人:ai08
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/core-edu_contract.md](../contracts/core-edu_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
core-edu 是教学核心服务,提供 ClassService、ExamService、HomeworkService、GradeService、AttendanceService,并基于 Outbox 模式发布领域事件。全阶段目标:P2 服务骨架+Outbox → P3 五大 Service 实现 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai08 core-edu 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a8a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai08 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai08
|
||||
- **交付物**:⚠️ 由 ai08 自行补充
|
||||
- **依赖**:见 [contracts/core-edu_contract.md](../contracts/core-edu_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai08 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai08 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai08 自行补充
|
||||
45
docs/architecture/issues/worklines/data-ana_workline.md
Normal file
45
docs/architecture/issues/worklines/data-ana_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# data-ana 工作排期
|
||||
|
||||
> 负责人:ai11
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/data-ana_contract.md](../contracts/data-ana_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
data-ana 是数据分析服务,提供 AnalyticsService(12 个 RPC),基于 ClickHouse 列存查询与 CDC 消费实现实时分析。全阶段目标:P2 ClickHouse 接入+CDC 消费 → P3 AnalyticsService 12 RPC → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai11 data-ana 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a11a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai11 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai11
|
||||
- **交付物**:⚠️ 由 ai11 自行补充
|
||||
- **依赖**:见 [contracts/data-ana_contract.md](../contracts/data-ana_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai11 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai11 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai11 自行补充
|
||||
56
docs/architecture/issues/worklines/iam_workline.md
Normal file
56
docs/architecture/issues/worklines/iam_workline.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# iam 工作排期
|
||||
|
||||
> 负责人:ai06
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/iam_contract.md](../contracts/iam_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
iam 是身份认证服务,全阶段目标:gRPC 50052 + 12 RPC + /readyz 深度 + iam_student_guardians + DataScope + 审计日志 + Outbox。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai06 iam 全阶段排期
|
||||
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.2-P6 扩展
|
||||
审计日志+Outbox :a6e, after a6d, 3d
|
||||
持续补全 :a6f, after a6e, 5d
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai06 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### P2.1:gRPC + 12 RPC + /readyz + DataScope
|
||||
|
||||
- **负责人**:ai06
|
||||
- **交付物**:
|
||||
- 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 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:无(iam 是基础服务)
|
||||
- **我的就绪信号**:gRPC 50052 启用 + GetPublicKey RPC 可用 + HealthService.Check 返回 SERVING
|
||||
45
docs/architecture/issues/worklines/msg_workline.md
Normal file
45
docs/architecture/issues/worklines/msg_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# msg 工作排期
|
||||
|
||||
> 负责人:ai10
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/msg_contract.md](../contracts/msg_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
msg 是消息服务,提供 NotificationService、PreferenceService、TemplateService,并基于 Outbox 模式发布消息事件。全阶段目标:P2 服务骨架+Outbox → P3 三大 Service 实现 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai10 msg 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a10a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai10 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai10
|
||||
- **交付物**:⚠️ 由 ai10 自行补充
|
||||
- **依赖**:见 [contracts/msg_contract.md](../contracts/msg_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai10 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai10 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai10 自行补充
|
||||
45
docs/architecture/issues/worklines/parent-bff_workline.md
Normal file
45
docs/architecture/issues/worklines/parent-bff_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# parent-bff 工作排期
|
||||
|
||||
> 负责人:ai05
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/parent-bff_contract.md](../contracts/parent-bff_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
parent-bff 为家长端提供 GraphQL 聚合 API,覆盖 Dashboard、多子女切换、成绩趋势等场景。全阶段目标:P2 GraphQL schema 骨架 → P3 Dashboard+多子女+成绩趋势 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai05 parent-bff 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a5a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai05 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai05
|
||||
- **交付物**:⚠️ 由 ai05 自行补充
|
||||
- **依赖**:见 [contracts/parent-bff_contract.md](../contracts/parent-bff_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai05 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai05 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai05 自行补充
|
||||
45
docs/architecture/issues/worklines/parent-portal_workline.md
Normal file
45
docs/architecture/issues/worklines/parent-portal_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# parent-portal 工作排期
|
||||
|
||||
> 负责人:ai15
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/parent-portal_contract.md](../contracts/parent-portal_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
parent-portal 是家长端微前端,通过 MF Remote 接入主应用,覆盖 Dashboard、多子女切换等场景。全阶段目标:P2 MF Remote 骨架 → P3 Dashboard+多子女切换 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai15 parent-portal 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a15a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai15 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai15
|
||||
- **交付物**:⚠️ 由 ai15 自行补充
|
||||
- **依赖**:见 [contracts/parent-portal_contract.md](../contracts/parent-portal_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai15 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai15 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai15 自行补充
|
||||
45
docs/architecture/issues/worklines/push-gateway_workline.md
Normal file
45
docs/architecture/issues/worklines/push-gateway_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# push-gateway 工作排期
|
||||
|
||||
> 负责人:ai02
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/push-gateway_contract.md](../contracts/push-gateway_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
push-gateway 负责长连接接入(HTTP /internal/* + WebSocket/SSE)与多设备会话管理,配合审计表实现消息推送可观测。全阶段目标:P2 HTTP+WS 接入 → P3 多设备会话 → P4-P6 审计与加固。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai02 push-gateway 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a2a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai02 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai02
|
||||
- **交付物**:⚠️ 由 ai02 自行补充
|
||||
- **依赖**:见 [contracts/push-gateway_contract.md](../contracts/push-gateway_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai02 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai02 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai02 自行补充
|
||||
45
docs/architecture/issues/worklines/student-bff_workline.md
Normal file
45
docs/architecture/issues/worklines/student-bff_workline.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# student-bff 工作排期
|
||||
|
||||
> 负责人:ai04
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/student-bff_contract.md](../contracts/student-bff_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
student-bff 为学生端提供 GraphQL 聚合 API,覆盖 Dashboard、考试作答、作业提交等场景。全阶段目标:P2 GraphQL schema 骨架 → P3 Dashboard+考试+作业聚合 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai04 student-bff 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a4a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai04 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai04
|
||||
- **交付物**:⚠️ 由 ai04 自行补充
|
||||
- **依赖**:见 [contracts/student-bff_contract.md](../contracts/student-bff_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai04 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai04 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai04 自行补充
|
||||
@@ -0,0 +1,45 @@
|
||||
# student-portal 工作排期
|
||||
|
||||
> 负责人:ai14
|
||||
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/student-portal_contract.md](../contracts/student-portal_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
student-portal 是学生端微前端,通过 MF Remote 接入主应用,覆盖考试作答、作业提交等场景。全阶段目标:P2 MF Remote 骨架 → P3 考试作答+作业提交 → P4-P6 持续优化。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai14 student-portal 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2-P6
|
||||
[阶段任务] :a14a, 2026-07-10, Xd
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai14 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### 全阶段任务
|
||||
|
||||
- **负责人**:ai14
|
||||
- **交付物**:⚠️ 由 ai14 自行补充
|
||||
- **依赖**:见 [contracts/student-portal_contract.md](../contracts/student-portal_contract.md)
|
||||
- **验收标准**:⚠️ 由 ai14 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:⚠️ 由 ai14 自行补充(见 contract.md)
|
||||
- **我的就绪信号**:⚠️ 由 ai14 自行补充
|
||||
58
docs/architecture/issues/worklines/teacher-bff_workline.md
Normal file
58
docs/architecture/issues/worklines/teacher-bff_workline.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# teacher-bff 工作排期
|
||||
|
||||
> 负责人:ai03
|
||||
> 关联:[workline.md](../workline.md)、[coord.md §1 ARB-001](../coord.md)、[contracts/teacher-bff_contract.md](../contracts/teacher-bff_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
teacher-bff 是教学场景域聚合层,全阶段目标:P2 GraphQL schema 第一版 → P3 扩展 exams/homework/grades → P4 学情分析 → P5 通知+SSE → P6 admin 命名空间。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6,各 AI 自行细化)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai03 teacher-bff 全阶段排期
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2 GraphQL 基础
|
||||
schema第一版(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
|
||||
|
||||
section P3-P6 扩展
|
||||
exams/homework/grades Query :a3e, after a3d, 3d
|
||||
学情分析+通知+SSE :a3f, after a3e, 4d
|
||||
admin命名空间 :a3g, after a3f, 2d
|
||||
```
|
||||
|
||||
> **注意**:以上为 coord 初始规划,ai03 接管后必须自行细化为完整 P2-P6 排期。
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### P2:GraphQL schema + 5 Query + DataLoader + ActionState
|
||||
|
||||
- **负责人**:ai03
|
||||
- **交付物**:
|
||||
- `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 自行补充
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
- **我依赖**:iam gRPC 50052(ai06)+ core-edu gRPC 50053(ai08,P3+)
|
||||
- **我的就绪信号**:POST /graphql 可用 + dashboard Query 返回正确数据
|
||||
170
docs/architecture/issues/worklines/teacher-portal_workline.md
Normal file
170
docs/architecture/issues/worklines/teacher-portal_workline.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# teacher-portal 工作排期
|
||||
|
||||
> 负责人:ai13
|
||||
> 关联:[workline.md](../workline.md)、[coord.md §2 ARB-002](../coord.md)、[contracts/teacher-portal_contract.md](../contracts/teacher-portal_contract.md)
|
||||
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock,最后统一集成测试)
|
||||
> 依据:president-final-rulings.md §3.5(ai13 P2 功能范围)+ §7.13(ai13 工作内容最终清单)+ 03-long-term-architecture.md §1.1 阶段能力累积矩阵
|
||||
|
||||
---
|
||||
|
||||
## §1 总览
|
||||
|
||||
teacher-portal 是教学场景域微前端(MF Shell),全阶段目标:P2 MF Shell + GraphQL client + 基础页面 → P3 考试/作业/成绩 → P4 知识图谱/学情 → P5 推送/AI → P6 可观测性硬化。
|
||||
|
||||
**全并行模式**:ai13 一口气完成 P2-P6 全部代码,开发期间用 MSW mock 上游(teacher-bff GraphQL / api-gateway HTTP / push-gateway WebSocket),上游就绪后在 [matrix.md](../matrix.md) §8 更新就绪信号,最后统一集成测试。
|
||||
|
||||
---
|
||||
|
||||
## §2 全阶段甘特图(P2-P6)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title ai13 teacher-portal 全阶段排期(全并行)
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m-%d
|
||||
|
||||
section P2 基础
|
||||
packages骨架(ui-tokens/ui-components/hooks) :crit, a13p0, 2026-07-09, 1d
|
||||
MF Shell配置(NextFederationPlugin) :crit, a13a, after a13p0, 2d
|
||||
GraphQLProvider+urql client单例 :crit, a13b, after a13a, 2d
|
||||
AppShell+路由骨架+设计令牌三层 :a13c, after a13b, 2d
|
||||
登录页+权限上下文+Token存储 :a13d, after a13c, 2d
|
||||
Dashboard框架+班级列表+学生列表+个人设置 :a13e, after a13d, 3d
|
||||
|
||||
section P3 教学核心
|
||||
考试管理页面(列表/创建/详情) :a13f, after a13e, 3d
|
||||
作业管理页面(布置/批改) :a13g, after a13f, 3d
|
||||
成绩管理页面(录入/分析) :a13h, after a13g, 2d
|
||||
乐观更新+多Tab会话同步 :a13i, after a13h, 2d
|
||||
|
||||
section P4 知识与学情
|
||||
知识图谱可视化页面 :a13j, after a13i, 3d
|
||||
学情分析仪表盘(宽表5s) :a13k, after a13j, 3d
|
||||
parent-portal Remote接入 :a13l, after a13k, 1d
|
||||
|
||||
section P5 推送与AI
|
||||
WebSocket通知中心 :a13m, after a13l, 2d
|
||||
AI辅助出题(SSE流式) :a13n, after a13m, 3d
|
||||
AI生成教案/学情报告 :a13o, after a13n, 2d
|
||||
|
||||
section P6 硬化
|
||||
Sentry+RUM+OTel browser :a13p, after a13o, 2d
|
||||
A11y审计(WCAG 2.2 AA) :a13q, after a13p, 2d
|
||||
性能优化+bundle门禁 :a13r, after a13q, 2d
|
||||
localStorage→httpOnly Cookie迁移 :a13s, after a13r, 1d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §3 详细任务
|
||||
|
||||
### P2:MF Shell + GraphQL client + 基础页面
|
||||
|
||||
- **负责人**:ai13
|
||||
- **裁决依据**:总裁裁决 §3.5(ai13 P2 功能范围)+ F9(GraphQL P2 起)+ F12(localStorage token)+ §2.17(MF GraphQL client 单例方案 A)+ §2.18(packages 批次 0.15)
|
||||
- **交付物**:
|
||||
- ✅ packages 骨架(ui-tokens/ui-components/hooks,批次 0.15 已完成)
|
||||
- NextFederationPlugin 配置(exposes AppShell + shared react/urql/graphql singleton,无 remotes)
|
||||
- GraphQLProvider + urql client 单例(Shell 暴露,Remote 复用,§2.17 方案 A)
|
||||
- AppShell(Layout + 侧边栏 + 路由守卫 + ErrorBoundary + 设计令牌三层 primitive/semantic/tailwind-theme)
|
||||
- 登录页(POST /api/auth/login → JWT 存 localStorage,F12)
|
||||
- 权限上下文(usePermission + useViewports,权限点 `<RESOURCE>_<ACTION>` F7)
|
||||
- Dashboard 框架(teacherDashboard GraphQL query)
|
||||
- 班级列表页(myClasses GraphQL query,iam 数据)
|
||||
- 学生列表页(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)
|
||||
- **验收标准**:MF 配置不破坏单体 + GraphQL 拉取数据 + 登录→Dashboard→班级列表→学生列表链路通
|
||||
|
||||
### P3:考试/作业/成绩 + 乐观更新 + 多 Tab 同步
|
||||
|
||||
- **负责人**:ai13
|
||||
- **交付物**:
|
||||
- 考试管理页面(classExams list / createExam mutation / examDetail)
|
||||
- 作业管理页面(classHomework list / assignHomework mutation / homeworkDetail / 批改界面)
|
||||
- 成绩管理页面(studentGrades list / recordGrade mutation / 成绩分析图表)
|
||||
- 乐观更新(useMutation onMutate 回滚 + invalidateQueries,见 03 §10.1)
|
||||
- 多 Tab 会话同步(BroadcastChannel('edu-session'),见 03 §2.5)
|
||||
- **依赖**:teacher-bff 扩展 core-edu gRPC 调用(ai03 P3,core-edu ai08 P3 启用 gRPC)
|
||||
- **Mock 策略**:MSW 返回固定考试/作业/成绩数据
|
||||
- **验收标准**:教师创建考试→学生(student-portal Remote)作答→教师批改全链路(mock)+ 乐观更新回滚正确 + 多 Tab 登出同步
|
||||
|
||||
### P4:知识图谱 + 学情分析 + parent-portal Remote
|
||||
|
||||
- **负责人**:ai13
|
||||
- **交付物**:
|
||||
- 知识图谱可视化页面(recharts/d3 节点图,CSR + @next/dynamic 懒加载)
|
||||
- 学情分析仪表盘(宽表 5s 返回,ISR revalidate 60s + CSR 交互)
|
||||
- parent-portal Remote 接入(MF remotes 配置,feature flag NEXT_PUBLIC_MF_ENABLED)
|
||||
- **依赖**:teacher-bff 扩展 content/data-ana gRPC 调用(ai03 P4,content ai09 P4 / data-ana ai11 P4 启用 gRPC)+ parent-portal(ai15 P4)
|
||||
- **Mock 策略**:MSW 返回固定知识图谱 + 学情宽表数据
|
||||
- **验收标准**:教师查看知识图谱 + 学情宽表 5s 返回(mock)+ parent-portal Remote 在 Shell 内渲染
|
||||
|
||||
### P5:WebSocket 推送 + AI 辅助 + 通知中心
|
||||
|
||||
- **负责人**:ai13
|
||||
- **交付物**:
|
||||
- WebSocket 通知中心(ws://push-gateway:8081/ws,实时通知 + BroadcastChannel 跨 Tab)
|
||||
- AI 辅助出题(SSE 流式,generateQuestion subscription)
|
||||
- AI 生成教案/学情报告(generateLessonPlan / generateReport mutation)
|
||||
- **依赖**:push-gateway WebSocket(ai02 P5)+ ai 服务 gRPC(ai12 P5 启用 gRPC)+ msg 服务(ai10 P5)
|
||||
- **Mock 策略**:mock-socket 模拟 WS 推送 + MSW 返回固定 AI 响应(SSE 用 ReadableStream mock)
|
||||
- **验收标准**:全校广播实时到达(mock)+ AI 流式出题(mock)+ 通知中心实时更新
|
||||
|
||||
### P6:可观测性硬化 + A11y + 性能 + Token 迁移
|
||||
|
||||
- **负责人**:ai13
|
||||
- **交付物**:
|
||||
- Sentry 错误追踪 + Session Replay(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 门禁:Shell <150KB / Remote <80KB / CSS <50KB)
|
||||
- localStorage → httpOnly Cookie 迁移(iam refresh cookie 端点就绪后 + CSRF 防护)
|
||||
- **依赖**:全链路可观测就绪(iam refresh cookie + Sentry DSN + OTel collector)
|
||||
- **验收标准**:99.9% 可用 + WCAG 2.2 AA + LCP <2.5s / INP <200ms / CLS <0.1(P75)
|
||||
|
||||
---
|
||||
|
||||
## §4 依赖与就绪信号
|
||||
|
||||
### §4.1 我依赖的上游就绪标志
|
||||
|
||||
| 上游 | 就绪标志 | 阻塞阶段 | 状态 |
|
||||
| --------------------------------------------------------- | ------------------------------------------------- | -------- | ---------------------- |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| ai 服务 gRPC(ai12 P5) | generateQuestion/generateLessonPlan 可调 | P5 启动 | ⏳ 待 ai12 P5 |
|
||||
| iam refresh cookie 端点(ai06 P6) | POST /iam/auth/refresh 返回 httpOnly cookie | P6 启动 | ⏳ 待 ai06 P6 |
|
||||
|
||||
### §4.2 我的就绪信号(供下游消费)
|
||||
|
||||
| 信号 | 就绪标志 | 消费方 |
|
||||
| ------------------------------- | -------------------------------------- | ---------------------------------- |
|
||||
| teacher-portal dev server :4000 | next dev -p 4000 可访问 | 无(最前端) |
|
||||
| MF Shell 可加载 | 首页渲染 AppShell + 导航 | student/parent/admin Remote(P3+) |
|
||||
| 登录流程可用 | POST /api/auth/login → JWT → Dashboard | 无(最前端) |
|
||||
| GraphQL 查询可执行 | currentUser/myClasses 返回数据 | 无(最前端) |
|
||||
| parent-portal Remote 接入点 | MF remotes 配置就绪 | parent-portal(ai15 P4) |
|
||||
|
||||
---
|
||||
|
||||
## §5 全并行开发说明
|
||||
|
||||
按 [matrix.md](../matrix.md) §全并行模式:
|
||||
|
||||
1. ai13 一口气完成 P2-P6 全部代码,开发期间用 MSW mock 上游
|
||||
2. 上游就绪后在 matrix.md §8 更新就绪信号
|
||||
3. 所有模块就绪后统一集成测试(matrix.md §9 检查清单)
|
||||
4. Mock 切换:NEXT_PUBLIC_API_MOCKING=enabled → disabled
|
||||
|
||||
---
|
||||
|
||||
**AI Agent**: ai13(teacher-portal)
|
||||
**Branch**: feat/teacher-portal-workline-ai13
|
||||
**Coordinator**: coord-ai
|
||||
1240
docs/architecture/president-final-rulings.md
Normal file
1240
docs/architecture/president-final-rulings.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2205,7 +2205,11 @@
|
||||
|
||||
---
|
||||
|
||||
## 3.6 ai16 复审批次(2026-07-09)
|
||||
> **ai16 复审批次记录已迁移**:见 [issues/objections/admin-portal_issue.md](./architecture/issues/objections/admin-portal_issue.md)(2026-07-09 设计文档重构,issues.md 已归档)
|
||||
|
||||
---
|
||||
|
||||
## 2.9 ai02 补充的待仲裁问题(ISSUE-053 ~ ISSUE-058)
|
||||
|
||||
**复审范围**:coord-final-decisions.md(前端 4 portal F1-F12 + 通用 G14/G16 + X2/X3/X6 待 P6+ 事项 + §7 各 AI 回写义务)+ ai-work-orchestration.md(批次 5 P6 ai16 工作范围)+ issues.md v1.5(含 ai16 新增 5 个问题 ISSUE-044~048)+ ai16 自身 `apps/admin-portal/docs/02-architecture-design.md`(§4.1 数据获取路径 / §6.1 权限表 27 路由 / §6.2 错误码清单 / §12.4 错误码前缀表 / §18 coord 仲裁清单 9 子项)
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# AI 协作文档体系重构设计
|
||||
|
||||
> 日期:2026-07-09
|
||||
> 状态:已批准,已实现
|
||||
> 关联:[president-final-rulings.md](../../architecture/president-final-rulings.md)、[ai-work-orchestration.md](../../architecture/ai-work-orchestration.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
### 1.1 原始问题
|
||||
|
||||
Edu 项目有 16 个 AI 协作开发微服务架构。此前的协作文档存在两个核心问题:
|
||||
|
||||
1. **issues.md 单文件多 AI 写入冲突**:70+ 问题由多个 AI 追加到同一文件,导致编号严重冲突(ISSUE-024/025/028~~031/032~~036/044~048 多处重复),文件结构混乱(3 个 `## 2.` 标题,章节标题与 issue 标题合并)。
|
||||
|
||||
2. **coord 集中规划批次不符合全并行需求**:原模式按 6 批次串行推进(批次 0→1→2→...→5),各 AI 等待批次信号才能开始。用户决定改为**全并行模式**:各 AI 一口气完成自己模块从 P2 到 P6 的所有代码,开发期间用 mock,最后统一集成测试。
|
||||
|
||||
### 1.2 用户需求
|
||||
|
||||
- 模块问题和模块 workline 分子文件夹存放
|
||||
- coord 的 coord.md 和 workline.md 位置不变(留根目录)
|
||||
- workline 由各模块 AI 自己详细规划(不是 coord 规划)
|
||||
- 需要一份上下游对接契约文档,支持全并行开发期间用 mock
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计方案
|
||||
|
||||
### 2.1 目录结构
|
||||
|
||||
```
|
||||
docs/architecture/issues/
|
||||
├── coord.md # coord 仲裁汇总(不变)
|
||||
├── workline.md # coord 总工作安排(不变)
|
||||
├── matrix.md # 新增:上下游对接总矩阵
|
||||
├── objections/ # 各模块问题/异议(AI 提请,coord 仲裁)
|
||||
│ ├── [模块名]_issue.md # 15 个模块各一个
|
||||
│ └── cross_issue.md # 跨模块问题
|
||||
├── worklines/ # 各模块工作排期(AI 自己写,覆盖 P2-P6 全阶段)
|
||||
│ └── [模块名]_workline.md # 15 个模块各一个
|
||||
└── contracts/ # 各模块对接契约(我提供什么 + 我消费什么 + mock 策略)
|
||||
└── [模块名]_contract.md # 15 个模块各一个
|
||||
```
|
||||
|
||||
**命名冲突解决**:外层文件夹保持 `issues/`(coord.md/workline.md 位置不变),内层"模块问题"子文件夹改名为 `objections/`(语义更准确:AI 提请的异议),避免 `issues/issues/` 路径。
|
||||
|
||||
### 2.2 三类文档职责
|
||||
|
||||
| 文档类型 | 子文件夹 | 维护方 | 内容 |
|
||||
| ---------------- | ------------- | ---------------------- | ---------------------------------------------- |
|
||||
| issue(异议) | `objections/` | 各 AI 提请,coord 仲裁 | 遇到问题时追加条目,coord 仲裁后更新状态 |
|
||||
| workline(排期) | `worklines/` | 各 AI 自己写 | 全阶段甘特图 + 详细任务 + 依赖与就绪信号 |
|
||||
| contract(契约) | `contracts/` | 各 AI 维护,coord 汇总 | 我提供什么 + 我消费什么 + mock 策略 + 就绪信号 |
|
||||
|
||||
**coord 根目录文件**:
|
||||
|
||||
- `coord.md`:coord 仲裁结果汇总(各 AI 提请异议后,coord 在此写入仲裁结论)
|
||||
- `workline.md`:coord 总工作安排(16 AI 甘特图 + 批次概览 + 各 AI 工作清单链接)
|
||||
- `matrix.md`:coord 汇总的上下游对接总矩阵(服务依赖图 + gRPC/GraphQL/Kafka/HTTP 矩阵 + mock 策略 + 就绪信号跟踪)
|
||||
|
||||
### 2.3 全并行开发模式
|
||||
|
||||
**核心原则**:各 AI 一口气完成自己模块从 P2 到 P6 的所有代码,开发期间用 mock,最后统一集成测试。
|
||||
|
||||
**工作流**:
|
||||
|
||||
1. 各 AI 读取自己的 `contract.md` 了解上下游接口
|
||||
2. 各 AI 在 `workline.md` 详细规划全阶段任务
|
||||
3. 开发期间消费上游的 mock(见 contract.md §4 mock 策略)
|
||||
4. 上游就绪后在 `matrix.md` §8 更新就绪信号
|
||||
5. 所有模块就绪后统一集成测试(见 matrix.md §9 检查清单)
|
||||
|
||||
**Mock 策略**(见 matrix.md §7):
|
||||
|
||||
- gRPC 调用:grpc-mock 拦截 + 固定 JSON
|
||||
- GraphQL 调用:MSW 拦截 + 固定 response
|
||||
- Kafka 事件:本地 Kafka mock producer
|
||||
- HTTP 调用:MSW / fetch mock
|
||||
|
||||
### 2.4 contract.md 模板结构
|
||||
|
||||
```markdown
|
||||
# [模块名] 对接契约
|
||||
|
||||
## §1 我提供什么(对外接口)
|
||||
|
||||
- 1.1 gRPC 接口(如有)
|
||||
- 1.2 HTTP 端点(如有)
|
||||
- 1.3 GraphQL schema(如 BFF)
|
||||
- 1.4 Kafka 事件发布(如有)
|
||||
- 1.5 错误码前缀
|
||||
|
||||
## §2 我消费什么(依赖上游)
|
||||
|
||||
- 2.1 gRPC 调用(同步)+ mock 策略
|
||||
- 2.2 Kafka 事件订阅(异步)+ mock 策略
|
||||
- 2.3 HTTP 调用(如有)+ mock 策略
|
||||
|
||||
## §3 就绪信号
|
||||
|
||||
- 3.1 我依赖的上游就绪标志
|
||||
- 3.2 我的就绪标志(供下游消费)
|
||||
|
||||
## §4 Mock 策略
|
||||
|
||||
- 4.1 我提供的 mock
|
||||
- 4.2 我消费的 mock
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 实现清单
|
||||
|
||||
### 3.1 文件创建(共 48 个文件)
|
||||
|
||||
| 类型 | 数量 | 位置 |
|
||||
| ----------- | -------------------------- | ----------- |
|
||||
| contract.md | 15 | contracts/ |
|
||||
| workline.md | 15(4 个已有 + 11 个新建) | worklines/ |
|
||||
| issue.md | 16(15 模块 + 1 跨模块) | objections/ |
|
||||
| matrix.md | 1 | 根目录 |
|
||||
| **合计** | 47 + matrix.md = 48 | |
|
||||
|
||||
### 3.2 文件迁移
|
||||
|
||||
- 旧 `issues/[模块名]_workline.md`(4 个)→ 迁移到 `worklines/` 子文件夹,更新路径引用
|
||||
- 旧文件删除
|
||||
|
||||
### 3.3 文档更新
|
||||
|
||||
- `coord.md` §3:更新仲裁流程指向 `objections/` 子文件夹
|
||||
- `workline.md`:更新维护规则指向 `worklines/` 子文件夹 + 全并行模式说明
|
||||
- `issues.md`(旧):添加迁移说明指向新流程
|
||||
- `ai-work-orchestration.md` §9.2:指向新流程
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- [ ] `docs/architecture/issues/` 下有 3 个子文件夹(objections/ worklines/ contracts/)
|
||||
- [ ] 每个子文件夹有 15-16 个 `.md` 文件
|
||||
- [ ] `coord.md` 和 `workline.md` 在根目录
|
||||
- [ ] `matrix.md` 在根目录
|
||||
- [ ] 所有路径引用正确(`../coord.md` 而非 `./coord.md`)
|
||||
- [ ] 旧 `issues/[模块名]_workline.md` 文件已删除
|
||||
- [ ] `issues.md` 顶部有迁移说明
|
||||
132
infra/port-allocation.md
Normal file
132
infra/port-allocation.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Edu 全局端口分配表
|
||||
|
||||
> 版本:1.0
|
||||
> 日期:2026-07-09
|
||||
> 维护者:coord(AI 协调者)
|
||||
> 关联文档:[004 架构影响地图 §1.2](../docs/architecture/004_architecture_impact_map.md)、[full-stack-runbook](../docs/standards/full-stack-runbook.md)
|
||||
>
|
||||
> 本文件是 Edu 项目所有服务端口的**唯一源**。任何端口新增/变更必须由 coord 同步更新此文件 + 004 §1.2。
|
||||
|
||||
---
|
||||
|
||||
## 1. 端口分配原则
|
||||
|
||||
1. **不冲突**:同一端口不可被两个服务占用(含开发/生产环境)
|
||||
2. **段位划分**:
|
||||
- `8080-8089`:Go 网关层(api-gateway / push-gateway)
|
||||
- `3000-3099`:NestJS BFF + 业务服务(HTTP)
|
||||
- `4000-4009`:Next.js 微前端 portal(dev server / 生产端口)
|
||||
- `50051-50099`:gRPC 服务端口
|
||||
- `9090-9099`:基础设施(Prometheus / Grafana / Alertmanager)
|
||||
- `9100-9199`:Exporter(node/mysqld/redis)
|
||||
3. **dev/prod 一致**:开发与生产环境使用相同端口,避免环境差异
|
||||
4. **占用即登记**:新增服务必须先在此表登记端口,再写代码
|
||||
|
||||
---
|
||||
|
||||
## 2. Go 网关层(8080-8089)
|
||||
|
||||
| 服务 | HTTP 端口 | gRPC 端口 | 说明 | 阶段 |
|
||||
| ------------ | --------- | --------- | ------------------------------------------------------------------- | ----- |
|
||||
| api-gateway | 8080 | — | 纯 HTTP 反向代理,不做 HTTP↔gRPC 转换 | P1 ✅ |
|
||||
| push-gateway | 8081 | — | WebSocket + HTTP /internal/*;**coord 仲裁豁免 gRPC**(2026-07-09) | P5 |
|
||||
|
||||
> **push-gateway gRPC 豁免说明**:ai-allocation §5 原要求 push-gateway 实现 gRPC PushService。coord 仲裁豁免,理由:(1) push-gateway 是基础设施层无流式推送场景;(2) 现有 HTTP /internal/push 已落地;(3) msg/student-bff 调用方改用 HTTP。50058 端口让给 ai 服务。
|
||||
|
||||
---
|
||||
|
||||
## 3. NestJS BFF + 业务服务(3000-3099)
|
||||
|
||||
| 服务 | HTTP 端口 | gRPC 端口 | 说明 | 阶段 |
|
||||
| ----------- | --------- | --------- | ----------------------------------------------- | ------- |
|
||||
| iam | 3002 | 50052 | P2 启用 gRPC server(I1 裁决,2026-07-09 修正) | P2 ✅ |
|
||||
| teacher-bff | 3003 | — | BFF 不暴露 gRPC,对下游走 gRPC | P2 ✅ |
|
||||
| core-edu | 3004 | 50053 | 含原 classes 服务(合并后) | P3 |
|
||||
| content | 3005 | 50054 | Neo4j + ES | P4 |
|
||||
| data-ana | 3006 | 50055 | HTTP 保留作 Gateway 直连降级,gRPC 为 P4 主入口 | P4 |
|
||||
| msg | 3007 | 50056 | 通知中心 | P5 |
|
||||
| ai | 3008 | 50058 | LLM 网关 | P5 |
|
||||
| student-bff | 3009 | — | BFF 不暴露 gRPC | P3 |
|
||||
| parent-bff | 3010 | — | BFF 不暴露 gRPC | P4 |
|
||||
| ~~classes~~ | ~~3001~~ | ~~—~~ | 已合并入 core-edu(P3) | P1 历史 |
|
||||
|
||||
> **端口空闲**:3000、3011-3099 预留扩展。
|
||||
|
||||
---
|
||||
|
||||
## 4. Next.js 微前端 portal(4000-4009)
|
||||
|
||||
| Portal | dev 端口 | 生产端口 | MF 角色 | 阶段 |
|
||||
| -------------- | -------- | -------- | ---------- | ----- |
|
||||
| teacher-portal | 4000 | 4000 | Shell 宿主 | P2 ✅ |
|
||||
| student-portal | 4001 | 4001 | Remote | P3 |
|
||||
| parent-portal | 4002 | 4002 | Remote | P4 |
|
||||
| admin-portal | 4003 | 4003 | Remote | P6 |
|
||||
|
||||
> **coord 仲裁**(2026-07-09):
|
||||
>
|
||||
> - admin-portal §12.1 表格原写 3003(与 teacher-bff 冲突),统一修正为 **4003**
|
||||
> - admin-portal MF 配置 `remotes.teacher` 原写 `http://localhost:3000`,修正为 `http://localhost:4000/teacher/remoteEntry.js`
|
||||
> - 4 端 dev/prod 端口一致,避免环境差异
|
||||
|
||||
---
|
||||
|
||||
## 5. gRPC 服务端口(50051-50099)
|
||||
|
||||
| 服务 | gRPC 端口 | 暴露 Service | 阶段 |
|
||||
| ---------------- | --------- | --------------------------------------------------------------------------------- | ------------- |
|
||||
| iam | 50052 | IamService(P2 启用,I1 裁决) | P2 |
|
||||
| core-edu | 50053 | ExamService / HomeworkService / GradeService / AttendanceService / ClassService | P3 |
|
||||
| content | 50054 | TextbookService / ChapterService / KnowledgeGraphService / QuestionService | P4 |
|
||||
| data-ana | 50055 | AnalyticsService | P4 |
|
||||
| msg | 50056 | NotificationService / NotificationPreferenceService / NotificationTemplateService | P5 |
|
||||
| ai | 50058 | AiService | P5 |
|
||||
| ~~push-gateway~~ | ~~50057~~ | ~~PushService~~ | 豁免(见 §2) |
|
||||
|
||||
> **端口空闲**:50051、50057、50059-50099 预留扩展。
|
||||
> **push-gateway 50057**:原计划预留,因 gRPC 豁免释放。50058 让给 ai 服务。
|
||||
|
||||
---
|
||||
|
||||
## 6. 基础设施端口(9080-9199)
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
| ---------------- | --------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| MySQL | 3306 | 业务数据库(external) |
|
||||
| Redis | 6379 | 缓存 + 会话 + Pub/Sub |
|
||||
| Kafka | 9092(OUTSIDE)/ 29092(INSIDE) | 双监听器 |
|
||||
| Kafka UI | 9000 | kafka-ui 容器 |
|
||||
| Zookeeper | 2181 | Kafka 协调 |
|
||||
| Debezium Connect | 8083 | CDC connector |
|
||||
| ClickHouse | 8123(HTTP)/ 9000(TCP) | 数据分析宽表 |
|
||||
| Neo4j | 7474(HTTP)/ 7687(Bolt) | 知识图谱 |
|
||||
| Elasticsearch | 9200(HTTP)/ 9300(TCP) | 全文检索 |
|
||||
| Prometheus | 9090 | 指标采集(--web.enable-lifecycle) |
|
||||
| Grafana | 3000(容器内,**与 teacher-bff 3003 不冲突,因 teacher-bff 在 host 网络**) | — | 实际部署需注意 |
|
||||
| Alertmanager | 9093 | 告警 |
|
||||
| Jaeger | 16686(UI)/ 4317(OTLP gRPC)/ 4318(OTLP HTTP) | 链路追踪 |
|
||||
| node-exporter | 9100 | 主机指标(host.docker.internal:9100) |
|
||||
| mysqld-exporter | 9104 | MySQL 指标 |
|
||||
| redis-exporter | 9121 | Redis 指标 |
|
||||
|
||||
> **Grafana 端口冲突风险**:Grafana 默认 3000,与 teacher-bff 3003 不冲突(不同主机层),但开发环境若同主机部署需注意。建议 Grafana 改用 3030 避免混淆。
|
||||
|
||||
---
|
||||
|
||||
## 7. 变更记录
|
||||
|
||||
| 日期 | 变更 | 决策者 |
|
||||
| ---------- | ------------------------------------------------------ | ------ |
|
||||
| 2026-07-09 | 初始创建,登记全部 15 服务 + 基础设施端口 | coord |
|
||||
| 2026-07-09 | 仲裁 admin-portal 3003 → 4003;MF 配置 3000 → 4000 | coord |
|
||||
| 2026-07-09 | 仲裁 push-gateway 豁免 gRPC,释放 50057;50058 让给 ai | coord |
|
||||
| 2026-07-09 | classes 3001 标记为历史(已合并入 core-edu) | coord |
|
||||
|
||||
---
|
||||
|
||||
## 8. 待办
|
||||
|
||||
- [ ] P3 启用前:iam gRPC 50052 / core-edu gRPC 50053 必须落地
|
||||
- [ ] P4 启用前:content gRPC 50054 / data-ana gRPC 50055 必须落地
|
||||
- [ ] P5 启用前:msg gRPC 50056 / ai gRPC 50058 必须落地
|
||||
- [ ] Grafana 端口考虑改为 3030 避免与 teacher-bff 混淆
|
||||
@@ -3,6 +3,7 @@ module.exports = {
|
||||
// Go 工具链不在 git hook PATH 中,Go 文件格式化由 go fmt 手动执行
|
||||
// golangci-lint 安装后恢复:['gofmt -w', 'golangci-lint run --fix']
|
||||
'*.py': ['ruff check --fix', 'ruff format'],
|
||||
'*.proto': ['buf format --write'],
|
||||
// buf format 一次只接受 1 个文件参数,需逐个调用
|
||||
'*.proto': (files) => files.map((file) => `buf format --write "${file}"`),
|
||||
'*.md': ['prettier --write'],
|
||||
};
|
||||
|
||||
24
packages/hooks/package.json
Normal file
24
packages/hooks/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@edu/hooks",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edu/ui-components": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
62
packages/hooks/src/index.ts
Normal file
62
packages/hooks/src/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @edu/hooks - 共享 Hooks 库
|
||||
*
|
||||
* 维护者:ai13(teacher-portal)
|
||||
* 关联文档:teacher-portal 02-architecture-design.md §8
|
||||
*
|
||||
* 包含:
|
||||
* - useAuth:会话状态(user/token/login/logout/refresh)
|
||||
* - usePermission:权限查询(hasPermission/hasAny/hasAll + dataScope)
|
||||
* - useViewports:视口列表(按 scope 过滤)
|
||||
* - useApi:ApiClient 实例(注入 token + 401 处理 + trace_id)
|
||||
* - useA11yId:唯一 ARIA ID 生成
|
||||
* - useAriaLive:aria-live 区域管理
|
||||
* - useToast:全局 toast 通知
|
||||
* - useTraceId:当前会话 trace_id
|
||||
*
|
||||
* 设计原则:
|
||||
* - hooks 不直接调 API,保持纯客户端逻辑(API 调用由 apps/* 注入)
|
||||
* - 权限/视口数据通过 props 注入,避免 hooks 包耦合 iam
|
||||
* - token 存储使用 localStorage(F12 裁决,P2 阶段)
|
||||
*/
|
||||
|
||||
export { useAuth } from "./use-auth.js";
|
||||
export type { UseAuthReturn } from "./use-auth.js";
|
||||
|
||||
export { usePermission } from "./use-permission.js";
|
||||
export type {
|
||||
UsePermissionProps,
|
||||
UsePermissionReturn,
|
||||
} from "./use-permission.js";
|
||||
|
||||
export { useViewports } from "./use-viewports.js";
|
||||
export type { UseViewportsProps, UseViewportsReturn } from "./use-viewports.js";
|
||||
|
||||
export { useApi } from "./use-api.js";
|
||||
export type { UseApiProps, UseApiReturn } from "./use-api.js";
|
||||
|
||||
export {
|
||||
useA11yId,
|
||||
useA11yIds,
|
||||
mergeA11yProps,
|
||||
describeInput,
|
||||
} from "./use-a11y-id.js";
|
||||
|
||||
export { useAriaLive } from "./use-aria-live.js";
|
||||
export type { UseAriaLiveReturn, AriaLivePoliteness } from "./use-aria-live.js";
|
||||
|
||||
export { useToast } from "./use-toast.js";
|
||||
export type { UseToastReturn } from "./use-toast.js";
|
||||
|
||||
export { useTraceId } from "./use-trace-id.js";
|
||||
export type { UseTraceIdReturn } from "./use-trace-id.js";
|
||||
|
||||
// 共享类型
|
||||
export type {
|
||||
UserSession,
|
||||
DataScope,
|
||||
PermissionContext,
|
||||
Viewport,
|
||||
ToastMessage,
|
||||
AuthState,
|
||||
} from "./types.js";
|
||||
68
packages/hooks/src/types.ts
Normal file
68
packages/hooks/src/types.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 共享类型定义(hooks 包内部使用)
|
||||
*
|
||||
* 维护者:ai13(teacher-portal)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 用户会话信息
|
||||
*/
|
||||
export interface UserSession {
|
||||
userId: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
avatarUrl?: string;
|
||||
roles: string[];
|
||||
/** 数据范围(DataScope 6 级) */
|
||||
dataScope?: DataScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据范围(对齐 iam DataScope 6 级)
|
||||
*/
|
||||
export type DataScope =
|
||||
"ALL" | "SCHOOL" | "GRADE" | "CLASS" | "SUBJECT" | "SELF";
|
||||
|
||||
/**
|
||||
* 权限点检查结果
|
||||
*/
|
||||
export interface PermissionContext {
|
||||
/** 当前用户拥有的权限点列表 */
|
||||
permissions: string[];
|
||||
/** 当前用户的数据范围 */
|
||||
dataScope: DataScope;
|
||||
/** 当前用户的角色列表 */
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 视口(Viewport)- iam 4 层视口模型
|
||||
*/
|
||||
export interface Viewport {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "admin" | "teacher" | "student" | "parent";
|
||||
scope: DataScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知消息(Toast)
|
||||
*/
|
||||
export interface ToastMessage {
|
||||
id: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
title: string;
|
||||
description?: string;
|
||||
/** 自动关闭时长(ms),0 表示不自动关闭 */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 鉴权状态
|
||||
*/
|
||||
export interface AuthState {
|
||||
user: UserSession | null;
|
||||
token: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
119
packages/hooks/src/use-a11y-id.ts
Normal file
119
packages/hooks/src/use-a11y-id.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
/**
|
||||
* useA11yId - 唯一 ARIA ID 生成器
|
||||
*
|
||||
* 用途:为表单元素生成 aria-labelledby / aria-describedby 关联的唯一 ID。
|
||||
* 迁移自 CICD 项目 A11y 工具集。
|
||||
*
|
||||
* @example
|
||||
* const id = useA11yId("email-input");
|
||||
* <input id={id} aria-describedby={`${id}-error`} />
|
||||
* <span id={`${id}-error`}>{errorMessage}</span>
|
||||
*/
|
||||
|
||||
let idCounter = 0;
|
||||
|
||||
/**
|
||||
* 生成唯一 ID(带可选前缀)。
|
||||
* 使用计数器 + 随机数,避免 SSR/CSR hydration mismatch。
|
||||
*/
|
||||
export function useA11yId(prefix?: string): string {
|
||||
const [id] = useState(() => {
|
||||
idCounter += 1;
|
||||
const random = Math.random().toString(36).slice(2, 8);
|
||||
const base = prefix
|
||||
? `${prefix}-${idCounter}-${random}`
|
||||
: `a11y-${idCounter}-${random}`;
|
||||
return base;
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成关联 ID(用于表单 input + label + error + description 关联)
|
||||
*
|
||||
* @example
|
||||
* const ids = useA11yIds("email");
|
||||
* // ids = { input: "email-input-1-xxx", label: "email-label-1-xxx", error: "email-error-1-xxx", description: "email-description-1-xxx" }
|
||||
*/
|
||||
export function useA11yIds(prefix: string): {
|
||||
input: string;
|
||||
label: string;
|
||||
error: string;
|
||||
description: string;
|
||||
} {
|
||||
const inputId = useA11yId(`${prefix}-input`);
|
||||
const labelId = useA11yId(`${prefix}-label`);
|
||||
const errorId = useA11yId(`${prefix}-error`);
|
||||
const descriptionId = useA11yId(`${prefix}-description`);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
input: inputId,
|
||||
label: labelId,
|
||||
error: errorId,
|
||||
description: descriptionId,
|
||||
}),
|
||||
[inputId, labelId, errorId, descriptionId],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 ARIA 属性(覆盖优先级:后者覆盖前者)
|
||||
*
|
||||
* @example
|
||||
* mergeA11yProps({ "aria-label": "默认" }, { "aria-label": "自定义" })
|
||||
* // => { "aria-label": "自定义" }
|
||||
*/
|
||||
export function mergeA11yProps(
|
||||
...props: Array<Record<string, unknown> | undefined>
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const prop of props) {
|
||||
if (prop) {
|
||||
Object.assign(result, prop);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 描述输入框的 ARIA 属性
|
||||
*
|
||||
* @example
|
||||
* const a11y = describeInput({ label: "邮箱", required: true, error: "邮箱格式错误", description: "请输入工作邮箱" });
|
||||
* <input {...a11y} />
|
||||
*/
|
||||
export function describeInput(options: {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
description?: string;
|
||||
invalid?: boolean;
|
||||
}): Record<string, unknown> {
|
||||
const { label, required, error, description, invalid } = options;
|
||||
const props: Record<string, unknown> = {
|
||||
"aria-label": label,
|
||||
};
|
||||
|
||||
if (required) {
|
||||
props["aria-required"] = true;
|
||||
}
|
||||
|
||||
if (invalid || error) {
|
||||
props["aria-invalid"] = true;
|
||||
}
|
||||
|
||||
// aria-describedby 由调用方拼接(需关联 error/description 的 ID)
|
||||
const describedBy: string[] = [];
|
||||
if (description) describedBy.push("description");
|
||||
if (error) describedBy.push("error");
|
||||
|
||||
if (describedBy.length > 0) {
|
||||
props["aria-describedby"] = describedBy.join(" ");
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
90
packages/hooks/src/use-api.ts
Normal file
90
packages/hooks/src/use-api.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
/**
|
||||
* useApi - ApiClient 实例管理
|
||||
*
|
||||
* 用途:注入 token + 401 处理 + trace_id 透传。
|
||||
* P2 阶段:GraphQL client(urql/apollo)注入;本 hook 提供统一 fetcher 封装。
|
||||
*
|
||||
* 注意:本 hook 不直接创建 GraphQL client(由 Shell 的 GraphQLProvider 注入)。
|
||||
* 本 hook 提供"API 调用层"封装,用于非 GraphQL 场景(如文件上传 / SSE 流)。
|
||||
*
|
||||
* @example
|
||||
* const { fetchJson, fetchWithAuth } = useApi({ token, traceId });
|
||||
* const data = await fetchJson("/api/v1/upload", { method: "POST", body: formData });
|
||||
*/
|
||||
|
||||
export interface UseApiProps {
|
||||
/** 认证 token(从 useAuth 获取) */
|
||||
token: string | null;
|
||||
/** trace_id(从 useTraceId 获取) */
|
||||
traceId?: string | null;
|
||||
/** 401 回调(通常跳转登录页) */
|
||||
onUnauthorized?: () => void;
|
||||
}
|
||||
|
||||
export interface UseApiReturn {
|
||||
/** 带 auth + trace_id 的 fetch 封装 */
|
||||
fetchWithAuth: (url: string, init?: RequestInit) => Promise<Response>;
|
||||
/** fetchJson - 解析 JSON 响应 */
|
||||
fetchJson: <T>(url: string, init?: RequestInit) => Promise<T>;
|
||||
/** 构造带 auth 的 headers */
|
||||
authHeaders: Record<string, string>;
|
||||
}
|
||||
|
||||
export function useApi({
|
||||
token,
|
||||
traceId,
|
||||
onUnauthorized,
|
||||
}: UseApiProps): UseApiReturn {
|
||||
const authHeaders = useMemo(() => {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
if (traceId) {
|
||||
headers["X-Trace-Id"] = traceId;
|
||||
}
|
||||
return headers;
|
||||
}, [token, traceId]);
|
||||
|
||||
const fetchWithAuth = useCallback(
|
||||
async (url: string, init?: RequestInit): Promise<Response> => {
|
||||
const mergedInit: RequestInit = {
|
||||
...init,
|
||||
headers: {
|
||||
...authHeaders,
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(url, mergedInit);
|
||||
|
||||
if (response.status === 401) {
|
||||
onUnauthorized?.();
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
[authHeaders, onUnauthorized],
|
||||
);
|
||||
|
||||
const fetchJson = useCallback(
|
||||
async <T>(url: string, init?: RequestInit): Promise<T> => {
|
||||
const response = await fetchWithAuth(url, init);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
},
|
||||
[fetchWithAuth],
|
||||
);
|
||||
|
||||
return {
|
||||
fetchWithAuth,
|
||||
fetchJson,
|
||||
authHeaders,
|
||||
};
|
||||
}
|
||||
52
packages/hooks/src/use-aria-live.ts
Normal file
52
packages/hooks/src/use-aria-live.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* useAriaLive - aria-live 区域管理
|
||||
*
|
||||
* 用途:动态向 aria-live 区域推送消息,供屏幕阅读器播报。
|
||||
* 迁移自 CICD 项目 A11y 工具集。
|
||||
*
|
||||
* @example
|
||||
* const { message, announce } = useAriaLive();
|
||||
* announce("表单提交成功", "polite");
|
||||
* // 渲染:<div aria-live="polite">{message}</div>
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export type AriaLivePoliteness = "polite" | "assertive" | "off";
|
||||
|
||||
export interface UseAriaLiveReturn {
|
||||
/** 当前消息内容 */
|
||||
message: string;
|
||||
/** 当前 politeness 级别 */
|
||||
politeness: AriaLivePoliteness;
|
||||
/** 推送消息 */
|
||||
announce: (message: string, politeness?: AriaLivePoliteness) => void;
|
||||
/** 清除消息 */
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export function useAriaLive(): UseAriaLiveReturn {
|
||||
const [message, setMessage] = useState("");
|
||||
const [politeness, setPoliteness] = useState<AriaLivePoliteness>("polite");
|
||||
|
||||
const announce = useCallback(
|
||||
(msg: string, level: AriaLivePoliteness = "polite"): void => {
|
||||
setPoliteness(level);
|
||||
// 先清空再设置,确保相同消息也能触发播报
|
||||
setMessage("");
|
||||
requestAnimationFrame(() => setMessage(msg));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const clear = useCallback((): void => {
|
||||
setMessage("");
|
||||
}, []);
|
||||
|
||||
return {
|
||||
message,
|
||||
politeness,
|
||||
announce,
|
||||
clear,
|
||||
};
|
||||
}
|
||||
93
packages/hooks/src/use-auth.ts
Normal file
93
packages/hooks/src/use-auth.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { AuthState, UserSession } from "./types.js";
|
||||
|
||||
/**
|
||||
* useAuth - 会话状态管理
|
||||
*
|
||||
* 用途:管理用户登录状态、token、用户信息。
|
||||
* P2 阶段:token 存储在 localStorage(F12 裁决),P6 评估迁移 httpOnly Cookie。
|
||||
*
|
||||
* 注意:本 hook 提供"状态管理"骨架,具体的登录/登出 API 调用
|
||||
* 由 apps/* 注入(通过 AuthProvider Context 或 props 传入 fetcher)。
|
||||
*
|
||||
* @example
|
||||
* const { user, isAuthenticated, login, logout } = useAuth();
|
||||
*/
|
||||
|
||||
const TOKEN_KEY = "edu_auth_token";
|
||||
const USER_KEY = "edu_auth_user";
|
||||
|
||||
export interface UseAuthReturn extends AuthState {
|
||||
/** 登录(由调用方注入 fetcher) */
|
||||
login: (token: string, user: UserSession) => void;
|
||||
/** 登出 */
|
||||
logout: () => void;
|
||||
/** 刷新用户信息 */
|
||||
refreshUser: (user: UserSession) => void;
|
||||
}
|
||||
|
||||
export function useAuth(): UseAuthReturn {
|
||||
const [state, setState] = useState<AuthState>({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
// 初始化:从 localStorage 读取 token + user(F12 裁决 P2 用 localStorage)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const userJson = localStorage.getItem(USER_KEY);
|
||||
const user = userJson ? (JSON.parse(userJson) as UserSession) : null;
|
||||
|
||||
setState({
|
||||
user,
|
||||
token,
|
||||
isAuthenticated: Boolean(token && user),
|
||||
isLoading: false,
|
||||
});
|
||||
} catch {
|
||||
setState({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = useCallback((token: string, user: UserSession): void => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
setState({
|
||||
user,
|
||||
token,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const logout = useCallback((): void => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
setState({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const refreshUser = useCallback((user: UserSession): void => {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
setState((prev) => ({ ...prev, user }));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
login,
|
||||
logout,
|
||||
refreshUser,
|
||||
};
|
||||
}
|
||||
76
packages/hooks/src/use-permission.ts
Normal file
76
packages/hooks/src/use-permission.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { PermissionContext } from "./types.js";
|
||||
|
||||
/**
|
||||
* usePermission - 权限查询 Hook
|
||||
*
|
||||
* 用途:L2 路由级 + L3 组件级视口控制。
|
||||
* 权限点命名遵循 F7 裁决:`<RESOURCE>_<ACTION>`(如 `EXAM_READ`)。
|
||||
* 数据范围用后缀 `_OWN`/`_CHILD`(如 `GRADE_READ_CHILD`)。
|
||||
*
|
||||
* 注意:权限数据由调用方注入(apps/* 从 iam /iam/permissions/effective 获取后传入)。
|
||||
* 本 hook 不直接调 API,保持纯客户端逻辑。
|
||||
*
|
||||
* @example
|
||||
* const { hasPermission, hasAny, hasAll } = usePermission({ permissions, dataScope, roles });
|
||||
*
|
||||
* if (hasPermission("EXAM_CREATE")) { ... }
|
||||
* if (hasPermission("GRADE_READ_CHILD")) { ... } // 数据范围后缀
|
||||
*/
|
||||
|
||||
export interface UsePermissionProps {
|
||||
/** 权限上下文(从 iam /iam/permissions/effective 获取) */
|
||||
context: PermissionContext | null;
|
||||
}
|
||||
|
||||
export interface UsePermissionReturn {
|
||||
/** 检查是否拥有指定权限点 */
|
||||
hasPermission: (perm: string) => boolean;
|
||||
/** 检查是否拥有任一权限点 */
|
||||
hasAny: (...perms: string[]) => boolean;
|
||||
/** 检查是否拥有全部权限点 */
|
||||
hasAll: (...perms: string[]) => boolean;
|
||||
/** 检查是否拥有指定角色 */
|
||||
hasRole: (role: string) => boolean;
|
||||
/** 当前数据范围 */
|
||||
dataScope: PermissionContext["dataScope"] | null;
|
||||
}
|
||||
|
||||
export function usePermission({
|
||||
context,
|
||||
}: UsePermissionProps): UsePermissionReturn {
|
||||
const permissions = context?.permissions ?? [];
|
||||
const roles = context?.roles ?? [];
|
||||
const dataScope = context?.dataScope ?? null;
|
||||
|
||||
const permissionSet = useMemo(() => new Set(permissions), [permissions]);
|
||||
const roleSet = useMemo(() => new Set(roles), [roles]);
|
||||
|
||||
const hasPermission = useCallback(
|
||||
(perm: string): boolean => permissionSet.has(perm),
|
||||
[permissionSet],
|
||||
);
|
||||
|
||||
const hasAny = useCallback(
|
||||
(...perms: string[]): boolean => perms.some((p) => permissionSet.has(p)),
|
||||
[permissionSet],
|
||||
);
|
||||
|
||||
const hasAll = useCallback(
|
||||
(...perms: string[]): boolean => perms.every((p) => permissionSet.has(p)),
|
||||
[permissionSet],
|
||||
);
|
||||
|
||||
const hasRole = useCallback(
|
||||
(role: string): boolean => roleSet.has(role),
|
||||
[roleSet],
|
||||
);
|
||||
|
||||
return {
|
||||
hasPermission,
|
||||
hasAny,
|
||||
hasAll,
|
||||
hasRole,
|
||||
dataScope,
|
||||
};
|
||||
}
|
||||
66
packages/hooks/src/use-toast.ts
Normal file
66
packages/hooks/src/use-toast.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ToastMessage } from "./types.js";
|
||||
|
||||
/**
|
||||
* useToast - 全局 Toast 通知管理
|
||||
*
|
||||
* 用途:成功/错误/警告/信息提示,自动关闭。
|
||||
* 与 Zustand ui-store 解耦,提供 hook 层 API。
|
||||
*
|
||||
* @example
|
||||
* const { toasts, showToast, dismissToast } = useToast();
|
||||
* showToast({ type: "success", title: "保存成功" });
|
||||
*/
|
||||
|
||||
export interface UseToastReturn {
|
||||
/** 当前活跃的 toast 列表 */
|
||||
toasts: ToastMessage[];
|
||||
/** 展示 toast */
|
||||
showToast: (toast: Omit<ToastMessage, "id">) => string;
|
||||
/** 关闭指定 toast */
|
||||
dismissToast: (id: string) => void;
|
||||
/** 关闭全部 toast */
|
||||
dismissAll: () => void;
|
||||
}
|
||||
|
||||
export function useToast(): UseToastReturn {
|
||||
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
||||
|
||||
const dismissToast = useCallback((id: string): void => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback(
|
||||
(toast: Omit<ToastMessage, "id">): string => {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const fullToast: ToastMessage = {
|
||||
id,
|
||||
duration: 4000,
|
||||
...toast,
|
||||
};
|
||||
|
||||
setToasts((prev) => [...prev, fullToast]);
|
||||
|
||||
// 自动关闭
|
||||
if (fullToast.duration && fullToast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
dismissToast(id);
|
||||
}, fullToast.duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
[dismissToast],
|
||||
);
|
||||
|
||||
const dismissAll = useCallback((): void => {
|
||||
setToasts([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
toasts,
|
||||
showToast,
|
||||
dismissToast,
|
||||
dismissAll,
|
||||
};
|
||||
}
|
||||
61
packages/hooks/src/use-trace-id.ts
Normal file
61
packages/hooks/src/use-trace-id.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
/**
|
||||
* useTraceId - 当前会话 trace_id 管理
|
||||
*
|
||||
* 用途:OpenTelemetry traceparent 透传,用于日志关联 + 链路追踪。
|
||||
* ai13 新增(对齐 project_rules §12 可观测性规范)。
|
||||
*
|
||||
* @example
|
||||
* const { traceId, setTraceId, withTraceId } = useTraceId();
|
||||
* setTraceId("abc123");
|
||||
* withTraceId(() => fetch("/api/v1/exams", { headers: { "X-Trace-Id": traceId } }));
|
||||
*/
|
||||
|
||||
export interface UseTraceIdReturn {
|
||||
/** 当前 trace_id */
|
||||
traceId: string | null;
|
||||
/** 设置 trace_id(从响应头 X-Trace-Id 获取) */
|
||||
setTraceId: (id: string) => void;
|
||||
/** 清除 trace_id */
|
||||
clear: () => void;
|
||||
/**
|
||||
* 在 trace_id 上下文中执行回调(自动注入 header)
|
||||
* 用于 fetch 包装
|
||||
*/
|
||||
withTraceId: <T>(
|
||||
fn: (headers: Record<string, string>) => Promise<T>,
|
||||
) => Promise<T>;
|
||||
}
|
||||
|
||||
export function useTraceId(): UseTraceIdReturn {
|
||||
const [traceId, setTraceIdState] = useState<string | null>(null);
|
||||
|
||||
const setTraceId = useCallback((id: string): void => {
|
||||
setTraceIdState(id);
|
||||
}, []);
|
||||
|
||||
const clear = useCallback((): void => {
|
||||
setTraceIdState(null);
|
||||
}, []);
|
||||
|
||||
const withTraceId = useCallback(
|
||||
async <T>(
|
||||
fn: (headers: Record<string, string>) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const headers: Record<string, string> = {};
|
||||
if (traceId) {
|
||||
headers["X-Trace-Id"] = traceId;
|
||||
}
|
||||
return fn(headers);
|
||||
},
|
||||
[traceId],
|
||||
);
|
||||
|
||||
return {
|
||||
traceId,
|
||||
setTraceId,
|
||||
clear,
|
||||
withTraceId,
|
||||
};
|
||||
}
|
||||
65
packages/hooks/src/use-viewports.ts
Normal file
65
packages/hooks/src/use-viewports.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useMemo } from "react";
|
||||
import type { Viewport } from "./types.js";
|
||||
|
||||
/**
|
||||
* useViewports - 视口列表查询
|
||||
*
|
||||
* 用途:按 scope 过滤当前用户的可用视口(iam 4 层视口模型)。
|
||||
* 视口数据由调用方注入(apps/* 从 iam /iam/viewports 获取后传入)。
|
||||
*
|
||||
* iam 视口 4 层:admin / teacher / student / parent
|
||||
*
|
||||
* @example
|
||||
* const { viewports, getByType, hasViewport } = useViewports({ viewports });
|
||||
* const teacherViewports = getByType("teacher");
|
||||
*/
|
||||
|
||||
export interface UseViewportsProps {
|
||||
/** 当前用户的视口列表(从 iam 获取) */
|
||||
viewports: Viewport[] | null;
|
||||
}
|
||||
|
||||
export interface UseViewportsReturn {
|
||||
/** 全部视口 */
|
||||
viewports: Viewport[];
|
||||
/** 按类型过滤 */
|
||||
getByType: (type: Viewport["type"]) => Viewport[];
|
||||
/** 检查是否拥有指定类型视口 */
|
||||
hasViewport: (type: Viewport["type"]) => boolean;
|
||||
/** 按 ID 查找 */
|
||||
getById: (id: string) => Viewport | undefined;
|
||||
}
|
||||
|
||||
export function useViewports({
|
||||
viewports: input,
|
||||
}: UseViewportsProps): UseViewportsReturn {
|
||||
const viewports = useMemo(() => input ?? [], [input]);
|
||||
|
||||
const getByType = useMemo(
|
||||
() =>
|
||||
(type: Viewport["type"]): Viewport[] =>
|
||||
viewports.filter((v) => v.type === type),
|
||||
[viewports],
|
||||
);
|
||||
|
||||
const hasViewport = useMemo(
|
||||
() =>
|
||||
(type: Viewport["type"]): boolean =>
|
||||
viewports.some((v) => v.type === type),
|
||||
[viewports],
|
||||
);
|
||||
|
||||
const getById = useMemo(
|
||||
() =>
|
||||
(id: string): Viewport | undefined =>
|
||||
viewports.find((v) => v.id === id),
|
||||
[viewports],
|
||||
);
|
||||
|
||||
return {
|
||||
viewports,
|
||||
getByType,
|
||||
hasViewport,
|
||||
getById,
|
||||
};
|
||||
}
|
||||
20
packages/hooks/tsconfig.json
Normal file
20
packages/hooks/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
76
packages/shared-go/env/env.go
vendored
Normal file
76
packages/shared-go/env/env.go
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
// Package env provides typed helpers for reading environment variables.
|
||||
//
|
||||
// All functions treat an unset variable and an empty string as distinct only
|
||||
// when explicitly documented: Must/Get distinguish "not set" from "set to empty"
|
||||
// via os.LookupEnv, while GetInt/GetBool/GetDuration fall back to the default
|
||||
// value when parsing fails.
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Must returns the value of the environment variable named by key.
|
||||
// It panics if the variable is not set, which is intended for values that the
|
||||
// service cannot start without (DB URLs, JWT secrets, ...).
|
||||
func Must(key string) string {
|
||||
v, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("env: required environment variable %q is not set", key))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Get returns the value of the environment variable named by key, or
|
||||
// defaultValue when the variable is not set.
|
||||
func Get(key, defaultValue string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetInt returns the integer value of the environment variable named by key,
|
||||
// or defaultValue when the variable is not set or cannot be parsed as an int.
|
||||
func GetInt(key string, defaultValue int) int {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return n
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetBool returns the boolean value of the environment variable named by key,
|
||||
// or defaultValue when the variable is not set or cannot be parsed as a bool
|
||||
// (accepted values are those understood by strconv.ParseBool: 1, t, T, TRUE,
|
||||
// true, True, 0, f, F, FALSE, false, False).
|
||||
func GetBool(key string, defaultValue bool) bool {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return b
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetDuration returns the duration value of the environment variable named by
|
||||
// key, or defaultValue when the variable is not set or cannot be parsed by
|
||||
// time.ParseDuration (e.g. "30s", "5m", "2h").
|
||||
func GetDuration(key string, defaultValue time.Duration) time.Duration {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return d
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
2
packages/shared-go/gen/proto/.gitkeep
Normal file
2
packages/shared-go/gen/proto/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# buf generate 产物输出到此目录(见 packages/shared-proto/buf.gen.yaml)。
|
||||
# 保留此文件以便 gen/proto 目录在产物生成前被 git 跟踪。
|
||||
8
packages/shared-go/go.mod
Normal file
8
packages/shared-go/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module github.com/edu-cloud/shared-go
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
go.uber.org/zap v1.27.0
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
)
|
||||
183
packages/shared-go/jwks/jwks.go
Normal file
183
packages/shared-go/jwks/jwks.go
Normal file
@@ -0,0 +1,183 @@
|
||||
// Package jwks fetches JSON Web Key Sets and validates RS256 JWTs against them.
|
||||
//
|
||||
// A Fetcher caches parsed RSA public keys in memory for 5 minutes per kid so
|
||||
// repeated token validation does not re-hit the issuer's JWKS endpoint. It is
|
||||
// safe for concurrent use.
|
||||
package jwks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// cacheTTL is how long a cached public key is considered fresh before the
|
||||
// Fetcher re-fetches the JWKS document.
|
||||
const cacheTTL = 5 * time.Minute
|
||||
|
||||
// Claims is the set of assertions carried by an Edu access token. The
|
||||
// embedded RegisteredClaims provides exp, iss, aud and the other standard
|
||||
// JWT claims.
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
DataScope string `json:"data_scope"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// cachedKey holds a parsed public key together with the moment it expires.
|
||||
type cachedKey struct {
|
||||
key *rsa.PublicKey
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// Fetcher downloads and caches a JWKS document and validates tokens against
|
||||
// the cached keys.
|
||||
type Fetcher struct {
|
||||
jwksURL string
|
||||
httpClient *http.Client
|
||||
cache sync.Map
|
||||
}
|
||||
|
||||
// NewFetcher returns a Fetcher that retrieves keys from jwksURL.
|
||||
func NewFetcher(jwksURL string) *Fetcher {
|
||||
return &Fetcher{
|
||||
jwksURL: jwksURL,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// GetPublicKey returns the RSA public key identified by kid. The key is served
|
||||
// from the in-memory cache when fresh; otherwise the JWKS document is
|
||||
// re-fetched and the cache repopulated.
|
||||
func (f *Fetcher) GetPublicKey(kid string) (*rsa.PublicKey, error) {
|
||||
if v, ok := f.cache.Load(kid); ok {
|
||||
if ck, ok := v.(*cachedKey); ok && time.Now().Before(ck.expiresAt) {
|
||||
return ck.key, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.refreshCache(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, ok := f.cache.Load(kid)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("jwks: kid %q not found in JWKS", kid)
|
||||
}
|
||||
ck, ok := v.(*cachedKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("jwks: cached entry for kid %q has unexpected type", kid)
|
||||
}
|
||||
return ck.key, nil
|
||||
}
|
||||
|
||||
// refreshCache downloads the JWKS document and replaces every cached key.
|
||||
func (f *Fetcher) refreshCache() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, f.jwksURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jwks: building request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := f.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jwks: fetching JWKS: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("jwks: unexpected status %d from %s", resp.StatusCode, f.jwksURL)
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Keys []struct {
|
||||
Kid string `json:"kid"`
|
||||
Kty string `json:"kty"`
|
||||
N string `json:"n"`
|
||||
E string `json:"e"`
|
||||
} `json:"keys"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return fmt.Errorf("jwks: decoding response: %w", err)
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(cacheTTL)
|
||||
for _, k := range body.Keys {
|
||||
if k.Kty != "RSA" {
|
||||
continue
|
||||
}
|
||||
pk, err := buildRSAPublicKey(k.N, k.E)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jwks: parsing key %q: %w", k.Kid, err)
|
||||
}
|
||||
f.cache.Store(k.Kid, &cachedKey{key: pk, expiresAt: expiresAt})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildRSAPublicKey reconstructs an rsa.PublicKey from the base64url-encoded
|
||||
// modulus (n) and exponent (e) fields of a JWK.
|
||||
func buildRSAPublicKey(nStr, eStr string) (*rsa.PublicKey, error) {
|
||||
nBytes, err := base64.RawURLEncoding.DecodeString(nStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding modulus: %w", err)
|
||||
}
|
||||
eBytes, err := base64.RawURLEncoding.DecodeString(eStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding exponent: %w", err)
|
||||
}
|
||||
|
||||
var e int
|
||||
for _, b := range eBytes {
|
||||
e = e<<8 + int(b)
|
||||
}
|
||||
if e == 0 {
|
||||
return nil, fmt.Errorf("empty exponent")
|
||||
}
|
||||
|
||||
return &rsa.PublicKey{
|
||||
N: new(big.Int).SetBytes(nBytes),
|
||||
E: e,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateToken parses and validates an RS256 JWT using the cached JWKS keys.
|
||||
// The kid in the token header selects the verification key. On success the
|
||||
// populated Claims (including user_id, role, data_scope and the standard
|
||||
// registered claims) are returned.
|
||||
func (f *Fetcher) ValidateToken(token string) (*Claims, error) {
|
||||
parsed, err := jwt.ParseWithClaims(token, &Claims{}, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("jwks: unexpected signing method %v", t.Header["alg"])
|
||||
}
|
||||
kid, ok := t.Header["kid"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("jwks: token header missing kid")
|
||||
}
|
||||
pk, err := f.GetPublicKey(kid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pk, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jwks: parsing token: %w", err)
|
||||
}
|
||||
|
||||
claims, ok := parsed.Claims.(*Claims)
|
||||
if !ok || !parsed.Valid {
|
||||
return nil, fmt.Errorf("jwks: invalid token claims")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
65
packages/shared-go/logger/logger.go
Normal file
65
packages/shared-go/logger/logger.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Package logger provides a structured logger built on zap with OpenTelemetry
|
||||
// trace correlation.
|
||||
//
|
||||
// In production (the default) logs are emitted as JSON at Info level. When the
|
||||
// ENV environment variable is set to "development" the logger switches to a
|
||||
// console encoder at Debug level for human-readable output.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// ctxKey is an unexported type so context values cannot collide with callers.
|
||||
type ctxKey struct{}
|
||||
|
||||
// fallback is returned by FromContext when no logger has been injected into
|
||||
// the context; it discards all output so callers never receive a nil logger.
|
||||
var fallback = zap.NewNop()
|
||||
|
||||
// New creates a structured logger tagged with serviceName. The encoder and
|
||||
// level are selected from the ENV environment variable as described in the
|
||||
// package documentation.
|
||||
func New(serviceName string) *zap.Logger {
|
||||
var cfg zap.Config
|
||||
if os.Getenv("ENV") == "development" {
|
||||
cfg = zap.NewDevelopmentConfig()
|
||||
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
} else {
|
||||
cfg = zap.NewProductionConfig()
|
||||
cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
|
||||
}
|
||||
|
||||
l, err := cfg.Build(zap.Fields(zap.String("service", serviceName)))
|
||||
if err != nil {
|
||||
// A logger that cannot be constructed is a fatal misconfiguration;
|
||||
// there is no safe way to continue.
|
||||
panic(err)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// WithContext returns a copy of ctx that carries l so it can later be
|
||||
// retrieved via FromContext.
|
||||
func WithContext(ctx context.Context, l *zap.Logger) context.Context {
|
||||
return context.WithValue(ctx, ctxKey{}, l)
|
||||
}
|
||||
|
||||
// FromContext returns the logger stored in ctx. When an active OpenTelemetry
|
||||
// span is present, the returned logger is decorated with a trace_id field so
|
||||
// log lines can be correlated to traces. If no logger was stored in ctx a
|
||||
// no-op logger is returned.
|
||||
func FromContext(ctx context.Context) *zap.Logger {
|
||||
if v, ok := ctx.Value(ctxKey{}).(*zap.Logger); ok && v != nil {
|
||||
if sc := trace.SpanContextFromContext(ctx); sc.HasTraceID() {
|
||||
return v.With(zap.String("trace_id", sc.TraceID().String()))
|
||||
}
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
92
packages/shared-go/tracer/tracer.go
Normal file
92
packages/shared-go/tracer/tracer.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Package tracer bootstraps the OpenTelemetry SDK for a service and exports
|
||||
// traces over OTLP/HTTP (default endpoint localhost:4318, overridable via the
|
||||
// OTEL_EXPORTER_OTLP_ENDPOINT environment variable).
|
||||
//
|
||||
// Typical usage:
|
||||
//
|
||||
// if err := tracer.Init("api-gateway"); err != nil { log.Fatal(err) }
|
||||
// defer tracer.Shutdown(context.Background())
|
||||
package tracer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// tp is the process-wide TracerProvider created by Init and torn down by
|
||||
// Shutdown.
|
||||
var tp *sdktrace.TracerProvider
|
||||
|
||||
// Init initializes the OpenTelemetry SDK with an OTLP HTTP exporter and
|
||||
// registers it as the global TracerProvider. The service is identified by
|
||||
// serviceName on all exported spans.
|
||||
func Init(serviceName string) error {
|
||||
endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
if endpoint == "" {
|
||||
endpoint = "localhost:4318"
|
||||
}
|
||||
|
||||
exp, err := otlptracehttp.New(
|
||||
context.Background(),
|
||||
otlptracehttp.WithEndpoint(endpoint),
|
||||
otlptracehttp.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tracer: creating OTLP exporter: %w", err)
|
||||
}
|
||||
|
||||
res, err := resource.Merge(
|
||||
resource.Default(),
|
||||
resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
semconv.ServiceName(serviceName),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tracer: building resource: %w", err)
|
||||
}
|
||||
|
||||
tp = sdktrace.NewTracerProvider(
|
||||
sdktrace.WithBatcher(exp),
|
||||
sdktrace.WithResource(res),
|
||||
)
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown flushes pending spans and releases resources held by the
|
||||
// TracerProvider. It is a no-op when Init was never called. The provided
|
||||
// context bounds the flush to 5 seconds.
|
||||
func Shutdown(ctx context.Context) error {
|
||||
if tp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
if err := tp.Shutdown(ctx); err != nil {
|
||||
return fmt.Errorf("tracer: shutting down provider: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromContext returns the active span from ctx. When no span is active the
|
||||
// returned span is a no-op, so callers may safely record attributes or events
|
||||
// without nil checks.
|
||||
func FromContext(ctx context.Context) trace.Span {
|
||||
return trace.SpanFromContext(ctx)
|
||||
}
|
||||
30
packages/shared-ts/package.json
Normal file
30
packages/shared-ts/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@edu/shared-ts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./outbox": {
|
||||
"types": "./src/outbox/index.ts",
|
||||
"default": "./src/outbox/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.0",
|
||||
"@nestjs/core": "^10.4.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"drizzle-orm": "^0.31.0",
|
||||
"kafkajs": "^2.2.0",
|
||||
"pino": "^9.4.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
22
packages/shared-ts/src/outbox/index.ts
Normal file
22
packages/shared-ts/src/outbox/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export { OutboxModule } from "./outbox.module.js";
|
||||
export type { OutboxRootOptions } from "./outbox.module.js";
|
||||
export { OutboxService } from "./outbox.service.js";
|
||||
export { OutboxPublisher } from "./publisher.js";
|
||||
export { createOutboxTable } from "./schema.js";
|
||||
export type { OutboxTable, OutboxRow, NewOutboxRow } from "./schema.js";
|
||||
export {
|
||||
OUTBOX_CONFIG,
|
||||
OUTBOX_DB,
|
||||
OUTBOX_KAFKA_PRODUCER,
|
||||
OUTBOX_LOGGER,
|
||||
OUTBOX_TABLE,
|
||||
} from "./types.js";
|
||||
export type {
|
||||
OutboxConfig,
|
||||
OutboxDbClient,
|
||||
OutboxKafkaProducer,
|
||||
OutboxLogger,
|
||||
OutboxRecord,
|
||||
OutboxStatus,
|
||||
PublishOptions,
|
||||
} from "./types.js";
|
||||
75
packages/shared-ts/src/outbox/outbox.module.ts
Normal file
75
packages/shared-ts/src/outbox/outbox.module.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Module, type DynamicModule } from "@nestjs/common";
|
||||
import pino from "pino";
|
||||
import { OutboxService } from "./outbox.service.js";
|
||||
import { OutboxPublisher } from "./publisher.js";
|
||||
import { createOutboxTable } from "./schema.js";
|
||||
import {
|
||||
OUTBOX_CONFIG,
|
||||
OUTBOX_DB,
|
||||
OUTBOX_KAFKA_PRODUCER,
|
||||
OUTBOX_LOGGER,
|
||||
OUTBOX_TABLE,
|
||||
type OutboxConfig,
|
||||
type OutboxDbClient,
|
||||
type OutboxKafkaProducer,
|
||||
type OutboxLogger,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* forRoot 选项。
|
||||
*
|
||||
* `config` 即 OutboxConfig(表名 / topic / 轮询参数)。
|
||||
* `db` / `kafkaProducer` / `logger` 为基础设施实例:NestJS DynamicModule
|
||||
* 无法跨模块边界注入宿主已存在的具体单例,故统一通过 forRoot 注入。
|
||||
*
|
||||
* `kafkaProducer` 应由调用方以 idempotent 方式创建:
|
||||
* `kafka.producer({ idempotent: true, transactionalId: '<service>-tx' })`。
|
||||
*/
|
||||
export interface OutboxRootOptions {
|
||||
config: OutboxConfig;
|
||||
db: OutboxDbClient;
|
||||
kafkaProducer: OutboxKafkaProducer;
|
||||
/** 可选;省略时使用默认 pino logger(name=outbox) */
|
||||
logger?: OutboxLogger;
|
||||
}
|
||||
|
||||
function resolveLogger(logger: OutboxLogger | undefined): OutboxLogger {
|
||||
return logger ?? pino({ name: "outbox", level: "info" });
|
||||
}
|
||||
|
||||
/**
|
||||
* OutboxModule —— 事务性 Outbox 模式的 NestJS DynamicModule。
|
||||
*
|
||||
* 注册并启动:
|
||||
* - OutboxService:业务代码注入后调用 `publish` 写入 outbox 记录
|
||||
* - OutboxPublisher:onModuleInit 启动轮询,onModuleDestroy 停止轮询
|
||||
*
|
||||
* 用法:
|
||||
* ```ts
|
||||
* @Module({
|
||||
* imports: [OutboxModule.forRoot({ config, db, kafkaProducer })],
|
||||
* })
|
||||
* export class AppModule {}
|
||||
* ```
|
||||
*/
|
||||
@Module({})
|
||||
export class OutboxModule {
|
||||
static forRoot(options: OutboxRootOptions): DynamicModule {
|
||||
const table = createOutboxTable(options.config.tableName);
|
||||
const logger = resolveLogger(options.logger);
|
||||
|
||||
return {
|
||||
module: OutboxModule,
|
||||
providers: [
|
||||
{ provide: OUTBOX_CONFIG, useValue: options.config },
|
||||
{ provide: OUTBOX_DB, useValue: options.db },
|
||||
{ provide: OUTBOX_TABLE, useValue: table },
|
||||
{ provide: OUTBOX_KAFKA_PRODUCER, useValue: options.kafkaProducer },
|
||||
{ provide: OUTBOX_LOGGER, useValue: logger },
|
||||
OutboxService,
|
||||
OutboxPublisher,
|
||||
],
|
||||
exports: [OutboxService],
|
||||
};
|
||||
}
|
||||
}
|
||||
74
packages/shared-ts/src/outbox/outbox.service.ts
Normal file
74
packages/shared-ts/src/outbox/outbox.service.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import type { NewOutboxRow, OutboxTable } from "./schema.js";
|
||||
import {
|
||||
OUTBOX_CONFIG,
|
||||
OUTBOX_DB,
|
||||
OUTBOX_LOGGER,
|
||||
OUTBOX_TABLE,
|
||||
type OutboxConfig,
|
||||
type OutboxDbClient,
|
||||
type OutboxLogger,
|
||||
type PublishOptions,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* OutboxService —— 事务性 Outbox 写入服务。
|
||||
*
|
||||
* 调用方在业务事务内调用 `publish`,将事件记录写入 outbox 表,
|
||||
* 与业务写在同一事务中原子提交,保证“业务变更”与“事件发布”的一致性。
|
||||
*
|
||||
* 由独立的 OutboxPublisher 负责异步轮询 pending 记录并投递到 Kafka,
|
||||
* 实现 at-least-once 投递语义。
|
||||
*/
|
||||
@Injectable()
|
||||
export class OutboxService {
|
||||
constructor(
|
||||
@Inject(OUTBOX_DB) private readonly db: OutboxDbClient,
|
||||
@Inject(OUTBOX_TABLE) private readonly table: OutboxTable,
|
||||
@Inject(OUTBOX_CONFIG) private readonly config: OutboxConfig,
|
||||
@Inject(OUTBOX_LOGGER) private readonly logger: OutboxLogger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 写入一条 outbox 记录。
|
||||
*
|
||||
* @param eventType 事件类型(如 `ExamCreated`),用于 Kafka topic 路由
|
||||
* @param payload 事件负载,将被 JSON 序列化存入 outbox 行
|
||||
* @param options 可选:aggregateId / metadata / delayMs / tx
|
||||
* @returns event id(cuid2),供调用方追踪;同时作为 Kafka 消息 key 实现幂等去重
|
||||
*
|
||||
* 事务管理:传入 `options.tx` 则在调用方事务内写入(推荐,保证与业务表原子提交);
|
||||
* 省略时使用模块注入的 db 执行单条插入(仅保证 outbox 行自身的原子性)。
|
||||
*/
|
||||
async publish(
|
||||
eventType: string,
|
||||
payload: unknown,
|
||||
options?: PublishOptions,
|
||||
): Promise<string> {
|
||||
const id = createId();
|
||||
const now = new Date();
|
||||
const nextRetryAt =
|
||||
options?.delayMs !== undefined && options.delayMs > 0
|
||||
? new Date(now.getTime() + options.delayMs)
|
||||
: null;
|
||||
|
||||
const record: NewOutboxRow = {
|
||||
id,
|
||||
eventType,
|
||||
payload: JSON.stringify(payload),
|
||||
aggregateId: options?.aggregateId ?? id,
|
||||
maxRetryCount: this.config.maxRetryCount,
|
||||
nextRetryAt,
|
||||
metadata: options?.metadata ?? null,
|
||||
};
|
||||
|
||||
await (options?.tx ?? this.db).insert(this.table).values(record);
|
||||
|
||||
this.logger.debug(
|
||||
{ id, eventType, aggregateId: record.aggregateId },
|
||||
"Outbox record enqueued",
|
||||
);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
205
packages/shared-ts/src/outbox/publisher.ts
Normal file
205
packages/shared-ts/src/outbox/publisher.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
type OnModuleDestroy,
|
||||
type OnModuleInit,
|
||||
} from "@nestjs/common";
|
||||
import { and, eq, isNull, lte, or, type SQL } from "drizzle-orm";
|
||||
import type { OutboxRow, OutboxTable } from "./schema.js";
|
||||
import {
|
||||
OUTBOX_CONFIG,
|
||||
OUTBOX_DB,
|
||||
OUTBOX_KAFKA_PRODUCER,
|
||||
OUTBOX_LOGGER,
|
||||
OUTBOX_TABLE,
|
||||
type OutboxConfig,
|
||||
type OutboxDbClient,
|
||||
type OutboxKafkaProducer,
|
||||
type OutboxLogger,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* OutboxPublisher —— 轮询 pending 记录并投递到 Kafka。
|
||||
*
|
||||
* 实现 at-least-once 投递语义:
|
||||
* 1. 周期性(pollIntervalMs)批量拉取 status=pending 且到期的记录
|
||||
* 2. 逐条投递到 Kafka(message key = event id,保证同聚合有序)
|
||||
* 3. 投递成功 → 标记 published
|
||||
* 4. 投递失败 → 指数退避更新 nextRetryAt;重试耗尽 → 标记 failed
|
||||
*
|
||||
* 幂等性:
|
||||
* - producer 应由调用方配置为 idempotent(`kafka.producer({ idempotent: true, transactionalId })`),
|
||||
* 避免生产端重试产生重复消息
|
||||
* - message key = event id(cuid2),消费端可基于 event_id 去重(Redis SETNX 或 DB 唯一索引)
|
||||
*/
|
||||
@Injectable()
|
||||
export class OutboxPublisher implements OnModuleInit, OnModuleDestroy {
|
||||
private intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
private isPolling = false;
|
||||
|
||||
constructor(
|
||||
@Inject(OUTBOX_DB) private readonly db: OutboxDbClient,
|
||||
@Inject(OUTBOX_TABLE) private readonly table: OutboxTable,
|
||||
@Inject(OUTBOX_KAFKA_PRODUCER)
|
||||
private readonly producer: OutboxKafkaProducer,
|
||||
@Inject(OUTBOX_CONFIG) private readonly config: OutboxConfig,
|
||||
@Inject(OUTBOX_LOGGER) private readonly logger: OutboxLogger,
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
await this.start();
|
||||
}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
await this.stop();
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.intervalId) return;
|
||||
this.logger.info(
|
||||
{
|
||||
topic: this.config.kafkaTopic,
|
||||
pollIntervalMs: this.config.pollIntervalMs,
|
||||
batchSize: this.config.batchSize,
|
||||
},
|
||||
"OutboxPublisher started",
|
||||
);
|
||||
this.intervalId = setInterval(() => {
|
||||
void this.poll();
|
||||
}, this.config.pollIntervalMs);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
this.logger.info("OutboxPublisher stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取一批到期 pending 记录并逐条投递。
|
||||
* 通过 isPolling 标志保证不会并发轮询。
|
||||
*/
|
||||
private async poll(): Promise<void> {
|
||||
if (this.isPolling) return;
|
||||
this.isPolling = true;
|
||||
try {
|
||||
const now = new Date();
|
||||
const where: SQL | undefined = and(
|
||||
eq(this.table.status, "pending"),
|
||||
or(isNull(this.table.nextRetryAt), lte(this.table.nextRetryAt, now)),
|
||||
);
|
||||
const messages: OutboxRow[] = await this.db
|
||||
.select()
|
||||
.from(this.table)
|
||||
.where(where)
|
||||
.limit(this.config.batchSize);
|
||||
|
||||
for (const message of messages) {
|
||||
await this.dispatch(message);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({ error }, "Outbox poll failed");
|
||||
} finally {
|
||||
this.isPolling = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 投递单条记录到 Kafka 并更新状态。
|
||||
*/
|
||||
private async dispatch(message: OutboxRow): Promise<void> {
|
||||
try {
|
||||
await this.producer.send({
|
||||
topic: this.config.kafkaTopic,
|
||||
messages: [
|
||||
{
|
||||
key: message.id,
|
||||
value: message.payload,
|
||||
headers: this.buildHeaders(message),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await this.db
|
||||
.update(this.table)
|
||||
.set({ status: "published", publishedAt: new Date(), lastError: null })
|
||||
.where(eq(this.table.id, message.id));
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
id: message.id,
|
||||
eventType: message.eventType,
|
||||
topic: this.config.kafkaTopic,
|
||||
},
|
||||
"Outbox message published",
|
||||
);
|
||||
} catch (error) {
|
||||
await this.handleFailure(message, this.toErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败处理:指数退避更新 nextRetryAt,重试耗尽则标记 failed。
|
||||
*/
|
||||
private async handleFailure(
|
||||
message: OutboxRow,
|
||||
errorMessage: string,
|
||||
): Promise<void> {
|
||||
const newRetryCount = message.retryCount + 1;
|
||||
this.logger.error(
|
||||
{
|
||||
id: message.id,
|
||||
eventType: message.eventType,
|
||||
retryCount: newRetryCount,
|
||||
error: errorMessage,
|
||||
},
|
||||
"Outbox publish failed",
|
||||
);
|
||||
|
||||
if (newRetryCount >= message.maxRetryCount) {
|
||||
await this.db
|
||||
.update(this.table)
|
||||
.set({
|
||||
status: "failed",
|
||||
retryCount: newRetryCount,
|
||||
lastError: errorMessage,
|
||||
})
|
||||
.where(eq(this.table.id, message.id));
|
||||
return;
|
||||
}
|
||||
|
||||
const backoffMs = this.config.retryBackoffMs * 2 ** newRetryCount;
|
||||
const nextRetryAt = new Date(Date.now() + backoffMs);
|
||||
await this.db
|
||||
.update(this.table)
|
||||
.set({
|
||||
retryCount: newRetryCount,
|
||||
nextRetryAt,
|
||||
lastError: errorMessage,
|
||||
})
|
||||
.where(eq(this.table.id, message.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Kafka 消息头:eventId / eventType / aggregateId + 调用方 metadata。
|
||||
*/
|
||||
private buildHeaders(message: OutboxRow): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
eventId: message.id,
|
||||
eventType: message.eventType,
|
||||
aggregateId: message.aggregateId,
|
||||
};
|
||||
if (message.metadata) {
|
||||
for (const [key, value] of Object.entries(message.metadata)) {
|
||||
headers[key] = value;
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private toErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
56
packages/shared-ts/src/outbox/schema.ts
Normal file
56
packages/shared-ts/src/outbox/schema.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
int,
|
||||
json,
|
||||
mysqlTable,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
} from "drizzle-orm/mysql-core";
|
||||
import type { OutboxRecord, OutboxStatus } from "./types.js";
|
||||
|
||||
/**
|
||||
* 创建 outbox 表的 Drizzle schema。
|
||||
*
|
||||
* 表名由调用方通过 OutboxConfig.tableName 提供(每服务独立 outbox 表),
|
||||
* 因此以工厂函数形式返回,避免多服务共用同一表名常量。
|
||||
*
|
||||
* 字段与 OutboxRecord 接口一一对齐:
|
||||
* - id:cuid2(varchar(32)),作为 event id 与 Kafka 消息 key
|
||||
* - status:通过 $type<OutboxStatus> 收窄为联合类型
|
||||
* - metadata:通过 $type<Record<string,string> | null> 收窄
|
||||
*/
|
||||
export function createOutboxTable(tableName: string) {
|
||||
return mysqlTable(tableName, {
|
||||
id: varchar("id", { length: 32 }).notNull().primaryKey(),
|
||||
eventType: varchar("event_type", { length: 100 }).notNull(),
|
||||
payload: text("payload").notNull(),
|
||||
aggregateId: varchar("aggregate_id", { length: 32 }).notNull(),
|
||||
status: varchar("status", { length: 20 })
|
||||
.notNull()
|
||||
.default("pending")
|
||||
.$type<OutboxStatus>(),
|
||||
retryCount: int("retry_count").notNull().default(0),
|
||||
maxRetryCount: int("max_retry_count").notNull().default(5),
|
||||
nextRetryAt: timestamp("next_retry_at"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
publishedAt: timestamp("published_at"),
|
||||
lastError: text("last_error"),
|
||||
metadata: json("metadata").$type<Record<string, string> | null>(),
|
||||
});
|
||||
}
|
||||
|
||||
/** outbox 表类型 */
|
||||
export type OutboxTable = ReturnType<typeof createOutboxTable>;
|
||||
|
||||
/** select 推断行类型(应结构兼容 OutboxRecord) */
|
||||
export type OutboxRow = OutboxTable["$inferSelect"];
|
||||
|
||||
/** insert 推断类型 */
|
||||
export type NewOutboxRow = OutboxTable["$inferInsert"];
|
||||
|
||||
/**
|
||||
* 编译期断言:OutboxRow 与 OutboxRecord 结构兼容。
|
||||
* 若 schema 与接口偏离,此处会在严格模式下编译失败。
|
||||
*/
|
||||
const _assertOutboxRow: OutboxRow extends OutboxRecord ? true : never = true;
|
||||
void _assertOutboxRow;
|
||||
87
packages/shared-ts/src/outbox/types.ts
Normal file
87
packages/shared-ts/src/outbox/types.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { MySql2Database } from "drizzle-orm/mysql2";
|
||||
import type { Producer } from "kafkajs";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
/**
|
||||
* Outbox 记录状态机:
|
||||
* - pending:已写入 outbox 表,等待 publisher 投递
|
||||
* - published:已成功投递到 Kafka
|
||||
* - failed:重试次数耗尽,需人工介入
|
||||
*/
|
||||
export type OutboxStatus = "pending" | "published" | "failed";
|
||||
|
||||
/**
|
||||
* Outbox 模块配置(由各服务在 forRoot 时提供)。
|
||||
*/
|
||||
export interface OutboxConfig {
|
||||
/** outbox 表名(每服务独立,如 core_edu_outbox) */
|
||||
tableName: string;
|
||||
/** Kafka 投递目标 topic */
|
||||
kafkaTopic: string;
|
||||
/** 轮询 pending 记录的间隔(毫秒) */
|
||||
pollIntervalMs: number;
|
||||
/** 每次轮询批量处理的记录数 */
|
||||
batchSize: number;
|
||||
/** 单条记录最大重试次数 */
|
||||
maxRetryCount: number;
|
||||
/** 重试退避基数(毫秒),实际退避 = retryBackoffMs * 2^retryCount */
|
||||
retryBackoffMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* publish 选项。
|
||||
*
|
||||
* `tx` 用于由调用方管理事务:调用方在 `db.transaction(async (tx) => { ... })`
|
||||
* 中同时写入业务表与 outbox 记录,保证两者原子提交(事务性 Outbox 模式)。
|
||||
*/
|
||||
export interface PublishOptions {
|
||||
/** 聚合根 ID,作为 Kafka 消息 key 用于分区与顺序保证 */
|
||||
aggregateId?: string;
|
||||
/** 附加元数据,写入 outbox 行并随消息头投递 */
|
||||
metadata?: Record<string, string>;
|
||||
/** 延迟投递毫秒数;设置后 nextRetryAt 推迟,到期前不会被轮询 */
|
||||
delayMs?: number;
|
||||
/** 调用方管理的事务客户端;省略时使用模块注入的 db(非事务单条插入) */
|
||||
tx?: OutboxDbClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outbox 行结构(与 schema.ts 的 $inferSelect 对齐)。
|
||||
*/
|
||||
export interface OutboxRecord {
|
||||
id: string;
|
||||
eventType: string;
|
||||
/** JSON 序列化后的 payload */
|
||||
payload: string;
|
||||
aggregateId: string;
|
||||
status: OutboxStatus;
|
||||
retryCount: number;
|
||||
maxRetryCount: number;
|
||||
nextRetryAt: Date | null;
|
||||
createdAt: Date;
|
||||
publishedAt: Date | null;
|
||||
lastError: string | null;
|
||||
metadata: Record<string, string> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drizzle 数据库客户端类型。
|
||||
*
|
||||
* 服务以无 schema 绑定方式创建 db(`drizzle(pool)`),其类型为
|
||||
* `MySql2Database<Record<string, never>>`。由于 `MySql2Transaction extends
|
||||
* MySqlDatabase`,事务客户端同样可赋值给本类型,故 publish 可接受调用方传入的 tx。
|
||||
*/
|
||||
export type OutboxDbClient = MySql2Database<Record<string, never>>;
|
||||
|
||||
/** Kafka producer 类型别名 */
|
||||
export type OutboxKafkaProducer = Producer;
|
||||
|
||||
/** pino Logger 类型别名 */
|
||||
export type OutboxLogger = Logger;
|
||||
|
||||
// —— NestJS 注入 Token —— //
|
||||
export const OUTBOX_CONFIG = Symbol("OUTBOX_CONFIG");
|
||||
export const OUTBOX_DB = Symbol("OUTBOX_DB");
|
||||
export const OUTBOX_TABLE = Symbol("OUTBOX_TABLE");
|
||||
export const OUTBOX_KAFKA_PRODUCER = Symbol("OUTBOX_KAFKA_PRODUCER");
|
||||
export const OUTBOX_LOGGER = Symbol("OUTBOX_LOGGER");
|
||||
16
packages/shared-ts/tsconfig.json
Normal file
16
packages/shared-ts/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"incremental": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
24
packages/ui-components/package.json
Normal file
24
packages/ui-components/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@edu/ui-components",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edu/ui-tokens": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
72
packages/ui-components/src/empty.tsx
Normal file
72
packages/ui-components/src/empty.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "./utils/cn.js";
|
||||
|
||||
/**
|
||||
* Empty - 空态展示(插画占位 + 文案 + CTA)
|
||||
*
|
||||
* 用途:数据列表为空时展示友好提示,引导用户操作。
|
||||
*
|
||||
* @example
|
||||
* <Empty title="暂无班级" description="点击下方按钮创建第一个班级" action={<Button>新建班级</Button>} />
|
||||
*/
|
||||
|
||||
export interface EmptyProps {
|
||||
/** 标题(必填) */
|
||||
title: string;
|
||||
/** 描述文案 */
|
||||
description?: string;
|
||||
/** 行动按钮(CTA) */
|
||||
action?: ReactNode;
|
||||
/** 自定义插画;未提供时使用默认占位 */
|
||||
illustration?: ReactNode;
|
||||
/** 自定义类名 */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Empty({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
illustration,
|
||||
className,
|
||||
}: EmptyProps): ReactNode {
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center gap-4 p-8 text-center",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{illustration ?? <DefaultIllustration />}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-medium">{title}</h3>
|
||||
{description ? (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{action ?? null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultIllustration(): ReactNode {
|
||||
return (
|
||||
<div
|
||||
className="flex h-16 w-16 items-center justify-center rounded-full bg-muted"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
className="h-8 w-8 text-muted-foreground"
|
||||
>
|
||||
<path d="M9 17a2 2 0 002 2h2a2 2 0 002-2M5 9h14l-1 8H6L5 9z" />
|
||||
<path d="M9 9V5a3 3 0 016 0v4" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
packages/ui-components/src/error-boundary.tsx
Normal file
88
packages/ui-components/src/error-boundary.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* ErrorBoundary - React 渲染异常兜底(fallback UI)
|
||||
*
|
||||
* 用途:捕获子组件树渲染异常,展示降级 UI,避免整页白屏。
|
||||
*
|
||||
* @example
|
||||
* <ErrorBoundary fallback={<ErrorFallback />}>
|
||||
* <Dashboard />
|
||||
* </ErrorBoundary>
|
||||
*/
|
||||
|
||||
export interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
/** 自定义降级 UI;未提供时使用默认 fallback */
|
||||
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
||||
/** 错误回调(上报 / 日志) */
|
||||
onError?: (error: Error, info: ErrorInfo) => void;
|
||||
/** 重置回调(用于外部触发重试) */
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
export interface ErrorBoundaryState {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
state: ErrorBoundaryState = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo): void {
|
||||
this.props.onError?.(error, info);
|
||||
}
|
||||
|
||||
reset = (): void => {
|
||||
this.props.onReset?.();
|
||||
this.setState({ error: null });
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const { error } = this.state;
|
||||
const { children, fallback } = this.props;
|
||||
|
||||
if (error) {
|
||||
if (typeof fallback === "function") {
|
||||
return fallback(error, this.reset);
|
||||
}
|
||||
if (fallback !== undefined) {
|
||||
return fallback;
|
||||
}
|
||||
return <DefaultErrorFallback error={error} onReset={this.reset} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
function DefaultErrorFallback({
|
||||
error,
|
||||
onReset,
|
||||
}: {
|
||||
error: Error;
|
||||
onReset: () => void;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex min-h-[200px] flex-col items-center justify-center gap-4 p-8"
|
||||
>
|
||||
<h2 className="text-lg font-semibold">出错了</h2>
|
||||
<p className="text-sm text-muted-foreground">{error.message}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
packages/ui-components/src/index.ts
Normal file
38
packages/ui-components/src/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @edu/ui-components - 共享组件库
|
||||
*
|
||||
* 维护者:ai13(teacher-portal)
|
||||
* 关联文档:teacher-portal 02-architecture-design.md §7
|
||||
*
|
||||
* 包含:
|
||||
* - ErrorBoundary:React 渲染异常兜底
|
||||
* - Loading:骨架屏 / 加载占位
|
||||
* - Empty:空态展示
|
||||
* - RequirePermission:L3 组件级视口控制
|
||||
* - cn:类名合并工具(project_rules §3.9)
|
||||
*
|
||||
* 后续扩展(P2+):
|
||||
* - AppShell(MF Shell 布局)
|
||||
* - shadcn/ui 基础组件(Button/Input/Select/Modal/Toast/DataTable)
|
||||
* - A11y 工具集(useA11yId/mergeA11yProps/describeInput/focus-trap)
|
||||
* - Form(react-hook-form + zodResolver)
|
||||
* - RichTextEditor(Tiptap 封装)
|
||||
* - Chart(recharts 封装)
|
||||
*/
|
||||
|
||||
export { ErrorBoundary } from "./error-boundary.js";
|
||||
export type {
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState,
|
||||
} from "./error-boundary.js";
|
||||
|
||||
export { Loading } from "./loading.js";
|
||||
export type { LoadingProps } from "./loading.js";
|
||||
|
||||
export { Empty } from "./empty.js";
|
||||
export type { EmptyProps } from "./empty.js";
|
||||
|
||||
export { RequirePermission } from "./require-permission.js";
|
||||
export type { RequirePermissionProps } from "./require-permission.js";
|
||||
|
||||
export { cn } from "./utils/cn.js";
|
||||
65
packages/ui-components/src/loading.tsx
Normal file
65
packages/ui-components/src/loading.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "./utils/cn.js";
|
||||
|
||||
/**
|
||||
* Loading - 骨架屏 / 加载占位
|
||||
*
|
||||
* 用途:数据加载期间展示骨架屏,避免内容跳动。
|
||||
*
|
||||
* @example
|
||||
* <Loading /> // 默认 3 行骨架
|
||||
* <Loading lines={5} /> // 5 行骨架
|
||||
* <Loading variant="spinner" /> // 旋转图标
|
||||
*/
|
||||
|
||||
export interface LoadingProps {
|
||||
/** 骨架行数(variant="skeleton" 时生效) */
|
||||
lines?: number;
|
||||
/** 展示形态 */
|
||||
variant?: "skeleton" | "spinner";
|
||||
/** 自定义类名 */
|
||||
className?: string;
|
||||
/** aria-label(无障碍) */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export function Loading({
|
||||
lines = 3,
|
||||
variant = "skeleton",
|
||||
className,
|
||||
ariaLabel = "加载中",
|
||||
}: LoadingProps): ReactNode {
|
||||
if (variant === "spinner") {
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-label={ariaLabel}
|
||||
className={cn("flex items-center justify-center p-8", className)}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="sr-only">{ariaLabel}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-label={ariaLabel}
|
||||
className={cn("flex flex-col gap-2 p-4", className)}
|
||||
>
|
||||
{Array.from({ length: lines }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-4 animate-pulse rounded bg-muted"
|
||||
style={{ width: `${100 - i * 10}%` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
))}
|
||||
<span className="sr-only">{ariaLabel}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
packages/ui-components/src/require-permission.tsx
Normal file
48
packages/ui-components/src/require-permission.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* RequirePermission - L3 组件级视口控制
|
||||
*
|
||||
* 用途:根据当前用户权限点决定是否渲染 children。
|
||||
* 无权限时不渲染 children(返回 null 或自定义 fallback)。
|
||||
*
|
||||
* 权限点命名遵循 F7 裁决:`<RESOURCE>_<ACTION>`(如 `EXAM_READ`、`GRADE_READ_CHILD`)。
|
||||
* 数据范围用后缀 `_OWN`/`_CHILD`(如 `GRADE_READ_CHILD`)。
|
||||
*
|
||||
* 注意:本组件依赖 `@edu/hooks` 的 `usePermission()`。为避免循环依赖,
|
||||
* 权限检查函数通过 props 注入,由调用方(apps/*)从 usePermission() 获取后传入。
|
||||
*
|
||||
* @example
|
||||
* import { usePermission } from "@edu/hooks";
|
||||
* const { hasPermission } = usePermission();
|
||||
*
|
||||
* <RequirePermission perm="EXAM_CREATE" hasPermission={hasPermission}>
|
||||
* <Button>新建考试</Button>
|
||||
* </RequirePermission>
|
||||
*/
|
||||
|
||||
export interface RequirePermissionProps {
|
||||
/** 权限点(F7 规范:<RESOURCE>_<ACTION>) */
|
||||
perm: string;
|
||||
/**
|
||||
* 权限检查函数(由 @edu/hooks 的 usePermission().hasPermission 注入)
|
||||
* 返回 true 渲染 children,false 渲染 fallback。
|
||||
*/
|
||||
hasPermission: (perm: string) => boolean;
|
||||
/** 子节点(有权限时渲染) */
|
||||
children: ReactNode;
|
||||
/** 无权限时的降级 UI;未提供时返回 null */
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
export function RequirePermission({
|
||||
perm,
|
||||
hasPermission,
|
||||
children,
|
||||
fallback = null,
|
||||
}: RequirePermissionProps): ReactNode {
|
||||
if (!hasPermission(perm)) {
|
||||
return fallback;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
83
packages/ui-components/src/utils/cn.ts
Normal file
83
packages/ui-components/src/utils/cn.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 类名合并工具(project_rules §3.9 强制使用)
|
||||
*
|
||||
* 用于管理条件类名,禁止字符串拼接动态类名(如 `bg-${color}-500`)。
|
||||
* 基于 clsx + tailwind-merge 的轻量实现,后续接入 shadcn/ui 时替换为官方 cn()。
|
||||
*/
|
||||
|
||||
type ClassValue =
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
| false
|
||||
| undefined
|
||||
| ClassValue[]
|
||||
| { [key: string]: unknown };
|
||||
|
||||
/**
|
||||
* 合并类名,过滤 falsy 值。
|
||||
*
|
||||
* @example
|
||||
* cn("px-2 py-1", isActive && "bg-primary", { "text-muted": isDisabled })
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
const classes: string[] = [];
|
||||
|
||||
for (const input of inputs) {
|
||||
if (!input) continue;
|
||||
|
||||
if (typeof input === "string" || typeof input === "number") {
|
||||
classes.push(String(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
const nested = cn(...input);
|
||||
if (nested) classes.push(nested);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof input === "object") {
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value) classes.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重 + 合并 Tailwind 冲突类(基础实现,后续替换为 tailwind-merge)
|
||||
return dedupeTailwindClasses(classes.join(" "));
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础 Tailwind 类去重(同一前缀后者覆盖前者)。
|
||||
* 完整实现待引入 tailwind-merge。
|
||||
*/
|
||||
function dedupeTailwindClasses(className: string): string {
|
||||
const seen = new Set<string>();
|
||||
const tokens = className.split(/\s+/).filter(Boolean);
|
||||
|
||||
// 反向遍历,保留后出现的同类令牌
|
||||
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||
const token = tokens[i];
|
||||
if (!token) continue;
|
||||
|
||||
const prefix = getTailwindPrefix(token);
|
||||
const key = prefix ? `__${prefix}` : token;
|
||||
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
}
|
||||
|
||||
// 恢复原始顺序
|
||||
return tokens
|
||||
.filter((t) => t && (seen.has(t) || seen.has(`__${getTailwindPrefix(t)}`)))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function getTailwindPrefix(token: string): string | null {
|
||||
// 匹配 Tailwind 前缀:bg- text- p- m- w- h- border- 等
|
||||
const match = token.match(
|
||||
/^(bg|text|p|m|px|py|mx|my|w|h|min-h|min-w|border|rounded|shadow|font|leading|tracking|gap|space|flex|grid|col|row|inset|top|right|bottom|left|z|opacity|transition|duration|delay|animate|hover|focus|sm|md|lg|xl|2xl)-/,
|
||||
);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
20
packages/ui-components/tsconfig.json
Normal file
20
packages/ui-components/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
8
packages/ui-tokens/package.json
Normal file
8
packages/ui-tokens/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@edu/ui-tokens",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
39
packages/ui-tokens/src/colors.ts
Normal file
39
packages/ui-tokens/src/colors.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export type ColorScheme = {
|
||||
background: string;
|
||||
foreground: string;
|
||||
primary: string;
|
||||
secondary: string;
|
||||
muted: string;
|
||||
accent: string;
|
||||
destructive: string;
|
||||
border: string;
|
||||
input: string;
|
||||
ring: string;
|
||||
};
|
||||
|
||||
export const colors = {
|
||||
light: {
|
||||
background: "210 40% 98%",
|
||||
foreground: "222 47% 11%",
|
||||
primary: "221 83% 53%",
|
||||
secondary: "210 40% 96%",
|
||||
muted: "210 40% 96%",
|
||||
accent: "210 40% 94%",
|
||||
destructive: "0 84% 60%",
|
||||
border: "214 32% 91%",
|
||||
input: "214 32% 91%",
|
||||
ring: "221 83% 53%",
|
||||
},
|
||||
dark: {
|
||||
background: "222 47% 11%",
|
||||
foreground: "210 40% 98%",
|
||||
primary: "217 91% 60%",
|
||||
secondary: "217 33% 17%",
|
||||
muted: "217 33% 17%",
|
||||
accent: "217 33% 20%",
|
||||
destructive: "0 63% 31%",
|
||||
border: "217 33% 20%",
|
||||
input: "217 33% 20%",
|
||||
ring: "217 91% 60%",
|
||||
},
|
||||
} as const satisfies Record<"light" | "dark", ColorScheme>;
|
||||
4
packages/ui-tokens/src/index.ts
Normal file
4
packages/ui-tokens/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./colors.js";
|
||||
export * from "./typography.js";
|
||||
export * from "./spacing.js";
|
||||
export * from "./shadows.js";
|
||||
6
packages/ui-tokens/src/shadows.ts
Normal file
6
packages/ui-tokens/src/shadows.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const shadows = {
|
||||
sm: "0 1px 2px 0 hsl(220 23% 15% / 0.05)",
|
||||
md: "0 4px 6px -1px hsl(220 23% 15% / 0.1), 0 2px 4px -2px hsl(220 23% 15% / 0.1)",
|
||||
lg: "0 10px 15px -3px hsl(220 23% 15% / 0.1), 0 4px 6px -4px hsl(220 23% 15% / 0.1)",
|
||||
xl: "0 20px 25px -5px hsl(220 23% 15% / 0.1), 0 8px 10px -6px hsl(220 23% 15% / 0.1)",
|
||||
} as const;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user