# content 对接契约

> 负责人：ai09
> 关联：[matrix.md](../matrix.md)、[content.proto](../../../packages/shared-proto/proto/content.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[../../services/content/docs/02-architecture-design.md](../../../services/content/docs/02-architecture-design.md)、[../objections/content_issue.md](../objections/content_issue.md)、[../worklines/content_workline.md](../worklines/content_workline.md)
> 当前分支：`feat-review-content-module-docs-WAIyMA`
> 契约策略：[coord-final-decisions.md §2 B2](../../coord-final-decisions.md) "首次实现即 gRPC 调用下游"——content 对外契约统一为 gRPC（REST 端点仅自身管理面用，不作为下游消费契约）

---

## §0 契约状态总览

| 维度       | 当前状态                                      | 目标状态（P4 完成）                                               |
| ---------- | --------------------------------------------- | ----------------------------------------------------------------- |
| gRPC       | ✅ 已实现 22 RPC（4 Service，ARB-005 已裁决） | ✅ 22 RPC（4 Service）                                            |
| HTTP REST  | ✅ 已实现 22 端点（4 Controller）             | 🔁 保留作为管理面（不作为下游契约，下游统一 gRPC）                |
| Kafka 发布 | ✅ 已实现 4 聚合 topic（ARB-005 §5.3 裁决）   | ✅ 4 聚合 topic                                                   |
| Kafka 消费 | ❌ 未实现                                     | 🟢 可选（content 是上游，不主动消费 core-edu 事件，见 ISSUE-005） |

---

## §1 我提供什么（对外接口）

### 1.1 gRPC 接口（目标态 · P4 完成）

> 依据 [coord-final-decisions.md §3.3](../../coord-final-decisions.md) N1/N3/N5 + [02-architecture-design.md §4.2](../../../services/content/docs/02-architecture-design.md)
> ARB-005 已裁决（2026-07-10），按 22 RPC 定稿（TextbookService 5 + ChapterService 5 + KnowledgeGraphService 4 + QuestionService 8）

| Service               | RPC                  | 请求                                                                                                                        | 响应                                                         | 端口  | 阶段 |
| --------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ----- | ---- |
| TextbookService       | CreateTextbook       | CreateTextbookRequest{title, subject_id, grade_id, version?}                                                                | Textbook                                                     | 50054 | P4   |
| TextbookService       | GetTextbook          | GetTextbookRequest{id}                                                                                                      | Textbook                                                     | 50054 | P4   |
| TextbookService       | ListTextbooks        | ListTextbooksRequest{subject_id?, grade_id?, page_token, page_size}                                                         | ListTextbooksResponse{textbooks[], next_page_token}          | 50054 | P4   |
| TextbookService       | UpdateTextbook       | UpdateTextbookRequest{id, title?, status?, metadata?}                                                                       | Textbook                                                     | 50054 | P4   |
| TextbookService       | DeleteTextbook       | DeleteTextbookRequest{id}                                                                                                   | Empty                                                        | 50054 | P4   |
| ChapterService        | CreateChapter        | CreateChapterRequest{textbook_id, title, order, parent_id?}                                                                 | Chapter                                                      | 50054 | P4   |
| ChapterService        | GetChapter           | GetChapterRequest{id}                                                                                                       | Chapter                                                      | 50054 | P4   |
| ChapterService        | ListChapters         | ListChaptersRequest{textbook_id, parent_id?}                                                                                | ListChaptersResponse{chapters[]}                             | 50054 | P4   |
| ChapterService        | UpdateChapter        | UpdateChapterRequest{id, title?, order?, status?}                                                                           | Chapter                                                      | 50054 | P4   |
| ChapterService        | DeleteChapter        | DeleteChapterRequest{id}                                                                                                    | Empty                                                        | 50054 | P4   |
| KnowledgeGraphService | GetPrerequisites     | GetPrerequisitesRequest{knowledge_point_id, depth?}                                                                         | KnowledgePointsResponse{points[]}                            | 50054 | P4   |
| KnowledgeGraphService | GetLearningPath      | GetLearningPathRequest{student_id, subject_id}                                                                              | LearningPath{points[], recommended_order[]}                  | 50054 | P4   |
| KnowledgeGraphService | AddPrerequisite      | AddPrerequisiteRequest{kp_id, prerequisite_id}                                                                              | Empty                                                        | 50054 | P4   |
| KnowledgeGraphService | RemovePrerequisite   | RemovePrerequisiteRequest{kp_id, prerequisite_id}                                                                           | Empty                                                        | 50054 | P4   |
| QuestionService       | CreateQuestion       | CreateQuestionRequest{knowledge_point_id, type, content, options?, answer, explanation?, difficulty?, source?, created_by?} | Question                                                     | 50054 | P4   |
| QuestionService       | BatchCreateQuestions | BatchCreateQuestionsRequest{questions[]}                                                                                    | BatchCreateQuestionsResponse{ids[], failed[]}                | 50054 | P4   |
| QuestionService       | GetQuestion          | GetQuestionRequest{id}                                                                                                      | Question                                                     | 50054 | P4   |
| QuestionService       | ListQuestions        | ListQuestionsRequest{knowledge_point_id?, type?, difficulty?, status?, page_token, page_size}                               | ListQuestionsResponse{questions[], next_page_token}          | 50054 | P4   |
| QuestionService       | UpdateQuestion       | UpdateQuestionRequest{id, content?, answer?, status?}                                                                       | Question                                                     | 50054 | P4   |
| QuestionService       | DeleteQuestion       | DeleteQuestionRequest{id}                                                                                                   | Empty                                                        | 50054 | P4   |
| QuestionService       | PublishQuestion      | PublishQuestionRequest{id}                                                                                                  | Empty                                                        | 50054 | P4   |
| QuestionService       | SearchQuestions      | SearchQuestionsRequest{q?, type?, difficulty?, knowledge_point_id?, page_token, page_size}                                  | SearchQuestionsResponse{questions[], total, next_page_token} | 50054 | P5   |

**RPC 总数**：22（TextbookService 5 + ChapterService 5 + KnowledgeGraphService 4 + QuestionService 8）

> ✅ **matrix.md §2 已同步**：ARB-005 裁决后已更新为 22 RPC

#### proto message 字段说明（关键字段）

```protobuf
message Textbook {
  string id = 1;
  string title = 2;
  string subject_id = 3;
  string grade_id = 4;
  string version = 5;
  string status = 6;        // draft/pending_review/published/archived
  string tenant_id = 7;     // 多租户预留
  google.protobuf.Struct metadata = 8;  // 扩展字段
  int64 created_at = 9;
  int64 updated_at = 10;
}

message Chapter {
  string id = 1;
  string textbook_id = 2;
  string title = 3;
  int32 order = 4;          // DB 列名 order_num，proto 字段名 order
  string parent_id = 5;     // 树形结构
  string status = 6;
  int64 created_at = 7;
  int64 updated_at = 8;
}

message KnowledgePoint {
  string id = 1;
  string chapter_id = 2;
  string title = 3;
  string description = 4;
  int32 difficulty = 5;     // 1-5
  google.protobuf.Struct metadata = 6;
  int64 created_at = 7;
  int64 updated_at = 8;
}

message Question {
  string id = 1;
  string knowledge_point_id = 2;
  string type = 3;          // single_choice/multiple_choice/short_answer/essay
  string content = 4;       // 题干（HTML/markdown）
  google.protobuf.Struct options = 5;  // 选项（选择题）
  string answer = 6;
  string explanation = 7;
  int32 difficulty = 8;     // 1-5
  string status = 9;        // draft/pending_review/published/rejected/archived
  string source = 10;       // manual/ai_generated/imported
  string created_by = 11;
  google.protobuf.Struct metadata = 12;
  int64 created_at = 13;
  int64 updated_at = 14;
}
```

### 1.2 HTTP 端点（管理面 · 非下游契约）

> ⚠️ 以下 REST 端点仅供 content 自身管理面/直接 Gateway 访问用，**下游服务（teacher-bff/student-bff/ai）统一走 gRPC**，不消费以下 REST 端点。

当前已实现 22 端点（4 Controller），详见 [02-architecture-design.md §4.1](../../../services/content/docs/02-architecture-design.md)。P4 重构后将统一加 `/v1/` 版本前缀（见 ISSUE-008）。

### 1.3 GraphQL schema

不适用。content 是 gRPC 服务，不暴露 GraphQL。

### 1.4 Kafka 事件发布（目标态 · P4 完成）

> 待 ISSUE-002 仲裁确认 topic 命名策略，当前按**聚合 topic + action 字段**策略列出（与 matrix.md §4 / events.proto ClassEvent 模式一致）

| Topic                              | Event message       | action 字段值                                                 | 消费方                                             | 阶段 |
| ---------------------------------- | ------------------- | ------------------------------------------------------------- | -------------------------------------------------- | ---- |
| edu.content.textbook.events        | TextbookEvent       | created / updated / published / archived                      | data-ana / msg（通知教师教材发布）                 | P4   |
| edu.content.chapter.events         | ChapterEvent        | created / updated / deleted                                   | data-ana                                           | P4   |
| edu.content.knowledge_point.events | KnowledgePointEvent | created / updated / prerequisite_added / prerequisite_removed | data-ana / ai / Neo4j Sync Worker / ES Sync Worker | P4   |
| edu.content.question.events        | QuestionEvent       | created / updated / published / deleted                       | data-ana / ai / ES Sync Worker                     | P4   |

> ⚠️ **ISSUE-003 待仲裁**：当前 matrix.md §4 仅登记 kp + question 两类 topic，Textbook/Chapter 事件未登记。本契约按 design doc §5.1 补全 4 类，待 coord 仲裁后同步 matrix.md。

#### 事件 payload schema（待补 events.proto）

需在 [events.proto](../../../packages/shared-proto/proto/events.proto) 追加 4 个 message：

```protobuf
message TextbookEvent {
  string event_id = 1;
  string aggregate_id = 2;
  string event_type = 3;       // edu.content.textbook.created 等
  int64 occurred_at = 4;
  string textbook_id = 5;
  string title = 6;
  string subject_id = 7;
  string grade_id = 8;
  string version = 9;
  string action = 10;          // created/updated/published/archived
  map<string, string> metadata = 11;
}

message ChapterEvent {
  string event_id = 1;
  string aggregate_id = 2;
  string event_type = 3;
  int64 occurred_at = 4;
  string chapter_id = 5;
  string textbook_id = 6;
  string title = 7;
  int32 order = 8;
  string action = 9;           // created/updated/deleted
  map<string, string> metadata = 10;
}

message KnowledgePointEvent {
  string event_id = 1;
  string aggregate_id = 2;
  string event_type = 3;
  int64 occurred_at = 4;
  string kp_id = 5;
  string chapter_id = 6;
  string title = 7;
  int32 difficulty = 8;
  string action = 9;           // created/updated/prerequisite_added/prerequisite_removed
  string prerequisite_id = 10; // 仅 prerequisite_* 有值
  map<string, string> metadata = 11;
}

message QuestionEvent {
  string event_id = 1;
  string aggregate_id = 2;
  string event_type = 3;
  int64 occurred_at = 4;
  string question_id = 5;
  string kp_id = 6;
  string type = 7;             // single_choice 等
  int32 difficulty = 8;
  string status = 9;
  string source = 10;          // manual/ai_generated/imported
  string created_by = 11;
  string action = 12;          // created/updated/published/deleted
  map<string, string> metadata = 13;
}
```

### 1.5 错误码前缀

`CONTENT_`（详见 [02-architecture-design.md §6.2](../../../services/content/docs/02-architecture-design.md)）

| 错误码                    | HTTP | gRPC status         | 触发条件                           |
| ------------------------- | ---- | ------------------- | ---------------------------------- |
| CONTENT_VALIDATION_ERROR  | 400  | INVALID_ARGUMENT    | Zod 校验失败 / 题型非法 / 难度越界 |
| CONTENT_NOT_FOUND         | 404  | NOT_FOUND           | 资源不存在                         |
| CONTENT_PERMISSION_DENIED | 403  | PERMISSION_DENIED   | 权限不足                           |
| CONTENT_CONFLICT          | 409  | ALREADY_EXISTS      | 唯一约束冲突 / 状态机非法转换      |
| CONTENT_BUSINESS_ERROR    | 422  | FAILED_PRECONDITION | 业务规则违反（如循环依赖检测）     |
| CONTENT_DATABASE_ERROR    | 500  | INTERNAL            | Drizzle 操作异常                   |
| CONTENT_INTERNAL_ERROR    | 500  | INTERNAL            | 未知异常                           |
| CONTENT_NEO4J_UNAVAILABLE | 503  | UNAVAILABLE         | Neo4j 不可用且无降级路径           |

### 1.6 健康检查端点

| 端点         | 用途                                              | 鉴权 | 响应                                                           |
| ------------ | ------------------------------------------------- | ---- | -------------------------------------------------------------- |
| GET /healthz | 存活探针（liveness），仅返回进程状态              | 无   | `{ "status": "ok", "service": "content", "timestamp": "..." }` |
| GET /readyz  | 就绪探针（readiness），检查 DB/Neo4j/Kafka 三依赖 | 无   | `{ "status": "ok                                               | degraded | down", "checks": { database, neo4j, kafka_producer, kafka_consumer } }` |
| GET /metrics | Prometheus 指标                                   | 无   | Prometheus exposition format                                   |

---

## §2 我消费什么（依赖上游）

### 2.1 gRPC 调用（同步）

**无**。content 是内容资源上游提供方，不主动调用其他业务服务 gRPC。

> content 与 core-edu 的关系澄清（见 ISSUE-005）：content 是 core-edu 的上游（core-edu 调 content 查知识点），不是下游。content 不消费 core-edu 事件。

### 2.2 Kafka 事件订阅（异步）

**P4 不订阅任何事件**（按 ISSUE-005 建议删除原 `edu.teaching.content.invalidated` 条目）。

若 P6+ 有教材/章节联动需求，由 core-edu 主动调用 content gRPC UpdateQuestion/UpdateTextbook 状态变更，而非事件驱动。

### 2.3 HTTP 调用

**无**。

### 2.4 基础设施依赖

| 依赖            | 用途                              | 配置项（env.ts）             | 必需性                  |
| --------------- | --------------------------------- | ---------------------------- | ----------------------- |
| MySQL 8         | 写模型主库（4 张业务表 + outbox） | DATABASE_URL                 | 🔴 必需                 |
| Neo4j 5         | 知识图谱（PREREQUISITE_OF）       | NEO4J_URL / NEO4J_PASSWORD   | 🔴 必需（图谱查询核心） |
| Kafka           | Outbox 事件发布                   | KAFKA_BROKERS（待补 env.ts） | 🔴 必需（Outbox 强制）  |
| Redis           | 缓存（教材树/章节树，P5+）        | REDIS_URL（已预留）          | 🟢 P5+ 可选             |
| Elasticsearch 8 | 题库全文检索（P5+）               | ES_URL（已预留）             | 🟢 P5+ 必需             |
| OTLP Collector  | 链路追踪                          | OTEL_EXPORTER_OTLP_ENDPOINT  | 🟢 可选（未配置时降级） |

---

## §3 就绪信号

### 3.1 我依赖的上游就绪标志

| 上游            | 就绪标志                          | 必需性                   | mock 策略                                     |
| --------------- | --------------------------------- | ------------------------ | --------------------------------------------- |
| infra           | MySQL 8 可用                      | 🔴 必需                  | 本地 docker-compose                           |
| infra           | Neo4j 5 可用                      | 🔴 必需                  | 本地 docker-compose；未配置时图谱查询返回 503 |
| infra           | Kafka 集群可用                    | 🔴 必需                  | 本地 docker-compose                           |
| shared-proto    | content.proto / events.proto 补全 | 🔴 必需（P4.6 自身完成） | ai09 自行修改 proto                           |
| core-edu (ai08) | gRPC 50053 启用                   | 🟢 可选（content 独立）  | 不依赖 core-edu 实时数据                      |
| ai (ai12)       | gRPC 50058 启用                   | 🟢 P5 联调时必需         | grpc-mock 拦截                                |

### 3.2 我的就绪标志（供下游消费）

#### P4 就绪（核心交付）

- [x] content gRPC 50054 启用（HealthService.Check 返回 SERVING）
- [x] TextbookService 5 RPC 可调用（Create/Get/List/Update/Delete）
- [x] ChapterService 5 RPC 可调用（Create/Get/List/Update/Delete）
- [x] KnowledgeGraphService 4 RPC 可调用（GetPrerequisites/GetLearningPath/AddPrerequisite/RemovePrerequisite）
- [x] QuestionService 8 RPC 可调用（Create/BatchCreate/Get/List/Update/Delete/Publish/Search）
- [x] edu.content.textbook.events / chapter.events / knowledge_point.events / question.events 4 topic 可发布
- [x] /readyz 返回 DB/Neo4j/Kafka 三依赖状态
- [x] Outbox Publisher worker 运行中（content_outbox_events 表 PENDING 事件 < 100）
- [x] Neo4j Sync Worker 运行中（知识点节点最终一致延迟 < 2s）

#### P5 就绪（检索 + AI 集成）

- [ ] GET /questions/search 检索 API 可用（延迟 < 200ms）
- [ ] QuestionService.SearchQuestions gRPC 可调用
- [ ] QuestionService.BatchCreateQuestions 与 ai12 联调通过
- [ ] ES Sync Worker 运行中（题目索引最终一致延迟 < 2s）
- [ ] 测试覆盖率 ≥ 80%

#### P6+ 就绪（演进）

- [ ] Question 审核工作流状态机完整
- [ ] 知识图谱可视化 API 可用
- [ ] 教材版本管理启用

---

## §4 Mock 策略

### 4.1 我提供的 mock（供下游消费）

在 content 真实服务就绪前，为下游（teacher-bff / student-bff / ai / data-ana）提供以下 mock：

#### gRPC mock（grpc-mock 拦截 50054 端口）

| Service               | RPC                  | Mock 返回                                                          |
| --------------------- | -------------------- | ------------------------------------------------------------------ |
| TextbookService       | ListTextbooks        | 固定 5 个 Textbook（语数英理化，subject_id=math/chn/eng/phy/chem） |
| TextbookService       | GetTextbook          | 返回第一个 Textbook                                                |
| ChapterService        | ListChapters         | 固定章节树（每教材 10 章，含 parent_id 树形结构）                  |
| KnowledgeGraphService | GetLearningPath      | 固定 8 个 KnowledgePoint 推荐顺序（按 difficulty 升序）            |
| KnowledgeGraphService | GetPrerequisites     | 固定 3 个前置知识点                                                |
| QuestionService       | ListQuestions        | 固定 20 个 Question（含 4 种题型各 5 个）                          |
| QuestionService       | SearchQuestions      | 固定 20 个 Question（含 options）                                  |
| QuestionService       | BatchCreateQuestions | 返回成功 + 生成 20 个 cuid2 ID                                     |
| QuestionService       | CreateQuestion       | 返回成功 + 生成 1 个 cuid2 ID                                      |

#### Kafka mock

content 就绪前不发布真实事件，下游使用本地 stub（data-ana / ai 各自维护测试数据集）。

### 4.2 我消费的 mock

content P4 不消费任何上游事件，无需 mock 上游。

P5 联调阶段消费 ai (ai12) 的 gRPC，使用 grpc-mock 拦截 50058 端口：

- AiService.GenerateQuestion 返回固定 1 个 Question（source=ai_generated）
- AiService.Chat 返回固定文本响应

---

## §5 契约一致性核查（2026-07-10）

> 本节记录 contract.md 与 design doc / matrix.md / proto 的对齐情况

### 5.1 与 02-architecture-design.md 对齐

| 维度        | design doc            | contract.md（本文件） | 一致性 | 备注                                                       |
| ----------- | --------------------- | --------------------- | ------ | ---------------------------------------------------------- |
| gRPC RPC 数 | §4.2 列 18 RPC        | §1.1 列 22 RPC        | ✅     | ARB-005 已裁决：22 RPC 为准；design doc 待 ai09 同步       |
| 事件 topic  | §5.1 列 12 独立 topic | §1.4 列 4 聚合 topic  | ✅     | ARB-005 已裁决：4 聚合 topic 为准；design doc 待 ai09 同步 |
| 错误码      | §6.2 列 8 个          | §1.5 列 8 个          | ✅     | 一致                                                       |
| 健康检查    | §6.6 /readyz 多依赖   | §1.6 三依赖           | ✅     | 一致                                                       |

### 5.2 与 matrix.md 对齐

| 维度          | matrix.md                                     | contract.md（本文件） | 一致性 | 备注                 |
| ------------- | --------------------------------------------- | --------------------- | ------ | -------------------- |
| gRPC RPC 总数 | §2 列 22 RPC                                  | §1.1 列 22 RPC        | ✅     | ARB-005 裁决后已同步 |
| Kafka topic   | §4 列 4 topic（textbook/chapter/kp/question） | §1.4 列 4 topic       | ✅     | ARB-005 裁决后已同步 |
| 错误码前缀    | §6 列 CONTENT_                                | §1.5 列 CONTENT_      | ✅     | 一致                 |
| 端口          | §2 列 50054                                   | §1.1 列 50054         | ✅     | 一致                 |

### 5.3 与 proto 文件对齐

| 文件          | 当前状态                                                           | 目标状态（P4 完成）         |
| ------------- | ------------------------------------------------------------------ | --------------------------- |
| content.proto | ✅ 22 RPC（4 Service 完整，ARB-005 已裁决）                        | ✅ 22 RPC（4 Service 完整） |
| events.proto  | ✅ 含 TextbookEvent/ChapterEvent/KnowledgePointEvent/QuestionEvent | ✅ 4 content message 已补全 |

---

## §6 变更记录

| 日期       | 版本 | 变更内容                                                                                                                                                                                                                                                                                               | 变更人 |
| ---------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
| 2026-07-09 | v1.0 | 初版（18 RPC，2 topic）                                                                                                                                                                                                                                                                                | coord  |
| 2026-07-10 | v1.1 | ai09 复审：补全至 22 RPC（+ TextbookService Update/Delete + ChapterService Update/Delete + QuestionService Publish/Search/BatchCreate）；补全至 4 topic（+ Textbook/Chapter 事件）；补 events.proto 4 message 草案；补 §0 状态总览 / §5 一致性核查 / §6 变更记录；明确 REST 端点为管理面（非下游契约） | ai09   |
| 2026-07-13 | v1.2 | ARB-005 全部 10 项 issue 已裁决并落实；P4 就绪信号全部勾选；matrix.md 已同步（22 RPC + 4 topic）；一致性核查全部 ✅                                                                                                                                                                                    | ai09   |
