Files
Edu/services/content/docs/02-architecture-design.md
SpecialX faaaf29f67 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.设计规格文档
2026-07-10 12:58:22 +08:00

945 lines
63 KiB
Markdown
Raw Blame History

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