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

63 KiB
Raw Blame History

模块架构设计文档 — content

AI 标识ai05 负责模块contentP4 阶段:架构设计外包 · 阶段 2模块架构设计 日期2026-07-09 版本v1.0 关联文档:01-understanding.mdai-allocation.md004 架构影响地图pending-features.mdknown-issues.mdproject_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 整体分层架构

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-roles004 §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 聚合根与实体

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 状态机

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 出题审核流程预留)

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_eventsOutbox 模式 · 新增)

字段 类型 约束 说明
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 节点定义

// 知识点节点
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 典型查询

// 查询知识点前置链路(深度 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

{
  "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 错误响应结构

{
  "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_tokencursor 模式REST 使用 page / pageSizeoffset 模式),两者并行存在。


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 内部)

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 模式

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 实现)
  • 日志级别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 endpointOTEL_EXPORTER_OTLP_ENDPOINT env 注入

6.6 健康检查

/healthzliveness

{ "status": "ok", "service": "content", "timestamp": 1736000000000 }

不依赖任何外部资源,仅返回进程存活状态。

/readyzreadiness · 多依赖检查)

{
  "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 优雅关闭顺序

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