Files
Edu/docs/architecture/issues/contracts/content_contract.md

371 lines
26 KiB
Markdown
Raw Permalink 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 对接契约
> 负责人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 对外契约统一为 gRPCREST 端点仅自身管理面用,不作为下游消费契约)
---
## §0 契约状态总览
| 维度 | 当前状态 | 目标状态P4 完成) |
| ---------- | --------------------------------------------- | ----------------------------------------------------------------- |
| gRPC | ✅ 已实现 22 RPC4 ServiceARB-005 已裁决) | ✅ 22 RPC4 Service |
| HTTP REST | ✅ 已实现 22 端点4 Controller | 🔁 保留作为管理面(不作为下游契约,下游统一 gRPC |
| Kafka 发布 | ✅ 已实现 4 聚合 topicARB-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 总数**22TextbookService 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_numproto 字段名 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 两类 topicTextbook/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-005content 是 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 mockgrpc-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 就绪前不发布真实事件,下游使用本地 stubdata-ana / ai 各自维护测试数据集)。
### 4.2 我消费的 mock
content P4 不消费任何上游事件,无需 mock 上游。
P5 联调阶段消费 ai (ai12) 的 gRPC使用 grpc-mock 拦截 50058 端口:
- AiService.GenerateQuestion 返回固定 1 个 Questionsource=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 topictextbook/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 RPC4 Service 完整ARB-005 已裁决) | ✅ 22 RPC4 Service 完整) |
| events.proto | ✅ 含 TextbookEvent/ChapterEvent/KnowledgePointEvent/QuestionEvent | ✅ 4 content message 已补全 |
---
## §6 变更记录
| 日期 | 版本 | 变更内容 | 变更人 |
| ---------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
| 2026-07-09 | v1.0 | 初版18 RPC2 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 |