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:
384
services/ai/docs/01-understanding.md
Normal file
384
services/ai/docs/01-understanding.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# 模块理解确认书 — ai
|
||||
|
||||
> AI 标识:ai12(复核与长期演进接手)← ai06(初稿,2026-07-09)
|
||||
> 负责模块:ai(P5)— 按 [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-09(ai06 初稿)/ 2026-07-09(ai12 复核与补充)
|
||||
> 关联文档:[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 §4,ai12 必读 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.1:BFF → ai,P5 起转 gRPC)
|
||||
- **ai12 增补**:student-bff(P3+)/ parent-bff(P4+)未来可能调用 AI 服务用于学生自适应学习、家长学情报告 AI 解读
|
||||
- **下游**:
|
||||
- content 服务(gRPC 查询知识点 / 题库,004 §4.1:AI → Content gRPC)
|
||||
- data-ana 服务(gRPC 查询学情数据,004 §4.1:AI → DataAna gRPC)
|
||||
- LLM Provider(外部 HTTP,OpenAI 兼容 REST API)
|
||||
- **ai12 增补**:iam 服务(gRPC 查询 `GetEffectiveDataScope`,用于多租户/多学校用量配额校验;当前 iam.proto 未提供此 RPC,coord 已仲裁 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` 流式 RPC),004 §4.2 明确 P5 启用
|
||||
- 出口:gRPC 调 content / data-ana(**当前未实现,仅 LLM HTTP 调用**);SSE 流式对前端;Kafka 事件外发用量计费
|
||||
- **端口**:3008(HTTP)+ 50058(gRPC,**ai12 增补**:004 §1.2 gRPC 端口规则 `HTTP+10050`,需在 §1.2 端口矩阵补登,提请 coord 同步)
|
||||
|
||||
### 1.2 服务边界与无状态原则(ai12 增补)
|
||||
|
||||
ai 是**严格无状态服务**,这与 data-ana 的"无 MySQL 写但有 ClickHouse 读模型"形成鲜明对比。无状态原则的含义与边界:
|
||||
|
||||
| 维度 | ai(无状态) | data-ana(有读模型) |
|
||||
| ---------- | ----------------------------------------------------- | ----------------------------------------- |
|
||||
| 持久化存储 | 无独占 DB | ClickHouse 宽表(读模型) |
|
||||
| 会话状态 | 单次请求内(无跨请求会话,聊天历史由前端维护) | 无 |
|
||||
| 缓存 | Redis(Prompt 模板、限流计数、响应缓存) | Redis(DataScope 缓存) |
|
||||
| 事件发布 | `AIUsageRecorded`(派生数据,豁免 Outbox,004 §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 + Gateway,ai 信任 `x-user-id`/`x-user-roles` 头)
|
||||
- **ai12 增补**:不负责教学大纲管理(→ content 服务的 Textbook/Chapter/KnowledgePoint)
|
||||
- **ai12 增补**:不负责学生作业批改(→ core-edu,ai 仅生成题目与解析,不批改学生作答)
|
||||
- **数据自治**:**无独占数据库**(设计上无状态;用量计费通过 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 message(ai06 初稿 + 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` BaseSettings(env_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 演进,规划如下技术栈演进:
|
||||
|
||||
| 类别 | 当前 | P5(M14-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 复用 content;P6+ 评估独立向量库支持语义检索 |
|
||||
| 模板管理 | 硬编码 | YAML 文件 + Jinja2 | + DB 持久化 + 版本化 + A/B 测试 | P5 用文件简单;P6+ 评估迁移 DB 支持动态更新 |
|
||||
| 缓存 | 无 | Redis(hash + 令牌桶) | + 语义缓存(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 surface;OpenAI 兼容 REST API 已成为事实标准(百川/Ollama/vLLM 均兼容)
|
||||
2. **P5 不引入 LangChain**:LangChain 抽象过重,与 LLMProvider 抽象层职责重叠;按 004 §2.3 列出的 LangGraph 待 P6+ 评估时再决定
|
||||
3. **P5 不引入向量库**:RAG 检索复用 content 服务的 Neo4j + ES(content 已规划 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 key(OpenAI / Anthropic / 百川 / 本地模型 Ollama)
|
||||
- **下游依赖我**:
|
||||
- P5 teacher-bff 聚合 AI 出题 mutation(004 §9.3:教师用 AI 出题并发布到班级)
|
||||
- P5 teacher-portal SSE 流式 AI 对话(前端)
|
||||
- **ai12 增补**:P5+ student-bff(学生自适应学习,未来阶段)
|
||||
- **ai12 增补**:P5+ parent-bff(家长学情报告 AI 解读,未来阶段)
|
||||
|
||||
### 5.2 ai12 增补:阶段交付里程碑细分
|
||||
|
||||
将 P5(M14-M16,3 个月)细分为 3 个里程碑,便于优先级排序:
|
||||
|
||||
| 里程碑 | 时间 | 交付内容 | 退出标准 |
|
||||
| ------ | ----- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| M14 | 第1月 | LLMProvider 抽象 + OpenAI/百川/Ollama 适配器 + gRPC server(Chat/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 无显式 drain(LLM 流式请求需等待完成)
|
||||
- [ ] 测试覆盖率 ≥ 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 预算,超额拒绝调用(避免单一学校耗尽预算)
|
||||
|
||||
---
|
||||
|
||||
## 服务审计表 — ai12(ai 部分)
|
||||
|
||||
> 对照 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 | ✅ OTel(dev 跳过) | ✅ | ✅(含 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.2;events.proto 待补 `AIUsageEvent` message(coord 负责) |
|
||||
| 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 50058(HTTP+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 schema;ai12 已在 02-architecture-design.md §5.2 给出建议 schema |
|
||||
| A4 | **iam `GetEffectiveDataScope` gRPC RPC**:ai 需查询用户 DataScope 用于多租户配额校验 | ai + coord + iam | coord 已仲裁 P4 补全(§15.3 #5),ai12 在 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
|
||||
1269
services/ai/docs/02-architecture-design.md
Normal file
1269
services/ai/docs/02-architecture-design.md
Normal file
File diff suppressed because it is too large
Load Diff
198
services/classes/docs/01-understanding.md
Normal file
198
services/classes/docs/01-understanding.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 模块理解确认书 — classes
|
||||
|
||||
> AI:ai07(TS / 教学组织 · 黄金模板)
|
||||
> 阶段:阶段 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/Gin):HTTP 反向代理 `/api/v1/classes/*` → `http://classes:3001/classes/*`(Gateway 已注册 classes 路由块,含精确路径 + 通配路径)
|
||||
- `teacher-bff`(NestJS):P2 起通过 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)
|
||||
- 出口:MySQL(Drizzle ORM)
|
||||
- 演进:proto 已定义 `ClassService`(5 个 RPC),P3 起 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.1b):classes 独占此领域(当前),P3 起与 subjects、enrollment 共同组成 D2
|
||||
- 上游依赖:无业务领域依赖(教学组织是基础数据,不依赖其他业务领域)
|
||||
- 下游被依赖:
|
||||
- core-edu D3 教学核心域(考试/作业按班级归属)
|
||||
- data-ana D6 智能洞察域(班级维度统计聚合)
|
||||
- msg D5 沟通通知域(班级群组广播)
|
||||
|
||||
### 2.3 我不负责什么(边界外)
|
||||
|
||||
- ❌ 学科(subjects)→ core-edu(P3 新增,D2 教学组织)
|
||||
- ❌ 选课 / 学生分班(enrollment)→ core-edu(P3 新增)
|
||||
- ❌ 考试 / 作业 / 成绩 → core-edu(D3 教学核心)
|
||||
- ❌ 教材 / 知识点 / 题库 → content(D4 内容资源)
|
||||
- ❌ 站内信 / 通知投递 → msg(D5 沟通通知)
|
||||
- ❌ 学情分析 / 掌握度计算 → data-ana(D6 智能洞察)
|
||||
- ❌ 用户 / 角色 / 权限管理 → iam(D1 身份认证)
|
||||
- ❌ JWT 校验 → api-gateway(classes 仅读 `x-user-id` / `x-user-roles` 头)
|
||||
- ❌ 年级(grade)数据 → 当前 classes 仅持有 `gradeId` 外键引用,年级实体归属待 core-edu P3 定义(当前由前端/管理端维护)
|
||||
|
||||
---
|
||||
|
||||
## 3. 我与外部的契约
|
||||
|
||||
### 3.1 我消费的 proto message(从 shared-proto)
|
||||
|
||||
- 当前**不消费**任何外部 proto(classes 是基础数据源,不反向依赖业务服务)
|
||||
- 未来 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}` | 删除(先校验存在) |
|
||||
|
||||
> **响应信封**:遵循 ActionState(004 §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 50053(004 §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 8(mysql2 驱动) | 连接池 `connectionLimit: 10` |
|
||||
| 校验 | Zod 3.23 | 输入 DTO 校验,controller 层 `schema.parse(body)` |
|
||||
| 日志 | pino 9 + pino-pretty(dev) | 结构化 JSON,`base:{service,version}` |
|
||||
| 指标 | prom-client 15 | 自定义 Counter/Histogram + 默认进程指标,`/metrics` 端点 |
|
||||
| 链路 | OpenTelemetry SDK + OTLP exporter | auto-instrumentations,serviceName=`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 P1):classes 域 CRUD 端到端跑通 + 全横切关注点落地 + 30 分钟可复制新服务
|
||||
- **依赖上游阶段产出**:无(P1 是地基,classes 是最早交付的服务)
|
||||
- **未来演进路径**:
|
||||
- P3:合并入 core-edu(ai08 负责),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 类错误码均带前缀 | — |
|
||||
| logger(pino) | ✅ | [logger.ts](../src/shared/observability/logger.ts) | — |
|
||||
| metrics(prom-client) | ✅ | [metrics.ts](../src/shared/observability/metrics.ts) + `/metrics` 端点 | — |
|
||||
| tracer(OTel) | ⚠️ | [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 字段类型 |
|
||||
709
services/classes/docs/02-architecture-design.md
Normal file
709
services/classes/docs/02-architecture-design.md
Normal file
@@ -0,0 +1,709 @@
|
||||
# 模块架构设计文档 — classes
|
||||
|
||||
> AI:ai07(TS / 教学组织 · 黄金模板)
|
||||
> 阶段:阶段 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 ms(Drizzle 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 模式。
|
||||
|
||||
| 事件 | 触发时机 | Topic(004 §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; // 幂等去重 ID(cuid2)
|
||||
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 幂等性设计
|
||||
|
||||
- **Producer(Outbox Relay)**:Kafka `idempotent=true` + `transactionalId=classes-outbox-relay`
|
||||
- **Consumer**:基于 `event_id` 去重(DB 唯一索引 `idx_outbox_event_id` 或 Redis SETNX,TTL 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_PERMISSIONS(project_memory 已记录该约束)。
|
||||
|
||||
### 6.2 错误码清单
|
||||
|
||||
见 [§4.3](#43-错误响应actionstate-失败信封)。前缀 `CLASSES_`(004 §11.4 确认保留,P3 合并入 core-edu 后保留历史遗留前缀)。
|
||||
|
||||
### 6.3 Logger
|
||||
|
||||
| 项 | 配置 |
|
||||
| --------- | ------------------------------------------------------------- |
|
||||
| 库 | pino 9 + pino-pretty(dev) |
|
||||
| 初始化 | [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 collectDefaultMetrics(CPU/内存/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 | HTTP(P2)/ gRPC(P3) | `GET /classes` 或 `ClassService.ListClasses` | BFF 聚合班级列表 | P2 |
|
||||
| 调用 | MySQL | TCP | mysql2 连接池 | 数据读写 | P1 ✅ |
|
||||
| 调用 | Redis | TCP(P3) | ioredis | 班级列表缓存 | P3 |
|
||||
| 发布 | — | Kafka(P3) | `edu.org.class.created/updated/deleted/transferred` | 领域事件 | P3 |
|
||||
| 消费 | iam | Kafka(P3) | `edu.identity.user.deleted` | 班主任置空 | P3 |
|
||||
| 被调用 | core-edu | gRPC(P3) | `ClassService.GetClassesByTeacher` | 教师所属班级查询 | P3 |
|
||||
|
||||
> **P3 合并边界**:classes 合并入 core-edu 后,"被调用"方从 classes 改为 core-edu,端口从 3001 改为 3004,gRPC 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 层保持薄,业务逻辑在 Service,gRPC 实现可直接复用 Service
|
||||
- 响应信封 ActionState 在 gRPC 侧用 metadata 传递 error(gRPC 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 WHERE;dataScope 从 `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% 覆盖率 + 集成测试 + 契约测试。
|
||||
|
||||
| 测试类型 | 范围 | 工具 | 目标覆盖率 |
|
||||
| -------- | -------------------------------------------- | ----------------------- | ---------- |
|
||||
| 单元测试 | Service(mock repo) | Vitest | 80% |
|
||||
| 集成测试 | Controller + Service + Repository(test 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)抽离为 yaml,P6 接入 Consul 热更新
|
||||
|
||||
---
|
||||
|
||||
## 10. 黄金模板对照 checklist(供其他 TS 服务自检)
|
||||
|
||||
> 本 checklist 是 classes 作为黄金模板的核心产出,供 ai01-ai10 的 TS 服务在实现前自检对齐。
|
||||
|
||||
### 10.1 目录结构
|
||||
|
||||
```
|
||||
services/<service>/src/
|
||||
├─ <domain>/ # 限界上下文
|
||||
│ ├─ <domain>.controller.ts # Controller(HTTP/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 里程碑
|
||||
|
||||
| 里程碑 | 交付物 | 验收标准 |
|
||||
| ------------- | ----------------------- | ------------------------------------------------- |
|
||||
| M1(P1) | 黄金模板整改完成 | 6 项 P1 整改全部完成 + lint/typecheck/test 零错误 |
|
||||
| M2(P1) | 黄金模板 checklist 产出 | 本文档 §10 + 其他 AI 自检通过 |
|
||||
| M3(P3 交接) | 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 服务的强制对齐基准。
|
||||
207
services/content/docs/01-understanding.md
Normal file
207
services/content/docs/01-understanding.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# 模块理解确认书 — content
|
||||
|
||||
> AI 标识:ai05(初稿)→ ai09(复核与阶段 2 接手)
|
||||
> 负责模块:content(P4)— 按 [ai-allocation.md §3.2](../../../docs/architecture/ai-allocation.md) 最新分配,content 由 ai09 专责
|
||||
> 阶段:架构设计外包 · 阶段 1(全局理解,ai05 初稿)+ ai09 复核
|
||||
> 日期:2026-07-09(初稿)/ 2026-07-09(ai09 复核)
|
||||
> 关联文档:[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. 我在架构中的位置
|
||||
|
||||
- **层级**:业务微服务层(L5),DDD 限界上下文
|
||||
- **业务领域**:D4 内容资源领域(004 §1.1b)
|
||||
- **上游**(谁调用我):
|
||||
- teacher-bff(3003):教学场景聚合,教师查教材/知识点/题库
|
||||
- student-bff(学习场景,P3 起):学生查学习路径、知识点前置
|
||||
- ai(Python,P5):gRPC 查询知识点/题库用于 AI 出题(004 §4.1 `AI -.gRPC.-> Content`,§9.3 AI 辅助出题流程)
|
||||
- **下游**(我调用谁):
|
||||
- MySQL(写模型主库,独占)
|
||||
- Neo4j(知识图谱,前置依赖图)
|
||||
- Elasticsearch(题库全文检索,P4 后续补充,当前未实现)
|
||||
- **通信方式**:
|
||||
- 当前:HTTP REST(Controller,无 gRPC controller)
|
||||
- 目标态(004 §4.1 / pending-features P4):gRPC 暴露 `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/`:教材 CRUD(5 端点)
|
||||
- `chapters/`:章节 CRUD(5 端点,按 textbook 查询)
|
||||
- `knowledge-points/`:知识点 CRUD + Neo4j 知识图谱(7 端点,含前置链路查询/添加)
|
||||
- `questions/`:题库 CRUD(5 端点,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
|
||||
- **当前实现均为 REST,gRPC 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.6(ESM 模式,相对 import 带 `.js` 后缀)
|
||||
- 框架:NestJS 10
|
||||
- ORM:Drizzle ORM 0.31 + mysql2 3.11
|
||||
- 知识图谱:neo4j-driver 5.23(PREREQUISITE_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 schema:textbooks / 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] 优雅关闭 SIGTERM(main.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 仅 CRUD,P5 引入 ES" | 确认 content 的 ES 检索归属 P4 还是 P5(004 §2.2 列 ES 使用者为 Content、AI) |
|
||||
| C2 | **无 Outbox / Kafka**:骨架无 shared/outbox/,无 Kafka producer/consumer | 004 §7.2 列 `edu.content.question.published` 事件需发布 | 确认 content 是否需 Outbox(iam 无 Outbox,core-edu 有) |
|
||||
| C3 | **README 与实现脱节**:README 声称 `TextbooksService.createKnowledgeGraph` 和 `getPrerequisites`,源码中 createKnowledgeGraph 不存在,getPrerequisites 在 KnowledgePointsService | 文档误导 | 阶段 2 设计需同步修正 README |
|
||||
| C4 | **gRPC 未实现**:proto 定义了 TextbookService/KnowledgeGraphService,但无 gRPC controller | pending-features P4 未强制 gRPC,004 §4.1 目标态 gRPC | 确认 P4 是否启用 gRPC(ai03 提请统一决策) |
|
||||
| C5 | **schema 定义分散**:textbooks.schema.ts 定义 3 张表(textbooks/chapters/knowledgePoints),chapters/knowledge-points schema 仅 re-export;questions.schema.ts 独立 | 维护成本 | 阶段 2 设计统一 schema 归属 |
|
||||
| C6 | **DB 连接模式与 iam 不一致**:content 用模块级 `const db`,iam 用 `getDb()` 函数式懒加载 | 测试 mock 困难 | coord 已在 known-issues 记录"db 常量导出对齐黄金模板",需确认统一方向 |
|
||||
| C7 | **表时间戳不统一**:textbooks/questions 有 created_at+updated_at,chapters/knowledge_points 无时间戳 | 审计追踪缺失 | 阶段 2 设计补齐 |
|
||||
| C8 | **无外键约束**:questions.knowledge_point_id → knowledge_points.id 等无 Drizzle 外键 | 引用完整性靠应用层 | 阶段 2 设计评估是否加外键 |
|
||||
| C9 | **addPrerequisite 降级策略不一致**:safeCreateNode 非阻塞(Neo4j 失败仅 warn),addPrerequisite 在 Neo4j 不可用时抛 InternalError | 行为不一致 | 阶段 2 设计统一降级策略 |
|
||||
| C10 | **proto 包名**:实际 `next_edu_cloud.content.v1`,project_rules §5 规定 `edu.content.v1` | 命名规范不一致 | **coord 仲裁**(ai03 已提请) |
|
||||
|
||||
---
|
||||
|
||||
## 服务审计表 — ai05(content 部分)
|
||||
|
||||
> 审计标准对照 [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 检索归属**:P4(pending-features 注明"P4 仅 CRUD,P5 引入 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 | **与 ai12(ai 服务)的 gRPC 接口未细化** | ai-allocation §5 明确要求"查询知识点/题库用于 AI 出题" + 004 §9.3 AI 辅助出题流程 | 契约设计补 `QuestionBankService.ListQuestions`/`SearchQuestions`/`BatchCreateQuestions` |
|
||||
| L5 | **ES 索引 mapping 完整未设计** | ai-allocation §5 明确要求"ES 索引 mapping 设计(题库全文检索 + 标签过滤 + 难度/年级 facet)" | 阶段 2 完整设计 mapping,但实现归属 P5(pending-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
|
||||
944
services/content/docs/02-architecture-design.md
Normal file
944
services/content/docs/02-architecture-design.md
Normal file
@@ -0,0 +1,944 @@
|
||||
# 模块架构设计文档 — content
|
||||
|
||||
> AI 标识:ai05
|
||||
> 负责模块:content(P4)
|
||||
> 阶段:架构设计外包 · 阶段 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_num,TS 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 v4(producer 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 健康检查
|
||||
|
||||
#### /healthz(liveness)
|
||||
|
||||
```json
|
||||
{ "status": "ok", "service": "content", "timestamp": 1736000000000 }
|
||||
```
|
||||
|
||||
不依赖任何外部资源,仅返回进程存活状态。
|
||||
|
||||
#### /readyz(readiness · 多依赖检查)
|
||||
|
||||
```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 | 教师检索题库 | P5(ES 引入后) |
|
||||
| 消费 | 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 RPC(CreateQuestion/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_num,TS 字段名 order | ✅ 已实现 | 仅记录 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 演进路线
|
||||
|
||||
### 8.1 P4 阶段(当前)
|
||||
|
||||
**目标**:CRUD + 知识图谱 + Outbox + Kafka 事件发布
|
||||
|
||||
**交付物**:
|
||||
|
||||
- 4 张 MySQL 表(含补齐的时间戳、新增 status/source/created_by/metadata 字段)
|
||||
- content_outbox_events 表 + Outbox Publisher worker
|
||||
- Kafka producer(idempotent + 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
|
||||
- 题库全文检索 API(GET /questions/search)
|
||||
- 检索延迟 < 200ms(P5 退出标准)
|
||||
- 测试覆盖率 ≥ 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 还是 P5?(pending-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 producer(idempotent + 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)
|
||||
194
services/core-edu/docs/01-understanding.md
Normal file
194
services/core-edu/docs/01-understanding.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# 模块理解确认书 — core-edu
|
||||
|
||||
> AI 标识:ai08(按 [ai-allocation.md §3.2](../../../docs/architecture/ai-allocation.md) 接管,原 ai03 阶段 1 文档已归位)
|
||||
> 负责模块:core-edu(P3)
|
||||
> 阶段:架构设计外包 · 阶段 1(全局理解)· ai08 审计补全版
|
||||
> 日期:2026-07-09(初稿)/ 2026-07-09(ai08 审计补全)
|
||||
> 关联文档:[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-bff(P2 已 REST fetch 调用 `/exams` `/homework` `/grades`)
|
||||
- student-bff(P3 依赖,作答/查成绩)
|
||||
- parent-bff(P4 依赖,查看子女成绩/考勤)
|
||||
- api-gateway(直接路由 `/api/v1/exams` 等,绕过 BFF 的内部直连场景)
|
||||
- **下游**:MySQL(独占库 `core_edu_*` 表前缀)、Kafka(Outbox 事件发布)、Redis(P3 引入:作业高并发提交锁 + 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)):
|
||||
- HTTP:3004(P3)
|
||||
- gRPC:50053(P3 启用)
|
||||
- /metrics:随 HTTP 3004(Prometheus 抓取)
|
||||
|
||||
## 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、Attendance(P3 新增)
|
||||
- **我不负责**:
|
||||
- 不负责题库内容(→ 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 查题库)
|
||||
- 不负责权限/角色管理(→ iam,core-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 整改清单 #14,P3 补全,[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
|
||||
- ORM:Drizzle ORM(mysql2 driver,直接 `db` 导出,**与 classes 的 `getDb()` 不一致**,需 P3 统一)
|
||||
- 存储:MySQL 8(独占库 `core_edu_*` 表前缀)、Redis(P3 引入)、Kafka(kafkajs,idempotent + 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 schema:exams / exam_questions / homework_assignments / homework_submissions / homework_answers / grade_records / outbox_events(**当前 schema 仅 4 表**,缺 exam_questions / homework_submissions / homework_answers,P3 必须补全)
|
||||
3. Outbox 模式 + Outbox relay worker(**当前 relay 在 NestJS 进程内 setInterval 5s,pending-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-bff(P3)、parent-bff(P4,查看子女成绩/考勤)、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] 优雅关闭 SIGTERM(main.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 启用,端口 50053,proto 已定义)
|
||||
- [ ] **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.*" 规则)
|
||||
|
||||
---
|
||||
|
||||
## 服务审计表 — ai08(core-edu)
|
||||
|
||||
> 对照 [黄金模板 classes 服务](../../classes/src/),审计已实现的 core-edu 服务。状态:✅ 达标 / ⚠️ 部分 / ❌ 缺失
|
||||
|
||||
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile | gRPC server | ActionState |
|
||||
| -------- | -------------------------------------- | --------------- | -------------------------------- | --------------------- | ------------ | -------- | ------------------------------------- | -------- | ---------- | ---------- | ---------------- | ----------- |
|
||||
| core-edu | ✅ `@RequirePermission(EXAM_*)` 全覆盖 | ✅ `CORE_EDU_*` | ⚠️ pino(kafka.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. ⚠️ 入口仍为 REST,proto 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 整改 #14,P3 必须补全)
|
||||
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)
|
||||
1290
services/core-edu/docs/02-architecture-design.md
Normal file
1290
services/core-edu/docs/02-architecture-design.md
Normal file
File diff suppressed because it is too large
Load Diff
232
services/data-ana/docs/01-understanding.md
Normal file
232
services/data-ana/docs/01-understanding.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 模块理解确认书 — data-ana
|
||||
|
||||
> AI 标识:ai11(v2 由 ai11 审核 ai06 v1 后修订)
|
||||
> 负责模块:data-ana(P4)
|
||||
> 语言:Python 3.12+ / FastAPI 0.115+
|
||||
> 阶段:架构设计外包 · 阶段 1(全局理解)v2 审核版
|
||||
> 日期:2026-07-09(v1 by ai06)/ 2026-07-10(v2 审核修订 by ai11)
|
||||
> 关联文档:[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[known-issues.md](../../../docs/troubleshooting/known-issues.md)
|
||||
> 审核修订说明:ai-allocation.md §3.2 将 data-ana 重新分配给 ai11(ai 单独分配给 ai12),ai11 接手后对 ai06 v1 进行审核,本版为 v2 修订。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1 必读清单完成确认
|
||||
|
||||
按 ai-allocation.md §4,ai11 必读 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.2:BFF → 业务服务 gRPC,**P4 启用 gRPC**)
|
||||
- ai 服务通过 gRPC 反查学情数据(004 §4.1:AI → DataAna gRPC,AI 出题编排时调 `GetStudentWeakness` / `GetLearningTrend`)
|
||||
- **下游**:
|
||||
- ClickHouse(独占读模型,宽表 `student_dashboard_view` / `student_errors` / `mastery_snapshot` / `ai_usage_log`)
|
||||
- Redis(DataScope 缓存 5min + 预警去重位图 + CDC 幂等去重 SETNX,004 §6.3 缓存策略矩阵)
|
||||
- Kafka(消费 Debezium CDC 事件 + 发布 `edu.insight.mastery.updated` 派生数据事件)
|
||||
- iam gRPC(调 `GetEffectiveDataScope` 解析数据范围,已仲裁 P4 补全,见 004 §15.3 #5)
|
||||
- **通信方式**:
|
||||
- 入口: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 写 MySQL,data-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 统一响应信封 ActionState(Python 服务必须改 ActionState,degraded 作为 `details.degraded` 子字段)。阶段 2 设计已对齐(见 02-architecture-design.md §4.3)。
|
||||
|
||||
**gRPC 契约**(analytics.proto,P4 启用 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 透传)
|
||||
|
||||
**发布的领域事件**:
|
||||
|
||||
| 事件 | Topic(004 §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` 直接发布(已豁免 Outbox,004 §15.3 #6 仲裁结论),下游 core-edu / msg 消费。失败重试 3 次仍失败落 `mastery_publish_failed` 本地表。
|
||||
|
||||
- **错误码前缀**:`DATA_ANA_*`(004 §11.4 错误码前缀矩阵已登记,清单见阶段 2 §6.2)
|
||||
- **缓存**:Redis(DataScope 缓存 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` BaseSettings(env_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(待引入)
|
||||
- 序列化:JSON(Debezium 事件 `schemas.enable=false`,直接 `json.loads`)+ protobuf(gRPC)
|
||||
- 测试:pytest + pytest-asyncio + Testcontainers(ClickHouse + 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 P4):DataAna 学情诊断宽表 5s 内返回 + CDC 链路延迟 < 5s + **双轨读策略落地(实时查主库 + 聚合查宽表)** + CDC 模式回写黄金模板 README + 打 tag `v0.4.0-p4`
|
||||
- **依赖上游**:
|
||||
- P1 地基:api-gateway 路由 + arch.db 扫描器 Python 支持
|
||||
- P2 身份:iam `GetEffectiveDataScope` gRPC(已仲裁 P4 补全,见 004 §15.3 #5)
|
||||
- P3 核心教学:core-edu 写成绩到 MySQL(Debezium 监听 binlog)+ Outbox 领域事件(备通道)
|
||||
- P4 同期:content 服务(提供知识点 ID 供掌握度计算,content → data-ana 事件流见 004 §4 服务依赖图)
|
||||
- **下游依赖我**:
|
||||
- P5 ai 服务通过 gRPC 查询学情数据(004 §4.1:AI → DataAna gRPC)+ 投递 `edu.insight.ai.usage` 用量事件
|
||||
- P5 msg 服务消费 `mastery.updated` 触发预警
|
||||
- **未来阶段演进铺垫**(长远性):
|
||||
- P5:消费 `edu.insight.ai.usage` 落 ai_usage_log;gRPC `SubscribeMasteryUpdate` stream RPC 供 BFF SSE 透传
|
||||
- P6:CDC 消费者水平扩展(多实例 + 分区重平衡 + 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 + Testcontainers(ClickHouse + 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 def,aiokafka 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)
|
||||
|
||||
---
|
||||
|
||||
## 服务审计表 — ai11(data-ana,v2 修订)
|
||||
|
||||
> 对照 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}` 必须改为 ActionState,degraded 作为 `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` 事件未发布(已豁免 Outbox,004 §15.3 #6 仲裁),下游 core-edu/msg 无法消费。需设计掌握度计算 + Kafka producer 直接发布链路
|
||||
5. **P1 ClickHouse schema 不规范**:当前 `student_dashboard_view` 实为 MergeTree 表,应为 ReplacingMergeTree(last_updated);查询未加 FINAL/argMax 可能读重复版本。无 DDL 文件管理。阶段 2 需产出完整 ClickHouse DDL(5 张宽表:考试/作业/成绩/掌握度/出勤,对应 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 #5:iam 新增 `GetEffectiveDataScope` gRPC RPC(P4 补全);data-ana 在 ClickHouse 查询 SQL 拼接时注入 WHERE | ✅ v2 已对齐:§3 列出 iam.proto 调用契约;阶段 2 §6.1 设计 `inject_data_scope` Depends |
|
||||
| 5 | **data-ana ClickHouse DDL 管理位置** | §15.3 #7:采纳 `infra/clickhouse/ddl/`(coord 建立),data-ana 提供 DDL 内容 | ✅ v2 已对齐:阶段 2 §3 标注"DDL 文件由 coord 统一管理在 `infra/clickhouse/ddl/`" |
|
||||
| 6 | **新增 `edu.insight.ai.usage` topic**(v1 阶段 2 §8.3 #3 提请) | §15.3 #4:补登,见 004 §7.2 | ✅ v2 已对齐:阶段 2 §5.1 列出消费此 topic |
|
||||
| 7 | **iam GetEffectiveDataScope proto 新增**(v1 阶段 2 §8.3 #2 提请) | §15.3 #5:P4 补全 | ✅ v2 已对齐:§3 列出 iam.proto 调用契约 |
|
||||
|
||||
> 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 + ai,ai-allocation §3.2 v2 重新分配后 data-ana → ai11 / ai → ai12)
|
||||
979
services/data-ana/docs/02-architecture-design.md
Normal file
979
services/data-ana/docs/02-architecture-design.md
Normal file
@@ -0,0 +1,979 @@
|
||||
# 模块架构设计文档 — data-ana
|
||||
|
||||
> AI 标识:ai11(v2 由 ai11 审核 ai06 v1 后修订)
|
||||
> 负责模块:data-ana(P4)
|
||||
> 阶段:架构设计外包 · 阶段 2(模块架构设计)v2 审核版
|
||||
> 日期:2026-07-09(v1 by ai06)/ 2026-07-10(v2 审核修订 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.3;v2 在 v1 基础上对齐已仲裁约束 + 补强长远性/全面性
|
||||
> v2 修订要点:(1) 身份归属 ai06 → ai11;(2) 响应信封对齐 ActionState(004 §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` 派生数据事件(豁免 Outbox,004 §12.2 + §15.3 #6 已仲裁)
|
||||
4. **gRPC 优先**:004 §4.1 + §4.2 明确 BFF → 业务服务走 gRPC,**P4 启用 data-ana gRPC server 端口 50055**(HTTP 保留作 Gateway 直连降级)
|
||||
5. **DataScope 过滤**:004 §5.3 DataScope 6 级在查询层注入 WHERE;iam `GetEffectiveDataScope` gRPC(004 §15.3 #5 已仲裁 P4 补全)
|
||||
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. **长远架构演进**:为 P5(ai 用量消费 / gRPC stream)、P6(CDC 水平扩展 / 容量规划 / 数据治理 / 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 缓存 5min(004 §6.3) |
|
||||
|
||||
> **双轨读边界**(004 §1.1a):BFF 实时查主库走 core-edu,data-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}` | readiness(v2 补 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 CDC(core-edu MySQL) | Debezium JSON(before/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 ExamCache(exam_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 | JSON(UsageRecord) | 落 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 天)
|
||||
- 消费者 offset:v1 用 `enable_auto_commit=True` 简化;**v2 改进**:手动 commit(at-least-once),commit 前确保 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 实例消费所有 topic,consumer group = `data-ana-cdc`
|
||||
- ExamCache 内存 LRU(max 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 requests(uvicorn 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) |
|
||||
| 被调用 | ai(P5+) | gRPC | `AnalyticsService.GetStudentWeakness / GetLearningTrend / SubscribeMasteryUpdate` | AI 个性化出题上下文 + 实时掌握度推送 |
|
||||
| 调用 | iam | gRPC | `IamService.GetEffectiveDataScope`(004 §15.3 #5 已仲裁 P4 补全) | DataScope 解析 |
|
||||
| 消费 | core-edu(CDC) | Kafka | `edu-cdc.next_edu_cloud.core_edu_grades/exams/homework_submissions/attendance` | 学情 + 考勤数据投递 |
|
||||
| 消费 | core-edu(CDC) | Kafka | `edu-cdc.next_edu_cloud.classes` | 班级维度同步 |
|
||||
| 消费 | iam(CDC) | Kafka | `edu-cdc.next_edu_cloud.iam_users` | 用户 dataScope 同步 |
|
||||
| 消费 | content(CDC) | Kafka | `edu-cdc.next_edu_cloud.content_knowledge_points` | 知识点元数据同步(v2 新增) |
|
||||
| 消费 | ai(P5+) | Kafka | `edu.insight.ai.usage`(004 §15.3 #6 已仲裁,004 §7.2 已登记) | AI 用量落库 |
|
||||
| 发布 | core-edu / msg | Kafka | `edu.insight.mastery.updated`(004 §12.2 + §15.3 #6 已仲裁:派生数据豁免 Outbox) | 掌握度更新通知 |
|
||||
| 发布 | 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 API(004 §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` topic(004 §15.3 #6 已仲裁)
|
||||
|
||||
### 8.2 技术风险
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
| --------------------------- | ------------------------------------ | ------------------------------------------------------------ |
|
||||
| ClickHouse 查询延迟超 5s | 违反 P4 退出标准 | 宽表索引优化 + 物化视图预聚合 + 查询超时 3s 降级 |
|
||||
| CDC 消费者 lag 过大 | 学情数据延迟 > 5s | 监控 lag + 告警 + 水平扩展消费者(分区数提升) |
|
||||
| ExamCache 内存泄漏 | 长期运行 OOM | LRU 淘汰策略(max 10000 条)+ 定期清理过期 exam;P6 迁 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)是否合规 | ✅ 已仲裁:派生数据事件豁免 Outbox(004 §12.2 + §15.3 #6) | 004 §12.2 |
|
||||
| 2 | 新增 `edu.insight.ai.usage` topic + `AIUsageEvent` proto message | ✅ 已仲裁:coord 在 004 §7.2 登记 + events.proto 补 message | 004 §7.2 + events.proto |
|
||||
| 3 | iam 新增 `GetEffectiveDataScope` gRPC RPC | ✅ 已仲裁:P4 补全(004 §15.3 #5) | iam.proto |
|
||||
| 4 | data-ana 实现 gRPC server 决策 | ✅ 已仲裁:P4 启用(004 §4.2 gRPC 启用阶段矩阵) | 004 §4.2 |
|
||||
| 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 | ExamCache(P6 多实例共享) | 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_tokens,prompt 内容不入库(ai 服务侧处理)
|
||||
|
||||
## 13. 测试策略(v2 新增)
|
||||
|
||||
### 13.1 测试金字塔
|
||||
|
||||
| 层级 | 工具 | 覆盖范围 | 目标覆盖率 |
|
||||
| -------- | ----------------------- | -------------------------------------------- | ---------- |
|
||||
| 单元测试 | pytest + pytest-asyncio | 算法(掌握度计算/预警阈值)、Pydantic 模型 | ≥ 80% |
|
||||
| 集成测试 | pytest + Testcontainers | ClickHouse DDL + 查询、Kafka producer、Redis | ≥ 60% |
|
||||
| 契约测试 | pact-python(v2 评估) | 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/...`(当前省略 v1,v2 起强制)
|
||||
- 破坏性变更:新增 `/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 + v2(6 个月过渡)
|
||||
- 非破坏性变更:直接加字段,schema_version 不变
|
||||
|
||||
## 17. Projection 演进路径(v2 新增)
|
||||
|
||||
### 17.1 当前(P4):CDC 直连
|
||||
|
||||
```
|
||||
MySQL binlog → Debezium → Kafka (edu-cdc.*) → data-ana CdcConsumer → ClickHouse
|
||||
```
|
||||
|
||||
- 优点:实时性好(< 5s),无需 core-edu 改造
|
||||
- 缺点:依赖 binlog schema,core-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 远期(P6):CDC + 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-streaming,AI 实时获取掌握度变化 |
|
||||
| 预警触发通知 | `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 注入 sidecar,mTLS 自动双向 |
|
||||
| 数据治理 | 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 RPC,v2 提案扩展 7 RPC |
|
||||
| 路由 | ✅ | HTTP 13 端点 + gRPC 10 RPC |
|
||||
| 数据访问 | ✅ | ClickHouse 5 宽表 + Redis 缓存 + iam gRPC |
|
||||
| 鉴权 | ✅ | FastAPI Depends 等价物 + 7 权限点 |
|
||||
| 错误处理 | ✅ | 13 错误码 `DATA_ANA_*` + ActionState 信封 |
|
||||
| 可观测 - 日志 | ✅ | structlog + JSONRenderer(v2 改进) |
|
||||
| 可观测 - 指标 | ✅ | 16 Prometheus 指标 |
|
||||
| 可观测 - 链路 | ✅ | OTel + gRPC interceptor + CDC span 注入 |
|
||||
| 健康检查 | ✅ | /healthz + /readyz(v2 补 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),文档进入实现阶段无阻塞
|
||||
191
services/msg/docs/01-understanding.md
Normal file
191
services/msg/docs/01-understanding.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 模块理解确认书 — msg
|
||||
|
||||
> AI 标识:ai05(初版)→ ai10(复核修订,按 ai-allocation.md §3.2 重新分配)
|
||||
> 负责模块:msg(P5)
|
||||
> 阶段:架构设计外包 · 阶段 1(全局理解)
|
||||
> 初版日期:2026-07-09
|
||||
> 复核修订日期:2026-07-09(ai10 接手后修正 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. 我在架构中的位置
|
||||
|
||||
- **层级**:业务微服务层(L5),DDD 限界上下文
|
||||
- **业务领域**:D5 沟通通知领域(004 §1.1b)
|
||||
- **上游**(谁调用我):
|
||||
- teacher-bff / student-bff / parent-bff:聚合层调 msg 查询用户通知列表、发送通知
|
||||
- api-gateway:REST 转发通知请求
|
||||
- Kafka:消费 core-edu / iam 事件触发通知(004 §4.1 `CoreEdu -.事件.-> Msg`、`IAM -.事件.-> Msg`)
|
||||
- **下游**(我调用谁):
|
||||
- MySQL(写模型主库,独占)
|
||||
- Elasticsearch(全文检索,已实现 safeSearch)
|
||||
- push-gateway(gRPC 推送通道,004 §4.1 `PushGW → Msg`,当前用 fetch POST /internal/push 降级)
|
||||
- **通信方式**:
|
||||
- 当前:HTTP REST(Controller,无 gRPC controller)
|
||||
- 目标态(004 §4.1 / pending-features P5):gRPC 暴露 `NotificationService`
|
||||
- Kafka:消费 core-edu(ExamPublished/HomeworkGraded/GradeRecorded)、iam(UserRegistered)事件(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` 的事件 message(ClassEvent/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
|
||||
- **当前实现均为 REST,gRPC 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.6(ESM 模式)
|
||||
- 框架:NestJS 10
|
||||
- ORM:Drizzle 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] 优雅关闭 SIGTERM(main.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-42,ai10 复核修正)
|
||||
- [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 是否启用 gRPC(ai03 提请统一决策) |
|
||||
| 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.*` 格式) | 无(命名正确) | ✅ 命名正确,无需处理 |
|
||||
|
||||
---
|
||||
|
||||
## 服务审计表 — ai10(msg 复核)
|
||||
|
||||
> 审计标准对照 [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 对 ai10(msg)明确列出 6 项设计重点,对照现有骨架:
|
||||
|
||||
| ai-allocation §5 设计重点 | 现状 | 阶段 2 是否需设计 |
|
||||
| --------------------------------------------------- | ------------------------------ | ----------------- |
|
||||
| 通知渠道抽象(站内信/邮件/短信,策略模式) | ❌ 无抽象,硬编码 | ✅ 必须 |
|
||||
| ES 降级查询策略(DB 不可用时走 ES 索引) | ❌ 仅 ES 降级,无 DB 降级到 ES | ✅ 必须 |
|
||||
| Kafka 消费幂等设计(event_id 去重 via Redis SETNX) | ❌ 无 Kafka 代码 | ✅ 必须 |
|
||||
| 与 ai02(push-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_deliveries(per-channel 投递状态) | 无法追踪邮件/短信投递成功失败 |
|
||||
| M18 | **无幂等键**:send 无 idempotencyKey 参数,重复请求会重复发通知 | 客户端重试导致重复通知 |
|
||||
| M19 | **无通知优先级/分类**:缺 priority/category 字段 | 无法区分紧急通知与普通通知,无法按类退订 |
|
||||
| M20 | **无调度/延迟发送**:缺 scheduledAt 字段与调度器 | 未来定时通知、每日摘要无法支持 |
|
||||
| M21 | **权限模型过简**:仅 3 个权限点,硬编码 ROLE_PERMISSIONS | 应对齐 iam 的 RBAC + DataScope(DATA_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 POST,push-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 message(coord 维护 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)
|
||||
1157
services/msg/docs/02-architecture-design.md
Normal file
1157
services/msg/docs/02-architecture-design.md
Normal file
File diff suppressed because it is too large
Load Diff
816
services/msg/docs/02-architecture.md
Normal file
816
services/msg/docs/02-architecture.md
Normal file
@@ -0,0 +1,816 @@
|
||||
# 模块架构设计文档 — msg
|
||||
|
||||
> AI 标识:ai10
|
||||
> 负责模块:msg(P5 沟通通知)
|
||||
> 阶段:架构设计外包 · 阶段 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 表清单(MySQL,msg 独占库)
|
||||
|
||||
| 表名 | 用途 | 现状 |
|
||||
| ------------------------------ | -------------------------- | --------------- |
|
||||
| `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 |
|
||||
| ------------ | ---------------------- | -------------------- | ---------------- |
|
||||
| 用户通知列表 | DB(idx_user_created) | ES(user_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 API(HTTP,经 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 API(NotificationService,proto 包 `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.Push(ai02 定义) | 实时推送(替代当前 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 Bitmap:key `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 truth,Redis 是缓存;标记已读时双写(DB update + Redis SETBIT),Redis 失败仅 log 不阻断
|
||||
|
||||
### 未来扩展
|
||||
|
||||
- 位图支持按 category 分桶(`msg:unread:{userId}:ACADEMIC`)实现分类未读数
|
||||
|
||||
---
|
||||
|
||||
## 10. ES 降级查询策略(ai-allocation §5)
|
||||
|
||||
### 双向降级
|
||||
|
||||
| 触发场景 | 主路径 | 降级路径 |
|
||||
| ------------ | ------ | -------------------------- |
|
||||
| 正常列表查询 | DB | ES(DB 不可用时) |
|
||||
| 正常全文检索 | ES | DB LIKE(ES 不可用时) |
|
||||
| DB 不可用 | — | ES 读模型(可能延迟 1s) |
|
||||
| ES 不可用 | — | DB LIKE 或返回空(检索类) |
|
||||
|
||||
### 实现要点
|
||||
|
||||
- `NotificationRepository` 内置 `listByUser()` 优先 DB,catch DB error 后 fallback 到 `esSearch()`
|
||||
- `search()` 优先 ES,catch 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[告警 + 人工处理]
|
||||
```
|
||||
|
||||
**双保险**:
|
||||
|
||||
- L1:Redis 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 消费积压 | 通知延迟 | 监控 lag,HPA 扩容 consumer 实例 |
|
||||
| 模板变量缺失导致渲染失败 | 通知发不出 | 渲染失败记 MSG_TEMPLATE_RENDER_ERROR,降级用默认文案 |
|
||||
| ES 与 DB 数据不一致 | 检索结果缺失 | 定期 reconcile job(未来 P6),降级读标注 |
|
||||
|
||||
### 12.2 假设
|
||||
|
||||
- 假设 iam 在 P2 已提供 `getEffectivePermissions` API,msg 的 PermissionGuard 可读取权限(过渡期用 ROLE_PERMISSIONS 兜底)
|
||||
- 假设 push-gateway(ai02)P5 提供 gRPC PushService(当前 fetch /internal/push 作为降级)
|
||||
- 假设 coord 在 events.proto 补充 NotificationEvent message
|
||||
- 假设 Redis 纳入 P5 基础设施(infra/docker-compose.yml 已有 edu-redis,msg 共用)
|
||||
- 假设 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/Sms(M14) | push-gateway gRPC |
|
||||
| P0 | 通知模板模块(M13) | — |
|
||||
| P0 | Redis 位图已读/未读(M15) | Redis |
|
||||
| P0 | ES mapping + ensureIndex(M3) | — |
|
||||
| P0 | DB→ES 降级读路径(M16) | — |
|
||||
| P1 | 投递记录表 + 重试(M17) | — |
|
||||
| P1 | 幂等键(HTTP send)(M18) | — |
|
||||
| P1 | 优先级/分类(M19) | — |
|
||||
| P1 | /readyz 补 Redis+ES 检查 | — |
|
||||
| P1 | 优雅关闭统一到 LifecycleService(M11) | — |
|
||||
| P1 | NotificationsModule exports(M7) | — |
|
||||
| P1 | 权限模型对齐 iam RBAC(M21) | iam getEffectivePermissions |
|
||||
| P1 | gRPC controller(M9,待 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)
|
||||
1506
services/parent-bff/docs/02-architecture-design.md
Normal file
1506
services/parent-bff/docs/02-architecture-design.md
Normal file
File diff suppressed because it is too large
Load Diff
1496
services/student-bff/docs/02-architecture-design.md
Normal file
1496
services/student-bff/docs/02-architecture-design.md
Normal file
File diff suppressed because it is too large
Load Diff
133
services/teacher-bff/docs/01-understanding.md
Normal file
133
services/teacher-bff/docs/01-understanding.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 模块理解确认书 — teacher-bff
|
||||
|
||||
> AI 标识:ai03
|
||||
> 负责模块:teacher-bff(P2)
|
||||
> 阶段:架构设计外包 · 阶段 1(全局理解)
|
||||
> 日期:2026-07-09(v1)/ 2026-07-09(v2 审计修订)
|
||||
> 关联文档:[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-bff,core-edu 由 ai08 负责);修正 /readyz 实际状态;修正错误码前缀描述;补充前瞻性内容(SSE 流式透传、Kafka 缓存失效、DataLoader 覆盖、DataScope 透传)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 我在架构中的位置
|
||||
|
||||
- **层级**:BFF 聚合层(L4)
|
||||
- **上游**:api-gateway(Go/Gin)通过 HTTP 转发请求,注入 `x-user-id` / `x-user-roles` 头
|
||||
- **下游**:iam(3002 / gRPC 50052)、classes(3001 / P3 合并入 core-edu)、core-edu(3004 / gRPC 50053);P4 扩展 content(gRPC 50054)、data-ana(gRPC 50055);P5 扩展 msg(gRPC 50056)、ai(gRPC 50057,含 StreamChat 流式 RPC)
|
||||
- **通信方式**:
|
||||
- 当前(P2):对前端 REST,对下游 REST `fetch`(同步)
|
||||
- 目标态(004 §4.1 / pending-features P2):**gRPC** 调下游业务服务 + **GraphQL Yoga** 对前端暴露 + DataLoader 防 N+1
|
||||
- 演进路径:P2 REST 内部封装为 client 抽象层 → P3 引入 gRPC client(iam + 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/Mutation(dashboard / viewports / classes / exams / homework / grades / knowledgePath / classPerformance / studentWeakness / notifications)+ Mutation(createExam / assignHomework / recordGrade / generateQuestion)
|
||||
- P5:SSE 流式端点(AI 对话透传)
|
||||
- **错误码前缀**:当前代码用 `TEACHER_BFF_*`(违反 project_memory 规则 "BFF 错误码必须用 `BFF_XXX_` 前缀"),**需迁移为 `BFF_TEACHER_*`**(coord P0 整改 #3);业务错误透传下游 `CORE_EDU_*` / `IAM_*` / `CLASSES_*`
|
||||
- **缓存**:聚合结果 Redis 短缓存 5-30s(004 §6.3,当前未实现);权限列表 5min 缓存 + 事件驱动失效(订阅 `edu.identity.user.role_changed` / `edu.identity.role.updated`,P3+ 引入 Kafka consumer,P2 用短 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 + DataLoader(urql 客户端)
|
||||
- 缓存: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 breaker(opossum)+ retry(gRPC interceptor)
|
||||
- 测试:Vitest(待引入,目标覆盖率 ≥ 80%)
|
||||
|
||||
## 5. 我的阶段归属
|
||||
|
||||
- **P2 身份**:教师登录 → 获取 JWT → 访问 teacher-portal → 侧边栏按 viewports.L1 渲染 → 空白 Dashboard
|
||||
- **P3 扩展**:考试/作业/成绩的查询与 mutation;iam + core-edu 切 gRPC;引入 Redis 聚合缓存
|
||||
- **P4 扩展**:知识图谱查询(content)+ 学情诊断(data-ana);GraphQL Yoga 对前端 + DataLoader;content + data-ana 切 gRPC
|
||||
- **P5 扩展**:AI 辅助出题(SSE 流式透传)+ 通知查询聚合(msg);ai + msg 切 gRPC;可选 Kafka 缓存失效消费者
|
||||
- **P6 硬化**:circuit breaker + retry + HPA + mTLS;99.9% 可用性
|
||||
- **依赖上游**:P1 黄金模板 classes、P2 iam(getEffectivePermissions + 视口)
|
||||
|
||||
## 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] 优雅关闭 SIGTERM(main.ts 已处理)
|
||||
- [ ] 测试覆盖率 ≥ 80%(**当前 0%**,无测试文件,P2 引入 Vitest)
|
||||
- [x] Dockerfile 多阶段构建(builder + runtime,已具备,见 [Dockerfile](../Dockerfile))
|
||||
- [ ] Zod 输入验证(当前 Controller 直接透传 unknown,**未做 Zod 校验**,P2 补全)
|
||||
- [x] GlobalErrorFilter 统一兜底
|
||||
|
||||
---
|
||||
|
||||
## 服务审计表 — ai03(teacher-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+1,pending-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) | iam(ai06) | ⚠️ 当前 teacher-bff 调 `/iam/viewports` 与 `/iam/me`,未定义此聚合 API 的 proto |
|
||||
| iam 补 RPC:GetViewports / GetEffectivePermissions / Logout / GetPublicKey | 4 个 RPC | iam(ai06) | ❌ proto 缺失,coord 裁决 P2 补全 |
|
||||
| core-edu 补 AttendanceService + GetClassesByTeacher | 1 个 service + 1 个 RPC | core-edu(ai08) | ❌ proto 缺失,coord 裁决 P3 补全 |
|
||||
| data-ana 补 GetTeacherDashboardStats | 1 个 RPC | data-ana(ai11) | ❌ proto 缺失,提请 coord 仲裁 |
|
||||
| GraphQL vs REST 仲裁 | P2 即 GraphQL(pending-features)vs P2-P3 REST、P4+ GraphQL(coord 裁决) | 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)
|
||||
1513
services/teacher-bff/docs/02-architecture-design.md
Normal file
1513
services/teacher-bff/docs/02-architecture-design.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user