docs: ai 协作文档体系重构与多 ai 仲裁结果落地

1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md)

2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration)

3.各服务 01/02 文档补全

4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens)

5.Proto 契约补全

6.004 架构影响地图更新

7.端口分配表

8.设计规格文档
This commit is contained in:
SpecialX
2026-07-10 12:58:22 +08:00
parent 2a2a56f541
commit faaaf29f67
120 changed files with 23201 additions and 2 deletions

View File

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

View File

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