# content 内容资源服务 > 版本:1.0(P4 完整实现) > 端口:HTTP 3005 / gRPC 50054 ## 职责 内容资源限界上下文,管理 Textbook、Chapter、KnowledgePoint、Question 四个聚合。 支持多存储:MySQL(写模型权威源)+ Neo4j(知识图谱派生读模型)+ Elasticsearch(P5 引入,题库全文检索)。 事件驱动:Outbox 模式发布 4 个聚合 topic,Neo4j Sync Worker 异步消费事件同步图谱。 ## 技术栈 - NestJS 10.x + TypeScript 5.6(ESM,相对 import 带 `.js` 后缀) - Drizzle ORM 0.31(MySQL,`getDb()` 函数式懒加载) - neo4j-driver 5.x(知识图谱,仅读模型查询) - KafkaJS 2.2(idempotent producer + transactionalId) - @grpc/grpc-js + @nestjs/microservices(gRPC server 50054) - @paralleldrive/cuid2(统一 ID 策略) - Zod 3.x(Controller 层全量 schema.parse) - pino(日志)+ prom-client(指标)+ OpenTelemetry(追踪) ## 开发 ```bash pnpm install pnpm dev # HTTP 3005 + gRPC 50054 pnpm build pnpm typecheck pnpm lint pnpm test ``` ## 环境变量 | 变量 | 说明 | | ----------------------------- | -------------------------------------------------- | | `PORT` | HTTP 服务端口(默认 3005) | | `GRPC_PORT` | gRPC 服务端口(默认 50054) | | `DATABASE_URL` | MySQL 连接串(必填) | | `KAFKA_BROKERS` | Kafka broker 列表(逗号分隔,默认 localhost:9092) | | `NEO4J_URL` | Neo4j Bolt 连接 URL(可选) | | `NEO4J_PASSWORD` | Neo4j 密码(可选) | | `REDIS_URL` | Redis 连接(预留,P5+ 缓存) | | `ES_URL` | Elasticsearch 地址(P5 引入) | | `JWT_SECRET` | JWT 密钥(Gateway 校验,服务可选) | | `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry OTLP 端点(可选) | ## 模块结构 ``` src/ ├─ config/ # env / database(getDb) / kafka / neo4j ├─ grpc/ # gRPC controllers(4 Service, 22 RPC) │ ├─ grpc.module.ts │ ├─ grpc-types.ts # 手动定义 proto message TS 类型 │ ├─ textbook.grpc.controller.ts │ ├─ chapter.grpc.controller.ts │ ├─ knowledge-graph.grpc.controller.ts │ └─ question.grpc.controller.ts ├─ textbooks/ # 教材领域(schema/repository/service/controller/dto) ├─ chapters/ # 章节领域 ├─ knowledge-points/ # 知识点领域(含前置依赖管理) ├─ questions/ # 题库领域 ├─ shared/ │ ├─ errors/ # ApplicationError + GlobalErrorFilter(CONTENT_* 前缀 + ZodError 分支) │ ├─ health/ # /healthz + /readyz(DB/Neo4j/Kafka 多依赖检查) │ ├─ lifecycle/ # 优雅关闭 │ ├─ observability/ # logger / metrics / tracer │ ├─ outbox/ # Outbox 模式(schema/repository/publisher/service/module/events) │ └─ sync/ # Neo4j Sync Worker(消费 Kafka 事件异步同步图谱) ├─ middleware/ # auth.middleware + permission.guard ├─ app.module.ts └─ main.ts # HTTP + gRPC hybrid app ``` ## REST API 端点 ### Textbooks | 方法 | 路径 | 说明 | | -------- | ---------------- | ------------------------- | | `POST` | `/textbooks` | 创建教材 | | `GET` | `/textbooks` | 教材列表(支持分页/过滤) | | `GET` | `/textbooks/:id` | 教材详情 | | `PUT` | `/textbooks/:id` | 更新教材 | | `DELETE` | `/textbooks/:id` | 删除教材 | ### Chapters | 方法 | 路径 | 说明 | | -------- | -------------------------------- | -------------- | | `POST` | `/chapters` | 创建章节 | | `GET` | `/chapters/textbook/:textbookId` | 按教材列出章节 | | `GET` | `/chapters/:id` | 章节详情 | | `PUT` | `/chapters/:id` | 更新章节 | | `DELETE` | `/chapters/:id` | 删除章节 | ### Knowledge Points | 方法 | 路径 | 说明 | | -------- | ----------------------------------------------------- | ------------------------------ | | `POST` | `/knowledge-points` | 创建知识点 | | `GET` | `/knowledge-points/chapter/:chapterId` | 按章节列出知识点 | | `GET` | `/knowledge-points/:id` | 知识点详情 | | `GET` | `/knowledge-points/:id/prerequisites` | 查询前置依赖(读 Neo4j) | | `POST` | `/knowledge-points/:id/prerequisites` | 添加前置依赖(发 Outbox 事件) | | `DELETE` | `/knowledge-points/:id/prerequisites/:prerequisiteId` | 移除前置依赖(发 Outbox 事件) | | `PUT` | `/knowledge-points/:id` | 更新知识点 | | `DELETE` | `/knowledge-points/:id` | 删除知识点 | ### Questions | 方法 | 路径 | 说明 | | -------- | ---------------------------------------------- | ------------------------- | | `POST` | `/questions` | 创建题目 | | `GET` | `/questions` | 题目列表(支持分页/过滤) | | `GET` | `/questions/knowledge-point/:knowledgePointId` | 按知识点列出题目 | | `GET` | `/questions/:id` | 题目详情 | | `PUT` | `/questions/:id` | 更新题目 | | `DELETE` | `/questions/:id` | 删除题目 | ## gRPC API(端口 50054) Proto 定义:`packages/shared-proto/proto/content.proto`(包名 `next_edu_cloud.content.v1`) | Service | RPC 数 | 关键方法 | | ----------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | `TextbookService` | 5 | CreateTextbook / GetTextbook / ListTextbooks / UpdateTextbook / DeleteTextbook | | `ChapterService` | 5 | CreateChapter / GetChapter / ListChapters / UpdateChapter / DeleteChapter | | `KnowledgeGraphService` | 4 | GetPrerequisites / GetLearningPath / AddPrerequisite / RemovePrerequisite | | `QuestionService` | 8 | CreateQuestion / BatchCreateQuestions / GetQuestion / ListQuestions / UpdateQuestion / DeleteQuestion / PublishQuestion / SearchQuestions | **合计 22 RPC。** ## 事件驱动 ### Outbox 模式 业务操作(create/update/delete)在同一事务内写业务表 + `content_outbox_events` 表。 独立 `OutboxPublisher` worker 轮询 pending 记录投递 Kafka(at-least-once,最大重试 5 次,指数退避)。 ### Kafka Topics(4 个聚合 topic,action 字段区分事件类型) | Topic | 事件类型(action) | | ------------------------------------ | ------------------------------------------------------------- | | `edu.content.textbook.events` | created / updated / published / archived | | `edu.content.chapter.events` | created / updated / deleted | | `edu.content.knowledge_point.events` | created / updated / prerequisite_added / prerequisite_removed | | `edu.content.question.events` | created / updated / published / deleted | ### Neo4j Sync Worker 消费 `edu.content.knowledge_point.events` topic,异步同步知识图谱: - `kp.created` / `kp.updated` → MERGE 节点 - `prerequisite_added` → MERGE 关系 - `prerequisite_removed` → DELETE 关系 Neo4j 不可用时非阻塞跳过(不影响 MySQL 写入和主流程)。 ## 健康检查 | 端点 | 用途 | 鉴权 | | -------------- | -------------------------------------------------------- | ---- | | `GET /healthz` | 存活探针(liveness),仅返回进程状态 | 无 | | `GET /readyz` | 就绪探针(readiness),检查 DB/Neo4j/Kafka,失败返回 503 | 无 | ## 质量校验 | 检查项 | 命令 | 状态 | | -------- | ----------------------- | ----------------- | | 类型检查 | `pnpm typecheck` | ✅ 0 错误 | | Lint | `pnpm lint` | ✅ 0 错误 | | 单元测试 | `pnpm test` | ✅ 182 tests 通过 | | 覆盖率 | `vitest run --coverage` | ✅ 66.94%(≥60%) | ## 对外契约 - gRPC proto:`packages/shared-proto/proto/content.proto` - 事件 proto:`packages/shared-proto/proto/events.proto`(TextbookEvent / ChapterEvent / KnowledgePointEvent / QuestionEvent) - 契约文档:`docs/architecture/issues/contracts/content_contract.md`