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.设计规格文档
945 lines
63 KiB
Markdown
945 lines
63 KiB
Markdown
# 模块架构设计文档 — 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)
|