From 5f1882130218d7ad601d05d5ec75d5c546c961ac Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Thu, 9 Jul 2026 08:52:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(api-gateway):=20=E6=B7=BB=E5=8A=A0content?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E4=BB=A3=E7=90=86=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main.go添加textbooks/chapters/knowledge-points/questions四组路由 config.go新增ContentServiceURL字段 docs: known-issues记录P4 content端到端验证经验 --- docs/troubleshooting/known-issues.md | 18 ++++++++++++------ services/api-gateway/internal/config/config.go | 2 ++ services/api-gateway/main.go | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md index 7dac01c..1576bdb 100644 --- a/docs/troubleshooting/known-issues.md +++ b/docs/troubleshooting/known-issues.md @@ -230,12 +230,17 @@ ### 2.5 content(TS/NestJS,P4) -| 场景 | 技术/规则 | -| ---------- | ------------------------------------------------------------------ | -| 知识图谱 | Neo4j 查询前置依赖图(秒级返回) | -| 题库 CRUD | P4 仅 CRUD,P5 引入 ES 实现检索,避免 MySQL FULLTEXT → ES 迁移成本 | -| 双写避免 | Neo4j/ES 不直接双写,由消费 Kafka 事件同步,天然最终一致 | -| Neo4j 写入 | Content 服务写 MySQL 同时发事件,独立 worker 消费事件同步 Neo4j | +| 场景 | 技术/规则 | +| -------------- | ------------------------------------------------------------------------------- | +| 知识图谱 | Neo4j 查询前置依赖图(秒级返回) | +| 题库 CRUD | P4 仅 CRUD,P5 引入 ES 实现检索,避免 MySQL FULLTEXT → ES 迁移成本 | +| 双写避免 | Neo4j/ES 不直接双写,由消费 Kafka 事件同步,天然最终一致 | +| Neo4j 写入 | Content 服务写 MySQL 同时发事件,独立 worker 消费事件同步 Neo4j | +| API 字段名 | 请求体用 TS schema 字段名(如 `order`),非 DB 列名(如 `order_num`) | +| Neo4j 不可用 | 未设置 NEO4J_URL 时 driver=null,getNeo4jSession 返回 null,业务正常落库 MySQL | +| Neo4j 连接超时 | driver 配置 connectionTimeout:3000,避免 Neo4j 不可用时拖慢 HTTP 响应 | +| Drizzle int | drizzle-orm/mysql-core 导出 `int()` 不是 `integer()` | +| db 常量导出 | database.ts 导出 `db` 常量替代 `getDb()` 函数,与 core-edu/classes 黄金模板对齐 | ### 2.6 data-ana(Python/FastAPI,P4) @@ -318,6 +323,7 @@ | 日期 | 时间 | 模块 | 做了什么 + 学到什么 | | ---------- | ---- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-09 | 上午 | content/api-gateway | **P4 内容分析服务端到端打通**:(1) content 服务系统性修复:database.ts 导出 db 常量;env.ts JWT_SECRET/ES_URL/NEO4J_URL/NEO4J_PASSWORD 改 optional 加 DEV_MODE;neo4j.ts driver 惰性创建+try/catch+connectionTimeout:3000;health/lifecycle 改用 Drizzle;global-error.filter 移除 @types/express 依赖;textbooks.schema 修复 integer→int + 导出 NewTextbook/NewChapter 类型;textbooks.controller 移除 body as any + 加 PUT/DELETE。(2) 新建 3 模块:chapters(CRUD + 按 textbook 查询)、knowledge-points(CRUD + Neo4j 前置依赖图非阻塞查询)、questions(CRUD + 4 种题型校验)。(3) Gateway 路由扩展:textbooks/chapters/knowledge-points/questions 四组路由。(4) 数据库:content-init.sql 4 张表。(5) E2E 验证:POST /textbooks 201 → POST /chapters 201(字段用 order 非 orderNum)→ POST /knowledge-points 201(Neo4j 不可用 MySQL 正常写入)→ POST /questions 201 → GET 各列表 200。**学到**:Drizzle schema TS 字段名与 DB 列名解耦(order→order_num),API 请求体用 TS 字段名;Neo4j 不可用时必须 driver=null(不设 NEO4J_URL),否则每次请求尝试连接拖慢响应;neo4j-driver safeCreateNode 用 try/catch 非阻塞,MySQL 数据始终先落库。 | | 2026-07-09 | 凌晨 | core-edu/api-gateway | **P3 核心教学服务端到端打通**:(1) core-edu 服务系统性修复 13 项:database.ts 导出 db 常量替代 getDb();env.ts JWT_SECRET 改 optional 加 DEV_MODE;kafka.ts connectKafka 加 try/catch 不阻塞启动;main.ts 去全局 /api 前缀 + connectKafka 改 void 非阻塞;app.module 移除未用 AuthMiddleware/ClassesesModule 加 HealthModule;3 个 controller 路由去前缀去 UseGuards 从 x-user-id 读身份;exams/homework service datetime 列 ISO 字符串转 Date 修复 drizzle toISOString 错误;修正 10 处相对 import 路径;health/lifecycle 改用 Drizzle 原生查询;新增 core-edu-init.sql 4 张表。(2) Gateway 路由扩展:发现 internal/routing/routing.go 是死代码(未被 main 引用),真正路由在 main.go;在 main.go 添加 exams/homework/grades 三组路由(无尾斜杠+通配符);删除 routing.go;config.go 加 CoreEduServiceURL。(3) DEV_MODE 环境变量问题:Go 不自动加载 .env,必须在启动前 export DEV_MODE=true 否则 dev-token 被拒 401。(4) E2E 验证:POST /exams 201 → GET /exams/:id 200 → GET /exams/class/:id 200 → POST /homework 201 → POST /grades 201 → Outbox 3 条事件正确写入(exam.failed 因 Kafka 未启动,homework/grade pending)。**学到**:drizzle datetime 列需 Date 对象不是 ISO 字符串(mapToDriverValue 调 toISOString);Go 项目 .env 不会自动加载需显式 export 或 godotenv 库;NestJS controller 路由前缀与 Gateway 代理路径要协调(Gateway 去掉 /api/v1 后转发,controller 用裸路径如 'exams');Outbox 模式业务事务同写验证通过,Kafka 未启动时事件 status=failed 但业务数据已落库。 | | 2026-07-09 | 上午 | iam/teacher-bff/teacher-portal | **P2 身份阶段完整实现**:(1) Gateway 公开路径白名单(register/login/refresh)解决无 token 死锁。(2) IAM schema 扩展:users 加 dataScope,新增 role_viewports 表。(3) RBAC 端点 4 个 GET。(4) 视口按 requiredPermission 过滤 + sortOrder 排序;getEffectivePermissions 用 Set 去重。(5) JWT payload 含 dataScope,register 自动分配 teacher 角色。(6) 种子数据 7 权限+12 映射+7 视口。(7) Teacher BFF 视口聚合。(8) 前端:lib/auth.ts + login + AppShell + (app) 路由组 + dashboard + classes(真实 JWT)+ 根重定向。(9) E2E 全链路通过。**学到**:Next.js 路由组 (app) 不影响 URL,/login 与 /dashboard 共存只后者套壳;fetch headers 函数返回 Record 避免 TS2769;ESLint 9 需 flat config 留 P6;AppShell aside 用 flex flex-col + mt-auto 比 absolute 稳健。 | | 2026-07-08 | 晚上 | iam/classes/api-gateway | **P1 端到端链路验证 + IAM 服务修复**:验证 register → JWT → Gateway /iam/me → Gateway /classes CRUD → teacher-portal 前端渲染全链路打通。(1) IAM 服务 14 个 TS 编译错误修复:移除 typeorm/ioredis/kafkajs 依赖(IAM 用 Drizzle),health.controller.ts 改用 `db.execute(sql\`SELECT 1\`)`,lifecycle.service.ts 简化为只关闭 Drizzle 连接池;Drizzle API 修正(`r.roles`→`r.iam_roles`,`.in()`→`inArray()`)。(2) NestJS ESM DI 修复:iam.module.ts 简化 providers 为 `[IamService, IamRepository]`,iam.service.ts 构造器加 `@Inject(IamRepository)`(参考 classes 黄金模板),修复运行时 `Cannot read properties of undefined (reading 'findUserByEmail')`。(3) Gateway /iam/me 404 修复:iam.controller.ts 直接读 `req.headers['x-user-id']`替代未注册的`AuthenticatedRequest`。(4) 创建 `scripts/iam-init.sql`建 6 张 IAM 表 + 种子数据。(5) E2E 验证:iam:3002 注册/登录 → Gateway /iam/me 200 → Gateway GET /classes 200 → Gateway POST /classes(合法 UUID gradeId)201 → teacher-portal:3000 首页渲染 200 + 含"班级管理" → Next.js rewrites 透传 dev-token 到 Gateway 全链路通。**学到**:NestJS ESM 模式下 DI 无法通过类型推断解析 token,必须显式`@Inject(Token)`;Drizzle select 返回字段名按 schema 定义而非表名;classes.dto.ts 的 gradeId 要求 UUID 格式,测试数据不能用 "grade-12" 这类字符串;PowerShell 控制台中文显示为 `?`是编码问题,数据库实际存储正确;DEV_MODE 下前端用`Bearer dev-token` 即可走通链路,无需真实 JWT。 | diff --git a/services/api-gateway/internal/config/config.go b/services/api-gateway/internal/config/config.go index 0fbfc43..7c9807d 100644 --- a/services/api-gateway/internal/config/config.go +++ b/services/api-gateway/internal/config/config.go @@ -14,6 +14,7 @@ type Config struct { IamServiceURL string TeacherBffURL string CoreEduServiceURL string + ContentServiceURL string OTLPEndpoint string LogLevel string DevMode bool @@ -29,6 +30,7 @@ func Load() *Config { IamServiceURL: getEnv("IAM_SERVICE_URL", "http://localhost:3002"), TeacherBffURL: getEnv("TEACHER_BFF_URL", "http://localhost:3003"), CoreEduServiceURL: getEnv("CORE_EDU_SERVICE_URL", "http://localhost:3004"), + ContentServiceURL: getEnv("CONTENT_SERVICE_URL", "http://localhost:3005"), OTLPEndpoint: getEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"), LogLevel: getEnv("LOG_LEVEL", "info"), DevMode: getEnvBool("DEV_MODE", false), diff --git a/services/api-gateway/main.go b/services/api-gateway/main.go index cc4d517..00503af 100644 --- a/services/api-gateway/main.go +++ b/services/api-gateway/main.go @@ -93,6 +93,21 @@ func main() { api.Any("/homework/*path", coreEduHandler) api.Any("/grades", coreEduHandler) api.Any("/grades/*path", coreEduHandler) + + // content 服务路由(教材/章节/知识点/题库) + contentProxy, err := proxy.NewProxy(cfg.ContentServiceURL) + if err != nil { + log.Fatalf("failed to create content proxy: %v", err) + } + contentHandler := proxy.ProxyHandler(contentProxy) + api.Any("/textbooks", contentHandler) + api.Any("/textbooks/*path", contentHandler) + api.Any("/chapters", contentHandler) + api.Any("/chapters/*path", contentHandler) + api.Any("/knowledge-points", contentHandler) + api.Any("/knowledge-points/*path", contentHandler) + api.Any("/questions", contentHandler) + api.Any("/questions/*path", contentHandler) } srv := &http.Server{