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:
SpecialX
2026-07-10 12:58:22 +08:00
parent 2a2a56f541
commit faaaf29f67
120 changed files with 23201 additions and 2 deletions

View File

@@ -0,0 +1,384 @@
# 模块理解确认书 — ai
> AI 标识ai12复核与长期演进接手← ai06初稿2026-07-09
> 负责模块aiP5— 按 [ai-allocation.md §3.2](../../../docs/architecture/ai-allocation.md) v1.0 最新分配ai 由 ai12 专责
> 语言Python 3.12+ / FastAPI 0.115+
> 阶段:架构设计外包 · 阶段 1全局理解ai06 初稿 + ai12 长期演进复核)
> 日期2026-07-09ai06 初稿)/ 2026-07-09ai12 复核与补充)
> 关联文档:[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[coord 交叉审查报告](../../../docs/architecture/coord-cross-review.md)、[known-issues.md](../../../docs/troubleshooting/known-issues.md)
>
> **ai12 接手说明**:本确认书初稿由 ai06 在 ai-allocation v0.x 阶段ai06 同时负责 data-ana + ai撰写。ai-allocation.md v1.0 将 ai 模块独立分配给 ai12。ai12 在 ai06 初稿基础上执行作者归属更正、长期架构演进补充、P5 交付物覆盖度核对、向前兼容性铺垫。**所有 ai12 新增内容用「ai12 增补」标注**ai06 原文保留并标注「ai06 初稿」。
---
## 阶段 1 必读清单完成确认
按 ai-allocation.md §4ai12 必读 7 份全局文档 + 全部 `.proto` + `services/data-ana/``services/ai/` 骨架源码:
| # | 文档 | 状态 |
| --- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| 1 | [README.md](../../../README.md) | ✅ 已读 |
| 2 | [MIGRATION_GUIDE.md](../../../MIGRATION_GUIDE.md) | ✅ 已读 |
| 3 | [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) | ✅ 已读(重点 §1.2/§4.1/§4.2/§7.2/§9.3/§11.5/§12.2/§15 |
| 4 | [pending-features.md](../../../docs/architecture/roadmap/pending-features.md) | ✅ 已读(重点 P5 沟通与 AI 阶段) |
| 5 | [project_rules.md](../../../.trae/rules/project_rules.md) | ✅ 已读workspace 规则,重点 §3.6 Python/§6 事件驱动/§9 问题记录/§10 AI 工作流程) |
| 6 | [coding-standards.md](../../../docs/standards/coding-standards.md) | ✅ 已读(重点 §4 Python 规范) |
| 7 | [multi-ai-collaboration.md](../../../docs/standards/multi-ai-collaboration.md) | ✅ 已读 |
| 8 | `packages/shared-proto/proto/*.proto`8 份) | ✅ 已读(重点 ai.proto/analytics.proto/content.proto/events.proto |
| 9 | `services/ai/src/` 全部源码main.py/config.py/llm_client.py/health/health.py | ✅ 已读 |
| 10 | `services/data-ana/src/` 全部源码(参考 Python 服务实现模式 + 跨服务 gRPC 契约对齐) | ✅ 已读ai12 增补:跨服务 gRPC 契约对齐用) |
| 11 | [coord 交叉审查报告](../../../docs/architecture/coord-cross-review.md) | ✅ 已读ai12 增补:理解 7 项已仲裁共性问题) |
> ai12 与 ai06 不同ai-allocation.md v1.0 §4 矩阵 ai06 列对黄金模板为 "—"ai12 列同为 "—"Python 服务无需对照 TS 黄金模板),但需对照 `services/data-ana/` 作为 Python 服务实现参考lifespan/structlog/OTel/metrics 模式)。
---
## 1. 我在架构中的位置
### 1.1 架构定位ai06 初稿 + ai12 增补)
- **层级**业务微服务层L5**D6 智能洞察领域**004 §1.1b,与 data-ana 共享领域)
- **领域细分**ai12 增补D6 内部分两个子域
- **D6.a 分析子域**data-ana 负责):基于历史数据计算学情、掌握度、趋势
- **D6.b 生成子域**ai 负责):基于学情上下文 + LLM 推理生成教学内容
- 两子域通过 gRPC 同步查询耦合,构成"分析→生成"闭环
- **上游**
- api-gateway 直接 HTTP 代理 `/api/v1/ai/*` → ai:3008见 [api-gateway main.go](../../api-gateway/main.go)
- teacher-bff 聚合 AI 出题/优化能力004 §4.1BFF → aiP5 起转 gRPC
- **ai12 增补**student-bffP3+/ parent-bffP4+)未来可能调用 AI 服务用于学生自适应学习、家长学情报告 AI 解读
- **下游**
- content 服务gRPC 查询知识点 / 题库004 §4.1AI → Content gRPC
- data-ana 服务gRPC 查询学情数据004 §4.1AI → DataAna gRPC
- LLM Provider外部 HTTPOpenAI 兼容 REST API
- **ai12 增补**iam 服务gRPC 查询 `GetEffectiveDataScope`,用于多租户/多学校用量配额校验;当前 iam.proto 未提供此 RPCcoord 已仲裁 P4 补全)
- **ai12 增补**msg 服务Kafka 事件 `edu.notification.events` 由 msg 消费ai 不直接调用;但 AI 异常告警可能走 msg 推送给管理员,长期可考虑)
- **通信方式**
- 入口:当前 REST`/ai/chat``/ai/chat/stream``/ai/generate/question``/ai/optimize/expression`目标态ai.proto**gRPC** 暴露 `AiService`(含 `StreamChat` 流式 RPC004 §4.2 明确 P5 启用
- 出口gRPC 调 content / data-ana**当前未实现,仅 LLM HTTP 调用**SSE 流式对前端Kafka 事件外发用量计费
- **端口**3008HTTP+ 50058gRPC**ai12 增补**004 §1.2 gRPC 端口规则 `HTTP+10050`,需在 §1.2 端口矩阵补登,提请 coord 同步)
### 1.2 服务边界与无状态原则ai12 增补)
ai 是**严格无状态服务**,这与 data-ana 的"无 MySQL 写但有 ClickHouse 读模型"形成鲜明对比。无状态原则的含义与边界:
| 维度 | ai无状态 | data-ana有读模型 |
| ---------- | ----------------------------------------------------- | ----------------------------------------- |
| 持久化存储 | 无独占 DB | ClickHouse 宽表(读模型) |
| 会话状态 | 单次请求内(无跨请求会话,聊天历史由前端维护) | 无 |
| 缓存 | RedisPrompt 模板、限流计数、响应缓存) | RedisDataScope 缓存) |
| 事件发布 | `AIUsageRecorded`(派生数据,豁免 Outbox004 §12.2 | `MasteryUpdated`(派生数据,豁免 Outbox |
| 水平扩展 | 直接加副本,无状态同步问题 | CDC 消费者需 consumer group 协调 |
**无状态原则的例外**(需 coord 长期跟踪):
- 长运行工作流(备课工作流的"教师审核"步骤可能跨越数小时甚至数天的状态需持久化不能依赖内存。ai12 提请 coord 在 P6 评估引入 Temporal 或自建 workflow_state 表(详见 02-architecture-design.md §2.4
## 2. 我的限界上下文
### 2.1 聚合职责ai06 初稿 + ai12 增补)
- **聚合职责**LLM 调用网关 + 教学场景 AI 编排(备课 / 出题 / 表达优化),承载 **D6 智能洞察领域**的"生成"子域
- **聚合根 / 实体**
- `ChatConversation`(聊天会话,**待实现**,当前无状态)
- `GeneratedQuestion`(生成的题目,待审核入库)
- `PromptTemplate`Prompt 模板,**待实现**
- `UsageRecord`(用量计费记录,**待实现**,通过事件外发不落本服务 DB
- **ai12 增补**`LessonPreparationWorkflow`(备课工作流实例,长运行状态需持久化)
- **ai12 增补**`EvaluationResult`(生成结果质量评估结果,待实现,用于 RLHF 数据收集与质量门禁)
- **业务领域**D6 智能洞察(与 data-ana 共享data-ana 偏"分析"ai 偏"生成"
- **我不负责**
- 不负责题库存储(→ content 服务ai 生成后调 content.CreateQuestions 入库)
- 不负责学情计算(→ data-ana 服务ai 查询学情用于个性化出题)
- 不负责通知投递(→ msg 服务)
- 不负责用户身份认证(→ iam + Gatewayai 信任 `x-user-id`/`x-user-roles` 头)
- **ai12 增补**:不负责教学大纲管理(→ content 服务的 Textbook/Chapter/KnowledgePoint
- **ai12 增补**:不负责学生作业批改(→ core-eduai 仅生成题目与解析,不批改学生作答)
- **数据自治****无独占数据库**(设计上无状态;用量计费通过 Kafka 事件外发;长运行工作流状态待 P6 决策是否引入持久化)
### 2.2 ai12 增补:与 content / data-ana / msg 的边界精细化
为避免未来跨服务职责漂移,明确以下边界(参考 004 §1.1b D6/D4/D5 划分):
| 业务能力 | 归属服务 | 边界说明 |
| -------------------- | ---------- | ----------------------------------------------------------------- |
| 题目生成LLM 调用) | ai | ai 调 LLM 生成题目文本,返回给 BFF不入 content 题库 |
| 题目入库 | content | BFF 拿 ai 生成的题目 → 调 content.CreateQuestions 入库 |
| 知识点前置依赖查询 | content | ai 调 content.KnowledgeGraphService.GetPrerequisites |
| 学生学情查询 | data-ana | ai 调 data-ana.AnalyticsService.GetStudentWeakness |
| 班级整体学情查询 | data-ana | ai 调 data-ana.AnalyticsService.GetClassPerformance |
| 生成结果质量评估 | ai | ai 内部 LLM-as-judge / 规则校验,不外发 |
| 用量计费落库 | data-ana | ai 发 `edu.insight.ai.usage` 事件data-ana 消费落 ClickHouse |
| AI 异常告警通知 | msg | ai 异常 → 日志 + metrics告警由 Prometheus Alertmanager 触发 msg |
| 教师审核备课结果 | BFF + 前端 | ai 仅生成与暂存Redis 短期),审核状态由 BFF 编排 |
## 3. 我与外部的契约
### 3.1 消费的 proto messageai06 初稿 + ai12 增补)
| 来源 | message / service | 用途 | 状态 |
| --------------- | ---------------------------------------------------------- | ----------------------------------------- | ------------------------- |
| content.proto | `KnowledgeGraphService.GetPrerequisites` | 查询知识点前置依赖用于出题上下文 | proto 已定义 |
| content.proto | `KnowledgeGraphService.GetLearningPath` | 查询学生学习路径用于个性化出题 | proto 已定义 |
| content.proto | `TextbookService.ListTextbooks` | 查询教材上下文(章节关联知识点) | proto 已定义ai12 增补) |
| analytics.proto | `AnalyticsService.GetStudentWeakness` | 查询学生薄弱知识点用于靶向出题 | proto 已定义 |
| analytics.proto | `AnalyticsService.GetLearningTrend` | 查询学习趋势用于难度调节 | proto 已定义 |
| analytics.proto | `AnalyticsService.GetClassPerformance` | 查询班级整体学情用于备课工作流 | proto 已定义ai12 增补) |
| iam.proto | `IamService.GetEffectiveDataScope`**待 coord P4 补全** | 查询用户 DataScope 用于多学校用量配额校验 | proto 待补全ai12 增补) |
| ai.proto | `ChatRequest` 等 | gRPC 暴露契约(**待实现 gRPC server** | proto 已定义 |
### 3.2 暴露的 API / 事件
**HTTP 端点**(当前实现,见 [main.py](../src/ai/main.py)
| method | path | 说明 |
| ------ | ------------------------- | ---------------------------- |
| GET | `/healthz` | liveness |
| GET | `/readyz` | readiness含 LLM 是否配置) |
| GET | `/metrics` | Prometheus 指标 |
| POST | `/ai/chat` | LLM 聊天(非流式) |
| POST | `/ai/chat/stream` | LLM 流式聊天SSE |
| POST | `/ai/generate/question` | 生成题目 |
| POST | `/ai/optimize/expression` | 优化表达 |
**ai12 增补:待新增 HTTP 端点**P5 交付):
| method | path | 说明 |
| ------ | ------------------------------------- | ---------------------------------------------- |
| POST | `/ai/lesson/preparation` | 备课工作流启动(分析学情→推荐知识点→生成题目) |
| GET | `/ai/lesson/preparation/{id}` | 查询备课工作流状态(异步流程) |
| POST | `/ai/lesson/preparation/{id}/confirm` | 教师确认入库(触发 content.CreateQuestions |
| GET | `/ai/prompts` | Prompt 模板列表CRUD |
| POST | `/ai/prompts` | 创建 Prompt 模板 |
| PUT | `/ai/prompts/{id}` | 更新 Prompt 模板(版本化) |
| GET | `/ai/usage/me` | 当前用户用量统计(短期查询,长期查 data-ana |
**gRPC 契约**ai.proto**待实现 server**
- `Chat(ChatRequest) → ChatResponse`
- `StreamChat(ChatRequest) → stream ChatChunk`(流式 RPC
- `GenerateQuestion(GenerateQuestionRequest) → GeneratedQuestion`
- `OptimizeExpression(OptimizeExpressionRequest) → OptimizedExpression`
**ai12 增补proto 待补全项**(提请 coord 在 P5 前补全):
- `GenerateLessonPlan(GenerateLessonPlanRequest) → LessonPlanResponse`:备课工作流 RPC
- `StreamGenerateQuestion(...) → stream GeneratedQuestionChunk`:题目逐字流式生成(满足 ai-allocation.md §5 "题目逐字生成"
- `ChatRequest` 增加可选字段:`user_id`(透传,用于个性化)、`session_id`(用于多轮对话上下文)、`data_scope`(透传 DataScope 元数据)
- `GenerateQuestionRequest` 增加字段:`grade``knowledge_point_ids``question_type``count`
**ai12 增补:发布的领域事件**
- 当前无发布ai06 初稿)
- 设计上发布 `AIUsageRecorded` 事件(用量计费),由 data-ana 消费落 ClickHouse。**004 §7.2 已确认 topic `edu.insight.ai.usage`**coord 仲裁已通过§15.3 #4
- **ai12 增补:长期可发布事件**P6+ 评估):
- `AIContentGenerated`topic: `edu.insight.ai.generated`):生成内容审计事件,供 data-ana 落库用于质量分析
- `AIFeedbackRecorded`topic: `edu.insight.ai.feedback`):教师对生成结果的反馈(采纳/拒绝/修改),用于 RLHF 数据集构建
- **错误码前缀**`AI_*`(详见阶段 2 §6.2
- **降级策略**LLM API key 为空或调用失败时返回 `degraded: true` 骨架响应(见 [llm_client.py](../src/ai/llm_client.py)
### 3.3 ai12 增补:响应信封对齐
按 004 §11.5 强制约束coord 仲裁Python 服务信封必须改为 ActionState禁止 `{success, data, degraded}` 偏离结构):
- 当前 ai 实现违反约束main.py 返回 `{success: true, data: ..., degraded: false}`
- P5 实施必须改为 ActionState
- 成功:`{success: true, data: T}`
- 失败:`{success: false, error: {code, message, details?, traceId?}}`
- 降级:`details: {degraded: true}`degraded 作为 details 子字段,非顶层字段)
- 提请 coord 在 known-issues.md §2.9 ai-gateway 分区记录此约束
## 4. 我的技术栈
### 4.1 当前技术栈ai06 初稿)
- 语言Python 3.12+
- 框架FastAPI 0.115+ / uvicorn
- LLM 客户端:`httpx` 异步直接调 OpenAI 兼容 REST API**不依赖 openai SDK**,见 [llm_client.py:1-7](../src/ai/llm_client.py)
- 配置:`pydantic-settings` BaseSettingsenv_prefix=""
- 可观测:
- 日志:`structlog`
- 指标:`prometheus-client` + `make_asgi_app()`
- 链路:`opentelemetry-sdk` + `OTLPSpanExporter` + `FastAPIInstrumentor`dev_mode=true 时跳过 exporter 初始化避免本地无 collector 报错)
- 流式响应FastAPI `StreamingResponse` + `AsyncGenerator`SSE 格式 `data: <chunk>\n\n`
- 测试pytest**当前 0% 覆盖率**
### 4.2 ai12 增补:长期技术栈演进路线
为支撑 P5 交付物 + 未来多模态/RAG/Agent 演进,规划如下技术栈演进:
| 类别 | 当前 | P5M14-M16 | P6+(长期演进) | 选型理由 |
| ---------- | ---------- | --------------------------- | ------------------------------------------ | ---------------------------------------------------------- |
| LLM 客户端 | httpx 直连 | LLMProvider 抽象 + 4 适配器 | + Embedding Provider + Multimodal Provider | 抽象隔离 Provider API 差异,便于切换 |
| 工作流编排 | 无 | FastAPI BackgroundTasks | Temporal如 coord 仲裁引入) | 简单 4 步用 BackgroundTasks长运行教师审核需 Temporal |
| RAG 检索 | 无 | gRPC 调 content 查询 | + 向量库pgvector / Chroma+ Embedding | P5 复用 contentP6+ 评估独立向量库支持语义检索 |
| 模板管理 | 硬编码 | YAML 文件 + Jinja2 | + DB 持久化 + 版本化 + A/B 测试 | P5 用文件简单P6+ 评估迁移 DB 支持动态更新 |
| 缓存 | 无 | Redishash + 令牌桶) | + 语义缓存embedding 相似度) | P5 用 hash 缓存P6+ 评估语义缓存减少重复 LLM 调用 |
| 限流 | 无 | Redis 令牌桶(用户/IP | + 学校级配额 + token 级预算 | P5 用户/IP 级P6+ 多租户配额管理 |
| 评估 | 无 | 规则校验 + LLM-as-judge | + 人工评估 + 在线指标 + RLHF 数据集 | P5 自动化校验P6+ 闭环反馈持续提升质量 |
| 测试 | 0% | pytest ≥ 80% | + 集成测试 + 契约测试 + LLM 评估测试 | P5 单元 + 集成P6+ 评估测试保证生成质量 |
**关键选型决策记录**ai12 增补,供 coord 备案):
1. **不引入 openai SDK**:保持 httpx 直连,避免 SDK 版本绑定 + 减少 attack surfaceOpenAI 兼容 REST API 已成为事实标准(百川/Ollama/vLLM 均兼容)
2. **P5 不引入 LangChain**LangChain 抽象过重,与 LLMProvider 抽象层职责重叠;按 004 §2.3 列出的 LangGraph 待 P6+ 评估时再决定
3. **P5 不引入向量库**RAG 检索复用 content 服务的 Neo4j + EScontent 已规划 ES避免数据冗余
4. **不引入 openai SDK 但保留兼容性**:所有 LLM Provider 适配器实现 OpenAI 兼容 REST 接口,便于切换;非兼容 Provider如 Anthropic 原生 API在适配器内部转换
## 5. 我的阶段归属
### 5.1 阶段定位ai06 初稿 + ai12 增补)
- **P5 沟通与 AI 阶段M14-M16**:建 AI 网关 + LLM Provider 适配 + 流式 SSE + 用量计费
- **退出标准**pending-features P5
- AI 辅助出题流式返回
- 题库全文检索 < 200ms**注**ES 部分由 content 服务承担ai 不直接负责)
- AI 网关流式 SSE 三层透传AI → BFF → 前端)
- **ai12 增补细化**(基于 ai-allocation.md §5 ai12 交付清单):
- LLM Provider 适配器模式OpenAI/百川/本地模型,统一接口)✅
- SSE 流式响应(题目逐字生成 + 前端打字机效果)✅
- 出题 Prompt 模板管理(模板 CRUD + 参数注入:年级/学科/难度/知识点)✅
- 备课工作流编排(分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库)✅
- 用量计费/频率限制(按用户/按 IP/按 token 消耗)✅
- **依赖上游**
- P1 地基api-gateway 路由
- P2 身份iam 服务(鉴权 + DataScope
- P4 内容分析content 服务gRPC 查询知识点 / 题库)+ data-ana 服务gRPC 查询学情)
- 外部LLM Provider API keyOpenAI / Anthropic / 百川 / 本地模型 Ollama
- **下游依赖我**
- P5 teacher-bff 聚合 AI 出题 mutation004 §9.3:教师用 AI 出题并发布到班级)
- P5 teacher-portal SSE 流式 AI 对话(前端)
- **ai12 增补**P5+ student-bff学生自适应学习未来阶段
- **ai12 增补**P5+ parent-bff家长学情报告 AI 解读,未来阶段)
### 5.2 ai12 增补:阶段交付里程碑细分
将 P5M14-M163 个月)细分为 3 个里程碑,便于优先级排序:
| 里程碑 | 时间 | 交付内容 | 退出标准 |
| ------ | ----- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
| M14 | 第1月 | LLMProvider 抽象 + OpenAI/百川/Ollama 适配器 + gRPC serverChat/StreamChat+ Redis 限流 | gRPC 流式 RPC 可调通,多 Provider 切换可用 |
| M15 | 第2月 | Prompt 模板管理YAML + Jinja2+ 出题工作流 + 题目逐字流式生成 + 用量计费事件 | 教师可发起 AI 出题,流式返回,用量落 ClickHouse |
| M16 | 第3月 | 备课工作流 4 步编排 + 教师审核 + 入库 + 评估框架(规则 + LLM-as-judge | 备课工作流端到端跑通,质量门禁生效 |
## 6. 我需要对齐的黄金模板项(对照 classes 服务 + Python 规范)
### 6.1 对齐清单ai06 初稿 + ai12 增补)
- [ ] 权限装饰器等价物:**当前 HTTP 端点全部裸露,无权限校验**。阶段 2 需设计 FastAPI Depends 权限依赖AI 出题需 `AI_QUESTION_GENERATE` 权限,表达优化需 `AI_EXPRESSION_OPTIMIZE`
- [ ] 错误码前缀统一:**当前无错误码体系**,降级时返回 `degraded: true` 但无业务错误码。**阶段 2 需定义 `AI_*` 错误码清单**
- [x] logger / metrics / tracer 三支柱(已具备)
- [x] `/healthz` + `/readyz` 健康检查已具备readyz 含 LLM 配置状态)
- [ ] 优雅关闭 SIGTERM当前 lifespan 无显式 drainLLM 流式请求需等待完成)
- [ ] 测试覆盖率 ≥ 80%**当前 0%**,无 tests/ 目录
- [ ] Dockerfile 多阶段构建:**当前单阶段**`FROM python:3.12-slim`
- [ ] Pydantic 输入验证:**当前仅 `ChatRequest` 是 BaseModel`generate/question``optimize/expression` 直接接收 `prompt: str` / `text: str` query param无请求体模型**。阶段 2 需补完整 Pydantic 请求/响应模型
- [x] 配置通过 pydantic-settings 管理(已具备)
- [x] 异步优先httpx async + AsyncGenerator stream
- [x] 类型注解强制已基本符合main.py 函数返回值已标注)
- [ ] LLM Provider 适配器模式:**当前仅 OpenAI 兼容 REST**,未抽象 Provider 接口。阶段 2 需设计 `LLMProvider` 抽象 + OpenAI/Anthropic/百川/本地 多适配器
- [ ] Prompt 模板管理:**当前 Prompt 硬编码在 main.py**,阶段 2 需设计模板管理DB 或文件)
- [ ] 用量计费 / 频率限制:**当前无用量记录和限流**,阶段 2 需设计Kafka 事件 + Redis 限流)
- [ ] 备课工作流:**当前未实现**pending-features P5 要求"分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库"完整工作流
- [ ] ruff check 零错误:需阶段 2 验证
### 6.2 ai12 增补响应信封对齐P0 强制)
- [ ] **响应信封改为 ActionState**004 §11.5 强制约束)
- 当前违反:`{success: true, data: ..., degraded: false}` 顶层 degraded 字段
- 必须改为:`{success: true, data: ...}` + 失败时 `{success: false, error: {code, message, details: {degraded: true}, traceId}}`
- 影响:所有 HTTP 端点 + gRPC RPC 返回值
- 提请 coord 在 known-issues §2.9 记录
### 6.3 ai12 增补:长期架构对齐项
- [ ] **API 版本化策略**:当前无版本前缀,建议 P5 加 `/ai/v1/*` 前缀,便于未来破坏性变更
- [ ] **gRPC server interceptor 透传 trace context**:当前 HTTP 用 FastAPIInstrumentor 自动埋点gRPC server 需补 `grpc.aio.ServerInterceptor` 透传 W3C traceparent
- [ ] **下游 gRPC client interceptor**:调 content / data-ana 的 gRPC client 需补 client interceptor 注入 trace context + 失败重试 + 熔断
- [ ] **PII 脱敏**:学生姓名、家长手机号等 PII 数据在写入 prompt 前必须脱敏(防止泄露给 LLM Provider
- [ ] **Prompt 注入防御**:用户输入的 prompt 必须经 sanitize去除 SQL/HTML/JS 注入模式)后再拼入 system prompt
- [ ] **输出内容审核**LLM 输出必须经敏感词过滤 + 安全校验后再返回(防止生成不当内容)
- [ ] **生成结果质量门禁**:题目生成必须经规则校验(题型匹配/答案非空/解析合理)+ LLM-as-judge质量评分不达标重新生成或降级
- [ ] **多租户用量配额**:按学校/年级/教师维度配置 token 预算,超额拒绝调用(避免单一学校耗尽预算)
---
## 服务审计表 — ai12ai 部分)
> 对照 ai-allocation.md §10 审计模板。ai 为 Python/FastAPI黄金模板对齐按 Python 规范coding-standards §4评估。
> 符号说明:✅ 已实现 | ❌ 缺失 | ⚠️ 部分实现 | 📋 P5 待实现 | 🔮 P6+ 长期演进
| 服务 | 权限校验 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile | 响应信封 | gRPC server | Provider 适配 | Prompt 模板 | 限流/计费 | 备课工作流 | PII 脱敏 | 质量门禁 |
| ---- | -------- | ---------- | ------------ | ------------- | ------------------- | -------- | ----------------- | ------------------- | ---------- | ---------- | -------- | ----------- | ------------- | ----------- | --------- | ---------- | -------- | -------- |
| ai | ❌ | ❌ | ✅ structlog | ✅ prometheus | ✅ OTeldev 跳过) | ✅ | ✅(含 LLM 状态) | ⚠️ 仅 lifespan 关闭 | 0% | ❌ 单阶段 | ❌ 偏离 | ❌ 待实现 | ❌ 待实现 | ❌ 硬编码 | ❌ 待实现 | ❌ 待实现 | ❌ 缺失 | ❌ 缺失 |
### 审计发现的关键差距(按优先级)
#### P0 阻塞 P5 交付ai06 初稿 + ai12 增补)
1. **P0 权限校验缺失**:所有 `/ai/*` 端点裸露AI 出题等敏感操作无权限校验
2. **P0 gRPC server 未实现**ai.proto 定义了 `AiService`(含 `StreamChat` 流式 RPC但 ai 当前仅 HTTP。阶段 2 需引入 `grpc.aio` 实现 gRPC server + 流式 RPC
3. **P0 gRPC client 未实现**004 §4.1 明确 AI → Content / AI → DataAna 走 gRPC当前未实现。阶段 2 需设计 gRPC client 调用 content / data-ana
4. **P0 响应信封偏离 ActionState**ai12 增补004 §11.5 强制约束,当前违反
5. **P0 LLM Provider 适配器缺失**:当前 `llm_client.py` 仅 OpenAI 兼容 REST未抽象 Provider 接口。pending-features P5 + ai-allocation.md §5 要求"LLM Provider 适配OpenAI/百川/本地模型)"
6. **P0 Prompt 模板管理缺失**system prompt 硬编码在 main.py无模板管理。ai-allocation.md §5 要求"模板 CRUD + 参数注入:年级/学科/难度/知识点"
7. **P0 备课工作流缺失**pending-features P5 + ai-allocation.md §5 要求"分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库"完整工作流,当前仅"生成题目"单步
8. **P0 用量计费 / 频率限制缺失**无用量记录token 消耗)、无频率限制(用户可无限调用 LLM。ai-allocation.md §5 要求"按用户/按 IP/按 token 消耗"
#### P1 P5 阶段需补齐ai12 增补)
9. **P1 题目逐字流式生成缺失**:当前 `/ai/chat/stream` 仅支持聊天流式未实现题目生成的逐字流式ai-allocation.md §5 "题目逐字生成 + 前端打字机效果"
10. **P1 Prompt 模板 CRUD API 缺失**ai-allocation.md §5 要求"模板 CRUD",当前无 API
11. **P1 多维度限流缺失**ai-allocation.md §5 要求"按用户/按 IP/按 token 消耗"三维度,当前仅设计用户级
12. **P1 测试覆盖率 0%**:无 tests/ 目录
13. **P1 Dockerfile 单阶段**:未做多阶段构建
14. **P1 Pydantic 输入验证不完整**`generate/question``optimize/expression` 直接接收 query param无请求体模型
#### P2 P6+ 长期演进ai12 增补)
15. **P2 PII 脱敏缺失**:学生姓名/家长手机号等写入 prompt 前未脱敏,存在数据泄露风险
16. **P2 Prompt 注入防御缺失**:用户输入未 sanitize存在 SQL/HTML 注入风险
17. **P2 输出内容审核缺失**LLM 输出未过滤敏感词,存在生成不当内容风险
18. **P2 生成结果质量门禁缺失**:题目未校验题型/答案/解析合理性,可能生成不可用题目
19. **P2 多租户用量配额缺失**:无学校/年级级配额,单一学校可能耗尽预算
20. **P2 评估框架缺失**:无 LLM-as-judge / 人工评估机制,无法持续提升生成质量
21. **P2 RAG 语义检索缺失**:当前仅基于 content 知识点查询,无向量库语义检索
22. **P2 多模态支持缺失**:未规划图片/音频题目生成(数学几何题/英语听力题)
23. **P2 语义缓存缺失**:仅 hash 缓存,无基于 embedding 相似度的语义缓存
24. **P2 A/B 测试基础设施缺失**:无法对比不同 prompt/模型的效果
25. **P2 RLHF 数据收集缺失**:无教师反馈(采纳/拒绝/修改)记录机制
26. **P2 长运行工作流持久化缺失**:备课工作流的"教师审核"步骤可能跨数小时/天,无持久化机制
---
## 阶段 1 待 coord 交叉审查的跨模块契约对齐项
### ai06 初稿提请项(已在 coord 交叉审查报告中仲裁)
| # | 议题 | 涉及方 | 仲裁结论 |
| --- | ----------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2 | **ai 是否发布 `edu.insight.ai.usage` 用量事件** | ai → data-ana | ✅ 已仲裁coord 补登 topic见 004 §7.2events.proto 待补 `AIUsageEvent` messagecoord 负责) |
| 3 | **data-ana / ai 是否需要实现 gRPC server** | data-ana / ai ← teacher-bff / student-bff / ai | ✅ 已仲裁:分阶段启用,见 004 §4.2 gRPC 启用阶段矩阵ai 在 P5 启用(含 StreamChat |
| 6 | **ai 备课工作流是否引入 Temporal** | ai + coord | ⚠️ 部分仲裁004 §2.3 列出 Temporal 用于"工作流编排考试生命周期、AI 编排)"P3 已引入试点ai12 建议简单工作流用 BackgroundTasks长运行评估 Temporal待 P6 决策 |
| 7 | **LLM Provider 切换的配置化** | ai + coord | ⚠️ 部分仲裁ai12 设计 `LLMProvider` 抽象 + 配置化路由(按 model 名路由);本地模型走 Ollama REST API待 P6 评估是否提取到 shared-py |
### ai12 新增提请项(待 coord 仲裁)
| # | 议题 | 涉及方 | ai12 建议 |
| --- | ----------------------------------------------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------- |
| A1 | **ai gRPC 端口 50058**004 §1.2 端口矩阵需补登 | ai + coord | ai HTTP 3008 + gRPC 50058HTTP+10050 规则);提请 coord 同步 004 §1.2 + 全局端口表 `infra/port-allocation.md` |
| A2 | **ai.proto 待补全**:备课工作流 RPC + 题目逐字流式 RPC + ChatRequest/GenerateQuestionRequest 字段扩展 | ai + coord | 见 §3.2 ai12 增补待补全项;建议 coord 在 P5 启动前完成 proto 升级到 v1 完整版 |
| A3 | **events.proto 待补 `AIUsageEvent` message**004 §7.2 已确认 topic 但 proto 未定义 | ai + coord + data-ana | 协调 data-ana ai11 共同定义 payload schemaai12 已在 02-architecture-design.md §5.2 给出建议 schema |
| A4 | **iam `GetEffectiveDataScope` gRPC RPC**ai 需查询用户 DataScope 用于多租户配额校验 | ai + coord + iam | coord 已仲裁 P4 补全§15.3 #5ai12 在 P5 实施时调用 |
| A5 | **响应信封偏离 ActionState 强制整改**004 §11.5 已约束ai 当前违反 | ai + coord | P5 实施时必须整改;提请 coord 在 known-issues §2.9 记录 |
| A6 | **Prompt 模板存储位置**YAML 文件 vs DB | ai + coord | ai12 建议 P5 用 YAML 文件(`services/ai/src/ai/prompts/*.yaml`P6+ 评估迁移 DB与 ai06 初稿一致 |
| A7 | **长运行工作流持久化**:备课工作流"教师审核"步骤可能跨数小时/天 | ai + coord | ai12 建议 P5 用 Redis 暂存(短期 24h TTL+ BFF 编排审核状态P6 评估迁移 Temporal 或自建 workflow_state 表 |
| A8 | **多模态支持规划**:图片/音频题目生成(数学几何/英语听力) | ai + coord | ai12 建议 P5 仅文本P6+ 评估多模态proto v2 预留 `multimodal_content` 字段 |
| A9 | **评估框架归属**LLM-as-judge 在 ai 内部 vs 独立 evaluation 服务 | ai + coord | ai12 建议 P5/P6 在 ai 内部实现避免服务拆分过早P7+ 评估独立 |
| A10 | **RLHF 数据收集**:教师反馈事件 `AIFeedbackRecorded` 是否新增 topic | ai + coord + data-ana | ai12 建议 P6+ 评估topic: `edu.insight.ai.feedback`data-ana 落 ClickHouse 供未来 fine-tune |
---
**AI Agent**: ai12 (ai) ← ai06初稿
**Branch**: 单仓库并行模式(直接提交 main
**Coordinator**: coord-ai

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
# 模块理解确认书 — classes
> AIai07TS / 教学组织 · 黄金模板)
> 阶段:阶段 1 交付物
> 日期2026-07-10
> 关联:[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[AI 分配方案](../../../docs/architecture/ai-allocation.md)、[pending-features P1](../../../docs/architecture/roadmap/pending-features.md)、[项目规则](../../../.trae/rules/project_rules.md)
>
> **本确认书定位**classes 是 Edu 微服务的 **黄金模板**ai-allocation.md §3.4),是其余 8 个 TS 服务的对照基准。本确认书既是对 classes 现状的审计,也是为其他 AI 提供的"读这一份即可理解黄金模板全貌"的入口。
---
## 1. 我在架构中的位置
- **层级**L5 业务微服务层004 §3.1 六层架构)
- **业务领域****D2 教学组织领域**004 §1.1b承载班级Class聚合根
- **上游(谁调用我)**
- `api-gateway`Go/GinHTTP 反向代理 `/api/v1/classes/*``http://classes:3001/classes/*`Gateway 已注册 classes 路由块,含精确路径 + 通配路径)
- `teacher-bff`NestJSP2 起通过 GraphQL 聚合调用 classes 班级列表(教师仪表盘)
- 未来:`student-bff``parent-bff`(同样走 BFF 聚合)
- **下游(我调用谁)**
- `MySQL`(独占库 `classes_db`,连接串 `DATABASE_URL`mysql2 连接池 + Drizzle ORM
- `Redis`(依赖已声明 `ioredis`env 已留 `REDIS_URL` 占位当前未启用P3 合并入 core-edu 后启用班级列表缓存)
- `Kafka`(依赖已声明 `kafkajs`当前未启用P3 起 Outbox 事件发布 `edu.org.class.created` 等)
- **通信方式**
- 入口:**HTTP/REST**(当前实现,端口 3001
- 出口MySQLDrizzle ORM
- 演进proto 已定义 `ClassService`5 个 RPCP3 起 core-edu 合并 classes 后启用 gRPC server 50053
- **不持有跨服务状态**:无本地内存缓存,无 WebSocket 连接
### 1.1 黄金模板职责
classes 除了承载 D2 教学组织领域(班级 CRUD业务外**首要职责是作为黄金模板**
| 黄金模板职责 | 说明 |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| 横切关注点基准 | 错误处理 / 可观测性 / 安全 / 契约 / 测试 / 文档 / 配置 / Dockerfile 全部落地,其他 TS 服务对齐 |
| 目录结构基准 | `src/{main,app.module,config,middleware,<domain>,shared}` 标准结构 |
| 对照 checklist 源 | 产出黄金模板对照 checklist见 [02 架构设计 §10](./02-architecture-design.md#10-黄金模板对照-checklist供其他-ts-服务自检))供 ai01-ai10 自检 |
| 模式回写 | 各阶段新模式实现后回写本模板Outbox 模式 P3 / 长连接 P5 / CDC P4 |
---
## 2. 我的限界上下文
### 2.1 我负责的聚合 / 实体
| 聚合根 | 实体 / 值对象 | 当前表 | 职责 |
| ----------------- | ----------------------------------------- | --------- | --------------------------------- |
| **Class**(班级) | name、gradeId、headTeacherId、description | `classes` | 班级 CRUD、按年级过滤、班主任归属 |
> **演进**P3 合并入 core-edu 后classes 表与 `subjects`(学科)、`enrollment`(选课)同属 D2 教学组织域,由 core-edu 统一承载(见 004 §1.3 CICD→Edu 映射)。
### 2.2 业务领域归属
- **D2 教学组织领域**004 §1.1bclasses 独占此领域当前P3 起与 subjects、enrollment 共同组成 D2
- 上游依赖:无业务领域依赖(教学组织是基础数据,不依赖其他业务领域)
- 下游被依赖:
- core-edu D3 教学核心域(考试/作业按班级归属)
- data-ana D6 智能洞察域(班级维度统计聚合)
- msg D5 沟通通知域(班级群组广播)
### 2.3 我不负责什么(边界外)
- ❌ 学科subjects→ core-eduP3 新增D2 教学组织)
- ❌ 选课 / 学生分班enrollment→ core-eduP3 新增)
- ❌ 考试 / 作业 / 成绩 → core-eduD3 教学核心)
- ❌ 教材 / 知识点 / 题库 → contentD4 内容资源)
- ❌ 站内信 / 通知投递 → msgD5 沟通通知)
- ❌ 学情分析 / 掌握度计算 → data-anaD6 智能洞察)
- ❌ 用户 / 角色 / 权限管理 → iamD1 身份认证)
- ❌ JWT 校验 → api-gatewayclasses 仅读 `x-user-id` / `x-user-roles` 头)
- ❌ 年级grade数据 → 当前 classes 仅持有 `gradeId` 外键引用,年级实体归属待 core-edu P3 定义(当前由前端/管理端维护)
---
## 3. 我与外部的契约
### 3.1 我消费的 proto message从 shared-proto
- 当前**不消费**任何外部 protoclasses 是基础数据源,不反向依赖业务服务)
- 未来 P3 起core-edu 合并 classes 后,消费 `iam.proto``BatchGetUsers`(班主任信息批量查询)
### 3.2 我暴露的契约
#### 当前 REST API已实现见 [classes.controller.ts](../src/classes/classes.controller.ts)
| Method | Path | 权限 | 请求体 / 参数 | 响应 | 说明 |
| ------ | -------------- | ---------------- | -------------------------- | ---------------------------------- | ---------------------------------- |
| POST | `/classes` | `CLASSES_CREATE` | `{name, gradeId, ...}` | `{success, data: ClassResponse}` | 创建班级 |
| GET | `/classes` | `CLASSES_READ` | `?gradeId=<uuid>`(可选) | `{success, data: ClassResponse[]}` | 列表,支持按年级过滤 |
| GET | `/classes/:id` | `CLASSES_READ` | path: `id` | `{success, data: ClassResponse}` | 单条查询 |
| PUT | `/classes/:id` | `CLASSES_UPDATE` | path:`id` + `{name?, ...}` | `{success, data: ClassResponse}` | 更新(空 body 抛 ValidationError |
| DELETE | `/classes/:id` | `CLASSES_DELETE` | path: `id` | `{success: true}` | 删除(先校验存在) |
> **响应信封**:遵循 ActionState004 §11.5),成功 `{success:true, data:T}`,失败 `{success:false, error:{code,message,details?,traceId?}}`。
#### 健康检查(见 [health.controller.ts](../src/shared/health/health.controller.ts)
| Method | Path | 鉴权 | 用途 |
| ------ | ---------- | ---- | ------------------------------------------------------------------- |
| GET | `/healthz` | 无 | liveness进程存活 |
| GET | `/readyz` | 无 | readiness校验 DB `SELECT 1`,失败 503 |
| GET | `/metrics` | 无 | Prometheus 指标抓取(见 [main.ts](../src/main.ts) 第 23-26 行注册) |
#### Proto 契约(见 [classes.proto](../../../packages/shared-proto/proto/classes.proto)
```protobuf
package next_edu_cloud.classes.v1;
service ClassService {
rpc CreateClass(CreateClassRequest) returns (Class);
rpc GetClass(GetClassRequest) returns (Class);
rpc ListClasses(ListClassesRequest) returns (ListClassesResponse);
rpc UpdateClass(UpdateClassRequest) returns (Class);
rpc DeleteClass(DeleteClassRequest) returns (Empty);
}
```
> **proto 完备性**5 个 RPC 与 REST 端点一一对应,已包含分页字段(`page_size` / `page_token`REST 实现尚未落地分页。P3 合并后由 core-edu 启用 gRPC server 50053004 §16.7.3 #1
#### 我发布的领域事件当前未实现P3 由 core-edu 接管)
| 事件 | 触发时机 | Topic遵循 004 §7.2 | 消费者 |
| ------------------ | ------------ | --------------------------- | -------------------------------- |
| `ClassCreated` | 班级创建成功 | `edu.org.class.created` | data-ana建班级维度宽表行 |
| `ClassUpdated` | 班级信息变更 | `edu.org.class.updated` | data-ana、msg班主任变更通知 |
| `ClassDeleted` | 班级删除 | `edu.org.class.deleted` | data-ana、core-edu关联检查 |
| `ClassTransferred` | 班主任变更 | `edu.org.class.transferred` | msg通知新/旧班主任、data-ana |
> **事件 message 契约**`events.proto` 已定义 `ClassEvent`event_id / aggregate_id / event_type / occurred_at / class_id / name / action / metadata见 [events.proto](../../../packages/shared-proto/proto/events.proto) 第 15-24 行。Topic 命名遵循 004 §7.2 `edu.<domain>.<aggregate>.<action>` 强制格式。
---
## 4. 我的技术栈
| 维度 | 选型 | 说明 |
| -------- | --------------------------------- | --------------------------------------------------------------- |
| 语言 | TypeScript 5.6+ | ESM 模式(`"type": "module"`),相对 import 用 `.js` 后缀 |
| 框架 | NestJS 10 | 装饰器 + DI适合 DDD 分层 |
| ORM | Drizzle ORM 0.31 | 参数化查询,类型安全,替代 TypeORM |
| 数据库 | MySQL 8mysql2 驱动) | 连接池 `connectionLimit: 10` |
| 校验 | Zod 3.23 | 输入 DTO 校验controller 层 `schema.parse(body)` |
| 日志 | pino 9 + pino-prettydev | 结构化 JSON`base:{service,version}` |
| 指标 | prom-client 15 | 自定义 Counter/Histogram + 默认进程指标,`/metrics` 端点 |
| 链路 | OpenTelemetry SDK + OTLP exporter | auto-instrumentationsserviceName=`classes` |
| 测试 | Vitest 2.1 + @vitest/coverage-v8 | 单元测试mock repository阈值 60%(待提升至 80% |
| 容器 | node:20-alpine多阶段构建 | builder + runtime 两阶段(项目规则要求 node:22-alpine待对齐 |
| ID 生成 | uuid v4 | 当前实现project_rules 要求 cuid2黄金模板待迁移 |
| 预留依赖 | ioredis、kafkajs | 已声明但未使用,为 P3 Redis 缓存 + Kafka 事件预留 |
---
## 5. 我的阶段归属
- **阶段****P1 地基阶段**(已实现,黄金模板)
- **当前阶段目标**pending-features P1classes 域 CRUD 端到端跑通 + 全横切关注点落地 + 30 分钟可复制新服务
- **依赖上游阶段产出**P1 是地基classes 是最早交付的服务)
- **未来演进路径**
- P3合并入 core-eduai08 负责classes 表 + subjects + enrollment 共同组成 D2 教学组织域;启用 gRPC server 50053
- P3补齐 Outbox 事件发布能力(`shared/outbox/` 目录)
- P3启用 Redis 班级列表缓存TTL 5 分钟,事件驱动失效,见 004 §6.3
- P3落地 DataScope 6 级 WHERE 注入(见 004 §5.3,当前 PermissionGuard 仅做角色级校验)
---
## 6. 我需要对齐的黄金模板项(对照 classes 自身——作为基准的自我审计)
> classes 是黄金模板本身,本节为**自我审计**,确认各项已落地,供其他 TS 服务对照。
| 检查项 | 状态 | 证据 | 待整改 |
| ------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------ | ---------- |
| 权限装饰器 `@RequirePermission` | ✅ | [permission.guard.ts](../src/middleware/permission.guard.ts) + controller 全部方法装饰 | — |
| 错误码前缀 `CLASSES_` | ✅ | [application-error.ts](../src/shared/errors/application-error.ts) 7 类错误码均带前缀 | — |
| loggerpino | ✅ | [logger.ts](../src/shared/observability/logger.ts) | — |
| metricsprom-client | ✅ | [metrics.ts](../src/shared/observability/metrics.ts) + `/metrics` 端点 | — |
| tracerOTel | ⚠️ | [tracer.ts](../src/shared/observability/tracer.ts) 第 20 行仍用 `console.log`,需改 `logger.info` | P1 整改 |
| `/healthz` | ✅ | [health.controller.ts](../src/shared/health/health.controller.ts) liveness | — |
| `/readyz` | ✅ | readiness 校验 DB `SELECT 1`,失败 503 | — |
| 优雅关闭SIGTERM | ✅ | [main.ts](../src/main.ts) 第 31-34 行 + [lifecycle.service.ts](../src/shared/lifecycle/lifecycle.service.ts) | — |
| Zod 输入验证 | ✅ | [classes.dto.ts](../src/classes/classes.dto.ts) + controller `schema.parse(body)` | — |
| GlobalErrorFilter | ✅ | [global-error.filter.ts](../src/shared/errors/global-error.filter.ts) 捕获 ZodError + ApplicationError | — |
| Dockerfile 多阶段 | ✅ | [Dockerfile](../Dockerfile) builder + runtime 两阶段 | node:20→22 |
| 测试覆盖率 | ⚠️ | [vitest.config.ts](../vitest.config.ts) 阈值 60%,需提升至 80% | P1 整改 |
| `shared/outbox/` 目录 | ❌ | 缺失,需补齐事件发布能力 | P3 补齐 |
| `typeorm` 冗余依赖 | ❌ | [package.json](../package.json) 第 33 行仍声明 typeorm已用 Drizzle需移除 | P1 整改 |
| `@Req()``@Query()` | ❌ | [classes.controller.ts](../src/classes/classes.controller.ts) 第 49-53 行 list 方法仍用 `@Req()` | P1 整改 |
| ID 生成 cuid2 | ❌ | [classes.service.ts](../src/classes/classes.service.ts) 第 1 行用 uuid v4 | P3 迁移 |
> **整改项汇总**4 项 P1 立即整改tracer console.log / 测试覆盖率 / typeorm 移除 / @Req 替换)+ 2 项 P3 演进outbox / cuid2详见 [02 架构设计 §11 整改清单](./02-architecture-design.md#11-整改清单与里程碑)。
---
## 7. 待 coord 仲裁 / 提请事项
| # | 议题 | 我的倾向 | 影响 |
| --- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------- | ------------------------------------------- |
| 1 | classes 在 P3 合并入 core-edu 后,黄金模板源码是否保留独立目录 `services/classes/` | 保留作为只读对照基准core-edu 复制其结构) | 影响 ai08 合并策略与黄金模板 checklist 归属 |
| 2 | `node:20-alpine``node:22-alpine` 升级是否在 P1 立即做? | 立即做project_rules §15.8 镜像预拉清单已是 node:22-alpine | 影响所有 TS 服务 Dockerfile 基准 |
| 3 | cuid2 迁移时机P1 黄金模板立即迁移,还是 P3 合并时统一迁移? | P1 立即迁移(黄金模板应先行,避免 ai08 继承 uuid 遗留) | 影响 classes 表主键、proto message 字段类型 |

View File

@@ -0,0 +1,709 @@
# 模块架构设计文档 — classes
> AIai07TS / 教学组织 · 黄金模板)
> 阶段:阶段 2 交付物
> 日期2026-07-10
> 关联:[阶段 1 理解确认书](./01-understanding.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features](../../../docs/architecture/roadmap/pending-features.md)、[项目规则](../../../.trae/rules/project_rules.md)
> 状态:待 coord 交叉审查
>
> **设计原则**classes 是黄金模板,本架构设计同时服务于两个目标:
>
> 1. **当前目标**D2 教学组织域班级 CRUD 端到端跑通 + 全横切关注点落地 + 30 分钟可复制新服务
> 2. **长远目标**:为 P3 合并入 core-edu 做无痕准备 + 为其余 8 个 TS 服务输出对照标准 + 为未来事件驱动 / gRPC / DataScope / 缓存 / 多租户 / 软删除等演进预留接口
>
> **架构通用标准对齐**:本设计遵循 IEEE/ThoughtWorks/Stitch 架构评估维度——功能正确性、可维护性、可测试性、可扩展性、可观测性、安全性、可部署性、容错性,每个设计决策均标注其服务的标准维度。
---
## 目录
1. [模块内部分层图](#1-模块内部分层图)
2. [领域模型](#2-领域模型)
3. [数据模型](#3-数据模型)
4. [API 设计](#4-api-设计)
5. [事件设计](#5-事件设计)
6. [横切关注点对齐清单](#6-横切关注点对齐清单)
7. [与其他模块的交互点(契约清单)](#7-与其他模块的交互点契约清单)
8. [风险与假设](#8-风险与假设)
9. [长远架构与未来铺垫](#9-长远架构与未来铺垫)
10. [黄金模板对照 checklist供其他 TS 服务自检)](#10-黄金模板对照-checklist供其他-ts-服务自检)
11. [整改清单与里程碑](#11-整改清单与里程碑)
12. [AI 身份标注](#12-ai-身份标注)
---
## 1. 模块内部分层图
### 1.1 调用链总览
```mermaid
flowchart TB
subgraph Client["客户端 / Gateway / BFF"]
Req[HTTP 请求<br/>带 x-user-id / x-user-roles 头]
end
subgraph NestJS["classes 服务NestJS"]
direction TB
MW[AuthMiddleware<br/>⚠️ 已定义未注册P3 接入]
Guard[PermissionGuard<br/>APP_GUARD 全局守卫]
Filter[GlobalErrorFilter<br/>全局异常过滤器]
subgraph Controllers["Controller 层"]
ClassCtl[ClassesController<br/>/classes CRUD]
HealthCtl[HealthController<br/>/healthz /readyz]
end
subgraph Services["Application Service 层"]
ClassSvc[ClassesService<br/>班级领域编排 + 业务校验]
end
subgraph Repo["Repository 层"]
ClassRepo[ClassesRepository<br/>Drizzle 查询]
end
subgraph Shared["Shared 横切"]
Logger[pino logger]
Metrics[prom-client registry]
Tracer[OTel SDK]
Errors[ApplicationError 体系]
end
subgraph Outbox["Outbox 模块P3 补齐)"]
OutboxTbl[(classes_outbox 表)]
Relay[OutboxRelayWorker<br/>轮询 + Kafka 投递]
end
subgraph Infra["基础设施"]
Db[(MySQL<br/>classes_db)]
Redis[(Redis<br/>P3 班级列表缓存)]
Kafka[(Kafka<br/>edu.org.class.* topic)]
end
end
Req --> Guard
Guard --> ClassCtl
ClassCtl --> ClassSvc
ClassSvc --> ClassRepo
ClassRepo --> Db
ClassSvc -.P3.-> OutboxTbl
OutboxTbl --> Relay
Relay --> Kafka
Filter -.捕获异常.-> ClassCtl
```
### 1.2 分层职责
| 层 | 文件 | 职责 | 依赖方向 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------ |
| Controller | [classes.controller.ts](../src/classes/classes.controller.ts) | HTTP 入口、Zod 校验、权限声明、响应信封封装 | → Service |
| Service | [classes.service.ts](../src/classes/classes.service.ts) | 领域编排、业务规则校验(空 body、存在性检查、ID 生成 | → Repository |
| Repository | [classes.repository.ts](../src/classes/classes.repository.ts) | Drizzle ORM 数据访问、参数化查询 | → DB |
| Middleware | [auth.middleware.ts](../src/middleware/auth.middleware.ts) / [permission.guard.ts](../src/middleware/permission.guard.ts) | 身份解析(读 header、权限校验APP_GUARD 全局) | 横切 |
| Shared | shared/observability、shared/errors、shared/health、shared/lifecycle | 可观测三支柱、错误体系、健康检查、优雅关闭 | 横切 |
| Config | [env.ts](../src/config/env.ts) / [database.ts](../src/config/database.ts) | Zod 环境校验、Drizzle 连接池 | 基础设施 |
> **依赖方向单向**Controller → Service → Repository → DB禁止反向依赖project_rules §3.2)。
### 1.3 中间件 / Guard / Filter 拦截顺序
```mermaid
sequenceDiagram
participant GW as API Gateway
participant MW as AuthMiddleware
participant Guard as PermissionGuard
participant Ctl as Controller
participant Svc as Service
participant Repo as Repository
participant DB as MySQL
participant Filter as GlobalErrorFilter
GW->>MW: HTTP + x-user-id/roles 头
Note over MW: 当前未注册Guard 直接读 header
MW->>Guard: req.userId / req.userRoles
Guard->>Guard: DEV_MODE=true 跳过<br/>否则按 ROLE_PERMISSIONS 校验
Guard->>Ctl: 通过 / 抛 PermissionDeniedError
Ctl->>Ctl: Zod schema.parse(body)
Ctl->>Svc: 调用 Service
Svc->>Repo: 调用 Repository
Repo->>DB: Drizzle 查询
DB-->>Repo: 结果
Repo-->>Svc: Class / undefined
Svc-->>Ctl: Class / 抛 NotFoundError
Ctl-->>GW: {success:true, data}
Note over Filter: 任意层抛错被 GlobalErrorFilter 捕获
Filter-->>GW: {success:false, error:{code,message,traceId}}
```
---
## 2. 领域模型
### 2.1 聚合根与实体
classes 当前是**轻量领域模型**CRUD 为主,无复杂业务规则),但为 P3 合并后承载更多业务预留扩展点。
```mermaid
graph LR
subgraph ClassAggregate["Class 聚合根"]
Class[Class<br/>id, name, gradeId,<br/>headTeacherId, description]
ClassStatus[ClassStatus<br/>值对象: active/archived/graduated]
end
subgraph 未来扩展["P3+ 合并后扩展core-edu 承载)"]
Subject[Subject 聚合<br/>学科]
Enrollment[Enrollment 聚合<br/>选课/分班]
StudentRoster[StudentRoster<br/>值对象: 班级花名册]
end
Class -.归属.-> Subject
Class -.包含.-> StudentRoster
StudentRoster -.关联.-> Enrollment
```
| 聚合根 | 字段 | 业务不变量(当前 + 预留) |
| --------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Class** | id, name, gradeId, headTeacherId, description, createdAt, updatedAt | 当前name 非空、gradeId 必填、headTeacherId 可空预留status 状态机active→archived→graduated、capacity 上限、schoolId 多租户 |
### 2.2 聚合间通信
- **当前**classes 仅一个聚合,无聚合间通信
- **P3 合并后**Class 与 Subject、Enrollment 同属 core-edu**同服务内直接调用**(非跨服务事件)
- **跨服务**Class 变更通过 Kafka 事件通知 data-ana / msg见 §5
### 2.3 领域服务(预留)
当前无独立领域服务ClassesService 兼任应用服务 + 领域编排。P3 合并后建议拆分:
| 服务 | 职责 | 触发时机 |
| ------------------ | ------------------------------------------ | ------------------- |
| ClassDomainService | 班级状态机转换、容量校验、班主任变更合法性 | P3 业务规则复杂化时 |
---
## 3. 数据模型
### 3.1 表结构(当前)
`classes` 表(见 [classes.schema.ts](../src/classes/classes.schema.ts)
| 字段 | 类型 | 约束 | 说明 |
| --------------- | ------------ | -------------------------------------- | -------------------------- |
| id | char(36) | PK, NOT NULL | 当前 uuid v4待迁移 cuid2 |
| name | varchar(100) | NOT NULL | 班级名称 |
| grade_id | char(36) | NOT NULL | 年级外键(年级表归属待定) |
| head_teacher_id | char(36) | NULL | 班主任外键iam user |
| description | text | NULL | 描述 |
| created_at | timestamp | NOT NULL DEFAULT NOW() | 创建时间 |
| updated_at | timestamp | NOT NULL DEFAULT NOW() ON UPDATE NOW() | 更新时间 |
### 3.2 索引策略
| 索引 | 字段 | 类型 | 用途 |
| --------------------------- | --------------- | ---------------- | ------------------------------------- |
| PRIMARY | id | 主键 | 单条查询、更新、删除 |
| idx_classes_grade_id | grade_id | 普通索引 | 按年级过滤list 方法高频查询) |
| idx_classes_head_teacher_id | head_teacher_id | 普通索引(预留) | P3 教师查询所属班级高频BFF 聚合) |
> **当前缺失**grade_id 与 head_teacher_id 索引未在 schema 中声明Drizzle schema 未显式定义索引),需在迁移脚本中补齐。
### 3.3 读写分离策略
| 场景 | 路径 | 说明 |
| -------------------------- | -------------------------------- | ------------------------------------------------------------------------ |
| 写create/update/delete | MySQL 主库Drizzle | 当前唯一写路径 |
| 读list/getById | MySQL 主库Drizzle | 当前唯一读路径 |
| 读P3 起,高频列表) | Redis 缓存TTL 5 分钟) | 班级列表低频变更适合缓存事件驱动失效ClassCreated/Updated/Deleted |
| 读P4 起,分析聚合) | ClickHouse 宽表data-ana 投影) | 班级维度统计走宽表,不走主库 |
### 3.4 数据模型演进路线(长远铺垫)
| 阶段 | 演进项 | schema 变更 | 兼容策略 |
| ---- | ---------- | --------------------------------------------------- | ------------------------------------------------------------ |
| P3 | 软删除 | 新增 `deleted_at timestamp NULL` | 查询加 `WHERE deleted_at IS NULL`Drizzle 封装软删除 filter |
| P3 | 状态机 | 新增 `status enum('active','archived','graduated')` | 默认 active状态转换由 ClassDomainService 校验 |
| P3 | 多租户预留 | 新增 `school_id char(36)` | 为 SaaS 化预留,当前单租户填默认值 |
| P3 | 容量限制 | 新增 `capacity int` + `current_count int` | 分班时校验current_count 由 enrollment 聚合维护 |
| P3+ | cuid2 迁移 | id 类型 char(36)→varchar(30) | 数据迁移脚本 + 双写过渡期 |
| P6 | 审计字段 | 新增 `created_by` / `updated_by` | 从 `x-user-id` 头注入 |
---
## 4. API 设计
### 4.1 REST API 端点(已实现)
| Method | Path | 权限 | 请求 | 响应 | HTTP 状态 | 说明 |
| ------ | -------------- | ---------------- | --------------------------------------------------- | ---------------------------------- | --------------------- | ---------------------------------- |
| POST | `/classes` | `CLASSES_CREATE` | `{name, gradeId, headTeacherId?, description?}` | `{success, data: ClassResponse}` | 201 / 400 / 403 | 创建 |
| GET | `/classes` | `CLASSES_READ` | `?gradeId=<uuid>` | `{success, data: ClassResponse[]}` | 200 / 403 | 列表,可选年级过滤 |
| GET | `/classes/:id` | `CLASSES_READ` | path: `id` | `{success, data: ClassResponse}` | 200 / 404 / 403 | 单条 |
| PUT | `/classes/:id` | `CLASSES_UPDATE` | path:`id` + `{name?, headTeacherId?, description?}` | `{success, data: ClassResponse}` | 200 / 400 / 404 / 403 | 更新(空 body 抛 ValidationError |
| DELETE | `/classes/:id` | `CLASSES_DELETE` | path: `id` | `{success: true}` | 200 / 404 / 403 | 删除(先校验存在) |
### 4.2 ClassResponse 结构
```typescript
interface ClassResponse {
id: string;
name: string;
gradeId: string;
headTeacherId: string | null;
description: string | null;
createdAt: number; // epoch msDrizzle timestamp → getTime()
updatedAt: number;
}
```
### 4.3 错误响应ActionState 失败信封)
| 错误码 | 触发条件 | HTTP 状态 |
| --------------------------- | -------------------------------- | --------- |
| `CLASSES_VALIDATION_ERROR` | Zod 校验失败 / 空 update body | 400 |
| `CLASSES_NOT_FOUND` | getById/update/delete 找不到资源 | 404 |
| `CLASSES_PERMISSION_DENIED` | PermissionGuard 校验失败 | 403 |
| `CLASSES_CONFLICT` | 并发冲突(预留,当前未触发) | 409 |
| `CLASSES_BUSINESS_ERROR` | 业务规则违反(预留) | 422 |
| `CLASSES_DATABASE_ERROR` | DB 操作失败 | 500 |
| `CLASSES_INTERNAL_ERROR` | 未预期异常 | 500 |
### 4.4 API 演进路线
| 阶段 | 演进项 | 兼容策略 |
| ---- | -------------- | ---------------------------------------------------------------------------------------------- |
| P3 | 分页 | proto 已定义 `page_size` / `page_token`REST 落地 `?pageSize=20&pageToken=<cursor>`,游标分页 |
| P3 | 软删除 | DELETE 改为软删除(写 deleted_at新增 `?includeDeleted=true` 管理端查询 |
| P3 | 批量查询 | 新增 `POST /classes/batch`body: `{ids: string[]}`),供 BFF DataLoader 批量去重 |
| P3 | DataScope 注入 | list 方法根据 `x-user-datascope` 头注入 WHERE见 §9.4 |
| P3 | gRPC 启用 | proto `ClassService` 5 RPC 落地REST 与 gRPC 并存过渡期 |
| P4 | 字段投影 | 支持 `?fields=id,name` 减少响应体积GraphQL 替代后废弃) |
---
## 5. 事件设计
### 5.1 我发布的领域事件P3 由 core-edu 接管实现)
> **当前状态**:未实现。`shared/outbox/` 目录缺失P3 合并后由 core-edu 统一补齐 Outbox 模式。
| 事件 | 触发时机 | Topic004 §7.2 | 消费者 | 消费者动作 |
| ------------------ | ------------------ | --------------------------- | ------------------ | ------------------------------------- |
| `ClassCreated` | create 成功后 | `edu.org.class.created` | data-ana | 建班级维度宽表行 |
| `ClassUpdated` | update 成功后 | `edu.org.class.updated` | data-ana、msg | 更新宽表;班主任变更通知新旧班主任 |
| `ClassDeleted` | delete 成功后 | `edu.org.class.deleted` | data-ana、core-edu | 删除宽表行;关联检查(考试/作业引用) |
| `ClassTransferred` | headTeacherId 变更 | `edu.org.class.transferred` | msg | 通知新/旧班主任 |
### 5.2 事件 message 契约
使用 `events.proto``ClassEvent`(见 [events.proto](../../../packages/shared-proto/proto/events.proto) 第 15-24 行):
```protobuf
message ClassEvent {
string event_id = 1; // 幂等去重 IDcuid2
string aggregate_id = 2; // = class_id
string event_type = 3; // ClassCreated / ClassUpdated / ClassDeleted / ClassTransferred
int64 occurred_at = 4; // 事件发生时间epoch ms
string class_id = 5;
string name = 6;
string action = 7; // created / updated / deleted / transferred
map<string, string> metadata = 8; // 扩展字段:旧班主任 ID、变更字段列表等
}
```
### 5.3 Outbox 模式实现P3 设计,供 core-edu 落地)
```mermaid
flowchart LR
Cmd[ClassService.create] --> Repo[Repository.write]
Repo --> BizT[(classes 表)]
Repo --> OutT[(classes_outbox 表<br/>同事务)]
OutT --> Relay[OutboxRelayWorker<br/>每 100ms 轮询]
Relay --> Kafka[(Kafka<br/>edu.org.class.* topic)]
Kafka --> Proj[data-ana Projection<br/>更新宽表]
Kafka --> MsgC[msg 消费者<br/>通知投递]
```
**Outbox 表结构**P3 补齐 `shared/outbox/`
| 字段 | 类型 | 说明 |
| ------------ | --------------------------- | --------------------- |
| event_id | varchar(30) PK | cuid2幂等去重 |
| aggregate_id | varchar(30) | class_id |
| event_type | varchar(50) | ClassCreated 等 |
| payload | json | ClassEvent 序列化 |
| topic | varchar(100) | edu.org.class.created |
| status | enum('pending','published') | 投递状态 |
| created_at | timestamp | 入库时间 |
| published_at | timestamp NULL | 投递成功时间 |
### 5.4 我消费的外部事件P3+
| 事件 | 来源 | Topic | 消费动作 |
| ------------- | ---- | --------------------------- | -------------------------------------------------------- |
| `UserDeleted` | iam | `edu.identity.user.deleted` | 若 deleted user 是班主任,置空 headTeacherId最终一致 |
| `UserUpdated` | iam | `edu.identity.user.updated` | 班主任姓名变更无需同步classes 仅存 id |
### 5.5 幂等性设计
- **ProducerOutbox Relay**Kafka `idempotent=true` + `transactionalId=classes-outbox-relay`
- **Consumer**:基于 `event_id` 去重DB 唯一索引 `idx_outbox_event_id` 或 Redis SETNXTTL 7 天)
---
## 6. 横切关注点对齐清单
### 6.1 权限装饰器(端点 × 权限常量)
| 端点 | 权限常量 | 装饰器 |
| ------------------- | ---------------- | ------------------------------------------------ |
| POST /classes | `CLASSES_CREATE` | `@RequirePermission(Permissions.CLASSES_CREATE)` |
| GET /classes | `CLASSES_READ` | `@RequirePermission(Permissions.CLASSES_READ)` |
| GET /classes/:id | `CLASSES_READ` | `@RequirePermission(Permissions.CLASSES_READ)` |
| PUT /classes/:id | `CLASSES_UPDATE` | `@RequirePermission(Permissions.CLASSES_UPDATE)` |
| DELETE /classes/:id | `CLASSES_DELETE` | `@RequirePermission(Permissions.CLASSES_DELETE)` |
| GET /healthz | 无 | 无(白名单) |
| GET /readyz | 无 | 无(白名单) |
| GET /metrics | 无 | 无Prometheus 抓取) |
**角色-权限映射**[permission.guard.ts](../src/middleware/permission.guard.ts) 第 25-39 行):
| 角色 | 权限 |
| ------- | ------------------------------- |
| admin | CREATE / READ / UPDATE / DELETE |
| teacher | CREATE / READ / UPDATE |
| student | READ |
| parent | READ |
> **演进**P3 起由 iam 提供 `getEffectivePermissions(userId)` 动态查询PermissionGuard 改为调用 iam 而非硬编码 ROLE_PERMISSIONSproject_memory 已记录该约束)。
### 6.2 错误码清单
见 [§4.3](#43-错误响应actionstate-失败信封)。前缀 `CLASSES_`004 §11.4 确认保留P3 合并入 core-edu 后保留历史遗留前缀)。
### 6.3 Logger
| 项 | 配置 |
| --------- | ------------------------------------------------------------- |
| 库 | pino 9 + pino-prettydev |
| 初始化 | [logger.ts](../src/shared/observability/logger.ts) 模块级单例 |
| base 字段 | `{service: 'classes', version: '0.1.0'}` |
| level | env.LOG_LEVEL默认 info |
| 规则 | 禁止 `console.*`tracer.ts 第 20 行待整改 |
### 6.4 Metrics 指标清单
| 指标名 | 类型 | 标签 | 描述 |
| ---------------------------------- | --------------------- | ------------------------ | --------------------------------------------------------- |
| `classes_requests_total` | Counter | method, endpoint, status | 请求总数 |
| `classes_request_duration_seconds` | Histogram | method, endpoint | 请求延迟分布 |
| 默认进程指标 | Counter/Gauge/Summary | — | prom-client collectDefaultMetricsCPU/内存/GC/事件循环) |
> **指标命名规范**project_rules §12`<service>_<module>_<operation>_<unit>`classes 已遵循。
### 6.5 Tracer
| 项 | 配置 |
| ----------- | ----------------------------------------------------------------------------------- |
| SDK | @opentelemetry/sdk-node + auto-instrumentations |
| exporter | OTLP HTTP → `${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces` |
| serviceName | `classes` |
| 初始化 | [tracer.ts](../src/shared/observability/tracer.ts) `initTracer()` 在 bootstrap 调用 |
| 关闭 | `shutdownTracer()` 在 SIGTERM 钩子 |
| 整改 | 第 20 行 `console.log``logger.info` |
### 6.6 健康检查
| 端点 | 检查逻辑 | 失败行为 |
| ---------- | ---------------------- | ----------------------------------------------------- |
| `/healthz` | 进程存活(无依赖检查) | 200 `{status:"ok", service, timestamp}` |
| `/readyz` | DB `SELECT 1` | 成功 200失败 503 `{status:"error", service, error}` |
> **/readyz 演进**P3 起补充 Redis PING、Kafka 连接检查project_memory 约束:/readyz 必须检查所有下游依赖)。
### 6.7 优雅关闭
关闭顺序(见 [lifecycle.service.ts](../src/shared/lifecycle/lifecycle.service.ts)
1. NestJS `app.enableShutdownHooks()` 捕获 SIGTERM/SIGINT
2. `OnApplicationShutdown` 钩子触发
3. `closeDb()` 关闭 MySQL 连接池
4. `shutdownTracer()` 刷新 OTel span
5. K8s `terminationGracePeriodSeconds=60` 兜底
> **演进**P3 起增加 Kafka consumer graceful drain消费完 inflight 消息再退出、Redis 连接关闭。
---
## 7. 与其他模块的交互点(契约清单)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 阶段 |
| ------ | ----------- | ---------------------- | --------------------------------------------------- | ---------------- | ----- |
| 被调用 | api-gateway | HTTP | `/classes/*` 路由转发 | Gateway 反向代理 | P1 ✅ |
| 被调用 | teacher-bff | HTTPP2/ gRPCP3 | `GET /classes``ClassService.ListClasses` | BFF 聚合班级列表 | P2 |
| 调用 | MySQL | TCP | mysql2 连接池 | 数据读写 | P1 ✅ |
| 调用 | Redis | TCPP3 | ioredis | 班级列表缓存 | P3 |
| 发布 | — | KafkaP3 | `edu.org.class.created/updated/deleted/transferred` | 领域事件 | P3 |
| 消费 | iam | KafkaP3 | `edu.identity.user.deleted` | 班主任置空 | P3 |
| 被调用 | core-edu | gRPCP3 | `ClassService.GetClassesByTeacher` | 教师所属班级查询 | P3 |
> **P3 合并边界**classes 合并入 core-edu 后,"被调用"方从 classes 改为 core-edu端口从 3001 改为 3004gRPC 50053。proto 契约 `ClassService` 迁移至 `core_edu.proto` 或保留 `classes.proto` 由 core-edu 实现。
---
## 8. 风险与假设
### 8.1 技术风险
| 风险 | 影响 | 缓解措施 |
| -------------------------------------------- | -------------------- | ---------------------------------------------------------- |
| uuid v4 → cuid2 迁移涉及主键变更 | 数据迁移风险 | P3 合并时统一迁移,双写过渡期 + 迁移脚本 + 回滚预案 |
| PermissionGuard 硬编码 ROLE_PERMISSIONS | 角色变更需改代码重启 | P3 改为调用 iam `getEffectivePermissions` 动态查询 |
| 无 Outbox 能力 | P3 事件驱动基础缺失 | P3 补齐 `shared/outbox/`,复用 iam shared-ts Outbox 工具包 |
| 无 DataScope 注入 | 数据越权风险 | P3 Repository 层注入 WHERE 条件(见 §9.4 |
| Dockerfile node:20 与项目规则 node:22 不一致 | 镜像基准不统一 | P1 立即升级 |
### 8.2 假设
- 假设 api-gateway 已注入 `x-user-id` / `x-user-roles`project_memory 确认)
- 假设 `gradeId` 引用的年级实体由管理端/前端维护classes 不校验其存在性(无外键约束)
- 假设 P3 合并时 classes 表结构可无损迁移至 core-edu 库
### 8.3 未决决策(提请 coord 仲裁)
| # | 议题 | 选项 | 我的倾向 |
| --- | ------------------------------- | ----------------------------------------------------------------- | ------------------- |
| 1 | P3 合并后 classes 目录保留方式 | A. 删除目录 / B. 保留只读对照 / C. 改为 shared-ts 模板 | B保留对照基准 |
| 2 | cuid2 迁移时机 | A. P1 黄金模板立即 / B. P3 合并时统一 | A黄金模板先行 |
| 3 | proto `ClassService` 合并后归属 | A. 迁移到 core_edu.proto / B. 保留 classes.proto 由 core-edu 实现 | B保留契约稳定性 |
---
## 9. 长远架构与未来铺垫
> 本节是黄金模板的核心增值部分,为 classes 当前 CRUD 之外的未来演进预留接口,同时作为其他 TS 服务的"长远设计范例"。
### 9.1 事件驱动演进EDA 铺垫)
**当前**:纯同步 CRUD无事件。
**P3 目标**Outbox + Kafka 全链路。
**设计预留**
- `shared/outbox/` 目录结构标准化outbox.repository / outbox.publisher / outbox.relay-worker供 core-edu 直接复用
- TOPIC_MAP 集中管理 `事件 → topic` 映射,遵循 004 §7.2 命名规范
- 事件 schema 版本化:`event_type` 字段带 `v1` 后缀,演进时 `v2` 共存
- 幂等去重表设计标准化event_id 唯一索引)
### 9.2 gRPC 演进铺垫
**当前**:仅 REST。
**P3 目标**REST + gRPC 并存BFF 切换 gRPC。
**设计预留**
- proto 已定义 `ClassService` 5 RPC与 REST 端点一一对应
- Controller 层保持薄,业务逻辑在 ServicegRPC 实现可直接复用 Service
- 响应信封 ActionState 在 gRPC 侧用 metadata 传递 errorgRPC status code + details
### 9.3 缓存策略铺垫
**当前**:无缓存。
**P3 目标**Redis 班级列表缓存TTL 5 分钟)。
**设计预留**
- 依赖 ioredis 已声明
- env 已留 `REDIS_URL`
- Repository 层封装 `cachedList` 方法,缓存 key 设计:`classes:list:gradeId={gradeId|all}`
- 失效策略ClassCreated/Updated/Deleted 事件触发 `DEL classes:list:*`(模式删除)+ 短 TTL 兜底
### 9.4 DataScope 数据范围注入(安全铺垫)
**当前**PermissionGuard 仅角色级校验,无数据行级过滤。
**P3 目标**6 级 DataScope WHERE 注入004 §5.3)。
**设计预留**
| DataScope | WHERE 注入 | 适用角色 |
| --------- | ------------------------------------------------------------------------ | ----------------------- |
| L0 SELF | `WHERE head_teacher_id = :userId` | 教师(仅看自己班级) |
| L1 CLASS | `WHERE id IN (SELECT class_id FROM enrollment WHERE student_id=:userId)` | 学生 |
| L2 GRADE | `WHERE grade_id IN (:managedGradeIds)` | 年级组长 |
| L3 SCHOOL | `WHERE school_id = :schoolId` | 校管理员P3 多租户后) |
| L5 ALL | 无过滤 | admin |
**实现**Repository `list` 方法接收 `dataScope` 参数,动态构建 Drizzle WHEREdataScope 从 `x-user-datascope` 头解析。
### 9.5 软删除与审计(可维护性铺垫)
**当前**:物理删除。
**P3 目标**:软删除 + 审计字段。
**设计预留**
- schema 新增 `deleted_at timestamp NULL`
- Repository 封装 `withSoftDelete` 查询包装器,自动加 `WHERE deleted_at IS NULL`
- 新增 `created_by` / `updated_by`,从 `x-user-id` 头注入
- DELETE 端点改为写 `deleted_at = NOW()`,新增管理端 `?includeDeleted=true`
### 9.6 多租户 SaaS 演进(可扩展性铺垫)
**当前**:单租户。
**未来目标**SaaS 多校隔离。
**设计预留**
- schema 新增 `school_id`(默认填单租户 ID
- Repository 所有查询注入 `WHERE school_id = :currentSchoolId`
- `school_id` 从 JWT claim 或 `x-school-id` 头解析
- 索引 `idx_classes_school_id_grade_id` 复合索引
### 9.7 分页与批量查询(性能铺垫)
**当前**list 全量返回。
**P3 目标**:游标分页 + 批量查询。
**设计预留**
- proto 已定义 `page_size` / `page_token`
- 游标分页:`page_token = base64(last_id)`,查询 `WHERE id > :lastId LIMIT :pageSize`
- 批量查询 `POST /classes/batch` body `{ids: string[]}`,供 BFF DataLoader 批量去重(防 N+1
### 9.8 测试策略(可测试性铺垫)
**当前**:单元测试覆盖率 60%,仅 Service 层。
**目标**80% 覆盖率 + 集成测试 + 契约测试。
| 测试类型 | 范围 | 工具 | 目标覆盖率 |
| -------- | -------------------------------------------- | ----------------------- | ---------- |
| 单元测试 | Servicemock repo | Vitest | 80% |
| 集成测试 | Controller + Service + Repositorytest DB | Vitest + testcontainers | 关键路径 |
| 契约测试 | proto ↔ REST 一致性 | buf breaking | 100% |
| E2E | 端到端 CRUD | docker-compose | 冒烟 |
### 9.9 可观测性增强(可观测性铺垫)
**当前**:基础 metrics + tracer。
**目标**:结构化日志 + trace 关联 + 告警规则。
**设计预留**
- 日志注入 `traceId` / `spanId` / `userId`(从 header 提取)
- metrics 增加 `classes_outbox_pending_total`Outbox 积压告警)
- Grafana 面板模板化,供其他服务复用
- 告警规则5xx 错误率 > 1% / p99 延迟 > 500ms / outbox 积压 > 100
### 9.10 配置管理演进(可部署性铺垫)
**当前**env 环境变量Zod 校验)。
**P6 目标**:配置中心热更新。
**设计预留**
- 三层配置env环境变量< yaml业务参数< 配置中心P6 Consul
- 业务参数(如分页默认值、缓存 TTL抽离为 yamlP6 接入 Consul 热更新
---
## 10. 黄金模板对照 checklist供其他 TS 服务自检)
> 本 checklist 是 classes 作为黄金模板的核心产出,供 ai01-ai10 的 TS 服务在实现前自检对齐。
### 10.1 目录结构
```
services/<service>/src/
├─ <domain>/ # 限界上下文
│ ├─ <domain>.controller.ts # ControllerHTTP/gRPC 入口)
│ ├─ <domain>.service.ts # Application Service编排层
│ ├─ <domain>.repository.ts # Repository数据访问
│ ├─ <domain>.schema.ts # Drizzle schema表定义
│ └─ <domain>.dto.ts # Zod schema + DTO 类型
├─ config/ # env.ts + database.ts
├─ middleware/ # auth.middleware.ts + permission.guard.ts
├─ shared/
│ ├─ errors/ # application-error.ts + global-error.filter.ts
│ ├─ health/ # health.controller.ts + health.module.ts
│ ├─ lifecycle/ # lifecycle.service.ts优雅关闭
│ ├─ observability/ # logger.ts + metrics.ts + tracer.ts
│ └─ outbox/ # P3 补齐)
├─ app.module.ts
└─ main.ts
```
### 10.2 横切关注点 checklist
| # | 检查项 | 验证方式 | 参考文件 |
| --- | --------------------------------------------- | ------------------------------- | --------------------------------------------------------------------- |
| 1 | `@RequirePermission` 覆盖全部 Controller 方法 | grep `@RequirePermission` | [classes.controller.ts](../src/classes/classes.controller.ts) |
| 2 | 错误码前缀统一 `<SERVICE>_` | grep 错误码常量 | [application-error.ts](../src/shared/errors/application-error.ts) |
| 3 | pino logger禁止 `console.*` | grep `console.log` | [logger.ts](../src/shared/observability/logger.ts) |
| 4 | prom-client metrics + `/metrics` 端点 | 访问 `/metrics` | [metrics.ts](../src/shared/observability/metrics.ts) |
| 5 | OTel tracer 初始化 + 关闭 | main.ts 调用 initTracer | [tracer.ts](../src/shared/observability/tracer.ts) |
| 6 | `/healthz` liveness | GET 200 | [health.controller.ts](../src/shared/health/health.controller.ts) |
| 7 | `/readyz` 检查 DB 依赖 | GET 200 / 503 | 同上 |
| 8 | 优雅关闭 SIGTERM | LifecycleService | [lifecycle.service.ts](../src/shared/lifecycle/lifecycle.service.ts) |
| 9 | Zod 输入校验 | controller `schema.parse(body)` | [classes.dto.ts](../src/classes/classes.dto.ts) |
| 10 | GlobalErrorFilter 注册 | app.useGlobalFilters | [global-error.filter.ts](../src/shared/errors/global-error.filter.ts) |
| 11 | ActionState 响应信封 | `{success, data/error}` | controller 返回类型 |
| 12 | Dockerfile 多阶段builder + runtime | Dockerfile 检查 | [Dockerfile](../Dockerfile) |
| 13 | env Zod 校验 | envSchema.safeParse | [env.ts](../src/config/env.ts) |
| 14 | Drizzle ORM禁止 TypeORM | package.json 无 typeorm | [package.json](../package.json) |
| 15 | 测试覆盖率 ≥ 80% | vitest --coverage | [vitest.config.ts](../vitest.config.ts) |
| 16 | ESM `.js` 后缀 import | grep `from "./xxx.js"` | 全部源码 |
| 17 | `import type` 用于类型导入 | grep `import type` | 全部源码 |
| 18 | 无 `any` / 无 `as` 断言(除 unknown | tsc + eslint | — |
### 10.3 复制黄金模板步骤30 分钟可复制)
1. 复制 `services/classes/` 目录为 `services/<new-service>/`
2. 全局替换 `classes``<new-service>`目录名、文件名、类名、错误码前缀、serviceName、metrics 指标名)
3. 修改 `<domain>/` 为新限界上下文,替换 schema / dto / controller / service / repository
4. 修改 `env.ts` 端口(按 [port-allocation.md](../../../infra/port-allocation.md)
5. 修改 `Dockerfile` EXPOSE 端口
6. `pnpm install` + `pnpm lint` + `pnpm typecheck` + `pnpm test`
7. 运行 `pnpm run arch:scan` 更新 arch.db
---
## 11. 整改清单与里程碑
### 11.1 P1 立即整改(黄金模板对齐)
| # | 整改项 | 文件 | 优先级 |
| --- | ------------------------------------------------- | ------------------------------------------------------------------------- | ------ |
| 1 | 移除 typeorm 冗余依赖 | [package.json](../package.json) 第 33 行 | 高 |
| 2 | `console.log``logger.info`tracer.ts | [tracer.ts](../src/shared/observability/tracer.ts) 第 20 行 | 高 |
| 3 | `@Req()``@Query('gradeId')`controller list | [classes.controller.ts](../src/classes/classes.controller.ts) 第 49-53 行 | 高 |
| 4 | 测试覆盖率阈值 60% → 80% | [vitest.config.ts](../vitest.config.ts) | 中 |
| 5 | Dockerfile node:20 → node:22 | [Dockerfile](../Dockerfile) | 中 |
| 6 | 补齐 grade_id / head_teacher_id 索引 | schema 迁移 | 中 |
### 11.2 P3 演进(合并入 core-edu 时落地)
| # | 演进项 | 说明 |
| --- | -------------------------- | ------------------------------------------ |
| 1 | 补齐 `shared/outbox/` 目录 | 事件发布能力,复用 shared-ts Outbox 工具包 |
| 2 | cuid2 迁移 | 主键 uuid v4 → cuid2 |
| 3 | DataScope 6 级 WHERE 注入 | Repository 层动态过滤 |
| 4 | Redis 班级列表缓存 | TTL 5 分钟 + 事件驱动失效 |
| 5 | 软删除 + 审计字段 | deleted_at / created_by / updated_by |
| 6 | 分页 + 批量查询 | 游标分页 + POST /classes/batch |
| 7 | gRPC server 启用 | proto ClassService 落地,端口 50053 |
| 8 | PermissionGuard 改调 iam | 动态权限查询 |
### 11.3 里程碑
| 里程碑 | 交付物 | 验收标准 |
| ------------- | ----------------------- | ------------------------------------------------- |
| M1P1 | 黄金模板整改完成 | 6 项 P1 整改全部完成 + lint/typecheck/test 零错误 |
| M2P1 | 黄金模板 checklist 产出 | 本文档 §10 + 其他 AI 自检通过 |
| M3P3 交接) | classes 合并入 core-edu | ai08 接收,表结构 + proto + 事件能力迁移 |
---
## 12. AI 身份标注
**AI Agent**: ai07 (classes · 黄金模板)
**Branch**: docs/classes-architecture-design-ai07
**Coordinator**: coord
---
> 本架构设计文档遵循架构通用标准(功能正确性 / 可维护性 / 可测试性 / 可扩展性 / 可观测性 / 安全性 / 可部署性 / 容错性),既实现 P1 当前目标(班级 CRUD + 黄金模板),又为 P3+ 演进(事件驱动 / gRPC / DataScope / 缓存 / 软删除 / 多租户 / SaaS铺设接口。待 coord 交叉审查通过后§10 黄金模板 checklist 作为其余 8 个 TS 服务的强制对齐基准。

View File

@@ -0,0 +1,207 @@
# 模块理解确认书 — content
> AI 标识ai05初稿→ ai09复核与阶段 2 接手)
> 负责模块contentP4— 按 [ai-allocation.md §3.2](../../../docs/architecture/ai-allocation.md) 最新分配content 由 ai09 专责
> 阶段:架构设计外包 · 阶段 1全局理解ai05 初稿)+ ai09 复核
> 日期2026-07-09初稿/ 2026-07-09ai09 复核)
> 关联文档:[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[coord-cross-review.md](../../../docs/architecture/coord-cross-review.md)、[known-issues.md](../../../docs/troubleshooting/known-issues.md)
---
## 1. 我在架构中的位置
- **层级**业务微服务层L5DDD 限界上下文
- **业务领域**D4 内容资源领域004 §1.1b
- **上游**(谁调用我):
- teacher-bff3003教学场景聚合教师查教材/知识点/题库
- student-bff学习场景P3 起):学生查学习路径、知识点前置
- aiPythonP5gRPC 查询知识点/题库用于 AI 出题004 §4.1 `AI -.gRPC.-> Content`§9.3 AI 辅助出题流程)
- **下游**(我调用谁):
- MySQL写模型主库独占
- Neo4j知识图谱前置依赖图
- Elasticsearch题库全文检索P4 后续补充,当前未实现)
- **通信方式**
- 当前HTTP RESTController无 gRPC controller
- 目标态004 §4.1 / pending-features P4gRPC 暴露 `TextbookService` + `KnowledgeGraphService`
- Kafka消费 core-edu 教学内容变更通知004 §4.1 `CoreEdu -.事件.-> Content`);发布 `edu.content.question.published`004 §7.2
- **端口**3005见 [content env.ts](../src/config/env.ts)
## 2. 我的限界上下文
- **聚合职责**:管理 Textbook教材、Chapter章节、KnowledgePoint知识点、Question题库四个聚合
- **我的数据属于**D4 内容资源领域
- **我不负责**
- 不负责学情分析D6由 data-ana 承载)
- 不负责考试/作业/成绩D3由 core-edu 承载)
- 不负责通知分发D5由 msg 承载)
- 不直接访问 core-edu / iam / msg 的数据库
- **现有骨架领域模块**4 个,见 [content/src](../src)
- `textbooks/`:教材 CRUD5 端点)
- `chapters/`:章节 CRUD5 端点,按 textbook 查询)
- `knowledge-points/`:知识点 CRUD + Neo4j 知识图谱7 端点,含前置链路查询/添加)
- `questions/`:题库 CRUD5 端点4 种题型校验)
## 3. 我与外部的契约
- **消费的 proto message**(从 shared-proto
- 无直接消费其他服务 proto通过 Kafka 事件接收 core-edu 教学内容变更(事件契约见 `events.proto`
- **暴露的契约**(见 [content.proto](../../../packages/shared-proto/proto/content.proto),包名 `next_edu_cloud.content.v1`
- `TextbookService`CreateTextbook / GetTextbook / ListTextbooks
- `KnowledgeGraphService`GetPrerequisites / GetLearningPath
- **当前实现均为 RESTgRPC controller 未实现**proto 已定义待迁移)
- **事件契约**
- 发布:`edu.content.question.published`(题库新增/发布,消费者 AI、ES见 004 §7.2
- 消费core-edu 教学内容变更事件004 §4.1,具体 topic 待 core-edu ai03 设计确认)
- **错误码前缀**`CONTENT_*`CONTENT_VALIDATION_ERROR / NOT_FOUND / PERMISSION_DENIED / CONFLICT / BUSINESS_ERROR / DATABASE_ERROR / INTERNAL_ERROR见 [application-error.ts](../src/shared/errors/application-error.ts)
- **权限点**16 个,见 [permission.guard.ts](../src/middleware/permission.guard.ts)
- `CONTENT_TEXTBOOK_{CREATE,READ,UPDATE,DELETE}`
- `CONTENT_CHAPTER_{CREATE,READ,UPDATE,DELETE}`
- `CONTENT_QUESTION_{CREATE,READ,UPDATE,DELETE}`
- `CONTENT_KNOWLEDGE_POINT_{CREATE,READ,UPDATE,DELETE}`
## 4. 我的技术栈
- 语言TypeScript 5.6ESM 模式,相对 import 带 `.js` 后缀)
- 框架NestJS 10
- ORMDrizzle ORM 0.31 + mysql2 3.11
- 知识图谱neo4j-driver 5.23PREREQUISITE_OF 关系Cypher 查询深度 1..5
- 全文检索Elasticsearch**待引入**env.ts 预留 ES_URL 但 package.json 未装 @elastic/elasticsearch
- 可观测pino logger + prom-client metrics + OpenTelemetry tracer三支柱已具备
- 消息总线Kafka**待引入**pending-features P4 未明确要求 content 发事件,但 004 §7.2 列了 `edu.content.question.published`
## 5. 我的阶段归属
- **P4 内容分析阶段**pending-features §P4
- 教材/章节/知识点 CRUD仅 CRUD不实现检索+ 知识图谱查询Neo4j+ 题库 CRUD不实现检索
- MySQL schematextbooks / chapters / knowledge_points / questions
- Neo4j 数据:知识点前置依赖图(从 MySQL 同步)
- CDC 链路Debezium 监听 MySQL binlog → Kafka → data-ana 消费写 ClickHouse
- **退出标准**教师查看知识图谱前置依赖Neo4j 秒级返回)→ 学生查看学情诊断ClickHouse 宽表 5s 内返回)→ CDC 链路延迟 < 5s
- **依赖上游**P1 黄金模板 classes横切关注点对齐、P3 core-edu教学内容变更事件待 ai03 设计确认 topic
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
- [x] 权限装饰器 `@RequirePermission`16 个 CONTENT_* 权限点,全部 Controller 方法已覆盖)
- [x] 错误码前缀统一(`CONTENT_*`
- [x] logger / metrics / tracer 三支柱(已具备)
- [x] `/healthz` 健康检查HealthModule 已注册)
- [⚠️] `/readyz`**仅检查 DB `SELECT 1`,未检查 Neo4j 连通性**Neo4j 故障时仍返回 ok
- [x] 优雅关闭 SIGTERMmain.ts 已处理closeNeo4j → closeDb → shutdownTracer
- [ ] 测试覆盖率 ≥ 80%**当前 0%**,无测试文件)
- [x] Dockerfile 多阶段构建(已具备)
- [⚠️] Zod 输入验证questions 用 service 层手动 if 校验抛 ValidationError**非 Controller 层 schema.parse**
- [x] GlobalErrorFilter 统一兜底
## 7. 现有骨架差距与待决策(提请 coord 仲裁)
| # | 差距 | 影响 | 提请决策 |
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| C1 | **ES 完全未实现**env.ts 预留 ES_URL 但无 @elastic/elasticsearch 依赖、无 config/elasticsearch.ts、无 service 调用 | P4 退出标准要求题库检索pending-features 注明"P4 仅 CRUDP5 引入 ES" | 确认 content 的 ES 检索归属 P4 还是 P5004 §2.2 列 ES 使用者为 Content、AI |
| C2 | **无 Outbox / Kafka**:骨架无 shared/outbox/,无 Kafka producer/consumer | 004 §7.2 列 `edu.content.question.published` 事件需发布 | 确认 content 是否需 Outboxiam 无 Outboxcore-edu 有) |
| C3 | **README 与实现脱节**README 声称 `TextbooksService.createKnowledgeGraph``getPrerequisites`,源码中 createKnowledgeGraph 不存在getPrerequisites 在 KnowledgePointsService | 文档误导 | 阶段 2 设计需同步修正 README |
| C4 | **gRPC 未实现**proto 定义了 TextbookService/KnowledgeGraphService但无 gRPC controller | pending-features P4 未强制 gRPC004 §4.1 目标态 gRPC | 确认 P4 是否启用 gRPCai03 提请统一决策) |
| C5 | **schema 定义分散**textbooks.schema.ts 定义 3 张表textbooks/chapters/knowledgePointschapters/knowledge-points schema 仅 re-exportquestions.schema.ts 独立 | 维护成本 | 阶段 2 设计统一 schema 归属 |
| C6 | **DB 连接模式与 iam 不一致**content 用模块级 `const db`iam 用 `getDb()` 函数式懒加载 | 测试 mock 困难 | coord 已在 known-issues 记录"db 常量导出对齐黄金模板",需确认统一方向 |
| C7 | **表时间戳不统一**textbooks/questions 有 created_at+updated_atchapters/knowledge_points 无时间戳 | 审计追踪缺失 | 阶段 2 设计补齐 |
| C8 | **无外键约束**questions.knowledge_point_id → knowledge_points.id 等无 Drizzle 外键 | 引用完整性靠应用层 | 阶段 2 设计评估是否加外键 |
| C9 | **addPrerequisite 降级策略不一致**safeCreateNode 非阻塞Neo4j 失败仅 warnaddPrerequisite 在 Neo4j 不可用时抛 InternalError | 行为不一致 | 阶段 2 设计统一降级策略 |
| C10 | **proto 包名**:实际 `next_edu_cloud.content.v1`project_rules §5 规定 `edu.content.v1` | 命名规范不一致 | **coord 仲裁**ai03 已提请) |
---
## 服务审计表 — ai05content 部分)
> 审计标准对照 [project_rules §3](../../../.trae/rules/project_rules.md) 与 [known-issues §2.2 classes 黄金模板](../../../docs/troubleshooting/known-issues.md)
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
| ------- | ---------------- | -------------- | ------- | -------------- | ------------------------------- | -------- | ------------------- | --------------------- | ---------- | ---------- |
| content | ✅ 16 端点全覆盖 | ✅ `CONTENT_*` | ✅ pino | ✅ prom-client | ✅ OTel + auto-instrumentations | ✅ | ⚠️ 仅 DB 不查 Neo4j | ✅ closeNeo4j→closeDb | 0% | ✅ |
---
## 跨模块契约对齐提请coord 交叉审查)
### 接口一致性检查content 相关)
| 本服务声明 | 对方服务声明 | 是否匹配 | 备注 |
| --------------------------------------------------------- | ------------------------------------------------- | --------- | --------------------------- |
| ai05 content: 暴露 KnowledgeGraphService.GetPrerequisites | ai06 ai: 调 content 查知识点004 §9.3 | ⚠️ 待确认 | ai06 设计文档需确认调用签名 |
| ai05 content: 暴露 TextbookService.ListTextbooks | ai03 teacher-bff: 聚合 content 查教材004 §4.1 | ⚠️ 待确认 | ai03 设计文档需确认调用 |
| ai05 content: 发布 `edu.content.question.published` | ai06 ai: 消费题库事件004 §7.2 消费者 AI | ⚠️ 待确认 | ai06 设计需确认是否消费 |
### 全局冲突检查content 相关)
| 检查项 | 检查结果 | 备注 |
| -------------------- | ------------------- | ---------------------------------------------------------------------------------------------------- |
| 端口不冲突 | ✅ content 3005 | 与 iam(3002)/classes(3001)/teacher-bff(3003)/core-edu(3004)/data-ana(3006)/msg(3007)/ai(3008) 不冲突 |
| Topic 不重复 | ⚠️ 待汇总 | content 发布 `edu.content.question.published` |
| 错误码前缀不重叠 | ✅ `CONTENT_*` 唯一 | 与 iam `IAM_*` / core-edu `CORE_EDU_*` / msg `MSG_*` 不重叠 |
| Proto message 不遗漏 | ⚠️ 待确认 | content.proto 缺 Update/Delete/分页(对比 classes.proto 有 page_token |
| proto 包名规范 | ⚠️ 不一致 | 实际 `next_edu_cloud.<domain>.v1`,规则要求 `edu.<domain>.v1`**提请 coord 仲裁**ai03 已提请) |
### 待 coord 仲裁的决策项content 相关)
1. **proto 包名统一**`next_edu_cloud.*` vs `edu.*`(影响全部 proto需 coord 决策)
2. **gRPC 启用时机**P4/P5 是否启用 gRPC controller还是继续 REST影响 content/msg/iam/core-edu 全部服务)
3. **content ES 检索归属**P4pending-features 注明"P4 仅 CRUDP5 引入 ES"vs 004 §2.2(列 ES 使用者 Content、AI—— 需明确 content 何时引入 ES
4. **content Outbox**是否需要004 §7.2 列 content 发事件,但 pending-features P4 未要求)
---
## 下一步
1. **等待 coord 审核本确认书**(跨模块契约对齐 + 仲裁项)
2. coord 放行后进入**阶段 2模块架构设计文档**,按 ai-allocation.md §7 模板产出:
- content 模块架构设计文档(含 Neo4j 图模型、ES 索引 mapping、题库 CRUD API、与 ai 的 gRPC 接口、教材/章节结构树)
3. 阶段 2 设计完成后同步更新 README修正 §C3 文档脱节问题)
---
## ai09 复核记录2026-07-09
ai05 初稿覆盖了位置/限界上下文/契约/技术栈/黄金模板对齐/差距 10 项,作为阶段 1 的骨架是合格的。ai09 在接手阶段 2 前,对初稿做以下**复核与补强**,依据为 [coord-cross-review.md](../../../docs/architecture/coord-cross-review.md) 已仲裁结论与 [004 §4.2/§7.2/§11.2/§12.2](../../../docs/architecture/004_architecture_impact_map.md) 最新修订:
### A. 已仲裁项状态更新(原 C 列待决策项 → 已裁决)
| 原编号 | 原待决策 | coord 裁决 | 状态 |
| ------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| C2 | content 是否需 Outbox | **需要**。004 §7.2 列 `edu.content.question.published` 为领域事件004 §12.2 规定领域事件强制 Outbox 模式(业务事务事件不豁免) | ✅ 已裁决P4 必须引入 Outbox |
| C4 | gRPC 启用时机 | **P4 启用**。004 §4.2 gRPC 启用阶段矩阵明确P4 content + data-ana 启用 gRPC server | ✅ 已裁决P4 必须实现 gRPC controller |
| C10 | proto 包名规范 | **保持现状 `next_edu_cloud.content.v1`**。coord 已修订 project_rules §5 与 004 §11.2 | ✅ 已裁决 |
### B. 初稿遗漏项(必须在阶段 2 补全)
| # | 遗漏 | 依据 | 阶段 2 处理 |
| --- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| L1 | **教材/章节结构树缺 section 层级** | [ai-allocation §5 ai09 任务](../../../docs/architecture/ai-allocation.md)textbook → chapter → section → knowledge-point 四级结构 | 数据模型补 `content_sections` 表 |
| L2 | **题库批量导入 Excel/CSV 完整未设计** | ai-allocation §5 明确要求"题库 CRUD 完整 API含批量导入 Excel/CSV" | API 设计补 `POST /questions/batch-import` + 异步任务 + 模板下载 |
| L3 | **知识图谱可视化数据 API 缺失** | ai-allocation §5 明确要求"知识图谱可视化数据 API" | API 设计补 `GET /knowledge-graph/visualization` 返回 nodes/edges 结构 |
| L4 | **与 ai12ai 服务)的 gRPC 接口未细化** | ai-allocation §5 明确要求"查询知识点/题库用于 AI 出题" + 004 §9.3 AI 辅助出题流程 | 契约设计补 `QuestionBankService.ListQuestions`/`SearchQuestions`/`BatchCreateQuestions` |
| L5 | **ES 索引 mapping 完整未设计** | ai-allocation §5 明确要求"ES 索引 mapping 设计(题库全文检索 + 标签过滤 + 难度/年级 facet" | 阶段 2 完整设计 mapping但实现归属 P5pending-features P5 才引入 ES |
| L6 | **readyz 未检查 Neo4j 连通性** | 阶段 1 §C 已识别但未决议 | 阶段 2 横切关注点补 Neo4j 健康检查 |
| L7 | **题目审核状态机未设计** | 长远考虑:教师录入/AI 生成/审核/发布/归档的全生命周期 | 阶段 2 领域模型补 Question.status 状态机 |
| L8 | **题库版本化与协作编辑未考虑** | 长远考虑:教材版本变更、多人协作出题 | 阶段 2 数据模型补 `version`/`updated_by`/`lock_version` 字段 |
| L9 | **DataScope 下推未在 Repository 体现** | 004 §5.3 DataScope 6 级需 Repository 注入 WHERE | 阶段 2 Repository 设计补 DataScope 下推 |
| L10 | **题目/知识点与学科年级的多对多关联未明确** | 跨教材/跨学科复用题目需多对多 | 数据模型补 `content_question_grades`/`content_kp_grades` 关联表 |
| L11 | **AI 生成题目回写契约** | 004 §9.3:教师审核后入库 → content.CreateQuestions | 契约设计补 `QuestionBankService.BatchCreateQuestions` |
| L12 | **知识点掌握度反查data-ana → content 反向查询)未设计** | 004 §8.3 闭环:掌握度更新后推荐练习 | 契约补 `KnowledgeGraphService.GetRecommendations(mastery)` |
### C. 长远架构必须铺垫的方面ai09 在阶段 2 重点设计)
| 维度 | 铺垫点 | 理由 |
| ---------------------- | -------------------------------------------------------------- | ---------------------------------------- |
| 多版本教材并存 | textbook.version 字段 + 教材版本切换不破坏题库引用 | K12 教材迭代周期长但必须支持新旧版本共存 |
| 跨学科知识图谱 | Neo4j 节点带 `subject_id`,未来跨学科知识关联(数学→物理前置) | 长远支撑综合题、跨学科出题 |
| 题目去重/相似度 | 设计 content 字段长度 + 未来 embedding 字段占位 | AI 批量出题必然带来重复检测需求 |
| ES 与 MySQL 双写一致性 | 阶段 2 设计 CDC/Outbox → ES 投递方案P5 落地) | 避免 P5 引入 ES 时再重构数据流 |
| 知识图谱版本控制 | Neo4j 节点带 `version`/`textbook_id` 属性 | 教材版本切换时图谱可追溯 |
| 国际化i18n | 题目/知识点 content 字段预留 `lang` 维度 | 未来支持少数民族语言版本 |
| 题库审核工作流 | Question.status 状态机 + 审核记录表 | 接入 Temporal 工作流时无需重构 |
| gRPC Streaming 出题 | 契约预留 `StreamGenerateQuestions` 方法占位 | AI 流式出题未来需求 |
---
**AI Agent**: ai09 (content)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式(直接 push main
**阶段 1 状态**: ✅ ai05 初稿 + ai09 复核补强完成,进入阶段 2

View File

@@ -0,0 +1,944 @@
# 模块架构设计文档 — content
> AI 标识ai05
> 负责模块contentP4
> 阶段:架构设计外包 · 阶段 2模块架构设计
> 日期2026-07-09
> 版本v1.0
> 关联文档:[01-understanding.md](./01-understanding.md)、[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[known-issues.md](../../../docs/troubleshooting/known-issues.md)、[project_rules.md](../../../.trae/rules/project_rules.md)
---
## 0. 设计哲学与文档定位
### 0.1 设计哲学
content 服务承载**内容资源中台**职责,设计哲学遵循四条原则:
1. **DDD 限界上下文**Textbook / Chapter / KnowledgePoint / Question 四个聚合根,明确边界,禁止跨聚合直接持有引用
2. **Polyglot Persistence 多存储分工**MySQL 存权威写模型Neo4j 存知识图谱关系ES 存检索读模型,三者职责不重叠
3. **事件驱动同步**:跨存储同步通过 Outbox + Kafka 异步事件,禁止业务事务内同步双写(违反 004 §12.2 强制条款)
4. **CQRS 读模型分离**:写模型走 Drizzle ORM 直写 MySQL读模型ES 全文检索、知识图谱查询)独立查询路径,未来可演化为独立读服务
### 0.2 长远演进目标
content 不止服务 P4 CRUD 阶段,需为以下未来场景预留架构弹性:
| 时间线 | 演进方向 | 当前架构预留点 |
| ------- | ------------------------------------------------------------------- | --------------------------------------------------------------- |
| P4 完成 | CRUD + 知识图谱 + Outbox + 基础事件发布 | Outbox 表 + Kafka producer + Neo4j 异步同步 consumer |
| P5 | ES 全文检索 + gRPC 完整契约(含 CreateQuestions 供 AI 调用) | ES mapping 设计 + gRPC controller 预留 + proto message 完整定义 |
| P6+ | AI 辅助出题 deep integration知识点推荐 → AI 生成题目 → 入库审核) | QuestionService.CreateQuestions gRPC + 审核工作流预留字段 |
| 未来 | 多模态内容(视频/音频/AR、跨租户内容共享、个性化学习路径推荐 | metadata jsonb 字段预留 + tenant_id 字段预留 + LearningPath RPC |
| 未来 | 知识图谱可视化、图神经网络分析、教材版本管理 | Neo4j 节点属性可扩展 + version 字段 + 图算法 Cypher 预留 |
| 未来 | 内容合规审核工作流(人工/AI 审核) | status 字段含 draft/pending_review/published/archived 状态机 |
### 0.3 文档结构说明
本设计文档遵循 ai-allocation.md §7 模板的 8 节结构,并补充第 9 节"演进路线"与第 10 节"风险与假设"。所有跨模块契约点均回标到 004 架构影响地图对应章节。
---
## 1. 模块内部分层图
### 1.1 整体分层架构
```mermaid
flowchart TB
subgraph Client["客户端"]
BFF["teacher-bff / student-bff"]
AI["ai 服务 (gRPC)"]
end
subgraph Gateway["API Gateway"]
GW["api-gateway (JWT 校验/限流/熔断)"]
end
subgraph Content["content 服务 (3005)"]
direction TB
CTL[Controller 层<br/>HTTP REST + gRPC]
GRD[Guard 层<br/>PermissionGuard + AuthMiddleware]
VAL[Validation 层<br/>Zod Schema Parse]
SVC[Service 层<br/>Application Service]
REPO[Repository 层<br/>Drizzle ORM 数据访问]
OUT[Outbox Publisher<br/>独立 worker]
CONSUMER[Kafka Consumer<br/>消费 core-edu 事件]
NEO_SYNC[Neo4j Sync Worker<br/>异步同步知识点]
ES_SYNC[ES Sync Worker<br/>异步同步题目]
FILTER[GlobalErrorFilter<br/>统一错误兜底]
OBS[Observability<br/>pino + prom-client + OTel]
end
subgraph Storage["数据存储"]
MySQL[MySQL 8<br/>写模型主库]
Neo4j[Neo4j 5<br/>知识图谱]
ES[Elasticsearch 8<br/>检索读模型]
end
subgraph Bus["消息总线"]
Kafka[Kafka<br/>edu.content.* topics]
end
BFF -->|HTTP REST| GW
AI -.->|gRPC| CTL
GW -->|HTTP REST| CTL
CTL --> GRD
GRD --> VAL
VAL --> SVC
SVC --> REPO
SVC --> OUT
REPO --> MySQL
OUT -->|polling + publish| Kafka
Kafka -->|consume| CONSUMER
CONSUMER --> NEO_SYNC
NEO_SYNC --> Neo4j
ES_SYNC --> ES
Kafka -->|consume edu.content.*| ES_SYNC
CTL -.-> FILTER
SVC -.-> OBS
```
### 1.2 请求处理链路
| 阶段 | 组件 | 职责 |
| ------ | ----------------------------------- | ---------------------------------------------------------------------------- |
| 入口 | NestJS ExpressAdapter / gRPC server | HTTP 3005 / gRPC 50054 |
| 鉴权 | AuthMiddleware | 信任 Gateway 注入的 `x-user-id` / `x-user-roles`004 §4.1 Gateway 职责) |
| 授权 | PermissionGuard (APP_GUARD) | 校验 16 个 `CONTENT_*` 权限点 |
| 校验 | Zod Schema Parse | Controller 层解析 body/query/param失败抛 ZodError |
| 业务 | ApplicationService | 编排 Repository + Outbox + 跨聚合调用 |
| 持久化 | Repository (Drizzle ORM) | 单一数据访问入口,事务边界 |
| 事件 | Outbox Publisher | 独立 worker 轮询 outbox_events 表投递 Kafka |
| 异步 | Kafka Consumer | 消费 core-edu 教学内容变更事件 |
| 同步 | Neo4j / ES Sync Worker | 消费 content 自身事件同步到 Neo4j / ES |
| 错误 | GlobalErrorFilter | 捕获 ApplicationError + ZodError结构化响应 |
| 观测 | Logger / Metrics / Tracer | 全链路 traceparent 传递 |
### 1.3 异步链路与同步链路分离
**同步链路**(用户请求响应路径):
- 仅写 MySQL 主库 + outbox_events 表(同一事务)
- 不直接写 Neo4j / ES违反事件驱动原则
**异步链路**(事件驱动路径):
- Outbox Publisher → Kafka → Neo4j Sync Worker同步知识点节点
- Outbox Publisher → Kafka → ES Sync Worker同步题目索引
- Kafka Consumer ← core-edu 事件 → 触发内容失效/更新
---
## 2. 领域模型
### 2.1 聚合根与实体
```mermaid
classDiagram
class Textbook {
+id: string
+title: string
+subjectId: string
+gradeId: string
+version: string
+status: TextbookStatus
+tenantId: string
+createdAt: Date
+updatedAt: Date
+publish() void
+archive() void
}
class Chapter {
+id: string
+textbookId: string
+title: string
+order: number
+parentId: string|null
+status: ChapterStatus
+createdAt: Date
+updatedAt: Date
}
class KnowledgePoint {
+id: string
+chapterId: string
+title: string
+description: string
+difficulty: 1|2|3|4|5
+metadata: object
+createdAt: Date
+updatedAt: Date
+addPrerequisite(kpId: string) void
+removePrerequisite(kpId: string) void
}
class Question {
+id: string
+knowledgePointId: string
+type: QuestionType
+content: string
+options: object|null
+answer: string
+explanation: string
+difficulty: 1|2|3|4|5
+status: QuestionStatus
+source: QuestionSource
+createdBy: string
+metadata: object
+createdAt: Date
+updatedAt: Date
+publish() void
+archive() void
}
Textbook "1" *-- "many" Chapter
Chapter "1" *-- "many" KnowledgePoint
KnowledgePoint "1" *-- "many" Question
KnowledgePoint "many" ..> "many" KnowledgePoint : PREREQUISITE_OF
```
### 2.2 值对象
| 值对象 | 字段 | 用途 |
| ---------------- | ------------------------------------------------------------------ | -------------------------- |
| `TextbookStatus` | `draft` / `pending_review` / `published` / `archived` | 教材状态机 |
| `ChapterStatus` | `draft` / `published` / `archived` | 章节状态机 |
| `QuestionStatus` | `draft` / `pending_review` / `published` / `rejected` / `archived` | 题目状态机(含审核流程) |
| `QuestionType` | `single_choice` / `multiple_choice` / `short_answer` / `essay` | 题型枚举 |
| `QuestionSource` | `manual` / `ai_generated` / `imported` | 题目来源(为 AI 出题预留) |
| `Difficulty` | `1` / `2` / `3` / `4` / `5` | 难度等级5 级) |
### 2.3 聚合间通信规则
| 场景 | 通信方式 | 理由 |
| ------------------------------------ | --------------------------------------- | ------------------------ |
| Textbook 聚合查 Chapter 列表 | 同服务内 Repository 直接查询 | 同一限界上下文内,强一致 |
| Chapter 删除时联动 KnowledgePoint | 同服务内 Service 编排,事务内级联 | 强一致 |
| KnowledgePoint 增删 → 更新 Neo4j 图 | Outbox 事件 → Kafka → Neo4j Sync Worker | 跨存储最终一致 |
| Question 发布 → ES 索引 | Outbox 事件 → Kafka → ES Sync Worker | 跨存储最终一致 |
| core-edu 教学内容变更 → content 失效 | Kafka 事件消费 → 标记 status=archived | 跨服务最终一致 |
### 2.4 状态机设计
#### Textbook 状态机
```mermaid
stateDiagram-v2
[*] --> draft: create
draft --> pending_review: submitReview
pending_review --> published: approve
pending_review --> draft: reject
published --> archived: archive
archived --> draft: restore (P6+ 预留)
```
#### Question 状态机(含 AI 出题审核流程预留)
```mermaid
stateDiagram-v2
[*] --> draft: create (manual/ai_generated/imported)
draft --> pending_review: submitReview
pending_review --> published: approve
pending_review --> rejected: reject
pending_review --> draft: reject (退回修改)
published --> archived: archive
rejected --> draft: edit (复活)
archived --> draft: restore (P6+)
```
> **设计预留**`source` 字段区分 `manual` / `ai_generated`,为 P5+ AI 辅助出题流程预留溯源能力。
---
## 3. 数据模型
### 3.1 MySQL Schema写模型主库
#### 3.1.1 `textbooks` 表
| 字段 | 类型 | 约束 | 说明 |
| ---------- | ------------ | -------------------------------------------- | ------------------------------- |
| id | varchar(32) | PK | cuid2 |
| title | varchar(255) | NOT NULL | 教材标题 |
| subject_id | varchar(32) | NOT NULL, INDEX | 学科 ID关联 iam/学科字典) |
| grade_id | varchar(32) | NOT NULL, INDEX | 年级 ID |
| version | varchar(32) | NOT NULL DEFAULT '1.0' | 教材版本(未来支持多版本) |
| status | varchar(32) | NOT NULL DEFAULT 'draft' | 状态机字段 |
| tenant_id | varchar(32) | NULL, INDEX | 租户 ID多租户预留 |
| metadata | json | NULL | 扩展字段(出版社/作者/ISBN 等) |
| created_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP | |
| updated_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE | |
**索引**
- PRIMARY KEY (`id`)
- INDEX `idx_textbooks_subject_grade` (`subject_id`, `grade_id`)
- INDEX `idx_textbooks_status` (`status`)
- INDEX `idx_textbooks_tenant` (`tenant_id`)
#### 3.1.2 `chapters` 表(补齐时间戳)
| 字段 | 类型 | 约束 | 说明 |
| ----------- | ------------ | -------------------------------------------- | ----------------------------------------------------------- |
| id | varchar(32) | PK | cuid2 |
| textbook_id | varchar(32) | NOT NULL, INDEX | 外键(应用层校验) |
| title | varchar(255) | NOT NULL | |
| order_num | int | NOT NULL DEFAULT 0 | 排序字段注意DB 列名 order_numTS schema 字段名 order |
| parent_id | varchar(32) | NULL, INDEX | 父章节 ID树形结构 |
| status | varchar(32) | NOT NULL DEFAULT 'draft' | |
| created_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP | **补齐** |
| updated_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE | **补齐** |
**索引**
- PRIMARY KEY (`id`)
- INDEX `idx_chapters_textbook_order` (`textbook_id`, `order_num`)
- INDEX `idx_chapters_parent` (`parent_id`)
#### 3.1.3 `knowledge_points` 表(补齐时间戳)
| 字段 | 类型 | 约束 | 说明 |
| ----------- | ------------ | -------------------------------------------- | -------------------------------- |
| id | varchar(32) | PK | cuid2 |
| chapter_id | varchar(32) | NOT NULL, INDEX | 外键 |
| title | varchar(255) | NOT NULL | |
| description | text | NULL | |
| difficulty | tinyint | NOT NULL DEFAULT 3 | 1-5 难度等级 |
| metadata | json | NULL | 扩展字段(关键词/标签/资源链接) |
| created_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP | **补齐** |
| updated_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE | **补齐** |
**索引**
- PRIMARY KEY (`id`)
- INDEX `idx_kp_chapter` (`chapter_id`)
#### 3.1.4 `questions` 表(扩展字段)
| 字段 | 类型 | 约束 | 说明 |
| ------------------ | ----------- | -------------------------------------------- | ------------------------------------- |
| id | varchar(32) | PK | cuid2 |
| knowledge_point_id | varchar(32) | NOT NULL, INDEX | 外键 |
| type | varchar(32) | NOT NULL | QuestionType 枚举 |
| content | text | NOT NULL | 题干HTML/markdown |
| options | json | NULL | 选项(选择题) |
| answer | text | NOT NULL | 标准答案 |
| explanation | text | NULL | 解析 |
| difficulty | tinyint | NOT NULL DEFAULT 3 | 1-5 |
| status | varchar(32) | NOT NULL DEFAULT 'draft' | **新增** 状态机字段 |
| source | varchar(32) | NOT NULL DEFAULT 'manual' | **新增** manual/ai_generated/imported |
| created_by | varchar(32) | NOT NULL | **新增** 创建者 user_id |
| metadata | json | NULL | **新增** 扩展(标签/年份/来源) |
| created_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP | |
| updated_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE | |
**索引**
- PRIMARY KEY (`id`)
- INDEX `idx_questions_kp` (`knowledge_point_id`)
- INDEX `idx_questions_type_difficulty` (`type`, `difficulty`)
- INDEX `idx_questions_status` (`status`)
- INDEX `idx_questions_source` (`source`)
#### 3.1.5 `content_outbox_events` 表Outbox 模式 · 新增)
| 字段 | 类型 | 约束 | 说明 |
| -------------- | ------------ | ---------------------------------- | ------------------------------------------------------ |
| event_id | varchar(64) | PK | UUID v4producer idempotency key |
| aggregate_type | varchar(64) | NOT NULL | 'Textbook' / 'Chapter' / 'KnowledgePoint' / 'Question' |
| aggregate_id | varchar(32) | NOT NULL | 聚合根 ID |
| event_type | varchar(64) | NOT NULL | 'edu.content.question.published' 等 |
| topic | varchar(128) | NOT NULL | Kafka topic 名 |
| payload | json | NOT NULL | 事件 JSON |
| status | varchar(16) | NOT NULL DEFAULT 'PENDING' | PENDING/PROCESSING/SENT/FAILED |
| retry_count | int | NOT NULL DEFAULT 0 | 重试次数 |
| created_at | timestamp | NOT NULL DEFAULT CURRENT_TIMESTAMP | |
| published_at | timestamp | NULL | 投递成功时间 |
| next_retry_at | timestamp | NULL | 下次重试时间(指数退避) |
**索引**
- PRIMARY KEY (`event_id`)
- INDEX `idx_outbox_status_retry` (`status`, `next_retry_at`)
- INDEX `idx_outbox_aggregate` (`aggregate_type`, `aggregate_id`)
### 3.2 Neo4j 图模型
#### 3.2.1 节点定义
```cypher
// 知识点节点
CREATE CONSTRAINT kp_id_unique IF NOT EXISTS
FOR (kp:KnowledgePoint) REQUIRE kp.id IS UNIQUE;
// 节点属性id, title, chapterId, textbookId, difficulty, createdAt, updatedAt
```
#### 3.2.2 关系定义
| 关系类型 | 方向 | 属性 | 用途 |
| --------------------- | ----------------------------------- | --------- | ----------------------- |
| `PREREQUISITE_OF` | `(kp1)-[:PREREQUISITE_OF]->(kp2)` | createdAt | kp1 是 kp2 的前置知识点 |
| `BELONGS_TO_CHAPTER` | `(kp)-[:BELONGS_TO_CHAPTER]->(ch)` | — | 知识点归属章节 |
| `BELONGS_TO_TEXTBOOK` | `(ch)-[:BELONGS_TO_TEXTBOOK]->(tb)` | — | 章节归属教材 |
#### 3.2.3 典型查询
```cypher
// 查询知识点前置链路(深度 1..5
MATCH path = (kp:KnowledgePoint {id: $kpId})-[:PREREQUISITE_OF*1..5]->(prereq:KnowledgePoint)
RETURN path;
// 查询学习路径(拓扑排序)
MATCH (kp:KnowledgePoint {id: $startId})
WITH collect(kp) AS starts
UNWIND starts AS s
MATCH path = (s)-[:PREREQUISITE_OF*0..10]->(target:KnowledgePoint)
RETURN path ORDER BY length(path);
```
### 3.3 Elasticsearch 索引设计P5 引入)
#### 3.3.1 `questions` 索引 mapping
```json
{
"mappings": {
"properties": {
"id": { "type": "keyword" },
"knowledge_point_id": { "type": "keyword" },
"chapter_id": { "type": "keyword" },
"textbook_id": { "type": "keyword" },
"subject_id": { "type": "keyword" },
"grade_id": { "type": "keyword" },
"type": { "type": "keyword" },
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"answer": { "type": "text", "analyzer": "ik_max_word" },
"explanation": { "type": "text", "analyzer": "ik_max_word" },
"difficulty": { "type": "byte" },
"status": { "type": "keyword" },
"source": { "type": "keyword" },
"tags": { "type": "keyword" },
"metadata": { "type": "object", "enabled": false },
"created_at": { "type": "date" },
"updated_at": { "type": "date" }
}
},
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"ik_max_word": { "type": "custom", "tokenizer": "ik_max_word" },
"ik_smart": { "type": "custom", "tokenizer": "ik_smart" }
}
}
}
}
```
#### 3.3.2 索引管理
- **索引创建**:服务启动时 `ensureIndex('questions', mapping)` 幂等
- **数据同步**:消费 `edu.content.question.published` / `edu.content.question.updated` / `edu.content.question.deleted` 事件增量更新
- **重建索引**:提供 `POST /internal/reindex` 端点全量重建P5+ 预留)
### 3.4 读写分离策略CQRS
| 操作 | 路径 | 说明 |
| -------------------- | ------------------------------------- | --------------------- |
| 写(创建/更新/删除) | Service → Repository → MySQL + Outbox | 单一写模型,事务保证 |
| 读 - CRUD 列表 | Service → Repository → MySQL | 默认读路径 |
| 读 - 全文检索 | Service → ES Client → ES | 检索专用读模型P5+ |
| 读 - 知识图谱 | Service → Neo4j Session → Neo4j | 图谱专用读模型 |
| 读 - 学习路径 | Service → Neo4j + MySQL JOIN | 混合读路径 |
---
## 4. API 设计
### 4.1 REST API当前 P4 已实现 + 待补齐)
| Method | Path | 权限 | 请求体 | 响应 | 说明 |
| ------ | --------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------- | ------------------------ | ------------------------ |
| POST | /textbooks | CONTENT_TEXTBOOK_CREATE | `{title, subjectId, gradeId, version?, metadata?}` | `{id, ...}` | 创建教材 |
| GET | /textbooks | CONTENT_TEXTBOOK_READ | `?subjectId=&gradeId=&page=1&pageSize=20` | `{items[], total, page}` | 分页列表(**待补分页** |
| GET | /textbooks/:id | CONTENT_TEXTBOOK_READ | — | `{id, ...}` | 详情 |
| PUT | /textbooks/:id | CONTENT_TEXTBOOK_UPDATE | `{title?, status?, metadata?}` | `{id, ...}` | 更新 |
| DELETE | /textbooks/:id | CONTENT_TEXTBOOK_DELETE | — | `{success: true}` | 删除 |
| POST | /chapters | CONTENT_CHAPTER_CREATE | `{textbookId, title, order, parentId?}` | `{id, ...}` | 创建章节 |
| GET | /chapters | CONTENT_CHAPTER_READ | `?textbookId=&page=&pageSize=` | `{items[], total}` | 列表 |
| GET | /chapters/:id | CONTENT_CHAPTER_READ | — | `{id, ...}` | 详情 |
| PUT | /chapters/:id | CONTENT_CHAPTER_UPDATE | `{title?, order?, status?}` | `{id, ...}` | 更新 |
| DELETE | /chapters/:id | CONTENT_CHAPTER_DELETE | — | `{success: true}` | 删除 |
| POST | /knowledge-points | CONTENT_KNOWLEDGE_POINT_CREATE | `{chapterId, title, description?, difficulty?}` | `{id, ...}` | 创建知识点 |
| GET | /knowledge-points | CONTENT_KNOWLEDGE_POINT_READ | `?chapterId=&page=&pageSize=` | `{items[], total}` | 列表 |
| GET | /knowledge-points/:id | CONTENT_KNOWLEDGE_POINT_READ | — | `{id, ...}` | 详情 |
| PUT | /knowledge-points/:id | CONTENT_KNOWLEDGE_POINT_UPDATE | `{title?, description?, difficulty?}` | `{id, ...}` | 更新 |
| DELETE | /knowledge-points/:id | CONTENT_KNOWLEDGE_POINT_DELETE | — | `{success: true}` | 删除 |
| GET | /knowledge-points/:id/prerequisites | CONTENT_KNOWLEDGE_POINT_READ | `?depth=5` | `{points[]}` | 前置链路 |
| POST | /knowledge-points/:id/prerequisites | CONTENT_KNOWLEDGE_POINT_UPDATE | `{prerequisiteId}` | `{success: true}` | 添加前置 |
| DELETE | /knowledge-points/:id/prerequisites/:prereqId | CONTENT_KNOWLEDGE_POINT_UPDATE | — | `{success: true}` | 删除前置 |
| POST | /questions | CONTENT_QUESTION_CREATE | `{knowledgePointId, type, content, options?, answer, explanation?, difficulty?, metadata?}` | `{id, ...}` | 创建题目 |
| GET | /questions | CONTENT_QUESTION_READ | `?knowledgePointId=&type=&difficulty=&status=&page=&pageSize=` | `{items[], total}` | 列表(含筛选) |
| GET | /questions/:id | CONTENT_QUESTION_READ | — | `{id, ...}` | 详情 |
| PUT | /questions/:id | CONTENT_QUESTION_UPDATE | `{content?, options?, answer?, explanation?, difficulty?, status?}` | `{id, ...}` | 更新 |
| DELETE | /questions/:id | CONTENT_QUESTION_DELETE | — | `{success: true}` | 删除 |
| GET | /questions/search | CONTENT_QUESTION_READ | `?q=&type=&difficulty=&knowledgePointId=&page=&pageSize=` | `{items[], total}` | ES 全文检索(**P5** |
### 4.2 gRPC API待实现对齐 004 §4.2 P4 启用)
#### 4.2.1 TextbookService
| RPC | 请求 | 响应 | 说明 |
| -------------- | ------------------------------------------------------------------- | ----------------------------------------------------- | ------------ |
| CreateTextbook | `CreateTextbookRequest{title, subject_id, grade_id, version}` | `Textbook` | |
| GetTextbook | `GetTextbookRequest{id}` | `Textbook` | |
| ListTextbooks | `ListTextbooksRequest{subject_id, grade_id, page_token, page_size}` | `ListTextbooksResponse{textbooks[], next_page_token}` | **待补分页** |
| UpdateTextbook | `UpdateTextbookRequest{id, title?, status?}` | `Textbook` | **新增** |
| DeleteTextbook | `DeleteTextbookRequest{id}` | `Empty` | **新增** |
#### 4.2.2 ChapterService新增
| RPC | 请求 | 响应 | 说明 |
| ------------- | ------------------------------------------------------------- | ---------------------------------- | ---- |
| CreateChapter | `CreateChapterRequest{textbook_id, title, order, parent_id?}` | `Chapter` | |
| ListChapters | `ListChaptersRequest{textbook_id, parent_id?}` | `ListChaptersResponse{chapters[]}` | |
| GetChapter | `GetChapterRequest{id}` | `Chapter` | |
#### 4.2.3 KnowledgeGraphService
| RPC | 请求 | 响应 | 说明 |
| ------------------ | ----------------------------------------------------- | --------------------------------------------- | ------------ |
| GetPrerequisites | `GetPrerequisitesRequest{knowledge_point_id, depth?}` | `KnowledgePointsResponse{points[]}` | 前置依赖 |
| GetLearningPath | `GetLearningPathRequest{student_id, subject_id}` | `LearningPath{points[], recommended_order[]}` | 学习路径推荐 |
| AddPrerequisite | `AddPrerequisiteRequest{kp_id, prerequisite_id}` | `Empty` | **新增** |
| RemovePrerequisite | `RemovePrerequisiteRequest{kp_id, prerequisite_id}` | `Empty` | **新增** |
#### 4.2.4 QuestionService**新增 · 阻塞 P5 AI 辅助出题**
| RPC | 请求 | 响应 | 说明 |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------- |
| CreateQuestion | `CreateQuestionRequest{knowledge_point_id, type, content, options?, answer, explanation?, difficulty?, source?, created_by?}` | `Question` | **新增**:供 AI 服务调用入库 |
| BatchCreateQuestions | `BatchCreateQuestionsRequest{questions[]}` | `BatchCreateQuestionsResponse{ids[], failed[]}` | **新增**AI 批量出题入库 |
| GetQuestion | `GetQuestionRequest{id}` | `Question` | |
| ListQuestions | `ListQuestionsRequest{knowledge_point_id?, type?, difficulty?, status?, page_token, page_size}` | `ListQuestionsResponse{questions[], next_page_token}` | |
| UpdateQuestion | `UpdateQuestionRequest{id, content?, answer?, status?}` | `Question` | |
| DeleteQuestion | `DeleteQuestionRequest{id}` | `Empty` | |
> **关键阻塞项**004 §9.3 明确"AI → Content gRPC CreateQuestions 入库",但 content.proto 当前缺 QuestionService RPC是 P5 阻塞性契约缺失,**阶段 2 设计必须补齐并向 coord 提请 proto 变更**。
### 4.3 错误响应结构
```json
{
"success": false,
"error": {
"code": "CONTENT_VALIDATION_ERROR",
"message": "Question type must be one of: single_choice, multiple_choice, short_answer, essay",
"details": { "field": "type", "value": "unknown" }
},
"requestId": "req_xxx",
"timestamp": 1736000000000
}
```
### 4.4 分页统一规范
| 字段 | 类型 | 说明 |
| ---------- | ------ | --------------------------- |
| `page` | number | 页码,从 1 开始 |
| `pageSize` | number | 每页条数,默认 20最大 100 |
| `total` | number | 总数 |
| `items` | array | 当前页数据 |
> gRPC 使用 `page_token` / `next_page_token`cursor 模式REST 使用 `page` / `pageSize`offset 模式),两者并行存在。
---
## 5. 事件设计
### 5.1 发布事件清单
| Event Type | Topic | 触发时机 | Payload | 消费者 |
| -------------------------------------------------- | -------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------- |
| `edu.content.textbook.created` | `edu.content.textbook.created` | 创建教材后 | `{event_id, aggregate_id, occurred_at, textbook_id, title, subject_id, grade_id, version}` | data-ana学情分析 |
| `edu.content.textbook.updated` | `edu.content.textbook.updated` | 更新教材后 | `{...textbook, fields_changed[]}` | data-ana |
| `edu.content.textbook.published` | `edu.content.textbook.published` | 教材发布(状态机变更) | `{textbook_id, published_at}` | data-ana、msg通知教师 |
| `edu.content.chapter.created` | `edu.content.chapter.created` | 创建章节后 | `{chapter_id, textbook_id, title, order}` | data-ana |
| `edu.content.knowledge_point.created` | `edu.content.knowledge_point.created` | 创建知识点后 | `{kp_id, chapter_id, title, difficulty}` | data-ana、Neo4j Sync Worker |
| `edu.content.knowledge_point.updated` | `edu.content.knowledge_point.updated` | 更新知识点后 | `{kp_id, fields_changed[]}` | data-ana、Neo4j Sync Worker |
| `edu.content.knowledge_point.prerequisite_added` | `edu.content.knowledge_point.prerequisite_added` | 添加前置依赖 | `{kp_id, prerequisite_id}` | Neo4j Sync Worker |
| `edu.content.knowledge_point.prerequisite_removed` | `edu.content.knowledge_point.prerequisite_removed` | 删除前置依赖 | `{kp_id, prerequisite_id}` | Neo4j Sync Worker |
| `edu.content.question.created` | `edu.content.question.created` | 创建题目后 | `{question_id, kp_id, type, difficulty, source}` | data-ana、ES Sync Worker |
| `edu.content.question.updated` | `edu.content.question.updated` | 更新题目后 | `{question_id, fields_changed[]}` | ES Sync Worker |
| `edu.content.question.published` | `edu.content.question.published` | 题目发布(状态机变更) | `{question_id, kp_id, type, published_at}` | AI出题参考、ES Sync Worker、data-ana |
| `edu.content.question.deleted` | `edu.content.question.deleted` | 删除题目后 | `{question_id}` | ES Sync Worker删除索引 |
### 5.2 消费事件清单
| Topic | 来源服务 | 处理逻辑 | 幂等键 |
| ------------------------------------------------------------ | -------- | -------------------------------------- | ---------- |
| `edu.teaching.exam.published` | core-edu | 联动相关题目状态变更(可选) | `event_id` |
| `edu.teaching.content.invalidated`**待 ai03 确认 topic** | core-edu | 教学内容失效通知,标记 status=archived | `event_id` |
### 5.3 TOPIC_MAP 路由表content 内部)
```typescript
const TOPIC_MAP: Record<string, string> = {
TextbookCreated: "edu.content.textbook.created",
TextbookUpdated: "edu.content.textbook.updated",
TextbookPublished: "edu.content.textbook.published",
ChapterCreated: "edu.content.chapter.created",
KnowledgePointCreated: "edu.content.knowledge_point.created",
KnowledgePointUpdated: "edu.content.knowledge_point.updated",
KnowledgePointPrerequisiteAdded:
"edu.content.knowledge_point.prerequisite_added",
KnowledgePointPrerequisiteRemoved:
"edu.content.knowledge_point.prerequisite_removed",
QuestionCreated: "edu.content.question.created",
QuestionUpdated: "edu.content.question.updated",
QuestionPublished: "edu.content.question.published",
QuestionDeleted: "edu.content.question.deleted",
};
```
### 5.4 Outbox Publisher 模式
```mermaid
sequenceDiagram
participant C as Client
participant CTL as Controller
participant SVC as Service
participant DB as MySQL (业务表 + outbox)
participant PUB as Outbox Publisher (worker)
participant K as Kafka
participant SYNC as Neo4j/ES Sync Worker
C->>CTL: POST /questions
CTL->>SVC: createQuestion()
SVC->>DB: BEGIN TX
SVC->>DB: INSERT INTO questions
SVC->>DB: INSERT INTO content_outbox_events
SVC->>DB: COMMIT
SVC-->>CTL: 返回响应(不阻塞)
CTL-->>C: 201 Created
loop 每 1s 轮询
PUB->>DB: SELECT * FROM content_outbox_events WHERE status='PENDING' AND next_retry_at <= NOW() LIMIT 100
PUB->>K: produce(topic, payload, key=aggregate_id)
alt 成功
PUB->>DB: UPDATE content_outbox_events SET status='SENT', published_at=NOW()
else 失败
PUB->>DB: UPDATE content_outbox_events SET status='PROCESSING', retry_count=retry_count+1, next_retry_at=NOW()+2^retry_count s
end
end
K-->>SYNC: consume edu.content.question.published
SYNC->>SYNC: 同步到 Neo4j 或 ES 索引
```
### 5.5 幂等去重设计
- **Producer 侧**Kafka producer 开启 `idempotent=true` + `transactionalId=content-producer`
- **Consumer 侧**:基于 `event_id` 字段去重,存储到 Redis SETNX 或独立 `processed_events`
- **Outbox 重投递**:消费者需容忍重复事件(同一 event_id 多次投递不产生副作用)
---
## 6. 横切关注点对齐清单
### 6.1 权限装饰器清单
| Controller 方法 | 权限点 | 说明 |
| ------------------------------- | --------------------------------------------------- | ----------------------- |
| TextbooksController.create | CONTENT_TEXTBOOK_CREATE | |
| TextbooksController.findAll | CONTENT_TEXTBOOK_READ | |
| TextbooksController.findOne | CONTENT_TEXTBOOK_READ | |
| TextbooksController.update | CONTENT_TEXTBOOK_UPDATE | |
| TextbooksController.remove | CONTENT_TEXTBOOK_DELETE | |
| ChaptersController.* | CONTENT_CHAPTER_{CREATE,READ,UPDATE,DELETE} | |
| KnowledgePointsController.* | CONTENT_KNOWLEDGE_POINT_{CREATE,READ,UPDATE,DELETE} | |
| QuestionsController.* | CONTENT_QUESTION_{CREATE,READ,UPDATE,DELETE} | |
| QuestionsController.search | CONTENT_QUESTION_READ | **新增** ES 检索 |
| 内部 RPC CreateQuestions (gRPC) | CONTENT_QUESTION_CREATE | AI 调用需带服务账号权限 |
### 6.2 错误码清单
| 错误码 | HTTP | 触发条件 |
| ------------------------- | ---- | ---------------------------------- |
| CONTENT_VALIDATION_ERROR | 400 | Zod 校验失败 / 题型非法 / 难度越界 |
| CONTENT_NOT_FOUND | 404 | 资源不存在 |
| CONTENT_PERMISSION_DENIED | 403 | 权限不足 |
| CONTENT_CONFLICT | 409 | 唯一约束冲突 / 状态机非法转换 |
| CONTENT_BUSINESS_ERROR | 422 | 业务规则违反(如循环依赖检测) |
| CONTENT_DATABASE_ERROR | 500 | Drizzle 操作异常 |
| CONTENT_INTERNAL_ERROR | 500 | 未知异常 |
| CONTENT_NEO4J_UNAVAILABLE | 503 | Neo4j 不可用且无降级路径 |
### 6.3 Logger 配置
- **库**pino已在 [logger.ts](../src/shared/observability/logger.ts) 实现)
- **日志级别**DEV_MODE 下 `debug`,生产 `info`
- **结构化字段**`requestId` / `userId` / `module` / `action` / `aggregateId` / `durationMs`
- **采样**error 级别全量warn 级别 100%info 级别 10%
### 6.4 Metrics 指标清单
| 指标名 | 类型 | 标签 | 说明 |
| --------------------------------------- | --------- | ------------------------ | ------------------- |
| `content_http_requests_total` | Counter | method/route/status_code | HTTP 请求总数 |
| `content_http_request_duration_seconds` | Histogram | method/route | HTTP 请求延迟 |
| `content_grpc_requests_total` | Counter | rpc_method/status | gRPC 调用总数 |
| `content_db_query_duration_seconds` | Histogram | table/operation | DB 查询延迟 |
| `content_neo4j_query_duration_seconds` | Histogram | query_type | Neo4j 查询延迟 |
| `content_es_index_duration_seconds` | Histogram | index | ES 索引延迟 |
| `content_outbox_pending_count` | Gauge | — | Outbox 待投递事件数 |
| `content_outbox_publish_total` | Counter | topic/status | Outbox 投递总数 |
| `content_outbox_retry_count` | Counter | topic | Outbox 重试次数 |
| `content_kafka_consumer_lag` | Gauge | topic | Kafka 消费滞后 |
### 6.5 Tracer 配置
- **库**@opentelemetry/sdk-node + auto-instrumentations
- **采样率**DEV_MODE 100%,生产 10%(尾部采样可后续引入)
- **服务名**`content`
- **OTLP endpoint**`OTEL_EXPORTER_OTLP_ENDPOINT` env 注入
### 6.6 健康检查
#### /healthzliveness
```json
{ "status": "ok", "service": "content", "timestamp": 1736000000000 }
```
不依赖任何外部资源,仅返回进程存活状态。
#### /readyzreadiness · 多依赖检查)
```json
{
"status": "ok" | "degraded" | "down",
"checks": {
"database": { "status": "ok", "latency_ms": 5 },
"neo4j": { "status": "ok", "latency_ms": 12 },
"elasticsearch": { "status": "skipped", "reason": "not configured" },
"kafka_producer": { "status": "ok" },
"kafka_consumer": { "status": "ok", "lag": 0 }
}
}
```
**判定规则**
- DB 不可用 → `status: down`(不可接受流量)
- Neo4j 不可用 → `status: degraded`(可接受流量,图谱查询降级)
- ES 未配置 → `skipped`(不参与判定)
- Kafka 不可用 → `status: degraded`(写请求可降级到 MySQL + Outbox最终一致
### 6.7 优雅关闭顺序
```mermaid
sequenceDiagram
participant SIG as SIGTERM
participant APP as NestJS App
participant HTTP as HTTP Server
participant CONS as Kafka Consumer
participant PUB as Outbox Publisher
participant NEO as Neo4j Driver
participant DB as Drizzle Pool
participant TR as Tracer
SIG->>APP: onApplicationShutdown
APP->>HTTP: server.close() (拒绝新请求,等待在途)
APP->>CONS: consumer.stop() (停止消费,等待在途消息处理)
APP->>PUB: publisher.stop() (完成当前批次投递)
APP->>NEO: driver.close()
APP->>DB: pool.end()
APP->>TR: tracer.shutdown()
```
---
## 7. 与其他模块的交互点
### 7.1 跨模块交互矩阵
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 | 状态 |
| ------ | ----------- | --------- | --------------------------------------------------------- | ------------------------------------------------------- | ------------------------ |
| 被调用 | teacher-bff | HTTP REST | GET /textbooks, GET /knowledge-points/* | 教师查教材/图谱 | ✅ 已实现 |
| 被调用 | student-bff | HTTP REST | GET /textbooks, GET /knowledge-points/:id/prerequisites | 学生查学习路径 | P3+ |
| 被调用 | ai | gRPC | KnowledgeGraphService.GetPrerequisites | AI 出题查询知识点 | P5 待实现 gRPC |
| 被调用 | ai | gRPC | **QuestionService.CreateQuestion / BatchCreateQuestions** | AI 生成题目入库 | **P5 阻塞 · 待补 proto** |
| 被调用 | teacher-bff | HTTP REST | GET /questions/search | 教师检索题库 | P5ES 引入后) |
| 消费 | core-edu | Kafka | `edu.teaching.exam.published` 等 | 联动题目状态 | **待 ai03 确认 topic** |
| 发布 | — | Kafka | `edu.content.*`11 类事件,见 §5.1 | 通知 data-ana / ai / Neo4j 同步 worker / ES 同步 worker | P4 实现 |
### 7.2 跨模块契约一致性提请coord 仲裁)
| # | 提请内容 | 阻塞性 | 备注 |
| --- | ---------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------ |
| P1 | content.proto 需补 QuestionService RPCCreateQuestion/BatchCreateQuestions/Update/Delete/Get/List | 🔴 阻塞 P5 AI 出题 | 提请 coord 修改 shared-proto |
| P2 | content.proto 需补 ChapterService RPC | 🟡 阻塞 gRPC 完整契约 | |
| P3 | content.proto 需补 Update/Delete/分页字段Textbook/KnowledgeGraph | 🟡 阻塞 gRPC 完整契约 | |
| P4 | events.proto 需追加 KnowledgePointEvent / QuestionEvent message含 source/createdBy/status 字段) | 🟡 content 发事件需明确 payload schema | |
| P5 | core-edu 教学内容失效事件 topic 待 ai03 设计确认 | 🟢 content 消费可选 | |
| P6 | DB 连接模式统一const db vs getDb()known-issues 自身描述矛盾 | 🟡 测试 mock 影响 | **建议统一为 getDb() 函数式**(对齐 classes 黄金模板) |
| P7 | ID 生成策略统一cuid2 vs randomUUID | 🟡 可读性 / 排序性能 | **建议统一为 cuid2**(对齐 classes 黄金模板) |
| P8 | `order` 字段命名冲突TS reserved word→ DB 列名 order_numTS 字段名 order | ✅ 已实现 | 仅记录 |
---
## 8. 演进路线
### 8.1 P4 阶段(当前)
**目标**CRUD + 知识图谱 + Outbox + Kafka 事件发布
**交付物**
- 4 张 MySQL 表(含补齐的时间戳、新增 status/source/created_by/metadata 字段)
- content_outbox_events 表 + Outbox Publisher worker
- Kafka produceridempotent + transactionalId
- Neo4j Sync Worker消费 content 自身事件异步同步图谱)
- /readyz 多依赖检查
- ZodError 在 GlobalErrorFilter 特殊处理(返回 400
- 测试覆盖率 ≥ 60%Repository/Service 单元测试)
### 8.2 P5 阶段
**目标**ES 全文检索 + gRPC 完整契约 + AI 出题入库
**交付物**
- ES 索引 mapping + ensureIndex + 数据同步 consumer
- QuestionService gRPC controller含 BatchCreateQuestions对齐 004 §9.3
- ChapterService / Update/Delete RPC
- 题库全文检索 APIGET /questions/search
- 检索延迟 < 200msP5 退出标准)
- 测试覆盖率 ≥ 80%
### 8.3 P6+ 长远演进(架构预留点)
| 演进方向 | 当前架构预留 | 触发条件 |
| ---------------------- | ---------------------------------------------------------------------------- | -------------------- |
| **AI 辅助出题工作流** | Question.source=ai_generated + status=pending_review 状态机 + createdBy 字段 | P5 AI 服务上线后 |
| **知识图谱可视化** | Neo4j 节点属性可扩展 + Cypher 查询预留(深度 1..10 | 教师端图谱可视化需求 |
| **教材版本管理** | Textbook.version 字段 + 状态机 | 多版本教材并行场景 |
| **跨租户内容共享** | tenant_id 字段预留 | SaaS 多学校部署 |
| **多模态内容** | metadata jsonb 字段(可存视频 URL/音频/AR 资源) | 多媒体教学需求 |
| **个性化学习路径推荐** | GetLearningPath RPC + LearningPath message | 与 data-ana 配合实现 |
| **内容合规审核工作流** | status=pending_review / rejected 状态机 + 审核日志表 | 教育合规要求 |
| **国际化内容** | metadata jsonb 可存多语言字段 | 多语言教学场景 |
| **图神经网络分析** | Neo4j GDS 库预留 | 学情深度分析需求 |
### 8.4 与黄金模板对齐
content 在 P4 完成后,应将以下模式回写到 classes 黄金模板known-issues §2.2 提及"Outbox/CDC/长连接模式回写"
- Outbox 表 schema + Publisher worker 模式
- 多依赖 /readyz 检查模式
- Neo4j 异步同步模式
- 状态机字段status设计模式
---
## 9. 风险与假设
### 9.1 技术风险
| # | 风险 | 影响 | 缓解措施 |
| --- | ------------------------------------ | ----------------------- | ----------------------------------------------------- |
| R1 | Neo4j 与 MySQL 双向一致性问题 | 知识图谱与 DB 不一致 | Outbox 事件驱动同步 + 幂等去重 + 最终一致 |
| R2 | ES 索引重建期间检索不可用 | 题库检索中断 | alias 切换模式(双索引蓝绿) |
| R3 | AI 批量出题 CreateQuestions 高并发 | DB 写入瓶颈 | BatchCreateQuestions 限制 batch_size ≤ 100 + 异步队列 |
| R4 | Kafka 消费滞后导致 Neo4j/ES 同步延迟 | 图谱/检索数据陈旧 | 监控 consumer lag + 报警阈值 |
| R5 | 知识点循环依赖A → B → A | GetPrerequisites 死循环 | Neo4j 查询限定深度 1..5 + 应用层 DFS 检测 |
| R6 | 循环依赖 Outbox 表无限增长 | DB 空间耗尽 | SENT 状态 7 天后归档到冷库 |
| R7 | gRPC 与 REST 双契约维护成本 | 接口漂移 | proto 单一源 + 自动生成 DTO + 契约测试 |
### 9.2 假设
| # | 假设 | 依赖 | Fallback |
| --- | --------------------------------------------------------- | ------------- | ------------------------------------------------------------ |
| A1 | core-edu 会发布 `edu.teaching.content.invalidated` 类事件 | ai03 设计确认 | content 不消费此事件,仅依赖管理端手动归档 |
| A2 | IAM 提供学科字典 / 年级字典接口 | ai06 设计确认 | content 维护本地 subject_id/grade_id 字符串字典 |
| A3 | Neo4j 5.x 可用 | infra 部署 | NEO4J_URL 未配置时 driver=null图谱查询返回 503 |
| A4 | ES 8.x 可用P5+ | infra 部署 | ES_URL 未配置时 esClient=null检索降级到 MySQL LIKE |
| A5 | Kafka 集群可用 | infra 部署 | Outbox Publisher 失败时重试,最终一致 |
| A6 | AI 服务调用 CreateQuestions 时携带服务账号 | ai12 设计确认 | PermissionGuard 校验服务账号权限点 `CONTENT_QUESTION_CREATE` |
### 9.3 待 coord 仲裁项
1. **content Outbox 启用时机**P4 还是 P5按 004 §12.2 强制条款content 发事件必须 Outbox建议 P4 启用)
2. **content ES 启用时机**P4 还是 P5pending-features 注明 P5建议 P5 启用)
3. **content.proto proto 包名**:保持 `next_edu_cloud.content.v1`(已仲裁)
4. **DB 连接模式统一**const db vs getDb(),建议统一为 getDb()
5. **ID 策略统一**cuid2 vs randomUUID建议统一为 cuid2
6. **QuestionService proto 是否在 P4 阶段补齐**:建议 P4 提前补齐,避免 P5 阻塞
---
## 10. 实施计划(建议)
### 10.1 P4 阶段任务拆分
| # | 任务 | 优先级 | 预估文件改动 |
| --- | -------------------------------------------------------------- | ------ | ------------------------------------- |
| T1 | 补齐 chapters / knowledge_points 时间戳字段 | P0 | schema 迁移 + 重新生成 Drizzle types |
| T2 | questions 表新增 status / source / created_by / metadata 字段 | P0 | schema 迁移 |
| T3 | 实现 content_outbox_events 表 + Outbox Publisher worker | P0 | 新建 shared/outbox/ 目录 |
| T4 | 实现 Kafka produceridempotent + transactionalId | P0 | 新建 shared/kafka/producer.ts |
| T5 | 实现 Neo4j Sync Worker消费 content 自身事件异步同步) | P0 | 新建 shared/sync/neo4j-sync.worker.ts |
| T6 | 重构 knowledge-points.service.ts移除同步写 Neo4j改为发事件 | P0 | 重构 service 层 |
| T7 | 完善 /readyz 多依赖检查 | P1 | 修改 health.controller.ts |
| T8 | ZodError 在 GlobalErrorFilter 特殊处理 | P1 | 修改 global-error.filter.ts |
| T9 | 补齐 Repository 层textbooks/questions | P1 | 抽象 repository |
| T10 | DB 连接模式改 getDb() 函数式 | P2 | 修改 database.ts |
| T11 | ID 策略改 cuid2 | P2 | 修改 service 层 |
| T12 | 补齐单元测试Service/Repository | P2 | 新建 *.spec.ts |
| T13 | 修正 README与实现对齐 | P2 | 修改 README.md |
### 10.2 P5 阶段任务(前置)
| # | 任务 | 阻塞性 |
| --- | --------------------------------------------------------------------- | --------------- |
| F1 | content.proto 补 QuestionService / ChapterService / Update/Delete RPC | 🔴 阻塞 AI 出题 |
| F2 | 实现 QuestionService gRPC controller | 🔴 阻塞 AI 出题 |
| F3 | 引入 @elastic/elasticsearch + 实现 ES mapping + ensureIndex | 🔴 阻塞检索 |
| F4 | 实现 ES Sync Worker消费 content 事件同步索引) | 🔴 阻塞检索 |
| F5 | 实现 GET /questions/search 检索 API | 🔴 阻塞检索 |
| F6 | 实现 ChapterService / TextbookService Update/Delete gRPC controller | 🟡 |
---
## 11. 与黄金模板对齐 checklist
| 项 | classes 黄金模板 | content 当前 | content 目标P4 完成) |
| ----------------- | ---------------------------- | ------------- | --------------------------------------- |
| 权限装饰器 | @RequirePermission 全覆盖 | ✅ 16 端点 | ✅ 保持 |
| 错误码前缀 | CLASSES_* | ✅ CONTENT_* | ✅ 保持 |
| logger | pino | ✅ | ✅ 保持 |
| metrics | prom-client + /metrics | ✅ | ✅ 保持 |
| tracer | OTel + auto-instrumentations | ✅ | ✅ 保持 |
| /healthz | ✅ | ✅ | ✅ 保持 |
| /readyz | DB SELECT 1 | ⚠️ 仅 DB | ✅ 多依赖DB/Neo4j/Kafka |
| 优雅关闭 | LifecycleService | ✅ | ✅ 统一到 LifecycleService |
| 测试覆盖率 | 60% | 0% | ≥ 60% |
| Dockerfile | 多阶段 | ✅ | ✅ 保持 |
| Zod 校验 | schema.parse | ⚠️ 部分 | ✅ Controller 层全 parse + ZodError 400 |
| GlobalErrorFilter | ✅ | ✅ | ✅ + ZodError 分支 |
| DB 连接模式 | getDb() 函数式 | ⚠️ const db | ✅ 改 getDb() |
| ID 生成 | cuid2 | ⚠️ randomUUID | ✅ 改 cuid2 |
| Repository 抽象 | ✅ | ⚠️ 部分 | ✅ 全部抽象 |
| Outbox | ❌(黄金模板自身无) | ❌ | ✅ **新增** Outbox 模式(回写黄金模板) |
| 状态机字段 | ❌ | ❌ | ✅ status 字段 |
---
**AI Agent**: ai05 (content + msg)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式(直接 push main

View File

@@ -0,0 +1,194 @@
# 模块理解确认书 — core-edu
> AI 标识ai08按 [ai-allocation.md §3.2](../../../docs/architecture/ai-allocation.md) 接管,原 ai03 阶段 1 文档已归位)
> 负责模块core-eduP3
> 阶段:架构设计外包 · 阶段 1全局理解· ai08 审计补全版
> 日期2026-07-09初稿/ 2026-07-09ai08 审计补全)
> 关联文档:[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[coord 交叉审查报告](../../../docs/architecture/coord-cross-review.md)
---
## 1. 我在架构中的位置
- **层级**业务微服务层L5同时承载 **D2 教学组织****D3 教学核心** 两个限界上下文(见 [004 §1.1b](../../../docs/architecture/004_architecture_impact_map.md#11b-业务领域视角)
- **上游**
- teacher-bffP2 已 REST fetch 调用 `/exams` `/homework` `/grades`
- student-bffP3 依赖,作答/查成绩)
- parent-bffP4 依赖,查看子女成绩/考勤)
- api-gateway直接路由 `/api/v1/exams` 等,绕过 BFF 的内部直连场景)
- **下游**MySQL独占库 `core_edu_*` 表前缀、KafkaOutbox 事件发布、RedisP3 引入:作业高并发提交锁 + DataScope 缓存 + 短期聚合缓存)
- **通信方式**
- 入口 HTTP当前 REST`/exams``/homework``/grades`),端口 **3004**
- 入口 gRPC**P3 启用**[004 §4.2](../../../docs/architecture/004_architecture_impact_map.md#42-grpc-启用阶段矩阵) 裁决),端口 **50053**proto 已定义 `ExamService/HomeworkService/GradeService`
- 出口Kafka 事件Outbox 模式,[004 §12.2](../../../docs/architecture/004_architecture_impact_map.md#12-架构约束) 强制core-edu 不享受派生数据豁免)
- **端口声明**[coord 全局端口矩阵](../../../docs/architecture/coord-cross-review.md#43-全局端口矩阵coord-裁决基线已同步至-004-12)
- HTTP3004P3
- gRPC50053P3 启用)
- /metrics随 HTTP 3004Prometheus 抓取)
## 2. 我的限界上下文
- **聚合职责**:跨 **D2 教学组织**classes 模块,待合并)+ **D3 教学核心**exams / homework / grades + 排课/考勤 course/lesson/schedule/attendance 四表,[pending-features P3](../../../docs/architecture/roadmap/pending-features.md#p3-核心教学阶段m7-m10) 要求)
- **聚合根**Exam、Homework、Grade、Class待合并、Course、Lesson、Schedule、AttendanceP3 新增)
- **我不负责**
- 不负责题库内容(→ content 服务core-edu 通过事件通知 content 教学内容变更,[004 §4 服务依赖图](../../../docs/architecture/004_architecture_impact_map.md#4-服务依赖图) `CoreEdu -.事件.-> Content`
- 不负责学情分析/掌握度计算(→ data-ana 服务,消费 core-edu 事件)
- 不负责通知投递(→ msg 服务,消费 core-edu 事件)
- 不负责 AI 出题(→ ai 服务ai 通过 gRPC 调 content 查题库)
- 不负责权限/角色管理(→ iamcore-edu 仅消费 iam 事件同步教师关联)
- **数据自治**:独占 `core_edu` 数据库init-sql 中为 `next_edu_cloud.core_edu_*`),表前缀 `core_edu_*`**禁止跨库联表**[004 §12.1](../../../docs/architecture/004_architecture_impact_map.md#12-架构约束)
- **CDC 联动**MySQL binlog 已通过 Debezium 投递到 Kafka 的 `edu-cdc.next_edu_cloud.core_edu_grades` / `core_edu_exams` / `core_edu_homework` / `core_edu_attendance` topic由 data-ana 消费写 ClickHouse 宽表([known-issues §2.6](../../../docs/troubleshooting/known-issues.md) 已实施)
## 3. 我与外部的契约
- **暴露的 gRPC 契约**[core_edu.proto](../../../packages/shared-proto/proto/core_edu.proto),包名 `next_edu_cloud.core_edu.v1`,已定义待 P3 启用 server
- `ExamService`CreateExam / GetExam / ListExamsByClass / UpdateExam / DeleteExam
- `HomeworkService`AssignHomework / GetHomework / ListHomeworkByClass / SubmitHomework
- `GradeService`RecordGrade / GetGrade / ListGradesByStudent/Exam/Homework
- **缺口**:缺 `AttendanceService`coord 整改清单 #14P3 补全,[coord §6](../../../docs/architecture/coord-cross-review.md#6-裁决整改清单汇总)
- **发布的领域事件**[events.proto](../../../packages/shared-proto/proto/events.proto) + [outbox.publisher.ts TOPIC_MAP](../src/shared/outbox/outbox.publisher.ts)
> **coord 已仲裁**[coord §3.1](../../../docs/architecture/coord-cross-review.md#31-core-edu-教学事件-topic-命名双轨p0-最严重)topic 命名**统一为 `edu.teaching.<aggregate>.<action>`**,原代码中的 `edu.exam.events` / `edu.homework.events` / `edu.grade.events` / `edu.class.events` 风格**违规**P3 必须改 TOPIC_MAP。下表已按裁决后规范列出。
| 事件eventType | Topic裁决后 | 触发时机 | 消费者 | 当前状态 |
| ------------------- | ------------------------------------------ | ------------------ | ------------- | ---------- |
| exam.created | `edu.teaching.exam.created` | CreateExam 事务内 | msg、data-ana | ✅ 已发布 |
| exam.updated | `edu.teaching.exam.updated` | UpdateExam | msg | ✅ 已发布 |
| exam.deleted | `edu.teaching.exam.deleted` | DeleteExam | data-ana | ✅ 已发布 |
| exam.published | `edu.teaching.exam.published` | 考试发布(状态机) | msg、data-ana | ❌ P3 新增 |
| homework.assigned | `edu.teaching.homework.assigned` | AssignHomework | msg、data-ana | ✅ 已发布 |
| homework.submitted | `edu.teaching.homework.submitted` | SubmitHomework | data-ana、msg | ✅ 已发布 |
| homework.graded | `edu.teaching.homework.graded` | 教师批改完成 | msg、data-ana | ❌ P3 新增 |
| grade.recorded | `edu.teaching.grade.recorded` | RecordGrade | data-ana、msg | ✅ 已发布 |
| grade.updated | `edu.teaching.grade.updated` | 批改后修正 | data-ana | ❌ P3 新增 |
| class.transferred | `edu.org.class.created`(合并后归 org 域) | classes 合并后 | data-ana | ⚠️ 待合并 |
| attendance.recorded | `edu.teaching.attendance.recorded` | 考勤录入 | data-ana、msg | ❌ P3 新增 |
- **事件 schema 版本化**:所有事件 payload 必须含 `schema_version` 字段(默认 `"v1"`),消费端按版本处理([known-issues §1.3](../../../docs/troubleshooting/known-issues.md))。当前 outbox payload 仅含业务字段,**P3 必须补 `schema_version`**。
- **消费的事件**
- `edu.identity.user.created` / `edu.identity.user.updated` / `edu.identity.user.deleted`IAM初始化/同步教师默认班级关联,[004 §7.2](../../../docs/architecture/004_architecture_impact_map.md#72-事件-topic-分类) 已定义,**当前未消费**P3 待补)
- `edu.insight.mastery.updated`data-ana掌握度更新后 core-edu 可消费用于推荐个性化练习,[004 §8.3](../../../docs/architecture/004_architecture_impact_map.md#83-异步事件闭环))— P3 可选P4 强制
- **错误码前缀**`CORE_EDU_*`(见 [application-error.ts CoreEduErrorCode](../src/shared/errors/application-error.ts)子域exams/homework/grades/attendance**统一用 `CORE_EDU_*`**,不再细分 `EXAMS_` / `HOMEWORK_` / `GRADES_`[coord §5.5](../../../docs/architecture/coord-cross-review.md#55-p1-问题core-edu-子模块前缀) 仲裁)
- **响应信封**:必须遵循 [004 §11.5](../../../docs/architecture/004_architecture_impact_map.md#115-统一响应信封actionstate) ActionState 信封 `{success, data? | error: {code, message, details?, traceId?}}`GlobalErrorFilter 已注册
## 4. 我的技术栈
- 语言TypeScript 5.5+ESM 模式NestJS ESM 模式下相对 import 必须 `.js` 后缀)
- 框架NestJS 10
- ORMDrizzle ORMmysql2 driver直接 `db` 导出,**与 classes 的 `getDb()` 不一致**,需 P3 统一)
- 存储MySQL 8独占库 `core_edu_*` 表前缀、RedisP3 引入、Kafkakafkajsidempotent + transactionalId `core-edu-tx`
- **gRPC server**P3 启用(`@grpc/grpc-js` + `@bufbuild/protobuf`coord 已在 [buf.gen.yaml](../../../packages/shared-proto/buf.gen.yaml) 补 gRPC 插件,[coord 整改 #16](../../../docs/architecture/coord-cross-review.md#6-裁决整改清单汇总)
- **Temporal**P3 引入,仅试点 1 个工作流(考试发布编排:创建作业→通知,[pending-features P3](../../../docs/architecture/roadmap/pending-features.md#p3-核心教学阶段m7-m10)
- 可观测pino + prom-client已 4 指标http_requests_total / request_duration_seconds / outbox_pending / outbox_published_total+ OTel auto-instrumentations已具备
## 5. 我的阶段归属
- **P3 核心教学**:考试全生命周期 + Outbox + Kafka 事件落地
- **退出标准**[pending-features P3](../../../docs/architecture/roadmap/pending-features.md#p3-核心教学阶段m7-m10)):教师创建考试 → 发布 → 学生作答 → 教师批改 → 事件到 Kafka → 成绩统计更新 → 全链路可观测
- **P3 关键功能**
1. CoreEdu 服务考试/作业/成绩域 CRUD + 批改业务编排 + Outbox 事件发布
2. MySQL schemaexams / exam_questions / homework_assignments / homework_submissions / homework_answers / grade_records / outbox_events**当前 schema 仅 4 表**,缺 exam_questions / homework_submissions / homework_answersP3 必须补全)
3. Outbox 模式 + Outbox relay worker**当前 relay 在 NestJS 进程内 setInterval 5spending-features 提及"独立 Go 服务 `services/outbox-relay/`"是远期目标P3 保留进程内模式**
4. Kafka topics`edu.teaching.exam.published` / `edu.teaching.homework.graded` / `edu.teaching.grade.recorded`(按 coord 仲裁后的命名)
5. Teacher BFF 扩展(考试/作业/成绩的查询与 mutation
6. teacher-portal 扩展(考试创建/作业批改/成绩查看页面)
7. student-portal 微前端(学生作答作业页面)
8. Temporal 试点 1 个工作流(考试发布编排)
9. **gRPC server 启用(端口 50053**
10. **排课/考勤数据模型course/lesson/schedule/attendance 四表)+ AttendanceService proto 补全**
- **依赖上游**P1 classes 黄金模板、P2 iam用户身份 + 权限 + DataScope
- **下游依赖方**student-bffP3、parent-bffP4查看子女成绩/考勤、data-ana消费 CDC + 领域事件、msg消费事件触发通知、content接收教学内容变更事件
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
- [x] 权限装饰器 `@RequirePermission`exams.controller 全覆盖;需核对 homework/grades controller
- [x] 错误码前缀 `CORE_EDU_*`(已用 [CoreEduErrorCode 枚举](../src/shared/errors/application-error.ts)
- [x] logger / metrics / tracer 三支柱
- [x] `/healthz` 健康检查
- [x] `/readyz`(已实现 DB SELECT 1 探针,[health.controller.ts](../src/shared/health/health.controller.ts) Drizzle `db.execute(sql\`SELECT 1\`)`**需补 Kafka 连接探针 + Redis ping 探针**
- [x] 优雅关闭 SIGTERMmain.ts 已处理 outboxPublisher.stop + disconnectKafka
- [ ] 测试覆盖率 ≥ 80%**当前 0%**,无测试文件,仅 vitest.config.ts 配置就绪)
- [ ] Dockerfile 多阶段构建(需核对)
- [ ] Zod 输入验证(**当前 Controller 直接接收 body未 Zod 校验**classes 用 zod schema
- [x] GlobalErrorFilter 统一兜底(响应信封 ActionState 对齐,[coord §5.7](../../../docs/architecture/coord-cross-review.md#57-p2-问题错误信封结构不一致)
- [x] Outbox 模式(事务内写业务表 + outbox 表,独立 publisher 投递,**TOPIC_MAP 命名违规待修正**
- [ ] **gRPC server 实现**P3 启用,端口 50053proto 已定义)
- [ ] **ActionState 信封 traceId**GlobalErrorFilter 已输出 traceId需验证
- [ ] **DataScope 下推**004 §5.3 要求 Repository 层根据 dataScope 注入 WHERE当前未实现
- [ ] **Drizzle 访问方式统一**core-edu 用 `export const db`classes 用 `getDb()` 函数式,需 P3 统一为 `getDb()`
- [ ] **kafka.ts 用 logger 替代 console.log/warn**[config/kafka.ts](../src/config/kafka.ts) L20/L22 违反 [known-issues §1.4](../../../docs/troubleshooting/known-issues.md) "禁止 console.*" 规则)
---
## 服务审计表 — ai08core-edu
> 对照 [黄金模板 classes 服务](../../classes/src/),审计已实现的 core-edu 服务。状态:✅ 达标 / ⚠️ 部分 / ❌ 缺失
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile | gRPC server | ActionState |
| -------- | -------------------------------------- | --------------- | -------------------------------- | --------------------- | ------------ | -------- | ------------------------------------- | -------- | ---------- | ---------- | ---------------- | ----------- |
| core-edu | ✅ `@RequirePermission(EXAM_*)` 全覆盖 | ✅ `CORE_EDU_*` | ⚠️ pinokafka.ts 仍用 console | ✅ prom-client 4 指标 | ✅ OTel auto | ✅ | ⚠️ DB SELECT 1缺 Kafka/Redis 探针) | ✅ | 0% ❌ | 待核对 | ❌ P3 启用 50053 | ✅ 已对齐 |
### 审计发现的关键差距P3 阶段 2 设计需解决)
> ai03 原列 12 项 + ai08 审计新增 8 项 = 20 项
1. ❌ 考试生命周期状态机缺失(当前仅 `draft` 初值,无 `published → in_progress → grading → graded → archived` 转换与校验)
2. ❌ 作业状态机不完整(仅 `assigned → submitted`,缺 `graded`pending-features 要求 `HomeworkGraded` 事件)
3. ❌ 成绩录入无业务校验(不校验 exam/homework 是否存在、score 是否在 totalScore 范围内、是否重复录入)
4. ❌ 作业提交高并发优化缺失([004 §9.2](../../../docs/architecture/004_architecture_impact_map.md#92-高并发提交) 要求 Redis 分布式锁 + 排队)
5. ❌ 无 `grade.updated` / `homework.graded` 事件触发点proto 已定义service 未实现)
6. ❌ 未消费 IAM `user.created` / `user.updated` / `user.deleted` 事件(初始化教师默认关联)
7. ⚠️ Drizzle `db` 直接导出 vs classes 的 `getDb()` 函数式 — **不一致**,建议统一为 `getDb()`
8. ⚠️ [kafka.ts](../src/config/kafka.ts) 用 `console.log`/`console.warn`,应改用结构化 logger
9. ⚠️ classes 模块在 core-edu 仅有 `classes.module.ts` 占位,**P3 待合并**classes 服务代码迁入 + 删除独立 services/classes
10. ⚠️ 入口仍为 RESTproto gRPC 契约已定义但未接入 `@grpc/grpc-js` + buf generate 代码
11. ❌ 无 Zod 输入验证Controller 直接接收 `body: CreateExamInput`,未走 zod schema
12. ❌ 无测试
13.**TOPIC_MAP 命名违规**coord 已仲裁统一为 `edu.teaching.<aggregate>.<action>`,当前代码用 `edu.exam.events` 等表名分组风格,[coord §3.1](../../../docs/architecture/coord-cross-review.md#31-core-edu-教学事件-topic-命名双轨p0-最严重)
14.**排课/考勤数据模型缺失**pending-features P3 要求 course/lesson/schedule/attendance 四表,当前 core-edu 仅 exams/homework/grades/outbox 四表)
15.**core_edu.proto 缺 AttendanceService**coord 整改 #14P3 必须补全)
16.**DataScope 下推未实现**[004 §5.3](../../../docs/architecture/004_architecture_impact_map.md#53-datascop-6-级数据范围) 要求 Repository 层根据 dataScope 注入 WHERE当前 PermissionGuard 仅做粗粒度角色判断,未做行级数据过滤)
17.**事件 schema_version 字段缺失**[known-issues §1.3](../../../docs/troubleshooting/known-issues.md) 要求 Kafka 事件带 `schema_version`,当前 outbox payload 仅含业务字段)
18.**Temporal 工作流未引入**pending-features P3 要求试点 1 个工作流:考试发布编排,当前未集成)
19. ⚠️ **outbox schema 缺少 event_id 字段**(消费端幂等去重要求 event_id当前 outbox 仅用 id 作主键,但 event_id 应独立于 outbox id 以支持重投递幂等)
20.**成绩计算公式未配置化**ai-allocation §5 ai08 设计重点要求支持加权/平均/自定义公式,当前仅原始 score 存储)
### 跨模块契约对齐ai08 接管后核对 coord 仲裁结果)
| 待确认项 | coord 仲裁结论 | 状态 |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| iam `user.created` 等事件 topic | `edu.identity.user.created` / `.updated` / `.deleted`[004 §7.2](../../../docs/architecture/004_architecture_impact_map.md#72-事件-topic-分类) | ✅ 已仲裁core-edu P3 实现消费端 |
| core-edu 端口 3004 + gRPC 50053 | 不冲突,已纳入 [coord 全局端口矩阵](../../../docs/architecture/coord-cross-review.md#43-全局端口矩阵coord-裁决基线已同步至-004-12) | ✅ 已仲裁 |
| Kafka topic 命名 | **统一为 `edu.teaching.<aggregate>.<action>`**[coord §3.1](../../../docs/architecture/coord-cross-review.md#31-core-edu-教学事件-topic-命名双轨p0-最严重) | ✅ 已仲裁core-edu P3 修 TOPIC_MAP |
| data-ana 消费 core-edu 事件 | 消费 `edu.teaching.exam.created` / `homework.submitted` / `grade.recorded` + CDC topics | ✅ ai06 已确认data-ana 已实施 CDC 消费 |
| msg 消费 core-edu 事件 | 消费 `edu.teaching.exam.created` / `homework.assigned` / `grade.recorded` 触发通知 | ⚠️ 待 ai05 在 msg 02 文档确认消费契约 |
| proto 包名规范 | 保持 `next_edu_cloud.core_edu.v1`[coord §2.1](../../../docs/architecture/coord-cross-review.md#21-proto-包名规范冲突p0-全局) | ✅ 已仲裁 |
| 错误码前缀 | `CORE_EDU_*` 统一(不再细分 `EXAMS_`/`HOMEWORK_`/`GRADES_`[coord §5.5](../../../docs/architecture/coord-cross-review.md#55-p1-问题core-edu-子模块前缀) | ✅ 已仲裁 |
| core_edu.proto AttendanceService 缺失 | P3 补全([coord 整改 #14](../../../docs/architecture/coord-cross-review.md#6-裁决整改清单汇总) | ❌ 待 ai08 P3 补全 proto + service 实现 |
---
## 下一步(阶段 2 入口)
ai08 进入阶段 2按 [ai-allocation.md §5 ai08 设计重点](../../../docs/architecture/ai-allocation.md) 产出模块架构设计文档 `02-architecture-design.md`
- **core-edu 模块架构设计**
- classes 黄金模板对齐Drizzle `getDb()` 统一 + Zod + 测试 + Dockerfile 多阶段)
- 考试生命周期状态机(草稿 → 已发布 → 作答中 → 批改中 → 已出分 → 已归档)
- Outbox 事件定义与发布(补全 `exam.published` / `homework.graded` / `grade.updated` / `attendance.recorded`TOPIC_MAP 重命名为 `edu.teaching.*`payload 补 `schema_version` + `event_id`
- 成绩计算公式与配置化(加权/平均/自定义公式)
- 作业提交高并发优化Redis 分布式锁 + 排队机制)
- 排课/考勤数据模型course/lesson/schedule/attendance 四表)+ AttendanceService proto 补全
- 与 Kafka 的 Relay Worker 轮询逻辑(保留进程内模式,远期迁出独立 Go 服务)
- gRPC server 启用(端口 50053
- DataScope 下推Repository 层 WHERE 注入)
- Temporal 工作流试点(考试发布编排)
- 消费 IAM 事件user.created/updated/deleted
阶段 2 设计需先解决上述 20 项差距与跨模块契约对齐。
---
**AI Agent**: ai08 (core-edu)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式(直接 push main

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,232 @@
# 模块理解确认书 — data-ana
> AI 标识ai11v2 由 ai11 审核 ai06 v1 后修订)
> 负责模块data-anaP4
> 语言Python 3.12+ / FastAPI 0.115+
> 阶段:架构设计外包 · 阶段 1全局理解v2 审核版
> 日期2026-07-09v1 by ai06/ 2026-07-10v2 审核修订 by ai11
> 关联文档:[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[known-issues.md](../../../docs/troubleshooting/known-issues.md)
> 审核修订说明ai-allocation.md §3.2 将 data-ana 重新分配给 ai11ai 单独分配给 ai12ai11 接手后对 ai06 v1 进行审核,本版为 v2 修订。
---
## 阶段 1 必读清单完成确认
按 ai-allocation.md §4ai11 必读 7 份全局文档 + 全部 `.proto` + `services/data-ana/` 骨架源码:
| # | 文档 | 状态 |
| --- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 1 | [README.md](../../../README.md) | ✅ 已读 |
| 2 | [MIGRATION_GUIDE.md](../../../MIGRATION_GUIDE.md) | ✅ 已读 |
| 3 | [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md) | ✅ 已读v2 重点核对 §1.2/§4.2/§7.2/§11.5/§12.2/§15.3 已仲裁项) |
| 4 | [pending-features.md](../../../docs/architecture/roadmap/pending-features.md) | ✅ 已读 |
| 5 | [project_rules.md](../../../.trae/rules/project_rules.md) | ✅ 已读workspace 规则) |
| 6 | [coding-standards.md](../../../docs/standards/coding-standards.md) | ✅ 已读(重点 §4 Python 规范) |
| 7 | [multi-ai-collaboration.md](../../../docs/standards/multi-ai-collaboration.md) | ✅ 已读 |
| 8 | `packages/shared-proto/proto/*.proto`8 份,重点 analytics.proto + events.proto | ✅ 已读 |
| 9 | `services/data-ana/src/` 全部源码main/config/cdc_consumer/clickhouse_client | ✅ 已读 |
> ai11 不需要读 classes 黄金模板ai-allocation.md §4 矩阵 ai11 列对黄金模板行为 "—"),但审计表横切关注点对齐仍参考 classes 标准。
---
## 1. 我在架构中的位置
- **层级**业务微服务层L5**D6 智能洞察领域**004 §1.1b
- **上游**
- api-gateway 直接 HTTP 代理 `/api/v1/analytics/*` → data-ana:3006见 [api-gateway main.go:142-148](../../api-gateway/main.go)
- teacher-bff / student-bff / parent-bff 聚合查询004 §4.1 + §4.2BFF → 业务服务 gRPC**P4 启用 gRPC**
- ai 服务通过 gRPC 反查学情数据004 §4.1AI → DataAna gRPCAI 出题编排时调 `GetStudentWeakness` / `GetLearningTrend`
- **下游**
- ClickHouse独占读模型宽表 `student_dashboard_view` / `student_errors` / `mastery_snapshot` / `ai_usage_log`
- RedisDataScope 缓存 5min + 预警去重位图 + CDC 幂等去重 SETNX004 §6.3 缓存策略矩阵)
- Kafka消费 Debezium CDC 事件 + 发布 `edu.insight.mastery.updated` 派生数据事件)
- iam gRPC`GetEffectiveDataScope` 解析数据范围,已仲裁 P4 补全,见 004 §15.3 #5
- **通信方式**
- 入口HTTP`/analytics/*`,保留作 Gateway 直连降级)+ **gRPC** `AnalyticsService`P4 启用主入口BFF 调用)
- 出口Kafka 消费CDC 主通道 + 领域事件订阅备通道Kafka 发布(`edu.insight.mastery.updated`**未实现**,属派生数据豁免 Outbox见 004 §12.2
- **端口**HTTP=3006见 [data-ana config.py:7](../src/data_ana/config.py) + [api-gateway config.go:55](../../api-gateway/internal/config/config.go)**gRPC=50055**004 §1.2 服务清单 + §4.2 gRPC 启用阶段矩阵P4 启用)
## 2. 我的限界上下文
- **聚合职责**:学情分析读模型构建 + 查询服务,承载 **D6 智能洞察领域**的"分析/诊断"子域
- **聚合根 / 实体**视图型聚合根ClickHouse 物化):
- `StudentDashboard`学生学情宽表视图ClickHouse 物化)
- `StudentErrorBook`学生错题本ClickHouse 物化)
- `ClassPerformance`班级成绩聚合ClickHouse 即时聚合,不物化)
- `MasterySnapshot`(知识点掌握度快照,**待实现**,含历史版本支持趋势查询)
- `AiUsageLog`AI 用量记录,消费 ai 服务 `edu.insight.ai.usage` 事件落库,供计费/分析用)
- `AttendanceLog`(出勤日志宽表,消费 core-edu CDC `core_edu_attendance`**待实现**,对应 ai-allocation §5 五张宽表之一)
- **业务领域**D6 智能洞察(与 ai 服务共享领域ai 偏"生成"data-ana 偏"分析"
- **我不负责**
- 不负责写模型(成绩由 core-edu 写 MySQLdata-ana 只消费 CDC
- 不负责题库内容(→ content 服务,但消费 content 知识点变更事件用于掌握度维度同步,见 §7
- 不负责通知投递(→ msg 服务消费 data-ana 发布的 `mastery.updated` 事件触发预警)
- 不负责 AI 推理(→ ai 服务ai 通过 gRPC 反向查询 data-ana 学情数据)
- 不负责双轨读的"实时查主库"侧BFF 层负责双轨读data-ana 仅是"聚合查宽表"侧,见 pending-features P4 交付物)
- **数据自治**:独占 `edu_analytics` ClickHouse 数据库(不与 MySQL 写模型混用);可选 Redis与全局共享实例键名加 `data_ana:` 前缀)
## 3. 我与外部的契约
### 消费的 proto message
> **双通道说明**:当前实现走 Debezium CDC直接监听 MySQL binlog不消费 events.proto 领域事件。这是 ADR-008 的设计决策。下表"领域事件"为**未来双消费备通道**(待 P4 后期评估是否启用CDC 才是当前主通道。
| 来源 | message | 通道状态 | 用途 |
| --------------- | --------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------- |
| events.proto | `GradeEvent` | 备通道(未消费) | 未来双消费core-edu 成绩写入事件 → 更新学情宽表 |
| events.proto | `ExamEvent` | 备通道(未消费) | 未来双消费:考试事件 → 缓存 exam_id→class_id 映射 |
| events.proto | `HomeworkEvent` | 备通道(未消费) | 未来双消费:作业提交/批改事件 → 更新学情 |
| events.proto | `ClassEvent` | 备通道(未消费) | 未来双消费:班级变更事件 → 同步班级维度 |
| analytics.proto | `GetClassPerformanceRequest` 等 | 自身暴露契约(待实现 gRPC server | P4 启用 gRPC=50055 后暴露 `AnalyticsService` |
| iam.proto | `GetEffectiveDataScopeRequest`**待 coord 在 iam.proto 新增**004 §15.3 #5 已裁决 P4 补全) | 调用契约 | gRPC 调 iam 解析 DataScope结果 Redis 缓存 5min |
### 暴露的 API / 事件
**HTTP 端点**(当前实现,见 [main.py](../src/data_ana/main.py);保留作 Gateway 直连降级):
| method | path | 说明 |
| ------ | ------------------------------------------- | --------------------------------------------- |
| GET | `/healthz` | liveness |
| GET | `/readyz` | readiness含 ClickHouse + CDC + Redis 状态) |
| GET | `/metrics` | Prometheus 指标 |
| GET | `/analytics/class/{class_id}/performance` | 班级成绩分析(平均分/及格率/参考人数) |
| GET | `/analytics/student/{student_id}/weakness` | 学生薄弱知识点mastery < 0.6 |
| GET | `/analytics/student/{student_id}/errorbook` | 学生错题本 |
> **响应信封约束**:以上 HTTP 端点当前实现为 `{success, data, degraded}` 结构,违反 004 §11.5 统一响应信封 ActionStatePython 服务必须改 ActionStatedegraded 作为 `details.degraded` 子字段)。阶段 2 设计已对齐(见 02-architecture-design.md §4.3)。
**gRPC 契约**analytics.protoP4 启用 server端口 50055
- `GetClassPerformance(GetClassPerformanceRequest) → ClassPerformance`
- `GetStudentWeakness(GetStudentWeaknessRequest) → StudentWeakness`
- `GetLearningTrend(GetLearningTrendRequest) → LearningTrend`**当前 HTTP 未暴露**
> **proto 扩展提案**(待 coord 在 shared-proto 新增,见 02-architecture-design.md §4.4
>
> - `GetTeacherDashboard` / `GetStudentDashboard` / `GetParentDashboard` / `GetSchoolDashboard`:四端 Dashboard 聚合ai-allocation §5 要求)
> - `GetClassWarningList` / `GetStudentWarningList`:预警查询
> - `GetMasteryDistribution`:掌握度分布
> - `stream SubscribeMasteryUpdate`实时掌握度推送P5+ 演进,供 BFF SSE 透传)
**发布的领域事件**
| 事件 | Topic004 §7.2 | 触发时机 | 消费者004 §7.3 | Outbox 合规性 |
| ---------------- | ----------------------------- | -------------- | --------------------------------------- | ----------------------------------------------------------- |
| `MasteryUpdated` | `edu.insight.mastery.updated` | 掌握度计算完成 | core-edu推荐个性化练习、msg预警 | **豁免 Outbox**(派生数据,见 004 §12.2 + §15.3 #6 已仲裁) |
> **当前未实现发布**data-ana 当前只消费不发布。掌握度计算完成后通过 `aiokafka.AIOKafkaProducer` 直接发布(已豁免 Outbox004 §15.3 #6 仲裁结论),下游 core-edu / msg 消费。失败重试 3 次仍失败落 `mastery_publish_failed` 本地表。
- **错误码前缀**`DATA_ANA_*`004 §11.4 错误码前缀矩阵已登记,清单见阶段 2 §6.2
- **缓存**RedisDataScope 缓存 5min 事件驱动失效 / CDC event_id 幂等去重 SETNX TTL 7d / 预警去重位图);学情宽表走 ClickHouse 实时CDC 同步延迟 < 5s
## 4. 我的技术栈
- 语言Python 3.12+
- 框架FastAPI 0.115+ / uvicorn
- ORM / 客户端:
- `clickhouse-connect`HTTP 协议,非原生协议)
- `redis-py[asyncio]`**待引入**DataScope 缓存 + 幂等去重 + 预警位图)
- `grpc.aio` + `betterproto`**待引入**P4 启用 gRPC server + 调 iam gRPC
- 消息:`aiokafka`CDC 消费者 AIOKafkaConsumer + 掌握度发布 AIOKafkaProducer
- 配置:`pydantic-settings` BaseSettingsenv_prefix="",全大写环境变量)
- 可观测:
- 日志:`structlog` 24.x`make_filtering_bound_logger`**注意**:旧版 `make_filtering_logger` 已废弃;生产改 JSONRenderer
- 指标:`prometheus-client` + `make_asgi_app()` 挂载 `/metrics`
- 链路:`opentelemetry-sdk` + `OTLPSpanExporter` + `FastAPIInstrumentor.instrument_app(app)` + `grpc.aio` server interceptor待引入
- 序列化JSONDebezium 事件 `schemas.enable=false`,直接 `json.loads`+ protobufgRPC
- 测试pytest + pytest-asyncio + TestcontainersClickHouse + Kafka + Redis 真实依赖,**当前 0% 覆盖率,目标 ≥ 80%**
- 容器Dockerfile 多阶段builder 用 `uv sync --frozen`runtime 用 `python:3.12-slim` 拷贝 `.venv`**当前单阶段待改造**
## 5. 我的阶段归属
- **P4 内容分析阶段M11-M13**:建 DataAna 服务 + CDC 链路落地
- **退出标准**pending-features P4学生查看学情诊断 ClickHouse 宽表 5s 内返回 + CDC 链路延迟 < 5s
- **交付物**pending-features P4DataAna 学情诊断宽表 5s 内返回 + CDC 链路延迟 < 5s + **双轨读策略落地(实时查主库 + 聚合查宽表)** + CDC 模式回写黄金模板 README + 打 tag `v0.4.0-p4`
- **依赖上游**
- P1 地基api-gateway 路由 + arch.db 扫描器 Python 支持
- P2 身份iam `GetEffectiveDataScope` gRPC已仲裁 P4 补全,见 004 §15.3 #5
- P3 核心教学core-edu 写成绩到 MySQLDebezium 监听 binlog+ Outbox 领域事件(备通道)
- P4 同期content 服务(提供知识点 ID 供掌握度计算content → data-ana 事件流见 004 §4 服务依赖图)
- **下游依赖我**
- P5 ai 服务通过 gRPC 查询学情数据004 §4.1AI → DataAna gRPC+ 投递 `edu.insight.ai.usage` 用量事件
- P5 msg 服务消费 `mastery.updated` 触发预警
- **未来阶段演进铺垫**(长远性):
- P5消费 `edu.insight.ai.usage` 落 ai_usage_loggRPC `SubscribeMasteryUpdate` stream RPC 供 BFF SSE 透传
- P6CDC 消费者水平扩展(多实例 + 分区重平衡 + ExamCache Redis 化ClickHouse 容量规划 + TTL数据治理GDPR 删除权Service Mesh mTLS
## 6. 我需要对齐的黄金模板项(对照 classes 服务 + Python 规范)
> Python 服务无 NestJS 装饰器体系,权限校验等通过等价方式实现。
- [ ] 权限装饰器等价物:**当前 HTTP 端点全部裸露,无权限校验**。Gateway 层做 JWT 校验,但 data-ana 本身未校验 `x-user-id` / DataScope。**阶段 2 需设计 FastAPI Depends 权限依赖 + DataScope 过滤注入**
- [ ] 错误码前缀统一:**当前无错误码体系**,降级时返回 `degraded: true` 标记但无业务错误码。**阶段 2 需定义 `DATA_ANA_*` 错误码清单**004 §11.4 已登记前缀)
- [ ] **响应信封对齐 ActionState**004 §11.5 已仲裁 P0 整改):当前 `{success, data, degraded}` 偏离结构,须改为 `{success: true, data: T}` / `{success: false, error: {code, message, details?, traceId?}}`degraded 作为 `details.degraded` 子字段
- [x] logger / metrics / tracer 三支柱(已具备,见 main.py + clickhouse_client.py生产改 JSONRenderer
- [x] `/healthz` + `/readyz` 健康检查已具备readyz 含 ClickHouse ping + CDC 状态;**待补 Redis + iam gRPC 连通性检查**
- [ ] 优雅关闭 SIGTERM当前 lifespan 仅关闭 CDC task + ClickHouse client**未注册 SIGTERM 信号处理器**显式 drain。阶段 2 设计顺序HTTP stop → gRPC graceful stop 30s → CDC commit offset → Kafka producer flush → ClickHouse close → iam channel close
- [ ] 测试覆盖率 ≥ 80%**当前 0%**,无 tests/ 目录。阶段 2 设计测试策略pytest 单元 + pytest-asyncio + TestcontainersClickHouse + Kafka + Redis
- [ ] Dockerfile 多阶段构建:**当前单阶段**`FROM python:3.12-slim``uv sync``COPY src`),非多阶段
- [ ] Pydantic 输入验证:**当前端点直接接收 path param无 Pydantic 请求模型校验**(应补 `ClassPerformanceResponse` 等 response_model
- [x] 配置通过 pydantic-settings 管理(已具备;**待补 Redis / gRPC / 限流 / 降级开关 / feature flags 配置项**,见阶段 2 §6.9
- [x] 异步优先async defaiokafka async consumer
- [ ] 类型注解强制:**部分函数缺返回值标注**(如 `init_logger` 返回 `BoundLogger``_logger` 全局变量标注 `None`,需统一)
- [ ] ruff check 零错误:需阶段 2 验证
- [ ] **CDC 幂等性加强**:当前依赖 ReplacingMergeTree查询未加 `FINAL` / `argMax`,可能读到重复版本(阶段 2 §3.1 修复)
- [ ] **CDC 水平扩展**:当前 ExamCache 为内存缓存,多实例部署不同步,阶段 2 §5.3 设计 Redis 化方案
- [ ] **数据治理**长远性GDPR 删除权在 ClickHouse 的实现mutations 或分区删除)、脱敏字段、审计日志(阶段 2 §10
---
## 服务审计表 — ai11data-anav2 修订)
> 对照 ai-allocation.md §10 审计模板。data-ana 为 Python/FastAPI黄金模板对齐按 Python 规范coding-standards §4评估。
> 符号说明:✅ 已实现 | ❌ 缺失 | ⚠️ 部分实现
| 服务 | 权限校验 | 错误码前缀 | 响应信封 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile | Redis | gRPC server |
| -------- | -------- | ---------- | ---------------------- | ------------ | ------------- | ------- | -------- | ----------------------------- | ------------------- | ---------- | ---------- | ----- | --------------------- |
| data-ana | ❌ | ❌ | ❌(偏离 ActionState | ✅ structlog | ✅ prometheus | ✅ OTel | ✅ | ⚠️(含 CH+CDC缺 Redis/iam | ⚠️ 仅 lifespan 关闭 | 0% | ❌ 单阶段 | ❌ | ❌P4 待启用 50055 |
### 审计发现的关键差距按优先级v2 修订)
1. **P0 权限校验缺失**:所有 `/analytics/*` 端点裸露,无 DataScope 过滤。学生 A 可查询学生 B 的错题本(越权风险)。阶段 2 必须设计 `Depends(require_permission)` + `Depends(inject_data_scope)` 依赖注入
2. **P0 响应信封偏离 ActionState**004 §11.5 已仲裁 P0 整改v2 新增):当前 `{success, data, degraded}` 必须改为 ActionStatedegraded 作为 `details.degraded` 子字段
3. **P0 gRPC server 未实现**004 §4.2 P4 启用analytics.proto 定义了 `AnalyticsService` 但 data-ana 当前仅 HTTP。阶段 2 需引入 `grpc.aio` + `betterproto` 实现 gRPC server端口 50055
4. **P1 事件发布缺失**`edu.insight.mastery.updated` 事件未发布(已豁免 Outbox004 §15.3 #6 仲裁),下游 core-edu/msg 无法消费。需设计掌握度计算 + Kafka producer 直接发布链路
5. **P1 ClickHouse schema 不规范**:当前 `student_dashboard_view` 实为 MergeTree 表,应为 ReplacingMergeTree(last_updated);查询未加 FINAL/argMax 可能读重复版本。无 DDL 文件管理。阶段 2 需产出完整 ClickHouse DDL5 张宽表:考试/作业/成绩/掌握度/出勤,对应 ai-allocation §5 要求)
6. **P1 掌握度计算算法缺失**:当前 `_handle_grades_event``score / 100.0` 简化,未实现 pending-features 要求的"加权滑动平均 + 遗忘曲线"。阶段 2 §9 给出具体公式
7. **P1 Dashboard API 不完整**ai-allocation §5 要求):当前仅 3 个查询端点,缺教师/学生/家长/校管理 4 端 Dashboard 聚合 API
8. **P1 预警阈值设计缺失**ai-allocation §5 要求):未设计预警阈值配置 API 与触发逻辑
9. **P2 测试覆盖率 0%**:无 tests/ 目录pytest 未配置
10. **P2 Dockerfile 单阶段**未做多阶段构建builder + runtime镜像体积大
11. **P2 无 Pydantic 响应模型**:端点返回 `dict` 而非 `BaseModel`,无 `response_model` 校验
12. **P2 CDC 水平扩展缺失**ExamCache 内存缓存多实例不同步;缺消费者 group_id / 分区重平衡设计
13. **P2 缓存策略缺失**:未引入 Redis缺 DataScope 缓存 / CDC 幂等去重 / 预警位图
14. **P2 content 服务交互缺失**004 §4 服务依赖图显示 content → data-ana 事件流,当前未消费 content 知识点变更事件
15. **P2 数据治理缺失**长远性GDPR 删除权、脱敏、审计日志未设计
16. **P2 ClickHouse 容量规划缺失**5 张宽表年增量估算 + TTL + 冷热分层未设计
---
## coord 交叉审查结论对齐v2 修订,原 v1 标题"待 coord 交叉审查的跨模块契约对齐项"
> v1 提请的 5 项跨模块契约对齐项coord 已在 [coord-cross-review.md](../../../docs/architecture/coord-cross-review.md) 完成仲裁,裁决结论中涉及架构设计意图的部分已沉淀到 004 对应章节004 §15.3 共性问题 #1-#7。本节 v2 改为"对齐状态"。
| # | 议题v1 提请) | coord 裁决结论004 §15.3 | data-ana 文档对齐状态v2 |
| --- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| 1 | **data-ana 是否发布 `edu.insight.mastery.updated` 事件** | §15.3 #6:派生数据事件豁免 Outbox允许直接 Kafka producer | ✅ v2 已对齐§3 标注"豁免 Outbox",阶段 2 §5.2 设计直接 producer 链路 |
| 2 | **data-ana / ai 是否需要实现 gRPC server** | §4.2 gRPC 启用阶段矩阵P4 content + data-ana 启用 | ✅ v2 已对齐§1 标注"gRPC=50055 P4 启用",阶段 2 §1 分层图含 grpc.aio Server |
| 3 | **CDC 直连 vs Outbox 领域事件双通道** | §15.3 #6 + ADR-008维持 CDC 为主通道events.proto 作为业务语义补充,待 P4 后期评估是否双消费 | ✅ v2 已对齐§3 双通道说明 + 标注"领域事件为备通道" |
| 4 | **data-ana DataScope 过滤实现位置** | §15.3 #5iam 新增 `GetEffectiveDataScope` gRPC RPCP4 补全data-ana 在 ClickHouse 查询 SQL 拼接时注入 WHERE | ✅ v2 已对齐§3 列出 iam.proto 调用契约;阶段 2 §6.1 设计 `inject_data_scope` Depends |
| 5 | **data-ana ClickHouse DDL 管理位置** | §15.3 #7:采纳 `infra/clickhouse/ddl/`coord 建立data-ana 提供 DDL 内容 | ✅ v2 已对齐:阶段 2 §3 标注"DDL 文件由 coord 统一管理在 `infra/clickhouse/ddl/`" |
| 6 | **新增 `edu.insight.ai.usage` topic**v1 阶段 2 §8.3 #3 提请) | §15.3 #4:补登,见 004 §7.2 | ✅ v2 已对齐:阶段 2 §5.1 列出消费此 topic |
| 7 | **iam GetEffectiveDataScope proto 新增**v1 阶段 2 §8.3 #2 提请) | §15.3 #5P4 补全 | ✅ v2 已对齐§3 列出 iam.proto 调用契约 |
> v1 §8.3 "未决设计决策"3 项全部已被 coord 仲裁v2 不再列为"未决"。文档后续修订如发现新冲突,按 ai-allocation.md §9.4 proto 变更流程提请 coord。
---
**AI Agent**: ai11 (data-ana) — v2 审核修订自 ai06 v1
**Branch**: 单仓库并行模式(直接提交 main
**Coordinator**: coord-ai
**v1 作者**: ai06原同时负责 data-ana + aiai-allocation §3.2 v2 重新分配后 data-ana → ai11 / ai → ai12

View File

@@ -0,0 +1,979 @@
# 模块架构设计文档 — data-ana
> AI 标识ai11v2 由 ai11 审核 ai06 v1 后修订)
> 负责模块data-anaP4
> 阶段:架构设计外包 · 阶段 2模块架构设计v2 审核版
> 日期2026-07-09v1 by ai06/ 2026-07-10v2 审核修订 by ai11
> 关联文档:[阶段 1 理解确认书](./01-understanding.md)、[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)
> 审查状态v1 已通过 coord 交叉审查(见 [coord-cross-review.md](../../../docs/architecture/coord-cross-review.md)),裁决结论已沉淀到 004 §15.3v2 在 v1 基础上对齐已仲裁约束 + 补强长远性/全面性
> v2 修订要点:(1) 身份归属 ai06 → ai11(2) 响应信封对齐 ActionState004 §11.5 P0 整改);(3) 删除已仲裁的"未决设计决策"(4) 补 gRPC 端口 50055(5) 补 5 张宽表ai-allocation §5 要求的 attendance_logs(6) 补 4 端 Dashboard + 预警 API(7) 补掌握度计算算法具体公式;(8) 补缓存策略/CDC 水平扩展/容量规划/数据治理/测试策略/Dockerfile 多阶段/配置项清单/API 版本化/Projection 演进路径/未来阶段铺垫§9-§19 新增章节)
---
## 设计原则与全局约束
本设计遵循以下强制约束(来自 project_rules.md + coding-standards.md + 004
1. **契约先行**proto 已定义analytics.proto / events.proto / iam.proto实现前不修改 proto如需修改走 coord 流程ai-allocation.md §9.4
2. **CQRS 读写分离**data-ana 是纯读模型服务(无 MySQL 写ClickHouse 宽表由 CDC 投影构建
3. **事件驱动**data-ana 消费 CDC主通道+ 领域事件(备通道,待 P4 后期评估);发布 `edu.insight.mastery.updated` 派生数据事件(豁免 Outbox004 §12.2 + §15.3 #6 已仲裁)
4. **gRPC 优先**004 §4.1 + §4.2 明确 BFF → 业务服务走 gRPC**P4 启用 data-ana gRPC server 端口 50055**HTTP 保留作 Gateway 直连降级)
5. **DataScope 过滤**004 §5.3 DataScope 6 级在查询层注入 WHEREiam `GetEffectiveDataScope` gRPC004 §15.3 #5 已仲裁 P4 补全)
6. **三支柱可观测**structlog + prometheus-client + OpenTelemetry已具备需补业务指标 + gRPC server interceptor
7. **统一响应信封 ActionState**004 §11.5 已仲裁 P0 整改):成功 `{success: true, data: T}` / 失败 `{success: false, error: {code, message, details?, traceId?}}` / 降级 `degraded` 作为 `details.degraded` 子字段
8. **降级模式**外部依赖ClickHouse / Kafka / iam gRPC / Redis不可用时返回骨架数据 + `details.degraded: true`
9. **Python 规范**pydantic-settings 配置 / Pydantic 模型校验 / async 优先 / 类型注解强制 / ruff 零错误
10. **长远架构演进**:为 P5ai 用量消费 / gRPC stream、P6CDC 水平扩展 / 容量规划 / 数据治理 / Service Mesh做好铺垫见 §14 / §19
---
## 1. 模块内部分层图
```mermaid
flowchart TB
subgraph Entry["入口层"]
HTTP[FastAPI HTTP Router<br/>:3006 /analytics/* + /healthz + /readyz]
GRPC[grpc.aio Server<br/>:50055 AnalyticsService]
end
subgraph Middleware["中间件层"]
AUTH[AuthDepends<br/>校验 x-user-id / x-user-roles]
SCOPE[DataScopeDepends<br/>注入 data_scope 元数据]
TRACE[OTel FastAPIInstrumentor<br/>+ grpc.aio server interceptor]
end
subgraph Service["应用服务层 Application Service"]
S1[AnalyticsService<br/>班级/学生/趋势/Dashboard 查询编排]
S2[MasteryService<br/>掌握度计算 + 事件发布]
S3[ErrorBookService<br/>错题本查询]
S4[WarningService<br/>预警阈值评估 + 触发]
end
subgraph Repo["数据访问层 Repository"]
R1[ClickHouseRepository<br/>宽表查询 + DataScope WHERE 注入 + FINAL/argMax]
R2[KafkaProducer<br/>mastery.updated 派生数据事件发布]
R3[IamClient<br/>gRPC 调 iam.GetEffectiveDataScope]
R4[RedisClient<br/>DataScope 缓存 + CDC 幂等 + 预警位图]
end
subgraph Consumer["CDC 消费者(后台任务)"]
C1[CdcConsumer<br/>aiokafka AIOKafkaConsumer]
C2[ExamCache<br/>exam_id→class_id 映射 P4 内存 / P6 Redis 化]
C3[EventHandler<br/>grades/exams/homework/classes/attendance/kp 路由]
end
subgraph Storage["存储 / 总线"]
CH[(ClickHouse<br/>edu_analytics 库 5 宽表)]
KAFKA[(Kafka<br/>edu-cdc.* + edu.insight.mastery.updated + edu.insight.ai.usage)]
IAM[iam:3002 gRPC :50052]
REDIS[(Redis<br/>data_ana:* 键前缀)]
end
HTTP --> AUTH --> SCOPE --> S1
HTTP --> S3
HTTP --> S4
GRPC --> S1
S1 --> R1
S3 --> R1
S4 --> R1
S4 --> R2
S2 --> R1
S2 --> R2
SCOPE --> R3
R3 --> R4
R1 --> CH
R2 --> KAFKA
R3 --> IAM
R4 --> REDIS
C1 --> C3
C3 --> C2
C3 --> R1
C3 --> R4
C1 --> KAFKA
```
**分层规则**
- **入口层**HTTP `:3006`(保留作 Gateway 直连降级)+ gRPC `:50055`P4 启用主入口BFF / ai 调用)。两入口共享同一 Application Service
- **中间件层**FastAPI Depends 链(`AuthDepends``DataScopeDepends`gRPC 用 server interceptor 注入身份元数据
- **应用服务层**:编排查询 / 计算掌握度 / 评估预警 / 发布事件,不直接访问存储
- **数据访问层**ClickHouse 查询封装(含 `FINAL` / `argMax` 去重)+ Kafka producer + gRPC client调 iam+ Redis client
- **CDC 消费者**独立后台任务lifespan 启动),与 HTTP/gRPC 入口解耦P6 演进为多实例水平扩展ExamCache Redis 化)
## 2. 领域模型
data-ana 是**纯读模型服务**,不持有写聚合根。领域模型为**视图聚合**ClickHouse 物化):
### 聚合根(视图型)
| 聚合根 | 含义 | 物化载体 | 不变式 |
| ------------------ | ---------------- | ------------------------------------------- | ------------------------------------------------------------- |
| `StudentDashboard` | 学生学情宽表 | ClickHouse `student_dashboard_view` | 同一 (student_id, exam_id, knowledge_point_id) 仅保留最新版本 |
| `ClassPerformance` | 班级成绩聚合 | ClickHouse 即时聚合(不物化) | 聚合维度为 class_id + 时间窗 |
| `StudentErrorBook` | 学生错题本 | ClickHouse `student_errors` | 同一 (student_id, question_id) 累计 error_count |
| `MasterySnapshot` | 知识点掌握度快照 | ClickHouse `mastery_snapshot`**新增** | 同一 (student_id, knowledge_point_id) 保留历史版本 |
| `AttendanceLog` | 学生考勤记录 | ClickHouse `attendance_logs`**v2 新增** | 同一 (student_id, class_id, attendance_date) 仅保留最新版本 |
| `AiUsageLog` | AI 用量计费记录 | ClickHouse `ai_usage_log`**v2 新增** | 同一 request_id 唯一(消费方按 request_id 幂等) |
> **ai-allocation §5 对齐**5 张宽表完整覆盖student_dashboard_view / student_errors / mastery_snapshot / attendance_logs / ai_usage_log。exam_results / homework_submissions / grade_records 数据已折叠进 student_dashboard_view 宽表(按 event_type 区分),避免宽表数量爆炸。
### 值对象
- `WeakPoint`knowledge_point_id + title + mastery_level
- `TrendPoint`date + score
- `Warning`warning_type (LOW_MASTERY / SCORE_DROP / ABSENT_FREQUENT) + target_id + threshold + current_value + severity (INFO / WARN / CRITICAL)
- `DataScope`level (SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL) + scope_ids具体可见的 class_id / grade_id 列表)
- `MasteryMethod`枚举WEIGHTED_MOVING_AVG / SIMPLE_AVG / FORGETTING_CURVE见 §9
### 聚合间通信
- 同服务内直接函数调用Application Service → Repository
- 跨服务:仅通过 Kafka 事件(发布 `mastery.updated`+ gRPC调 iam 查 DataScope**待 P5+** ai 反向调 data-ana 查学情)
## 3. 数据模型ClickHouse DDL
> DDL 文件由 coord 统一管理在 `infra/clickhouse/ddl/`(待 coord 建立data-ana 提供内容。
### 3.1 宽表 `student_dashboard_view`
```sql
-- 学生学情宽表:每次成绩写入产生一行,按 ORDER BY 去重保留最新版本
CREATE TABLE IF NOT EXISTS student_dashboard_view
(
student_id String,
class_id String,
exam_id String,
subject_id String,
score Float64,
rank_in_class UInt32,
knowledge_point_id String,
mastery_level Float32, -- 0.0-1.0
error_count UInt32,
last_updated DateTime64(3, 'UTC')
)
ENGINE = ReplacingMergeTree(last_updated)
PARTITION BY toYYYYMM(last_updated)
ORDER BY (student_id, exam_id, knowledge_point_id)
SETTINGS index_granularity = 8192;
```
**索引策略**
- ORDER BY `(student_id, exam_id, knowledge_point_id)`:主键索引,支持按学生查学情、按考试查成绩、按知识点查掌握度
- PARTITION BY `toYYYYMM(last_updated)`:按月分区,支持历史数据归档
- ReplacingMergeTree(last_updated):同 ORDER BY 自动去重,保留 last_updated 最大版本(幂等消费保证)
### 3.2 错题本 `student_errors`
```sql
CREATE TABLE IF NOT EXISTS student_errors
(
student_id String,
question_id String,
knowledge_point_id String,
error_count UInt32,
last_error_time DateTime64(3, 'UTC'),
content String
)
ENGINE = ReplacingMergeTree(last_error_time)
PARTITION BY toYYYYMM(last_error_time)
ORDER BY (student_id, question_id);
```
### 3.3 掌握度快照 `mastery_snapshot`**新增**
```sql
-- 知识点掌握度历史快照:每次掌握度计算产生新版本,支持趋势查询
CREATE TABLE IF NOT EXISTS mastery_snapshot
(
student_id String,
knowledge_point_id String,
subject_id String,
mastery_level Float32,
calculated_at DateTime64(3, 'UTC'),
calculation_method LowCardinality(String) -- 'weighted_moving_avg' / 'simple_avg'
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(calculated_at)
ORDER BY (student_id, knowledge_point_id, calculated_at);
```
### 3.4 用量计费 `ai_usage_log`**新增,供 ai 服务写入**
```sql
-- AI 用量记录ai 服务通过 Kafka 事件投递data-ana 消费落库
CREATE TABLE IF NOT EXISTS ai_usage_log
(
request_id String,
user_id String,
provider LowCardinality(String), -- 'openai' / 'anthropic' / 'baichuan' / 'local'
model LowCardinality(String),
prompt_tokens UInt32,
completion_tokens UInt32,
total_tokens UInt32,
latency_ms UInt32,
success Boolean,
cost_cents UInt32, -- 计费(分),便于聚合
occurred_at DateTime64(3, 'UTC')
)
ENGINE = ReplacingMergeTree(occurred_at)
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (request_id); -- 按 request_id 幂等去重
```
### 3.5 考勤记录 `attendance_logs`**v2 新增ai-allocation §5 第 5 张宽表**
```sql
-- 学生考勤记录core-edu attendance 表 CDC 同步
CREATE TABLE IF NOT EXISTS attendance_logs
(
student_id String,
class_id String,
attendance_date Date,
status LowCardinality(String), -- 'present' / 'absent' / 'late' / 'leave'
recorded_by String, -- 教师用户 ID
remark String DEFAULT '',
occurred_at DateTime64(3, 'UTC')
)
ENGINE = ReplacingMergeTree(occurred_at)
PARTITION BY toYYYYMM(attendance_date)
ORDER BY (student_id, class_id, attendance_date);
```
**索引策略**
- ORDER BY `(student_id, class_id, attendance_date)`:支持按学生查考勤历史、按班级查当日考勤、按日期范围统计
- PARTITION BY `toYYYYMM(attendance_date)`:按月分区,支持历史归档
- ReplacingMergeTree(occurred_at):同一记录多版本去重(教师修改考勤状态时产生新版本)
### 3.6 读写分离策略
| 操作 | 路径 | 说明 |
| -------------- | ----------------------------------- | ------------------------------------------ |
| 学情查询 | ClickHouse 宽表 | 实时聚合,亚秒级响应 |
| 错题本查询 | ClickHouse student_errors | 实时查询 |
| 掌握度趋势 | ClickHouse mastery_snapshot | 历史快照 |
| 考勤查询 | ClickHouse attendance_logs | 学生考勤历史 |
| AI 用量统计 | ClickHouse ai_usage_log | 按用户/模型/时间窗聚合 |
| 掌握度计算 | CDC 触发 → 内存计算 → 写 ClickHouse | 派生数据,非事务写 |
| DataScope 解析 | gRPC 调 iam | 实时查询,结果 Redis 缓存 5min004 §6.3 |
> **双轨读边界**004 §1.1aBFF 实时查主库走 core-edudata-ana 仅承担聚合查宽表职责,二者不重叠。
## 4. API 设计
### 4.1 HTTP 端点(保留作 Gateway 直连降级)
| method | path | 权限 | 请求 | 响应 | 说明 |
| ------ | -------------------------------------------- | ----------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------- | ----------------------------------- |
| GET | `/healthz` | — | — | `{status, service}` | liveness |
| GET | `/readyz` | — | — | `{status, ready, degraded, clickhouse, cdc_consumer, redis, iam_grpc}` | readinessv2 补 redis / iam_grpc |
| GET | `/metrics` | — | — | Prometheus 格式 | 指标 |
| GET | `/analytics/class/{class_id}/performance` | `ANALYTICS_CLASS_READ` | query: `subject_id?`, `start_date?`, `end_date?` | `ActionState<ClassPerformanceData>` | 班级成绩分析 |
| GET | `/analytics/student/{student_id}/weakness` | `ANALYTICS_STUDENT_READ` | query: `subject_id?` | `ActionState<StudentWeaknessData>` | 学生薄弱知识点DataScope 过滤) |
| GET | `/analytics/student/{student_id}/errorbook` | `ANALYTICS_STUDENT_READ` | query: `page?`, `size?` | `ActionState<StudentErrorBookData>` | 学生错题本DataScope 过滤) |
| GET | `/analytics/student/{student_id}/trend` | `ANALYTICS_STUDENT_READ` | query: `start_date`, `end_date`, `subject_id?` | `ActionState<LearningTrendData>` | 学习趋势 |
| GET | `/analytics/student/{student_id}/attendance` | `ANALYTICS_STUDENT_READ` | query: `start_date`, `end_date` | `ActionState<AttendanceData>` | 学生考勤历史(**v2 新增** |
| GET | `/analytics/dashboard/teacher/{user_id}` | `ANALYTICS_TEACHER_DASHBOARD` | query: `class_id?` | `ActionState<TeacherDashboardData>` | 教师仪表盘聚合 |
| GET | `/analytics/dashboard/student/{user_id}` | `ANALYTICS_STUDENT_DASHBOARD` | — | `ActionState<StudentDashboardData>` | 学生仪表盘(**v2 新增** |
| GET | `/analytics/dashboard/parent/{user_id}` | `ANALYTICS_PARENT_DASHBOARD` | query: `student_id` | `ActionState<ParentDashboardData>` | 家长仪表盘(**v2 新增** |
| GET | `/analytics/dashboard/admin/{user_id}` | `ANALYTICS_ADMIN_DASHBOARD` | query: `scope?`, `scope_id?` | `ActionState<AdminDashboardData>` | 管理员仪表盘(**v2 新增** |
| GET | `/analytics/warnings` | `ANALYTICS_WARNING_READ` | query: `class_id?`, `severity?`, `since?` | `ActionState<WarningListData>` | 预警列表(**v2 新增** |
| GET | `/analytics/mastery/distribution` | `ANALYTICS_CLASS_READ` | query: `class_id`, `subject_id`, `knowledge_point_id?` | `ActionState<MasteryDistributionData>` | 班级掌握度分布(**v2 新增** |
### 4.2 gRPC 契约analytics.proto**v2 扩展**
| RPC | 请求 | 响应 | 权限 |
| ------------------------ | -------------------------------------------------------------------------- | --------------------------- | ----------------------------- |
| `GetClassPerformance` | `GetClassPerformanceRequest{class_id, subject_id, start_date, end_date}` | `ClassPerformance` | `ANALYTICS_CLASS_READ` |
| `GetStudentWeakness` | `GetStudentWeaknessRequest{student_id, subject_id}` | `StudentWeakness` | `ANALYTICS_STUDENT_READ` |
| `GetLearningTrend` | `GetLearningTrendRequest{student_id, start_date, end_date, subject_id?}` | `LearningTrend` | `ANALYTICS_STUDENT_READ` |
| `GetTeacherDashboard` | `GetTeacherDashboardRequest{user_id, class_id?}` | `TeacherDashboard` | `ANALYTICS_TEACHER_DASHBOARD` |
| `GetStudentDashboard` | `GetStudentDashboardRequest{user_id}` | `StudentDashboard` | `ANALYTICS_STUDENT_DASHBOARD` |
| `GetParentDashboard` | `GetParentDashboardRequest{user_id, student_id}` | `ParentDashboard` | `ANALYTICS_PARENT_DASHBOARD` |
| `GetAdminDashboard` | `GetAdminDashboardRequest{user_id, scope, scope_id?}` | `AdminDashboard` | `ANALYTICS_ADMIN_DASHBOARD` |
| `GetWarnings` | `GetWarningsRequest{class_id?, severity?, since?}` | `WarningList` | `ANALYTICS_WARNING_READ` |
| `GetMasteryDistribution` | `GetMasteryDistributionRequest{class_id, subject_id, knowledge_point_id?}` | `MasteryDistribution` | `ANALYTICS_CLASS_READ` |
| `SubscribeMasteryUpdate` | `SubscribeMasteryUpdateRequest{student_id?, class_id?}` | `stream MasteryUpdateEvent` | `ANALYTICS_STUDENT_READ` |
> **proto 扩展提案**(待 coord 审议):上述新增 RPC 的 message 定义需在 `packages/shared-proto/proto/analytics.proto` 补充。`SubscribeMasteryUpdate` 为 server-streaming RPC为 P5+ AI 个性化推荐预留实时推送通道。
**权限校验**gRPC server interceptor 从 metadata 提取 `x-user-id` / `x-user-roles` / `x-data-scope`,调用 `AuthDepends` 等价逻辑。
### 4.3 ActionState 统一响应信封004 §11.5 P0 整改)
所有 HTTP/gRPC 响应必须遵循 ActionState 信封:
```python
from pydantic import BaseModel
from typing import Generic, TypeVar
T = TypeVar("T")
class ActionStateError(BaseModel):
code: str
message: str
details: dict[str, Any] | None = None
trace_id: str | None = None
class ActionState(BaseModel, Generic[T]):
"""统一响应信封004 §11.5.
成功:{success: true, data: T}
失败:{success: false, error: {code, message, details?, trace_id?}}
降级success=true 但 error.details.degraded=true保留功能但数据可能不完整
"""
success: bool
data: T | None = None
error: ActionStateError | None = None
@classmethod
def ok(cls, data: T, *, degraded: bool = False) -> "ActionState[T]":
details = {"degraded": True} if degraded else None
# 简化degraded 作为 data 的元信息附加,复杂场景用 details
return cls(success=True, data=data, error=None if not degraded else
ActionStateError(code="DATA_ANA_DEGRADED", message="degraded mode",
details=details))
@classmethod
def fail(cls, code: str, message: str, *, trace_id: str | None = None,
details: dict[str, Any] | None = None) -> "ActionState[T]":
return cls(success=False, data=None,
error=ActionStateError(code=code, message=message,
details=details, trace_id=trace_id))
```
```python
# 示例:班级成绩分析响应
class ClassPerformanceData(BaseModel):
class_id: str
average_score: float
pass_rate: float
total_students: int
scores: list[StudentScore] = [] # 详细成绩列表(受 DataScope 过滤)
class StudentScore(BaseModel):
student_id: str
score: float
grade: str
# 端点返回类型ActionState[ClassPerformanceData]
```
> **P0 整改要点**:当前 main.py 返回 `{success, data, degraded}` 三字段平铺,违反 004 §11.5。实现阶段需重构为 `ActionState[T]` 泛型,`degraded` 移到 `error.details.degraded`。
## 5. 事件设计
### 5.1 消费的事件
| Topic | 来源 | 消息格式 | 消费动作 |
| ------------------------------------------------------ | ----------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------- |
| `edu-cdc.next_edu_cloud.core_edu_grades` | Debezium CDCcore-edu MySQL | Debezium JSONbefore/after/source/op/ts_ms | 解析 → 查 ExamCache 填 class_id → 计算掌握度 → upsert student_dashboard_view |
| `edu-cdc.next_edu_cloud.core_edu_exams` | Debezium CDC | Debezium JSON | upsert ExamCacheexam_id → class_id, subject_id |
| `edu-cdc.next_edu_cloud.core_edu_homework_submissions` | Debezium CDC | Debezium JSON | 记录作业提交行为 → 更新 student_dashboard_view |
| `edu-cdc.next_edu_cloud.core_edu_attendance` | Debezium CDC**v2 新增** | Debezium JSON | 落 attendance_logs 表 |
| `edu-cdc.next_edu_cloud.classes` | Debezium CDC | Debezium JSON | 同步班级维度head_teacher_id用于教师 DataScope |
| `edu-cdc.next_edu_cloud.iam_users` | Debezium CDC | Debezium JSON | 同步用户 dataScope 用于查询过滤(避免每次查 iam |
| `edu-cdc.next_edu_cloud.content_knowledge_points` | Debezium CDC**v2 新增**content 服务) | Debezium JSON | 同步知识点标题/前置关系到 mastery_snapshot 元数据 |
| `edu.insight.ai.usage` | ai 服务 Kafka producer | JSONUsageRecord | 落 ai_usage_log 表 |
> **004 §4.1 对齐**content → data-ana 事件流已声明("教学内容变更通知"),通过 CDC 落知识点元数据。当 content 服务发布 `KnowledgePointUpdated` 领域事件时(待 P4 后期评估),可切换为订阅 `edu.content.events` topic避免依赖 content MySQL binlog。
**幂等性**
- CDC 事件:依赖 ClickHouse `ReplacingMergeTree(last_updated)` 引擎按 ORDER BY 去重 + ReplacingMergeTree 的 version 列
- 领域事件(若消费):基于 `event_id` 去重Redis SETNX `data_ana:dedup:{event_id}` TTL 7 天)
- 消费者 offsetv1 用 `enable_auto_commit=True` 简化;**v2 改进**:手动 commitat-least-oncecommit 前确保 ClickHouse 写入成功
### 5.2 发布的事件
| 事件 | Topic | 触发时机 | 消费者 | Payload |
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------------- |
| `MasteryUpdated` | `edu.insight.mastery.updated` | 掌握度计算完成CDC grades 事件触发后异步计算) | core-edu推荐个性化练习、msg掌握度预警 | `{event_id, student_id, knowledge_point_id, mastery_level, calculated_at}` |
| `WarningTriggered` | `edu.insight.warning.triggered`**v2 新增** | 预警阈值触发(掌握度低于 0.4 / 成绩环比下降 20% / 缺勤 ≥ 3 次/周) | msg推送通知、core-edu标记关注 | `{event_id, warning_type, target_id, threshold, current_value, severity, occurred_at}` |
**发布实现**004 §12.2 + §15.3 #6 已仲裁:派生数据事件豁免 Outbox
- 掌握度计算、预警评估均为**派生数据**(非业务事务写),不需要 Outbox 保证事务一致
- 直接用 `aiokafka.AIOKafkaProducer` 发布,`idempotent=true` + `transactional_id="data-ana-producer"`
- 失败重试 3 次,仍失败记录日志 + 落 `publish_failed` 本地表SQLite 文件P6 评估是否引入 Outbox
- 事件 schema 版本化:`schema_version` 字段v1/v2/...),禁止破坏性变更,见 §17
### 5.3 CDC 水平扩展策略v2 新增P6 铺垫)
**P4 单实例模式**
- 1 个 CdcConsumer 实例消费所有 topicconsumer group = `data-ana-cdc`
- ExamCache 内存 LRUmax 10000 条)
- 风险:单点故障 + 重启 lag 累积
**P6 多实例水平扩展**
- consumer group 不变,多实例分摊 partition
- ExamCache → Redis 化key: `data_ana:exam:{exam_id}` TTL 30 天)
- 幂等去重 → Redis SETNX已具备多实例天然共享
- partition 数从 P4 的 1 → P6 的 N按 topic 流量评估)
- 监控consumer lag 指标(已具备 `data_ana_cdc_consumer_lag`+ HPA 阈值
**演进路径**
```
P4: 1 实例 + 内存 ExamCache + auto commit
P5: 1 实例 + Redis ExamCache + 手动 commit过渡
P6: N 实例 + Redis ExamCache + 手动 commit + HPA
```
> 单实例 → 多实例的代码改动量:仅 ExamCache 实现dict → Redis client其余抽象层不变。
## 6. 横切关注点对齐清单
### 6.1 权限装饰器等价物FastAPI Depends
```python
# 权限点常量(与 iam 权限点对齐v2 扩展 4 端 Dashboard + 预警权限)
class Permissions:
ANALYTICS_CLASS_READ = "analytics:class:read"
ANALYTICS_STUDENT_READ = "analytics:student:read"
ANALYTICS_TEACHER_DASHBOARD = "analytics:teacher:dashboard"
ANALYTICS_STUDENT_DASHBOARD = "analytics:student:dashboard" # v2 新增
ANALYTICS_PARENT_DASHBOARD = "analytics:parent:dashboard" # v2 新增
ANALYTICS_ADMIN_DASHBOARD = "analytics:admin:dashboard" # v2 新增
ANALYTICS_WARNING_READ = "analytics:warning:read" # v2 新增
async def require_permission(permission: str) -> UserContext:
"""FastAPI Depends 权限校验.
从 x-user-id / x-user-roles 头提取身份,校验角色是否含 permission。
"""
...
async def inject_data_scope(ctx: UserContext = Depends(require_permission(...)))-> DataScope:
"""注入 DataScope从 iam.getEffectiveDataScope 查询Redis 缓存 5min."""
...
```
### 6.2 错误码清单(前缀 `DATA_ANA_*`v2 扩展)
| 错误码 | 触发条件 | HTTP | gRPC status |
| --------------------------------- | --------------------------------------- | ------------------- | ------------------ |
| `DATA_ANA_UNAUTHORIZED` | 缺失 x-user-id 头或 token 无效 | 401 | UNAUTHENTICATED |
| `DATA_ANA_FORBIDDEN` | 角色无对应权限 | 403 | PERMISSION_DENIED |
| `DATA_ANA_DATASCOPE_VIOLATION` | 查询目标超出 DataScope 范围 | 403 | PERMISSION_DENIED |
| `DATA_ANA_CLICKHOUSE_UNAVAILABLE` | ClickHouse 不可达(降级模式仍返回骨架) | 200 + degraded:true | OK + degraded flag |
| `DATA_ANA_KAFKA_UNAVAILABLE` | Kafka producer 不可达(事件发布失败) | 200 + degraded | OK + degraded |
| `DATA_ANA_REDIS_UNAVAILABLE` | Redis 不可达(降级直查 iam | 200 + degraded | OK + degraded |
| `DATA_ANA_IAM_GRPC_UNAVAILABLE` | iam gRPC 不可达fallback SELF 范围) | 200 + degraded | OK + degraded |
| `DATA_ANA_INVALID_DATE_RANGE` | start_date > end_date | 400 | INVALID_ARGUMENT |
| `DATA_ANA_STUDENT_NOT_FOUND` | student_id 不存在 | 404 | NOT_FOUND |
| `DATA_ANA_CLASS_NOT_FOUND` | class_id 不存在 | 404 | NOT_FOUND |
| `DATA_ANA_WARNING_NOT_FOUND` | 预警 ID 不存在 | 404 | NOT_FOUND |
| `DATA_ANA_RATE_LIMITED` | 触发限流004 §4.3 限流策略) | 429 | RESOURCE_EXHAUSTED |
| `DATA_ANA_INTERNAL_ERROR` | 未捕获异常 | 500 | INTERNAL |
### 6.3 Logger 初始化
- 位置:`main.py` `init_logger()`(已具备)
- 配置:`structlog.make_filtering_bound_logger(level)` + `TimeStamper(fmt="iso")` + `ConsoleRenderer`
- **改进**:生产环境改用 `structlog.processors.JSONRenderer()`(当前 ConsoleRenderer 适合开发)
- 日志字段:`request_id` / `user_id` / `trace_id` / `service="data-ana"`004 §10 强制)
### 6.4 Metrics 指标清单v2 扩展业务指标)
| 指标名 | 类型 | 标签 | 描述 |
| --------------------------------------------- | --------- | ---------------------- | ---------------------------- |
| `data_ana_http_requests_total` | Counter | method, path, status | HTTP 请求总数 |
| `data_ana_http_request_duration_seconds` | Histogram | method, path | HTTP 请求延迟 |
| `data_ana_grpc_requests_total` | Counter | rpc, status | gRPC 请求总数v2 新增) |
| `data_ana_grpc_request_duration_seconds` | Histogram | rpc | gRPC 请求延迟v2 新增) |
| `data_ana_clickhouse_query_duration_seconds` | Histogram | query_type | ClickHouse 查询延迟 |
| `data_ana_clickhouse_query_total` | Counter | query_type, status | ClickHouse 查询总数 |
| `data_ana_cdc_events_consumed_total` | Counter | table, op | CDC 事件消费总数 |
| `data_ana_cdc_event_process_duration_seconds` | Histogram | table | CDC 事件处理延迟 |
| `data_ana_cdc_consumer_lag` | Gauge | topic, partition | CDC 消费者 lag |
| `data_ana_mastery_calculated_total` | Counter | method | 掌握度计算次数(按算法) |
| `data_ana_mastery_published_total` | Counter | status | mastery.updated 事件发布数 |
| `data_ana_warning_triggered_total` | Counter | warning_type, severity | 预警触发数v2 新增) |
| `data_ana_datascope_cache_hits_total` | Counter | — | DataScope 缓存命中 |
| `data_ana_redis_ops_total` | Counter | op, status | Redis 操作数v2 新增) |
| `data_ana_iam_grpc_calls_total` | Counter | status | iam gRPC 调用数v2 新增) |
| `data_ana_ai_usage_ingested_total` | Counter | provider, model | AI 用量事件消费数v2 新增) |
### 6.5 Tracer 初始化
- 位置:`main.py` `init_tracer()`(已具备)
- endpoint`settings.otel_endpoint` + `/v1/traces`
- **改进**gRPC server 注册 `grpc.aio.ServerInterceptor` 透传 W3C trace context
- **改进**CDC 消费者 span 注入 trace context从 Kafka header 提取 traceparent
### 6.6 /healthz 检查逻辑
- liveness仅进程存活已具备
### 6.7 /readyz 检查逻辑v2 扩展)
```python
async def readyz() -> dict:
return {
"status": "ok" if all_ready else "degraded",
"ready": all_ready,
"degraded": not all_ready,
"clickhouse": "ok" | "unreachable" | "not_configured",
"cdc_consumer": "running" | "disabled" | "failed",
"kafka_brokers": settings.kafka_brokers or None,
"iam_grpc": "ok" | "unreachable", # v2 新增
"redis": "ok" | "unreachable", # v2 新增
"timestamp": datetime.now(UTC).isoformat(),
}
```
### 6.8 优雅关闭顺序
1. HTTP server stop accepting new requestsuvicorn graceful shutdown
2. gRPC server graceful stop等待在途 RPC 完成30s 超时)
3. CDC consumer stop等待在途消息处理完成commit offset
4. Kafka producer flush + close确保 mastery.updated 事件已投递)
5. ClickHouse client close
6. iam gRPC channel close
**信号处理**:注册 `signal.SIGTERM` handler触发上述顺序。
## 7. 与其他模块的交互点契约清单v2 扩展)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 |
| ------ | -------------------------------------- | ----- | --------------------------------------------------------------------------------- | ------------------------------------ |
| 被调用 | api-gateway | HTTP | `/analytics/*` | Gateway 代理(降级通道) |
| 被调用 | teacher-bff / student-bff / parent-bff | gRPC | `AnalyticsService.*` | BFF 聚合查询4 端 Dashboard |
| 被调用 | aiP5+ | gRPC | `AnalyticsService.GetStudentWeakness / GetLearningTrend / SubscribeMasteryUpdate` | AI 个性化出题上下文 + 实时掌握度推送 |
| 调用 | iam | gRPC | `IamService.GetEffectiveDataScope`004 §15.3 #5 已仲裁 P4 补全) | DataScope 解析 |
| 消费 | core-eduCDC | Kafka | `edu-cdc.next_edu_cloud.core_edu_grades/exams/homework_submissions/attendance` | 学情 + 考勤数据投递 |
| 消费 | core-eduCDC | Kafka | `edu-cdc.next_edu_cloud.classes` | 班级维度同步 |
| 消费 | iamCDC | Kafka | `edu-cdc.next_edu_cloud.iam_users` | 用户 dataScope 同步 |
| 消费 | contentCDC | Kafka | `edu-cdc.next_edu_cloud.content_knowledge_points` | 知识点元数据同步v2 新增) |
| 消费 | aiP5+ | Kafka | `edu.insight.ai.usage`004 §15.3 #6 已仲裁004 §7.2 已登记) | AI 用量落库 |
| 发布 | core-edu / msg | Kafka | `edu.insight.mastery.updated`004 §12.2 + §15.3 #6 已仲裁:派生数据豁免 Outbox | 掌握度更新通知 |
| 发布 | msg / core-edu | Kafka | `edu.insight.warning.triggered`v2 新增,需 coord 在 004 §7.2 登记) | 预警触发通知 |
> **004 §4.1 服务间通信矩阵对齐**content → data-ana 已声明"教学内容变更通知",本次落实为 CDC 订阅 `content_knowledge_points` 表。
## 8. 风险与假设
### 8.1 假设
- **假设 1**iam 在 P4 阶段提供 `GetEffectiveDataScope(userId) → DataScope` gRPC API004 §15.3 #5 已仲裁)。若 iam 未及时提供fallback 为:从 `x-user-roles` 头推导admin=ALL, teacher=CLASS_TAUGHT, student=SELF但无法支持细粒度年级/学校范围
- **假设 2**core-edu 的 `core_edu_homework_submissions` / `core_edu_attendance` 表存在 binlog。若不存在需 core-edu 补表或走 Outbox 事件(领域事件备通道)
- **假设 3**ClickHouse `ReplacingMergeTree` 在查询时需 `FINAL` 关键字确保去重生效。**v2 修复要求**:所有查询加 `FINAL` 或使用 `argMax` 聚合(当前实现未加,是 P0 整改项)
- **假设 4**coord 已在 004 §7.2 登记新增 `edu.insight.ai.usage` topic004 §15.3 #6 已仲裁)
### 8.2 技术风险
| 风险 | 影响 | 缓解措施 |
| --------------------------- | ------------------------------------ | ------------------------------------------------------------ |
| ClickHouse 查询延迟超 5s | 违反 P4 退出标准 | 宽表索引优化 + 物化视图预聚合 + 查询超时 3s 降级 |
| CDC 消费者 lag 过大 | 学情数据延迟 > 5s | 监控 lag + 告警 + 水平扩展消费者(分区数提升) |
| ExamCache 内存泄漏 | 长期运行 OOM | LRU 淘汰策略max 10000 条)+ 定期清理过期 examP6 迁 Redis |
| mastery.updated 事件丢失 | 下游 core-edu/msg 收不到通知 | Kafka producer `acks=all` + 本地失败表重试 |
| iam gRPC 不可达 | DataScope 无法解析 → 查询降级为 SELF | Redis 缓存 5min + fallback SELF 范围(最保守) |
| ClickHouse `FINAL` 查询性能 | 查询变慢 | 使用 `argMax` 替代 `FINAL`,或在写入时去重 |
| ClickHouse 单点故障 | 全部查询不可用 | P6 引入 ClickHouse 集群ReplicatedMergeTree+ 副本 |
| 多实例 CDC 重复消费 | ClickHouse 重复写入 | ReplacingMergeTree ORDER BY 去重 + Redis SETNX 幂等 |
| 大数据量 Dashboard 聚合超时 | 管理员 Dashboard 全校聚合慢 | 物化视图预聚合 + 异步刷新 + 缓存 5min |
| 知识点元数据与成绩关联失败 | mastery_snapshot 缺知识点标题 | content CDC 同步 + 缺失时显示 `knowledge_point_id` |
### 8.3 coord 交叉审查结论对齐v2原"未决设计决策"已全部仲裁)
| # | 议题 | coord 仲裁结论 | 涉及文档 |
| --- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | ----------------------- |
| 1 | data-ana 发布 `edu.insight.mastery.updated` 用直接 producer非 Outbox是否合规 | ✅ 已仲裁:派生数据事件豁免 Outbox004 §12.2 + §15.3 #6 | 004 §12.2 |
| 2 | 新增 `edu.insight.ai.usage` topic + `AIUsageEvent` proto message | ✅ 已仲裁coord 在 004 §7.2 登记 + events.proto 补 message | 004 §7.2 + events.proto |
| 3 | iam 新增 `GetEffectiveDataScope` gRPC RPC | ✅ 已仲裁P4 补全004 §15.3 #5 | iam.proto |
| 4 | data-ana 实现 gRPC server 决策 | ✅ 已仲裁P4 启用004 §4.2 gRPC 启用阶段矩阵) | 004 §4.2 |
| 5 | ClickHouse DDL 管理位置(建议 `infra/clickhouse/ddl/` | ⏳ 待 coord 建立 `infra/clickhouse/ddl/` 目录 + data-ana 提供内容 | infra/ |
| 6 | 端口冲突检查data-ana HTTP=3006 / gRPC=50055 | ✅ 无冲突004 §1.2 端口分配) | 004 §1.2 |
| 7 | 错误码前缀检查:`DATA_ANA_*` | ✅ 无冲突004 §11.4 错误码前缀矩阵) | 004 §11.4 |
| 8 | 黄金模板对齐Python 服务无 NestJS 装饰器,权限校验用 FastAPI Depends 等价物是否认可 | ✅ 已仲裁认可004 §15.3 | — |
| 9 | `edu.insight.warning.triggered` topic 新增 | ⏳ 待 coord 在 004 §7.2 登记v2 新提案) | 004 §7.2 |
| 10 | analytics.proto 扩展4 端 Dashboard / Warning / MasteryDistribution / Stream RPC | ⏳ 待 coord 审议v2 新提案§4.2 | analytics.proto |
> 所有"未决设计决策"已消除,进入实现阶段无阻塞。剩余 3 项 ⏳ 为 v2 新提案,待 coord 审议但不阻塞 P4 主体实现(可先用现有 RPC + HTTP 端点兜底)。
---
## 9. 掌握度计算算法ai-allocation §5 + pending-features P4 要求)
### 9.1 算法选型
| 算法 | 公式 | 适用场景 | 默认 |
| --------------------- | ------------------------------------------------------------ | ---------------------------- | ---------------- |
| `WEIGHTED_MOVING_AVG` | $M = \sum_{i=1}^{n} w_i \cdot s_i / \sum w_i$$w_i = 0.6^i$ | 学生有多次同知识点考试成绩 | ✅ |
| `FORGETTING_CURVE` | $M(t) = M_0 \cdot e^{-t/S}$$S$ = 30 天 | 长期未考的知识点,掌握度衰减 | ✅(与上者叠加) |
| `SIMPLE_AVG` | $M = \frac{1}{n}\sum s_i$ | 数据稀疏(< 3 次) | 兜底 |
**综合公式**(默认策略 `WEIGHTED_MOVING_AVG + FORGETTING_CURVE`
```
M_final = max(
WEIGHTED_MOVING_AVG(近 5 次成绩, 时间倒序加权),
FORGETTING_CURVE(最近一次掌握度, 距今天数)
)
```
- 取 max 是为了避免"刚考完高分但长期没考"被遗忘曲线过度拉低
- 近 5 次不足时,用 SIMPLE_AVG 兜底
- 所有成绩归一化到 [0, 1]score / 100
### 9.2 算法参数(可配置)
| 参数 | 默认值 | 说明 |
| ---------------------- | ------ | ---------------------------- |
| `window_size` | 5 | 加权滑动窗口大小 |
| `decay_base` | 0.6 | 加权衰减基数(越近权重越高) |
| `forgetting_half_life` | 30 | 遗忘曲线半衰期(天) |
| `min_samples_for_wma` | 3 | 低于此数降级为 SIMPLE_AVG |
| `score_max` | 100 | 成绩满分(用于归一化) |
### 9.3 触发时机
1. CDC `core_edu_grades` 事件到达 → 异步计算 → 写 `mastery_snapshot` + `student_dashboard_view` → 发布 `MasteryUpdated`
2. 定时任务(每日 02:00→ 对所有知识点重算遗忘曲线衰减 → 更新 `mastery_snapshot`(不发布事件,避免风暴)
3. 手动触发(管理员 Dashboard "重算掌握度"按钮v2 预留 API `POST /analytics/mastery/recalculate`
### 9.4 预警阈值设计ai-allocation §5 要求)
| 预警类型 | 触发条件 | severity | 通知动作 |
| ----------------- | ---------------------- | -------- | ---------------------- |
| `LOW_MASTERY` | mastery_level < 0.4 | WARN | 推送学生 + 教师 |
| `CRITICAL_LOW` | mastery_level < 0.2 | CRITICAL | 推送学生 + 教师 + 家长 |
| `SCORE_DROP` | 本次成绩环比下降 ≥ 20% | WARN | 推送教师 |
| `ABSENT_FREQUENT` | 单周缺勤 ≥ 3 次 | WARN | 推送教师 + 家长 |
| `TREND_DECLINE` | 连续 3 次成绩下降 | INFO | 教师仪表盘标记 |
**预警去重**:同一 (target_id, warning_type) 24h 内只触发一次Redis 位图 `data_ana:warning:{target_id}:{type}:{date}`
## 10. 缓存策略004 §6.3 缓存策略矩阵对齐)
### 10.1 Redis 连接
```python
# config.py 新增
redis_url: str = "redis://edu-redis:6379/3" # data-ana 专用 db=3
redis_pool_size: int = 10
redis_socket_timeout_ms: int = 200
```
### 10.2 缓存键命名规范
| 键模式 | TTL | 用途 | 失效策略 |
| -------------------------------------------- | ----- | -------------------------- | ---------------------- |
| `data_ana:datascope:{user_id}` | 5min | DataScope 缓存 | 角色变更主动 DEL |
| `data_ana:exam:{exam_id}` | 30day | ExamCacheP6 多实例共享) | exam 结束 30 天后过期 |
| `data_ana:dedup:{event_id}` | 7day | 事件幂等去重 | 自然过期 |
| `data_ana:warning:{target_id}:{type}:{date}` | 25h | 预警去重位图 | 自然过期 |
| `data_ana:dashboard:{user_id}:{scope}` | 5min | Dashboard 聚合结果缓存 | 主动 DEL on 新成绩写入 |
| `data_ana:kp_meta:{knowledge_point_id}` | 1day | 知识点元数据缓存 | content CDC 更新时 DEL |
### 10.3 缓存降级
- Redis 不可达所有缓存查询跳过cache miss直查 ClickHouse / iam gRPC
- DataScope 缓存 miss → 调 iam gRPC增加 iam 负载,需监控)
- Dashboard 缓存 miss → 直查 ClickHouse增加 CH 负载,可能触发查询超时降级)
## 11. CDC 水平扩展P6 铺垫,详见 §5.3
P4 单实例 → P6 多实例演进路径已在 §5.3 详述。本节补充容量规划。
### 11.1 ClickHouse 容量规划
**假设**:全校 5000 学生,每学生每月 20 次成绩写入5 个知识点/次。
| 宽表 | 月增量(行) | 单行大小 | 月增量MB | 年增量GB | TTL |
| ------------------------ | ------------ | -------- | ------------ | ------------ | ---- |
| `student_dashboard_view` | 500K | 200B | 100 | 1.2 | 3 年 |
| `student_errors` | 100K | 150B | 15 | 0.18 | 2 年 |
| `mastery_snapshot` | 250K | 100B | 25 | 0.3 | 5 年 |
| `attendance_logs` | 500K | 80B | 40 | 0.48 | 3 年 |
| `ai_usage_log` | 50K | 120B | 6 | 0.07 | 1 年 |
**总计**:年增量 ~2.3 GB单节点 ClickHouse 可承载。P6 引入集群时按 class_id hash 分片。
### 11.2 冷热分层
- 热数据:近 6 个月SSD 存储
- 冷数据6 个月 - 3 年HDD 存储ClickHouse `TTL ... TO VOLUME 'cold'`
- 归档数据:> 3 年,导出 Parquet 到对象存储P6 评估)
### 11.3 Kafka topic partition 规划
| Topic | P4 partition | P6 partition | 依据 |
| ---------------------------------------- | ------------ | ------------ | -------------------------- |
| `edu-cdc.next_edu_cloud.core_edu_grades` | 1 | 4 | 高频写入,按 class_id 分区 |
| `edu-cdc.next_edu_cloud.core_edu_exams` | 1 | 2 | 低频 |
| `edu.insight.mastery.updated` | 1 | 4 | 下游多消费者 |
## 12. 数据治理与隐私v2 新增,长期合规铺垫)
### 12.1 GDPR / 个人信息保护法对齐
| 场景 | 实现 | 阶段 |
| ---------- | ---------------------------------------------------------------------------------------------- | ---- |
| 学生删除权 | ClickHouse 无 UPDATE/DELETE`ALTER TABLE ... DELETE WHERE student_id = ?`(异步 mutation | P6 |
| 数据导出权 | `GET /analytics/student/{id}/export` → JSON 全量导出v2 预留 API | P6 |
| 字段脱敏 | student_id 在日志中 hash 化structlog processor | P4 |
| 审计日志 | 所有查询记录 `user_id + query + timestamp` 到 ClickHouse `audit_log`v2 新增表P6 实现) | P6 |
| 数据保留期 | 见 §11.1 TTL 列 | P4+ |
### 12.2 敏感数据标记
- `student_id`PII日志中 hash 化
- `score`:非敏感,可明文
- `attendance_status`:非敏感
- `ai_usage`:含 user_id + prompt_tokensprompt 内容不入库ai 服务侧处理)
## 13. 测试策略v2 新增)
### 13.1 测试金字塔
| 层级 | 工具 | 覆盖范围 | 目标覆盖率 |
| -------- | ----------------------- | -------------------------------------------- | ---------- |
| 单元测试 | pytest + pytest-asyncio | 算法(掌握度计算/预警阈值、Pydantic 模型 | ≥ 80% |
| 集成测试 | pytest + Testcontainers | ClickHouse DDL + 查询、Kafka producer、Redis | ≥ 60% |
| 契约测试 | pact-pythonv2 评估) | gRPC 契约analytics.proto | 关键 RPC |
| E2E 测试 | pytest + docker-compose | CDC 链路端到端MySQL → Debezium → CH | 关键场景 |
### 13.2 Testcontainers 配置
```python
@pytest.fixture(scope="session")
async def clickhouse_container():
container = ClickHouseContainer("clickhouse/clickhouse-server:24-alpine")
container.start()
yield container
container.stop()
@pytest.fixture(scope="session")
async def kafka_container():
container = KafkaContainer("confluentinc/cp-kafka:7.6.0")
container.start()
yield container
container.stop()
```
### 13.3 测试数据
- 使用 Faker 生成 100 学生 × 10 班级 × 5 科目 × 20 次成绩的合成数据
- 边界用例空数据、单条数据、超大数据10000 行)、重复事件(幂等测试)
## 14. Dockerfile 多阶段构建v2 新增)
```dockerfile
# services/data-ana/Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src/ ./src/
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
EXPOSE 3006 50055
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3006/healthz || exit 1
CMD ["python", "-m", "uvicorn", "src.data_ana.main:app", \
"--host", "0.0.0.0", "--port", "3006"]
```
**优化点**
- 多阶段构建runtime 镜像 < 200MB
- uv 替代 pip安装速度 10x
- uv.lock 锁定依赖版本
- 健康检查集成
## 15. 完整配置项清单v2 新增,补全 config.py
```python
# config.py 完整配置项
class Settings(BaseSettings):
# 服务
service_name: str = "data-ana"
http_port: int = 3006
grpc_port: int = 50055
log_level: str = "INFO"
# ClickHouse
clickhouse_host: str = "edu-clickhouse"
clickhouse_port: int = 8123
clickhouse_user: str = "default"
clickhouse_password: str = ""
clickhouse_database: str = "edu_analytics"
clickhouse_connect_timeout_ms: int = 3000
clickhouse_query_timeout_s: int = 3 # P4 退出标准 5s查询 3s 超时降级
# Kafka
kafka_brokers: str = "kafka:29092"
kafka_consumer_group: str = "data-ana-cdc"
kafka_cdc_topics: str = (
"edu-cdc.next_edu_cloud.core_edu_grades,"
"edu-cdc.next_edu_cloud.core_edu_exams,"
"edu-cdc.next_edu_cloud.core_edu_homework_submissions,"
"edu-cdc.next_edu_cloud.core_edu_attendance,"
"edu-cdc.next_edu_cloud.classes,"
"edu-cdc.next_edu_cloud.iam_users,"
"edu-cdc.next_edu_cloud.content_knowledge_points"
)
kafka_mastery_topic: str = "edu.insight.mastery.updated"
kafka_warning_topic: str = "edu.insight.warning.triggered"
kafka_ai_usage_topic: str = "edu.insight.ai.usage"
kafka_enable_auto_commit: bool = False # v2: 手动 commit
kafka_auto_offset_reset: str = "latest"
# iam gRPC
iam_grpc_endpoint: str = "iam:50052"
iam_grpc_timeout_s: int = 2
datascope_cache_ttl_s: int = 300
# Redis
redis_url: str = "redis://edu-redis:6379/3"
redis_pool_size: int = 10
redis_socket_timeout_ms: int = 200
# OTel
otel_endpoint: str = "http://otel-collector:4317"
otel_service_name: str = "data-ana"
# 掌握度算法
mastery_window_size: int = 5
mastery_decay_base: float = 0.6
mastery_forgetting_half_life_days: int = 30
mastery_min_samples: int = 3
# 预警阈值
warning_low_mastery_threshold: float = 0.4
warning_critical_mastery_threshold: float = 0.2
warning_score_drop_percent: float = 0.2
warning_absent_per_week: int = 3
# 降级
degraded_mode_enabled: bool = True
model_config = SettingsConfigDict(env_file=".env", env_prefix="DATA_ANA_")
```
## 16. API 版本化策略v2 新增)
### 16.1 HTTP 版本化
- URL 前缀:`/analytics/v1/...`(当前省略 v1v2 起强制)
- 破坏性变更:新增 `/analytics/v2/...`,旧 v1 保留 6 个月
- 非破坏性变更:直接在 v1 上加字段Pydantic 模型字段可选)
### 16.2 gRPC 版本化
- proto package`next_edu_cloud.analytics.v1`(当前)
- 破坏性变更:新增 `next_edu_cloud.analytics.v2`v1 保留
- buf breaking 检查CI 强制FILE 级别)
### 16.3 事件版本化
- 事件 schema 字段:`schema_version: "v1"`(必填)
- 破坏性变更:发布 `v2` 事件consumer 同时支持 v1 + v26 个月过渡)
- 非破坏性变更直接加字段schema_version 不变
## 17. Projection 演进路径v2 新增)
### 17.1 当前P4CDC 直连
```
MySQL binlog → Debezium → Kafka (edu-cdc.*) → data-ana CdcConsumer → ClickHouse
```
- 优点:实时性好(< 5s无需 core-edu 改造
- 缺点:依赖 binlog schemacore-edu 表结构变更可能破坏 CDC
### 17.2 未来P4 后期评估):双消费
```
MySQL binlog → Debezium → Kafka (edu-cdc.*) → data-ana CdcConsumer → ClickHouse宽表
core-edu Outbox → Kafka (edu.domain.events) → data-ana EventConsumer → ClickHouse业务事件视图
```
- CDC 通道:保持宽表实时性
- 领域事件通道业务语义清晰schema 稳定proto 定义)
- 两通道并存,分别写不同 ClickHouse 表,避免重复
### 17.3 远期P6CDC + CQRS 物化视图
```
CDC → ClickHouse 原始表 → MaterializedView → 宽表(自动聚合)
```
- 减少应用层计算,由 ClickHouse MV 自动维护宽表
- 掌握度计算仍需应用层(复杂算法 CH 难以表达)
## 18. 未来阶段铺垫v2 新增,长远架构)
### 18.1 P5 铺垫AI 网关 + 沟通通知)
| 演进点 | P4 准备 | P5 实现 |
| ---------------- | ----------------------------------------------------------- | --------------------------------------------- |
| AI 用量消费 | `ai_usage_log` 表已建 + `edu.insight.ai.usage` topic 已订阅 | ai 服务发布事件data-ana 落库 + 用量统计 API |
| gRPC stream 推送 | `SubscribeMasteryUpdate` RPC 已在 proto 提案§4.2 | 实现 server-streamingAI 实时获取掌握度变化 |
| 预警触发通知 | `WarningTriggered` 事件已设计§5.2 | msg 服务消费事件 → 推送 push-gateway |
### 18.2 P6 铺垫(生产硬化)
| 演进点 | P4 准备 | P6 实现 |
| ----------------- | --------------------------------------------- | ------------------------------------ |
| CDC 水平扩展 | ExamCache 抽象层 + Redis 配置已就绪§5.3 | 多实例 + HPA + partition 扩容 |
| ClickHouse 集群 | DDL 用 ReplacingMergeTree兼容 Replicated* | ReplicatedMergeTree + 副本 + 分片 |
| Service Mesh mTLS | gRPC server 已抽象 interceptor | Istio 注入 sidecarmTLS 自动双向 |
| 数据治理 | student_id 日志 hash + TTL 设计§12 | ALTER TABLE DELETE + 审计日志表 |
| 配置中心 | Settings 用 pydantic-settings支持热更新 | Consul 接入watch 配置变更 |
| 灾备 | Kafka producer idempotent + transactional | Kafka 多副本 + ClickHouse 跨机房副本 |
### 18.3 远期P7+)预留
- **实时流式分析**Flink 替代 CdcConsumer支持窗口聚合如"近 7 天平均掌握度"实时计算)
- **数据湖**ClickHouse 冷数据 → S3/MinIO + Iceberg 表格式,支持 ad-hoc 查询
- **AI 驱动的学情诊断**data-ana 提供数据ai 服务提供模型,组合成"智能诊断报告"
- **多租户**:当前 school_id 隐含在 class_id 中,远期可显式多租户隔离
## 19. 服务审计表 v2黄金模板对齐
| 维度 | 状态 | 说明 |
| ------------- | ---- | ----------------------------------------------------------------------- |
| 契约proto | ✅ | analytics.proto 已定义 3 RPCv2 提案扩展 7 RPC |
| 路由 | ✅ | HTTP 13 端点 + gRPC 10 RPC |
| 数据访问 | ✅ | ClickHouse 5 宽表 + Redis 缓存 + iam gRPC |
| 鉴权 | ✅ | FastAPI Depends 等价物 + 7 权限点 |
| 错误处理 | ✅ | 13 错误码 `DATA_ANA_*` + ActionState 信封 |
| 可观测 - 日志 | ✅ | structlog + JSONRendererv2 改进) |
| 可观测 - 指标 | ✅ | 16 Prometheus 指标 |
| 可观测 - 链路 | ✅ | OTel + gRPC interceptor + CDC span 注入 |
| 健康检查 | ✅ | /healthz + /readyzv2 补 redis/iam_grpc |
| 配置管理 | ✅ | pydantic-settings 完整配置项§15 |
| 降级模式 | ✅ | 4 降级场景CH/Kafka/Redis/iam |
| 响应信封 | ⚠️ | v2 设计对齐 ActionState实现阶段需重构P0 整改) |
| Redis | ⚠️ | v2 设计就绪,实现阶段需新增 redis-py 依赖 + RedisClient |
| gRPC server | ⚠️ | v2 设计就绪,实现阶段需新增 grpc.aio + betterproto + server interceptor |
| Dockerfile | ⚠️ | v2 设计多阶段,实现阶段需新增 |
| 测试 | ⚠️ | v2 策略就绪,实现阶段需补 pytest + Testcontainers |
| CDC 幂等性 | ⚠️ | v2 改进手动 commit + Redis SETNX实现阶段需重构 |
| CDC 水平扩展 | ✅ | v2 设计 P6 演进路径P4 单实例可工作 |
| 数据治理 | ✅ | v2 设计 GDPR 对齐方案P6 实现 |
| 容量规划 | ✅ | v2 估算年增量 2.3GB,单节点可承载 |
---
**AI Agent**: ai11 (data-ana)
**Branch**: 单仓库并行模式(直接提交 main
**Coordinator**: coord-ai
**v2 修订依据**: ai-allocation.md §3.2 重新分配 + 004 §1.2/§4.2/§7.2/§11.5/§12.2/§15.3 + ai-allocation §5 设计重点 + pending-features P4 退出标准
**v2 审核结论**: 16 项遗漏已全部补强3 项 P0 + 5 项 P1 + 8 项 P2文档进入实现阶段无阻塞

View File

@@ -0,0 +1,191 @@
# 模块理解确认书 — msg
> AI 标识ai05初版→ ai10复核修订按 ai-allocation.md §3.2 重新分配)
> 负责模块msgP5
> 阶段:架构设计外包 · 阶段 1全局理解
> 初版日期2026-07-09
> 复核修订日期2026-07-09ai10 接手后修正 3 处失实条目 M2/M8/M12并补充 ai-allocation §5 缺失项)
> 关联文档:[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[known-issues.md](../../../docs/troubleshooting/known-issues.md)
---
## 1. 我在架构中的位置
- **层级**业务微服务层L5DDD 限界上下文
- **业务领域**D5 沟通通知领域004 §1.1b
- **上游**(谁调用我):
- teacher-bff / student-bff / parent-bff聚合层调 msg 查询用户通知列表、发送通知
- api-gatewayREST 转发通知请求
- Kafka消费 core-edu / iam 事件触发通知004 §4.1 `CoreEdu -.事件.-> Msg``IAM -.事件.-> Msg`
- **下游**(我调用谁):
- MySQL写模型主库独占
- Elasticsearch全文检索已实现 safeSearch
- push-gatewaygRPC 推送通道004 §4.1 `PushGW → Msg`,当前用 fetch POST /internal/push 降级)
- **通信方式**
- 当前HTTP RESTController无 gRPC controller
- 目标态004 §4.1 / pending-features P5gRPC 暴露 `NotificationService`
- Kafka消费 core-eduExamPublished/HomeworkGraded/GradeRecorded、iamUserRegistered事件004 §7.3
- **端口**3007见 [msg env.ts](../src/config/env.ts)
## 2. 我的限界上下文
- **聚合职责**:管理 Notification通知聚合 + NotificationPreference用户通知偏好
- **我的数据属于**D5 沟通通知领域
- **我不负责**
- 不负责 WebSocket 长连接管理(由 push-gateway 承载)
- 不负责业务数据变更(仅消费事件触发通知)
- 不直接访问 core-edu / iam 的数据库
- **现有骨架领域模块**1 个,见 [msg/src](../src)
- `notifications/`:通知 CRUD + ES 全文检索 + Push Gateway 推送 + 用户偏好6 端点)
## 3. 我与外部的契约
- **消费的 proto message**(从 shared-proto
- 消费 `events.proto` 的事件 messageClassEvent/ExamEvent/HomeworkEvent/GradeEvent
- **events.proto 当前无 NotificationEvent**,若 msg 发事件需补充
- **暴露的契约**(见 [msg.proto](../../../packages/shared-proto/proto/msg.proto),包名 `next_edu_cloud.msg.v1`
- `NotificationService`SendNotification / ListNotifications / MarkAsRead / SearchNotifications
- **当前实现均为 RESTgRPC controller 未实现**
- **事件契约**
- 消费004 §7.2 / §7.3
- `edu.identity.user.created` / `edu.identity.user.updated`IAM 发msg 发欢迎通知)
- `edu.teaching.exam.published`core-edu 发msg 推送考试通知给学生)
- `edu.teaching.assignment.submitted`core-edu 发msg 通知教师)
- `edu.teaching.grade.recorded`core-edu 发msg 通知学生)
- `edu.insight.mastery.updated`data-ana 发msg 触发预警)
- 发布无明确pending-features 未要求 msg 发事件)
- **错误码前缀**`MSG_*`MSG_VALIDATION_ERROR / NOT_FOUND / PERMISSION_DENIED / CONFLICT / BUSINESS_ERROR / DATABASE_ERROR / INTERNAL_ERROR见 [application-error.ts](../src/shared/errors/application-error.ts)
- **权限点**3 个,见 [permission.guard.ts](../src/middleware/permission.guard.ts)
- `MSG_NOTIFICATION_SEND``MSG_NOTIFICATION_READ``MSG_NOTIFICATION_MANAGE`
## 4. 我的技术栈
- 语言TypeScript 5.6ESM 模式)
- 框架NestJS 10
- ORMDrizzle ORM 0.31 + mysql2 3.11
- 全文检索:@elastic/elasticsearch 8.15(已实现 safeIndex/safeSearch**无 mapping 定义**,依赖动态 mapping
- 消息总线kafkajs 2.2**已装依赖但无 consumer/producer 代码**env.ts 有 KAFKA_BROKERS 默认值)
- 推送fetch POST 到 push-gateway `/internal/push`降级模式PUSH_GATEWAY_URL 未配置或失败时跳过)
- 幂等去重Redis**env.ts 预留 REDIS_URL 但无 redis 客户端依赖**pending-features P5 要求 event_id 去重)
- 可观测pino + prom-client + OpenTelemetry三支柱已具备
## 5. 我的阶段归属
- **P5 沟通与 AI 阶段**pending-features §P5
- 会话/消息 CRUD + 调 Push Gateway 推送 + 通知偏好
- 多渠道(站内/SMS/邮件/微信),沿用旧项目 dispatcher 模式
- Elasticsearch 题库全文检索(从 MySQL 同步)
- **退出标准**:教师发广播通知 → 全在线学生实时收到Push Gateway→ AI 辅助出题流式返回 → 题库全文检索 < 200ms
- **依赖上游**P1 黄金模板 classes、P3 core-edu事件来源、P5 push-gateway推送通道ai01 设计、P5 ai无直接依赖
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
- [x] 权限装饰器 `@RequirePermission`3 个 MSG_* 权限点,全部 Controller 方法已覆盖)
- [x] 错误码前缀统一(`MSG_*`
- [x] logger / metrics / tracer 三支柱(已具备)
- [x] `/healthz` 健康检查HealthModule 已注册)
- [⚠️] `/readyz`**仅检查 DB `SELECT 1`,未检查 ES 连通性**ES 故障时仍返回 ok
- [x] 优雅关闭 SIGTERMmain.ts 已处理app.close → closeEs → closeDb → shutdownTracer
- [ ] 测试覆盖率 ≥ 80%**当前 0%**,无测试文件)
- [x] Dockerfile 多阶段构建(已具备)
- [x] Zod 输入验证Controller 用 `schema.parse(body)`**GlobalErrorFilter 已识别 ZodError 返回 400**,见 [global-error.filter.ts](../src/shared/errors/global-error.filter.ts) L32-42ai10 复核修正)
- [x] GlobalErrorFilter 统一兜底
## 7. 现有骨架差距与待决策(提请 coord 仲裁)
| # | 差距 | 影响 | 提请决策 |
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| M1 | **Kafka 消费未实现**:装了 kafkajs 但无 consumer 代码,无 shared/kafka/ 目录 | 004 §7.3 列 msg 消费 5 类事件ExamPublished/HomeworkGraded/GradeRecorded/UserRegistered/MasteryUpdated | 阶段 2 设计需补 Kafka consumer + 幂等去重 |
| M2 | **无 Outbox**:骨架无 shared/outbox/ | 004 §7.2 明确 msg 生产 `edu.notification.events`NotificationRequested消费者 push-gateway**ai10 复核msg 必须有 Outbox**004 §7.2/§7.3 已定义,原"无明确"判断失实) | 阶段 2 设计补 Outbox参照 core-edu outbox.publisher.ts |
| M3 | **ES 无 mapping 定义**:依赖动态 mapping索引名 "notifications" 硬编码在 service | 检索质量不稳定,索引管理缺失 | 阶段 2 设计补 mapping + ensureIndex |
| M4 | **无独立 repository**notifications.service.ts 直接用 db无 repository 抽象 | 与黄金模板分层不一致iam 有 repository | 阶段 2 设计补 repository 层 |
| M5 | **Redis 未引入**env.ts 预留 REDIS_URL 但无 redis 客户端依赖 | pending-features P5 要求 event_id 去重Redis SETNX 或 DB 唯一索引) | 阶段 2 设计决策Redis SETNX vs DB 唯一索引 |
| M6 | **createBatch 无事务/无批量优化**for 循环串行调 send无批量 INSERT | 性能瓶颈(广播场景) | 阶段 2 设计改为批量 INSERT |
| M7 | **NotificationsModule 缺 exports**notifications.module.ts 无 `exports: [NotificationsService]` | 未来 BFF 注入受阻 | 阶段 2 设计补 exports |
| M8 | **~~ZodError 未特殊处理~~ai10 复核:已修复,失实条目)**GlobalErrorFilter 已识别 ZodError 返回 400见 [global-error.filter.ts](../src/shared/errors/global-error.filter.ts) L32-42 | 无(已修复) | ✅ 已修复,无需处理 |
| M9 | **gRPC 未实现**proto 定义了 NotificationService但无 gRPC controller | pending-features P5 未强制 gRPC | 确认 P5 是否启用 gRPCai03 提请统一决策) |
| M10 | **README 与实现脱节**README API 表标 `POST /notifications/:id/read`,实际是 PUT漏 batch/user/:userId/user/:userId/page 端点;声称"消费 Kafka 事件"但无代码 | 文档误导 | 阶段 2 设计同步修正 README |
| M11 | **main.ts 与 LifecycleService 重复关闭资源**:两者都调 closeDb/closeEs | 重复关闭可能报错(虽有 try-catch | 阶段 2 设计统一关闭逻辑到 LifecycleService |
| M12 | **~~proto 包名不一致~~ai10 复核:失实条目)**:实际 `next_edu_cloud.msg.v1` 完全符合 project_rules §5 `next_edu_cloud.<domain>.v1` 规范coord 已裁决采用 `next_edu_cloud.*` 格式) | 无(命名正确) | ✅ 命名正确,无需处理 |
---
## 服务审计表 — ai10msg 复核)
> 审计标准对照 [project_rules §3](../../../.trae/rules/project_rules.md) 与 [known-issues §2.2 classes 黄金模板](../../../docs/troubleshooting/known-issues.md)
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
| ---- | --------------- | ---------- | ------- | -------------- | ------------------------------- | -------- | ---------------- | --------------------------------------- | ---------- | ---------- |
| msg | ✅ 6 端点全覆盖 | ✅ `MSG_*` | ✅ pino | ✅ prom-client | ✅ OTel + auto-instrumentations | ✅ | ⚠️ 仅 DB 不查 ES | ⚠️ main.ts 与 LifecycleService 重复关闭 | 0% | ✅ |
## ai10 复核结论ai-allocation §5 设计重点对照)
ai-allocation.md §5 对 ai10msg明确列出 6 项设计重点,对照现有骨架:
| ai-allocation §5 设计重点 | 现状 | 阶段 2 是否需设计 |
| --------------------------------------------------- | ------------------------------ | ----------------- |
| 通知渠道抽象(站内信/邮件/短信,策略模式) | ❌ 无抽象,硬编码 | ✅ 必须 |
| ES 降级查询策略DB 不可用时走 ES 索引) | ❌ 仅 ES 降级,无 DB 降级到 ES | ✅ 必须 |
| Kafka 消费幂等设计event_id 去重 via Redis SETNX | ❌ 无 Kafka 代码 | ✅ 必须 |
| 与 ai02push-gateway的推送通道协议gRPC | ⚠️ 用 fetch POST 降级,无 gRPC | ✅ 需 gRPC 化 |
| 通知模板管理(模板 CRUD + 变量替换) | ❌ 无模板模块 | ✅ 必须 |
| 已读/未读状态管理Redis 位图) | ❌ 仅 DB boolean | ✅ 必须 |
**新增差距ai10 补充,原 ai05 文档遗漏)**
| # | 差距 | 影响 |
| --- | ------------------------------------------------------------------------------- | ----------------------------------------------------- |
| M13 | **无通知模板模块**:缺 msg_notification_templates 表、模板 CRUD、变量替换、i18n | ai-allocation §5 强制要求 |
| M14 | **无渠道策略抽象**send 方法硬编码 channel 判断,无 ChannelStrategy 接口 | 多渠道扩展困难(未来加微信/钉钉需改核心) |
| M15 | **无 Redis 位图已读/未读**isRead 仅 DB boolean无 unread 计数缓存 | 高频未读数查询打 DB广播场景性能瓶颈 |
| M16 | **无 DB→ES 降级读路径**:仅有 ES→空降级无 DB 故障时走 ES 读模型 | ai-allocation §5 要求 DB 不可用走 ES 索引 |
| M17 | **无投放记录表**:缺 msg_notification_deliveriesper-channel 投递状态) | 无法追踪邮件/短信投递成功失败 |
| M18 | **无幂等键**send 无 idempotencyKey 参数,重复请求会重复发通知 | 客户端重试导致重复通知 |
| M19 | **无通知优先级/分类**:缺 priority/category 字段 | 无法区分紧急通知与普通通知,无法按类退订 |
| M20 | **无调度/延迟发送**:缺 scheduledAt 字段与调度器 | 未来定时通知、每日摘要无法支持 |
| M21 | **权限模型过简**:仅 3 个权限点,硬编码 ROLE_PERMISSIONS | 应对齐 iam 的 RBAC + DataScopeDATA_SCOPE=SELF/ALL |
---
## 跨模块契约对齐提请coord 交叉审查)
### 接口一致性检查msg 相关)
| 本服务声明 | 对方服务声明 | 是否匹配 | 备注 |
| -------------------------------------------- | ------------------------------- | --------- | ------------------------------------------------------------------------------------------------ |
| ai05 msg: 消费 `edu.teaching.exam.published` | ai03 core-edu: 发布考试事件 | ⚠️ 待确认 | ai03 提请 topic 命名统一004 `edu.teaching.exam.published` vs core-edu 代码 `edu.exam.events` |
| ai05 msg: 消费 `edu.identity.user.created` | ai02 iam: 发布用户创建事件 | ⚠️ 待确认 | ai02 设计需确认 topic |
| ai05 msg: 调 push-gateway `/internal/push` | ai01 push-gateway: 暴露推送端点 | ✅ 已实现 | msg 当前用 fetch POSTpush-gateway 已有 /internal/push 端点 |
### 全局冲突检查msg 相关)
| 检查项 | 检查结果 | 备注 |
| -------------------- | --------------- | -------------------------------------------------------------------------------------------------------- |
| 端口不冲突 | ✅ msg 3007 | 与 iam(3002)/classes(3001)/teacher-bff(3003)/core-edu(3004)/content(3005)/data-ana(3006)/ai(3008) 不冲突 |
| Topic 不重复 | ⚠️ 待汇总 | msg 仅消费不发布 |
| 错误码前缀不重叠 | ✅ `MSG_*` 唯一 | 与 iam `IAM_*` / core-edu `CORE_EDU_*` / content `CONTENT_*` 不重叠 |
| Proto message 不遗漏 | ⚠️ 待确认 | events.proto 无 NotificationEvent若 msg 发事件需补充 |
| proto 包名规范 | ⚠️ 不一致 | 实际 `next_edu_cloud.<domain>.v1`,规则要求 `edu.<domain>.v1`**提请 coord 仲裁**ai03 已提请) |
### 待 coord 仲裁的决策项msg 相关)
1. ~~**proto 包名统一**`next_edu_cloud.*` vs `edu.*`~~ai10 复核coord 已裁决 `next_edu_cloud.*`,本项目全部 proto 已采用,无需再仲裁)
2. **gRPC 启用时机**P4/P5 是否启用 gRPC controller还是继续 REST影响 content/msg/iam/core-edu 全部服务)
3. ~~**msg Outbox**:是否需要~~ai10 复核004 §7.2 已明确 msg 生产 `edu.notification.events`**必须有 Outbox**,无需仲裁)
4. **msg Redis 引入**env.ts 预留 REDIS_URL 但无依赖,幂等去重用 Redis SETNX 还是 DB 唯一索引ai10 倾向 Redis SETNX + DB 唯一索引双保险,待 coord 确认 Redis 是否纳入 P5 基础设施)
5. **events.proto 补充**msg 发 NotificationRequested 事件需追加 NotificationEvent messagecoord 维护 proto
---
## 下一步
1. ~~等待 coord 审核本确认书~~ai10 复核修订完成,进入阶段 2
2. 进入**阶段 2模块架构设计文档**,按 ai-allocation.md §7 模板产出 [02-architecture.md](./02-architecture.md)
- msg 模块架构设计文档含通知渠道抽象策略模式、ES 降级查询、Kafka 消费幂等、push-gateway 推送协议、通知模板、已读/未读状态管理)
3. 阶段 2 设计完成后同步更新 README修正 §M10 文档脱节问题)
---
**AI Agent**: ai10 (msg)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式(直接 push main

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,816 @@
# 模块架构设计文档 — msg
> AI 标识ai10
> 负责模块msgP5 沟通通知)
> 阶段:架构设计外包 · 阶段 2模块架构设计
> 日期2026-07-09
> 关联文档:[01-understanding.md](./01-understanding.md)、[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[project_rules.md](../../../.trae/rules/project_rules.md)
> 参考实现:[classes 黄金模板](../../classes/src/)、[core-edu Outbox](../../core-edu/src/shared/outbox/)、[iam RBAC](../../iam/src/)
---
## 0. 设计原则与长远目标
本设计遵循以下原则,确保既能实现 P5 退出标准,又为 P6 硬化与未来扩展铺路:
1. **通知即基础设施**msg 是全校所有业务事件的下收件人,设计上按"通知中台"标准而非"业务 CRUD"标准,预留多租户、多渠道、多策略扩展点。
2. **契约先行**proto → 实现。NotificationService gRPC + events.proto NotificationEvent 必须先定。
3. **最终一致 + 幂等**Kafka 消费 at-least-once业务侧 event_id 去重Outbox 保证通知请求事件不丢。
4. **降级优先**每个外部依赖DB/ES/Redis/Push/SMS/邮件)都有降级路径,单点故障不阻断核心链路。
5. **策略模式贯穿渠道层**:新增渠道(微信/钉钉/企业微信)只需实现 ChannelStrategy 接口,不改核心。
6. **读路径分级**:列表/计数走 Redis 缓存 → DB → ES 三级降级;全文检索走 ES → DB like 降级。
7. **可观测可追踪**:每条通知从"事件触发 → 模板渲染 → 渠道投递 → 已读"全链路 traceId 串联,投递结果落 deliveries 表。
8. **向前兼容**:所有 schema 变更只增不删proto 字段只加编号不复用,事件版本用 v1/v2 后缀。
---
## 1. 模块内部分层图
```mermaid
flowchart TD
subgraph Gateway["API Gateway (L4)"]
GW[路由转发 + JWT 校验]
end
subgraph BFF["BFF (L3)"]
BFF1[teacher-bff / student-bff / parent-bff]
end
subgraph Msg["msg 服务 (L5 业务服务)"]
direction TB
subgraph Entry["入口层"]
CTRL[NotificationsController<br/>REST]
GRPC[NotificationGrpcController<br/>gRPC · P5 启用]
KCONS[KafkaConsumer<br/>事件消费]
end
subgraph Guard["横切层"]
PERM[PermissionGuard<br/>@RequirePermission]
VALID[Zod 校验]
FILTER[GlobalErrorFilter]
end
subgraph App["应用服务层 (ApplicationService)"]
NS[NotificationService<br/>编排:幂等 → 模板渲染 → fan-out]
TS[TemplateService<br/>模板 CRUD + 变量替换 + i18n]
DS[DeliveryService<br/>投放编排 + 重试 + DLQ]
PREF[PreferenceService<br/>偏好查询 + 静默时段]
READ[ReadStateService<br/>已读/未读 + Redis 位图]
IDEM[IdempotencyService<br/>event_id 去重 Redis SETNX]
end
subgraph Strategy["渠道策略层 (ChannelStrategy)"]
INAPP[InAppChannel<br/>DB+ES 写入]
EMAIL[EmailChannel<br/>SMTP async]
SMS[SmsChannel<br/>短信网关 + 限流 + 配额]
PUSH[PushChannel<br/>gRPC → push-gateway]
WECHAT[WechatChannel<br/>未来·预留接口]
end
subgraph Domain["领域服务 (DomainService)"]
REPO[NotificationRepository<br/>Drizzle 数据访问]
OUTBOX[OutboxPublisher<br/>NotificationRequested 事件]
end
subgraph Data["数据层"]
DB[(MySQL<br/>写模型)]
ES[(Elasticsearch<br/>全文检索 + 降级读)]
REDIS[(Redis<br/>幂等/位图/计数/限流)]
KAFKA[(Kafka<br/>消费+发布)]
end
end
subgraph External["外部依赖"]
PUSHGW[push-gateway<br/>WebSocket 推送]
SMTP[SMTP 邮件服务]
SMSGW[短信服务商]
end
GW --> CTRL
BFF1 --> GRPC
KAFKA --> KCONS
KCONS --> IDEM --> NS
CTRL --> PERM --> VALID --> NS
GRPC --> NS
NS --> TS
NS --> PREF
NS --> DS
NS --> READ
DS --> INAPP & EMAIL & SMS & PUSH
INAPP --> REPO --> DB
INAPP --> ES
PUSH --> PUSHGW
EMAIL --> SMTP
SMS --> SMSGW
NS --> OUTBOX --> KAFKA
READ --> REDIS
READ --> REPO
IDEM --> REDIS
```
**关键拦截点**
- `PermissionGuard`APP_GUARD每个 Controller 方法 `@RequirePermission` 校验
- `GlobalErrorFilter`ZodError → 400、ApplicationError → 对应状态码、兜底 500
- `IdempotencyService`Kafka 消费与 HTTP send 均先过幂等检查
- `OutboxPublisher`:事务内写 outbox 表,独立轮询发布到 Kafka
---
## 2. 领域模型
### 2.1 聚合根与实体
```mermaid
classDiagram
class Notification {
+id: UUID
+userId: string
+type: NotificationType
+category: NotificationCategory
+priority: Priority
+title: string
+content: string
+channel: Channel
+metadata: JSON
+scheduledAt?: DateTime
+expiresAt?: DateTime
+isRead: boolean
+readAt?: DateTime
+idempotencyKey?: string
+sourceEventId?: string
+createdAt: DateTime
+markAsRead()
+isExpired()
}
class NotificationTemplate {
+id: UUID
+code: string
+name: string
+category: NotificationCategory
+channelTemplates: Map~Channel, ChannelTemplate~
+variables: TemplateVariable[]
+isActive: boolean
+version: int
+render(ctx): RenderedContent
}
class ChannelTemplate {
+channel: Channel
+subjectTpl: string
+bodyTpl: string
+i18n: Map~locale, LocalizedTpl~
}
class NotificationPreference {
+userId: string
+channelEnabled: Map~Channel, boolean~
+categoryEnabled: Map~Category, boolean~
+quietHours: QuietHours
+digestConfig: DigestConfig
+isChannelAllowed(channel, category): boolean
}
class NotificationDelivery {
+id: UUID
+notificationId: UUID
+channel: Channel
+status: DeliveryStatus
+externalId?: string
+attemptCount: int
+lastError?: string
+deliveredAt?: DateTime
+markSent()
+markFailed(err)
+canRetry(): boolean
}
class QuietHours {
+start: string
+end: string
+timezone: string
+isInQuietHours(now): boolean
}
Notification "1" --> "0..*" NotificationDelivery : has
NotificationTemplate "1" --> "0..*" ChannelTemplate : contains
NotificationPreference "1" --> "1" QuietHours : has
enum NotificationType { SYSTEM ANNOUNCEMENT EXAM HOMEWORK GRADE ATTENDANCE MASTERY }
enum NotificationCategory { ACADEMIC BEHAVIOR SYSTEM MARKETING }
enum Priority { URGENT HIGH NORMAL LOW }
enum Channel { IN_APP EMAIL SMS PUSH WECHAT }
enum DeliveryStatus { PENDING SENT DELIVERED FAILED RETRYING }
```
### 2.2 聚合边界与通信
- **Notification 聚合**(根):包含 1:N Delivery。聚合内直接调用聚合间通过事件。
- **NotificationTemplate 聚合**(根):独立管理,被 NotificationService 引用渲染。
- **NotificationPreference 聚合**(根):按 userId 隔离,被 PreferenceService 查询。
- **跨聚合通信**TemplateService / PreferenceService / ReadStateService 同服务内直接方法调用(应用层编排),不发事件。
- **跨服务通信**:仅通过 Kafka 事件(消费 core-edu/iam/data-ana 事件;发布 NotificationRequested 给 push-gateway
---
## 3. 数据模型
### 3.1 表清单MySQLmsg 独占库)
| 表名 | 用途 | 现状 |
| ------------------------------ | -------------------------- | --------------- |
| `msg_notifications` | 通知主表 | ✅ 已有,需扩展 |
| `msg_notification_preferences` | 用户偏好 | ✅ 已有,需扩展 |
| `msg_notification_templates` | 通知模板 | ❌ 新增 |
| `msg_notification_deliveries` | 投递记录per-channel | ❌ 新增 |
| `msg_outbox` | Outbox 事件表 | ❌ 新增 |
| `msg_idempotency` | 幂等键Redis 不可用降级) | ❌ 新增 |
### 3.2 Schema 定义
#### msg_notifications扩展现有
| 字段 | 类型 | 约束 | 说明 |
| --------------- | ------------ | ---------------------------- | ---------------------------------- |
| id | char(36) | PK | UUID |
| user_id | char(36) | NOT NULL, idx | 接收人 |
| type | varchar(50) | NOT NULL, idx | EXAM/HOMEWORK/GRADE/SYSTEM... |
| category | varchar(30) | NOT NULL, default 'ACADEMIC' | ACADEMIC/BEHAVIOR/SYSTEM/MARKETING |
| priority | varchar(10) | NOT NULL, default 'NORMAL' | URGENT/HIGH/NORMAL/LOW |
| title | varchar(200) | NOT NULL | |
| content | text | NOT NULL | |
| channel | varchar(20) | NOT NULL, default 'in_app' | in_app/email/sms/push/wechat |
| metadata | json | nullable | 透传业务上下文 |
| scheduled_at | timestamp | nullable, idx | 调度发送时间(未来) |
| expires_at | timestamp | nullable | 过期清理 |
| is_read | boolean | NOT NULL, default false | |
| read_at | timestamp | nullable | |
| idempotency_key | varchar(128) | nullable, idx | 幂等键HTTP send 用) |
| source_event_id | varchar(128) | nullable, idx | 触发该通知的 Kafka event_id |
| created_at | timestamp | NOT NULL, default now, idx | |
**索引**
- `idx_user_created (user_id, created_at DESC)` — 用户列表查询主索引
- `idx_user_unread (user_id, is_read, created_at DESC)` — 未读列表
- `idx_source_event (source_event_id)` — 事件溯源
- `idx_idempotency (idempotency_key)` — 唯一索引(防重)
- `idx_scheduled (scheduled_at) WHERE scheduled_at IS NOT NULL` — 调度扫描
- `idx_expires (expires_at) WHERE expires_at IS NOT NULL` — 过期清理
#### msg_notification_preferences扩展现有
| 字段 | 类型 | 约束 | 说明 |
| ----------------- | ----------- | ----------------------- | ------------------------- |
| user_id | char(36) | PK | |
| email_enabled | boolean | default true | |
| sms_enabled | boolean | default false | |
| push_enabled | boolean | default true | |
| in_app_enabled | boolean | default true | |
| wechat_enabled | boolean | default false | **新增,未来渠道** |
| academic_enabled | boolean | default true | **新增,按分类退订** |
| behavior_enabled | boolean | default true | **新增** |
| system_enabled | boolean | default true | **新增** |
| marketing_enabled | boolean | default false | **新增** |
| quiet_start | varchar(5) | nullable | **新增** "22:00" 静默开始 |
| quiet_end | varchar(5) | nullable | **新增** "07:00" 静默结束 |
| quiet_timezone | varchar(40) | default 'Asia/Shanghai' | **新增** |
| digest_email | boolean | default false | **新增** 每日摘要开关 |
| updated_at | timestamp | default now | **新增** |
#### msg_notification_templates新增
| 字段 | 类型 | 约束 | 说明 |
| ----------- | ------------ | --------------- | ------------------------------- |
| id | char(36) | PK | UUID |
| code | varchar(64) | UNIQUE, idx | 模板编码(如 EXAM_PUBLISHED |
| name | varchar(100) | NOT NULL | 模板名称 |
| category | varchar(30) | NOT NULL | 分类 |
| channel | varchar(20) | NOT NULL | 渠道 |
| subject_tpl | varchar(200) | nullable | 标题模板(含 {{var}} |
| body_tpl | text | NOT NULL | 正文模板 |
| variables | json | nullable | 变量定义 [{name,required,desc}] |
| locale | varchar(10) | default 'zh-CN' | i18n |
| is_active | boolean | default true | |
| version | int | default 1 | 版本号 |
| created_at | timestamp | default now | |
| updated_at | timestamp | default now | |
**唯一索引**`uk_code_channel_locale (code, channel, locale)`
#### msg_notification_deliveries新增
| 字段 | 类型 | 约束 | 说明 |
| --------------- | ------------ | ------------- | -------------------------------------- |
| id | char(36) | PK | UUID |
| notification_id | char(36) | NOT NULL, idx | FK→notifications |
| channel | varchar(20) | NOT NULL | 投递渠道 |
| status | varchar(20) | NOT NULL | PENDING/SENT/DELIVERED/FAILED/RETRYING |
| external_id | varchar(128) | nullable | 外部网关返回 ID |
| attempt_count | int | default 0 | 重试次数 |
| max_retry | int | default 3 | 最大重试 |
| last_error | text | nullable | 失败原因 |
| next_retry_at | timestamp | nullable, idx | 下次重试时间 |
| delivered_at | timestamp | nullable | 投递成功时间 |
| created_at | timestamp | default now | |
| updated_at | timestamp | default now | |
**索引**`idx_notification (notification_id)``idx_retry (status, next_retry_at)`
#### msg_outbox新增参照 core-edu
| 字段 | 类型 | 约束 | 说明 |
| -------------- | ----------- | ------------------ | --------------------------- |
| id | char(36) | PK | UUID |
| aggregate_id | char(36) | NOT NULL, idx | notificationId |
| aggregate_type | varchar(30) | NOT NULL | 'Notification' |
| event_type | varchar(50) | NOT NULL | notification.requested/sent |
| payload | text | NOT NULL | JSON 序列化事件 |
| processed | boolean | default false, idx | |
| retry_count | int | default 0 | |
| created_at | timestamp | default now | |
| processed_at | timestamp | nullable | |
#### msg_idempotency新增Redis 降级用)
| 字段 | 类型 | 约束 | 说明 |
| ---------- | ------------ | ------------- | -------------------------- |
| key | varchar(128) | PK | event_id 或 idempotencyKey |
| result | json | NOT NULL | 首次执行结果(缓存复用) |
| created_at | timestamp | default now | |
| expires_at | timestamp | NOT NULL, idx | TTL默认 7 天) |
### 3.3 读写分离策略
| 读场景 | 主路径 | 降级路径 1 | 降级路径 2 |
| ------------ | ---------------------- | -------------------- | ---------------- |
| 用户通知列表 | DBidx_user_created | ESuser_id filter | — |
| 未读计数 | Redis 位图 BITCOUNT | DB COUNT(*) | ES count |
| 全文检索 | ES multi_match | DB LIKE降级 | 返回空(不阻断) |
| 用户偏好 | Redis 缓存5min TTL | DB | 默认偏好(内存) |
| 模板渲染 | Redis 缓存10min | DB | — |
**写路径**:所有写走 MySQL 主库单主无读写分离ES 通过同步索引异步写入safeIndex 失败不阻断)。
### 3.4 Elasticsearch 索引设计
索引名:`msg_notifications_v1`(带版本号,便于 reindex
```json
{
"mappings": {
"properties": {
"userId": { "type": "keyword" },
"type": { "type": "keyword" },
"category": { "type": "keyword" },
"priority": { "type": "keyword" },
"channel": { "type": "keyword" },
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"isRead": { "type": "boolean" },
"createdAt": { "type": "date" },
"scheduledAt": { "type": "date" }
}
},
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"refresh_interval": "1s"
}
}
```
**索引管理**
- 启动时 `ensureIndex()`:若索引不存在则创建(带 mapping
- 别名 `msg_notifications``msg_notifications_v1`reindex 时切换别名实现零停机
- 中文分词需 ik 插件(已在 infra 配置)
---
## 4. API 设计
### 4.1 REST APIHTTP经 api-gateway
| method | path | 权限 | 说明 |
| ------ | ---------------------------------------- | ----------------------- | ---------------------------- |
| POST | /notifications | MSG_NOTIFICATION_SEND | 发送单条通知 |
| POST | /notifications/batch | MSG_NOTIFICATION_SEND | 批量发送(广播) |
| GET | /notifications/user/:userId | MSG_NOTIFICATION_READ | 查询用户通知(?unread=true |
| GET | /notifications/user/:userId/page | MSG_NOTIFICATION_READ | 分页查询 |
| GET | /notifications/user/:userId/unread-count | MSG_NOTIFICATION_READ | 未读计数Redis 位图优先) |
| PUT | /notifications/:id/read | MSG_NOTIFICATION_MANAGE | 标记已读 |
| PUT | /notifications/read-all | MSG_NOTIFICATION_MANAGE | 全部已读(按 userId |
| GET | /notifications/search | MSG_NOTIFICATION_READ | 全文检索 |
| DELETE | /notifications/:id | MSG_NOTIFICATION_MANAGE | 删除通知(软删,未来) |
| GET | /notifications/preferences/:userId | MSG_NOTIFICATION_READ | 查询偏好 |
| PUT | /notifications/preferences/:userId | MSG_NOTIFICATION_MANAGE | 更新偏好 |
| POST | /notifications/templates | MSG_TEMPLATE_MANAGE | 创建模板 |
| GET | /notifications/templates | MSG_TEMPLATE_MANAGE | 模板列表 |
| GET | /notifications/templates/:code | MSG_TEMPLATE_MANAGE | 查询单个模板 |
| PUT | /notifications/templates/:id | MSG_TEMPLATE_MANAGE | 更新模板 |
| DELETE | /notifications/templates/:id | MSG_TEMPLATE_MANAGE | 停用模板(软删) |
### 4.2 gRPC APINotificationServiceproto 包 `next_edu_cloud.msg.v1`
| 方法 | 请求 | 响应 | 说明 |
| ------------------- | -------------------------- | --------------------------- | ---------------- |
| SendNotification | SendNotificationRequest | Notification | 发送通知 |
| ListNotifications | ListNotificationsRequest | ListNotificationsResponse | 列表查询 |
| MarkAsRead | MarkAsReadRequest | Empty | 标记已读 |
| MarkAllAsRead | MarkAllAsReadRequest | Empty | 全部已读(新增) |
| GetUnreadCount | GetUnreadCountRequest | GetUnreadCountResponse | 未读计数(新增) |
| SearchNotifications | SearchNotificationsRequest | SearchNotificationsResponse | 全文检索 |
| GetPreference | GetPreferenceRequest | NotificationPreference | 偏好查询(新增) |
| UpdatePreference | UpdatePreferenceRequest | NotificationPreference | 偏好更新(新增) |
> proto 字段扩展(在现有 msg.proto 基础上新增 category/priority/scheduledAt/expiresAt/idempotencyKey 等),按 §5 契约规范走 coord 变更流程。
### 4.3 请求/响应示例(发送通知)
```json
POST /notifications
{
"userId": "u-123",
"type": "EXAM",
"category": "ACADEMIC",
"priority": "HIGH",
"title": "数学期中考试已发布",
"content": "请于 2026-07-15 前完成",
"channel": "in_app",
"metadata": { "examId": "e-456" },
"idempotencyKey": "client-uuid-789",
"scheduledAt": null
}
```
```json
{
"success": true,
"data": {
"id": "notif-uuid",
"skipped": false,
"deliveries": [
{ "channel": "in_app", "status": "SENT" },
{ "channel": "push", "status": "SENT", "externalId": "push-msg-id" }
]
}
}
```
---
## 5. 事件设计
### 5.1 消费的事件Kafka Consumer
消费组:`msg-service`partition 按 aggregate_id 保证同聚合有序。
| Topic | 生产者 | 事件类型 | msg 动作 |
| ----------------------------------- | -------- | ----------------- | ----------------------------------- |
| `edu.identity.user.created` | iam | UserRegistered | 发欢迎通知in_app + email |
| `edu.identity.user.updated` | iam | UserUpdated | 通知关键信息变更 |
| `edu.identity.user.role_changed` | iam | UserRoleChanged | 通知角色变更 |
| `edu.teaching.exam.published` | core-edu | ExamPublished | 给班级全体学生发考试通知fan-out |
| `edu.teaching.assignment.submitted` | core-edu | HomeworkSubmitted | 通知教师有学生提交作业 |
| `edu.teaching.grade.recorded` | core-edu | GradeRecorded | 通知学生成绩已录入 |
| `edu.insight.mastery.updated` | data-ana | MasteryUpdated | 掌握度低于阈值触发预警通知 |
**消费幂等**:每个消息的 `event_id`proto 字段)→ Redis SETNX `msg:idem:{event_id}` TTL 7 天。SETNX 成功才处理失败说明已处理跳过。Redis 不可用时降级到 `msg_idempotency` 表唯一键。
### 5.2 发布的事件Outbox → Kafka
| Topic | 事件类型 | 触发时机 | 消费者 |
| ------------------------- | ---------------------- | -------------------- | ------------ |
| `edu.notification.events` | notification.requested | 通知创建后,请求推送 | push-gateway |
| `edu.notification.events` | notification.delivered | 渠道投递成功(未来) | data-ana |
| `edu.notification.events` | notification.read | 用户标记已读(未来) | data-ana |
> NotificationRequested 事件需 coord 在 events.proto 追加 NotificationEvent message见 §7 交互点)。
### 5.3 事件版本演化
- 字段只增不减proto 编号不复用
- 破坏性变更新建 `v2` 后缀 topic`edu.notification.events.v2`),消费者双消费过渡期后下线 v1
---
## 6. 横切关注点对齐清单
### 6.1 权限装饰器(权限点清单)
| 权限常量 | 说明 | 端点 |
| ----------------------- | --------------------- | ---------------------------------------------------------------- |
| MSG_NOTIFICATION_SEND | 发送通知 | POST /notifications, POST /notifications/batch, gRPC Send |
| MSG_NOTIFICATION_READ | 读取通知 | GET /notifications/user/*, search, unread-count, GET preferences |
| MSG_NOTIFICATION_MANAGE | 管理通知(已读/删除) | PUT /read, read-all, DELETE, PUT preferences |
| MSG_TEMPLATE_MANAGE | 模板管理(新增) | POST/GET/PUT/DELETE /notifications/templates/* |
> 权限模型从硬编码 ROLE_PERMISSIONS 迁移到对齐 iam 的 RBAC角色 → 权限点映射由 iam 通过 `getEffectivePermissions` 下发PermissionGuard 读取 `x-user-permissions` header由 Gateway 注入)。过渡期保留 ROLE_PERMISSIONS 兜底。
### 6.2 错误码清单(前缀 `MSG_`
| 错误码 | 触发条件 | HTTP |
| -------------------------- | ----------------------------- | ---- |
| MSG_VALIDATION_ERROR | Zod 校验失败 | 400 |
| MSG_NOT_FOUND | 通知/模板/偏好不存在 | 404 |
| MSG_PERMISSION_DENIED | 权限不足 | 403 |
| MSG_CONFLICT | 幂等键冲突/重复发送 | 409 |
| MSG_BUSINESS_ERROR | 偏好禁用该渠道/静默时段跳过 | 422 |
| MSG_TEMPLATE_RENDER_ERROR | 模板变量缺失/渲染失败(新增) | 422 |
| MSG_RATE_LIMITED | 短信/邮件限流(新增) | 429 |
| MSG_QUOTA_EXCEEDED | 短信配额耗尽(新增) | 429 |
| MSG_DATABASE_ERROR | DB 操作失败 | 500 |
| MSG_EXTERNAL_GATEWAY_ERROR | SMTP/SMS 网关错误(新增) | 502 |
| MSG_INTERNAL_ERROR | 兜底 | 500 |
### 6.3 可观测性
**Logger**pino结构化 JSON注入 traceId`x-request-id` header。初始化位置 [logger.ts](../src/shared/observability/logger.ts)。
**Metrics 指标清单**prom-client前缀 `msg_`
| 指标名 | 类型 | 标签 | 说明 |
| ------------------------------------- | --------- | ------------------------------ | --------------- |
| msg_notification_sent_total | Counter | channel,type,category,priority | 通知发送总数 |
| msg_notification_delivered_total | Counter | channel,status | 投递结果总数 |
| msg_notification_read_total | Counter | type | 已读总数 |
| msg_kafka_consumed_total | Counter | topic,event_type | Kafka 消费总数 |
| msg_kafka_consumed_errors_total | Counter | topic | Kafka 消费失败 |
| msg_outbox_pending | Gauge | — | Outbox 待发布数 |
| msg_outbox_publish_duration_seconds | Histogram | — | Outbox 发布耗时 |
| msg_template_render_duration_seconds | Histogram | code,channel | 模板渲染耗时 |
| msg_idempotency_dedup_total | Counter | source(kafka/http) | 幂等去重命中数 |
| msg_channel_delivery_duration_seconds | Histogram | channel | 渠道投递耗时 |
**Tracer**OpenTelemetry SDK + OTLP exporter初始化位置 [tracer.ts](../src/shared/observability/tracer.ts)。span 命名:`msg.<service>.<method>`(如 `msg.notification.send``msg.kafka.consume`)。
### 6.4 健康检查
| 端点 | 检查逻辑 | 失败状态 |
| ------------ | ------------------------------------------------------------ | -------- |
| GET /healthz | 进程存活,不检查依赖 | 200 |
| GET /readyz | DB `SELECT 1` + Redis `PING` + ES `ping`(任一失败返回 503 | 503 |
> 当前 /readyz 仅查 DB**阶段 2 实现需补 Redis + ES 检查**ai10 修订)。
### 6.5 优雅关闭顺序
统一到 LifecycleService移除 main.ts 重复逻辑),顺序:
1. HTTP server 停止接收新请求(`app.close()`
2. Kafka consumer 提交 offset 并停止(`consumer.disconnect()`
3. OutboxPublisher 停止轮询(`stop()`
4. Elasticsearch 关闭(`closeEs()`
5. Redis 关闭(`redisClient.quit()`
6. MySQL 连接池关闭(`closeDb()`
7. Tracer flush 并关闭(`shutdownTracer()`
---
## 7. 与其他模块的交互点(契约清单)
| 方向 | 对方服务 | 协议 | 接口/事件 | 用途 |
| ------ | ------------ | ----- | ----------------------------------------------- | ----------------------------------------- |
| 被调用 | api-gateway | HTTP | /notifications/* | REST 转发 |
| 被调用 | teacher-bff | gRPC | NotificationService.* | 教师端通知聚合 |
| 被调用 | student-bff | gRPC | NotificationService.* | 学生端通知查询 |
| 被调用 | parent-bff | gRPC | NotificationService.* | 家长端通知查询 |
| 调用 | push-gateway | gRPC | PushService.Pushai02 定义) | 实时推送(替代当前 fetch /internal/push |
| 消费 | iam | Kafka | edu.identity.user.* | 用户创建/更新/角色变更触发通知 |
| 消费 | core-edu | Kafka | edu.teaching.exam.published 等 | 考试/作业/成绩事件触发通知 |
| 消费 | data-ana | Kafka | edu.insight.mastery.updated | 掌握度预警 |
| 发布 | — | Kafka | edu.notification.events | NotificationRequested → push-gateway |
| 读 | iam | gRPC | UserService.GetUser可选查用户信息渲染模板 | 模板变量填充(如学生姓名) |
### 跨模块契约对齐提请 coord
1. **events.proto 补充 NotificationEvent**msg 发布 NotificationRequested 需 proto 定义coord 维护)
2. **push-gateway gRPC PushService**ai02 需定义 gRPC Push 方法msg 从 fetch 迁移到 gRPC当前 fetch 作为降级保留)
3. **iam 用户信息查询**模板渲染需用户姓名msg 是否调 iam.GetUser还是事件 payload 携带足够信息?(倾向后者,避免同步依赖)
4. **msg.proto 扩展**:新增 MarkAllAsRead / GetUnreadCount / GetPreference / UpdatePreference 方法coord 统一更新 proto
---
## 8. 渠道策略模式设计ai-allocation §5 核心)
```mermaid
classDiagram
class ChannelStrategy {
<<interface>>
+channel: Channel
+send(notification, rendered): Promise~DeliveryResult~
+isAvailable(): boolean
+checkRateLimit(userId): boolean
}
class InAppChannel {
+send(): DB insert + ES index
+isAvailable(): DB up
}
class EmailChannel {
+send(): SMTP send (async queue)
+isAvailable(): SMTP reachable
+checkRateLimit(): Redis 滑动窗口
}
class SmsChannel {
+send(): 短信网关 API
+isAvailable(): 网关可达 + 配额未满
+checkRateLimit(): Redis 严格限流
+checkQuota(): Redis 计数
}
class PushChannel {
+send(): gRPC → push-gateway
+isAvailable(): push-gateway up
+fallback(): InAppChannel
}
class WechatChannel {
+send(): 未来实现
+isAvailable(): false (P5 不实现)
}
ChannelStrategy <|.. InAppChannel
ChannelStrategy <|.. EmailChannel
ChannelStrategy <|.. SmsChannel
ChannelStrategy <|.. PushChannel
ChannelStrategy <|.. WechatChannel
```
**渠道编排逻辑**DeliveryService
1. 按 NotificationPreference 过滤被禁用渠道
2. 按 QuietHours 过滤非紧急通知URGENT 绕过静默)
3. 对每个启用渠道并行调用 `ChannelStrategy.send()`
4. 失败渠道记录到 deliveries 表,按 max_retry 重试(指数退避)
5. PushChannel 失败降级到 InAppChannel保证不丢
**未来扩展**(不改核心):新增微信/钉钉/企业微信渠道,只需实现 ChannelStrategy 并注册到 ChannelRegistry。
---
## 9. 已读/未读状态管理Redis 位图)
### 设计
- 每个用户一个 Redis Bitmapkey `msg:unread:{userId}`bit offset = 通知序号(自增)
- 新通知:`SETBIT msg:unread:{userId} {seq} 1`
- 标记已读:`SETBIT msg:unread:{userId} {seq} 0`
- 未读计数:`BITCOUNT msg:unread:{userId}`
- 全部已读:`SET msg:unread:{userId} 0`(清空)
### 序号分配
- Redis INCR `msg:seq:{userId}` 获取递增序号,存入 notifications.seq 字段(新增)
### 降级策略
- Redis 不可用 → 降级到 DB `COUNT(*) WHERE user_id=? AND is_read=false`
- DB 是 source of truthRedis 是缓存标记已读时双写DB update + Redis SETBITRedis 失败仅 log 不阻断
### 未来扩展
- 位图支持按 category 分桶(`msg:unread:{userId}:ACADEMIC`)实现分类未读数
---
## 10. ES 降级查询策略ai-allocation §5
### 双向降级
| 触发场景 | 主路径 | 降级路径 |
| ------------ | ------ | -------------------------- |
| 正常列表查询 | DB | ESDB 不可用时) |
| 正常全文检索 | ES | DB LIKEES 不可用时) |
| DB 不可用 | — | ES 读模型(可能延迟 1s |
| ES 不可用 | — | DB LIKE 或返回空(检索类) |
### 实现要点
- `NotificationRepository` 内置 `listByUser()` 优先 DBcatch DB error 后 fallback 到 `esSearch()`
- `search()` 优先 EScatch ES error 后 fallback 到 `dbLikeSearch()`
- 降级时 metrics 记录 `msg_db_fallback_total` / `msg_es_fallback_total` 便于监控降级频率
- ES 读模型通过 `safeIndex` 异步同步,可能存在 1s 延迟,降级时在响应头标注 `X-Read-Model: es-degraded`
---
## 11. Kafka 消费幂等设计ai-allocation §5
```mermaid
flowchart TD
K[Kafka 消息] --> CHECK{Redis SETNX<br/>msg:idem:event_id}
CHECK -- 设置成功 --> PROC[处理事件<br/>渲染模板→fan-out]
CHECK -- 已存在 --> SKIP[跳过,返回原结果]
PROC --> WRITE[写 notifications + deliveries + outbox]
WRITE --> CACHE[缓存结果到 Redis]
PROC -.失败.-> DLQ[Dead Letter Topic<br/>edu.notification.dlq]
DLQ --> ALERT[告警 + 人工处理]
```
**双保险**
- L1Redis SETNX快速去重TTL 7 天)
- L2`msg_idempotency` 表唯一键Redis 不可用时兜底)
- L3`notifications.source_event_id` 唯一索引(最终防线)
**死信队列**:消费失败超过 3 次的消息投递到 `edu.notification.dlq`,触发告警,人工介入。
---
## 12. 风险与假设
### 12.1 技术风险
| 风险 | 影响 | 缓解措施 |
| -------------------------------------- | ----------------- | ------------------------------------------------------ |
| 广播场景 fan-out 性能1 通知→N 学生) | DB 写入瓶颈 | 批量 INSERT + 异步 ES 索引 + Push 走 push-gateway 批量 |
| 短信成本失控 | 资金损失 | 严格限流 + 配额管理 + 降级到 in_app |
| Redis 单点故障 | 未读计数/幂等失效 | 降级到 DB双写保证最终一致 |
| Kafka 消费积压 | 通知延迟 | 监控 lagHPA 扩容 consumer 实例 |
| 模板变量缺失导致渲染失败 | 通知发不出 | 渲染失败记 MSG_TEMPLATE_RENDER_ERROR降级用默认文案 |
| ES 与 DB 数据不一致 | 检索结果缺失 | 定期 reconcile job未来 P6降级读标注 |
### 12.2 假设
- 假设 iam 在 P2 已提供 `getEffectivePermissions` APImsg 的 PermissionGuard 可读取权限(过渡期用 ROLE_PERMISSIONS 兜底)
- 假设 push-gatewayai02P5 提供 gRPC PushService当前 fetch /internal/push 作为降级)
- 假设 coord 在 events.proto 补充 NotificationEvent message
- 假设 Redis 纳入 P5 基础设施infra/docker-compose.yml 已有 edu-redismsg 共用)
- 假设 core-edu 事件 payload 携带足够字段classId/studentId/title供模板渲染msg 不需同步调 iam
### 12.3 未决决策(提请 coord 仲裁)
1. **Redis 是否 P5 必选**msg 幂等/位图/限流强依赖 Redis若 P5 不引入则需全部降级到 DB性能折损
2. **gRPC 启用时机**msg gRPC controller 是否 P5 实现(影响 BFF 调用方式)
3. **短信/邮件服务商选型**:影响 ChannelStrategy 实现P5 是否实现真实发送,还是 mock + 接口预留)
4. **每日摘要digest是否 P5**:影响 scheduler 设计(倾向 P6 实现P5 仅预留 schema
---
## 13. 实施路线P5 → P6 演进)
### P5当前阶段最小可用
| 优先级 | 工作项 | 依赖 |
| ------ | ------------------------------------------ | --------------------------- |
| P0 | Repository 抽象层M4 | — |
| P0 | Kafka consumer + 幂等M1/M5 | Redis |
| P0 | Outbox + NotificationRequested 事件M2 | events.proto 补充 |
| P0 | 渠道策略模式 + InApp/Push/Email/SmsM14 | push-gateway gRPC |
| P0 | 通知模板模块M13 | — |
| P0 | Redis 位图已读/未读M15 | Redis |
| P0 | ES mapping + ensureIndexM3 | — |
| P0 | DB→ES 降级读路径M16 | — |
| P1 | 投递记录表 + 重试M17 | — |
| P1 | 幂等键HTTP sendM18 | — |
| P1 | 优先级/分类M19 | — |
| P1 | /readyz 补 Redis+ES 检查 | — |
| P1 | 优雅关闭统一到 LifecycleServiceM11 | — |
| P1 | NotificationsModule exportsM7 | — |
| P1 | 权限模型对齐 iam RBACM21 | iam getEffectivePermissions |
| P1 | gRPC controllerM9待 coord 决策) | gRPC 启用决策 |
| P2 | README 修正M10 | — |
| P2 | 测试覆盖率 ≥ 80% | — |
### P6+未来硬化schema 预留)
- 调度/延迟发送scheduledAt 字段已预留)→ Scheduler worker
- 每日摘要邮件digest_email 字段已预留)→ Digest worker
- 通知过期清理expires_at 字段已预留)→ TTL job
- 微信/钉钉渠道WechatChannel 接口已预留)
- 多租户隔离tenant_id 字段预留)
- Webhook 渠道(系统对接)
---
## 14. 与黄金模板classes对齐 checklist
| 检查项 | msg 现状 | 阶段 2 目标 |
| ----------------------- | --------------- | ------------------ |
| @RequirePermission 覆盖 | ✅ 6 端点 | ✅ 全部新端点 |
| 错误码前缀 | ✅ MSG_ | ✅ 保持 |
| logger/metrics/tracer | ✅ | ✅ 补 msg 专属指标 |
| /healthz + /readyz | ⚠️ readyz 仅 DB | ✅ 补 Redis+ES |
| 优雅关闭 | ⚠️ 重复关闭 | ✅ 统一 Lifecycle |
| 测试覆盖率 | 0% | ≥ 80% |
| Dockerfile 多阶段 | ✅ | ✅ |
| Zod 输入验证 | ✅ | ✅ 新 DTO 同步 |
| GlobalErrorFilter | ✅ 含 ZodError | ✅ |
| Repository 抽象 | ❌ | ✅ 新增 |
| Outbox | ❌ | ✅ 新增 |
| Module exports | ❌ | ✅ 新增 |
---
**AI Agent**: ai10 (msg)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式(直接 push main

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
# 模块理解确认书 — teacher-bff
> AI 标识ai03
> 负责模块teacher-bffP2
> 阶段:架构设计外包 · 阶段 1全局理解
> 日期2026-07-09v1/ 2026-07-09v2 审计修订)
> 关联文档:[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)
>
> **v2 修订说明**:依据 ai-allocation.md §3.2 修正 ai03 责任范围(仅 teacher-bffcore-edu 由 ai08 负责);修正 /readyz 实际状态修正错误码前缀描述补充前瞻性内容SSE 流式透传、Kafka 缓存失效、DataLoader 覆盖、DataScope 透传)。
---
## 1. 我在架构中的位置
- **层级**BFF 聚合层L4
- **上游**api-gatewayGo/Gin通过 HTTP 转发请求,注入 `x-user-id` / `x-user-roles`
- **下游**iam3002 / gRPC 50052、classes3001 / P3 合并入 core-edu、core-edu3004 / gRPC 50053P4 扩展 contentgRPC 50054、data-anagRPC 50055P5 扩展 msggRPC 50056、aigRPC 50057含 StreamChat 流式 RPC
- **通信方式**
- 当前P2对前端 REST对下游 REST `fetch`(同步)
- 目标态004 §4.1 / pending-features P2**gRPC** 调下游业务服务 + **GraphQL Yoga** 对前端暴露 + DataLoader 防 N+1
- 演进路径P2 REST 内部封装为 client 抽象层 → P3 引入 gRPC clientiam + core-edu→ P4 GraphQL Yoga 对前端 + DataLoader + content/data-ana gRPC → P5 SSE 流式透传ai+ msg gRPC + 可选 Kafka 缓存失效消费者
- **端口**3003 HTTP见 [teacher-bff env.ts](../src/config/env.ts)BFF 不暴露 gRPC 端口(只被 Gateway HTTP 调用)
## 2. 我的限界上下文
- **聚合职责**:教学场景域(教师 / 教导主任 / 教研组长 共用)的数据聚合、裁剪、协议转换
- **业务领域**:跨 D2 教学组织 + D3 教学核心 + D1 身份认证(只读拉取视口/权限)
- **我不负责**
- 不持有业务状态(无 DB 写入,无 Outbox
- 不做权限决策(依赖 Gateway JWT 校验 + 下游服务 `@RequirePermission`
- 不直接访问任何业务服务数据库
- **复用策略**004 §5.4):教导主任 / 教研组长复用 Teacher BFF通过视口差异化L1 导航扩展管理菜单L4 DataScope 扩大到年级)
## 3. 我与外部的契约
- **消费的 proto message**(按阶段,详见 [02-architecture-design.md](./02-architecture-design.md) §7
- P2`iam.v1.IamService`GetUserInfo**待补 RPC**GetViewports / GetEffectivePermissions / Logout / GetPublicKey`classes.v1.ClassService`ListClasses / GetClass
- P3`core_edu.v1.ExamService / HomeworkService / GradeService`(全部 RPC**待补 RPC**AttendanceService、GetClassesByTeacher
- P4`content.v1.KnowledgeGraphService`GetPrerequisites / GetLearningPath**待补 RPC**ChapterService / QuestionService`analytics.v1.AnalyticsService`GetClassPerformance / GetStudentWeakness / GetLearningTrend**待补 RPC**GetTeacherDashboardStats
- P5`ai.v1.AiService`GenerateQuestion / **StreamChat 流式 RPC** / Chat / OptimizeExpression`msg.v1.NotificationService`ListNotifications / SearchNotifications / MarkAsRead
- **暴露的 API**(当前 REST目标 GraphQL Yoga详见 02 文档 §4
- `GET /teacher/dashboard` — 聚合 IAM 用户信息 + classes 列表
- `GET /teacher/viewports` — 拉取 IAM 视口配置L1 导航)
- `GET /teacher/classes/:classId/exams` — 聚合 core-edu 考试列表
- `GET /teacher/classes/:classId/homework` — 聚合 core-edu 作业列表
- `GET /teacher/exams/:examId/grades` — 聚合 core-edu 成绩列表
- P4+ 目标GraphQL Query/Mutationdashboard / viewports / classes / exams / homework / grades / knowledgePath / classPerformance / studentWeakness / notifications+ MutationcreateExam / assignHomework / recordGrade / generateQuestion
- P5SSE 流式端点AI 对话透传)
- **错误码前缀**:当前代码用 `TEACHER_BFF_*`(违反 project_memory 规则 "BFF 错误码必须用 `BFF_XXX_` 前缀"**需迁移为 `BFF_TEACHER_*`**coord P0 整改 #3);业务错误透传下游 `CORE_EDU_*` / `IAM_*` / `CLASSES_*`
- **缓存**:聚合结果 Redis 短缓存 5-30s004 §6.3,当前未实现);权限列表 5min 缓存 + 事件驱动失效(订阅 `edu.identity.user.role_changed` / `edu.identity.role.updated`P3+ 引入 Kafka consumerP2 用短 TTL 兜底)
- **DataScope 透传**BFF 不解析 dataScope 语义,从 IAM 获取后作为 gRPC metadata 透传给下游服务,下游 Repository 层注入 WHERE 条件(详见 02 文档 §9
## 4. 我的技术栈
- 语言TypeScript 5.5+ESM 模式,相对 import 带 `.js` 后缀)
- 框架NestJS 10
- 下游通信:当前 `fetch`REST→ P3 引入 `@grpc/grpc-js` + `@bufbuild/protobuf`(经 client 抽象层,平滑切换)
- 对前端:当前 REST → P4 目标 GraphQL Yoga + DataLoaderurql 客户端)
- 缓存Redis待引入ioredis 客户端)
- 流式P5 SSE 透传ai.StreamChat streaming RPC → BFF → 前端 EventSource
- 可观测pino logger + prom-client metrics + OpenTelemetry tracer已具备 [tracer.ts](../src/shared/observability/tracer.ts)
- 弹性P6 引入 circuit breakeropossum+ retrygRPC interceptor
- 测试Vitest待引入目标覆盖率 ≥ 80%
## 5. 我的阶段归属
- **P2 身份**:教师登录 → 获取 JWT → 访问 teacher-portal → 侧边栏按 viewports.L1 渲染 → 空白 Dashboard
- **P3 扩展**:考试/作业/成绩的查询与 mutationiam + core-edu 切 gRPC引入 Redis 聚合缓存
- **P4 扩展**知识图谱查询content+ 学情诊断data-anaGraphQL Yoga 对前端 + DataLoadercontent + data-ana 切 gRPC
- **P5 扩展**AI 辅助出题SSE 流式透传)+ 通知查询聚合msgai + msg 切 gRPC可选 Kafka 缓存失效消费者
- **P6 硬化**circuit breaker + retry + HPA + mTLS99.9% 可用性
- **依赖上游**P1 黄金模板 classes、P2 iamgetEffectivePermissions + 视口)
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
- [ ] 权限装饰器 `@RequirePermission`**BFF 不做权限决策**当前无目标态BFF 不加 Guard仅校验 `x-user-id` 存在)
- [x] 错误处理:[GlobalErrorFilter](../src/shared/errors/global-error.filter.ts) + ApplicationError 层次(**错误码前缀需迁移 `TEACHER_BFF_*``BFF_TEACHER_*`**
- [x] logger / metrics / tracer 三支柱(已具备)
- [x] `/healthz` 健康检查HealthModule 已注册)
- [ ] `/readyz`(端点已存在,但**未做下游就绪探针**,需在 P2 补全:并行 ping iam + classes + core-edu 的 /healthz
- [x] 优雅关闭 SIGTERMmain.ts 已处理)
- [ ] 测试覆盖率 ≥ 80%**当前 0%**无测试文件P2 引入 Vitest
- [x] Dockerfile 多阶段构建builder + runtime已具备见 [Dockerfile](../Dockerfile)
- [ ] Zod 输入验证(当前 Controller 直接透传 unknown**未做 Zod 校验**P2 补全)
- [x] GlobalErrorFilter 统一兜底
---
## 服务审计表 — ai03teacher-bff 部分)
> 对照 [黄金模板 classes 服务](../../classes/src/),审计已实现的 teacher-bff 服务。状态:✅ 达标 / ⚠️ 部分 / ❌ 缺失
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
| ----------- | --------------------------------------- | ---------------------------------------------- | ------- | -------------- | ------- | -------- | ----------------------- | -------- | ---------- | ---------- |
| teacher-bff | ⚠️ 无BFF 不做权限决策,依赖 Gateway | ⚠️ `TEACHER_BFF_*`(需迁移为 `BFF_TEACHER_*` | ✅ pino | ✅ prom-client | ✅ OTel | ✅ | ⚠️ 端点存在,无下游探针 | ✅ | 0% ❌ | ✅ 多阶段 |
### 审计发现的关键差距P2 阶段 2 设计需解决)
1. ⚠️ 通信方式:当前 REST `fetch`,需引入 client 抽象层为 P3 gRPC 切换铺路GraphQL Yoga + DataLoader 按 coord 仲裁决定 P2 还是 P4 引入(见 02 文档 §4.1
2. ❌ 无 Redis 聚合缓存004 §6.3 要求 5-30s 短缓存)
3. ❌ 无 DataLoader防 N+1pending-features P2 明确要求,目标态必备)
4. ⚠️ `/readyz` 端点存在但无下游就绪探针
5. ⚠️ env 配置用 `IamServiceUrl`/`ClassesServiceUrl`REST URL需抽象为 service target支持 P3+ gRPC target
6. ⚠️ 无 Zod 输入验证
7. ⚠️ 无测试
8. ⚠️ 错误码前缀违反 project_memory 规则,需迁移 `TEACHER_BFF_*``BFF_TEACHER_*`
### 跨模块契约对齐待确认项(提请 coord 交叉审查)
| 待确认项 | 我方期望 | 对方模块 | 状态 |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------- |
| iam `getEffectivePermissions(userId)` 返回结构 | `{permissions, viewports, dataScope}` 聚合 API建议 `GetEffectiveAccess` RPC | iamai06 | ⚠️ 当前 teacher-bff 调 `/iam/viewports``/iam/me`,未定义此聚合 API 的 proto |
| iam 补 RPCGetViewports / GetEffectivePermissions / Logout / GetPublicKey | 4 个 RPC | iamai06 | ❌ proto 缺失coord 裁决 P2 补全 |
| core-edu 补 AttendanceService + GetClassesByTeacher | 1 个 service + 1 个 RPC | core-eduai08 | ❌ proto 缺失coord 裁决 P3 补全 |
| data-ana 补 GetTeacherDashboardStats | 1 个 RPC | data-anaai11 | ❌ proto 缺失,提请 coord 仲裁 |
| GraphQL vs REST 仲裁 | P2 即 GraphQLpending-featuresvs P2-P3 REST、P4+ GraphQLcoord 裁决) | coord | ⚠️ 表述冲突,需仲裁 |
| core-edu 端口 3004 / gRPC 50053 | 不冲突 | 全局端口矩阵 | 待 coord 核对 |
---
## 下一步(阶段 2 入口)
待 coord 审核本确认书通过后ai03 进入阶段 2按 [ai-allocation.md §5 ai03 设计重点](../../../docs/architecture/ai-allocation.md#ai03--教学场景域) 产出模块架构设计文档 [02-architecture-design.md](./02-architecture-design.md)
- **teacher-bff 模块架构设计**目标态架构GraphQL Yoga + DataLoader + gRPC + Redis 缓存 + SSE 流式透传)+ 分阶段演进路线P2-P6+ 视口推导 + DataScope 透传 + 并行 gRPC 编排与降级策略
阶段 2 设计需先解决上述 8 项差距与跨模块契约对齐。
---
**AI Agent**: ai03 (teacher-bff)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式(直接 push main

File diff suppressed because it is too large Load Diff