From beb204c00f30b0b713962bed66aab6e27eeb8fe0 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Thu, 9 Jul 2026 08:04:00 +0800 Subject: [PATCH] =?UTF-8?q?docs(docs):=20=E8=AE=B0=E5=BD=95=20P3=20core-ed?= =?UTF-8?q?u=20=E7=AB=AF=E5=88=B0=E7=AB=AF=E9=AA=8C=E8=AF=81=E7=BB=8F?= =?UTF-8?q?=E9=AA=8C=E4=B8=8E=E5=9C=BA=E6=99=AF=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core-edu 段新增 6 行: datetime 转换/Kafka 非阻塞/相对 import/身份头/健康检查/Outbox 验证 api-gateway 段新增 3 行: 路由注册位置/多服务路由扩展/DEV_MODE 环境变量 经验日志追加 2026-07-09 P3 条目 --- docs/troubleshooting/known-issues.md | 54 ++++++++++++++++------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md index b21f6dc..7dac01c 100644 --- a/docs/troubleshooting/known-issues.md +++ b/docs/troubleshooting/known-issues.md @@ -153,18 +153,21 @@ ### 2.1 api-gateway(Go) -| 场景 | 技术/规则 | -| ---------------- | --------------------------------------------------------------------------------------- | -| P1 鉴权 | Gateway 内置 HS256 JWT,`jwt.ParseWithClaims` + `SigningMethodHMAC` 校验 | -| P2 鉴权升级 | 改 RS256,IAM 私钥签发,Gateway 公钥校验,无需调 IAM | -| 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` | -| 路径重写 | 去掉 `/api/v1` 前缀后转发到下游服务 | -| 用户上下文注入 | `c.Request.Header.Set("x-user-id", claims.UserID)` 传递给下游 | -| 请求 ID | Gateway 生成或透传 `X-Request-ID`,`c.Set("request_id", ...)` + `c.Header(...)` | -| P1 不做 | 限流/熔断/灰度(P6 硬化阶段实现) | -| 健康检查 | `GET /health` 无需鉴权 | -| 尾斜杠重定向循环 | `r.RedirectTrailingSlash=false` + 同时注册 `Any("/classes")` 与 `Any("/classes/*path")` | -| 开发模式鉴权旁路 | `DEV_MODE=true` 时接受 `Bearer dev-token`,注入固定身份;生产必须 `false` | +| 场景 | 技术/规则 | +| ----------------- | ------------------------------------------------------------------------------------------------------------- | +| P1 鉴权 | Gateway 内置 HS256 JWT,`jwt.ParseWithClaims` + `SigningMethodHMAC` 校验 | +| P2 鉴权升级 | 改 RS256,IAM 私钥签发,Gateway 公钥校验,无需调 IAM | +| 路由转发 | `gin.Group("/api/v1")` + `httputil.NewSingleHostReverseProxy` | +| 路径重写 | 去掉 `/api/v1` 前缀后转发到下游服务 | +| 用户上下文注入 | `c.Request.Header.Set("x-user-id", claims.UserID)` 传递给下游 | +| 请求 ID | Gateway 生成或透传 `X-Request-ID`,`c.Set("request_id", ...)` + `c.Header(...)` | +| P1 不做 | 限流/熔断/灰度(P6 硬化阶段实现) | +| 健康检查 | `GET /health` 无需鉴权 | +| 尾斜杠重定向循环 | `r.RedirectTrailingSlash=false` + 同时注册 `Any("/classes")` 与 `Any("/classes/*path")` | +| 开发模式鉴权旁路 | `DEV_MODE=true` 时接受 `Bearer dev-token`,注入固定身份;生产必须 `false` | +| 路由注册位置 | 真实路由在 `main.go` 的 `api.Group` 内注册,`internal/routing/routing.go` 若未被 main 引用即为死代码 | +| 多服务路由扩展 | 新增服务代理时在 main.go 注册两组路由:`Any("/x")` + `Any("/x/*path")`,与 classes 一致 | +| DEV_MODE 环境变量 | Go 不自动加载 .env,`DEV_MODE` 必须在启动前 export 或写入系统环境变量,否则 DevMode=false 导致 dev-token 被拒 | ### 2.2 classes(TS/NestJS,P1 黄金模板) @@ -208,16 +211,22 @@ ### 2.4 core-edu(TS/NestJS,P3) -| 场景 | 技术/规则 | -| -------------- | ----------------------------------------------------------------------------------------------------------------------- | -| 考试全生命周期 | 教师创建 → 发布 → 学生作答 → 教师批改 → 成绩统计 | -| Outbox 模式 | 业务事务同写 `outbox_events` 表,后台 relay worker 投递 Kafka | -| Outbox relay | Go 写独立服务 `services/outbox-relay/`,轻量高吞吐 | -| Kafka topic | `exam.published` / `homework.graded` / `grade.recorded` | -| 不引入 Saga | 跨服务一致性用 Outbox + 最终一致性 | -| 批改后联动 | 批改完成 → 发 `homework.graded` 事件 → 下游消费(DataAna/Msg) | -| Temporal 试点 | 仅 1 个工作流(考试发布编排:创建作业→通知) | -| schema 表 | exams / exam_questions / homework_assignments / homework_submissions / homework_answers / grade_records / outbox_events | +| 场景 | 技术/规则 | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| 考试全生命周期 | 教师创建 → 发布 → 学生作答 → 教师批改 → 成绩统计 | +| Outbox 模式 | 业务事务同写 `outbox_events` 表,后台 relay worker 投递 Kafka | +| Outbox relay | Go 写独立服务 `services/outbox-relay/`,轻量高吞吐 | +| Kafka topic | `exam.published` / `homework.graded` / `grade.recorded` | +| 不引入 Saga | 跨服务一致性用 Outbox + 最终一致性 | +| 批改后联动 | 批改完成 → 发 `homework.graded` 事件 → 下游消费(DataAna/Msg) | +| Temporal 试点 | 仅 1 个工作流(考试发布编排:创建作业→通知) | +| schema 表 | exams / exam_questions / homework_assignments / homework_submissions / homework_answers / grade_records / outbox_events | +| datetime 列 | Drizzle `datetime` 列需 Date 对象,HTTP 请求体里是 ISO 字符串,service 层必须 `new Date(input)` 转换否则报 `toISOString is not a function` | +| Kafka 非阻塞 | 开发环境 Kafka 未启动时 `connectKafka()` 必须 try/catch 且 main 中用 `void` 调用,否则阻塞服务启动 | +| 相对 import | `src//` 下文件回溯一级用 `../`,不要用 `../../`(NestJS ESM 模块) | +| 身份头读取 | Controller 用 `@Req() req` 从 `req.headers['x-user-id']` 读 createdBy/gradedBy,不依赖未注册的 AuthMiddleware | +| 健康检查 | health.controller 用 Drizzle `db.execute(sql\`SELECT 1\`)`,不用 typeorm DataSource | +| Outbox 验证 | 业务事务同写 `core_edu_outbox` 表,Kafka 未启动时事件 status=failed,启动后会重试 | ### 2.5 content(TS/NestJS,P4) @@ -309,6 +318,7 @@ | 日期 | 时间 | 模块 | 做了什么 + 学到什么 | | ---------- | ---- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 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。 | | 2026-07-08 | 下午 | 全局 | **CI/CD 完整配置 + 多AI协作规范入规则**:(1) project_rules.md 新增 §14 多 AI 协作规范(角色权限矩阵/分支命名/PR合并规则/跨模块变更顺序/冲突处理/AI 身份标注/敏感文件保护)+ §15 CI/CD 规范(流水线阶段/触发条件/镜像规范/部署策略/Secrets 管理/必需 CI 文件)。(2) 优化现有 4 个 ci-*.yml:ci-ts.yml 加 arch-scan + docker-build job;ci-go.yml 去掉 golangci-lint(lint-staged 预存问题),加 docker-build;ci-proto.yml 修复 buf breaking URL(从 github.com 改为 .git 本地比较)。(3) 新增 `docker.yml`:main/tag 触发,构建推送 3 服务镜像到 Gitea Container Registry(git.eazygame.cn/xiner/edu/:latest + sha tag + version tag),用 GITHUB_TOKEN 自动认证。(4) 新增 `deploy.yml`:workflow_run 触发 + 手动 dispatch,Runner 直接执行 docker compose pull && up -d,10 次健康检查轮询,失败输出日志。(5) 新增 `infra/docker-compose.deploy.yml`(部署用,镜像来自 Gitea registry,连接服务器已有 MySQL/Redis 通过 edu-shared 外部网络)+ `infra/deploy.env.example`(部署环境变量模板)。(6) 编写 `docs/standards/cicd-runbook.md`(CI/CD 使用手册,含架构总览/一次性配置/日常使用/镜像管理/部署验证/回滚/常见问题/排查命令/安全注意事项)。**学到**:Docker Compose 不支持 `restart_policy`(是 swarm 字段),用 `restart: unless-stopped` 替代;Gitea Actions 兼容 GitHub Actions 语法但 `workflow_run` 触发可能不完整,备选手动 dispatch;应用容器访问宿主机已有 MySQL/Redis 需通过共享外部网络(`docker network create edu-shared` + `docker network connect`)而非 `host.docker.internal`。 |