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:
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)
|
||||
Reference in New Issue
Block a user