NestJS: disable incremental in 6 services tsconfig.json to fix dist not emitted when nest-cli deleteOutDir conflicts with tsc tsbuildinfo. classes/iam: import HealthModule in AppModule to fix /healthz 404. classes: rewrite HealthController to Drizzle getDb from TypeORM DI. teacher-bff: add /metrics endpoint for Prometheus scraping. infra: add node/mysql/redis exporters to observability profile. mysql-exporter v0.15.1 uses command-line flags not DATA_SOURCE_NAME. prometheus: enable web.enable-lifecycle for hot reload.
94 KiB
94 KiB
已知问题速查
索引式速查手册:场景 → 技术/规则映射。不写代码示例。 架构规则见 ../architecture/004_architecture_impact_map.md 与 ../../.trae/rules/project_rules.md 工作经验日志按时间倒序追加(50 条上限),AI 发现更好方案时可更新本节。
一、全局经验
1.1 多语言 monorepo 配置
| 场景 | 技术/规则 |
|---|---|
| 多语言 workspace | pnpm workspace(TS)+ go.work(Go)+ pyproject.toml/uv workspace(Python)三套并存 |
| 根 package.json scripts | 封装多语言命令入口:pnpm dev / pnpm lint / pnpm test / pnpm build |
| pnpm-workspace.yaml | 仅声明 TS 包路径(packages/、services/classes、bff/、apps/、scripts/),Go/Python 不入 |
| go.work | 列出所有 Go 服务模块(services/api-gateway、services/push-gateway) |
| pyproject.toml | uv workspace members 列 Python 服务(services/data-ana、services/ai-gateway) |
| 跨语言共享类型 | protobuf 生成三端代码(TS/Go/Python),单一契约源 |
| tsx 执行 TS 脚本 | arch-scan 等工具脚本用 tsx 直接运行,无需编译 |
| husky + commitlint | pre-commit 跑 eslint+prettier,commit-msg 校验 Conventional Commits |
| .editorconfig 多语言缩进 | Go 用 tab,Python 用 4 空格,TS/默认用 2 空格 |
| ESLint 9 flat config | P6 硬化:创建 eslint.config.js(flat config),lint 脚本去掉 --ext .ts,lint-staged 恢复 eslint --fix |
| next lint 交互式初始化 | teacher-portal 无 .eslintrc.json 时 next lint 触发 Strict/Base 选择提示,CI 中需预置配置或改 eslint src |
1.2 Docker Compose 基础设施
| 场景 | 技术/规则 |
|---|---|
| 日常开发启动 | 用 docker-compose.minimal.yml 仅起 MySQL+Redis |
| 全量启动内存不足 | 按 profiles 分阶段启用(full/kafka/cdc/analytics/graph/search/config/observability) |
| 每服务 mem_limit | 避免单服务吃满内存:MySQL 512m、Redis 128m、Kafka 512m、ClickHouse 1g |
| 按阶段启用容器 | P1 仅 MySQL+Redis;P3 加 Kafka+Zookeeper;P4 加 Debezium+CH+Neo4j;P5 加 ES;P6 加 Consul+Istio |
| MySQL 初始化 | init-sql/01-init.sql 挂载到 /docker-entrypoint-initdb.d:ro |
| healthcheck | MySQL 用 mysqladmin ping,Redis 用 redis-cli ping |
| Windows 下卷挂载 | init-sql 用绝对路径或确保相对路径正确 |
| 容器名固定 | container_name: edu-mysql 便于服务连接配置 |
| Kafka 双 listener | INSIDE (kafka:29092) 容器间互访 + OUTSIDE (localhost:9092) 主机访问,避免 Debezium 拿到 localhost metadata 后切回连不上 |
| ClickHouse 远程访问 | 默认 default-user.xml 限制 127.0.0.1/::1 无密码,挂载 clickhouse/users.d/custom-users.xml 覆盖密码+任意 IP |
| Debezium Connect 镜像源 | daocloud 禁用 debezium/*,用 quay.io/debezium/connect:2.7 替代 |
| Debezium 跨网络访问 MySQL | MySQL 容器在 edu-minimal_default 时,docker network connect edu-full_default edu-mysql 让 Debezium 同时可达 |
| CDC 注册 connector | POST :8083/connectors,配置 topic.prefix/database.include.list/schema.history.internal.kafka.topic |
1.3 protobuf + buf 契约
| 场景 | 技术/规则 |
|---|---|
| 契约唯一源 | packages/shared-proto/proto/*.proto,禁止 REST/gRPC 混用 |
| breaking change 检测 | buf breaking --against '.git#branch=main' CI 强制,必须升版本号 |
| proto 版本化 | 包名带版本 xxx.v1、xxx.v2,新旧版本共存 |
| buf lint | buf lint 使用 STANDARD 规则集,可按需 except(如 PACKAGE_VERSION_SUFFIX) |
| 三端代码生成 | buf.gen.yaml 配置 TS/Go/Python 插件,pnpm proto:gen 统一生成 |
| 契约先行硬约束 | 任何服务间通信必须先改 proto,CI 检测 breaking change 阻止违规合并 |
| 事件 schema 版本化 | Kafka 事件带 schema_version 字段,消费端按版本处理 |
| 错误码定义在 proto | 错误码三端共享,格式 {服务前缀}_{错误类型}_{资源}(如 IAM_PERM_USER) |
1.4 NestJS 服务开发(TS)
| 场景 | 技术/规则 |
|---|---|
| 服务标准结构 | src/{main.ts,app.module.ts,config,middleware,<domain>,shared,generated} 黄金模板复制 |
| 三层配置 | env(环境变量)< yaml(业务参数)< 配置中心(P6 Consul);每服务启动时合并 |
| 环境变量校验 | Zod schema 校验 process.env,失败立即抛错终止启动 |
| ApplicationError 基类 | 统一错误体系:ValidationError(400)/NotFoundError(404)/PermissionDeniedError(403)/ConflictError(409) 等 |
| 响应信封 | { success: boolean, data?: T, error?: { code, message, details, traceId } } 统一跨服务 |
| 权限校验 | requirePermission(perm) 装饰器/中间件,admin 角色拥有全部权限 |
| DataScope 过滤 | 6 级(all/grade_managed/class_taught/class_members/children/owned),repository 下推 WHERE 条件 |
| 结构化日志 | pino + traceId/spanId/userId/service 字段,禁止 console.* |
| Metrics endpoint | prom-client 暴露 /metrics(QPS/延迟/错误率) |
| OpenTelemetry trace | OTel SDK 初始化,W3C TraceContext header 跨服务传播 |
| 健康检查 | /health endpoint 返回 { status, service, version } |
| 优雅关闭 | SIGTERM → app.close() → shutdownTracing() → process.exit(0) |
| i18n 错误码映射 | 错误码 → i18n key 映射表,后端返回 key + 参数,前端翻译 |
| Drizzle ORM | 参数化查询,禁止字符串拼接 SQL |
| Zod 输入校验 | controller 层 schema.safeParse(body),失败抛 ValidationError |
1.5 Go Gateway 开发
| 场景 | 技术/规则 |
|---|---|
| 框架 | Gin + httputil.ReverseProxy 路由转发 |
| P1 鉴权 | Gateway 内置 HS256 JWT 校验(测试密钥),P2 改 RS256(IAM 签发,公钥校验) |
| JWT claims | { user_id, roles[], registered_claims },校验后注入 x-user-id/x-user-roles 头转发 |
| 请求 ID 注入 | Gateway 生成或透传 X-Request-ID,全链路传递 |
| 健康检查 | GET /health 无需鉴权,返回 { status, timestamp } |
| 包结构 | internal/{config,middleware,proxy},P6 扩展 handler/service/repository |
| error 处理 | 显式处理,禁止 _ 忽略;gin.AbortWithStatusJSON 统一错误响应 |
| 配置加载 | 环境变量 + 默认值(getEnv(key, fallback)),P6 引入 viper |
| Dockerfile 多阶段 | golang:1.22-alpine 构建 → alpine:3.20 运行,CGO_ENABLED=0 |
1.6 可观测性(OTel + Prometheus + Loki)
| 场景 | 技术/规则 |
|---|---|
| P1 最小可观测集 | 每服务结构化日志 + /metrics + OTel SDK 初始化(不引入完整后端) |
| 三支柱 | Logs(pino/winston/zap)+ Metrics(prom-client)+ Traces(OTel SDK) |
| traceId 注入 | Gateway 注入 → 服务读取 header → 日志/响应携带 |
| P6 完整后端 | Loki(日志)+ Grafana(仪表盘)+ Jaeger(trace)+ Prometheus(metrics) |
| Prometheus 指标 | http_request_duration_seconds(Histogram)+ http_requests_total(Counter) |
| 采样策略 | P1 全量 trace,P6 引入采样率降低开销 |
| 日志参数顺序 | log.error({ err: error, userId, traceId }, "操作描述"),错误对象字段名用 err |
| P6 /metrics 端点 | NestJS 用 app.getHttpAdapter().get('/metrics', handler),不能用 app.get()(会被解析为 DI 容器 get) |
| P6 prometheus.yml | 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身;rule_files 引用 rules.yml;alerting 关联 alertmanager |
| P6 Grafana 数据源 | provisioning/datasources 同时声明 Prometheus(默认)和 Loki(uid=loki) |
| P6 Promtail 采集 | docker_sd_configs + relabel_configs 仅采集 edu-* 前缀容器日志,避免无关日志 |
| P6 service label | static_configs.labels.service 给所有抓取目标打服务标签,告警规则按 service 聚合 |
| P6 collectDefaultMetrics | prom-client 的 collectDefaultMetrics({ register }) 自动收集进程级指标(CPU/内存/事件循环/GC),无需业务埋点即可让 /metrics 有数据 |
| P6 Counter/Histogram 埋点缺失 | metrics.ts 定义了 Counter/Histogram 但 service/controller 未调用 .inc()/.observe(),需后续补 HTTP 中间件自动埋点或业务埋点 |
| P6 OTel instrumentations 缺失 | tracer.ts 只配置 traceExporter 未注册 auto-instrumentations(HttpInstrumentation/ExpressInstrumentation),导致 Jaeger 收不到业务 trace,需后续补 @opentelemetry/auto-instrumentations。已补全:NestJS 6 服务用 getNodeAutoInstrumentations();Python 2 服务用 FastAPIInstrumentor.instrument_app(app);Go 2 服务用 otelgin.Middleware() |
| P6 镜像源配置 | 国内 docker.io 被墙,compose image 必须加 docker.m.daocloud.io/ 前缀;Elastic 官方镜像在 docker.elastic.co 不被墙 |
| P6 Grafana 端口冲突 | Grafana 默认 3000 与 teacher-portal Next.js dev 冲突,改映射为 3030:3000 |
| P6 compose --no-deps | mysql/redis 已在另一 compose 项目运行时,启动新服务用 --no-deps + 显式指定服务名,避免重建依赖容器 |
1.7 微前端 Module Federation
| 场景 | 技术/规则 |
|---|---|
| 4 个稳定 portal | teacher-portal / student-portal / parent-app / admin-console,按场景域划分 |
| P1 测试页 | teacher-portal 仅一个测试页验证 classes CRUD 链路,P2 起配 Module Federation |
| 视口驱动渲染 | 侧边栏由 viewports.L1 驱动渲染,路由由 viewports.L2 控制 |
| 共享翻译资源包 | next-intl 微前端共享,BFF/服务返回 i18n key 不返回翻译文本 |
| Module Federation 配置 | P2 起在 next.config.js 配置,P1 用单一 Next.js 应用 |
1.8 从旧项目迁移的通用经验
| 场景 | 技术/规则 |
|---|---|
| React 19 乐观更新 | useOptimistic 替代手动 isPending,配合 useTransition 自动管理回滚 |
| Zustand 细粒度选择器 | 单字段 selector 优于 useShallow 多字段包装 |
| Tiptap SSR | 必须 immediatelyRender: false 避免 hydration mismatch |
| 请求级去重 | React cache() 包装优于跨请求缓存(权限数据易变,不用 unstable_cache) |
| 批量 SQL | INSERT batch db.insert().values([...]) + UPDATE CASE WHEN 单次执行,禁止循环单条 |
| 动态导入重 UI 库 | xxx-inner.tsx + xxx.tsx lazy wrapper(tiptap / @xyflow/react / AI SDK) |
| ReactFlowProvider | 留在外层同步渲染,保证 lazy 子组件 hook 可用 |
| 递归删除 | 批量收集后代 ID + inArray 单次删除,禁止递归逐个 |
| 统计走 SQL 聚合 | COUNT(*)/SUM(CASE WHEN ...) 替代拉全表内存循环 |
| 高频子组件 React.memo | AssignmentCard / StatusBadge / EmptyState 等 |
| 虚拟化大表 | @tanstack/react-virtual |
| 破坏性操作独立权限 | AUDIT_LOG_PURGE ≠ AUDIT_LOG_READ,沿用旧项目规则 |
| i18n key 命名 | 禁止包含 .,动态 key 必须包含完整嵌套路径(t(\type.${type}`)`) |
| i18n 翻译文件对称性 | zh-CN 与 en 必须同步新增/删除 key |
| 类型守卫优先 | JSON.parse 结果经 unknown 中转 + 守卫;禁止 as 断言(除非从 unknown 转换) |
| 函数返回值显式标注 | 特别是 Promise<T> |
| 仅类型导入 | import type |
1.9 架构工具与验证命令
| 场景 | 命令/规则 |
|---|---|
| arch.db 扫描 | pnpm run arch:scan(多语言:TS+Go+Python+Proto) |
| arch.db 查询 | pnpm run arch:query -- <command> |
| 查服务依赖 | pnpm run arch:query -- deps <module> |
| 查 proto 契约 | pnpm run arch:query -- sql "SELECT * FROM contracts" |
| 查 Kafka 事件 | pnpm run arch:query -- sql "SELECT * FROM events" |
| 查架构违规 | pnpm run arch:query -- violations(骨架,P1 后期补全) |
| proto lint | cd packages/shared-proto && pnpm exec buf lint |
| proto breaking 检测 | pnpm exec buf breaking --against '.git#branch=main' |
| proto 代码生成 | pnpm proto:gen |
| Go 编译验证 | cd services/api-gateway && go build ./... && go vet ./... |
| TS 类型检查 | pnpm -r exec tsc --noEmit |
| 全量 lint | pnpm lint |
| 全量测试 | pnpm test |
| 启动最小基础设施 | docker compose -f infra/docker-compose.minimal.yml up -d |
二、模块经验
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 |
| 路由注册位置 | 真实路由在 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 黄金模板)
| 场景 | 技术/规则 |
|---|---|
| 黄金模板定位 | P1 完整实现所有横切关注点,后续 8 个服务复制此模板 |
| 黄金模板复制流程 | cp -r services/classes services/xxx → 改错误码前缀 → 改 proto → 改业务逻辑 → 改 README → 改 CI |
| 横切关注点清单 | 错误处理 / 可观测 / 安全 / 契约 / 测试 / 文档 / 配置 / i18n / CI / Dockerfile |
| 错误处理 | ApplicationError 基类 + 子类,错误码 CLASSES_* 前缀 |
| 可观测 | pino logger + prom-client metrics + OTel tracer |
| 安全 | auth.middleware(信任 Gateway 头)+ permission.guard + data-scope.interceptor |
| 配置 | 三层配置 + Zod 校验 env |
| i18n | ERROR_CODES 映射表(错误码 → i18n key) |
| 测试四类 | 单元(vitest)+ 集成(Testcontainers)+ 契约(Pact)+ E2E(Playwright) |
| 覆盖率门槛 | 领域逻辑 ≥ 80%,Handler ≥ 60%,整体 ≥ 60% |
| Drizzle schema | mysqlTable + varchar/timestamp + index |
| ID 生成 | @paralleldrive/cuid2 的 createId() |
| 响应转换 | repository 返回 Date,service 转换为 createdAt: number(时间戳) |
| 阶段特有模式回写 | Outbox(P3)/ CDC(P4)/ 长连接(P5)实现后回写黄金模板 README |
| 健康检查依赖 | readyz 用 Drizzle getDb().execute(sql\SELECT 1`)` 校验,不要依赖 typeorm DataSource DI |
| AppModule 注册 | HealthModule 必须在 app.module.ts imports 数组显式声明,否则 NestFactory 不扫描 HealthController |
| 增量编译陷阱 | tsconfig.json 显式 "incremental": false 覆盖 base,避免 .tsbuildinfo 导致 nest watch 不 emit |
2.3 iam(TS/NestJS,P2)
| 场景 | 技术/规则 |
|---|---|
| 认证 | 登录/登出/JWT/2FA,RS256 非对称签名 |
| RBAC | 角色/权限/角色-权限 CRUD + getEffectivePermissions(userId) API |
| 视口配置 | 4 层模型(导航/路由/组件/数据),role_viewports 表 |
| DataScope 解析 | 6 级数据范围,注入 JWT payload |
| JWT payload | { userId, roles, permissions(bitmap), dataScope, exp } |
| Token TTL | access 15min / refresh 7day,refresh 用 Redis 黑名单失效 |
| 权限缓存 | getEffectivePermissions 结果 Redis 缓存 TTL 5 分钟,角色变更主动失效 |
| schema 表 | users / roles / permissions / role_permissions / role_viewports / parent_student_relations / class_subject_teachers |
| ESM 模式 DI | providers: [IamService, IamRepository] + 构造器 @Inject(IamRepository) 显式注入,避免 undefined 运行时错误 |
| Drizzle ORM API | inArray(col, vals) 替代不存在的 .in();select 返回字段名按 schema 定义(如 r.iam_roles 而非 r.roles) |
| 健康检查依赖 | readyz 用 db.execute(sql\SELECT 1`)` 校验连接,不要依赖 typeorm DataSource(IAM 用 Drizzle,无 typeorm) |
| Gateway 身份传递 | Controller 直接读 req.headers['x-user-id'] / x-user-roles,不要依赖未注册的 AuthMiddleware 的 AuthenticatedRequest |
| DEV_MODE 登录 | DEV_MODE=true 时 Gateway 接受 Bearer dev-token 注入固定身份,IAM 仍支持真实 JWT(HS256,P2 应改 RS256) |
| P2 公开路径白名单 | Gateway publicPaths map 含 /iam/register//iam/login//iam/refresh,AuthMiddleware 跳过鉴权避免死锁 |
| P2 视口过滤 | getUserViewports 按 requiredPermission 过滤 + sortOrder 字典序排序,无权限要求的视口全员可见 |
| P2 JWT payload | HS256 签名含 sub/email/roles/dataScope/type,register 自动分配 teacher 角色(TEACHER_ROLE_ID 固定 UUID) |
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 |
| datetime 列 | Drizzle datetime 列需 Date 对象,HTTP 请求体里是 ISO 字符串,service 层必须 new Date(input) 转换否则报 toISOString is not a function |
| Kafka 非阻塞 | 开发环境 Kafka 未启动时 connectKafka() 必须 try/catch 且 main 中用 void 调用,否则阻塞服务启动 |
| 相对 import | src/<domain>/ 下文件回溯一级用 ../,不要用 ../../(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)
| 场景 | 技术/规则 |
|---|---|
| 知识图谱 | 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)
| 场景 | 技术/规则 |
|---|---|
| 学情诊断 | ClickHouse 宽表查询,5s 内返回 |
| CDC 链路 | Debezium 监听 MySQL binlog → Kafka(edu-cdc.next_edu_cloud.<table>)→ DataAna 消费写 ClickHouse |
| CDC 延迟监控 | Debezium 暴露 lag metrics,超阈值告警 |
| 双轨读策略 | 实时查 MySQL 主库(刚提交的成绩),聚合查 CH 宽表(延迟 1-5s 可接受) |
| 幂等消费 | 所有事件消费者必须幂等(基于 event_id 去重) |
| CDC 消费者实现 | cdc_consumer.py 用 aiokafka AIOKafkaConsumer,lifespan 启动 asyncio.create_task 后台运行 |
| ClickHouse 写入 | clickhouse_client.upsert_student_dashboard() 用 client.insert() 写宽表,client 为 None 时降级返回 False |
| Debezium 事件解析 | before/after/source/op/ts_ms 五字段,op=r(快照)/c(新增)/u(更新)/d(删除) |
| 多表关联缓存 | 内存 ExamCache 缓存 exam_id→class_id 映射(来自 core_edu_exams CDC 事件),grades 事件触发时查缓存填充宽表 class_id |
| Consumer offset 重置 | kafka-consumer-groups --reset-offsets --to-earliest --execute 需先停消费者让 group 处于 Empty 状态 |
| structlog API | 24.x 用 make_filtering_bound_logger(level),旧版 make_filtering_logger 已废弃 |
2.7 messaging(TS/NestJS,P5)
| 场景 | 技术/规则 |
|---|---|
| 消息 CRUD | 会话/消息 + 调 Push Gateway 推送 + 通知偏好 |
| 通知批量化 | createBatch(items) 单次 INSERT,沿用旧项目 dispatcher 模式 |
| 多渠道 | 站内/SMS/邮件/微信,in_app 批量 + 其他渠道并行 |
| fan-out 分页 | listByUserWithPagination(userId, page, pageSize) 分页查询 |
| ES 降级 | ES_URL 未设置时 esClient=null,safeIndex/safeSearch 跳过返回空结果 |
| Push 推送降级 | PUSH_GATEWAY_URL 未设置或连接失败时 try/catch 跳过,不影响 DB 写入 |
| db 常量导出 | database.ts 导出 db 常量替代 getDb() 函数 |
2.8 push-gateway(Go,P5)
| 场景 | 技术/规则 |
|---|---|
| WebSocket 长连接 | 单节点支撑 10w+ 连接,业务服务只需调 /internal/push |
| 跨实例同步 | Redis PubSub(RedisURL 配置,预留 P6 实现) |
| 离线消息 | 仅推在线用户,离线消息存 MySQL,上线时拉取 |
| 并发写修复 | send chan + 单写协程模式,避免 gorilla/websocket 并发写竞争 |
| DEV_MODE 鉴权 | DEV_MODE=true 时接受 dev-token,生产环境必须 JWT 校验 |
| 广播端点 | POST /internal/broadcast,body {event, data},调用 hub.Broadcast |
2.9 ai-gateway(Python/FastAPI,P5)
| 场景 | 技术/规则 |
|---|---|
| LLM Provider 适配 | OpenAI 兼容 REST API(httpx 异步),不引入 openai SDK |
| 降级模式 | API key 为空或调用失败时返回骨架响应,标记 degraded: true |
| 流式 SSE | AI 网关 → BFF → 前端三层透传,BFF 不缓冲 |
| 路由前缀 | 业务路由加 /ai 前缀(APIRouter prefix="/ai"),Gateway 代理 /ai |
| dev_mode tracer | dev_mode=true 时跳过 OTel exporter 初始化 |
2.10 shared-proto(契约包)
| 场景 | 技术/规则 |
|---|---|
| 目录结构 | proto/*.proto + buf.yaml + buf.gen.yaml |
| P1 契约 | 仅 classes.proto,iam.proto/core_edu.proto 占位 |
| 代码生成输出 | TS → shared-ts/generated,Go → shared-go/,Python → services/*/generated/ |
2.11 arch-scan(多语言扫描器)
| 场景 | 技术/规则 |
|---|---|
| TS 扫描 | regex 提取(function/class/interface/UPPER_CASE const),避免 ts-morph 对未安装依赖文件解析失败 |
| Go 扫描 | 正则提取(行首锚定 ^func/^type),P2 起替换为 tree-sitter-go AST |
| Python 扫描 | 正则提取(行首锚定 ^def/^class),P4 起替换为 tree-sitter-python AST |
| arch.db schema | modules/symbols/dependencies/contracts/events/violations 六表 |
| 全量扫描 | 先清空旧数据再扫描,避免残留 |
| 并行扫描风险 | 并行子代理执行 arch:scan 可能因竞争报 FOREIGN KEY 错误,必须串行执行 |
| 扫描后验证 | npm run arch:query -- violations 输出 0 为合格 |
| Windows 路径 | 自定义 ESLint 规则加载用 path.join,不用 path.posix.join |
2.12 teacher-portal(微前端宿主,P1 测试页)
| 场景 | 技术/规则 |
|---|---|
| P1 测试页 | 单一 Next.js 应用,验证 classes CRUD 端到端链路 |
| API 调用 | fetch(${API_BASE}/api/v1/classes) + Authorization: Bearer ${TEST_JWT} |
| P1 测试 JWT | 开发工具生成 HS256 token,P2 起由 IAM 签发 RS256 |
| P2 Module Federation | next.config.js 配置,按场景域分 4 个稳定 portal |
| P2 路由组 + AppShell | app/(app)/layout.tsx 用 AppShell 包裹受保护页;/login 与 / 不套壳 |
| P2 真实 JWT | 登录后 token 存 localStorage,authHeaders() 读 Bearer ${getToken()} |
| P2 视口驱动侧边栏 | AppShell fetch /teacher/viewports 渲染左侧导航,active 路由高亮 |
| P2 根路径重定向 | app/page.tsx 客户端组件 router.replace(isAuthenticated() ? '/dashboard' : '/login') |
| fetch headers 类型 | authHeaders(): Record<string, string> 显式标注,避免 {} 与 HeadersInit 不兼容 |
三、工作经验日志
按时间倒序,50 条上限。AI 发现更好方案时可更新本节。
| 日期 | 时间 | 模块 | 做了什么 + 学到什么 |
|---|---|---|---|
| 2026-07-09 | 下午 | classes/全局 | 一键启动脚本 NestJS dist/ 不生成根因定位 + classes 健康检查修复:(1) 根因定位:tsconfig.base.json 的 incremental: true + nest-cli.json 的 deleteOutDir: true 冲突。nest start --watch 启动时先删除 dist/,tsc 读残留 .tsbuildinfo 认为无变化跳过 emit,dist/ 不生成 → Cannot find module dist/main。(2) 修复:6 个 NestJS 服务(classes/iam/teacher-bff/core-edu/content/msg)tsconfig.json 显式加 "incremental": false 覆盖 base 配置,删除所有残留 .tsbuildinfo 文件。(3) classes AppModule 缺 HealthModule 导入导致 /healthz 404,iam 同样问题,修复 app.module.ts 加 imports: [..., HealthModule]。(4) classes HealthController 误用 TypeORM DataSource DI(与 iam 不一致),运行时报 Nest can't resolve dependencies of the HealthController (DataSource)。修复:改为 Drizzle getDb() 函数式调用,与 iam 一致。(5) 一键启动验证:11/11 应用 + 11/11 基础设施 + 5/5 可观测性端点全绿。学到:NestJS + TypeScript incremental 编译是陷阱组合——nest-cli deleteOutDir 删 dist 但 tsc 读 tsbuildinfo 认为无变化,必须在服务级 tsconfig 显式 incremental: false;HealthModule 必须在 AppModule imports 中显式声明才能被 NestFactory 扫描到;5 个 NestJS 服务的 HealthController 应统一用 Drizzle getDb() 函数式调用而非 TypeORM DataSource DI(项目已弃 TypeORM 改 Drizzle)。 |
| 2026-07-09 | 下午 | 全局 | OTel auto-instrumentations 全服务补全:(1) NestJS 6 服务(iam/classes/core-edu/content/msg/teacher-bff)tracer.ts 补 getNodeAutoInstrumentations(),NodeSDK 传 instrumentations 参数自动埋点 HTTP/Express/DB。(2) Python 2 服务(data-ana/ai)main.py 补 FastAPIInstrumentor.instrument_app(app);ai 补缺失的 opentelemetry-exporter-otlp 依赖。(3) teacher-bff 从零补完整 OTel:env.ts 加 OTEL_EXPORTER_OTLP_ENDPOINT 字段 + 新建 shared/observability/tracer.ts + main.ts 调用 initTracer/shutdownTracer + package.json 加 sdk-node/exporter/auto-instrumentations 依赖。(4) Go 2 服务(api-gateway/push-gateway)新建 internal/observability/tracer.go(OTLP HTTP exporter + resource + TracerProvider + W3C propagator)+ main.go 调用 InitTracer + otelgin.Middleware 注册 Gin 中间件;push-gateway config.go 补 OTLPEndpoint 字段。(5) 质量校验全通过:TS typecheck 9 服务 + ESLint 6 服务 + ruff 2 服务 + go vet/build 2 服务零错误。学到:getNodeAutoInstrumentations() 一次注册所有 Node.js 自动埋点(http/express/dns/fs/net/grpc 等),比手动逐个注册 HttpInstrumentation 更简洁;Go OTel 用 otlptracehttp.WithEndpoint(host) + WithInsecure() 需从 "http://host:port" URL 解析出 host;otelgin.Middleware 必须在 Recovery 之后其他中间件之前注册,确保所有后续 handler 都被 trace;Python FastAPIInstrumentor.instrument_app(app) 在 app 创建后立即调用,lifespan 不受影响。 |
| 2026-07-09 | 下午 | 全局 | P6 硬化:可观测性 + 部署 + CI 硬化:(1) 可观测性栈完善:5 个 NestJS 服务 main.ts 添加 /metrics Prometheus 端点(用 app.getHttpAdapter().get('/metrics', ...) 绕过 DI 容器 get 方法);prometheus.yml 从 2 个目标扩展到 8 个应用服务 + MySQL/Redis + node-exporter + prometheus 自身 + rule_files + alertmanager 关联;monitoring compose 用 Loki + Promtail 替换未配置的 blackbox-exporter;Grafana datasource 新增 Loki;新建 promtail/config.yml 用 docker_sd_configs 仅采集 edu-* 容器日志。(2) 部署 compose 扩展:docker-compose.deploy.yml 从 3 服务扩展到 11 服务(+ iam/teacher-bff/core-edu/content/msg/ai/data-ana/push-gateway),每个服务带 healthcheck + depends_on 条件 + edu-net/edu-shared 双网络;deploy.env.example 补全 Neo4j/ES/ClickHouse/LLM/Kafka 可选依赖配置。(3) teacher-bff 补 health.controller.ts(原缺失 /healthz 导致 deploy depends_on service_healthy 失败)。(4) CI 硬化:移除 lint 步骤的 continue-on-error(ESLint 9 flat config 已配置完成),test 保留 continue-on-error(部分服务无 test 脚本)。学到:NestJS app.get('/metrics') 会被解析为 DI 容器 get(typeOrToken),必须用 app.getHttpAdapter().get() 才能注册 Express 路由;Promtail docker_sd_configs 通过 relabel_configs 的 regex: '/(edu-.*).*' 过滤容器名前缀;docker-compose.depends_on.condition: service_healthy 要求被依赖服务必须有 healthcheck 配置,否则启动失败。 |
| 2026-07-09 | 下午 | data-ana/infra | CDC 完整链路实现:MySQL binlog → Debezium Connect → Kafka → data-ana 消费者 → ClickHouse 宽表。(1) MySQL binlog 配置:log_bin=ON, binlog_format=ROW, binlog_row_image=FULL, server_id=1;用 root 创建 debezium 用户授予 REPLICATION SLAVE + REPLICATION CLIENT。(2) Debezium Connect 容器:daocloud 禁用 debezium 镜像改用 quay.io/debezium/connect:2.7;MySQL 容器在 edu-minimal_default 网络,需 docker network connect edu-full_default edu-mysql 让 Debezium 同时可达;Kafka 必须配置双 listener(INSIDE:kafka:29092 + OUTSIDE:localhost:9092),否则 Debezium 拿到 advertised.listeners 中的 localhost metadata 后切换失败;Debezium 2.x 容器环境变量名用 BOOTSTRAP_SERVERS(不带 KAFKA_ 前缀),通过 envsubst 替换到 connect-distributed.properties。(3) 注册 connector:POST :8083/connectors,配置 topic.prefix=edu-cdc, database.include.list=next_edu_cloud, snapshot.mode=initial,4 张表(core_edu_grades/exams/classes/iam_users)成功产生快照事件。(4) data-ana 消费者实现:新建 cdc_consumer.py 用 aiokafka AIOKafkaConsumer,lifespan 中 asyncio.create_task 后台运行;按 source.table 路由(exams→内存缓存 exam_id→class_id 映射,grades→查缓存填 class_id 后 upsert ClickHouse);readyz 端点附加 cdc_consumer 状态。(5) ClickHouse 远程访问:默认 default-user.xml 限制 127.0.0.1/::1 无密码,挂载 clickhouse/users.d/custom-users.xml 覆盖密码+任意 IP。(6) structlog 24.x API:make_filtering_bound_logger(level) 替代废弃的 make_filtering_logger。(7) E2E 验证:MySQL INSERT 成绩 → Debezium op=c 事件 → Kafka → 消费者写 ClickHouse 宽表(class_id 通过 exam 缓存正确填充)→ /readyz cdc_consumer=running → /analytics/student/student-002/weakness 返回实时 92 分数据。学到:Debezium 2.x 容器 bootstrap.servers 默认值是 0.0.0.0:9092 必须显式覆盖;Kafka 单 listener 配置 localhost 会让容器间通信的客户端拿到 metadata 后切换失败,必须用双 listener;ClickHouse users_xml 存储是 readonly 不能用 ALTER USER 修改密码,必须挂载 users.d 配置文件覆盖;消费者 offset 重置必须先停消费者让 group 处于 Empty 状态才能执行 --reset-offsets。 |
| 2026-07-09 | 下午 | 全局 | P6 硬化:ESLint 9 flat config 配置:(1) 根目录创建 eslint.config.js(ESLint 9 flat config 格式):用 typescript-eslint recommended 规则集 + @eslint/js recommended + eslint-config-prettier 禁用冲突规则;自定义规则:no-explicit-any warn + no-unused-vars 允许下划线前缀 + 测试文件放宽。(2) 6 个 TS 服务 package.json lint 脚本从 eslint src --ext .ts 改为 eslint src(flat config 不需要 --ext)。(3) lint-staged.config.js 恢复 eslint --fix。(4) 验证:classes/content/msg/core-edu 四服务 lint 全部零错误零警告通过。学到:ESLint 9 flat config 用 tseslint.config() 工厂函数组装配置数组;--ext 参数在 flat config 模式下被移除,ESLint 自动根据 eslint.config.js 中的 files 匹配;@typescript-eslint/consistent-type-assertions 规则选项格式在 v8 中变化(objectLiteralType → objectLiteralTypeAssertions),配置时需查最新文档。 |
| 2026-07-09 | 中午 | msg/push-gateway/ai/api-gateway | P5 沟通与 AI 阶段三服务完善:(1) msg 服务修复:database.ts 导出 db 常量;env.ts JWT_SECRET/ES_URL 改 optional 加 DEV_MODE/PUSH_GATEWAY_URL;elasticsearch.ts ES 降级(esClient=null 时 safeIndex/safeSearch 跳过);notifications.service.ts 加 createBatch + listByUserWithPagination + Push Gateway 推送调用(try/catch 降级);新建 msg-init.sql 2 张表。(2) push-gateway 完善:hub.go 重写用 send chan + 单写协程模式修复 gorilla/websocket 并发写竞争;handler.go 加 DEV_MODE dev-token 支持 + broadcast 端点;config.go 加 DevMode/RedisURL。(3) ai 服务完善:config.py 加 openai_api_key/base_url/dev_mode;新建 llm_client.py(httpx 异步调 OpenAI REST API);main.py 加 /ai 前缀 + 降级模式(无 key 返回骨架 + degraded: true)+ /readyz 端点。(4) Gateway 路由扩展:/notifications → msg,/ai → ai 服务。学到:gorilla/websocket 不支持并发写,必须用 send chan 串行化所有写入;FastAPI APIRouter prefix 与 Gateway 代理路径要协调(ai 服务加 /ai 前缀,Gateway 代理 /ai/*path);LLM 降级策略统一返回 degraded 标记,调用方据此判断是否路由流量。 |
| 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<string,string> 避免 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。 |
| 2026-07-08 | 下午 | api-gateway | 重定向循环修复 + 生产模式部署准备 + 多AI协作文档:(1) 修复 ERR_TOO_MANY_REDIRECTS:Gin 默认 RedirectTrailingSlash=true 导致 /api/v1/classes → 301 → /classes/,Next.js rewrites 代理时形成循环。修复:r.RedirectTrailingSlash=false + 同时注册无尾斜杠路由(/classes)与通配符路由(/classes/*path)。(2) 新增 DEV_MODE 旁路:config.go 加 DevMode 字段,auth.go 在 DEV_MODE=true 时接受 dev-token 注入固定身份(生产必须 false)。(3) 生产 Docker 化:新建 apps/teacher-portal/Dockerfile(多阶段 Next.js build)+ services/api-gateway/Dockerfile(多阶段 Go 静态编译)+ infra/docker-compose.prod.yml(三服务编排,强制 DEV_MODE=false)。(4) 编写 docs/standards/local-dev-runbook.md(本地启动手册,含端口表/开发模式/生产模式/常见问题)+ docs/standards/multi-ai-collaboration.md(多AI协作文档,含模块分工矩阵/分支命名/PR流程/合并策略/冲突处理/权限矩阵)。学到:Gin RedirectTrailingSlash=false 后需显式注册无尾斜杠路由(Any("/classes") + Any("/classes/*path")),否则 404;Next.js rewrites 代理会透传 301 给浏览器形成循环,开发模式旁路应通过环境变量控制而非硬编码。 |
| 2026-07-08 | 全天 | 全局 | P6 后续工作手册执行:完整执行 post-p6-followup.md 12 节任务。环境准备(pnpm 925 包 + go mod tidy 双服务 + uv sync 双服务 + buf 安装)→ 代码质量校验(Go vet/build 0 错误,Python ruff 8 错误自动修复)→ arch.db 同步(实现 4 个扫描器骨架,输出 12 模块/233 符号/138 契约)→ project_rules.md P0 修复(迁移到 .trae/rules/,17881 字节)→ 004 架构图修复(1.1a/1.1b 双图 + 1.2 业务领域列 + 5.4 视口四层)→ P6 集成测试(10 Go + 17 bash = 27 用例全通过)→ Helm Chart 演化(8 chart lint 通过)。学到:多语言 monorepo 工具链配置需统一镜像源(npmmirror/goproxy.cn/tuna),go.work BOM 字符会导致 unexpected input character 错误必须重写文件。 |
| 2026-07-08 | 上午 | 全局 | pnpm install 网络失败(ECONNRESET)→ 配置 npm config set registry https://registry.npmmirror.com + pnpm config set registry https://registry.npmmirror.com 重试成功。学到:Windows 下 pnpm 还需配置 PNPM_HOME 和 TMP 环境变量避免 _tmp_ 文件 ENOENT 错误。 |
| 2026-07-08 | 上午 | 全局 | project_rules.md 损坏(72 字节乱码,从 P1 提交 2ba4250 就损坏,git 历史无完整版本)→ 从 CICD 项目完整版迁移到 e:\Desktop\Edu\.trae\rules\project_rules.md(按用户要求放 .trae/rules/),按 MIGRATION_GUIDE 4.1 策略矩阵调整为微服务版(13 章 17881 字节),删除根目录损坏文件,更新 7 处引用(README/MIGRATION_GUIDE/004/known-issues/git-workflow/coding-standards)。学到:迁移文件后必须 `Get-Item |
| 2026-07-08 | 上午 | api-gateway | go.work BOM 字符 + 版本不匹配:unexpected input character '\ufeff' 和 module requires go >= 1.22.0, but go.work lists go 1.22。修复:重写 go.work 去除 BOM,版本改为 go 1.26.0,移除不存在的 ./packages/shared-go。学到:PowerShell Out-File 默认加 BOM,写 go.work 这类敏感文件应用 Write 工具或 [System.IO.File]::WriteAllText 指定 UTF8 无 BOM。 |
| 2026-07-08 | 上午 | arch-scan | arch:scan 返回 0 模块 0 符号 → 4 个扫描器(ts/go/py/proto)都是骨架实现。修复:完整实现 4 个扫描器,TS 用 regex 提取(避免 ts-morph 对未安装依赖文件解析失败),Go/Python 用行首锚定正则,Proto 扫描 service/message/rpc。结果:12 模块(≥10 ✓)、233 符号(≥100 ✓)、138 契约。学到:ts-morph Project 对未 pnpm install 的 workspace 文件会报模块解析失败,改用 regex 更鲁棒;scanner.ts main() 开头需 DELETE FROM 清空旧数据避免重跑重复。 |
| 2026-07-08 | 下午 | 004 | 架构图视角讨论(技术分层 vs 业务领域)→ 双图并存方案:1.1a 技术分层视角(部署/流量/网络边界,Users 层标注"场景域用户",BFF 层标注"按场景域分")+ 1.1b 业务领域视角(6 DDD 限界上下文 subgraph:D1 身份/D2 教学组织/D3 教学核心/D4 内容/D5 沟通/D6 智能洞察)。1.2 服务清单新增"业务领域"列。学到:双图互补,1.1a 服务运维/SRE 视角,1.1b 服务产品/架构视角;同一服务可横跨多领域(core-edu 同时承载 D2+D3)。 |
| 2026-07-08 | 下午 | 004 | 视口四层模型补充(5.4 章节):L1 导航(navigation_config 表)/ L2 路由(route_permission + Gateway 校验)/ L3 组件(usePermission().hasPermission)/ L4 数据(DataScope 枚举)。场景域 BFF 复用策略:按使用场景域分 BFF 而非按角色分,教导主任复用 Teacher BFF + 额外管理视口。iam 服务职责:认证 + RBAC + 视口配置 + DataScope + 权限解析 API。学到:视口既可独立配置(RoleViewport 表)也可由权限推导,新角色只需配权限集,视口自动推导。 |
| 2026-07-08 | 下午 | api-gateway | P6 集成测试补充:circuit-breaker_test.go(5 用例:ClosedToOpen/OpenToHalfOpen/HalfOpenToClosed/HalfOpenToOpen/4xxNotCounted)+ ratelimit_test.go(5 用例:AllowUnderBurst/RejectOverBurst/RefillTokens/PerIPIsolation/CleanupExpiredBuckets)+ test-backup-mysql.sh(8 用例 17 断言)。学到:gobreaker v2 ReadyToTrip 在 1 次失败后就触发(TotalFailures*2 > Requests 当 Requests=1 时 1*2>1=true),HALF_OPEN 状态只在探测执行期间可见,探测完成后立即转 CLOSED 或回 OPEN,测试需通过行为(503 vs 500)而非状态字段验证;rateLimiter cleanup 测试需用短周期参数(50ms/500ms)加速,且新鲜桶要在旧桶清理后再创建避免被一起清掉。 |
| 2026-07-08 | 下午 | infra/k8s | Helm Chart 演化:安装 Helm v4.2.2,创建 edu-platform 平台级 chart(namespace/configmap/secret/ingress/hpa + 4 环境 values 文件)+ api-gateway 服务级 chart(完整迁移自原 deployment.yaml,参数化所有字段)+ 6 业务服务 chart 桩(iam/core-edu/content/msg/data-ana/ai)。删除原 api-gateway-deployment.yaml,保留 namespace.yaml。学到:Helm {{- with ... -}} 双向修剪会导致标签连在一行(managed-by: Helmpart-of: edu-platform),应改为 {{- with ... }} 只修剪左侧;helm lint 全部通过但 helm template 才能发现 YAML 渲染错误,验证时两个都要跑。 |
| 2026-07-07 | 全天 | 全局 | 文档体系初始化:从旧项目(e:\Desktop\CICD,Next.js 单体)迁移 spec + plan + known-issues 模板到新仓库(e:\Desktop\Edu,微服务架构)。known-issues 重组为微服务分区:多语言 monorepo / Docker Compose / protobuf+buf / NestJS / Go Gateway / 可观测性 / 微前端。从旧项目提炼可迁移经验:React 19 useOptimistic / Zustand 细粒度选择器 / Tiptap SSR / 请求级去重 / 批量 SQL / 动态导入模式 / arch:scan 串行执行。新增微服务特有经验:契约先行 / Outbox / CDC / 双轨读 / DataScope / 黄金模板复制流程。路线图按 6 阶段组织:P1 地基 → P2 身份 → P3 核心教学 → P4 内容分析 → P5 沟通AI → P6 硬化。 |